0
0
0

Android改造者之路 06 · 魔高一丈

文章摘要
|

第 06 章 魔高一丈

续写章节|沿用「原木」系列的武侠讲解体,非原作者(dayeve)作品

前情提要

师傅把茶杯放下,看着我:“你知道这六道关,最大的共同点是什么吗?”

“……都在 app 里?”

“对。它们都在 app 里,而 app 在你手机上。”

“只要代码要在这台机器上跑,它就一定要现形——要么现形在磁盘上,要么现形在内存里,要么现形在调用那一刻。”

“你要做的,就是在它现形的那一瞬间,抓住它。”

我好像懂了什么,又好像没完全懂。

“那具体怎么做?”

师傅站起来,往屋里走:“明天再说。今晚先把那个天气 app 的验身君找出来——就当是作业。”

我:???

剧情片段

导演: 道高一尺,后面那句是啥来着?

助理: 魔高一丈。

剧情片段

前情提要


一、一夜

灯芯烧到第三遍的时候,我终于承认——我找不到。

那个天气 app 的 smali,我翻了整整一夜。

jadx 里搜 signature,四十七个结果。搜 sign,三百多个。搜 篡改、修改、verify,零个。

我一个一个点开看。看到第三十个的时候,眼睛开始发花;看到第四十个的时候,我已经分不清 sign 到底是"签名"还是"正负号"了。

窗外的天从黑变成灰,又从灰变成白。

我趴在桌上,第一次认真怀疑:师傅是故意的。

他明知道我一夜找不到,还是让我找。

“找到了?”

师傅的声音从背后传来。我不知道他站了多久。

“……没有。”

“找了几处?”

“四十七处 signature,三百多个 sign。我一个个点开看,看到现在。”

师傅笑了,在我对面坐下,给我倒了杯茶。

“你知道你笨在哪儿吗?”

“……哪儿?”

"你在找’验身君’这三个字。"他把茶杯推过来,“可验身君不会在自己脑门上写’我是验身君’。”

我愣住了。

"第五章我是不是说过——混淆能藏住名字,藏不住行为?"他敲了敲桌子,“这句话反过来也成立:你要找一个人,别找他叫什么,看他干了什么。”

“验身君会干什么?”

“他会去问系统要自己的签名。所以他一定会出现在同一个地方——”

我猛地抬起头。

"对。"师傅点点头,“去搜他调用了什么,别搜他叫什么。”

我打开 jadx,删掉搜索框里那个该死的 signature,重新输入:

getPackageInfo

回车。

三个结果。

第一个,就是它。


二、师傅的往事

我盯着那行代码,手有点抖。熬了一整夜,答案就在三个字符的距离之外。

师傅看着我的表情,忽然不说话了。

他端起茶,喝了一口,目光落在窗外那棵老树上。

"我像你这么大的时候,"他开口了,“也是个改造者。”

我差点把茶喷出来。

“你?”

"那时候我比你现在狠。"师傅笑了笑,“接活儿,改包,什么来钱改什么。去广告、破会员、改游戏数据——一天能出十几个包。”

“那后来呢?”

“后来有一次,我接了个单子,改一个挂号 app。”

他停了一下。

“我改得挺漂亮。界面干净,功能齐全,客户很满意。直到有天他告诉我,那个 app 是给乡镇卫生所用来排班的。我改掉了它的一个校验,结果排班数据错了三天。”

屋子里安静下来。

“那三天里,有个村子的疫苗接种通知全发错了。”

我张了张嘴,没说出话。

“从那以后我就不接活了。我去做了保卫者——给 app 加壳、加校验、加反调试。我想,既然有人拿这个害人,那我就把门守严点。”

“守了几年?”

“五年。”

“然后呢?”

师傅把茶杯放下,看着我。

“然后我发现,我加的每一道锁,最后都被人打开了。”

“有的被我当年用的招打开,有的被我没见过的招打开。我守得越严,对面的人长得越快。”

他叹了口气。

“到最后我才明白一件事——最锋利的矛,和最厚的盾,是同一块铁打的。”

