这个算法很久之前搞的,放个老版本的逆向过程出来吧

base64解码后对比:

000100000001ea06190d1fa214512beb5a50b7dd34cf93961a630baa1656c05b35beb244f3868efac83abd8794d357b26e0d04c1ed93a1e136d2356937a0
000100000001cd2e683e55d0035f2435a31ad9ca704f8c79050f46cb08f5aa76644b279adbdb8db46e141ff5b6b617982f0f35d5c91dca1ed6b08f7180fd

对比后发现前面12个字符都是固定的000100000001

0000: 41 45 41 41 41 41 42 36 67 59 5A 44 52 2B 69 46    AEAAAAB6gYZDR+iF
0010: 46 45 72 36 31 70 51 74 39 30 30 7A 35 4F 57 47    FEr61pQt900z5OWG
0020: 6D 4D 4C 71 68 5A 57 77 46 73 31 76 72 4A 45 38    mMLqhZWwFs1vrJE8
0030: 34 61 4F 2B 73 67 36 76 59 65 55 30 31 65 79 62    4aO+sg6vYeU01eyb
0040: 67 30 45 77 65 32 54 6F 65 45 32 30 6A 56 70 4E    g0Ewe2ToeE20jVpN
0050: 36 41 3D 00 00 00 00 0C DC 13 40 00 00 00 00 00    6A=.......@.....


414541414141423667595a44522b694646457236317051743930307a354f57476d4d4c71685a577746733176724a453834614f2b736736765965553031657962673045776532546f654532306a56704e36413d


AAEAAAAB6gYZDR+iFFEr61pQt900z5OWGmMLqhZWwFs1vrJE84aO+sg6vYeU01eybg0Ewe2ToeE20jVpN6A=

开始追踪:
在这里插入图片描述

在trace日志搜索5a596736
在这里插入图片描述

搜索abs=0xbfff9b50追踪写入点:

在这里插入图片描述

搜索找不到写入的位置,全部是加载数据的,按经验来看可能是单字节写入,而不是四字节写入,那么先找到[libtiny.so 0x4f9360]

在这里插入图片描述

发现所属的函数是sub_4F8FA0,之前分析过muasig就知道这个函数是xhs的自定义memcpy函数,经过测试直接搜索0xbfff9b50可以找到单字节处理:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

0x5a开始追踪,发现是从 获取的:

在这里插入图片描述

在这里插入图片描述

那么看一下0x6dd808这个地址:

在这里插入图片描述

byte_102220是个数组:

在这里插入图片描述

暂时没看出来是什么,那么 地址0x6dd808 发现是base64的码表:

在这里插入图片描述

解密base64看看:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

为了确保绝对正确,我们需要去验证下。因为该址所在的函数是sub_6DD6D4,复制伪代码让chatgpt分析一下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

chatgpt帮我们分析出来也是base64,而且还分析出来a4是输入的指针,那么hook0x6DD6D4函数,看一下输入的数据:

在这里插入图片描述

