经过一年多的努力,iSmart 的自动化已经能够基本实现,然而现在的实现方式总的来说还是一种 dirty fix,无法完全摆脱客户端的束缚,实在是太不优雅。而我的主力 PC 机也已经换成 linux 系统,刷 iSmart 多多少少有些不方便。最近 iSmart 移动端也开始支持答题并上传,而且 apk 也完全没有加固,那么不妨来逝一逝,看看在移动端上,能不能做出一些新的突破

  • 顺手宣传一下 PC 版自动化脚本:

开搞

强行打开 WebView 调试

  新版 iSmart 客户端本质上还是个套壳浏览器,要调试其中网页,自然是用 inspector 最方便。然而这版本的 iSmart 并没有像 WeLearn 一样默认开启 WebView 调试功能,所以我们简单写个 Xposed 模块强行把调试打开:

1
2
3
4
5
6
7
if (lpparam.processName == lpparam.packageName) {
findMethod(Application::class.java) {
name == "onCreate"
}!! after {
WebView.setWebContentsDebuggingEnabled(true)
}
}

断点分析

  进入答题页面后,给「提交」按钮的 onClick() 下个断点,然后挂上 mitmproxy 提交任务,一边步进一边查看客户端有没有发出请求,于是有了一幕奇怪的调试姿势:

  最终定位到一个 callHandler 函数:

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
function callHandler(handlerName, data, responseCallback) {
if (typeof data == 'function') {
responseCallback = data;
data = void (0);
}
try {
var result = null;
if (data === void (0)) {
result = window['_android_js_obj_'][handlerName]();
} else {
result = window['_android_js_obj_'][handlerName](JSON.stringify(data));
}
if(!responseCallback) return;
result = tryParseResultToJSONObject(result);
if (typeof(result) == 'string' && result.startsWith('async_callback_key_')) {
asyncHandles[result] = responseCallback;
} else {
setTimeout(function () {
responseCallback(result);
});
}
} catch (e) {
console.log('handleName:' + handlerName);
console.log('data:' + JSON.stringify(data));
console.log(e.stack);
}
}

  它有三个参数,分别是操作、数据和回调,这里我们重点关注前两个,调试得知提交任务时 handlerName 为 "submitAnswerData",其数据是一个 object,格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"uploadId": "3F37D7DA6A20672998ADC59B56B3682F",
"type": "textbook",
"data": {
"startTime": 1646554669877,
"submitTime": 1646554675291,
"score": 0,
"percent": "0.00",
"duration": 5000,
"userAnswerXmlStr": "",
"markDataXmlStr": "<element id=\"bfd041b1237dfea19ffdf674048d6758\"><mark_data id=\"1\"><score>0</score><result>2</result></mark_data><mark_data id=\"2\"><score>0</score><result>2</result></mark_data><mark_data id=\"3\"><score>0</score><result>2</result></mark_data><mark_data id=\"4\"><score>0</score><result>2</result></mark_data><mark_data id=\"5\"><score>0</score><result>2</result></mark_data><mark_data id=\"6\"><score>0</score><result>2</result></mark_data></element>",
"urls": "",
"wordIds": "",
"timeData": []
}
}

  通过对比发现,data 参数中的 uploadId 对应着请求中的 taskId;而且不出所料,请求中出现了 json 数据中不存在的 ut 签名。

  至此 web 层面的东西已经被我们挖掘得差不多了,下面进入 java 部分。

dex 逆向分析

  本着「缺什么找什么」的原则,首先尝试直接搜索 "ut",然后……居然一下就搜到了??不是 native???

  进入 StringUtils 类,发现有 getUtgetUtV2 两个方法, 那么来 Hook 一下验证调用栈:

1
2
3
4
5
6
7
8
9
"com.up366.common.StringUtils" hooks {
val hook = hooker {
before {
Log.i(Thread.currentThread().stackTrace.joinToString("\n"))
}
}
find { name == "getUtV2" }.hook(hook)
find { name == "getUt" }.hook(hook)
}

  这边走的是 V1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