“所以我谁也不做了。我就在这儿,教技术。至于拿它去做什么——那是你们自己的事。”

他站起来,往屋里走,走到门口停了一下。

“这就是我今天要教你的东西。但我先说清楚:我教你破关,不代表我赞成你去破。”

“记住第二章那句话——改造者是唯一中立的存在。”

“中立的意思不是’两边都不得罪’,是你得自己选,然后自己承担。”


三、破关六法

师傅在桌上摆了六个茶杯,从近到远排开。

“六道关,六种破法。我把它们讲透——讲透的意思,是让你知道每一步为什么有效,不是给你一个咒语让你念。”


3.1 破第一关:签名校验

核心思路:它要"比对",那你要么让它比不出来,要么让它比对的结果永远是"通过"。

招一 · 改判断(smali 层,最简单)

找到调用校验的地方,把结果钉死。

# 改之前
invoke-static {p0}, Lcom/demo/weather/SignCheck;->checkSign(Landroid/content/Context;)Z
move-result v0
if-eqz v0, :cond_pass      # v0==0(校验失败)就跳走

# 改之后:在 move-result 后插入一行,强制为真
invoke-static {p0}, Lcom/demo/weather/SignCheck;->checkSign(Landroid/content/Context;)Z
move-result v0
const/4 v0, 0x1            # ← 不管算出来是什么,强制通过
if-eqz v0, :cond_pass

为什么有效:校验函数本身照常运行,只是它的返回值被你接管了。这是最省事的一招,因为你完全不用去理解它怎么算的。

招二 · Hook 签名来源(Frida 层,不改包)

不改 apk,直接在运行时把"签名"这个数据源换掉。

Java.perform(function () {
    var Signature = Java.use("android.content.pm.Signature");

    // 让任何取签名字符串的地方,都返回原版签名
    Signature.toCharsString.implementation = function () {
        var real = this.toCharsString();
        console.log("[*] 原始签名: " + real);
        return "308201dd30820146a0030201...";  // ← 填原版签名串
    };
});

为什么有效:签名校验的流程是「取当前签名 → 与预埋值比对」。你不动后半段的比对,只把前半段取出来的值换成原版,比对自然就通过了。

怎么拿到"原版签名串"?先别填,直接跑一遍脚本,用 console.log 打出现在的值——那是你重签后的签名。然后去装一个官方原版 apk,再跑一次,打出来的就是原版签名。

招三 · 工具一键(最省事)

  • MT管理器:Dex 编辑器 → 搜索 getPackageInfo → 或用其内置的「去除签名校验」
  • NP管理器:同类功能
  • 核心破解(XP 模块):全局禁用签名校验,需 root

避坑

现象 原因
改完还是检测到 校验不止一处,jadx 全局搜 getPackageInfo 看命中几处
过了启动页,用一会儿又崩 有延迟校验,常见于进入某个功能时才触发
直接 System.exit(0) 找不到判断点 搜字符串「已被修改」,从弹窗/退出的代码往上反推谁调用了它

3.2 破第二关:完整性校验(CRC)

核心思路:它称的是"重量",那你要么把秤拨回原值,要么把秤砸了。

招一 · 改判断

同 3.1 招一——找到 if 判断,把结果钉死。

招二 · Hook CRC 来源

Java.perform(function () {
    var ZipEntry = Java.use("java.util.zip.ZipEntry");

    ZipEntry.getCrc.implementation = function () {
        var crc = this.getCrc();
        var name = this.getName();
        if (name.indexOf("classes.dex") >= 0) {
            console.log("[*] 真实 CRC: " + crc + " -> 返回原值");
            return 1234567890;   // ← 填原版 CRC(从原版 apk 读出来)
        }
        return crc;
    };
});

为什么有效:跟签名校验一模一样的逻辑——不改比对,改数据源。它问"多重",你就回答原本的重量。

招三 · 让校验方法空手而归

找到那个校验方法,第一行直接返回(第 04 章招式 B):

