在一个木函早先版本,有一个挺炫酷的功能:网页转App。那么这么一个功能是怎么实现的呢?
方案预想
如果我们使用IDE开发的话,这个功能完全可以使用一个WebView去实现,至于网页对应的URL只需要在打包的时候进行配置就行了,可是并无法做到在已安装App中直接出包并签名安装,而且在手机中,并没法直接将代码编译称APK。所以猜测一个木函是将一个已有的APK进行修改,然后进行签名。
本地修改APK
如上分析,如果我们要对一个APK进行修改,dex代码部分在手机中修改自然是有难度了,但是如果对清单文件,或者是其他资源文件进行修改就比较有可能了。在Github上有找到一个Java项目apkeditor,运行了这个项目发现,这个项目可以做到修改清单文件内容,替换图片资源文件。
本地签名
签名方案落后
不过同样这个项目有其不足的地方,毕竟是5年前的项目了。在KeyHelper中有以下代码
/**
* 签名前缀
* 首先用上面生成的keystore签名任意一个apk,解压出这个apk里面 META-INF/CERT.RSA 的文件
* @throws IOException
*/
private static void getSigPrefix() throws IOException, URISyntaxException {
System.out.println("----------");
String rsaFileName="CERT.RSA";
File file = new File(ClassLoader.getSystemClassLoader().getResource(rsaFileName).toURI());
FileInputStream fis = new FileInputStream(file);
/**
* RSA-keysize signature-length
# 512 64
# 1024 128
# 2048 256
*/
int same = (int) (file.length() - 64); //当前-keysize 512
byte[] buff = new byte[same];
fis.read(buff, 0, same);
fis.close();
String string = new String(Base64.encodeBase64(buff), "UTF-8");
System.out.println("sigPrefix -->> " + string);
}
很明显,这个签名长度只是512,在SignApk中有这样一段注释
/**
* HISTORICAL NOTE:
* <p/>
* Prior to the keylimepie release, SignApk ignored the signature
* algorithm specified in the certificate and always used SHA1withRSA.
* <p/>
* Starting with keylimepie, we support SHA256withRSA, and use the
* signature algorithm in the certificate to select which to use
* (SHA256withRSA or SHA1withRSA).
* <p/>
* Because there are old keys still in use whose certificate actually
* says "MD5withRSA", we treat these as though they say "SHA1withRSA"
* for compatibility with older releases. This can be changed by
* altering the getAlgorithm() function below.
*/
/**
* 原始代码见aosp项目目录 build/tools/signapk/SignApk.java
* 如何生成privateKey 和 sigPrefix 见{@see KeyHelper}
*/
以及这样一段代码
/**
* Add the hash(es) of every file to the manifest, creating it if
* necessary.
*/
private Manifest addDigestsToManifest(JarFile jar)
throws IOException, GeneralSecurityException {
Manifest input = jar.getManifest();
Manifest output = new Manifest();
Attributes main = output.getMainAttributes();
if (input != null) {
main.putAll(input.getMainAttributes());
} else {
main.putValue("Manifest-Version", "1.0");
main.putValue("Created-By", "1.0 (Android SignApk)");
}
MessageDigest md_sha1 = MessageDigest.getInstance("SHA1");
byte[] buffer = new byte[4096];
int num;
// We sort the input entries by name, and add them to the
// output manifest in sorted order. We expect that the output
// map will be deterministic.
TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
JarEntry entry = e.nextElement();
byName.put(entry.getName(), entry);
}
for (JarEntry entry : byName.values()) {
String name = entry.getName();
if (!entry.isDirectory() &&
(stripPattern == null || !stripPattern.matcher(name).matches())) {
InputStream data = jar.getInputStream(entry);
while ((num = data.read(buffer)) > 0) {
md_sha1.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", new String(Base64.encodeBase64(md_sha1.digest()), "ASCII"));
output.getEntries().put(name, attr);
}
}
return output;
}
并且分析了查看了签名文件的信息后
Creation date: Sep 22, 2015
Entry type: PrivateKeyEntry
Certificate chain length: 1
Certificate[1]:
Owner: CN=z, OU=z, O=z, L=shanghai, ST=shanghai, C=cn
Issuer: CN=z, OU=z, O=z, L=shanghai, ST=shanghai, C=cn
Serial number: 139a3b79
Valid from: Tue Sep 22 20:20:51 CST 2015 until: Thu Mar 29 20:20:51 CST 2125
Certificate fingerprints:
SHA1: BD:1C:65:A3:39:E6:D1:33:C3:C5:AD:B0:A4:22:05:BE:90:F3:6C:CD
SHA256: 92:57:56:C8:CD:EF:4F:43:E9:FD:ED:2D:13:DE:47:0C:99:94:92:94:97:30:F1:B4:52:24:C5:19:A9:AC:BC:F9
Signature algorithm name: SHA256withRSA
Subject Public Key Algorithm: 512-bit RSA key (weak)
Version: 3
Extensions:
#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 9E 3B 67 C8 52 6E BA 7C 8F 6F E1 33 F0 5D F0 B8 .;g.Rn...o.3.]..
0010: 95 31 A8 28 .1.(
]
]
发现使用的是SHA256withRSA,512位RAS 进行签名的,而且从签名的APK来看,只是进行了V1签名,但现在V3签名都已经出来了
那么我们现在在进行签名的签名文件是怎样的呢?
Creation date: Oct 30, 2019
Entry type: PrivateKeyEntry
Certificate chain length: 1
Certificate[1]:
Owner: CN=y, OU=y, O=y, L=y, ST=y, C=y
Issuer: CN=y, OU=y, O=y, L=y, ST=y, C=y
Serial number: 693a88f7
Valid from: Wed Oct 30 20:32:02 CST 2019 until: Sun Oct 23 20:32:02 CST 2044
Certificate fingerprints:
SHA1: 03:71:97:17:ED:B1:8B:84:BF:D3:61:AF:A1:AC:C0:22:4B:9D:E6:75
SHA256: D5:E0:1D:B4:1E:9C:3F:8C:E4:3B:F0:B4:89:3D:44:F7:86:49:CE:C3:8B:BA:7A:14:C5:5F:3F:38:D5:6A:35:AC
Signature algorithm name: SHA256withRSA
Subject Public Key Algorithm: 2048-bit RSA key
Version: 3
以上是我们新建的签名文件,这个使用的是SHA256withRSA,2048位RAS 进行签名的。
使用新的签名方案
虽然以上的签名方案比较落后,但是他的这一段注释让我找到了方向
/**
* 原始代码见aosp项目目录 build/tools/signapk/SignApk.java
* 如何生成privateKey 和 sigPrefix 见{@see KeyHelper}
*/
可以从Android源码去寻找最新的签名方案,从android sdk目录下的build-tools/28.0.3/lib/里面得到apksigner.jar,这个是build-tools 24以上才有的,只支持Android 7.0 以上系统运行,支持1-3代签名技术,但是同样的如果想要从电脑里面得到jarsigner就不太可能了,因为这个是由java提供的,是一个可执行文件,不是jar文件,但是在源码中存在这个文件 /prebuilts/sdk/tools/lib/signapk.jar,这个文件可以实现在Android 7.0以下运行,进行V1 签名。
通过以上操作,得到的两个jar文件,就可以实现在Android7.0以下进行1代签名和Android 7.0 以上进行1-3代签名了
分离PK8和PEM
apksigner.jar 中有个文件help_sign.txt,里面有这样的一段使用帮助
EXAMPLES
1. Sign an APK, in-place, using the one and only key in keystore release.jks:
$ apksigner sign --ks release.jks app.apk
1. Sign an APK, without overwriting, using the one and only key in keystore
release.jks:
$ apksigner sign --ks release.jks --in app.apk --out app-signed.apk
3. Sign an APK using a private key and certificate stored as individual files:
$ apksigner sign --key release.pk8 --cert release.x509.pem app.apk
4. Sign an APK using two keys:
$ apksigner sign --ks release.jks --next-signer --ks magic.jks app.apk
5. Sign an APK using PKCS #11 JCA Provider:
$ apksigner sign --provider-class sun.security.pkcs11.SunPKCS11 \
--provider-arg token.cfg --ks NONE --ks-type PKCS11 app.apk
6. Sign an APK using a non-ASCII password KeyStore created on English Windows.
The --pass-encoding parameter is not needed if apksigner is being run on
English Windows with Java 8 or older.
$ apksigner sign --ks release.jks --pass-encoding ibm437 app.apk
7. Sign an APK on Windows using a non-ASCII password KeyStore created on a
modern OSX or Linux machine:
$ apksigner sign --ks release.jks --pass-encoding utf-8 app.apk
8. Sign an APK with rotated signing certificate:
$ apksigner sign --ks release.jks --next-signer --ks release2.jks \
--lineage /path/to/signing/history/lineage app.apk
说明了使用签名的几种方案,但是为了不用输入密码,我选取了第三种方案进行签名,这样的话就需要得到PK8文件和PEM文件了
网上找了ks2x509.jar这样的一个工具,可以从一个jks签名文件提取生成PK8文件和PEM文件
signapk 准备
由于signapk.jar 的入口文件SignApk并不是public,所以我们需要使用一个同包名的类才能调用到它
package com.android.signapk;
public class ApkSignerProxy {
public static void main(String[] args) {
SignApk.main(args);
}
}
写一段测试代码
一切准备就行后,使用代码测试一下
findViewById(R.id.signButton).setOnClickListener(view -> {
File unsignFile = new File(
Environment.getExternalStorageDirectory(),
"app_debug.apk"
);
Log.d(TAG, "unsignFile--->" + unsignFile.getAbsolutePath());
File outapk = new File(
Environment.getExternalStorageDirectory(),
"temp.apk"
);
Log.d(TAG, "outapk--->" + outapk.getAbsolutePath());
if (outapk.exists()) {
outapk.delete();
}
File pk8 = new File(
Environment.getExternalStorageDirectory(),
"testkey.pk8"
);
Log.d(TAG, "pk8--->" + pk8.getAbsolutePath());
File pem = new File(
Environment.getExternalStorageDirectory(),
"testkey.x509.pem"
);
Log.d(TAG, "pem--->" + pem.getAbsolutePath());
try {
Log.d(TAG, "onClick: 签名开始");
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
ApkSignerTool.main(new String[]{"sign",
"--key",
pk8.getAbsolutePath(),
"--cert",
pem.getAbsolutePath(),
"--v2-signing-enabled",
"false",
"--out",
outapk.getAbsolutePath(),
"--in",
unsignFile.getAbsolutePath()});
} else {
ApkSignerProxy.main(new String[]{
pem.getAbsolutePath(),
pk8.getAbsolutePath(),
unsignFile.getAbsolutePath(),
outapk.getAbsolutePath()
});
}
Log.d(TAG, "onClick: 签名结束");
} catch (Exception e) {
e.printStackTrace();
}
while (!outapk.exists()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Log.d(TAG, "签名完成");
runOnUiThread(() -> Toast.makeText(MainActivity.this, "签名完成", Toast.LENGTH_SHORT).show());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
运行后成功的在Android6.0设备和Android8.0设备对文件进行签名