本文共 10226 字,大约阅读时间需要 34 分钟。
你已经看到了强有力的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中它不会被改变毕竟在我写这本书的时候我还没看见后面发布的版本的情况.
图片.png这里有一些按钮来执行两种push动画, 并且这里有还有一个叫做Execute Methods的按钮. 他将会遍历一个给定类的所有已知的Objective-C的implemented/overriden方法. 如果这个方法没有参数, 它就会执行这个方法.
例如, 第一个视图控制器是以ObjCViewController显示的.如果你点击了Execute Methods, 它将会调用anEmptyMethod以及所有被重写的属性的getter方法, 因为这些方法不需要参数. 现在, 开始愉快的学习吧!跳转到OjbCViewController.m文件里然后看一下这个类实现的IBAction方法.在终端中创建一个DTrace并确保你你看到了这些方法被触发了.在终端中:sudo dtrace -n 'objc$target:ObjCViewController::entry' -ppgrepVCTransitions
确保模拟器运行的是VCTransitions项目. 按下回车键来启动这个坏孩子. 当DTrace需要你输入密码的时候请输入你的密码然后回到模拟器中开始点击按钮.你将会看到DTrace终端窗口充满了ObjCViewController实现的IBAction方法. 图片.png
现在, 在SwiftViewController视图控制器点击一个push按钮.
尽管这是一个UIViewController的子类, 点击IBActions, objcPID探针不会产生任何结果. 尽管这里有动态方法的实现或者重写的SwiftViewController的方法, 并且是通过objc_msgSend执行的, 但是实际上是Swift的代码(即便这些事@objc桥接的方法).你可以通过在你的DTrace脚本中增加提取任何Objective-C方法的方式确认这些内容而且你可以检索关键字cool, 它是SwiftViewController中一个变量的名字.想下面这样:sudo dtrace -n 'objc$target:::entry' -ppgrep 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.
图片.png
这会运行同样的target(可执行文件)作为VCTransitionsAPP, 除了Xoce将会生成一个没有包含任何调试信息的stripped build.
选择Stripped VCTransitionsscheme, 确保它是在 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_unnamedsymbol293$$VCTransitionslldb_unnamed_symbol294$$VCTransitions_lldb_unnamedsymbol295$$VCTransitionslldb_unnamed_symbol296$$VCTransitions该死, LLDB获取不到这些函数的名字. 你认为DTrace可以督导精简后的二进制文件的内容吗?在终端中输入下面的内容:sudo dtrace -ln 'objc$target:ObjCViewController::' -p pgrepVCTransitions
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(UIViewControllerCla***ef, "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 !OBJC2Class super_classOBJC2_UNAVAILABLE;const char nameOBJC2_UNAVAILABLE;long versionOBJC2_UNAVAILABLE;long infoOBJC2_UNAVAILABLE;long instance_sizeOBJC2_UNAVAILABLE;struct objc_ivar_list ivarsOBJC2_UNAVAILABLE;struct objc_method_list *methodListsOBJC2_UNAVAILABLE;struct objc_cache cacheOBJC2_UNAVAILABLE;struct objc_protocol_list protocolsOBJC2_UNAVAILABLE;#endif} OBJC2_UNAVAILABLE;/ UseClass
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函数有着下面的声明:/**
(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) po (char *)class_getName(0x0000000109d4ce60)
在你执行完上面的命令之后, DTrace脚本会输出下面这些class_getName调用后的函数列表::~ sudo dtrace -n 'pid$target:::entry' -p pgrep VCTransitions
(lldb) b objc_class::demangledName(bool)
重新运行这个表达式, 但是告诉LLDB要重视这个断点.(lldb) exp -i0 -O -- class_getName([UIView class])
在你按下回车键之后, LLDB将会停在objc_class::demangledName(bool)函数处.图片.png
好好看看这些汇编.吓人的汇编, 第一部分
像往常一样, 这些内容第一眼看上去的时候非常恐怖. 但是当你有条不紊的看一遍之后, 它并没有想象中的那么可怕. 实际上你可以将这些汇编拆成一部分一部分的浏览.第一部分将会是0~55.
查看一下寄存器以便知道你在处理的内容是什么:(lldb) po $rdi
你将会得到一个nil. 这是bool参数. 而nil是0, 因此这里就是false.是时候拆解一下这些内容了. 在这里涉及到的偏移量就是中括号里的这些值. 因此偏移13就是<+13>.图片.png• 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 是用0x7ffffffffff8AND'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:
图片.png
创建一个新的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”中的自定义命令, 方法来确认一下.转载于:https://blog.51cto.com/haidragon/2126802