iOS逆向 02:函數(shù)本質(zhì)(上)

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

本文的主要目的是理解函數(shù)棧以及涉及的相關(guān)指令

在講函數(shù)的本質(zhì)之前,首先需要講下以下幾個概念棧、SP、FP

常識

  • 棧:是一種具有特殊的訪問方式的存儲空間(即先進后出 Last In First Out, LIFO
    入棧出棧圖示
    • 高地址往低地址存數(shù)據(jù)(存:高-->低

    • 棧空間開辟:往低地址開辟(開辟:高-->低

SP和FP寄存器

  • SP寄存器:在任意時刻會保存棧頂?shù)牡刂?/code>

  • FP寄存器(也稱為x29寄存器):屬于通用寄存器,但是在某些時刻(例如函數(shù)嵌套調(diào)用時)可以利用它保存棧底的地址

注意:

  • arm64開始,取消了32位的LDM、STM、PUSH、POP指令,取而代之的是 ldr/ldp、str/stp(r和p的區(qū)別在于處理的寄存器個數(shù),r表示處理1個寄存器,p表示處理兩個寄存器)

  • arm64中,對棧的操作是16字節(jié)對齊的!!!

以下是arm64之前和arm64之后的一個對比


對比
  • 在arm64之前,棧頂指針是壓棧時一個數(shù)據(jù)移動一個單元

  • 在arm64開始,首先是從高地址往低地址開辟一段棧空間(由編譯器決定),然后再放入數(shù)據(jù),所以不存在push、pop操作。這種情況可以通過內(nèi)存讀寫指令(ldr/ldp、str/stp)對其進行操作

函數(shù)調(diào)用棧

以下是常見的函數(shù)調(diào)用開辟 (sub)以及恢復棧空間 (add)的匯編代碼

//開辟棧空間
sub    sp, sp, #0x40             ; 拉伸0x40(64字節(jié))空間
stp    x29, x30, [sp, #0x30]     ;x29\x30 寄存器入棧保護
add    x29, sp, #0x30            ; x29指向棧幀的底部
... 
ldp    x29, x30, [sp, #0x30]     ;恢復x29/x30 寄存器的值
//恢復棧空間
add    sp, sp, #0x40             ; 棧平衡
ret

內(nèi)存讀寫指令

  • str(store register)指令(能和內(nèi)存和寄存器交互的專門的指令):將數(shù)據(jù)從寄存器中讀出來,存到內(nèi)存中 (即一個寄存器是8字節(jié)-64位

  • ldr(load register)指令:將數(shù)據(jù)從內(nèi)存中讀出來,存到寄存器中

  • 此時ldr和str的變種 ldp和stp 還可以操作2個寄存器(即128位-16字節(jié)

注意:

  • 讀/寫數(shù)據(jù)都是往高地址讀/寫

  • 寫數(shù)據(jù):先拉伸棧空間,再拿sp進行寫數(shù)據(jù),即先申請空間再寫數(shù)據(jù)

練習

使用32個字節(jié)空間作為這段程序的棧空間,然后利用棧將x0和x1的值進行交換

sub sp, sp, #0x20       ;拉伸棧空間32個字節(jié)
stp x0, x1, [sp, #0x10] ;sp往上加16個字節(jié),存放x0和x1
ldp x1, x0, [sp, #0x10] ;將sp偏移16個字節(jié)的值取出來,放入x1和x0,內(nèi)存是temp(寄存器里面的值進行交換了)
add sp, sp, #0x20       ;棧平衡
ret                     ;返回

棧的操作如下圖所示


棧的操作

調(diào)試查看棧

  • 重寫x0、x1的值


    調(diào)試查看棧-01
  • register read sp【查看棧的存儲情況:debug - debug workflow - view Memory

    調(diào)試查看棧-02

  • 然后單步往下執(zhí)行,發(fā)現(xiàn)x0、x1已經(jīng)變成我們寫入的值


    調(diào)試查看棧-03

    )
    查看內(nèi)存變化,發(fā)現(xiàn)sp拉伸了32字節(jié)


    調(diào)試查看棧-04
  • stp x0, x1, [sp, #0x10]:將x0、x1寫入fp偏移0x10的位置,繼續(xù)往下執(zhí)行一步

    調(diào)試查看棧-05

    調(diào)試查看棧-06

    此時sp的值并沒有變化,還是指向40
    調(diào)試查看棧-07

  • ldp x1, x0, [sp, #0x10]:讀取x0,x1的數(shù)據(jù)并交換,繼續(xù)往下執(zhí)行一步,此時內(nèi)存并沒有變化

    調(diào)試查看棧-08

    疑問:再來看sp是否有變化?
    從結(jié)果來看,也沒有變化。所以這里只是讀出來進行的交換,并不會導致內(nèi)存變化
    調(diào)試查看棧-09

  • add sp, sp, #0x20:繼續(xù)執(zhí)行一步,走到棧平衡,即sp恢復了,此時的a和b仍然在內(nèi)存中,等待著下一輪棧拉伸后數(shù)據(jù)的寫入覆蓋。如果此時讀取,讀取到的是垃圾數(shù)據(jù)

    調(diào)試查看棧-10

疑問:棧空間不斷開辟,死循環(huán),會不會崩潰?

在這里我們將會處理上篇iOS逆向 01:初識匯編文章中文末遺留的問題

下面我們通過一個匯編代碼來演示

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

_B:
    sub sp,sp,#0x20
    stp x0,x1,[sp,#0x10]
    ldp x1,x0,[sp,#0x10];寄存器里面的值進行交換
    bl _B
    add sp,sp,#0x20
    ret
    
<!--調(diào)用-->
int B();

int main(int argc, char * argv[]) {
    B();
}

運行結(jié)果發(fā)現(xiàn):死循環(huán)會崩潰,會導致堆棧溢出

死循環(huán)崩潰圖示

bl 、ret指令

  • b 標號 :跳轉(zhuǎn)

  • bl標號

    • 將下一條指令的地址放入lr(x30)寄存器(lr保存的是回家的路)(即l)
    • 轉(zhuǎn)到標號處執(zhí)行指令(即b)


      bl圖示

      等到B函數(shù)ret時,通過lr獲取回家的路(注:lr就是保存回家的路)

  • ret

    • 默認使用lr(x30)寄存器的值,通過底層指令提示CPU此處作為下條指令地址

    • arm64平臺的特色指令,它面向硬件做了優(yōu)化處理的

練習

下面通過匯編代碼來演示bl、ret指令

.text
.global _A, _B

_A:
    mov x0. #0xaaaa
    bl _B
    mov x0, #0xaaaa
    ret

_B:
    mov x0, #0xbbbb
    ret
  • 斷點運行

    演示bl、ret指令-01

    疑問:發(fā)現(xiàn)A和print之間你還有幾個匯編操作,這個是什么意思呢?
    演示bl、ret指令-02

  • 執(zhí)行mov x0. #0xaaaa:x0變成aaaa,此時此刻lr寄存器保存的是5f34

    演示bl、ret指令-03

  • 驗證lr是否保存的是5f34,通過查看寄存器發(fā)現(xiàn)結(jié)果與預期是一致的

    演示bl、ret指令-04

  • 繼續(xù)執(zhí)行bl _B,跳轉(zhuǎn)到B,此時的lr會變成A中bl的下一條指令的地址5eb8

    演示bl、ret指令-05

  • 執(zhí)行完B中的mov x0, #0xbbbb,x0變成bbbb

    演示bl、ret指令-06

  • 執(zhí)行B中的ret,會回到A中5eb8

    演示bl、ret指令-07

  • 繼續(xù)執(zhí)行A中的ret,會再次回到5eb8

    演示bl、ret指令-08

    走到這里,發(fā)現(xiàn)死循環(huán)了,主要是因為lr一直是5eb8,ret只會看lr。其中pc是指接下來要執(zhí)行的內(nèi)存地址,ret是指讓CPU將lr作為接下來執(zhí)行的地址(相當于將lr賦值給pc)
    演示bl、ret指令-09

疑問1:此時B回到A沒問題,那么A回到viewDidload怎么回呢?

  • 需要在A的bl之前保護lr寄存器
    • 疑問2:是否可以保存到其他寄存器上?
      答案是不可以,原因是不安全,因為你不確定這個寄存器會在什么時候被別人使用
    • 正確做法:保存到棧區(qū)域

系統(tǒng)中函數(shù)嵌套是如何返回?
下面我們來看下系統(tǒng)是如何操作的,例如:d -> c -> viewDidLoad

void d(){
}
void c(){
    d();
    return;
}
- (void)viewDidLoad{
    [super viewDidLoad];
    printf("A");
    c();
    printf("B");
}
  • 查看匯編,斷點斷在c函數(shù)


    函數(shù)嵌套調(diào)試-01
  • 進入c函數(shù)的匯編


    函數(shù)嵌套調(diào)試-02
    • stp x29,x30,[sp,#-0x10]!:邊開辟棧,邊寫入,其中 x29就是fp,x30是lr!表示將這里算出來的結(jié)果,賦值給sp

    • lsp x29,x30,[sp],#0x10:讀取sp指向地址的數(shù)據(jù),放入x29、x30,然后,,#0x10表示將sp+0x10,賦值給sp

  • 結(jié)論:當有函數(shù)嵌套調(diào)用時,將上一個函數(shù)的地址通過x30(即lr)放在棧中保存,保證可以找到回家的路,如下圖所示

    函數(shù)嵌套調(diào)試-03

自定義匯編代碼完善:_A中保存回家的路
所以根據(jù)系統(tǒng)的函數(shù)嵌套操作,最終在_A中增加了如下匯編代碼,用于保存回家的路

<!--導致死循環(huán)的匯編代碼-->
_A:
    mov x0. #0xaaaa
    bl _B
    mov x0, #0xaaaa
    ret
    
<!--增加lr保存:可以找到回家的路-->
_A:
    sub sp, sp, #0x10  //拉伸
    str x30, [sp]     //存
    mov x0, #0xaaaa
    //保護lr寄存器,存儲到棧區(qū)域
    bl _B
    mov x0, #0xaaa
    ldr x30, [sp]      //修改lr,用于A找到回家的路
    add sp, sp, #0x10 //棧平衡
    ret

修改_A、_B:改成簡寫形式

  • 其中lrx30的一個別名
_A:
    sub sp, sp, #0x10  //拉伸
    str x30, [sp]     //存
    mov x0, #0xaaaa
    //保護lr寄存器,存儲到棧區(qū)域
    bl _B
    mov x0, #0xaaa
    ldr x30, [sp]      //修改lr,用于A找到回家的路
    add sp, sp, #0x10 //棧平衡
    ret

_B:
    mov x0, #0xbbbb
    ret
    
<!--改成簡寫形式-->
_A:
    //sub sp, sp, #0x10  //拉伸
    //str x30, [sp]     //存
    str x30, [sp, #-0x10]
    mov x0, #0xaaaa
    //保護lr寄存器,存儲到棧區(qū)域
    bl _B
    mov x0, #0xaaa
    //ldr x30, [sp]      //修改lr,用于A找到回家的路
    //add sp, sp, #0x10 //棧平衡
    ldr x30, [sp], #0x10 //將sp的值讀取出來,給到x30,然后sp += 0x10
    ret

_B:
    mov x0, #0xbbbb
    ret

斷點調(diào)試

  • 查看此時sp寄存器的地址


    函數(shù)嵌套調(diào)試-04
  • 執(zhí)行str x30, [sp, #-0x10],繼續(xù)查看sp,發(fā)現(xiàn)sp變化了,但是此時lr沒變

    函數(shù)嵌套調(diào)試-05

    查看0x16f5a1c50的memory,此時放入的是lr的值 861f2c,即ViewDidLoad中的bl下一條指令的地址,目前只放了8個字節(jié)(1個寄存器)
    函數(shù)嵌套調(diào)試-06

  • 執(zhí)行A中的mov x0, #0xaaaa:x0變成aaaa

    函數(shù)嵌套調(diào)試-07

  • 執(zhí)行A中的bl _B,跳轉(zhuǎn)到B,此時lr變成 1e94,x0變成bbbb

    函數(shù)嵌套調(diào)試-08

  • 執(zhí)行B的ret:從B回到A,此時lr還是 1e94

    函數(shù)嵌套調(diào)試-09

  • 執(zhí)行A中的ldr x30, [sp], #0x10

    函數(shù)嵌套調(diào)試-10

    發(fā)現(xiàn)此時sp也變了,從0x16f5a1c50->0x16f5a1c60。從這里可以看出,A找到了回家的路
    函數(shù)嵌套調(diào)試-11

疑問:為什么是拉伸16字節(jié),而不是8字節(jié)呢?
通過手動嘗試,有以下說明:

  • 寫入沒問題

  • 讀取時會崩潰:因為sp中,對棧的操作必須是16字節(jié)對齊的,所以會在做棧的操作時就會崩潰

    函數(shù)嵌套調(diào)試-12

x30寄存器

  • x30寄存器存放的是函數(shù)的返回地址,當ret指令執(zhí)行時刻,會尋找x30寄存器保存的地址值

  • 注意:在函數(shù)嵌套調(diào)用時,需要將x30入棧

  • lr是x30的別名

  • sp棧里面的操作必須是16字節(jié)對齊,崩潰是在棧的操作時掛的

總結(jié)

  • 棧:是一種具有特殊的訪問方式的存儲空間(后進先出,Last in First out, LIFO

    • ARM64里面對棧的操作16字節(jié)對齊
  • SPFP寄存器

    • SP寄存器在任意時刻會保存棧頂?shù)牡刂?/code>
    • FP寄存器也稱為x29寄存器,屬于通用寄存器,但是在某些時刻利用它保存棧底的地址
  • 棧的讀寫指令

    • 讀:ldr(load register)指令 LDR、LDP

    • 寫:str(store register)指令 STR、STP

  • 匯編練習

    • 指令
      • sub sp,sp,$0x10 ;拉伸棧空間18字節(jié)

      • stp x0,x1,[sp] ;sp所在位置存放x0、x1

    • 簡寫
      • str x0,x1,[sp,$-0x10]!(!就是將[]里面的結(jié)果賦值給sp)
  • bl指令

    • 跳轉(zhuǎn)指令:bl 標號,表示程序執(zhí)行到標號處,將下一條指令的地址保存到lr寄存器

    • B代表著跳轉(zhuǎn)

    • L表示lr(x30)寄存ios_reverse_02器

  • ret指令

    • 類似函數(shù)的return
    • 讓CPU執(zhí)行l(wèi)r寄存器所指向的指令
  • 避免嵌套函數(shù)無法回去:需要保護bl(即lr寄存器,存放回家的路),保存在當前函數(shù)自己的棧空間

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

推薦閱讀更多精彩內(nèi)容