Android提高 - Android 数字签名学习

来源:互联网 发布:js输入框不可编辑 编辑:程序博客网 时间:2024/05/18 03:57

在Android系统中,所有安装到系统的应用程序都必有一个数字证书,此数字证书用于标识应用程序的作者和在应用程序之间建立信任关系,如果一个permission的protectionLevel为signature,那么就只有那些跟该permission所在的程序拥有同一个数字证书的应用程序才能取得该权限。Android使用Java的数字证书相关的机制来给apk加盖数字证书,要理解android的数字证书,需要先了解以下数字证书的概念和java的数字证书机制。Android系统要求每一个安装进系统的应用程序都是经过数字证书签名的,数字证书的私钥则保存在程序开发者的手中。Android将数字证书用来标识应用程序的作者和在应用程序之间建立信任关系,不是用来决定最终用户可以安装哪些应用程序。这个数字证书并不需要权威的数字证书签名机构认证,它只是用来让应用程序包自我认证的。

同一个开发者的多个程序尽可能使用同一个数字证书,这可以带来以下好处。

1)、有利于程序升级,当新版程序和旧版程序的数字证书相同时,Android系统才会认为这两个程序是同一个程序的不同版本。如果新版程序和旧版程序的数字证书不相同,则Android系统认为他们是不同的程序,并产生冲突,会要求新程序更改包名。

2)、有利于程序的模块化设计和开发。Android系统允许拥有同一个数字签名的程序运行在一个进程中,Android程序会将他们视为同一个程序。所以开发者可以将自己的程序分模块开发,而用户只需要在需要的时候下载适当的模块。

3)、可以通过权限(permission)的方式在多个程序间共享数据和代码。Android提供了基于数字证书的权限赋予机制,应用程序可以和其他的程序共享该功能或者数据给那那些与自己拥有相同数字证书的程序。如果某个权限(permission)的protectionLevel是signature,则这个权限就只能授予那些跟该权限所在的包拥有同一个数字证书的程序。

在签名时,需要考虑数字证书的有效期

1)、数字证书的有效期要包含程序的预计生命周期,一旦数字证书失效,持有改数字证书的程序将不能正常升级。

2)、如果多个程序使用同一个数字证书,则该数字证书的有效期要包含所有程序的预计生命周期。

3)、Android Market强制要求所有应用程序数字证书的有效期要持续到2033年10月22日以后。

Android数字证书包含以下几个要点:

1)、所有的应用程序都必须有数字证书,Android系统不会安装一个没有数字证书的应用程序。

2)、Android程序包使用的数字证书可以是自签名的,不需要一个权威的数字证书机构签名认证。

3)、如果要正式发布一个Android,必须使用一个合适的私钥生成的数字证书来给程序签名,而不能使用adt插件或者ant工具生成的调试证书来发布。

4)、数字证书都是有有效期的,Android只是在应用程序安装的时候才会检查证书的有效期。如果程序已经安装在系统中,即使证书过期也不会影响程序的正常功能。

5)、Android使用标准的java工具Keytooland Jarsigner来生成数字证书,并给应用程序包签名。

6)、使用zipalign优化程序。

Android系统不会安装运行任何一款未经数字签名的apk程序,无论是在模拟器上还是在实际的物理设备上。Android的开发工具(ADT插件和Ant)都可以协助开发者给apk程序签名,它们都有两种模式:调试模式(debug mode)和发布模式(release mode)。

在调试模式下,android的开发工具会在每次编译时使用调试用的数字证书给程序签名,开发者无须关心。

当要发布程序时,开发者就需要使用自己的数字证书给apk包签名,可以有两种方法。

1)、在命令行下使用JDK中的和Keytool(用于生成数字证书)和Jarsigner(用于使用数字证书签名)来给apk包签名。

2)、使用ADT Export Wizard进行签名(如果没有数字证书可能需要生成数字证书)。

使用Keytool和Jarsigner给程序签名

命令:

keytool -genkey -v -keystore android.keystore -alias android -keyalg RSA -validity 20000

中间不换行,按下“Enter”键,并根据提示填写相关信息。该命令中,-keystoreophone.keystore表示生成的证书,可以加上路径(默认在用户主目录下);-alias ophone表示证书的别名是ophone;-keyalg RSA表示采用的RSA算法;-validity 20000表示证书的有效期是20000天。


此时,我们会在互用主目录下看到ophone.keystore,即我们刚刚创建的证书。

接着对程序进行签名:

jarsigner用法: [选项] jar 文件别名

jarsigner -verify [选项] jar 文件

执行:

jarsigner -verbose -keystore android.keystore -signedjar android123_signed.apk android123.apk 

