首页
社区
课程
招聘
[原创] 某招聘 App 爱加密抽取壳的内存脱壳与请求签名逆向
发表于: 11小时前 212

[原创] 某招聘 App 爱加密抽取壳的内存脱壳与请求签名逆向

11小时前
212

目标是逆向某招聘 App(包名 base64:Y29tLmpvYi5hbmRyb2lk)的网络请求签名 sign

该样本使用了爱加密企业版加固,核心业务方法采用了指令抽取保护,反编译后方法体均为 nop 桩。跟进分析发现,爱加密采用运行期惰性恢复机制:只有被调用到的方法,壳才会将对应的 code_item 回填至内存。由于发起网络请求必然触发签名计算,直接通过 /proc/pid/mem 转储内存即可获取已解密的签名逻辑。

本文记录加固特征分析、/proc/pid/mem 内存脱壳、签名算法还原与 Python 离线复现的过程;后半部分针对未执行代码,记录基于 ArtMethod 级主动调用的全量脱壳方案,以及排查和解决爱加密动态 patch libart.so 引发崩溃的对抗细节。

入口 Application 为 s.h.e.l.l.S,具备典型的爱加密加固特征。解压 APK 后检查 assets 与 lib 目录,关键文件分布如下:

图片描述

该加固方案结合了指令抽取与 VMP:大部分业务方法指令被抽取,少量逻辑由 libijm-emulator.so 解释执行,加密后的业务 DEX 存放在 ijiami.dat 中,由壳在运行时动态解密并映射加载。未脱壳前使用 jadx 查看原始 base APK 仅有 143 个类,主要为 ARouter 路由表。

壳的指令回填采用按需惰性策略:方法首次执行时才由壳回填 code_item,未执行的方法在内存中保持 nop 占位。以网络拦截器 jobs.android.retrofitnetwork.BasicParamsInterceptor 为例,在未触发网络请求时,jadx 反编译结果全为 return null,并提示 Invalid debug info offset

图片描述

由于网络请求触发前必然执行签名计算,签名相关的类与方法在内存中已被解密还原。因此无需提前全量脱壳,直接转储运行时内存即可获取有效字节码。

常规动态注入手段在此场景下受限,Frida 在 attach 或 spawn 阶段均会被 libzxprotect.so 拦截终止:

由于进程内注入面临反调试对抗,直接利用 root 权限读取 /proc/pid/mem 是一种无侵入方案。该方法不涉及目标进程内的代码执行与线程创建,且内核接口允许直接读取 VMA 权限被设置为不可读(如 -wxp)的 DEX 内存页。

转储流程如下:首先解析 /proc/pid/maps,筛选出名为 [anon:dalvik-DEX data] 的内存段,在各段起始地址检索 DEX 魔数 dex\n035\0。DEX 头部前 0x70 字节中,用于重建文件的关键字段如下:

读取到 file_size 后,按该长度连续转储内存。对于体积较大的 DEX,其在 maps 中常跨越多个相邻的匿名内存映射区域。此时需按虚拟地址连续读取超出当前段大小的数据(相邻 VMA 在虚拟地址空间上连续),最后按 file_size 截断。

内存中转储的 DEX 镜像虽已包含回填的 code_item,但头部 checksumsignature 仍需修复。需按照 DEX 规范重新计算 Adler32 与 SHA-1 并回写,以确保 jadx / baksmali 正常反编译。

在提取过程中需注意地址计算问题:手机端 toybox shell 默认使用 32 位整型算术,而 64 位系统下 DEX 映射位于 40 位以上的高位地址空间,直接在 shell 脚本内执行 skip = dex_vaddr 算术运算会导致溢出为负值。稳妥的做法是将 maps 导出至 PC 端,计算好页对齐的偏移参数,再通过 dd 按页读取:

修复后共提取出 16 个有效 DEX 文件,包含 55,054 个类。
full_6f9d1b1000.dex 中定位到了签名核心类 EncryptAndSignUtilSignFor51。检查其 CodeItem:doEncryptOrSign 包含 439 条指令,getRequestBodyAfter 包含 202 条指令,getSignJsonDataFromMap 包含 52 条指令,hmacSha256 包含 42 条指令,指令流完整,未被抽空为 nop。签名相关逻辑已成功恢复。

检索常用密码学 API(MessageDigestMacCipher)的交叉引用,定位至核心签名类 com.jobs.network.digest.SignFor51
该类方法实现均为标准加密原语:hmacSha256 对应标准 HMAC-SHA256,getSHA256 对应 SHA-256,toHexString 使用 Formatter("%02x") 生成小写十六进制字符串。反编译代码完整可读:

图片描述

请求签名的装配逻辑位于拦截器 com.jobs.network.EncryptAndSignUtil。首先由 isNeedSign 判定目标 host 是否处于验签范围(涵盖 VAPI / IM / AppApiV3 / Young-Cupid 站点,且无 needSign 请求头或该头值为 true)。核心装配逻辑位于 getRequestBodyAfter(以 young/cupid 业务线为例):

