最近还是有些忙的, 刚好需要做资源保护这一方面的东西, 顺便记录一下Ambiguity方案的一个实现过程, 回头自己忘了也可以看下, 不过资源保护这东西, 针对性还是比较强的,需要不断地有新套路, 不过对于不懂资源文件的人来说还是有一定阻挡作用的…

AndroidManifest Ambiguity这个方案实际上就是一张图:

这张图的大致意思是, AndroidManifest.xml中的属性是有对应的属性名(name)和对应的Res Id号的, 比如android:name=”xxxx”这个属性, 有一个对应的属性名(不是android:name)和对应的Res Id号(就是我们反编译经常看到的0Xxxxxxxx之类的), 当Res Id号为0(或者为一个不合法的值,比如0x01017FFF), Android系统是不会对齐进行解析的, 会无视这个属性, 但是对于apktool来说, 是会解析这个属性的, 这样的话虽然反编译的时候没什么问题, 但是在重打包的时候会因为非法Res Id导致重打包失败, 从而对app进行一定程度的保护。

那么, 要搞懂或者实测这个方案, 先要对AndroidManifest.xml这个文件了解才行, 一般说AndroidManifest.xml, 都会用看雪的某张图:

以一个示例APK的AndroidManifest.xml进行说明重点的部分

总体的(就是上面那张神图的中间的方块):

Magic Number -> 4Bytes, 固定的值0x00080003, 可以看做标识, 上图前四个字节
File Size -> 4Bytes, 整个AndroidManifest.xml的字节数, 在上图是0x00000938

接下来就是StringChunk了, 我把图稍微截大点:

Chunk Type -> 4Bytes, 固定的值0x001C0001, 上图的0x8-0xB位
Chunk Size -> 4Bytes, 整个StringChunk的大小, 上图的0xC-0xF位0x0000550
String Count -> 4Bytes, 字符串数目, 上图的0x10-0x13位0x00000022, 表示有34个字符串
Style Count -> 4Bytes, 样式数目, 上图的0x14-0x17位0x00000000, 表示有0个样式
UnKnown -> 4Bytes, 固定值0x00000000, 没用的东西
String Pool Offset -> 4Bytes, 字符串起始偏移, 这个是相对StringChunk的, 以上图为例, String Pool Offset为0x000000A4, 则起始偏移 = A4 + 8 = AC
Style Pool Offset -> 4Bytes, 样式起始偏移, 也是相对StringChunk的, 上图0x20-0x23为0x00000000, 表示没有Style
String Offset数组 -> String Count * 4Bytes, 以上图为例, 大小为 0x22 * 4 = 88, 上图的String Offset数组范围为0x24 - 0xAB
Style Offset数组 -> Style Count * 4Bytes, 跟String Offset相同, 这个例子明显没有这一块
String Data -> 字符串数据块, 下面分析
Style Data -> 样式数据块, 下面分析

首先分析String Offset数组, 上面我们说了范围是0x24-0xAC, 那么先取0x24-0x27这四位, 即第一个字符串的偏移, 为0x00000000, 即为0 + String Pool Offset + 8 = 0 + A4 + 8 = AC, 那么看一下上图的0xAC位:

0B 00 76 00 65 00 72 00 73 00 ....

这里我们要说明一个字符串在AndroidManifest.xml的存储方式是UTF16, 并且前面是先存字符数目, 最后填充00(相当于\0这种东西): 比如有一个字符串fuck, 转为十六进制ASCII码即为66 75 63 6B, 转为UTF16则为66 00 75 00 63 00 6B 00, fuck字符为四个, 那么加上04 00, 最后填充00 00, 就成了:

// fuck的最终存储形式
04 00 66 00 75 00 63 00 6B 00 00 00

这里我圈了出来:

对于Style, 跟String的分析是一样的, 可以自己拿个APK看下。

然后就是ResourceChunk了:

Chunk Type -> 4Bytes, 固定值0x00080180, 看0x00000558 - 0x0000055B位
Chunk Size -> 4Bytes, 整个ResourceChunk的大小, 以上图为例, 0x0000055C-0x0000055F位, 为0x0000002C
ResourceIds数组 -> ChunkSize / 4 - 2个, 每个4Bytes, 表示对应的Res Id值

