1. 重新符号化OC二进制文件
对于stripped
的可执行文件(没有DWARF
调试信息的可执行文件),LLDB将没有符号信息来提供堆栈跟踪。LLDB将为识别为方法的方法生成一个合成名称,但不知道该怎么调用。
下面是LLDB在一个探索过程中看到的合成方法名的示例:
___lldb_unnamed_symbol906$$SpringBoard
逆向工程此方法名称的一种策略是在其上创建断点,并在方法的开头探索寄存器。
使用Objective-C运行时的汇编知识,可以知道RSI
寄存器(x64)或X1
寄存器(ARM64)将包含持有方法名称的Objective-C选择器。此外,还拥有RDI
(x64)或X0
(ARM64)寄存器,用于保存对实例(或类)的引用。
但一旦离开函数序言,这些寄存器中的值就可能会被覆盖。如果感兴趣的stripped
方法调用另一个函数呢?我们关心的寄存器现在没有了。因为它们现在是为这个新函数的参数设置的。我们需要一种不用依赖这些寄存器就可以重新对堆栈跟踪进行符号化的方法。
1.1 我们要怎么做呢?
我们首先讨论如何使用Objective-C运行时在stripped
二进制文件中重新编码Objective-C代码。
Objective-C运行时可以列出特定image
中的所有类(image
是主可执行文件、动态库、NSBundle等),前提是我们拥有image
的完整路径。这可以通过objc_copyClassNamesForImage
实现。拿到objc_copyClassNamesForImage
返回的所有类的列表,在这里我们使用类copyMethodList
获取特定类的所有类和实例方法。
因此,我们可以获取所有方法地址,并将它们与堆栈跟踪的地址进行比较。如果堆栈跟踪的函数无法生成默认函数名,则可以假定LLDB没有此地址的调试信息。
使用lldb Python模块,我们可以获得特定函数的起始地址。这是通过使用SBValue
对SBAddress
的引用来实现的。我们可以将获得的所有Objective-C方法的地址与合成SBSymbol
的起始地址进行比较。如果两个地址匹配,那么可以交换stripped
方法名,并用Objective-C运行时获得的函数名替换它。
1.2 50 Shades of Ray应用
通过应用的dumpObjCMethodsTapped方法打印:
2020-03-10 15:35:50.724595+0800 50 Shades of Ray[61374:2688886] {
4322743232 = "-[ViewController generateRayViewTapped:]";
4322743824 = "-[ViewController preferredStatusBarStyle]";
4322743856 = "-[ViewController dumpObjCMethodsTapped:]";
4322745504 = "-[ViewController toolBar]";
4322745568 = "-[ViewController setToolBar:]";
4322745632 = "-[ViewController .cxx_destruct]";
4322745680 = "-[AppDelegate window]";
4322745712 = "-[AppDelegate setWindow:]";
4322745776 = "-[AppDelegate .cxx_destruct]";
4322746000 = "-[RayView initWithFrame:]";
}
我们看到-[ViewController dumpObjCMethodsTapped:]方法的地址是4322743856
。
(lldb) image lookup -a 4322743856
Address: 50 Shades of Ray[0x0000000100001630] (50 Shades of Ray.__TEXT.__text + 624)
Summary: 50 Shades of Ray`-[ViewController dumpObjCMethodsTapped:] at ViewController.m:56
dumpObjCMethodsTapped方法:
- 在主可执行文件中实现的所有Objective-C类都通过
objc_copyClassNamesForImage
遍历。 - 对于每个类,都有获取所有类和实例方法的逻辑。
- 为了获取特定Objective-C类的类方法,必须获取元类。元类是负责特定类的静态方法的类。
- 所有方法都存放到一个NSMutableDictionary中,其中每个方法的键值是函数所在的内存位置。
用script命令探路
在LLDB中设置一个断点:
(lldb) b NSLog
断点停住后:
(lldb) script print(lldb.frame)
frame #0: 0x00007fff25762dfa Foundation`NSLog
通过查看SBFrame
的文档,我们发现里面没有可以获取到方法开始地址的API。我们接着看一下SBFrame
的SBSymbol
:
(lldb) script print(lldb.frame.symbol)
id = {0x0000500e}, range = [0x00000000000aadfa-0x00000000000aae9c), name="NSLog"
SBSymbol
可以拿到NSLog
的实现偏移地址。也就是说,SBSymbol
将告诉我们这个函数在模块中的实现位置。注意它不保存NSLog加载到内存中的实际地址。但是,我们可以使用SBAddress
属性和SBAddress
的GetLoadAddress函数来查找
NSLog`的起始位置在当前进程中的位置。
(lldb) script print(lldb.frame.symbol.addr.GetLoadAddress(lldb.target))
140733821890042
lldb.value和NSDictionary
我们如何解析这个包含所有这些地址的NSDictionary?
我们将几乎一字不差地复制生成所有方法的代码,并用于EvaluateExpression
获取SBValue
。
跳转到调用帧-[ViewController dumpObjCMethodsTapped:]。
(lldb) f 1
现在我们可以访问此方法中的所有变量,包括负责存储可执行文件中实现的所有方法的retdict
。下面获取retdict
的SBValue
引用,并利用deref
可以查看详细信息。
(lldb) script print(lldb.frame.FindVariable('retdict'))
(__NSDictionaryM *) retdict = 0x0000600002e25b20 10 key/value pairs
(lldb) script print(lldb.frame.FindVariable('retdict').deref)
(__NSDictionaryM) *retdict = {
[0] = {
key = 0x0000600002e25c60 @"4342197904"
value = 0x00006000020f2b80 @"-[RayView initWithFrame:]"
}
[1] = {
key = 0x0000600002e25ca0 @"4342197584"
value = 0x00006000020f27f0 @"-[AppDelegate window]"
}
...
}
}
用lldb.value
生成一个SBValue
并赋值给变量a
,可以方便地查看里面的值。
(lldb) script a = lldb.value(lldb.frame.FindVariable('retdict').deref)
(lldb) script print(a[0])
(__lldb_autogen_nspair) [0] = {
key = 0x0000600002e25c60
value = 0x00006000020f2b80
}
(lldb) script print(a[0].key)
(__NSCFString *) key = 0x0000600002e25c60 @"4342197904"
(lldb) script print(a[0].value)
(__NSCFString *) value = 0x00006000020f2b80 @"-[RayView initWithFrame:]"
//如果不想看地址信息可以用
(lldb) script print(a[0].value.sbvalue.description)
-[RayView initWithFrame:]
//打印所有键
(lldb) script print('\n'.join([x.key.sbvalue.description for x in a]))
4342197904
4342197584
4342197408
4342197616
4342197680
4342197472
4342197536
4342195728
4342195136
4342195760
//打印所有的值
(lldb) script print('\n'.join([x.value.sbvalue.description for x in a]))
1.3 "stripped" 50 Shades of Ray
在"stripped" 50 Shades of Ray中,设置一个符号断点-[UIView initWithFrame:]
,条件是(BOOL)[$arg1 isKindOfClass:(id)objc_getClass("RayView")]
。
触发断点:
栈帧1和3里面没有调试信息。LLDB默认为这些方法生成一个合成函数名。通过script lldb.frame.symbol.synthetic
我们可以查看当前帧的函数名是否是合成的
(lldb) f 0
frame #0: 0x00007fff48543de1 UIKitCore`-[UIView initWithFrame:]
UIKitCore`-[UIView initWithFrame:]:
-> 0x7fff48543de1 <+0>: pushq %rbp
0x7fff48543de2 <+1>: movq %rsp, %rbp
0x7fff48543de5 <+4>: pushq %r14
0x7fff48543de7 <+6>: pushq %rbx
(lldb) script lldb.frame.symbol.synthetic
False
(lldb) f 1
frame #1: 0x000000010744421a ShadesOfRay`___lldb_unnamed_symbol14$$ShadesOfRay + 906
ShadesOfRay`___lldb_unnamed_symbol14$$ShadesOfRay:
-> 0x10744421a <+906>: movq %rax, %rcx
0x10744421d <+909>: movq %rcx, -0x30(%rbp)
0x107444221 <+913>: leaq -0x30(%rbp), %rcx
0x107444225 <+917>: movq %rcx, %rdi
(lldb) script lldb.frame.symbol.synthetic
True
1.4 配置sbt.py
starter
文件夹中包含一个名为sbt.py
的Python脚本。将此脚本粘贴到~/lldb
目录中。如果已经安装了lldbinit.py
脚本,这将把所有Python文件加载到LLDB目录中。然后来到generateExecutableMethodsScript
函数。
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 += '];'
...
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
lldb.value的执行速度是非常慢的。如果我们正在探索一个使用许多方法的巨大可执行文件,那么Python遍历NSDictionary中的每个值所需的时间将是永远。
相反,我们不需要获取对NSDictionary中每个函数的每个引用。只需要获取堆栈跟踪中每个函数的开始位置。这样我们就不需要计算潜在的很多很多个Objective-C方法,只需要在NSDictionary中计算不到20个键,或者在栈帧中计算合成函数的数量。
(lldb) reload_script
(lldb) sbt
frame #0 : 0x7fff48543de1 UIKitCore`-[UIView initWithFrame:]
frame #1 : 0x10744421a ShadesOfRay`___lldb_unnamed_symbol14$$ShadesOfRay + 906
frame #2 : 0x7fff48543695 UIKitCore`-[UIView init] + 44
frame #3 : 0x107443406 ShadesOfRay`___lldb_unnamed_symbol1$$ShadesOfRay + 70
...
目前的sbt
只会打印出一个普通的栈帧信息,还没有逻辑来重新符号化。
1.5 重新符号化
methods = target.EvaluateExpression(script, generateOptions())
methodsVal = lldb.value(methods.deref)
调用脚本,并把NSDictionary
返回值赋值到了SBValue
变量methods
。把SBValue
转换成lldb.value
,并把它赋值给methodsVal
。
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 = methodsVal[i].key.sbvalue.description # 5
if key == loadAddr:
name = methodsVal[i].value.sbvalue.description # 6
break
else:
name = symbol.name # 7
- 正在遍历发生在代码块范围之外的帧。对于每个符号,将执行检查以查看该符号是否为合成符号。如果是,则将内存地址与所收集地址的
NSDictionary
进行比较。 - 获取要枚举的
lldb.value
中的子项数,以查看是否与Objective-C类列表中的子项匹配。 - 先把名字改为无法解析,如果后面查找成功,会覆盖这个值。
- 获取合成函数在内存中的地址。
-
lldb.value
给出的键值在内部是由NSNumber
创建的。因此需要获取此方法的描述并将其转换为数字。 - 如果
key
变量等于loadAddr
,则存在一个匹配项。将name
变量赋值为NSDictionary
中变量的描述。
运行一下。现在栈帧0和3就可以正常阅读了。
(lldb) reload_script
(lldb) sbt
frame #0 : 0x7fff48543de1 UIKitCore`-[UIView initWithFrame:]
frame #1 : 0x10744421a ShadesOfRay`-[RayView initWithFrame:] + 906
frame #2 : 0x7fff48543695 UIKitCore`-[UIView init] + 44
frame #3 : 0x107443406 ShadesOfRay`-[ViewController generateRayViewTapped:] + 70
frame #4 : 0x7fff48093fff UIKitCore`-[UIApplication sendAction:to:from:forEvent:] + 83