iOS逆向指南:静态分析

静态分析是指对二进制包进行反编译,分析静态的代码逻辑。

本文内容包括:app 砸壳过程、工具和环境的坑、导出 OC 头文件、使用 hopper 和 IDA 反编译、arm 寄存器功能、静态分析经验、推荐的 IDA 插件、如何分析系统库。

对 app 砸壳解密

从 App Store 下载的 app 是经过加密的,需要对其进行解密后,才能进行分析。如果你懒得砸壳,可以直接去各种苹果助手下载越狱版 app,那些是已经解密过的。但是如果要找的 app 在助手上没有,就只能自己砸壳了。

砸壳可以使用 dumpdecrypted,也可以使用更简单的 clutch。这里用 dumpdecrypted 讲解。步骤如下。

1.下载 dumpdecrypted

从https://github.com/AloneMonkey/dumpdecrypted下载源码,编译出一个 dumpdecrypted.dylib 文件。这个版本的 dumpdecrypted 添加了对 framework 的 dump。

2.安装 openSSH

iOS 9及以下系统,在 Cydia 里安装 openSSH 即可。

iOS 10越狱自带了 openSSH,但是默认是关闭的,需要做一点修改。

如果是用的 yalu 越狱:

  • 1.用苹果助手或者其他工具进入 iOS 的/private/var/containers/Bundle/Application/yalu102/yalu102.app/。
  • 2.用文本编辑器打开 dropbear.plist 文件。
  • 3.替换 127.0.0.1:22 为 22。
  • 4.重启设备,重新使用越狱工具恢复越狱。

参考:http://bbs.iosre.com/t/make-package-ssh-ios10-2/7564

或者直接去 Cydia 里安装 dropbear 插件。

3.连接到 iOS 设备

iOS 设备安装了 openSSH 后,在 Mac 端打开终端,确保 Mac 和 iOS 设备连接到同一网络,在终端里输入命令:ssh root@iOSIP。iOS 设备的 ip 地址:

iOS IP

在终端中输入命令:ssh root@10.5.53.182,回车,接着输入 ssh 的默认密码alpine后即可连接到 iOS 设备。

4.找到需要砸壳的 app

找到 app 所在目录,格式为/var/mobile/Containers/Data/Application/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/,可以使用同步助手、itools 等工具查找。

也可以在 Cydia 里安装 ps 命令行工具后,使用ps –e命令查找,方法是 ssh 成功后,关闭所有 app,打开需要砸壳的 app,输入ps –e命令,即可打印出所有进程,/var/mobile开头的那个目录就是 app 所在的目录。

dump path

5.进行砸壳

下面的砸壳是旧版 dumpdecrypted 的方法,比较繁琐。AloneMonkey 的 这个 https://github.com/AloneMonkey/dumpdecrypted 更加简单。

  • 把dumpdecrypted.dylib拷贝到/usr/lib。 iOS 9之前是拷贝到 app 的 Document 目录的, iOS 9 之后出现了权限问题,所以拷贝到/usr/lib
  • 修改 user 为mobile:su mobile
  • 进入到某个具有写权限的目录,例如cd /var/mobile/Documents
  • 使用DYLD_INSERT_LIBRARIES加载动态库到 app 上,格式为DYLD_INSERT_LIBRARIES='dumpdecrypted.dylib的目录' '需要砸壳的app执行文件的目录',例如:DYLD_INSERT_LIBRARIES=/usr/lib/dumpdecrypted.dylib /var/mobile/Applications/F7753B03-3F06-4524-A735-5BF5B398C730/WeChat.app/WeChat。这是系统的 dyld 提供的加载动态库的功能,可以在 dyld 源代码中看到这部分逻辑。

如果出现dyld: could not load inserted library 'dumpdecrypted.dylib' because no suitable image found. Did find: dumpdecrypted.dylib: required code signature missing for 'dumpdecrypted.dylib'
,需要对 dumpdecrypted.dylib 进行签名。

在 Mac 上列出证书:security find-identity -v -p codesigning,用列出的证书签名:
codesign --force --verify --verbose --sign "iPhone Developer: xxx xxxx (xxxxxxxxxx)" dumpdecrypted.dylib。把签名后的dumpdecrypted.dylib重新拷到 iOS 设备上,重新进行砸壳。

砸壳完毕后,在当前目录会生成一个.decrypted后缀的文件,这就是砸壳后的文件,将其拷贝到 Mac 上即可导入其头文件、用反编译工具打开分析。可以在 Mac 上使用 scp 命令拷贝越狱机上的文件:scp -P 端口号(默认22) root@iOSIP:/var/mobile/Documents/xxx.decrypted ~/Documents/xxx.decrypted。如果拷贝的是文件夹,加上-r参数。

