6.3 LLDB的使用技巧

上一节是不是为你开启了iOS逆向工程的另一扇门?IDA和LLDB的配合简直是无坚不摧,再配合ARM指令集文档,似乎已经达成了“它俩在手,天下我有”的境界。你是不是已经迫不及待,想要废寝忘食地实践刚学到的新知识了呢?

先别急。6.2节的2个例子虽然已经综合运用了IDA和LLDB,但仍没有涵盖LLDB的常用场景。因此下面以几个短例示范一下LLDB的使用技巧,它们在实战中的合理运用能够大大减少我们的工作量。

6.3.1 寻找函数调用者

在上一节的2个例子里,在还原函数调用链时,主要分析的是一个函数调用了哪些函数,也就是还原了函数调用链的下游。当需要追溯函数调用链上游的时候,那就需要分析一个函数的调用者是谁了。看下面这样一段代码:


// clang -arch armv7 -isysroot `xcrun --sdk iphoneos --show-
sdk-path` -framework Foundation -o MainBinary main.m
#include <stdio.h>
#include <dlfcn.h>
#import <Foundation/Foundation.h>
extern void TestFunction0(void)
{
        NSLog(@"iOSRE: %u", arc4random_uniform(0));
}
extern void TestFunction1(void)
{
        NSLog(@"iOSRE: %u", arc4random_uniform(1));
}
extern void TestFunction2(void)
{
        NSLog(@"iOSRE: %u", arc4random_uniform(2));
}
extern void TestFunction3(void)
{
        NSLog(@"iOSRE: %u", arc4random_uniform(3));
}
int main(int argc, char **argv)
{
        TestFunction3();
        return 0;
}

把这段代码存成名为main.m的文件,用注释里的那句话编译它,然后把MainBinary拖进IDA,并查看NSLog的交叉引用,如图6-48所示。

图6-48 查看NSLog的交叉引用

可以看到,在这段代码中,NSLog出现在了4个函数里,如果在逆向时发现syslog中出现了“iOSRE:0”,那么这个输出到底是来自哪个NSLog呢?当代码的逻辑比较简单时,靠人工就可以指出只有TestFunction3得到了调用,它进而又调用了NSLog。可如果这里有20个TestFunction,分别被8个不同的函数调用呢?逻辑变得复杂,人工分析就很吃力了。在这种情况下要寻找NSLog的调用者,LLDB就能起到很大的作用;用LLDB寻找函数调用者,主要有2种方法。

1.查看LR

还记得6.1.3节介绍的LR寄存器吗?它的作用是保存返回地址。什么是返回地址?举例如下:


void FunctionA()
{
……
FunctionB();
……
}

在上面的伪代码中,FunctionA调用FunctionB,而A和B一般位于内存中的2块不同区域,它们的地址没有直接关联。B执行结束后,需要回到A里继续执行接下来的指令,如图6-49所示。

图6-49 返回地址示意图

B执行结束后返回的那个地方,就是返回地址。因为它位于调用者的内部,所以如果能知道LR的值,就可以知道调用者是谁;概念不好懂,操作一遍你就全明白了。先把Foundation.framework的二进制文件拖进IDA,初始分析结束后定位到NSLog,查看其基地址,如图6-50所示。

它的基地址是0x2261ab94,等会我们要在它上下断点,打印LR的值。接着用debugserver启动MainBinary,如下:

图6-50 查看NSLog基地址


FunMaker-5:~ root# debugserver -x backboard *:1234 /var/tmp/MainBinary
debugserver-@(#)PROGRAM:debugserver  PROJECT:debugserver-320.2.89
 for armv7.
Listening to port 1234 for a connection from *...

再用LLDB连过去,如下:


(lldb) process connect connect://localhost:1234
Process 450336 stopped
* thread #1: tid = 0x6df20, 0x1fec7000 dyld`_dyld_start, stop reason = signal SIGSTOP
    frame #0: 0x1fec7000 dyld`_dyld_start
dyld`_dyld_start:
-> 0x1fec7000:  mov    r8, sp
   0x1fec7004:  sub    sp, sp, #16
   0x1fec7008:  bic    sp, sp, #7
   0x1fec700c:  ldr    r3, [pc, #112]         ; _dyld_start + 132
(lldb) image list -f
[  0] /Users/snakeninny/Library/Developer/Xcode/iOSDeviceSupport/8.1 (12B411)/Symbols/usr/lib/dyld

此时MainBinary还未启动,我们位于dyld内部。接下来,一直执行“ni”命令,直到出现“error:invalid thread”的提示,如下:


(lldb) ni
Process 450336 stopped
* thread #1: tid = 0x6df20, 0x1fec7004 dyld`_dyld_start + 4, stop reason = instruction step over
    frame #0: 0x1fec7004 dyld`_dyld_start + 4
dyld`_dyld_start + 4:
-> 0x1fec7004:  sub    sp, sp, #16
   0x1fec7008:  bic    sp, sp, #7
   0x1fec700c:  ldr    r3, [pc, #112]            ; _dyld_start + 132
   0x1fec7010:  sub    r0, pc, #8
(lldb) 
Process 450336 stopped
* thread #1: tid = 0x6df20, 0x1fec7008 dyld`_dyld_start + 8, stop reason = instruction step over
    frame #0: 0x1fec7008 dyld`_dyld_start + 8
