本文章内容仅用于逆向学习,请勿用于黑产行为,如有侵权,请联系本人删除,未经本人允许,不可转载。

抓包分析

QUIC降级:

QUIC是基于 UDP 协议

主流抓包工具,本质上是建立一个 HTTP/HTTPS 代理服务器。它们主要工作在 TCP 协议 之上

因此需要让它放弃使用UDP而使用TCP,因此要先降级处理

正常抓包只有okhttpd走代理,要先降级抓包,jadx搜索cronetConfig找到对应设置配置的地方,打开libageon.so并搜索字符串enable_quic可以找到处理配置的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
clazz.c.implementation = function(a,b){
showStacks();
console.log("[c]args1-> ",a," ","args2-> ",b);
if(a == "cronetConfig"){
var replace = "{\"enable_quic\": false, \"enable_http2\": false}"
var retval1 = this.c(a,replace);
console.log("[replace]retval-> ",retval1);
return retval1;
}
var retval3 = this.c(a,b);
console.log("[original]retval-> ",retval3);
return retval3;
}

然后就可以正常抓到带sig3的包了

使用reqable进行抓包

Java层浅析

字符串搜索sig3很容易找到传入逻辑,就不多说了(在其他例子中可能会出现没找到的情况,可以通过hook hash.put或者其他可能的java方法,很喜欢通过这些方法将header放进去,例如下面这段代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Java.perform(function () {
var HashMap = Java.use("java.util.HashMap");

HashMap.put.overload('java.lang.Object', 'java.lang.Object').implementation = function (key, value) {

if (key !== null && key.toString() === "__NS_sig3") {

showStacks();
console.log("[+] Value (__NS_sig3): " + value);
return this.put(key, value);
}
return this.put(key, value);
};
console.log("hook win -> ",HashMap);
});
1
2
[vd6.r.b]r.b is called: str=/rest/n/feed/selectionfb4e77xxxxxxx2541
[vd6.r.b]r.b result=cedfaf8xxxxx59b97998f

Java层入口传入/rest/n/feed/selectionfb4e77xxxxx2541

其中/rest/n/feed/selection是API接口,fb4e77xxxxxxx2541是sig的值

sig指纹

sig是通过传入的request得到的

sig是通过ce6.d$d.d获取的,拿一个栗子

1
[ce6.d$d.d]ce6.d$d.d is called: abi=androidApiLevel=android_os=app=appver=boardPlatform=bottom_navigation=browseType=c=cdid_tag=clientRealReportData=client_key=cold=coldStart=cold_launch_time_ms=count=country_code=cs=darkMode=ddpi=deviceBit=device_abi=did=did_gt=did_tag=earphoneMode=edgeRecoBit=edgeRerankConfigVersion=egid=ftt=grant_browse_type=hotfix_ver=injectTask=isOpenAutoPlay=is_background=isp=iuid=kcv=keyconfig_state=kpf=kpn=language=max_memory=mod=nbh=net=newOc=newUserAction=newUserRefreshTimes=oDid=oc=os=page=pcursor=pv=rdid=realShowPhotoIds=recoReportContext=sbh=seid=sh=slh=socName=source=sw=sys=teenageAge=thermal=totalMemory=uQaTag=ud=userRecoBit=ver=videoModelCrowdTag=result=

扔给gemini分析一下

第一部分:设备唯一标识 (Device Identifiers)

这些参数用于唯一标识一台设备,是风控最关注的部分。

Key Value 说明
abi ****** CPU 架构指令集 (Application Binary Interface)。
boardPlatform ****** 芯片平台代号 (这里指 Google Tensor 芯片)。
cdid_tag ****** 设备 ID 相关的标签,用于区分 ID 类型或状态。
ddpi ****** 屏幕像素密度 (Density DPI)。
deviceBit ****** 设备位宽标识 (可能指 32/64 位或其他特性)。
device_abi ****** 同 abi,设备支持的指令集。
did ****** 核心设备 ID (Device ID),最关键的风控标识。
did_gt ****** Device ID 生成的时间戳 (Generate Time)。
did_tag ****** 另一个设备 ID 标签。
egid ****** 扩展全局 ID (Extended Global ID),长指纹字符串。
max_memory ****** App 可使用的最大内存限制 (MB)。
mod ****** 手机型号 (Model)。
oDid ****** 原始设备 ID (Original Device ID),可能指未重置前的 ID。
rdid ****** 随机设备 ID (Random Device ID)。
socName ****** 芯片型号名称 (System on Chip)。
sw ****** 屏幕宽度 (Screen Width)。
sh ****** 屏幕高度 (Screen Height)。
thermal ****** 热状态/温度信息。
totalMemory ****** 设备总内存 (MB)。

第二部分:系统与环境信息 (System & Env)

这些参数描述 App 运行的软件环境。

Key Value 说明
androidApiLevel ****** Android SDK 版本 (33 对应 Android 13)。
android_os ****** 操作系统标识 (0 可能代表原生 Android)。
app ****** App 类型标识 (0 通常指主 App)。
appver ****** App 详细版本号,签名计算的关键。
channel (c) ****** App 安装渠道 (VIVO 应用商店)。
country_code ****** 国家代码 (中国)。
cs ****** 可能指 Cold Start (冷启动) 的缩写,或者是某种开关状态。
darkMode ****** 深色模式状态。
earphoneMode ****** 耳机模式状态 (0: 未插入)。
hotfix_ver ****** 热修复版本号。
is_background ****** 是否在后台运行 (1: 是)。
isp ****** 运营商信息 (Internet Service Provider)。
keyconfig_state ****** 键值配置状态。
kpf ****** 平台标识 (Kwai Platform Form - Android Phone)。
kpn ****** 产品标识 (Kwai Product Name - 快手主站)。
language ****** 系统语言。
net ****** 网络状态。
newOc ****** 新渠道标识 (New Original Channel)。
oc ****** 原始渠道 (Original Channel)。
os ****** 操作系统名称。
sbh ****** 状态栏高度 (Status Bar Height)。
sys ****** 系统版本名称。
ver ****** App 大版本号。

第三部分:业务与请求参数 (Business Logic)

这些参数随用户的具体操作(如刷新首页)而变化。

Key Value 说明
bottom_navigation ****** 是否显示底部导航栏。
browseType ****** 浏览类型 (4 可能指推荐流/发现页)。
clientRealReportData ****** 客户端实时上报数据容器。
client_key ****** 客户端密钥/标识。
cold ****** 是否冷启动状态。
coldStart ****** 明确的冷启动标记。
cold_launch_time_ms ****** 冷启动时间戳。
count ****** 请求返回的数据条数 (这里指请求 6 个视频)。
edgeRecoBit ****** 边缘推荐相关位标识。
edgeRerankConfigVersion ****** 边缘重排配置版本。
ftt ****** First Time Token 或类似首次标识。
grant_browse_type ****** 授权浏览类型 (初始化)。
injectTask ****** 注入任务标识。
isOpenAutoPlay ****** 是否开启自动播放。
iuid ****** 可能是 IMEI 或其他用户唯一 ID。
kcv ****** Key Config Version (配置版本)。
nbh ****** Navigation Bar Height (导航栏高度)。
newUserAction ****** JSON数据:新用户的交互行为记录(点击、关注、点赞均为空)。
newUserRefreshTimes ****** 新用户刷新次数。
page ****** 当前页码。
pcursor ****** 分页游标 (Page Cursor),通常配合 feed 流使用。
pv ****** Page View 标记。
realShowPhotoIds ****** 真实展示过的视频 ID 列表 (逗号分隔),用于去重。
recoReportContext ****** JSON数据:推荐上报上下文,包含 GPS 权限、刷新 ID、电池电量等详细环境信息。
seid ****** Session ID (会话 ID)。
slh ****** 可能指搜索栏高度或其他布局高度。
source ****** 来源标识。
teenageAge ****** 青少年模式年龄设置。
uQaTag ****** QA 测试标签。
ud ****** User Data 或某种用户状态标识。
userRecoBit ****** 用户推荐位标识。
videoModelCrowdTag ****** 视频模型人群标签。

然后就是一个标准MD5就能获取到649xxxxx879d(之所以跟第一次的不同是因为这不是同一个例子🤣)

sig3传入

image-20251216164501152

第一个是监控计算签名耗时(可能会拿来做检测?)

第二个是关键计算sig3函数

最后走的是com.kuaishou.android.security.internal.crypto.e.c方法,但是从jadx看该java层代码被严重混淆,jadx无法正确反汇编,因此直接阅读smali

通过gemini可以初步恢复成:

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
public String c(String str, boolean z, String str2) throws KSException {
// 1. 热修复检查 (PatchProxy) - Smali 开头部分
if (PatchProxy.isSupport(e.class)) {
return PatchProxy.accessDispatch(...);
}

try {
// 2. 构造请求对象 (Request Builder)
// 对应 Smali: Lcom/kuaishou/android/security/internal/plugin/n;->b()
n.a builder = n.b();

// 设置 AppKey, Map, Int 等基础参数
builder.a(b.i().j().a())
.a((Map) null)
.b(0);

// 设置传入的参数 z 和 str2
builder.a(z); // 对应 p2
builder.e(str2); // 对应 p3
builder.b(""); // 设置某个默认为空的字段

// 【关键】将输入的 str 转为 byte[] 放入请求对象
// 对应 Smali: getBytes("UTF-8")
byte[] strBytes = str.getBytes(Charset.forName("UTF-8"));
builder.a(strBytes);

// 构建最终请求对象 n
n requestObj = builder.a();

// 3. 获取插件接口 (Plugin Interface)
// 对应 Smali: Lcom/kuaishou/android/security/internal/plugin/bundle/d;
e dispatch = e.a(this.context);
d pluginInterface = dispatch.g();

if (pluginInterface == null) {
throw new KSException("SecurityGuardManager... return null");
}

// 4. 【核心调用】 调用接口方法 f,传入命令字 "0335"
// 对应 Smali: const-string v0, "0335"
// invoke-interface {p3, p1, v0}, ...->f(L.../n;Ljava/lang/String;)Z
boolean success = pluginInterface.f(requestObj, "0335");

// 5. 第一次尝试获取结果
if (success && requestObj.g() != null && requestObj.g().a().length > 0) {
// 成功,跳转到返回逻辑
} else {
// 失败,记录日志 "signPlus return enull"
// 6. 【重试逻辑】 再次调用一次 f
success = pluginInterface.f(requestObj, "0335");
}

// 7. 处理最终结果
if (success && requestObj.g() != null) {
byte[] resultBytes = requestObj.g().a();
if (resultBytes.length > 0) {
// 将结果字节数组转回 String 返回
String resultStr = new String(resultBytes);
return resultStr;
}
}

// 失败抛出异常
throw new KSException("signPlus return enull final");

} catch (Exception e) {
// 异常上报逻辑...
throw new KSException(...);
}
}
1
invoke-interface {p3, p1, v0}, Lcom/kuaishou/android/security/internal/plugin/bundle/d;->f(Lcom/kuaishou/android/security/internal/plugin/n;Ljava/lang/String;)Z

经过分析调用的是com.kuaishou.android.security.internal.crypto.j.f方法,

1
2
j.f is called: nVar=SecurityGuardParamContext{paramMap=null, appKey=d7b7d042-d4f2-4012-be60-d97ff2429c17, requestType=0, reserved1=null, reserved2=null, input=[47, 114, 101, 115, xxxxQwQxxxx ,99, 101, 52, 101, 99, 100, 49, 102, 97, 100, 48, 51, 99], output=com.kuaishou.android.security.internal.plugin.n$b@fd4eede, errorCode=0, privateKey=[], sdkId=, isInnerInvoke=false, did=}, str=0335
j.f result=true

image-20251216164543778

最后走到了这个函数

1
public static native Object doCommandNative(int i4, Object[] objArr);

是一个so层实现的函数

1
2
[com.kuaishou.android.security.internal.plugin.k.a]k.a is called: i4=10418, objArr=[Ljava.lang.String;@8d18d16,d7b7d042-d4f2-4012-be60-d97ff2429c17,-1,false,com.yxcorp.gifshow.App@9da18de,,false,
[com.kuaishou.android.security.internal.plugin.k.a]k.a result=5c4d3d1xxxxxe81366709050b1d

快手采用了非常经典的“通用分发”模式。它没有为每个功能写一个 JNI 函数

而是写了一个通用的 doCommandNative,通过整数 ID (i4) 来区分要执行什么功能

Ljava.lang.String;@8d18d16这里存储了处理过的字符串MD5(sig)

d7b7d042-d4f2-4012-be60-d97ff2429c17是快手AppKey

com.yxcorp.gifshow.App@9da18de为Native层留一个调用Java方法的对象

Native层浅析

入口

Hook registerNative函数获取

1
2
3
4
5
6
7
8
9
10
[+] Captured RegisterNatives for: com.kuaishou.android.security.internal.dispatch.JNICLibrary
Method count: 5
-----------------------------------------
[TARGET FOUND] Method: doCommandNative
Signature: (I[Ljava/lang/Object;)Ljava/lang/Object;
Absolute Address: 0x75f4981680
Module: libkwsgmain.so
Base Address: 0x75f4940000
!!! OFFSET: 0x41680 !!!
-----------------------------------------

libkwsgmain.so

image-20251211163631717

1
2
3
4
jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
JUMPOUT(0x4631C);
}

其实就是一个goto指令,直接patch BR X9-> B 0x4631C,IDA就会自己生成一个sub_4631C函数

该软件的花指令基本都是这个,非常简单,只需要patch一个指令即可,可以写个脚本批量匹配特征值进行patch,但是我懒,所以都是手动patch

字符串没找到但是可以通过Hook的结果直接得知对应函数是sub_41680

先unidbg搭架子,把函数运行起来先

unidbg部分在网上就很多了,基本拿来都能直接用,这里就不贴了

64位想正常运行read需要将

unidbg-android/src/main/java/com/github/unidbg/virtualmodule/android/AndroidModule.java

里的throw new BackendException();改为return read(emulator, vm);才可以正常运行

发现多次运行相同的参数结果却不相同,猜测在运行过程中调用了获取时间戳的函数,增加了随机性,在分析之前需要先固定随机

修改gettimeofday64

1
2
3
protected long currentTimeMillis() {
return 1960949932368L;//System.currentTimeMillis();
}

这样每次运行的结果都是c5d4a4xxxxxxxxx77e3909c9284

unidbg配合静态分析

目标函数存在大量ollvm混淆的代码,D-810貌似有点bug,所以我写了一个配合unidbg模拟执行然后nop掉一些无用逻辑的IDA插件

https://github.com/s1nec-1o/TraceClean

如果有用,球球star⭐

sub_11BDC函数

调用前:

1
x0=0x404d80e0 x1=0x404d3240 x2=0x30 x3=0xbffff528 x4=0xbffff540
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
>-----------------------------------------------------------------------------<
[19:44:54 838]x0=RW@0x404d80e0, md5=388eb550700dae4d7b8c77fce6fd7868, hex=d8fd06400000000050f6ffbf0000000090f5ffbf00000000670000000000000041000000000000003600000000000000404e4e40000000000000000000000000380307400000000050904d4000000000e0814d400000000000824d4000000000d80207400000000080404e4000000000
size: 112
0000: D8 FD 06 40 00 00 00 00 50 F6 FF BF 00 00 00 00 ...@....P.......
0010: 90 F5 FF BF 00 00 00 00 67 00 00 00 00 00 00 00 ........g.......
0020: 41 00 00 00 00 00 00 00 36 00 00 00 00 00 00 00 A.......6.......
0030: 40 4E 4E 40 00 00 00 00 00 00 00 00 00 00 00 00 @NN@............
0040: 38 03 07 40 00 00 00 00 50 90 4D 40 00 00 00 00 8..@....P.M@....
0050: E0 81 4D 40 00 00 00 00 00 82 4D 40 00 00 00 00 ..M@......M@....
0060: D8 02 07 40 00 00 00 00 80 40 4E 40 00 00 00 00 ...@.....@N@....
^-----------------------------------------------------------------------------^

>-----------------------------------------------------------------------------<
[19:46:25 591]RW@0x404e4e40, md5=bd4xxxx4102fcf47, hex=2f7265xxxx65637xxxxx4343634623738xxxx000000000000000xxxx33930396339323834
size: 112
0000: 2F 72 65 73 74 2F 6E 2F 66 65 65 64 2F 73 65 6C /rest/n/feed/sel
xxx
0030: 32 37 62 61 32 37 00 00 00 00 00 00 00 00 00 00 27ba27..........
0040: 63 35 64 34 61 34 38 37 39 64 33 34 32 63 66 63 c5d4a4879d342cfc
xxx
0060: 33 38 32 64 37 37 65 33 39 30 39 63 39 32 38 34 382d77e3909c9284
^-----------------------------------------------------------------------------^

>-----------------------------------------------------------------------------<
[19:45:00 748]x1=RW@0x404d3240, md5=27bxxxxxxf6de3, hex=63356xxxxxx43865386637323965383736663338326437376533393039633932383430663933386334663039393561383363396266333166306336343332323538390000000000000000000000000000000034623462383431373330666131313837
size: 112
0000: 63 35 64 34 61 34 38 37 39 64 33 34 32 63 66 63 c5d4a4879d342cfc
xxxxx
0040: 39 62 66 33 31 66 30 63 36 34 33 32 32 35 38 39 9bf31f0c64322589
0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0060: 34 62 34 62 38 34 31 37 33 30 66 61 31 31 38 37 4b4b841730fa1187
^-----------------------------------------------------------------------------^

>-----------------------------------------------------------------------------<
[19:45:11 154]x3=unidbg@0xbffff528, md5=6b63f389a162b301ff80879932d5cf04, hex=b2cbbfab951b87030ee89e8c9d6e9a5fec65610e885c1f09010000000300000000000000040000000000000000000000002007400000000000200740000000003c0000000000000000404e400000000000000000000000000000000000000000000000000000000000cf0700eec9b64f
size: 112
0000: B2 CB BF AB 95 1B 87 03 0E E8 9E 8C 9D 6E 9A 5F .............n._
0010: EC 65 61 0E 88 5C 1F 09 01 00 00 00 03 00 00 00 .ea..\..........
0020: 00 00 00 00 04 00 00 00 00 00 00 00 00 00 00 00 ................
0030: 00 20 07 40 00 00 00 00 00 20 07 40 00 00 00 00 . .@..... .@....
0040: 3C 00 00 00 00 00 00 00 00 40 4E 40 00 00 00 00 <........@N@....
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 CF 07 00 EE C9 B6 4F ...............O
^-----------------------------------------------------------------------------^

>-----------------------------------------------------------------------------<
[19:45:13 548]x4=unidbg@0xbffff540, md5=3e1e49c8021db65491c22c54bd370bb3, hex=010000000300000000000000040000000000000000000000002007400000000000200740000000003c0000000000000000404e400000000000000000000000000000000000000000000000000000000000cf0700eec9b64f220000000000000000f6ffbf000000003100000000000000
size: 112
0000: 01 00 00 00 03 00 00 00 00 00 00 00 04 00 00 00 ................
0010: 00 00 00 00 00 00 00 00 00 20 07 40 00 00 00 00 ......... .@....
0020: 00 20 07 40 00 00 00 00 3C 00 00 00 00 00 00 00 . .@....<.......
0030: 00 40 4E 40 00 00 00 00 00 00 00 00 00 00 00 00 .@N@............
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050: 00 CF 07 00 EE C9 B6 4F 22 00 00 00 00 00 00 00 .......O".......
0060: 00 F6 FF BF 00 00 00 00 31 00 00 00 00 00 00 00 ........1.......
^-----------------------------------------------------------------------------^

调用后:

1
2
3
4
5
6
7
8
9
10
11
>-----------------------------------------------------------------------------<
[19:47:13 553]x0=RW@0x404d3300, md5=6fc600b2f4392f46ab880d69b03eb31f, hex=6335xxxxx6438653866xxxxx37376533393039633932383400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
size: 112
0000: 63 35 64 34 61 34 38 37 39 64 33 34 32 63 66 63 c5d4a4879d342cfc
xxxxx
0020: 33 38 32 64 37 37 65 33 39 30 39 63 39 32 38 34 382d77e3909c9284
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 ................
^-----------------------------------------------------------------------------^

可以发现甚至在调用这个函数之前就已经存在结果了,那么肯定在之前就有赋值操作了!

image-20251214201659299

会发现其实就是v100,但是v100没有任何直接赋值的操作!

image-20251214201725361

第二个显然就不可能,第一个就是之前的原语!

我开始怀疑是不是我插件的BUG了?但是这么简单的插件怎么可能有问题呢,unidbg检测一下这个地址附近的值

1
emulator.traceWrite(0x404e4e80,0x404e4e80+0x30);
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
[20:12:51 483] Memory WRITE at 0x404e4e80, data size = 1, data value = 0x63, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
[20:12:51 483] Memory WRITE at 0x404e4e81, data size = 1, data value = 0x35, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
[20:12:51 483] Memory WRITE at 0x404e4e82, data size = 1, data value = 0x64, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
[20:12:51 483] Memory WRITE at 0x404e4e83, data size = 1, data value = 0x34, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
[20:12:51 483] Memory WRITE at 0x404e4e84, data size = 1, data value = 0x61, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
xxxxxxx
[20:12:51 483] Memory WRITE at 0x404e4e8f, data size = 1, data value = 0x63, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
[20:12:51 483] Memory WRITE at 0x404e4e90, data size = 1, data value = 0x38, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
xxxxxx
[20:12:51 483] Memory WRITE at 0x404e4e96, data size = 1, data value = 0x38, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
[20:12:51 483] Memory WRITE at 0x404e4e97, data size = 1, data value = 0x66, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
[20:12:51 483] Memory WRITE at 0x404e4e98, data size = 1, data value = 0x37, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
[20:12:51 483] Memory WRITE at 0x404e4e99, data size = 1, data value = 0x32, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
xxxx
[20:12:51 484] Memory WRITE at 0x404e4ea5, data size = 1, data value = 0x37, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
[20:12:51 484] Memory WRITE at 0x404e4ea6, data size = 1, data value = 0x65, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
[20:12:51 484] Memory WRITE at 0x404e4ea7, data size = 1, data value = 0x33,
xxxx
[20:12:51 484] Memory WRITE at 0x404e4eaf, data size = 1, data value = 0x34, PC=RX@0x40013bf0[libkwsgmain.so]0x13bf0, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
[20:12:51 484] Memory WRITE at 0x404e4eb0, data size = 1, data value = 0x00, PC=RX@0x40013c34[libkwsgmain.so]0x13c34, LR=RX@0x40013bd0[libkwsgmain.so]0x13bd0
[20:12:51 485] Memory WRITE at 0x404e4e80, data size = 8, data value = 0x3738346134643563, PC=RX@0x401dc17c[libc.so]0x1c17c, LR=RX@0x4000d908[libkwsgmain.so]0xd908
[20:12:51 485] Memory WRITE at 0x404e4e88, data size = 8, data value = 0x6366633234336439, PC=RX@0x401dc17c[libc.so]0x1c17c,
xxxxx
[20:12:51 485] Memory WRITE at 0x404e4ea8, data size = 8, data value = 0x3438323963393039, PC=RX@0x401dc18c[libc.so]0x1c18c, LR=RX@0x4000d908[libkwsgmain.so]0xd908

根据这个trace可以找到调用逻辑

1
sub_A720->sub_1E5B8->sub_3D5F4->sub_13B1C

其中有几个函数的部分函数调用是不会调用的,直接可以nop

大致知道调用顺序了,那么开始分析吧,从尾巴开始往上分析

HMAC-SHA256

sub_3D5F4image-20251214224136911

这里的v31十分可疑

image-20251214224228829

是通过23578的a2来赋值

image-20251214224310065

这里会发现调用的是下面的两个函数

通过插件能首先发现具有SHA256特征,因此尝试SHA256,但与结果存在较大差异性,然后发现这个函数多传入了两个可疑参数,经过比对分析,发现是HMAC的性能优化的实现,为了避免每次计算签名都重复进行Key ^ 0x36和Key ^ 0x5C的运算,程序预先计算好了这两个状态保存在内存中,经过unidbg打印出对应的值就能看出来,密钥:vWqd4fRXxXxxxxxxxxxeRitxT7VwbK

然后就能写出下面的脚本:

1
2
3
4
5
6
7
import hmac
import hashlib
secret_key = b"vWqd4fRxxxxxxxitxT7VwbK"
data = b"/rest/n/feed/selection62ccedxxxxxxba27"
signature = hmac.new(secret_key, data, hashlib.sha256).hexdigest()
print(f"Calculated: {signature}")
assert signature == "e46ed2xxxxxxxxxx52c08"

一般来说如果看到是SHA256但输出却不是标准SHA256,有以下几种情况:

  1. HMAC-SHA256(概率最高)
  2. 输入被加Salt或者对输入进行了预处理(前后加Salt、转hex、大小端、特殊字符)
  3. 魔改初始向量(最好判断)
  4. 魔改轮常量(修改K表,找0x428A2F98找不到就是K表被改了)
  5. 魔改逻辑/位移量(修改Sigma或Ch/Maj的位移数,很难判断,需要一行一行比对汇编逻辑)
  6. 输出后处理(trace一下,也好判断)
  7. …….

然后看到03D5F4函数

image-20251215143650309

这个函数感觉就是C++的某个库函数,涉及到流的操作,但是其实就是一个toHex函数

将类似0xC5->0x63 0x35

通过跟踪trace发现这是较早出现结果的地方

执行过三次这个toHex函数,第一次是HMAC-SHA256的写入,第二次也是某个算法,第三次就是最后的结果了,从最后一次看

image-20251215144742653

image-20251215144851860

但是发现结果是24位的显然是不对的,结果应该是48位才对,应该是后面还有拼接吧

不过这24位确实是结果

0:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__int64 *sub_2BD20()
{
unsigned __int8 v0; // w8

v0 = atomic_load((unsigned __int8 *)&byte_72808);
if ( (v0 & 1) == 0 && __cxa_guard_acquire((__guard *)&byte_72808) )// 线程安全,确保只执行一次初始化操作
{
qword_727A0 = (__int64)&off_702A8;
xmmword_727F8 = 0u;
xmmword_727E8 = 0u;
xmmword_727D8 = 0u;
xmmword_727C8 = 0u;
__cxa_atexit((void (*)(void *))&sub_301A4, &qword_727A0, &off_71000);
__cxa_guard_release((__guard *)&byte_72808);
}
return &qword_727A0;
}

这个函数是对全局this指针的初始化,并返回this指针,经过trace发现这里返回结果[3]+8的值都是固定的22

因此0是固定的0x51412200

1:

貌似也是固定的0x1db5ae7f

3:

wbAES

先dump参数

1
2
x0=
cab68c9xxxxxxxxxb3ebe0ed8ea4aab0c1d5f4519f8d19c4948f

这个x0是第二次toHex的入参

x1是0x30就是len

x2是CRC32b_poly_Constant_57C78

那就先看x0是怎么获取的

image-20251215155608558

通过sub_1E2C4函数获取

1
2
sub_26E44(v20, *a1);
sub_26A14(v20, HMAC_SHA256_final_1_1, HMAC_SHA256_final_len, src, src_len);

v20依赖全局变量,然后调用26A14

image-20251215160747750

所以v24存储的就是我们在寻找的字符串,然后看到v24是malloc出来的堆内存0x404d3240trace看看

1
2
3
4
5
6
7
8
9
[16:15:11 186] Memory WRITE at 0x404d3240, data size = 1, data value = 0xca, PC=RX@0x400265fc[libkwsgmain.so]0x265fc, LR=RX@0x4002646c[libkwsgmain.so]0x2646c
[16:15:11 186] Memory WRITE at 0x404d3241, data size = 1, data value = 0xb6, PC=RX@0x400265fc[libkwsgmain.so]0x265fc, LR=RX@0x4002646c[libkwsgmain.so]0x2646c
[16:15:11 186] Memory WRITE at 0x404d3242, data size = 1, data value = 0x8c, PC=RX@0x400265fc[libkwsgmain.so]0x265fc, LR=RX@0x4002646c[libkwsgmain.so]0x2646c
[16:15:11 186] Memory WRITE at 0x404d3243, data size = 1, data value = 0x9f, PC=RX@0x400265fc[libkwsgmain.so]0x265fc, LR=RX@0x4002646c[libkwsgmain.so]0x2646c
[16:15:11 186] Memory WRITE at 0x404d3244, data size = 1, data value = 0x78,
xxx
PC=RX@0x400265fc[libkwsgmain.so]0x265fc, LR=RX@0x4002646c[libkwsgmain.so]0x2646c
[16:15:11 188] Memory WRITE at 0x404d326e, data size = 1, data value = 0x94, PC=RX@0x400265fc[libkwsgmain.so]0x265fc, LR=RX@0x4002646c[libkwsgmain.so]0x2646c
[16:15:11 188] Memory WRITE at 0x404d326f, data size = 1, data value = 0x8f, PC=RX@0x400265fc[libkwsgmain.so]0x265fc, LR=RX@0x4002646c[libkwsgmain.so]0x2646c

位于2636C函数中,初步判断为白盒AES

根据调试可以知道大致流程如下:

首先在进入这个函数之前会将HMAC-SHA256的结果进行填充\x10到0x30个字节

然后每0x10个字节进去这个函数一次,输出0x10个字节的密文

简单补个环境

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
private void call_AES() {
String hexInput = "E46ED2xxxxxxxxB349C97C5BF21F44519D2E6452C0810101010101010101010101010101010";
byte[] inputData = hexToByteArray(hexInput);

UnidbgPointer inputPtr = emulator.getMemory().malloc(inputData.length, true).getPointer();
inputPtr.write(inputData);

UnidbgPointer outputPtr = emulator.getMemory().malloc(inputData.length, true).getPointer();

UnidbgPointer arg1Ptr = emulator.getMemory().malloc(16, true).getPointer();

System.out.println("=== 开始分块加密 (3轮 x 16字节) ===");

for (int i = 0; i < 3; i++) {
int offset = i * 16; // 每次偏移 0, 16, 32

module.callFunction(emulator, 0x2636C,
arg1Ptr, // Arg1: 固定
inputPtr.share(offset), // Arg2: 输入指针偏移
outputPtr.share(offset) // Arg3: 输出指针偏移 (结果会自动拼接)
);
}

System.out.println("Full Ciphertext (Arg3):");
Inspector.inspect(outputPtr.getByteArray(0, inputData.length), "AES Result");
}

public static byte[] hexToByteArray(String hex) {
int len = hex.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i+1), 16));
}
return data;
}

