Getting Started

O-MVLL is a code obfuscator based on LLVM and designed to work with Android and iOS toolchains. It supports AArch64 and AArch32 as target architectures. Theoretically, it could be run as simply as using the compiler flag -fpass-plugin=, as follows:

# Create/edit './omvll_config.py' to configure the obfuscator and run:
$ clang -fpass-plugin=OMVLL.{so, dylib} main.c -o main

Practically, there are additional configuration steps.

O-MVLL Configuration File

Firstly, the O-MVLL Python configuration file is not always located next to the clang binary and we might want to change the name of the file.

By default, O-MVLL tries to import omvll_config.py from the current directory in which clang is called. If this file can’t be resolved, it raises the following error:

...
error: ModuleNotFoundError: No module named 'omvll_config'
make: *** [Makefile:31: strings.bin] Error 1

To get rid of both limitations: the name of the Python file and the location of the file, one can set the OMVLL_CONFIG environment variable to the full path of your custom configuration file:

export OMVLL_CONFIG=~/project/obfu/config_test.py
clang -fpass-plugin=OMVLL.{so, dylib} main.c -o main

The O-MVLL configuration file must implements at least one function: omvll_get_config

import omvll

def omvll_get_config() -> omvll.ObfuscationConfig:
    """
    Return an instance of `ObfuscationConfig` which
    aims at describing the obfuscation scheme
    """

This function is called by the pass plugin to access the obfuscation scheme defined by the user. Since the instance of the configuration must be unique, we highly recommend wrapping this function with the @functools.lru_cache decorator:

import omvll
from functools import lru_cache

@lru_cache(maxsize=1)
def omvll_get_config() -> omvll.ObfuscationConfig:
    """
    Return an instance of `ObfuscationConfig` which
    aims at describing the obfuscation scheme
    """
    return MyConfig()

This decorator is used to get a singleton which simplifies the management of a global variable.

Then, the configuration of the obfuscations relies on implementing a class inheriting from omvll.ObfuscationConfig:

import omvll
from functools import lru_cache

class MyConfig(omvll.ObfuscationConfig):
    def __init__(self):
        super().__init__()

@lru_cache(maxsize=1)
def omvll_get_config() -> omvll.ObfuscationConfig:
    """
    Return an instance of `ObfuscationConfig` which
    aims at describing the obfuscation scheme
    """
    return MyConfig()

MyConfig is the class that contains all the logic to define and configure the obfuscation scheme. For instance, we can trigger the strings encoding pass by implementing the function obfuscate_string:

class MyConfig(omvll.ObfuscationConfig):
    def __init__(self):
        super().__init__()

    def obfuscate_string(self, module: omvll.Module, func: omvll.Function,
                               string: bytes):

        if func.demangled_name == "Hello::say_hi()":
            return True

        if "debug.cpp" in module.name:
            return "<REMOVED>"

        return False

Global Exclusions

You can configure global exclusion for both modules and functions inside MyConfig class:

Module Exclusion

As you may know, a module is a top-level container that represents a single unit of compilation. This means a module is each of the compile units you have (every .c, .cpp …).

omvll.config.global_mod_exclude = [excluded_module_1, excluded_module_2]

Function Exclusion

omvll.config.global_func_exclude = [excluded_function_1, excluded_function_2]

Conditional Obfuscation

Additionally, you can use the following helper function to decide whether to apply a given obfuscation pass to a given function:

omvll.ObfuscationConfig.default_config(self, module, func, [excluded_module_value], [excluded_function_value], [included_function_value], probability)

This function returns a boolean value indicating whether the obfuscation should be applied, based on a common algorithm:

  1. Returns False if the module name is in the excluded modules list.
  2. Returns False if the function name is in the excluded functions list.
  3. Returns True if the function name is in the included function list.
  4. Finally, if none of the conditions above are met, returns True with the probability passed in as the last parameter.

This allows users to easily force / skip the application of individual obfuscation passes to any given function or module, while at the same time applying a randomised approach to the functions that are not present in exclude / include lists.

