1. 程式人生 > >iOS高級調試&逆向技術-匯編寄存器調用

iOS高級調試&逆向技術-匯編寄存器調用

lac 匯編指令 x64 有一點 city cpu 符號 zip 默認

序言

技術分享

通過本教程,你會可以看到CPU使用的寄存器,並探索和修改傳遞給函數調用的參數。還將學習常見的蘋果計算機架構以及如何在函數中使用寄存器。這就是所謂架構的調用約定。

了解匯編是如何工作的,以及特定架構調用約定是如何工作是一項極其重要的技能。它可以讓你在沒有源碼的情況下,觀察和修改傳遞給函數的參數。此外,因為源碼存在不同或未知名稱的變量情況,所以有時候更適合使用匯編。

比如說,假設你總想知道調用函數的第二個參數,不管參數的名稱。匯編知識為你提供一個很好的基礎層來操作和觀察函數中的參數。

匯編

等等,匯編是什麽?

你有沒有停在一個沒有源碼的函數中,你會看到一系列內存地址,後面跟著一些嚇人的短命令?你擁抱成球輕聲在耳邊私語告訴自己你從來不看這些東西?嗯…這些東西就是所謂的匯編!

這是一張Xcode裏的回溯圖片,它展示了模擬器裏的匯編函數。

技術分享

看上面的圖片,這個匯編可以分成幾個部分部分。每一行的匯編指令都包含一個操作碼,它可以被認為是非常簡單的計算機指令。

那麽操作碼看起來像什麽樣子呢?一個操作碼執行計算機中的一個簡單的任務的指令。比如,思考下面的匯編代碼段:

pushq   %rbx  
subq    $0x228, %rsp  
movq    %rdi, %rbx  
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

在這個匯編塊中,你會看到三個操作碼pushqsubqmovq。思考下這些操作碼執行的動作。操作碼後面是來源和目標的標簽。這些就是操作碼行為項。

在上一個例子中,有一系列寄存器

,分別是 rbxrsprdi,在每個%後面的都稱為寄存器。

另外,你可以找到16進制的常量如0x228。這個$後面的常量都為絕對數。

目前都不需要知道這些代碼在做什麽,因為你首先需要了解函數的寄存器和調用約定。

Note:在上面例子中,寄存器和常量之前有一堆%$。這是一種怎樣的表達方式。然而,有兩種主要方式展示匯編 。第一種是Intel匯編,第二種是AT&T匯編。

默認的,蘋果反匯編工具庫顯示的是AT&T格式。正如上面例子中,雖然這是一種很好的格式,但可以肯定它有一點困難。

x86_64 vs ARM64

作為apple平臺的開發者,當你學習匯編時,將會處理兩種主要的匯編架構:x86_64

架構和 ARM64 架構,x86_64可能是你的macOS計算機架構,除非你運行在比較舊的電腦上。 x86_64是一種64-bit的架構,意味著每個地址可以容納64個1和0。另外,老的蘋果電腦使用32-bit架構,但蘋果在2010年已經停止生產32位的計算機了。程序運行在MacOS下可以兼容64位,包括模擬器程序。也就是說,即使你是x86_64的MacOS,它仍然可以運行32位程序。

如果你對工作的硬件的架構表示任何的疑惑,可以在終端運行如下命令:

uname -m
  • 1
  • 2
  • 1
  • 2

ARM64 架構使用在移動設備如iPhone,控制電量消耗是最重要的。
ARM 強調電源保護,所以它減少了一些操作碼,這助於在復雜匯編指令下的能源消耗減少。這對你來說是個好消息,因為在ARM架構上學習的指令更少。

下面是前面顯示的相同方法的截圖,這一次是跑在iPhone 7的ARM64位匯編下:

技術分享

在他們的這麽多設備中,但後來都移動到 64 位 ARM 處理器。32位設備幾乎過時了,因為 Apple 已經通過各種 iOS 版本淘汰了他們。比如iPhone 4s 是32 位設備已經不支持 ios 10。在32位 iPhone 系列中剩下的只有 iPhone 5 支持 iOS 10。

