现在的位置: 首页 > 综合 > 正文

Android安全——签名机制

2017年11月10日 ⁄ 综合 ⁄ 共 7551字 ⁄ 字号 评论关闭

Android签名机制

    为了说明APK签名对软件安全的有效性,我们有必要了解一下Android APK的签名机制。

[java]
view plain
copy

  1. 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进行编码。具体代码见这个方法:

[java]
view plain
copy

  1. private static Manifest addDigestsToManifest(JarFile jar)  

关键代码如下:

 1     for (JarEntry entry: byName.values()) {
 2         String name = entry.getName();
 3         if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) &&
 4             !name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) &&
 5                (stripPattern == null ||!stripPattern.matcher(name).matches())) {
 6                 InputStream data = jar.getInputStream(entry);
 7                 while ((num = data.read(buffer)) > 0) {
 8                     md.update(buffer, 0, num);
 9                 }
10                 Attributes attr = null;
11                 if (input != null) attr = input.getAttributes(name);
12                 attr = attr != null ? new Attributes(attr) : new Attributes();
13                 attr.putValue("SHA1-Digest", base64.encode(md.digest()));
14                 output.getEntries().put(name, attr);
15           }
16     }

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

1     Manifest manifest = addDigestsToManifest(inputJar);
2     je = new JarEntry(JarFile.MANIFEST_NAME);
3     je.setTime(timestamp);
4     outputJar.putNextEntry(je);
5     manifest.write(outputJar);

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

2、 生成CERT.SF文件:

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

1     Signature signature = Signature.getInstance("SHA1withRSA");
2     signature.initSign(privateKey);
3     je = new JarEntry(CERT_SF_NAME);
4     je.setTime(timestamp);
5     outputJar.putNextEntry(je);
6     writeSignatureFile(manifest,
7     new SignatureOutputStream(outputJar, signature));

    RSA是一种非对称加密算法。用私钥通过RSA算法对摘要信息进行加密。在安装时只能使用公钥才能解密它。解密之后,将它与未加密的摘要信息进行对比,如果相符,则表明内容没有被异常修改。

3、 生成CERT.RSA文件:

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

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

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

    其中writeSignatureBlock的代码如下:

1     private static void writeSignatureBlock(
 2         Signature signature, X509Certificate publicKey, OutputStream out)
 3             throws IOException, GeneralSecurityException {
 4                 SignerInfo signerInfo = new SignerInfo(
 5                 new X500Name(publicKey.getIssuerX500Principal().getName()),
 6                 publicKey.getSerialNumber(),
 7                 AlgorithmId.get("SHA1"),
 8                 AlgorithmId.get("RSA"),
 9                 signature.sign());
10 
11         PKCS7 pkcs7 = new PKCS7(
12             new AlgorithmId[] { AlgorithmId.get("SHA1") },
13             new ContentInfo(ContentInfo.DATA_OID, null),
14             new X509Certificate[] { publicKey },
15             new SignerInfo[] { signerInfo });
16 
17         pkcs7.encodeSignedData(out);
18     }

    好了,分析完APK包的签名流程,我们可以清楚地意识到:

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

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

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

    好了,分析完APK包的签名流程,我们可以清楚地意识到:

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

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

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

APK对文件的访问控制

/data目录权限:

drwxrwx--xsystem  system           2011-01-03 23:41 data

这种情况下,用ESExplorer查看/data时,目录为空。File("/data")对象的canRead/canWrite方法测试,不可读不可写文件存在。

说明默认情况下APK的gid中没有system。

/system目录权限:

drwxr-xr-xroot    root             2011-03-05 19:23 system

这种情况下,ESExplorer可以进入到/system目录,File("/system")对象的canRead/canWrite方法测试,可读不可写文件存在。

说明默认情况下APK的gid中有root。

对于一个目录来说,假如该目录的权限设置对于APK来说不可读不可写,用File("")对象的canRead/canWrite方法测试,不可读不可写文件存在。

试验了一下午,总结一下:

1、普通APK运行时,属性root组,但不属性system组。

2、对于父目录a没有读写权限但子目录a/b有读写权限的的情况,直接使用File("a/b")方式可以对a/b目录进行读写。

这一点儿对一些特殊场合下的APK比较有意义。比如一个系统的内置程序有一些加密信息需要放在本地,但又不能让其它程序和用户能访问到。就可以对系统做一下定制,在/data下面建立一个someone文件夹,权限为777.因为/data本身是700,所以第三方程序是无法访问到这个目录底下的任何东西的。但对于我们的这个特殊APK来说,通过File("/data/someone")这种方式是可以访问到,并且可读可写的。

3、上面试验中没有提到的一点,我在一个APK中使用ProcessBuilder启动一个本地进程时,本地进程和这个APK具有相同的UID和GID。

