Description
What integer does this program print with argument 3251107833? Analyze the ARM assembly, which includes looping and conditional logic.
Setup
Download the ARM assembly source file.
wget https://mercury.picoctf.net/static/.../chall_4.Scat chall_4.SSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Map the call graph: func1 picks one of two branchesObservationI noticed the file defines multiple small functions (func1 through func8) each with branch mnemonics like bls and bhi, which suggested tracing the full call graph from main outward before attempting any arithmetic, since the branching structure determines which formula applies to the given input.main calls func1(atoi(argv[1])). func1 splits on whether the input is <=100 or >100. The other small functions (func3, func4, func5, func7, func8) are all leaves; func6 is dead code (never called from this path).bashgrep -nE '^func[0-9]+:|cmp|bls|bhi|bl ' chall_4.SWhat didn't work first
Tried: Treat the bls/bhi branch conditions as signed comparisons (like C's <= and > on int) when translating to Python.
bls is 'branch if lower or same' using the unsigned carry/zero flags, and bhi is 'branch if higher' unsigned. Treating them as signed means inputs above 2^31 (like 3251107833) land in the wrong branch, producing a completely wrong answer. The fix is to mask the input to 32 bits with x &= 0xFFFFFFFF before every comparison.
Tried: Skip tracing func4 and assume it performs arithmetic on its argument because it calls func1 internally.
func4 does call func1(17) but immediately discards the return value and returns its own argument x untouched - making it an identity function. Assuming func4 returns func1(17) or some transform of x produces a wrong expression in the collapsed formula for the 100 < x <= 399 case.
Learn more
Branching summary, after walking each function:
func7(x) = (x > 100) ? x : 7 func8(x) = x + 2 func5(x) = func8(x) ; = x + 2 func4(x) = call func1(17), discard, return x ; identity func3(x) = func7(x) ; = (x > 100) ? x : 7 func2(x): if (unsigned) x > 499: return func5(x + 13) ; = x + 15 else: return func4(x - 86) ; = x - 86 func1(x): if (unsigned) x <= 100: return func3(x) ; = 7 else: return func2(x + 100)Note the unsigned compares. The branches use
bls(branch if lower-or-same, unsigned) andbhi(branch if higher, unsigned), so a Python translation must compare with the input masked to 32 bits, not as a signed int. Inputs above 231 are still positive in this world.Step 2
Collapse the call graph into three casesObservationI noticed that each leaf function reduced to a simple arithmetic expression (identity, +2, or a constant), which suggested substituting them bottom-up into func1 to get a single piecewise formula that could be evaluated directly in Python for any input, including the 32-bit-large argument 3251107833.Once func4 is identity and func5 is +2, every path through func1 reduces to a piecewise linear formula on the input.pythonpython3 - <<'EOF' def chall4(x): x &= 0xFFFFFFFF if x <= 100: return 7 elif (x + 100) <= 499: # 100 < x <= 399 return (x + 100 - 86) & 0xFFFFFFFF # = x + 14 else: # x > 399 (after +100 overflow check, none here) return (x + 100 + 15) & 0xFFFFFFFF # = x + 115 for arg in [50, 200, 600, 3251107833]: r = chall4(arg) print(f"arg={arg:<12} -> {r} (hex {r:#010x})") EOFExpected output
picoCTF{...}What didn't work first
Tried: Run the Python solver without masking intermediate results to 32 bits, treating all arithmetic as unbounded Python integers.
ARM registers are 32-bit and wrap on overflow. The input 3251107833 plus 115 overflows a 32-bit register, so the real program yields a wrapped result. Python's integers never overflow, so skipping the & 0xFFFFFFFF mask on the return value produces 3251107948 without wrapping - which happens to be correct here, but the approach breaks silently on inputs where the addition itself overflows 32 bits.
Tried: Submit the decimal result 3251107948 directly as the flag without converting it to an 8-digit lowercase hex string.
ARM assembly challenges on picoCTF ask for the printed register value, which the stub code outputs as a hex-formatted integer. The flag format requires the lowercase 8-hex-digit form (c1c7f86c) wrapped in picoCTF{}, not the decimal integer.
Learn more
Worked output:
arg=50 -> 7 (hex 0x00000007) arg=200 -> 214 (hex 0x000000d6) arg=600 -> 715 (hex 0x000002cb) arg=3251107833 -> 3251107948 (hex 0xc1c7f86c)
For arg=3251107833 the answer is
3251107948. Convert that to lowercase 8-hex-digit form (no0xprefix) and wrap withpicoCTF{...}for submission.Sanity check via QEMU if you doubt the trace:
aarch64-linux-gnu-gcc -static -o chall_4 chall_4.S && qemu-aarch64 ./chall_4 3251107833should printResult: 3251107948.See Python for CTF for the simulation idiom and Ghidra reverse engineering if you want a graphical view of the dispatch tree.
Flag
Reveal flag
picoCTF{c1c7f86c}
Per-instance challenge. The reference computation shows arg=3251107833 -> result=3251107948 -> hex c1c7f86c. Different picoCTF instances get different arguments and thus different flags.