$$
\begin{array}{l}\text{state} \leftarrow \text{plaintext} \\text{AddRoundKey}(\text{state}, k_0) \\text{for } r = 1 \dots 9 \\quad \text{SubBytes}(\text{state}) \\quad \text{ShiftRows}(\text{state}) \\quad \text{MixColumns}(\text{state}) \\quad \text{AddRoundKey}(\text{state}, k_r) \\text{SubBytes}(\text{state}) \\text{ShiftRows}(\text{state}) \\text{AddRoundKey}(\text{state}, k_{10}) \\text{ciphertext} \leftarrow \text{state}\end{array}
$$

判断白盒AES-128的逻辑如下:

image-20251216122932442

这里有10轮的循环,符合AES-128的标准轮数

image-20251216123106090

看到v3,经过简单分析,发现主循环中是4次循环,而我们输入的字节数是0x10,4*4矩阵,处理4次

image-20251216123228705

这个得详细分析一下

1
2
3
4
5
*((_BYTE *)a1 + v10) = v33[v23] | (0x10 * v32[v20]);
*((_BYTE *)a1 + v9) = v27 | (16 * v21);
*((_BYTE *)a1 + v8) = v16 | (16 * v15);
v3 = v40 + 1;
*((_BYTE *)a1 + v7) = v24 | (16 * v31);

最后结果是通过两个半字节也就是一个hex拼接成一个字节

其中v33和v32是int8类型,但是前面的逻辑,它的值只会在0x00-0x0F,v27等虽然是int64但在此之前做过LOBYTE处理了,LOBYTE就是BYTE的低位,也就是半个字节

然后看到这个5AB70的表:

image-20251216131655788

显然这是一个异或表,可以将a ^ b 转换成 第a行的第b列的值

image-20251216132125439

可以掩盖AES的xor特征

sub_25938通过unidbg执行可以确定,一次执行AES执行了10次,然后详细分析就确定是行移位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
long double __fastcall sub_25938(_OWORD *a1)
{
__int64 v1; // x9
long double result; // q0
long double v3; // [xsp+8h] [xbp-18h]
__int64 v4; // [xsp+18h] [xbp-8h]

v1 = 0;
v4 = *(_QWORD *)(_ReadStatusReg(TPIDR_EL0) + 40);
do
{
*((_BYTE *)&v3 + v1) = *((_BYTE *)a1 + dword_5AB30[v1]);
++v1;
}
while ( v1 != 16 );
result = v3;
*(long double *)a1 = v3;
return result;
}

那么上述的查表操作就是字节代换+列混合+轮密钥加了

那么直接通过DFA Hook行移位函数,将其第一个参数随机替换一位

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
    private void call_AES() {
String hexInput = "E46ED2A1xxxxxxxxx1F44519D2E6452C0810101010101010101010101010101010";
byte[] inputData = hexToByteArray(hexInput);

UnidbgPointer inputPtr = emulator.getMemory().malloc(inputData.length, true).getPointer();
inputPtr.write(inputData);

UnidbgPointer outputPtr = emulator.getMemory().malloc(inputData.length, true).getPointer();

UnidbgPointer arg1Ptr = emulator.getMemory().malloc(16, true).getPointer();

// System.out.println("=== 开始分块加密 (3轮 x 16字节) ===");
emulator.attach().addBreakPoint(module.base+0x2636C);
module.callFunction(emulator, 0x2636C,
arg1Ptr, // Arg1: 固定
inputPtr, // Arg2: 输入指针偏移
outputPtr // Arg3: 输出指针偏移
);

String res = bytesToHex(outputPtr.getByteArray(0,0x10));
System.out.println(res);
}

