书籍:逆向工程权威指南上(看书笔记)

这本书有各种架构的相关说明,省略掉非arm64架构的相关记录

1、CPU

cpu:执行程序机器码的硬件单元。

指令码:cpu处理的底层命令。

机器码:给cpu执行的程序代码。

汇编语言:为了便于编写而创造出来的,最接近机器码的语言。

指令集架构:不同架构的cpu处理的指令集都不一样,arm指令集最早先都被封装在4个字节里,后来由于发现4个字节很少用满,就推出了封装在2个字节的Thumb指令集及架构,但是不能封装所有的arm指令。就推出了2个字节封装不下的指令就由4字节封装的Thumb-2。最后arm64指令集又回到了固定使用4字节的指令集。

2、简单函数

1
2
3
4
5
6
7
8
int f(){
return 123;
}
--------汇编代码--------
f:
mov r0,#0x7b ;
bx lr
endp

这里可以看到arm程序使用r0寄存器存放返回值,使用lr存放函数结束后返回的地址。

3、arm64的hello world例子

首先写个例子

1
2
3
4
5
#include <stdio.h>
int main(){
printf("hello world!");
return 0;
}

然后我们需要先编译成android的可执行程序

首先下载个用来编译的工具ndk-r12b

1
http://dl.google.com/android/repository/android-ndk-r12b-linux-x86_64.zip

然后编写我们的编译脚本build_android.sh

1
2
3
4
5
6
7
CC_HOME=/home/king/Android/Sdk/ndk/android-ndk-r12b
BIN_PATH=${CC_HOME}/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin
CC=${BIN_PATH}/aarch64-linux-android-gcc
CXX=${BIN_PATH}/aarch64-linux-android-g++
AS=${BIN_PATH}/aarch64-linux-android-as

${CC} -o hello main.c -pie -fPIE -I${CC_HOME}/platforms/android-24/arch-arm64/usr/include -L${CC_HOME}/platforms/android-24/arch-arm64/usr/lib --sysroot=${CC_HOME}/platforms/android-23/arch-arm64/usr/

或者是使用llvm也一样可以编译

1
2
3
4
//编译arm版本
/Users/king/Library/Android/sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/darwin-x86_64/bin/armv7a-linux-androideabi24-clang++ ./main.cpp
//编译arm64版本
/Users/king/Library/Android/sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android24-clang++ ./main.cpp

编译完成之后,使用file hello查看一下刚刚编译的文件是否正确

1
hello: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /system/bin/linker64, not stripped

接着我们用ida来分析这个文件,查看汇编代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
; int __cdecl main(int argc, const char **argv, const char **envp)
EXPORT main
main

var_s0= 0

