Description
What integer does this program print with arguments 2541761492 and 4030728319? Analyze the ARM assembly to compute the output.
Setup
Download the ARM assembly source file.
wget https://mercury.picoctf.net/static/.../chall_3.Scat chall_3.SSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Read the source: a tiny dispatch into func1 and func2ObservationI noticed the challenge provides an ARM assembly source file and asks for the printed integer given a specific argument, which suggested reading the .S file to understand how func1 and func2 interact before attempting any computation.main calls atoi(argv[1]) once, passes the result to func1, and prints whatever func1 returns. The second argument the prompt mentions is unused. func1 is the loop you need to translate, func2 is a one-liner.bashcat chall_3.S | sed -n '/^func1:/,/.size\s*func1/p'bashcat chall_3.S | sed -n '/^func2:/,/.size\s*func2/p'Expected output
decimal=42 hex=0x0000002a
What didn't work first
Tried: Running the .S file directly with bash or as a script to see what it prints
The file is ARM assembly source, not x86 machine code or a shell script. Running it on an x86 host produces 'Exec format error' or garbled output. You need to either cross-compile with aarch64-linux-gnu-gcc and emulate with qemu-aarch64, or translate the logic manually into Python.
Tried: Grepping for the return value or a print statement to read the answer directly from the source
The .S file prints a runtime-computed result via printf, not a hardcoded string. Grepping for 'Result' or 'answer' only finds the format string literal. The actual number comes from func1's computation on the argv[1] input, so you must trace the logic rather than search for a static value.
Learn more
The relevant excerpt of
func1:func1: str w0, [x29, 28] ; n = arg str wzr, [x29, 44] ; result = 0 b .L2 .L4: ldr w0, [x29, 28] and w0, w0, 1 ; if (n & 1) cbz w0, .L3 ldr w0, [x29, 44] bl func2 ; result = func2(result) str w0, [x29, 44] .L3: ldr w0, [x29, 28] lsr w0, w0, 1 ; n >>= 1 str w0, [x29, 28] .L2: ldr w0, [x29, 28] cbnz w0, .L4 ; while (n != 0) ldr w0, [x29, 44] retfunc2is justx + 3.Translated: walk the bits of
nfrom low to high. Each set bit contributes one call tofunc2(result), which adds 3. So the final return value ispopcount(n) * 3.See the Ghidra reverse engineering guide if you want the GUI walkthrough instead.
Step 2
Compute the answer in one Python lineObservationI noticed that func1's loop tests bit 0 and shifts right on each iteration while calling func2 for each set bit, and func2 simply adds 3; this identified the entire computation as popcount(n) * 3, which can be evaluated instantly in Python rather than emulating the assembly.The result is bin(arg).count('1') * 3 with 32-bit masking. Pass your instance's argument and read the printed hex.pythonpython3 -c "arg = 2541761492; r = (bin(arg & 0xFFFFFFFF).count('1') * 3) & 0xFFFFFFFF; print(f'decimal={r} hex={r:#010x}')"bash# arg=2541761492 -> 42 (0x0000002a)bash# arg=4030728319 -> 39 (0x00000027) (in case your instance uses this one)What didn't work first
Tried: Counting set bits in the input and multiplying by 1 instead of 3 because func2 was not checked
func1 calls func2 on each set bit, and func2 returns x + 3, not x + 1. Skipping the func2 analysis gives popcount(n) instead of popcount(n) * 3. For arg=2541761492 this produces 14 instead of 42, which is a valid-looking small number but maps to the wrong hex string and a wrong flag.
Tried: Submitting the decimal result (42) directly as the flag instead of converting to an 8-digit lowercase hex string
The program prints the result formatted as a hex string, not a decimal. The flag is picoCTF{} wrapping the 8-character zero-padded lowercase hex form (e.g. 0000002a), not the decimal integer 42. Submitting the raw decimal is the most common last-step mistake on ARMssembly challenges.
Learn more
Why 32-bit masking matters here: ARMv8
w0is a 32-bit register, andaddwraps modulo 232.popcount * 3for any 32-bit input maxes at32 * 3 = 96, so the wrap doesn't kick in. The mask is defensive boilerplate; copy it because you'll need it on the next ARMssembly challenge where wrapping does matter.Alternative: just emulate the binary. If hand-translating feels brittle, build and run with QEMU:
aarch64-linux-gnu-gcc -static -o chall_3 chall_3.S && qemu-aarch64 ./chall_3 2541761492. The output lineResult: Nis your answer; convert N to 8-hex-digit lowercase for the flag.See Python for CTF for more reverse-engineering one-liners and Pwntools for CTF for QEMU automation.
Flag
Reveal flag
picoCTF{0000002a}
Per-instance challenge. The reference computation shows arg=2541761492 -> popcount=14 -> 14*3=42 -> hex 0000002a. Different picoCTF instances get different arguments and thus different flags.