← Back to Writeups
CUSTOMEasyReversing

Angry1

XESXOR9/2/20267 min read
#reversing#custom#easy#disassembly_emulation#per_character_constraint_solvi#objdump_parsing#angr_symbolic_execution_attemp#magic_constant_division_emulat

Angry1

Platform: CUSTOM | Category: Reversing | Type: Challenge | Difficulty: Easy | OS: N/A | **Author: XESXOR Techniques: disassembly_emulation, per_character_constraint_solving, objdump_parsing, angr_symbolic_execution_attempt, magic_constant_division_emulation

Summary

Walkthrough of the CUSTOM Reversing Angry1. 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:

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
  • main at 0x400616 is the only user function; imports are read, puts, exit, __stack_chk_fail.
  • No flag / correct / wrong strings — only welldone! (puts at 0x401273), and there is no early feedback: on any failed byte the program simply calls exit@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:

  1. mov dword ptr [rbp-0x120], K — load the target constant K for this byte.
  2. movzx eax, byte ptr [rbp-OFF] — load the input byte (OFF selects the byte; byte index = 0x110 - OFF, so byte 0 lives at rbp-0x110).
  3. A short chain of arithmetic on eax (add / sub / shl / imul / lea, and for some bytes a compiler magic-constant division).
  4. cmp eax, dword ptr [rbp-0x120] — compare the computed value to K.
  5. jne/je → on mismatch jump to exit@plt; on match fall through to the next block. After all 36 blocks pass, execution reaches puts("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
proj = 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-byte mov ..., IMM instructions — split on tabs instead.
  • Track the last mov dword ptr [rbp-0x120], K as the current target; a block starts at movzx eax, byte ptr [rbp-OFF] and ends at the following cmp.
  • Handle the temp spill/reload slots ([rbp-0x11c], -0x118, -0x114) the compiler uses for intermediate values. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solver code

python
#!/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. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering disassembly_emulation, per_character_constraint_solving, objdump_parsing, angr_symbolic_execution_attempt, magic_constant_division_emulation was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-07-01 • CUSTOM • Angry1 • Difficulty: Easy