/oxorany

obfuscated any constant encryption in compile time on any platform

Primary LanguageC++MIT LicenseMIT

带有混淆的编译时任意常量加密

LICENSE

介绍

我们综合了开源项目ollvmxorstr一些实现思路,以及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获得解密后的数据,不同于 Armaririsflounder
  • 带有伪造控制流功能的解密算法
  • 通过编译优化为每一个加密算法生成唯一的控制流
  • 通过__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__

image


IDA中的控制流程图

image


编译优化测试

这里是测试编译优化对控制流程图的影响,期望的结果是每一次编译都拥有不同的控制流程图

#include "oxorany.h"
int main() {
	return oxorany(0);
}

✅ 使用msvc多次编译后IDA中的控制流程图

image


✅ 使用clang多次编译后IDA中的控制流程图

image


✅ 使用gcc多次编译后IDA中的控制流程图

image


✅ 使用android ndk编译后IDA中的控制流程图

image


✅ 使用leetcode gcc进行测试 (剑指 Offer 05. 替换空格)

S5(LFNXH~_KM6UH@L}U(CY6


✅ 使用wdk多次编译后IDA中的控制流程图

image


✅ 使用ollvm编译后IDA中的控制流程图

image


不透明谓词

不透明: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中全局xy的定义

      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,以为xx + 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伪造控制流功能的启发,我们创建了两个全局变量xy,并赋初值为0,作为实现不透明谓词的基础

image


由于栈环境的复杂性,我们将全局变量xy分别赋值给两个局部变量stack_xstack_y,以提高逆向的难度

image


我们在函数的很多位置创建了label,使用stack_xstack_y进行恒为真的判断进行混淆,在无法到达的基本快内添加goto label以尽可能得对基本块进行拆分。我们在多处对解密后的数据decrypted使用错误的key进行解密,使得真实的key在众多的错误的key中难以被识别,乱花渐欲迷人眼,浅草才能没马蹄

image


生成带有范围限制的随机数,因为这里可以出现相同的值,同时又因为编译优化的存在,重复的条件会被消除,这使得我们每一次的编译,都拥有不尽相同的控制流程图

image


我们在无法到达的基本快内加入非法的栈操作再加上代码中经过精心分配的控制流使得IDA的栈帧分析失败,以对抗F5

image


我们在将数据按16字节对齐并加上一定的随机值以模糊数据长度,这可能会浪费一点空间

image


我们在将xor替换为一种更加复杂的实现方式,以提高逆向的难度

image


使用__TIME__宏实现每一次编译都拥有唯一的key

image


带有范围限制的随机数产生器,使得不透明谓词相似于正常的谓词

image


综上所述,在oxorany的帮助下,软件的安全性将会得到进一步的提高

参考

Github

https://github.com/llxiaoyuan/oxorany