首页
社区
课程
招聘
[原创] 某网游加速器程序UPX 脱壳后续,让它跑起来!
发表于: 2026-8-15 20:01 284

[原创] 某网游加速器程序UPX 脱壳后续,让它跑起来!

2026-8-15 20:01
284

接上篇《某网游加速器程序 UPX 加壳分析与手工脱壳》。
上篇按 stub 逐指令复刻,把 52MB 的 Go 镜像解压、反过滤、恢复导入/重定位、重建 PE,静态验证全过。
结果一运行就 0xc0000005。这篇记录从"静态对、运行崩"到定位到解码器一个字节的初始化错误的全过程。
声明:本文仅用于安全研究与学习交流,请勿用于非法用途。

0x0 静态全对,一跑就崩

上篇做出来的镜像(ImageBase 0x400000,SizeOfImage 0x36bf000),fileobjdump 都正常:

$ file unpacked.exe
PE32 executable (console) Intel 80386 (stripped to external PDB), for MS Windows
$ objdump -h unpacked.exe   # 10 个节、导入表、.reloc 全部合法

pefile 校验 PE 头、节表、数据目录全部通过,节无重叠。但双击直接崩溃:

0xc0000005 (Access Violation),发生在加载器初始化阶段

注意,崩的是"加载器初始化",不是程序自己 —— 说明问题出在镜像里加载器要读的那几块元数据(TLS、.CRT、重定位结果),而不是 Go 运行时本身的逻辑。这个判断基本决定了后面的排查方向。

0x1 四层崩溃,一层一层剥

每修一层,崩溃就往前推一层,一路剥了四层:

  1. 启动即 AV:ntdll LdrpInitializeProcess 读 TLS 目录时 READ fault @ 0x81fc5c03
  2. 报缺 nfapi.dll(目录中的一个 netfilter 库),拷过来就好 —— 这个只是环境问题
  3. 又 AV:msvcrt initterm 遍历 .CRT 表,表内指针被重定位成了 2.9GB 的垃圾地址
  4. 又 AV,EIP 停在 RVA 0x11d9:RVA 0x13bf 处一条 jmp 的目标差 1 字节,落进了指令中间

到第四层就明白了:这是系统性 bug,点修一处下一处必崩。

TLS 目录是垃圾

TLS 目录 DD[9] 指向 VA 0x310b9a4 / Size 0x18,结构体内容:

StartAddressOfRawData = 0x9f300001
EndAddressOfRawData   = 0x9f300403
AddressOfIndex        = 0x9efc5c03   <- 注意高字节 0x9
AddressOfCallBacks    = 0x9f201c03
SizeOfZeroFill        = 3

全是垃圾。ntdll 初始化线程局部存储时按 AddressOfIndex 去读,高字节 0x9 直接指向未映射地址,READ fault。
先清零 DD[9] 顶着(Go 运行时用的是 Win32 TlsAlloc,不依赖 PE TLS 目录,安全)。

.CRT 表被错位重定位

过了 TLS 又崩在 initterm。.CRT 节(VA 0x35f2000 / VSize 0x30)本应是 __xi_a/__xi_z__xc_a/__xc_z 的函数指针表,文件里却是:

+0x04 = 0x40112000   +0x10 = 0x40101000
+0x1c = 0x27976000   +0x20 = 0x27971001

运行时被重定位成 0xAD112000 / 0xAD101000 / 0x94976000 / 0x94971001。delta=0x6D000000,是镜像 delta 0x6D0000 的 256 倍,明显不对。
.reloc 表发现这 4 条 HIGHLOW 条目全部错位 +1 字节:条目落在 0x35f2005 / 0x35f2011 / 0x35f201d / 0x35f2021,而指针在 -1 处。加完 delta,本该是 0x401120 的指针变成了 2.9GB,initterm 顺着它 call 过去就炸了。先清零 4 个指针 + 4 条错位重定位改 ABSOLUTE。

jmp 位移差 1,警报拉响

又崩,EIP=RVA 0x11d9。反汇编看现场:

4011d0: c7 05 08 00 51 03 01 00 00 00   mov dword ptr [0x03510008], 1   ; 10 字节, 0x11d0-0x11d9
4013bf: e9 15 fe ff ff                  jmp -0x1eb                       ; -> 0x4011D9 !!

jmp 目标 0x4011D9 正好落在上面那条 mov 的最后一个字节 00 上,从那儿执行到 add [ecx+...], ah 写非法地址 → AV。
差 1 字节。而且同一条代码路径里 call 全对,就这一条 jmp 错 1 —— 这已经不是某一处坏了,是整体有问题。点修到此为止。

0x2 转机:拿 running 的 packed 进程当 ground truth

原版 packed exe 是能跑的,产品本身没坏。stub 运行时把 Go 镜像解压进内存,这份内存就是权威解压结果
用调试器把 packed exe 跑起来,6 秒后 dump 完整镜像(54MB),跟脱壳文件逐字节对:

位置 内存(权威) 脱壳文件
RVA 0x13bf e9 16 fe ff ff0x4011DA e9 15 fe ff ff → 0x4011D9
.text 起始 c3 8d b4 26 00 00 00 00 8d b4 26 00 00 00 00 90 c3 8d b4 26 00 00 00 00 00 b4 26 00 00 00 00 00

看到 .text 起始那两行,问题基本就锁定了。

0x3 首分歧:不是 E8/E9,是第一个 match 多吐了一个字节