public void callDfa(){
Debugger debugger = emulator.attach();
debugger.addBreakPoint(module.base+0x25938,new BreakPointCallback() {
int num = 1;

@Override
public boolean onHit(Emulator<?> emulator, long address) {
UnidbgPointer pointer;
RegisterContext context = emulator.getContext();

pointer = context.getPointerArg(0);
if(num%9==0){
pointer.setByte(randint(0,15),(byte) randint(0,0xff));
}
// System.out.println("callDfa num=" + num);
num+=1;
return true;
}

});
}

public static int randint(int min,int max){
Random rand = new Random();
return rand.nextInt((max-min)+1)+min;
}

public static byte[] hexToByteArray(String hex) {
int len = hex.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i+1), 16));
}
return data;
}
public static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
int unsignedInt = b & 0xff;
String hex = Integer.toHexString(unsignedInt);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}

然后在main中200次循环获取

剩余的就是使用工具获取密钥了

获取第10轮密钥:https://github.com/SideChannelMarvels/JeanGrey/tree/master/phoenixAES

获取真正的密钥:https://github.com/SideChannelMarvels/Stark

成功获取68455xxxxxxxx5A5476

image-20251216134122712

成功获取密钥

因此可以得知CRC的x0就是通过标准AES ECB 128 加密 HMAC-SHA256 结果的值