android就可以生成签名的apk文件,这里输入文件android123.apk,最终生成android123_signed.apk为Android签名后的APK执行文件。下面提示输入的密码和keytool输入的一样就行了。(不过在我的JDK目录下没有找到jarsigner这个程序,不知道是怎么回 事)


使用ADT Export Wizard进行签名

应用程序(apk)签名,在EC中,右键单击应用程序工程,如图选择:

选择证书的存放路径,填写相关资料,完成,即可生成被签名的apk文件。如下图所示:


如上图所示,我们可以看到也可以在这里选择“Create new keystore”来创建一个证书。输入密码,点击下一步,填写相关信息,如下图所示。

使用zipalign优化APK

根据官方文档的描述,Android系统中Application的数据都保存在它的APK文件中,同时可以被多个进程访问,安装的过程包括如下几个步骤:

1)、Installer通过每个apk的manifest文件获取与当前应用程序相关联的permissions信息;

2)、Home application读取当前APK的Name和Icon等信息;

3)、System server将读取一些与Application运行相关信息,例如:获取和处理Application的notifications请求等;

4)、APK所包含的内容不仅限于当前Application所使用,而且可以被其它的Application调用,提高系统资源的可复用性。

zipalign优化的最根本目的是帮助操作系统更高效率的根据请求索引资源,将resource-handling code统一将Data structure alignment(数据结构对齐标准:DSA)限定为4-byteboundaries。如果不采取对齐的标准,处理器无法准确和快速的在内存地址中定位相关资源。目前的系统中使用fallback mechanism机制处理那些没有应用DSA标准的应用程序,这的确大大的方便了普通开发者无需关注繁琐的内存操作问题。但是相反,对于这样的应用程序将给普通用户带来一定的麻烦,不但影响程序的运行的效率,而且使系统的整体执行效率下降和占用大量不必要的内存资源,甚至消耗一定的电池资源(battery life)。

命令行方式手动优化

利用tools文件夹下的zipalign工具。首先调出cmd命令行,然后执行:

zipalign -v 4 source.apk androidres.apk

这个方法不受APILevel的限制,可以对任何版本的APK执行Align优化。同时可以利用zipalign工具检查当前APK是否已经执行过Align优化。命令:

zipalign -c -v 4 androidres.apk

使用ADT自动优化:

从ADT 0.9.3版本开始,可以通过export wizard自动对发布的application packages执行align操作。设置方法:鼠标右键点击Project,然后选择Android Tools >ExportSigned Application Package

综上所述,可以使用KeytoolJarsignerzipalign给程序签名并优化程序,这样就需要三个不同的工具:

keytool -genkey -v -keystore android.keystore -alias android -keyalg RSA -validity 20000jarsigner -verbose -keystore android.keystore -signedjar android123_signed.apk android123.apk androidzipalign -v 4 android123_signed.apk android123_signed_aligned.apk

当然,也可以通过ADT插件中Export Signed Application Package执行,图形界面更为简单、形象、直观。

APK文件签名keytool&jarsigner

1、生成密钥库

将位置定位在jdk的bin文件中,输入以下命名行:

keytool -genkey -alias ChangeBackgroundWidget.keystore -keyalg RSA -validity 20000 -keystore ChangeBackgroundWidget.keystore

中间不换行,按下“Enter”键,并根据提示填写相关信息(validity为有效期,这里有效期为20000天):

输入keystore密码:再次输入新密码:您的名字与姓氏是什么?  [Unknown]:  mythlink您的组织单位名称是什么?  [Unknown]:  www.mythlink.com您的组织名称是什么?  [Unknown]:  www.mythlink.com您所在的城市或区域名称是什么?  [Unknown]:  Shen Zhen您所在的州或省份名称是什么?  [Unknown]:  Guang Dong该单位的两字母国家代码是什么  [Unknown]:  CNCN=mythlink, OU=www.mythlink.com, O=www.mythlink.com, L=Shen Zhen, ST=Guang Dong, C=CN 正确吗?  [否]:  Y输入<ChangeBackgroundWidget.keystore>的主密码        (如果和 keystore 密码相同,按回车):

2、可以生成签名的apk文件,这里输入文件ChangeBackgroundWidget.apk,最终生成ChangeBackgroundWidget_signed.apk为Android签名后的APK执行文件

输入以下命令行:

jarsigner -verbose -keystore ChangeBackgroundWidget.keystore -signedjar ChangeBackgroundWidget_signed.apk ChangeBackgroundWidget.apk ChangeBackgroundWidget.keystore

中间不换行,按“Enter”键,根据提示输入密钥库的口令短语(即密码),详细信息如下图:


