-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathindex.json
More file actions
1 lines (1 loc) · 247 KB
/
index.json
File metadata and controls
1 lines (1 loc) · 247 KB
1
[{"authors":null,"categories":null,"content":"O-LLVM is the first public LLVM-based obfuscator that was released in 2015 before becoming the strong.code\u0026rsquo;s product and eventually being bought by Snapchat.\n","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1718355017,"objectID":"ce3744327a89e5bf750d486217d244cf","permalink":"https://obfuscator.re/acknowledgements/o-llvm/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/acknowledgements/o-llvm/","section":"acknowledgements","summary":"O-LLVM is the first public LLVM-based obfuscator that was released in 2015 before becoming the strong.code\u0026rsquo;s product and eventually being bought by Snapchat.\n","tags":null,"title":"O-LLVM","type":"acknowledgements"},{"authors":null,"categories":null,"content":"Getting Started Since dProtect is based on Proguard, it relies on the same integration mechanism.\nIn particular, there is this official documentation on the Guardsquare\u0026rsquo;s website: ProGuard manual: Quick Start\nCompared to the Proguard integration, there are a few differences for using dProtect in your project.\nAndroid Gradle project Within an Android Gradle project, one can integrate dProtect by first adding the Github\u0026rsquo;s Maven repository associated to dProtect:\nbuildscript { repositories { mavenCentral() maven { url = uri(\u0026#34;https://maven.pkg.github.com/open-obfuscator/dProtect\u0026#34;) } } dependencies { classpath \u0026#39;re.obfuscator:dprotect-gradle:1.0.0\u0026#39; } } Even if the packages are public, you need to generate a github token to access GitHub Maven Packages:\nbuildscript { repositories { mavenCentral() maven { url = uri(\u0026#34;https://maven.pkg.github.com/open-obfuscator/dProtect\u0026#34;) credentials { username = \u0026#34;your-username\u0026#34; password = \u0026#34;the-token\u0026#34; } } } dependencies { classpath \u0026#39;re.obfuscator:dprotect-gradle:1.0.0\u0026#39; } } The generated token must have the repo scope enabled. You can also look at the GitHub documentation for the details: Using a published package.\nThen, in the build.gradle of the application, we can instantiate the Gradle plugin and define the dProtect configuration block:\napply plugin: \u0026#39;com.android.application\u0026#39; apply plugin: \u0026#39;re.obfuscator.dprotect\u0026#39; android { compileSdkVersion 30 buildTypes { release { minifyEnabled false // Prevent using r8 } debug { minifyEnabled false // Prevent using r8 } } dProtect { configurations { release { defaultConfiguration \u0026#39;proguard-android-optimize.txt\u0026#39; defaultConfiguration \u0026#39;proguard-android.txt\u0026#39; configuration \u0026#39;proguard-rules.pro\u0026#39; configuration \u0026#39;dprotect-rules.pro\u0026#39; } debug { defaultConfiguration \u0026#39;proguard-android.txt\u0026#39; configuration \u0026#39;proguard-rules.pro\u0026#39; } } } The dProtect configuration block is exactly the same as the proguard{...} block. This block has been renamed from proguard to dProtect to avoid conflicts and errors.\nThe *.pro files referenced in the configuration block follow the same syntax as Proguard and we can transparently use the original Proguard files since dProtect is \u0026ndash; first and foremost \u0026ndash; an extension of Proguard.\nMaven Central dProtect packages are currently only hosted on the Github\u0026rsquo;s Maven repository but they will also be uploaded on Maven Central once the integration model is validated. Standalone dProtect comes also as a standalone package that can be used independently of Android, Gradle, and its plugin. Typically, we can run dProtect on a .jar archive as follows:\n$ dprotect.sh \\ -injars ./my-sdk.jar \\ -outjar ./my-obfuscated-sdk.jar \\ @./rules/dprotect.pro This standalone run can be useful to protect a third-party SDK for which we only have the .aar/.jar archive.\nIndeed, since dProtect/ProGuard are working on the Java bytecode to obfuscate and optimize the code, we don\u0026rsquo;t need the original source code of the archive to apply an obfuscation scheme.\nDownload You can download the standalone archive on the release page of dProtect: open-obfuscator/dprotect/releases\nYou can also download the nightly package here: http://nightly.obfuscator.re/latest/dprotect\n","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"0405747e68cf92c5d3bc537e134a240e","permalink":"https://obfuscator.re/dprotect/introduction/getting-started/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/dprotect/introduction/getting-started/","section":"dprotect","summary":"Getting Started Since dProtect is based on Proguard, it relies on the same integration mechanism.\nIn particular, there is this official documentation on the Guardsquare\u0026rsquo;s website: ProGuard manual: Quick Start\nCompared to the Proguard integration, there are a few differences for using dProtect in your project.\nAndroid Gradle project Within an Android Gradle project, one can integrate dProtect by first adding the Github\u0026rsquo;s Maven repository associated to dProtect:\n","tags":null,"title":"Getting started","type":"dprotect"},{"authors":null,"categories":null,"content":"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:\n# Create/edit \u0026#39;./omvll_config.py\u0026#39; to configure the obfuscator and run: $ clang -fpass-plugin=OMVLL.{so, dylib} main.c -o main Practically, there are additional configuration steps.\nO-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.\nBy default, O-MVLL tries to import omvll_config.py from the current directory in which clang is called. If this file can\u0026rsquo;t be resolved, it raises the following error:\n... error: ModuleNotFoundError: No module named \u0026#39;omvll_config\u0026#39; 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:\nexport 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\nimport omvll def omvll_get_config() -\u0026gt; omvll.ObfuscationConfig: \u0026#34;\u0026#34;\u0026#34; Return an instance of `ObfuscationConfig` which aims at describing the obfuscation scheme \u0026#34;\u0026#34;\u0026#34; 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:\nimport omvll from functools import lru_cache @lru_cache(maxsize=1) def omvll_get_config() -\u0026gt; omvll.ObfuscationConfig: \u0026#34;\u0026#34;\u0026#34; Return an instance of `ObfuscationConfig` which aims at describing the obfuscation scheme \u0026#34;\u0026#34;\u0026#34; return MyConfig() This decorator is used to get a singleton which simplifies the management of a global variable.\nThen, the configuration of the obfuscations relies on implementing a class inheriting from omvll.ObfuscationConfig:\nimport omvll from functools import lru_cache class MyConfig(omvll.ObfuscationConfig): def __init__(self): super().__init__() @lru_cache(maxsize=1) def omvll_get_config() -\u0026gt; omvll.ObfuscationConfig: \u0026#34;\u0026#34;\u0026#34; Return an instance of `ObfuscationConfig` which aims at describing the obfuscation scheme \u0026#34;\u0026#34;\u0026#34; 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:\nclass MyConfig(omvll.ObfuscationConfig): def __init__(self): super().__init__() def obfuscate_string(self, module: omvll.Module, func: omvll.Function, string: bytes): if func.demangled_name == \u0026#34;Hello::say_hi()\u0026#34;: return True if \u0026#34;debug.cpp\u0026#34; in module.name: return \u0026#34;\u0026lt;REMOVED\u0026gt;\u0026#34; return False Global Exclusions You can configure global exclusion for both modules and functions inside MyConfig class:\nModule 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 \u0026hellip;).\nomvll.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:\nomvll.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:\nReturns False if the module name is in the excluded modules list. Returns False if the function name is in the excluded functions list. Returns True if the function name is in the included function list. 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.\nGlobal excludes take precedence over local include lists.\nclass 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/).\nIf the directory of the Python Standard Library can\u0026rsquo;t be resolved, O-MVLL will raise an error like this:\n... \u0026#39;/cpython-install/lib/python310.zip\u0026#39;, \u0026#39;/cpython-install/lib/python3.10\u0026#39;, \u0026#39;/cpython-install/lib/lib-dynload\u0026#39;, ] 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 \u0026#39;encodings\u0026#39; Current thread 0x00007f612cb48040 (most recent call first): \u0026lt;no Python frame\u0026gt; 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.\nHere is an example with Python 3.10:\ncurl -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.\nFor 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.\nFor instance, if the compiler is called from:\n/home/romain/dev/o-mvll/test/build O-MVLL will check if a file omvll.yml is present in the following paths:\n/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:\nOMVLL_PYTHONPATH: \u0026#34;\u0026lt;mirror of $OMVLL_PYTHONPATH\u0026gt;\u0026#34; OMVLL_CONFIG: \u0026#34;\u0026lt;mirror of $OMVLL_CONFIG\u0026gt;\u0026#34; 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++.\nMost of the Linux distributions provide by default the GNU C++ standard library, aka libstdc++, and not the LLVM-based standard library, libc++.\nSince libc++.so is not usually installed on the system, when clang tries to dynamically load , it fails with the following error:\n$ clang -fpass-plugin=./omvll_ndk_r26d.so main.c -o main Could not load library \u0026#39;./omvll_ndk_r26d.so\u0026#39;: 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:\nLD_LIBRARY_PATH=\u0026lt;NDK_HOME\u0026gt;/toolchains/llvm/prebuilt/linux-x86_64/lib64 \u0026lt;NDK_HOME\u0026gt; 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.\nThe clang binary provided in the NDK is also linked with libc++.so but we don\u0026rsquo;t need to manually provide the lib64 directory as it uses a RUNPATH set to $ORIGIN/../lib64. 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.\nCould not load library \u0026#39;./OMVLL.dylib\u0026#39;: Signature does not match To resolve this, you must either:\nOption 1: Disable SIP\nYou can fully disable SIP using:\ncsrutil disable \u0026#x26a0;\u0026#xfe0f; This requires booting into macOS Recovery Mode and has significant security implications. Only use this method if you understand the risks.\nOption 2: Code Sign NDK Tools\nYou can sign the NDK binaries with your own developer identity and entitlements:\nCreate an entitlements file\nSave the following as myentitlements.entitlements:\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE plist PUBLIC \u0026#34;-//Apple//DTD PLIST 1.0//EN\u0026#34; \u0026#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd\u0026#34;\u0026gt; \u0026lt;plist version=\u0026#34;1.0\u0026#34;\u0026gt; \u0026lt;dict\u0026gt; \u0026lt;key\u0026gt;com.apple.security.cs.disable-library-validation\u0026lt;/key\u0026gt; \u0026lt;true/\u0026gt; \u0026lt;key\u0026gt;com.apple.security.cs.allow-unsigned-executable-memory\u0026lt;/key\u0026gt; \u0026lt;true/\u0026gt; \u0026lt;key\u0026gt;com.apple.security.cs.allow-dyld-environment-variables\u0026lt;/key\u0026gt; \u0026lt;true/\u0026gt; \u0026lt;key\u0026gt;com.apple.security.cs.allow-jit\u0026lt;/key\u0026gt; \u0026lt;true/\u0026gt; \u0026lt;key\u0026gt;com.apple.security.cs.disable-executable-page-protection\u0026lt;/key\u0026gt; \u0026lt;true/\u0026gt; \u0026lt;/dict\u0026gt; \u0026lt;/plist\u0026gt; Sign the NDK toolchain executables\nReplace \u0026lt;identity\u0026gt; with your valid code signing identity (you can find it using security find-identity):\ncodesign --force --options runtime --verbose=4 -s \u0026lt;identity\u0026gt; \\ --entitlements myentitlements.entitlements \\ $ANDROID_HOME/ndk/26.3.11579264/toolchains/llvm/prebuilt/darwin-x86_64/bin/clang codesign --force --options runtime --verbose=4 -s \u0026lt;identity\u0026gt; \\ --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.\nGradle Integration Within an Android project, we can setup O-MVLL by using the cppFlags, cFlags attributes in the ExternalNativeCmakeOptions DSL block:\nandroid { compileSdkVersion 30 ndkVersion \u0026#34;26.3.11579264\u0026#34; ... buildTypes { release { ndk.abiFilters \u0026#39;arm64-v8a\u0026#39; // Force ARM64 externalNativeBuild { cmake { cppFlags \u0026#39;-fpass-plugin=\u0026lt;path\u0026gt;/omvll_ndk_r26d.so\u0026#39; // or omvll-ndk.dylib in MacOS systems cFlags \u0026#39;-fpass-plugin=\u0026lt;path\u0026gt;/omvll_ndk_r26d.so\u0026#39; // or omvll-ndk.dylib in MacOS systems } } }}} There are important options associated with this configuration:\nndkVersion 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. As a side effect of only supporting arm architecures, a released APK that only embeds arm* 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, \u0026hellip;).\nTo 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:\n# 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:\n. ├── 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.\nIt can be worth adding o-config.py, omvll.yml, and omvll.env in .gitignore to avoid leaks. Android NDK (WSL) Thank you to Tomáš Soukal from Talsec for this section. Preparing the WSL for commandline Android development Based on this article WSL for Developers!: Installing the Android SDK\nInstalling 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:\nexport 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:\nyes | sdkmanager --licenses Installing SDK components pay attention to use ndk version matching the downloaded obfuscator (I used - omvll_ndk_r26d.so) ./sdkmanager --update ./sdkmanager \u0026#34;platforms;android-31\u0026#34; \u0026#34;build-tools;31.0.0\u0026#34; \u0026#34;ndk;26.3.11579264\u0026#34; \u0026#34;platform-tools\u0026#34; Obfuscator related changes build.gradle adjust path to obfuscator binary omvll_ndk_r26d.so, change \u0026rsquo;tom\u0026rsquo; to your username: externalNativeBuild { cmake { cppFlags \u0026#34;-std=c++14 -frtti -fexceptions -fpass-plugin=/mnt/c/Users/tom/path-to-project/omvll_ndk_r26d.so\u0026#34; } } 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:\n... sdk.dir=/home/tom/android ... Troubleshooting I ran into this issue when running gradlew,\nenv: bash\\r: No such file or directory The following change helped me:\nvim gradlew :set fileformat=unix :wq Finally:\ngradlew clean build, gradlew assembleRelease, or whatever you like :)\niOS Using O-MVLL with Xcode is a bit easier than Android since we don\u0026rsquo;t need to deal with different libstdc++/libc++. To enable O-MVLL, one needs to set the following in Xcode:\nBuild Settings \u0026gt; Apple Clang - Custom Compiler Flags \u0026gt; Other C/C++ Flags\nand add -fpass-plugin=\u0026lt;path\u0026gt;/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.\nFinally, we can create an omvll.yml file next to the *.xcodeproj file which defines OMVLL_PYTHONPATH and OMVLL_CONFIG.\nEt voila :)\nOMVLL_PYTHONPATH: \u0026#34;/Users/romain/Downloads/Python-3.10.8/Lib\u0026#34; OMVLL_CONFIG: \u0026#34;/Users/romain/dev/ios-app/demo/omvll_conf/base.py\u0026#34; Code Completion The PyPI package omvll can be used to get code completion while using O-MVLL:\n$ python -m pip install [--user] omvll Requirements and Limitations Cross Compilation O-MVLL is currently tested and CI-compiled for the following configurations:\nAndroid NDK: Linux Debian Stretch and macOS 15.4 (arm64 \u0026amp; x86-64) iOS: macOS 15.4 (arm64 \u0026amp; 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:\nReleases: https://github.com/open-obfuscator/o-mvll/releases/ 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.\nEnvironment 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: \u0026#34;\u0026lt;path\u0026gt;/Python-3.10.8/Lib\u0026#34; OMVLL_CONFIG: \u0026#34;\u0026lt;path\u0026gt;/myconfig.py\u0026#34; ","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1758443561,"objectID":"4e220b8e399cc083345051a2dc9c4a6d","permalink":"https://obfuscator.re/omvll/introduction/getting-started/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/omvll/introduction/getting-started/","section":"omvll","summary":"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:\n# Create/edit \u0026#39;./omvll_config.py\u0026#39; to configure the obfuscator and run: $ clang -fpass-plugin=OMVLL.{so, dylib} main.c -o main Practically, there are additional configuration steps.\nO-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.\n","tags":null,"title":"Getting started","type":"omvll"},{"authors":null,"categories":null,"content":"dProtect is strongly based on Proguard and Proguard-Core two open-source projects developed by Guardsquare.\n","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1718355017,"objectID":"77b043267815265b41745e6845762389","permalink":"https://obfuscator.re/acknowledgements/guardsquare/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/acknowledgements/guardsquare/","section":"acknowledgements","summary":"dProtect is strongly based on Proguard and Proguard-Core two open-source projects developed by Guardsquare.\n","tags":null,"title":"Guardsquare","type":"acknowledgements"},{"authors":null,"categories":null,"content":"Troubleshooting Be aware that the support for iOS is currently very limited. In particular for Objective-C/Swift code. NDK Specific Can\u0026rsquo;t load libc++abi.so.1/libc++.so.1: error: unable to load plugin \u0026#39;\u0026lt;path\u0026gt;/libOMVLL.so\u0026#39;: \u0026#39;Could not load library \u0026#39;\u0026lt;path\u0026gt;/libOMVLL.so\u0026#39;: libc++abi.so.1: cannot open shared object file: No such file or directory\u0026#39; Set export LD_LIBRARY_PATH to the NDK directory which contains these libraries: \u0026lt;ndk\u0026gt;/toolchains/llvm/prebuilt/linux-x86_64/lib64 Python ModuleNotFoundError: No module named \u0026rsquo;encodings' 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 \u0026#39;encodings\u0026#39; Current thread 0x00007fd10ad91040 (most recent call first): \u0026lt;no Python frame\u0026gt; ninja: build stopped: subcommand failed. Set the environment variable OMVLL_PYTHONPATH to the Python STL as mentioned in Getting Started. You can also set the yaml key: OMVLL_PYTHONPATH. ModuleNotFoundError: No module named \u0026lsquo;omvll_config\u0026rsquo;: error: ModuleNotFoundError: No module named \u0026#39;omvll_config\u0026#39; ninja: build stopped: subcommand failed. You should set the environment variable OMVLL_CONFIG with the path to the O-MVLL Python configuration file. Alternatively, you can set this value in the yaml file. The C++ compiler is not able to compile a simple test program: CMake Error at /opt/android/sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeTestCXXCompiler.cmake:62 (message): The C++ compiler \u0026#34;/home/romain/android/sdk/ndk/25.0.8775105/toolchains/llvm/prebuilt/linux-x86_64/bin/clang++\u0026#34; is not able to compile a simple test program. It fails with the following output: This error is likely due to an error in the O-MVLL configuration file. You could try to set -DCMAKE_CXX_COMPILER_WORKS=1 and see if the error message is more meaningful. How to open a GitHub issue O-MVLL might crash while trying to obfuscate code that is not well supported by the passes, or it could also introduce an inconsistency in the obfuscated code.\nIf you think that you identified a bug, please, feel free to open a GitHub issue with the following information:\nDescription Description of the bug or the problem.\nHow to reproduce the issue Please describe and attach all the necessary materials (backtrace, binary, snippet, code) to reproduce the issue.\nAn issue without enough information to reproduce the problem will be closed without notice.\nIf you need to share sensitive information (like code) that could help to address the issue, you can use one of these email addresses: ping@obfuscator.re me@romainthomas.fr Here is also a GPG Key.\nO-MVLL Python Configuration You can put the O-MVLL configuration that triggers the bug (if relevant)\nEnvironment Target: Android/iOS? O-MVLL Version: strings libOMVLL.so|grep \u0026quot;OMVLL Version:\u0026quot; Compilation of O-MVLL: CI/By your own If you compiled O-MVLL by yourself, please attach the library to the issue.\nAdditional context Any other information that could help to address the issue?\n","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"c1dd2846a4f8c9de2e8b843c9198b60a","permalink":"https://obfuscator.re/omvll/other-topics/troubleshooting/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/omvll/other-topics/troubleshooting/","section":"omvll","summary":"Troubleshooting Be aware that the support for iOS is currently very limited. In particular for Objective-C/Swift code. NDK Specific Can\u0026rsquo;t load libc++abi.so.1/libc++.so.1: error: unable to load plugin \u0026#39;\u0026lt;path\u0026gt;/libOMVLL.so\u0026#39;: \u0026#39;Could not load library \u0026#39;\u0026lt;path\u0026gt;/libOMVLL.so\u0026#39;: libc++abi.so.1: cannot open shared object file: No such file or directory\u0026#39; Set export LD_LIBRARY_PATH to the NDK directory which contains these libraries: \u0026lt;ndk\u0026gt;/toolchains/llvm/prebuilt/linux-x86_64/lib64 Python ModuleNotFoundError: No module named \u0026rsquo;encodings' 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 \u0026#39;encodings\u0026#39; Current thread 0x00007fd10ad91040 (most recent call first): \u0026lt;no Python frame\u0026gt; ninja: build stopped: subcommand failed. Set the environment variable OMVLL_PYTHONPATH to the Python STL as mentioned in Getting Started. You can also set the yaml key: OMVLL_PYTHONPATH. ModuleNotFoundError: No module named \u0026lsquo;omvll_config\u0026rsquo;: error: ModuleNotFoundError: No module named \u0026#39;omvll_config\u0026#39; ninja: build stopped: subcommand failed. You should set the environment variable OMVLL_CONFIG with the path to the O-MVLL Python configuration file. Alternatively, you can set this value in the yaml file. The C++ compiler is not able to compile a simple test program: CMake Error at /opt/android/sdk/cmake/3.22.1/share/cmake-3.22/Modules/CMakeTestCXXCompiler.cmake:62 (message): The C++ compiler \u0026#34;/home/romain/android/sdk/ndk/25.0.8775105/toolchains/llvm/prebuilt/linux-x86_64/bin/clang++\u0026#34; is not able to compile a simple test program. It fails with the following output: This error is likely due to an error in the O-MVLL configuration file. You could try to set -DCMAKE_CXX_COMPILER_WORKS=1 and see if the error message is more meaningful. How to open a GitHub issue O-MVLL might crash while trying to obfuscate code that is not well supported by the passes, or it could also introduce an inconsistency in the obfuscated code.\n","tags":null,"title":"Troubleshooting","type":"omvll"},{"authors":null,"categories":null,"content":"Yes, it already exists several open-source projects that aim at protecting code against reverse engineering.\nCompare to these projects, O-MVLL is different in the API used to configure and obfuscate the native code as well as in the design of some of obfuscation passes. On the other hand, dProtect is rooted by Proguard which is known by Android developers and thus, easier to integrate than other solutions.\nIn addition, this project is strongly attached to:\nProviding a clean and in-depth documentation Having an easy toolchain integration Exposing a user-friendly API ","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1718355017,"objectID":"fa8d322f8a9e8666f7d9e9a466e67081","permalink":"https://obfuscator.re/faq/why-another-obfuscator/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/faq/why-another-obfuscator/","section":"faq","summary":"Yes, it already exists several open-source projects that aim at protecting code against reverse engineering.\nCompare to these projects, O-MVLL is different in the API used to configure and obfuscate the native code as well as in the design of some of obfuscation passes. On the other hand, dProtect is rooted by Proguard which is known by Android developers and thus, easier to integrate than other solutions.\nIn addition, this project is strongly attached to:\n","tags":null,"title":"Why another obfuscator?","type":"faq"},{"authors":null,"categories":null,"content":"Compilation O-MVLL relies on the following dependencies:\nLLVM (version depending on the toolchain version) CPython (3.10.7) spdlog (1.10.0) Pybind11 (2.10.0) As a first step, let\u0026rsquo;s consider that we compiled all the dependencies. O-MVLL is a C++17 out-of-tree LLVM pass so we don\u0026rsquo;t need to compile LLVM for each build.\nThe CMake configuration step can be invoked as follows:\n$ git clone https://github.com/open-obfuscator/o-mvll.git $ mkdir o-mvll/src/build \u0026amp;\u0026amp; cd o-mvll/src/build On Linux export NDK_STAGE1=${LLVM_ROOT}/out/stage1-install export NDK_STAGE2=${LLVM_ROOT}/out/stage2 $ cmake -GNinja .. \\ -DCMAKE_BUILD_TYPE=Release \\ -DCMAKE_CXX_COMPILER=${NDK_STAGE1}/bin/clang++ \\ -DCMAKE_C_COMPILER=${NDK_STAGE1}/bin/clang \\ -DCMAKE_CXX_FLAGS=\u0026#34;-stdlib=libc++\u0026#34; \\ -DPython3_ROOT_DIR=${PYTHON_ROOT} \\ -DPython3_LIBRARY=${PYTHON_ROOT}/lib/libpython3.10.a \\ -DPython3_INCLUDE_DIR=${PYTHON_ROOT}/include/python3.10 \\ -Dpybind11_DIR=${PYBIND11_ROOT}/share/cmake/pybind11 \\ -Dspdlog_DIR=${SPDLOG_ROOT}/lib/cmake/spdlog \\ -DLLVM_DIR=${NDK_STAGE2}/lib64/cmake/llvm \\ -DOMVLL_ABI=CustomAndroid $ ninja On macOS The plugin can be built on macOS for both NDK and Xcode toolchains. Prerequisite: Set the $LLVM_ROOT environment variable to point to the LLVM package corresponding to your target toolchain:\nNDK: Package ends with NDK*-Darwin Xcode: Package ends with git-{arch}-Darwin. Xcode export XCODE_TOOLCHAIN=\u0026#34;$(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain\u0026#34; $ cmake -GNinja \\ -S src \\ -B build \\ -DCMAKE_BUILD_TYPE=Release \\ -DCMAKE_C_COMPILER=${XCODE_TOOLCHAIN}/usr/bin/clang \\ -DCMAKE_CXX_COMPILER=${XCODE_TOOLCHAIN}/usr/bin/clang++ \\ -DCMAKE_OSX_DEPLOYMENT_TARGET=\u0026#34;15.4\u0026#34; \\ -DPython3_ROOT_DIR=${PYTHON_ROOT} \\ -DPython3_LIBRARY=${PYTHON_ROOT}/lib/libpython3.10.a \\ -DPython3_INCLUDE_DIR=${PYTHON_ROOT}/include/python3.10 \\ -Dpybind11_DIR=${PYBIND11_ROOT}/share/cmake/pybind11 \\ -Dspdlog_DIR=${SPDLOG_ROOT}/lib/cmake/spdlog \\ -DLLVM_DIR=${LLVM_ROOT}/lib/cmake/llvm \\ -DPYBIND11_NOPYTHON=1 \\ -DOMVLL_ABI=Apple $ ninja -C build NDK The macOS-compatible NDK toolchain compiler is available through: android/ndk/releases. export NDK_COMPILER=\u0026#34;/path/to/toolchains/llvm/prebuilt/darwin\u0026#34; $ cmake -GNinja \\ -S src \\ -B build \\ -DCMAKE_BUILD_TYPE=Release \\ -DCMAKE_C_COMPILER=${NDK_COMPILER}/bin/clang \\ -DCMAKE_CXX_COMPILER=${NDK_COMPILER}/bin/clang++ \\ -DCMAKE_OSX_DEPLOYMENT_TARGET=\u0026#34;15.4\u0026#34; \\ -DPython3_ROOT_DIR=${PYTHON_ROOT} \\ -DPython3_LIBRARY=${PYTHON_ROOT}/lib/libpython3.10.a \\ -DPython3_INCLUDE_DIR=${PYTHON_ROOT}/include/python3.10 \\ -Dpybind11_DIR=${PYBIND11_ROOT}/share/cmake/pybind11 \\ -Dspdlog_DIR=${SPDLOG_ROOT}/lib/cmake/spdlog \\ -DLLVM_DIR=${LLVM_ROOT}/lib/cmake/llvm \\ -DPYBIND11_NOPYTHON=1 \\ -DOMVLL_ABI=Android $ ninja -C build You can download the dependencies at the following links.\nNDK on Linux All the dependencies are compiled from the Docker image: openobfuscator/omvll-ndk. Version URL r25c https://open-obfuscator.build38.io/static/omvll-deps-ndk-r25c.tar r26d https://open-obfuscator.build38.io/static/omvll-deps-ndk-r26d.tar NDK on macOS Version URL r26d https://open-obfuscator.build38.io/static/omvll-v1.4.0-macos-deps.tar Xcode All the dependencies are compiled with both architectures: arm64 \u0026amp; x86-64 from an Apple M1. Version URL 14.1 https://open-obfuscator.build38.io/static/omvll-deps-xcode-14_1.tar 15.2 https://open-obfuscator.build38.io/static/omvll-deps-xcode-15_2.tar 16.3 https://open-obfuscator.build38.io/static/omvll-v1.4.0-macos-deps.tar Compilation Time Since O-MVLL is an out-of-tree plugin, it takes about 2 minutes to fully compile the obfuscator (using the pre-compiled dependencies). Generate NDK deps O-MVLL dependencies can be generated easily using the same repository:\n# Make sure you have expected ANDROID_HOME variable is already satisfied ANDROID_HOME=\u0026#34;sdk folder installation\u0026#34; # Install ndk version used by the project $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install \u0026#39;ndk;25.2.9519653\u0026#39; git clone https://github.com/open-obfuscator/o-mvll.git o-mvll \u0026amp;\u0026amp; cd o-mvll ./scripts/deps/generate_deps.sh -p ndk -o output.tar.gz # output.tar.gz will be generated with all the required dependencies. Generate XCode deps Manually generating the LLVM deps for Xcode is no longer supported. Please consider using the prebuilt dependencies instead.\nDocker Build To ease the compilation of O-MVLL, we provide Docker images with the correct environment to compile O-MVLL for both Android NDK and the Xcode toolchain.\nAndroid NDK $ docker pull openobfuscator/omvll-ndk $ git clone https://github.com/open-obfuscator/o-mvll $ curl -LO https://open-obfuscator.build38.io/static/omvll-deps-ndk-r26d.tar $ mkdir -p ./third-party $ tar xvf omvll-deps-ndk-r26d.tar -C ./third-party $ docker run --rm \\ -v $(pwd)/o-mvll:/o-mvll \\ -v $(pwd)/third-party:/third-party \\ openobfuscator/omvll-ndk sh /o-mvll/scripts/docker/ndk_r26d_compile.sh Xcode $ docker pull openobfuscator/omvll-xcode $ git clone https://github.com/open-obfuscator/o-mvll $ curl -LO https://open-obfuscator.build38.io/static/omvll-v1.4.0-macos-deps.tar $ mkdir -p ./third-party $ tar xvf omvll-v1.4.0-macos-deps.tar -C ./third-party $ docker run --rm \\ -v $(pwd)/o-mvll:/o-mvll \\ -v $(pwd)/third-party:/third-party \\ openobfuscator/omvll-xcode sh /o-mvll/scripts/docker/xcode_16_compile.sh ","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1758443561,"objectID":"5e0e16898f6d9f5e3690a5611328f828","permalink":"https://obfuscator.re/omvll/introduction/compilation/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/omvll/introduction/compilation/","section":"omvll","summary":"Compilation O-MVLL relies on the following dependencies:\nLLVM (version depending on the toolchain version) CPython (3.10.7) spdlog (1.10.0) Pybind11 (2.10.0) As a first step, let\u0026rsquo;s consider that we compiled all the dependencies. O-MVLL is a C++17 out-of-tree LLVM pass so we don\u0026rsquo;t need to compile LLVM for each build.\nThe CMake configuration step can be invoked as follows:\n$ git clone https://github.com/open-obfuscator/o-mvll.git $ mkdir o-mvll/src/build \u0026amp;\u0026amp; cd o-mvll/src/build On Linux export NDK_STAGE1=${LLVM_ROOT}/out/stage1-install export NDK_STAGE2=${LLVM_ROOT}/out/stage2 $ cmake -GNinja .. \\ -DCMAKE_BUILD_TYPE=Release \\ -DCMAKE_CXX_COMPILER=${NDK_STAGE1}/bin/clang++ \\ -DCMAKE_C_COMPILER=${NDK_STAGE1}/bin/clang \\ -DCMAKE_CXX_FLAGS=\u0026#34;-stdlib=libc++\u0026#34; \\ -DPython3_ROOT_DIR=${PYTHON_ROOT} \\ -DPython3_LIBRARY=${PYTHON_ROOT}/lib/libpython3.10.a \\ -DPython3_INCLUDE_DIR=${PYTHON_ROOT}/include/python3.10 \\ -Dpybind11_DIR=${PYBIND11_ROOT}/share/cmake/pybind11 \\ -Dspdlog_DIR=${SPDLOG_ROOT}/lib/cmake/spdlog \\ -DLLVM_DIR=${NDK_STAGE2}/lib64/cmake/llvm \\ -DOMVLL_ABI=CustomAndroid $ ninja On macOS The plugin can be built on macOS for both NDK and Xcode toolchains. Prerequisite: Set the $LLVM_ROOT environment variable to point to the LLVM package corresponding to your target toolchain:\n","tags":null,"title":"Compilation","type":"omvll"},{"authors":null,"categories":null,"content":"O-MVLL is based on LLVM which provides most of the functionalities to perform native code obfuscation.\n","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1718355017,"objectID":"1ba31c2540d087479f322341da4bf6fe","permalink":"https://obfuscator.re/acknowledgements/llvm/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/acknowledgements/llvm/","section":"acknowledgements","summary":"O-MVLL is based on LLVM which provides most of the functionalities to perform native code obfuscation.\n","tags":null,"title":"LLVM","type":"acknowledgements"},{"authors":null,"categories":null,"content":"From a personal standpoint, I really do believe that an open-source design does not \u0026ndash; if wisely thought and used \u0026ndash; weaken the overall protection against reverse engineering. Said differently, I believe that we can reach a good level of protection against reverse engineering even though the design is known.\nBasically, obfuscation passes introduce overhead for the reverse engineering process but it is not an absolute protection. If this overhead is too high, the attacker could timeout or find a way to automate and potentially scale the deobfuscation process. The automation and the scalability of the deobfuscation could be mitigated in the design of the obfuscation.\nIn its current version, the design of the passes in O-MVLL and dProtect can be improved in a lot of ways, but at least the project is bootstrapped, documented, and open source.\nI also do hope that the challenges are a good opportunity to assess and publish attacks on O-MVLL and dProtect which could be a benefit for both: reverse engineering and obfuscation.\n","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1718355017,"objectID":"423c294f85935a7b431dbdaef8b0d353","permalink":"https://obfuscator.re/faq/why-an-open-source-obfuscator/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/faq/why-an-open-source-obfuscator/","section":"faq","summary":"From a personal standpoint, I really do believe that an open-source design does not \u0026ndash; if wisely thought and used \u0026ndash; weaken the overall protection against reverse engineering. Said differently, I believe that we can reach a good level of protection against reverse engineering even though the design is known.\nBasically, obfuscation passes introduce overhead for the reverse engineering process but it is not an absolute protection. If this overhead is too high, the attacker could timeout or find a way to automate and potentially scale the deobfuscation process. The automation and the scalability of the deobfuscation could be mitigated in the design of the obfuscation.\n","tags":null,"title":"Why an open-source obfuscator?","type":"faq"},{"authors":null,"categories":null,"content":"Compilation dProtect / Proguard dProtect is derived from two projects developed by Guardsquare:\nProguardCORE Proguard ProguardCORE provides all the functionalities to read and modify the Java bytecode while Proguard contains the logic for optimizing, shrinking, and mangling (or obfuscating) class names.\nAs mentioned throughout the documentation dProtect is an extension of Proguard with enhanced obfuscation passes. From an implementation perspective, it adds a custom obfuscation pipeline that is similar to the optimization pipeline.\nTo provide this additional obfuscation pipeline, we forked Proguard and ProguardCORE into dProtect and dProtect-core:\n\u003c?xml version=\"1.0\" ?\u003e The commits of Proguard on which dProtect and dProtect-Core are based are referenced in the guardsquare/master branches:\ndProtect: guardsquare/master dProtect-core: guardsquare/master Local Development To test or prototype dProtect, we first need to clone dProtect:\n$ git clone https://github.com/open-obfuscator/dprotect ~/dev/dprotect Within the cloned directory, the first four lines of ./gradle.properties contains information about the version of Proguard on which dProtect is based, and the version of dProtectCore:\n// ./gradle.properties proguardVersion = 7.2.3 dProtectVersion = 1.0.0 dprotectCoreVersion = 1.0.0 dprotectCoreCommit = latest ... Name Description proguardVersion The version of Proguard on which the fork is based dProtectVersion The current version of dProtect dprotectCoreVersion The version of dProtect-core required by dProtect dprotectCoreCommit Specific commit or branch for non-released version From this file, dprotectCoreCommit tells us which version/commit/branch should be installed for the cloned version of dProtect. latest means that dProtect can use the upstream version of dProtect-core while other values mean tag, branch, or commit.\nFor a released version of dProtect, dprotectCoreCommit should match dprotectCoreVersion. Then, based on dprotectCoreCommit property, we can clone the appropriated version of dProtect-core in another directory:\n# dprotectCoreCommit == latest $ git clone \\ https://github.com/open-obfuscator/dprotect-core \\ ~/dev/dprotect-core # dprotectCoreCommit == branch $ git clone --branch=feature/reflection \\ https://github.com/open-obfuscator/dprotect-core \\ ~/dev/dprotect-core # dprotectCoreCommit == commit $ git clone https://github.com/open-obfuscator/dprotect-core \\ ~/dev/dprotect-core $ cd ~/dev/dprotect-core $ git checkout \u0026lt;commit\u0026gt; Alternatively, one can use the script ./scripts/fetch_dprotect_core.py which automates this process:\n# Within ./dprotect $ python ./scripts/fetch_dprotect_core.py . ~/dev/dprotect-core Once dProtect-core cloned, we can build the project and publish its package in the local Maven repository:\n$ cd ~/dev/dprotect-core $ ./gradlew :dprotect-core:publishToMavenLocal This step is required to address this requirement in dProtect:\n// base/build.gradle dependencies { api \u0026#34;re.obfuscator:dprotect-core:${dprotectCoreVersion}\u0026#34; ... } We can now move to the main dProtect repository (that has been cloned in this first step) and run the build:\n$ cd ~/dev/dprotect $ ./gradlew distZip ","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"c11d794a6c0b19134e035b5b7019d1c0","permalink":"https://obfuscator.re/dprotect/introduction/compilation/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/dprotect/introduction/compilation/","section":"dprotect","summary":"Compilation dProtect / Proguard dProtect is derived from two projects developed by Guardsquare:\nProguardCORE Proguard ProguardCORE provides all the functionalities to read and modify the Java bytecode while Proguard contains the logic for optimizing, shrinking, and mangling (or obfuscating) class names.\nAs mentioned throughout the documentation dProtect is an extension of Proguard with enhanced obfuscation passes. From an implementation perspective, it adds a custom obfuscation pipeline that is similar to the optimization pipeline.\n","tags":null,"title":"Compilation","type":"dprotect"},{"authors":null,"categories":null,"content":"Whilst no longer maintained, Hikari added new and original obfuscation passes for iOS and Objective-C.\n","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1718355017,"objectID":"f7290cfb5ee314287874067cec0a7493","permalink":"https://obfuscator.re/acknowledgements/hikari/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/acknowledgements/hikari/","section":"acknowledgements","summary":"Whilst no longer maintained, Hikari added new and original obfuscation passes for iOS and Objective-C.\n","tags":null,"title":"Hikari","type":"acknowledgements"},{"authors":null,"categories":null,"content":"Proguard is a Java bytecode optimizer and class names obfuscator developed by Guardsquare. On the other hand, dProtect is an extension of Proguard with an additional code obfuscation pipeline.\nAll the features of dProtect are highly grounded by Proguard and Proguard-Core. We \u0026ldquo;just\u0026rdquo; added code obfuscation passes to Proguard to improve the protection against reverse engineering.\n","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1718355017,"objectID":"673d46def217af3b59171afd1291a545","permalink":"https://obfuscator.re/faq/proguard-vs-dprotect/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/faq/proguard-vs-dprotect/","section":"faq","summary":"Proguard is a Java bytecode optimizer and class names obfuscator developed by Guardsquare. On the other hand, dProtect is an extension of Proguard with an additional code obfuscation pipeline.\nAll the features of dProtect are highly grounded by Proguard and Proguard-Core. We \u0026ldquo;just\u0026rdquo; added code obfuscation passes to Proguard to improve the protection against reverse engineering.\n","tags":null,"title":"What is the difference between Proguard and dProtect?","type":"faq"},{"authors":null,"categories":null,"content":"Cheat-Sheet Arithmetic Obfuscation -obfuscate-arithmetic,low class re.dprotect.** { *; } -obfuscate-arithmetic class re.dprotect.MyClass { java.lang.String decode(); } Constants Obfuscation -obfuscate-constants class re.dprotect.** { *; } -obfuscate-constants class re.dprotect.MyClass { private static void init(); } Control-Flow Obfuscation -obfuscate-control-flow class class com.password4j.Argon2Function { *; } -obfuscate-control-flow class class re.dprotect.** { *; } Strings Encryption -obfuscate-strings \u0026#34;https://api.dprotect.re/v1/*\u0026#34;, \u0026#34;AES/CBC/PKCS5PADDING\u0026#34;, \u0026#34;android_id\u0026#34; -obfuscate-strings class re.dprotect.MyClass { private static java.lang.String API_KEY; public static java.lang.String getToken(); } ","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"7726de0008e12baacfd43de3e0a73eff","permalink":"https://obfuscator.re/dprotect/introduction/cheat-sheet/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/dprotect/introduction/cheat-sheet/","section":"dprotect","summary":"Cheat-Sheet Arithmetic Obfuscation -obfuscate-arithmetic,low class re.dprotect.** { *; } -obfuscate-arithmetic class re.dprotect.MyClass { java.lang.String decode(); } Constants Obfuscation -obfuscate-constants class re.dprotect.** { *; } -obfuscate-constants class re.dprotect.MyClass { private static void init(); } Control-Flow Obfuscation -obfuscate-control-flow class class com.password4j.Argon2Function { *; } -obfuscate-control-flow class class re.dprotect.** { *; } Strings Encryption -obfuscate-strings \u0026#34;https://api.dprotect.re/v1/*\u0026#34;, \u0026#34;AES/CBC/PKCS5PADDING\u0026#34;, \u0026#34;android_id\u0026#34; -obfuscate-strings class re.dprotect.MyClass { private static java.lang.String API_KEY; public static java.lang.String getToken(); } ","tags":null,"title":"Cheat-Sheet","type":"dprotect"},{"authors":null,"categories":null,"content":"The O-MVLL\u0026rsquo;s Python API relies on Pybind11.\n","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1718355017,"objectID":"a3d5e64dcf243c109f12d6a0f54ac818","permalink":"https://obfuscator.re/acknowledgements/pybind11/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/acknowledgements/pybind11/","section":"acknowledgements","summary":"The O-MVLL\u0026rsquo;s Python API relies on Pybind11.\n","tags":null,"title":"Pybind11","type":"acknowledgements"},{"authors":null,"categories":null,"content":"Troubleshooting Since dProtect is based on Proguard, some questions might be addressed by looking at Proguard\u0026rsquo;s issues or Proguard documentation: Proguard Troubleshooting. Gradle Exceptions java.io.IOException: Please correct the above warnings first: $ ./gradlew build \u0026gt; Task :app:transformClassesAndResourcesWithProguardTransformForDebug FAILED FAILURE: Build failed with an exception. * What went wrong: Execution failed for task \u0026#39;:app:transformClassesAndResourcesWithProguardTransformForDebug\u0026#39;. \u0026gt; java.io.IOException: Please correct the above warnings first. You need to fix the *.pro files to resolve warnings raised above exception.\nAs a temporary workaround, you could use: -ignorewarnings\nHow to open a GitHub issue If you think that you identified a bug, please, feel free to open a GitHub issue with the following information:\nDescription Description of the bug or the problem.\nHow to reproduce the issue Please describe and attach all the necessary materials (backtrace, binary, snippet, code) to reproduce the issue.\nAn issue without enough information to reproduce the problem will be closed without notice.\nIf you need to share sensitive information (like code) that could help to address the issue, you can use one of these email addresses: ping@obfuscator.re me@romainthomas.fr Here is also a GPG Key.\ndProtect Configuration You can put the dProtect configuration that triggers the bug (if relevant)\nAdditional context Any other information that could help to address the issue?\nDo not forget to check if a similar issue is open in Proguard\u0026rsquo;s issues. ","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1669633091,"objectID":"a47684996d42bb3a891fb7eaf6f808b3","permalink":"https://obfuscator.re/dprotect/introduction/troubleshooting/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/dprotect/introduction/troubleshooting/","section":"dprotect","summary":"Troubleshooting Since dProtect is based on Proguard, some questions might be addressed by looking at Proguard\u0026rsquo;s issues or Proguard documentation: Proguard Troubleshooting. Gradle Exceptions java.io.IOException: Please correct the above warnings first: $ ./gradlew build \u0026gt; Task :app:transformClassesAndResourcesWithProguardTransformForDebug FAILED FAILURE: Build failed with an exception. * What went wrong: Execution failed for task \u0026#39;:app:transformClassesAndResourcesWithProguardTransformForDebug\u0026#39;. \u0026gt; java.io.IOException: Please correct the above warnings first. You need to fix the *.pro files to resolve warnings raised above exception.\n","tags":null,"title":"Troubleshooting","type":"dprotect"},{"authors":null,"categories":null,"content":"The main difference lies in the API and some obfuscation passes. While most of the LLVM-based obfuscators rely on in-tree obfuscation passes configured with compilation flags, O-MVLL is different in the way that it uses out-of-tree passes configured with a Python API.\n","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1718355017,"objectID":"e41074dcf4dcc26931946e5d331789f4","permalink":"https://obfuscator.re/faq/omvll-vs-ollvm/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/faq/omvll-vs-ollvm/","section":"faq","summary":"The main difference lies in the API and some obfuscation passes. While most of the LLVM-based obfuscators rely on in-tree obfuscation passes configured with compilation flags, O-MVLL is different in the way that it uses out-of-tree passes configured with a Python API.\n","tags":null,"title":"What is the difference between O-MVLL and O-LLVM (and its forks)?","type":"faq"},{"authors":null,"categories":null,"content":"The bootstrap of the O-MVLL\u0026rsquo;s JIT engine is inspired by DragonFFI.\n","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1718355017,"objectID":"b2afcf08a850e262c7a0f729c0905b8c","permalink":"https://obfuscator.re/acknowledgements/dragonffi/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/acknowledgements/dragonffi/","section":"acknowledgements","summary":"The bootstrap of the O-MVLL\u0026rsquo;s JIT engine is inspired by DragonFFI.\n","tags":null,"title":"DragonFFI","type":"acknowledgements"},{"authors":null,"categories":null,"content":"Short answer: all the contexts (personal, educational, commercial, etc) as long as it respects the LLVM License (Apache License 2.0) and Proguard License (GPL v2.0).\nOpen-obfuscator has been created to provide a free and open-source solution for reverse engineers and mobile developers. On one hand, to practice, challenge, and improve reverse engineering techniques as well as automation on obfuscated code. On the other hand, for developers to provide a solution to protect their applications.\nYou could use open-obfuscator for a commercial purposes but we don\u0026rsquo;t aim at providing a commercial support. If you have questions or special requests, you can reach out by email on ping@obfuscator.re.\n","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1718355017,"objectID":"2b1d93f59f732d3d0ad92ade75d770f0","permalink":"https://obfuscator.re/faq/usage/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/faq/usage/","section":"faq","summary":"Short answer: all the contexts (personal, educational, commercial, etc) as long as it respects the LLVM License (Apache License 2.0) and Proguard License (GPL v2.0).\nOpen-obfuscator has been created to provide a free and open-source solution for reverse engineers and mobile developers. On one hand, to practice, challenge, and improve reverse engineering techniques as well as automation on obfuscated code. On the other hand, for developers to provide a solution to protect their applications.\n","tags":null,"title":"In which context could I use Open-Obfuscator?","type":"faq"},{"authors":null,"categories":null,"content":"The idea of a pass-plugin obfuscator has been inspired by eShard\u0026rsquo;s obfuscator-llvm.\n","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1718355017,"objectID":"0898443ed9157a482fecbbd3c7791b75","permalink":"https://obfuscator.re/acknowledgements/eshard/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/acknowledgements/eshard/","section":"acknowledgements","summary":"The idea of a pass-plugin obfuscator has been inspired by eShard\u0026rsquo;s obfuscator-llvm.\n","tags":null,"title":"eShard","type":"acknowledgements"},{"authors":null,"categories":null,"content":"The objective of this challenge is to find the correct login/password that leads to \u0026ldquo;Access Granted\u0026rdquo;.\nThe given Android application is protected with different layers of protections (obfuscation, RASP checks, ELF modifications) but the design of all these protections is public:\nCode obfuscation is provided by O-MVLL/dProtect. The RASP checks are publicly known and documented on the internet. The ELF format modifications are described here: The Poor Man\u0026rsquo;s Obfuscator. All the algorithms used in the challenge are public. As obfuscation is a matter of time, the first prize of this challenge will be eligible for 6 months. During this period, the first person to find the correct login/password will be able to choose between a BinaryNinja license or a cash prize of 1300$/€.\nThe second prize will reward the write-up quality. This second prize will be the one that has not been chosen in the first part (if any). It will last for 3 months more after the end of the first part. So in total, this challenge is running for 9 months.\nIf you have found the flag or if you want to submit a write-up, you can send an email to this address: challenge-pydroid@obfuscator.re. If you have any questions, you can reach out at this address: ping@obfuscator.re or join the Discord server at this address: https://discord.gg/FTk4G9vhTM.\nThe list of the participants who found the flag or submitted a write-up will be updated in the section below and you can find the details of the rules in this document: Rules_Description.pdf.\nHappy reverse engineering!\nChecksum of the APK: a0b07e97197e2dfe48bb7df65dba4f145d485660ecf4bd0d3ab65b14039ec8d6\n","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1693126527,"objectID":"9df77d65d4cc926e139b5e5eaecb8153","permalink":"https://obfuscator.re/challenges/2022-12-android-challenge/description/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/challenges/2022-12-android-challenge/description/","section":"challenges","summary":"The objective of this challenge is to find the correct login/password that leads to \u0026ldquo;Access Granted\u0026rdquo;.\nThe given Android application is protected with different layers of protections (obfuscation, RASP checks, ELF modifications) but the design of all these protections is public:\nCode obfuscation is provided by O-MVLL/dProtect. The RASP checks are publicly known and documented on the internet. The ELF format modifications are described here: The Poor Man\u0026rsquo;s Obfuscator. All the algorithms used in the challenge are public. As obfuscation is a matter of time, the first prize of this challenge will be eligible for 6 months. During this period, the first person to find the correct login/password will be able to choose between a BinaryNinja license or a cash prize of 1300$/€.\n","tags":null,"title":"","type":"2022-12-android-challenge"},{"authors":null,"categories":null,"content":" Anti-Hooking The purpose of this pass is to protect functions against hooking frameworks like Frida. This pass is still a work in progress on iOS. \u003c?xml version=\"1.0\" ?\u003e \u003c?xml version=\"1.0\" ?\u003e Hooking is used for hijacking a function call to observe or change the parameters and the return value of the hijacked function. You should protect a function against hooking if the parameters of the function are sensitive.\nFor instance, let\u0026rsquo;s consider the following snippet:\nvoid encrypt(char key[16]) { ... } The function encrypt() should be protected against hooking otherwise an attacker could easily observe the effective value of the key parameter when the function is called.\nSimilarly, if the value returned by the function is sensitive then, you should also protect your function.\nbool has_secure_enclave() { ... } When to use it? You may want to enable this for all sensitive functions. This pass introduces a very low overhead while still being efficient against Frida.\nHow to use it? In the O-MVLL configuration file, this protection can be enabled by defining the following method:\ndef anti_hooking(self, mod: omvll.Module, func: omvll.Function) -\u0026gt; omvll.AntiHookOpt: if func.name in [\u0026#34;encrypt\u0026#34;, \u0026#34;has_secure_enclave\u0026#34;]: return True return False Implementation Usually, hooking frameworks need a scratch register to relocate or access metadata associated with the function currently hooked. In the case of Frida, the relocator needs one of the x16, x17 AArch64 registers as we can see in gumarm64relocator.c\nif (available_scratch_reg != NULL) { gboolean x16_used, x17_used; guint insn_index; x16_used = FALSE; x17_used = FALSE; ... if (!x16_used) *available_scratch_reg = ARM64_REG_X16; else if (!x17_used) *available_scratch_reg = ARM64_REG_X17; else *available_scratch_reg = ARM64_REG_INVALID; } If the prologue of the function starts with instructions that use these two registers, Frida fails to hook the function. Let\u0026rsquo;s consider the following Frida script:\nconsole.log(\u0026#34;Hello!\u0026#34;) Interceptor.attach(Module.getExportByName(\u0026#39;omvll_test.bin\u0026#39;, \u0026#39;hook_me\u0026#39;), { onEnter(args) { console.log(\u0026#34;Yes I hooked you!\u0026#34;) }, onLeave(retval) { console.log(\u0026#34;Frida exit from hook_me()\u0026#34;) } }); If the prologue of hook_me() starts by using x16 and x17, Frida raises the following error:\n$ frida --file=/data/local/tmp/omvll_test.bin --load=./test.js --stdio=pipe --no-pause ____ / _ | Frida 15.1.17 - A world-class dynamic instrumentation toolkit | (_| | \u0026gt; _ | Commands: /_/ |_| help -\u0026gt; Displays the help system . . . . object? -\u0026gt; Display information about \u0026#39;object\u0026#39; . . . . exit/quit -\u0026gt; Exit . . . . . . . . More info at https://frida.re/docs/home/ Spawning `/data/local/tmp/omvll_test.bin`... Hello! Spawned `/data/local/tmp/omvll_test.bin`. Resuming main thread! Error: unable to intercept function at 0x623d443158; please file a bug at value (frida/runtime/core.js:316) at \u0026lt;eval\u0026gt; (/test.js:19) On the O-MVLL implementation hand side, it works by setting a custom prologue to the llvm::Function that must be protected:\nbool AntiHook::runOnFunction(llvm::Function \u0026amp;F) { std::unique_ptr\u0026lt;MemoryBuffer\u0026gt; Buffer = ...; auto* Int8Ty = Type::getInt8Ty(F.getContext()); auto* Prologue = ConstantDataVector::getRaw( Buffer-\u0026gt;getBuffer(), Buffer-\u0026gt;getBufferSize(), Int8Ty); F.setPrologueData(Prologue); return true; } The prologue injected by the pass is located in a MemoryBuffer that is generated through the O-MVLL\u0026rsquo;s JIT engine (based on LLVM ORCv2). Within the Jitter, it generates the raw stub as follows:\nstd::unique_ptr\u0026lt;MemoryBuffer\u0026gt; Buffer = jitter-\u0026gt;jitAsm( R\u0026#34;delim( mov x17, x17; mov x16, x16; )delim\u0026#34; ); This API is modular enough to dynamically JIT different stubs.\nLimitations The current implementation does not use a strong binding between the custom anti-hooking prologue and the original function. It means that an attacker could patch the prologue of the function to remove the instructions that trigger the Frida error.\nIn its current form, the stub injected in the prologue of the functions only prevents Frida from hooking a user-controlled function. In particular, this pass is not able to protect against hooking on imported functions (like fopen, read etc) and does not prevent other hooking frameworks like Dobby from working correctly.\nIf hooking really matters, we strongly recommend adding another detection layer to perform enhanced checks based – for instance – on the artifacts left by the different hooking frameworks.\nOrigin of this pass I actually experienced this limitation of Frida while reverse-engineering SafetyNet/DroidGuard.\nThe bytecode of the VM performs \u0026ndash; at some point \u0026ndash; an integrity check of the code using SHA-1 from OpenSSL. The implementation of this hash algorithm in OpenSSL is partially based on a raw assembly stub, in which the function sha1_block_data_order() begins as follows in crypto/sha/asm/sha1-armv8.pl:\nsha1_block_data_order: ldr x16, .LOPENSSL_armcap_P adr x17, .LOPENSSL_armcap_P add x16, x16, x17 ldr w16, [x16] tst w16, #ARMV8_SHA1 b.ne .Lv8_entry ... ","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1725428622,"objectID":"4148e13c6bf5ca14a18beb2906ae1a1a","permalink":"https://obfuscator.re/omvll/passes/anti-hook/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/omvll/passes/anti-hook/","section":"omvll","summary":" Anti-Hooking The purpose of this pass is to protect functions against hooking frameworks like Frida. This pass is still a work in progress on iOS. \u003c?xml version=\"1.0\" ?\u003e \u003c?xml version=\"1.0\" ?\u003e Hooking is used for hijacking a function call to observe or change the parameters and the return value of the hijacked function. You should protect a function against hooking if the parameters of the function are sensitive.\n","tags":null,"title":"Anti-Hooking","type":"omvll"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"b291a4076f110b48a83c7ebbe8df5406","permalink":"https://obfuscator.re/resources/appdome/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/appdome/","section":"resources","summary":"","tags":null,"title":"Appdome","type":"resources"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"7fb676acfd4dae5eadda1d31f9601c3f","permalink":"https://obfuscator.re/resources/appsealing/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/appsealing/","section":"resources","summary":"","tags":null,"title":"Appsealing","type":"resources"},{"authors":null,"categories":null,"content":" Arithmetic Obfuscation The purpose of this pass is to transform arithmetic operations into complex expressions. \u003c?xml version=\"1.0\" ?\u003e \u003c?xml version=\"1.0\" ?\u003e Because of the design of the Java bytecode, arithmetic operations are (usually) exactly translated in their dedicated bytecode.\nFor instance, if we consider the following Java code:\nint doAdd(int a, int b) { return a + b; } Once compiled, it generates the following instructions:\nint doAdd(int, int) { Code: 0: iload_1 1: iload_2 2: iadd 3: ireturn } Moreover, the Java arithmetic opcodes are strongly typed:\niadd for integer number addition. ladd for long number addition. fadd for floating numbers addition. dadd for double numbers addition. It means that decompilers can reliably determine the type of the arithmetic operands and efficiently decompile arithmetic expressions.\nIn addition, decompilers are doing simplifications on the top of these expressions (like constant propagation).\nSimilarly to the Arithmetic Obfuscation in O-MVLL, this pass transforms arithmetic operations into Mixed Boolean-Arithmetic expressions (MBA).\nWhen to use it? You should use this protection when arithmetic operations matter in the logic of your class. This is particularly true for:\nCryptography algorithms (known or custom). Encoding algorithms. Bitwise manipulations. Some operations on float/double and arrays are not supported yet. Hence, be aware of the limitations. How to use it? Within the dProtect configuration file, we can enable this protection with:\n-obfuscate-arithmetic \u0026lt;class specifier\u0026gt; In addition, this -obfuscate-arithmetic option accepts the following modifiers:\n-obfuscate-arithmetic,low \u0026lt;class specifier\u0026gt; -obfuscate-arithmetic,medium \u0026lt;class specifier\u0026gt; -obfuscate-arithmetic,high \u0026lt;class specifier\u0026gt; low, medium, and high define the number of transformations applied to a single operation:\n[-\u0026gt;] Operation: X ^ Y [\u0026lt;-] low : (X | Y) - (X \u0026amp; Y) medium : (((Y + X) - (Y \u0026amp; X)) ^ ((Y | X) - (Y + X))) + 2*(((Y + X) - (Y \u0026amp; X)) \u0026amp; ((Y | X) - (Y + X))) high : \u0026lt;not printable\u0026gt; Level Number of Iterations low 1 medium 2 high 3 In the future, -obfuscate-arithmetic might also accept other modifiers to disable/enable this obfuscation on categories of instructions:\n-obfuscate-arithmetic,skiparray \u0026lt;class specifier\u0026gt; -obfuscate-arithmetic,skipfloat \u0026lt;class specifier\u0026gt; Nevertheless, the support of these extra modifiers will require some changes in the design of the pass.\nImplementation To transform arithmetic operations into Mixed Boolean-Arithmetic expressions (MBA), the pass implements the proguard.obfuscate.util.ReplacementSequences interface which provides the API to define rewriting rules:\nimport proguard.obfuscate.util.ReplacementSequences; public class MBAObfuscationAdd implements ReplacementSequences { ... @Override public Instruction[][][] getSequences() { return new Instruction[][][] { // X + Y --\u0026gt; (X \u0026amp; Y) + (X | Y) { // Sequence of instructions to match ____.iload(X) // | .iload(Y) // | .iadd() // | X + Y .__(), // | // Transformation ____.iload(X) // | First term .iload(Y) // | .iand() // | X \u0026amp; Y .iload(X) // | Second term .iload(Y) // | .ior() // | X | Y .iadd() // | Addition of the previous terms .__(), // | (X \u0026amp; Y) + (X | Y) Given the different rewriting classes associated with arithmetic operations, they are successively applied on the class pool using the Proguard MultiMemberVisitor visitor:\nprogramClassPool.accept( new AllClassVisitor( new MBAObfuscationFilter( new AllMethodVisitor( new MultiMemberVisitor( new InstructionSequenceObfuscator(new MBAObfuscationAdd(...)), new InstructionSequenceObfuscator(new MBAObfuscationXor(...)), new InstructionSequenceObfuscator(new MBAObfuscationAnd(...)), new InstructionSequenceObfuscator(new MBAObfuscationOr (...)), new InstructionSequenceObfuscator(new MBAObfuscationSub(...)) ))))); To limit the number of rewriting rules, the pass performs an early normalization of the instructions. This normalization is implemented by the MBANormalizer visitor. Limitations Classical arithmetic operations on integers and longs are supported by the pass. Nevertheless, the pass does not (yet) support expressions with inner operations in the left and right operands:\nint A = B + C; // Supported int A = (B \u0026gt;\u0026gt; 3) + (C \u0026lt;\u0026lt; 4); // /!\\ Addition not supported int A = (B \u0026amp; 0xFF) + (C | 1); // /!\\ Addition not supported In addition, the operations on floats and doubles are not yet supported but following the existing rules on integers/long, it should not be complicated to support.\nFor array operations (e.g. table[i] + table[y]), there is early support but it is far from being complete.\n","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"b198ff38291ee5569146a303d5b11747","permalink":"https://obfuscator.re/dprotect/passes/arithmetic/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/dprotect/passes/arithmetic/","section":"dprotect","summary":" Arithmetic Obfuscation The purpose of this pass is to transform arithmetic operations into complex expressions. \u003c?xml version=\"1.0\" ?\u003e \u003c?xml version=\"1.0\" ?\u003e Because of the design of the Java bytecode, arithmetic operations are (usually) exactly translated in their dedicated bytecode.\n","tags":null,"title":"Arithmetic Obfuscation","type":"dprotect"},{"authors":null,"categories":null,"content":" Arithmetic Obfuscation The purpose of this pass is to transform arithmetic operations into complex expressions. \u003c?xml version=\"1.0\" ?\u003e int add = (x \u0026amp; y) + (x | y); int sub = (x ^ -y) + 2*(x \u0026amp; -y); int xor = (x | y) - (x \u0026amp; y); int and = (x + y) - (x | y); int or = x + y + 1 + (~x | ~y); \u003c?xml version=\"1.0\" ?\u003e int add = x + y; int sub = x - y; int xor = x ^ y; int and = x \u0026amp; y; int or = x | y; When to use it? The majority of the time, arithmetic operations (+, -, ^, \u0026amp;, |, ...) are compiled into assembly instructions that exactly resemble the original operation1.\nFor instance, consider the compilation of this function in which the xor operation is considered sensitive:\nvoid encode(uint8_t* data, size_t len) { for (size_t i = 0; i \u0026lt; len; ++i) { data[i] = data[i] ^ 0x23; } } The following sequence of AArch64 instructions is generated:\nmov w10, #35 ... ldrb w11, [x9] eor w11, w11, w10 strb w11, [x9], #1 From a reverse engineering perspective, the sensitive xor operation associated with the EOR instruction can be straightforwardly identified.\nThus, if you reckon that the arithmetic operations performed by your function may be sensitive, you may want to consider enabling this obfuscation pass.\nAs a result, when this obfuscation pass is enabled, the previous operation is transformed into a more complex expression:\n// Transformation of data[i] = data[i] ^ 0x23; uint32_t C = data[i]; int32_t X = -(C \u0026amp; 0x23); data[i] = ((C + (0xffffffdc | !C) + 0x24) ^ X) + (((C + (0xffffffdc | !C) + 0x24) \u0026amp; X) \u0026lt;\u0026lt; 1); How to use it? In the O-MVLL configuration file, you must define the following method in the Python class implementing omvll.ObfuscationConfig\ndef obfuscate_arithmetic(self, mod: omvll.Module, fun: omvll.Function) -\u0026gt; omvll.ArithmeticOpt: if fun.name == \u0026#34;encode\u0026#34;: # Explicitly define the number of iterations # applied on the arithmetic expressions return omvll.ArithmeticOpt(rounds=8) if mod.name.endswith(\u0026#34;encrypt.cpp\u0026#34;): # Obfuscate with a number of iteration defined by O-MVLL return True return False The rounds parameter defines the number of loops transforming the arithmetic operation:\nOperation : X ^ Y #1 Loop : (X | Y) - (X \u0026amp; Y) #2 Loop : (((Y + X) - (Y \u0026amp; X)) ^ ((Y | X) - (Y + X))) + 2*(((Y + X) - (Y \u0026amp; X)) \u0026amp; ((Y | X) - (Y + X))) It is more than highly recommended to run this pass with at least 2 iterations.\nSee the next section for the details\nImplementation The transformation of the arithmetic operations works by iterating over all the instructions of a basic block and selecting arithmetic operations (+, -, ^, \u0026amp;, |) that can be transformed with a Mixed Boolean-Arithmetic expression:\nfor (Instruction\u0026amp; I : BasicBlock) { auto* BinOp = dyn_cast\u0026lt;BinaryOperator\u0026gt;(\u0026amp;I); if (isSupported(*BinOp)) { // Process ... } } In the first stage, the processing of the operation consists in extracting and wrapping the arithmetic operation (e.g. add) with a function.\nFor instance, let\u0026rsquo;s consider this addition:\nint b = a + b; After creating its wrapper, it becomes:\nint __omvll_add(int x, int y) { return x + y; } int b = __omvll_add(x, y); \u0026ldquo;Why doing such transformation and not directly replacing the addition with its MBA equivalent?\u0026rdquo;\nFirst, the compiler itself is able to simplify MBA as it is discussed in the next section: Limitation \u0026amp; Attacks. One solution to prevent these optimizations is to create a function flagged with the attribute OptimizeNone, which is roughly equivalent to:\n__attribute__((optnone)) int __omvll_add(int x, int y) { return x + y; } There does not exist an equivalent attribute for basic blocks or instructions. The second advantage of this call-transformation lies in the implementation of the iteration process. If we perform the transformations on the arithmetic operations on the fly in the original basic block, for each iteration, we must re-process all the other instructions of the basic block.\nWith a wrapper, it only requires applying the iterations on the isolated function, not on all the instructions of the original basic block:\n__attribute__((optnone)) int __omvll_add(int x, int y) { E = x + y; for (size_t i = 0; i \u0026lt; NB_ITERATIONS; ++i) { E = MBA_add_xor_sub_or_and(E); } } Last but not least, we have to make sure that the wrapped function is inlined at the end of the processing otherwise, it would be a weakness.\nWrapper-\u0026gt;addFnAttr(Attribute::AlwaysInline); This LLVM inlining code is equivalent to:\n__attribute__((optnone)) __attribute__((alwaysinline)) int __omvll_add(int x, int y) { E = x + y; for (size_t i = 0; i \u0026lt; NB_ITERATIONS; ++i) { E = MBA_add_xor_sub_or_and(E); } } Regarding the transformations themselves, they rely on the built-in LLVM pattern matcher:\nInstruction\u0026amp; I = ...; Value *X, *Y; // Match X \u0026amp; Y if (!match(\u0026amp;I, m_And(m_Value(X), m_Value(Y)))) { return nullptr; } Upon a match , the instruction associated with the operation is replaced with a Mixed Boolean-Arithmetic expression (MBA):\n// (X + Y) - (X | Y) return BinaryOperator::CreateSub( Builder.CreateAdd(X, Y), Builder.CreateOr(X, Y) ); The overall structure of the transformer is highly inspired by the InstructionCombining pass and the MBA used in this pass are taken from sspam developed by Ninon Eyrolles:\nOperation MBA Transformation X ^ Y (X | Y) - (X \u0026amp; Y) X + Y (X \u0026amp; Y) + (X | Y) X - Y (X ^ -Y) + 2*(X \u0026amp; -Y) X \u0026amp; Y (X + Y) - (X | Y) X | Y X + Y + 1 + (~X | ~Y) Limitations \u0026amp; Attacks First and foremost, LLVM is able to simplify some MBAs as we can see from llvm/lib/Transforms/InstCombine/InstCombineAndOrXor.cpp:\n... // (A ^ B) ^ (A | C) --\u0026gt; (~A \u0026amp; C) ^ B -- There are 4 commuted variants. if (match(\u0026amp;I, m_c_Xor(m_OneUse(m_Xor(m_Value(A), m_Value(B))), m_OneUse(m_c_Or(m_Deferred(A), m_Value(C)))))) return BinaryOperator::CreateXor( Builder.CreateAnd(Builder.CreateNot(A), C), B); // (A ^ B) ^ (B | C) --\u0026gt; (~B \u0026amp; C) ^ A -- There are 4 commuted variants. if (match(\u0026amp;I, m_c_Xor(m_OneUse(m_Xor(m_Value(A), m_Value(B))), m_OneUse(m_c_Or(m_Deferred(B), m_Value(C)))))) return BinaryOperator::CreateXor( Builder.CreateAnd(Builder.CreateNot(B), C), A); // (A \u0026amp; B) ^ (A ^ B) -\u0026gt; (A | B) if (match(Op0, m_And(m_Value(A), m_Value(B))) \u0026amp;\u0026amp; match(Op1, m_c_Xor(m_Specific(A), m_Specific(B)))) return BinaryOperator::CreateOr(A, B); // (A ^ B) ^ (A \u0026amp; B) -\u0026gt; (A | B) if (match(Op0, m_Xor(m_Value(A), m_Value(B))) \u0026amp;\u0026amp; match(Op1, m_c_And(m_Specific(A), m_Specific(B)))) return BinaryOperator::CreateOr(A, B); ... Hence, we must be careful not to trigger these simplifications (c.f. Implementation)\nSecond, the MBA currently used in this pass are:\nFixed and statically encoded. Simplified by the LLVM optimization pipeline if misconfigured. Can be simplified with existing public tools once extracted. For the third point, msynth or Triton could be used to verify if the expressions are simplified.\nNevertheless, automatically identifying AND extracting MBAs from a compiled binary is not trivial so we can consider that this pass introduces a non-negligible overhead for the reverse engineers.\nFor these reasons and, in its current form, this pass should be considered as a cosmetic protection rather than an advanced protection.\nReferences Publications Efficient Deobfuscation of Linear Mixed Boolean-Arithmetic Expressions by Benjamin Reichenwallner, Peter Meerwald-Stadler\nImproving MBA Deobfuscation using Equality Saturation by Matteo Favaro, Tim Blazytko\nGreybox Program Synthesis: A New Approach to Attack Dataflow Obfuscation by Robin David\nGreybox Program Synthesis: A New Approach to Attack Dataflow Obfuscation by Robin David\nQSynth - A Program Synthesis based Approach for Binary Code Deobfuscation by Robin David, Luigi Coniglio, Mariano Ceccato\nObfuscation with Mixed Boolean-Arithmetic Expressions : reconstruction, analysis and simplification tools by Ninon Eyrolles\nArybo: cleaning obfuscation by playing with mixed boolean and arithmetic operations by Adrien Guinet\nTools msynth by Tim Blazytko\nqsynthesis by Robin David\nTalks Greybox Program Synthesis: A New Approach to Attack Dataflow Obfuscation by Robin David\nAdvanced Code Obfuscation With MBA Expressions by Arnau Gàmez Montolio\nAttacks Oracle Synthesis Meets Equality Saturation PoC associated with the blog post: \u0026#39;Improving MBA Deobfuscation using Equality Saturation\u0026#39;. The compiler might also optimize the operation into instructions that are equivalent but less meaningful from a reverse engineering perspective. For instance, x % 8 is usually transformed in x \u0026amp; 7. Hacker\u0026rsquo;s Delight is a good reference for this kind of transformations.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1725428622,"objectID":"40b1adb2f126b527f871c51bae2c34a5","permalink":"https://obfuscator.re/omvll/passes/arithmetic/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/omvll/passes/arithmetic/","section":"omvll","summary":" Arithmetic Obfuscation The purpose of this pass is to transform arithmetic operations into complex expressions. \u003c?xml version=\"1.0\" ?\u003e int add = (x \u0026amp; y) + (x | y); int sub = (x ^ -y) + 2*(x \u0026amp; -y); int xor = (x | y) - (x \u0026amp; y); int and = (x + y) - (x | y); int or = x + y + 1 + (~x | ~y); \u003c?xml version=\"1.0\" ?\u003e int add = x + y; int sub = x - y; int xor = x ^ y; int and = x \u0026amp; y; int or = x | y; When to use it? The majority of the time, arithmetic operations (+, -, ^, \u0026amp;, |, ...) are compiled into assembly instructions that exactly resemble the original operation1.\n","tags":null,"title":"Arithmetic Obfuscation","type":"omvll"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"a8a8fe23a65d8d70efec45bd83281e4d","permalink":"https://obfuscator.re/resources/arxan-digital-ai/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/arxan-digital-ai/","section":"resources","summary":"","tags":null,"title":"Arxan / Digital.ai","type":"resources"},{"authors":null,"categories":null,"content":" Basic Block Duplicate This pass aims at duplicating selected basic blocks and inserting a coin-flip branch that picks, at runtime, whether to execute the original block or its clone. \u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e \u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e As analyzing program\u0026rsquo;s control-flow graph is a fundamental step during static reverse engineering efforts, this pass attempts to introduce noise and redundancy by creating duplicated blocks, for the selected ones, and jump onto the original or cloned versions depending on a runtime condition.\nWhen to use it? As for the other passes, Basic Block Duplicate is best applied to functions performing security-sensitive operations. Due to the significant code size growth and noise it generates, enabling the pass with a low to moderate probability is suggested to level off the overhead. For increased security, it is recommended to activate the pass in conjunction with other passes.\nHow to use it? In the Python configuration callback, enable the pass by returning True to apply everywhere, or enable it probabilistically (at the granularity of basic blocks), as shown below:\ndef basic_block_duplicate(self, mod: omvll.Module, func: omvll.Function): # Select basic blocks with a 30% probability. return omvll.BasicBlockDuplicateWithProbability(20) Implementation The pass works in multiple stages. Each selected block is split and cloned into a new block, and within such a new block, all instructions and their operands are remapped. The original block terminator is then replaced with a conditional branch that uses a runtime coin flip (via __omvll_coinflip, whose routine is initialized once per module) to decide whether to jump to the original block or its cloned version. Afterwards, PHI nodes in the successor blocks are updated to account for the new incoming value from the cloned block. For values defined in the old or duplicated block that may have users outside of their definition, the pass may insert new PHI nodes at the merge points to preserve SSA form. The toss of a coin in __omvll_coinflip is implemented using lrand48 routine.\nFor instance, the following basic block:\nblock: %val = add i32 %x, %y br label %next Is rewritten as follows:\nblock: %coin = call i1 @__omvll_coinflip() br i1 %coin, label %block, label %block.clone block: %val.clone = add i32 %x, %y br label %next block.clone: %val.clone = add i32 %x, %y br label %next next: %val.merged = phi i32 [ %val, %block ], [ %val.clone, %block.clone ] br label %continue Limitations One may incur high performance penalty due to the overhead coming from duplicated code, in addition to an increase in code size. ","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1758444760,"objectID":"366158ddc726b7f872edb2a508ea70c5","permalink":"https://obfuscator.re/omvll/passes/basic-block-duplicate/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/omvll/passes/basic-block-duplicate/","section":"omvll","summary":" Basic Block Duplicate This pass aims at duplicating selected basic blocks and inserting a coin-flip branch that picks, at runtime, whether to execute the original block or its clone. \u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e \u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e As analyzing program\u0026rsquo;s control-flow graph is a fundamental step during static reverse engineering efforts, this pass attempts to introduce noise and redundancy by creating duplicated blocks, for the selected ones, and jump onto the original or cloned versions depending on a runtime condition.\n","tags":null,"title":"Basic Block Duplicate","type":"omvll"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"31781421aa255f5e4db2e918e3e2aa95","permalink":"https://obfuscator.re/resources/binaryninja/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/binaryninja/","section":"resources","summary":"","tags":null,"title":"BinaryNinja","type":"resources"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"2c5cfb45a45d573bd352049f5da99adb","permalink":"https://obfuscator.re/resources/blackobfuscator/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/blackobfuscator/","section":"resources","summary":"","tags":null,"title":"BlackObfuscator","type":"resources"},{"authors":null,"categories":null,"content":" Constants Obfuscation The purpose of this pass is to hide the constants used in Java/Kotlin classes. \u003c?xml version=\"1.0\" ?\u003e \u003c?xml version=\"1.0\" ?\u003e As for native code, the constants used in a Java class can provide hints for the reverse engineers about the purpose of the class.\nThis pass can be used to hide constants by masking them in a table.\nIf we consider the initialization of a ChaCha block in which the first four entries are initialized with \u0026quot;expand 32-byte k\u0026quot;:\nthis.matrix[ 0] = 0x61707865; this.matrix[ 1] = 0x3320646e; this.matrix[ 2] = 0x79622d32; this.matrix[ 3] = 0x6b206574; After applying this pass, the initialization looks like this:\nChaCha20.a[ 0] = 0x2262775c; ChaCha20.a[ 2] = 0x2fd5c988; ChaCha20.a[ 4] = 0x093052e7; ChaCha20.a[ 9] = 0x6b6cbd0a; ChaCha20.a[10] = 0x79ec4b16; ChaCha20.a[11] = 0x48c2f323; ChaCha20.a[12] = 0x535be40d; ChaCha20.a[13] = 0x5870b603; ... this.matrix[ChaCha20.a[9] ^ 0x6B6CBD0A] = a[10] ^ 0x189C3373; this.matrix[ChaCha20.a[0] ^ 0x2262775D] = a[11] ^ 0x7BE2974D; this.matrix[ChaCha20.a[2] ^ 0x2FD5C98A] = a[12] ^ 0x2A39C93F; this.matrix[ChaCha20.a[4] ^ 0x093052E4] = a[13] ^ 0x3350D377; The ChaCha20.a[] array holds the masked constants and the original matrix accesses are replaced with a lookup to the ChaCha20.a[] array.\nWhen to use it? You should enable this pass when you have (hard-coded) constants that are involved in sensitive computations.\nHow to use it? Within the dProtect configuration file we can enable this protection through:\n-obfuscate-constants \u0026lt;class specifier\u0026gt; In addition, it is highly recommended to combine this pass with Arithmetic Obfuscation as constants are usually associated with an arithmetic computation.\nImplementation In its current form, this obfuscation pass works by tabulating the constants and by masking them with a xor.\nProgrammatically, the pass implements a Proguard ClassVisitor that is used to add the static OPAQUE_CONSTANTS_ARRAY attribute:\n@Override public void visitProgramClass(ProgramClass programClass) { ClassBuilder classBuilder = new ClassBuilder(programClass); classBuilder.addAndReturnField(AccessConstants.PRIVATE | AccessConstants.STATIC, \u0026#34;OPAQUE_CONSTANTS_ARRAY\u0026#34;, \u0026#34;[J\u0026#34;); } OPAQUE_CONSTANTS_ARRAY is a long[] array that is used to store the masked constants collected by pass. The collection of the constants is performed with the combination of an InstructionVisitor and a ConstantVisitor.\nLet\u0026rsquo;s consider the protection of iconst_3 (pushing 3 onto the stack).\nThis instruction can be visited tanks to the InstructionVisitor.visitSimpleInstruction() overloaded method:\n@Override public void visitSimpleInstruction(..., SimpleInstruction instruction) { switch (instruction.opcode) { case Instruction.OP_ICONST_3: { // Processing } } The pass processes the value 3 associated with this instruction, by inserting (if not already present) the constant in an internal array of the pass:\n// Processing int value = instruction.constant; int index = getOrInsert((long)value); When inserting the value, the pass also generates a random key that is used to mask the original constant:\n// Processing int value = instruction.constant; int index = getOrInsert((long)value); Long key = keys.get(value); // Masking Key associated with the constants value Then, the pass replaces the original iconst_3 with a lookup into the OPAQUE_CONSTANTS_ARRAY:\n// Equivalent to OPAQUE_CONSTANTS_ARRAY[index] ^ key ____.getstatic(\u0026#34;OPAQUE_CONSTANTS_ARRAY\u0026#34;) .ldc(index) .laload() .l2i() .ldc(key.intValue()) .ixor() .__()); On the other hand, the OPAQUE_CONSTANTS_ARRAY is filled with the (masked) constants by updating the static { } constructor of the class:\nnew InitializerEditor().addStaticInitializerInstructions( ____ -\u0026gt; { // Equivalent to new long[constants.size()] ____.ldc(constants.size()) .newarray(Instruction.ARRAY_T_LONG) .putstatic(programClass, arrayField); // Push the constans for (int i = 0; i \u0026lt; constants.size(); ++i) { Long value = constants.get(i); Long key = keys.get(value); Long encoded = value ^ key; ____.getstatic(programClass, arrayField) .sipush(i) .ldc2_w(encoded) .lastore(); } }); Limitations First, this pass does handle (yet) double or float constants. In addition, the constants targeted by this pass are ALWAYS masked with a xor operation which could be improved by using random arithmetic-based masking.\nThis pass is also unlikely resilient against a Jadx plugin or a Proguard-based deobfuscation pass that would identify the OPAQUE_CONSTANTS_ARRAY attribute and the instructions that use it.\nJEB Decompiler also published a blog post about how this protection can be defeated: Reversing dProtect.\nAttacks Reversing dProtect - Constants Scrambling This blog post explains how JEB Decompiler can recover constants protected with dProtect. DOptUnsafeArrayAccessSubstV3.java JEB Script used for recovering obfuscated constants. ","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"1905f091a1e3e1ea8f64e207bafb2a4d","permalink":"https://obfuscator.re/dprotect/passes/constants/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/dprotect/passes/constants/","section":"dprotect","summary":" Constants Obfuscation The purpose of this pass is to hide the constants used in Java/Kotlin classes. \u003c?xml version=\"1.0\" ?\u003e \u003c?xml version=\"1.0\" ?\u003e As for native code, the constants used in a Java class can provide hints for the reverse engineers about the purpose of the class.\n","tags":null,"title":"Constants Obfuscation","type":"dprotect"},{"authors":null,"categories":null,"content":" Control-Flow Breaking The purpose of this pass is to break the control-flow representation. \u003c?xml version=\"1.0\" ?\u003e \u003c?xml version=\"1.0\" ?\u003e Control-Flow representation is an important piece of information for reverse engineers as it is used to represent elementary computational block (or basic block) and how they relate to each other.\nA large part of the decompilation process relies on such a control flow representation, and the disassembler will likely fail the decompilation if it does not manage to determine the control-flow graph of the function.\nThe idea of this pass is to inject custom and non-executing instructions at the beginning of the function which break and unalign – to some extent – the analysis of the disassemblers.\nHere are the results with IDA and BinaryNinja:\nIDA BinaryNinja Actually, in BinaryNinja we can recover the original code by manually creating the function where they are undefined:\nThe cross references are still broken though.\nThe output of Ghidra is very close to BinaryNinja: without a manual action, Ghidra fails to represent the function.\nWith manual actions, Ghidra manages to recover the function but misses the first instructions (stack allocation). When to use it? If you consider that a function is sensitive, you may want to enable this pass. This protection has the advantage to introduce a very low overhead on the execution and the binary\u0026rsquo;s size while being pretty efficient against decompilation and static analysis.\nHow to use it? We can trigger this pass by defining the function break_control_flow in the configuration class:\nclass Config(omvll.ObfuscationConfig): def break_control_flow(self, mod: omvll.Module, func: omvll.Function): if func.demangled_name.startswith(\u0026#34;break_control_flow\u0026#34;): return True return False Implementation This obfuscation pass works in three steps:\nClone the original function Inject a custom-breaking prologue in the cloned function Remove all the instructions of the original function and write a stub to jump after the custom prologue (i.e. the original instruction) These steps are summarized in the following figure:\n\u003c?xml version=\"1.0\" ?\u003e 1. Cloning First, we need to clone the original function since we want to create a custom jump after the breaking prologue:\nbool BreakControlFlow::runOnFunction(Function \u0026amp;F) { ValueToValueMapTy VMap; Function* FCopied = CloneFunction(\u0026amp;F, VMap, nullptr); } 2. Injecting Then, all the efficiency of this obfuscation pass relies on injecting a custom assembly prologue that should confuse disassemblers.\nIn its current form, the pass injects a prologue similar to this code:\nadr x1, #0x10; ldr x0, [x1, #31]; ldr x1, #16; blr x1; ldr x1, #48; blr x3; .byte 0xF1, 0xFF, 0xF2, 0xA2; .byte 0xF8, 0xFF, 0xE2, 0xC2; The first instructions are used to make believe that the code is consistent while the last .byte \u0026ldquo;instructions\u0026rdquo; are used to break this consistency.\nWithin the consistent instructions, there is a bunch of ldr x0, #offset 1 which are used to confuse the disassembler about the type of the data at the given pc-relative #offset.\nldr x0, #offset would load data at pc + #offset which likely points in the code of the function to protect. Therefore, the disassembler might consider that the content at this address is not an instruction, while it is.\nThis is the reason why in IDA, we have this broken representation of the function:\nThe custom prologue is Jitted with the same mechanism as described in the Anti-Hooking pass:\nauto jitter = Jitter::Create(F.getParent()-\u0026gt;getTargetTriple()); std::unique_ptr\u0026lt;MemoryBuffer\u0026gt; insts = jitter_-\u0026gt;jitAsm(R\u0026#34;delim( adr x1, #0x10; ldr x0, [x1, #61]; ldr x1, #16; blr x1; ldr x1, #48; blr x3; .byte 0xF1, 0xFF, 0xF2, 0xA2; .byte 0xF8, 0xFF, 0xE2, 0xC2; )delim\u0026#34;); FCopied-\u0026gt;setPrologueData(insts); 3. Jumping Once we have injected the custom prologue, we must replace the instructions of the original function with a jump right after the injected prologue.\nUsing the LLVM IR, we can get a \u0026ldquo;pointer\u0026rdquo; to the cloned function using the CreatePtrToInt instruction:\nIRBuilder\u0026lt;NoFolder\u0026gt; IRB(Entry); IRB.CreatePtrToInt(FCopied, IRB.getInt64Ty()); Then, we can add the offset of the prologue to this value:\nIRBuilder\u0026lt;NoFolder\u0026gt; IRB(Entry); IRB.CreateAdd(IRB.CreatePtrToInt(FCopied, IRB.getInt64Ty()), ConstantInt::get(IRB.getInt64Ty(), PrologueSize)); Finally, we can create a call:\nIRBuilder\u0026lt;NoFolder\u0026gt; IRB(Entry); IRB.CreateAdd(IRB.CreatePtrToInt(FCopied, IRB.getInt64Ty()), ConstantInt::get(IRB.getInt64Ty(), PrologueSize)); Value* FPtr = IRB.CreatePointerCast(FAddr, FCopied-\u0026gt;getFunctionType()); IRB.CreateRet(IRB.CreateCall(FCopied-\u0026gt;getFunctionType(), FPtr, args)); Without any additional protections, the indirect call \u0026amp;FCopied + PrologueSize would be resolved by the disassemblers which can strongly weaken the protection.\nTo prevent this resolution, we are applying MBA and opaque constants thanks to the annotations mechanism:\nValue* DstAddr = IRB.CreateAdd(OpaqueFAddr, ConstantInt::get(IRB.getInt64Ty(), PrologueSize)); if (auto* Op = dyn_cast\u0026lt;Instruction\u0026gt;(DstAddr)) { addMetadata(*Op, {MetaObf(OPAQUE_OP, 2llu), MetaObf(OPAQUE_CST)}); } In the end, the wrapper looks like this:\nLimitations Small functions As already mentioned, this pass could be defeated with manual actions on the inconsistent code. In addition, the disassemblers might improve their heuristics in future versions which would make this pass less efficient.\nThere is also a limitation for small functions as the injected prologue assumes that pc + {16, 48, ...} points in the function to protect. If the function is too small, these offsets might point in a function next to the targeted one.\niOS Swift generated functions The Swift compiler may introduce helper functions / thunks – which use different calling conventions –, in ways that are not visible at the source level. If BreakControlFlow pass is applied to these compiler-generated routines, it may cause duplicate symbol errors or violate the expected calling convention.\nIn order to prevent such issues, restrict the pass to user-defined functions only.\nThis instruction loads the content at pc + #offset\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1758443561,"objectID":"2bdd749bc34bcf1d9eadb74af5359a8c","permalink":"https://obfuscator.re/omvll/passes/control-flow-breaking/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/omvll/passes/control-flow-breaking/","section":"omvll","summary":" Control-Flow Breaking The purpose of this pass is to break the control-flow representation. \u003c?xml version=\"1.0\" ?\u003e \u003c?xml version=\"1.0\" ?\u003e Control-Flow representation is an important piece of information for reverse engineers as it is used to represent elementary computational block (or basic block) and how they relate to each other.\n","tags":null,"title":"Control-Flow Breaking","type":"omvll"},{"authors":null,"categories":null,"content":" Control-Flow Flattening The purpose of this pass is to modify the graph representation of a function, by flattening its basic blocks. \u003c?xml version=\"1.0\" ?\u003e \u003c?xml version=\"1.0\" ?\u003e When to use it? While reverse engineering a function, the control-flow graph is really important for as it provides information about the different conditions to reach the basic blocks of the function. In addition, the decompilation process relies \u0026ndash; to some extend \u0026ndash; on this control flow.\nLet\u0026rsquo;s consider this function:\nbool check_password(const char* passwd, size_t len) { if (len != 5) { return false; } if (passwd[0] == \u0026#39;O\u0026#39;) { if (passwd[1] == \u0026#39;M\u0026#39;) { if (passwd[2] == \u0026#39;V\u0026#39;) { if (passwd[3] == \u0026#39;L\u0026#39;) { if (passwd[4] == \u0026#39;L\u0026#39;) { return true; } } } } } return false; } When the function is compiled, we can observe the following graphs in the reverse engineering tools:\nWe can clearly identify both: the conditions and how the sequence of the basic block is driven. By using the control-flow flattening protection, the (basic) blocks a wrapped in a kind of switch-case:\nbool check_password(const char* passwd, size_t len) { state_t state; switch (ENC(state)) { case 0xedf34: state = (len != 5) ? 0x48179 : 0xd51b; break; case 0xedf34: state = (passwd[0] == \u0026#39;O\u0026#39;) ? 0x48179 : 0xd51b; break; case 0xedf34: state = (passwd[0] == \u0026#39;O\u0026#39;) ? 0x48179 : 0xd51b; break; ... case XXX: return true; case XXX: return false; } } From this previous code, we could \u0026ndash; with some effort \u0026ndash; recover the semantic of the original function but when a reverse engineer has to deal with this protection on a large function, it\u0026rsquo;s a static pain. In the following slider, you can observe the effect of this pass on the control-flow graph:\nConsequently, if the overall logic of the function is sensitive, you should enable this pass.\nTo provide enhanced protection, this pass should be combined with data flow obfucation passes like String Encoding, Opaque Fields Access, Arithmetic Obfuscation. How to use it? In the O-MVLL configuration file, you must define the following method in the Python class implementing omvll.ObfuscationConfig:\ndef flatten_cfg(self, mod: omvll.Module, func: omvll.Function): if func.name == \u0026#34;check_password\u0026#34;: return True return False Implementation This pass was already present in the original O-LLVM project (Obfuscation/Flattening.cpp), but we did some improvements.\nFirst, the next state to execute is determined by a random identifier (like in O-LLVM) but also by an encoding function. In the original implementation of O-LLVM, we have this kind of transformation:\nswitch (state) { case 0xedf34: state = 0x48179; case 0x48179: state = ...; } In the O-MVLL implementation, we added an encoding on the state variable:\nswitch (Encoding(state)) { case 0xedf34: state = 0xaaaa; case 0x48179: state = ...; } This additional encoding is used to hinder a static identification of the next states of a basic block. It also hinders attacks which would consist in tracing the memory writes accesses on the state variable. This encoding is not a very strong additional protection but it introduces overhead for the reverse engineer .\nSecond enhancement, the default basic block of the switch case is filled with corrupted assembly instructions:\nauto* defaultCase = BasicBlock::Create(F.getContext(), \u0026#34;DefaultCase\u0026#34;, \u0026amp;F, flatLoopEnd); ... Value* rawAsm = InlineAsm::get( FType, R\u0026#34;delim( ldr x1, #-8; blr x1; mov x0, x1; .byte 0xF1, 0xFF; .byte 0xF2, 0xA2; )delim\u0026#34;, \u0026#34;\u0026#34;, /* hasSideEffects */ true, /* isStackAligned */ true ); DefaultCaseIR.CreateCall(FType, rawAsm); DefaultCaseIR.CreateBr(flatLoopEnd); This stub is the reason why IDA fails to represent the graph of the function being flattened.\nldr Xz, #-+OFFSET is pretty efficient to break the graph representation in IDA (but it does not affect the other tools). The default case of the switch should not be reached as all the states should be covered by the pass (or there is a bug). Thus, we can leverage this basic block to put instructions that are confusing for the disassemblers.\nLimitations This pass modifies the sequence of the basic blocks to hinder the overall structure of the function, BUT it does not protect the code of the individual basic blocks.\nThis means that a reverse engineer can still analyze the function at a basic block level to potentially extract the original logic. That\u0026rsquo;s why the basic blocks need to be protected with other obfuscation passes like String Encoding, Opaque Fields Access, Arithmetic Obfuscation.\nIn the current implementation, the encoding function is linear and does not change:\n$$E(S) = (S \\oplus A) + B$$\nNevertheless, $A$ and $B$ are randomly selected for each function.\nReferences Publications Control Flow Flattening: How to build your own by Sam Russell\nApproov Control Flow Unflattening by Ahmet Bilal Can\niOS Native Code Obfuscation and Syscall Hooking by Romain Thomas\nD810: A journey into control flow unflattening by Batteaux Boris\nAutomated Detection of Control-flow Flattening by Tim Blazytko\nDeobfuscation: recovering an OLLVM-protected program by Francis Gabriel\nTools d810 by Boris Batteux\nd810 (fork by Ahmet Bilal Can) by Ahmet Bilal Can\nAttacks ollvm-breaker BinaryNinja script to recover O-LLVM flattened function llvm-deobfuscator Performs the inverse operation of the control flow flattening pass ","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1725428622,"objectID":"9e8c7dbd9081fe7d66c0b9066fb46236","permalink":"https://obfuscator.re/omvll/passes/control-flow-flattening/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/omvll/passes/control-flow-flattening/","section":"omvll","summary":" Control-Flow Flattening The purpose of this pass is to modify the graph representation of a function, by flattening its basic blocks. \u003c?xml version=\"1.0\" ?\u003e \u003c?xml version=\"1.0\" ?\u003e When to use it? While reverse engineering a function, the control-flow graph is really important for as it provides information about the different conditions to reach the basic blocks of the function. In addition, the decompilation process relies \u0026ndash; to some extend \u0026ndash; on this control flow.\n","tags":null,"title":"Control-Flow Flattening","type":"omvll"},{"authors":null,"categories":null,"content":" Control-Flow Obfuscation The purpose of this pass is to protect the code of the functions to hinder reverse-engineering. \u003c?xml version=\"1.0\" ?\u003e \u003c?xml version=\"1.0\" ?\u003e public static boolean test1(int x) { boolean True = x == 1; boolean False = x * x - 1 != (x - 1) * (x + 1); return x == 1 ? True : False; } The control-flow graph of a function is a representation of the elementary computational blocks and the conditions to reach them. This representation is usually an early step in the decompilation process.\nFor instance, let\u0026rsquo;s consider the isWithXor function implemented in the class Argon2Function.java of the project Password4j:\nprivate boolean isWithXor(int pass) { return !(pass == 0 || version == ARGON2_VERSION_10); } Once compiled, we get this flat representation of its bytecode:\nprivate boolean isWithXor(int pass) { L0 { iload 1 ifeq L1 aload 0 // reference to self getfield com/password4j/Argon2Function.version:int bipush 16 if_icmpeq L1 iconst_1 goto L2 } L1 { iconst_0 } L2 { ireturn } } The control-flow graph of this function is represented in the following figure in which we can observe the two conditions that lead to the return of true (iconst_1) or false (iconst_0)\nIn the end, decompilers can use this representation to generate code or pseudo code that is close to the original implementation:\n\u003c?xml version=\"1.0\" ?\u003eCodeSmaliObfuscatedResourcesAPK signatureSummarySource codeandroid.support.v4RArgon2FunctiononPostExecute15483()voidm19cdd()voidcdd()intattachBaseContext(Context)voidattachBaseContext(String)StringattachBaseContext(byte[], int)intattachBaseContext(int, int)intdbdaObject[]badbooleancom.password4jcom.sample.password4j.apkArgon2FunctionViewFileNavigationHelpToolspackage com.password4j;public class Argon2Function extends AbstractHashingFunction { private boolean isWithXor(int pass) { return (pass == 0 || this.version == 16) ? false : true; }} When the control flow is obfuscated, the output of the decompilation is less readable and the logic of the function takes more time to be reverse-engineered:\n\u003c?xml version=\"1.0\" ?\u003eCodeSmaliObfuscatedResourcesAPK signatureSummarySource codeandroid.support.v4RArgon2FunctiononPostExecute15483()voidm19cdd()voidcdd()intattachBaseContext(Context)voidattachBaseContext(String)StringattachBaseContext(byte[], int)intattachBaseContext(int, int)intdbdaObject[]badbooleancom.password4jcom.sample.password4j.apkArgon2FunctionViewFileNavigationHelpToolspackage com.password4j;public class Argon2Function extends AbstractHashingFunction { private boolean isWithXor(int i) { if (i != 0) { for (int i2 = this.version; i2 != 16; i2 = 1) { if ((a + 1) % 2 != 0) { return true; } } } return false; }} \u003c?xml version=\"1.0\" ?\u003eCodeSmaliObfuscatedResourcesAPK signatureSummarySource codeandroid.support.v4RArgon2FunctiononPostExecute15483()voidm19cdd()voidcdd()intattachBaseContext(Context)voidattachBaseContext(String)StringattachBaseContext(byte[], int)intattachBaseContext(int, int)intdbdaObject[]badbooleancom.password4jcom.sample.password4j.apkArgon2FunctionViewFileNavigationHelpToolspackage com.password4j;public class Argon2Function extends AbstractHashingFunction { private boolean isWithXor(int pass) { return (pass == 0 || this.version == 16) ? false : true; }} O-MVLL provides a similar protection for native code through the following passes:\nControl-Flow Flattening Control-Flow Breaking When to use it? You can use this obfuscation pass for methods that have sensitive logic in terms of input/output processing. Usually, this protection should be enabled class-wide to provide a good level of protection.\nIn addition, it is usually a good practice to obfuscate functions that are close to your (real) sensitive function for introducing confusion about where the sensitive logic is located.\nIn other words, if you obfuscate only one function among several, reverse engineers will likely focus on this single protected function.\nHow to use it? You can trigger this pass with the option: -obfuscate-control-flow:\n-obfuscate-control-flow class com.password4j.Argon2Function { *; } -obfuscate-control-flow class com.dprotect.** { *; } In its current form, this pass takes a class specifier as argument and does not have extra modifiers. We highly recommend combining this pass with other obfuscation passes like Arithmetic Obfuscation and Constants Obfuscation.\nWith these additional passes the previous isWithXor() function is more complicated to reverse:\n\u003c?xml version=\"1.0\" ?\u003eCodeSmaliObfuscatedResourcesAPK signatureSummarySource codeandroid.support.v4RArgon2FunctiononPostExecute15483()voidm19cdd()voidcdd()intattachBaseContext(Context)voidattachBaseContext(String)StringattachBaseContext(byte[], int)intattachBaseContext(int, int)intdbdaObject[]badbooleancom.password4jcom.sample.password4j.apkArgon2FunctionViewFileNavigationHelpToolspackage com.password4j;public class Argon2Function extends AbstractHashingFunction { private boolean isWithXor(int i) { if (i != 0) { for (int i2 = this.version; i2 != 16; i2 = 1) { if ((a + 1) % 2 != 0) { return true; } } } return false; }}package com.password4j;public class Argon2Function extends AbstractHashingFunction { private boolean isWithXor(int i) { if (i != 0) { int i2 = this.version; long[] jArr = b; if (i2 != (((int) jArr[12]) ^ 1597221156)) { boolean z = ((int) jArr[2]) ^ 1626592579; int i3 = ((int) jArr[49]) ^ 240201901; a = i3; int i4 = ((i3 * i3) + i3) + (((int) jArr[10]) ^ 183466799) i4 = i4 % (((int) jArr[11]) ^ 1817805206); return z; } } return ((int) b[1]) ^ 1392894933; }} \u003c?xml version=\"1.0\" ?\u003eCodeSmaliObfuscatedResourcesAPK signatureSummarySource codeandroid.support.v4RArgon2FunctiononPostExecute15483()voidm19cdd()voidcdd()intattachBaseContext(Context)voidattachBaseContext(String)StringattachBaseContext(byte[], int)intattachBaseContext(int, int)intdbdaObject[]badbooleancom.password4jcom.sample.password4j.apkArgon2FunctionViewFileNavigationHelpToolspackage com.password4j;public class Argon2Function extends AbstractHashingFunction { private boolean isWithXor(int pass) { return (pass == 0 || this.version == 16) ? false : true; }} Implementation In its current form, this protection works by targeting the GOTO #offset instructions. Basically, the idea is to use an opaque predicate to create an opaque condition on the (unconditional) goto:\n\u003c?xml version=\"1.0\" ?\u003e We can target these instructions by using a Proguard\u0026rsquo;s InstructionVisitor in which we override the visitBranchInstruction method:\npublic class ControlFlowObfuscation implements ClassVisitor, ..., InstructionVisitor { @Override public void visitAnyInstruction(...) { /* Do nothing, only target branches */ } @Override public void visitBranchInstruction(Clazz clazz, Method method, CodeAttribute codeAttribute, int offset, BranchInstruction branch) { // ... } } Within the visitBranchInstruction method, we can filter the GOTO instructions and replace them with an opaque block:\nvoid visitBranchInstruction(...) { // Only target GOTO instructions if (branch.opcode != Instruction.OP_GOTO) { return; } // Create instructions builder // This is more or less the equivalent of llvm::IRBuilder InstructionSequenceBuilder ____ = new InstructionSequenceBuilder((ProgramClass)clazz); // Create the opaque predicate { ____.ldc(0x456) .ldc(0x123) .iadd() ... // } // Create the opaque condition { ____.ifne(branch.branchOffset) // \u0026lt;--- ALWAYS TAKEN .goto(???) // \u0026lt;--- NEVER TAKEN // } } For the opaque predicates, the pass uses well-known equations:\n$(X + 1) \\neq 0 \\mod 2$ when $X$ is even $X^2 + X + 7 \\neq 0 \\mod 81$ $7X^2 - 1 - X \\neq 0$ These equations are randomly chosen upon a GOTO replacement. Then, this opaque predicate is followed by two created instructions:\nvoid visitBranchInstruction(...) { ... ____.ifne(branch.branchOffset) .goto(???) } On the ifne instruction, because of the previous opaque predicates that are always returning a non-zero value, the condition is true and it jumps to the original goto offset: branch.branchOffset.\nOn the other hand, there is still a pending question about the offset of the never-taken .goto(???)?\nIn a first attempt, we can create (along with the opaque instructions) a new block that would perform useless computations since it\u0026rsquo;s never reached:\nCodeAttributeEditor.Label OPAQUE_BLOCK = codeAttributeEditor.label(); ____.label(OPAQUE_BLOCK) .iconst_1() .pop() .iconst_2() .pop(); ... ____.ifne(branch.branchOffset) .goto(OPAQUE_BLOCK.offset()) This approach works well but it has some drawbacks:\n1. Overhead For one goto replaced, the pass introduces: sizeof(Opaque Predicate) + sizeof(ifne) + sizeof(goto) + sizeof(OPAQUE_BLOCK) of new instructions\n2. (Non) Confusion With this approach, the code generated is not really confusing for the decompilers and once the opaque predicates are identified, it would quite easy to get rid of them.\nIdeally, it could be better to goto an already-existing instruction:\n\u003c?xml version=\"1.0\" ?\u003e Java is far less permissive than native code for introducing inconsistent code. In particular, we can\u0026rsquo;t jump anywhere in the current method as it could be inconsistent with the expected stack frame. Because of the Java bytecode verifier, we can goto into an existing offset, only where the stack frame and the local frame match the current one.\nTo better understand this restriction, let\u0026rsquo;s consider the following bytecode in which the last column traces the status of the stack after the instruction:\n// int var = 1 + 2; # | Inst | Stack (after) ----------------------------- 0 | nop | - 1 | iconst_1 | Int(1) 2 | iconst_2 | Int(1), Int(2) 3 | iadd | Int(3) 4 | istore_1 | - 5 | goto +4 | - Since the layout of the stack before executing the goto +4 is empty, we can only jump to index 1 where the stack is also empty.\n// int var = 1 + 2; # | Inst | Stack (after) ----------------------------- 0 | nop | - \u0026lt;---------+ 1 | iconst_1 | Int(1) | 2 | iconst_2 | Int(1), Int(2) | 3 | iadd | Int(3) | 4 | istore_1 | - | 5 | goto +4 | - -------+ Therefore, to correctly goto a valid offset, we must be able to determine the stack frame (and the local frame) for the different instructions of the bytecode.\nThis computation can be performed with the PartialEvaluator available in ProguardCORE. The purpose of this component and how it works is described in the official documentation of ProguardCORE: Data flow analysis with partial evaluation.\nIn the context of this obfuscation pass, we are using the PartialEvaluator to identify all the instructions for which the stack/local frame matches the current one associated with the obfuscated goto. For the details, you can check out the FrameFinder class in the file ControlFlowObfuscation.java.\nIn the case where we can\u0026rsquo;t find a suitable offset to goto, the pass fallback in the creation of an opaque block (OPAQUE_BLOCK).\nLimitations This pass is currently limited to the goto instructions. This means that if the method does not have at least one goto, the method won\u0026rsquo;t be protected by the pass.\nIn addition, I guess that the opaque predicates could be pattern-matched and potentially simplified. Nevertheless, at the time of writing and as far I know, it does not exist public tools/scripts that could be used to recover the original control flow.\nReferences Java Control Flow Obfuscation by Douglas Low (1998) ","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1672682740,"objectID":"b558255ac5ad4b60bc43693338963a04","permalink":"https://obfuscator.re/dprotect/passes/control-flow/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/dprotect/passes/control-flow/","section":"dprotect","summary":" Control-Flow Obfuscation The purpose of this pass is to protect the code of the functions to hinder reverse-engineering. \u003c?xml version=\"1.0\" ?\u003e \u003c?xml version=\"1.0\" ?\u003e public static boolean test1(int x) { boolean True = x == 1; boolean False = x * x - 1 != (x - 1) * (x + 1); return x == 1 ? True : False; } The control-flow graph of a function is a representation of the elementary computational blocks and the conditions to reach them. This representation is usually an early step in the decompilation process.\n","tags":null,"title":"Control-Flow Obfuscation","type":"dprotect"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"8306d3759d993c9a5ef457185e29f51f","permalink":"https://obfuscator.re/resources/declang/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/declang/","section":"resources","summary":"","tags":null,"title":"DeClang","type":"resources"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"d2935dcbb9395a36e7eee7e377b6b1b8","permalink":"https://obfuscator.re/resources/guardsquare/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/guardsquare/","section":"resources","summary":"","tags":null,"title":"DexGuard / iXGuard","type":"resources"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"4a8b85ab21393fa42b2c76d24ede88a6","permalink":"https://obfuscator.re/resources/dexprotector/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/dexprotector/","section":"resources","summary":"","tags":null,"title":"DexProtector","type":"resources"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"8de60e492e0cd12dc8598b1396eabc7d","permalink":"https://obfuscator.re/resources/frida/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/frida/","section":"resources","summary":"","tags":null,"title":"Frida","type":"resources"},{"authors":null,"categories":null,"content":" Function Outline This pass aims at fragmenting the program\u0026rsquo;s functions by outlining selected basic blocks into new dedicated functions. The outlined region is replaced with a call-site to the newly-created function. \u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e \u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e Static analysis tools rely on examining the control-flow graph of a function to understand its logic and reason about its behavior. This pass attempts to disrupt this premise by randomly breaking up function bodies, lifting basic blocks into dedicated functions.\nWhen to use it? As for the other passes, it is recommended to employ FunctionOutline on functions calling security-sensitive routines. To limit code-size growth and extra calls overhead, it is also recommended to enable the pass probabilistically. Furthermore, it may be recommended to schedule the pass either after Inliner has executed, or marking the outlined function as noinline. For increased security, it is suggested to enable the pass in conjunction with other passes.\nHow to use it? In the Python configuration callback, enable the pass by returning True to apply everywhere, or enable it probabilistically (at the granularity of basic blocks), as shown below:\ndef function_outline(self, mod: omvll.Module, func: omvll.Function): # Select basic blocks with a 30% probability. return omvll.FunctionOutlineWithProbability(20) Implementation The pass visits each function and samples randomly outline candidates based on a user-provided probability. For each collected basic block, some checks filters are performed (on top of CodeExtractor::isEligible ones), such as excluding blocks with stack manipulation intrinsics or those performing exception handling. LLVM CodeExtractor utility is then used on the single-block region to create a new function and replace the original block with a call-site to it. Any values defined or used across the boundary may become parameters or return values.\nFor example, the following basic block:\nentry: ; ... br label %block block: %add = add i32 %x, %y ret i32 %add May be rewritten as follows (modulo some optimizations):\nentry: %val = call i32 @outlined.func(i32 %x, i32 %y) ret i32 %val Where outlined.func is the outlined function:\ndefine i32 @outlined.func(i32 %x, i32 %y) { entry: %add = add i32 %x, %y ret i32 %add } Limitations One may incur moderate performance penalty due to the overhead coming from increased code size as well as extra bookkeeping (prologues/epilogues, pushing return address, etc.) for the new outlined functions. ","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1758444760,"objectID":"0fcea2919e2ddf4ad1cd42c1c65a4359","permalink":"https://obfuscator.re/omvll/passes/function-outline/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/omvll/passes/function-outline/","section":"omvll","summary":" Function Outline This pass aims at fragmenting the program\u0026rsquo;s functions by outlining selected basic blocks into new dedicated functions. The outlined region is replaced with a call-site to the newly-created function. \u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e \u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e Static analysis tools rely on examining the control-flow graph of a function to understand its logic and reason about its behavior. This pass attempts to disrupt this premise by randomly breaking up function bodies, lifting basic blocks into dedicated functions.\n","tags":null,"title":"Function Outline","type":"omvll"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"9c37ba8a03e5d179cd99eee89c6f01f3","permalink":"https://obfuscator.re/resources/ghidra/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/ghidra/","section":"resources","summary":"","tags":null,"title":"Ghidra","type":"resources"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"e0e9964baa6751ed151bb41e40a46e6a","permalink":"https://obfuscator.re/resources/goron/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/goron/","section":"resources","summary":"","tags":null,"title":"goron","type":"resources"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"e1f17d637f32919cbfbdb6ea6fc5fc72","permalink":"https://obfuscator.re/resources/hikari/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/hikari/","section":"resources","summary":"","tags":null,"title":"Hikari","type":"resources"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"891e81b39ccff03abc3a30ec1c2295fa","permalink":"https://obfuscator.re/resources/hikari15/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/hikari15/","section":"resources","summary":"","tags":null,"title":"Hikari-LLVM15","type":"resources"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"2cc00cdb07de0982882ae46e39e40bef","permalink":"https://obfuscator.re/resources/ida/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/ida/","section":"resources","summary":"","tags":null,"title":"IDA","type":"resources"},{"authors":null,"categories":null,"content":" Indirect Branch This pass aims at concealing the program control-flow graph by converting direct branches into indirect ones, making jump targets statically unknown. Jump destination are fetched from a shuffled, read-only jump table. There are known limitations on iOS for applications built in Release mode. \u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e \u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e Static analysis tools rely on explicit branch targets when attempting to reconstruct the boundaries of a function and its control-flow, as accurately as possible. IndirectBranch pass aims at severing the control-flow edges: each jump successor is added to a table, which is later indexed in order to compute the jump target at runtime.\nWhen to use it? As for the other passes, it is recommended to employ IndirectBranch on sensible functions. The pass comes with a high runtime performance overhead and extra code size, hence, it is advisable to enable it with a low probability, or on a handful of selected routines. For increased security, it is suggested to enable the pass in conjunction with other passes.\nHow to use it? In the Python configuration callback, the pass is suggested to be enabled for selective usage by leveraging the default_config method as follows:\ndef indirect_branch(self, mod: omvll.Module, func: omvll.Function): # Skip obfuscating third-party modules and apply the pass with a 5% likelihood. return omvll.ObfuscationConfig.default_config(self, mod, fun, [\u0026#34;third-party/\u0026#34;], [], [], 5) Implementation First off, branch and switch instructions terminators are collected. These are those which delimits a basic block. Likewise, all the basic block successors are gathered, and a per-function global array is created with all the basic block addresses (a LLVM BlockAddress), sparsed randomly.\nNext, each previously gathered branch is visited and is replaced into an indirect branch by locating and loading the actual successor target from the jump table. More specifically, the following br IR instruction performing a conditional branch depending on whether the integer %value is zero or not:\n%cmp = icmp eq i32 %value, 0 br i1 %cmp, label %true, label %false Is rewritten as follows:\n%idx.bb.true = getelementptr inbounds ([8 x i64], ptr @.indbr.block_addresses, i64 0, i64 %idx) %idx.bb.false = getelementptr inbounds ([8 x i64], ptr @.indbr.block_addresses, i64 0, i64 %idx2) %bb.true_or_bb.false = select i1 %cmp, ptr %idx.bb.true, ptr %idx.bb.false %bb.address = load ptr, ptr %bb.true_or_bb.false indirectbr ptr %bb.address, [label %true, label %false] As it can be seen, the direct jump has been translated via a indirectbr, whose target address derives from a blockaddress stored in the @.indbr.block_addresses global array. Depending on condition %cmp, the basic block address corresponding to the true label or the false one is loaded and passed as operand to the indirectbr.\nAs such, the jump address label is reconstructed and computed at runtime.\nLimitations The pass currently has known limitations on iOS applications built in Release mode.\nFunctions annotated with alwaysinline are skipped as indirectbr instructions may prevent inlining from occurring.\nThe pass currently attempts to translate LLVM critical edges too, although this may break invariants that later code-motion optimizations expect.\nOne may incur high performance penalties due to the overhead coming from the further layer of indirection.\n","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1757649987,"objectID":"a28680864e4722d659aa21896ba83cd8","permalink":"https://obfuscator.re/omvll/passes/indirect-branch/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/omvll/passes/indirect-branch/","section":"omvll","summary":" Indirect Branch This pass aims at concealing the program control-flow graph by converting direct branches into indirect ones, making jump targets statically unknown. Jump destination are fetched from a shuffled, read-only jump table. There are known limitations on iOS for applications built in Release mode. \u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e \u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e Static analysis tools rely on explicit branch targets when attempting to reconstruct the boundaries of a function and its control-flow, as accurately as possible. IndirectBranch pass aims at severing the control-flow edges: each jump successor is added to a table, which is later indexed in order to compute the jump target at runtime.\n","tags":null,"title":"Indirect Branch","type":"omvll"},{"authors":null,"categories":null,"content":" Indirect Call This pass aims at concealing the program\u0026rsquo;s call graph by converting direct function calls into indirect calls, whose target address is reconstructed on the fly from two random, statically stored shares. \u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e \u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e Static analysis tools lean heavily on the call graph for interprocedural analyses, as it allows them to follow control-flow across different functions, propagate data-flow facts, as well as figure out how these calls affect the state of the program. IndirectCall pass attempts to break these premises, by hiding function call edges via an extra layer of indirection, whilst preserving the original execution semantics.\nWhen to use it? As for the other passes, it is recommended to employ IndirectCall on functions calling security-sensitive routines, where these latter are possibly referenced only a handful of times. In order to incur low runtime overhead and keep the code size moderate, it is also recommended to enable the pass probabilistically. For increased security, it is suggested to enable the pass in conjunction with other passes.\nHow to use it? In the Python configuration callback, enable the pass by returning True to apply everywhere, or, e.g., the following for selective usage:\ndef indirect_call(self, mod: omvll.Module, func: omvll.Function): # Skip obfuscating third-party modules and apply the pass with a 20% likelihood. return omvll.ObfuscationConfig.default_config(self, mod, fun, [\u0026#34;third-party/\u0026#34;], [], [], 20) Implementation At each eligible call-site, the pass picks an aligned random value $S_1$ and computes:\n$$S_2 = S_1 + CalleeAddress$$\nas a LLVM constant expression. Such direct calls are collected, and so is being done for the shares, which are grouped into two separate internal read-only global arrays.\nNext, each previously gathered call-site is visited, and the called function operand is replaced with the difference of the two shares, both indexed from the two arrays for this call-site. More specifically, the following IR instruction performing a call to function callee:\ncall void @callee() Is rewritten as follows:\n%idx.share1 = getelementptr inbounds ([8 x i64], ptr @.icall.shares1, i64 0, i64 %idx) %idx.share2 = getelementptr inbounds ([8 x i64], ptr @.icall.shares2, i64 0, i64 %idx) %share1 = load i64, ptr %idx.share1 %share2 = load i64, ptr %idx.share2 %callee_address = sub i64 %share2, %share1 %fn_ptr = inttoptr i64 %callee_address to ptr call void %fn_ptr() As it can be seen, the two shares are loaded from the two global arrays @.icall.shares1 and @.icall.shares2, and the difference between the two is carried out. The result is cast to a pointer, which is now used as operand of the call-site, leading to a call to a pointer to the original function.\nAs such, the target address is reconstructed and computed at runtime.\nLimitations Functions marked as alwaysinline are skipped to let the call-site be inlineable.\nOne may incur performance penalties due to the overhead coming from the further layer of indirection.\n","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1757649987,"objectID":"bbee2621038ebbb1925ee99370104980","permalink":"https://obfuscator.re/omvll/passes/indirect-call/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/omvll/passes/indirect-call/","section":"omvll","summary":" Indirect Call This pass aims at concealing the program\u0026rsquo;s call graph by converting direct function calls into indirect calls, whose target address is reconstructed on the fly from two random, statically stored shares. \u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e \u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e Static analysis tools lean heavily on the call graph for interprocedural analyses, as it allows them to follow control-flow across different functions, propagate data-flow facts, as well as figure out how these calls affect the state of the program. IndirectCall pass attempts to break these premises, by hiding function call edges via an extra layer of indirection, whilst preserving the original execution semantics.\n","tags":null,"title":"Indirect Call","type":"omvll"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"3b4b0e462170362441fc6e1ad911c4db","permalink":"https://obfuscator.re/resources/irdeto/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/irdeto/","section":"resources","summary":"","tags":null,"title":"Irdeto","type":"resources"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"b7b6764d2bdb03960eff462460d18f46","permalink":"https://obfuscator.re/resources/jadx/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/jadx/","section":"resources","summary":"","tags":null,"title":"Jadx","type":"resources"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"0a3dba5f4bc25758fd1d1075accc436e","permalink":"https://obfuscator.re/resources/java-obfuscator/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/java-obfuscator/","section":"resources","summary":"","tags":null,"title":"Java Obfuscator","type":"resources"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"5346982623d66e29de0c28b01aa37afc","permalink":"https://obfuscator.re/resources/jeb/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/jeb/","section":"resources","summary":"","tags":null,"title":"JEB","type":"resources"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"deeddeab641ec26def4008c61641a7e5","permalink":"https://obfuscator.re/omvll/other-topics/jit/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/omvll/other-topics/jit/","section":"omvll","summary":"","tags":null,"title":"LLVM JIT","type":"omvll"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"13ca380ec80019923ee672a7859ff932","permalink":"https://obfuscator.re/resources/obfuscapk/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/obfuscapk/","section":"resources","summary":"","tags":null,"title":"Obfuscapk","type":"resources"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"6b9d26f78c8842b6ba35701b225e192b","permalink":"https://obfuscator.re/omvll/other-topics/annotations/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/omvll/other-topics/annotations/","section":"omvll","summary":"","tags":null,"title":"Obfuscation Annotations","type":"omvll"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"e62cd02b5f4f7757cfac98465c97b262","permalink":"https://obfuscator.re/resources/obfuscator-llvm/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/obfuscator-llvm/","section":"resources","summary":"","tags":null,"title":"obfuscator-llvm","type":"resources"},{"authors":null,"categories":null,"content":" Objective-C Cleaner Work in Progress This pass is not ready yet but it will be enable to obfuscate Objective-C symbols such as class names and methods. ","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"ea9f5926c6bed7f788d9be368c6b48a5","permalink":"https://obfuscator.re/omvll/passes/objcleaner/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/omvll/passes/objcleaner/","section":"omvll","summary":" Objective-C Cleaner Work in Progress This pass is not ready yet but it will be enable to obfuscate Objective-C symbols such as class names and methods. ","tags":null,"title":"Objective-C Cleaner","type":"omvll"},{"authors":null,"categories":null,"content":" Opaque Constants The purpose of this pass is to protect constants using opaque values. \u003c?xml version=\"1.0\" ?\u003e \u003c?xml version=\"1.0\" ?\u003e While statically reverse engineering binaries, plain strings, symbols, and integer constants are crucial information that can be used to quickly infer the purpose of a function.\nThis pass protects integer constants by using opaque operations.\nWhen to use it? You should use it if your function is using integer constants that characterizes its logic. This is particularly true for cryptographic and encoding functions.\nYou could also trigger this pass to add another layer of protection on a sensitive function, even though the constants of the function are not sensitive.\nHow to use it? As for the other passes, this protection can be enabled by defining the function obfuscate_constants in the class that inherits from omvll.ObfuscationConfig:\nclass Config(omvll.ObfuscationConfig): def obfuscate_constants(self, mod: omvll.Module, func: omvll.Function): # Logic goes here The returned values of this method depend on which constants should be protected. Let\u0026rsquo;s take a real example with the SHA-256 initialization routine. Usually, this routine initializes a buffer with the SHA-256 constants. For instance, in mbedtls it is implemented as follows:\nint mbedtls_sha256_starts(mbedtls_sha256_context *ctx, int is224) { if (!is224) { /* SHA-256 */ ctx-\u0026gt;state[0] = 0x6A09E667; ctx-\u0026gt;state[1] = 0xBB67AE85; ctx-\u0026gt;state[2] = 0x3C6EF372; ctx-\u0026gt;state[3] = 0xA54FF53A; ctx-\u0026gt;state[4] = 0x510E527F; ctx-\u0026gt;state[5] = 0x9B05688C; ctx-\u0026gt;state[6] = 0x1F83D9AB; ctx-\u0026gt;state[7] = 0x5BE0CD19; } } For the purpose of this documentation, let\u0026rsquo;s slightly modify this routine:\nuint32_t init_context_all(context_t\u0026amp; ctx, uint32_t secret) { ctx.state[0] = 0x6A09E667; ctx.state[1] = 0xBB67AE85; ctx.state[2] = 0x3C6EF372; ctx.state[3] = 0xA54FF53A; ctx.state[4] = 0x510E527F; ctx.state[5] = 0x9B05688C; ctx.state[6] = 0x1F83D9AB; ctx.state[7] = 0x5BE0CD19; ctx.result = ctx.state[0] ^ ctx.state[1] ^ ctx.state[2] ^ ctx.state[3] ^ 0x03 ^ ctx.state[4] ^ ctx.state[5] ^ ctx.state[6] ^ ctx.state[7] ^ secret; return ctx.result; } Once compiled, this function looks like this in a decompiler:\nFrom this output, we can clearly identify the initialization constants.\nFrom a reverse engineering perspective, this initialization routine is usually enough to completely reverse the other functions associated with the crypto/hash algorithm.\nIndeed, most of the cryptographic libraries follow this pattern for a given algorithm:\ninit() update() finalize() Therefore, using cross-references, the other functions (update, finalize, \u0026hellip;) may be easily identified.\nTo protect all the constants, we can return True from the configuration callback:\nclass Config(omvll.ObfuscationConfig): def obfuscate_constants(_, __, func: omvll.Function): if \u0026#34;init_context_all\u0026#34; in func.demangled_name: return True return False And False for none of them. As a result, here are the differences before and after enabling the pass:\nWe might also want to only protect a subset of constants. In that case, we can return a lower limit for which only the constants above this limit will be obfuscated:\nclass Config(omvll.ObfuscationConfig): def obfuscate_constants(_, __, func: omvll.Function): if \u0026#34;init_context_all\u0026#34; in func.demangled_name: # Obfuscate the constants that are greater (strict) than 3 return omvll.OpaqueConstantsLowerLimit(3) return False With such a configuration, the constants below or equal to 3 are not obfuscated:\nFinally, the function can also return a list of constants that must be obfuscated:\nclass Config(omvll.ObfuscationConfig): def obfuscate_constants(_, __, func: omvll.Function): if \u0026#34;init_context_all\u0026#34; in func.demangled_name: # Only obfuscate the constants that are in this list return [0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19] return False Implementation This pass works by iterating over all the instruction\u0026rsquo;s operands and by checking if it is a constant:\nfor (Function\u0026amp; F : M) { for (BasicBlock\u0026amp; BB : F) { for (Instruction\u0026amp; I : BB) { for (Use\u0026amp; Op : I.operands()) { if (auto* CI = dyn_cast\u0026lt;ConstantInt\u0026gt;(Op)) { Process(I, Op, *CI, opt); } } } } } Upon a constant, the pass generates the same constant under an opaque form:\nOpaque Zero The pass generates an opaque zero value as follows:\n0 == MBA(X ^ Y) - (X ^ Y) 0 == (X | Y) - (X \u0026amp; Y) - (X ^ Y) Opaque One Opaque 1 leverages the fact that the stack is aligned, which means that the lower bits are set to 0:\nLSB = LSB(stack address) Odd = random_odd() 1 == (LSB + Odd) % 2 Opaque Value To obfuscate a random constant that is not 0 or 1, the pass randomly splits the constant in two parts:\nuint64_t Val = CST; // \u0026lt;--- To protect uint8_t Split = random(1, min(255, Val)); uint64_t LHS = Val - Split; // Part 1 uint64_t RHS = Split; // Part 2 Then, the pass generates IR instructions to reconstruct the original value. This reconstruction is essentially an addition between LHS and RHS, and it uses two intermediate variables to prevent constant propagation optimizations:\n__omvll_opaque_gv __omvll_opaque_stack_allocated The pass also adds an opaque zero using the lower bits of the stack address. In the end, the transformation of the constant looks like this:\nstatic uint64_t __omvll_opaque_gv; void function() { uint64_t __omvll_opaque_stack_allocated; __omvll_opaque_gv = LHS; __omvll_opaque_stack_allocated = RHS; __omvll_opaque_gv += OpaqueZero(0); __omvll_opaque_stack_allocated += OpaqueZero(0); // The original value: uint64_t Val = __omvll_opaque_gv + __omvll_opaque_stack_allocated; } Limitations In its current form, the opaque values are generated with the same transformation. Hence, if an attacker manages to automate the deobfuscation, the attack scales easily.\nIf the reverse engineering tools (IDA, BinaryNinja, \u0026hellip;) introduce assumption about lower bits of the stack, it could also threaten the pass.\n","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1725428622,"objectID":"0034d1b4abbf1ef4eb85cb22029555b3","permalink":"https://obfuscator.re/omvll/passes/opaque-constants/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/omvll/passes/opaque-constants/","section":"omvll","summary":" Opaque Constants The purpose of this pass is to protect constants using opaque values. \u003c?xml version=\"1.0\" ?\u003e \u003c?xml version=\"1.0\" ?\u003e While statically reverse engineering binaries, plain strings, symbols, and integer constants are crucial information that can be used to quickly infer the purpose of a function.\n","tags":null,"title":"Opaque Constants","type":"omvll"},{"authors":null,"categories":null,"content":" Opaque Fields Access The purpose of the pass is to obfuscate the access of structure\u0026rsquo;s fields. This pass is still a work in progress. \u003c?xml version=\"1.0\" ?\u003e \u003c?xml version=\"1.0\" ?\u003e As mentioned in the talk Automation Techniques in C++ Reverse Engineering by R. Rolles, reverse engineers spend a non-negligible amount of time to identify structures and their attributes:\nIn summary, I noticed that when I reverse engineer C++ programs, I spend almost all of my time recreating structures, applying structure offsets in the disassembly listing, and applying structure types to Hex-Rays variables and function arguments Rolf Rolles in Automation Techniques in C\u0026#43;\u0026#43; Reverse Engineering And I completely agree!\nTo better understand how structures are involved in reverse engineering, let\u0026rsquo;s consider the following code which involves a JNI function:\nclass SecretString { public: SecretString(const char* input) : value_(input) {} bool check() { checked_ = (value_ == \u0026#34;OMVLL\u0026#34;); return checked_; } private: bool checked_ = false; std::string value_; }; bool check_jni_password(JNIEnv* env, jstring passwd) { const char* pass = env-\u0026gt;GetStringUTFChars(passwd, nullptr); SecretString secret(pass); return secret.check(); } When this code is compiled, env-\u0026gt;GetStringUTFChars() is called through:\nAn access to the GetStringUTFChars pointer in the JNIEnv structure. A call on the dereferenced pointer. In assembly it looks like this:\nldr x8, [x8, #1352] ; 1352 is the offset of GetStringUTFChars blr x8 ; in the JNIEnv structure This pattern is also very similar to C++ vtable accesses. When decompiling the check_jni_password function, we can effectively observe this offset, and most of the disassemblers can also resolve the structure\u0026rsquo;s attribute, once the user has resolved and provided its type:\nOutput of the decompilation when the user has reversed the types Similarly, once we have identified and reversed the layout of the SecretString* this pointer, the SecretString::check function is a bit more meaningful:\nThe inlined std::string structure is still a bit confusing since the beginning of the previous code is related to the optimization performed by the STL for small strings. On the other hand, when using this pass on the structures JNIEnv and SecretString, the output of the decompilation is confusing even if, we manually define the type of the registers associated with JNIEnv and SecretString.\nThe following figures show the differences in BinaryNinja and the output of IDA is very close:\ncheck_jni_password() before and after the obfuscation Section of SecretString::check() before and after the obfuscation When to use it? You should trigger this pass on structures that aim at containing sensitive information. It might be also worth enabling this pass on the JNIEnv structure for JNI functions involves in sensitive computations.\nHow to use it? You can trigger this pass by defining the method obfuscate_struct_access in the configuration class file:\ndef obfuscate_struct_access(self, _: omvll.Module, __: omvll.Function, struct: omvll.Struct): if struct.name.endswith(\u0026#34;JNINativeInterface\u0026#34;): return True if struct.name == \u0026#34;class.SecretString\u0026#34;: return True return False In the current version, O-MVLL expects a boolean value but futures versions should also be able to accept an option on the access type (read or write). For instance:\nif struct.name == \u0026#34;class.SecretString\u0026#34;: return omvll.StructAccessOpt(read=True, write=False) Implementation This pass works with a first stage which consists in identifying the LLVM instructions: llvm::LoadInst and llvm::StoreInst.\nThen, there is a processing of the operands for these instructions, to check if they are used to access the content of a structure or an element of a global variable. In such a case, it resolves the name of the structure or the name of the global variable and calls the user-defined callback to determine whether the access should be obfuscated.\nUpon positive feedback from the user\u0026rsquo;s callback, O-MVLL transforms the access from this:\nldr x0, [x1, #offset]; Into that:\n$var := #offset + 0; ldr x0, [x1, $var]; Without any additional layer of protection, $var := #offset + 0; can be folded by the compiler which would result in the original instruction. To prevent this simplification, the instruction #offset + 0 is annotated1 to automatically apply Opaque Constants and Arithmetic Obfuscation on this instructions:\nIRBuilder\u0026lt;NoFolder\u0026gt; IRB(\u0026amp;Load); Value* opaqueOffset = IRB.CreateAdd(ConstantInt::get(IRB.getInt32Ty(), 0), ConstantInt::get(IRB.getInt32Ty(), ComputedOffset)); if (auto* OpAdd = dyn_cast\u0026lt;Instruction\u0026gt;(opaqueOffset)) { addMetadata(*OpAdd, {MetaObf(OPAQUE_CST), MetaObf(OPAQUE_OP, 2llu)}); } Limitations This pass would not resist against the Dynamic Structure Reconstruction technique presented by R. Rolles in the presentation mentioned in the introduction.\nNevertheless, it would require to use an AArch64 DBI which does not exist yet2.\nReferences Publications Ghidra 101: Creating Structures in Ghidra by Craig Young\nAutomation Techniques in C\u0026#43;\u0026#43; Reverse Engineering by Rolf Rolles\nAutomated Reverse Engineering of Relationships Between Data Structures in C\u0026#43;\u0026#43; Binaries by Nick Collisson for NCC Group\ndynStruct: An automatic reverse engineering tool for structure recovery and memory use analysis by Daniel Mercier\nTools dynStruct by Daniel Mercier\nSee the section Annotations for the details.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nI personally worked on this support in Quarkslab\u0026rsquo;s QBDI but since I left the company this support is owned by Quarkslab. It might be published by Quarkslab though.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1725428622,"objectID":"ac2478e4ddb68e71b9c3839c59cac80a","permalink":"https://obfuscator.re/omvll/passes/opaque-fields-access/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/omvll/passes/opaque-fields-access/","section":"omvll","summary":" Opaque Fields Access The purpose of the pass is to obfuscate the access of structure\u0026rsquo;s fields. This pass is still a work in progress. \u003c?xml version=\"1.0\" ?\u003e \u003c?xml version=\"1.0\" ?\u003e As mentioned in the talk Automation Techniques in C++ Reverse Engineering by R. Rolles, reverse engineers spend a non-negligible amount of time to identify structures and their attributes:\n","tags":null,"title":"Opaque Fields Access","type":"omvll"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"d90d0fc9718b6a7a952251b234744003","permalink":"https://obfuscator.re/resources/pluto-obfuscator/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/pluto-obfuscator/","section":"resources","summary":"","tags":null,"title":"Pluto-Obfuscator","type":"resources"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"4403fdea6a027a40efe76ab5a24ed698","permalink":"https://obfuscator.re/resources/pretzel_logic/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/pretzel_logic/","section":"resources","summary":"","tags":null,"title":"pRETzel_logic","type":"resources"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"38e223c81fdf708b0cb1c6ed8655e384","permalink":"https://obfuscator.re/resources/promon/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/promon/","section":"resources","summary":"","tags":null,"title":"Promon","type":"resources"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"e0fed0704aecd7da6a1eff857e13e00e","permalink":"https://obfuscator.re/resources/qshield/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/qshield/","section":"resources","summary":"","tags":null,"title":"QShield","type":"resources"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"8b0a2d7375b0932c9f73d7f965b4263f","permalink":"https://obfuscator.re/resources/radare2/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/radare2/","section":"resources","summary":"","tags":null,"title":"Radare2","type":"resources"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"8a4240db1fbfed1b14b7cc71ff312351","permalink":"https://obfuscator.re/resources/ssagepass/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/ssagepass/","section":"resources","summary":"","tags":null,"title":"SsagePass","type":"resources"},{"authors":null,"categories":null,"content":" Strings Encoding The purpose of the pass is to protect strings from a static analysis. Strings, along with constants and symbols, are the kind of information that are quickly accessible and very efficient in reverse engineering to guess or infer the purpose of a function.\nIn addition, some macros like __FILE__ might also leak information about the original filename which is also a valuable information. If this macro is used in a header coming from a third-party SDK you might not be aware of this leak.\nO-MVLL provides a modular configuration API to protect these information with different levels of protection.\nHow to use it? First, let\u0026rsquo;s start with removing unwanted strings. If we consider the following function:\n#define LOG_ERROR(MSG) fprintf(stderr,\u0026#34;Error: %s (%s:%d)\\n\u0026#34;, MSG, __FILE__, __LINE__) bool check_code(int code) { if (code != 47839) { LOG_ERROR(\u0026#34;Wrong input\u0026#34;); return false; } return true; } We can observe a leak of the original filename in the compiled binary:\n$ strings ./strings.bin ... libc.so LIBC libdl.so libm.so /home/romain/dev/o-mvll/tests/strings.cpp ... To remove this unwanted string, we first need – as for the other obfuscation passes – to define the associated function, obfuscate_string, in the configuration class:\n# LLVM module where the string is referenced def obfuscate_string(self, mod: omvll.Module, # Function that uses the string func: omvll.Function, # The string itself as a Python bytes object string: bytes): pass If the function obfuscate_string returns a string, then the original string is replaced with the string returned. In our example, the original filename could be removed with this code:\ndef obfuscate_string(self, _, __, string: bytes): if string.startswith(b\u0026#34;/home\u0026#34;) and string.endswith(b\u0026#34;.cpp\u0026#34;): return \u0026#34;REDACTED\u0026#34; The input parameter string is a bytes object. Thus if we compare with a Python str object, it will be always false (like: string.endswith(\u0026quot;.cpp\u0026quot;)) The redacted string can be confirmed in the decompiled output:\nLet\u0026rsquo;s now consider the case where, instead of removing a string, we want to protect it:\nbool check_code(int code) { const char BANNER[] = R\u0026#34;delim( Hello gentle reverser, You are asked to enter the correct input that resolves this function. Thank you! )delim\u0026#34;; printf(\u0026#34;%s\\n\u0026#34;, BANNER); if (code != 47839) { LOG_ERROR(\u0026#34;Wrong input\u0026#34;); return false; } return true; } In this updated version of check_code(), the function prints a message that requires to be protected. Since this string is large and not really sensitive, it is recommended to protect this string with the O-MVLL option: StringEncOptGlobal().\nAll the options accepted by this function are synthesized at the end of this section. We can trigger this kind of protection as follows:\ndef obfuscate_string(self, _, __, string: bytes): if string.startswith(b\u0026#34;/home\u0026#34;) and string.endswith(b\u0026#34;.cpp\u0026#34;): return \u0026#34;REDACTED\u0026#34; if b\u0026#34;Hello gentle reverse\u0026#34; in string: return omvll.StringEncOptGlobal() With this option, the original string is encoded and stored in the .data section of the binary. As soon as the binary is loaded, the string is decoded in place by a constructor function.\nconst char BANNER[] = \u0026#34;\\xD4\\x24...\u0026#34; __attribute__((constructor)) void decode() { // Decode the BANNER encoded buffer } bool check_code(int code) { ... printf(\u0026#34;%s\\n\u0026#34;, BANNER); ... } Performances \u0026amp; Overhead StringEncOptGlobal is the option that produces the least overhead in terms of code size and execution time.\nOn the other hand, the clear string is present in memory as soon as the binary is loaded which makes it easily accessible through a memory dump. This limitation is discussed in the section Limitations.\nNow let\u0026rsquo;s evalutate the options that provide a better level of protection. The .data section is easy to dump during the execution of the binary so it is not the best spot to decode a sensitive string.\n-\u0026gt; What about the strings local to a function? It turns out that, we can decode mutable strings (i.e., coming from data section) and constant strings directly within the function itself.\nLet\u0026rsquo;s consider this new version of the check_code function:\nbool check_code(int code, const char* passwd) { const char BANNER[] = R\u0026#34;delim( Hello gentle reverser, You are asked to enter the correct input that resolves this function. Thank you! )delim\u0026#34;; printf(\u0026#34;%s\\n\u0026#34;, BANNER); char PASS[] = \u0026#34;OMVLL\u0026#34;; if (code != 47839 || strncmp(passwd, PASS, 6)) { LOG_ERROR(\u0026#34;Wrong input\u0026#34;); return false; } return true; } If we want to protect the \u0026quot;OMVLL\u0026quot; string through a local decoding, we can leverage the StringEncOptLocal option:\ndef obfuscate_string(self, _, __, string: bytes): if string.startswith(b\u0026#34;/home\u0026#34;) and string.endswith(b\u0026#34;.cpp\u0026#34;): return \u0026#34;REDACTED\u0026#34; if b\u0026#34;Hello gentle reverse\u0026#34; in string: return omvll.StringEncOptGlobal() if string == b\u0026#34;OMVLL\u0026#34;: return omvll.StringEncOptLocal() With such an option, b\u0026quot;OMVLL\u0026quot; will be decoded as follows:\nconst char ENC_OMVLL = \u0026#34;\\x1a\\x9A\\x21\\x79\\x37\\x02\u0026#34;; bool check_code(int code) { static char OMVLL_DECODED[6]; OMVLL_DECODED[1] = ENC_OMVLL[1] ^ 0xd7; OMVLL_DECODED[5] = ENC_OMVLL[5] ^ 0x02; OMVLL_DECODED[2] = ENC_OMVLL[2] ^ 0x77; OMVLL_DECODED[0] = ENC_OMVLL[0] ^ 0x55; OMVLL_DECODED[4] = ENC_OMVLL[4] ^ 0x7b; OMVLL_DECODED[3] = ENC_OMVLL[3] ^ 0x35; if (code != 47839 || strncmp(passwd, OMVLL_DECODED, 6)) { ... } } As we can observe, a global buffer local to the function with the same size as the original string is materialized in the IR. It is also worth highlighting some aspects of this protection:\nThe indexes of the buffer where the 'char' is decoded are shuffled. The keystream used for decoding the string is unique. The memory accesses of both, OMVLL_DECODED and ENC_OMVLL are protected with Opaque Fields Access. The xor operation is protected with Arithmetic Obfuscation. Key\u0026rsquo;s values are protected with Opaque Constants. So in the end, the compiled and protected binary looks like this:\nWhile the option drastically increases the code size for which the overhead is proportional to the original length of the string, the decoding is performed once and lazily (upon visiting the string).\nAs a general tip, to avoid huge performance penalties, it is recommended to enable this option on sensitive-only strings. From an implementation perspective, the loop is dynamically jitted from C code which is something pretty new compared to the other LLVM-based obfuscator. Feel free to jump on Implementation for the details.\nHere is the table that summarizes the different options:\nValue Returned Protection Overhead False, None None None True Depends Depends StringEncOptGlobal Medium Low StringEncOptLocal High High When to use it? This pass should always be enabled on your code, at least for checking and removing debug information or leaks from macros.\nFor the other aspects of your code, you should consider enabling this protection for sensitive strings like API Token (if any), log messages, secrets, etc.\nKeep in mind that an insignificant string might be very significant for a reverse engineer even though it is not directly related to a sensitive asset.\nImplementation This pass works by iterating over all the instructions of a function and by filtering on those that access a llvm::GlobalVariable.\nIf the GlobalVariable is associated with a C-String, the pass calls the user\u0026rsquo;s callback to determine which protection should be used. Depending on the value returned by the user\u0026rsquo;s callback, the pass performs one of the following operations:\nStringEncOptGlobal With this option, the pass replaces the original clear string with its encoded version:\nstd::vector\u0026lt;uint8_t\u0026gt; encoded(str.size()); ... Constant* StrEnc = ConstantDataArray::get(BB.getContext(), encoded); G.setInitializer(StrEnc); Then, it injects the decoding function as a global constructor within the current llvm::Module:\nstd::string Id = G.getGlobalIdentifier(); FunctionCallee FCallee = module-\u0026gt;getOrInsertFunction(Id, FVoidTy); auto* FCtor = cast\u0026lt;Function\u0026gt;(FCallee.getCallee()); FCtor-\u0026gt;setLinkage(llvm::GlobalValue::PrivateLinkage); ... appendToGlobalCtors(module, FCtor, 0); This step is very similar to what we can observe in the O-LLVM\u0026rsquo;s forks 1. Nevertheless, there is one difference that matters in terms of reverse engineering:\n... FCtor-\u0026gt;setLinkage(llvm::GlobalValue::PrivateLinkage); ... Actually, when using getOrInsertFunction, LLVM creates the function (if not already present) with a default EXTERNAL visibility:\n// In llvm/lib/IR/Module.cpp, as of LLVM 16 FunctionCallee Module::getOrInsertFunction(StringRef Name, FunctionType *Ty, AttributeList AttributeList) { if (!F) { // Nope, add it Function *New = Function::Create(Ty, GlobalVariable::ExternalLinkage, DL.getProgramAddressSpace(), Name); ... } This ExternalLinkage means that the constructor function will be considered as exported in the final binary. Since the function is exported, its associated symbol can\u0026rsquo;t be stripped.\nIn our implementation, the name of the constructor comes from getGlobalIdentifier() instead of .datadiv_decode_...:\nReturn the modified name for this global value suitable to be used as the key for a global lookup (e.g. profile or ThinLTO). GlobalValue::getGlobalIdentifier() in llvm documentation but we do take care of hashing the global identifier. Conversely, in most of the O-LLVM\u0026rsquo;s forks the name of the constructor is set as follows:\nuint64_t StringObfDecodeRandomName = cryptoutils-\u0026gt;get_uint64_t(); ... std::string Id = \u0026#34;.datadiv_decode\u0026#34; + StringObfDecodeRandomName; Since this name can\u0026rsquo;t be stripped with an ExternalLinkage, the symbol is accessible from reverse engineers who can immediately identify the purpose of the function. In addition, this symbol can be used as a marker to fingerprint the obfuscator2:\nrule ollvm_v5_0_strenc : obfuscator { meta: description = \u0026#34;Obfuscator-LLVM version 5.0 (string encryption)\u0026#34; url = \u0026#34;https://github.com/obfuscator-llvm/obfuscator/wiki\u0026#34; sample = \u0026#34;a794a080a92987ce5ed9cf5cd872ef87f9bfb9acd4c07653b615f4beaff3ace2\u0026#34; author = \u0026#34;Eduardo Novella\u0026#34; strings: // \u0026#34;Obfuscator-LLVM clang version 5.0.2 (based on Obfuscator-LLVM 5.0.2)\u0026#34; $clang_version = \u0026#34;Obfuscator-LLVM clang version 5.0.\u0026#34; $based_on = \u0026#34;(based on Obfuscator-LLVM 5.0.\u0026#34; $strenc = /\\.datadiv_decode[\\d]{18,20}/ // Enumerating elf.symtab_entries fails! condition: is_elf and all of them } Once, we added the constructor with a proper visibility, the pending question is:\nHow to fill the constructor with instructions?\nBasically, the constructor must contain the instructions that decode the encoded string. Thanks to the IR LLVM API, we can manually create a loop, add the decoding operations, etc. This approach works and is efficient but there are a few drawbacks:\nThe decoding logic is not really modular: we have to manually write the routine with the llvm::IRBuilder. It is error prone if the decoding logic is complex. On the other hand, LLVM also contains a JIT engine and a C/C++ frontend \u0026ndash; aka clang \u0026ndash; we could use to dynamically JIT C/C++ source code:\nllvm::Module* MJIT = TargetJIT-\u0026gt;generate(R\u0026#34;delim( void decode(char* out, char* in, unsigned long long key, int size) { unsigned char* raw_key = (unsigned char*)(\u0026amp;key); for (int i = 0; i \u0026lt; size; ++i) { out[i] = in[i] ^ raw_key[i % sizeof(key)] ^ i; } } )delim\u0026#34;); Function* FDecode = MJIT-\u0026gt;getFunction(\u0026#34;decode\u0026#34;); CloneFunctionInto(FCtor, FDecode, ...); Since a decoding routine is paired with an encoding routine, we can also JIT the encoding routine (for the architecture on which O-MVLL is running), to \u0026ldquo;blindly\u0026rdquo; encode the string:\nauto JIT = HostJIT-\u0026gt;compile( R\u0026#34;delim( void encode(char* out, char* in, unsigned long long key, int size) { unsigned char* raw_key = (unsigned char*)(\u0026amp;key); for (int i = 0; i \u0026lt; size; ++i) { out[i] = in[i] ^ raw_key[i % sizeof(key)] ^ i; } return; } )delim\u0026#34;); if (auto E = HostJit-\u0026gt;lookup(\u0026#34;encode\u0026#34;)) { auto enc = reinterpret_cast\u0026lt;enc_routine_t\u0026gt;(E-\u0026gt;getAddress()); enc(encoded.data(), str.data(), key, str.size()); } ... Constant* StrEnc = ConstantDataArray::get(BB.getContext(), encoded); G.setInitializer(StrEnc); In its current implementation, the encode/decode functions are statically written in the code of the pass, but we could also imagine supporting routines provided by the user through Python APIs:\ndef obfuscate_string(self, _, __, string: bytes): return StringEncOpt(\u0026#34;\u0026#34;\u0026#34; void encode(...) { /* My secret implementation */ } void decode(...) { /* My secret implementation */ } \u0026#34;\u0026#34;\u0026#34;) You can find more details about the JIT engine used in O-MVLL in the section LLVM JIT.\nStringEncOptLocal If the option StringEncOptLocal is provided, for which the string is eligible to be obfuscated, the pass starts by allocating a global buffer:\nGlobalVariable *ClearBuffer = new GlobalVariable(*M, BufferTy, false, GlobalValue::InternalLinkage, Constant::getNullValue(BufferTy)); Then it injects the decoding routine using the same JIT-technique as StringEncOptGlobal.\nFinally, it replaces the original instruction\u0026rsquo;s operand \u0026ndash; which referenced the clear string \u0026ndash; with the new buffer:\nI.setOperand(Op.getOperandNo(), ClearBuffer); An additional global variable is introduced to track whether the string has already been decoded. If it has not, the decoding process is then carried out.\nLimitations As already mentioned at the beginning, a string protected with the option StringEncOptGlobal can be easily recovered by dumping the data section once the binary is loaded. On the other hand, whereas, strings protected with the StringEncOptLocal option are not subject to the dump attack but they could be recovered with a memory trace generated by a DBI (c.f. Android Native Library Analysis with QBDI: Encoding Routine).\nAttackers could also use code lifting or emulation to automatically decode the strings. The scalability and the feasibility of the code lifting and the emulation highly depend on the design of the function.\nThis pass does not currently support Objective-C strings. References Publications Recovering Obfuscated Strings in PokemonGO by Romain Thomas\nAutomation in Reverse Engineering: String Decryption by Tim Blazytko\nSee for instance: kk-laoguo/ollvm-13\u0026#160;\u0026#x21a9;\u0026#xfe0e;\napkid/rules/elf/obfuscators.yara\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1758443561,"objectID":"f4c157af47e9acd56ae2fd4676b25471","permalink":"https://obfuscator.re/omvll/passes/strings-encoding/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/omvll/passes/strings-encoding/","section":"omvll","summary":" Strings Encoding The purpose of the pass is to protect strings from a static analysis. Strings, along with constants and symbols, are the kind of information that are quickly accessible and very efficient in reverse engineering to guess or infer the purpose of a function.\nIn addition, some macros like __FILE__ might also leak information about the original filename which is also a valuable information. If this macro is used in a header coming from a third-party SDK you might not be aware of this leak.\n","tags":null,"title":"Strings Encoding","type":"omvll"},{"authors":null,"categories":null,"content":" Strings Encryption The purpose of this pass is to protect sensitive strings present in Java/Kotlin classes \u003c?xml version=\"1.0\" ?\u003e \u003c?xml version=\"1.0\" ?\u003e The strings used within Java or Kotlin classes are a good indicator for reverse engineers. This protection statically encodes strings such as the clear strings are only present at runtime when the class\u0026rsquo; methods need them.\nHow to use it? This protection can be activated by using the -obfuscate-strings option in the dProtect configuration file:\n-obfuscate-strings ... -obfuscate-strings accepts two kinds of argument:\n# 1. List of strings -obfuscate-strings \u0026#34;hello*\u0026#34;, \u0026#34;world\u0026#34; # 2. Class specifications -obfuscate-strings class dprotect.tests.string.TestObfuscationSyntax { private static java.lang.String API_KEY; public static java.lang.String sayHello(); } 1. Class specifications The regular usage of this option is very close to the -keep option1:\nWe define classes, methods, and fields for which, we want to obfuscate the strings.\nTo better understand the impact of this option, let\u0026rsquo;s consider the following code:\npackage re.obfuscator.dprotect; public class MySensitiveClass { private static String API_KEY = \u0026#34;XEYnuNOGoEQtj7cFOPmXBMvQTE8FyAWC\u0026#34;; boolean isAuth; public String getApiKey() { return String.format(\u0026#34;TOKEN: %s\u0026#34;, API_KEY); } public String toString() { return String.format(\u0026#34;MySensitiveClass{isAuth: %b | Token: %s}\u0026#34;, isAuth, API_KEY); } } First, if we want to protect the API Key associated with the API_KEY attribute, we can use this definition:\n# Class specifications -obfuscate-strings class re.obfuscator.dprotect.MySensitiveClass { private static java.lang.String API_KEY; } This configuration produces this protection:\n\u003c?xml version=\"1.0\" ?\u003eCodeSmaliObfuscatedResourcesAPK signatureSummarySource codeandroid.support.v4RMySensitiveClassonPostExecute15483()voidm19cdd()voidcdd()intattachBaseContext(Context)voidattachBaseContext(String)StringattachBaseContext(byte[], int)intattachBaseContext(int, int)intdbdaObject[]badbooleanre.obfuscator.dprotectcom.sample.dprotect.apkApplicationViewFileNavigationHelpToolspackage re.obfuscator.dprotect;public class MySensitiveClass { private static String API_KEY = \u0026quot;XEYnuNOGoEQtj7cFOPmXBMvQTE8FyAWC\u0026quot;; boolean isAuth; public String getApiKey() { return String.format(\u0026quot;TOKEN: %s\u0026quot;, API_KEY); } public String toString() { return String.format(\u0026quot;MySensitiveClass{isAuth: %b | Token: %s}\u0026quot;, isAuth, API_KEY); }} \u003c?xml version=\"1.0\" ?\u003eCodeSmaliObfuscatedResourcesAPK signatureSummarySource codeandroid.support.v4RMySensitiveClassonPostExecute15483()voidm19cdd()voidcdd()intattachBaseContext(Context)voidattachBaseContext(String)StringattachBaseContext(byte[], int)intattachBaseContext(int, int)intdbdaObject[]badbooleanre.obfuscator.dprotectcom.sample.dprotect.apkApplicationViewFileNavigationHelpToolspackage re.obfuscator.dprotect;public class MySensitiveClass { private static String API_KEY = PROTECTED(\u0026quot;\\u034\\u324\\u023...\u0026quot;); boolean isAuth; public String getApiKey() { return String.format(\u0026quot;TOKEN: %s\u0026quot;, API_KEY); } public String toString() { return String.format(\u0026quot;MySensitiveClass{isAuth: %b | Token: %s}\u0026quot;, isAuth, API_KEY); }} If we also want to protect the string(s) in the getApiKey() method, we must add this definition:\n# Class specifications -obfuscate-strings class re.obfuscator.dprotect.MySensitiveClass { private static java.lang.String API_KEY; public java.lang.String getApiKey(); } This new definition provides the following changes:\n\u003c?xml version=\"1.0\" ?\u003eCodeSmaliObfuscatedResourcesAPK signatureSummarySource codeandroid.support.v4RMySensitiveClassonPostExecute15483()voidm19cdd()voidcdd()intattachBaseContext(Context)voidattachBaseContext(String)StringattachBaseContext(byte[], int)intattachBaseContext(int, int)intdbdaObject[]badbooleanre.obfuscator.dprotectcom.sample.dprotect.apkApplicationViewFileNavigationHelpToolspackage re.obfuscator.dprotect;public class MySensitiveClass { private static String API_KEY = PROTECTED(\u0026quot;\\u034\\u324\\u023...\u0026quot;); boolean isAuth; public String getApiKey() { return String.format(\u0026quot;TOKEN: %s\u0026quot;, API_KEY); } public String toString() { return String.format(\u0026quot;MySensitiveClass{isAuth: %b | Token: %s}\u0026quot;, isAuth, API_KEY); }} \u003c?xml version=\"1.0\" ?\u003eCodeSmaliObfuscatedResourcesAPK signatureSummarySource codeandroid.support.v4RMySensitiveClassonPostExecute15483()voidm19cdd()voidcdd()intattachBaseContext(Context)voidattachBaseContext(String)StringattachBaseContext(byte[], int)intattachBaseContext(int, int)intdbdaObject[]badbooleanre.obfuscator.dprotectcom.sample.dprotect.apkApplicationViewFileNavigationHelpToolspackage re.obfuscator.dprotect;public class MySensitiveClass { private static String API_KEY = PROTECTED(\u0026quot;\\u034\\u324\\u023...\u0026quot;); boolean isAuth; public String getApiKey() { return String.format(PROTECTED(\u0026quot;\\u094\\u402\\u122...\u0026quot;), API_KEY); } public String toString() { return String.format(\u0026quot;MySensitiveClass{isAuth: %b | Token: %s}\u0026quot;, isAuth, API_KEY); }} Finally, we could protect all the strings of the class by using the wildcard option:\n# Class specifications -obfuscate-strings class re.obfuscator.dprotect.MySensitiveClass { *; } And we get these transformations:\n\u003c?xml version=\"1.0\" ?\u003eCodeSmaliObfuscatedResourcesAPK signatureSummarySource codeandroid.support.v4RMySensitiveClassonPostExecute15483()voidm19cdd()voidcdd()intattachBaseContext(Context)voidattachBaseContext(String)StringattachBaseContext(byte[], int)intattachBaseContext(int, int)intdbdaObject[]badbooleanre.obfuscator.dprotectcom.sample.dprotect.apkApplicationViewFileNavigationHelpToolspackage re.obfuscator.dprotect;public class MySensitiveClass { private static String API_KEY = PROTECTED(\u0026quot;\\u034\\u324\\u023...\u0026quot;); boolean isAuth; public String getApiKey() { return String.format(PROTECTED(\u0026quot;\\u094\\u402\\u122...\u0026quot;), API_KEY); } public String toString() { return String.format(\u0026quot;MySensitiveClass{isAuth: %b | Token: %s}\u0026quot;, isAuth, API_KEY); }} \u003c?xml version=\"1.0\" ?\u003eCodeSmaliObfuscatedResourcesAPK signatureSummarySource codeandroid.support.v4RMySensitiveClassonPostExecute15483()voidm19cdd()voidcdd()intattachBaseContext(Context)voidattachBaseContext(String)StringattachBaseContext(byte[], int)intattachBaseContext(int, int)intdbdaObject[]badbooleanre.obfuscator.dprotectcom.sample.dprotect.apkApplicationViewFileNavigationHelpToolspackage re.obfuscator.dprotect;public class MySensitiveClass { private static String API_KEY = PROTECTED(\u0026quot;\\u034\\u324\\u023...\u0026quot;); boolean isAuth; public String getApiKey() { return String.format(PROTECTED(\u0026quot;\\u094\\u402\\u122...\u0026quot;), API_KEY); } public String toString() { return String.format(PROTECTED(\u0026quot;\\u094\\u402\\u122\\u123\\u324...\u0026quot;), isAuth, API_KEY); }} The details about the class specifications syntax are documented in the official Proguard documentation: ProGuard manual.\nNow, let\u0026rsquo;s see how we can use a list of strings for the -obfuscate-strings option.\n2. List of strings In addition to a class specifier, we can feed -obfuscate-strings with a list of strings delimited by a comma.\n-obfuscate-strings \u0026#34;hello*\u0026#34;, \u0026#34;world\u0026#34; With this option, all the strings that match one of the elements specified in the list will be protected.\nClass Specifications Required This option requires to be paired with class specifications. Indeed, the pass does not iterate over all the strings of all the classes to check if they match the obfuscation list provided by the user. This would strongly impact the compilation time!\nInstead, the input strings are sourced by the classes specified with the -obfuscate-strings specifier: Pitfall # DOES NOT PROTECT ANY STRING -obfuscate-strings \u0026#34;check*\u0026#34;, \u0026#34;world\u0026#34; Protected -obfuscate-strings \u0026#34;check*\u0026#34;, \u0026#34;world\u0026#34; -obfuscate-strings class dprotect.** # Protect the strings \u0026#34;check\u0026#34; \u0026#34;check password \u0026#34;, \u0026#34;world\u0026#34;, ... # present in the package \u0026#39;dprotect\u0026#39; When to use it? This pass should be enabled for all sensitive classes. We also recommend protecting all the strings of the class as any clear string \u0026ndash; even though it might not seem sensitive at first sight \u0026ndash; could provide information to reverse engineers.\nImplementation The logic of the pass is located in the package dprotect.obfuscation.strings.\nFirst, the CodeObfuscator filters the classes that have been flagged as string-obfuscated:\nprogramClassPool.accept( new AllClassVisitor( new ClassVisitor() { public void visitAnyClass(Clazz clazz) { if (ApplyStringObfuscation(clazz)) { // 1. Flag strings field markStringsField(); // 2. Encode strings runObfuscator(); } } })); The initial step markStringsField() is used to mark strings that are associated with a class\u0026rsquo;s attributes that are marked as \u0026ldquo;protected\u0026rdquo; by the user:\n# Class specifications -obfuscate-strings class re.obfuscator.dprotect.MySensitiveClass { private static java.lang.String API_KEY; public java.lang.String getApiKey(); } To identify the strings that are paired with a class attribute, we basically try to fingerprint this sequence of instructions:\nCode: 1: ldc #9 // java.lang.String \u0026lt;to protect\u0026gt; ... 4: putfield #14 // Field API_KEY:Ljava/lang/String; This identification is performed by implementing the Proguard\u0026rsquo;s InstructionVisitor and ConstantVisitor which are used for backtracking the strings involved in the putfield/putstaticfield instructions:\n// Pseudo-code for the logic of markStringsField() public void visitConstantInstruction(...) { if (opcode == Instruction.OP_LDC) { // Keep a reference of the current string visited this.stringConstant = ...; } else if (opcode == Instruction.OP_PUTFIELD) { if (IsMarked(field) \u0026amp;\u0026amp; this.stringConstant != null) { mark(this.stringConstant); } } } Once the strings associated with fields are marked, we can process the whole class for the obfuscation:\n// 1. Flag strings field markStringsField(); // 2. Obfuscate strings runObfuscator(); The overall logic behind runObfuscator is to:\nInject a decoding routine in the classes for which strings must be protected. Replace all the strings with their encoded representation. Add a call to the injected decoding routine for the encoded strings. For the first step, the idea is very similar to the O-MVLL String Encoding pass:\nWe JIT predefined encoding routines. The class dprotect.runtime.strings.StringEncoding implements a set of encoding/decoding routines that are used for the injection.\n\u003c?xml version=\"1.0\" ?\u003e The idea of this injection is that, on one hand, Proguard has all the functionalities to add, create and modify the class\u0026rsquo; methods. Therefore, given a compiled .class file, we could copy the bytecode of a specific method within the class that aims to welcome the decoding routine.\nOn the other hand, the Java bytecode associated with the injected routine can also be executed by the pass itself to get the encoded string.\nThe injection of the decoding routine is performed by the following (pseudo) code:\n// Class in which we want to inject the decoding routine ProgramClass target = ... ClassBuilder builder = new ClassBuilder(target); // Create a (empty) method into the targeted class ProgramMethod decodingRoutine = builder.addAndReturnMethod( AccessConstants.STATIC, /* Name */ \u0026#34;myDecodingRoutine\u0026#34;, /* Prototype */ \u0026#34;(Ljava/lang/String;)Ljava/lang/String;\u0026#34;); // Lift the bytecode into target.myDecodingRoutine // from StringEncoding.myDecodingRoutine MethodCopier.copy(target, decodingRoutine, StringEncoding.class, \u0026#34;decodingRoutine\u0026#34;); MethodCopier MethodCopier is not present in the original version of ProguardCORE and has been added for the purpose of this pass. Once the decoding routine injected into the targeted class, we can address the next points which consist in replacing the original strings with their encoded representation.\nFor that purpose, we can combine the following Proguard\u0026rsquo;s visitors (pseudo-code):\n// AttributeVisitor. @Override public void visitCodeAttribute(Clazz clazz, Method method, ...) { // Prepare the \u0026#34;editors\u0026#34; and trigger the instructions visitor constantPoolEditor = new ConstantPoolEditor((ProgramClass)clazz); codeAttributeEditor.reset(codeAttribute.u4codeLength); // Trigger InstructionVisitor that is implemetned by the same class codeAttribute.instructionsAccept(clazz, method, this); } // InstructionVisitor. @Override public void visitConstantInstruction(Clazz clazz, Method method, CodeAttribute codeAttribute, int offset, ConstantInstruction instruction) { // Filter on the LDC/LDC_W opcodes which load strings if (instruction.opcode == Instruction.OP_LDC || instruction.opcode == Instruction.OP_LDC_W) { // Find the decoding routine which has been injected in the step 1 Method decodingRoutine = clazz.findMethod(...); // Create a static-call instruction for the decoding routine Instruction call = new ConstantInstruction(Instruction.OP_INVOKESTATIC, decodingRoutine); // Replace the string with its encoded version String encoded = encode(originalString); instruction.constantIndex = constantPoolEditor.addStringConstant(encoded); codeEditor.replaceInstruction(offset, instruction); // Add the static call to the decoding routine codeEditor.insertAfterInstruction(offset, replacementInstruction); } } In the previous snippet, String encoded = encode(originalString) actually uses Java reflection to call the encoding routine implemented in StringEncoding (whilst the decoding routine has been injected with MethodCopier in the class).\nThe full implementation is a bit more complex but the previous description provides a good overview of the process.\nLimitations Regarding the limitations, this pass might introduce a certain overhead on the size of the final application since a new method is added for all the classes in which strings must be protected. Nevertheless, this overhead is balanced by the fact that the decoding routines are usually small and self-consistent.\nThe decoding routine could also be hooked by an attacker to access the clear string at runtime. Nevertheless, this would require to setup hooks for all the classes as the decoding routines are local and different for each class.\nLast but not least, JEB Decompiler is able to recover the original string as described in this blog post: Reversing dProtect. References Attacks Reversing dProtect - Strings Obfuscation This blog post explains how JEB Decompiler can recover strings protected with dProtect. -obfuscate-string relies on the same parser as the -keep option.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"b44838090e96a6cde8ef108d9a72742b","permalink":"https://obfuscator.re/dprotect/passes/strings/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/dprotect/passes/strings/","section":"dprotect","summary":" Strings Encryption The purpose of this pass is to protect sensitive strings present in Java/Kotlin classes \u003c?xml version=\"1.0\" ?\u003e \u003c?xml version=\"1.0\" ?\u003e The strings used within Java or Kotlin classes are a good indicator for reverse engineers. This protection statically encodes strings such as the clear strings are only present at runtime when the class\u0026rsquo; methods need them.\n","tags":null,"title":"Strings Encryption","type":"dprotect"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"8c5c1093306763e8bd6f384e2013a252","permalink":"https://obfuscator.re/resources/tigress/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/tigress/","section":"resources","summary":"","tags":null,"title":"Tigress","type":"resources"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"29eb21ba7bd73b2733b43f3b95883598","permalink":"https://obfuscator.re/resources/verimatrix/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/verimatrix/","section":"resources","summary":"","tags":null,"title":"Verimatrix","type":"resources"},{"authors":["korami"],"categories":null,"content":"Writeup for obfuscator.re Challenge 1 The application has a simple login screen:\nLogin screen Checking the source code in jadx:\n$ jadx-gui apks/challenge-pydroid.apk 2\u0026gt;\u0026amp;1 \u0026gt;/dev/null \u0026amp; We see that the code behind the check is inside a native function:\nInstalling the app and running frida:\n$ adb install apks/challenge-pydroid.apk Searching for system.load in jadx we can find where the lib is being loaded: Let\u0026rsquo;s write a script to decrypt the string and see what the name of the lib that is being loaded:\nJava.perform(function(){ let Application = Java.use(\u0026#34;re.obfuscator.challenge01.Application\u0026#34;); console.log(Application[\u0026#34;de\u0026#34;](\u0026#34;\\uf74d\\uf71c\\uf75c\\uf74a\\uf718\\uf71a\u0026#34;)); }); Injecting the script on boot:\n$ frida -l decryptString.js -f re.obfuscator.challenge01 --no-pause [Pixel 4 XL::re.obfuscator.challenge01 ]-\u0026gt; a1re03 It seems like the library name is a1re03, since it\u0026rsquo;s using the api call system.loadLibrary we should find a file with the prefix lib liba1re03.so:\n$ apktool d apks/challenge-pydroid.apk -o challenge-pydroid $ ls challenge-pydroid/lib/arm64-v8a liba1re03.so Openning the library in ghidra we can see and check the entrypoints, and we can see the .init_array is not initialised:\nI tried to search for functions in the symbol-tree with the prefix java_ but didn\u0026rsquo;t find any, so I believe the linking between Java and the native code should be done with the registerNatives function somewhere in the JNI_OnLoad function: It seems like there will be an indirect call, so instead of diving into the code, I used jnitrace to trace the function JNI-\u0026gt;registerNatives to locate in ghidra the respective code related to the native function in Java:\n/* TID 22199 */ 239 ms [+] JNIEnv-\u0026gt;RegisterNatives 239 ms |- JNIEnv* : 0xb400007cb2c51df0 239 ms |- jclass : 0x95 { re/obfuscator/challenge01/VCyPLJeiyfu } 239 ms |- JNINativeMethod* : 0x7febbf85c0 239 ms |: 0x7b66211428 - PGPyIMEWUxFr(Ljava/lang/String;)Z 239 ms |- jint : 1 239 ms |= jint : 0 239 ms --------------------------------Backtrace-------------------------------- 239 ms |-\u0026gt; 0x7b661ab0ac: liba1re03.so!0x1720ac (liba1re03.so:0x7b66039000) 239 ms |-\u0026gt; 0x7b661ab0ac: liba1re03.so!0x1720ac (liba1re03.so:0x7b66039000) We can see where register natives is being called at 0x1720ac. To see the code in ghidra, we can just go to the address 0x1720ac + 0x100000 (We need to add 100k because ghidra by default will load the lib at that address).\nThe logic we truly want to check on is the function PGPyIMEWUxFr, jnitrace will give us the base address of the lib and the address of the start of the function, so basically, to calculate its real offset in ghidra we could just do 0x7b66211428-0x7b66039000+0x100000 = 0x2d8428.\nA lot of functions are not decompiled in ghidra and didn\u0026rsquo;t perform the backtrack references through the code, mostly because of some of the techniques used described here.\nDue to this problem, I decided to dump the library from memory and fix the elf and in some way solve some of the problems generated by this, also we know omvll is based of o-llvm and some versions uses globals for the strings, based on experience the fastest way to circuvent string encryption for global variables is to use a dump, this is also described in the documentation.\nWe could write our own frida script to dump from memory, but to save time. There are already some scripts that perform the dump and fix the elf for us. One example of such is this frida_dump\nPerhaps we will encounter a problem while trying to dump (the code will dump the specified lib in the frontmost application):\n$ python dump_so.py liba1re03.so ... frida.core.RPCException: Error: access violation accessing 0x7b6cdcf000 at \u0026lt;anonymous\u0026gt; (frida/runtime/core.js:138) at dumpmodule (/script1.js:12) at apply (native) at \u0026lt;anonymous\u0026gt; (frida/runtime/message-dispatcher.js:13) at c (frida/runtime/message-dispatcher.js:23) Seems like there is a section of the lib that doesn\u0026rsquo;t have read permissions, to solve this we must adapt the dump_so.js to change the memory region, also this line of code doesn\u0026rsquo;t seem to fully work:\n... Memory.protect(ptr(libso.base), libso.size, \u0026#39;rwx\u0026#39;); ... If we investigate the address mapping:\n$ adb shell \u0026#34;ps | grep -i \u0026#39;re.obfuscator.challenge01\u0026#39;\u0026#34; u0_a282 22584 9119 15130452 373024 SyS_epoll_wait 0 S re.obfuscator.challenge01 $ adb shell \u0026#34;cat /proc/22584/maps | grep \u0026#39;liba1re03.so\u0026#39;\u0026#34; 7b6b4d1000-7b6b910000 rwxp 00000000 fd:04 143995 /data/app/~~wo0sC2WdNe1hirJvBsr8gQ==/re.obfuscator.challenge01-jU98uNg4F1DuhOs3utp2zA==/lib/arm64/liba1re03.so 7b6b910000-7b6b919000 r--p 0043e000 fd:04 143995 /data/app/~~wo0sC2WdNe1hirJvBsr8gQ==/re.obfuscator.challenge01-jU98uNg4F1DuhOs3utp2zA==/lib/arm64/liba1re03.so 7b6b919000-7b6cdc7000 rw-p 00446000 fd:04 143995 /data/app/~~wo0sC2WdNe1hirJvBsr8gQ==/re.obfuscator.challenge01-jU98uNg4F1DuhOs3utp2zA==/lib/arm64/liba1re03.so 7b6edc6000-7b6eeef000 rwxp 018f5000 fd:04 143995 /data/app/~~wo0sC2WdNe1hirJvBsr8gQ==/re.obfuscator.challenge01-jU98uNg4F1DuhOs3utp2zA==/lib/arm64/liba1re03.so Maybe because changing the entire permissions of lib may cause some problems to solve this, we just adapt that special region of memory and do this:\nMemory.protect(ptr(0x7b6cdcf000), libso.size-(0x7b6cdcf000-libso.base), \u0026#39;rwx\u0026#39;); Since we are attaching to the process, we don\u0026rsquo;t need to update the address 0x7b6cdcf000 but if you are trying to do the same, you will need to update your address depending on the error.\n$ python dump_so.py liba1re03.so 1 ⚙ {\u0026#39;name\u0026#39;: \u0026#39;liba1re03.so\u0026#39;, \u0026#39;base\u0026#39;: \u0026#39;0x7b6b4d1000\u0026#39;, \u0026#39;size\u0026#39;: 60940288, \u0026#39;path\u0026#39;: \u0026#39;/data/app/~~wo0sC2WdNe1hirJvBsr8gQ==/re.obfuscator.challenge01-jU98uNg4F1DuhOs3utp2zA==/lib/arm64/liba1re03.so\u0026#39;} android/SoFixer64: 1 file pushed, 0 skipped. 22.3 MB/s (186656 bytes in 0.008s) liba1re03.so.dump.so: 1 file pushed, 0 skipped. 37.3 MB/s (60940288 bytes in 1.558s) adb shell /data/local/tmp/SoFixer -m 0x7b6b4d1000 -s /data/local/tmp/liba1re03.so.dump.so -o /data/local/tmp/liba1re03.so.dump.so.fix.so [main_loop:87]start to rebuild elf file [Load:69]dynamic segment have been found in loadable segment, argument baseso will be ignored. [RebuildPhdr:25]=============LoadDynamicSectionFromBaseSource==========RebuildPhdr========================= [RebuildPhdr:37]=====================RebuildPhdr End====================== [ReadSoInfo:549]=======================ReadSoInfo========================= [ReadSoInfo:696]soname [ReadSoInfo:699]Unused DT entry: type 0x6ffffffb arg 0x00000001 [ReadSoInfo:699]Unused DT entry: type 0x00000009 arg 0x00000018 [ReadSoInfo:699]Unused DT entry: type 0x6ffffff9 arg 0x00002d60 [ReadSoInfo:591] plt_rel (DT_JMPREL) found at 498a8 [ReadSoInfo:595] plt_rel_count (DT_PLTRELSZ) 549 [ReadSoInfo:584]symbol table found at 38f5000 [ReadSoInfo:580]string table found at 393b7a0 [ReadSoInfo:699]Unused DT entry: type 0x6ffffef5 arg 0x03a12219 [ReadSoInfo:629] constructors (DT_INIT_ARRAY) found at 445738 [ReadSoInfo:633] constructors (DT_INIT_ARRAYSZ) 13 [ReadSoInfo:637] destructors (DT_FINI_ARRAY) found at 445728 [ReadSoInfo:641] destructors (DT_FINI_ARRAYSZ) 2 [ReadSoInfo:699]Unused DT entry: type 0x6ffffff0 arg 0x03a0c421 [ReadSoInfo:699]Unused DT entry: type 0x6ffffffe arg 0x00003c68 [ReadSoInfo:699]Unused DT entry: type 0x6fffffff arg 0x00000003 [ReadSoInfo:703]=======================ReadSoInfo End========================= [RebuildShdr:42]=======================RebuildShdr========================= [RebuildShdr:536]=====================RebuildShdr End====================== [RebuildRelocs:783]=======================RebuildRelocs========================= [RebuildRelocs:809]=======================RebuildRelocs End======================= [RebuildFin:709]=======================try to finish file rebuild ========================= [RebuildFin:733]=======================End========================= [main:123]Done!!! /data/local/tmp/liba1re03.so.dump.so.fix.so: 1 file pulled, 0 skipped. 38.0 MB/s (60941163 bytes in 1.528s) liba1re03.so_0x7b6b4d1000_60940288_fix.so Now if we view .init_array section we can see a bunch of pointers to functions that will initialize globals and important stuff for the lib: _INIT_4 seems to have some python code related to the flag: Extracting the code from the string we get:\nimport android from android import decode, hash import json data = json.loads(json_data) login, password = data login = decode(login) password = decode(password) flag = login + password h = hash(flag).hex() if h != android.__FLAG__: android.print(\u0026#34;Humm it looks like, it\u0026#39;s not the good flag ...\u0026#34;) android.print(\u0026#34;It should be {} while it is {}\u0026#34;.format(android.__FLAG__, h)) else: android.print(\u0026#34;Well done!\u0026#34;) is_valid = True It seems flag check is being done here, and the flag is the combination of login and password, looks like the function hash is from a custom module named android, for now we still don\u0026rsquo;t know what is the value of android.__FLAG__ and what the function hash does, but if look into adb logcat we can actually see the function print is just some logging function which will appear in the logcat:\nadb shell logcat | grep \u0026#39;omvll\u0026#39; 07-23 00:34:09.465 23655 23655 I omvll : Humm it looks like, it\u0026#39;s not the good flag ... 07-23 00:34:09.465 23655 23655 I omvll : It should be f5ca458deb9629a74d4b0c3669deb5078a6a85a90afba9a3c76f5306a4bafb06 while it is e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 It looks like the concatenation of the login and password should be e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 after applying the hash function, from the size of the hash it looks like this is some kind of sha256 but we need confirmation.\nWe could try to look in the native lib where the module is being initiated or loaded, but since we know that the global variable is located at 0x548778 - 0x100000 we can just write a frida script and inject our own python code to inspect this module!\nvar libname = \u0026#34;liba1re03.so\u0026#34;; var moduleBaseAddress = Module.findBaseAddress(libname); var ghidra_base = 0x100000; const inject_python = `import android from android import decode, hash android.print(hash.__doc__)`; const python_addr = moduleBaseAddress.add(0x548778-ghidra_base); python_addr.writeUtf8String(inject_python); The output:\n$ echo -n \u0026#39;abc\u0026#39; | sha256sum ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad $ frida -Ul inj_k.js -F --no-pause $ adb shell logcat | grep \u0026#39;omvll\u0026#39; # login in the app to trigger the print 07-23 01:54:37.928 24105 24105 I omvll : ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad This confirms that we indeed are dealing with sha256 hash. When I got this confirmation, I said to myself that there is no way this challenge is to bruteforce the login and password with a dictionary attack or something. I started to believe that maybe the dev left something within the custom android module that is not being used in the main script that could give us some tips about how the hash got generated or something:\nvar libname = \u0026#34;liba1re03.so\u0026#34;; var moduleBaseAddress = Module.findBaseAddress(libname); var ghidra_base = 0x100000; const inject_python = `import android from android import decode, hash android.print(str(dir(android)))`; const python_addr = moduleBaseAddress.add(0x548778-ghidra_base); python_addr.writeUtf8String(inject_python); And we saw 3 interesting fields MvtKNJXCOGJe, __bc__ and __doc__.\n07-23 15:28:56.809 27698 27698 I omvll : [\u0026#39;MvtKNJXCOGJe\u0026#39;, \u0026#39;__FLAG__\u0026#39;, \u0026#39;__bc__\u0026#39;, \u0026#39;__doc__\u0026#39;, \u0026#39;__loader__\u0026#39;, \u0026#39;__name__\u0026#39;, \u0026#39;__package__\u0026#39;, \u0026#39;__spec__\u0026#39;, \u0026#39;decode\u0026#39;, \u0026#39;hash\u0026#39;, \u0026#39;print\u0026#39;] MvtKNJXCOGJe is a function that receives a string and returns bytes:\nandroid.print(str(android.MvtKNJXCOGJe.__doc__)); The documentation of the function:\nI omvll : MvtKNJXCOGJe(arg0: str) -\u0026gt; bytes __bc__ seems to be a sequence of python bytecode which, after removing the new lines and decode hex data we get something very similar to a pyc file ? (header seems to be different and decompilers won\u0026rsquo;t work)\nandroid.print(str(android.__bc__)); 7-23 21:24:54.840 29800 29800 I omvll : 700d0d0a000000004aaf626335010000e300000000000000000000000000000000040000004 07-23 21:24:54.840 29800 29800 I omvll : 00000007338000000640064016d005a00640064016d015a0164026502640365036604640464 07-23 21:24:54.840 29800 29800 I omvll : 0583045a04640265026403650366046406640783045a05640153002908e9000000004eda046 ... __doc__ This contains some hash similar to the sha256 but we don\u0026rsquo;t know yet for what it used.\nandroid.print(str(android.__doc__)); 07-23 21:25:44.407 29800 29800 I omvll : 9c16a9c3017d2b3876323bc4f9dad2b7530c My next step was to see what code is behind MvtKNJXCOGJe we tried using the built-in module dis to get the disassemble code but it seems the function returns an error:\nAbort message: \u0026#39;terminating with uncaught exception of type pybind11::error_already_set: TypeError: don\u0026#39;t know how to disassemble builtin_function_or_method objects This means that this module is being loaded in the native code using cpython or pybind11.\nTo understand a little better I did some research on google and I learned that you could create a python module using cpython like this:\n#include \u0026lt;Python.h\u0026gt; static char* __flag__ = \u0026#34;f0d15e5bb173d9a281cfaf2a2b01779a7e78c2b24a48f2cc74563b235c4c5b9b\u0026#34;; // Module method table static PyMethodDef AndroidMethods[] = { {NULL, NULL, 0, NULL} }; // Module definition static struct PyModuleDef androidmodule = { PyModuleDef_HEAD_INIT, \u0026#34;android\u0026#34;, NULL, -1, AndroidMethods }; // Module initialization function PyMODINIT_FUNC PyInit_android(void) { PyObject* module = PyModule_Create(\u0026amp;androidmodule); // Add the __flag__ variable to the module PyObject* flag = Py_BuildValue(\u0026#34;s\u0026#34;, __flag__); if (flag) { PyModule_AddObject(module, \u0026#34;__flag__\u0026#34;, flag); } return module; } In a main program we could do something like this:\n#include \u0026lt;Python.h\u0026gt; // Declare the init function for the \u0026#34;android\u0026#34; module extern PyObject* PyInit_android(void); int main(int argc, char* argv[]) { // Initialize the Python interpreter Py_Initialize(); // Add the \u0026#34;android\u0026#34; module to the pyinittab PyImport_AppendInittab(\u0026#34;android\u0026#34;, \u0026amp;PyInit_android); // Start the interpreter Py_Main(argc, argv); // Finalize the Python interpreter Py_Finalize(); return 0; } After running:\n$ ./interpreter Python 3.6.9 (default, Jul 23 2023, 00:00:00) [GCC 8.4.0] on linux Type \u0026#34;help\u0026#34;, \u0026#34;copyright\u0026#34;, \u0026#34;credits\u0026#34; or \u0026#34;license\u0026#34; for more information. \u0026gt;\u0026gt;\u0026gt; import android \u0026gt;\u0026gt;\u0026gt; print(android.__flag__) \u0026#39;f0d15e5bb173d9a281cfaf2a2b01779a7e78c2b24a48f2cc74563b235c4c5b9b\u0026#39; A good strategy here is to actually find where the string \u0026ldquo;android\u0026rdquo; is being called in the android code: This already looks promissing: Diving in FUN_00280c08 we can see that there is a function that looks like is adding somekind of variable __flag__ to the module: Searching for xrefs to those functions lead me to more assignments of __bc__ and __doc__: But the most important one was the function FUN_002c22b0 contains the print string, which probably means that this function might be responsible for function attribution, searching for xrefs didn\u0026rsquo;t find anything which means this is probably some kind of proxy call, so we might need to check some of the internal calls: Searching for MvtKNJXCOGJe I didn\u0026rsquo;t find anything, so this means that the author might have used StringEncOptStack instead of StringEncOptGlobal to hide this string, so I assumed that these internal functions are related to function attributions to the python module, probably related to pybind11 so I decided to hook FUN_002ce5cc we know that the second parameter is the name of the function and the 3rd is most likely the pointer to the function definition so we can write a frida script to hook this:\nvar do_dlopen = null; var call_ctor = null; var moduleBaseAddress = null; var hooked = false; var libname = \u0026#34;liba1re03.so\u0026#34;; var ghidra_base = 0x100000; Process.findModuleByName(Process.pointerSize === 4 ? \u0026#39;linker\u0026#39; : \u0026#39;linker64\u0026#39;).enumerateSymbols().forEach(function (sym) { if (sym.name.indexOf(\u0026#39;do_dlopen\u0026#39;) \u0026gt;= 0) { do_dlopen = sym.address; } else if (sym.name.indexOf(\u0026#39;call_constructor\u0026#39;) \u0026gt;= 0) { call_ctor = sym.address; } }); Interceptor.attach(do_dlopen,{ onEnter: function(args){ var soName = args[0].readCString(); var temp = soName.split(\u0026#34;/\u0026#34;).pop(); this.libname = temp; if (temp.indexOf(libname) \u0026gt; -1) { Interceptor.attach(call_ctor, function () { if(hooked == false) { moduleBaseAddress = Module.findBaseAddress(temp); hooked = true; before_init_initarray(temp); } }); } }, onLeave: function(retval){ if (this.libname.includes(libname)) { after_init_initarray(this.libname); } } }); function before_init_initarray(libname){ Interceptor.attach(moduleBaseAddress.add(0x2ce5cc-ghidra_base),{ onEnter: function(args){ console.log(args[1].readCString() + \u0026#34; \u0026#34; + args[2]); console.log(\u0026#39;called from:\\n\u0026#39; + Thread.backtrace(this.context, Backtracer.ACCURATE) .map(DebugSymbol.fromAddress).join(\u0026#39;\\n\u0026#39;) + \u0026#39;\\n\u0026#39;); }, onLeave: function(retval){ }}); } function after_init_initarray(libname){} The code above is not entirely necessary. I added this in case you want to hook something before some function in .init_array executes. This involves hooking some android linker functions and stuff, but it\u0026rsquo;s not necessary if you really want, you could just attach to the app and only contain the code inside of before_init_initarray function.\n$ frida -Ul inj_k2.js -f re.obfuscator.challenge01 --no-pause # prints will only trigger after performing the login in the app [Pixel 4 XL::re.obfuscator.challenge01 ]-\u0026gt; print called from: 0x7b6449033c liba1re03.so!0x1c233c 0x7b6449033c liba1re03.so!0x1c233c MvtKNJXCOGJe called from: 0x7b644904c4 liba1re03.so!0x1c24c4 0x7b644904c4 liba1re03.so!0x1c24c4 decode called from: 0x7b644911f8 liba1re03.so!0x1c31f8 0x7b644911f8 liba1re03.so!0x1c31f8 hash called from: 0x7b644907bc liba1re03.so!0x1c27bc 0x7b644907bc liba1re03.so!0x1c27bc Looking at the address call 0x1c24c4 + 0x100000 in ghidra: If we instruct ghidra to disassemble the code: Let\u0026rsquo;s hook that line and trigger the call by injecting python:\nvar libname = \u0026#34;liba1re03.so\u0026#34;; var moduleBaseAddress = Module.findBaseAddress(libname); var ghidra_base = 0x100000; // on blr x8 to get the function pointer to the indirect call Interceptor.attach(moduleBaseAddress.add(0x2c3488-ghidra_base),{ onEnter: function(args){ console.log(this.context.x8.sub(moduleBaseAddress).add(ghidra_base)); }, onLeave: function(retval){ } }); const inject_python = `import android android.MvtKNJXCOGJe(\u0026#39;abc\u0026#39;)`; const python_addr = moduleBaseAddress.add(0x548778-ghidra_base); python_addr.writeUtf8String(inject_python); We get the address:\n$ frida -Ul inj_k2.js -F --no-pause # print will trigger only after trying to login [Pixel 4 XL::Open-Obfuscator Challenge ]-\u0026gt; 0x2c0ea8 After disassembling the function, we get a huge function: I didn\u0026rsquo;t want to dive in into this function before understanding the context of this, I could end up reversing an entire function for nothing. So, after analysing the application with more attention, we noticed some files that were dropped into the cache folder /data/data/re.obfuscator.challenge01/cache:\n$ adb shell \u0026#34;ls /data/data/re.obfuscator.challenge01/cache/WebView/Default/Web3\u0026#34; 1 ⚙ LICENSE.txt __future__.py __phello__.foo.py __pycache__ _aix_support.py _bootsubprocess.py _collections_abc.py _compat_pickle.py _compression.py _markupbase.py _osx_support.py _py_abc.py _pydecimal.py _pyio.py _sitebuiltins.py _strptime.py _sysconfigdata__linux_aarch64-linux-android.py _threading_local.py _weakrefset.py ... By reading the license file, we realized this seems to be the source code of python. Some of the files here are python built-ins. After finding this, we pulled the folder:\n$ adb pull /data/data/re.obfuscator.challenge01/cache/WebView By searching for one of the strange variables we found in android module with recursive grep, we found it was referenced in one of the files:\n$ grep -ria \u0026#39;__bc__\u0026#39; WebView WebView/Default/Web3/pyloader.py: return bytes.fromhex(android.__bc__.replace(\u0026#34;\\n\u0026#34;, \u0026#34;\u0026#34;).strip().replace(\u0026#34; \u0026#34;, \u0026#34;\u0026#34;)) The python file:\nimport importlib from importlib.machinery import SourcelessFileLoader from importlib.util import spec_from_file_location import sys import android class FileLoader(SourcelessFileLoader): def __init__(self): super().__init__(\u0026#34;checker\u0026#34;, \u0026#34;checker.cpython-310.pyc\u0026#34;) def get_data(self, path: str): return bytes.fromhex(android.__bc__.replace(\u0026#34;\\n\u0026#34;, \u0026#34;\u0026#34;).strip().replace(\u0026#34; \u0026#34;, \u0026#34;\u0026#34;)) def import_checker(): loader = FileLoader() spec = spec_from_file_location(\u0026#39;checker\u0026#39;, \u0026#34;checker.cpython-310.pyc\u0026#34;,loader=loader) module = importlib._bootstrap._load(spec) sys.modules[\u0026#39;checker\u0026#39;] = module return module Looks like the __bc__ is a hidden module, like I said before we tried before to decompile this specific variable, but it looks like Romain Thomas did change the python source code, making it harder for us to recover the original code. Running this code on our machine also wouldn\u0026rsquo;t work because of these modifications. The bytecode would throw errors, then I had the idea of actually injecting this code into the interpreter in the application like we did before for other purposes, then we could list all objects in the module and maybe use the builtin dis on the functions to view a better representation of the bytecode:\nvar libname = \u0026#34;liba1re03.so\u0026#34;; var moduleBaseAddress = Module.findBaseAddress(libname); var ghidra_base = 0x100000; const inject_python = `import importlib from importlib.machinery import SourcelessFileLoader from importlib.util import spec_from_file_location import sys import android,dis,string class FileLoader(SourcelessFileLoader): def __init__(self): super().__init__(\u0026#34;checker\u0026#34;, \u0026#34;checker.cpython-310.pyc\u0026#34;) def get_data(self, path: str): import android return bytes.fromhex(android.__bc__.replace(\u0026#34;\\\\n\u0026#34;, \u0026#34;\u0026#34;).strip().replace(\u0026#34; \u0026#34;, \u0026#34;\u0026#34;)) loader = FileLoader() spec = spec_from_file_location(\u0026#39;checker\u0026#39;, \u0026#34;checker.cpython-310.pyc\u0026#34;,loader=loader) module = importlib._bootstrap._load(spec) android.print(str(dir(module))) const python_addr = moduleBaseAddress.add(0x548778-ghidra_base); python_addr.writeUtf8String(inject_python); $ adb logcat | grep \u0026#39;omvll\u0026#39; 07-24 01:35:30.516 31523 31523 I omvll : [\u0026#39;__builtins__\u0026#39;, \u0026#39;__cached__\u0026#39;, \u0026#39;__doc__\u0026#39;, \u0026#39;__file__\u0026#39;, \u0026#39;__loader__\u0026#39;, \u0026#39;__name__\u0026#39;, \u0026#39;__package__\u0026#39;, \u0026#39;__spec__\u0026#39;, \u0026#39;android\u0026#39;, \u0026#39;check\u0026#39;, \u0026#39;json\u0026#39;, \u0026#39;verify\u0026#39;] I found this interesting function named check, so let\u0026rsquo;s use dis to disassemble the function and view the code:\nvar libname = \u0026#34;liba1re03.so\u0026#34;; var moduleBaseAddress = Module.findBaseAddress(libname); var ghidra_base = 0x100000; const inject_python = `import importlib from importlib.machinery import SourcelessFileLoader from importlib.util import spec_from_file_location import sys import android,dis,string class FileLoader(SourcelessFileLoader): def __init__(self): super().__init__(\u0026#34;checker\u0026#34;, \u0026#34;checker.cpython-310.pyc\u0026#34;) def get_data(self, path: str): import android return bytes.fromhex(android.__bc__.replace(\u0026#34;\\\\n\u0026#34;, \u0026#34;\u0026#34;).strip().replace(\u0026#34; \u0026#34;, \u0026#34;\u0026#34;)) loader = FileLoader() spec = spec_from_file_location(\u0026#39;checker\u0026#39;, \u0026#34;checker.cpython-310.pyc\u0026#34;,loader=loader) module = importlib._bootstrap._load(spec) def get_instruction_repr(instruction): import dis opcode, arg, lineno = instruction.opname,instruction.argval, instruction.starts_line if instruction.arg is not None: arg_str = f\u0026#34; {arg}\u0026#34; return f\u0026#34;{lineno}: {opcode}{arg_str}\u0026#34; else: return f\u0026#34;{lineno}: {opcode}\u0026#34; bytecode = dis.Bytecode(module.check) for instruction in bytecode: android.print(get_instruction_repr(instruction)) android.print(\u0026#34;android.__doc__ -\u0026gt; \u0026#34;+android.__doc__) `; const python_addr = moduleBaseAddress.add(0x548778-ghidra_base); python_addr.writeUtf8String(inject_python); The code is very simple to understand and we can see a very similar code to the code we saw in the global string comparison with the sha256 hash:\n07-24 01:35:30.516 31523 31523 I omvll : 5: LOAD_GLOBAL json 07-24 01:35:30.516 31523 31523 I omvll : None: LOAD_METHOD_ENC loads 07-24 01:35:30.516 31523 31523 I omvll : None: LOAD_FAST data 07-24 01:35:30.516 31523 31523 I omvll : None: CALL_METHOD 1 07-24 01:35:30.516 31523 31523 I omvll : None: UNPACK_SEQUENCE 2 07-24 01:35:30.516 31523 31523 I omvll : None: STORE_FAST login 07-24 01:35:30.516 31523 31523 I omvll : None: STORE_FAST password 07-24 01:35:30.516 31523 31523 I omvll : 6: LOAD_GLOBAL android 07-24 01:35:30.516 31523 31523 I omvll : None: LOAD_METHOD_ENC decode 07-24 01:35:30.516 31523 31523 I omvll : None: LOAD_FAST login 07-24 01:35:30.516 31523 31523 I omvll : None: CALL_METHOD 1 07-24 01:35:30.516 31523 31523 I omvll : None: STORE_FAST login 07-24 01:35:30.517 31523 31523 I omvll : 7: LOAD_GLOBAL android 07-24 01:35:30.517 31523 31523 I omvll : None: LOAD_METHOD_ENC decode 07-24 01:35:30.517 31523 31523 I omvll : None: LOAD_FAST password 07-24 01:35:30.517 31523 31523 I omvll : None: CALL_METHOD 1 07-24 01:35:30.517 31523 31523 I omvll : None: STORE_FAST password 07-24 01:35:30.517 31523 31523 I omvll : 8: LOAD_GLOBAL android 07-24 01:35:30.517 31523 31523 I omvll : None: LOAD_METHOD_ENC __obfuscated__ 07-24 01:35:30.517 31523 31523 I omvll : None: LOAD_FAST login 07-24 01:35:30.517 31523 31523 I omvll : None: LOAD_FAST password 07-24 01:35:30.517 31523 31523 I omvll : None: BINARY_ADD 07-24 01:35:30.517 31523 31523 I omvll : None: CALL_METHOD 1 07-24 01:35:30.517 31523 31523 I omvll : None: LOAD_METHOD_ENC hex 07-24 01:35:30.517 31523 31523 I omvll : None: CALL_METHOD 0 07-24 01:35:30.517 31523 31523 I omvll : None: LOAD_GLOBAL android 07-24 01:35:30.517 31523 31523 I omvll : None: LOAD_ATTR __doc__ 07-24 01:35:30.517 31523 31523 I omvll : None: COMPARE_OP == 07-24 01:35:30.517 31523 31523 I omvll : None: RETURN_VALUE 07-24 01:38:36.452 31523 31523 I omvll : 5: LOAD_GLOBAL json 07-24 01:38:36.453 31523 31523 I omvll : None: LOAD_METHOD_ENC loads 07-24 01:38:36.453 31523 31523 I omvll : None: LOAD_FAST data 07-24 01:38:36.453 31523 31523 I omvll : None: CALL_METHOD 1 07-24 01:38:36.453 31523 31523 I omvll : None: UNPACK_SEQUENCE 2 07-24 01:38:36.453 31523 31523 I omvll : None: STORE_FAST login 07-24 01:38:36.453 31523 31523 I omvll : None: STORE_FAST password 07-24 01:38:36.453 31523 31523 I omvll : 6: LOAD_GLOBAL android 07-24 01:38:36.453 31523 31523 I omvll : None: LOAD_METHOD_ENC decode 07-24 01:38:36.453 31523 31523 I omvll : None: LOAD_FAST login 07-24 01:38:36.453 31523 31523 I omvll : None: CALL_METHOD 1 07-24 01:38:36.453 31523 31523 I omvll : None: STORE_FAST login 07-24 01:38:36.453 31523 31523 I omvll : 7: LOAD_GLOBAL android 07-24 01:38:36.453 31523 31523 I omvll : None: LOAD_METHOD_ENC decode 07-24 01:38:36.453 31523 31523 I omvll : None: LOAD_FAST password 07-24 01:38:36.453 31523 31523 I omvll : None: CALL_METHOD 1 07-24 01:38:36.453 31523 31523 I omvll : None: STORE_FAST password 07-24 01:38:36.453 31523 31523 I omvll : 8: LOAD_GLOBAL android 07-24 01:38:36.453 31523 31523 I omvll : None: LOAD_METHOD_ENC __obfuscated__ 07-24 01:38:36.453 31523 31523 I omvll : None: LOAD_FAST login 07-24 01:38:36.453 31523 31523 I omvll : None: LOAD_FAST password 07-24 01:38:36.453 31523 31523 I omvll : None: BINARY_ADD 07-24 01:38:36.453 31523 31523 I omvll : None: CALL_METHOD 1 07-24 01:38:36.453 31523 31523 I omvll : None: LOAD_METHOD_ENC hex 07-24 01:38:36.454 31523 31523 I omvll : None: CALL_METHOD 0 07-24 01:38:36.454 31523 31523 I omvll : None: LOAD_GLOBAL android 07-24 01:38:36.454 31523 31523 I omvll : None: LOAD_ATTR __doc__ 07-24 01:38:36.454 31523 31523 I omvll : None: COMPARE_OP == 07-24 01:38:36.454 31523 31523 I omvll : None: RETURN_VALUE 07-24 01:38:36.454 31523 31523 I omvll : android.__doc__ -\u0026gt; 9c16a9c3017d2b3876323bc4f9dad2b7530c The most important part is the fact the function is using a function __obfuscated__ which we believe to be the same as MvtKNJXCOGJe and, instead of comparing the input with android.__flag__ it will compare with android.__doc__ which was the hash we didn\u0026rsquo;t know what was its purpose.\nAgain, before going deep into the native code of MvtKNJXCOGJe I did some tests with a few inputs and I realized that the function was a simple encryption function that was encrypting the input byte by byte. Knowing this, I knew we could just bruteforce and get the password:\nvar libname = \u0026#34;liba1re03.so\u0026#34;; var moduleBaseAddress = Module.findBaseAddress(libname); var ghidra_base = 0x100000; const inject_python = `import importlib from importlib.machinery import SourcelessFileLoader from importlib.util import spec_from_file_location import sys import android,string res = bytes.fromhex(android.__doc__) i = 0x0 flag = \u0026#39;\u0026#39; while i\u0026lt; len(res): for c in string.printable: _enc = android.MvtKNJXCOGJe(flag+c)[i] if _enc == res[i]: flag += c break i +=1 android.print(flag)`; const python_addr = moduleBaseAddress.add(0x548778-ghidra_base); python_addr.writeUtf8String(inject_python); After running we got the password:\n07-24 01:45:27.152 31523 31523 I omvll : 0MvLL_And_dPr0t3ct ","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1692555089,"objectID":"10cf95e88f4be4b59ec75cdac0c627c2","permalink":"https://obfuscator.re/challenges/2022-12-android-challenge/writeups/2023_07_24-korami/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/challenges/2022-12-android-challenge/writeups/2023_07_24-korami/","section":"challenges","summary":"Writeup for obfuscator.re Challenge 1 The application has a simple login screen:\nLogin screen Checking the source code in jadx:\n$ jadx-gui apks/challenge-pydroid.apk 2\u0026gt;\u0026amp;1 \u0026gt;/dev/null \u0026amp; We see that the code behind the check is inside a native function:\nInstalling the app and running frida:\n$ adb install apks/challenge-pydroid.apk Searching for system.load in jadx we can find where the lib is being loaded: ","tags":null,"title":"Writeup by korami","type":"writeups"},{"authors":["Robert Xiao"],"categories":null,"content":"Writeup for obfuscator.re Challenge 1 Author: Robert Xiao nneonneo@gmail.com\nWe\u0026rsquo;re provided an APK file, and told that it has been protected by O-MVLL (native code), dProtect (dex code), ELF format modifications, and runtime application self protection (RASP). Let\u0026rsquo;s dive in!\nOur first stop is to unzip the APK. There\u0026rsquo;s a single native library, lib/arm64-v8a/liba1re03.so weighing in at 27MB, along with bits of Kotlin data suggesting that parts of the app are written in Kotlin.\nI didn\u0026rsquo;t bother to install or run this app in any way; most of my reversing is done statically.\nJava/Kotlin code We can use JADX to inspect the Dalvik (dex) code and the AndroidManifest.xml. The code decompiles mostly fine (no bytecode-level tampering), but class and method names are obfuscated (as expected) and strings and numbers are encrypted.\nThere\u0026rsquo;s a ton of obfuscated classes in the anonymous package, most of which seem to correspond to bits of Kotlin code. Luckily, the really interesting stuff is all in the re.obfuscator.challenge01 package.\nFrom AndroidManifest.xml, we see that the main application class is re.obfuscator.challenge01.AHGILuuQdMj and the main activity is re.obfuscator.challenge01.sTxUFmGsNP; we can rename them to MainApplication and MainActivity respectively. From res/navigation/nav_graph.xml, we see that re.obfuscator.challenge01.XBuUyhhspa corresponds to FirstFragment, re.obfuscator.challenge01.LvfvlwOEfOn to SecondFragment, and re.obfuscator.challenge01.VCyPLJeiyfu to Validate; we can rename these accordingly.\nValidate (VCyPLJeiyfu) contains the only native method, public final native boolean PGPyIMEWUxFr(String str);. We see that it is called as follows:\nvalidate.aLO().aLB().mo1567aj(validate.aLN().etC.getText().toString()); validate.aLO().aLC().mo1567aj(Boolean.valueOf(validate.PGPyIMEWUxFr(validate.aLO().aLE()))); aLE appears to gather some values, presumably the login and password, and then creates a cfb object:\nreturn m13766a.m13833bf(new cfb(str, value3, ((int) esT[0]) ^ 2122205795)); There\u0026rsquo;s only one other reference to cfb in the code, in the function List\u0026lt;String\u0026gt; yakKojVORPA.toJson(cfb cfbVar). It calls cev.m6193dh on each of the cfb data elements, which in turn calls Base64.encodeToString. So, we can guess that the login and password are base64-encoded, put into a list and JSON-encoded, and then fed to the native function to validate the input. Knowing the input, we can now turn our attention to the native code.\nNative code liba1re03.so is quite large. Since we suspect the ELF headers have been tampered with, the first step is to fix them using a little script I wrote: rebuild_elf_sections.py. This script basically builds brand-new ELF section headers based on the result of parsing the segment headers and DYNAMIC segment.\nThe resulting binary loads just fine into Ghidra. We see a ton of garbage function references - these are the \u0026ldquo;fake exports\u0026rdquo; mentioned in the poor man\u0026rsquo;s obfuscator talk. It\u0026rsquo;s easy to just delete them in Ghidra - the fake symbols are all at odd addresses, which is impossible for AARCH64.\nThere are two relevant bits of code: the initialization functions from .init_array, called when the library is loaded, and the JNI_OnLoad function, called from System.loadLibrary in Java.\nThere are a total of 13 init_array functions, which I named init_0 through init_12. At a glance, init_0 through init_8 (except init_5) are just performing string decryption. init_5 is more complex; Ghidra decompiles it as follows:\nvoid init_5(void) { undefined *puVar1; ulong uVar2; long lVar3; undefined *puVar4; ulong uVar5; ulong uVar6; undefined auStack_40 [16]; undefined *local_30; code *local_28; undefined8 ****local_20 [2]; local_20[0] = local_20 + 1; local_30 = \u0026amp;DAT_00308cc4; uVar6 = (ulong)local_20[0] | (ulong)local_20; uVar2 = ((ulong)local_20[0] \u0026amp; (ulong)local_20) + uVar6; uVar5 = (long)local_20[0] + (long)local_20; uVar6 = ~uVar5 + uVar6; uVar6 = ((uVar2 | 1) \u0026amp; uVar6) + (uVar2 | 1 | uVar6); uVar5 += ~(ulong)local_20[0] | (ulong)local_20 ^ 0xffffffffffffffff; lVar3 = (uVar5 ^ uVar2 ^ 0xffffffffffffffff) + (uVar2 \u0026amp; (uVar5 ^ 0xffffffffffffffff)) * 2; uVar2 = -lVar3; uVar5 = uVar6 | uVar2; uVar2 = uVar5 - (uVar6 \u0026amp; uVar2); lVar3 = (uVar6 - lVar3) - uVar5; uVar2 = ((uVar2 \u0026amp; lVar3 * 2) + (uVar2 | lVar3 * 2)) - ((ulong)local_20[0] ^ (ulong)local_20); puVar4 = \u0026amp;DAT_00308cc4 + (uVar2 - (uVar2 | 0x308cc4)); puVar1 = \u0026amp;DAT_00308cc4 + (uVar2 ^ 0xffffffffffffffff | 0xffffffffffcf733b) + uVar2 + 1; uVar2 = ((ulong)puVar4 \u0026amp; (ulong)puVar1) + ((ulong)puVar4 | (ulong)puVar1); DAT_019f60c8 = 0x12; local_20[1] = (undefined8 ****)0xe; uVar6 = (uVar2 + 0x20) - (uVar2 | 0x20); uVar2 = uVar2 + 0x20 + (~uVar2 | 0xffffffffffffffdf) + 1; local_28 = (code *)((uVar6 \u0026amp; uVar2) + (uVar6 | uVar2)); (*local_28)(); return; } I recognize this as the control-flow breaking pass from O-MVLL: this is a wrapper which uses opaque constants derived from the stack pointer to obfuscate the real address of the function (called at the end via local_28). The code uses the low bits of sp, which are always zero due to stack alignment, resulting in a constant result. However, since the decompiler does not make this assumption, it outputs the raw (obfuscated) calculations.\nLuckily, I have a trick: using Set Register Values... in Ghidra, we can just set sp to a concrete value (say, 0x7fff000b0000) at the start of the function, and Ghidra\u0026rsquo;s constant propagation will do the rest:\n// WARNING: This function may have set the stack pointer void init_5(void) { undefined8 unaff_x30; uRam00007fff000affe0 = 0x7fff000affe8; puRam00007fff000affd0 = \u0026amp;DAT_00308cc4; DAT_019f60c8 = 0x12; uRam00007fff000affe8 = 0xe; pcRam00007fff000affd8 = FUN_00308ce4; uRam00007fff000afff0 = unaff_x30; FUN_00308ce4(); return; } Note that I also added 0x7fff\u0026hellip; to the memory map (Window -\u0026gt; Memory Map) so that stack references would still work. Now, it\u0026rsquo;s trivial to identify the real function (FUN_00308ce4) and decompile it to find another string decryption routine.\nWe can use emulation to recover the decrypted strings. I wrote a little script that uses the Unicorn engine to call functions from the binary. Note that Ghidra loads using a base address of 0x100000, but I don\u0026rsquo;t want to handle relocations, so I just load at address 0:\nfrom unicorn import * from unicorn.arm64_const import * import lief uc = Uc(UC_ARCH_ARM64, UC_MODE_ARM) STACK_TOP = 0x7fff_ffff_0000 END_ADDR = 0xffff_ffff_ffff_f000 # stack uc.mem_map(STACK_TOP - 0x100000, 0x101000, UC_PROT_READ|UC_PROT_WRITE) # return page uc.mem_map(END_ADDR, 0x1000, UC_PROT_EXEC) def align_down(addr: int, align: int = 4096) -\u0026gt; int: return addr // align * align def align_up(addr: int, align: int = 4096) -\u0026gt; int: return (addr + align - 1) // align * align def elf_prot_to_uc(prot: lief.ELF.SEGMENT_FLAGS) -\u0026gt; int: res = 0 if prot \u0026amp; lief.ELF.SEGMENT_FLAGS.R: res |= UC_PROT_READ if prot \u0026amp; lief.ELF.SEGMENT_FLAGS.W: res |= UC_PROT_WRITE if prot \u0026amp; lief.ELF.SEGMENT_FLAGS.X: res |= UC_PROT_EXEC return res def load_elf(uc, filename): f = lief.parse(filename) for seg in f.segments: if seg.type == lief.ELF.SEGMENT_TYPES.LOAD: page_start = align_down(seg.virtual_address) page_end = align_up(seg.virtual_address + seg.virtual_size) uc.mem_map(page_start, page_end - page_start, elf_prot_to_uc(seg.flags)) if seg.content: uc.mem_write(seg.virtual_address, bytes(seg.content)) def call_func(uc, addr): uc.reg_write(UC_ARM64_REG_SP, STACK_TOP) uc.reg_write(UC_ARM64_REG_LR, END_ADDR) uc.emu_start(addr, END_ADDR) def rreg(name): return uc.reg_read(globals()[\u0026#34;UC_ARM64_REG_\u0026#34; + name.upper()]) def rstr(addr): res = bytearray() while 1: b = uc.mem_read(addr, 1) if b == b\u0026#34;\\x00\u0026#34;: break res += b addr += 1 return res load_elf(uc, \u0026#34;liba1re03.so\u0026#34;) call_func(uc, 0x1c2004) print(uc.mem_read(0x4481f0, 9)) call_func(uc, 0x1cda30) print(uc.mem_read(0x4481f9, 9)) call_func(uc, 0x1cdc70) print(uc.mem_read(0x448201, 0x50d)) call_func(uc, 0x1cd628) print(uc.mem_read(0x44870e, 7)) call_func(uc, 0x1de9d4) print(uc.mem_read(0x448778, 0x212)) call_func(uc, 0x2021a4) print(uc.mem_read(0x44898a, 16)) call_func(uc, 0x202720) print(uc.mem_read(0x44899a, 0x1a)) call_func(uc, 0x201540) print(uc.mem_read(0x4489b4, 8)) call_func(uc, 0x1ff984) print(uc.mem_read(0x4489bc, 0x1a)) This gives us the following strings:\nbytearray(b\u0026#39;__FLAG__\\x00\u0026#39;) bytearray(b\u0026#39;__doc__\\x00\\x9b\u0026#39;) bytearray(b\u0026#39;\\n 700d0d0a000000004aaf626335010000e300000000000000000000000000000000040000004\\n 00000007338000000640064016d005a00640064016d015a0164026502640365036604640464\\n 0583045a04640265026403650366046406640783045a05640153002908e9000000004eda046\\n 4617461da0672657475726e6301000000000000000000000003000000040000004300000073\\n 3a0000007400a1017d00a2015c027c017c027402a1037d01a2017c017402a1037d02a2017c0\\n 27402a1047d017d021800a201a105a20074026a066b02530029014e2907da046a736f6eda05\\n 6c6f616473da07616e64726f6964da066465636f6465da0e5f5f6f6266757363617465645f5\\n fda03686578da075f5f646f635f5f290372020000005a056c6f67696eda0870617373776f72\\n 64a900720c000000fa502f686f6d652f726f6d61696e2f6465762f6f70656e2d6f626675736\\n 361746f722f6368616c6c656e67652f6368616c6c656e67652d30312f736372697074732f65\\n 787472612f636865636b65722e7079da05636865636b0400000073080000000e010a010a011\\n 801720e00000063010000000000000000000000010000000200000043000000730c00000074\\n 007d00840164016b02530029024e72010000002901da036c656e29017202000000720c00000\\n 0720c000000720d000000da067665726966790a00000073020000000c017210000000290672\\n 060000007204000000da03737472da04626f6f6c720e0000007210000000720c000000720c0\\n 00000720c000000720d000000da083c6d6f64756c653e010000007308000000080008011202\\n 1606\\n \\x00\u0026#39;) bytearray(b\u0026#39;__bc__\\x00\u0026#39;) bytearray(b\u0026#39;\\n import android\\n from android import decode, hash\\n import json\\n data = json.loads(json_data)\\n login, password = data\\n\\n login = decode(login)\\n password = decode(password)\\n\\n flag = login + password\\n h = hash(flag).hex()\\n if h != android.__FLAG__:\\n android.print(\u0026#34;Humm it looks like, it\\\u0026#39;s not the good flag ...\u0026#34;)\\n android.print(\u0026#34;It should be {} while it is {}\u0026#34;.format(android.__FLAG__, h))\\n else:\\n android.print(\u0026#34;Well done!\u0026#34;)\\n is_valid = True\\n \\x00\u0026#39;) bytearray(b\u0026#39;/proc/self/task\\x00\u0026#39;) bytearray(b\u0026#39;/proc/self/task/{}/status\\x00\u0026#39;) bytearray(b\u0026#39;libc.so\\x00\u0026#39;) bytearray(b\u0026#39;__system_property_foreach\\x00\u0026#39;) Looks like Python code! This would explain why the binary is so huge. Let\u0026rsquo;s take a detour to look at the Python bits\u0026hellip;\nEmbedded Python bits A Python interpreter means the Python stdlib must be present. Since there\u0026rsquo;s no Python stuff in the APK, the stdlib is probably packed in the binary. Indeed, at offset 0x4469E0, we can find a ZIP header (PK\\3\\4). Extracting this, we get a 21.5 MB ZIP file containing the entire Python 3.10 standard library. We also get a few extra files of interest:\n_sysconfigdata__linux_aarch64-linux-android.py: shows the full configuration of the build, including file paths pyloader.py: not part of the Python standard lib, contains the following code: import importlib from importlib.machinery import SourcelessFileLoader from importlib.util import spec_from_file_location import sys import android class FileLoader(SourcelessFileLoader): def __init__(self): super().__init__(\u0026#34;checker\u0026#34;, \u0026#34;checker.cpython-310.pyc\u0026#34;) def get_data(self, path: str): return bytes.fromhex(android.__bc__.replace(\u0026#34;\\n\u0026#34;, \u0026#34;\u0026#34;).strip().replace(\u0026#34; \u0026#34;, \u0026#34;\u0026#34;)) def import_checker(): loader = FileLoader() spec = spec_from_file_location(\u0026#39;checker\u0026#39;, \u0026#34;checker.cpython-310.pyc\u0026#34;, loader=loader) module = importlib._bootstrap._load(spec) sys.modules[\u0026#39;checker\u0026#39;] = module return module config-3.10: a directory containing lots of Python build artifacts, including config.c, Makefile, Setup, libpython3.10.a and python.o. The inclusion of config-3.10 is strange, and very pointless: libpython3.10.a is huge (23.6 MB) and completely unnecessary at runtime, plus it contains symbols for the entire Python standard library. We can take advantage of it: by compiling python.o with libpython3.10.a, we get a binary with all of the Python symbols:\naarch64-linux-android-gcc python.o -l python3.10 -o python.elf --sysroot=${NDK_HOME}/platforms/android-24/arch-arm64 -L. -lm\nI loaded this binary into Ghidra, then used the Version Tracker (a bindiff-like tool) to apply all of the symbols to liba1re03.so, thereby allowing me to see proper symbols for pretty much the entire Python interpreter. I also imported all of the data types from the libpython DWARF.\npyloader.py is also pretty interesting. It suggests that the real checker function is loaded from the __bc__ constant, which is referred to in our decrypted strings above. We\u0026rsquo;ll revisit this soon.\nReturning to the native binary init_9 looks like it\u0026rsquo;s setting up some kind of thread, maybe a security mechanism. I ignored it. init_10 just calls __cxa_atexit, and init_12 is checking to see if the CPU has LSE atomics (__aarch64_have_lse_atomics).\ninit_11 is more interesting. From the strings it uses and our recovered Python interpreter symbols, we can see that it\u0026rsquo;s defining a pybind11 extension module named android. The main function is FUN_002c35d8; this is a long function with a lot of inlined string decryption junk. Again, I chose to just run this in Unicorn, after working out what functions it calls:\n# pybind11_init_android uc.reg_write(UC_ARM64_REG_SP, STACK_TOP) uc.reg_write(UC_ARM64_REG_LR, END_ADDR) def hook_code(uc, addr, sz, userdata): pc = uc.reg_read(UC_ARM64_REG_PC) if not 0x1c35d8 \u0026lt;= pc \u0026lt; 0x1cd318: if pc == END_ADDR: uc.emu_stop() elif pc == 0x164888: print(f\u0026#34;str({rreg(\u0026#39;x0\u0026#39;):#x}, {rstr(rreg(\u0026#39;x1\u0026#39;)).decode()!r})\u0026#34;) elif pc == 0x180c08: print(f\u0026#34;str_attr_accessor({rreg(\u0026#39;x8\u0026#39;):#x}, {rreg(\u0026#39;x0\u0026#39;):#x}, {rstr(rreg(\u0026#39;x1\u0026#39;)).decode()!r})\u0026#34;) elif pc == 0x1bfa48: print(f\u0026#34;str_attr_accessor::=({rreg(\u0026#39;x0\u0026#39;):#x}, {rreg(\u0026#39;x1\u0026#39;):#x})\u0026#34;) elif pc == 0x1a6e18: print(f\u0026#34;~str_attr_accessor({rreg(\u0026#39;x0\u0026#39;):#x})\u0026#34;) elif pc == 0x1ab018: print(f\u0026#34;~str({rreg(\u0026#39;x0\u0026#39;):#x})\u0026#34;) elif pc == 0x1c22b0: print(f\u0026#34;0x1c22b0({rreg(\u0026#39;x0\u0026#39;):#x}, {rstr(rreg(\u0026#39;x1\u0026#39;)).decode()!r}, {rreg(\u0026#39;x2\u0026#39;):#x})\u0026#34;) elif pc == 0x1c2438: print(f\u0026#34;0x1c2438({rreg(\u0026#39;x0\u0026#39;):#x}, {rstr(rreg(\u0026#39;x1\u0026#39;)).decode()!r}, {rreg(\u0026#39;x2\u0026#39;):#x})\u0026#34;) elif pc == 0x1c316c: print(f\u0026#34;0x1c316c({rreg(\u0026#39;x0\u0026#39;):#x}, {rstr(rreg(\u0026#39;x1\u0026#39;)).decode()!r}, {rreg(\u0026#39;x2\u0026#39;):#x})\u0026#34;) elif pc == 0x1c2730: print(f\u0026#34;0x1c2730({rreg(\u0026#39;x0\u0026#39;):#x}, {rstr(rreg(\u0026#39;x1\u0026#39;)).decode()!r}, {rreg(\u0026#39;x2\u0026#39;):#x})\u0026#34;) else: print(hex(pc), hex(uc.reg_read(UC_ARM64_REG_X0)), hex(uc.reg_read(UC_ARM64_REG_X1))) uc.reg_write(UC_ARM64_REG_PC, uc.reg_read(UC_ARM64_REG_LR)) hook = uc.hook_add(UC_HOOK_CODE, hook_code, None, 0, 0xffffffff) uc.emu_start(0x1c35d8, 0x1cd318) uc.hook_del(hook) This is quite helpful. We get lots of information:\nstr(0x7ffffffefeb8, \u0026#39;f5ca458deb9629a74d4b0c3669deb5078a6a85a90afba9a3c76f5306a4bafb06\u0026#39;) str_attr_accessor(0x7ffffffeff70, 0x0, \u0026#39;__FLAG__\u0026#39;) str_attr_accessor::=(0x7ffffffeff70, 0x7ffffffefeb8) ~str_attr_accessor(0x7ffffffeff70) ~str(0x7ffffffefeb8) str(0x7ffffffefeb0, \u0026#39;9c16a9c3017d2b3876323bc4f9dad2b7530c\u0026#39;) str_attr_accessor(0x7ffffffeff50, 0x0, \u0026#39;__doc__\u0026#39;) str_attr_accessor::=(0x7ffffffeff50, 0x7ffffffefeb0) ~str_attr_accessor(0x7ffffffeff50) ~str(0x7ffffffefeb0) str(0x7ffffffefea8, \u0026#39;\\n 700d0d0a000000004aaf626335010000e300000000000000000000000000000000040000004\\n 00000007338000000640064016d005a00640064016d015a0164026502640365036604640464\\n 0583045a04640265026403650366046406640783045a05640153002908e9000000004eda046\\n 4617461da0672657475726e6301000000000000000000000003000000040000004300000073\\n 3a0000007400a1017d00a2015c027c017c027402a1037d01a2017c017402a1037d02a2017c0\\n 27402a1047d017d021800a201a105a20074026a066b02530029014e2907da046a736f6eda05\\n 6c6f616473da07616e64726f6964da066465636f6465da0e5f5f6f6266757363617465645f5\\n fda03686578da075f5f646f635f5f290372020000005a056c6f67696eda0870617373776f72\\n 64a900720c000000fa502f686f6d652f726f6d61696e2f6465762f6f70656e2d6f626675736\\n 361746f722f6368616c6c656e67652f6368616c6c656e67652d30312f736372697074732f65\\n 787472612f636865636b65722e7079da05636865636b0400000073080000000e010a010a011\\n 801720e00000063010000000000000000000000010000000200000043000000730c00000074\\n 007d00840164016b02530029024e72010000002901da036c656e29017202000000720c00000\\n 0720c000000720d000000da067665726966790a00000073020000000c017210000000290672\\n 060000007204000000da03737472da04626f6f6c720e0000007210000000720c000000720c0\\n 00000720c000000720d000000da083c6d6f64756c653e010000007308000000080008011202\\n 1606\\n \u0026#39;) str_attr_accessor(0x7ffffffeff30, 0x0, \u0026#39;__bc__\u0026#39;) str_attr_accessor::=(0x7ffffffeff30, 0x7ffffffefea8) ~str_attr_accessor(0x7ffffffeff30) ~str(0x7ffffffefea8) 0x1c22b0(0x0, \u0026#39;print\u0026#39;, 0x7ffffffefea0) 0x1c2438(0x0, \u0026#39;MvtKNJXCOGJe\u0026#39;, 0x1c32e0) 0x1c316c(0x0, \u0026#39;decode\u0026#39;, 0x1c2640) 0x1c2730(0x0, \u0026#39;hash\u0026#39;, 0x7ffffffefe98) From this, we can tell what the android module looks like:\n__FLAG__ = \u0026quot;f5ca458deb9629a74d4b0c3669deb5078a6a85a90afba9a3c76f5306a4bafb06\u0026quot; __doc__ = \u0026quot;9c16a9c3017d2b3876323bc4f9dad2b7530c\u0026quot; __bc__ = \u0026quot;\\n 700d0d0a000000004aaf626335010000e300000...\u0026quot; print = ? hash = ? MvtKNJXCOGJe = native function at 0x1c32e0 decode = native function at 0x1c2640 Finally, we have JNI_OnLoad. The function is another obfuscated wrapper, which we can fix by setting sp. The real JNI_OnLoad is obfuscated using control-flow flattening and more opaque constants; setting sp fixes the latter issue, and the function is pretty simple so the flattened control flow is not hard to deal with.\nTracing through the function, we see that it grabs a JNIEnv via the helper function at FUN_0026f0d0, sets up a JNINativeMethod structure on the stack and then calls env-\u0026gt;RegisterNatives to register a single function. The function pointer is either FUN_002e71a4 or FUN_002d8428 depending on some thread-local flag (run-time protection again?).\nFUN_002e71a4 is an obfuscated wrapper that calls 2d6288. That calls env-\u0026gt;GetStringUTFChars, py::initialize_interpreter_ and FUN_002bf3a8. This latter function does some more string decryption, ultimately calling pybind11::module_::import(\u0026quot;pyloader\u0026quot;) and accessing the import_checker attribute: this is what kicks off the pyloader.py code we saw earlier.\nPython, again pyloader.py loads a module from __bc__. This module won\u0026rsquo;t decompile properly, so I decided to examine the disassembly (dis.dis(marshal.loads(bytes.fromhex(__bc__.replace(\u0026quot; \u0026quot;, \u0026quot;\u0026quot;).replace(\u0026quot;\\n\u0026quot;, \u0026quot;\u0026quot;))[16:]))):\n1 0 LOAD_CONST 0 (0) 2 LOAD_CONST 1 (None) 4 IMPORT_FROM 0 (android) 6 STORE_NAME 0 (android) 2 8 LOAD_CONST 0 (0) 10 LOAD_CONST 1 (None) 12 IMPORT_FROM 1 (json) 14 STORE_NAME 1 (json) 4 16 LOAD_CONST 2 (\u0026#39;data\u0026#39;) 18 LOAD_NAME 2 (str) 20 LOAD_CONST 3 (\u0026#39;return\u0026#39;) 22 LOAD_NAME 3 (bool) 24 BUILD_TUPLE 4 26 LOAD_CONST 4 (\u0026lt;code object check at 0x109ae1dc0, file \u0026#34;/home/romain/dev/open-obfuscator/challenge/challenge-01/scripts/extra/checker.py\u0026#34;, line 4\u0026gt;) 28 LOAD_CONST 5 (\u0026#39;check\u0026#39;) 30 CALL_FUNCTION 4 32 STORE_NAME 4 (check) 10 34 LOAD_CONST 2 (\u0026#39;data\u0026#39;) 36 LOAD_NAME 2 (str) 38 LOAD_CONST 3 (\u0026#39;return\u0026#39;) 40 LOAD_NAME 3 (bool) 42 BUILD_TUPLE 4 44 LOAD_CONST 6 (\u0026lt;code object verify at 0x109ae16e0, file \u0026#34;/home/romain/dev/open-obfuscator/challenge/challenge-01/scripts/extra/checker.py\u0026#34;, line 10\u0026gt;) 46 LOAD_CONST 7 (\u0026#39;verify\u0026#39;) 48 CALL_FUNCTION 4 50 STORE_NAME 5 (verify) 52 LOAD_CONST 1 (None) 54 RETURN_VALUE Disassembly of \u0026lt;code object check at 0x109ae1dc0, file \u0026#34;/home/romain/dev/open-obfuscator/challenge/challenge-01/scripts/extra/checker.py\u0026#34;, line 4\u0026gt;: 5 0 LOAD_GLOBAL 0 (json) 2 CALL_METHOD 1 4 STORE_FAST 0 (data) 6 LIST_EXTEND 1 8 UNPACK_SEQUENCE 2 10 LOAD_FAST 1 (login) 12 LOAD_FAST 2 (password) 6 14 LOAD_GLOBAL 2 (android) 16 CALL_METHOD 3 18 STORE_FAST 1 (login) 20 LIST_EXTEND 1 22 LOAD_FAST 1 (login) 7 24 LOAD_GLOBAL 2 (android) 26 CALL_METHOD 3 28 STORE_FAST 2 (password) 30 LIST_EXTEND 1 32 LOAD_FAST 2 (password) 8 34 LOAD_GLOBAL 2 (android) 36 CALL_METHOD 4 38 STORE_FAST 1 (login) 40 STORE_FAST 2 (password) 42 BINARY_SUBTRACT 44 LIST_EXTEND 1 46 CALL_METHOD 5 48 LIST_EXTEND 0 50 LOAD_GLOBAL 2 (android) 52 LOAD_ATTR 6 (__doc__) 54 COMPARE_OP 2 (==) 56 RETURN_VALUE Disassembly of \u0026lt;code object verify at 0x109ae16e0, file \u0026#34;/home/romain/dev/open-obfuscator/challenge/challenge-01/scripts/extra/checker.py\u0026#34;, line 10\u0026gt;: 11 0 LOAD_GLOBAL 0 (len) 2 STORE_FAST 0 (data) 4 MAKE_FUNCTION 1 (defaults) 6 LOAD_CONST 1 (0) 8 COMPARE_OP 2 (==) 10 RETURN_VALUE This looks mostly reasonable, but some of the bytecodes look wrong: IMPORT_FROM should be IMPORT_NAME, LOAD_FAST should be STORE_FAST, etc. Indeed, we can guess that we\u0026rsquo;re running on some kind of modified interpreter, where some of the opcodes have been swapped around. This is a popular obfuscation technique, but usually the opcodes are all permuted; swapping just some of the opcodes is a sneaky trick!\nWe can guess that the real code looks like this:\nimport android import json def check(data: str) -\u0026gt; bool: login, password = json.loads(data) login = android.decode(login) password = android.decode(password) return android.__obfuscated__(login + password).hex() == android.__doc__ def verify(data: str) -\u0026gt; bool: return len(data) == 0 __obfuscated__ is not in the android module. However, I found this string in the python.elf binary I compiled from libpython3.10.a: it\u0026rsquo;s referenced in _PyEval_EvalFrameDefault:\niVar6 = _PyUnicode_EqualToASCIIString((PyObject *)pPVar58,\u0026#34;__obfuscated__\u0026#34;); if (iVar6 != 0) { pPVar58 = (PyTypeObject *)PyUnicode_FromString(\u0026#34;MvtKNJXCOGJe\u0026#34;); (pPVar58-\u0026gt;ob_base).ob_base.ob_refcnt = (pPVar58-\u0026gt;ob_base).ob_base.ob_refcnt + 1; } So, the interpreter has also been hacked to replace mentions of __obfuscated__ with MvtKNJXCOGJe, which is in the android module.\nNative code, again Finally, we need to understand the decode (0x2c2640) and MvtKNJXCOGJe (0x2c32e0) functions. decode is straightforward base64 decoding with no obfuscation. MvtKNJXCOGJe, on the other hand, is heavily obfuscated: it\u0026rsquo;s a wrapper for FUN_002c0ea8, which is a fairly long control-flow-flattened function. By following each of the state labels, it\u0026rsquo;s easy enough to reconstruct the overall flow of the function.\nIt starts off by putting a bunch of stuff on the stack; a bit of emulation reveals the data:\nuc.reg_write(UC_ARM64_REG_SP, STACK_TOP) uc.reg_write(UC_ARM64_REG_LR, END_ADDR) uc.mem_write(STACK_TOP - 0x1000, b\u0026#34;\\x00\u0026#34; * 0x1000) uc.emu_start(0x1c0ea8, 0x1c0ef0) # initialize stack and persistent vars uc.emu_start(0x1c10ec, 0x1c1990) print(uc.mem_read(STACK_TOP - 0xc0, 0x40)) This produces bytearray(b'expand 32-byte ke\\x84tp~\\xca//b\\x92fu~\\x93atb\\x82.rh\\xdf\\x00\\x00\\r\\xf0\\x00\\x00\\r\\xf0\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00'). We can recognize this as the initial state of a ChaCha cipher with a counter and nonce of 0. Indeed, examining some of the functions that are called confirms this suspicion. FUN_0030ca2c in particular performs one double-round of the cipher:\nvoid FUN_0030ca2c(uint *param_1) { FUN_0030c698(param_1,0,4,8,0xc); FUN_0030c698(param_1,1,5,9,0xd); FUN_0030c698(param_1,2,6,10,0xe); FUN_0030c698(param_1,3,7,0xb,0xf); FUN_0030c698(param_1,0,5,10,0xf); FUN_0030c698(param_1,1,6,0xb,0xc); FUN_0030c698(param_1,2,7,8,0xd); FUN_0030c698(param_1,3,4,9,0xe); return; } After encrypting the input with ChaCha, the MvtKNJXCOGJe function does the following:\nuRam00007fff000cfe98 = 0; while(1) { uRam00007fff000cfde8 = uRam00007fff000cfe98; p2_len = string::size((basic_string *)\u0026amp;obf_output); if (p2_len \u0026lt;= uRam00007fff000cfde8) { FUN_002bfc08(uVar12,\u0026amp;obf_output); FUN_0051e7d0(\u0026amp;obf_output); lVar6 = tpidr_el0; if (*(long *)(lVar6 + 0x28) == lRam00007fff000cffd8) { return; } // WARNING: Subroutine does not return __stack_chk_fail(); } pbVar13 = (byte *)string::operator[](\u0026amp;obf_output,uRam00007fff000cfe98); /* obfuscated math elided... */ iRam00007fff000cfde4 = (uint)bVar4 * 17; iVar2 = 256; iVar5 = 0; if (iVar2 != 0) { iVar5 = iRam00007fff000cfde4 / iVar2; } iRam00007fff000cfde4 -= iVar5 * iVar2; puVar14 = (undefined *)string::operator[](puVar7,uRam00007fff000cfe98); *puVar14 = (char)iRam00007fff000cfde4; uRam00007fff000cfe98 += 1; } So, this multiplies each byte with 17, mod 256. This all is easy enough to invert:\ndata = bytes.fromhex(\u0026#39;9c16a9c3017d2b3876323bc4f9dad2b7530c\u0026#39;) inv17 = pow(17, -1, 256) data = bytes([(c * inv17) % 256 for c in data]) from Crypto.Cipher import ChaCha20 key = b\u0026#34;e\\x84tp~\\xca//b\\x92fu~\\x93atb\\x82.rh\\xdf\\x00\\x00\\r\\xf0\\x00\\x00\\r\\xf0\\x00\\x00\u0026#34; cipher = ChaCha20.new(key=key, nonce=b\u0026#39;\\x00\u0026#39; * 8) print(cipher.decrypt(data)) And we get our final answer, 0MvLL_And_dPr0t3ct. (Note that the key is simply https://obfuscator.re/ NUL-padded to 32 bytes and XORed with the repeating key \\x0d\\xf0\\x00\\x00).\nSummary of the program flow Obfuscated Java/Kotlin code JSON-encodes [base64(login), base64(password)] and passes it to native code Obfuscated native code loads a hacked Python interpreter and an extension module android using pybind11 Lightly obfuscated Python module loads the JSON and calls android.MvtKNJXCOGJe(login + password) android.MvtKNJXCOGJe uses ChaCha20 to encrypt the input, then multiplies each byte by 17 The final result is compared against an obfuscated string constant. Summary of obfuscation techniques dProtect: very little program logic was in Java, so this had minimal impact. In particular, the package name re.obfuscator.challenge01 was not obfuscated, nor were crucial functions like toJson. run-time checks: not applicable, static reversing only ELF protections: easily defeated (rebuild_elf_sections.py plus simply ignoring garbage symbols) arithmetic obfuscation: quite annoying in general, but fairly predictable as all encodings were applied the same number of times, making the overall \u0026ldquo;shape\u0026rdquo; of each obfuscated operation discernable. opaque constants: largely not an issue by setting SP at the start of each function opaque field access: as it depends on opaque constants, not an issue control-flow breaking: as it depends on opaque constants, not an issue control-flow flattening: definitely annoying; encryption of the variable was not impactful (Ghidra automatically constant-folds) but recovering control flow was done manually in most cases. (Tools exist, but the functions were not sizable enough to warrant using the tools) string encoding: surprisingly annoying, especially when written to the stack; the use of obfuscated arithmetic + randomized encoding algorithm meant that emulation was often the fastest way to recover strings ","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1693126527,"objectID":"12cf4cb20b9e38655ef1ec5e811245f5","permalink":"https://obfuscator.re/challenges/2022-12-android-challenge/writeups/2023_04_28-nneonneo/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/challenges/2022-12-android-challenge/writeups/2023_04_28-nneonneo/","section":"challenges","summary":"Writeup for obfuscator.re Challenge 1 Author: Robert Xiao nneonneo@gmail.com\nWe\u0026rsquo;re provided an APK file, and told that it has been protected by O-MVLL (native code), dProtect (dex code), ELF format modifications, and runtime application self protection (RASP). Let\u0026rsquo;s dive in!\nOur first stop is to unzip the APK. There\u0026rsquo;s a single native library, lib/arm64-v8a/liba1re03.so weighing in at 27MB, along with bits of Kotlin data suggesting that parts of the app are written in Kotlin.\n","tags":null,"title":"Writeup by Robert Bo Xiao","type":"writeups"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"8a97f3f9fe3d342e3497a9f33590d3c9","permalink":"https://obfuscator.re/resources/yansollvm/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/yansollvm/","section":"resources","summary":"","tags":null,"title":"YANSOllvm","type":"resources"},{"authors":null,"categories":null,"content":"","date":-62135596800,"expirydate":-62135596800,"kind":"page","lang":"en","lastmod":1664824759,"objectID":"3259754c28d45dd96e648f55f63b8a12","permalink":"https://obfuscator.re/resources/zshield/","publishdate":"0001-01-01T00:00:00Z","relpermalink":"/resources/zshield/","section":"resources","summary":"","tags":null,"title":"zShield","type":"resources"}]