浅谈 iOS 关键代码的混淆

前言

前几天我在微博上跟人吹牛,说我会做 iOS 的花指令。

其实我不会做。于是我赶紧上网查资料现学现做,研究了两个晚上之后终于发现是不可行的。

精确点说,iOS 程序以真机为目标编译时,由于 ARM 架构的限制,无法像 x86 程序一样通过加入垃圾数据的方法干扰反汇编器。

简单点说,定长指令的 RISC 平台做不了花指令。

虽然吹牛吹瞎了,但是实践当中也学会了不少东西,因此在这里记录一下。

由于我的越狱设备只有一台 iPhone 4s @ iOS 6.1.3 ,因此以下内容都是基于 ARMv7 平台。原理学会了,其他的就都好说。

CrackMe

首先写一个 CrackMe 。

在 Storyboard 里面拖一个文本框和一个按钮。然后在按钮的点击事件里写验证逻辑:

1
2
3
4
5
6
7
8
9
10
11
- (IBAction)registerButtonTouched:(UIButton *)sender {
if ([self isValidRegisterCode:_textField.text]) {
NSLog(@"Success");
} else {
NSLog(@"Failed");
}
}

- (BOOL)isValidRegisterCode: (NSString *)registerCode {
return [registerCode isEqualToString:@"abcd"];
}

程序的逻辑很简单,如果文本框输入的内容是abcd就注册成功,否则失败。编译运行,程序是对的。

然后把程序从 iPhone 里面拷出来,扔到 Hopper Disassembler 里面:

可以看到符号直接被导出来了。

点击 -[ViewController isValidRegisterCode:] 这个符号,可以看到对逻辑部分的反汇编,注册码就明文写在那。

用 C 语言改写关键逻辑

我记得之前也不在哪看过一个说法,用 C 语言写的函数 classdump 是导不出头文件的。

于是尝试用 C 语言改写上面的验证逻辑:

1
2
3
static bool c_isValidRegisterCode(const char* registerCode) {
return strcmp(registerCode, "abcd") == 0;
}

注意,由于这里我偷懒没有新建一个文件来放这个函数,因此声明这个函数时要加上标记,防止它被内联,即按如下形式声明:

1
__attribute__((noinline)) static bool c_isValidRegisterCode(const char* registerCode);

可以看到还是扒得一干二净。

用汇编语言改写关键逻辑

其实我不会 ARM 汇编。好在可以上网搜索。LLVM 的伪指令参照 GNU 汇编格式,其他的参考 ARMv7 Instruction set 和官网写的 calling convention.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
.section __TEXT,__text,regular,pure_instructions
.global _isValidRegisterCode // 全局符号名
.extern _strcmp // 链接 strcmp 函数

_isValidRegisterCode:

push {r4-r7, lr} // 保护现场
add r7, sp, #12
push {r8, r10, r11}
vstmdb sp!, {d8-d15}

ldr r1, =string
blx _strcmp // 由于上一步传参时输入的密码已经在 r0 里面,所以这里可以直接调
movs r1, #0
cmp r0, #0
it eq
moveq r1, #1
mov r0, r1

vldmia sp!, {d8-d15} // 恢复现场
pop {r8, r10, r11}
pop {r4-r7, pc}

string:
.asciz "abcd" // .asciz 代表 C 格式字符串,末尾自动加 '\0'

然后再给他做一个头文件,这样才能在 ObjC 中调用:

1
2
3
4
#include <stdbool.h>
#include <string.h>

extern bool isValidRegisterCode(const char* registerCode);

我在命名上偷了个懒,在汇编的符号名上用了 C 函数的 Name mangling,所以这里可以直接这样声明成 C 函数的形式。

编译运行,逻辑没错。再拖到 Hopper 里面:

虽然 strcmp 还在,但是密码abcd已经被解析成代码了。这也是花指令的原理,通过位置的设计让反汇编器把垃圾数据解析成汇编指令。

再扔到 ida 里面,符号同样在,直接跳到 string 部分:

可以看到字符串同样被解析成了指令,而且解析的结果和 Hopper Disassembler 的结果是不同的。

花指令?

下面就到了吹牛吹破的点。尝试在关键逻辑处加花指令:

1
2
3
4
5
6
7
    b       realwork
.ascii "A"
realwork:
ldr r1, =string
blx _strcmp
movs r1, #0
cmp r0, #0

结果编译失败。编译器报告说指令没有对齐。

查了下资料,ARM 架构,或者说 RISC 家族的一大特点就是指令定长,相比 x86 的变长指令,每次取指令的结果都是确定的。

ARMv7 平台按 4 位对齐,因此如果填一个 4 位对齐的垃圾数据,是可以通过编译的,比如.ascii "junk" ,但是这样也失去了加花指令的意义——通过对代码和数据的混淆误导反汇编器。毕竟,就算反汇编器把数据解析成了指令,它也是对齐的,不会影响到后面的真实代码。

总结

虽然 ARM 之类的 RISC 家族平台实现不了经典的 x86 式花指令,但是对关键逻辑的保护还有很多可以做,比如为了隐藏strcmp符号可以加密字符串然后运行时链接,用多次的指针变换、结构体包装等方法隐藏入口等等。还有人把正常汇编代码中插入的逻辑无关指令也叫做花指令。

不过,这篇文章本来就是为了圆上花指令的话题,所以这里就不深入讨论怎么对这段逻辑加密了。网上也有很多类似的文章,大家可以自己找找看。

另外,这次探索也体现出我对计算机结构的认识还有很大不足,常识性的问题都不会。该读一遍 CSAPP 啦!

参考

Reduced instruction set computing

ARM architecture

GNU Assembler Examples

ARMv7 Function Calling Conventions