HarmonyOS开发:代码混淆——Release混淆

举报
Jack20 发表于 2026/06/25 20:38:43 2026/06/25
【摘要】 HarmonyOS开发:代码混淆——Release混淆📌 核心要点:ArkTS代码混淆是Release构建的关键步骤,通过重命名标识符、打乱控制流、加密字符串来保护核心逻辑,但混淆规则写不好,线上直接崩给你看。 背景与动机你写了一个核心算法,花了三个月调优,性能比竞品好30%。结果上线一周,竞品就出了同样的功能——你的算法被逆向了。HarmonyOS的ArkTS代码编译成ABC字节码后,...

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反解工具

混淆后的崩溃堆栈,变量名全是abc,根本看不懂。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开了。

解法:排查步骤:

  1. 确认崩溃是否只在Release包出现
  2. 如果是,大概率是混淆导致的
  3. 开启SourceMap(-enable-source-map),反解崩溃堆栈
  4. 找到混淆后的名称,在-keep-*中保留
  5. 重新打包验证

坑5:SourceMap泄露导致混淆白做

SourceMap文件包含混淆前后的完整映射,泄露了等于混淆白做。

解法

  • Release发布包不要包含SourceMap文件
  • SourceMap只在开发/调试时使用,存放在安全位置
  • CI构建时SourceMap上传到错误监控平台(如Sentry),不在产物中保留
  • .gitignore中排除SourceMap文件

HarmonyOS 6适配说明

HarmonyOS 6在代码混淆方面有以下改进:

  1. 控制流混淆增强:HarmonyOS 6新增控制流平坦化(Control Flow Flattening)能力,将代码的控制流结构打乱,增加逆向难度。在obfuscation-rules.txt中启用:
-enable-control-flow-obfuscation
  1. 字符串加密:新增字符串常量加密能力,代码中的字符串常量在编译时加密,运行时解密。防止通过搜索字符串定位关键逻辑:
-enable-string-property-obfuscation
  1. 增量混淆:hvigor 5.0支持增量混淆,只对变化的代码重新混淆,构建速度大幅提升。

  2. 混淆规则冲突检测:构建工具会自动检测模块混淆规则和依赖库consumer-rules的冲突,给出明确的警告和建议。

  3. SourceMap格式升级:SourceMap格式升级到v4,支持更精确的行列映射。反解后的堆栈信息更准确,定位问题更容易。

总结

代码混淆是Release构建的标配,但"开了混淆"不等于"安全了"。混淆只是增加逆向难度,不是绝对防护。

核心原则:

  • 保留什么比混淆什么更重要——保留少了崩,保留多了等于没混淆
  • 序列化属性必须保留——这是最高频的混淆崩溃原因
  • SourceMap是救命稻草——出了问题能反解,没SourceMap你只能猜
  • 混淆规则要持续维护——每次加新功能,检查是否需要更新保留规则
维度 评价
学习难度 ⭐⭐⭐⭐ 规则语法简单,但"保留什么"需要经验判断
使用频率 ⭐⭐⭐⭐⭐ 每次Release构建都涉及
重要程度 ⭐⭐⭐⭐ 代码保护的基础手段,不做混淆等于裸奔
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。