看点
01
一. Android签名背景
看点
02
二. 安全目标
通常定义的信息安全主要有三大目标:
除了这三点,有时大家也会加上另外两点要求:
看点
03
三. JNI注册方式
JNI全称是Java Native Interface(Java本地接口)单词首字母的缩写,本地接口就是指用C和C++开发的接口。由于JNI是JVM规范中的一部份,因此可以将我们写的JNI程序在任何实现了JNI规范的Java虚拟机中运行。同时,这个特性使我们可以复用以前用C/C++写的大量代码。JNI目前提供两种注册方式,静态注册方式实现较为简单,但有一些系列的缺陷,动态注册要复写JNI_OnLoad函数,过程稍微复杂。
这种方法我们比较常见,但比较麻烦,大致流程如下:
1. 先创建Java类,声明Native方法,编译成.class文件。
2. 使用Javah命令生成C/C++的头文件,例如:javah -jni com.jd.jnidemo.MainActivity
,则会生成一个以.h为后缀的文件com_jd_jnidemo_MainActivity.h
。
3. 创建.h对应的源文件,然后实现对应的native方法,如下图所示:
3. 静态注册的弊端
1. 书写很不方便,因为JNI层函数的名字必须遵循特定的格式,且名字特别长;
2. 会导致程序员的工作量很大,因为必须为所有声明了native函数的java类编写JNI头文件;
3. 程序运行效率低,因为初次调用native函数时需要根据根据函数名在JNI层中搜索对应的本地函数,然后建立对应关系,这个过程比较耗时。
动态注册在JNi层实现的,JAVA层不需要关心,因为在system.load时就会去掉JNI_OnLoad,有就注册,没就不注册,因为jni.h里有这么一个结构体,分别如下表示
typedef struct {
const char* name; java层函数名
注:一个签名信息包含JAVA的参数和返回,这个貌似有命令生成javap,应该是
const char* signature; java层函数名的签名信息
void* fnPtr; Jni层对应的函数指针。
} JNINativeMethod;
看点
04
四. 签名验证
一般情况下为了防止被反编译,会把关键代码写到so文件中(比如加解密),一般使用到的是在so里加上判断APk包签名是否一致的代码,避免so被二次打包。其实用JNI读签名就是用了Java的反射机制。
反射代码如下所示:
PackageManager pm = context.getPackageManager();
String packageName = context.getPackageName();
try {
PackageInfo packageInfo = pm.getPackageInfo(packageName,
PackageManager.GET_SIGNATURES);
Signature sign = info.signatures[0];
Log.i("test", "hashCode : " + sign.hashCode());
} catch (NameNotFoundException e) {
e.printStackTrace();
}
以上我们做了一件事情,获取 PackageInfo 中的 Signature。当然也可以继续获取公钥SHA1如下
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 (PackageManager.NameNotFoundException | CertificateException |
NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
计算出 Signature或计算出SHA1 之后,我们就可以进行对比了。下面我们看看对应的 native 代码。(由于篇幅原因这里列举只计算到Signature的过程)
int getSignHashCode(JNIEnv *env, jobject context) {
jclass context_clazz = (*env)->GetObjectClass(env, context);//Context的类
jmethodID methodID_getPackageManager = (*env)->GetMethodID(env, context_clazz,
"getPackageManager", "()Landroid/content/pm/PackageManager;");// 得到 getPackageManager 方法的 ID
jobject packageManager = (*env)->CallObjectMethod(env, context,
methodID_getPackageManager);// 获得PackageManager对象
jclass pm_clazz = (*env)->GetObjectClass(env, packageManager);// 获得 PackageManager 类
jmethodID methodID_pm = (*env)->GetMethodID(env, pm_clazz, "getPackageInfo",
"(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");// 得到 getPackageInfo 方法的 ID
jmethodID methodID_pack = (*env)->GetMethodID(env, context_clazz,
"getPackageName", "()Ljava/lang/String;");// 得到 getPackageName 方法的 ID
jstring application_package = (*env)->CallObjectMethod(env, context,
methodID_pack);// 获得当前应用的包名
const char *str = (*env)->GetStringUTFChars(env, application_package, 0);
__android_log_print(ANDROID_LOG_DEBUG, "JNI", "packageName: %s\n", str);
jobject packageInfo = (*env)->CallObjectMethod(env, packageManager,
methodID_pm, application_package, 64);// 获得PackageInfo
jclass packageinfo_clazz = (*env)->GetObjectClass(env, packageInfo);
jfieldID fieldID_signatures = (*env)->GetFieldID(env, packageinfo_clazz,
"signatures", "[Landroid/content/pm/Signature;");
jobjectArray signature_arr = (jobjectArray)(*env)->GetObjectField(env,
packageInfo, fieldID_signatures);
jobject signature = (*env)->GetObjectArrayElement(env, signature_arr, 0);//Signature数组中取出第一个元素
jclass signature_clazz = (*env)->GetObjectClass(env, signature);//读signature的hashcode
jmethodID methodID_hashcode = (*env)->GetMethodID(env, signature_clazz,
"hashCode", "()I");
jint hashCode = (*env)->CallIntMethod(env, signature, methodID_hashcode);
__android_log_print(ANDROID_LOG_DEBUG, "JNI", "hashcode: %d\n", hashCode);
return hashCode;
}
本示例中这种认证方式在 Android Studio 中会有一个 Lint 警告,“android-fake-id-vulnerability”,受影响系统版本:部分Android 4.4及所有4.4以下版本,这个问题属于系统bug,在获取cert的方法findCert中判断有缺陷,但在4.4以后大google已经对此修复。
示例中需要传入context,其实context也也可以在native层通过反射的方式拿到,本人感受:native的代码不难就是写起来比较复杂,只要有耐心就可以了。
4.1内容是通过context获取的signature获取的签名验证,我们知道在签名后apk文件会多出以下文件
其中我们上述过程其实就获取CERT.RSA中的签名,但上述过程依赖context进行依赖认证,攻击者可获取context进行内容替换修改,截取签名替换等方式显示二次打包。
所以,我们可以解析RSA文件,通过本地验证的方式来完成 '证书完整性校验' 。
查看证书指纹.keystores命令
keytool –list –v –keystore debug.keystore
查看证书指纹.RSA文件命令
keytool –printcert –file CERT.RSA
使用openssl查看.RSA文件
openssl pkcs7 -inform DER -in CERT.RSA -noout -print_certs –text
查看证书指纹后会发现,RSA文件和.keystores,证书指纹相同,MD5,SHA1,SHA256三种指纹均相同。
1.X.509证书格式如下图所示:
• 这里看到证书中并不包含apk签名流程中生成CERT.RSA时对用私钥计算出的签名。所以证书的信息是不会改变的,这也验证了上面所说的RSA中证书指纹和.keystone中的指纹相同的问题
2.对CERT.RSA进行详细解析
明确了上面的问题之后,对CERT.RSA 文件进行详细解析,得到下图:
说明:
openssl pkcs7 -inform DER -in CERT.RSA -print_certs
由于Android生成的apk文件是以zip文件格式生成的,我们可以查看源码查看Android签名校验机制
可参考:Apk在安装的过程中核心类: frameworks\base\services\core\java\com\android\server\pm\PackageManagerService.java
private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {
……
}
Apk 包中的META-INF目录下,CERT.RSA,它是一个PKCS7 格式的文件。
获取证书的方法如下(上面几张中已经使用openssl获取相关信息):
import sun.security.pkcs.PKCS7; //注意:需要引入jar包android-support-v4
import java.io.FileInputStream;
import java.io.IOException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
public class Test {
public static void main(String[] args) throws CertificateException, IOException {
FileInputStream fis = new FileInputStream("/home/AnyMarvel/CERT.RSA");
PKCS7 pkcs7 = new PKCS7(fis);
X509Certificate publicKey = pkcs7.getCertificates()[0];
System.out.println("issuer1:" + publicKey.getIssuerDN());
System.out.println("subject2:" + publicKey.getSubjectDN());
System.out.println(publicKey.getPublicKey());
}
}
也可以转化为native代码进行校验,加固安全性。以上就是目前主流的两种通过签名校验的方式。
看点
05
五. 常见的破解方式及加固方案总结
破解条件
getPackageManager().getPackageInfo.signatures 获取签名信息;
破解方式
pp.collectCertificates(pkg, parseFlags);
pp.collectManifestDigest(pkg);
修改实现方式。
(具体实现请参考LuckyPatchSign实现 新浪微博 @人生无NG 2015.10.10)
应对方案
通过以上信息,我们可以得到的是,证书作为不变的内容放在PKCS7格式的.RSA文件中,我们在RSA文件上验证的也只有证书。
context.getPackageManager().getPackageInfo(
this.getPackageName(), PackageManager.GET_SIGNATURES).signatures)