小小的更新

该页面有相关的 个人项目 , 您在文章中看到的某些代码片段可能会在其中。

上周末终于抽出时间更新了一下之前写的 dmzj备份脚本。一方面是因为最近工作稍微闲了些,另一方面则是由于一直主用的第三方 App 频繁抽风,也让我萌生了干脆毕其功于一役、彻底转向 self-hosting 的念头。

于是又重新扒了一遍他们的 APK(不确定是不是因为美国 IP 的缘故,官方应用的广告似乎收敛了不少,可用性尚可,但考虑到他们的 App 实在写得有些💩,我还是提不起兴趣去用)。这次倒是发现了一些新东西,简单做个记录。

API changes

这次距离上次更新时间并不算久,其实整体变化不大。虽然如此,他们的网络请求写法依然挺雷人的,每次看到都忍不住多看两眼:长长的Query直接拼字符串;用中文字符串充当枚举走分支判断来加一些特定参数;关键接口都要加密处理,但依旧不肯用 Retrofit,而是直接在 UI 层手动解密,整个突出一个“自由”。

真正变化的只有API路径改成了nnv4api.${DOMAIN}/v2/comic/xxx, 这个会强制校验coreTokenuid,然后proto又加了一个字段名曰isCanRead。让Claude帮忙对应更新了下。

coreToken revisit

之前的API其实并不会用到这个玩意,于是也没细究,但新的接口终于加上了强制校验,token不对直接什么都不返回。

于是不得不逆向分析一下这个 libdmzj.so,看看这个所谓的加密 token 生成算法是否藏有黑科技。毕竟,遇事不决就写 JNI,倒也有几分大厂 Mobile Arch 的味道了。然而……这个 token 的生成逻辑无非就是两次hash:先通过 getSignatureBytes 获取应用签名并进行一次 MD5,然后将其与 timestamp 和 salt 拼接后再进行一次 MD5,最终将该 MD5 与 timestamp 拼接,生成 token。

其实需要奇文共赏的是这个getSignatureBytes方法。不难发现,它本质上就是Java反射代码,却被包装成 JNI 来调用一个 Android Framework 的公共 API (也就是说,并不需要反射)。在Kotlin里可以one-liner写的功能,却非要用反射再加 JNI,可谓是双重的脱裤子放屁。

Before

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

/* WARNING: Unknown calling convention */

jbyteArray getSignatureByte(JNIEnv *env,jobject context)

{
jclass p_Var1;
jmethodID p_Var2;
jobject p_Var3;
jclass p_Var4;
jmethodID methodID;
jobject p_Var5;
char *pcVar6;
jfieldID p_Var7;
jclass packageManager_clazz;
jclass packageinfo_clazz;
jobjectArray signature_arr;
jclass signature_clazz;
jmethodID methodID_getPackageManager;
jmethodID methodID_getPackageName;
jfieldID fieldID_signatures;
jmethodID methodID_byteArray;
char *package_name;
jstring application_package;
jobject packageInfo;
jobject signature;
jclass context_clazz;
jobject packageManager;
jmethodID methodID_getPackageInfo;

p_Var1 = (*env->functions->GetObjectClass)(env,context);
p_Var2 = (*env->functions->GetMethodID)
(env,p_Var1,"getPackageManager","()Landroid/content/pm/PackageManager;");
p_Var3 = _JNIEnv::CallObjectMethod(env,context,p_Var2);
p_Var4 = (*env->functions->GetObjectClass)(env,p_Var3);
p_Var2 = (*env->functions->GetMethodID)
(env,p_Var4,"getPackageInfo",
"(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
methodID = (*env->functions->GetMethodID)(env,p_Var1,"getPackageName","()Ljava/lang/String;");
p_Var5 = _JNIEnv::CallObjectMethod(env,context,methodID);
pcVar6 = (*env->functions->GetStringUTFChars)(env,(jstring)p_Var5,(jboolean *)0x0);
__android_log_print(3,"dmzj_core","packageName: %s",pcVar6);
p_Var3 = _JNIEnv::CallObjectMethod(env,p_Var3,p_Var2,p_Var5,0x40);
p_Var1 = (*env->functions->GetObjectClass)(env,p_Var3);
p_Var7 = (*env->functions->GetFieldID)(env,p_Var1,"signatures","[Landroid/content/pm/Signature;");
p_Var3 = (*env->functions->GetObjectField)(env,p_Var3,p_Var7);
p_Var3 = (*env->functions->GetObjectArrayElement)(env,(jobjectArray)p_Var3,0);
p_Var1 = (*env->functions->GetObjectClass)(env,p_Var3);
p_Var2 = (*env->functions->GetMethodID)(env,p_Var1,"toByteArray","()[B");
p_Var3 = _JNIEnv::CallObjectMethod(env,p_Var3,p_Var2);
return (jbyteArray)p_Var3;
}

After

1
pm.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES).signatures!![0].toByteArray()

而且说到底,通过编写 native 方法来规避 ART 的隐藏 API 检查,压根也不是这么实现的。Google 的工程师可没笨到只在 Java 层的 getDeclaredMethod 中加检查,而在 JNI 的 GetMethodID 上就放松警惕。下面是一个正确的规避方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}

const char *VMRuntime_class_name = "dalvik/system/VMRuntime";
jclass vmRumtime_class = env->FindClass(VMRuntime_class_name);
void *getRuntime_method = env->GetStaticMethodID(vmRumtime_class,
"getRuntime",
"()Ldalvik/system/VMRuntime;");
jobject vmRuntime_instance = env->CallStaticObjectMethod(vmRumtime_class, (jmethodID)getRuntime_method);

