0
votes

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.

2
Tried debugging? There is a nice dump() method for Instruction, BasicBlock and Function classes, so you can inspect the IR in the process of altering it. - arrowd
Actually, the function dump() has been removed in latest releases of LLVM, although "print()" can be used instead. It is helpful indeed. - plowsec

2 Answers

1
votes

The IRBuilder performs basic constant folding by default (as reported by the question author in his answer).

To disable constant folding in IRBuilder create it as follows.

IRBuilder<NoFolder> Builder;
0
votes

The problem here is IRBuilder, which performs Constant Folding by default upon creating the new IR instructions.

To solve this issue, I had to create a new volatile (volatile is not obligatory, but I could so I did) variable in IR, perform the "obfuscated" arithmetic operations on it, and replace the operand of the instruction that uses "1337" with the resulting value.

The code is the same as in the question, except for the function obfuscateInt(...), which now looks as follows:

  // replValue = ~(originalInt ^ key) -1
  Value *obfuscateInt(BasicBlock &BB, Instruction &Inst, Constant *C) {

    srand(time(NULL));
    int key = std::rand();
    int32_t replacedValue = ~(C->getUniqueInteger().getLimitedValue() ^ key);

    Constant *replValue = ConstantInt::get(C->getType(), replacedValue),
             *keyValue = ConstantInt::get(C->getType(), key);

    IRBuilder<> Builder(&Inst);

    // allocate enough space on the stack to store a 32-bit value. Var name = "AA"
    AllocaInst *varAlloc = Builder.CreateAlloca(Builder.getInt32Ty(), nullptr, "AA");

    // Store the key in AA, set "volatile" to true
    Builder.CreateStore(keyValue, varAlloc, true);

    // read the variable "AA"
    LoadInst *loadVar = Builder.CreateLoad(varAlloc, true, "AA");

    // use it
    Value *repl = Builder.CreateXor(replValue, loadVar);
    Value *finValue = Builder.CreateNeg(repl);

    return Builder.CreateSub(finValue, Builder.getInt32(1));
  }

The generated IR now looks like:

  %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 = alloca i32
  store volatile i32 525933950, i32* %4
  %5 = load volatile i32, i32* %4
  %6 = xor i32 -525932616, %5
  %7 = sub i32 0, %6
  %8 = sub i32 %7, 1
  %9 = add nsw i32 %3, %8
  store volatile i32 %9, i32* %2, align 4
  %10 = load volatile i32, i32* %2, align 4
  %11 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([41 x i8], [41 x i8]* @.str, i32 0, i32 0), i32 %10)
  %12 = load volatile i32, i32* %2, align 4
  ret i32 %12

And the disassembly shows that 1337 doesn't appear anywhere, yet the behaviour of the program is preserved:

0000000000001140 <main>:
    1140:       55                      push   rbp
    1141:       48 89 e5                mov    rbp,rsp
    1144:       48 83 ec 10             sub    rsp,0x10
    1148:       31 c0                   xor    eax,eax
    114a:       c7 45 fc 00 00 00 00    mov    DWORD PTR [rbp-0x4],0x0
    1151:       c7 45 f8 03 00 00 00    mov    DWORD PTR [rbp-0x8],0x3
    1158:       8b 4d f8                mov    ecx,DWORD PTR [rbp-0x8]
    115b:       c7 45 f4 02 77 c4 31    mov    DWORD PTR [rbp-0xc],0x31c47702
    1162:       8b 55 f4                mov    edx,DWORD PTR [rbp-0xc]
    1165:       81 f2 c4 8d 3b ce       xor    edx,0xce3b8dc4
    116b:       29 d0                   sub    eax,edx
    116d:       83 e8 01                sub    eax,0x1
    1170:       01 c1                   add    ecx,eax
    1172:       89 4d f8                mov    DWORD PTR [rbp-0x8],ecx
    ....