如果说IDA是倚天剑,那么LLDB就是屠龙刀,两者在iOS逆向工程中的地位不相上下,难分伯仲。LLDB全称为“Low Level Debugger”,是由苹果出品,内置于Xcode中的动态调试工具,不但通吃C、C++、Objective-C,还全盘支持OSX、iOS,以及iOS模拟器。LLDB的功能可以概括为以下四点:
·在指定的条件下启动程序;
·在指定的条件下停止程序;
·在程序停止的时候检查程序内部发生的事;
·在程序停止的时候对程序进行改动,观察程序的执行过程有什么变化。
LLDB没有图形界面,使用时看着Terminal中黑压压的一片文字,初学者很容易被吓跑,但是一旦掌握其基本用法,配合IDA双管齐下,就能解决很多难倒大片初学者的问题,投资回报极高。LLDB是运行在OSX中的,要想调试iOS,还需要另一个工具的配合,它就是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,发挥它们在逆向工程中的真正威力。
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。
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上完成的。
在了解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宜动,熟练地使用这两个工具是成为逆向高手的必经之路。
1.调试的二进制文件必须从iOS中提取
IDA分析的二进制文件必须与LLDB调试的二进制文件相同,这样偏移前基地址、ASLR偏移、偏移后基地址才能对应得上。IDA分析的二进制文件可以通过第3章介绍的dyld_decache工具从本机获取;从其他渠道(如SDK、模拟器等)提取的文件一般不能用作动态调试。
2.LLDB中的简化输入
在使用LLDB时,如果想重复执行上一条指令,直接按回车键就可以了;如果想查看以前执行过的指令,按方向键的向上和向下键就可以了。
LLDB的命令都很简单,但怎么用简单的命令去解决复杂的问题,却不简单。在第6章还会列举一些LLDB的常用场景,但在那之前,请大家务必掌握本节的知识。