学习背景:在非越狱逆向开发的过程中,我们可以从界面(Reveal)、类与方法(class-dump)、或者网络(Charles)等方面及一个应用的功能点入手去寻找一些需要实现功能的入口线索。但是在未知源代码的情况下,想要精确定位一个功能点的实现位置及实现原理,就需要结合静态分析和动态调试来分析程序运行的动态和行为实现。
静态分析
静态分析是指在程序没有运行的情况下进行程序分析的一种方法,一般分为以下三种:
- 基于ipa和app包静态分析
- 基于文件格式的静态分析
- 基于二进制反汇编的静态分析
在二进制反汇编静态分析的过程中,我们可以两种反汇编的工具,一个是Hopper,一个是IDA。两者的功能都非常强大,可以显示被分析文件的反汇编代码、流程图及伪代码。其中常用的功能有字符串搜索,跳转地址,变量重命名等。
动态调试
动态调试和静态分析是相辅相成的。静态分析只能分析静态的函数内部执行。要想动态获取程序在运行时的参数传递、执行流程及寄存器内存等信息,就需要使用动态调试的方法。
LLDB动态调试
我们都知道,LLDB是Xcode自带的调试工具,既可以在本地调试Mac应用程序,也可以远程调试iPhone真机。我们平时在开发中调试程序接口和运行状态时也会经常使用。
其实原理是这样的:当使用Xcode调试真机App时,Xcode会将debugserver文件复制到手机中,以便在真机上启动一个服务,等待Xcode进行远程连接调试。所以只有当设备连接计算机真机调试App后,debugserver文件才会安装到设备的/Developer/usr/bin
目录下。但是,debugserver文件默认只能调试自己开发的应用,在调试从App Store下载应用时会出现"unable to start the exception thread"错误。
LLDB调试环境设置
不过既然知道了原理就可以试着去对debugserver去做一些权限处理就可以了,下面是具体的步骤:
1.准备工作
为了调试其他应用,需要给debugserver文件富裕task_for_pid权限。在调试前需要做如下准备工作。
(1)复制debugserver文件
使用scp
命令将debugserver文件从手机复制到Mac计算机上。
scp -P 2222 root@localhost:/Developer/usr/bin/debugserver ./
(2)签名权限
新建一个entitlement.plist文件,在其中写入如下内容:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.springboard.debugapplications</key> <ture/>
<key>run-unsigned-code</key>
<ture/>
<key>get-task-allow</key>
<ture/>
<key>task_for_pid-allow</key>
<ture/>
</dict>
</plist>
再使用codesign进行签名,命令如下。
codesign -s - --entitlements entitlements.plist -f debugserver
(3)将debugserver文件复制回手机
因为/Developer/
是系统的只读文件夹,所以也可以把签好权限的debugserver文件复制到bin/
目录下:
scp -P 2222 ./debugserver root@localhost:/usr/bin/debugserver
2.进行调试
准备好有task_for_pid权限的debugserver文件后,就可以调试第三方应用了。debugserver支持通过进程名和进程ID进行调试。通过运行ps aux
找到启动的进程后,执行命令:
debugserver *:1234 -a SogouInput
命令执行完成之后,被调试的进程会被卡住。接下来,需要通过Mac远程连接目标的监听服务,打开Terminal,输入lldb调试启动后,同过以下命令连接debugserver启动的服务:
-> ~ lldb
(lldb) process connect connect://10.129.17.89:1234
//10.129.17.89:手机的IP地址
//1234:对应服务的端口号,可通过iproxy进行端口转发
到这儿为止就可以开始动态调试第三方应用的功能了!为了能够更好的进行lldb调试,下面便来介绍一下LLDB调试的指令集:
LLDB调试指令集
(1)断点设置
//1、设置方法名断点
(lldb) breakpoint/b set -n xxx //xxx:方法名
//2、根据对应的类设置该类的方法名断点(可多个:一组)
(lldb) breakpoint/b set -n "-[Class1 xxx]" -n "-[Class2 xxx]" //xxx:方法名 Class:类名
如果我们想给整个工程中的同一个Selector都打断点呢,这个在做逆向时会很有用,便于跟踪一个Seletor的功能:
//3、根据selecor打上工程中所有的同selector断点
(lldb) breakpoint/b set --selector xxx" //xxx:方法名
可以发现有66个地方存在这个selector(touchesBegan:withEvent:)。
//4、根据selecor打上指定文件中所有的同selector断点
(lldb) breakpoint/b set --file xxx.m--selector xxx" //xxx.m:指定文件
//5、给遍历整个项目中满足xxx这个字符的所有方法打断点
(lldb) breakpoint/b set -r xxx" //xxx:满足字符
//6、列出当前程序中存在的所有断点(按组分)
(lldb) breakpoint/b list"
//7、禁用或者启用一组或某个断点
(lldb) breakpoint enable/disable 3/3.1"
//8、删除一组或某个断点
(lldb) breakpoint delete 3/3.1"
(2)添加执行代码
说到执行代码,其中最常用的就是:(lldb) p
和(lldb) po
用来打印一些运行时的变量信息了,不过po和p指令不是一般理解的print的缩写,而是expression的意思,我们可以通过(lldb) help p
来查看解释,如下:
po也可以通过如下(lldb)help expression
来查看解释:
//1、查看变量的信息
(lldb) expression/p xxx //xxx:变量名
//2、添加代码行
(lldb) expression/p xxx; //xxx:代码行
同时还可以添加多行代码,需要通过Option+Enter
键来进行换行操作。
//3、添加多行代码
(lldb) expression/p xxx;//xxx:代码行
xxx;
xxx;
以上如果麻烦的话,可以通过下面的指令直接给breakpoint添加代码
//4、提前给断点添加代码
(lldb) breakpoint/b command add x //x:断点编号
这样就在运行的过程中注入一些调试代码了!
(3)查看堆栈信息
//1、查看调用栈
(lldb) backtrack/bt
//2、调用栈回退和前进
(lldb) up/down
同时我们也可以通过以下指令直接定位到任意调用栈函数:
//3、直接查看对应序列号调用栈方法
(lldb) frame select x //x:序列号
//4、可查看定位到的调用栈方法参数值
(lldb) frame variable
注:以上栈的回退和定位只是查看调用的方法,而不能做出修改,因为调用栈方法方法已经执行
(4)流程控制
//1、继续执行
(lldb) continue/c
//2、单步运行,将子函数当做整体一步运行
(lldb) next/n
//3、单步运行,遇到子函数会进入
(lldb) stepi/s //step-in
其实,这些流程控制的指令就是Xcode上面控制台的几个可视化按钮,其功能是一样的,同样可以结合Ctrl
和Option
键扩展功能。
(5)内存断点
//1、设置查看成员变量的内存断点
(lldb) watchpoint set variable xxx //xxx:成员变量
之后可以通过p指令去查看地址对应的值,包括oldValue和newValue。
不过如果我们在做逆向分析,将不能获取到成员变量,还可以通过内存地址来设置内存断点:
//2、通过内存地址设置查看成员变量的内存断点
(lldb) watchpoint set expression xxx //xxx:内存地址
其他一些常用的副指令跟breakpoint很相似,可以去参考。
(6)stop-hook
//1、每次stop的时候执行代码,只对breakpoint、watchpoint有用
(lldb) target stop-hook add -o "xxx" //xxx:代码行
如图可以看到在每个断点处都打印了方法的相关信息和参数信息。
(7)image
如果我们在真机调试的时候遇到崩溃,就不能够向自己程序调试的时候看到的具体调用函数,这样的话我们可以借助image来解析对应call stack中的内存地址来定位函数:
//1、First throw call stack:(xxx,xxx,xxx)
(lldb) image lookup -a xxx //xxx:崩溃backtrace地址
如图可以由内存地址定位到数组崩溃问题。
// 2、查看类名的头文件
(lldb) image lookup -t xxx //xxx:类名
//3、 image是项目中包含的的库,可以看到每个库对应的内存地址
(lldb) image list
(8)其他
//1、读取寄存器信息
(lldb) register read
// 2、读取内存信息
(lldb) memory read
LLDB导入Python脚本
LLDB提供了很多API来帮助我们获取和修改调试信息、进程信息及内存信息。这些接口可以直接通过Python脚本调用,然后透过LLDB加载使用。我们可以阅读LLDB官方提供的API文档http://lldb.llvm.org/python_reference/index.html。下面有两个开源库很实用。
- chisel:Facebook开源的LLDB命令工具(http://github.com/facebook/chisel)
- LLDB:Derek Selander开源的工具。包含了一些高级调试技巧(http://github.com/DerekSelander/LLDB)
LLDB初始化文件路径/Users/xxx/.lldbinit xxx:Mac用户名
(此文件为隐藏文件,如果没有可以自行创建),在这个文件中可以导入一些配置文件,比如上面说的一些开源库。
//创建lldbinit文件
touch ~/.lldbinit
//安装开源库
brew install chisel
//在~/.lldbinit文件中导入下载的脚本
文件 ~/.lldbinit
···
command script import /Users/xxx/Documents/iosreversecode/LLDB/chisel/fblldb.py
//加载脚本
(lldb) command source ~/.lldbinit
这样我们就可以进行一些高级调试了,比如输入以下指令,就可以实现许多类似于Cycript的功能了,还是对逆向很有帮助。
(lldb) expression @import UIKit
//打印App层级结构
(lldb) pviews
//查看控件的响应链
(lldb) presponder xxx //xxx:控件内存地址
//搜索存在所有的控件类对象
(lldb) search xxx //xxx:控件类
总结
LLDB功能非常强大,这里只是介绍了一些简单常用的指令,还可以进一步去探索。比如类似于使用dumpdecrypted砸壳的解密附加功能;同时我们还可以通过对应的动态调试的内存地址结合静态分析的IDA或者Hopper来静态分析代码,这样一来对逆向分析来说就有了一些很有力的工具。