03-初学汇编之状态寄存器&全局变量分析&简单代码逆向

879 阅读10分钟

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的标志位,例如 subs adds CMP等等一些算术指令和逻辑指令

  • 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的精确存储地址0x1000dfd49

      lldb调试:

    • 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
    • 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, w8 mov x1, w9 参数有2个
    • mov w8,#0xA mov 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 a int b(注意,类型是无法确定的,根据main里面的分析,来填写)
    • sub sp, sp, #0x20 stp x29,x30, [sp, #0x10+var_s0] add x29, sp, #0x10对栈的操作
    • stur w0, [x29, #var_4] 存储函数传入参数(x0~x7是存储函数传入参数的寄存器),可以当成是对之前申明的局部var_4进行赋值。。由此,这句汇编代码是对传入参数进行保护,反汇编可以这样--> var_4 = a
    • str w1, [sp, #0x10+var_8], 同上理解, var_8 = b
    • adrp x0, #aHahha@Page add x0,x0,#aHahaPageOff--> ida已经帮我们分析出,这个在查找 "haha" 字符串; bl _printf 跳转printf函数,整体分析,得出 printf("haha");
    • adrp x30, #_global@page add x30,x30,#_global@pageoff --> 得到一个global全局变量的内存地址(也可以是静态局部变量,暂定全局变量) --> int * x30 = &global; int global=0;--> 全局global变量,变量值无法确定,可以写0,也可以随便写,global的类型也是无法确定的,暂时写int
    • ldur w1, [x29, #var_4] ldr w8,[sp, #0x10+var_8] 从栈中取值到寄存器 --> int w1 = var_4, int w8 = var_8;
    • add w8, w1, w8 --> w8=w1+w8
    • ldr w1, [x30] 从内存中取值--> w1=*(x30)
    • add w8, w8,w1 --> w8=w8+w1
    • mov x0, x8 --> 函数返回值,w8
    • ldp 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;
    }