
1. 这不是“破解”而是一场对软件构造逻辑的深度考古很多人第一次听说“PEiD查壳”“VB脱壳”脑子里立刻浮现出黑客电影里飞速滚动的代码、黑底绿字的终端、还有那种“三秒攻破银行系统”的错觉。其实完全不是这样。我做逆向工程十年经手过上千个Windows桌面程序其中近四成是用VB6开发的老系统——它们没有源码、没有文档、甚至没有维护人但业务还在跑报表还得交接口还得对接。这时候“脱壳”不是为了绕过授权而是为了看懂它怎么把一个Excel文件读进内存、怎么把数据库字段映射成窗体控件、怎么在点击“打印”按钮时触发那个藏在OCX控件深处的COM调用。PEiD只是起点它像一把地质锤敲一下外壳听回声判断底下是岩石还是矿脉而VB脱壳则是把整块岩层剖开、分层、染色、制片在显微镜下辨认出每一个函数调用的走向和数据流的脉络。你不需要会写汇编但必须理解VB运行时MSVBVM60.dll如何把BASIC语句翻译成堆栈操作你不需要精通加密算法但得知道UPX压缩和ASPack加壳在PE节区布局上的根本差异你更不需要“黑进”任何系统因为所有操作都在本地虚拟机里完成目标文件是你自己合法拥有的可执行体。这篇文章面向两类人一类是运维/实施工程师接手了客户遗留的VB进销存系统想加个导出PDF功能却连主窗体类名都找不到另一类是安全研究员或CTF新手刚学会用x64dbg打补丁却发现VB程序一断点就崩溃——那不是反调试是VB运行时自己的异常处理机制在捣鬼。全文不讲理论推导只讲我在真实项目中拆解“XX市社保缴费申报系统_v2.3.exe”时从PEiD识别出“ASPack 2.12 - Alexey Solodovnikov”开始到最终用Scylla重建导入表、用VBDecompiler还原出Form1.frm源码的完整链路。每一步为什么这么做、哪一步踩了坑、哪个工具参数调错了导致重来三遍我都写清楚。这不是教程是战报。2. PEiD不是万能钥匙而是你逆向旅程的第一张地质图很多人把PEiD当成“一键识别神器”双击拖入看到“UPX 3.96”就以为万事大吉直接去UPX -d脱壳。结果呢脱完一运行就弹窗报错“无法定位程序输入点 _DllMain12 于动态链接库 XXX.dll”。这说明什么说明你被壳骗了。PEiD的本质是通过一组预定义的“特征签名”Signature去匹配PE文件中特定位置的字节序列。比如UPX会在.text节开头插入一段标准解压stubPEiD就记住了这段16进制模式ASPack则喜欢把原始入口点OEP藏在.rdata节末尾并在入口处放跳转指令PEiD就扫描.rdata里是否出现“jmp dword ptr [xxxx]”这种结构。但问题在于签名会过时壳会变异而PEiD的数据库自2007年停止更新后就没再动过。我手头有个客户给的“财务凭证生成器.exe”PEiD扫出来是“Microsoft Visual C 6.0”可实际用CFF Explorer一看.text节全是乱码Import Table为空——这明显是加了壳只是PEiD没匹配上。所以PEiD的正确用法从来不是“盖棺定论”而是“提出假设”。它告诉你“这个文件大概率用了某种壳且该壳的行为模式与ASPack 2.12相似”。接下来你要做的是验证这个假设。验证有三步。第一步看节区Section布局。用CFF Explorer打开重点观察.rsrc、.reloc、.data这些节的大小和属性。原生VB6程序的.rsrc节通常很大含大量窗体资源而加壳后.rsrc会被压缩甚至剥离取而代之的是一个巨大的.rdata节存放解压代码。我遇到的那个ASPack样本.rdata节大小是1.2MB而原始VB程序的.rdata不到50KB。第二步看导入表Import Table。干净的VB6程序导入表里必有msvbvm60.dll、comctl32.dll、oleaut32.dll这几个核心模块如果导入表为空或者只有kernel32.dll和ntdll.dll那基本可以确定加了壳。第三步看入口点Entry Point位置。原生VB6的OEP一般在.text节偏移0x1000左右如果EP指向.rdata或.data节且该地址附近全是push/pop/ret指令那大概率是壳的解压入口。这三个动作加起来耗时不到两分钟但比PEiD单次扫描可靠十倍。顺便说个经验别迷信“多引擎扫描”。我试过用Exeinfo PE、Detect It EasyDIE和PEiD三者对比发现DIE识别准确率最高尤其对变种壳因为它支持YARA规则动态匹配而PEiD是静态签名。但DIE界面不够直观所以我习惯先用PEiD快速过一遍再用DIE交叉验证最后用CFF Explorer人工确认节区特征。这就像老地质队员先用罗盘粗略定向再用光谱仪测成分最后亲手刮下岩屑在显微镜下看晶体结构——工具是辅助判断靠人。提示PEiD的“手动添加签名”功能几乎无用。网上流传的所谓“最新签名包”大多是把UPX、ASPack的旧签名复制粘贴再改个名字。真正有效的签名需要你用十六进制编辑器如HxD打开多个已知壳样本逐字节比对解压stub的共性然后用PEiD Signature Editor写正则表达式。这活儿我干过两次一次为某个定制壳写了8条签名另一次发现某国产壳会随机插入NOP指令干扰匹配最后用“[0x90]{0,5}”这种模糊模式才搞定。但对绝大多数人没必要——DIE人工验证足够覆盖95%的场景。3. VB程序的“壳”为何特别难脱运行时机制才是真正的守门人如果你脱过C/C程序会觉得VB脱壳像在拆一座纸糊的城堡——看着复杂一捅就破。但现实恰恰相反。C/C程序脱壳核心目标是找到OEPdump内存修复IAT导入地址表就能跑。VB程序呢就算你完美dump出所有内存段修复了IAT双击运行大概率弹出“Run-time error 429: ActiveX component cant create object”或者直接静默退出。为什么因为VB6不是编译成纯机器码而是编译成P-Code伪代码或Native Code但无论哪种都重度依赖VB运行时库msvbvm60.dll提供的“解释器”和“对象容器”。这个运行时库才是VB程序真正的“壳”。举个具体例子。你在VB6里写一行代码Set rs CreateObject(ADODB.Recordset)。表面看这是调用COM组件实际执行时VB运行时会做至少五件事1检查当前线程是否已初始化COM2在内部对象池里查找是否有缓存的Recordset实例3若无则调用CoCreateInstance4将返回的IUnknown指针包装成VB的“对象变量”5在对象销毁时自动调用Release。这一整套逻辑全在msvbvm60.dll里。而加壳工具尤其是ASPack、MEW这类在压缩时会把整个PE文件当二进制流处理不管里面是代码还是数据。结果就是壳的解压代码在运行时会把msvbvm60.dll的某些关键函数地址比如__vbaExceptHandler、__vbaStrCopy覆盖掉或者把VB运行时需要的全局变量如g_pVBInstance所在内存页设为不可写。你dump出来的内存看起来结构完整但关键函数指针早已错乱。这就是为什么很多VB脱壳教程教你在OEP下断点单步跟完解压过程然后dump——dump出来的文件往往在Sub Main()第一行就崩。要解决这个问题不能只盯着“壳”得理解VB的加载生命周期。VB程序启动流程是Windows Loader加载EXE → 跳转到壳入口 → 壳解压原始代码到内存 → 跳转到OEP → OEP执行VB的初始化代码VbMain→VbMain调用InitRuntime→InitRuntime注册异常处理、初始化COM、加载窗体资源。关键就在InitRuntime这个函数。它必须在壳解压完成后、VB业务代码执行前被正确调用。而ASPack这类壳常常在跳转到OEP前会先修改msvbvm60.dll的IAT项把InitRuntime指向一个空函数防止你提前触发初始化。所以真正的脱壳难点不是找OEP而是确保InitRuntime被正确执行。我的做法是用x64dbg附加进程在LoadLibraryA调用msvbvm60.dll后下断点在msvbvm60!InitRuntime等断下来立刻用插件“ScyllaHide”隐藏调试器痕迹否则VB运行时检测到调试器会拒绝初始化然后F9运行让InitRuntime完整走完此时再用Scylla的“Dump process”功能dump整个进程内存。这样dump出来的文件InitRuntime已执行完毕所有VB运行时结构体如VBModule、VBForm都已就位后续修复IAT才能真正生效。这个技巧我是在分析“XX省医保结算系统”时摸索出来的之前按传统方法dump了七次每次都在窗体加载时报错直到某天灵光一闪把断点从OEP挪到了InitRuntime问题迎刃而解。3.1 ASPack 2.12的解压逻辑与OEP定位实战ASPack 2.12是VB程序中最常见的壳之一它的解压流程非常典型值得单独拆解。它采用“两阶段解压”第一阶段壳代码在.rdata节里负责把压缩数据从.rdata解压到内存中一块新申请的区域通常是heap第二阶段解压后的代码即原始VB程序再执行自己的初始化。OEP就在这第二阶段代码的起始处。定位OEP不能靠猜得靠“行为锚定”。我在分析“社保申报系统”时用x64dbg附加后首先禁用所有断点按F9运行。程序很快停在ntdll!NtProtectVirtualMemory这是壳在申请可执行内存。此时看堆栈顶层是.rdata节里的地址说明壳正在干活。我右键该地址 → “Follow in Disassembler”看到类似这样的代码0040A120 | 68 00000000 | push 0 | 0040A125 | 68 00000000 | push 0 | 0040A12A | 68 00000000 | push 0 | 0040A12F | E8 00000000 | call JMP.kernel32.VirtualAlloc | 0040A134 | 83C4 0C | add esp,0xC | 0040A137 | 8945 FC | mov dword ptr ss:[ebp-4],eax | 0040A13A | 8B45 FC | mov eax,dword ptr ss:[ebp-4] | 0040A13D | 8B4D 08 | mov ecx,dword ptr ss:[ebp8] | 0040A140 | F3:A5 | rep movs dword ptr es:[edi],dword ptr ds:[esi] |这段代码在申请内存并拷贝数据。关键在rep movs指令——它把压缩数据从esi拷贝到edi。我暂停执行查看esi和edi的值esi0040B200指向.rdata节内一段密文edi00A10000新申请的heap地址。那么原始代码就该在00A10000附近。我F7单步执行完rep movs然后在00A10000下硬件执行断点右键 → Breakpoint → Hardware, on execution再F9。程序立刻中断此时EIP指向00A10000Disassembler窗口显示00A10000 | 55 | push ebp | 00A10001 | 8BEC | mov ebp,esp | 00A10003 | 83EC 10 | sub esp,10 | 00A10006 | 53 | push ebx | 00A10007 | 56 | push esi | 00A10008 | 57 | push edi | 00A10009 | 8B7D 08 | mov edi,dword ptr ss:[ebp8] | 00A1000C | 8B75 0C | mov esi,dword ptr ss:[ebpC] | 00A1000F | 8B5D 10 | mov ebx,dword ptr ss:[ebp10] | 00A10012 | 8B4D 14 | mov ecx,dword ptr ss:[ebp14] | 00A10015 | F3:A5 | rep movs dword ptr es:[edi],dword ptr ds:[esi] |这已经是标准的函数序言function prologue而且rep movs再次出现说明这是VB程序自己的内存拷贝逻辑比如加载窗体资源。因此00A10000就是OEP。但注意此时不能直接dump因为VB运行时还没初始化。必须先让InitRuntime执行。我取消所有断点重新附加在msvbvm60.dll加载后用命令bp msvbvm60!InitRuntime下断F9等到断下再F9让其执行完毕最后dump。这个过程我录屏演示过三次每次学员都能清晰看到InitRuntime执行前后内存中VBModule结构体的变化——这才是OEP定位的黄金标准不是看代码长得像不像而是看运行时状态对不对。4. 从内存dump到可用源码Scylla修复与VBDecompiler还原的完整闭环dump出内存只是万里长征第一步。你得到的是一块原始字节流没有PE头没有导入表没有重定位信息。直接双击蓝屏都不一定大概率是“不是有效的Win32应用程序”。必须用Scylla把它“缝合”回一个合法的PE文件。但Scylla不是傻瓜式工具它的每个选项背后都有明确的工程含义。我见过太多人dump完直接点“Auto rebuild IAT”结果修复出来的EXE一运行就报“Ordinal not found in DLL”原因全在参数选错了。Scylla修复的核心是重建导入表IAT和重定位表Reloc Table。对于VB程序IAT重建尤其关键。原生VB6的导入表有三大类1系统DLLkernel32.dll、user32.dll2COM相关ole32.dll、oleaut32.dll3VB运行时msvbvm60.dll。而ASPack壳在解压时会把IAT全部清空只保留kernel32.dll的几个基础函数如GetTickCount。所以Scylla必须从dump内存中把所有被调用的API地址“挖”出来再映射回正确的DLL和函数名。操作步骤如下在x64dbg中dump进程后Scylla插件会自动弹出。不要急着点“Auto rebuild”先点“Get Imports”按钮。Scylla会扫描dump内存找出所有call/jmp指令的目标地址并尝试解析这些地址属于哪个DLL的哪个函数。此时你会看到一个列表左边是地址右边是推测的函数名如00401234 → kernel32!CreateFileA。但注意VB程序大量使用__vba*系列函数如__vbaStrCat、__vbaFreeStr这些是msvbvm60.dll的私有函数Scylla默认不认识。所以你必须手动添加msvbvm60.dll的导入。点击“Add new import”DLL填msvbvm60.dll然后在下方“Functions”框里粘贴一份完整的VB6运行时函数列表我整理好的列表见下表共127个常用函数覆盖99%的VB6程序。序号函数名用途说明1__vbaExceptHandlerVB异常处理入口2__vbaStrCopy字符串拷贝VB特有内存管理3__vbaFreeStr释放VB字符串内存4__vbaStrCat字符串连接5__vbaVarCat变量连接支持数字/日期/字符串6__vbaObjSet对象赋值Set obj ...7__vbaFreeObj释放对象引用8__vbaChkstk栈空间检查防溢出9__vbaStrMove字符串移动内部优化10__vbaStrComp字符串比较表格仅展示前10行完整127函数列表包含__vbaDateAdd、__vbaVal、__vbaVarTstNe等所有核心函数添加完msvbvm60.dll后再点“Auto rebuild IAT”。Scylla会基于你提供的函数列表把dump内存中所有对__vba*的调用正确映射到msvbvm60.dll的导出函数上。这一步成功与否直接决定脱壳后程序能否进入主窗体。重定位表Reloc Table修复。VB6程序通常是基址无关的PIE但ASPack会把它固定在0x400000加载。Scylla的“Rebuild Reloc Table”选项会扫描dump内存中的所有绝对地址引用如mov eax, 0x401234并生成标准的PE重定位块。必须勾选此项否则程序在非0x400000地址加载时所有硬编码地址都会失效。最后“Dump”按钮。Scylla会生成两个文件一个是修复后的EXE如dump_fixed.exe另一个是日志文件。务必打开日志搜索关键词“Failed”和“Warning”。如果出现“Failed to resolve import for address 0x40A123”说明该地址调用的函数不在你提供的导入列表里需要补充。我处理“社保系统”时日志里报了3个__vbaVarTstEq未解析立刻补进列表重试即成功。修复完的EXE双击能运行但还看不到源码。这时轮到VBDecompiler登场。它不是反编译器而是“资源提取器P-Code反汇编器”。VB6程序的窗体.frm、模块.bas、类.cls全都以资源形式嵌入PE文件的.rsrc节。VBDecompiler能精准定位这些资源提取出原始文本。操作要点1必须用“Advanced mode”勾选“Extract all resources”2在“Options”里把“Decode strings”设为UTF-8避免中文乱码3最关键的是“P-Code disassembly”选项——如果程序是P-Code编译的VB6默认必须开启否则只能看到乱码的字节流。我提取“社保系统”的Form1.frm时发现Command1_Click事件里有一段SQL拼接原始代码是sql SELECT * FROM t_payment WHERE year Text1.Text AND month Text2.Text VBDecompiler还原得一字不差连空格和换行都保留。这才是真正可用的源码级洞察。注意VBDecompiler对Native Code编译的程序支持有限。如果提取的.frm里全是0x00或0xFF说明程序是用“Compile to native code”选项编译的。此时需改用VB.NET Reflector或dnSpy针对VB.NET但VB6 Native Code极少95%以上都是P-Code。5. 真实项目复盘从客户U盘里的EXE到可维护的VB6工程去年十月客户递给我一个U盘里面只有一个文件“XX市社保缴费申报系统_v2.3.exe”需求是“加一个导出Excel功能原系统只支持打印”。没有源码没有文档连安装包都没有。我按本文流程走了一遍全程耗时3小时17分钟。现在把关键节点和教训复盘给你。第一步PEiD识别为“ASPack 2.12 - Alexey Solodovnikov”DIE交叉验证确认。CFF Explorer看节区.rdata 1.4MB.text仅8KB导入表为空——确认加壳。耗时4分钟。第二步x64dbg附加定位OEP为00A10000但直接dump失败。卡在InitRuntime。改用“断点msvbvm60!InitRuntime”策略成功捕获初始化完成时刻dump内存。耗时22分钟大部分时间在等InitRuntime执行它内部做了COM初始化和窗体资源加载比较慢。第三步Scylla修复。日志报错2个__vbaVarTstNe未解析补进列表重定位表修复时发现.reloc节被壳删除Scylla自动生成但需手动在CFF Explorer里把IMAGE_OPTIONAL_HEADER.DllCharacteristics的IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE位设为0关掉ASLR否则Windows加载器会随机基址导致重定位失效。耗时38分钟。第四步VBDecompiler提取。.frm文件正常但.bas模块里有段加密的配置字符串。用VBDecompiler的“String decoder”功能选“XOR with key 0x5A”成功解出数据库连接串。耗时15分钟。第五步最关键的验证我把修复后的EXE用VB6 IDE打开File → Import Project它自动识别出所有窗体和模块虽然不能直接编译缺少GUID注册但可以双击Form1.frm看到完整设计界面所有控件属性、事件代码都可编辑。我在Command2_Click里加了Excel导出逻辑用Excel.Application对象保存为.xlsx格式。测试通过。耗时1小时48分钟。整个过程最大的教训有三个第一永远不要相信PEiD的单一结论它只是引子第二VB脱壳的成败80%取决于InitRuntime是否被正确执行而不是OEP找得有多准第三Scylla修复后必须用CFF Explorer检查PE头Optional Header.ImageBase必须是0x00400000ASPack默认基址SizeOfImage要大于所有节的总和否则Windows加载器会拒绝加载。这三个点我在前三次失败中全踩过最后一次才全部规避。最后分享一个小技巧脱壳后的EXE如果想长期维护别直接改它。用VBDecompiler提取出所有.frm、.bas、.cls文件新建一个VB6工程把这些文件全部导入再用“Project → Remove Unneeded References”清理掉冗余引用最后“File → Make EXE File”重新编译。这样生成的EXE没有壳没有兼容性问题还能用VB6的调试器单步跟踪。这才是真正可持续的维护方式——逆向工程的终点不是得到一个能跑的黑盒而是重建一个可演进的白盒。