Building Zyrox: A Custom LLVM Obfuscator (Part IV — The Finale)
If you are here after reading the previous parts, welcome back! If not, you can find the earlier parts here:
The Finale
An obfuscator, is best when we can configure the level of evilness per function. Zyrox allows you to do just that, with its function attributes.
Even better: Zyrox uses quickjs as an embedded scripting engine, allowing you to write custom scripts to define how functions should be obfuscated.
Function Attributes
LLVM treats function attributes as metadata attached to functions. Zyrox uses this to define which obfuscation passes should be applied to each function. To keep things clean and simple, passes run by order. meaning if you specify cff, sibr, cff, Control-Flow Flattening will run twice, with SIBR in between.
We load these annotations once per module, at the start of the obfuscation process.
void ModuleUtils::ExpandCustomAnnotations(Module &m)
{
if (GlobalVariable *global_annotations =
m.getNamedGlobal("llvm.global.annotations"))
{
ConstantArray *annotations_array =
cast<ConstantArray>(global_annotations->getInitializer());
for (int i = 0; i < annotations_array->getNumOperands(); i++)
{
ConstantStruct *annotation_struct =
cast<ConstantStruct>(annotations_array->getOperand(i));
if (Function *f =
dyn_cast<Function>(annotation_struct->getOperand(0)))
{
GlobalVariable *annotation_str =
cast<GlobalVariable>(annotation_struct->getOperand(1));
if (isa<ConstantDataArray>(annotation_str->getInitializer()))
{
StringRef annotation = cast<ConstantDataArray>(
annotation_str->getInitializer())
->getAsCString();
AddPotentialPass(*f, annotation);
}
}
}
}
}expand codeEvery pass will first register it's own annotation:
class IndirectBranch
{
public:
static void RunOnFunction(Function &f, ZyroxPassOptions *options);
static void RegisterFromAnnotation(Function &f, ZyroxAnnotationArgs *args);
inline static ZyroxFunctionPass pass_info = {
.RunOnFunction = RunOnFunction,
.RegisterFromAnnotation = RegisterFromAnnotation,
.Name = "IndirectBranch",
.CodeName = "ibr",
};
private:
static void ObfuscateFunction(Function &f, int replace_br_chance);
};expand codeThen we simply use it like this:
__attribute__((annotate("mba:2 bbs:1,2,5,100 sibr:1,100")))
int XOR(int a, int b) {
return a ^ b;
}expand codeScripting with QuickJS
For who knows me, I love quickjs. It is a small, embeddable JavaScript engine that is easy to use and extend. Zyrox uses quickjs to allow users to write custom scripts to define how functions should be obfuscated.
The API I had in mind (and made), is simple:
/**
* @implements {ZyroxPlugin}
*/
class ZyroxPluginImpl {
RunOnFunction(Name) {
z.RegisterPass(ObfuscationType.BasicBlockSplitter, {
PassIterations: 1,
"BasicBlockSplitter.SplitBlockChance": 100,
"BasicBlockSplitter.SplitBlockMinSize": 2,
"BasicBlockSplitter.SplitBlockMaxSize": 5,
});
if (Name == "main") {
z.RegisterPass(ObfuscationType.ControlFlowFlattening, {
PassIterations: 2,
"ControlFlowFlattening.UseFunctionResolverChance": 60,
"ControlFlowFlattening.UseGlobalStateVariablesChance": 60,
"ControlFlowFlattening.UseOpaqueTransformationChance": 40,
"ControlFlowFlattening.UseGlobalVariableOpaquesChance": 80,
"ControlFlowFlattening.UseSipHashedStateChance": 40,
"ControlFlowFlattening.CloneSipHashChance": 80,
});
}
}
OnString(Str) {
if (Str.startsWith("secret")) {
return z.Stack;
}
return z.None;
}
Init() {
z.AddMetaData("somestringtobeaddedinthefinalbinaryforfun");
}
}
z.RegisterClass(new ZyroxPluginImpl());expand codeThen, we run RunOnFunction for every function in the module, passing the function name as an argument. Inside, we can register passes with custom options, based on the function name or any other criteria. Same goes for OnString, which is called for every string literal in the module, allowing us to define custom string obfuscation strategies.
Finally, Init is called once at the start of the obfuscation process, allowing us to add any metadata or perform any setup required.
for (Function &f : m) { if (f.isDeclaration()) continue; auto demangled = demangle(f.getName()); const char *name = demangled.c_str(); argv[0] = JS_NewString(ctx, name); current_function = &f; JSValue rv = JS_Call(ctx, js_run_on_function, config_class_thiz, 1, argv); JS_FreeValue(ctx, argv[0]); if (JS_IsUndefined(rv)) continue; if (JS_IsException(rv)) { JSValue exc = JS_GetException(ctx); JSValue str = JS_ToString(ctx, exc); const char *ptr = JS_ToCString(ctx, str); Logger::Warn("RunOnFunction returned an exception: {}", ptr); JS_FreeCString(ctx, ptr); JS_FreeValue(ctx, str); JS_FreeValue(ctx, exc); continue; } JS_FreeValue(ctx, rv); }expand code
Compiling Zyrox
For who reached this point, congratulations! You are now ready to compile Zyrox and start obfuscating your own projects.
You can do a fast run with:
clang -O0 -flto=full -c main.c -o out/main.o clang -flto=full -fuse-ld=lld -Wl,--load-pass-plugin=../build/libzyrox.so out/main.o -o out/main.soexpand code
Or, for a larger project, add it to your cmake:
set(ZYROX_PLUGIN "<your path here>/libzyrox.so" CACHE FILEPATH "Path to libzyrox.so plugin") file(REAL_PATH "${ZYROX_PLUGIN}" ZYROX_PLUGIN_ABS) target_link_options(target PRIVATE "-fuse-ld=/usr/bin/ld.lld" "-Wl,--load-pass-plugin=${ZYROX_PLUGIN_ABS}" )expand code
Do not forget to enable LTO in your cmake as well, to make zyrox effective:
target_compile_options(target PRIVATE -flto=full )expand code
and don't forget to run the post-compile python script if you are using encrypted jump tables. you can add it to cmake using add_custom_command after the linking step.
Star the Project
Seriously, why not? It helps a lot!
Contacts
I get this is a complex topic, and this project was mostly for educational purposes, as well as to serve BSD Brawl. If you have any question, or just want to chat, feel free to reach me out:
Discord:
@s.bEmail:
[email protected]or[email protected]
Bonus
String Encryption
By default, depending on config (if OnString returns anything but not z.None), Zyrox will encrypt strings on stack if they start with "/stack:", without the prefix itself.
It was designed like this to easily decide which strings to encrypt on stack, without having to annotate every string in the code, or mirroring them in JS config.
More like this
You can check previous blogs about Zyrox here.
And feel free to explore different blogs here.
... You can also check this page, which I wrote for a university project and fun.