cab68c9fxxxxxxx88de61509c5dxxxxxxxxc4948fe0ed8ea4aab0c1d5f4519f8d19c4948f

结果截取96个字符就是CRC的x0了

cab68c9fxxxxxxx88de61509c5dxxxxxxxxc4948f

CRC32

1
2
3
4
5
6
.rodata:0000000000057C78 CRC32b_poly_Constant_57C78 DCD 0x4C11DB7
.rodata:0000000000057C78 ; DATA XREF: sub_11BDC+BC↑o
.rodata:0000000000057C78 ; sub_11BDC+D4↑o ...
.rodata:0000000000057C7C DCD 0xFFFFFFFF
.rodata:0000000000057C80 DCD 0xFFFFFFFF
.rodata:0000000000057C84 DCD 0x101
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
__int64 __fastcall sub_120C4(unsigned __int8 *a1, __int64 a2, unsigned int *a3)
{
int v3; // w9
unsigned int v4; // w12
int v5; // w10
int v6; // w11
int v7; // w8
__int64 v8; // x10
unsigned __int8 v9; // w8
__int64 v10; // x11
int v11; // w11
int v12; // w10
int v14; // w0
int v15; // w8
__int64 v16; // [xsp+0h] [xbp-30h]
unsigned int *v17; // [xsp+8h] [xbp-28h]
int v18; // [xsp+14h] [xbp-1Ch]
unsigned __int8 *v19; // [xsp+18h] [xbp-18h]

v3 = a3[1];
if ( *((_BYTE *)a3 + 12) )
{
v4 = *a3;
v5 = 0;
v6 = 0;
do
{
++v5;
v7 = v4 & 1 | (2 * v6);
v4 >>= 1;
v6 = v7;
}
while ( (unsigned __int16)v5 < 0x20u );
for ( ; a2; ++a1 )
{
v3 ^= *a1;
v8 = 8;
do
{
--v8;
v3 = (v3 << 31 >> 31) & v7 ^ ((unsigned int)v3 >> 1);
}
while ( v8 );
--a2;
}
}
else
{
v9 = atomic_load((unsigned __int8 *)&byte_72638);
if ( (v9 & 1) == 0 )
{
v19 = a1;
v16 = a2;
v17 = a3;
v18 = a3[1];
v14 = __cxa_guard_acquire((__guard *)&byte_72638);
v3 = v18;
a2 = v16;
a3 = v17;
v15 = v14;
a1 = v19;
if ( v15 )
{
dword_72634 = 24;
__cxa_guard_release((__guard *)&byte_72638);
v3 = v18;
a1 = v19;
a2 = v16;
a3 = v17;
}
}
for ( ; a2; ++a1 )
{
v3 ^= *a1 << dword_72634;
v10 = 8;
do
{
--v10;
v3 = *a3 & (v3 >> 31) ^ (2 * v3);
}
while ( v10 );
--a2;
}
}
if ( *((unsigned __int8 *)a3 + 12) == *((unsigned __int8 *)a3 + 13) )
{
v11 = v3;
}
else
{
v12 = 0;
v11 = 0;
do
{
++v12;
v11 = v3 & 1 | (2 * v11);
v3 = (unsigned int)v3 >> 1;
}
while ( (unsigned __int16)v12 < 0x20u );
}
return v11 ^ a3[2];
}

