0
0
0

Android改造者之路 05 · 道高一尺

文章摘要
|

第 05 章 道高一尺

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

前情提要

我把那个天气 app 的开屏广告去掉了,界面清爽了不少。虽然只是注释掉了一行 invoke。

但那是我第一次,真正意义上让一个程序听我的话。

师傅端着茶杯走过来,瞟了一眼屏幕:“改了?”

“改了。”

“改了几处?”

“……三处。前两处没反应。”

师傅笑了:“记住这个感觉。改造这件事,八成的时间花在找对地方,两成的时间花在动手。”

我点点头,正想说话,手机突然一震。

屏幕亮起来,是那个天气 app 弹出的新提示:

「检测到应用已被修改」

我:???

师傅放下茶杯,笑得更开心了:“哦?看来是时候跟你讲讲——那些不希望你改造的人,都藏了什么手段了。”

剧情片段

导演: 收工。下一场,该轮到保卫者登场了吧?

助理: 对,听说还要讲签名校验、加固、混淆那一套。

导演: 行,那就叫——道高一尺。

剧情片段

前情提要

我盯着屏幕上的「检测到应用已被修改」,半天没说出话来。

“它怎么知道的?”

师傅抿了口茶:“你改了它的身子,它当然知道。你以为 android 世界里,只有你会改造?”

“那……保卫者到底藏了哪些手段?”

师傅伸出六根手指。

“六道关。这六道关,是开发者与保卫者联手筑起来的。我一道一道讲给你听。”


一、六道关

先给个全貌,心里有个底:

关卡 手段 它在防什么 对新手的恶心程度
第一关 签名校验 防止 apk 被重打包 ★★☆☆☆
第二关 完整性校验 防止文件被改动 ★★☆☆☆
第三关 代码混淆 让你读不懂代码 ★★★☆☆
第四关 加固加壳 让你连代码都看不到 ★★★★☆
第五关 环境检测 发现你在调试/Hook/root ★★★★☆
第六关 服务端校验 把关键逻辑挪走,本地改了也没用 ★★★★★

记住一个总原则:这六道关不是用来"拦住"你的,是用来"劝退"你的。

所有的防护都只在做一件事——提高你改造的成本。当成本高过收益,大多数人就放弃了。


二、小剧场模式:保卫者登场

小剧场模式

导演: 这一场是保卫者集体亮相,气势要足。

助理: 3 2 1。。。action

验身君: 我是第一关,验身君。我干的事很简单——app 一启动,我就去问系统:“装在我身上的这个签名,跟作者原来盖的那枚一样吗?” 不一样?那就说明你动过手脚。有多少改造者改得漂漂亮亮,最后栽在我这一句问话上。

秤重君: 我是第二关,秤重君。验身君只看印章,我看分量。我会去读 apk 里 classes.dex 的 CRC32——你哪怕只改了一个字节,这个数就变了。改完不重新签名?过不了验身君。重新签了?那也过不了我。

迷魂君: 我是第三关,迷魂君,江湖人称混淆。我不拦你,我让你看不懂。你熟悉的 UserManager、checkVipStatus,经过我的手,统统变成 a、b、a()。再加上字符串加密、控制流打乱——保证你打开 jadx 的那一瞬间,就想关掉电脑。

铁甲君: 我是第四关,铁甲君,加固/加壳。我不只是让你看不懂,我是让你根本看不到。真正的 dex 被我加密起来了,你 apktool 拆出来的那个 classes.dex,只有几十 KB——那是我留下的空壳。真身在运行时才从内存里现形。

照妖君: 我是第五关,照妖君。前面四关都是"静态"的,我是"动态"的——我看的是你这个人。你手机 root 了吗?装了 Magisk 吗?开着 frida-server 吗?挂了 Xposed 吗?是不是在模拟器上跑?是不是正被调试器挂着?但凡有一项对不上,我就不让你继续。

云端君: 我是第六关,也是最后一道,云端君。前面五位再怎么防,都在你自己手机上——手机在你手里,理论上你总能改。所以我干脆把最关键的那句话放到服务器上:会员是不是有效、试用还剩几天,全由我说了算。你在本地改得再花哨,我一返回,全都打回原形。

云端君: 所以记住——凡是联网后打回原形的改造,都是我的功劳。

导演: cut!! 好,六关亮相完毕。

我: (小声)……那我还改个什么劲?

师傅: (笑)所以下一章才叫魔高一丈啊。

小剧场模式


三、第一关:签名校验

这是最常见、也是新手最先撞上的一道。

它长什么样