0000: 00 01 00 00 00 01 EA 06 19 0D 1F A2 14 51 2B EB    .............Q+.
0010: 5A 50 B7 DD 34 CF 93 96 1A 63 0B AA 16 56 C0 5B    ZP..4....c...V.[
0020: 35 BE B2 44 F3 86 8E FA C8 3A BD 87 94 D3 57 B2    5..D.....:....W.
0030: 6E 0D 04 C1 ED 93 A1 E1 36 D2 35 69 37 A0 

000100000001ea06190d1fa214512beb5a50b7dd34cf93961a630baa1656c05b35beb244f3868efac83abd8794d357b26e0d04c1ed93a1e136d2356937a0

把这个数据base64编码回去也是正确的:

在这里插入图片描述

刚才我们发现每次加密前面几位都是一样的,那么也就是0001000000是固定的,而01ea06190d1fa214512beb5a50b7dd34cf93961a630baa1656c05b35beb244f3868efac83abd8794d357b26e0d04c1ed93a1e136d2356937a0是变化的,这是我们要分析的

我们可以hook 小红书自定义的memcpy函数:

public void watchMemery(long offset, int regNum, int arraySize) {

    this.emulator.attach().addBreakPoint(this.module, offset, new BreakPointCallback() {
        @Override
        public boolean onHit(Emulator<?> emulator, long address) {
            RegisterContext registerContext = emulator.getContext();
            Pointer argPtr = registerContext.getPointerArg(regNum);
            if (arraySize > 0){
                Inspector.inspect(argPtr.getByteArray(regNum, arraySize), String.format("[watchMemery] reg[x%s]: %s", regNum, argPtr)
                );
            }

            // 有时候寄存器值必须通过getXPointer获取,原因待研究 todo
//                argPtr = ((BackendArm64RegisterContext) registerContext).getXPointer(regNum);
//                if (argPtr.toString().contains(value)) {
//                    return false; // 暂停
//                }
            return true; // 不会暂停
        }
    });
}

tiny.watchMemery(0x4F8FA0, 1, 500);  // 自定义memcpy

然后尝试搜索ea06190d,发现最早是在0x41088b80地址写入的,但是这里开头的01没有,而是从第二个字节EA开始写入的,估计01是单独处理的:

在这里插入图片描述

traceWrite(0x41088b80, 0x41088b80 + 0x3e)看看是那里写入的:

在这里插入图片描述

trace文件搜索0x40619f6c,看到数据是保存到q寄存器的,这里由于unidbg默认trace没有打印出q寄存器,非常不便于分析:

在这里插入图片描述

怎么办呢,记得之前如画星球有讲过这个https://articles.zsxq.com/id_vs7wjwb3c6t8.html:

在这里插入图片描述

另外看到另一个luo大佬的改造似乎更加全面:

在这里插入图片描述

这里我们优先尝试更全面的:

package com.github.unidbg;

import capstone.api.Instruction;
import com.github.unidbg.arm.Cpsr;
import com.github.unidbg.arm.backend.Backend;
import unicorn.Arm64Const;
import unicorn.ArmConst;

import java.math.BigInteger;
import java.util.Arrays;
import java.util.Locale;

final class RegAccessPrinter {

    private final long address;
    private final Instruction instruction;
    private final short[] accessRegs;
    private boolean forWriteRegs;

    public RegAccessPrinter(long address, Instruction instruction, short[] accessRegs, boolean forWriteRegs) {
        this.address = address;
        this.instruction = instruction;
        this.accessRegs = accessRegs;
        this.forWriteRegs = forWriteRegs;
    }

    public void print(Emulator<?> emulator, Backend backend, StringBuilder builder, long address) {
        if (this.address != address) {
            return;
        }
        for (short reg : accessRegs) {
            int regId = instruction.mapToUnicornReg(reg);
            if (emulator.is32Bit()) {
                if ((regId >= ArmConst.UC_ARM_REG_R0 && regId <= ArmConst.UC_ARM_REG_R12) ||
                        regId == ArmConst.UC_ARM_REG_LR || regId == ArmConst.UC_ARM_REG_SP ||
                        regId == ArmConst.UC_ARM_REG_CPSR) {
                    if (forWriteRegs) {
                        builder.append(" =>");
                        forWriteRegs = false;
                    }
                    if (regId == ArmConst.UC_ARM_REG_CPSR) {
                        Cpsr cpsr = Cpsr.getArm(backend);
                        builder.append(String.format(Locale.US, " cpsr: N=%d, Z=%d, C=%d, V=%d",
                                cpsr.isNegative() ? 1 : 0,
                                cpsr.isZero() ? 1 : 0,
                                cpsr.hasCarry() ? 1 : 0,
                                cpsr.isOverflow() ? 1 : 0));
                    } else {
                        int value = backend.reg_read(regId).intValue();
                        builder.append(' ').append(instruction.regName(reg)).append("=0x").append(Long.toHexString(value & 0xffffffffL));
                    }
                }
            } else {
                if ((regId >= Arm64Const.UC_ARM64_REG_X0 && regId <= Arm64Const.UC_ARM64_REG_X28) ||
                        (regId >= Arm64Const.UC_ARM64_REG_X29 && regId <= Arm64Const.UC_ARM64_REG_SP)) {
                    if (forWriteRegs) {
                        builder.append(" =>");
                        forWriteRegs = false;
                    }
                    long value = backend.reg_read(regId).longValue();
                    builder.append(' ').append(instruction.regName(reg)).append("=0x").append(Long.toHexString(value));
                } else if (regId == Arm64Const.UC_ARM64_REG_NZCV) {
                    if (forWriteRegs) {
                        builder.append(" =>");
                        forWriteRegs = false;
                    }
                    Cpsr cpsr = Cpsr.getArm64(backend);
                    if (cpsr.isA32()) {
                        builder.append(String.format(Locale.US, " cpsr: N=%d, Z=%d, C=%d, V=%d",
                                cpsr.isNegative() ? 1 : 0,
                                cpsr.isZero() ? 1 : 0,
                                cpsr.hasCarry() ? 1 : 0,
                                cpsr.isOverflow() ? 1 : 0));
                    } else {
                        builder.append(String.format(Locale.US, " nzcv: N=%d, Z=%d, C=%d, V=%d",
                                cpsr.isNegative() ? 1 : 0,
                                cpsr.isZero() ? 1 : 0,
                                cpsr.hasCarry() ? 1 : 0,
                                cpsr.isOverflow() ? 1 : 0));
                    }
                } else if (regId >= Arm64Const.UC_ARM64_REG_W0 && regId <= Arm64Const.UC_ARM64_REG_W30) {
                    if (forWriteRegs) {
                        builder.append(" =>");
                        forWriteRegs = false;
                    }
                    int value = backend.reg_read(regId).intValue();
                    builder.append(' ').append(instruction.regName(reg)).append("=0x").append(Long.toHexString(value & 0xffffffffL));
                } else if (regId >= Arm64Const.UC_ARM64_REG_Q0 && regId <= Arm64Const.UC_ARM64_REG_Q31) {
                    if (forWriteRegs) {
                        builder.append(" =>");
                        forWriteRegs = false;
                    }
                    byte[] data = backend.reg_read_vector(reg);
                    if (data != null) {
                        builder.append(' ').append(instruction.regName(reg)).append("=0x").append(newBigInteger(data).toString(16));
                    }
                } else if (regId >= Arm64Const.UC_ARM64_REG_V0 && regId <= Arm64Const.UC_ARM64_REG_V31) {
                    if (forWriteRegs) {
                        builder.append(" =>");
                        forWriteRegs = false;
                    }
                    byte[] data = backend.reg_read_vector(reg - 124);
                    if (data != null) {
                        builder.append(' ').append(instruction.regName(reg)).append("=0x").append(newBigInteger(data).toString(16));
                    }
                }
                // 新增寄存器支持
                // 处理 S 寄存器 (单精度浮点)
                else if (regId >= Arm64Const.UC_ARM64_REG_S0 && regId <= Arm64Const.UC_ARM64_REG_S31) {
                    if (forWriteRegs) {
                        builder.append(" =>");
                        forWriteRegs = false;
                    }
                    int value = backend.reg_read(regId).intValue();
                    builder.append(' ').append(instruction.regName(reg)).append("=0x").append(String.format("%08x", value));
                }
                // 处理 D 寄存器 (双精度浮点)
                else if (regId >= Arm64Const.UC_ARM64_REG_D0 && regId <= Arm64Const.UC_ARM64_REG_D31) {
                    if (forWriteRegs) {
                        builder.append(" =>");
                        forWriteRegs = false;
                    }
                    long value = backend.reg_read(regId).longValue();
                    builder.append(' ').append(instruction.regName(reg)).append("=0x").append(String.format("%016x", value));
                }
                // 处理 H 寄存器 (半精度浮点)
                else if (regId >= Arm64Const.UC_ARM64_REG_H0 && regId <= Arm64Const.UC_ARM64_REG_H31) {
                    if (forWriteRegs) {
                        builder.append(" =>");
                        forWriteRegs = false;
                    }
                    int value = backend.reg_read(regId).intValue();
                    builder.append(' ').append(instruction.regName(reg)).append("=0x").append(String.format("%04x", value & 0xFFFF));
                }
                // 处理 B 寄存器 (字节)
                else if (regId >= Arm64Const.UC_ARM64_REG_B0 && regId <= Arm64Const.UC_ARM64_REG_B31) {
                    if (forWriteRegs) {
                        builder.append(" =>");
                        forWriteRegs = false;
                    }
                    int value = backend.reg_read(regId).intValue();
                    builder.append(' ').append(instruction.regName(reg)).append("=0x").append(String.format("%02x", value & 0xFF));
                }
                // 处理 Z 寄存器 (SVE 向量)
                else if (regId >= Arm64Const.UC_ARM64_REG_Z0 && regId <= Arm64Const.UC_ARM64_REG_Z31) {
                    if (forWriteRegs) {
                        builder.append(" =>");
                        forWriteRegs = false;
                    }
                    byte[] data = backend.reg_read_vector(regId - Arm64Const.UC_ARM64_REG_Z0 + 1);
                    if (data != null) {
                        builder.append(' ').append(instruction.regName(reg)).append("=0x").append(bytesToHex(data));
                    }
                }
                // 处理 P 寄存器 (SVE 谓词)
                else if (regId >= Arm64Const.UC_ARM64_REG_P0 && regId <= Arm64Const.UC_ARM64_REG_P15) {
                    if (forWriteRegs) {
                        builder.append(" =>");
                        forWriteRegs = false;
                    }
                    byte[] data = backend.reg_read_vector(regId);
                    if (data != null) {
                        builder.append(' ').append(instruction.regName(reg)).append("=0x").append(bytesToHex(data));
                    }
                }
            }
        }
    }

    private static BigInteger newBigInteger(byte[] data) {
        if (data.length != 16) {
            throw new IllegalStateException("data.length=" + data.length);
        }
        byte[] copy = Arrays.copyOf(data, data.length);
        for (int i = 0; i < 8; i++) {
            byte b = copy[i];
            copy[i] = copy[15 - i];
            copy[15 - i] = b;
        }
        byte[] bytes = new byte[copy.length + 1];
        System.arraycopy(copy, 0, bytes, 1, copy.length);
        return new BigInteger(bytes);
    }


    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b & 0xFF));
        }
        return sb.toString();
    }
}