签名计算公式为:
sign = HMAC-SHA256(perHostKey, urlAfterHost + gsonJson(params))
结果置于 HTTP 请求头的 sign 字段。其中 urlAfterHost 为 host 之后的完整 URI。需要注意拦截器的执行次序:CommonParamInterceptor 会先行将公共 query 参数(partnerguiduuidclientidapiversion 等)追加至 URL 中,后续执行的 doEncryptOrSign 对截取后的完整字符串进行签名,因此该截取段包含 query 查询串,而非仅有 path。

参数序列化与签名的关键约束如下:
getSignJsonDataFromMap 遍历参数 Map(实际传入类型为 LinkedHashMap,保证键值插入顺序),对各 value 执行 String.valueOf(v) 转换为字符串存入 JsonObject,最终调用 toString() 输出无冗余空格的 JSON 串。签名拼接的 message 尾部内容与实际发送的 POST 请求体来源于同一次序列化结果,二者需保持逐字节一致。任何键序、类型转换(如数字与字符串格式)或空白字符差异均会导致服务端校验失败。请求头中的 Client-TimeCommonHeaderInterceptor 单独生成,取 GMT+8 时区整点秒级时间戳(分、秒、毫秒归零),不参与 cupid 站点的 HMAC 计算

getRequestBodyAfter 中还存在两套分支逻辑:非 young/cupid 站点调用 SignFor51.getRequestBody,计算公式为 signData = MD5(SHA256(clientTime + jsonData + signKey));旧版 appapi 站点调用 CQEncrypt.encrypt(formData, true),body 格式为 application/x-www-form-urlencoded。数据流向梳理如下:

图片描述

密钥按 host 维度进行配置。枚举类 EncryptAndSignUtil$SignKey<clinit> 中定义了明文密钥常量:

getSignKeyForHost(url) 依据请求 host 选取对应的 key,部分站点结合 query 参数做二级匹配:

例如首页职位列表请求发往 cupid.51job.com,匹配的密钥即为 SIGN_KEY_51JOB

静态内存转储仅能获取已在运行期触发的方法。针对未被执行、仍为 nop 占位的冷门业务逻辑,需借助进程内 ArtMethod 级主动调用机制实现全量解密。
其原理是在运行时遍历已加载 DexFileClassDef,解析所有 direct 与 virtual 方法对应的 ArtMethod 指针,主动调用 ArtMethod::GetCodeItem()。由于爱加密在首次解析方法 code_item 时完成回填,主动遍历调用可促使壳执行解密流程,随后顺着 CodeItem* 指针将解密后的字节码转储落盘。

测试基于修改版 Vector 框架实现。Vector 基于 Zygisk 并在 ART 层提供 hook 能力。方案中将 HookInline 底层接入 stealth-poc 的内核无痕 hook 引擎(shpte KPM):利用内核缺页异常机制将目标 libart 函数重定向至克隆的 ghost 内存页执行,避开用户态内存校验与 maps 扫描,未开启时回退至 Dobby。脱壳逻辑位于 native/src/unpack,通过系统属性进行调度:dexfind 负责通过 ART VisitClasses 枚举类,trigger 负责对各方法调用 GetCodeItem 强制触发解密并输出数据。

初次注入测试时,App 在启动 1 秒内发生崩溃。抓取的 tombstone 堆栈信息如下:

图片描述

崩溃表现为主线程向 libart.so.text 代码段执行写操作时触发 SEGV_ACCERR。崩溃现场 pc 位于匿名可执行段,x18 寄存器指向爱加密的 libexec.sox27 = 0xd503233f(AArch64 下的 PACIASP 指令),x1 = 0x1000(4KB 内存页大小)。
分析原因为:爱加密壳在启动初期会通过 mmap 创建私有内存空间,逐页覆盖原始 libart 函数序言(包含 PAC 保护指令)以建立其私有 hook 机制。若脱壳 worker 线程在此阶段并发 attach 至 ART 并解析符号,将与壳的内存覆盖逻辑产生写冲突并引发异常。对照实验表明:关闭脱壳开关(unpack=0)时 App 正常运行;开启后一旦 worker 执行 AttachCurrentThread,主线程即触发崩溃。

检查 libexec.so 导入表可以印证上述行为:虽然敏感字符串在静态分析中已加密,但其导入的系统 API 组合明确展示了其运行时行为特征——mmapmprotectmemcpy 用于修改代码段属性并回写指令,dlopendl_iterate_phdr 用于模块定位,ptracekillfork 负责反调试检测,sigsetjmpsiglongjmp 用于异常处理:

图片描述

定位冲突原因后,对 Vector 进行了针对性调整:


冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

最后于 11小时前 被星野安全编辑 ,原因:
收藏
免费 15
打赏
分享
最新回复 (3)
雪    币: 6
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
感谢分享
7小时前
0
雪    币: 1560
活跃值: (5703)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
3
555
6小时前
0
雪    币: 222
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
感谢分享
5小时前
0
游客
登录 | 注册 方可回帖
返回