这篇文章主要介绍将lldb Python模块的知识和Objective-C 的运行时结合起来可以做的事情. 当LLDB来解析精简过的可执行文件(一个没有DWARF调试信息的可执行文件)时, LLDB没有栈记录的符号化的信息.
取而代之的是, LLDB将会为一个它认识的方法生成一个合成的名字作为方法名, 但是不知道什么调用了这个方法.
这里有一个LLDB创建的合成方法名的例子:
___lldb_unnamed_symbol906$$SpringBoard
一个逆向工程这个方法名字的策略就是在这个方法出创建一个断点然后浏览这个方法开始执行时的寄存器.
用你Objective-C运行时的汇编知识, 你知道RSI
寄存器(x64)或者X1
寄存器(ARM64)将会存储着Objective-C
Selector的名字. 此外, 你还有RDI
(x64)或者X0(ARM64)寄存器持有这实例(或者类)的引用.
然而, 你一离开函数实现位置的函数名, 你就无法保证寄存器里的值还是我们想要的那个值, 因为他们的值很有可能被重写. 如果一个被精简过的函数调用另一个函数会怎么样呢? 那么你关心的寄存器里的值就会丢失, 因为i它们被设置为了这个新函数的参数. 你需要一种没必要已来上面这些寄存器的方式去重新符号化栈记录.
在本章中, 你将会构建一个能够重新符号化栈记录中精简过的Objective-C函数的 LLDB脚本.
当你在这个进程上调用
bt
的时候, LLDB没有高亮部分的函数名字.
你将会构建一个新的叫做
sbt
的可以查看精简过的函数并尝试将这些函数使用Objective-C的运行时重新符号化的命令. 在这一章的末尾, 你的
sbt
命令将会产生下面的结果:
那些曾经被精简过的Objective-C函数现在被重新符号化了. 因为这些脚本是独立的, 所以你可以在任何LLDB可以附加到的Objective-C可执行文件上运行这个新的sbt
脚本.
你是如何准确的做到这些的呢?
让我们首先讨论一下如何用 Objective-C 的运行时重新符号化Objective-C代码.
假如你有这个模块的完整路径的话 Objective-C 的运行时的运行时可以列出一个特定模块中所有的类名(被映射到主执行文件的一个动态库,一个NSBundle等等). 这个可以通过牛逼的objc_copyClassNamesForImage
API实现.
从这里开始, 你可以使用class_copyMethodList
API列出objc_copyClassNamesForImage
方法返回的所有类的中的类方法和实例方法.
因此, 你可以抓取所有的方法地址并且将这些地址与栈记录中的地址做一个比较.如果栈记录的函数不能生成一个默认的函数名(例如如果SBSymbol是LLDB合成的), 然后你就可以假设LLDB在这个地址出没有调试信息.
使用LLDB的Python模块, 你可以得到一个函数的起始地址--甚至一个函数已经执行一部分以后的地址. 这就是用SBValue引用一个SBAddress. 从这里开始, 你可以比较所有你已经获得的SBSymbol合成的Objective-C方法的起始地址. 如果有两个地址是匹配的, 然后你就可以换掉精简过的(合成的)方法名字并且用包含在Objective-C运行时里的函数名代替.
不要担心: 在我们构建这些Python脚本之前你将会用LLDB的脚本命令系统的学习这些内容.
50 Shades of Ray
在starter
文件夹中包含一个叫做50 Shades of Ray
的应用程序. 我为显示许多Ray Wenderlich
的脸的项目起的一个很好的名字.
这就是绅士的Ray
, 这就是超级英雄Ray
, 这就是困惑的Ray
!
当点击底部按钮的时候, 一个随机生成的随机大小的
Ray
的图片就会弹出来.
哇, 它将在App Store上创造收入!
打开
50 Shades of Ray
项目然后构架并运行这个APP. 在Xcode项目中, 有两个scheme. 确保你选中的是
50 Shades of Ray
scheme而不是
Stripped
scheme. 你会在后面用到那个精简过的scheme.
如果你已经随机生成了一张
Ray
的图片, 点击右上角的
ObjC
UIBarButtonItem.
这个UIBarButtonItem绑定了一个能够打印出主执行文件里实现的所有方法的
IBAction
并且会将他们输出到你的控制台中.
事实上, 你可以看到输出到控制台里的这些方法的名字!
在控制台中找到
-[ViewController dumpObjCMethodsTapped:]
方法.这就是在主执行文件中提取出所有Objective-C 方法的方法.
在这个函数的前面是一个数字(在我这里的数字是, 4483016672), 这个数字持有这个Objective-C 方法的起始地址.
不相信我吗?暂停执行并在LLDB中输入下面内容:
(lldb) image lookup -a 4483016672
你的地址与我的可能有些不同. 这条指令会提取出4483016672
这个地址在内存中的位置并且查看它在你的项目中相关的引用.
Address: 50 Shades of Ray[0x00000001000017e0] (50 Shades of
Ray.__TEXT.__text + 624)
Summary: 50 Shades of Ray`-[ViewController dumpObjCMethodsTapped:] at
ViewController.m:36
很巧妙. 这告诉了我们4483016672
在内存中的位置也就是-[ViewController dumpObjCMethodsTapped:]
加载到内存中的位置. 让我们看看这个方法的代码.
进入到ViewController.m
中的顶部然后找到dumpObjCMethodsTapped:
. 它的注释写的很详细不需要在这里过度描述, 但是有下面几点需要指出的:
• 所有在主执行文件里实现的方法都会通过objc_copyClassNamesForImage
被枚举出来.
• 对每一个类而言, 这里有都说有抓取所有的类方法和实例方法的逻辑.
• 为了抓取一个特定的Objective-C类的类方法, 你必须得到meta
类. meta
类是负责响应一个类的静态方法. 例如, 所有以+
开头的方法都是被meta
实现的而不是那个类本身.
• 所有的方法都被汇聚在一个NSMutableDictionary
里, 字典里的key就是每个方法在内存里的位置.
用脚本指引你的道路
是时候用脚本生成的LLDB命令来浏览lldb模块的APIs并构建一个快速的POC来看一下你如何一个函数在内存中的起始地址.
在LLDB控制台中, 在NSLog处设置一个断点:
(lldb) b NSLog
你会多次触发SBBreakpointLocation
. 这是好事. 现在继续运行这个应用程序.
在模拟器中点击右上角的ObjC
按钮.
执行应该在内容输出到stderr
之前正确的停下来.
使用全局变量lldb.frame
, 挖掘出哪些APIs可以让你用来抓取NSLog函数的起始地址.
从这里开始构建并使用全局变量:
(lldb) script print lldb.frame
你将会得到代表SBFrame的__str__()
. 没有新鲜的东西.
frame #0: 0x000000010b472390 Foundation`NSLog
如果你决定用gdocumentation
去搜索SBFrame
的文档(从第19章, “Script Bridging Classes and Hierarchy”), 你将会看到SBFrame
有一些潜在的方法来获取一个函数的起始地址. pc
看起来是抓取RIP
寄存器(X64)或者PC
(ARM64), 但是它们只在函数的起始位置生效. 你需要从SBFrame
里的任何偏移抓取起始地址.
不幸的是, 在SBFrame
里没有你可以使用的APIs可以让你从函数的任何指令偏移里获取起始地址. 你需要将你的注意力转移到SBFrame引用的其它类来找到你需要的东西.
从SBFrame上抓取SBSymbol引用:
(lldb) script print lldb.frame.symbol
SBSymbol
是负责NSLog的偏移地址的. 也就是说, SBSymbol
将会告诉你这个函数在模块中实现的位置; 它并不是NSLog函数被加载到内存里的实际地址.
然而, 你可以使用SBAddress
属性沿着SBAddress
属性的GetLoadAddress
API去发现NSLog在你当前进程里的起始位置.
(lldb) script print lldb.frame.symbol.addr.GetLoadAddress(lldb.target)
你将会得到一个10进制的数字.我得到的是4484178832
. 用LLDB将它转换成16进制然后比较输出的NSLog的起始地址:
(lldb) p/x 4484178832
我得到的16进制地址是0x000000010b472390
.
将你的输出和NSLog函数的其实地址做一个比较看他们两个时候匹配.
哇! 是匹配的! 这就是我们重新符号化的过程.
带有NSDictionary的lldb.value
鉴于你已经读到这里了, 因此你可以浏览更多的内容. 你如何去解析汇聚了所有地址的NSDictionary呢?
你将会复制这些代码并且几乎是逐字复制那些生成的所有方法并且将它们应用到EvaluateExpression
API上来获取SBValue.
你应该仍然会暂停在NSLog的起始位置. 跳转到调用-[ViewController dumpObjCMethodsTapped:]
的那一帧.
(lldb) f 1
这将会获取前面的那一帧, dumpObjCMethodsTapped:
. 你现在已经访问了这个方法里的所有变量, retdict里包含的是负责提取主可执行文件中实现的所有方法.
抓取SBValue
说明retdict的引用.
(lldb) script print lldb.frame.FindVariable('retdict')
这行指令会打印出retdict
的SBValue
:
(__NSDictionaryM *) retdict = 0x000060800024ce10 10 key/value pairs
鉴于这是一个NSDictionary
, 实际上你需要解引用这个值以便你可以枚举他的内容.
(lldb) script print lldb.frame.FindVariable('retdict').deref
你将会得到更多相关的输出(下面只是输出内容的一部分):
(__NSDictionaryM) *retdict = {
[0] = {
key = 0x000060800002bb80 @"4411948768"
value = 0x000060800024c660 @"-[AppDelegate window]"
}
[1] = {
key = 0x000060800002c1e0 @"4411948592"
value = 0x000060800024dd10 @"-[ViewController toolBar]"
}
[2] = {
key = 0x000060800002bc00 @"4411948800"
value = 0x000060800024c7e0 @"-[AppDelegate setWindow:]"
}
[3] = {
key = 0x000060800002bba0 @"4411948864"
value = 0x000060800004afe0 @"-[AppDelegate .cxx_destruct]"
}
这就是你开始着手的地方, 因为它打印出了所有key的值.
在这个SBValue
的外面创建一个lldb.value
然后将它赋值给a
.
(lldb) script a = lldb.value(lldb.frame.FindVariable('retdict').deref)
这就是我更喜欢使用lldb.value
而不是SBValue
的一次表现. 在这里, 你可以轻松的浏览NSDictionary
里的值.
打印出lldb.value
NSDictionary里的第一个值.
(lldb) script print a[0]
在这里, 你可以同时打印出key
或者value
.
首先打印出key
:
(lldb) script print a[0].key
你将会得到一些类似下面的输出:
(__NSCFString *) key = 0x000060800002bb80 @"4411948768"
然后打印出value
:
(lldb) script print a[0].value
这将会打印出类似下面的内容:
(__NSCFString *) value = 0x000060800024c660 @"-[AppDelegate window]"
如果你只想要没有引用地址的返回值, 你需要指明将这个
lldb.value
返回到一个
SBValue
里然后抓取
description
.
(lldb) script print a[0].value.sbvalue.description
这将会输出你渴望的-[AppDelegate window]
. 注意你也许会有一个不同的方法.
如果你想提取出这个lldb.value
实例里所有的keys, 你可以使用Python的数组尝试将所有的key提取出来.
(lldb) script print '\n'.join([x.key.sbvalue.description for x in a])
你将会得到一些类似下面的输出:
4411948768
4411948592
4411948800
4411948864
4411948656
4411948720
4411949072
4411946944
4411946352
4411946976
用同样的方法提取出所有的value:
(lldb) script print '\n'.join([x.value.sbvalue.description for x in a])
现在你知道了如何剖析这个NSDictionary, 试想一下啊, 如果它被一些JIT 代码替代了的话怎么办...
我们的计划是将代码从dumpObjCMethodsTapped:
拷贝到Python脚本中, 并将它作为JIT代码执行. 在这里, 你需要用同样的程序将它从NSDictionary中解析出来.
听起来很不错是吧?既然你的计划已经准备好了那就进入下一章节吧!
精简过的50 Shades of Ray
耶, 这个标题吸引了你的注意, 对吧?
在50 Shades of Ray
的Xcode schemes中, 有一个叫做Stripped 50 Shades of Ray
的scheme.
停止执行当前进程(⌘ + .)然后选择Stripped 50Shades of Ray这个scheme.
这个scheme将会构建一个可调试的可执行文件, 但是移除了在你日复一日开发中习惯了的调试信息.构建并运行. 在这个项目中包含一个 shared symbolicbreakpoint. 启用这个断点.
这里不需要修改这个符号断点, 但是这个断点做的时候汉武价值.
这个断点将停止在-[UIView initWithFrame:]
并且有一个条件只在这个UIView类是RayView
的类型时才会停下来. 这个RayView
师傅则显示可爱的Ray Wenderlich
的头像的.
点击**Generate a Ray! **按钮. 执行将会停在-[UIView initWithFrame:]
方法处.
注意观察栈记录.
在第一帧和第三帧处有一些有趣的事情: 这里没有调试信息. LLDB默认为这些方法生成了一个合成的函数名.
在LLDB中确认这一点.
在LLDB, 确保你再起始帧的位置(initWithFrame:):
(lldb) f 0
使用脚本来查看一些它是否是合成的:
(lldb) script lldb.frame.symbol.synthetic
你将会得到False
. 情理之中, 因为你知道这是initWithFrame:
. 跳转到一个合成的帧上面:
(lldb) f 1
执行前一个脚本的逻辑:
(lldb) script lldb.frame.symbol.synthetic
这一次你得到的结果将是true
.
构建sbt.py
在starter文件夹中包含这一个名字叫做sbt.py的Python脚本.
将这个脚本粘贴到你的~/lldb目录下. 假设你已经安装了lldbinit.py
脚本, 这会将所有的Python文件加载LLDB目录中.
如果你没有看第二十二章, “SB Examples, Improved Lookup”, 你可以通过修改你的~/.lldbinit
文件手动安装sbt.py
.
如果你已经替换了~/lldb目录下的sbt.py
文件, 使用你再第十九章中创建的reload_script重新加载~/.lldbinit中的命令.
检查LLDB是否正确的识别除了sbt命令:
(lldb) help sbt
如果LLDB识别出了这个命令你将会得到一些帮助信息. 这将是sbt命令的起点.
打开这个文件并且跳转到generateExecutableMethodsScript
. 这里有一些有趣的事情需要指出来.
你是否还记得之前的章节中, 我如何注意到lldb.value
是如此的慢? 如果你正在浏览一个包含许多方法的巨大的可执行文件, Python检查NSDictionary中每一个值的时间将是永远.
取而代之的是, 你不需要抓取NSDictionary中每一个函数的引用. 你只需要抓取每一个函数在栈记录中的起始位置.
def generateExecutableMethodsScript(frame_addresses):
frame_addr_str = 'NSArray *ar = @['
for f in frame_addresses:
frame_addr_str += '@"' + str(f) + '",'
frame_addr_str = frame_addr_str[:-1]
frame_addr_str += '];'
# #############################
# Truncated content...
# #############################
command_script += frame_addr_str
command_script += r'''
NSMutableDictionary *stackDict = [NSMutableDictionary dictionary];
[retdict keysOfEntriesPassingTest:^BOOL(id key, id obj, BOOL *stop) {
if ([ar containsObject:key]) {
[stackDict setObject:obj forKey:key];
return YES;
}
return NO;
}];
stackDict;
'''
return command_script
这是一个甜美的优化, 因为取代评估成千上万的Objective-C 方法的是, 你只需要不超过20个keys或者是一个NSDictionary, 或者只是栈帧里面合成函数的数量.
伴随着符号断点仍然是激活状态并且程序是暂停的, 运行一下脚本. 只有一个没有逻辑去重新符号化符号的普通栈帧将会被打印出来.
是时候做一点修改来修复这个问题了.
实现这些代码
JIT
代码已经设置好了. 你所需要做的就是调用它, 然后将返回的NSDictionary与任何合成的SBValues做一个对比.
在processStackTraceStringFromAddresses
里, 搜索下面的注释:
# New content start 1
# New content end 1
将你的新代码粘贴到这里调用JIT代码去生成一个NSDictionary中的潜在方法列表.
# New content start 1
methods = target.EvaluateExpression(script, generateOptions())
methodsVal = lldb.value(methods.deref)
# New content end 1
你已经调用了返回NSDictionary的代码并且将它赋值给了SBValue实例变量的methods
.
你可以将SBValue传到lldb.value里(技术上讲他只是一个值, 但是你可能会困惑在这里我是不是没有模块)并且将它复制给变量methodsVal
.
现在到了Python代码的最后一部分. 你所需要做的就是检查一个SBFrame的SBSymbol是否是合成的然后执行相应的逻辑.
在processStackTraceStringFromAddresses:
中进一步搜索出下面的代码:
# New content start 2
name = symbol.name
# New content end 2
将这一部分改成下面的样子:
# New content start 2
if symbol.synthetic: # 1
children = methodsVal.sbvalue.GetNumChildren() # 2
name = symbol.name + r' ... unresolved womp womp' # 3
loadAddr = symbol.addr.GetLoadAddress(target) # 4
for i in range(children):
key = long(methodsVal[i].key.sbvalue.description) # 5
if key == loadAddr:
name = methodsVal[i].value.sbvalue.description # 6
break
else:
name = symbol.name # 7
# New content end 2
offset_str = ''
讲这一部分拆解如下:
- 你已经枚举了这些发生在这个代码块外部的帧. 对每一个符号而言, 执行了一个检查来查看这个符号是否是合成的. 如果它是合成的, 他的内存地址就会与我们收集到NSDictionary里的地址做一个比较.
- 这将会抓取lldb.value中在Objective-C类列表里是匹配到的子类的数量.
- 没关系, 栈记录中一个
name
变量的有效引用需要被产生并显示出来 . 你正准备说你知道这是一个合成的函数, 但是解决失败了如果你的即将到来的逻辑产生一个结果的时候失败了. - 这获取了问题中的合成函数在内存中的地址.
-
lldb.value
给出的键值本质上来自NSNumber, 因此你需要抓取这个方法的description并且将它复制给一个number
.令人困惑的是, 它同样赋值给了一个叫做key的Python变量. - 如果key变量与
loadAddr
相等, 然后你就匹配到了一个值. 将name
变量赋值给NSDictionary中变量的description
.
那应该就是它. 保存你的成果然后使用reload_script
重新加载你的LLDB内容并试着运行一下.
假设你现在仍然在Stripped 50 Shades of Ray这个scheme下并且暂停在只在UIView的initWithFrame:
掉使用时才会触发的符号断点处, 在调试器中运行sbt
命令来看一下原来不可用的的第一帧和第三帧是否可读.
frame #0: 0x1053fe694 UIKit`-[UIView initWithFrame:]
frame #1: 0x103cf53ac ShadesOfRay`-[RayView initWithFrame:] + 924
frame #2: 0x1053fdda2 UIKit`-[UIView init] + 62
frame #3: 0x103cf45bf ShadesOfRay`-[ViewController
generateRayViewTapped:] + 79
太漂亮了!
我们为什么要学这些?
祝贺你!你已经用Objective-C 的运行时成功的重新符号化了一个精简过的二机制文件! 你在恰当的Objective-C应用程序上可以做的事情简直太疯狂了.
在这个脚本中仍然有几个漏洞. 这个脚本不能够很好的支持Objective-C的blocks. 然而, 仔细的学习blocks
是如何实现的以及浏览LLDB的Python模块可能会揭示出一个方法来指明已经被精简过的Objective-C block函数.
此外, 这个脚本不支持release模式的iOS可执行文件. LLDB不会发现合成的SBSymbol
引用的起始地址的函数. 这就意味着在ARM64汇编中你将不得不手动向上搜索直到你在通过一个看起阿里像函数起始位置的汇编指令被绊了一跤的时候(你可以猜出要寻找的那个指令吗?).
如果你对这些脚本的的拓展不感兴趣, 试着弄清楚如果重新符号化Swift可执行文件. 这个挑战一定会让你有一个巨大的提升, 但是它仍然属于LLDB能做的事情的范围.
祝你开心!