最近闲下来,想着把之前没弄明白的一件事重新研究一下。那会儿我在分析币安 App 的登录接口,想搞清楚请求头里的 sig
参数是怎么生成的。一开始用 jadx 简单看了下,结果发现这个参数的生成逻辑藏在 so 层(也就是 native 层的动态库里)。一看就挺复杂,估计要花不少时间琢磨,所以当时就先放下了。这次决定重新动手,好好把这个问题解决掉。
准备
- Frida
- Fiddler
- IDA64
- Jadx
- Android手机(已root)
- 币安 2.79.5 apk
关于工具配置以及环境搭建等问题请自行 Google 解决。另外,如遇 Fiddler 无法抓包,请将 Fiddler 证书设置为系统级别证书,一般可解决。
抓包
开启抓包软件,打开币安,输入账号点击登录,可以发现有好几个包,只需要看关于登录操作的第一个包,请求数据如下:
POST https://www.yingwangtech.club/bapi/accounts/v1/public/account/security/precheck HTTP/1.1
Cache-Control: no-cache, no-store
clienttype: android
x-trace-id: android_00b26de5-64eb-4823-9ed5-dc8261d7b873
fvideo-id: 23a105016402afefb4b6c7bf1082ca74bff85b28
fvideo-token: 2njCtuScAjgWnd9XqENtQaUK/FwUiWr7t/IObMhu7RbqT/szBC5yzeiMuEnAnuqApL/doIomtjl8BldkyJRhtcBf14emblPXjKv2eqQ9tQSDC+XYK4IS1jEH6FrYVgykqile+CylqQBt0+nb09JF4KHP7cYyIbHc+dmTDMLiX5Fm57lqf9ETrJxaJaVSG1bG4=6e
lang: zh-CN
versioncode: 27905
versionname: 2.79.5
isNight: true
BNC-App-Mode: lite
BNC-UUID: 4f609e63-938f-4d6c-8692-b05209204c19
BNC-Time-Zone: Asia/Shanghai
BNC-App-Channel: binance
device-info: eyJkZXZpY2VfaWQiOiIiLCJhX2Jvb3Rsb2FkZXIiOiJ1bmtub3duIiwiYV9icmFuZCI6IlJlZG1pIiwiYV9sb2NhdGlvbl9jaXR5IjoidW5rbm93biIsImFfY3B1X2FiaSI6Ilthcm02NC12OGEsIGFybWVhYmktdjdhLCBhcm1lYWJpXSIsImFfZGV2aWNlX2xvZ2luX25hbWUiOiJwZWFybCIsImRldmljZV9uYW1lIjoiMjMwNTRSQTE5QyIsImFfZGlzcGxheSI6IlRQMUEuMjIwNjI0LjAxNCIsImFfZmluZ2VycHJpbnQiOiJSZWRtaS9wZWFybC9wZWFybDoxMy9UUDFBLjIyMDYyNC4wMTQvVjE0LjAuNi4wLlRMSENOWE06dXNlci9yZWxlYXNlLWtleXMiLCJhX2hvc3QiOiJwYW5ndS1idWlsZC1jb21wb25lbnQtc3lzdGVtLTIxMDk4OC1xNGpwcC1yNDl2Ny14c3pxcCIsImFfZGV2aWNlX3ZlcnNpb25faWQiOiJUUDFBLjIyMDYyNC4wMTQiLCJhX2ltZWkiOiJ1bmtub3duIiwiYV9yb21fc2l6ZSI6IjIzMSwwNjFNQiIsImFfbWFjX2FkZHJlc3MiOiJ1bmtub3duIiwiYV9nZXRfbGluZV9udW1iZXIiOiJ1bmtub3duIiwiYV9wcm9kdWN0IjoicGVhcmwiLCJhX3JhbV9zaXplICI6IjExLDQ5OU1CIiwiYV9zY3JlZW5IZWlnaHQiOiIyMjMwIiwiYV9zY3JlZW5XaWR0aCI6IjEwODAiLCJhX3NkayI6IjMzIiwiYV9zZXJpYWxfaW5mbyI6InVua25vd24iLCJhX3NpbV9zZXJpYWxfbnVtYmVyIjoidW5rbm93biIsImFfYnVpbGRfdGltZSI6IjE2ODg5ODYxMjUwMDAiLCJhX3VzZXIiOiJidWlsZGVyIiwiYnJhbmRfbW9kZWwiOiJSZWRtaTIzMDU0UkExOUMiLCJhX2FwcF9pbnN0YWxsX2RhdGUiOiIxNzEwMzQwOTMwNDI2IiwianVkZ2Vfcm9vdCI6MCwic2NyZWVuX3Jlc29sdXRpb24iOiIxMDgwKjIyMzAiLCJzeXN0ZW1fbGFuZyI6InpoLUNOIiwic3lzdGVtX3ZlcnNpb24iOiIzMyIsInN5c3RlbV92ZXJzaW9uX25hbWUiOiIxMyIsInRpbWV6b25lIjoiR01UKzA4MDAifQ==
mclient-x-tag: pch5D9lsORjgObhyjdSK
BNC-Currency: CNY
referer: https://www.binance.com/
x-seccheck-sig: a1.5.4#NwEAAJQAAABXAAAAKAEAAAMpCJCBG6gss1NtWENQ5NJfZdK6u5ZrsWOdKivvYe15xwIVq39PCr6uCkzvwcTryB-JBXlhRP415kpIYQByS3LqVbXn7gclnT737wbyWF775TGq0-fbj1cSPuhwjTOi-BGTUbQwcb2veXzc4ppGFGmDimK_2OxC84ZQF8ifrC_I1h8muzQjSNyZuDoCysHz-kqzlWWUscj3PR9UcjF0AWT1zU4sjCo3WgL03mg0CjnOZg5HnTsE3LqxeD-U3gWiK_x700QspKMMM12jVZBSEnmlzBsR4TsrQsc_OqkK7eTm-vVimuAQBzrA_FLkysd2V_ikNnYxAHOk07Tw5xlbU58cO6TaO6h2Pi4zanG1PXYKxc5vrpImtK28lsbsvuKBDeia9jk6pxFfKKx1DEga1XKP58hQJNoLN1gI9UuBP-bMJLtaLnS5PcADzveUVGQIu8K2qSEW_8y53xVdjwTbwsmZhN09VkeCMido09fNgbJkBJBWCQ
x-seccheck-token: a1.5.4#9wAAABQCAABXAQAAyAEAADXrtoQZCgS6KOzH_RNBIU_sDNHfkyBw5DzH_t2Uyq9o-FJ4Gk0FFULPC8LoZGDVD55D4i1AMLNSDo6p3afkcEEVIJCA_BHeJS59c18wNDZzcY3BuOZphJ9L1-0pvzGFUMvvjIMTOlS29uakXrRvmEdyJvcpGPoebXf5HA6iBOvmGuXYCykRaO7GJ759YMYiqIgjbESKF1rLT-QLEukhi7031E2XACc6oR5IlxkV1ju-67fL8o5vqPZSbSVX2Tsf1Qr7GDh4460zLyX_bOZBG_EsWxPJm-XBe32KJaIHScDQMBGdypVkXIgYUYYzVeWvpe3kHg69Smj3fWMt3ZD9GzTmvtRD7pzV7GK7whwMpvsl3NvhIys8nTyJnIFrArc5imY9H4mlYJSTQIcN0sRX8itaMFLK0-sZhFf-8hRZaEBPjd-SmHaNxtlV6CHjSMuJQdtHOwWC8d1XtTykiDQGmncMiCeQeLfPdwQwlCd08FknWawu2Ujc3sYWtE5Mozmgq0RaiLujudB0QStoSZPUcKdFfOP_tUTvFyt2fjUljCXOV72Y3nChNczsRCRHL9w35oyDq_APjR_rVhXaTc0dgXKgCAiItLTFJ8hPj3jPia78oB0VStR915i31Wp0L28TjTr7Op8wkwW0yWIBszB1_c_D3wPF8O_3AMQdWM_fLlhOCvMZmgLYbvFu73g_LcJPfFUlDU8UZD4w-TIWQIcn2hmTh-W2OQ6EGs3Yd5fFa7Sbo9aLVVPT8A0VBHwQm9Q6F80_QDmhJzUAUqQfICJlov2-shE8Y7pDux4Az4Ri4l9GQXq3RREEVJYfqbDoL8UBEEUk-71Ry5_nOfP0yxBEJQqDPvdlqs3s2dVtdsdoo_kh98ksDID2oHoIQ7w3kulAzLfIpsZtpwhqyEd6eUrAtwvkX_OghBWpn-LUqfuUdPPy3Wrd0vyBkOP_qvYvEOrluIByTG6pjOLXTHCgrazvtlEV1wMscBelAt0q-HKc-XOLEPt7qiwV9V61BLsyOckCbKeaaI_JcWDV1sSJ3bwUd6FEHdl6s4BrPQA3SnV3B7dHt4Wr4w#4E37E891
Content-Type: application/json
Content-Length: 117
Host: www.yingwangtech.club
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/4.11.0
{"email":"[email protected]","bizType":"login","type":"sessionId","bizId":"login","playIntegritySupport":true,"cs":0}
经过多次测试,可以确定这个 precheck 的功能是在登录前检查账号是否需要验证码。可以看到协议头中有个 x-seccheck-sig 字段,我们的目标就是要找到它是怎么生成的。
java 层分析
将币安 apk 文件拖入 Jadx ,搜索有关于字符串 “x-seccheck-sig” 的代码,会得到三个结果,且都在一个代码文件,双击其中一个进入,代码片段:
List<String> mo34942a = this.f70909d.mo34942a();
Pair<String, String> m35712c = m35712c(request);
String component1 = m35712c.component1();
String component2 = m35712c.component2();
C48071xe6a6226 c48071xe6a6226 = c41166x51ee57462.f87968b;
C48071xe6a6226.C480724 c480724 = Headers.f1275744;
C48071xe6a6226.C480724.m667d(c480724, "x-seccheck-sig");
C48071xe6a6226.C480724.m671a(c480724, component1, "x-seccheck-sig");
c48071xe6a6226.f127576a.add("x-seccheck-sig");
c48071xe6a6226.f127576a.add(StringsKt.trim((CharSequence) component1).toString());
C48071xe6a6226 c48071xe6a62262 = c41166x51ee57462.f87968b;
C48071xe6a6226.C480724 c4807242 = Headers.f1275744;
C48071xe6a6226.C480724.m667d(c4807242, "x-seccheck-token");
C48071xe6a6226.C480724.m671a(c4807242, component2, "x-seccheck-token");
c48071xe6a62262.f127576a.add("x-seccheck-token");
c48071xe6a62262.f127576a.add(StringsKt.trim((CharSequence) component2).toString());
boolean z2 = true;
if (!mo34942a.isEmpty()) {
List<String> list = mo34942a;
if (!(list instanceof Collection) || !list.isEmpty()) {
for (String str : list) {
if (StringsKt.endsWith$default(obj, str, false, 2, (Object) null)) {
z = true;
break;
}
}
}
z = false;
}
Pair<String, String> m35712c = m35712c(request);
可以看出,x-seccheck-token 的来源是函数 m35712c() 返回的 Pair,第一个是 sig,第二个是token,我们只关心第一个,双击函数继续往上回溯,代码片段:
/* renamed from: c */
private Pair<String, String> m35712c(detachViewInternal detachviewinternal) {
try {
String signature = SecCheck.createApiSig().getSignature(detachviewinternal);
SecCheck secCheck = SecCheck.getInstance();
String token = secCheck != null ? secCheck.getToken(this.f70909d.mo34937e()) : null;
if (token == null) {
token = "";
}
return TuplesKt.m933to(signature, token);
} catch (Throwable th) {
String obj = detachviewinternal.f87966i.toString();
StringBuilder sb = new StringBuilder();
sb.append("Get secCheck token,Exception: ");
sb.append(th.getMessage());
try {
this.f70909d.mo34940b(obj, sb.toString());
} catch (Throwable unused) {
}
return TuplesKt.m933to("", "");
}
return TuplesKt.m933to(signature, token); 这段代码显然是在构造 Pair,第一个参数是 signature ,往上回溯 getSignature 函数,代码片段:
public String getSignature(detachViewInternal detachviewinternal) {
return calcSig(detachviewinternal);
}
public SecApiSig(SecCheckConfig secCheckConfig) {
this.f2279b = secCheckConfig;
initSecApiSig();
}
private String calcSig(detachViewInternal detachviewinternal) {
String requestBodyText;
String obj = detachviewinternal.f87966i.toString();
ArrayList arrayList = new ArrayList();
ArrayList arrayList2 = new ArrayList();
ArrayList arrayList3 = new ArrayList();
String headerValues = getHeaderValues(detachviewinternal);
String requestQueryParams = getRequestQueryParams(detachviewinternal);
C3253d c3253d = new C3253d();
if (isFormRequest(detachviewinternal)) {
requestBodyText = getRequestFormParams(detachviewinternal, c3253d);
} else {
requestBodyText = getRequestBodyText(detachviewinternal, c3253d);
}
if (!headerValues.isEmpty()) {
arrayList.add(headerValues);
} else {
c3253d.f2293a = true;
}
if (!requestQueryParams.isEmpty()) {
arrayList.add(requestQueryParams);
} else {
c3253d.f2300h = true;
}
arrayList2.addAll(arrayList);
arrayList3.addAll(arrayList);
String str = "";
if (!requestBodyText.isEmpty()) {
arrayList.add(requestBodyText);
String gsrq = SecCheckNative.gsrq(requestBodyText);
if (gsrq != null) {
arrayList2.add(gsrq);
} else {
c3253d.f2302j = true;
}
C3248a m93441a = C3249b.m93441a(requestBodyText, c3253d);
if (m93441a != null) {
StringBuilder sb = new StringBuilder();
sb.append(m93441a.f2280a);
sb.append(TopicsStore.DIVIDER_QUEUE_OPERATIONS);
sb.append(m93441a.f2281b);
str = sb.toString();
gsrq = SecCheckNative.gsrq(m93441a.f2281b);
}
if (gsrq != null) {
arrayList3.add(gsrq);
} else {
c3253d.f2303k = true;
}
} else {
c3253d.f2301i = true;
}
return SecCheckNative.m93447as(C3254e.m93433a(IOUtils.LINE_SEPARATOR_UNIX, arrayList), C3254e.m93433a(IOUtils.LINE_SEPARATOR_UNIX, arrayList2), C3254e.m93433a(IOUtils.LINE_SEPARATOR_UNIX, arrayList3), headerValues, requestBodyText, str, obj, c3253d.m93436a());
}
继续回溯 calcSig 函数,发现是调用了 SecCheckNative.m93447as 这个函数,经过一番数据处理,并传入足足八个参数,可怕。继续向上回溯,代码片段:
/* loaded from: classes6.dex */
public class SecCheckNative {
static {
setPaddingEnd.m8282a("com/bina/security/secsdk/SecCheckNative.<clinit>(l1)->java/lang/System.loadLibrary");
System.loadLibrary(C3247a.f2272b);
setPaddingEnd.m8278e("com/bina/security/secsdk/SecCheckNative.<clinit>(l1)->java/lang/System.loadLibrary");
}
/* renamed from: as */
public static native String m93447as(String str, String str2, String str3, String str4, String str5, String str6, String str7, String str8);
/* renamed from: di */
public static native void m93446di();
/* renamed from: ed */
public static native String m93445ed(String str);
public static native String gcd();
public static native String gdd();
/* renamed from: gs */
public static native String m93444gs(String str, int i);
public static native String gsrq(String str);
public static native String gsrqb(byte[] bArr);
/* renamed from: gt */
public static native String m93443gt(String str);
/* renamed from: ii */
public static native long m93442ii(String str);
public static native void sgd(String str);
}
“System.loadLibrary”,看见它,基本可以确定这里是调用的 so 库了,无法再继续回溯,只能跟到这
/* renamed from: gs */ public static native String m93444gs(String str, int i) ,
注意这里的 gs 是在 so 中的函数名。在分析 so 前需先拿到 so 文件名以及对应的 so 函数名。
System.loadLibrary(C3247a.f2272b) 这一句是加载 so 文件,C3247a.f2272b 里边储存着文件名。
so文件:com.bina.security.secsdk.so
函数名:gs
现在已经知道是哪个 so 文件了,用 7-zip 打开 apk,进入 lib 目录,找到包含字段 ”com.bina.security.secsdk“ 的 so 文件,将它解压到桌面,以供后续分析。
so 层分析
通过前面在 java 层的分析,我们可以确定 x-seccheck-token 的生成是调用了 so 中名为 gas(s1,s2,s3,s4,s5,s6,s7,s8) 的函数,并给它传入了八个参数,下面我们深入 so 中分析下这个函数。
用 IDA64 打开前面准备好的 libcom.bina.security.secsdk.so 文件,在导出函数列表按下快捷键 ctrl+f ,搜索 “gas”


如上图,可以看到有两个结果,直接双击第二个函数,因为第一个函数 j_gas 就是调用的 gas。双击后你会发现右边的汇编视图已经转到这个 gas 函数的汇编代码处,将鼠标焦点移至汇编窗口并按下快捷键 f5 ,此时 IDA64 便会将汇编代码解析为 C 语言伪代码,如下图

现在能够清晰地看见整个 gas 函数,而且输入参数的数量也恰好是八个,应该是找对地方了,我们分析一下这个 gas 函数的代码片段
char *__fastcall gas(const char *a1, const char *a2, const char *a3, const char *a4, const char *a5, __int64 a6, __int64 a7, const char *a8)
{
char *v15; // x28
__int64 v16; // x0
char *v17; // x28
__int64 v18; // x0
char *v19; // x28
__int64 v20; // x0
char *v21; // x28
__int64 v22; // x0
char *v23; // x28
__int64 v24; // x0
const char *v25; // x24
char *v26; // x28
const char *v27; // x26
size_t v28; // w19
size_t v29; // w19
char *v30; // x20
const char *v31; // x19
char *v32; // x21
size_t v34; // [xsp+28h] [xbp-88h]
const char *v35; // [xsp+30h] [xbp-80h]
const char *v36; // [xsp+38h] [xbp-78h]
const char *v37; // [xsp+40h] [xbp-70h]
const char *v38; // [xsp+48h] [xbp-68h]
__int64 v39; // [xsp+50h] [xbp-60h]
v15 = (char *)calloc(0x20u, 1u);
v16 = strlen(a1);
j_hmac_sha256_get(v15, a1, v16, "y*TFyoQQkqwUzZD9MCofUYPwqoAwucukGooP--KcZCyoDFXZ.TV2ogZiE!Y79dXC", 64LL);
v38 = (const char *)calloc(9u, 1u);
j_toHex(v15 + 28, 4LL, v38);
free(v15);
v17 = (char *)calloc(0x20u, 1u);
v18 = strlen(a2);
j_hmac_sha256_get(v17, a2, v18, "y*TFyoQQkqwUzZD9MCofUYPwqoAwucukGooP--KcZCyoDFXZ.TV2ogZiE!Y79dXC", 64LL);
v37 = (const char *)calloc(9u, 1u);
j_toHex(v17 + 28, 4LL, v37);
free(v17);
v19 = (char *)calloc(0x20u, 1u);
v20 = strlen(a3);
j_hmac_sha256_get(v19, a3, v20, "y*TFyoQQkqwUzZD9MCofUYPwqoAwucukGooP--KcZCyoDFXZ.TV2ogZiE!Y79dXC", 64LL);
v36 = (const char *)calloc(9u, 1u);
j_toHex(v19 + 28, 4LL, v36);
free(v19);
v21 = (char *)calloc(0x20u, 1u);
v22 = strlen(a4);
j_hmac_sha256_get(v21, a4, v22, "y*TFyoQQkqwUzZD9MCofUYPwqoAwucukGooP--KcZCyoDFXZ.TV2ogZiE!Y79dXC", 64LL);
v35 = (const char *)calloc(9u, 1u);
j_toHex(v21 + 28, 4LL, v35);
free(v21);
v23 = (char *)calloc(0x20u, 1u);
v24 = strlen(a5);
j_hmac_sha256_get(v23, a5, v24, "y*TFyoQQkqwUzZD9MCofUYPwqoAwucukGooP--KcZCyoDFXZ.TV2ogZiE!Y79dXC", 64LL);
v25 = (const char *)calloc(9u, 1u);
j_toHex(v23 + 28, 4LL, v25);
free(v23);
v26 = (char *)j_getRequestContent(a7, a6);
v39 = j_getTimestamp();
v27 = (const char *)calloc(0x25u, 1u);
j_generateUuid();
v34 = strlen(v27);
v28 = strlen(v38);
LODWORD(a1) = strlen(v37);
LODWORD(a2) = strlen(v36);
LODWORD(a7) = strlen(v35);
LODWORD(a3) = strlen(v25);
LODWORD(a4) = strlen(v26);
v29 = v34 + v28 + (_DWORD)a1 + (_DWORD)a2 + a7 + (_DWORD)a3 + (_DWORD)a4 + strlen(a8) + 256;
v30 = (char *)calloc(v29, 1u);
snprintf(
v30,
v29,
"{\"tm\": \"%lld\", \"nc\": \"%s\", \"sig\": \"%s\", \"sig1\": \"%s\", \"sig2\": \"%s\", \"ht\": \"%s\", \"bdy\": \"%"
"s\", \"fl\": \"%s\", \"cnt\": \"%s\"}",
v39,
v27,
v38,
v37,
v36,
v35,
v25,
a8,
v26);
v31 = (const char *)j_ed(v30);
free(v30);
free(v26);
LODWORD(v30) = strlen(v31) + 8;
v32 = (char *)calloc((size_t)v30, 1u);
snprintf(v32, (size_t)v30, "%s%s%s", "a1.5.4", "#", v31);
return v32;
}
为了验证是否找对函数,先使用 Frida hook 看看这个函数的传入参数跟返回值,写一段 Hook 这个 gas 函数的 Frida 代码,但在 Hook 之前我们得先拿到这个 gas 函数在内存中的地址。
function find_func(functionName) {
Java.perform(function () {
var modules = Process.enumerateModules();
var found = false;
for (var i = 0; i < modules.length; i++) {
var module = modules[i];
if (module.path.includes("lib")) {
console.log("Module Name:", module.name);
var libName = module.name;
const hooks = Module.load(libName);
var exports = hooks.enumerateExports();
for (var j = 0; j < exports.length; j++) {
var expt = exports[j];
if (expt.name == functionName) {
console.log("Type:", expt.type);
console.log("Name:", expt.name);
console.log("Module:", expt.module);
console.log("Address:", expt.address);
found = true;
break;
}
}
}
if (found) break;
}
});
}
执行 find_func(”gas”) ,这段代码是遍历这个 apk 运行时所有加载的 so 文件中的函数,一旦匹配则跳出循环,输出函数信息。
Module Name: [email protected]
Module Name: libcom.bina.security.secsdk.so
Type: function
Name: gas
Module: undefined
Address: 0x72daf8cb64
[23054RA19C::com.binance.dev ]->
OK,拿到函数地址为:0x72daf8cb64,开始编写 Hook 代码。注意,这个地址不是固定的,每次打开 apk 都会变化,所以需要每次 hook 前遍历函数地址。
function hook_gas() {
var targetFunctionAddress = new NativePointer('0x72daf8cb64');
var targetFunctionName = "gas"
// 拦截目标函数调用
Interceptor.attach(targetFunctionAddress, {
onEnter: function (args) {
//char *__fastcall gas(const char *a1, const char *a2, const char *a3, const char *a4, const char *a5, __int64 a6, __int64 a7, const char *a8)
console.log("[*]HOOK ", targetFunctionName)
console.log('[*]arg1 输入字符串: ' + args[0].readCString());
console.log('[*]arg2 输入字符串: ' + args[1].readCString());
console.log('[*]arg3 输入字符串: ' + args[2].readCString());
console.log('[*]arg4 输入字符串: ' + args[3].readCString());
console.log('[*]arg5 输入字符串: ' + args[4].readCString());
console.log('[*]arg6 输入字符串: ' + args[5].readCString());
console.log('[*]arg7 输入字符串: ' + args[6].readCString());
console.log('[*]arg8 输入字符串: ' + args[7].readCString());
}, onLeave: function (retval) {
console.log("[*]", targetFunctionName, " 结果", retval.readCString());
}
});
}
[*]HOOK gas
[*]arg1 输入字符串: clienttype:android&versioncode:27905&versionname:2.79.5
{"email":"[email protected]","bizType":"login","type":"sessionId","bizId":"login","playIntegritySupport":true,"cs":0}
[*]arg2 输入字符串: clienttype:android&versioncode:27905&versionname:2.79.5
13A9E4F27ECA3C0EE6C8F9A4AF5967D5E5F1B2F7949179233D8E829BBB3095A2
[*]arg3 输入字符串: clienttype:android&versioncode:27905&versionname:2.79.5
86F9C6AE16A043F00F8D5A5A4C51D16B480DE8C9793534A0ABBB52092745665C
[*]arg4 输入字符串: clienttype:android&versioncode:27905&versionname:2.79.5
[*]arg5 输入字符串: {"email":"[email protected]","bizType":"login","type":"sessionId","bizId":"login","playIntegritySupport":true,"cs":0}
[*]arg6 输入字符串: email:[email protected]|bizType:login|type:sessionId|bizId:login|playIntegritySupport:true|cs:0,9589fefa47a4a6b763cce7014f9b2e9f
[*]arg7 输入字符串: https://www.yingwangtech.club/bapi/accounts/v1/public/account/security/precheck
[*]arg8 输入字符串: 00000001000000
[*] gas 结果 a1.5.4#..............省略亿点字
执行 hook_gas() ,然后回到手机,点击登录,这时会发起第一个请求包,由于要调用 gas 生成 x-seccheck-sig ,不出意外 Hook 会被触发,可以看到,已被触发。再来看看 gas 执行结果能不能对上请求包中的 x-seccheck-sig,对上了!!!那我们确实找对地方了,接下来的任务就是剖析 gas 函数的伪代码,并将其翻译成其它语言。

分析 gas 的伪代码
函数原型 char *__fastcall gas(const char *a1, const char *a2, const char *a3, const char *a4, const char *a5, __int64 a6, __int64 a7, const char *a8)
输入参数 a1,a2,a3,a4,a5,a6,a7,a8,字符串类型
a1 = clienttype:android&versioncode:27905&versionname:2.79.5 + 换行 + {"email":"[email protected]","bizType":"login","type":"sessionId","bizId":"login","playIntegritySupport":true,"cs":0}
a2 = clienttype:android&versioncode:27905&versionname:2.79.5 + 换行 + 13A9E4F27ECA3C0EE6C8F9A4AF5967D5E5F1B2F7949179233D8E829BBB3095A2
a3 = clienttype:android&versioncode:27905&versionname:2.79.5 + 换行 + 86F9C6AE16A043F00F8D5A5A4C51D16B480DE8C9793534A0ABBB52092745665C
a4 = clienttype:android&versioncode:27905&versionname:2.79.5
a5 = {"email":"[email protected]","bizType":"login","type":"sessionId","bizId":"login","playIntegritySupport":true,"cs":0}
a6 = email:[email protected]|bizType:login|type:sessionId|bizId:login|playIntegritySupport:true|cs:0,9589fefa47a4a6b763cce7014f9b2e9f
a7 = https://www.yingwangtech.club/bapi/accounts/v1/public/account/security/precheck
a8 = 00000001000000
分析 a1 – a5
v15 = (char *)calloc(0x20u, 1u);
v16 = strlen(a1);
j_hmac_sha256_get(v15, a1, v16, "y*TFyoQQkqwUzZD9MCofUYPwqoAwucukGooP--KcZCyoDFXZ.TV2ogZiE!Y79dXC", 64LL);
v38 = (const char *)calloc(9u, 1u);
j_toHex(v15 + 28, 4LL, v38);
free(v15);
首先是申请了 0x20u(十进制32)个字节大小的内存并将这块内存的指针放入 v15,然后将 a1 字符串的长度放入 v16,再调用 HMAC-SHA256 对 a1 进行加密,密码是 “y*TFyoQQkqwUzZD9MCofUYPwqoAwucukGooP–KcZCyoDFXZ.TV2ogZiE!Y79dXC”, 将加密结果放入 v15,接着申请 9 个字节的内存并将指针存入 v38,调用 tohex 函数将 v15 字节数组转为 HEX 字符串(大写字母),HEX 结果被放到了 v38。
需要注意,v15 + 28,这里向后偏移了 28 个字节,也就是前 28 个字节不要,只要后 4 个字节的 HEX 字符串,HMAC-SHA256 的密文长度是 32 字节。
分析 a6,a7
v26 = (char *)j_getRequestContent(a7, a6);
这里是调用了 j_getRequestContent 函数,并将结果放入了 v26。同样的,在 IDA 导出函数窗口搜索这个函数,向上回溯,最终来到
char *__fastcall getRequestContent(const char *a1, const char *a2)
{
char *v4; // x19
unsigned __int64 v5; // x8
size_t v6; // w2
char *v7; // x0
const char *v8; // x1
__int64 v9; // x20
char *v10; // x21
__int64 v12[2]; // [xsp+0h] [xbp-30h] BYREF
v12[1] = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
v4 = (char *)calloc(0xFBu, 1u);
if ( strstr(a1, "public/account/security/precheck")
|| strstr(a1, "gift-box/pick/campaign/pool")
|| strstr(a1, "gift-box/code/grab/campaign")
|| strstr(a1, "gift-box/code/grabV2") )
{
v5 = strlen(a2);
if ( v5 < 0xFB )
{
v7 = v4;
v8 = a2;
v6 = v5;
}
else
{
v6 = 250;
v7 = v4;
v8 = a2;
}
strncpy(v7, v8, v6);
v12[0] = 9LL;
v9 = strlen(v4);
v10 = (char *)calloc(2 * (int)v9, 1u);
j_b64url_encode(v4, v9, v10, v12);
free(v4);
v4 = v10;
}
return v4;
}
可以看出,大概意思就是判断 a1 是否包含指定的字符串,如果包含则对 a2 进行 base64url 编码然后返回,这里的 a1,a2 分别对应前面传入的 a7,a6。
继续看 gas 代码,开始分析后半段代码
v39 = j_getTimestamp();
v27 = (const char *)calloc(0x25u, 1u);
j_generateUuid();
v34 = strlen(v27);
v28 = strlen(v38);
LODWORD(a1) = strlen(v37);
LODWORD(a2) = strlen(v36);
LODWORD(a7) = strlen(v35);
LODWORD(a3) = strlen(v25);
LODWORD(a4) = strlen(v26);
v29 = v34 + v28 + (_DWORD)a1 + (_DWORD)a2 + a7 + (_DWORD)a3 + (_DWORD)a4 + strlen(a8) + 256;
v30 = (char *)calloc(v29, 1u);
snprintf(
v30,
v29,
"{\"tm\": \"%lld\", \"nc\": \"%s\", \"sig\": \"%s\", \"sig1\": \"%s\", \"sig2\": \"%s\", \"ht\": \"%s\", \"bdy\": \"%"
"s\", \"fl\": \"%s\", \"cnt\": \"%s\"}",
v39,
v27,
v38,
v37,
v36,
v35,
v25,
a8,
v26);
v31 = (const char *)j_ed(v30);
free(v30);
free(v26);
LODWORD(v30) = strlen(v31) + 8;
v32 = (char *)calloc((size_t)v30, 1u);
snprintf(v32, (size_t)v30, "%s%s%s", "a1.5.4", "#", v31);
return v32;
}
v39 = j_getTimestamp(),这里将13位的毫秒时间戳存入 v39,为什么是13位毫秒而不是10位秒?因为我已经 Hook 验证过了,你也可以尝试 Hook 一下。
j_generateUuid(),这一句就很奇怪,没传参数,也没变量接收返回,可能是 IDA 解析错误了,看着前面申请了内存,应该就是将 UUID 放入了 v27(经过 Hook 验证,确实是放入了 v27,还做了一些处理,转大写并去除了 “-” 符号)。
再来看看这个 snprintf ,它把前面所生成的数据格式化到了 v30
snprintf(
v30,
v29,
"{\"tm\": \"%lld\", \"nc\": \"%s\", \"sig\": \"%s\", \"sig1\": \"%s\", \"sig2\": \"%s\", \"ht\": \"%s\", \"bdy\": \"%"
"s\", \"fl\": \"%s\", \"cnt\": \"%s\"}",
13位时间戳,
特殊UUID,
hmac-hash256(a1)结果后4字节,
hmac-hash256(a2)结果后4字节,
hmac-hash256(a3)结果后4字节,
hmac-hash256(a4)结果后4字节,
hmac-hash256(a5)结果后4字节,
a8,
j_getRequestContent(a7, a6),
);
v31 = (const char *)j_ed(v30);
然后又将 v30 传给了 j_ed() 这个函数,先回溯到这个函数看看
void *__fastcall ed(const char *a1)
{
__int64 v2; // x21
void *v3; // x22
__int64 v4; // x8
unsigned __int64 v5; // x20
__int64 v6; // x23
void *v7; // x0
void *v8; // x19
_OWORD *v9; // x21
__int64 v10; // x0
__int64 v11; // x20
void *v12; // x21
void *v13; // x22
size_t v14; // w23
void *v15; // x20
__int64 v17; // [xsp+8h] [xbp-118h] BYREF
__int64 v18; // [xsp+10h] [xbp-110h] BYREF
char v19[192]; // [xsp+18h] [xbp-108h] BYREF
__int64 v20; // [xsp+D8h] [xbp-48h]
v20 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
v2 = strlen(a1);
v3 = calloc((int)v2 + 1, 1u);
memcpy(v3, a1, v2);
j_swapGroup(v3, v2);
j_swapBit(v3, v2);
v4 = v2 + 16;
if ( v2 + 16 < 0 )
v4 = v2 + 31;
v5 = v4 & 0xFFFFFFFFFFFFFFF0LL;
v6 = (v4 & 0xFFFFFFFFFFFFFFF0LL) - v2;
v7 = calloc((unsigned int)v4 & 0xFFFFFFF0, 1u);
v8 = v7;
if ( v6 )
memset(v7, v6, v5);
memcpy(v8, v3, v2);
v9 = calloc(0x10u, 1u);
*v9 = *(_OWORD *)"kYp3s6v9y$B&E)H@";
j_swapGroup(v9, 16LL);
j_swapBit(v9, 16LL);
j_AES_init_ctx_iv(v19, v9, "9z$C&F)J@NcRfUjX");
j_AES_CBC_encrypt_buffer(v19, v8, v5);
free(v9);
j_swapGroup(v8, v5);
j_swapBit(v8, v5);
v18 = 0LL;
v10 = j_salting(v8, v5, &v18);
v11 = v18;
v12 = (void *)v10;
v13 = calloc(2 * (int)v18, 1u);
v17 = 0LL;
j_b64url_encode(v12, v11, v13, &v17);
v14 = v17;
v15 = calloc((int)v17 + 1, 1u);
memcpy(v15, v13, v14);
free(v8);
free(v12);
return v15;
}
一眼看去,这应该是一个 AES 加密,前前后后还有点复杂呢… 没办法,只能硬着头皮上。
经过一番 hook 之后(内容过长,过程省略),我来说说这几个函数是干什么的吧
j_swapGroup 交换数组中相邻两个位置的数据
j_swapBit 交换数组中相邻两个数据的字节低四位
j_salting 对数据进行加盐混淆
好了,以上就这些是重点,继续分析,可以看见,它先是对数据依次进行 swapGroup,swapBit,结果丢给了 AES CBC 加密,然后又将加密结果依次进行 swapGroup,swapBit,salting,最终丢给 base64url 编码(末尾不带==),然后返回。
好了,差不多搞清楚了,回到 gas 代码片段
v31 = (const char *)j_ed(v30);
free(v30);
free(v26);
LODWORD(v30) = strlen(v31) + 8;
v32 = (char *)calloc((size_t)v30, 1u);
snprintf(v32, (size_t)v30, "%s%s%s", "a1.5.4", "#", v31);
return v32;
经过 j_ed() 加密后的数据被放到了 v31 ,然后将 v31 格式化成 x-seccheck-sig 的模样,最终将格式化结果返回,结果大概长这样
x-seccheck-sig: a1.5.4#NwEAAJQAAABXAAAAKAEAAAMpCJCBG6gss1NtWENQ5NJfZdK6u5ZrsWOdKivvYe15xwIVq39PCr6uCkzvwcTryB……省略n字
至此,整个分析过程就已完成。另外我再附上 swapGroup,swapBit,salting 的 go 语言实现,都是对照着伪代码一句句翻译过来的,经过 Hook 验证,翻译正确。
salting
func salting(a1 []byte, a2 int) []byte {
v5 := make([]byte, 16)
v6 := make([]byte, 4)
var v8, v10, v12, v14 int
rand.New(rand.NewSource(time.Now().Unix()))
v8 = rand.Int() //rand
v9 := v8 - v8/a2*a2
v5[0] = byte(v9)
v10 = rand.Int() //rand
v11 := v10 - v10/a2*a2
v5[4] = byte(v11)
v12 = rand.Int() //rand
v13 := v12 - v12/a2*a2
v5[8] = byte(v13)
v14 = rand.Int() //rand
v15 := v14 - v14/a2*a2
v5[12] = byte(v15)
//存放结果
v16 := make([]byte, a2+20)
copy(v16, v5)
//前面已经填充了16个字节 跳过4个字节后再填充
copy(v16[16:], a1)
var v19, v20, v21, v22 int
rand.New(rand.NewSource(time.Now().Unix()))
v6[0] = a1[v9]
v19 = rand.Int() //rand
v16[16+v9] = byte(v19 - v19/a2*a2)
v6[1] = a1[v11]
v20 = rand.Int() //rand
v16[16+v11] = byte(v20 - v20/a2*a2)
v6[2] = a1[v13]
v21 = rand.Int() //rand
v16[16+v13] = byte(v21 - v21/a2*a2)
v6[3] = a1[v15]
v22 = rand.Int() //rand
v16[16+v15] = byte(v22 - v22/a2*a2)
//*(unsigned int *)((char *)v17 + a2) = v23;
for i := 0; i < 4; i++ {
v16[16+a2+i] = v6[i]
}
return v16
}
swapBit
func swapBit(data []byte, groupSize int) {
// 确保 groupSize 在有效范围内
if groupSize > len(data) {
groupSize = len(data)
}
for i := 0; i < groupSize-1; i += 2 {
data[i], data[i+1] = data[i]&0xf0|data[i+1]&0x0f, data[i+1]&0xf0|data[i]&0x0f
}
}
swapGroup
func swapGroup(data []byte, groupSize int) {
// 确保 groupSize 在有效范围内
if groupSize > len(data) {
groupSize = len(data)
}
// 从最后一个分组开始向前遍历,每个分组的相邻元素互换
for i := 0; i < groupSize-1; i += 2 {
data[i], data[i+1] = data[i+1], data[i]
}
}
大神,你好,我最近也在研究币安,你的x-seccheck-sig算法我看明白了,但是x-seccheck-token我没有看懂,可有偿,谢谢大神