iOS汇编教程(四)基于 LLDB 动态调试快速分析系统函数的实现

5,196 阅读18分钟

系列文章

  1. iOS汇编入门教程(一)ARM64汇编基础
  2. iOS汇编入门教程(二)在Xcode工程中嵌入汇编代码
  3. iOS汇编入门教程(三)汇编中的 Section 与数据存取

前言

在前三篇文章中,主要介绍了汇编中的常见指令和寻址方法,本文将结合这些知识介绍一种基于汇编代码和动态调试快速分析函数逻辑的方法。

在进行逆向工程,或是涉及到底层的正向开发(如性能优化、安全防护)时,常常会使用一些系统级的函数,有些时候从细节上了解这些函数的逻辑是十分必要的,例如 objc_getClassobject_getClass 两个方法,前者通过名称拿到类对象,后者通过入参为实例、类对象和元类对象的不同分别返回类对象、元类对象和根元类对象,在不阅读源码的情况下,我们只能通过实验的方式去黑盒分析函数逻辑,好在 Objective-C Runtime的源码 是开放的,我们可以直接阅读这两个函数的源码来分析逻辑,这是一种很好的方法,可是如果我们要分析的函数没有源码呢?例如 NSClassFromString 这个函数。

静态分析

NSClassFromString 来自 Foundation,一个直接的思路是使用 IDA 或 Hopper 对 Foundation 进行静态分析,然后将 NSClassFromString 反编译成 C 的伪代码来分析,这种方式的优点在于代码的可读性较强,能够快速分析出逻辑,缺点也很明显,对于较大的 Framework 静态分析速度很慢,遇到费解的逻辑还是需要结合动态调试来分析。

动态分析

动态分析一般是基于 IDA 或 Hopper 的结果对特定的符号或地址下断点分析,当然也可以直接基于内存中的 __TEXT,__text 段,借助于 LLDB 的反汇编器来直接分析,本文将采用这种方法,以 NSClassFromString 为例展开分析。

动态分析 NSClassFromString

添加符号断点

首先随便找一个 iOS 工程,添加一个 NSClassFromString 的符号断点:

随后连接一台 64 位的 iOS 设备,运行,随后程序断在了 NSClassFromString 函数内,并展示了这个符号的反汇编结果:

Foundation`NSClassFromString:
->  0x18bfd2c98 <+0>:   stp    x28, x27, [sp, #-0x40]!
    0x18bfd2c9c <+4>:   stp    x22, x21, [sp, #0x10]
    0x18bfd2ca0 <+8>:   stp    x20, x19, [sp, #0x20]
    0x18bfd2ca4 <+12>:  stp    x29, x30, [sp, #0x30]
    0x18bfd2ca8 <+16>:  add    x29, sp, #0x30            ; =0x30 
    0x18bfd2cac <+20>:  sub    sp, sp, #0x3f0            ; =0x3f0 
    0x18bfd2cb0 <+24>:  mov    x19, x0
    0x18bfd2cb4 <+28>:  adrp   x8, 196266
    0x18bfd2cb8 <+32>:  ldr    x8, [x8, #0xf0]
    0x18bfd2cbc <+36>:  ldr    x8, [x8]
    0x18bfd2cc0 <+40>:  stur   x8, [x29, #-0x38]
    0x18bfd2cc4 <+44>:  cbz    x19, 0x18bfd2d6c          ; <+212>
    0x18bfd2cc8 <+48>:  adrp   x8, 224951
    0x18bfd2ccc <+52>:  ldr    x1, [x8, #0x4c0]
    0x18bfd2cd0 <+56>:  mov    x0, x19
    0x18bfd2cd4 <+60>:  bl     0x18a72cd60               ; objc_msgSend
    0x18bfd2cd8 <+64>:  mov    x20, x0
    0x18bfd2cdc <+68>:  adrp   x8, 183685
    0x18bfd2ce0 <+72>:  add    x1, x8, #0x8b8            ; =0x8b8 
    0x18bfd2ce4 <+76>:  mov    x21, sp
    0x18bfd2ce8 <+80>:  mov    x2, sp
    0x18bfd2cec <+84>:  mov    w3, #0x3e8
    0x18bfd2cf0 <+88>:  orr    w4, wzr, #0x4
    0x18bfd2cf4 <+92>:  mov    x0, x19
    0x18bfd2cf8 <+96>:  bl     0x18a72cd60               ; objc_msgSend
    0x18bfd2cfc <+100>: cmp    w0, #0x0                  ; =0x0 
    0x18bfd2d00 <+104>: csel   x21, x21, xzr, ne
    0x18bfd2d04 <+108>: cbz    w0, 0x18bfd2d18           ; <+128>
    0x18bfd2d08 <+112>: mov    x0, x21
    0x18bfd2d0c <+116>: bl     0x18c1391a8               ; symbol stub for: +[NSUnitElectricPotentialDifference megavolts]
    0x18bfd2d10 <+120>: cmp    x0, x20
    0x18bfd2d14 <+124>: b.eq   0x18bfd2d5c               ; <+196>
    0x18bfd2d18 <+128>: cbz    x20, 0x18bfd2d48          ; <+176>
    0x18bfd2d1c <+132>: mov    x21, #0x0
    0x18bfd2d20 <+136>: adrp   x8, 183600
    0x18bfd2d24 <+140>: add    x22, x8, #0x9ce           ; =0x9ce 
    0x18bfd2d28 <+144>: mov    x0, x19
    0x18bfd2d2c <+148>: mov    x1, x22
    0x18bfd2d30 <+152>: mov    x2, x21
    0x18bfd2d34 <+156>: bl     0x18a72cd60               ; objc_msgSend
    0x18bfd2d38 <+160>: cbz    w0, 0x18bfd2d68           ; <+208>
    0x18bfd2d3c <+164>: add    x21, x21, #0x1            ; =0x1 
    0x18bfd2d40 <+168>: cmp    x21, x20
    0x18bfd2d44 <+172>: b.lo   0x18bfd2d28               ; <+144>
    0x18bfd2d48 <+176>: adrp   x8, 183538
    0x18bfd2d4c <+180>: add    x1, x8, #0x800            ; =0x800 
    0x18bfd2d50 <+184>: mov    x0, x19
    0x18bfd2d54 <+188>: bl     0x18a72cd60               ; objc_msgSend
    0x18bfd2d58 <+192>: mov    x21, x0
    0x18bfd2d5c <+196>: mov    x0, x21
    0x18bfd2d60 <+200>: bl     0x18a7273e0               ; objc_lookUpClass
    0x18bfd2d64 <+204>: b      0x18bfd2d6c               ; <+212>
    0x18bfd2d68 <+208>: mov    x0, #0x0
    0x18bfd2d6c <+212>: ldur   x8, [x29, #-0x38]
    0x18bfd2d70 <+216>: adrp   x9, 196266
    0x18bfd2d74 <+220>: ldr    x9, [x9, #0xf0]
    0x18bfd2d78 <+224>: ldr    x9, [x9]
    0x18bfd2d7c <+228>: cmp    x9, x8
    0x18bfd2d80 <+232>: b.ne   0x18bfd2d9c               ; <+260>
    0x18bfd2d84 <+236>: add    sp, sp, #0x3f0            ; =0x3f0 
    0x18bfd2d88 <+240>: ldp    x29, x30, [sp, #0x30]
    0x18bfd2d8c <+244>: ldp    x20, x19, [sp, #0x20]
    0x18bfd2d90 <+248>: ldp    x22, x21, [sp, #0x10]
    0x18bfd2d94 <+252>: ldp    x28, x27, [sp], #0x40
    0x18bfd2d98 <+256>: ret    
    0x18bfd2d9c <+260>: bl     0x18b03358c               ; __stack_chk_fail

根据调试器给出的注释,我们可以大致看到 NSClassFromString 函数的调用中包含了 4 个 OC 方法调用,看起来逻辑并不是非常直截了当,下面我们从头开始分析。

首先略过前 6 句对状态的暂存,我们看第一个 objc_msgSend 的逻辑:

0x18bfd2cb0 <+24>:  mov    x19, x0
0x18bfd2cb4 <+28>:  adrp   x8, 196266
0x18bfd2cb8 <+32>:  ldr    x8, [x8, #0xf0]
0x18bfd2cbc <+36>:  ldr    x8, [x8]
0x18bfd2cc0 <+40>:  stur   x8, [x29, #-0x38]
0x18bfd2cc4 <+44>:  cbz    x19, 0x18bfd2d6c          ; <+212>
0x18bfd2cc8 <+48>:  adrp   x8, 224951
0x18bfd2ccc <+52>:  ldr    x1, [x8, #0x4c0]
0x18bfd2cd0 <+56>:  mov    x0, x19
0x18bfd2cd4 <+60>:  bl     0x18a72cd60               ; objc_msgSend
0x18bfd2cd8 <+64>:  mov    x20, x0

栈溢出保护

从 +28 到 +40 的这段代码用于保存栈的哨兵 stack_chk_guard,它通过多次间接寻址拿到哨兵的值,将其写入 x29 - 0x38 的区域,这是为了防止栈溢出而添加的程序保护,运行到 +40 后,栈上的内存布局如下:

stack_guard 有效的保护了栈底的寄存器不被覆盖。当栈溢出时,首先会覆盖 stack_guard 的值,在函数返回前,通过校验 stack_guard 是否合法即可判断是否出现了栈溢出,程序的 +212 到 +232 这段代码即是退出前检查:

0x18bfd2d6c <+212>: ldur   x8, [x29, #-0x38]
0x18bfd2d70 <+216>: adrp   x9, 196266
0x18bfd2d74 <+220>: ldr    x9, [x9, #0xf0]
0x18bfd2d78 <+224>: ldr    x9, [x9]
0x18bfd2d7c <+228>: cmp    x9, x8
0x18bfd2d80 <+232>: b.ne   0x18bfd2d9c               ; <+260>

# __stack_chk_fail
0x18bfd2d9c <+260>: bl     0x18b03358c               ; __stack_chk_fail

这段代码从栈上相同的位置取出了 stack_guard,随后同哨兵的原始值作比较,不相等则直接跳转到栈溢出的处理逻辑,这种保护手段主要是为了保护开发者,防止开发者的代码发生了栈溢出而不自知,从而引起奇怪而未知的 BUG,但不能有效的防止栈溢出攻击,因为攻击者可以刻意构造栈溢出的内容,通过伪造哨兵来绕过检查,因此在写偏底层的代码时要注意提高代码质量,通过 CodeReview 和静态、动态扫描等方案规避栈溢出漏洞。

第一个 objc_msgSend 分析

基于上面的分析,我们略去栈溢出保护的代码,得到如下代码:

0x18bfd2cb0 <+24>:  mov    x19, x0
0x18bfd2cc8 <+48>:  adrp   x8, 224951
0x18bfd2ccc <+52>:  ldr    x1, [x8, #0x4c0]
0x18bfd2cd0 <+56>:  mov    x0, x19
0x18bfd2cd4 <+60>:  bl     0x18a72cd60               ; objc_msgSend
0x18bfd2cd8 <+64>:  mov    x20, x0

我们知道,objc_msgSend 的第一个参数为类的实例,第二个参数为 SEL,这里的关键是根据间接寻址指令计算出 SEL 从而得到调用的方法,有两种方式:动态分析和手动计算,由于这段代码没有分支逻辑,因此我们可以直接对 +60 行下断点,通过 lldb 的 register read 指令获取 SEL 的值:

(lldb) register read x1
x1 = 0x00000001b8c67778  "length"

显然这是一个 -[NSString length] 的调用,即获取 OC 字符串的长度,随后存入 x20:

x20 = [aClassName length]

第二个 objc_msgSend 分析

随后我们分析 +68 到 +108 的代码片段:

0x18bfd2cdc <+68>:  adrp   x8, 183685
0x18bfd2ce0 <+72>:  add    x1, x8, #0x8b8            ; =0x8b8 
0x18bfd2ce4 <+76>:  mov    x21, sp
0x18bfd2ce8 <+80>:  mov    x2, sp
0x18bfd2cec <+84>:  mov    w3, #0x3e8
0x18bfd2cf0 <+88>:  orr    w4, wzr, #0x4
0x18bfd2cf4 <+92>:  mov    x0, x19
0x18bfd2cf8 <+96>:  bl     0x18a72cd60               ; objc_msgSend
0x18bfd2cfc <+100>: cmp    w0, #0x0                  ; =0x0 
0x18bfd2d00 <+104>: csel   x21, x21, xzr, ne
0x18bfd2d04 <+108>: cbz    w0, 0x18bfd2d18           ; <+128>

显然 +68 到 +92 行是在为 objc_msgSend 准备参数,除去前两个固定入参外,还有 3 个参数:

  1. +80 行准备了方法的第 1 个入参 sp,sp 的值在 +0 处减去了 0x40,随后在 +20 处减去了 0x3f0,即存储在距离栈底 0x430 位置,这是一个局部指针变量,根据 +76 可知 x21 保存了这个指针的地址。
  2. +84 行准备了方法的第 2 个入参 0x3e8
  3. +88 行准备了方法的第 3 个入参 0x4,其中 orr 是逻辑或,wzr 是 word zero register,类似于 /dev/zero,写入无效,读出为 0。

采用同样的策略,在 +96 处下断点,得到 SEL 的内容:

(lldb) register read x1
x1 = 0x00000001b8d578b8  "getCString:maxLength:encoding:"

综合上面的分析,这一方法调用可以表示成如下形式:

char *aClassNameC; // x21
x0 = [aClassName getCString:aClassNameC maxLength:0x3e8 encoding:0x4];

随后的 +100 到 +108 行是对函数返回值和 CString 的处理,这里 +104 行出现了一个 csel 指令,该指令是 ARM 中的三目运算:

csel  Wd, Wn, Wm, cond 
# 等价于
Wd = cond ? Wn : Wm

因此整体可以翻译为:

char *aClassNameC; // x21
bool success = [aClassName getCString:aClassNameC maxLength:0x3e8 encoding:0x4];
aClassNameC = success ? aClassNameC : NULL;
if (!success) {
    goto 0x18bfd2d18;
}

这一处跳转是将 NSString 转换成长度不超过 1000 的 CString 的一个异常处理,我们接下来分析这个异常处理。

NSString2CString 的异常处理

从 0x18bfd2d18 开始,又涉及到一个分支逻辑:

0x18bfd2d18 <+128>: cbz    x20, 0x18bfd2d48          ; <+176>
0x18bfd2d1c <+132>: mov    x21, #0x0
0x18bfd2d20 <+136>: adrp   x8, 183600
0x18bfd2d24 <+140>: add    x22, x8, #0x9ce           ; =0x9ce 
0x18bfd2d28 <+144>: mov    x0, x19
0x18bfd2d2c <+148>: mov    x1, x22
0x18bfd2d30 <+152>: mov    x2, x21
0x18bfd2d34 <+156>: bl     0x18a72cd60               ; objc_msgSend

NSString 长度为 0 的情况

从上面的分析可知,x20 是入参 ClassName 的长度,如果转换失败,且入参长度为 0,则跳转到 0x18bfd2d48:

0x18bfd2d48 <+176>: adrp   x8, 183538
0x18bfd2d4c <+180>: add    x1, x8, #0x800            ; =0x800 
0x18bfd2d50 <+184>: mov    x0, x19
0x18bfd2d54 <+188>: bl     0x18a72cd60               ; objc_msgSend
0x18bfd2d58 <+192>: mov    x21, x0
0x18bfd2d5c <+196>: mov    x0, x21
0x18bfd2d60 <+200>: bl     0x18a7273e0               ; objc_lookUpClass
0x18bfd2d64 <+204>: b      0x18bfd2d6c               ; <+212>

这段代码先是调用了一个 OC 方法,随后通过 objc_lookUpClass 直接查找类对象,由于 objc_lookUpClass 的入参是 const char * 类型,因此这个 OC 方法一定是将 NSString 转为 CString 的方法,为了确定方法,我们需要解出 SEL,与上面不同的是,在这之前涉及到了两个分支跳转,代码不一定能走到这里,因此我们不能再偷懒通过 register read 直接拿到 SEL,而是需要手动计算,下面介绍手动计算 adrp 寻址的方法。

手动计算 ADRP 寻址

iOS汇编入门教程(三)汇编中的 Section 与数据存取 中我们介绍了 ARM64 的指令长度为 32 位,因此单条指令很难存下一个较大的偏移量,在基于 PC 寻址时,采用了指令拆分的形式,为了基于 PC 进行 +-4GB 的寻址,我们先用 adrp 取出基于二进制基址的页偏移的高 21 位(@PAGE)到寄存器,随后累加低 12 位(@PAGEOFF),即可得到目标地址,以上面的 SEL 取值过程为例:

0x18bfd2d48 <+176>: adrp   x8, 183538
0x18bfd2d4c <+180>: add    x1, x8, #0x800            ; =0x800 

我们首先将 adrp 所在地址的低 12 位清空得到 0x18bfd2000,183538 是 33 位中高 21 位的值,因此需要左移 33 - 21 = 12 位,随后加上 0x800 即可得到目标地址,通过 memory read 读取即可得到 SEL 的值:

(lldb) p/x 0x18bfd2000 + (183538 << 12) + 0x800
(long) $1 = 0x00000001b8cc4800
(lldb) memory read 0x00000001b8cc4800
0x1b8cc4800: 55 54 46 38 53 74 72 69 6e 67 00 73 74 72 69 6e  UTF8String.strin
0x1b8cc4810: 67 57 69 74 68 43 68 61 72 61 63 74 65 72 73 3a  gWithCharacters:

由此可见,这是一个 [aClassName UTF8String] 的调用,因此 +176 到 +204 可以翻译为:

aClassNameC = [aClassName UTF8String];
Class clazz = objc_lookUpClass(aClassNameC);
goto 0x18bfd2d6c;

随后我们看 0x18bfd2d6c 的代码:

0x18bfd2d6c <+212>: ldur   x8, [x29, #-0x38]
0x18bfd2d70 <+216>: adrp   x9, 196266
0x18bfd2d74 <+220>: ldr    x9, [x9, #0xf0]
0x18bfd2d78 <+224>: ldr    x9, [x9]
0x18bfd2d7c <+228>: cmp    x9, x8
0x18bfd2d80 <+232>: b.ne   0x18bfd2d9c               ; <+260>
0x18bfd2d84 <+236>: add    sp, sp, #0x3f0            ; =0x3f0 
0x18bfd2d88 <+240>: ldp    x29, x30, [sp, #0x30]
0x18bfd2d8c <+244>: ldp    x20, x19, [sp, #0x20]
0x18bfd2d90 <+248>: ldp    x22, x21, [sp, #0x10]
0x18bfd2d94 <+252>: ldp    x28, x27, [sp], #0x40
0x18bfd2d98 <+256>: ret    
0x18bfd2d9c <+260>: bl     0x18b03358c               ; __stack_chk_fail

其中 +212 到 +232 以及 +260 上面已经讲过,是栈溢出的检测代码,+236 到 +252 是函数返回前的恢复工作,去掉这些代码后,0x18bfd2d6c 就只剩下了一条 ret,即函数返回,因此上述代码可以完整地翻译为:

aClassNameC = [aClassName UTF8String];
return objc_lookUpClass(aClassNameC);

NSString 长度不为 0 的情况

即从 +132 到 +156:

0x18bfd2d1c <+132>: mov    x21, #0x0
0x18bfd2d20 <+136>: adrp   x8, 183600
0x18bfd2d24 <+140>: add    x22, x8, #0x9ce           ; =0x9ce 
0x18bfd2d28 <+144>: mov    x0, x19
0x18bfd2d2c <+148>: mov    x1, x22
0x18bfd2d30 <+152>: mov    x2, x21
0x18bfd2d34 <+156>: bl     0x18a72cd60               ; objc_msgSend
0x18bfd2d38 <+160>: cbz    w0, 0x18bfd2d68           ; <+208>
0x18bfd2d3c <+164>: add    x21, x21, #0x1            ; =0x1 
0x18bfd2d40 <+168>: cmp    x21, x20
0x18bfd2d44 <+172>: b.lo   0x18bfd2d28               ; <+144>

采用手工计算的方法,我们可以得到这个方法的 SEL 为 characterAtIndex:

(lldb) p/x 0x18bfd2000 + (183600 << 12) + 0x9ce
(long) $2 = 0x00000001b8d029ce
(lldb) memory read 0x00000001b8d029ce
0x1b8d029ce: 63 68 61 72 61 63 74 65 72 41 74 49 6e 64 65 78  characterAtIndex
0x1b8d029de: 3a 00 67 65 74 41 72 67 75 6d 65 6e 74 3a 61 74  :.getArgument:at

这段代码是一个循环结构,其中 x21 为迭代器从 0 开始 (+132),不断的从 aClassName 中取出第 x21 个字符 (+152 到 +156),判断是否为 0 (+160),为 0 则跳转到 0x18bfd2d68,不为 0 则将 x21 + 1 循环直到字符串结尾(+168 到 + 172),这里的 b.lo 是无符号小于比较,综上所述这段代码可翻译为:

// x20 为 aClassName 的 length
// x20 = [aClassName length];
for (int i = 0; i < aClassName.length; i++) {
    if ([aClassName characterAtIndex:i] == '\0') {
        goto 0x18bfd2d68;
    }
}

这段代码其实是为了检查 NSString 中间是否包含了 \0,如果包含则走 0x18bfd2d68 的异常逻辑,这是因为 \0 在 CString 中是结束符,会在转换时将字符串截断,带来未知的后果,接下来我们看 0x18bfd2d68 的处理:

0x18bfd2d68 <+208>: mov    x0, #0x0
0x18bfd2d6c <+212>: ldur   x8, [x29, #-0x38]
0x18bfd2d70 <+216>: adrp   x9, 196266
0x18bfd2d74 <+220>: ldr    x9, [x9, #0xf0]
0x18bfd2d78 <+224>: ldr    x9, [x9]
0x18bfd2d7c <+228>: cmp    x9, x8
0x18bfd2d80 <+232>: b.ne   0x18bfd2d9c               ; <+260>

由此可见跳转到了我们分析过的 +212 的上一行,+212 开始的代码是函数的返回工作,因此 +208 的作用是将返回值设为 0,即返回 nil 作为返回值,到这里上面的循环可以完整的翻译如下:

for (int i = 0; i < aClassName.length; i++) {
    if ([aClassName characterAtIndex:i] == '\0') {
        return nil;
    }
}

循环后面的代码从 +176 开始,这段代码我们在 NSString 长度为 0 的情况 中已经分析过了,是将 NSString 转成 UTF-8 编码的 CString 后调用 objc_lookUpClass 并返回。综合上面的分析,我们可以得到如下的代码片段:

NSUInteger lengthOC = [aClassName length];
char *aClassNameC; // x21
bool success = [aClassName getCString:aClassNameC maxLength:0x3e8 encoding:0x4];
aClassNameC = success ? aClassNameC : NULL;
if (!success) {
    if (lengthOC == 0) {
        aClassNameC = [aClassName UTF8String];
        return objc_lookUpClass(aClassNameC);
    } else {
        for (int i = 0; i < aClassName.length; i++) {
            if ([aClassName characterAtIndex:i] == '\0') {
                return nil;
            }
        }
        aClassNameC = [aClassName UTF8String];
        return objc_lookUpClass(aClassNameC);
    }
}

不要被 Xcode 和 LLDB 欺骗

接下来的分析针对 getCString 成功的分支逻辑,即从 +112 开始的代码,与上面的分析过程大同小异,这里留给读者自行完成,这里只讲解一下 +116 行的调用:

0x18bfd2d0c <+116>: bl     0x18c1391a8 ; symbol stub for: +[NSUnitElectricPotentialDifference megavolts]

看 Xcode 中提供的注释,这似乎是拿了一下电量中兆伏特 (MV) 的物理单位,真是费解成一匹马,其实这是 LLDB 动态分析的 BUG,我们可以反汇编 0x18c1391a8 的内容一探究竟:

(lldb) dis -a 0x18c1391a8
Foundation`+[NSUnitElectricPotentialDifference megavolts]:
0x18c1391a8 <+0>: adrp   x16, 195909
0x18c1391ac <+4>: ldr    x16, [x16, #0x8b8]
0x18c1391b0 <+8>: br     x16

这里取出了一个符号地址存入 x16 来执行,我们来看看 x16 里到底是什么:

(lldb) p/x 0x18c139000 + (195909 << 12) + 0x8b8
(long) $3 = 0x00000001bbe7e8b8
(lldb) memory read 0x00000001bbe7e8b8
0x1bbe7e8b8: 80 23 15 8b 01 00 00 00 38 06 03 8b 01 00 00 00  .#......8.......
0x1bbe7e8c8: f0 23 15 8b 01 00 00 00 1c 1c 15 8b 01 00 00 00  .#..............
(lldb) dis -a 0x018b152380
libsystem_platform.dylib`_platform_strlen:
    0x18b152380 <+0>:  and    x1, x0, #0xfffffffffffffff0
    0x18b152384 <+4>:  ldr    q0, [x1]

一顿操作后发现原来是 strlen,如果你一味地轻信 Xcode 显示的注释,分析就无法进行下去了,这告诉我们做事情一定要抱着怀疑的态度。

完整分析结果

注意,这里是完全按照控制流翻译出的代码,其中有大量的重复逻辑可以简化

NSClassFromString(NSString *aClassName) {
    if (aClassName == nil) {
    	return nil;
    }
    NSUInteger lengthOC = [aClassName length];
    char *aClassNameC;
    bool success = [aClassName getCString:aClassNameC maxLength:0x3e8 encoding:0x4];
    aClassNameC = success ? aClassNameC : NULL;
    if (!success) {
    	if (lengthOC == 0) { 
    	    aStringNameC = [aClassName UTF8String];
    	    return objc_lookUpClass(aStringNameC);
    	} else {
    	    for (int i = 0; i < lengthOC; i++) {
    	    	if ([aClassName characterAtIndex:0] == '\0') {
                    return nil;
                }
            }
            aStringNameC = [aClassName UTF8String];
            return objc_lookUpClass(aStringNameC);
    	}
    } else {
    	if (strlen(aClassNameC) == lengthOC) {
            return objc_lookUpClass(aClassNameC);
    	} else if (lengthOC == 0) {
            aStringNamcC = [aClassName UTF8String];
            return objc_lookUpClass(aStringNameC);
    	} else {
    	    for (int i = 0; i < lengthOC; i++) {
    	    	if ([aClassName characterAtIndex:0] == '\0') {
                    return nil;
                }
            }
            aStringNameC = [aClassName UTF8String];
            return objc_lookUpClass(aStringNameC);
    	}
    }
}

总结

直接基于 LLDB 动态分析函数实现还是比较浪费时间的,对此可在 lldb 中封装一些工具来辅助分析,例如识别栈溢出保护代码并移除、自动解析循环、自动解析间接寻址等,这会在接下来的文章中进行介绍。