根据a3的配置,可以知道这里执行的CRC32是标准的CRC32

即首先将a3的第一个4字节先进行32位的镜像反转

1
2
3
4
5
6
7
8
9
10
11
v4 = *a3;
v5 = 0;
v6 = 0;
do
{
++v5;
v7 = v4 & 1 | (2 * v6);
v4 >> = 1;
v6 = v7;
}
while ( (unsigned __int16)v5 < 0x20u );

这里就是将4字节铺平成32bit,然后第0bit和第31bit进行交换……

0x04C11DB7 -> 0xEDB88320

然后逐字节处理->逐位计算:

1
2
3
4
5
6
7
8
9
10
11
12
for ( ; a2; ++a1 )
{
v3 ^= *a1;
v8 = 8;
do
{
--v8;
v3 = (v3 << 31 >> 31) & v7 ^ ((unsigned int)v3 >> 1);
}
while ( v8 );
--a2;
}

模拟二进制除法(其实就是移位和异或)

1
2
v11 = v3;
return v11 ^ a3[2];

最后直接return v3 ^ 0xFFFFFFFF

可以直接调用python的zlib-crc32库,但是发现其代码量并不多,直接写逻辑即可

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
def sub_120C4_simulation(hex_string):

CONFIG_POLY = 0x04C11DB7 # *a3
CONFIG_INIT = 0xFFFFFFFF # a3[1]
CONFIG_XOR_OUT = 0xFFFFFFFF # a3[2]
FLAG_REF_IN = True
FLAG_REF_OUT = True
try:
data = bytes.fromhex(hex_string)
except ValueError:
return
v3 = CONFIG_INIT
if FLAG_REF_IN:

v4 = CONFIG_POLY
v5 = 0
v6 = 0

for _ in range(32):
v5 += 1
v7 = (v4 & 1) | ((v6 << 1) & 0xFFFFFFFF)
v4 >> = 1
v6 = v7

poly_reversed = v6

for byte in data:
v3 ^ = byte

v8 = 8
while v8 > 0:
v8 - = 1

if (v3 & 1) == 1:
v3 = (v3 >> 1) ^ poly_reversed
else:
v3 = (v3 >> 1)

else:
pass

if FLAG_REF_IN == FLAG_REF_OUT:
v11 = v3
else:
v11 = v3

result = v11 ^ CONFIG_XOR_OUT

return result & 0xFFFFFFFF


input_hex_std = "cab68c9fxxxxxxxxd8ea4aab0c1d5f4519f8d19c4948f"
crc_val_std = sub_120C4_simulation(input_hex_std)
print(f"输入 Hex: {input_hex_std}")
print(f"计算结果: {hex(crc_val_std).upper()}")
1
2
输入 Hex: cab68c9f7xxxxxxxa4aab0c1d5f4519f8d19c4948f
计算结果: 0XE40xx7FA

经过调试确认结果的一致!

4:

image-20251216142405479

看着就像是时间戳

调试发现是

image-20251216142508300