输入密钥库的口令短语:   正在添加: META-INF/MANIFEST.MF   正在添加: META-INF/CHANGEBA.SF   正在添加: META-INF/CHANGEBA.RSA  正在签名: res/drawable/icon.png  正在签名: res/drawable/icon_audio.png  正在签名: res/drawable/icon_exit.png  正在签名: res/drawable/icon_folder.png  正在签名: res/drawable/icon_home.png  正在签名: res/drawable/icon_img.png  正在签名: res/drawable/icon_left.png  正在签名: res/drawable/icon_mantou.png  正在签名: res/drawable/icon_other.png  正在签名: res/drawable/icon_pause.png  正在签名: res/drawable/icon_play.png  正在签名: res/drawable/icon_return.png  正在签名: res/drawable/icon_right.png  正在签名: res/drawable/icon_set.png  正在签名: res/drawable/icon_text.png  正在签名: res/drawable/icon_xin.png  正在签名: res/layout/fileitem.xml  正在签名: res/layout/filelist.xml  正在签名: res/layout/main.xml  正在签名: res/layout/widget.xml  正在签名: res/xml/widget_info.xml  正在签名: AndroidManifest.xml  正在签名: resources.arsc  正在签名: classes.dex

在运行这种方式中,碰到过几种不同的问题:

问题:jarsigner:无法打开jar文件:ChangeBackgroundWidget.apk

将要进行签名的APK放到对应的文件下,如我把要签名的ChangeBackgroundWidget.apk放到JDK的bin文件里。

问题:jarsigner:无法对jar进行签名:java.util.zip.ZipException:invalid entry comp ressed size (expected 1598 but got 1622 bytes)

方法一:Android开发网提示这些问题主要是由于资源文件造成的,对于android开发来说应该检查res文件夹中的文件,逐个排查。这个问题可以通过升级系统的JDK和JRE版本来解决。

方法二:这是因为默认给apk做了debug签名,所以无法做新的签名这时就必须点工程右键->AndroidTools ->Export Unsigned Application Package或者从AndroidManifest.xml的 Exporting上也是一样的。然后再基于这个导出的unsigned apk做签名,导出的时候最好将其目录选在你之前产生keystore的那个目录下,这样操作起来就方便了。


在弹出的对话框中将这个未签名的apk储存在jdk的bin文件夹中,如图:


然后在运行上面的步骤即可。

APK Crack

所谓APK指的是Android操作系统的应用程序安装文件。所谓Crack,简单地理解为“破解”。我具体指的是反编译APK文件进行汇编级的代码分析,并修改或插入自己的代码,重新签名打包为APK文件,以达到改变程序原有行为的目的。由以上的说明可知,我们要Crack一个APK文件,主要流程有三步:反编译、代码分析、重新打包签名。

基本准备

我们需要一些基本的工具进行一些主要的工作。如果你是一个会做Android APK汉化的朋友,那么你应该对这些工具非常熟悉:

第一个工具是android-apktool,A tool forreengineering Android apk files 。这个工具是我们完成APK Crack的核心,利用它实现APK文件的反编译和重新打包。它是Google Code上一个非常著名的开源项目,大家可以在Google Code的网页上获取它和它的Wiki、源码及其他相关信息。网址是:http://code.google.com/p/android-apktool/ 。

第二个工具是Auto-sign。这个工具实现的是APK打包后的签名工作,属于一个小工具。

除了这些基本工具外,为了更好的分析代码,你可能还需要用到一些其他工具,例如:dex2jar和jd-gui等,这里不做详述。

反编译

如果你是一个经常汉化APK程序的朋友,那么反编译这一步你肯定不会陌生。不过,既然这篇文章侧重于基本流程讲解,那么这一步想来是不能省掉的。所以,觉得罗嗦的朋友,请跳过。首先我们需要有一个待反编译的APK。这里我自己写了一个HelloWorld的APK,代码如下:

package com.zh_weir.helloworld;import android.app.Activity;import android.os.Bundle;public class MainActivity extends Activity {/** Called when the activity is first created. */@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.main);}}

我们通过android-apktool对这个APK进行反编译。对于android-apktool的使用,我就不做太多翻译的工作,直接给出说明文档吧。简单一句话,就是命令行执行。

