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

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

蘋果 proc read 將不 use 額外 沒有 cor gen

前言

本文翻譯自Assembly Register Calling Convention Tutorial

序言

技術分享圖片

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

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

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

匯編

等等,匯編是什麽?

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

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

技術分享圖片

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

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

pushq   %rbx  
subq    $0x228, %rsp  
movq    %rdi, %rbx  

在這個匯編塊中,你會看到三個操作碼

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

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"); 

它有四個參數傳遞到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應用程序功能行為的寄存器,創建一個NSViewControllerviewDidLoad方法符號斷點。推薦使用”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,創建一個NSRespondermouseUp:斷點,然後繼續執行。如果你不記得怎麽做,這裏有個命令行你可能需要:

(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而言,你將會遇到兩個方面的困難。

  1. 首先,寄存器不能用在Swift調試上下文。意味著你無論想要獲得什麽數據,你得使用OC調試上下文輸出寄存器。記住你可以使用 expression -l objc -O --命令。幸運的是 register read 命令是可以在Swift環境中使用的。
  2. 第二,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位的調用約定。RDIRSIRDXRCXR8R9 持有了你的六位參數。

你還可能註意到其他參數存儲在其它寄存器中。雖然這是真的,但它只是代碼的剩余部分,它為其余參數設置到棧上。記住,第六個參數之後的都在棧上。

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實例的內存引用地址,你馬上就會用到它。現在創建一個斷點在UILabelsetText:方法:

(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高級調試&逆向技術-匯編寄存器調用