中间有个String NameSpace Chunk 部分, 可以跳过也可以停下来看下, 范围为0x00000584 - 0x0000059B:

Chunk Type -> 4Bytes, 固定值0x00100100
Chunk Size -> 4Bytes, 这里上图的值为18即24个字节, 所以范围是0x00000584-0x0000059B
Line Number -> 4Bytes
Unknown -> 4Bytes
Prefix -> 4Bytes
Uri -> 4Bytes

最后是TagChunk部分, TagChunk有很多个Chunk(Start Tag Chunk 或者End Tag Chunk), 按示例是从0x0000059C开始:

Chunk Type -> 4Bytes, 有两个固定值, 为0x00100102表示这是一个Start Tag, 0x00100103表示这是一个End Tag
Chunk Size -> 4Bytes
Line Number -> 4Bytes, 不知道什么用
Unknown -> 4Bytes, 固定值0xFFFFFFFF, 没用的东西
Namespace uri -> 4Bytes, 这个可以不管, 对应上面的String NameSpace Chunk
Name -> 4Bytes, 对应StringChunk的字符串索引
Flags -> 4Bytes, 固定值0x00140014, 没什么用
Attr Count -> 4Bytes, 这个Tag的属性数目
Class Attribute -> 4Bytes, 不知道干什么
Attr 数组 -> 数组元素数目为Attr Count, 每个Attr都是一个20字节的结构体, 所以总长度为Attr Count * 20

上面说Attr是个20字节的结构体, 这里进行简要说明:

// uint32实际上表示是4Bytes
type Attr struct {
    Uri uint32          // 对应字符串索引, 比如上图的"application"在字符串索引是0A, Application Tag Chunk所有的attr的uri都是0A
    Name uint32         // 这个很关键, 也是个索引, 但是同时表示String和Resource索引
    String uint32       // 指向字符串索引, 当Type为0x03000008时与Data相等
    Type uint32         // 类型, 比如0x030000008 表示这是个android:name
    Data uint32         // 数据
}

上面这些东西, 不能用光看的, 要自己用一个示例一步一步分析下来才会印象深刻些, 关于分析二进制文件的, 可以用C32ASM, 或者神奇010 Editor(导入AXMLTemplate,直接都给你分好了, 看起来更容易)

把上面的结构大致了解了以后, 我们需要定义一个方案实现, 分别是针对StringChunk, ResourceChunk, 以及Application TagChunk进行手术, 在本例中用go(js用多了, 速成go感觉很不适应规则)撸了一个, 首先是StringChunk模块:

1. 确定插入字符串a, b
2. 将字符串a, b插入string data块(需要对齐, 如果上面你仔细看, 会发现都是4Bytes的段, 而字符串是UTF16即2Bytes的, 所以要进行4Bytes对齐)
3. 计算两个字符串的偏移值, 添加进string offset数组中
4. 修改起始string offset值(+8, 因为第三步增加了8个字节, 所以起始偏移值要加8)
5. 如果有style的话(判断起始style offset是否为0), 那么起始style offset需要修正(+8 + 两个字符串的长度)
6. 修正string count
7. 修正string chunk size
8. 修正fileSize

