We integrated some implementation ideas from open source projects
ollvm
andxorstr
, as well as the newconstexpr
keyword in thec++14
standard and some template knowledge, to complete the compile-time obfuscation encryption of arbitrary constants.
Before C++14, if we want to protect the constants in the program, we first encrypt the constants, here we take the string
"some_data_or_string"
byte by byte-1
as an example, and then write the encrypted data "rnld^c`s`^nq^rsqhmf" to the code, while doing byte by byte+1
decryption.
Code show as below
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);
The above method can only be used when the amount of data to be protected is relatively small, and when the amount of data increases, the time taken by the tedious encryption process will also rise, and it makes the readability and maintainability of the code greatly reduced. And it is not possible to design a separate decryption algorithm and key for each data, which makes a general decryption tool easier to write.
With the advent of
oxorany
, the above process will be changed
- Support any platform(
C++14
) - Higher operability, using
__asm
_emit
can further increase the difficulty of reverse - Obfuscated encryption of any constants at compile time
- All the decryption process is done inside the stack, and the decrypted data cannot be obtained through runtime
dump
,unlike Armariris、flounder - Decryption algorithm with
Bogus Control Flow
likeollvm
- Generate a unique control flow for each encryption algorithm through
compile optimization
- Generate a unique
key
for each encryption algorithm with the__COUNTER__
macro - Dynamically generate
key
via the__TIME__
macro - The code has been crafted so that can Destroying the stack to anti
IDA
F5
- Stack variable based
Opaque Predicate
- Fuzzy data length
- Since most of the code for the decryption algorithm is not executed, the impact on efficiency is not particularly significant
- The complexity of the decryption algorithm can be improve
- Because of the
implicit conversion
feature of constants inC++
, some constants may require forced type conversion - Easy to use, tested in
msvc
,clang
,gcc
- There is no guarantee that the data will be embedded directly into code,Want embedded
- String(
char*
wchar_t*
) - Macro
- Enume
- Integer(
int8_t
int16_t
int32_t
int64_t
uint8_t
uint16_t
uint32_t
uint64_t
) - Floating point(
float
double
)
-
msvc
-
clang
(llvm
) -
gcc
-
android ndk
-
leetcode gcc
-
...
#include <iostream>
//#define OXORANY_DISABLE_OBFUSCATION
#include "oxorany.h"
enum class MyEnum : int {
first = 1,
second = 2,
};
#define NUM_1 1
int main() {
// output:
// 1 1 2 12 1234 12345678 1234567887654321 1.000000 2.000000
// string wstring raw string raw wstring
printf(oxorany("%d %d %d %hhx %hx %x %llx %f %lf\n%s %S %s %S\n") //string
, oxorany(NUM_1) //macro
, oxorany(MyEnum::first), oxorany(MyEnum::second) //enum
, oxorany((uint8_t)0x12) //uint8_t
, oxorany((uint16_t)0x1234) //uint16_t
, oxorany((uint32_t)0x12345678) //uint32_t
, oxorany((uint64_t)0x1234567887654321) //uint64_t
, oxorany(1.0f) //float
, oxorany(2.0) //double
, oxorany("string") //string
, oxorany(L"wstring") //wstring
, oxorany(R"(raw string)") //raw string
, oxorany(LR"(raw wstring)") //raw wstring
);
return oxorany(0);
}
0 error 0 warning
MessageBoxA(0, 0, 0, 0);
error(active) E0167 Real parameters of type "int" are incompatible with formal parameters of type "HWND"
MessageBoxA(oxorany(0), 0, 0, 0);
The reason for the above problem is due to the peculiarity of
0
inC/C++
, because it can be implicitly converted to a pointer of any type, and is also related to the definition ofNULL
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
So we add a forced type conversion to
HWND
to solve the problem
MessageBoxA(oxorany((HWND)0), 0, 0, 0);
#include "oxorany.h"
int main() {
return oxorany(0);
}
✅ Testing with leetcode gcc
(Sword Pointing Offer 05. replace space)
The
opaque predicate
can be understood as"the judgment of the result cannot be determined"
,The words themselves do not contain the meaning that the result must be true or must be false, but only the condition that the result must be true is used here for obfuscation
The
rand() % 2 == 0
in the code is actually an opaque predicate, because we can't determine its result, so we can't be sure whether the program is outputtinghello
orworld
#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;
}
But in another case, here we create a global variable
zeor
and assign an initial value of0
, without modifying the value ofzeor
or making reasonable modifications to ensure that the result of the predicate is constant, then the predicatezeor < 1
is constant, and at the same time the compiler will not optimize due to the natural opacity of global variables, so we add a forged to the control flow. We can addany code
inside an unreachable basic block, and here we add a99 multiplication table
as an example.。
#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;
}
Here
copy
the code fromollvm
,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)
Definition of global
x
,y
in ollvm`
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);
The implementation of the opaque predicate
y < 10 || x * (x + 1) % 2 == 0
inollvm
is shown byInstruction::Sub
, which, although annotated withx + 1
, actually usesx - 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));
Adjusting our code above slightly to show the implementation of
ollvm
, herex * (x + 1) % 2 == 0
, thinking thatx
andx + 1
, must be an odd number and an even number, according to the operation of parity we can learn that the result ofx * (x + 1)
must be even, so the judgment of% 2 == 0
will necessarily hold
#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;
}
Inspired by the
Bogus Control Flow
function inollvm
, we created two global variablesx
andy
and assigned initial values of0
as the basis for implementing opaque predicates
Due to the complexity of the stack environment, we assign the global variables
x
andy
to two local variablesstack_x
andstack_y
respectively to make the inverse more difficult
We created
label
in many positions of the function, usedstack_x
,stack_y
to judge the constant to be true for confusion, and addedgoto label
in the basic block that could not be reached to remove the basic block as much as possible point. We use the wrong key to decrypt the decrypted datadecrypted
in many places, so that the real key is difficult to identify among the many wrong keys.
Generate random numbers with range limition, since the same values can occur here, while duplicate conditions are removed by compilation optimization because of the existence of compilation optimization, which makes us have a different control flow diagram for each compilation
We add illegal stack operations within the unreachable basic fast to make
IDA
's stack frame analysis fail againstF5
We are aligning the data by
16
bytes and adding a certain random value to obscure the data length, which may waste a little space
We are replacing
xor
with a more complex implementation to make the inversion more difficult
Use the
__TIME__
macro to implement a differentkey
for each compilation
Random number generator with range restrictions, making
opaque predicates
similar to normalpredicates
To sum up, with the help of
oxorany
, the security of the software will be further improved