dalvik.system.VMStack.getThreadStackTrace(Native Method)
java.lang.Thread.getStackTrace(Thread.java:1736)
mufanc.tools.ismartauto.HookEntry$onHandleLoadPackage$3$hook$1$1.invoke(HookEntry.kt:22)
mufanc.tools.ismartauto.HookEntry$onHandleLoadPackage$3$hook$1$1.invoke(HookEntry.kt:21)
com.github.mufanc.easyhook.util.Hooker.beforeHookedMethod(Hook.kt:22)
de.robv.android.xposed.LspHooker.handleHookedMethod(Unknown Source:75)
LspHooker_.getUt(Unknown Source:15)
com.up366.mobile.book.BookLogMgr$1.<init>(BookLogMgr.java:79)
com.up366.mobile.book.BookLogMgr.addSubmitTaskToQueue(BookLogMgr.java:77)
com.up366.mobile.book.BookLogMgr.addBookTaskLog(BookLogMgr.java:39)
com.up366.mobile.book.exercise.js.ExerciseHelper.lambda$submitCourseBook$0$ExerciseHelper(ExerciseHelper.java:261)
com.up366.mobile.book.exercise.js.-$$Lambda$ExerciseHelper$B2goyhWONRORlD9gD_ui7KTeyhk.run(Unknown Source:6)
com.up366.common.task.TaskWrapper.run(TaskWrapper.java:18)
java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:462)
java.util.concurrent.FutureTask.run(FutureTask.java:266)
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
java.lang.Thread.run(Thread.java:923)

  然后再把参数也打一下:

1
2
3
4
5
findMethod("com.up366.common.StringUtils") {
name == "getUt"
}!! before {
Log.i(it.args[0])
}

  确定无误之后,开始研究算法,跟进 StringUtils#getUt(String, int),其中只有两行有用的代码:

  继续看里面的 EncryptUtil#enc(byte[], int),果然还是遇上了 native:

关于 native-lib 的研究

  我们知道,一般的 native 函数是会在生成的 so 文件中生成对应符号的,使用 Android Studio 新建一个 Native C++ 工程,得到如下的 native-lib.cpp

