Skip to main content

ARMssembly 0 picoCTF 2021 Solution

Read and trace ARM32 assembly by hand to determine the output of a simple comparison function.

Published: April 2, 2026Updated: September 22, 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 1Read the assembly and identify the pattern
    Observation
    The challenge gives a raw AArch64 assembly source file and asks what it returns for two integer inputs. So 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 has a completely different register file and calling convention from x86-64. Arguments arrive in x0 through x7, not rdi and rsi, and a w-prefixed name is the 32-bit alias of the matching x-register rather than a separate one. Bringing x86 intuitions along means misreading which value sits where.

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

    chall.S is already readable assembly source, not an ELF binary. objdump expects a compiled binary and will error or print garbage when handed a raw .S file. Read the source with cat or less, or assemble it with aarch64-linux-gnu-gcc first.

    Learn more

    AArch64 ARM (64-bit ARM) passes the first eight integer arguments in registers x0 through x7. 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 2Pick the larger argument and convert to hex
    Observation
    The assembly implements max(a, b) with a cmp and a conditional branch. So evaluate max(182476535, 3742084308), then format the 32-bit result as exactly 8 lowercase hex digits for the flag.
    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 compares the two values and the branch picks the larger one, not the first. 3742084308 is larger than 182476535, so execution returns the second argument. Confusing argument order with magnitude is the classic mistake on max and min assembly problems.

    Tried: Format the result as uppercase hex, like 'DF0BACD4', or leave a '0x' prefix on it from hex().

    The flag format is 8 lowercase hex characters for a 32-bit value. This particular value already fills all 8 digits, so padding is not the trap here; case and the 0x prefix are. Python's :08x gives lowercase, no prefix, and the zero-padding you will need on the smaller values in the rest of the series.

    Learn more

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

  3. Step 3Verify by running the binary under QEMU
    Observation
    The whole answer hinges on reading the branch condition correctly. Cross-compiling chall.S with aarch64-linux-gnu-gcc and running it under qemu-aarch64-static confirms the value before you submit.
    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.

    A shell exit code is a single byte, 0 to 255, so it carries only the low 8 bits of a 32-bit return value. The low byte of 0xdf0bacd4 is 0xd4, or 212, which is what you see instead of the answer. Use a printf wrapper or the Python computation to get 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. The assembler rejects AArch64 mnemonics like cmp w0, w1 with an unknown-mnemonic error, because x86-64 uses different registers and instructions. Assembling ARM source on an x86 host needs the aarch64-linux-gnu-gcc toolchain.

    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.

Interactive tools
  • Hex ViewerView text or raw hex bytes as a xxd-style hex dump with byte offset, hex columns, and ASCII sidebar. Highlights printable characters and null bytes.
  • Number Base ConverterConvert numbers between binary, octal, decimal, and hexadecimal instantly. Enter any value and see all four bases update in real time.
  • Endianness ConverterConvert between big-endian and little-endian byte order with visual byte layout. Supports 16-bit, 32-bit, and 64-bit words.

Flag

Reveal flag

picoCTF{df0b...}

max(182476535, 3742084308) = 3742084308 = 0xdf0bacd4. Submit it inside the picoCTF{...} wrapper as the challenge expects. The flag is shown abbreviated on this page; work the steps above to recover the full value. Your instance issues its own binary, so the value you compute will differ from the one shown here; only the shape of the answer transfers.

Key takeaway

ARM reverse engineering is foundational for mobile and embedded security work. AArch64 has a predictable calling convention, arguments in x0 through x7 and the return value in x0, plus a small set of comparison and branch mnemonics that map straight onto C control flow. Read a cmp plus conditional branch as an if-statement, or a two-input comparison as min or max, and you reconstruct the high-level logic without running anything. The same pattern-matching carries into firmware, Android native libraries, and iOS binaries.

Related reading

Useful tools for Reverse Engineering

Where to go next