首页 > 代码库 > iOS高级调试&逆向技术-汇编寄存器调用

iOS高级调试&逆向技术-汇编寄存器调用

序言

技术分享

通过本教程,你会可以看到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高级调试&逆向技术-汇编寄存器调用