首页
社区
课程
招聘
[原创] 某网游加速器程序 UPX 加壳分析与手工脱壳
发表于: 2026-8-15 15:34 380

[原创] 某网游加速器程序 UPX 加壳分析与手工脱壳

2026-8-15 15:34
380

目标文件:C:\Program Files (x86)\Thunder Network\xlacc\...\lib\accservice.exe
大小 15.4 MB,PE32 Console,i386,带外部 PDB 符号
声明:本文仅用于安全研究与学习交流,请勿用于非法用途。

在分析该加速器程序 accservice.exe 时,发现其 file 命令识别结果为 UPX compressed。正常来说 UPX 的壳直接 upx -d 一把梭即可,但实际却并非如此 —— 标准 UPX 工具直接拒绝脱壳。经过逆向发现,这是一个魔改过的 UPX 变种:解压 stub 是标准 NRV2B,但头部字段被篡改、过滤器与重定位逻辑都被改动。

本文记录完整的逆向分析与手工脱壳过程,并附上可复用的脱壳脚本。

先用 fileobjdump 看一下基本面貌:

典型的 UPX 特征:节区名 UPX0/UPX1/UPX2,UPX0 文件偏移处 raw size = 0(全部运行时解压),整体熵约 7.93 bits/byte,接近随机。

upx -d 通过查找 UPX! magic 来校验头部,报错说明 magic 被移除或加密了。把 UPX! 手工补到 UPX1 起始处(文件偏移 0x400)再试:

错误从 NotPackedException 变成 header corrupted 4,说明 magic 校验已通过,但头部其余字段也被改过。对照 UPX 源码(src/packhead.cpp)的 decodePackHeaderFromBuf,header corrupted 4 对应 c_len < 2 || u_len < 2 等字段校验失败。标准工具这条路走不通,只能手工脱。

入口点 EP = 0x03727490(VA),位于 UPX1 段尾部。dump 出来反汇编:

一眼可辨:这是 UPX 的 NRV2B 解压器,位读取方式是经典的 add ebx,ebx / mov (%esi),%ebx / sub esi,-4 / adc ebx,ebx 结构。但继续往下读时发现两处与官方 stub 不同:

即偏移阈值 -0xd00 -> -0x500、附加常数 1 -> 2 都被改了。这说明压缩流是用同参数魔改的压缩器生成的,标准 UCL/NRV2B 解码器无法直接解。必须照着这个 stub 逐指令实现。

stub 的 getbit 语义(理解这点是解码成败的关键):

也就是说每次装载 4 字节后,最低位补了个"哨兵 1",下一次取出的位是新 dword 的 bit31。翻译成 Python:

对照反汇编把匹配长度/偏移的解码逻辑逐指令转写(把 CF/ZF 的传播显式写出来):

这里有一个重要的魔改点:官方 stub 在路径 A 计算完偏移后是 getbit 1; adc ecx,ecx 再继续,而这个 stub 直接 jmp 0x752e,用 sar 移出的最低位当作控制位(CF)。因此解码时必须严格复刻这一行为,否则位流对不上。

解压起点:stub 中 mov esi, 0x02c7b015 指向 UPX1+0x15,换算成文件偏移就是 0x400 + 0x15 = 0x415

解压后拿到的镜像代码里,E8/E9 调用指令的目标被以绝对地址大端存储了(官方 cto 过滤器还会加 addvalue 和 cto8 标记字节,这里都去掉了)。stub 里的反变换循环:

翻译成 Python(遍历 filter_length = 0x53bfc 个调用点):

处理了 343,036 个调用后,入口点代码从 e9 00 00 01 5c(目标被替换到"空旷地址空间")还原成正常的 jmp 0x401160

stub 在反过滤之后执行导入处理:

即:记录区在 dest+0x36be000,DLL 名字基址在 dest+0x3727000 = 0x3b28000(正好是 UPX2 节 VA,名字字符串存在里面)。记录格式为:

解析脚本(节选):

恢复出完整导入表:

其中 nfapi.dll 正是程序目录中的网络过滤库,印证了该服务的作用。

按标准 PE 布局重建 IMAGE_IMPORT_DESCRIPTOR + IAT + Hint/Name 表,写入 .idata 节:

小坑:Python 的 bytearray 切片返回的是 bytearray 而非 bytes,isinstance(f, bytes) 为 False,会导致全部被误判成按序号导入(0x800000xx)。需要显式 bytes(...) 转换。

重定位数据紧随导入记录之后(找 null 记录 dllname_off==0,再 +4)。解压算法与官方 UPX 相同,用 (byte & 0x0f)<<16 | le16 做扩展偏移:

每个重定位点的回写是 bswap(value) + esi,即指针以大端存储、减去基址,运行时加回基址还原成绝对地址。

最初我按官方 stub 语义 lea -4(%esi),%ebx 处理,结果入口点代码被"修坏"了:

而把基准改为 esi-3 后,入口点还原成完全合理的代码:

且防篡改代码中的 MZ 校验也还原成功:

cmp [0xf0ffff] 被修复成 cmp [0x400000](检查镜像基址处 MZ 签名),语义完全说得通。因此该魔改版的 delta 编码基准是 esi-3自动化脱壳时,可以对候选基准逐一尝试,选取"修复后指针落在镜像内"比例最高的一组:

最终 395,510 个重定位点,基准 -3 时 395,440 个修复值落在镜像内(命中率 99.98%),可靠收敛。

解压镜像末尾是 extra_info(skip = le32(img[-4:])),内含完整原始 PE 头(PE\0\0 + COFF + 可选头 + 节表)。按以下步骤写出文件:

最终验证:

导入表、入口点代码、重定位块全部验证通过,与手工推演结果字节级一致(52 MB)。

本次逆向对象是一种 UPX 变种,魔改点共四处:

关键经验:

完整脱壳脚本已整理为 unpack_upx.py(纯 Python 标准库),自动提取 stub 常量、解压、反过滤、恢复导入/重定位并重建 PE,已在本样本上验证通过。

DLL 函数数 代表函数
KERNEL32.DLL 59 AddVectoredExceptionHandler, CreateThread...
fwpuclnt.dll 7 FwpmEngineOpen0, FwpmFilterEnum0...
msvcrt.dll 41 __getmainargs, _beginthread...
nfapi.dll 15 nf_getPoolStatus, nf_setIPEventHandler...
ole32.dll 3 CoCreateInstance, CoInitialize...
WS2_32.dll 2 inet_ntop, inet_pton
WSOCK32.DLL 3 htons, inet_addr, ntohs

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

最后于 2026-8-15 20:16 被cendart编辑 ,原因: 去掉目标程序商业名称
上传的附件:
收藏
免费 8
打赏
分享
最新回复 (4)
雪    币: 4
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
6666
2026-8-15 15:41
0
雪    币: 206
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
good
2026-8-22 02:48
0
雪    币: 214
活跃值: (805)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
888
2026-8-23 14:07
0
雪    币: 63
活跃值: (923)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
1
2026-8-23 21:18
0
游客
登录 | 注册 方可回帖
返回