Skip to content

Commit fd2e56a

Browse files
committed
publish flare on serpentine post
1 parent 545a7ed commit fd2e56a

12 files changed

+878
-2
lines changed

_posts/writeups/2024/2024-11-11-flareon-11-serpentine.md

Lines changed: 511 additions & 0 deletions
Large diffs are not rendered by default.

_posts/writeups/2024/2024-11-26-structured-exception-handler-x64.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title: A deep dive into modern Windows Structured Exception Handler (SEH)
33
description: Understanding how SEH works in x64
44
date: 2024-11-26 00:00:00 +0800
5-
categories: [writeups]
5+
categories: [Research]
66
img_path: /assets/posts/2024-11-26-structured-exception-handler-x64/
77
tags: [rev]
88
toc: True
@@ -237,7 +237,7 @@ BOOLEAN RtlInstallFunctionTableCallback(
237237
);
238238
```
239239

240-
The second way is using `RtlAddGrowableFunctionTable`. Unlike the previous API, you have to provide `RUNTIME_FUNCTION` entries upfront which will be added to an array of `RUNTIME_FUNCTION` entries that will be looked up when an exception occurs.
240+
The second way is using `RtlAddFunctionTable`/`RtlAddGrowableFunctionTable`. Unlike the previous API, you have to provide `RUNTIME_FUNCTION` entries upfront which will be added to an array of `RUNTIME_FUNCTION` entries that will be looked up when an exception occurs.
241241

242242
```c
243243
NTSTATUS RtlAddGrowableFunctionTable(
Binary file not shown.
Loading
Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
import ida_idp
2+
import ida_auto
3+
import idc
4+
import ida_funcs
5+
import ida_hexrays
6+
import ida_bytes
7+
import ida_ua
8+
import re
9+
import keystone
10+
import stitcher # https://github.com/allthingsida/allthingsida/blob/main/ctfs/y0da_flareon10/sticher.py
11+
12+
ks = keystone.Ks(keystone.KS_ARCH_X86, keystone.KS_MODE_64)
13+
mxcsr_loc = 0x140097AF0+0x7f0000
14+
base = 0x140097AF0
15+
16+
CONTEXT_STRUCT = {
17+
0x00000030: "ContextFlags",
18+
0x00000034: "mxcsr",
19+
0x00000038: "SegCs",
20+
0x0000003A: "SegDs",
21+
0x0000003C: "SegEs",
22+
0x0000003E: "SegFs",
23+
0x00000040: "SegGs",
24+
0x00000042: "SegSs",
25+
0x00000044: "EFlags",
26+
0x00000048: "Dr0",
27+
0x00000050: "Dr1",
28+
0x00000058: "Dr2",
29+
0x00000060: "Dr3",
30+
0x00000068: "Dr6",
31+
0x00000070: "Dr7",
32+
0x00000078: "rax",
33+
0x00000080: "rcx",
34+
0x00000088: "rdx",
35+
0x00000090: "rbx",
36+
0x00000098: "rsp",
37+
0x000000A0: "rbp",
38+
0x000000A8: "rsi",
39+
0x000000B0: "rdi",
40+
0x000000B8: "r8",
41+
0x000000C0: "r9",
42+
0x000000C8: "r10",
43+
0x000000D0: "r11",
44+
0x000000D8: "r12",
45+
0x000000E0: "r13",
46+
0x000000E8: "r14",
47+
0x000000F0: "r15",
48+
0x000000F8: "rip",
49+
}
50+
51+
class UWOP_CODES:
52+
UWOP_PUSH_NONVOL = 0
53+
UWOP_ALLOC_LARGE = 1
54+
UWOP_ALLOC_SMALL = 2
55+
UWOP_SET_FPREG = 3
56+
UWOP_SAVE_NONVOL = 4
57+
UWOP_SAVE_NONVOL_FAR = 5
58+
UWOP_EPILOG = 6
59+
UWOP_SPARE_CODE = 7
60+
UWOP_SAVE_XMM128 = 8
61+
UWOP_SAVE_XMM128_FAR = 9
62+
UWOP_PUSH_MACHFRAME = 10
63+
64+
codes = UWOP_CODES()
65+
66+
# in this function, we can secretly fixup encrypted instructions
67+
def disassemble_at(address):
68+
insn_to_disassemble = ida_ua.insn_t()
69+
cur = address
70+
71+
while True:
72+
cur += ida_ua.decode_insn(insn_to_disassemble, cur)
73+
74+
if insn_to_disassemble.get_canon_mnem() == "call":
75+
call_loc = insn_to_disassemble.ip
76+
77+
routine = call_loc + ida_bytes.get_dword(call_loc+1) + 5
78+
len_decrypted_insn = ida_bytes.get_byte(routine+2) - 0x29
79+
80+
insn = ida_ua.insn_t()
81+
82+
ida_ua.decode_insn(insn, routine)
83+
ida_bytes.patch_bytes(insn.Op1.addr, int(call_loc+5-(base&0xffff)).to_bytes(8, 'little')) # we resolve the dependency of call_loc+5
84+
85+
ida_ua.decode_insn(insn, routine+14)
86+
ah = ida_bytes.get_byte(insn.Op2.addr) # we depend on insn.Op2.addr
87+
88+
ida_ua.decode_insn(insn, routine+20)
89+
decrypted_insn = (((ah<<8) + insn.Op2.addr) & 0xffffffff).to_bytes(4, 'little') + ida_bytes.get_bytes(routine+0x26, len_decrypted_insn-4)
90+
ida_bytes.patch_bytes(routine+0x22, decrypted_insn)
91+
tmp = ida_ua.insn_t()
92+
ida_ua.create_insn(routine+0x22)
93+
ida_ua.decode_insn(tmp, routine+0x22)
94+
95+
if decrypted_insn[0] == 0xe9:
96+
absolute_loc = (int.from_bytes(decrypted_insn[1:5], 'little') + 5 + (routine + 0x22)) & 0xffffffff
97+
new_rela_offset = (absolute_loc - (5 + call_loc)) & 0xffffffff
98+
decrypted_insn = decrypted_insn[0:1] + new_rela_offset.to_bytes(4, 'little') + decrypted_insn[5:]
99+
elif tmp.Op1.type == 0x1 and tmp.Op2.type == 0x2:
100+
decrypted_insn = bytes(ks.asm(f"lea {ida_idp.get_reg_name(tmp.Op1.reg, 8)}, ds:{hex(tmp.Op2.addr)}", addr=call_loc)[0])
101+
if len(decrypted_insn) > len_decrypted_insn:
102+
call_loc = call_loc - (len(decrypted_insn) - len_decrypted_insn)
103+
decrypted_insn = bytes(ks.asm(f"lea {ida_idp.get_reg_name(tmp.Op1.reg, 8)}, ds:{hex(tmp.Op2.addr)}", addr=call_loc)[0])
104+
elif len(decrypted_insn) < len_decrypted_insn:
105+
decrypted_insn += b"\x90" * (len_decrypted_insn - len(decrypted_insn))
106+
107+
ida_bytes.patch_bytes(call_loc, decrypted_insn)
108+
print(f"successfully patched {hex(call_loc)}")
109+
110+
insn_to_disassemble = ida_ua.insn_t()
111+
ida_ua.create_insn(call_loc)
112+
cur = call_loc + ida_ua.decode_insn(insn_to_disassemble, call_loc)
113+
114+
if insn_to_disassemble.get_canon_mnem() == "jmp":
115+
if insn_to_disassemble.Op1.addr:
116+
cur = insn_to_disassemble.Op1.addr
117+
else:
118+
print("funny jump at", hex(cur))
119+
yield insn_to_disassemble
120+
121+
halt_address = base
122+
to_be_stitched = [base]
123+
124+
for i in range(33):
125+
while True:
126+
# get next exception handler
127+
unwind_info = halt_address + ida_bytes.get_byte(halt_address+1) + 2
128+
unwind_info += int((unwind_info & 1) != 0)
129+
count_of_codes = ida_bytes.get_byte(unwind_info+2)
130+
handler_offs = ida_bytes.get_dword(unwind_info+2*(count_of_codes+int((count_of_codes&1) != 0))+4)
131+
exception_handler = base + handler_offs
132+
133+
unwind_instructions = []
134+
135+
# unwind stack
136+
unwind_codes = ida_bytes.get_bytes(unwind_info+4,count_of_codes*2)
137+
i = 0
138+
OFFSET = 0
139+
RSP_DEREF_OFFSET = 0
140+
REG_USED = False
141+
RSP_DEREFED = False
142+
FINAL_REG = None
143+
while (i < count_of_codes*2):
144+
match (unwind_codes[i+1] & 0xf):
145+
case codes.UWOP_PUSH_NONVOL:
146+
FINAL_REG = CONTEXT_STRUCT[0x78 + (unwind_codes[i+1] >> 4) * 8]
147+
print("UWOP_PUSH_NONVOL")
148+
i += 2
149+
case codes.UWOP_ALLOC_LARGE:
150+
if (unwind_codes[i+1] >> 4):
151+
if REG_USED or RSP_DEREFED:
152+
OFFSET += int.from_bytes(unwind_codes[i+2:i+6], 'little')
153+
else:
154+
RSP_DEREF_OFFSET += int.from_bytes(unwind_codes[i+2:i+6], 'little')
155+
print("UWOP_ALLOC_LARGE")
156+
i += 6
157+
else:
158+
if REG_USED or RSP_DEREFED:
159+
OFFSET += int.from_bytes(unwind_codes[i+2:i+4], 'little') * 8
160+
else:
161+
RSP_DEREF_OFFSET += int.from_bytes(unwind_codes[i+2:i+4], 'little') * 8
162+
print("UWOP_ALLOC_LARGE")
163+
i += 4
164+
case codes.UWOP_ALLOC_SMALL:
165+
if REG_USED or RSP_DEREFED:
166+
OFFSET += ((unwind_codes[i+1] >> 4) * 8) + 8
167+
else:
168+
RSP_DEREF_OFFSET += ((unwind_codes[i+1] >> 4) * 8) + 8
169+
print("UWOP_ALLOC_SMALL")
170+
i += 2
171+
case codes.UWOP_SET_FPREG:
172+
REG_USED = CONTEXT_STRUCT[0x78+ (ida_bytes.get_byte(unwind_info+3) & 0xf) * 8]
173+
OFFSET -= (ida_bytes.get_byte(unwind_info+3) >> 4) * 16
174+
print("UWOP_SET_FPREG")
175+
i += 2
176+
case codes.UWOP_PUSH_MACHFRAME: # RSP is dereferenced!
177+
RSP_DEREF_OFFSET += (unwind_codes[i+1] >> 4) * 8
178+
RSP_DEREF_OFFSET += 0x18
179+
RSP_DEREFED = True
180+
print("UWOP_PUSH_MACHFRAME")
181+
i += 2
182+
case _:
183+
print(f"count of codes: {count_of_codes}\nunwind codes: {unwind_instructions}\nunwind info: {hex(unwind_info)}")
184+
print("@@@@@@@@@@@@@@@@ i donut recognize this opcode")
185+
break
186+
187+
if count_of_codes:
188+
unwind_instructions = []
189+
if REG_USED:
190+
unwind_instructions.append(ks.asm(f"mov {FINAL_REG}, {REG_USED}")[0])
191+
unwind_instructions.append(ks.asm(f"mov {FINAL_REG}, [{FINAL_REG}+{OFFSET}]")[0])
192+
elif RSP_DEREFED:
193+
unwind_instructions.append(ks.asm(f"mov {FINAL_REG}, [rsp+{RSP_DEREF_OFFSET}]")[0])
194+
unwind_instructions.append(ks.asm(f"mov {FINAL_REG}, [{FINAL_REG}+{OFFSET}]")[0])
195+
else:
196+
print(f"count of codes: {count_of_codes}\nunwind codes: {unwind_instructions}\nunwind info: {hex(unwind_info)}")
197+
print("i do not know how to resolve this...")
198+
raise Exception
199+
200+
unwind_instructions = b"".join([bytes(i) for i in unwind_instructions])
201+
ida_bytes.patch_bytes(halt_address, unwind_instructions)
202+
ida_ua.create_insn(halt_address)
203+
halt_address += len(unwind_instructions)
204+
205+
206+
# fixup halt
207+
insn = b"\xe9"
208+
insn += (exception_handler - halt_address - 5).to_bytes(4, 'little')
209+
ida_bytes.patch_bytes(halt_address, insn)
210+
ida_ua.create_insn(halt_address)
211+
212+
213+
# fixup exception handler, we disassemble until next 'hlt'
214+
gen = disassemble_at(exception_handler)
215+
context_record_register = False
216+
final_insns = [] # stores the final sets of instructions for each stub
217+
tainted = [] # stores registers that are written to. this is to know which registers has been tainted since the last stub
218+
unused_registers = ["r10", "r11", "r8", "rax", "rcx", "rdx", "rdi", "rsi", "rbx", "r12", "r13", "r14", "r15", "rbp", "r9"] # stores all registers that has yet to be used in the stub
219+
tainted_resolve = {}
220+
221+
while True:
222+
x = next(gen)
223+
raw = generate_disasm_line(x.ip, 1)
224+
225+
if (x.Op1.type in [0x1, 0x2, 0x3, 0x4]):
226+
r = ida_idp.get_reg_name(x.Op1.reg, 8)
227+
if r in unused_registers:
228+
unused_registers.remove(r)
229+
230+
if (x.Op2.type in [0x1, 0x2, 0x3, 0x4]):
231+
r = ida_idp.get_reg_name(x.Op2.reg, 8)
232+
if r in unused_registers:
233+
unused_registers.remove(r)
234+
235+
if (x.Op3.type in [0x1, 0x2, 0x3, 0x4]):
236+
r = ida_idp.get_reg_name(x.Op3.reg, 8)
237+
if r in unused_registers:
238+
unused_registers.remove(r)
239+
240+
# case 1: mnemonic is ldmxcsr, and its not a nop
241+
if "ldmxcsr" in raw.lower():
242+
if CONTEXT_STRUCT[x.Op1.addr] != "mxcsr":
243+
new = f"mov ds:{mxcsr_loc}, {CONTEXT_STRUCT[x.Op1.addr]}"
244+
r = CONTEXT_STRUCT[x.Op1.addr]
245+
if r in unused_registers:
246+
unused_registers.remove(r)
247+
final_insns.append(new)
248+
print(raw, "=>", new)
249+
250+
# case 2: mov rXX, [r9+0x28] <-- we are setting the context record
251+
elif x.get_canon_mnem() == "mov" and x.Op2.reg == 0x9 and x.Op2.addr == 0x28:
252+
context_record_register = ida_idp.get_reg_name(x.Op1.reg, 8)
253+
print(f"context record is stored in {context_record_register}")
254+
255+
# case 3: we are using our context record register
256+
elif context_record_register and context_record_register in raw:
257+
258+
if (context_record_register in ida_idp.get_reg_name(x.Op2.reg, 8) and x.Op2.type == 0x4) or (context_record_register in ida_idp.get_reg_name(x.Op1.reg, 8) and x.Op1.type == 0x4):
259+
260+
subj = re.findall(r"\[" + context_record_register + r"\+\w+?h]", raw)[0]
261+
offs = int(re.findall(r"\+(\w+?)h", subj)[0], 16)
262+
263+
reg_to_use = CONTEXT_STRUCT[offs]
264+
r = CONTEXT_STRUCT[offs]
265+
if r in unused_registers:
266+
unused_registers.remove(r)
267+
268+
if reg_to_use in tainted:
269+
# if reg_to_use in tainted_resolve:
270+
# reg_to_use = tainted_resolve[reg_to_use]
271+
# else:
272+
tainted_resolve[reg_to_use] = unused_registers.pop(0)
273+
final_insns.insert(0, f"mov {tainted_resolve[reg_to_use]}, {reg_to_use}")
274+
reg_to_use = tainted_resolve[reg_to_use]
275+
print("TAINTED", hex(x.ip))
276+
277+
if reg_to_use == "mxcsr":
278+
new = raw.replace(subj, f"ds:{mxcsr_loc}")
279+
final_insns.append(new)
280+
print(raw, "=>", new)
281+
# we have to account for the case where the context register is tainted
282+
else:
283+
new = raw.replace(subj, reg_to_use)
284+
final_insns.append(new)
285+
print(raw, "=>", new)
286+
else:
287+
final_insns.append(raw)
288+
289+
# case 3a: we are modifying our context record register in Op1
290+
if context_record_register in ida_idp.get_reg_name(x.Op1.reg, 8):
291+
context_record_register = False
292+
293+
if x.Op1.type == 0x1:
294+
tainted.append(ida_idp.get_reg_name(x.Op1.reg, 8))
295+
296+
else:
297+
if x.Op1.type == 0x1:
298+
tainted.append(ida_idp.get_reg_name(x.Op1.reg, 8))
299+
300+
if raw[:2] == "db":
301+
raise Exception
302+
final_insns.append(raw)
303+
304+
if x.get_canon_mnem() == "hlt":
305+
halt_address = x.ip
306+
print("halt at",hex(halt_address))
307+
ida_ua.create_insn(halt_address)
308+
break
309+
310+
cur = exception_handler
311+
kill = False
312+
for insn in final_insns[:-2]:
313+
if ';' in insn:
314+
insn = insn.split(";")[0]
315+
if 'loc_' in insn:
316+
insn = insn.replace("loc_", "ds:0x")
317+
if 'unk_' in insn:
318+
insn = insn.replace("unk_", "ds:0x")
319+
if 'jmp' in insn:
320+
kill = True
321+
i = ks.asm(insn, addr=cur)[0]
322+
ida_bytes.patch_bytes(cur, bytes(i))
323+
cur += len(i)
324+
325+
i = ks.asm(f"jmp {halt_address}", addr=cur)[0]
326+
ida_bytes.patch_bytes(cur, bytes(i))
327+
if kill:
328+
break
329+
330+
if i != 32:
331+
stage_addr = int("0x"+re.findall(r"unk_(\w+)", final_insns[-2])[0], 16)
332+
to_be_stitched.append(stage_addr)
333+
334+
# create stage functions
335+
i = 1
336+
for idx, stage_addr in enumerate(to_be_stitched[:-1]):
337+
ida_funcs.add_func(stage_addr)
338+
stitcher.stitch(do_stitch=True, addr=stage_addr)
339+
ida_auto.auto_wait()
340+
idc.set_name(stage_addr, f"stage{i}")
341+
i += 1
342+
343+
# give sane symbol names
344+
xor_table = 0x140094AC0
345+
or_table = 0x1400952C0
346+
addition_table = 0x140095AC0
347+
overflow_table = 0x1400962C0
348+
subtraction_table = 0x140096AC0
349+
underflow_table = 0x1400972C0
350+
tables = [xor_table, or_table, addition_table, overflow_table, subtraction_table, underflow_table]
351+
352+
for i in range(256):
353+
for j in tables:
354+
idc.create_qword(j+i*8)
355+
idc.set_name(xor_table+i*8, f"xor_table_{hex(i)[2:].zfill(2)}")
356+
idc.set_name(addition_table+i*8, f"addition_table_{hex(i)[2:].zfill(2)}")
357+
idc.set_name(subtraction_table+i*8, f"subtraction_table_{hex(i)[2:].zfill(2)}")
358+
idc.set_name(underflow_table+i*8, f"underflow_table_{hex(i)[2:].zfill(2)}")
359+
idc.set_name(overflow_table+i*8, f"overflow_table_{hex(i)[2:].zfill(2)}")
360+
idc.set_name(or_table+i*8, f"or_table_{hex(i)[2:].zfill(2)}")
361+
362+
# dump decompilation
363+
for i in range(1, 33):
364+
with open(rf"C:\Users\user\Desktop\Flare-On 11\9\serpentine\decompilations\stage{i}.txt", "w") as f:
365+
f.write(str(ida_hexrays.decompile(idc.get_name_ea(0, f"stage{i}"))))
Loading
Loading
Loading
Loading
Loading
Loading
Loading

0 commit comments

Comments
 (0)