Transforming Java to Native with Dex2C: A Comprehensive Guide to Android Code Hardening

Copyright belongs to the author. If reprinted, please indicate the source of the article: https://cyrus-studio.github.io/blog/

Introduction to 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 &mid = 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 &mid = 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 &mid = 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 &mid = 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 &mid = 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;
}

DCC (Dex-to-C Compiler)

DCC is an open-source compiler that transforms smali instruction streams into semantically equivalent C/C++ code.

Project address: https://github.com/amimo/dcc.git

1. Install Dependencies

cd dcc
pip3 install -r requirements.txt
  • • Download apktool, rename it to apktool.jar and place it in the dcc/tools directory
  • • Install NDK (r17+), 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 the 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 nothing
            }
        }
    }
}

3. Specify Methods to Compile

3.1 Using Whitelists and Blacklists

DCC supports filtering functions to compile or prohibit compilation using whitelists and blacklists. Modify filter.txt and configure the functions to be processed using regular expressions.

By default, it 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
#.*

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. Harden the APP

Use the following command to harden 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 hardened 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 harden the APK

python dcc.py app-debug.apk -o out.apk

It reports the following error during the signing step:

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 that comes with DCC is too old and incompatible with your current version of Java.

Switching to the apksigner tool provided by the Android SDK for signing will work.

Add the apksigner configuration in dcc.cfg

{
    "apktool": "tools/apktool.jar",
    "ndk_dir": "D:\App\android\sdk\ndk\27.1.12297006",
    "apksigner": "D:\App\android\sdk\build-tools\35.0.0\apksigner.bat"
}

Parse the apksigner configuration in the main method of dcc.py

if 'apksigner' in dcc_cfg and os.path.exists(dcc_cfg['apksigner']):
    apksigner = dcc_cfg['apksigner']

Modify the sign method in dcc.py

def sign(unsigned_apk, signed_apk):
    pem = os.path.join('tests/testkey/testkey.x509.pem')
    pk8 = os.path.join('tests/testkey/testkey.pk8')
    logger.info("signing %s -> %s" % (unsigned_apk, signed_apk))
    subprocess.check_call(['java', '-jar', SIGNJAR, pem, pk8, unsigned_apk, signed_apk])

Change the sign method to use apksigner for signing

def sign(unsigned_apk, signed_apk):
    keystore = 'cyrus.jks'
    alias = 'cyrus_studio'
    storepass = 'cyrus_studio'
    keypass = 'cyrus_studio'

    logger.info("signing %s -> %s" % (unsigned_apk, signed_apk))

    # The sign command of apksigner directly operates on the output file (--out), so I first copy unsigned to signed
    import shutil
    shutil.copy(unsigned_apk, signed_apk)

    # Call apksigner to sign
    subprocess.check_call([
        apksigner, 'sign',
        '--ks', keystore,
        '--ks-key-alias', alias,
        '--ks-pass', f'pass:{storepass}',
        '--key-pass', f'pass:{keypass}',
        '--out', signed_apk,
        signed_apk
    ])

Why not use jarsigner?

Because jarsigner can only generate the old v1 signature, while apksigner supports v1/v2/v3/v4 new signatures, ensuring that the APK can be installed normally on Android 7.0 and above.

Resolving Installation Error Messages

Installation error message:

adb: failed to install .\out.apk: Failure [INSTALL_FAILED_TEST_ONLY: installPackageLI]

The APK has been marked with android:testOnly=”true”, and the system does not allow direct installation.

Using ApkToolPlus to view the AndroidManifest.xml information of the APK indeed shows android:testOnly

Transforming Java to Native with Dex2C: A Comprehensive Guide to Android Code Hardening
word/media/image1.png

Tool address: https://github.com/CYRUS-STUDIO/ApkToolPlus

Usually, this is because you packaged the APK in debug mode, and the system prohibits users from manually installing such APKs for security reasons.

Add the -t parameter to force installation, allowing adb to ignore the testOnly flag

adb install -t .\out.apk

Or: Because the APK packaged in debug mode is unsigned, repackaging and signing will solve the issue.

Debugging DCC

Create a new run/debug configuration in PyCharm, select dcc.py as the script file, and add Script parameters

Transforming Java to Native with Dex2C: A Comprehensive Guide to Android Code Hardening
word/media/image2.png

Set breakpoints and run debugging

Transforming Java to Native with Dex2C: A Comprehensive Guide to Android Code Hardening
word/media/image3.png

Reverse Analysis of Hardened APK

Conversion completed