dyld`_dyld_start + 8:
-> 0x1fec7008:  bic    sp, sp, #7
   0x1fec700c:  ldr    r3, [pc, #112]            ; _dyld_start + 132
   0x1fec7010:  sub    r0, pc, #8
   0x1fec7014:  ldr    r3, [r0, r3]
……
(lldb)
error: invalid thread

到这里,不要再执行“ni”命令了,此时dyld开始加载MainBinary,等待一会,进程又会停下来,这时我们已经在MainBinary内部,可以开始调试了,如下:


Process 450336 stopped
* thread #1: tid = 0x6df20, 0x1fec7040 dyld`_dyld_start + 64, queue = 'com.apple.main-thread, stop reason = instruction step over
    frame #0: 0x1fec7040 dyld`_dyld_start + 64
dyld`_dyld_start + 64:
-> 0x1fec7040:  ldr    r5, [sp, #12]
   0x1fec7044:  cmp    r5, #0
   0x1fec7048:  bne    0x1fec7054                ; _dyld_start + 84
   0x1fec704c:  add    sp, r8, #4

下面看看Foundation.framework的ASLR偏移,如下:


(lldb) image list -o -f
[  0] 0x000fc000 /private/var/tmp/MainBinary(0x0000000000100000)
[  1] 0x000c6000 /Users/snakeninny/Library/Developer/Xcode/iOS DeviceSupport/8.1 (12B411)/Symbols/usr/lib/dyld
[  2] 0x06db3000 /Users/snakeninny/Library/Developer/Xcode/iOS DeviceSupport/8.1 (12B411)/Symbols/System/Library/Frameworks/Foundation.framework/Foundation
……

断点下在0x6db3000+0x2261ab94=0x293CDB94。然后执行“c”命令,触发断点,如下:


(lldb) br s -a 0x293CDB94
Breakpoint 1: where = Foundation`NSLog, address = 0x293cdb94
(lldb) c
Process 450336 resuming
Process 450336 stopped
* thread #1: tid = 0x6df20, 0x293cdb94 Foundation`NSLog, queue = 'com.apple.main-thread, stop reason = breakpoint 1.1
    frame #0: 0x293cdb94 Foundation`NSLog
Foundation`NSLog:
-> 0x293cdb94:  sub    sp, #12
   0x293cdb96:  push   {r7, lr}
   0x293cdb98:  mov    r7, sp
   0x293cdb9a:  sub    sp, #4

最后打印LR的值,如下:


(lldb) p/x $lr
(unsigned int) $0 = 0x00107f8d

因为MainBinary的基地址是0x000fc000,所以在IDA里打开MainBinary,然后跳转到0x107f8d-0xfc000=0xBF8D,如图6-51所示。

图6-51 TestFunction3

它位于TestFunction3中“BLX_NSLog”的正下方,我们找到了NSLog的调用者。有一点需要强调的是,因为LR在被调用者内部可能会产生变化,所以断点一定要下在基地址上。很简单吧?

2.执行“ni”命令到调用者内部

虽然“查看LR”的方法很简单,但在上面的例子里,我们耍了个小花样:因为事先知道MainBinary调用了NSLog,所以才用LR减去MainBinary的ASLR偏移得到地址,然后在IDA中跳过去。而一般情况下,我们不知道哪个函数调用了NSLog,更不知道哪个模块调用了NSLog,因此也就不知道该用LR减去谁的ASLR偏移了。要解决这个问题,我们的理论依据仍是“B执行结束后,需要回到A里,继续执行接下来的指令”——只要在被调用者的末尾下个断点,然后一直执行“ni”命令,就会回到调用者内部,从而发现调用者。还是来操作一遍:重复上面的步骤,用debugserver重新启动MainBinary,用LLDB挂接过去,直到进入MainBinary内部,然后查看Foundation.framework的ASLR偏移,如下:


(lldb) image list -o -f
[  0] 0x0000c000 /private/var/tmp/MainBinary(0x0000000000010000)
[  1] 0x000c5000 /Users/snakeninny/Library/Developer/Xcode/iOS DeviceSupport/8.1 (12B411)/Symbols/usr/lib/dyld
[  2] 0x06db3000 /Users/snakeninny/Library/Developer/Xcode/iOSDeviceSupport/8.1 (12B411)/Symbols/System/Library/Frameworks/Foundation.framework/Foundation
……

它的ASLR偏移是0x6db3000。依图6-50,NSLog最后一条指令的地址是0x2261ABB6,因此,在0x6db3000+0x2261ABB6=0x293CDBB6上下一个断点,然后执行“c”命令,触发断点,如下:


(lldb) br s -a 0x293CDBB6
Breakpoint 1: where = Foundation`NSLog + 34, address = 0x293cdbb6
(lldb) c
Process 452269 resuming
(lldb) 2014-11-30 23:45:37.070 MainBinary[3454:452269] iOSRE: 1
Process 452269 stopped
* thread #1: tid = 0x6e6ad, 0x293cdbb6 Foundation`NSLog + 34, queue = 'com.apple.main-thread, stop reason = breakpoint 1.1
    frame #0: 0x293cdbb6 Foundation`NSLog + 34