Global excludes take precedence over local include lists.

class MyConfig(omvll.ObfuscationConfig):
    def __init__(self):
        super().__init__()

    def obfuscate_string(self, module: omvll.Module, func: omvll.Function,
                               string: bytes):

        return omvll.ObfuscationConfig.default_config(self, module, func, [], [], [], 50)

Python Standard Library

O-MVLL is statically linked with the Python VM. This static link allows us to not require a specific version of Python installed on the system. On the other hand, the Python VM requires a path to the directory where the Python Standard Library is installed (e.g. /usr/lib/python3.10/).

If the directory of the Python Standard Library can’t be resolved, O-MVLL will raise an error like this:

...
    '/cpython-install/lib/python310.zip',
    '/cpython-install/lib/python3.10',
    '/cpython-install/lib/lib-dynload',
  ]
Fatal Python error: init_fs_encoding: failed to get the Python codec of the filesystem encoding
Python runtime state: core initialized
ModuleNotFoundError: No module named 'encodings'

Current thread 0x00007f612cb48040 (most recent call first):
  <no Python frame>

If this error is triggered, we can download the Python source code associated with the version used in O-MVLL and set the environment variable OMVLL_PYTHONPATH to the Lib/ directory.

Here is an example with Python 3.10:

curl -LO https://www.python.org/ftp/python/3.10.7/Python-3.10.7.tgz
tar xzvf Python-3.10.7.tgz

export OMVLL_PYTHONPATH=$(pwd)/Python-3.10.7/Lib
clang -fpass-plugin=OMVLL.{so, dylib} main.c -o main

YAML Configuration File

Setting environment variables is not always easy, especially with IDE like Xcode and Android Studio.

For this reason, O-MVLL is also aware of an omvll.yml file that would be present in the directories from the root / to the current working directory.

For instance, if the compiler is called from:

/home/romain/dev/o-mvll/test/build

O-MVLL will check if a file omvll.yml is present in the following paths:

/home/romain/dev/o-mvll/test/build/omvll.yml
/home/romain/dev/o-mvll/test/omvll.yml
/home/romain/dev/o-mvll/omvll.yml
/home/romain/dev/omvll.yml
/home/romain/omvll.yml
/home/omvll.yml
/omvll.yml

If this file exists, it will load the following keys:

OMVLL_PYTHONPATH: "<mirror of $OMVLL_PYTHONPATH>"
OMVLL_CONFIG:     "<mirror of $OMVLL_CONFIG>"

Android NDK (Linux and MacOS)

The toolchain provided by the Android NDK is based on LLVM and linked with libc++. To avoid ABI issues, O-MVLL (and its dependencies) are also compiled and linked using libc++.

Most of the Linux distributions provide by default the GNU C++ standard library, aka libstdc++, and not the LLVM-based standard library, libc++.

Since libc++.so is not usually installed on the system, when clang tries to dynamically load , it fails with the following error:

$ clang -fpass-plugin=./omvll_ndk_r26d.so main.c -o main
Could not load library './omvll_ndk_r26d.so':
libc++abi.so.1: cannot open shared object file: No such file or directory

To prevent this error, we must add the NDK directory that contains libc++.so and libc++abi.so.1 in the list of the lookup directories. This can be done by setting the environment variable LD_LIBRARY_PATH:

LD_LIBRARY_PATH=<NDK_HOME>/toolchains/llvm/prebuilt/linux-x86_64/lib64

<NDK_HOME> is the root directory of the NDK. If the NDK is installed along with the Android SDK, it should be located in $ANDROID_HOME/ndk/26.3.11579264 for the version 26.3.11579264.

On macOS, you may encounter issues running NDK binaries like clang or clang++ from NDK 26.3.11579264 and omvll pass, due to System Integrity Protection (SIP) and hardened runtime restrictions.

Could not load library './OMVLL.dylib': Signature does not match

To resolve this, you must either:

Option 1: Disable SIP

You can fully disable SIP using:

csrutil disable