Apktool v1.3.2 - a tool for reengineering Android apk filesCopyright 2010 Ryszard Wi?niewski <brut.alll@gmail.com>Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)Usage: apktool [-v|--verbose] COMMAND [...]COMMANDs are:    d[ecode] [OPTS] <file.apk> [<dir>]        Decode <file.apk> to <dir>.        OPTS:        -s, --no-src            Do not decode sources.        -r, --no-res            Do not decode resources.        -d, --debug            Decode in debug mode. Check project page for more info.        -f, --force            Force delete destination directory.        -t <tag>, --frame-tag <tag>            Try to use framework files tagged by <tag>.        --keep-broken-res            Use if there was an error and some resources were dropped, e.g.:            "Invalid config flags detected. Dropping resources", but you            want to decode them anyway, even with errors. You will have to            fix them manually before building.    b[uild] [OPTS] [<app_path>] [<out_file>]        Build an apk from already decoded application located in <app_path>.        It will automatically detect, whether files was changed and perform        needed steps only.        If you omit <app_path> then current directory will be used.        If you omit <out_file> then <app_path>/dist/<name_of_original.apk>        will be used.        OPTS:        -f, --force-all            Skip changes detection and build all files.        -d, --debug            Build in debug mode. Check project page for more info.    if|install-framework <framework.apk> [<tag>]        Install framework file to your system.For additional info, see: http://code.google.com/p/android-apktool/

通过apktool dHelloWorld.apk的命令,我们就完成了一个简单的APK的反编译工作。得到了一个叫做“HelloWorld”的文件夹。你可以看见文件夹下有Manifest文件,有反编译出的res资源文件。这些东西都是平时汉化特别关心的,而不是我们要注意的重点。我们需要注意的是一个叫做“smali”的文件夹。

仔细观察,你会发现这个文件夹下的文件组织结构和我们的Android工程中java源码的组织结构几乎一致。只不过Java文件被.smali的文件取而代之了。我们用文本编辑器打开这些.smali文件,你会发现它们都是可识别的、并且非常“整齐”的文本文件,大致如下:

.class public Lcom/zh_weir/helloworld/MainActivity;.super Landroid/app/Activity;.source "MainActivity.java"# direct methods.method public constructor <init>()V    .locals 0    .prologue    .line 6    invoke-direct {p0}, Landroid/app/Activity;-><init>()V    return-void.end method# virtual methods.method public onCreate(Landroid/os/Bundle;)V    .locals 1    .parameter "savedInstanceState"    .prologue    .line 10    invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V    .line 11    const/high16 v0, 0x7f03    invoke-virtual {p0, v0}, Lcom/zh_weir/helloworld/MainActivity;->setContentView(I)V    .line 12    return-void.end method

Smali文件其实就是dalvik虚拟机运行的dex字节码文件对应的汇编文件了。如果你了解Java虚拟机的汇编语言Jasmin的话,你会发现两者的语法非常相似。关于smali的语法等问题就不深入下去了,如果你想了解更多,可以访问Google Code上Smali项目主页:http://code.google.com/p/smali/

代码分析与修改

即使你不会Jasmin语法,你也能很容易看明白上面的汇编代码。需要指出的是,apktool反编译出来的汇编代码同样也是面向对象的,而不是面向过程的。这点和C++的反汇编可能有所不同。

根据上面的代码,我们可以看出,这个MainActivity的类有两个成员方法。一个是默认的构造函数;另一个就是我们重载的OnCreate方法了。

在java汇编中,每个成员方法需要首先申明自己所使用的局部变量的个数,以便实现分配存储空间。例如OnCreate使用了一个局部变量,就声明:.locals 1 。后面则使用v0表示。

在一个非静态的成员方法中,p0代表的是这个类本身的引用,相当于this,p1开始才是函数的参数;而对于静态方法,由于没有this指针,所以p0就是函数的第一个参数。(其实本身this指针就是作为一个隐含的参数传递给非静态成员函数的)。

通过分析上面Oncreate的汇编代码,我们可以知道,它首先是调用super类的onCreate方法,然后再setContentView设置显示。其中I、V等表示的是函数的参数和返回变量的类型,这是通用做法,这里就不多做说明了。

分析到这一步,你是否发现一个问题?那就是如果我们按照同样的语法修改或者增删一个语句,是否就可以实现对程序的修改了呢?答案是肯定的。

例如,我们希望这个APK程序在运行时会弹出一个Toast,提示它被破解了。用Java的话,应该这样表述:

Toast.makeText(this, "I'm Cracked!",Toast.LENGTH_LONG).show();

而用Java汇编的话,则应该表述为这样:

const-string v0, "I\'m Cracked!"const/4 v1, 0x1invoke-static {p0, v0, v1}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;move-result-object v0invoke-virtual {v0}, Landroid/widget/Toast;->show()V

只要我们将这段代码插入到原来程序的OnCreate中,再重新打包程序,我们就能实现在这个程序运行时弹出Toast了。改之后的代码,大致如下:

# virtual methods.method public onCreate(Landroid/os/Bundle;)V    .locals 2    .parameter"savedInstanceState"    .prologue    .line 11    invoke-super {p0, p1},Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V    .line 12    const/high16 v0, 0x7f03    invoke-virtual {p0, v0},Lcom/zh_weir/helloworld/MainActivity;->setContentView(I)V    .line 14    const-string v0,"I\'m Cracked!"    const/4 v1, 0x1    invoke-static {p0, v0,v1},Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;    move-result-object v0    invoke-virtual {v0},Landroid/widget/Toast;->show()V    .line 15    return-void.end method

