你好,歡迎來到IOS教程網

 Ios教程網 >> IOS教程 >> 關於IOS教程 >> ARM匯編逆向iOS 實戰

ARM匯編逆向iOS 實戰

編輯:關於IOS教程

我們先講一些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), 要說清楚要費點筆墨,後續再詳細介紹

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文件,添加如下函數:

#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"等,這些是匯編器需要的,我們不用去管。把這些"."開頭的及注釋增掉後,代碼如下:

_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相加再把結果賦值給r0ldr.w r12, [sp] 把最的一個參數f從棧上裝載到r12寄存器add r0, r2 把參數c累加到r0上ldr.w r9, [sp, #4] 把參數e從棧上裝載到r9寄存器add r0, r3 累加d累加到r0add r0, r12 累加參數f到r0add r0, r9 累加參數e到r0

至此,全部的a到f 共6個值全部累加到r0寄存器上。前面說了r0是存放返回值的。

bx lr: 返回調用函數。

-----------------------------------------------------------華麗的分割線-------------------------------------------------------------

實戰部分(二):

為了讓大家看清楚函數調用時棧上的變化,下面以一個有三個函數,兩個調用的C代碼的匯編代碼為例講解一下。

上代碼:

#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:

_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:

還是一行行的解釋:

1.push {r7, lr} 就是前面基礎知識部分說的函數調用的序言(prologs)部分的1, 2兩條,將lr, r7 存到棧上去

2.mov r7, sp 序言(prolog)之3。

3.sub sp, #4 在棧上分配一個4字節空間用來存放局部變量, 即參數。前面我們說過,r0-r3可以傳遞4個參數,但超過的只能通過棧來傳遞。

4.movs r0, #55 把立即數55存入r0

5.movs r1, #22 把22存入r1

6.str r0, [sp] 把r0的值存入棧指針sp指向的內存。即棧上存了參數55

7.接下來三條指令moves r0, #11 moves r2, #33 moves r3, #44 把相應的立即數存入指定的寄存器。 到目前為止,r0-r3分別存放了11, 22, 33,44共4個立即數參數,棧上存放了55這一個參數。

8.bl _fooFunction 調用fooFunction, 調用後跳轉到fooFunction中的情況下面再分析。

9.add sp, #4 棧指針向上移動4個字節,回收第3個指令sub sp, #4分配的空間。

10.pop {r7, pc} 恢復第一條指令push {r7, lr}到棧中的值, 把之前的lr值賦給pc。注意:在進入initFunction的時候lr是調用initFunction的函數的下一條指令,所以現在把當時的lr中的值賦給pc程序計數器,這樣執行lr指向的這一條指令,函數就反回了。

指令1,2, 3是函數序言(prologs),指令9, 10是結語(epilogs)。這基本上是一個套路,看多了自然就知道了,都不用停下來一條條分析。

為了方便和棧的變化聯系起來,我們畫出指令8, bl __fooFunction時的棧布局如圖二:

          圖(二)

在上面的initFunction調用第8條指令bl _fooFunction之後,進入fooFunction, 其它匯編如下:

fooFunction:

_fooFunction:
  .cfi_startproc
Lfunc_begin1:
  push  {r4, r5, r7, lr}
  add r7, sp, #8
  sub sp, #8
  ldr r4, [r7, #8]
  movs  r5, #66
  strd  r4, r5, [sp]
  bl _addFunction
  add sp, #8
  pop {r4, r5, r7, pc}
Lfunc_end1:

一樣,我們一行行來看:

1.push {r4, r5, r7, lr}             你應該發現了,這次和initFunction不同,除了lr和r7也把r4, r5 push到棧上去了,這是因為我們下面會用到r4, r5,所以我們先把r4,r5存到棧上,這樣我們在退出fooFunction返回initFunction的時候好恢復r4, r5的值。push到棧上的順序是lr, r7, r4, r5。
2.add r7, sp, #8                     在initFunction中我們沒有push r4, r5所以sp指向的位置正好是新的r7的值,但是這裡我們把r4, r5也push到棧上了,現在sp指向棧上的r4的位置,而棧是向下生長的,所以我們把sp + #8個字節就是存放舊r7的位置。
3.sub sp, #8                          在棧上分配8個字節。
4.ldr r4, [r7, #8]                    r7加8個字節,在棧上的位置正好是在initFunction中我們存放的參數55的位置。因此,這裡是把55賦值給r4
5.movs  r5, #66                     立即數賦值,不解釋了
6.strd r4, r5, [sp]                   把r4, r5中的值存到棧上。我們在initFunction中已經把11,22,33,44這4個參數存放到了r0-r3,現在55,66我們存放在棧上
7.bl _addFunction                   參數已經准備好了,因此現在調用addFunction。
8.add sp, #8                          回收棧空間
9.pop {r4, r5, r7, pc}              這最後兩條指令和 initFunction類似,只是多了個恢復r4,r5。不過也是一個指令就完事。

在指令bl _addFunction 調用addFunction後,棧的布局如圖(三):

            圖(三)

上面的fooFunction第7條指令bl _addFunction之後,進入addFunction。匯編代碼如下:

addFunction:

_addFunction:
  .cfi_startproc
Lfunc_begin0:
  add r0, r1
  ldr.w  r12, [sp]
  add r0, r2
  ldr.w  r9, [sp, #4]
  add r0, r3
  add r0, r12
  add r0, r9
  bx lr
Lfunc_end0:

逐行解釋之:

add r0, r1         r0 += r1
ldr.w r12, [sp]      把sp指向的內容load到r12寄存器。從圖(三)我們知道sp指向66,因此r12存的66
add r0, r2         r0 += r2
ldr.w r9, [sp, #4]    從圖(三) sp加4個字節存的是55, r9存的55
add r0, r3         r0 += r3
add r0, r12        r0 += r12
add r0, r9        r0 += r9。 至此r0-r4存的11,22,33,44,及棧上存的55,66想加存到了r0上。
bx lr             返回。

大家應該有注意到因為addFunction沒有調用其它的函數,序言和結語與initFunction和fooFunction不一樣。因為我們不調用其它函數,就不會有bl, blx這樣的指令,所以不會個性lr, 所以我們沒有push lr。

在這裡我們用了r9, r12為什麼不需要保存與恢復,我還沒大搞明白,大俠們若能賜教,將不勝感激。

iOS ABI Reference上是這樣說的:

關於R9:

In iOS 2.x, register R9 is reserved for operating system use and must not be used by application code. Failure to do so can result in application crashes or aberrant behavior. However, in iOS 3.0 and later, register R9 can be used as a volatile scratch register. These guidelines differ from the general usage provided for by the AAPCS document.

關於R12

R12is the intra-procedure scratch register, also known as IP. It is used by the dynamic linker and is volatile across all function calls. However, it can be used as a scratch register between function calls.

這是C函數的匯編。下篇講obj-c函數的匯編,包括objc block。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved