软件混淆技术
- 软件
- 代码
- 数据
- 软件混淆(从宏观角度看)
- 代码混淆
- 数据混淆
- 软件混淆(从软件的编译与链接生成过程看)
- 事前的代码混淆
- 事中的编译期混淆
- 事后的二进制混淆
- 代码混淆
- Code Obfuscation
- 指混淆过程发生在代码级别,包括对源码进行静态修改的源码混淆,和基于源码(针对 C++ 语言)模板展开的模板混淆
- 编译期混淆
- 发生于代码编译期间
- 使用 LLVM 编译套件的情况下,可对代码进行混淆处理
- 分为:
- 编译器前端在进行原生代码分析时采取的前端混淆
- 编译原生代码并生成中间代码 IR 时采取的 IR 混淆
- 使用 ProGuard 对 DEX 进行优化保护时,采取的是 DEX 混淆
- 二进制混淆
- 指在生成目标软件后对软件的二进制代码进行的混淆
- 典型
- 生成 DEX 后对其进行反编译
- 对 DEX 进行二次混淆
源码混淆
- 即对源码进行混淆,让人从代码级别就无法理解程序的执行行为
- 方法
- 手工或使用工具替换源码中变量和符号信息
- 优点
- 经过这种处理的代码分析难度提高
- 缺点
- 若手工替换,则缺少自动化操作,开发和维护成本提高
- 若用工具替换,虽能防止被逆向分析,但自身的调试和维护会很麻烦,当出现 bug,定位问题发生的位置会更难
模板混淆
-
编译源码时自动进行代码混淆
-
《关于 C++ 模板元编程实现 C++ 代码的加密混淆》(Binary code obfuscation through C++ metaprogramming):首次基于编译期代码混淆技术的成果性的讨论和实践
-
C++ 的宏模板具有在编译时展开的特性,可将其运用到代码的混淆加密中
-
如使用 C 语言计算一个数的阶乘(factorial):
long Factorial(int n) { if (n == 0) { return 1; } return n * Factorial(n - 1); }
- 将上述代码编译成二进制程序,程序会在运行时计算某个数的阶乘。若用 C++ 的宏模板实现上述计算:
template<int N> struct Factorial
{
static const int value = N * Factorial<N-1>::value;
};
template<> struct Factorial<0>
{
static const int value = 1;
};
// std::cout << Factorial<6>::value << std::endl;
-
使用宏模板后,编译生成的二进制程序不再包含阶乘的代码调用,取而代之的是计算后的数值 720
-
使用宏模板也能高效实现线性同余生成器(Linear Congruential Generator),以便为加密算法生成伪随机数:
typedef unsigned int u32; typedef unsigned long long u64; template<u32 S, u32 A = 16807UL, u32 C = 0UL, u32 M = (1UL<<31)-1> struct LinearGegerator { static const u32 state = ((u64)S * A + C) % M; static const u32 value = state; typedef LinearGenerator<state> next; struct Split // Leapfrog { typedef LinearGenerator<state, A*A, 0, M> Gen1; typedef LinearGenerator<next::state, A*A, 0, M> Gen2; }; };
- 有了随机数生成器,只要扩展思路,配合代数恒等式
x+y==(x^y)+2(x&y)
的特点,即可实现加法运算的混淆加密。部分代码:
template<typename T, typename RandGen> struct ObfuscatedAdder
{
enum Adder_Method
{
Adder_Method_0,
// ...
Adder_Method_Count
};
template<typename Gen, u32 MutationIndex> struct Mutation;
template<typename Gen> struct Mutation<Gen, Adder_Method_0>
{
template<u32 N, typename D> struct Remaining
{
static inline T eval(const T a, const T b)
{
typedef Mutation<typename Gen::next, Gen::value%Adder_Method_Count> NextMutation;
return NextMutation::template Remaining<N-1, D>::eval(a^b, 2*(a&b));
}
};
template<typename D> struct Remaining<0, D>
{
static inline T eval(const T a, const T b)
{
return a^b; // no more carries to propagate, just xor
}
};
}; // ...
static inline T eval(const T a, const T b)
{
typedef typename Mutation<RandGen::next, RandGen::value%Adder_Method_Count> M;
return M::template Remaining<Bits<T>::value, NullType>::eval(a, b);
}
};
- 使用宏模板,不仅能对代码进行混淆,还能对数据进行混淆
AST 混淆
-
模板混淆技术只能应用于 C++,无法应用于其他语言
-
LLVM 采用模块化和分层的思想完成程序的构建,基于 LLVM 的编译器前端 Clang,同样采用这种思想
-
Clang 在编译程序时会对目标代码进行分析,生成结构化的树状语法表述。此表述方式通称抽象语法树(Abstract Syntax Tree,AST)
-
将程序的代码表示为 AST 给编译时的分析和优化带来极大便利。如,对高级语言而言,不同的类或名字空间中可能有相同的变量名,因此在重命名一个变量时不能简单地搜索变量名并将其替换(可能会对原程序的执行产生影响);若用 AST 将程序表示,替换变量名的操作将变得简单:因每个变量的定义都对应独一无二的抽象语法树节点,故只要改变对应变量用于声明 AST 节点的名称字段,把抽象语法树转换为原始代码即可
-
基于此种操作 AST 的思想,可实现和模板混淆相同的代码混淆,即 AST 混淆
-
Clang 以高度模块化的方式驱动,所有编译时功能都有供外部使用的接口,同时提供了操作 AST 的 API 接口。LLVM 在其文档的 Using Clang as a Library 部分指出将 Clang 作为库使用的三种方法:LibClang、ClangPlugins、LibTooling,它们的差异主要体现在代码编译期间能操作的对象上。LIbTooling 能执行的操作最多,故以其为例学习如何替换源码中的所有字符串
-
将 Clang 作为库使用时,要在程序中定义一个 ClangTool 对象,它将作为一个“内部”编译器操作源文件。构建该对象时,要传入一个命令行参数选项和两个操作目标文件的参数(可用 CommonOptionsParser 生成):
CommonOptionsParser op(argc, (const char**)argv, StatSampleCategory); ClangTool Tool(op.getCompilations(), op.g