1. 项目概述:为什么Harmony的so文件值得一探?

最近在技术社区里,关于Harmony的讨论热度一直不减。作为一个从Android逆向转过来折腾了一段时间的开发者,我发现一个挺有意思的现象:大家聊Harmony应用开发、聊ArkTS、聊方舟编译器头头是道,但一涉及到底层的、编译后的产物分析,比如那个关键的 .so 动态库文件,讨论的声音就少了很多,资料也相对零散。这恰恰说明了这个领域的价值——它是一片尚未被过度开发的“蓝海”。逆向分析Harmony的so文件,绝不仅仅是把Android NDK那套经验照搬过来那么简单。它背后关联着Harmony独特的应用模型、全新的ArkTS/ArkUI框架、以及方舟编译器带来的全新ABI(应用程序二进制接口)和字节码体系。搞清楚这些,无论是做安全审计、性能优化、还是竞品分析,都能让你比别人看得更深一层。

简单来说,这个“初探”项目,就是带你绕过表面的API调用,直接深入到Harmony应用最核心的“发动机舱”——那些用C/C++或其它原生语言编写,最终被编译成 .so 文件的本地库。我们会一起看看这些库在Harmony里长什么样,怎么找到它们,用什么工具打开它们,以及如何理解里面那些看似天书的机器指令和符号信息。这个过程,就像拿到一个精密设备的黑盒子,我们要想办法在不破坏它的前提下,搞清楚里面的电路板和运行逻辑。对于应用安全研究员、底层性能调优工程师,或者单纯对系统原理有极强好奇心的开发者来说,掌握这套方法,无疑是打开了一扇通往Harmony系统更深层次的大门。

2. 核心思路与工具选型:从Android到Harmony的思维转换

刚开始接触Harmony逆向时,最容易犯的错误就是带着完全的“Android NDK思维”往里冲。你会下意识地去寻找 libc++_shared.so ,去用 readelf 看动态节,用 objdump 反汇编,这没错,但这些只是基础操作。Harmony带来的新变化,要求我们的分析思路必须进行一次升级。

2.1 Harmony应用模型下的so文件定位

在Android里,so文件通常乖乖地待在APK的 lib/<abi> 目录下。但在Harmony应用(特别是基于Stage模型的应用)的HAP包中,原生库的存放规则有了变化。一个HAP包解压后,你会在根目录下发现一个 libs 目录,里面按照CPU架构(如 arm64-v8a , armeabi-v7a )进一步划分,so文件就存放在这里。这一点和Android类似,但关键区别在于 模块化 。一个复杂的Harmony应用可能由多个HAP包组成(例如一个主Entry HAP和多个Feature HAP),每个HAP都可以携带自己独立的原生库。这意味着,分析时你必须先搞清楚目标功能或漏洞点可能位于哪个HAP模块中,而不是想当然地在主包里寻找。

另一个核心变化是 依赖关系 。Harmony系统提供了自己的一套原生API,这些接口也通过so库的形式暴露给开发者,例如与ArkUI渲染引擎、分布式能力、安全等子系统交互的库。在逆向分析一个第三方so时,它很可能动态链接了这些系统级的so。因此,你的分析环境(无论是真机还是模拟器)必须具备相应的系统库文件,否则在解析导入符号时会遇到大量“未定义”的困扰。这要求我们不仅要分析目标so,还要对其运行环境——即特定版本的Harmony系统镜像——有基本的了解。

2.2 工具链的“守正”与“出奇”

工欲善其事,必先利其器。对于so文件逆向,工具链可以分为静态分析和动态分析两大类,我们的“初探”以静态为主,动态为辅。

静态分析“三板斧”:

  1. 基础信息侦查: file readelf objdump 。这是永远不会过时的经典组合。 file 命令快速确认文件类型和架构; readelf -d 查看动态段,获取其依赖的库( NEEDED )和导出的符号(如果存在); objdump -d 进行反汇编。在Linux或配置了相关工具链的Windows(如WSL、MSYS2)上都能直接使用。对于Harmony的so,要特别注意 readelf 输出的 SONAME NEEDED 字段,这里可能藏着Harmony特有库的线索。
  2. 高级反汇编与伪代码:IDA Pro/Ghidra 。这是逆向工程师的“主战场”。IDA Pro以其强大的反汇编引擎和交互性著称,而Ghidra作为NSA开源的工具,在反编译和代码分析上表现不俗,且免费。将它们用于Harmony so分析时,首要任务是确保反编译器能正确识别指令集架构(ARM64是主流)。一个常见的坑是,由于方舟编译器的优化策略,生成的指令流可能有一些特殊模式,导致反编译器的某些分析算法(如函数识别、栈帧分析)出现偏差,这时需要手动进行校正。
  3. 符号恢复与增强: nm strings 、Frida 。如果so文件保留了符号表(非Strip), nm -D 可以列出动态符号,这是理解库功能的金钥匙。但发布版本通常会被Strip(去除符号)。这时, strings 命令可以提取文件中的所有可打印字符串,函数名、日志标签、硬编码的密钥或URL都可能在这里暴露。更进一步,可以结合Frida进行动态挂钩,在运行时拦截对特定原生函数的调用,观察其参数和返回值,从而动态地“恢复”函数的功能语义。

