[Codex全程] KWAI REST / sig / NStokensig / sig3
全程Codex, 我只测试了一下感觉大概能用, 包括文章也是, 只有这里是我自己写的, 他没解决DFP
我靠... 逆向你也可以失业了...
0x00 前言:这次到底要解决什么
一开始目标不是“随便发一个 HTTP 请求”。快手这种 App 的 REST 请求至少有四层东西会互相影响:
- Retrofit/Jadx 里能看到的接口路径和字段。
sig、__NStokensig这种 Java 参数层签名。__NS_sig3这种从 Java 进入 KSecurity/native 的安全签名。- 设备态、DFP、DID/EGID、Azeroth、HAR 登录态和 Cronet 抓包环境。
如果只看 HAR,会得到一堆字段,但不知道字段怎么生成;如果只看 Java,会看到 KSecurity.atlasSign(),但不知道 native 里 command id 怎么走;如果只看 IDA,会看到混淆 C 伪代码,但不知道业务请求把什么字符串送进去。
所以我最后采用的路线是:
flowchart TD
A["HAR / Retrofit annotation"] --> B["ParamsUtils 合并参数"]
B --> C["KwaiParams 计算 sig / token sig"]
C --> D["path + sig -> WebUtils.a"]
D --> E["KSecurity.atlasSign"]
E --> F["bridge / JNI / libkwsgmain"]
F --> G["IDA command 10405 / 10417 / 10418"]
G --> H["Python algorithms.py"]
H --> I["client.py 封装 REST / 登录 / HAR / 私信"]
I --> J["kws_client.py CLI + tests"]
下面从最外层请求开始往里剥。
0x01 先从 Retrofit 参数处理下手:sig 和 __NStokensig 是怎么塞进去的
抓包里能看到 sig,但 HAR 本身只能证明“请求里有这个字段”,不能证明签名输入。Jadx 里先搜 __NStokensig,最短路径直接落到 ParamsUtils.java。这段代码很短,但价值很高:它告诉我们公共参数、业务参数、auth 参数怎么合并,sig 放在哪里,client_salt 什么时候参与。
Retrofit 参数合并证据:ParamsUtils.java
- 文件:
sources/com/yxcorp/retrofit/utils/ParamsUtils.java - 行数:
55 - 字节:
1761 - SHA256:
c33956be7f272026a6d5bcfa75b4550c77cc826def6dfc36edad1c9c98d45d9a
为什么看它:它是 Retrofit 请求进入签名前的 Map 归并点。这里能看到 client_salt 被从多个 Map 里移除、aVar.c() 产出 sig、aVar.f(sig, salt) 产出 __NStokensig。
读完得到的结论:sig 不应该从 HAR 复制,而要由排序后的参数重新算;__NStokensig 是 sig + client_salt 的派生;最后还会调用 aVar.e() 给 profile 一次追加签名的机会。
package com.yxcorp.retrofit.utils;
import android.text.TextUtils;
import android.util.Pair;
import java.util.Map;
import nt.RetrofitConfig;
import okhttp3.Request;
/* compiled from: ParamsUtils.java */
/* renamed from: com.yxcorp.retrofit.utils.b, reason: use source file name */
/* loaded from: classes.dex */
public class ParamsUtils {
private static void a(Map<String, String> map, Map<String, String> map2) {
for (String str : map.keySet()) {
if (map.get(str) == null) {
map.put(str, "");
}
}
if (map2 != null) {
for (String str2 : map2.keySet()) {
if (map2.get(str2) == null) {
map2.put(str2, "");
}
}
}
}
public static Pair<Map<String, String>, Map<String, String>> b(Request request, RetrofitConfig.a aVar, Map<String, String> map, Map<String, String> map2, boolean z2) {
Map<String, String> b = aVar.b();
b.putAll(map2);
Map<String, String> d = aVar.d();
String remove = map.remove("client_salt");
if (remove == null) {
remove = b.remove("client_salt");
}
String remove2 = remove == null ? d.remove("client_salt") : remove;
if (z2) {
b.putAll(map);
} else {
d.putAll(map);
}
a(b, d);
String c = aVar.c(b, d);
d.put("sig", c);
if (!TextUtils.isEmpty(remove2)) {
d.put("__NStokensig", aVar.f(c, remove2));
}
aVar.e(request, b, d, remove2, "");
if (z2) {
b.putAll(d);
d.clear();
}
return new Pair<>(b, d);
}
}
对照 Python:把 Java 的 Map 合并翻成可测试算法
Java 里 aVar.c(b, d) 的具体实现后面在 KwaiParams 里看。先把最基础的排序拼接和 token sig 固化在 algorithms.py:
def kwai_canonical_params(*maps: Mapping[str, object | None]) -> bytes:
parts: list[str] = []
for params in maps:
for key, value in params.items():
parts.append(f"{key}={java_value(value)}")
return "".join(sorted(parts)).encode("utf-8")
def kwai_sig(*maps: Mapping[str, object | None]) -> str:
return get_clock(kwai_canonical_params(*maps))
def kwai_token_sig(sig: str, client_salt: str) -> str:
return hashlib.sha256((sig + client_salt).encode("utf-8")).hexdigest()
这里的实现理由很明确:ParamsUtils 不对 key 做业务过滤,而是把进入签名 Map 的内容交给 profile;所以 Python 也不硬编码某个接口字段,而是按请求构造时的 Map 输入统一算。
0x02 跟进 KwaiParams:真正决定 __NS_sig3 输入的是 path + sig
只恢复 sig 还不够。快手主 App 请求里更关键的是 __NS_sig3。最容易踩坑的地方是:它不是对参数串签名,也不是对完整 URL 签名,而是 request.url().h() + sig。这个结论来自 KwaiParams.h(),不能靠猜。
主 App 参数 profile:KwaiParams.java
- 文件:
sources/lo/KwaiParams.java - 行数:
245 - 字节:
9246 - SHA256:
2b7f3386dc6c715983db189400b78a00093b1efcf85d5d4e23f672676d3091ab
为什么看它:它实现 RetrofitConfig.a,是主 App 公共参数、auth 参数、binary bodyMd5、sig2、__NStokensig 和 __NS_sig3 的集中证据。
读完得到的结论:第一个关键点是 WebUtils.a(request.url().h() + sig);第二个关键点是登录态里 token、kuaishou.api_st、client_salt 的来源;第三个关键点是二进制 body 会额外加 bodyMd5。
package lo;
import android.os.Build;
import android.os.SystemClock;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import cj.AppEnv;
import com.antman.utility.g0;
import com.antman.utility.k;
import com.antman.utility.v;
import com.kuaishou.weapon.ks.x;
import com.yxcorp.buildconfig.BuildConfig;
import com.yxcorp.gifshow.log.antman.utils.AbiUtil;
import com.yxcorp.gifshow.log.series.LogSeriesDeviceSampleRatio;
import com.yxcorp.gifshow.util.CPU;
import com.yxcorp.gifshow.util.JsonStringBuilder;
import com.yxcorp.gifshow.util.WebUtils;
import com.yxcorp.init.ChannelInitUtil;
import com.yxcorp.init.LogManagerInitUtil;
import com.yxcorp.utils.antman.AntmanShellSPHelper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import nt.RetrofitConfig;
import okhttp3.Request;
/* compiled from: KwaiParams.java */
/* renamed from: lo.a, reason: use source file name */
/* loaded from: classes.dex */
public class KwaiParams implements RetrofitConfig.a {
private static String a;
private void h(@NonNull Request request, Map<String, String> map) {
String str = map.get("sig");
if (TextUtils.isEmpty(str)) {
return;
}
String a2 = WebUtils.a(request.url().h() + str);
if (str.length() != 32) {
LogManagerInitUtil.G().a("sig3_fail", String.format("sig3 sig length <> 32 sig[%s]sig3[%s]", str, a2));
}
if (TextUtils.isEmpty(a2)) {
return;
}
map.put("__NS_sig3", a2);
}
private static String i() {
if (a == null) {
try {
String str = AppEnv.f;
a = AppEnv.f.substring(0, str.indexOf(".", str.indexOf(".") + 1));
} catch (Exception unused) {
a = AppEnv.f;
}
}
return a;
}
@Override // nt.RetrofitConfig.a
@NonNull
public Map<String, String> a() {
HashMap hashMap = new HashMap();
hashMap.put("User-Agent", "kwai-android");
hashMap.put("Accept-Language", g0.e());
hashMap.put("X-REQUESTID", String.valueOf(SystemClock.elapsedRealtime()));
hashMap.put("Connection", "keep-alive");
AntmanShellSPHelper antmanShellSPHelper = AntmanShellSPHelper.a;
String y2 = antmanShellSPHelper.y();
if (!TextUtils.isEmpty(y2)) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("token=");
stringBuffer.append(y2);
hashMap.put("Cookie", stringBuffer.toString());
}
if (ChannelInitUtil.c(AppEnv.b)) {
String v2 = antmanShellSPHelper.v();
if (!TextUtils.isEmpty(v2)) {
JsonStringBuilder e3 = JsonStringBuilder.e();
e3.b("laneId", v2);
hashMap.put("trace-context", e3.d());
}
}
return hashMap;
}
@Override // nt.RetrofitConfig.a
@NonNull
public Map<String, String> b() {
HashMap hashMap = new HashMap();
String str = LogSeriesDeviceSampleRatio.DEVICE_SAMPLE_NONE;
hashMap.put("lat", LogSeriesDeviceSampleRatio.DEVICE_SAMPLE_NONE);
hashMap.put("lon", LogSeriesDeviceSampleRatio.DEVICE_SAMPLE_NONE);
AntmanShellSPHelper antmanShellSPHelper = AntmanShellSPHelper.a;
if (antmanShellSPHelper.l()) {
hashMap.put("cl", "1");
}
hashMap.put("ver", i());
hashMap.put("ud", antmanShellSPHelper.x());
hashMap.put("sys", AppEnv.f1019k);
hashMap.put(x.f2587p, AppEnv.b);
hashMap.put("oc", antmanShellSPHelper.p());
hashMap.put("newOc", AppEnv.c);
hashMap.put("net", v.d(AppEnv.a));
hashMap.put("did", antmanShellSPHelper.e());
hashMap.put("did_tag", String.valueOf(1));
hashMap.put("cdid_tag", antmanShellSPHelper.c());
hashMap.put("rdid", antmanShellSPHelper.s());
if (antmanShellSPHelper.i() != 0) {
hashMap.put("did_gt", String.valueOf(antmanShellSPHelper.i()));
}
hashMap.put("egid", antmanShellSPHelper.h());
hashMap.put("mod", AppEnv.d);
if (AppEnv.b.equalsIgnoreCase("GOOGLE_PLAY")) {
str = "1";
}
hashMap.put("app", str);
hashMap.put("language", g0.e());
hashMap.put("country_code", antmanShellSPHelper.d());
hashMap.put("appver", AppEnv.f);
hashMap.put("ftt", "");
hashMap.put("iuid", AppEnv.f1018j);
String r = antmanShellSPHelper.r();
if (!TextUtils.isEmpty(r)) {
hashMap.put("pm_tag", r);
}
hashMap.put("max_memory", AppEnv.b());
hashMap.put("kpn", BuildConfig.KPN);
hashMap.put("apptype", BuildConfig.APP_TYPE);
hashMap.put("kpf", "ANDROID_PHONE");
hashMap.put("browseType", "1");
hashMap.put("isAntman", "true");
if (AbiUtil.b()) {
hashMap.put("abi", "arm64");
} else {
hashMap.put("abi", "arm32");
}
return hashMap;
}
@Override // nt.RetrofitConfig.a
public String c(Map<String, String> map, Map<String, String> map2) {
ArrayList arrayList = new ArrayList();
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (true) {
String str = "";
if (!it.hasNext()) {
break;
}
Map.Entry<String, String> next = it.next();
StringBuilder sb = new StringBuilder();
sb.append(next.getKey());
sb.append("=");
if (next.getValue() != null) {
str = next.getValue();
}
sb.append(str);
arrayList.add(sb.toString());
}
for (Map.Entry<String, String> entry : map2.entrySet()) {
StringBuilder sb2 = new StringBuilder();
sb2.append(entry.getKey());
sb2.append("=");
sb2.append(entry.getValue() == null ? "" : entry.getValue());
arrayList.add(sb2.toString());
}
try {
Collections.sort(arrayList);
} catch (Exception e3) {
e3.printStackTrace();
}
return CPU.a(AppEnv.a, TextUtils.join("", arrayList).getBytes(hw.a.a), Build.VERSION.SDK_INT);
}
@Override // nt.RetrofitConfig.a
@NonNull
public Map<String, String> d() {
HashMap hashMap = new HashMap();
hashMap.put("os", "android");
hashMap.put("client_key", BuildConfig.CLIENT_KEY);
AntmanShellSPHelper antmanShellSPHelper = AntmanShellSPHelper.a;
if (antmanShellSPHelper.B()) {
String y2 = antmanShellSPHelper.y();
String u2 = antmanShellSPHelper.u();
String a2 = antmanShellSPHelper.a();
if (!TextUtils.isEmpty(y2)) {
hashMap.put("token", y2);
}
if (!TextUtils.isEmpty(a2)) {
hashMap.put("kuaishou.api_st", a2);
}
hashMap.put("client_salt", u2);
}
return hashMap;
}
@Override // nt.RetrofitConfig.a
public void e(Request request, Map<String, String> map, Map<String, String> map2, String str, String str2) {
h(request, map2);
}
@Override // nt.RetrofitConfig.a
public String f(String str, String str2) {
return gw.a.m(str + str2);
}
@Override // nt.RetrofitConfig.a
public Map<String, String> g(Map<String, String> map, byte[] bArr) {
Map<String, String> b = b();
HashMap hashMap = new HashMap();
HashMap hashMap2 = (HashMap) b;
hashMap2.put("os", "android");
hashMap2.put("client_key", BuildConfig.CLIENT_KEY);
for (Map.Entry<String, String> entry : map.entrySet()) {
hashMap2.put(entry.getKey(), entry.getValue());
}
hashMap2.put("bodyMd5", k.d(bArr));
AntmanShellSPHelper antmanShellSPHelper = AntmanShellSPHelper.a;
if (antmanShellSPHelper.B()) {
hashMap.put("token", antmanShellSPHelper.y());
String y2 = antmanShellSPHelper.y();
String a2 = antmanShellSPHelper.a();
if (!TextUtils.isEmpty(y2)) {
hashMap.put("token", y2);
}
if (!TextUtils.isEmpty(a2)) {
hashMap.put("kuaishou.api_st", a2);
}
}
for (Map.Entry entry2 : hashMap2.entrySet()) {
hashMap.put((String) entry2.getKey(), (String) entry2.getValue());
}
String c = c(hashMap, new HashMap<>());
hashMap2.put("sig2", c);
AntmanShellSPHelper antmanShellSPHelper2 = AntmanShellSPHelper.a;
String u2 = antmanShellSPHelper2.u();
if (antmanShellSPHelper2.B() && !TextUtils.isEmpty(u2)) {
hashMap2.put("__NStokensig", f(c, u2));
}
hashMap2.remove("bodyMd5");
return b;
}
}
这里为什么能定死 sig3_input = path + sig
关键 Java 逻辑是:
String str = map.get("sig");
String a2 = WebUtils.a(request.url().h() + str);
map.put("__NS_sig3", a2);
这三行对应 Python 的请求签名过程:先算 sig,再拼 path + sig,最后调用 make_sig3()。这也是之前跑 path matrix / sig3 input matrix 时最关键的判断依据。
0x03 Java 层只是一层壳:WebUtils 到 KSecurity
到 KwaiParams 为止,我们知道了送进 sig3 的字符串是什么,但还不知道算法。继续跟 WebUtils.a()。这一步很像 2zxz 文里的写法:搜到一个可疑函数,不急着下结论,先看它到底有没有算法。结果很明显,Java 层没有算法,只是调用 KSecurity。
WebUtils.a():把字符串送进 KSecurity
- 文件:
sources/com/yxcorp/gifshow/util/WebUtils.java - 行数:
40 - 字节:
1490 - SHA256:
1dd741f4382fd5afeee8b179a90003f431ded4fd34d03a0ad771478ecb24ad24
为什么看它:它是 KwaiParams 和安全 SDK 的连接点。
读完得到的结论:WebUtils.a() 只做空值、耗时、异常日志,然后调用 KSecurity.atlasSign(str)。算法不在这里。
package com.yxcorp.gifshow.util;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Log;
import com.kuaishou.android.security.KSecurity;
import com.kuaishou.android.security.base.exception.KSException;
import com.yxcorp.init.LogManagerInitUtil;
import kotlin.jvm.JvmStatic;
import kotlin.jvm.internal.h;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/* compiled from: WebUtils.kt */
/* renamed from: com.yxcorp.gifshow.util.c, reason: use source file name */
/* loaded from: classes.dex */
public final class WebUtils {
@JvmStatic
@NotNull
public static final String a(@Nullable String str) {
if (TextUtils.isEmpty(str)) {
return "";
}
try {
long elapsedRealtime = SystemClock.elapsedRealtime();
h.c(str);
String atlasSign = KSecurity.atlasSign(str);
h.e(atlasSign, "atlasSign(sig!!)");
LogManagerInitUtil.G().a("sig3_cost", "" + (SystemClock.elapsedRealtime() - elapsedRealtime));
return atlasSign;
} catch (Exception e3) {
if (!(e3 instanceof KSException)) {
LogManagerInitUtil.G().a("sig3_error", Log.getStackTraceString(e3));
return "";
}
LogManagerInitUtil.G().a("sig3_fail", ((KSException) e3).getErrorCode() + '_' + e3.getMessage());
return "";
}
}
}
KSecurity 公开 API:KSecurity.java
- 文件:
sources/com/kuaishou/android/security/KSecurity.java - 行数:
242 - 字节:
8499 - SHA256:
b2ae31588f548644b7e8733a37d8e549cff4773b4869813f65b064432c38727e
为什么看它:它暴露 atlasSign、atlasSignPlus、atlasSignPlusIner、Initialize、getSecurityValue 等 API,是 Java 进入 bridge/native 前最后一层清晰入口。
读完得到的结论:atlasSign(str) 实际调用 b.i().d(str, false, "");atlasSignPlus 和 inner 分支用于别的安全场景,DFP/MXSec 会用到类似能力。
package com.kuaishou.android.security;
import android.content.Context;
import androidx.annotation.NonNull;
import com.kuaishou.android.security.base.exception.KSException;
import com.kuaishou.android.security.base.log.KSecuritySdkILog;
import com.kuaishou.android.security.base.util.KSecurityTrack;
import com.kuaishou.android.security.base.util.n;
import com.kuaishou.android.security.bridge.main.b;
import com.kuaishou.android.security.features.drm.DrmContext;
import com.kuaishou.android.security.internal.common.KSecurityContext;
import com.kuaishou.android.security.internal.common.h;
import com.kuaishou.android.security.internal.dispatch.JNICLibrary;
import com.kuaishou.android.security.internal.init.SecDidProxy;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import org.json.JSONObject;
/* loaded from: classes.dex */
public class KSecurity {
private static long a;
private static long b;
private static AtomicBoolean c = new AtomicBoolean(true);
private static KSecuritySdkILog d = null;
public enum ENV {
ROOT(0),
MALWARE(1),
HOOK(2),
EMULATOR(3),
ANTIDEBUG(4),
REPACK(5);
private final int value;
ENV(int i2) {
this.value = i2;
}
public int getIntValue() {
return this.value;
}
}
public static synchronized int Initialize(@NonNull Context context, @NonNull String str, @NonNull String str2, @NonNull KSecuritySdkILog kSecuritySdkILog) throws KSException {
int a2;
synchronized (KSecurity.class) {
a = System.currentTimeMillis();
b.i().h().setProductName("");
b.i().h().setWithFeature(KSecurityContext.Feature.GUARD);
b.i().h().setDid("");
a2 = b.i().a(h.b().a(context).a(str).b(str2).a(kSecuritySdkILog).a(KSecurityContext.Mode.ASYNC).a(KSecurityTrack.getDelegateCb()));
}
return a2;
}
public static synchronized int Initialize(@NonNull Context context, @NonNull String str, @NonNull String str2, @NonNull String str3, @NonNull String str4, @NonNull KSecuritySdkILog kSecuritySdkILog) throws KSException {
int Initialize;
synchronized (KSecurity.class) {
Initialize = Initialize(context, str, str2, str3, str4, "", "", kSecuritySdkILog, KSecurityContext.Mode.ASYNC);
}
return Initialize;
}
public static synchronized int Initialize(@NonNull Context context, @NonNull String str, @NonNull String str2, @NonNull String str3, @NonNull String str4, @NonNull KSecuritySdkILog kSecuritySdkILog, @NonNull KSecurityContext.Mode mode) throws KSException {
int Initialize;
synchronized (KSecurity.class) {
a = System.currentTimeMillis();
Initialize = Initialize(context, str, str2, str3, str4, "", "", kSecuritySdkILog, mode);
}
return Initialize;
}
public static synchronized int Initialize(@NonNull Context context, @NonNull String str, @NonNull String str2, @NonNull String str3, @NonNull String str4, @NonNull String str5, @NonNull String str6, @NonNull KSecuritySdkILog kSecuritySdkILog, @NonNull KSecurityContext.Mode mode) throws KSException {
int a2;
synchronized (KSecurity.class) {
a = System.currentTimeMillis();
b.i().h().setProductName(str3);
b.i().h().setWithFeature(KSecurityContext.Feature.ALL);
b.i().h().setDid(str4);
b.i().h().setRdid(str5);
b.i().h().setRdidtag(str6);
a2 = b.i().a(h.b().a(context).a(str).b(str2).a(kSecuritySdkILog).a(mode).a(KSecurityTrack.getDelegateCb()));
}
return a2;
}
public static synchronized int InitializeKuaiShou(@NonNull Context context, @NonNull String str, @NonNull String str2, @NonNull String str3, SecDidProxy secDidProxy, @NonNull KSecuritySdkILog kSecuritySdkILog, @NonNull KSecurityContext.Mode mode) throws KSException {
int a2;
synchronized (KSecurity.class) {
a = System.currentTimeMillis();
a = System.currentTimeMillis();
b.i().h().setProductName(str3);
b.i().h().setWithFeature(KSecurityContext.Feature.ALL);
b.i().h().setSecDidProxy(secDidProxy);
a2 = b.i().a(h.b().a(context).a(str).b(str2).a(kSecuritySdkILog).a(mode).a(KSecurityTrack.getDelegateCb()));
}
return a2;
}
@NonNull
@Deprecated
public static byte[] asymmetricDecrypt(@NonNull byte[] bArr, @NonNull com.kuaishou.android.security.internal.common.b bVar) throws KSException {
return b.i().a(bArr, bVar, true, false, "");
}
@NonNull
@Deprecated
public static byte[] asymmetricEncrypt(@NonNull byte[] bArr, @NonNull com.kuaishou.android.security.internal.common.b bVar) throws KSException {
return b.i().b(bArr, bVar, true, false, "");
}
@NonNull
public static byte[] atlasDecrypt(@NonNull byte[] bArr) throws KSException {
return b.i().b(bArr, false, "");
}
@NonNull
public static byte[] atlasEncrypt(@NonNull byte[] bArr) throws KSException {
return b.i().a(bArr, false, "");
}
@NonNull
public static String atlasSign(@NonNull String str) throws KSException {
String d3 = b.i().d(str, false, "");
c.getAndSet(false);
return d3;
}
@NonNull
public static String atlasSignPlus(@NonNull String str) throws KSException {
return b.i().a(str, false, "");
}
@NonNull
public static String atlasSignPlusIner(@NonNull String str, @NonNull String str2) throws KSException {
return b.i().a(str, true, str2);
}
@NonNull
public static String challenge(@NonNull String str) throws KSException {
return b.i().c(str, false, "");
}
@NonNull
@Deprecated
public static String checkEnv(@NonNull String str) throws KSException {
return b.i().b(str, false, "");
}
@NonNull
public static boolean detectEnvironment(@NonNull ENV env) throws KSException {
return b.i().a(env, false, "");
}
public static void doSentiveWork(boolean z2) throws KSException {
}
public static long getAppStartTime() {
return b;
}
public static long getAppTime() {
return b;
}
public static long getDRMBridgeFuncAddr() {
if (isInitialize()) {
return JNICLibrary.gDBF();
}
return 0L;
}
public static long getInitTime() {
return a;
}
public static long getKSBridgeFuncAddr() {
if (isInitialize()) {
return JNICLibrary.gKSF();
}
return 0L;
}
@NonNull
public static String getSecurityValue(@NonNull int i2) throws KSException {
return b.i().a(i2, false, "");
}
public static KSecurityContext getkSecurityParameterContext() {
return b.i().h();
}
public static boolean isInitialize() {
return b.i().g();
}
@NonNull
public static String localChallenge(String str) throws KSException {
return b.i().a(false, "", str);
}
public static void sendReportLog(Context context, String str, String str2, JSONObject jSONObject) {
n.a(str2, jSONObject.toString());
}
public static void setAppStartTime(long j3) {
b = j3;
}
public static void setAppTime(long j3) {
b = j3;
}
public static void setDRMUserinfo(@NonNull String str, @NonNull String str2, @NonNull String str3) {
DrmContext.setDid(str3);
DrmContext.setUid(str2);
DrmContext.setToken(str);
}
public static void setDrmDebugHost(boolean z2) {
DrmContext.setIsDebugModel(z2);
}
public static void setInitTime(long j3) {
a = j3;
}
public static void setKCObject(Map<String, Boolean> map) {
b.i().h().setKeyconfigMap(map);
}
@NonNull
public static byte[] uDecrypt(@NonNull byte[] bArr) throws KSException {
return b.i().a(bArr, false, false, "");
}
@NonNull
public static byte[] uEncrypt(@NonNull byte[] bArr) throws KSException {
return b.i().b(bArr, false, false, "");
}
}
security bridge 主类:bridge/main/a.java
- 文件:
sources/com/kuaishou/android/security/bridge/main/a.java - 行数:
375 - 字节:
17889 - SHA256:
3cd86d671937eeb14ac7d92f6574e8f74aed469c62d202e49f0991c0f7594bb7
为什么看它:这里能看到未初始化、参数非法、环境异常时的错误处理和安全日志;它解释为什么实机 hook/未初始化会出现空串或异常。
读完得到的结论:Java bridge 不是算法主体,但它决定是否能成功进入 native,以及失败时返回什么错误。
package com.kuaishou.android.security.bridge.main;
import androidx.annotation.NonNull;
import com.kuaishou.android.security.KSecurity;
import com.kuaishou.android.security.base.exception.KSException;
import com.kuaishou.android.security.base.perf.a;
import com.kuaishou.android.security.base.perf.d;
import com.kuaishou.android.security.base.perf.j;
import com.kuaishou.android.security.base.perf.k;
import com.kuaishou.android.security.base.util.KSecurityTrack;
import com.kuaishou.android.security.internal.common.c;
import com.kuaishou.android.security.internal.common.g;
import com.kuaishou.livestream.message.nano.LivePetMessages;
/* loaded from: classes.dex */
public abstract class a extends com.kuaishou.android.security.internal.dispatch.b implements g {
/* renamed from: com.kuaishou.android.security.bridge.main.a$a, reason: collision with other inner class name */
public static /* synthetic */ class C0048a {
public static final /* synthetic */ int[] a;
static {
int[] iArr = new int[KSecurity.ENV.values().length];
a = iArr;
try {
iArr[KSecurity.ENV.ROOT.ordinal()] = 1;
} catch (NoSuchFieldError unused) {
}
try {
a[KSecurity.ENV.MALWARE.ordinal()] = 2;
} catch (NoSuchFieldError unused2) {
}
try {
a[KSecurity.ENV.HOOK.ordinal()] = 3;
} catch (NoSuchFieldError unused3) {
}
try {
a[KSecurity.ENV.EMULATOR.ordinal()] = 4;
} catch (NoSuchFieldError unused4) {
}
try {
a[KSecurity.ENV.ANTIDEBUG.ordinal()] = 5;
} catch (NoSuchFieldError unused5) {
}
try {
a[KSecurity.ENV.REPACK.ordinal()] = 6;
} catch (NoSuchFieldError unused6) {
}
}
}
@Override // com.kuaishou.android.security.internal.common.g
@NonNull
public String a(@NonNull int i2, boolean z2, String str) throws KSException {
try {
if (b.i().h() != null && b.i().h().getKeyconfigMap() != null && b.i().h().getKeyconfigMap().get(c.f1739k) != null) {
if (b.i().h().getKeyconfigMap().get(c.f1739k).booleanValue()) {
return "";
}
}
} catch (Exception e3) {
e3.printStackTrace();
}
if (!g()) {
if (b.i().j() != null) {
b.i().j().e().onSeucrityError(new KSException(String.format("[KGE]KWSecurity[%b] getSecurityValue dont Init [%s]", Boolean.valueOf(z2), com.kuaishou.android.security.base.perf.c.a()), LivePetMessages.LivePetActionType.THINK));
}
throw new KSException(LivePetMessages.LivePetActionType.THINK);
}
j a = k.a();
String a2 = e().a(i2, z2, str);
k.a(a.EnumC0044a.GETSVALUE, a);
return a2;
}
@Override // com.kuaishou.android.security.internal.common.g
@NonNull
public String a(@NonNull String str, boolean z2, String str2) throws KSException {
try {
if (b.i().h() != null && b.i().h().getKeyconfigMap() != null && b.i().h().getKeyconfigMap().get(c.f1741m) != null) {
if (b.i().h().getKeyconfigMap().get(c.f1741m).booleanValue()) {
return "";
}
}
} catch (Exception e3) {
e3.printStackTrace();
}
long currentTimeMillis = System.currentTimeMillis();
if (g()) {
if (str.length() == 0) {
b.i().j().e().onSeucrityError(new KSException(String.format("[KGE]KWSecurity atlasSignPlus invalid parameter [%s]", com.kuaishou.android.security.base.perf.c.a()), 11106));
return "";
}
try {
return d().c(str, z2, str2);
} catch (Throwable th) {
th.printStackTrace();
d.a(d.b.ALL, th.getLocalizedMessage(), 11100);
return d().b(str, z2, str2);
}
}
KSecurityTrack.sLog(99, currentTimeMillis);
KSecurityTrack.sLog(-99);
String format = String.format("[KGE]KWSecurity[%b] atlassignPlus dont Init [isloadding:{%s}][%s]", Boolean.valueOf(z2), Boolean.valueOf(b.i().m()), com.kuaishou.android.security.base.perf.c.a());
KSException kSException = new KSException(format, LivePetMessages.LivePetActionType.THINK);
if (b.i().j() != null) {
b.i().j().e().onSeucrityError(kSException);
}
d.a(d.b.ALL, format, LivePetMessages.LivePetActionType.THINK);
throw kSException;
}
@Override // com.kuaishou.android.security.internal.common.g
@NonNull
public String a(boolean z2, String str, String str2) throws KSException {
try {
if (b.i().h() != null && b.i().h().getKeyconfigMap() != null && b.i().h().getKeyconfigMap().get(c.f1742n) != null) {
if (b.i().h().getKeyconfigMap().get(c.f1742n).booleanValue()) {
return "";
}
}
} catch (Exception e3) {
e3.printStackTrace();
}
long currentTimeMillis = System.currentTimeMillis();
if (g()) {
return d().a(z2, str, str2);
}
KSecurityTrack.sLog(99, currentTimeMillis);
KSecurityTrack.sLog(-99);
String format = String.format("[KGE]KWSecurity[%b] local challenge dont Init [isloadding:{%s}][%s]", Boolean.valueOf(z2), Boolean.valueOf(b.i().m()), com.kuaishou.android.security.base.perf.c.a());
KSException kSException = new KSException(format, LivePetMessages.LivePetActionType.THINK);
if (b.i().j() != null) {
b.i().j().e().onSeucrityError(kSException);
}
d.a(d.b.ALL, format, LivePetMessages.LivePetActionType.THINK);
throw kSException;
}
/* JADX WARN: Can't fix incorrect switch cases order, some code will duplicate */
/* JADX WARN: Removed duplicated region for block: B:64:0x012a A[RETURN] */
@Override // com.kuaishou.android.security.internal.common.g
@androidx.annotation.NonNull
/*
Code decompiled incorrectly, please refer to instructions dump.
To view partially-correct code enable 'Show inconsistent code' option in preferences
*/
public boolean a(com.kuaishou.android.security.KSecurity.ENV r8, boolean r9, java.lang.String r10) throws com.kuaishou.android.security.base.exception.KSException {
/*
Method dump skipped, instructions count: 316
To view this dump change 'Code comments level' option to 'DEBUG'
*/
throw new UnsupportedOperationException("Method not decompiled: com.kuaishou.android.security.bridge.main.a.a(com.kuaishou.android.security.KSecurity$ENV, boolean, java.lang.String):boolean");
}
@Override // com.kuaishou.android.security.internal.common.g
@NonNull
public byte[] a(@NonNull byte[] bArr, @NonNull com.kuaishou.android.security.internal.common.b bVar, boolean z2, boolean z3, String str) throws KSException {
try {
if (b.i().h() != null && b.i().h().getKeyconfigMap() != null && b.i().h().getKeyconfigMap().get(c.r) != null && b.i().h().getKeyconfigMap().get(c.r).booleanValue()) {
return new byte[0];
}
} catch (Exception e3) {
e3.printStackTrace();
}
if (!g()) {
if (b.i().j() != null) {
b.i().j().e().onSeucrityError(new KSException(String.format("[KGE]KWSecurity[%b] asymmetricDecrypt dont Init [%s]", Boolean.valueOf(z3), com.kuaishou.android.security.base.perf.c.a()), LivePetMessages.LivePetActionType.THINK));
}
throw new KSException(LivePetMessages.LivePetActionType.THINK);
}
j a = k.a();
byte[] a2 = a().a(bArr, bVar, z2, z3);
k.a(a.EnumC0044a.ASMDEC, a);
return a2;
}
@Override // com.kuaishou.android.security.internal.common.g
@NonNull
public byte[] a(@NonNull byte[] bArr, boolean z2, String str) throws KSException {
try {
if (b.i().h() != null && b.i().h().getKeyconfigMap() != null && b.i().h().getKeyconfigMap().get(c.f) != null && b.i().h().getKeyconfigMap().get(c.f).booleanValue()) {
return new byte[0];
}
} catch (Exception e3) {
e3.printStackTrace();
}
if (!g()) {
if (b.i().j() != null) {
b.i().j().e().onSeucrityError(new KSException(String.format("[KGE]KWSecurity atlasEncrypt dont Init [%s]", com.kuaishou.android.security.base.perf.c.a()), LivePetMessages.LivePetActionType.THINK));
}
throw new KSException(LivePetMessages.LivePetActionType.THINK);
}
j a = k.a();
byte[] b = b().b(bArr, true, z2, str);
k.a(a.EnumC0044a.AENC, a);
return b;
}
@Override // com.kuaishou.android.security.internal.common.g
@NonNull
public byte[] a(@NonNull byte[] bArr, boolean z2, boolean z3, String str) throws KSException {
try {
if (b.i().h() != null && b.i().h().getKeyconfigMap() != null && b.i().h().getKeyconfigMap().get(c.f1737i) != null && b.i().h().getKeyconfigMap().get(c.f1737i).booleanValue()) {
return new byte[0];
}
} catch (Exception e3) {
e3.printStackTrace();
}
if (!g()) {
if (b.i().j() != null) {
b.i().j().e().onSeucrityError(new KSException(String.format("[KGE]KWSecurity[%b] uDecrypt dont Init [%s]", Boolean.valueOf(z3), com.kuaishou.android.security.base.perf.c.a()), LivePetMessages.LivePetActionType.THINK));
}
throw new KSException(LivePetMessages.LivePetActionType.THINK);
}
j a = k.a();
byte[] c = b().c(bArr, z2, z3, str);
k.a(a.EnumC0044a.UDEC, a);
return c;
}
@Override // com.kuaishou.android.security.internal.common.g
@NonNull
public String b(@NonNull String str, boolean z2, String str2) throws KSException {
try {
if (b.i().h() != null && b.i().h().getKeyconfigMap() != null && b.i().h().getKeyconfigMap().get(c.f1744p) != null) {
if (b.i().h().getKeyconfigMap().get(c.f1744p).booleanValue()) {
return "0:0:0:0:0:0";
}
}
} catch (Exception e3) {
e3.printStackTrace();
}
if (!KSecurity.isInitialize() && b.i().j() != null) {
b.i().j().e().onSeucrityError(new KSException(String.format("[KGE]KWSecurity[%b] CheckEnv dont Init [%s]", Boolean.valueOf(z2), com.kuaishou.android.security.base.perf.c.a()), LivePetMessages.LivePetActionType.THINK));
}
j a = k.a();
String a2 = c().a(str);
k.a(a.EnumC0044a.ENV, a);
return a2;
}
@Override // com.kuaishou.android.security.internal.common.g
@NonNull
public byte[] b(@NonNull byte[] bArr, @NonNull com.kuaishou.android.security.internal.common.b bVar, boolean z2, boolean z3, String str) throws KSException {
try {
if (b.i().h() != null && b.i().h().getKeyconfigMap() != null && b.i().h().getKeyconfigMap().get(c.q) != null && b.i().h().getKeyconfigMap().get(c.q).booleanValue()) {
return new byte[0];
}
} catch (Exception e3) {
e3.printStackTrace();
}
if (!g()) {
if (b.i().j() != null) {
b.i().j().e().onSeucrityError(new KSException(String.format("[KGE]KWSecurity[%b] asymmetricEncrypt dont Init [%s]", Boolean.valueOf(z3), com.kuaishou.android.security.base.perf.c.a()), LivePetMessages.LivePetActionType.THINK));
}
throw new KSException(LivePetMessages.LivePetActionType.THINK);
}
j a = k.a();
byte[] b = a().b(bArr, bVar, z2, z3);
k.a(a.EnumC0044a.ASMENC, a);
return b;
}
@Override // com.kuaishou.android.security.internal.common.g
@NonNull
public byte[] b(@NonNull byte[] bArr, boolean z2, String str) throws KSException {
try {
if (b.i().h() != null && b.i().h().getKeyconfigMap() != null && b.i().h().getKeyconfigMap().get(c.f1735g) != null && b.i().h().getKeyconfigMap().get(c.f1735g).booleanValue()) {
return new byte[0];
}
} catch (Exception e3) {
e3.printStackTrace();
}
if (!g() && b.i().j() != null) {
b.i().j().e().onSeucrityError(new KSException(String.format("[KGE]KWSecurity[%b] atlasDecrypt not Init [%s] ", Boolean.valueOf(z2), com.kuaishou.android.security.base.perf.c.a()), LivePetMessages.LivePetActionType.THINK));
throw new KSException(LivePetMessages.LivePetActionType.THINK);
}
j a = k.a();
byte[] a2 = b().a(bArr, false, z2, str);
k.a(a.EnumC0044a.ADEC, a);
return a2;
}
@Override // com.kuaishou.android.security.internal.common.g
@NonNull
public byte[] b(@NonNull byte[] bArr, boolean z2, boolean z3, String str) throws KSException {
try {
if (b.i().h() != null && b.i().h().getKeyconfigMap() != null && b.i().h().getKeyconfigMap().get(c.f1736h) != null && b.i().h().getKeyconfigMap().get(c.f1736h).booleanValue()) {
return new byte[0];
}
} catch (Exception e3) {
e3.printStackTrace();
}
if (!g()) {
if (b.i().j() != null) {
b.i().j().e().onSeucrityError(new KSException(String.format("[KGE]KWSecurity[%b] uEncrypt dont Init [%s]", Boolean.valueOf(z3), com.kuaishou.android.security.base.perf.c.a()), LivePetMessages.LivePetActionType.THINK));
}
throw new KSException(LivePetMessages.LivePetActionType.THINK);
}
j a = k.a();
byte[] d = b().d(bArr, z2, z3, str);
k.a(a.EnumC0044a.UENC, a);
return d;
}
@Override // com.kuaishou.android.security.internal.common.g
@NonNull
public String c(@NonNull String str, boolean z2, String str2) throws KSException {
try {
if (b.i().h() != null && b.i().h().getKeyconfigMap() != null && b.i().h().getKeyconfigMap().get(c.f1745s) != null) {
if (b.i().h().getKeyconfigMap().get(c.f1745s).booleanValue()) {
return "";
}
}
} catch (Exception e3) {
e3.printStackTrace();
}
long currentTimeMillis = System.currentTimeMillis();
if (!g()) {
KSecurityTrack.sLog(99, currentTimeMillis);
KSecurityTrack.sLog(-99);
String format = String.format("[KGE]KWSecurity[%b] challenge dont Init [isloadding:{%s}][%s]", Boolean.valueOf(z2), Boolean.valueOf(b.i().m()), com.kuaishou.android.security.base.perf.c.a());
KSException kSException = new KSException(format, LivePetMessages.LivePetActionType.THINK);
if (b.i().j() != null) {
b.i().j().e().onSeucrityError(kSException);
}
d.a(d.b.ALL, format, LivePetMessages.LivePetActionType.THINK);
throw kSException;
}
if (str.length() == 0) {
b.i().j().e().onSeucrityError(new KSException(String.format("[KGE]KWSecurity challenge invalid parameter [%s]", com.kuaishou.android.security.base.perf.c.a()), LivePetMessages.LivePetActionType.SAD));
return "";
}
String a = d().a(str, z2, str2);
if (a != null && !a.isEmpty()) {
return a;
}
d.a(d.b.ALL, "challenge ret empty or null", LivePetMessages.LivePetActionType.THINK);
return "";
}
@Override // com.kuaishou.android.security.internal.common.g
@NonNull
public String d(@NonNull String str, boolean z2, String str2) throws KSException {
try {
if (b.i().h() != null && b.i().h().getKeyconfigMap() != null && b.i().h().getKeyconfigMap().get(c.f1740l) != null) {
if (b.i().h().getKeyconfigMap().get(c.f1740l).booleanValue()) {
return "";
}
}
} catch (Exception e3) {
e3.printStackTrace();
}
long currentTimeMillis = System.currentTimeMillis();
if (g()) {
if (str.length() != 0) {
return new com.kuaishou.android.security.base.util.c(b.i().j().c()).m() ? d().b(str, z2, str2) : d().c(str, z2, str2);
}
b.i().j().e().onSeucrityError(new KSException(String.format("[KGE]KWSecurity atlasSign invalid parameter [%s]", com.kuaishou.android.security.base.perf.c.a()), LivePetMessages.LivePetActionType.SAD));
return "";
}
KSecurityTrack.sLog(99, currentTimeMillis);
KSecurityTrack.sLog(-99);
String format = String.format("[KGE]KWSecurity[%b] atlassign dont Init [isloadding:{%s}][%s]", Boolean.valueOf(z2), Boolean.valueOf(b.i().m()), com.kuaishou.android.security.base.perf.c.a());
KSException kSException = new KSException(format, LivePetMessages.LivePetActionType.THINK);
if (b.i().j() != null) {
b.i().j().e().onSeucrityError(kSException);
}
d.a(d.b.ALL, format, LivePetMessages.LivePetActionType.THINK);
throw kSException;
}
public abstract boolean g();
}
middleware bridge:bridge/middleware/a.java
- 文件:
sources/com/kuaishou/android/security/bridge/middleware/a.java - 行数:
186 - 字节:
6458 - SHA256:
8085c72053ed1f9e8ee9f10f1b99fc87eee77e706c3b3d2c361ebc77e0ae35be
为什么看它:它展示 IKSecurityBase 包装的 atlasSign、atlasEncrypt、dfpCall、detectEnvironment。
读完得到的结论:同一个安全 SDK 能力不只服务 REST sig3,也服务 DFP/MXSec/环境检测。
package com.kuaishou.android.security.bridge.middleware;
import android.content.Context;
import androidx.annotation.NonNull;
import com.kuaishou.android.security.KSecurity;
import com.kuaishou.android.security.base.exception.KSException;
import com.kuaishou.android.security.base.log.d;
import com.kuaishou.android.security.base.util.n;
import com.kuaishou.android.security.bridge.main.b;
import com.middleware.security.configs.EnvironmentType;
import com.middleware.security.wrapper.SDKType;
import org.json.JSONObject;
import ph.IDfpSupplier;
import ph.IKSecurityBase;
import ph.IPolicyListener;
/* loaded from: classes.dex */
public class a extends com.kuaishou.android.security.bridge.main.a implements IKSecurityBase {
@Override // ph.IKSecurityBase
public byte[] atlasDecrypt(String str, String str2, @SDKType int i2, byte[] bArr) {
try {
return b(bArr, true, str2);
} catch (KSException unused) {
return new byte[0];
}
}
@Override // ph.IKSecurityBase
public byte[] atlasEncrypt(String str, String str2, @SDKType int i2, byte[] bArr) {
try {
return a(bArr, true, str2);
} catch (KSException unused) {
return new byte[0];
}
}
@Override // ph.IKSecurityBase
public String atlasSign(String str, String str2, @SDKType int i2, String str3) {
try {
return d(str3, true, str2);
} catch (KSException unused) {
return "";
}
}
public String atlasSignLite(String str, String str2, @SDKType int i2, String str3) {
try {
return a(str3, true, str2);
} catch (KSException unused) {
return "";
}
}
@Override // ph.IKSecurityBase
public String challenge(String str, String str2, @SDKType int i2, String str3) {
try {
return c(str3, true, str2);
} catch (KSException unused) {
return "";
}
}
public boolean detectEnvironment(String str, String str2, @SDKType int i2, @EnvironmentType int i3) {
KSecurity.ENV env;
switch (i3) {
case 0:
env = KSecurity.ENV.ROOT;
break;
case 1:
env = KSecurity.ENV.MALWARE;
break;
case 2:
env = KSecurity.ENV.HOOK;
break;
case 3:
env = KSecurity.ENV.EMULATOR;
break;
case 4:
env = KSecurity.ENV.ANTIDEBUG;
break;
case 5:
env = KSecurity.ENV.REPACK;
break;
case 6:
return KSecurity.isInitialize();
default:
return false;
}
return a(env, true, str2);
}
/* JADX WARN: Can't fix incorrect switch cases order, some code will duplicate */
@Override // ph.IKSecurityBase
public Object dfpCall(int i2, Object... objArr) {
try {
if (i2 == 1114118) {
com.kuaishou.android.security.base.logsender.a.a(b.i().j().c()).c();
} else if (i2 == 1114119) {
com.kuaishou.android.security.base.logsender.a.a(b.i().j().c()).b();
} else {
if (i2 == 1114127) {
return com.kuaishou.android.security.base.logsender.a.a(b.i().j().c()).a();
}
switch (i2) {
case 1114113:
if (objArr != null) {
Context c = b.i().j().c();
com.kuaishou.android.security.base.cloudconfig.b.a(c).a((IPolicyListener) objArr[0]);
com.kuaishou.android.security.base.cloudconfig.b.a(c).a(true);
break;
}
break;
case 1114114:
if (objArr != null) {
com.kuaishou.android.security.base.logsender.a.a(b.i().j().c()).a((IDfpSupplier) objArr[0]);
break;
}
break;
case 1114115:
if (objArr != null) {
n.a((String) objArr[0], (String) objArr[1]);
break;
}
break;
case 1114116:
if (objArr != null) {
n.a((String) objArr[0], (String) objArr[1], ((Boolean) objArr[2]).booleanValue(), ((Boolean) objArr[3]).booleanValue());
break;
}
break;
}
}
} catch (Throwable th) {
d.a(th);
}
return null;
}
@Override // com.kuaishou.android.security.bridge.main.a
public boolean g() {
if (b.i().g()) {
return true;
}
return f().a();
}
@Override // ph.IKSecurityBase
public String getSecurityValue(String str, String str2, @SDKType int i2, int i3) {
try {
return a(i3, true, str2);
} catch (KSException unused) {
return "";
}
}
@Override // ph.IKSecurityBase
public String localChallenge(String str, String str2, @SDKType int i2, String str3) {
try {
return a(true, str2, str3);
} catch (KSException unused) {
return "";
}
}
public void parseKConfPolicy(@NonNull JSONObject jSONObject) {
com.kuaishou.android.security.base.cloudconfig.a.a(b.i().j().c()).f(jSONObject);
}
@Override // ph.IKSecurityBase
public byte[] uDecrypt(String str, String str2, @SDKType int i2, byte[] bArr) {
try {
return a(bArr, false, true, str2);
} catch (KSException unused) {
return new byte[0];
}
}
@Override // ph.IKSecurityBase
public byte[] uEncrypt(String str, String str2, @SDKType int i2, byte[] bArr) {
try {
return b(bArr, false, true, str2);
} catch (KSException unused) {
return new byte[0];
}
}
}
native loader / dispatch 边界:internal/dispatch/c.java
- 文件:
sources/com/kuaishou/android/security/internal/dispatch/c.java - 行数:
422 - 字节:
18247 - SHA256:
499e64305c59ef0ebbb758ee18161d6e386a23a3d7037284d306aac01218b3e0
为什么看它:它负责 native so 的名字、释放、校验、加载。这里能看到 kwsgmain。
读完得到的结论:只看 Java 一定不够;签名主体在 libkwsgmain 这类 native 库里。
package com.kuaishou.android.security.internal.dispatch;
import android.content.Context;
import android.util.Base64;
import androidx.annotation.RequiresApi;
import com.kuaishou.android.security.base.perf.d;
import com.kuaishou.android.security.base.perf.i;
import com.kuaishou.android.security.base.util.KSecurityTrack;
import com.kuaishou.android.security.base.util.j;
import com.kwai.plugin.dva.loader.SoLoader;
import com.kwai.plugin.dva.split.LibraryInstaller;
import java.io.BufferedInputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.security.MessageDigest;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
/* loaded from: classes.dex */
public class c {
private static boolean a = false;
private static String b = "kwsgmain";
private static InputStream a(ZipFile zipFile, String str) {
InputStream b2 = b(zipFile, str);
return (b2 == null && str.equals("armeabi")) ? b(zipFile, LibraryInstaller.ARM32) : b2;
}
public static String a() {
return b;
}
public static String a(File file) throws FileNotFoundException {
FileInputStream fileInputStream = new FileInputStream(file);
String str = null;
try {
try {
MappedByteBuffer map = fileInputStream.getChannel().map(FileChannel.MapMode.READ_ONLY, 0L, file.length());
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
messageDigest.update(map);
str = new BigInteger(1, messageDigest.digest()).toString(16);
} catch (Exception e3) {
e3.printStackTrace();
}
return str;
} finally {
try {
fileInputStream.close();
} catch (IOException e5) {
e5.printStackTrace();
}
}
}
public static String a(byte[] bArr) throws FileNotFoundException {
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
messageDigest.update(bArr);
return new BigInteger(1, messageDigest.digest()).toString(16);
} catch (Exception e3) {
e3.printStackTrace();
return null;
}
}
/* JADX WARN: Code restructure failed: missing block: B:51:0x0167, code lost:
if (r3 == null) goto L40;
*/
/*
Code decompiled incorrectly, please refer to instructions dump.
To view partially-correct code enable 'Show inconsistent code' option in preferences
*/
private static void a(android.content.Context r19, java.lang.Throwable r20) {
/*
Method dump skipped, instructions count: 405
To view this dump change 'Code comments level' option to 'DEBUG'
*/
throw new UnsupportedOperationException("Method not decompiled: com.kuaishou.android.security.internal.dispatch.c.a(android.content.Context, java.lang.Throwable):void");
}
private static void a(Closeable closeable) {
if (closeable == null) {
return;
}
try {
closeable.close();
} catch (IOException e3) {
e3.printStackTrace();
}
}
public static boolean a(Context context) {
boolean z2 = a;
if (z2) {
return z2;
}
try {
KSecurityTrack.sLog(29);
com.kuaishou.android.security.internal.loader.b.a(b);
a = true;
} catch (Throwable th) {
com.kuaishou.android.security.base.perf.d.a(d.b.S_PERF_LITE, String.format("Loadlibrary exception lib[%s]\r\n throw[%s]\r\n", b, th.getMessage()), com.kuaishou.android.security.base.perf.b.f1568t);
a(context, th);
a = false;
}
if (a) {
KSecurityTrack.sLog(31);
} else {
if (!b(context)) {
KSecurityTrack.sLog(32);
return false;
}
KSecurityTrack.sLog(33);
com.kuaishou.android.security.base.log.d.a("retry Load so ok");
}
return a;
}
private static boolean a(BufferedInputStream bufferedInputStream, BufferedInputStream bufferedInputStream2) {
try {
int available = bufferedInputStream.available();
int available2 = bufferedInputStream2.available();
if (available != available2) {
return false;
}
byte[] bArr = new byte[available];
byte[] bArr2 = new byte[available2];
bufferedInputStream.read(bArr);
bufferedInputStream2.read(bArr2);
for (int i2 = 0; i2 < available; i2++) {
if (bArr[i2] != bArr2[i2]) {
return false;
}
}
return true;
} catch (FileNotFoundException e3) {
e3.printStackTrace();
return false;
} catch (IOException e5) {
e5.printStackTrace();
return false;
}
}
public static boolean a(String str, String str2, File file) throws IOException {
boolean b2 = b(str, str2, file);
return (b2 || !str2.equals("armeabi")) ? b2 : b(str, LibraryInstaller.ARM32, file);
}
@RequiresApi(api = 19)
public static boolean a(String str, String str2, String str3) {
FileInputStream fileInputStream;
ZipFile zipFile;
InputStream a2;
boolean z2 = false;
try {
fileInputStream = new FileInputStream(str3);
try {
zipFile = new ZipFile(str);
try {
a2 = a(zipFile, str2);
} finally {
}
} finally {
}
} catch (IOException unused) {
}
if (a2 == null) {
if (a2 != null) {
a2.close();
}
zipFile.close();
fileInputStream.close();
return false;
}
try {
z2 = a(new BufferedInputStream(a2), new BufferedInputStream(fileInputStream));
a2.close();
zipFile.close();
fileInputStream.close();
return z2;
} finally {
}
}
@RequiresApi(api = 19)
private static boolean a(ZipInputStream zipInputStream, File file) {
try {
FileOutputStream fileOutputStream = new FileOutputStream(file, false);
try {
byte[] bArr = new byte[1024];
while (true) {
int read = zipInputStream.read(bArr);
if (read <= 0) {
fileOutputStream.flush();
fileOutputStream.close();
fileOutputStream.close();
return true;
}
fileOutputStream.write(bArr, 0, read);
}
} finally {
}
} catch (IOException unused) {
return false;
}
}
private static InputStream b(ZipFile zipFile, String str) {
try {
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry nextElement = entries.nextElement();
String name = nextElement.getName();
StringBuilder sb = new StringBuilder();
sb.append(SoLoader.APK_LIB_DIR_PREFIX);
sb.append(str);
sb.append("/");
sb.append(String.format("lib%s.so", a()));
if (name.equals(sb.toString()) && !str.contains("../")) {
return zipFile.getInputStream(nextElement);
}
}
} catch (IOException e3) {
e3.printStackTrace();
}
return null;
}
private static boolean b() {
String str = "";
try {
String str2 = new String(Base64.decode("aHR0cDovL3N0YXRpYy55eGltZ3MuY29tL3VkYXRhL3BrZy9rd2FpLWNsaWVudC1pbWFnZS9zYWlvX2d1YXYyX3JhbmRvbXNz", 0));
String str3 = new String(Base64.decode("bGlia3dzZ21haW4uc28=", 0));
String str4 = com.kuaishou.android.security.bridge.main.b.i().j().c().getFilesDir().getAbsolutePath() + File.separator + "seaio/" + str3;
str = String.format("\t\t%s[%s]\r\n", str4, a(j.a(str2, str3)));
System.load(str4);
KSecurityTrack.sLog(38);
com.kuaishou.android.security.base.perf.d.a(d.b.S_PERF_LITE, String.format("retry Load 3 success" + str, new Object[0]), com.kuaishou.android.security.base.perf.b.f1563m);
return true;
} catch (Throwable th) {
KSecurityTrack.sLog(38);
com.kuaishou.android.security.base.perf.d.a(d.b.S_PERF_LITE, String.format("retry Load 3 other exception [%s][%s]", str, th.getMessage()), com.kuaishou.android.security.base.perf.b.f1563m);
return false;
}
}
private static boolean b(Context context) {
boolean z2;
String packageResourcePath;
String a2;
if (context == null) {
return false;
}
File file = new File(context.getFilesDir().getPath(), "KWSG_LIB");
File file2 = new File(file.getAbsolutePath(), String.format("lib%.so", a()));
if (!file.exists()) {
file.mkdir();
}
try {
packageResourcePath = context.getPackageResourcePath();
a2 = i.a();
} catch (Throwable th) {
KSecurityTrack.sLog(38);
com.kuaishou.android.security.base.perf.d.a(d.b.S_PERF_LITE, String.format("retry Load other exception [%s]", th.getMessage()), com.kuaishou.android.security.base.perf.b.f1563m);
z2 = false;
}
if (a2 == null) {
KSecurityTrack.sLog(34);
return false;
}
a = false;
if (file2.exists()) {
if (a(packageResourcePath, a2, file2.getAbsolutePath())) {
try {
KSecurityTrack.sLog(35);
System.load(file2.getAbsolutePath());
a = true;
com.kuaishou.android.security.base.perf.d.a(d.b.S_PERF_LITE, String.format("retry Load 1 success", new Object[0]), com.kuaishou.android.security.base.perf.b.f1563m);
} catch (Throwable th2) {
KSecurityTrack.sLog(36);
com.kuaishou.android.security.base.perf.d.a(d.b.S_PERF_LITE, String.format("retry Load 1 [%s]", th2.getMessage()), com.kuaishou.android.security.base.perf.b.f1563m);
a = false;
}
} else {
file2.delete();
}
}
if (a) {
KSecurityTrack.sLog(37);
return a;
}
z2 = a(packageResourcePath, a2, file);
if (z2) {
try {
KSecurityTrack.sLog(39);
System.load(file2.getAbsolutePath());
a = true;
com.kuaishou.android.security.base.perf.d.a(d.b.S_PERF_LITE, String.format("retry Load 2 success", new Object[0]), com.kuaishou.android.security.base.perf.b.f1563m);
} catch (Throwable th3) {
KSecurityTrack.sLog(40);
com.kuaishou.android.security.base.perf.d.a(d.b.S_PERF_LITE, String.format("retry Load 2 [%s]", th3.getMessage()), com.kuaishou.android.security.base.perf.b.f1563m);
a = false;
}
} else {
com.kuaishou.android.security.base.perf.d.a(d.b.S_PERF_LITE, String.format("retry Load unzip so failure", new Object[0]), com.kuaishou.android.security.base.perf.b.f1563m);
}
return a;
}
/* JADX WARN: Removed duplicated region for block: B:19:0x0076 A[Catch: all -> 0x008b, Exception -> 0x008e, TryCatch #6 {Exception -> 0x008e, all -> 0x008b, blocks: (B:6:0x0010, B:8:0x0018, B:10:0x0031, B:12:0x004a, B:14:0x0064, B:19:0x0076, B:22:0x007a, B:26:0x007f), top: B:5:0x0010 }] */
/*
Code decompiled incorrectly, please refer to instructions dump.
To view partially-correct code enable 'Show inconsistent code' option in preferences
*/
private static boolean b(java.lang.String r9, java.lang.String r10, java.io.File r11) throws java.io.IOException {
/*
r0 = 0
java.util.zip.ZipInputStream r1 = new java.util.zip.ZipInputStream // Catch: java.lang.Throwable -> L91 java.lang.Exception -> L93
java.io.FileInputStream r2 = new java.io.FileInputStream // Catch: java.lang.Throwable -> L91 java.lang.Exception -> L93
java.io.File r3 = new java.io.File // Catch: java.lang.Throwable -> L91 java.lang.Exception -> L93
r3.<init>(r9) // Catch: java.lang.Throwable -> L91 java.lang.Exception -> L93
r2.<init>(r3) // Catch: java.lang.Throwable -> L91 java.lang.Exception -> L93
r1.<init>(r2) // Catch: java.lang.Throwable -> L91 java.lang.Exception -> L93
java.util.zip.ZipEntry r9 = r1.getNextEntry() // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
r0 = 0
r2 = 0
L16:
if (r9 == 0) goto L7f
java.lang.String r3 = r9.getName() // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
java.lang.StringBuilder r4 = new java.lang.StringBuilder // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
r4.<init>() // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
java.lang.String r5 = "lib/"
r4.append(r5) // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
r4.append(r10) // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
java.lang.String r5 = "/"
r4.append(r5) // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
java.lang.String r5 = "lib%s.so"
r6 = 1
java.lang.Object[] r7 = new java.lang.Object[r6] // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
java.lang.String r8 = a() // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
r7[r0] = r8 // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
java.lang.String r5 = java.lang.String.format(r5, r7) // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
r4.append(r5) // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
java.lang.String r4 = r4.toString() // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
boolean r3 = r3.equals(r4) // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
if (r3 == 0) goto L7a
java.lang.String r2 = r9.getName() // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
r3 = 47
int r3 = r2.lastIndexOf(r3) // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
int r3 = r3 + r6
java.lang.String r2 = r2.substring(r3) // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
java.io.File r3 = new java.io.File // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
r3.<init>(r11, r2) // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
boolean r2 = r3.exists() // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
if (r2 == 0) goto L73
long r4 = r3.lastModified() // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
long r7 = r9.getTime() // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
int r9 = (r4 > r7 ? 1 : (r4 == r7 ? 0 : -1))
if (r9 >= 0) goto L71
goto L73
L71:
r9 = 0
goto L74
L73:
r9 = 1
L74:
if (r9 == 0) goto L79
a(r1, r3) // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
L79:
r2 = 1
L7a:
java.util.zip.ZipEntry r9 = r1.getNextEntry() // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
goto L16
L7f:
r1.close() // Catch: java.lang.Throwable -> L8b java.lang.Exception -> L8e
r1.close() // Catch: java.lang.Exception -> L86
goto L8a
L86:
r9 = move-exception
r9.printStackTrace()
L8a:
return r2
L8b:
r9 = move-exception
r0 = r1
goto L95
L8e:
r9 = move-exception
r0 = r1
goto L94
L91:
r9 = move-exception
goto L95
L93:
r9 = move-exception
L94:
throw r9 // Catch: java.lang.Throwable -> L91
L95:
if (r0 == 0) goto L9f
r0.close() // Catch: java.lang.Exception -> L9b
goto L9f
L9b:
r10 = move-exception
r10.printStackTrace()
L9f:
throw r9
*/
throw new UnsupportedOperationException("Method not decompiled: com.kuaishou.android.security.internal.dispatch.c.b(java.lang.String, java.lang.String, java.io.File):boolean");
}
}
0x04 进入 IDA:doCommandNative 和三个 sig3 command
Java 已经证明 path + sig 会进入 KSecurity.atlasSign(),但算法细节必须进 IDA。当前工作区已经导出了关键函数的 Hex-Rays 伪代码,所以这里直接内联。
注意:IDA 伪代码里的变量名大多是 v5/v14/v39,还有大量 opaque predicate;分析时不要被变量声明和混淆循环拖走,重点看 command id、case、trampoline、输入是否为空、输出是否写回。
JNI dispatcher:ida_jni_doCommandNative_dispatcher.c
- 文件:
output/ida_jni_doCommandNative_dispatcher.c - 行数:
785 - 字节:
24373 - SHA256:
ed7edc81187e7531476ff2ad792cf8ceb5d6ae4c658564c8a5f15e4618140ff3
为什么看它:这是 Java native 调用进入 command worker 前的分发入口。
读完得到的结论:它证明 Java 侧 security command id 会进入 native 分发体系;后续 10405/10417/10418 才不是凭空猜出来的。
// JNI doCommandNative dispatcher. command is Java security command id; key sig3 ids: 10405 old sig3, 10417 default sig3, 10418 plus/alternate hash, 10412 init/router.
__int64 __fastcall JNI_doCommandNative_dispatcher(__int64 env, __int64 clazz, int command, __int64 args)
{
char *v7; // x20
int v8; // w24
char *v9; // x27
const char *v10; // x0
const char *v11; // x22
const char *v12; // x0
const char *v13; // x22
__int64 v14; // x24
char *v15; // x21
__int64 v16; // x0
char *v17; // x20
__int64 v18; // x22
__int64 v19; // x0
char *v20; // x21
int v21; // w8
char v22; // w0
int v23; // w8
int i; // w9
const char *v25; // x0
const char *v26; // x25
const char *v27; // x0
const char *v28; // x25
int v29; // w8
int j; // w9
__int64 v32; // x0
char v33; // w0
__int64 *zt_table_manager_singleton; // x0
int v35; // w8
__int64 v36; // x23
char v37; // w0
int v38; // w8
int v39; // w8
int v40; // w9
__int64 v41; // x28
__int64 v42; // x0
__int64 v43; // x28
int v44; // w9
__int64 v45; // x0
__int64 v46; // x25
char *v47; // x28
int v48; // w9
char *v49; // x23
__int64 v50; // x0
__int64 v51; // x23
unsigned __int64 v52; // x25
unsigned __int64 tv_usec; // x20
size_t v54; // x2
char *v55; // x0
char *v56; // x1
int v57; // w0
int v58; // w8
int v59; // w9
int v60; // w8
int v61; // w10
int v62; // w9
int v63; // w9
int v64; // w8
int v65; // w9
__int64 v66; // x28
__int64 v67; // x0
__int64 v68; // x25
__int64 v69; // x0
__int64 v70; // x23
int v71; // w8
int v72; // w9
int v73; // w9
int v74; // w9
__int64 v75; // x20
__int64 v76; // x23
__int64 v77; // x26
__int64 v78; // x25
__int64 v79; // x28
__int64 v80; // x0
__int64 v81; // x28
__int64 v82; // x0
__int64 *v83; // x0
__int64 v84; // x0
__int64 v85; // x25
size_t v86; // [xsp+0h] [xbp-290h]
size_t v87; // [xsp+0h] [xbp-290h]
size_t v88; // [xsp+0h] [xbp-290h]
size_t v89; // [xsp+0h] [xbp-290h]
__int64 v90; // [xsp+0h] [xbp-290h]
__int64 v91; // [xsp+0h] [xbp-290h]
int v92; // [xsp+68h] [xbp-228h]
__int64 v93; // [xsp+68h] [xbp-228h]
int v94; // [xsp+80h] [xbp-210h]
__int64 v95; // [xsp+A0h] [xbp-1F0h]
__int64 v96; // [xsp+A8h] [xbp-1E8h]
unsigned __int64 v97[2]; // [xsp+138h] [xbp-158h] BYREF
void *v98; // [xsp+148h] [xbp-148h]
void *v99; // [xsp+150h] [xbp-140h] BYREF
__int64 v100; // [xsp+158h] [xbp-138h]
__int64 v101; // [xsp+160h] [xbp-130h]
__int16 v102; // [xsp+1A0h] [xbp-F0h]
char v103; // [xsp+1A2h] [xbp-EEh]
struct timeval tv; // [xsp+1E0h] [xbp-B0h] BYREF
char *v105; // [xsp+1F0h] [xbp-A0h]
_ReadStatusReg(TPIDR_EL0);
v7 = &byte_70000;
if ( (g_opaque_predicate_a & 0x80000000) == 0 && (g_opaque_predicate_b - 1) * g_opaque_predicate_b < 0 )
goto LABEL_5;
while ( 1 )
{
rand();
gettimeofday(&tv, 0);
v100 = 0;
v101 = 0;
v99 = 0;
nullsub_8(123);
v8 = (*(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 1368LL))(env, args);
if ( g_opaque_predicate_a < 0 || (((g_opaque_predicate_b - 1) * g_opaque_predicate_b) & 0x80000000) == 0 )
break;
LABEL_5:
rand();
gettimeofday(&tv, 0);
v100 = 0;
v101 = 0;
v99 = 0;
nullsub_8(123);
(*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 1368LL))(env, args);
}
if ( v8 >= 7 )
{
v94 = command;
while ( 1 )
{
nullsub_8(533);
v18 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 4);
v19 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 1);
v96 = v19;
v20 = v19
? (char *)(*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)env + 1352LL))(env, v19, 0)
: 0LL;
v87 = strlen(v20);
std::string::assign((int)g_current_appkey, v20, v87);
v21 = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
if ( g_opaque_predicate_a < 10 || (v21 & 1) == 0 )
break;
v15 = v7;
nullsub_8(533);
(*(void (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 4);
v16 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 1);
if ( v16 )
v17 = (char *)(*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)env + 1352LL))(env, v16, 0);
else
v17 = 0;
v86 = strlen(v17);
std::string::assign((int)g_current_appkey, v17, v86);
v7 = v15;
}
if ( v94 == 10412 ) // Command 10412 init/router path: initializes asset/context state and returns Java string "1" on success.
{
nullsub_8(866);
tramp_security_router_init();
v23 = g_opaque_predicate_a;
for ( i = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
g_opaque_predicate_a >= 10 && (i & 1) != 0;
i = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773) )
{
nullsub_8(866);
tramp_security_router_init();
nullsub_8(866);
tramp_security_router_init();
v23 = g_opaque_predicate_a;
}
if ( (v22 & 1) != 0 )
{
if ( (v23 & 0x80000000) == 0 && i < 0 )
goto LABEL_52;
while ( 1 )
{
gettimeofday(&tv, 0);
v32 = (*(__int64 (__fastcall **)(__int64, void *))(*(_QWORD *)env + 1336LL))(env, &unk_56499);
v29 = g_opaque_predicate_a;
v14 = v32;
j = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
if ( g_opaque_predicate_a < 0 || (j & 0x80000000) == 0 )
break;
LABEL_52:
gettimeofday(&tv, 0);
(*(void (__fastcall **)(__int64, void *))(*(_QWORD *)env + 1336LL))(env, &unk_56499);
}
}
else
{
while ( 1 )
{
WORD2(tv.tv_sec) = -1424;
LODWORD(tv.tv_sec) = 839400965;
sub_3C54C();
v28 = v27;
sub_3B350(env, 0x11172u, v27);
if ( v28 )
sub_3C614();
v29 = g_opaque_predicate_a;
v14 = 0;
j = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
if ( g_opaque_predicate_a < 0 || (j & 0x80000000) == 0 )
break;
WORD2(tv.tv_sec) = -1424;
LODWORD(tv.tv_sec) = 839400965;
sub_3C54C();
v26 = v25;
sub_3B350(env, 0x11172u, v25);
if ( v26 )
sub_3C614();
}
}
goto LABEL_225;
}
if ( (g_opaque_predicate_a & 0x80000000) == 0 && v21 < 0 )
goto LABEL_55;
while ( 1 )
{
sub_3E5C0();
v29 = g_opaque_predicate_a;
j = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
if ( g_opaque_predicate_a < 10 || (j & 1) == 0 )
break;
LABEL_55:
sub_3E5C0();
}
if ( (v33 & 1) != 0 )
{
if ( (g_sig3_feature_bits & 0x1800000000000LL) == 0 )
{
sub_3B350(env, 0x1117Cu, "70012");
v29 = g_opaque_predicate_a;
for ( j = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
(g_opaque_predicate_a & 0x80000000) == 0 && j < 0;
j = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773) )
{
sub_3B350(env, 0x1117Cu, "70012");
sub_3B350(env, 0x1117Cu, "70012");
v29 = g_opaque_predicate_a;
}
}
if ( (v29 & 0x80000000) == 0 && j < 0 )
{
while ( 1 )
;
}
if ( (qword_70918 & 0x2000000000000000LL) == 0 )
{
if ( v29 >= 10 && (j & 1) != 0 )
goto LABEL_101;
while ( 1 )
{
sub_3B350(env, 0x111E5u, (const char *)&unk_59396);
v29 = g_opaque_predicate_a;
j = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
if ( g_opaque_predicate_a < 10 || (j & 1) == 0 )
break;
LABEL_101:
sub_3B350(env, 0x111E5u, (const char *)&unk_59396);
}
}
}
else
{
sub_3B350(env, 0x1117Eu, (const char *)&unk_59396);
v29 = g_opaque_predicate_a;
for ( j = *((_DWORD *)v7 + 773); g_opaque_predicate_a >= 10 && (((j - 1) * j) & 1) != 0; j = *((_DWORD *)v7 + 773) )
{
sub_3B350(env, 0x1117Eu, (const char *)&unk_59396);
sub_3B350(env, 0x1117Eu, (const char *)&unk_59396);
v29 = g_opaque_predicate_a;
}
LOBYTE(j) = (j - 1) * j;
if ( v94 != 10411 )
{
v14 = 0;
goto LABEL_225;
}
}
if ( v29 >= 10 && (j & 1) != 0 )
goto LABEL_81;
while ( 1 )
{
zt_table_manager_singleton = get_zt_table_manager_singleton();
v35 = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
if ( g_opaque_predicate_a < 0 || (v35 & 0x80000000) == 0 )
break;
LABEL_81:
get_zt_table_manager_singleton();
}
if ( !*(_QWORD *)(zt_table_manager_singleton[1] + 8) )
{
if ( g_opaque_predicate_a >= 10 && (v35 & 1) != 0 )
goto LABEL_104;
while ( 1 )
{
sub_3B350(env, 0x11181u, (const char *)&unk_59396);
v29 = g_opaque_predicate_a;
v14 = 0;
j = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
if ( g_opaque_predicate_a < 0 || (j & 0x80000000) == 0 )
break;
LABEL_104:
sub_3B350(env, 0x11181u, (const char *)&unk_59396);
}
goto LABEL_225;
}
while ( 1 )
{
v36 = (*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)env + 1384LL))(env, args, 0);
v37 = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)env + 1824LL))(env);
v38 = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
if ( g_opaque_predicate_a < 0 || (v38 & 0x80000000) == 0 )
break;
(*(void (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)env + 1384LL))(env, args, 0);
(*(void (__fastcall **)(__int64))(*(_QWORD *)env + 1824LL))(env);
}
if ( !v36 || v37 == 1 )
{
while ( 1 )
{
(*(void (__fastcall **)(__int64))(*(_QWORD *)env + 136LL))(env);
sub_3B350(env, 0x11182u, (const char *)&unk_59396);
v39 = g_opaque_predicate_a;
v40 = (((unsigned __int8)*((_DWORD *)v7 + 773) - 1) * (unsigned __int8)*((_DWORD *)v7 + 773)) & 1;
if ( g_opaque_predicate_a < 10
|| ((((unsigned __int8)*((_DWORD *)v7 + 773) - 1) * (unsigned __int8)*((_DWORD *)v7 + 773)) & 1) == 0 )
{
break;
}
(*(void (__fastcall **)(__int64))(*(_QWORD *)env + 136LL))(env);
sub_3B350(env, 0x11182u, (const char *)&unk_59396);
}
v14 = 0;
v41 = v36;
LABEL_217:
if ( v39 >= 10 && v40 && v41 )
{
(*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 184LL))(env, v41);
v41 = 0;
}
do
{
if ( v41 )
(*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 184LL))(env, v41);
v29 = g_opaque_predicate_a;
j = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
if ( g_opaque_predicate_a < 10 )
break;
v41 = 0;
}
while ( (j & 1) != 0 );
LABEL_225:
v9 = v7;
if ( v29 < 10 )
{
v77 = v96;
v76 = v96;
v79 = v96;
if ( !v20 )
goto LABEL_235;
goto LABEL_234;
}
v75 = v96;
v76 = v96;
v77 = v96;
v78 = v96;
v79 = v96;
if ( (j & 1) == 0 )
goto LABEL_233;
if ( v20 )
{
LABEL_228:
(*(void (__fastcall **)(__int64, __int64, char *))(*(_QWORD *)env + 1360LL))(env, v79, v20);
goto LABEL_229;
}
while ( 1 )
{
LABEL_229:
if ( v75 )
(*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 184LL))(env, v78);
if ( v18 )
{
(*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 184LL))(env, v18);
v76 = 0;
v77 = 0;
v18 = 0;
if ( v20 )
goto LABEL_234;
}
else
{
v76 = 0;
v77 = 0;
LABEL_233:
if ( v20 )
LABEL_234:
(*(void (__fastcall **)(__int64, __int64, char *))(*(_QWORD *)env + 1360LL))(env, v79, v20);
}
LABEL_235:
if ( v77 )
(*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 184LL))(env, v76);
if ( v18 )
(*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 184LL))(env, v18);
if ( g_opaque_predicate_a < 0 || (((*((_DWORD *)v9 + 773) - 1) * *((_DWORD *)v9 + 773)) & 0x80000000) == 0 )
goto LABEL_45;
v78 = 0;
v75 = 0;
v18 = 0;
if ( v20 )
goto LABEL_228;
}
}
if ( g_opaque_predicate_a >= 10 && (v38 & 1) != 0 )
goto LABEL_112;
while ( 1 )
{
gettimeofday(&tv, 0);
v42 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 1);
if ( v36 == v42 )
{
v43 = v36;
}
else
{
v43 = v42;
if ( v36 )
(*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 184LL))(env, v36);
}
std_string_from_cstr(v97, (char *)&unk_59396);
v44 = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
if ( g_opaque_predicate_a < 10 || (v44 & 1) == 0 )
break;
v36 = v43;
LABEL_112:
gettimeofday(&tv, 0);
v45 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 1);
if ( v36 == v45 )
{
v46 = v36;
}
else
{
v46 = v45;
if ( v36 )
(*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 184LL))(env, v36);
}
std_string_from_cstr(v97, (char *)&unk_59396);
v36 = v46;
}
if ( !v43 )
{
if ( g_opaque_predicate_a >= 0 && v44 < 0 )
goto LABEL_255;
while ( 1 )
{
sub_3B350(env, 0x11185u, (const char *)&unk_59396);
v64 = g_opaque_predicate_a;
v41 = 0;
v14 = 0;
v65 = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
if ( g_opaque_predicate_a < 10 || (v65 & 1) == 0 )
break;
LABEL_255:
sub_3B350(env, 0x11185u, (const char *)&unk_59396);
}
goto LABEL_208;
}
v95 = v43;
if ( g_opaque_predicate_a >= 0 && v44 < 0 )
goto LABEL_124;
while ( 1 )
{
v47 = (char *)(*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)env + 1352LL))(env, v95, 0);
v88 = strlen(v47);
std::string::assign((int)v97, v47, v88);
v48 = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
if ( g_opaque_predicate_a < 10 || (v48 & 1) == 0 )
break;
LABEL_124:
v49 = (char *)(*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)env + 1352LL))(env, v95, 0);
v89 = strlen(v49);
std::string::assign((int)v97, v49, v89);
}
if ( !*v47 )
{
if ( g_opaque_predicate_a >= 0 && v48 < 0 )
goto LABEL_181;
while ( 1 )
{
sub_3B350(env, 0x11184u, (const char *)&unk_59396);
v60 = g_opaque_predicate_a;
v63 = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
if ( g_opaque_predicate_a < 0 || (v63 & 0x80000000) == 0 )
break;
LABEL_181:
sub_3B350(env, 0x11184u, (const char *)&unk_59396);
}
v62 = v63 & 1;
v92 = 1;
goto LABEL_183;
}
if ( g_opaque_predicate_a >= 0 && v48 < 0 )
goto LABEL_152;
while ( 1 )
{
v103 = 11;
v102 = 32514;
sub_3C54C();
v51 = v50;
get_zt_table_manager_singleton();
sub_2CCFC();
if ( (v97[0] & 1) != 0 )
v52 = v97[1];
else
v52 = (unsigned __int64)LOBYTE(v97[0]) >> 1;
if ( (tv.tv_sec & 1) != 0 )
tv_usec = tv.tv_usec;
else
tv_usec = (unsigned __int64)LOBYTE(tv.tv_sec) >> 1;
if ( v52 >= tv_usec )
v54 = tv_usec;
else
v54 = v52;
if ( !v54
|| ((tv.tv_sec & 1) != 0 ? (v55 = v105) : (v55 = (char *)&tv.tv_sec + 1),
(v97[0] & 1) != 0 ? (v56 = (char *)v98) : (v56 = (char *)v97 + 1),
(v57 = memcmp(v55, v56, v54)) == 0) )
{
if ( tv_usec < v52 )
v57 = -1;
else
v57 = v52 < tv_usec;
}
v58 = g_opaque_predicate_a;
v59 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
if ( g_opaque_predicate_a < 0 || (v59 & 0x80000000) == 0 )
break;
LABEL_152:
v103 = 11;
v102 = 32514;
sub_3C54C();
get_zt_table_manager_singleton();
sub_2CCFC();
}
if ( v57 )
{
sub_3B350(env, 0x11183u, (const char *)&unk_59396);
v58 = g_opaque_predicate_a;
v59 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
v92 = 1;
if ( (g_opaque_predicate_a & 0x80000000) == 0 )
{
v7 = &byte_70000;
do
{
if ( (v59 & 0x80000000) == 0 )
break;
sub_3B350(env, 0x11183u, (const char *)&unk_59396);
sub_3B350(env, 0x11183u, (const char *)&unk_59396);
v58 = g_opaque_predicate_a;
v59 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
v92 = 1;
}
while ( (g_opaque_predicate_a & 0x80000000) == 0 );
goto LABEL_161;
}
}
else
{
v92 = 0;
}
v7 = &byte_70000;
LABEL_161:
if ( v58 >= 10 && (v59 & 1) != 0 )
goto LABEL_170;
while ( 1 )
{
if ( (tv.tv_sec & 1) != 0 )
operator delete(v105);
if ( v51 )
sub_3C614();
v60 = g_opaque_predicate_a;
v61 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
v62 = v61 & 1;
if ( g_opaque_predicate_a < 10 || (v61 & 1) == 0 )
break;
v51 = 0;
LABEL_170:
if ( (tv.tv_sec & 1) != 0 )
operator delete(v105);
if ( v51 )
sub_3C614();
v51 = 0;
}
if ( !v92 )
{
v92 = 0;
if ( (g_opaque_predicate_a & 0x80000000) == 0 && v61 < 0 )
{
while ( 1 )
;
}
}
LABEL_183:
if ( v60 >= 10 && v62 )
goto LABEL_189;
LABEL_185:
if ( v47 )
(*(void (__fastcall **)(__int64, __int64, char *))(*(_QWORD *)env + 1360LL))(env, v95, v47);
while ( 1 )
{
v64 = g_opaque_predicate_a;
v65 = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
if ( g_opaque_predicate_a < 0 || (v65 & 0x80000000) == 0 )
break;
LABEL_189:
if ( v47 )
{
(*(void (__fastcall **)(__int64, __int64, char *))(*(_QWORD *)env + 1360LL))(env, v95, v47);
goto LABEL_185;
}
}
if ( !v92 )
{
v66 = v95;
while ( 1 )
{
gettimeofday(&tv, 0);
v69 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 2);
if ( v66 == v69 )
{
v70 = v66;
}
else
{
v70 = v69;
if ( v66 )
(*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 184LL))(env, v66);
}
v71 = g_opaque_predicate_a;
v72 = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
if ( g_opaque_predicate_a < 0 || (v72 & 0x80000000) == 0 )
break;
gettimeofday(&tv, 0);
v67 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 2);
v66 = v70;
if ( v70 != v67 )
{
v68 = v67;
v66 = v67;
if ( v70 )
{
(*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 184LL))(env, v70);
v66 = v68;
}
}
}
v73 = v72 & 1;
if ( v70 )
{
if ( g_opaque_predicate_a >= 10 && v73 )
goto LABEL_249;
while ( 1 )
{
sub_386DC(env, v70);
v71 = g_opaque_predicate_a;
v73 = (((unsigned __int8)*((_DWORD *)v7 + 773) - 1) * (unsigned __int8)*((_DWORD *)v7 + 773)) & 1;
if ( g_opaque_predicate_a < 10
|| ((((unsigned __int8)*((_DWORD *)v7 + 773) - 1) * (unsigned __int8)*((_DWORD *)v7 + 773)) & 1) == 0 )
{
break;
}
LABEL_249:
sub_386DC(env, v70);
}
}
if ( v71 >= 10 && v73 )
goto LABEL_263;
while ( 1 )
{
v80 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 3);
if ( v70 == v80 )
{
v81 = v70;
}
else
{
v81 = v80;
if ( v70 )
(*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 184LL))(env, v70);
}
v82 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 3);
sub_38474(env, v82);
v93 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 6);
sub_38474(env, v93);
v83 = get_zt_table_manager_singleton();
if ( g_opaque_predicate_a < 10 || (((*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773)) & 1) == 0 )
{
if ( v83[3] )
{
while ( 1 )
{
get_zt_table_manager_singleton();
if ( g_opaque_predicate_a < 0 || (((*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773)) & 0x80000000) == 0 )
break;
get_zt_table_manager_singleton();
}
}
JUMPOUT(0x42148);
}
v70 = v81;
LABEL_263:
v84 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 3);
if ( v70 == v84 )
{
v85 = v70;
}
else
{
v85 = v84;
if ( v70 )
(*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)env + 184LL))(env, v70);
}
v90 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 3);
sub_38474(env, v90);
v91 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)env + 1384LL))(env, args, 6);
sub_38474(env, v91);
get_zt_table_manager_singleton();
v70 = v85;
}
}
v41 = v95;
v14 = 0;
LABEL_208:
if ( (v64 & 0x80000000) == 0 && v65 < 0 )
goto LABEL_214;
while ( 1 )
{
if ( (v97[0] & 1) != 0 )
operator delete(v98);
v39 = g_opaque_predicate_a;
v74 = (*((_DWORD *)v7 + 773) - 1) * *((_DWORD *)v7 + 773);
if ( g_opaque_predicate_a < 0 || (v74 & 0x80000000) == 0 )
break;
LABEL_214:
if ( (v97[0] & 1) != 0 )
operator delete(v98);
}
v40 = v74 & 1;
goto LABEL_217;
}
v9 = &byte_70000;
while ( 1 )
{
nullsub_8(111);
WORD2(tv.tv_sec) = -656;
LODWORD(tv.tv_sec) = 839400965;
sub_3C54C();
v13 = v12;
sub_3B350(env, 0x11171u, v12);
if ( v13 )
sub_3C614();
v14 = 0;
if ( g_opaque_predicate_a < 0 || (((g_opaque_predicate_b - 1) * g_opaque_predicate_b) & 0x80000000) == 0 )
break;
nullsub_8(111);
WORD2(tv.tv_sec) = -656;
LODWORD(tv.tv_sec) = 839400965;
sub_3C54C();
v11 = v10;
sub_3B350(env, 0x11171u, v10);
if ( v11 )
sub_3C614();
}
LABEL_45:
sub_DCE8(&v99);
while ( (g_opaque_predicate_a & 0x80000000) == 0 && (*((_DWORD *)v9 + 773) - 1) * *((_DWORD *)v9 + 773) < 0 )
{
sub_DCE8(&v99);
sub_DCE8(&v99);
}
return v14;
}
核心 command worker:ida_sg_command_dispatch_body.c
- 文件:
output/ida_sg_command_dispatch_body.c - 行数:
1187 - 字节:
39388 - SHA256:
dc049f909f85d4584f3834406bc1fe7152ce2661b781a621957bfd40b5b43130
为什么看它:这是恢复 __NS_sig3 的核心伪代码。完整分支里能看到 case 10417、case 10418 和 default 下 10405。
读完得到的结论:Python 的三种模式直接对应这里的三个 native command:sig3_10405()、sig3_10417()、sig3_10418()。
// Central command worker reached through trampoline. Dispatches 10405/10417/10418 and crypto command families.
void __usercall sg_command_dispatch_body(int a2@<W2>, unsigned __int8 *a3@<X8>, ...)
{
int v4; // w11
char *v5; // x22
__int64 *stack; // x8
__int64 v7; // x24
__int64 *v8; // x19
__int64 v9; // x0
__int64 v10; // x26
__int64 v11; // x0
__int64 v12; // x20
void *v13; // x21
int v14; // w8
int i; // w9
int v16; // w10
signed __int64 v17; // x8
void **v18; // x0
char *v19; // x19
char *v20; // x0
char *v21; // x23
void *v22; // x23
char *v23; // x26
char v24; // w28
signed __int64 v25; // x8
void **v26; // x27
void **v27; // x19
char *v28; // x22
void **v29; // x0
char *v30; // x0
char *v31; // x25
int v32; // w8
int v33; // w9
unsigned int v34; // w27
void **v35; // x1
void **v36; // x1
int v37; // w8
int v38; // w9
unsigned __int64 v39; // x10
unsigned __int64 v40; // x10
int v41; // w22
unsigned __int64 v42; // x11
int v43; // w10
unsigned __int64 v44; // x11
int v45; // w24
__int64 v46; // x0
__int64 v47; // x24
__int64 v48; // x0
__int64 v49; // x24
unsigned int v50; // w8
int v51; // w8
unsigned int v52; // w9
int v53; // w9
int v54; // w9
__int64 v55; // x1
__int64 v56; // x1
char v57; // w0
int v58; // w8
__int64 v59; // x0
__int64 v60; // x22
int v61; // w9
char v62; // w9
int v63; // w8
char v64; // w9
int v65; // w9
int v66; // w8
int v67; // w9
int v68; // w9
char v69; // w0
int v70; // w8
char v71; // w9
int v72; // w8
char v73; // w9
char v74; // w9
int v75; // w8
char v76; // w9
__int64 v77; // x1
char v78; // w0
int v79; // w9
__int64 v80; // x1
_BOOL4 v81; // w8
int v82; // w9
int v83; // w8
int v84; // w9
__int64 v85; // x0
void *v86; // x23
int v87; // w8
char *v88; // x0
void *v89; // x0
int v90; // w8
int v91; // w8
int v92; // w9
__int128 v93; // q0
__int128 v94; // q0
int v95; // w9
__int64 v96; // x0
__int64 v97; // x22
__int64 v98; // x0
__int64 v99; // x22
void **v100; // x1
__int128 v101; // q0
int v102; // w9
char *v103; // x1
_BYTE *v104; // x8
__int128 v105; // q0
unsigned __int8 *v106; // x9
unsigned __int64 v107; // x8
int v108; // t1
char v109; // w8
unsigned __int8 *v110; // x19
__int64 *v111; // x0
void **v112; // x2
__int64 *zt_table_manager_singleton; // x0
char *v114; // x2
int v115; // w8
int v116; // w9
int v117; // w23
__int64 v118; // x0
__int64 v119; // x0
int v120; // w8
int v121; // w9
unsigned __int64 v122; // x10
_BYTE *v123; // x10
__int128 v124; // q0
_BYTE *v125; // x10
__int128 v126; // q0
size_t n; // [xsp+D8h] [xbp-E8h] BYREF
__int128 hex_string; // [xsp+E0h] [xbp-E0h] BYREF
void *v130; // [xsp+F0h] [xbp-D0h]
void *v131; // [xsp+F8h] [xbp-C8h] BYREF
unsigned __int64 v132; // [xsp+100h] [xbp-C0h]
void **v133; // [xsp+108h] [xbp-B8h]
__int128 v134; // [xsp+110h] [xbp-B0h] BYREF
_BYTE *v135; // [xsp+120h] [xbp-A0h]
gcc_va_list va; // [xsp+128h] [xbp-98h] BYREF
__int128 v137; // [xsp+148h] [xbp-78h] BYREF
void *v138; // [xsp+158h] [xbp-68h]
__int64 v139; // [xsp+160h] [xbp-60h]
v139 = *(_QWORD *)(_ReadStatusReg(TPIDR_EL0) + 40);
if ( g_opaque_predicate_a >= 10 && (((g_opaque_predicate_b - 1) * g_opaque_predicate_b) & 1) != 0 )
goto LABEL_5;
while ( 1 )
{
va_end(va);
va_start(va, a3);
v4 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
if ( g_opaque_predicate_a < 0 || (v4 & 0x80000000) == 0 )
{
if ( g_opaque_predicate_a >= 10 && (v4 & 1) != 0 )
va[0].__gr_offs = -32;
va[0].__gr_offs = -32;
if ( g_opaque_predicate_a >= 10 && (v4 & 1) != 0 )
{
while ( 1 )
;
}
v5 = (char *)*((_QWORD *)va[0].__gr_top - 5);
va[0].__gr_offs = -24;
if ( g_opaque_predicate_a >= 10 && (v4 & 1) != 0 )
{
while ( 1 )
;
}
va[0].__gr_offs = 0;
v34 = *((_BYTE *)va[0].__gr_top - 8) & 1;
stack = (__int64 *)va[0].__stack;
va[0].__stack = (char *)va[0].__stack + 8;
v7 = *stack;
v8 = (__int64 *)va[0].__stack;
va[0].__stack = (char *)va[0].__stack + 8;
while ( 1 )
{
v10 = *v8;
v135 = 0;
v134 = 0u;
sub_3C54C();
v12 = v11;
v13 = (void *)operator new(1u);
sub_1F404(v13, v34);
v14 = g_opaque_predicate_a;
i = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
v16 = i & 1;
if ( g_opaque_predicate_a < 10 || (i & 1) == 0 )
break;
v135 = 0;
v134 = 0u;
sub_3C54C();
v9 = operator new(1u);
sub_1F404(v9, v34);
}
switch ( a2 )
{
case 10411:
while ( 1 )
{
v22 = (void *)operator new(1u);
v23 = v5;
std_string_from_cstr((unsigned __int64 *)&v131, v5);
v24 = (char)v131;
v25 = v132;
v26 = v133;
if ( ((unsigned __int8)v131 & 1) != 0 )
v27 = v133;
else
v27 = (void **)((char *)&v131 + 1);
if ( ((unsigned __int8)v131 & 1) == 0 )
v25 = (unsigned __int64)(unsigned __int8)v131 >> 1;
v28 = (char *)v27 + v25;
if ( v25 >= 6 )
{
v29 = v27;
do
{
v30 = (char *)memchr(v29, 114, v25 - 5);
if ( !v30 )
break;
v31 = v30;
if ( !memcmp(v30, "result", 6u) )
goto LABEL_36;
v29 = (void **)(v31 + 1);
v25 = v28 - (v31 + 1);
}
while ( v25 > 5 );
}
v31 = v28;
LABEL_36:
v32 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
if ( g_opaque_predicate_a < 10 || (v32 & 1) == 0 )
break;
v5 = v23;
std_string_from_cstr((unsigned __int64 *)&v131, v23);
v17 = (unsigned __int64)(unsigned __int8)v131 >> 1;
if ( ((unsigned __int8)v131 & 1) != 0 )
{
v17 = v132;
v18 = v133;
}
else
{
v18 = (void **)((char *)&v131 + 1);
}
if ( v17 >= 6 )
{
v19 = (char *)v18 + v17;
do
{
v20 = (char *)memchr(v18, 114, v17 - 5);
if ( !v20 )
break;
v21 = v20;
if ( !memcmp(v20, "result", 6u) )
break;
v18 = (void **)(v21 + 1);
v17 = v19 - (v21 + 1);
}
while ( v17 >= 6 );
}
}
if ( v31 == v28 || v31 - (char *)v27 == -1 )
{
while ( 1 )
{
v36 = (v24 & 1) != 0 ? v26 : (void **)((char *)&v131 + 1);
sub_1AD30(&hex_string, v22, v36);
if ( (v134 & 1) != 0 )
{
*v135 = 0;
*((_QWORD *)&v134 + 1) = 0;
if ( (v134 & 1) != 0 )
{
operator delete(v135);
*(_QWORD *)&v134 = 0;
}
}
else
{
LOWORD(v134) = 0;
}
v37 = g_opaque_predicate_a;
v135 = v130;
v38 = (((_BYTE)g_opaque_predicate_b - 1) * (_BYTE)g_opaque_predicate_b) & 1;
v134 = hex_string;
if ( g_opaque_predicate_a < 10
|| ((((_BYTE)g_opaque_predicate_b - 1) * (_BYTE)g_opaque_predicate_b) & 1) == 0 )
{
break;
}
if ( ((unsigned __int8)v131 & 1) != 0 )
v35 = v133;
else
v35 = (void **)((char *)&v131 + 1);
sub_1AD30(&hex_string, v22, v35);
if ( (v134 & 1) != 0 )
{
*v135 = 0;
*((_QWORD *)&v134 + 1) = 0;
if ( (v134 & 1) != 0 )
{
operator delete(v135);
*(_QWORD *)&v134 = 0;
}
}
else
{
LOWORD(v134) = 0;
}
v24 = (char)v131;
v26 = v133;
v135 = v130;
v134 = hex_string;
}
}
else
{
v33 = v24 & 1;
if ( (g_opaque_predicate_a & 0x80000000) == 0 && v32 < 0 )
goto LABEL_268;
while ( 1 )
{
v100 = v33 ? v26 : (void **)((char *)&v131 + 1);
sub_1B3B8(&hex_string, v22, v100);
if ( (v134 & 1) != 0 )
{
*v135 = 0;
*((_QWORD *)&v134 + 1) = 0;
if ( (v134 & 1) != 0 )
{
operator delete(v135);
*(_QWORD *)&v134 = 0;
}
}
else
{
LOWORD(v134) = 0;
}
v101 = hex_string;
v37 = g_opaque_predicate_a;
v135 = v130;
v102 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
hex_string = 0u;
v130 = 0;
v134 = v101;
if ( g_opaque_predicate_a < 0 || (v102 & 0x80000000) == 0 )
break;
v24 = (char)v131;
v26 = v133;
LABEL_268:
if ( (v24 & 1) != 0 )
v103 = (char *)v26;
else
v103 = (char *)&v131 + 1;
sub_1B3B8(&hex_string, v22, v103);
if ( (v134 & 1) != 0 )
{
*v135 = 0;
*((_QWORD *)&v134 + 1) = 0;
if ( (v134 & 1) != 0 )
{
operator delete(v135);
*(_QWORD *)&v134 = 0;
}
}
else
{
LOWORD(v134) = 0;
}
v104 = v130;
v105 = hex_string;
v26 = v133;
hex_string = 0u;
v130 = 0;
v135 = v104;
v134 = v105;
v33 = (unsigned __int8)v131 & 1;
}
v38 = v102 & 1;
}
if ( v37 >= 10 && v38 )
goto LABEL_286;
while ( 1 )
{
if ( ((unsigned __int8)v131 & 1) != 0 )
operator delete(v133);
if ( v22 )
operator delete(v22);
v14 = g_opaque_predicate_a;
i = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
v16 = i & 1;
if ( g_opaque_predicate_a < 10 || (i & 1) == 0 )
break;
v22 = 0;
LABEL_286:
if ( ((unsigned __int8)v131 & 1) != 0 )
operator delete(v133);
if ( v22 )
{
operator delete(v22);
v22 = 0;
}
}
goto LABEL_378;
case 10412:
case 10414:
case 10415:
case 10416:
goto LABEL_378;
case 10413:
if ( g_opaque_predicate_a < 0 || (i & 0x80000000) == 0 )
goto LABEL_290;
do
{
std_string_from_cstr((unsigned __int64 *)&v131, v5);
LABEL_290:
std_string_from_cstr((unsigned __int64 *)&v131, v5);
v106 = (unsigned __int8 *)v133;
if ( ((unsigned __int8)v131 & 1) != 0 )
v107 = v132;
else
v107 = (unsigned __int64)(unsigned __int8)v131 >> 1;
if ( ((unsigned __int8)v131 & 1) == 0 )
v106 = (unsigned __int8 *)&v131 + 1;
if ( v107 )
{
while ( 1 )
{
v108 = *v106++;
if ( (unsigned int)(v108 - 48) >= 0xA )
break;
if ( !--v107 )
goto LABEL_298;
}
v109 = 0;
}
else
{
LABEL_298:
v109 = 1;
}
}
while ( (g_opaque_predicate_a & 0x80000000) == 0 && (g_opaque_predicate_b - 1) * g_opaque_predicate_b < 0 );
if ( (v109 & 1) != 0 )
{
sub_DBD4((int)&v131, 0, "p", 1u);
v130 = 0;
for ( hex_string = 0u;
(g_opaque_predicate_a & 0x80000000) == 0 && (g_opaque_predicate_b - 1) * g_opaque_predicate_b < 0;
hex_string = 0u )
{
sub_DBD4((int)&v131, 0, "p", 1u);
sub_DBD4((int)&v131, 0, "p", 1u);
v130 = 0;
}
if ( (v34 & 1) != 0 )
{
v110 = a3;
while ( 1 )
{
zt_table_manager_singleton = get_zt_table_manager_singleton();
v114 = ((unsigned __int8)v131 & 1) != 0 ? (char *)v133 : (char *)&v131 + 1;
sub_2B248(&v137, zt_table_manager_singleton, 3, v114, v7, v10);
if ( (hex_string & 1) != 0 )
{
*(_BYTE *)v130 = 0;
*((_QWORD *)&hex_string + 1) = 0;
if ( (hex_string & 1) != 0 )
{
operator delete(v130);
*(_QWORD *)&hex_string = 0;
}
}
else
{
LOWORD(hex_string) = 0;
}
v115 = g_opaque_predicate_a;
v130 = v138;
v116 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
hex_string = v137;
if ( g_opaque_predicate_a < 10 || (v116 & 1) == 0 )
break;
v111 = get_zt_table_manager_singleton();
if ( ((unsigned __int8)v131 & 1) != 0 )
v112 = v133;
else
v112 = (void **)((char *)&v131 + 1);
sub_2B248(&v137, v111, 3, v112, v7, v10);
if ( (hex_string & 1) != 0 )
{
*(_BYTE *)v130 = 0;
*((_QWORD *)&hex_string + 1) = 0;
if ( (hex_string & 1) != 0 )
{
operator delete(v130);
*(_QWORD *)&hex_string = 0;
}
}
else
{
LOWORD(hex_string) = 0;
}
v130 = v138;
hex_string = v137;
}
}
else
{
v110 = a3;
while ( 1 )
{
get_zt_table_manager_singleton();
sub_2CCFC();
if ( (hex_string & 1) != 0 )
{
*(_BYTE *)v130 = 0;
*((_QWORD *)&hex_string + 1) = 0;
if ( (hex_string & 1) != 0 )
{
operator delete(v130);
*(_QWORD *)&hex_string = 0;
}
}
else
{
LOWORD(hex_string) = 0;
}
v115 = g_opaque_predicate_a;
v130 = v138;
v116 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
hex_string = v137;
if ( g_opaque_predicate_a < 10 || (v116 & 1) == 0 )
break;
get_zt_table_manager_singleton();
sub_2CCFC();
if ( (hex_string & 1) != 0 )
{
*(_BYTE *)v130 = 0;
*((_QWORD *)&hex_string + 1) = 0;
if ( (hex_string & 1) != 0 )
{
operator delete(v130);
*(_QWORD *)&hex_string = 0;
}
}
else
{
LOWORD(hex_string) = 0;
}
v130 = v138;
hex_string = v137;
}
}
if ( v115 >= 0 && v116 < 0 )
{
while ( 1 )
;
}
if ( (hex_string & 1) != 0 )
v122 = *((_QWORD *)&hex_string + 1);
else
v122 = (unsigned __int64)(unsigned __int8)hex_string >> 1;
if ( v122 )
{
fun_memcpy((Poco::Util::Option *)&v134, (const std::string *)&hex_string);
v115 = g_opaque_predicate_a;
v117 = 2;
v116 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
if ( (g_opaque_predicate_a & 0x80000000) == 0 && v116 < 0 )
{
v117 = 2;
do
{
fun_memcpy((Poco::Util::Option *)&v134, (const std::string *)&hex_string);
fun_memcpy((Poco::Util::Option *)&v134, (const std::string *)&hex_string);
v115 = g_opaque_predicate_a;
v116 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
}
while ( (g_opaque_predicate_a & 0x80000000) == 0 && v116 < 0 );
}
}
else
{
v117 = 1;
*(_QWORD *)v110 = 0;
*((_QWORD *)v110 + 1) = 0;
*((_QWORD *)v110 + 2) = 0;
}
if ( v115 >= 10 && (v116 & 1) != 0 )
goto LABEL_367;
while ( 1 )
{
if ( (hex_string & 1) != 0 )
operator delete(v130);
v120 = g_opaque_predicate_a;
v121 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
if ( g_opaque_predicate_a < 10 || (v121 & 1) == 0 )
break;
LABEL_367:
if ( (hex_string & 1) != 0 )
operator delete(v130);
}
}
else
{
v117 = 1;
while ( 1 )
{
sub_3C54C();
*((_QWORD *)a3 + 1) = 0;
*((_QWORD *)a3 + 2) = 0;
*(_QWORD *)a3 = 0;
if ( v119 )
sub_3C614();
v120 = g_opaque_predicate_a;
v121 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
if ( g_opaque_predicate_a < 0 || (v121 & 0x80000000) == 0 )
break;
sub_3C54C();
*((_QWORD *)a3 + 1) = 0;
*((_QWORD *)a3 + 2) = 0;
*(_QWORD *)a3 = 0;
if ( v118 )
sub_3C614();
}
}
if ( (v120 & 0x80000000) == 0 && v121 < 0 )
goto LABEL_375;
while ( 1 )
{
if ( ((unsigned __int8)v131 & 1) != 0 )
operator delete(v133);
v14 = g_opaque_predicate_a;
i = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
v16 = i & 1;
if ( g_opaque_predicate_a < 10 || (i & 1) == 0 )
break;
LABEL_375:
if ( ((unsigned __int8)v131 & 1) != 0 )
operator delete(v133);
}
if ( v117 == 2 )
goto LABEL_378;
goto LABEL_383;
case 10417:
if ( g_opaque_predicate_a < 0 || (i & 0x80000000) == 0 )
goto LABEL_117;
do
{
do
v52 = __ldaxr((unsigned int *)&dword_70430);
while ( __stlxr(v52 + 1, (unsigned int *)&dword_70430) );
do
LABEL_117:
v50 = __ldaxr((unsigned int *)&dword_70430);
while ( __stlxr(v50 + 1, (unsigned int *)&dword_70430) );
v51 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
}
while ( g_opaque_predicate_a >= 10 && (v51 & 1) != 0 );
if ( !v5 )
goto LABEL_127;
if ( (g_opaque_predicate_a & 0x80000000) == 0 && v51 < 0 )
{
while ( 1 )
;
}
if ( *v5 )
{
while ( 1 )
{
std_string_from_cstr((unsigned __int64 *)&hex_string, v5);
hex_string_to_bytes_vector(&hex_string, v56);
if ( (hex_string & 1) != 0 )
operator delete(v130);
v137 = 0u;
sub_203F4(&v137, v131);
v57 = sub_203FC(&v137);
v58 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
if ( g_opaque_predicate_a < 0 || (v58 & 0x80000000) == 0 )
break;
std_string_from_cstr((unsigned __int64 *)&hex_string, v5);
hex_string_to_bytes_vector(&hex_string, v55);
if ( (hex_string & 1) != 0 )
operator delete(v130);
v137 = 0u;
sub_203F4(&v137, v131);
sub_203FC(&v137);
}
if ( (v57 & 1) != 0 )
{
while ( 1 )
{
v59 = sub_C0C4(&v137);
v60 = v59;
v61 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
if ( g_opaque_predicate_a < 10 || (v61 & 1) == 0 )
break;
sub_C0C4(&v137);
}
if ( v59 )
{
if ( g_opaque_predicate_a < 0 || v61 >= 0 )
goto LABEL_165;
do
{
(*(void (__fastcall **)(__int64))(*(_QWORD *)v60 + 16LL))(v60);
LABEL_165:
v69 = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)v60 + 16LL))(v60);
v70 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
}
while ( (g_opaque_predicate_a & 0x80000000) == 0 && v70 < 0 );
if ( (v69 & 1) != 0 )
{
if ( g_opaque_predicate_a >= 10 && (v70 & 1) != 0 )
goto LABEL_179;
while ( 1 )
{
v77 = atomic_load((unsigned int *)&dword_70430);
v78 = sub_1FB44(v60, v77);
v79 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
if ( g_opaque_predicate_a < 0 || (v79 & 0x80000000) == 0 )
break;
LABEL_179:
v80 = atomic_load((unsigned int *)&dword_70430);
sub_1FB44(v60, v80);
}
v81 = g_opaque_predicate_a < 10 || (v79 & 1) == 0;
if ( (v78 & 1) != 0 )
{
if ( v81 )
goto LABEL_187;
do
{
n = 0;
sub_1FDAC(v60, &n);
LABEL_187:
n = 0;
v85 = sub_1FDAC(v60, &n);
v86 = (void *)v85;
v87 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
}
while ( (g_opaque_predicate_a & 0x80000000) == 0 && v87 < 0 );
if ( v85 )
{
while ( 1 )
{
v130 = 0;
hex_string = 0u;
std::string::resize(&hex_string, n, 0);
v89 = (hex_string & 1) != 0 ? v130 : (char *)&hex_string + 1;
memcpy(v89, v86, n);
v90 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
if ( g_opaque_predicate_a < 0 || (v90 & 0x80000000) == 0 )
break;
v130 = 0;
hex_string = 0u;
std::string::resize(&hex_string, n, 0);
if ( (hex_string & 1) != 0 )
v88 = (char *)v130;
else
v88 = (char *)&hex_string + 1;
memcpy(v88, v86, n);
}
if ( g_opaque_predicate_a >= 10 && (v90 & 1) != 0 )
goto LABEL_209;
while ( 1 )
{
operator delete(v86);
v91 = g_opaque_predicate_a;
v92 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
if ( g_opaque_predicate_a < 10 || (v92 & 1) == 0 )
break;
LABEL_209:
operator delete(v86);
}
if ( (g_opaque_predicate_a & 0x80000000) == 0 && v92 < 0 )
goto LABEL_214;
while ( 1 )
{
v93 = hex_string;
*((_QWORD *)a3 + 2) = v130;
*(_OWORD *)a3 = v93;
if ( v91 < 0 || (v92 & 0x80000000) == 0 )
break;
LABEL_214:
v94 = hex_string;
*((_QWORD *)a3 + 2) = v130;
*(_OWORD *)a3 = v94;
}
}
else
{
if ( g_opaque_predicate_a >= 10 && (v87 & 1) != 0 )
goto LABEL_217;
while ( 1 )
{
sub_3B7B4(10002205, &unk_59396);
v95 = g_opaque_predicate_b;
v91 = g_opaque_predicate_a;
*((_QWORD *)a3 + 1) = 0;
*((_QWORD *)a3 + 2) = 0;
*(_QWORD *)a3 = 0;
v92 = (v95 - 1) * v95;
if ( v91 < 0 || (v92 & 0x80000000) == 0 )
break;
LABEL_217:
sub_3B7B4(10002205, &unk_59396);
*((_QWORD *)a3 + 1) = 0;
*((_QWORD *)a3 + 2) = 0;
*(_QWORD *)a3 = 0;
}
}
if ( v91 >= 10 && (v92 & 1) != 0 )
{
while ( 1 )
;
}
}
else
{
if ( !v81 )
goto LABEL_186;
while ( 1 )
{
sub_3B7B4(10002204, &unk_59396);
v82 = g_opaque_predicate_b;
v83 = g_opaque_predicate_a;
*((_QWORD *)a3 + 1) = 0;
*((_QWORD *)a3 + 2) = 0;
*(_QWORD *)a3 = 0;
v84 = (v82 - 1) * v82;
if ( v83 < 0 || (v84 & 0x80000000) == 0 )
break;
LABEL_186:
sub_3B7B4(10002204, &unk_59396);
*((_QWORD *)a3 + 1) = 0;
*((_QWORD *)a3 + 2) = 0;
*(_QWORD *)a3 = 0;
}
}
}
else
{
sub_3B7B4(10002203, &unk_59396);
v71 = g_opaque_predicate_b;
v72 = g_opaque_predicate_a;
*((_QWORD *)a3 + 1) = 0;
*((_QWORD *)a3 + 2) = 0;
*(_QWORD *)a3 = 0;
v73 = (v71 - 1) * v71;
if ( v72 >= 10 && (v73 & 1) != 0 )
{
do
{
sub_3B7B4(10002203, &unk_59396);
*((_QWORD *)a3 + 1) = 0;
*((_QWORD *)a3 + 2) = 0;
*(_QWORD *)a3 = 0;
sub_3B7B4(10002203, &unk_59396);
v74 = g_opaque_predicate_b;
v75 = g_opaque_predicate_a;
*((_QWORD *)a3 + 1) = 0;
*((_QWORD *)a3 + 2) = 0;
*(_QWORD *)a3 = 0;
v76 = (v74 - 1) * v74;
}
while ( v75 >= 10 && (v76 & 1) != 0 );
}
}
}
else
{
if ( g_opaque_predicate_a >= 0 && v61 < 0 )
goto LABEL_152;
while ( 1 )
{
sub_3B7B4(10002202, &unk_59396);
v62 = g_opaque_predicate_b;
v63 = g_opaque_predicate_a;
*((_QWORD *)a3 + 1) = 0;
*((_QWORD *)a3 + 2) = 0;
*(_QWORD *)a3 = 0;
v64 = (v62 - 1) * v62;
if ( v63 < 10 || (v64 & 1) == 0 )
break;
LABEL_152:
sub_3B7B4(10002202, &unk_59396);
*((_QWORD *)a3 + 1) = 0;
*((_QWORD *)a3 + 2) = 0;
*(_QWORD *)a3 = 0;
}
}
while ( 1 )
{
if ( v60 )
(*(void (__fastcall **)(__int64))(*(_QWORD *)v60 + 8LL))(v60);
v66 = g_opaque_predicate_a;
v68 = (((_BYTE)g_opaque_predicate_b - 1) * (_BYTE)g_opaque_predicate_b) & 1;
if ( g_opaque_predicate_a < 10
|| ((((_BYTE)g_opaque_predicate_b - 1) * (_BYTE)g_opaque_predicate_b) & 1) == 0 )
{
break;
}
v60 = 0;
}
}
else
{
if ( g_opaque_predicate_a >= 10 && (v58 & 1) != 0 )
goto LABEL_155;
while ( 1 )
{
sub_3B7B4(10002201, v5);
v65 = g_opaque_predicate_b;
v66 = g_opaque_predicate_a;
*((_QWORD *)a3 + 1) = 0;
*((_QWORD *)a3 + 2) = 0;
*(_QWORD *)a3 = 0;
v67 = (v65 - 1) * v65;
if ( v66 < 0 || (v67 & 0x80000000) == 0 )
break;
LABEL_155:
sub_3B7B4(10002201, v5);
*((_QWORD *)a3 + 1) = 0;
*((_QWORD *)a3 + 2) = 0;
*(_QWORD *)a3 = 0;
}
v68 = v67 & 1;
}
if ( v66 >= 10 && v68 )
goto LABEL_163;
while ( 1 )
{
if ( v131 )
{
v132 = (unsigned __int64)v131;
operator delete(v131);
}
v14 = g_opaque_predicate_a;
i = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
if ( g_opaque_predicate_a < 0 || (i & 0x80000000) == 0 )
break;
LABEL_163:
if ( v131 )
{
v132 = (unsigned __int64)v131;
operator delete(v131);
}
}
}
else
{
LABEL_127:
sub_3B7B4(10002200, &unk_59396);
v53 = g_opaque_predicate_b;
v14 = g_opaque_predicate_a;
*((_QWORD *)a3 + 1) = 0;
*((_QWORD *)a3 + 2) = 0;
*(_QWORD *)a3 = 0;
for ( i = (v53 - 1) * v53; v14 >= 10 && (i & 1) != 0; i = (v54 - 1) * v54 )
{
sub_3B7B4(10002200, &unk_59396);
*((_QWORD *)a3 + 1) = 0;
*((_QWORD *)a3 + 2) = 0;
*(_QWORD *)a3 = 0;
sub_3B7B4(10002200, &unk_59396);
v54 = g_opaque_predicate_b;
v14 = g_opaque_predicate_a;
*((_QWORD *)a3 + 1) = 0;
*((_QWORD *)a3 + 2) = 0;
*(_QWORD *)a3 = 0;
}
}
goto LABEL_383;
case 10418:
if ( (g_opaque_predicate_a & 0x80000000) == 0 && i < 0 )
{
while ( 1 )
;
}
if ( !v5 || !*v5 )
goto LABEL_378;
while ( 1 )
{
strlen(v5);
tramp_sign10418_hash();
v14 = g_opaque_predicate_a;
v39 = *a3;
i = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
if ( g_opaque_predicate_a < 10 || (i & 1) == 0 )
break;
strlen(v5);
tramp_sign10418_hash();
}
if ( (v39 & 1) != 0 )
v40 = *((_QWORD *)a3 + 1);
else
v40 = v39 >> 1;
if ( v40 )
{
v41 = 1;
}
else
{
if ( (g_opaque_predicate_a & 0x80000000) == 0 && i < 0 )
goto LABEL_231;
while ( 1 )
{
sub_3C54C();
v97 = v96;
sub_3B7B4(10001200, &unk_59396);
if ( v97 )
sub_3C614();
v14 = g_opaque_predicate_a;
v41 = 0;
i = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
if ( g_opaque_predicate_a < 10 || (i & 1) == 0 )
break;
LABEL_231:
sub_3C54C();
v99 = v98;
sub_3B7B4(10001200, &unk_59396);
if ( v99 )
sub_3C614();
}
}
v16 = i & 1;
while ( v14 >= 10 && (i & 1) != 0 )
;
if ( (v41 & 1) == 0 )
{
while ( 1 )
{
if ( (*a3 & 1) != 0 )
operator delete(*((void **)a3 + 2));
v14 = g_opaque_predicate_a;
i = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
if ( g_opaque_predicate_a < 0 || (i & 0x80000000) == 0 )
break;
if ( (*a3 & 1) != 0 )
operator delete(*((void **)a3 + 2));
}
v16 = i & 1;
}
while ( v14 >= 0 && i < 0 )
;
if ( !v41 )
{
LABEL_378:
if ( v14 >= 10 && v16 )
goto LABEL_382;
while ( 1 )
{
v123 = v135;
v124 = v134;
v134 = 0u;
v135 = 0;
*((_QWORD *)a3 + 2) = v123;
*(_OWORD *)a3 = v124;
if ( v14 < 0 || (i & 0x80000000) == 0 )
break;
LABEL_382:
v125 = v135;
v126 = v134;
v134 = 0u;
v135 = 0;
*((_QWORD *)a3 + 2) = v125;
*(_OWORD *)a3 = v126;
}
}
LABEL_383:
if ( (v14 & 0x80000000) == 0 && i < 0 )
{
if ( v13 )
operator delete(v13);
while ( 1 )
{
if ( v12 )
sub_3C614();
if ( (v134 & 1) != 0 )
operator delete(v135);
LABEL_395:
if ( (v134 & 1) != 0 )
operator delete(v135);
if ( g_opaque_predicate_a < 10 || (((g_opaque_predicate_b - 1) * g_opaque_predicate_b) & 1) == 0 )
JUMPOUT(0xC000);
v12 = 0;
}
}
operator delete(v13);
if ( v12 )
sub_3C614();
goto LABEL_395;
default:
if ( a2 != 10405 || !v5 )
goto LABEL_378;
if ( (g_opaque_predicate_a & 0x80000000) == 0 && i < 0 )
{
while ( 1 )
;
}
if ( !*v5 )
goto LABEL_378;
while ( 1 )
{
strlen(v5);
tramp_sig3_10405_sha256_hex();
v14 = g_opaque_predicate_a;
v42 = *a3;
i = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
v43 = i & 1;
if ( g_opaque_predicate_a < 10 || (i & 1) == 0 )
break;
strlen(v5);
tramp_sig3_10405_sha256_hex();
}
if ( (v42 & 1) != 0 )
v44 = *((_QWORD *)a3 + 1);
else
v44 = v42 >> 1;
if ( v44 )
{
v45 = 1;
if ( (g_opaque_predicate_a & 0x80000000) == 0 && i < 0 )
{
while ( 1 )
;
}
}
else
{
while ( 1 )
{
sub_3C54C();
v49 = v48;
sub_3B7B4(70024, &unk_59396);
if ( v49 )
sub_3C614();
v14 = g_opaque_predicate_a;
v45 = 0;
i = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
v43 = i & 1;
if ( g_opaque_predicate_a < 10 || (i & 1) == 0 )
break;
sub_3C54C();
v47 = v46;
sub_3B7B4(70024, &unk_59396);
if ( v47 )
sub_3C614();
}
}
if ( v14 >= 10 && v43 )
{
while ( 1 )
;
}
if ( (v45 & 1) != 0 )
goto LABEL_253;
if ( v14 < 0 || (i & 0x80000000) == 0 )
goto LABEL_247;
do
{
if ( (*a3 & 1) != 0 )
operator delete(*((void **)a3 + 2));
LABEL_247:
if ( (*a3 & 1) != 0 )
operator delete(*((void **)a3 + 2));
v14 = g_opaque_predicate_a;
i = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
}
while ( g_opaque_predicate_a >= 10 && (i & 1) != 0 );
LABEL_253:
if ( (v14 & 0x80000000) == 0 && i < 0 )
{
while ( 1 )
;
}
if ( v45 )
goto LABEL_383;
v16 = i & 1;
goto LABEL_378;
}
}
LABEL_5:
va_end(va);
va_start(va, a3);
}
}
SHA256 wrapper:ida_sha256_hash_wrapper_body.c
- 文件:
output/ida_sha256_hash_wrapper_body.c - 行数:
45 - 字节:
1093 - SHA256:
6f625e8b7949b3b4663325301ca3a03ee7a72f54f983b468c2028c6eca1725cc
为什么看它:10418 不是普通 Java SHA256,native 里还有 wrapper 和 table manager 路径。
读完得到的结论:Python 里用 kwsg_sha256_digest() 表达这个 native wrapper 行为。
void __fastcall sha256_hash_wrapper_body(_BYTE *a1)
{
int v1; // w10
int v2; // w8
int v3; // w9
v1 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
while ( g_opaque_predicate_a >= 0 && v1 < 0 )
;
if ( *a1 )
{
if ( g_opaque_predicate_a < 10 || (v1 & 1) == 0 )
goto LABEL_10;
do
{
tramp_sha256_core();
LABEL_10:
tramp_sha256_core();
v2 = g_opaque_predicate_a;
v3 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
}
while ( g_opaque_predicate_a >= 10 && (v3 & 1) != 0 );
}
else
{
while ( 1 )
{
get_zt_table_manager_singleton();
get_zt_table_manager_singleton();
tramp_sha256_core();
v2 = g_opaque_predicate_a;
v3 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
if ( g_opaque_predicate_a < 10 || (v3 & 1) == 0 )
break;
get_zt_table_manager_singleton();
get_zt_table_manager_singleton();
tramp_sha256_core();
}
}
if ( (v2 & 0x80000000) == 0 && v3 < 0 )
{
while ( 1 )
;
}
}
10418 hash helper:ida_sign10418_hash_body.c
- 文件:
output/ida_sign10418_hash_body.c - 行数:
334 - 字节:
9033 - SHA256:
e182199adf6c0549c3c59089e052e8429328c30fd643ed7baceee215d5c8dae3
为什么看它:它是 10418 分支进入 digest、padding、ZT transform 的关键。
读完得到的结论:Python sig3_10418() 的 digest -> PKCS#7 pad -> zt_table_transform -> hex 就来自这条链。
void __usercall sign10418_hash_body(__int64 a2@<X3>, __int64 a3@<X4>, struct timeval *a4@<X8>)
{
int v5; // w8
int v6; // w11
_BOOL4 v7; // w10
int v8; // w9
bool v9; // w11
char v10; // w0
int v11; // w10
unsigned __int8 *v12; // x20
int v13; // w22
char v14; // w0
int v15; // w8
int v16; // w9
int v17; // w8
int v18; // w9
void *v19; // x0
int v20; // w8
void *v21; // x20
int v22; // w9
char *v23; // x0
unsigned __int64 tv_usec; // x10
void *v25; // x8
struct timeval v26; // q0
struct timeval v27; // [xsp+8h] [xbp-98h] BYREF
void *v28; // [xsp+18h] [xbp-88h]
size_t v29; // [xsp+20h] [xbp-80h]
void *src; // [xsp+28h] [xbp-78h]
struct timeval tv; // [xsp+30h] [xbp-70h] BYREF
void *v32; // [xsp+40h] [xbp-60h]
__int64 v33; // [xsp+48h] [xbp-58h]
unsigned __int8 *v34; // [xsp+50h] [xbp-50h]
__int64 v35; // [xsp+58h] [xbp-48h]
v35 = *(_QWORD *)(_ReadStatusReg(TPIDR_EL0) + 40);
v5 = g_opaque_predicate_a;
v6 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
v7 = g_opaque_predicate_a < 10 || (v6 & 1) == 0;
v8 = v6 & 1;
v9 = g_opaque_predicate_a >= 0 && v6 < 0;
while ( v9 )
;
if ( a2 && a3 )
{
if ( v7 )
goto LABEL_8;
do
{
v33 = 0;
v34 = 0;
gettimeofday(&tv, 0);
tramp_sha256_digest_bytes();
LABEL_8:
v33 = 0;
v34 = 0;
gettimeofday(&tv, 0);
tramp_sha256_digest_bytes();
v5 = g_opaque_predicate_a;
v11 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
}
while ( (g_opaque_predicate_a & 0x80000000) == 0 && v11 < 0 );
if ( (v10 & 1) != 0 )
{
v13 = v33;
v12 = v34;
while ( 1 )
{
bytes_to_lower_hex_string(v12, v13);
v29 = 0;
src = 0;
gettimeofday(&v27, 0);
sub_1E07C();
v15 = g_opaque_predicate_a;
v16 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
if ( g_opaque_predicate_a < 0 || (v16 & 0x80000000) == 0 )
break;
bytes_to_lower_hex_string(v12, v13);
v29 = 0;
src = 0;
gettimeofday(&v27, 0);
sub_1E07C();
}
if ( (v14 & 1) != 0 )
{
if ( g_opaque_predicate_a < 10 || (v16 & 1) == 0 )
goto LABEL_47;
do
{
bytes_to_lower_hex_string((unsigned __int8 *)src, v29);
if ( (tv.tv_sec & 1) != 0 )
{
*(_BYTE *)v32 = 0;
tv.tv_usec = 0;
if ( (tv.tv_sec & 1) != 0 )
{
operator delete(v32);
tv.tv_sec = 0;
}
}
else
{
LOWORD(tv.tv_sec) = 0;
}
v32 = v28;
tv = v27;
gettimeofday(&v27, 0);
LABEL_47:
bytes_to_lower_hex_string((unsigned __int8 *)src, v29);
if ( (tv.tv_sec & 1) != 0 )
{
*(_BYTE *)v32 = 0;
tv.tv_usec = 0;
if ( (tv.tv_sec & 1) != 0 )
{
operator delete(v32);
tv.tv_sec = 0;
}
}
else
{
LOWORD(tv.tv_sec) = 0;
}
v32 = v28;
tv = v27;
gettimeofday(&v27, 0);
v17 = g_opaque_predicate_a;
v18 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
}
while ( g_opaque_predicate_a >= 10 && (v18 & 1) != 0 );
if ( v12 )
{
while ( 1 )
{
free(v12);
v17 = g_opaque_predicate_a;
v18 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
if ( g_opaque_predicate_a < 10 || (v18 & 1) == 0 )
break;
free(v12);
}
}
if ( (v17 & 0x80000000) == 0 && v18 < 0 )
goto LABEL_70;
while ( 1 )
{
v27.tv_usec = 0;
v28 = 0;
v27.tv_sec = 0;
std::string::resize(&v27, v29, 0);
v19 = (v27.tv_sec & 1) != 0 ? v28 : (char *)&v27.tv_sec + 1;
memcpy(v19, src, v29);
v20 = g_opaque_predicate_a;
v21 = src;
v22 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
if ( g_opaque_predicate_a < 0 || (v22 & 0x80000000) == 0 )
break;
LABEL_70:
v27.tv_usec = 0;
v28 = 0;
v27.tv_sec = 0;
std::string::resize(&v27, v29, 0);
if ( (v27.tv_sec & 1) != 0 )
v23 = (char *)v28;
else
v23 = (char *)&v27.tv_sec + 1;
memcpy(v23, src, v29);
}
if ( (v27.tv_sec & 1) != 0 )
tv_usec = v27.tv_usec;
else
tv_usec = (unsigned __int64)LOBYTE(v27.tv_sec) >> 1;
if ( tv_usec )
{
if ( g_opaque_predicate_a >= 10 && (v22 & 1) != 0 )
{
while ( 1 )
;
}
if ( src )
{
while ( 1 )
{
free(v21);
v20 = g_opaque_predicate_a;
v22 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
if ( g_opaque_predicate_a < 0 || (v22 & 0x80000000) == 0 )
break;
free(v21);
}
}
if ( v20 >= 10 && (v22 & 1) != 0 )
{
while ( 1 )
{
a4->tv_sec = 0;
a4->tv_usec = 0;
a4[1].tv_sec = 0;
v27.tv_usec = 0;
v28 = 0;
v27.tv_sec = 0;
}
}
v25 = v28;
v26 = v27;
v27.tv_sec = 0;
v27.tv_usec = 0;
v28 = 0;
a4[1].tv_sec = (__time_t)v25;
*a4 = v26;
}
else
{
if ( src )
{
if ( g_opaque_predicate_a >= 10 && (v22 & 1) != 0 )
goto LABEL_96;
while ( 1 )
{
free(v21);
v20 = g_opaque_predicate_a;
v22 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
if ( g_opaque_predicate_a < 10 || (v22 & 1) == 0 )
break;
LABEL_96:
free(v21);
}
}
if ( (v20 & 0x80000000) == 0 && v22 < 0 )
goto LABEL_101;
while ( 1 )
{
a4->tv_sec = 0;
a4->tv_usec = 0;
a4[1].tv_sec = 0;
if ( v20 < 10 || (v22 & 1) == 0 )
break;
LABEL_101:
a4->tv_sec = 0;
a4->tv_usec = 0;
a4[1].tv_sec = 0;
}
}
while ( 1 )
{
if ( (v27.tv_sec & 1) != 0 )
operator delete(v28);
if ( g_opaque_predicate_a < 10 || (((g_opaque_predicate_b - 1) * g_opaque_predicate_b) & 1) == 0 )
break;
if ( (v27.tv_sec & 1) != 0 )
operator delete(v28);
}
}
else
{
if ( g_opaque_predicate_a >= 10 && (v16 & 1) != 0 )
{
while ( 1 )
;
}
if ( v12 )
{
while ( 1 )
{
free(v12);
v15 = g_opaque_predicate_a;
v16 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
if ( g_opaque_predicate_a < 0 || (v16 & 0x80000000) == 0 )
break;
free(v12);
}
}
if ( v15 >= 10 && (v16 & 1) != 0 )
goto LABEL_33;
while ( 1 )
{
a4->tv_sec = 0;
a4->tv_usec = 0;
a4[1].tv_sec = 0;
if ( v15 < 0 || (v16 & 0x80000000) == 0 )
break;
LABEL_33:
a4->tv_sec = 0;
a4->tv_usec = 0;
a4[1].tv_sec = 0;
}
if ( v15 >= 10 && (v16 & 1) != 0 )
goto LABEL_35;
}
while ( 1 )
{
if ( (tv.tv_sec & 1) != 0 )
operator delete(v32);
v5 = g_opaque_predicate_a;
v11 = (g_opaque_predicate_b - 1) * g_opaque_predicate_b;
v8 = v11 & 1;
if ( g_opaque_predicate_a < 10 || (v11 & 1) == 0 )
break;
LABEL_35:
if ( (tv.tv_sec & 1) != 0 )
operator delete(v32);
}
LABEL_41:
while ( (v5 & 0x80000000) == 0 )
{
LABEL_42:
if ( (v11 & 0x80000000) == 0 )
break;
}
}
else
{
v8 = v11 & 1;
a4->tv_sec = 0;
a4->tv_usec = 0;
a4[1].tv_sec = 0;
if ( v5 < 10 || (v11 & 1) == 0 )
goto LABEL_41;
v8 = 1;
if ( (v5 & 0x80000000) == 0 )
goto LABEL_42;
}
}
else
{
a4->tv_sec = 0;
a4->tv_usec = 0;
a4[1].tv_sec = 0;
}
if ( v5 >= 10 && v8 )
{
while ( 1 )
;
}
}
IDA 到 Python 的映射理由
从 IDA 回到 Python,不是逐行翻译伪代码。混淆函数里大量循环是 opaque predicate,真正要保留的是语义:
| IDA 现象 | 语义 | Python 落点 |
|---|---|---|
case 10417 + atomic counter |
10417 有计数器/时间形态 | sig3_10417(data, table, now, counter) |
case 10418 + tramp_sign10418_hash() |
10418 走 native hash helper | sig3_10418(data, table) |
a2 != 10405 default 判断 |
老模式 10405 | sig3_10405(data, table, now) |
| table manager / transform helper | ZT 表参与输出变换 | KwaiZtTable / zt_table_transform() |
0x05 ZT、sig、sig3、DFP、Azeroth:算法层完整实现
到这里 Java 和 IDA 只告诉我们“应该怎么走”。真正能重复跑、能单测、能接 REST 的,是 Python 算法层。下面完整内联 kws_recovered/algorithms.py。
算法层完整实现:kws_recovered/algorithms.py
- 文件:
kws_recovered/algorithms.py - 行数:
871 - 字节:
31401 - SHA256:
800ad3eae0c395bb0664f3cdacd6a35e2205b0c61b50dbe42e5d010869304608
为什么看它:这里把 Java/IDA 证据落成可执行算法:kwai_sig、kwai_token_sig、sig3_10405/10417/10418、PNG/动态容器 ZT 表、DFP、Azeroth HMAC。
读完得到的结论:它是所有 REST wrapper 的底座,也是测试里最先固定的部分。
from __future__ import annotations
import base64
import binascii
import hashlib
import hmac
import json
import os
import random
import struct
import time
import urllib.parse
import zlib
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, Mapping, MutableMapping, Optional
APPKEY_MAIN = "7e46b28a-8c93-4940-8238-4c60e64e3c81"
APPKEY_AZEROTH = "010a11c6-f2cb-4016-887d-0d958aef1534"
KWE_N = "KWE_N"
DFP_SDK_VERSION = "8.9.1antman.40.05d13a7f"
DFP_SDK_PLATFORM = "AND"
GET_CLOCK_SALT = b"382700b563f4"
GET_MAGIC_VALUE = "46a8qpMw6643TDiV"
KWSG_STATIC_SHA256_KEY00 = bytes.fromhex(
"58525c4478457c47007000675d0e435740556c620300710464654554545362"
"676e006479537f52466e7763635b036e5e05745a7c427f616c40045f54414f6072"
)
KWSG_STATIC_SHA256_KEY01 = bytes.fromhex(
"3238362e122f162d6a1a6a0d3764293d2a3f0608696a1b6e0e0f2f3e3e39080d"
"046a0e133915382c041d0909316904346f1e301628150b062a6e353e2b250a18"
)
def kwsg_sha256_digest(data: bytes, table: KwaiZtTable | None = None, *, inner: bool = False) -> bytes:
if inner:
key00 = KWSG_STATIC_SHA256_KEY00
key01 = KWSG_STATIC_SHA256_KEY01
elif table is not None and table.key00 and table.key01:
key00 = table.key00
key01 = table.key01
else:
return hashlib.sha256(data).digest()
return hashlib.sha256(key01 + hashlib.sha256(key00 + data).digest()).digest()
LITE_DFP_KEYS = (
"k5",
"k14",
"k22",
"k27",
"k29",
"k31",
"k34",
"k35",
"k36",
"k39",
"k40",
"k46",
"k57",
"k64",
"k66",
"k68",
"k83",
"k86",
"k93",
"k97",
"k101",
"k102",
"k105",
"k106",
"k107",
"k108",
"k109",
"k110",
"k111",
"k112",
"k113",
)
@dataclass(frozen=True)
class KwaiZtTable:
appkey: str
platform: str
version: int
scopes: tuple[str, ...]
x5: str
raw: Mapping[str, object]
table_a: bytes = b""
table_b: bytes = b""
table_c: bytes = b""
key00: bytes = b""
key01: bytes = b""
@property
def selector_byte(self) -> int:
# Native takes byte 8 from x1/appkey; for UUID appkeys this is '-'.
return ord(self.appkey[8]) if len(self.appkey) > 8 else 2
@property
def header_word(self) -> int:
# load_zt_table_for_appkey_body stores sdk.x3 at table+0x30; zt_wrap reads
# that word via an internal pointer and XORs it with 0xCDEF.
return self.version & 0xFFFF
def crc32_ieee(data: bytes) -> int:
return binascii.crc32(data) & 0xFFFFFFFF
def _u16le(value: int) -> bytes:
return struct.pack("<H", value & 0xFFFF)
def _u32le(value: int) -> bytes:
return struct.pack("<I", value & 0xFFFFFFFF)
def get_magic() -> str:
return GET_MAGIC_VALUE
def get_clock(data: bytes | str) -> str:
if isinstance(data, str):
data = data.encode("utf-8")
return hashlib.md5(data + GET_CLOCK_SALT).hexdigest()
def java_value(value: object | None) -> str:
return "" if value is None else str(value)
def kwai_canonical_params(*maps: Mapping[str, object | None]) -> bytes:
parts: list[str] = []
for params in maps:
for key, value in params.items():
parts.append(f"{key}={java_value(value)}")
return "".join(sorted(parts)).encode("utf-8")
def kwai_sig(*maps: Mapping[str, object | None]) -> str:
return get_clock(kwai_canonical_params(*maps))
def kwai_token_sig(sig: str, client_salt: str) -> str:
return hashlib.sha256((sig + client_salt).encode("utf-8")).hexdigest()
def sig3_10405(
data: bytes | str,
appkey: str = APPKEY_MAIN,
now: int | None = None,
table: KwaiZtTable | None = None,
) -> str:
if isinstance(data, str):
data = data.encode("utf-8")
if now is None:
now = int(time.time())
sha_hex = hashlib.sha256(data).hexdigest()
prefix = str((now ^ 0xDDCC0DEF) & 0xFFFFFFFF)[:9]
selector = table.selector_byte if table else (ord(appkey[8]) if len(appkey) > 8 else 0)
middle = "9" + f"{(selector ^ 0xCD) & 0xFF:02x}"
return prefix + middle + sha_hex[:30]
def _sig3_feature_flags(feature_bits: int) -> int:
out = 0x0D00
if (feature_bits >> 61) & 1:
out |= 0x01
if (feature_bits >> 57) & 1:
out |= 0x02
if (feature_bits >> 60) & 1:
out |= 0x04
if (feature_bits >> 53) & 1:
out |= 0x10
if (feature_bits >> 54) & 1:
out |= 0x20
if (feature_bits >> 44) & 1:
out |= 0x40
return out
def _sig3_10417_payload(
data: bytes,
selector: int,
runtime_value: int,
counter: int,
now_seconds: int,
feature_bits: int,
) -> bytes:
payload = bytearray(24)
payload[0:2] = b"AQ"
payload[2:4] = _u16le(selector)
payload[4:8] = _u32le(runtime_value)
payload[8:12] = _u32le(counter)
payload[12:16] = _u32le(crc32_ieee(data))
payload[16:20] = _u32le(now_seconds)
payload[20:24] = _u32le(_sig3_feature_flags(feature_bits))
checksum = sum(payload[:23])
payload[23] = (checksum if checksum <= 0xFF else -checksum) & 0xFF
for index in range(23):
payload[index] ^= payload[23] ^ index
return bytes(payload)
def zt_wrap_payload_hex(payload_hex: str, table: KwaiZtTable) -> str:
raw_payload = bytes.fromhex(payload_hex)
key = table.x5.encode("utf-8")
if not key:
return payload_hex
header = bytearray()
header += b"ZT"
header += _u16le(table.header_word ^ 0xCDEF)
header += _u32le(crc32_ieee(table.platform.encode("utf-8")))
encrypted = bytes(byte ^ key[index & 0x0F] for index, byte in enumerate(raw_payload))
return header.hex() + encrypted.hex()
def sig3_10417(
data: bytes | str,
table: KwaiZtTable | None = None,
now: int | None = None,
counter: int = 1,
runtime_value: int = 0,
feature_bits: int = 0,
wrap: bool = True,
) -> str:
if isinstance(data, str):
data = data.encode("utf-8")
if now is None:
now = int(time.time())
selector = table.selector_byte if table else 2
payload_hex = _sig3_10417_payload(
data=data,
selector=selector,
runtime_value=runtime_value,
counter=counter,
now_seconds=now,
feature_bits=feature_bits,
).hex()
if wrap and table:
return zt_wrap_payload_hex(payload_hex, table)
return payload_hex
def _pkcs7_pad(data: bytes, block_size: int = 16) -> bytes:
pad_len = block_size - (len(data) % block_size)
return data + bytes([pad_len]) * pad_len
def _aes_ecb_encrypt(key: bytes, data: bytes) -> bytes:
try:
from Crypto.Cipher import AES
return AES.new(key, AES.MODE_ECB).encrypt(data)
except Exception:
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
encryptor = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend()).encryptor()
return encryptor.update(data) + encryptor.finalize()
_KWAi_PAYLOAD_XOR = bytes(
[0x1D, 0x1E, 0x17, 0x0B, 0x1D, 0x15, 0x1B, 0x14, 0x15, 0x16, 0x17, 0x10, 0x11, 0x12, 0x13, 0x0C]
)
_KWAi_NAME_XOR = bytes(
[0x1D, 0x17, 0x0B, 0x1D, 0x15, 0x1A, 0x1B, 0x14, 0x15, 0x16, 0x17, 0x10, 0x11, 0x12, 0x13, 0x0C]
)
_ZT_PERMUTE = (0, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12, 1, 6, 11)
def _u32le_from(data: bytes, offset: int) -> int:
return struct.unpack_from("<I", data, offset)[0]
def native_obfuscated_string(encoded: bytes) -> str:
"""Decode the one-byte-length string format used by native sub_3C54C."""
if not encoded:
return ""
length = encoded[0]
seed = b"Vuz4fCHxn1CO"
state_a = 324508639
state_b = 610839776
state_c = (-38177487) & 0xFFFFFFFF
for byte in seed[4:8]:
state_a = ((state_a << 8) | byte) & 0xFFFFFFFF
state_b = ((state_b << 8) | byte) & 0xFFFFFFFF
state_c = ((state_c << 8) | byte) & 0xFFFFFFFF
out = bytearray()
for byte in encoded[1 : 1 + length]:
bit_c = state_c & 1
bit_b = state_b & 1
stream_byte = 0
mixed = 0
for _ in range(8):
if state_a & 1:
state_a = (0x40000031 ^ state_a | 0x80000000) & 0xFFFFFFFF
if state_b & 1:
state_b = (0x20000010 ^ state_b | 0xC0000000) & 0xFFFFFFFF
bit_b = 1
else:
state_b = (0x3FFFFFFF & (state_b >> 1)) & 0xFFFFFFFF
bit_b = 0
else:
state_a = (0x7FFFFFFF & (state_a >> 1)) & 0xFFFFFFFF
if state_c & 1:
state_c = (0x08000001 ^ state_c | 0xF0000000) & 0xFFFFFFFF
bit_c = 1
else:
state_c = (0x0FFFFFFF & (state_c >> 1)) & 0xFFFFFFFF
bit_c = 0
mixed = (bit_c ^ bit_b) | (2 * stream_byte)
stream_byte = mixed & 0xFF
out.append(((mixed + 3) & 0xFF) ^ byte)
return out.decode("latin1")
def decode_kwai_container(blob: bytes) -> dict[str, bytes]:
"""Decode the native `kwai` resource container used for zt pass tables.
The CDN/asset file keeps a small plaintext directory, then a zlib stream whose
first two bytes are deliberately clobbered. Native restores them to 0x78da
before inflating and treats directory offsets as if the directory header were
still present, so extracted offsets are adjusted by the first data offset.
Directory names and payload bytes use adjacent but different 16-byte XOR keys.
"""
if not blob.startswith(b"kwai"):
raise ValueError("not a kwai zt resource container")
compressed_size = int.from_bytes(blob[12:16], "big")
entry_count = blob[20]
cursor = 21
entries: list[tuple[str, int, int]] = []
for _ in range(entry_count):
name_size = int.from_bytes(blob[cursor : cursor + 4], "big")
cursor += 4
encoded_name = blob[cursor : cursor + name_size]
cursor += name_size
offset = int.from_bytes(blob[cursor : cursor + 4], "big")
size = int.from_bytes(blob[cursor + 4 : cursor + 8], "big")
cursor += 8
name = bytes(byte ^ _KWAi_NAME_XOR[index & 0x0F] for index, byte in enumerate(encoded_name)).decode(
"latin1"
)
entries.append((name, offset, size))
first_offset = min(offset for _, offset, _ in entries)
compressed = bytearray(blob[first_offset : first_offset + compressed_size])
if len(compressed) < 2:
raise ValueError("empty kwai zt compressed payload")
compressed[0:2] = b"\x78\xda"
inflated = zlib.decompress(bytes(compressed))
decoded: dict[str, bytes] = {}
for name, offset, size in entries:
data = inflated[offset - first_offset : offset - first_offset + size]
decoded[name] = bytes(byte ^ _KWAi_PAYLOAD_XOR[index & 0x0F] for index, byte in enumerate(data))
return decoded
def load_kwai_dynamic_sections(path: str | os.PathLike[str]) -> dict[str, bytes]:
return decode_kwai_container(Path(path).read_bytes())
def _with_dynamic_sections(table: KwaiZtTable, sections: Mapping[str, bytes]) -> KwaiZtTable:
return KwaiZtTable(
appkey=table.appkey,
platform=table.platform,
version=table.version,
scopes=table.scopes,
x5=table.x5,
raw=table.raw,
table_a=sections.get("TBoxes_40960", sections.get("TKsnm|_40960", table.table_a)),
table_b=sections.get("TyiBoxes_147456", sections.get("TpuTgwes_147456", table.table_b)),
table_c=sections.get("TyiTables_4096", sections.get("TpuBimles_4096", table.table_c)),
key00=sections.get("sha256key00_64", sections.get("sa}$=9key00_64", table.key00)),
key01=sections.get("sha256key01_64", sections.get("sa}$=9key01_64", table.key01)),
)
def _zt_permute_block(block: bytearray) -> None:
original = bytes(block)
for index, source_index in enumerate(_ZT_PERMUTE):
block[index] = original[source_index]
def _zt_table_transform_block(block: bytes, table: KwaiZtTable) -> bytes:
if len(block) != 16:
raise ValueError("zt table transform operates on 16-byte blocks")
if len(table.table_a) < 40960 or len(table.table_b) < 147456:
raise ValueError("kwai zt dynamic pass tables are missing")
state = bytearray(block)
table_a = table.table_a
table_b = table.table_b
for round_index in range(9):
_zt_permute_block(state)
round_base = round_index << 14
for column in range(4):
byte_pos = column << 2
value = 0
for sub_index in range(4):
state_index = byte_pos | sub_index
table_offset = round_base + (state_index << 10) + (state[state_index] << 2)
value ^= _u32le_from(table_b, table_offset)
state[byte_pos] = (value >> 24) & 0xFF
state[byte_pos | 1] = (value >> 16) & 0xFF
state[byte_pos | 2] = (value >> 8) & 0xFF
state[byte_pos | 3] = value & 0xFF
_zt_permute_block(state)
for index, value in enumerate(state):
state[index] = table_a[36864 + (index << 8) + value]
return bytes(state)
def _zt_dynamic_op(table_c: bytes, left: int, right: int) -> int:
return table_c[((left & 0xFF) << 4) + (right & 0xFF)]
def _zt_dynamic_combine_words(first: int, second: int, third: int, fourth: int, table_c: bytes) -> bytes:
first &= 0xFFFFFFFF
second &= 0xFFFFFFFF
third &= 0xFFFFFFFF
fourth &= 0xFFFFFFFF
mid_12 = _zt_dynamic_op(table_c, (first >> 12) & 0xF, (second >> 12) & 0xF)
mid_08 = _zt_dynamic_op(table_c, (first >> 8) & 0xF, (second >> 8) & 0xF)
mid_20 = _zt_dynamic_op(table_c, (first >> 20) & 0xF, (second >> 20) & 0xF)
mid_04 = _zt_dynamic_op(table_c, (first >> 4) & 0xF, (second >> 4) & 0xF)
mid_16 = _zt_dynamic_op(table_c, (first >> 16) & 0xF, (second >> 16) & 0xF)
low_04 = _zt_dynamic_op(table_c, (third >> 4) & 0xF, (fourth >> 4) & 0xF)
mid_00 = _zt_dynamic_op(table_c, first & 0xF, second & 0xF)
mid_28 = _zt_dynamic_op(table_c, (first >> 28) & 0xF, (second >> 28) & 0xF)
low_00 = _zt_dynamic_op(table_c, third & 0xF, fourth & 0xF)
mid_24 = _zt_dynamic_op(table_c, (first >> 24) & 0xF, (second >> 24) & 0xF)
low_12 = _zt_dynamic_op(table_c, (third >> 12) & 0xF, (fourth >> 12) & 0xF)
low_20 = _zt_dynamic_op(table_c, (third >> 20) & 0xF, (fourth >> 20) & 0xF)
low_08 = _zt_dynamic_op(table_c, (third >> 8) & 0xF, (fourth >> 8) & 0xF)
low_16 = _zt_dynamic_op(table_c, (third >> 16) & 0xF, (fourth >> 16) & 0xF)
low_24 = _zt_dynamic_op(table_c, (third >> 24) & 0xF, (fourth >> 24) & 0xF)
low_28 = _zt_dynamic_op(table_c, (third >> 28) & 0xF, (fourth >> 28) & 0xF)
out0_low = _zt_dynamic_op(table_c, mid_24, low_24)
out0_high = _zt_dynamic_op(table_c, mid_28, low_28)
out1_low = _zt_dynamic_op(table_c, mid_16, low_16)
out1_high = _zt_dynamic_op(table_c, mid_20, low_20)
out2_low = _zt_dynamic_op(table_c, mid_08, low_08)
out2_high = _zt_dynamic_op(table_c, mid_12, low_12)
out3_low = _zt_dynamic_op(table_c, mid_00, low_00)
out3_high = _zt_dynamic_op(table_c, mid_04, low_04)
return bytes(
(
(out0_low | (out0_high << 4)) & 0xFF,
(out1_low | (out1_high << 4)) & 0xFF,
(out2_low | (out2_high << 4)) & 0xFF,
(out3_low | (out3_high << 4)) & 0xFF,
)
)
def _zt_table_transform_dynamic_block(block: bytes, table: KwaiZtTable) -> bytes:
if len(block) != 16:
raise ValueError("zt table transform operates on 16-byte blocks")
if len(table.table_a) < 40960 or len(table.table_b) < 147456 or len(table.table_c) < 4096:
raise ValueError("kwai zt dynamic pass tables are missing")
state = bytearray(block)
table_a = table.table_a
table_b = table.table_b
table_c = table.table_c
for round_index in range(9):
_zt_permute_block(state)
round_base = round_index << 14
for column in range(4):
byte_pos = column << 2
words = []
for sub_index in range(4):
state_index = byte_pos | sub_index
table_offset = round_base + (state_index << 10) + (state[state_index] << 2)
words.append(_u32le_from(table_b, table_offset))
state[byte_pos : byte_pos + 4] = _zt_dynamic_combine_words(*words, table_c=table_c)
_zt_permute_block(state)
for index, value in enumerate(state):
state[index] = table_a[36864 + (index << 8) + value]
return bytes(state)
def zt_table_transform(data: bytes, table: KwaiZtTable, *, inner: bool = False) -> bytes:
if len(data) % 16:
raise ValueError("zt table transform input must be 16-byte aligned")
out = bytearray()
# Native 10418 has two call sites (normal atlasSign and MXSec/DFP inner),
# but both reach the same nibble-XOR combine table at byte_59080. The inner
# path only picks a different manager slot for the same TBoxes/TyiBoxes data;
# TyiTables_4096 belongs to a different transform and must not be used here.
block_transform = _zt_table_transform_block
for offset in range(0, len(data), 16):
out += block_transform(data[offset : offset + 16], table)
return bytes(out)
def sig3_10418(
data: bytes | str,
table: KwaiZtTable,
*,
inner: bool = False,
) -> str:
if isinstance(data, str):
data = data.encode("utf-8")
# Command 10418 computes the native SHA wrapper digest, PKCS#7-pads the
# raw 32-byte digest, then applies the zt pass-table transform. The first
# bytes_to_lower_hex_string() call in libkwsgmain.so:0x1E4E8 is a temporary
# std::string side path; sub_1E07C still receives the raw digest pointer and
# length. MXSec/DFP passes inner=true, selecting the embedded static pads.
digest = kwsg_sha256_digest(data, table, inner=inner)
return zt_table_transform(_pkcs7_pad(digest), table, inner=inner).hex()
def _png_paeth(a: int, b: int, c: int) -> int:
p = a + b - c
pa = abs(p - a)
pb = abs(p - b)
pc = abs(p - c)
if pa <= pb and pa <= pc:
return a
if pb <= pc:
return b
return c
def _png_unfilter(raw: bytes, width: int, height: int, channels: int) -> bytes:
stride = width * channels
bpp = channels
out = bytearray()
cursor = 0
prev = bytearray(stride)
for _ in range(height):
filter_type = raw[cursor]
cursor += 1
row = bytearray(raw[cursor : cursor + stride])
cursor += stride
for i, value in enumerate(row):
left = row[i - bpp] if i >= bpp else 0
up = prev[i]
up_left = prev[i - bpp] if i >= bpp else 0
if filter_type == 1:
row[i] = (value + left) & 0xFF
elif filter_type == 2:
row[i] = (value + up) & 0xFF
elif filter_type == 3:
row[i] = (value + ((left + up) >> 1)) & 0xFF
elif filter_type == 4:
row[i] = (value + _png_paeth(left, up, up_left)) & 0xFF
elif filter_type != 0:
raise ValueError(f"unsupported PNG filter {filter_type}")
out.extend(row)
prev = row
return bytes(out)
def _extract_json_object(data: bytes) -> dict[str, object]:
start = data.find(b"{")
if start < 0:
raise ValueError("hidden PNG payload does not contain JSON")
depth = 0
in_string = False
escape = False
for pos in range(start, len(data)):
byte = data[pos]
if in_string:
if escape:
escape = False
elif byte == 0x5C:
escape = True
elif byte == 0x22:
in_string = False
continue
if byte == 0x22:
in_string = True
elif byte == 0x7B:
depth += 1
elif byte == 0x7D:
depth -= 1
if depth == 0:
return json.loads(data[start : pos + 1].decode("utf-8"))
raise ValueError("unterminated hidden PNG JSON")
def decode_zt_png(path: str | os.PathLike[str]) -> KwaiZtTable:
blob = Path(path).read_bytes()
if not blob.startswith(b"\x89PNG\r\n\x1a\n"):
raise ValueError(f"not a PNG file: {path}")
pos = 8
width = height = bit_depth = color_type = None
idat = bytearray()
while pos < len(blob):
length = struct.unpack(">I", blob[pos : pos + 4])[0]
chunk_type = blob[pos + 4 : pos + 8]
chunk_data = blob[pos + 8 : pos + 8 + length]
pos += 12 + length
if chunk_type == b"IHDR":
width, height, bit_depth, color_type = struct.unpack(">IIBB", chunk_data[:10])
elif chunk_type == b"IDAT":
idat.extend(chunk_data)
elif chunk_type == b"IEND":
break
if width is None or height is None or bit_depth != 8:
raise ValueError("unsupported PNG metadata")
channels_by_type = {0: 1, 2: 3, 4: 2, 6: 4}
channels = channels_by_type.get(color_type)
if channels not in (3, 4):
raise ValueError(f"unsupported zt PNG color type {color_type}")
pixels = _png_unfilter(zlib.decompress(bytes(idat)), width, height, channels)
triples: list[tuple[int, int, int]] = []
for offset in range(0, len(pixels), channels):
triples.append((pixels[offset], pixels[offset + 1], pixels[offset + 2]))
if not triples:
raise ValueError("empty zt PNG")
first_r, first_g, first_b = triples[0]
selector = ((first_r & 1) << 2) | ((first_g & 1) << 1) | (first_b & 1)
orders = {
0: (0, 1, 2),
1: (0, 2, 1),
2: (1, 0, 2),
3: (1, 2, 0),
4: (1, 0, 2), # bundled tables use selector 4 => G,R,B
5: (2, 0, 1),
6: (2, 1, 0),
7: (0, 1, 2),
}
order = orders[selector]
bits: list[int] = []
for rgb in triples[1:]:
for channel_index in order:
bits.append(rgb[channel_index] & 1)
hidden = bytearray()
for offset in range(0, len(bits) - 7, 8):
value = 0
for bit in bits[offset : offset + 8]:
value = (value << 1) | bit
hidden.append(value)
parsed = _extract_json_object(bytes(hidden))
sdk = parsed.get("sdk") if isinstance(parsed.get("sdk"), dict) else parsed
if not isinstance(sdk, dict):
raise ValueError("zt JSON does not contain sdk object")
scopes = sdk.get("x4") or []
if isinstance(scopes, str):
scopes_tuple = (scopes,)
else:
scopes_tuple = tuple(str(item) for item in scopes)
return KwaiZtTable(
appkey=str(sdk.get("x1", "")),
platform=str(sdk.get("x2", "")),
version=int(str(sdk.get("x3", "0")) or "0"),
scopes=scopes_tuple,
x5=str(sdk.get("x5", "")),
raw=parsed,
)
def find_zt_table(appkey: str, assets_root: str | os.PathLike[str] = "resources/assets") -> KwaiZtTable:
root = Path(assets_root)
table = decode_zt_png(root / "saio_res" / f"zt_{appkey}.png")
dynamic_path = root / "video_yh_loading_icon.png"
if dynamic_path.exists():
try:
table = _with_dynamic_sections(table, load_kwai_dynamic_sections(dynamic_path))
except Exception:
pass
return table
def varint(value: int) -> bytes:
out = bytearray()
while value >= 0x80:
out.append((value & 0x7F) | 0x80)
value >>= 7
out.append(value)
return bytes(out)
def proto_string(field_number: int, value: object | None) -> bytes:
if value is None:
return b""
data = str(value).encode("utf-8")
if not data:
return b""
return varint((field_number << 3) | 2) + varint(len(data)) + data
def encode_dfp_proto(fields: Mapping[str, object | None]) -> bytes:
encoded = bytearray()
for key in sorted(fields, key=lambda item: int(item[1:]) if item.startswith("k") and item[1:].isdigit() else 10**9):
if key.startswith("k") and key[1:].isdigit():
encoded += proto_string(int(key[1:]), fields[key])
return bytes(encoded)
def build_lite_dfp_fields(
values: Mapping[str, object | None] | None = None,
android_release: str = KWE_N,
sdk_version: str = DFP_SDK_VERSION,
sdk_platform: str = DFP_SDK_PLATFORM,
) -> dict[str, str]:
values = values or {}
fields: dict[str, str] = {key: KWE_N for key in LITE_DFP_KEYS}
fields.update({key: java_value(value) or KWE_N for key, value in values.items() if key in fields})
fields["k14"] = sdk_platform
fields["k35"] = java_value(values.get("k35", android_release)) or KWE_N
fields["k36"] = java_value(values.get("k36", sdk_version)) or KWE_N
crc = 0
for key in LITE_DFP_KEYS:
update_value = sdk_platform if key == "k14" else fields.get(key, "")
if update_value:
crc = binascii.crc32(update_value.encode("utf-8"), crc)
fields["k14"] = f"{sdk_platform}:{crc & 0xFFFFFFFF}"
return fields
def build_lite_dfp(
values: Mapping[str, object | None] | None = None,
*,
encrypt: bool = False,
table: KwaiZtTable | None = None,
android_release: str = KWE_N,
) -> str:
fields = build_lite_dfp_fields(values, android_release=android_release)
raw_proto = encode_dfp_proto(fields)
payload = zt_atlas_encrypt_like(raw_proto, table) if encrypt and table else raw_proto
return urllib.parse.quote(base64.b64encode(payload).decode("ascii"), safe="")
def _time_prefix(now: int | None = None) -> bytes:
if now is None:
now = int(time.time())
return str((now ^ 0xDDCC0DEF) & 0xFFFFFFFF)[:9].encode("ascii")
def _zt_appkey_header_fields(table: KwaiZtTable) -> tuple[int, bytes, bytes, int]:
appkey_bytes = table.appkey.encode("utf-8")
if len(appkey_bytes) < 17:
selector = table.selector_byte
return selector, bytes((selector, selector)), _u32le(crc32_ieee(appkey_bytes)), selector
return appkey_bytes[8], appkey_bytes[9:11], appkey_bytes[12:16], appkey_bytes[16]
def zt_atlas_encrypt_like(
data: bytes,
table: KwaiZtTable,
*,
now: int | None = None,
enable_header: bool = True,
) -> bytes:
# Recovered native 10400/10406 family path: mode=1 pack, then zt_wrap
# case 1. This returns binary; Java Base64-encodes it for DFP.
key = table.x5.encode("utf-8")
if not key:
return data
table0, table_word1, table_dword4, table8 = _zt_appkey_header_fields(table)
if enable_header:
inner = bytearray(32)
inner[0:4] = _u32le(0xDEADC0DE)
inner[4:6] = _u16le(32)
inner[6:15] = _time_prefix(now)
inner[15] = table0
inner[16:18] = table_word1
inner[18] = 1
inner[19:23] = table_dword4
inner[23] = table8
inner[24:28] = _u32le(crc32_ieee(data))
inner[28:32] = _u32le(len(data))
packed = bytes(inner) + data
else:
inner = bytearray(17)
inner[0] = table0
inner[1:3] = table_word1
inner[3] = 0
inner[4:8] = table_dword4
inner[8] = table8
inner[9:13] = _u32le(crc32_ieee(data))
inner[13:17] = _u32le(len(data))
packed = bytes(inner) + data
header = b"ZT" + _u16le(table.header_word ^ 0xCDEF) + _u32le(crc32_ieee(table.platform.encode("utf-8")))
encrypted = bytes(byte ^ key[index & 0x0F] for index, byte in enumerate(packed))
return header + encrypted
def rc4_ksa_prga(key: bytes | str, data: bytes | str) -> bytes:
if isinstance(key, str):
key = key.encode("utf-8")
if isinstance(data, str):
data = data.encode("utf-8")
if not key:
raise ValueError("RC4 key cannot be empty")
state = list(range(256))
j = 0
key_index = 0
for i in range(256):
j = (j + state[i] + key[key_index]) & 0xFF
state[i], state[j] = state[j], state[i]
key_index = (key_index + 1) % len(key)
out = bytearray()
i = j = 0
for byte in data:
i = (i + 1) & 0xFF
j = (j + state[i]) & 0xFF
state[i], state[j] = state[j], state[i]
out.append(byte ^ state[(state[i] + state[j]) & 0xFF])
return bytes(out)
def encrypt_param_with_fix(value: str, security_value: bytes | str) -> str:
encrypted = rc4_ksa_prga(security_value, value.encode("utf-8"))
return "ZTSP__" + base64.b64encode(encrypted).decode("ascii").replace("\n", "") + "__ZTSP"
def azeroth_canonical(
method: str,
path: str,
params: Mapping[str, object | None],
nonce: int | None = None,
) -> str:
parts = [method.upper().strip(), path.strip()]
for key in sorted(params):
if not key.startswith("__"):
parts.append(f"{key}={java_value(params[key])}")
if nonce is not None:
parts.append(str(nonce).strip())
return "&".join(parts)
def _android_urlsafe_b64_no_wrap_no_padding(data: bytes) -> str:
return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")
def azeroth_nonce(now_ms: int | None = None, rand32: int | None = None) -> int:
if now_ms is None:
now_ms = int(time.time() * 1000)
minute = int(now_ms // 60000) & 0xFFFFFFFF
if rand32 is None:
rand32 = random.getrandbits(32)
# Java source is `this.a = i2 | (i3 << 32)` where i2/i3 are int.
# Java masks int shift counts with 0x1f, so `i3 << 32` is actually
# `i3 << 0`; the OR result is then sign-extended when assigned to long.
value = (minute | (int(rand32) & 0xFFFFFFFF)) & 0xFFFFFFFF
return value - 0x100000000 if value & 0x80000000 else value
def azeroth_hmac_client_sign(
method: str,
path: str,
params: Mapping[str, object | None],
security_b64: str,
*,
nonce: int | None = None,
) -> str:
if not security_b64:
return ""
if nonce is None:
nonce = azeroth_nonce()
key = base64.b64decode(security_b64)
canonical = azeroth_canonical(method, path, params, nonce).encode("utf-8")
digest = hmac.new(key, canonical, hashlib.sha256).digest()
return _android_urlsafe_b64_no_wrap_no_padding((nonce & 0xFFFFFFFFFFFFFFFF).to_bytes(8, "big") + digest)
def load_all_zt_tables(assets_root: str | os.PathLike[str] = "resources/assets") -> dict[str, KwaiZtTable]:
root = Path(assets_root) / "saio_res"
tables: dict[str, KwaiZtTable] = {}
for path in root.glob("zt_*.png"):
table = decode_zt_png(path)
tables[table.appkey] = table
return tables
为什么算法层要单独拆出来
原因很简单:签名算法要可独立测试。REST 请求失败可能是账号态、风控、参数 profile、路径 rewrite、host、cookie 的问题;如果算法和请求发送混在一起,就无法定位失败到底来自哪里。
因此 algorithms.py 不发送请求,只负责纯函数:输入 bytes/map/table,输出 digest/signature/form 字段。
0x06 DFP / DID / EGID:不是一个普通接口,而是 KSecurity context 派生
DFP 这块如果只看 endpoint,很容易误以为就是一个普通 /rest/infra/... 表单。但 Java 初始化代码告诉我们,它先初始化 KSecurity,再设置 did/product/feature。
DFP 初始化:DFPInitUtils.java
- 文件:
sources/com/yxcorp/init/DFPInitUtils.java - 行数:
162 - 字节:
5868 - SHA256:
1db13f0e1457f48eea3608e0d90f667d08cd4c3639c5027408d38f6e505e2c19
为什么看它:这里能看到 KSecurity.Initialize(...) 的 appkey/secret,以及 setDid、setProductName、setWithFeature(ALL)。
读完得到的结论:Python 的 DFP 表单构造不能脱离设备态;fetch_egid() / fetch_did() 只是最后发送层。
package com.yxcorp.init;
import cj.AppEnv;
import com.antman.utility.Log;
import com.kuaishou.android.security.KSecurity;
import com.kuaishou.android.security.base.exception.KSException;
import com.kuaishou.android.security.base.log.KSecuritySdkILog;
import com.kuaishou.android.security.base.util.KSecurityTrack;
import com.kuaishou.android.security.internal.common.KSecurityContext;
import com.yxcorp.buildconfig.BuildConfig;
import com.yxcorp.utils.antman.AntmanShellSPHelper;
import kotlin.jvm.internal.h;
import org.jetbrains.annotations.NotNull;
/* compiled from: DFPInitUtils.kt */
/* renamed from: com.yxcorp.init.d, reason: use source file name */
/* loaded from: classes.dex */
public final class DFPInitUtils {
/* compiled from: DFPInitUtils.kt */
/* renamed from: com.yxcorp.init.d$a */
public static final class a implements KSecuritySdkILog {
a() {
}
@Override // com.kuaishou.android.security.base.log.KSecuritySdkILog
public void onSecuriySuccess() {
}
@Override // com.kuaishou.android.security.base.log.KSecuritySdkILog
public void onSeucrityError(@NotNull KSException e3) {
h.f(e3, "e");
}
@Override // com.kuaishou.android.security.base.log.KSecuritySdkILog
public void report(@NotNull String s2, @NotNull String s1) {
h.f(s2, "s");
h.f(s1, "s1");
}
}
/* compiled from: DFPInitUtils.kt */
/* renamed from: com.yxcorp.init.d$b */
public static final class b implements KSecurityTrack.IKSecurityTrackCallback {
/* compiled from: DFPInitUtils.kt */
/* renamed from: com.yxcorp.init.d$b$a */
public /* synthetic */ class a {
public static final /* synthetic */ int[] a;
static {
int[] iArr = new int[KSecurityTrack.LEVEL.values().length];
try {
iArr[KSecurityTrack.LEVEL.VERBOSE.ordinal()] = 1;
} catch (NoSuchFieldError unused) {
}
try {
iArr[KSecurityTrack.LEVEL.DEBUG.ordinal()] = 2;
} catch (NoSuchFieldError unused2) {
}
try {
iArr[KSecurityTrack.LEVEL.INFO.ordinal()] = 3;
} catch (NoSuchFieldError unused3) {
}
try {
iArr[KSecurityTrack.LEVEL.WARN.ordinal()] = 4;
} catch (NoSuchFieldError unused4) {
}
try {
iArr[KSecurityTrack.LEVEL.ERROR.ordinal()] = 5;
} catch (NoSuchFieldError unused5) {
}
a = iArr;
}
}
b() {
}
@Override // com.kuaishou.android.security.base.util.KSecurityTrack.IKSecurityTrackCallback
public long getAppStartTime() {
return -1L;
}
@Override // com.kuaishou.android.security.base.util.KSecurityTrack.IKSecurityTrackCallback
public long getHomeStartTime() {
return -1L;
}
@Override // com.kuaishou.android.security.base.util.KSecurityTrack.IKSecurityTrackCallback
public int getLaunchSource() {
return -1;
}
@Override // com.kuaishou.android.security.base.util.KSecurityTrack.IKSecurityTrackCallback
@NotNull
public String getSessionId() {
return "";
}
@Override // com.kuaishou.android.security.base.util.KSecurityTrack.IKSecurityTrackCallback
public boolean isAppOnForeground() {
return AppEnv.f1021m;
}
@Override // com.kuaishou.android.security.base.util.KSecurityTrack.IKSecurityTrackCallback
public boolean isColdStart() {
return AppEnv.f1020l;
}
@Override // com.kuaishou.android.security.base.util.KSecurityTrack.IKSecurityTrackCallback
public void log(@NotNull KSecurityTrack.LEVEL level, @NotNull String s2, @NotNull String s1, @NotNull Throwable throwable) {
h.f(level, "level");
h.f(s2, "s");
h.f(s1, "s1");
h.f(throwable, "throwable");
int i2 = a.a[level.ordinal()];
if (i2 == 1) {
Log.l(s2, s1, throwable);
return;
}
if (i2 == 2) {
Log.c(s2, s1, throwable);
return;
}
if (i2 == 3) {
Log.g(s2, s1, throwable);
return;
}
if (i2 == 4) {
Log.n(s2, s1, throwable);
} else if (i2 != 5) {
Log.g(s2, s1, throwable);
} else {
Log.e(s2, s1, throwable);
}
}
@Override // com.kuaishou.android.security.base.util.KSecurityTrack.IKSecurityTrackCallback
public void logsdkReport(@NotNull String s2, @NotNull String s1) {
h.f(s2, "s");
h.f(s1, "s1");
LogManagerInitUtil.G().a(s2, s1);
}
}
public static final void a() {
try {
KSecurity.Initialize(AppEnv.a(), "d7b7d042-d4f2-4012-be60-d97ff2429c17", "lD6We1E8i", new a());
} catch (KSException e3) {
e3.printStackTrace();
}
b();
KSecurity.getkSecurityParameterContext().setDid(AntmanShellSPHelper.a.e());
KSecurity.getkSecurityParameterContext().setProductName(BuildConfig.BIG_KPN);
KSecurity.getkSecurityParameterContext().setWithFeature(KSecurityContext.Feature.ALL);
}
private static final void b() {
KSecurityTrack.setDelegate(new b());
}
}
DFP 对照实现
DFP 的实现没有单独放一个新文件,而是在 algorithms.py 里构造字段,在 client.py 里发请求。等会儿 client.py 完整内联时可以看到 DFP_EGID_ENDPOINT、DFP_DID_ENDPOINT、build_dfp_form()、fetch_egid()、fetch_did()。
0x07 Azeroth:另一套签名模型,不要和主 App sig3 混用
Azeroth 是另一条线。主 App __NS_sig3 是 path + sig,但 Azeroth 的 Java 证据显示它走 method + path + params canonical,再交给 MXSec wrapper。
Azeroth 参数处理:AzerothParamProcessor.java
- 文件:
sources/vc/AzerothParamProcessor.java - 行数:
202 - 字节:
7441 - SHA256:
5774a8817a407e34f54e086a0802ca8b7feba7d99f79eb2daec6442bce79f00c
为什么看它:它明确读取 request.method()、request.url().h() 和 params,再调用 SignatureUtil.d() / MXSec wrapper。
读完得到的结论:Python 对应 azeroth_canonical()、azeroth_nonce()、azeroth_hmac_client_sign(),不能复用主 App 的 path + sig。
package vc;
import ad.EncryptParamHandler;
import android.os.SystemClock;
import com.kuaishou.dfp.c.k;
import com.kuaishou.weapon.ks.x;
import com.kwai.middleware.azeroth.Azeroth;
import com.kwai.middleware.azeroth.Azeroth2;
import com.kwai.middleware.leia.handler.LeiaParamProcessor;
import java.util.LinkedHashMap;
import java.util.Map;
import kotlin.jvm.internal.Ref$ObjectRef;
import kotlin.jvm.internal.h;
import mh.MXSec;
import mh.MXWrapper;
import okhttp3.Request;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import qd.SignatureUtil;
import td.NetExt;
/* compiled from: AzerothParamProcessor.kt */
/* renamed from: vc.c, reason: use source file name */
/* loaded from: classes.dex */
public class AzerothParamProcessor extends LeiaParamProcessor {
private String b;
private final AzerothParamExtractor c;
/* JADX WARN: 'super' call moved to the top of the method (can break code semantics) */
public AzerothParamProcessor(@NotNull AzerothParamExtractor azerothExtractor) {
super(azerothExtractor);
h.g(azerothExtractor, "azerothExtractor");
this.c = azerothExtractor;
}
@Override // com.kwai.middleware.leia.handler.LeiaParamProcessor
@NotNull
public Map<String, String> a() {
LinkedHashMap linkedHashMap = new LinkedHashMap();
linkedHashMap.put("Accept-Language", d().h());
linkedHashMap.put("X-REQUESTID", String.valueOf(SystemClock.elapsedRealtime()));
linkedHashMap.put("Connection", "keep-alive");
String n2 = n(i());
if (n2 != null) {
if (n2.length() > 0) {
linkedHashMap.put("Cookie", n2);
}
}
return linkedHashMap;
}
@Override // com.kwai.middleware.leia.handler.LeiaParamProcessor
@NotNull
public Map<String, String> b() {
return super.b();
}
@Override // com.kwai.middleware.leia.handler.LeiaParamProcessor
@NotNull
public Map<String, String> c() {
LinkedHashMap linkedHashMap = new LinkedHashMap();
linkedHashMap.put("kpn", d().m());
linkedHashMap.put("kpf", d().l());
linkedHashMap.put("appver", d().b());
linkedHashMap.put("ver", d().c());
linkedHashMap.put("gid", d().g());
String f = d().f();
if (f.length() == 0) {
throw new IllegalArgumentException("The device id cannot be null or empty.");
}
linkedHashMap.put("did", f);
linkedHashMap.put("userId", d().r());
if (td.b.e(Azeroth2.f2692s.j(), k.f2103j)) {
String valueOf = String.valueOf(d().i());
String valueOf2 = String.valueOf(d().j());
if (this.c.B()) {
valueOf = EncryptParamHandler.c(valueOf);
h.b(valueOf, "EncryptParamHandler.encryptWithFix(latitude)");
valueOf2 = EncryptParamHandler.c(valueOf2);
h.b(valueOf2, "EncryptParamHandler.encryptWithFix(longitude)");
}
linkedHashMap.put("lat", valueOf);
linkedHashMap.put("lon", valueOf2);
}
linkedHashMap.put("mod", d().k());
linkedHashMap.put("net", NetExt.c(d().a()));
linkedHashMap.put("os", "android");
linkedHashMap.put(x.f2587p, d().d());
linkedHashMap.put("language", d().h());
linkedHashMap.put("countryCode", d().e());
linkedHashMap.put("sys", d().q());
String str = this.b;
if (str != null) {
linkedHashMap.put("subBiz", str);
}
return linkedHashMap;
}
@Override // com.kwai.middleware.leia.handler.LeiaParamProcessor
public void g(@NotNull String path, @NotNull Map<String, String> urlParams) {
h.g(path, "path");
h.g(urlParams, "urlParams");
Azeroth c = Azeroth.c();
h.b(c, "Azeroth.get()");
lc.h g3 = c.g();
h.b(g3, "Azeroth.get().initParams");
g3.c().a(path, urlParams);
}
/* JADX WARN: Multi-variable type inference failed */
/* JADX WARN: Type inference failed for: r2v3, types: [T, java.lang.String] */
@Override // com.kwai.middleware.leia.handler.LeiaParamProcessor
@NotNull
public Map<String, String> h(@NotNull Request request, @NotNull Map<String, String> params) {
h.g(request, "request");
h.g(params, "params");
LinkedHashMap linkedHashMap = new LinkedHashMap();
Ref$ObjectRef ref$ObjectRef = new Ref$ObjectRef();
?? f = f(request, params, Azeroth2.f2692s.v().n());
ref$ObjectRef.element = f;
if (f.length() > 0) {
linkedHashMap.put("__clientSign", (String) ref$ObjectRef.element);
}
if (this.c.t()) {
String k3 = k(request, params);
if (!(k3.length() > 0)) {
throw new IllegalStateException("麻烦联系安全组张艳生,升级或者接入KWSecuritySDK:3.9.1.4 + 版本,以便完成__NS_sig3的计算流程");
}
linkedHashMap.put("__NS_sig3", k3);
}
return linkedHashMap;
}
@NotNull
public Map<String, String> i() {
LinkedHashMap linkedHashMap = new LinkedHashMap();
String p2 = d().p();
String o2 = d().o();
linkedHashMap.put("did", d().f());
if (p2.length() > 0) {
if (o2.length() > 0) {
linkedHashMap.put(o2 + "_st", p2);
}
}
return linkedHashMap;
}
@NotNull
public String j(@NotNull String method, @NotNull String path, @NotNull Map<String, String> params) {
h.g(method, "method");
h.g(path, "path");
h.g(params, "params");
String d = SignatureUtil.d(method, path, params, null);
MXSec a = MXSec.a();
h.b(a, "MXSec.get()");
MXWrapper d3 = a.d();
Azeroth2 azeroth2 = Azeroth2.f2692s;
String a2 = d3.a("azeroth", "010a11c6-f2cb-4016-887d-0d958aef1534", 0, d);
h.b(a2, "MXSec.get().mxWrapper.at…stringThatNeedToBeSigned)");
return a2;
}
@NotNull
public String k(@NotNull Request request, @NotNull Map<String, String> params) {
h.g(request, "request");
h.g(params, "params");
String method = request.method();
h.b(method, "request.method()");
String h3 = request.url().h();
h.b(h3, "request.url().encodedPath()");
return j(method, h3, params);
}
@Nullable
protected final String l() {
return this.b;
}
public final void m(@NotNull String subBiz) {
h.g(subBiz, "subBiz");
this.b = subBiz;
}
@Nullable
protected final String n(@NotNull Map<String, String> cookieMap) {
h.g(cookieMap, "cookieMap");
if (cookieMap.isEmpty()) {
return "";
}
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : cookieMap.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
sb.append(key);
sb.append('=');
sb.append(value);
sb.append(";");
}
sb.deleteCharAt(sb.length() - 1);
return sb.toString();
}
}
Azeroth 配置接口:AzerothService.java
- 文件:
sources/com/kwai/middleware/azeroth/api/AzerothService.java - 行数:
25 - 字节:
1484 - SHA256:
0582fc9705615d4ffbfa0f565e9695e9e7e604c84bb9e59b34dbcfc5ba59d234
为什么看它:它给出 /rest/zt/appsupport/configs 的 Retrofit 注解来源。
读完得到的结论:Python RestEndpoint 里的 azeroth_configs 与它对应。
package com.kwai.middleware.azeroth.api;
import com.google.gson.JsonObject;
import com.kuaishou.weapon.ks.x;
import io.reactivex.Observable;
import java.util.Map;
import kotlin.Metadata;
import kotlin.jvm.JvmSuppressWildcards;
import org.jetbrains.annotations.NotNull;
import retrofit2.http.GET;
import retrofit2.http.Headers;
import retrofit2.http.QueryMap;
import wc.AzerothResponse;
/* compiled from: AzerothService.kt */
@Metadata(bv = {1, 0, 3}, d1 = {"\u0000\"\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\u0010$\n\u0002\u0010\u000e\n\u0000\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\bf\u0018\u00002\u00020\u0001J*\u0010\b\u001a\u000e\u0012\n\u0012\b\u0012\u0004\u0012\u00020\u00070\u00060\u00052\u0014\b\u0001\u0010\u0004\u001a\u000e\u0012\u0004\u0012\u00020\u0003\u0012\u0004\u0012\u00020\u00010\u0002H'¨\u0006\t"}, d2 = {"Lcom/kwai/middleware/azeroth/api/a;", "", "", "", "extraParams", "Lio/reactivex/n;", "Lwc/b;", "Lcom/google/gson/JsonObject;", x.f2583l, "azeroth_release"}, k = 1, mv = {1, 4, 0})
/* renamed from: com.kwai.middleware.azeroth.api.a, reason: use source file name */
/* loaded from: classes.dex */
public interface AzerothService {
@GET("/rest/zt/appsupport/configs")
@JvmSuppressWildcards
@NotNull
@Headers({"Content-Type:application/x-www-form-urlencoded; charset=utf-8"})
Observable<AzerothResponse<JsonObject>> a(@QueryMap @NotNull Map<String, Object> extraParams);
}
0x08 REST 客户端:把算法接到真实请求 shape
算法能跑不代表请求能跑。请求还需要:host、path rewrite、GET/POST 参数位置、auth 字段、dynamic capture、HAR 导入、私信/搜索/视频/用户/消息 wrapper。所有这些集中在 client.py。
协议层完整实现:kws_recovered/client.py
- 文件:
kws_recovered/client.py - 行数:
3318 - 字节:
137308 - SHA256:
4d13369551b7e2d8d1158876ca3876abcce7f1d32043319153dca095c96e8459
为什么看它:这是项目里最核心的工程文件:设备状态、REST endpoint、签名请求、HAR 导入、登录态、动态字段捕获、搜索/视频/用户/消息/私信 wrapper 都在这里。
读完得到的结论:它把 ParamsUtils、KwaiParams、HAR observed、IDA/Python 算法串成可调用的客户端。
from __future__ import annotations
import base64
import socket
import hashlib
import json
import re
import time
import urllib.parse
import uuid
from contextlib import contextmanager
from dataclasses import dataclass, field, fields, replace
from http.cookies import SimpleCookie
from pathlib import Path
from typing import Any, Iterable, Mapping
from .dex_annotations import extract_retrofit_endpoints
from .algorithms import (
APPKEY_AZEROTH,
APPKEY_MAIN,
KwaiZtTable,
azeroth_canonical,
azeroth_hmac_client_sign,
build_lite_dfp,
build_lite_dfp_fields,
encrypt_param_with_fix,
encode_dfp_proto,
find_zt_table,
kwai_sig,
kwai_token_sig,
sig3_10405,
sig3_10417,
sig3_10418,
zt_atlas_encrypt_like,
)
DFP_APPKEY = APPKEY_MAIN
DFP_PRODUCT_NAME = "KUAISHOU"
DFP_APP_VERSION = "10.6.30.12345"
DFP_EGID_ENDPOINT = "https://gdfpsec.ksapisrv.com/rest/infra/gdfp/report/kuaishou/android"
DFP_DID_ENDPOINT = "https://gdfpsec.ksapisrv.com/rest/infra/unifiedId/fetch/android"
DNS_OVERRIDE_IPS = {
"gdfpsec.ksapisrv.com": (
"124.156.126.37",
"103.107.217.68",
"103.102.202.85",
"103.102.200.2",
"103.107.217.28",
),
}
CAPTCHA_VERIFY_ENDPOINT = "https://app.m.kuaishou.com/rest/wd/captcha/verify"
CAPTCHA_IFRAME_BASE = "https://captcha.zt.kuaishou.com/iframe/index.html"
MAIN_VIDEO_LEGACY_APPVER = "6.5.5.9591"
MAIN_VIDEO_LEGACY_VER = "6.5"
MAIN_VIDEO_LOGIN_APPVER = "6.9.9.9999"
MAIN_VIDEO_LOGIN_VER = "6.9"
MAIN_VIDEO_MAX_MEMORY = "192"
AZEROTH_HOST = "https://api.kuaishouzt.com"
AZEROTH_LATEST_APPVER = "14.3.30.47512"
AZEROTH_LATEST_VER = "14.3"
AZEROTH_ANDROID_API_LEVEL = "31"
@dataclass(frozen=True)
class CaptchaChallenge:
error_url: str
key: str
verify_type: str
uri: str
captcha_session: str = ""
iframe_type: str = ""
config_url: str = ""
@property
def iframe_url(self) -> str:
if not self.captcha_session or not self.config_url:
return ""
query = urllib.parse.urlencode(
{
"captchaSession": self.captcha_session,
"type": self.iframe_type or "1",
"configUrl": self.config_url,
}
)
return f"{CAPTCHA_IFRAME_BASE}?{query}"
def _first_query_value(query: Mapping[str, list[str]], key: str) -> str:
values = query.get(key) or [""]
return values[0] if values else ""
def _regex_group(pattern: str, text: str, default: str = "") -> str:
match = re.search(pattern, text, re.S)
return match.group(1) if match else default
def parse_captcha_challenge(error_url: str, page_html: str = "") -> CaptchaChallenge:
parsed = urllib.parse.urlsplit(error_url)
query = urllib.parse.parse_qs(parsed.query)
key = _first_query_value(query, "key") or _regex_group(r'id=["\']captcha-key["\'][^>]*value=["\']([^"\']+)', page_html)
verify_type = _first_query_value(query, "type")
uri = urllib.parse.unquote(_first_query_value(query, "uri"))
captcha_body = _regex_group(r"window\.kwaiCaptchaData\s*=\s*\{(.*?)\}\s*</script>", page_html)
captcha_session = _regex_group(r'captchaSession\s*:\s*"([^"]+)"', captcha_body)
iframe_type = _regex_group(r'type\s*:\s*([0-9]+)', captcha_body)
config_url = _regex_group(r'configUrl\s*:\s*"([^"]+)"', captcha_body)
return CaptchaChallenge(
error_url=error_url,
key=key,
verify_type=verify_type,
uri=uri,
captcha_session=captcha_session,
iframe_type=iframe_type,
config_url=config_url,
)
def build_captcha_manual_html(challenge: CaptchaChallenge) -> str:
payload = json.dumps(
{
"error_url": challenge.error_url,
"key": challenge.key,
"verify_type": challenge.verify_type,
"uri": challenge.uri,
"captcha_session": challenge.captcha_session,
"iframe_type": challenge.iframe_type,
"config_url": challenge.config_url,
"iframe_url": challenge.iframe_url,
},
ensure_ascii=False,
)
return """<!doctype html>
<html lang=\"zh-CN\">
<head>
<meta charset=\"utf-8\">
<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">
<title>Kwai captcha manual helper</title>
<style>
body{{font-family:system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;margin:24px;background:#f6f7f8;color:#111}}
main{{max-width:760px;margin:auto;background:#fff;border-radius:12px;padding:18px;box-shadow:0 8px 30px rgba(0,0,0,.08)}}
iframe{{width:100%;height:420px;border:1px solid #ddd;border-radius:10px;background:#fff}}
pre{{white-space:pre-wrap;word-break:break-all;background:#111;color:#d7ffd7;border-radius:8px;padding:12px}}
.muted{{color:#666}}
</style>
</head>
<body>
<main>
<h2>Kwai captcha manual helper</h2>
<p class=\"muted\">Solve the slider captcha. This page will show the iframe ticket; pass it to <code>captcha-verify</code> to get the final <code>captcha_token</code>.</p>
<iframe id=\"captcha\" src=\"{iframe_url}\"></iframe>
<h3>Challenge</h3>
<pre id=\"challenge\"></pre>
<h3>Iframe result / ticket</h3>
<pre id=\"result\">Waiting for captcha result...</pre>
<h3>Verify command</h3>
<pre id=\"command\">Waiting for ticket...</pre>
</main>
<script>
const challenge = {payload};
document.getElementById('challenge').textContent = JSON.stringify(challenge, null, 2);
function quoteArg(value) {{ return '\"' + String(value).replace(/\"/g, '\\\"') + '\"'; }}
window.addEventListener('message', function(event) {{
let data = event.data;
try {{ if (typeof data === 'string') data = JSON.parse(data); }} catch (e) {{ return; }}
if (!data || data.msgType !== 'RESULT') return;
const msg = data.msg || {{}};
const ticket = msg.ticket || msg.token || msg.captchaToken || '';
document.getElementById('result').textContent = JSON.stringify(data, null, 2);
if (ticket) {{
const cmd = 'python kws_client.py captcha-verify --error-url ' + quoteArg(challenge.error_url) + ' --ticket ' + quoteArg(ticket);
document.getElementById('command').textContent = cmd;
navigator.clipboard && navigator.clipboard.writeText(ticket).catch(function(){{}});
}}
}});
</script>
</body>
</html>
""".format(iframe_url=challenge.iframe_url, payload=payload)
DEVICE_FIELD_ALIASES = {
"did": ("did", "cloud_did", "deviceId", "device_id", "antman_shell_device_id"),
"cdid_tag": ("cdid_tag", "did_tag", "cloud_did_tag", "cloudDeviceIdTag", "antman_shell_cloud_id_tag"),
"rdid": ("rdid", "randomDeviceId", "random_device_id", "antman_shell_random_device_id"),
"odid": ("odid", "old_did", "origin_did", "originalDid"),
"decision_did": ("decisionDid", "decision_did"),
"decision": ("decision", "decisionType", "decision_type"),
"decision_inputs": ("decisionInputs", "decision_inputs"),
"egid": ("egid", "EGID"),
"token": ("token", "gifshow_token", "gifshowToken"),
"api_st": ("kuaishou.api_st", "api_st", "apiSt", "nebula_token"),
"client_salt": ("client_salt", "clientSalt", "token_client_salt", "kuaishou.api_client_salt"),
"service_token": ("serviceToken", "service_token", "azeroth_service_token"),
"azeroth_security": ("security", "azeroth_security", "clientSecurity"),
"ud": ("ud", "user_id", "userId", "gifshow_userid"),
"iuid": ("iuid", "iUserId"),
"did_gt": ("did_gt", "new_device_install_app_time"),
}
SAFE_RESPONSE_FIELD_ALIASES = {
**DEVICE_FIELD_ALIASES,
"ud": ("ud", "gifshow_userid"),
}
DEVICE_ALIAS_NAMES = {alias for aliases in DEVICE_FIELD_ALIASES.values() for alias in aliases}
CAPTURE_REQUEST_FIELD_ALIASES = {
**DEVICE_FIELD_ALIASES,
"appver": ("appver", "appVer"),
"channel": ("c", "channel"),
"country_code": ("country_code", "countryCode"),
"language": ("language",),
"mod": ("mod", "deviceName"),
"net": ("net",),
"new_oc": ("newOc", "new_oc"),
"oc": ("oc",),
"sys": ("sys", "sysver"),
}
NON_LOGIN_TOKEN_MARKERS = ("captcha", "verification", "ztbasic", "zt.basic")
def _iter_mappings(value: Any) -> Iterable[Mapping[str, Any]]:
if isinstance(value, Mapping):
yield value
for nested in value.values():
yield from _iter_mappings(nested)
elif isinstance(value, list):
for item in value:
yield from _iter_mappings(item)
def _high_confidence_response_mappings(value: Any) -> Iterable[Mapping[str, Any]]:
if not isinstance(value, Mapping):
return
yield value
for key in ("data", "result", "tokenInfo", "loginInfo", "user", "userInfo"):
nested = value.get(key)
if isinstance(nested, Mapping):
yield nested
def _response_json(response: Any) -> Any:
try:
return response.json()
except Exception:
text = getattr(response, "text", "") or ""
if not text:
return None
try:
return json.loads(text)
except Exception:
return None
def _response_cookies(response: Any) -> dict[str, str]:
cookies: dict[str, str] = {}
jar = getattr(response, "cookies", None)
if jar is not None:
try:
cookies.update({str(key): str(value) for key, value in jar.get_dict().items() if value})
except Exception:
try:
cookies.update({str(cookie.name): str(cookie.value) for cookie in jar if cookie.value})
except Exception:
pass
headers = getattr(response, "headers", None) or {}
set_cookie = ""
try:
set_cookie = headers.get("Set-Cookie", "") or headers.get("set-cookie", "") or ""
except Exception:
set_cookie = ""
if set_cookie:
parsed = SimpleCookie()
try:
parsed.load(set_cookie)
cookies.update({key: morsel.value for key, morsel in parsed.items() if morsel.value})
except Exception:
pass
return cookies
SENSITIVE_STATE_FIELDS = {
"token",
"api_st",
"client_salt",
"service_token",
"azeroth_security",
"account_private_key_pem",
}
SENSITIVE_COOKIE_NAMES = {
"token",
"kuaishou.api_st",
"api_st",
"client_salt",
"clientSalt",
"serviceToken",
"service_token",
}
def _redact_value(value: Any) -> Any:
if value in (None, "", {}, []):
return value
text = str(value)
if len(text) <= 8:
return "<set>"
return f"<set:{len(text)} chars:{text[:4]}...{text[-4:]}>"
def redact_state_mapping(mapping: Mapping[str, Any]) -> dict[str, Any]:
redacted: dict[str, Any] = {}
for key, value in mapping.items():
if key in SENSITIVE_STATE_FIELDS:
redacted[key] = _redact_value(value)
elif key == "cookie_extras" and isinstance(value, Mapping):
redacted[key] = {
str(cookie_key): _redact_value(cookie_value)
if str(cookie_key) in SENSITIVE_COOKIE_NAMES
else cookie_value
for cookie_key, cookie_value in value.items()
}
else:
redacted[key] = value
return redacted
def _parse_cookie_header_value(cookie_header: Any) -> dict[str, str]:
if cookie_header is None:
return {}
text = str(cookie_header).strip()
if not text:
return {}
cookies: dict[str, str] = {}
parsed = SimpleCookie()
try:
parsed.load(text)
cookies.update({key: morsel.value for key, morsel in parsed.items() if morsel.value})
except Exception:
pass
if cookies:
return cookies
for item in text.split(";"):
if "=" not in item:
continue
key, value = item.split("=", 1)
key = key.strip()
value = value.strip()
if key and value:
cookies[key] = value
return cookies
def _header_items_from_value(value: Any) -> Iterable[tuple[str, str]]:
if isinstance(value, Mapping):
for key, item in value.items():
if item is not None:
yield str(key), str(item)
elif isinstance(value, list):
for item in value:
if not isinstance(item, Mapping):
continue
name = item.get("name") or item.get("key")
header_value = item.get("value") or item.get("val")
if name is not None and header_value is not None:
yield str(name), str(header_value)
def _name_value_pairs_from_value(value: Any) -> dict[str, str]:
pairs: dict[str, str] = {}
if isinstance(value, Mapping):
for key, item in value.items():
if item is not None:
pairs[str(key)] = str(item)
elif isinstance(value, list):
for item in value:
if not isinstance(item, Mapping):
continue
name = item.get("name") or item.get("key")
pair_value = item.get("value") or item.get("val")
if name is not None and pair_value is not None:
pairs[str(name)] = str(pair_value)
return pairs
def _capture_json_payload(text: str) -> Any:
stripped = text.lstrip("\ufeff").strip()
if not stripped or stripped[0] not in "[{":
return None
try:
return json.loads(stripped)
except Exception:
return None
def _coerce_capture_payload(capture: Any) -> tuple[Any, str]:
if isinstance(capture, bytes):
text = capture.decode("utf-8", errors="replace")
return _capture_json_payload(text), text
if isinstance(capture, str):
return _capture_json_payload(capture), capture
if isinstance(capture, (Mapping, list)):
try:
text = json.dumps(capture, ensure_ascii=False)
except Exception:
text = ""
return capture, text
return None, "" if capture is None else str(capture)
def _decode_token_probe(value: Any) -> str:
text = urllib.parse.unquote_plus(str(value or "")).strip()
if not text:
return ""
compact = re.sub(r"[^A-Za-z0-9_=-]", "", text[:1024])
if len(compact) < 8:
return ""
padded = compact + ("=" * ((4 - len(compact) % 4) % 4))
try:
return base64.urlsafe_b64decode(padded.encode("ascii", errors="ignore")).decode("utf-8", errors="ignore")
except Exception:
return ""
def _looks_like_non_login_token(key: str, value: Any) -> bool:
if key != "token":
return False
text = str(value or "")
probe = (text[:256] + " " + _decode_token_probe(text)[:512]).lower()
return any(marker in probe for marker in NON_LOGIN_TOKEN_MARKERS)
def _filter_capture_auth_pairs(pairs: Mapping[str, str]) -> dict[str, str]:
return {
str(key): str(value)
for key, value in pairs.items()
if value is not None and not _looks_like_non_login_token(str(key), value)
}
def _filter_capture_mapping(mapping: Mapping[str, Any]) -> dict[str, Any]:
return {
str(key): value
for key, value in mapping.items()
if value is not None and not _looks_like_non_login_token(str(key), value)
}
def _urlencoded_pairs_from_text(text: str) -> dict[str, str]:
if "=" not in text:
return {}
try:
return {str(key): str(value) for key, value in urllib.parse.parse_qsl(text, keep_blank_values=True)}
except Exception:
return {}
def _is_har_payload(payload: Any) -> bool:
return isinstance(payload, Mapping) and (
isinstance(payload.get("log"), Mapping) and isinstance(payload.get("log", {}).get("entries"), list)
or isinstance(payload.get("entries"), list)
)
def _request_pairs_from_capture_payload(payload: Any) -> dict[str, str]:
pairs: dict[str, str] = {}
for mapping in _iter_mappings(payload):
url = mapping.get("url")
if isinstance(url, str) and url:
pairs.update(dict(urllib.parse.parse_qsl(urllib.parse.urlsplit(url).query, keep_blank_values=True)))
for key in ("queryString", "query", "params", "form", "data", "body"):
value = mapping.get(key)
if isinstance(value, str):
pairs.update(_urlencoded_pairs_from_text(value))
else:
pairs.update(_name_value_pairs_from_value(value))
post_data = mapping.get("postData")
if isinstance(post_data, Mapping):
pairs.update(_name_value_pairs_from_value(post_data.get("params")))
text = post_data.get("text")
if isinstance(text, str):
pairs.update(_urlencoded_pairs_from_text(text))
return _filter_capture_auth_pairs(pairs)
def _auth_pairs_from_text(text: str) -> dict[str, str]:
pairs: dict[str, str] = {}
alias_names = DEVICE_ALIAS_NAMES | {"kuaishou.api_st", "api_st", "client_salt", "clientSalt"}
pattern = re.compile(r"(?<![\w.-])([A-Za-z0-9_.-]{2,48})=([^&;\s'\"<>]+)")
for match in pattern.finditer(text):
key = urllib.parse.unquote_plus(match.group(1))
if key not in alias_names:
continue
value = urllib.parse.unquote_plus(match.group(2))
if value:
pairs[key] = value
json_pair_pattern = re.compile(r'''["']([A-Za-z0-9_.-]{2,48})["']\s*:\s*["']([^"']+)["']''')
for match in json_pair_pattern.finditer(text):
key = match.group(1)
if key in alias_names:
pairs[key] = match.group(2)
return _filter_capture_auth_pairs(pairs)
def _cookies_from_capture_payload(payload: Any) -> dict[str, str]:
cookies: dict[str, str] = {}
for mapping in _iter_mappings(payload):
for key in ("Cookie", "cookie", "Set-Cookie", "set-cookie"):
if key in mapping:
cookies.update(_parse_cookie_header_value(mapping.get(key)))
for header_name, header_value in _header_items_from_value(mapping.get("headers")):
if header_name.lower() in {"cookie", "set-cookie"}:
cookies.update(_parse_cookie_header_value(header_value))
cookie_value = mapping.get("cookies")
if isinstance(cookie_value, str):
cookies.update(_parse_cookie_header_value(cookie_value))
else:
cookies.update(_name_value_pairs_from_value(cookie_value))
return cookies
def _auth_pairs_from_capture_payload(payload: Any) -> dict[str, str]:
pairs: dict[str, str] = {}
for mapping in _iter_mappings(payload):
for key in ("queryString", "query", "params", "form", "data", "body", "postData"):
value = mapping.get(key)
if isinstance(value, str):
pairs.update(_auth_pairs_from_text(value))
else:
pairs.update(_name_value_pairs_from_value(value))
post_data = mapping.get("postData")
if isinstance(post_data, Mapping):
params = post_data.get("params")
pairs.update(_name_value_pairs_from_value(params))
text = post_data.get("text")
if isinstance(text, str):
pairs.update(_auth_pairs_from_text(text))
return {key: value for key, value in pairs.items() if key in DEVICE_ALIAS_NAMES or key in {"kuaishou.api_st", "api_st", "client_salt", "clientSalt"}}
def _capture_text_values(payload: Any) -> Iterable[str]:
for mapping in _iter_mappings(payload):
for key in ("text", "body", "responseBody", "raw", "content"):
value = mapping.get(key)
if not isinstance(value, str) or not value:
continue
if key == "text" and str(mapping.get("encoding", "")).lower() == "base64":
try:
yield base64.b64decode(value).decode("utf-8", errors="replace")
continue
except Exception:
pass
yield value
def _json_mappings_from_capture_texts(payload: Any) -> Iterable[Mapping[str, Any]]:
for text in _capture_text_values(payload):
parsed = _capture_json_payload(text)
if isinstance(parsed, Mapping):
yield parsed
def _json_mappings_from_raw_text(text: str) -> Iterable[Mapping[str, Any]]:
decoder = json.JSONDecoder()
text = text.lstrip("\ufeff")
for index, char in enumerate(text):
if char not in "[{":
continue
try:
parsed, _ = decoder.raw_decode(text[index:])
except Exception:
continue
for mapping in _iter_mappings(parsed):
yield mapping
return
def _cookies_from_raw_capture_text(text: str) -> dict[str, str]:
cookies: dict[str, str] = {}
header_pattern = re.compile(r"(?im)^\s*(?:Cookie|Set-Cookie)\s*:\s*(.+)$")
for match in header_pattern.finditer(text):
cookies.update(_parse_cookie_header_value(match.group(1)))
curl_header_pattern = re.compile(r"(?is)-H\s+(['\"])(Cookie|Set-Cookie):\s*(.*?)\1")
for match in curl_header_pattern.finditer(text):
cookies.update(_parse_cookie_header_value(match.group(3)))
return cookies
def import_login_capture_into_device(
device: "KwaiDeviceContext",
capture: Any,
*,
overwrite: bool = True,
) -> dict[str, str]:
payload, raw_text = _coerce_capture_payload(capture)
changed: dict[str, str] = {}
is_har = _is_har_payload(payload)
if payload is not None:
if is_har:
request_pairs = _request_pairs_from_capture_payload(payload)
if request_pairs:
changed.update(
device.update_from_mapping(
request_pairs,
overwrite=overwrite,
aliases=CAPTURE_REQUEST_FIELD_ALIASES,
recursive=False,
)
)
else:
changed.update(device.update_from_mapping(payload, overwrite=overwrite))
cookie_map = _filter_capture_auth_pairs(_cookies_from_capture_payload(payload))
if cookie_map:
changed.update(device.update_from_mapping(cookie_map, overwrite=overwrite, recursive=False))
device.cookie_extras.update({key: value for key, value in cookie_map.items() if key not in DEVICE_ALIAS_NAMES and value})
auth_pairs = _auth_pairs_from_capture_payload(payload)
if auth_pairs:
changed.update(device.update_from_mapping(auth_pairs, overwrite=overwrite, recursive=False))
if raw_text and not is_har:
cookie_map = _filter_capture_auth_pairs(_cookies_from_raw_capture_text(raw_text))
if cookie_map:
changed.update(device.update_from_mapping(cookie_map, overwrite=overwrite, recursive=False))
device.cookie_extras.update({key: value for key, value in cookie_map.items() if key not in DEVICE_ALIAS_NAMES and value})
auth_pairs = _auth_pairs_from_text(raw_text)
if auth_pairs:
changed.update(device.update_from_mapping(auth_pairs, overwrite=overwrite, recursive=False))
for parsed_mapping in _json_mappings_from_raw_text(raw_text):
changed.update(device.update_from_mapping(parsed_mapping, overwrite=overwrite))
if payload is not None:
for parsed_mapping in _json_mappings_from_capture_texts(payload):
if is_har:
auth_response = any(
key in parsed_mapping
for key in ("token", "api_st", "kuaishou.api_st", "clientSalt", "client_salt", "tokenInfo", "loginInfo")
)
response_aliases = DEVICE_FIELD_ALIASES if auth_response else SAFE_RESPONSE_FIELD_ALIASES
for response_mapping in _high_confidence_response_mappings(parsed_mapping):
filtered_mapping = _filter_capture_mapping(response_mapping)
changed.update(
device.update_from_mapping(
filtered_mapping,
overwrite=overwrite,
aliases=response_aliases,
recursive=False,
)
)
else:
filtered_mapping = _filter_capture_mapping(parsed_mapping)
changed.update(device.update_from_mapping(filtered_mapping, overwrite=overwrite))
nested_auth_pairs = _auth_pairs_from_capture_payload(filtered_mapping)
if nested_auth_pairs:
changed.update(device.update_from_mapping(nested_auth_pairs, overwrite=overwrite, recursive=False))
return changed
def _android_base64_default(data: bytes) -> str:
# Android Base64.DEFAULT wraps at 76 chars and appends a trailing LF.
return base64.encodebytes(data).decode("ascii")
def _dfp_encode_payload(data: bytes) -> str:
return urllib.parse.quote(_android_base64_default(data), safe="")
def _dfp_sign_value(data: bytes, table: KwaiZtTable | None, sign_mode: str, *, inner: bool = True) -> str:
if sign_mode == "none":
return ""
if sign_mode == "sha256":
return hashlib.sha256(data).hexdigest()
if sign_mode == "double-sha256":
return hashlib.sha256(hashlib.sha256(data).hexdigest().encode("utf-8")).hexdigest()
if sign_mode == "10405":
from .algorithms import sig3_10405
return sig3_10405(data, table=table)
if sign_mode == "10418-sha256":
return sig3_10417(hashlib.sha256(data).hexdigest().encode("utf-8"), table)
if sign_mode == "10417":
return sig3_10417(data, table)
if sign_mode == "10418":
if table is None:
return ""
return sig3_10418(data, table, inner=inner)
raise ValueError(f"unsupported DFP sign mode: {sign_mode}")
@contextmanager
def _temporary_dns_override(hostname: str, ip_address: str):
original_getaddrinfo = socket.getaddrinfo
def patched_getaddrinfo(host: str, port: int, *args: Any, **kwargs: Any) -> Any:
if host == hostname:
host = ip_address
return original_getaddrinfo(host, port, *args, **kwargs)
socket.getaddrinfo = patched_getaddrinfo
try:
yield
finally:
socket.getaddrinfo = original_getaddrinfo
def _is_dns_resolution_error(exc: BaseException) -> bool:
text = repr(exc).lower()
return any(
marker in text
for marker in (
"nameresolutionerror",
"failed to resolve",
"name or service not known",
"getaddrinfo failed",
"nodename nor servname",
"temporary failure in name resolution",
)
)
def _stringify_value(value: Any) -> str:
if value is None:
return ""
if isinstance(value, bool):
return "true" if value else "false"
if isinstance(value, (list, tuple, set)):
return ",".join(_stringify_value(item) for item in value)
return str(value)
def _stringify_mapping(value: Mapping[str, Any] | None) -> dict[str, str]:
return {str(key): _stringify_value(item) for key, item in (value or {}).items()}
def _dfp_non_empty_items(params: Mapping[str, str]) -> list[tuple[str, str]]:
return [(key, value) for key, value in sorted(params.items()) if value]
def _dfp_request_id() -> str:
return uuid.uuid4().hex[:16]
@dataclass
class KwaiDeviceContext:
did: str = field(default_factory=lambda: uuid.uuid4().hex)
egid: str = ""
rdid: str = ""
ud: str = "0"
cdid_tag: str = ""
did_gt: str = ""
odid: str = ""
decision_did: str = ""
decision: str = ""
decision_inputs: str = ""
oc: str = "UNKNOWN"
new_oc: str = "ANTMAN_PC"
channel: str = "ANTMAN_PC"
sys: str = "ANDROID_12"
mod: str = "Xiaomi(M2012K11AC)"
net: str = "WIFI"
language: str = "zh-cn"
country_code: str = "CN"
lat: str = "0"
lon: str = "0"
appver: str = "10.3.39.6655"
version_short: str = "10.3"
max_memory: str = "256"
abi: str = "arm64"
iuid: str = ""
pm_tag: str = ""
token: str = ""
api_st: str = ""
client_salt: str = ""
service_token: str = ""
azeroth_security: str = ""
account_private_key_pem: str = ""
cookie_extras: dict[str, str] = field(default_factory=dict)
@classmethod
def from_json_file(cls, path: str | Path) -> "KwaiDeviceContext":
data = json.loads(Path(path).read_text(encoding="utf-8"))
if not isinstance(data, Mapping):
raise ValueError("device state JSON must be an object")
return cls.from_mapping(data)
@classmethod
def from_mapping(cls, data: Mapping[str, Any]) -> "KwaiDeviceContext":
device = cls()
device.update_from_mapping(data, overwrite=True)
return device
def to_dict(self) -> dict[str, Any]:
return {item.name: getattr(self, item.name) for item in fields(self)}
def save_json_file(self, path: str | Path) -> None:
Path(path).write_text(json.dumps(self.to_dict(), ensure_ascii=False, indent=2), encoding="utf-8")
def to_redacted_dict(self) -> dict[str, Any]:
return redact_state_mapping(self.to_dict())
def import_login_capture(self, capture: Any, *, overwrite: bool = True) -> dict[str, str]:
return import_login_capture_into_device(self, capture, overwrite=overwrite)
def update_from_mapping(
self,
data: Mapping[str, Any],
*,
overwrite: bool = True,
aliases: Mapping[str, tuple[str, ...]] | None = None,
recursive: bool = True,
) -> dict[str, str]:
changed: dict[str, str] = {}
alias_map = aliases or DEVICE_FIELD_ALIASES
known_fields = {item.name for item in fields(self)}
mappings = _iter_mappings(data) if recursive else (data,)
for mapping in mappings:
for field_name in known_fields:
if field_name not in mapping:
continue
value = mapping.get(field_name)
if value is None:
continue
current = getattr(self, field_name)
if isinstance(current, dict):
if not isinstance(value, Mapping):
continue
normalized = {str(key): str(item) for key, item in value.items() if item}
if overwrite or not current:
if current != normalized:
setattr(self, field_name, normalized)
changed[field_name] = json.dumps(normalized, ensure_ascii=False)
continue
text = str(value)
if not text:
continue
if overwrite or not current:
if current != text:
setattr(self, field_name, text)
changed[field_name] = text
for field_name, field_aliases in alias_map.items():
if field_name not in known_fields:
continue
for alias in field_aliases:
if alias not in mapping:
continue
value = mapping.get(alias)
if value is None:
continue
text = str(value)
if not text:
continue
current = getattr(self, field_name)
if overwrite or not current:
if current != text:
setattr(self, field_name, text)
changed[field_name] = text
break
return changed
@dataclass
class SignedRequest:
method: str
url: str
headers: dict[str, str]
params: dict[str, str]
data: bytes | dict[str, str] | None = None
@dataclass(frozen=True)
class RestEndpoint:
name: str
method: str
path: str
include_auth: bool = True
safe: bool = False
defaults: Mapping[str, str] = field(default_factory=dict)
query_defaults: Mapping[str, str] = field(default_factory=dict)
description: str = ""
service: str = ""
java_method: str = ""
fields: tuple[str, ...] = ()
query_fields: tuple[str, ...] = ()
param_profile: str = "antman"
rewrite_path: bool = True
REST_ENDPOINTS: dict[str, RestEndpoint] = {
"startup": RestEndpoint(
"startup",
"POST",
"/rest/system/startup",
safe=True,
defaults={"extId": "", "gp_referer": "", "oaid": ""},
description="启动配置;extId 走 query,gp_referer/oaid 走 body",
),
"version_check": RestEndpoint("version_check", "GET", "/rest/antman/versionCheck", safe=True),
"system_speed": RestEndpoint("system_speed", "GET", "/rest/n/system/speed", safe=True),
"system_dialog": RestEndpoint("system_dialog", "POST", "n/system/dialog", safe=False, defaults={"source": "startup"}),
"cold_start_deeplink": RestEndpoint(
"cold_start_deeplink",
"POST",
"/rest/n/system/dialog",
safe=False,
defaults={"source": "startup", "imeis": "", "oaid": "", "clipboard": ""},
),
"dialog_report": RestEndpoint("dialog_report", "POST", "/rest/system/dialog/report", defaults={"source": "startup"}),
"user_settings": RestEndpoint("user_settings", "POST", "n/user/settings"),
"user_profile": RestEndpoint("user_profile", "POST", "n/user/profile"),
"profile_list_friends": RestEndpoint("profile_list_friends", "POST", "n/user/profile/listFriends", defaults={"user": "", "cursor": "", "count": "20"}),
"follow_recommend": RestEndpoint("follow_recommend", "POST", "n/user/recommend/follow"),
"follow_recommend_delete": RestEndpoint("follow_recommend_delete", "POST", "n/user/recommend/follow/delete", defaults={"user_id": ""}),
"mobile_check": RestEndpoint("mobile_check", "POST", "n/user/mobile/checker", include_auth=False, defaults={"mobileCountryCode": "86", "mobile": ""}),
"request_mobile_code": RestEndpoint("request_mobile_code", "POST", "n/user/requestMobileCode", include_auth=False, defaults={"mobileCountryCode": "86", "mobile": "", "type": "27"}),
"verify_code_login": RestEndpoint("verify_code_login", "POST", "n/user/login/mobileVerifyCode", include_auth=False, defaults={"code": "", "mobileCountryCode": "86", "mobile": "", "type": "27"}),
"token_login": RestEndpoint("token_login", "POST", "n/user/login/token", include_auth=False, defaults={"token": "", "type": "302"}),
"register_phone_v2": RestEndpoint("register_phone_v2", "POST", "n/user/register/mobileV2", include_auth=False, defaults={"userName": "", "mobileCountryCode": "86", "mobile": "", "mobileCode": "", "password": "", "gender": "U"}),
"refresh_token": RestEndpoint("refresh_token", "POST", "n/token/infra/refreshToken", defaults={"token": "", "client_salt": ""}),
"logout": RestEndpoint("logout", "POST", "n/user/logout", defaults={"token": "", "client_salt": ""}),
"third_platform_login": RestEndpoint("third_platform_login", "POST", "user/thirdPlatformLogin", include_auth=False),
"bind_platform": RestEndpoint("bind_platform", "POST", "n/user/thirdPlatform/bind", defaults={"platform": "", "accessToken": "", "openId": ""}),
"verify_trust_device": RestEndpoint("verify_trust_device", "POST", "n/user/verifyTrustDevice", include_auth=False, defaults={"isAddAccount": "false"}),
"reset_mobile": RestEndpoint("reset_mobile", "POST", "n/user/reset/mobile", include_auth=False),
"reset_password": RestEndpoint("reset_password", "POST", "n/user/password/reset", include_auth=False),
"contacts": RestEndpoint("contacts", "POST", "n/user/contacts", include_auth=False, defaults={"contacts": "", "page": ""}),
"qq_friends": RestEndpoint("qq_friends", "POST", "n/user/qqFriends", include_auth=False, defaults={"data": "", "page": "0"}),
"change_user_info": RestEndpoint("change_user_info", "POST", "n/user/modify", defaults={"user_name": "", "user_sex": "", "forceUnique": "false"}),
"change_birthday": RestEndpoint("change_birthday", "POST", "n/user/modify", defaults={"birthdayTs": ""}),
"change_city_code": RestEndpoint("change_city_code", "POST", "n/user/modify", defaults={"cityCode": ""}),
"change_user_sex": RestEndpoint("change_user_sex", "POST", "n/user/modify", defaults={"user_sex": ""}),
"change_option": RestEndpoint("change_option", "POST", "n/user/changeOption", defaults={"key": "", "value": ""}),
"change_setting": RestEndpoint("change_setting", "POST", "n/user/changeSetting", defaults={"key": "", "value": ""}),
"modify_profile_background": RestEndpoint("modify_profile_background", "POST", "n/user/modifyProfileBG"),
"delete_profile_background": RestEndpoint("delete_profile_background", "POST", "n/user/modifyProfileBG", defaults={"delete": "true"}),
"antman_video_feed_hot": RestEndpoint(
"antman_video_feed_hot",
"GET",
"/rest/n/feed/hot",
safe=False,
defaults={"count": "1", "pcursor": ""},
description="Antman-route feed probe retained for comparison; CTF video feed uses video_feed_hot",
),
"antman_video_photo_comments": RestEndpoint(
"antman_video_photo_comments",
"GET",
"/rest/photo/comment/list",
safe=False,
defaults={"photoId": "", "pcursor": "", "count": "20"},
description="Antman-route comment probe retained for comparison; CTF comments use video_photo_comments",
),
"video_feed_hot": RestEndpoint(
"video_feed_hot",
"POST",
"/rest/n/feed/hot",
safe=False,
defaults={"count": "1", "pcursor": ""},
description="read-only main-app hot video feed; verified result=1 with main_video params",
param_profile="main_video",
rewrite_path=False,
),
"video_feed_profile": RestEndpoint(
"video_feed_profile",
"POST",
"/rest/n/feed/profile2",
safe=False,
defaults={"count": "1", "pcursor": "", "user_id": ""},
description="read-only main-app profile feed v2; requires a user_id from feed",
param_profile="main_video",
rewrite_path=False,
),
"video_photo_info": RestEndpoint(
"video_photo_info",
"POST",
"/rest/n/photo/info",
safe=False,
defaults={"photoIds": ""},
description="read-only main-app photo info; pass photoId=... or photoIds=... from feed",
param_profile="main_video",
rewrite_path=False,
),
"video_photo_comments": RestEndpoint(
"video_photo_comments",
"POST",
"/rest/n/comment/list/v2",
safe=False,
defaults={"photoId": "", "count": "20", "order": "desc", "enableEmotion": "true"},
description="read-only main-app comment list v2; verified result=1 with a photoId from feed",
param_profile="main_video",
rewrite_path=False,
),
"azeroth_configs": RestEndpoint(
"azeroth_configs",
"GET",
"/rest/zt/appsupport/configs",
safe=False,
defaults={},
description="AzerothService.a @QueryMap; uses AzerothParamProcessor rather than KwaiParams",
service="AzerothService",
java_method="a",
),
"plugin_startup": RestEndpoint(
"plugin_startup",
"POST",
"system/startup",
safe=False,
defaults={"extId": "", "gp_referer": "", "oaid": ""},
description="PluginLoadKSwitchManager.b.startup; extId is @Query, gp_referer/oaid are @Field",
service="PluginLoadKSwitchManager.b",
java_method="startup",
fields=("gp_referer", "oaid"),
),
"test_speed_dynamic_url": RestEndpoint(
"test_speed_dynamic_url",
"POST",
"@Url",
safe=False,
defaults={"url": "", "op": ""},
description="TestSpeedService.a; url is Retrofit @Url, op is @Field",
service="TestSpeedService",
java_method="a",
fields=("url", "op"),
),
}
_MANUAL_REST_ENDPOINT_NAMES = frozenset(REST_ENDPOINTS)
def _snake_name(value: str) -> str:
value = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", value)
value = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", value)
return re.sub(r"[^a-zA-Z0-9]+", "_", value).strip("_").lower()
def _normalize_service_name(service: str) -> str:
service = service.strip().strip(";")
if "/" in service:
service = service.rsplit("/", 1)[-1]
if service.startswith("L"):
service = service[1:]
return service
def _endpoint_needs_login_auth(service: str, java_method: str, path: str) -> bool:
lowered = f"{service} {java_method} {path}".lower()
anonymous_markers = (
"login",
"register",
"requestmobilecode",
"resetpassword",
"resetmobile",
"thirdplatformlogin",
"mobile/checker",
)
return not any(marker in lowered for marker in anonymous_markers)
def _parse_retrofit_annotation_file(path: Path) -> dict[str, RestEndpoint]:
if not path.exists():
return {}
endpoints: dict[str, RestEndpoint] = {}
current: dict[str, Any] | None = None
def flush() -> None:
nonlocal current
if not current or not current.get("http_method") or not current.get("path"):
current = None
return
service = _normalize_service_name(str(current["service"]))
java_method = str(current["java_method"])
prefix = {"KwaiApiService": "api", "KwaiHttpsService": "https", "KwaiPayService": "pay"}.get(service, _snake_name(service))
base_name = f"{prefix}_{_snake_name(java_method)}"
name = base_name
suffix = 2
while name in _MANUAL_REST_ENDPOINT_NAMES or name in endpoints:
name = f"{base_name}_{suffix}"
suffix += 1
fields_tuple = tuple(str(field) for field in current.get("fields", ()))
endpoints[name] = RestEndpoint(
name=name,
method=str(current["http_method"]),
path=str(current["path"]),
include_auth=_endpoint_needs_login_auth(service, java_method, str(current["path"])),
safe=False,
defaults={field: "" for field in fields_tuple},
description="auto parsed from Retrofit annotations",
service=service,
java_method=java_method,
fields=fields_tuple,
)
current = None
service_line = re.compile(r"^(L[^;]+;)\s+(\S+)\s+(\S+)\s+PARAMS\s+(.*)")
http_line = re.compile(r"(GET|POST)\{'value': '([^']+)'\}")
field_line = re.compile(r"'value': '([^']+)'")
for raw_line in path.read_text(encoding="utf-8", errors="replace").splitlines():
line = raw_line.strip()
match = service_line.match(line)
if match:
flush()
current = {"service": match.group(1), "java_method": match.group(2), "http_method": "", "path": "", "fields": []}
continue
if current is None:
continue
match = http_line.search(line)
if match:
current["http_method"] = match.group(1)
current["path"] = match.group(2).strip()
continue
if "Lretrofit2/http/" in line:
match = field_line.search(line)
if match:
current["fields"].append(match.group(1))
flush()
return endpoints
def _load_retrofit_annotation_json(path: Path) -> dict[str, RestEndpoint]:
if not path.exists():
return {}
try:
records = json.loads(path.read_text(encoding="utf-8-sig", errors="replace"))
except json.JSONDecodeError:
return {}
if not isinstance(records, list):
return {}
endpoints: dict[str, RestEndpoint] = {}
for record in records:
if not isinstance(record, Mapping):
continue
http_method = str(record.get("http_method") or "").upper()
endpoint_path = str(record.get("path") or "").strip()
java_method = str(record.get("method_name") or record.get("java_method") or "")
if not http_method or not endpoint_path or not java_method:
continue
service = _normalize_service_name(str(record.get("service") or record.get("class_name") or ""))
prefix = {"KwaiApiService": "api", "KwaiHttpsService": "https", "KwaiPayService": "pay"}.get(
service, _snake_name(service)
)
base_name = f"{prefix}_{_snake_name(java_method)}" if prefix else _snake_name(java_method)
name = base_name
suffix = 2
while name in _MANUAL_REST_ENDPOINT_NAMES or name in endpoints:
name = f"{base_name}_{suffix}"
suffix += 1
body_fields = tuple(str(field) for field in (record.get("body_fields") or record.get("fields") or ()) if field)
query_fields = tuple(str(field) for field in (record.get("query_fields") or ()) if field)
endpoints[name] = RestEndpoint(
name=name,
method=http_method,
path=endpoint_path,
include_auth=_endpoint_needs_login_auth(service, java_method, endpoint_path),
safe=False,
defaults={field: "" for field in body_fields},
query_defaults={field: "" for field in query_fields},
description="auto parsed from full Retrofit annotation JSON",
service=service,
java_method=java_method,
fields=body_fields,
query_fields=query_fields,
)
return endpoints
def load_recovered_rest_endpoints(path: str | Path | None = None) -> dict[str, RestEndpoint]:
if path is None:
root = Path(__file__).resolve().parents[1]
full_json = root / "tmp_rest_annotations_full.json"
if full_json.exists():
return _load_retrofit_annotation_json(full_json)
path = root / "tmp_plugin_annotations.txt"
path = Path(path)
if path.suffix.lower() == ".json":
return _load_retrofit_annotation_json(path)
return _parse_retrofit_annotation_file(path)
REST_ENDPOINTS.update(load_recovered_rest_endpoints())
HAR_OBSERVED_REST_ENDPOINTS = {
"api_promotion_widget": RestEndpoint(
"api_promotion_widget",
"GET",
"/rest/antman/promotion/widget",
safe=True,
description="observed in HAR: promotion/widget information",
param_profile="main_video",
rewrite_path=False,
),
"api_encode_android": RestEndpoint(
"api_encode_android",
"POST",
"/rest/antman/encode/android",
safe=True,
description="observed in HAR: encode/android configuration probe",
param_profile="main_video",
rewrite_path=False,
),
"api_system_stat_observed": RestEndpoint(
"api_system_stat_observed",
"POST",
"/rest/antman/system/stat",
safe=False,
defaults={
"mark": "",
"manufacturer": "",
"startup": "4",
"channel": "ANTMAN_PC",
"original_channel": "UNKNOWN",
"data": "",
"third_platform_tokens": "[]",
"enable_push": "1",
},
description="observed in HAR: telemetry/stat; not marked safe",
param_profile="main_video",
rewrite_path=False,
),
}
for _name, _endpoint in HAR_OBSERVED_REST_ENDPOINTS.items():
REST_ENDPOINTS.setdefault(_name, _endpoint)
def _retune_rest_endpoint(name: str, **edits: Any) -> None:
endpoint = REST_ENDPOINTS.get(name)
if endpoint is not None:
REST_ENDPOINTS[name] = replace(endpoint, **edits)
for _name in (
"mobile_check",
"request_mobile_code",
"verify_code_login",
"token_login",
"register_phone_v2",
"verify_trust_device",
):
_retune_rest_endpoint(_name, param_profile="main_video_login", rewrite_path=False)
for _name in (
"user_settings",
"user_profile",
"profile_list_friends",
"api_fetch_hybrid_version",
"api_check_hybrid_update",
"api_check_url_update",
"api_experiment",
"api_lab_config_response",
"api_encode_config",
"api_app_info",
):
_retune_rest_endpoint(_name, param_profile="main_video", rewrite_path=False)
for _name in ("api_startup", "plugin_startup", "api_experiment"):
_retune_rest_endpoint(_name, safe=True)
_retune_rest_endpoint("azeroth_configs", param_profile="azeroth", rewrite_path=True)
DEFAULT_REST_SMOKE_ENDPOINT_NAMES = (
"api_experiment",
"api_startup",
"plugin_startup",
"startup",
"system_speed",
"version_check",
)
SEARCH_REST_ENDPOINT_NAMES = frozenset(
{
"api_search",
"api_search_home",
"api_search_kwai_hot_billboard",
"api_search_recommend",
"api_search_suggest",
"api_search_tag",
"api_search_tag_recommend",
"api_search_user",
}
)
VIDEO_REST_ENDPOINT_NAMES = frozenset(
{
"api_comment_list_by_pivot",
"api_comment_list_v2",
"api_comment_sub_list",
"api_feed_like_list",
"api_feed_my_follow",
"api_feed_my_follow_live_stream",
"api_feed_my_follow_photo",
"api_feed_near_by",
"api_feed_tag",
"api_get_domino_feeds",
"api_get_feed_selection",
"api_get_hot_items",
"api_get_hot_photos_in_magic_face_tag",
"api_get_hot_photos_in_music_tag",
"api_get_hot_photos_in_same_frame_tag",
"api_get_hot_photos_in_text_tag",
"api_get_latest_photos_in_magic_tag",
"api_get_latest_photos_in_music_tag",
"api_get_latest_photos_in_same_frame_tag",
"api_get_latest_photos_in_text_tag",
"api_get_magic_face_tag_info",
"api_get_music_tag_info",
"api_get_hot_items",
"api_get_photo_infos",
"api_likers",
"api_music",
"api_profile_feed",
"api_get_same_frame_tag_info",
"api_get_text_tag_info",
"api_tag_detail",
"video_feed_hot",
"video_feed_profile",
"video_photo_comments",
"video_photo_info",
}
)
USER_REST_ENDPOINT_NAMES = frozenset(
{
"api_device_verify_status",
"api_get_all_alias_list",
"api_get_all_follow_users",
"api_get_at_users",
"api_get_follow_users",
"api_get_friend_users",
"api_get_relation_friends",
"api_get_users_profile_batch",
"api_profile_feed",
"api_profile_list_friends",
"api_trust_device_list",
"api_user_info",
"api_user_profile_v2",
"profile_list_friends",
"user_profile",
"user_settings",
}
)
HAR_OBSERVED_REST_ENDPOINT_NAMES = frozenset(
{
"api_ad_list",
"api_check_photo",
"api_encode_android",
"api_get_follow_users",
"api_live_auth_status",
"api_promotion_widget",
"api_update_config",
"system_dialog",
}
)
MESSAGE_REST_ENDPOINT_NAMES = frozenset(
{
"api_get_friend_users",
"api_get_share_user_list",
"api_get_users_profile_batch",
"api_message_dialog",
"api_message_load",
"api_nebula_notify_load",
"api_news_load",
"api_notify_load",
}
)
REST_INFO_CATEGORIES = {
"message": MESSAGE_REST_ENDPOINT_NAMES,
"observed": HAR_OBSERVED_REST_ENDPOINT_NAMES,
"search": SEARCH_REST_ENDPOINT_NAMES,
"user": USER_REST_ENDPOINT_NAMES,
"video": VIDEO_REST_ENDPOINT_NAMES,
}
INFO_REST_ENDPOINT_NAMES = frozenset().union(*REST_INFO_CATEGORIES.values())
for _name in INFO_REST_ENDPOINT_NAMES:
_retune_rest_endpoint(_name, safe=True)
for _name in INFO_REST_ENDPOINT_NAMES - {"profile_list_friends", "user_profile", "user_settings"}:
if _name.startswith("video_"):
_retune_rest_endpoint(_name, param_profile="main_video", rewrite_path=False)
else:
_retune_rest_endpoint(_name, param_profile="main_video", rewrite_path=True)
_retune_rest_endpoint("api_search_tag_recommend", param_profile="main_video", rewrite_path=False)
_retune_rest_endpoint("api_send_message", param_profile="main_video", rewrite_path=True)
for _name in HAR_OBSERVED_REST_ENDPOINT_NAMES:
_retune_rest_endpoint(_name, safe=True, param_profile="main_video", rewrite_path=True)
_retune_rest_endpoint("api_promotion_widget", safe=True, param_profile="main_video", rewrite_path=False)
_retune_rest_endpoint("api_encode_android", safe=True, param_profile="main_video", rewrite_path=False)
_retune_rest_endpoint("api_system_stat_observed", safe=False, param_profile="main_video", rewrite_path=False)
class KwaiClient:
def __init__(
self,
device: KwaiDeviceContext | None = None,
*,
host: str = "https://apissl.ksapisrv.com",
appkey: str = APPKEY_MAIN,
assets_root: str = "resources/assets",
zt_table: KwaiZtTable | None = None,
timeout: float = 15.0,
session: Any = None,
) -> None:
self.device = device or KwaiDeviceContext()
self.host = host.rstrip("/")
self.appkey = appkey
self.assets_root = str(assets_root)
self.timeout = timeout
self.sig3_mode = "10418"
try:
self.zt_table = zt_table or find_zt_table(appkey, assets_root)
except Exception:
self.zt_table = zt_table
try:
self.azeroth_zt_table = find_zt_table(APPKEY_AZEROTH, assets_root)
except Exception:
self.azeroth_zt_table = None
if session is not None:
self.session = session
else:
try:
import requests
self.session = requests.Session()
except Exception:
self.session = None
def import_login_capture(self, capture: Any, *, overwrite: bool = True) -> dict[str, str]:
return self.device.import_login_capture(capture, overwrite=overwrite)
def base_headers(self) -> dict[str, str]:
headers = {
"User-Agent": "kwai-android",
"Accept-Language": self.device.language,
"X-REQUESTID": str(int(time.monotonic() * 1000)),
"Connection": "keep-alive",
}
cookie = self.cookie_header()
if cookie:
headers["Cookie"] = cookie
return headers
def cookie_header(self) -> str:
cookies = dict(self.device.cookie_extras)
if self.device.token:
cookies.setdefault("token", self.device.token)
return ";".join(f"{key}={value}" for key, value in cookies.items() if value)
def common_params(self) -> dict[str, str]:
params = {
"lat": self.device.lat,
"lon": self.device.lon,
"ver": self.device.version_short,
"ud": self.device.ud,
"sys": self.device.sys,
"c": self.device.channel,
"oc": self.device.oc,
"newOc": self.device.new_oc,
"net": self.device.net,
"did": self.device.did,
"did_tag": "1",
"cdid_tag": self.device.cdid_tag,
"rdid": self.device.rdid,
"egid": self.device.egid,
"mod": self.device.mod,
"app": "0" if self.device.channel.upper() != "GOOGLE_PLAY" else "1",
"language": self.device.language,
"country_code": self.device.country_code,
"appver": self.device.appver,
"ftt": "",
"iuid": self.device.iuid,
"max_memory": self.device.max_memory,
"kpn": "KUAISHOU_ANTMAN",
"apptype": "31",
"kpf": "ANDROID_PHONE",
"browseType": "1",
"isAntman": "true",
"abi": self.device.abi,
}
if self.device.did_gt:
params["did_gt"] = self.device.did_gt
if self.device.pm_tag:
params["pm_tag"] = self.device.pm_tag
return params
def main_video_params(self, *, use_device_version: bool = False, login_compat_version: bool = False) -> dict[str, str]:
did = self.device.did
if not did.startswith("ANDROID_"):
did = "ANDROID_" + re.sub(r"[^0-9a-fA-F]", "", did)[:16].ljust(16, "0")
did_gt = self.device.did_gt or "1562212339132"
channel = "MYAPP,1"
if use_device_version:
appver = self.device.appver
version_short = self.device.version_short
max_memory = self.device.max_memory
elif login_compat_version:
appver = MAIN_VIDEO_LOGIN_APPVER
version_short = MAIN_VIDEO_LOGIN_VER
max_memory = MAIN_VIDEO_MAX_MEMORY
else:
appver = MAIN_VIDEO_LEGACY_APPVER
version_short = MAIN_VIDEO_LEGACY_VER
max_memory = MAIN_VIDEO_MAX_MEMORY
return {
"app": "0",
"lon": self.device.lon,
"did_gt": did_gt,
"c": channel,
"sys": self.device.sys if self.device.sys and self.device.sys != "ANDROID_12" else "ANDROID_8.1",
"isp": "",
"mod": self.device.mod if self.device.mod else "LGE(AOSP on TTOG)",
"did": did,
"hotfix_ver": "",
"ver": version_short,
"net": self.device.net,
"country_code": self.device.country_code.upper() if self.device.country_code else "CN",
"iuid": self.device.iuid,
"appver": appver,
"max_memory": max_memory,
"oc": channel,
"ftt": "",
"kpn": "KUAISHOU",
"ud": self.device.ud,
"language": self.device.language,
"kpf": "ANDROID_PHONE",
"lat": self.device.lat,
}
def azeroth_did(self) -> str:
did = self.device.did
if did.startswith("ANDROID_"):
return did
cleaned = re.sub(r"[^0-9a-fA-F]", "", did)[:16].ljust(16, "0")
return "ANDROID_" + cleaned
def azeroth_params(self, *, use_latest_version: bool = True) -> dict[str, str]:
appver = AZEROTH_LATEST_APPVER if use_latest_version else self.device.appver
ver = AZEROTH_LATEST_VER if use_latest_version else self.device.version_short
channel = self.device.oc or "UNKNOWN"
params = {
"kpn": "KUAISHOU",
"kpf": "ANDROID_PHONE",
"appver": appver,
"ver": ver,
"gid": "",
"did": self.azeroth_did(),
"userId": self.device.ud or "0",
"lat": str(self.device.lat if self.device.lat else "0.0"),
"lon": str(self.device.lon if self.device.lon else "0.0"),
"mod": self.device.mod or "Xiaomi(M2012K11AC)",
"net": self.device.net or "WIFI",
"sys": self.device.sys or "ANDROID_12",
"os": "android",
"c": channel,
"language": self.device.language or "zh-cn",
"countryCode": self.device.country_code.upper() if self.device.country_code else "CN",
"mcc": "",
"androidApiLevel": AZEROTH_ANDROID_API_LEVEL,
}
if self.device.service_token:
params["kuaishou.api_st"] = self.device.service_token
elif self.device.api_st:
params["kuaishou.api_st"] = self.device.api_st
if self.device.token:
params["token"] = self.device.token
return params
def params_for_profile(self, profile: str) -> dict[str, str]:
if profile == "auto":
profile = "antman"
if profile in {"azeroth", "azeroth_latest"}:
return self.azeroth_params(use_latest_version=True)
if profile == "azeroth_device":
return self.azeroth_params(use_latest_version=False)
if profile == "main_video":
return self.main_video_params()
if profile in {"main_video_login", "main_video_compat"}:
return self.main_video_params(login_compat_version=True)
if profile == "main_video_device":
return self.main_video_params(use_device_version=True)
if profile != "antman":
raise ValueError(f"unsupported parameter profile: {profile}")
return self.common_params()
@staticmethod
def auto_param_profile(path_or_url: str) -> str:
path = urllib.parse.urlsplit(path_or_url).path if "://" in path_or_url else path_or_url
if not path.startswith("/"):
path = "/" + path
login_fragments = (
"/user/mobile/checker",
"/user/requestMobileCode",
"/user/login/mobileVerifyCode",
"/user/register/mobileV2",
"/user/login/token",
"/user/thirdPlatformLogin",
"/wechat/oauth2/authByCode",
"/user/verifyTrustDevice",
)
if any(fragment in path for fragment in login_fragments):
return "main_video_login"
video_fragments = (
"/feed/",
"/photo/",
"/comment/",
"/search/",
)
if any(fragment in path for fragment in video_fragments):
return "main_video"
return "antman"
def auth_params(self, *, include_client_salt: bool = False) -> dict[str, str]:
params = {"os": "android", "client_key": "3c2cd3f3"}
if self.device.token:
params["token"] = self.device.token
if self.device.api_st:
params["kuaishou.api_st"] = self.device.api_st
if include_client_salt and self.device.client_salt:
params["client_salt"] = self.device.client_salt
return params
@staticmethod
def rewrite_path(path: str) -> str:
if "token/infra/refreshToken" in path:
return path
if not path.startswith("/"):
path = "/" + path
if path.startswith("/rest/antman/"):
return path
if path.startswith("/rest/n/"):
return "/rest/antman/" + path[len("/rest/n/") :]
if path.startswith("/rest/"):
return "/rest/antman/" + path[len("/rest/") :]
return path
def build_url(self, path_or_url: str, *, rewrite_path: bool = True) -> tuple[str, str]:
parsed = urllib.parse.urlsplit(path_or_url)
if parsed.scheme and parsed.netloc:
path = self.rewrite_path(parsed.path) if rewrite_path else parsed.path
if not path.startswith("/"):
path = "/" + path
url = urllib.parse.urlunsplit((parsed.scheme, parsed.netloc, path, parsed.query, ""))
return url, path
path = self.rewrite_path(path_or_url) if rewrite_path else path_or_url
if not path.startswith("/"):
path = "/" + path
return self.host + path, path
def sign_query(
self,
path: str,
user_params: Mapping[str, Any] | None = None,
*,
include_auth: bool = True,
now: int | None = None,
counter: int = 1,
sig3_mode: str | None = None,
param_profile: str = "antman",
) -> dict[str, str]:
user = {str(k): "" if v is None else str(v) for k, v in (user_params or {}).items()}
request_salt = user.pop("client_salt", None) or self.device.client_salt
params = self.params_for_profile(param_profile)
if include_auth and param_profile == "antman":
params.update(self.auth_params())
params.update(user)
sig = kwai_sig(params)
params["sig"] = sig
if request_salt:
params["__NStokensig"] = kwai_token_sig(sig, request_salt)
ns_input = (path + sig).encode("utf-8")
params["__NS_sig3"] = self.make_sig3(ns_input, now=now, counter=counter, mode=sig3_mode)
return params
def sign_form(
self,
path: str,
form_params: Mapping[str, Any] | None = None,
*,
query_params: Mapping[str, Any] | None = None,
body_bytes: bytes | None = None,
include_auth: bool = True,
now: int | None = None,
counter: int = 1,
sig3_mode: str | None = None,
) -> dict[str, str]:
_, body = self.sign_form_parts(
path,
form_params,
query_params=query_params,
body_bytes=body_bytes,
include_auth=include_auth,
now=now,
counter=counter,
sig3_mode=sig3_mode,
)
return body
def sign_form_parts(
self,
path: str,
form_params: Mapping[str, Any] | None = None,
*,
query_params: Mapping[str, Any] | None = None,
body_bytes: bytes | None = None,
include_auth: bool = True,
now: int | None = None,
counter: int = 1,
sig3_mode: str | None = None,
param_profile: str = "antman",
) -> tuple[dict[str, str], dict[str, str]]:
form = {str(k): "" if v is None else str(v) for k, v in (form_params or {}).items()}
query = {str(k): "" if v is None else str(v) for k, v in (query_params or {}).items()}
query_signed = self.params_for_profile(param_profile)
query_signed.update(query)
auth = self.auth_params(include_client_salt=True)
request_salt = form.pop("client_salt", None) or query_signed.pop("client_salt", None) or auth.pop("client_salt", None)
body_signed = {"os": auth.pop("os", "android"), "client_key": auth.pop("client_key", "3c2cd3f3")}
if include_auth and param_profile == "antman":
body_signed.update(auth)
body_signed.update(form)
sig = kwai_sig(query_signed, body_signed)
body_signed["sig"] = sig
if request_salt:
body_signed["__NStokensig"] = kwai_token_sig(sig, request_salt)
body_signed["__NS_sig3"] = self.make_sig3((path + sig).encode("utf-8"), now=now, counter=counter, mode=sig3_mode)
return query_signed, body_signed
def sign_binary_query(
self,
body_bytes: bytes,
extra_params: Mapping[str, Any] | None = None,
*,
include_auth: bool = True,
) -> dict[str, str]:
params = self.common_params()
params["os"] = "android"
params["client_key"] = "3c2cd3f3"
params.update({str(k): "" if v is None else str(v) for k, v in (extra_params or {}).items()})
body_md5 = hashlib.md5(body_bytes).hexdigest()
sign_map = dict(params)
sign_map["bodyMd5"] = body_md5
if include_auth:
auth_for_sign = self.auth_params(include_client_salt=True)
request_salt = auth_for_sign.pop("client_salt", None)
for key in ("token", "kuaishou.api_st"):
if key in auth_for_sign:
sign_map[key] = auth_for_sign[key]
params[key] = auth_for_sign[key]
else:
request_salt = self.device.client_salt
sig2 = kwai_sig(sign_map)
params["sig2"] = sig2
if request_salt:
params["__NStokensig"] = kwai_token_sig(sig2, request_salt)
return params
def make_sig3(
self,
data: bytes | str,
*,
now: int | None = None,
counter: int = 1,
mode: str | None = None,
) -> str:
mode = mode or self.sig3_mode
if mode == "10418":
if not self.zt_table:
raise ValueError("10418 sig3 requires a zt table")
return sig3_10418(data, self.zt_table)
if mode == "10417":
return sig3_10417(data, self.zt_table, now=now, counter=counter)
if mode == "10405":
return sig3_10405(data, table=self.zt_table, now=now)
raise ValueError(f"unsupported sig3 mode: {mode}")
def make_azeroth_sig3(
self,
data: bytes | str,
*,
now: int | None = None,
counter: int = 1,
mode: str | None = None,
) -> str:
mode = mode or "10418"
table = self.azeroth_zt_table or self.zt_table
if mode == "10418":
if not table:
raise ValueError("Azeroth 10418 sig3 requires a zt table")
return sig3_10418(data, table)
if mode == "10417":
return sig3_10417(data, table, now=now, counter=counter)
if mode == "10405":
return sig3_10405(data, table=table, now=now)
raise ValueError(f"unsupported Azeroth sig3 mode: {mode}")
def sign_azeroth_parts(
self,
method: str,
path: str,
form_params: Mapping[str, Any] | None = None,
*,
query_params: Mapping[str, Any] | None = None,
include_auth: bool = True,
now: int | None = None,
counter: int = 1,
sig3_mode: str | None = None,
param_profile: str = "azeroth",
) -> tuple[dict[str, str], dict[str, str]]:
query_signed = self.params_for_profile(param_profile)
if not include_auth:
query_signed.pop("kuaishou.api_st", None)
query_signed.pop("token", None)
query_signed.update(_stringify_mapping(query_params))
body_signed = _stringify_mapping(form_params)
sign_map = dict(query_signed)
sign_map.update(body_signed)
client_sign = azeroth_hmac_client_sign(method, path, sign_map, self.device.azeroth_security)
if client_sign:
body_signed["__clientSign"] = client_sign
canonical = azeroth_canonical(method, path, sign_map)
body_signed["__NS_sig3"] = self.make_azeroth_sig3(
canonical.encode("utf-8"), now=now, counter=counter, mode=sig3_mode
)
return query_signed, body_signed
def signed_azeroth_request(
self,
method: str,
path_or_url: str,
*,
params: Mapping[str, Any] | None = None,
data: Mapping[str, Any] | bytes | None = None,
headers: Mapping[str, str] | None = None,
sign_body: bool | None = None,
include_auth: bool = True,
sig3_mode: str | None = None,
param_profile: str = "azeroth",
rewrite_path: bool = False,
post_sign_data: Mapping[str, Any] | None = None,
) -> SignedRequest:
method = method.upper()
url, path = self.build_url(path_or_url, rewrite_path=rewrite_path)
merged_headers = self.base_headers()
if headers:
merged_headers.update(headers)
if sign_body is None:
sign_body = method in {"POST", "PUT", "PATCH"}
if isinstance(data, bytes):
raise ValueError("Azeroth signing currently supports form requests, not raw bytes")
body_params = _stringify_mapping(data)
if not sign_body:
query_params = dict(body_params)
query_params.update(_stringify_mapping(params))
signed_params, signed_body = self.sign_azeroth_parts(
method,
path,
{},
query_params=query_params,
include_auth=include_auth,
sig3_mode=sig3_mode,
param_profile=param_profile,
)
signed_params.update(signed_body)
signed_params.update(_stringify_mapping(post_sign_data))
return SignedRequest(method, url, merged_headers, signed_params, None)
signed_params, signed_body = self.sign_azeroth_parts(
method,
path,
body_params,
query_params=params,
include_auth=include_auth,
sig3_mode=sig3_mode,
param_profile=param_profile,
)
signed_body.update(_stringify_mapping(post_sign_data))
merged_headers.setdefault("Content-Type", "application/x-www-form-urlencoded")
return SignedRequest(method, url, merged_headers, signed_params, signed_body)
def signed_request(
self,
method: str,
path_or_url: str,
*,
params: Mapping[str, Any] | None = None,
data: Mapping[str, Any] | bytes | None = None,
headers: Mapping[str, str] | None = None,
sign_body: bool | None = None,
include_auth: bool = True,
sig3_mode: str | None = None,
param_profile: str = "antman",
rewrite_path: bool = True,
post_sign_data: Mapping[str, Any] | None = None,
) -> SignedRequest:
method = method.upper()
if param_profile == "auto":
param_profile = self.auto_param_profile(path_or_url)
if param_profile != "antman":
rewrite_path = False
if param_profile.startswith("azeroth"):
return self.signed_azeroth_request(
method,
path_or_url,
params=params,
data=data,
headers=headers,
sign_body=sign_body,
include_auth=include_auth,
sig3_mode=sig3_mode,
param_profile=param_profile,
rewrite_path=rewrite_path,
post_sign_data=post_sign_data,
)
url, path = self.build_url(path_or_url, rewrite_path=rewrite_path)
merged_headers = self.base_headers()
if headers:
merged_headers.update(headers)
if sign_body is None:
sign_body = method in {"POST", "PUT", "PATCH"}
if sign_body:
if isinstance(data, bytes):
body_bytes = data
signed_params = self.sign_binary_query(body_bytes, params, include_auth=include_auth)
merged_headers.setdefault("Content-Type", "application/octet-stream")
return SignedRequest(method, url, merged_headers, signed_params, data)
body_params = {str(k): "" if v is None else str(v) for k, v in (data or {}).items()}
signed_params, signed_body = self.sign_form_parts(
path,
body_params,
query_params=params,
include_auth=include_auth,
sig3_mode=sig3_mode,
param_profile=param_profile,
)
signed_body.update(_stringify_mapping(post_sign_data))
merged_headers.setdefault("Content-Type", "application/x-www-form-urlencoded")
return SignedRequest(method, url, merged_headers, signed_params, signed_body)
signed_params = self.sign_query(
path,
params,
include_auth=include_auth,
sig3_mode=sig3_mode,
param_profile=param_profile,
)
signed_params.update(_stringify_mapping(post_sign_data))
return SignedRequest(method, url, merged_headers, signed_params, None)
def send(self, signed: SignedRequest, *, capture_dynamic: bool = True, **kwargs: Any) -> Any:
if self.session is None:
raise RuntimeError("requests is not installed; inspect SignedRequest or install requests")
request_kwargs = {
"headers": signed.headers,
"params": signed.params,
"timeout": self.timeout,
}
if isinstance(signed.data, bytes):
request_kwargs["data"] = signed.data
elif signed.data is not None:
request_kwargs["data"] = signed.data
request_kwargs.update(kwargs)
try:
response = self.session.request(signed.method, signed.url, **request_kwargs)
except Exception as exc:
hostname = urllib.parse.urlsplit(signed.url).hostname or ""
fallback_ips = DNS_OVERRIDE_IPS.get(hostname, ())
if not fallback_ips or not _is_dns_resolution_error(exc):
raise
last_error: BaseException = exc
for ip_address in fallback_ips:
try:
with _temporary_dns_override(hostname, ip_address):
response = self.session.request(signed.method, signed.url, **request_kwargs)
break
except Exception as retry_exc:
last_error = retry_exc
else:
raise last_error
if capture_dynamic:
self.capture_dynamic_fields(response, request_path=urllib.parse.urlsplit(signed.url).path)
return response
def fetch_captcha_challenge(self, error_url: str) -> tuple[CaptchaChallenge, Any]:
if self.session is None:
raise RuntimeError("requests is not installed; cannot fetch captcha page")
headers = {
"User-Agent": "Mozilla/5.0 (Linux; Android 10; Kwai) AppleWebKit/537.36 (KHTML, like Gecko) Mobile Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Referer": "https://app.m.kuaishou.com/",
}
response = self.session.get(error_url, headers=headers, timeout=self.timeout)
return parse_captcha_challenge(error_url, response.text), response
def captcha_verify(
self,
key: str,
ticket: str,
*,
uri: str = "/rest/n/user/login/mobileVerifyCode",
verify_type: int | str = 7,
referer: str = "",
) -> Any:
if self.session is None:
raise RuntimeError("requests is not installed; cannot verify captcha ticket")
try:
request_type: int | str = int(str(verify_type))
except ValueError:
request_type = str(verify_type)
headers = {
"User-Agent": "Mozilla/5.0 (Linux; Android 10; Kwai) AppleWebKit/537.36 (KHTML, like Gecko) Mobile Safari/537.36",
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/json; charset=utf-8",
"Origin": "https://app.m.kuaishou.com",
"Referer": referer or "https://app.m.kuaishou.com/verify/captcha.html",
}
payload = {"type": request_type, "uri": uri, "key": key, "input": ticket}
body = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
return self.session.post(CAPTCHA_VERIFY_ENDPOINT, headers=headers, data=body, timeout=self.timeout)
def request(self, method: str, path_or_url: str, **kwargs: Any) -> Any:
signed = self.signed_request(method, path_or_url, **kwargs)
return self.send(signed)
def endpoint_names(self, *, safe_only: bool = False) -> list[str]:
names = sorted(REST_ENDPOINTS)
if safe_only:
names = [name for name in names if REST_ENDPOINTS[name].safe]
return names
def rest_endpoint(self, name: str) -> RestEndpoint:
try:
return REST_ENDPOINTS[name]
except KeyError as exc:
known = ", ".join(self.endpoint_names())
raise ValueError(f"unknown REST endpoint {name!r}; known: {known}") from exc
def rest_request(
self,
name: str,
*,
data: Mapping[str, Any] | None = None,
params: Mapping[str, Any] | None = None,
include_auth: bool | None = None,
sig3_mode: str | None = None,
) -> SignedRequest:
endpoint = self.rest_endpoint(name)
path = endpoint.path
merged_data = _stringify_mapping(endpoint.defaults)
merged_params = _stringify_mapping(endpoint.query_defaults)
merged_params.update(_stringify_mapping(params))
if name == "startup" and "extId" in merged_data:
merged_params.setdefault("extId", merged_data.pop("extId"))
elif name == "plugin_startup" and "extId" in merged_data:
merged_params.setdefault("extId", merged_data.pop("extId"))
if name == "refresh_token":
merged_data.update({"token": self.device.token, "client_salt": self.device.client_salt})
elif name == "logout":
merged_data.update({"token": self.device.token, "client_salt": self.device.client_salt})
if data:
merged_data.update(_stringify_mapping(data))
if name == "video_photo_info" and not merged_data.get("photoIds") and merged_data.get("photoId"):
merged_data["photoIds"] = str(merged_data.pop("photoId"))
if path == "@Url":
path = str(merged_params.pop("url", "") or merged_data.pop("url", ""))
if not path:
raise ValueError(f"REST endpoint {name!r} requires a url=... value for Retrofit @Url")
elif not urllib.parse.urlsplit(path).scheme and not path.startswith("/rest/"):
path = "/rest/" + path.lstrip("/")
auth = endpoint.include_auth if include_auth is None else include_auth
method = endpoint.method.upper()
if method == "GET":
merged_params = {**merged_data, **merged_params}
merged_data = {}
return self.signed_request(
method,
path,
params=merged_params,
data=merged_data if method != "GET" else None,
sign_body=method != "GET",
include_auth=auth,
sig3_mode=sig3_mode,
param_profile=endpoint.param_profile,
rewrite_path=endpoint.rewrite_path,
)
def call_rest(self, name: str, **kwargs: Any) -> Any:
signed = self.rest_request(name, **kwargs)
return self.send(signed, capture_dynamic=True)
def api(
self,
name: str,
*,
data: Mapping[str, Any] | None = None,
params: Mapping[str, Any] | None = None,
include_auth: bool | None = None,
sig3_mode: str | None = None,
send: bool = True,
**fields: Any,
) -> Any:
endpoint = self.rest_endpoint(name)
body_data = _stringify_mapping(data)
query_params = _stringify_mapping(params)
for key, value in fields.items():
if key in endpoint.query_fields or (endpoint.method.upper() == "GET" and key not in endpoint.fields):
query_params[key] = _stringify_value(value)
else:
body_data[key] = _stringify_value(value)
signed = self.rest_request(
name,
data=body_data,
params=query_params,
include_auth=include_auth,
sig3_mode=sig3_mode,
)
if not send:
return signed
return self.send(signed, capture_dynamic=True)
def main_app_rest_request(
self,
name: str,
*,
data: Mapping[str, Any] | None = None,
params: Mapping[str, Any] | None = None,
include_auth: bool | None = None,
sig3_mode: str | None = None,
rewrite_path: bool = True,
) -> SignedRequest:
endpoint = self.rest_endpoint(name)
path = endpoint.path
merged_data = _stringify_mapping(endpoint.defaults)
merged_params = _stringify_mapping(endpoint.query_defaults)
merged_params.update(_stringify_mapping(params))
if data:
merged_data.update(_stringify_mapping(data))
if path == "@Url":
path = str(merged_params.pop("url", "") or merged_data.pop("url", ""))
if not path:
raise ValueError(f"REST endpoint {name!r} requires a url=... value for Retrofit @Url")
elif not urllib.parse.urlsplit(path).scheme and not path.startswith("/rest/"):
path = "/rest/" + path.lstrip("/")
auth = endpoint.include_auth if include_auth is None else include_auth
method = endpoint.method.upper()
if method == "GET":
merged_params = {**merged_data, **merged_params}
merged_data = {}
return self.signed_request(
method,
path,
params=merged_params,
data=merged_data if method != "GET" else None,
sign_body=method != "GET",
include_auth=auth,
sig3_mode=sig3_mode,
param_profile="main_video",
rewrite_path=rewrite_path,
)
def call_main_app_rest(self, name: str, **kwargs: Any) -> Any:
signed = self.main_app_rest_request(name, **kwargs)
return self.send(signed, capture_dynamic=True)
def video_feed_hot(self, count: int | str = 1, pcursor: str = "", extra: Mapping[str, Any] | None = None) -> Any:
data = {"count": str(count), "pcursor": pcursor}
if extra:
data.update(_stringify_mapping(extra))
return self.call_rest("video_feed_hot", data=data)
def video_feed_profile(
self,
user_id: str,
count: int | str = 1,
pcursor: str = "",
extra: Mapping[str, Any] | None = None,
) -> Any:
data = {"user_id": user_id, "count": str(count), "pcursor": pcursor}
if extra:
data.update(_stringify_mapping(extra))
return self.call_rest("video_feed_profile", data=data)
def video_photo_info(
self,
photo_id: str,
*,
user_id: str = "",
exp_tag: str = "",
llsid: str = "",
photo_index: int | str | None = None,
extra: Mapping[str, Any] | None = None,
) -> Any:
data = {"photoIds": photo_id}
if user_id:
data.setdefault("preUserId", str(user_id))
if exp_tag:
data.setdefault("preExpTag", str(exp_tag))
if llsid:
data.setdefault("preLLSId", str(llsid))
if photo_index is not None:
data.setdefault("prePhotoIndex", str(photo_index))
if extra:
data.update(_stringify_mapping(extra))
return self.call_rest("video_photo_info", data=data)
def video_photo_comments(
self,
photo_id: str,
count: int | str = 20,
order: str = "desc",
extra: Mapping[str, Any] | None = None,
) -> Any:
data = {"photoId": photo_id, "count": str(count), "order": order, "enableEmotion": "true"}
if extra:
data.update(_stringify_mapping(extra))
return self.call_rest("video_photo_comments", data=data)
def api_user_info(self, user_ids: str | Iterable[Any], extra: Mapping[str, Any] | None = None) -> Any:
data = {"userIds": _stringify_value(user_ids)}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("api_user_info", data=data)
def api_get_users_profile_batch(self, user_ids: str | Iterable[Any], extra: Mapping[str, Any] | None = None) -> Any:
data = {"userIds": _stringify_value(user_ids)}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("api_get_users_profile_batch", data=data)
def api_get_photo_infos(self, photo_ids: str | Iterable[Any], extra: Mapping[str, Any] | None = None) -> Any:
data = {"photoIds": _stringify_value(photo_ids)}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("api_get_photo_infos", data=data)
def api_comment_list_v2(
self,
photo_id: str,
*,
user_id: str = "",
order: str = "desc",
pcursor: str = "",
count: int | str = 20,
photo_page_type: int | str = "",
extra: Mapping[str, Any] | None = None,
) -> Any:
data = {
"token": self.device.token,
"photoId": photo_id,
"user_id": user_id,
"order": order,
"pcursor": pcursor,
"count": str(count),
"photoPageType": str(photo_page_type),
}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("api_comment_list_v2", data=data)
def api_comment_sub_list(
self,
photo_id: str,
root_comment_id: str,
*,
user_id: str = "",
order: str = "desc",
pcursor: str = "",
extra: Mapping[str, Any] | None = None,
) -> Any:
data = {
"token": self.device.token,
"photoId": photo_id,
"user_id": user_id,
"order": order,
"pcursor": pcursor,
"rootCommentId": root_comment_id,
}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("api_comment_sub_list", data=data)
def api_comment_list_by_pivot(
self,
photo_id: str,
root_comment_id: str,
comment_id: str,
*,
user_id: str = "",
order: str = "desc",
pcursor: str = "",
filter_sub_comment: bool = False,
extra: Mapping[str, Any] | None = None,
) -> Any:
data = {
"token": self.device.token,
"photoId": photo_id,
"user_id": user_id,
"order": order,
"pcursor": pcursor,
"rootCommentId": root_comment_id,
"commentId": comment_id,
"filterSubComment": filter_sub_comment,
}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("api_comment_list_by_pivot", data=data)
def api_search(self, keyword: str, pcursor: str = "", ussid: str = "", extra: Mapping[str, Any] | None = None) -> Any:
data = {"keyword": keyword, "pcursor": pcursor, "ussid": ussid}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("api_search", data=data)
def api_search_user(self, keyword: str, pcursor: str = "", ussid: str = "", extra: Mapping[str, Any] | None = None) -> Any:
data = {"keyword": keyword, "pcursor": pcursor, "ussid": ussid}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("api_search_user", data=data)
def api_search_tag(self, keyword: str, pcursor: str = "", ussid: str = "", extra: Mapping[str, Any] | None = None) -> Any:
data = {"keyword": keyword, "pcursor": pcursor, "ussid": ussid}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("api_search_tag", data=data)
def api_search_suggest(self, keyword: str, extra: Mapping[str, Any] | None = None) -> Any:
data = {"keyword": keyword}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("api_search_suggest", data=data)
def api_search_home(self, pcursor: str = "", prsid: str = "", extra: Mapping[str, Any] | None = None) -> Any:
data = {"pcursor": pcursor, "prsid": prsid}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("api_search_home", data=data)
def api_search_recommend(self, pcursor: str = "", prsid: str = "", extra: Mapping[str, Any] | None = None) -> Any:
data = {"pcursor": pcursor, "prsid": prsid}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("api_search_recommend", data=data)
def api_search_kwai_hot_billboard(self, pcursor: str = "", extra: Mapping[str, Any] | None = None) -> Any:
data = {"pcursor": pcursor}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("api_search_kwai_hot_billboard", data=data)
def api_profile_feed(
self,
user_id: str,
*,
lang: str = "zh-cn",
count: int | str = 20,
privacy: str = "",
pcursor: str = "",
referer: str = "",
extra: Mapping[str, Any] | None = None,
) -> Any:
data = {
"token": self.device.token,
"user_id": user_id,
"lang": lang,
"count": str(count),
"privacy": privacy,
"pcursor": pcursor,
"referer": referer,
}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("api_profile_feed", data=data)
def api_user_profile_v2(self, user: str, ussid: str = "", extra: Mapping[str, Any] | None = None) -> Any:
data = {"user": user, "token": self.device.token, "ussid": ussid}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("api_user_profile_v2", data=data)
def api_feed_tag(
self,
tag: str,
*,
count: int | str = 20,
pcursor: str = "",
ussid: str = "",
tag_source: str = "",
extra: Mapping[str, Any] | None = None,
) -> Any:
data = {"tag": tag, "count": str(count), "pcursor": pcursor, "ussid": ussid, "tagSource": tag_source}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("api_feed_tag", data=data)
def api_tag_detail(self, tag: str, extra: Mapping[str, Any] | None = None) -> Any:
data = {"tag": tag}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("api_tag_detail", data=data)
def api_likers(self, photo_id: str, pcursor: str = "", extra: Mapping[str, Any] | None = None) -> Any:
data = {"photo_id": photo_id, "pcursor": pcursor}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("api_likers", data=data)
def api_photo_check_filter(self, photo_id: str, extra: Mapping[str, Any] | None = None) -> Any:
data = {"photoId": photo_id}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("api_check_photo", data=data)
def api_ad_list(self, extra: Mapping[str, Any] | None = None) -> Any:
data = _stringify_mapping(extra)
return self.call_main_app_rest("api_ad_list", data=data)
def api_promotion_widget(self, extra: Mapping[str, Any] | None = None) -> Any:
params = _stringify_mapping(extra)
return self.call_rest("api_promotion_widget", params=params)
def api_resource_meta(self, name: str = "android.json", extra: Mapping[str, Any] | None = None) -> Any:
data = {"name": name}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("api_update_config", data=data)
def api_system_dialog(self, source: str = "startup", extra: Mapping[str, Any] | None = None) -> Any:
data = {"source": source}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("system_dialog", data=data)
def api_live_auth_status(self, extra: Mapping[str, Any] | None = None) -> Any:
data = _stringify_mapping(extra)
return self.call_main_app_rest("api_live_auth_status", data=data)
def api_follow_users(
self,
touid: str = "0",
*,
ftype: int | str = 1,
page: int | str = "",
pcursor: str = "",
latest_insert_time: str = "",
extra: Mapping[str, Any] | None = None,
) -> Any:
data = {
"touid": touid,
"ftype": ftype,
"page": page,
"pcursor": pcursor,
"latest_insert_time": latest_insert_time,
}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("api_get_follow_users", data=data)
def api_message_dialog(
self,
count: int | str = 20,
page: int | str = "",
pcursor: str = "",
extra: Mapping[str, Any] | None = None,
) -> Any:
data = {"token": self.device.token, "count": str(count), "page": str(page), "pcursor": pcursor}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("api_message_dialog", data=data)
def api_message_load(
self,
user_id: str,
page: int | str = "",
order: str = "desc",
pcursor: str = "",
extra: Mapping[str, Any] | None = None,
) -> Any:
data = {"page": str(page), "token": self.device.token, "user_id": user_id, "order": order, "pcursor": pcursor}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("api_message_load", data=data)
def api_send_message(
self,
user_id: str,
content: str,
*,
copy: bool | str = False,
photo_id: str = "",
live_stream_id: str = "",
informed_user_id: str = "",
extra: Mapping[str, Any] | None = None,
send: bool = True,
) -> Any:
data = {
"user_id": user_id,
"content": content,
"copy": copy,
"photoId": photo_id,
"liveStreamId": live_stream_id,
"informedUserId": informed_user_id,
}
data.update(_stringify_mapping(extra))
signed = self.main_app_rest_request("api_send_message", data=data)
if not send:
return signed
return self.send(signed, capture_dynamic=True)
def private_chat_list(
self,
count: int | str = 20,
page: int | str = "",
pcursor: str = "",
extra: Mapping[str, Any] | None = None,
) -> Any:
return self.api_message_dialog(count=count, page=page, pcursor=pcursor, extra=extra)
def private_chat_messages(
self,
user_id: str,
page: int | str = "",
order: str = "desc",
pcursor: str = "",
extra: Mapping[str, Any] | None = None,
) -> Any:
return self.api_message_load(user_id=user_id, page=page, order=order, pcursor=pcursor, extra=extra)
def private_chat_send(
self,
user_id: str,
content: str,
*,
copy: bool | str = False,
photo_id: str = "",
live_stream_id: str = "",
informed_user_id: str = "",
extra: Mapping[str, Any] | None = None,
send: bool = True,
) -> Any:
return self.api_send_message(
user_id,
content,
copy=copy,
photo_id=photo_id,
live_stream_id=live_stream_id,
informed_user_id=informed_user_id,
extra=extra,
send=send,
)
def api_get_feed_selection(
self,
*,
page: int | str = 1,
cold: bool | str = "",
cold_start: bool | str = False,
count: int | str = 20,
pcursor: str = "",
pv: str = "false",
photo_infos: str = "",
new_user_refresh_times: int | str = 0,
new_user_action: str = "",
reco_report_context: str = "",
source: int | str = 1,
edge_reco_bit: int | str = 0,
real_show_photo_ids: str = "",
extra: Mapping[str, Any] | None = None,
query_extra: Mapping[str, Any] | None = None,
) -> Any:
data = {
"page": page,
"coldStart": cold_start,
"count": count,
"pcursor": pcursor,
"pv": pv,
"photoInfos": photo_infos,
"newUserRefreshTimes": new_user_refresh_times,
"newUserAction": new_user_action,
"recoReportContext": reco_report_context,
"source": source,
"edgeRecoBit": edge_reco_bit,
"realShowPhotoIds": real_show_photo_ids,
}
data.update(_stringify_mapping(extra))
params = {"cold": cold}
params.update(_stringify_mapping(query_extra))
return self.call_main_app_rest("api_get_feed_selection", data=data, params=params)
def api_feed_like_list(
self,
user_id: str,
*,
count: int | str = 20,
pcursor: str = "",
referer: str = "",
extra: Mapping[str, Any] | None = None,
) -> Any:
data = {"token": self.device.token, "id": user_id, "count": str(count), "pcursor": pcursor, "referer": referer}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("api_feed_like_list", data=data)
def api_feed_my_follow(
self,
*,
feed_type: int | str = 0,
page: int | str = 1,
count: int | str = 20,
feed_id: int | str = 0,
pcursor: str = "",
refresh_times: int | str = 0,
cold_start: bool | str = False,
source: int | str = 1,
my_follow_slide_type: int | str = 0,
extra: Mapping[str, Any] | None = None,
) -> Any:
data = {
"type": feed_type,
"page": page,
"token": self.device.token,
"count": count,
"id": feed_id,
"pcursor": pcursor,
"refreshTimes": refresh_times,
"coldStart": cold_start,
"source": source,
"myFollowSlideType": my_follow_slide_type,
}
data.update(_stringify_mapping(extra))
return self.call_main_app_rest("api_feed_my_follow", data=data)
@staticmethod
def extract_video_ids(payload: Any) -> list[dict[str, str]]:
items: list[dict[str, str]] = []
if not isinstance(payload, Mapping):
return items
feeds = payload.get("feeds")
if not isinstance(feeds, list):
return items
for feed in feeds:
if not isinstance(feed, Mapping):
continue
photo_id = feed.get("photo_id") or feed.get("photoId") or feed.get("id")
user_id = feed.get("user_id") or feed.get("userId")
feed_log_ctx = feed.get("feedLogCtx")
if photo_id:
items.append(
{
"photo_id": str(photo_id),
"user_id": "" if user_id is None else str(user_id),
"exp_tag": "" if feed.get("exp_tag") is None else str(feed.get("exp_tag")),
"server_exp_tag": "" if feed.get("serverExpTag") is None else str(feed.get("serverExpTag")),
"llsid": "" if payload.get("llsid") is None else str(payload.get("llsid")),
"stid_container": ""
if not isinstance(feed_log_ctx, Mapping) or feed_log_ctx.get("stidContainer") is None
else str(feed_log_ctx.get("stidContainer")),
}
)
return items
@staticmethod
def response_summary(name: str, response: Any, *, preview_len: int = 360) -> dict[str, Any]:
text = getattr(response, "text", "") or ""
payload = _response_json(response)
result_code = payload.get("result") if isinstance(payload, Mapping) else None
error_msg = str(payload.get("error_msg", "")) if isinstance(payload, Mapping) else ""
return {
"name": name,
"status_code": getattr(response, "status_code", None),
"result": result_code,
"signature_ok": not (result_code == 50 and ("??" in error_msg or "signature" in error_msg.lower())),
"error_msg": error_msg,
"body_preview": text[:preview_len],
}
def info_endpoint_names(self, category: str = "all") -> list[str]:
if category == "all":
names = INFO_REST_ENDPOINT_NAMES
else:
try:
names = REST_INFO_CATEGORIES[category]
except KeyError as exc:
known = ", ".join(["all", *sorted(REST_INFO_CATEGORIES)])
raise ValueError(f"unknown info REST category {category!r}; known: {known}") from exc
return sorted(name for name in names if name in REST_ENDPOINTS)
def _record_info_call(
self,
results: list[dict[str, Any]],
name: str,
*,
data: Mapping[str, Any] | None = None,
params: Mapping[str, Any] | None = None,
preview_len: int = 360,
) -> Any:
try:
response = self.call_rest(name, data=data, params=params)
results.append(self.response_summary(name, response, preview_len=preview_len))
return response
except Exception as exc:
results.append({"name": name, "error": str(exc)})
return None
def _first_feed_identity(self, count: int | str = 1) -> dict[str, str]:
response = self.video_feed_hot(count=count)
payload = _response_json(response)
extracted = self.extract_video_ids(payload)
return extracted[0] if extracted else {}
def smoke_test_info_rest(
self,
*,
category: str = "all",
count: int | str = 1,
keyword: str = "猫",
) -> list[dict[str, Any]]:
categories = sorted(REST_INFO_CATEGORIES) if category == "all" else [category]
results: list[dict[str, Any]] = []
identity: dict[str, str] = {}
for item in categories:
if item not in REST_INFO_CATEGORIES:
known = ", ".join(["all", *sorted(REST_INFO_CATEGORIES)])
raise ValueError(f"unknown info REST category {item!r}; known: {known}")
if item == "search":
if keyword:
self._record_info_call(results, "api_search_suggest", data={"keyword": keyword})
self._record_info_call(results, "api_search", data={"keyword": keyword, "pcursor": "", "ussid": ""})
self._record_info_call(results, "api_search_user", data={"keyword": keyword, "pcursor": "", "ussid": ""})
self._record_info_call(results, "api_search_tag", data={"keyword": keyword, "pcursor": "", "ussid": ""})
self._record_info_call(results, "api_search_home", data={"pcursor": "", "prsid": ""})
self._record_info_call(results, "api_search_recommend", data={"pcursor": "", "prsid": ""})
self._record_info_call(results, "api_search_kwai_hot_billboard", data={"pcursor": ""})
self._record_info_call(results, "api_search_tag_recommend", data={"pcursor": ""})
elif item == "video":
feed_response = self._record_info_call(results, "video_feed_hot", data={"count": str(count), "pcursor": ""})
feed_payload = _response_json(feed_response) if feed_response is not None else None
extracted = self.extract_video_ids(feed_payload)
if extracted:
identity = extracted[0]
photo_id = identity.get("photo_id", "")
user_id = identity.get("user_id", "")
self._record_info_call(results, "api_get_hot_items", data={"count": str(count), "pcursor": "", "page": "1"})
self._record_info_call(results, "api_get_feed_selection", data={"count": str(count), "page": "1", "pcursor": ""})
if photo_id:
self._record_info_call(results, "api_get_photo_infos", data={"photoIds": photo_id})
self._record_info_call(results, "video_photo_info", data={"photoIds": photo_id})
self._record_info_call(results, "api_comment_list_v2", data={"photoId": photo_id, "user_id": user_id, "count": str(count), "order": "desc"})
self._record_info_call(results, "video_photo_comments", data={"photoId": photo_id, "count": str(count), "order": "desc", "enableEmotion": "true"})
self._record_info_call(results, "api_likers", data={"photo_id": photo_id, "pcursor": ""})
if user_id:
self._record_info_call(results, "video_feed_profile", data={"user_id": user_id, "count": str(count), "pcursor": ""})
if keyword:
self._record_info_call(results, "api_feed_tag", data={"tag": keyword, "count": str(count), "pcursor": ""})
self._record_info_call(results, "api_tag_detail", data={"tag": keyword})
elif item == "user":
if not identity:
identity = self._first_feed_identity(count=count)
user_id = identity.get("user_id", "")
if user_id:
self._record_info_call(results, "api_user_info", data={"userIds": user_id})
self._record_info_call(results, "api_get_users_profile_batch", data={"userIds": user_id})
self._record_info_call(results, "api_user_profile_v2", data={"user": user_id, "token": self.device.token, "ussid": ""})
self._record_info_call(results, "api_profile_feed", data={"user_id": user_id, "token": self.device.token, "count": str(count), "pcursor": ""})
self._record_info_call(results, "api_profile_list_friends", data={"user": user_id, "count": str(count), "pcursor": ""})
self._record_info_call(results, "api_get_follow_users", data={"touid": user_id, "page": "1", "pcursor": ""})
self._record_info_call(results, "api_get_relation_friends", data={"touid": user_id, "pcursor": ""})
self._record_info_call(results, "api_get_friend_users", data={"lastModified": ""})
self._record_info_call(results, "api_get_at_users", data={"keyword": keyword, "pcursor": ""})
self._record_info_call(results, "user_profile", data={})
self._record_info_call(results, "user_settings", data={})
elif item == "observed":
if not identity:
identity = self._first_feed_identity(count=count)
photo_id = identity.get("photo_id", "")
user_id = identity.get("user_id", "")
self._record_info_call(results, "api_promotion_widget", params={}, preview_len=160)
self._record_info_call(results, "api_ad_list", data={}, preview_len=160)
self._record_info_call(results, "api_update_config", data={"name": "android.json"}, preview_len=160)
self._record_info_call(results, "system_dialog", data={"source": "startup"}, preview_len=160)
self._record_info_call(results, "api_live_auth_status", data={}, preview_len=160)
if photo_id:
self._record_info_call(results, "api_check_photo", data={"photoId": photo_id}, preview_len=160)
if user_id:
self._record_info_call(results, "api_get_follow_users", data={"touid": user_id, "ftype": "1"}, preview_len=160)
elif item == "message":
self._record_info_call(results, "api_message_dialog", data={"token": self.device.token, "count": str(count), "page": "", "pcursor": ""}, preview_len=160)
self._record_info_call(results, "api_message_load", data={"page": "", "token": self.device.token, "user_id": "", "order": "desc", "pcursor": ""}, preview_len=160)
self._record_info_call(results, "api_news_load", data={"token": self.device.token, "count": str(count), "page": "", "pcursor": ""}, preview_len=160)
self._record_info_call(results, "api_notify_load", data={"token": self.device.token, "subVersion": "", "pcursor": "", "latest_insert_time": ""}, preview_len=160)
self._record_info_call(results, "api_nebula_notify_load", data={"token": self.device.token, "subVersion": "", "pcursor": "", "latest_insert_time": ""}, preview_len=160)
self._record_info_call(results, "api_get_share_user_list", data={}, preview_len=160)
self._record_info_call(results, "api_get_friend_users", data={"lastModified": ""}, preview_len=160)
return results
def build_rest_probe(self, names: Iterable[str] | None = None) -> list[dict[str, Any]]:
selected = list(names or self.endpoint_names())
results: list[dict[str, Any]] = []
for name in selected:
endpoint = self.rest_endpoint(name)
data = {field: f"{field}-demo" for field in endpoint.fields}
params = {field: f"{field}-demo" for field in endpoint.query_fields}
if endpoint.path == "@Url":
data["url"] = "https://example.test/probe"
try:
signed = self.rest_request(name, data=data, params=params)
sig_location = "params" if signed.params.get("__NS_sig3") else "data"
results.append(
{
"name": name,
"ok": True,
"method": signed.method,
"url": signed.url,
"include_auth": endpoint.include_auth,
"param_profile": endpoint.param_profile,
"sig3_location": sig_location,
}
)
except Exception as exc:
results.append({"name": name, "ok": False, "error": str(exc)})
return results
def smoke_test_public_apis(self, *, count: int | str = 1, keyword: str = "??") -> list[dict[str, Any]]:
results: list[dict[str, Any]] = []
def record(name: str, response: Any) -> None:
results.append(self.response_summary(name, response))
feed_response = self.video_feed_hot(count=count)
record("video_feed_hot", feed_response)
feed_payload = _response_json(feed_response)
extracted = self.extract_video_ids(feed_payload)
if extracted:
first = extracted[0]
photo_id = first.get("photo_id", "")
user_id = first.get("user_id", "")
exp_tag = first.get("exp_tag", "")
llsid = first.get("llsid", "")
if photo_id:
record("api_get_photo_infos", self.api_get_photo_infos(photo_id))
record("video_photo_info", self.video_photo_info(photo_id, user_id=user_id, exp_tag=exp_tag, llsid=llsid, photo_index=0))
record("api_comment_list_v2", self.api_comment_list_v2(photo_id, user_id=user_id, count=1))
record("video_photo_comments", self.video_photo_comments(photo_id, count=1))
if user_id:
record("api_user_info", self.api_user_info(user_id))
record("api_user_profile_v2", self.api_user_profile_v2(user_id))
record("api_profile_feed", self.api_profile_feed(user_id, count=1))
if keyword:
record("api_search_suggest", self.api_search_suggest(keyword))
record("api_search", self.api_search(keyword))
return results
def smoke_test_rest(self, names: Iterable[str] | None = None) -> list[dict[str, Any]]:
selected = list(names or DEFAULT_REST_SMOKE_ENDPOINT_NAMES)
results: list[dict[str, Any]] = []
for name in selected:
endpoint = self.rest_endpoint(name)
if not endpoint.safe:
results.append({"name": name, "skipped": True, "reason": "endpoint is not marked safe"})
continue
try:
response = self.call_rest(name)
text = getattr(response, "text", "") or ""
payload = _response_json(response)
result_code = payload.get("result") if isinstance(payload, Mapping) else None
error_msg = str(payload.get("error_msg", "")) if isinstance(payload, Mapping) else ""
results.append(
{
"name": name,
"status_code": getattr(response, "status_code", None),
"result": result_code,
"signature_ok": not (result_code == 50 and ("签名" in error_msg or "signature" in error_msg.lower())),
"body_preview": text[:240],
}
)
except Exception as exc:
results.append({"name": name, "error": str(exc)})
return results
def get(self, path_or_url: str, params: Mapping[str, Any] | None = None, **kwargs: Any) -> Any:
return self.request("GET", path_or_url, params=params, sign_body=False, **kwargs)
def post_form(self, path_or_url: str, data: Mapping[str, Any] | None = None, **kwargs: Any) -> Any:
return self.request("POST", path_or_url, data=data, sign_body=True, **kwargs)
def login(
self,
path_or_url: str,
data: Mapping[str, Any] | None = None,
*,
params: Mapping[str, Any] | None = None,
method: str = "POST",
include_auth: bool = False,
param_profile: str = "main_video_login",
rewrite_path: bool = True,
**kwargs: Any,
) -> Any:
sig3_mode = kwargs.pop("sig3_mode", None)
signed = self.signed_request(
method,
path_or_url,
params=params,
data=data,
sign_body=method.upper() != "GET",
include_auth=include_auth,
sig3_mode=sig3_mode,
param_profile=param_profile,
rewrite_path=rewrite_path,
)
return self.send(signed, capture_dynamic=True, **kwargs)
def login_mobile_check(self, mobile: str, country_code: str = "86", **kwargs: Any) -> Any:
return self.login(
"/rest/n/user/mobile/checker",
{"mobileCountryCode": country_code, "mobile": mobile},
include_auth=False,
rewrite_path=False,
**kwargs,
)
def request_mobile_code(self, mobile: str, country_code: str = "86", code_type: int = 27, **kwargs: Any) -> Any:
return self.login(
"/rest/n/user/requestMobileCode",
{"mobileCountryCode": country_code, "mobile": mobile, "type": str(code_type)},
include_auth=False,
rewrite_path=False,
**kwargs,
)
def verify_code_login(
self,
mobile: str,
code: str,
country_code: str = "86",
code_type: int = 27,
extra: Mapping[str, Any] | None = None,
use_account_keypair: bool = True,
include_public_key: bool = False,
**kwargs: Any,
) -> Any:
form = {
"code": code,
"mobileCountryCode": country_code,
"mobile": mobile,
"type": str(code_type),
}
if extra:
form.update(_stringify_mapping(extra))
if use_account_keypair:
if include_public_key and not form.get("publicKey"):
form["publicKey"] = self.account_public_key()
form.setdefault("deviceName", self.device.mod)
form.setdefault("deviceMode", self.device.mod)
self.add_account_security_fields(form)
return self.login("/rest/n/user/login/mobileVerifyCode", form, include_auth=False, rewrite_path=False, **kwargs)
def phone_code_login(
self,
mobile: str,
code: str,
country_code: str = "86",
code_type: int = 27,
extra: Mapping[str, Any] | None = None,
use_account_keypair: bool = True,
include_public_key: bool = False,
**kwargs: Any,
) -> Any:
return self.verify_code_login(
mobile,
code,
country_code=country_code,
code_type=code_type,
extra=extra,
use_account_keypair=use_account_keypair,
include_public_key=include_public_key,
**kwargs,
)
def register_by_phone_code_login(
self,
mobile: str,
code: str,
country_code: str = "86",
extra: Mapping[str, Any] | None = None,
use_account_keypair: bool = True,
**kwargs: Any,
) -> Any:
form = {
"mobileCountryCode": country_code,
"mobile": mobile,
"mobileCode": code,
"publicKey": self.account_public_key(),
"deviceName": self.device.mod,
"deviceMode": self.device.mod,
"type": "302",
}
if extra:
form.update(_stringify_mapping(extra))
if use_account_keypair:
self.add_account_security_fields(form)
return self.login("/rest/n/user/register/mobileV2", form, include_auth=False, rewrite_path=False, **kwargs)
def token_login(
self,
token: str,
extra: Mapping[str, Any] | None = None,
*,
use_account_keypair: bool = True,
**kwargs: Any,
) -> Any:
form = {"token": token, "type": "302", "deviceName": self.device.mod, "deviceMode": self.device.mod}
if extra:
form.update(_stringify_mapping(extra))
if use_account_keypair:
if not form.get("publicKey"):
form["publicKey"] = self.account_public_key()
self.add_account_security_fields(form)
return self.login("/rest/n/user/login/token", form, include_auth=False, **kwargs)
def third_platform_login(
self,
platform: str,
access_token: str,
open_id: str = "",
extra: Mapping[str, Any] | None = None,
*,
use_account_keypair: bool = True,
**kwargs: Any,
) -> Any:
form = {"platform": platform, "accessToken": access_token, "openId": open_id}
form.update(_stringify_mapping(extra))
if use_account_keypair:
if not form.get("publicKey"):
form["publicKey"] = self.account_public_key()
self.add_account_security_fields(form)
return self.login("/rest/user/thirdPlatformLogin", form, include_auth=False, rewrite_path=False, **kwargs)
def auth_wechat_code(self, code: str, extra: Mapping[str, Any] | None = None, **kwargs: Any) -> Any:
form = {"code": code}
form.update(_stringify_mapping(extra))
return self.login("/rest/n/wechat/oauth2/authByCode", form, include_auth=False, **kwargs)
def verify_trust_device(
self,
is_add_account: bool | str = False,
extra: Mapping[str, Any] | None = None,
**kwargs: Any,
) -> Any:
form = {"isAddAccount": is_add_account}
form.update(_stringify_mapping(extra))
return self.login("/rest/n/user/verifyTrustDevice", form, include_auth=True, **kwargs)
def account_public_key(self) -> str:
private_key = self._load_or_create_account_private_key()
from cryptography.hazmat.primitives import serialization
public_der = private_key.public_key().public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
return base64.b64encode(public_der).decode("ascii")
def add_account_security_fields(self, form: dict[str, str], *, now_ms: int | None = None) -> None:
if form.get("raw") and form.get("secret"):
return
raw = str(now_ms if now_ms is not None else int(time.time() * 1000))
form["raw"] = raw
form["secret"] = self.sign_account_raw(raw)
def sign_account_raw(self, raw: str) -> str:
private_key = self._load_or_create_account_private_key()
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
signature = private_key.sign(raw.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256())
return base64.b64encode(signature).decode("ascii")
def _load_or_create_account_private_key(self) -> Any:
try:
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
except Exception as exc:
raise RuntimeError("cryptography is required for account raw/secret RSA fields") from exc
if self.device.account_private_key_pem:
return serialization.load_pem_private_key(self.device.account_private_key_pem.encode("utf-8"), password=None)
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
self.device.account_private_key_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
).decode("ascii")
return private_key
def register_by_phone_v2(
self,
mobile: str,
code: str,
password: str,
user_name: str = "",
gender: str = "U",
country_code: str = "86",
extra: Mapping[str, Any] | None = None,
**kwargs: Any,
) -> Any:
form = {
"userName": user_name,
"mobileCountryCode": country_code,
"mobile": mobile,
"mobileCode": code,
"password": password,
"gender": gender,
}
if extra:
form.update(_stringify_mapping(extra))
return self.login("/rest/n/user/register/mobileV2", form, include_auth=False, **kwargs)
def refresh_token(self, extra: Mapping[str, Any] | None = None, **kwargs: Any) -> Any:
form = {
"token": self.device.token,
"client_salt": self.device.client_salt,
}
if extra:
form.update(_stringify_mapping(extra))
return self.login("/rest/n/token/infra/refreshToken", form, include_auth=True, param_profile="antman", **kwargs)
def logout(self, **kwargs: Any) -> Any:
return self.login(
"/rest/n/user/logout",
{"token": self.device.token, "client_salt": self.device.client_salt},
include_auth=True,
param_profile="antman",
**kwargs,
)
def startup(self, ext_id: str = "", gp_referer: str = "", oaid: str = "", **kwargs: Any) -> Any:
signed = self.signed_request(
"POST",
"/rest/system/startup",
params={"extId": ext_id},
data={"gp_referer": gp_referer, "oaid": oaid},
sign_body=True,
include_auth=True,
)
return self.send(signed, capture_dynamic=True, **kwargs)
def version_check(self, **kwargs: Any) -> Any:
return self.get("/rest/antman/versionCheck", **kwargs)
def capture_dynamic_fields(self, response: Any, *, request_path: str = "") -> dict[str, str]:
changed: dict[str, str] = {}
cookies = _response_cookies(response)
if cookies:
changed.update(self.device.update_from_mapping(cookies, overwrite=True))
extras = {key: value for key, value in cookies.items() if key not in DEVICE_ALIAS_NAMES and value}
self.device.cookie_extras.update(extras)
payload = _response_json(response)
if payload is not None:
is_auth_response = any(marker in request_path for marker in ("/user/login/", "/token/infra/refreshToken"))
aliases = DEVICE_FIELD_ALIASES if is_auth_response else SAFE_RESPONSE_FIELD_ALIASES
for mapping in _high_confidence_response_mappings(payload):
changed.update(self.device.update_from_mapping(mapping, overwrite=True, aliases=aliases, recursive=False))
return changed
def dfp_device_info_bytes(self, fields: Mapping[str, Any] | None = None) -> bytes:
return encode_dfp_proto(build_lite_dfp_fields(fields))
def dfp_signed_form(
self,
field_name: str,
payload: bytes | str,
*,
product_name: str = DFP_PRODUCT_NAME,
now_ms: int | None = None,
sv: str = "2",
include_rdid: bool = True,
sign_mode: str = "10418",
encrypt_payload: bool = True,
) -> dict[str, str]:
if isinstance(payload, str):
payload = payload.encode("utf-8")
if now_ms is None:
now_ms = int(time.time() * 1000)
encrypted = zt_atlas_encrypt_like(payload, self.zt_table) if encrypt_payload and self.zt_table else payload
encoded = _dfp_encode_payload(encrypted)
ts = str(now_ms)
sign_input = (product_name + ts + sv + encoded).encode("utf-8")
sign = _dfp_sign_value(sign_input, self.zt_table, sign_mode)
if not encrypt_payload and sv == "2":
sv = "3"
sign_input = (product_name + ts + sv + encoded).encode("utf-8")
sign = _dfp_sign_value(sign_input, self.zt_table, sign_mode)
form = {
"productName": product_name,
"ts": ts,
field_name: encoded,
"sign": sign,
"sv": sv,
}
if include_rdid and self.device.rdid:
form["rdid"] = self.device.rdid
form["didtag"] = self.device.cdid_tag or "-1"
return form
def dfp_egid_request(
self,
*,
fields: Mapping[str, Any] | None = None,
endpoint: str = DFP_EGID_ENDPOINT,
sign_mode: str = "10418",
encrypt_payload: bool = True,
) -> SignedRequest:
form = self.dfp_signed_form(
"deviceInfo",
self.dfp_device_info_bytes(fields),
sign_mode=sign_mode,
encrypt_payload=encrypt_payload,
)
headers = self.base_headers()
headers.setdefault("Content-Type", "application/x-www-form-urlencoded")
return SignedRequest("POST", endpoint, headers, {}, form)
def dfp_did_request(
self,
*,
fields: Mapping[str, Any] | None = None,
endpoint: str = DFP_DID_ENDPOINT,
sign_mode: str = "10418",
encrypt_payload: bool = True,
) -> SignedRequest:
form = self.dfp_sync_lite_form(fields=fields, sign_mode=sign_mode, encrypt_payload=encrypt_payload)
headers = self.base_headers()
headers.setdefault("Content-Type", "application/x-www-form-urlencoded")
return SignedRequest("POST", endpoint, headers, {}, form)
def dfp_sync_lite_form(
self,
*,
fields: Mapping[str, Any] | None = None,
sign_mode: str = "10418",
encrypt_payload: bool = True,
request_id: str = "",
) -> dict[str, str]:
payload = self.dfp_device_info_bytes(fields)
encrypted = zt_atlas_encrypt_like(payload, self.zt_table) if encrypt_payload and self.zt_table else payload
encoded = _dfp_encode_payload(encrypted)
tree: dict[str, str] = {"didTag": "-1"}
self._add_dfp_fetch_common(tree, include_repair_fields=True)
tree["deviceInfo"] = encoded
tree["requestId"] = request_id or _dfp_request_id()
tree["sv"] = "2" if encrypt_payload else "3"
sign_input = "".join(value for _, value in _dfp_non_empty_items(tree))
form = {key: value for key, value in _dfp_non_empty_items(tree)}
form["sign"] = _dfp_sign_value(sign_input.encode("utf-8"), self.zt_table, sign_mode)
return form
def dfp_fetch_form(
self,
*,
fields: Mapping[str, Any] | None = None,
sign_mode: str = "10418",
encrypt_payload: bool = True,
request_id: str = "",
hgid_report_id: str = "",
) -> dict[str, str]:
payload = self.dfp_device_info_bytes(fields)
encrypted = zt_atlas_encrypt_like(payload, self.zt_table) if encrypt_payload and self.zt_table else payload
encoded = _dfp_encode_payload(encrypted)
decision_did = self.device.decision_did
if not decision_did and self.device.cdid_tag:
decision_did = self.device.did
decision = self.device.decision
if not decision and decision_did:
decision = "8"
decision_inputs = self.device.decision_inputs
tree: dict[str, str] = {
"didTag": self.device.cdid_tag or "-1",
"odid": self.device.odid or self.device.did,
}
self._add_dfp_fetch_common(tree, include_repair_fields=True)
tree["deviceInfo"] = encoded
tree["decisionDid"] = decision_did
tree["decision"] = decision
tree["decisionInputs"] = decision_inputs
if hgid_report_id:
tree["hgidReportId"] = hgid_report_id
if request_id:
tree["requestId"] = request_id
tree["sv"] = "2" if encrypt_payload else "3"
sign_input = "".join(value for _, value in _dfp_non_empty_items(tree))
form = {key: value for key, value in _dfp_non_empty_items(tree)}
form["sign"] = _dfp_sign_value(sign_input.encode("utf-8"), self.zt_table, sign_mode)
return form
def _add_dfp_fetch_common(self, form: dict[str, str], *, include_repair_fields: bool = False) -> None:
product_name = DFP_PRODUCT_NAME or "unknow"
form["ts"] = str(int(time.time() * 1000))
form["productName"] = product_name
form["appVersion"] = "10.6.30.1234" if product_name == "NEBULA" else DFP_APP_VERSION
form["sdkVersion"] = "8.9.1antman.40.05d13a7f"
form["platform"] = "1"
form.setdefault("didTag", self.device.cdid_tag or "-1")
if include_repair_fields:
if self.device.rdid:
form["rdid"] = self.device.rdid
form["aegon"] = "false"
form["did"] = self.device.did
def fetch_egid(
self,
*,
fields: Mapping[str, Any] | None = None,
endpoint: str = DFP_EGID_ENDPOINT,
sign_mode: str = "10418",
encrypt_payload: bool = True,
**kwargs: Any,
) -> Any:
return self.send(
self.dfp_egid_request(fields=fields, endpoint=endpoint, sign_mode=sign_mode, encrypt_payload=encrypt_payload),
capture_dynamic=True,
**kwargs,
)
def fetch_did(
self,
*,
fields: Mapping[str, Any] | None = None,
endpoint: str = DFP_DID_ENDPOINT,
sign_mode: str = "10418",
encrypt_payload: bool = True,
**kwargs: Any,
) -> Any:
return self.send(
self.dfp_did_request(fields=fields, endpoint=endpoint, sign_mode=sign_mode, encrypt_payload=encrypt_payload),
capture_dynamic=True,
**kwargs,
)
def local_dfp(self, fields: Mapping[str, Any] | None = None, *, encrypt: bool = False) -> str:
return build_lite_dfp(fields, encrypt=encrypt, table=self.zt_table)
def encrypt_azeroth_param(self, value: str, security_value: str) -> str:
return encrypt_param_with_fix(value, security_value)
def azeroth_client_sign(self, method: str, path: str, params: Mapping[str, Any]) -> str:
# Azeroth Java uses MXSec atlasSign; for this APK that resolves to the
# same 10417 sig3 body over SignatureUtil.d(method,path,params,null).
canonical = azeroth_canonical(method, path, params)
return sig3_10417(canonical.encode("utf-8"), self.zt_table)
client.py 的几个关键设计理由
RestEndpoint把 method/path/defaults/safe/profile/rewrite 分离,避免每个接口散落 if/else。signed_request()区分 GET 和 POST:GET 签 query,POST 签 body。这个对应ParamsUtils的z2分支。param_profile='auto'是为了让 antman/main_video/azeroth 不互相污染。capture-login导入 HAR 时过滤 captcha token,避免把验证码 token 当登录 token。- 私信发送提供
send=Falsedry-run,避免误触发真实发消息。
0x09 CLI:复现实验必须有最短入口
如果只有 Python class,每次复现实验都要手写脚本。CLI 的作用就是把关键操作固定成命令:打印请求、导入 HAR、列接口、跑信息类 smoke、私信 dry-run、fetch DID/EGID。
CLI 完整实现:kws_client.py
- 文件:
kws_client.py - 行数:
760 - 字节:
36555 - SHA256:
efd8ce61d733bfc233737974f071a9302485fca5ebdbfa9e74850ab317f9e443
为什么看它:它把 KwaiClient 的能力暴露成命令,便于复现和调试。
读完得到的结论:它不是算法来源,但它是复现实验入口;文中的命令都来自这里。
from __future__ import annotations
import argparse
import json
import sys
from http.cookies import SimpleCookie
from pathlib import Path
from kws_recovered.client import (
INFO_REST_ENDPOINT_NAMES,
REST_ENDPOINTS,
REST_INFO_CATEGORIES,
KwaiClient,
KwaiDeviceContext,
SignedRequest,
build_captcha_manual_html,
parse_captcha_challenge,
)
ROOT = Path(__file__).resolve().parent
DEFAULT_ASSETS = ROOT / "resources" / "assets"
DEFAULT_STATE = ROOT / "kws_device_state.json"
PARAM_PROFILES = [
"auto",
"antman",
"main_video",
"main_video_login",
"main_video_compat",
"main_video_device",
"azeroth",
"azeroth_latest",
"azeroth_device",
]
def parse_cookie_header(cookie_header: str) -> dict[str, str]:
if not cookie_header:
return {}
parsed = SimpleCookie()
try:
parsed.load(cookie_header)
return {key: morsel.value for key, morsel in parsed.items() if morsel.value}
except Exception:
out: dict[str, str] = {}
for item in cookie_header.split(";"):
if "=" not in item:
continue
key, value = item.split("=", 1)
key = key.strip()
value = value.strip()
if key and value:
out[key] = value
return out
def parse_pairs(items: list[str]) -> dict[str, str]:
out: dict[str, str] = {}
for item in items:
if "=" not in item:
raise SystemExit(f"bad key=value argument: {item}")
key, value = item.split("=", 1)
out[key] = value
return out
def print_signed(signed: SignedRequest) -> None:
print(
json.dumps(
{
"method": signed.method,
"url": signed.url,
"headers": signed.headers,
"params": signed.params,
"data": signed.data,
},
ensure_ascii=False,
indent=2,
)
)
def build_device(args: argparse.Namespace) -> KwaiDeviceContext:
state_path = Path(args.state)
if state_path.exists():
device = KwaiDeviceContext.from_json_file(state_path)
else:
device = KwaiDeviceContext()
overrides = {
"did": args.did,
"egid": args.egid,
"rdid": args.rdid,
"odid": args.odid,
"decision_did": args.decision_did,
"decision": args.decision,
"decision_inputs": args.decision_inputs,
"token": args.token,
"api_st": args.api_st,
"client_salt": args.client_salt,
"ud": args.ud,
"service_token": args.service_token,
"azeroth_security": args.azeroth_security,
}
device.update_from_mapping({key: value for key, value in overrides.items() if value}, overwrite=True)
cookie_map = parse_cookie_header(args.cookie)
if cookie_map:
device.update_from_mapping(cookie_map, overwrite=True)
device.cookie_extras.update({key: value for key, value in cookie_map.items() if key and value})
return device
def maybe_save(device: KwaiDeviceContext, args: argparse.Namespace) -> None:
stateful_actions = {
"capture-login",
"login",
"startup",
"mobile-check",
"request-mobile-code",
"verify-code-login",
"phone-login",
"register-phone-code-login",
"token-login",
"register-phone-v2",
"refresh-token",
"logout",
"rest-call",
"rest-smoke",
"rest-info-smoke",
"private-chat-list",
"private-chat-messages",
"fetch-egid",
"fetch-did",
}
if args.save_state or getattr(args, "send", False) or getattr(args, "action", "") in stateful_actions:
device.save_json_file(args.state)
def add_common(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--host", default="https://apissl.ksapisrv.com")
parser.add_argument("--assets", default=str(DEFAULT_ASSETS))
parser.add_argument("--state", default=str(DEFAULT_STATE), help="device/auth state JSON")
parser.add_argument("--save-state", action="store_true")
parser.add_argument("--did", default="")
parser.add_argument("--egid", default="")
parser.add_argument("--rdid", default="")
parser.add_argument("--odid", default="")
parser.add_argument("--decision-did", default="")
parser.add_argument("--decision", default="")
parser.add_argument("--decision-inputs", default="")
parser.add_argument("--token", default="")
parser.add_argument("--api-st", default="")
parser.add_argument("--client-salt", default="")
parser.add_argument("--ud", default="", help="logged-in user id")
parser.add_argument("--service-token", default="", help="Azeroth/service token")
parser.add_argument("--azeroth-security", default="", help="Azeroth client security secret")
parser.add_argument("--cookie", default="", help="raw Cookie/Set-Cookie header to import into state")
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Recovered Kwai web API signing client")
add_common(parser)
subparsers = parser.add_subparsers(dest="action")
sign = subparsers.add_parser("sign", help="sign any API request")
sign.add_argument("method", help="HTTP method, e.g. GET/POST")
sign.add_argument("path", help="API path or full URL, e.g. /rest/n/feed/hot")
sign.add_argument("--param", action="append", default=[], help="query key=value; can repeat")
sign.add_argument("--data", action="append", default=[], help="form key=value; can repeat")
sign.add_argument("--sig3-mode", default="10418", choices=["10405", "10417", "10418"])
sign.add_argument("--param-profile", default="auto", choices=PARAM_PROFILES)
sign.add_argument("--no-rewrite", action="store_true", help="do not rewrite /rest/n to /rest/antman")
sign.add_argument("--send", action="store_true", help="actually send request with requests")
login = subparsers.add_parser("login", help="send a signed login request and capture token fields")
login.add_argument("path", help="login API path or full URL")
login.add_argument("--method", default="POST")
login.add_argument("--param", action="append", default=[], help="query key=value; can repeat")
login.add_argument("--data", action="append", default=[], help="form key=value; can repeat")
login.add_argument("--include-auth", action="store_true")
login.add_argument("--sig3-mode", default="10418", choices=["10405", "10417", "10418"])
login.add_argument("--param-profile", default="main_video_login", choices=PARAM_PROFILES)
login.add_argument("--no-rewrite", action="store_true", help="do not rewrite /rest/n to /rest/antman")
fetch_egid = subparsers.add_parser("fetch-egid", help="request server-issued egid using recovered DFP form")
sign_modes = ["sha256", "double-sha256", "10418", "10417", "10405", "10418-sha256", "none"]
fetch_egid.add_argument("--sign-mode", default="10418", choices=sign_modes)
fetch_egid.add_argument("--plain", action="store_true", help="send unencrypted fallback sv=3 payload")
fetch_did = subparsers.add_parser("fetch-did", help="request server-issued cloud did using recovered DFP form")
fetch_did.add_argument("--sign-mode", default="10418", choices=sign_modes)
fetch_did.add_argument("--plain", action="store_true", help="send unencrypted fallback sv=3 payload")
startup = subparsers.add_parser("startup", help="POST /rest/system/startup and capture issued fields")
startup.add_argument("--ext-id", default="")
startup.add_argument("--gp-referer", default="")
startup.add_argument("--oaid", default="")
subparsers.add_parser("version-check", help="GET /rest/antman/versionCheck")
mobile_check = subparsers.add_parser("mobile-check", help="POST n/user/mobile/checker")
mobile_check.add_argument("--mobile", required=True)
mobile_check.add_argument("--country-code", default="86")
request_code = subparsers.add_parser("request-mobile-code", help="POST n/user/requestMobileCode; sends an SMS")
request_code.add_argument("--mobile", required=True)
request_code.add_argument("--country-code", default="86")
request_code.add_argument("--type", type=int, default=27)
captcha_page = subparsers.add_parser("captcha-page", help="build a manual captcha page from a 705 error_url")
captcha_page.add_argument("--error-url", default="")
captcha_page.add_argument("--from-response", default=str(ROOT / "ctf_phone_login_response_private.json"))
captcha_page.add_argument("--out", default=str(ROOT / "ctf_captcha_manual.html"))
captcha_verify = subparsers.add_parser("captcha-verify", help="exchange captcha iframe ticket for captcha_token")
captcha_verify.add_argument("--error-url", default="", help="705 error_url; used as referer and to fill key/type/uri")
captcha_verify.add_argument("--key", default="")
captcha_verify.add_argument("--uri", default="")
captcha_verify.add_argument("--type", dest="challenge_type", default="")
captcha_verify.add_argument("--ticket", required=True)
verify_code = subparsers.add_parser("verify-code-login", help="POST n/user/login/mobileVerifyCode")
verify_code.add_argument("--mobile", required=True)
verify_code.add_argument("--code", required=True)
verify_code.add_argument("--country-code", default="86")
verify_code.add_argument("--type", type=int, default=27)
verify_code.add_argument("--extra", action="append", default=[], help="extra form key=value; can repeat")
verify_code.add_argument("--captcha-token", default="", help="captcha_token returned by captcha-verify")
verify_code.add_argument("--param-profile", default="main_video_login", choices=PARAM_PROFILES)
verify_code.add_argument("--sig3-mode", default="10418", choices=["10405", "10417", "10418"])
verify_code.add_argument("--no-account-keypair", action="store_true", help="do not add raw/secret RSA fields")
verify_code.add_argument("--include-public-key", action="store_true", help="also add publicKey/deviceName/deviceMode fields")
phone_login = subparsers.add_parser("phone-login", help="alias for verify-code-login; captures and saves login state")
phone_login.add_argument("--mobile", required=True)
phone_login.add_argument("--code", required=True)
phone_login.add_argument("--country-code", default="86")
phone_login.add_argument("--type", type=int, default=27)
phone_login.add_argument("--extra", action="append", default=[], help="extra form key=value; can repeat")
phone_login.add_argument("--captcha-token", default="", help="captcha_token returned by captcha-verify")
phone_login.add_argument("--param-profile", default="main_video_login", choices=PARAM_PROFILES)
phone_login.add_argument("--sig3-mode", default="10418", choices=["10405", "10417", "10418"])
phone_login.add_argument("--no-account-keypair", action="store_true", help="do not add raw/secret RSA fields")
phone_login.add_argument("--include-public-key", action="store_true", help="also add publicKey/deviceName/deviceMode fields")
register_code_login = subparsers.add_parser("register-phone-code-login", help="POST n/user/register/mobileV2 with plugin FieldMap")
register_code_login.add_argument("--mobile", required=True)
register_code_login.add_argument("--code", required=True)
register_code_login.add_argument("--country-code", default="86")
register_code_login.add_argument("--extra", action="append", default=[], help="extra form key=value; can repeat")
register_code_login.add_argument("--captcha-token", default="", help="captcha_token returned by captcha-verify")
register_code_login.add_argument("--param-profile", default="main_video_login", choices=PARAM_PROFILES)
register_code_login.add_argument("--sig3-mode", default="10418", choices=["10405", "10417", "10418"])
register_code_login.add_argument("--no-account-keypair", action="store_true", help="do not add raw/secret RSA fields")
token_login = subparsers.add_parser("token-login", help="POST n/user/login/token")
token_login.add_argument("--login-token", required=True)
token_login.add_argument("--extra", action="append", default=[], help="extra form key=value; can repeat")
token_login.add_argument("--no-account-keypair", action="store_true", help="do not add publicKey/raw/secret RSA fields")
third_login = subparsers.add_parser("third-platform-login", help="POST user/thirdPlatformLogin")
third_login.add_argument("--platform", required=True, help="e.g. wechat/qq/weibo")
third_login.add_argument("--access-token", required=True)
third_login.add_argument("--open-id", default="")
third_login.add_argument("--extra", action="append", default=[], help="extra form key=value; can repeat")
third_login.add_argument("--no-account-keypair", action="store_true", help="do not add publicKey/raw/secret RSA fields")
wechat_auth = subparsers.add_parser("wechat-auth-code", help="POST n/wechat/oauth2/authByCode")
wechat_auth.add_argument("--code", required=True)
wechat_auth.add_argument("--extra", action="append", default=[], help="extra form key=value; can repeat")
verify_trust = subparsers.add_parser("verify-trust-device", help="POST n/user/verifyTrustDevice")
verify_trust.add_argument("--is-add-account", action="store_true")
verify_trust.add_argument("--extra", action="append", default=[], help="extra form key=value; can repeat")
register_phone = subparsers.add_parser("register-phone-v2", help="POST n/user/register/mobileV2")
register_phone.add_argument("--mobile", required=True)
register_phone.add_argument("--code", required=True)
register_phone.add_argument("--password", required=True)
register_phone.add_argument("--user-name", default="")
register_phone.add_argument("--gender", default="U")
register_phone.add_argument("--country-code", default="86")
register_phone.add_argument("--extra", action="append", default=[], help="extra form key=value; can repeat")
refresh_token = subparsers.add_parser("refresh-token", help="POST n/token/infra/refreshToken")
refresh_token.add_argument("--extra", action="append", default=[], help="extra form key=value; can repeat")
subparsers.add_parser("logout", help="POST n/user/logout")
rest_list = subparsers.add_parser("rest-list", help="list recovered REST endpoints")
rest_list.add_argument("--safe-only", action="store_true")
rest_list.add_argument("--info-only", action="store_true", help="only list read-only information endpoints")
rest_list.add_argument("--category", choices=["all", *sorted(REST_INFO_CATEGORIES)], default="all", help="filter read-only information endpoints by category")
rest_call = subparsers.add_parser("rest-call", help="call a recovered REST endpoint by name")
rest_call.add_argument("name", choices=sorted(REST_ENDPOINTS))
rest_call.add_argument("--param", action="append", default=[], help="query key=value; can repeat")
rest_call.add_argument("--data", action="append", default=[], help="form key=value; can repeat")
rest_call.add_argument("--sig3-mode", default="10418", choices=["10405", "10417", "10418"])
rest_call.add_argument("--print-only", action="store_true", help="only print the signed request")
rest_smoke = subparsers.add_parser("rest-smoke", help="call safe recovered REST endpoints")
rest_smoke.add_argument("--name", action="append", default=[], choices=sorted(REST_ENDPOINTS), help="safe endpoint name; can repeat")
rest_info = subparsers.add_parser("rest-info-smoke", help="call read-only information REST chain")
rest_info.add_argument("--category", choices=["all", *sorted(REST_INFO_CATEGORIES)], default="all", help="which information category to smoke test")
rest_info.add_argument("--count", default="1", help="feed/comment count for chained probes")
rest_info.add_argument("--keyword", default="猫", help="search keyword for search probes; empty to skip")
private_chat_list = subparsers.add_parser("private-chat-list", help="list private chat dialogs")
private_chat_list.add_argument("--count", default="20")
private_chat_list.add_argument("--page", default="")
private_chat_list.add_argument("--pcursor", default="")
private_chat_list.add_argument("--extra", action="append", default=[], help="extra form key=value; can repeat")
private_chat_messages = subparsers.add_parser("private-chat-messages", help="list private chat messages with a user")
private_chat_messages.add_argument("--user-id", required=True)
private_chat_messages.add_argument("--page", default="")
private_chat_messages.add_argument("--order", default="desc")
private_chat_messages.add_argument("--pcursor", default="")
private_chat_messages.add_argument("--extra", action="append", default=[], help="extra form key=value; can repeat")
private_chat_send = subparsers.add_parser("private-chat-send", help="send a private chat message")
private_chat_send.add_argument("--user-id", required=True)
private_chat_send.add_argument("--content", required=True)
private_chat_send.add_argument("--copy", action="store_true")
private_chat_send.add_argument("--photo-id", default="")
private_chat_send.add_argument("--live-stream-id", default="")
private_chat_send.add_argument("--informed-user-id", default="")
private_chat_send.add_argument("--extra", action="append", default=[], help="extra form key=value; can repeat")
private_chat_send.add_argument("--dry-run", action="store_true", help="print signed request without sending")
capture_login = subparsers.add_parser("capture-login", help="import login state from captured request/response data")
capture_login.add_argument("--file", default="", help="capture file: HAR/JSON/curl/raw headers/raw body")
capture_login.add_argument("--stdin", action="store_true", help="read capture data from stdin")
capture_login.add_argument("--no-overwrite", action="store_true", help="keep existing state fields when already set")
capture_login.add_argument("--show-sensitive", action="store_true", help="print unredacted imported state summary")
video_chain = subparsers.add_parser("video-chain", help="fetch hot feed, then photo info and comments for the first photo")
video_chain.add_argument("--count", default="1")
video_chain.add_argument("--photo-id", default="", help="skip feed extraction and use this photo id")
video_chain.add_argument("--user-id", default="", help="preUserId for photo info when --photo-id is used")
video_chain.add_argument("--exp-tag", default="", help="preExpTag for photo info when --photo-id is used")
video_chain.add_argument("--llsid", default="", help="preLLSId/list-load-sequence id for photo info")
video_chain.add_argument("--comment-count", default="1")
subparsers.add_parser("state", help="print current device/auth state")
return parser
def send_and_print(client: KwaiClient, signed: SignedRequest) -> int:
response = client.send(signed)
sys.stdout.buffer.write(f"{response.status_code}\n".encode("ascii"))
sys.stdout.buffer.write(response.text.encode("utf-8", errors="replace"))
sys.stdout.buffer.write(b"\n")
return 0
def print_response(response: object) -> int:
status_code = getattr(response, "status_code", "")
sys.stdout.buffer.write(f"{status_code}\n".encode("utf-8", errors="replace"))
text = getattr(response, "text", "")
sys.stdout.buffer.write(str(text).encode("utf-8", errors="replace"))
sys.stdout.buffer.write(b"\n")
return 0
def print_json(payload: object) -> int:
sys.stdout.buffer.write(json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8", errors="replace"))
sys.stdout.buffer.write(b"\n")
return 0
def load_captcha_error_url(error_url: str, response_path: str) -> str:
if error_url:
return error_url
path = Path(response_path)
if path.exists():
payload = json.loads(path.read_text(encoding="utf-8"))
if isinstance(payload, dict):
return str(payload.get("error_url") or "")
return ""
def parse_extra_args(args: argparse.Namespace) -> dict[str, str]:
extra = parse_pairs(getattr(args, "extra", []))
captcha_token = getattr(args, "captcha_token", "")
if captcha_token:
extra["captcha_token"] = captcha_token
return extra
def load_capture_payload(args: argparse.Namespace) -> object:
if getattr(args, "stdin", False):
return sys.stdin.buffer.read().decode("utf-8", errors="replace")
capture_file = getattr(args, "file", "")
if not capture_file:
raise SystemExit("capture-login requires --file or --stdin")
text = Path(capture_file).read_bytes().decode("utf-8-sig", errors="replace")
try:
return json.loads(text)
except Exception:
return text
def main(argv: list[str] | None = None) -> int:
raw = list(sys.argv[1:] if argv is None else argv)
if raw and raw[0].upper() in {"GET", "POST", "PUT", "PATCH", "DELETE"}:
raw.insert(0, "sign")
args = build_parser().parse_args(raw)
if not args.action:
args.action = "state"
device = build_device(args)
client = KwaiClient(device, host=args.host, assets_root=args.assets)
try:
if args.action == "state":
print(json.dumps(device.to_dict(), ensure_ascii=False, indent=2))
return 0
if args.action == "captcha-page":
error_url = load_captcha_error_url(args.error_url, args.from_response)
if not error_url:
raise SystemExit("captcha-page requires --error-url or a response JSON containing error_url")
challenge, response = client.fetch_captcha_challenge(error_url)
latest_page = ROOT / "ctf_captcha_page_latest.html"
latest_page.write_text(getattr(response, "text", "") or "", encoding="utf-8")
fallback_page = ROOT / "ctf_captcha_page.html"
used_fallback = False
if not challenge.captcha_session and fallback_page.exists():
fallback = parse_captcha_challenge(error_url, fallback_page.read_text(encoding="utf-8", errors="replace"))
if fallback.captcha_session:
challenge = fallback
used_fallback = True
out = Path(args.out)
out.write_text(build_captcha_manual_html(challenge), encoding="utf-8")
return print_json(
{
"out": str(out),
"page_status": getattr(response, "status_code", None),
"saved_page": str(latest_page),
"used_saved_page_fallback": used_fallback,
"key": challenge.key,
"type": challenge.verify_type,
"uri": challenge.uri,
"captcha_session_found": bool(challenge.captcha_session),
"iframe_url": challenge.iframe_url,
}
)
if args.action == "captcha-verify":
challenge = parse_captcha_challenge(args.error_url) if args.error_url else None
if args.error_url:
try:
challenge, _ = client.fetch_captcha_challenge(args.error_url)
except Exception:
challenge = parse_captcha_challenge(args.error_url)
key = args.key or (challenge.key if challenge else "")
uri = args.uri or (challenge.uri if challenge else "") or "/rest/n/user/login/mobileVerifyCode"
challenge_type = args.challenge_type or (challenge.verify_type if challenge else "") or "7"
if not key:
raise SystemExit("captcha-verify requires --key or --error-url with key")
return print_response(
client.captcha_verify(
key,
args.ticket,
uri=uri,
verify_type=challenge_type,
referer=args.error_url,
)
)
if args.action == "rest-list":
endpoints = []
names = client.endpoint_names(safe_only=args.safe_only)
if args.info_only:
info_names = INFO_REST_ENDPOINT_NAMES if args.category == "all" else REST_INFO_CATEGORIES[args.category]
names = [name for name in names if name in info_names]
for name in names:
endpoint = REST_ENDPOINTS[name]
endpoints.append(
{
"name": endpoint.name,
"method": endpoint.method,
"path": endpoint.path,
"safe": endpoint.safe,
"include_auth": endpoint.include_auth,
"service": endpoint.service,
"java_method": endpoint.java_method,
"fields": list(endpoint.fields),
"query_fields": list(endpoint.query_fields),
"param_profile": endpoint.param_profile,
"rewrite_path": endpoint.rewrite_path,
"defaults": dict(endpoint.defaults),
"query_defaults": dict(endpoint.query_defaults),
"description": endpoint.description,
}
)
return print_json(endpoints)
if args.action == "rest-call":
signed = client.rest_request(
args.name,
params=parse_pairs(args.param),
data=parse_pairs(args.data),
sig3_mode=args.sig3_mode,
)
if args.print_only:
print_signed(signed)
return 0
return send_and_print(client, signed)
if args.action == "rest-smoke":
names = args.name or None
results = client.smoke_test_rest(names)
return print_json(results)
if args.action == "rest-info-smoke":
return print_json(client.smoke_test_info_rest(category=args.category, count=args.count, keyword=args.keyword))
if args.action == "private-chat-list":
return print_response(
client.private_chat_list(
count=args.count,
page=args.page,
pcursor=args.pcursor,
extra=parse_extra_args(args),
)
)
if args.action == "private-chat-messages":
return print_response(
client.private_chat_messages(
args.user_id,
page=args.page,
order=args.order,
pcursor=args.pcursor,
extra=parse_extra_args(args),
)
)
if args.action == "private-chat-send":
result = client.private_chat_send(
args.user_id,
args.content,
copy=args.copy,
photo_id=args.photo_id,
live_stream_id=args.live_stream_id,
informed_user_id=args.informed_user_id,
extra=parse_extra_args(args),
send=not args.dry_run,
)
if args.dry_run:
print_signed(result)
return 0
return print_response(result)
if args.action == "capture-login":
changed = device.import_login_capture(load_capture_payload(args), overwrite=not args.no_overwrite)
state = device.to_dict() if args.show_sensitive else device.to_redacted_dict()
return print_json(
{
"changed_fields": sorted(changed),
"has_login": bool(device.token or device.api_st or device.service_token),
"state": state,
}
)
if args.action == "video-chain":
result: dict[str, object] = {}
photo_id = args.photo_id
user_id = args.user_id
exp_tag = args.exp_tag
llsid = args.llsid
photo_index = 0
if not photo_id:
feed_response = client.video_feed_hot(count=args.count)
feed_payload = feed_response.json()
result["feed_status"] = feed_response.status_code
result["feed_result"] = feed_payload.get("result") if isinstance(feed_payload, dict) else None
result["feed_preview"] = getattr(feed_response, "text", "")[:500]
extracted = client.extract_video_ids(feed_payload)
result["extracted"] = extracted[:5]
if extracted:
photo_id = extracted[0]["photo_id"]
user_id = extracted[0].get("user_id", "")
exp_tag = extracted[0].get("exp_tag", "")
llsid = extracted[0].get("llsid", "")
result["user_id"] = user_id
result["exp_tag"] = exp_tag
result["llsid"] = llsid
result["photo_id"] = photo_id
if photo_id:
info_response = client.video_photo_info(
photo_id,
user_id=user_id,
exp_tag=exp_tag,
llsid=llsid,
photo_index=photo_index,
)
comments_response = client.video_photo_comments(photo_id, count=args.comment_count)
result["photo_info_status"] = info_response.status_code
result["photo_info_preview"] = getattr(info_response, "text", "")[:1200]
result["comments_status"] = comments_response.status_code
result["comments_preview"] = getattr(comments_response, "text", "")[:1200]
return print_json(result)
if args.action == "sign":
params = parse_pairs(args.param)
data = parse_pairs(args.data)
signed = client.signed_request(
args.method,
args.path,
params=params,
data=data if args.method.upper() != "GET" else None,
sign_body=args.method.upper() != "GET",
sig3_mode=args.sig3_mode,
param_profile=args.param_profile,
rewrite_path=not args.no_rewrite,
)
if args.send:
return send_and_print(client, signed)
print_signed(signed)
return 0
if args.action == "login":
response = client.login(
args.path,
data=parse_pairs(args.data),
params=parse_pairs(args.param),
method=args.method,
include_auth=args.include_auth,
sig3_mode=args.sig3_mode,
param_profile=args.param_profile,
rewrite_path=not args.no_rewrite,
)
return print_response(response)
if args.action == "startup":
return print_response(client.startup(ext_id=args.ext_id, gp_referer=args.gp_referer, oaid=args.oaid))
if args.action == "version-check":
return print_response(client.version_check())
if args.action == "mobile-check":
return print_response(client.login_mobile_check(args.mobile, country_code=args.country_code))
if args.action == "request-mobile-code":
return print_response(client.request_mobile_code(args.mobile, country_code=args.country_code, code_type=args.type))
if args.action == "verify-code-login":
return print_response(
client.verify_code_login(
args.mobile,
args.code,
country_code=args.country_code,
code_type=args.type,
extra=parse_extra_args(args),
param_profile=args.param_profile,
sig3_mode=args.sig3_mode,
use_account_keypair=not args.no_account_keypair,
include_public_key=args.include_public_key,
)
)
if args.action == "phone-login":
return print_response(
client.phone_code_login(
args.mobile,
args.code,
country_code=args.country_code,
code_type=args.type,
extra=parse_extra_args(args),
param_profile=args.param_profile,
sig3_mode=args.sig3_mode,
use_account_keypair=not args.no_account_keypair,
include_public_key=args.include_public_key,
)
)
if args.action == "register-phone-code-login":
return print_response(
client.register_by_phone_code_login(
args.mobile,
args.code,
country_code=args.country_code,
extra=parse_extra_args(args),
param_profile=args.param_profile,
sig3_mode=args.sig3_mode,
use_account_keypair=not args.no_account_keypair,
)
)
if args.action == "token-login":
return print_response(
client.token_login(
args.login_token,
extra=parse_extra_args(args),
use_account_keypair=not args.no_account_keypair,
)
)
if args.action == "third-platform-login":
return print_response(
client.third_platform_login(
args.platform,
args.access_token,
open_id=args.open_id,
extra=parse_extra_args(args),
use_account_keypair=not args.no_account_keypair,
)
)
if args.action == "wechat-auth-code":
return print_response(client.auth_wechat_code(args.code, extra=parse_extra_args(args)))
if args.action == "verify-trust-device":
return print_response(
client.verify_trust_device(
is_add_account=args.is_add_account,
extra=parse_extra_args(args),
)
)
if args.action == "register-phone-v2":
return print_response(
client.register_by_phone_v2(
args.mobile,
args.code,
args.password,
user_name=args.user_name,
gender=args.gender,
country_code=args.country_code,
extra=parse_extra_args(args),
)
)
if args.action == "refresh-token":
return print_response(client.refresh_token(extra=parse_extra_args(args)))
if args.action == "logout":
return print_response(client.logout())
if args.action == "fetch-egid":
response = client.fetch_egid(sign_mode=args.sign_mode, encrypt_payload=not args.plain)
print(response.status_code)
print(response.text)
return 0
if args.action == "fetch-did":
response = client.fetch_did(sign_mode=args.sign_mode, encrypt_payload=not args.plain)
print(response.status_code)
print(response.text)
return 0
raise SystemExit(f"unknown action: {args.action}")
finally:
maybe_save(device, args)
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
0x0A 抓包环境:Cronet-only QUIC 降级脚本
之前实机 hook ClassLoader 崩过,说明太激进的 Java/ART 热路径 hook 会触发不稳定甚至检测。因此最终 Frida 脚本只处理 Cronet Builder,不枚举 ClassLoader,不 hook UDP/native。
Frida 完整脚本:cronet_disable_quic.js
- 文件:
frida/cronet_disable_quic.js - 行数:
244 - 字节:
6952 - SHA256:
4e3cc86b7d73676b3e4ff0e8ab61afc86021d41d750229a384e1fbd0f35f03ad
为什么看它:它只尝试禁用 Cronet QUIC/HTTP3,让请求回落到 TCP/TLS,方便代理抓包。
读完得到的结论:这不是绕过 HTTPS,也不是绕风控;只是降低 QUIC 对抓包的影响。
'use strict';
/*
* com.smile.gifmaker / Kwai 14.0.40.46379
* Cronet-only QUIC downgrade helper.
*
* Low-intrusion build for anti-crash/anti-detection cases:
* - no ClassLoader.loadClass hook
* - no enumerateLoadedClasses / enumerateClassLoaders
* - no OkHttp, UDP, socket, or native hooks
* - only patches visible Cronet Java builder APIs
*/
const CFG = {
packageName: 'com.smile.gifmaker',
versionName: '14.0.40.46379',
verbose: true,
scanIntervalMs: 2000,
stopAfterMs: 120000,
};
const CRONET_BUILDERS = [
'org.chromium.net.CronetEngine$Builder',
'org.chromium.net.ExperimentalCronetEngine$Builder',
'org.chromium.net.impl.CronetEngineBuilderImpl',
'org.chromium.net.impl.ExperimentalCronetEngineBuilderImpl',
'org.chromium.net.impl.NativeCronetEngineBuilderImpl',
'org.chromium.net.impl.JavaCronetEngineBuilderImpl',
'com.google.android.gms.net.CronetEngine$Builder',
];
const installed = Object.create(null);
const startTime = Date.now();
let timer = null;
function log(msg) {
if (CFG.verbose) {
console.log('[kws-cronet-noquic] ' + msg);
}
}
function once(key, fn) {
if (installed[key]) return false;
try {
fn();
installed[key] = true;
log('installed ' + key);
return true;
} catch (e) {
const s = String(e);
if (s.indexOf('ClassNotFoundException') === -1 && s.indexOf('Unable to find class') === -1) {
log('skip ' + key + ': ' + s);
}
return false;
}
}
function noQuicOptions(input) {
let obj = {};
const text = input === null || input === undefined ? '' : String(input).trim();
if (text.length > 0) {
try {
obj = JSON.parse(text);
} catch (_) {
obj = {};
}
}
if (!obj.QUIC || typeof obj.QUIC !== 'object' || Array.isArray(obj.QUIC)) {
obj.QUIC = {};
}
obj.QUIC.enable_quic = false;
obj.QUIC.enabled = false;
obj.QUIC.host_whitelist = '';
obj.QUIC.quic_host_whitelist = '';
obj.QUIC.connection_options = '';
obj.QUIC.client_connection_options = '';
obj.QUIC.max_server_configs_stored_in_properties = 0;
return JSON.stringify(obj);
}
function hookBuilder(className) {
const Builder = Java.use(className);
let changed = false;
if (Builder.enableQuic) {
Builder.enableQuic.overloads.forEach(function (ov, i) {
if (ov.argumentTypes.length === 1 && ov.argumentTypes[0].className === 'boolean') {
changed = once(className + '.enableQuic#' + i, function () {
ov.implementation = function (enabled) {
log(className + '.enableQuic(' + enabled + ') => false');
return ov.call(this, false);
};
}) || changed;
}
});
}
if (Builder.setQuicEnabled) {
Builder.setQuicEnabled.overloads.forEach(function (ov, i) {
if (ov.argumentTypes.length === 1 && ov.argumentTypes[0].className === 'boolean') {
changed = once(className + '.setQuicEnabled#' + i, function () {
ov.implementation = function (enabled) {
log(className + '.setQuicEnabled(' + enabled + ') => false');
return ov.call(this, false);
};
}) || changed;
}
});
}
if (Builder.addQuicHint) {
Builder.addQuicHint.overloads.forEach(function (ov, i) {
changed = once(className + '.addQuicHint#' + i, function () {
ov.implementation = function () {
log(className + '.addQuicHint(...) ignored');
return this;
};
}) || changed;
});
}
if (Builder.setExperimentalOptions) {
Builder.setExperimentalOptions.overloads.forEach(function (ov, i) {
if (ov.argumentTypes.length === 1 && ov.argumentTypes[0].className === 'java.lang.String') {
changed = once(className + '.setExperimentalOptions#' + i, function () {
ov.implementation = function (options) {
log(className + '.setExperimentalOptions(...) => QUIC off');
return ov.call(this, noQuicOptions(options));
};
}) || changed;
}
});
}
if (Builder.build) {
Builder.build.overloads.forEach(function (ov, i) {
changed = once(className + '.build#' + i, function () {
ov.implementation = function () {
try {
if (this.enableQuic) this.enableQuic(false);
} catch (_) {}
try {
if (this.setQuicEnabled) this.setQuicEnabled(false);
} catch (_) {}
try {
if (this.setExperimentalOptions) this.setExperimentalOptions(noQuicOptions('{}'));
} catch (_) {}
log(className + '.build() => force QUIC off before build');
return ov.call(this);
};
}) || changed;
});
}
return changed;
}
function patchKnownCronetClasses() {
let count = 0;
CRONET_BUILDERS.forEach(function (name) {
try {
if (hookBuilder(name)) count++;
} catch (e) {
const s = String(e);
if (s.indexOf('ClassNotFoundException') === -1 && s.indexOf('Unable to find class') === -1) {
log('probe failed ' + name + ': ' + s);
}
}
});
return count;
}
function withLoader(loader, label) {
if (!loader) return 0;
let previous = null;
try {
previous = Java.classFactory.loader;
Java.classFactory.loader = loader;
const hits = patchKnownCronetClasses();
if (hits > 0) log(label + ' loader hits=' + hits);
return hits;
} catch (e) {
log(label + ' loader probe failed: ' + e);
return 0;
} finally {
try {
Java.classFactory.loader = previous;
} catch (_) {}
}
}
function getApplication() {
try {
const ActivityThread = Java.use('android.app.ActivityThread');
return ActivityThread.currentApplication();
} catch (_) {
return null;
}
}
function identityAndLoaders() {
const loaders = [];
try {
const app = getApplication();
if (app) {
const pkg = String(app.getPackageName());
log('attached package=' + pkg + ', target=' + CFG.packageName + ', apk=' + CFG.versionName);
loaders.push({ label: 'application', loader: app.getClassLoader() });
}
} catch (e) {
log('application loader skipped: ' + e);
}
try {
const Thread = Java.use('java.lang.Thread');
loaders.push({ label: 'context', loader: Thread.currentThread().getContextClassLoader() });
} catch (e) {
log('context loader skipped: ' + e);
}
return loaders;
}
function scan() {
Java.perform(function () {
let hits = patchKnownCronetClasses();
identityAndLoaders().forEach(function (item) {
hits += withLoader(item.loader, item.label);
});
if (hits > 0) {
log('Cronet hooks ready, hits=' + hits);
}
if (Date.now() - startTime > CFG.stopAfterMs && timer !== null) {
clearInterval(timer);
timer = null;
log('stop polling; keep installed hooks active');
}
});
}
if (Java.available) {
setImmediate(scan);
timer = setInterval(scan, CFG.scanIntervalMs);
} else {
log('Java runtime unavailable');
}
0x0B 测试:把逆向结论固定下来
逆向最怕“今天能跑,明天改坏”。所以所有关键结论都要进测试:sig shape、sig3 三模式、DFP form、Azeroth canonical、REST endpoint shape、HAR 导入、私信 dry-run。
测试完整代码:tests/test_recovered_algorithms.py
- 文件:
tests/test_recovered_algorithms.py - 行数:
854 - 字节:
35194 - SHA256:
d82a39ef7233e191546223a140951681813863fed300e2a4c53125a045a259fa
为什么看它:它固定算法、协议 shape、HAR 导入策略、私信 API shape。
读完得到的结论:测试是本文证据链的最后一环:不只是分析出来,还要能被机器重复验证。
from pathlib import Path
from kws_recovered.algorithms import (
APPKEY_AZEROTH,
APPKEY_MAIN,
build_lite_dfp,
decode_zt_png,
find_zt_table,
get_clock,
load_kwai_dynamic_sections,
kwai_sig,
kwai_token_sig,
native_obfuscated_string,
azeroth_canonical,
azeroth_nonce,
rc4_ksa_prga,
sig3_10405,
sig3_10417,
sig3_10418,
)
from kws_recovered.client import (
INFO_REST_ENDPOINT_NAMES,
MAIN_VIDEO_LOGIN_APPVER,
MAIN_VIDEO_LOGIN_VER,
REST_ENDPOINTS,
REST_INFO_CATEGORIES,
KwaiClient,
KwaiDeviceContext,
)
ROOT = Path(__file__).resolve().parents[1]
ASSETS = ROOT / "resources" / "assets"
class FakeResponse:
status_code = 200
text = '{"result":1}'
headers = {}
cookies = {}
def json(self):
return {"result": 1}
class FakeSession:
def __init__(self):
self.calls = []
def request(self, method, url, **kwargs):
self.calls.append((method, url, kwargs))
return FakeResponse()
class RoutedFakeSession:
def __init__(self):
self.calls = []
def request(self, method, url, **kwargs):
self.calls.append((method, url, kwargs))
response = FakeResponse()
response.text = '{"result":1,"url":"' + url + '"}'
return response
def test_decode_known_zt_tables():
known = {
APPKEY_MAIN: ("2", "gAZFlwExV0NN2XzT"),
APPKEY_AZEROTH: ("1", "LqARcOYEg8gGHby2"),
"0d2aa853-8455-41b5-b06c-9a36680ef3f0": ("1", "POLKM52lbfYgwD2g"),
"0dc6922f-e3ee-4c57-8eb1-b72d86635497": ("1", "h0hF5ZqvBZagMGhn"),
"5bbcf3cd-727b-48ab-b4b4-5f01e61ee9a5": ("1", "lealm6bxeMABH3rQ"),
"bbd910da-fda5-49e7-8667-f57200dac474": ("1", "lpYKvL0Mz9bEtHXO"),
}
for appkey, (version, x5) in known.items():
table = decode_zt_png(ASSETS / "saio_res" / f"zt_{appkey}.png")
assert table.appkey == appkey
assert str(table.version) == version
assert table.platform == "Android"
assert table.x5 == x5
assert table.selector_byte == ord("-")
def test_decode_kwai_dynamic_zt_container():
sections = load_kwai_dynamic_sections(ASSETS / "video_yh_loading_icon.png")
assert len(sections["TyiBoxes_147456"]) == 147456
assert len(sections["TBoxes_40960"]) == 40960
assert len(sections["TyiTables_4096"]) == 4096
assert len(sections["sha256key00_64"]) == 64
assert len(sections["sha256key01_64"]) == 64
assert sections["custom_2018.png"].startswith(b"\x89PNG\r\n\x1a\n")
def test_native_obfuscated_string_decoder():
assert native_obfuscated_string(bytes.fromhex("0e7c506372fb379527383058633608")) == "sha256key00_64"
assert native_obfuscated_string(bytes.fromhex("0f5b416b02a1799b311e315c0b3409c4")) == "TyiBoxes_147456"
def test_core_hash_algorithms_are_stable():
assert get_clock(b"") == "d9fb5ffc70b7624a88d77c04c43f8e26"
assert kwai_sig({"b": "2", "a": "1"}) == get_clock(b"a=1b=2")
assert kwai_token_sig("a" * 32, "salt") == "e07c9dd4d2addfecb5f0f5218ce70e23c226a0dc11c31e5e43a88f346aa93af1"
def test_old_sig3_10405_shape_and_fixed_value():
assert sig3_10405(b"abc", now=1700000000) == "3097492719e0ba7816bf8f01cfea414140de5dae22"
def test_new_sig3_10417_plain_and_wrapped():
table = decode_zt_png(ASSETS / "saio_res" / f"zt_{APPKEY_MAIN}.png")
plain = sig3_10417(b"abc", None, now=1700000000, counter=1, runtime_value=0, feature_bits=0, wrap=False)
wrapped = sig3_10417(b"abc", table, now=1700000000, counter=1, runtime_value=0, feature_bits=0)
assert plain == "1809595a5d5c5f5e505053529715736349b9182f4d414f59"
assert len(plain) == 48
assert wrapped == "5a54edcdb18f015d083f5b6b465c6d5171176a6bd23a7e40598f351e56417d56"
assert len(wrapped) == 64
def test_default_atlas_sign_10418_shape_and_fixed_value():
table = find_zt_table(APPKEY_MAIN, ASSETS)
sig3 = sig3_10418(b"abc", table)
assert sig3 == "c97ab3636f06fdae22e53a155e3ba66db65f40af9942a070084bdc4fd0264287606fff704c33f0439d2f342ed83c930d"
assert len(sig3) == 96
def test_dfp_and_rc4_helpers():
dfp = build_lite_dfp({"k5": "demo"})
assert "%3D" in dfp or dfp
data = b"hello world"
encrypted = rc4_ksa_prga(b"key", data)
assert rc4_ksa_prga(b"key", encrypted) == data
def test_client_builds_signed_any_web_api_request():
device = KwaiDeviceContext(did="did-demo", egid="egid-demo", rdid="rdid-demo")
client = KwaiClient(device, assets_root=str(ASSETS), host="https://apissl.ksapisrv.com")
signed = client.signed_request("GET", "/rest/n/feed/hot", params={"count": 1})
assert signed.url == "https://apissl.ksapisrv.com/rest/antman/feed/hot"
assert signed.method == "GET"
assert signed.params["sig"]
assert len(signed.params["sig"]) == 32
assert len(signed.params["__NS_sig3"]) == 96
assert signed.params["kpn"] == "KUAISHOU_ANTMAN"
assert signed.params["appver"] == "10.3.39.6655"
assert signed.params["newOc"] == "ANTMAN_PC"
def test_client_salt_is_used_but_not_sent():
device = KwaiDeviceContext(did="did-demo", token="token-demo", api_st="api-st-demo", client_salt="salt-demo")
client = KwaiClient(device, assets_root=str(ASSETS), host="https://apissl.ksapisrv.com")
signed = client.signed_request("GET", "/rest/n/feed/hot", params={"count": 1, "client_salt": "request-salt"})
assert "client_salt" not in signed.params
assert signed.params["token"] == "token-demo"
assert signed.params["kuaishou.api_st"] == "api-st-demo"
assert signed.params["__NStokensig"] == kwai_token_sig(signed.params["sig"], "request-salt")
def test_client_form_request_matches_interceptor_split():
device = KwaiDeviceContext(did="did-demo", token="token-demo", api_st="api-st-demo", client_salt="salt-demo")
client = KwaiClient(device, assets_root=str(ASSETS), host="https://apissl.ksapisrv.com")
signed = client.signed_request("POST", "/rest/n/account/login", params={"q": "1"}, data={"mobile": "13800000000"})
assert "lat" in signed.params
assert signed.params["q"] == "1"
assert "sig" not in signed.params
assert isinstance(signed.data, dict)
assert signed.data["mobile"] == "13800000000"
assert signed.data["token"] == "token-demo"
assert signed.data["kuaishou.api_st"] == "api-st-demo"
assert signed.data["sig"]
assert "sig2" not in signed.data
assert signed.data["__NStokensig"] == kwai_token_sig(signed.data["sig"], "salt-demo")
def test_login_helpers_use_recovered_plugin_endpoints():
session = FakeSession()
device = KwaiDeviceContext(did="did-demo", rdid="rdid-demo")
client = KwaiClient(device, assets_root=str(ASSETS), host="https://apissl.ksapisrv.com", session=session)
client.login_mobile_check("13800000000")
client.request_mobile_code("13800000000", code_type=1)
client.verify_code_login("13800000000", "123456", extra={"captcha_token": "captcha-demo"})
urls = [call[1] for call in session.calls]
assert urls == [
"https://apissl.ksapisrv.com/rest/n/user/mobile/checker",
"https://apissl.ksapisrv.com/rest/n/user/requestMobileCode",
"https://apissl.ksapisrv.com/rest/n/user/login/mobileVerifyCode",
]
for _, _, kwargs in session.calls:
assert kwargs["params"]["did"].startswith("ANDROID_")
assert kwargs["params"]["kpn"] == "KUAISHOU"
assert kwargs["params"]["appver"] == MAIN_VIDEO_LOGIN_APPVER
assert kwargs["params"]["ver"] == MAIN_VIDEO_LOGIN_VER
assert "sig" not in kwargs["params"]
assert kwargs["data"]["sig"]
assert len(kwargs["data"]["__NS_sig3"]) == 96
assert session.calls[2][2]["data"]["code"] == "123456"
assert session.calls[2][2]["data"]["type"] == "27"
assert session.calls[2][2]["data"]["captcha_token"] == "captcha-demo"
assert session.calls[2][2]["data"]["raw"]
assert session.calls[2][2]["data"]["secret"]
def test_auto_profile_keeps_login_on_working_legacy_video_signature():
device = KwaiDeviceContext(did="did-demo")
client = KwaiClient(device, assets_root=str(ASSETS), host="https://apissl.ksapisrv.com")
signed = client.signed_request(
"POST",
"/rest/n/user/mobile/checker",
data={"mobileCountryCode": "86", "mobile": "13800000000"},
include_auth=False,
param_profile="auto",
)
assert signed.url == "https://apissl.ksapisrv.com/rest/n/user/mobile/checker"
assert signed.params["kpn"] == "KUAISHOU"
assert signed.params["appver"] == MAIN_VIDEO_LOGIN_APPVER
assert signed.params["ver"] == MAIN_VIDEO_LOGIN_VER
assert "isAntman" not in signed.params
assert isinstance(signed.data, dict)
assert signed.data["sig"]
assert len(signed.data["__NS_sig3"]) == 96
def test_azeroth_request_uses_14x_params_and_sig3_body():
device = KwaiDeviceContext(did="did-demo", token="token-demo", api_st="api-st-demo", ud="123")
client = KwaiClient(device, assets_root=str(ASSETS), host="https://api.kuaishouzt.com")
signed = client.signed_request(
"POST",
"/rest/n/user/login/mobileVerifyCode",
data={"mobile": "13800000000", "code": "000000"},
include_auth=False,
param_profile="azeroth",
rewrite_path=False,
)
assert signed.url == "https://api.kuaishouzt.com/rest/n/user/login/mobileVerifyCode"
assert signed.params["kpn"] == "KUAISHOU"
assert signed.params["kpf"] == "ANDROID_PHONE"
assert signed.params["appver"].startswith("14.3.30")
assert signed.params["ver"] == "14.3"
assert signed.params["did"].startswith("ANDROID_")
assert signed.params["userId"] == "123"
assert "sig" not in signed.params
assert "sig" not in signed.data
assert "token" not in signed.params
assert "kuaishou.api_st" not in signed.params
assert len(signed.data["__NS_sig3"]) == 96
def test_azeroth_nonce_matches_java_int_shift_semantics():
assert azeroth_nonce(now_ms=1700000000000, rand32=0x01020304) == 0x01B25715
assert azeroth_nonce(now_ms=1700000000000, rand32=0xFFFFFFFF) == -1
def test_azeroth_canonical_filters_signature_fields():
canonical = azeroth_canonical("post", "/rest/demo", {"b": "2", "__NS_sig3": "skip", "a": None})
assert canonical == "POST&/rest/demo&a=&b=2"
def test_register_and_token_login_forms_are_signed():
session = FakeSession()
client = KwaiClient(KwaiDeviceContext(did="did-demo"), assets_root=str(ASSETS), session=session)
client.token_login("login-token-demo", extra={"publicKey": "public-key-demo"})
client.register_by_phone_v2("13800000000", "123456", "pw", user_name="u", gender="M")
client.third_platform_login("wechat", "access-token-demo", open_id="open-id-demo", extra={"unionId": "union-demo"})
client.auth_wechat_code("wechat-code-demo")
token_form = session.calls[0][2]["data"]
register_form = session.calls[1][2]["data"]
third_form = session.calls[2][2]["data"]
wechat_form = session.calls[3][2]["data"]
assert session.calls[0][1].endswith("/rest/antman/user/login/token")
assert token_form["token"] == "login-token-demo"
assert token_form["type"] == "302"
assert token_form["publicKey"] == "public-key-demo"
assert token_form["raw"] and token_form["secret"]
assert token_form["sig"] and token_form["__NS_sig3"]
assert session.calls[1][1].endswith("/rest/antman/user/register/mobileV2")
assert register_form["mobileCode"] == "123456"
assert register_form["password"] == "pw"
assert register_form["gender"] == "M"
assert session.calls[2][1].endswith("/rest/user/thirdPlatformLogin")
assert third_form["platform"] == "wechat"
assert third_form["accessToken"] == "access-token-demo"
assert third_form["openId"] == "open-id-demo"
assert third_form["unionId"] == "union-demo"
assert third_form["publicKey"] and third_form["raw"] and third_form["secret"]
assert session.calls[3][1].endswith("/rest/antman/wechat/oauth2/authByCode")
assert wechat_form["code"] == "wechat-code-demo"
def test_all_recovered_rest_endpoints_can_build_signed_requests():
client = KwaiClient(
KwaiDeviceContext(did="did-demo", token="token-demo", client_salt="salt-demo"),
assets_root=str(ASSETS),
session=FakeSession(),
)
failures = []
for name, endpoint in sorted(REST_ENDPOINTS.items()):
data = {field: f"{field}-demo" for field in endpoint.fields}
params = {field: f"{field}-demo" for field in endpoint.query_fields}
if endpoint.path == "@Url":
data["url"] = "https://example.test/probe"
try:
signed = client.rest_request(name, data=data, params=params)
except Exception as exc:
failures.append(f"{name}: {exc}")
continue
assert signed.method in {"GET", "POST"}
assert signed.url.startswith("http")
assert signed.params.get("__NS_sig3") or (isinstance(signed.data, dict) and signed.data.get("__NS_sig3"))
assert failures == []
def test_refresh_and_logout_send_token_salt_fields():
session = FakeSession()
device = KwaiDeviceContext(did="did-demo", token="token-demo", client_salt="salt-demo", api_st="api-st-demo")
client = KwaiClient(device, assets_root=str(ASSETS), session=session)
client.refresh_token(extra={"refreshToken": "refresh-demo"})
client.logout()
refresh_form = session.calls[0][2]["data"]
logout_form = session.calls[1][2]["data"]
assert session.calls[0][1].endswith("/rest/n/token/infra/refreshToken")
assert refresh_form["refreshToken"] == "refresh-demo"
assert refresh_form["client_salt"] == "salt-demo"
assert refresh_form["__NStokensig"] == kwai_token_sig(refresh_form["sig"], "salt-demo")
assert session.calls[1][1].endswith("/rest/antman/user/logout")
assert logout_form["token"] == "token-demo"
assert logout_form["client_salt"] == "salt-demo"
assert logout_form["__NStokensig"] == kwai_token_sig(logout_form["sig"], "salt-demo")
def test_rest_endpoint_catalog_builds_signed_requests():
device = KwaiDeviceContext(did="did-demo", token="token-demo", client_salt="salt-demo")
client = KwaiClient(device, assets_root=str(ASSETS), session=FakeSession())
assert "startup" in REST_ENDPOINTS
assert "request_mobile_code" in REST_ENDPOINTS
assert "startup" in client.endpoint_names(safe_only=True)
assert "request_mobile_code" not in client.endpoint_names(safe_only=True)
signed = client.rest_request("mobile_check", data={"mobile": "13800000000"})
assert signed.url.endswith("/rest/n/user/mobile/checker")
assert isinstance(signed.data, dict)
assert signed.data["mobile"] == "13800000000"
assert signed.data["sig"]
assert "token" not in signed.data
refresh = client.rest_request("refresh_token", data={"refreshToken": "refresh-demo"})
assert refresh.url.endswith("/rest/n/token/infra/refreshToken")
assert isinstance(refresh.data, dict)
assert refresh.data["token"] == "token-demo"
assert refresh.data["client_salt"] == "salt-demo"
startup = client.rest_request("startup")
assert startup.url.endswith("/rest/antman/system/startup")
assert startup.params["extId"] == ""
assert isinstance(startup.data, dict)
assert startup.data["gp_referer"] == ""
def test_auto_retrofit_endpoint_catalog_is_stable_and_usable():
device = KwaiDeviceContext(did="did-demo", token="token-demo", client_salt="salt-demo")
client = KwaiClient(device, assets_root=str(ASSETS), session=FakeSession())
auto_names = [name for name, endpoint in REST_ENDPOINTS.items() if endpoint.service]
assert len(auto_names) >= 200
assert "api_check_version" in REST_ENDPOINTS
assert "api_check_version_2" not in REST_ENDPOINTS
assert "https_login_mobile_check" in REST_ENDPOINTS
assert "pay_verify_id_card" in REST_ENDPOINTS
assert "api_get_photo_infos" in REST_ENDPOINTS
assert "api_comment_list_v2" in REST_ENDPOINTS
assert "api_search" in REST_ENDPOINTS
assert "api_get_feed_selection" in REST_ENDPOINTS
assert "azeroth_configs" in REST_ENDPOINTS
assert "plugin_startup" in REST_ENDPOINTS
assert "test_speed_dynamic_url" in REST_ENDPOINTS
mobile_endpoint = REST_ENDPOINTS["https_login_mobile_check"]
assert mobile_endpoint.service == "KwaiHttpsService"
assert mobile_endpoint.java_method == "loginMobileCheck"
assert mobile_endpoint.fields == ("mobileCountryCode", "mobile")
assert mobile_endpoint.include_auth is False
signed = client.rest_request("https_login_mobile_check", data={"mobileCountryCode": "86", "mobile": "13800000000"})
assert signed.url.endswith("/rest/antman/user/mobile/checker")
assert isinstance(signed.data, dict)
assert signed.data["mobile"] == "13800000000"
assert signed.data["sig"]
assert "token" not in signed.data
selection_endpoint = REST_ENDPOINTS["api_get_feed_selection"]
assert selection_endpoint.fields[:4] == ("page", "coldStart", "count", "pcursor")
assert selection_endpoint.query_fields == ("cold",)
selection = client.rest_request("api_get_feed_selection", data={"count": 3}, params={"cold": True})
assert selection.url.endswith("/rest/antman/feed/selection")
assert selection.params["cold"] == "true"
assert isinstance(selection.data, dict)
assert selection.data["count"] == "3"
assert selection.data["page"] == ""
def test_manual_java_layer_endpoints_have_special_parameter_routing():
client = KwaiClient(KwaiDeviceContext(did="did-demo"), assets_root=str(ASSETS), session=FakeSession())
plugin_startup = client.rest_request("plugin_startup")
assert plugin_startup.url.endswith("/rest/antman/system/startup")
assert plugin_startup.params["extId"] == ""
assert isinstance(plugin_startup.data, dict)
assert plugin_startup.data["gp_referer"] == ""
assert plugin_startup.data["oaid"] == ""
speed = client.rest_request(
"test_speed_dynamic_url",
data={"url": "https://example.test/speed", "op": "probe"},
)
assert speed.url == "https://example.test/speed"
assert isinstance(speed.data, dict)
assert speed.data["op"] == "probe"
assert "url" not in speed.data
def test_readonly_video_endpoints_are_query_signed():
client = KwaiClient(KwaiDeviceContext(did="did-demo"), assets_root=str(ASSETS), session=FakeSession())
feed = client.rest_request("video_feed_hot", data={"count": "1"})
assert feed.method == "POST"
assert feed.url.endswith("/rest/n/feed/hot")
assert isinstance(feed.data, dict)
assert feed.data["count"] == "1"
assert feed.data["sig"]
assert feed.data["__NS_sig3"]
assert feed.params["kpn"] == "KUAISHOU"
assert feed.params["appver"] == "6.5.5.9591"
assert feed.params["did"].startswith("ANDROID_")
comments = client.rest_request("video_photo_comments", data={"photoId": "photo-demo", "count": "1"})
assert comments.method == "POST"
assert comments.url.endswith("/rest/n/comment/list/v2")
assert isinstance(comments.data, dict)
assert comments.data["photoId"] == "photo-demo"
assert comments.data["count"] == "1"
info = client.rest_request("video_photo_info", data={"photoId": "photo-demo"})
assert info.method == "POST"
assert info.url.endswith("/rest/n/photo/info")
assert isinstance(info.data, dict)
assert info.data["photoIds"] == "photo-demo"
assert "photoId" not in info.data
enriched = client.rest_request(
"video_photo_info",
data={
"photoIds": "photo-demo",
"preUserId": "user-demo",
"preExpTag": "exp-demo",
"preLLSId": "llsid-demo",
"prePhotoIndex": "0",
},
)
assert isinstance(enriched.data, dict)
assert enriched.data["preUserId"] == "user-demo"
assert enriched.data["preExpTag"] == "exp-demo"
assert enriched.data["preLLSId"] == "llsid-demo"
def test_readonly_api_wrapper_shapes_match_retrofit_annotations():
session = FakeSession()
client = KwaiClient(
KwaiDeviceContext(did="did-demo", token="token-demo", client_salt="salt-demo"),
assets_root=str(ASSETS),
session=session,
)
client.api_get_photo_infos(["photo-a", "photo-b"])
method, url, kwargs = session.calls[-1]
assert method == "POST"
assert url.endswith("/rest/antman/photo/info")
assert kwargs["params"]["kpn"] == "KUAISHOU"
assert kwargs["data"]["photoIds"] == "photo-a,photo-b"
assert kwargs["data"]["__NStokensig"]
client.api_comment_list_v2("photo-a", user_id="user-a", count=2, photo_page_type=1)
_, url, kwargs = session.calls[-1]
assert url.endswith("/rest/antman/comment/list/v2")
assert kwargs["data"]["token"] == "token-demo"
assert kwargs["data"]["photoId"] == "photo-a"
assert kwargs["data"]["user_id"] == "user-a"
assert kwargs["data"]["count"] == "2"
assert kwargs["data"]["photoPageType"] == "1"
client.api_search("猫", pcursor="cursor-a", ussid="ussid-a")
_, url, kwargs = session.calls[-1]
assert url.endswith("/rest/antman/search")
assert kwargs["data"]["keyword"] == "猫"
assert kwargs["data"]["pcursor"] == "cursor-a"
assert kwargs["data"]["ussid"] == "ussid-a"
client.api_profile_feed("user-a", count=4, privacy="public", referer="feed")
_, url, kwargs = session.calls[-1]
assert url.endswith("/rest/antman/feed/profile2")
assert kwargs["data"]["token"] == "token-demo"
assert kwargs["data"]["user_id"] == "user-a"
assert kwargs["data"]["count"] == "4"
assert kwargs["data"]["referer"] == "feed"
client.api_get_feed_selection(cold=True, count=5, page=2, cold_start=True)
_, url, kwargs = session.calls[-1]
assert url.endswith("/rest/antman/feed/selection")
assert kwargs["params"]["cold"] == "true"
assert kwargs["data"]["page"] == "2"
assert kwargs["data"]["coldStart"] == "true"
assert kwargs["data"]["count"] == "5"
def test_rest_smoke_only_calls_safe_endpoints():
session = RoutedFakeSession()
client = KwaiClient(KwaiDeviceContext(did="did-demo"), assets_root=str(ASSETS), session=session)
results = client.smoke_test_rest(["version_check", "request_mobile_code"])
assert results[0]["name"] == "version_check"
assert results[0]["signature_ok"] is True
assert results[1]["skipped"] is True
assert len(session.calls) == 1
def test_rest_info_endpoint_group_is_safe_but_not_default_smoke():
session = RoutedFakeSession()
client = KwaiClient(KwaiDeviceContext(did="did-demo"), assets_root=str(ASSETS), session=session)
assert "video_feed_hot" in INFO_REST_ENDPOINT_NAMES
assert "api_user_info" in INFO_REST_ENDPOINT_NAMES
assert "api_search" in REST_INFO_CATEGORIES["search"]
assert "video_feed_hot" in REST_INFO_CATEGORIES["video"]
assert "api_user_info" in REST_INFO_CATEGORIES["user"]
assert "api_message_dialog" in REST_INFO_CATEGORIES["message"]
assert REST_ENDPOINTS["video_feed_hot"].safe is True
assert REST_ENDPOINTS["api_user_info"].safe is True
assert REST_ENDPOINTS["api_message_dialog"].safe is True
assert REST_ENDPOINTS["api_search_tag_recommend"].rewrite_path is False
results = client.smoke_test_rest()
assert {item["name"] for item in results} == {
"api_experiment",
"api_startup",
"plugin_startup",
"startup",
"system_speed",
"version_check",
}
assert all("video" not in call[1] for call in session.calls)
def test_info_smoke_category_builds_message_requests_only():
session = RoutedFakeSession()
client = KwaiClient(
KwaiDeviceContext(did="did-demo", token="token-demo", client_salt="salt-demo"),
assets_root=str(ASSETS),
session=session,
)
results = client.smoke_test_info_rest(category="message", count=1)
assert {item["name"] for item in results} == {
"api_get_friend_users",
"api_get_share_user_list",
"api_message_dialog",
"api_message_load",
"api_nebula_notify_load",
"api_news_load",
"api_notify_load",
}
assert all("/rest/antman/" in call[1] for call in session.calls)
assert any(call[1].endswith("/rest/antman/message/dialog") for call in session.calls)
assert any(call[1].endswith("/rest/antman/notify/load/v2") for call in session.calls)
def test_private_chat_python_interfaces_shape():
session = FakeSession()
device = KwaiDeviceContext(did="did-demo", token="token-demo", client_salt="salt-demo")
client = KwaiClient(device, assets_root=str(ASSETS), session=session)
client.private_chat_list(count=3, page=1, pcursor="cursor-demo")
method, url, kwargs = session.calls[-1]
assert method == "POST"
assert url.endswith("/rest/antman/message/dialog")
assert kwargs["data"]["token"] == "token-demo"
assert kwargs["data"]["count"] == "3"
assert kwargs["data"]["page"] == "1"
assert kwargs["data"]["pcursor"] == "cursor-demo"
client.private_chat_messages("user-demo", page=2, order="asc", pcursor="msg-cursor")
_, url, kwargs = session.calls[-1]
assert url.endswith("/rest/antman/message/load")
assert kwargs["data"]["user_id"] == "user-demo"
assert kwargs["data"]["page"] == "2"
assert kwargs["data"]["order"] == "asc"
assert kwargs["data"]["pcursor"] == "msg-cursor"
signed = client.private_chat_send("user-demo", "hello", copy=True, send=False)
assert len(session.calls) == 2
assert signed.method == "POST"
assert signed.url.endswith("/rest/antman/message/send")
assert isinstance(signed.data, dict)
assert signed.data["user_id"] == "user-demo"
assert signed.data["content"] == "hello"
assert signed.data["copy"] == "true"
assert signed.data["sig"]
assert signed.data["__NStokensig"] == kwai_token_sig(signed.data["sig"], "salt-demo")
def test_extract_video_ids_includes_detail_context():
items = KwaiClient.extract_video_ids(
{
"llsid": "llsid-demo",
"feeds": [
{
"photo_id": "photo-demo",
"user_id": "user-demo",
"exp_tag": "exp-demo",
"serverExpTag": "server-exp-demo",
"feedLogCtx": {"stidContainer": "stid-demo"},
}
],
}
)
assert items == [
{
"photo_id": "photo-demo",
"user_id": "user-demo",
"exp_tag": "exp-demo",
"server_exp_tag": "server-exp-demo",
"llsid": "llsid-demo",
"stid_container": "stid-demo",
}
]
def test_account_security_fields_are_real_rsa_signatures():
device = KwaiDeviceContext(did="did-demo")
client = KwaiClient(device, assets_root=str(ASSETS), session=FakeSession())
form = {"mobile": "13800000000"}
client.add_account_security_fields(form, now_ms=1700000000000)
first_private_key = device.account_private_key_pem
second_public_key = client.account_public_key()
assert form["raw"] == "1700000000000"
assert form["secret"]
assert second_public_key
assert device.account_private_key_pem == first_private_key
def test_phone_login_uses_java_fieldmap_and_original_path():
device = KwaiDeviceContext(did="did-demo")
session = FakeSession()
client = KwaiClient(device, assets_root=str(ASSETS), session=session)
client.phone_code_login("13800000000", "1234", country_code="86", include_public_key=True)
method, url, kwargs = session.calls[-1]
assert method == "POST"
assert url.endswith("/rest/n/user/login/mobileVerifyCode")
assert kwargs["data"]["code"] == "1234"
assert kwargs["data"]["mobileCountryCode"] == "86"
assert kwargs["data"]["mobile"] == "13800000000"
assert kwargs["data"]["type"] == "27"
assert kwargs["data"]["raw"]
assert kwargs["data"]["secret"]
assert kwargs["data"]["publicKey"]
assert kwargs["data"]["deviceName"] == device.mod
def test_register_phone_code_login_uses_plugin_fieldmap_and_original_path():
device = KwaiDeviceContext(did="did-demo")
session = FakeSession()
client = KwaiClient(device, assets_root=str(ASSETS), session=session)
client.register_by_phone_code_login("13800000000", "1234", country_code="86")
method, url, kwargs = session.calls[-1]
assert method == "POST"
assert url.endswith("/rest/n/user/register/mobileV2")
assert kwargs["data"]["mobileCode"] == "1234"
assert kwargs["data"]["mobileCountryCode"] == "86"
assert kwargs["data"]["mobile"] == "13800000000"
assert kwargs["data"]["type"] == "302"
assert kwargs["data"]["publicKey"]
assert kwargs["data"]["deviceName"] == device.mod
assert kwargs["data"]["raw"]
assert kwargs["data"]["secret"]
def test_captures_new_login_client_salt_alias():
device = KwaiDeviceContext(did="did-demo")
changed = device.update_from_mapping({"kuaishou.api_client_salt": "salt-demo"}, overwrite=True)
assert changed["client_salt"] == "salt-demo"
assert device.client_salt == "salt-demo"
def test_dynamic_field_capture_from_mapping():
device = KwaiDeviceContext(did="did-demo")
changed = device.update_from_mapping(
{
"data": {
"cloud_did": "cloud-did-demo",
"did_tag": "8",
"egid": "DFP" + "a" * 61,
"kuaishou.api_st": "api-st-demo",
"clientSalt": "client-salt-demo",
}
}
)
assert changed["did"] == "cloud-did-demo"
assert device.cdid_tag == "8"
assert device.egid == "DFP" + "a" * 61
assert device.api_st == "api-st-demo"
assert device.client_salt == "client-salt-demo"
def test_dynamic_capture_ignores_business_user_ids():
class FeedResponse(FakeResponse):
text = '{"result":1}'
def json(self):
return {
"result": 1,
"feeds": [
{"photo_id": "photo-demo", "user_id": "business-user-demo"},
{"photoId": "photo-demo-2", "userId": "business-user-demo-2"},
],
"data": {"clientSalt": "client-salt-demo"},
}
device = KwaiDeviceContext(did="did-demo", ud="0")
client = KwaiClient(device, assets_root=str(ASSETS), session=FakeSession())
changed = client.capture_dynamic_fields(FeedResponse(), request_path="/rest/n/feed/hot")
assert "ud" not in changed
assert device.ud == "0"
assert device.client_salt == "client-salt-demo"
def test_auth_response_can_capture_login_user_id():
class LoginResponse(FakeResponse):
text = '{"result":1}'
def json(self):
return {"result": 1, "user": {"user_id": "login-user-demo"}}
device = KwaiDeviceContext(did="did-demo", ud="0")
client = KwaiClient(device, assets_root=str(ASSETS), session=FakeSession())
changed = client.capture_dynamic_fields(LoginResponse(), request_path="/rest/n/user/login/mobileVerifyCode")
assert changed["ud"] == "login-user-demo"
assert device.ud == "login-user-demo"
def test_import_login_capture_from_har_request_and_response():
device = KwaiDeviceContext(did="did-demo", ud="0")
capture = {
"log": {
"entries": [
{
"request": {
"headers": [
{
"name": "Cookie",
"value": "kuaishou.api_st=api-st-cookie; client_salt=salt-cookie; extra_cookie=extra-demo",
}
],
"postData": {
"text": "token=token-form&client_salt=salt-form&ud=login-user-form",
"params": [{"name": "did", "value": "did-from-capture"}],
},
},
"response": {
"content": {
"text": '{"result":1,"tokenInfo":{"token":"token-json","clientSalt":"salt-json"},"user":{"user_id":"login-user-json"}}'
}
},
}
]
}
}
changed = device.import_login_capture(capture)
assert changed["did"] == "did-from-capture"
assert changed["token"] == "token-json"
assert changed["api_st"] == "api-st-cookie"
assert changed["client_salt"] == "salt-json"
assert changed["ud"] == "login-user-json"
assert device.cookie_extras["extra_cookie"] == "extra-demo"
def test_import_login_capture_from_raw_headers_and_json_text():
device = KwaiDeviceContext(did="did-demo")
raw_capture = """
POST /rest/n/user/login/mobileVerifyCode HTTP/2
Cookie: kuaishou.api_st=api-st-demo; client_salt=salt-cookie
{"result":1,"token":"token-demo","clientSalt":"salt-demo","user":{"user_id":"user-demo"}}
"""
changed = device.import_login_capture(raw_capture)
assert changed["token"] == "token-demo"
assert changed["api_st"] == "api-st-demo"
assert changed["client_salt"] == "salt-demo"
assert changed["ud"] == "user-demo"
def test_dfp_signed_form_shape():
device = KwaiDeviceContext(did="did-demo", rdid="rdid-demo", cdid_tag="8")
client = KwaiClient(device, assets_root=str(ASSETS), host="https://apissl.ksapisrv.com")
form = client.dfp_signed_form("deviceInfo", b"abc", now_ms=1700000000000)
assert form["productName"] == "KUAISHOU"
assert form["ts"] == "1700000000000"
assert form["sv"] == "2"
assert form["rdid"] == "rdid-demo"
assert form["didtag"] == "8"
assert form["deviceInfo"]
assert len(form["sign"]) == 96
def test_device_context_preserves_direct_state_fields():
device = KwaiDeviceContext.from_mapping(
{
"did": "did-demo",
"token": "token-demo",
"api_st": "api-st-demo",
"client_salt": "salt-demo",
"account_private_key_pem": "pem-demo",
"cookie_extras": {"token": "token-demo", "kuaishou.api_st": "api-st-demo"},
}
)
assert device.did == "did-demo"
assert device.token == "token-demo"
assert device.api_st == "api-st-demo"
assert device.client_salt == "salt-demo"
assert device.account_private_key_pem == "pem-demo"
assert device.cookie_extras == {"token": "token-demo", "kuaishou.api_st": "api-st-demo"}
0x0C 复现命令
从零复核建议按下面顺序:
# 1. 导出证据 bundle 和 manifest
powershell -ExecutionPolicy Bypass -File .\tools\export_kwai_evidence.ps1
Get-Content .\output\kwai_repro_evidence\MANIFEST.tsv -Encoding UTF8
# 2. 校验 Python 语法
python -m py_compile .\kws_recovered\algorithms.py .\kws_recovered\client.py .\kws_client.py
# 3. 跑测试
$env:PYTHONPATH=(Get-Location).Path
pytest -q
# 4. 打印签名请求,不发送
python kws_client.py rest-call video_photo_info --data photoId=123 --print-only
# 5. 查看接口目录
python kws_client.py rest-list --info-only --category search
python kws_client.py rest-list --info-only --category message
python kws_client.py rest-list --info-only --category observed
如果要从 IDA 重新导出 native 伪代码:
IDA Pro -> 打开目标 native IDB/libkwsgmain.so -> File -> Script file...
选择 tools/ida_export_kwai_functions.py
输出 output/ida_reexported/
0x0D 总结:这条链路为什么可信
本文不是把 HAR 字段抄成 Python,也不是把 IDA 伪代码机械翻译成脚本,而是建立了一条互相校验的链:
| 层级 | 证据 | 结论 | 实现 |
|---|---|---|---|
| Retrofit 参数层 | ParamsUtils.java |
sig / __NStokensig 注入位置 |
kwai_sig() / kwai_token_sig() |
| 主 App profile | KwaiParams.java |
__NS_sig3 输入是 path + sig |
sign_query() / sign_form_parts() |
| Java 安全 SDK | WebUtils.java / KSecurity.java |
Java 只包装,算法在 native | make_sig3() 分发 |
| Native command | IDA ida_sg_command_dispatch_body.c |
10405/10417/10418 三代模式 | sig3_10405/10417/10418 |
| DFP | DFPInitUtils.java |
KSecurity context 派生设备表单 | fetch_egid() / fetch_did() |
| Azeroth | AzerothParamProcessor.java |
method + path + params canonical |
azeroth_hmac_client_sign() |
| REST 工程化 | HAR / annotation JSON / client.py |
endpoint、profile、rewrite、auth、HAR 导入 | KwaiClient / CLI |
| 防回归 | tests/test_recovered_algorithms.py |
关键 shape 可重复验证 | pytest -q |
这就是为什么最终实现不是一堆魔法常量,而是一套能继续扩展的逆向工程资产。后续如果 App 版本、签名模式、HAR 字段发生变化,继续沿着这条链路查:先看 Java 参数入口,再看 native command,再改 Python 算法,最后补测试。