美团walle多渠道打包工具的原理分析以及python实现

简介

这篇文章主要讲了美团walle多渠道打包工具的基本原理,并且基于此原理利用python实现类似功能,然后用walle工具将python写入的渠道号读出,python采用3.6版本实现
关键字:walle python 多渠道 android v2签名

原理介绍

android的apk是基于zip格式压缩的,所以在文件打包生成的apk文件中,存储格式是严格依照zip的标准的,另外android利用v2签名生成的apk,是在zip的标准格式之中插入了一个 apk sign block


签名前后的apk格式

除了apk sign block 可以插入很多数据外,其他三个区域是几乎不能修改的(End of Central Directory 区块里面有个标志位表示Central Directory 区块的起始位置,这个是要改的,其他内容完全不能改变)
代码要做的事情就是先找到apk sign block 区块,然后插入我们要加入的渠道信息然后把关联的字节内容改下即可
所以问题第一步是怎么找到apk sign block 这个区块

1.1 apk sign block 区块查找

查找方式我们是按照倒过来查找到,先找找到 End of Central Directory 区块,然后根据 End of Central Directory区块里面的标志Central Directory的开头位置到Central Directory,Central Directory前面就是apk sign block
那么怎么能找到 End of Central Directory?这需要看zip格式的定义


zip 格式中 End of Central Directory

简单的来说就是从文件最后的22个字节开始往前遍历,直到找到符合 0x50 0x4b 0x05 0x06 组合的4个字节(注:zip文件中字节按照 little-endian 方式存放的),这就是 End of Central Directory的开头,理论上android的apk应该就是倒数第22个字节为End of Central Directory的开头,然后End of Central Directory区域的第17个字节到第20个字节记录的就是Central Directory的位置,这个位置可以直接索引到Central Directory的开头,同时在插入数据后这个位置的值需要改变
接下来就是apk sign block的文档说明:


apk sign block的文档说明

意思就是在Central Directory的前16个字节对应的ascii值为“APK Sig Block 42”这个校验通过说明此文件为v2签名过的apk,否则不是我们就不应该继续改动
然后“APK Sig Block 42”前面8个字节表示apk sign block区域除了开8个字节以外的长度,这个值需要和开头8个字节的一致,这样在Central Directory的开头再往前加apk sign block区块size再加8就得到了apk sign block 的开头位置
对应的python代码如下
def pack(filebytes):
    # apk文件的字节
    readbytes = bytearray(filebytes)
    # 从倒数22个字节开始向前查找End of central directory
    for i in range(len(readbytes) - 22):
        if (readbytes[-22 - i] == 0x50 and readbytes[-21 - i] == 0x4b and readbytes[-20 - i] == 0x05 and readbytes[
                -19 - i] == 0x06):
            print("find i is %d" % i)
            break
    # 标记着start of central directory的4个字节的位置以前读出来的值
    start_central_directory_value_pos = -6 - i
    start_central_directory = readbytes[start_central_directory_value_pos + 3] * 256 ** 3 + readbytes[
                                                                                                start_central_directory_value_pos + 2] * 256 ** 2 + \
                              readbytes[
                                  start_central_directory_value_pos + 1] * 256 + \
                              readbytes[start_central_directory_value_pos]

    # 校验 APK Sig Block 42是否在 start_central_directory之前
    if readbytes[start_central_directory - 16:start_central_directory] == 'APK Sig Block 42'.encode("utf-8"):
        print("check v2 true")
    else:
        print("check v2 fail")
        return
        # 读取sign_block长度
    sign_block_size = 0
    sgin_block_value_pos = start_central_directory - 16 - 8
    for i in range(8):
        sign_block_size += 256 ** i * readbytes[sgin_block_value_pos + i]
    print("sigin size is" + str(sign_block_size))
    # sign_block的开头位置
    sign_block_start_pos = start_central_directory - sign_block_size - 8
1.2 将渠道信息写入APK Sig Block

从apk sign block的文档说明可以看到 在apk sign block的开头位置后8个字节开始,是一序列的id-value pairs,这些id - value paies 是由 8个字节的 id+values 的总长度,4个字节的id长度,和长度可变的values组成,为了简单处理可以限定它的长度小于等于251 即 id+value小于等于0xff,这样我们在sgin_block_value_pos之前插入渠道数据即可,同时看了下walle读取渠道数据的源码,它在读取一个id为0x71777777的 json字符串,渠道信息存储为{"channel":"xxxx"},这里的id需要按照ittle-endian 方式存放,即 0x77 0x77 0x77 0x71的顺序插入,json串转为utf-8的字节数组,不需要改变顺序(注:此为walle的读取规则,这个理论上是可以自定义的),这里我们就写渠道号”002“和"百度"试一下
整个python代码如下,上面的代码包含在这个里面

# encoding=utf-8
filepath = "/Users/tom/Downloads/OneDevice/app/build/outputs/apk/app-release.apk"
onlysee = False


# filepath="/Users/tom/Downloads/OneDevice/app/build/outputs/apk/app-release-channel002.apk"
# onlysee=True

def getvaluefrombytes(tbytes):
    va = 0
    for j in range(len(tbytes)):
        va += tbytes[j] * 256 ** j
    return va


def decodebytesfromnum(num, lens=4):
    tbytes = bytearray()
    for i in range(lens):
        tbytes.append((num & (0xff << 8 * i)) >> 8 * i)
    return tbytes