重新编译打包签名

修改完成后,我们就可以对这个文件夹进行编译打包了。同样,我们使用的工具是apktool。通过命令apktool b HelloWorld,就可以实现程序编译打包了。这时会在这个文件夹下生成两个文件夹:存放中间文件的文件夹build和存放最后的apk文件的文件夹dist。

如果一切顺利的话,你就可以在dist文件夹中看到我们修改后的HelloWorld.apk了。不过需要注意的是,这个APK文件是还没有签名的,所以无法安装运行。我们还需要进行最后一步,那就是对这个APK进行签名。

签名我们需要用到的工具是Auto-sign。它主要是利用批处理命令,使用signapk.jar对APK文件进行签名的。你可以用记事本打开Sign.bat,看看它的具体调用关系。关键如下:

java -jar signapk.jar testkey.x509.pem testkey.pk8 update.apkupdate_signed.apk

到此,我们对这个APK的Crack就结束了。现在在模拟器或者Android机器上安装上签好名的APK程序,运行试试。一切与我们的预期相同~

如何反破解

这篇文章本来到此就结束了。不过,对于软件安全来说,有攻就要有防才对。不然,Android整个产业链就会被这样的Crack给毁掉。由于篇幅有限,反破解方面就不做详细描述了,仅仅做一个思路上的说明,希望大家谅解。

第一种办法:将核心代码用JNI写进so库中。由于so库的反编译和破解的难度加大,所以这种方式防止反编译效果不错。缺点是,对于Java层的代码没有保护作用,同样可以被篡改。

第二种办法:在线签名比较。在程序初始化时,联网将运行的程序的签名与服务器上的官方标准签名进行比较,从而达到让反编译后的程序无法正常运行的效果。缺点是,如果此部分联网检验的代码被篡改跳过,则整套机制失效。

第三种办法:代码混淆。为了加大反编译后代码分析的难度,对代码进行混淆。缺点是,治标不治本,同样可以修改(甚至据说还有反混淆工具,没用过,不多做评论)。

这三种办法都有各自的缺点,所以单单靠某一项要实现完美的软件保护都是不可能的。不过,我们可以采用联合几种办法的方式,来增强软件保护的力度。曾经反编译过Android版的卡巴斯基,它的保护思路似乎是这样的:

代码混淆,那是必须的,不过不指望它能有很好的效果。在程序初始化时,就直接通过JNI的so库初始化程序。程序激活部分也是通过JNI联网下载文件,然后在JNI层读文件并做相应激活与否的判断的。

卡巴斯基是将大部分功能模块都放在JNI层来实现的,如果我们的程序都这样处理,耗费的精力必然很大。所以,我们只是借鉴它的思路而已。具体操作思路如下:

代码混淆。初始化时JNI层联网验证签名。验证失败则直接在JNI层退出程序。值得注意的是需要保证如果绕过JNI层的初始化,则程序无法正常启动。这点不保证的话,破解还是很容易。

转载自: http://www.blogjava.net/zh-weir/archive/2011/06/11/352099.html

Android APK 签名机制

发布过Android应用的朋友们应该都知道,Android APK的发布是需要签名的。签名机制在Android应用和框架中有着十分重要的作用。

例如,Android系统禁止更新安装签名不一致的APK;如果应用需要使用system权限,必须保证APK签名与Framework签名一致,等等。在APK Crack中,我们了解到,要破解一个APK,必然需要重新对APK进行签名。而这个签名,一般情况无法再与APK原先的签名保持一致。(除非APK原作者的私钥泄漏,那已经是另一个层次的软件安全问题了。)

简单地说,签名机制标明了APK的发行机构。因此,站在软件安全的角度,我们就可以通过比对APK的签名情况,判断此APK是否由“官方”发行,而不是被破解篡改过重新签名打包的“盗版软件”。

Android签名机制

为了说明APK签名比对对软件安全的有效性,我们有必要了解一下Android APK的签名机制。为了更易于大家理解,我们从Auto-Sign工具的一条批处理命令说起。

在APK Crack中,我们了解到,要签名一个没有签名过的APK,可以使用一个叫作Auto-sign的工具。Auto-sign工具实际运行的是一个叫做Sign.bat的批处理命令。用文本编辑器打开这个批处理文件,我们可以发现,实现签名功能的命令主要是这一行命令:

java -jar signapk.jar testkey.x509.pem testkey.pk8 update.apk update_signed.apk

