前言
對於應用層開發人員而言,僅僅掌握Objective-C和系統框架即可較好的完成開發,但在涉及到應用加固、逆向分析等內容時僅有應用層開發技能就會顯得非常的無力,因此掌握彙編對於突破iOS開發水平的瓶頸十分有效。
一個例子
以反除錯為例,我們知道,通過呼叫ptrace函式可以阻止偵錯程式依附。
ptrace(31, 0, 0, 0)
複製程式碼
這種方式能夠被函式hook輕易破解,例如使用facebook的fishhook。 為了防止函式被hook,我們可以將函式呼叫轉為通過彙編發起系統呼叫,即使用下面的程式碼。
mov x0, #31
mov x1, #0
mov x2, #0
mov x3, #0
mov x16, #26
svc #0x80
複製程式碼
其中x0-x3儲存的為函式入參,x16儲存的為函式編號,通過Apple提供的System Call Table 可以查出ptrace的編號為26,最後一句指令發起了系統呼叫。 通過使用__asm__指令能夠將彙編程式碼嵌入我們的函式中,構成反除錯方法。
// 使用inline方式將函式在呼叫處強制展開,防止被hook和追蹤符號
static __attribute__((always_inline)) void anti_debug() {
// 判斷是否是ARM64處理器指令集
#ifdef __arm64__
// volatile修飾符能夠防止彙編指令被編譯器忽略
__asm__ __volatile__(
"mov x0, #31\n"
"mov x1, #0\n"
"mov x2, #0\n"
"mov x3, #0\n"
"mov x16, #26\n"
"svc #0x80\n"
);
#endif
}
複製程式碼
雖然上面的反除錯機制並不完善,但是比直接呼叫ptrace要好上很多倍,從這一點來看,掌握彙編技能對於iOS應用安全和底層研究非常有利。
入門攻略
iOS裝置主要使用的為ARM64彙編,因此本文主要介紹ARM64彙編的入門技巧。 彙編入門最難的地方在於對棧的理解,彙編的所有指令操作都是圍繞棧實現的,在彙編中,沒有變數的概念,只有暫存器和記憶體。
棧
彙編中的棧是由高地址向低地址生長的資料結構,sp指標永遠指向棧頂,需要記住的是,在某位置進行儲存時,是向高地址進行的,下面以一個簡單的例子講解彙編的棧操作。 我們以一段簡單的C程式碼為例。
// hello.c
#include <stdio.h>
int test(int a, int b) {
int res = a + b;
return res;
}
int main() {
int res = test(1, 2);
return 0;
}
複製程式碼
使用clang可以將其編譯為特定指令集的彙編程式碼,這裡我們將其編譯為ARM64指令集的彙編程式碼。
clang -S -arch arm64 -isysroot `xcrun --sdk iphoneos --show-sdk-path` hello.c
複製程式碼
完整的彙編程式碼如下。
.section __TEXT,__text,regular,pure_instructions
.ios_version_min 11, 2
.globl _test
.p2align 2
_test: ; @test
; BB#0:
sub sp, sp, #16 ; =16
str w0, [sp, #12]
str w1, [sp, #8]
ldr w0, [sp, #12]
ldr w1, [sp, #8]
add w0, w0, w1
str w0, [sp, #4]
ldr w0, [sp, #4]
add sp, sp, #16 ; =16
ret
.globl _main
.p2align 2
_main: ; @main
; BB#0:
sub sp, sp, #32 ; =32
stp x29, x30, [sp, #16] ; 8-byte Folded Spill
add x29, sp, #16 ; =16
orr w0, wzr, #0x1
orr w1, wzr, #0x2
stur wzr, [x29, #-4]
bl _test
mov w1, #0
str w0, [sp, #8]
mov x0, x1
ldp x29, x30, [sp, #16] ; 8-byte Folded Reload
add sp, sp, #32 ; =32
ret
.subsections_via_symbols
複製程式碼
本節我們只討論棧操作,因此忽略main函式和printf呼叫部分,我們只看對test函式的呼叫,節選這一段彙編程式碼如下。
sub sp, sp, #16 ; =16
str w0, [sp, #12]
str w1, [sp, #8]
ldr w0, [sp, #12]
ldr w1, [sp, #8]
add w0, w0, w1
str w0, [sp, #4]
ldr w0, [sp, #4]
add sp, sp, #16 ; =16
ret
複製程式碼
首先介紹一下基本指令和指令的學習方式,要查詢某個指令如何使用,最好的方式是去查詢ARM公司提供的官方文件,在官方文件頁面可以直接搜尋指令並檢視用法和例程,本文會簡單講解上面的彙編程式碼中出現的指令。
sub用於對暫存器實施減法,sub a, b, c
等價於a = b - c
,在ARM彙編中,目的運算元一般出現最前方,例如mov ra, rb
代表將rb暫存器的值複製到ra暫存器。add和sub同理,只是將減法變成了加法。
str和ldr是一對指令,str的全稱是store register,即將暫存器的值儲存到記憶體中,ldr的全稱是load register,即將記憶體中的值讀到暫存器,因此他們的第一個引數都是暫存器,第二個引數都是記憶體地址。[sp, #12]
代表sp+12
這個地址,同理[sp, #-12]
代表sp-12
這個地址。注意這裡的數字都是以位元組為單位的偏移量,以str w0, [sp, #12]
為例,w是4位元組的暫存器,這個指令代表將w0暫存器的值儲存在sp+12這個地址上,由於w0有4個位元組,所以儲存後會佔據sp+12~sp+16
這個記憶體區域。
下面將分段講解這段彙編程式碼,在編譯器生成彙編時,首先會計算需要的棧空間大小,並利用sp指標向低地址開闢相應的空間,我們再來看一下test函式。
int test(int a, int b) {
int res = a + b;
return res;
}
複製程式碼
這裡涉及了3個int變數,分別是a、b、res,int變數佔據4個位元組,因此一共需要12個位元組,但ARM64彙編為了提高訪問效率要求按照16位元組進行對齊,因此需要16byte的空間,也就是需要在棧上開闢16位元組的空間,我們來看彙編的第一句,正是將sp指標下移16位元組。
sub sp, sp, #16
複製程式碼
sp下移16後,留下了4個4位元組的記憶體空格,共計16位元組,我們繼續看下面的句子。
str w0, [sp, #12]
str w1, [sp, #8]
複製程式碼
這兩句的含義是將w0儲存在sp+12的格子中,w1儲存在sp+8的格子中,上面的例子中提到
x0, x1等暫存器將順序存放函式的入參,x0和w0是同一個暫存器的不同尺寸形式,x0為8位元組,w0為x0的前4個位元組,因此w0是函式的第一個入參a,w1是函式的第二個入參b,由於儲存是從低地址到高地址的,所以a將佔據sp+12~sp+16
,同理b將佔據sp+8~sp+12
,則棧的結構變為下圖。
按照“上帝視角”,接下來test函式應該將a和b相加,需要注意的是,只有暫存器才能參與運算,因此接下來的彙編程式碼又將變數的值從記憶體中讀出,進行相加運算。
ldr w0, [sp, #12]
ldr w1, [sp, #8]
add w0, w0, w1
複製程式碼
由此可見先儲存再讀取後運算其實是多餘的,這是沒有進行編譯優化的結果,學習不進行編譯優化的彙編更能讓我們理解其工作機制。
接下來的程式碼將w0存入了sp+4,也就是res變數的記憶體區域。
str w0, [sp, #4]
複製程式碼
接下來就要進行返回了,在例子中我們提到,函式的返回值一般儲存在x0暫存器中返回,因此我們需要將res的值載入x0暫存器。
ldr w0, [sp, #4]
複製程式碼
這裡之所以使用w暫存器,是因為int為4位元組,這也就是型別轉換時帶來資訊丟失的原因,例如從long到int的轉換就類似於將x暫存器的值以w的形式進行儲存。最後的程式碼為將棧還原,並返回到函式呼叫處繼續向下執行。
add sp, sp, #16
ret
複製程式碼
顯然,經過這樣的操作,棧被完全還原到了函式呼叫以前的樣子,需要注意的細節是,棧空間中的記憶體單元並未被清空,這也就導致下一次使用低地址的棧時,未初始化單元的值是不確定的,這也就是區域性變數不初始化值隨機的根本原因。
通過上面的例子,我們對棧有了基本的認識,彙編的操作基本都是對棧進行的,只要理解了棧機制,只需要學習各種指令,即可掌握足夠使用的彙編技能。
深入
在瞭解了棧以後,就可以看一些較為複雜的彙編片段來進行學習了,初級階段可以嘗試看著函式寫彙編程式碼,高階階段要求能夠看著彙編還原成函式邏輯,本文僅僅介紹入門基礎,下面推薦一些大牛的部落格供大家深入學習彙編技能。
1.知兵的知乎專欄
總結
掌握ARM彙編能夠幫助開發者更好地瞭解編譯器和CPU的工作原理,除了能夠指導編碼外,還能夠擴寬視野,通過反編譯分析一些閉原始碼的邏輯或是進行一些安全加固,因此在彙編上付出時間是十分值得的。
參考資料
2.ARM官方文件
3.反除錯和繞過