书籍:逆向工程权威指南上(看书笔记)
这本书有各种架构的相关说明,省略掉非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}
|