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.
Setup
Download chall_1.S.
wget <url>/chall_1.SSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Trace the arithmetic in the assemblyObservationI noticed the challenge provides a raw ARM assembly source file chall_1.S and asks for the specific integer argument that causes a 'win' branch, which suggested manually tracing the data flow through the arithmetic instructions (lsl, udiv) to find the exact 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.bashcat chall_1.SWhat 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. Opening the .S directly gives a flat hex view with no decompiled output because Ghidra has no assembler to produce a binary. The correct approach is to read the .S file directly - it is already human-readable ARM source assembly, not a compiled binary.
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 stands for Logical Shift Left, not right - lsr is Logical Shift Right. Shifting left by 2 bits multiplies by 2^2 = 4, so 68 * 4 = 272, not 68 / 4 = 17. Checking the ARM instruction reference confirms: 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, then272 / 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, andlsl #3would 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
divoridivinstruction, which implicitly reads from and writes to specific registers (eax/edx). ARM'sudiv(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 issdiv. 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-asand run it underqemu-aarch64-staticto verify your manual calculation against actual execution.Step 2
Convert the winning argument to 8-digit hexObservationI noticed the flag format explicitly requires 8 lowercase hex characters representing the 32-bit argument, which suggested zero-padding the decimal result 90 to a full 8-digit hex value rather than submitting the 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.pythonpython3 -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 explicitly requires 8 lowercase hex characters representing the argument as a 32-bit value. 90 decimal is 0x5a, which is only 2 hex digits. Without zero-padding to 8 digits the submission picoCTF{5a} will be rejected. The python3 format spec '{:08x}' applies zero-padding to exactly 8 digits, yielding 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.
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.