Android签名与渠道包制作-V2/V3渠道包原理
2021.04.09
林嘉伟
系列文章:
上一篇文章我们详细描述了V2/V3签名的原理,大概的原理的就是在APK中插入签名块保存签名信息:
APK签名块的格式如下:
V2的签名数据id为0x7109871a,V3的签名数据id为0xf05368c0。由于校验流程里面对APK本身做了校验,所以V1版本的添加zip file comment的方法就失效了。
但是由于V2/V3签名对这个APK签名块是没有做校验的,所以我们可以添加一个自定义的渠道包信息键值对保存渠道信息。这篇文章就通过分析Demo代码来讲解V2/V3渠道包的原理。
渠道信息写入
基本原理就是渠道信息打包成一个id-value键值对放到键值对序列的最后:
插入的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| @Override public boolean addChannelInfo(String srcApk, String outputApk, String channelInfo) {
if (channelInfo == null || channelInfo.isEmpty()) { return true; }
RandomAccessFile zipFile = null; FileOutputStream fos = null; FileChannel srcChannel = null; FileChannel dstChannel = null; try { zipFile = new RandomAccessFile(new File(srcApk), "r"); srcChannel = zipFile.getChannel();
fos = new FileOutputStream(outputApk); dstChannel = fos.getChannel();
ByteBuffer eocd = Utils.findEocd(srcChannel); if (eocd == null) { return false; }
long socdOffset = Utils.getSocdOffset(eocd); Utils.Pair<Long, ByteBuffer> oldSignV2Block = Utils.getSignV2Block(zipFile, socdOffset); if (oldSignV2Block == null) { return false; }
ByteBuffer newSignV2Block = addChannelInfo(oldSignV2Block.second, channelInfo);
changeSocdOffset(eocd, channelInfo);
srcChannel.position(0); Utils.copyByLength(srcChannel, dstChannel, oldSignV2Block.first);
dstChannel.write(newSignV2Block);
srcChannel.position(socdOffset); Utils.copyByLength(srcChannel, dstChannel, srcChannel.size() - socdOffset - eocd.capacity());
eocd.position(0); dstChannel.write(eocd); } catch (Exception e) { e.printStackTrace(); return false; } finally { Utils.safeClose(srcChannel, zipFile, dstChannel, fos); } return true; }
|
基本流程并不复杂,就是:
- 通过魔数查找eocd
- 在eocd中查找central directory的偏移地址socd
- socd往前读就是APK签名块
- 在APK签名块中插入渠道信息
- 由于APK签名块在socd的签名,插入渠道信息后需要将socd往后移
- 将修改后的各部分重新写入apk
socd的修改很简单,读取原来的值加上渠道信息键值对的长度重新写入即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| private void changeSocdOffset(ByteBuffer eocd, String channelInfo) {
eocd.position(Utils.EOCD_POSITION_SOCD_OFFSET); int originOffset = eocd.getInt();
eocd.position(Utils.EOCD_POSITION_SOCD_OFFSET); eocd.putInt(originOffset + Long.BYTES + Integer.BYTES + channelInfo.getBytes().length); }
|
往APK签名块中插入渠道信息稍微复杂一点。我们想将渠道信息插入到id-value键值对的最后,但是这里并没有去遍历这个键值对序列,而是用了一种取巧的方式。
由于键值对直接并没有什么指针关系去指定下一个键值对,仅仅只是将将它们排列在一起,所以我们只需要从APK签名块的末尾往前跳过16字节的魔数和8字节的长度信息就能找到插入位置的地址偏移:
由于插入之后APK签名块的长度会增加,所以长度信息需要同步修改,完整的插入代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| private static ByteBuffer addChannelInfo(ByteBuffer oldSignV2BlockSize, String channelInfo) {
long infoLength = channelInfo.getBytes().length; long channelBlockRealSize = Long.BYTES + Integer.BYTES + infoLength;
ByteBuffer buffer = ByteBuffer.allocate((int) (oldSignV2BlockSize.capacity() + channelBlockRealSize)); buffer.order(ByteOrder.LITTLE_ENDIAN);
oldSignV2BlockSize.position(0); buffer.put(oldSignV2BlockSize);
oldSignV2BlockSize.position(0); long originSize = oldSignV2BlockSize.getLong();
buffer.position(0); buffer.putLong(originSize + channelBlockRealSize);
long magicNumberSize = Utils.SIG_V2_MAGIC_NUMBER.getBytes().length; buffer.position((int) (oldSignV2BlockSize.capacity() - magicNumberSize - Long.BYTES));
buffer.putLong(infoLength + Integer.BYTES); buffer.putInt(Utils.CHANNEL_INFO_SIG); buffer.put(channelInfo.getBytes());
buffer.putLong(originSize + channelBlockRealSize);
buffer.put(Utils.SIG_V2_MAGIC_NUMBER.getBytes());
buffer.flip(); return buffer; }
|
渠道信息读取
读取部分的逻辑也比较清晰:
- 通过魔数查找eocd
- 在eocd中查找central directory的偏移地址socd
- socd往前读就是APK签名块
- 跳过APK签名块头8个字节(长度信息)就是第一个id-value键值对
- 遍历键值对查找渠道信息键值对的id
- 找到渠道信息键值对之后读取value部分返回即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| public String getChannelInfo(Context context) { String apkPath = Utils.getApkPath(context); if (apkPath == null) { return null; } RandomAccessFile apk = null; try { apk = new RandomAccessFile(apkPath, "r");
ByteBuffer eocd = Utils.findEocd(apk.getChannel()); if (eocd == null) { return null; }
Utils.Pair<Long, ByteBuffer> signV2Block = Utils.getSignV2Block(apk, Utils.getSocdOffset(eocd)); if (signV2Block == null) { return null; }
int id; long length,realLength; long positionLimit = signV2Block.second.capacity() - Long.BYTES - Utils.SIG_V2_MAGIC_NUMBER.getBytes().length;
int position = Long.BYTES;
do { signV2Block.second.position(position);
length = signV2Block.second.getLong();
realLength = Long.BYTES + length;
id = signV2Block.second.getInt();
position += realLength;
} while (id != Utils.CHANNEL_INFO_SIG && position <= positionLimit);
if (id == Utils.CHANNEL_INFO_SIG) { return Utils.readString(signV2Block.second, (int) (length - Integer.BYTES)); } return null; } catch (Exception e) { e.printStackTrace(); } finally { Utils.safeClose(apk); }
return null; }
|
完整代码
由于V2/V3的原理其实是大致相同的,都是在APK签名块里面插入id-value键值对,所以我们这个做法是能够兼容V2/V3版本的签名的。完整的demo已经上传到Github,我将添加渠道信息的操作放到了单元测试里,编译完之后执行插入渠道信息。