DTrace(第二十七章:DTrace VS objc_msgSend (一))

你已经看到了强有力的DTrace是如何破解你拥有的Objective-CSwift代码的, 或者那些Framework中的代码比如UIKit. 你已经用DTrace追踪了这些代码并且在没有对编译过的代码做任何改变的情况下做了一些有趣的改变.
不幸的是, 在DTrace在破解脱壳了的可执行文件时, 它不能够创建任何探针来动态检查这些函数.
然而在浏览Apple的代码的时候, 你仍然有一个非常强大的助手在你旁边:objc_msgSend. 在本章中你将会使用DTrace去拦截objc_msgSend的入口并且提取出所有调用Objective-C selector的类名.在这一章的末尾, 你将会用LLDB生成一个脚本 一个仅仅生成追踪道德在主执行文件中调用objc_msgSend的代码的信息.

构建你的概念证明

starter文件夹中是一个叫做VCTransitions的APP, 是一个非常基础的Objective-C/Swift应用程序, 它展示了一个普通的UINavigationControllerpush动画, 以及一个自定义的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' -p `pgrepVCTransitions`

确保模拟器运行的是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' -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.

图片.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_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去跟踪这个方法并且查看这个类调用了哪些方法.
能让我们看到希望的是, 你的VCTransitionsAPP仍然在运行. 如果没有, 则重新运行这个程序. 在运行起来之后, 在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)函数处.

图片.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]

现在你将会得到一个的代表UIAlertControllerchar*类型的引用.记住如果这个类独有的方法被执行了, Objective-C运行时就会加载这个类.
现在, 这个类方法(i.e. -[NSObject class])不是UIAlertController独有的, 但是猜一下什么是他独有的?
你正在po这个对象然而debugDescriptiondescription方法是这个类独有(重写)的!
因此, 在po一个UIAlertController类的时候, 它会被加载到运行时里!
如果你有任何疑惑的话在UIAlertController上运行你在第十四章“DynamicFrameworks”中的自定义命令, 方法来确认一下.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容