1 状态寄存器
-
1.1 了解状态寄存器
CPSR寄存器(current program status register), 我们称之状态寄存器,它和别的寄存器不一样,别的寄存器存放的数据是整个寄存器具有一个含义; 而CPSR寄存器是按位起作用的,也就是说,它的每一位都有专门的含义,记录特定的信息.

- CPSR寄存器是32位的
- CPSR的低8位(包括I、F、T和M[4:0])称为控制位,程序无法修改,除非CPU运行于特权模式下,程序才能修改控制位!
- N、Z、C、V均为条件码标志位。它们的内容可被算术或逻辑运算的结果所改变,并且可以决定某条指令是否被执行!意义重大!
ARM64中,一些指令的执行结果会影响CPSR的标志位,例如
subsaddsCMP等等一些算术指令和逻辑指令 -
1.2 N位解读
CPSR的第31位是 N位标识(Negative),符号标志位。它记录相关指令执行后, 其结果是否为负。如果为负 N = 1, 如果是非负数 N = 0.
理解:negative 意思 负的,n=1就是yes,n=0就是no
举例
void func() { asm( "mov w0, #0xffffffff\n" "adds w0, w0, #0x0\n" ); } int main(int argc, char * argv[]) { func(); }分析
我们将 0xffffffff 看成一个有符号数,那么运算结果是负的,因此结果是负的,那么cpsr的第31位是1(调试结果:cpsr的值 0x80000000)
注意,无符号数的算术运算不存在结果为负的情况,只有溢出的情况,因此如果是无符号数相加,无需考虑N位
-
1.3 Z位解读
CPSR的第30位是Z位 (Zero),0标志位。它记录相关指令执行后,其结果是否为0.如果结果为0.那么Z = 1.如果结果不为0,那么Z = 0.
理解:zero 零,z=1表示yes,z=0表示no
举例
void func() { asm( "mov w0, #0x0\n" "adds w0, w0, #0x0\n" ); } int main(int argc, char * argv[]) { func(); }结果
cpsr: 0x40000000 -
1.4 C位解读
CPSR的第29位是C (Carray),进位标志位。一般情况下,进行无符号数的运算时,产生进位或者借位会影响C位。
- 加法运算:当运算结果产生了进位时(无符号数溢出),C=1,否则C=0。
- 减法运算(包括CMP):当运算产生了借位时(无符号数溢出),C=0,否则C=1。
理解:加法进位时,进1,没进位时,进0;减法借位时,原来有1,借位变成0,没借位时还是1 ====> 只是便于理解,并不是原理是这样
最高位
对于位数为N的无符号数来说,其对应的二进制信息的最高位,即第N - 1位,就是它的最高有效位,而假想存在的第N位,就是相对于最高有效位的更高位。如下图所示:
进位
当两个数据相加的时候,有可能产生从最高有效位向更高位的进位。比如两个32位数据:0xaaaaaaaa + 0xaaaaaaaa,将产生进位。由于这个进位值在32位中无法保存,我们就只是简单的说这个进位值丢失了。其实CPU在运算的时候,并不丢弃这个进位制,而是记录在一个特殊的寄存器的某一位上。ARM下就用C位来记录这个进位值void func() { asm( "mov w0, #0xaaaaaaaa\n" "adds w0, w0, w0\n" "adds w0, w0, w0\n" ); } int main(int argc, char * argv[]) { func(); }第一次adds: cpsr值为0x30000000,产生了进位,C位置1
第二次adds: cpsr值为0x90000000, 没有产生进位,C位置0借位
当两个数据做减法的时候,有可能向更高位借位。再比如,两个32位数据:0x00000000 - 0x000000ff,将产生借位,借位后,相当于计算0x100000000 - 0x000000ff。得到0xffffff01 这个值。由于借了一位,所以C位 用来标记借位。C = 0.void func() { asm( "mov w0, #0x0\n" "subs w0, w0, #0xff\n" "subs w0, w0, #0xff\n" ); } int main(int argc, char * argv[]) { func(); }第一次subs,cpsr值0x80000000, 产生了借位,C位置0
第二次subs,cpsr值0xa0000000, 没有借位,C位置1 -
1.5 V位解读
CPSR的第28位是V(Overflow),溢出标志位。在进行有符号数运算的时候,如果超过了机器所能标识的范围,称为溢出
- 正数 + 正数 为负数 溢出
- 负数 + 负数 为正数 溢出
- 正数 + 负数 不可能溢出
理解:overflow 溢出,v=1表示yes,v=0表示no
void func() { asm( "mov w0, #0x7fffffff\n" "adds w0, w0, #0x2\n" "mov w0, #0x80000000\n" "subs w0, w0, #0x2\n" ); } int main(int argc, char * argv[]) { func(); }adds: cpsr值0x90000000; 两个正数相加之后,w0变成了负数,溢出,V位置1
subs: cpsr值0x30000000; sub相当于加负数,两个负数相加变为正数,V位置1
2 全局变量、常量、static变量
-
2.1 了解存储位置
它们都是在编译的时候确定了存储地址,并且在程序运行过程中地址是不变的
常量: 常量区; 全局变量、静态变量:全局数据区; 代码:代码段
由此:计算机可以知道常量、全局变量、静态变量它们的存储地址距离某个代码指令的地址的偏移量是多少,汇编就是根据偏移量计算出地址,然后从地址中去取值/赋值
-
2.2 实战举例
int g = 10; int fun(int a) { printf("haha"); static int b = 0; b = a; int c = g + a + b; printf("g:%p--b:%p", &g, &b); return c; } int main(int argc, char * argv[]) { printf("%d", fun(1)); }汇编