有意思的是,所有的 Apple 手表目前都是 32 位。這很可能是因為 32 位 ARM CPU 通常比它們的 64 位兄弟有更小的功率。這對手表很重要,因為電池很小。

x86_64 寄存器調用約定

你的CPU使用一組寄存器處理運行中的數據。這些是存儲設備,就像你計算機裏的內存。然而它們的位於CPU本身,非常接近CPU部分。所以CPU訪問它們的時候非常快。

大多數指令涉及一個或多個寄存器,並執行操作。就像寫寄存器到內存中,讀內存的內容到寄存器,或在兩個寄存器上執行算術操作(加減等等)。

x64(這裏開始,x64是x86_64的縮寫),有16個通用寄存器的機器用來操縱數據。

這些寄存器分別是 RAXRBXRCXRDXRDIRSIRSPR8R15。你現在可能並不清楚這些名字的含意,但你很快就會探索這些重要的寄存器。

當你在x64下調用函數,這種方式和使用寄存器,後面有非常具體的約定。這決定了函數的參數應該在哪裏,在函數完成時函數的返回值在哪裏。這很重要,因為用一個編譯器編譯的代碼可以使用另一個編譯器編譯的代碼。
舉個例子,看一下下面這個 Object-C 代碼:

NSString *name = @"Zoltan";  
NSLog(@"Hello world, I am %@. I‘m %d, and I live in %@.", name, 30, @"my father‘s basement"); 
  • 1
  • 2
  • 1
  • 2

它有四個參數傳遞到NSLog函數調用,有些變量是直接訪問的,有一個參數是定義在本地變量中,然後引用參數在函數裏。然而,通過匯編看代碼時候,計算機不會關心變量的名稱,它只關心內存中的地址。
下面的寄存器在x64匯編下作為函數調用時的參數。試著把這些內存提交他們到內存中,因為將來,你會經常使用這些內存。

  • 第一個參數:RDI
  • 第二個參數:RSI
  • 第三個參數:RDX
  • 第四個參數:RCD
  • 第五個參數:R8
  • 第六個參數:R9

如果超過六個參數,在函數裏就會通過棧來訪問額外的參數。

返回到上面的OC例子中,你可以重新定義寄存器就像下面的偽代碼:

RDI = @"Hello world, I am %@. I‘m %d, and I live in %@.";  
RSI = @"Zoltan";  
RDX = 30;  
RCX = @"my father‘s basement";  
NSLog(RDI, RSI, RDX, RCX);  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

NSLog函數開始,這些寄存器會包含適當的值。如上圖所示。

不管如何,當函數序言(function prologue)(準備棧和寄存器的函數開始部分)完成執行,這些寄存器上的值很有可能就會改變。通常在代碼不需要它們的時候,匯編將會重寫這些值,或簡單的丟棄引用。

意味著當你離開函數時開始(通過stepping over,stepping in, or stepping out),你再也不能假設寄存器將保留你希望觀察到的值,除非你實際看到匯編代碼它正在做什麽。

這個函數調用嚴重影響你的調試(斷點)策略,你是否想自動化任何類型的中斷去探索,你應該停止在函數調用之前,以便檢查或修改參數,而不是真正到達匯編裏。

Objective-C 和 寄存器

寄存器使用具體的調用約定。你可以使用相同的知識應用在其它語言中。

當 OC 執行方法內部,其實是通過一個具體的名為 objc_msgSend 的C函數來執行。這實際上函數有幾種不同的類型,稍後再談。這是消息轉發的核心。第一個參數,objc_msgSend 引用發送消息的對象。然後是 selector,這是一個簡單的char *指定的在對象上執行的函數名稱。最後,objc_msgSend 采用可變參數在函數裏。
讓我們看個 iOS 環境上的實際例子:

[UIApplication sharedApplication];
  • 1
  • 1