jstring pattern = env->NewStringUTF("L");
jclass cls = env->FindClass("java/lang/String");
jobjectArray jarray = env->NewObjectArray(1, cls, nullptr);
env->SetObjectArrayElement(jarray, 0, pattern);

void *setHiddenApiExemptions_method = env->GetMethodID(vmRumtime_class,
"setHiddenApiExemptions",
"([Ljava/lang/String;)V");
env->CallVoidMethod(vmRuntime_instance, (jmethodID)setHiddenApiExemptions_method, jarray);
return JNI_VERSION_1_6;
}

所以他们为什么要在JNI写这个coreToken的生成逻辑呢?到头来还是希腊奶。

getCoreToken() disassembly (trimmed):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
jstring getToken(JNIEnv *env,jclass clazz,jobject context)

{
//...

lVar3 = tpidr_el0;
lVar10 = *(long *)(lVar3 + 0x28);
gettimeofday(&local_b0,(__timezone_ptr_t)0x0);
sprintf(certMD5 + 0x3c,(char *)0xb,&DAT_0012aa7a,local_b0.tv_sec);
std::__ndk1::basic_string<>::basic_string<nullptr_t>(&local_108,certMD5 + 0x3c);
/* try { // try from 00111348 to 00111353 has its CatchHandler @ 00111714 */
p_Var7 = getSignatureByte(env,context);
/* try { // try from 00111360 to 0011136b has its CatchHandler @ 00111710 */
jVar6 = (*env->functions->GetArrayLength)(env,(jarray)p_Var7);
/* try { // try from 00111378 to 00111387 has its CatchHandler @ 0011170c */
__src = (*env->functions->GetByteArrayElements)(env,p_Var7,(jboolean *)0x0);
/* try { // try from 00111394 to 0011139b has its CatchHandler @ 00111708 */
__s = operator_new[]((long)(jVar6 + 1));
memset(__s,0,(long)(jVar6 + 1));
sVar11 = (size_t)jVar6;
memcpy(__s,__src,sVar11);
/* try { // try from 001113d4 to 001113e7 has its CatchHandler @ 00111704 */
hashByteArray(HASH_MD5,__s,sVar11,(char *)&local_b0);
pcVar4 = APK_PACKAGE;
/* try { // try from 001113f0 to 001113fb has its CatchHandler @ 00111700 */
std::__ndk1::basic_string<>::basic_string<nullptr_t>(&local_158,(char *)&local_b0);
sVar11 = strlen(pcVar4);
/* try { // try from 00111408 to 00111417 has its CatchHandler @ 001116ec */
pbVar8 = std::__ndk1::basic_string<>::insert(&local_158,0,(value_type *)pcVar4,sVar11);
local_140 = pbVar8;
pbVar8 = NULL;
__n = (ulong)((byte)local_108 >> 1);
uVar2 = (pointer)(value_type *)((ulong)&local_108 | 1);
if (((byte)local_108 & 1) != 0) {
__n = local_108.__size_;
uVar2 = local_108.__data_;
}
/* try { // try from 00111450 to 00111457 has its CatchHandler @ 001116cc */
pbVar8 = std::__ndk1::basic_string<>::append(&local_140,(value_type *)uVar2,__n);
local_f0 = pbVar8;
pbVar8 = NULL;
pcVar4 = APK_TOKEN_SALT;
sVar11 = strlen(APK_TOKEN_SALT);
/* try { // try from 00111484 to 0011148f has its CatchHandler @ 00111698 */
pbVar8 = std::__ndk1::basic_string<>::append(&local_f0,(value_type *)pcVar4,sVar11);
local_110 = pbVar8;
sStack_118 = pbVar8.__size_;
local_120 = pbVar8.__cap_;
pbVar8 = NULL;
pcVar1 = (pointer)((ulong)&local_120 | 1);
if ((local_120 & 1) != 0) {
pcVar1 = local_110;
}
/* try { // try from 001114e0 to 001114f7 has its CatchHandler @ 00111688 */
__android_log_print(3,"dmzj_core","token string: %s",pcVar1);
uStack_c8 = 0;
local_d0 = 0;
uStack_b8 = 0;
uStack_c0 = 0;
bVar5 = (local_120 & 1) != 0;
pcVar1 = (pointer)((ulong)&local_120 | 1);
if (bVar5) {
pcVar1 = local_110;
}
local_f0 = NULL;
numBytes = local_120 >> 1 & 0x7f;
if (bVar5) {
numBytes = sStack_118;
}
/* try { // try from 0011151c to 00111527 has its CatchHandler @ 00111684 */
hashByteArray(HASH_MD5,pcVar1,numBytes,(char *)&local_f0);
/* try { // try from 00111528 to 0011153b has its CatchHandler @ 00111680 */
std::__ndk1::operator+<>(&local_158,&local_108,"|");
sVar11 = strlen((char *)&local_f0);
/* try { // try from 00111548 to 00111553 has its CatchHandler @ 0011166c */
pbVar8 = std::__ndk1::basic_string<>::append(&local_158,(value_type *)&local_f0,sVar11);
local_140 = pbVar8;
pbVar8 = NULL;
...
uVar2 = (char *)((ulong)&local_140 | 1);
/* try { // try from 00111594 to 001115cb has its CatchHandler @ 00111724 */
//...

}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!