
米游社salt(LK2 & K2)获取
反编译apk获取米游社salt(LK2 & K2)
周五 7月 11 2025jadx
2701 字 · 16 分钟
使用jadx获取内部加密salt
变量名随机,自行对应
打开jadx,直接搜索DS或LK2关键词 || 找到com.mihoyo.hyperion.net.aaaaa或com.mihoyo.hyperion.net.bbbbb文件里的a2222变量进行搜索(下图告诉你两个方法定位的东西是一样的)

定位到的DS/LK2
可以看到上面LK2变量是通过一个叫f57949q的int数组定义的;双击定位

定位f57949q
复制值备用(注意将变量替代为值),例如这里2.92.0版本的为
{-74, 60, -78, 204, 96, -88, -82, 78, 162, 54, -68, 78, -84, 84, -80, 66, 216, -80, 180, 90, MediaPlayer.MEDIA_PLAYER_OPTION_ABR_AVERAGE_BITRATE, C5432a.f120322i, -74, 66, 114, -98, -116, 54, 84, 204, 204, C5432a.f120320g}替换为
{-74, 60, -78, 204, 96, -88, -82, 78, 162, 54, -68, 78, -84, 84, -80, 66, 216, -80, 180, 90, 174, -106, -74, 66, 114, -98, -116, 54, 84, 204, 204, -104}这里很容易让人怀疑f57949q变量上面的f57948p变量是干什么的,搜索一下就可以定位到了,刚好是K2

定位f57948p
同样操作,复制备用,这里值为
{-98, -66, 192, 120, 60, 198, 168, -68, 216, 66, 168, 126, -108, -76, 126, -82, 114, -88, -88, -122, 162, -112, -66, 180, -122, 72, 108, 180, 78, -108, -106, -116}至此获取完原始salt
获取加密方法
通过上图,我们发现LK2变量传入了c14930a.m82567b方法,直接定位

