Getting Started
O-MVLL is an AArch64 obfuscator designed to work with Android and iOS toolchains.
Theoretically, it could be run as simply as using the -fpass-plugin=
compiler flag:
# 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
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
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 OMVLL.so
,
it fails with the following error:
$ clang -fpass-plugin=./OMVLL.so main.c -o main
Could not load library './OMVLL.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/24.0.8215888
for the version 24.0.8215888
.
clang
binary provided in the NDK is also linked with libc++.so
but we don’t need to manually provide the lib64
directory as it uses a RUNPATH
set to $ORIGIN/../lib64
.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 "25.0.8775105"
...
buildTypes {
release {
ndk.abiFilters 'arm64-v8a' // Force ARM64
externalNativeBuild {
cmake {
cppFlags '-fpass-plugin=<path>/OMVLL.so'
cFlags '-fpass-plugin=<path>/OMVLL.so'
}
}
}}}
There are important options associated with this configuration:
ndkVersion
must match the NDK version for which O-MVLL has been downloaded.ndk.abiFilters
must always be'arm64-v8a'
since O-MVLL only supports this architecture.
AArch64
, a released APK that only embeds arm64-v8a
native libraries
is a simple way to limit code emulation and code lifting.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=25.0.8775105
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.
o-config.py
, omvll.yml
, and omvll.env
in .gitignore
to avoid leaks.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
25.0.8775105 - omvll_ndk_r25.so
)
./sdkmanager --update
./sdkmanager "platforms;android-31" "build-tools;31.0.0" "ndk;25.0.8775105" "platform-tools"
Obfuscator related changes
build.gradle
- adjust path to obfuscator binary
omvll_ndk_r25.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_r25.so"
}
}
omvll.env
export NDK_VERSION=25.0.8775105
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
Requirements and Limitations
Cross Compilation
O-MVLL is currently tested and CI-compiled for the following configurations:
- Android NDK: (Linux Debian Stretch)
- iOS: macOS 14.5 (arm64 & x86-64)
Nightly Packages
There is a CI for O-MVLL. For all builds, the packages are nightly uploaded at the following addresses:
- Nightly: http://nightly.obfuscator.re/latest/omvll
- Experimental: https://open-obfuscator.build38.io/ci/index.html
- CI: https://github.com/open-obfuscator/o-mvll/actions
Thus, one can enjoy a beta version before waiting for a final release.
Environment Variables
Environment Variable | Description |
---|---|
OMVLL_PYTHONPATH | Path to the Python Standard Library (which contains abc.py ) |
OMVLL_CONFIG | Path to the O-MVLL Configuration file (default is ./omvll_config.py ) |
YAML Keys
Key | Description |
---|---|
OMVLL_PYTHONPATH | Path to the Python Standard Library (which contains abc.py ) |
OMVLL_CONFIG | Path 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"