首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
加壳脱壳
发新帖
0
11
[原创] 某网游加速器程序 UPX 加壳分析与手工脱壳
发表于: 2026-8-15 15:34
928
[原创] 某网游加速器程序 UPX 加壳分析与手工脱壳
cendart
2026-8-15 15:34
928
# 【原创】某网游加速器程序 UPX 加壳分析与手工脱壳 > 目标文件:`C:\Program Files (x86)\Thunder Network\xlacc\...\lib\accservice.exe` > 大小 15.4 MB,PE32 Console,i386,带外部 PDB 符号 > 声明:本文仅用于安全研究与学习交流,请勿用于非法用途。 ## 0x0 前言 在分析该加速器程序 `accservice.exe` 时,发现其 `file` 命令识别结果为 `UPX compressed`。正常来说 UPX 的壳直接 `upx -d` 一把梭即可,但实际却并非如此 —— 标准 UPX 工具直接拒绝脱壳。经过逆向发现,这是一个**魔改过的 UPX 变种**:解压 stub 是标准 NRV2B,但头部字段被篡改、过滤器与重定位逻辑都被改动。 本文记录完整的逆向分析与手工脱壳过程,并附上可复用的脱壳脚本。 ## 0x1 加壳识别 ### 1.1 节区与熵 先用 `file` 和 `objdump` 看一下基本面貌: ```bash $ file accservice.exe PE32 executable (console) Intel 80386 (stripped to external PDB), for MS Windows, UPX compressed $ objdump -h accservice.exe | grep -E "UPX|rsrc" 0 UPX0 0287a000 00401000 00401000 00000400 1 UPX1 00eac800 02c7b000 02c7b000 00000400 2 UPX2 00000200 03b28000 03b28000 00eacc00 3 .rsrc 000001a8 03b29000 03b29000 00eace00 ``` 典型的 UPX 特征:节区名 `UPX0/UPX1/UPX2`,`UPX0` 文件偏移处 raw size = 0(全部运行时解压),整体熵约 7.93 bits/byte,接近随机。 ### 1.2 标准 upx -d 失败 ```bash $ upx -d accservice.exe upx: accservice.exe: NotPackedException: not packed by UPX ``` `upx -d` 通过查找 `UPX!` magic 来校验头部,报错说明 magic 被移除或加密了。把 `UPX!` 手工补到 `UPX1` 起始处(文件偏移 0x400)再试: ```bash $ upx -t -f acc_magic.exe upx: acc_magic.exe: CantUnpackException: header corrupted 4 ``` 错误从 `NotPackedException` 变成 `header corrupted 4`,说明 magic 校验已通过,但**头部其余字段也被改过**。对照 UPX 源码(`src/packhead.cpp`)的 `decodePackHeaderFromBuf`,`header corrupted 4` 对应 `c_len < 2 || u_len < 2` 等字段校验失败。标准工具这条路走不通,只能手工脱。 ## 0x2 定位解压 stub 入口点 `EP = 0x03727490`(VA),位于 `UPX1` 段尾部。dump 出来反汇编: ```nasm 03727490: 60 pushad 03727491: be 15 b0 c7 02 mov esi, 0x02c7b015 ; 压缩流 VA 03727496: 8d be eb 5f 78 fd lea edi, [esi-0x287a015] ; dest = 0x401000 0372749c: 8d 87 5c ec 5e 03 lea eax, [edi+0x35eec5c] 037274a2: ff 30 push (%eax) 037274a4: c7 00 d0 00 1e 1e movl $0x1e1e00d0,(%eax) ; 标记写入 037274aa: 50 push %eax 037274ab: 57 push %edi 037274ac: 83 cd ff or ebp,-1 037274af: eb 11 jmp +0x11 037274c2: 8b 1e mov (%esi),%ebx ; getbit: 装载 4 字节 037274c4: 83 ee fc sub $0xfffffffc,%esi ; esi += 4 037274c7: 11 db adc %ebx,%ebx 037274c9: 72 ed jb -0x13 ; 1 -> 字面量 037274cb: b8 01 00 00 00 mov $0x1,%eax 037274d0: 01 db add %ebx,%ebx ; getbit(1) ... ``` 一眼可辨:这是 UPX 的 **NRV2B 解压器**,位读取方式是经典的 `add ebx,ebx / mov (%esi),%ebx / sub esi,-4 / adc ebx,ebx` 结构。但继续往下读时发现两处与官方 stub 不同: ```nasm 0372755d: 81 fd 00 fb ff ff cmp $0xfffffb00,%ebp ; 官方是 -0xd00 03727563: 83 d1 02 adc $0x2,%ecx ; 官方是 adc 1 ``` 即偏移阈值 `-0xd00 -> -0x500`、附加常数 `1 -> 2` 都被改了。**这说明压缩流是用同参数魔改的压缩器生成的,标准 UCL/NRV2B 解码器无法直接解**。必须照着这个 stub 逐指令实现。 ## 0x3 手写 NRV2B 解压器 ### 3.1 getbit 位读取器 stub 的 getbit 语义(理解这点是解码成败的关键): - `add ebx,ebx` 左移 1 位,返回原 bit31; - 当移位后 `ebx == 0`(ZF=1)时重新装载 4 字节,注意 `sub esi,-4` 恒产生 borrow,所以 `adc ebx,ebx` 实际是 `ebx = (dword<<1) | 1`; 也就是说每次装载 4 字节后,最低位补了个"哨兵 1",下一次取出的位是**新 dword 的 bit31**。翻译成 Python: ```python def getbit(): nonlocal ebx, si bit = (ebx >> 31) & 1 ebx = (ebx << 1) & 0xffffffff if ebx == 0: # ZF -> 重新装载 d = le32(src, si); si += 4 ebx = ((d << 1) | 1) & 0xffffffff bit = (d >> 31) & 1 # 返回新 dword 的 bit31 return bit ``` ### 3.2 解压主循环 对照反汇编把匹配长度/偏移的解码逻辑逐指令转写(把 CF/ZF 的传播显式写出来): ```python eax = 1 while True: eax = ((eax << 1) | getbit()) & M # adc eax,eax if getbit(): # jae -> break break eax = (eax - 1) & M # dec eax eax = ((eax << 1) | getbit()) & M ecx = 0 old, eax = eax, (eax - 3) & M if old < 3: # jb: 偏移沿用上一次 CF = getbit() else: # 大偏移: 读 1 字节后取反 eax = ((eax << 8) & M) | src[si]; si += 1 eax ^= 0xffffffff if eax == 0: # EOF break ebp = eax >> 1 | (0x80000000 if eax & 0x80000000 else 0) # sar CF = eax & 1 # 魔改点: CF 来自 sar # 0x752e 处的长度解码 + 复制(略), 见完整脚本 ``` 这里有一个**重要的魔改点**:官方 stub 在路径 A 计算完偏移后是 `getbit 1; adc ecx,ecx` 再继续,而这个 stub 直接 `jmp 0x752e`,用 `sar` 移出的最低位当作控制位(CF)。因此解码时必须严格复刻这一行为,否则位流对不上。 解压起点:stub 中 `mov esi, 0x02c7b015` 指向 `UPX1+0x15`,换算成文件偏移就是 `0x400 + 0x15 = 0x415`。 ```python img, si_end = nrv2b_decompress(packed, 0x415, 0xfffffb00, 2) # 输出 57,824,366 字节 (~55 MB) ``` ## 0x4 call8 过滤器反变换 解压后拿到的镜像代码里,`E8/E9` 调用指令的目标被**以绝对地址大端存储**了(官方 cto 过滤器还会加 addvalue 和 cto8 标记字节,这里都去掉了)。stub 里的反变换循环: ```nasm b9 fc 3b 05 00 mov ecx, 0x53bfc ; 调用个数 8a 07 mov al,[edi] 47 inc edi 2c e8 sub al,0xe8 3c 01 cmp al,1 77 f7 ja 扫描下一个 ; 非 E8/E9 跳过 8b 07 mov eax,[edi] ; 读 4 字节(目标地址) ... bswap eax ... 29 f8 sub eax,edi ; 减去当前地址 01 f0 add eax,esi ; 加上基址 => 还原相对偏移 89 07 mov [edi],eax 83 c7 05 add edi,5 e2 e0 loop ``` 翻译成 Python(遍历 `filter_length = 0x53bfc` 个调用点): ```python def unfilter_cto(buf, filter_length, dest_va): out = bytearray(buf) ecx, edi, n = filter_length, 0, 0 while ecx > 0 and edi + 6 < len(out): if out[edi] in (0xE8, 0xE9): v = (out[edi+1]<<24)|(out[edi+2]<<16)|(out[edi+3]<<8)|out[edi+4] rel = (v - (edi + 1)) & 0xffffffff # 绝对地址 - 当前地址 struct.pack_into("<I", out, edi+1, rel) n += 1; edi += 5; ecx -= 1 else: edi += 1 return bytes(out), n ``` 处理了 343,036 个调用后,入口点代码从 `e9 00 00 01 5c`(目标被替换到"空旷地址空间")还原成正常的 `jmp 0x401160`。 ## 0x5 导入表恢复 ### 5.1 压缩导入记录 stub 在反过滤之后执行导入处理: ```nasm 8d be 00 e0 6b 03 lea edi, [esi+0x36be000] ; 压缩导入记录区 8d 84 30 00 70 72 03 lea eax, 0x3727000(%eax,%esi,1) ; DLL 名字基址 ``` 即:记录区在 `dest+0x36be000`,DLL 名字基址在 `dest+0x3727000 = 0x3b28000`(正好是 `UPX2` 节 VA,名字字符串存在里面)。记录格式为: ``` [le32 dllname_off][le32 iatoffs] 函数记录: 0x01 + 名字\0 (0xFF + le16 序数 / 0xFE + le32 序数) 0x00 结束一个 DLL ``` 解析脚本(节选): ```python while True: dllname_off, iatoffs = struct.unpack_from("<II", img, p) if dllname_off == 0: break q = p + 8 while True: b = img[q] if b == 0: q += 1; break elif b == 1: e2 = img.find(b"\0", q+1) funcs.append(bytes(img[q+1:e2])); q = e2+1 elif b == 0xFF: funcs.append((0xFF, le16(img, q+1))); q += 3 ``` 恢复出完整导入表: | 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 | 其中 `nfapi.dll` 正是程序目录中的网络过滤库,印证了该服务的作用。 ### 5.2 重建导入描述符 按标准 PE 布局重建 `IMAGE_IMPORT_DESCRIPTOR` + IAT + Hint/Name 表,写入 `.idata` 节: ```python for di, (dll, iat, funcs) in enumerate(dlls): for fi, fn in enumerate(funcs): if isinstance(fn, bytes): val = rva(hn_pos[(di, fi)]) # 指向 Hint/Name else: val = 0x80000000 | fn[1] # 按序号导入 struct.pack_into("<I", out, iat + 4*fi, val) struct.pack_into("<IIIII", out, descs + di*20, rva(iat), 0, 0, rva(dllname_pos[di]), rva(iat)) ``` > 小坑:Python 的 `bytearray` 切片返回的是 `bytearray` 而非 `bytes`,`isinstance(f, bytes)` 为 False,会导致全部被误判成按序号导入(0x800000xx)。需要显式 `bytes(...)` 转换。 ## 0x6 重定位恢复 ### 6.1 压缩重定位流 重定位数据紧随导入记录之后(找 null 记录 `dllname_off==0`,再 +4)。解压算法与官方 UPX 相同,用 `(byte & 0x0f)<<16 | le16` 做扩展偏移: ``` delta 流: 0x01..0xEF 直接为 delta;0xF0..0xFF 走扩展; (byte&0xF)<<16 | le16;若为 0 再读 le32 ``` 每个重定位点的回写是 `bswap(value) + esi`,即指针以**大端存储、减去基址**,运行时加回基址还原成绝对地址。 ### 6.2 基准校准(esi-3 之谜) 最初我按官方 stub 语义 `lea -4(%esi),%ebx` 处理,结果入口点代码被"修坏"了: ```nasm 4013f1: c7 ec 6e 43 05 64 ... ; (bad) 无效指令 ``` 而把基准改为 `esi-3` 后,入口点还原成完全合理的代码: ```nasm 4013f0: 90 nop 4013f1: c7 05 64 fc 9e 03 00 00 00 00 movl $0x0, 0x039efc64 ; 写防护标记 4013fb: e9 60 fd ff ff jmp 0x401160 ``` 且防篡改代码中的 MZ 校验也还原成功: ```nasm 401018: 66 81 3d 00 00 40 00 4d 5a ; cmp word [0x400000], 0x5a4d 检查 PE 头 ``` `cmp [0xf0ffff]` 被修复成 `cmp [0x400000]`(检查镜像基址处 MZ 签名),语义完全说得通。因此该魔改版的 delta 编码基准是 `esi-3`。**自动化脱壳时,可以对候选基准逐一尝试,选取"修复后指针落在镜像内"比例最高的一组**: ```python for base in range(-6, 1): ...应用重定位... valid = 修复后值在镜像内的个数 # 取 valid 最大者 ``` 最终 395,510 个重定位点,基准 -3 时 395,440 个修复值落在镜像内(命中率 99.98%),可靠收敛。 ## 0x7 重建 PE 与验证 解压镜像末尾是 extra_info(`skip = le32(img[-4:])`),内含完整原始 PE 头(`PE\0\0` + COFF + 可选头 + 节表)。按以下步骤写出文件: 1. 还原 DOS 头 + PE 签名 + COFF + 可选头(EP=0x13f0, ImageBase=0x400000, SizeOfImage=0x36bf000); 2. 修正节表 `rawptr/rawsz`(`.bss` 置空); 3. 依次写出 10 个节的数据; 4. 修正数据目录:置零 `SECURITY/DEBUG/BOUND_IMPORT/IAT`,更新 `BASERELOC` 大小; 5. 清零校验和。 最终验证: ```bash $ file accservice_unpacked.exe PE32 executable (console) Intel 80386 (stripped to external PDB), for MS Windows $ objdump -h accservice_unpacked.exe Idx Name Size VMA File off 0 .text 00e7a048 00401000 00000400 1 .data 000c94d0 0127c000 00e7a600 2 .rdata 021c69dc 01346000 00f43c00 ... ``` 导入表、入口点代码、重定位块全部验证通过,与手工推演结果**字节级一致**(52 MB)。 ## 0x8 总结 本次逆向对象是**一种 UPX 变种**,魔改点共四处: | 组件 | 官方 UPX | 本变种 | |---|---|---| | 头部 | `UPX!` magic + 标准字段 | magic 移除、字段篡改(upx -d 拒绝) | | NRV2B 常量 | `cmp ebp,-0xd00; adc ecx,1` | `cmp ebp,-0x500; adc ecx,2` | | call8 过滤 | 加 addvalue+cto8 标记 | 无标记,直接存绝对地址 | | 重定位基准 | `esi-4` | `esi-3` | 关键经验: 1. **魔改 UPX 脱壳要"信 stub,不信工具"** —— 标准 upx 的 C 解码路径与运行时 stub 可能不一致,必须逐指令复刻 stub; 2. **重定位基准可以通过"修复值是否落在镜像内"来自动校准**,这是最可靠的收敛判据; 3. 该程序入口处带有**防篡改代码**(写 guard 标记 + MZ 校验),脱壳后结构完整可静态分析,但能否直接运行取决于其自校验,建议 IDA/Ghidra 加载分析。 完整脱壳脚本已整理为 `unpack_upx.py`(纯 Python 标准库),自动提取 stub 常量、解压、反过滤、恢复导入/重定位并重建 PE,已在本样本上验证通过。
回复或点赞可查看完整内容
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
最后于
2026-8-15 20:16 被cendart编辑 ,原因: 去掉目标程序商业名称
上传的附件:
unpack_upx.py
(27.62kb,5次下载)
收藏
・
0
点赞
・
11
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
git_99440qimingminimax
为你点赞!
1天前
mb_cbtxgctx
这个讨论对我很有帮助,谢谢!
2026-9-17 09:21
git_51951meggadf3df
非常支持你的观点!
2026-9-12 13:57
LeoChen..
感谢你的贡献,论坛因你而更加精彩!
2026-8-27 20:07
mb_lthgjpwj
为你点赞!
2026-8-21 07:32
mb_bzztlmaj
这个讨论对我很有帮助,谢谢!
2026-8-16 21:56
huangyalei
为你点赞!
2026-8-16 01:27
顽劣
期待更多优质内容的分享,论坛有你更精彩!
2026-8-15 20:26
mb_heqvlbnm
你的分享对大家帮助很大,非常感谢!
2026-8-15 18:18
jyotidwi
感谢你分享这么好的资源!
2026-8-15 15:55
伟叔叔
感谢你的积极参与,期待更多精彩内容!
2026-8-15 15:45
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
5
)
Snowman_Liam
雪 币:
4
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
138
粉丝
1
关注
私信
Snowman_Liam
2
楼
6666
2026-8-15 15:41
0
mb_rqlksylg
雪 币:
96
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
74
粉丝
0
关注
私信
mb_rqlksylg
3
楼
good
2026-8-22 02:48
0
AK1988DA
雪 币:
134
活跃值:
(850)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
110
粉丝
0
关注
私信
AK1988DA
4
楼
888
2026-8-23 14:07
0
mb_kppjdiod
雪 币:
63
活跃值:
(933)
能力值:
( LV2,RANK:10 )
在线值:
发帖
8
回帖
37
粉丝
3
关注
私信
mb_kppjdiod
5
楼
1
2026-8-23 21:18
0
petersc
雪 币:
2417
活跃值:
(3028)
能力值:
( LV2,RANK:10 )
在线值:
发帖
14
回帖
110
粉丝
1
关注
私信
petersc
6
楼
漂亮
1天前
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
cendart
4
发帖
4
回帖
10
RANK
关注
私信
他的文章
[原创] 某网游加速器程序UPX 脱壳后续,让它跑起来!
721
[原创] 某网游加速器程序 UPX 加壳分析与手工脱壳
924
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
谁下载
×
petersc
mb_rqlksylg
jyotidwi
LeoChen..
mb_lthgjpwj
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部