# Romain Thomas > Software obfuscation, reverse engineering, program analysis, and open-source binary tooling by Romain Thomas. This file contains the full text of every published page on https://www.romainthomas.fr/ concatenated as Markdown for one-shot ingestion by language models. - Compact link map: [llms.txt](https://www.romainthomas.fr/llms.txt) - Anchored, hash-addressed retrieval chunks: [index.chunks.json](https://www.romainthomas.fr/index.chunks.json) - Relative links inside a document resolve against that document's canonical URL. --- # A Glimpse Into DexProtector - Canonical: https://www.romainthomas.fr/post/26-01-dexprotector/ - Markdown: https://www.romainthomas.fr/post/26-01-dexprotector/index.md - Section: post - Published: 2026-01-04 - Modified: 2026-08-04 - Tags: android, reverse engineering, obfuscation, dexprotector, bypass > This blog post provides a high-level overview of DexProtector's security features and their limitations ## Introduction [DexProtector](https://licelus.com/products/dexprotector) is a comprehensive security solution providing a complete set of features to protect mobile apps (Android/iOS) against different threats including reverse engineering and malware. Its core capabilities include: - Obfuscation, Encryption, and Virtualization - RASP (Runtime Application Self-Protection) - Anti-Tampering and Integrity Control This protector renewed my interest when I noticed that [Revolut][com.revolut.revolut] is using this solution to protect their apps. Interestingly, I also found that the solution was used by [Live Net TV](./img/livenet.png), a dubious IPTV application. This post synthesizes my findings from a deep dive into DexProtector. You can download the original LiveNet APK for reference here: [com.playnet.androidtv.ads.5.0.1.apk](./assets/com.playnet.androidtv.ads.5.0.1.apk)[^checksum] ## Bootstrap DexProtector uses a complex loading chain designed to hinder static/dynamic analysis and memory dumping. It all starts with a custom class named `Protected` which is injected in the main package of the application and referenced in the `AndroidManifest.xml`: ```xml ``` This class is involved in various stages of DexProtector but first, it is used to load a native library: `libdpboot.so`: ```java package com.playnet.androidtv; public class ProtectedLiveNetTV extends Application { @Override protected void attachBaseContext(Context context) { super.attachBaseContext(context); try { DeFcpynjg(); // Basic integrity check System.loadLibrary("dpboot"); oagfhBoAe(); // Load libdexprotector.so (or libdexprotector_h.so) } catch (Throwable th) { ProtectedLiveNetTV$R$id.EfxsfkH(this, th); } } } ``` `libdpboot.so` serves multiple purposes, one of which is loading `libdexprotector.so`. `libdexprotector.so` is loaded by a Java native function (named `oagfhBoAe` in the previous example) that uses the JNI to call `System.loadLibrary("dexprotector")`. `libdexprotector.so` is a custom ELF loader[^liblinker] that is responsible for decrypting and mapping the final protected payload into memory. This protected payload is embedded within the library itself: ![Diagram of libdexprotector.so containing an embedded DPLF packed-library payload.](img/libdexprotector.webp) In some versions of DexProtector, the beginning of the packed library can be identified by looking for the magic bytes: `DPLF`: ```hexdump 0000fac0 44 50 4c 46 c0 b1 f2 ea e1 c6 0d 5b 45 6e fd e5 DPLF.......[En.. 0000fad0 86 f2 2e c5 46 82 66 44 e7 68 b4 e1 5b 87 36 9e ....F.fD.h..[.6. 0000fae0 09 54 ef b4 17 94 94 71 46 88 8d 47 c4 ee ba a7 .T.....qF..G.... 0000faf0 e7 aa da c0 55 32 4b b3 8c 1f 09 db fc a6 04 fd ....U2K......... 0000fb00 0e 22 04 8c d6 11 05 18 fb 93 3b 27 32 ca 97 e6 ."........;'2... 0000fb10 b2 9b 7b 87 ed 35 64 32 aa 8b 0e ee ca 1c 02 7b ..{..5d2.......{ 0000fb20 56 e9 8f c7 1e dd e1 58 4d 9b d9 ca cd 5f 38 f1 V......XM...._8. ``` In other versions, the payload is located in the last `PT_LOAD` segment: ```bash -> revolut-10-109 git:(main) ✗ readelf -lW ./libdexprotector.so Elf file type is DYN (Shared object file) Entry point 0x0 There are 8 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000040 0x0000000000000040 0x0000000000000040 0x0001c0 0x0001c0 R 0x8 LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x0026bc 0x0026bc R E 0x4000 LOAD 0x0026c0 0x00000000000066c0 0x00000000000066c0 0x0000f8 0x0000f8 RW 0x4000 LOAD 0x0027b8 0x000000000000a7b8 0x000000000000a7b8 0x000a70 0x000a80 RW 0x4000 DYNAMIC 0x0026c8 0x00000000000066c8 0x00000000000066c8 0x0000f0 0x0000f0 RW 0x8 GNU_RELRO 0x0026c0 0x00000000000066c0 0x00000000000066c0 0x0000f8 0x001940 R 0x1 GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x0 LOAD 0x003630 0x000000000000f630 0x000000000000f630 0x057535 0x057535 RW 0x4000 ^ | +------------ Packed library ``` The most clever aspect of `libdexprotector.so` is how it derives the 32-byte key that is used to decrypt the payload. It uses a static salt located in its library but it also uses the **runtime state** of the system linker. The key is partially derived from the assembly code of the linker function `rtld_db_dlactivity()`. By default, `rtld_db_dlactivity()` is an empty function (i.e. a `ret`). However, when `frida-server` is used, it hooks this function by injecting a "trampoline" It is worth mentioning that this trampoline is persistent even if `frida-server` is no longer running. This means that if `frida-server` runs at least **once**, the key will be corrupted by the **persistent** trampoline. Consequently, the second stage won't be executed ![DexProtector key derivation from linker code: a Frida trampoline corrupts the key, while the unmodified linker decrypts the payload.](img/libdexprotector-2.webp) Given the correct computed key, `libdexprotector.so` decrypts the beginning of the payload, which starts with a header followed by ELF-like segments describing the content to be mapped into memory. ![DexProtector custom DP header and segment table mapping protected segments into libdp.so.](img/libdexprotector-3.webp) The unpacked library was originally named `libdp.so`. It is worth mentioning that neither the packed nor the unpacked library contains the original ELF header. Instead, `libdexprotector.so` acts as a custom ELF loader that relies on its own custom header rather than using the official `Elf64_Ehdr` structure. Similarly, the segments table uses a custom structure to represent the segments that need to be mapped in memory. When `libdexprotector.so` has finished mapping the protected-packed library, it jumps to the function referenced in the `DT_FINI_ARRAY` entry of the protected library. > **Note** During the loading phase, `libdexprotector.so` clears the different regions referenced in the dynamic table. For instance, the relocations table referenced in the `DT_ANDROID_RELA` entry is cleared with zeros once `libdexprotector.so` has processed the relocations. This means that if attackers try to dump the protected library after it has fully loaded, they will miss critical information from the dynamic table. ## `libdp.so` The protected library loaded through `libdexprotector.so` is a key component to understand most of the DexProtector's security features. It contains the RASP detections, the engine to load encrypted classes, the logic to load protected `assets/` etc. It's a masterpiece of engineering and different detections are very juicy. From a cryptography perspective, it uses various algorithms and everything is implemented following standards and good practices. In addition, DexProtector uses a highly context-sensitive approach to generate and derive key material. ## Key Derivation One of the purposes of `libdp.so` is to generate a 32-byte master key. This key is critical, as it is used to derive the subkeys necessary for various security features, such as asset decryption. To ensure integrity, the master key is generated using specific elements that create a strong cryptographic binding to the host application. These elements typically include: - The APK signature - Unprotected DEX files - The DexProtector configuration (embedded within `libdp.so`) Because of this binding, even minimal static or dynamic modifications to the APK will result in a corrupted master key, preventing the application from executing correctly. The key derivation process also uses the content of `libdp.so` to derive or corrupt the key. This acts as an anti-tampering measure: if an attacker attempts to hook or instrument functions within `libdp.so`, the resulting key will be invalid. ![Master-key derivation binding protected DEX bytecode, resource files, and protection configuration to libdp.so.](img/libdexprotector-4.webp) In theory, this design is robust. However, while it was challenging, I managed to develop a workaround to instrument and hook `libdp.so` without triggering these corruption mechanisms. Ultimately, I was able to generate the valid master key without executing the protected applications (e.g., Revolut, Kaspersky). With this master key, it is straightforward to derive the subkeys required to decrypt assets and access DexProtector's proprietary files, such as: - `se.dat` - `resources.dat` - `mm.dat` - `dp.mp3` - `classes.dex.dat` - `ic.dat` - `ct.dat` - `rcdb.dat` ## Class Encryption One of the major features provided by DexProtector is the ability to encrypt classes. As detailed in the official documentation[^dexpro-config], this is configured by defining the target classes or packages within the `` tag: ```xml glob:com/mypackage/** ``` Internally, DexProtector protects all `classes.dex` files that match the classes or packages defined in the configuration. For instance, protecting the packages `com/mypackage` and `com/iptv` may require DexProtector to protect the entire `classes.dex` and `classes2.dex`. The protected DEX files are bundled into a single file located in `assets/classes.dex.dat`. This file contains the encrypted and compressed DEX data, along with a header located at the end of the file. At runtime, the protection works by decrypting and decompressing the given DEX files and then using internal Android APIs to dynamically load the clear DEX files from memory. ![Class-encryption flow compressing selected DEX files into classes.dex.dat and restoring them at runtime.](img/libdexprotector-5.webp) Note that DexProtector implements an anti-dump mechanism to prevent an attacker from extracting the clear DEX file from memory. This mechanism works by unmapping[^munmap-area] unused regions of the in-memory DEX files. For instance, consider that the plain `classes.dex` is mapped in the memory region `[0x60000, 0x70000]` and that DexProtector unmaps the unused region `[0x64000, 0x68000]`. If an attacker tries to dump the whole range `[0x60000, 0x70000]`, it will trigger a `SEGV_MAPERR` because the region `[0x64000, 0x68000]` is unmapped. Nevertheless, this protection can be defeated to access the "unprotected" DEX files: **`com.playnet.androidtv.ads - assets/classes.dex.dat`** - [classes0.decrypted.dex](./assets/classes0.decrypted.dex) - [classes1.decrypted.dex](./assets/classes1.decrypted.dex) - [classes2.decrypted.dex](./assets/classes2.decrypted.dex) - [classes3.decrypted.dex](./assets/classes3.decrypted.dex) When we open these unprotected DEX files, we notice that some classes exhibit obfuscated code: ```java package com.playnet.androidtv; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; public class BootReceiver extends BroadcastReceiver { @Override // android.content.BroadcastReceiver public void onReceive(Context context, Intent intent) { Object objI; try { Object objI2 = LibLiveNetTV.i(1263, intent); if (objI2 == null || !LibLiveNetTV.i(0, objI2, ProtectedLiveNetTV.s("\u5a7d")) || !LibLiveNetTV.i(440, LibLiveNetTV.i(666, context), LibLiveNetTV.i(3238, context, 2131951803), false) || (objI = LibLiveNetTV.i(567, LibLiveNetTV.i(2489, context), LibLiveNetTV.i(1465, context))) == null) { return; } LibLiveNetTV.i(904, objI, 268435456); LibLiveNetTV.i(1054, context, objI); } catch (Exception e) { LibLiveNetTV.i(69, e); } } } ``` This output demonstrates the presence of two additional security layers: string encryption and indirect method/field access (invocation hiding). ## String Encryption As described in the official documentation[^dexpro-config], developers can protect sensitive strings by applying the `` tag in their configuration: ```xml glob:!**/** glob:com/test/** ``` From an implementation perspective, this protection works by replacing sensitive strings with calls to a native function. This function accepts an encoded index (passed as a string) to retrieve the original string. Consider the following example: ```java public class BootReceiver extends BroadcastReceiver { @Override // android.content.BroadcastReceiver public void onReceive(Context context, Intent intent) { // ... String clear = ProtectedLiveNetTV.s("\u5a7d"); // ... } } ``` In this example, the native function is `ProtectedLiveNetTV.s`, and the index is `0x5a7d` (represented by the character `\u5a7d`). The native function `ProtectedLiveNetTV.s(String enc)` is implemented within the library `libdp.so` and the decryption process operates as follows: - **Lookup**: The function uses the external `assets/se.dat` file to convert the input index (`0x5a7d`) into a file offset. - **Retrieval**: This offset points to the specific location of `se.dat`. - **Decryption**: `ProtectedLiveNetTV.s` decrypts the data found at that offset and returns the plain-text string using a standard cryptography algorithm and a custom one. The algorithm used to decrypt the strings relies on a specific key and a nonce constructed using a combination of: 1. The string index (e.g., `0x5a7d`). 2. The hash code of the calling class (e.g., `com.playnet.androidtv.BootReceiver`). ![se.dat layout with a string-encryption header, offsets table, and encrypted string records.](img/libdexprotector-6.webp)
After that, we get the clear string `android.intent.action.BOOT_COMPLETED`. ![ProtectedLiveNetTV.s resolving encrypted string index 0x5a7d to android.intent.action.BOOT_COMPLETED.](img/libdexprotector-8.webp)
> **Note** DexProtector adds an additional layer of security by binding the decryption logic to the memory address of the native function itself. The internal crypto context used for decryption is masked using the address of `ProtectedLiveNetTV.s`. This acts as an integrity check: if an attacker attempts replace the function during `env->RegisterNative`, the memory address will not match. Consequently, the unmasking process will fail, the crypto context will be corrupted, and the string will not decrypt correctly. ## Method & Field Access Protection The second layer of protection focuses on obfuscating method calls and field access. This process involves transforming these operations into native invocations. ```diff - context.getPackageName() + LibLiveNetTV.i(1465, context) ``` Similar to string encryption, developers can use the `` tag to apply this protection to specific packages and classes defined in the filters: ```xml glob:!**/** glob:com/test/** ``` When an instruction requires protection, DexProtector replaces it with a call to a native bridge function (e.g., `LibLiveNetTV.i(...)`). This function accepts an index as the first parameter, followed by any arguments required by the original method or field. This index is used to resolve the targeted method or field thanks to the asset file `assets/dp.mp3`. This file is decrypted and decompressed during the DexProtector's initialization routine and it contains the information to make the relationship between indexes and the hidden methods or fields. ![dp.mp3 hidden-access file layout with its header, elements array, classes array, and strings pool.](img/libdexprotector-7.webp)
The layout of the data file is divided into four distinct sections: **Header** Contains integrity hashes and the number of elements in the subsequent sections. **Elements Array** An array of structures describing the hidden methods and fields. Each element contains references to: - The name (e.g., `getPackageName`) - The signature (e.g., `()Ljava/lang/String;`) - The defining class (e.g., `android/content/Context`) **Classes Array** An array listing the class names that own the elements in the previous section. Note that this is not an array of strings, but an array of integers serving as references into the Strings Pool. **Strings Pool** A collection of all string literals referenced by the previous sections. Using the previous example, `LibLiveNetTV.i(1465, context)`: 1. The native function `LibLiveNetTV.i` whose implementation is located in `libdp.so` takes the index `1465`. 2. This number is used as an index into the *Elements Array* of `dp.mp3` 3. It resolves the mapping to: `android/content/Context.getPackageName() - ()Ljava/lang/String;` ![LibLiveNetTV.i resolving index 1465 to Context.getPackageName and invoking it through JNI.](img/libdexprotector-9.webp)
Then, it executes the function via the JNI: ```cpp jclass clazz = env->FindClass("android/content/Context"); jmethodID mid = env->GetMethodID(clazz, "getPackageName", "()Ljava/lang/String;"); return env->CallObjectMethod(context, mid); ``` ## Recovery Based on our understanding of the string encryption and hidden access mechanisms, we can now strip the protections from the different DEX files using [Redex](https://github.com/facebook/redex). Redex is a DEX bytecode optimizer that provides a reliable framework for reading, writing, and analyzing `.dex` files. It also offers facilities to orchestrate and configure passes and perform both type inference and abstract interpretation. These features make it the ideal tool to strip these protections. To achieve this, we create two custom passes, one targeting each protection mechanism: ```json {hl_lines=[4,5]} { "redex" : { "passes" : [ "StringEncryption", "RecoverHiddenAccess", "PeepholePass", "ConstantPropagationPass", "ResultPropagationPass", "RegAllocPass", "CopyPropagationPass", "LocalDcePass", "ReduceGotosPass" ] }, "RecoverHiddenAccess": { "info": "/home/romain/research/dexprotector/livenet/dp.mp3" }, "StringEncryption": { "se_dat_file": "/home/romain/research/dexprotector/livenet/se.dat.clear" }, } ``` These passes work by identifying calls to the obfuscation wrappers, specifically `ProtectedLiveNetTV.s()` or `LibLiveNetTV.i()`. The system then replaces these calls with the recovered data: 1. Strings are restored using the `se.dat` file. 2. Methods/Fields are restored using the `dp.mp3` file. The output is an unprotected DEX file. ![Protected DexProtector wrapper code beside the Redex-deobfuscated implementation.](img/libdexprotector-10.webp)
To verify the effectiveness of the Redex approach, you can compare the files below: - **Before Redex:** [classes2.decrypted.dex](./assets/classes2.decrypted.dex) - **After Redex:** [classes2.unprotected.dex](./assets/classes2.unprotected.dex) This Redex-based deobfuscation approach has been successfully tested on other applications secured by DexProtector (examples below). ![Comparison: before](./diff/revolut/A.svg) ![Comparison: after](./diff/revolut/B.svg)

![Comparison: before](./diff/envchecks/A.svg) ![Comparison: after](./diff/envchecks/B.svg)

![Comparison: before](./diff/appcloner/A.svg) ![Comparison: after](./diff/appcloner/B.svg)

![Comparison: before](./diff/flashget/B.svg) ![Comparison: after](./diff/flashget/A.svg) > **Note** It is worth mentioning that this app, which has been downloaded over 10 million times, uses weak `DES/ECB-MD5` cipher suite along with clear and **explicit** `http://` communications. (c.f., `network-security-config.xml`) ## Assets Protections Sensitive application data is often stored within files attached to the APK/XAPK. These assets can include certificates, images, Machine Learning models, or serialized keystores. DexProtector provides a means to protect these embedded resources. According to the documentation[^dexpro-config], asset protection can be configured using the following structure: ```xml glob:cert/** glob:raw/** glob:fonts/** my_api_key glob:mobile_token* glob:payments_** glob:sensitive_strings_arrays_etc* ``` To demonstrate this protection, I will analyze the application [com.dexprotector.detector.envchecks](https://play.google.com/store/apps/details?id=com.dexprotector.detector.envchecks). The `.xapk` can be downloaded here: [`com.dexprotector.detector.envchecks.2.1.xapk`](assets/com.dexprotector.detector.envchecks.2.1.xapk). > **Note** The assets protected by LiveNet (`zpoasosdi.dat, regtbeonuev.dat, and btylusqrepu.dat`) are serialized BouncyCastle keystores used to authenticate the application on the IPTV backend. Due to the sensitive nature of this identification, I took a different application to illustrate how this protection mechanism works. This application contains a file named `assets/chinook.db`. While the extension suggests it is a database, the file is protected and the hexdump reveals high entropy data rather than a standard file header. ```hexdump 00000000 7c 96 af 76 c2 8b 88 b5 18 e6 d7 12 d1 8d f1 a5 |...v............| 00000010 00 80 0d 00 cc 6f ce 95 30 3d 50 61 05 cd 8e 5f |.....o..0=Pa..._| 00000020 2a 55 ae 81 85 32 24 53 cb 11 c6 a1 f1 f7 bd 56 |*U...2$S.......V| 00000030 bc 1a 67 0e 1e b5 fc 60 3c 20 6a 08 dc f1 d2 7f |..g....`< j.....| 00000040 8e f8 7a 5b 89 14 2e 37 fc 4b 5e f9 db d9 e2 f5 |..z[...7.K^.....| 00000050 6c e4 be 83 2b 18 2e 22 00 b4 1a f1 6b d4 3c 86 |l...+.."....k.<.| 00000060 78 0a f6 0e 5c 39 fd 2b 5a b1 33 e4 6f 19 23 49 |x...\9.+Z.3.o.#I| ``` When DexProtector runs its initialization routine via `libdp.so`, it modifies the vtable of the internal class related to assets processing which is located in `libandroidfw.so`. The modifications of the vtable are not trivial but the main idea is to intercept all the virtual calls from `android::_FileAsset::*`. This interception occurs whenever the application attempts to access asset files using: - The Java API: `AssetManager.open()` - The Native API: `AAssetManager_open()` When DexProtector intercepts these calls, it decrypts and potentially uncompress the underlying file on-the-fly, providing the clear content to the application. The key and nonce required to decrypt the file are distributed across different elements, including the file header and a subkey derived from a master key. By recovering these elements, it is possible to decrypt the asset manually and reveal the original content. ```hexdump 00000000 53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00 |SQLite format 3.| 00000010 04 00 01 01 00 40 20 20 00 00 00 19 00 00 03 60 |.....@ .......`| 00000020 00 00 00 00 00 00 00 00 00 00 00 22 00 00 00 01 |..........."....| 00000030 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 |................| 00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 19 |................| 00000060 00 2d e2 1e 05 00 00 00 07 03 dd 00 00 00 00 19 |.-..............| ``` You can find the encrypted and decrypted files here: - [`chinook.db`](./assets/chinook.db) - [`chinook.decrypted.db`](./assets/chinook.decrypted.db) > **Note** This file is actually not sensitive and it taken from https://github.com/lerocha/chinook-database The other mechanisms used by DexProtector to protect resources under the tags `, ` are similar but less sophisticated. They consist of hooking internal Android API like `android.content.res.StringBlock.{nativeGetString, nativeGetResourceStringArray}` and `android/content/res/AssetManager.nativeGetResourceIdentifier` to decrypt the protected content on-the-fly. ## RASP DexProtector uses state-of-the-art RASP mechanisms that secure both its core and the application against tampering. For instance, it bypasses the standard `PackageManager` API in favor of raw Binder communication to detect installed root-related packages (such as `com.zachspong.temprootremovejb`). Developers can enable these protections using the following configuration: ```xml true true true true ``` When DexProtector flags a threat (such as hooking), it typically records the detection and defers its reaction to a later point in the execution flow. However, if a threat occurs very early during startup, it may trigger immediate countermeasures, such as corrupting the master key or terminating the application. Despite these measures, these detections are susceptible to bypass and reverse engineering in a quasi-systematic way: ![EnvChecks reports all RASP flags false on a Magisk-rooted device with no bypass module installed.](img/libdexprotector-11.webp) ## Conclusion DexProtector provides a post-build, no-code solution requiring minimal configuration by developers to protect their mobile applications. While this approach is appealing, it introduces a generic design that weakens the solution: successfully reverse engineering one instance of DexProtector enables a scalable attack on all applications protected by this tool (see [Annexes](#annexes)). Although DexProtector uses a highly context-sensitive approach to derive cryptographic material, this is insufficient to prevent key recovery and access protected assets. DexProtector remains a good solution for protecting assets and IP but its limitations must be weighed against the sensitivity of the content being secured. You can find additional material in this repo: [ romainthomas/dexprotector](https://github.com/romainthomas/dexprotector) *These different weaknesses were shared with Licel ahead of time.* ### Annexes List of applications successfully unprotected: | App | Version | |------------------------------------------------------------------------------|------------------| | [`com.revolut.revolut`][com.revolut.revolut] | `10.109.1` | | [`istark.vpn.starkreloaded`][istark.vpn.starkreloaded] | `7.1-rc` | | [`com.dexprotector.detector.envchecks`][com.dexprotector.detector.envchecks] | `2.1` | | [`ar.tvplayer.tv`][ar.tvplayer.tv] | `5.2.0` | | [`org.unhcr.zakat`][org.unhcr.zakat] | `2.1.54` | | [`com.Hyatt.hyt`][com.Hyatt.hyt] | `6.16.0` | | [`com.kms.free`][com.kms.free] | `11.129.4.14969` | | [`com.flashget.parentalcontrol`][com.flashget.parentalcontrol] | `1.3.6.0` | | [`com.belongtail.ai`][com.belongtail.ai] | `2.8.4` | | [`com.kidoprotect.app`][com.kidoprotect.app] | `11.1` | [com.revolut.revolut]: https://play.google.com/store/apps/details?id=com.revolut.revolut [istark.vpn.starkreloaded]: https://play.google.com/store/apps/details?id=istark.vpn.starkreloaded [com.dexprotector.detector.envchecks]: https://play.google.com/store/apps/details?id=com.dexprotector.detector.envchecks [ar.tvplayer.tv]: https://play.google.com/store/apps/details?id=ar.tvplayer.tv [com.gss.crocodile]: https://cafebazaar.ir/app/com.gss.crocodile [org.unhcr.zakat]: https://play.google.com/store/apps/details?id=org.unhcr.zakat [com.Hyatt.hyt]: https://play.google.com/store/apps/details?id=com.Hyatt.hyt [com.flashget.parentalcontrol]: https://play.google.com/store/apps/details?id=com.flashget.parentalcontrol [com.belongtail.ai]: https://play.google.com/store/apps/details?id=com.belongtail.ai [com.kms.free]: https://support.kaspersky.com/common/beforeinstall/16085 [com.kidoprotect.app]: https://play.google.com/store/apps/details?id=com.kidoprotect.app [^dt_so_name]: Name given from the `DT_SONAME` [^dexpro-config]: https://licelus.com/products/dexprotector/docs/android/configuring-dexprotector [^munmap-area]: These regions are described in the header located at the end of the packaged dex files (`classes.dex.dat`). [^unprotected]: In this context, unprotected means: computing the master key, accessing the protected DEX files, recovering strings, methods, and fields and, accessing protected assets if any. [^checksum]: sha256: `810634a3757a9ab1bfc37fb7a48fa7928fe917befd9ef0619f65eeb88173ad4a` [^liblinker]: Its original name is `liblinker.so` --- # Fuzzing Windows ARM64 closed-source binary - Canonical: https://www.romainthomas.fr/post/25-04-windows-arm64-qbdi-fuzzing/ - Markdown: https://www.romainthomas.fr/post/25-04-windows-arm64-qbdi-fuzzing/index.md - Section: post - Published: 2025-04-28 - Modified: 2026-08-04 - Tags: windows, arm64 > This blog post introduces coverage-guided fuzzing with QBDI and libFuzzer targeting Windows ARM64. ## Introduction Coverage-guided fuzzing is a well-known technique that improves the efficiency of a fuzzer by providing runtime feedback This blog post explores this concept on Windows ARM64, using QBDI for code instrumentation and LLVM's libFuzzer as the fuzzing engine. In addition to the fact that QBDI is based on LLVM and libFuzzer is under the LLVM umbrella, the support for Windows ARM64 (`arm64-pc-windows-msvc`) in LLVM is sufficient for cross-compiling executables and libraries from Linux. All the LLVM components mentioned in this blog post are based on LLVM 20.1.3 (2025-04-16) ## libFuzzer 101 First, let's consider that we have access to the source code of the function that we want to fuzz: ```cpp int fuzzme(const uint8_t *data, size_t size) { if (size > 0 && data[0] == 'Q') { if (size > 1 && data[1] == 'B') { if (size > 2 && data[2] == 'D') { if (size > 3 && data[3] == 'I') { if (size > 4 && data[4] == '!') { __builtin_trap(); } } } } } return 0; } extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { return fuzzme(data, size); } ``` As mentioned in the documentation of libFuzzer[^doc], we can run clang on this source file to leverage built-in coverage instrumentation: ```bash $ clang++ --target=arm64-pc-windows-msvc -fsanitize=fuzzer -c fuzzme.cpp -o fuzzme.obj ``` When compiling `fuzzme.cpp` with the `-fsanitize=fuzzer` flag, Clang instruments the code at sensitive locations to enhance fuzzer efficiency. In this context, these sensitive locations are: - Comparisons (e.g. `data[3] == 'I'`) - Basic block edges You can find more about the LLVM code coverage instrumentation built-in on the [SanitizerCoverage](https://clang.llvm.org/docs/SanitizerCoverage.html) page. When we open `fuzzme.obj` in Binary Ninja, we get the following representation: ![Binary Ninja control-flow graph of fuzzme.obj with comparison-feedback hooks and edge-coverage counters highlighted.](./img/llvm-instru.webp) - `__sanitizer_cov_trace_const_cmp{4,8}` is injected before comparisons - Edge coverage is done by incrementing a bitmap. For more insight about how a bitmap is used by a fuzzer, I recommend this presentation by P. - batcido - Hernault : [Fuzzing binaries using Dynamic Instrumentation](https://project.inria.fr/FranceJapanICST/files/2019/04/19-Kyoto-Fuzzing_Binaries_using_Dynamic_Instrumentation.pdf) The hidden instrumentation produced by Clang with the `-fsanitize=fuzzer` flag can also be achieved by manually modifying the source code: ```cpp {hl_lines=[25,26,28,29,31,32,34,35,37,38,"41-45"]} extern "C" void __sanitizer_cov_trace_const_cmp8(uint64_t Arg1, uint64_t Arg2); extern "C" void __sanitizer_cov_trace_const_cmp4(uint32_t Arg1, uint32_t Arg2); extern "C" void __sanitizer_cov_8bit_counters_init(uint8_t *Start, uint8_t *Stop); static std::array BITMAP = {}; // For the *NIX folks, this code is equivalent to // __attribute__((constructor)) void ctor() { // __sanitizer_cov_8bit_counters_init(BITMAP.data(), BITMAP.data() + BITMAP.size()) // } // but since __attribute__((constructor)) is not available and its equivalent is // painfull to write, we allocate a static class whose constructor init the bitmap class InitBitMap { public: InitBitMap() { BITMAP.fill(0); __sanitizer_cov_8bit_counters_init(BITMAP.data(), BITMAP.data() + BITMAP.size()); } }; static InitBitMap _; int fuzzme(const uint8_t *data, size_t size) { __sanitizer_cov_trace_const_cmp8(size, 0); __sanitizer_cov_trace_const_cmp4(data[0], 'Q'); if (size > 0 && data[0] == 'Q') { __sanitizer_cov_trace_const_cmp8(size, 1); __sanitizer_cov_trace_const_cmp4(data[1], 'B'); if (size > 1 && data[1] == 'B') { __sanitizer_cov_trace_const_cmp8(size, 2); __sanitizer_cov_trace_const_cmp4(data[2], 'D'); if (size > 2 && data[2] == 'D') { __sanitizer_cov_trace_const_cmp8(size, 3); __sanitizer_cov_trace_const_cmp4(data[3], 'I'); if (size > 3 && data[3] == 'I') { __sanitizer_cov_trace_const_cmp8(size, 4); __sanitizer_cov_trace_const_cmp4(data[4], '!'); if (size > 4 && data[4] == '!') { __builtin_trap(); } else { ++BITMAP[4]; } } else { ++BITMAP[3]; } } else { ++BITMAP[2]; } } else { ++BITMAP[1]; } } else { ++BITMAP[0]; } return 0; } ``` With this **manual** source-based instrumentation, we simply need to link the compiled object file (`fuzzme.obj`) with the libFuzzer runtime (`lib/clang/20/lib/arm64-pc-windows-msvc/clang_rt.fuzzer.lib`): ```bash $ clang++ -fuse-ld=lld-link -fsanitize=fuzzer fuzzme.obj -o fuzzme.exe ``` Et voilà. As depicted in the following screenshot, when running `fuzzme.exe` on my Inspiron 14 Plus (Snapdragon X Elite), libFuzzer finds the relevant input (`QBDI!`) in less than a second.
![Fuzzing with source instrumentation feedback](img/fuzzing-src-feedback.webp)
Fuzzing with source instrumentation feedback

The key point here is that providing feedback to the fuzzer engine (libFuzzer) enhances the chances of finding meaningful inputs. While in this section, we assumed we had access to the function's original source code, the next section assumes a black-box approach. ## DBI-based Fuzzing Now let's consider that we don't have access to the source code of the function, which means we cannot use the `-fsanitize=fuzzer` option at compile time or manually modify the source code. This situation is similar to fuzzing a closed-source function. However, in our case, the implementation, and the harness of `fuzzme` is simple which is usually not the case with real-world targets. Without any kind of feedback to libFuzzer, `fuzzme.exe` runs at ~300k execs/s but it fails to find the input that triggers the `__builtin_trap` case before hours.
![Fuzzing without feedback](img/fuzzing-nofeedback.webp)
Fuzzing without feedback

By using a DBI like Intel PIN, DynamoRIO, Frida, QBDI or emulating the code (QEMU/Unicorn), we can gather information about the code being executed or emulated. This information can then be used to provide feedback to libFuzzer. While I'm sure the following example could work with QEMU, Frida or other DBI, I will focus on demonstrating these concepts using [QBDI](https://github.com/QBDI/QBDI). ### QBDI Bootstrap To instrument a function through QBDI, we first need to create and instantiate `QBDI::VM`[^qbdi-note]: ```cpp extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { QBDI::VM dbi; dbi.addInstrumentedModuleFromAddr((uintptr_t)&fuzzme); QBDI::GPRState* gpr = dbi.getGPRState(); gpr->pc = reinterpret_cast(fuzzme); gpr->lr = 0xdeadc0de; gpr->x0 = reinterpret_cast(data); gpr->x1 = reinterpret_cast(size); dbi.run(gpr->pc, gpr->lr); return dbi.getGPRState()->x0; } ``` This code is just bootstrapping the execution of `fuzzme()` through QBDI. In particular, we initiate `x0` and `x1` to match the inputs of `LLVMFuzzerTestOneInput`. ### Basic Block Coverage Once we have setup the execution through QBDI, we can define instrumentation callbacks to provide feedback to libFuzzer. For instance, we can offer coverage feedback using the QBDI event `BASIC_BLOCK_ENTRY`: ```cpp static std::vector BITMAP; dbi.addVMEventCB(VMEvent::BASIC_BLOCK_ENTRY, [] (VM* vm, const VMState* state, GPRState* gpr, FPRState* fpr, void* ctx) { size_t bitmap_idx = to_index(state->basicBlockStart); BITMAP[bitmap_idx] += 1; return VMAction::CONTINUE; }); ``` ### Comparison Feedback We can also use QBDI to provide feedback about the comparisons similarly to `__sanitizer_cov_trace_const_cmp{4,8}`. At the assembly level, these comparisons are represented as follows: ```text 14004ada8 08044039 ldrb w8, [x0, #0x1] 14004adac 1f090171 cmp w8, #0x42 14004adb0 c1010054 b.ne 0x14004ade8 ``` In particular, the LLVM `MCInst` representation of `cmp w8, #0x42` is: ```text :1:1: note: parsed instruction: ['cmp', , 66] cmp w8, #0x42 ^ cmp w8, #0x42 // encoding: [0x1f,0x09,0x01,0x71] // // // // > ``` One of the most powerful features of QBDI compared to other DBIs is the ability to specify the conditions under which we want an instrumentation callback. This means that the overhead associated with the DBI's context switch and the callback only occur when the specified condition is met. In our context, we don't want to pay the overhead for every instruction. Rather, we only want a "hook" for comparison operations. To achieve this, we can use the [`addMnemonicCB`](https://github.com/QBDI/QBDI/blob/34bf7e47ec90d825a320a3dd3479c1d6494358af/include/QBDI/VM.h#L459-L473) function or more efficiently, using the LLVM opcode: ```cpp dbi->addOpcodeCB(llvm::AArch64::SUBSWri, InstPosition::PREINST, [] (VM* dbi, GPRState* gpr, FPRState*, void*) { // [...] return VMAction::CONTINUE; }, /*data=*/nullptr); ``` This callback is triggered before any `cmp w[0-29], #cst` instruction. Ideally, we would like to call `__sanitizer_cov_trace_const_cmp4` with the values coming from the DBI. Something like: ```cpp {hl_lines=[3]} dbi->addOpcodeCB(llvm::AArch64::SUBSWri, InstPosition::PREINST, [] (VM* dbi, GPRState* gpr, FPRState*, void*) { __sanitizer_cov_trace_const_cmp4(inst.operands[0], inst.operands[1]); return VMAction::CONTINUE; }, /*data=*/nullptr); ``` This would work but `__sanitizer_cov_trace_const_cmp4` is computing PC with a macro that we can't control. Therefore, one solution consists of replicating the implementation of `__sanitizer_cov_trace_const_cmp4`: ```diff {hl_lines=[8]} dbi->addOpcodeCB(llvm::AArch64::SUBSWri, InstPosition::PREINST, [] (VM* dbi, GPRState* gpr, FPRState*, void*) { - __sanitizer_cov_trace_const_cmp4(inst.operands[0], inst.operands[1]); + const llvm::MCInst* inst = dbi->getOriginalMCInst(); + const size_t regw_idx = inst->getOperand(1).getReg() - llvm::AArch64::W0; + const uintptr_t cst = inst->getOperand(2).getImm(); + const auto* gpr_ptr = reinterpret_cast(gpr); + fuzzer::TPC.HandleCmp(gpr->pc, cst, gpr_ptr[regw_idx]); return VMAction::CONTINUE; }, /*data=*/nullptr); ``` Et voilà. We now provide comparison feedback to libFuzzer. As you can see in this screenshot, libFuzzer can efficiently identify the `QBDI!` input in less than 10 seconds.
![Fuzzing with QBDI feedback](img/fuzzing-qbdi.webp)
Fuzzing with QBDI feedback

## The Hidden Bits The attentive reader may have noticed significant simplifications regarding some technical aspects discussed in this blog post. For example, in the section on [Basic Block Coverage](#basic-block-coverage) I reference a function `to_index` which is intended to convert a basic block's start address into a bitmap index: ```cpp {hl_lines=[3]} dbi.addVMEventCB(VMEvent::BASIC_BLOCK_ENTRY, [] (VM* vm, const VMState* state, GPRState* gpr, FPRState* fpr, void* ctx) { size_t bitmap_idx = to_index(state->basicBlockStart); BITMAP[bitmap_idx] += 1; return VMAction::CONTINUE; }); ``` Theoretically, this code could work if we establish a unique mapping between the address of the basic block and the bitmap index (essentially, a bijection). This approach also operates under the assumption that we have a bitmap of unlimited size, as we cannot predict in advance how many basic blocks will be reached by the DBI. In practice, fulfilling these two conditions is quite challenging. This topic is also examined in [Google's Atheris blog post](https://security.googleblog.com/2020/12/how-atheris-python-fuzzer-works.html)[^atheris] which has been a source of inspiration for this blog post. You can review the actual implementation I used in the GitHub repository associated with this blog post: [ romainthomas/windows-arm64-qbdi-fuzzing](https://github.com/romainthomas/windows-arm64-qbdi-fuzzing) When it comes to QBDI, I simplified the process by stating that we only need to instantiate a `QBDI::VM` object: ```cpp extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { QBDI::VM dbi; dbi.addInstrumentedModuleFromAddr((uintptr_t)&fuzzme); [...] } ``` This approach works well, but it doesn't use a key optimization feature of QBDI: instrumented basic block caching. Essentially, QBDI caches instrumented basic blocks so that when we re-execute a known basic block, we don't need to patch and instrument it again. To take advantage of this optimization, we can store the `QBDI::VM` object in a static variable and initialize it just once. This allows us to leverage the caching mechanism effectively: ```cpp std::unique_ptr get_dbi() { auto dbi = std::make_unique(); dbi->addInstrumentedModuleFromAddr((uintptr_t)&fuzzme); [...] return dbi; } extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { static std::unique_ptr DBI = get_dbi(); [...] } ``` ## Final Words This blog post brings nothing new in terms of fuzzing techniques, but it demonstrates that: 1. QBDI is able to run and instrument Windows ARM64 code 2. LLVM libFuzzer works effectively on Windows ARM64. 3. QBDI and libFuzzer can work together to fuzz binaries without built-in coverage instrumentation (i.e. closed source) 4. LLVM excels in different areas from compilation[^ccompile], DBI (QBDI), reverse-engineering, and fuzzing :heart: The source code and the binaries used in this blog post are available on GitHub at this address: [ romainthomas/windows-arm64-qbdi-fuzzing](https://github.com/romainthomas/windows-arm64-qbdi-fuzzing) Happy Fuzzing [^doc]: https://llvm.org/docs/LibFuzzer.html [^qbdi-note]: Don't be confused by the name `VM`, we are talking about dynamic instrumentation, not emulation. [^atheris]: https://security.googleblog.com/2020/12/how-atheris-python-fuzzer-works.html [^ccompile]: **All** the compilation and link steps in this blog post are cross-compiled from Linux for Windows ARM64. This includes the (cross)compilation of QBDI and LLVM for Windows ARM64. --- # Instrumenting an Apple Vision Pro Library with QBDI - Canonical: https://www.romainthomas.fr/post/24-09-apple-lockdown-dbi-lifting/ - Markdown: https://www.romainthomas.fr/post/24-09-apple-lockdown-dbi-lifting/index.md - Section: post - Published: 2024-09-29 - Modified: 2026-08-04 - Tags: iOS, OSX > This blog post demonstrates how to extract liblockdown.dylib from the visionOS dyld shared cache to be instrumented with QBDI on an Apple M1. ## Introduction The purpose of this blog post is to demonstrate how to extract a library from an Apple visionOS 2.0 dyld shared cache to instrument it with QBDI on an Apple M1. Since the Apple Vision Pro and Apple M1 share the same architecture (ARM64), we should theoretically be able to execute and instrument binaries from one platform on the other. The iOS and visionOS environments are more restricted than macOS running as root with SIP disabled. Therefore, lifting and instrumenting their binaries on an Apple M1 can create new opportunities for fuzzing, reverse engineering, and vulnerability research. In general, running or instrumenting an arbitrary function compiled for iOS/visionOS on an Apple M1 is not possible especially if the given function is using hardware-specific inputs. That being said, research has already shown that we can mock, emulate[^emulate] or trick platform- or hardware-specific functions. I have no doubt that people have already found workarounds for that :) [^emulate]: Behavior emulation, not code emulation ## Dyld Shared Cache To speed up program loading, Apple bundles important libraries into a "shared" cache which is located in a file (or several files) named `dyld_shared_cache_.`. On macOS 14 - Sonoma, these shared cache files are located in `/System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/`: ```text -rwxr-xr-x 1 root admin 2.3G Aug 4 12:31 dyld_shared_cache_arm64e -rwxr-xr-x 1 root admin 1.5G Aug 4 12:31 dyld_shared_cache_arm64e.01 -rwxr-xr-x 1 root admin 984K Aug 4 12:31 dyld_shared_cache_arm64e.map -rwxr-xr-x 1 root admin 819M Aug 4 12:31 dyld_shared_cache_x86_64 -rwxr-xr-x 1 root admin 786M Aug 4 12:31 dyld_shared_cache_x86_64.01 -rwxr-xr-x 1 root admin 723M Aug 4 12:31 dyld_shared_cache_x86_64.02 -rwxr-xr-x 1 root admin 720M Aug 4 12:31 dyld_shared_cache_x86_64.03 -rwxr-xr-x 1 root admin 724M Aug 4 12:31 dyld_shared_cache_x86_64.04 -rwxr-xr-x 1 root admin 169M Aug 4 12:31 dyld_shared_cache_x86_64.05 -rwxr-xr-x 1 root admin 800K Aug 4 12:31 dyld_shared_cache_x86_64.map ``` If you already wondered where is located `/usr/lib/libSystem.B.dylib` while this library is not present on the filesystem, the answer is in the dyld shared cache. Prior to dyld v940 (publicly released in February 2022) the dyld shared cache was composed of a single file (but one per architecture). From version 940, it is composed of several files. ![Dyld shared cache evolution](./dyldshared_cache.webp) One of the interesting questions about the dyld shared cache is: *how do we extract or recover a library from this cache?* Indeed, the dyld shared cache encapsulates system libraries (including sensitive ones) so getting back these libraries as regular Mach-O binaries can be a legitimate question. There are different open source tools available for that: - https://github.com/keith/dyld-shared-cache-extractor - https://github.com/blacktop/ipsw - https://github.com/arandomdev/DyldExtractor The good news is that [LIEF](https://lief.re) is also supporting the shared cache with a C++/Rust/Python API (not publicly released yet). Given a visionOS dyld shared cache directory (from `Apple_Vision_Pro_2.0_22N320_Restore.ipsw`), we can load it with LIEF through: ```console $ ls -l visionOS-2.0/ dyld_shared_cache_arm64e dyld_shared_cache_arm64e.01 ... dyld_shared_cache_arm64e.57 dyld_shared_cache_arm64e.58 dyld_shared_cache_arm64e.59.dylddata dyld_shared_cache_arm64e.60.dyldlinkedit dyld_shared_cache_arm64e.symbols ``` ```python import lief shared_cache: lief.dyldsc.DyldSharedCache = lief.dyldsc.load("visionOS-2.0/") ``` From this `lief.dyldsc.DyldSharedCache` object, we can iterate over the embedded dylibs with: ```python shared_cache: lief.dyldsc.DyldSharedCache = ... for dylib in shared_cache.libraries: print(f"0x{dylib.address:016x}: {dylib.path}") ``` ```text 0x00000001800f4000: /usr/lib/libobjc.A.dylib 0x000000018013e000: /System/Library/AccessibilityBundles/ARKit.axbundle/ARKit 0x0000000180140000: /System/Library/AccessibilityBundles/ARTraceModule.axbundle/ARTraceModule 0x0000000180142000: /System/Library/AccessibilityBundles/ASMessagesProvider.axbundle/ASMessagesProvider ... 0x0000000251766000: /usr/lib/system/libxpc.dylib 0x00000002517ad000: /usr/lib/updaters/libAce3Updater.dylib 0x00000002517d2000: /usr/lib/updaters/libAppleTCONUpdater.dylib 0x00000002517da000: /usr/lib/updaters/libAppleTypeCRetimerUpdater.dylib 0x0000000251834000: /usr/lib/updaters/libBoraUpdater.dylib 0x000000025184a000: /usr/lib/updaters/libDurantUpdater.dylib 0x0000000251854000: /usr/lib/updaters/libSEUpdater.dylib 0x00000002518c1000: /usr/lib/updaters/libSavageRestoreInfo_iOS.dylib 0x00000002518cc000: /usr/lib/updaters/libSavageUpdater_iOS.dylib 0x00000002519f0000: /usr/lib/usd/libusd_ms.dylib 0x00000002528a7000: /usr/lib/xr/libRuntimeSupport.dylib ``` Getting back to the original question about extracting a library from the dyld shared cache, we can extract the in-cache Mach-O binary of `/usr/lib/liblockdown.dylib` using `dylib.get()`: ```python import lief shared_cache: lief.dyldsc.DyldSharedCache = ... liblockdown: lief.dyldsc.Dylib = shared_cache.find_library("/usr/lib/liblockdown.dylib") liblockdown_macho: lief.MachO.Binary = liblockdown.get() liblockdown_macho.write("liblockdown.1.dylib") ``` And we have come full circle: we can parse with LIEF shared cache files to get a `DyldSharedCache` object. From this instance, we can access the `lief.dyldsc.Dylib` object associated with `/usr/lib/liblockdown.dylib`. From this object, we can retrieve a LIEF's Mach-O binary with `.get()`. Finally, we can re-write back the Mach-O object with LIEF's writer: `liblockdown_macho.write("liblockdown.1.dylib")`. ![Schema from dyld shared cache to on-disk library](./img/lief-circle.webp) You can download `liblockdown.1.dylib` here: [romainthomas/visionOS-liblockdown/bin/liblockdown.1.dylib](https://github.com/romainthomas/visionOS-liblockdown/raw/refs/heads/main/bin/liblockdown.1.dylib). However, if we open this `liblockdown.1.dylib` library in IDA or BinaryNinja we might complain about the result: ![Calls & Memory Accesses Broken: before](img/ida-1.webp) ![Calls & Memory Accesses Broken: after](img/bn-1.webp) ![__cfstrings Relocations Broken: before](img/ida-2.webp) ![__cfstrings Relocations Broken: after](img/bn-2.webp) ![Broken import stubs: before](img/ida-3.webp) ![Broken import stubs: after](img/bn-3.webp) When Apple generates the dyld shared cache, it performs some optimizations and pre-loading, such as the in-cache libraries are usually referencing addresses specific to the shared cache. These optimizations usually break: - Relocations - Symbol bindings - Calls - Got accesses - ObjC metadata Last but not least, on recent shared cache, the `LC_DYLD_CHAINED_FIXUPS` command is striped from the library before being added to the shared cache: [![LC_DYLD_CHAINED_FIXUPS removed during cache builder](./img/dyld-removed-cmd.webp)](https://github.com/romainthomas/dyld/blob/main/cache-builder/OptimizerLinkedit.cpp#L387-L390) That means that we can't rely on this command to recover the original bindings or relocations. Fortunately, we can deoptimize the in-cache library by accessing internal structures of the dyld shared cache. All these de-optimizations can be enabled while calling the `.get()` function on a `LIEF::dyldsc::Dylib` object: ```cpp #include std::unique_ptr extract(const LIEF::dyldsc::DyldSharedCache& cache) { std::unique_ptr liblockdown = cache.find_library("liblockdown.dylib"); return liblockdown.get({ .fix_branches = true, .fix_memory = true, .fix_relocations = true, }); } ``` We can also ask LIEF to **recreate** a `LC_DYLD_CHAINED_FIXUPS` command based on information recovered from previous stages: ```diff #include std::unique_ptr extract(const LIEF::dyldsc::DyldSharedCache& cache) { std::unique_ptr liblockdown = cache.find_library("liblockdown.dylib"); return liblockdown.get({ .fix_branches = true, .fix_memory = true, .fix_relocations = true, + .create_dyld_chained_fixup_cmd = true, }); } ``` Et voilà: [romainthomas/visionOS-liblockdown/bin/liblockdown.2.dylib](https://github.com/romainthomas/visionOS-liblockdown/raw/refs/heads/main/bin/liblockdown.2.dylib). ![Schema from dyld shared cache to on-disk library](./img/lief-circle-next.webp) Using LIEF Python API it could be done with: ```python def extract(cache: lief.dyldsc.DyldSharedCache) -> lief.MachO.Binary: dylib: lief.dyldsc.Dylib = cache.find("liblockdown.dylib") return dylib.get( fix_branches=True, fix_memory=True, fix_relocations=True, create_dyld_chained_fixup_cmd=True, ) extract(vision_os_cache).write("liblockdown.2.dylib") ``` You can observe the differences between the raw extracted `liblockdown.1.dylib` and the de-optimized `liblockdown.2.dylib`: ![: before](img/ida-1.webp) ![: after](img/ida-1-1.webp) ![: before](img/bn-1.webp) ![: after](img/bn-1-1.webp) ![: before](img/ida-2.webp) ![: after](img/ida-2-1.webp) ![: before](img/bn-2.webp) ![: after](img/bn-2-1.webp) ![: before](img/ida-3.webp) ![: after](img/ida-3-1.webp) ![: before](img/bn-3.webp) ![: after](img/bn-3-1.webp) Now that we have a pretty well-structured `liblockdown.dylib` with all the information needed for loading (relocations, bindings, ...) we can start considering loading this `visionOS` library on macOS. ## Loading a VisionPro library on macOS Back in the day I was working at Quarkslab, I had the chance to create with [Adrien Guinet](https://github.com/aguinet) QBDL: QuarkslaB Dynamic Linker library. The idea of this project is to have a cross-platform/cross-format library to load executables (based on LIEF). For the details, you can check: - [SSTIC Presentation](https://www.sstic.org/2021/presentation/qbdl_quarkslab_dynamic_loader/) - [Quarkslab's Blog](https://blog.quarkslab.com/introducing-qbdl-how-to-run-the-nvidia-ngx-sdk-under-linux.html) - [Documentation](https://quarkslab.github.io/QBDL/0.1.0/) From an executable format perspective only, the primary differences between Mach-O binaries for visionOS and macOS (Silicon) lie in their linked libraries and their imports. We could fairly expect that `liblockdown.dylib` from the visionOS dyld shared cache is linked with visionOS-specific libraries or importing specific symbols. Nonetheless, Apple is an **ecosystem** and they try to minimize the cost for developers to create an app or a framework that can target different platforms (visionOS/macOS/iOS/watchOS). This comes with an API abstraction that we can leverage to lift executables from one platform on another. In other words, we can likely expect that some libraries used by `liblockdown.dylib` for visionOS are also available on macOS. Let's check this with QBDL: ```cpp #include #include using namespace QBDL; using namespace LIEF::MachO; using namespace LIEF; struct FinalTargetSystem: public Engines::Native::TargetSystem { using Engines::Native::TargetSystem::TargetSystem; uint64_t symlink(Loaders::MachO& loader, const LIEF::MachO::Symbol& symbol) override { } } int main() { auto mem = std::make_unique(); auto system = std::make_unique(*mem); auto loader = Loaders::MachO::from_file("./liblockdown.2.dylib" Engines::Native::arch(), *system, Loader::BIND::NOW ); return 0; } ``` In this code `symlink` is a kind of callback that is used by QBDL whenever a symbol needs to be resolved. For instance, you could redirect `printf` with: ```cpp int my_printf(const char *restrict format, ...) { printf("w000t"); return 5; } uint64_t symlink(Loaders::MachO& loader, const LIEF::MachO::Symbol& symbol) override { if (symbol.name() == "printf") { return (uint64_t)&my_printf; } return 0; } ``` With our `liblockdown.dylib` case, we can naively try to resolve symbols based on the existing macOS libraries: ```cpp uint64_t symlink(Loaders::MachO& loader, const LIEF::MachO::Symbol& symbol) override { const LIEF::MachO::DylibCommand* lib = symbol.library() void* hdl = dlopen(lib->name().c_str(), /*mode=*/RTLD_NOW); if (hdl == nullptr) { fprintf(stderr, "Can't find library %s on the current macOS system\n", lib->name().c_str()); return 0; } void* addr = dlsym(hdl, symbol.name().c_str()); if (addr == nullptr) { fprintf(stderr, "Can't find '%s' in %s\n", symbol.name().c_str(), lib->name().c_str()); return 0; } return (uint64_t)addr; } ``` When executing this code, we can observe that the library `liblockdown.dylib` from visionOS is *binding* 395 symbols. From these 395 symbols, only **4 of them fail to be resolved** using macOS library: ```text Can't find '__SSLCopyPeerCertificates' in /System/Library/Frameworks/Security.framework/Security Can't find '__SSLDisposeContext' in /System/Library/Frameworks/Security.framework/Security Can't find '__SSLNewContext' in /System/Library/Frameworks/Security.framework/Security Can't find '__SSLSetEnableCertVerify' in /System/Library/Frameworks/Security.framework/Security ``` In other words, **98% of the functions** imported by `liblockdown.dylib` on the visionOS are **natively available** on macOS 14. The missing symbols could be mocked or emulated but for the sake of simplicity we just skip them. Now that we have a mean to resolve imported symbols pretty well, we can ask QBDL to get a pointer to the function `_lockdown_connect`: ```cpp struct FinalTargetSystem: public Engines::Native::TargetSystem { using Engines::Native::TargetSystem::TargetSystem; uint64_t symlink(Loaders::MachO& loader, const LIEF::MachO::Symbol& symbol) override { // Logic described previously } } int main() { auto mem = std::make_unique(); auto system = std::make_unique(*mem); auto loader = Loaders::MachO::from_file("./liblockdown.2.dylib" Engines::Native::arch(), *system, Loader::BIND::NOW ); uint64_t addr = loader->get_address("_lockdown_connect"); LK_INFO("_lockdown_connect: 0x{:016x}", addr); return 0; } ``` At this point we are in the situation where: 1. We extracted `/usr/lib/liblockdown.dylib` from a visionOS dyld shared cache 2. We removed shared cache optimizations and we re-created a `LC_DYLD_CHAINED_FIXUPS` 3. We loaded the library with QBDL and most of the imported symbols are resolved 4. We have a pointer to `_lockdown_connect` in macOS memory Depending on the needs, we could start lldb to debug the function or feed the function with fuzzing inputs. Since I talked about [LIEF](https://lief.re) & [QBDL](https://github.com/quarkslab/QBDL), I can't finish this blog post without mentioning [QBDI](https://qbdi.quarkslab.com/). ## Instrumenting `_lockdown_connect` with QBDI First off, we have to instantiate and initialize a QBDI's VM[^vm] object: ```cpp // [...] auto loader = Loaders::MachO::from_file("./liblockdown.2.dylib" Engines::Native::arch(), *system, Loader::BIND::NOW ); uint64_t lockdown_connect_addr = loader->get_address("_lockdown_connect"); LK_INFO("_lockdown_connect: 0x{:016x}", lockdown_connect_addr); QBDI::VM dbi; QBDI::GPRState* state = vm.getGPRState(); state->pc = lockdown_connect_addr; uint64_t start = loader->base_address(); uint64_t end = start + loader->get_binary().virtual_size(); vm.addInstrumentedRange(start, end); ``` I omitted stack registers initialization (`sp`, `x29`) but the previous code is essentially setting the PC register and scoping the range of virtual addresses we want to instrument. After this initialization, we can define what we want to do with the instrumented code. In this example, we just trace the instructions: ```cpp vm.addCodeCB(QBDI::InstPosition::PREINST, [] (QBDI::VM* vm, QBDI::GPRState* gpr, QBDI::FPRState*, void* data) { const QBDI::InstAnalysis* inst = vm->getInstAnalysis(); LK_INFO("0x{:016x}: {}", inst->address, inst->disassembly); return QBDI::VMAction::CONTINUE; }, nullptr ); ``` Let's go? ```cpp vm.run(lockdown_connect_addr, lr); ``` And here we go: ```cpp _lockdown_connect: 0x00000001039ed024 0x00000001039ed024: pacibsp 0x00000001039ed028: sub sp, sp, #208 0x00000001039ed02c: stp x22, x21, [sp, #160] 0x00000001039ed030: stp x20, x19, [sp, #176] 0x00000001039ed034: stp x29, x30, [sp, #192] 0x00000001039ed038: add x29, sp, #192 0x00000001039ed03c: adrp x8, #70324224 0x00000001039ed040: ldr x8, [x8, #3352] 0x00000001039ed044: ldr x8, [x8] 0x00000001039ed048: stur x8, [x29, #-40] 0x00000001039ed04c: mov w21, #1 0x00000001039ed050: mov w0, #1 0x00000001039ed054: mov w1, #1 0x00000001039ed058: mov w2, #0 0x00000001039ed05c: bl #10372 0x00000001039ef8e0: adrp x17, #254316544 0x00000001039ef8e4: add x17, x17, #3176 0x00000001039ef8e8: ldr x16, [x17] 0x00000001039ef8ec: braa x16, x17 0x00000001039ed060: cmn w0, #1 ... ``` Given this instruction-level granularity, we could also intercept syscall instructions. I won't advocate any longer about how powerful QBDI is, but for those who are interested in more detail you can check these posts/publications: - [Android Native Library Analysis with QBDI](/post/android-native-library-analysis-with-qbdi/) - [Dynamic Binary Instrumentation Techniques to Address Native Code Obfuscation](/publication/20-bh-asia-dbi/) - [r2-pay - part 1](/post/20-09-r2con-obfuscated-whitebox-part1/) & [r2-pay - part 2](/post/20-09-r2con-obfuscated-whitebox-part2/) [^vm]: The term VM can be confusing but are still talking about dynamic binary instrumentation like Intel PIN not emulation:) ## Closing Words This blog post demonstrates that it is possible to extract a library from a visionOS dyld shared cache that can be instrumented on a different platform. While the same approach would also work for an iOS dyld shared cache, I thought it would be more challenging to tackle a recent device that lacks the same knowledge base as iOS. Back in January 2024, dfsec also did a publication about running an iOS binary on macOS: [Will macOS and iOS merge?](https://www.df-f.com/blog/macosandiosmerge) Both approaches are complementary and I recommend reading their blog post. For those who seek more details about QBDL/QBDI and a live demo of the instrumentation, I recorded this video where I detail more aspects about QBDL loading and QBDI instrumentation: [YouTube video](https://www.youtube.com/watch?v=5L05OE5mL2o) Finally, the source code of the PoC is on GitHub at this address: [ romainthomas/visionOS-liblockdown](https://github.com/romainthomas/visionOS-liblockdown) The dyld shared cache support in LIEF is still in progress but you can join [ LIEF's Discord channel](https://discord.com/invite/7hRFGWYedu) to be notified when this will be released. Thank you for reading, Romain ### References - https://www.nowsecure.com/blog/2024/09/11/reversing-ios-system-libraries-using-radare2-a-deep-dive-into-dyld-cache-part-1/ - https://worthdoingbadly.com/dscextract/ --- # iCDump: A Modern Objective-C Class Dump - Canonical: https://www.romainthomas.fr/post/23-01-icdump/ - Markdown: https://www.romainthomas.fr/post/23-01-icdump/index.md - Section: post - Published: 2023-01-04 - Modified: 2026-08-04 - Tags: iOS, Objective-C, LLVM, OSX > This blog post introduces iCDump, a new Objective-C class dump based on LLVM. ## Introduction iCDump is a tool to access and process Objective-C metadata located in 64-bits Mach-O binaries. It uses LIEF to load the raw Objective-C data and LLVM to output the reconstructed Objective-C structures. The *LLVM output* was inspired by [rellic](https://github.com/lifting-bits/rellic) developed by Trail of Bits. The project is available on GitHub: [romainthomas/iCDump](https://github.com/romainthomas/iCDump). ## Quick Start Python wheels for Linux and OSX are available on PyPI such as one can install iCDump through: ```bash $ pip install [--user] icdump ``` Once installed, you can run [readobjc.py](https://github.com/romainthomas/iCDump/blob/main/bindings/python/tools/readobjc.py) to quickly extract Objective-C structures from a Mach-O binary: ```bash $ readobjc.py ./RNCryptor.bin ``` ```objc @protocol __ARCLiteKeyedSubscripting__ - (NSObject *)objectForKeyedSubscript:(NSObject *)self :(SEL)id :(NSObject *)arg2; - (void)setObject:(NSObject *)self forKeyedSubscript:(SEL)id :(NSObject *)arg2 :(NSObject *)arg3; @end @interface PodsDummy_RNCryptor_iOS @end @interface RNCryptor.RNCryptor.Encryptor{ NSObject * encryptor; } @end @interface RNCryptor.RNCryptor.Decryptor{ NSObject * decryptors; NSObject * buffer; NSObject * decryptor; NSObject * password; } @end ``` Using the Python API, we can output these header-like structures as follows: ```python import icdump metadata = icdump.objc.parse("./RNCryptor.bin") print(metadata.to_decl()) ``` ## Limitations In its current form, iCDump can only process Objective-C metadata but the Swift structures also aim at being supported by iCDump. I started a PoC for this part but it's far from being ready to be merged. The second limitation is Windows. Theoretically, iCDump and its Python bindings, could be compiled for Windows but to be honest, for this release I was lazy to set up the CI pipeline with LLVM for Windows. Nevertheless, Windows users should be able to play with iCDump thanks to WSL. The API documentation needs to be written and generated with Sphinx but in the meanwhile, you can directly take a look at the [Python binding](https://github.com/romainthomas/iCDump/blob/main/bindings/python/src/ObjC/init.cpp) which synthesis the important functions. --- # Open-Obfuscator: A free and open-source obfuscator for mobile applications - Canonical: https://www.romainthomas.fr/post/22-10-open-obfuscator/ - Markdown: https://www.romainthomas.fr/post/22-10-open-obfuscator/index.md - Section: post - Published: 2022-10-31 - Modified: 2026-08-04 - Tags: Android, iOS, obfuscation, LLVM, Proguard > This blog post introduces open-obfuscator, a new open-source project to obfuscate mobile applications. ## Introduction This post is about a new project on which I (intensively) worked this month. It aims at providing a free and open-source solution for obfuscating mobile applications. > **Note** open-obfuscator is available at this URL: https://obfuscator.re, and this blog post is more about the motivation behind this project. You can also check the GitHub repositories: [O-MVLL](https://github.com/open-obfuscator/o-mvll) & [dProtect](https://github.com/open-obfuscator/dprotect) ## The origin of this project For several years now, I'm doing reverse engineering on (obfuscated) mobile applications and my latest publications tend to be more about how to "defeat" obfuscation rather than how to protect code. I actually enjoy both aspects and I'm also aware that breaking[^breaking] something or reversing a RASP check is -- most of the time -- easier than finding and building something that works **at scale** and for **different users/environments** (like an obfuscator). An idea started to grow after a recent discussion about the legal aspect of reverse engineering on obfuscated code. It's true: reverse engineering an obfuscator is not permitted and publishing the results is even less. Depending on the interlocutor, you might be lucky or not (c.f. [Promon vs University Researchers](https://www.heise.de/newsticker/meldung/Offenlegung-von-Softwareluecken-Rechtsstreit-endet-mit-Vergleich-4156393.html) and this [36C3 talk](https://www.youtube.com/watch?v=RGNarKVO-WI)). That being said, the publications of this blog followed a responsible disclosure in which the stakeholders have been contacted ahead time of the publications. For instance, the feedback from Google for SafetyNet was very fair: ![Google's feedback](./snet.webp "Google's feedback") On the other hand, if one would have to decide which solution would be better to protect his assets, this person could only refer (and infer) datasheets, demos[^terms], or testimonials. In particular, there are no public benchmarks to get an idea about how solutions are positioned from each other. Moreover, it can be difficult for developers to understand the purpose of an obfuscation scheme and how it really works underneath. Thus, why not trying to take another hat and creating an obfuscator that fulfills these objectives: - Easy to use and easy to integrate for the developers. - Highly documented so that developers have a good understanding of the protections. - Open source: 1. To welcome **public improvements**. 2. To welcome **public attacks**, free from legal issues, which in the end are used to enhance the protections. 3. To be objectively and technically benchmarked. - Realistic: trying to be as close as possible to the state of the art for the benefit of both: developers and attackers. > **Note**

Obfuscation and open source sounds a bit contradictory no?

Somehow yes. If the design is known attacks are easier. **But**, even if an obfuscation technique is known, attacks can remain costly. Let's take an example with the control-flow flattening protection. The technique is known, documented and there are public attacks[^cfgflat] but defeating this pass (statically) can be painful and time-consuming. Usually (including myself), it forces the reverse engineer to analyze the program dynamically which shifts the problem to dynamic protections. Thus, we can assume that the pass is efficient against static analysis even if it is known. Another example, [SafetyNet](/publication/22-sstic-blackhat-droidguard-safetynet). The overall design of the protection is based on a virtual machine. Once we reversed the different handlers, we can assume that the design is "known". Some layers of obfuscation are also based on **known** MBA. But, Google did something very smart, a new version is published regularly and the internal components are shuffled. I do believe that the strongest protection against reverse engineering comes from a well-thought design, not an individual obfuscation pass. Obfuscation is also about time, and I think that by combining several (known) obfuscation techniques, we can trigger a time-out in the attackers. Talking about time, this month I had the time to bridge all these ideas. ## Technical choices So far, the ideas were on the paper and I did not want to create "*yet another O-LLVM fork*" [^yansollvm] or spend time on something that already exists. ## Native code obfuscation No doubt about using LLVM and while looking at the current solutions based on LLVM, I found [eShard/obfuscator-llvm](https://github.com/eshard/obfuscator-llvm) on GitHub, which is developed by [eShard](https://eshard.com/) (a company known for side channels attacks on whiteboxes [^scared][^estraces]). Compared to other LLVM-based obfuscators, it uses the new LLVM pass manager which enables us to load an **out-of-tree plugin** with clang: ```bash clang -fpass-plugin=/libLLVMObfuscator.so \ hello_world.c -o hello_world ``` I was definitely convinced by this way of using an obfuscator. Especially because it keeps the original compiler from the toolchain which simplifies this kind of issue: [Hikari/Troubleshooting - AArch64e Support](https://github.com/HikariObfuscator/Hikari/wiki/Troubleshooting#aarch64e-support). On the other hand, I was less convinced of using environment variables to trigger and configure the obfuscation passes. ## Java/Kotlin obfuscation To support Java and Kotlin obfuscation for Android, I took a look at [Proguard](https://github.com/Guardsquare/proguard)/[Proguard-core](https://github.com/Guardsquare/proguard-core) which actually provides all the components to create an obfuscation pipeline. Proguard is known for obfuscating symbols (class names, methods, ...) and optimizing code but it's just the tip of the iceberg. The project is **really** well designed and modular such as it was very easy to add an obfuscation pipeline. ## O-MVLL/dProtect I ended up with two projects: 1. O-MVLL, in reference to the well know, parent of all the forks: [O-LLVM](https://github.com/obfuscator-llvm). 2. **d**Protect for **`{dex,droid}`**-Protector ### O-MVLL O-MVLL uses the original idea of eShard: a *`pass-plugin`* obfuscator. On the other hand, it uses a Python API instead of environment variables. In short, the user defines what she/he wants to protect and how she/he wants to protect it in Python. For instance, to obfuscate strings the developer must override the `obfuscate_string` method: ```python def obfuscate_string(self, mod: omvll.Module, func: omvll.Function, string: bytes): # Replace the string return "REDACTED" # Obfuscate the string on the stack return omvll.StringEncOptStack() # Obfsucate the string the stack with a # loop if the length is too long return omvll.StringEncOptStack(loopThreshold=0) ... ``` There is also `mod: omvll.Module` and `func: omvll.Function` in the arguments of the function. These arguments are LLVM objects wrapped with Python bindings. Thus, the user can use all the information (name, flags, visibility [^flag]) provided by LLVM for these objects. If the user only wants to protect strings in the function `omvll::decode()`, she/he can use this condition: ```python def obfuscate_string(self, mod: omvll.Module, func: omvll.Function, string: bytes): if func.demangled_name == "omvll::decode()": return StringEncOptGlobal() ... ``` You can find more details about this pass [here](https://obfuscator.re/omvll/passes/strings-encoding/) and you can also explore the O-MVLL documentation [here](https://obfuscator.re/omvll). ### dProtect For dProtect, nothing really new in terms of API compared to Proguard. I added custom obfuscation passes that can be enabled as follows: ```bash # Mixed boolean-arithmetic Obfuscation -obfuscate-arithmetic,high class com.dprotect.salsa20 { *; } # Strings protection -obfuscate-strings "XEYnuNOGoEQ*", "TOKEN: *" -obfuscate-strings class dprotect.Connect { private static java.lang.String API_KEY; public static java.lang.String getToken(); } # Constants -obfuscate-constants class com.dprotect.secret.** { *; } # Control-Flow -obfuscate-control-flow class com.dprotect.internal.** { *; } ``` The logic of these dProtect's passes relies on existing internal components available in Proguard and Proguard-CORE which have been developed by [Eric Lafortune](https://www.lafortune.eu/) and [James Hamilton](https://jameshamilton.eu/). In its current version, dProtect provides the following obfuscation passes: - [Arithmetic Obfuscation](https://obfuscator.re/dprotect/passes/arithmetic/) - [Constants Obfuscation](https://obfuscator.re/dprotect/passes/constants/) - [Control-Flow Obfuscation](https://obfuscator.re/dprotect/passes/control-flow/) - [Strings Encryption](https://obfuscator.re/dprotect/passes/strings/) ## CI O-MVLL and dProtect are CI compiled with nightly packages available at these addresses: - http://nightly.obfuscator.re/latest/omvll - http://nightly.obfuscator.re/latest/dprotect The CI for O-MVLL could have been challenging to bootstrap because it is based on an out-of-tree plugin. However, once the right version of LLVM is precompiled, building the project from scratch for both the NDK and Xcode toolchains takes about **2 minutes**. The environment to compile O-MVLL for the Android NDK and Xcode is fully Dockerized and available on Docker Hub: https://hub.docker.com/u/openobfuscator: - [openobfuscator/omvll-ndk](https://hub.docker.com/r/openobfuscator/omvll-ndk) - [openobfuscator/omvll-xcode](https://hub.docker.com/r/openobfuscator/omvll-xcode) For those who are interested, you can check out the GitHub Actions configurations: - [NDK Workflow](https://github.com/open-obfuscator/o-mvll/blob/main/.github/workflows/ndk.yml) - [Xcode Workflow](https://github.com/open-obfuscator/o-mvll/blob/main/.github/workflows/xcode.yml) ## Demo I think the better way to demonstrate the level of obfuscation that can be provided by O-MVLL and dProtect in their current version, is to obfuscate an open-source project. For that purpose, I choose [optiv/android-ndk-crackme](https://github.com/optiv/android-ndk-crackme) and the configuration of O-MVLL and dProtect are available here: - [`o-conf.py`](https://gist.github.com/romainthomas/68ecda22775c2b3ca036bd58e4e73e5a) - [dProtect.pro](https://gist.github.com/romainthomas/0d89bcd2547ff5968a0f3a5f4f7c067d) The cherry on the cake, I also applied some techniques described in ["The Poor Man Obfuscator"](/publication/22-pst-the-poor-mans-obfuscator): - [elf_tricks.py](https://gist.github.com/romainthomas/5898cd2c64b01b29270d9cad6787c1ff) In the end, we have this un-obfuscated version of the crackme: [com.optiv.ndkcrackme.apk](https://data.romainthomas.fr/22-10-open-obfuscator/com.optiv.ndkcrackme.apk) and this one, protected with O-MVLL and dProtect: [com.optiv.ndkcrackme-protected.apk](https://data.romainthomas.fr/22-10-open-obfuscator/com.optiv.ndkcrackme-protected.apk) About iOS, O-MVLL works **as a PoC and the support is far from being finished**: > **Note** The obfuscation passes [Anti-Hooking](https://obfuscator.re/omvll/passes/anti-hook/) and [Control-Flow Breaking](https://obfuscator.re/omvll/passes/control-flow-breaking) are not working correctly due to an issue (from O-MVLL) in the JIT engine.

The overall support for iOS is **highly** experimental. Nevertheless, you can compare this (unprotected) version of zlib for iOS: [libz.dylib](https://data.romainthomas.fr/22-10-open-obfuscator/libz.dylib) with this protected one: [libz-obfuscated.dylib](https://data.romainthomas.fr/22-10-open-obfuscator/libz-obfuscated.dylib). The obfuscation has been done with this configuration file: [ios-zlib-obf.py](https://gist.github.com/romainthomas/149ce793702f3dc28126f7840f5d6870). ## Last words The project is one month old so you can expect bugs, limitations, and typos. I released a beta version on GitHub so feel free to try and send your feedback. Regarding the time spent on the project: - 60% on the documentation, UI, website, etc - 30% on the obfuscation passes themselves - 5% on the CI - 5% on the tests So yes, some obfuscation passes still need to be improved to be really efficient and the support for iOS is very experimental (i.e. depending on the configuration, **the compiler might crash**). There is also a test suite for both dProtect and O-MVLL but it is not public yet. Open-obfuscator is going to be my second largest project after [LIEF](https://lief-project.github.io/) and I hope it will serve its long-term purpose: **Providing an obfuscation playground for both, developers, and reverse engineers**.
Thank you for reading and happy Halloween :jack_o_lantern: Romain [^yansollvm]: This fork exists: https://github.com/emc2314/YANSOllvm [^breaking]: Breaking obfuscation is a moot topic. [^terms]: I guess the *terms of use* of the demo does not allow you to reverse it. [^flag]: In the current version, only the name is provided. [^cfgflat]: See: https://obfuscator.re/omvll/passes/control-flow-flattening/#references [^scared]: https://gitlab.com/eshard/scared [^estraces]: https://gitlab.com/eshard/estraces --- # Part 2 – iOS Native Code Obfuscation and Syscall Hooking - Canonical: https://www.romainthomas.fr/post/22-09-ios-obfuscation-syscall-hooking/ - Markdown: https://www.romainthomas.fr/post/22-09-ios-obfuscation-syscall-hooking/index.md - Section: post - Published: 2022-09-13 - Modified: 2026-09-05 - Tags: ios, reverse engineering, obfuscation > This second blog post deals with native code obfuscation and RASP syscall interception > **Note** The first part is here: [Part 1 -- SingPass RASP Analysis](/post/22-08-singpass-rasp-analysis) After SingPass, I took a look at another application protected with the same obfuscator but with enhanced protections. Compared to the previous application, this new application crashes immediately as soon as it is launched. By checking the crash log, we don't get any meaningful information since the obfuscator trashes some registers like `LR` before crashing. By trashing `LR`, the iOS crash analytics service is not able to correctly build the call stack of the functions that led to the crash. On the other hand, by tracing the libraries loaded by the application, we can identify in which loaded library the application crashes, and thus, the library is likely in charge of checking the environment's integrity. ```text $ ijector.py --spawn ios.app iTrace started PID: 63969 | tid: 771 Home: /private/var/mobile/Containers/Data/Application/A59541E1-106A-4C31-8188-0830E651449E ... ImageLoader::containsAddress(0x1065f948c): cxxreact!1948c ImageLoader::containsAddress(0x10564e270): ReactCommon!1a270 ImageLoader::containsAddress(0x103e5ed84): GRDB!12ed84 ImageLoader::containsAddress(0x104407790): Intercom!1bb790 ImageLoader::containsAddress(0x104c29d7c): KaaSLogging!9d7c ImageLoader::containsAddress(0x105871bb4): RxSwift!91bb4 ImageLoader::containsAddress(0x1056f00cc): RxBluetoothKit!440cc ImageLoader::containsAddress(0x104633f50): KaaSBle!bbf50 ---> CRASH! ``` So the application crashes when loading the `KaaSBle` library embedded as a third-party framework of the application. Compared SingPass, the library does not leak symbols about the RASP checks nor about the obfuscator. In addition, some functions are obfuscated with control-flow flattening and Mixed Boolean-Arithmetic (MBA) expressions as we can observe in the following figure:
![iOS Control-Flow Flattening](imgs/cfg_flat_macho_ctor.webp)
Figure 1 - Control-Flow Flattening in the Constructor of KaaSBle

Based on the previous analysis of `SingPass`, we know that RASP checks related to jailbreak or debugger detection use uncommon functions like `getpid`, `unmount` or `pathconf`. It turns out that, these functions are also imported by `KaaSBle` which enables to identify where some of the RASP checks are located. > **Note** Uncommon imported functions like `unmount` are usually a good signature to identify potential RASP checks For instance, the function sub_EBDC which uses `getpid` is likely involved in the debugger detection. This function is obfuscated with an MBA and control-flow flattening and, its graph is represented in Figure 2[^graph]
![Technical diagram](svg/bn_graph_ebdc.svg)

Figure 2 - BinaryNinja HLIL Graph of sub_EBDC

## Control-Flow Flattening I won't detail how generally control-flow flattening works as it already exists a good bunch of articles on this topic: - [Deobfuscation: recovering an OLLVM-protected program ](https://blog.quarkslab.com/deobfuscation-recovering-an-ollvm-protected-program.html) by [Quarkslab](https://www.quarkslab.com/) - [Automated Detection of Control-flow Flattening](https://synthesis.to/2021/03/03/flattening_detection.html) by [Tim Blazytko](https://twitter.com/mr_phrazer) - [D810: A journey into control flow unflattening](https://eshard.com/posts/D810-a-journey-into-control-flow-unflattening) by [eShard](https://eshard.com) Nevertheless, we can notice that the state variable that is used to drive the execution through the flattened blocks is linear and not encoded: > **Note**

The state variable set at the end of the basic block exactly defines the next basic block to execute.

This means that given: 1. A state value 2. The switch table 3. The *switch base address* It is possible to easily compute the targeted basic block:
![Design of Graph Flattening](imgs/cfg-flat.webp)
Fig 3. Computation of the Basic Block from a State Variable
![Technical diagram](svg/cfg_flat_overview.svg)
Fig 4. Simplified Overview
Since there is no encoding, we can determine the next states of a basic block by looking at the constant written in the local stack variable [sp, 0x50+var_4c] or the state_variable of the BinaryNinja High Level IL representation ([Figure 2](#fig-2)). From a graph recovery perspective, this design **completely fits** in the case of the Quarkslab's blog: [recovering an OLLVM-protected program ](https://blog.quarkslab.com/deobfuscation-recovering-an-ollvm-protected-program.html), thus the original graph could be completely recovered. > **Note** I also checked other large control-flow-flattened functions in the binary and they follow the same design with the same weakness. ### Improvements > **Note** Spoiler: *This example comes from an on-going larger project: [open-obfuscator](https://github.com/open-obfuscator).* Actually we can enhance the protections of the control-flow flattening by encoding the state variable and by identifying the basic blocks of the switch table with random numbers (instead of 1, 2, 3 etc). The following figure outlines this design:
![Technical diagram](svg/cfg_flat_overview_new.svg)
Fig 5. Control-Flow Flattening with Random ID and Encoding
Concretely, the code generated **does not use a lookup-switch table** and the dispatcher is a succession of conditions:
![Head of the O-MVLL flattened function](imgs/cfg-flat-omvll-head.webp)
Figure 6 - Head of the Control-Flow Flattening

We can also observe the encoding block at the end of the graph:
![Tail of the O-MVLL flattened function](imgs/cfg-flat-omvll-tail.webp)
Figure 7 - Tail of the Control-Flow Flattening

In this example, the encoding is simply $E(X) = X \oplus A + B$ but it could be protected with an MBA and generated with different expressions, unique per function. Globally speaking, any injective (or bijective) function should fit as an encoding. In the end, it would increase the complexity of recovering the original graph **at scale** (even though the design is known). ## Mixed-Boolean Arithmetic We can also observe in [Figure 2](#fig-2) that the function uses an MBA as an opaque zero or more precisely an opaque boolean. Generally speaking, MBA are widely used by the obfuscator but they are usually represented under their *simple* form like $(A \oplus B) + (A \\& B) \times 2$. In other words, we **can't quickly** identify the underlying arithmetic operation but with limited efforts, we can simplify the expression using public tools. If you want to dig more into MBA deobfuscation, I highly recommend this recent blog post [Improving MBA Deobfuscation using Equality Saturation](https://secret.club/2022/08/08/eqsat-oracle-synthesis.html) by [Tim Blazytko](https://twitter.com/mr_phrazer) and [Matteo](https://twitter.com/fvrmatteo) which also lists open-source tools that can be used for simplifying MBA like: - [sspam](https://github.com/quarkslab/sspam) - [msynth](https://github.com/mrphrazer/msynth/) (Used for this binary) > **Note** [Triton](https://triton-library.github.io/) also supports program synthesis: [synthesizing_obfuscated_expressions.py](https://github.com/JonathanSalwan/Triton/blob/96104b1a860dc7ddb9ab123859b2fd668e388f72/src/examples/python/synthesizing_obfuscated_expressions.py) :) ## Strings Encoding Most of the strings used in the library are encoded which prevents identifying quickly sensitive functions. These encoded strings are decoded *just-in-time* near the instruction that uses given the string. In the blog post about [PokemonGO](/post/21-07-pokemongo-anti-frida-jailbreak-bypass), **all the strings** were decrypted at once in the Mach-O constructors which enabled to recover all of these strings without caring about reverse engineering the decoding routines. For the current obfuscator, we can't exactly apply this technique.
![Technical diagram](svg/arxan_vs_ixguard.svg)

Fig 8. Differences in Designing String Encryption

To better understand the difficulty, let's take a closer look at how strings are encoded with the _unmount() function. As a reminder, this function is used as a part of jailbreak detection. In the KaaSBle library, there are five cross-references to _unmount():
![Technical diagram](svg/xref_unmount.svg)

When looking at the prologue of the _unmount() calls, we get the following basic blocks:
![Decoding Routine for /.bootstrapped](imgs/boostrapped.webp)
Figure 9 - Decoding Routine for the String /.bootstrapped

Which is equivalent to this snippet: ```python from itertools import cycle def decode(encrypted: bytes, key: str, op): key = bytes.fromhex(key) encrypted = bytes.fromhex(encrypted) out = "" for idx, (k, v) in enumerate(zip(encrypted, cycle(key))): out += chr(op(idx, k, v) & 0xFF) return out # /.bootstrapped clear = decode("9f0b698a3abc17e70bb54332271180", # Encoded string "b0250be555c8649379d43342427580", # Key lambda _, k, v: (k ^ v)) # Operation ``` It is worth mentioning that the string is not decoded *in-placed* but in another ``__data`` variable. This means that an encoded string takes potentially twice its size in the final binary. Another example of a decoding routine:
![Decoding Routine for /.installed_odyssey](imgs/installed_odyssey.webp)
Figure 10 - Decoding Routine for the String /.installed_odyssey

Which is equivalent to: ```python # /.installed_odyssey clear = decode("1bec336463362f66602b365d672e4f756f3353", # Encoded string "ecbdc8f3", # Key lambda i, k, v: (k - v - i)) # Operation ``` In this case, the key is an ``uint32_t`` integer for which the bytes are accessed through a stack variable. The *weird* operation ``x12 = x8 & (x8 ^ 0xfffffffffffffffc)`` is simply a modulus ``sizeof(uint32_t)`` :) In summary, because of the **disparity** of the encodings which are mixed with MBA and unique keys, it would be quite difficult to **statically** decode all the strings of the library. On the other hand, since the clear strings are written in the `__data` section of the binary, we can dump -- at some point in the execution -- this section and observe the clear strings (c.f. [SingPass RASP Analysis - Jailbreak Detection](/post/22-08-singpass-rasp-analysis#jailbreak-detection)). ## Crash Analysis When the obfuscator detects that the environment is compromised (jailbroken device, debugger attached, ...), it reacts by crashing the application. This crash occurs through different techniques among which: 1. Corrupting a global pointer 2. Executing a break instruction (BRK #1) 3. Trashing the link register and frame register (LR / FP) 4. Calling `objc_msgSend` with corrupted parameters The instructions involved in crashing the application are **inlined** in the function where the check occurs. This means that there is as many *crash routine* as there are RASP checks. In particular, with such a design, we can't target a single function to bypass the different checks as I did for SingPass. ## Hooking the Syscalls > **Note** This approach is inspired by this talk at Pass the Salt: [Jailbreak Detection and How to Bypass Them](https://archives.pass-the-salt.org/Pass%20the%20SALT/2021/slides/PTS2021-Talk-01-JailBreak_detection.pdf) To better understand the *problem*, let's recap the situation: 1. The code is obfuscated with CFG flattening, MBA, etc 2. The RASP checks are **inlined** in the code 3. The application crashes near the detection spot. In particular and compared to SingPass, there is no RASP endpoint that can be hooked. The following figure depicts the differences in the RASP reaction between the two applications:
![Technical diagram](svg/callback_vs_crash.svg)

Figure 11 - RASP Reaction: User Callback vs Crash

We can't actually hook a function to bypass the RASP checks **but** the structure of the AArch64 instructions has a valuable property: > **Note**

The size of an AArch64 instruction is fixed

As a consequence, we can **linearly** search the SVC #80 instructions which are encoded as 0xD4001001. ### Interception Let's consider the following approach to intercept the syscalls: 1. We linearly scan the `__text` section to find the `SVC` instructions (i.e. the four-bytes `0xD4001001`) 2. We replace this instruction with a branch (`BL #imm`) to a function we *control* 3. We process the redirection to disable the RASP checks For the first point, thanks to the fixed instruction's size, we can search syscalls by reading the whole `__text` section: ```cpp static constexpr uint32_t SVC = 0xD4001001; // SVC #0x80 static constexpr size_t SIZEOF_INST = 4; for (size_t addr = text_start; addr < text_end; addr += SIZEOF_INST) { // Read the instruction auto inst = *reinterpret_cast(addr); if (inst != SVC) { continue; } // We found a syscall instruction at: `addr` } ``` For the second point, on a syscall instruction, we have to patch the syscall with a branch. To do so, Frida's `gum_memory_patch_code` is pretty convenient: ```cpp void* svc_addr = /* Address of the syscall to patch */ gum_memory_patch_code(svc_addr, /* sizeof an arm64 inst */ 4, [] (void* addr, void*) { GumArm64Writer* writer = gum_arm64_writer_new(addr); /* Transform a SVC #0x80 into BL #AABBCC */ gum_arm64_writer_put_bl_imm(writer, 0xAABBCC); }, nullptr); ); ``` The pending question is where to branch the new `BL` instruction instead of `0xAABBCC`? Ideally, we would like to jump on our own dedicated stub: ```cpp void handler() { // ... } { // ... gum_arm64_writer_put_bl_imm(writer, &handler); } ``` But, the `bl #imm` instruction only accepts an immediate value in the range of `]-0x8000000; 0x8000000[`. This range might be too narrow to encode our absolute pointer `&handler`. > **Note** The BL instruction encodes the **signed** `#imm` as a multiple of 4 on 26 bits. Thus, and because of the sign bit, this `#imm` can range from: `±1 << (26 + 2 - 1);` We can actually bypass this restriction by using a *trampoline* located **in the library** where the RASP checks occur. It is quite common for large binary to find small functions with one or two instructions that are not likely or rarely used:
![Small C++ vtable function](imgs/small_func_1.webp)
Figure 12 - Small C++ vtable function

![Small C++ vtable function](imgs/small_func_2.webp)
Figure 13 - Small C++ vtable function

The idea is to use one of these functions as a placeholder to write **two instructions** which enables to branch an **absolute address**: ```asm LDR x15, =&handler BR x15 ``` Since this placeholder function is located within the library where the syscalls take place, we can `BL #imm` to this function without risking too much that `#imm` overflows the range `]-0x8000000; 0x8000000[`.
![Technical diagram](svg/svc_hook_1.svg)
Fig 14. Syscall Patch
Now that we found a mechanism to redirect the syscall instruction, we can focus on the `handler` function which aims at welcoming the syscall's redirection. First, the `SVC` instructions are *atomic* which means that our `handler` function must take care of not corrupting the values of the registers. In particular, `handler` can't follow the ARM64 calling convention. If we consider the following instructions: ```asm mov x6, #0 ... svc #0x80 ... mov x2, x6 ``` `svc #0x80` does not corrupt `x6` while this code: ```asm mov x6, #0 ... BL #imm ... mov x2, x6 ``` Could corrupt `x6` according to the ARM64 calling convention. Therefore, our `handler()` function must **really** mimic an interruption and take care of correctly saving/restoring the registers. In other words, we must write a small assembly stub to save and restore the registers[^asm-stub] ```asm stp x0, x1, [sp, -16]! ... stp x28, x29, [sp, -16]! stp x30, xzr, [sp, -16]! mov x0, sp bl _syscall_handler; ldp x30, xzr, [sp], 16 ldp x28, x29, [sp], 16 ... ldp xzr, x1, [sp], 16 ret ``` The `syscall_handler` function takes a pointer to the stack frame as a parameter. Thus, we can access the saved registers: ```cpp extern "C" { uintptr_t syscall_handler(uintptr_t* sp) { uintptr_t x16 = sp[14]; // Syscall number return -1; } } ``` > **Note** Apple prefixes (or mangles) symbols with a **`_`** this is why `syscall_handler` is referenced by `_syscall_handler` in the assembly code. Given our `syscall_handler` function, we have access to the original AArch64 registers such as we can access the syscall number and its parameters. We are also able to modify the return value since the original syscall is replaced by a branch.
![Technical diagram](svg/svc_hook.svg)

Fig 14. Syscall Redirection

A PoC that wraps all this logic will be published on GitHub. ## Conclusion Whilst this application uses the same obfuscator as in the previous blog post, it was configured with multi-layered code obfuscation which includes control-flow flattening and MBA. In addition, the RASP checks are also configured to crash the application instead of calling a callback function and displaying a message. These improvements in the configuration of the obfuscator make the reverse engineering of the application harder compared to the previous SingPass application. This blog post also detailed a new AArch64-generic technique to intercept RASP syscalls which resulted in a successful bypass of the RASP checks. This technique should also apply to Android AArch64. This is the last part of this series about iOS obfuscation. As I said in the first disclaimer, the obfuscator used for protecting these applications is and remains a good choice to protect assets from reverse engineering. [^graph]: The graph is more convenient to explore if JavaScript is enabled. [^asm-stub]: We don't restore `x0` as we want to change the return value from `_syscall_handler`. --- # Part 1 – SingPass RASP Analysis - Canonical: https://www.romainthomas.fr/post/22-08-singpass-rasp-analysis/ - Markdown: https://www.romainthomas.fr/post/22-08-singpass-rasp-analysis/index.md - Section: post - Published: 2022-08-29 - Modified: 2026-08-04 - Tags: ios, reverse engineering, obfuscation > This first blog post introduces the RASP checks used in SingPass ## Introduction I started to dig into the [SingPass](https://apps.apple.com/sg/app/singpass/id1340660807) application which turned out to be obfuscated and protected with Runtime Application Self-Protection (RASP). Retrospectively, this application is pretty interesting to analyze RASP functionalities since: 1. It embeds advanced RASP functionalities (Jailbreak detection, Frida Stalker detection, ...). 2. The native code is *lightly* obfuscated. 3. The application starts by showing an error message which is a good *oracle* to know whether we managed to circumvent the RASP detections.
![Technical diagram](svg/splash_screen.svg)
> **Note** All the findings and the details of this blog post has been shared with the editor of the obfuscator. The overall results have also been shared with SingPass. In addition, SingPass is part of a bug bounty program on HackerOne. Bypassing these RASP checks are a prerequisite to go further in the security assessment of this application. By grepping some keywords in the install directory of the application, we actually get two results which reveal the name of the obfuscator: ```text iPhone:/private/[...]/SingPass.app root# grep -Ri +++++++ * Binary file Frameworks/NuDetectSDK.framework/NuDetectSDK matches Binary file SingPass matches ``` The NuDetectSDK binary also uses the same obfuscator but it does not seem involved in the early jailbreak detection shown in the previous figure. On the other hand, `SingPass` is the main binary of the application and we can observe strings related to threat detections: ```text $ SingPass.app strings ./SingPass|grep -i +++++++ +++++++ThreatLogAPI(headers:) +++++++CallbackHandler(context:) ``` > **Note** For those who would like to follow this blog post with the original binary, you can download the decrypted SingPass Mach-O binary [from the research files](bin/SingPass). The name of the obfuscator has been redacted but it does not impact the content of the code. Unfortunately, the binary does not leak other strings that could help to identify where and how the application detects jailbroken devices but fortunately, the application does not crash ... If we assume that the obfuscator decrypts strings at runtime, we can try to dump the content of the `__data` section when the error message is displayed. At this point of the execution, the strings used for detecting jailbroken devices are likely decoded and clearly present in the memory. > **Note** This is actually quite the same technique used in **PokemonGO**: [*What About LIEF*](/post/21-07-pokemongo-anti-frida-jailbreak-bypass#what-about-lief) 1. We run the application and we wait for the *jailbreak* message 2. We attach to SingPass with Frida and we inject a library that: - Parses in-memory the `SingPass` binary (thanks to LIEF) - Dumps the content of the `__data` section - Write the dump in the iPhone's `/tmp` directory Once the data section is dumped, we end up with the following changes in some parts of the `__data` section:
![Technical diagram](svg/01-data-encrypted.svg)

![Technical diagram](svg/01-data-clear.svg)
Fig 1. Slices of the __data section before and after the dump
> **Note** The string encoding routines will be analyzed in the **second** part of this series of blog posts In addition, we can observe the following strings which seem to be related to the RASP functionalities of the obfuscator:
![Technical diagram](svg/ixg-data-redacted.svg)
Fig 2. Strings Related to the RASP Features


All the `EVT_*` strings are referenced by one **and only one** function that I named on_rasp_detection. This function turns out to be the threat detection callback used by the app's developers to perform action(s) when a RASP event is triggered. To better understand the logic of the checks behind these strings, let's start with `EVT_CODE_PROLOGUE` which is used to detect hooked functions. ## EVT_CODE_PROLOGUE: Hook Detection While going through the assembly code closes to the cross-references of on_rasp_detection, we can spot several times this pattern:
![Technical diagram](svg/graph_ixg_code_prologue.svg)

To detect if a given function is hooked, the obfuscator loads the **first byte** of the function and compares this byte with the value `0xFF`. `0xFF` might seem -- at first glance -- arbitrary but it's not. Actually, regular functions start with a prologue that allocates space on stack for saving registers defined by the calling convention and stack variables required by the function. In AArch64, this allocation can be performed in two ways: ```asm stp REG, REG, [SP, 0xAA]! ; or sub SP, SP, 0xBB stp REG, REG, [SP, 0xCC] ``` These instructions are **not equivalent**, but somehow and with the good offsets, they could lead to the same result. In the second case, the instruction `sub SP, SP, #CST` is encoded with the following bytes:
![Technical diagram](/img/stockholm/Code/Code.svg) 0xff ** 0x00 0xd1


As we can see, the encoding of this instruction starts with `0xFF`. If it is not the case, then either the function starts with a different stack-allocation prologue or potentially starts with a hooking trampoline. Since the code of the application is compiled through obfuscator's compiler, the compiler is able to distinguish these two cases and insert the right check for the correct function's prologue. If the first byte of the instruction of the function does not pass the check, it jumps to the red basic block. The purpose of this basic block is to trigger a user-defined callback that will process the detection according to the application's design and the developers' choices: - Printing an error - Crashing the application - Corrupting internal data - ... From the previous figure, we can observe that the detection callback is loaded from **a static variable** located at #hook_detect_cbk_ptr. When calling this detection callback, the obfuscator provides the following information to the callback: 1. A detection code: 0x400 for EVT_CODE_PROLOGUE 2. A *corrupted* pointer which could be used to crash the application. Let's now take a closer look at the design of the detection callback(s) as a whole. ## Detection Callbacks As explained in the previous section, when the obfuscator detects tampering, it reacts by calling a detection callback stored in the **static variable** at the address: 0x10109D760
![Technical diagram](svg/data_hook_callback.svg)

By statically analyzing hook_detect_cbk, the implementation seems to corrupt the pointer provided in the callback's parameters. On the other hand, when running the application we observe a jailbreak detection message and not a crash of the application. If we look at the cross-references which read or write at this address, we get this list of instructions:
![Technical diagram](svg/data_hook_xref.svg)

So actually **only one** instruction -- init_and_check_rasp+01BC -- is **overwriting** the *default* detection callback with another function:
![Technical diagram](svg/code-callback-override.svg)

Compared to the default callback: hook_detect_cbk, the overridden function, hook_detect_cbk_user_def does not corrupt a pointer that would make the application crash. Instead, it calls the function on_rasp_detection which references all the strings `EVT_CODE_TRACING`, `EVT_CODE_SYSTEM_LIB`, etc, listed in the [figure 2](#fig-2). > **Note** `hook_detect_cbk_user_def` is called on a RASP event. That's why this application does not crash. By looking at the function init_and_check_rasp as a whole, we can notice that the X23 register is also used to initialize other static variables:
![Technical diagram](svg/str_x23.svg)
Fig 3. X23 Writes Instructions


These memory writes mean that the callback hook_detect_cbk_user_def is used to initialize other static variables. In particular, these other static variables are likely used for the other RASP checks. By looking at the cross-references of these static variables #EVT_CODE_TRACING_cbk_ptr, #EVT_ENV_JAILBREAK_cbk_ptr etc, we can locate where the other RASP checks are performed and under which conditions they are triggered. ### EVT_CODE_SYSTEM_LIB
![Technical diagram](svg/ixg_code_system_lib.svg)
### EVT_ENV_DEBUGGER
![Technical diagram](svg/ixg_env_debugger.svg)
### EVT_ENV_JAILBREAK
![Technical diagram](svg/ixg_env_jailbreak.svg)
Thanks to the #EVT_\* cross-references, we can go **statically** through all the basic blocks that use these #EVT_\* variables and highlight the underlying checks that could trigger the RASP callback(s). Before detailing the checks, it is worth mentioning the following points: 1. Whilst the application uses a commercial obfuscator which provides native code obfuscation in addition to RASP, the code is **lightly obfuscated** which makes static assembly code analysis doable very easily. 2. As it will be discussed in ["*RASP Weaknesses*"](#rasp-design-weaknesses), the application setups the **same callback** for **all** the RASP events. Thus, it eases the RASP bypass and the dynamic analysis of the application. ## Anti-Debug The version of the obfuscator used by SingPass implements two kinds of debug check. First, it checks if the parent process id (`ppid`) is the same as `/sbin/launchd` which should be **1**. ```cpp static constexpr pid_t LAUNCHD_PID = 1; pid_t ppid = getppid(); if (ppid != LAUNCHD_PID) { // Trigger EVT_ENV_DEBUGGER } ``` > **Note** `getppid` is called either through a function or with a syscall. If it is not the case, it triggers the `EVT_ENV_DEBUGGER` event. The second check is based on `sysctl` which is used to access the `extern_proc.p_flag` value. If this flag contains the `P_TRACED` value, the RASP routine triggers the `EVT_ENV_DEBUGGER` event. ```cpp int names[] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid(), }; kinfo_proc info; int sizeof_info = sizeof(kinfo_proc); int ret = sysctl(names, 4, &info, &sizeof_info, nullptr, nullptr); if (info.kp_proc.p_flag & P_TRACED) { // Trigger EVT_ENV_DEBUGGER } ``` In the SingPass binary, we can find an instance of these two checks in the following ranges of addresses: > **Note** ppid:   0x10071F420 -- 0x10071F474
sysctl: 0x100151668 -- 0x100151730 ## Jailbreak Detection As for most of the jailbreak detections, the obfuscator tries to detect if the device is jailbroken by checking if some files exist (or not) on the device. Files or directories are checked with syscalls or a regular functions thanks to the following *helpers*: > **Note**
pathconf:    0x100008EB0 -- 0x100008F28
utimes:      0x10000D8D4 -- 0x10000D948
stat:        0x100012188 -- 0x10001221C
open:        0x10002D478 -- 0x10002D4D8
fopen:       0x1000474E4 -- 0x100047554
stat64:      0x10006AA30 -- 0x10006AAD8
getfsstat64: 0x10047E82C -- 0x10047E914
While in the introduction, I mentioned that a dump of the section `__data` reveals strings related to jailbreak detection, the dump does not reveal **all the strings** used by the obfuscator. By looking closely at the strings encoding mechanism, it turns out that some strings are decoded *just-in-time* in a *temporary variable*. I'll explain the strings encoding mechanism in the second part of this series of blog posts but at this point, we can uncover the strings by setting hooks on functions like `fopen`, `utimes` and dumping the `__data` section right after these calls. Then, we can iterate over the different dumps to see if new strings appear. ```text $ python dump_analysis.py Processing __data_0.raw 0x01010b935c h/.installed_unc0ver 0x01010b986a w/taurine/pspawn_payload.dylib Processing __data_392.raw 0x01010b910e y__TEXT 0x01010b91b3 /System/Library/dyld/dyld_shared_cache_arm64e 0x01010b9174 /System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e 0x01010b9136 /System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64 0x01010b9126 dyld_v1 arm64e 0x01010b9116 dyld_v1 arm64 Processing __data_393.raw 0x01010afb90 /Users/xxxxxxx/Desktop/Xcode/ndi-sp-mobile-ios-swift/SingPass/[...] 0x01010b942c /var/jb 0x01010af910 https://bio-stream.singpass.gov.sg 0x01010af6a0 https://api.myinfo.gov.sg/spm/v3 0x01010b93b0 /.mount_rw ``` In the end, the approach does not enable having all the strings decoded but it enables to have a *good coverage*. The list of the files used for detecting jailbreak is given the [Annexes](#annexes). There is also a particular check for detecting the `unc0ver` jailbreak which consists in trying to unmount `/.installed_unc0ver`: ```cpp 0x100E4D814: _unmount("/.installed_unc0ver") ``` ## Environment The obfuscator also checks environment variables that trigger the `EVT_ENV_JAILBREAK` event. Some of these checks seem to be related to code lifting detection while still triggering the `EVT_ENV_JAILBREAK` event. ```cpp if (strncmp(_dyld_get_image_name(0), "/private/var/folders", 0x14)) { -> Trigger EVT_ENV_JAILBREAK } ``` ```cpp if (strncmp(getenv("HOME"), "/Users", 6) == 0) { -> Trigger EVT_ENV_JAILBREAK } ``` ```cpp if (strncmp(getenv("HOME"), "mobile", 6) != 0) { -> Trigger EVT_ENV_JAILBREAK } ``` ```cpp char buffer[0x400]; size_t buff_size = 0x400; _NSGetExecutablePath(buffer, &buff_size); if (buffer.startswith("/private/var/folders")) { -> Trigger EVT_ENV_JAILBREAK } ``` > **Note** From a reverse engineering perspective, startswith() is actually implemented as a succession of xor that are "or-ed" to get a boolean. This might be the result of an optimization from the compiler. You can observe this pattern in the basic block located at the address: 0x100015684. ## Advanced Detections In addition to regular checks, the obfuscator performs advanced checks like verifying the current status of the SIP (**S**ystem **I**ntegrity **P**rotection), and more precisely, the KEXTS code signing status. > **Note** From my weak experience in iOS jailbreaking, I think that no Jailbreak disables the `CSR_ALLOW_UNTRUSTED_KEXTS` flag. Instead, I guess that it is used to detect if the application is running on an Apple M1 which allows such deactivation. ```cpp csr_config_t buffer = 0; if (__csrctl(CSR_ALLOW_UNTRUSTED_KEXTS, buffer, sizeof(csr_config_t)) { /* * SIP is disabled with CSR_ALLOW_UNTRUSTED_KEXTS * -> Trigger EVT_ENV_JAILBREAK */ } ``` > **Note** Assembly range: 0x100004640 -- 0x1000046B8 The obfuscator also uses the Sandbox API to verify if some paths exist: ```cpp int ret = __mac_syscall("Sandbox", /* Sandbox Check */ 2, getpid(), "file-test-existence", SANDBOX_FILTER_PATH, "/opt/homebrew/bin/brew"); ``` The paths checked through this API are OSX-related directories, so I guess it is also used to verify that the current code has not been lifted on an Apple Silicon. Here is, for instance, a list of directories checked with the *Sandbox* API: ```text /Applications/Xcode.app/Contents/MacOS/Xcode /System/iOSSupport/ /opt/homebrew/bin/brew /usr/local/bin/brew ``` > **Note** Assembly range: 0x100ED7684 (function) In addition, it uses the Sandbox attribute `file-read-metadata` as an alternative to the `stat()` function. > **Note** Assembly range: 0x1000ECA5C -- 0x1000ECE54 The application uses the sandbox API through private syscalls to determine whether some jailbreak artifacts exists. This is very smart but I guess it's not really compliant with the Apple policy. ## Code Symbol Table The purpose of this check is to verify that the addresses of the resolved imports point to the right library. In other words, this check verifies that the import table is not tampered with pointers that could be used to hook imported functions. > **Note** Initialization: part of sub_100E544E8 > **Note** Assembly range: 0x100016FC4 -- 0x100017024 During the RASP checks initialization (`sub_100E544E8`), the obfuscator **manually** resolves the imported functions. It iterates over the symbols in the `SingPass` binary and checks which library imports each symbol. It then accesses that library's in-memory `__LINKEDIT` segment and parses the exports trie. This manual resolution fills a table that contains the **absolute address** of the resolved symbols. In addition, the initialization routine setups -- what I called -- a metadata structure that follows this layout:
![Technical diagram](svg/ixg_symbol_table_metadata.svg)

symbols_index is a kind of translation table that converts an index known by the obfuscator into an index in the `__got` or the `__la_symbol_ptr` section. The index's origin (i.e `__got` or `__la_symbol_ptr`) is determined by the origins table which contains enum-like integers: ```cpp enum SYM_ORIGINS : uint8_t { NONE = 0, LA_SYMBOL = 1, GOT = 2, }; ``` The length of both tables: symbols_index and origins, is defined by the static variable nb_symbols which is set to 0x399. The metadata structure is followed by two pointers: `resolved_la_syms` and `resolved_got_syms` which point to the imports address table manually filled by the obfuscator. > **Note** There is a dedicated table for each section: `__got` and `__la_symbol_ptr`. Then, macho_la_syms points to the beginning of the `__la_symbol_ptr` section while macho_got_syms points to the `__got` section. Finally, stub_helper_start / stub_helper_end holds the memory range of the `__stub_helper` section. I'll describe the purpose of these values later. All the values of this metadata structure are set during the initialization which takes place in the function `sub_100E544E8`. In different places of the `SingPass` binary, the obfuscator uses this metadata information to verify the integrity of the resolved import(s). It starts by accessing the symbols_index and the origins with a fixed value:
![Technical diagram](svg/ixg_symbol_table_index_1.svg)

> **Note** Since the `symbols_index` table contains `uint32_t` values, `#0xCA8` matches `#0x32A` (index for the origins table) when divided by `sizeof(uint32_t)`: `0xCA8 = 0x32A * sizeof(uint32_t)` In other words, we have the following operations: ```cpp const uint32_t sym_idx = metadata.symbols_index[0x32a]; const SYM_ORIGINS origin = metadata.origins[0x32a] ``` Then, given the `sym_idx` value and depending on the origin of the symbol, the function accesses either the resolved `__got` table or the resolved `__la_symbol_ptr` table. This access is done with a helper function located at `sub_100ED6CC0`. It can be summed up with the following pseudo-code: ```cpp uintptr_t* section_ptr = nullptr; uintptr_t* manually_resolved = nullptr; if (origin == /* 1 */ SYM_ORIGINS::LA_SYMBOL) { section_ptr = metadata.macho_la_syms; manually_resolved = metadata.resolved_la_syms; } else if (origin == /* 2 */ SYM_ORIGINS::GOT) { section_ptr = metadata.macho_got_syms; manually_resolved = metadata.resolved_got_syms; } ``` The entries at the index `sym_idx` of `section_ptr` and `manually_resolved` are compared and if they don't match, the event #EVT_CODE_SYMBOL_TABLE is triggered. Actually, the comparison covers different cases. First, the obfuscator handles the case where the symbol at `sym_idx` is not yet resolved. In that case, `section_ptr[sym_idx]` points to the symbols resolution stub located in the section `__stub_helper`. That's why the `metadata` structure contains the memory range of this section: ```cpp const uintptr_t addr_from_section = section_ptr[sym_idx]; if (metadata.stub_helper_start <= addr && addr < metadata.stub_helper_end) { // Skip } ``` In addition, if the pointers do not match, the function verifies their location using `dladdr`: ```cpp const uintptr_t addr_from_section = section_ptr[sym_idx]; const uintptr_t addr_from_resolution = manually_resolved[sym_idx]; if (addr_from_section != addr_from_resolution) { Dl_info info_section; Dl_info info_resolution; dl_info(addr_from_section, &info_section); dl_info(addr_from_resolution, &info_resolution); if (info_section.dli_fbase != info_resolution.dli_fbase) { // --> Trigger EVT_CODE_SYMBOL_TABLE; } } ``` > **Note** Two pointers might not match if, for instance, an imported function is hooked with Frida. In the case where the `origin[sym_idx]` is set to `SYM_ORIGINS::NONE` the function skips the check. Thus, we can simply disable this RASP check by filling the original table with 0. The number of symbols is close to the metadata structure and the address of the metadata structure is leaked by the `___atomic_load` and `___atomic_store` functions.
![Technical diagram](svg/ixg_code_symbol_table.svg)
## Code Tracing The *Code Tracing* check aims to verify that the current is not *traced*. By looking at the cross-references of #EVT_CODE_TRACING_cbk_ptr, we can identify two kinds of verification. ### GumExecCtx EVT_CODE_TRACING seems able to **detect** if the **Frida's Stalker** is running. It's the first time I can observe this kind of check and it's very smart. For those who would like to follow this analysis with the raw assembly code, I will use this range of addresses from the [SingPass](bin/SingPass) binary: > **Note** 0x10019B6FC -- 0x10019B82C Here is the graph of the function that performs the Frida Stalker check:
![Technical diagram](svg/frida-stalker.svg)
Code associated with Frida Stalker Detection
Yes, this code is able to detect the Stalker. How? Let's start with the first basic block. _pthread_mach_thread_np(_pthread_self()) aims at getting the thread id of the function that invokes this check. Then more subtly, MRS(TPIDRRO_EL0) & #-8 is used to **manually** access the thread local storage area. On ARM64, Apple uses the least significant byte of `TPIDRRO_EL0` to store the number of CPU while the MSB contains the TLS base address. > **Note** See also: [dyld -- threadLocalHelpers.s](https://github.com/apple-oss-distributions/dyld/blob/5c9192436bb195e7a8fe61f22a229ee3d30d8222/libdyld/threadLocalHelpers.s#L237-L238) Then, the second basic block -- which is the loop's entry -- accesses the thread local variable with the *key* `tlv_idx` which ranges from `0x100` to `0x200` in the loop:
*(tlv_table + (tlv_idx << 3))

The following basic block which calls _vm_region_64(...) is used to verify that the `tlv_addr` variable contains a valid address with a *correct* size (i.e. larger than `0x30`). Under these conditions, it jumps into the following basic block with these *strange* memory accesses:
![Technical diagram](svg/frida-stalker-cond.svg)
Condition that (somehow) Triggers EVT_CODE_TRACING
To figure out the meaning of these memory accesses, let's remind that this function is associated with the `EVT_CODE_TRACING` event. Which well-known public tool could be associated with code tracing? Without too much risk, we can assume the Frida's Stalker. If we look at the implementation of the Stalker, we can notice this kind of initialization (in `gumstalker-arm64.c`): ```cpp void gum_stalker_init (GumStalker* self) { [...] self->exec_ctx = gum_tls_key_new(); [...] } void* _gum_stalker_do_follow_me(GumStalker* self, ...) { GumExecCtx* ctx = gum_stalker_create_exec_ctx(...); gum_tls_key_set_value (self->exec_ctx, ctx); } ``` So the Stalker creates a thread local variable that is used to store a pointer to the `GumExecCtx` structure which has the following layout: ```cpp struct _GumExecCtx { volatile gint state; gint64 destroy_pending_since; GumStalker * stalker; GumThreadId thread_id; GumArm64Writer code_writer; GumArm64Relocator relocator; [...] } ``` If we add the offsets of this layout and if we *virtually* inline the `GumArm64Writer` structure, we can get this representation: ```cpp struct _GumExecCtx { /* 0x00 */ volatile gint state; /* 0x08 */ gint64 destroy_pending_since; /* 0x10 */ GumStalker * stalker; /* 0x18 */ GumThreadId thread_id; GumArm64Writer code_writer { /* 0x20 */ volatile gint ref_count; /* 0x24 */ GumOS target_os; /* 0x28 */ GumPtrauthSupport ptrauth_support; ... }; } ``` > **Note** `destroy_pending_since` is located at the offset **`0x08`** and not **`0x04`** because of the alignment enforced by the compiler. With this representation, we can observe that: - *(tlv_table + 0x18) effectively matches the `GumThreadId thread_id` attribute. - *(tlv_table + 0x24) matches `GumOS target_os` - *(tlv_table + 0x28) matches `GumPtrauthSupport ptrauth_support` `GumOS` and `GumPtrauthSupport` are enums defined in `gumdefs.h` and `gummemory.h` with these values: ```cpp enum _GumOS { GUM_OS_WINDOWS, GUM_OS_MACOS, GUM_OS_LINUX, GUM_OS_IOS, GUM_OS_ANDROID, GUM_OS_QNX }; enum _GumPtrauthSupport { GUM_PTRAUTH_INVALID, GUM_PTRAUTH_UNSUPPORTED, GUM_PTRAUTH_SUPPORTED }; ``` `GumOS` contains 6 entries starting from `GUM_OS_WINDOWS = 0` up to `GUM_OS_QNX = 5` and similarly, `GUM_PTRAUTH_INVALID = 0` while the last entry is associated with `GUM_PTRAUTH_SUPPORTED = 2` Therefore, the previous *strange* conditions are used to fingerprint the `GumExecCtx` structure:
![Technical diagram](svg/tlv_resolved.svg)

One way to prevent this Stalker detection would be to recompile Frida with swapped fields in the `_GumExecCtx` structure. ### Thread Check An alternative to the previous Frida stalker check consists in accessing the current thread status through the following call: ```cpp thread_read_t target = pthread_mach_thread_np(pthread_self()); uint32_t count = ARM_UNIFIED_THREAD_STATE_COUNT; arm_unified_thread_state state; thread_get_state(target, ARM_UNIFIED_THREAD_STATE, &state, &count); ``` Then, it checks if `state->ts_64.__pc` is within the `libsystem_kernel.dylib` thanks to the following comparison: ```cpp const auto mach_msg_addr = reinterpret_cast(&mach_msg); const uintptr_t delta = abs(state->ts_64.__pc - mach_msg_addr) if (delta > 0x4000) { rasp_event_info info; info.event = 0x2000; // EVT_CODE_TRACING; info.ptr = (uintptr_t*)0x13b71a24724edfe; EVT_CODE_TRACING_cbk_ptr(info); } ``` In other words, `state->ts_64.__pc` is considered to be in `libsystem_kernel.dylib`, if its distance from `&mach_msg` is smaller than `0x4000`. At first sight, I was a bit confused by this RASP check but since the previous checks, associated with EVT_CODE_TRACING, aims at detecting the Frida Stalker, this check is also likely designed to detect the Frida Stalker. To confirm this hypothesis, I developed a small test case that reproduces this check, in a standalone binary and we can observe a difference depending on whether it runs through the Frida stalker or not:
![Technical diagram](svg/with_stalker.svg)
Output of the Test Case with the Stalker
![Technical diagram](svg/without_stalker.svg)
Output of the Test Case without the Stalker
This check can be bypassed without too much difficulty by using the function [`gum_stalker_exclude`](https://github.com/oleavr/frida-gum/blob/b679d454b1f323fa9c181f324ec17d515a7c2f81/gum/gumstalker.h#L62-L63) to exclude the library `libsystem_kernel.dylib` from the stalker: ```cpp GumStalker* stalker = gum_stalker_new(); exclude(stalker, "libsystem_kernel.dylib"); { // Stalker Check } ``` As a result of this exclusion, `state->ts_64.__pc` is located in `libsystem_kernel.dylib`:
![Technical diagram](svg/stalker_bypass.svg)
Output of the Test Case with Excluded Memory Ranges
## App Loaded Libraries The RASP event EVT_APP_LOADED_LIBRARIES aims at checking the integrity of the Mach-O's dependencies. In other words, it checks that the Mach-O imported libraries have not been altered. > **Note** Assembly ranges: 0x100E4CDF8 -- 0x100e4d39c The code associated with this check starts by accessing the Mach-O header thanks to the `dladdr` function: ```cpp Dl_info dl_info; dladdr(&static_var, &dl_info); ``` `Dl_info` contains the base address of the library which encompasses the address provided in the first parameter and since, a Mach-O binary is loaded with its header, `dl_info.dli_fbase` actually points to a `mach_header_64`. Then the function iterates over the `LC_ID_DYLIB`-like commands to access dependency's name:
![Technical diagram](svg/app_loaded_library.svg)

This name contains the path to the dependency. For instance, we can access this list as follows: ```python import lief singpass = lief.parse("./SingPass") for lib in singpass.libraries: print(lib.name) # Output: /System/Library/Frameworks/AVFoundation.framework/AVFoundation /System/Library/Frameworks/AVKit.framework/AVKit ... @rpath/leveldb.framework/leveldb @rpath/nanopb.framework/nanopb ``` The dependency's names are used to fill a hash table in which a hash value in encoded on 32 bits: ```cpp // Pseudo code uint32_t TABLE[0x6d] for (size_t i = 0; i < 0x6d; ++i) { TABLE[i] = hash(lib_names[i]); } ``` Later in the code, this computed table is compared with another hash table -- **hard-coded in the code** -- which looks like this:
![Technical diagram](svg/hash_lib_loaded.svg)
Fig 4. Examples of Hashes
If some libraries have been modified to inject, for instance, `FridaGadget.dylib` then the hash **dynamically** computed will not match the hash hard-coded in the code. Whilst the implementation of this check is pretty *"standard"*, there are a few points worth mentioning: - Firstly, the hash function seems be a derivation of the [MurmurHash](https://en.wikipedia.org/wiki/MurmurHash). - Secondly, the hash is encoded on **32 bits** but the code in the [Figure 4](#fig-4) references the X11/X12 registers which are 64 bits. This is actually a compiler optimization to limit the number of memory accesses. - Thirdly, the hard coded hash values are duplicated in the binary for each instance of the check. In SingPass, this RASP check is present twice thus, we find these values at the following locations: `0x100E4CF38`, `0x100E55678`. This duplication is likely used to prevent a single spot location that would be easy to patch. ## Code System Lib This check is associated with the event EVT_CODE_SYSTEM_LIB which consists in verifying the integrity of the **in-memory** system libraries with their content in the dyld shared cache (**on-disk**). > **Note** Assembly ranges: 0x100ED5BF8 -- 0x100ED5D6C and 0x100ED5E0C -- 0x100ED62D4 This check usually starts with the following pattern:
![Technical diagram](svg/ixg_code_system_lib_prologue.svg)

If the result of `iterate_system_region` with the given `check_region_cbk` callback is not 0, it triggers the EVT_CODE_SYSTEM_LIB event: ```cpp if (iterate_system_region(check_region_cbk) != 0) { // Trigger `EVT_CODE_SYSTEM_LIB` } ``` To understand the logic behind this check, we need to understand the purpose of the `iterate_system_region` function and its relationship with the callback `check_region_cbk`. ### iterate_system_region > **Note** As for all the functions referenced in the blog post, their names come from my own analysis and might be inaccurate. Most of the functions related to the RASP checks were obviously stripped. In this case, `iterate_system_region` matches the original *`sub_100ED5BF8`* This function aims to call the system function `vm_region_recurse_64` and then, filter its output on conditions that could trigger the callback given in the first parameter: check_region_cbk. iterate_system_region starts by accessing the base address of the dyld shared cache thanks to the SYS_shared_region_check_np syscall. This address is used to read and **memoize** a few attributes from the `dyld_cache_header` structure: 1. The shared cache header 2. The shared cache end address 3. Other limits related to the shared cache The following snippet gives an overview of these computations: ```cpp static dyld_shared_cache* header = nullptr; /* At: 0x1010DE940 */ static uintptr_t g_shared_cache_end; /* At: 0x1010DE948 */ static uintptr_t g_overflow_address; /* At: 0x1010DE950 */ static uintptr_t g_module_last_addr; /* At: 0x1010DE958 */ if (header == nullptr) { // return; } uintptr_t shared_cache_base; syscall(SYS_shared_region_check_np, &shared_cache_base); header = shared_cache_base; g_shared_cache_end = shared_cache_addr + header->mappings[0].size; g_overflow_address = -1; g_module_last_addr = g_shared_cache_end; if (header->imagesTextCount > 0) { uintptr_t slide = shared_cache_addr - header->mappings[0].address; uintptr_t tmp_overflow_address = -1; uintptr_t shared_cache_end_tmp = shared_cache_end; for (size_t i = 0; i < header->imagesTextCount; ++i) { const uintptr_t txt_start_addr = slide + header->imagesText[i].loadAddress; const uintptr_t txt_end_addr = start_addr + header->imagesText[i].textSegmentSize; if (txt_start_addr >= shared_cache_end_tmp && txt_start_addr < tmp_overflow_address) { g_overflow_address = start_addr; tmp_overflow_address = start_addr; } if (txt_end_addr >= shared_cache_end_tmp) { g_module_last_addr = txt_end_addr; shared_cache_end_tmp = txt_end_addr; } } } ``` > **Note** From a reverse engineering point of view, the stack variable used to memoize these information is aliased with the parameter `info` of `vm_region_recurse_64` that is called later. I don't know if this aliasing is on purpose, but it makes the reverse engineering of the structures a bit more complicated. Following this memoization, there is a loop on `vm_region_recurse_64` which queries the `vm_region_submap_info_64` information for these addresses in the range of the dyld shared cache. We can identify the type of the query (`vm_region_submap_info_64`) thanks to the `mach_msg_type_number_t *infoCnt` argument which is set to `19`:
![Technical diagram](svg/ixg_code_system_lib_call2vmregion.svg)

This loop breaks under certain conditions and the callback is triggered with other conditions. As it is explained a bit later, the callback verifies the in-memory integrity of the library present in the dyld shared cache. The verification and the logic behind this check is prone to take time, that's why the authors of the check took care of filtering the addresses to check to avoid useless (heavy) computations. Basically, the callback that performs the in-depth inspection of the shared cache is triggered if:
![Technical diagram](svg/ixg_code_system_lib_callback_cond.svg)
#### check_region_cbk When the conditions are met, iterate_system_region calls the check_region_cbk with the suspicious address in the first parameter: ```cpp int iterate_system_region(callback_t cbk) { int ret = 0; if (cond(address)) { ret = cbk(address) { // Checks on the dyld_shared_cache } } return ret; } ``` During the analysis of SingPass, only **one** callback is used in pair with iterate_system_region, and its code is not **especially obfuscated** (except the strings). Once we know that the checks are related to the dyld shared cache, we can quite easily figure out the structures involved in this function. This callback is located at the address `0x100ed5e0c` and renamed check_region_cbk. Firstly, it starts by accessing the information about the address: ```cpp int check_region_cbk(uintptr_t address) { Dl_info info; dladdr(address, info); // ... } ``` This information is used to read the content of the `__TEXT` segment associated with the `address` parameter: ```cpp auto* header = reinterpret_cast(info.dli_fbase); segment_command_64 __TEXT = get_text_segment(header); vm_offset_t data = 0; mach_msg_type_number_t* dataCnt = 0; vm_read(task_self_trap(), info.dli_fbase, __TEXT.vmsize, &data, &dataCnt); ``` > **Note** The `__TEXT` strings is encoded as well as the different paths of the shared cache like `/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e` and the header's magic values: `0x01010b9126: dyld_v1 arm64e` or `0x01010b9116: dyld_v1 arm64` On the other hand, the function opens the `dyld_shared_cache` and looks for the section of the shared cache that contains the library associated with the `address` parameter: ```cpp int fd = open('/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64'); (1) mmap(nullptr, 0x100000, VM_PROT_READ, MAP_NOCACHE | MAP_PRIVATE, fd, 0x0): 0x109680000 // Look for the shared cache entry associated with the provided address (2) mmap(nullptr, 0xad000, VM_PROT_READ, MAP_NOCACHE | MAP_PRIVATE, fd, 0x150a9000): 0x109681000 ``` The purpose of the second call to `mmap()` is to load the slice of the shared cache that contains the code of the library. Then, the function checks **byte per byte** that the `__TEXT` segment's content matches the in-memory content. The loop which performs this comparison is located between these addresses: `0x100ED6C58 - 0x100ED6C70`. ---- As we can observe from the description of this RASP check, the authors paid a lot of attention to avoid performance issues and memory overhead. On the other hand, the callback check_region_cbk was never called during my experimentations (even when I hooked system function). I don't know if it's because I misunderstood the conditions but in the end, I had to manually force the conditions (by forcing the `pages_swapped_out` to 1). > **Note** `vm_region_recurse_64` seems also always paired with an anti-hooking verification that is slightly different from the check described at the beginning of this blog post. Its analysis is quite easy and can be a good exercise. ## RASP Design Weaknesses Thanks to the different #EVT_\* static variables that hold function pointers, the obfuscator enables to have dedicated callbacks for the supported RASP events. Nevertheless, the function init_and_check_rasp defined by the application's developers initialize **all** these pointers **to the same** callback: hook_detect_cbk_user_def. In such a design, all the RASP events end up in a single function which weakens the strength of the different RASP checks. It means that we only have to target this function to disable or bypass the RASP checks. Using Frida Gum, the bypass is as simple as using `gum_interceptor_replace` with an empty function: ```cpp enum class RASP_EVENTS : uint32_t { EVT_ENV_JAILBREAK = 0x1, EVT_ENV_DEBUGGER = 0x2, EVT_APP_SIGNATURE = 0x20, EVT_APP_LOADED_LIBRARIES = 0x40, EVT_CODE_PROLOGUE = 0x400, EVT_CODE_SYMBOL_TABLE = 0x800, EVT_CODE_SYSTEM_LIB = 0x1000, EVT_CODE_TRACING = 0x2000, }; struct event_info_t { RASP_EVENTS event; uintptr_t** ptr_to_corrupt; }; void do_nothing(event_info_t info) { RASP_EVENTS evt = info.event; // ... return; } // This is **pseudo code** gum_interceptor_replace( listener->interceptor, reinterpret_cast(&hook_detect_cbk_user_def) do_nothing, reinterpret_cast(&hook_detect_cbk_user_def) ); ``` Thanks to this weakness, I could prevent the error message from being displayed as soon as the application starts.
SingPass Jailbreak & RASP Bypass
> **Note** It exists two other RASP checks: `EVT_APP_MACHO` and `EVT_APP_SIGNATURE` which were not enabled by the developers and thus, are not present in SingPass. ## Conclusion This first part is a good example of the challenges when using or designing an obfuscator with RASP features. On one hand, the commercial solution implements strong and advanced RASP functionalities with, for instance, inlined syscalls spread in different places of the application. On the other hand, the app's developers weakened the RASP functionalities by setting the **same callback** for all the events. In addition, it seems that the application **does not use the native code obfuscation** provided by the commercial solution which makes the RASP checks un-protected against static code analysis. It could be worth enforcing code obfuscation on these checks regardless the configuration provided by the user. From a developer's perspective, understanding the reverse-engineering impact of sharing one callback can be difficult, even when it is a sound architectural decision. In the second part of this series about iOS code obfuscation, we examine native code obfuscation in another application. This application reacts differently to RASP events, and its code uses MBA, control-flow flattening, and other obfuscation techniques. If you have questions feel free to ping me :mailbox:. ### Annexes | JB Detection Files | Listed in PokemonGO | |-------------------------------------------------|---------------------| | `/.bootstrapped` | No | | `/.installed_taurine` | No | | `/.mount_rw` | No | | `/Library/dpkg/lock` | No | | `/binpack` | **Yes** | | `/odyssey/cstmp` | No | | `/odyssey/jailbreakd` | No | | `/payload` | No | | `/payload.dylib` | No | | `/private/var/mobile/Library/Caches/kjc.loader` | No | | `/private/var/mobile/Library/Sileo` | No | | `/taurine` | No | | `/taurine/amfidebilitate` | No | | `/taurine/cstmp` | No | | `/taurine/jailbreakd` | No | | `/taurine/jbexec` | No | | `/taurine/launchjailbreak` | No | | `/taurine/pspawn_payload.dylib` | No | | `/var/dropbear` | No | | `/var/jb` | No | | `/var/lib/undecimus/apt` | No | | `/var/motd` | No | | `/var/tmp/cydia.log` | No | | Flagged Packages | |-------------------------------------------------------------------------| | `/Applications/AutoTouch.app/AutoTouch` | | `/Applications/iGameGod.app/iGameGod` | | `/Applications/zxtouch.app/zxtouch` | | `/Library/Activator/Listeners/me.autotouch.AutoTouch.ios8` | | `/Library/LaunchDaemons/com.rpetrich.rocketbootstrapd.plist` | | `/Library/LaunchDaemons/com.tigisoftware.filza.helper.plist` | | `/Library/MobileSubstrate/DynamicLibraries/ATTweak.dylib` | | `/Library/MobileSubstrate/DynamicLibraries/GameGod.dylib` | | `/Library/MobileSubstrate/DynamicLibraries/LocalIAPStore.dylib` | | `/Library/MobileSubstrate/DynamicLibraries/Satella.dylib` | | `/Library/MobileSubstrate/DynamicLibraries/iOSGodsiAPCracker.dylib` | | `/Library/MobileSubstrate/DynamicLibraries/pccontrol.dylib` | | `/Library/PreferenceBundles/SatellaPrefs.bundle/SatellaPrefs` | | `/Library/PreferenceBundles/iOSGodsiAPCracker.bundle/iOSGodsiAPCracker` | --- # A Journey in iOS App Obfuscation - Canonical: https://www.romainthomas.fr/post/22-08-ios-obfuscation/ - Markdown: https://www.romainthomas.fr/post/22-08-ios-obfuscation/index.md - Section: post - Published: 2022-08-22 - Modified: 2026-08-04 - Tags: ios, reverse engineering, obfuscation > This series of blog posts details how obfuscators can protect iOS applications from reverse engineering Back in July 2021, I took a look at the protections provided by Arxan to detect jailbroken devices in PokemonGO for iOS ([*Gotta Catch 'Em All: Frida & jailbreak detection*](/post/21-07-pokemongo-anti-frida-jailbreak-bypass)). To continue walking along the path of iOS reverse engineering, I recently took a look at two iOS applications protected by a solution providing both native code obfuscation and RASP (*Runtime Application Self Protection*) protections. I ended up with two blog posts: - [Part 1 -- SingPass RASP Analysis](/post/22-08-singpass-rasp-analysis) - [Part 2 -- Native Code Obfuscation and RASP Syscalls Bypass](/post/22-09-ios-obfuscation-syscall-hooking) The first part is an in-depth analysis of RASP detections methods on iOS while the second part details native code obfuscation and a new technique to bypass inlined syscalls (without Frida/Frida's stalker and without a disassembler) > **Note** The obfuscator mentioned in these blog posts provides strong and state-of-the-art protections to hinder reverse engineering. When dealing with obfuscation, saying that something is broken does not make really sense as if an attacker is skilled and **strongly motivated**, he will very likely achieve his goal. Moreover, this series of blog posts do not (and can't) **exhaustively** evaluate the strength of this commercial solution because: 1. The applications analyzed might not use the latest version of the obfuscator. 2. All the obfuscation features might not have been enabled by the developers (e.g. for performance reasons). 3. The developers might have weakened the obfuscation scheme (unintentionally). In summary, these blog posts aim at sharing -- from a technical point of view -- what RASP and native code obfuscation look like on iOS. The scripts/code associated with these blog posts will not be published as it does not really bring more information. The commercial solution not mentioned in the blog posts is and remains a good choice for protecting assets from reverse engineering. If you have doubts I would be very happy to discuss it. --- # PGSharp: Analysis of a Cheating App for PokemonGO - Canonical: https://www.romainthomas.fr/post/21-11-pgsharp-analysis/ - Markdown: https://www.romainthomas.fr/post/21-11-pgsharp-analysis/index.md - Section: post - Published: 2021-11-07 - Modified: 2026-09-05 - Tags: Android, reverse engineering, obfuscation > This blog post is about the internal mechanisms of PGSharp, a cheat engine for PokemonGO. ## Introduction A few days after the release of the blog post [*Gotta Catch 'Em All: Frida & jailbreak detection*](/post/21-07-pokemongo-anti-frida-jailbreak-bypass), someone on [reddit - r/ReverseEngineering](https://www.reddit.com/r/ReverseEngineering/comments/on6ya9/comment/h5rhkoc/) caught my attention on a cheating app for the Android version of PokemonGO: ![reddit comment about PGSharp](reddit.png) So here it is! PGSharp belongs to the family of PokemonGO's cheating app that is not (yet) banned by Niantic. This cheat provides an *enhanced* game experience with interesting functionalities such as: * GPS Spoofing * Quick Catch * Pokemon Feed * Nearby Radar * ... Last but not least, PGSharp runs on regular devices, **rooted or not**. I enjoyed exploring this cheat over four months, and the technical investigation was worthwhile. As discussed in this blog post, PGSharp uses several interesting tricks. > **Note** The content of this blog post is based on **PGSharp 1.33.0** which is related to the following APKs: [PGSharp v1.33.0](https://data.romainthomas.fr/21-09-pgsharp/pgs1.33.0.apk) [PokemonGO v0.221.0](https://data.romainthomas.fr/21-09-pgsharp/com.nianticlabs.pokemongo_0.221.0-2021093001.apk) This blog post is quite **long** but the different parts are more or less independents, so feel free to jump on them depending on your interests: * [ Code Protection](#code-protection) * [Lua VM](#lua-vm) * [Java Obfuscation](#java-obfuscation) * [ Cheat Mechanisms](#cheat-mechanisms) * [DEX Files Comparison](#dex-files-diff) * [libmain.so](#libmain) * [Signature Bypass](#signature-bypass) * [Dynamic APK Loading](#dynamic-apk-loading) * [GPS Spoofing](#gps-spoofing) * [JNIEnv Proxifier](#jnienv-proxifier) * [Unity Hooks](#unity-hooks) * [Network Communications](#network) * [SafetyNet](#safetynet) * [When PGSharp avoids PokemonGO pitfalls](#pgsharp-signature-check) * [ Final Words](#final-words) * [ Acknowledgments](#acknowledgments) * [ Annexes](#annexes) You can also check the slides to get an overview of the content: [PDF document](/publication/21-ekoparty-mobile-hacking-space-pgsharp/21-10-ekoparty-mobile-hacking-space-pgsharp.pdf)


Enjoy!

## Code Protection PokemonGO is a target of choice for reverse engineers and some critical functionalities are protected by a commercial solution. It is worth mentioning that only a subset of the game is obfuscated. For instance, the "Java" part of the game is absolutely not protected, such as we have the original class and method names. The Unity part is "compiled" into ``libil2cpp.so`` but we can recover some metadata with [Perfare/Il2CppDumper](https://github.com/Perfare/Il2CppDumper). All the obfuscation is focused on ``libNianticLabsPlugin.so`` (c.f. [*Gotta Catch 'Em All: Frida & jailbreak detection*](/post/21-07-pokemongo-anti-frida-jailbreak-bypass)), and since only this part of the game is heavily obfuscated, it gives a hint about where the critical functionalities are. On the other hand, PGSharp uses different layers of obfuscation to prevent its analysis. First of all, it uses O-LLVM to obfuscate the native code that includes, at least, control-flow flattening and string encryption. Nevertheless, the obfuscation is *relatively* weak against emulation and static analysis[^weak-obf]. ### Lua VM Some obfuscation techniques are based on transforming the original code through a VM (like [VMProtect](https://vmpsoft.com/)). It adds another layer to reverse, as we need to understand the VM architecture before being able to understand the original semantic of the code.

But what about using an interpreted language (like Python) and obfuscate its VM or its interpreter with O-LLVM?

This is what PGSharp does with Lua. Some parts of the cheat are written in Lua whose VM has been modified to: 1. Fake the version: try to make believe ``Lua 5.1`` while it's ``Lua 5.3`` 2. Add new opcodes (``OP_RUN``, ``OP_GETDOWNVAL``, ``OP_OLDTABLE``, and ``OP_XXOR``) to break decompilation and common Lua tools. The native library that implements the cheat functionalities and that contains the Lua VM being stripped, one of the challenges lies in recognizing the Lua C API among the library's functions[^static-link]. For instance, here is a basic block of a native function that uses the Lua API: ![Stripped PGSharp function](lua_func_re.png) Among all the Lua C functions, some of them are worth identifying to ease reverse engineering: - ``luaL_loadbuffer`` : > *"Load a buffer as a Lua chunk."*. : Basically, it loads a Lua bytecode from a buffer given in parameter. This Lua bytecode is the result of the *compilation* of the original script with [luac](https://www.lua.org/manual/5.3/luac.html). By hooking this function, we can recover the following files: - base64.luac - class.luac - global.luac - init.luac - json.luac - from https://github.com/rxi/json.lua - location.luac - md5.luac - from https://github.com/kikito/md5.lua - `pgo.luac` - pgodump.luac - plugin.luac - reflect.luac The orange files are utilities, while the green ones contain cheat mechanisms. - ``luaD_precall`` : Function that is involved when calling a C native function or a pure Lua function. Since its prototype is (lua_State *L, StkId func, int nresults), it can help to dynamically identify which function is called: ```text 0x6776a8 luaD_precall('gamehelper') 0x6776a8 luaD_precall('@./app/arm64-v8a/luac/global.lua:0 - sub_71733ea5d0') { 0x694d90 luaD_precall('@./app/arm64-v8a/luac/global.lua:246 - sub_71733f7b50') { 0x6776a8 luaD_precall('@./app/arm64-v8a/luac/location.lua:38 - sub_717346d650') { ``` - ``lua_pushcclosure`` : > Pushes a new C closure onto the stack. : This function is particularly interesting to recover native C functions linked to Lua function: ```text 0x0e9cc0: lua_pushcclosure('initil2cppmethods') 0x0e9cd4: lua_setfield(-2, 'initil2cppmethods', 'func_0xedaa0') ... 0x0e9d10: lua_pushcclosure('nar') 0x0e9d24: lua_setfield(-2, 'nar', 'func_0xeddbc') ... 0x0ea020: lua_pushcclosure('ipf') 0x0ea034: lua_setfield(-2, 'ipf', 'func_0x1318b0') ``` - ``lua_pushstring`` : > *"Pushes the zero-terminated string pointed to onto the stack."* : This function enables to dynamically recover strings that might not be present in the native code or somehow encoded: ```text 0x0ebdfc: lua_pushstring('https://tens.pgsharp.com/v1/scc-2-[...]/') 0x0ebe28: lua_pushstring('me.uw.hela.pref') 0x0c56ac: lua_pushstring('AIza[...]XhM4') 0x0e15b4: lua_pushstring('token=[Redacted]') ``` > **Note** To dynamically understand the behavior of the Lua VM, we can compile the Frida Gum SDK along with Lua v5.3. It enables to hook Lua functions with Frida and to leverage the compiled Lua v5.3 to inspect the parameters: ```cpp extern "C" { #include "lua.h" #include "ldo.h" #include "ldebug.h" } gum_interceptor_attach(listener_->interceptor, luaD_precall_addr, listener_ luaD_precall_addr); void native_listener_on_enter(GumInvocationListener *listener, GumInvocationContext* ic) { auto* L = reinterpret_cast(ic->cpu_context->x[0]); auto func = reinterpret_cast(ic->cpu_context->x[1]); auto narg = static_cast(ic->cpu_context->x[2]); if (ttype(func) != LUA_TLCL) { return log("sub_{:x}", ptr); } Proto *p = clLvalue(func)->p; return log("{}:{:d} - sub_{:x}", getstr(p->source), p->linedefined, ptr); } ``` ### Java Obfuscation Contrary to the PokemonGO's Java layer, PGSharp protects its Java code with Proguard and the strings are xored with the hardcoded key:

vqGqQWCVnDRrNXTR

This key seems to not change across the different versions of PGSharp and the encoded strings look like this: ```java public void q() { String a = GL.a(r3.a("FAQgLiQlLw=="), (String) null); if (a != null) { JSONObject jSONObject = new JSONObject(); Context context = GL.c; jSONObject.put(r3.a("Agg3FA=="), r3.a("Axg=")); jSONObject.put(r3.a("Axgj"), UI.g(context)); jSONObject.put(r3.a("BQUmBTQ="), this.s); jSONObject.put(r3.a("BQEoHjc+LTE="), ((Boolean) ...); jSONObject.put(r3.a("BAUr"), UI.f()); jSONObject.put(r3.a("Gh8g"), Locale.getDefault().getDisplayLanguage()); jSONObject.put(r3.a("FxMu"), UI.a()); jSONObject.put(r3.a("FBA1"), LayoutInflater$Factory2o.i.e(context)); jSONObject.put(r3.a("Gx4j"), Build.MODEL); String str = Build.VERSION.RELEASE; } } ``` The string encoding routine being easy to reverse, we can create a Jadx plugin that automatically decodes these strings: ```java [...] passes.add(new SimplifyVisitor()); passes.add(new PGSharpString()); // Automatically decode the strings passes.add(new CheckRegions()); [...] ``` It results in this kind of output: ```java public void q() { String a = GL.a("bug_url", (String) null); if (a != null) { JSONObject jSONObject = new JSONObject(); Context context = GL.c; jSONObject.put("type", "ui"); jSONObject.put("uid", UI.g(context)); jSONObject.put("state", this.s); jSONObject.put("spoofing", ((Boolean) PL.a("hlspoofing")).booleanValue()); jSONObject.put("rtl", UI.f()); jSONObject.put("lng", Locale.getDefault().getDisplayLanguage()); jSONObject.put("abi", UI.a()); jSONObject.put("bar", LayoutInflater$Factory2o.i.e(context)); jSONObject.put("mod", Build.MODEL); String str = Build.VERSION.RELEASE; } } ``` The whole Jadx plugin is available on GitHub: [PGSharpStrings.java](https://github.com/romainthomas/pgsharp/blob/9addafbb6672571d2b7fbba43899f662c21aac8e/jadx/PGSharpStrings.java)
## Cheat Mechanisms One ~~disruptive~~ feature of PGSharp is that it does not require a rooted device. Until recently, most of the PokemonGO cheating apps required a jailbroken or a rooted device which raises a barrier for people who are not familiar with rooting. > But wait, how *hell* they do that? The structure of the PGSharp APK is **very** close to the genuine PokemonGO application, which leads identifying which parts of the game have been tampered with. A naive comparison (cf. [zip_diff.py](https://github.com/romainthomas/pgsharp/blob/9addafbb6672571d2b7fbba43899f662c21aac8e/zip_diff.py)) raises mismatches on the following files: | File | Size in PGSharp | Size in PGO | Delta | :------ | :----------- | :------------ | :------------ | classes.dex | 9057844 | 8953000 | +1.17% | classes2.dex | 7131864 | 7107296 | +0.34% | lib/arm64-v8a/libmain.so | 21278480 | 6424 | +331134% | META-INF/MANIFEST.MF | 351045 | 355533 | -1.26% The high level of similarity between the two applications, associated with a different signature confirms that PGSharp repackaged the original application. #### DEX Files Comparison To figure out which parts of the DEX files have been modified, we can use LIEF (yes, LIEF can read the DEX format). Basically, the idea is to check which method(s) has a bytecode whose size is different from the real PokemonGO application: ```python import zipfile import lief with zipfile.ZipFile(CHEAT_FILE) as zip_file: with zip_file.open(target) as f: hela_dex = f.read() with zipfile.ZipFile(ORIG_FILE) as zip_file: with zip_file.open(target) as f: pgo_dex = f.read() hela_dex = lief.DEX.parse(list(hela_dex)) pgo_dex = lief.DEX.parse(list(pgo_dex)) hela = {f"{m.cls.pretty_name}.{m.name}.{m.prototype!s}": len(m.bytecode) \ for m in hela_dex.methods} pgo = {f"{m.cls.pretty_name}.{m.name}.{m.prototype!s}": len(m.bytecode) \ for m in pgo_dex.methods} for k, size_hela in hela.items(): size_pgo = pgo[k] if size_pgo != size_hela: print(f"Mismatch: {k}") ``` By running this script on ``classes.dex``, we don't find any difference. Actually, the PGSharp authors tried to prevent *diffing* by changing the line number attribute of the DEX classes. If we try to diff the two applications from the output of apktool or Jadx, we get a lot of noise as the line number is used in the output. On the other hand, the size bytecode for this kind of repackaging is suitable[^qb-modded]. Running the same script on ``classes2.dex`` raises the following mismatches: - ``holoholo.libholoholo.unity.UnityMainActivity.onActivityResult`` - ``holoholo.nativelib.Library.`` In ``UnityMainActivity.onActivityResult``, they changed this piece of code: ```java public void onActivityResult(int i, int i2, Intent intent) { UnityCallbackInfo unityCallbackInfo = this.activityCallbacks.get(Integer.valueOf(i)); if (unityCallbackInfo != null) { UnityPlayer.UnitySendMessage(unityCallbackInfo.mGameObjectName, unityCallbackInfo.mMethodName, String.valueOf(i2)); } else { Client.handleActivityResult(i, intent); } } ``` into: ```java public void onActivityResult(int i, int i2, Intent intent) { UnityCallbackInfo unityCallbackInfo = this.activityCallbacks.get(Integer.valueOf(i)); if (unityCallbackInfo != null) { String mGameObjectName = unityCallbackInfo.mGameObjectName; UnityPlayer.UnitySendMessage(mGameObjectName, unityCallbackInfo.mMethodName, "HL.PL".equals(mGameObjectName) ? intent == null ? "" : intent.getData().toString() : String.valueOf(i2)); return; } Client.handleActivityResult(i, intent); } ``` While in the static constructor of the ``Library`` class, they force the loading of libmain.so: ```java static { System.loadLibrary("main"); System.loadLibrary("holoholo"); } ``` Now, let's look at libmain.so #### libmain.so Compared to the original PokemonGO APK, libmain.so in PGSharp is substantially larger. Moreover, the ELF metadata leaks the original file name of the file: ```console $ readelf -d libmain.so ... 0x000000000000000e (SONAME) Library soname: [libhela.so] ... ``` During the analysis of PGSharp, we find references to [Hela](https://en.wikipedia.org/wiki/Hela_(comics)) in different places, like the package name of the dynamically-loaded APK: ``me.underworld.helaplugin``. Originally, the purpose of this library is to initialize some parts of the Unity engine but PGSharp uses it to load its main payload. In the cheating app, libmain.so is responsible for: 1. Initializing the Lua VM 2. Implementing Lua native C functions 3. Implementing JNI functions 4. Calling the Lua scripts libmain.so exposes ``JNI_OnLoad`` which is used as an entrypoint to perform the actions listed above. The JNI functions don't have a meaningful name but thanks to their callsites, we can figure out their purpose: | Name | Description | Rename | :------ | :-------- | :---- | NRL | Trigger Lua function from Java | NativeRunLua | NSMTC | Trigger PGSharp Action | - | NOHRB | OkHtttp callback | NativeOkHttpResponseByte | NOHR | OkHtttp callback | NativeOkHttpResponse | NOHF | OkHtttp callback | NativeOkHttpFailure | NIOS | Google Signing? | - | NIOR | *Seems not used* | - | NOT | Perform periodic actions on Lua threads | NativeOnTimer | NIPE | Related to PokemonGO Plus | - | NIOF | *Seems to do nothing relevant* | - Similarly, for the Lua C closures, we get the following table: | Name | Description | Rename | :------ | :-------- | :---- | callpgo | Trigger Lua function from Java | - | add_unity_task | Trigger PGSharp Action | - | initil2cppbase | OkHtttp callback | - | initil2cpphooks | OkHtttp callback | - | initil2cppmethods | OkHtttp callback | - | newjbytearray | Create a *Java* bytearray from Lua | - | nar | - | nativeAttestResponse | ngak | - | nativeGetApiKey | findclass | Find a *Java* class from Lua | - | gettid | Get Thread ID | - | logi | Log info (empty) | - | logv | Log verbose (empty) | - | init_plugin_natives | Init Java layer (JNI + ``nUSlwbRIjReLowOP``) | - | uf_whitelist | *empty* | - | uf_forbid | *empty* | - | uf_redirect | *empty* | - | fkinitjni | Lua wrapper[^wrapper] | FakeInitJNI | fknalp | Lua wrapper[^wrapper] | FakeNativeAddLocationProvider | fkngsu | Lua wrapper[^wrapper] | FakeNativeGpsStatusUpdate | fknlu | Lua wrapper[^wrapper] | FakeNativeLocationUpdate | getPoisFromCache | Related to the autowalk feature | - > **Spoiler: NRL Actions** | Action | Event Task | :------ | :-------- | 1 | plg.float.click | 2 | plg.float.remove | 3 | plg.map.tp | 4 | plg.setspeed | 5 | plg.randomwalk | 6 | plg.enablespoof | 7 | plg.joystart | 8 | plg.joystop | 9 | plg.entergame | 10 | plg.pause > **Note** Long story short, PGSharp repackages the PokemonGO application and implements its payload in libmain.so > *But wait, since they repackage the application they have to re-sign the application and you won't tell me that PokemonGO does have signature checks?*

And this is where the fun begins!


The functionalities of PGSharp heavily rely on hooking but not the hooking you might think of ... ### Signature Bypass As it is detailed in the next section, libmain.so dynamically loads another APK. Within this APK, and more precisely in the class ``androidx.appcompat.app.AppCompatDelegateImpl``[^rename], we can notice this method: ```java /* renamed from: g */ public static void proxifySignatureCheck(Context context) { String packageName = context.getPackageName(); Class aThreadCls = Class.forName("android.app.ActivityThread"); Object mCurrentActivityThread = aThreadCls.getDeclaredMethod("currentActivityThread", new Class[0]).invoke(null, new Object[0]); Field sPackageManager = aThreadCls.getDeclaredField("sPackageManager"); sPackageManager.setAccessible(true); Object pm = sPackageManager.get(mCurrentActivityThread); Class IPackageManager = Class.forName("android.content.pm.IPackageManager"); SignatureMock mock = new SignatureMock(pm, "30820 [ ... ] aa001f55", packageName) Object newProxyInstance = Proxy.newProxyInstance(IPackageManager.getClassLoader(), new Class[]{IPackageManager}, mock); sPackageManager.set(mCurrentActivityThread, newProxyInstance); PackageManager packageManager = context.getPackageManager(); Field mPM = packageManager.getClass().getDeclaredField("mPM"); mPM.setAccessible(true); mPM.set(packageManager, newProxyInstance); } ``` This code leverages the Java *hooking* API, [java.lang.reflect.Proxy](https://developer.android.com/reference/java/lang/reflect/Proxy), to *proxify* the Android PackageManager ¯\\\_(ツ)\_/¯. The *mocked* PackageManager looks like this: ```java public SignatureMock(Object pm, String originalSignature, String packageName) { this.mPackageManager = pm; this.mOriginalSignature = originalSignature; this.mPackageName = packageName; } @Override // java.lang.reflect.InvocationHandler public Object invoke(Object obj, Method inMeth, Object[] args) { PackageInfo packageInfo; SigningInfo signingInfo; // Hook getPackageInfo if ("getPackageInfo".equals(inMeth.getName())) { String pkgName = (String) args[0]; int flags = ((Integer) args[1]).intValue(); // Handle both // GET_SIGNATURES (0x00000040) - Deprecated in API 28 // GET_SIGNING_CERTIFICATES (0x08000000) if ((flags & PackageManager.GET_SIGNATURES) != 0 && this.mPackageName.equals(pkgName)) { PackageInfo fakePkgInfo = (PackageInfo) inMeth.invoke(this.mPackageManager, args); // Fake the signature fakePkgInfo.signatures[0] = new Signature(this.mOriginalSignature); return fakePkgInfo; } else if (Build.VERSION.SDK_INT >= 28 && (flags & GET_SIGNING_CERTIFICATES) != 0 && this.mPackageName.equals(pkgName) && (signingInfo = (packageInfo = (PackageInfo) method.invoke(this.mPackageManager, args)).signingInfo) != null) { Field FieldSigningDetails = signingInfo.getClass().getDeclaredField("mSigningDetails"); FieldSigningDetails.setAccessible(true); Object mSigningDetails = FieldSigningD.get(packageInfo); Signature[] fakeSigArray = {new Signature(this.mOriginalSignature)}; Field FieldSignatures = mSigningDetails.getClass().getDeclaredField("signatures"); FieldSignatures.setAccessible(true); FieldSignatures.set(FieldSigningDetails, fakeSigArray); return packageInfo; } } return inMeth.invoke(this.mPackageManager, args); } ``` In doing so, when PokemonGO accesses the PackageManager, it gets a *mocked* version of the PackageManager that is **controlled** by PGSharp. PGSharp changes the behavior of ``getPackageInfo()`` to return the real PokemonGO signature instead of its own. The following figure outlines the process: ![Mock Android PackageManager](mock_signature.png) ### Dynamic APK Loading In the Lua script ``plugin.lua``, PGSharp defines an ``init`` function that performs the following actions: ```lua local filesdir = (ref.call_method)(runtime.app, "getFilesDir", "()Ljava/io/File;") local filesdirpath = (ref.call_method)(filesdir, "getAbsolutePath", "()Ljava/lang/String;") u_plugin_path = (gh.ipf)(loadjstring(filesdirpath)) ``` ``ipf`` is a function that takes the output of ``cxt.getFilesDir().getAbsolutePath()`` as parameter, in other words, the path of the *files* directory of PokemonGO: ``/data/data/com.nianticlabs.pokemongo/files``, and returns a ``u_plugin_path`` as a Lua string. If we look for ``ipf`` in the Lua scripts, we don't find any implementation. Actually, this function is referenced in the ``gamehelper()`` function of libmain.so where it is linked as follows: ![Lua registering IPF](ipf.png) So ``ipf`` is a native Lua C function registered with ``lua_pushcclosure``. Once we identified the location of ``ipf``, the logic of the function can be summarized with this pseudo code: ```cpp // file_dir: /data/user/0/com.nianticlabs.pokemongo/files void ipf(lua_State *L) { // std::string ctor @0xA4F00 std::string outpath = lua_tostring(L, -1); // std::string::append @0xD868C outpath.append("/"); outpath.append("LZZqoKpt.plg"); FILE* fout = fopen(outpath.c_str(), "wb"); // @0x634424 extract_apk_file(FILE* fout) { for (chunk : chunks) { decode(chunk, 0x2710); fwrite(chunk, 0x2710, 1, fout); } } fclose(fout); lua_pushlstring(L, outpath.c_str(), outpath.size()); } void inline_decode(uint8_t* data, size_t size) { for (size_t i = 0; i < size; ++i) { // Byte decoding data[i] = (0xb3 & ~data[i]) | (data[i] & 0x4c) } } ``` > **Note** Since the decoded file is written in the ``/data`` partition, one can also pull the file from the device (this file is not removed when PGSharp stops running). The written file, ``LZZqoKpt.plg``, is actually an APK that is loaded with ``PathClassLoader`` in the Lua script: ```lua u_classloader = (ref.new_instance)("dalvik/system/PathClassLoader", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/ClassLoader;)V", (env.NewStringUTF)(u_plugin_path), nativeLibraryDir, gh.pgo_classloader); ``` > **Spoiler: Rest of the function** ```lua u_plugin_cls = findclass("me/underworld/helaplugin/PL", u_classloader) u_global_cls = findclass("me/underworld/helaplugin/GL", u_classloader) u_runnable_cls = findclass("me/underworld/helaplugin/HR", u_classloader) u_global_cls = (env.NewGlobalRef)(u_global_cls) u_plugin_cls = (env.NewGlobalRef)(u_plugin_cls) u_classloader = (env.NewGlobalRef)(u_classloader) u_runnable_cls = (env.NewGlobalRef)(u_runnable_cls) u_runnable_init_mid = (env.GetMethodID)(u_runnable_cls, "", "(ILjava/lang/Object;)V") u_geturl_mid = (env.GetStaticMethodID)(u_plugin_cls, "GU", "(Ljava/lang/String;Ljava/lang/String;)I") u_postString_mid = (env.GetStaticMethodID)(u_plugin_cls, "vtEdUZmWQYAgtGWs", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I") u_postBytes_mid = (env.GetStaticMethodID)(u_plugin_cls, "BbTwaTXurePBxTDt", "(Ljava/lang/String;[BLjava/lang/String;)I") u_onLuaMessage_mid = (env.GetStaticMethodID)(u_plugin_cls, "tFAxNZCNHOXBTYGM", "(ILjava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;") u_global_updatelocation_mid = (env.GetStaticMethodID)(u_global_cls, "ul", "(DD)V") u_global_savelocation_mid = (env.GetStaticMethodID)(u_global_cls, "sl", "()V") (gh.init_plugin_natives)(u_classloader) (ref.call_static_method)(u_plugin_cls, "rDymrMuxPIlIESFe", "(Landroid/app/Application;Ljava/lang/String;I)V", runtime.app, (env.NewStringUTF)(u_plugin_path), runtime.log_level) local cls = (gh.findclass)("me.underworld.helaplugin.HLVM", u_classloader) local hlviewmanagerref = (ref.call_static_method)(cls, "getInstance", "()Lme/underworld/helaplugin/HLVM;") u_sm_mid = (env.GetMethodID)(cls, "SM", "(Ljava/lang/String;I)V") u_setviewshow_mid = (env.GetMethodID)(cls, "SVC", "(Ljava/lang/String;Z)V") u_hlviewmanager = (env.NewGlobalRef)(hlviewmanagerref) plugin.classloader = u_classloader ``` ### GPS Spoofing Since PokemonGO heavily relies on the user's location, the must-have feature for the PokemonGO cheat engines is to be able to spoof the GPS location. The genuine PokemonGO application manages the user location through the Java class ``NianticLocationManager``, which exposes three natives functions: 1. nativeAddLocationProviders(Context ctx) 2. nativeGpsStatusUpdate(int i, SatelliteInfo[] info) 3. nativeLocationUpdate(String providerName, Location location, ...) ``nativeAddLocationProviders`` aims at instantiating the different location providers as Java object: 1. FusedLocationProvider 2. GnssLocationProvider 3. GpsLocationProvider 4. NetworkLocationProvider while ``nativeLocationUpdate`` and ``nativeGpsStatusUpdate`` are a kind of callbacks triggered when there is a new user location to consider. The implementation of ``nativeLocationUpdate`` checks natively if the location object given in the second parameter is *mocked* (cf. [isMock()](https://developer.android.com/reference/android/location/Location#isMock()) or [isFromMockProvider()](https://developer.android.com/reference/android/location/Location#isFromMockProvider())). Actually PGSharp hooks two of these three native methods: 1. nativeAddLocationProviders(Context ctx) 2. nativeGpsStatusUpdate(int i, SatelliteInfo[] info) 3. nativeLocationUpdate(String provider, Location location, ...) By hooking ``nativeLocationUpdate``, they can modify the value of the ``Location`` parameter to change the real location. > *"You assert that PGSharp hooks ``nativeLocationUpdate`` and ``nativeAddLocationProviders`` in > ``libNianticLabsPlugin.so``, but this library is protected by a commercial obfuscator > which has anti-hooks features. How do they hook these functions?"* And this is where the fun reaches another level :rocket: ### JNIEnv Proxifier I would assume that ``nativeLocationUpdate`` and ``nativeAddLocationProviders`` are critical enough to be protected against hooking. It turns out that PGSharp embeds a hooking framework to hook Unity functions, but they don't use it on these functions.

The authors of PGSharp found a subtle trick to circumvent the anti-hook protection.

``nativeLocationUpdate`` and ``nativeAddLocationProviders`` are JNI functions that are **dynamically** registered by ``Java_com_nianticlabs_nia_unity_UnityUtil_nativeInit``: ```cpp Java_com_nianticlabs_nia_unity_UnityUtil_nativeInit(env, ...) { env->RegisterNatives(...); } ``` The ``env`` parameter refers to the JNIEnv structure which is an array of function pointers: ```cpp struct JNINativeInterface { ... jclass (*GetObjectClass)(JNIEnv*, jobject); jboolean (*IsInstanceOf)(JNIEnv*, jobject, jclass); jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*); ... } ``` The values of these pointers are defined by the implementation of the "JVM" which is, for Android, the **A**ndroid **R**un**T**ime (ART). For instance, ``FindClass`` is actually a pointer to ``art::{CheckJNI, JNIImpl}::FindClass`` located in ``art/runtime/jni/{jni_internal, check_jni}.cc``: ```cpp static jclass FindClass(JNIEnv* env, const char* name) { Runtime* runtime = Runtime::Current(); ClassLinker* class_linker = runtime->GetClassLinker(); std::string descriptor(NormalizeJniClassDescriptor(name)); ScopedObjectAccess soa(env); ObjPtr c = nullptr; if (runtime->IsStarted()) { StackHandleScope<1> hs(soa.Self()); Handle class_loader(hs.NewHandle(GetClassLoader(soa))); c = class_linker->FindClass(soa.Self(), descriptor.c_str(), class_loader); } else { c = class_linker->FindSystemClass(soa.Self(), descriptor.c_str()); } return soa.AddLocalReference(c); } ``` If we hook ``Java_com_nianticlabs_nia_unity_UnityUtil_nativeInit`` from the **genuine** PokemonGO, and we check where the pointers of the ``JNIEnv`` structure point to, we get this kind of output: ![Normal values of the JNI Pointers](jni_ptr_clean.png) This output is consistent with what we said about the *JVM* and the runtime ART. If we do the same check **on PGSharp**, we get this result: ![Modified values of the JNI Pointers](jni_ptr_cheat.png) As we can see, some pointers have been relocated to point in libmain.so:
| JNI Function | Offset in libmain.so (v1.33.0) | :------ | :-------- | GetMethodID | 0x14540c | CallObjectMethodV | 0x145194 | CallVoidMethodV | 0x145040 | RegisterNatives | 0x1452f0
It means that when ``libNianticLabsPlugin.so`` is calling one of the functions listed above, the execution is forwarded to libmain.so instead of libart.so. ![JNIEnv Proxy](jnienv_proxy.png) PGSharp proxifies these functions for the following purposes: **GetMethodID** To monitor: 1. SafetyNetService.attest 2. SafetyNetService.cancel 3. NianticLocationManager.addLocationProvider **CallVoidMethodV** To monitor the parameters of: 1. SafetyNetService.attest (to intercept the nonce) 2. SafetyNetService.cancel **RegisterNatives** Proxified to get, and potentially change, the effective location of the
``libNianticLabsPlugin.so`` JNI functions: 1. nativeAttestResponse 2. nativeGetApiKey 3. nativeAddLocationProviders 4. nativeLocationUpdate 5. initJni 6. nativeInjectEvent 7. nativeUnitySendMessage 8. nativeRender 9. nativeMuteMasterAudio By managing the function ``JNIEnv::RegisterNatives``, they are able to change the value of ``JNINativeMethod.fnPtr``, such as when PokemonGO calls ``nativeLocationUpdate``, it actually calls a function managed by PGSharp. It results that JNI functions used by ``libNianticLabsPlugin.so`` have been *redefined*: | JNI Function | Location | :------ | :-------- | NianticLocationManager.nativeAddLocationProviders | libmain.so!ea868 | NianticLocationManager.nativeGpsStatusUpdate | libNianticLabsPlugin.so!bc508 | NianticLocationManager.nativeLocationUpdate | libmain.so!ea8bc | NLog.nativeDispatchLogMessage | libNianticLabsPlugin.so!4beaa0 | NetworkConnectivity.nativeNotifyNetworkStateChanged | libNianticLabsPlugin.so!6f8118 | NianticTrustManager.nativeCheckClientTrusted | libNianticLabsPlugin.so!9b9cc | NianticTrustManager.nativeCheckServerTrusted | libNianticLabsPlugin.so!73b42c | NianticTrustManager.nativeGetAcceptedIssuers | libNianticLabsPlugin.so!6dc5c8 | WebsocketController.nativeOnDidClose | libNianticLabsPlugin.so!6fcabc | WebsocketController.nativeOnDidFail | libNianticLabsPlugin.so!4a0d0c | WebsocketController.nativeOnDidOpen | libNianticLabsPlugin.so!5922b0 | WebsocketController.nativeOnDidReceiveData | libNianticLabsPlugin.so!742fa8 | SafetyNetService.nativeAttestResponse | libNianticLabsPlugin.so!5ea1b8 | SafetyNetService.nativeGetApiKey | libNianticLabsPlugin.so!6bc60 | NianticSensorManager.nativeCompassUpdate | libNianticLabsPlugin.so!4b9bc0 | NianticSensorManager.nativeSensorUpdate | libNianticLabsPlugin.so!177c44 ### Unity Hooks In addition to GPS spoofing, PGSharp provides other functionalities such as, Pokemon feed, skip evolve animation ... In the genuine PokemonGO application, these functionalities are implemented in the Unity layer that is *compiled* into ``libil2cpp.so``. To perform these functionalities, PGSharp hooks (hooking like Frida) some of these Unity functions. They tried to hide[^hook-hide] the underlying hooking framework used to perform these hooks, unfortunately they missed to remove important strings: ```console $ strings ./libmain.so|grep -i -E "\w\+\.cc" E:/work/code/Hela/app/src/main/cpp/Dobby/source/InterceptRouting/Routing/FunctionInlineReplace/FunctionInlineReplaceExport.cc E:/work/code/Hela/app/src/main/cpp/Dobby/source/TrampolineBridge/Trampoline/arm64/trampoline-arm64.cc E:/work/code/Hela/app/src/main/cpp/Dobby/source/MemoryAllocator/MemoryArena.cc E:/work/code/Hela/app/src/main/cpp/Dobby/source/InstructionRelocation/arm64/ARM64InstructionRelocation.cc E:/work/code/Hela/app/src/main/cpp/Dobby/source/UserMode/PlatformUtil/Linux/ProcessRuntimeUtility.cc E:/work/code/Hela/app/src/main/cpp/Dobby/source/UserMode/UnifiedInterface/platform-posix.cc ``` So the hooking framework is Dobby: Unity hooks start, in the script ``init.lua`` of PGSharp where they wait for the loading of ``libil2cpp.so``[^illoadinfo]: ```lua set_event_handler("pgo.il2ready", function(il2base) runtime.il2base = il2base; (gh.initil2cppmethods)(); (gh.initil2cpphooks)(); end ) ``` ``initil2cppmethods()`` aims at resolving the address of PokemonGO Unity functions needed to perform cheating actions, while ``initil2cpphooks()`` uses Dobby to hook some Unity functions and change their behavior. In the version 1.33 of PGSharp, they hook 207 functions of ``libil2cpp.so`` and we will take one of them to detail the internal mechanisms: - ``UnityEngine.Application$$OpenURL`` First of all, if we look at the symbols or the strings of ``libil2cpp.so``, we don't find meaningful information that could help to figure out the original purpose of the Unity functions. In fact, the Unity metadata are embedded in ``global-metadata.dat``, and to recover the bindings between this file and ``libil2cpp.so``, we can use [Perfare/Il2CppDumper](https://github.com/Perfare/Il2CppDumper). They compute the absolute of a Unity function by adding the offset provided by ``global-metadata.dat`` to the base address of ``libil2cpp.so``. Here is an example ``UnityEngine.Application$$OpenURL``: ```arm MOV W13, 0x4bfeea0 ; Offset of the function (thanks to global-metadata.dat) ADRP X14, #Application_OpenURL ADD X13, X8, X13 ; Add the libil2cpp.so base address STR X13, [X14, #Application_OpenURL] ; Store the absolute address in libmain.so ``` The function associated with ``initil2cpphooks()`` is quite large as shown in the figure below: ![initil2cpphooks](initil2cpphooks.png) Actually, the function is large but easily understandable statically[^miss-ollvm]. The right-hand side of the figure is actually the ``catch { ... }`` handlers of the exceptions, while the left-hand side that goes down, initializes C++ objects. In this area of the CFG, we find the same pattern that repeats all the way down: ![Dobby Hooking VTable](dobby_hook_vtable.png) From what we can see, it initializes a C++ object (on the stack) and the first instructions set up the VTable. We can find the relevant function in the last entry of the VTable that contains the hooking logic: ![Dobby Hooking OpenURL](hook_OpenURL.png) From this code, we can see that they perform the resolution of the absolute address of ``UnityEngine.Application$$OpenURL``. Also, thanks to the prototype of ``DobbyHook()`` we can quickly understand that the new behavior of ``OpenURL`` is located in the function ``sub_6C983C``: ![Dobby Hooking OpenURL](il2cpp_hooks.png) In this hook, they check if PokemonGO is opening its Google Play URL and redirect the user to the PGSharp home page. ### Network Communications and Encryption The cheating application communicates with its servers through the TLS/HTTP protocol and adds another layer of encryption on the top of TLS. To encrypt the HTTP payload, they use AES in the CBC mode. We can identify the AES algorithm thanks to clear S-BOX present in the ``.rodata`` section. It seems that they use different keys, depending on the endpoint the application targets but we can retrieve them by hooking the AES key schedule function. It results that we can decrypt the communication between the application and the PGSharp servers[^aes-hook]. Here are examples of endpoints and the data sent by PGSharp: > **Spoiler: hazelnuts** - **POST ``hazelnuts``** * Action: Handshake * Request: ```json { "bid": "com.nianticlabs.pokemongo", "clt": "pgs", "host": "Samsung A40", "lv": -1, "nonce": "", "nonce_key": "", "pgver": "0.221.0", "uid": "00000000-00000000-[...]", "ver": "1.33.0" } ``` * Response: ```json { "err": 0, "shiny": ["List of shiny"], "hotplaces": [ { "name": "🇧🇷 Consolacao, São Paulo, Brazil", "lat": -23.5512, "lng": -46.6584 }, ] } ``` > **Spoiler: Cw8dfkXpW7mq2i** - **POST ``Cw8dfkXpW7mq2i``** * Action: ``PGS_ACTIONS.GETPLAYER`` * Request: ```json { "bid": "com.nianticlabs.pokemongo", "clt": "pgs", "host": "Samsung A40", "lv": 4, "nonce": "", "nonce_key": "", "pgver": "0.221.0", "player": { "ban": 0, "captured": 3, "encountered": 5, "kmwalked": 10.50, "outage": 0, "pid": "[redacted]", "serverlo": 0, "susp": 0, "suspa": 0, "visits": 1, "warn": 0, "warna": 0, "warndt": 623234511000000000, "warntm": 0 }, "uid": "00000000-00000000-[...]", "ver": "1.33.0" } ``` > **Spoiler: SSZgBPn6Ixq2ZK** - **POST ``SSZgBPn6Ixq2ZK``** * Action: ``PGS_ACTIONS.GETREPORTABLE`` * Request: ```json { "bid": "com.nianticlabs.pokemongo", "clt": "pgs", "host": "Samsung A40", "lv": 4, "nonce": "", "nonce_key": "", "pgver": "0.221.0", "raid": [ { "battle": 1634490000000, "campaignId": "", "complete": false, "costume": 12233, "dex": 326, "eligible": false, "end": 1634490000000, "exclusive": false, "form": 0, "free": false, "gender": 1, "hidden": false, "lat": 0.1234, "lng": 5.6789, "lv": 3, "mov1": 163, "mov2": 90, "schedule": false, "seed": 5000000, "spawn": 1634400000000, "team": 1, "web": 0, "wec": 3 }, ], "uid": "00000000-00000000-[...]", "ver": "1.33.0" } ``` > **Spoiler: /pga/keycode/v-q2mgqcyji/** - **POST ``/pga/keycode/v-q2mgqcyji/``** * Action: Activate PGSharp with a premium key * Request: ```json { "ver": "1.33.0", "gi": 1, "key": "AVerySecretKey", "host": "Samsung A40", "clt": "pgs", "ua": "Samsung A40/11/[redacted]/arm64-v8a/[redacted]/unknow/unknown/English", "uid": "00000000-00000000-[...]" } ``` ### SafetyNet ![safetynet](snet.svg) > **Note** I skimmed this layer this weekend, so some parts might be inaccurate or wrong. In addition to standard code obfuscation, PokemonGO uses SafetyNet as an attestation mechanism. Similarly to the GPS management, we find a (non-obfuscated) Java layer implemented in the class ``SafetyNetService``. This class exposes two native functions: 1. String nativeGetApiKey() 2. void nativeAttestResponse(byte[] nonce, String jwtResult) The implementation of these two functions is obfuscated within ``libNianticLabsPlugin.so``. The first function is used to get the Google SafetyNet API key (``AIzaSyCh8l[...]_eOTXhM4``) while the second one, is involved in the validation of the SafetyNet attestation. Thanks to the JNIEnv proxy on ``GetMethodID`` and ``CallVoidMethodV``, PGSharp is able to monitor the calls to ``SafetyNetService.attest(bytes[] nonce)``. When this function is called, PGSharp intercepts the nonce and forward the request to its servers:

https://tens.pgsharp.com/v1/scc-2-eg[...]4/

The request is performed through an HTTP POST, whose data are encrypted with AES. The clear payload has the following layout: ```json { "n": "Cy[...]", "type": "attest", "ver": "", "k": "i3[...]", "clt": "pgs", "data": { "k": "AIzaSyCh8l[...]_eOTXhM4 <- From nativeGetApiKey", "n": "" } } ``` On success, the server responses with an AES-encrypted payload which has the following layout: ``` {"result":"suc: "} ``` The JWT SafetyNet value is then forwarded by PGSharp to ``nativeAttestResponse()`` with the original nonce. At some point, this JWT attestation is sent to Niantic's servers (on the endpoint ``plfe/112/rpc2``) wrapped by a Protobuf structure. To understand how they *"bypass"* SafetyNet, let's look at the JWT payload: ```json { "nonce": "", "timestampMs": 1636265656, "apkPackageName": "com.nianticlabs.pokemongo", "apkDigestSha256": "ioYmlh5mk5EhMUH/DsaG1jrhUoQJDK/2IvK61eiAXJE=", "ctsProfileMatch": true, "apkCertificateDigestSha256": [ "lEvaRm6vZL4ck4ltXI6aRUoHyNj8vEre7vs1RbM16Xk=" ], "basicIntegrity": true, "evaluationType": "BASIC" } ``` First of all, the JWT is correctly signed by Google SafetyNet's key and the ``apkCertificateDigestSha256`` matches the signature of the real PokemonGO application. But ... The value of ``apkDigestSha256`` does not match the checksum of the genuine PokemonGO application :confused: **Here are my hypothesis:** The server ``https://tens.pgsharp.com/v1/scc-2-eg/...`` forwards the SafetyNet request to a real application that runs on a real device. This application would have been created by PGSharp authors to *really* run SafetyNet and to get a valid attestation signed with a valid Google key. The fake application would have been created with ``com.nianticlabs.pokemongo`` as package name and would implement signature mocking, as discussed in the first part. If they would have managed to break SafetyNet, the ``apkDigestSha256`` value would have been consistent. The JWT attestation is forwarded to Niantic so they might check the consistency of ``apkDigestSha256`` but they might only focus on the signature (which can be faked) and not this value ... ### When PGSharp avoids PokemonGO pitfalls As discussed in the section [*Signature Bypass*](#signature-bypass), PGSharp tricks the Android PackageManager to mock the signature of the application. It turns out that PGSharp is also concerned about app repackaging. As they provide premium features, they don't want to be cheated ... In the function associated with ``PGS_ACTIONS.INITPOST`` they perform a device fingerprint whose one of these elements is the APK's signature. But instead of using the Android PackageManager to retrieve the signature, they use [DimaKoz/stunning-signature](https://github.com/DimaKoz/stunning-signature) to compute the MD5 digest of the signature. ## Final Words When I started to look at this cheating app, I did not expect to find such nice tricks and challenges. The PGSharp's authors know the sneaky tricks to hinder reverse engineering. Unfortunately, O-LLVM is relatively weak in this context compared to the commercial obfuscator used by Niantic. On the other hand, the design of PokemonGO is such that all the reverse engineering difficulties lie in one single module that can be treated in black-box once we identified the API. In particular, the un-obfuscated Java layer helps a lot to identify these API. Regarding the signature bypass, at first, I thought it would be easy to prevent by checking the integrity of the ``.apk`` and/or the native libraries. But, there are some points that need to be taken into account: **APK Integrity Check** Naively, we might want to compute a checksum of the APK or re-compute the signature (as it's done by PGSharp). But in fact, for a few years, Google tries to push developers to use app bundle such as an application is no longer a single ``.apk`` but a split ``.apk``. While this feature optimizes the device's data partition size, it complicates the verification of the signature since it would require to deal with different files and different checksum. It's not infeasible, but it complicates its implementation in the APK build & development pipeline. **Native Library Integrity Check** I guess that ``libNianticLabsPlugin.so`` implements checksum on its own library as it is a sensitive part of the application. Regarding the other libraries, some of them are owned by Niantic (like ``libholoholo.so``) and others come from third-parties (like ``libmain.so``). Depending on how they are integrated, the checksum of these external libraries might not be easy to automatically compute while releasing a new version of PokemonGO. These third-party libraries are, most of the time, not copy-pasted by the developers but automatically bundled when compiling the application. Therefore, computing their checksums might require tweaking the build process in a non-easy way. On the top of that, Niantic releases a new version of its games on a monthly basis. It means that these checks need to be automated in CI/CD pipeline which might not be trivial to do.
It was a funny and very interesting journey, for those who want to dig a bit more in PGSharp, I pushed some materials and documents on GitHub. In particular, you can find the symbol list of libmain.so based on reverse engineering. ## Acknowledgments This analysis has been independently done in my spare time while being at [Quarkslab](https://www.quarkslab.com) and UL, my current employer. ## Annexes ### Third-Party Here is the (non exhaustive) list of the open-source projects used by PGSharp: | | :------ | https://github.com/jmpews/Dobby | https://github.com/or-dvir/EasySettings | https://github.com/zupet/LuaTinker or https://github.com/yanwei1983/luatinkerE | https://github.com/DimaKoz/stunning-signature | https://www.sqlite.org/index.html | https://github.com/nlohmann/json | https://www.lua.org/manual/5.3/ | https://github.com/kikito/md5.lua | https://github.com/rxi/json.lua ### List of the Unity Functions used by PGSharp > **Spoiler: Expand** ```text object__Invoke String_CreateString1 String_CreateString3 ulong_object___get_Item ulong_object___ContainsKey ulong_object___TryGetValue Application_OpenURL Application_set_targetFrameRate Quaternion_Angle PlayerPrefs_TrySetInt PlayerPrefs_TrySetFloat PlayerPrefs_TrySetSetString PlayerPrefs_SetInt PlayerPrefs_GetInt PlayerPrefs_SetFloat PlayerPrefs_GetFloat PlayerPrefs_SetString PlayerPrefs_GetStringNoDefault PlayerPrefs_HasKey PlayerPrefs_DeleteKey Component_get_gameObject Transform_get_rotation Animator_get_speed Animator_set_speed Animator_SetTriggerID Animator_Update Promise__ctor Promise_Complete MapMath_MetersBetween NL_NLAny_object_ NL_NLFirst_object_ InputField_ActivateInputField InputField_DeactivateInputField Text_set_text Text_set_fontSize DiContainer_InjectExplicitInternal Schedule_WaitOn_c__AnonStorey0____m__0 GameState_EnterState GameState_ExitState RpcBindings_Send RpcManager_DispatchCallbacks Animator_SetTriggerID RpcManager_DispatchCallbacks AuthService_get_CachedCredentialsExist AuthService_Logout DeviceServiceExtensions_IsUsable GameMasterData_IsPokemonWeatherBoosted Animator_SetTriggerID AuthService_get_CachedCredentialsExist AuthService_Logout PgpApi_UpdateNotifications ARPlusEncounterValuesProto__ctor ARPlusEncounterValuesProto__cctor PlayerService_GetPlayerDayBucket PlayerService_get_PlayerStats PlayerService_get_CurrentPokeball PlayerService_get_CurrentLinkedLogins AuthService_get_CachedCredentialsExist PlayerService_get_PokemonBag PlayerService_GetPlayerProfile PlayerService_set_CurrentPokeball PlayerService_get_BagIsFull PlayerService_GetCandyCountForPokemon StateToken_Complete TimeUtil_ServerNowMs RequestGymDetailsById_onSucceed RequestGymDetailsById_onError PlayerPrefs_SetInt BluetoothUtil_get_IsBluetoothEnabled PgpGuiController_ClickIcon PgpGuiService_SetSfidaIconVisible PgpGuiService_EnableSfidaIcon ulong_object___get_Item PgpService_get_IsSessionActive PgpService_GetCurrentNotificationType ItemBagImpl_GetItemCount PokemonBagImpl_GetPokemon ulong_object___get_Item PgpApi_UpdateNotifications Animator_set_speed StateToken_Complete VersionCheckService_CheckVersion ulong_object___TryGetValue QuestMapPokemon_get_Pokemon QuestService_BeginQuestEncounterWithOut EulaGuiController_PressAccept StarterMapPokemon_get_Pokemon OpenRemoteGym_gymOpner OpenRemoteGym_onSucceed BluetoothUtil_get_IsBluetoothEnabled QuestService_BeginQuestEncounterWithOut RaidState_ExitGymWithRaidDetails AccountChoiceState_ClickNewPlayer AccountChoiceState_ClickExistingPlayer LoginAgeGateState_SubmitSelections LoginChoiceState_ClickPtc LoginChoiceState_ClickGoogle LoginGuiController_ClickSubmit PtcLoginState_SubmitLogin I18n_PokemonMoveName I18n_SetUpLanguageTable I18n_PokemonNameTemporaryEvolution I18n_Text I18n_PokemonName FriendsGuiState_StartOpenGiftFlow FriendsGuiState_StartSendGiftFlow FriendsRpcService_RemoveGiftbox GiftingRpcService_SendGift GiftingRpcService_OpenGift StickerService_GetStickerInventory CombatDirector_Initialize MapPokestop_get_PoiId MapPokestop_get_ActiveIncidentType MapPokestop_get_Location EncounterParkCameraController_PlayIntro RunPokemonCaptured_onDitto EncounterInteractionState_RunAway MapMath_MetersBetween PlayerPrefs_TrySetSetString EncounterInteractionState_IntroCompleted AttemptCapture_onResponse EncounterIntroState_ExitState EncounterPokemon_get_MapPokemon PlayerPrefs_HasKey ItemBagImpl_GetItemCount Pokeball_TryHitPokemon Pokeball_FlyStateImpl_Capture__MoveNext Pokeball_DropStateImpl_Capture__MoveNext EncounterGuiController_ShowPokemonFlee EncounterState_get_EncounterType EncounterState_EncounterStateComplete EncounterState_EncounterStateComplete EncounterState_get_MapPokemon EncounterState_OnEncounterResponse DefaultEncounter_get_DefaultBall ExtraMapPokemon_get_Pokemon ResearchEncounter_get_DefaultBall ResearchEncounter_get_DefaultBall object_object__object___CurrentPageIndex PokemonInventoryCellView_Initialize ToastService_OneLineMedium ToastService_RewardItemNameAmount ToastService_RewardItemDefault ToastService_RewardItemStardust ToastService_OneLineMediumWithParams ToastService_RewardItemXlCandy ToastService_RewardItemAmount ToastService_TwoLine ToastService_RewardItemAmountType ToastService_RewardItemMegaResource ToastService_RewardSticker ToastService_OneLineWithParams ToastService_OneLineBig ToastService_OneLineBigWithParams ToastService_RewardItemCandy UserPromptsService_HasActiveModal UserPromptsService_DismissActiveModal PokemonInfoDynoScrollRect_Cleanup Quaternion_Angle Animator_get_speed PokemonInfoPanel_DoUpdate GymRootController_get_View GymRootController_get_MapGym MapGym_get_PoiId MapGym_OnTap MapGym_get_Location RaidMapPokemon_get_Pokemon MapContentHandler_UpdateCells MapEntityCell_get_Pois MapEntityService_get_Cells MapEntityService_GetMapPoi MapEntityService_UpdatePois MapExploreState_GymSelected MapExploreState_EnterQuestEncounter MapPokemon_get_Location MapPokemon_get_DespawnTime MapPokemon_TryCapture PhotobombingMapPokemon_get_Pokemon MapPokestop_get_ActiveIncidentType SendEncounterRequestCapture_onResponse PoiMapPokemon_get_SpawnPointId PoiMapPokemon_get_EncounterId WildMapPokemon_get_Pokemon WildMapPokemon_SendEncounterRequest PoiDirectoryService_AddPokemon PoiDirectoryService_RemovePokemon RaidMapPokemon_get_Pokemon IncidentMapPokemon_get_Pokemon IncenseMapPokemon_SendEncounterRequest IncenseMapPokemon_OnDestroy IncenseMapPokemon_get_Pokemon TroyDiskMapPokemon_SendEncounterRequest TroyDiskMapPokemon_get_Pokemon GroundTapHandler_OnTap GroundTapHandler_OnTap1 MapViewHandler_GetGroundLocation MapViewHandler_GetGroundPosition MapViewHandler_GetWorldLocation NL_NLFirst_object_ CompassGuiController_Update PlayerService_SetPlayerProto MapPokemon_LogEncounterMetrics ``` [^weak-obf]: O-LLVM provides control-flow obfuscation that can be *recovered* with emulation. On the other hand, the data-flow (function parameters, stack values, memory accesses) can be analyzed at a basic-block level with static analysis. [^qb-modded]: Modded apps use similar tricks as discussed in [Android Application Diffing: Analysis of Modded Version](https://blog.quarkslab.com/android-application-diffing-analysis-of-modded-version.html#defeating-obfuscation) [^static-link]: libmain.so is also **statically** linked against other libraries like [jmpews/Dobby](https://github.com/jmpews/Dobby), [nlohmann/json](https://github.com/nlohmann/json), [SQLite](https://github.com/sqlite/sqlite) ... [^aes-hook]: One can also hook the AES encrypt/decrypt functions whose prototype is ``(uint8_*t key_schedule, uint8_t* inout_buffer, size_t size)`` [^rename]: The package names are stripped with Proguard but we can quite easily recover those packages. [^hook-hide]: Basically, they renamed [DobbyHook](https://github.com/jmpews/Dobby/blob/bba23cbee8e3cfff5622ef8b63fb797703baea5f/include/dobby.h#L157) in ``FbbUePBslRNHWkdS`` [^illoadinfo]: The event is triggered when ``nativeMuteMasterAudio`` or ``nativeRender`` is registered and they get the base address by iterating over ``/proc//maps``. [^miss-ollvm]: O-LLVM seems not applied on this function [^wrapper]: In order to arbitrarily call the underlying function when needed. --- # Gotta Catch 'Em All: Frida & jailbreak detection - Canonical: https://www.romainthomas.fr/post/21-07-pokemongo-anti-frida-jailbreak-bypass/ - Markdown: https://www.romainthomas.fr/post/21-07-pokemongo-anti-frida-jailbreak-bypass/index.md - Section: post - Published: 2021-07-18 - Modified: 2026-09-05 - Tags: ios, reverse engineering, obfuscation > This blog post analyzes the Frida and Jailbreak detection in PokemonGO for iOS. > **Note** Do not expect a *click & play* solution for PokemonGO in this blog post. This blog post is more about the technical aspects of jailbreak detection than a bypass for this game. ## Introduction While working on LIEF during my vacations to support in-memory parsing for Mach-O files, I found that PokemonGO was an interesting use case to introduce this feature. It led me to look at the jailbreak and Frida detection implemented in this game. Being more familiar with Android than iOS, the analysis workflow on this platform is quite different, which is also a good opportunity to improve tooling. The first challenge stems from jailbreaking the device. Fortunately, checkra1n eases this step. The second difficulty lies in extracting the encrypted iOS app from the device. In contrast to Android, iOS apps are encrypted on the disk and decrypted by the kernel when loaded. It means that one way to get the unencrypted code is to dump the file from memory. One could also leverage the function ``mremap_encrypted()`` as described in [Decrypting Apps on iOS](https://www.linkedin.com/pulse/decrypting-apps-ios-john-coates/). ## PokemonGO Overview When running PokemonGO on a jailbroken device[^DEVICE], the application immediately crashes with the following backtrace: ```text 0 ??? 0x000000020ac46ab8 0 + 8770579128 1 libdyld.dylib 0x0000000184df8304 invocation function for block in dyld3::AllImages::runAllInitializersInImage(dyld3::closure::Image const*, dyld3::MachOLoaded const*) + 136 2 libdyld.dylib 0x0000000184dea5b0 dyld3::closure::Image::forEachInitializer(void const*, void (void const*) block_pointer) const + 96 3 libdyld.dylib 0x0000000184df8160 invocation function for block in dyld3::AllImages::runInitialzersBottomUp(dyld3::closure::Image const*) + 296 4 libdyld.dylib 0x0000000184deae6c dyld3::closure::Image::forEachImageToInitBefore(void (unsigned int, bool&) block_pointer) const + 92 5 libdyld.dylib 0x0000000184df8b48 dyld3::AllImages::loadImage(Diagnostics&, char const*, unsigned int, dyld3::closure::DlopenClosure const*, bool, bool, bool, bool, void const*) + 776 6 libdyld.dylib 0x0000000184df8698 dyld3::AllImages::dlopen(Diagnostics&, char const*, bool, bool, bool, bool, bool, void const*, bool) + 872 7 libdyld.dylib 0x0000000184dfa2b4 dyld3::dlopen_internal(char const*, int, void*) + 368 8 libdyld.dylib 0x0000000184ded5b0 dlopen_internal(char const*, int, void*) + 108 9 CoreFoundation 0x00000001850ed038 _CFBundleDlfcnLoadFramework + 136 10 CoreFoundation 0x00000001850be974 _CFBundleLoadExecutableAndReturnError + 376 11 Foundation 0x0000000186359ba8 -[NSBundle loadAndReturnError:] + 332 12 pokemongo 0x00000001041a7c5c 0x1041a0000 + 31836 13 pokemongo 0x00000001041a7d50 0x1041a0000 + 32080 14 libdyld.dylib 0x0000000184de9588 start + 4 ``` > **Note** The full crash log is available [here](backtrace.log). In this backtrace, the main ``pokemongo`` binary is a kind of *stub* that loads the Unity binary: ``UnityFramework`` which contains the main logic of the game. This library is loaded by the ``dlopen_internal`` function at index **8** in the backtrace as a result of ``-[NSBundle loadAndReturnError:]``. Since ``UnityFramework`` depends on other libraries, they are (pre)loaded with ``Image::forEachImageToInitBefore`` which processes the following files: 1. @/usr/lib/libc++.1.dylib 2. ... 3. @rpath/NianticLabsPlugin.framework/NianticLabsPlugin 4. ... 5. @rpath/libswiftos.dylib Among those dependencies, we can notice the NianticLabsPlugin library which is a cross-platform Unity plugin - also present in the Android version - that contains the main protections of the game. These protections are used to prevent cheat, bots, GPS spoofing, in PokemonGO. The whole being obfuscated by Digital.ai (formerly known as Arxan). NianticLabsPlugin communicates with the ``UnityFramework`` through an exported function ``GetN2Api`` that returns an array of functions (pointers). The following figure outlines these different components: ![PokemonGO overview](overview.png) Getting back to the backtrace, if we assume that the application crashes when loading NianticLabsPlugin, it precisely crashes when calling the Mach-O constructors in ``AllImages::runAllInitializersInImage``. Since the application is heavily obfuscated, a static analysis reaches quickly its limits, which forces us to emulate or dynamically analyze the functions of interest. > **Note** The addresses of the functions/instructions mentioned in this blog post are based on the following version of NianticLabsPlugin: [NianticLabsPlugin - 2140426ccdfdfb2529f454697cb5cc83](NianticLabsPlugin.bin) PokemonGO v0.211.2 - June 2021 ## Analyzing Mach-O constructors with Frida From the previous section, we surmised that the application crashed because of the NianticLabsPlugin's constructors. Since these functions are called before **any other functions** of the library, it raises the question of finding a way to perform actions (or hook) before they are executed. On Android, when we need to analyze a library's constructors, we can hook the ``call_array`` function from [Bionic's linker (ELF loader)](https://github.com/aosp-mirror/platform_bionic/blob/c44b1d0676ded732df4b3b21c5f798eacae93228/linker/linker_soinfo.cpp#L488): ```cpp // Mangled as __dl__ZL10call_arrayIPFviPPcS1_EEvPKcPT_mbS5_ in /system/bin/linker64 template static void call_array(const char* array_name __unused, F* functions, size_t count, bool reverse, const char* realpath) { if (functions == nullptr) { return; } TRACE("[ Calling %s (size %zd) @ %p for '%s' ]", array_name, count, functions, realpath); int begin = reverse ? (count - 1) : 0; int end = reverse ? -1 : count; int step = reverse ? -1 : 1; for (int i = begin; i != end; i += step) { TRACE("[ %s[%d] == %p ]", array_name, i, functions[i]); call_function("function", functions[i], realpath); } TRACE("[ Done calling %s for '%s' ]", array_name, realpath); } ``` If we try to apply the same approach on iOS, the mirror of the ELF loader on iOS is ``dyld`` which contains most of the logic to load Mach-O files. It turns out that at some points, the Mach-O's constructors are processed in the ``doModInitFunctions`` function (from [ImageLoaderMachO.cpp](https://github.com/apple-opensource/dyld/blob/1128192c016372ae94793d88530bc5978c1fce93/src/ImageLoaderMachO.cpp#L2290)). ```cpp void ImageLoaderMachO::doModInitFunctions(const LinkContext& context) { ... for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) { const uint8_t type = sect->flags & SECTION_TYPE; if ( type == S_MOD_INIT_FUNC_POINTERS ) { Initializer* inits = (Initializer*)(sect->addr + fSlide); ... if (!this->containsAddress(stripPointer((void*)func)) ) { dyld::throwf("initializer function %p not in mapped image for %s\n", func, this->getPath()); } ... func(context.argc, context.argv, context.envp, context.apple, &context.programVars); } } ... } ``` From this code, we can notice that **all** constructor addresses are checked **beforehand** by the ``containsAddress`` function. Therefore, it makes this function a good hooking spot as it is executed before calling the constructor itself. One can use the native SDK of [frida-gum](https://github.com/frida/frida-gum) to perform this action: ```cpp // Address of ImageLoader::containsAddress in /usr/lib/dyld const uintptr_t containsAddress_ptr = ...; // Setup hooks with gum_interceptor_attach GumAttachReturn attach_ret = gum_interceptor_attach( listener_->interceptor, /* target */ reinterpret_cast(containsAddress_ptr), reinterpret_cast(listener_), /* ID */ reinterpret_cast(containsAddress_ptr) ); .... // Equivalent of onEnter in Javascript void native_listener_on_enter(GumInvocationListener* listener, GumInvocationContext* ic) { const uintptr_t ctor_function_addr = ic->cpu_context->x[1]; // Do stuff with ctor_function_addr } ``` > **Note** ``containsAddress`` is a member function, therefore ``x0`` contains a pointer on ``this`` and the address to check is located in ``x1``. By hooking ``containsAddress()``, we get the **control before** the execution of the constructors. It gives us the ability to perform the following actions that can help to identify the constructor involved in the crash: 1. Trace the constructors (see: [constructors_trace.log](constructors_trace.log)) 2. Replace/disable a constructor (``gum_interceptor_replace``) 3. Detect the **first** constructor and hook the next ones (``gum_interceptor_attach``) NianticLabsPlugin embeds no less than 120 constructors among those, 6[^SIXCTOR] are involved in detecting Frida, jailbroken devices, anti-debug, etc: | Index | Offset | Description | |-------|----------|----------------------------------------| | 15 | 0x4369e0 | Anti-debug & anti-emulation | | 16 | 0x00e0d8 | Frida detection | | 17 | 0x26bd5c | Anti-bypass? | | 18 | 0x449b84 | Anti-jailbreak, anti-Frida | | 19 | 0x731b90 | Anti-jailbreak, anti-debug, anti-frida | | 20 | 0x359194 | Anti-jailbreak | Once we reduced the set of functions involved in the crash, we can combine dynamic analysis with Frida and emulation with Unicorn. ## Anti-debug One of the redundant checks we can find in many functions (not only the constructors) are the anti-debugs. They always come in two parts: 1. Try to *"kill"* its own pid with the 0-signal 2. Check if PTRACE is flagged ```cpp void try_kill() { const int pid = getpid(); // syscall@0x436cdc int ret = kill(pid, 0); // syscall@0x436d28 } ``` According to the man page of kill (``man 2 kill``), the signal ``0`` is used to check that the ``pid`` given in the first parameter really exists. > [...] A value of 0, however, will cause error checking to be performed (with no signal being sent). This can be used > to check the validity of pid. This *kill* operation is followed by three ``PTRACE`` checks: ```cpp // Done three times inline bool ptrace_detect() { int32_t opt[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid(), }; kinfo_proc info; sysctl(opt, 4, &info, sizeof(kinfo_proc), nullptr, 0); return info.kp_proc.p_flag & P_TRACED; } ``` These three ``P_TRACED`` checks **always** come together: ```text 0x436cdc: getpid(): 6015 0x436d28: kill(6015, 0): 0 0x4374b0: sysctl(CTL_KERN, KERN_PROC, KERN_PROC_PID, 6015) 0x4371e8: sysctl(CTL_KERN, KERN_PROC, KERN_PROC_PID, 6015) 0x437398: sysctl(CTL_KERN, KERN_PROC, KERN_PROC_PID, 6015) ``` ## Frida Detection Frida is detected by the application through its client-server mode, which binds the localhost on the port ``27042``. When PokemonGO is starting, it tries to open a socket on this port and if it manages to connect, it tests the Frida handshake. ```text 0x00e3b8: getifaddrs(0x16b7fb518): 0x0 0x016990: socket('IPV4', 'TCP', '0'): 0x8 0x019d60: bind('PF_INET', 0x8, '127.0.0.1:27042') 0x01805c: close(0x8) 0x016990: socket('IPV4', 'TCP', '0'): 0x8 0x019d60: bind('PF_INET', 0x8, '192.168.0.26:27042') 0x01805c: close(0x8) 0x00e3ec: freeifaddrs(0x10601ac00): 0x105360a00 ``` The application also iterates over the list of the libraries loaded in memory with the ``_dyld_image_count``/``_dyld_get_image_name`` functions. Nevertheless, it seems that they are not used to detect Frida libraries artifacts (like ``FridaGadget.dylib``). ## Jailbreak Detection The application implements jailbreak detection by checking if some files are accessible or not on the device. Most of the checks are done by using the ``access()`` syscalls that are inlined in different places: ```text 0x44c390: access('/bin/grep', 0x0) ... 0x7326b0: access('/private/var/checkra1n.dmg', 0x0) ``` The list of the checked files is given in the [annexes of the blog post](#annexes). > **Note** This list is very close to [vnodebypass/hidePathList.plist](https://github.com/XsF1re/vnodebypass/blob/870b21fd3566736cca285355b4faf7f289baa4d5/layout/usr/share/vnodebypass/hidePathList.plist) In addition to *raw* ``access`` syscall, the application enhances its detection by creating a symbolic link of the root directory in a temporary app data directory: ``` 0x734e08: symlink('/Applications/..', '/private/var/mobile/Containers/Data/Application/D933FBC9-90E7-4584-851E-CE2D5E900446/tmp/WCH38bnM0x101a9e7d0') ``` Then, it performs the same checks with the app data directory as prefix: `[...]/tmp/WCH38bnM0x101a9e7d0`: ``` 0x7376d8: access('/private/var/mobile/Containers/Data/Application/D933FBC9-90E7-4584-851E-CE2D5E900446/tmp/3Odis0x101a9dfd0/usr/bin/passwd', 0x0) ``` ## Signature Check At some point, one function checks the integrity of the signature of the ``pokemongo`` binary. This check starts by opening the main ``pokemongo`` binary from **the disk**: ```text 0x7392ec: add x0, x19, #6,lsl#12 0x7392f0: add x0, x0, #0x540 0x7392f4: mov w1, #0x1000000 0x7392f8: mov x2, #0 0x7392fc: svc 0x80 ; x16 -> SYS_open = 5 // open('/private/var/containers/Bundle/Application/[...]/pokemongo.app/pokemongo'): fd_pgo ``` Then, it reads the beginning of the file in a stack buffer: ```text 0x74b494: ldr x0, [x19, #0xc0] ; fd 0x74b498: ldr x1, [x19, #0x130] ; buff 0x74b49c: ldr x2, [x19, #0xb8] ; buff_size 0x74b4a0: svc 0x80 ; x16 -> SYS_read = 3 // uint8_t macho_head[0x4167]; // 0x74b4a0: read(fd_pgo, macho_header, 0x4167); ``` To iterate over the Mach-O load commands: ```text ; x8 points to the read's buffer 0x73942c: ldr w8, [x8, #0x10] ; Number of LC_COMMANDS for (size_t i = 0; i < nb_cmds; ++i) { 0x74bc40: ldr w10, [x9, #4] ; Command's size 0x74ade4: ldr w9, [x9] ; command's type if (cmd.type == LC_CODE_SIGNATURE) { 0x74b1b8: ldr w10, [x10, #8] ; read signature offset -> 0xc3d0 } } ``` With the offset of the Mach-O ``LC_CODE_SIGNATURE`` command, it reads the raw signature using the ``lseek/read`` syscalls: ```text uint8_t sig_header[0x205]; 0x73a978: lseek(fd_pgo, LC_CODE_SIGNATURE offset, 0x0) 0x73aecc: read(fd_pgo, &sig_header, 0x205); ``` The raw signature buffer is processed by chunks of 10 bytes in a function that looks like a checksum: ```text [...] 0x73ad58: ldrsb w13, [x12] 0x73ad5c: mov w14, #83 0x73ad60: sub w13, w14, w13 0x73ad64: ldrsb w14, [x12, #1] 0x73ad68: mov w15, #87 0x73ad6c: sub w14, w15, w14 0x73ad70: ldrsb w16, [x12, #2] 0x73ad74: mov w17, #53 0x73ad78: sub w16, w17, w16 0x73ad7c: ldrsb w17, [x12, #3] 0x73ad80: mov w0, #52 0x73ad84: sub w17, w0, w17 0x73ad88: ldrsb w0, [x12, #4] 0x73ad8c: sub w0, w15, w0 0x73ad90: ldrsb w1, [x12, #5] 0x73ad94: mov w2, #51 0x73ad98: sub w1, w2, w1 0x73ad9c: ldrsb w2, [x12, #6] 0x73ada0: mov w3, #54 0x73ada4: sub w2, w3, w2 0x73ada8: ldrsb w3, [x12, #7] 0x73adac: sub w15, w15, w3 0x73adb0: ldrsb w3, [x12, #8] 0x73adb4: mov w4, #78 0x73adb8: sub w3, w4, w3 0x73adbc: ldrsb w12, [x12, #9] 0x73adc0: mov w4, #70 0x73adc4: sub w4, w4, w12 [...] ``` I did not manage to identify the underlying checksum algorithm, but it involves square multiplications and the key(?): ``SW5436NF`` ## Control-Fault Injection Once we determined the functions involved in the detections, we might want to disable them in order to run the game smoothly. Actually, PokemonGO is protected against such bypass with global variables that assert if a function ran successfully or not. This protection is equivalent to the following piece of code: ```cpp static constexpr uintptr_t GOOD = 0x00627178; // bqx ? static uintptr_t MAGIC_CFI = 0xdeadc0de; __attribute__((constructor)) void frida_detect() { if (is_frida_running()) { crash(); } MAGIC_CFI = GOOD; } __attribute__((constructor)) void control_fault_check() { if (MAGIC_CFI != GOOD) { crash(); } } ``` If we only disable ``frida_detect()``, the application will crash because of ``control_fault_check()``. We could bypass this protection by identifying the address of the ``MAGIC_CFI`` in the ``__data`` section, or by disabling the ``control_fault_check()``. ## What about LIEF? As mentioned in the introduction, it started with an ongoing feature to parse Mach-O files from memory. Basically, LIEF will[^WILLETA] enable parsing Mach-O files[^2] from an absolute address with this kind of API: ```cpp // 0x10234400 -> start of the Mach-O file auto bin = LIEF::MachO::Parser::parse_from_memory(0x10234400); ``` Depending on the user's needs, the ``write()`` operation will optionally undo all the relocations and the symbol bindings. This could be useful if we aim at (re)running the file dumped (on an Apple M1?). As expected, the strings used within the NianticLabsPlugin library are encoded by the obfuscator. We could statically analyze the decoding routine (cf. Tim Blazytko's [blog post](https://synthesis.to/2021/06/30/automating_string_decryption.html)) ,but another technique consists in using a property of the obfuscator's string encoding mechanism. It seems that the obfuscator put **all** the strings[^3] in the data section and decrypts **all of them** in a **single** constructor function. For instance, if we have the string "TOKEN" to protect in the following functions: ```cpp void protect_me() { sensitive("TOKEN"); } void protect_me_2() { sensitive("TOKEN2"); } ``` The obfuscator transforms and decodes the strings into something like: ```cpp // __data section static char var_TOKEN[] = "\x00\x1D\xDD\xEE\xAB"; static char var_TOKEN_2[] = "\x00\x1D\xDD\xEE\xAF"; __attribute__((constructor)) void decode_strings() { decode(var_TOKEN); decode(var_TOKEN_2); } void protect_me() { sensitive(var_TOKEN); } void protect_me_2() { sensitive(var_TOKEN_2); } ``` Since **all** the strings are decoded at once in one of the first constructors, if we manage to dump the binary right after this constructor, we can recover the original strings for free. Programmatically, it can be done using (again) frida-gum SDK with the following pseudocode: ```cpp // Hook associated with ImageLoader::containsAddress void native_listener_on_enter(GumInvocationListener* listener, GumInvocationContext* ic) { static size_t CTOR_ID = 0; const uintptr_t ctor_function_addr = ic->cpu_context->x[1]; std::string libname = module_from_addr(ctor_function_addr); if (libname == "NianticLabsPlugin" && CTOR_ID++ == 3) { const uintptr_t base_address = base_addr_from_ptr(ctor_function_addr); auto bin = LIEF::MachO::Parser::parse_from_memory(base_address); bin->write("/tmp/pokemongo_after_ctor.bin"); // /tmp on the iPhone } } ``` In the end, the dumped file contains the decoded strings: ![Data area after LIEF dump](strings.png) If we skim the ``__data`` section, we can also observe the following changes: ![Data area after LIEF dump](data_dump.png) A practiced eye might notice[^4] that some strings of the section are actually embedded in Protobuf structures. We can confirm this observation by trying to infer the data as Protobuf types: ```python from . import proto_dump import lief pgo = lief.parse("pokemongo_after_ctor.bin") start = 0x12A51A7 end = 0x12A51E2 raw_proto = pgo.get_content_from_virtual_address(start, end - start) print(proto_dump(raw_proto)) ``` ```text { #3 = 4 #4 (repeated) = 1 { #1 = "CheatReputation" #2 (repeated) = { #1 = "UNSET" #2 = 0 } { #1 = "BOT" #2 = 1 } { #1 = "SPOOFER" #2 = 2 } } #5 = 8 #8 = [] } ``` ## Final Words The application embeds other checks in the constructors and in the functions returned by ``GetN2Api``. It can make a good exercise for those that are interested in. Generally speaking, the application, and the protections are well designed since they slow down reverse engineers. Nevertheless, ``anti-{jb, frida, debug}`` are quite difficult to protect as they need to interact with the OS through functions or syscalls with unprotected parameters. As a result, and once identified, we can bypass them. One technique consists in injecting a library with Frida's injector that aims at hooking the ``containsAddress()`` to disable/patch the functions involved in the detections: ![PokemonGo Jailbreak bypass](jbfree.png) [Video](pokemongo_jb_bypass.mp4) Nevertheless, this technique is **not persistent** and version-dependant. After writing this post, it turned out that its structure is very close to [Reverse Engineering Starling Bank](https://hot3eed.github.io/2020/08/02/starling_p2_detections_mitigations.html). In particular, we can find the same anti-debug and the same Frida detection routine. These similarities suggest that these two application uses the same obfuscator that also provides ``anti-{jb, frida, debug}`` as built-in. You might also be interested in the recent talk of [Eloi Benoist-Vanderbeken](https://twitter.com/elvanderb) [@Pass the Salt](https://2021.pass-the-salt.org/) Watch the talk Download the slides who detailed another approach to identify and bypass jailbreak detections. > **Note** [LIEF](https://lief-project.github.io) is a tool developed at [Quarkslab](https://www.quarkslab.com/) along with [QBDI](https://qbdi.quarkslab.com) & [Triton](https://triton.quarkslab.com). ----------- ### Annexes | Files that trigger the JB detection | Files that should be present | |--------------------------------------------------------------------|------------------------------| | ``/.bootstrapped_electra`` | ``/cores`` | | ``/Applications/Anemone.app`` | ``/dev/null`` | | ``/Applications/Cydia.app`` | ``/etc/hosts`` | | ``/Applications/SafeMode.app`` | ``/etc/passwd`` | | ``/Library/Frameworks/CydiaSubstrate.framework`` | ``/sbin`` | | ``/Library/MobileSubstrate/DynamicLibraries/FlyJB.dylb`` | ``/sbin/launchd`` | | ``/Library/MobileSubstrate/MobileSubstrate.dylib`` | ``/sbin/mount`` | | ``/Library/PreferenceBundles/LaunchInSafeMode.bundle`` | ``/usr`` | | ``/Library/PreferenceLoader/Preferences/LaunchInSafeMode.plist`` | | | ``/Library/Themes`` | | | ``/Library/dpkg/info/com.inoahdev.launchinsafemode.list`` | | | ``/Library/dpkg/info/com.inoahdev.launchinsafemode.md5sums`` | | | ``/bin/bash`` | | | ``/bin/bunzip2`` | | | ``/bin/bzip2`` | | | ``/bin/cat`` | | | ``/bin/chgrp`` | | | ``/bin/chmod`` | | | ``/bin/chown`` | | | ``/bin/cp`` | | | ``/bin/grep`` | | | ``/bin/gzip`` | | | ``/bin/kill`` | | | ``/bin/ln`` | | | ``/bin/ls`` | | | ``/bin/mkdir`` | | | ``/bin/mv`` | | | ``/bin/sed`` | | | ``/bin/sh`` | | | ``/bin/su`` | | | ``/bin/tar`` | | | ``/binpack`` | | | ``/bootstrap`` | | | ``/chimera`` | | | ``/electra`` | | | ``/etc/apt`` | | | ``/etc/profile`` | | | ``/jb`` | | | ``/private/var/binpack`` | | | ``/private/var/checkra1n.dmg`` | | | ``/private/var/lib/apt`` | | | ``/usr/bin/diff`` | | | ``/usr/bin/hostinfo`` | | | ``/usr/bin/killall`` | | | ``/usr/bin/passwd`` | | | ``/usr/bin/recache`` | | | ``/usr/bin/tar`` | | | ``/usr/bin/which`` | | | ``/usr/bin/xargs`` | | | ``/usr/lib/SBInject`` | | | ``/usr/lib/SBInject.dylib`` | | | ``/usr/lib/TweakInject`` | | | ``/usr/lib/TweakInject.dylib`` | | | ``/usr/lib/TweakInjectMapsCheck.dylib`` | | | ``/usr/lib/libjailbreak.dylib`` | | | ``/usr/lib/libsubstitute.0.dylib`` | | | ``/usr/lib/libsubstitute.dylib`` | | | ``/usr/lib/libsubstrate.dylib`` | | | ``/usr/libexec/sftp-server`` | | | ``/usr/sbin/sshd`` | | | ``/usr/share/terminfo`` | | | ``/var/mobile/Library/.sbinjectSafeMode`` | | | ``/var/mobile/Library/Preferences/jp.akusio.kernbypass.plist`` | | [^DEVICE]: iPhone 6 running on iOS 14.2 with checkra1n. [^2]: The Mach-O format is very suitable for this feature as the header in mapped in memory. Therefore, it eases the parsing. [^3]: More generally, it can encode local data (strings, bytes arrays, ...) [^4]: Protobuf strings can be identified as they usually start with ``0xA``, ``0xB``, followed by their lengths and the string itself (see: [protocol-buffers/docs/encoding](https://developers.google.com/protocol-buffers/docs/encoding#strings)) [^SIXCTOR]: We can identify them by trial and error. [^WILLETA]: ETA: likely by the end of the year --- # r2-pay: whitebox (part 2) - Canonical: https://www.romainthomas.fr/post/20-09-r2con-obfuscated-whitebox-part2/ - Markdown: https://www.romainthomas.fr/post/20-09-r2con-obfuscated-whitebox-part2/index.md - Section: post - Published: 2020-09-27 - Modified: 2026-08-04 - Tags: android, reverse engineering, write-up, obfuscation, whitebox, cryptography > This second blog post explains how to recover the whitebox's key from the obfuscated library libnative-lib.so ## Introduction In the [first part](/post/20-09-r2con-obfuscated-whitebox-part1/) of this write-up, we described the anti-frida, anti-debug and anti-root techniques used in the application and how to remove most of them. This second part digs into the JNI function ``gXftm3iswpkVgBNDUp`` and the underlying whitebox implementation. ## Library Shimming The inputs of the function ``gXftm3iswpkVgBNDUp`` are provided by the GUI widgets and the function is triggered when we press the *Generate R2Coin* button. Nevertheless, the behavior of ``gXftm3iswpkVgBNDUp`` does not rely on UI features nor the application's context[^1]. To take a closer look at the logic of ``gXftm3iswpkVgBNDUp``, it would be pretty useful to be able to feed the function's inputs with our **own standalone binary**. Basically, we would like to achieve this kind of interface: ```cpp int main(int argc, char** argv) { void* dlopen("libnative-lib.so", RTLD_NOW); ... jbyteArray out = gXftm3iswpkVgBNDUp(env, ...); return 0; } ``` This technique is not new and has been already described in a blog post by [Caleb Fenton](https://twitter.com/caleb_fenton)[^2]. The idea is to get the ``JNIEnv* env`` variable with ``JNI_CreateJavaVM`` which is exported by the Android runtime: ``libart.so``. Once we have this variable, we can call the ``gXftm3iswpkVgBNDUp`` function as well as manipulating the JNI buffers: - ``env->NewByteArray()`` - ``env->GetArrayLength()`` - ... ![Shimming of whitebox library](shim_mechanism.png) Long story short, we can instantiate the Android runtime with the following piece of code: ```cpp int main(int argc, char** argv) { JavaVMOption opt[2]; opt[0].optionString = "-Djava.class.path=/data/local/tmp/re.pwnme.1.0.apk"; opt[1].optionString = "-Djava.library.path=/data/local/tmp"; JavaVMInitArgs args; args.version = JNI_VERSION_1_6; args.options = opt; args.nOptions = 2; args.ignoreUnrecognized = JNI_FALSE; void* handler = dlopen("/system/lib64/libart.so", RTLD_NOW); auto JNI_CreateJavaVM_f = reinterpret_cast(dlsym(handler, "JNI_CreateJavaVM")); JNI_CreateJavaVM_f(&jvm, &env, &args); } ``` Then, we can resolve the ``gXftm3iswpkVgBNDUp`` function with the base address of ``libnative-lib.so`` and the associated offset ``0x9B41C``: ```cpp void* hdl = dlopen("libnative-lib.so", RTLD_NOW); uintptr_t base_address = get_base_address("libnative-lib.so"); using gXftm3iswpkVgBNDUp_t = jbyteArray(*)(JNIEnv*, jobject, jbyteArray, jbyte); gXftm3iswpkVgBNDUp = reinterpret_cast(base_address + 0x9B41C); ``` Finally, we can run the function with our own inputs: ```cpp std::string pin_amount = "0000123400004567"; jbyteArray array = convert_to_jbyteArray(pin_amount, ptr); jbyteArray jencrypted_buffer = gXftm3iswpkVgBNDUp(env, nullptr, array, 0xF0); const std::vector encrypted_buffer = from_jbytes(jencrypted_buffer); std::string hex_str = to_hex(encrypted_buffer); LOG_INFO("{} --> {}", pin_amount, ref_str); ``` > **Note** The whole implementation is available [here ](https://github.com/romainthomas/r2pay/blob/master/shim-whitebox). ## Function Tracing Now that we are able to run the ``gXftm3iswpkVgBNDUp`` function without the GUI layer, we can easily create an interface with [QBDI](https://qbdi.quarkslab.com): ```cpp VM vm; vm.addInstrumentedModule("libnative-lib.so"); ... jbyteArray array = to_jarray(pin_amount, ptr); jbyteArray qbdi_encrypted_buffer; vm.call( /* ret */ reinterpret_cast(&qbdi_encrypted_buffer), /* target */ reinterpret_cast(gXftm3iswpkVgBNDUp), /* params */ { /* p_0: JNIEnv* */ reinterpret_cast(env), /* p_1: jobject thiz */ reinterpret_cast(nullptr), /* p_2: inbuffer */ reinterpret_cast(array), 0xF0 } ); ``` The execution in QBDI **without user's callbacks** takes about **3minutes 30s** which is quite huge compared to the **real execution** that takes about **853 milliseconds**: ![Performances with different configurations](benchmark.svg) This overhead is mostly due to the function ``0x1038f0`` that is executed ~20 000 times. After a quick analysis, it turns out that this function is not relevant to instrument to break the whitebox. We can force its *real* execution (i.e. outside QBDI) **by removing the function's address from the instrumented range**[^3]. ```cpp static constexpr uintptr_t HEAVY_FUNCTION = 0x1038f0; vm.removeInstrumentedRange( base_address + HEAVY_FUNCTION, base_address + HEAVY_FUNCTION + 1 ); ``` This small adjustment **drops the execution to 3'30 seconds**. --- Some cryptographic algorithms can be fingerprinted either with predefined constants or with their memory accesses. According to the Quarkslab's blog post: [Differential Fault Analysis on White-box AES Implementations](https://blog.quarkslab.com/differential-fault-analysis-on-white-box-aes-implementations.html), the whitebox lookup tables are likely to be stored in the ``.data, .rodata, ...`` sections. By looking at the sizes of these sections, only the ``.data`` section seems to have an appropriate size. We can generate a memory trace on this section to see if we can outline some patterns. It can be made with the following piece of code: ```cpp vm.recordMemoryAccess(MEMORY_READ_WRITE); vm.addMemRangeCB( /* .data start address */ base_address + 0x127000, /* .data end address */ base_address + 0x127000 + 0x8e000, /* Record both: reads and writes */ MEMORY_READ_WRITE, /* Memory callback */ [] (VM* vm, GPRState*, FPRState*, void* data) { auto ctx = reinterpret_cast(data); /* * 'for' loop since on AArch64 we can have multiple reads / writes * at once. (e.g. stp x0, x1, [sp, #128]) */ for (const MemoryAccess& mem_access : vm->getInstMemoryAccess()) { ctx->trace->push_back({ mem_access.instAddress - base_address, mem_access.accessAddress - base_address, mem_access.size, }); } return VMAction::CONTINUE; }, &ctx); ``` > **Note** Generating the memory trace takes about 11 seconds which is acceptable. It leads to the following graph in which we can notice a characteristic pattern at the end of the trace: ![Memory trace generated with QBDI](memory_trace.png) ## Fault Injection The pattern at the end of the trace is quite characteristic of AES-128 where we can identify 10 rounds. ![AES rounds](rounds.png) We now have all the necessary information to make a *fault injection attack*: 1. We can identify the 9th round 2. We can **accurately** fault the ``.data`` section thanks to the memory trace ![Fault injection in the 9th round](injection.png) > **Note** The memory trace is available in the [ mem_trace.JSON](https://github.com/romainthomas/r2pay/blob/master/assets/mem_trace.json) file of the repository. To efficiently make the injection, we can be first reduce the memory addresses to only keep those that are used in the last 2 rounds: ```python trace_file = CWD / ".." / "assets" / "mem_trace.json" trace = json.loads(trace_file.read_bytes())[0] # Keep the entries that are involved in the last 2-rounds (empirical number) nice_trace = trace[-1000:] ``` Then, we can use our shim mechanism to inject the faults in the ``.data`` section with the addresses previously selected. Moreover, we can reduce the set of ``.data`` addresses with the faults that introduce exactly **4 differences** in the ciphertext: ```cpp // Make sure the .data section is writable mprotect( reinterpret_cast(base_address + /* .data */ 0x127000), 0x8e000, PROT_READ | PROT_WRITE ); for (uintptr_t fault_addr : selected_addresses) { uint8_t& target_byte = *reinterpret_cast(base_address + fault_addr); uint8_t backup = target_byte; // Fault 1 byte: target_byte ^= 0x33; // Run the whitebox with the faulty byte const std::vector encrypted = encrypt(msg); // Restore the original byte target_byte = backup; // Compute the number of errors // ... } ``` Finally, with the subset of the addresses that affect exactly 4 bytes, we can generate several faults for a given address: ```cpp for (uintptr_t nice_fault_addr : four_bytes_fault_addresses) { for (size_t i = 0; i < 255; ++i) { const std::vector& output = inject_fault(addr, PIN_AMOUNT, i); const size_t nb_errors = get_error(genuine_value, output); if (nb_errors == 4 and unique.insert(output).second) { // Record the entry ... } } } ``` The code above gives an idea about how to generate the faults. One can find the whole implementation in this file: [shim-whitebox/src/main.cpp](https://github.com/romainthomas/r2pay/blob/master/shim-whitebox/src/main.cpp#L343-L365) that produces this set of files [assets/wb-traces](https://github.com/romainthomas/r2pay/blob/master/assets/wb-traces). ## Key Extraction Thanks to the [ Side-Channel Marvels](https://github.com/SideChannelMarvels) project, we can use [JeanGrey](https://github.com/SideChannelMarvels/JeanGrey), developed by Philippe Teuwen, to recover the whitebox's key from the faulty traces: ```python import pathlib import phoenixAES CWD = pathlib.Path(__file__).parent trace_dir = CWD / ".." / "assets" / "wb-traces" for f in trace_dir.iterdir(): x = phoenixAES.crack_file(f) if x is not None: print(x, f.name) ``` It provides the following results which enable retrieving the key: ```console $ python wb_key_recovery.py ..8D....7F............9A....79.. injection-1a930d.trace ..8D....7F............9A....79.. injection-1a95bd.trace ....19....62....B0............8F injection-1a91b2.trace ....19....62....B0............8F injection-1a8fdf.trace 76............1E....D3....E1.... injection-1a8549.trace ......E1....A0....CD....28...... injection-1a8978.trace ....19....62....B0............8F injection-1a90ce.trace ....19....62....B0............8F injection-1a8efd.trace r 2 p 4 y 1 s N 0 w S e c u r 3 ``` Finally, we can verify that **r2p4y1sN0wSecur3** is the right key by trying to decrypt ``9497cdf1df2600e7f63778d0ae91dcbb``[^4]: ```python from Crypto.Cipher import AES WB_KEY = b"r2p4y1sN0wSecur3" cipher = AES.new(WB_KEY, AES.MODE_ECB) output = cipher.decrypt(bytes.fromhex("9497cdf1df2600e7f63778d0ae91dcbb")) print(output.decode()) ``` ```console $ python ./aes_test.py 0000123400004567 ``` ## Side note about the ``.data`` section Most of the obfuscators encode strings so that we don't have any clue about functions' logic. The obfuscator used in the challenge follows this rule and running the ``strings`` utility on the library does not reveal any interesting information. Nevertheless, we can find a lot of ``.datadiv_decode`` in the ELF constructors of the library. As explained in the previous part, they are generated by the obfuscator and aimed to decode the strings. Since these functions are in the **ELF constructors**, this means that they are executed as soon as the library is loaded. In particular, when calling ``dlopen(...)`` these constructors are executed. It can be confirmed by dumping the ``.data`` section right after ``dlopen()``: ```cpp dlopen("libnative-lib.so", RTLD_NOW); std::ofstream ofs{fmt::format("/data/local/tmp/{}", output)}; auto start = reinterpret_cast(base_address + 0x127000); ofs.write(start, /* sizeof(.data) */ 0x8d49f); ``` Then, we can compare the bytes distribution with [binvis.io](https://binvis.io/): ![Bytes distribution in the .data section](data_strings.png) At the end of the in-memory ``.data`` section, we can found interesting strings used to detect Frida and the device's root state. ## Conclusion Thanks again to Eduardo Novella ([@enovella_](https://twitter.com/enovella_)) and Gautam Arvind ([@darvincisec](https://twitter.com/darvincisec)) for this second part of the challenge :) Also thanks to [Quarkslab](https://www.quarkslab.com) that allowed this publication. One can find related blog posts about whitebox attacks on the Quarkslab's blog: - [Introduction to Whiteboxes and Collision-Based Attacks With QBDI ](https://blog.quarkslab.com/introduction-to-whiteboxes-and-collision-based-attacks-with-qbdi.html) by Paul Hernault ([@0xAcid](https://twitter.com/0xAcid)) - [When SideChannelMarvels meet LIEF ](https://blog.quarkslab.com/when-sidechannelmarvels-meet-lief.html) - [Differential Fault Analysis on White-box AES Implementations](https://blog.quarkslab.com/differential-fault-analysis-on-white-box-aes-implementations.html) by Philippe Teuwen ([@doegox](https://twitter.com/doegox)). *I used this blog post as a reference to resolve this part of the challenge.* ### References [^1]: https://developer.android.com/reference/android/content/Context [^2]: https://calebfenton.github.io/2017/04/05/creating_java_vm_from_android_native_code/ [^3]: QBDI will execute the function using the [ExecBroker](https://qbdi.readthedocs.io/en/stable/api_cpp.html#execution-filtering) mechanism. [^4]: It is the output of the function when entering ``1234`` in the PIN field and ``4567`` in the amount field. --- # r2-pay: anti-debug, anti-root & anti-frida (part 1) - Canonical: https://www.romainthomas.fr/post/20-09-r2con-obfuscated-whitebox-part1/ - Markdown: https://www.romainthomas.fr/post/20-09-r2con-obfuscated-whitebox-part1/index.md - Section: post - Published: 2020-09-20 - Modified: 2026-08-04 - Tags: android, reverse engineering, write-up, obfuscation, whitebox > This first blog post describes the protections in the challenge r2-pay. ## Introduction This series of blog posts explains one way to resolve the r2-pay challenge released during the [r2con2020](https://rada.re/con/2020/) conference. This first part is about the anti-analysis tricks used to hinder reverse-engineering while the second part will be more focused on breaking the whitebox. The resolution took me more than a week-end but it covers nice topics that worth it: **obfuscation & whitebox**. It was also the opportunity to practice attacks against whiteboxes and to test [SideChannelMarvels/JeanGrey](https://github.com/SideChannelMarvels/JeanGrey) developed by Philippe Teuwen (aka. [@doegox](https://twitter.com/doegox)). The challenge has been resolved with the AArch64 version on a device running on Android 9 and rooted with Magisk. > **Note** Here are the files used in this write-up: [re.pwnme.1.0.apk - af019d3016720592aade7bde9890110c](re.pwnme.1.0.apk) [libnative-lib.so (arm64-v8a version)](libnative-lib.so) ## Overview When opening the application on a non-tempered device (or with Magisk hide enabled), we are asked to enter a PIN and an amount that is used to generate a *token*. To resolve the challenge, we have to find the *master key* that is used to generate the token. Few days before the CTF I was told that one of the challenges would involve an obfuscated whitebox ... The main interface of the APK is located in the Java class ``re.pwnme.MainActivity`` which forwards the user inputs (PIN & amount) to a JNI function named ``gXftm3iswpkVgBNDUp``. This function takes the concatenated input $PIN\ ||\ Amount$ and returns the token as a byte array. The **static constructor** of the class loads the "native-lib" library which is available for the architectures: ``arm64-v8a``, ``armeabi-v7a``, and ``x86_64``. Unsurprisingly, this library is obfuscated and some symbols suggest that it has been compiled with a fork of O-LLVM[^1]. ![re.pwnme.MainActivity in r2pay](mainactivity_tag.png) In addition, the library does not export the expected symbol ``Java_re_pwnme_MainActivity_gXftm3iswpkVgBNDUp`` but prefers to use the ``JNI_OnLoad`` technique[^2]. ``JNI_OnLoad()`` is also obfuscated along with control-flow-flattening. The main task of the challenge is to understand the logic of the ``gXftm3iswpkVgBNDUp`` function to figure out how the *token* is generated. ## Anti-Root & Anti-Frida Along with the ``libnative-lib.so`` library, the applications embeds another library ``libtool-checker.so`` whose name sounds quite familiar: it comes from the open-source project [RootBeer](https://github.com/scottyab/rootbeer) which is used to detect if the device is rooted. Some of the root-checks are done in the MainActivity class and if the device is rooted the application raises an exception by dividing a number with 0. On this point, we can disable the check by using [Frida](https://frida.re/) on the RootBeer's functions involved in the detection: ```javascript // frida -U -l ./bypass-root.js --no-pause -f re.pwnme Java.perform(function () { var RootCheck = Java.use('\u266b.\u1d64'); RootCheck['₤'].implementation = function () { console.log("Skip root"); return false; } RootCheck['θ'].overload().implementation = function () { console.log("Skip root"); return false; } }) ``` Nevertheless, the application still crashes as soon as it starts and generates the following backtrace: ```text F libc : Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xfa929095 in tid 8875 (re.pwnme), pid 8849 (re.pwnme) F DEBUG : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** F DEBUG : Build fingerprint: 'google/taimen/taimen:9/PQ3A.190801.002/5670241:user/release-keys' F DEBUG : Revision: 'rev_10' F DEBUG : ABI: 'arm64' F DEBUG : pid: 8849, tid: 8875, name: re.pwnme >>> com.google.android.gms <<< F DEBUG : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xfa929095 F DEBUG : x0 0000007f041f6610 x1 0000007f2565c800 x2 0000007f25600000 x3 000000000000001d F DEBUG : x4 000000000000005c x5 0000000000000001 x6 0000000000000001 x7 0000000000000000 F DEBUG : x8 0000007f041f6610 x9 0000007f041f6600 x10 00000000fa929095 x11 00000000000035b2 F DEBUG : x12 00000000e34d79ac x13 00000000fffffff7 x14 00000000a139577d x15 0000000000000001 F DEBUG : x16 0000007fa66af220 x17 0000007fa65e3608 x18 0000000000000000 x19 0000007f041f6680 F DEBUG : x20 0000000000000000 x21 0000000000000000 x22 0000229100002291 x23 0000000000000000 F DEBUG : x24 0000007f041ff570 x25 0000007f04102000 x26 0000007fab1ad5e0 x27 0000007f0421a690 F DEBUG : x28 0000007f04209080 x29 0000007f041ff490 F DEBUG : sp 0000007f041f65f0 lr 0000007f0423de04 pc 0000007f0423f980 F DEBUG : F DEBUG : backtrace: F DEBUG : #00 pc 000000000003f980 /data/app/re.pwnme-7O3ynhSmMsg2_E5_uqbQxQ==/lib/arm64/libnative-lib.so ``` The backtrace suggests that other checks are performed in the native library. By looking at the ELF's constructors, we can notice two functions that differ from those generated by the obfuscator: ![ELF constructors involved in the detection](elf_ctor.png) > **Note** ``.datadiv_decode13003153710004289592`` functions are in the ELF constructors since they decode global strings that need to be available as soon as the library is loaded. By tracing these functions with [QBDI](https://qbdi.quarkslab.com/), we quickly understand that sub_9080 iterates over ``/proc/self/maps`` with the syscalls openat/read that are located at the addresses 0x009870 and 0x00b448. Then, we observe the following sequence: ```text 0x011fb0: syscall: openat(0xffffffffffffff9c, '/system/lib64/libc.so') 0x012884: syscall: read(51, 0x7ffc006c58, 64): 'ELF@)@8@' 0x013170: syscall: lseek(51, 0x112918, 0) 0x0145f8: syscall: read(51, 0x7ffc006c18, 64) 0x0145f8: syscall: read(51, 0x7ffc006c18, 64) 0x0145f8: syscall: read(51, 0x7ffc006c18, 64): '/ ' 0x0145f8: syscall: read(51, 0x7ffc006c18, 64): 'B88' 0x0145f8: syscall: read(51, 0x7ffc006c18, 64): 'J>' 0x0145f8: syscall: read(51, 0x7ffc006c18, 64): 'RoP)' 0x0145f8: syscall: read(51, 0x7ffc006c18, 64): '\o((' 0x0145f8: syscall: read(51, 0x7ffc006c18, 64): 'io' 0x0145f8: syscall: read(51, 0x7ffc006c18, 64): 'xo0' 0x0145f8: syscall: read(51, 0x7ffc006c18, 64) 0x0145f8: syscall: read(51, 0x7ffc006c18, 64): 'Bxx`-' 0x0145f8: syscall: read(51, 0x7ffc006c18, 64): 'PP`' 0x0151f4: malloc(0x18): 0x7f0c21f4c0 0x0156e4: syscall: lseek(51, 0x1a650, 0) 0x015a68: malloc(0x1e60): 0x7f0acb2000 0x015fa0: syscall: read(51, 0x7f0acb2000, 0x1e60): '{n@b r@ v@ z@ ~@ @ @" @B @b @ @ @ @ @ @" @B @b @ @ @ @ @ @" @B @b @ @ @ @ @ @" @B @b @ @ @ @ A A" AB Ab A A A A "A &A" *AB .Ab 2A 6A :A >A BA FA" JAB NAb RA VA ZA ^A bA fA" jAB nAb rA vA zA ~A A A" AB Ab A A A A A A" AB Ab A A A A A A" AB Ab A A A A A A" AB Ab A A A A B B" BB Bb B B B B "B &B" *BB .Bb 2B 6B :B >B BB FB" JBB NBb RB VB ZB ^B bB fB" jBB nBb rB vB zB ~B B B" BB Bb B B B B B B" B ...' 0x016cfc: free(0x7f0acb2000) -> {n@b r@ v@ z@ ~@ @ @" @B @b @ @ @ @ @ @" @B @b @ @ @ @ @ @" @B @b @ @ @ @ @ @" @B @b @ @ @ @ A A" AB Ab A A A A "A &A" *AB .Ab 2A 6A :A >A BA FA" JAB NAb RA VA ZA ^A bA fA" jAB nAb rA vA zA ~A A A" AB Ab A A A A A A" AB Ab A A A A A A" AB Ab A A A A A A" AB Ab A A A A B B" BB Bb B B B B "B &B" *BB .Bb 2B 6B :B >B BB FB" JBB NBb RB VB ZB ^B bB fB" jBB nBb rB vB zB ~B B B" BB Bb B B B B B B" B ... 0x017118: syscall: close(51) ``` From this output, we can infer the following logic: 1. ``0x011fb0``: the function opens the libc 2. ``0x012884``: it reads the ELF header 3. ``0x013170``: it jumps to the ELF sections table 4. ``0x0145f8``: it looks for the ``.plt`` section 5. ``0x015a68``, ``0x015fa0``: it reads the content of the ``.plt`` section These operations suggest that the function checks if the ``.plt`` of ``/system/lib64/libc.so`` is not tampered with. In particular, if we use Frida on a libc's function this check won't pass. After this check, the function sub_9080 spawns a thread: ```text 0x0195dc: pthread_create(0xf1079f10, 0x0, 0x1a690, 0x0) ``` The libc integrity check makes more sense as it is probably used to protect the library against a hook of ``pthread_create()``. The thread's routine sub_1a690 starts by making two calls to the mathematical function ``tan()``: ```text 0x01b774: tan(0.): 0. 0x01b79c: tan(-7832.0): -0.00951489 0x01cc74: memcpy(0x7ffc006598, libnative-lib.so!0x1267f0, 80) -> !7Nl 0x01ceb8: rand() 0x01f774: tan(0.): 0. 0x01f79c: tan(-7832.0): -0.00951489 ``` My understanding of these calls is that the application tries to protect against tools that would not support floating-point instructions such as ``FCMP`` or ``FMOV``. In addition, I think that if we mock the behavior of ``tan()`` with a constant value it would trigger a crash. ![tan](tan_instruction.png) Then it follows a check of ``TracerPid`` value in ``/proc/self/status``. This value is set when the process is being debugged with `ptrace` (which is the case with gdb). Dynamically, we observe syscalls that open ``/proc/self/status`` and read the content byte-per-byte: ```text 0x020ee0: syscall: openat(0xffffffffffffff9c, '/proc/self/status'): 51 0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'N' 0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'a' 0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'm' 0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'e' 0x0231e8: syscall: read(51, 0x7ffc00656c, 1): ':' 0x0231e8: syscall: read(51, 0x7ffc00656c, 1) 0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'r' 0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'e' 0x0231e8: syscall: read(51, 0x7ffc00656c, 1): '.' 0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'p' 0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'w' 0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'n' 0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'm' 0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'e' 0x0231e8: syscall: read(51, 0x7ffc00656c, 1) 0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'S' 0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 't' 0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'a' 0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 't' 0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'e' 0x0231e8: syscall: read(51, 0x7ffc00656c, 1): ':' ... ``` ## Anti-Frida #1 Still in the thread's routine sub_1a690, the function checks if Frida is running by looking at all the values of ``/proc/self/task//status`` and by checking if one of the names is gmain. It turns out that it's the case when Frida is used in the application :-) ```text 0x0368e4: snprintf('/proc/self/task/9719/status', '/proc/self/task/%s/status'): '/proc/self/task/9719/status' 0x036a1c: syscall: openat(0xffffffffffffff9c, '/proc/self/task/9719/status') 0x03897c: syscall: read(73, 0x7ffc400af4, 1): 'N' 0x03897c: syscall: read(73, 0x7ffc400af4, 1): 'a' 0x03897c: syscall: read(73, 0x7ffc400af4, 1): 'm' 0x03897c: syscall: read(73, 0x7ffc400af4, 1): 'e' 0x03897c: syscall: read(73, 0x7ffc400af4, 1): ':' 0x03897c: syscall: read(73, 0x7ffc400af4, 1) 0x03897c: syscall: read(73, 0x7ffc400af4, 1): 'g' 0x03897c: syscall: read(73, 0x7ffc400af4, 1): 'm' 0x03897c: syscall: read(73, 0x7ffc400af4, 1): 'a' 0x03897c: syscall: read(73, 0x7ffc400af4, 1): 'i' 0x03897c: syscall: read(73, 0x7ffc400af4, 1): 'n' 0x03897c: syscall: read(73, 0x7ffc400af4, 1) 0x03897c: closedir() # Crash! ``` To bypass this check, one can statically patch the syscall or we can dynamically change the behavior of ``snprintf(..., '/proc/self/task/%s/status')`` in order to **always** returns the same status (e.g. ``/proc/self/task/123/status``). Concretely, it could be done by hooking ``snprintf`` and by forcing the *output* string to ``/proc/self/task/123/status``. ## Anti-Frida #2 Still in the sub_1a690 function, the anti-frida checks continue by inspecting the file descriptors of the process. It iterates over ``/proc/self/fd/%s`` and looks at the underlying symlink. Frida server (which runs globally on the device) and Frida agent (which is injected in the process) communicate with named pipes that are associated with a file descriptor. If Frida server is running, we can observe the following values: ```text 0x04308c: lstat('/proc/self/fd/32') 0x043448: syscall: readlinkat(0xffffffffffffff9c, '/proc/self/fd/32', 0x7ffbffdc10, 256): 'anon_inode:[eventfd]' 0x041844: readdir('33') 0x043078: snprintf('/proc/self/fd/33', '/proc/self/fd/%s'): '/proc/self/fd/33' 0x04308c: lstat('/proc/self/fd/33') 0x043448: syscall: readlinkat(0xffffffffffffff9c, '/proc/self/fd/33', 0x7ffbffdc10, 256): 'anon_inode:[eventfd]' 0x041844: readdir('34') 0x043078: snprintf('/proc/self/fd/34', '/proc/self/fd/%s'): '/proc/self/fd/34' 0x04308c: lstat('/proc/self/fd/34') 0x043448: syscall: readlinkat(0xffffffffffffff9c, '/proc/self/fd/34', 0x7ffbffdc10, 256): '/data/local/tmp/re.frida.server/linjector-500' # Crash! ``` In this case, the file descriptor ``34`` is associated with ``/data/local/tmp/re.frida.server/linjector-500`` which triggers the detection and the application crashes. As for ``/proc/self/task//status``, one can disable this check by **statically patching** the syscalls or by **dynamically changing** the result of ``readlinkat()``. For instance, we can use [QBDI](https://qbdi.quarkslab.com/) to instrument syscall instructions and process the result of ``readlinkat()`` in a user callback: ```cpp vm.addMnemonicCB("SVC", POST_INST, [] (VMInstanceRef vm, GPRState* gprState, FPRState*, void* data) { if (gprState->x8 != __NR_readlinkat) { return VMAction::CONTINUE; } std::string buf = reinterpret_cast(gprState->x2); if (buf.find("re.frida.server") != std::string::npos) { static const std::string FAKE_VALUE = "anon_inode:[eventfd]"; // Bypass Frida detection! memcpy( reinterpret_cast(gprState->x2), reinterpret_cast(FAKE_VALUE.c_str()), FAKE_VALUE.size() + 1 ); gprState->x0 = FAKE_VALUE.size() + 1; } return VMAction::CONTINUE; }, ctx); ``` ## Anti-Frida #3 ? I'm not sure if the following calls sequence is used to check the libc's integrity against Frida but at the end of the thread's routine, we can observe these syscalls: ```text 0x048ff0: syscall: openat(0xffffffffffffff9c, '/proc/self/maps'): 51 0x04ae6c: syscall: read(51, 0x7ffc006578, 1): '1' 0x04ae6c: syscall: read(51, 0x7ffc006578, 1): '2' 0x04ae6c: syscall: read(51, 0x7ffc006578, 1): 'c' 0x04ae6c: syscall: read(51, 0x7ffc006578, 1): '0' 0x04ae6c: syscall: read(51, 0x7ffc006578, 1): '0' 0x04ae6c: syscall: read(51, 0x7ffc006578, 1): '0' 0x04ae6c: syscall: read(51, 0x7ffc006578, 1): '0' 0x04ae6c: syscall: read(51, 0x7ffc006578, 1): '0' ... 0x0513f8: sscanf('7fa65c0000-7fa65dc000 r-xp 00000000 08:07 1275/system/lib64/libc.so', '%lx-%lx %s %s %s %s %s') 0x056034: syscall: close(51) ``` The result of ``sscanf()`` could be used to check the page permissions (e.g. ``r``w``xp``) or to the libc's base address (to check if it is consistent). ## Anti-Root In addition to the root-beer detection, the library embeds another root detection located in the **second** ELF constructor. This constructor, sub_77D14, performs the same early checks as the first constructor on the libc's ``.plt`` integrity before spawning another thread routine, sub_98c00. ```text 0x08861c: pthread_create(0xfa780b70, 0x0, 0x98c00, 0x0) ``` ![ELF constructors: anti-frida and anti-root](ctor_pthread.png) By tracing the thread's routine, we notice that it checks if ``su`` files are present on the device through three different calls: 1. One call to ``open()``: 0x099180: open('/system/xbin/su') 2. One syscall to ``openat()``: 0x0992a4: syscall: openat(..., '/data/su') 3. One syscall to ``faccessat()``: 0x0993f0: syscall: faccessat('/sbin/su') ```text 0x099180: open('/data/local/su'): -1 0x0992a4: syscall: openat(0xffffffffffffff9c, '/data/local/su'): -2 0x0993f0: syscall: faccessat('/data/local/su'): -2 0x099180: open('/data/local/bin/su'): -1 0x0992a4: syscall: openat(0xffffffffffffff9c, '/data/local/bin/su'): -2 0x0993f0: syscall: faccessat('/data/local/bin/su'): -2 0x099180: open('/data/local/xbin/su'): -1 0x0992a4: syscall: openat(0xffffffffffffff9c, '/data/local/xbin/su'): -2 0x0993f0: syscall: faccessat('/data/local/xbin/su'): -2 0x099180: open('/sbin/su'): 51 0x0992a4: syscall: openat(0xffffffffffffff9c, '/sbin/su'): 52 0x0993f0: syscall: faccessat('/sbin/su'): 52 Crash! ``` By forcing the results of these functions to ``-1`` or ``-2``, we can disable the checks. Here is the list of the su-files that are used in this detection: * /data/local/su * /data/local/bin/su * /data/local/xbin/su * /sbin/su * /su/bin/su * /system/bin/su * /system/bin/.ext/su * /system/bin/failsafe/su * /system/sd/xbin/su * /system/usr/we-need-root/su * /system/xbin/su * /cache/su * /data/su * /dev/su At the end of the thread's routine, we can also observe the following calls that are probably used to check if the application is running on a real Android system. ```text 0x099e30: syscall: faccessat('/system') 0x099e30: syscall: faccessat('/system/bin') 0x099e30: syscall: faccessat('/system/sbin') 0x099e30: syscall: faccessat('/system/xbin') 0x099e30: syscall: faccessat('/vendor/bin') 0x099e30: syscall: faccessat('/sbin') 0x099e30: syscall: faccessat('/etc') ``` ## Static bypass with LIEF In the previous sections, we described the anti-root, anti-debug and anti-frida checks made in the ELF constructors. The **same** dynamic checks are also performed in the ``gXftm3iswpkVgBNDUp`` function at the following locations: * 0x09f2f8: /proc/self/status * 0x0d4840: /proc/self/fd/ * 0x0dec8c: /proc/self/task/\/status While the checks in ``gXftm3iswpkVgBNDUp`` can be dynamically disabled when instrumenting the function, the checks in the ELF constructors are annoying. One way to disable the checks in the thread's routines is to disable the ``pthread_create(...)``. It can be achieved by patching the ``.plt`` entry associated with the function: ```armasm mov x0, xzr; ret; ``` Thanks to ``llvm-mc``, we can get the raw bytes of these instructions: ```console $ echo "mov x0, xzr;ret;"|llvm-mc -arch=aarch64 -show-encoding .text mov x0, xzr // encoding: [0xe0,0x03,0x1f,0xaa] ret // encoding: [0xc0,0x03,0x5f,0xd6] ``` Finally, we can patch the ``.plt`` with [LIEF](https://lief.quarkslab.com): ```python import lief lib = lief.parse("./libnative-lib.so") lib.patch_address(0x5870, [0xe0,0x03,0x1f,0xaa]) lib.patch_address(0x5874, [0xc0,0x03,0x5f,0xd6]) lib.write("./libnative-lib-patched.so") ``` ![pthread_create patches](patching.png) Using these patches and the Frida script exposed in the first section, we are able to **load** the application but the other detections are triggered in ``gXftm3iswpkVgBNDUp``. Nevertheless, with the Frida's stalker or QBDI we can trace the instructions and disable the other checks. If one wants to completely bypass all the protections statically, here are the patches: ```python import lief lib = lief.parse("./libnative-lib.so") # Keys are str objects for a better understanding :) INST = { "mov x0, #0": [0xe0, 0x03, 0x1f, 0xaa], "ret": [0xc0, 0x03, 0x5f, 0xd6], "nop": [0x1f, 0x20, 0x03, 0xd5], } PATCHES = [ # Patch the .plt entry of pthread_create (0x5870, INST["mov x0, #0"]), (0x5874, INST["ret"]), # Disable anti-frida checks (0x0d718c, INST["mov x0, #0"]), # /proc/self/fd : patch the result of readlinkat syscall (0x0e1940, INST["mov x0, #0"]), # /proc/self/task//status: patch the result of read syscall # Disable .text integrity checks (0xB64D0, INST["nop"]), ] for patch in PATCHES: lib.patch_address(*patch) lib.write("libnative-lib.so") ``` When writing this write-up, I realized that patching the syscalls involved in the anti-frida (/proc/self/fd/ and /proc/self/task/\/status) makes the application crash. It turns out that the library seems to implement **code integrity on the ``.text`` section** that I didn't notice when running the function through QBDI. Nevertheless, by tracing the basic block[^3] we can identify the basic block involved in the integrity check and patch it. ![Code integrity patches](code_integrity.png) > **Note** The scripts and the patched library are available [here ](https://github.com/romainthomas/r2pay). Regarding ``JNI_OnLoad()``, a trace generated with [QBDI's ExecBroker](https://github.com/QBDI/examples/blob/d589d28b237f46d16cab3b11aa36bbb51102e307/packer-android-x86/src/libshellx_qbdi.cpp#L18-L85) leads to following result: ```cpp JNI_OnLoad() { 0x09af3c: GetEnv(0x7fcb507460, 0x10006) 0x09b0ac: FindClass("re/pwnme/MainActivity"): 537 0x09b1b4: RegisterNatives() gXftm3iswpkVgBNDUp ([BB)[B -> "libnative-lib.so@0x9b41c" } ``` Then, we can extract the function's offset: gXftm3iswpkVgBNDUp: 0x9b41c. ## Summary & Conclusion Whilst Frida detections are usually based on sockets and library names in ``/proc/self/maps``, this challenge introduces two detections based on named pipes:``/proc/self/fd`` and thread status: ``/proc/self/task//status`` which are pretty cool :-) These checks are performed in two locations: 1. The ELF constructors 2. The function ``gXftm3iswpkVgBNDUp()`` The implementation in the ELF constructors might be tricky to analyze since the functions are called before **any** other classical functions (which includes ``JNI_OnLoad()``). Nevertheless, thanks to the interface of the ELF loader, it exposes the function ``call_array(...)``[^4] which is handy to process the ELF constructors. > **Note** This function is mangled as ``__dl__ZL10call_arrayIPFviPPcS1_EEvPKcPT_mbS5_`` in ``/system/bin/linker64`` ![Overview of the anti-root and anti-frida](protection_overview.png) Since QBDI is not detected in this challenge, it's a good opportunity to give it a try: ## Acknowledgments Thanks to Eduardo Novella ([@enovella_](https://twitter.com/enovella_)) and Gautam Arvind ([@darvincisec](https://twitter.com/darvincisec)) for this interesting and realistic challenge they created! Also thanks to [Quarkslab](https://www.quarkslab.com) that allowed this publication. For those who are interested in similar topics, you can take a look at the Quarkslab's [blog](https://blog.quarkslab.com/). ### References [^1]: [GoSSIP-SJTU/Armariris](https://github.com/GoSSIP-SJTU/Armariris) - [``StringObfuscation.cpp#L140``](https://github.com/GoSSIP-SJTU/Armariris/blob/0cba41329244a29c7cb94e25458191b68967b6e8/lib/Transforms/Obfuscation/StringObfuscation.cpp#L140) [^2]: https://developer.android.com/training/articles/perf-jni#native-libraries [^3]: [``addVMEventCB(VMEvent::BASIC_BLOCK_ENTRY, ...);``](https://github.com/QBDI/QBDI/blob/a20653f07df3ae78250e7ecf28ed699b2d727027/include/QBDI/VM.h#L296-L305) [^4]: [``linker/linker_soinfo.cpp:420``](https://android.googlesource.com/platform/bionic/+/refs/tags/android-9.0.0_r60/linker/linker_soinfo.cpp#420) --- # A Glimpse Into Tencent's Legu Packer - Canonical: https://www.romainthomas.fr/post/a-glimpse-into-tencents-legu-packer/ - Markdown: https://www.romainthomas.fr/post/a-glimpse-into-tencents-legu-packer/index.md - Section: post - Published: 2019-11-26 - Modified: 2026-08-04 - Tags: android, reverse engineering, packer > Analysis of Tencent Legu: a packer for Android applications. > **Note** This post has been originally posted on the Quarkslab's Blog ## Introduction This blog post deals with the Legu packer, an Android protector developed by Tencent that is currently one of the state-of-the-art solutions to protect APK DEX files. The packer is updated frequently and this blog post focuses on versions ``4.1.0.15`` and ``4.1.0.18``. ## Overview An application protected with Legu is composed of two native libraries: libshell-super.2019.so and ``libshella-4.1.0.XY.so`` as well as raw binary files embedded in the resources of the APK: - tosversion - 0OO00l111l1l - ``0OO00oo01l1l`` - ``o0oooOO0ooOo.dat`` The main logic of the packer is located in the native library libshell-super.2019.so which basically unpacks and loads the protected DEX files from the resources. Some functions of the library are obfuscated but thanks to Frida/QBDI their analysis is not a big deal. ## Internals Basically, the original DEX files are located in the assets/0OO00l111l1l file along with the information required to unpack them. The following figure lays out the structure of this file. ![Legu packed-file layout with two DEX files, hashmaps, and Dalvik bytecode blocks.](packed_file.png) In the assets/0OO00l111l1l file, the first part contains the original DEX files with the same number of ``classes.dex`` according to the multi-DEX feature of the original APK. These DEX files are not exactly the original ones, as their Dalvik bytecode have been *NOP-ed* by Legu. Therefore, a dump of these files only gives information about the classes' names, not the code logic: ![Disassembled AppLockApplication class with method bodies replaced by nop instructions.](noped.png) Then follows what we called a *hashmap* that is used to link a class name (e.g. Lcom/tencent/mmkv/MMKV;) to an offset in the data block located in the third part of the file. This data block contains the original Dalvik bytecode of the methods. Actually, the first part that contains the altered DEX files, is compressed with **NRV**[^1]. The second part, the hashmap, is also compressed with NRV but the packer adds a layer of encryption through a slightly modified version of **XTEA**[^2]. Finally, the last part is compressed and encrypted with the same algorithms as the previous one. Regarding the *hashmap*, it uses a custom structure that has been reversed and lead to a Kaitai structure available here: [legu_packed_file.ksy](https://github.com/quarkslab/legu_unpacker_2019/blob/master/legu_packed_file.ksy), [legu_hashmap.ksy](https://github.com/quarkslab/legu_unpacker_2019/blob/master/legu_hashmap.ksy) Its overall layout is exposed in the next figure: ![Legu hashmap lookup from a class-name hash to method metadata, packed bytecode, and the NOP-filled DEX.](hashmap.png) ## Unpacking process Let's say that the application needs to use the packed Java class Lcom/tencent/mmkv/MMKV;. First, the packer's runtime transforms the class name into an integer with the ``dvmComputeUtf8Hash()`` hash function[^3]. This integer is then used as an index into the *hashmap* whose value is a structure that contains information about the class in the packed data (blue area in the figure). The first attribute of this structure, ``utf8_hash``, is a copy of the hash value which is used to check that it is the right key/value association. The ``class_info`` structure (blue block in the figure) next contains the packed method information (yellow area in the figure) whose size is the same as the original number of methods in the class. This structure makes the relationship between the NOP-ed bytecode offset in the altered DEX files and the offset in the original bytecode (red block). Finally, the packer copies the original bytecode into the altered DEX files. To summarize, the first part contains the original DEX files with the Dalvik bytecode removed (*NOP-ed*). The last part contains the missing Dalvik bytecode and the second part makes the bridge between the altered DEX files and the Dalvik bytecode. # Compression & Encryption To decrypt the hashmap and the Dalvik bytecode, the packer uses the first 16 bytes of assets/tosversion xored with a hard-coded key: ``^hHc7Ql]N9Z4:+1m~nTcA&3a7|?GB1z@``. ```python LIB_KEY = b"^hHc7Ql]N9Z4:+1m~nTcA&3a7|?GB1z@" def key_derivation(key: bytes) -> bytes: return bytes(x1 ^ x2 for x1, x2 in zip(LIB_KEY, cycle(key))) ``` Then, it uses a slightly modified version of XTEA that is given in the next listing: ```cpp int xtea_decrypt(uint32_t* key, uint32_t* buf, size_t ilen, size_t nb_round) { const size_t count = ilen / 8; const size_t key_off = (ilen & 8) / 4; static constexpr uint32_t DELTA = 0x9e3779b9; const uint32_t key_0 = key[key_off + 0]; const uint32_t key_1 = key[key_off + 1]; for (size_t i = 0; i < count * 2; i += 2) { buf[i + 0] ^= key_0; buf[i + 1] ^= key_1; uint32_t sum = DELTA * nb_round; uint32_t temp0 = buf[i + 0]; uint32_t temp1 = buf[i + 1]; for (size_t j = 0; j < nb_round; ++j) { temp1 -= (key[2] + (temp0 << 4)) ^ (key[3] + (temp0 >> 5)) ^ (temp0 + sum); temp0 -= (key[0] + (temp1 << 4)) ^ (key[1] + (temp1 >> 5)) ^ (temp1 + sum); sum -= DELTA; } buf[i + 0] = temp0; buf[i + 1] = temp1; } return 0; } ``` After the decryption routine, the packer decompresses the data with ``NRV``, the same algorithm used to compress the altered DEX files: ```python key = key_derivation(open("assets/tosversion", "rb").read()[:16]) for i in range(nb_dex_files): hashmap[i] = nrv_decompress(xtea_decrypt(blob1, key)) dalvik_bytecodes[i] = nrv_decompress(xtea_decrypt(blob2, key)) ``` ## Unpacking Putting all the pieces together, we can **statically** unpack protected APKs and recover the original bytecode: ![Recovered AppLockApplication disassembly with its original Dalvik instructions restored.](unpacked.png) Hence, as we can automatically unpack such APKs, the unpacking process could be integrated into an automatic analysis pipeline. The script and the Kaitai structures are available in the Quarkslab repository: [legu_unpacker_2019](https://github.com/quarkslab/legu_unpacker_2019), along with a suspicious application [^4], [packed](https://github.com/quarkslab/legu_unpacker_2019/blob/master/samples/com.intotherain.voicechange.apk) and [unpacked](https://github.com/quarkslab/legu_unpacker_2019/blob/master/samples/com.intotherain.voicechange_unpacked.apk). ## Acknowledgments Thanks to my colleagues who proofread this article. ### References [^1]: http://www.oberhumer.com/opensource/ucl/ [^2]: https://en.wikipedia.org/wiki/XTEA [^3]: http://androidxref.com/4.4.4_r1/xref/dalvik/vm/UtfString.cpp#88 [^4]: https://www.virustotal.com/gui/file/708e6967920dcf2789b7183d714e73ab79a2f8b3ca71929b12aadeb2c58c2867/detection --- # Android Native Library Analysis with QBDI - Canonical: https://www.romainthomas.fr/post/android-native-library-analysis-with-qbdi/ - Markdown: https://www.romainthomas.fr/post/android-native-library-analysis-with-qbdi/index.md - Section: post - Published: 2019-06-03 - Modified: 2026-08-04 - Tags: android, qbdi > This blog post deals with QBDI and how it can be used to reverse an Android JNI library > **Note** This post has been originally posted on the Quarkslab's Blog ## Introduction During the past few months we improved the ARM support in QBDI. More precisely, we enhanced the QBDI's engine to support Thumb and Thumb2 instructions as well as Neon registers. Development is still in progress and we need to clean the code and add non-regression tests compared to the x86-64 support. To add Thumb and Thumb2 support, we tested the DBI against well-known obfuscators such as [Epona](https://epona.quarkslab.com), [O-LLVM](https://github.com/obfuscator-llvm/obfuscator) or [Arxan](https://www.arxan.com/), as we could expect good instruction coverage, corner cases and nice use cases. The native code came from Android JNI libraries embedded in different APKs. This blog post introduces some QBDI features that could be useful to assess native code and speedup reverse engineering. To expose these features, we analyzed an Android SDK that aims to protect applications against API misuse. ## Dynamic Instrumentation on Android [Frida](https://www.frida.re/) is one of the Android day-to-day dynamic instrumentation framework widely used to instrument applications. It can address both native code with inline hooking and *Java* side thanks to ART instrumentation[^1]. Frida works at the function level and in some cases we may need to have a finer granularity at the basic-block level or at the instruction level (i.e. have *hooks* on instructions) To address this limitation, one trick commonly used is to combine hooking with emulation. One can use Frida to hook the function that we are interested in, then we can dump the CPU context and the memory state of the process and eventually continue the execution through an emulator like [Miasm](https://miasm.re/blog/) or [Unicorn](https://www.unicorn-engine.org/) This approach works pretty well but has a few limitations: * **Speed**: For large sets of functions. * **External calls**: One needs to mock external calls behavior (e.g. ``strlen``, ``malloc``, ...). * **Some behaviors can be difficult to emulate**: Thread, Android internal frameworks, ... Moreover, while it is quite simple to mock the behavior of ``strlen``, it may be more challenging to mock JNI functions behavior like ``FindClass()``, ``GetMethodID()``, ``RegisterNatives()``, ... The design of QBDI provides a good trade-off between full instrumentation and partial emulation thanks to the ``ExecBrocker`` that enables to switch between instrumented code (our function) and non-instrumented code: ``strlen()``, ``FindClass()``, ``pthread_call_once()``, ... This diagram represents the instrumentation flow for the different scenarios: ![QBDI instrumentation flow switching between instrumented execution and real or mocked external calls.](qbdi_flow.png) For those who are interested in QBDI internals you can look at the 34C3 talk by Charles and Cédric[^2]. There are also examples in the GitHub repository[^3]. To summarize, we can bootstrap QBDI as follows: ```cpp // QBDI main interface QBDI::VM vm; // QBDI CPU state for GPR registers GPRState* state = vm.getGPRState(); // Setup virtual stack uint8_t *fakestack = nullptr; QBDI::allocateVirtualStack(state, /* size */0x100000, &fakestack); // { // Setup instrumentation ranges, callbacks etc, ... // } // Start Instrumentation: uintptr_t retval; bool ok = vm.call(&retval, /* Address of the function to instrument */); // Instrumentation Finished ``` ## SDK Overview Among the QBDI tests, we analyzed an SDK that aims to protect applications against API abuses. This kind of protection is used to protect API endpoints against illegitimate uses: emulator, bots, ... To protect the main application, the solution collects information about the device state: rooted, debugged, custom, then encodes this information with a *proprietary* algorithm and sends the encoded data to a server. The **server** decodes the information sent by the device collector, performs analyzes to check the device integrity and sends back a token that handles the information about whether the device is corrupted or not. The following figure summarizes this process: ![Mobile SDK sending collected device data to a server, which evaluates integrity and returns a token.](overview.png) Such architecture is robust and similar to the one in [SafetyNet](https://developer.android.com/training/safetynet/attestation)[^4]. On the other hand, the SDK has fewer permissions than SafetyNet, therefore it cannot collect as much data about the device as SafetyNet does. We started the analysis by monitoring the network traffic between the SDK and its server. At some point, we can observe the following request: ![Captured HTTP POST request containing JSON metadata and an encoded device-state payload.](network.png) It is JSON encoded and the characters that look like random values are the encoded information sent by the device collector. The analysis of the SDK aims to address these questions: * How the SDK checks if the device is rooted or not? * How the SDK detects if the application is being debugged? * What kind of information is collected from the device and how it is encoded? After a look at the Java layer, we found that the logic of the solution is implemented in a JNI library that will be named ``libApp.so`` [^5]. The library exposes the following JNI functions: ![JNI exports](exports.png) With static analysis, we can identify that the function ``Java_XXX_JNIWrapper_ca3_14008()`` is the one involved in the generation of the sequence ``"QJRR{JJJGQJ~|MJJJ..."``. It returns the encoded data as a ``java.lang.String`` and takes two parameters that are not mandatory: ``bArr``, ``iArr`` [^6]. ![JNIWrapper native-method declarations with ca3_14008 highlighted as the encoded-data generator.](java.png) The library as a whole is not especially obfuscated. Nonetheless, we find strings encoding and syscall replacement on well-known ``libc`` functions: * ``read`` * ``openat`` * ``close`` * ... This technique is commonly used to avoid hooking but the fact is that the given syscalls are wrapped in functions that are not inlined. Hence, one can hook the functions that wrap the associated syscall. ## Get Started with QBDI In order to fully understand the logic of this function, we instrumented the function through QBDI [^7] associated with a set of instrumentation callbacks. These callbacks aim to provide different kinds of information that will be useful to the analyst to understand the function logic. For instance, we can setup a first callback that records all the syscall instructions, we can also add a callback that records memory access. The purpose of this blog post is to show how a small number of well-chosen callbacks can help analysts understand the logic of the function. First of all, the native library embedded in the SDK can be loaded outside of the original APK using ``dlopen()`` / ``dlsym()``. Moreover, one can instantiate a JVM thanks to the ART runtime (``libart.so``): ```cpp int main(int argc, char** argv) { static constexpr const char* TARGET_LIB = "libApp.so"; void* hdl = dlopen(TARGET_LIB, RTLD_NOW); using jni_func_t = jstring(*)(JNIEnv* /* Other parameters are not required */); auto jni_func = reinterpret_cast(dlsym(hdl, "Java_XXX_JNIWrapper_ca3_14008")); JavaVM* jvm, JNIEnv* env; ART_Kitchen(jvm, env); // Instantiate the JVM and initialize the jvm and env pointers } ``` At this point, the ``jni_func()`` function is tied to ``Java_XXX_JNIWrapper_ca3_14008`` and ready to be executed in ``main()``: ```cpp jstring output = jni_func(env); const char* cstring = env->GetStringUTFChars(output, nullptr); console->info("Real Output: {}", cstring); ``` ![Terminal output from direct JNI execution showing root: 1 and the encoded collector string.](real_output.png) The output seems consistent with the network capture and the value ``"root: 1"`` too since we are on a rooted device [^8] Now, let's run the function through QBDI: ```cpp console->info("Initializing VM ..."); QBDI::VM vm; GPRState* state = vm.getGPRState(); uint8_t *fakestack = nullptr; QBDI::allocateVirtualStack(state, 0x100000, &fakestack); console->info("Instument module: {}", TARGET_LIB); vm.addInstrumentedModule(TARGET_LIB); console->info("Simulate call in QBDI"); jstring dbioutput; bool ok = vm.call(&dbioutput, reinterpret_cast(jni_func), {reinterpret_cast(env)}); if (ok and dbioutput != nullptr) { console->info("DBI output {:x}", env->GetStringUTFChars(dbioutput, nullptr)); } ``` This code provides the following output: ![QBDI log showing full instrumentation of libApp.so with output matching direct execution.](dbi_output.png) Everything looks good, QBDI managed to **fully** instrument the function (which includes ARM / Thumb switch) and the result is similar to the real execution. Analysis ======== Now that we are able to run and instrument the function, we can start to add instrumentation callbacks to analyze its behavior. One of the first callbacks that is useful to setup is a callback that instruments syscall instructions (i.e. ``svc #0``). To do so, we can use the ``vm.addSyscallCB(position, callback, data)``. * **Position** - It stands for the position of the callback: Before or after the syscall. * **callback** - The callback itself. * **Data** - Pointer to user data (e.g. user context that register dynamic information) It leads to the following piece of code: ```cpp auto syscall_enter_cbk = [] (VMInstanceRef vm, GPRState *gprState, FPRState *fprState, void *data) { const InstAnalysis* analysis = vm->getInstAnalysis(ANALYSIS_INSTRUCTION | ANALYSIS_DISASSEMBLY); rword syscall_number = gprState->r7; /* * std::string sys_str = lookup[syscall_number]; // Lookup table that convert syscall number to function */ console->info("0x{:06x} {} ({})", addr, analysis->disassembly, sys_str); return VMAction::CONTINUE; } vm.addSyscallCB(PREINST, syscall_enter_cbk, /* data */ nullptr); ``` Before any syscall instructions, we perform a basic lookup on the syscall number stored in the **R7** register to resolve its name. It results in the following output: ![QBDI syscall trace resolving ARM SVC instructions to syscall names.](syscall-1.png) Since we are able to resolve syscall numbers into function names, we can improve the logic of callback to dispatch and print function parameters: ```cpp auto syscall_enter_cbk = [] (...) { ... /* * Lookup table (syscall number, function pointer) * { * 322 -> on_openat * } */ auto function_wrapper = func_lookup[syscall_number]; return function_wrapper(...) } // Wrapper for openat syscall VMAction on_openat(VMInstanceRef vm, GPRState *gprState, ...) { auto path = reinterpret_cast(gprState->r1); console->info("openat({})", path); return VMAction::CONTINUE; } ``` By doing so on the common syscalls number, we get this new trace: ![Annotated syscall trace separating debugger checks, device-state collection, and data sent to the server.](syscall-2.png) Based on this output, we can figure out how root check (orange area) is done. It is performed by checking the existence of the following binaries: - /system/bin/su - /system/xbin/su - /sbin/su - ... The function also checks if some directories are present on the device (``faccessat`` syscall): - /data - /tmp - /system - ... Especially, it would be suspicious if the directory ``/tmp`` were present on the *device* while it is standard to have ``/system`` and ``/data`` directories. Regarding the debug state of the process (blue area), it is done by looking at ``/proc/self/status``. After analysis, the function checks the ``TracerPID`` attribute (cf [More Android Anti-Debugging Fun - B. Mueller](https://www.vantagepoint.sg/blog/89-more-android-anti-debugging-fun)) Finally, the function processes the output of ``/proc/self/maps`` right before to returning the encoded values. It suggests that the data collected by the solution are based on this resource. ### Encoding Routine In the previous part we got a global overview about how the solution achieves root detection, debug detection and what kind of data is collected (i.e. process memory map). However, some questions are pending: - What part of the process memory map is used: Base addresses? Module paths? Permissions? - How the data are encoded (i.e. how ``QJRR{JJJGQJ~|MJJJ...`` is generated) ? Along with the QBDI ARM support, we also added ARM support to resolve **memory addresses** during the instrumentation. It means that QBDI is now able to resolve **the effective memory address** of instructions such as: ```armasm LDR R0, [R1, R2]; # Resolve R1 + R2 STR R1, [R2, R3, LSL #2]; # Resolve R2 + R3 * 4 LDRB [PC, #4]; # Resolve **real** PC + 4 ``` Moreover, QBDI is also able to get **the effective memory value** that is read or written. This feature is quite useful in the case of conditional instructions such as: ```armasm ITT LS; LDRLS R0, [R4]; LDRLS R1, [R0, #4] ``` The **effective** value of ``R0`` and ``R1`` is stored in QBDI. It may not be ``*(r4)`` and ``*(r0 + 4)`` since the ``LS`` condition may not be verified. To add a callback on memory accesses, we can use the ``addMemAccessCB(...)`` function on the VM instance: ```cpp vm.addMemAccessCB(MEMORY_READ_WRITE, memory_callback, /* data */ nullptr); ``` In the given ``memory_callback(...)`` function, we perform the following actions: * Track memory **byte** accesses. * Check if the value is printable. * Pretty print the R/W value. The idea of this callback is to track memory accesses that are performed on printable characters. It enables to quickly identify strings encoding/decoding routines. Here is the implementation of the callback: ```cpp VMAction memory_callback(VMInstanceRef vm, GPRState *gprState, ...) { auto&& acc = vm->getInstMemoryAccess(); // Get last memory access MemoryAccess maccess = acc.back(); // Retrieve access information: rword addr = maccess.accessAddress; // Address accessed rword value = maccess.value; // Value read or written rword size = maccess.size; // Access size // Only look for byte access if (size != sizeof(char)) { return VMAction::CONTINUE; } // Read / Write operation as a string const std::string kind = maccess.type == MemoryAccessType::MEMORY_READ ? "[R]" : "[W]"; // Cast the value into a char const char cvalue = static_cast(value); // Check if the value read or written is printable if (::isprint(cvalue)) { logger->info("0x{:x} {}: {}", addr, kind, cvalue); // Pretty print } // Continue this execution return VMAction::CONTINUE; } ``` With this new callback, we can observe such output between two ``openat()`` syscalls involved in the ``root`` check routine: ![Memory-access trace reconstructing /system/bin/su during the root-check string-decoding routine.](su_decode.png) It is basically the string decoding routine in action. Note that some read operations are missing since we only track **printable** characters. However, all write operations are present. The routine **loads** characters with the instruction at address **0x295e** and **stores** the decoded value at address **0x2972**. If we look at the function that handles these two addresses, we find the decoding routine: ![Control-flow graph of the string decoder, highlighting loads in green, writes in red, and decode logic in blue.](decoding_routine.png) In the above figure, the **green** section highlights the memory **load access** while the **red** one highlights the **write operation**. The **blue** area is the **decoding logic**. The output of **all** read / write accesses turns out to be quite verbose on the whole execution of the function. We can improve the instrumentation by adding two callbacks before and after function **calls** with this purpose: 1. Before calls, we print the target address (e.g. ``0x123: blx r3 -> .text!0xABC``). 2. After calls we print **all** printable characters being read or written **within the called function**. The ``addCallCB(...)`` is still in experimentation but it aims to put callbacks before or after call instructions: ```cpp // Callback before ``call`` instructions vm.addCallCB(PRECALL, on_call_enter, nullptr); // Callback when a ``call`` returns vm.addCallCB(POSTCALL, on_call_exit, nullptr); ``` With these two callbacks we get the following output: ![Memory and call trace showing the string-decoding routine and its call stack.](memtrace-0.png) By going further in the memory trace, we can observe this output: ![Annotated memory trace distinguishing skipped data from per-character processing and function calls.](memtrace.png) From this output we can infer the behavior of the collector (pseudo-code): ```python f = open("/proc/self/maps") for line in f.readlines(): if not "/" in line: # Avoid entries such as XXX-YYY ... [anon:linker_alloc] continue if not "-xp" in line # Process executable segments only continue buffer += encode(line) ``` We can also observe a sequence of 1. **READ** ``line[i]`` 2. **CALL** ``.text!0xd2ba`` 3. **WRITE** ``encoded(line[i])`` It suggests that the logic of the ``encode()`` function is implemented at address **0xd2ba**. The CFG of this function is compounded by instructions that compare the input against *magic* printable values and we manually checked that it is the encoding function. Moreover, this function is, by design, reversible since the server side algorithm needs to process the *encoded* data. ![Control-flow graph of the reversible data-encoding function at address 0xd2ba.](encoding_routine.png) Library lifting =============== In the previous parts, we targeted the ARM version of the library. It turns out that SDKs which use native libraries usually provide the libraries for all architectures (``arm``, ``arm64``, ``x86``, ``x86-64``). Indeed, they do not want to limit developers to some architectures. The solution previously analyzed also comes with a ``x86-64`` version of ``libApp.so`` with the exact same interface. Moreover, the analysis done in the previous sections shows that there are no real dependencies to the Android system: - Syscall are standards and available on Linux. - ``/proc/self/maps`` and ``/proc/self/status`` are available on Linux. Thus, we can *lift* the library and run it on Linux. This technique has already been described in this blog post: [When SideChannelMarvels meet LIEF](https://blog.quarkslab.com/when-sidechannelmarvels-meet-lief.html). In a first step, we have to patch the library with LIEF: ```python import lief libApp = lief.parse("libApp.so") # Patch library names # =================== libApp.get_library("libc.so").name = "libc.so.6" libApp.get_library("liblog.so").name = "libc.so.6" libApp.get_library("libm.so").name = "libm.so.6" libApp.get_library("libdl.so").name = "libdl.so.2" # Patch dynamic entries # ===================== # 1. Remove ELF constructors libApp[lief.ELF.DYNAMIC_TAGS.INIT_ARRAY].array = [] libApp[lief.ELF.DYNAMIC_TAGS.INIT_ARRAY].tag = lief.ELF.DYNAMIC_TAGS.DEBUG libApp[lief.ELF.DYNAMIC_TAGS.INIT_ARRAYSZ].value = 0 libApp[lief.ELF.DYNAMIC_TAGS.FINI_ARRAY].array = [] libApp[lief.ELF.DYNAMIC_TAGS.FINI_ARRAY].tag = lief.ELF.DYNAMIC_TAGS.DEBUG libApp[lief.ELF.DYNAMIC_TAGS.FINI_ARRAYSZ].value = 0 # 2. Remove symbol versioning libApp[lief.ELF.DYNAMIC_TAGS.VERNEEDNUM].tag = lief.ELF.DYNAMIC_TAGS.DEBUG libApp[lief.ELF.DYNAMIC_TAGS.VERNEED].tag = lief.ELF.DYNAMIC_TAGS.DEBUG libApp[lief.ELF.DYNAMIC_TAGS.VERDEFNUM].tag = lief.ELF.DYNAMIC_TAGS.DEBUG libApp[lief.ELF.DYNAMIC_TAGS.VERDEF].tag = lief.ELF.DYNAMIC_TAGS.DEBUG libApp[lief.ELF.DYNAMIC_TAGS.VERSYM].tag = lief.ELF.DYNAMIC_TAGS.DEBUG libApp.write("libApp-x86-64.so") ``` Then, we can instantiate a Linux JVM and run the native function: ```cpp int main() { JavaVM *jvm = nullptr; JNIEnv* env = nullptr; // JVM options JavaVMOption opt[1]; JavaVMInitArgs args; ... // JVM instantiation JNI_CreateJavaVM(&jvm, reinterpret_cast(&env), &args); // Load the library void* hdl = dlopen("libApp-x86-64.so", RTLD_LAZY | RTLD_LOCAL); // Resolve the functions using abi_t = jint(*)(JNIEnv*); using jni_func_t = jstring(*)(JNIEnv*); auto&& jni_get_abi = reinterpret_cast(dlsym(hdl, "Java_XXX_JNIWrapper_ca3_14007")); auto&& jni_func = reinterpret_cast(dlsym(hdl, "Java_XXX_JNIWrapper_ca3_14008")); // Execute jint abi = jni_get_abi(env); console->info("ABI: {:d}", abi); jstring encoded = jni_func(env); console->info("ca3_14008(): {}", env->GetStringUTFChars(encoded, nullptr)); return EXIT_SUCCESS; } ``` By executing this code, we get a similar output as seen in the previous parts: ![Linux terminal output from the lifted x86-64 Android library creating a JVM and producing encoded device data.](rip.png) We can also run the ``strace`` utility to inspect the syscalls: ![strace output from the lifted library showing filesystem probes used to collect device state.](strace.png) Since we are able to run the function on Linux, we could also use ``gdb``, ``Intel PIN`` or ``QBDI(x86-64)`` to analyze the library. ## Conclusion While it has been quite challenging to add the whole ARM support in QBDI, it starts to work pretty well on real use cases. Such support should also lead to interesting applications among which: - HongFuzz / QBDI for Android. - [SideChannelMarvels](https://github.com/SideChannelMarvels) integration for CPA attacks. - Trustlets instrumentation. The raw traces used in this blog post are available here: [traces.zip](traces.zip) ## Acknowledgments Many thanks to Charles Hubain and Cédric Tessier who developed and designed QBDI. It is really pleasant to work on the concepts involved in this DBI. Thanks to the LLVM community to provide such framework without which this project would not be possible. Thanks to my Quarkslab colleagues who proofread this article. ## References [^1]: Frida modifies fields of the ``art::ArtMethod`` object associated with the Java method. [^2]: [Slides](https://qbdi.quarkslab.com/QBDI_34c3.pdf) - [Talk](https://media.ccc.de/v/34c3-9006-implementing_an_llvm_based_dynamic_binary_instrumentation_framework) [^3]: https://github.com/QBDI/QBDI/blob/master/examples [^4]: DroidGuard being the SafetyNet module that collects information about the device. [^5]: The name has been intentionally changed. [^6]: Plus the ``this`` parameter which is a ``jclass`` object for a static method. [^7]: Even though static analysis would be enough in this case. [^8]: Nexus 5X - Android 8.1.0 - Rooted with Magisk. --- # Android crackme challenge - Canonical: https://www.romainthomas.fr/post/android-crackme/ - Markdown: https://www.romainthomas.fr/post/android-crackme/index.md - Section: post - Published: 2018-11-20 - Modified: 2026-08-04 - Tags: android, challenge, crackme > Android crackme that uses system's internals > **Note** This post has been originally posted on the Quarkslab's Blog Here is an Android crackme developed for the Android training given at Quarkslab. The objective is to find the correct phone number that leads to the following message: ![success](screen.png) The application can be run on an emulator or a real device (whatever the architecture) but the Android version must be at **least Marshmallow** (> 6.0). [crackme-telegram.apk.zip](crackme-telegram.apk.zip) - ``SHA256: d66b82ebc14708b214a581760e99894af17e10598bcef95e75441a12b948bbf0`` Password is ``cr4ckm3`` --- # Android VDEX formats - Canonical: https://www.romainthomas.fr/post/android-vdex/ - Markdown: https://www.romainthomas.fr/post/android-vdex/index.md - Section: post - Published: 2018-06-25 - Modified: 2022-04-25 - Tags: android, vdex, format > Internal structures of VDEX format Here are internal structures of Android VDEX: # VDEX 10 ![VDEX 10](vdex_10.png) [PDF Version](vdex_10.pdf) # VDEX 06 ![VDEX 06](vdex_06.png) [PDF Version](vdex_06.pdf) --- # Android OAT formats - Canonical: https://www.romainthomas.fr/post/android-oat/ - Markdown: https://www.romainthomas.fr/post/android-oat/index.md - Section: post - Published: 2018-06-25 - Modified: 2026-08-04 - Tags: android, oat > Internal structures of OAT format Here are internal structures of Android OAT: # OAT 124 ![OAT 124](oat_124.webp) [PDF Version](oat_124.pdf) # OAT 79 ![OAT 79](oat_79.webp) [PDF Version](oat_79.pdf) # OAT 64 ![OAT 64](oat_64.webp) [PDF Version](oat_64.pdf) --- # When SideChannelMarvels meets LIEF - Canonical: https://www.romainthomas.fr/post/18-05-when-sidechannelmarvels-meets-lief/ - Markdown: https://www.romainthomas.fr/post/18-05-when-sidechannelmarvels-meets-lief/index.md - Section: post - Published: 2018-05-03 - Modified: 2026-08-04 > On how we used LIEF to lift an Android x86_64 library to Linux to perform our usual white-box attacks on it. > **Note** This post has been originally posted on the [Quarkslab's Blog](https://blog.quarkslab.com/when-sidechannelmarvels-meet-lief.html) ## Introduction For those of you following our SideChannelMarvels[^1], you know that we add each non-commercial white-box implementation we encounter to the Deadpool project. This project collects various public white-box cryptographic implementations and their practical attacks. This time, we wanted to take a look at the white-box created by Sanghwan (h2spice) Ahn and proposed during SECCON2016 CTF[^2]. Apparently only PPP solved it during the competition and Sanghwan wrote himself a write-up[^3]. The challenge consists in an Android APK. When you launch it, it displays a flag encrypted a random number of times (between 1 and 3601). When encrypted only once, the flag is `g1UlZafiuGdCgpTkWYjaZg3kE6qCd7kF3kV+nMKcGHc=`. To be able to 'plug' the challenge into our tools, we need to get an easy access to the input and output of the AES encryption function. A quick look reveals that the actual cryptographic operations are done in a native library called libnative-lib.so, conveniently available for several architectures. The function `TfcqPqf1lNhu0DC2qGsAAeML0SEmOBYX4jpYUnyT8qYWIlEq(unsigned char*, unsigned char*)` is the AES encryption function we want to attack. Note that the library is obfuscated with Obfuscator-LLVM 3.6.1, as we can see from its `.comment` section. But we're lazy, so we'd like to reuse the x86-64 version of `libnative-lib.so` under a Linux environment, where all the SideChannelMarvels toolchain is ready to crunch white-boxes. That's not that simple because, even if they look alike, dynamic libraries compiled for Android or for Linux have a number of differences and a naive attempt to load an Android dynamic library under Linux will simply fail. Fortunately, we have a nifty tool for parsing and modifying binaries. We're talking about LIEF[^4] of course! ## Converting an Android library to Linux with LIEF The white-box is implemented in the `libnative-lib.so` which is available for ARM, ARM64, x86, and x86-64 architectures. It's a tiny library exporting one **JNI** function: `Java_kr_repo_h2spice_crypto500_MainActivity_a` and importing three functions from external libraries. Lifting this library to Linux is possible because the three imported functions (`__cxa_finalize`, `__cxa_atexit`, `__stack_chk_fail`) are not specific to Android. The linked libraries of `libnative-lib.so` are standard: `libc`, `libstdc++` ... except for `liblog`. But `libnative-lib.so` doesn't use any of liblog functions, as we can see in readelf output: ```console $ readelf -s -d -W ./libnative-lib.so Dynamic section at offset 0x2ad00 contains 31 entries: Tag Type Name/Value ... 0x01 (NEEDED) Shared library: [liblog.so] 0x01 (NEEDED) Shared library: [libm.so] 0x01 (NEEDED) Shared library: [libstdc++.so] 0x01 (NEEDED) Shared library: [libdl.so] 0x01 (NEEDED) Shared library: [libc.so] ... Symbol table '.dynsym' contains 32 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000 0 NOTYPE LOCAL DEFAULT UND 1: 00000 0 FUNC GLOBAL DEFAULT UND __cxa_finalize@LIBC (2) 2: 00000 0 FUNC GLOBAL DEFAULT UND __cxa_atexit@LIBC (2) 3: 04fe0 865 FUNC GLOBAL DEFAULT 11 Java_kr_repo_h2spice_crypto500_MainActivity_a 4: 02070 2281 FUNC GLOBAL DEFAULT 11 _Z48APtMDGO79Go3cbIkFca2rN0KszanZXOZ7dIPsxDBletW5gdoPcPKci 5: 01100 3916 FUNC GLOBAL DEFAULT 11 _Z48DENCPKY6hzMem3SuzgIXu4u6vxbF1sajPOJ75aN2VTdc7SCLPcPKc 6: 00bb0 1345 FUNC GLOBAL DEFAULT 11 _Z48KwUmSQBCaOVJKeqvABGpVnuErM7j8YCSOagNYBmr2ah0NZBePKc 7: 03860 6011 FUNC GLOBAL DEFAULT 11 _Z48TfcqPqf1lNhu0DC2qGsAAeML0SEmOBYX4jpYUnyT8qYWIlEqPhS_ 8: 02050 30 FUNC GLOBAL DEFAULT 11 _Z48h8AU0jPcyu9vXF9Kvg0bGDSl6H3TtcJIoOoU1ZOObCvegZ84i 9: 00000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@LIBC (2) 10: 02960 3836 FUNC GLOBAL DEFAULT 11 _Z48lrsFdMdlAT0vSMVedxmqOkCBF7sCTbhCjYEp1rLP8vatWEGDPh 29: 2c008 0 NOTYPE GLOBAL DEFAULT ABS _edata 30: 2c008 0 NOTYPE GLOBAL DEFAULT ABS __bss_start 31: 2c050 0 NOTYPE GLOBAL DEFAULT ABS _end ``` Thus we can simply remove the `liblog` library by setting its dynamic tag to `DT_NULL`: ```python import lief libnative = lief.parse("libnative-lib.so") liblog = libnative.get_library("liblog.so") liblog.tag = lief.ELF.DYNAMIC_TAGS.NULL ``` We also notice that the `libc` is named `libc.so` while the one on the current Linux version is named libc.so.6. To address this issue, one solution would be to create a symbol link of `libc.so.6` to `libc.so` and set the environment variable `LD_LIBRARY_PATH` to the directory that contains the symlink. A more elegant solution is to rename the library with LIEF: ```python libnative.get_library("libc.so").name = "libc.so.6" ``` Lastly, `libnative-lib.so` imports `__cxa_finalize`, `__cxa_atexit` and `__stack_chk_fail` with a specific version. The version can be seen in the imported names, next to the `@` character. For these symbols, the associated version is "`LIBC`" and, during the loading step, the loader will look for the `__cxa_finalize` in `libc.so.6` with this exact version. But the Linux `libc.so.6` defines these symbols with a "`GLIBC_2.2.5`" version: ```console $ readelf -s -W /usr/lib64/libc.so.6|grep __cxa_finalize 1944: 00037cf0 535 FUNC GLOBAL DEFAULT 12 __cxa_finalize@@GLIBC_2.2.5 ``` To fix the version issue, we can simply change the version to unspecified by setting its value to `1`: ```python for s in filter(lambda e: e.has_version, libnative.dynamic_symbols): if s.symbol_version.value > 1: # Library-defined version s.symbol_version.value = 1 # Set to unspecified ``` And then build the modified library: ```python libnative.write("libnative-fixed.so") ``` Finally, we can load and execute the lifted library with `dlopen` / `dlsym`: (error handling being stripped for readability) ```cpp using fnc_t = uint64_t(*)(unsigned char*, unsigned char*); int main(void) { void* h = dlopen("./libnative-fixed.so", RTLD_NOW); void* sh = dlsym(h, "_Z48TfcqPqf1lNhu0DC2qGsAAeML0SEmOBYX4jpYUnyT8qYWIlEqPhS_"); fnc_t AES_128_encrypt = reinterpret_cast(sh); unsigned char plaintext[16]; unsigned char ciphertext[16]; fread(plaintext, 1, 16, stdin); AES_128_encrypt(plaintext, ciphertext); fwrite(ciphertext, 1, 16, stdout); return 0; } ``` This native library has a special structure that enables the transformation: 1. It doesn't use functions specific to Android. 2. It doesn't use packed relocations. 3. It doesn't use exceptions. 4. It doesn't use Thread Local Storage (TLS). The first point is very uncommon for JNI libraries and this transformation won't be possible for usual libraries. ## Eventually breaking the white-box Now that we got a Linux binary of the AES white-box with standardized input/output, we're back into usual white-box attacks business. The Differential Fault Analysis attack on white-box using our tools is largely explained in a [previous blogpost](https://blog.quarkslab.com/differential-fault-analysis-on-white-box-aes-implementations.html). In short, we inject statically some faults in the white-box tables (here, we'll shoot on the entire `.rodata` section of the dynamic library), execute the AES on a constant input, and observe the output for faults. These steps are automated in the `deadpool_dfa.Acquisition` function, part of our [SideChannelMarvels/Deadpool](https://github.com/SideChannelMarvels/Deadpool) repository. Once we collected enough faulty outputs, we can apply a well-known DFA attack to recover the AES key, which is implemented in the `phoenixAES.crack` function from the [SideChannelMarvels/JeanGrey](https://github.com/SideChannelMarvels/JeanGrey) repository. ```python import deadpool_dfa import phoenixAES def processinput(iblock, blocksize): return (bytes.fromhex('%0*x' % (2*blocksize, iblock)), None) def processoutput(output, blocksize): return int.from_bytes(output, byteorder='big', signed=False) engine = deadpool_dfa.Acquisition( # main white-box executable targetbin='./main64', # file where to inject faults, and a reference copy targetdata='./libnative-fixed.so', goldendata='./libnative-fixed.so.gold', # hook to the DFA library, to validate faulty outputs dfa=phoenixAES, # hooks to process I/O as expected by the white-box executable processinput=processinput, processoutput=processoutput, # some tuning, telling we want to try up to single byte faults verbose=2, minleaf=1, minleafnail=1, # the libnative-fixed.so .rodata section address range addresses=[0x6350,0x2b490] ) outputs = engine.run()[0][0] phoenixAES.crack(outputs) ``` Execution: ```text ... Lvl 016 [0x000226DF-0x000226E0[ xor 0x86 -> B25BE351AD6986FF15D1E152E7802EC7 GoodEncFault Column:1 Logged Lvl 016 [0x000226DF-0x000226E0[ xor 0x69 -> B235E351806986FF15D1E1A4E780A6C7 GoodEncFault Column:1 Logged Saving 17 traces in dfa_enc_20180427_112029-112038_17.txt Last round key #N found: 040D08DA68001026F3DC0D68897148B4 ``` The DFA recovers the last (tenth) round key but the AES key schedule is invertible so we can go back to the original AES key: ```console $ aes_keyschedule 040D08DA68001026F3DC0D68897148B4 10 K00: 6C2893F21B6185E8567238CB78184945 ``` The key falls in 10.2s and 3300 executions. This is indeed the correct AES key: ```console $ echo g1UlZafiuGdCgpTkWYjaZg3kE6qCd7kF3kV+nMKcGHc=|base64 -d|\ openssl enc -d -aes-128-ecb -nopad -K 6C2893F21B6185E8567238CB78184945 SECCON{owSkwPeH1CHQdPV9KWrSmz9n} ``` ## Final Words We hope this little exercise will make you feel like using our tools! The whitebox and all the scripts to convert the library and apply the attack are available online [^5] and LIEF has its own website[^4]. Thanks to all Quarkslab colleagues who proofread this article and provided valuable feedback. [^1]: Side-Channel Marvels repository, on [GitHub](https://github.com/SideChannelMarvels). [^2]: SECCON2016 Online CTF-Binary / Crypto500 Obfuscated AES, archived [here](https://github.com/SECCON/SECCON2016_online_CTF/tree/master/Binary/500_Obfuscated%20AES). [^3]: Sanghwan's Obfuscated AES Write-Up, in [English](http://www.repo.kr/2016/12/seccon-2016-online-ctf-binarycrypto500_13.html), [korean,](http://www.repo.kr/2016/12/seccon-2016-online-ctf-binarycrypto500.html) and [Japanese](http://www.repo.kr/2016/12/seccon-2016-online-ctf-binarycrypto500_30.html) [^4]: [Library to Instrument Executable Formats](https://lief-project.github.io) [^5]: SECCON 2016 Obfuscated AES artifacts [in Deadpool](https://github.com/SideChannelMarvels/Deadpool/tree/master/wbs_aes_seccon2016) --- # How to use frida on a non-rooted device - Canonical: https://www.romainthomas.fr/post/how-to-use-frida-on-a-non-rooted-device/ - Markdown: https://www.romainthomas.fr/post/how-to-use-frida-on-a-non-rooted-device/index.md - Section: post - Published: 2018-03-03 - Modified: 2022-05-14 - Tags: lief > This post explains how to use Frida gadget on a non-rooted device. This post is a part of the LIEF tutorials. It explains how to combine [Frida](https://www.frida.re/) and [LIEF](https://lief-project.github.io/) to run ``frida-gadget`` on a non rooted device. [See more](https://lief-project.github.io/doc/latest/tutorials/09_frida_lief.html) --- # Have fun with LIEF and Executable Formats - Canonical: https://www.romainthomas.fr/post/17-11-have-fun-with-lief-and-executable-formats/ - Markdown: https://www.romainthomas.fr/post/17-11-have-fun-with-lief-and-executable-formats/index.md - Section: post - Published: 2017-11-02 - Modified: 2026-08-04 - Tags: lief > This blog post introduces new features of LIEF as well as some uses cases. > **Note** This post has been originally posted on the [Quarkslab's blog](https://blog.quarkslab.com/have-fun-with-lief-and-executable-formats.html) This blog post introduces new features of LIEF as well as some uses cases. > **Note** **TL;DR**: LIEF v0.8.3 is out. The main changelog is available [here](https://lief.quarkslab.com/doc/changelog.html#october-16-2017) and packages can be downloaded on the [official website](https://lief.quarkslab.com/#download). To install the Python package: ```console $ pip install lief ``` ## Development process We attach a great importance to the automation of some development tasks like testing, distributing, packaging, etc. Here is a summary of these processes: Each commits is tested on - Linux - x86-64 - Python{2.7, 3.5, 3.6} - Windows - x86 / x86-64 - Python{2.7, 3.5, 3.6} - OSX - x86-64 - Python{2.7, 3.5, 3.6} The test suite includes: - Tests on the Python API - Tests on the C API - Tests on the parsers - Tests on the builders If tests succeeds packages are automatically uploaded on the https://github.com/lief-project/packages repository. For tagged version, packages are uploaded on the GitHub release page: https://github.com/lief-project/LIEF/releases. Dockerlief ### Dockerlief To facilitate the compilation and the use of LIEF, we created the [Dockerlief](https://github.com/lief-project/Dockerlief) repo which includes various [Dockerfiles](https://github.com/lief-project/Dockerlief/tree/v0.1.0/dockerlief/dockerfiles) as well as the `dockerlief` utility. `dockerlief` is basically a wrapper on docker build . Among Dockerfiles, we provide a [Dockerfile](https://github.com/lief-project/Dockerlief/blob/v0.1.0/dockerlief/dockerfiles/android.docker) to cross compile LIEF for Android (`ARM`, `AARCH64`, `x86`, `x86-64`) To cross compile LIEF for Android ARM, one can run: ```console $ dockerlief build --api-level 21 --arm lief-android [INFO] - Location of the Dockerfiles: ~/dockerfiles [INFO] - Building Dockerfile: 'lief-android' [INFO] - Target architecture: armeabi-v7a [INFO] - Target API Level: 21 ``` The SDK package `LIEF-0.8.3-Android_API21_armeabi-v7a.tar.gz` is automatically pulled from the Docker to the current directory. ### Integration of libFuzzer Fuzzing our own library is a good way to detect bugs, memory leak, unsanitized inputs ... Thus, we integrated [libFuzzer](https://llvm.org/docs/LibFuzzer.html) in the project. Fuzzing the LIEF ELF, PE, Mach-O parser is as simple as: ```cpp #include #include #include extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { std::vector raw = {data, data + size}; try { std::unique_ptr b{LIEF::Parser::parse(raw)}; } catch (const LIEF::exception& e) { std::cout << e.what() << std::endl; } return 0; } ``` To launch the fuzzer, one can run the following commands: ```console $ make fuzz-elf # Launch ELF Fuzzer $ make fuzz-pe # Launch PE Fuzzer $ make fuzz-macho # Launch MachO Fuzzer $ make fuzz # Launch ELF, PE and MachO Fuzzer ``` ## ELF ### Play with ELF symbols - Part 2 In the [tutorial #03](https://lief.quarkslab.com/doc/tutorials/03_elf_change_symbols.html) we demonstrated how to swap dynamic symbols between a binary and a library. In this part, we will see how we can rename these symbols. Changing symbol names is not a trivial modification, since modifying the string table of the `PT_DYNAMIC` segment has side effects: - It requires to update the hash table (GNU Hash / SYSV). - It usually requires to extend the `DYNAMIC` part of the ELF format. The previous version of LIEF already implements the rebuilding of the hash table but not the extending of the `DYNAMIC` part. With the `v0.8.3` we can extend the `DYNAMIC` part. Therefore: - We can add new entries in the `.dynamic` section - We can change dynamic symbols names - We can change `DT_RUNPATH` and `DT_RPATH` without length restriction We will rename all **imported** functions of `gpg` that are imported from `libgcrypt.so.20` into `a_very_long_name_of_function_XX` and all exported functions of `libgcrypt.so.20` into the same name (*XX* is the symbol index). [^1] ```python import lief # Load targets gpg = lief.parse("/usr/bin/gpg") libgcrypt = lief.parse("/usr/lib/libgcrypt.so.20") # Change names for idx, lsym in enumerate(filter(lambda e : e.exported, libgcrypt.dynamic_symbols)): new_name = 'a_very_long_name_of_function_{:d}'.format(idx) print("New name for '{}': {}".format(lsym.name, new_name)) for bsym in filter(lambda e : e.name == lsym.name, gpg.dynamic_symbols): bsym.name = new_name lsym.name = new_name # Write back binary.write(gpg.name) libgcrypt.write(libgcrypt.name) ``` By using `readelf` we can check that function names have been modified: ```console $ readelf -s ./gpg|grep "a_very_long_name" 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND a_very_long_name_of_funct@GCRYPT_1.6 (2) 3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND a_very_long_name_of_funct@GCRYPT_1.6 (2) 11: 0000000000000000 0 FUNC GLOBAL DEFAULT UND a_very_long_name_of_funct@GCRYPT_1.6 (2) 13: 0000000000000000 0 FUNC GLOBAL DEFAULT UND a_very_long_name_of_funct@GCRYPT_1.6 (2) ... $ readelf -s ./libgcrypt.so.20|grep "a_very_long_name" 88: 000000000000d050 6 FUNC GLOBAL DEFAULT 10 a_very_long_name_of_funct@@GCRYPT_1.6 89: 000000000000dcd0 69 FUNC GLOBAL DEFAULT 10 a_very_long_name_of_funct@@GCRYPT_1.6 90: 000000000000d310 34 FUNC GLOBAL DEFAULT 10 a_very_long_name_of_funct@@GCRYPT_1.6 91: 000000000000de70 81 FUNC GLOBAL DEFAULT 10 a_very_long_name_of_funct@@GCRYPT_1.6 ... ``` ![IDA graph of the modified gpg binary showing imports renamed to a_very_long_name_of_function_*.](./ida_gpg.png) Now if we run the new `gpg` binary, we get the following error: ```console $ ./gpg --output bar.txt --symmetric ./foo.txt relocation error: ./gpg: symbol a_very_long_name_of_function_8, version GCRYPT_1.6 not defined in file libgcrypt.so.20 with link time reference ``` Because the Linux loader tries to resolve the function `a_very_long_name_of_function_8` against `/usr/lib/libgcrypt.so.20` and that library doesn't include the updated names we get the error. One way to fix this error is to set the environment variable `LD_LIBRARY_PATH` to the current directory: ```console $ LD_LIBRARY_PATH=. ./gpg --output bar.txt --symmetric ./foo.txt $ xxd ./bar.txt|head -n1 00000000: 8c0d 0407 0302 c5af 9fba cab1 9545 ebd2 .............E.. $ LD_LIBRARY_PATH=. ./gpg --output foo_decrypted.txt --decrypt ./bar.txt $ xxd ./foo_decrypted.txt|head -n1 00000000: 4865 6c6c 6f20 576f 726c 640a Hello World. ``` Another way to fix it is to add a new entry in `.dynamic` section. As mentioned at the beginning, we can now add new entries in the `.dynamic` so let's add a `DT_RUNPATH` entry with the `$ORIGIN` value so that the Linux loader resolves the modified `libgcrypt.so.20` instead of the system one: ```python ... # Add a DT_RUNPATH entry gpg += lief.ELF.DynamicEntryRunPath("$ORIGIN") # Write back binary.write(gpg.name) libgcrypt.write(libgcrypt.name) ``` And we don't need the `LD_LIBRARY_PATH` anymore: ```console $ readelf -d ./gpg|grep RUNPATH 0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN] $ ./gpg --decrypt ./bar.txt gpg: AES encrypted data gpg: encrypted with 1 passphrase Hello World ``` ### Hiding its symbols While IDA v7.0 has been released recently, among the [changelog](https://www.hex-rays.com/products/ida/7.0/index.shtml) one can notice two changes: > - ELF: describe symbols using symtab from DYNAMIC section > - ELF: IDA now uses the PHT by default instead of the SHT to load segments from ELF files These changes are partially true. Let's see what go wrong in IDA with the following snippet: ```python id = lief.parse("/usr/bin/id") dynsym = id.get_section(".dynsym") dynsym.entry_size = dynsym.size // 2 id.write("id_test") ``` This snippet defines the size of **one** symbol as the entire size of `.dynsym` section divided by 2. The *normal* size of ELF symbols would be: ```python >>> print(int(lief.ELF.ELF32.SIZES.SYM)) # For 32-bits 16 >>> print(int(lief.ELF.ELF64.SIZES.SYM)) # For 64-bits 24 ``` In the case of the 64-bits `id` binary, we set this size to **924**. When opening `id_test` in IDA and forcing to use **Segment** for parsing and not **Sections** we get the following imports:
![IDA Loading Dialog](ida_loading.png)
![IDA Loading Dialog](ida_imports.png)
Only one import is resolved and the others are **hidden**. Note that `id_test` is still executable: ```console $ id_test uid=1000(romain) gid=1000(romain) ... ``` By using `readelf` we can still retrieve the symbols and we have an error indicating that symbol size is corrupted. ```console $ readelf -s id_test readelf: Error: Section 5 has invalid sh_entsize of 000000000000039c readelf: Error: (Using the expected size of 24 for the rest of this dump) Symbol table '.dynsym' contains 77 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND endgrent@GLIBC_2.2.5 (2) 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __uflow@GLIBC_2.2.5 (2) 3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getenv@GLIBC_2.2.5 (2) 4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND free@GLIBC_2.2.5 (2) 5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND abort@GLIBC_2.2.5 (2) ... ``` In LIEF the (dynamic) symbol table address is computed through the `DT_SYMTAB` from the `PT_DYNAMIC` segment. To compute the number of dynamic symbols LIEF uses three heuristics: 1. Based on hash tables ([GNU Hash](https://github.com/lief-project/LIEF/blob/0.8.3/src/ELF/Parser.tcc#L711-L796) / [SYSV Hash](https://github.com/lief-project/LIEF/blob/0.8.3/src/ELF/Parser.tcc#L690-L708)) 2. Based on [relocations](https://github.com/lief-project/LIEF/blob/0.8.3/src/ELF/Parser.tcc#L513-L648) 3. Based on [sections](https://github.com/lief-project/LIEF/blob/0.8.3/src/ELF/Parser.tcc#L653-L672) Malwares start to use this kind of corruption as we will see in the next part. ### Rootnik Malware Rootnik is a malware targeting Android devices. It has been analyzed by Fortinet security researcher. A full analysis of the malware is available on the [Fortinet blog](https://blog.fortinet.com/2017/07/09/unmasking-android-malware-a-deep-dive-into-a-new-rootnik-variant-part-i). This part is focused on the ELF format analysis of one component: libshell. Actually there are two libraries `libshella_2.10.3.1.so` and `libshellx_2.10.3.1.so`. As they have the same purpose, we will use the x86 version. First if we look at the ELF sections of `libshellx_2.10.3.1.so` we can notice that the **address**, **offset,** and **size** of some sections like `.text`, `.init_array`, `.dynstr`, `.dynsym` are set to 0. This kind of modification is used to disturb tools that rely on **sections** to parse some ELF structures (like objdump, readelf, IDA ...) ```console $ readelf -S ./libshellx-2.10.3.1.so There are 21 section headers, starting at offset 0x2431c: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .dynsym DYNSYM 00000114 000114 000300 10 A 2 1 4 [ 2] .dynstr STRTAB 00000414 000414 0001e2 00 A 0 0 1 [ 3] .hash HASH 00000000 000000 000000 04 A 1 0 4 [ 4] .rel.dyn REL 00000000 000000 000000 08 A 1 0 4 [ 5] .rel.plt REL 00000000 000000 000000 08 AI 1 6 4 [ 6] .plt PROGBITS 00000000 000000 000000 04 AX 0 0 16 [ 7] .text PROGBITS 00000000 000000 000000 00 AX 0 0 16 [ 8] .code PROGBITS 00000000 000000 000000 00 AX 0 0 16 [ 9] .eh_frame PROGBITS 00000000 000000 000000 00 A 0 0 4 [10] .eh_frame_hdr PROGBITS 00000000 000000 000000 00 A 0 0 4 [11] .fini_array FINI_ARRAY 00000000 000000 000000 00 WA 0 0 4 [12] .init_array INIT_ARRAY 00000000 000000 000000 00 WA 0 0 4 [13] .dynamic DYNAMIC 0000ce50 00be50 0000f8 08 WA 2 0 4 [14] .got PROGBITS 00000000 000000 000000 00 WA 0 0 4 [15] .got.plt PROGBITS 00000000 000000 000000 00 WA 0 0 4 [16] .data PROGBITS 00000000 000000 000000 00 WA 0 0 16 [17] .bss NOBITS 0000d398 00c395 000000 00 WA 0 0 4 [18] .comment PROGBITS 00000000 00c395 000045 01 MS 0 0 1 [19] .note.gnu.gold-ve NOTE 00000000 00c3dc 00001c 00 0 0 4 [20] .shstrtab STRTAB 00000000 024268 0000b1 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), I (info), L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), p (processor specific) ``` If we open the given library in IDA we have no exports, no imports and no sections: ![IDA Shella](libshell_ida.png) Based on the segments and dynamic entries we can recover most of these information: - `.init_array` address and size are available through the `DT_INIT_ARRAY` and `DT_INIT_ARRAYSZ` entries - `.dynstr` address and size are available through the `DT_STRTAB` and `DT_STRSZ` - `.dynsym` address is available through the `DT_SYMTAB` The script [recover_shellx.py](https://gist.github.com/romainthomas/c262be2c29ab374451663b9f6dcdfc0d) recovers the missing values, patch sections and rebuild a *fixed* library. ![IDA Shella recovered](libshell_FIXED_ida.png) Now if we open the new `libshellx-2.10.3.1_FIXED.so` we have access to imports / exports and some sections. The `.init_array` section contains 2 functions: 1. `tencent652524168491435794009` 2. `sub_60C0` The `tencent652524168491435794009` function basically do a stack alignment and the `sub_60C0` is one of the decryption routines[^2]. This function is obfuscated with graph flattening and looks like to O-LLVM graph flattening passe[^3]: ![CFG Flattening](shellx_cfg.png) Fortunately there are few "*relevant blocks*" and there are not obfuscated. The function `sub_60C0` basically iterates over the program headers to find the encrypted one and decrypt it using a custom algorithm (based on shift, xor, etc). ![CFG Flattening Handlers](shellx_cfg_1-2.png) ![CFG Flattening Decryption Routine](shellx_cfg_2-2.png) ### Triggering CVE-2017-1000249 The CVE-2017-1000249 is a stack based buffer overflow in the `file` utility. It affects the versions `5.29`, `5.30` and `5.31`. Basically the overflow occurs in the size of the note description. Using LIEF we can trigger the overflow as follows: ```python import lief target = lief.parse("/usr/bin/id") note_build_id = target[lief.ELF.NOTE_TYPES.BUILD_ID] note_build_id.description = [0x41] * 30 target.write("id_overflow") ``` ```console $ file --version file-5.29 magic file from /usr/share/file/misc/magic $ id_overflow uid=1000(romain) gid=1000(romain) ... $ file id_overflow *** buffer overflow detected ***: file terminated ./id_overflow: [1] 3418 abort (core dumped) file ./id_overflow ``` Here is the commit that introduced the bug: [9611f3](https://github.com/file/file/commit/9611f31313a93aa036389c5f3b15eea53510d4d1#diff-bc5c24ef9f39a5f4963ca28ecbc645b3L512). ## PE The *Load Configuration* directory is now parsed into the [LoadConfiguration](https://github.com/lief-project/LIEF/blob/0.8.3/include/LIEF/PE/LoadConfigurations/LoadConfiguration.hpp) object. This structure evolves with the Windows versions and LIEF has been designed to support this evolution. You can take a look at [LoadConfigurationV0](https://github.com/lief-project/LIEF/blob/0.8.3/include/LIEF/PE/LoadConfigurations/LoadConfigurationV0.hpp#L47-L52), [LoadConfigurationV6](https://github.com/lief-project/LIEF/blob/0.8.3/include/LIEF/PE/LoadConfigurations/LoadConfigurationV6.hpp#L47-L54). One can find the different versions of this structure in the following directories: * `include/LIEF/PE/LoadConfigurations` * `src/PE/LoadConfigurations` The current version of LIEF is able to parse the structure up to Windows 10 build 15002 with the *hotpatch table offset*. Here are some examples of the `LoadConfiguration` API: ```python >>> target = lief.parse("PE64_x86-64_binary_WinApp.exe") >>> target.has_configuration True >>> config = target.load_configuration >>> config.version WIN_VERSION.WIN10_0_15002 >>> hex(config.guard_rf_failure_routine) '0x140001040' ``` LIEF also provides an API to serialize any ELF or PE objects into JSON[^4] For examples to transform `LoadConfiguration` object into JSON: ```python >>> from lief import to_json >>> to_json(config) '{"characteristics":248,"code_integrity":{"catalog":0,"catalog_offset":0 ... }}' # Not fully printed ``` One can also serialize the whole Binary object: ```python >>> to_json(target) '{"data_directories":[{"RVA":0,"size":0,"type":"EXPORT_TABLE"},{"RVA":62584,"section" ...}}' # # Not fully printed ``` ## Mach-O For Mach-O binary, dynamic executables embed the `LC_DYLD_INFO` command which is associated with the `dyld_info_command` structure. The structure is basically a list of offsets and sizes pointing to other data structures. From `/usr/lib/mach-o/loader.h` the structure looks like this: ```cpp struct dyld_info_command { uint32_t cmd; uint32_t cmdsize; uint32_t rebase_off; uint32_t rebase_size; uint32_t bind_off; uint32_t bind_size; uint32_t weak_bind_off; uint32_t weak_bind_size; uint32_t lazy_bind_off; uint32_t lazy_bind_size; uint32_t export_off; uint32_t export_size; }; ``` The `dyld` loader uses this structure to: - Rebase the executable - Bind symbols to addresses - Retrieve exported functions (or symbols) Whereas in the ELF and PE format relocations are basically a **table**, Mach-O format uses **byte streams** to rebase the image and to bind symbols with addresses. For exports it uses a **trie** as subjacent structure. In the new version of LIEF, the Mach-O parser is able to handle these underlying structures to provide a user-friendly API: The export trie is represented by the [ExportInfo](https://github.com/lief-project/LIEF/blob/0.8.3/include/LIEF/MachO/ExportInfo.hpp) object which is usually tied to a [Symbol](https://github.com/lief-project/LIEF/blob/master/include/LIEF/MachO/Symbol.hpp). The binding byte stream is represented trough the [BindingInfo](https://github.com/lief-project/LIEF/blob/0.8.3/include/LIEF/MachO/BindingInfo.hpp) object. For the rebase byte stream, the parser create virtual relocations to model the rebasing process. These virtual relocations are represented by the [RelocationDyld](https://github.com/lief-project/LIEF/blob/0.8.3/include/LIEF/MachO/RelocationDyld.hpp) object and among other attributes it contains `address`, `size` and `type`[^5]. Here is an example using the Python API: ```python >>> id = lief.parse("/usr/bin/id") >>> print(id.relocations[0]) 100002000 POINTER 64 DYLDINFO __DATA.__eh_frame dyld_stub_binder >>> print(id.has_dyld_info) True >>> dyldinfo = id.dyld_info >>> print(dyldinfo.bindings[0]) Class: STANDARD Type: POINTER Address: 0x100002010 Symbol: ___stderrp Segment: __DATA Library: /usr/lib/libSystem.B.dylib >>> print(dyldinfo.exports[0]) Node Offset: 18 Flags: 0 Address: 0 Symbol: __mh_execute_header ``` ## Conclusion In this release we did a large improvement of the ELF builder. Mach-O and PE parts gain new objects and new functions. LIEF is now available on [PyPI](https://pypi.python.org/pypi/lief) and can be added in the requirements of Python projects whatever the Python version and the target platform. Since the `v0.7.0` LIEF has been presented at [RMLL](https://prog2017.rmll.info/programme/securite-entre-transparence-et-opacite/lief-bibliotheque-d-instrumentation-de-formats-executables-mais-ca-fait-bife-c?lang=en) and the [MISP](http://www.misp-project.org/) project uses it for its *PyMISP objects*. Some may complain about the C API. They are right! Until the `v1.0.0` we will provide a minimal C API. Once C++ API is stable we plan to provide full APIs for Python, C, Java, OCaml[^6], etc. Next version should be focused on the Mach-O builder especially for adding sections and segments. We also plan to support PE `.NET` headers and fix some performances issues. For questions you can join the [Gitter channel](https://gitter.im/lief-project). [^1]: All Python examples are done with the 3.5 version [^2]: As mentioned in the Fortinet blog post, the library is packed. [^3]: See the blog post about O-LLVM analysis: https://blog.quarkslab.com/deobfuscation-recovering-an-ollvm-protected-program.html [^4]: This feature is not yet available for MachO objects [^5]: Due to the inheritance relationship and abstraction these attributes are located in the MachO::Relocation and LIEF::Relocation objects. [^6]: https://github.com/aziem/LIEF-ocaml --- # Open-sourcing LIEF - Canonical: https://www.romainthomas.fr/post/lief-release/ - Markdown: https://www.romainthomas.fr/post/lief-release/index.md - Section: post - Published: 2017-04-04 - Modified: 2026-09-05 - Tags: lief > We are open-sourcing LIEF, a library to parse and manipulate ELF, PE, and Mach-O binary formats. This blog post explains the purpose of this project and some parts of its … > **Note** This post has been originally posted on the Quarkslab's blog Executable File Formats in a Nutshell ===================================== When dealing with executable files, the first layer of information is the format in which the code is wrapped. We can see an executable file format as an envelope. It contains information so that the postman (i.e. Operating System) can handle and deliver (i.e. execute) it. The message wrapped by this envelope would be the machine code. There are mainly three mainstream formats, one per OS: * **Portable Executable** (**PE**) for Windows systems * **Executable and Linkable Format** (**ELF**) for UNIX systems (Linux, Android...). * Mach-O for OS-X, iOS... Other executable file formats, such as ``COFF``, exist but they are less relevant. Usually each format has a header which describes at least the target architecture, the program's entry point and the type of the wrapped object (executable, library...) Then we have blocks of data that will be mapped by the OS's loader. These blocks of data could hold machine code (``.text``), read-only data (``.rodata``) or other OS specific information. For PE there is only one kind of such block: **Section**. For ELF and Mach-O formats, a section has a different meaning. In these formats, sections are used by the **linker** at the **compilation** step, whereas **segments** (second type of block) are used by the OS's loader at **execution** step. Thus, sections are not mandatory for ELF and Mach-O formats and can be removed without affecting the execution. Purpose of LIEF =============== It turns out that many projects need to parse executable file formats but don't use a *standard* library and re-implement their own parser (and the wheel). Moreover, these parsers are usually bound to one language. On Unix system one can find the ``objdump`` and ``objcopy`` utilities but they are limited to Unix and the API is not user-friendly. The purpose of LIEF is to fill this void: * Providing a cross-platform library which can parse and modify (in a certain extent) ELF, PE, and Mach-O formats using a common abstraction * Providing an API for different languages (Python, C++, ,C ...) * Abstract common features from the different formats (Section, header, entry point, symbols, ...) The following snippets show how to obtain information about an executable using different API of LIEF: ```python import lief # ELF binary = lief.parse("/usr/bin/ls") print(binary) # PE binary = lief.parse("C:\\Windows\\explorer.exe") print(binary) # Mach-O binary = lief.parse("/usr/bin/ls") print(binary) ``` With the ``C++`` API: ```cpp #include int main(int argc, const char** argv) { LIEF::ELF::Binary* elf = LIEF::ELF::Parser::parse("/usr/bin/ls"); LIEF::PE::Binary* pe = LIEF::PE::Parser::parse("C:\\Windows\\explorer.exe"); LIEF::MachO::Binary* macho = LIEF::MachO::Parser::parse("/usr/bin/ls"); std::cout << *elf << std::endl; std::cout << *pe << std::endl; std::cout << *macho << std::endl; delete elf; delete pe; delete macho; } ``` And finally with the ``C`` API: ```C #include int main(int argc, const char** argv) { Elf_Binary_t* elf_binary = elf_parse("/usr/bin/ls"); Pe_Binary_t* pe_binary = pe_parse("C:\\Windows\\explorer.exe"); Macho_Binary_t** macho_binaries = macho_parse("/usr/bin/ls"); Pe_Section_t** pe_sections = pe_binary->sections; Elf_Section_t** elf_sections = elf_binary->sections; Macho_Section_t** macho_sections = macho_binaries[0]->sections; for (size_t i = 0; pe_sections[i] != NULL; ++i) { printf("%s\n", pe_sections[i]->name) } for (size_t i = 0; elf_sections[i] != NULL; ++i) { printf("%s\n", elf_sections[i]->name) } for (size_t i = 0; macho_sections[i] != NULL; ++i) { printf("%s\n", macho_sections[i]->name) } elf_binary_destroy(elf_binary); pe_binary_destroy(pe_binary); macho_binaries_destroy(macho_binaries); } ``` LIEF supports FAT-MachO and one can iterate over binaries as follows: ```python import lief binaries = lief.MachO.parse("/usr/lib/libc++abi.dylib") for binary in binaries: print(binary) ``` > **Note** The above script uses the ``lief.MachO.parse`` function instead of the ``lief.parse`` function because ``lief.parse`` returns a **single** ``lief.MachO.binary`` object whereas ``lief.MachO.parse`` returns a **list** of ``lief.MachO.binary`` (according to the FAT-MachO format). Along with standard format components like headers, sections, import table, load commands, symbols, etc. LIEF is also able to parse PE Authenticode: ```python import lief driver = lief.parse("driver.sys") for crt in driver.signature.certificates: print(crt) ``` ```console Version: 3 Serial Number: 61:07:02:dc:00:00:00:00:00:0b Signature Algorithm: SHA1_WITH_RSA_ENCRYPTION Valid from: 2005-9-15 21:55:41 Valid to: 2016-3-15 22:5:41 Issuer: DC=com, DC=microsoft, CN=Microsoft Root Certificate Authority Subject: C=US, ST=Washington, L=Redmond, O=Microsoft Corporation, CN=Microsoft Windows Verification PCA ... ``` Full API documentation is available here * [Python API](https://lief-project.github.io/doc/latest/api/python/index.html) * [C++ API](https://lief-project.github.io/doc/latest/api/cpp/index.html) * [C API](https://lief-project.github.io/doc/latest/api/c/index.html) Architecture ============ In the ``LIEF`` architecture, each format implements at least the following classes: * Parser: Parse the format and decompose it into a ``Binary`` class * Binary: Modelize the format and provide an API to modify and explore it. * Builder: Transform the binary object into a valid file. ![LIEF parser, binary, and builder architecture](archi.png) To factor common characteristics in formats we have an inheritance relationship between these characteristics. For symbols it gives the following diagram: ![LIEF cross-format symbol inheritance diagram](symbol_inheritance.png) It enables to write cross-format utility like ``nm``. ``nm`` is a Unix utility to list symbols in an executable. The source code is available here: [binutils](https://github.com/gittup/binutils/blob/0af702d47a443acea853b84157c2e81f6c131e77/binutils/nm.c) With the given inheritance relationship one can write this utility for the three formats in a single script: ```python import lief import sys def nm(binary): for symbol in binary.symbols: print(symbol) return 0 if __name__ == "__main__": r = nm(sys.argv[1]) sys.exit(r) ``` Conclusion ========== As LIEF is still a young project we hope to have feedback, ideas, suggestions, and pull requests. The source code is available here: https://github.com/lief-project (under Apache 2.0 license) and the associated website: http://lief.quarkslab.com If you are interested in use cases, you can take a look at these tutorials: * [Parse and manipulate formats](https://lief-project.github.io/doc/latest/tutorials/01_play_with_formats.html) * [Create a PE from scratch](https://lief-project.github.io/doc/latest/tutorials/02_pe_from_scratch.html) * [Play with ELF symbols](https://lief-project.github.io/doc/latest/tutorials/03_elf_change_symbols.html) * [Hooking](https://lief-project.github.io/doc/latest/tutorials/04_elf_hooking.html) * [Infecting the PLT/GOT](https://lief-project.github.io/doc/latest/tutorials/05_elf_infect_plt_got.html) The project will be presented at the [Third French Japanese Meeting on Cybersecurity](http://cyber.science-japon.org/) Contact ======= * lief [at] quarkslab [dot] com * Gitter: [lief-project](https://gitter.im/lief-project) Thanks ====== Thanks to Serge Guelton and Adrien Guinet for their advice about the design and their code review. Thanks to Quarkslab for making this project open-source. --- # HITB 2015 Write-up - Crypto 400 - Canonical: https://www.romainthomas.fr/post/15-11-hitb2015-crypto400/ - Markdown: https://www.romainthomas.fr/post/15-11-hitb2015-crypto400/index.md - Section: post - Published: 2015-11-03 - Modified: 2026-09-05 - Tags: write-up, cryptography > Write up ## Introduction The crypto 400 challenge deals with the Even Mansour cryptosystem. To validate this challenge we have to send the flag to a server. The server checks if the answer matches the flag by encrypting the flag and the given message with a **random** key. If both are equal the flag is correct otherwise it fails and the encrypted message is printed. ```python key = os.urandom(32) enc_flag = encrypt(flag, key) enc = encrypt(answer, key) if enc == enc_flag: response = "You lucky bastard, %s is indeed the correct flag!\n" % flag else: response = "Unfortunately that is not our flag :(\n" response += "Your guess encrypts as\n%s" % enc response += "whereas our flag encrypts as\n%s" % enc_flag ``` ## Even Mansour scheme In this Even-Mansour scheme, the block size is 16 bytes and the 32-byte key is split into two 16-byte values: $k_1$ and $k_2$. At first, the message $M$ is xor-ed with $k_1$ then $M \oplus k_1$ is going through a $F$ function which will be discussed later. Finally the output is xor-ed with $k_2$. ![Even–Mansour construction: M xor k1, permutation F, then xor k2 to produce C.](evenmansour.png) So we have: $$C = F(M \oplus k_1) \oplus k_2$$ ```python def EvenMansour(block, key): block = xor(block, key[:16]) block = F(block) block = xor(block, key[16:]) return block ``` In this challenge, the weakness comes from the $F$ function. ## $F$ function The $F$ function is composed of 64-rounds that perform the ``step(...)`` transformation: ```python def F(block): for i in range(64): block = step(block) return block ``` `step` uses an S-Box to transform the block in this way: ![White-box cryptography challenge steps](step.png) $$\begin{cases} \text{block}^{n+1}\_0 = \text{SBox}(\text{block}^{n}\_{10} \oplus \text{block}^{n}\_{12} \oplus \text{block}^{n}\_{13} \oplus \text{block}^{n+1}_{15}) & k = 0 \\\\\\ \text{block}^{n+1}\_k = \text{block}^{n}\_{k - 1} & k > 0 \end{cases}$$ $\text{block}^{n}_k$ is the byte $k$ of the *block* at round $n$ ($0 \leq k < 16$ and $0 \leq n < 64$) ```python def step(block): return chr( S[ ord(block[10]) ^ ord(block[12]) ^ ord(block[13]) ^ ord(block[15]) ] ) + block[:15] ``` By plotting $y = \text{S-Box}(x)$ we can notice that the S-Box has a special construction: ![White-box transformation overview](figure_1-1.png) I thought about computing the differential characteristics which is the probability that given the input difference $\Delta = x \oplus y$ we get the output delta: $\delta = S(x) \oplus S(y)$. We will call this probability $P(\Delta | \delta)$ and with following function, we can compute this probability: ```python def P(dx ,dy): count = 0; for x in range(len(SBox)): dY = SBox[x] ^ SBox[x ^ dx] if dY == dy: count += 1; return float(count) / float(256) ``` For all $\Delta$ and $\delta$, we notice that this probability is either 0 or 1. Consequently, if we know $\delta$ we are sure to find $\Delta$. This will be useful for the differential attack. ## Differential Attack To find the flag, we will perform a differential attack. We have a message $M_1$ that we know and we have an unknown second message $M_2$ that is the flag. We also know $C_1$ and $C_2$ such as: $$\begin{align} C\_1 & = & F(M\_1 \oplus k\_1) \oplus k\_2 \\\\\\ C\_2 & = & F(M\_2 \oplus k\_1) \oplus k\_2 \end{align}$$ ![Two Even–Mansour evaluations for M1 and M2, with intermediate values V1, V2, W1, and W2.](diff.png) By xor-ing $C_1$ and $C_2$ we can get $\Delta W = W_1 \oplus W_2 = C_1 \oplus C_2$. If somehow we can resolve $\Delta V$: $$\begin{align} \Delta V & = & V\_1 \oplus V\_2 \\\\\\ & = & M\_1 \oplus k\_1 \oplus M\_2 \oplus k\_1\\\\\\ & = & M\_1 \oplus M\_2. \end{align}$$ We can extract $M_2$ with: $$M_2 = \Delta V \oplus M_1$$ ## Recovering $\Delta V$ Now, let's see how to resolve $\Delta V$ from $\Delta W$. From the `step` function, we know that $\text{block}^{n+1}\_k = \text{block}^{n}\_{k - 1}$ therefore: $$\Delta W^{n-1}\_k = \Delta W^{n}\_{k + 1} \forall k < 15$$ We know also $\Delta W^{n-1}\_{0,1,2 \ldots 14}$ but not $\Delta W^{n-1}\_{15}$ To find $\Delta W^{n-1}\_{15}$ we will use the fact that $P(\Delta X | \Delta W^{n-1}\_{15}) = 1$ for a given $\Delta X$. Concretely, I built a table *diffTable* which maps $\delta$ to $\Delta$. \begin{align} \text{diffTable}(\Delta W^{n}\_{0}) & = & \Delta W^{n-1}\_{10} \oplus \Delta W^{n-1}\_{12} \oplus \Delta W^{n-1}\_{13} \oplus \Delta W^{n-1}\_{15} \\\\\\ & = & \Delta W^{n}\_{11} \oplus \Delta W^{n}\_{13} \oplus \Delta W^{n}\_{14} \oplus \Delta W^{n-1}\_{15}\\\\\\ \Delta W^{n-1}\_{15} & = & \text{diffTable}(\Delta W^{n}\_{0}) \oplus \Delta W^{n}\_{11} \oplus \Delta W^{n}\_{13} \oplus \Delta W^{n}\_{14} \end{align} Which enables to recover $\Delta W^{n - 1}$ from $\Delta W^{n}$. Then, with recursion we can compute $\Delta W^{0} = \Delta V$ ## Implementation The following script is the implementation of the attack: ```python #!/usr/bin/python2.7 # -*- coding: utf-8 -*- import os S = [ 0xa5,0xc6,0x62,0x01,0x49,0x2a,0x8e,0xed,0x1f,0x7c,0xd8,0xbb,0xf3,0x90,0x34,0x57, 0xb3,0xd0,0x74,0x17,0x5f,0x3c,0x98,0xfb,0x09,0x6a,0xce,0xad,0xe5,0x86,0x22,0x41, 0x89,0xea,0x4e,0x2d,0x65,0x06,0xa2,0xc1,0x33,0x50,0xf4,0x97,0xdf,0xbc,0x18,0x7b, 0x9f,0xfc,0x58,0x3b,0x73,0x10,0xb4,0xd7,0x25,0x46,0xe2,0x81,0xc9,0xaa,0x0e,0x6d, 0xfd,0x9e,0x3a,0x59,0x11,0x72,0xd6,0xb5,0x47,0x24,0x80,0xe3,0xab,0xc8,0x6c,0x0f, 0xeb,0x88,0x2c,0x4f,0x07,0x64,0xc0,0xa3,0x51,0x32,0x96,0xf5,0xbd,0xde,0x7a,0x19, 0xd1,0xb2,0x16,0x75,0x3d,0x5e,0xfa,0x99,0x6b,0x08,0xac,0xcf,0x87,0xe4,0x40,0x23, 0xc7,0xa4,0x00,0x63,0x2b,0x48,0xec,0x8f,0x7d,0x1e,0xba,0xd9,0x91,0xf2,0x56,0x35, 0x14,0x77,0xd3,0xb0,0xf8,0x9b,0x3f,0x5c,0xae,0xcd,0x69,0x0a,0x42,0x21,0x85,0xe6, 0x02,0x61,0xc5,0xa6,0xee,0x8d,0x29,0x4a,0xb8,0xdb,0x7f,0x1c,0x54,0x37,0x93,0xf0, 0x38,0x5b,0xff,0x9c,0xd4,0xb7,0x13,0x70,0x82,0xe1,0x45,0x26,0x6e,0x0d,0xa9,0xca, 0x2e,0x4d,0xe9,0x8a,0xc2,0xa1,0x05,0x66,0x94,0xf7,0x53,0x30,0x78,0x1b,0xbf,0xdc, 0x4c,0x2f,0x8b,0xe8,0xa0,0xc3,0x67,0x04,0xf6,0x95,0x31,0x52,0x1a,0x79,0xdd,0xbe, 0x5a,0x39,0x9d,0xfe,0xb6,0xd5,0x71,0x12,0xe0,0x83,0x27,0x44,0x0c,0x6f,0xcb,0xa8, 0x60,0x03,0xa7,0xc4,0x8c,0xef,0x4b,0x28,0xda,0xb9,0x1d,0x7e,0x36,0x55,0xf1,0x92, 0x76,0x15,0xb1,0xd2,0x9a,0xf9,0x5d,0x3e,0xcc,0xaf,0x0b,0x68,0x20,0x43,0xe7,0x84 ] def xor(block1, block2): return "".join( chr(ord(a) ^ ord(b)) for (a,b) in zip(block1, block2)) def step(block): return chr(S[ord(block[10]) ^ ord(block[12]) ^ ord(block[13]) ^ ord(block[15])]) + block[:15] def F(block): for i in xrange(64): block = step(block) return block def EvenMansour(block, key): block = xor(block, key[:16]) block = F(block) block = xor(block, key[16:]) return block def encrypt(data, key): data, num_blocks = pad(data) res = "" for i in xrange(num_blocks): block = EvenMansour(data[16*i:16*i+16], key) res += block return res def pad(data): while True: data += '\x00' if len(data) % 16 == 0: return data, len(data) / 16 # # Table T[a] = b such as # S[x] ^ S[y] = a and b = x ^ y # def DiffTable(S): table = [0 for i in range(len(S))] for delta in range(len(S)): for x in range(len(S)): dY = S[x] ^ S[x ^ delta] table[dY] = delta return table def main(): key = os.urandom(32) M1 = "hitb{0123456789abcdef}" M2 = "aaaaaaaaaaaaaaaaaaaaaa" C1 = encrypt(M1, key) C2 = encrypt(M2, key) numberOfBlocks = len(C2) / 16 diffTable = DiffTable(S) clearText = "" for block in range(numberOfBlocks): dW = xor(C1,C2)[16 * block : 16 * (block + 1)] for i in range(64): dWtemp = [dW[i + 1] for i in range(15)] delta = diffTable[ord(dW[0])] dW15 = chr(ord(dW[11]) ^ ord(dW[13]) ^ ord(dW[14]) ^ delta) dWtemp.append(dW15) dW = "".join(dWtemp) M = xor(dW, M2[16 * block : 16 * (block + 1)]) clearText += M print clearText if __name__ == '__main__': main() ``` ## Conclusion In fact by noticing that $$F(x \oplus y) = F(x) \oplus F(y) \oplus C^{te}$$ ```python for b in xrange(10): u = os.urandom(16); v = os.urandom(16); d = xor(F(xor(u,v)), xor(F(u), F(v))) print d.encode("hex") ``` We have: \begin{align} F(M_1 \oplus k_1) \oplus F(M_2 \oplus k_1) & = & F(M_1) \oplus F(M_2)\\ M_2 & = & F^{-1}(F(M_1) \oplus C_1 \oplus C_2) \end{align} Which is far easier to resolve. Thanks to **`jb^`**, who helped me find the previous technique. Sources are available [here](hitb2015-crypto400.tar.gz) --- # HITB 2015 Write-up - Crypto 300 - Canonical: https://www.romainthomas.fr/post/15-11-hitb2015-crypto300/ - Markdown: https://www.romainthomas.fr/post/15-11-hitb2015-crypto300/index.md - Section: post - Published: 2015-11-03 - Modified: 2026-09-05 - Tags: write-up, cryptography > Write-up for the Crypto 300 challenge ## Introduction The crypto 300 challenge was about RSA with a special generation of the prime numbers $p$ and $q$. We were given a mail [mail.msg](mail.msg) which has been encrypted with RSA and whose the public key is in the [hitbctf.crt](hitbctf.crt) certificate. ## RSA's Parameters Construction The modulus $N$ is built by choosing randomly a first prime number $p$, the second prime number $q$ is constructed in the following way: $$\alpha \cdot (p - 1) \equiv 1 \pmod{e}$$ $$q = (p\alpha \bmod e) + k\cdot e$$ $k$ is a positive integer such as $q$ is a prime number and $e$ is the public exponent which is also a random prime number. The following code is the implementation in Python: ```python def gen_rsa_parameters(): r = os.urandom(63) e = int(r.encode('hex'), 16) e = next_prime(e) r = os.urandom(64) p = int(r.encode('hex'), 16) p = next_prime(p) q = (p*modinv(p-1, e)%e) while not is_prime(q): q += e N = p*q phi = (p-1)*(q-1) d = modinv(e,phi) return N,e,d,p,q ``` ## Theoretical attack Let's $N^{\prime} = N \bmod{e}$. So we have \begin{align} N^{\prime} & \equiv & p \cdot q \pmod{e} \\\\\\ & \equiv & p \cdot ((p\alpha \bmod e) + k\cdot e) \pmod{e} \\\\\\ & \equiv & p^2\alpha \pmod{e} \\\\\\ \end{align} We have $\alpha$ in the equation so we can introduce $p - 1$ to remove $\alpha$ \begin{align} N^{\prime} & \equiv & (p - 1 + 1)^2\alpha \pmod{e} \\\\\\ & \equiv & (p - 1)^2\alpha + 2(p - 1)\alpha + \alpha \pmod{e} \\\\\\ & \equiv & (p - 1) + 2 + \alpha \pmod{e} \\\\\\ (p - 1)N^{\prime} & \equiv & (p - 1)^2 + 2(p - 1) + 1 \pmod{e} \end{align} $$\boxed{(p - 1)^2 - (N^{\prime} - 2)(p - 1) + 1 \equiv 0 \pmod{e}}$$ Now we have a quadratic equation which only depends on $p$. Let's $X = p - 1$ and suppose that $N^{\prime} - 2$ is even and $N^{\prime} - 2 = 2b$. \begin{align} X^2 - 2bX + 1 & \equiv & 0 & \pmod{e} \\\\\\ (X - b)^2 - b^2 + 1 & \equiv & 0 & \pmod{e}\\\\\\ (X - b)^2 & \equiv & b^2 - 1 & \pmod{e} \end{align} By using [quadratic residue](https://en.wikipedia.org/wiki/Quadratic_residue) we can find a solution. We can also use *SAGE* and the `sqrt()` function: ```python Np = N % e b = (Np - 2) / 2 p = Mod(pow(b, 2) - 1, e).sqrt() + b + 1 ``` At this point, we find $p \bmod{e}$ but not $p$ ! I tried to find $p$ by adding some $e$ but the *distance* between $p$ and $p \bmod{e}$ is huge. So I had to find another way. By knowing $p \bmod{e}$ we can compute $\alpha$. Remember $$\alpha \cdot (p - 1) \equiv 1 \pmod{e}$$ and by having $\alpha$ and $p \bmod{e}$ we can brute force $q$ by adding $e$ until $(p\alpha \bmod e) + k\cdot e$ is prime and it divide $N$. We did the assumption that $N^{\prime} - 2$ has to be even (so $N^{\prime}$ must be even) and in the certificate $N^{\prime}$ is even so everything is right. ## Practical Attack First we have to extract the modulus $N$ and the public key $e$ from the certificate: ```console $ openssl x509 -in hitbctf.crt -text -noout Certificate: Data: Version: 1 (0x0) Serial Number: 18379438180976429416 (0xff10e1a5ac5a0968) Signature Algorithm: sha1WithRSAEncryption Issuer: C=NL, ST=Noord-Holland, L=Amsterdam, O=HITB, OU=CTF Validity Not Before: May 24 09:58:26 2015 GMT Not After : May 23 09:58:26 2016 GMT Subject: C=NL, ST=Noord-Holland, L=Amsterdam, O=HITB, OU=CTF Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (1024 bit) Modulus: 00:e6:eb:89:c1:8d:49:c9:a2:02:2b:e0:b4:65:14: 6e:0f:90:45:1e:a3:4c:6b:60:56:00:4e:bd:15:59: 55:b1:35:96:c2:d6:83:ad:2f:23:6b:0b:2c:0e:0b: 88:83:b5:d6:cb:8a:0b:4f:f9:b7:eb:64:8c:95:2b: 6b:ef:5a:6f:04:f5:64:17:f5:1c:a9:14:d9:ea:73: e7:dd:c5:f2:0d:ce:c3:9c:e8:4b:72:2a:0c:f3:d8: 5e:80:ce:78:64:63:e1:44:f6:1d:b5:9c:cf:45:ff: 0e:d3:7f:d0:ce:bd:37:a5:8d:8a:4b:08:33:9e:a3: 2c:bc:ab:61:64:03:fd:2c:c5 Exponent: 69:60:2d:93:8a:81:5f:14:cf:9f:b8:36:c2:e0:4d: 4d:De:82:ba:fc:8d:56:c2:6d:8c:89:ef:3c:40:69: 5d:d5:d4:ef:a7:36:36:43:15:14:95:f3:8c:bf:24: ae:94:30:92:40:79:12:00:1b:17:f5:53:33:9e:92: 70:70:49 Signature Algorithm: sha1WithRSAEncryption 17:2b:ea:be:90:ad:98:f2:2b:ff:f5:61:d3:ea:af:fb:35:3a: 67:10:91:13:db:60:55:d9:09:8b:c2:1a:cf:6b:c6:1f:f2:10: 7a:d1:7b:9d:ff:10:f2:f2:c0:a9:f5:aa:2e:09:93:40:88:92: 7d:98:ff:e1:cb:dc:db:35:8d:e0:4b:21:99:76:bf:db:04:a2: 62:a4:18:4e:fc:bb:a7:53:be:6a:a1:ef:ec:15:86:c1:f1:1e: 87:6a:e9:af:fe:d1:08:eb:de:22:28:c4:5e:be:f1:41:0a:ca: cf:cf:da:63:b1:c1:56:e8:0c:8e:56:7f:08:94:0d:2b:2a:08: ``` `N = 1621575882314321757502664197090844942567381491984167028188381926885851` `995558397927547394115469298694885747314992315746872071523931715177680193273` `386465775883129725436206653605910592810579794603402792446164893148622893121` `957048204358672599654432857497196823273138934901636721473789115585263150131` `66594183212229` `e = 21558488234539889837938770635971330903489839146766895224490179041465516` `1931455822669631548838317075220811407344210520390992334648372016602816069805` `30249` With SAGE: ```python sage: Np = N % e sage: b = (Np - 2) / 2 sage: pp = int(Mod(pow(b, 2) - 1, e).sqrt()) + b + 1 sage: alpha = inverse_mod(int(X), int(e)) sage: q = (pp * alpha) % e sage: while not is_prime(q) and N % q != 0: ....: q += e sage: p = N / q sage: p 13317713478157317654574552532079837937895228108820477140030796245493222349714497856652987583926206280627498615972491072112647669795345566943409669535038641 sage: q 12176083266650126897170100375931110708350668494730113414987801764299563774952801449439933220072280766145748279998832962142839152786620322097065894585706069 ``` We can now generate the private key by using [rsatool](rsatool.py): ```bash $ ./rsatools.py -o private.pem \ -e 21558488234539889837938770635971330903489839146766895224490179041465516193145582266963154883831707522081140734421052039099233464837201660281606980530249 \ -p 13317713478157317654574552532079837937895228108820477140030796245493222349714497856652987583926206280627498615972491072112647669795345566943409669535038641 \ -q 12176083266650126897170100375931110708350668494730113414987801764299563774952801449439933220072280766145748279998832962142839152786620322097065894585706069 ``` Finally, we can decrypt the message: ```bash openssl smime -decrypt -in mail.msg -inkey private.pem hitb{0b21cc2025534dbd2965390d2bcef45d} ``` The sources are available [here](hitb2015-crypto300.tar.gz) --- # Code coverage using a dynamic symbolic execution - Canonical: https://www.romainthomas.fr/post/15-10-triton-code-coverage/ - Markdown: https://www.romainthomas.fr/post/15-10-triton-code-coverage/index.md - Section: post - Published: 2015-10-12 - Modified: 2026-09-05 > This blog post introduces code coverage with Triton ## Introduction Code coverage is mainly used in the vulnerability research area. The goal is to generate inputs which will reach different parts of the program's code. Then, if an input makes the program crash, we check if the crash can be exploited or not. A lot of methods exist to perform code coverage, such as random testing or mutation generation. In this short blog post, we focus on code coverage using dynamic symbolic execution (DSE) and explain why it is not a trivial task. Note that covering the code does not mean finding every possible bugs. Some bugs do not make the program crash and this [talk](https://shell-storm.org/talks/StHack2015_Dynamic_Behavior_Analysis_using_Binary_Instrumentation_Jonathan_Salwan.pdf) from slides 35 to 38 explains why. However, if we perform model checking associated with code coverage, it starts to get interesting =). ## Code coverage and DSE Note that unlike an SSE (static symbolic execution), a DSE is applied on a trace and can discover new branches only if these ones are reached during the execution. To go through another path, we must solve one of the last branch constraints discovered from the last trace. Then, we repeat this operation until all branches are taken. For example, let's assume a program $P$ which takes an input called $I$, where I may be a model $M$ or a random seed $R$. An execution is denoted $P(I)$ and returns a set of constraints $PC$. All $\varphi_{i}$ represent basic blocks and $\pi_i$ represent the branches constraint. A model $M_i$ is (at least) one valid solution of a constraint $\pi_i$. For example, $M_1 = Solution(\neg\pi_1 \land \pi_2)$. To discover all paths, we maintain a worklist denoted $W$ which is a set of $M$. At the first iteration, $I = R, W = \emptyset$ and $P(I) \rightarrow PC$. Then, $\forall \pi \in PC, W = W \cup \{ Solution(\pi) \}$ and we execute once again the program such that $\forall M \in W, P(M)$. When a model $M$ is injected in the program's input, it is deleted from the worklist $W$. Then, we repeat this operation until $W$ is empty. ![DSE Code Coverage](dse_coverage.png) Symbolic code coverage has pros and cons. It is useful when we work on an obfuscated binary because it can detect opaque predicates or unreachable code and repair a flattened graph. The main drawback occurs when expressions become too complex, causing an SMT solver timeout or high memory consumption. In the past, our largest symbolic expression consumed about 450 gigabytes of RAM before timing out. This scenario mainly occurs when we analyze large or obfuscated binaries which contain polynomial functions. Some of these cons may partially be fixed by optimizing symbolic expressions but this subject will be another story to come later :). ## Performing code coverage using Triton Since the version `v0.1 build 633` (commit [474fe2](https://github.com/JonathanSalwan/Triton/commit/474fe240e66ff6ab3e3501f8d7fc88ce1fcb3ef6)), Triton integrates everything we need to perform code coverage. These new features allow us to deal and compute the SMT2-Lib representation over an AST. In the rest of the blog post, we will focus on the design and the algorithm used to perform code coverage. ### Algorithm As an introduction (and to not turn our brain upside down), let's assume this following sample of code which comes from the samples directory. ```cpp char *serial = "\x31\x3e\x3d\x26\x31"; int check(char *ptr) { int i = 0; while (i < 5){ if (((ptr[i] - 1) ^ 0x55) != serial[i]) return 1; i++; } return 0; } ``` Basically, this function checks if the input is equal to `elite`, and returns `0` if it is true, otherwise it returns `1`. The control flow graph of this function is described below. It's an interesting first example, because to cover all basic blocks we need to find the good input. ![IDA control-flow graph of the check function, whose branches compare the input with elite.](check_bbs.png) We can see that only one variable can be controlled, the one located at the address `rbp+var_18` which refers to the `argv[1]`'s pointer. The goal is to reach all the basic blocks in the function check by computing the constraints and using the snapshot engine until every basic block is reached. For instance, the constraint to reach the basic block located at the address `0x4005C3` is `[rbp+var_4] > 4` but we do not control this variable directly. On the other hand, the jump at the address `0x4005B0` depends on the user input and this constraint can be solved by performing a symbolic execution. The algorithm which generalizes the previous idea is based on Microsoft's fuzzer algorithm ([SAGE](http://research.microsoft.com/en-us/um/people/pg/public_psfiles/ndss2008.pdf)) and the next diagram represents our check function with its constraints. The start and end nodes represent respectively the function's prologue (`0x40056D`) and the function's epilogue (`0x4005C8`). ![Control-flow graph labeled φ1–φ5 with branch constraints π0 and π1.](cc1.png) Before the first execution, we know nothing about branches' constraints. So, as explained in the previous chapter, we inject some random seeds to collect the first $PC$ and build our set $W$. The trace of the first execution $P(I)$ is represented by the basic blocks in blue. This execution gives us our first path constraint $P(I) \rightarrow (\pi_0 \land \neg \pi_1)$. ![First execution path through φ1, φ2, and φ5 highlighted after injecting an input seed.](cc2.png) Based on our first trace, we know that there are two branches ($\pi_0 \land \neg \pi_1$) discovered and so 2 others undiscovered. To reach the basic bloc $\varphi_3$, we compute the negation of the first branch constraint. If and only if the solution $Solution(\neg \pi_0)$ is SAT, we add the model to the worklist W. Same for $\varphi_4$ such that $W = W \cup {Solution(\pi_0 \land \neg(\neg \pi_1))}$. Once all solutions have been generated and models added to the worklist, we execute every models from the worklist. ![Successive worklist executions M0 and M1 covering the remaining paths to φ3 and φ4.](cc3.png) ### Implementation One condition to perform code coverage, is to predict the next instruction address when we are on a jump instruction. This condition is necessary to build the path constraint. We can not put a callback after a branch instruction because the `RIP` register has already changed. As Triton creates semantics expressions for all registers, the idea is to evaluate `RIP` when we are on a branch instruction. In a first time, we have developed an SMT evaluator to compute the `RIP` but we saw a little bit later that Pin provides `IARG_BRANCH_TARGET_ADDR` and `IARG_BRANCH_TAKEN` which can be used to know the next `RIP` values. With Pin, computing the next address is very easy, nevertheless the SMT evaluator was useful to [check instruction's semantics](https://github.com/JonathanSalwan/Triton/blob/c9648bb1b1f7d8a2afef0941ab267dc9387fd91c/tests/test_semantics.py). To perform the evaluation, we implemented the [visitor pattern](https://en.wikipedia.org/wiki/Visitor_pattern) to transform the SMT abstract syntax tree (AST) to a Z3 AST. This design can be used to transform our SMT AST into any others representations. The Z3 AST is easier to handle and can be evaluated or simplified with Z3 API. The transformation is performed by [src/smt2lib/z3AST.h](https://github.com/JonathanSalwan/Triton/blob/c9648bb1b1f7d8a2afef0941ab267dc9387fd91c/src/includes/Z3ast.h) and [src/smt2lib/z3AST.cpp](https://github.com/JonathanSalwan/Triton/blob/c9648bb1b1f7d8a2afef0941ab267dc9387fd91c/src/smt2lib/z3AST.cpp). ----- We will now explain how the code coverage's tool works. Let's assume that inputs come from command's line. Firstly, we have: ```python def run(inputSeed, entryPoint, exitPoint, whitelist = []): ... if __name__=='__main__': TritonExecution.run("bad !", 0x400480, 0x40061B, ["main", "check"]) # crackme_xor ``` At line 176, we define the input seed `bad !` which is the first program's argument (`argv[1]`). Then, we give the address from the beginning of the code coverage (**start block**) - it's at this address that we will take a snapshot. The third argument matches with the **end block** - it's at this address that we will restore the snapshot. Finally, we can set a whitelist to avoid specific functions like library's functions, cryptographic's function and so on. ```python def mainAnalysis(threadId): print "[+] In main" rdi = getRegValue(IDREF.REG.RDI) # argc rsi = getRegValue(IDREF.REG.RSI) # argv argv0_addr = getMemValue(rsi, IDREF.CPUSIZE.QWORD) # argv[0] pointer argv1_addr = getMemValue(rsi + 8, IDREF.CPUSIZE.QWORD) # argv[1] pointer print "[+] In main() we set :" od = OrderedDict(sorted(TritonExecution.input.dataAddr.items())) for k,v in od.iteritems(): print "\t[0x%x] = %x %c" % (k, v, v) setMemValue(k, IDREF.CPUSIZE.BYTE, v) convertMemToSymVar(k, IDREF.CPUSIZE.BYTE, "addr_%d" % k) for idx, byte in enumerate(TritonExecution.input.data): if argv1_addr + idx not in TritonExecution.input.dataAddr: # Not overwrite the previous setting print "\t[0x%x] = %x %c" % (argv1_addr + idx, ord(byte), ord(byte)) setMemValue(argv1_addr + idx, IDREF.CPUSIZE.BYTE, ord(byte)) convertMemToSymVar(argv1_addr + idx, IDREF.CPUSIZE.BYTE, "addr_%d" % idx) ``` The next code being executed is the `mainAnalysis` callback, we inject values to the inputs selected (line 148, 154) and we convert these inputs as symbolic variables (line 149, 155). All inputs selected are stored in a global variable called `TritonExecution.input`. Then, we can begin the code exploration. ```python if instruction.getAddress() == TritonExecution.entryPoint and not isSnapshotEnabled(): print "[+] Take Snapshot" takeSnapshot() return ``` When we are at the entry point, we take a snapshot in order to replay code exploration with a new input. ```python if instruction.isBranch() and instruction.getRoutineName() in TritonExecution.whitelist: addr1 = instruction.getAddress() + 2 # Address next to this one addr2 = instruction.getOperands()[0].getValue() # Address in the instruction condition # [PC id, address taken, address not taken] if instruction.isBranchTaken(): TritonExecution.myPC.append([ripId, addr2, addr1]) else: TritonExecution.myPC.append([ripId, addr1, addr2]) return ``` This test above checks if we are on a branch instruction like (`jnz, jle` ...) and if we are in a function which is in the *allowlist*. If so, we get the two possible addresses (`addr1` and `addr2`) and the effective address is computed by `isBranchTaken()` (line 69). Then, we store into the path constraint the `RIP` expression, the address taken and the address not taken (line 73–76). ```python if instruction.getAddress() == TritonExecution.exitPoint: print "[+] Exit point" # SAGE algorithm # http://research.microsoft.com/en-us/um/people/pg/public_psfiles/ndss2008.pdf for j in range(TritonExecution.input.bound, len(TritonExecution.myPC)): expr = [] for i in range(0,j): ripId = TritonExecution.myPC[i][0] symExp = getFullExpression(getSymExpr(ripId).getAst()) addr = TritonExecution.myPC[i][1] expr.append(smt2lib.smtAssert(smt2lib.equal(symExp, smt2lib.bv(addr, 64)))) ripId = TritonExecution.myPC[j][0] symExp = getFullExpression(getSymExpr(ripId).getAst()) addr = TritonExecution.myPC[j][2] expr.append(smt2lib.smtAssert(smt2lib.equal(symExp, smt2lib.bv(addr, 64)))) expr = smt2lib.compound(expr) model = getModel(expr) if len(model) > 0: newInput = TritonExecution.input newInput.setBound(j + 1) for k,v in model.items(): symVar = getSymVar(k) newInput.addDataAddress(symVar.getKindValue(), v) print newInput.dataAddr isPresent = False for inp in TritonExecution.worklist: if inp.dataAddr == newInput.dataAddr: isPresent = True break if not isPresent: TritonExecution.worklist.append(newInput) # If there is input to test in the worklist, we restore the snapshot if len(TritonExecution.worklist) > 0 and isSnapshotEnabled(): print "[+] Restore snapshot" restoreSnapshot() return ``` The last step happens when we are on the **exit point**. Lines 84 to 120 are the SAGE implementation. In few words, we browse the path constraints' list and for each **PC**, we try to get the model which satisfies the negation. If there is a valid model to reach the new target basic block, we add the model into the worklist. Once all models are inserted into the worklist, we restore the snapshot and we re-inject each model as input seed. The full code can be found [here](https://github.com/JonathanSalwan/Triton/blob/c9648bb1b1f7d8a2afef0941ab267dc9387fd91c/tools/code_coverage.py) and its execution on our example looks like this: ```bash $ ./triton ./tools/code_coverage.py ./samples/crackmes/crackme_xor abc [+] Take Snapshot [+] In main [+] In main() we set : [0x7ffd5ef8254d] = 62 b [0x7ffd5ef8254e] = 61 a [0x7ffd5ef8254f] = 64 d [0x7ffd5ef82550] = 20 [0x7ffd5ef82551] = 21 ! loose [+] Exit point {140726196774221: 101} [+] Restore snapshot [+] In main [+] In main() we set : [0x7ffd5ef8254d] = 65 e [0x7ffd5ef8254e] = 61 a [0x7ffd5ef8254f] = 64 d [0x7ffd5ef82550] = 20 [0x7ffd5ef82551] = 21 ! loose [+] Exit point {140726196774221: 101, 140726196774222: 108} [+] Restore snapshot [+] In main [+] In main() we set : [0x7ffd5ef8254d] = 65 e [0x7ffd5ef8254e] = 6c l [0x7ffd5ef8254f] = 64 d [0x7ffd5ef82550] = 20 [0x7ffd5ef82551] = 21 ! loose [+] Exit point {140726196774221: 101, 140726196774222: 108, 140726196774223: 105} [+] Restore snapshot [+] In main [+] In main() we set : [0x7ffd5ef8254d] = 65 e [0x7ffd5ef8254e] = 6c l [0x7ffd5ef8254f] = 69 i [0x7ffd5ef82550] = 20 [0x7ffd5ef82551] = 21 ! loose [+] Exit point {140726196774224: 116, 140726196774221: 101, 140726196774222: 108, 140726196774223: 105} [+] Restore snapshot [+] In main [+] In main() we set : [0x7ffd5ef8254d] = 65 e [0x7ffd5ef8254e] = 6c l [0x7ffd5ef8254f] = 69 i [0x7ffd5ef82550] = 74 t [0x7ffd5ef82551] = 21 ! loose [+] Exit point {140726196774224: 116, 140726196774225: 101, 140726196774221: 101, 140726196774222: 108, 140726196774223: 105} [+] Restore snapshot [+] In main [+] In main() we set : [0x7ffd5ef8254d] = 65 e [0x7ffd5ef8254e] = 6c l [0x7ffd5ef8254f] = 69 i [0x7ffd5ef82550] = 74 t [0x7ffd5ef82551] = 65 e Win [+] Exit point [+] Done ! ``` ### Further improvement Currently, the evaluator is quite slow and we loose a lot of time to evaluate expressions. One feature that should improve the evaluator speed is an SMT simplifier. We plan to develop a passes system (like LLVM) to simplify the SMT tree. The goal is to register some expressions transformation rules before sending expressions to the evaluator or the solver. For example, that's what [miasm2 already does](https://github.com/cea-sec/miasm/tree/7ee593d00488e75dadb6edad7ffe5a7dcf6b155d/miasm/expression). ![SMT tree flowing through an iterative simplifier to the evaluator or SAT solver.](./chain.svg) There are a lot of mini tricks to lighten symbolic expressions which are easy to implement and really beneficial. For example, the transformation of the expression `rax1 = (bvxor rax0 rax0) -> rax1 = (_ bv64 0)` will break the `rax`'s symbolic expression chain. ## Conclusion Although the code coverage using a symbolic resolution is a nice way to cover a code without guessing the inputs, it's clearly not a trivial task. The paths explosion implies the memory consumption and in several cases the expressions are too complex to be computed but this method remains truly effective on short parts of code. > **Note** To improve the symbolic coverage, it could be interesting to deal with bits-flip/random seeds when expressions are too complex or to deal with symbolic execution and abstract domains. --- # The Poor Man's Obfuscator - Canonical: https://www.romainthomas.fr/publication/22-pst-the-poor-mans-obfuscator/ - Markdown: https://www.romainthomas.fr/publication/22-pst-the-poor-mans-obfuscator/index.md - Section: publication - Published: 2022-07-04 - Modified: 2026-09-05 - Presented at: Pass The Salt > The purpose of this publication is to present ELF and Mach-O transformations which impact or hinder disassemblers like IDA, BinaryNinja, Ghidra, and Radare2. ## Slides [PDF document](slides.pdf) ## Whitepaper [PDF document](whitepaper.pdf) ## Video (English) [Video](https://1884242627.rsc.cdn77.org/resources/passthesalt/r1263fff27edevq0oj41ih40jkmedo/media_1440_zKpK3tD2km.mp4) ## Assets https://github.com/romainthomas/the-poor-mans-obfuscator --- # DroidGuard: A Deep Dive into SafetyNet - Canonical: https://www.romainthomas.fr/publication/22-sstic-blackhat-droidguard-safetynet/ - Markdown: https://www.romainthomas.fr/publication/22-sstic-blackhat-droidguard-safetynet/index.md - Section: publication - Published: 2022-05-12 - Modified: 2026-09-05 - Presented at: SSTIC & BlackHat Asia > SafetyNet is the Android component developed by Google to verify the devices' integrity. These checks are used by the developers to prevent running applications on devices that … ## Slides [PDF document](slides.pdf) ## Whitepaper [PDF document](whitepaper.pdf) ## Talk at [BlackHat](https://www.blackhat.com/asia-22/briefings/schedule/#droidguard-a-deep-dive-into-safetynet-25835) (English) [YouTube video](https://www.youtube.com/watch?v=zcFg0ZJ2E_A) ## Talk at [SSTIC](https://www.sstic.org/2022/presentation/droidguard_a_deep_dive_into_safetynet/) (French) [Video](https://static.sstic.org/videos2022/1080p/droidguard_a_deep_dive_into_safetynet.mp4) ## Material https://github.com/romainthomas/droidguard-samples --- # PGSharp: Analysis of a Cheat Engine on Android - Canonical: https://www.romainthomas.fr/publication/21-ekoparty-mobile-hacking-space-pgsharp/ - Markdown: https://www.romainthomas.fr/publication/21-ekoparty-mobile-hacking-space-pgsharp/index.md - Section: publication - Published: 2021-11-05 - Modified: 2026-08-04 - Presented at: Ekoparty > PGSharp is a cheating app for PokemonGO that works on non-rooted devices. This talk introduces its functionalities and the protections used to prevent reverse-engineering. ## Slides [PDF document](21-10-ekoparty-mobile-hacking-space-pgsharp.pdf) ## Talk [YouTube video](https://www.youtube.com/watch?v=gcMQgjgajPk)

## Blog Post [*PGSharp: Analysis of a Cheating App for PokemonGO*](/post/21-11-pgsharp-analysis) --- # QBDL: QuarksLab Dynamic Loader - Canonical: https://www.romainthomas.fr/publication/21-sstic-qbdl/ - Markdown: https://www.romainthomas.fr/publication/21-sstic-qbdl/index.md - Section: publication - Published: 2021-06-03 - Modified: 2026-08-04 - Presented at: SSTIC > The QuarkslaB Dynamic Loader (QBDL) is a modular, portable library for dynamically loading and linking binaries. ## Slides [PDF document](SSTIC2021-Slides-qbdl_quarkslab_dynamic_loader-guinet_thomas.pdf) ## Talk (In French) [Video](https://static.sstic.org/videos2021/1080p/qbdl_quarkslab_dynamic_loader.mp4) --- # Dynamic Binary Instrumentation Techniques to Address Native Code Obfuscation - Canonical: https://www.romainthomas.fr/publication/20-bh-asia-dbi/ - Markdown: https://www.romainthomas.fr/publication/20-bh-asia-dbi/index.md - Section: publication - Published: 2020-10-01 - Modified: 2026-08-04 - Presented at: BlackHat Asia > Android applications are becoming more and more obfuscated to prevent reverse engineering. While obfuscation can be applied on both, the Dalvik bytecode and the native code, the … ## Slides [PDF document](asia-20-Thomas-Dynamic-Binary-Instrumentation-Techniques-to-Address-Native-Code-Obfuscation.pdf) ## Whitepaper [PDF document](asia-20-Thomas-Dynamic-Binary-Instrumentation-Techniques-to-Address-Native-Code-Obfuscation-wp.pdf) ## Talk [YouTube video](https://www.youtube.com/watch?v=MRku-2fW42w)
## Demo #1: Snapchat [Video](snapchat_demo.mp4)
## Demo #2: Legu Packer [Video](qbdi-tencent-packer.mp4) *Note: The videos are intentionally quick. Do not hesitate to pause them.* --- # Android Runtime Restrictions Bypass - Canonical: https://www.romainthomas.fr/publication/android-restrictions-bypass/ - Markdown: https://www.romainthomas.fr/publication/android-restrictions-bypass/index.md - Section: publication - Published: 2019-03-23 - Modified: 2022-04-25 > This paper explains how to disable runtime restrictions without root privileges > **Note** This publication is also available on the [Quarkslab Blog](https://blog.quarkslab.com/android-runtime-restrictions-bypass.html). With the release of Android Nougat, Google introduced restriction about native libraries that can be loaded from an Android application. Basically, it prevents developers to link against some internal libraries such as ``libart.so``. Later on and with the release of Android Pie, they introduced a new restriction on the access to internal Java methods (or fields). Basically, these restrictions are used to prevent developers to access parts of the Android internal framework. Whereas these limitations aim to be used for compatibility purposes, this article shows how we can take advantage of Android internal to disable them. We briefly explain how these restrictions work and how to disable them from an application without **privileges**. The first part deals with the native library loading restriction while the second is about Java internal framework restriction. [PDF document](/publication/android-restrictions-bypass/report.pdf) --- # Static Instrumentation Based on Executable Formats - Canonical: https://www.romainthomas.fr/publication/static-instrumentation/ - Markdown: https://www.romainthomas.fr/publication/static-instrumentation/index.md - Section: publication - Published: 2018-06-20 - Modified: 2026-08-04 - Presented at: Recon Montréal & PST > Talk given at Recon Montréal and PassTheSalt18 about static instrumentation and its use cases. Many instrumentation techniques are based on modifying code or system environment of the target. It can be suitable for scenarios but it could not work under certain circumstance (integrity checking, non-rooted environment...) In this talk we propose similar techniques by only modifying the executable format. This enables to be architecture independent, injection, and hooking does not require privileged environment. [PDF document](18-06-Recon18-Formats-Instrumentation.pdf) --- # LIEF: Library to Instrument Executable Formats - Canonical: https://www.romainthomas.fr/publication/lief/ - Markdown: https://www.romainthomas.fr/publication/lief/index.md - Section: publication - Published: 2017-07-04 - Modified: 2026-08-04 - Presented at: RMLL & Cybersecurity France-Japan > When analyzing an executable, the first layer of information is the format in which the executable is wrapped. Many tools and libraries can analyze and instrument machine code … When analyzing an executable, the first layer of information is the format in which the executable is wrapped. Many tools and libraries can analyze and instrument machine code wrapped by one format. However, no library handled all three mainstream executable formats while supporting both reading and modification. LIEF was developed to that end. In the talk, we explain the rationale behind LIEF's architecture choices, what LIEF allows us to do, and several use cases. LIEF is a cross-platform library and it can be used through a Python, C++ and C API. The library enables to parse standard structures as well as more complex ones like PE Signature (Authenticode) and ELF hash table. As use cases we can inject code into a binary or a library, we can also redirect the control flow to hook functions and it can be used to obfuscate some parts of a binary. Another feature of LIEF is that common characteristics of these formats are factorized so that we can develop a single script which works for the three formats. ![LIEF](lief.webp) ## Slides of the talk given at RMLL [PDF document](17-07-RMLL-LIEF.pdf) You can also grab the slides of the talk given at [Cybersecurity France-Japan](https://project.inria.fr/FranceJapanICST/wokshops/2017-program/presentations/): [17-04-cybersecurity-frjp-LIEF.pdf](17-04-cybersecurity-frjp-LIEF.pdf) --- # How Triton can help to reverse virtual machine based software protections - Canonical: https://www.romainthomas.fr/publication/triton-vm-protection/ - Markdown: https://www.romainthomas.fr/publication/triton-vm-protection/index.md - Section: publication - Published: 2016-11-10 - Modified: 2022-05-14 - Presented at: CSAW SOS > The first part of the talk is going to be an introduction to the Triton framework to expose its components and to explain how they work together. Then, the second part will include … ## Slides [PDF document](csaw2016-sos-rthomas-jsalwan.pdf) ## Video [Video](how-triton-can-help-to-reverse-virtual-machine-based-software-protections.mp4) --- # Dynamic Binary Analysis and Obfuscated Codes - Canonical: https://www.romainthomas.fr/publication/dynamic-binary-analysis-and-obfuscation/ - Markdown: https://www.romainthomas.fr/publication/dynamic-binary-analysis-and-obfuscation/index.md - Section: publication - Published: 2016-04-08 - Modified: 2026-08-04 - Presented at: St'Hack > This presentation explains how dynamic binary analysis (DBA) can help reverse engineers understand obfuscated code. We introduce basic obfuscation techniques and demonstrate how … [PDF document](sthack2016-rthomas-jsalwan.pdf) --- # How Triton may help analyze obfuscated binaries - Canonical: https://www.romainthomas.fr/publication/triton/ - Markdown: https://www.romainthomas.fr/publication/triton/index.md - Section: publication - Published: 2015-09-01 - Modified: 2026-08-04 > Binary obfuscation protects software intellectual property by transforming a binary while preserving its semantics. It preserves the original information among irrelevant data to … [PDF document](misc82-triton.pdf) --- # Symbi - Canonical: https://www.romainthomas.fr/project/symbi/ - Markdown: https://www.romainthomas.fr/project/symbi/index.md - Section: project - Published: 2023-01-01 - Modified: 2026-08-04 - Tags: symbolization, tracing, dynamir > A dynamic trace symbolizer built on DynaMIR. Symbi builds on DynaMIR to turn dynamic instruction traces into symbol-aware execution data. It is the analysis-facing layer between low-level runtime events and the higher-level context consumed by automated reverse-engineering tools. --- # MCStone - Canonical: https://www.romainthomas.fr/project/mcstone/ - Markdown: https://www.romainthomas.fr/project/mcstone/index.md - Section: project - Published: 2023-01-01 - Modified: 2026-09-05 - Tags: LLVM MC, assembler, disassembler > A clean, high-performance assembler and disassembler built on LLVM's MC layer for engineering and reverse-engineering workflows. MCStone is an assembler and disassembler built directly on LLVM's MC layer. It exposes a modern C++ API to disassemble streams of bytes and to assemble text listings. It ships instruction and operand models for x86, x86-64, AArch64, RISC-V, MIPS, PowerPC, and eBPF, plus ARM and Thumb at the instruction level. LLVM provides a strong and reliable engine to assemble and disassemble code for many architectures. Nevertheless, its `llvm::MCDisassembler` API is not easy to interface with: it requires instantiating a bunch of other `llvm::MC*` objects first, each with its own lifetime and configuration: ```cpp #include std::unique_ptr MSI{target->createMCSubtargetInfo( /*TheTriple=*/*triple, /*CPU=*/details.cpu, /*Features=*/SF.getString() )}; auto MC = std::make_unique( *triple, MAI.get(), MRI.get(), MSI.get(), SrcMgr.get(), MTO.get()); std::unique_ptr MD{target->createMCDisassembler(*MSI, *MC)}; ``` The purpose of MCStone is to absorb all this boilerplate and to keep the LLVM implementation **private** to the library, while exposing a user-friendly API that covers most reverse-engineering and binary analysis use cases. ## Disassembly: Streams and Iterators The cross-architecture decoding and encoding logic itself comes from LLVM and its TableGen definitions. MCStone does not reimplement it. From an API perspective, `llvm::MCDisassembler` takes a sized buffer of bytes as input and emits a `llvm::MCInst`. For instance, `cmp byte ptr [rip + 4901546], 0` is decoded as follows: ```text # encoding: [0x80,0x3d,0xaa,0xca,0x4a,0x00,0x00] cmp byte ptr [rip + 4901546], 0 # # # # # # > ``` MCStone uses this low-level API as a primitive and builds two facilities on top of it: 1. Streams 2. Lazy iterators ### Streams `llvm::MCDisassembler` takes a sized buffer as input. In many reverse-engineering situations, we don't (exactly) know the boundaries of the bytes we want to disassemble. This happens, for instance, when disassembling a function directly from memory: ```cpp auto anti_hook_ptr = imagebase + 0x40090; auto instructions = engine->disassemble(anti_hook_ptr); ``` To address this practical need, the MCStone disassembly API abstracts its input behind a stream: ```cpp namespace mcstone { class Engine { public: // [...] // Disassemble a non-owning stream instructions_it disassemble(Stream& stream, uint64_t addr); // Disassemble the given stream and take its ownership instructions_it disassemble(std::unique_ptr stream, uint64_t addr); // Disassemble the memory of the current process, starting at addr instructions_it disassemble(uint64_t addr); }; } ``` A `Stream` can be bounded by a size, but the user is also free to create an unbounded stream when this size is unknown. This is what the address-only overload does with a `MemoryStream`: bytes are read on demand from the process memory. > **Note** The MCStone `Stream` interface is close to [LIEF](https://lief.re)'s `LIEF::BinaryStream`. A custom stream, for instance one reading the memory of a remote process, only needs to implement `peek_in()` and `size()`. Regular functions that take a pointer and a size, or a `std::vector`, are also exposed. They wrap their input in a non-owning `RefStream`: ```cpp {linenos=inline hl_lines=[15,17]} namespace mcstone { class Engine { public: // [...] // Disassemble a non-owning stream instructions_it disassemble(Stream& stream, uint64_t addr); // Disassemble the given stream and take its ownership instructions_it disassemble(std::unique_ptr stream, uint64_t addr); // Disassemble the memory of the current process, starting at addr instructions_it disassemble(uint64_t addr); instructions_it disassemble(const uint8_t* buffer, size_t size, uint64_t addr); instructions_it disassemble(std::vector bytes, uint64_t addr); }; } ``` ### Lazy iterators MCStone disassembles streams by returning an iterator range: `instructions_it`. Calling `disassemble(...)` positions the iterator on the first instruction and stops there. Nothing else is decoded until the user advances it. ```cpp {linenos=false hl_lines=[11,12]} #include using namespace mcstone; std::vector get_bytes(); int main() { auto engine = Engine::create(Engine::Platform::LINUX, Engine::Architecture::X86_64); // Only the first instruction has been decoded at this point auto instructions = engine->disassemble(get_bytes(), /*address=*/0x1000); return 0; } ``` Therefore, the user **only** pays the memory and runtime cost of the instructions that are **consumed**. This matters when the goal is to find a specific set of instructions (e.g. syscalls) within a multi-gigabyte buffer, or to stop at the first `ret` of a function whose size is unknown. As mentioned in the introduction, `llvm::MCDisassembler` emits `llvm::MCInst` objects to represent the decoded instructions. This representation is not user-friendly from an instruction-analysis perspective. So in addition to lazily decoding the stream, the iterator yields a `mcstone::Instruction` that is specialized for each supported architecture: ```cpp {linenos=false hl_lines=["13-15"]} #include using namespace mcstone; std::vector get_bytes(); int main() { auto engine = Engine::create(Engine::Platform::LINUX, Engine::Architecture::X86_64); auto instructions = engine->disassemble(get_bytes(), /*address=*/0x1000); for (const mcstone::Instruction& inst : instructions) { std::println("{} | {}", inst, inst.is_syscall()); } return 0; } ``` > **Note** `mcstone::Instruction` combines the TableGen instruction properties exposed by `llvm::MCInstrDesc` with opcode-based predicates such as `is_syscall()`: ```cpp {linenos=false} class Instruction { // [...] virtual bool is_call() const; virtual bool is_syscall() const; virtual bool is_trap() const; virtual bool is_barrier() const; virtual bool is_return() const; virtual std::optional branch_target() const; // [...] }; ``` ### Operands Given a handle on an `mcstone::Instruction`, the natural next step is to inspect its operands. `llvm::MCInst` exposes **raw** operands through `llvm::MCOperand`, but these operands do not carry the original semantics. For instance, the `MCInst` representation of `ldrb w0, [x1, x2]` has these operands: ```text ldrb w0, [x1, x2] // encoding: [0x20,0x68,0x62,0x38] // // // // // > ``` The memory operand `[x1, x2]` is scattered across the last four operands, and none of them describes the structure of the memory address. MCStone fills this gap with a lazily evaluated iterator over the operands that reconstructs their higher-level semantics: ```cpp {linenos=false hl_lines=["8-19"]} std::unique_ptr I = get_inst("ldrb w0, [x1, x2]"); const auto& arm64 = *I->cast(); std::println("{}", arm64); for (size_t idx = 0; const auto& op : arm64.operands()) { std::println(" [{}]: {}", ++idx, *op); if (const auto* mem = op->as()) { std::println("Base: {}", get_register_name(mem->base())); std::visit(overloaded{ [](int64_t offset) { std::println("Offset (imm): {}", offset); }, [](mcstone::aarch64::REG reg) { std::println("Offset (reg): {}", get_register_name(reg)); }, [](std::monostate) {}, }, mem->offset()); } } ``` This code generates the following output: ```text 0x000000: ldrb w0, [x1, x2] [1]: W0 [2]: [x1, x2] Base: X1 Offset (reg): X2 ``` `arm64.operands()` yields only two operands. The second one is an `mcstone::aarch64::Memory` instance that exposes the base register, the offset (an immediate or a register), and the shift or extension of the memory access. This semantic-aware operand iterator is what we need to analyze instruction operands accurately. MCStone still relies on the original `llvm::MCOperand` values, but with an extra layer of processing on top. Please note that this operand processing is **only** performed when the `operands()` iterator is consumed. Users don't pay this cost unless they iterate over it. ### Ranges and Views Beyond the performance aspect, lazily evaluated iterators have an interesting property when combined with C++ ranges. Users can create a view of what they **intend** to observe: ```cpp auto engine = mcstone::Engine::from_process(); auto insts = engine->disassemble((uintptr_t)&sensitive_function); auto insts_view = insts | std::views::take_while([](const auto& I) { return !I->is_return(); }) | std::views::transform([](const auto& I) { return I->to_string(); }); ``` This `insts_view` is just a _view_ of the instructions before the first `ret`, transformed into their textual representation. To **effectively** disassemble and observe these instructions, we must iterate over the view: ```cpp for (const std::string& inst_str : insts_view) { std::println("{}", inst_str); } ``` ```bash $ ./run 0x7f042cdbec60: push rax 0x7f042cdbec61: mov qword ptr [rsp], rdi 0x7f042cdbec65: inc qword ptr [rip + 0xfaec] 0x7f042cdbec6c: mov rax, qword ptr [rip + 0xe695] 0x7f042cdbec73: mov qword ptr [rax], rdi 0x7f042cdbec76: mov rax, qword ptr [rip + 0xe693] 0x7f042cdbec7d: mov rdi, qword ptr [rax] 0x7f042cdbec80: lea rdx, [rip - 0x7020] 0x7f042cdbec87: mov rcx, rsp 0x7f042cdbec8a: mov esi, 0x9 0x7f042cdbec8f: call 0xcc0c 0x7f042cdbec94: pop rax ``` The interesting aspect of this view is that it can be observed again later. The range reads the process memory each time it is traversed. So if the underlying code uses some kind of polymorphic protection, or if an attacker hooks the function, the **same** code observes the change: ```cpp {linenos=false hl_lines=["13-24"]} auto engine = mcstone::Engine::from_process(); auto insts = engine->disassemble((uintptr_t)&sensitive_function); auto insts_view = insts | std::views::take_while([](const auto& I) { return !I->is_return(); }) | std::views::transform([](const auto& I) { return I->to_string(); }); std::println("Before hooking:"); for (const std::string& inst_str : insts_view) { std::println("{}", inst_str); } hooky.hook((uintptr_t)&sensitive_function, [] (hooky::Engine&, hooky::HookingContext& ctx, bool is_enter) { if (is_enter) { std::println("Who: {}", (const char*)ctx.cpu.rdi); } }); std::println("After hooking:"); for (const std::string& inst_str : insts_view) { std::println("{}", inst_str); } ``` ```bash $ ./run Before hooking: 0x7f042cdbec60: push rax 0x7f042cdbec61: mov qword ptr [rsp], rdi 0x7f042cdbec65: inc qword ptr [rip + 0xfaec] 0x7f042cdbec6c: mov rax, qword ptr [rip + 0xe695] 0x7f042cdbec73: mov qword ptr [rax], rdi 0x7f042cdbec76: mov rax, qword ptr [rip + 0xe693] 0x7f042cdbec7d: mov rdi, qword ptr [rax] 0x7f042cdbec80: lea rdx, [rip - 0x7020] 0x7f042cdbec87: mov rcx, rsp 0x7f042cdbec8a: mov esi, 0x9 0x7f042cdbec8f: call 0xcc0c 0x7f042cdbec94: pop rax After hooking: 0x7f042cdbec60: jmp qword ptr [rip] 0x7f042cdbec66: sbb al, 0x60 0x7f042cdbec68: shr byte ptr [rsp + rax], cl 0x7f042cdbec6b: jg 0x0 0x7f042cdbec6d: add byte ptr [rax - 0x6f6f6f70], dl 0x7f042cdbec73: nop 0x7f042cdbec74: nop [...] ``` Same **view**, different **runtime** observation. ## Assembly The same `Engine` also assembles. It takes a text listing and returns the encoded bytes, which makes round trips between text and machine code simple: ```cpp auto engine = Engine::create(Engine::Platform::LINUX, Engine::Architecture::ARM64); std::vector raw = engine->assemble(R"asm( mov x16, x0; mov x17, x1; )asm"); for (const auto& inst : engine->disassemble(raw, /*addr=*/0)) { std::println("{}", *inst); } ``` On x86, the assembler supports both the Intel and AT&T dialects. Symbols are resolved through an `AssemblerConfig` callback, and an existing `llvm::MCInst` can be re-encoded directly. This last point is what allows, for instance, `x86::Instruction::lock()` to return a copy of an instruction re-encoded with a `LOCK` prefix. ![MCStone Overview](./overview.webp) ## MCStone compared to Capstone & Zydis MCStone shares the same foundation as [Capstone](https://www.capstone-engine.org/): both decode instructions with the tables that LLVM generates from its TableGen definitions. Capstone uses its own generated copy of these tables for each architecture, while MCStone links against upstream LLVM (23.1.0 at the time of writing). New instruction set extensions, decoder fixes, and the assembler side all land with each LLVM release. [Zydis](https://zydis.re/) is a different design: a hand-written, dependency-free x86 and x86-64 decoder, and the throughput reference of this benchmark. The numbers below come from the MCStone benchmark. Every engine uses the same corpus: the first `16 MiB` of the `.text` section of `libLLVM.so.22.1`, about 3.7 million x86-64 instructions. The three libraries are built by the same clang 24 (LLVM `main` branch) at `-O3`. MCStone is on `LLVM 23.1.0`, Capstone `6.0.0-Alpha10`, and the Zydis `v5` development branch. In the tables below, an empty cell means that the benchmark has no equivalent measurement for that engine. ### Throughput The engines do not perform the same amount of work in their default call, so the rows are grouped by the API level they exercise. Throughput is in MiB/s, higher is better: | API level | Zydis | MCStone | Capstone | |---|---:|---:|---:| | Decode only | 110.0 | 84.9 | | | Instruction objects | | 53.0 | | | Instruction objects and operands | 66.9 | 40.2 | 24.5 [^capstone] | | Formatted text | 40.6 | 16.5 | 30.3 | - Zydis leads at every level. - The MCStone decode-only row is its raw `llvm::MCInst` iterator, without `mcstone::Instruction` objects. - With operands, which is what analysis code needs, MCStone is 1.6 times faster than Capstone's detail mode. - Text formatting is the weak point of MCStone: Capstone is 1.8 times faster and Zydis 2.5 times faster. This path goes through LLVM's `MCInstPrinter`. [^capstone]: Capstone always formats the text, so its detail mode does more work than the two operand-only cells. It has no public decode-only path either. ### Initialization and Memory | | MCStone | Capstone | Zydis | |---|---:|---:|---:| | Engine initialization (ms) | 0.194 | 0.060 | 0.002 | | Retained instruction without operands (bytes) | 270 | 260 | | | Retained instruction with operands (bytes) | 270 [^mcstone-operand] | 2,484 | 1,161 | | Peak RSS, full disassembly (MiB) | 133.2 | 132.1 | 130.3 | - `Engine::create()` registers the LLVM targets and builds the MC context, the disassembler, the printer, the assembler, and the target machine. It is slower than the others to start but stays below 0.2 milliseconds. - Streaming keeps O(1) decoder state for all three engines: the peak memory on the full `92.7 MiB` corpus is the process base plus the corpus itself. - A retained `mcstone::Instruction` costs about 270 bytes, close to a Capstone instruction without details. It is 4 to 9 times smaller than the full-detail records of Capstone and Zydis. [^mcstone-operand]: There is no separate detailed record: the operands are derived on demand from the retained instruction. ### What it means MCStone does not try to beat a hand-written x86 decoder. Its value lies in the combination: LLVM decoding **and** encoding for many architectures, a modern C++ API with streams, lazy iterators, and semantic operands. The performances sits between Capstone and Zydis for analysis workloads. ## Integration and public API MCStone is a core component of my reverse-engineering workflow, and it powers other private components such as [Hooky](/project/hooky) and [DynaMIR](/project/dynamir). The public-facing API is available in [LIEF Extended](https://extended.lief.re), which also provides Python and Rust bindings. You can find the documentation on these pages: - [Disassembler](https://lief.re/doc/latest/extended/disassembler/index.html) - [Assembler](https://lief.re/doc/latest/extended/assembler/index.html) --- # Lypid - Canonical: https://www.romainthomas.fr/project/lypid/ - Markdown: https://www.romainthomas.fr/project/lypid/index.md - Section: project - Published: 2023-01-01 - Modified: 2026-08-04 - Tags: DWARF, PDB, debug information > A user-friendly library for inspecting and generating DWARF and PDB debug information. Lypid provides a modern C++ API for navigating DWARF and PDB debug information: compilation units, functions, variables, types, public symbols, and source-line data can all be inspected through a consistent interface. Beyond parsing, it can reconstruct source-level declarations and generate debug information for ELF, PE, and Mach-O targets. This makes DWARF and PDB a programmable building block for LIEF Extended and higher-level binary analysis workflows. --- # iCDump - Canonical: https://www.romainthomas.fr/project/icdump/ - Markdown: https://www.romainthomas.fr/project/icdump/index.md - Section: project - Published: 2023-01-01 - Modified: 2026-09-05 - Tags: Objective-C, Mach-O, LIEF Extended > A modern, cross-platform Objective-C class dump that reconstructs declarations from Mach-O metadata with LIEF and LLVM. iCDump parses Objective-C metadata from Mach-O binaries and reconstructs classes, protocols, categories, properties, instance variables, and methods as readable declarations. It runs independently of the Apple ecosystem and uses LIEF for binary access and LLVM for declaration output. The project is now closed source and its Objective-C engine is integrated into LIEF Extended. The original public release is documented in [iCDump: A Modern Objective-C Class Dump](/post/23-01-icdump). --- # Hooky - Canonical: https://www.romainthomas.fr/project/hooky/ - Markdown: https://www.romainthomas.fr/project/hooky/index.md - Section: project - Published: 2023-01-01 - Modified: 2026-08-04 - Tags: hooking, x86-64, ARM64, RISC-V64 > A modern C++ hooking framework for x86-64, ARM64, and RISC-V64, with cross-platform detours and function replacement. Hooky is a cross-platform, cross-architecture function-hooking framework with a modern C++ API. Hooks can observe full CPU context on function entry and exit, replace functions, and call generated trampolines while retaining explicit control over hook state. It targets x86-64, ARM64, and RISC-V64 and fills a role similar to Frida's Interceptor and Dobby. Internally, Hooky composes with LIEF Runtime, MCStone, and LLVM into a portable detour engine. --- # DynaMIR - Canonical: https://www.romainthomas.fr/project/dynamir/ - Markdown: https://www.romainthomas.fr/project/dynamir/index.md - Section: project - Published: 2023-01-01 - Modified: 2026-08-04 - Tags: DBI, MLIR, x86-64, ARM64, RISC-V64 > A modern dynamic binary instrumentation engine for x86-64, ARM64, and RISC-V64, built around a custom MLIR-based IR. DynaMIR is a dynamic binary instrumentation engine for x86-64, ARM64, and RISC-V64. It lifts machine code into a custom MLIR-based intermediate representation, applies instrumentation, and lowers the result back into executable instructions. Conceptually similar to QBDI, DynaMIR is designed around a modern, composable IR and integrates directly with MCStone and LIEF. It provides the execution layer for tracing, program analysis, and trace-based workflows. --- # CLayout - Canonical: https://www.romainthomas.fr/project/clayout/ - Markdown: https://www.romainthomas.fr/project/clayout/index.md - Section: project - Published: 2023-01-01 - Modified: 2026-08-04 - Tags: Clang, C++, type layout > A Clang-powered analyzer for C and C++ layouts that resolves target-specific records, field offsets, sizes, methods, and types. CLayout parses C and C++ declarations into a queryable description of their target ABI layout. Backed by Clang, it resolves structures, enums, field types and offsets, record sizes, methods, constructors, and return types for the exact compiler arguments and target supplied by the caller. Its C++ and Python APIs make foreign interfaces and platform headers easier to inspect programmatically, including target-specific layouts such as Android NDK types for AArch64. --- # BinLift - Canonical: https://www.romainthomas.fr/project/binlift/ - Markdown: https://www.romainthomas.fr/project/binlift/index.md - Section: project - Published: 2023-01-01 - Modified: 2026-08-04 - Tags: binary lifting, QBDL, QBDI, DWARF > A user-friendly binary lifter built on QBDL, with DWARF-aware types and native or instrumented function calls. BinLift combines QBDL's portable binary loading with DynaMIR instrumentation, DWARF-backed types, and modern C++ and Python APIs. It provides a high-level way to resolve functions and variables, translate addresses, access memory, and call native or instrumented code without rebuilding the surrounding runtime by hand. This tool was used to break DexProtector; the resulting analysis is documented in [A Glimpse Into DexProtector](/post/26-01-dexprotector). --- # Open-Obfuscator - Canonical: https://www.romainthomas.fr/project/open-obfuscator/ - Markdown: https://www.romainthomas.fr/project/open-obfuscator/index.md - Section: project - Published: 2022-10-31 - Modified: 2022-11-24 - Tags: obfuscation, reverse-engineering > A free and open-source obfuscator for mobile applications Open-obfuscator is an open-source project composed of [o-mvll](https://obfuscator.re/omvll) and [dProtect](https://obfuscator.re/dprotect). The purpose of this project is to provide an obfuscation playground for developers and reverse engineers as described in this blog post: [*Open-Obfuscator: A free and open-source obfuscator for mobile applications*](/post/22-10-open-obfuscator). --- # QBDL - Canonical: https://www.romainthomas.fr/project/qbdl/ - Markdown: https://www.romainthomas.fr/project/qbdl/index.md - Section: project - Published: 2021-06-03 - Modified: 2026-08-04 - Tags: lief, loader > QuarkslaB Dynamic Loader: Generic loader for ELF, PE, and Mach-O QBDL (**Q**uarksla**B** **D**ynamic **L**oader) is a cross-platform library that enables to load ELF, PE, and Mach-O binaries with an abstraction on the targeted system. It abstracts the memory model on which the binary is loaded and provides an enhanced API to the user to process symbols resolution. In a nutshell, it enables to load binaries on foreign systems without reinventing the wheel. Here is an [example](https://github.com/quarkslab/QBDL/blob/83a64211dae71e870495bc795dd065278f93993f/bindings/python/examples/triton_macho_x64.py) to load a Mach-O in [Triton](https://github.com/JonathanSalwan/Triton) with QBDL: ```python class TritonVM(pyqbdl.TargetMemory): def __init__(self, ctx: TritonContext): super().__init__() self.ctx = ctx def mmap(self, ptr, size): return ptr def mprotect(self, ptr, size, access): return True def write(self, ptr, data): self.ctx.setConcreteMemoryAreaValue(ptr, bytes(data)) def read(self, ptr, size): return self.ctx.getConcreteMemoryAreaValue(ptr, size) class TritonSystem(pyqbdl.TargetSystem): def __init__(self, arch, ctx): super().__init__(TritonVM(ctx)) self.arch = arch self.ctx = ctx def symlink(self, loader, sym): for name, impl, addr in externalFunctions: if sym.name == name: return addr return 0 def supports(self, bin_): return pyqbdl.Arch.from_bin(bin_) == self.arch def base_address_hint(self, bin_ba, vsize): return bin_ba x86_64_arch = pyqbdl.Arch(lief.ARCHITECTURES.X86, lief.ENDIANNESS.LITTLE, True) loader = pyqbdl.loaders.MachO.from_file(args.filename, x86_64_arch, TritonSystem(x86_64_arch, ctx), pyqbdl.Loader.BIND.NOW) ``` --- # Tencent Legu Unpacker - Canonical: https://www.romainthomas.fr/project/legu_unpacker/ - Markdown: https://www.romainthomas.fr/project/legu_unpacker/index.md - Section: project - Published: 2020-01-23 - Modified: 2026-08-04 - Tags: android > Scripts to unpack Android applications protected by Tencent Legu # Legu Unpacker Scripts to unpack Android applications protected by Tencent Legu. It only works with versions **4.1.0.15** and **4.1.0.18** of Legu. Blog post: /post/a-glimpse-into-tencents-legu-packer ## Overview The original DEX files are located in ``assets/0OO00l111l1l`` with the following layout:

![Layout of the packed Tencent Legu DEX files](packed_file.png)

One can find the details of this structure in the Kaitai file: [legu_packed_file.ks](https://github.com/quarkslab/legu_unpacker_2019/tree/7b4aec6d223dfb24a60bb99151b15d0df76ea6fa/legu_packed_file.ksy) The *hashmap* embedded in the second part is described in the [legu_hashmap.ks](https://github.com/quarkslab/legu_unpacker_2019/tree/7b4aec6d223dfb24a60bb99151b15d0df76ea6fa/) file:

![Structure of the Legu embedded hashmap](hashmap.png)

## pylegu [pylegu](https://github.com/quarkslab/legu_unpacker_2019/tree/7b4aec6d223dfb24a60bb99151b15d0df76ea6fa/pylegu) contains the Python bindings to decrypt and uncompress the data embedded in ``assets/0OO00l111l1l``. To compile and install ``pylegu``: ```bash $ cd pylegu $ python3.7 ./setup.py build -j4 install --user $ python -c "import pylegu" ``` One could also use [jap/pyucl](https://github.com/jap/pyucl) to decompress the data and [aguinet/dragonffi](https://github.com/aguinet/dragonffi) to bind the custom implementation of XTEA. ## Get Started The sample [com.intotherain.voicechange.apk](./samples/com.intotherain.voicechange.apk) is a [suspicious application](https://www.virustotal.com/gui/file/708e6967920dcf2789b7183d714e73ab79a2f8b3ca71929b12aadeb2c58c2867/detection) that can be unpacked as follows: ```bash $ python ./unpack.py ./samples/com.intotherain.voicechange.apk [+] Legu version: 4.1.0.15 [+] Password is 'IPk2Hw7AKTuIQBlc' [+] Number of dex files: 1 [+] Unpacking #1 DEX files ... [+] dex 0 compressed size: 0x1619a3 [+] dex 0 uncompressed size: 0x5671f8 [+] Unpacking #1 hashmap ... [+] hashmap 0 compressed size: 0x4399c [+] hashmap 0 uncompressed size: 0x95558 [+] Unpacking #1 packed methods ... [+] packed methods 0 compressed_size: 0xf4636 [+] packed methods 0 uncompressed_size: 0x1e3072 [+] Stage 2: Patching DEX files [+] Unpacked APK: unpacked.apk ``` The unpacked DEX files are located in the ``unpacked.apk`` file. ## Requirements - Python >= 3.7 - Kaitai Struct - LIEF - pylegu --- # Android Runtime Restrictions Bypass (PoC) - Canonical: https://www.romainthomas.fr/project/android-runtime-restrictions-bypass/ - Markdown: https://www.romainthomas.fr/project/android-runtime-restrictions-bypass/index.md - Section: project - Published: 2019-03-27 - Modified: 2026-08-04 - Tags: android > Android application that disables Android restrictions without root privileges PoC that demonstrates how to disable runtime restrictions (hidden-api and ``dlopen`` namespaces) in user-land. --- # LIEF - Canonical: https://www.romainthomas.fr/project/lief/ - Markdown: https://www.romainthomas.fr/project/lief/index.md - Section: project - Published: 2016-04-27 - Modified: 2026-08-04 - Tags: ELF, PE, Mach-O, DEX, multi-language > Parse, inspect, modify, and build ELF, PE, Mach-O, DEX, and more through one consistent C++, Python, Rust, Java, or C API. LIEF is a cross-platform library to parse, inspect, modify, and rebuild executable formats through a consistent API. It supports ELF, PE, Mach-O, COFF, DEX, OAT, VDEX, ART, and more, with bindings for C++, Python, Rust, Java, and C. It turns out that many projects need to parse executable formats and they usually re-implement their own parser. Moreover, these parsers are usually bound to one language. LIEF fills this void with one well-tested abstraction used by reverse-engineering tools, security products, instrumentation pipelines, and research projects. --- # Introduction to Reverse Engineering - Canonical: https://www.romainthomas.fr/training/reverse-engineering-intro/ - Markdown: https://www.romainthomas.fr/training/reverse-engineering-intro/index.md - Section: training - Published: 2022-12-03 - Modified: 2022-12-27 > This workshop introduces the main concepts to get started in reverse engineering This workshop aims to provide the key concepts in reverse engineering to address the main challenges while analyzing a binary. In particular, it provides an overview of the compiler's optimizations, how to identify and reconstruct structures, and the methodology to analyze large binaries.