4、Android系统中所有查看用户ID相关的命令都被删除了。所以,上面都是猜的

packages.xml解析

/data/system/packages.xml这个文件由PackageManagerService.java生成,里面记录了系统当中安装的APK的所有属性,权限等信息。当系统中的APK安装、删除、升级时,文件就会被更新。

<permissions>标签定义了目前系统中定义的所有权限。主要分为两类:系统定义的(package属性为Android)和APK定义的(package属性为APK的包名)。

<package>代表一个APK的属性,它的属性含义如下。

name:APK的包名

codePath:安装路径。有/system/app系统APK和/data/app两种。/system/app存放系统出厂时预置的一些APK,/data/app存放用户安装的第三方APK。

system:如果APK被安装在/system/app下,system的值为true;安装在/data/app下面的话,值为true。

ts:时间戳

version:APK的版本号

sharedUserId/userId:Android系统启动一个普通的APK时,会为这个APK分配一个独立的UID,这就是userId。 如果APK要和系统中其它APK使用相同的UID的话,那就是sharedUserId。关于共享UID,下面有更详细的描述。

perms:APK的AndroidManifest.xml文件中,每使用一个<uses-permission>标签,<perms>标签中就会增加一项。

<shared-user>代表一个共享UID,通常,共同实现一系列相似功能的APK共享一个UID。<perms>标 签中的权限代表了这个共享UID的权限,所有使用的同一个共享UID的APK运行在同一进程中,这个进程的UID就是这个共享UID,这些APK都具有这 个共享UID的权限。

name:共享UID的名字,在APK的android:sharedUserId属性中使用。

userId:使用这个共享UID的所有APK运行时所在的进程的UID。

共享UID和签名

安装在设备中的每一个Android包文件(.apk)都会被分配到一个属于自己的统一的Linux用户ID,并且为它创建一个沙箱,以防止影响其他应用程序(或者其他应用程序影响它)。用户ID 在应用程序安装到设备中时被分配,并且在这个设备中保持它的永久性。

通过Shared User id,拥有同一个User id的多个APK可以配置成运行在同一个进程中.所以默认就是可以互相访问任意数据. 也可以配置成运行成不同的进程, 同时可以访问其他APK的数据目录下的数据库和文件.就像访问本程序的数据一样.

对于一个APK来说,如果要使用某个共享UID的话,必须做三步:

1、在Manifest节点中增加android:sharedUserId属性。

2、在Android.mk中增加LOCAL_CERTIFICATE的定义。

如果增加了上面的属性但没有定义与之对应的LOCAL_CERTIFICATE的话,APK是安装不上去的。提示错误是:Package com.test.MyTest hasno signatures that match those in shared user android.uid.system; ignoring!也就是说,仅有相同签名和相同sharedUserID标签的两个应用程序签名都会被分配相同的用户ID。例如所有和 media/download相关的APK都使用android.media作为sharedUserId的话,那么它们必须有相同的签名media。

3、把APK的源码放到packages/apps/目录下,用mm进行编译。

举例说明一下。

系统中所有使用android.uid.system作为共享UID的APK,都会首先在manifest节点中增加 android:sharedUserId="android.uid.system",然后在Android.mk中增加 LOCAL_CERTIFICATE := platform。可以参见Settings等

系统中所有使用android.uid.shared作为共享UID的APK,都会在manifest节点中增加 android:sharedUserId="android.uid.shared",然后在Android.mk中增加 LOCAL_CERTIFICATE := shared。可以参见Launcher等

系统中所有使用android.media作为共享UID的APK,都会在manifest节点中增加 android:sharedUserId="android.media",然后在Android.mk中增加LOCAL_CERTIFICATE := media。可以参见Gallery等。

另外,应用创建的任何文件都会被赋予应用的用户标识,并且正常情况下不能被其他包访问。当通过 getSharedPreferences(String,int)、openFileOutput(String、int)或者 openOrCreate Database(String、int、SQLiteDatabase.CursorFactory)创建一个新文件时,开发者可以同时或分别使用 MODE_WORLD_READABLE和MODE_WORLD_RITEABLE标志允许其他包读/写此文件。当设置了这些标志后,这个文件仍然属于自
己的应用程序,但是它的全局读/写和读/写权限已经设置,所以其他任何应用程序可以看到它。

关于签名:

build/target/product/security目录中有四组默认签名供Android.mk在编译APK使用:

1、testkey:普通APK,默认情况下使用。

2、platform:该APK完成一些系统的核心功能。经过对系统中存在的文件夹的访问测试,这种方式编译出来的APK所在进程的UID为system。

3、shared:该APK需要和home/contacts进程共享数据。

4、media:该APK是media/download系统中的一环。

应用程序的Android.mk中有一个LOCAL_CERTIFICATE字段,由它指定用哪个key签名,未指定的默认用testkey.

抱歉!评论已关闭.