STP X29, X30, [SP,#-0x10+var_s0]!
MOV X29, SP
ADRL X0, aHelloWorld ; "hello world!"
BL .printf
MOV W0, #0
LDP X29, X30, [SP+var_s0],#0x10
RET
; End of function main

可以看到在函数开始时使用指令STP将X29和X30寄存器存放到了数据栈中保存,然后在函数结束的时候,使用指令LDP将X29和X30再从数据栈取值填充回去。同样也是通过W0寄存器存放返回值。在ARM64中,X29相当于是FP寄存器,X30是LR寄存器。所以这两个寄存器经常在函数的开始部分和结尾部分成对出现

ADR指令是将PC寄存器的值加上字符串偏移量,由此找到内存中字符串常量的绝对地址,然后赋值给X0寄存器。

BL调用printf实际上是将下一条指令MOV W0, #0的地址保存到LR寄存器中,然后将printf的函数地址写入PC寄存器,下一条指令会执行PC寄存器指向的地址,当printf执行完成后,会从LR寄存器获取到返回地址,然后函数继续执行。

最后返回W0寄存器是X0寄存器的低32位,如果我们定义的函数返回值不是int而是int64,则这里会变成返回X0寄存器。

4、函数序言和函数尾声

也就是前面看到的FP和LR寄存器值保存到数据栈中。这是为了在函数执行期间,这些寄存器的值不受后汉书运行的影响,在函数结束时,能将这些寄存器的值还原给调用者。通过序言和尾声的特征,我们可以在汇编中识别出各个函数的范围。

5、栈的用途

前面的例子中,在函数的开始和结尾,就用到了栈来保护相关寄存器的值不受影响。

如果有一个函数不调用其他函数,就像树上最末端的样子一样你,就叫做叶函数(leaf function),叶函数的特点是不用保存LR寄存器的值。如果叶函数代码短到只用几个寄存器,那么也有可能不会使用数据栈,这种叶函数的运行速度就会很快。

也可以使用栈来为函数传递参数。

alloca函数直接使用栈来分配内存,是很请的内存不用特地free来释放。比如x86里函数尾声的代码会还原寄存器ESP的值,把数据栈还原为函数启动之前的状态。

栈的脏数据,在函数退出以后,原有栈空间的局部变量不会自动清除,就成了栈的脏数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
void f1(){
int a=1,b=2,c=3;
}
void f2(){
int a,b,c;
printf("%d,%d,%d",a,b,c);
}
int main()
{
f1();
f2();
return 0;
}

上面的例子最后打印1,2,3。就是因为使用到了栈上的脏数据。

所以就是在运行一个函数时,栈中的值受前一个函数的影响,而获得了前一个函数的变量值,这种就是脏数据。

6、printf参数的传递

在arm中,参数传递时,前4个参数会传递给r0-r3寄存器,如果参数更加长,其他的参数会存放在栈中。

b指令和bl指令的差别是,b指令只负责跳转,不会根据LR寄存器返回,而BL则会将LR寄存器保存到栈中。

下面看个printf的参数传递例子

1
2
3
4
5
6
#include <stdio.h>

int main(){
printf("data: %d %d %d\n",1,2,3);
return 0;
}

生成汇编如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
STMFD           SP!, {R11,LR}
MOV R11, SP
SUB SP, SP, #8
MOV R0, #0
STR R0, [SP,#8+var_4]
LDR R0, =(aDataDDD - 0x1478)
ADD R0, PC, R0 ; "data: %d %d %d\n"
MOV R1, #1
MOV R2, #2
MOV R3, #3
BL printf
MOV R0, #0
MOV SP, R11
LDMFD SP!, {R11,PC}

可以看到三个值是直接存放在R1,R2,R3寄存器中,接下来看看更多参数的情况

1
2
3
4
int main(){
printf("data: %d %d %d %d %d %d %d %d %d\n",1,2,3,4,5,6,7,8,9);
return 0;
}

然后再用ida查看一下

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
STMFD           SP!, {R4-R7,R11,LR}
ADD R11, SP, #0x10
SUB SP, SP, #0x20
MOV R0, #0
STR R0, [R11,#var_14]
LDR R0, =(aDataDDDDDDDDD - 0x1498)
ADD R0, PC, R0 ; "data: %d %d %d %d %d %d %d %d %d\n"
MOV R1, #1
MOV R2, #2
MOV R3, #3
MOV R7, #4
MOV R6, #5
MOV R5, #6
MOV R4, #7
MOV LR, #8
MOV R12, #9
STR R7, [SP,#0x30+var_30]
STR R6, [SP,#0x30+var_2C]
STR R5, [SP,#0x30+var_28]
STR R4, [SP,#0x30+var_24]
STR LR, [SP,#0x30+var_20]
STR R12, [SP,#0x30+var_1C]
BL printf
MOV R0, #0
SUB SP, R11, #0x10
LDMFD SP!, {R4-R7,R11,PC}

这里可以看到r0-r3的值没有放进栈中,r4后面的寄存器放到了栈中,再调用的printf。

另外我们可以看到对栈的处理,就知道是如何在函数执行完成时还原栈的地址。

1
2
3
4
ADD             R11, SP, #0x10  //函数开始时,先将sp+0x10的值保存到r11
...
...
SUB SP, R11, #0x10 //函数开始时,先将r11-0x10的值还原到sp

上面可以看到结束函数时栈的地址是如何还原,原本栈上的数据依然在那里,所以前面那个脏数据的例子就会出现那样的效果。

7、scanf函数

这里作者主要通过这个函数演示一下数据指针的传递。例子如下

1
2
3
4
5
6
7
int main(){
printf("enter num:\n");
int num;
scanf("%d",&num);
printf("input num:%d",num);
return 0;
}

然后看看arm的汇编如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
STMFD           SP!, {R11,LR}
MOV R11, SP
SUB SP, SP, #8
MOV R0, #0
STR R0, [SP,#8+var_4]
LDR R0, =(aEnterNum - 0x14A0)
ADD R0, PC, R0 ; "enter num:\n"
BL printf
LDR R0, =(unk_3C4 - 0x14AC)
ADD R0, PC, R0 ; unk_3C4 ; format
MOV R1, SP
BL scanf
LDR R1, [SP,#8+var_8]
LDR R0, =(aInputNumD - 0x14C0)
ADD R0, PC, R0 ; "input num:%d"
BL printf
MOV R0, #0
MOV SP, R11
LDMFD SP!, {R11,PC}

可以看到scanf的第二个参数直接是将sp传进去了,然后执行结束后,再用ldr将sp中的数据读取出来。

下面看看在arm64时的汇编,区别不大。不过看起来更加清晰一些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
SUB             SP, SP, #0x20
STP X29, X30, [SP,#0x10+var_s0]
ADD X29, SP, #0x10
MOV W8, WZR
STR W8, [SP,#0x10+var_C]
STUR WZR, [X29,#var_4]
ADRP X0, #aEnterNum@PAGE ; "enter num:\n"
ADD X0, X0, #aEnterNum@PAGEOFF ; "enter num:\n"
BL .printf
ADRP X0, #unk_578@PAGE
ADD X0, X0, #unk_578@PAGEOFF
ADD X1, SP, #0x10+var_8
BL .scanf
LDR W1, [SP,#0x10+var_8]
ADRP X0, #aInputNumD@PAGE ; "input num:%d"
ADD X0, X0, #aInputNumD@PAGEOFF ; "input num:%d"
BL .printf
LDR W0, [SP,#0x10+var_C]
LDP X29, X30, [SP,#0x10+var_s0]
ADD SP, SP, #0x20
RET

如果num变量是全局变量的情况又会有什么不同呢。下面看看调整后的代码

1
2
3
4
5
6
7
int num;
int main(){
printf("enter num:\n");
scanf("%d",&num);
printf("input num:%d",num);
return 0;
}

下面是arm汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
STMFD           SP!, {R11,LR}
MOV R11, SP
SUB SP, SP, #8
MOV R0, #0
STR R0, [SP,#8+var_4]
LDR R0, =(aEnterNum - 0x14A0)
ADD R0, PC, R0 ; "enter num:\n"
BL printf
LDR R0, =(unk_3C4 - 0x14AC)
ADD R0, PC, R0 ; unk_3C4 ; format
LDR R1, =(num - 0x14B4)
ADD R1, PC, R1 ; num
BL scanf
LDR R0, =(num - 0x14C0)
ADD R0, PC, R0 ; num
LDR R1, [R0]
LDR R0, =(aInputNumD - 0x14CC)
ADD R0, PC, R0 ; "input num:%d"
BL printf
MOV R0, #0
MOV SP, R11
LDMFD SP!, {R11,PC}