def pack(filebytes,channelname):
    # apk文件的字节
    readbytes = bytearray(filebytes)
    # 从倒数22个字节开始向前查找End of central directory
    for i in range(len(readbytes) - 22):
        if (readbytes[-22 - i] == 0x50 and readbytes[-21 - i] == 0x4b and readbytes[-20 - i] == 0x05 and readbytes[
                -19 - i] == 0x06):
            print("find i is %d" % i)
            break
    # 标记着start of central directory的4个字节的位置以前读出来的值
    start_central_directory_value_pos = -6 - i
    start_central_directory = readbytes[start_central_directory_value_pos + 3] * 256 ** 3 + readbytes[
                                                                                                start_central_directory_value_pos + 2] * 256 ** 2 + \
                              readbytes[
                                  start_central_directory_value_pos + 1] * 256 + \
                              readbytes[start_central_directory_value_pos]

    # 校验 APK Sig Block 42是否在 start_central_directory之前
    if readbytes[start_central_directory - 16:start_central_directory] == 'APK Sig Block 42'.encode("utf-8"):
        print("check v2 true")
    else:
        print("check v2 fail")
        return
        # 读取sign_block长度
    sign_block_size = 0
    sgin_block_value_pos = start_central_directory - 16 - 8
    for i in range(8):
        sign_block_size += 256 ** i * readbytes[sgin_block_value_pos + i]
    print("sigin size is" + str(sign_block_size))
    # sign_block的开头位置
    sign_block_start_pos = start_central_directory - sign_block_size - 8

    k = 0;
    keybytes = {}
    tempbytes = []
   # 打印区块里面的所有内容,调试用,没实际意义
    for i in range(sign_block_start_pos + 8, sgin_block_value_pos):
        if k == 4:
            print("----", end=" ")
        if k == 8:
            print("")
            k = 0
        print(hex(readbytes[i]), end=" ")
        k += 1
    # 如果插入值的话,block的长度和centroffset要变,对应位置的字节也要变
    strkey = 0
    print("start ouptaaa")
    k = 0
    #walleid 0x71777777
    walleIDbytes = bytearray()
    walleIDbytes.append(0x77)
    walleIDbytes.append(0x77)
    walleIDbytes.append(0x77)
    walleIDbytes.append(0x71)
    print(hex(walleIDbytes[0]))
    pairlen = 0
    # 遍历区块,打印里面所有的id_value组合出来看看
    for i in range(sign_block_start_pos + 8, sgin_block_value_pos):
        tempbytes.append(readbytes[i])
        if k == 7:
            pairlen = getvaluefrombytes(tempbytes)
            print("pairlen is %d" % pairlen)
            tempbytes.clear()
        if k == 11:
            strkey = getvaluefrombytes(tempbytes)
            tempbytes.clear()
        if k == pairlen + 7:
            keybytes[strkey] = bytes(tempbytes)
            tempbytes.clear()
            k = -1
        k += 1
    print("lask k is %d" % k)
    print(keybytes)

    #
   # if onlysee:
    #    return
    channel = "{\"channel\":\"%s\"}"%channelname
    channelbytes = bytearray(channel.encode("utf-8"))
    if len(channelbytes) > 252:
        print("只接受252字符以下的渠道信息")
        return
    insertbytes = bytearray()
    #插入的渠道号信息长度+id 4个字节长度
    #因为限定了小于0xff,所以对应的8个字节长度为 value 0 0 0 0 0 0 0
    channelbytes_len = len(channelbytes) + 4
    insertbytes.append(channelbytes_len)
    for i in range(7):
        insertbytes.append(0)
      #插入id和渠道值
    for i in walleIDbytes:
        insertbytes.append(i)
    for i in channelbytes:
        insertbytes.append(i)
    print(insertbytes)
    #因为要插入数据,所以对应的start_central_directory的值和sign_block_size的值要发生变化
    start_central_directory += len(insertbytes)
    dictoffsetbytes = decodebytesfromnum(start_central_directory)
    for index in range(4):
        readbytes[start_central_directory_value_pos + index] = dictoffsetbytes[index]
    sign_block_size += len(insertbytes)
    signsizebytes = decodebytesfromnum(sign_block_size, 8)
    for index in range(8):
        readbytes[sign_block_start_pos + index] = signsizebytes[index]
        readbytes[sgin_block_value_pos + index] = signsizebytes[index]
      # 将要插入的渠道数据格式的idvalue字节插入文件bytes中
    for b in insertbytes[::-1]:
        print(hex(b))
        readbytes.insert(sgin_block_value_pos, b)
    #输出文件
    outfilepath = "/Users/tom/Downloads/OneDevice/app/build/outputs/apk/app-release-channel%s.apk"%channelname
    with open(outfilepath, "wb") as fout:
        fout.write(readbytes)


readbytes = bytearray()
with open(filepath, "rb") as f:
    readbytes = bytearray(f.read())
#写个002和百度的测试一下

pack(readbytes,"002")
pack(readbytes,"百度")


代码里面filepath是打好了v2签名的apk包,
android代码里面依赖walle,然后我在一个按钮里面加入

findViewById(R.id.fab_4) .setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(MainActivity.this, WalleChannelReader.getChannel(getApplicationContext()),Toast.LENGTH_SHORT).show();

            }
        });

执行结果如下

执行结果

总结

这篇文章主要就是从底层分析了下walle构建多渠道包的原理,整体来说了解了原理对于工作中的使用还是有所帮助的

参考链接

1 维基百科 zip格式 可能需要翻墙
2 android v2签名官方文档 需要翻墙
3 walle 美团技术文档

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容