停课复习了,闲暇时可以刷刷pwn,writeup参考和媳妇一起学Pwn 之 3x17
0x01 寻找main函数
这道题是静态链接,很多函数没有直接给出名称,一开始比较明显的只能定位到start。
寻找main函数的话有两种方法:
1.1 查找字符串
我们可以直接按查找字符串,寻找运行程序时出现的字符串,然后看这个字符串在那个函数里被使用过就可以了。
现在找到了addr这个字符串
然后点击后面那个函数就可以进入main函数了
1.2 根据start函数的参数
这个涉及到glibc入口函数的知识,我前面写博客总结过,《程序员的自我修养11章》有比较详细的介绍,总结来说start函数的伪代码如下
1 | void _start() |
由此可以判断出此题start函数的基本结构,这样同样可以锁定main函数。
0x02 分析程序逻辑
先上main函数,这里把一眼能看出来的read和write都已经标好,但是有一个sub_3b9330
好像看不出来是什么东西,而且点进去之后那个代码好复杂,顿时就不想看了。但可以看出来逻辑大概是:
- 读入一个数据
- 把这个数据转化成一个地址
- 然后向这个地址里写内容
所以还是动态调试一下,看看这个看不出功能的函数是什么功能吧
随意输入一个16,然后发现返回值是0x10,看来这个函数是一个把字符串转化成整形的一个函数
程序逻辑就是 一个0x18字节的任意地址写
0x03 漏洞利用
然而这个程序没有泄露栈地址,所以我也不知道返回地址在什么位置,而且这个程序没有后门函数,只能利用rop或者shellcode,所以0x18个字节的写入完全不够。
其实如果是有经验的大佬可以直接想到利用libc_csu_fini
,但是很多时候我们并不清楚这个知识点,难道这题就做不出来了么?(好吧,还真的是做不出来)
但还是要尽量找一下程序里有什么地方是不是可以控制rip,比如下面这部分
1 | .text:0000000000402960 sub_402960 proc near ; DATA XREF: start+F↑o |
3.1 libc_csu_fini
还记的__libc_start_main的几个参数里有两个东西么(init,fini),这俩是个啥呢?
1 | .text:0000000000401A5F mov r8, offset sub_402960 |
这俩其实就是两个函数的地址,分别是:libc_csu_fini(sub_402960),libc_csu_init(loc_4028D0),至于为啥init的被IDA识别成loc,就不知道了。因为是静态编译的,这两个本身是libc的函数,但是可以在这个二进制中直接点进去看到函数的实现。
顾名思义,一个是init,开始时函数。一个是fini,结束时的函数。所以可见main函数的地位并没有我们刚接触c语言是那么至高无上,他既不是程序执行时的第一个函数,也不是最后一个函数。
另外在IDA的 view -> open subviews -> segments可以看到如下四个段:
- .init
- .init_array
- .fini
- .fini_array
并且可以看到init和fini都是函数,而init_arry和fini_arry都是数组,所在空间有可读写权限,他们的执行顺序为:
- __libc_csu_init
- main
- __libc_csu_fini
更精细的执行顺序如下:
- .init
- .init_array[0]
- .init_array[1]
- …
- .init_array[n]
- main
- .fini_array[n]
- …
- .fini_array[1]
- .fini_array[0]
- .fini
所以无论是看汇编还是源码,都能看出来,.fini_array数组中的函数是倒着调用的。题目中的off_4B40F0这个地址,就是.fini_array:
3.2 覆写fini_arry
我们如果把.fini_array[1]覆盖成main,把 .fini_array[0]覆盖成 __libc_csu_fini,这可以样就可以一直循环调用main函数啦!但好像看起来还是无法写多次啊,因为byte_4B9330这个全局变量一直在自增啊,永远比1大呀。观察一下这个变量:
1 | (unsigned __int8)++byte_4B9330 |
这是8bit的整型,从byte_4B9330这个变量名也能看出来(byte),所以当我们按照如上的方法改写.fini_array段,这个变量会疯狂加一,自增一会就溢出了,然后又会回到1,然后就会停到read系统调用等待写入,就又可以写了。
3.2 栈迁移
1 | .text:0000000000402960 push rbp |
可见在这个函数中rbp之前的值暂时被放到栈里了,然后将rbp当做通用寄存器去存放了一个固定的值0x4b40f0,然后就去调用了fini_array的函数,call之后的指令我们就可控了,我们可以劫持RIP到任何地方。考虑如下情况:
1 | lea rbp, off_4B40F0 ; rbp = 0x4b40f0 , rsp = 未知 |
则rsp被劫持到0x4b4100,rip和rbp分别为.fini_array[1]和.fini_array[0]的内容
则我们可以在0x4b4100的地址向上布置rop链,只要rip指向的位置的代码不会破坏高地址栈结构,然后还有个ret指令,那么就可以实现ROP啦。所以我们要完成三件事:
- 布置好从0x4b4100开始的栈空间(利用任意地址写)
- 保证.fini_array[1]指向的代码不破坏栈结构,还有个ret,或者直接就一句ret也行
- 通过上文类似的方法劫持rsp到0x4b4100,即可触发ROP
3.3 exp
1 | from pwn import * |
0x04 总结
这道题设计的很巧妙:
- 最开始时只有一次任意地址写,通过修改.fini_array段,利用__libc_csu_fini函数性质构造循环调用main函数,并溢出检查字段绕,变成多次任意地址写
- 继续利用任意地址写和__libc_csu_fini函数性质,迁移rsp,并劫持rip,完成ROP
这道题给我带来的新思路和收获
- 碰到复杂函数不必急于看代码,可以先动态调试一下,猜测它的功能
- 巩固了程序入口函数的知识
- 寻找利用途径时可以多关注可以控制程序ip的地方
- 可以想办法让一次任意地址读变为多次任意地址读