上一节是不是为你开启了iOS逆向工程的另一扇门?IDA和LLDB的配合简直是无坚不摧,再配合ARM指令集文档,似乎已经达成了“它俩在手,天下我有”的境界。你是不是已经迫不及待,想要废寝忘食地实践刚学到的新知识了呢?
先别急。6.2节的2个例子虽然已经综合运用了IDA和LLDB,但仍没有涵盖LLDB的常用场景。因此下面以几个短例示范一下LLDB的使用技巧,它们在实战中的合理运用能够大大减少我们的工作量。
在上一节的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。
两种寻找调用者的方法都很简单粗暴,大家根据自己的喜好随便选一种就可以了。
为什么要更改进程执行逻辑?最常见的原因之一是因为有些时候,你想要调试的代码需要满足一定的条件才能触发执行,而这种条件不借助外界力量很难重现,所以可以更改进程执行逻辑,把进程引导向目标代码,从而调试它们。这句话听起来很拗口,举一个例子你就清楚了。看下面这样一段代码:
// 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)
我们通过动态更改寄存器的值来改变进程执行逻辑,达到了目的。