Skip to content

Commit ce7f97f

Browse files
committed
added pingctf rev writeups
1 parent 5528514 commit ce7f97f

14 files changed

+1857
-0
lines changed

src/content/blog/pingCTF_2025/README.md

Lines changed: 801 additions & 0 deletions
Large diffs are not rendered by default.
8.83 KB
Binary file not shown.

src/content/blog/pingCTF_2025/cat-detector.ipynb

Lines changed: 320 additions & 0 deletions
Large diffs are not rendered by default.
17.4 KB
Binary file not shown.
2.09 MB
Binary file not shown.
112 KB
Loading
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
#!/usr/bin/env python3
2+
import sys
3+
import argparse
4+
from typing import List, Tuple, Optional
5+
6+
# Opcode mappings
7+
OPCODES = {
8+
0x00: "exit",
9+
0x01: "mov",
10+
0x02: "add",
11+
0x03: "sub",
12+
0x04: "mul",
13+
0x05: "xor",
14+
0x06: "or",
15+
0x07: "and",
16+
0x08: "not",
17+
0x09: "push",
18+
0x0a: "pop",
19+
0x0b: "cmp",
20+
0x0c: "jmp",
21+
0x0d: "je",
22+
0x0e: "jne",
23+
0x0f: "jg",
24+
0x10: "jl",
25+
0x11: "syscall",
26+
0x12: "wmem",
27+
0x13: "rmem"
28+
}
29+
30+
class Decompiler:
31+
def __init__(self, bytecode_file: str, output_file: str = None, pretty: bool = True):
32+
self.bytecode_file = bytecode_file
33+
self.output_file = output_file
34+
self.bytecode = []
35+
self.pretty = pretty
36+
self.labels = {}
37+
self.inst_size = 6 # Each instruction is 6 bytes
38+
39+
def load_bytecode(self) -> None:
40+
"""Load bytecode from file into memory."""
41+
try:
42+
with open(self.bytecode_file, "rb") as f:
43+
self.bytecode = list(f.read())
44+
print(f"Loaded {len(self.bytecode)} bytes from {self.bytecode_file}")
45+
except Exception as e:
46+
print(f"Failed to open file: {e}")
47+
sys.exit(1)
48+
49+
def find_jump_targets(self) -> None:
50+
"""Scan through bytecode to find all jump targets and create labels."""
51+
for i in range(0, len(self.bytecode) - 5, self.inst_size):
52+
opcode = self.bytecode[i]
53+
type_field = self.bytecode[i + 1]
54+
operand_a = self.bytecode[i + 2] | (self.bytecode[i + 3] << 8)
55+
56+
# Check if this is a jump instruction with immediate addressing
57+
if opcode in [0x0c, 0x0d, 0x0e, 0x0f, 0x10] and type_field == 0:
58+
# Add target to labels dict if not register-based jump
59+
if operand_a not in self.labels:
60+
self.labels[operand_a] = f"label_{operand_a:04x}"
61+
62+
def format_operand(self, type_field: int, operand: int, register_prefix: str = "r") -> str:
63+
"""Format operand based on type field."""
64+
# Different instructions have different type field interpretations
65+
# This is a simplified version, might need to be expanded for specific opcodes
66+
if type_field == 0: # Register
67+
return f"{register_prefix}{operand}"
68+
elif type_field == 1: # Immediate
69+
return f"{operand}"
70+
elif type_field == 2: # Memory address operand
71+
return f"[{operand}]"
72+
elif type_field == 3: # Register to memory
73+
return f"[{operand}]"
74+
elif type_field == 4: # Immediate to memory
75+
return f"[{operand}]"
76+
elif type_field == 5: # Memory to memory
77+
return f"[{operand}]"
78+
elif type_field == 6: # Special (e.g., IP)
79+
return f"ip"
80+
else:
81+
return f"{operand} (unknown addressing mode {type_field})"
82+
83+
def format_instruction(self, addr: int, opcode: int, type_field: int,
84+
operand_a: int, operand_b: int) -> str:
85+
"""Format a single instruction as assembly code."""
86+
if opcode not in OPCODES:
87+
return f"; Invalid opcode: {opcode:02x}"
88+
89+
mnemonic = OPCODES[opcode]
90+
label = self.labels.get(addr, "")
91+
label_str = f"{label}:" if label else ""
92+
93+
# Format based on instruction type
94+
if opcode == 0x00: # exit
95+
return f"{label_str:<12} exit"
96+
97+
elif opcode in [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]: # Binary ops
98+
# mov, add, sub, mul, xor, or, and
99+
dst = self.format_operand(0, operand_a) # Always a register for these
100+
src = self.format_operand(type_field, operand_b)
101+
return f"{label_str:<12} {mnemonic} {dst}, {src}"
102+
103+
elif opcode == 0x08: # not (unary op)
104+
dst = self.format_operand(0, operand_a)
105+
src = self.format_operand(type_field, operand_b)
106+
return f"{label_str:<12} not {dst}, {src}"
107+
108+
elif opcode == 0x09: # push
109+
if type_field == 0:
110+
operand = self.format_operand(0, operand_a)
111+
else:
112+
operand = self.format_operand(type_field, operand_a)
113+
return f"{label_str:<12} push {operand}"
114+
115+
elif opcode == 0x0a: # pop
116+
operand = self.format_operand(0, operand_a)
117+
return f"{label_str:<12} pop {operand}"
118+
119+
elif opcode == 0x0b: # cmp
120+
left = self.format_operand(0, operand_a)
121+
right = self.format_operand(type_field, operand_b)
122+
return f"{label_str:<12} cmp {left}, {right}"
123+
124+
elif opcode in [0x0c, 0x0d, 0x0e, 0x0f, 0x10]: # Jumps
125+
# jmp, je, jne, jg, jl
126+
if type_field == 0: # Direct jump
127+
target = self.labels.get(operand_a, f"0x{operand_a:04x}")
128+
return f"{label_str:<12} {mnemonic} {target}"
129+
else: # Register jump
130+
reg = self.format_operand(0, operand_a)
131+
return f"{label_str:<12} {mnemonic} {reg}"
132+
133+
elif opcode == 0x11: # syscall
134+
return f"{label_str:<12} syscall {operand_a}"
135+
136+
elif opcode == 0x12: # wmem
137+
addr_reg = self.format_operand(0, operand_a)
138+
if type_field == 0:
139+
value = self.format_operand(0, operand_b)
140+
else:
141+
value = self.format_operand(type_field, operand_b)
142+
return f"{label_str:<12} wmem [{addr_reg}], {value}"
143+
144+
elif opcode == 0x13: # rmem
145+
dst_reg = self.format_operand(0, operand_a)
146+
if type_field == 0:
147+
addr = f"[{self.format_operand(0, operand_b)}]"
148+
else:
149+
addr = self.format_operand(type_field, operand_b)
150+
return f"{label_str:<12} rmem {dst_reg}, {addr}"
151+
152+
else:
153+
return f"{label_str:<12} ; Unknown instruction {mnemonic} {type_field} {operand_a} {operand_b}"
154+
155+
def decompile(self) -> List[str]:
156+
"""Decompile bytecode into assembly instructions."""
157+
self.find_jump_targets()
158+
159+
asm_lines = ["; Decompiled from bytecode file: " + self.bytecode_file,
160+
"; Format: [label:] instruction operands",
161+
""]
162+
163+
# Add labels section
164+
if self.pretty and self.labels:
165+
asm_lines.extend(["; Jump targets:", ""])
166+
167+
# Disassemble each instruction
168+
for addr in range(0, len(self.bytecode) - 5, self.inst_size):
169+
opcode = self.bytecode[addr]
170+
type_field = self.bytecode[addr + 1]
171+
operand_a = self.bytecode[addr + 2] | (self.bytecode[addr + 3] << 8)
172+
operand_b = self.bytecode[addr + 4] | (self.bytecode[addr + 5] << 8)
173+
174+
# Add address comment if pretty printing
175+
176+
# Format and add the instruction
177+
asm_line = self.format_instruction(addr, opcode, type_field, operand_a, operand_b)
178+
asm_lines.append(asm_line)
179+
180+
# Add empty line after each instruction if pretty printing
181+
if self.pretty:
182+
asm_lines.append("")
183+
184+
return asm_lines
185+
186+
def save_output(self, asm_lines: List[str]) -> None:
187+
"""Save the decompiled assembly to file or print to stdout."""
188+
if self.output_file:
189+
try:
190+
with open(self.output_file, "w") as f:
191+
f.write("\n".join(asm_lines))
192+
print(f"Output saved to {self.output_file}")
193+
except Exception as e:
194+
print(f"Failed to write output file: {e}")
195+
sys.exit(1)
196+
else:
197+
print("".join(asm_lines))
198+
199+
def main():
200+
parser = argparse.ArgumentParser(description="Decompile VM bytecode to assembly")
201+
parser.add_argument("input_file", help="Input bytecode file")
202+
parser.add_argument("-o", "--output", help="Output assembly file")
203+
parser.add_argument("-p", "--pretty", action="store_true", help="Enable pretty printing")
204+
args = parser.parse_args()
205+
206+
decompiler = Decompiler(args.input_file, args.output, args.pretty)
207+
decompiler.load_bytecode()
208+
asm_lines = decompiler.decompile()
209+
decompiler.save_output(asm_lines)
210+
211+
if __name__ == "__main__":
212+
main()
34.9 KB
Binary file not shown.
161 KB
Loading
512 KB
Binary file not shown.

0 commit comments

Comments
 (0)