Skip to content

Commit 22b8eba

Browse files
committed
hitcon
1 parent d183fe3 commit 22b8eba

15 files changed

+1825
-0
lines changed

2022/hitcon-2022/checker/README.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
```
2+
3+
im making a CTF challenge writeup. im kind of drunk. can u please clean this up a bit for me? thanks
4+
***
5+
okkkkk this one is a windows driver, the userland component basically does nothing so just ignore it. just look at the driver, the driver entry basically sets up an ioctl, when you call the ioctl you can pass in some cmd number to the driver, then the driver does a thing depending on what the number is.
6+
7+
its a flag decrypter challenge . the binary decrpyt hte flag, figure out what right input is and that will affect the decrpyiton, make the decrpyiton happy to get the flag.
8+
9+
basically if u do some number from like 0x222000,0x222020,0x222030, ... 0x222070 etc it will do something with the corresponding number 0 thru 7. theres 8 of these thingys and then it will set the done flag for that thingy to 1. looks like we gotta collect all the thingys (atleast once) before it will let us do the needful. so this itself is a bit of a hint from the author that we need a permutation, strictly speaking we dont need a permutation because it could be one or more so whatever.
10+
11+
so anyways each of the thingys basically just xors some of the code in the driver so its some selfm odifying code, it xors the code of this univariate scalar function that operates on uint32s, then calls that on the flag so basically we gotta find the right sequence of things to xor the mathy scalary function on and then do it in the right order and do it for the 8 thingys to decrypt the flag, oh no what order is it? u can write a backdtracking algorithm to solve this but my hunch was right that theres no funny business backtracking needed since if u start from each element of the permutation theres actually only 1 possible valid thingy there so its not 8! u only have to do 8^2 tries.
12+
13+
also i used unicorn engine to emulate the thing since im lazy i had kdnet setup and all but i dont wanna fucking blue screen. i put the solve script in cocksleeve.py plz check it out its also good documentation on how to use unicorn because that shits really poorly documented
14+
***
15+
include all of the details. when u make the writeup make sure to organize it clearly with section headings and everything
16+
```
17+
18+
CTF Challenge: Flag Decrypter
19+
20+
Overview:
21+
22+
The challenge is a Windows driver with a userland component that does nothing. The driver entry sets up an ioctl, which when called with a specific cmd number, performs an action depending on the number provided.
23+
24+
The driver decrypts the flag, and the challenge is to determine the correct input to affect the decryption. In order to do this, we must collect all of the "thingys" (eight in total) and set the done flag for each of them to 1. This indicates that we need a permutation to solve the challenge.
25+
26+
Each thingy xors some of the code in the driver, which is a self-modifying code. This xors the code of a univariate scalar function that operates on uint32s, and then calls that on the flag. In order to decrypt the flag, we need to find the right sequence of thingys to xor the function and do it in the correct order for all eight thingys.
27+
28+
To solve the challenge, we can use a backtracking algorithm, but it is not necessary as there is only one possible valid thingy for each element of the permutation. We only need to try 8^2 combinations.
29+
30+
To make things easier, we can use the Unicorn engine to emulate the driver. The solve script, called cocksleeve.py, can be found in the repository and provides good documentation on how to use Unicorn.
31+
32+
33+
```
34+
35+
this is the most important part of the solver code please explain the code as part of the ctf writeup.
36+
***
37+
38+
def emulate(start_va, rcx):
39+
block_disasm.clear()
40+
41+
reset_regs()
42+
setup_stack_frame()
43+
setup_regs_rip_rbp_rsp_rcx()
44+
try:
45+
mu.emu_start(start_va, 0x6969696969696969) # fake retaddr
46+
except UcError as e:
47+
print("ERROR: %s" % e)
48+
print('RIP=%x' % mu.reg_read(UC_X86_REG_RIP))
49+
return 0, False
50+
rax = mu.reg_read(UC_X86_REG_RAX)
51+
return rax, True
52+
53+
def simulate_ioctl(i):
54+
mem_xor(pmathshit, pFucker+i, 16)
55+
56+
for j in range(43):
57+
inp = mu.mem_read(pFlag+j,1)[0]
58+
rax, ok = emulate_mathshit(inp) # call 0x140001B30
59+
if not ok:
60+
return None, False
61+
al = rax&0xff
62+
mu.mem_write(pFlag+j, bytes([al]))
63+
64+
mem_xor(pmathshit, pFucker+i+16, 16)
65+
return block_disasm, True
66+
67+
for i in path:
68+
print('i',hex(i))
69+
result, ok = simulate_ioctl(i)
70+
if not ok:
71+
return None, False
72+
print('code', result)
73+
result=mu.mem_read(pFlag,43)
74+
return result, True
75+
***
76+
```
77+
78+
The emulate function is used to emulate the driver code using the Unicorn engine. It starts emulation at the specified start_va and passes in the rcx value as a parameter. The block_disasm is cleared, and the registers are reset and setup with the appropriate values for the stack frame, rip, rbp, rsp, and rcx. The emulator is then started at the start_va address with a fake retaddr. If an error occurs, it is caught and printed, and the function returns 0 and False. Otherwise, the value in the rax register is returned along with True.
79+
80+
The simulate_ioctl function uses the emulate function to simulate the ioctl call. It first performs a memory xor operation on the pmathshit and pFucker+i addresses with a length of 16. It then loops through 43 iterations, each time reading one byte from the pFlag address, calling the emulate_mathshit function with the input byte, and writing the result to the pFlag address. If an error occurs during the emulate_mathshit call, the function returns None and False. Otherwise, it performs another memory xor operation on the pmathshit and pFucker+i+16 addresses with a length of 16 and returns the disassembled code block and True.
81+
82+
The solver code then loops through the path array, simulating the ioctl call for each element in the array. If an error occurs during the simulate_ioctl call, the function returns None and False. Otherwise, it prints the disassembled code block and continues to the next element in the path array. After all elements in the path array have been processed, the function reads the 43 bytes from the pFlag address and returns the result along with True.
83+
84+
```
85+
86+
can you explain it at a higher level that's less focused on fine grained details and more about how this helps solve the challenge with the driver
87+
```
88+
89+
The solver code uses the Unicorn engine to emulate the driver code in order to solve the challenge. It first sets up the emulator with the appropriate values for the registers and stack frame, and then starts the emulator at a specific address with a fake retaddr.
90+
91+
Next, the solver simulates an ioctl call by performing memory xor operations and calling the emulate_mathshit function on the flag. The result of the emulate_mathshit function is written back to the flag, and the code is disassembled to verify that the decryption is being performed correctly.
92+
93+
Finally, the solver loops through a provided path array and simulates the ioctl call for each element in the array. This allows the solver to determine the correct sequence and order of the thingys needed to decrypt the flag. The final result of the flag decryption is returned and can be checked against the correct flag.
94+

