Featured image of post 某物newSign参数逆向分析

某物newSign参数逆向分析

准备

首先肯定是抓包啊,我抓的是这个 login 这个接口,它里面会携带一个 newSign 的参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "cipherParam": "userName",
  "countryCode": 86,
  "loginToken": "",
  "newSign": "b119ee2cd2aa8e3a7bff1d18abf63027",
  "password": "ca8f119a27ec17f98b463807cd0b6b62",
  "platform": "android",
  "timestamp": "1766477585241",
  "type": "pwd",
  "userName": "df0751c510410bb57cd24c5e5dc90b86_1",
  "v": "5.75.5"
}

其他的好像除了 adi 不是固定的,其他都是固定的

这个 newSign 长度是 32 位,很有可能是 md5 或者 hmacMd5 之类的算法
hook 一下 newStringUTF 这个函数,如果是 so 层生成的算法,应该就会返回相应的结果,因为需要将 C 字符串转换成 java 字符串

 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
var symbols = Module.enumerateSymbolsSync("libart.so");
//2 定义一个变量,用来接收一会找到的NewStringUTF的地址
var addrNewStringUTF = null;
//3 循环找出libart.so中所有成员,匹配是NewStringUTF的函数,取出地址,赋值给上面的变量
for (var i = 0; i < symbols.length; i++) {
    //3.1 取出libart.so的一个个方法对象
    var symbol = symbols[i];
    //3.2 判断方法对象的名字是不是包含 NewStringUTF和CheckJNI---》因为在真正底层,函数名不叫NewStringUTF,前后有别的字符串
    // 实际它真正的名字:asdfa_NewStringUTF_dadsfasfd
    if (symbol.name.indexOf("NewStringUTF") >= 0 && symbol.name.indexOf("CheckJNI") < 0) {
        // 3.3 找到后,把地址赋值个上面的变量
        addrNewStringUTF = symbol.address;
        // 3.4 控制台打印一下
        console.log("NewStringUTF is at ", symbol.address, symbol.name);
        break
    }
}
// 4 如果不为空,我们开始hook它(通过地址hook,有onEnter和onExit,所有的参数都给了args,通过位置取到每个参数)
if (addrNewStringUTF != null) {
    Interceptor.attach(addrNewStringUTF, {
        onEnter: function (args) {
            // 4.1 取出NewStringUTF传入的第一个参数
            var c_string = args[1];
            // 4.2 第一个参数是c的字符串,我们把它转一下,变成真正的字符串
            var dataString = c_string.readCString();
            // 4.3 改字符串不为空,且长度为32,我们输出一下,并且打印出它的调用栈
            if (dataString) {
                // 只需要改这里后期
                console.log(dataString);
                // 4.4 读取当前在执行那个so文件,及so文件中的地址
                console.log(Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n') + '\n');
                // 4.5 打印调用栈
                console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
            }

        }
    });
}

但是当我 hook 上去之后,发现并没有返回哪个 md5 值,那有没有可能是 so 层生成的一串密文,然后 Java 层得到这串密文进行的 md5 的结果呢?可以用 hook 脚本看一下是否在 java 层生成的 md5 加密结果

发现确实是在 Java 层生成的 md5 值
那接着就需要找这个明文是在哪儿生成的,再次 hook newStringUTF
得到了它的调用栈

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQXC+/dIN8Kof9Gm2x1kil7S/8E3s013q5Me8muLoeGr7kDHtlbjkJRJP84YRY4V2sjuZnELPxA8zqBmuSkbZUY5RSg82smLag2bANscc9kl6AYGRYCtwcvmKHI5NH8DB84aGmjSiqtgZBmq19mDo6H/vq4W20lY8vhiPbLZjRm7v0nbhKx0wBmg5NvC3qt4bxAA0JZKeGf/03ivn7QFFKJo6J2QjBBVujmzF1vg4Bdhhgg==
0x6f01f83ab8 frida-agent-64.so!0x1eaab8
0x6f01f83618 frida-agent-64.so!0x1ea618
0x6f01f83618 frida-agent-64.so!0x1ea618

java.lang.Throwable
        at com.shizhuang.duapp.common.helper.ee.DuHelper.encodeByte(Native Method)
        at lte.NCall.IL(Native Method)
        at com.shizhuang.duapp.common.helper.ee.DuHelper.doWork(Unknown Source:18)
        at lte.NCall.IL(Native Method)
        at com.shizhuang.duapp.common.utils.RequestUtils.e(Unknown Source:28)
        at lte.NCall.IL(Native Method)
        at com.shizhuang.duapp.common.helper.net.interceptor.HttpRequestInterceptor.intercept(Unknown Source:18)
        at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10)
        at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1)
        at cb.b.intercept(SourceFile:130)
        at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10)
        at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1)

去看一下 encodeByte 这个方法,它是在 native 层的方法,果然

