苹果调试和逆向工程进阶-笔记(一)
引言
写这本书的目的
每个开发人员都应该研究调试代码的艺术。 但是,有些人会从本书中获得更多。 本书是为:
希望更好地使用LLDB进行调试的开发人员
希望使用LLDB构建复杂调试命令的开发人员
希望深入了解Swift和Objective-C内部的开发人员
有兴趣了解:通过逆向工程,他们可以做些什么的开发人员
对现代主动逆向工程策略感兴趣的开发人员
希望在发现有关其计算机或软件问题的答案时有所帮助的开发人员
自定义LLDB脚本repo:
https://github.com/DerekSelander/LLDB
这些脚本将有助于您的调试/逆向工程,并为您自己的LLDB脚本提供新颖的想法。
第一节:开始LLDB命令
到本节结束时,您将能够使用调试器来执行调试所需的大多数基本任务,以及创建自己的简单自定义命令
第1章:入门
想知道为什么命令是po? po代表打印对象。 还有p,它只是打印RDI的内容。 po通常更有用,因为它提供了NSObject的描述或debugDescription方法(如果可用)。
如果您想将调试提升到一个新的水平,汇编(Assembly)是一项重要的技能。
它可以让您深入了解Apple的代码 - 即使您没有任何源代码可供阅读。 它将使您更好地了解Swift编译器团队如何使用Swift在Objective-C中跳出,并且它将使您更好地了解Apple设备上的一切是如何工作的。
如果可以,我将始终选择在调试器中使用Objective-C,因为使用Objective-C LLDB比使用Swift更稳定。
第2章:Help和Apropos
help
将转储(dump)所有可用的命令,包括从〜/ .lldbinit加载的自定义命令。
1 | (lldb) help breakpoint |
apropos
命令可以为您执行此操作;这有点像使用搜索引擎在网络上找到一些东西。
1 | (lldb) apropos swift |
第3章:使用LLDB Attaching
LLDB“Attaching”的短语实际上有点误导。 名为debugserver(位于Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/)的程序负责附加(attaching)到目标进程。
如果它是远程进程,例如在远程设备上运行的iOS,watchOS或tvOS应用程序,则会在该远程设备上启动远程调试服务器。 LLDB的工作是启动,连接和协调调试服务器,以处理调试应用程序时的所有交互。
附加到现有进程
1 | lldb -n Xcode |
附加到未来的进程
1 | lldb -n Finder -w |
launch可选的参数
这告诉LLDB使用/bin/ls
(文件列表命令)作为目标可执行文件。
1 | lldb -f /bin/ls |
使用 process 的选项 -w
更改工作目录
1 | (lldb) process launch -w /Applications |
直接传递参数给程序
,也就是/bin/ls
1 | (lldb) process launch -- /Applications |
等价于
1 | $ ls /Applications |
-X
选项可扩展您提供的任何shell参数,例如代字号。
1 | (lldb) process launch -X true -- ~/Desktop |
run
是 process launch -X true —
的缩写,所以
1 | run ~/Desktop |
stdin也有一个选项-i
,用来处理标准的输入输出。
1 | (lldb) target delete |
1 | $ echo "hello world" > /tmp/wc_input.txt |
1 | (lldb) process launch -i /tmp/wc_input.txt |
等同于
1 | $ wc < /tmp/wc_input.txt |
第4章:在代码中停止
无论您是在技术堆栈中使用Swift,Objective-C,C ++,C还是完全不同的语言,您都需要学习如何创建断点。 可以轻松地在Xcode中单击侧面板以使用GUI创建断点,但LLDB控制台可以让您更好地控制断点。
Signals(信号)
Unix信号是进程间通信的基本形式。
例如,其中一个信号SIGSTOP可用于保存状态并暂停执行进程,而其对应的SIGCONT则被发送到程序以恢复执行。调试器可以使用这两个信号暂停并继续执行程序。
Xcode 断点
符号断点(Symbolic breakpoints)是Xcode的一个很好的调试功能。它们允许您在应用程序中的某个符号上设置断点。例如[NSObject init]
,它引用NSObject实例的init方法。
您将学习如何在第10章“汇编,寄存器和调用约定”中正确使用和操作寄存器,但是现?在,只需知道
arg1
与$rdi
寄存器同义,并且可以被认为是持有实例的调用init时的类。
还有Swift错误断点,它通过在swift_willThrow方法上创建断点来随时停止Swift抛出错误。如果您正在处理任何容易出错的API,这是一个很好的选择,因为它可以让您快速诊断情况,而不会对代码的正确性做出错误的假设。
LLDB断点语法
image
命令是一个很好的工具,可以帮助内省对设置断点至关重要的细节。
1 | (lldb) image lookup -n "-[UIViewController viewDidLoad]" |
1 | (lldb) image lookup -rn test |
Objective-C properties
1 | @interface TestClass : NSObject |
1 | (lldb) image lookup -n "-[TestClass name]" |
Objective-C properties and dot notation
1 | TestClass *a = [[TestClass alloc] init]; |
重要的是要知道您是在处理Objective-C代码并尝试使用点表示法在setter和getter属性上创建断点。
Swift properties
1 | class SwiftTestClass: NSObject { |
In the LLDB console, type the following:
1 | (lldb) image lookup -rn Signals.SwiftTestClass.name.setter |
You’ll get output similar to below:
1 | 1 match found in /Users/derekselander/Library/Developer/Xcode/ DerivedData/Signals-atqcdyprrotlrvdanihoufkwzyqh/Build/Products/Debugiphonesimulator/Signals.app/Signals: |
使用以下正则表达式查询同时搜索name属性的SwiftTestClass setter和getter:
1 | (lldb) image lookup -rn Signals.SwiftTestClass.name |
创建断点
有几种不同的方法可以创建断点。最基本的方法是只输入字母b,后跟断点名称。这在Objective-C和C中相当容易,因为名称简短且易于键入(例如 - [NSObject init]或 - [UIView setAlpha:])。输入C ++和Swift非常棘手,因为编译器会将您的方法转换为具有相当长名称的符号。
1 | (lldb) b -[UIViewController viewDidLoad] |
与许多速记命令一样,b是另一个更长的LLDB命令的缩写。
正则表达式断点和范围
rb命令将扩展到rbreak(假设您没有任何以“rb”开头的其他LLDB命令)。
1 | (lldb) rb SwiftTestClass.name.setter |
这将在包含短语name.setter的任何内容上生成断点:
1 | (lldb) rb name\.setter |
在UIViewController的每个Objective-C实例方法上创建一个断点:
1 | (lldb) rb '\-\[UIViewController\ ' |
删除所有断点:
1 | (lldb) breakpoint delete |
在断点中的UIViewController之后,这提供了带有一个或多个字母数字字符后跟空格的可选括号。
1 | (lldb) rb '\-\[UIViewController(\(\w+\))?\ ' |
使用正则表达式断点可以使用单个表达式捕获各种断点。
您可以使用-f选项将断点的范围限制为特定文件。
如果您正在调试DetailViewController.swift,这将非常有用。它将在此文件中的所有属性getter / setter,块/闭包,扩展/类别和函数/方法上设置断点。 -f称为范围限制。
1 | (lldb) rb . -f DetailViewController.swift |
使用-s可以限制此共享库中设置断点。
1 | (lldb) rb . -s UIKit |
-o选项为此提供了解决方案。它创造了所谓的“一次性”断点。当这些断点命中时,断点将被删除。所以它只会打一次。
1 | (lldb) breakpoint delete |
其他很酷的断点选项
-L选项允许您按源代码的语言进行过滤。因此,如果您只想在Signals应用程序的Commons模块中使用Swift代码,则可以执行以下操作:
1 | (lldb) breakpoint set -L swift -r . -s Commons |
这将在Commons模块中的每个Swift方法上设置断点。
如果你想在Swift语句if let
寻找一些有趣的东西,如果让它完全忘记你的应用程序在哪里,该怎么办?您可以使用源正则表达式断点来帮助确定感兴趣的位置!像这样:
1 | (lldb) breakpoint set -A -p "if let" |
这将在包含
if let
的每个源代码位置创建一个断点。 -A选项表示搜索项目已知的所有源文件。
如果想进一限制文件范围:
1 | (lldb) breakpoint set -p "if let" -f MasterViewController.swift -f DetailViewController.swift |
这将获取所有源文件(-A),但只过滤那些属于Signals可执行文件(使用-s Signals选项)的文件。
再来一个很酷的断点选项示例?好的。每当viewDidLoad被命中时,将创建一个打印UIViewController的断点,但是将通过LLDB控制台而不是符号断点窗口来执行此操作。然后将这个断点导出到文件中,这样就可以通过使用断点读取和断点写入命令展现给同事。
1 | (lldb) breakpoint delete //清理断点 |
修改和移除断点
断点会从1开始分配ID
1 | (lldb) b main |
第5章:表达式
格式化 p 和 po
po通常用于Swift和Objective-C代码中以打印出感兴趣的项目。这可以是对象中的实例变量,对象的本地引用或寄存器,如本书前面所述。它甚至可以是一个任意的内存引用 - 只要该地址有一个对象!
po实际上是expression -O --
的简写表达式。 -O参数用于打印对象的描述。
1 | (lldb) help po |
po经常被忽视的兄弟p,是另一个省略-O选项的缩写,expression --
。打印出的p的格式更依赖于LLDB类型系统。
1 | (lldb) help p |
例如:
1 | override var description: String { |
在viewDidLoad中添加Xcode断点:
1 | (lldb) po self |
输出:
1 | debugDescription: Yay! debugging <Signals.MasterViewController: 0x7fb71fd04080> |
Swift 和 Objective-C 调试上下(文contexts)
重要的是要注意调试程序时有两个调试上下文:非Swift调试上下文和Swift上下文。默认情况下,当您停止使用Objective-C代码时,LLDB将使用非Swift(Objective-C)调试上下文,而如果您在Swift代码中停止,则LLDB将使用Swift上下文。听起来合乎逻辑,对吧?
但是你也可以指定上下文:
1 | (lldb) expression -l objc -O -- [UIApplication sharedApplication] |
在这里,您告诉LLDB使用Objective-C的objc语言。如有必要,您还可以使用objc + +作为Objective-C ++。
用户定义变量
如前所述,LLDB将在打印对象时代表您自动创建局部变量。您也可以创建自己的变量。
但是记得名字前面加$
1 | (lldb) po id $test = [NSObject new] |
这是一个正在积极开发的领域,Objective-C和Swift之间通过LLDB的桥梁可能会随着时间的推移而有所改善。
在Xcoode创建符号断点:
1 | Signals.MasterContainerViewController.viewDidLoad() -> () |
然后
1 | (lldb) p self |
通过键入continue或按Xcode中的播放按钮来恢复应用程序。就会发现标题已经更新为 ’new title‘
类型格式化(Type formatting)
LLDB的一个不错的选择是能够格式化基本数据类型的输出。这使得LLDB成为了解编译器如何格式化基本C类型的绝佳工具。当你正在探索汇编部分时,这是必须知道的,你将在本书后面做。
1 | (lldb) expression -G x -- 10 //指定十进制格式化输出 |
1 | -G <gdb-format> ( --gdb-format <gdb-format> ) |
输出格式的完整列表如下(取自https://sourceware.org/gdb/ onlinedocs / gdb / Output-Formats.html):
- x:十六进制
- d:十进制
- u:无符号十进制
- o:八进制
- t:二进制
- a:地址
- c:字符常量
- f:浮点型
- s:字符串
如果这些格式不够,您可以使用LLDB的额外格式化程序,但您将无法使用GDB格式化语法。
1 | (lldb) expression -f Y -- 1430672467 |
1 | -f <format> ( --format <format> ) |
LLDB具有以下格式化程序(摘自http://lldb.llvm.org/varformats.html):
B:布尔值
b:二进制
y:字节
Y:ASCII字节
c:字符
C:可打印字符
F:复杂的浮点型(包含实部和虚部)
s:c-string
i:十进制
E:枚举
x:十六进制
f:浮点型
o:八进制
O:MacOS OSType
U:UTF-16
u:无符号十进制
p:指针
第6章:线程,帧和步进(Thread, Frame & Stepping Around)
您已经学习了如何创建断点,如何打印和修改值,以及如何在调试器中暂停时执行代码。但到目前为止,您已经处于高度干燥的状态,如何在调试器中移动并检查数据之外的数据。现在是时候了!
在本章中,您将学习如何在LLDB当前暂停时将调试器移入和移出功能。
这是一项关键技能,因为您经常需要在输入或退出代码片段时随时检查值。
(栈的第一课)Stack 101
当计算机程序执行时,它将值存储在堆和栈中。两者都有其优点。作为高级调试器,您需要充分了解这些工作原理。现在,让我们简要介绍一下这个栈。
栈是LIFO(后进先出)队列,用于存储对当前正在执行的代码的引用。这种LIFO排序意味着最近添加的任何内容都会被删除。想想一栈盘子。在顶部添加一个盘子,它将是你首先取下的盘子。
栈指针指向栈的当前顶部。在板块类比中,栈指针指向顶板,告诉您从哪里取下一块板,或者在哪里放下一块板。
在此图中,高地址显示在顶部(0xFFFFFFFF),低地址显示在底部(0x00000000),显示栈将向下增长。
一些插图喜欢在底部具有高地址以与板类比匹配,因为栈将显示为向上增长。但是,我相信任何展示栈的图表都应显示从高地址向下增长,因为这会在以后讨论栈指针的偏移时引起较少的麻烦。
检查栈的帧
这里我将使用 iPhone X Simulator。
添加断点:
1 | Signals.MasterViewController.viewWillAppear(Swift.Bool) -> () |
在Debug Navigator面板中,将显示栈跟踪,显示栈帧列表,第一个是viewWillAppear(_ :)
。接下来是Swift / Objective-C桥接方法,@objc MasterViewController.viewWillAppear(Bool) ->():
。这个方法是自动生成的,所以Objective-C可以进入Swift代码。
之后,有一些来自UIKit的Objective-C代码栈帧。深入挖掘一下,你会看到一些属于CoreAnimation的C ++代码。更深入的是,你会看到一些方法都包含属于CoreFoundation的名称CFRunLoop。最后,最重要的是,main函数。
下面进入LLDB console:
1 | (lldb) thread backtrace |
也可以使用bt
1 | (lldb) help bt |
1 | (lldb) bt |
如您所见,此输出与Debug Navigator中找到的内容相匹配。那么,如果您只是从Debug Navigator中看到所有内容,为什么这甚至很重要?好吧,使用LLDB控制台可以对您想要查看的信息进行细致的控制。此外,您将制作自定义LLDB脚本,其中这些命令将变得非常有用。知道Xcode从哪里获取信息也很好,对吧?
然后:
1 | (lldb) frame select 1 |
记下汇编中的绿线。在该行之前是负责执行viewWillAppear(_:)
的callq
指令,您在之前设置了断点。
不要让汇编太模糊你的眼睛。你还没有走出汇编树林……
步进
掌握LLDB时,您可以在程序暂停时执行的三个最重要的导航操作围绕着逐步执行程序。通过LLDB,您可以跳过(step over),单步执行(step in)或退出(step out)代码。
跳过
1 | (lldb) run //'run' is an abbreviation for 'process launch -X true --' |
单步执行
1 | (lldb) step //'step' is an abbreviation for 'thread step-in' |
退出
1 | (lldb) finish //'finish' is an abbreviation for 'thread step-out' |
请记住,只需按Enter键,LLDB将执行您输入的最后一个命令。
Xcode 界面中步进
虽然使用控制台可以获得更加精细的控制,但Xcode已经为LLDB控制台上方的按钮提供了这些选项。
检查栈中的数据
frame
命令的一个非常有趣的选项是 frame variable
子命令。
此命令将获取可执行文件头中的调试符号信息(如果您的应用程序被剥离,则为dYSM …稍后会详细介绍)并转储该特定栈帧的信息。由于调试信息,frame variable命令可以使用适当的选项轻松告诉您函数中所有变量的范围以及程序中的任何全局变量。
1 | (lldb) frame variable |
1 | -F ( --flat ) |
提供有关Apple私有API的ivars的更多信息,而不是Variables View。
第7章:镜像(Image)
现在是时候探索通过LLDB的权力找到感兴趣代码的最佳工具之一。在本章中,您将深入了解image
命令。
image
命令是target modules
子命令的别名。image
命令专门查询 module 信息;也就是说,代码在进程中加载和执行。Module 可以包含许多东西,包括主要的可执行文件(main executables),框架(framework)或插件(plugin)。但是,这些module中的大多数通常以动态库(dynamic libraries)的形式出现。动态库的示例包括适用于iOS的UIKit或适用于macOS的AppKit。
Module
1 | (lldb) image list |
这是一种有用的方法,可以找到有关所需module的信息。
让我们来探索这个输出。 那里有一些有趣的东西:
首先打印module的UUID(FD4BF3C6-63C9-3A30-BEEA-D39F6F8EEB13)。UUID对于搜索符号信息和唯一标识Foundation module的版本非常重要。
在UUID之后是加载地址(0x0000000107d70000)。 这标识将Foundation module加载到Signals可执行文件的进程空间中。
最后,您拥有module在磁盘上所在位置的完整路径。
1 | (lldb) image dump symtab UIKitCore -s address |
这将转储UIKitCore可用的所有符号表信息。由于-s address
参数,此命令按私有UIKitCore模块中实现函数的地址对输出进行排序。
但是可读性不佳,需要另外一个命令来搜索:
1 | (lldb) image lookup -n "-[UIViewController viewDidLoad]" |
这将转储与UIViewController的viewDidLoad实例方法相关的信息。
如果想用正则来模糊搜索:
1 | (lldb) image lookup -rn UIViewController |
但是这还不够,因为结果包含了UIViewControllerBuiltinTransitionViewAnimator
,这并不是我们想要的,所改为:
1 | (lldb) image lookup -rn '\[UIViewController\ ' |
但是如果搜索Category,命名规则:UIViewController(CategoryName)
,改进如下:
1 | (lldb) image lookup -rn '\[UIViewController\(\w+\)\ ' |
当然这些都是参考正则表达式的规则,然后用在实际的需求中。
寻找代码
您可以使用上面的image lookup
命令找到UIViewController方法。您还使用它来寻找第4章“在代码中停止”中如何命名Swift属性setter和getter。
但是Block如何搜索呢?如下面例子
1 | dispatch_once(&onceToken, ^{ |
我们先在Xcode添加断点,然后:
1 | (lldb) frame info |
可以看到完整的函数名: __34+[UnixSignalHandler sharedHandler]_block_invoke
让我们先试试:
1 | (lldb) image lookup -rn _block_invoke |
结果太多,添加范围限制:
1 | (lldb) image lookup -rn _block_invoke Commons |
现在创建断点:
1 | (lldb) rb appendSignal.*_block_invoke -s Commons |
之后我回到终端:
1 | pkill -SIGIO Signals |
就会触发刚刚创建的断点:
1 | 2019-07-01 14:16:15.922561+0800 Signals[7583:429444] Appending new signal: SIGIO |
您需要跳过一个语句,因此块执行了一些初始逻辑来设置函数,也称为函数序言(function prologue)。函数序言是与汇编相关的主题,您将在第II节中了解。
这实际上非常有趣。首先,您会看到一个block的对象,正在调用的。然后有sig和siginfo参数传递给Objective-C方法,在该方法中调用此块。这些如何传递到block中?
好吧,当创建一个block时,编译器足够聪明,可以确定它正在使用哪些参数。然后它创建一个将这些作为参数的函数。调用块时,调用此函数,并传入相关参数。
1 | (lldb) image lookup -t __block_literal_5 |
这是定义block的对象!
正如您所看到的,这几乎与头文件一样好,可以告诉您如何在block中导航内存。如果将内存中的引用转换为__block_literal_5
类型,则可以轻松打印出block引用的所有变量。
1 | (lldb) po ((__block_literal_5 *)0x0000600002f1d580) |
深入研究(Snooping around)
好的,您已经发现了如何以静态方式检查私有类的实例变量,但是该块内存地址太过诱人而无法置之不理。 尝试将其打印出来并使用动态分析进行探索。
直接打印block的内存地址:
1 | (lldb) po 0x0000600002f1d580 |
您现在将尝试在block上调用此方法。但是,当保留此block的引用释放其控制时,您不希望块消失,从而降低retainCount,并可能释放block。
有一种简单的方法来保持这个block - 只需 retain
它!在LLDB中键入以下内容,将地址替换为block的地址:
1 | (lldb) po id $block = (id)0x0000600002f1d580 |
这表明你已经再次调用了block!
这种用于探索公共和私有类,然后探索它们实现的方法的方法,是了解程序覆盖范围内的内容的好方法。稍后您将对方法使用相同的发现过程,然后分析这些方法执行的程序集,为您提供原始方法源代码的非常接近的近似值。
Private debugging methods
image lookup
命令可以很好地搜索私有方法以及您在Apple开发职业生涯中看到的公共方法。
但是,在调试自己的代码时,有一些隐藏的方法非常有用。
例如,以_
开头的方法通常表示自己是一个私有(也可能是重要的!)方法。
让我们尝试在所有以下划线字符开头的模块中搜索任何Objective-C方法,并在其中包含单词“description”。
如果您在阅读此行代码时有些迷惑不解,强烈建议您仔细阅读 https://docs.python.org/2/library/re.html 以了解正则表达式查询;从现在开始,它只会变得更加复杂。
1 | (lldb) image lookup -rn (?i)\ _\w+description\] //不区分大小写 |
通过地址调用对象:
1 | (lldb) po [[UIApplication sharedApplication] statusBar] |
未完待续