HarmonyOS开发:代码混淆——Release混淆
HarmonyOS开发:代码混淆——Release混淆
📌 核心要点:ArkTS代码混淆是Release构建的关键步骤,通过重命名标识符、打乱控制流、加密字符串来保护核心逻辑,但混淆规则写不好,线上直接崩给你看。
背景与动机
你写了一个核心算法,花了三个月调优,性能比竞品好30%。结果上线一周,竞品就出了同样的功能——你的算法被逆向了。
HarmonyOS的ArkTS代码编译成ABC字节码后,虽然比JavaScript更难读,但依然可以被反编译。变量名、函数名、字符串常量,这些信息在ABC里都还保留着。有经验的人,花点时间就能还原你的逻辑。
代码混淆就是把这些可读的信息变得不可读。变量名从calculateScore变成a,函数调用链被打乱,字符串常量被加密——逆向的难度直接翻了好几倍。
但混淆不是"开了就行"。混淆规则写错了,该保留的没保留,运行时直接报"function not found"然后崩溃。而且这种崩溃只在Release包上出现,Debug包一切正常——因为Debug没开混淆。
这篇文章就讲清楚:混淆怎么配、规则怎么写、哪些必须保留、出了问题怎么排查。
核心原理
ArkTS混淆能做什么?
HarmonyOS的ArkTS混淆支持四种能力:
flowchart TD
A[ArkTS代码混淆] --> B[标识符混淆]
A --> C[控制流混淆]
A --> D[字符串加密]
A --> E[文件名混淆]
B --> B1["变量名 → a, b, c"]
B --> B2["函数名 → x1, x2"]
B --> B3["属性名 → p1, p2"]
B --> B4["类名 → C1, C2"]
C --> C1[打乱if-else分支]
C --> C2[插入无效代码块]
C --> C3[展平switch-case]
D --> D1["'hello' → decrypt(x)"]
D --> D2["API地址加密存储"]
E --> E1["utils/Logger.ets → a/b.ets"]
classDef main fill:#4A90D9,stroke:#2C5F8A,color:#fff
classDef id fill:#FF6B6B,stroke:#CC5555,color:#fff
classDef ctrl fill:#F39C12,stroke:#D68910,color:#fff
classDef str fill:#2ECC71,stroke:#25A55A,color:#fff
classDef file fill:#9B59B6,stroke:#8E44AD,color:#fff
class A main
class B,B1,B2,B3,B4 id
class C,C1,C2,C3 ctrl
class D,D1,D2 str
class E,E1 file
混淆前后对比
混淆前:
// 清晰可读
export class PaymentManager {
private apiKey: string = 'sk_live_abc123';
async processPayment(amount: number, orderId: string): Promise<boolean> {
const signature = this.generateSignature(amount, orderId);
const response = await this.sendRequest(signature);
return response.success;
}
private generateSignature(amount: number, orderId: string): string {
return `${amount}:${orderId}:${this.apiKey}`;
}
}
混淆后(示意):
// 完全不可读
export class a {
private b: string = _0x3f2a('0x1a');
async c(d: number, e: string): Promise<boolean> {
const f = this.g(d, e);
const h = await this.i(f);
return h.j;
}
private g(k: number, l: string): string {
return `${k}:${l}:${this.b}`;
}
}
混淆配置体系
HarmonyOS的混淆配置通过obfuscation-rules.txt文件管理:
entry/
├── obfuscation-rules.txt # 模块级混淆规则
└── consumer-rules.txt # 库的消费者规则(HAR/HSP发布时使用)
混淆规则优先级:模块级规则 > 依赖库的consumer-rules > 默认规则。
代码实战
基础用法:开启混淆与基本配置
第一步:在构建配置中开启混淆
// build-profile.json5
{
"app": {
"products": [
{
"name": "default",
"signingConfig": "default"
},
{
"name": "release",
"signingConfig": "release",
"buildOption": {
"arkOptions": {
"obfuscation": {
"enable": true, // 开启混淆
"rulesFiles": ["./obfuscation-rules.txt"] // 规则文件
}
}
}
}
]
}
}
第二步:编写混淆规则
// entry/obfuscation-rules.txt
# ===== 开启混淆能力 =====
# 标识符混淆(变量名、函数名、属性名)
-enable-property-obfuscation
-enable-toplevel-obfuscation
-enable-filename-obfuscation
-enable-export-obfuscation
# ===== 保留规则(不混淆的内容)=====
# 保留Ability入口类名
-keep-file-name
EntryAbility
MainAbility
# 保留被反射调用的属性名
-keep-property-name
onCreate
onDestroy
onWindowStageCreate
onWindowStageDestroy
onForeground
onBackground
# 保留被动态引用的全局名称
-keep-global-name
Logger
NetworkUtil
StorageUtil
# 保留通过字符串引用的属性(重要!)
-keep-property-name
message
code
data
result
status
success
error
# 保留序列化/反序列化相关的属性
-keep-property-name
id
name
type
value
timestamp
version
第三步:验证混淆效果
# Release打包
hvigorw assembleHap --mode module -p module=entry@default -p product=release
# 解压HAP查看混淆后的代码
unzip entry-default-signed.hap -d hap_content
# 查看 ets/modules.abc(ABC字节码,需要专用工具反编译查看)
进阶用法:精细化的保留规则
混淆最关键的不是"怎么混淆",而是"保留什么"。保留太少,运行时崩溃;保留太多,等于没混淆。
场景1:保留序列化数据类的属性
网络请求返回的JSON数据,如果你的数据类属性被混淆了,反序列化就会失败:
// 混淆前:属性名和JSON字段一一对应
export class UserInfo {
userId: string = ''; // JSON: {"userId": "123"}
userName: string = ''; // JSON: {"userName": "张三"}
avatarUrl: string = ''; // JSON: {"avatarUrl": "https://..."}
}
// 混淆后:属性名变了,JSON反序列化失败
export class UserInfo {
a: string = ''; // JSON里没有字段叫"a"
b: string = ''; // JSON里没有字段叫"b"
c: string = ''; // JSON里没有字段叫"c"
}
解法:保留所有参与序列化的属性名:
// obfuscation-rules.txt
# 保留数据模型属性(参与JSON序列化的)
-keep-property-name
userId
userName
avatarUrl
orderId
amount
status
createTime
updateTime
token
refreshToken
场景2:保留被动态引用的名称
通过字符串拼接或[]操作符动态访问的属性,混淆后引用会断裂:
// 动态属性访问
function getProperty(obj: object, key: string): any {
return obj[key]; // key是运行时字符串,混淆后属性名变了
}
// 字符串拼接引用
const iconName = 'icon_' + type;
const icon = $r(`app.media.${iconName}`); // 资源名被混淆就找不到了
解法:保留所有被动态引用的属性名和资源名:
// obfuscation-rules.txt
# 保留动态引用的属性名
-keep-property-name
icon_home
icon_search
icon_profile
icon_settings
bg_main
bg_detail
# 保留通过[]访问的属性
-keep-property-name
length
push
pop
splice
forEach
map
filter
reduce
场景3:保留三方库的导出名称
三方库的导出名称被混淆后,import就找不到了:
// obfuscation-rules.txt
# 保留三方库的导出
-keep-global-name
Lottie
QRCode
Chart
WebView
# 保留HSP模块的导出
-keep-global-name
NetworkUtil
StorageUtil
DateUtil
RouterUtil
场景4:保留native方法名
调用原生C/C++方法的函数名不能混淆,否则JNI/Node-API找不到对应函数:
// obfuscation-rules.txt
# 保留native方法
-keep-property-name
nativeInit
nativeProcess
nativeRelease
完整示例:混淆规则模板与SourceMap反解
完整的混淆规则模板:
// entry/obfuscation-rules.txt
# ========================================
# HarmonyOS ArkTS 混淆规则模板
# ========================================
# ----- 混淆能力开关 -----
-enable-property-obfuscation # 属性名混淆
-enable-toplevel-obfuscation # 顶层名称混淆
-enable-filename-obfuscation # 文件名混淆
-enable-export-obfuscation # 导出名混淆
# ----- 保留Ability入口 -----
-keep-file-name
EntryAbility
MainAbility
PayAbility
LiveAbility
# ----- 保留生命周期方法 -----
-keep-property-name
onCreate
onDestroy
onWindowStageCreate
onWindowStageDestroy
onForeground
onBackground
onPageShow
onPageHide
onBackPress
# ----- 保留UI装饰器相关 -----
-keep-property-name
build
aboutToAppear
aboutToDisappear
onPageShow
onPageHide
# ----- 保留JSON序列化属性 -----
-keep-property-name
# 用户相关
userId
userName
avatarUrl
nickName
phone
email
# 订单相关
orderId
amount
status
createTime
updateTime
# 通用
code
message
data
result
success
error
token
id
name
type
value
# ----- 保留动态引用属性 -----
-keep-property-name
length
push
pop
shift
unshift
splice
slice
forEach
map
filter
reduce
find
indexOf
includes
toString
valueOf
# ----- 保留全局导出 -----
-keep-global-name
Logger
NetworkUtil
StorageUtil
DateUtil
RouterUtil
SecurityGuard
# ----- 保留native方法 -----
-keep-property-name
nativeInit
nativeProcess
# ----- SourceMap(调试用)-----
# 开启后生成SourceMap文件,用于反解混淆后的堆栈
# Release发布时可关闭以减小包体积
# -enable-source-map
SourceMap反解工具:
混淆后的崩溃堆栈,变量名全是a、b、c,根本看不懂。SourceMap就是"翻译字典",把混淆后的名字还原成原始名字。
// scripts/sourcemap-decoder.ets
// SourceMap反解工具
interface SourceMapEntry {
generatedLine: number;
generatedColumn: number;
originalFile: string;
originalLine: number;
originalColumn: number;
name: string;
}
// 解析SourceMap文件
function parseSourceMap(sourceMapPath: string): Map<string, string> {
const sourceMapContent = fs.readFileSync(sourceMapPath, 'utf-8');
const sourceMap = JSON.parse(sourceMapContent);
// 构建混淆名→原始名的映射
const nameMap = new Map<string, string>();
if (sourceMap.names && sourceMap.mappings) {
for (let i = 0; i < sourceMap.names.length; i++) {
const originalName = sourceMap.names[i];
// 从mappings中提取混淆后的名称
// 简化处理:直接使用names数组
nameMap.set(`name_${i}`, originalName);
}
}
return nameMap;
}
// 反解混淆后的堆栈
function decodeStackTrace(
stackTrace: string,
sourceMapPath: string
): string {
const nameMap = parseSourceMap(sourceMapPath);
let decoded = stackTrace;
// 替换混淆后的名称
for (const [obfuscated, original] of nameMap) {
decoded = decoded.replace(
new RegExp(`\\b${escapeRegex(obfuscated)}\\b`, 'g'),
original
);
}
return decoded;
}
// 反解混淆后的错误日志
function decodeErrorLog(
errorLog: string,
sourceMapDir: string
): string {
// 查找对应的SourceMap文件
const sourceMapFiles = fs.readdirSync(sourceMapDir)
.filter(f => f.endsWith('.map'));
let decoded = errorLog;
for (const mapFile of sourceMapFiles) {
const nameMap = parseSourceMap(path.join(sourceMapDir, mapFile));
for (const [obfuscated, original] of nameMap) {
decoded = decoded.replace(
new RegExp(`\\b${escapeRegex(obfuscated)}\\b`, 'g'),
original
);
}
}
return decoded;
}
// 辅助:转义正则特殊字符
function escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// 使用示例
const obfuscatedStack = `
Error: Cannot read property 'a' of undefined
at c (entry/ets/pages/Index.ets:10:15)
at b (entry/ets/utils/NetworkUtil.ets:25:8)
at a (entry/ets/entryability/EntryAbility.ets:30:5)
`;
const decodedStack = decodeStackTrace(
obfuscatedStack,
'./entry/build/default/outputs/default/entry-default-sourceMap.map'
);
console.log('混淆堆栈:\n' + obfuscatedStack);
console.log('反解堆栈:\n' + decodedStack);
命令行反解工具:
# 使用hvigor提供的SourceMap反解命令
hvigorw decodeStack --source-map entry/build/default/outputs/default/sourceMaps.map --stack "混淆后的堆栈字符串"
# 或者使用DevEco Studio的"Analyze Stack Trace"功能
# 直接在IDE中粘贴混淆堆栈,自动反解
踩坑与注意事项
坑1:混淆后JSON反序列化失败
这是最高频的混淆问题。数据类的属性名被混淆了,JSON字段名和属性名对不上,反序列化结果全是undefined。
解法:所有参与JSON序列化的属性名,必须在-keep-property-name中保留。建议写代码时给数据类加注释标记,混淆规则统一维护:
// 标记参与序列化的属性
export class OrderInfo {
orderId: string = ''; // [serialize] 必须保留
amount: number = 0; // [serialize] 必须保留
status: string = ''; // [serialize] 必须保留
// 内部计算属性,不参与序列化,可以混淆
private _formattedAmount: string = '';
}
坑2:混淆后路由跳转失败
页面路由使用字符串指定Ability名或页面路径,混淆后名称变了,路由找不到:
// 混淆前
router.pushUrl({ url: 'pages/OrderDetail' });
// 混淆后,pages/OrderDetail 变成了 a/b
// 路由找不到 a/b,跳转失败
解法:在-keep-file-name中保留所有页面文件名和Ability名:
// obfuscation-rules.txt
-keep-file-name
Index
OrderDetail
PaymentPage
SettingsPage
EntryAbility
坑3:混淆后三方库报错
三方库的导出名被混淆了,import时找不到对应的模块:
// 混淆前
import { Lottie } from '@ohos/lottie';
// 混淆后,Lottie变成了某个随机名
// import找不到,运行时报错
解法:在-keep-global-name中保留三方库的导出名。更好的做法是,三方库的consumer-rules.txt中已经声明了需要保留的名称,你只需要确保模块的混淆规则文件中include了三方库的consumer-rules。
坑4:混淆后只在Release包崩溃
Debug包正常,Release包崩溃——这是混淆问题的典型特征。因为Debug没开混淆,Release开了。
解法:排查步骤:
- 确认崩溃是否只在Release包出现
- 如果是,大概率是混淆导致的
- 开启SourceMap(
-enable-source-map),反解崩溃堆栈 - 找到混淆后的名称,在
-keep-*中保留 - 重新打包验证
坑5:SourceMap泄露导致混淆白做
SourceMap文件包含混淆前后的完整映射,泄露了等于混淆白做。
解法:
- Release发布包不要包含SourceMap文件
- SourceMap只在开发/调试时使用,存放在安全位置
- CI构建时SourceMap上传到错误监控平台(如Sentry),不在产物中保留
.gitignore中排除SourceMap文件
HarmonyOS 6适配说明
HarmonyOS 6在代码混淆方面有以下改进:
- 控制流混淆增强:HarmonyOS 6新增控制流平坦化(Control Flow Flattening)能力,将代码的控制流结构打乱,增加逆向难度。在
obfuscation-rules.txt中启用:
-enable-control-flow-obfuscation
- 字符串加密:新增字符串常量加密能力,代码中的字符串常量在编译时加密,运行时解密。防止通过搜索字符串定位关键逻辑:
-enable-string-property-obfuscation
-
增量混淆:hvigor 5.0支持增量混淆,只对变化的代码重新混淆,构建速度大幅提升。
-
混淆规则冲突检测:构建工具会自动检测模块混淆规则和依赖库consumer-rules的冲突,给出明确的警告和建议。
-
SourceMap格式升级:SourceMap格式升级到v4,支持更精确的行列映射。反解后的堆栈信息更准确,定位问题更容易。
总结
代码混淆是Release构建的标配,但"开了混淆"不等于"安全了"。混淆只是增加逆向难度,不是绝对防护。
核心原则:
- 保留什么比混淆什么更重要——保留少了崩,保留多了等于没混淆
- 序列化属性必须保留——这是最高频的混淆崩溃原因
- SourceMap是救命稻草——出了问题能反解,没SourceMap你只能猜
- 混淆规则要持续维护——每次加新功能,检查是否需要更新保留规则
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐⭐⭐ 规则语法简单,但"保留什么"需要经验判断 |
| 使用频率 | ⭐⭐⭐⭐⭐ 每次Release构建都涉及 |
| 重要程度 | ⭐⭐⭐⭐ 代码保护的基础手段,不做混淆等于裸奔 |
- 点赞
- 收藏
- 关注作者
评论(0)