2022/hitcon-2022/checker/checker.exe

10.5 KB
Binary file not shown.
270 KB
Binary file not shown.
10.6 KB
Binary file not shown.
558 KB
Binary file not shown.
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
from unicorn import *
2+
from unicorn.x86_const import *
3+
from capstone import *
4+
from keystone import *
5+
import pefile
6+
7+
import os
8+
import sys
9+
import mmap
10+
11+
filename = 'checker_drv.sys'
12+
f = open(filename, 'rb')
13+
mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
14+
15+
pe = pefile.PE(filename)
16+
pe.full_load()
17+
18+
def doit(path):
19+
mu = Uc(UC_ARCH_X86, UC_MODE_64)
20+
md = Cs(CS_ARCH_X86, CS_MODE_64)
21+
ks = Ks(KS_ARCH_X86, KS_MODE_64)
22+
23+
block_disasm = []
24+
25+
def map_region(base_va, virtual_size, contents):
26+
if virtual_size & 0xfff:
27+
virtual_size += 4096 - (virtual_size & 0xfff)
28+
if virtual_size > len(contents):
29+
contents += b'\x00' * (virtual_size-len(contents))
30+
elif virtual_size < len(contents):
31+
contents = contents[:virtual_size]
32+
assert len(contents) == virtual_size
33+
34+
mu.mem_map(base_va, virtual_size)
35+
mu.mem_write(base_va, contents)
36+
print(f'Mapped region {base_va:x}-{base_va+virtual_size:x}')
37+
38+
for s in pe.sections:
39+
start_va = pe.NT_HEADERS.OPTIONAL_HEADER.ImageBase+s.VirtualAddress
40+
start_fo = s.PointerToRawData
41+
end_fo = start_fo + s.SizeOfRawData
42+
contents = mm[start_fo:end_fo]
43+
map_region(start_va, s.Misc_VirtualSize, contents)
44+
45+
def hook_mem_invalid(uc, access, address, size, value, user_data):
46+
# fault this region in if it exists in the dump. otherwise, abort
47+
print ('>>> Page fault accessing unmapped memory at 0x%x, data size = %u, data value = 0x%x' %(address, size, value))
48+
return False # halt emulation
49+
50+
mu.hook_add(UC_HOOK_MEM_READ_UNMAPPED | UC_HOOK_MEM_WRITE_UNMAPPED, hook_mem_invalid)
51+
52+
def hook_mem_access(uc, access, address, size, value, user_data):
53+
return
54+
if access == UC_MEM_WRITE:
55+
print(">>> Memory is being WRITE at 0x%x, data size = %u, data value = 0x%x" %(address, size, value))
56+
else: # READ
57+
print(">>> Memory is being READ at 0x%x, data size = %u" %(address, size))
58+
59+
def hook_block(uc, address, size, user_data):
60+
# print(">>> Tracing basic block at 0x%x, block size = 0x%x" %(address, size))
61+
pass
62+
63+
# callback for tracing instructions
64+
def hook_code64(uc, address, size, user_data):
65+
# print(">>> Tracing instruction at 0x%x, instruction size = 0x%x" %(address, size))
66+
if size == 0xf1f1f1f1: # invalid instruction
67+
print('>>> ' + hex(address) + ': invalid')
68+
return False
69+
code = mu.mem_read(address, size)
70+
# print(code.hex())
71+
disasm = list(md.disasm_lite(code, size))
72+
# print(disasm)
73+
if len(disasm) == 1:
74+
_, disasm_size, mnemonic, op_str = disasm[0]
75+
if disasm_size != size:
76+
print('wtf bad disasm')
77+
print(disasm_size,size,code.hex())
78+
# print('>>> ' + hex(address) + ': ' + mnemonic + ' ' + op_str)
79+
if mnemonic in ['out', 'hlt'] : # yeaaaa idts
80+
print('no thats a bad instruction. smh')
81+
mu.reg_write(UC_X86_REG_RIP, 0)
82+
return False
83+
block_disasm.append((address, size, mnemonic, op_str))
84+
else:
85+
print('>>> bad disasm?')
86+
print(code.hex(), size)
87+
block_disasm.append(code.hex())
88+
# mu.reg_write(UC_X86_REG_RIP, 0)
89+
return False
90+
91+
mu.hook_add(UC_HOOK_MEM_WRITE, hook_mem_access)
92+
mu.hook_add(UC_HOOK_MEM_READ, hook_mem_access)
93+
mu.hook_add(UC_HOOK_BLOCK, hook_block)
94+
mu.hook_add(UC_HOOK_CODE, hook_code64)
95+
96+
# map a stack
97+
stack_base, stack_size = 0x7ffffff0000, 0x10000
98+
mu.mem_map(stack_base, stack_size)
99+
100+
def mem_xor(dst, src, len):
101+
for i in range(len):
102+
x = mu.mem_read(dst,1)[0]
103+
y = mu.mem_read(src,1)[0]
104+
mu.mem_write(dst,bytes([x^y]))
105+
dst += 1
106+
src += 1
107+
mu.ctl_remove_cache(dst-len,dst) # fucking flush emulator code cache
108+
109+
def reset_regs():
110+
for regno in [UC_X86_REG_RAX, UC_X86_REG_RBP, UC_X86_REG_RBX, UC_X86_REG_RCX, UC_X86_REG_RDI, UC_X86_REG_RDX, UC_X86_REG_RIP, UC_X86_REG_RSI, UC_X86_REG_RSP, UC_X86_REG_R8, UC_X86_REG_R9, UC_X86_REG_R10, UC_X86_REG_R11, UC_X86_REG_R12, UC_X86_REG_R13, UC_X86_REG_R14, UC_X86_REG_R15, UC_X86_REG_EFLAGS]:
111+
mu.reg_write(regno, 0)
112+
113+
pmathshit = 0x140001B30
114+
MyObjPreCallback = 0x140001430
115+
pFucker = 0x140003030
116+
pFlag = 0x140003000
117+
118+
mem_xor(pmathshit, MyObjPreCallback, 16)
119+
mem_xor(pmathshit, MyObjPreCallback+16, 16)
120+
121+
def emulate(start_va, rcx):
122+
block_disasm.clear()
123+
124+
reset_regs()
125+
mu.mem_write(stack_base, b'\x00' * stack_size)
126+
stack_frame = stack_base + (stack_size - 0x1000)
127+
mu.mem_write(stack_frame, b'iiiiiiii') # fake return address
128+
mu.reg_write(UC_X86_REG_RIP, start_va) # i think this is unnecessary
129+
mu.reg_write(UC_X86_REG_RBP, stack_frame)
130+
mu.reg_write(UC_X86_REG_RSP, stack_frame)
131+
mu.reg_write(UC_X86_REG_RCX, rcx)
132+
try:
133+
mu.emu_start(start_va, 0x6969696969696969) # fake retaddr
134+
except UcError as e:
135+
print("ERROR: %s" % e)
136+
print('RIP=%x' % mu.reg_read(UC_X86_REG_RIP))
137+
return 0, False
138+
# print('ok?')
139+
# print(hex(mu.reg_read(UC_X86_REG_RIP)))
140+
rax = mu.reg_read(UC_X86_REG_RAX)
141+
# print (f'rax={rax:x}')
142+
return rax, True
143+
144+
def emulate_mathshit(arg):
145+
print('call mathshit(%02x)' % arg)
146+
rax, ok = emulate(0x140001B30, arg)
147+
if ok:
148+
print('-> ok: 0x%x'% rax);
149+
else:
150+
print('-> not ok')
151+
return rax, ok
152+
153+
def simulate_ioctl(i):
154+
mem_xor(pmathshit, pFucker+i, 16)
155+
156+
for j in range(43):
157+
inp = mu.mem_read(pFlag+j,1)[0]
158+
rax, ok = emulate_mathshit(inp)
159+
if not ok:
160+
return None, False
161+
al = rax&0xff
162+
# print('inp %02x' % inp, 'plain %02x'%( al))
163+
mu.mem_write(pFlag+j, bytes([al]))
164+
165+
mem_xor(pmathshit, pFucker+i+16, 16)
166+
return block_disasm, True
167+
168+
for i in path:
169+
print('i',hex(i))
170+
result, ok = simulate_ioctl(i)
171+
if not ok:
172+
return None, False
173+
print('code', result)
174+
result=mu.mem_read(pFlag,43)
175+
return result, True
176+
177+
path = [0xe0,0x40,192,0,32,128,96,160]
178+
result,ok = doit(path)
179+
print(result,ok)
180+
181+
# gucci = []
182+
# for i in range(0,0x100,0x20):
183+
# try_path = path[:] + [i]
184+
# block_disasm, ok = doit(try_path)
185+
# if ok:
186+
# gucci.append((i, block_disasm))
187+
# print()
188+
# print()
189+
# print()
190+
# print()
191+
192+
# for g in gucci:
193+
# print(g)

