(十四)SB示例 重新符号化OC二进制文件

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模块,我们可以获得特定函数的起始地址。这是通过使用SBValueSBAddress的引用来实现的。我们可以将获得的所有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。我们接着看一下SBFrameSBSymbol

(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
NSLog的进程地址
lldb.value和NSDictionary

我们如何解析这个包含所有这些地址的NSDictionary?

我们将几乎一字不差地复制生成所有方法的代码,并用于EvaluateExpression获取SBValue

跳转到调用帧-[ViewController dumpObjCMethodsTapped:]

(lldb) f 1

现在我们可以访问此方法中的所有变量,包括负责存储可执行文件中实现的所有方法的retdict。下面获取retdictSBValue引用,并利用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
  1. 正在遍历发生在代码块范围之外的帧。对于每个符号,将执行检查以查看该符号是否为合成符号。如果是,则将内存地址与所收集地址的NSDictionary进行比较。
  2. 获取要枚举的lldb.value中的子项数,以查看是否与Objective-C类列表中的子项匹配。
  3. 先把名字改为无法解析,如果后面查找成功,会覆盖这个值。
  4. 获取合成函数在内存中的地址。
  5. lldb.value给出的键值在内部是由NSNumber创建的。因此需要获取此方法的描述并将其转换为数字。
  6. 如果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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,302评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,232评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,337评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,977评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,920评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,194评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,638评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,319评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,455评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,379评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,426评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,106评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,696评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,786评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,996评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,467评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,043评论 2 341

推荐阅读更多精彩内容