首页 / 资讯中心 / 文章详情

jocky实战:Eclipse老工程Java混淆接入与避坑

jocky实战:Eclipse老工程Java混淆接入与避坑 ★ FEATURED ARTICLE
简介这份面向Java开发者的Eclipse插件工具包围绕Jocky源码级混淆编译器展开适合需要在开发阶段直接完成代码混淆、减少后期额外处理环节的工程人员。资源共13个文件以4个txt说明文档、3个jar插件包和2个xml配置文件为主另含zip压缩包、url链接、jpg示意图及mf清单文件整体约692KB体积轻便便于快速取用与部署。已有1000人学习下载说明其在Eclipse混淆插件场景中具备一定参考价值。通过插件方式接入后用户可在工程右键菜单中调用Jocky相关功能其特点是在编译过程中直接对源码做混淆处理而非仅针对字节码二次加工。包内说明文档与配置样例可用于理解插件目录组织、接入方式及基础使用流程帮助开发者较快完成环境准备与功能验证。1. 为什么还在用 jocky一个 Eclipse 老工程的混淆选择前阵子帮一个做工业手持设备的团队看打包脚本工程还是 Eclipse 时代的 ADT 结构ProGuard 的配置整段被注释掉了。诉求很具体APK 要过甲方的静态扫描类名和方法名不能裸奔但 ProGuard 一开就报一堆cant find referenced class关掉又等于没混淆。翻工具目录时发现一个 jocky 的 jar这是早年安卓圈常用的 Java 代码混淆工具走字节码改写路线不依赖 ProGuard 那套规则体系对 Eclipse 老工程尤其省心。它能干三件事类/方法/字段重命名、字符串常量加密、调试信息清理适合还在维护老工程、又不想重构构建链的人。下面按接入、配置、踩坑、验证的顺序拆一遍。2. 拆开 jocky 的混淆链路字节码改写到底改了什么2.1 从 class 到 dexjocky 介入的时机Eclipse ADT 工程的打包链路并不复杂src下的 Java 文件先被 Eclipse 内置编译器编成bin/classes下的 class 文件然后 ADT 调用 dx 把这些 class 转成classes.dex最后和资源一起打进 APK。jocky 的位置就在 class 之后、dx 之前输入是 class 目录或者打好的 jar输出是改写后的 class 或 jar。它和 ProGuard 抢的是同一个卡位但两者的处理深度完全不同。ProGuard 的流程是 shrink删无用代码、optimize优化字节码、obfuscate改名、preverify预校验四步每一步都要读完整的依赖库和规则文件所以配置重、报错多。jocky 只做 obfuscate 这一层不删代码、不做数据流优化因此它不需要完整的 classpath也不需要-dontwarn那套东西。这个取舍带来的直接好处是配置轻、跑得快代价是它不会帮你瘦身方法数该多少还是多少。提示如果工程的痛点是 64K 方法数超限jocky 解决不了得上 multidex 或者换 R8它的价值只在“让反编译出来的东西不可读”这一件事上。还有一个容易被忽略的点jocky 必须先扫全量类才能改名。因为改名要保证继承链一致——父类的getUser()改成a()子类重写的getUser()也必须改成a()否则多态直接失效接口方法和实现类方法同理。这意味着它不能单文件处理必须先构建一张类图再统一决策。增量构建时类图不完整改名决策就会出错后面第 5 章会专门讲这个坑。2.2 四类混淆动作重命名、字符串加密、控制流、调试信息第一类是标识符重命名也是 jocky 的核心动作。包名会被压平com.example.app.ui可能变成a.b类名、方法名、字段名换成短名或者形近字符。这里有个细节重载方法改名后不能撞车两个同名不同参的方法会被加上不同后缀区分。字段改名还要考虑父类字段和子类字段同名的情况处理不干净会出现“子类字段遮蔽父类字段”的语义变化这类问题在编译期看不出来运行时才炸。第二类是字符串常量加密。class 常量池里的CONSTANT_String会被替换成密文同时插入一段解密调用运行时第一次用到才还原。接口地址、渠道号、硬编码的密钥这类东西反编译出来就是明文加上这层之后至少不是一眼可见。代价是启动时多几次解密调用量级在毫秒级老设备上能感知到一点但通常可以接受。第三类是控制流混淆把基本块打散、插入条件跳转或者把if/else改写成switch。jocky 在这块强度一般对抗手工逆向不够看但能把自动化分析工具的产出搅乱。第四类是调试信息清理删掉SourceFile、LineNumberTable、LocalVariableTable崩溃堆栈里的行号会变成 Unknown类名方法名也变成短名。这一步对减小包体有一点点帮助但真正的代价是线上排查必须依赖 mapping 文件。注意字符串加密和反射是天然冲突的。反射里写的Class.forName(com.example.model.User)是字符串加密之后运行时解出来还是原名但类本身已经被改名了照样找不到。这类字符串要么 keep 类名要么单独做映射替换。2.3 和 ProGuard 的边界什么场景该选谁把两者的差异摊在一张表里选型就清楚了维度jockyProGuard / R8处理内容重命名、字符串加密、调试信息清理压缩、优化、混淆、预校验规则体系简单的 keep 列表完整的-keep/-dontwarn规则依赖分析不需要完整 classpath需要缺依赖就报错处理速度快不做数据流分析慢开 optimize 更慢对新版字节码支持有限高版本 class 容易翻车持续更新反射友好度差全靠手动 keep同样差但生态里有现成规则维护状态基本停更活跃结论不复杂类数量可控、反射点少、构建链老、打包机内存紧张的工程jocky 是能用的新工程、依赖几十个 aar、用 Kotlin 和 Java 8 特性的别碰它直接上 R8。我一般把 jocky 当成“老工程的后悔药”而不是通用方案。2.4 混淆后的产物结构包名压平带来的连锁反应jocky 处理完之后输出目录里的 class 文件路径会跟着包名一起变。原本com/example/app/ui/MainActivity.class可能变成a/b/c.class这对 dx 没影响但对构建脚本有影响——任何在混淆之后按原路径找 class 的步骤都会失败比如自定义的资源处理脚本、按包名过滤的拷贝任务。另一个连锁反应是R类。资源 ID 的引用在字节码里是常量改名不影响但如果有代码通过反射拿R.drawable.xxx的字段名就会断。常见做法是把R类整个 keep 住别让它参与改名。还有BuildConfig里面有DEBUG、APPLICATION_ID这类常量混淆后字段名变了代码里如果按字段名反射取值同样会断。产物里还会多出两个文件mapping 和 log。mapping 是“原名 → 混淆名”的对照表log 记录哪些类被跳过、哪些规则没命中。这两个文件别随手删后面还原堆栈、排查 keep 规则全靠它们。3. 在 Eclipse 工程里接上 jocky插件与 Ant 两条路3.1 插件方式工程属性里的混淆开关最省事的接法是 Eclipse 插件。把发行包解压plugins目录下的 jar 复制到 Eclipse 安装目录的dropins文件夹重启 IDE。然后在工程上右键 Properties如果插件加载成功会多出一个 jocky 的配置页里面通常有启用开关、keep 文件路径、输出目录、日志路径这几项。配置完之后走Export Android Application导出 APK控制台会打印混淆过程的日志。第一次跑建议把日志级别调高确认它确实处理了你的类而不是静默跳过。注意插件对 Eclipse 版本比较敏感太新的 IDE 可能认不出老插件的扩展点另外它一般只挂在导出向导上Run As不会触发混淆调试时别指望看到混淆后的行为。插件方式适合单人维护的小工程但它有两个硬伤一是配置项少很多命令行参数暴露不出来二是构建过程不透明出了问题只能看日志猜。团队协作或者要接 CI 的场景还是得走 Ant 或命令行。3.2 Ant 任务方式把混淆挂到打包流程Ant 方式可控性最好适合把混淆和编译、打包串成一条固定流水线。核心是在build.xml里声明 taskdef然后按顺序调 compile、jar、jocky、dx。?xml version1.0 encodingUTF-8? project nameobfuscate-demo defaultobfuscate basedir. !-- jocky 的 jar 路径按实际发行包位置改 -- property namejocky.jar valuetools/jocky.jar / property namein.jar valuebuild/classes.jar / property nameout.jar valuebuild/classes-obf.jar / !-- classname 按发行包 README 里的 taskdef 说明填这里只是示意 -- taskdef namejocky classnamecom.jocky.JockyTask classpath pathelement location${jocky.jar} / /classpath /taskdef target nameobfuscate !-- 输入用干净的编译产物输出单独放别覆盖输入 -- jocky inputJar${in.jar} outputJar${out.jar} keepFileconfig/keep.txt mappingFilebuild/mapping.txt logFilebuild/jocky.log level5 encryptStringfalse / /target /project这段配置的逻辑很直白taskdef把 jocky 的任务类注册进 Antjocky标签里的属性就是一轮混淆的全部输入输出。inputJar和outputJar必须分开覆盖输入会导致第二次构建拿到已经被混淆过的 class再混淆一遍直接坏掉。level先给 5稳定之后再往上加。encryptString第一次先关等主流程跑通了再开不然字符串解密的问题会和混淆本身的问题混在一起排查成本翻倍。在 Eclipse 里挂 Ant Builder 的步骤工程 Properties → Builders → New → Ant Builder选中build.xml把 target 设成obfuscate触发时机勾在 Java Builder 之后。这样每次保存编译完混淆会自动跑一遍日志直接进 Console。3.3 命令行方式脱离 IDE 跑批量构建CI 或者打包机上没有 Eclipse就用命令行。参数形式和 Ant 属性基本对应只是前缀不同# 先看帮助确认当前发行版的参数名不同版本前缀可能是 - 或 -- java -jar tools/jocky.jar --help # 执行混淆输入 jar、输出 jar、keep 列表、映射表、日志 java -jar tools/jocky.jar \ --in build/classes.jar \ --out build/classes-obf.jar \ --keep config/keep.txt \ --mapping build/mapping.txt \ --log build/jocky.log \ --level 5命令行的好处是退出码可判。混淆失败通常返回非 0CI 里set -e或者链式判断就能拦住不至于把半成品打进去。日志里重点看两类行skip开头的说明某个类被跳过keep开头的说明白名单命中。如果关键类出现在 skip 列表里说明输入路径不对或者类没编译进去。参数名以--help输出为准不要照抄别处的脚本版本之间改过名字是很常见的事。4. 混淆配置的参数表keep 规则、级别与输出4.1 keep 白名单怎么写才不漏keep 文件是 jocky 配置里最需要认真对待的部分写少了运行时崩溃写多了混淆等于没做。它的格式比 ProGuard 简单通常是逐行声明粒度到类全名或包前缀不支持复杂的成员签名通配。下面是我常用的一个骨架# 安卓组件系统按类名反射实例化必须保留 com.example.app.MainActivity com.example.app.SyncService com.example.app.BootReceiver # 自定义 View布局 XML 里按类名引用 com.example.app.widget.TrendChart # 反射入口Class.forName / getMethod 用到的类 com.example.model.User com.example.model.Order # JNI 入口类名和方法名决定 C 侧符号 com.example.jni.NativeBridge # 第三方 SDK内部有反射整包保留 com.thirdparty.sdk.必须进白名单的清单我一般按这几类过一遍四大组件和 Application自定义 ViewSerializable / Parcelable 的模型类所有Class.forName、getDeclaredMethod出现的位置native 方法所在类注解处理器生成的类第三方 SDK 的包前缀。最后一项最容易被漏很多统计、推送 SDK 内部用类名做 key混淆后初始化直接崩。提示keep 文件要进版本库和代码一起评审。我见过有人本地改完忘了提交打包机拉下来是旧规则问题复现不了查了一下午。4.2 混淆级别与字符串加密开关参数不多但每个都影响结果列成表更清楚参数作用建议起步值inputJar输入 class 或 jar干净的全量编译产物outputJar输出路径单独目录不与输入重叠keepFile白名单版本化管理mappingFile名字映射表每次构建归档logFile过程日志保留最近若干次level混淆强度先 5稳定后加到 7 或 8encryptString字符串加密先 false跑通后开removeLog删日志调用发布包开调试包关level的含义是改名激进程度数字越大包名压平越彻底、成员名越短。不要一上来拉满先低级别跑通全流程再逐级往上加每加一级跑一遍冒烟用例。这样出问题时能定位到是哪一级引入的。encryptString单独说一句开了之后反编译看到的字符串是密文但解密逻辑本身也在字节码里专业逆向还是能还原。它的价值是提高门槛不是绝对安全。另外加密后的字符串在 log 里打印出来是密文调试时别被这个吓到。4.3 mapping 文件与日志出问题时的黑匣子mapping 文件记录的是“原名 → 混淆名”的完整对照线上崩溃堆栈还原全靠它。归档策略很简单每次构建生成一份按版本号或构建号命名跟 APK 一起存别覆盖。丢了 mapping 的混淆包堆栈基本等于天书只能靠猜。日志文件主要看三件事哪些类被 keep 命中、哪些类被 skip、有没有“找不到引用”的警告。skip 列表里出现业务类说明输入路径或者编译产物有问题警告里出现“referenced class not found”说明依赖没进输入这类警告攒多了混淆结果可能不完整。还原堆栈的做法不复杂把崩溃日志里的短类名和方法名按 mapping 逐条替换回原名再对着源码看行号。工具不是必需的mapping 格式就是纯文本写个脚本做字符串替换也能凑合关键是 mapping 得在。5. 避坑记录五类最容易翻车的场景5.1 反射调用被改名ClassNotFoundException 与 NoSuchMethodException现象混淆包启动就崩堆栈里是ClassNotFoundException: com.example.model.User或者NoSuchMethodException: init()。奇怪的是同一份代码不混淆跑得好好的。原因反射里的类名和方法名是字符串常量jocky 改的是符号引用不会去解析字符串内容。类被改成a.b.c之后Class.forName(com.example.model.User)还在找原名自然找不到。方法同理getMethod(init)找的是原方法名改名后对不上。解决把反射目标类整体加进 keep类名和成员名都保留如果反射点很多、keep 列表爆炸就在混淆后做一轮字符串替换把代码里的原类名映射成混淆名这需要读 mapping 文件更彻底的做法是把反射改成接口调用从根上消除字符串依赖。我一般先 keep等稳定了再逐步把热点反射改掉。5.2 注解与序列化字段JSON 解析出来全是 null现象接口返回的数据解析出来字段全是 null或者 Parcelable 反序列化后字段丢失。日志里没有异常就是数据不对。原因Gson、Jackson 这类框架默认按字段名匹配 JSON key字段被改成a、b之后user_name匹配不上任何字段静默赋 null。Parcelable 稍微好一点它按写入顺序读改名不影响但如果模型类里混用了反射赋值同样会出问题。解决模型类整体 keep字段名不动或者给每个字段加SerializedName(user_name)显式指定 key这样字段名可以混淆注解值不变。后一种做法混淆强度更高但要确认混淆过程不会把注解抹掉第一次用的时候拿一个类做验证。Parcelable 的类建议直接 keep省事。5.3 JNI 与 native 方法UnsatisfiedLinkError 找上门现象调用 native 方法时报UnsatisfiedLinkError: No implementation found for native ...附带的符号名是一串短名。原因静态注册的 JNI 靠命名约定绑定C 侧符号是Java_包名_类名_方法名Java 侧的类名或方法名一改符号对不上动态库加载得到但方法找不到。包名被压平之后这个符号更是面目全非。解决把 JNI 类和所有 native 方法加进 keep类名、方法名、包名都不能动。如果不想 keep就改成动态注册在JNI_OnLoad里用RegisterNatives手动绑定方法签名这样 Java 侧类名可以混淆但签名表要跟着维护改方法参数时两边都得动维护成本不低。5.4 增量构建与 class 版本本地能跑打包机上报 VerifyError现象本地 Eclipse 里导出正常打包机上报VerifyError或ClassFormatError指向某个看起来毫无问题的类。原因有两个。一是增量编译Eclipse 只重编改动过的文件bin/classes里混着新旧 classjocky 拿到不完整的类图做改名决策继承链对不上就产出非法字节码。二是 class 文件版本高版本 JDK 编出来的 class 带invokedynamic等新指令老版本 jocky 依赖的字节码库不认处理时直接写坏。解决混淆前强制 clean全量编译输入目录只放这一次构建的产物编译目标版本降到 jocky 能处理的级别Java 8 的 lambda 如果报错就改成匿名内部类。打包脚本里加一步校验混淆后跑一次 dxdx 能过基本说明字节码没坏。5.5 与第三方库的冲突某个 SDK 初始化就崩现象接入的某个统计或推送 SDK 初始化崩溃堆栈指向它内部的类报错信息含糊看不出所以然。原因这类 SDK 内部大量使用反射用类名做 key、用字段名拼请求参数甚至有自己的类加载逻辑。整包混淆之后内部约定全断。解决把 SDK 的包前缀整包 keep别心疼那点混淆强度翻 SDK 文档看有没有官方 keep 规则有就照抄如果文档缺失就逐个类试从入口类开始 keep崩了再加下一个。还有一种情况是 SDK 依赖的公共库被混淆导致冲突这种只能把公共库也 keep 掉。6. 验证与收尾混淆后怎么确认没改坏混淆这件事配完只是开始验证才是收尾。我习惯先做静态抽查再跑冒烟用例两步都过才算完成。静态抽查看三样东西包名有没有压平、成员名有没有变形、字符串还是不是明文。用unzip列出 jar 内容再拿javap看一个类的字节码# 列出混淆后的 jar确认包路径已经变短 unzip -l build/classes-obf.jar | head -n 30 # 抽查某个类的成员名和常量池类名按上一步的实际输出替换 javap -p -c -classpath build/classes-obf.jar a.b.c | head -n 60第一段输出里如果还能看到com/example/app这种完整路径说明包名压平没生效检查level是不是太低。第二段输出重点看方法名是不是短名、常量池里的字符串是不是密文如果字符串仍是明文要么encryptString没开要么这个类被 keep 了。冒烟用例不用多覆盖三条链路就够登录走网络和序列化、列表加载走反射和模型解析、一个回调入口走系统反射实例化。跑完抓 logcat 里的堆栈拿 mapping 还原确认崩溃点落在业务代码而不是混淆引入的怪问题。我还会对比混淆前后的方法数方法数暴涨通常意味着某处生成了大量合成方法得回头查。从那以后我每次接老工程的混淆都先把 keep 文件当代码一样评审一遍再跑这三条链路最后把 mapping 按版本归档。这套流程不新鲜但能省掉大部分半夜被叫起来看堆栈的机会。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站