校验数字签名防止 apk 被二次打包 - Java层校验(大众点评为例)

原文链接:  https://kiya-z.github.io/2015/11/12/check-signature-for-avoiding-fake-app-java-level-check/
测试环境
Ubuntu 14.04
Lenovo Android 5.1
Lenovo Android 4.2.2
Android Studio

普及签名包名知识

包名 (Package Name) 相当于「应用的身份证」,是系统用来区分不同应用的字段,重复的包名会被认为是同一款应用。
签名文件相当于「开发者的身份证」,目的是为了检验应用是否被人更改过(应用必须签名过才能正常安装)。

包名相同签名相同时,会发生 替换安装 / 应用升级;
包名相同签名不同时,安装失败;
包名不同签名相同时,相当于同一开发者的两个应用,互相不冲突。

签名的注意事项
所有的Android应用都必须有数字签名,没有不存在数字签名的应用,包括模拟器上运行的。Android系统不会安装没有数字证书的应用。
签名的数字证书不需要权威机构来认证,是开发者自己产生的数字证书,即所谓的自签名。
正式发布一个Android应用时,必须使用一个合适的私钥生成的数字证书来给程序签名,不能使用ADT插件或者ANT工具生成的调试证书来发布。
Android将数字证书用来标识应用程序的作者和在应用程序之间建立信任关系,而不是用来决定最终用户可以安装哪些应用程序。


为大众点评换签名

按照常规步骤使用 apktool + signapk 反编译、编译、签名并安装到手机上(没有修改任何代码),打开 app 选择城市后界面如下图并很快退出:

dianping-crash

说明点评对签名进行了校验 。


分析校验方法

怎么退出的?

打开 apktool 反编译得到的文件夹下的 AndroidManifest.xml ,得到程序包名:com.dianping.v1 。
清除大众点评的数据,打开 as,连上手机,log 的过滤条件设为 com.dianping ,在选择城市之前清一下 log ,在 log 里搜索 “die”,比较明显的是有四处:

进程死亡:

1
2
3
Process com.dianping.v1 (pid 19182) has died
Process com.dianping.v1 (pid 19586) has died
Process com.dianping.v1 (pid 19650) has died

app 死亡:

1
Force removing ActivityRecord{266e5efd u0 com.dianping.v1/.NovaMainActivity t14010}: app died, no saved state

其中前两个进程死亡之后都有开启进程的操作,说明第一次校验失败后重试了两次:

1
startProcess: name=com.dianping.v1 app=null knownToBeDead=true thread=null pid=-1

1
startProcess: name=com.dianping.v1 app=null knownToBeDead=true thread=null pid=-1

最后一个直接杀死了 app,没有再继续创建进程。

在进程结束之前,发生错误的调用记录:

1
2
3
4
5
6
9586-19586/? D/AccessibilityManager:     at com.dianping.base.app.NovaActivity.setContentView(NovaActivity.java:722)
 9586-19586/? D/AccessibilityManager:     at com.dianping.main.guide.MainActivity.setOnContentView(MainActivity.java:339)
 9586-19586/? D/AccessibilityManager:     at com.dianping.base.basic.FragmentTabActivity.onCreate(FragmentTabActivity.java:51)
 9586-19586/? D/AccessibilityManager:     at com.dianping.base.widget.NovaFragmentTabActivity.onCreate(NovaFragmentTabActivity.java:26)
 9586-19586/? D/AccessibilityManager:     at com.dianping.main.guide.MainActivity.onCreate(MainActivity.java:169)
 9586-19586/? D/AccessibilityManager:     at com.dianping.v1.NovaMainActivity.onCreate(NovaMainActivity.java:15)

代码探索

解压 apk 文件,发现有 3 个 dex 文件,先拿第一个下手,JD-GUI 打开发现代码没有混淆!

调用记录中的文件从上往下过一遍,发现在 com.dianping.main.guide.MainActivity.onCreate() 方法中有校验签名的函数:

1
2
3
if (!checkSignature()) {    
      Process.killProcess(Process.myPid());
    }

checkSignature 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private boolean checkSignature()
{
 try
 {
   Signature[] arrayOfSignature = getPackageManager().getPackageInfo(getPackageName(), 64).signatures;     //获得签名数组
   if (arrayOfSignature != null)
   {
     if (arrayOfSignature.length == 0) {
       return false;
     }
     int j = arrayOfSignature.length;
     int i = 0;
     while (i < j)   //如果数组中的某个元素值与 'ac6fc3fe' 相等,返回校验成功;如果直到结束也没有相等的元素,返回失败
     {               //只比较一个特定的元素,可能也是为了不把整个签名泄露出来,同时也做到了一定程度的校验
       String str = Integer.toHexString(arrayOfSignature[i].toCharsString().hashCode());
       if (!"ac6fc3fe".equalsIgnoreCase(str))   
       {
         boolean bool = "600cf559".equalsIgnoreCase(str);       //这个比较好像没用
         if (!bool) {}
       }
       else
       {
         return true;
       }
       i += 1;
     }
   }
   return false;
 }
 catch (Exception localException) {}
}

相关 API:

public Signature[] signatures
Array of all signatures read from the package file. This is only filled in if the flag GET_SIGNATURES was set.

public static final int GET_SIGNATURES
PackageInfo flag: return information about the signatures included in the package.
Constant Value: 64 (0x00000040)

public boolean equalsIgnoreCase (String string)
Compares the given string to this string ignoring case.
The strings are compared one char at a time.

流程修改

在 smali/com/dianping/main/guide/MainActivity.smali 中搜索 ac6fc3fe :

1
2
3
4
5
6
7
8
9
.line 358
    .local v4, "myHash":Ljava/lang/String;
    const-string v9, "ac6fc3fe"

    invoke-virtual {v9, v4}, Ljava/lang/String;->equalsIgnoreCase(Ljava/lang/String;)Z

    move-result v9

    if-nez v9, :cond_2      //if(!equal(..)) return 1

找到 con_2 的定义:

1
2
3
4
5
.line 359
:cond_2
const/4 v8, 0x1

goto :goto_0

1
2
:goto_0
    return v8

所以把 if-nez v9, :cond_2 改成 if-eqz v9, :cond_2 就可以了,当然,修改方法还有很多。

打包签名

点评可以正常打开,正常登录,正常使用了。


番外:
而另一台手机 (Lenovo Android 4.2.2) 测试进程会不断重新创建。
应用 crash 之后 App 对应的 Process 都被杀死,然后安排重启 Service,重新启动 Task 栈顶的 Activity 。


Reference

Android软件安全与逆向分析
洗白白手记:绕开 Android 应用开发的那些「坑」
给 Android 应用程序签名