这条命令的意义是:通过signapk.jar这个可执行jar包,以“testkey.x509.pem”这个公钥文件和“testkey.pk8”这个私钥文件对“update.apk”进行签名,签名后的文件保存为“update_signed.apk”。

对于此处所使用的私钥和公钥的生成方式,将在下文中进一步介绍。我们这里要讲的是signapk.jar到底做了什么。

signapk.jar是Android源码包中的一个签名工具。由于Android是个开源项目,所以,我们可以直接找到signapk.jar的源码。路径为/build/tools/signapk/SignApk.java。

对比一个没有签名的APK和一个签名好的APK,我们会发现,签名好的APK包中多了一个叫做META-INF的文件夹。里面有三个文件,分别名为MANIFEST.MF、CERT.SF和CERT.RSA。signapk.jar就是生成了这几个文件(其他文件没有任何改变。因此我们可以很容易去掉原有签名信息)。通过阅读signapk源码,我们可以理清签名APK包的整个过程。

1、 生成MANIFEST.MF文件:

程序遍历update.apk包中的所有文件(entry),对非文件夹非签名文件的文件,逐个生成SHA1的数字签名信息,再用Base64进行编码。具体代码见这个方法:

private static Manifest addDigestsToManifest(JarFile jar)

关键代码如下:

for (JarEntry entry: byName.values()) {String name = entry.getName();if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) &&!name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) &&(stripPattern == null ||!stripPattern.matcher(name).matches())) {                 InputStream data = jar.getInputStream(entry);                 while ((num = data.read(buffer)) > 0) {                     md.update(buffer, 0, num);                 }                 Attributes attr = null;                 if (input != null) attr = input.getAttributes(name);                 attr = attr != null ? new Attributes(attr) : new Attributes();                 attr.putValue("SHA1-Digest", base64.encode(md.digest()));                 output.getEntries().put(name, attr);}}

之后将生成的签名写入MANIFEST.MF文件。关键代码如下:

Manifest manifest = addDigestsToManifest(inputJar);je = new JarEntry(JarFile.MANIFEST_NAME);je.setTime(timestamp);outputJar.putNextEntry(je);manifest.write(outputJar);
这里简单介绍下SHA1数字签名。简单地说,它就是一种安全哈希算法,类似于MD5算法。它把任意长度的输入,通过散列算法变成固定长度的输出(这里我们称作“摘要信息”)。你不能仅通过这个摘要信息复原原来的信息。另外,它保证不同信息的摘要信息彼此不同。因此,如果你改变了apk包中的文件,那么在apk安装校验时,改变后的文件摘要信息与MANIFEST.MF的检验信息不同,于是程序就不能成功安装。

2、生成CERT.SF文件

对前一步生成的Manifest,使用SHA1-RSA算法,用私钥进行签名。关键代码如下:

Signature signature = Signature.getInstance("SHA1withRSA");signature.initSign(privateKey);je = new JarEntry(CERT_SF_NAME);je.setTime(timestamp);outputJar.putNextEntry(je);writeSignatureFile(manifest,new SignatureOutputStream(outputJar, signature));
RSA是一种非对称加密算法。用私钥通过RSA算法对摘要信息进行加密。在安装时只能使用公钥才能解密它。解密之后,将它与未加密的摘要信息进行对比,如果相符,则表明内容没有被异常修改。

3、生成CERT.RSA文件

生成MANIFEST.MF没有使用密钥信息,生成CERT.SF文件使用了私钥文件。那么我们可以很容易猜测到,CERT.RSA文件的生成肯定和公钥相关。

CERT.RSA文件中保存了公钥、所采用的加密算法等信息。核心代码如下:

je = new JarEntry(CERT_RSA_NAME);je.setTime(timestamp);outputJar.putNextEntry(je);writeSignatureBlock(signature, publicKey, outputJar);

其中writeSignatureBlock的代码如下:

private static void writeSignatureBlock(Signature signature, X509Certificate publicKey, OutputStream out)throws IOException, GeneralSecurityException {                 SignerInfo signerInfo = new SignerInfo(                 new X500Name(publicKey.getIssuerX500Principal().getName()),                 publicKey.getSerialNumber(),                 AlgorithmId.get("SHA1"),                 AlgorithmId.get("RSA"),                 signature.sign());         PKCS7 pkcs7 = new PKCS7(             new AlgorithmId[] { AlgorithmId.get("SHA1") },             new ContentInfo(ContentInfo.DATA_OID, null),             new X509Certificate[] { publicKey },             new SignerInfo[] { signerInfo });         pkcs7.encodeSignedData(out);}
好了,分析完APK包的签名流程,我们可以清楚地意识到:

1、 Android签名机制其实是对APK包完整性和发布机构唯一性的一种校验机制。

2、 Android签名机制不能阻止APK包被修改,但修改后的再签名无法与原先的签名保持一致。(拥有私钥的情况除外)。

