[轉]iOS高級調試&逆向技術-匯編寄存器調用
前言
本文翻譯自Assembly Register Calling Convention Tutorial
序言
通過本教程,你會可以看到CPU使用的寄存器,並探索和修改傳遞給函數調用的參數。還將學習常見的蘋果計算機架構以及如何在函數中使用寄存器。這就是所謂架構的調用約定。
了解匯編是如何工作的,以及特定架構調用約定是如何工作是一項極其重要的技能。它可以讓你在沒有源碼的情況下,觀察和修改傳遞給函數的參數。此外,因為源碼存在不同或未知名稱的變量情況,所以有時候更適合使用匯編。
比如說,假設你總想知道調用函數的第二個參數,不管參數的名稱。匯編知識為你提供一個很好的基礎層來操作和觀察函數中的參數。
匯編
等等,匯編是什麽?
你有沒有停在一個沒有源碼的函數中,你會看到一系列內存地址,後面跟著一些嚇人的短命令?你擁抱成球輕聲在耳邊私語告訴自己你從來不看這些東西?嗯…這些東西就是所謂的匯編!
這是一張Xcode裏的回溯圖片,它展示了模擬器裏的匯編函數。
看上面的圖片,這個匯編可以分成幾個部分部分。每一行的匯編指令都包含一個操作碼,它可以被認為是非常簡單的計算機指令。
那麽操作碼看起來像什麽樣子呢?一個操作碼執行計算機中的一個簡單的任務的指令。比如,思考下面的匯編代碼段:
pushq %rbx
subq $0x228, %rsp
movq %rdi, %rbx
在這個匯編塊中,你會看到三個操作碼
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
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");
它有四個參數傳遞到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);
當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];
編譯器會把代碼轉成如下偽代碼:
id UIApplicationClass = [UIApplication class];
objc_msgSend(UIApplicationClass, "sharedApplication");
第一個參數引用是UIApplication類,緊接著是 sharedApplication 的selector。
告訴參數的一個簡單方法是檢查selector的冒號。每個冒號代表跟隨一個參數。
這是另一個OC例子:
NSString *helloWorldString = [@"Can‘t Sleep; " stringByAppendingString:@"Clowns will eat me"];
編譯器會轉成如下偽代碼:
NSString *helloWorldString;
helloWorldString = objc_msgSend(@"Can‘t Sleep; ", "stringByAppendingString:", @"Clowns will eat me");
第一個參數是實例NSString(@"Can‘t Sleep; ")
,緊接著是selector,最後是一個參數,也是NSString
實例。
使用objc_msgSend
知識,你可以使用x64寄存器幫助探索上下文,這是一種捷徑。
理論到實際
你可以下載教程項目在這裏
在這章,你將使用項目提供的教程資源bundle調用寄存器,打開項目在Xcode裏,並運行它。
這是一個相當簡單的應用程序,僅僅顯示x64寄存器的內容。重要的是要註意,這個應用程序不能在任何給定的時刻顯示寄存器的值,它只能顯示在指定函數調用時寄存器的值。意味著當函數使用寄存器的值進行調用時,你不會看到太多寄存器變化的值。
現在你將會理解macOS應用程序功能行為的寄存器,創建一個NSViewController
的viewDidLoad
方法符號斷點。推薦使用”NS”代替”UI”,因為你正在運行Cocoa程序。
構建然後返回應用程序,第一次斷點停止,在LLDB控制臺裏輸入:
(lldb) register read
在執行狀態暫停,會顯示主要寄存器的列表。無論如何,這些信息在多了。你應該有選擇地輸出寄存器和修復他們成為OC對象。
如果你重新調用,-[NSViewController viewDidLoad]
將會轉換成如下匯編偽代碼:
RDI = UIViewControllerInstance
RSI = "viewDidLoad"
objc_msgSend(RDI, RSI)
記住x64調用約定,了解 objc_msgSend 的執行,你可以找到被加載具體的NSViewController
實例。
在LLDB控制臺輸入:
(lldb) po $rdi
你將會得到輸出:
<Registers.ViewController: 0x6080000c13b0>
這將會輸出隱藏在RDI寄存器中的NSViewController
引用,你知道,對於函數這是第一個參數。
在LLDB裏,重要的是$
前綴是寄存器,所以LLDB知道你想要寄存器的值,而不是當前源碼範圍內的變量。是的,這與在反匯編視圖中看的匯編不同!有點惱人,是吧?
Note:細心觀察當你OC停止方法時,你從沒看到
objc_msgSend
在LLDB的回溯裏,這是因為objc_msgSend
這類函數執行是jmp
,或是是跳轉操作碼的匯編指令。這個意思是objc_msgSend
行動就像跳轉函數,一但OC代碼開始運行,所有有關objc_msgSend
歷史的棧都會被優化。這種優化稱為尾部調用優化
.
嘗試輸出RSI
寄存器,希望包含被調用的selector,輸出以下內容在LLDB中:
(lldb) po $rsi
不幸的是,你獲得了無效輸出信息,看起來像這樣:
140735181830794
為什麽是這樣?
OC selector本質上是char *
。這意味著,像所有的C類型,LLDB並不知道應用什麽樣式來展現數據。結果,你必須明確地轉換成你想要的數據類型。
嘗試轉換成正確的類型:
(lldb) po (char *)$rsi
現在你得到了你的預期:
"viewDidLoad"
當然,你也可以輸出Selector類型,產生同樣的結果:
(lldb) po (SEL)$rsi
現在,是時候探索OC方法的參數了,從你停止在viewDidLoad
,你可以安全的假設NSView
實例已經被加載了。下面我們來看一下NSView的父類NSResponder
的一個比較有趣的方法mouseUp:
。
在LLDB,創建一個NSResponder
的mouseUp:
斷點,然後繼續執行。如果你不記得怎麽做,這裏有個命令行你可能需要:
(lldb) b -[NSResponder mouseUp:]
(lldb) continue
現在,點擊應用程序窗口,確認點擊是NSScrollView的外面,否則你的點擊會被NSScrollView捕獲,-[NSResponder mouseUp:]
斷點將不會觸發。
當用鼠標或觸控板點擊,LLDB會停止在mouseUp:斷點。通過輸出接下來的內容到控制臺,來輸出引用的NSResponder:
(lldb) po $rdi
接著你會得到類似地輸出:
<NSView: 0x608000120140>
無論如何,這是一個有趣的selector,它包含冒號在裏面,意味著他有參數可以探索!輸出以下內容到LLDB控制臺中:
(lldb) po $rdx
你將獲得有關NSEvent的描述:
NSEvent: type=LMouseUp loc=(351.672,137.914) time=175929.4 flags=0 win=0x6100001e0400 winNum=8622 ctxt=0x0 evNum=10956 click=1 buttonNumber=0 pressure=0 deviceID:0x300000014400000 subtype=NSEventSubtypeTouch
為什麽稱它為NSEvent?
嗯,你可以看在線文檔關於-[NSResponder mouseUp:]
或者你可以簡單使用OC來獲得類型:
(lldb) po [$rdx class]
很酷,是吧?
有時候使用寄存器和斷點是很有用的,以便獲取已經內存中的對象引用。
舉例來說,如果你想把前置的NSWindow變成紅色,但你代碼中沒有此視圖的引用,同時你也不想重新編譯任何代碼的改變?你可以簡單的創建一個斷點,從寄存器和操作實例對象來獲得引用。你可以嘗試著改變主窗口成紅色。
Note:盡管每個
NSResponder
實現了mouseDown:
,NSWindow
通過繼承重載此方法。你可以不通過源碼方式找出所有實現了mouseDown:
方法的類,確定哪些繼承了NSResponder
的類。 舉個輸出所有實現了mouseDown:
的Objective-C類的例子:image lookup -rn ‘\ mouseDown:
首先移除所有的之前的斷點
(lldb) breakpoint delete
About to delete all breakpoints, do you want to do that?: [Y/n]
然後輸出以下
(lldb) breakpoint set -o -S "-[NSWindow mouseDown:]"
(lldb) continue
這會設置一個斷點,它只觸發一次 —— 一次性斷點。
點擊應用程序,點擊之後立即就會觸發斷點。然後輸入以下在LLDB控制臺:
(lldb) po [$rdi setBackgroundColor:[NSColor redColor]]
(lldb) continue
恢復斷點,NSWindow就會變成紅色!
Swift 和 寄存器
當在Swift探索寄存器的時候,相比較OC而言,你將會遇到兩個方面的困難。
- 首先,寄存器
不能
用在Swift調試上下文。意味著你無論想要獲得什麽數據,你得使用OC調試上下文輸出寄存器。記住你可以使用expression -l objc -O --
命令。幸運的是register read
命令是可以在Swift環境中使用的。 - 第二,Swift相比較OC並不是動態語言。事實上,有時候最好假設Swift像C語言,除了有一個非常非常暴躁和專橫的編譯器。如果你有內存地址,你需要明確地轉換成你期望的對象,否則,Swift調試環境並不知道解釋內存地址。
也就是說,Swift也使用了相同的寄存器調用約定。無論如何,這是一個非常重要的不同點。當Swift調用函數,它不需要使用objc_msgSend
,除非你標記方面為dynamic
。意味著Swift調用函數,之前RSI寄存器關聯的是selector,而實際上是函數的第二個參數。
理論足夠了–是時候該行動了。
在Registers項目中,導航到ViewController.swift
,然後增加相關的函數在類裏:
func executeLotsOfArguments(one: Int, two: Int, three: Int,
four: Int, five: Int, six: Int,
seven: Int, eight: Int, nine: Int,
ten: Int) {
print("arguments are: \(one), \(two), \(three), \(four), \(five), \(six), \(seven), \(eight), \(nine), \(ten)"
)
}
現在,在 viewDidLoad
中,調用該函數與相應的參數:
override func viewDidLoad() {
super.viewDidLoad()
self.executeLotsOfArguments(one: 1, two: 2, three: 3, four: 4,
five: 5, six: 6, seven: 7,
eight: 8, nine: 9, ten: 10)
}
放置一個斷點在 executeLotsOfArguments
函數調用的行上,然後調試器會停在最開始函數的地方。這很重要,否則在函數執行的時候寄存器將會被破壞。
然後移除之前的設置在-[NSViewController viewDidLoad]
斷點。
構建和運行app,然後等待executeLotsOfArguments
斷點讓程序暫停。
再次,一種好的方式是開始調查輸出寄存器列表,在LLDB中,輸出以下:
(lldb) register read -f d
這會輸出寄存器並使用數字格式-f d
顯示。輸出看起來是這樣:
General Purpose Registers:
rax = 7
rbx = 9
rcx = 4
rdx = 3
rdi = 1
rsi = 2
rbp = 140734799801424
rsp = 140734799801264
r8 = 5
r9 = 6
r10 = 10
r11 = 8
r12 = 107202385676032
r13 = 106652628550688
r14 = 10
r15 = 4298620128 libswiftCore.dylib`swift_isaMask
rip = 4294972615 Registers`Registers.ViewController.viewDidLoad () -> () + 167 at ViewController.swift:16
rflags = 518
cs = 43
fs = 0
gs = 0
正如你看到的,寄存器跟隨x64位的調用約定。RDI
,RSI
,RDX
,RCX
,R8
和 R9
持有了你的六位參數。
你還可能註意到其他參數存儲在其它寄存器中。雖然這是真的,但它只是代碼的剩余部分,它為其余參數設置到棧上。記住,第六個參數之後的都在棧上。
RAX,保存返回值的寄存器
等等–還有更多!到目前為止,你已經了解如何在函數中調用六個寄存器,但是有關返回值的呢?
幸運的是,只有一個函數返回值的寄存器:RAX
。返回到 executeLotsOfArguments
然後修改函數返回為 String
,像這樣:
func executeLotsOfArguments(one: Int, two: Int, three: Int,
four: Int, five: Int, six: Int,
seven: Int, eight: Int, nine: Int,
ten: Int) -> String {
print("arguments are: \(one), \(two), \(three), \(four), \(five), \(six), \(seven), \(eight), \(nine), \(ten)")
return "Mom, what happened to the cat?"
}
在 viewDidLoad
,修改函數調用接收並忽略字符串的值。
override func viewDidLoad() {
super.viewDidLoad()
let _ = self.executeLotsOfArguments(one: 1, two: 2,
three: 3, four: 4, five: 5, six: 6, seven: 7,
eight: 8, nine: 9, ten: 10)
}
創建斷點在 executeLotsOfArguments
的任意地方。再次構建和運行。然後在函數裏等待執行到暫停。下一步,輸出以下到LLDB控制臺:
(lldb) finish
它會完成執行當前函數並再次暫停調試器。此刻,函數返回值應該已經在 RAX
。輸出以下內容在LLDB中:
(lldb) register read rax
你會得到相似的結果:
rax = 0x0000000100003760 "Mom, what happened to the cat?"
找到了!你返回的值!
了解返回值在 RAX
中是非常重要的,你將會在下個段落寫函數的調試腳本。
通過寄存器改變返回值
為了鞏固對寄存器的理解,你將修改已編譯應用程序中的寄存器。
關閉Xcode和Registers項目。打開終端窗口然後運行iPhone 7模擬器,像下面這樣輸入
xcrun simctl list
你會得到一個很長的設備列表,找到最後一個iOS版本的模擬器。在下面找到iPhone 7設備。看起來像是這樣:
iPhone 7 (269B10E1-15BE-40B4-AD24-B6EED125BC28) (Shutdown)
這個UUID你將會在後面用到。使用下面命令打開模擬器並替換你的UUID:
open /Applications/Xcode.app/Contents/Developer/Applications/Simulator.app --args -CurrentDeviceUDID 269B10E1-15BE-40B4-AD24-B6EED125BC28
確認模擬器已經被運行,並到主屏幕中。你可以通過快捷鍵Command + Shift + H
到主屏幕。一旦模擬器運行完成。在終端窗口後面寫上如下命令設置到SpringBoard程序:
lldb -n Spring Board
這個把正在iOS模擬器中運行的SpringBoard實例,綁定到LLDB上!SpringBoard是一個在iOS上控制主屏幕的應用程序。
輸入如下命令在LLDB:
(lldb) p/x @"Yay! Debugging"
你會獲得相似的輸出:
(__NSCFString *) $3 = 0x0000618000644080 @"Yay! Debugging!"
記錄下這個新創建的NSString實例的內存引用地址,你馬上就會用到它。現在創建一個斷點在UILabel
的setText:
方法:
(lldb) b -[UILabel setText:]
下一步輸入以下:
(lldb) breakpoint command add
LLDB變成支持多行的編輯模式。這個命令讓你增加額外的命令當你想命中斷點時來執行。輸入以下,並替換內存地址,像這樣:
> po $rdx = 0x0000618000644080
> continue
> DONE
> ```
回過頭來看一下剛剛做的事情,你已經創建了一個斷點在 ``UILabel`` 的 ``setText:`` 方法上。當此方法被命中,你會替換 ``RDX`` —第三個參數—不同的字符串實例``"Yay!Debugging!"``。
恢復調試使用``continue``命令:
(lldb) continue
“`
嘗試探索SpringBoard模擬器app,會看到文本內容發生改變。劃動手指從下往上拉出控制中心。然後觀察改變:
嘗試瀏覽其它新彈出的地方,因為這可能會使視圖被延遲加載,導致斷點行動被命中。
雖然這可能看起來很酷的編程技術,提供了一種在有限的寄存器和匯編知識情況下,沒有源碼就可以產生巨大的變化。 從調試觀點來看這很有用,你可以快速在視覺上進行驗證,當-[UILabel setText:]
在SpringBoard應用執行和運行在斷點條件下去找精確的代碼行去設置指定UILabel的文本。
繼續這個想法,有些 UILabel
實例的文本並沒有改變也告訴你一些事情。比如說,UIButton
的文本並沒有改變。也許 UILabel 的 setText:在早期就被調用過?或者可能開發SpringBoard程序的開發者選擇使用setAttributtedText:
來代替?或者他們使用還未公開給第三方開發者的私有函數?
正如你看到的,使用和操作寄存器可以提供給你很多的觀察力,去了解應用程序的函數。
何去何從?
好了!這篇文章很長,不是嗎?坐下來休息一下喝杯飲料;你獲得了它。 你可以下載完整的項目從這篇教程的這裏。
你學習了什麽?
- 架構定義調用約定了哪個指令哪位參數到函數和它返回的值被保存。
- 在Objective-C中,
RDI
寄存器用來引用調用的對象,RSI
是selector,RDX
是首個參數等等。 - 在Swift中,RDI 是第一個參數,RSI第 二個參數,然後等等,只要Swift方法不使用動態分發(dynamic dispatch)。
-RAX
寄存器為函數返回值使用,不管你是用在用OC還是Swift。 - 當使用
$
打印寄存器的時候,確認當前環境是OC。
你可以用寄存器做很多事情。嘗試探索沒有源碼的app;這很有趣,為解決調試問題打下了良好的基礎。
嘗試在iOS模擬器和地圖的UIViewControllers一樣出現使用匯編、智能的斷點和斷點命令。
[轉]iOS高級調試&逆向技術-匯編寄存器調用