Software Security

Obfuscation of Rust code with LLVM

Patrice Blin
|
-
|
Jul 2023
Back to all articles
SHARE

LLVM and Obfuscation-LLVM (OLLVM)

Back in the days, in 2010, the University of Applied Sciences and Arts Western Switzerland of Yverdon-les-Bains published a fork of LLVM providing code obfuscation. Sadly support for this project stopped in 2016 on LLVM 4.0

Last year we looked to revitalize this project by making it a pass-plugin library able to be build out-of-tree and keeping up to date with LLVM. Currently this project is available on github at eshard/obfuscator-llvm

The goals of this project are:

  • Obfuscate Rust code
  • Follow LLVM updates
  • Extend passes testing
  • Cross-compile
  • Make it easy to add new passes

One of the advantage of LLVM is its powerful intermediate representation (LLVM-IR) used for transformation and analysis during the compilation of source code (C, C++, Rust) to target binary (x86, ARM, RISC-V, Mips, …). Usually it’s used for optimizations and checking code correctness, but it also make it easy and language agnostic to implement obfuscation transformation.

As the Rust programming language is using LLVM we can leverage O-LLVM to provide code obfuscation with a shared library and few Cargo options.

In this article I will detail the steps to effectively obfuscate any Rust code.

Obfuscate Rust code

Recompile Rust’s LLVM fork

To compile our obfuscator we need LLVM headers compatible with the Rust version we are looking to use, in essence we will rebuild our own toolchain.
For this you need to setup Rust with rustup and the dependencies needed to rebuild LLVM.

For example on debian/ubuntu you need

sudo apt install python3 git build-essential curl cmake ninja-build pkg-config libssl-dev

Now choose a Rust version, currently the stable is on 1.79.0

rustc --version
# rustc 1.79.0 (129f3b996 2024-06-10)

Then clone the fork on the same tag.

mkdir ~/tmp && cd ~/tmp
git clone --depth 1 --branch 1.79.0 https://github.com/rust-lang/rust
cd rust
python3 x.py setup user # (this will setup the build env)

And edit config.toml to add theses options:

[llvm]
clang = true
link-shared = true
download-ci-llvm = false

You can check the other options available in config.example.toml (for example if you want build for multiple targets)

Now start the compilation of this new toolchain

python3 x.py build

Add custom toolchains to rustup

In the directory of the current build you will find stage1 and stage2 toolchains

ls build/x86_64-unknown-linux-gnu/
# llvm  stage0  stage0-rustc  stage0-std  stage0-sysroot  stage1  stage1-rustc  stage1-std  stage1-tools  stage2  stage2-tools  stage2-tools-bin
rustup toolchain link my-rust-stage1 $(readlink -f build/x86_64-unknown-linux-gnu/stage1)
rustup toolchain link my-rust-stage2 $(readlink -f build/x86_64-unknown-linux-gnu/stage2)

You can use either the stage1 or stage2 toolchain with respectively +my-rust-stage1 and +my-rust-stage2
We now need to build O-LLVM with the LLVM version we just rebuilt.

Build the obfuscation plugin

LLVM cmake files are located at build/x86_64-unknown-linux-gnu/llvm/lib/cmake

cd ~/tmp
git clone git@github.com:eshard/obfuscator-llvm.git
cd obfuscator-llvm
mkdir build && cd build
cmake -DLLVM_DIR=~/tmp/rust/build/x86_64-unknown-linux-gnu/llvm/lib/cmake ..
make -j8 && cp libLLVMObfuscator.so /tmp

Obfuscate Rust code

Within your project, you need to provide additional flags to use the newly built libLLVMObfuscator.so and select which passes you can to use.

Using Cargo, add the following rustflags inside .cargo/config

[target.x86_64-unknown-linux-gnu]
rustflags = [
   "-Z", "llvm-plugins=/tmp/libLLVMObfuscator.so",
   "-C", "passes=flattening bogus",
]

Lastly build your project with cargo

cargo +my-rust-stage1 build

Passes selection

When obfuscating code, you can insert passes (code modification) at different stages of the compilation to alter the generated LLVM-IR before it’s compiled to the target. To select which pass to apply at which stage ollvm is looking for environment variables,

For example, to apply the passes “bogus” and “flattening” at the VectorizerStart stage do

export LLVM_OBF_VECTORIZERSTART_PASSES="bogus, flattening"

You can also provide multiples stages and the same pass multiples times

export LLVM_OBF_PEEPHOLE_PASSES="split-basic-blocks, split-basic-blocks, flattening"
export LLVM_OBF_SCALAROPTIMIZERLATE_PASSES="bogus"

Note: adding more passes will lead to more transformations and will lead to higher memory consumption during compilation and slower execution

Exemple on Alloy

Searching at random on github for a well tested rust project I stumbled upon alloy-rs

cd ~/tmp
git clone https://github.com/alloy-rs/core.git
cd core && mkdir .cargo

cat << EOF  >> .cargo/config
[target.x86_64-unknown-linux-gnu]
rustflags = [
   "-Z", "llvm-plugins=/tmp/libLLVMObfuscator.so",
]
EOF

export LLVM_OBF_PEEPHOLE_PASSES="split-basic-blocks, split-basic-blocks, flattening"
export LLVM_OBF_SCALAROPTIMIZERLATE_PASSES="bogus"

cargo +my-rust-stage1 test