Information Security 16 min read

Android Signature Verification and JNI Registration: Security Challenges and Mitigation Strategies

This article explains the background of Android app signing, outlines the core security objectives of confidentiality, integrity and availability, compares static and dynamic JNI registration methods, demonstrates native signature verification and certificate integrity checks, and summarizes common cracking techniques together with practical hardening solutions.

JD Retail Technology
JD Retail Technology
JD Retail Technology
Android Signature Verification and JNI Registration: Security Challenges and Mitigation Strategies

Android applications are distributed as .apk files, which are essentially ZIP archives containing compiled Java code, resources, and metadata. Because most Android code is written in Java, the APK format shares many characteristics with Java JAR files.

To ensure that an app and its updates originate from the same developer, Android uses a developer‑signed certificate; the system simply compares the binary representation of this certificate during installation and updates.

Given Android's open‑source nature, security is a major concern. Attackers often embed signature verification logic in native .so libraries via JNI to make reverse‑engineering harder. The article therefore explores Android’s attack‑defense landscape from a JNI perspective.

Security Goals

Traditional information‑security goals—confidentiality, integrity, and availability—are emphasized, with additional considerations of controllability and non‑repudiation. Android provides digital signatures, but verifying them solely in Java is vulnerable, prompting a shift to native verification.

JNI Registration Methods

Two JNI registration approaches are described:

Static registration : The native function name must follow a strict pattern (e.g., Java_com_jd_jnidemo_MainActivity_nativeMethod ), requiring a generated header via javah -jni com.jd.jnidemo.MainActivity and a corresponding .h file ( com_jd_jnidemo_MainActivity.h ). This method is simple but suffers from long names, high maintenance effort, and slower first‑call performance.

Dynamic registration : Uses a JNINativeMethod array to map Java method names and signatures to native function pointers, registered in JNI_OnLoad . This avoids the naming constraints and improves performance.

The JNINativeMethod structure is defined as:

typedef struct {
    const char* name;        // Java method name
    const char* signature;  // Method signature
    void* fnPtr;            // Pointer to native implementation
} JNINativeMethod;

Local Signature Verification

Java code to obtain the app's signature:

PackageManager pm = context.getPackageManager();
String packageName = context.getPackageName();
PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
Signature sign = packageInfo.signatures[0];
Log.i("test", "hashCode : " + sign.hashCode());

To compute the SHA‑1 fingerprint of the certificate:

private byte[] getCertificateSHA1(Context context) {
    PackageManager pm = context.getPackageManager();
    String packageName = context.getPackageName();
    try {
        PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
        Signature[] signatures = packageInfo.signatures;
        byte[] cert = signatures[0].toByteArray();
        X509Certificate x509 = X509Certificate.getInstance(cert);
        MessageDigest md = MessageDigest.getInstance("SHA1");
        return md.digest(x509.getEncoded());
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

The corresponding native implementation (JNI) retrieves the same signature hash code:

int getSignHashCode(JNIEnv *env, jobject context) {
    jclass ctxCls = (*env)->GetObjectClass(env, context);
    jmethodID getPM = (*env)->GetMethodID(env, ctxCls, "getPackageManager", "()Landroid/content/pm/PackageManager;");
    jobject pm = (*env)->CallObjectMethod(env, context, getPM);
    jclass pmCls = (*env)->GetObjectClass(env, pm);
    jmethodID getInfo = (*env)->GetMethodID(env, pmCls, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
    jmethodID getPkgName = (*env)->GetMethodID(env, ctxCls, "getPackageName", "()Ljava/lang/String;");
    jstring pkgName = (*env)->CallObjectMethod(env, context, getPkgName);
    jobject pkgInfo = (*env)->CallObjectMethod(env, pm, getInfo, pkgName, 64);
    jclass pkgInfoCls = (*env)->GetObjectClass(env, pkgInfo);
    jfieldID sigField = (*env)->GetFieldID(env, pkgInfoCls, "signatures", "[Landroid/content/pm/Signature;");
    jobjectArray sigArray = (jobjectArray)(*env)->GetObjectField(env, pkgInfo, sigField);
    jobject signature = (*env)->GetObjectArrayElement(env, sigArray, 0);
    jclass sigCls = (*env)->GetObjectClass(env, signature);
    jmethodID hashCode = (*env)->GetMethodID(env, sigCls, "hashCode", "()I");
    jint hash = (*env)->CallIntMethod(env, signature, hashCode);
    __android_log_print(ANDROID_LOG_DEBUG, "JNI", "hashcode: %d", hash);
    return hash;
}

Android versions prior to 4.4 contain a known Lint warning ( android-fake-id-vulnerability ) due to a bug in certificate extraction; newer versions have fixed this.

Certificate Integrity Verification

Beyond signature hash comparison, the article suggests parsing the CERT.RSA file (a PKCS#7 container) to verify the embedded X.509 certificate directly. Commands for inspection include:

keytool -list -v -keystore debug.keystore
keytool -printcert -file CERT.RSA
openssl pkcs7 -inform DER -in CERT.RSA -noout -print_certs -text

Sample Java code using the Android‑compatible PKCS7 class reads the certificate:

FileInputStream fis = new FileInputStream("/path/CERT.RSA");
PKCS7 pkcs7 = new PKCS7(fis);
X509Certificate cert = pkcs7.getCertificates()[0];
System.out.println("issuer:" + cert.getIssuerDN());
System.out.println("subject:" + cert.getSubjectDN());
System.out.println(cert.getPublicKey());

These techniques can be moved to native code for stronger protection.

Common Cracking Techniques and Countermeasures

Typical attack vectors include:

Java‑level retrieval of signatures via getPackageManager().getPackageInfo(...).signatures .

Native or script‑level reflection to obtain the same data.

Direct extraction of the .RSA file from the APK’s META‑INF directory.

Automated detection of the app’s protection scheme.

Mitigation strategies presented are:

Use native verification of the certificate hash (MD5/SHA‑1) rather than Java checks.

Invoke hidden Android classes such as PackageParser.collectCertificates to obtain the original certificate.

Employ OpenSSL in native code to parse the PKCS#7 container, despite increased binary size.

Manually locate and read the CERT.RSA file, then verify its public‑key information using a PKCS7 library.

Implement a custom verification flow (e.g., custom CERT placed in assets) that bypasses Android’s built‑in signature mechanism.

By moving critical verification logic to the native layer and employing multiple independent checks, developers can raise the difficulty for attackers attempting to bypass or tamper with Android app signatures.

NativeAndroidsecurityJNISignature Verification
JD Retail Technology
Written by

JD Retail Technology

Official platform of JD Retail Technology, delivering insightful R&D news and a deep look into the lives and work of technologists.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.