1
2
3
4
5
6
7
8
9
10
#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}

  然后编译打包成 apk,从中解压出 so 文件,使用 readelf -sW libnative-lib.so 命令读取其中所有导出的符号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Symbol table '.dynsym' contains 417 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __cxa_atexit@LIBC (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __cxa_finalize@LIBC (2)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __register_atfork@LIBC (2)
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __memcpy_chk@LIBC (2)
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND memcpy@LIBC (2)

(...... 此处省略若干行 ......)

410: 0000000000010100 137 FUNC GLOBAL DEFAULT 13 _ZNSt11logic_errorC1ERKNSt6__ndk112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEE
411: 0000000000036ce0 40 OBJECT GLOBAL DEFAULT 18 _ZTVSt9exception
412: 000000000000f1a0 164 FUNC GLOBAL DEFAULT 13 Java_com_example_MainActivity_stringFromJNI
413: 000000000000ff60 5 FUNC WEAK DEFAULT 13 _ZdaPvm
414: 0000000000039410 32 OBJECT GLOBAL DEFAULT 18 _ZTIPDh
415: 00000000000395f0 32 OBJECT GLOBAL DEFAULT 18 _ZTIPDi
416: 0000000000036db0 40 OBJECT GLOBAL DEFAULT 18 _ZTVSt13runtime_error

  然而当我们使用同样的方法查看 iSmart 的 libnative-lib.so 时,却连一个符号都找不到:

1
2
readelf -sW *.so | grep Java_ 
# 无输出

  看来 iSmart 也许是采用了 env->RegisterNatives 动态注册的方式来实现这几个函数,这样一来便大大增加了逆向破解的难度。以我目前的技术力,自认没有本事从将近 600kb 的 so 库中抠出加密逻辑,纯 Python 实现方案也就此宣告破产。

失败的尝试:Sideload 方案

  最终……还是无法摆脱客户端吗?正当我陷入苦恼之际,冥冥之中一个新的想法在脑海中闪过!我们为何要致力于摆脱客户端,究其根本是为了避免在设备上安装冗余的(潜在的流氓)软件,然而不管刷课与否,大家基本上都会安装 iSmart 的手机客户端以应付各种在线测试。

  我们知道 Android 中有一个神奇的函数 Context#createPackageContext,搭配合适的 flag 后,就可以将另一个应用的代码加载过来执行,迅速敲出一个 demo:

1
2
3
4
5
6
7
8
9
10
val context = createPackageContext(
"com.up366.ismart",
Context.CONTEXT_IGNORE_SECURITY or CONTEXT_INCLUDE_CODE
)
@Suppress("LocalVariableName")
val EncryptUtil = context.classLoader.loadClass("com.up366.common.utils.EncryptUtil")
val encrypt = EncryptUtil.declaredMethods.find {
it.name == "enc"
}!!
Log.i(TAG, "${encrypt.invoke(null, "Hello World!".toByteArray(), 99)}")

  然后呢?……然后就报了满屏的错😰

  错误信息太长,这里就不全放出来了,不过可以看出大致的意思是 jni 访问到了一些未初始化的变量,从而导致 nullptr 错误。看来我还是想得太简单了,通过这种方式加载的应用并不会调用 Application#onCrate,因此在其中初始化变量的操作也相应的得不到执行。

退让:LSPatch 方案

新版本 LSPatch 已经集成了优秀的过签性能,我们只需正常编写 Xposed 模块,然后用其打包即可

  那么再退一步,通过 LSPatch 对 iSmart 重打包实现注入行不行呢?

  LSPatch 是 LSPosed 开发团队基于 Xpatch 实现的应用打包工具,能够将 Xposed 模块直接打包进入目标 Apk 中,同时拥有不错的过签能力,使用方法如下:

1
java -jar lspatch.jar iSmart-origin.apk -m xposed-module.apk --force

  现在让我们简单测试一下:用 MT 管理器提取 iSmart 安装包,什么都不改,重新签名后装回去,发现没法正常登录,说明存在签名校验。

1
2
3
4
5
6
7
catch {
findClass("android.app.ApplicationPackageManager").declaredMethods.forEach {
it before { param ->
Log.i("${it.name}(${param.args.joinToString(", ")})")
}
}
}

  简单写个钩子 hook PackageManager,发现确实有很多获取签名的地方(超长警告⚠️):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
I/iSmartAuto: getPackageInfo(com.up366.ismart, 64)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.up366.ismart, 64, 0)
I/iSmartAuto: getPackageInfo(android, 64)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(android, 64, 0)
I/iSmartAuto: getApplicationInfo(com.up366.ismart, 128)
I/iSmartAuto: getUserId()
I/iSmartAuto: getApplicationInfoAsUser(com.up366.ismart, 128, 0)
I/iSmartAuto: maybeAdjustApplicationInfo(ApplicationInfo{fefb985 com.up366.ismart})
I/iSmartAuto: hasSystemFeature(android.hardware.vulkan.version, 4198400)
I/iSmartAuto: resolveContentProvider(com.up366.ismart.udeskfileprovider, 128)
I/iSmartAuto: getUserId()
I/iSmartAuto: resolveContentProviderAsUser(com.up366.ismart.udeskfileprovider, 128, 0)
I/iSmartAuto: onImplicitDirectBoot(0)
I/iSmartAuto: getXml(com.up366.ismart, 2131886087, ApplicationInfo{2f3300b com.up366.ismart})
I/iSmartAuto: getResourcesForApplication(ApplicationInfo{2f3300b com.up366.ismart})
I/iSmartAuto: resolveContentProvider(com.up366.ismart, 128)
I/iSmartAuto: getUserId()
I/iSmartAuto: resolveContentProviderAsUser(com.up366.ismart, 128, 0)
I/iSmartAuto: onImplicitDirectBoot(0)
I/iSmartAuto: getXml(com.up366.ismart, 2131886080, ApplicationInfo{99c33a6 com.up366.ismart})
I/iSmartAuto: getResourcesForApplication(ApplicationInfo{99c33a6 com.up366.ismart})
I/iSmartAuto: hasSystemFeature(android.software.webview)
I/iSmartAuto: hasSystemFeature(android.software.webview, 0)
I/iSmartAuto: getPackageInfo(com.google.android.webview, 268444864)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.google.android.webview, 268444864, 0)
I/iSmartAuto: getComponentEnabledSetting(ComponentInfo{com.google.android.webview/org.chromium.android_webview.devui.DeveloperModeState})
I/iSmartAuto: getUserId()
I/iSmartAuto: getComponentEnabledSetting(ComponentInfo{com.google.android.webview/org.chromium.android_webview.SafeModeState})
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfo(com.up366.ismart, 0)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.up366.ismart, 0, 0)
I/iSmartAuto: getServiceInfo(ComponentInfo{com.google.android.webview/org.chromium.content.app.SandboxedProcessService0}, 0)
I/iSmartAuto: getUserId()
I/iSmartAuto: onImplicitDirectBoot(0)
I/iSmartAuto: getPackageInfo(com.up366.ismart, 0)
I/iSmartAuto: getApplicationInfo(com.up366.ismart, 128)
I/iSmartAuto: getUserId()
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.up366.ismart, 0, 0)
I/iSmartAuto: getApplicationInfoAsUser(com.up366.ismart, 128, 0)
I/iSmartAuto: maybeAdjustApplicationInfo(ApplicationInfo{189f2d7 com.up366.ismart})
I/iSmartAuto: getApplicationLabel(ApplicationInfo{90417c4 com.up366.ismart})
I/iSmartAuto: getText(com.up366.ismart, 2131689508, ApplicationInfo{90417c4 com.up366.ismart})
I/iSmartAuto: getCachedString({ResourceName com.up366.ismart / 2131689508})
I/iSmartAuto: isInstantApp()
I/iSmartAuto: isInstantApp(com.up366.ismart)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfo(com.google.android.gms, 0)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.google.android.gms, 0, 0)
I/iSmartAuto: getResourcesForApplication(ApplicationInfo{90417c4 com.up366.ismart})
I/iSmartAuto: putCachedString({ResourceName com.up366.ismart / 2131689508}, iSmart-学生)
I/iSmartAuto: getInstallerPackageName(com.google.android.webview)
I/iSmartAuto: getPackageInfo(com.google.android.gms, 0)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.google.android.gms, 0, 0)
I/iSmartAuto: getPackageInfo(projekt.substratum, 0)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(projekt.substratum, 0, 0)
I/iSmartAuto: getPackageInfo(com.google.android.webview, 9216)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.google.android.webview, 9216, 0)
I/iSmartAuto: hasSystemFeature(android.hardware.type.automotive)
I/iSmartAuto: hasSystemFeature(android.hardware.type.automotive, 0)
I/iSmartAuto: hasSystemFeature(android.hardware.type.watch)
I/iSmartAuto: hasSystemFeature(android.hardware.type.watch, 0)
I/iSmartAuto: hasSystemFeature(android.hardware.type.iot)
I/iSmartAuto: hasSystemFeature(android.hardware.type.iot, 0)
I/iSmartAuto: hasSystemFeature(android.hardware.type.embedded)
I/iSmartAuto: hasSystemFeature(android.hardware.type.embedded, 0)
I/iSmartAuto: getPackageInfo(com.android.vending, 8256)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.android.vending, 8256, 0)
I/iSmartAuto: getPackageInfo(com.google.android.gms, 64)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.google.android.gms, 64, 0)
I/iSmartAuto: getPackageInfo(com.android.vending, 8256)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.android.vending, 8256, 0)
I/iSmartAuto: getPackageInfo(com.google.android.gms, 64)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.google.android.gms, 64, 0)
I/iSmartAuto: getPackageInfo(com.up366.ismart, 0)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.up366.ismart, 0, 0)
I/iSmartAuto: getApplicationInfo(com.up366.ismart, 128)
I/iSmartAuto: getUserId()
I/iSmartAuto: getApplicationInfoAsUser(com.up366.ismart, 128, 0)
I/iSmartAuto: maybeAdjustApplicationInfo(ApplicationInfo{149a8b7 com.up366.ismart})
I/iSmartAuto: getPackageInfo(com.up366.ismart, 4096)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.up366.ismart, 4096, 0)
I/iSmartAuto: getPackageInfo(com.up366.ismart, 128)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.up366.ismart, 128, 0)
I/iSmartAuto: getPackageInfo(com.up366.ismart, 4096)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfo(com.up366.ismart, 64)
I/iSmartAuto: getPackageInfoAsUser(com.up366.ismart, 4096, 0)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.up366.ismart, 64, 0)
I/iSmartAuto: getPackageInfo(com.up366.ismart, 64)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.up366.ismart, 64, 0)
I/iSmartAuto: queryIntentServices(Intent { cmp=com.up366.ismart/com.up366.mobile.common.service.Up366PushService }, 0)
I/iSmartAuto: getUserId()
I/iSmartAuto: queryIntentServicesAsUser(Intent { cmp=com.up366.ismart/com.up366.mobile.common.service.Up366PushService }, 0, 0)
I/iSmartAuto: onImplicitDirectBoot(0)
I/iSmartAuto: queryIntentServices(Intent { cmp=com.up366.ismart/com.up366.mobile.common.service.PushIntentService }, 0)
I/iSmartAuto: getUserId()
I/iSmartAuto: queryIntentServicesAsUser(Intent { cmp=com.up366.ismart/com.up366.mobile.common.service.PushIntentService }, 0, 0)
I/iSmartAuto: onImplicitDirectBoot(0)
I/iSmartAuto: getPackageInfo(com.up366.ismart, 4096)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.up366.ismart, 4096, 0)
I/iSmartAuto: getPackageInfo(com.up366.ismart, 4096)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.up366.ismart, 4096, 0)
I/iSmartAuto: getText(com.up366.ismart, 2131689508, ApplicationInfo{88283f com.up366.ismart})
I/iSmartAuto: getCachedString({ResourceName com.up366.ismart / 2131689508})
I/iSmartAuto: getResourcesForApplication(ApplicationInfo{5f6850c com.up366.ismart})
I/iSmartAuto: hasSystemFeature(android.software.picture_in_picture)
I/iSmartAuto: hasSystemFeature(android.software.picture_in_picture, 0)
I/iSmartAuto: getActivityInfo(ComponentInfo{com.up366.ismart/com.up366.mobile.SplashActivity}, 269221888)
I/iSmartAuto: getUserId()
I/iSmartAuto: getActivityInfo(ComponentInfo{com.up366.ismart/com.up366.mobile.SplashActivity}, 269222528)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfo(com.miui.contentcatcher, 0)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.miui.contentcatcher, 0, 0)
I/iSmartAuto: getPackageInfo(com.up366.ismart, 64)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.up366.ismart, 64, 0)
I/iSmartAuto: getPackageInfo(com.up366.ismart, 64)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.up366.ismart, 64, 0)
I/iSmartAuto: getPackageInfo(com.up366.ismart, 64)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.up366.ismart, 64, 0)
I/iSmartAuto: getPackageInfo(com.up366.ismart, 0)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.up366.ismart, 0, 0)
I/iSmartAuto: getPackageInfo(com.up366.ismart, 0)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.up366.ismart, 0, 0)
I/iSmartAuto: getPackageInfo(com.up366.ismart, 0)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.up366.ismart, 0, 0)
I/iSmartAuto: getPackageInfo(com.miui.catcherpatch, 0)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.miui.catcherpatch, 0, 0)
I/iSmartAuto: checkPermission(android.permission.ACCESS_NETWORK_STATE, com.up366.ismart)
I/iSmartAuto: getUserId()
I/iSmartAuto: checkPermission(android.permission.ACCESS_NETWORK_STATE, com.up366.ismart)
I/iSmartAuto: getUserId()
I/iSmartAuto: getText(com.up366.ismart, 2131689508, ApplicationInfo{b1dee39 com.up366.ismart})
I/iSmartAuto: getCachedString({ResourceName com.up366.ismart / 2131689508})
I/iSmartAuto: getResourcesForApplication(ApplicationInfo{5f6850c com.up366.ismart})
I/iSmartAuto: hasSystemFeature(android.software.picture_in_picture)
I/iSmartAuto: hasSystemFeature(android.software.picture_in_picture, 0)
I/iSmartAuto: getActivityInfo(ComponentInfo{com.up366.ismart/com.up366.mobile.main.MainActivity}, 269221888)
I/iSmartAuto: getUserId()
I/iSmartAuto: getActivityInfo(ComponentInfo{com.up366.ismart/com.up366.mobile.main.MainActivity}, 269222528)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfo(com.up366.ismart, 0)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.up366.ismart, 0, 0)
I/iSmartAuto: getPackageInfo(com.up366.ismart, 0)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.up366.ismart, 0, 0)
I/iSmartAuto: getText(com.up366.ismart, 2131689508, ApplicationInfo{7baeec7 com.up366.ismart})
I/iSmartAuto: getCachedString({ResourceName com.up366.ismart / 2131689508})
I/iSmartAuto: getPackageInfo(com.up366.ismart, 0)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.up366.ismart, 0, 0)
I/iSmartAuto: getApplicationInfo(com.up366.ismart, 128)
I/iSmartAuto: getUserId()
I/iSmartAuto: getApplicationInfoAsUser(com.up366.ismart, 128, 0)
I/iSmartAuto: maybeAdjustApplicationInfo(ApplicationInfo{a00eb84 com.up366.ismart})
I/iSmartAuto: getPackageInfo(com.up366.ismart, 16384)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.up366.ismart, 16384, 0)
I/iSmartAuto: getPackageInfo(com.android.vending, 0)
I/iSmartAuto: getUserId()
I/iSmartAuto: getPackageInfoAsUser(com.android.vending, 0, 0)
I/iSmartAuto: getApplicationInfo(com.up366.ismart, 128)
I/iSmartAuto: getUserId()
I/iSmartAuto: getApplicationInfoAsUser(com.up366.ismart, 128, 0)
I/iSmartAuto: maybeAdjustApplicationInfo(ApplicationInfo{4764b21 com.up366.ismart})
I/iSmartAuto: checkPermission(android.permission.ACCESS_NETWORK_STATE, com.up366.ismart)
I/iSmartAuto: getUserId()
I/iSmartAuto: getLaunchIntentForPackage(com.up366.ismart)
I/iSmartAuto: queryIntentActivities(Intent { act=android.intent.action.MAIN cat=[android.intent.category.INFO] pkg=com.up366.ismart }, 0)
I/iSmartAuto: getUserId()
I/iSmartAuto: queryIntentActivitiesAsUser(Intent { act=android.intent.action.MAIN cat=[android.intent.category.INFO] pkg=com.up366.ismart }, 0, 0)
I/iSmartAuto: onImplicitDirectBoot(0)
I/iSmartAuto: queryIntentActivities(Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] pkg=com.up366.ismart }, 0)
I/iSmartAuto: getUserId()
I/iSmartAuto: queryIntentActivitiesAsUser(Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] pkg=com.up366.ismart }, 0, 0)
I/iSmartAuto: onImplicitDirectBoot(0)
I/iSmartAuto: queryIntentActivities(Intent { act=android.intent.action.MAIN cat=[android.intent.category.HOME] }, 65536)
I/iSmartAuto: getUserId()
I/iSmartAuto: queryIntentActivitiesAsUser(Intent { act=android.intent.action.MAIN cat=[android.intent.category.HOME] }, 65536, 0)
I/iSmartAuto: onImplicitDirectBoot(0)
I/iSmartAuto: queryBroadcastReceivers(Intent { act=android.intent.action.BADGE_COUNT_UPDATE (has extras) }, 0)
I/iSmartAuto: getUserId()
I/iSmartAuto: queryBroadcastReceiversAsUser(Intent { act=android.intent.action.BADGE_COUNT_UPDATE (has extras) }, 0, 0)
I/iSmartAuto: onImplicitDirectBoot(0)
I/iSmartAuto: checkPermission(android.permission.ACCESS_NETWORK_STATE, com.up366.ismart)
I/iSmartAuto: getUserId()

  注意那些 getPackageInfo(xxxxxxxxxxxx, 64) 的地方,64 其实就是 PackageManager.GET_SIGNATURES,这些都是非常明显的特征。我们首先把它 hook 掉:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
