CSAPP BombLab AArch64

Words 1.7k
Views
Visitors
Timeline

Timeline

2025-06-24

init

This article describes the process of reverse engineering and solving the AArch64 version of CSAPP BombLab in a Win11 WSL2 environment. The article details the environment setup method, and the use of the GDB debugger to analyze the main function call flow, gradually cracking phase_0 to phase_3's steps. Among them, phase_0 requires input of integer 2022; phase_1 requires input of the string "Fault Tolerance: Reliable Systems from Unreliable Components."; phase_2 requires input of 8 numbers satisfying a specific recurrence relation (1,1,6,11,21,36,61,101); phase_3 involves more complex logical judgments. The article summarizes the stack frame structure, register usage, and key comparison instructions of each phase, providing readers with a complete problem-solving approach and answers.

Environment

Win11 WSL2:

Hardware environment
Hardware environment

Environment setup

123
git clone https://github.com/SJTU-IPADS/OS-Course-Lab.gitcd OS-Course-Lab/Lab0sudo apt-get install qemu-user gdb-multiarch

terminal1
You can write the answers in ans.txt, so you don’t have to type them again for the ones already solved.

1
qemu-aarch64-static -g 1234 ./bomb < ans.txt

terminal2

1
gdb-multiarch -ex "set architecture aarch64" -ex "target remote localhost:1234" -ex "file bomb"

main

The main function has C source code.

1234567891011121314151617181920212223242526272829303132333435
#include <stdio.h>#include "phases.h"#include "utils.h"int main() {  char* input;  printf("Type in your defuse password!\n");  input = read_line();  phase_0(input);  phase_defused();  input = read_line();  phase_1(input);  phase_defused();  input = read_line();  phase_2(input);  phase_defused();  input = read_line();  phase_3(input);  phase_defused();  input = read_line();  phase_4(input);  phase_defused();  input = read_line();  phase_5(input);  phase_defused();  printf("Congrats! You have defused all phases!\n");  return 0;}

You can see that read_line’s return value is passed as a parameter to phase_x

The main function reads input and calls the disassembly of each phase.
The main function reads input and calls the disassembly of each phase.

You can see that the main function calls read each time_line to read input, then calls phase_x function, then calls phase_defused to print information

read_line function stack frame setup
read_line function stack frame setup

Function prologue (stack frame setup):