⚠️ This requires booting into macOS Recovery Mode and has significant security implications. Only use this method if you understand the risks.


Option 2: Code Sign NDK Tools

You can sign the NDK binaries with your own developer identity and entitlements:

Create an entitlements file

Save the following as myentitlements.entitlements:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>
    <key>com.apple.security.cs.allow-dyld-environment-variables</key>
    <true/>
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
    <key>com.apple.security.cs.disable-executable-page-protection</key>
    <true/>
</dict>
</plist>

Sign the NDK toolchain executables

Replace <identity> with your valid code signing identity (you can find it using security find-identity):

codesign --force --options runtime --verbose=4 -s <identity> \
  --entitlements myentitlements.entitlements \
  $ANDROID_HOME/ndk/26.3.11579264/toolchains/llvm/prebuilt/darwin-x86_64/bin/clang

codesign --force --options runtime --verbose=4 -s <identity> \
  --entitlements myentitlements.entitlements \
  $ANDROID_HOME/ndk/26.3.11579264/toolchains/llvm/prebuilt/darwin-x86_64/bin/clang++

After signing, the NDK tools should run correctly even with SIP enabled.

Gradle Integration

Within an Android project, we can setup O-MVLL by using the cppFlags, cFlags attributes in the ExternalNativeCmakeOptions DSL block:

android {
    compileSdkVersion 30
    ndkVersion        "26.3.11579264"
    ...
    buildTypes {
      release {
        ndk.abiFilters 'arm64-v8a' // Force ARM64
        externalNativeBuild {
          cmake {
            cppFlags '-fpass-plugin=<path>/omvll_ndk_r26d.so' // or omvll-ndk.dylib in MacOS systems
            cFlags   '-fpass-plugin=<path>/omvll_ndk_r26d.so' // or omvll-ndk.dylib in MacOS systems
          }
        }
}}}

There are important options associated with this configuration:

  • ndkVersion must match the NDK version for which O-MVLL has been downloaded.
  • ndk.abiFilters must be 'arm64-v8a' and/or 'armeabi-v7a', since O-MVLL supports these architectures.

In addition, we might need to satisfy the environment variables mentioned previously (LD_LIBRARY_PATH, OMVLL_CONFIG, …).

To expose these variables, we can create an environment file, omvll.env, that defines the variables and which is sourced before running Gradle or Android Studio:

# File: omvll.env
export NDK_VERSION=26.3.11579264
export LD_LIBRARY_PATH=${ANDROID_HOME}/ndk/${NDK_VERSION}/toolchains/llvm/prebuilt/linux-x86_64/lib64
export OMVLL_CONFIG=$(pwd)/app/o-config.py
export OMVLL_PYTHONPATH=$HOME/path/python/Python-3.10.7/Lib
source ./omvll.env
$ ./gradlew assembleRelease
# Or Android Studio:
$ studio.sh

In the end, the Android project might follow this layout:

.
├── app
│   ├── build.gradle
│   ├── o-config.py
│   └── src
├── build.gradle
├── gradle
│   └── wrapper
├── gradle.properties
├── gradlew
├── local.properties
├── omvll.env
└── settings.gradle

Alternatively, you could also create an omvll.yml file next to the omvll.env but the LD_LIBRARY_PATH still needs to be set.

Android NDK (WSL)

Preparing the WSL for commandline Android development

Based on this article WSL for Developers!: Installing the Android SDK

Installing OpenJDK and Gradle

sudo apt-get update
sudo apt install openjdk-8-jdk-headless gradle
export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64

Installing Android Command Line Tools

cd ~ # Make sure you are at home!
curl https://dl.google.com/android/repository/commandlinetools-linux-8512546_latest.zip -o /tmp/cmd-tools.zip
mkdir -p android/cmdline-tools
unzip -q -d android/cmdline-tools /tmp/cmd-tools.zip
mv android/cmdline-tools/cmdline-tools android/cmdline-tools/latest
rm /tmp/cmd-tools.zip # delete the zip file (optional)