逐字节对齐后,第一个分歧根本不在任何 call/jmp 上,而在 .text 最开头:

RVA 0x1000..0x1007  rec == gt   (c3 8d b4 26 00 00 00 00)  前 8 字节完全一致
RVA 0x1008..0x100f  rec = 00 b4 26 00 00 00 00 00  (8 字节!)
                    gt  = 8d b4 26 00 00 00 00     (7 字节)
RVA 0x1010 起       rec == gt   (90 83 ec 1c ...)  从此全部错位 1 字节

解码出来是这样的:

  • 正确:c3(ret) + 两个 7 字节 lea-padding 8d b4 26 00 00 00 00(第二个是回引第一个的 match)
  • 错误:c3 + 8d b4 26 00 00 00 00 + 00 b4 26 00 00 00 00 + 多余一个 00

也就是解压器在第一个 match 处多插了 1 个字节,且首字节 00 应为 8d。这一个字节让之后所有字节位置偏移 +1 —— E8/E9 位移、TLS/.CRT 指针"普遍差 1"全是它的连锁反应,0x0/0x1 里看到的三个症状同源。

复盘一下,之前全量 diff 得出"9.2M 个 4 字节不匹配"、"shift=-1 最优对齐",都是被这个单字节插入骗了。那不是恒定偏移,而是从插入点之后整体漂移 1 字节。一开始就该先找首分歧,而不是看统计数字。

0x4 根因:初始 ebp 少了 or ebp, -1

对照 stub 反汇编把解码器的每个分支(m_off / m_len / getbit / cmp ebp,-0x500; adc ecx,2)逐条核了一遍,全都对得上,最后盯住了寄存器初值

037274ac: 83 cd ff   or  ebp, -1        ; <- stub 入口把 ebp 置成 -1

而我解码器里的初始化是:

ecx = eax = ebp = edi = edx = 0          # ebp = 0,错!

ebp 是"上一次匹配偏移"。当某个 match 走 reuse-offset 路径(m_off 解出 eax < 3,即小偏移复用上一次值)时,用的就是初始 ebp。第一个 match 恰好是小偏移:

初始 ebp 行为 输出
0 源偏移 0 → 非法、走 copy4、len=4 00 00 00 00(多 1 字节)
stub -1 (0xFFFFFFFF) 源偏移 1 → 指回上一字节 00、走 copy1、len=3 00 00 00(正确)

差这 1 字节的原因:ebp=-1 时源指针 edx = edi + ebp = 输出位置 - 1,正好回指到刚写入的最后一个字节(00),len=3 恰好补出 00 00 00ebp=0edx = 输出位置(还没写数据),copy4 读出一串 0 还多写 1 字节。另外 cmp ebp,-0x500-10 的 CF 也不同(-10xFFFFFFFF 不比 0xFFFFFB00 小 → ecx+=20ecx+=3),长度也跟着差 1。两个症状同一个错误初值产生,全对上了。

0x5 修复与验证

改一行:

ecx = eax = edi = edx = 0
ebp = 0xFFFFFFFF   # stub: or ebp, -1

重跑整条流水线,后面全部变正常:

修复前 修复后
解压输出 57,824,366 B 57,824,365 B(少 1 字节 = 插入的那字节)
extra_info PE 签名 0x37251d5(错位) 0x37251d4(正确)
.text 起始 16 B c3 8d b4 26 00 00 00 00 00 b4 26 00 00 00 00 00 与内存一致
E8/E9 位移 大量差 +1 反汇编 325,995 条真实 call/jmp,0 条不一致
TLS 目录 垃圾(AddressOfIndex=0x9efc5c03) 正常(0x039efc5c,.reloc 4 字段全有 HIGHLOW 覆盖)
.CRT 表 4 指针 2.9GB 垃圾 正常(+04=0x401120 等合法函数指针)

验证时有个比 diff 更干净的判据:E8/E9 的相对位移不随加载基址变化,逐条比对是纯代码正确性的最强信号。

跑起来:

$ ./unpacked_fixed.exe &   # 52 MB
$ ps                        # 进程存活, 多线程(Go runtime 特征), 无 0xc0000005

再附加调试器确认了一下:主线程 RIP 停在 WOW64 的 syscall 跳板(正常的等待点),不是异常现场。

0x6 总结

组件 上篇结论 本篇补充
NRV2B 常量 cmp -0x500; adc 2 初始 ebp = -1or ebp,-1),转写时漏设
运行 能否运行存疑 修复后可直接运行

几点教训:

  1. "静态验证全过"不等于"能运行"。导入表、重定位、PE 头全对,只要加载器要读的元数据(TLS/.CRT)差一字节就崩;
  2. 逐指令复刻解码器时,寄存器初值也是指令or ebp,-1 这种初始化最容易漏,而且只在首条 reuse-offset 匹配时才暴露;
  3. 拿运行中的 packed 进程当 ground truth 是验证后面是不是全修好的捷径,相对位移(E8/E9)是不受基址影响的纯代码正确性判据;
  4. 单字节插入会伪装成"全局 shift=-1"。先找首分歧再下结论,别被全量 diff 的统计数字带偏。

修复只动了解码器一处初始化(ebp = 0xFFFFFFFF,对应 stub 的 or ebp,-1),重跑一遍后镜像、导入表、重定位、TLS/.CRT 就都正常了。


传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!

最后于 2026-8-15 20:14 被cendart编辑 ,原因: 去掉目标程序商业名称
上传的附件:
收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回