Logoflag1
Logoflag1
Platform: CUSTOM | Category: Reversing | Type: Challenge | Difficulty: Easy | OS: N/A | **Author: XESXOR Techniques: cross_platform_binary_verification_via_docker, manual_equation_extraction_from_asm, static_disassembly_analysis, z3_smt_solving
Summary
Walkthrough of the CUSTOM Reversing Logoflag1. Task: 64-bit ELF (rev-kids20.forkbomb.ru task1) reads a string via gets() and passes it to a C++ test(char*) function that checks length==8 then 8 nonlinear integer equations over the input bytes. Sol
Description
(Source: rev-kids20.forkbomb.ru, task1) "Just give a valid flag." Binary download: https://rev-kids20.forkbomb.ru/files/rev/re8/task1
English summary: A single ELF binary (task1) is given with no other artifacts. It reads a line of input and reports "Valid flag" or "Invalid flag". The goal is to find an input string that the binary accepts as valid — that string itself is the flag (no spbCTF{...} wrapper was present anywhere in the binary).
Analysis
file task1→ 64-bit ELF, x86-64, dynamically linked, not stripped, BuildID present. Not stripped meant symbol names (including the C++ mangledtestfunction) were directly visible inobjdump -d.mainreads user input into a stack buffer usinggets()— a classic unbounded-read primitive that would normally suggest a stack buffer overflow / ret2win path. This was considered as a candidate attack family, but it turned out to be a red herring / unused primitive: there is nowin()function, no interesting gadget target, and the challenge is entirely a keygen-style algorithmic check.gets()here is simply lazy/unsafe input handling, not the intended exploitation vector. (Useful negative-hypothesis note: presence of an unsafe input function does not always mean the solve path is memory corruption — always check whether a simpler "satisfy the validator" path exists first.)- After reading input,
maincalls_Z4testPc(Itanium C++ mangled name, demangles totest(char*)) with the input pointer and branches on the boolean return value to print "Valid flag" / "Invalid flag". - Disassembling
test(char*):- First check:
strlen(input) == 8. Any other length returns false immediately — this pins the flag length at exactly 8 characters. - The function then evaluates a sequential chain of 8 blocks. Each block:
- Loads several input bytes via
movzx/movsx(sign-extendingchartoint), - Computes a small nonlinear arithmetic expression (
imul,add,sub) combining 2–4 of the bytes, - Compares the result against a hardcoded 32-bit immediate with
cmp, - On mismatch, calls
printfwith some debug/intermediate value and returnsfalse— this printed value is NOT itself a required condition, it's just diagnostic output on the failure path of that specific equation and can be ignored for solving. - On match, falls through to the next block.
- Loads several input bytes via
- Only if all 8 comparisons succeed does execution reach
mov eax, 1at address0x400ba3(success/return true).
- First check:
- Reading the immediates and byte indices directly off the disassembly yielded 8 nonlinear equations over
s0..s7(the 8 input bytes as signed ints):
E1: s0 + 2*s5*s7 == 0x194e (6478)
E2: s1 + s2 + s0*s4 == 0x203b (8251)
E3: s0*s2 + s3 - s5 == 0x14e4 (5348)
E4: s0*s3 + s2 + s6 == 0xf14 (3860)
E5: s1 - s2 - s0*s3 == -3747 (0xfffff15d as 32-bit signed)
E6: s0 + s3 + s5*s6 == 0x191e (6430)
E7: s1 + s6 + s0*s2 == 0x1585 (5509)
E8: s2*s3*s4 + s0*s5 - s2 == 0x5caa2 (380066)
This is a small nonlinear Diophantine-style system — not solvable cleanly by hand due to the products of variables (E8 has a triple product), but trivial for an SMT solver once each byte is bounded to a sane range (printable ASCII).
Solution
- Downloaded the binary and ran
file/objdump -d -M intel task1to disassemble. - Located
main→gets()→ call to_Z4testPc, and disassembledtest(char*)fully (saved totest_func.txt). - Manually walked each of the 8 comparison blocks, extracting which bytes participate, the arithmetic operation, and the target immediate, translating each into an algebraic equation as shown above.
- Modeled the 8 equations in Z3 with each byte constrained to
[32, 126](printable ASCII range) to keep the search space small and force a human-readable solution. - Solved instantly with Z3 → byte values
[76, 48, 71, 49, 107, 97, 65, 33]→ ASCII stringREDACTED. - Verified correctness: since the binary is a Linux x86-64 ELF and the analysis host is Apple Silicon macOS, ran it inside a Docker container (
--platform linux/amd64 ubuntu:22.04) and piped the candidate string to stdin:
Output:docker run --rm -v <taskdir>:/w -w /w --platform linux/amd64 ubuntu:22.04 \ bash -c "echo 'REDACTED' | ./task1"Valid flag— confirming the recovered string satisfies all 8 constraints and the length check.
python read-only • copy allowed#!/usr/bin/env python3 from z3 import * s = [Int(f's{i}') for i in range(8)] solver = Solver() for x in s: solver.add(x >= 32, x <= 126) s0,s1,s2,s3,s4,s5,s6,s7 = s solver.add(s0 + 2*s5*s7 == 0x194e) solver.add(s1 + s2 + s0*s4 == 0x203b) solver.add(s0*s2 + s3 - s5 == 0x14e4) solver.add(s0*s3 + s2 + s6 == 0xf14) solver.add(s1 - s2 - s0*s3 == -3747) solver.add(s0 + s3 + s5*s6 == 0x191e) solver.add(s1 + s6 + s0*s2 == 0x1585) solver.add(s2*s3*s4 + s0*s5 - s2 == 0x5caa2) if solver.check() == sat: m = solver.model() vals = [m[x].as_long() for x in s] print(vals) print(''.join(chr(v) for v in vals)) else: print("unsat")
Key Takeaways / Lessons
- Mastering cross_platform_binary_verification_via_docker, manual_equation_extraction_from_asm, static_disassembly_analysis, z3_smt_solving was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-07-01 • CUSTOM • Logoflag1 • Difficulty: Easy