frida hook 一下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Java.perform(function () {
    var DuHelper = Java.use("com.shizhuang.duapp.common.helper.ee.DuHelper");
    function bytesToString(byteArray) {
        if (byteArray === null) return "null";
        try {
            return Java.use("java.lang.String").$new(byteArray);
        } catch (e) {
            return "decode error";
        }
    }
    DuHelper["encodeByte"].implementation = function (bArr, str) {
        console.log(`DuHelper.encodeByte is called: bArr=${bytesToString(bArr)}`);
        console.log(`DuHelper.encodeByte str=${str}`);
        let result = this["encodeByte"](bArr, str);
        console.log(`DuHelper.encodeByte result=${result}`);
        return result;
    };
})

得到的结果如下:

1
2
3
bArr=cipherParamuserNamecountryCode86loginTokenpasswordca8f119a27ec17f98b463807cd0b6b62platformandroidtimestamp1766477470308typepwduserNamedf0751c510410bb57cd24c5e5dc90b86_1uuid8271c326e36d0dc4v5.75.5
DuHelper.encodeByte str=010110100010001010010010000011000111001011101010101000101110111010011010101101101010001000101100010110100010001010011010110011001111001011100010101000100100110010110010100010101011110010111100
DuHelper.encodeByte result=dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQXC+/dIN8Kof9Gm2x1kil7S/8E3s013q5Me8muLoeGr7kDHtlbjkJRJP84YRY4V2sjuZnELPxA8zqBmuSkbZUY5R6UalVRwH6x+GSNamBQsKTwEzP7gb2kPhj7/7yLrvQ95ipDatc7p/W38oetoOdXRWRISLjDrLzA9PnQhm+Lk2r/jKJYnSxepP8bY/ZX4WU0XFAXAehqQ1BJBFGjsSnetxvzAHF4+DtgFTBeeRX1EtTA==

参数 1,data 里面的值直接拼接起来,参数 2 这些 01 字符串好像是固定的,不固定再说 hook 一下 registerNative 看一下是在哪个 so 中注册的

1
2
3
4
5
6
7
8
9
==========================