[WARNING ] androguard.core.api_specific_resources: Requested API level 34 is larger than maximum we have, returning API level 28 instead.
make: Entering directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
Android NDK: android-19 is unsupported. Using minimum supported version android-21.
make: Leaving directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
make: Entering directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
[armeabi-v7a] Compile++ thumb: nc <= DynamicRegister.cpp
make: Leaving directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
make: Entering directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
[armeabi-v7a] Compile++ thumb: nc <= Java_AESUtils_encrypt_00024lambda_000240__B.cpp
make: Leaving directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
make: Entering directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
[armeabi-v7a] Compile++ thumb: nc <= Java_AESUtils_decrypt__Ljava_lang_String_2Ljava_lang_String_2Ljava_lang_String_2Ljava_lang_String_2.cpp        
make: Leaving directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
make: Entering directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
[armeabi-v7a] Compile++ thumb: nc <= Java_AESUtils_encrypt__Ljava_lang_String_2Ljava_lang_String_2Ljava_lang_String_2Ljava_lang_String_2.cpp        
make: Leaving directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
make: Entering directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
[armeabi-v7a] Compile++ thumb: nc <= Java_AESUtils_getTransformation__Ljava_lang_String_2.cpp
make: Leaving directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
make: Entering directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
[armeabi-v7a] Compile++ thumb: nc <= Java_AESUtils_stringToIV__Ljava_lang_String_2.cpp
make: Leaving directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
make: Entering directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
[armeabi-v7a] Compile++ thumb: nc <= Java_AESUtils_stringToSecretKey__Ljava_lang_String_2.cpp
make: Leaving directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
make: Entering directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
[armeabi-v7a] SharedLibrary  : libnc.so
make: Leaving directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
make: Entering directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
[arm64-v8a] Compile++      : nc <= DynamicRegister.cpp
make: Leaving directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
make: Entering directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
[armeabi-v7a] Compile++ thumb: nc <= Dex2C.cpp
make: Leaving directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
make: Entering directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
[arm64-v8a] Compile++      : nc <= Java_AESUtils_decrypt__Ljava_lang_String_2Ljava_lang_String_2Ljava_lang_String_2Ljava_lang_String_2.cpp
make: Leaving directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
make: Entering directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
[arm64-v8a] Compile++      : nc <= Java_AESUtils_encrypt_00024lambda_000240__B.cpp
make: Leaving directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
make: Entering directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
[arm64-v8a] Compile++      : nc <= Java_AESUtils_encrypt__Ljava_lang_String_2Ljava_lang_String_2Ljava_lang_String_2Ljava_lang_String_2.cpp
make: Leaving directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
make: Entering directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
[arm64-v8a] Compile++      : nc <= well_known_classes.cpp
make: Leaving directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
make: Entering directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
[arm64-v8a] SharedLibrary  : libnc.so
make: Leaving directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
make: Entering directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
[arm64-v8a] Install        : libnc.so => libs/armeabi-v7a/libnc.so
make: Leaving directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
make: Entering directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
[arm64-v8a] SharedLibrary  : libnc.so
make: Leaving directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
make: Entering directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
[arm64-v8a] Install        : libnc.so => libs/arm64-v8a/libnc.so
make: Leaving directory 'C:/Users/cyrus/AppData/Local/Temp/dcc-project-nz2i_v6g'
I: Using Apktool 2.11.1 on app.apk with 8 threads
I: Baksmaling classes.dex...
I: Copying raw resources...
I: Baksmaling classes2.dex...
I: Copying raw manifest...
I: Copying original files...
I: Copying assets...
I: Copying lib...
I: Copying unknown files...
I: Using Apktool 2.11.1 on tmpe27zhohx-unsigned.apk with 8 threads
I: Checking whether resources have changed...
I: Checking whether sources have changed...
I: Checking whether sources have changed...
I: Smaling smali folder into classes.dex...
I: Copying raw resources...
I: Smaling smali_classes2 folder into classes2.dex...
I: Building apk file...
I: Importing assets...
I: Importing lib...
I: Importing unknown files...
I: Built apk into: C:\Users\cyrus\AppData\Local\Temp\tmpe27zhohx-unsigned.apk
[INFO    ] dcc: signing C:\Users\cyrus\AppData\Local\Temp\tmpe27zhohx-unsigned.apk -> out.apk
[INFO    ] dcc: removing C:\Users\cyrus\AppData\Local\Temp\dcc-project-nz2i_v6g
[INFO    ] dcc: removing C:\Users\cyrus\AppData\Local\Temp\tmpgzmhlbm5-dcc
[INFO    ] dcc: removing C:\Users\cyrus\AppData\Local\Temp\dcc-apktool-h7yloeiq
[INFO    ] dcc: removing C:\Users\cyrus\AppData\Local\Temp\tmpe27zhohx-unsigned.apk

Using GDA to open out.apk, you can see that all Java methods in the AESUtils class that have been annotated have been converted into native functions

Transforming Java to Native with Dex2C: A Comprehensive Guide to Android Code Hardening
word/media/image4.png

The Java methods in the AESUtils class have been converted to the following C++ code

Transforming Java to Native with Dex2C: A Comprehensive Guide to Android Code Hardening
word/media/image5.png
Transforming Java to Native with Dex2C: A Comprehensive Guide to Android Code Hardening
word/media/image6.png

Using IDA to open libnc.so, you can see that the protected functions are all statically registered through JNI

Transforming Java to Native with Dex2C: A Comprehensive Guide to Android Code Hardening
word/media/image7.png

Complete Source Code

Open source addresses:

References

[1] Miniconda Guide: Elegantly Manage Your Python Environment: https://cyrus-studio.github.io/blog/posts/miniconda-%E5%85%A8%E6%94%BB%E7%95%A5%E4%BC%98%E9%9B%85%E7%AE%A1%E7%90%86%E4%BD%A0%E7%9A%84-python-%E7%8E%AF%E5%A2%83/[2] apktool: https://apktool.org/[3] NDK(r17+): https://developer.android.com/ndk/guides?hl=zh-cn

Leave a Comment