Skip to main content

ARMssembly 3 picoCTF 2021 Solution

Trace ARM assembly code through loops and bitwise operations to determine the correct output for a given input.

Published: April 2, 2026Updated: August 25, 2026

Description

What integer does this program print with arguments 2541761492 and 4030728319? Analyze the ARM assembly to compute the output.

Download the ARM assembly source file.

bash
wget <url>/chall_3.S
bash
cat chall_3.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 source: a tiny dispatch into func1 and func2
    Observation
    The 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.
    bash
    cat chall_3.S | sed -n '/^func1:/,/.size\s*func1/p'
    bash
    cat 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]
        ret

    func2 is just x + 3.

    Translated: walk the bits of n from low to high. Each set bit contributes one call to func2(result), which adds 3. So the final return value is popcount(n) * 3.

    See the Ghidra reverse engineering guide if you want the GUI walkthrough instead.

  2. Step 2Compute the answer in one Python line
    Observation
    func1'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.
    python
    python3 -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 w0 is a 32-bit register, and add wraps modulo 232. popcount * 3 for any 32-bit input maxes at 32 * 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 line Result: N is 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.

Key takeaway

Bit manipulation patterns, especially testing and consuming bits one at a time with 'and reg, 1' followed by 'lsr reg, 1', turn up constantly in assembly on every architecture. Recognize it as a popcount loop, counting set bits, and the whole function collapses to a one-liner. The same shape drives CRC computation, hash functions, and cryptographic primitives, and lifting assembly loops into mathematical expressions is what separates fast reverse engineering from slow instruction-by-instruction tracing.

Related reading

Useful tools for Reverse Engineering

Where to go next