标识符混淆
念大婶在博客中介绍了两种方法,用于保护代码逻辑,对抗逆向分析
代码混淆 通过宏定义,混淆objective-c消息(函数),用于对抗class-dump。
敏感逻辑用C实现 通过static关键字和函数指针的方式,将关键逻辑隐藏,可以对抗class-dump和Cycript攻击。
如果用了第二种方式,将函数改用c实现,虽然通过class-dump
得不到有价值的信息,但通过nm
命令或者IDA/Hopper
等工具仍然能从符号表中找到这些c函数以及衍生出的一些静态变量。针对这种情况,我们还是可以通过宏定义的方式,将这些c的标识符(函数名、变量名)替换为随机字符串。
举个例子:
1
2
3
4
5
6
7
8
9
#define func1 gtBFTcseXSElp
#define func2 yNGYcdrCDEzaqZAQki
#define globalValue uNHUvfrVFRxawXAWlo
int globalValue ;
void func1 () {
}
void func2 ( int i ) {
func1 ();
}
nm
检查符号表,结果如下
1
2
3
0000000000000000 T _gtBFTcseXSElp
0000000000000004 C _uNHUvfrVFRxawXAWlo
0000000000000010 T _yNGYcdrCDEzaqZAQki
说明宏替换对于c的标识符同样有效。但是要一个个手动去define,感觉是要累死的节奏。如果能通过一个脚本,自动从源代码里把所有的标识符声明提取出来,生成一个头文件就好了。可以考虑几种方案:
使用正则表达式,根据标识符的声明语法提取
先解析为语法树,再提取标识符节点
给需要混淆的符号打个标记
很显然,前两种方案都很繁琐,不好维护。并且如果我要做一个library给第三方使用,必然要暴露一些接口不能被混淆,只有第三种方式可以灵活地选择那些需要混淆哪些不需要,而这种方案实现起来也最简单。最终实现如下:
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
26
27
28
29
30
31
32
//test.c
#ifdef SYMBOL_OBFUSCATE // 通过外部宏定义控制是否混淆
#include "symbols.h" // 引入生成的混淆头文件
#define SYMBOL(name) asm(name) // 使用asm label语法修改符号名称
#else
#define SYMBOL(name) // 将宏定义为空,即不混淆
#endif
// 声明并标记需要混淆的符号
int globalValue SYMBOL ( _globalValue );
void func1 () SYMBOL ( _func1 );
void func2 ( int a ) SYMBOL ( _func2 );
void func3 (); // 不混淆
// 以下不需要做任何处理,保持原样即可
void func1 () {
}
void func2 ( int a ) {
func1 ();
}
void func3 () {
}
使用asm label
语法的好处是,只需要将符号的声明标记出来进行替换即可, 不需要对该符号的引用进行标记和替换。如果要混淆已经完成的代码,这一点非常省时省力。
扫描源代码并生成混淆头文件的脚本:
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#!/bin/bash
# 本脚本用于对源代码中的函数名及全局变量名进行混淆,生成映射文件
# usage: rand a b
# 生成[a, b)之间的随机数
function rand(){
min = $1
max = $(( $2 - $min ))
num = $(( $RANDOM + 1000000000 ))
echo $(( $num % $max + $min ))
}
# 生成随机字符
function rand_c() {
base = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_$"
echo ${ base : $( rand 0 54) : 1 }
}
# 生成16-32长度的随机变量名
function rand_s() {
symbol = ""
for i in $( seq $( rand 16 33)) ; do
symbol = $symbol $( rand_c)
done
echo $symbol
}
file = $2
src = $1
# 生成文件头,注释
cat > $file << EOF
//
// $file
//
/*
* This is the symbol substitution mapping file.
* Auto-generated by $0, from the source file $src.
* You can change the value of macro defination freely, but DO NOT DELETE any of them.
*/
EOF
# 提取源文件中所有的SYMBOL(_xxx)宏,并生成随机标识符
cat $src | sed -n "s/.*SYMBOL(\(_.*\)).*/\1/p" | while read symbol
do
rand_symbol = ` rand_s`
echo -e "\033[32m$symbol\033[m -> \033[33m$rand_symbol\033[m"
echo "#define $symbol \"$rand_symbol\"" >> $file
done
exit
测试一下效果:
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
#正常编译并查看符号表
$ clang -c test.c -o test.o && nm test.o
0000000000000000 T _func1
0000000000000010 T _func2
0000000000000030 T _func3
0000000000000004 C _globalValue
#扫描源代码,生成混淆头文件
$ ./obfuscate.sh test.c symbols.h
_globalValue -> vftVFRxswXAWlo$LOmhu
_func1 -> UvftVFTxsweCSElpqLPmjiMJIb
_func2 -> BGTcseCSEzpqLPQjiM
#查看生成的头文件
$ tail -n 3 symbols.h
#define _globalValue "vftVFRxswXAWlo$LOmhu"
#define _func1 "UvftVFTxsweCSElpqLPmjiMJIb"
#define _func2 "BGTcseCSEzpqLPQjiM"
#混淆编译,并查看符号表
$ clang -DSYMBOL_OBFUSCATE -c test.c -o test.o && nm test.o
0000000000000010 T BGTcseCSEzpqLPQjiM
0000000000000000 T UvftVFTxsweCSElpqLPmjiMJIb
0000000000000030 T _func3
0000000000000004 C vftVFRxswXAWlo$LOmhu
如果你有点懵,可以看一下混淆的过程是怎样的
1
2
3
4
5
6
7
8
9
void func1() SYMBOL( _func1) ; == > void func1() ;
|| 不混淆时的展开
\| | / SYMBOL宏展开
\/
void func1() asm( _func1) ;
||
\| | / _func1宏展开
\/
void func1() asm( "UvftVFTxsweCSElpqLPmjiMJIb" ) ;
asm label
的语法解释,可以参考gcc的onlinedocs
字符串混淆
字符串也是逆向分析的一大切入点,可以根据目标字符串快速定位目标代码,有针对性地进行调试、分析。在binary中隐藏字符串可以有效提升静态分析的难度,因此需要在源代码中将字符串进行加密,运行时先解密后再使用。但如果在源代码中直接写加密后的字符串,代码的可读性就会变得非常差。
但字符串无法像标识符那样,在预编译阶段直接通过几个宏就替换为加密的形式。我想了一个不是很优雅,但是很有效的方法:
将源代码中的字符串通过函数宏手动标记
备份源代码
将源代码中所有标记过的字符串,替换成decrypt("密文")
的形式
在适当的位置,插入decrypt
函数的实现(或者事先在源代码中写好)
编译
还原备份的源代码
示例,混淆这份代码中的字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
#import <Foundation/Foundation.h>
#ifndef STRING_OBFUSCATE
#define NSSTRING(string) @string
#define CSTRING(string) string
#endif
int main () {
NSLog ( @"%@" , NSSTRING ( "Hello, world!" ));
printf ( "%s \n " , CSTRING ( "Hello, world!" ));
}
反编译的结果
字符串混淆脚本,字符串加密选用简单的抑或,仅为示例
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 本脚本用于对源代码中的字符串进行加密
# 1. 在源代码中插入解密函数decryptConstString
# 2. 插入宏,替换所有的NSSTRING(...)和CSTRING(...)为decryptConstString(encrypted_string)
# 3. 替换所有字符串常量为加密的char数组,形式((char[]){1, 2, 3, 0})
import sys
import re
import os
# 插入宏和解密函数,解密方法:每个字节与0xAA异或
insert_code = '''#define STRING_OBFUSCATE
static char* decryptConstString(char* string) __attribute__((always_inline));
#define NSSTRING(string) [NSString stringWithUTF8String:decryptConstString(string)]
#define CSTRING(string) decryptConstString(string)
static char* decryptConstString(char* string)
{
char* origin_string = string;
while(*string) {
*string ^= 0xAA;
string++;
}
return origin_string;
}
#ifndef STRING_OBFUSCATE'''
# 替换字符串为((char[]){1, 2, 3, 0})的形式,同时让每个字节与0xAA异或进行加密
def replace ( match ):
# print match.group()
string = match . group ( 2 ) + ' \x00 '
replaced_string = '((char []) {' + ', ' . join ([ " %i " % (( ord ( c ) ^ 0xAA ) if c != ' \0 ' else 0 ) for c in list ( string )]) + '})'
# print replaced_string
return match . group ( 1 ) + replaced_string + match . group ( 3 )
# 修改源代码,加入字符串加密的函数
def obfuscate ( file ):
with open ( file , 'r' ) as f :
code = f . read ()
f . close ()
code = re . sub ( r'(NSSTRING\(|CSTRING\()"(.*?)"(\))' , replace , code )
code = code . replace ( '#ifndef STRING_OBFUSCATE' , insert_code )
# print code
with open ( file , 'w' ) as f :
f . write ( code )
f . close ()
if __name__ == '__main__' :
if len ( sys . argv ) == 2 and os . path . exists ( sys . argv [ 1 ]):
obfuscate ( sys . argv [ 1 ])
else :
sys . exit ()
执行字符串混淆脚本,源代码变为:
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
26
27
28
29
30
#import <Foundation/Foundation.h>
#define STRING_OBFUSCATE
static char * decryptConstString ( char * string ) __attribute__ (( always_inline ));
#define NSSTRING(string) [NSString stringWithUTF8String:decryptConstString(string)]
#define CSTRING(string) decryptConstString(string)
static char * decryptConstString ( char * string )
{
char * origin_string = string ;
while ( * string ) {
* string ^= 0xAA ;
string ++ ;
}
return origin_string ;
}
#ifndef STRING_OBFUSCATE
#define NSSTRING(string) @string
#define CSTRING(string) string
#endif
int main () {
NSLog ( @"%@" , NSSTRING ((( char []) { 226 , 207 , 198 , 198 , 197 , 134 , 138 , 221 , 197 , 216 , 198 , 206 , 139 , 0 })));
printf ( "%s \n " , CSTRING ((( char []) { 226 , 207 , 198 , 198 , 197 , 134 , 138 , 221 , 197 , 216 , 198 , 206 , 139 , 0 })));
}
测试一下效果
1
2
3
4
5
$ python obfuscate . py string . m
$ clang string . m - framework Foundation
$ . / a . out
2017 - 01 - 01 19 : 34 : 17.144 a . out [ 3563 : 143969 ] Hello , world !
Hello , world !
反编译一下,已经隐藏了字符串特征
__cstring
中也看不到原始的字符串,连混淆后的字符串也看不到
说明:
如果把字符串"Hello"
转化为char[]{'H','e','l','l','o',0}
的形式进行编译,字符串就会从__cstring
中的明文字符,变为__text
中的一段代码,可以防止被搜索到。因此如果要兼顾执行效率和混淆的效果,只需要把字符串转换成char数组的形式就可以了,不需要再添加解密的步骤。