Android改造者之路 09 · 抉择
第 09 章 抉择
续写章节|沿用「原木」系列的武侠讲解体,非原作者(dayeve)作品
前情提要
我用三种方式各实现了一遍:改 smali、写 Frida 脚本、写 Xposed 模块。三种都成功了。
我把手机递给师傅看,有点得意。
他接过去,点了点,问我一句话。
“现在,你会去改别人的 app 吗?”
我张了张嘴:“我……我可以改。”
“我知道你可以。我问的是——你会吗?”
师傅沉默了一会儿,忽然说:“明天有个客人来。来请你改东西的。”
“他给的价钱,是你想象不到的那种。”
剧情片段
导演: 兵器给完了,心法也教完了,最后要考的——不是技术。
导演: 下一章,叫抉择。
剧情片段
前情提要
一、客人
客人是个四十来岁的男人,穿得很普通,说话也客气。
他提了个布包,坐下之后先从里面拿出一叠打印纸,摊在桌上。
"师傅介绍我来的。"他说,“我姓陈。”
师傅坐在角落里喝茶,没说话。
陈先生指着纸说:“我们社区医院有个挂号 app,老人用不了。我听说您懂这个,想请您改改。”
我看了看那些纸——是 app 的截图,字体很小,流程有七步。
"老人确实难用。"他说,“我妈七十多了,每次挂号都得我帮她弄。我想请您把流程简化一下,最好能自动填好身份信息,再跳过那个排队页面,进去直接就能挂。”
我愣了一下。
“跳过排队页面?”
"就是先别排队,直接进。"他摆摆手,“号嘛,能挂上就行。”
我看了看师傅。
师傅在喝茶。
陈先生从布包里又拿出一个信封,放在桌上,推过来。
“这个您先拿着。改好了,再加三倍。”
我瞄了一眼,信封很厚。
我心跳有点快。
这是我第一次意识到——我手上的这些东西,是能换钱的。
二、我说:我先看看
我把信封推回去一半。
“钱的事以后说。我得先看看那个 app。”
陈先生有点意外,但很快笑了:“应该的,应该的。”
他留下一台测试机和包名,走了。
师傅这才放下茶杯。
“你没接钱。”
“……我总得先知道我要改的是什么。”
师傅点点头,没夸我,也没批评我。
"那就按流程走一遍。"他说,“从头到尾,一步不落。”
“就当是——毕业考。”
三、第一遍 · 观察
我把 app 拉下来,jadx 打开。
包名:com.med.hospital
第一件事,按师傅教的——先看 AndroidManifest.xml。
<application
android:name="com.secneo.apkwrapper.ApplicationWrapper" ← ?
... >
<activity android:name="com.med.hospital.SplashActivity" />
<activity android:name="com.med.hospital.MainActivity" />
<activity android:name="com.med.hospital.QueueActivity" /> ← 排队页
...
</application>
两个发现:
QueueActivity就是陈先生要跳过的排队页application的名字很奇怪 ——com.secneo.apkwrapper.ApplicationWrapper
"这是什么?"我问。
师傅瞟了一眼:“看 lib 目录。”
$ unzip -l hospital.apk | grep lib
lib/armeabi-v7a/libDexHelper.so ← 加固的壳
再看 classes.dex——只有 62 KB。
"铁甲君。"我说。
"铁甲君。"师傅点头。
四、第二遍 · 脱壳
按第七章学的:壳就是一个抢跑的 Application。它在 Activity 被加载之前,把真 dex 解出来塞进 ClassLoader。
那它解完之后,真 dex 就一定在内存里。
# 1. 手机跑 frida-server
# 2. spawn 方式启动(抢在壳完成之前,也便于完整 dump)
frida-dexdump -U -f com.med.hospital -o dex_dump/
# 3. 看产物
$ ls dex_dump/
classes_0x7a1b2c3d.dex 1.2M
classes_0x7a1b2c3e.dex 3.4M ← 真身在这儿
classes_0x7a1b2c3f.dex 2.1M
把那个 3.4M 的丢进 jadx。
真身出来了。
我竟然有点兴奋——这是第一次在实战里用上脱壳。
师傅在旁边泼冷水:“别高兴太早。看看里面是什么。”
五、第三遍 · 定位
真 dex 打开之后,我傻眼了。
package com.med.hospital.a;
public class b {
public static boolean a(Context context, String str) { ... }
public static int b() { ... }
public static void c() { ... }
}
全是 a、b、c。
迷魂君。
"别找名字。"师傅在旁边提醒,“找什么?”
“……找它调用了什么。”
我按第七章那张表,搜系统 API:
| 搜索 | 命中 | 判断 |
|---|---|---|
QueueActivity |
0 | 类名被混淆了 |
startActivity |
12 | 找跳转 |
getPackageInfo |
2 | 验身君 |
getCrc |
1 | 秤重君 |
okhttp3 |
38 | 有网络请求 |
/api/queue |
1 | 排队接口 |
我顺着 /api/queue 往上追,找到了那段逻辑的大致形状:
// 反混淆后我手动重命名的版本
public class QueueManager {
public boolean isQueuePassed() {
// 请求服务器,问"我排到了吗"
Response r = httpClient.get("/api/queue?userId=" + userId);
return r.isPassed(); // ← 判定权在服务器
}
}
我盯着那行 r.isPassed(),看了很久。
判定权,在服务器。
六、第四遍 · 动刀(不管怎样,先走完流程)
"继续。"师傅说。
我按第四章的六步走:
apktool d hospital.apk -o hosp/
找到 QueueActivity 对应的 smali(混淆后是 com/med/hospital/a/g.smali),在 onCreate 里:
.method protected onCreate(Landroid/os/Bundle;)V
.registers 4
invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V
# 判断是否已排队通过
invoke-static {p0}, Lcom/med/hospital/a/f;->a(Landroid/content/Context;)Z
move-result v0
if-nez v0, :cond_passed # 没通过 -> 停在排队页
# 停在排队页
invoke-direct {p0}, Lcom/med/hospital/a/g;->a()V
return-void
:cond_passed
# 进入挂号页
invoke-direct {p0}, Lcom/med/hospital/a/g;->b()V
return-void
.end method
按第六章的招式——不去理解它怎么算的,去接管它算出来的结果:
invoke-static {p0}, Lcom/med/hospital/a/f;->a(Landroid/content/Context;)Z
move-result v0
const/4 v0, 0x1 # ← 强制:排队已通过
if-nez v0, :cond_passed
改完,回装:
apktool b hosp/ -o hosp_new.apk
七、第五遍 · 盖章,然后撞上验身君
zipalign -v 4 hosp_new.apk hosp_aligned.apk
apksigner sign --ks my.keystore --ks-key-alias mykey hosp_aligned.apk
adb install -r hosp_aligned.apk
安装成功。点开——
「检测到应用已被修改,请到官方渠道下载」
然后退出。
验身君。
我笑了。这次是胸有成竹的那种。
按第六章招一,去搜 getPackageInfo(不是搜"签名"!):
Lcom/med/hospital/a/d;->a(Landroid/content/Context;)Z
里面:
invoke-virtual {v1, v2, v3}, Landroid/content/pm/PackageManager;->getPackageInfo(...)...
move-result-object v1
iget-object v1, v1, Landroid/content/pm/PackageInfo;->signatures:[Landroid/content/pm/Signature;
...
invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v0
return v0
在调用它的地方钉死结果:
invoke-static {p0}, Lcom/med/hospital/a/d;->a(Landroid/content/Context;)Z
move-result v0
const/4 v0, 0x1 # ← 强制"签名正确"
if-eqz v0, :cond_bad
重新打包、对齐、签名、安装。
进了。
排队页一闪而过,直接到了挂号页。
我做到了。
八、我停下来了
我坐在那儿,看着那个挂号页,很久没动。
师傅没说话。
"师傅,"我开口,“我能改。技术上……全都能改。”
“嗯。”
“但是——”
我指着屏幕上那个"剩余号源"的数字。
剩余号源:3
"这个数字,"我说,“是服务器给的。”
“对。”
"也就是说——"我慢慢地说,“这个排队页,不是’麻烦’,它是在分配 3 个号。”
师傅终于放下茶杯,看我。
“继续说。”
"如果我把排队跳过去,"我说,“那我进去了,别人就得等着。号只有 3 个。我跳过去了,就有人少一个号。”
"还有一个问题。"师傅说,“你能给多少人装这个改过的 app?”
我算了一下,血一下子凉了。
“……有多少人装,就有多少人插队。”
这不是"帮老人挂号"。
这是"给所有装了这个 app 的人发一张插队券"。
如果陈先生把它发给一百个人,那就有一百个人在插队。而那些真的在排队页面一秒一秒等的老人——
他们一个号都抢不到。
九、那三天
我忽然想起师傅说的那个村子。
"师傅,"我问,“您当年改的那个挂号 app——”
“嗯。”
“也是这样的?”
师傅沉默了很久。
"我当年改的时候,"他说,“不知道。”
“我看它是个挂号 app,客户说想改掉一个限制,我就改了。我甚至没打开看过它到底是干什么的。”
“改完收了钱,走了。”
“三年后我才知道,那三天里有个村子的接种通知全发错了。”
他从口袋里摸出烟,没点,就捏在手里。
“我这辈子最后悔的不是改了它。是改之前,我连看都没看一眼。”
他把烟放回口袋。
“所以我现在教你——接活之前,先看清楚你要改的是什么。”
“技术只管’能不能’。'该不该’这件事,技术不管,得你自己管。”
十、我说不
第二天,陈先生来了。
"改好了?"他眼睛亮了一下。
"改好了。"我把手机推过去,“您点一下看看。”
他点开,走到挂号页,愣住了。
“这么快?”
“嗯。排队页跳过去了。”
他高兴地拍了一下桌子,把信封又推过来:“那这——”
"陈先生,"我说,“这个我不能给您。”
他愣住了。
“……什么意思?钱不够?我可以加。”
“不是钱的事。”
我把手机拿回来,点给他看那个"剩余号源:3"。
“您看这个数字。”
“什么意思?”
“这是服务器给的号源数。今天只剩 3 个号。”
“这个排队页,不是个麻烦,它是在给所有人排顺序。谁先来谁先挂。”
陈先生皱起眉:“我就给我妈用,就一个人——”
"您昨天说,‘最好装给社区里的老人都用’。"我说。
他噎住了。
"装给一百个人,"我说,“就是一百个人插在那些老老实实排队的人前面。”
“那些老人不会用这个 app,更不会装您这个改过的版本。他们只会发现——怎么今天又没号了。”
陈先生的脸涨红了。
"你懂什么!"他声音高了起来,“我妈排了两个月都没排上!那些黄牛用脚本抢,我搞不过他们,我找个懂的人帮我一下怎么了?!”
屋子里安静下来。
师傅在角落里,还是没说话。
我等他气消了一点,才开口。
“陈先生,黄牛用脚本抢号——那是违法行为,该举报。”
"但如果我们也用这个法子插进去,"我说,“我们跟他们,就没区别了。”
他张了张嘴,没说出话。
"您要的不是’跳过排队’,"我说,“您要的是’让老人挂得上号’。这两件事不是同一件。”
十一、替代方案
我把那叠打印纸拉过来,翻到最后一页。
"我看了这个 app。"我说,“它流程确实烂,七步才挂上号,字还小。老人用不了是真的。”
“但问题不在排队页。问题在整个 app 就没给老人做过设计。”
我给他写了三条:
第一,走官方渠道。
“很多医院有电话预约和现场窗口,专门给不用智能手机的老人留着。您去问问社区医院,多半有绿色通道。这不丢人,这是他们该提供的。”
第二,别改 app,做一个"帮老人操作"的东西。
“您可以做一个无障碍辅助的小工具——只做朗读、放大字体、自动填表,不碰号源逻辑。这种工具不违规,也能真的帮上忙。”
第三,把问题反馈上去。
“把这七步流程的截图整理一下,发给医院的信息科。他们要是知道老人用不了,多半会改。这才是真正解决一百个老人的办法。”
我把纸推过去。
“改 app 只能帮一个人,而且是从别人手里抢。”
“反馈问题,能帮所有人。”
陈先生看着那张纸,看了很久。
最后他把信封收了回去。
“……我明白了。”
他站起来,走到门口,回头看了我一眼。
“你跟你师傅,真不一样。”
"他当年,"他说,“二话没说就给我改了。”
门关上之后,屋子里很静。
我转过头看师傅。
师傅终于开口了,声音很轻:
“对。”
“所以我才在这儿等你。”
十二、毕业考
那天晚上,师傅破天荒地给我倒了杯酒。
"知道今天是什么吗?"他问。
“……我搞砸了一单?”
“你毕业了。”
我愣住了。
"从第一章到第八章,我教你的一直是’能不能’。"他举起杯子,“今天考的是’该不该’。”
“你过了。”
我端着酒,忽然鼻子有点酸。
"师傅,"我问,“您找了我多久?”
他喝了一口,笑了笑。
“很久。”
“在您之前,我教过三个人。”
“第一个,学完就去做灰产了,后来进去了。”
“第二个,学了一半跑了,说这行来钱太慢。”
"第三个——"他停了一下,“第三个技术最好,比我还好。他接了个单子,改了个东西,出了事。他到现在都没出来。”
“所以后来我改了教法。先教技术,最后考人心。”
“技术好的我有的是办法教出来。但一个会说’不’的人,我教不出来——只能等。”
他把杯子里剩下的酒喝了。
“所以我那天问你’你会去改别人的 app 吗’。”
“你答不上来。答不上来,就还有救。”
剧情片段
导演: cut!! 完美。
助理: 导演,那最后……
导演: 该收了。
导演: 兵器、心法、破关、抉择——都齐了。
导演: 大结局,就叫——改造者。
剧情片段


