Android Studio NDK 入门教程(6)--JNI签名验证防止恶意调用

概述

根据前面的文章来看,JNI其实只实现了关键代码加密,如果别人拿到了你的Java Native方法定义和对应的so,即可完成对你so里方法的调。因为native 方法和类都是不能混淆的,混淆了方法的函数名就变了,调用的时候就找不到方法了,因此如果反编译APK可以非常容易拿到相关文件和代码。 显然我们需要一些手段来在JNI的验证请求接口的是不是我们的程序。

签名验证的原理

可以用如下图来表明加了验证之后调用JNI的逻辑,用一个isValid 来表明请求的应用是不是我们自己的应用。isValid 通过init 去初始化。
这里写图片描述

如何判别调用者的有效性

直接有效的方案就是使用签名进行判定,如果你的keystore没有泄漏,第三方破解概率几乎为零。当然这都是相对的,任何防护都会有破绽。大多数第三方的Android SDK也都通过签名来判断申请的key是否用在了你申请的应用上。因此在大多数SDK申请key的时候会让你填写SHA1,因为在程序运行的时候SDK会获取你签名的SHA1去向服务器验证,你申请的appkey和SHA1是否正确。

签名验证的实现

在代码中获取签名的SHA1

先尝试在Java代码中获取签名的SHA1

    private String getCertSHA1(Context context)
    {
        try
        {
            //获取包管理器
            PackageManager packageManager=context.getPackageManager();
            //获取包名
            String packageName=context.getPackageName();
            //获得包信息
            PackageInfo   pis = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
            //获得签名
            Signature[] signs = pis.signatures; //签名
            //获得签名数组的第一位
            Signature sign=signs[0];
            //获得X.509证书工厂
            CertificateFactory certFactory = CertificateFactory.getInstance("X.509");

            byte[] signBytes=sign.toByteArray();
            ByteArrayInputStream byteIn=new ByteArrayInputStream(signBytes);
            //获取X509证书
            X509Certificate cert = (X509Certificate) certFactory.generateCertificate(byteIn);
            //获取证书发行者SHA1
            MessageDigest sha1=MessageDigest.getInstance("SHA1");
            byte[] certByte=cert.getEncoded();
            byte[] bs=sha1.digest (certByte);
            return toHex(bs);
        }
        catch (CertificateException e)
        {

            e.printStackTrace();
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        return null;
    }

    //将Byte转换成HexString 辅助函数
     char[] cs=new char[16];
    {
        for (int i=0;i < 10;i++)
        {
            cs[i] = (char) ('0' + i);
        }
        for (int i=10;i < 16;i++)
        {
            cs[i] = (char) ('A' + i - 10);
        }
    }
    String toHex(byte[] bs)
    {
        char[] cs=new char[bs.length * 2];
        int x;
        for (int i=0;i < bs.length;i++)
        {
            x = bs[i] & 0xff;
            cs[2 * i] = this.cs[x / 16];
            cs[2 * i + 1] = this.cs[x % 16];
        }
        return new String(cs);
    }

这里使用的是debug.keystore
LogCat输出:
LogCat输出
Keytool输出:
控制台输出
可以看到我们获取的SHA1是正确的。

注意:这里的signature只与签名时使用的证书有关系,这里生成的SHA1,也可以通过APK包中的CERT.RSA(解压APK之后,META-INF文件夹中)文件得到。
CERT.RSA

在JNI中获取签名SHA1

我并没有在Android的C库中找到类似于PackageManager之类的东西,所以任然采用JNI的反射机制去调用相关的Java方法,其实就是将上面的Java方法翻译成C++代码。

    //Native 方法声明,显然这里需要传递一个Context过去用于获取包管理器以及包名
   public static native void native_init(Context context);
 //C++实现
static bool is_valid= false;
const char *app_signature_sha1="40C438E3DAC29E04718E141BD816B3FC1A53E389";
const char HexCode[]={'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
JNIEXPORT void JNICALL Java_com_wastrel_signtest_NativeFunc_native_1init
        (JNIEnv *env, jclass clz, jobject context_object){
    jclass context_class = env->GetObjectClass(context_object);

    //context.getPackageManager()
    jmethodID methodId = env->GetMethodID(context_class, "getPackageManager", "()Landroid/content/pm/PackageManager;");
    jobject package_manager_object = env->CallObjectMethod(context_object, methodId);
    if (package_manager_object == NULL) {
       LOGE("getPackageManager() Failed!");
        return;
    }

    //context.getPackageName()
    methodId = env->GetMethodID(context_class, "getPackageName", "()Ljava/lang/String;");
    jstring package_name_string = (jstring)env->CallObjectMethod(context_object, methodId);
    if (package_name_string == NULL) {
       LOGE("getPackageName() Failed!");
        return ;
    }
    env->DeleteLocalRef(context_class);

    //PackageManager.getPackageInfo(Sting, int)
    //public static final int GET_SIGNATURES= 0x00000040;
    jclass pack_manager_class = env->GetObjectClass(package_manager_object);
    methodId = env->GetMethodID(pack_manager_class, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
    env->DeleteLocalRef(pack_manager_class);
    jobject package_info_object = env->CallObjectMethod(package_manager_object, methodId, package_name_string, 0x40);
    if (package_info_object == NULL) {
        LOGE("getPackageInfo() Failed!");
        return ;
    }
    env->DeleteLocalRef(package_manager_object);

    //PackageInfo.signatures[0]
    jclass package_info_class = env->GetObjectClass(package_info_object);
    jfieldID fieldId = env->GetFieldID(package_info_class, "signatures", "[Landroid/content/pm/Signature;");
    env->DeleteLocalRef(package_info_class);
    jobjectArray signature_object_array = (jobjectArray)env->GetObjectField(package_info_object, fieldId);
    if (signature_object_array == NULL) {
        LOGE("PackageInfo.signatures[] is null");
        return ;
    }
    jobject signature_object = env->GetObjectArrayElement(signature_object_array, 0);
    env->DeleteLocalRef(package_info_object);

    //Signature.toByteArray()
    jclass signature_class = env->GetObjectClass(signature_object);
    methodId = env->GetMethodID(signature_class, "toByteArray", "()[B");
    env->DeleteLocalRef(signature_class);
    jbyteArray signature_byte = (jbyteArray) env->CallObjectMethod(signature_object, methodId);

    //new ByteArrayInputStream
    jclass byte_array_input_class=env->FindClass("java/io/ByteArrayInputStream");
    methodId=env->GetMethodID(byte_array_input_class,"<init>","([B)V");
    jobject byte_array_input=env->NewObject(byte_array_input_class,methodId,signature_byte);

    //CertificateFactory.getInstance("X.509")
    jclass certificate_factory_class=env->FindClass("java/security/cert/CertificateFactory");
    methodId=env->GetStaticMethodID(certificate_factory_class,"getInstance","(Ljava/lang/String;)Ljava/security/cert/CertificateFactory;");
    jstring x_509_jstring=env->NewStringUTF("X.509");
    jobject cert_factory=env->CallStaticObjectMethod(certificate_factory_class,methodId,x_509_jstring);

    //certFactory.generateCertificate(byteIn);
    methodId=env->GetMethodID(certificate_factory_class,"generateCertificate",("(Ljava/io/InputStream;)Ljava/security/cert/Certificate;"));
    jobject x509_cert=env->CallObjectMethod(cert_factory,methodId,byte_array_input);
    env->DeleteLocalRef(certificate_factory_class);

    //cert.getEncoded()
    jclass x509_cert_class=env->GetObjectClass(x509_cert);
    methodId=env->GetMethodID(x509_cert_class,"getEncoded","()[B");
    jbyteArray cert_byte=(jbyteArray)env->CallObjectMethod(x509_cert,methodId);
    env->DeleteLocalRef(x509_cert_class);

    //MessageDigest.getInstance("SHA1")
    jclass message_digest_class=env->FindClass("java/security/MessageDigest");
    methodId=env->GetStaticMethodID(message_digest_class,"getInstance","(Ljava/lang/String;)Ljava/security/MessageDigest;");
    jstring sha1_jstring=env->NewStringUTF("SHA1");
    jobject sha1_digest=env->CallStaticObjectMethod(message_digest_class,methodId,sha1_jstring);

    //sha1.digest (certByte)
    methodId=env->GetMethodID(message_digest_class,"digest","([B)[B");
    jbyteArray sha1_byte=(jbyteArray)env->CallObjectMethod(sha1_digest,methodId,cert_byte);
    env->DeleteLocalRef(message_digest_class);

    //toHexString
    jsize array_size=env->GetArrayLength(sha1_byte);
    jbyte* sha1 =env->GetByteArrayElements(sha1_byte,NULL);
    char *hex_sha=new char[array_size*2+1];
    for (int i = 0; i <array_size ; ++i) {
        hex_sha[2*i]=HexCode[((unsigned char)sha1[i])/16];
        hex_sha[2*i+1]=HexCode[((unsigned char)sha1[i])%16];
    }
    hex_sha[array_size*2]='\0';
    LOGE(" %s ",hex_sha);
    //比较签名
    if (strcmp(hex_sha,app_signature_sha1)==0)
    {
       LOGE("验证通过");
        is_valid= true;
    } else{
        ThrowRuntimeExcption(env,"验证失败");
    }
    return ;
}

运行结果:
运行结果

显然我们的结果达到了。在JNI中获得了签名的SHA1。验证通过之后isValid=true。别的JNIFunc调用的时候先判断一下isValid的值再操作即可。当应用签名和so里预置签名不一致时将无法获得正确结果。

其实代码不用这么长

从上面的Java代码或C++代码都可以看到,从CertificateFactory开始其实签名的元数据已经没有改变。因此其实我们不必大废周章的通过CertificateFactoryMessageDigest两个类去求签名的SHA1值。其实直接比较元数据即可完成验证。那么我们如何获得签名的元数据呢?

  • 最简单的办法就是用发布的keystore签名APk,然后在Java代码中打印一次signature.toCharsString,然后从Logcat中拷贝出来。
  • 使用keytool工具提取。
    • 使用命令keytool -list -rfc -keystore your.keystore
      命令结果
    • 拷贝-----BEGIN CERTIFICATE----------END CERTIFICATE-----之间的内容。很显然这是一段Base64,然后使用在线解码工具即可完成解码,注意要勾选结果使用16进制显示。
      Base64解码
    • 去掉空格和\x等垃圾数据。

修改签名常量为获得的字符串:

const char *app_signature= "308201dd30820146020101300d06092a864886"  "f70d010105050030373116301406035504030c0d416e64726f6964204465"
"6275673110300e060355040a0c07416e64726f6964310b30090603550406"
"13025553301e170d3136303732363134323834315a170d34363037313931"
"34323834315a30373116301406035504030c0d416e64726f696420446562"
"75673110300e060355040a0c07416e64726f6964310b3009060355040613"
"02555330819f300d06092a864886f70d010101050003818d003081890281"    "8100879fe1f34241cccb893c9a97fee78b89c74836086cd475145e32a2b37"
"dc149a2a141c5924aa877b8571defb43bfc0fa3182f5f888a7a063186af5"
"13a0afdd0a874c8d201656f6453bdb11e6fd2a3f59491c79399f3b73e611"
"cc0ddba58e30bdce9a12aaf63d298fb9c87675570a339e7fcf896edde5fb"
"b5236b6f6eff954e4330203010001300d06092a864886f70d01010505000"
"38181002f6cd4d87d190edd21964dcbdf0c5f27225737d85a9501f1601f7"
"d20dc00182504288871356383f7d4f01cc031c9a4faf395f210385aad0197"
"f98031e259ca69746f47768b077e9f0f965bc008b961fdd0747a2affa147f"
"707703123e1d0346e9f2f4fda391217a0fdeae4c1f842ccdfff4d346c74d"
"9e92b6f2c617c8cb4958f";

然后修改C++方法中的//Signature.toByteArray()注释以下的内容:

    jclass signature_class = env->GetObjectClass(signature_object);
    methodId = env->GetMethodID(signature_class, "toCharsString", "()Ljava/lang/String;");
    env->DeleteLocalRef(signature_class);
    jstring signature_jstirng = (jstring) env->CallObjectMethod(signature_object, methodId);

    const  char *sign=env->GetStringUTFChars(signature_jstirng,NULL); 
    if (strcmp(sign,app_signature)==0)
    {
       LOGE("验证通过");
        is_valid= true;
    } else{
        ThrowRuntimeExcption(env,"验证失败");
    }
    return;

总结

libxxx.so认证,其实只是利用了Android签名的安全性来完成验证。本文大量从C++中调用了Java方法,包括静态方法,非晶态方法等等。

资源地址:http://download.csdn.net/detail/venusic/9615261

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
### 回答1: Android C 高级编程是指在Android开发中使用C语言进行高级编程的技术。而使用NDK(Native Development Kit)可以使开发者在Android应用中使用C/C++等本地语言进行编程。 NDK是一个工具集,它允许开发者在Android应用中嵌入本地代码,并且提供了一系列的开发工具和库,以便开发者能够在Android应用中使用C/C++进行高级编程。使用NDK可以提供更高的性能和更低的内存占用,适用于需要处理大量数据和高性能计算的应用场景。 在使用NDK进行Android C高级编程时,可以使用PDF(Portable Document Format)作为文档格式,以便对代码和项目进行更好的管理和文档化。在NDK的开发过程中,可以使用PDF文档记录关键的设计思路、代码逻辑、接口定义等信息,以方便团队协作和后续的维护。 使用NDK进行Android C高级编程的步骤大致如下: 1. 准备开发环境:安装NDK并配置好开发环境,包括设置NDK的路径和编译器等。 2. 创建新项目:使用Android Studio创建一个新的Android项目,并在项目中引入NDK的支持。 3. 编写C代码:使用C/C++语言编写需要调用的函数、算法或者数据结构等代码,并将其保存在适当的目录下。 4. 编写JNI接口:在生成的Java代码中,使用JNI(Java Native Interface)定义对应C代码的接口,以便在Java层调用C代码。 5. 编译和构建:使用NDK的工具集进行编译和构建,将C代码编译成适合Android平台使用的库文件(.so文件)。 6. 在Java代码中调用C代码:在需要调用C代码的地方,使用JNI接口调用对应的C函数,以实现和C代码的交互和调用。 使用PDF文档进行文档化可以帮助开发者更好地组织和管理代码、接口和设计文档等,方便后续的代码维护和项目协作。同时,也可以作为项目的参考文档,方便其他开发人员了解和使用项目。 ### 回答2: Android C 高级编程是针对使用NDK(Native Development Kit)的一种高级编程技术。NDKAndroid开发工具包中的一个工具,允许开发者使用C、C++或其他本地编程语言编写Android应用程序的部分或全部代码。 使用NDK进行Android C高级编程有许多优点。首先,NDK提供了更高的性能和更好的控制权,特别是在处理图形、音频和计算密集型任务时。通过使用本地编程语言,开发者能够更好地利用底层系统资源,提高应用程序的执行效率和速度。 其次,NDK还提供了对现有C和C++库的支持。这意味着开发者可以使用许多已经存在的库和功能来加快开发进程。无需重新编写现有的代码,直接使用NDK与这些库进行集成即可。 在使用NDK进行Android C高级编程时,一种常见的用途是开发游戏。使用C或C++编写游戏代码可以获得更好的性能和更低的延迟,这对于游戏的流畅运行至关重要。 此外,开发者还可以使用NDK为现有的Java应用程序添加本地本地扩展。这样可以通过使用C或C++编写某些关键组件,以改进应用程序的性能或添加新的功能。 总的来说,通过使用NDK进行Android C高级编程,开发者可以获得更高的性能、更好的控制权和更好的资源利用。无论是开发游戏还是优化应用程序,使用NDK都是提高性能和扩展功能的好方法。通过阅读相关的PDF文档,开发者可以更深入地了解如何使用NDK进行Android C高级编程。 ### 回答3: Android NDK (Native Development Kit) 是一个用于开发 Android 应用程序的工具集,它使开发者能够使用 C 或 C++ 编写原生代码,并将其与 Java 编写的 Android 应用程序一起使用。使用 NDK 可以达到增加性能、复用现有的 C/C++ 代码以及访问底层硬件等目的。 在 Android C 高级编程中,使用 NDK 商用 PDF 库可以实现在 Android 应用程序中处理 PDF 文件的功能。PDF 文件是一种常见的电子文档格式,使用 PDF 库可以读取、编辑和生成 PDF 文件。 使用 NDK 进行 PDF 处理的一般步骤如下: 1. 集成 PDF 库:首先,需要将商用的 PDF 库 (.so 文件) 集成到 Android 项目中。可以通过在 Android.mk 文件中添加相关配置,确保 .so 文件正确地被编译和链接到应用程序中。 2. 创建 JNI 接口:为了在 Java 层与 C/C++ 层之间进行通信,需要创建 JNI (Java Native Interface) 接口。可以在创建 JNI 方法时使用 JNAerator 或者手动编写 JNI 代码,以便在 Java 层调用 C/C++ 的功能。 3. 对 PDF 文件进行处理:在 C/C++ 层,可以使用 PDF 库提供的功能来处理 PDF 文件。例如,可以使用库提供的函数来解析、渲染、添加标注、提取内容等。 4. 将数据返回给 Java 层:在 C/C++ 层处理完之后,可以通过 JNI 接口将处理后的数据返回给 Java 层。这样就可以在 Android 应用程序中显示或者存储处理后的 PDF 文件。 需要注意的是,在使用商用 PDF 库时,需要遵循相关的许可协议,并确保在开发和分发过程中合法使用该库。 总之,通过使用 NDK 和商用 PDF 库,可以使 Android 应用程序具有处理 PDF 文件的高级编程能力。同时,开发者需要具备 C/C++ 编程和 JNI 接口的使用经验,以便顺利地进行开发工作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值