来自<libdewuhelper.so>
函数名字encodeByte
函数签名([BLjava/lang/String;)Ljava/lang/String;
当前函数绝对地址0x6ee2afa724
偏移地址0x1724

==========================

Unidbg 模拟调用

 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
package com.dewu;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.file.FileResult;
import com.github.unidbg.file.IOResolver;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;

import java.io.File;

public class DeWu extends AbstractJni implements IOResolver {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;
    private final DvmClass DuHelper;

    public DeWu() {
        // 创建模拟器实例(根据 SO 架构选择 32/64 位,这里使用 64 位)
        emulator = AndroidEmulatorBuilder.for64Bit()
                .setProcessName("com.shizhuang.duapp")
                .build();

        // 获取内存操作接口
        final Memory memory = emulator.getMemory();
        // 设置 Android 版本(23 对应 Android 6.0)
        memory.setLibraryResolver(new AndroidResolver(23));

        // 创建 Dalvik 虚拟机(可传入 APK 文件进行解析)
        vm = emulator.createDalvikVM(new File("apks/dewu/得物5.75.5.apk"));
        vm.setJni(this);           // 设置 JNI 接口
        vm.setVerbose(true);       // 开启详细日志(调试时建议打开)

        // 加载目标 SO
        DalvikModule dm = vm.loadLibrary("dewuhelper", true);
        dm.callJNI_OnLoad(emulator); // 调用 JNI_OnLoad

        module = dm.getModule();

        // 获取目标 Java 类
        DuHelper = vm.resolveClass("com/shizhuang/duapp/common/helper/ee/DuHelper");
    }

    public String encodeByte(byte[] bArr, String str) {
        DvmObject<?> result = DuHelper.callStaticJniMethodObject(
                emulator,
                "encodeByte([BLjava/lang/String;)Ljava/lang/String;",
                new ByteArray(vm, bArr),
                new StringObject(vm, str)
        );

        return result.getValue().toString();
    }

    public static void main(String[] args) {
        DeWu deWu = new DeWu();

        String newSign = deWu.encodeByte(
                "cipherParamuserNamecountryCode86loginTokenpasswordca8f119a27ec17f98b463807cd0b6b62platformandroidtimestamp1766477470308typepwduserNamedf0751c510410bb57cd24c5e5dc90b86_1uuid8271c326e36d0dc4v5.75.5".getBytes(),
                "010110100010001010010010000011000111001011101010101000101110111010011010101101101010001000101100010110100010001010011010110011001111001011100010101000100100110010110010100010101011110010111100"
        );

        System.out.println("NewSign:: " + newSign);
    }

    @Override
    public FileResult resolve(Emulator emulator, String pathname, int oflags) {
        System.out.println("file open::"+pathname);
        return null;
    }
}

这个参数直接返回结果了

1
2
JNIEnv->NewStringUTF("dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQXC+/dIN8Kof9Gm2x1kil7S/8E3s013q5Me8muLoeGr7kDHtlbjkJRJP84YRY4V2sjuZnELPxA8zqBmuSkbZUY5R6UalVRwH6x+GSNamBQsKTwEzP7gb2kPhj7/7yLrvQ95ipDatc7p/W38oetoOdXRWRISLjDrLzA9PnQhm+Lk2r/jKJYnSxepP8bY/ZX4WU0XFAXAehqQ1BJBFGjsSnetxvzAHF4+DtgFTBeeRX1EtTA==") was called from RX@0x1200185c[libdewuhelper.so]0x185c
NewSign:: dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQXC+/dIN8Kof9Gm2x1kil7S/8E3s013q5Me8muLoeGr7kDHtlbjkJRJP84YRY4V2sjuZnELPxA8zqBmuSkbZUY5R6UalVRwH6x+GSNamBQsKTwEzP7gb2kPhj7/7yLrvQ95ipDatc7p/W38oetoOdXRWRISLjDrLzA9PnQhm+Lk2r/jKJYnSxepP8bY/ZX4WU0XFAXAehqQ1BJBFGjsSnetxvzAHF4+DtgFTBeeRX1EtTA==

而且这个参数我每次运行,它的结果都是一样的,说明并没有时间戳或者随机数这些随机因子的参与

我把 so 放到 ida 中,发现直接打不开,是 so 加壳了

先用大佬的脚本把 so dump 下来,接着使用 soFixer 进行脱壳修复

 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
function dump_so(so_name) {
    Java.perform(function () {
        var currentApplication = Java.use("android.app.ActivityThread").currentApplication();
        var dir = currentApplication.getApplicationContext().getFilesDir().getPath();
        var libso = Process.getModuleByName(so_name);

        console.log("[name]:", libso.name);
        console.log("[base]:", libso.base);
        console.log("[size]:", ptr(libso.size));
        console.log("[path]:", libso.path);

        var file_path = dir + "/" + libso.name + "_" + libso.base + "_" + ptr(libso.size) + ".so";
        var file_handle = new File(file_path, "wb");

        if (file_handle && file_handle != null) {
            Memory.protect(ptr(libso.base), libso.size, 'rwx');
            var libso_buffer = ptr(libso.base).readByteArray(libso.size);
            file_handle.write(libso_buffer);
            file_handle.flush();
            file_handle.close();
            console.log("[dump]:", file_path);
        }
    });
}

dump_so("libdewuhelper.so");

接着进行修复

1
 .\SoFixer-Windows-64.exe -s libdewuhelper.so_0x6ed92e7000_0x7000.so -o libdewuhelper_fix.so -m 0x6ed92e7000 -d

将修复过后的 so 文件拖到 ida 中看一下吧,发现现在就可以正常打开了,使用 ida 的插件 findcrypt 过一下

发现这个不是很复杂,点 AES 的进去看一下
这么好心吗,符号都没去

1
debugger.addBreakPoint(module.base+0x2EC4);

看一下参数 1 和参数 2 代表什么

 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
mx0

>-----------------------------------------------------------------------------<
[20:21:12 851]x0=RW@0x1231a000, md5=02696f4b9754b0aa1bf0b8668de15e38, hex=636970686572506172616d757365724e616d65636f756e747279436f646538366c6f67696e546f6b656e70617373776f72646361386631313961323765633137663938623436333830376364306236623632706c6174666f726d616e64726f696474696d657374616d70313736363437
size: 112
0000: 63 69 70 68 65 72 50 61 72 61 6D 75 73 65 72 4E    cipherParamuserN
0010: 61 6D 65 63 6F 75 6E 74 72 79 43 6F 64 65 38 36    amecountryCode86
0020: 6C 6F 67 69 6E 54 6F 6B 65 6E 70 61 73 73 77 6F    loginTokenpasswo
0030: 72 64 63 61 38 66 31 31 39 61 32 37 65 63 31 37    rdca8f119a27ec17
0040: 66 39 38 62 34 36 33 38 30 37 63 64 30 62 36 62    f98b463807cd0b6b
0050: 36 32 70 6C 61 74 66 6F 72 6D 61 6E 64 72 6F 69    62platformandroi
0060: 64 74 69 6D 65 73 74 61 6D 70 31 37 36 36 34 37    dtimestamp176647
^-----------------------------------------------------------------------------^
mx1

>-----------------------------------------------------------------------------<
[20:21:15 101]x1=RW@0x12319000, md5=e608ea25e8d9a43a3bcbdfaa22e60d30, hex=64323435613062613864363738613631000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
size: 112
0000: 64 32 34 35 61 30 62 61 38 64 36 37 38 61 36 31    d245a0ba8d678a61
0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
^-----------------------------------------------------------------------------^

参数 1 是明文啊,就是传进去的参数,参数 2 就是 AES 的 key,没有 iv,ECB 模式
那 d245a0ba8d678a61 大概率是 key 了,刚好 AES128,16 个字节

接着看一下 userName 和 password,这个直接在 Java 层加密的,之前的 hook 脚本就能够体现到 这个是 userName

这个是 password,就一个 md5 加密