2022/hitcon-2022/meow-way/README.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
```
2+
3+
im making a CTF challenge writeup. im kind of drunk. can u please clean this up a bit for me? thanks
4+
5+
solution script in unfuck3.py
6+
nice idb in the i64 idb
7+
8+
summary of the challenge: return to 64 bits heavens gate nonsense
9+
do the far return switch cs selector to the 64 bits one
10+
some wow64 shit. very classic trick in windows reversing.
11+
i dont feel like describing right now, google heavens gate
12+
13+
rest is straightforward reversing . the 64 bits mode shit is all very repetitive so i just solve parsing the diosassembly in python lol. wasnt lying about that in the tweet about reversing with copy paste into sublime text
14+
15+
to make the shit nice to view in ida i wrote some idapython scripts to make segments between 64 bits and 32 bits and shit based on the call gates
16+
17+
this part is my favorite poart i think it's the most funny
18+
19+
from z3 import *
20+
good_inp=b''
21+
for i,ptr in enumerate(order):
22+
unk = BitVec('flag_%d' % i, 8)
23+
cl = BitVecVal(lilShitter[ptr],8)
24+
is_add_or_sub, xor_key = wowies[ptr]
25+
if is_add_or_sub:
26+
cl += unk
27+
else:
28+
cl -= unk
29+
cl ^= xor_key
30+
s = Solver()
31+
s.add(ciphert[i]==cl)
32+
assert s.check()==sat
33+
m=s.model()
34+
inp = m[unk].as_long()
35+
good_inp += bytes([inp])
36+
print(good_inp)
37+
38+
39+
lol
40+
```
41+
42+
OpenAI
43+
44+
Here is a cleaned-up version of the writeup:
45+
46+
Summary
47+
-------
48+
49+
The challenge is a Windows binary that uses the "Heaven's Gate" trick to switch from 32-bit mode to 64-bit mode. The solution involves reversing the binary and using a Python script to parse the disassembly. The script uses the `z3` library to solve the encryption used in the binary.
50+
51+
Reversing the Binary
52+
--------------------
53+
54+
To reverse the binary, we can use IDA Pro and some IDAPython scripts to make the disassembly easier to read. In particular, we can use the scripts to segment the code into the different modes (32-bit and 64-bit) based on the call gates used in the "Heaven's Gate" trick.
55+
56+
Once we have the disassembly segmented, we can focus on the encryption used in the binary. It turns out that the encryption is a simple XOR cipher with an added or subtracted value, depending on the value of a flag. We can use the `z3` library to solve for the correct input that will decrypt the ciphertext.
57+
58+
Solution Script
59+
---------------
60+
61+
Here is the Python script that solves the challenge:
62+
63+
```python
64+
from z3 import *
65+
66+
# define the variables used in the cipher
67+
good_inp = b''
68+
for i, ptr in enumerate(order):
69+
unk = BitVec('flag_%d' % i, 8)
70+
cl = BitVecVal(lilShitter[ptr], 8)
71+
is_add_or_sub, xor_key = wowies[ptr]
72+
if is_add_or_sub:
73+
cl += unk
74+
else:
75+
cl -= unk
76+
cl ^= xor_key
77+
78+
# use z3 to solve for the correct input
79+
s = Solver()
80+
s.add(ciphert[i] == cl)
81+
assert s.check() == sat
82+
m = s.model()
83+
inp = m[unk].as_long()
84+
good_inp += bytes([inp])
85+
86+
# print the decrypted input
87+
print(good_inp)
88+
```
89+
90+
This script will output the correct input that can be used to solve the challenge.
405 KB
Binary file not shown.
390 KB
Binary file not shown.

0 commit comments

Comments
 (0)