dumpdecrypted原理是 app 启动后会被系统解密,因此可以把解密后的内存 dump 出来。但是如果要对 app extension 进行砸壳,由于 extension 是依赖于主 app 的,不能独立启动,所以砸壳方法就失效了。可以参考这个改进版对 extension 砸壳的方法:https://github.com/CarinaTT/dumpdecrypted

使用 class-dump 导出 app 的头文件

Class-dump 是一个可以导出 Objective-C 头文件的工具,官网:http://stevenygard.com。

通过分析头文件里的 API,可以简单地分析一个类的实现,或者查找一些私有 API。

class-dump 官网上的版本不能导出用 swift 编写的工程的头文件,当出现Error: Cannot find offset for address 0x3a546a04 in dataOffsetForAddress:这样的错误时,就说明这个 app 可能是用 swift 编写的。

建议去 github 上手动编译最新版的 class-dump,或者使用 class-dump-z 代替,下载地址:https://code.google.com/archive/p/networkpx/downloads。

把下载到的class-dump-z执行文件放到/usr/local/bin/,赋予执行权限chmod +x /usr/local/bin/class-dump-z。这样就可以在终端使用 class-dump 命令了:class-dump-z –H '需要导出头文件的app目录' –o '导出头文件的存放目录'。

例如要 dump 系统自带的计算器,导出它的头文件,命令如下:
class-dump-z -H /Applications/Calculator.app -o ~/Documents/headers。

拿到砸壳后的 .decrypted 文件后,直接使用class-dump-z即可导出头文件。

此时,使用之前 reveal 定位到的类名,即可找到对应的文件,查看类里面的方法。

class-dump

可以看到,在扫一扫界面,微信使用了- (void)captureOutput: didOutputSampleBuffer: fromConnection:这个方法,说明它是截取了视频流的帧图像,再对图像进行二维码分析,而不是用AVFoundiation提供的二维码识别方法。

如果还想进一步查看方法的逻辑,可以使用Hopper Disassembler对 .decrypted 文件进行反编译。

使用使用 Hopper Disassembler 静态分析

一个专门反编译 OC 程序的工具。官网:http://www.hopperapp.com。试用版有功能限制,30分钟退出一次,不能保存和导入反编译后的文件,不能动态调试等。

打开 Hopper Disassembler,直接将 .decrypted 文件拖入,选择对应的 CPU 架构类型即可,例如这个.decrypted 是从 iPad mini2 上生成的,那么就是 arm64。

打开后会自动进行分析,列出方法名、字符串等信息,但是大多数都是汇编语言。阅读汇编语言,还需要了解对应架构寄存器功能的知识。

在左侧可以搜索类名,方法名。

hopper1

右侧的 is referenced by 和 have reference to 可以看到方法之间的的交叉引用关系:

hopper2

按空格键可以弹出方法的逻辑跳转图:

hopper3

Hopper Disassembler 可以将汇编语言转换为 OC 风格的伪代码,但是旧版的 hopper 不能对 arm64 文件使用这个功能。建议使用 armv7s 以下的 iOS 设备的原因就在这里。以下是使用 iPad2 越狱设备反编译后,生成的汇编代码和对应的伪代码,由于微信的代码比较复杂,这里选用的是另外一个更简单的二维码 app 的代码:

hopper4

hopper5

可以看到aptureOutput: didOutputSampleBuffer: fromConnection:里,首先用取到的帧生成了一张图片,再用createRotatedImage:degrees:对图片做了一次处理,最后用decodeImage:cgimg:对图片进行二维码分析。要想查看这些方法,只需要再搜索对应的方法名就可以了。最新版 hopper 也可以双击直接跳转。

另外一个反编译工具 IDA 也可以反编译 armv7 的 app ,使用方法类似,可以和 Hopper Disassembler 对照着看。需要注意的是 IDA 的 Pro 版才支持 arm64 的 app,而 Pro 版不支持免费试用。

静态分析经验总结

追踪调用流程

  • 对于静态函数,直接用交叉引用功能is referenced by查看函数在哪里被引用。注意 hopper 面板里列出的引用不是完整的,可以用快捷键x列出完整的引用
  • 对于 OC 方法,由于 runtime 在调用时不是直接引用方法,而是引用了 selector,所以需要搜索方法名字符串和 selector,然后再用is referenced by查找哪些地址引用了此字符串或者 selector,来查找方法调用
  • 通过寄存器的赋值操作回溯参数的传递
  • 通过查找某些关键字符串,回溯到关键函数

注意,反汇编工具有时候会分析出错误的指令,所以有些函数体是丢失的,需要在反编译时手动 undefined。

