Skip to main content

ARMssembly 1 picoCTF 2021 Solution

Analyze ARM32 assembly to manually trace arithmetic operations and determine the resulting value.

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

Description

For what argument does this ARM program print 'win'? Flag format: picoCTF{XXXXXXXX} - 8 lowercase hex characters representing the argument as a 32-bit value.

Download chall_1.S.

bash
wget <url>/chall_1.S

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Trace the arithmetic in the assembly
    Observation
    The challenge gives a raw ARM assembly file, chall_1.S, and asks which integer argument reaches the win branch. Tracing the data flow through the arithmetic instructions, lsl and udiv, finds the constant the input is compared against.
    Open chall_1.S and read the comparison logic. The program computes a target constant through two operations: it starts with 68, shifts left by 2 (multiply by 4), giving 272, then performs integer division by 3, giving 90. The program prints 'win' when the argument equals 90.
    bash
    cat chall_1.S
    What didn't work first

    Tried: Decompile chall_1.S with Ghidra to read the logic instead of reading the raw assembly

    Ghidra expects a binary, ELF or PE, not a raw .S source file. Open the .S directly and you get a flat hex view with no decompilation, because Ghidra has no assembler to build a binary from it. Just read the file: it is already human-readable ARM assembly.

    Tried: Interpret lsl #2 as a shift right (divide by 4) instead of a shift left (multiply by 4), arriving at 68/4=17 as the target constant

    lsl is Logical Shift Left; lsr is the right shift. Shifting left by 2 bits multiplies by 4, so 68 becomes 272, not 17. The ARM instruction reference is unambiguous: lsl #n is always a left shift, which is multiplication by a power of two.

    Learn more

    Logical shift left (lsl #2) multiplies by 2^2 = 4. Unsigned division (udiv) performs integer division truncating toward zero. So: 68 * 4 = 272, then 272 / 3 = 90 (integer division truncates toward zero; the true quotient is 90.67 but the fractional part is discarded). The assembly compares the result with the input argument and branches to the win or lose label accordingly.

    Reading ARM assembly requires recognizing the calling convention: arguments come in w0/x0, and immediate values embedded in instructions are the constants the program uses for its computation. Tracing data flow from input through operations to the final comparison is the core skill.

    Why shift left instead of multiply? On older processors, integer multiplication was a slow operation (multiple clock cycles), while a bitwise shift was a single-cycle instruction. Compilers routinely replace multiplications by powers of two with shift instructions as an optimization. lsl #2 (logical shift left by 2) is equivalent to multiplying by 4, and lsl #3 would be multiply by 8. When reading compiled assembly, always translate shifts back to their multiplication equivalents to understand the original arithmetic.

    ARM vs x86 differences: In x86 assembly, integer division uses the div or idiv instruction, which implicitly reads from and writes to specific registers (eax/edx). ARM's udiv (unsigned divide) takes explicit source and destination register operands, making it easier to trace. The "u" prefix means unsigned - the operands are treated as non-negative integers. The signed equivalent is sdiv. Choosing the wrong variant when analyzing code produces incorrect results if the values could be negative.

    Approach when the logic is more complex: For straightforward arithmetic like this challenge, manual tracing is fastest. For more complicated control flow (nested branches, loops), use a disassembler like Ghidra or radare2. You can cross-compile the .S file with aarch64-linux-gnu-as and run it under qemu-aarch64-static to verify your manual calculation against actual execution.

  2. Step 2Convert the winning argument to 8-digit hex
    Observation
    The flag format wants 8 lowercase hex characters for the 32-bit argument. So zero-pad the decimal result 90 out to a full 8-digit hex value, rather than submitting a bare integer or an unpadded hex string.
    The winning argument is 90 decimal. Convert to a 32-bit hex value zero-padded to 8 digits: 0x0000005a.
    python
    python3 -c "print(f'{90:08x}')"

    Expected output

    0000005a
    What didn't work first

    Tried: Submit the flag as picoCTF{90} or picoCTF{5a} without zero-padding to 8 hex digits

    The flag format wants 8 lowercase hex characters for a 32-bit value, and 90 decimal is 0x5a, just two digits. Without padding, picoCTF{5a} is rejected. Python's '{:08x}' pads to exactly 8 digits, giving 0000005a.

    Tried: Use uppercase hex formatting with '{:08X}' and submit picoCTF{0000005A}

    The flag format specifies lowercase hex characters. Using '{:08X}' produces 0000005A with a capital A, which will not match the expected flag. The correct format spec is '{:08x}' (lowercase x) to produce 0000005a.

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{0000005a}

Follow the shift-left and integer-divide operations: 68*4=272, 272/3=90. The program checks if the argument equals this computed constant.

Key takeaway

Compilers routinely rewrite source arithmetic into equivalent but structurally different assembly. Multiplication by a power of two becomes a logical shift, and division by a small constant often becomes a multiply-and-shift sequence. Reversing those idioms is essential for static analysis without source, whether the target is a CTF crackme, a license check, or production firmware. Knowing that lsl #n multiplies by 2^n, and udiv truncates, lets you reconstruct the original arithmetic in one pass.

Related reading

Useful tools for Reverse Engineering

Where to go next