4.3 LLDB与debugserver

4.3.1 LLDB简介

如果说IDA是倚天剑,那么LLDB就是屠龙刀,两者在iOS逆向工程中的地位不相上下,难分伯仲。LLDB全称为“Low Level Debugger”,是由苹果出品,内置于Xcode中的动态调试工具,不但通吃C、C++、Objective-C,还全盘支持OSX、iOS,以及iOS模拟器。LLDB的功能可以概括为以下四点:

·在指定的条件下启动程序;

·在指定的条件下停止程序;

·在程序停止的时候检查程序内部发生的事;

·在程序停止的时候对程序进行改动,观察程序的执行过程有什么变化。

LLDB没有图形界面,使用时看着Terminal中黑压压的一片文字,初学者很容易被吓跑,但是一旦掌握其基本用法,配合IDA双管齐下,就能解决很多难倒大片初学者的问题,投资回报极高。LLDB是运行在OSX中的,要想调试iOS,还需要另一个工具的配合,它就是debugserver。

4.3.2 debugserver简介

debugserver运行在iOS上,顾名思义,它作为服务端,实际执行LLDB(作为客户端)传过来的命令,再把执行结果反馈给LLDB,显示给用户,即所谓的“远程调试”。在默认情况下,iOS上并没有安装debugserver,只有在设备连接过一次Xcode,并在Window→Devices菜单中添加此设备后,debugserver才会被Xcode安装到iOS的“/Developer/usr/bin/”目录下。

但是,因为缺少task_for_pid权限,通过Xcode安装的debugserver只能调试我们自己的App——调试自己的App是正向开发的事儿,而我们是想搞逆向工程,我们有自己App的源代码,还需要逆哪门子向?要能debug别人的App才够给力啊!别担心,下面就以笔者的操作为例,看看怎么配置debugserver+LLDB,动态调试别人的App,发挥它们在逆向工程中的真正威力。

4.3.3 配置debugserver

1.帮debugserver减肥

对照表4-1,记下设备的ARM信息。

表4-1 支持iOS 8的设备一览

笔者的设备是iPhone 5,对应的ARM是armv7s。将未经处理的debugserver从iOS拷贝到OSX中的“/Users/snakeninny/”目录下,命令如下:


snakeninnysiMac:~ snakeninny$ scp root@iOSIP:/Developer/usr/bin/debugserver ~/debugserver

然后帮它减肥,命令如下:


snakeninnysiMac:~ snakeninny$ lipo -thin armv7s ~/debugserver -output ~/debugserver

注意把这里的“armv7s”换成你的设备所对应的ARM。

2.给debugserver添加task_for_pid权限

下载http://iosre.com/ent.xml 到OSX的“/Users/snakeninny/”目录,然后运行如下命令:


snakeninnysiMac:~ snakeninny$ /opt/theos/bin/ldid -Sent.xml debugserver

注意,“-S”选项与“ent.xml”之间是没有空格的。

正常情况下,上面这条命令会在5秒内执行完毕。如果ldid卡住了,执行超时,就换一种方案:下载http://iosre.com/ent.plist 到“/Users/snakeninny/”,然后运行如下命令:


snakeninnysiMac:~ snakeninny$ codesign -s - --entitlements ent.plist -f debugserver

3.将经过处理的debugserver拷回iOS

将经过处理的debugserver拷回iOS,并添加执行权限,命令如下:


snakeninnysiMac:~ snakeninny$ scp ~/debugserver root@iOSIP:/usr/bin/debugserver
snakeninnysiMac:~ snakeninny$ ssh root@iOSIP
FunMaker-5:~ root# chmod +x /usr/bin/debugserver

这里之所以把处理过的debugserver存放在iOS的“/usr/bin/”下,而没有覆盖“/Developer/usr/bin/”下的原版debugserver,一是因为原版debugserver是不可写的,无法覆盖;二是因为“/usr/bin/”下的命令无须输入全路径就可以执行,即在任意目录下运行“debugserver”都可启动处理过的debugserver。

4.3.4 用debugserver启动或附加进程

debugserver最常用的2种场景,就是启动和附加进程,它们的命令都很简单,分别是:


debugserver -x backboard IP:port /path/to/executable

debugserver会启动executable,并开启port端口,等待来自IP的LLDB接入。


debugserver IP:port -a "ProcessName"

debugserver会附加ProcessName,并开启port端口,等待来自IP的LLDB接入。

例如:


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

上面的代码会启动MobileSMS,并开启1234端口,等待任意IP地址的LLDB接入。而:


FunMaker-5:~ root# debugserver 192.168.1.6:1234 -a "MobileSMS"
debugserver-@(#)PROGRAM:debugserver  PROJECT:debugserver-320.2.89
 for armv7.
Attaching to process MobileNotes...
Listening to port 1234 for a connection from 192.168.1.6...

会附加MobileSMS,并开启1234端口,等待来自192.168.1.6的LLDB接入。

如果上面的命令在执行时报错,如下:


FunMaker-5:~ root# debugserver *:1234 -a "MobileSMS"
dyld: Library not loaded: /Developer/Library/PrivateFrameworks/ARMDisassembler.framework/ARMDisassembler
  Referenced from: /usr/bin/debugserver
  Reason: image not found
Trace/BPT trap: 5

说明iOS上的“/Developer/”目录下缺少必要的调试数据。这种情况一般是因为没有在Xcode的Window→Devices菜单中添加此设备,重新添加设备就可以解决问题。

当退出debugserver时,当前调试的进程也会一并退出。debugserver的配置到此结束,接下来的所有操作都是在LLDB上完成的。

4.3.5 LLDB的使用说明

在了解LLDB的用法之前,需要对LLDB的一个大Bug有所了解:Xcode 6所附带的LLDB(版本号320.x.xx)在armv7和armv7s设备上有时会混淆ARM和THUMB指令,根本无法调试,且在本书截稿之时,此Bug仍未得到修复。一个暂时的解决方案是从https://developer.apple.com/downloads/index.action 下载安装Xcode 5.0.1或Xcode 5.0.2,它们所附带的LLDB(版本号300.x.xx)可以正常调试armv7和armv7s设备。在安装旧版Xcode的时候,注意将其安装在与当前Xcode不同的路径下,如/Applications/OldXcode.app,这样就不会影响当前的Xcode了。在启用LLDB时,在Terminal中输入如下命令:


snakeninnysiMac:~ snakeninny$ /Applications/OldXcode.app/Contents/Developer/usr/bin/lldb

即可启动旧版LLDB,然后用LLDB连接正在等待的debugserver,命令如下:


(lldb) process connect connect://iOSIP:1234
Process 790987 stopped
* thread #1: tid = 0xc11cb, 0x3995b4f0 libsystem_kernel.dylib`mach_msg_trap + 20, queue = 'com.apple.main-thread, stop reason = signal SIGSTOP
    frame #0: 0x3995b4f0 libsystem_kernel.dylib`mach_msg_trap + 20
libsystem_kernel.dylib`mach_msg_trap + 20:
-> 0x3995b4f0:  pop    {r4, r5, r6, r8}
   0x3995b4f4:  bx     lr
libsystem_kernel.dylib`mach_msg_overwrite_trap:
   0x3995b4f8:  mov    r12, sp
   0x3995b4fc:  push   {r4, r5, r6, r8}

注意,“process connect connect://iOSIP:1234”的执行耗时较长,在WiFi条件下一般需要3分钟以上时间,请耐心等待。在4.6节里,会有通过USB连接调试的介绍,届时速度会大幅增加。当进程停下来时,就可以正式开始调试了。接下来看看常用的LLDB命令有哪些。

1.image list

“image list”与GDB中的“info shared”类似,用于列举当前进程中的所有模块(image)。因为ASLR(Address Space Layout Randomization,详见http://theiphonewiki.com/wiki/ASLR )的关系,每次进程启动时,同一进程的所有模块在虚拟内存中的起始地址都会产生随机偏移。

举个简单的例子,进程A中有一个模块B,B模块的大小是100字节。进程A第一次启动时,模块B可能会被加载到虚拟内存的0x00到0x64,第二次启动被加载到0x10到0x74,第三次被加载到0x60到0xC4,也就是说它的大小虽然未变,但起始地址每次都在变,然而这个起始地址恰恰是接下来会频繁用到的一个关键数据。那么问题来了,如何获得这个数据呢?

答案就是使用“image list-o-f”命令。待LLDB连接debugserver后,输入“image list-o-f”命令,输出如下:


(lldb) image list -o -f
[  0] 0x000cf000 /private/var/db/stash/_.29LMeZ/Applications/SMSNinja.app/SMSNinja(0x00000000000d3000)
[  1] 0x0021a000 /Library/MobileSubstrate/MobileSubstrate.dylib(0x000000000021a000)
[  2] 0x01645000 /usr/lib/libobjc.A.dylib(0x00000000307b5000)
[  3] 0x01645000 /System/Library/Frameworks/Foundation.framework/Foundation (0x0000000023c4f000)
[  4] 0x01645000 /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation(0x0000000022f0b000)
[  5] 0x01645000 /System/Library/Frameworks/UIKit.framework/UIKit (0x00000000264c1000)
[  6] 0x01645000 /System/Library/Frameworks/CoreGraphics.framework/CoreGraphics (0x0000000023238000)
……
 [235] 0x01645000 /System/Library/Frameworks/CoreGraphics.framework/Resources/libCGXType.A.dylib(0x00000000233a2000)
[236] 0x0008a000 /usr/lib/dyld(0x000000001fe8a000)

在上面的输出内容中,第一列[X]是模块的序号;第二列是模块在虚拟内存中的起始地址因ASLR而产生的随机偏移(以下简称ASLR偏移);第三列是模块的全路径,括号里是偏移之后的起始地址。各种偏移,各种地址,是不是把你绕晕了?没关系,看一个简单的示例你就全明白了。

假设虚拟内存是一个靶场,有1000个靶位。进程的模块是靶子,一共有600个靶子。1000个靶位只摆了600个靶子,这些靶子均匀地排成一横排,靶位1放着靶子1,靶位2放着靶子2,依此类推,靶位600放着靶子600,而靶位601到1000是空着的,如图4-13所示(上面是靶位号,下面是靶子号)。

图4-13 靶场(1)

模块在内存中的起始地址,就是靶子所在的靶位,术语叫模块基地址(image base address)。现在靶场觉得这样的靶子排列过于简单,打靶的人在适应靶子排列规律后,很容易百发百中,因此将每块靶子往后随机移动了若干靶位,移动之后靶位5放着靶子1,靶位6放着靶子2,靶位8放着靶子3,靶位13放着靶子4,靶位15放着靶子5……靶位886放着靶子600,如图4-14所示。

图4-14 靶场(2)

也就是靶子1偏移了4个靶位,靶子2偏移了4个靶位,靶子3偏移了5个靶位,靶子4偏移了9个靶位,靶子5偏移了10个靶位,靶子600偏移了286个靶位——这种随机偏移(ASLR)大大增加了打靶的难度。对于靶子1来说,偏移前的基地址是靶位1,偏移后的基地址是靶位5,而偏移的值是4个靶位,即


偏移后模块基地址 = 偏移前模块基地址 + ASLR偏移

回到逆向工程的场景里来,以刚才“image list-o-f”输出中的第4个模块(即Foundation)为例,它的ASLR偏移是0x1645000,偏移后模块基地址是0x23c4f000,所以它的偏移前模块基地址是0x23c4f000-0x1645000=0x2260A000。

0x2260A000是哪里来的呢?把Foundation二进制文件拖到IDA里,初始分析完成后的界面如图4-15所示。

图4-15 在IDA中分析Foundation

把IDA View-A拉到最上面,看到第一行的“HEADER:2260A000”了吗?这就是0x2260A000的来源。

既然明白了“基地址”的意思是“起始地址”,那么趁热打铁,了解一下与“模块基地址”相似的另一概念:“符号基地址(symbol base address)”。回到IDA,在Functions window里搜索“NSLog”,然后跳转到它的实现,如图4-16所示。

图4-16 NSLog

因为Foundation的基地址是已知的,而NSLog函数在Foundation中的位置是固定的,所以可以根据下面的公式,得出NSLog的基地址:


NSLog的基地址 = NSLog在Foundation中的相对位置 + Foundation的基地址

那“NSLog函数在Foundation中的相对位置”又是什么呢?回到图4-16,NSLog函数第一条指令“SUB SP,SP,#0xC”左边的那个数0x2261AB94,代表NSLog在Foundation中的位置,减去Foundation第一行“HEADER:2260A000”中提取出来的0x2260A000,就是NSLog函数在Foundation中的相对位置,即0x10B94。

因此,NSLog的基地址=0x10B94+0x23c4f000=0x23C5FB94。细心的朋友一定已经发现了,公式


偏移后模块基地址 = 偏移前模块基地址 + ASLR偏移

稍作修改,就可以用到符号基地址的计算中:


偏移后符号基地址 = 偏移前符号基地址 + 符号所在模块的ASLR偏移

下面来验证一下。

NSLog的偏移前符号基地址是0x2261AB94,Foundation的ASLR偏移是0x1645000,两者相加正是0x23C5FB94。

举一反三,指令基地址的计算也可以套用上面的公式:


偏移后指令基地址 = 偏移前指令基地址 + 指令所在模块的ASLR偏移

自然,符号基地址=符号对应函数第一条指令的基地址。

在接下来的内容中,会大量用到偏移后基地址,因此必须把这一节的几个概念弄懂,然后记住:偏移前基地址从IDA里看,ASLR偏移从LLDB里看,两者相加就是偏移后基地址。至于看哪里,怎么看,文中也已解释清楚,现在要靠你自己完全掌握了。

2.breakpoint

“breakpoint”与GDB中的“break”类似,用于设置断点。在逆向工程中一般用到的是:


b function


br s –a address

以及


br s –a 'ASLROffset+address'

前者在函数的起始位置设置断点,如下面的命令:


(lldb) b NSLog
Breakpoint 2: where = Foundation`NSLog, address = 0x23c5fb94

后两者在地址处设置断点,如下面的命令:


(lldb) br s -a 0xCCCCC
 Breakpoint 5: where = SpringBoard`___lldb_unnamed_function303$$SpringBoard, address = 0x000ccccc
(lldb) br s -a '0x6+0x9'
Breakpoint 6: address = 0x0000000f

注意,在输出的“Breakpoint X:”中,这个X是断点的序号,稍后就会用到。当进程停在断点上时,断点所在的那一行代码并未得到执行。

因为逆向工程中的调试涉及的多是汇编代码,所以大多数情况下都是在某一条汇编指令上下断点,在函数上下断点的情况很少。要在汇编指令上下断点,就要知道它的偏移后基地址,前面已经详细讲解过了。我们以在“-[SpringBoard_menuButtonDown:]”函数的第一条指令设置断点为例,演示一下操作流程。

(1)用IDA查看偏移前基地址

在IDA中打开SpringBoard二进制文件,待初始分析结束后切换到Text view,定位到“-[SpringBoard_menuButtonDown:]”,如图4-17所示。

可以看到,第一条指令“PUSH{R4-R7,LR}”的偏移前基地址是0x17730。

(2)用LLDB查看ASLR偏移

先ssh到iOS中配置debugserver,命令如下:


snakeninnysiMac:~ snakeninny$ ssh root@iOSIP
FunMaker-5:~ root# debugserver *:1234 -a "SpringBoard"
debugserver-@(#)PROGRAM:debugserver  PROJECT:debugserver-320.2.89
 for armv7.
Attaching to process SpringBoard...
Listening to port 1234 for a connection from *...

图4-17 [SpringBoard_menuButtonDown:]

然后在OSX中用LLDB远程连接,并查看ASLR偏移,命令如下:


snakeninnysiMac:~ snakeninny$ /Applications/OldXcode.app/Contents/Developer/usr/bin/lldb 
(lldb) process connect connect://iOSIP:1234
Process 93770 stopped
* thread #1: tid = 0x16e4a, 0x30dee4f0 libsystem_kernel.dylib`mach_msg_trap + 20, queue = 'com.apple.main-thread, stop reason = signal SIGSTOP
    frame #0: 0x30dee4f0 libsystem_kernel.dylib`mach_msg_trap + 20
libsystem_kernel.dylib`mach_msg_trap + 20:
-> 0x30dee4f0:  pop    {r4, r5, r6, r8}
   0x30dee4f4:  bx     lr
libsystem_kernel.dylib`mach_msg_overwrite_trap:
   0x30dee4f8:  mov    r12, sp
   0x30dee4fc:  push   {r4, r5, r6, r8}
(lldb) image list -o -f
[0] 0x000b5000 /System/Library/CoreServices/SpringBoard.app/SpringBoard (0x00000000000b9000)
[1] 0x006ea000 /Library/MobileSubstrate/MobileSubstrate.dylib(0x00000000006ea000)
[2] 0x01645000 /System/Library/PrivateFrameworks/StoreServices.framework/StoreServices (0x000000002ca70000)
[3] 0x01645000 /System/Library/PrivateFrameworks/AirTraffic.framework/AirTraffic (0x0000000027783000)
……
[419] 0x00041000 /usr/lib/dyld(0x000000001fe41000)
 (lldb) c
Process 93770 resuming

SpringBoard模块的ASLR偏移是0xb5000。

(3)设置并触发断点

综上,第一条指令的偏移后基地址是0x17730+0xb5000=0xCC730。在LLDB中输入“br s-a 0xCC730”即可在第一条指令处设下断点,如下:


(lldb) br s -a 0xCC730
Breakpoint 1: where = SpringBoard`___lldb_unnamed_function299$$SpringBoard, address = 0x000cc730

按下设备上的home键,触发断点,如下:


(lldb) br s -a 0xCC730
Breakpoint 1: where = SpringBoard`___lldb_unnamed_function299$$SpringBoard, address = 0x000cc730
Process 93770 stopped
* thread #1: tid = 0x16e4a, 0x000cc730 SpringBoard`___lldb_unnamed_function299$$SpringBoard, queue = 'com.apple.main-thread, stop reason = breakpoint 1.1
    frame #0: 0x000cc730 SpringBoard`___lldb_unnamed_function299$$SpringBoard
SpringBoard`___lldb_unnamed_function299$$SpringBoard:
-> 0xcc730:  push   {r4, r5, r6, r7, lr}
   0xcc732:  add    r7, sp, #12
   0xcc734:  push.w {r8, r10, r11}
   0xcc738:  sub    sp, #80
(lldb) p (char *)$r1
(char *) $0 = 0x0042f774 "_menuButtonDown:"

当进程停下来之后,可以用“c”命令让进程继续运行。LLDB相较于GDB的一个重大改进,是可以在进程运行的过程中输入LLDB命令。需要注意的是,部分进程(如SpringBoard)在停止一段时间后会因响应超时而自动重启,对于这类进程,要尽量让它维持在运行状态,避免因自动重启而导致调试信息丢失的悲剧发生。

还可以通过“br dis”、“br en”和“br del”系列命令来禁用、启用和删除断点。如果要禁用所有断点(“dis”代表“disable”),命令如下:


(lldb) br dis
All breakpoints disabled. (2 breakpoints)

禁用某个断点的命令如下:


(lldb) br dis 6
1 breakpoints disabled.

启用所有断点(“en”代表“enable”)的命令如下:


(lldb) br en
All breakpoints enabled. (2 breakpoints)

启用某个断点的命令如下:


 (lldb) br en 6
1 breakpoints enabled.

删除所有断点(“del”代表“delete”)的命令如下:


(lldb) br del
About to delete all breakpoints, do you want to do that?: [Y/n] Y

删除某个断点的命令如下:


(lldb) br del 8
1 breakpoints deleted; 0 breakpoint locations disabled.

另一个非常有用的命令,是指定在某个断点得到触发的时候,执行预先设置的指令,它的用法如下(假设1号断点位于某个objc_msgSend函数上):


(lldb) br com add 1

执行这条命令后,LLDB会要求我们设置一系列指令,以“DONE”结束,如下:


Enter your debugger command(s).  Type 'DONE' to end.
> po [$r0 class]
> p (char *)$r1
> c
> DONE

这里输入了3条指令,1号断点一旦触发,就会顺序执行它们,如下:


(lldb) c
Process 97048 resuming
__NSArrayM
(char *) $11 = 0x26c6bbc3 "count"
Process 97048 resuming
Command #3 'c' continued the target.

“br com add”命令一般用于自动观察某个断点被触发时其上下文的变化,找到进一步分析的线索,在本书的后半部分,将会看到它的使用场景。

3.print

LLDB的主要功能之一是“在程序停止的时候检查程序内部发生的事”,而这个功能正是通过“print”命令完成的,它可以打印某处的值。仍然以“-[SpringBoard_menuButtonDown:]”里的指令为例,演示它的一系列用法,如图4-18所示。

已知“MOVS R6,#0”的偏移后基地址为0xE37DE,在这条指令上下一个断点,待断点被触发后,看看当前R6的值,如下:

图4-18 [SpringBoard_menuButtonDown:]


(lldb) br s -a 0xE37DE
Breakpoint 2: where = SpringBoard`___lldb_unnamed_function299$$SpringBoard + 174, address = 0x000e37de
Process 99787 stopped
* thread #1: tid = 0x185cb, 0x000e37de SpringBoard`___lldb_unnamed_function299$$SpringBoard + 174, queue = 'com.apple.main-thread, stop reason = breakpoint 2.1
    frame #0: 0x000e37de SpringBoard`___lldb_unnamed_function299$$SpringBoard + 174
SpringBoard`___lldb_unnamed_function299$$SpringBoard + 174:
-> 0xe37de:  movs   r6, #0
   0xe37e0:  movt   r0, #75
   0xe37e4:  movs   r1, #1
   0xe37e6:  add    r0, pc
(lldb) p $r6
(unsigned int) $1 = 364526080

此条指令执行之后,R6应该被置0。输入“ni”执行此条指令,再次查看R6的值,如下:


(lldb) ni
Process 99787 stopped
* thread #1: tid = 0x185cb, 0x000e37e0 SpringBoard`___lldb_unnamed_function299 $$SpringBoard + 176, queue = 'com.apple.main-thread, stop reason = instruction step over
    frame #0: 0x000e37e0 SpringBoard`___lldb_unnamed_function299$$SpringBoard + 176
SpringBoard`___lldb_unnamed_function299$$SpringBoard + 176:
-> 0xe37e0:  movt   r0, #75
   0xe37e4:  movs   r1, #1
   0xe37e6:  add    r0, pc
   0xe37e8:  cmp    r5, #0
(lldb) p $r6
(unsigned int) $2 = 0
(lldb) c
Process 99787 resuming

可以看到,“p”命令将R6的值正确打印了出来。

在Objective-C中,[someObject someMethod]的底层实现,实际是objc_msgSend(someObject,someMethod),其中,前者是一个Objective-C对象,后者则可以强制转换成一个字符串(第6章将详细讲解这些内容)。在图4-19中,“BLX_objc_msgSend”执行了[SBTelephonyManager sharedTelephonyManaer]。

图4-19 还原objc_msgSend

已知“BLX_objc_msgSend”的偏移后地址是0xCC8A2,在上面下一个断点,待触发后打印出“objc_msgSend”的参数,如下:


(lldb) br s -a 0xCC8A2
Breakpoint 1: where = SpringBoard`___lldb_unnamed_function299$$SpringBoard + 370, address = 0x000cc8a2
Process 103706 stopped
* thread #1: tid = 0x1951a, 0x000cc8a2 SpringBoard`___lldb_unnamed_function299$$SpringBoard + 370, queue = 'com.apple.main-thread, stop reason = breakpoint 1.1
    frame #0: 0x000cc8a2 SpringBoard`___lldb_unnamed_function299$$SpringBoard + 370
SpringBoard`___lldb_unnamed_function299$$SpringBoard + 370:
-> 0xcc8a2:  blx    0x3e3798            ; symbol stub for: objc_msgSend
   0xcc8a6:  mov    r6, r0
   0xcc8a8:  movw   r0, #31088
   0xcc8ac:  movt   r0, #74
(lldb) po [$r0 class]
SBTelephonyManager
(lldb) po $r0
SBTelephonyManager
(lldb) p (char *)$r1
(char *) $2 = 0x0042eee6 "sharedTelephonyManager"
(lldb) c
Process 103706 resuming

可以看到,用“po”命令打印了Objective-C对象,用“p(char*)”通过强制转换的方式打印了C语言基本数据类型对象,简单明了。需要注意的是,当进程停在某一条“BL”指令上时,LLDB会自动解析这条指令,把指令中地址对应的符号注释出来,如上例中的


-> 0xcc8a2:  blx    0x3e3798                  ; symbol stub for: objc_msgSend

但是,LLDB的解析有时会出错,注释出的符号不对。这种情况下,请以IDA静态解析出的符号为准。

最后,可以用“x”命令打印一个地址处存放的值,如下:


(lldb) p/x $sp
(unsigned int) $4 = 0x006e838c
(lldb) x/10 $sp
0x006e838c: 0x00000000 0x22f2c975 0x00000000 0x00000000
0x006e839c: 0x26c6bf8c 0x0000000c 0x17a753c0 0x17a753c8
0x006e83ac: 0x000001c8 0x17a75200
(lldb) x/10 0x006e838c
0x006e838c: 0x00000000 0x22f2c975 0x00000000 0x00000000
0x006e839c: 0x26c6bf8c 0x0000000c 0x17a753c0 0x17a753c8
0x006e83ac: 0x000001c8 0x17a75200

上面用“p/x”以十六进制方式打印了SP,它是一个指针,值为0x6e838c。而“x/10”则打印出了这个指针指向的连续10个字(word)的数据。

4.nexti与stepi

“nexti”与“stepi”的作用都是执行下一条机器指令,它们最大的区别是前者不进入函数体,而后者会进入函数体。它们可分别简写为“ni”与“si”,是调试时使用最多的指令之一。你可能会问,“进入或者不进入函数体”,是什么意思?这里举个“-[SpringBoard_menuButtonDown:]”里的例子来说明,如图4-20所示。

“BL__SpringBoard__accessibilityObjectWithinProximity__0”的偏移后基地址是0xEE92E,它调用了_SpringBoard__accessibilityObjectWithinProximity__0函数。在它上面下断点,然后使用“ni”命令,如下:

图4-20 [SpringBoard_menuButtonDown:]


(lldb) br s -a 0xEE92E
Breakpoint 2: where = SpringBoard`___lldb_unnamed_function299$$SpringBoard + 510, address = 0x000ee92e
Process 731 stopped
* thread #1: tid = 0x02db, 0x000ee92e SpringBoard`___lldb_unnamed_function299$$SpringBoard + 510, queue = 'com.apple.main-thread, stop reason = breakpoint 2.1
    frame #0: 0x000ee92e SpringBoard`___lldb_unnamed_function299$$SpringBoard + 510
SpringBoard`___lldb_unnamed_function299$$SpringBoard + 510:
-> 0xee92e:  bl     0x2fd654                  ; ___lldb_unnamed_function16405$$SpringBoard
   0xee932:  tst.w  r0, #255
   0xee936:  beq    0xee942                   ; ___lldb_unnamed_function299$$SpringBoard + 530
   0xee938:  blx    0x403f08                  ; symbol stub for: BKSHIDServicesResetProximityCalibration
(lldb) ni
Process 731 stopped
* thread #1: tid = 0x02db, 0x000ee932 SpringBoard`___lldb_unnamed_function299$$SpringBoard + 514, queue = 'com.apple.main-thread, stop reason = instruction step over
    frame #0: 0x000ee932 SpringBoard`___lldb_unnamed_function299$$SpringBoard + 514
SpringBoard`___lldb_unnamed_function299$$SpringBoard + 514:
-> 0xee932:  tst.w  r0, #255
   0xee936:  beq    0xee942                   ; ___lldb_unnamed_function299$$SpringBoard + 530
   0xee938:  blx    0x403f08                  ; symbol stub for: BKSHIDServicesResetProximityCalibration
   0xee93c:  movs   r0, #0
 (lldb) c
Process 731 resuming

可见,“ni”没有进入_SpringBoard__accessibilityObjectWithinProximity__0函数体。再来看看“si”,如下:


Process 731 stopped
* thread #1: tid = 0x02db, 0x000ee92e SpringBoard`___lldb_unnamed_function299$$SpringBoard + 510, queue = 'com.apple.main-thread, stop reason = breakpoint 2.1
    frame #0: 0x000ee92e SpringBoard`___lldb_unnamed_function299$$SpringBoard + 510
SpringBoard`___lldb_unnamed_function299$$SpringBoard + 510:
-> 0xee92e:  bl     0x2fd654                  ; ___lldb_unnamed_function16405$$SpringBoard
   0xee932:  tst.w  r0, #255
   0xee936:  beq    0xee942                   ; ___lldb_unnamed_function299$$SpringBoard + 530
   0xee938:  blx    0x403f08                  ; symbol stub for: BKSHIDServicesResetProximityCalibration
(lldb) si
Process 731 stopped
* thread #1: tid = 0x02db, 0x002fd654 SpringBoard`___lldb_unnamed_function16405$$SpringBoard, queue = 'com.apple.main-thread, stop reason = instruction step into
    frame #0: 0x002fd654 SpringBoard`___lldb_unnamed_function16405$$SpringBoard
SpringBoard`___lldb_unnamed_function16405$$SpringBoard:
-> 0x2fd654:  movw   r0, #33920
   0x2fd658:  movt   r0, #43
   0x2fd65c:  add    r0, pc
   0x2fd65e:  ldrsb.w r0, [r0]
(lldb) c
Process 731 resuming

“movw r0,#33920”的偏移前基地址是0x226654,在IDA中如图4-21所示。

图4-21 SpringBoard__accessibilityObjectWithinProximity__0

其位于_SpringBoard__accessibilityObjectWithinProximity__0函数内部,即“si”命令进入了函数体,这就是“进入或者不进入函数体”的意思。

5.register write

“register write”命令用于给指定的寄存器赋值,从而“对程序进行改动,观察程序的执行过程有什么变化”。在图4-22所示的代码中,已知“TST.W R0,#0xFF”的偏移后基地址是0xEE7A2,如果R0的值是0,进程会走左边的分支,否则会走右边的分支。

图4-22 分支

这里下个断点看看这里R0的值是多少,如下:


(lldb) br s -a 0xEE7A2
Breakpoint 3: where = SpringBoard`___lldb_unnamed_function299$$SpringBoard + 114, address = 0x000ee7a2
Process 731 stopped
* thread #1: tid = 0x02db, 0x000ee7a2 SpringBoard`___lldb_unnamed_function299$$SpringBoard + 114, queue = 'com.apple.main-thread, stop reason = breakpoint 3.1
    frame #0: 0x000ee7a2 SpringBoard`___lldb_unnamed_function299$$SpringBoard + 114
SpringBoard`___lldb_unnamed_function299$$SpringBoard + 114:
-> 0xee7a2:  tst.w  r0, #255
   0xee7a6:  bne    0xee7b2                   ; ___lldb_unnamed_function299$$SpringBoard + 130
   0xee7a8:  bl     0x10d340                  ; ___lldb_unnamed_function1110$$SpringBoard
   0xee7ac:  tst.w  r0, #255
(lldb) p $r0
(unsigned int) $0 = 0

由于R0的值是0,因此在BNE的作用下,它会走左边的分支,如下:


(lldb) ni
Process 731 stopped
* thread #1: tid = 0x02db, 0x000ee7a6 SpringBoard`___lldb_unnamed_function299$$SpringBoard + 118, queue = 'com.apple.main-thread, stop reason = instruction step over
    frame #0: 0x000ee7a6 SpringBoard`___lldb_unnamed_function299$$SpringBoard + 118
SpringBoard`___lldb_unnamed_function299$$SpringBoard + 118:
-> 0xee7a6:  bne    0xee7b2                   ; ___lldb_unnamed_function299$$SpringBoard + 130
   0xee7a8:  bl     0x10d340                  ; ___lldb_unnamed_function1110$$SpringBoard
   0xee7ac:  tst.w  r0, #255
   0xee7b0:  beq    0xee7da                   ; ___lldb_unnamed_function299$$SpringBoard + 170
(lldb) ni
Process 731 stopped
* thread #1: tid = 0x02db, 0x000ee7a8 SpringBoard`___lldb_unnamed_function299$$SpringBoard + 120, queue = 'com.apple.main-thread, stop reason = instruction step over
    frame #0: 0x000ee7a8 SpringBoard`___lldb_unnamed_function299$$SpringBoard + 120
SpringBoard`___lldb_unnamed_function299$$SpringBoard + 120:
-> 0xee7a8:  bl     0x10d340                  ; ___lldb_unnamed_function1110$$SpringBoard
   0xee7ac:  tst.w  r0, #255
   0xee7b0:  beq    0xee7da                   ; ___lldb_unnamed_function299$$SpringBoard + 170
   0xee7b2:  movw   r0, #2174

再次触发断点,通过“register write”命令更改R0的值为1,看看它会走哪个分支,如下:


Process 731 stopped
* thread #1: tid = 0x02db, 0x000ee7a2 SpringBoard`___lldb_unnamed_function299$$SpringBoard + 114, queue = 'com.apple.main-thread, stop reason = breakpoint 3.1
    frame #0: 0x000ee7a2 SpringBoard`___lldb_unnamed_function299$$SpringBoard + 114
SpringBoard`___lldb_unnamed_function299$$SpringBoard + 114:
-> 0xee7a2:  tst.w  r0, #255
   0xee7a6:  bne    0xee7b2                   ; ___lldb_unnamed_function299$$SpringBoard + 130
   0xee7a8:  bl     0x10d340                  ; ___lldb_unnamed_function1110$$SpringBoard
   0xee7ac:  tst.w  r0, #255
(lldb) p $r0
(unsigned int) $5 = 0
(lldb) register write r0 1
(lldb) p $r0
(unsigned int) $6 = 1
(lldb) ni
Process 731 stopped
* thread #1: tid = 0x02db, 0x000ee7a6 SpringBoard`___lldb_unnamed_function299$$SpringBoard + 118, queue = 'com.apple.main-thread, stop reason = instruction step over
    frame #0: 0x000ee7a6 SpringBoard`___lldb_unnamed_function299$$SpringBoard + 118
SpringBoard`___lldb_unnamed_function299$$SpringBoard + 118:
-> 0xee7a6:  bne    0xee7b2                   ; ___lldb_unnamed_function299$$SpringBoard + 130
   0xee7a8:  bl     0x10d340                  ; ___lldb_unnamed_function1110$$SpringBoard
   0xee7ac:  tst.w  r0, #255
   0xee7b0:  beq    0xee7da                   ; ___lldb_unnamed_function299$$SpringBoard + 170
(lldb) 
Process 731 stopped
* thread #1: tid = 0x02db, 0x000ee7b2 SpringBoard`___lldb_unnamed_function299$$SpringBoard + 130, queue = 'com.apple.main-thread, stop reason = instruction step over
    frame #0: 0x000ee7b2 SpringBoard`___lldb_unnamed_function299$$SpringBoard + 130
SpringBoard`___lldb_unnamed_function299$$SpringBoard + 130:
-> 0xee7b2:  movw   r0, #2174
   0xee7b6:  movt   r0, #63
   0xee7ba:  add    r0, pc
   0xee7bc:  ldr    r0, [r0]

此时,进程改道右边的分支了。

LLDB的命令还有很多种,这里只列举了iOS逆向工程初期最常用的五种,希望读者能够窥一斑而见全豹,感受到LLDB的强大威力。LLDB仍处在开发阶段,除了几个官方网站,还未见成熟的教程;LLDB脱胎于GDB,虽然两者的命令有差别,但用法和思路是一脉相承的。要想完整地熟悉LLDB的使用,推荐阅读“Peter’s GDB tutorial”和“RMS’s gdb Debugger Tutorial”(Google一下)。IDA宜静,LLDB宜动,熟练地使用这两个工具是成为逆向高手的必经之路。

4.3.6 LLDB使用小提示

1.调试的二进制文件必须从iOS中提取

IDA分析的二进制文件必须与LLDB调试的二进制文件相同,这样偏移前基地址、ASLR偏移、偏移后基地址才能对应得上。IDA分析的二进制文件可以通过第3章介绍的dyld_decache工具从本机获取;从其他渠道(如SDK、模拟器等)提取的文件一般不能用作动态调试。

2.LLDB中的简化输入

在使用LLDB时,如果想重复执行上一条指令,直接按回车键就可以了;如果想查看以前执行过的指令,按方向键的向上和向下键就可以了。

LLDB的命令都很简单,但怎么用简单的命令去解决复杂的问题,却不简单。在第6章还会列举一些LLDB的常用场景,但在那之前,请大家务必掌握本节的知识。