[iOS 逆向 12] 加密与动态保护

前面详细介绍了 App 的逆向过程,可以发现逆向工具层出不穷,使逆向开发的门槛越来越低。回看自己开发的 App,也面临着被分析、解除使用限制等危险。下面介绍几种保护方案。

数据加密

App 内需要保护的数据主要包括静态字符串、本地存储数据和网络传输数据,一般开发者不会对这些数据加密后使用,而是直接明文保存在本地或网络传输。分析者通过分析本地文件或拦截网络请求就可以轻松读取或修改数据。

加密算法

根据加密结果能否解密出原文,分成可逆或不可逆加密。可逆加密依照加解密的密钥是否一样,又分成对称或非对称加密。下面是几个常用的算法。
(1)Base64:将二进制数据每6位用一个可打印字符代替,用于传输。
(2)MD5:计算数据的摘要,用于校验。
(3)DES:数据加密标准,是一种对称加密算法。
(4)AES:高级加密标准,用来代替 DES。
(5)RSA:是一种非对称加密算法,使用一对公钥和私钥,速度较慢。

本地存储加密

本地存储的方式如数据库、NSUserDefaults 使用的 plist 文件、自定义格式文件,都不要直接用明文保存敏感信息。对于数据库,可以使用带有加密功能的开源数据库框架 SQLCipher。对于 NSUserDefaults 保存在 plist 文件中的信息,例如前面对应用“番茄ToDo”的分析,其中判断是否是会员的函数竟然是直接读取本地 NSUserDefaults中的值,那么如果想使用会员权限,只需要修改沙盒内对应的 plist 文件中的一行就可以了,但考虑到这个值可能会被更新,所以更好的做法是 hook 会员的函数而已。总之,敏感信息应该先使用对称加密算法加密后再存储,或者直接保存到苹果服务器。

网络传输加密

网络传输的安全机制包括两个部分,一个是传输安全,一个是确保 API 调用来自我的 App。

传输安全需要解决中间人攻击等问题,解决思路实际就是 https 的思路。

客户首先需要得到服务器的公钥,如在互联网的某个地方下载或每次通信时服务器发给客户,但是都不能证明是真的公钥(传输过程中可能被黑客拦截),于是出现数字证书。数字证书包含:颁发机构、有效期、公钥、所有者、签名算法、指纹及指纹算法等信息,并且数字证书可以确保证书里的公钥确实是这个证书的所有者的(后面会解释)。下面是一个交互情景:

客户 -> 服务器:给我你的公钥 
服务器 -> 客户:我是服务器,这里是我的数字证书 // 内含公钥
客户 -> 服务器:你是真的服务器吗,有私钥吗?给我加密一个随机串看看
服务器 -> 客户:{随机字符串} [私钥|RSA] 
客户 -> 服务器:{验证成功。后面用对称加密通信,指定加密算法和密钥} [公钥|RSA]     
服务器 -> 客户:{收到} [密钥|对称加密算法]
客户 -> 服务器:{查询账号余额} [密钥|对称加密算法]
服务器 -> 客户:{100元} [密钥|对称加密算法]

其中,如果黑客发送一些简单的有规律的字符串给服务器加密,寻找加密规律有可能猜出私钥。所以服务器会对字符串先计算 hash 值,然后对 hash 值用私钥加密。客户端用公钥解密后,计算原字符串 hash 值然后比对,降低了规律字符串攻击几率。 通过自定协议或者直接使用 https 来解决传输安全问题。

第二个问题,确保 API 调用来自 App。
分析者获得 API 后,可以用软件模拟访问过程,恶意调用接口注册、刷单。一种办法是对请求签名。对(密钥 + API 名字 + API 参数)计算 hash 值并一起传输,服务器收到后,用相同的密钥 + API + 参数计算 hash 值,然后比对即可。

字符串加密

前面提到的加密方案都用到了密钥,那么密钥在代码中怎么保存才能避免静态分析时被看到?除了密钥,程序中其他的一些关键字符串也不能直接写在源码中。这时就需要对静态字符串进行一些操作。

