iOS高級調試&逆向技術-匯編寄存器調用
序言
通過本教程,你會可以看到CPU使用的寄存器,並探索和修改傳遞給函數調用的參數。還將學習常見的蘋果計算機架構以及如何在函數中使用寄存器。這就是所謂架構的調用約定。
了解匯編是如何工作的,以及特定架構調用約定是如何工作是一項極其重要的技能。它可以讓你在沒有源碼的情況下,觀察和修改傳遞給函數的參數。此外,因為源碼存在不同或未知名稱的變量情況,所以有時候更適合使用匯編。
比如說,假設你總想知道調用函數的第二個參數,不管參數的名稱。匯編知識為你提供一個很好的基礎層來操作和觀察函數中的參數。
匯編
等等,匯編是什麽?
你有沒有停在一個沒有源碼的函數中,你會看到一系列內存地址,後面跟著一些嚇人的短命令?你擁抱成球輕聲在耳邊私語告訴自己你從來不看這些東西?嗯…這些東西就是所謂的匯編!
這是一張Xcode裏的回溯圖片,它展示了模擬器裏的匯編函數。
看上面的圖片,這個匯編可以分成幾個部分部分。每一行的匯編指令都包含一個操作碼,它可以被認為是非常簡單的計算機指令。
那麽操作碼看起來像什麽樣子呢?一個操作碼執行計算機中的一個簡單的任務的指令。比如,思考下面的匯編代碼段:
pushq %rbx
subq $0x228, %rsp
movq %rdi, %rbx
- 1
- 2
- 3
- 1
- 2
- 3
在這個匯編塊中,你會看到三個操作碼
,pushq
,subq
和 movq
。思考下這些操作碼執行的動作。操作碼後面是來源和目標的標簽。這些就是操作碼行為項。
在上一個例子中,有一系列寄存器
rbx
,rsp
和 rdi
,在每個%
後面的都稱為寄存器。
另外,你可以找到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個通用寄存器的機器用來操縱數據。
這些寄存器分別是 RAX
,RBX
,RCX
,RDX
,RDI
,RSI
,RSP
和 R8
到 R15
。你現在可能並不清楚這些名字的含意,但你很快就會探索這些重要的寄存器。
當你在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應用程序功能行為的寄存器,創建一個NSViewController
的viewDidLoad
方法符號斷點。推薦使用”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高級調試&逆向技術-匯編寄存器調用