ARMssembly 3 picoCTF 2021 Solution

Published: April 2, 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 https://mercury.picoctf.net/static/.../chall_3.S
bash
cat chall_3.S
  1. Step 1Read the source: a tiny dispatch into func1 and func2
    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'
    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
    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)
    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.

Flag

picoCTF{...}

func1 is popcount(n) * 3 and func2 just adds 3. For arg=2541761492, popcount=14, so result=42 in decimal. Different picoCTF instances seed different inputs; plug yours into the Python one-liner and submit the lowercase 8-hex-digit form.

Want more picoCTF 2021 writeups?

Useful tools for Reverse Engineering

Related reading

What to try next