Foundation`NSLog + 34:
-> 0x293cdbb6:  bx     lr
Foundation`NSLogv:
   0x293cdbb8:  push   {r4, r5, r6, r7, lr}
   0x293cdbba:  add    r7, sp, #12
   0x293cdbbc:  sub    sp, #12

注意“->”上方的文字,它指示了当前的模块。接着执行“ni”命令,如下:


(lldb) ni
Process 452269 stopped
* thread #1: tid = 0x6e6ad, 0x00017fa6 MainBinary`main + 22, queue = 'com.apple.main-thread, stop reason = instruction step over
    frame #0: 0x00017fa6 MainBinary`main + 22
MainBinary`main + 22:
-> 0x17fa6:  movs   r0, #0
   0x17fa8:  movt   r0, #0
   0x17fac:  add    sp, #12
   0x17fae:  pop    {r7, pc}

进入了MainBinary,停在了0x17fa6。0x17fa6–0xc000=0xbfa6,对照图6-51,我们找到了NSLog的调用者TestFunction3。

两种寻找调用者的方法都很简单粗暴,大家根据自己的喜好随便选一种就可以了。

6.3.2 更改进程执行逻辑

为什么要更改进程执行逻辑?最常见的原因之一是因为有些时候,你想要调试的代码需要满足一定的条件才能触发执行,而这种条件不借助外界力量很难重现,所以可以更改进程执行逻辑,把进程引导向目标代码,从而调试它们。这句话听起来很拗口,举一个例子你就清楚了。看下面这样一段代码:


// clang -arch armv7 -isysroot `xcrun --sdk iphoneos --show-sdk-path` -framework Foundation -framework UIKit -o MainBinary main.m
#include <stdio.h>
#include <dlfcn.h>
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
extern void ImportantAndComplicatedFunction(void)
{
      NSLog(@"iOSRE: Suppose I'm a very important and 
complicated function");
}
int main(int argc, char **argv)
{
      if ([[[UIDevice currentDevice] systemVersion] 
isEqualToString:@"8.1.1"]) ImportantAndComplicatedFunction();
      return 0;
}

把这段代码存成名为main.m的文件,用注释里的那句话编译它,然后把MainBinary拷到iOS的“/var/tmp/”下,如下:


snakeninnys-MacBook:6 snakeninny$ scp MainBinary root@iOSIP:/var/tmp/
MainBinary                        100%49KB  48.6KB/s   00:00

运行它,效果如下:


FunMaker-5:~ root# /var/tmp/MainBinary
FunMaker-5:~ root# 

因为笔者的iOS系统是8.1,所以自然没有任何输出。笔者对ImportantAndComplicated-Function很感兴趣,想动态调试它,但手头没有8.1.1的系统,怎么办呢?那就动态更改代码,让这个函数得到执行。下面来操作一遍,请读者注意观察。先把MainBinary拖进IDA,定位到ImportantAndComplicatedFunction被调用之前的指令,如图6-52所示。

然后用debugserver启动MainBinary,用LLDB挂接过去,直到进入MainBinary内部,再查看MainBinary的ASLR偏移,如下:


(lldb) image list -o -f
[  0] 0x0000e000 /private/var/tmp/MainBinary(0x0000000000012000)
……

因为图6-52最上面的那个“CMP R0,#0”地址是0xBF46,所以把断点下在0xbf46+0xe000=0x19F46,然后执行“c”命令触发它,然后看看R0的值,如下:

图6-52 ImportantAndComplicatedFunction得到调用之前


(lldb) br s -a 0x19F46
Breakpoint 1: where = MainBinary`main + 134, address = 0x00019f46
(lldb) c
Process 456316 resuming
Process 456316 stopped
* thread #1: tid = 0x6f67c, 0x00019f46 MainBinary`main + 134, queue = 'com.apple.main-thread, stop reason = breakpoint 1.1
    frame #0: 0x00019f46 MainBinary`main + 134
MainBinary`main + 134:
-> 0x19f46:  cmp    r0, #0
   0x19f48:  beq    0x19f4e                   ; main + 142
   0x19f4a:  bl     0x19ea4                   ; ImportantAndComplicatedFunction
   0x19f4e:  movs   r0, #0
(lldb) p $r0
(unsigned int) $0 = 0

R0是0,因此ImportantAndComplicatedFunction得不到执行。如果把R0改成1,情况就不同了,如下:


(lldb) register write r0 1
(lldb) p $r0
(unsigned int) $1 = 1
(lldb) c
Process 456316 resuming
(lldb) 2014-12-01 00:41:47.779 MainBinary[3482:457105] iOSRE: Suppose I'm a very important and complicated function
Process 456316 exited with status = 0 (0x00000000)

我们通过动态更改寄存器的值来改变进程执行逻辑,达到了目的。