主要思路是,设计一个算法,将原字符串映射到一个新字符串。在项目源码中,只使用新字符串和还原算法。静态分析时,分析者可以利用 IDA 等工具看到还原算法的汇编代码和伪代码,然后推导出算法流程。推导难度取决于算法的复杂程度和分析者的能力,也就是说,字符串加密只能增加分析的难度,但实际上已经足以难倒绝大部分分析者了。

如果分析者使用动态分析呢?分析者直接调试程序,在还原算法输出的地方打断点查看寄存器和内存,不就直接看到结果了吗?这部分后面动态保护会提到。

静态混淆

使用 class-dump 可以提取出可执行文件中所有的类、方法、属性、Category、Protocol 等内容,这是不可避免的,因为 Mach-O 格式文件把这些内容写的清清楚楚。而水平越高的程序员,写的类名、方法名、属性名等越易懂,静态分析时一看方法名就知道是哪块功能,恰恰降低了静态分析的难度,因此需要对所有符号的名字进行混淆。

宏定义

一般在项目的 Release 编译选项中添加宏定义,将所有自定义的类名、方法名、属性名等替换为无意义的字符串。需要先创建一个 pch 文件,然后在 Build Settings 中搜索 Prefix Header,添加文件的路径。注意,如果使用了 storyboard 或 nib,还需要手动修改这些文件中的符号名。

手动在 pch 文件中添加宏定义以及修改 storyboard 文件显然比较慢,可以使用 brew 安装 ios-class-guard 工具,该工具获取符号时会自动过滤掉所有系统库用到的符号,只生成自定义的符号,并自动修改项目中 storyboard 文件中的符号名:
ios-class-guard MyApp --sdk-root /path/of/iPhoneos.sdk -X /path/of/MyApp -O Header.h
必须在使用 ios-class-guard 前备份 storyboard 文件,便于再次编辑。

修改二进制

尽管在 pch 文件中设置的宏定义只在 Release 模式中生效,但理想情况下还是不修改源码。由于 class-dump 是根据 Mach-O 格式读取 Mach-O 文件中类名、方法名等对应的 Section,那么我们可以直接修改文件的二进制,如保存类名的段 __TEXT,__objc_classname,修改字符串内容,最后对二进制文件重签名。

缺点

使用静态混淆,可能会带来很多问题,特别是程序中使用 Runtime API 实现一些功能时。Runtime API 通常都是用字符串获得类对象、调用函数,例如 objc_getClass(char *)objc_msgSend(id, char *);消息转发机制等动态特性都是靠判断方法名的字符串转发的;大型项目中都是用 Runtime 根据字符串获得指定模块实现组件化。总之,使用静态混淆的成本很高,特别是大型项目。

动态保护

对于静态分析中很难得到的内容,比如被加密字符串的原始值,一般都是通过动态调试获取。在逆向开发中,当我们找到目标函数后,就开始写 hook 函数、注入动态库。有攻才有守,因此 App 的动态保护也是从这些方面入手去保护。

反调试

反调试的思路有两种:阻止调试器附加和检测调试器是否存在。

阻止调试器附加

要想阻止调试器附加,首先需要明白调试器是如何附加并调试程序的。我们在调试程序时,是把目标进程作为参数传给 debugserver,然后指定一个端口号,让调试程序连接这个端口号后给 debugserver 发调试指令。那么 debugserver 是如何给目标进程发指令的呢?Unix 系统中提供了一种对进程跟踪、控制的手段,ptrace 系统调用。该系统调用的参数分别是:要执行的操作,进程号,地址,数据。根据不同的操作,读出地址上的数据或把数据写到地址上或者不需要读写数据,debugserver 就是通过 ptrace 系统调用附加到目标进程。通过观察 ptrace 的操作列表,可以发现一个 DENY_ATTACH 的操作,也就是拒绝别的进程附加调试,因此阻止调试器附加的办法出现了,在代码中执行 ptrace(PT_DENY_ATTACH, 0, 0, 0);。ptrace 的系统调用号为 26,因此也可以使用 syscall(26, PT_DENY_ATTACH, 0, 0, 0); 来达到同样效果。由于 syscall 里面实际调用了汇编指令 svc 来中断进程,因此也可以手写汇编指令给 x0~x3 和 x16 寄存器传参数,调用 svc 来中断。

检测调试器是否存在

