1. 前言
關(guān)于Android的簽名機(jī)制,在一個(gè)月前就看過(guò)了,當(dāng)時(shí)還寫(xiě)了下流程,感覺(jué)沒(méi)有太大的技術(shù)含量就沒(méi)有記錄。最近在看APK安裝過(guò)程,突然又想起安裝過(guò)程包含了APK的驗(yàn)證,關(guān)于APK的驗(yàn)證無(wú)非就是簽名的逆過(guò)程。但是發(fā)現(xiàn)自己對(duì)簽名過(guò)程好像模糊了很多,遂決定記錄下簽名過(guò)程。
2. 關(guān)于簽名
Android的簽名現(xiàn)在分為兩個(gè)版本:v1和v2,因?yàn)関1版本簽名過(guò)程的缺陷,造成了APK可能被攻擊。
v1簽名:簽名和摘要文件為APK解壓后的META-INF文件夾下的*.MF、*.SF、*.RSA文件,其簽名過(guò)程需要對(duì)文件進(jìn)行解壓并且計(jì)算每個(gè)文件的摘要。
v2簽名:簽名信息存儲(chǔ)在ZIP文件格式中。7.0以上支持,7.0以下不支持,只能采用v1簽名。
3. v1簽名源碼
我用到的源碼都是在在線源碼網(wǎng)站上下載的,這里用到了SignApk.java
文件。
我們都知道如果使用命令行簽名的話(huà),都是執(zhí)行的main
方法:
public static void main(String[] args) {
// 對(duì)輸入?yún)?shù)的解析和驗(yàn)證
......
boolean signWholeFile = false;
String providerClass = null;
int alignment = 4;
int minSdkVersion = 0;
boolean signUsingApkSignatureSchemeV2 = true;
int argstart = 0;
// 對(duì)輸入?yún)?shù)的解析和驗(yàn)證
......
loadProviderIfNecessary(providerClass);
String inputFilename = args[args.length - 2];
String outputFilename = args[args.length - 1];
JarFile inputJar = null;
FileOutputStream outputFile = null;
int hashes = 0;
try {
// 公鑰文件
File firstPublicKeyFile = new File(args[argstart + 0]);
X509Certificate[] publicKey = new X509Certificate[numKeys];
......
long timestamp = 1230768000000L;
timestamp -= TimeZone.getDefault().getOffset(timestamp);
// 私鑰文件
PrivateKey[] privateKey = new PrivateKey[numKeys];
for (int i = 0; i < numKeys; ++i) {
int argNum = argstart + i * 2 + 1;
privateKey[i] = readPrivateKey(new File(args[argNum]));
}
// 輸入的文件
inputJar = new JarFile(new File(inputFilename), false); // Don't verify.
// 輸出文件
outputFile = new FileOutputStream(outputFilename);
// 直接對(duì)整個(gè)文件簽名,這里不看
if (signWholeFile) {
SignApk.signWholeFile(inputJar, firstPublicKeyFile,
publicKey[0], privateKey[0],
timestamp, minSdkVersion,
outputFile);
} else {
ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream();
JarOutputStream outputJar = new JarOutputStream(v1SignedApkBuf);
outputJar.setLevel(9);
// 1. 生成.MF信息內(nèi)容
Manifest manifest = addDigestsToManifest(inputJar, hashes);
copyFiles(manifest, inputJar, outputJar, timestamp, alignment);
// 2. 對(duì)文件簽名,生成.SF文件內(nèi)容并且簽名
signFile(
manifest,
publicKey, privateKey,
timestamp, minSdkVersion, signUsingApkSignatureSchemeV2,
outputJar);
outputJar.close();
ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray());
v1SignedApkBuf.reset();
ByteBuffer[] outputChunks;
// 使用v2簽名
if (signUsingApkSignatureSchemeV2) {
// Additionally sign the APK using the APK Signature Scheme v2.
ByteBuffer apkContents = v1SignedApk;
List<ApkSignerV2.SignerConfig> signerConfigs =
createV2SignerConfigs(
privateKey,
publicKey,
new String[]{APK_SIG_SCHEME_V2_DIGEST_ALGORITHM});
outputChunks = ApkSignerV2.sign(apkContents, signerConfigs);
} else {
// Output the JAR-signed APK as is.
outputChunks = new ByteBuffer[]{v1SignedApk};
}
// This assumes outputChunks are array-backed. To avoid this assumption, the
// code could be rewritten to use FileChannel.
for (ByteBuffer outputChunk : outputChunks) {
outputFile.write(
outputChunk.array(),
outputChunk.arrayOffset() + outputChunk.position(),
outputChunk.remaining());
outputChunk.position(outputChunk.limit());
}
outputFile.close();
outputFile = null;
return;
}
}
......
}
v1簽名過(guò)程很簡(jiǎn)單,一共分為了三個(gè)部分:
- 對(duì)非目錄文件以及過(guò)濾文件進(jìn)行摘要,存儲(chǔ)在MANIFEST.MF文件中。
- 對(duì)MANIFEST.MF文件的進(jìn)行摘要以及對(duì)MANIFEST.MF文件的每個(gè)條目?jī)?nèi)容進(jìn)行摘要,存儲(chǔ)在CERT.SF文件中。
-
使用指定的私鑰對(duì)CERT.SF文件計(jì)算簽名,然后將簽名以及包含公鑰信息的數(shù)字證書(shū)寫(xiě)入 CERT.RSA。
這三個(gè)文件也就是我們APK解壓后META-INF目錄下的文件:
簽名和摘要文件
3.1 .MF文件內(nèi)容生成
上面已經(jīng)說(shuō)了,MANIFEST.MF的內(nèi)容是通過(guò)addDigestsToManifest
方法生成的,代碼如下:
/**
* 添加對(duì)所有不是目錄文件的摘要(SHA1或SHA256)
*/
private static Manifest addDigestsToManifest(JarFile jar, int hashes)
throws IOException, GeneralSecurityException {
// 最上面那部分內(nèi)容
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)");
}
// 根據(jù)輸入來(lái)選擇摘要算法
MessageDigest md_sha1 = null;
MessageDigest md_sha256 = null;
if ((hashes & USE_SHA1) != 0) {
md_sha1 = MessageDigest.getInstance("SHA1");
}
if ((hashes & USE_SHA256) != 0) {
md_sha256 = MessageDigest.getInstance("SHA256");
}
byte[] buffer = new byte[4096];
int num;
TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
// 把a(bǔ)pk文件所有條目添加到treemap中
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();
// 如果不是目錄并且不是特定的文件 attern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" +
// Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
if (!entry.isDirectory() &&
(stripPattern == null || !stripPattern.matcher(name).matches())) {
InputStream data = jar.getInputStream(entry);
while ((num = data.read(buffer)) > 0) {
if (md_sha1 != null) md_sha1.update(buffer, 0, num);
if (md_sha256 != null) md_sha256.update(buffer, 0, num);
}
Attributes attr = null;
if (input != null) attr = input.getAttributes(name);
attr = attr != null ? new Attributes(attr) : new Attributes();
for (Iterator<Object> i = attr.keySet().iterator(); i.hasNext(); ) {
Object key = i.next();
if (!(key instanceof Attributes.Name)) {
continue;
}
String attributeNameLowerCase =
((Attributes.Name) key).toString().toLowerCase(Locale.US);
if (attributeNameLowerCase.endsWith("-digest")) {
i.remove();
}
}
// 計(jì)算摘要 并且使用base64進(jìn)行encode
// Add SHA-1 digest if requested
if (md_sha1 != null) {
attr.putValue("SHA1-Digest",
new String(Base64.encode(md_sha1.digest()), "ASCII"));
}
if (md_sha256 != null) {
attr.putValue("SHA-256-Digest",
new String(Base64.encode(md_sha256.digest()), "ASCII"));
}
output.getEntries().put(name, attr);
}
}
return output;
}
其過(guò)程分為三步:
-
添加最上面內(nèi)容信息
上部分內(nèi)容 - 將APK內(nèi)容遍歷,尋找不為目錄并且沒(méi)有被過(guò)濾的文件,對(duì)其進(jìn)行摘要計(jì)算。
- 將摘要信息寫(xiě)入。
驗(yàn)證一下:
AndroidManifest.xml文件對(duì)應(yīng)的摘要
文件摘要
這么看這兩個(gè)值還不相同呢,但是我們仔細(xì)看下代碼new String(Base64.encode(md_sha1.digest()), "ASCII")
這里將摘要內(nèi)容進(jìn)行Base64編碼后又將其轉(zhuǎn)成String了,我們可以看下:
byte[] bytes = {(byte) 0xD0, (byte) 0xF9, (byte) 0xE4, 0x2B, (byte) 0xB2, (byte) 0xC7, (byte) 0xB0, 0x72, 0x45, (byte) 0x8C, 0x27, (byte) 0xC3, 0x7F, 0x3D, 0x01, 0x78, 0x5C, (byte) 0x82, (byte) 0xA8, (byte) 0xB5};
String ascii = new String(Base64.getEncoder().encode(bytes), "ASCII");
System.out.println(ascii);
輸出內(nèi)容:
簡(jiǎn)化的代碼如下:
private static Manifest addDigestsToManifest(JarFile jarFile) throws IOException, NoSuchAlgorithmException {
Manifest input = jarFile.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 sha1 = MessageDigest.getInstance("SHA1");
TreeMap<String, JarEntry> byName = new TreeMap<>();
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry jarEntry = entries.nextElement();
byName.put(jarEntry.getName(), jarEntry);
}
byte[] data = new byte[4096];
int num = 0;
for (JarEntry jarEntry : byName.values()) {
if (!jarEntry.isDirectory()) {
InputStream inputStream = jarFile.getInputStream(jarEntry);
while ((num = inputStream.read(data)) > 0) {
sha1.update(data, 0, num);
}
Attributes attributes = null;
if (input != null) {
attributes = input.getAttributes(jarEntry.getName());
}
if (attributes == null) {
attributes = new Attributes();
}
attributes.putValue("SHA1-Digest", new String(Base64.getEncoder().encode(sha1.digest()), "ASCII"));
output.getEntries().put(jarEntry.getName(), attributes);
}
}
output.write(new FileOutputStream("C:\\Users\\nick\\Desktop\\MANIFEST.MF"));
return output;
}
3.2 .SF文件內(nèi)容生成
.SF文件內(nèi)容是需要依賴(lài).MF文件內(nèi)容:
/**
* Write a .SF file with a digest of the specified manifest.
* 寫(xiě)入.sf文件
*/
private static void writeSignatureFile(Manifest manifest, OutputStream out,
int hash, boolean additionallySignedUsingAnApkSignatureScheme)
throws IOException, GeneralSecurityException {
Manifest sf = new Manifest();
Attributes main = sf.getMainAttributes();
// 添加內(nèi)容
main.putValue("Signature-Version", "1.0");
main.putValue("Created-By", "1.0 (Android SignApk)");
// v2簽名 添加
if (additionallySignedUsingAnApkSignatureScheme) {
main.putValue(
ApkSignerV2.SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME,
ApkSignerV2.SF_ATTRIBUTE_ANDROID_APK_SIGNED_VALUE);
}
MessageDigest md = MessageDigest.getInstance(
hash == USE_SHA256 ? "SHA256" : "SHA1");
PrintStream print = new PrintStream(
new DigestOutputStream(new ByteArrayOutputStream(), md),
true, "UTF-8");
manifest.write(print);
print.flush();
main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",
new String(Base64.encode(md.digest()), "ASCII"));
// 這段代碼將上面的.MF的內(nèi)容以
// Name: res/layout/fb_community_manage_activity.xml\r\nSHA1-Digest: 2JZzBj3bimvi5pwxQZH4LlJJTcg=\r\n\r\n
// 獲取其摘要,并且按照相同的格式存儲(chǔ)
Map<String, Attributes> entries = manifest.getEntries();
for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
// Digest of the manifest stanza for this entry.
print.print("Name: " + entry.getKey() + "\r\n");
for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
print.print(att.getKey() + ": " + att.getValue() + "\r\n");
}
print.print("\r\n");
print.flush();
Attributes sfAttr = new Attributes();
sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest",
new String(Base64.encode(md.digest()), "ASCII"));
sf.getEntries().put(entry.getKey(), sfAttr);
}
CountOutputStream cout = new CountOutputStream(out);
sf.write(cout);
if ((cout.size() % 1024) == 0) {
cout.write('\r');
cout.write('\n');
}
}
主要兩個(gè)步驟:
- 計(jì)算.MF整個(gè)文件內(nèi)容摘要,存放在上面的位置。
- 計(jì)算.MF每一項(xiàng)內(nèi)容,將其拼接成
Name: res/layout/fb_community_manage_activity.xml\r\nSHA1-Digest: 2JZzBj3bimvi5pwxQZH4LlJJTcg=\r\n\r\n
格式并計(jì)算這段內(nèi)容的摘要并以相同格式保存
驗(yàn)證:
String s = "Name: AndroidManifest.xml\r\nSHA1-Digest: 0PnkK7LHsHJFjCfDfz0BeFyCqLU=\r\n\r\n";
MessageDigest messageDigest = MessageDigest.getInstance("SHA1");
messageDigest.update(s.getBytes());
byte[] digest = messageDigest.digest();
System.out.println(new String(Base64.getEncoder().encode(digest), "ASCII"));
輸出:
簡(jiǎn)化版:
private static void signFile(Manifest outPut) throws NoSuchAlgorithmException, IOException {
Manifest sf = new Manifest();
Attributes main = sf.getMainAttributes();
main.putValue("Signature-Version", "1.0");
main.putValue("Created-By", "1.0 (Android SignApk)");
MessageDigest messageDigest = MessageDigest.getInstance("SHA1");
PrintStream print = new PrintStream(
new DigestOutputStream(new ByteArrayOutputStream(), messageDigest),
true, "UTF-8");
outPut.write(print);
print.flush();
main.putValue("SHA1-Digest-Manifest",
new String(Base64.getEncoder().encode(messageDigest.digest()), "ASCII"));
Map<String, Attributes> entries = outPut.getEntries();
for (Map.Entry<String, Attributes> stringAttributesEntry : entries.entrySet()) {
print.print("Name: " + stringAttributesEntry.getKey() + "\r\n");
for (Map.Entry<Object, Object> att : stringAttributesEntry.getValue().entrySet()) {
print.print(att.getKey() + ": " + att.getValue() + "\r\n");
}
print.print("\r\n");
print.flush();
Attributes sfAttr = new Attributes();
sfAttr.putValue("SHA1-Digest",
new String(Base64.getEncoder().encode(messageDigest.digest()), "ASCII"));
sf.getEntries().put(stringAttributesEntry.getKey(), sfAttr);
}
sf.write(new FileOutputStream("C:\\Users\\nick\\Desktop\\CERT.SF"));
}
這里有個(gè)問(wèn)題,.SF文件在老的APK(可能是使用v1簽名?)中確實(shí)是由上面代碼生成,但是我看最新的APK文件中.SF文件內(nèi)容和.MF文件內(nèi)容一致,猜想可能是v1和v2簽名的原因,具體不詳。
3.3 簽名
上面我們得到了.SF文件的內(nèi)容,通過(guò)私鑰和公鑰就可以對(duì)其獲得簽名信息,根據(jù)簽名信息即可生成.RSA文件(沒(méi)有驗(yàn)證過(guò)程)。
4.1 v2簽名
先復(fù)制一下v1簽名的漏洞:
1、安卓在4.4中引入了新的執(zhí)行虛擬機(jī)ART,這個(gè)虛擬機(jī)經(jīng)過(guò)重新的設(shè)計(jì),實(shí)現(xiàn)了大量的優(yōu)化,提高了應(yīng)用的運(yùn)行效率。與“Janus”有關(guān)的一個(gè)技術(shù)點(diǎn)是,ART允許運(yùn)行一個(gè)raw dex,也就是一個(gè)純粹的dex文件,不需要在外面包裝一層zip。而ART的前任DALVIK虛擬機(jī)就要求dex必須包裝在一個(gè)zip內(nèi)部且名字是classes.dex才能運(yùn)行。當(dāng)然ART也支持運(yùn)行包裝在ZIP內(nèi)部的dex文件,要區(qū)別文件是ZIP還是dex,就通過(guò)文件頭的magic字段進(jìn)行判斷:ZIP文件的開(kāi)頭是‘PK’, 而dex文件的開(kāi)頭是’dex’.
2、ZIP文件的讀取方式是通過(guò)在文件末尾定位central directory, 然后通過(guò)里面的索引定位到各個(gè)zip entry,每個(gè)entry解壓之后都對(duì)應(yīng)一個(gè)文件。
通過(guò)漏洞就可以知道系統(tǒng)在解壓ZIP文件時(shí)根據(jù)其末尾來(lái)解壓,但是執(zhí)行的過(guò)程又根據(jù)其頭部來(lái)執(zhí)行,這樣就可以通過(guò)注入新的dex在頭部來(lái)實(shí)現(xiàn)攻擊的目的。
v2簽名官方文檔,以下內(nèi)容來(lái)自官方文檔:
APK 簽名方案 v2 是一種全文件簽名方案,該方案能夠發(fā)現(xiàn)對(duì) APK 的受保護(hù)部分進(jìn)行的所有更改,從而有助于加快驗(yàn)證速度并增強(qiáng)完整性保證。
使用 APK 簽名方案 v2 進(jìn)行簽名時(shí),會(huì)在 APK 文件中插入一個(gè) APK 簽名分塊,該分塊位于“ZIP 中央目錄”部分之前并緊鄰該部分。在“APK 簽名分塊”內(nèi),v2 簽名和簽名者身份信息會(huì)存儲(chǔ)在APK 簽名方案 v2 分塊中。
image.png
APK 簽名分塊
為了保持與當(dāng)前 APK 格式向后兼容,v2 及更高版本的 APK 簽名會(huì)存儲(chǔ)在“APK 簽名分塊”內(nèi),該分塊是為了支持 APK 簽名方案 v2 而引入的一個(gè)新容器。在 APK 文件中,“APK 簽名分塊”位于“ZIP 中央目錄”(位于文件末尾)之前并緊鄰該部分。
該分塊包含多個(gè)“ID-值”對(duì),所采用的封裝方式有助于更輕松地在 APK 中找到該分塊。APK 的 v2 簽名會(huì)存儲(chǔ)為一個(gè)“ID-值”對(duì),其中 ID 為 0x7109871a。
APK 簽名方案 v2 分塊
APK 由一個(gè)或多個(gè)簽名者/身份簽名,每個(gè)簽名者/身份均由一個(gè)簽名密鑰來(lái)表示。該信息會(huì)以“APK 簽名方案 v2 分塊”的形式存儲(chǔ)。對(duì)于每個(gè)簽名者,都會(huì)存儲(chǔ)以下信息:
(簽名算法、摘要、簽名)元組。摘要會(huì)存儲(chǔ)起來(lái),以便將簽名驗(yàn)證和 APK 內(nèi)容完整性檢查拆開(kāi)進(jìn)行。
表示簽名者身份的 X.509 證書(shū)鏈。
采用鍵值對(duì)形式的其他屬性。
對(duì)于每位簽名者,都會(huì)使用收到的列表中支持的簽名來(lái)驗(yàn)證 APK。簽名算法未知的簽名會(huì)被忽略。如果遇到多個(gè)支持的簽名,則由每個(gè)實(shí)現(xiàn)來(lái)選擇使用哪個(gè)簽名。這樣一來(lái),以后便能夠以向后兼容的方式引入安全系數(shù)更高的簽名方法。建議的方法是驗(yàn)證安全系數(shù)最高的簽名。
v2與v1簽名最大的區(qū)別就是v2修改了APK文件的內(nèi)容,將其簽名塊放到了APK文件中(v2簽名驗(yàn)證需要驗(yàn)證APK文件的這部分)。
簽名塊生成,這里代碼用的時(shí)ApkSignerV2.java
:
public static ByteBuffer[] sign(
ByteBuffer inputApk,
List<SignerConfig> signerConfigs)
throws ApkParseException, InvalidKeyException, SignatureException {
ByteBuffer originalInputApk = inputApk;
inputApk = originalInputApk.slice();
inputApk.order(ByteOrder.LITTLE_ENDIAN);
// 獲取EoCD位置以及對(duì)ZIP文件的校驗(yàn)
int eocdOffset = ZipUtils.findZipEndOfCentralDirectoryRecord(inputApk);
......
inputApk.clear();
ByteBuffer beforeCentralDir = getByteBuffer(inputApk, centralDirOffset);
ByteBuffer centralDir = getByteBuffer(inputApk, eocdOffset - centralDirOffset);
byte[] eocdBytes = new byte[inputApk.remaining()];
inputApk.get(eocdBytes);
ByteBuffer eocd = ByteBuffer.wrap(eocdBytes);
eocd.order(inputApk.order());
Set<Integer> contentDigestAlgorithms = new HashSet<>();
for (SignerConfig signerConfig : signerConfigs) {
for (int signatureAlgorithm : signerConfig.signatureAlgorithms) {
contentDigestAlgorithms.add(
getSignatureAlgorithmContentDigestAlgorithm(signatureAlgorithm));
}
}
// Compute digests of APK contents.
Map<Integer, byte[]> contentDigests; // digest algorithm ID -> digest
try {
// 計(jì)算內(nèi)容摘要
contentDigests =
computeContentDigests(
contentDigestAlgorithms,
new ByteBuffer[]{beforeCentralDir, centralDir, eocd});
} catch (DigestException e) {
throw new SignatureException("Failed to compute digests of APK", e);
}
// 生成簽名塊
ByteBuffer apkSigningBlock =
ByteBuffer.wrap(generateApkSigningBlock(signerConfigs, contentDigests));
centralDirOffset += apkSigningBlock.remaining();
eocd.clear();
ZipUtils.setZipEocdCentralDirectoryOffset(eocd, centralDirOffset);
originalInputApk.position(originalInputApk.limit());
beforeCentralDir.clear();
centralDir.clear();
eocd.clear();
// Insert APK Signing Block immediately before the ZIP Central Directory.
// 將內(nèi)容重組
// 1. ZIP 條目的內(nèi)容(從偏移量 0 處開(kāi)始一直到“APK 簽名分塊”的起始位置)
// 2. APK 簽名分塊
// 3. ZIP 中央目錄
// 4. ZIP 中央目錄結(jié)尾
return new ByteBuffer[]{
beforeCentralDir,
apkSigningBlock,
centralDir,
eocd,
};
}
代碼也不多,分為三步:
- 計(jì)算內(nèi)容摘要。
- 對(duì)內(nèi)容摘要進(jìn)行簽名,并生成簽名塊。
- 將簽名塊添加到原APK文件內(nèi)容中。
4.1 計(jì)算內(nèi)容摘要
第 1、3 和 4 部分的完整性通過(guò)其內(nèi)容的一個(gè)或多個(gè)摘要來(lái)保護(hù),這些摘要存儲(chǔ)在 signed data
分塊中,而這些分塊則通過(guò)一個(gè)或多個(gè)簽名來(lái)保護(hù)。
第 1、3 和 4 部分的摘要采用以下計(jì)算方式,類(lèi)似于兩級(jí) Merkle 樹(shù)。 每個(gè)部分都會(huì)被拆分成多個(gè)大小為 1 MB(220 個(gè)字節(jié))的連續(xù)塊。每個(gè)部分的最后一個(gè)塊可能會(huì)短一些。每個(gè)塊的摘要均通過(guò)字節(jié) 0xa5
的連接、塊的長(zhǎng)度(采用小端字節(jié)序的 uint32 值,以字節(jié)數(shù)計(jì))和塊的內(nèi)容進(jìn)行計(jì)算。頂級(jí)摘要通過(guò)字節(jié) 0x5a
的連接、塊數(shù)(采用小端字節(jié)序的 uint32 值)以及塊的摘要的連接(按照塊在 APK 中顯示的順序)進(jìn)行計(jì)算。摘要以分塊方式計(jì)算,以便通過(guò)并行處理來(lái)加快計(jì)算速度。
生成代碼如下:
/**
* 計(jì)算內(nèi)容摘要
*
* @param digestAlgorithms 摘要算法
* @param contents 內(nèi)容,這里三塊
* @return 摘要Map
* @throws DigestException
*/
private static Map<Integer, byte[]> computeContentDigests(
Set<Integer> digestAlgorithms,
ByteBuffer[] contents) throws DigestException {
// 計(jì)算分成1M大小的數(shù)量
int chunkCount = 0;
for (ByteBuffer input : contents) {
chunkCount += getChunkCount(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
}
// 設(shè)置摘要算法和摘要內(nèi)容的Map
final Map<Integer, byte[]> digestsOfChunks = new HashMap<>(digestAlgorithms.size());
for (int digestAlgorithm : digestAlgorithms) {
int digestOutputSizeBytes = getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
byte[] concatenationOfChunkCountAndChunkDigests =
new byte[5 + chunkCount * digestOutputSizeBytes];
concatenationOfChunkCountAndChunkDigests[0] = 0x5a;
setUnsignedInt32LittleEngian(
chunkCount, concatenationOfChunkCountAndChunkDigests, 1);
digestsOfChunks.put(digestAlgorithm, concatenationOfChunkCountAndChunkDigests);
}
int chunkIndex = 0;
byte[] chunkContentPrefix = new byte[5];
chunkContentPrefix[0] = (byte) 0xa5;
// Optimization opportunity: digests of chunks can be computed in parallel.
// 遍歷內(nèi)容
for (ByteBuffer input : contents) {
while (input.hasRemaining()) {
// 檢查剩下的大小,取其和1M中小的哪個(gè)
int chunkSize =
Math.min(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
final ByteBuffer chunk = getByteBuffer(input, chunkSize);
// 遍歷摘要算法
for (int digestAlgorithm : digestAlgorithms) {
String jcaAlgorithmName =
getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
MessageDigest md;
try {
md = MessageDigest.getInstance(jcaAlgorithmName);
} catch (NoSuchAlgorithmException e) {
throw new DigestException(
jcaAlgorithmName + " MessageDigest not supported", e);
}
chunk.clear();
// 設(shè)置chunkContentPrefix為0xa5 + chunk.remaining()
setUnsignedInt32LittleEngian(chunk.remaining(), chunkContentPrefix, 1);
md.update(chunkContentPrefix);
md.update(chunk);
// 獲得剛才保存的摘要算法對(duì)應(yīng)的內(nèi)容,0xa5+length,剩下全為0
byte[] concatenationOfChunkCountAndChunkDigests =
digestsOfChunks.get(digestAlgorithm);
// 期望的長(zhǎng)度
int expectedDigestSizeBytes =
getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
// 通過(guò)摘要算法后已經(jīng)修改內(nèi)容的長(zhǎng)度
// 這里是將concatenationOfChunkCountAndChunkDigests內(nèi)容更新為最新的摘要
int actualDigestSizeBytes =
md.digest(
concatenationOfChunkCountAndChunkDigests,
5 + chunkIndex * expectedDigestSizeBytes,
expectedDigestSizeBytes);
if (actualDigestSizeBytes != expectedDigestSizeBytes) {
throw new DigestException(
"Unexpected output size of " + md.getAlgorithm()
+ " digest: " + actualDigestSizeBytes);
}
}
chunkIndex++;
}
}
// 對(duì)concatenationOfChunkCountAndChunkDigests也就是我們每一塊 0xa5 + chunkCount + (0xa5+length+內(nèi)容的摘要)* chunkCount
Map<Integer, byte[]> result = new HashMap<>(digestAlgorithms.size());
for (Map.Entry<Integer, byte[]> entry : digestsOfChunks.entrySet()) {
int digestAlgorithm = entry.getKey();
byte[] concatenationOfChunkCountAndChunkDigests = entry.getValue();
String jcaAlgorithmName = getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
MessageDigest md;
try {
md = MessageDigest.getInstance(jcaAlgorithmName);
} catch (NoSuchAlgorithmException e) {
throw new DigestException(jcaAlgorithmName + " MessageDigest not supported", e);
}
result.put(digestAlgorithm, md.digest(concatenationOfChunkCountAndChunkDigests));
}
return result;
}
原理也簡(jiǎn)單,就是將其他部分內(nèi)容分成1M大小的塊,每個(gè)塊的摘要均通過(guò)字節(jié) 0xa5
的連接、塊的長(zhǎng)度(采用小端字節(jié)序的 uint32 值,以字節(jié)數(shù)計(jì))和塊的內(nèi)容進(jìn)行摘要計(jì)算,將計(jì)算的結(jié)果放到以0xa5+length
開(kāi)頭的數(shù)組中,最后將其進(jìn)行摘要計(jì)算。
4.2 簽名并且生成簽名塊
簽名時(shí)對(duì)我們剛剛得到的摘要信息進(jìn)行簽名,簽名的過(guò)程無(wú)非就是通過(guò)公鑰和私鑰進(jìn)行簽名計(jì)算,生成對(duì)應(yīng)的簽名信息(簽名過(guò)程省略)。
簽名塊的生成:
/**
* 生成簽名塊
*
* @param apkSignatureSchemeV2Block apk簽名
* @return
*/
private static byte[] generateApkSigningBlock(byte[] apkSignatureSchemeV2Block) {
// FORMAT:
// uint64: size (excluding this field)
// repeated ID-value pairs:
// uint64: size (excluding this field)
// uint32: ID
// (size - 4) bytes: value
// uint64: size (same as the one above)
// uint128: magic
int resultSize =
8 // size
+ 8 + 4 + apkSignatureSchemeV2Block.length // v2Block as ID-value pair
+ 8 // size
+ 16 // magic
;
ByteBuffer result = ByteBuffer.allocate(resultSize);
result.order(ByteOrder.LITTLE_ENDIAN);
long blockSizeFieldValue = resultSize - 8;
// size of block,以字節(jié)數(shù)(不含此字段)計(jì) (uint64)
result.putLong(blockSizeFieldValue);
long pairSizeFieldValue = 4 + apkSignatureSchemeV2Block.length;
// size
result.putLong(pairSizeFieldValue);
// id 0x7109871a
result.putInt(APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
// (size - 4) bytes簽名塊
result.put(apkSignatureSchemeV2Block);
// size of block,以字節(jié)數(shù)計(jì) - 與第一個(gè)字段相同 (uint64)
result.putLong(blockSizeFieldValue);
// magic“APK 簽名分塊 42”(16 個(gè)字節(jié))
result.put(APK_SIGNING_BLOCK_MAGIC);
return result.array();
}
簽名塊按照格式:
“APK 簽名分塊”的格式如下(所有數(shù)字字段均采用小端字節(jié)序):
size of block,以字節(jié)數(shù)(不含此字段)計(jì) (uint64)
帶 uint64 長(zhǎng)度前綴的“ID-值”對(duì)序列:
ID (uint32)
value(可變長(zhǎng)度:“ID-值”對(duì)的長(zhǎng)度 - 4 個(gè)字節(jié))
size of block,以字節(jié)數(shù)計(jì) - 與第一個(gè)字段相同 (uint64)
magic“APK 簽名分塊 42”(16 個(gè)字節(jié)
生成。
4.3 生成簽名后APK
生成簽名后的APK很簡(jiǎn)單,我們已經(jīng)獲得了每塊的內(nèi)容:
1. Contents of ZIP entries
2. Central Directory
3. End of Central Directory
APK Signing Block
我們只需要將內(nèi)容合并即可,合并順序?yàn)椋?/p>
1. Contents of ZIP entries
2. APK Signing Block
3. Central Directory
4. End of Central Directory
代碼為:
// Insert APK Signing Block immediately before the ZIP Central Directory.
// 將內(nèi)容重組
// 1. ZIP 條目的內(nèi)容(從偏移量 0 處開(kāi)始一直到“APK 簽名分塊”的起始位置)
// 2. APK 簽名分塊
// 3. ZIP 中央目錄
// 4. ZIP 中央目錄結(jié)尾
return new ByteBuffer[]{
beforeCentralDir,
apkSigningBlock,
centralDir,
eocd,
};
5 后記
啰里啰唆說(shuō)了一大堆,終于將簽名過(guò)程寫(xiě)完了。在Android APK安裝時(shí)肯定會(huì)有對(duì)APK的簽名信息驗(yàn)證的過(guò)程,這部分如果有時(shí)間去看Android APK安裝流程時(shí)再仔細(xì)分析了。