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(),你根本分不清谁是谁。
混淆的三种层次
| 类型 | 做了什么 | 破起来有多累 |
|---|---|---|
| 名称混淆 | 类名/方法名/字段名换成无意义短名 | 累,但能啃 |
| 字符串加密 | 代码里的字符串运行时才解密 | 很累,字符串搜不到了 |
| 控制流混淆 | 打乱执行顺序、插入无用分支、平坦化 | 极累,读不出逻辑 |
新手怎么啃混淆
- 别去读"名字",去读"行为" —— 名字没有意义,看它调用了什么 API 才是关键
- 从"锚点"入手 —— 系统 API 的名字不会被混淆(
setText、getPackageInfo、Log.d都还是原样),顺着这些锚点往上推 - 看资源 id ——
0x7f0xxxxx是固定的,去resources.arsc反查它对应哪个字符串,能猜出附近代码在干嘛 - 用动态调试验证猜想 —— 静态推不出来就上 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,功能还在 → 本地校验,能改
- 断网就失效 / 报错 → 服务端校验,本地改了没用
- 联网后打回原形 → 一定是服务端校验
这一关意味着什么
它意味着——改造者的天花板在这里。
你可以在本地把界面改得花里胡哨,但只要核心价值是服务器给的,你就动不了它。
到这一步,摆在你面前的就只剩两条路:
- 改通信 —— 抓包、改请求或改响应(这是另一门功课)
- 放弃 —— 承认这个东西不该改
而第二条路,往往是更成熟的选择。
九、道高一尺
六道关讲完了。回头看,会发现一个规律:
所有的防护都在提高你的成本,而不是让你绝对做不到。
- 验身君、秤重君:提高你改完能跑起来的成本
- 迷魂君:提高你读懂代码的成本
- 铁甲君:提高你看到代码的成本
- 照妖君:提高你使用工具的自由
- 云端君:直接改变游戏规则——它不跟你玩本地
所以师傅说"道高一尺",说的其实是:保卫者每加高一尺,改造者就得长高一丈。
那么问题来了:改造者凭什么长这一丈?
师傅把茶杯放下,看着我:“你知道这六道关,最大的共同点是什么吗?”
我想了想:“……都在 app 里?”
“对。它们都在 app 里,而 app 在你手机上。”
“只要代码要在这台机器上跑,它就一定要现形——要么现形在磁盘上,要么现形在内存里,要么现形在调用那一刻。”
“你要做的,就是在它现形的那一瞬间,抓住它。”
我好像懂了什么,又好像没完全懂。
“那具体怎么做?”
师傅站起来,往屋里走:“明天再说。今晚先把那个天气 app 的验身君找出来——就当是作业。”
我:???
(这师傅……是故意的吧)
剧情片段
导演: 收工。
助理: 导演,下一章标题定了吗?
导演: 道高一尺,后面那句是啥来着?
助理: 魔高一丈。
导演: 行,就它了。
剧情片段