findClass("android.content.pm.IPackageManager\$Stub\$Proxy") hooks {
find { name == "getPackageInfo" } after { param ->
val packageName = param.args[0] as String
val flags = param.args[1] as Int
@Suppress("Deprecation")
if (packageName == "com.up366.ismart" && flags and PackageManager.GET_SIGNATURES != 0) {
val result = param.result as PackageInfo
val sign = result.signatures
// // 提取原签名信息
// val parcel = Parcel.obtain()
// sign[0].writeToParcel(parcel, 0)
// Log.e(Arrays.toString(parcel.marshall()))
val parcel = Parcel.obtain()
parcel.unmarshall(signature, 0, signature.size)
parcel.setDataPosition(0)
sign[0] = Signature.CREATOR.createFromParcel(parcel)
result.signatures = sign
}
}
}

  其中 signature 是原 iSmart 的签名信息,暂且硬编码在 companion object 中:

1
2
3
companion object {
val signature: ByteArray = byteArrayOf(117, 3, 0, 0, 48, -126, 3, 113, 48, -126, 2, 89, -96, 3, 2, 1, 2, 2, 4, 113, 93, -9, 87, 48, 13, 6, 9, 42, -122, 72, -122, -9, ......) // 太长了不写了
}

  将 patch 后的安装包安装——尝试登录——成功!做几道题目试试——也没问题!剩下的工作就是将刷课逻辑用 Kotlin 重写后集成到模块中,再做个简单的 GUI 即可