V2簽名分析
簡單過一下V1簽名多渠道方案
V1簽名原理
v1 簽名方案重要的原理就是對 apk 中所有的文件計算摘要 保存到 MANIFEST.MF 文件中;然後計算 MANIFEST.MF 中每個條目的摘要以及 MANIFEST.MF 本身的摘要,保存到 CERT.SF 文件中;最後用私鑰對 CERT.SF 文件加密,然後保存到 CERT.RSA 文件中(這個文件還包含了簽名算法、公鑰信息)
(1) .MF文件
apk當中的原始文件信息用摘要算法如SHA1計算得到的摘要信息並用base64編碼保存,以及對應採用的摘要算法如SHA1(這個算法的特性是不管多大的文件內容都能夠得到長度相同的摘要信息但是不同的文件內容信息得到的摘要信息肯定不同)
(2) .SF文件
.MF文件的摘要信息以及.MF文件當中每個條目在用摘要算法計算得到的摘要信息並用base64編碼保存
(3) .RSA文件
存放證書信息,公鑰信息,以及用私鑰對.SF文件的加密數據即簽名信息,這段數據是無法僞造的,除非有私鑰,另外.RSA文件還記錄了所用的簽名算法等信息。
Apk包在安裝的時候,是按照從(3)到(1)的順序依次校驗的,先用公鑰還原簽名信息,然後和.SF文件中的信息比對,然後用同樣的摘要算法對.MF文件裏面的每一個條目計算對應的摘要信息,然後比對.MF文件是否一致
V1多渠道方案(1)
向META-INF目錄中創建文件寫入渠道信息,因爲在v1簽名驗證時候,並不會驗證META-INF的除了.MF文件的其它文件
V1多渠道方案(2)
在APK的任意位置填寫空文件夾(注意是空文件夾),在apk簽名過程中,只會對apk中的所有文件進行摘要+簽名的,不會對文件夾進行這樣的操作(文件夾無法計算摘要),所以也不會驗證這個文件夾
V1多渠道方案(3)
這個就要了解下APK結構(其實就是ZIP的結構)了,看下面的圖
從圖中我們能看出,有一個End of Central Directory的數據塊,這塊是什麼呢?其實這塊就是結尾信息,裏面記錄了部分信息,簡稱EOCD
我們主要看最後有一個註釋長度的標識,那我們是不是可以通過這個加入渠道呢?當然可以,這個只是zip文件的一個註釋字段,所以並不影響ZIP裏面的文件,所以更不會影響APK的簽名,Apk安裝的前提是要把Zip解壓,在進行驗簽名等過程,所以想要多渠道修改APK第一點就是保證Zip結構的正確,第二點纔是不違背簽名
只需要在Zip結尾處增加一個註釋即可
邏輯代碼:
/**
* 讀取渠道信息
* @throws IOException
*/
private static void readCannel() throws IOException {
File in = new File("/Users/wenyingzhi/Desktop/Study/apksign/app-out.apk");
RandomAccessFile inR = new RandomAccessFile(in, "r");
// 讀取渠道長度(讀取後4個字節)
inR.seek(inR.length() - 4);
ByteBuffer commentLengthBytes = ByteBuffer.allocate(4);
commentLengthBytes.order(ByteOrder.LITTLE_ENDIAN);
inR.readFully(commentLengthBytes.array(), commentLengthBytes.arrayOffset(), commentLengthBytes.capacity());
// 得到註釋長度了
int commentLength = commentLengthBytes.getInt();
System.out.println("當前註釋長度:" + commentLength);
// 移動文件指針,讀取備註 文件長度 - 存儲註釋長度的int(佔4位) -註釋長度
inR.seek(inR.length() - 4 - commentLength);
// 讀取註釋內容
ByteBuffer commentBytes = ByteBuffer.allocate(commentLength);
commentBytes.order(ByteOrder.LITTLE_ENDIAN);
inR.readFully(commentBytes.array(), commentBytes.arrayOffset(), commentBytes.capacity());
String comment = new String(commentBytes.array());
System.out.println("渠道信息:" + comment);
}
/**
* 寫渠道信息
*/
private static void writeCannel() {
try {
// 原始包
File in = new File("/Users/wenyingzhi/Desktop/Study/apksign/app-release.apk");
// 生成的新的渠道包
File out = new File("/Users/wenyingzhi/Desktop/Study/apksign/app-out.apk");
FileUtil.copyFile(new FileInputStream(in), new FileOutputStream(out));
// 渠道信息
byte[] channelData = "channel_value".getBytes();
// 渠道長度
byte[] length = StreamTool.intToByte(channelData.length);
// 渠道信息的buffer
ByteBuffer channelBuffer = ByteBuffer.allocate(channelData.length + length.length);
channelBuffer.order(ByteOrder.LITTLE_ENDIAN);
// 寫入渠道信息
channelBuffer.put(channelData);
// 寫入渠道信息的長度
channelBuffer.put(length);
// todo 開始寫入渠道信息
// 計算備註長度
byte[] commentLength = StreamTool.shortToByte((short) (channelData.length + length.length));
RandomAccessFile outR = new RandomAccessFile(out, "rw");
// 移動位置,向前移動2個字節
outR.seek(outR.length() - 2);
// 修改原來的備註長度,改成
outR.write(commentLength);
// 寫入註釋信息
outR.write(channelBuffer.array());
outR.close();
D("寫入渠道包完成");
} catch (Exception e) {
e.printStackTrace();
}
}
V2渠道原理
學習之前先閱讀下這篇文章要翻牆哦從這篇文章中我們有一個很重要的圖
我們從上一篇位置中得知V2簽名是按照Apk分塊簽名的(1024kb做一次摘要計算),然後將簽名信息保存在Apk的指定位置(上門的紅色區域),所以V1簽名的所有方案都不行了,因爲V1的所有方案都會改變Apk一定字節,導致簽名不正確,試想下Apk簽名是最後生成的,所以這塊數據是絕對不會驗籤的,我感覺我說了句屁話
v2簽名的原理可以簡單理解爲:
我們的apk其實是個zip,我們可以理解爲3塊:塊1+塊2+塊3
簽名讓我們的apk變成了4部分:塊1+簽名塊+塊2+塊3
V2多渠道包實現原理
定位簽名塊位置
我們通過zip文件格式,知道末尾都有一塊Eocd塊中16到20字節存儲的是,核心目錄開始位置相對於archive開始的位移(簡單理解就是快3的其實位置或簽名塊的結束位置),有了這數據我們就能快速定位到簽名塊的結束位置了,只需要在得到簽名塊的大小就能讀取出簽名塊的所有數據,看下簽名塊的結構
這裏要注意一個,簽名塊大小記錄的值是指的紅色區域對應的大小
通過簽名塊的結構我們可以讀取EOCD中央偏移量-24,在讀取8位轉long即可得到簽名塊的大小了
/**
* 獲取簽名塊的開始位置
* @throws IOException
*/
private static long getSignStartIndex() throws IOException {
File file = new File("/Users/wenyingzhi/Desktop/Study/apksign/app-release.apk");
RandomAccessFile r = new RandomAccessFile(file, "r");
// 讀取EOCD渠道中央偏移量 6=註解長度short佔2個字節+偏移量int佔4個字節
r.seek(r.length() - 6);
ByteBuffer buffer = ByteBuffer.allocate(4);
buffer.order(ByteOrder.LITTLE_ENDIAN);
r.readFully(buffer.array(), buffer.arrayOffset(), buffer.capacity());
int centralOffset = buffer.getInt();
System.out.println("中央偏移量:" + centralOffset);
// 計算簽名大小位置 24=魔術16字節+簽名大小long8字節
int singLengthOffset = centralOffset - 24;
// 移動位置
r.seek(singLengthOffset);
// 讀取簽名大小
ByteBuffer singLengthbuffer = ByteBuffer.allocate(8);
singLengthbuffer.order(ByteOrder.LITTLE_ENDIAN);
r.readFully(singLengthbuffer.array(), singLengthbuffer.arrayOffset(), singLengthbuffer.capacity());
long singLength = singLengthbuffer.getLong();
System.out.println("簽名長度大小:" + singLength);
// 通過 簽名塊的開始位置 = 中央偏移量 - 簽名塊大小 - 8(頭部的簽名大小)
long singStart = centralOffset - singLength - 8;
r.close();
return singStart;
}
簽名塊中增加渠道信息
我們根據上面的簽名塊圖可以在圖中黃色區域加入Key-Value,其實讀取和增加是一個原理,我們直貼讀取的代碼了,增加只需要忘指定位置寫即可
/**
* 讀取簽名的key-value
*/
private static void readSignKeyValue() {
try {
// 獲取簽名塊的開始位置
long signStartIndex = getSignStartIndex();
File file = new File("/Users/wenyingzhi/Desktop/Study/apksign/app-release.apk");
RandomAccessFile r = new RandomAccessFile(file, "r");
// 讀取開頭的簽名塊長度
r.seek(signStartIndex);
ByteBuffer startSignLengthbuffer = ByteBuffer.allocate(8);
startSignLengthbuffer.order(ByteOrder.LITTLE_ENDIAN);
r.readFully(startSignLengthbuffer.array(), startSignLengthbuffer.arrayOffset(), startSignLengthbuffer.capacity());
// 簽名塊 開始的 簽名長度
long startSignLength = startSignLengthbuffer.getLong();
System.out.println("開始簽名長度:" + startSignLength);
// 讀取key-value的簽名數據 讀取長度=簽名塊長度-16位魔數-8位結尾簽名長度
ByteBuffer signLengthbuffer = ByteBuffer.allocate((int) (startSignLength - 24));
signLengthbuffer.order(ByteOrder.LITTLE_ENDIAN);
r.readFully(signLengthbuffer.array(), signLengthbuffer.arrayOffset(), signLengthbuffer.capacity());
// 遍歷簽名信息
while (signLengthbuffer.hasRemaining()) {
// 讀取ID-value-size
long size = signLengthbuffer.getLong();
// 讀取id
int id = signLengthbuffer.getInt();
// 讀取對用的data 長度=size-id佔4個字節
byte[] dataBytes = new byte[(int) (size - 4)];
signLengthbuffer.get(dataBytes);
// 十六進制 7109871a 存儲的就是簽名信息
System.out.println(Integer.toHexString(id) + ":數據長度" + dataBytes.length);
}
// 讀取結尾簽名長度 這個值應該和開始的是一個值
ByteBuffer endSignLengthbuffer = ByteBuffer.allocate(8);
endSignLengthbuffer.order(ByteOrder.LITTLE_ENDIAN);
r.readFully(endSignLengthbuffer.array(), endSignLengthbuffer.arrayOffset(), endSignLengthbuffer.capacity());
long endSignLong = endSignLengthbuffer.getLong();
System.out.println("結尾簽名長度:" + endSignLong + " 開始簽名長度:" + startSignLength);
// 讀取16位
byte[] dataBytes = new byte[16];
r.readFully(dataBytes);
System.out.println("魔數讀取數據:" + Arrays.toString(dataBytes));
r.close();
} catch (Exception e) {
e.printStackTrace();
}
}
修改EOCD中央偏移量
這個就很簡單了,只需要把seek(lenght-16)讀取原來的中央偏移量,新的中央偏移量=原來中央偏移量+渠道數據長度+4+8爲何要加4加8是因爲key佔4個字節,總長度佔8個字節
結尾
好啦,這樣就能實現多渠道打包了