你已经看到了强有力的DTrace
是如何破解你拥有的Objective-C
和Swift
代码的, 或者那些Framework
中的代码比如UIKit
. 你已经用DTrace
追踪了这些代码并且在没有对编译过的代码做任何改变的情况下做了一些有趣的改变.
不幸的是, 在DTrace在破解脱壳了的可执行文件时, 它不能够创建任何探针来动态检查这些函数.
然而在浏览Apple的代码的时候, 你仍然有一个非常强大的助手在你旁边:objc_msgSend
. 在本章中你将会使用DTrace
去拦截objc_msgSend
的入口并且提取出所有调用Objective-C selector
的类名.在这一章的末尾, 你将会用LLDB生成一个脚本 一个仅仅生成追踪道德在主执行文件中调用objc_msgSend
的代码的信息.
构建你的概念证明
在starter
文件夹中是一个叫做VCTransitions
的APP, 是一个非常基础的Objective-C/Swift
应用程序, 它展示了一个普通的UINavigationController
的push
动画, 以及一个自定义的push动画.
打开这个Xcode项目, 用iPhone 7 Plus 模拟器构建并运行并快速的预览一遍.
注意: 通常情况下我不关心你正在运行的软件的具体版本, 截至目前是iOS 10. 然而这一次我要求你运行在iOS 10.3.x(或者之前的版本), 因为你即将看到的汇编在将来的版本中可能会被改变. 在本章中你可能会看到一点汇编, 但是我不能保证在最新版的iOS中它不会被改变毕竟在我写这本书的时候我还没看见后面发布的版本的情况.
这里有一些按钮来执行两种push动画, 并且这里有还有一个叫做
Execute Methods
的按钮. 他将会遍历一个给定类的所有已知的Objective-C的
implemented/overriden
方法. 如果这个方法没有参数, 它就会执行这个方法.
例如, 第一个视图控制器是以
ObjCViewController
显示的.如果你点击了
Execute Methods
, 它将会调用
anEmptyMethod
以及所有被重写的属性的
getter
方法, 因为这些方法不需要参数. 现在, 开始愉快的学习吧!
跳转到
OjbCViewController.m
文件里然后看一下这个类实现的
IBAction
方法.
在终端中创建一个
DTrace
并确保你你看到了这些方法被触发了.
在终端中:
sudo dtrace -n 'objc$target:ObjCViewController::entry' -p `pgrepVCTransitions`
确保模拟器运行的是VCTransitions
项目. 按下回车键来启动这个坏孩子. 当DTrace需要你输入密码的时候请输入你的密码然后回到模拟器中开始点击按钮.你将会看到DTrace
终端窗口充满了ObjCViewController实现的IBAction
方法.
现在, 在
SwiftViewController
视图控制器点击一个push按钮.
尽管这是一个
UIViewController
的子类, 点击
IBActions
, objcPID探针不会产生任何结果. 尽管这里有动态方法的实现或者重写的
SwiftViewController
的方法, 并且是通过
objc_msgSend
执行的, 但是实际上是Swift的代码(即便这些事@objc桥接的方法).
你可以通过在你的
DTrace
脚本中增加提取任何Objective-C方法的方式确认这些内容而且你可以检索关键字
cool
, 它是
SwiftViewController
中一个变量的名字.想下面这样:
sudo dtrace -n 'objc$target:::entry' -p `pgrep VCTransitions` | grep -icool
你可能认为这将会产生一些输出因为SwiftViewController包含下面的代码:
dynamic var coolViewDTraceTest: UIView? = nil
dynamic var coolBooleanDTraceTest: Bool = false
然而, 这个探针不会做任何事情. 你需要使用pid$target
替代objc$target
和打乱的的Swift的名字, 就像你在前面章节中做的那样.因为调用objc_msgSend
可能先与Swift
代码, 这是用objc_msgSend
替代objc$target
探针的另外一个原因.
在stripped scheme中重复以便你刚才的操作步骤
在这个项目中包含着一个叫做Stripped VCTransitions
的scheme.
这会运行同样的target(可执行文件)作为
VCTransitions
APP, 除了Xoce将会生成一个没有包含任何调试信息的
stripped build
.
选择
Stripped VCTransitions
scheme, 确保它是在 iPhone 7 Plus模拟器上(系统版本是iOS10.3.x之前的版本)构建并运行的.
运行起来之后, 暂停应用程序并进入LLDB控制台.搜索属于
SwiftViewController
的任何代码使用你最近创建的
image lookup
命令, 你在第22“SB Examples, Improved Lookup”中创建的
lookup
命令.(如果你跳过了那一章, 默认情况下是使用
image lookup -rn
).
(lldb) lookup SwiftViewController
呃....你不会触发任何断点.难道是Swift的bug?尝试提取出与ObjCViewController
有关的代码:
(lldb) lookup ObjCViewController
仍然什么都没有.发生了什么事?
这个可执行文件已经去掉了他的信息. 你不能使用调试中的symbols最典型的就是一个内存中的引用.
然而, 事实上LLDB足够智能到意识到这些函数在内存中的位置. LLDB会为方法生成一个唯一的没有信息的函数名.自动生成的函数名将会遵循下面的形式:
___lldb_unnamed_symbol[FUNCTION_ID]$$[MODULE_NAME]
这就意味着你可以使用下面的命令列出LLDB在VCTransitions
可执行文件中生成的所有的函数:
(lldb) lookup VCTransitions
我的到了296结果, 下面就是其中的一部分:
...
___lldb_unnamed_symbol293$$VCTransitions
___lldb_unnamed_symbol294$$VCTransitions
___lldb_unnamed_symbol295$$VCTransitions
___lldb_unnamed_symbol296$$VCTransitions
该死, LLDB获取不到这些函数的名字. 你认为DTrace
可以督导精简后的二进制文件的内容吗?
在终端中输入下面的内容:
sudo dtrace -ln 'objc$target:ObjCViewController::' -p `pgrepVCTransitions`
这条指令查询VCTransitions
进程中包含在ObjCViewController
模块中的探针的数量, 这就是DTrace引用一个Objective-C
类的方式.
我获取到下面的输出:
ID PROVIDER MODULE FUNCTION NAME
dtrace: failed to match objc57009:ObjCViewController:: No probe matchesdescription
我可以知道我的PID是57009并且我捕获到了0个!
如果我想确认ObjCViewController
产生了有效的探针(正如你在前面看到的那样), 只需要简单的使用没有精简过的Xcode scheme重新构建这个项目, 然后再次运行上面的终端命令. 如果你对于证明这是有效的很感兴趣, 我就把他留给你作为一个练习.
如何绕过精简过的没有探针的二进制文件
所以如何设计一个可以绕过这些不能够检查的精简过的二进制文件的DTrace命令或者探针呢?
既然你已经知道了Objective-C (和 动态的 Swift) 方法许啊哟通过objc_msgSend (或者类似的父类方法), 那么你就可以使用这些你已经学过的关于objc_msgSend
知识来弄清楚, 如何创建一个可以打印出这个类中Objective-C
的selector的名字的DTrace指令.
这里有一个objc_msgSend
是如何工作的快速的提示. 这个函数的生灵看起来像下面这个样子:
objc_msgSend(instance_or_class, SEL, ...);
所以, objc_msgSend
需要一个类或者实例作为第一个参数, Objective-C selector
作为第二个参数, 后面跟着的是一些参数的变量.
知道了那些之后, 如果你有下面的代码:
UIViewController *vc = [UIViewController new];
[vc setTitle:@"yay, DTrace"];
编译器将会把它翻译成下面的伪代码:
vc = objc_msgSend(UIViewControllerClassRef, "new");objc_msgSend(vc, "setTitle", @"yay, DTrace");
从DTrace
的角度来看, 获取Objective-C selector 是相当轻松的.只需要copyinstr(arg1)
. 正如你前面学到的那样, 这将会复制arg1
中的指针, Objective-C selector
(是一个 char*), 因此DTrace在内核中可以读到它.
现在看一下难点:你想要获取到作为一个char*
传给objc_msgSend
的类名.
DTrace不会让你执行任意的方法, 因此你可以使用Objective-C的运行时, 或者任何它实现的方法, 从未挖掘出你想要的信息. 取而代之的是, 你通过查看arg0
实例的内存并且自己发现代表着类名的char*
, 然后通过DTrace脚本实现自动化.
嗨, 这是你DTrace技术综合运用的高潮! 你可能同样也想将他们都用出来.
使用DTrace重新搜索调用的方法!
让我们看一下是否有一些成文的方法来做这些事. 在objc/runtime.h
头文件中, 你会看到这下面这些声明:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class
OBJC2_UNAVAILABLE;
const char *name
OBJC2_UNAVAILABLE;
long version
OBJC2_UNAVAILABLE;
long info
OBJC2_UNAVAILABLE;
long instance_size
OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars
OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists
OBJC2_UNAVAILABLE;
struct objc_cache *cache
OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols
OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
回到在64位的机器上使用Objective-C 2.0
的日子里, 如果你有一个指向一个有效类X
的指针,, 你可以获取到那个在#if !__OBJC2__
中描述的const char *name
.
po *(char *)(X + 0x10)
不幸的是,这已经相当陈旧了. 这个类数据结构回到之前的Objective-C 2.0的样子. 结构和指针的位置已经改变很久了. Apple已经选择为objc_class结构体使用更少的公开信息的结构布局, 这在你观察的时候可以更愉悦.
这就意味着你需要捕获一个带着 Objective-C 类(或者一个类的实例)并且为这个类返回一个char*
的函数, 以便我们弄明白它做了哪些事情.
幸运的是, 回到objc/runtime.h
头文件中, 这里同样有一个 class_getName
函数.
通过查看头文件, 你会发现class_getName
函数有着下面的声明:
/**
* Returns the name of a class.
*
* @param cls A class object.
*
* @return The name of the class, or the empty string if \e cls is \c
Nil. */
OBJC_EXPORT const char *class_getName(Class cls)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
这个函数带着一个Class
参数并且返回一个char*
. 你将会用DTrace
去跟踪这个方法并且查看这个类调用了哪些方法.
能让我们看到希望的是, 你的VCTransitions
APP仍然在运行. 如果没有, 则重新运行这个程序. 在运行起来之后, 在LLDB中暂停这个程序.
获取UIView Class
的引用:
(lldb) p/x [UIView class]
你将会得到一些下面的输出:
(Class) $0 = 0x0000000109d4ce60 UIView
这个引用是从UIView这个类里获取的, 将它应用到class_getName
函数中:
(lldb) po class_getName(0x0000000109d4ce60)
你将会得到一个数字?为什么是一串数字呢?
0x000000010999319f
哦, 是的. 这个函数返回了一个C char*
. 你需要指明这些:
(lldb) po (char *)class_getName(0x0000000109d4ce60)
现在你将可以使用DTrace去追踪class_getName
调用后所有的非Objective-C方法.
跳到一个新的终端窗口中并且执行下面的DTrace指令:
sudo dtrace -n 'pid$target:::entry' -p `pgrep VCTransitions`
这一次, LLDB应该仍然会被暂停当设置你的DTrace脚本时.回到LLDB中然后用UIView类的引用重新执行class_getName
函数. 你的UIView类额指针可能会有点不同:
(lldb) po (char *)class_getName(0x0000000109d4ce60)
在你执行完上面的命令之后, DTrace脚本会输出下面这些class_getName调用后的函数列表:
:~ sudo dtrace -n 'pid$target:::entry' -p `pgrep VCTransitions`
Password:
dtrace: description 'pid$target:::entry' matched 901911 probes
CPU ID FUNCTION:NAME
6 1405417 class_getName:entry
6 1405416 objc_class::demangledName(bool):entry
6 566986 _NSPrintForDebugger:entry
6 1405847 objc_msgSend:entry
看起来objc_class::demangledName(bool):
函数是一个值得我们浏览的有趣的地方.
杀掉DTrace脚本. 你肯定不想它干扰到你的LLDB断点, 因为在DTrace探针上设置一个断点会出现重大的意外.在DTrace脚本中断之后, 用LLDB在objc_class::demangledName(bool)
出设置一个断点, 像这样:
(lldb) b objc_class::demangledName(bool)
重新运行这个表达式, 但是告诉LLDB要重视这个断点.
(lldb) exp -i0 -O -- class_getName([UIView class])
在你按下回车键之后, LLDB将会停在objc_class::demangledName(bool)
函数处.
好好看看这些汇编.
吓人的汇编, 第一部分
像往常一样, 这些内容第一眼看上去的时候非常恐怖. 但是当你有条不紊的看一遍之后, 它并没有想象中的那么可怕. 实际上你可以将这些汇编拆成一部分一部分的浏览.第一部分将会是0~55.
查看一下寄存器以便知道你在处理的内容是什么:
(lldb) po $rdi
你将会得到一个nil
. 这是bool
参数. 而nil
是0, 因此这里就是false
.
是时候拆解一下这些内容了. 在这里涉及到的偏移量就是中括号里的这些值. 因此偏移13就是<+13>.
• Offset 13:
在这一行之后, 函数序言就执行完毕了. 是在这个函数中实际应用一下了.
• Offset 17:
这将
esi
赋值给
r12d
. 这就是传进来的Boolean值.我们之前查看的
rsi
并且看到了它是
0
, 因此
r12d
将会同样是0.
• Offset 20:
rdi
包含着UIView类的引用并且赋值给了
r15
.
• Offset 33:
这与
r15
的偏移量是
0x20
的值并且解引用. 也就是说
rax = (*([UIView class] + 0x20))
.
• Offset 37:
这个值存储在
rax
是用
0x7ffffffffff8
AND'd(可能是
与
操作)然后存储到了
rax
.
• Offset 48:
这个值是
rax
偏移0x38后的值然后解引用并存储在
rbx
. 也就是说,
rbx = *(rax + 0x38)
.
• Offset 52-55:
检查
rab
是否是0. 如果它返回一个非零的数字, 然后跳到<+310>结束这个函数, 在函数结束之前是正确的.
如果这个检查偏移量55的值失败了(也就是说, 如果
rbx
是0 ), 执行将会矩形下一句湖边指令, <+61>.
偏移量在0~55的逻辑是负责将一个Objective-C的类作为一个
char*
返回给你 如果(并且仅仅只在如果)那个类已经被正确的加载之后. 这通常发生在那个类里面至少有一个方法(也就是说, 那个方法在那个类中必须被实现或者重写)被执行了.
例如, 如果一个新类被调用了, 然而在你的进程存活期间还没有执行任何初始化, 偏移量在0~55之间的逻辑将会返回
nil
. 稍后你将会构建一个
command regex
来确认这点.
看这些汇编, 你可以推断出下面的内容.
如果你有一个已经初始化的
X
类的实例, 并且如果你将
X
偏移了
0x20
然后引用它, 输出的内容看起来应该是下面这个样子:
*(uint64_t *)(X + 0x20)
然后你用0x7ffffffffff8
按位与
这个值:
*(uint64_t *)(X + 0x20) & 0x7ffffffffff8
接下来, 使用这个值, 用0x38
偏移这个值然后解引用:
*(uint64_t *)((*(uint64_t *)(X + 0x20) & 0x7ffffffffff8) + 0x38)
这是最终的地址, 因此你只需要将它输出到正确的类型里, 一个char *
:
(char *)*(uint64_t *)((*(uint64_t *)(X + 0x20) & 0x7ffffffffff8) + 0x38)
现在, 如果你有一个NSObject的引用, 你从第21章 “ScriptBridging with SBValue & Language Contexts” 中了解到这个对象起始位置的内存地址指向它自己(就是那个isa
指针) . 如果你不理解那些, 回过头去重新阅读第21章-- 否则这一章剩下的内容将会变得更刺激.
将所有这些内容放在一起, 将一个实例的类名作为一个char*
获取, 看这个怪物:
(char *)*(uint64_t *)((*(uint64_t *)((*(uint64_t *)Instance_of_X) + 0x20)& 0x7ffffffffff8) + 0x38)
是的, 你可以将这条指令复制到LLDB中来确认一下它是OK的!
注意: 我会再重复一次: 这不适用于还没有被初始化的Objective-C的类. 这里有一个你为什么使用UIView的原因, 因为如果你可以在你的屏幕上看到UI, 然后UIView类已经明确的被初始化了, 至少有一个UIView被初始化了.
在LLDB中, 看一下UIView 类:
(lldb) p/x [UIView class]
(Class) $1 = 0x000000010c09ce60 UIView
你将会得到一个不同的地址. 将它复制到你的剪切板中:
取到那个地址并将它偏移0x20
然后查看那个位置的内存:
(lldb) x/gx '0x000000010c09ce60 + 0x20'
你将会得到一些值:
0x10c09ce80: 0x0000608000064b80
用0x7ffffffffff8
(that's 10 f's)按位与
那个值:
(lldb) p/x 0x7ffffffffff8 & 0x0000608000064b80
你将会得到另一个数字:
0x0000608000064b80
取到那个数字, 将它偏移0x38
然后解引用:
(lldb) x/gx '0x0000608000064b80 + 0x38'
你将会得到一些下面的输出:
0x608000064bb8: 0x000000010bce319f
观察一下0x000000010bce319f
(至少在我这里是这个地址)的值是否包含char*
指针.
(lldb) po (char *)0x000000010bce319f
如果一切顺利的话, 你将会得到一个代表UIView的char*
:
创建一个新的
regex command
来确认一下我告诉你的都是真的. 只需要将这些输入到控制台中; 不需要将这些放到你的
~/.lldbinit
文件里:
command regex getcls 's/(.+)/expression -lobjc -O -- (char *)*(uint64_t*)((*(uint64_t *)((*(uint64_t *)%1) + 0x20) & 0x7ffffffffff8) + 0x38)/'
这条指令会从已经加载到你的进程里的任何实例上抓取char*
类名.
在你将这条指令输入到LLDB控制台之后, 用它验证一下之前OK的UIView:
(lldb) getcls [UIView new]
现在看一下还没有被初始化或者还没有执行任何方法的那些类, 比如UIAlertController
:
(lldb) getcls [UIAlertController new]
你将会得到一个nil
, 因为这个类还没有执行可以唯一标示这个类的任何代码.
(lldb) po [UIAlertController class]
重新执行getcls
命令:
(lldb) getcls [UIAlertController new]
现在你将会得到一个的代表UIAlertController
的char*
类型的引用.记住如果这个类独有的方法被执行了, Objective-C运行时就会加载这个类.
现在, 这个类方法(i.e. -[NSObject class])不是UIAlertController独有的, 但是猜一下什么是他独有的?
你正在po
这个对象然而debugDescription
和description
方法是这个类独有(重写)的!
因此, 在po
一个UIAlertController类的时候, 它会被加载到运行时里!
如果你有任何疑惑的话在UIAlertController
上运行你在第十四章“DynamicFrameworks”中的自定义命令, 方法来确认一下.