动态调试环境搭建思路: 静态分析遇到瓶颈时,必须动起来。Harmony应用的原生代码调试目前不如Android成熟,但仍有路径可循。一种思路是使用Harmony模拟器或支持开发者模式的真机,通过 gdbserver 附加到目标进程进行调试。你需要从Harmony的SDK或设备系统中获取对应架构的 gdbserver ,并推送到设备上。这个过程涉及端口转发、符号文件加载等步骤,比Android更繁琐,但对分析复杂逻辑至关重要。

注意:逆向分析工作务必在合法合规的前提下进行,仅针对自己拥有所有权或已获得明确授权的软件,用于安全研究、学习或调试。任何对他人软件未经授权的逆向分析都可能涉及法律风险。

3. 实操流程:拆解一个Harmony so文件的完整过程

理论说得再多,不如亲手拆一个。假设我们现在拿到了一个名为 libbusiness_core.z.so 的Harmony动态库文件,接下来我会一步步展示如何把它“庖丁解牛”。

3.1 第一步:前期侦查与信息收集

首先,我们建立一个专门的工作目录,把目标so文件放进去。打开终端,开始信息收集。

# 1. 确认文件基本信息
file libbusiness_core.z.so
# 期望输出类似:libbusiness_core.z.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, stripped
# 这告诉我们它是ARM64架构的动态链接库,并且被剥离了符号(stripped),难度增加了。

# 2. 查看ELF头信息和动态段
readelf -h libbusiness_core.z.so # 查看ELF头
readelf -d libbusiness_core.z.so | head -20 # 查看动态段,重点关注NEEDED

readelf -d 的输出至关重要。你可能会看到类似下面的信息:

Dynamic section at offset 0x2e000 contains 30 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libc.so]
 0x0000000000000001 (NEEDED)             Shared library: [libm.so]
 0x0000000000000001 (NEEDED)             Shared library: [libhilog.so]
 0x0000000000000001 (NEEDED)             Shared library: [libace_napi.z.so]
 ...

这里, libhilog.so libace_napi.z.so 就是非常明显的Harmony系统库依赖。 libhilog 是Harmony的日志系统, libace_napi 是NAPI(Native API)的实现库,用于ArkTS/JS与C++的交互。看到这些,你就能立刻意识到,这个so很可能通过NAPI向ArkTS层暴露了接口。

# 3. 尝试提取字符串信息
strings libbusiness_core.z.so | grep -i "error\|fail\|key\|secret\|http" | head -30
strings libbusiness_core.z.so | grep -E "napi_.*|OH_.*" | head -30 # 查找Harmony NAPI或系统相关字符串

第一个 strings 命令帮助寻找可能的错误信息、硬编码密钥或网络地址。第二个命令则专门针对Harmony生态, napi_ 开头的函数通常是NAPI接口, OH_ 开头可能是Harmony定义的一些宏或标识。这些字符串能为我们后续在反汇编器中定位关键代码区域提供“路标”。

3.2 第二步:使用IDA Pro进行静态深度分析

