0
0
0

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/*.xml
  • SQLite → /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?

导演: 对。看懂了,就该动手造。

助理: 名字呢?

导演: 藏经阁是"看",下一章是"做"——

导演: 叫打通任督二脉。

剧情片段

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

评论

欢迎来到我的博客!