Description
What integer does this program print with argument 3251107833? Analyze the ARM assembly, which includes looping and conditional logic.
Setup
Download the ARM assembly source file.
wget <url>/chall_4.Scat chall_4.SSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Map the call graph: func1 picks one of two branches
ObservationThe file defines eight small functions, func1 through func8, each with branch mnemonics like bls and bhi. Trace the call graph from main outward before touching any arithmetic, because the branching decides which formula applies to a given input.main calls func1(atoi(argv[1])). func1 splits on whether the input is <=100 or >100. The other small functions (func3, func4, func5, func7, func8) are all leaves; func6 is dead code (never called from this path).bashgrep -nE '^func[0-9]+:|cmp|bls|bhi|bl ' chall_4.SWhat didn't work first
Tried: Treat the bls/bhi branch conditions as signed comparisons (like C's <= and > on int) when translating to Python.
bls is branch-if-lower-or-same and bhi is branch-if-higher, both unsigned, reading the carry and zero flags. Treat them as signed and any input above 2^31, such as 3251107833, takes the wrong branch and the answer is completely wrong. Mask the input to 32 bits with x &= 0xFFFFFFFF before every comparison.
Tried: Skip tracing func4 and assume it performs arithmetic on its argument because it calls func1 internally.
func4 does call func1(17), but it discards the return value and hands back its own argument untouched, which makes it an identity function. Assume it returns func1(17), or some transform of x, and the collapsed formula for the 100 to 399 case comes out wrong.
Learn more
Branching summary, after walking each function:
func7(x) = (x > 100) ? x : 7 func8(x) = x + 2 func5(x) = func8(x) ; = x + 2 func4(x) = call func1(17), discard, return x ; identity func3(x) = func7(x) ; = (x > 100) ? x : 7 func2(x): if (unsigned) x > 499: return func5(x + 13) ; = x + 15 else: return func4(x - 86) ; = x - 86 func1(x): if (unsigned) x <= 100: return func3(x) ; = 7 else: return func2(x + 100)Note the unsigned compares. The branches use
bls(branch if lower-or-same, unsigned) andbhi(branch if higher, unsigned), so a Python translation must compare with the input masked to 32 bits, not as a signed int. Inputs above 231 are still positive in this world.Step 2Collapse the call graph into three cases
ObservationEach leaf function reduces to something simple: identity, +2, or a constant. Substituting them bottom-up into func1 gives one piecewise formula that Python evaluates directly for any input, including the 32-bit-large argument 3251107833.Once func4 is identity and func5 is +2, every path through func1 reduces to a piecewise linear formula on the input.pythonpython3 - <<'EOF' def chall4(x): x &= 0xFFFFFFFF if x <= 100: return 7 elif (x + 100) <= 499: # 100 < x <= 399 return (x + 100 - 86) & 0xFFFFFFFF # = x + 14 else: # x > 399 (after +100 overflow check, none here) return (x + 100 + 15) & 0xFFFFFFFF # = x + 115 for arg in [50, 200, 600, 3251107833]: r = chall4(arg) print(f"arg={arg:<12} -> {r} (hex {r:#010x})") EOFExpected output
arg=3251107833 -> 3251107948 (hex 0xc1c7f86c)
What didn't work first
Tried: Run the Python solver without masking intermediate results to 32 bits, treating all arithmetic as unbounded Python integers.
The w registers are 32 bits and wrap on overflow, while Python integers grow without bound. Here 3251107833 plus 115 is 3251107948, still under 2^32, so the unmasked answer happens to match. Any argument close enough to 0xFFFFFFFF that the addition crosses the boundary makes the unmasked model silently disagree with the real program.
Tried: Submit the decimal result 3251107948 directly as the flag without converting it to an 8-digit lowercase hex string.
ARM assembly challenges on picoCTF ask for the printed register value, which the stub code outputs as a hex-formatted integer. The flag format requires the lowercase 8-hex-digit form (c1c7f86c) wrapped in picoCTF{}, not the decimal integer.
Learn more
Worked output:
arg=50 -> 7 (hex 0x00000007) arg=200 -> 214 (hex 0x000000d6) arg=600 -> 715 (hex 0x000002cb) arg=3251107833 -> 3251107948 (hex 0xc1c7f86c)
For arg=3251107833 the answer is
3251107948. Convert that to lowercase 8-hex-digit form (no0xprefix) and wrap withpicoCTF{...}for submission.Sanity check via QEMU if you doubt the trace:
aarch64-linux-gnu-gcc -static -o chall_4 chall_4.S && qemu-aarch64 ./chall_4 3251107833should printResult: 3251107948.See Python for CTF for the simulation idiom and Ghidra reverse engineering if you want a graphical view of the dispatch tree.
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{c1c7f86c}
Per-instance challenge. The reference computation shows arg=3251107833 -> result=3251107948 -> hex c1c7f86c. Different picoCTF instances get different arguments and thus different flags.