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 <url>/chall_3.Scat chall_3.SSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Read the source: a tiny dispatch into func1 and func2
ObservationThe challenge gives an ARM assembly source file and asks what integer it prints for a given argument. Read the .S file first to see how func1 and func2 interact, before computing anything.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 and not a shell script, so running it on an x86 host gives an exec format error or garbled output. Either cross-compile with aarch64-linux-gnu-gcc and emulate under qemu-aarch64, or translate the logic 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 through printf, not a hardcoded string, so grepping for 'Result' or 'answer' only turns up the format string. The number comes out of func1's computation on argv[1], so trace the logic rather than searching 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 2Compute the answer in one Python line
Observationfunc1's loop tests bit 0 and shifts right each iteration, calling func2 on every set bit, and func2 just adds 3. The whole computation is popcount(n) * 3, which Python evaluates instantly with no emulation.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. Skip that step and you get popcount(n) rather than popcount(n) * 3. For 2541761492 that is 14 instead of 42, a plausible-looking number that 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 its result as hex, not decimal. The flag wraps the 8-character zero-padded lowercase hex form, 0000002a, not the decimal 42. Submitting the raw decimal is the classic last-step mistake on these 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.
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{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.