典型的写法是这样(Java):

public static boolean checkSign(Context ctx) {
    String REAL_SIGN = "308201dd30820146a00302010202...";  // 作者预先算好的签名串
    try {
        PackageInfo pi = ctx.getPackageManager().getPackageInfo(
                ctx.getPackageName(), PackageManager.GET_SIGNATURES);
        Signature[] sigs = pi.signatures;
        String current = sigs[0].toCharsString();
        return REAL_SIGN.equals(current);
    } catch (Exception e) {
        return false;
    }
}

逻辑就一句话:取出当前 apk 的签名,跟预埋在代码里的那串比对,不一样就判定被改。

判定之后通常干这几件事之一:

  • 弹个「应用已被修改,请到官方渠道下载」的对话框然后退出
  • 直接 System.exit(0)
  • 偷偷上报服务器,或者让某个功能静默失效(最阴险的一种)

补充:Android 9(API 28)起 GET_SIGNATURES 被废弃,新版 app 会用 GET_SIGNING_CERTIFICATES。你在 smali 里看到的可能是 getPackageInfo(Ljava/lang/String;I),那第二个参数就是标志位。

在 smali 里怎么认出它

搜这几个关键词,命中基本就是:

Landroid/content/pm/PackageManager;->getPackageInfo(
Landroid/content/pm/PackageInfo;->signatures
Landroid/content/pm/Signature;->toCharsString(

或者搜字符串:signature、签名、已被修改、verify。


四、第二关:完整性校验

签名校验看的是"章",完整性校验看的是"肉身"。

常见做法

// 读取自身 apk 中 classes.dex 的 CRC32,与预埋值比对
ZipFile zf = new ZipFile(ctx.getPackageCodePath());
ZipEntry ze = zf.getEntry("classes.dex");
long crc = ze.getCrc();          // 你改一个字节,这个数就变
if (crc != EXPECTED_CRC) {
    // 判定被改
}

除了 CRC,还有这些变体:

  • 计算 classes.dex 或整个 apk 的 MD5 / SHA1
  • 检查 META-INF 目录下文件的数量与名称
  • 检查自身 apk 的文件大小是否等于预埋值
  • 检查 /data/local/tmp、sdcard 下有没有可疑文件

为什么它比签名校验更烦

因为你改完了一定会重新签名,签名校验就过了;但只要你改过 dex,CRC 一定变——所以秤重君这一关是躲不过去的,只能正面处理。


五、第三关:代码混淆

混淆不拦你,只让你读不懂。

混淆前后长什么样

混淆前:

public class UserManager {
    private boolean isVip;
    public boolean checkVipStatus() {
        return isVip;
    }
}

混淆后(ProGuard / R8):

public class a {
    private boolean a;
    public boolean a() {
        return this.a;
    }
}

类名、方法名、字段名全变成 a、b、c。一个包里几十个 a.a.a(),你根本分不清谁是谁。

混淆的三种层次

类型 做了什么 破起来有多累
名称混淆 类名/方法名/字段名换成无意义短名 累,但能啃
字符串加密 代码里的字符串运行时才解密 很累,字符串搜不到了
控制流混淆 打乱执行顺序、插入无用分支、平坦化 极累,读不出逻辑

新手怎么啃混淆

  1. 别去读"名字",去读"行为" —— 名字没有意义,看它调用了什么 API 才是关键
  2. 从"锚点"入手 —— 系统 API 的名字不会被混淆(setText、getPackageInfo、Log.d 都还是原样),顺着这些锚点往上推
  3. 看资源 id —— 0x7f0xxxxx 是固定的,去 resources.arsc 反查它对应哪个字符串,能猜出附近代码在干嘛
  4. 用动态调试验证猜想 —— 静态推不出来就上 Frida 打印参数和返回值

记住:混淆能藏住名字,藏不住行为。 一个方法就算叫 a(),只要它在调 getPackageInfo 比对签名,那它就是签名校验。


六、第四关:加固加壳

这是新手最容易一头撞死的一关。

怎么一眼认出它

解压 apk,看两处:

$ unzip -l app.apk | grep lib
lib/armeabi-v7a/libjiagu.so        # 360 加固
lib/armeabi/libexec.so             # 梆梆加固
lib/armeabi/libDexHelper.so        # 腾讯乐固
lib/armeabi/libprotectClass.so     # 爱加密

再看 classes.dex 的大小——如果只有几十 KB,而 app 功能却很复杂,那基本就是套了壳。

还有个信号:反编译后 AndroidManifest.xml 里的 application 节点被换成了一个奇怪的名字(壳的入口)。

三代壳

代际 原理 拆出来的 dex
一代壳 dex 整体加密,运行时解密后用 DexClassLoader 加载 空壳,真 dex 在内存里
二代壳 方法级抽取:把方法体指令抽空,运行时才回填 dex 有结构但方法体是空的
三代壳 Dex2C / VMP:Java 逻辑转成 native 或虚拟机指令 你看到的东西基本没意义了

新手的第一反应应该是:绕开

看到加固的第一反应不该是"我要脱了它",而是"我能不能换个没加固的样本"。

原因很简单:脱壳本身是另一门功课,涉及 Hook ClassLoader、内存 dump、修复 dex 头,对新手极其不友好。

如果非脱不可,思路是这样的(本章只讲原理,实操放到后面):

# 思路:app 运行时,真 dex 一定会被加载进内存 → 从内存里 dump 出来
frida-dexdump -U -f com.example.app -o dex_dump/

dump 出真 dex 后,再拿去 jadx / apktool 正常分析。

一句话总结脱壳的本质:壳能藏住磁盘上的 dex,但藏不住内存里的 dex——因为 CPU 要执行它,它就必须现形。


七、第五关:环境检测

这一关不看你的成品,看你这个人、这台设备。

它在查什么

检测项 常见手段
是否被调试 android.os.Debug.isDebuggerConnected();读 /proc/self/status 里的 TracerPid,不为 0 说明被 attach
是否装了 Frida 检测 27042 端口是否开放;/proc/self/maps 里搜 frida-agent、gadget、linjector
是否装了 Xposed 检测 libxposed_art.so;遍历已安装包名;在调用栈里搜 de.robv.android.xposed
是否 root 找 /system/bin/su、/system/xbin/su;检测 com.topjohnwu.magisk 包;ro.build.tags 含 test-keys
是否模拟器 ro.product.model 含 sdk/Emulator;ro.build.manufacturer 为 unknown/Genymotion;IMEI 为空或固定值;传感器数量异常

小剧场里照妖君那句话的深意

他说"我看的是你这个人"——意思是:前面几关是 app 在防守,这一关是 app 在观察你。

这带来一个重要的实战心法:

如果你的改造在真机上正常、在模拟器上崩溃,或者开着 Frida 就崩、关掉就好——那不是你改错了,是照妖君发现你了。


八、第六关:服务端校验

这是唯一一道理论上无法从本地绕过的关卡。

它是什么样的

关键判断不在 app 里,而在服务器上:

app  ──请求──>  服务器
     <──响应──  { "isVip": false, "trialDays": 7 }

你在本地把 isVip 改成 true 有什么用?下次请求一来,服务器还是告诉你 false。

常见的形式:

  • 会员状态、试用天数、次数限制全部由服务端返回
  • 请求带 sign/timestamp,签名算法在客户端(可逆向,但很累)
  • 关键计算(比如成绩、金币)在服务端完成

怎么判断是不是服务端校验

最简单的一招:断网。

  • 断网打开 app,功能还在 → 本地校验,能改
  • 断网就失效 / 报错 → 服务端校验,本地改了没用
  • 联网后打回原形 → 一定是服务端校验

这一关意味着什么

它意味着——改造者的天花板在这里。

你可以在本地把界面改得花里胡哨,但只要核心价值是服务器给的,你就动不了它。

到这一步,摆在你面前的就只剩两条路:

  1. 改通信 —— 抓包、改请求或改响应(这是另一门功课)
  2. 放弃 —— 承认这个东西不该改

而第二条路,往往是更成熟的选择。


九、道高一尺

六道关讲完了。回头看,会发现一个规律:

所有的防护都在提高你的成本,而不是让你绝对做不到。

  • 验身君、秤重君:提高你改完能跑起来的成本
  • 迷魂君:提高你读懂代码的成本
  • 铁甲君:提高你看到代码的成本
  • 照妖君:提高你使用工具的自由
  • 云端君:直接改变游戏规则——它不跟你玩本地

所以师傅说"道高一尺",说的其实是:保卫者每加高一尺,改造者就得长高一丈。

那么问题来了:改造者凭什么长这一丈?

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

我想了想:“……都在 app 里?”

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

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

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

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

“那具体怎么做?”

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

我:???

(这师傅……是故意的吧)

剧情片段

导演: 收工。

助理: 导演,下一章标题定了吗?

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

助理: 魔高一丈。

导演: 行,就它了。

剧情片段

支持与分享

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

评论

欢迎来到我的博客!