iOS混淆方案调研
目的
公司最近有个马甲包的任务,为了提升马甲包的开发上线效率,需要对混淆的原理进行探究,找到合适的混淆方案.最好是能够自己编写一套混淆的工具,和通用稳定的流程.
目前存在的困难
- 混淆工具质量参差不齐,不能保证一定有效,且价格昂贵.
- 苹果的机审策略不明确,无法有针对性的选择混淆方案.
混淆方式
目前主要有2种混淆的方式:
- 基于源码的混淆: 目前大部分的混淆工具都是在源码层面进行混淆,直接重写项目也属于这一种
- 基于编译产物MachO文件的混淆: 主要是基于LLVM Pass框架开发的llvm套件,obfuscator, 一般称为ollvm. ollvm可以编译成Toochain直接集成到XCode中
源码混淆方案
同时两种方案的侧重点不同,基于源码的混淆,通常情况下会做一下几件事:
- 类名和方法名的替换
- 字符串的加密
- 垃圾代码的生成
以上的步骤中, 第一步会修改到数据段的_objc_methodname和 _objc_className这两个分区. 第二步会修改到StringTable,第三步会修改到数据段的__text分区,但是通常情况下无法消除__text分区中指令的逻辑特征.后文中有举例说明.
除了上述问题以外,源码混淆的方案还有特点是需要对不同的语音进行不同的脚本实现.
OLLVM
原理:OLLVM的混淆操作就是在中间表示IR层,通过编写Pass来混淆IR,然后后端依据IR来生成的目标代码也就被混淆了. OLLVM提供了下面几种混淆功能
- Instructions Substitution (-mllvm -sub), 这个是将一些运算指令替换为等效的其他指令的组合,比如
a = b + c
可以替换为
a = b - (-c)
或者
r = rand (); a = b + r; a = a + c; a = a - r
或者
r = rand (); a = b - r; a = a + b; a = a + r
同时还提供了 减法, 逻辑与, 逻辑或, 逻辑异或的指令替代.
- Bogus Control Flow (-mllvm -bcf) ,这个模式主要嵌套几层判断逻辑,一个简单的运算都会在外面包几层if-else,所以这个模式加上编译速度会慢很多因为要做几层假的逻辑包裹真正有用的代码。开启此功能会严重拖慢编译速度.
- Control Flow Flattening
这个模式主要是把一些if-else语句,嵌套成do-while语句 - 字符串混淆 (-mllvm -sobf -mllvm -seed=0xdeadbeaf) 字符串加密,0xdeadbeaf为随机数生成器种子,Armariris 是 原LLVM的移植版本, 提供了字符串混淆的功能.
OLLVM的优势在于, 它可以提供代码逻辑的混淆,直接修改__text分区,使得反编译还原代码逻辑十分困难, 而且由于混淆是作用域IR层, 对于swift和oc的代码混淆都是一致的.
OLLVM设计之初是为了提供编译文件的加固,并非为了提供马甲包的过审功能, 因此虽然它可以修改__text分区中的代码逻辑,但是并没有为项目添加新的Class和Function,因此虽然进行了混淆,__text段中的一些特征任然会得到保留.比方说现在有2个关联的类,ClassA和ClassB
Class A {
func bar(){
B().foo()
}
}
Class B{
func foo(){
...
}
}
从MachO中,可以观察到ClassA对ClassB方法的符号引用,无论是否进行混淆,这一特征都是存在的. 如果采用源码层面的混淆方案,则可以新建一个ClassC, 在ClassA 的源码中插入对ClassC的符号引用,新增了A关联的类,这样就可以改变这一特质.
总结
在不清楚苹果具体的机审策略的情况下,为了保证过审概率,我们需要保证混淆前后的MachO文件在所有的section都尽可能的降低相似性, 为了达到这一点.两种策略需要同时采用.
测试
目的
测试的目的,主要是验证一下2种混淆方案下对构建MachO文件产生的不同效果,所有的测试构建均是在Release模式下进行的,保证编译器做了最大的优化.
使用工具
- MachOView
- Hopper Disassembler 5.7
- Xcode13.2.1
- ollvm 13.x版本
测试一
源文件直接插入空的c语言函数调用,函数中仅做无意义的循环操作,只有局部变量的自增逻辑.
#import "ViewController.h"
CG_INLINE void junkCode(){
int i = 0;
while (i++ < 100){
//do nothing
}
}
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UILabel *label = [UILabel new];
[self.view addSubview:label];
label.frame = CGRectMake(100, 100, 100, 100);
label.backgroundColor = UIColor.redColor;
label.text = @"JunkInjected";
junkCode();
}
@end
本例中注入的是垃圾c代码,其中有一段循环没有做任何事,编译得到MachO产物,分别用MachOView和Hopper打开
下面我们来看看ViewController被编译链接之后的样子,看看我们的垃圾代码有没有被优化
- step1.在MachOView中找到代码段的起始地址,如下图红框
- step2.打开Hopper按下键盘g,输入step1中的地址,跳转到代码段的起始位置,如图
可以看到代码段的起始位置就是
ViewController,这是编译顺序决定的.
- step3.选择伪代码模式,从图中可以看到,我们注入的垃圾c代码被编译器优化了.
值得一提的是,如果不开启编译优化(Xcode中debug默认选项),则不会被优化.测试时需要注意.
接下来修改垃圾代码,在循环中对全局变量进行自增,同时这个全局变量不会被任和外部对象或者函数引用.
//修改后的代码
int globalInt = 0;
CG_INLINE void junkCode(){
int i = 0;
while (i++ < 100){
globalInt ++;
}
}
Hopper伪代码结果
可以看到函数逻辑得到了保留
结论
如果要进行c语言的函数调用的垃圾代码注入, 需要对外部变量进行修改, 否则会被编译器优化.如果是oc的代码, 由于其动态的特性,编译器应该不会进行优化.
测试二
如果只做方法名和类名的修改, 这些修改从MachO文件上如何体现.即要找到方法名和类名存放在何处.
OC中所有的方法调用,在runtime看来都是给对象发消息, 就是通过objc_msgSend或者 objc_msgSendSuper2给对象传递方法名的字符串比如
Foo *foo = ...
[foo bar];
//实际上是调用以下方法
objc_msgSend(foo, sel_registerName("bar"));
上述的代码涉及到3个符号, objc_msgSend 和 sel_register 是runtime库提供的函数, 以及一个字符串"bar".
要探究上述问题, 我们可以分析一下Hopper是如何通过分析__text中指令的地址来解析出方法名的.
接下来我们以测试一中的代码viewDidLoad方法中的[super viewDidLoad];方法举例.
用Hopper打开构建MachO文件,找到对应的代码段
得到地址
双击地址跳转到
__objc_methname分区
可以看到实际的方法名是保存在Text段的
__objc_methname这个section中的
切换到HexMode可以查看十六进制编码
其中的76 69 65 77 44 69 64 4C 6F 61 64就是viewDidLoad这个字符串的**utf-8**编码
我们也可以使用MachOView来查看
结论
__text分区中对方法名的引用是通过地址映射到__objc_methname分区中的,因而如果只做方法名的混淆, 是无法改__text分区的特征的, 同样的方法可以分析得出
__text中对类名的引用是保存在__objc_classname分区中- 方法类型的信息保存在
__objc_methtype分区中
值得一提的是,__objc_methname,__objc_classname,__objc_methtype虽然存储的是字符串信息,但是同样属于代码段,而不是直觉中的数据段. 如果对只类名和方法名做混淆,会改变__objc_methname,__objc_classname分区存储的字符串信息, 无法对__text分区,甚至是__objc_methtype进行修改.
测试三 ollvm的混淆
step1.ollvm 编译
注意这里的版本是13.x要和Xcode版本一致,原始的ollvm项目很早就放弃维护了,这里采用的是移植的版本,测试中会存在一些小问题.
git clone -b llvm-13.x https://github.com/heroims/obfuscator.git
mkdir build
cd build
cmake DLLVM_ENABLE_NEW_PASS_MANAGER=OFF -DCMAKE_BUILD_TYPE=Release -DLLVM_CREATE_XCODE_TOOLCHAIN=ON -DLLVM_ENABLE_PROJECTS="clang;libcxx;libcxxabi;compiler-rt" ../obfuscator/llvm
make -j8
sudo make install-xcode-toolchain
sudo mv /usr/local/Toolchains /Library/Developer/
上述指令执行完成后,XCode会新增一个名为org.llvm.13.0.1 的ToolChain
step2. 打开需要混淆的工程,选择ToolChain
step3.
新建Configuration命名Confuse ,copy from Release, 新增XCConfig命名Confuse.xcconfig文件配置如下, 将Configurations中Confuse的configuration set 设置为新增的Confuse.xcconfig
OTHER_CFLAGS=-mllvm -sub -mllvm -sobf -mllvm -fla -mllvm -bcf -flegacy-pass-manager
CLANG_CXX_LANGUAGE_STANDARD = "compiler-default";
CLANG_CXX_LIBRARY = "compiler-default";
COMPILER_INDEX_STORE_ENABLE = NO;
ENABLE_BITCODE = NO; // 网上有人说开启bitcode可能会被拒.
配置完毕.可以开始测试混淆了. 注意这里为了使测试更准确,需要将Build Configuration设置为Release, 保证编译器会开启优化.直接build然后找到MachO文件用Hopper打开分析. 直接选择伪代码模式查看ViewController的代码逻辑.
//混淆后伪代码
void -[ViewController viewDidLoad](int arg0) {
var_50 = r28;
stack[-88] = r27;
r29 = &saved_fp;
r31 = r31 + 0xffffffffffffffa0 - 0x50;
r19 = arg0;
r8 = *_x;
r8 = (r8 - 0x1) * r8;
if (((r8 ^ 0xfffffffe) & r8) == 0x0) {
if (CPU_FLAGS & E) {
r8 = 0x1;
}
}
var_52 = r8;
if (*0x10000d69c < 0xa) {
if (CPU_FLAGS & L) {
r8 = 0x1;
}
}
var_51 = r8;
r8 = 0x7494d76d;
do {
while (r8 <= 0x1f14e8b3) {
if (r8 != 0x84e2eac1) {
if (r8 == 0x928ae5d6) {
r0 = &var_A0 - 0x10;
r31 = r0;
*(int128_t *)(&var_A0 + 0xfffffffffffffff0) = r19;
*(int128_t *)(&var_A0 + 0xfffffffffffffff8) = *0x10000d388;
[[r0 super] viewDidLoad];
r28 = [UILabel new];
r0 = [r19 view];
r0 = [r0 retain];
[r0 addSubview:r28];
[r0 release];
[r28 setFrame:r28];
r0 = [UIColor redColor];
r29 = r29;
[r0 retain];
[r28 setBackgroundColor:r2];
[r20 release];
[r28 setText:0x100008068];
r8 = 0x0;
r9 = 0x41c52c87;
while (r9 != 0x7d060e1e) {
if (r9 != 0x41c52c87) {
continue;
}
r9 = (r8 - 0xc44cc07b) + 0x1;
COND = r8 < 0x64;
r8 = r9 + 0xc44cc07b;
if (!COND) {
continue;
}
if (!CPU_FLAGS & B) {
r9 = 0x7d060e1e;
}
else {
r9 = 0x41c52c87;
}
}
[r28 release];
r8 = 0x84e2eac1;
}
}
else {
r0 = &var_A0 - 0x10;
r31 = r0;
*(int128_t *)(&var_A0 + 0xfffffffffffffff0) = r19;
*(int128_t *)(&var_A0 + 0xfffffffffffffff8) = *0x10000d388;
[[r0 super] viewDidLoad];
r28 = [UILabel new];
r0 = [r19 view];
r0 = [r0 retain];
[r0 addSubview:r28];
[r0 release];
[r28 setFrame:r28];
r0 = [UIColor redColor];
r29 = r29;
[r0 retain];
[r28 setBackgroundColor:r2];
[r20 release];
[r28 setText:0x100008068];
r8 = 0x0;
r9 = 0x41c52c87;
while (r9 != 0x7d060e1e) {
if (r9 != 0x41c52c87) {
continue;
}
COND = r8 < 0x64;
r8 = r8 + 0x1;
if (!COND) {
continue;
}
if (!CPU_FLAGS & B) {
r9 = 0x7d060e1e;
}
else {
r9 = 0x41c52c87;
}
}
[r28 release];
r8 = *_x;
r8 = ((r8 - 0x1) + 0xbf4cca1c) * r8 ^ 0xffffffff | 0xfffffffe;
if (r8 != -0x1) {
if (CPU_FLAGS & NE) {
r8 = 0x1;
}
}
r9 = *0x10000d69c;
if (r9 > 0x9) {
r9 = *0x10000d69c;
if (CPU_FLAGS & G) {
r9 = 0x1;
}
}
r8 = r9 ^ r8 | (r9 | r8) ^ 0x1;
if (r8 != 0x0) {
if (!CPU_FLAGS & NE) {
r8 = 0x928ae5d6;
}
else {
r8 = 0x1f14e8b4;
}
}
}
}
if (r8 == 0x1f14e8b4) {
break;
}
if (r8 == 0x7494d76d) {
r8 = var_52 ^ 0x1 | var_51 ^ 0x1;
asm { orn w8, w10, w8 };
if ((r8 & 0x1) != 0x0) {
if (!CPU_FLAGS & NE) {
r8 = 0x928ae5d6;
}
else {
r8 = 0x84e2eac1;
}
}
}
} while (true);
return;
}
与测试一中的伪码对比,可以看出增加了很多条件分支,,可以看出ollvm改变了代码执行的流程,在一定程度上改变了
__text分区的特征. 同时加入了字符串的混淆setText:参数不可解析了,用MachOView打开检查StringTabel,.测试结果和预期一致.