ARMssembly 0 picoCTF 2021 Solution

Published: April 2, 2026

Description

What integer does this ARM assembly program return when given arguments 182476535 and 3742084308? Flag format: picoCTF{XXXXXXXX} - 8 lowercase hex characters representing a 32-bit value.

Download chall.S.

bash
wget <url>/chall.S

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Read the assembly and identify the pattern
    Observation
    I noticed the challenge provides a raw AArch64 assembly source file and asks for the return value given two integer inputs, which suggested the first step was to open chall.S and trace the register flow through its comparison and branch instructions.
    Open chall.S. The function takes two 32-bit args in w0 and w1, compares them with cmp, and returns the larger via a conditional branch. That's max(a, b).
    bash
    less chall.S
    What didn't work first

    Tried: Treat the two arguments as x86 registers instead of ARM w-registers and try to trace execution on an x86 mental model.

    AArch64 uses a completely different register file and calling convention from x86-64. Arguments arrive in x0-x7 (not rdi, rsi), and the w-prefix is the 32-bit alias of the corresponding x-register - not an independent register. Applying x86 intuitions leads to misreading which value is in which register, producing wrong results.

    Tried: Run objdump on chall.S directly to disassemble it.

    chall.S is already a human-readable assembly source file, not an ELF binary. objdump expects a compiled binary as input and will error or produce garbage when handed a raw .S file. You need to read the source with cat or less, or assemble it first with aarch64-linux-gnu-gcc before disassembling.

    Learn more

    AArch64 ARM (64-bit ARM) passes the first four integer arguments in registers x0 through x3. The w-prefixed registers (w0, w1) are the lower 32 bits of the corresponding x registers. The assembly uses cmp to set flags, then a conditional branch to pick the larger value and store it in w0 (the return value register).

    How cmp works: The cmp w0, w1 instruction subtracts w1 from w0 and discards the result, but updates the processor's condition flags: N (negative), Z (zero), C (carry), and V (overflow). Subsequent conditional branch instructions like b.gt (branch if greater than), b.le (branch if less than or equal), or b.eq (branch if equal) read these flags. The combination of cmp followed by a conditional branch is the ARM equivalent of an if statement.

    ARM is everywhere: AArch64 (ARM64) runs all Apple Silicon Macs, all modern Android phones, and most embedded systems. ARM CTF challenges are good practice for real-world mobile and embedded security work. For deeper static analysis on these binaries, see the Ghidra reverse engineering guide.

  2. Step 2
    Pick the larger argument and convert to hex
    Observation
    I noticed the assembly implements max(a, b) via a cmp followed by a conditional branch, so I needed to evaluate max(182476535, 3742084308) and then format the 32-bit result as exactly 8 lowercase hex digits to match the flag format.
    max(182476535, 3742084308) = 3742084308. Convert to hex with Python and zero-pad to 8 chars.
    python
    python3 -c "print(f'{max(182476535, 3742084308):08x}')"

    Expected output

    df0bacd4
    What didn't work first

    Tried: Return the first argument (182476535) because it appears first in the function signature.

    The cmp instruction compares the two values and the conditional branch selects the larger one - not the first. 3742084308 is larger than 182476535, so the branch routes execution to return the second argument. Confusing argument order with magnitude is the most common mistake on max/min assembly problems.

    Tried: Format the result as uppercase hex or without zero-padding, like 'DF0BACD4' or 'df0bacd4' without a length specifier.

    The flag format is explicitly 8 lowercase hex characters representing a 32-bit value. :08x in the Python format string enforces both lowercase and zero-padding to 8 digits. Submitting uppercase or without padding will be rejected by the judge even though the numeric value is correct.

    Learn more

    f'{n:08x}' formats n as lowercase hex padded to 8 digits. For 3742084308 this gives df0bacd4.

  3. Step 3
    Verify by running the binary under QEMU
    Observation
    I noticed the static analysis answer depends on correctly reading the branch condition, so I wanted to cross-compile chall.S with aarch64-linux-gnu-gcc and run it under qemu-aarch64-static to confirm the computed return value before submitting.
    Cross-compile chall.S to an AArch64 binary, run it under qemu-aarch64-static, and inspect the return value. The exit code is masked to 8 bits, so add a tiny printf wrapper to see the full 32-bit value.
    bash
    aarch64-linux-gnu-gcc -static -o chall chall.S
    bash
    qemu-aarch64-static ./chall 182476535 3742084308; echo $?
    What didn't work first

    Tried: Read the exit code directly as the answer after 'echo $?' and submit it as the flag.

    The shell exit code is a single byte (0-255), so it only captures the low 8 bits of the 32-bit return value. 0xdf0bacd4 has a low byte of 0xd4 = 212, so the exit code will be 212 - not the full answer. You need a printf wrapper or the Python computation to recover all 32 bits.

    Tried: Compile chall.S with gcc (native x86) instead of aarch64-linux-gnu-gcc and run it directly.

    gcc without a cross-compiler prefix targets the host architecture (x86-64). The assembler will reject AArch64 mnemonics like cmp w0, w1 with 'Error: unknown mnemonic' because x86-64 uses different register names and instructions. You must use the aarch64-linux-gnu-gcc cross-compiler toolchain to assemble ARM source files on an x86 host.

    Learn more

    QEMU emulates AArch64 binaries on an x86 host. Combined with aarch64-linux-gnu-gcc you can assemble and execute ARM code without physical ARM hardware. The exit code is truncated to 8 bits, so the bare program will print something like 212 (which is 0xd4, the low byte of 0xdf0bacd4) confirming the low byte of your computed answer. To see all 32 bits, write a small C wrapper that calls the function and printfs the result, or use Ghidra's AArch64 decompiler to confirm the logic statically.

Flag

Reveal flag

picoCTF{df0bacd4}

max(182476535, 3742084308) = 3742084308 = 0xdf0bacd4. Submit it inside the picoCTF{...} wrapper as the challenge expects.

Key takeaway

ARM assembly reverse engineering is a foundational skill for mobile and embedded security work. AArch64 follows a predictable register calling convention (arguments in x0-x7, return value in x0) and a small set of comparison and branch mnemonics that map directly onto C control flow. Recognizing patterns like cmp followed by a conditional branch as an if-statement, or a function that picks one of two inputs based on a comparison as min/max, lets you reconstruct high-level logic without executing the binary. The same pattern-matching approach applies to firmware analysis, Android native libraries, and iOS binaries.

Related reading

Want more picoCTF 2021 writeups?

Useful tools for Reverse Engineering

What to try next