image-20251216142527223

就是上述我们填入的时间戳1960949932368L / 1000

2:

image-20251216143106130

1
2
LDAXR           W9, [X8]
STLXR W10, W9, [X8]

LD和ST分别是加载和载入,X是独占的语义,说明在这个期间X8指向的内存只有这条汇编可以改写,如果写入期间发现有其他的线程修改了这个内存地址,那么会返回1,继续读取然后自增,如果没有就直接返回0退出这个循环

其实说白了,就是一个自增器,确保内容的唯一性

而v68就是一个原子递增后的新序列号,这样每发一次包,就会生成一个唯一的请求ID!

猜测服务端接收时会按照这个ID来进行对抗

5:

image-20251216144336964

这里调试发现结果都是0xd00(QwQ这里感觉如果是固定的就很简单,估计不同的功能这里是不一样的,但是有点复杂,这里只考虑当前情况)

可以做个简单的小总结

1
2
3
4
5
6
7
8
struct SignatureBlock {
int magic; // [0] 0x4151 (ASCII 'AQ') - 头部魔数 0x41512200
int user_id; // [1] 用户ID 或 盐值 (从 qword_72998 获取) 0x1dbxxe7f
int seq_id; // [2] 序列号 (原子自增 v68)
int crc32; // [3] 数据的校验和 (CRC32_120C4)
int timestamp; // [4] 当前时间戳 (gettimeofday)
int feature_mask;// [5] <--- 这里就是 v72 (特性掩码) 0xd00
};

之前以为到这里还没结束,其实是已经结束了,因为之前说是需要48个字节是因为默认其通过编码存储,但是发现都是通过Hex存储的,之后只需要整理一下写个纯算python即可

image-20251216154209057

这里就是异或,还原一下即可,没什么难度。之后就可以按照这个写出sig3的纯算python了

贴一下这部分的代码:

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
def simulate_obfuscation(packed_data_bytes):
final_result = bytearray(packed_data_bytes)

v71 = 0
for i in range(23):
v73 = final_result[i]
v71 += v73

if v71 > 0xFF:
v71 = -v71

v72 = 0xd00

v71_32bit = v71 & 0xFFFFFFFF
shifted_v71 = (v71_32bit << 24) & 0xFFFFFFFF

new_int_val = v72 | shifted_v71

struct.pack_into('<I', final_result, 20, new_int_val)

v71_low_byte = v71 & 0xFF

for v74 in range(23):
xor_key = v71_low_byte ^ (v74 & 0xFF)
final_result[v74] ^= xor_key

return final_result

也不会特别困难,其中v72是之前的0xd00,有个可能会出现的问题就是range(23)而不是range(24)根据逆向可知其不会对最后一位进行异或处理

1
2
3
4
5
6
7
8
9
10
11
hmac = HMAC_SHA256(input_val)
aes_result = aes_ecb_encrypt(hmac, aes_key)
crc32_result = sub_120C4_simulation(aes_result)
packed_data = struct.pack('<6I',
magic,
user_id,
seq_id,
crc32_result,
time,
feature_mask)
result1 = simulate_obfuscation(packed_data)

总结

这个算法总的来说就是HMAC-SHA256 -> AES-128 -> CRC32

总体难度不高QwQ