Setting up environment variables

You could possibly join include these lines in omvll.env file:

export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
export ANDROID_HOME=$HOME/android
export ANDROID_SDK_ROOT=${ANDROID_HOME}
export PATH=${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/tools:${ANDROID_HOME}/tools/bin:${PATH}

Accepting SDK licenses

You will find sdkmanager in /tools/bin/sdkmanager:

yes | sdkmanager --licenses

Installing SDK components

  • pay attention to use ndk version matching the downloaded obfuscator (I used - omvll_ndk_r26d.so)
./sdkmanager --update
./sdkmanager "platforms;android-31" "build-tools;31.0.0" "ndk;26.3.11579264" "platform-tools"

build.gradle

  • adjust path to obfuscator binary omvll_ndk_r26d.so, change ’tom’ to your username:
externalNativeBuild {
    cmake {
        cppFlags "-std=c++14 -frtti -fexceptions
                  -fpass-plugin=/mnt/c/Users/tom/path-to-project/omvll_ndk_r26d.so"
    }
}

omvll.env

export NDK_VERSION=26.3.11579264
export LD_LIBRARY_PATH=/home/tom/android/ndk/${NDK_VERSION}/toolchains/llvm/prebuilt/linux-x86_64/lib64
export OMVLL_CONFIG=/mnt/c/Users/tom/path-to-project/omvll-config.py
export OMVLL_PYTHONPATH=/mnt/c/Users/tom/path-to-project/Python-3.10.7/Lib

local.properties

I needed to adjust the line with sdk.dir in the local.properties file:

...
sdk.dir=/home/tom/android
...

Troubleshooting

I ran into this issue when running gradlew,

env: bash\r: No such file or directory

The following change helped me:

vim gradlew
:set fileformat=unix
:wq

Finally:

gradlew clean build, gradlew assembleRelease, or whatever you like :)

iOS

Using O-MVLL with Xcode is a bit easier than Android since we don’t need to deal with different libstdc++/libc++. To enable O-MVLL, one needs to set the following in Xcode:

Build Settings > Apple Clang - Custom Compiler Flags > Other C/C++ Flags

and add -fpass-plugin=<path>/omvll_xcode_15_2.dylib. For versions targeting Xcode 14.5 and lower, the legacy pass manager needs to be disabled as well via -fno-legacy-pass-manager.

Finally, we can create an omvll.yml file next to the *.xcodeproj file which defines OMVLL_PYTHONPATH and OMVLL_CONFIG.

Et voila :)

OMVLL_PYTHONPATH: "/Users/romain/Downloads/Python-3.10.8/Lib"
OMVLL_CONFIG:     "/Users/romain/dev/ios-app/demo/omvll_conf/base.py"

Code Completion

The PyPI package omvll can be used to get code completion while using O-MVLL:

$ python -m pip install [--user] omvll

O-MVLL Code Completion

Requirements and Limitations

Cross Compilation

O-MVLL is currently tested and CI-compiled for the following configurations:

  • Android NDK: Linux Debian Stretch and macOS 15.4 (arm64 & x86-64)
  • iOS: macOS 15.4 (arm64 & x86-64)
In particular, we did not test and we do not provide O-MVLL for cross-compiling Android projects on Windows.

CI Packages

There is a CI for O-MVLL. For all builds, the packages are uploaded at the following addresses:

Thus, one can enjoy a beta version before waiting for a final release.

Environment Variables

Environment VariableDescription
OMVLL_PYTHONPATHPath to the Python Standard Library (which contains abc.py)
OMVLL_CONFIGPath to the O-MVLL Configuration file (default is ./omvll_config.py)

YAML Keys

KeyDescription
OMVLL_PYTHONPATHPath to the Python Standard Library (which contains abc.py)
OMVLL_CONFIGPath to the O-MVLL Configuration file (default is ./omvll_config.py)
Example:
OMVLL_PYTHONPATH: "<path>/Python-3.10.8/Lib"
OMVLL_CONFIG:     "<path>/myconfig.py"