Table of Contents
- re-010 — Network byte-transform checker
- re-012 — Constraint-based checker (f1..f8)
- Carbonara — jump-obfuscation and data-flow recovery
re-010 — Network byte-transform checker
Summary
- Type: Reversing (ELF64, stripped, PIE)
- Behavior: TCP server flag checker
- Port:
31338 - Expected input length:
37bytes - Result:
:)on success,:(on failure - Flag:
CBC{c72b6b1feb2dbb3132380e1a9f967441}
Notes
- The binary does not read from stdin directly; it binds/listens and validates client input over a socket.
- Validation is split into 3 blocks of 16 transformed-byte checks using constant tables in
.rodata. - Recovered plaintext chunks:
CBC{c72b6b1feb2dbb3132380e1a9f967441}
- Combined final flag:
CBC{c72b6b1feb2dbb3132380e1a9f967441}
Validation
# run service
./chall
# in another shell
python3 - << 'PY'
import socket
flag='CBC{c72b6b1feb2dbb3132380e1a9f967441}'
s=socket.create_connection(('127.0.0.1',31338))
print(s.recv(200))
s.sendall(flag.encode()+b'\n')
print(s.recv(200))
PY
Expected response contains :).
Minimal solver (repro)
# recovers: CBC{c72b6b1feb2dbb3132380e1a9f967441}
idx=[3,8,13,2,7,12,1,6,11,0,5,10,15,4,9,14]
adds=[7,8,11,16,23,32,43,56,71,88,107,128,151,176,203,232]
blk1=[0x82,0x3e,0x6d,0x53,0x79,0x85,0x6d,0x6a,0xad,0x9b,0xa2,0xb1,0xfb,0x12,0x2c,0x19]
blk2=[0x38,0x38,0x71,0x43,0x4f,0x59,0x8d,0x6b,0xa8,0xba,0x9d,0xb1,0xcd,0xe3,0x2f,0x20]
blk3=[0x38,0x08,0x0b,0x44,0x17,0x20,0x5f,0x38,0x47,0x8f,0x6b,0x80,0x97,0x2c,0xcb,0xe8]
def inv_byte(out, add):
for x in range(256):
if (x + add) % 257 == out:
return x
raise ValueError('no solution')
p1=[0]*16
p2=[0]*16
p3=[0]*16
for i in range(16):
p1[idx[i]] = inv_byte(blk1[i], adds[i])
p2[idx[i]] = inv_byte(blk2[i], adds[i])
p3[idx[i]] = inv_byte(blk3[i], adds[i])
flag = (bytes(p1)+bytes(p2)+bytes(p3)).split(b'\x00',1)[0].decode('ascii')
print(flag)
re-012 — Constraint-based checker (f1..f8)
Summary
- Type: Reversing (ELF64, stripped, PIE)
- Behavior: TCP flag-checker server (not stdin checker)
- Port:
57005 - Final flag:
CBC{116c7c40348dcf6385e848d6b49cb2b8}
1) Initial recon
file chall
checksec --file=chall
strings -tx chall | egrep 'Enter flag:|Nope|Congrats|CBC\{'
Findings:
- 64-bit ELF, dynamically linked, stripped.
- Full RELRO, Canary, NX, PIE.
- Strings showed:
Enter flag:NopeCongrats, flag : CBC{
Running directly looked like a hang. strace showed why:
- program binds
0.0.0.0:57005 - calls
listen()then blocks onaccept()
So it is a socket service checker.
2) Confirm I/O model
nc 127.0.0.1 57005
Protocol observed:
- server sends
Enter flag: - client sends one line
- server replies
Nopeor success string
Also verified checker expects exactly 32 chars for the inside of CBC{...}.
3) Reverse core logic
Disassembly around the checker function (starts near 0x31c5) shows:
- Reads input into a C++ string
- Rejects unless length is
0x20(32) - Copies bytes into a 32-byte buffer
- Iterates over a table of function descriptors in
.data - Calls tiny predicates (
f1..f8) at addresses around0x25e9..0x2f85
Each predicate is a byte-level arithmetic relation over selected indices of the 32-byte input.
The .data table contains 48 constraints total.
4) Solve with Z3
Modeled 32 unknown bytes as BitVec(8) and encoded all 48 relations.
Solver output:
116c7c40348dcf6385e848d6b49cb2b8
Final flag:
CBC{116c7c40348dcf6385e848d6b49cb2b8}
5) Validation
Submitting that string to the service returned:
Congrats, flag : CBC{116c7c40348dcf6385e848d6b49cb2b8}
Confirmed correct.
Minimal solver (repro)
from z3 import *
from pathlib import Path
import struct
p = Path('chall').read_bytes()
DATA_OFF = 0x8000
NREC = 48
name_map = {
0x6008: 'f1', 0x600b: 'f2', 0x600e: 'f3', 0x6011: 'f4',
0x6014: 'f5', 0x6017: 'f6', 0x601a: 'f7', 0x601d: 'f8'
}
recs = []
for i in range(NREC):
b = p[DATA_OFF + i*0x40 : DATA_OFF + (i+1)*0x40]
nameptr = struct.unpack_from('<Q', b, 0x20)[0]
f = name_map[nameptr]
a,b1,c,d,k1,k2 = struct.unpack_from('<iiiiii', b, 0x28)
recs.append((f,a,b1,c,d,k1,k2))
x = [BitVec(f'x{i}', 8) for i in range(32)]
s = Solver()
for xi in x:
s.add(And(xi >= 0x20, xi <= 0x7e))
for (f,a,b,c,d,k1,k2) in recs:
A, B = ZeroExt(24, x[a]), ZeroExt(24, x[b])
if f == 'f1':
C = ZeroExt(24, x[c]); s.add(((A ^ B) - C) & 0xff == (k1 & 0xff))
elif f == 'f2':
C = ZeroExt(24, x[c]); s.add(((A + B) ^ C) & 0xff == (k1 & 0xff))
elif f == 'f3':
C = ZeroExt(24, x[c]); s.add(((A - B) ^ C) & 0xff == (k1 & 0xff))
elif f == 'f4':
C = ZeroExt(24, x[c]); s.add(((A ^ B) + C) & 0xff == (k1 & 0xff))
elif f == 'f5':
C, D = ZeroExt(24, x[c]), ZeroExt(24, x[d])
s.add((((A + B) ^ C) - D) & 0xff == (k1 & 0xff))
elif f == 'f6':
s.add((A ^ B) & 0xff == ((k1 ^ k2) & 0xff))
elif f == 'f7':
s.add((A + B) & 0xff == ((k1 - k2) & 0xff))
elif f == 'f8':
s.add((A - B) & 0xff == ((k1 + k2) & 0xff))
assert s.check() == sat
m = s.model()
inner = ''.join(chr(m[xi].as_long()) for xi in x)
print('CBC{' + inner + '}')
Carbonara — jump-obfuscation and data-flow recovery
Summary
- Type: Reversing (ELF64, PIE, not stripped)
- Challenge:
MRMCDCTF 2019 - Carbonara - Difficulty: medium
- Recovered flag:
MRMCDCTF{spaghetti_code_for_breakfast}
Challenge pattern
The binary uses control-flow obfuscation by splitting logic into tiny instruction chunks and chaining them with pairs of opposite conditional jumps to the same destination.
Typical shape:
- one real instruction
jc targetjnc target
(or jo/jno, jp/jnp, etc.)
So one branch is always taken, and either way goes to the same place. This makes decompilers produce noisy pseudocode, but execution is still deterministic.
Practical solve approach
Instead of fully recovering CFG, I followed data flow:
- run binary and locate input path (
fgets->strtok->strlen), - inspect transformed bytes in memory,
- invert the byte transform used by checker.
Encoded qwords observed:
0x88968586858f948f
0xb6a7aaa9a3b2b5bd
0xa1a7a6b1a5a1abb6
0xa3a7b4a4a1b4b1a8
0x0042bfb6b5a3a8ad
Interpreting as little-endian bytes and applying inverse transform (- 0x42) yields the flag string.
Minimal repro solver
#!/usr/bin/env python3
vals = [
0x88968586858F948F,
0xB6A7AAA9A3B2B5BD,
0xA1A7A6B1A5A1ABB6,
0xA3A7B4A4A1B4B1A8,
0x0042BFB6B5A3A8AD,
]
buf = b"".join(v.to_bytes(8, "little") for v in vals)
enc = buf.split(b"\x00", 1)[0]
plain = bytes((b - 0x42) & 0xFF for b in enc).split(b"\x00", 1)[0]
print(plain.decode())
Expected output:
MRMCDCTF{spaghetti_code_for_breakfast}
Takeaway
If CFG is intentionally noisy, follow data transformations first. It is often the fastest path to a deterministic recovery.