12
400b10: a9bf7bfd    stp x29, x30, [sp, #-16]!    // Save frame pointer and return address to stack, sp -= 16400b14: 910003fd    mov x29, sp                  // Set new frame pointer x29 = sp

Get the target buffer of fgets (x0), prepare to call_IO_fgets

The function is defined as follows:

1
char *fgets(char *s, int size, FILE *stream);

You can see that the first parameter is char* (x0), the second parameter is int (w1), and the third parameter is a struct pointer (x2).

12345678910
400b18: f00004e0    adrp x0, 49f000400b1c: f946d000    ldr x0, [x0, #3488]400b20: f9400002    ldr x2, [x0]                 // FILE *stream400b24: 52800a21    mov w1, #0x51                // w1 = 81, read at most 81 bytes400b28: d0000500    adrp x0, 4a2000400b2c: 9104e000    add x0, x0, #0x138           // x0 = 0x4a2000 + 0x138, x0 is the destination buffer pointer400b30: 94004a78    bl  413510 <_IO_fgets>       // Call _IO_fgets(buffer, 81, stream)

At this point, the function has read a line of string from input into0x4a2138address.

Process fgets result: iterate through the string to find newline:

12345678910111213
400b34: d2800000    mov x0, #0                   // x0 = 0, as string offset index// Construct x2 = buffer base address again400b38: d0000502    adrp x2, 4a2000400b3c: 9104e042    add x2, x2, #0x138           // x2 = buffer400b40: 38626801    ldrb w1, [x0, x2]            // w1 = buffer[x0]400b44: 34000141    cbz w1, 400b6c               // If w1 == 0, string ends, jump to return400b48: 7100283f    cmp w1, #0xa                 // Check if it is newline '\n'400b4c: 540000a0    b.eq 400b60                  // It is newline, jump to remove newline400b50: 91000400    add x0, x0, #1               // x0++, continue checking next character400b54: f101401f    cmp x0, #0x50                // Check at most 0x50 bytes (at most 80 bytes)400b58: 54ffff41    b.ne 400b40                  // If not reached the end, continue loop

If ‘\n’ is not found in the first 80 bytes, callexplode

1
400b5c: 97ffffe6    bl 400af4 <explode>          // No '\n', explode

If newline is found\n, replace it with\0(end of string):

123
400b60: d0000501    adrp x1, 4a2000400b64: 9104e021    add x1, x1, #0x138           // x1 = buffer400b68: 3820c83f    strb wzr, [x1, w0, sxtw]     // buffer[x0] = 0 (wzr is zero register)

Restore context before returning:

12345
400b6c: d0000500    adrp x0, 4a2000400b70: 9104e000    add x0, x0, #0x138           // x0 = buffer, as return value400b74: a8c17bfd    ldp x29, x30, [sp], #16      // Restore frame pointer and return address, sp += 16400b78: d65f03c0    ret                          // Return

phase_defused function disassembly
phase_defused function disassembly

It can be seen that the phase_defused function reads a global variable value, then decrements it by one

Then it loads a global address 0x464000+0x7c0 = 0x4647c0 as an argument to call printf for printing

Global variable decrement operation
Global variable decrement operation

And w1 stores the value of the global variable that was decremented by one at the beginning; the address is 0x4a0000 + 80 = 0x4a0000 + 0x50 = 0x4a0050.

The value at global variable memory address 0x4a0050
The value at global variable memory address 0x4a0050

phase_0

phase_0 function disassembly
phase_0 function disassembly

1
stp	x29, x30, [sp, #-16]!
  • Purpose: Push x29 (frame pointer) and x30 (link register, return address) onto the stack. This step 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 a new stack frame for the current function.

From now on, x29 points to the bottom of this function call’s stack frame, making it convenient to access local variables or pass parameters later.

Then, the function first calls read_int to read an int value, stores the return value in w0, and compares it with the value at 0x4a0000 + 0x54 (84) = 0x4a0054 to see if they are equal. You can use the gdb examine command to view the value at this memory location:

GDB view of the value at memory address 0x4a0054: 2022
GDB view of the value at memory address 0x4a0054: 2022

It can be seen that it is 2022

Then a cmp instruction compares the input value (stored in x0) with 2022 (stored in x1). If they are not equal, it jumps to the line that calls the explode function, so they must be equal; that is, the input value must be 2022.

phase_1

phase_1 and phase_0 is similar

phase_1 function disassembly
phase_1 function disassembly

It can be seen that the function loads the value at address 0x4a0000+0x58 (88) = 0x4a0058 into x1, then calls strcmp. Its parameters should be x0 and x1, where x0 is our input value. That is, it compares the string we input with the string at address 0x4a0058, and checks whether the return value w0 is 0. If it is not 0, it jumps to the line that calls explode.

Address 0x4a0058 stores a string pointer
Address 0x4a0058 stores a string pointer

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.

The answer is: “Fault Tolerance: Reliable Systems from Unreliable Components.”

phase_2

phase_2 allocates 64 bytes of stack space
phase_2 allocates 64 bytes of stack space

The function allocates 64 bytes of stack space, which can hold 64/8=8 64-bit values. First, it places x29 at sp and x30 at sp+8.

Then it saves sp to the x29 register.

Then it places the values of the x19 and x20 registers at sp+16 and sp+24, i.e.,

  • sp —> x29
  • sp+8 —> x30
  • sp+16 —> x19
  • sp+24 —> x20

Then x1 = sp + 0x20 = sp + 32, as read_8_the second argument of numbers, then call read_8_the function numbers

read_8_Disassembly of the numbers function
read_8_Disassembly of the numbers function

It can be seen that the function first allocates 0x20, i.e., 32 bytes, of stack space, sp = sp - 32, then stores x29 at sp+16 and x30 at sp+24.

Then it sets the frame pointer x29 to sp+0x10, i.e., sp+16.

Then it puts x1 into x2; x1 is read_8_the second parameter of numbers

Then, x1 = x1 + 0x1c = x1 + 28

Store x1 at sp+8

x1 = x2 + 0x18 = x2 + 24

Store 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; it is clear here that this is the starting address of the format string, used as the __second parameter of isoc99_scanf

1
int sscanf(const char *str, const char *format, ...);

The first parameter is x0, which has remained unchanged since returning from read_line.

sscanf's format string
sscanf's format string

After the function call, it compares the return value with 0x7; if it is less than or equal, it explodes, so you must enter 8 numbers.

After the function returns, it restores the original stack structure.

  • sp —> x29
  • sp+8 —> x30
  • sp+16 —> x19
  • sp+24 —> x20
  • sp+32 —> the starting address of array, a total of 8 numbers, which is also the address of the first number, 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]

It exactly fills the 64 bytes initially allocated by phase_2.

phase_2 determines that the first two elements of the array must be 1.
phase_2 determines that the first two elements of the array must be 1.

Then comes the check on the 8 input numbers; it is easy to see here that the first and second numbers must both be 1.

Then there is a loop.

phase_2 loop calculates the subsequent elements of the array.
phase_2 loop calculates the subsequent elements of the array.

x19 = sp+0x20 = sp+32, i.e., array[0]

x20 = sp+0x38 = sp+56, i.e., the termination condition is (56-32)/4=6, that is, it stops at the sixth one.

First, b 0x4007d0 skips the increment of x19 and the comparison with x20.

If x19 == x20, it jumps to phase_2+108 to finish.

Otherwise, first load x19 as an address into w0 (array[i]), then load the next one after it into w1 (array[i+1])

Let w0 = array[i] + array[i+1] + 4

w1 = array[i+2]

Compare whether w0 equals w1; if equal, jump to phase_2+60 and increment i

If they are not equal, explode

The first and second numbers must be 1

Then the third number is 1+1+4=6

The fourth number is 1+6+4=11

The fifth number is 6+11+4=21

The sixth number is 11+21+4=36

The seventh number is 21+36+4=61

The eighth number is 61+36+4=101

Therefore the answer is

1
1 1 6 11 21 36 61 101

phase_3

Loading comments…