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(): Promisevoid { 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): Promisestring { 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): Promisestring { 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(): PromisecryptoFramework.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 version1.0 encodingutf-8? network-security-config domain-config cleartextTrafficPermittedfalse domain includeSubdomainstrueapi.yourdomain.com/domain pin-set expiration2024-12-31 !-- 你的域名证书公钥哈希Base64编码 -- pin digestSHA-256AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/pin !-- 备份证书公钥哈希 -- pin digestSHA-256BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB/pin /pin-set !-- 强制使用证书锁定 -- trust-anchors certificates srcraw/your_certificate/ /trust-anchors /domain-config /network-security-config步骤2双向SSL认证import { http } from kit.NetworkKit import { cryptoCert } from kit.CryptoArchitectureKit async function secureRequest(): Promisevoid { 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(): Promiseboolean { 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(): Promiseboolean { 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 Attackimport { 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(): Promisevoid { 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) } }四、踩坑记录官方文档没写的安全细节混淆导致的“灵异bug”混淆后ArkUI的State变量名可能被改变导致UI不更新。解决方案在混淆规则中保留所有State、Prop、Link等装饰器修饰的变量-keepclassmembers class * { ohos.arkui.State *; }。HUKS的性能陷阱每次加密解密都从HUKS获取密钥会有性能开销。解决方案在应用启动时获取一次密钥缓存在内存中注意内存清理或使用密钥派生函数KDF从主密钥派生会话密钥。证书锁定的维护噩梦证书过期或更换会导致应用无法联网。解决方案在pin-set中配置备份证书实现证书轮换机制在旧证书过期前通过云端下发新证书指纹。Root检测的误报某些安全软件或系统更新可能导致Root检测误报。解决方案不要仅依赖单一检测方法结合多种检测手段检测到Root时采取降级处理如禁用敏感功能而非直接崩溃。Native层防护的必要性JS代码容易被反编译核心安全逻辑如签名验证、密钥派生应放在Native层C/C实现并通过NAPI调用。虽然Native代码也能被逆向但难度呈指数级上升。