m82567b方法
这非常明显就是一个加密方法,略微重写一下
public static String m61080b(int[] intArr) { int i12; StringBuilder sb2 = new StringBuilder(); ArrayList<Integer> arrayList = new ArrayList<>(intArr.length); for (int i13 : intArr) { if (i13 < 0) { i12 = ((double) (-i13)) >= Math.pow(3.0d, 6.0d) ? (int) (((Math.log(-i13) / Math.log(3.0d)) - 6) + 48) : ~i13; } else { i12 = (i13 / 3) + 48; } arrayList.add(i12); } for (Integer integer : arrayList) { sb2.append((char) integer.intValue()); } return sb2.toString(); }现在只需要传入数组就能得到加密后的salt,同理定位获取K2变量所用的加密方法(事实上两个加密方法是一样的)
结束
public static final int[] k2 = {-98, -66, 192, 120, 60, 198, 168, -68, 216, 66, 168, 126, -108, -76, 126, -82, 114, -88, -88, -122, 162, -112, -66, 180, -122, 72, 108, 180, 78, -108, -106, -116}; public static final int[] lk2 = {-74, 60, -78, 204, 96, -88, -82, 78, 162, 54, -68, 78, -84, 84, -80, 66, 216, -80, 180, 90, 174, -106, -74, 66, 114, -98, -116, 54, 84, 204, 204, -104}; public static void main(String[] args) { System.out.println(m61080b(k2)); System.out.println(m61080b(lk2)); }运行结果:
aApXDrhCxFhZkKZQVWWyfoAlyHTlJkisIDMtPWQJfBCJSLOFxOlNjiIFVasBLttg至此即得
Claude Code Skill:自动化提取
以下是一个用于 Claude Code 的 skill,可以通过 jadx-mcp-server 自动化完成上述定位和解码流程。
使用方法:将下方内容保存为
SKILL.md放入.claude/skills/find-miyoushe-ds-salt/目录即可。
---name: find-miyoushe-ds-saltdescription: > 从米游社(miyoushe/hyperion) APK 中提取 DS 签名参数(K2/LK2 salt)。 当用户要求分析米游社的 DS 签名机制、查找 K2/LK2 salt、逆向米游社网络请求签名、 或通过 jadx-mcp-server 分析 com.mihoyo.hyperion 包时使用此 skill。 适用于任何版本的米游社 APK,变量名会因混淆而不同,但逻辑结构不变。---
# 米游社 DS 签名参数提取指南
## 背景
米游社 APK 使用 DS (Dynamic Signature) 机制对网络请求进行签名。DS 由 native (JNI) 方法生成,Java 层负责组装参数并注入到 HTTP 请求头中。核心参数 K2 和 LK2 是 `int[]` 数组,通过 `AManager` 类解码为 salt 字符串后传给 native 方法。
## 前置条件
- jadx-mcp-server 已启动并连接到 JADX-GUI- JADX-GUI 已打开目标 APK 且反编译完成- 通过 `curl http://127.0.0.1:8650/health` 验证连接正常
## 定位流程
以下步骤的类名和字段名会因 APK 版本不同而变化(混淆重命名),但逻辑结构始终一致。每一步都给出了识别特征,不要硬编码类名。
### 第一步:找到 native JNI 包装类
DS 签名的核心实现在 native 层。先找到 `com.mihoyo.hyperion.net` 包下的 native 包装类。
> ⚠️ **搜索 API 注意事项(必读)**:`search-classes-by-keyword` 的 `search_in=class`> 模式会按 `.` 把类名切成 token,再对**单个 token** 做前缀/整词匹配。因此**任何带 `.` 的限定包名**> (如 `com.mihoyo.hyperion.net`、`com.mihoyo`、`hyperion.net`)都会返回 0 条。> 正确做法是:用**单 token**(如 `mihoyo`/`hyperion`/`net`),或改用 `search_in=code` 搜代码级特征。
**推荐查询(最精准,直接命中 native 库的加载点):**
```bash# 搜代码里的 System.loadLibrary,再从结果里挑 com.mihoyo.hyperion.net.* 的类curl -s "http://127.0.0.1:8650/search-classes-by-keyword?search_term=loadLibrary&search_in=code&offset=0&count=20"```
**备选查询(单 token 包名片段,需自己用 `com.mihoyo.hyperion.net` 前缀过滤):**
```bashcurl -s "http://127.0.0.1:8650/search-classes-by-keyword?search_term=net&search_in=class&offset=0&count=40"```
识别特征:该包下通常有 2 个类,满足以下条件:- `static { System.loadLibrary("xxx"); }` 加载 native 库- 包含 `native` 方法,参数和返回值都是 `String`- 类名极短(被混淆),如 `aaaaa`、`bbbbb`
记录这两个类,分别记为 **类A** 和 **类B**。它们各自有一个接受 1-2 个 String 参数、返回 String 的 native 方法(通常是同名的,如 `a2222`)。
### 第二步:找到 DS 拦截器(OkHttp Interceptor)
使用交叉引用找到谁调用了这些 native 方法。
```bash# 对类A和类B的 native 签名方法分别做 xrefscurl -s "http://127.0.0.1:8650/xrefs-to-method?class_name=<类A全名>&method_name=<方法名>&offset=0&count=20"curl -s "http://127.0.0.1:8650/xrefs-to-method?class_name=<类B全名>&method_name=<方法名>&offset=0&count=20"```
识别特征:xrefs 中会有一个类实现了 `Interceptor`(OkHttp 的 `InterfaceC18926w`),且其 `intercept` 方法中包含 `chain.proceed(...)` 和 `"DS"` 字符串。
这个类就是 **DS 拦截器**。但实测中**拦截器多为内层类**(如 `hn.C33707m`),而 MCP 的 `class-source`与 `xrefs-to-class` 对带内层类尾部标记(如 `C33707m`)的类都会返回 "Class not found";只有`xrefs-to-method`(按方法)能解析。另外**不存在 `/method-source` 端点**,不要尝试。
> ✅ **推荐程序化走法(无需手动开 GUI)**:对 native 方法做 `xrefs-to-method` 找到**调用方**> (通常是 `xxx.app.xxxApplicationHelper` 这类 Application 初始化类),再去读**调用方的**> `class-source`(能正常取到),从中搜索 `new <拦截器类>(K22)` / `int[] K22 = ...` 即可反推出盐值数组。> 这绕开了取不到拦截器源码的问题。
兜底(调用方也取不到时):在 JADX-GUI 中手动打开拦截器类,再用 `current-class` API 读取:
```bashcurl -s "http://127.0.0.1:8650/current-class"```
### 第三步:分析 DS 拦截器逻辑
从拦截器的 `intercept` 方法中可以看到:
1. **DS 类型判断**:通过请求 tag 或路径决定使用 DS1 还是 DS22. **DS1 路径**:调用某个 native 方法,传入一个 salt 字符串3. **DS2 路径**:调用另一个 native 方法,传入请求体和查询串4. **salt 来源**:salt 由 `int[]` 数组通过某个管理器类解码得到
关键代码模式:```java// DS2 路径(特殊请求)if (isDS2) { ds = nativeMethod2(requestBody, queryString);} else { // DS1 路径(普通请求) ds = nativeMethod1(manager.decode(intArray));}chain.proceed(request.addHeader("DS", ds).build());```
### 第四步:找到 salt 数组的定义位置
优先用第二步的"程序化走法":读拦截器的**调用方** `class-source`,搜索 `new <拦截器>(K22)` 即可定位盐值数组。若坚持用 xrefs,注意 `xrefs-to-class` 对拦截器内层类同样可能返回 not found:
```bashcurl -s "http://127.0.0.1:8650/xrefs-to-class?class_name=<DS拦截器类名>&offset=0&count=20"```
> ⚠️ **K2/LK2 未必在 BuildConfig 里**:`com.mihoyo.hyperion.utils.BuildConfig`> 只是标准 Gradle 产物(只有 `BUILD_TYPE` 等),两个 `int[]` 实际在另一个类(如 `p232Df.C1989a`)。> 不要硬编码"找 BuildConfig",应找**含两个相邻 `public static final int[]` 静态字段的类**,> 字段名被混淆(如 `f37869p`/`f37870q`),顺序固定:上 **K2**、下 **LK2**。
识别特征:实例化通常在 Application 初始化方法中(如 `initAppEnv` 或类似名称)。找到类似代码:
```javaint[] K22 = SomeClass.K2_FIELD; // 上方字段 = K2new DSInterceptor(K22);// 同类下方还有 LK2_FIELD = LK2```
记录该类的两个 `int[]` 字段(名字被混淆)。通常两个数组字段紧挨着定义,上面一个是 **K2**,下面一个是 **LK2**(顺序固定)。可用第五步的 JS Bridge 类交叉确认。
如果该类可通过 API 访问,直接获取:```bashcurl -s "http://127.0.0.1:8650/class-source?class_name=<盐值数组所在类名>"```
### 第五步:确认 K2 和 LK2 的对应关系
通过 xrefs 或搜索找到另一个使用这些数组的地方。`GetDSMethodImpl`(JS Bridge 实现)会使用 LK2,可以通过搜索其中的数组引用来确认:
```bash# 搜索包含两个 native 方法名的类(通常是 JS Bridge)curl -s "http://127.0.0.1:8650/search-classes-by-keyword?search_term=getDS&search_in=code&offset=0&count=10"```
该类中会有类似代码来明确标识 LK2:```javaint[] LK2 = SomeClass.LK2_FIELD; // 与第四步同一个类data.put("DS", nativeMethod(manager.decode(LK2)));```
### 第六步:解码 salt
找到 `AManager` 类(负责将 `int[]` 解码为 String 的工具类)。
识别特征:- 包名通常在 `bn` 包下- 包含两个参数和返回值相同的方法(一个给 K2 用,一个给 LK2 用)- 方法逻辑:遍历 int 数组,对每个元素做数学运算转为 char
解码算法(版本间不变):```javascript// Node.js 解码脚本function decodeSalt(intArr) { return intArr.map(x => { if (x < 0) { return Math.abs(x) >= 729 ? String.fromCharCode(Math.floor(Math.log(-x) / Math.log(3) - 6 + 48)) : String.fromCharCode(~x); } return String.fromCharCode(Math.floor(x / 3) + 48); }).join('');}```
直接执行:```bashnode -e "const k2 = [<K2数组值>];const lk2 = [<LK2数组值>];function d(a){return a.map(x=>x<0?Math.abs(x)>=729?String.fromCharCode(Math.floor(Math.log(-x)/Math.log(3)-6+48)):String.fromCharCode(~x):String.fromCharCode(Math.floor(x/3)+48)).join('');}console.log('K2 salt:', d(k2));console.log('LK2 salt:', d(lk2));"```
## 版本差异说明
| 元素 | 是否变化 | 说明 ||---------------|--------|----------------------------------------------------------------------------|| 包结构 | 不变 | `com.mihoyo.hyperion.net` 始终存在 || native 包装类名 | 变化 | 混淆为短名,如 `aaaaa`/`bbbbb` || native 方法名 | 变化 | 混淆为短名,如 `a2222`/`b5555` || native 库名 | 变化 | 混淆为短名,如 `libdddd.so` || DS 拦截器类名 | 变化 | 混淆,但一定实现 Interceptor 接口 || 盐值数组所在类名 | 变化 | 包名/类名会被混淆,且**不一定是 BuildConfig**(本版本实测在 `p232Df.C1989a`);一定含两个相邻 `int[]` 字段 || K2/LK2 数组值 | **变化** | 每个版本的 salt 不同 || AManager 解码算法 | 不变 | 数学逻辑固定 || DS header 名称 | 不变 | 始终为 `"DS"` || DS1/DS2 判断逻辑 | 不变 | 通过 tag 或路径判断 |
## 快速参考
DS 生成的完整调用链:```OkHttp 请求 → DS 拦截器.intercept() ├─ DS2: nativeMethod(body, query) → 加密签名 └─ DS1: nativeMethod(AManager.decode(K2/LK2)) → 加密签名 └─ int[] → 解码算法 → salt 字符串最终: request.addHeader("DS", 签名值)```
K2 用于主 API 拦截器,LK2 用于 WebView JS Bridge 的 `getDS` 方法。两者解码方式完全相同,只是应用场景不同。 感谢您的阅读!如果可以,给俺点些关注吧~