HarmonyOS 6.1 安全攻防实战:从“裸奔”到“防弹”的代码级防护

📅 2026/7/24 9:20:38 👁️ 阅读次数 📝 编程学习
HarmonyOS 6.1 安全攻防实战:从“裸奔”到“防弹”的代码级防护

系列安全加固篇·第48篇。AI融合篇后,有安全专家问:“Demo功能这么全,但代码有没有被反编译的风险?用户数据会不会被中间人劫持?界面会不会被恶意APP覆盖钓鱼?” 安全是应用的底线。今天我们将电商Demo进行一次彻底的安全“体检”和“加固”,覆盖代码混淆、防逆向、数据加密、防界面劫持、证书锁定、Root检测等关键领域。我们将使用DevEco Studio安全插件华为安全检测服务,模拟黑客攻击并修复漏洞。全程基于API23,含官方文档未涉及的“Native层防护”和“运行时自检”技巧。

一、前言:为什么“功能完整”不等于“可以上线”?

很多开发者认为“只要功能跑通了,加个HTTPS就是安全”。现实是残酷的:

  • 静态分析:黑客用jadx反编译你的HAP包,秒读你的API Key和加密逻辑。

  • 动态调试:用fridahook你的登录函数,绕过密码验证。

  • 中间人攻击:用Charles抓包,看光你的所有网络请求。

  • 界面劫持:恶意APP覆盖你的支付界面,诱导用户输入密码。

安全原则:永远不要相信客户端环境。今天的实战,我们将假设客户端运行在“敌占区”,通过层层防御,即使代码被反编译,数据被拦截,也能将损失降到最低。

二、核心概念辨析(常见安全误区)

误区

真相

正确做法

HTTPS=安全

HTTPS只加密传输,不防客户端破解

代码混淆+数据二次加密

前端验证=安全

前端验证可被绕过

关键逻辑必须后端验证

硬编码密钥=方便

硬编码密钥等于没加密

使用KeyStore/Enclave存储

日志调试=高效

日志可能泄露敏感信息

发布版移除所有调试日志

权限越多=功能越强

过多权限增加攻击面

最小权限原则

三、代码实现:从“裸奔”到“防弹”

3.1 代码混淆与字符串加密

防止反编译后直接看到明文逻辑。

步骤1:开启混淆

build-profile.json5中配置:

{ "buildOption": { "arkOptions": { "obfuscation": { "enable": true, // 开启混淆 "rules": "./obfuscation-rules.txt" // 混淆规则文件 } } } }

步骤2:编写混淆规则 (obfuscation-rules.txt)

# 保留ArkUI组件,否则界面会白屏 -keep class * extends ohos.arkui.UIComponent { *; } -keep class * extends ohos.arkui.Component { *; } # 保留@Entry注解的类 -keep @ohos.arkui.annotation.Entry class * { *; } # 保留Native方法 -keepclasseswithmembernames class * { native <methods>; } # 混淆所有本地变量名和方法名 -obfuscate_names -obfuscate_methods -obfuscate_fields # 移除日志(关键!) -assumenosideeffects class ohos.hiviewdfx.HiLog { public static *** d(...); public static *** i(...); public static *** w(...); }

步骤3:字符串加密(自定义脚本)

创建scripts/encrypt_strings.js,在构建前加密敏感字符串:

// 简单的异或加密示例 const crypto = require('crypto'); function xorEncrypt(text, key) { let result = ''; for (let i = 0; i < text.length; i++) { result += String.fromCharCode(text.charCodeAt(i) ^ key.charCodeAt(i % key.length)); } return Buffer.from(result).toString('base64'); } // 加密API Key const apiKey = 'your_real_api_key_here'; const encryptedKey = xorEncrypt(apiKey, 'secret_key'); console.log(`Encrypted API Key: ${encryptedKey}`); // 在ETS代码中,使用解密函数 // const decryptedKey = xorDecrypt(encryptedKey, 'secret_key');

3.2 数据安全:多层加密与KeyStore

错误示范

// 明文存储密码 preferences.putSync('password', '123456');

正确做法

import { cryptoFramework } from '@kit.CryptoArchitectureKit' import { huks } from '@kit.UniversalKeyStoreKit' export class SecureStorage { private static readonly KEY_ALIAS = 'user_password_key' private static readonly TRANSFORMATION = 'AES/GCM/NoPadding' /** * 生成并存储AES密钥到HUKS(硬件安全环境) */ async generateAndStoreKey(): Promise<void> { const keyProperties = { alg: huks.HuksKeyAlg.HUKS_ALG_AES, size: huks.HuksKeySize.HUKS_AES_KEY_SIZE_256, purpose: huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_ENCRYPT | huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_DECRYPT, digest: huks.HuksKeyDigest.HUKS_DIGEST_SHA256, padding: huks.HuksKeyPadding.HUKS_PADDING_NONE, blockMode: huks.HuksKeyMode.HUKS_MODE_GCM } const options = { properties: [keyProperties] } try { await huks.generateKey(SecureStorage.KEY_ALIAS, options) console.log('密钥已安全存储在HUKS中') } catch (err) { console.error('密钥生成失败:', err) } } /** * 加密数据 */ async encrypt(data: string): Promise<string> { const cipher = cryptoFramework.createCipher(SecureStorage.TRANSFORMATION) const key = await this.getKey() const iv = cryptoFramework.createRandomIv(12) // GCM模式需要12字节IV await cipher.init(cryptoFramework.CryptoMode.ENCRYPT_MODE, key, iv) const inputData = { data: new Uint8Array(new TextEncoder().encode(data)) } const encryptedData = await cipher.doFinal(inputData) // 将IV和密文一起存储 const result = new Uint8Array(iv.data.length + encryptedData.data.length) result.set(iv.data, 0) result.set(encryptedData.data, iv.data.length) return Buffer.from(result).toString('base64') } /** * 解密数据 */ async decrypt(base64Data: string): Promise<string> { const data = Buffer.from(base64Data, 'base64') const iv = data.slice(0, 12) const encrypted = data.slice(12) const cipher = cryptoFramework.createCipher(SecureStorage.TRANSFORMATION) const key = await this.getKey() const ivParam = { data: new Uint8Array(iv) } await cipher.init(cryptoFramework.CryptoMode.DECRYPT_MODE, key, ivParam) const encryptedParam = { data: new Uint8Array(encrypted) } const decryptedData = await cipher.doFinal(encryptedParam) return new TextDecoder().decode(decryptedData.data) } /** * 从HUKS获取密钥 */ private async getKey(): Promise<cryptoFramework.SymKey> { const keyBlob = await huks.getKey(SecureStorage.KEY_ALIAS, { properties: [] }) return cryptoFramework.convertKey(keyBlob) } }

3.3 网络安全:证书锁定与双向认证

防止中间人攻击(MitM)。

步骤1:证书锁定(Certificate Pinning)

module.json5中配置:

{ "module": { "metadata": [ { "name": "network-security-config", "resource": "$profile:network_security_config" } ] } }

创建resources/base/profile/network_security_config.xml

<?xml version="1.0" encoding="utf-8"?> <network-security-config> <domain-config cleartextTrafficPermitted="false"> <domain includeSubdomains="true">api.yourdomain.com</domain> <pin-set expiration="2024-12-31"> <!-- 你的域名证书公钥哈希(Base64编码) --> <pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin> <!-- 备份证书公钥哈希 --> <pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin> </pin-set> <!-- 强制使用证书锁定 --> <trust-anchors> <certificates src="@raw/your_certificate"/> </trust-anchors> </domain-config> </network-security-config>

步骤2:双向SSL认证

import { http } from '@kit.NetworkKit' import { cryptoCert } from '@kit.CryptoArchitectureKit' async function secureRequest(): Promise<void> { const httpRequest = http.createHttp() // 加载客户端证书(P12文件) const clientCert = await fileIo.readFile(getContext().filesDir + '/client.p12') const certBlob: cryptoCert.EncodingBlob = { data: new Uint8Array(clientCert), encoding: cryptoCert.EncodingFormat.FORMAT_PKCS12 } // 配置双向认证 const options: http.HttpRequestOptions = { method: http.RequestMethod.POST, header: { 'Content-Type': 'application/json' }, caCert: '-----BEGIN CERTIFICATE-----\n...服务器证书...\n-----END CERTIFICATE-----', clientCert: certBlob, clientCertPassword: 'cert_password', // 证书密码 sslVerifyMode: http.SslVerifyMode.VERIFY_PEER | http.SslVerifyMode.VERIFY_HOSTNAME } try { const response = await httpRequest.request('https://api.yourdomain.com/secure-endpoint', options) console.log('安全请求成功:', response.result) } catch (err) { console.error('安全请求失败:', err) // 证书验证失败,可能是中间人攻击 promptAction.showToast({ message: '网络连接不安全,请检查网络环境' }) } }

3.4 运行时防护:防调试、防Root、防注入

检测调试器

import { hidebug } from '@kit.PerformanceAnalysisKit' export class RuntimeProtection { /** * 检测是否被调试 */ static isBeingDebugged(): boolean { try { // hidebug.isDebuggerConnected() 返回是否连接调试器 return hidebug.isDebuggerConnected() } catch (err) { console.error('检测调试器失败:', err) return false } } /** * 检测设备是否已Root/Jailbreak */ static isDeviceRooted(): Promise<boolean> { return new Promise((resolve) => { // 检查常见Root路径 const rootPaths = [ '/system/bin/su', '/system/xbin/su', '/sbin/su', '/vendor/bin/su', '/system/app/Superuser.apk' ] let foundRoot = false rootPaths.forEach(path => { try { fileIo.accessSync(path) foundRoot = true } catch (e) { // 文件不存在 } }) // 检查是否可以写入系统目录 try { const testFile = '/system/test_rooted' fileIo.writeSync(fileIo.openSync(testFile, fileIo.OpenMode.WRITE_ONLY), new Uint8Array([1])) fileIo.unlinkSync(testFile) foundRoot = true } catch (e) { // 无法写入 } resolve(foundRoot) }) } /** * 检测是否被Frida等工具注入 */ static isInjected(): boolean { // 检查/proc/self/maps中是否存在frida相关模块 try { const maps = fileIo.readTextSync('/proc/self/maps') if (maps.includes('frida') || maps.includes('gadget')) { return true } } catch (err) { // 无法读取maps文件 } return false } /** * 运行时自检 */ static async selfCheck(): Promise<boolean> { console.log('开始运行时自检...') if (this.isBeingDebugged()) { console.error('检测到调试器连接!') return false } if (await this.isDeviceRooted()) { console.warn('设备已Root,安全性降低') // 可以选择降低功能级别,而非直接退出 } if (this.isInjected()) { console.error('检测到代码注入!') return false } console.log('运行时自检通过') return true } }

3.5 防界面劫持(Overlay Attack)

import { window } from '@kit.ArkUI' import { abilityAccessCtrl } from '@kit.AbilityKit' @Entry @Component struct SecurePaymentPage { private windowClass: window.Window | null = null aboutToAppear(): void { this.protectAgainstOverlay() } /** * 防止界面被覆盖 */ async protectAgainstOverlay(): Promise<void> { try { // 1. 获取窗口实例 this.windowClass = await window.getLastWindow(getContext(this)) // 2. 设置窗口属性,防止被其他应用覆盖 // 注意:HarmonyOS没有直接等同于Android SYSTEM_ALERT_WINDOW的权限 // 但可以通过以下方式增强安全性: // 设置窗口为全屏,减少被覆盖的可能性 this.windowClass.setWindowLayoutFullScreen(true) // 设置窗口焦点变化监听 this.windowClass.on('windowEvent', (data) => { if (data === window.WindowEventType.WINDOW_EVENT_FOCUS_OUT) { // 窗口失去焦点,可能被其他应用覆盖 this.onWindowFocusLost() } }) // 3. 检查悬浮窗权限(如果应用使用了悬浮窗) const atManager = abilityAccessCtrl.createAtManager() const result = await atManager.checkAccessToken( abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED, 'ohos.permission.SYSTEM_FLOAT_WINDOW' ) if (result !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) { console.log('未授予悬浮窗权限,降低被覆盖风险') } } catch (err) { console.error('防覆盖设置失败:', err) } } onWindowFocusLost(): void { // 窗口失去焦点时的处理逻辑 console.warn('支付页面失去焦点,可能存在安全风险') promptAction.showToast({ message: '警告:页面失去焦点,请确认环境安全' }) // 可以选择暂时模糊页面内容 this.blurContent() } blurContent(): void { // 实现页面模糊效果 console.log('模糊页面内容以保护隐私') } build() { Column() { Text('安全支付页面') .fontSize(24) .margin(20) Text('请确认当前页面为官方应用,警惕仿冒界面') .fontSize(14) .fontColor('#FF0000') .margin(10) // ... 支付表单 } .width('100%') .height('100%') .backgroundColor('#FFFFFF') } }

四、踩坑记录(官方文档没写的安全细节)

  1. 混淆导致的“灵异bug”:混淆后,ArkUI的@State变量名可能被改变,导致UI不更新。解决方案:在混淆规则中保留所有@State@Prop@Link等装饰器修饰的变量:-keepclassmembers class * { @ohos.arkui.State *; }

  2. HUKS的性能陷阱:每次加密解密都从HUKS获取密钥会有性能开销。解决方案:在应用启动时获取一次密钥,缓存在内存中(注意内存清理);或使用密钥派生函数(KDF)从主密钥派生会话密钥。

  3. 证书锁定的维护噩梦:证书过期或更换会导致应用无法联网。解决方案:在pin-set中配置备份证书;实现证书轮换机制,在旧证书过期前,通过云端下发新证书指纹。

  4. Root检测的误报:某些安全软件或系统更新可能导致Root检测误报。解决方案:不要仅依赖单一检测方法,结合多种检测手段;检测到Root时,采取降级处理(如禁用敏感功能),而非直接崩溃。

  5. Native层防护的必要性:JS代码容易被反编译,核心安全逻辑(如签名验证、密钥派生)应放在Native层(C/C++)实现,并通过NAPI调用。虽然Native代码也能被逆向,但难度呈指数级上升。