.method private checkIntegrity()Z
    .registers 5
    const/4 v0, 0x1        # ← 直接返回 true
    return v0              # 原逻辑全在下面,永远执行不到
    ...
.end method

注意返回类型:方法签名是 Z(boolean),就得 return v0;如果是 V(void),就得 return-void。类型不匹配会直接崩溃——这是新手最常见的闪退原因之一。

避坑

  • CRC 值怎么拿到原版?装原版 apk 跑一遍脚本打印出来;或者直接解压原版 apk 看 classes.dex 的 CRC
  • 有些 app 校验的是整个 apk 的 MD5,那就去 Hook MessageDigest.digest()
  • 本质是同一招:找到它"测量"的那个函数,Hook 掉

3.3 破第三关:代码混淆

核心思路:混淆只改名字,不改行为。所以——顺着行为找,别顺着名字找。

(就是我熬了一夜才懂的那件事。)

招一 · 优先搜 API,不搜业务词

这是最实用的一招,优先级从高到低:

要找什么 搜这个(系统 API 不会被混淆)
签名校验 getPackageInfo
完整性校验 getCrc / getPackageCodePath / MessageDigest
字符串判等 equals
弹窗提示 Toast / setText / show
网络请求 okhttp / HttpURLConnection / JSONObject
日志 Log.d / Log.e

系统 API 的名字是乱不了的吗?对,因为它们不在 app 里,在 Android 框架里。

招二 · jadx 反混淆 + 手动重命名

jadx --deobf app.apk -d out        # --deobf 开启反混淆

jadx 里右键类名/方法名可以 Rename(快捷键 N)。你读懂一个 a() 是干什么的,就把它改成 checkVip()——读一个,改一个,慢慢就把地图拼出来了。

招三 · 用资源 id 反查

代码里出现 const v0, 0x7f0d0012 这种 id 时,去 res/values/public.xml 或 resources.arsc 里反查它对应哪个字符串/布局。

一个 id 旁边写着"支付成功",那段代码大概率就在处理支付结果。

招四 · 动态追踪(最省力)

静态推不出来就别推了,直接看它运行时干了什么:

Java.perform(function () {
    var Target = Java.use("com.demo.a.a");   // 就算叫 a.a 也无所谓
    Target.a.overload("java.lang.String").implementation = function (arg) {
        console.log("[*] a() 参数: " + arg);
        var ret = this.a(arg);
        console.log("[*] a() 返回: " + ret);
        return ret;
    };
});

为什么有效:你不需要知道它叫什么,你只需要知道它进出的是什么数据。参数是加密前的字符串,返回值是判定结果——一看就懂。


3.4 破第四关:加固加壳

核心思路:壳能藏住磁盘上的 dex,但藏不住内存里的 dex。

原理一句话

app 运行时,真 dex 必须被加载进内存才能执行。所以——等它加载完,从内存里拷出来。

磁盘上的 classes.dex  ──(壳,加密)──>  你看不懂
        ↓ 运行时解密加载
内存中的 dex          ──(明文)──>      dump 出来,可读

实操

# 1. 手机上运行 frida-server
# 2. 启动目标 app(壳会在这时解密真 dex)
# 3. dump
frida-dexdump -U -f com.example.app -o dex_dump/

# 产物:一堆 dex,拿去 jadx 看
jadx-gui dex_dump/classes_0x1234.dex

同类工具:FDex2、dumpDex(Xposed 模块)、BlackDex(免 root)。

三代壳怎么办

代际 能不能脱 建议
一代(整体加密) 能,dump 效果很好 直接上 frida-dexdump
二代(方法抽取) 能,但要回填方法体 用专业脱壳机(如 FART)
三代(Dex2C / VMP) 基本不能 建议放弃或换方向

三代壳把 Java 逻辑转成了 native 或自定义虚拟机指令。这时候你面对的已经不是 smali 问题,是二进制逆向问题——那是 IDA 的领域,不是新手该碰的。

最实用的一句话

看到加固,先问自己:这个 app 值得我花这个力气吗?

大多数情况下,答案是"不值得"。