将so文件拖入IDA Pro(这里以IDA 7.7为例)。加载时,IDA会识别出ARM64架构,并开始自动分析。

  1. 初始导航与函数识别 :由于文件被Strip, Functions 窗口里看到的都是 sub_XXXXX 这样的匿名函数。我们的第一个目标是找到入口点或已知的调用点。通常,NAPI模块的注册函数是一个很好的起点,它的函数名可能丢失,但其功能模式固定:它会调用 napi_create_object napi_set_property napi_define_properties 等系列函数来向JS环境导出模块。
  2. 寻找NAPI模块注册 :在IDA的 Strings 窗口(快捷键 Shift+F12 ),搜索之前 strings 命令找到的疑似模块名,比如 businessCore 。双击跳转到字符串引用位置,再通过交叉引用(快捷键 X )找到哪些函数使用了这个字符串。通常,在初始化函数中,这个字符串会作为 napi_module 结构体的一个字段。找到这个结构体,就能顺藤摸瓜找到模块初始化函数。
  3. 分析关键业务函数 :假设我们通过字符串引用找到了一个函数 sub_12345 ,它内部调用了 napi_get_cb_info (用于从JS参数中获取信息)和一系列加密函数(如 AES_encrypt )。这时,我们需要:
    • 重命名函数 :在IDA中,按 N 键将 sub_12345 重命名为有意义的名称,如 js_encryptData
    • 分析参数和逻辑 :通过 napi_get_cb_info 的用法推断JS传入的参数个数和类型。然后,逐步分析后续的加密流程:密钥从哪里来(是硬编码、从参数传入、还是从文件读取)?使用的加密算法和模式是什么(AES-128-CBC?)?结果如何返回给JS?
    • 注释与结构体定义 :对重要的变量、缓冲区、结构体使用注释(快捷键 : )。如果识别出自定义的数据结构,可以在 Structures 窗口中定义新的结构体,然后应用到相应的内存地址上,这能极大提升代码的可读性。
  4. 处理Harmony特有API :在分析过程中,你会遇到很多来自 libace_napi.z.so libhilog.so 的导入函数。IDA可能无法自动解析它们的参数。这时,你需要查阅Harmony的官方Native API文档(尽管开放有限),或者根据函数名和上下文进行合理推测。例如,以 hilog 开头的函数显然是写日志的,可以据此推断其参数大致为日志级别、标签、内容。

3.3 第三步:动态验证与Frida脚本辅助

静态分析得出的结论需要动态验证。我们编写一个简单的Frida脚本,来挂钩我们推测出的加密函数。

// frida_hook_businesscore.js
Java.perform(function() {
    // 首先,确保能进入Native层上下文
    Interceptor.attach(Module.findExportByName("libbusiness_core.z.so", "napi_get_cb_info"), {
        onEnter: function(args) {
            console.log("[+] napi_get_cb_info called. 可以在此处查看JS传入的参数环境。");
        }
    });

    // 假设我们通过静态分析,推测加密函数在偏移地址 0x12345
    var baseAddr = Module.findBaseAddress("libbusiness_core.z.so");
    if (baseAddr) {
        var encryptFuncAddr = baseAddr.add(0x12345);
        Interceptor.attach(encryptFuncAddr, {
            onEnter: function(args) {
                console.log("[+] 疑似加密函数被调用!");
                // 打印前三个参数(假设,具体需根据反汇编调整)
                console.log("arg[0]: " + args[0]);
                console.log("arg[1]: " + args[1] + " -> " + Memory.readUtf8String(args[1]));
                console.log("arg[2]: " + args[2]);
            },
            onLeave: function(retval) {
                console.log("[+] 函数返回: " + retval);
            }
        });
    } else {
        console.log("[-] 未找到 libbusiness_core.z.so 模块。");
    }
});

使用Frida将脚本注入到目标Harmony应用中:

frida -U -f com.example.ohosapp -l frida_hook_businesscore.js --no-pause

当应用执行到相关Native代码时,我们就能在控制台看到打印的信息。通过对比静态分析的参数推断和动态打印的实际值,可以验证我们的分析是否正确,并可能发现更多细节。

4. 难点解析与避坑指南:Harmony逆向特有的“雷区”

走过一遍流程,你会发现和Android NDK逆向相比,Harmony so分析有几个特别容易踩坑的地方。

4.1 符号缺失与模糊的函数边界

Strip过的so是常态。面对满屏的 sub_XXXX ,策略如下:

  • 以字符串和交叉引用为锚点 :不要漫无目的地看汇编。始终从有意义的字符串(错误信息、日志标签、常量字符串)出发,利用交叉引用定位到使用它们的函数。这个函数很可能就是你的突破口。
  • 识别标准库和系统调用模式 :熟悉ARM64的调用约定(AAPCS64),了解常见标准库函数(如 memcpy , strlen , malloc )的汇编特征。更重要的是,熟悉Harmony NAPI函数的调用模式。例如, napi_create_string_utf8 的调用前后,通常伴随着对 napi_value 变量的操作。识别出这些模式,就能在匿名代码中划出功能边界。
  • 利用Frida动态生成符号 :对于复杂的so,可以先用Frida的 Stalker 功能或类似 frida-trace 工具,大规模追踪函数调用,记录下调用频繁的函数地址。虽然不能直接恢复原名,但你可以根据调用关系图,为这些地址赋予临时但有意义的名字(如 do_network_request , parse_config ),并在IDA中同步更新,逐步还原代码逻辑图。

4.2 Harmony NAPI与系统依赖的复杂性

