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 1Read the assembly and identify the pattern
ObservationThe 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).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 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
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 2Pick the larger argument and convert to hex
ObservationThe 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.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 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}'formatsnas lowercase hex padded to 8 digits. For 3742084308 this givesdf0bacd4.Step 3Verify by running the binary under QEMU
ObservationThe 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.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.
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-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.
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.