Android改造者之路 07 · 万法归宗
第 07 章 万法归宗
续写章节|沿用「原木」系列的武侠讲解体,非原作者(dayeve)作品
前情提要
师傅走到门口停了一下,叫住我。
“小子。那个 app——你只改了广告,别的什么都没动,对吧?”
“对。”
"很好。"他转过身去,摆摆手,“明天早点来。该讲藏经阁了。”
我:“???”
剧情片段
导演: 兵器有了,招式有了,破关的法子也有了。那接下来该问的是——这些东西,是从哪儿来的?
导演: 从看得懂别人写的东西开始。下一章,就叫——万法归宗。
剧情片段
前情提要
一、藏经阁
我原以师傅说的"藏经阁"是个比喻。
不是。
他真的带我走到屋后,指着那间看起来快要塌掉的旧阁楼说:“到了。”
阁楼里没有书。
四面墙从上到下钉满了纸——不是印的,是手抄的。密密麻麻的代码、类名、调用关系图,一张贴着一张,一直贴到天花板。中间只留了一条能过人的窄道。
"这些都是你抄的?"我数了数,墙上的纸至少有上千张。
"抄了二十年。"师傅点上一盏灯,“坐。”
我在唯一的矮凳上坐下,抬头看着这四面墙,忽然有点喘不过气。
"你改了这么久,"师傅在旁边坐下,“有没有想过一件事——你手上那些招式,是从哪儿来的?”
我想了想:“……书上,论坛上。”
“那些招式是别人总结的。那写那些招式的人,又是从哪儿知道的?”
我答不上来。
师傅抬手,指了指整面墙。
“这儿。”
“这面墙上每一个类名、每一行代码,都不是我编的。**它们本来就写在 android 里。**我只是把它们抄下来,弄懂,然后才能教你怎么改。”
他把灯往我这边推了推。
“所有的招式,都是某个人先写出来的。你一直在别人的作品上动刀子,却从来没看过人家是怎么写的。”
“今天开始,看。”
二、为什么改造者必须学"正向"
"师傅,"我有点不服气,“我是来学改造的,不是来学开发的。”
师傅笑了。
“我问你——第四章你改那个天气 app,改了三处才改对。你知不知道为什么前两处没反应?”
“……因为我没找对地方。”
“为什么没找对地方?”
我愣住了。
"因为你不知道开发者会把它放在哪儿。"师傅说,“你不知道他写代码的时候脑子里在想什么。”
“一个写 Android 的人,看到’开屏广告’,他会新建一个 SplashActivity;看到’判断会员’,他会写个 isVip() 放在工具类里;看到’要保存配置’,他会用 SharedPreferences。”
“他这么做是有原因的——因为框架就是这么设计的。你不知道框架怎么设计,就只能靠猜。”
他从墙上揭下一张纸,递给我。
“改造这件事,八成的时间花在找对地方。而这八成,靠的全是正向功底。”
一句话:逆向不是"不学开发",逆向是"先学会开发,再反过来拆"。你连
onCreate是干什么的都说不清,就别指望能在里面准确下刀。
三、四梁八柱:Android 的地基
师傅在桌上画了四个格子。
“android 世界里所有的 app,都由这四根柱子撑着。你改任何东西,都会碰到它们。”
四大组件
| 组件 | 干什么 | 逆向时你会在哪儿碰见它 |
|---|---|---|
| Activity | 一个界面 | 找"某个页面"的逻辑 → 先找对应的 Activity |
| Service | 后台跑的东西 | 下载、播放、保活、长连接的逻辑在这 |
| BroadcastReceiver | 收广播 | 开机启动、网络变化、推送到达后的行为 |
| ContentProvider | 提供数据给别人 | 跨 app 的数据读写,也常被用来做进程间通信 |
"它们全都必须在 AndroidManifest.xml 里注册——"师傅敲了敲桌子,“所以改任何 app,第一件事都是先看 Manifest。那是一张地图。”
Activity 生命周期
onCreate() ← 界面创建,初始化(最常改的地方)
↓
onStart() ← 界面可见
↓
onResume() ← 界面可交互(回到前台也会走)
↓
onPause() ← 失去焦点
↓
onStop() ← 完全不可见
↓
onDestroy() ← 销毁
"为什么要背这个?"师傅问。
“因为……要知道代码在哪执行?”
"对,也不全对。"他说,“更重要的用处是——你要知道’什么时候’该去改。”
“你想让 app 一打开就跳过广告,那你要改的是 onCreate 还是 onResume?”
我想了想:“onCreate?”
“不一定。很多开屏广告是在 onResume 里判断倒计时的。你改错地方,倒计时照样走完。”
“生命周期就是时间表。你要在正确的时间点动手。”
Context:最容易让人栽跟头的东西
“Context 是什么?”
“……上下文?”
"等于没说。"师傅摇头,“你就记一句话——Context 是 app 跟系统打交道的通行证。”
“要读文件?要 Context。要起 Activity?要 Context。要拿包名、要拿签名、要访问数据库?全都要 Context。”
"所以——"他看着我,“第五章那个验身君,它的函数签名是不是带一个 Context 参数?”
我猛地反应过来。
checkSign(Context ctx)。
“对。**凡是长着 Context 参数的方法,都是在跟系统要东西。**想找签名校验、找文件操作、找权限判断,搜 Context 准没错。”
Intent:组件之间怎么说话
// 显式:指名道姓
Intent it = new Intent(this, MainActivity.class);
startActivity(it);
// 隐式:说要干什么,谁会干谁上
Intent it = new Intent(Intent.ACTION_VIEW);
it.setData(Uri.parse("https://example.com"));
startActivity(it);
"Intent 里带的 extra 数据,是逆向的好东西。"师傅说,“你在 jadx 里搜 putExtra 和 getStringExtra,能直接看出数据在各个页面之间怎么流转。”
四、类加载机制(这一节最重要)
师傅忽然严肃起来,把茶杯推到一边。
“接下来这个,是整座藏经阁里最要紧的一块。你要是只能记住今天一件事,就记这个。”
他在纸上画了一条链:
ClassLoader
└── BaseDexClassLoader
├── PathClassLoader ← 加载"已安装的 apk"
├── DexClassLoader ← 加载"任意位置的 dex/jar/apk"
└── InMemoryDexClassLoader(Android 8.0+)
两者的区别
| PathClassLoader | DexClassLoader | |
|---|---|---|
| 加载谁 | 系统已安装的 apk(/data/app/...) |
任意路径的 dex / jar / apk |
| 谁能用 | 系统自己用(每个 app 默认就是它) | 插件化、热修复、你自己写的代码 |
| 关键参数 | 无需指定优化目录 | 需指定 optimizedDirectory(旧版) |
核心:dexElements 数组
"重点来了。"师傅在 BaseDexClassLoader 下面画了个方框。
BaseDexClassLoader
└── DexPathList
└── Element[] dexElements ← ★ 就是这玩意儿
“一个 app 里所有能被加载的 dex,都排在这个数组里。 系统要找一个 class,就从头到尾遍历这个数组,找到第一个就返回。”
“所以——”
他停下来,看我。
我脑子里有什么东西闪了一下。
“……所以,只要我能往这个数组里塞东西,我就能让 app 加载我自己的代码?”
师傅笑了。
“加固的壳、插件化框架、热修复、Xposed——四样完全不同的东西,用的是同一个原理。”
他把纸推给我。
- 一代壳:启动时光壳,运行时把真 dex 解密出来,用 DexClassLoader 加载 → 本质上就是往 dexElements 里塞
- 插件化:把插件 apk 的 dex 合并进宿主 → 还是塞
- 热修复:把修复后的 dex 插到数组最前面,让系统先找到它 → 还是塞,但讲究顺序
- Xposed:把 hook 代码注入到 Zygote,让所有 app 一出生就带着它
“所以第六章那个 frida-dexdump 为什么能把 dex 从内存里抠出来?”
“因为……真 dex 被 DexClassLoader 加载过,就一定在内存里。”
"万法归宗。"师傅点头,“这四个字说的就是这个——招式千变万化,底下是同一套东西。”
五、读源码的三把钥匙
“那我怎么去看这些源码?”
师傅伸出三根手指。
钥匙一:从 Log 找
adb logcat | grep "包名"
app 里凡是打了 Log.d(TAG, ...) 的地方,都会在日志里留下 TAG。拿 TAG 去源码里搜,能直接定位到类。
很多 app 上线忘了关日志,这是白送的地图。
钥匙二:从崩溃栈找
崩溃时 adb logcat 会打出完整的调用栈:
java.lang.NullPointerException:
at com.demo.weather.MainActivity.onCreate(MainActivity.java:42)
at android.app.Activity.performCreate(Activity.java:8000)
at android.app.ActivityThread.handleLaunchActivity(...)
栈里的每一行都是一个类名 + 方法名,从上往下就是调用链。这东西比什么都准。
钥匙三:从官方文档和类名猜
Android 框架的类名是有规律的,而且是公开的:
| 想找什么 | 去翻哪个类 |
|---|---|
| Activity 怎么启动的 | ActivityThread、ActivityManagerService、Instrumentation |
| 签名怎么取的 | PackageManagerService、PackageParser |
| 资源 id 怎么解析的 | AssetManager、Resources |
| 方法怎么被调用的 | ArtMethod(native 层) |
师傅补了一句:“**框架的代码是开源的,随便你看。**只有 app 自己的代码才是黑盒。很多人逆向搞不动,是因为他们连白盒都没看过。”
去哪儿看源码
| 途径 | 说明 |
|---|---|
| cs.android.com | Google 官方源码搜索,最推荐,可以直接搜类名 |
| android.googlesource.com | 完整 AOSP 仓库 |
| androidxref.com | 老牌源码交叉索引(版本较旧) |
| 拉系统 framework | adb pull /system/framework/framework.jar → 转 dex → jadx 打开 |
# 想看系统框架的实现,直接把它从手机里拉出来
adb pull /system/framework/framework.jar .
# framework.jar 里是 dex,用 jadx 直接打开即可
jadx-gui framework.jar
六、实战:跟着 Activity 启动走一遍
"光说不练假把式。"师傅在纸上画了一条线,“我们跟着一个 app 从点击图标到界面出现,走一遍。”
① Launcher 被点击
↓
② Launcher 通过 Binder 通知 ActivityManagerService(AMS)
↓
③ AMS 检查这个 Activity 有没有注册(看 Manifest!)
↓
④ AMS 让 Zygote fork 出一个新进程
↓
⑤ 新进程里跑 ActivityThread.main()
↓
⑥ 创建 Application(你的 Application 类在这儿被实例化)
↓
⑦ ActivityThread.handleLaunchActivity()
↓
⑧ 用 ClassLoader 加载目标 Activity 类 → newInstance()
↓
⑨ Instrumentation.callActivityOnCreate() → 回调你的 onCreate()
↓
⑩ 界面出现
"看第 ⑧ 步。"师傅敲了敲。
“Activity 是用 ClassLoader 加载出来的。”
"所以加固的壳可以干什么?"他看我。
我脱口而出:“在 Activity 被加载之前,先把真的 dex 准备好。”
“对。壳的 Application 会在第 ⑥ 步就动手——抢在一切之前,把加密的 dex 解出来、塞进 ClassLoader。等到第 ⑧ 步系统来找 Activity 的时候,真东西已经在里面了。”
我盯着那张纸,看了很久。
“所以……壳不是什么神秘的东西。”
"壳就是一个抢跑的 Application。"师傅把笔放下,“原理简单得要命。难的从来不是原理,是工程细节。”
七、该装进脑子里的清单
师傅把一张写得密密麻麻的纸钉在我面前。
“这些不用背,但你要知道它们存在。碰到的时候能想起来去查,就够了。”
框架层必知
Activity/Fragment生命周期Context家族(Activity / Service / Application 都是 Context)Intent与组件间通信Handler/Looper/MessageQueue(逆向时找 UI 更新特别有用)Binder(跨进程通信的地基,Xposed / 各大 hook 框架都绕不开)ClassLoader与dexElements
数据存放
SharedPreferences→ 存在/data/data/包名/shared_prefs/*.xmlSQLite→/data/data/包名/databases/- 文件 →
/data/data/包名/files/、/cache/ assets/→ 打包进 apk,只读
常见第三方库(认得脸就行)
- 网络:OkHttp / Retrofit(抓包时看到的特征)
- 图片:Glide / Picasso
- 序列化:Gson / Fastjson(改数据格式时常见)
- 事件总线:EventBus
工具命令
adb logcat # 看日志
adb shell ps | grep 包名 # 看进程
adb shell dumpsys package 包名 # 看包信息
adb pull /data/data/包名/ ./ # 拉数据(需 root)
八、尾声
我在藏经阁待到天黑。
四面墙上的字,我连十分之一都没看完。
临走的时候,师傅叫住我。
“明天带你的电脑来。”
“干嘛?”
“看了一天别人的东西,该自己写一个了。”
我有点紧张:“我写?”
"你不是一直想知道 Hook 到底是怎么做到的吗?"师傅把灯吹灭,“光看别人写,你永远只会用。”
“自己写一遍,那才是你的。”
阁楼暗下来,只剩门口一点光。
我在门口站了一会儿,回头看了一眼——那些密密麻麻的手抄纸,在黑暗里像一片沉默的星图。
剧情片段
导演: 收工。
助理: 导演,下一章讲写自己的 Hook?
导演: 对。看懂了,就该动手造。
助理: 名字呢?
导演: 藏经阁是"看",下一章是"做"——
导演: 叫打通任督二脉。
剧情片段