// 插入字符串aonosora.class, name
func modifyStringChunk(axml * AXML, axmlBytes []byte) []byte {
    // UTF8ToUTF16() 将其转为可以塞进二进制文件的字符串: 0x0E 0x00 0x60 0x00 0x6F 0x00 ....  0x73 0x00 0x00 0x00
    appendStr1 := UTF8ToUTF16([]byte{
        0x61,0x6F,0x6E,0x6F,0x73,0x6F,0x72,
        0x61,0x2E,0x63,0x6C,0x61,0x73,0x73,
    })
    // 同转为字符串: 0x04 0x00 0x6E 0x00 0x61 0x00 0x6D 0x00 0x65 0x00 0x00 0x00
    appendStr2 := UTF8ToUTF16([]byte{
        0x6E,0x61,0x6D,0x65,
    })

    // 计算出字符串data块长度
    var strLen uint32
    if axml.StyleOffset == 0 {
        // 没有style块的情况, string data块就是StringChunk的最后一块, 直接用ChunkSize去减
        strLen = axml.StringChunkSize - axml.StringOffset
    } else {
        // 如果有Style的情况, 字符串data块为StyleOffset - StringOffset
        strLen = axml.StyleOffset - axml.StringOffset
    }

    // string data块的结尾偏移
    strEndOffset := axml.StringOffset + 0x8 + strLen

    // 连续插入字符串
    axmlBytes = append(axmlBytes[:strEndOffset],
        append(appendStr1, append(appendStr2, axmlBytes[strEndOffset:]...)...)...)

    // 对齐String, 一般在有Style的情况才有改变效果
    // 因为String在这里是UTF16形式的, 而其他字段都是UInt32形式的, 所以要进行对齐
    // 对齐的方式在StringData的后面插入00
    strLenAlignedOffset := strLen
    strLenAligned := (strLenAlignedOffset + uint32(0x03)) & (^uint32(0x03))
    strLenAlignedOffset2 := strLenAligned + uint32(len(appendStr1))
    strLenAligned2 := (strLenAlignedOffset2 + uint32(0x03)) & (^uint32(0x03))

    // 计算出对齐需要的空白字节数
    alignBytesLen := strLenAligned2 - strLenAlignedOffset2 + strLenAligned - strLenAlignedOffset

    externSize := 0x8 + uint32(len(appendStr1)) + uint32(len(appendStr2))

    strEndOffset = strEndOffset + uint32(len(appendStr1)) + uint32(len(appendStr2))

    // 一次填充空白字节到string data块中
    for i:= 0; i < int(alignBytesLen); i++ {
        axmlBytes = append(axmlBytes[:strEndOffset], append([]byte{0x00}, axmlBytes[strEndOffset:]...)...)
        strEndOffset += 1
    }

    externSize = externSize + alignBytesLen

    // 增加StringOffset偏移索引表

    // 字符串aonosora.class的相对Offset
    // UInt32ToBytes()将一个UInt32转为一个长度为4的字节段
    str1OffsetIndex := UInt32ToBytes(strLen)
    // 字符串name的相对Offset
    str2OffsetIndex := UInt32ToBytes(strLen + uint32(len(appendStr1)))

    // 计算出string offset数组的结尾offset
    strEndOffsetIndex := 36 + axml.StringCount * 4
    // 填充两个新的字符串对应的偏移值
    axmlBytes = append(axmlBytes[:strEndOffsetIndex],
        append(str1OffsetIndex, append(str2OffsetIndex, axmlBytes[strEndOffsetIndex:]...)...)...)

    axml.StringCount = axml.StringCount + 2
    axml.StringOffset = axml.StringOffset + 0x8

    // 修正StringOffset起始偏移
    fixBytes(axmlBytes, axml.StringOffset, 28)

    // 修正StringCount
    fixBytes(axmlBytes, axml.StringCount, 16)

    // 有Style的情况要修正StyleOffset
    if axml.StyleOffset != 0 {
        axml.StyleOffset = axml.StyleOffset + externSize
        fixBytes(axmlBytes, axml.StyleOffset, 32)
    }

    // 修正StringChunkSize
    axml.StringChunkSize = axml.StringChunkSize + externSize
    fixBytes(axmlBytes, axml.StringChunkSize, 12)

    axml.FileSize = uint32(len(axmlBytes))
    // 由于增加了字节, 原先的Offset需要重新设置
    axml.ResourceChunkOffset = axml.ResourceChunkOffset + externSize
    axml.AppChunkOffset = axml.AppChunkOffset + externSize

    return axmlBytes
}

然后是ResourceChunk, 这个简单些:

1. 由上一步计算出新的String Count(字符串个数), 让这个数减去现有Resource数得出需要填充的Resource个数
2. 需要填充的Resource全是非法的Res Id值
3. 修正ResourceChunkSize(+ 填充的Resource个数 * 4)
4. 修正FileSize