編譯器會把代碼轉成如下偽代碼:

id UIApplicationClass = [UIApplication class];  
objc_msgSend(UIApplicationClass, "sharedApplication"); 
  • 1
  • 2
  • 1
  • 2

第一個參數引用是UIApplication類,緊接著是 sharedApplication 的selector。

告訴參數的一個簡單方法是檢查selector的冒號。每個冒號代表跟隨一個參數。

這是另一個OC例子:

NSString *helloWorldString = [@"Can‘t Sleep; " stringByAppendingString:@"Clowns will eat me"];  
  • 1
  • 1

編譯器會轉成如下偽代碼:

NSString *helloWorldString;  
helloWorldString = objc_msgSend(@"Can‘t Sleep; ", "stringByAppendingString:", @"Clowns will eat me");  
  • 1
  • 2
  • 1
  • 2

第一個參數是實例NSString(@"Can‘t Sleep; "),緊接著是selector,最後是一個參數,也是NSString實例。
使用objc_msgSend知識,你可以使用x64寄存器幫助探索上下文,這是一種捷徑。

理論到實際

你可以下載教程項目在這裏

在這章,你將使用項目提供的教程資源bundle調用寄存器,打開項目在Xcode裏,並運行它。

技術分享

這是一個相當簡單的應用程序,僅僅顯示x64寄存器的內容。重要的是要註意,這個應用程序不能在任何給定的時刻顯示寄存器的值,它只能顯示在指定函數調用時寄存器的值。意味著當函數使用寄存器的值進行調用時,你不會看到太多寄存器變化的值。

現在你將會理解macOS應用程序功能行為的寄存器,創建一個NSViewControllerviewDidLoad方法符號斷點。推薦使用”NS”代替”UI”,因為你正在運行Cocoa程序。

技術分享

構建然後返回應用程序,第一次斷點停止,在LLDB控制臺裏輸入:

(lldb) register read
  • 1
  • 1

在執行狀態暫停,會顯示主要寄存器的列表。無論如何,這些信息在多了。你應該有選擇地輸出寄存器和修復他們成為OC對象。

如果你重新調用,-[NSViewController viewDidLoad] 將會轉換成如下匯編偽代碼:

RDI = UIViewControllerInstance  
RSI = "viewDidLoad"  
objc_msgSend(RDI, RSI)  
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

記住x64調用約定,了解 objc_msgSend 的執行,你可以找到被加載具體的NSViewController實例。

在LLDB控制臺輸入:

(lldb) po $rdi
  • 1
  • 1

你將會得到輸出:

<Registers.ViewController: 0x6080000c13b0>  
  • 1
  • 1

這將會輸出隱藏在RDI寄存器中的NSViewController引用,你知道,對於函數這是第一個參數。

在LLDB裏,重要的是$前綴是寄存器,所以LLDB知道你想要寄存器的值,而不是當前源碼範圍內的變量。是的,這與在反匯編視圖中看的匯編不同!有點惱人,是吧?

Note:細心觀察當你OC停止方法時,你從沒看到 objc_msgSend 在LLDB的回溯裏,這是因為objc_msgSend這類函數執行是 jmp ,或是是跳轉操作碼的匯編指令。這個意思是objc_msgSend行動就像跳轉函數,一但OC代碼開始運行,所有有關 objc_msgSend 歷史的棧都會被優化。這種優化稱為尾部調用優化.

嘗試輸出RSI寄存器,希望包含被調用的selector,輸出以下內容在LLDB中:

(lldb) po $rsi
  • 1
  • 1

不幸的是,你獲得了無效輸出信息,看起來像這樣:

140735181830794  
  • 1
  • 1

為什麽是這樣?

OC selector本質上是char *。這意味著,像所有的C類型,LLDB並不知道應用什麽樣式來展現數據。結果,你必須明確地轉換成你想要的數據類型。

嘗試轉換成正確的類型:

iOS高級調試&逆向技術-匯編寄存器調用