After having read this nice article from Quarkslab on obfuscating zeroes, I thought I would go ahead a tweak it a bit to obfuscate arbitrary integer constants.
However, it appears that my pass is ignored or hasn't any effects on the resulting LLVM bitcode (or even the binary executable).
The simple obfuscation works as follows: a random int is generated, then the constant to hide is XORed with this key. Two's complement is applied on the result.
This produces an integer, that it then computed to its original value by emitting the required LLVM bitcode.
Here is my PoC (adapted from 1):
#include "llvm/IR/Constants.h"
#include "llvm/IR/DataLayout.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/GlobalVariable.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/IntrinsicInst.h"
#include "llvm/IR/LegacyPassManager.h"
#include "llvm/IR/Module.h"
#include "llvm/Pass.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/Transforms/IPO/PassManagerBuilder.h"
#include "llvm/Transforms/Utils/BasicBlockUtils.h"
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <sstream>
using namespace llvm;
namespace {
class MyPass : public BasicBlockPass {
public:
static char ID;
MyPass() : BasicBlockPass(ID) {}
bool runOnBasicBlock(BasicBlock &BB) override {
bool modified = false;
for (typename BasicBlock::iterator I = BB.getFirstInsertionPt(),
end = BB.end();
I != end; ++I) {
Instruction &Inst = *I;
if (!isValidCandidateInstruction(Inst))
continue;
for (size_t i = 0; i < Inst.getNumOperands(); ++i) {
if (Constant *C = isValidCandidateOperand(Inst.getOperand(i))) {
std::stringstream stream;
stream << std::hex << C->getUniqueInteger().getLimitedValue();
std::string result(stream.str());
errs() << "Found an integer: 0x" << result << "\n";
if (C->getUniqueInteger().getLimitedValue() == 1337) {
errs() << "Obfuscating constant 1337\n";
if (Value *New_val = obfuscateInt(Inst, C)) {
Inst.setOperand(i, New_val);
modified = true;
errs() << "Replaced with " << New_val << "\n";
} else {
errs() << "ObfuscateZero: could not rand pick a variable for "
"replacement\n";
}
}
}
}
}
return modified;
}
// replValue = ~(originalInt ^ key) -1
Value *obfuscateInt(Instruction &Inst, Constant *C) {
srand(time(NULL));
int key = std::rand();
int64_t replacedValue = ~(C->getUniqueInteger().getLimitedValue() ^ key);
Constant *replValue = ConstantInt::get(C->getType(), replacedValue),
*keyValue = ConstantInt::get(C->getType(), key);
IRBuilder<> Builder(&Inst);
Value *repl = Builder.CreateXor(replValue, keyValue);
Value *finValue = Builder.CreateNeg(repl);
return Builder.CreateSub(finValue, ConstantInt::get(C->getType(), 1));
}
// only interested in integer values
Constant *isValidCandidateOperand(Value *V) {
Constant *C;
if (!(C = dyn_cast<Constant>(V)))
return nullptr;
if (!C->getType()->isIntegerTy()) {
return nullptr;
}
return C;
}
bool isValidCandidateInstruction(Instruction &Inst) {
if (isa<GetElementPtrInst>(&Inst)) {
errs() << "Ignoring GEP\n";
return false;
} else if (isa<SwitchInst>(&Inst)) {
errs() << "Ignoring Switch\n";
return false;
} else if (isa<CallInst>(&Inst)) {
errs() << "Ignoring Calls\n";
return false;
} else {
return true;
}
}
};
} // namespace
char MyPass::ID = 0;
static RegisterPass<MyPass> X("MyPass", "Obfuscates 1337", true, false);
// register pass for clang use
static void registerMyPassPass(const PassManagerBuilder &,
llvm::legacy::PassManagerBase &PM) {
PM.add(new MyPass());
}
static RegisterStandardPasses
RegisterMBAPass(PassManagerBuilder::EP_OptimizerLast, registerMyPassPass);
And the simple test program:
int main(void)
{
volatile int a = 3;
a += 1337;
return a;
}
I compile the LLVM pass as follows:
clang -g3 -shared -fPIC MyPass.cpp -o pass/MyPass.so
Then I run the pass on the LLVM bitcode of the aforementioned simple test:
opt -S -load pass/MyPass.so -MyPass bin/simple_test.ll -o bin/out.ll
The content of bin/out.ll is the same as bin/simple_test.ll, which is obviously the opposite of what I want:
; ModuleID = 'bin/simple_test.ll'
source_filename = "tests/simple_test.c"
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"
; Function Attrs: noinline nounwind optnone sspstrong uwtable
define dso_local i32 @main() #0 {
%1 = alloca i32, align 4
%2 = alloca i32, align 4
store i32 0, i32* %1, align 4
store volatile i32 3, i32* %2, align 4
%3 = load volatile i32, i32* %2, align 4
%4 = add nsw i32 %3, 1337
store volatile i32 %4, i32* %2, align 4
%5 = load volatile i32, i32* %2, align 4
ret i32 %5
}
attributes #0 = { noinline nounwind optnone sspstrong uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
!llvm.module.flags = !{!0, !1, !2}
!llvm.ident = !{!3}
!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{i32 7, !"PIC Level", i32 2}
!2 = !{i32 7, !"PIE Level", i32 2}
!3 = !{!"clang version 7.0.1 (tags/RELEASE_701/final)"}
Of course, I thought that the compiler optimized away my small obfuscation attempt, but after having manually applied the small transformation to the test program, I could see the additional XOR, NEG and SUB operations in the resulting disassembly, which makes me think the optimizer is not at fault here.
I'm interested in a proof-of-concept where the constant 1337 is (slightly) "hidden", just for the sake of it. Less interested in comments saying that obfuscation is futile, or pointing out things that you don't like in the code that are irrelevant to the question.
dump()method forInstruction,BasicBlockandFunctionclasses, so you can inspect the IR in the process of altering it. - arrowd