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: August 13, 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 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 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 or without zero-padding, like 'DF0BACD4' or 'df0bacd4' without a length specifier.

    The flag format is 8 lowercase hex characters for a 32-bit value. Python's :08x gives you both the lowercase and the zero-padding. Submit uppercase, or without padding, and the judge rejects it even though the number is right.

    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{df0bacd4}

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

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