跳到正文
hello world
文章封面

微信读书APP签名逆向libencrypt.so

最近在分析微信读书接口时,发现请求中存在signature参数。

最开始尝试:

SHA256(timestamp+deviceId+random)

但是生成结果和真实signature不一致。

因此判断真实签名逻辑并不在Java层,而是在Native层的libencrypt.so中实现。

本文记录一次完整的逆向过程,目标是定位

weread.encrypt.EncryptUtils

中的

nativeGetSignatures

方法,并还原签名生成流程。


一、HookJava层定位入口

首先定位Java层:

weread.encrypt.EncryptUtils

找到:

nativeGetSignatures(String[]strArr)

方法。

通过FridaHook调试:

Java.perform(function () {

    const EncryptUtils =
        Java.use(
            "weread.encrypt.EncryptUtils"
        );


    EncryptUtils.nativeGetSignatures
        .implementation = function(arr) {


            console.log(
                "参数数量:"
                + arr.length
            );


            for(let i = 0; i < arr.length; i++){

                console.log(
                    "arg[" + i + "]="
                    + arr[i]
                );

            }


            let result =
                this.nativeGetSignatures(arr);


            console.log(
                "返回:"
                + result
            );


            return result;

        };

});

运行后:

发现返回值长度:

64位:
dc3ea89d77e43ce790a0b9dfbddd02a2a8d3525abd5f35e4ecd7c822d1f32cfb

64位十六进制字符串符合SHA256输出特征。

因此初步判断:

signature=SHA256(...)

二、定位libencrypt.so

Java层最终进入Native。

找到:

libencrypt.so

其中核心JNI函数:

Java_weread_encrypt_EncryptUtils_nativeGetSignatures

继续分析,发现主要调用:

weread::GenSignature(n)

真正的签名生成逻辑在:weread::GenSignature中。


三、分析Native层参数处理

主动调用调试:

发现Java层提交的数组进入Native后,并不是直接拼接。

首先进行了sort()排序。

也就是说,Java层传入:

[
参数1,
参数2,
参数3
]

进入Native后:

排序后的参数

才参与后续签名计算。

因此:

参数顺序非常重要。

不能直接按照调用处看到的顺序进行拼接。


四、发现隐藏salt

继续分析:

在:

weread::GenSignature

中发现最终参与签名的数据比传入数组多一个字段。

原本:

3个参数

经过处理后:

4个参数

多出来的就是:

salt

最终类似:

参数1
+
参数2
+
参数3
+
salt

五、分析salt生成方式

继续跟踪,发现调用:

weread::RemapString()

进行字符串重映射。

Native中保存了一张:

256字节查找表

转换逻辑:

输入字符

↓

ASCII值作为索引

↓

查询byte数组

↓

生成新的字符串

Python还原:

def transform_string(input_str):

    byte_Index = [
        0x34,
        0xCA,
        0x55,
        0x40,
        0x1D,
        0xB6,
        0x93,
        0xC6,
        # ...
        # 完整256字节表
        0xB7,
        0x15
    ]


    result = []


    for char in input_str:

        index = ord(char)


        if index < 0 or index >= len(byte_Index):

            raise ValueError(
                f"{char} 超出范围"
            )


        result.append(
            byte_Index[index]
        )


    return bytes(result)

将IDA中找到的256字节表复制出来,即可还原真实salt。


六、分析Hash流程

继续跟踪:

发现签名并不是简单一次SHA256。

实际流程:

两次SHA256

并且每次Hash前都会进行一次数据重排序。

完整流程:

输入4个参数

↓

sort排序

↓

追加salt

↓

第一次数据重排序

↓

SHA256

↓

第二次数据重排序

↓

SHA256

↓

输出signature

七、自定义数据重排序算法

通过IDA分析发现:

每次SHA256前都会计算一个校验值。

1.计算校验值

通过异或:

checksum=byte1^byte2^byte3...

得到:

0-10

范围内的值。


2.根据校验值循环位移

根据checksum:

对输入数据进行循环移动。

类似:

def reorder(data):

    checksum = 0


    for b in data:

        checksum ^= b


    offset = checksum % 10


    return (
        data[offset:]
        +
        data[:offset]
    )

八、最终签名流程

最终还原:

Java层参数

↓

nativeGetSignatures

↓

weread::GenSignature

↓

参数sort排序

↓

RemapString生成salt

↓

追加salt

↓

第一次数据重排序

↓

SHA256

↓

第二次数据重排序

↓

SHA256

↓

signature

拿到signature后那不就随便造了么

评论

填写昵称与邮箱即可评论,无需登录。

推荐阅读