花了将近一天半的时间研究这一题,因为最近在熟练srop技术,所以在看到pwnbale.kr上的那道unexploitable之后,想趁热打铁来试试这道500p的题目。收货颇丰啊~~~ 虽然看了网上的思路,但exp还是自己搞了出来,还是有点小小成就感。
题目的二进制文件和libc自己去网站上找吧pwnable.tw.
首先拿到题目我们看到没有了syscall,变为了call read,这个变化导致题目直接上了一个档次。使我们的思路陷入死活同:我们需要调用system(’/bin/sh’)的话,需要先泄露libc,但是程序本身没有输出函数,我们又只能通过系统调用来调用write等函数,可是又没有syscall;或者我们需要调用execve(‘/bin/sh’),仍然需要syscall来触发。也就是说没有syscall我们什么都干不了。。。。
- 这个时候就需要独特的思路和脑洞。。。这也是为什么值500p的原因。。。。我们动态调试read函数内部会发现其第三条语句就是syscall,而且read()和syscall的LSB相等!!如果先将read()最后一位改为syscall的,再搭配64位通用gadgets,call [read_got]就等于jmp syscall。我们获得了syscall。
- 之后思路分为:
- 先利用 pop rbp; ret; 和 leave; ret; 来劫持栈到我们选定的bss段(实际上是加载后的空闲段)。
- 然后用64位通用gadgets来改写read()的LSB(这一步过后就没法调用read()了,所以我们得用其他函数来控制rax),之后rax的值为1,正好调用write(),再利用write()出59个字符使rax为59,再调用exevce()(其参数应早一步在bss段布置好) (这种思路最简单,已成功)
- 或者用write()泄露出Libc,然后调用system(),但是这里需要再输入一次,所以我们必须利用write()来控制sleep(),再利用sleep()返回0,来调用read()写入system()真实地址。(这种思路未成功,因为不知道nanosleep()的返回值具体机制,但是似乎没有调用之后read())
思考和收货:
-
是不是所有的系统调用函数内部都是通过syscall实现的。(我猜应该是的)
-
gdb的print fun功能(直接打印函数fun的真实地址)和peda的find ‘‘string’’ addr1 addr2(在addr1和addr2之间找string的位置。注意双引号不能换成单引号!)
-
劫持栈的gadget:p64(pop_rbp)+p64(fake_stack_addr)+p64(leave_addr)
-
64位通用gadgets的熟悉运用,定义函数(大致结构,具体因题而异):
#part1为地址大的,part2为地址小的。 def call_fun(fun_addr,arg1,arg2,arg3): payload = p64(part1) payload+= p64(0) payload+= p64(1) payload+= p64(fun_addr) payload+= p64(arg1) payload+= p64(arg2) payload+= p64(arg3) payload+= p64(part2) payload+= 'a'*0x38 return payload
-
p = process(’./unexploitable’,env={‘LD_PRELOAD’ :’./libc_64.so.6’}) 用或不用LD_PRELOAD会影响动态链接加载的函数真实地址,最好看ida,最后一位不会变的。因为分页分配内存。
-
如果两次read()之间没有recv()的话,需要加一个sleep(),或者raw_input()来暂停一下,不然会有吞字的现象。
-
因为内存的分页分配机制,0x1000为最小单位,所以我们会发现我们的程序实际上只到0x601070(比如),但之后到0x602000都是可写入的空的内存,我们可以任意使用(权限足够的话)。
-
one_gadgets 可以写入一个数组。orz.