iOS彙編入門教程(一)ARM64彙編基礎

前言

對於應用層開發人員而言,僅僅掌握Objective-C和系統框架便可較好的完成開發,但在涉及到應用加固、逆向分析等內容時僅有應用層開發技能就會顯得很是的無力,所以掌握彙編對於突破iOS開發水平的瓶頸十分有效。html

一個例子

以反調試爲例,咱們知道,經過調用ptrace函數能夠阻止調試器依附。ios

ptrace(31, 0, 0, 0)
複製代碼

這種方式可以被函數hook輕易破解,例如使用facebook的fishhook。 爲了防止函數被hook,咱們能夠將函數調用轉爲經過彙編發起系統調用,即便用下面的代碼。git

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__指令可以將彙編代碼嵌入咱們的函數中,構成反調試方法。github

// 使用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應用安全和底層研究很是有利。shell

入門攻略

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
複製代碼

完整的彙編代碼以下。iphone

.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函數的調用,節選這一段彙編代碼以下。jsp

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.知兵的知乎專欄

2.劉坤的彙編入門文章

總結

掌握ARM彙編可以幫助開發者更好地瞭解編譯器和CPU的工做原理,除了可以指導編碼外,還可以擴寬視野,經過反編譯分析一些閉源代碼的邏輯或是進行一些安全加固,所以在彙編上付出時間是十分值得的。

參考資料

1.知兵. iOS調試進階

2.ARM官方文檔

3.反調試和繞過

相關文章
相關標籤/搜索