NAPI是ArkTS与C++交互的桥梁,但其复杂性高于Android的JNI。

  • napi_value 的不透明性 :所有JS值在Native层都被封装为 napi_value 。分析时,需要跟踪 napi_value 的生命周期和类型转换。看到一个 napi_value 类型的参数,要向前查找它是通过 napi_create_* 系列函数创建的,还是从 napi_get_cb_info 中获取的入参。
  • 异步回调与Promise :Harmony NAPI支持异步工作线程和Promise。代码中可能出现 napi_create_async_work napi_create_promise 等函数。分析这类代码时,必须理清初始化、执行体、完成回调这三部分逻辑分别在哪个函数里,它们之间如何通过 async_work deferred 对象关联。这块逻辑如果理不清,很容易跟丢数据流。
  • 系统库版本差异 :不同版本的Harmony SDK,其系统so(如 libace_napi.z.so )的导出函数可能有增减。你在一个版本上分析得到的函数偏移地址或函数签名,在另一个版本上可能不适用。因此,记录分析所对应的Harmony版本号至关重要。最好能获取到对应版本的SDK中的头文件(如果可能),这对理解导入函数的功能有巨大帮助。

4.3 分析工具链的适配问题

  • IDA/Ghidra的处理器模块 :确保你使用的反汇编工具支持ARM v8-A (AArch64)指令集,并且是最新的版本。旧版本可能无法正确解析某些ARM64扩展指令。
  • 调试符号与源映射 :对于Harmony系统自身的so(如 libace_napi.z.so ),理论上华为可能提供带调试符号的版本给合作伙伴,但普通开发者极难获取。这意味着逆向系统库本身几乎就是纯汇编分析,难度极大。我们的策略应该是 理解其接口契约(即函数功能)而非内部实现 。通过官方文档、头文件片段、以及动态调试时的行为来推断其作用。
  • 动态调试环境不稳定 :Harmony设备的root权限和调试接口不如Android开放。模拟器可能是更稳定的动态分析环境。确保你使用的Harmony模拟器镜像版本与目标so编译所针对的API版本尽可能一致,以减少因系统库不匹配导致的崩溃或异常行为。

5. 从分析到实践:安全审计与性能优化的视角

逆向分析本身不是目的,它应该服务于更高的目标。以我们分析的 libbusiness_core.z.so 为例,假设它负责核心的数据加密和网络通信。

从安全审计角度:

  1. 密钥管理 :加密密钥是硬编码在字符串常量里,还是从相对安全的存储(如系统密钥库)中获取?在反汇编代码中搜索 AES_set_encrypt_key RSA_public_encrypt 等函数的参数,追踪密钥来源。
  2. 算法与模式 :使用的是否是过时或不安全的算法(如DES、ECB模式)?通过识别加密函数常量(如AES的S盒、RSA的模数)或动态调试输入输出,可以判断算法和模式。
  3. 输入验证 :NAPI接口函数是否对来自JS层的输入参数进行了充分的长度、类型、范围检查?寻找 memcpy strcpy 等危险函数,检查其源缓冲区长度是否受控,是否存在缓冲区溢出的风险。
  4. 日志泄露 hilog 输出的日志是否包含敏感信息(如令牌、手机号)?通过 strings 或反汇编查找日志输出点,评估信息泄露风险。

从性能优化角度:

  1. 计算密集型函数 :通过静态分析识别出复杂的加密、图像处理或数据序列化函数。结合动态采样(如简单的Frida计时钩子),评估其耗时。
  2. 内存操作 :是否存在大量的、不必要的内存拷贝?分析循环中的 memcpy / memmove 操作,看是否能用指针操作或优化算法来减少拷贝。
  3. 系统调用频率 :是否在紧密循环中频繁调用 hilog 打印调试日志?即使日志级别很高,函数调用本身也有开销。在性能关键路径上,应避免或减少日志输出。
  4. 异步使用是否合理 :对于本应使用 napi_create_async_work 移到工作线程的耗时操作,检查其是否仍在主线程(即Node.js的Event Loop线程)上执行,这可能会阻塞JS响应。

逆向分析如同一场侦探游戏,每一个so文件都是一个待破解的谜题。Harmony的独特架构为这个游戏增加了一些新规则和新道具。这次“初探”仅仅揭开了帷幕的一角,重点在于建立从Android逆向到Harmony逆向的思维转换地图,并掌握一套基础但完整的信息收集、静态分析和动态验证的方法论。真正的精通,源于对一个个具体so文件的反复剖析,在不断的“猜测-验证-修正”循环中,积累对Harmony Native层代码模式的直觉。当你能够独立理清一个复杂NAPI模块的完整调用链,并准确指出其潜在的安全或性能隐患时,你就已经穿越了初探的迷雾,进入了Harmony底层世界的更深处。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