分析汇编代码

  • 使用 hopper 的伪代码转换功能,可以将 OC 方法的汇编代码转换为 OC 风格的伪代码。此功能对 arm64 的支持不是很好,建议使用 armv7 或者 armv7s 的越狱机
  • 在函数的开始,32 位 arm 上前四个参数存放在 r0-r3 中,其他参数存放在栈中,结束后,返回值放在 r0 中;在 arm64 上,前7个参数存放在 x0–x7 中,返回值存在 x0 中
  • 有些代码是被开发者故意混效过的,例如打乱执行流程、加入冗余代码,可以借助一些 IDA 插件处理后再分析,例如 CrowdDetox、optimice python plugin,不过只是分析 iOS 的话,很少会遇到这种情况

基本的汇编知识

你并不需要花时间理解每一条汇编指令,只需要梳理出关键点就能理清代码的逻辑。

逆向中关键的指令:

  • ldr,mov,读取指令,从地址读取数据到寄存器。
  • str,保存指令,保存数据到寄存器。
  • b,跳转指令,跳转到某个地址。
  • cmp,比较指令,说明这里有分支。

32 位 arm 的调用约定:

寄存器 描述
r0-r3 传递参数与返回值。如果断点在 OC 方法的第一行,那 r0 就是 self,r1 就是 cmd。如果超过四个参数,或者一些例如结构体的参数超过了32位 bit,那么参数将会通过栈来传递;返回值一般都在 r0 上
r4-r6, r8, r10-r11 没有特殊规定,通用寄存器
r7 栈帧指针寄存器(Frame Pointer),指向前一个保存的栈帧(stack frame)和链接寄存器(link register, lr)在栈上的地址
r9 操作系统保留
r12 IP 寄存器(intra-procedure scratch)
r13 SP 寄存器(stack pointer),是栈顶指针
r14 LR 寄存器(link register),存放函数返回后需要继续执行的指令地址
r15 PC 寄存器(program counter),指向当前指令地址
CPSR 当前程序状态寄存器(Current Program State Register),在用户状态下存放像 condition 标志中断禁用等标志

arm64 的调用约定:

arm64有 r0 - r30 是31个通用整形寄存器,PC 不能再作为寄存器直接访问。每个寄存器可以存取一个64位大小的数。 当使用 x0 - x30 访问时,它就是一个64位的数。当使用 w0 - w30 访问时,访问的是这些寄存器的低32位。

寄存器 描述
x0–x7 传递参数与返回值。如果参数个数超过了8个,多余的参数会存在栈上;返回值一般都在 x0 上
x29 栈帧指针寄存器(Frame Pointer),指向前一个保存的栈帧(stack frame)和链接寄存器(link register, lr)在栈上的地址
x31 SP 寄存器(stack pointer),是栈顶指针;根据不同指令,也有可能是 zero register
x30 LR 寄存器(link register),存放函数的返回地址
CPSR 当前程序状态寄存器(Current Program State Register),在用户状态下存放像 condition 标志中断禁用等标志

x86-64 的调用约定:

x86-64 有16个64位寄存器,分别是:

rax,rbx,rcx,rdx,esi,edi,rbp,rsp,r8,r9,r10,r11,r12,r13,r14,r15

寄存器 描述
rax 作为函数返回值使用
rsp 栈指针寄存器,指向栈顶
rdi,rsi,rdx,rcx,r8,r9 依次用作函数参数;如果断点在 OC 方法的第一行,那 rdi 就是 self,rsi 就是 cmd
rbx,rbp,r10,r11,r12,r13,r14,r15 通用寄存器

栈帧相关的知识,可以参考:iOS开发同学的arm64汇编入门

汇编指令速查插件

有许多很有用的插件可以对静态分析提供帮助。

有时候看到不了解的汇编指令,每次都去 Google 查找,是一件很低效的事。可以安装插件,直接在 hopper 和 IDA 中显示指令的功能。

Hopper 插件:hopperref

Hopper 可以使用 Python 编写的扩展插件。安装插件hopperref,把Show Instruction Reference.py``arm.sql``x86-64.sql拷贝到~/Library/Application Support/Hopper/Scripts/目录下即可。之后就能在 hopper 界面的菜单栏Scripts中找到Show Instruction Reference选项,点击即可输出选中指令的详细文档。

mov指令的文档:

hopperref

IDA 插件:idaref

hopperref 插件是源自 一个 IDA 的插件 idaref。

把idaref.py拷贝到your_ida_path/ida.app/Contents/MacOS/plugins/下,把archs文件夹拷贝到your_ida_path/ida.app/Contents/MacOS/plugins/archs。archs文件夹里是汇编指令的文档x86-64.sql``x86-64_old.sql``arm.sql``mips32.sql``xtensa.sql。

之后打开 IDA,就可以在Edit菜单中多出了idaref选项,选择Start Idaref就开启了自动提示,

idaref

当选中汇编指令时,对应的文档就会显示在Instruction Reference窗口中。

idaref output

IDA 插件:FRIEND

