好难的一关,顺着这关学了好多东西……
Part0 符号名呢
摸索
本题是一个strip后的静态链接文件……
当我打开IDA,我看不到任何一个函数名,只有一大堆地址迎接我。
于是我在libc里耗了一天,成果只是大致知道执行了哪些函数,并且给read、write库函数标了名称。
然后我想了一个方法,我是不是可以根据函数的地址来看出这是哪一个libc版本,然后就可以给每个函数都标上名称了?然而不行。
静态链接不像动态链接,它只把用到了的函数链接进文件,因此库函数的地址和它在库中的位置毫无关系。
然后当天晚上做梦的时候,我梦到真的有这么一个库,我一把库拖进IDA PRO,软件自动给所有的函数都标上了名字。
醒来的时候我一想,会不会真有根据函数特征来识别函数名的功能?拿起枕边手机一查就查到了。(话说你不能早点查吗)
解决
参考利用ida pro的flare功能识别静态链接函数签名_Anciety的博客
IDA支持给特定库生成一个签名,然后用这个签名识别库函数的名称!
有人已经生成过很多签名了,可以直接去push0ebp/sig-database: IDA FLIRT Signature Database (github.com)下载。
那么问题来了,下哪个libc版本呢?
pwnable.tw的官网首页说,题目都运行在ubuntu16.04或18.04上,所以我先去把这两个系统对应的libc都下了下来,发现只识别了五十几个库函数……
然后又下了一大堆libc版本,最后在19.04里找到的libc6_2.28成功匹配到了六百多个库函数。
于是我终于知道哪个是main函数了……然后发现离成功还尚早……
Part1 分析(放弃)
本关开启了NX和Canary,没开PIE,那么应该是可以修改某些东西的。
main函数干了四件事:
- write一个”addr:”
- read一个0x18长度的字符串,并用一个库函数将其转换成数字(当成10进制数)。
- write一个”data:”
- read一个0x18长度的字符串,地址是刚刚输入的数。
然后就ret了。可以发现,我们没有任何泄露栈地址的方法,没办法进行简单的ret2xxx系列攻击。
(然后我就放弃了,这题大概又是超出我知识水平范围的,所以去网上找writeup:和媳妇一起学Pwn 之 3x17 | Clang裁缝店看了)
Part2 main函数的启动过程
参考教程:linux编程之main()函数启动过程_gary_ygl的博客
读了文章,学到很多姿势,尤其是对于C程序的抽象->具象:
从一开始的程序运行过程就是main开始到结束;
到后来知道从start开始,start负责调用__libc_start_main(),__libc_start_main()再调用main()函数;
再到现在发现__libc_start_main()干了很多事情,包括在调用main()函数之前,调用__libc_csu_init()函数,并且用_cxa_atexit()函数设置程序退出前执行__libc_csu_fini()函数(具体来说exit()调用_run_exit_handlers(),并在其中按照倒序调用之前用_cxa_atexit()注册过的函数)。并且在调用main()之后,会调用exit()函数。
(其实还干了一些初始化以及善后工作,但是和链接比较相关,和本题不那么相关)
而逆向本题可以看到,__libc_csu_init()主要做两件事:
- 调用位于.init段中的_init_proc()
- 按顺序调用位于.init_array中的函数(这是一个函数指针数组)(数组大小固定,汇编中直接用立即数地址计算数组大小)
类似地,__libc_csu_fini()也干两件事,但是和init是正好顺序相反的:
- 按逆序调用位于.fini_array中的函数(这是一个函数指针数组)(数组大小固定,汇编中直接用立即数地址计算数组大小)
- 调用位于.fini段中的term_proc()
然后画个图表示一下我的理解:
而.init_array和.fini_array都是rw的,可写!
然后我决定在懂得了这些之后再自己尝试一下利用!
Part3 Exploitation
通过覆写一次fini_array,可以达到如图的效果。
由于不存在wx的段,所以放弃shellcode,想想如何ROP。
光凭fini_array这两个call是没有用的,必须想办法stack pivot一下。
刚开始的思路是利用
1 | 0x00418820: mov rax, qword [0x00000000004B7120] ; ret ; |
这两个gadget来把rsp弄到我想要的地方。但是我发现这做不到,原因是fini_array只有两个元素,我不论怎么修改这个数组,都只能实际调用一个gadget。
原因如下:
我们必须要用一个gadget完成stack pivot,这意味着要么有一个gadget同时涵盖了赋值+修改rsp的工作,要么利用寄存器或栈上已有的值。
GDB动态调试到这里,发现确实有几个寄存器存着RW的位置,其中就包括rbp。然后回忆一下:leave = mov rsp, rbp; pop rbp;
,用这个来stack pivot。
然后利用静态链接程序的丰富gadget库轻松写出了ROP chain,拿到了shell。
1 | from pwn import * |
一个小技巧:
如果不间断地给程序send数据,很可能send到同一个read()里。
面对这种情况,可以在两个send()中间recv()一下,又或者加上一个pause()手动停止,又或者加上一个sleep(0.15)来自动停止。