Copyright belongs to the author. If reprinted, please indicate the source: https://cyrus-studio.github.io/blog/
Dex2C
Dex2C is a tool that converts DEX bytecode (Java layer code) in Android applications into semantically equivalent C code.
After processing with Dex2C, Java methods become native methods, thus achieving source code obfuscation protection.
Java code:
object AESUtils { // Convert a regular string to IvParameterSpec private fun stringToIV(iv: String): IvParameterSpec { // Convert the string to a byte array using UTF-8 encoding, ensuring its length is 16 bytes val ivBytes = iv.toByteArray(Charsets.UTF_8) val ivArray = ByteArray(16) System.arraycopy(ivBytes, 0, ivArray, 0, Math.min(ivBytes.size, 16)) return IvParameterSpec(ivArray) }}
Converted C/C++ code:
#include "Dex2C.h" /* LAESUtils;->stringToIV(Ljava/lang/String;)Ljavax/crypto/spec/IvParameterSpec; */ extern "C" JNIEXPORT jobject JNICALL Java_AESUtils_stringToIV__Ljava_lang_String_2(JNIEnv* env, jobject thiz, jstring p4) { jobject v0 = NULL; jobject v1 = NULL; jobject v2 = NULL; jobject v3 = NULL; jint v4; jobject v5 = NULL; jint v6; jint v7; jclass cls0 = NULL, cls1 = NULL, cls2 = NULL, cls3 = NULL, cls4 = NULL, cls5 = NULL, cls6 = NULL; jfieldID fld0 = NULL; jmethodID mth0 = NULL, mth1 = NULL, mth2 = NULL, mth3 = NULL, mth4 = NULL; v0 = (jobject) env->NewLocalRef(thiz); v1 = (jobject) env->NewLocalRef(p4); L0: LOGD("0:sget-object \x76\x30\x2c\x20\x4c\x6b\x6f\x74\x6c\x69\x6e\x2f\x74\x65\x78\x74\x2f\x43\x68\x61\x72\x73\x65\x74\x73\x3b\x2d\x3e\x55\x54\x46\x5f\x38\x20\x4c\x6a\x61\x76\x61\x2f\x6e\x69\x6f\x2f\x63\x68\x61\x72\x73\x65\x74\x2f\x43\x68\x61\x72\x73\x65\x74\x3b"); { #define EX_HANDLE EX_UnwindBlock if (v2) { LOGD("env->DeleteLocalRef(%p):v2", v2); env->DeleteLocalRef(v2); } jclass &clz = cls0; jfieldID &fld = fld0; D2C_RESOLVE_STATIC_FIELD(clz, fld, "kotlin/text/Charsets", "UTF_8", "Ljava/nio/charset/Charset;"); v2 = (jobject) env->GetStaticObjectField(clz, fld); D2C_CHECK_PENDING_EX; #undef EX_HANDLE } LOGD("4:invoke-virtual \x76\x34\x2c\x20\x76\x30\x2c\x20\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x74\x72\x69\x6e\x67\x3b\x2d\x3e\x67\x65\x74\x42\x79\x74\x65\x73\x28\x4c\x6a\x61\x76\x61\x2f\x6e\x69\x6f\x2f\x63\x68\x61\x72\x73\x65\x74\x2f\x43\x68\x61\x72\x73\x65\x74\x3b\x29\x5b\x42"); { #define EX_HANDLE EX_UnwindBlock D2C_NOT_NULL(v1); jclass &clz = cls1; jmethodID ∣ = mth0; D2C_RESOLVE_METHOD(clz, mid, "java/lang/String", "getBytes", "(Ljava/nio/charset/Charset;)[B"); jvalue args[] = {{.l = v2}}; v3 = (jarray) env->CallObjectMethodA(v1, mid, args); D2C_CHECK_PENDING_EX; #undef EX_HANDLE } LOGD("a:move-result-object \x76\x34"); if (v1) { LOGD("env->DeleteLocalRef(%p):v1", v1); env->DeleteLocalRef(v1); } v1 = (jobject) v3; LOGD("c:const-string \x76\x30\x2c\x20\x27\x67\x65\x74\x42\x79\x74\x65\x73\x28\x2e\x2e\x2e\x29\x27"); if (v2) { LOGD("env->DeleteLocalRef(%p):v2", v2); env->DeleteLocalRef(v2); } v2 = (jstring) env->NewStringUTF("\x67\x65\x74\x42\x79\x74\x65\x73\x28\x2e\x2e\x2e\x29"); LOGD("10:invoke-static \x76\x34\x2c\x20\x76\x30\x2c\x20\x4c\x6b\x6f\x74\x6c\x69\x6e\x2f\x6a\x76\x6d\x2f\x69\x6e\x74\x65\x72\x6e\x61\x6c\x2f\x49\x6e\x74\x72\x69\x6e\x73\x69\x63\x73\x3b\x2d\x3e\x63\x68\x65\x63\x6b\x4e\x6f\x74\x4e\x75\x6c\x6c\x45\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x56\x61\x6c\x75\x65\x28\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x4f\x62\x6a\x65\x63\x74\x3b\x20\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x74\x72\x69\x6e\x67\x3b\x29\x56"); { #define EX_HANDLE EX_UnwindBlock jclass &clz = cls2; jmethodID ∣ = mth1; D2C_RESOLVE_STATIC_METHOD(clz, mid, "kotlin/jvm/internal/Intrinsics", "checkNotNullExpressionValue", "(Ljava/lang/Object;Ljava/lang/String;)V"); jvalue args[] = {{.l = v1}, {.l = v2}}; env->CallStaticVoidMethodA(clz, mid, args); D2C_CHECK_PENDING_EX; #undef EX_HANDLE } v4 = 16; v4 = 16; LOGD("1a:new-array \x76\x31\x2c\x20\x76\x30\x2c\x20\x5b\x42"); { #define EX_HANDLE EX_UnwindBlock if (v4 < 0) { d2c_throw_exception(env, "java/lang/NegativeArraySizeException", "negative array size"); goto EX_HANDLE; } if (v5) { LOGD("env->DeleteLocalRef(%p):v5", v5); env->DeleteLocalRef(v5); } v5 = (jarray) env->NewByteArray((jint) v4); D2C_CHECK_PENDING_EX; #undef EX_HANDLE } LOGD("1e:array-length \x76\x32\x2c\x20\x76\x34"); { #define EX_HANDLE EX_UnwindBlock D2C_NOT_NULL(v1); v6 = env->GetArrayLength((jarray) v1); D2C_CHECK_PENDING_EX; #undef EX_HANDLE } LOGD("20:invoke-static \x76\x32\x2c\x20\x76\x30\x2c\x20\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x4d\x61\x74\x68\x3b\x2d\x3e\x6d\x69\x6e\x28\x49\x20\x49\x29\x49"); { #define EX_HANDLE EX_UnwindBlock jclass &clz = cls4; jmethodID ∣ = mth2; D2C_RESOLVE_STATIC_METHOD(clz, mid, "java/lang/Math", "min", "(II)I"); jvalue args[] = {{.i = v6}, {.i = v4}}; v7 = (jint) env->CallStaticIntMethodA(clz, mid, args); D2C_CHECK_PENDING_EX; #undef EX_HANDLE } LOGD("26:move-result \x76\x30"); v4 = (jint) v7; v6 = 0; LOGD("2a:invoke-static \x76\x34\x2c\x20\x76\x32\x2c\x20\x76\x31\x2c\x20\x76\x32\x2c\x20\x76\x30\x2c\x20\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x79\x73\x74\x65\x6d\x3b\x2d\x3e\x61\x72\x72\x61\x79\x63\x6f\x70\x79\x28\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x4f\x62\x6a\x65\x63\x74\x3b\x20\x49\x20\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x4f\x62\x6a\x65\x63\x74\x3b\x20\x49\x20\x49\x29\x56"); { #define EX_HANDLE EX_UnwindBlock jclass &clz = cls5; jmethodID ∣ = mth3; D2C_RESOLVE_STATIC_METHOD(clz, mid, "java/lang/System", "arraycopy", "(Ljava/lang/Object;ILjava/lang/Object;II)V"); jvalue args[] = {{.l = v1}, {.i = v6}, {.l = v5}, {.i = v6}, {.i = v4}}; env->CallStaticVoidMethodA(clz, mid, args); D2C_CHECK_PENDING_EX; #undef EX_HANDLE } LOGD("30:new-instance \x76\x34\x2c\x20\x4c\x6a\x61\x76\x61\x78\x2f\x63\x72\x79\x70\x74\x6f\x2f\x73\x70\x65\x63\x2f\x49\x76\x50\x61\x72\x61\x6d\x65\x74\x65\x72\x53\x70\x65\x63\x3b"); { #define EX_HANDLE EX_UnwindBlock if (v1) { LOGD("env->DeleteLocalRef(%p):v1", v1); env->DeleteLocalRef(v1); } jclass &clz = cls6; D2C_RESOLVE_CLASS(clz, "javax/crypto/spec/IvParameterSpec"); v1 = (jobject) env->AllocObject(clz); D2C_CHECK_PENDING_EX; #undef EX_HANDLE } LOGD("34:invoke-direct \x76\x34\x2c\x20\x76\x31\x2c\x20\x4c\x6a\x61\x76\x61\x78\x2f\x63\x72\x79\x70\x74\x6f\x2f\x73\x70\x65\x63\x2f\x49\x76\x50\x61\x72\x61\x6d\x65\x74\x65\x72\x53\x70\x65\x63\x3b\x2d\x3e\x3c\x69\x6e\x69\x74\x3e\x28\x5b\x42\x29\x56"); { #define EX_HANDLE EX_UnwindBlock D2C_NOT_NULL(v1); jclass &clz = cls6; jmethodID ∣ = mth4; D2C_RESOLVE_METHOD(clz, mid, "javax/crypto/spec/IvParameterSpec", "<init>", "([B)V"); jvalue args[] = {{.l = v5}}; env->CallVoidMethodA(v1, mid, args); D2C_CHECK_PENDING_EX; #undef EX_HANDLE } return (jobject) v1; EX_UnwindBlock: return NULL; }</init>
DCC (Dex-to-C Compiler)
An open-source compiler that converts smali instruction streams into semantically equivalent C/C++ code.
Project address: https://github.com/amimo/dcc.git
1. Install Dependencies
• Install JRE or JDK and add Java to the PATH.• Install Python 3, refer to:Using Miniconda to Manage Python Environments[1]• Install project dependencies
cd dcc
pip3 install -r requirements.txt
• Download apktool[2], rename it to apktool[3].jar and place it in the dcc/tools directory• Install NDK(r17+)[4], modify ndk_dir in dcc.cfg to the NDK installation directory
2. Load Native Libraries
First, add the code to load the so library in the appropriate place in the app code, such as the static code block of Application or onCreate, and regenerate the apk
package com.cyrus.example
import android.app.Application
class CyrusStudioApplication: Application() { companion object { init { try { System.loadLibrary("nc") } catch (e: Throwable) { // Load failed, do not handle } } } }
3. Specify Methods to Compile
3.1 Using Whitelists and Blacklists
DCC supports using whitelists and blacklists to filter functions that need to be compiled or prohibited from compilation. Modify filter.txt to configure the functions to be processed using regular expressions.
The default compiles Activity.onCreate and all functions in the test demo.
# Do not compile constructors (<init>) and class initialization blocks (<clinit>). (! indicates exclusion) !<clinit|init>
# Do not compile methods named bigGoto !bigGoto
# Compile classes/methods containing TestCompiler .*TestCompiler.*
# Compile any method signature containing onCreate(Landroid/os/Bundle; .*;onCreate(Landroid/os/Bundle;.*
# Compile all methods #.*</clinit|init></clinit></init>
3.2 Using Annotations
Add a Dex2C annotation class in any package
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Dex2C
Then use Dex2C to mark the classes that need to be compiled
import com.cyrus.example.dex2c.Dex2C
@Dex2C
object AESUtils { ... }
Or methods
package com.cyrus.example.md5
import com.cyrus.example.dex2c.Dex2C
import java.security.MessageDigest
object MD5Tools {
@Dex2C
fun javaMD5(input: String): String { val md = MessageDigest.getInstance("MD5") val digest = md.digest(input.toByteArray()) return digest.joinToString("") { "%02x".format(it) }}
}
However, it has been tested that annotations on methods do not take effect.
4. Strengthen the APP
Use the following command to strengthen app.apk
python dcc.py app.apk -o out.apk
• This command will generate two files out.apk and project-source.zip.• Among them, out.apk is the strengthened app that has been signed and can be installed directly.• project-source.zip is a JNI project that contains the compiled C code, which can be directly used for NDK compilation after extraction.
Resolving Signature Failure Issues
Use the command below to strengthen the apk
python dcc.py app-debug.apk -o out.apk
At the signing step, the following error occurs:
I: Built apk into: C:\Users\cyrus\AppData\Local\Temp\tmp6qg2cdco-unsigned.apk
[INFO ] dcc: signing C:\Users\cyrus\AppData\Local\Temp\tmp6qg2cdco-unsigned.apk -> out.apk
Exception in thread "main" java.lang.NoClassDefFoundError: sun/misc/BASE64Encoder
at com.android.signapk.SignApk.addDigestsToManifest(SignApk.java:184)
at com.android.signapk.SignApk.main(SignApk.java:504)
Caused by: java.lang.ClassNotFoundException: sun.misc.BASE64Encoder
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
at java.base/jdk.internal.loader.ClassLoader.loadClass(ClassLoader.java:526)... 2 more
[ERROR ] dcc: Compile app-debug.apk failed! Traceback (most recent call last): File "D:\Projects\dcc\dcc.py", line 541, in <module> dcc_main(infile, filtercfg, outapk, do_compile, project_dir, source_archive, dynamic_register) File "D:\Projects\dcc\dcc.py", line 495, in dcc_main sign(unsigned_apk, outapk) File "D:\Projects\dcc\dcc.py", line 83, in sign subprocess.check_call(['java', '-jar', SIGNJAR, pem, pk8, unsigned_apk, signed_apk]) File "D:\App\Miniconda3\envs\anti-app\lib\subprocess.py", line 373, in check_call raise CalledProcessError(retcode, cmd) subprocess.CalledProcessError: Command '['java', '-jar', 'tools/signapk.jar', 'tests/testkey/testkey.x509.pem', 'tests/testkey/testkey.pk8', 'C:\Users\cyrus\AppData\Local\Temp\tmp6qg2cdco-unsigned.apk', 'out.apk']' returned non-zero exit status 1.
[INFO ] dcc: removing C:\Users\cyrus\AppData\Local\Temp\dcc-project-hrosdmqy
[INFO ] dcc: removing C:\Users\cyrus\AppData\Local\Temp\tmpzhdviz75-dcc
[INFO ] dcc: removing C:\Users\cyrus\AppData\Local\Temp\dcc-apktool-2jznztrx
[INFO ] dcc: removing C:\Users\cyrus\AppData\Local\Temp\tmp6qg2cdco-unsigned.apk
The reason for this exception is:
• DCC internally uses an old signing tool signapk.jar• signapk.jar depends on sun.misc.BASE64Encoder, but you are using Java 9 or above• Starting from Java 9, sun.misc.BASE64Encoder has been removed
In simple terms: the signing tool provided by DCC is too old and incompatible with the new version of Java you are using.
Change to use the apksigner tool provided by the Android SDK for signing.
Add the apksigner configuration in dcc.cfg
{