func汇编 
func汇编--续 分析
-
printf("haha");查看汇编我们知道,第8行准备调用printf函数,在上一次的学习中知道函数的参数一般在x0~x7中,由此:第6、7行在取参数haha给printf函数adrp x0, 3:adrp指令,address page ,主要做三件事
- 将3左移12位(指令集的规定。。)
- 将执行这条指令时的pc寄存器的低12位值清零(其实就是adrp这条指令的地址值--> 0x1000dc9c8)
- 两个数值相加,得到的地址值存入x0
- 由上面的计算可知:
haha这个常量存储地址,在0x1000df000~0x1000dffff 这4kb内存空间之内
add x0, x0, #0xd49 -->
haha的精确存储地址0x1000dfd49lldb调试:

-
static int b = 0; b=a- 第9、10行:取得static int b的地址:0x1000e1230,并保存b的地址到x30
- 第5行:a参数入栈
- 13行:读取a参数在栈中的值,放入w9,也就是w9现在为1
- 14行:w9写入x30指向的地址,也就是对b进行赋值

-
int c = g + a + b;- 11、12行:取得全局变量 g的地址:0x1000e1188,地址保存在x8中

- 15行:根据x8中地址取g的值到w9
- 16行:读取a参数
- 17行:计算g+a
- 18行:读取b
- 19行:计算g+a+b
- 11、12行:取得全局变量 g的地址:0x1000e1188,地址保存在x8中
-
printf("g:%p--b:%p", &g, &b);写这个只是为了验证我们的分析是否正确,汇编分析和上面差不多,省略。
-
-
2.3 小结
函数中对全局变量、静态变量、常量的操作实际上是获取它们的地址,然后进行操作,高级语言中,常量无法修改只不过是我们的编译器的设定,其实在动态调试过程中我们是可以修改的。。。。
3 简单代码逆向
根据已经学习的汇编的一些知识,从汇编代码,推导出c函数的一个实战记录
-
3.1 准备
- 工具准备:ida64
- Mach-O文件:
- Xcode项目-> products -> 0427-demo.app -> show in finder -> 显示包内容 -> 0427-demo
- file命令查看文件信息
file 0427-demo 输出: 0427-demo: Mach-O 64-bit executable arm64 - 项目代码
int global = 10; int func(int a, int b) { printf("haha"); return a + b + global; } int main(int argc, char * argv[]) { func(10, 20); return 0; }
-
3.2 ida汇编代码
分析main
main函数 bl _func准备跳转函数- 跳转之前
mov x0, w8mov x1, w9参数有2个 mov w8,#0xAmov w9, #0x14参数是传入参数是0xa, 0x14- 由此:
void func(int a, int b){}; 参数的类型是无法准确推断的,ida是静态分析,需要在lldb动态调试的时候分析出来。。大致int 可以满足,short也可以满足
分析func
func函数 - var_c、var_8、var_4、var_s0是ida帮忙生成的,他们主要是用在栈内存地址的偏移,通常我们可以将他们看成是高级语言中函数内的局部变量,类似
int aint b(注意,类型是无法确定的,根据main里面的分析,来填写) sub sp, sp, #0x20stp x29,x30, [sp, #0x10+var_s0]add x29, sp, #0x10对栈的操作stur w0, [x29, #var_4]存储函数传入参数(x0~x7是存储函数传入参数的寄存器),可以当成是对之前申明的局部var_4进行赋值。。由此,这句汇编代码是对传入参数进行保护,反汇编可以这样-->var_4 = astr w1, [sp, #0x10+var_8], 同上理解,var_8 = badrp x0, #aHahha@Pageadd x0,x0,#aHahaPageOff--> ida已经帮我们分析出,这个在查找 "haha" 字符串;bl _printf跳转printf函数,整体分析,得出printf("haha");adrp x30, #_global@pageadd x30,x30,#_global@pageoff--> 得到一个global全局变量的内存地址(也可以是静态局部变量,暂定全局变量) -->int * x30 = &global;int global=0;--> 全局global变量,变量值无法确定,可以写0,也可以随便写,global的类型也是无法确定的,暂时写intldur w1, [x29, #var_4]ldr w8,[sp, #0x10+var_8]从栈中取值到寄存器 -->int w1 = var_4, int w8 = var_8;add w8, w1, w8-->w8=w1+w8ldr w1, [x30]从内存中取值-->w1=*(x30)add w8, w8,w1-->w8=w8+w1mov x0, x8--> 函数返回值,w8ldp x29, x30, [sp, #0x10+var_s0]add sp,sp,#0x20--> 函数返回的准备(还原x29,lr) 函数栈的还原- 整理代码:
int global = 10; int func(int a, int b){ // 下面的是根据ida的分析,我们加的变量,why??看分析 // 类型:需要和参数保持一致,why??对参数的保护生成的局部变量 int var_s0, var_4, var_8, var_c; var_4 = a; var_8 = b; printf("haha"); // 获取global变量的地址, int类型是当前不确定的,为了和参数保持一致,写的,在函数外定义一个global全局变量,global值不确定,先写一个初始值 int * x30 = &global; int w1 = var_4; int w8 = var_8; w8=w1+w8; w1 = *(x30); // w1=*(&global)--> w1=global w8=w8+w1; return w8; // 函数有返回值,猜测和参数类型一致 }- 最终
int blobal = 10; // 值??,类型?? int func(int a, int b){ // 参数类型?? 返回类型?? printf("haha"); return a+b+global; }