小红书x-mini-s1算法逆向分析
这个算法很久之前搞的,放个老版本的逆向过程出来吧
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,之前分析过mua和sig就知道这个函数是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 通道上的私信交互流程同样跑通了。
写这篇文章主要是把算法还原的过程简要记录下来,分享给有同样兴趣的小伙伴——毕竟这些链路确实挺绕的,独立研究并不轻松,花了不少时间才把算法完整还原并做到与真机一致
实际验证了账号注册、发文、评论、私信等场景都能正常跑通,也算没白忙活。
更多推荐



所有评论(0)