CSAPP BombLab AArch64

Words 1.6k
Views
Visitors

Timeline

Timeline

2025-06-24

init

This article introduces the detailed reverse engineering analysis process of the CSAPP BombLab experiment under the AArch64 architecture, covering environment setup, main function stack frame logic, and phase_0 to phase_2's assembly code analysis and problem-solving ideas.

Environment

win11 WSL2:

Hardware environment
Hardware environment

Environment setup

1
2
3
git clone https://github.com/SJTU-IPADS/OS-Course-Lab.git
cd OS-Course-Lab/Lab0
sudo apt-get install qemu-user gdb-multiarch

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

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 code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#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;
}

It can be seen that read_line’s return value is passed as a parameter to phase_x

image-20250629225404452
image-20250629225404452

It can be seen that the main function calls read each time_line to read input, then calls phase_x function, then calls phase_defused to print information

image-20250629225341441
image-20250629225341441

Function start (stack frame setup):

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

Get the target buffer for fgets (x0), ready to call_IO_fgets

The function is defined as follows:

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

It can be seen that the first parameter is char* (x0), the second parameter is int (w1), and the third parameter is a struct pointer (x2)

1
2
3
4
5
6
7
8
9
10
400b18: f00004e0    adrp x0, 49f000
400b1c: f946d000 ldr x0, [x0, #3488]
400b20: f9400002 ldr x2, [x0] // FILE *stream

400b24: 52800a21 mov w1, #0x51 // w1 = 81, reads up to 81 bytes

400b28: d0000500 adrp x0, 4a2000
400b2c: 9104e000 add x0, x0, #0x138 // x0 = 0x4a2000 + 0x138, x0 is the target buffer pointer

400b30: 94004a78 bl 413510 <_IO_fgets> // Call _IO_fgets(buffer, 81, stream)

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

Processing fgets result: Traversing the string to find the newline character:

1
2
3
4
5
6
7
8
9
10
11
12
13
400b34: d2800000    mov x0, #0                   // x0 = 0, as the string offset index

// Construct x2 = buffer base address again
400b38: d0000502 adrp x2, 4a2000
400b3c: 9104e042 add x2, x2, #0x138 // x2 = buffer

400b40: 38626801 ldrb w1, [x0, x2] // w1 = buffer[x0]
400b44: 34000141 cbz w1, 400b6c // If w1 == 0, string ends, jump to return
400b48: 7100283f cmp w1, #0xa // Check if it is a newline character '\n'
400b4c: 540000a0 b.eq 400b60 // It is a newline character, jump to remove newline character
400b50: 91000400 add x0, x0, #1 // x0++, continue checking the next character
400b54: f101401f cmp x0, #0x50 // Check at most 0x50 bytes (at most 80 bytes)
400b58: 54ffff41 b.ne 400b40 // If not at the end, continue the loop

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

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

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

1
2
3
400b60: d0000501    adrp x1, 4a2000
400b64: 9104e021 add x1, x1, #0x138 // x1 = buffer
400b68: 3820c83f strb wzr, [x1, w0, sxtw] // buffer[x0] = 0 (wzr is the zero register)

Restore context before returning:

1
2
3
4
5
400b6c: d0000500    adrp x0, 4a2000
400b70: 9104e000 add x0, x0, #0x138 // x0 = buffer, as the return value

400b74: a8c17bfd ldp x29, x30, [sp], #16 // Restore frame pointer and return address, sp += 16
400b78: d65f03c0 ret // Return

image-20250629225645707
image-20250629225645707

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

Then loads a global address 0x464000+0x7c0 = 0x4647c0 as a parameter to call printf to print

image-20250630125413000
image-20250630125413000

And w1 stores the value of the initially decremented global variable, the address is 0x4a0000+ 80 = 0x4a0000 + 0x50 = 0x4a0050.

image-20250630125539188
image-20250630125539188

phase_0

image-20250624203514602
image-20250624203514602

1
stp	x29, x30, [sp, #-16]!
  • Function: 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 easy restoration later.
1
mov	x29, sp
  • Function: Assign the current stack pointer sp to the frame pointer x29, i.e., establish the new stack frame for the current function.

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

Subsequently, the function first calls the read_int function to read an int value, stores the return value in w0, and then 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:

image-20250624204122130
image-20250624204122130

It can be seen that it is 2022

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

phase_1

phase_1 and phase_0 is close enough

image-20250624205525968
image-20250624205525968

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 to say, it compares our input string with the string at address 0x4a0058, and checks if the return value w0 is 0. If it is not 0, it jumps to the line that calls explode.

image-20250624205829889
image-20250624205829889

It should be noted that the address 0x4a0058 does not store a string, but the address of a string. Therefore, you must first read out this address, and then read the string at that string address.

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

phase_2

image-20250629223737226
image-20250629223737226

The first line allocates 64 bytes of stack space, which can hold 64/8=8 64-bit values in total. The first line 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 positions sp+16 and sp+24, namely

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

Then x1 = sp + 0x20 = sp + 32, serving as the read_8_numbers’ second parameter, then calls read_8_numbers function

image-20250629224840291
image-20250629224840291

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

Then it sets the stack frame pointer x29 to sp + 0x10, which is sp + 16.

Then it puts x1 into x2, x1 is the read_8_numbers’ second parameter

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

Store x1 into sp+8

x1 = x2 + 0x18 =x1 + 24

Store x1 into sp

Subsequently

  • x7 = x2+ 0x14

  • x6 = x2 + 0x10

  • x5 = x2+0xc

  • x4 = x2+ 0x8

  • x3 = x2+0x4

x1 is the value at address 0x464000+0x858, which is a pointer. It is obvious here that this is the starting address of an array. It can be seen that it is a formatted string, serving 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.

image-20250630140224444
image-20250630140224444

After the function call ends, it compares the return value with 0x7. If it is less than or equal to, it explodes. Therefore, you must input 8 numbers.

After the function returns, the original stack structure is restored.

  • sp —> x29
  • sp+8 —> x30
  • sp+16 —> x19
  • sp+24 —> x20
  • sp+32 —> the starting address of the 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]

Exactly fills the 64 bytes initially allocated by phase_2

image-20250630142117435
image-20250630142117435

Then comes the judgment of the 8 input numbers. It is easy to see that the first and second numbers must both be 1.

Then comes a loop

image-20250630142644664
image-20250630142644664

x19 = sp+0x20 = sp+32, which is array[0]

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

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

If x19 == x20, jump to phase_2+108 to end

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

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

w1 = array[i+2]

Compare if w0 equals w1; if equal, jump to phase_2+60, incrementing i

If not equal, it explodes

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