Building the right LLVM tool chain;
Below is a proof-of-concept recipe showing how to obfuscate a C++ program (SuperTux) with Tigress, provided to us by Bart Coppens. At some future date we hope to integrate the LLVM C backend with Tigress to automate this process.
We need five steps to transform the C++ code into C, and then obfuscate with Tigress:
Building the right LLVM tool chain;
Building the (patched/hacked) LLVM C Backend;
Compiling Supertux with the LLVM tool chain from (1) to create an LLVM Intermediate Representation of Supertux as a whole;
Running the patched LLVM C Backend on the LLVM IR from (3) to create a single C file;
Some gentle post-processing of the single C file of (4)
From here on we will assume that everything is done in the directory /projects
.
If you're using Docker, do the following:
docker pull debian:12 ;
docker run -v ${PWD}/projects:/projects -ti debian:12 bash
sed -i 's/deb$/deb deb-src/' /etc/apt/sources.list.d/debian.sources
apt-get update
apt-get build-dep supertux
apt-get install vim python3.11-venv
cd /projects
git clone https://url.usb.m.mimecastprotect.com/s/MexdCoAWMofr6Q610i1fgCYOGkE?domain=github.com
cd llvm-project
git switch release/17.x
mkdir llvm/build
cd llvm/build
cmake -DCMAKE_BUILD_TYPE=Release \
-DLLVM_ENABLE_DUMP=ON \
-DLLVM_BUILD_LLVM_DYLIB=ON \
-DLLVM_ENABLE_PROJECTS=clang \
-DLLVM_ENABLE_ASSERTIONS=ON ..
If you ever need to debug things, add -DMAKE_BUILD_TYPE=Debug
or
-DMAKE_BUILD_TYPE=RelWithDebInfo
. Then, build:
make -jnumber of CPU cores
cd /projects
git clone https://url.usb.m.mimecastprotect.com/s/h9m8Cp9WNpfnp7pJBSDh7CXcChH?domain=github.com
cd llvm-cbe
git switch development
mkdir build
cd build
export PATH=/projects/llvm-project/llvm/build/bin/:$PATH
cmake ..
make
If you get a build error here about undefined reference to `typeinfo for llvm::cl::GenericOptionValue'
,
add the following at the end of ../tools/llvm-cbe/CMakeLists.txt
:
if (NOT ${LLVM_ENABLE_RTTI})
target_compile_options(llvm-cbe PRIVATE "-fno-rtti")
target_link_options(llvm-cbe PRIVATE "-fno-rtti")
endif()
First, set up a python venv for wllvm. This is a tool that allows for creating objects and binaries with LLVM IR embedded inside them. This can be used for link-time optimization but also for our purposes:
cd /projects
python3 -m venv venv-wllvm
. venv-wllvm/bin/activate
pip3 install wllvm
Next, set up the aliases and exports such that wllvm works correctly. This still depends on the active venv and the PATH being set correctly from earlier:
export LLVM_COMPILER=clang
export LLVM_COMPILER_PATH=/projects/llvm-project/llvm/build/bin/
alias wllvm="wllvm -g"
Next, get Supertux. In this example, we built on a Debian 12 machine, which at the time had supertux-0.6.3. Get the dependencies to build it:
sudo apt-get build-dep supertux
cd /projects
apt-get source supertux
cd supertux-0.6.3/
mkdir build
cd build
Next, configure Supertux to build with wllvm (the other build options were based on those from Debian), and then build it:
CC=wllvm CXX=wllvm++ CFLAGS=-g CXXFLAGS=-g \
cmake -DBUILD_SHARED_LIBS=OFF \
-DCMAKE_BUILD_RPATH_USE_ORIGIN=ON \
-DENABLE_BOOST_STATIC_LIBS=OFF \
-DUSE_SYSTEM_PHYSFS=ON \
-DIS_SUPERTUX_RELEASE=ON ..
make -j number of CPU cores
This might output a few warnings, but you should now have a supertux2 binary. This binary will also contain info to get our LLVM IR file from, which we extract as follows:
extract-bc --manifest supertux2
This results in supertux2.bc
, which is the LLVM bitcode file with the IR in.
As the C backend starts from textual disassembly of that IR, we first disassemble it:
llvm-dis supertux2.bc
This results in supertux2.ll
. Finally, we prepare for running supertux later on:
make install
This will install the data files in /usr/local. However, if we build our merged file later on in the build directory and run it from there, it will expect those data files in the build directory. One possibility is to change the PREFIX of cmake where it installs, but instead we just do the following:
make install
mkdir -p /projects/supertux-0.6.3/build/share/games
cp -a /usr/local/share/games/supertux2 /projects/supertux-0.6.3/build/share/games/
Do the following:
/projects/llvm-cbe/build/tools/llvm-cbe/llvm-cbe supertux2.ll
This results in supertux2.cbe.c
.
The C backend helpfully adds #line
statements so that the lines of the C file
get mapped back to the original source files. This is probably useful when debugging a
crash that would also be in the original code, but definitely not useful in debugging
the case where it is the transpiled code that is broken. So first create a file without
the line statements:
grep -v '#line' supertux2.cbe.c > supertux2.cbe_noline.c
Create an object file:
gcc -g -Wno-discarded-qualifiers -std=c99 -o supertux2_cbe.o -c supertux2.cbe_noline.c
Now, here we arrive at a fragile issue: the exception handling hack. If the above code would not
compile due to errors related to 'cbe_get_landingpad'
that should return 'l_unnamed_1'
(or something similar), edit the supertux2.cbe_noline.c
and change
struct cbe_landingpad cbe_get_landingpad() {
struct cbe_landingpad r;
r.field1 = 1;
r.field0 = cbe_exception.o;
return r;
}
void __attribute__((noreturn))
__cbe_resume(struct cbe_landingpad l) __attribute__((noreturn)) {
longjmp(ExceptionStates[--jmp_idx].buf, 1);
}
into
struct l_unnamed_1 cbe_get_landingpad() {
struct l_unnamed_1 r;
r.field1 = 1;
r.field0 = cbe_exception.o;
return r;
}
void __attribute__((noreturn))
__cbe_resume(struct l_unnamed_1 l) {
longjmp(ExceptionStates[--jmp_idx].buf, 1);
}
Again, it might instead be something like l_unnamed_1
that you then have to use instead.
It might be that you have to remove the second __attribute__((noreturn))
regardless.
Similarly, if it complains about setjmp/longjmp
, edit the file as follows.
Search for and delete the following 4 lines:
uint32_t _setjmp(void* _147370) __ATTRIBUTELIST__((nothrow, returns_twice));
__noreturn void longjmp(void* _147371, uint32_t _147372) __ATTRIBUTELIST__((nothrow));
Note that the numbered argument names might differ.
Finally, link the file:
g++ -o supertux2_cbe supertux2_cbe.o \
-L/projects/supertux-0.6.3/build-wllvm-14.x/SDL_ttf/lib \
-Wl,-rpath,"\$ORIGIN/SDL_ttf/lib:" libsupertux2_lib.a \
/usr/lib/x86_64-linux-gnu/libboost_filesystem.so.1.74.0 \
/usr/lib/x86_64-linux-gnu/libboost_locale.so.1.74.0 \
/usr/lib/x86_64-linux-gnu/libboost_chrono.so.1.74.0 \
/usr/lib/x86_64-linux-gnu/libboost_thread.so.1.74.0 \
/usr/lib/x86_64-linux-gnu/libboost_atomic.so.1.74.0 \
-lSDL2 -lSDL2_image -lSDL2 -lSDL2_image SDL_ttf//lib/libSDL2_ttf.a \
-lfreetype -lharfbuzz -lfribidi -lraqm \
squirrel/ex/lib/libsquirrel_static.a \
squirrel/ex/lib/libsqstdlib_static.a \
tinygettext//lib/libtinygettext.a \
libsexp.a libsavepng.a -lpng \
libpartio_zip_lib.a \
-lz -lopenal -lvorbisfile -lvorbis -logg \
/usr/lib/x86_64-linux-gnu/libboost_system.so.1.74.0 \
/usr/lib/x86_64-linux-gnu/libboost_date_time.so.1.74.0 \
-lphysfs -lGL -lGLU \
/usr/lib/x86_64-linux-gnu/libGLEW.so \
-lcurl -latomic
This link statement is basically adapted from what you would get when linking the original Supertux binary, which you can find out with 'make VERBOSE=1'
Now you should be able to start ./supertux2_cbe
and it should run. Now, you can finally apply Tigress:
tigress \
--Verbosity=1 \
--Environment=x86_64:Linux:Gcc:12.2 \
--Transform=Virtualize \
--Functions=_ZN13ScreenManager9loop_iterEv \
--VirtualizeDispatch=direct \
--Transform=Flatten \
--Functions=_ZN13ScreenManager4drawER10CompositorRNS_9FPS_StatsE \
--Transform=EncodeArithmetic \
--Functions=main,_ZN4Main3runEiPPc \
--out=supertux2.cbe_noline_tigress_3_3_1_actuallyobfuscated.c \
supertux2.cbe_noline_atomicandmorehacks.c