A Simple ROP Exploit /bin/sh via syscall
为了用sys_execve syscall执行/bin/sh,需要解决一些障碍,根据参考,需要设置寄存器如下;
EAX = 11 (or 0x0B in hex) – The execve syscall number
EBX = Address in memory of the string “/bin/sh”
ECX = Address of a pointer to the string “/bin/sh”
EDX = Null (可选的指向描述环境的结构的指针)
一旦所有这些都设置好了,执行int 0x80指令应该会生成一个shell。
后面的工作
从想要的开始, 一次性链接一起解决问题, 直到已经完成了利用链, ROP exploitation的本质是解决一个问题常常引入另一个, 所以它重要, 集中和逻辑, 总是寻找不同的路线和替代解决方案。
首先需要的是放置/bin/sh字符串的地方,还需要一种方法将这个字符串写入这个选择的位置,以及一个包含指向这个位置的指针的内存地址。还需要能够向EDX写入空值(这通常是有问题的),并向EAX、EBX和ECX写入任意值。最后,需要一个int 0x80指令。
Gadgets
可以用gadget的二进制, gadget是一个有用的ret指令, ret指令是至关重要的, 因为这是允许链接多个gadgets连接到一个链, 然后执行流, 返回从一个指令。如果exploit是一条链那么每个gadget都是链中的一环。
有许多工具可以简化寻找gadget和构建ROP链的过程,但暂时不考虑这些工具,而是自己动手,以确保对整个过程有一个很好的理解。
虽然正在看的二进制文件相当小,但它包含了很多gadgets,包括一个可疑的密集指令区域,看起来就像是故意构建的。
这部分内存包含了需要的所有东西——从将数据从堆栈中弹出到EAX、EBX、ECX和EDX的gadget开始。如果控制了堆栈,那么就可以设置这些寄存器的值。
例如,从0x08048225开始的指令是pop %eax; ret,这个gadget获取堆栈顶部的任意4个字节,并将它们放入EAX寄存器,对堆栈指针加1,然后返回到下一个地址。为了使用它,将以下内容放入链中;
1 | [0x08048225][<Value to put into EAX>][<Next Gadget>] ... |
还有gadget xor %edx,%edx; ret — 如果不能在漏洞中发送空字节,这将非常方便,因为已经知道需要将EDX设置为0。对寄存器自身进行0. XORing是使其归零的常用方法。
接下来,就得到了一个真正强大的工具 - 在0x0804822F的gadget是 mov %eax, (%edx); ret 将获取EAX中的任何内容,并将其复制到EDX中的地址所在的内存位置,因为可以将其与前面的gadget链接起来,以控制EAX和EDX的值,这就形成了write-what-where原语。
最后,可以在0x8048210找到一个int 0x80; ret gadget
可写内存
着手将想要执行的命令的字符串写进程序的内存中。可以通过 objdump -x 找到一个合适的可写内存部分
1 | 4 .bss 0000001c 0804a000 0804a000 00001000 2**2 |
值得注意的是,这个内存位置在最不重要的位置包含一个空字节,如果需要,可以添加一个小偏移量来避免这种情况,比如0x0804A004
Exploit
这是大量的准备工作,现在可以开始编写exploit了。正如在开始时提到的,为了获得最大的可读性,布局代码非常重要,所以要做的第一件事是为所有gadget提供可读的名称,并定义一个函数address()来进行小端地址转换。
1 | pop_eax = 0x08048225 |
还需要设置变量来保存可写内存地址,想放在那里的字符串,要放入64字节中的填充,最后是要覆盖EBP的垃圾值。因为这不会影响利用,它是一个小细节,但它有助于安排一切有条不紊。
1 | writable_memory=0x0804a010 # .bss section is writable |
知道在利用缓冲区中控制了68字节的EIP,因此可以用64字节 + 4字节构建缓冲区的第一部分来覆盖EBP
1 | buffer = text + padding * (64 - len(text)) |
String Write 1
利用缓冲区中接下来的4个字节是设置EIP的第一个值,也是ROP链的开始。第一个任务是使用write-what-where函数将字符串 “/bin/sh” 放入内存,作为syscall的参数1,第一步是将要写入的值放入EAX。
1 | buffer += address(pop_eax) # place value into EAX |
当执行pop EAX gadget时,ESP将指向下一个4字节的字,即对应于ascii字符串 “/bin” 的字节。
下一步是使用pop edx gadget将想要写入的地址放入edx中
1 | buffer += address(pop_edx) # place value into edx |
在这里使用address函数将字节重新排序为小的端序,因为两者都是内存地址。
最后,调用 mov %eax, (%edx) gadget来写内存
1 | buffer += address(writewhatwhere) |
String Write 2
现在重复这个过程,将字符串的下一个4个字节写入内存。从技术上讲,只剩下3个字节可以写入,但可以稍微作弊一下,因为多个/字符会被execve忽略。写 “//sh” 需要更新EDX以添加 +4 偏移量,并更改EAX中的值。
1 | buffer += address(pop_eax) |
字符串需要以空结束,在这里写入的内存被初始化为0,所以在这种情况下不需要担心,但它值得记住。
Address Write
现在需要的字符串在内存中,还需要向内存中写入指向它的指针。这是write-what-where原语的另一种用法。注意,写的偏移量是+12而不是+8,因为需要null结束字符串,不能把指针写在它旁边,需要有一个小的间隙。
1 | # write the address containing /bin/sh string into memory |
Zero EDX
需要的第三个参数存储在EDX中,应该是0x00000000。可以使用pop_edx gadget将空值移动到寄存器中,在这种情况下,这很好,因为可以在exploit中写入空值。或者,可以使用 xor %edx, %edx gadget,这是一种更通用的方法,因为0x00经常是一个坏字符。现在已经完成了write-what-where的使用,不再需要它了,所以是时候将它归零了。
1 | buffer += address(zero_edx) |
设置寄存器
现在是为syscall准备EBX和ECX的时候了,这是很容易做到的,因为两者都是内存位置;
1 | buffer += address(pop_ebx) |
最后一个参数是需要放入EAX中的syscall number。同样,0x0000000B包含空字节,这可能导致一些问题是利用。可能有必要(例如)将寄存器置零,并将其增加到11,以避免坏字符,或其他一些更迂回的设置寄存器的方法。
1 | buffer += address(pop_eax) |
Execve Syscall
现在该执行执行syscall了。现在正确设置了call的所有参数,这非常简单。还在这里完成了exploit,打印正在构建的缓冲区的内容。
1 | buffer += address(execute_syscall) |
运行exploit
在测试中,可以看到shell按照预期创建,但是当主机程序关闭时,它会立即关闭。这带来了一些问题,直到一个朋友指出,这是一个众所周知的常见问题,很容易通过使用cat保持输入管道打开来解决;
1 | root@kali:~# python exploit.py > exploit |
好了,一个适用于简单缓冲区溢出的ROP exploit。这是一个很有趣的挑战,在完成它的过程中学到了很多。希望本walkthrough对您的学习有用,并/或有趣。
参考链接(译)
- A Simple ROP Exploit – /bin/sh via syscall