改造后重新trace搜索0x40619f6c,我们可以看到q寄存器的值了:

在这里插入图片描述

但是呢在trace日志中继续往上就追踪不到q0寄存器值的写入在哪里了,那么我们先看一下0x619f6c,它所在的函数是sub_619CB4,那么可以让chatgpt分析一下函数sub_619CB4的作用:

在这里插入图片描述

原来也是内存移动/复制操作相关的,由于刚才traceWrite0x41088b80时找到了上一级调用处0x617944,发现这里就是调用了sub_619CB4函数:

在这里插入图片描述

那么对这个调用位置Hook:

tiny.doDebug(0x617944); // 经过13次按c mx0 找到目标数据

在这里插入图片描述

发现是在0x41016ec1内存处写入的目标数据,那么去trace日志中搜索abs=0x41016ec1,发现原来是4字节写入的:

在这里插入图片描述

可以看到值来自0x40619d3c: "ldr w9, [x20]" ; mem[READ] abs=0x41088bd0 x20=0x41088bd0 => w9=0xd1906ea,那么搜索abs=0x41088bd0:

在这里插入图片描述

一般来说,同个类型数据的写入都是在同一条汇编,这里可以正则搜索一下其他单字节的写入,最终发现确实有目标数据

经过多次尝试,最终搜索正则0x406b8d28.*WRITE.*abs=0x41088b

