oxorany
带有混淆的编译时任意常量加密
介绍
我们综合了开源项目
ollvm
、xorstr
一些实现思路,以及c++14
标准中新加入的constexpr
关键字和一些模板的知识,完成了编译时的任意常量的混淆(可选)
和加密功能。
在C++14之前,我们如果要对程序中的常量进行保护,我们首先对常量进行加密操作,这里以字符串
"some_data_or_string"
逐字节-1
为例,然后将加密后的数据"rnld^c`s`^nq^rsqhmf",写到代码里,同时进行逐字节+1
解密。
代码如下
char encrypted[] = {"rnld^c`s`^nq^rsqhmf"};
char key = 0x1;
for (size_t i = 0; i < strlen(encrypted); i++) {
encrypted[i] += key;
}
//output: some_data_or_string
printf("%s\n", encrypted);
上述的方法只能在需要被保护的数据的数量比较少时使用,当数据量增大,繁琐的加密过程所占用的时间也会水涨船高,而且使得代码的可读性、可维护性大大降低。而且不可能为每一个数据都单独设计一个解密算法和key,使得通用的解密工具更易于编写。
随着
oxorany
的出现,上述过程将被改变
🎨 特性
- 支持任意平台(
C++14
),已在所有诸多编译器中进行了测试 - 较高的可操作性,使用
__asm
_emit
可进一步提高逆向难度 - 所有的加密过程均在编译时完成
- 所有的解密过程均在栈内完成,无法通过运行时
dump
获得解密后的数据,不同于 Armariris、flounder - 带有
伪造控制流
功能的解密算法 - 通过
编译优化
为每一个加密算法生成唯一的控制流 - 通过
__COUNTER__
宏为每一个加密算法生成唯一的key
- 通过
__TIME__
宏动态产生key
- 代码经过精心编写,足以破坏堆栈以对抗
IDA
F5
- 基于堆栈变量的
不透明谓词
- 模糊数据长度
- 由于解密算法的大部分代码不会被执行,所以对于效率的影响并不会特别大
- 解密算法的复杂度仍有提升空间
- 因为
C++
中常量的隐式转换
特性,某些常量可能需要强制类型转换 - 相当简单的使用方法
想要内联
不能保证数据会被内联到代码段,支持的数据类型
- 字符串(
char*
wchar_t*
) - 宏
- 枚举
- 字符(
char
wchar_t
) - 指针(
NULL
nullptr
) - 整数(
int8_t
int16_t
int32_t
int64_t
uint8_t
uint16_t
uint32_t
uint64_t
) - 浮点(
float
double
)(会保留原数据)
支持的编译器
-
msvc
-
clang
(llvm
)(支持叠加ollvm) -
gcc
-
android ndk
(支持安卓) -
leetcode gcc
(支持类似的云编译器) -
wdk
(支持Windows驱动程序) -
...
🚀 使用
#include <iostream>
#define OXORANY_DISABLE_OBFUSCATION
//use OXORANY_USE_BIT_CAST for remove float double src data
#define OXORANY_USE_BIT_CAST
#include "oxorany.h"
enum class MyEnum : int {
first = 1,
second = 2,
};
#define NUM_1 1
int main() {
// output:
// 1 1 2 c w 00000000 00000000 12 1234 12345678 1234567887654321 1.000000 2.000000
// string u8 string wstring raw string raw wstring abcd
printf(oxorany("%d %d %d %c %C %p %p %hhx %hx %x %llx %f %lf\n%s %s %S %s %S %s\n") //string
, oxorany(NUM_1) //macro
, oxorany(MyEnum::first), oxorany(MyEnum::second) //enum
, oxorany('c') //char
, oxorany(L'w') //wchar_t
, oxorany(NULL), oxorany(nullptr) //pointer
, oxorany(0x12) //int8_t
, oxorany(0x1234) //int16_t
, oxorany(0x12345678) //int32_t
, oxorany(0x1234567887654321) //int64_t
, oxorany_flt(1.0f) //float
, oxorany_flt(2.0) //double
, oxorany("string") //string
, oxorany(u8"u8 string") //u8 string
, oxorany(L"wstring") //wstring
, oxorany(R"(raw string)") //raw string
, oxorany(LR"(raw wstring)") //raw wstring
, oxorany("\x61\x62\x63\x64") //binary data
);
return 0;
}
⚙️ 需要强制类型转换的示例
0 error 0 warning
MessageBoxA(0, 0, 0, 0);
错误(活动) E0167 "int" 类型的实参与 "HWND" 类型的形参不兼容
MessageBoxA(oxorany(0), 0, 0, 0);
出现上述问题的原因是因为
C/C++
中0
的特殊性,因为它可以隐式转换到任意类型的指针,也和NULL
的定义有关
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
所以我们添加一个到
HWND
的强制类型转换就可以解决该问题
MessageBoxA(oxorany((HWND)0), 0, 0, 0);
⚙️ 在wdk
中使用时需启用__TIME__
宏
IDA
中的控制流程图
编译优化
测试
这里是测试
编译优化
对控制流程图的影响,期望的结果是每一次编译都拥有不同的控制流程图
#include "oxorany.h"
int main() {
return oxorany(0);
}
✅ 使用msvc
多次编译后IDA
中的控制流程图
✅ 使用clang
多次编译后IDA
中的控制流程图
✅ 使用gcc
多次编译后IDA
中的控制流程图
✅ 使用android ndk
编译后IDA
中的控制流程图
✅ 使用leetcode gcc
进行测试 (剑指 Offer 05. 替换空格)
wdk
多次编译后IDA
中的控制流程图
✅ 使用
✅ 使用ollvm
编译后IDA
中的控制流程图
不透明谓词
不透明:
opaque
★
来自拉丁语opacus,有阴影的,黑暗的,模糊的。
谓词:
predicate
★
来自拉丁语praedicare,预测,断言,声称,来自prae,在前,早于,dicare,说,声称,词源同diction.并引申诸相关词义。
不透明谓词
可以理解为“无法确定结果的判断”
,词语本身并没有包含结果必为真或者必为假的含义,只是在这里使用了结果必为真的条件进行混淆。
代码中的
rand() % 2 == 0
实际上也是一个不透明谓词,因为我们无法确定它的结果,所以就无法确实程序是输出hello
还是输出world
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
srand((unsigned int)time(NULL));
if (rand() % 2 == 0) {
printf("hello\n");
}
else {
printf("world\n");
}
return 0;
}
但是换一种情况,这里我们创建了一个全局变量
zeor
,并赋初值为0
,不去修改zeor
的值或者在保证谓词结果恒定的情况下进行合理的修改,那么谓词zeor < 1
就是恒成立的,同时又由于全局变量的天然的不透明性,编译器不会进行优化,所以我们就增加一个伪造的控制流,无中生有
。我们可以在不可达的基本块内加入任意代码
,这里我们添加了一个典中典99乘法表
作为示例,暗度陈仓
。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int zeor = 0;
int main() {
if (zeor < 1) {
printf("hello\n");
}
else {
//unreachable
for (int i = 1; i <= 9; i++) {
for (int j = 1; j <= 9; j++) {
printf("%d*%d=%2d\t", i, j, i * j);
}
printf("\n");
}
}
return 0;
}
这里
copy
一下ollvm
中的代码,ASCII Picasso
// Before :
// entry
// |
// ______v______
// | Original |
// |_____________|
// |
// v
// return
//
// After :
// entry
// |
// ____v_____
// |condition*| (false)
// |__________|----+
// (true)| |
// | |
// ______v______ |
// +-->| Original* | |
// | |_____________| (true)
// | (false)| !-----------> return
// | ______v______ |
// | | Altered |<--!
// | |_____________|
// |__________|
//
// * The results of these terminator's branch's conditions are always true, but these predicates are
// opacificated. For this, we declare two global values: x and y, and replace the FCMP_TRUE
// predicate with (y < 10 || x * (x + 1) % 2 == 0) (this could be improved, as the global
// values give a hint on where are the opaque predicates)
ollvm
中全局x
、y
的定义
GlobalVariable * x = new GlobalVariable(M, Type::getInt32Ty(M.getContext()), false,
GlobalValue::CommonLinkage, (Constant * )x1,
*varX);
GlobalVariable * y = new GlobalVariable(M, Type::getInt32Ty(M.getContext()), false,
GlobalValue::CommonLinkage, (Constant * )y1,
*varY);
ollvm
中不透明谓词y < 10 || x * (x + 1) % 2 == 0
的实现,由Instruction::Sub
可知,虽然注释是x + 1
,但实际使用的确实x - 1
,问题不大,殊途同归
//if y < 10 || x*(x+1) % 2 == 0
opX = new LoadInst ((Value *)x, "", (*i));
opY = new LoadInst ((Value *)y, "", (*i));
op = BinaryOperator::Create(Instruction::Sub, (Value *)opX,
ConstantInt::get(Type::getInt32Ty(M.getContext()), 1,
false), "", (*i));
op1 = BinaryOperator::Create(Instruction::Mul, (Value *)opX, op, "", (*i));
op = BinaryOperator::Create(Instruction::URem, op1,
ConstantInt::get(Type::getInt32Ty(M.getContext()), 2,
false), "", (*i));
condition = new ICmpInst((*i), ICmpInst::ICMP_EQ, op,
ConstantInt::get(Type::getInt32Ty(M.getContext()), 0,
false));
condition2 = new ICmpInst((*i), ICmpInst::ICMP_SLT, opY,
ConstantInt::get(Type::getInt32Ty(M.getContext()), 10,
false));
op1 = BinaryOperator::Create(Instruction::Or, (Value *)condition,
(Value *)condition2, "", (*i));
将我们上面的代码稍作调整,以展示
ollvm
的实现,这里的x * (x + 1) % 2 == 0
,以为x
和x + 1
,必然是一个奇数一个偶数,根据奇偶性的运算法则可以得知x * (x + 1)
的结果必然是偶数,所以% 2 == 0
的判断将必然成立
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int x = 0;
int y = 0;
int main() {
if (y < 10 || x * (x + 1) % 2 == 0) {
printf("hello\n");
}
else {
//unreachable
for (int i = 1; i <= 9; i++) {
for (int j = 1; j <= 9; j++) {
printf("%d*%d=%2d\t", i, j, i * j);
}
printf("\n");
}
}
return 0;
}
实现
受到
ollvm
中伪造控制流
功能的启发,我们创建了两个全局变量x
、y
,并赋初值为0
,作为实现不透明谓词的基础
由于栈环境的复杂性,我们将全局变量
x
、y
分别赋值给两个局部变量stack_x
、stack_y
,以提高逆向的难度
我们在函数的很多位置创建了
label
,使用stack_x
、stack_y
进行恒为真的判断进行混淆,在无法到达的基本快内添加goto label
以尽可能得对基本块进行拆分。我们在多处对解密后的数据decrypted
使用错误的key
进行解密,使得真实的key
在众多的错误的key
中难以被识别,乱花渐欲迷人眼,浅草才能没马蹄
生成带有范围限制的随机数,因为这里可以出现相同的值,同时又因为编译优化的存在,重复的条件会被消除,这使得我们每一次的编译,都拥有不尽相同的控制流程图
我们在无法到达的基本快内加入非法的栈操作再加上代码中经过精心分配的控制流使得
IDA
的栈帧分析失败,以对抗F5
我们在将数据按
16
字节对齐并加上一定的随机值以模糊数据长度,这可能会浪费一点空间
我们在将
xor
替换为一种更加复杂的实现方式,以提高逆向的难度
使用
__TIME__
宏实现每一次编译都拥有唯一的key
带有范围限制的随机数产生器,使得
不透明谓词
相似于正常的谓词
综上所述,在
oxorany
的帮助下,软件的安全性将会得到进一步的提高