3、 APK包加密的公钥就打包在APK包内,且不同的私钥对应不同的公钥。换言之,不同的私钥签名的APK公钥也必不相同。所以我们可以根据公钥的对比,来判断私钥是否一致。

APK签名比对的实现方式

好了,通过Android签名机制的分析,我们从理论上证明了通过APK公钥的比对能判断一个APK的发布机构。并且这个发布机构是很难伪装的,我们暂时可以认为是不可伪装的。

有了理论基础后,我们就可以开始实践了。那么如何获取到APK文件的公钥信息呢?因为Android系统安装程序肯定会获取APK信息进行比对,所以我们可以通过Android源码获得一些思路和帮助。

源码中有一个隐藏的类用于APK包的解析。这个类叫PackageParser,路径为frameworks\base\core\java\android\content\pm\PackageParser.java。当我们需要获取APK包的相关信息时,可以直接使用这个类,下面代码就是一个例子函数:

private PackageInfo parsePackage(String archiveFilePath, int flags){         PackageParser packageParser = new PackageParser(archiveFilePath);         DisplayMetrics metrics = new DisplayMetrics();         metrics.setToDefaults();         final File sourceFile = new File(archiveFilePath);         PackageParser.Package pkg = packageParser.parsePackage(                 sourceFile, archiveFilePath, metrics, 0);         if (pkg == null) {             return null;         }         packageParser.collectCertificates(pkg, 0);          return PackageParser.generatePackageInfo(pkg, null, flags, 0, 0);}
其中参数archiveFilePath指定APK文件路径;

flags需设置PackageManager.GET_SIGNATURES位,以保证返回证书签名信息。

具体如何通过PackageParser获取签名信息在此处不做详述,具体代码请参考PackageParser中的

public boolean collectCertificates(Package pkg, int flags)

private Certificate[] loadCertificates(JarFile jarFile, JarEntry je, byte[] readBuffer)

方法。

紧接着,我们就可以通过packageInfo.signatures来访问到APK的签名信息。还需要说明的是 Android中Signature和Java中Certificate的对应关系。它们的关系如下面代码所示:

pkg.mSignatures = new Signature[certs.length];for (int i=0; i<N; i++) {pkg.mSignatures[i] = new Signature(certs[i].getEncoded());}
也就是说signature = new Signature(certificate.getEncoded());certificate证书中包含了公钥和证书的其他基本信息。公钥不同,证书肯定互不相同。我们可以通过certificate的getPublicKey方法获取公钥信息。所以比对签名证书本质上就是比对公钥信息。

获取到APK签名证书之后,就剩下比对了。这个简单,功能函数如下所示:

private boolean IsSignaturesSame(Signature[] s1, Signature[] s2) {if (s1 == null) {return false;}if (s2 == null) {return false;}HashSet<Signature> set1 = new HashSet<Signature>();for (Signature sig : s1) {set1.add(sig);}HashSet<Signature> set2 = new HashSet<Signature>();for (Signature sig : s2) {set2.add(sig);}// Make sure s2 contains all signatures in s1.if (set1.equals(set2)) {return true;}return false;}
APK签名比对的应用场景

经过以上的论述,想必大家已经明白签名比对的原理和我的实现方式了。那么什么时候什么情况适合使用签名对比来保障Android APK的软件安全呢?个人认为主要有以下三种场景:

1、程序自检测。在程序运行时,自我进行签名比对。比对样本可以存放在APK包内,也可存放于云端。缺点是程序被破解时,自检测功能同样可能遭到破坏,使其失效。

2、可信赖的第三方检测。由可信赖的第三方程序负责APK的软件安全问题。对比样本由第三方收集,放在云端。这种方式适用于杀毒安全软件或者APP Market之类的软件下载市场。缺点是需要联网检测,在无网络情况下无法实现功能。(不可能把大量的签名数据放在移动设备本地)。

3、系统限定安装。这就涉及到改Android系统了。限定仅能安装某些证书的APK。软件发布商需要向系统发布上申请证书。如果发现问题,能追踪到是哪个软件发布商的责任。适用于系统提供商或者终端产品生产商。缺点是过于封闭,不利于系统的开放性。

以上三种场景,虽然各有缺点,但缺点并不是不能克服的。例如,我们可以考虑程序自检测的功能用native method的方法实现等等。软件安全是一个复杂的课题,往往需要多种技术联合使用,才能更好的保障软件不被恶意破坏。

签名用的Key的产生方法

产生RSA私钥(private key)

openssl genrsa -3 -out testkey.pem 2048

-3是算法的参数(public exponent)。2048是私钥长度。testkey.pem是输出的文件。

产生PKCS#10格式的认证请求。所谓认证请求就是发给认证机构认证的一个请求,它主要包括一个公钥和一些相关信息(如组织名称和联系人邮件地址)。

openssl req -new -x509 -key testkey.pem-out testkey.x509.pem -days 10000 /-subj ‘/C=US/ST=California/L=MountainView/O=Android/OU=Android/CN=Android/emailAddress=android@android.com’

如果不提供最后两个参数,openssl会提示你输入相关信息,这里的信息可以根据你自己的实际情况填写。如:

openssl req -new -x509 -key testkey.pem-out testkey.x509.pem -days 10000

You are about to be asked to enterinformation that will be incorporated into your certificate request. What youare about to enter is what is called a Distinguished Name or a DN. There arequite a few fields but you can leave some blank. For some fields there will bea default value, If you enter ‘.’, the field will be left blank. —–

Country Name (2 letter code) [GB]:CN

State or Province Name (full name)[Berkshire]:GuangDong

Locality Name (eg, city)[Newbury]:ShenZhen

Organization Name (eg, company) [MyCompany Ltd]:Topwise

Organizational Unit Name (eg, section)[]:Broncho

Common Name (eg, your name or yourserver’s hostname) []:broncho.cn

Email Address []:bronchosales@gmail.com

把私钥的格式转换成PKCS#8(Private-KeyInformation Syntax Standard.)

openssl pkcs8 -in testkey.pem -topk8-outform DER -out testkey.pk8 -nocrypt

私钥是不能让别人知道的,否则就起不到保密的作用了。私钥通常是要加密保存的,但这里指定了-nocryp,表示不加密。

Android提供了一个脚本mkkey.sh用来简化上面的步骤:

if ["$1" == ""]; then    echo "Create atest certificate key."    echo "Usage: $0NAME"    echo "Willgenerate NAME.pk8 and NAME.x509.pem"    echo " /C=US/ST=California/L=MountainView/O=Android/OU=Android/CN=Android/emailAddress=android@android.com"    returnfiopenssl genrsa -3 -out $1.pem 2048openssl req -new -x509 -key $1.pem -out$1.x509.pem -days 10000 /    -subj'/C=US/ST=California/L=Mountain View/O=Android/OU=Android/CN=Android/emailAddress=android@android.com'openssl pkcs8 -in $1.pem -topk8 -outformDER -out $1.pk8 -nocrypt
签名的原理

Android提供了为jar/zip文件签名的程序signapk.jar。它的用法如下:

Usage: signapk publickey.x509[.pem]privatekey.pk8 input.jar output.jar

第一个参数是公钥,即前面第二步产生的testkey.x509.pem。第二个参数是私钥,即前面第三步产生的testkey.pk8。第三个参数是要签名的文件。第四个参数是输出的文件(即签名后的文件)。如:

java -jar signapk.jar testkey.x509.pemtestkey.pk8 update.zip update-signed.zip

现在我们来看看签名到底做了些什么:

先为输入的jar/zip文件中的所有文件生成SHA1数字签名(除了CERT.RSA、CERT.SF和MANIFEST.MF)

for (JarEntry entry: byName.values()) {String name = entry.getName();if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) &&                !name.equals(CERT_SF_NAME) &&!name.equals(CERT_RSA_NAME) &&                (stripPattern == null || !stripPattern.matcher(name).matches())) {                InputStream data = jar.getInputStream(entry);                while ((num = data.read(buffer)) > 0) {                      md.update(buffer, 0, num);                }                 Attributes attr = null;                if (input != null) attr = input.getAttributes(name);                attr = attr != null ? new Attributes(attr) : new Attributes();                attr.putValue("SHA1-Digest", base64.encode(md.digest()));                output.getEntries().put(name, attr);       }}

并把数字签名信息写入MANIFEST.MF

je = new JarEntry(JarFile.MANIFEST_NAME);je.setTime(timestamp);outputJar.putNextEntry(je);manifest.write(outputJar);

对manifest签名并写入CERT.SF

// CERT.SFSignature signature = Signature.getInstance("SHA1withRSA");signature.initSign(privateKey);je = new JarEntry(CERT_SF_NAME);je.setTime(timestamp);outputJar.putNextEntry(je);writeSignatureFile(manifest,new SignatureOutputStream(outputJar, signature));

把对输出文件的签名和公钥写入CERT.RSA。

// CERT.RSAje = new JarEntry(CERT_RSA_NAME);je.setTime(timestamp);outputJar.putNextEntry(je);writeSignatureBlock(signature, publicKey, outputJar);

签名的作用:签名的主要目的为了检测文件是否被别人修改了。但它并不能禁止别人修改,因为你完全重新生成签名,但是你生成的签名和原来是不一样的。

0 0
原创粉丝点击