在这里插入图片描述

但是可以发现以上缺少最后8个字节:a0376935d236e1a1,也就如下图:

在这里插入图片描述

那么可能这8个字节是跟前面的是不一样的。

这里从字节0xea开始向上追踪:

在这里插入图片描述

是从内存加载来的,那么搜索abs=0x4164865c,找写入点:

在这里插入图片描述

在往上跟踪abs=0x4100aa3e就跟踪不到了,通过搜素地址0x406bd430可以找到所有的值,经过对比发现,所有的值都是从0x41000000 + offset偏移取值,也就是说x11=0x41000000是一张表,x10=0xaa3e是偏移offset,但是这些偏移offset并没有任何规律。那么就要追踪这个offset是怎么生成的。

在这里插入图片描述
由于篇幅太长了,最终经过分析还原出来完整算法,并且保证和真机计算出来的值相同:
在这里插入图片描述

小红书算法的版本一直在迭代,目前对 v9.33 的逆向分析已经全部完成,像 shield、x-mini-mua、x-mini-sig、x-mini-s1、x-mini-nsig 这些关键参数的生成链路都已梳理清楚,设备注册和指纹相关的协议细节也一并整理完毕。此外,tcp 通道上的私信交互流程同样跑通了。

写这篇文章主要是把算法还原的过程简要记录下来,分享给有同样兴趣的小伙伴——毕竟这些链路确实挺绕的,独立研究并不轻松,花了不少时间才把算法完整还原并做到与真机一致

实际验证了账号注册、发文、评论、私信等场景都能正常跑通,也算没白忙活。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