除了 idaref,还有另一个插件 FRIEND 也提供了汇编指令和寄存器的文档功能。只要把鼠标停在指令或者寄存器上就会显示文档悬浮窗。

需要注意的是,编译出来的 IDA dylib 插件是对应 IDA 版本的,如果要使用不同版本的 IDA,就需要重新编译。把对应版本的FRIEND.dylib和FRIEND64.dylib拷贝到your_ida_path/ida.app/Contents/MacOS/plugins/下,再打开 IDA 就会在Edit->Plugins中多出FRIEND选项。

ida FRIEND

点击选项,打开 FRIEND 的设置。需要加载 FRIEND 提供的 XML 配置文件,对应二进制文件的 x86_64 或者 arm 平台。例如x86_64.xml配置中提供了x86_64 instructions项,选中后,勾上下面的四个功能选项,点击 OK 保存。

ida FRIEND settings

之后,当鼠标停在指令或者寄存器上就会显示文档悬浮窗。

ida FRIEND instructions

识别库函数

很多时候,二进制文件中的函数都被去掉了符号,因此只能看到很多sub_100017D90这样的函数,难以直观分析。而程序会使用到很多第三方库,例如加密库、压缩库、网络库,这些第三方库一般都是开源的,可以得到函数符号,如果能恢复这部分函数的符号,就能避免浪费时间在分析这些开源代码上,也能通过分析开源库的交叉引用,追踪程序自身的逻辑。

这部分代码一般都是 C 和 C++ 函数,OC 方法的名字都保存在 Mach-O 文件的符号表中,不会被去除符号。如果你需要分析 C++ 程序,可以使用下面的工具进行辅助。

FLIRT:库快速识别和鉴定技术

IDA 提供了FLIRT Signature功能,FLIRT 全称是库快速识别和鉴定技术,可以为带有符号的库文件中的函数生成签名,再把签名文件导入到分析后的 app 中,就会识别出匹配到的函数,重命名为正确的符号。

但是生成正确的签名并不容易。用于生成签名的库文件,编译时的编译器版本、配置和 app 中用到的库的编译器版本、配置需要相同。这样才能生成相同的代码,从而生成相同的代码签名。

具体的使用方法,可以在书籍IDA Pro 权威指南中找到。

识别加密函数

类似的,有些 IDA 插件可以识别程序中用到的加密常数、加密方法和压缩方法。例如 Find Crypt 可以寻找常用加密算法中的常数,IDA signsrch 可以寻找二进制文件所使用的加密、压缩算法,IDA scope 可以自动识别 windows 函数和压缩、加密算法。

可以从这些关键函数入手,寻找程序中的关键逻辑。

如何分析系统库

有时候在分析某个 crash 时,或者对某个系统功能感兴趣时,会需要分析特定版本的 iOS 系统库的实现,例如UIKit.framework Foundiation.framework。

绝大部分时候,只需要分析模拟器版本的系统库就可以了。因为模拟器的系统库保留了所有的符号,查找交叉引用更直接。

不过有些系统库只在真机上才有,或者你需要特定版本的库用于分析 crash 时,可以从这里下载对应的系统库。

真机的系统库和模拟器的有些差别。系统库在真机上经过了很多编译优化,去除了大部分私有的函数符号,交叉引用也不像模拟器版本的那样直接。真机上的所有系统 framework 都被整合成了一个大文件,名为dyld_shared_cache_arm64或者dyld_shared_cache_armv7。函数在寻址时,是基于整个dyld_shared_cache_xxx文件进行寻址的。

当你把真机连接到 Xcode,Xcode 会把真机上的系统库拷贝到~/Library/Developer/Xcode/iOS DeviceSupport,从dyld_shared_cache_xxx中切分出每个单独的 framework。但是当你反编译这些 framework 时,会发现代码里会使用很多无效地址的函数指针,难以分析。这是因为在dyld_shared_cache_xxx中,一个 framework 引用另一个 framework 中的函数时,是相当于在一个库中直接引用的,直接跳转到对应的地址,而不是再用函数符号经过 lazy binder 进行调用。当 framework 从dyld_shared_cache_xxx中切分出来后,这些函数调用的地址就会指向 framework 外,无法追踪。

所以在分析真机的系统库时,最好是配合模拟器版本的系统库辅助分析,可以看到私有的符号,也可以看到更明确的交叉引用。或者用 IDA 直接分析整个 dyld_shared_cache_xxx文件,不过这样做需要反汇编整个文件,耗时很大。

结尾

静态分析的整个流程如上,剩下的就是积累经验了。通过静态分析查看一些简单函数的实现,在大部分情况下都足够了。不过静态分析的信息是有限的,有时候很难找到想要的函数,这时候就需要动态分析上场了。下一篇文章将讲解动态分析。

参考