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.
Setup
Download chall.S.
wget <url>/chall.SSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Read the assembly and identify the patternObservationI 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).bashless chall.SWhat 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
x0throughx3. Thew-prefixed registers (w0, w1) are the lower 32 bits of the correspondingxregisters. The assembly usescmpto set flags, then a conditional branch to pick the larger value and store it inw0(the return value register).How
cmpworks: Thecmp w0, w1instruction 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 likeb.gt(branch if greater than),b.le(branch if less than or equal), orb.eq(branch if equal) read these flags. The combination ofcmpfollowed by a conditional branch is the ARM equivalent of anifstatement.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.
Step 2
Pick the larger argument and convert to hexObservationI 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.pythonpython3 -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}'formatsnas lowercase hex padded to 8 digits. For 3742084308 this givesdf0bacd4.Step 3
Verify by running the binary under QEMUObservationI 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.bashaarch64-linux-gnu-gcc -static -o chall chall.Sbashqemu-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-gccyou 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 like212(which is0xd4, the low byte of0xdf0bacd4) 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.