进程中有标志位代表是否正在被调试,使用 sysctl 系统调用获取当前进程的信息:
sysctl(query, 4, &info, &info_size, 0, 0);
然后从 info 中读出标志位判断是否正在被调试。sysctl 的系统调用号为 202,同样也可以使用 syscall 函数或 svc 指令进行中断。此外还有一些系统调用可以检测调试器是否存在,原理都相似。

反-反调试

既然通过一些函数可以阻止或检测调试器,那么我 hook 这些函数,改变执行流程不就行了吗?hook 上面提到的几个系统调用函数,判断参数是不是要终结调试器或检测调试器,如果是就返回没有调试器。前面已经介绍过,对于越狱设备可以直接用 Cydia Substrate 框架提供的 MSHookFuntion 函数对 C 函数进行 hook,对于非越狱设备,可以使用 fishhook 对 C 函数进行 hook。

如果不使用 hook,或者源代码中是内联汇编调用中断指令,都可以用 lldb 直接修改寄存器以改变参数或返回值,让对应的系统调用无法正常工作。可以在 IDA 中搜索 svc 指令找到调用位置。

此外,有人提出一个办法,可以在函数被 hook 之前就阻止调试器附加。利用动态库的加载顺序,在原项目中添加的动态库比后来添加的动态库先加载,也就是先执行动态库的加载回调,在回调中拒绝附加。对于这种反调试策略,可以从 dyld 开始打断点:编辑 ~/.lldbinit 文件,添加 settings set target.process.stop-on-sharedlibrary-events 1,这样在 dyld 中就可以停下程序。然后在执行动态库的加载回调或 constructor 函数中,修改系统调用的寄存器以改变参数或返回值。

总之,不论反调试是调用函数还是内联汇编,总有办法找到指令位置,然后可以动态调试修改寄存器内容,或者静态修改二进制文件中的指令,改为 mov x0, #0 之类。反调试策略只能增加逆向分析的难度。

检测注入

检测是否有其他动态库注入。使用 _dyld_image_count() 函数获取加载的 image 数量;通过 _dyld_get_image_name(i) 获取第 i 个 image 的完整名称。可以在源代码中保存动态库白名单,根据返回的 image 名称是否在名单上来判断是否被注入。和反调试一样,检测注入也只是增加逆向分析难度。

检测 hook

前面提到的几种 hook 方式,涉及的原理包括 Method Swizzling、修改符号表、修改内存指令。针对这三种原理做出不同的检测方法,增加逆向难度。

  • Method Swizzling
    OC 方法重排是给方法替换了 C 函数指针,逆向工程使用这种方式 hook 后,新的 C 函数指针所在的动态库肯定和原方法所在的 image 不同。函数 int dladdr(void *address, Dl_info *info) 可以获得一个地址所在的 image 的信息,对目标方法使用 runtime API 获取到 C 函数地址,然后使用 dladdr 获取该函数所在的 image 信息。从 info 中读取模块名,如果 image 是主程序模块,或者是在白名单中的动态库则检查通过。
  • 修改符号表
    fishhook 通过修改符号表中的指针实现 hook,符号表中原指针要么是桩辅助函数,要么是真实地址。遍历符号中的指针,然后用 dladdr 检查指针所在模块,操作和前面相同。
  • 修改内存指令
    读出目标函数地址 +4 处的内存,判断是否是 BL 指令。
完整性校验

逆向工程中可能会修改 Mach-O 文件的加载命令、二进制代码等内容,然后重签名。因此文件的完整性校验也从这些方面入手。

  • 加载命令
    逆向时经常通过添加动态库加载命令给程序注入动态库,因此可以遍历文件的加载命令,找到所有 LC_LOAD_DYLIB 命令中的动态库名,判断动态库是否在白名单上。
  • 代码段
    获取到内存中代码段的 __text 节,对该节计算摘要,可以判断内存中的代码是否被修改过。
  • 重签名
    前面介绍过重签名的过程,因此可以从 LC_CODE_SIGNATURE 加载命令中读取签名信息,判断 Bundle ID 是否被替换。或者读取 App 目录下有没有 Provisioning Profile 文件,根据内容判断是否被重签名。

总结

不管是数据加密、静态混淆还是动态保护,只能提高逆向难度。让分析者在逆向代价和收益之间做权衡。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值