最近對iOS逆向工程很感興趣。
目前iOS逆向的書籍有: 《Hacking and Securing IOS Applications》, 《iOS Hacker's Handbook》中文書籍有《iOS應用逆向工程:分析與實戰》 中文博客有: 程序員念茜的《iOS安全攻防系列》 英文博客有:Prateek Gianchandani的iOS 安全系列博客 這些資料中都涉及到有ARM匯編,但都只是很泛地用到,並沒有對iOS上的ARM匯編進行比較詳細的講解。因此,經過一系列的學習對iOS下的ARM有了一定的理解。在此打算用幾篇博文記錄下來,備忘之,分享之, 限於本人水平有限,如有錯誤請不吝賜教。 我們先講一些ARM匯編的基礎知識。(我們以ARMV7為例,最新iPhone5s上的64位暫不討論) 基礎知識部分: 首先你介紹一下寄存器: R0-R3:用於函數參數及返回值的傳遞 R4-R6, R8, R10-R11:沒有特殊規定,就是普通的通用寄存器 R7:棧幀指針(Frame Pointer).指向前一個保存的棧幀(stack frame)和鏈接寄存器(link register, lr)在棧上的地址。 R9:操作系統保留 R12:又叫IP(intra-procedure scratch ), 要說清楚要費點筆墨,參見http://blog.csdn.net/gooogleman/article/details/3529413 R13:又叫SP(stack pointer),是棧頂指針 R14:又叫LR(link register),存放函數的返回地址。 R15:又叫PC(program counter),指向當前指令地址。 CPSR:當前程序狀態寄存器(Current Program State Register),在用戶狀態下存放像condition標志中斷禁用等標志的。 在其它系統狀態中斷狀等狀態下與CPSR對應還有一個SPSR,在這裡不詳述了。 另外還有VFP(向量浮點運算)相關的寄存器,在此我們略過,感興趣的可以從後面的參考鏈接去查看。 基本的指令: add 加指令 sub 減指令 str 把寄存器內容存到棧上去 ldr 把棧上內容載入一寄存器中 .w是一個可選的指令寬度說明符。它不會影響為此指令的行為,它只是確保生成 32 位指令。Infocenter.arm.com的詳細信息 bl 執行函數調用,並把使lr指向調用者(caller)的下一條指令,即函數的返回地址 blx 同上,但是在ARM和thumb指令集間切換。 bx bx lr返回調用函數(caller)。 接下來是函數調用的一些規則。 一. 在iOS中你需要使用BLX,BX這些指令來調用函數,不能使用MOV指令(具體意義下面會說) 二. ARM使用一個棧來來維護函數的調用及返回。ARM中棧是向下生長(由高地址向低地址生長的)。 函數調用前後棧的布局如圖一(引用的蘋果iOS ABI Reference): 圖(一) SP(stack pointer)指向棧頂(棧低在高地址)。棧幀(stack frame)其實就是通過R7及存在棧上的舊R7來標識的棧上的一塊一塊的存儲空間。棧幀包括: 參數區域(parameter area),存放調用函數傳遞的參數。對於32位ARM,前4個參數通過r0-r3傳遞,多余的參數通過棧來傳遞,就是存放在這個區域的。 鏈接區域(linkage area),存放調用者(caller)的下一條指令。 棧幀指針存放區域(saved frame pointer),存放調用函數的棧幀的底部,標識著調用者(caller)棧幀的結束及被調用函數(callee)的棧幀開始。 局部變量存儲區(local storage area)。用於存被調函數(callee)的局部變量及在被調用函數(callee)結束後反回調用函數(call)之前需要恢復的寄存器內容。 寄存器存儲區(saved registers area)。Apple的文檔中是這樣說的。但我認為這個區域和local storage area相鄰且干的事也是存放需要恢復的寄存器內容,因此我覺得要不就把這個區域在概念上不區分出來,要不就把存放需要恢復的寄存器這項功能從local storage area中分出來。 當然這些都只是概念上的,其實實質上是沒有區別的。 接下來看看在調用子函數開始及結尾時所要做的事情。(官方叫序言和結語, prologs and epilogs) 調用開始: LR入棧 R7入棧 R7 = SP地址。在經過前面兩條入棧指令後,SP指向的地址向下移動,再把SP賦值給R7, 標志著caller棧幀的結束及callee的棧幀的開始 將callee會修改且在返回caller時需要恢復的寄存器入棧。 分配棧空間給子程序使用。由於棧是從高地址向低地址生長,所以通常使用sub sp, #size來分配。 調用結尾: 釋放棧空間。add sp, #size指令。 恢復所保存的寄存器。 恢復R7 將之前存放的LR從棧上彈出到PC,這樣函數就返回了。 -----------------------------------------------------------華麗的分割線------------------------------------------------------------- 實戰部分(一): 用XCode創建一個Test工程,新建一個.c文件,添加如下函數: 1 2 3 4 5 6 7 #include <stdio.h> int func(int a, int b, int c, int d, int e, int f) { int g = a + b + c + d + e + f; return g; } 查看匯編語言: 在XCode左上角選中targe 在真機下編譯,這樣產生的才是ARM匯編,不然在模擬器下生成的是x86匯編。 點擊 XCode => Product => Perform Action => Assemble file.c 生成匯編代碼。 代碼很多,有很多"."開頭的".section", ".loc"等,這些是匯編器需要的,我們不用去管。把這些"."開頭的及注釋增掉後,代碼如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 _func: .cfi_startproc Lfunc_begin0: add r0, r1 Ltmp0: ldr.w r12, [sp] add r0, r2 ldr.w r9, [sp, #4] add r0, r3 add r0, r12 add r0, r9 bx lr Ltmp2: Lfunc_end0: _func:表示接下來是func函數的內容。Lfunc_begin0及Lfunc_end0標識函數定義的起止。函數起止一般是"xxx_beginx:"及"xxx_endx:" 下面來一行行代碼解釋: add r0, r1 將參數a和參數b相加再把結果賦值給r0 ldr.w r12, [sp] 把最的一個參數f從棧上裝載到r12寄存器 add r0, r2 把參數c累加到r0上 ldr.w r9, [sp, #4] 把參數e從棧上裝載到r9寄存器 add r0, r3 累加d累加到r0 add r0, r12 累加參數f到r0 add r0, r9 累加參數e到r0 至此,全部的a到f 共6個值全部累加到r0寄存器上。前面說了r0是存放返回值的。 bx lr: 返回調用函數。 -----------------------------------------------------------華麗的分割線------------------------------------------------------------- 實戰部分(二): 為了讓大家看清楚函數調用時棧上的變化,下面以一個有三個函數,兩個調用的C代碼的匯編代碼為例講解一下。 上代碼: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <stdio.h> __attribute__((noinline)) int addFunction(int a, int b, int c, int d, int e, int f) { int r = a + b + c + d + e + f; return r; } __attribute__((noinline)) int fooFunction(int a, int b, int c, int d, int f) { int r = addFunction(a, b, c, d, f, 66); return r; } int initFunction() { int r = fooFunction(11, 22, 33, 44, 55); return r; } 由於我們是要看函數調用及棧的變化的,所以在這裡我們加上__attribute__((noinline))防止編譯器把函數內聯(如果你不懂內聯,請google之)。 在XCode左上角選中targe 在真機下編譯,這樣產生的才是ARM匯編,不然在模擬器下生成的是x86匯編。 點擊 XCode => Product => Perform Action => Assemble file.c 生成匯編代碼, 如下: 為了能更符合我們人的思考方式,我們從調用函數講起。 initFunction: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 _initFunction: .cfi_startproc Lfunc_begin2: @ BB#0: push {r7, lr} mov r7, sp sub sp, #4 movs r0, #55 movs r1, #22 Ltmp6: str r0, [sp] movs r0, #11 movs r2, #33 movs r3, #44 bl _fooFunction add sp, #4 pop {r7, pc} Ltmp7: Lfunc_end2: 還是一行行的解釋: push {r7, lr} 就是前面基礎知識部分說的函數調用的序言(prologs)部分的1, 2兩條,將lr, r7 存到棧上去 mov r7, sp 序言(prolog)之3。 sub sp, #4 在棧上分配一個4字節空間用來存放局部變量, 即參數。前面我們說過,r0-r3可以傳遞4個參數,但超過的只能通過棧來傳遞。 movs r0, #55 把立即數55存入r0 movs r1, #22 把22存入r1 str r0, [sp] 把r0的值存入棧指針sp指向的內存。即棧上存了參數55 接下來三條指令 moves r0, #11 moves r2, #33 moves r3, #44 把相應的立即數存入指定的寄存器。 到目前為止,r0-r3分別存放了11, 22, 33,44共4個立即數參數,棧上存放了55這一個參數。 bl _fooFunction 調用fooFunction, 調用後跳轉到fooFunction中的情況下面再分析。 add sp, #44 棧指針向上移動4個字節,回收第3個指令 sub sp, #4分配的空間。 pop {r7, pc} 恢復第一條指令push {r7, lr}到棧中的值, 把之前的lr值賦給pc。注意:在進入initFunction的時候lr是調用initFunction的函數的下一條指令,所以現在把當時的lr中的值賦給pc程序計數器,這樣執行lr指向的這一條指令,函數就反回了。 指令1,2, 3是函數序言(prologs),指令9, 10是結語(epilogs)。這基本上是一個套路,看多了自然就知道了,都不用停下來一條條分析。