3.5 破第五关:环境检测

核心思路:它在"观察你",那你就别让它看见,或者让它看见它想看的。

招一 · 隐藏 root(对付 root 检测)

  • Magisk → 设置 → 隐藏 Magisk 应用(改包名)
  • 开启 DenyList,把目标 app 勾上
  • 装 Shamiko 模块(比 DenyList 更彻底的隐藏)

招二 · 改 Frida 的"样子"(对付 frida 检测)

默认 frida-server 监听 27042 端口,且进程里有 frida-agent 字样——这是它的两个招牌。

# 换端口启动
./frida-server-16.0.0-android-arm64 -l 0.0.0.0:8080

或者改用 frida-gadget:把 so 打包进 apk 里,没有独立进程、没有默认端口,很难被检测。

招三 · Hook 掉检测函数

Java.perform(function () {
    // 1. 反调试:让"是否被调试"永远返回 false
    var Debug = Java.use("android.os.Debug");
    Debug.isDebuggerConnected.implementation = function () {
        return false;
    };

    // 2. 反 root:让 su 文件"不存在"
    var File = Java.use("java.io.File");
    File.exists.implementation = function () {
        var p = this.getPath();
        if (p.indexOf("/su") >= 0 || p.indexOf("magisk") >= 0) {
            console.log("[*] 屏蔽 root 检测: " + p);
            return false;
        }
        return this.exists();
    };
});

注意:Hook File.exists 是全局的,会影响所有文件判断,可能引发奇怪的副作用。实战中尽量只 Hook 具体的检测函数,这个例子只是演示思路。

招四 · 用现成工具

Objection(基于 Frida 的命令行工具)内置了一堆开关:

objection -g com.example.app explore
android root disable          # 一键禁用 root 检测
android sslpinning disable    # 一键绕过 SSL Pinning
android emulator detect       # 看看它检测了哪些模拟器特征

一个重要的判断

如果你的改造在真机上正常、模拟器上崩溃,或开着 Frida 就崩、关掉就好——先别怀疑自己的代码。

先怀疑照妖君。


3.6 破第六关:服务端校验

核心思路:本地改不了服务器,那就改你们之间的对话。

招一 · 抓包改响应(最常用)

以 Charles 为例:

  1. 手机配代理,装 Charles 根证书(Android 7+ 需装进系统证书区)
  2. 找到 app 请求会员状态的那个接口
  3. 右键 → Breakpoints,下次请求会暂停,你手动把响应里的 "isVip": false 改成 true
  4. 或者用 Map Local,把某个接口永久指向你写好的本地 JSON
{
  "code": 0,
  "data": {
    "isVip": true,
    "expireDate": "2099-12-31"
  }
}

招二 · 改请求(改你发出的东西)

把请求里的参数改掉,比如把 userId 换掉、把 amount 改小。

招三 · 认清它的边界

这一关跟前五关有个本质区别:

  • 前五关:东西在你手机上,你总有办法
  • 第六关:东西在别人服务器上,你能改的只有"你看到的"和"你发出的"

所以服务端校验理论上无法从本地真正绕过——抓包改响应,改的只是你自己这一台手机的显示。

换个手机、换个账号,一切照旧。

该收手的时候

师傅说到这儿,停了一下。

“这一关我跟你说清楚——如果对方的关键价值在服务器上,那就不是你该改的东西。”

“改通信跟改本地,是两回事。前者只影响你自己,后者可能动到别人的东西。”


破关速查表

关卡 最快的破法 备选 难度
一 · 签名校验 smali 里 const/4 v0, 0x1 钉死结果 Hook Signature.toCharsString;MT 一键 ★☆☆☆☆
二 · 完整性校验 同上,或整个方法 return Hook ZipEntry.getCrc ★★☆☆☆
三 · 混淆 搜系统 API 而非业务词 jadx --deobf + 重命名;动态追踪 ★★★☆☆
四 · 加固 frida-dexdump 内存 dump FDex2 / BlackDex;三代壳建议放弃 ★★★★☆
五 · 环境检测 Magisk DenyList + Shamiko 改 frida 端口;Objection 一键 ★★★☆☆
六 · 服务端 抓包 + Breakpoints 改响应 Map Local;改请求参数 ★★★★★

