Angry1
Angry1
Platform: spbctf | Category: Reversing | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-07-01 | Status: Solved Techniques: disassembly_emulation, per_character_constraint_solving, objdump_parsing, angr_symbolic_execution_attempt, magic_constant_division_emulation
Summary
Task: not-stripped x86-64 C crackme that reads 36 bytes and checks each byte independently against a hard-coded constant via a short affine arithmetic chain, calling exit(0) on any mismatch. Solution: parse objdump of main into 36 per-byte check blocks, emulate the register-level x86-64 arithmetic (including compiler magic-constant signed division), and brute-force each byte 0..255 for the printable solution.
Angry1 — spbctf
Description
Angry1 — reverse (SPbCTF / forkbomb rev-kids). Binary:
t1_t2_t3, download: https://rev-kids20.forkbomb.ru/files/rev/re8/t1_t2_t3
A classic x86-64 crackme. The program reads input from stdin and prints
welldone! only if the input is correct. Recover the accepted input (the flag).
The flag itself is the joke: REDACTED — every byte is an
independent, trivially solvable equation, so you could do it by hand, but a
tiny emulator is faster.
Recon
$ file t1_t2_t3
ELF 64-bit LSB executable, x86-64, dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2, not stripped, GCC Ubuntu 5.4.0
$ strings t1_t2_t3 | grep -iE 'welldone|flag|correct|wrong'
welldone! # the ONLY interesting string
$ nm t1_t2_t3 | grep -iE ' main| read| puts| exit'
0000000000400616 T main
U read@@GLIBC_2.2.5
U puts@@GLIBC_2.2.5
U exit@@GLIBC_2.2.5
mainat0x400616is the only user function; imports areread,puts,exit,__stack_chk_fail.- No
flag/correct/wrongstrings — onlywelldone!(putsat0x401273), and there is no early feedback: on any failed byte the program simply callsexit@plt(0x400500).
Analysis
main does:
read(0, buf, 0x40) ; read up to 0x40 bytes into a stack buffer
; then 36 independent per-byte check blocks
Each check block has a fixed shape:
mov dword ptr [rbp-0x120], K— load the target constantKfor this byte.movzx eax, byte ptr [rbp-OFF]— load the input byte (OFFselects the byte; byte index =0x110 - OFF, so byte 0 lives atrbp-0x110).- A short chain of arithmetic on
eax(add / sub /shl/imul/lea, and for some bytes a compiler magic-constant division). cmp eax, dword ptr [rbp-0x120]— compare the computed value toK.jne/je→ on mismatch jump toexit@plt; on match fall through to the next block. After all 36 blocks pass, execution reachesputs("welldone!").
Because each byte is processed in isolation and compared to a constant, every check is a single-variable affine equation — solvable independently.
Concrete hand-decoded blocks
byte 0 [rbp-0x110]: (c + 3 - 2 + 3) == 0x6a (106) => c = 102 = 'f'
byte 1 [rbp-0x10f]: ((c + 0x1f)*2 - 9) == 0x10d (269) => c = 108 = 'l'
byte 2 [rbp-0x10e]: ((c + 3 - 2)*7) == 0x2ae (686) => c = 97 = 'a'
byte 3 [rbp-0x10d]: (3*c - 0x19 + 0x38) == 0x154 (340) => c = 103 = 'g'
byte 4 [rbp-0x10c]: (2*c + 0x11) == 0x107 (263) => c = 123 = '{'
Compiler magic-constant division
Some later blocks divide by a small constant. GCC emits this as a signed high-multiply, e.g.:
mov edx, 0x6bca1af3 ; magic reciprocal
imul edx ; edx:eax = eax * magic (signed 64-bit)
sar edx, 0x3 ; take the high dword, arithmetic shift
sar eax, 0x1f ; sign fixup
sub edx, eax
This must be emulated faithfully as signed 32-bit arithmetic: imul (one-operand
form) produces the 64-bit product in edx:eax, and only the high dword (edx)
is used after the shift. Naively treating it as a plain multiply gives wrong
answers.
Solution
Failed / slow path: angr
First attempt was symbolic execution — set find = 0x401273 (the welldone!
puts) and explore:
python read-only • copy allowedproj = angr.Project('t1_t2_t3', auto_load_libs=False) inp = claripy.BVS('inp', 8*0x40) st = proj.factory.entry_state(stdin=inp) simgr = proj.factory.simulation_manager(st) simgr.explore(find=0x401273) # TIMES OUT (>2 min)
This timed out (>2 minutes). Each of the 36 checks forks a
success/fail branch, so blind explore() suffers combinatorial path explosion.
(You can rescue angr by feeding the target byte-by-byte with per-check
constraints, but the emulation approach below is much simpler here.)
Working path: parse + emulate + brute-force
Because every byte is independent, dump objdump -d -M intel main, split it into
the 36 per-byte blocks, capture each block's target constant K, emulate the
register arithmetic of the block, and brute-force the input byte c over
0..255, keeping the printable solution.
Parser gotchas worth noting:
- objdump lines are tab-separated (
addr:\tbytes\tmnemonic\toperands). A greedy regex that strips the hex-byte column mis-parses multi-bytemov ..., IMMinstructions — split on tabs instead. - Track the last
mov dword ptr [rbp-0x120], Kas the current target; a block starts atmovzx eax, byte ptr [rbp-OFF]and ends at the followingcmp. - Handle the temp spill/reload slots (
[rbp-0x11c],-0x118,-0x114) the compiler uses for intermediate values.
Solver code
python read-only • copy allowed#!/usr/bin/env python3 # objdump -d -M intel t1_t2_t3 > main.asm (main function only) import re insns = [] for l in open('main.asm').read().splitlines(): if '\t' not in l: continue p = l.split('\t') if len(p) < 2: continue mn = p[1].strip() ops = p[2].strip() if len(p) > 2 else '' if re.match(r'[a-z]', mn): insns.append((mn, ops)) def imm(s): s = s.strip().rstrip(',') return int(s, 16) if s.startswith('0x') else int(s) def s32(v): v &= 0xffffffff return v - 0x100000000 if v >= 0x80000000 else v # split into (OFF, K, ops) check blocks blocks, i, curK = [], 0, None while i < len(insns): mn, op = insns[i] if mn == 'mov' and op.startswith('dword ptr [rbp - 0x120],'): curK = imm(op.split(',')[-1]); i += 1; continue if mn == 'movzx' and 'byte ptr' in op: off = imm(re.search(r'\[rbp - (0x[0-9a-f]+)\]', op).group(1)) j, ops = i + 1, [] while j < len(insns) and insns[j][0] != 'cmp': ops.append(insns[j]); j += 1 blocks.append((off, curK, ops)); i = j + 1; continue i += 1 mem = {} def eval_lea(expr, A, D): e = expr.replace(' ', '') for r in ('rax', 'eax'): e = e.replace(r, 'A') for r in ('rdx', 'edx'): e = e.replace(r, 'D') return eval(e, {'A': A, 'D': D}) def emulate(c, ops): reg = {'eax': c, 'edx': 0, 'ecx': 0} for mn, o in ops: parts = [x.strip() for x in o.split(',')] if mn in ('movsx', 'movzx'): reg[parts[0]] = c if c < 128 else c - 256 # signed load elif mn == 'mov': dst, src = parts[0], parts[1] if 'ptr' in dst: # spill temp slot = dst.split('[')[1].split(']')[0] mem[slot] = reg.get(src); continue if 'ptr' in src: # reload temp reg[dst] = mem[src.split('[')[1].split(']')[0]]; continue reg[dst] = imm(src) if src.startswith('0x') or src.lstrip('-').isdigit() else reg[src] elif mn == 'add': reg[parts[0]] += reg[parts[1]] if parts[1] in reg else imm(parts[1]) elif mn == 'sub': reg[parts[0]] -= reg[parts[1]] if parts[1] in reg else imm(parts[1]) elif mn == 'shl': reg[parts[0]] <<= imm(parts[1]) elif mn == 'sar': reg[parts[0]] = s32(reg[parts[0]]) >> imm(parts[1]) elif mn == 'imul': if len(parts) == 3: # imul r, r, imm reg[parts[0]] *= imm(parts[2]) elif len(parts) == 2: # imul r, r reg[parts[0]] *= reg[parts[1]] else: # imul r -> edx:eax m = (reg['eax'] * (reg[parts[0]] if parts[0] in reg else imm(parts[0]))) & (2**64 - 1) reg['eax'] = s32(m & 0xffffffff) reg['edx'] = s32((m >> 32) & 0xffffffff) elif mn == 'lea': reg[parts[0]] = eval_lea(o.split(',', 1)[1].strip()[1:-1], reg['eax'], reg['edx']) return reg['eax'] result = [None] * 0x40 maxoff = 0 for off, K, ops in blocks: idx = 0x110 - off # byte 0 at rbp-0x110 found = [c for c in range(256) if (emulate(c, ops) & 0xffffffff) == (K & 0xffffffff)] printable = [c for c in found if 32 <= c < 127] or found if printable: result[idx] = printable[0] maxoff = max(maxoff, idx) flag = ''.join(chr(result[i]) if result[i] is not None else '?' for i in range(maxoff + 1)) print("blocks:", len(blocks)) print("FLAG:", flag)
Output:
blocks: 36
FLAG: flag{REDACTED}
Every one of the 36 positions resolves to a unique printable character.