func modifyResourceChunk(axml * AXML, axmlBytes []byte) []byte {
    resourceCounts := axml.ResourceChunkSize / 4 - 2
    // 计算出需要填充的ResourceIds的个数, 全部填充为0x00即可, 因为字符串的个数远大于资源的个数, 所以用StringCount - resourceCounts
    paddingCounts := axml.StringCount - resourceCounts
    // 计算出从哪个偏移开始填充
    paddedStartOffset := axml.ResourceChunkOffset + axml.ResourceChunkSize
    for i := 0; i < int(paddingCounts); i++ {
        // 这里特别对第一个插入的字符串用了个Res Id的非法值(尽管0也是非法, 其实都可以用0)
        if i == int(paddingCounts) - 2 {
            axmlBytes = append(axmlBytes[:paddedStartOffset],
                append(UInt32ToBytes(uint32(0x01017FFF)), axmlBytes[paddedStartOffset:]...)...)
        } else {
            axmlBytes = append(axmlBytes[:paddedStartOffset],
                append(UInt32ToBytes(uint32(0x00000000)), axmlBytes[paddedStartOffset:]...)...)
        }
        paddedStartOffset = paddedStartOffset + 4
    }

    // 修复ResourceChunkSize
    axml.ResourceChunkSize = axml.ResourceChunkSize + paddingCounts * 4
    fixBytes(axmlBytes, axml.ResourceChunkSize, axml.ResourceChunkOffset + 4)

    axml.FileSize = uint32(len(axmlBytes))
    // 增加字节改变Offset位置
    axml.AppChunkOffset = axml.AppChunkOffset + paddingCounts * 4
    return axmlBytes
}

最后是Application TagChunk, 相对也简单些:

1. 构造出一个Attr 结构体(属性名和Res Id索引都是我们插入的最后一个字符串的索引值, URI为Application对应结构体的URI)
2. 直接插入到这个TagChunk中
3. 修正这个TagChunk的attr count(+1)
4. 修正这个TagChunk的Chunk Size(+20)
5. 修正FileSize

func modifyTagChunk(axml * AXML, axmlBytes []byte) []byte {

    appChunkSize := BytesToUInt32(axmlBytes[axml.AppChunkOffset + 4: axml.AppChunkOffset + 8])
    appChunkAttrCount := BytesToUInt32(axmlBytes[axml.AppChunkOffset + 28: axml.AppChunkOffset + 32])
    // 取得AppChunk 结尾偏移
    appChunkEndOffset := axml.AppChunkOffset + appChunkSize

    // 表明属性名为name, 属性值为Android:name = "aonosora.class"
    attr := new(Attr)
    attr.Uri = axml.AppURIIndex
    attr.Name = axml.StringCount - 1
    attr.String = axml.StringCount - 2
    attr.Type = 0x03000008
    attr.Data = axml.StringCount - 2

    attrRefValue := reflect.ValueOf(attr).Elem()
    for i := 0; i < attrRefValue.NumField(); i++ {
        value := uint32(attrRefValue.Field(i).Uint())
        axmlBytes = append(axmlBytes[:appChunkEndOffset],
            append(UInt32ToBytes(value), axmlBytes[appChunkEndOffset:]...)...)
        appChunkEndOffset += 4
    }

    // 修正AppChunk Attr Count
    fixBytes(axmlBytes, appChunkAttrCount + 1, axml.AppChunkOffset + 28)

    // 修正AppChunk Size
    fixBytes(axmlBytes, appChunkSize + 20, axml.AppChunkOffset + 4)

    axml.FileSize = uint32(len(axmlBytes))
    return axmlBytes
}

跑一下程序, 运行出来是这个鬼样子(用的Android_Killer, 因为我特别对ShakaApktool进行了设置才能反编译, 不然连反编译都不行了, Android逆向助手跑出来是个空文件夹, ApkToolKit直接提示失败):

不过, 这种方式跟多数资源保护一样, 具有比较强的针对性(就是针对apktool这个东西), 很多的Android反编译工具(比如Android逆向助手, Android_Killer, ApkToolKit),看文件目录的话, 其实都差不多的(包含apktool, dex2jar, aapt这些基本的东西), 所以一防住apktool的话, 很多工具就是用不了的, 这对于隔离新手来说还是有用的, 缺点也是针对性比较强, 毕竟这种方式其实就是利用反编译工具的逻辑漏洞, 那么工具做一下更新就行了, apktool这个东西在github上是开源的: https://github.com/iBotPeaches/Apktool, 版本是不断更新的, 只要它做了对应的更新, 那么方案就无效了。

http://blog.csdn.net/weimeig/article/details/79637920