贯穿六关的一条总纲:

不要去理解它怎么算的,去接管它算出来的结果。

签名、CRC、root 检测、会员判定——全都一样。找到那个"产出结论"的函数,把结论换成你想要的,你甚至不需要读懂中间那几十行代码。


四、小剧场模式:矛与盾

小剧场模式

导演: 最后一场,矛与盾的对手戏。灯光暗一点。

助理: 3 2 1。。。action

验身君: 我验它身上的印章。

我: 我就让印章变成原来那枚。

秤重君: 我称它的分量。

我: 我就报给你原本的分量。

迷魂君: 我让它看不懂。

我: 那我就不看名字,我看它干什么。

铁甲君: 我让它连看都看不到。

我: 那我就等它跑起来,从内存里把它揪出来。

照妖君: 我看它是不是改造者。

我: 那我就……不是。

云端君: (沉默良久)我把东西放在它够不着的地方。

我: ……

我: 这个,我确实够不着。

云端君: 那你打算怎么办?

我: 我想想。

我: 如果我够不着,那说明——那东西本来就不该我拿。

云端君: (微微点头)算你识相。

保卫者众: ……

我: 不过话说回来——你们六位,守了这么多年,有没有守住过?

验身君: ……没有。

铁甲君: 也没有。

照妖君: 一次都没有。

我: 那你们还守?

铁甲君: 守。

我: 为什么?

铁甲君: 因为守得住一次,就少一个人受害一次。 不需要永远守住。

我: ……(沉默)

我: 那我呢?我学这些,是为了什么?

师傅: (画外音)你说呢?

导演: cut!! 完美,收工。

小剧场模式


五、尾声

第二天天黑的时候,我把那个天气 app 装回了手机。

开屏广告没了,界面清爽。启动,运行,一切正常。

那个「检测到应用已被修改」的弹窗,再也没出现过。

师傅坐在藤椅上,看我折腾完,问了一句:

“所以,谁赢了?”

我愣了一下。

“你是说,我和那个 app?”

"我是说,"他慢慢地说,“道高一尺,魔高一丈——你觉得这是在说谁压过谁?”

我想了想:“……是说魔比道厉害?”

师傅笑了。

“是说这事儿没有终点。”

“道长一尺,魔就长一丈;魔长一丈,道又长一尺。两边都在长,谁也吃不掉谁。”

“所以你今天破的这道关,明天就会长出新的。你明天学会的这招,后天就不灵了。”

我有点沮丧:“那我学这些有什么用?”

"有用。"师傅站起来,走到窗边,“因为你在长。”

“这行里真正值钱的,从来不是某一招某一式,是你在一次次失效里,练出来的那双眼睛。”

"洞察、推算、分析、辨别……"他回头看我,“第二章那七个字,你现在能数出第七个了吗?”

我掰着手指头:洞察、推算、知识、分析、辨别、联想……

"领悟。"我说。

师傅点点头,笑了。

“领悟不是学会了,是明白了正确的使用姿势,并能在未知的领域发挥效果。”

“记住这句话。兵器会旧,招式会过时,但这个不会。”

我点点头,收起手机。

走到门口的时候,师傅忽然叫住我。

“小子。”

“嗯?”

“那个 app——你只改了广告,别的什么都没动,对吧?”

“对。”

“很好。”

他转过身去,摆摆手。

“明天早点来。该讲藏经阁了。”

我:“???”

(又来……)

剧情片段

导演: 收工。

助理: 导演,藏经阁是什么?

导演: 兵器有了,招式有了,破关的法子也有了。

导演: 那接下来该问的是——这些东西,是从哪儿来的?

助理: 您的意思是……

导演: 从看得懂别人写的东西开始。

导演: 下一章,就叫——万法归宗。

剧情片段

支持与分享

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

评论

欢迎来到我的博客!