iOS逆向 02:函數本質(下)

iOS 底層原理 + 逆向 文章匯總

本文主要是講解函數的參數返回值局部變量在匯編中是如何存儲,以及CPSR標志寄存器

函數的參數和返回值

  • arm64下,函數的參數是存放在x0-x7(w0-w7)這8個寄存器里面的,如果超過8個參數,就會入棧

    • 如果自定義函數時,參數最好不要超過6個(因為有兩個隱藏參數 self,_cmd

    • 如果函數需要多個參數,可以傳入數組、結構體、指針等類型

  • 函數的返回值放在x0寄存器

    • 如果返回值大于8個字節,就會利用內存傳遞

查看系統的參數匯編

下面通過系統中對函數的匯編來查看系統對參數、返回值是如何操作的

int sum(int a, int b){
    return a + b;
}
- (void)viewDidLoad{
    [super viewDidLoad];
    sum(10, 20);
}
  • 查看匯編,在跳轉到sum函數之前,已經將參數存入了w0、w1


    系統的參數匯編-01
  • 在sum函數中,讀取w0、w1,放入w8、w9。然后將相加后的結果放入w0(即返回值在w0寄存器)


    系統的參數匯編-02

自己優化實現suma

運行發現,其結果與sum函數是一致的,結果都是30

<!--asm.s-->
.text
.global _suma

_suma:
    add x0, x0, x1
    ret 
    
<!--調用-->
int suma;
- (void)viewDidLoad{
    [super viewDidLoad];
    suma(10, 20);
}

編譯器優化

來看以下代碼的匯編

int test(int a, int b, int c, int d, int e, int f, int g, int h, int i){
    return a + b + c + d + e + f + g + h + i;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    test(1, 2, 3, 4, 5, 6, 7, 8, 9);
}
  • 在test函數斷住,查看匯編

    未優化分析-01

    以下是viewDidLoad棧空間的存入分析過程
    未優化分析-02

    下圖是對匯編代碼中入棧過程的一個圖示
    未優化分析-03

  • 以下是test函數的匯編分析

    未優化分析-04

編譯器優化

  • debug模式改成release模式,此時再來查看匯編代碼,發現沒有test函數,被優化掉了


    優化分析-01
  • 如果非要執行test,可以這樣寫

- (void)viewDidLoad {
    [super viewDidLoad];
    printf("%d", test(1, 2, 3, 4, 5, 6, 7, 8, 9));
}

匯編代碼如下,發現優化后的test函數在匯編中,其本質是一個,也就是test函數的返回值。(相當于將printf("%d", test(1, 2, 3, 4, 5, 6, 7, 8, 9));直接優化成了printf("%d", 45);

優化分析-02

通過匯編實現函數

  • 定義函數聲明及調用
int funcA(int a, int b);

- (void)viewDidLoad {
    [super viewDidLoad];
    int a = funcA(10, 20);
    printf("%d", a);
}
  • 匯編實現funcA
.text
.global _funcA, _sum

_funcA:
    sub sp, sp, #0x10
    stp x29, x30, [sp] //保護lr
    bl _sum
    ldp x29, x30, [sp]
    add sp, sp, #0x10
    ret

_sum:
    add x0, x0, x1
    ret
    
<!--簡寫-->

_funcA:
    stp x29, x30, [sp, #-0x10]!
    bl _sum
    ldp x29, x30, [sp], #0x10
    ret

_sum:
    add x0, x0, x1
    ret
    
<!--巧合:還可以將bl替換成b-->
//b就是簡單跳轉,在逆向中用于繞過某些代碼(例如安全監測)

_funcA:
    b _sum

_sum:
    add x0, x0, x1
    ret

運行結果如下所示


匯編實現函數結果

說明:

  • 關于b指令:只是跳轉,不改變lr寄存器
  • 拉伸棧空間和參數個數有沒有關系?:有關系,當參數越多時,如果寄存器放不下,就需要用到內存。就會將棧空間放大,影響棧空間的不僅僅是參數個數,還有局部變量

函數的返回值

如果返回值是一個結構體類型,一個寄存器放不下,這時是什么情況?

有以下代碼,運行查看其匯編

struct str {
    int a;
    int b;
    int c;
    int d;
    int f;
    int g;
};
struct Str getStr(int a, int b, int c, int d, int f, int g){
    struct Str str1;
    str1.a = a;
    str1.b = b;
    str1.c = c;
    str1.d = d;
    str1.f = f;
    str1.g = g;
    return str1;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    struct Str str2 = getStr(1, 2, 3, 4, 5, 6);
}
  • 斷點運行,以下是viewDidLoad函數的匯編
    返回值調試-01
  • 以下是getStr函數的匯編代碼


    返回值調試-02

結論:如果返回值大于x0的8個字節,也會使用棧空間來存儲

練習

1、如果函數的參數/返回是9個呢?

struct Str{
    int a;
    int b;
    int c;
    int d;
    int e;
    int f;
    int g;
    int h;
    int i;
};

struct Str getStr(int a, int b, int c, int d, int e, int f, int g, int h, int i){
    struct Str str1;
    str1.a = a;
    str1.b = b;
    str1.c = c;
    str1.d = d;
    str1.e = e;
    str1.f = f;
    str1.g = g;
    str1.h = h;
    str1.i = i;
    return str1;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    struct Str str2 = getStr(1, 2, 3, 4, 5, 6, 7, 8, 9);
}
  • 查看viewDidLoad函數匯編

    返回值練習-01

  • 查看getStr函數匯編

    返回值練習-02

    結論:發現前8個參數是存儲到寄存器,第9個參數是存儲到棧空間

2、如果結構體參數是10個呢?

struct Str{
    int a;
    int b;
    int c;
    int d;
    int e;
    int f;
    int g;
    int h;
    int i;
    int j;
};

struct Str getStr(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j){
    struct Str str1;
    str1.a = a;
    str1.b = b;
    str1.c = c;
    str1.d = d;
    str1.e = e;
    str1.f = f;
    str1.g = g;
    str1.h = h;
    str1.i = i;
    str1.j = j;
    return str1;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    struct Str str2 = getStr(1, 2, 3, 4, 5, 6, 7, 8, 9);
  • 查看viewDidLoad函數匯編

    返回值練習-03

  • 查看getStr函數匯編

    返回值練習-04

    • 其中w0-w7都是參數
    • x8用于返回值參照
    • w9用作臨時變量

結論:前8個參數存儲到寄存器,后兩個參數存儲到棧空間

函數的局部變量

  • 函數的局部變量放在里面

分析下面代碼的匯編

int funcB(int a, int b){
    int c = 6;
    return a + b + c;
}

- (void)viewDidLoad {
    [super viewDidLoad]; 
    funcB(10, 20);
}
  • 查看viewDidLoad的匯編

    局部變量-01

  • 查看funcB的匯編

    局部變量-02

總結

  • 局部變量存儲在空間
  • 參數的傳遞是用的寄存器,然后將寄存器的值寫入棧

如果函數有嵌套調用的情況呢?

int funcB(int a, int b){
    int c = 6;
    int d = funcSum(a, b, c);
    return d;
}

int funcSum(int a, int b, int c){
    int d = a + b + c;
    printf("%d", d);
    return d;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    funcB(10, 20);
}
  • 從viewDidLoad到funcB沒有任何變化


    函數嵌套-01
  • 來看funcB的匯編


    函數嵌套-02
    • 參數入棧,其實本質也是對參數的一個保護
    • 所以使用的參數是從棧中獲取,而不是直接通過寄存器獲取
    • stur 操作4個字節時使用

標記/狀態寄存器

標記/狀態寄存器:主要用于控制程序的執行流程

引子

分析下面函數的匯編

void func(){
    int a = 1;
    int b = 2;
    
    if (a == b){
        printf("a == b");
    }else{
        printf("error");
    }
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    func();
}
  • 查看func的匯編
    引子-01
  • 手動更改cpsr的值(從1000 -> 0100)
    引子-02

    輸入c執行,此時的輸出是 a == b
    引子-03

    所以,從這里可以說明,高四位(31-28)是有特殊含義的

CPSR

  • CPU內部的寄存器中,有一種特殊的寄存器(對于不同的處理器,個數和數據結構都可能不同)。這種寄存器在ARM中,被稱為狀態寄存器,即CPSR(current program status register)寄存器

  • CPSR和其他寄存器不一樣,其他寄存器是用來存放數據的,都是整個寄存器具有一個含義,而CPSR寄存器是按位起作用的,即它的每一位都有專門的含義,用于記錄特定的信息

  • 注意:CPSR寄存器是32位

  • CPSR的低8位(包括I、F、T和M[4:0])稱為控制位程序無法修改,除非CPU運行于特權模式下,程序才能修改控制位

  • 中間27-8是保留位,主要作用是為了升級,即更新擴展

  • N、Z、C、V均為條件碼標志位,它們的內容可被算術或邏輯運算的結果所改變,并且可以決定某條指令是否被執行,意義重大!

    CPSR圖示

N(Negative)標志

  • CPSR的第31位是N符號標志位,它記錄相關指令執行后,其結果是否為負
    • 如果為負,則 N=1

    • 如果是非負數,則 N=0

  • 注意:在ARM64的指令集中,有的指令的執行時影響狀態寄存器的。例如adds/sub/or等,他們大都是運算指令(用于進行邏輯/算數運算)
    • adds 執行add運算,且會改變標志位

案例調試

查看以下代碼的匯編

void func(){
     asm(
        "mov w0, #0xffffffff\n"
        "adds w0, w0, #0x0\n"
        );
}
- (void)viewDidLoad {
    [super viewDidLoad];
    
    func();
}
  • 執行mov w0, #0xffffffff:w0賦值-1

    N調試-01

    查看此時的cpsr,高4位是6(即0110)
    N調試-02

  • 執行adds w0, w0, #0x0:因為w0位負數,加上0x0后,仍為負數,所以N標志為1

    N調試-03

    從cpsr的值中可以看出,6變成了8,即 0110 -> 1000,N從0變成了1,符合我們的預期

Z(zero)標志

  • CPSR的第30位是Z,0標志位,它記錄相關指令執行后,其結果是否為0
    • 如果結果為0,則 Z=1(此時能確定前面兩位為01,即N非負,為0,z為1)

    • 如果結果不為0,則 Z=0

對于Z的值,可以這樣看,Z標記相關指令的計算結果是否為0,如果為0,則Z要記錄下”是0“這樣的肯定信息。在計算機中1表示邏輯真,表示肯定,所以當結果為0時Z=1(表示”結果為0“)。如果結果不為0,則Z要記錄下”不是0“這樣的否定信息。在計算機中0表示邏輯假,表示否定,所以當結果不為0時Z=0,表示”結果不為0“

案例調試

目的:驗證z為1時,N必為0

void func(){
    asm(
        "mov w0, #0x0\n"
        "adds w0, w0, #0x0\n"
    );
}
  • 查看此時的CPSR

    Z調試-01

  • 執行mov w0, #0x0adds w0, w0, #0x0,發現N和Z仍然是0和1

  • 修改:將adds中的0x0更改為0x1

void func(){
    asm(
        "mov w0, #0x0\n"
        "adds w0, w0, #0x1\n"
    );
}

查看CPSR,從圖中可以看出,由于為結果為非負數,所以N為0,同時也不為0,則Z為0

Z調試-02

C(Carry)標志

  • CPSR的第29位是C進位標志位,一般情況下,進行無符號數的運算

  • 加法運算:當運算結果產生了進位時(無符號溢出),則C=1,否則C=0

    • 例如 1111 1111 + 1 --> 1 0000 0000,此時的1就保存在C標志位
  • 減法運算(包括CMP):當運算時產生了借位時(無符號數溢出),C=0,否則C=1

    C圖示

    • 例如 0000 0001 - 0000 0010 --> 1111 1111,

對于位數為N的無符號數來說,其對應的二進制信息的最高位,即第N-1位,就是它的最高有效位,而假想存在的第N位,就是相對于最高有效位的更高位,如下所示


無符號的C圖示
進位

當兩個數相加時,有可能產生從最高有效位向更高位的進位,例如兩個32位數據0xaaaaaaaa + 0xaaaaaaaa,將產生進位,由于這個進位值在32位中無法保存,就說這個進位值丟失了。其實CPU在運算時,并不丟棄這個進位制,而是記錄在一個特殊的寄存器的某一位上,ARM下就用C位來記錄這個進位值,例如下面的指令

mov w0,#0xaaaaaaaa;0xa 的二進制是 1010
adds w0,w0,w0; 執行后 相當于 1010 << 1 進位1(無符號溢出) 所以C標記 為 1
adds w0,w0,w0; 執行后 相當于 0101 << 1 進位0(無符號沒溢出) 所以C標記 為 0
adds w0,w0,w0; 重復上面操作
adds w0,w0,w0
  • 首先將CPSR變成0x00000000,然后查看執行mov w0,#0xaaaaaaaa后的CPSR

    進位調試-01

  • 執行第一次adds w0,w0,w0:進位1,C為1

    進位調試-02

  • 執行第二次adds w0,w0,w0:無進位,C由1變成0

    進位調試-03

  • 執行第三次adds w0,w0,w0:有進位,0變成1

    進位調試-04

  • 執行第四次adds w0,w0,w0,無進位,1變成0

    進位調試-05

借位

當兩個數據做減法時,有可能向更高位借位,例如,兩個32位數據0x00000000 - 0x000000ff,將產生借位,借位后,相當于計算0x100000000 - 0x000000ff,得到0xffffff01這個值,由于借了一位,所以C位用來標記借位。C=0,例如下面的指令

mov w0,#0x0
subs w0,w0,#0xff
subs w0,w0,#0xff
subs w0,w0,#0xff
  • 將CPSR修改為0x00000000,執行mov w0,#0x0

    借位調試-01

  • 執行第一次subs w0,w0,#0xff:產生了借位,所以C=0

    借位調試-02

  • 執行第二次subs w0,w0,#0xff:無借位,所以C=1

    借位調試-03

  • 執行第三次subs w0,w0,#0xff:無借位,所以C=1

    借位調試-04

總結

  • 函數參數

    • arm64中,參數是放在x0-x7的8個寄存器中

    • 如果是浮點數,就會用浮點數寄存器

    • 如果超過8個參數就會用棧傳遞

  • 函數返回值

    • 一般函數的返回值使用x0寄存器保存

    • 如果返回值大于了8個字節(x0寄存器大小是8個字節),就會利用內存傳遞返回值

  • 函數局部變量

    • 局部變量存儲在空間
  • 函數的嵌套調用:會將x29、x30寄存器入棧保護

  • 狀態(標志)寄存器 - CPSR

    • arm64中cpsr寄存器(32位)為狀態寄存器

    • 最高4位(28、29、30、31)為標志位

      • N標志(負標記位)
        • 執行結果為負數N=1

        • 執行結果非負數N=0

      • Z標志(0標記位)
        • 結果為0則Z=1

        • 結果非0則Z=0

      • C標志(無符號溢出)
        • 加法:進位 C=1,否則C=0

        • 減法:借位 C=0,否則C=1

      • V標志(有符號溢出)
        • 正數+正數=負數,則V=1

        • 正數+負數=正數,則V=0

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容