In this series: Part 1, Part 2, Part 3
What started as a ProGuard + basic string encryption + code reflection tool evolved into a multi-platform, complex solution including: control-flow obfuscation, complex and varied data and resources encryption, bytecode encryption, virtual environment and rooted system detection, application signature and certificate pinning enforcement, native code protection, as well as bytecode virtualization 1, and more.
This article presents the obfuscation techniques used by this app protector, as well as facility made available at runtime to protected programs 2. The analysis that follows was done statically, with JEB 3.20.
Identifying apps protected by this protector is relatively easy. It seems the default bytecode obfuscation settings place most classes in the o
package, and some will be renamed to invalid names on a Windows system, such as con
or aux
. Closer inspection of the code will reveal stronger hints than obfuscated names: decryption stubs, specific encrypted data, the presence of some so
library files, are all tell tale signs, as shown below.
Let’s run a Global Analysis (menu Android, Global analysis…) with standard settings on the file and see what gets auto-decrypted and auto-unreflected:
Lots of strings were decrypted, many of them specific to the app’s business logic itself, others related to RASP – that is, library code embedded within the APK, responsible for performing app signature verification for instance. That gives us valuable pointers into where we should be looking at if we’d like to focus on the protection code specifically.
The first section of this blog focuses on bytecode obfuscation and how JEB deals with it. It is mostly automated, but a final step requires manual assistance to achieve the best results.
Most obfuscated routines exhibit the following characteristics:
As an example, the following class is used to perform app certificate validation in order, for instance, to prevent resigned apps from functioning. A few items were renamed for clarity; decompilation is done with disabled Deobfuscators (MOD1+TAB, untick “Enable deobfuscators”):
In practice, such code is quite hard to comprehend on complex methods. With obfuscators enabled (the default setting), most of the above will be cleared.
See the re-decompilation of the same class, below.
Let’s give a hint to JEB as to what OPI0/OPI1 are.
That final output is clean and readable.
Other obfuscation techniques not exposed in this short routine above are arithmetic obfuscation and other operation complexification techniques. JEB will seamlessly deal with many of them. Example:
is optimized to
To summarize bytecode obfuscation:
RASP library routines are used at the developers’ discretion. They consist of a set of classes that the application code can call at any time, to perform tasks such as:
The client decides when and where to use them as well as what action should be taken on the results. The code itself is protected, that goes without saying.
PackageManager.getPackageInfo(packageName, GET_SIGNATURES).signatures
IntBuffer
or LongBuffer
.Debuggability check
The following checks must pass:
Context.ctx.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE
is falsero.debuggable
property, in two ways to ensure consistencyandroid.os.SystemProperties.get()
(private API)getprop
‘s binaryDebugging session check
The following checks must pass:
android.os.Debug.isDebuggerConnected()
is falsetracerpid
entry in /proc/<pid>/status
must be <= 0Debug key signing
PackageInfo.signatures
getSubjectX500Principal()
to verify that no certificate has a subject distinguished name (DN) equals to "CN=Android Debug,O=Android,C=US"
, which is the standard DN for debug certificates generated by the SDK toolsEmulator detection is done by checking any of the below.
1) All properties defined in system/build.prop
are retrieved, hashed, and matched against a small set of hard-coded hashes:
86701cb958c69d64cd59322dfebacede -> property ??? 19385aafbb452f39b5079513f668bbeb -> property ??? 24ad686ec83d904347c5a916acbe1779 -> property ??? b8c8255febc6c46a3e43b369225ded3e -> property ??? d76386ddf2c96a9a92fc4bc8f829173c -> property ??? 15fed45d5ca405da4e6aa9805daf2fbf -> property ??? (unused)
Unfortunately, we were not able to reverse those hashes back to known property strings – however, it was tried only on AOSP emulator images. If anybody wants to help and run the below on other build.prop files, feel free to let us know what property strings those hashes match to. Here is the hash verification source, to be run be on build.prop files.
2) The following file is readable:
/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_cur_freq
3) Verify if any of those qemu, genymotion and bluestacks emulator files exist and are readable:
/dev/qemu_pipe /dev/socket/baseband_genyd /dev/socket/genyd /dev/socket/qemud /sys/qemu_trace /system/lib/libc_malloc_debug_qemu.so /dev/bst_gps /dev/bst_time /dev/socket/bstfolderd /system/lib/libbstfolder_jni.so
4) Check for the presence of wired network interfaces: (via NetworkInterface.getNetworkInterfaces
)
eth0 eth1
5) If the app has the permission READ_PHONE_STATE
, telephony information is verified, an emulator is detected if any of the below matches (standard emulator image settings):
- "getLine1Number": "15555215554", "15555215556", "15555215558", "15555215560", "15555215562", "15555215564", "15555215566", "15555215568", "15555215570", "15555215572", "15555215574", "15555215576", "15555215578", "15555215580", "15555215582", "15555215584" - "getNetworkOperatorName": "android" - "getSimSerialNumber": "89014103211118510720" - "getSubscriberId": "310260000000000" - "getDeviceId": "000000000000000", "e21833235b6eef10", "012345678912345"
6) /proc
checks:
/proc/ioports: entry "0ff :" (unknown port, likely used by some emulators) /proc/self/maps: entry "gralloc.goldfish.so" (GF: older emulator kernel name)
7) Property checks (done in multiple ways with a consistency checks, as explained earlier), failed if any entry is found and start with one of the provided values:
- "ro.product.manufacturer": "Genymotion", "unknown", "chromium" - "ro.product.device": "vbox86p", "generic", "generic_x86", "generic_x86_64" - "ro.product.model": "sdk", "emulator", "App Runtime for Chrome", "Android SDK built for x86", "Android SDK built for x86_64" - "ro.hardware": "goldfish", "vbox86", "ranchu" - "ro.product.brand": "generic", "chromium" - "ro.kernel.qemu": "1" - "ro.secure": "0" - "ro.build.product": "sdk", "vbox86p", "full_x86", "generic_x86", "generic_x86_64" - "ro.build.fingerprint": "generic/sdk/generic", "generic_x86/sdk_x86/generic_x86", "generic/google_sdk/generic", "generic/vbox86p/vbox86p", "google/sdk_gphone_x86/generic_x86" - "ro.bootloader": "unknown" - "ro.bootimage.build.fingerprint": "Android-x86" - "ro.build.display.id": "test-" - "init.svc.qemu-props" (any value) - "qemu.hw.mainkeys" (any value) - "qemu.sf.fake_camera" (any value) - "qemu.sf.lcd_density" (any value) - "ro.kernel.android.qemud" (any value)
The term covers a wide range of techniques designed to intercept regular control flow in order to examine and/or modify execution.
1) Xposed instrumentation framework detection, by attempting to load any of the classes:
de.robv.android.xposed.XposedBridge de.robv.android.xposed.XC_MethodHook
Class loading is done in different ways in an attempt to circumvent hooking itself, using Class.forName
with a variety of class loaders, custom class loaders and ClassLoader.getLoadedClass
, as well as lower-level private methods, such as Class.classForName
.
2) Cydia Substrate instrumentation framework detection.
3) ADBI (Android Dynamic Binary Instrumentation) detection
4) Stack frame verification: an exception is generated in order to retrieve a stack frame. The callers are hashed and compared to an expected hard-coded value.
5) Native code checks. This will be detailed in another blog, if time allows.
While root detection overlaps with most of the above, it is still another layer of security a determined attacker would have to jump over (or walk around) in order to get protected apps to run on unusual systems. Checks are plenty, and as is the case for all the code described here, heavily obfuscated. If you are analyzing such files, keeping the Deobfuscators enabled and providing guard0/guard1 hints is key to a smooth analysis.
Build.prop checks. As was described in emulator detection.
su execution. Attempt to execute su
, and verify whether su -c id
== root
su presence. su
is looked up in the following locations:
/data/local/ /data/local/bin/ /data/local/xbin/ /sbin/ /system/bin/ /system/bin/.ext/ /system/bin/failsafe/ /system/sd/xbin/ /system/usr/we-need-root/ /system/xbin/
Magisk detection through mount. Check whether mount can be executed and contains databases/su.db
(indicative of Magisk) or whether /proc/mounts
contains references to databases/su.db
.
Read-only system partitions. Check if any system partition is mounted as read-write (when it should be read-only). The result of mount
is examined for any of the following entries marked rw
:
/system /system/bin /system/sbin /system/xbin /vendor/bin /sbin /etc
Verify installed apps in the hope of finding one whose package name hashes to the hard-coded value:
0x9E6AE9309DBE9ECFL
Unfortunately, that value was not reversed, let us know if you find which package name generates this hash – see the algorithm below:
public static long hashstring(String str) { long h = 0L; for(int i = 0; i < str.length(); i++) { int c = str.charAt(i); h = h << 5 ^ (0xFFFFFFFFF8000000L & h) >> 27 ^ ((long)c); } return h; }
NOTE: App enumeration is performed in two ways to maximize chances of evading partial hooks.
PackageManager.getInstalledApplications
MAIN
intents: PackageManager.queryIntentActivities(new Intent("android.intent.action.MAIN"))
, derive the package name from the intent via ResolveInfo.activityInfo.packageName
SElinux verification. If the file /sys/fs/selinux/policy
cannot be read, the check immediately passes. If it is readable, the policy is examined and hints indicative of a rooted device are looked for by hash comparison:
472001035L -601740789L
The hashing algorithm is extremely simple, see below. For each byte of the file, the crc is updated and compared to hard-coded values.
long h = 0L; //for each byte: h = (h << 5 ^ ((long)(((char)b)))) & 0x3FFFFFFFL; // check h against known list
Running processes checks. All running processes and their command-lines are enumerated and hashed, and specific values are indirectly looked up by comparing against hard-coded lists.
This verifier parses compressed entries in the APK (zip) file and compares them against well-known, hard-coded CRC values.
Consistency checks on the application Manifest consists of enumerating the entries using two different ways and comparing results. Discrepancies are reported.
Context.getAssets()
, parse manuallyJarFile(Context.getPackageCodePath()).getManifest().getEntries()
Discrepancies in the Manifest could indicate system hooks attempting to conceal files added to the application.
This routine checks for permission discrepancies between what’s declared by the app and what the system grant the app.
INTERACT_ACROSS_USERS
and INTERACT_ACROSS_USERS_FULL
permissions, checkCallingOrSelfPermission
(API 22-) or checkSelfPermission
(API 23+) to verify that the permission is not granted.Permission discrepancies could be used to find out system hooks or unorthodox execution environments.
Other runtime components include library code to perform SSL certificate pinning, as well as obfuscated wrappers around web view clients. None of those are of particular interest.
That’s it for the obfuscation and runtime protection facility. Key take-away to analyze such protected code:
The second part in the series presents bytecode encryption and assets encryption.