最近對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)。這基本上是一個套路,看多了自然就知道了,都不用停下來一條條分析。