我们综合了开源项目
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
)(
会保留原数据
)
clang
(
llvm
)(
支持叠加ollvm
)
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);
这里是测试编译优化
对控制流程图的影响,期望的结果是每一次编译都拥有不同的控制流程图
#include "oxorany.h"
int main() {
return oxorany(0);
✅ 使用leetcode gcc
进行测试 (剑指 Offer 05. 替换空格)
不透明: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
,作为实现不透明谓词的基础