CSAPP BombLab AArch64
Timeline
2025-06-24
init
Environment
Win11 WSL2:
Environment setup:
1 | git clone https://github.com/SJTU-IPADS/OS-Course-Lab.git |
Terminal 1 — write answers to ans.txt so you don’t have to retype previously solved ones:
1 | qemu-aarch64-static -g 1234 ./bomb < ans.txt |
Terminal 2:
1 | gdb-multiarch -ex "set architecture aarch64" -ex "target remote localhost:1234" -ex "file bomb" |
main
The main function has C source code:
1 |
|
We can see that the return value of read_line is passed as an argument to phase_x.
As shown, main calls read_line to read input each time, then calls phase_x, then calls phase_defused to print information.
Function prologue (stack frame setup):
1 | 400b10: a9bf7bfd stp x29, x30, [sp, #-16]! // Save frame pointer and return address to stack, sp -= 16 |
Get the destination buffer (x0) for fgets, preparing to call _IO_fgets:
The function is defined as:
1 | char *fgets(char *s, int size, FILE *stream); |
The first parameter is char* (x0), the second is int (w1), and the third is a struct pointer (x2).
1 | 400b18: f00004e0 adrp x0, 49f000 |
At this point, the function has read one line of input into the address
0x4a2138.
Processing fgets result: iterating through the string looking for newline:
1 | 400b34: d2800000 mov x0, #0 // x0 = 0, string offset index |
If ‘\n’ is not found in the first 80 bytes, call explode:
1 | 400b5c: 97ffffe6 bl 400af4 <explode> // No '\n', explode |
If newline \n is found, replace it with \0 (string terminator):
1 | 400b60: d0000501 adrp x1, 4a2000 |
Restore before returning:
1 | 400b6c: d0000500 adrp x0, 4a2000 |
We can see phase_defused reads a global variable value, then decrements it.
Then loads a global address 0x464000+0x7c0 = 0x4647c0 as an argument to call printf.
And w1 holds the value of the global variable that was decremented earlier, at address 0x4a0000 + 80 = 0x4a0000 + 0x50 = 0x4a0050.
phase_0
1 | stp x29, x30, [sp, #-16]! |
- Purpose: Push x29 (frame pointer) and x30 (link register, return address) onto the stack. This saves the caller’s frame pointer and return address for later restoration.
1 | mov x29, sp |
- Purpose: Assign the current stack pointer sp to the frame pointer x29, establishing the current function’s new stack frame.
From now on, x29 points to the bottom of this function call’s stack frame, convenient for accessing local variables or passing parameters later.
Next, the function calls read_int to read an int value, stores the return value in w0, then compares it with the value at 0x4a0000 + 0x54(84) = 0x4a0054 to check equality. Use gdb’s examine command to view this memory location:
We can see it’s 2022.
Then the cmp instruction compares the input value (stored in x0) with 2022 (stored in x1). If not equal, jump to the line calling explode, so they must be equal. The input must be 2022.
phase_1
phase_1 is similar to phase_0.
The function loads the value at address 0x4a0000+0x58(88) = 0x4a0058 into x1, then calls strcmp. Its parameters should be x0, x1, where x0 is our input value. This means it compares our input string with the string at address 0x4a0058, checking if the return value w0 is 0. If not 0, jump to the line calling explode.
Note that address 0x4a0058 does not store the string itself, but the address of the string. So you need to first read this address, then read the string at that string address.
Answer: “Fault Tolerance: Reliable Systems from Unreliable Components.”
phase_2
The first line allocates 64 bytes of stack space, enough for 64/8=8 64-bit values. The first line stores x29 at sp, x30 at sp+8.
Then saves sp to the x29 register.
Then stores x19, x20 registers at sp+16, sp+24, i.e.:
- sp —> x29
- sp+8 —> x30
- sp+16 —> x19
- sp+24 —> x20
Then x1 = sp+0x20 = sp+32, as the second parameter of read_8_numbers, then calls read_8_numbers.
The function first allocates 0x20 (32 bytes) of stack space: sp = sp-32, then stores x29 at sp+16, x30 at sp+24.
Then sets frame pointer x29 to sp+0x10 (sp+16).
Then puts x1 into x2 — x1 is read_8_numbers’s second parameter.
Then x1 = x1+0x1c = x1+28, stores x1 at sp+8; x1 = x2+0x18 = x1+24, stores x1 at sp.
Then:
- x7 = x2+0x14
- x6 = x2+0x10
- x5 = x2+0xc
- x4 = x2+0x8
- x3 = x2+0x4
x1 is the value at address 0x464000+0x858, a pointer — clearly the start of an array, a format string, used as __isoc99_scanf’s second parameter:
1 | int sscanf(const char *str, const char *format, ...); |
The first parameter is x0, which has remained unchanged since returning from read_line.
After the function call, compare the return value with 0x7. If less than or equal, explode — so you must input 8 numbers.
After the function returns, restore the original stack structure:
- sp —> x29
- sp+8 —> x30
- sp+16 —> x19
- sp+24 —> x20
- sp+32 —> array start address, 8 numbers, also the address of array[0]
- sp+36 —> array[1]
- sp+40 —> array[2]
- sp+44 —> array[3]
- sp+48 —> array[4]
- sp+52 —> array[5]
- sp+56 —> array[6]
- sp+60 —> array[7]
Exactly filling the 64 bytes that phase_2 initially allocated.
Then the 8 input numbers are checked. It’s easy to see the first and second numbers must both be 1.
Then a loop:
x19 = sp+0x20 = sp+32, i.e. array[0].
x20 = sp+0x38 = sp+56, termination condition: (56-32)/4=6, stop at the 6th element.
First b 0x4007d0 skips one increment of x19 and comparison with x20.
If x19 == x20, jump to phase_2+108 to end.
Otherwise, first load x19 as address into w0 (array[i]), load the next into w1 (array[i+1]).
Let w0 = array[i] + array[i+1] + 4.
w1 = array[i+2].
Compare w0 == w1. If equal, jump to phase_2+60, increment i++.
If not equal, explode.
The first and second numbers must be 1.
So the third number is 1+1+4=6.
Fourth: 1+6+4=11.
Fifth: 6+11+4=21.
Sixth: 11+21+4=36.
Seventh: 21+36+4=61.
Eighth: 61+36+4=101.
So the answer is:
1 | 1 1 6 11 21 36 61 101 |














