ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

虚拟机保护逆向实战:IDA 静态还原 FuelVM 虚拟机指令并编写 Keygen

虚拟机保护逆向实战:IDA 静态还原 FuelVM 虚拟机指令并编写 Keygen 文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载虚拟机VM保护是 CTF 逆向中常见且极具挑战性的代码混淆手段程序将真实业务逻辑翻译成自定义字节码再由内置解释器以「取指-译码-执行」循环还原语义导致常规静态分析几乎无从下手。本篇以 ctf-wiki 逆向专题中的经典 Crackme ——FuelVM.exe为完整案例演示一套可复用的虚拟机保护逆向方法论从 API 交叉引用定位输入入口到识别 SEH 异常处理链与反调试技巧再到手动恢复堆栈平衡、逐条还原 VM opcode 语义最终推导出校验算法并写出注册机Keygen。读完本文你将掌握一套不依赖特定工具、可推广到任意 VM 保护题目的系统化分析流程。1. 题目背景FuelVM 的结构与配套文件本案例对应的程序为FuelVM.exe是作者特意设计的一个具有多种指令的简单虚拟机。题目配套提供三个关键文件FuelVM.exe待分析的 Crackme 主程序32 位 Windows PE使用 IDA 分析fuelvm_keygen.py验证分析结果的 Keygen 脚本FuelVM.idb已完成的 IDA 数据库可供对照参考。从概念上讲虚拟机保护的思路与 软件逆向工程简介 中提到的代码混淆一脉相承程序不直接执行用户可见的逻辑而是维护一组自定义寄存器、一个虚拟机栈vm_sp与程序计数器vm_pc将明文校验逻辑编译成一张字节码表再用解释器逐条取出、译码并执行。分析的难点在于opcode 与指令语义的映射关系完全由作者自定义没有任何现成资料可查只能靠比对反汇编片段逐一猜测还原。为了让讲解更清晰本文作者对 IDA 反汇编出的部分变量与函数重新命名如process_input、seh_handler、vm_main等后文将沿用这些命名。2. 运行程序从界面布局确定逆向入口直接运行FuelVM.exe程序界面如下界面布局非常直观Name 输入框用于输入用户名Key 输入框用于输入注册密钥Go 按钮提交输入并触发校验Exit 按钮退出程序。这是一个典型的「用户名 序列号」校验型 Crackme最终目标就是找到 Name 到 Key 的映射关系并写出 Keygen。逆向的第一步是定位关键代码。参照 软件逆向工程简介 中总结的常规逆向流程与「定位关键代码 tips」对于图形界面程序最高效的入口是利用 API 交叉引用程序想获取用户输入必然要调用 Windows API。在 IDA 中搜索GetDlgItemTextA的交叉引用Xref即可顺藤摸瓜找到处理输入的代码。3. 定位用户输入GetDlgItemTextA 交叉引用GetDlgItemTextA是对话框程序获取控件文本的标准 API其原型为UINT GetDlgItemTextA( HWND hDlg, // 对话框句柄 int nIDDlgItem, // 控件 ID LPSTR lpString, // 接收文本的缓冲区 int cchMax // 缓冲区最大长度 );获取的输入字符串会保存在lpString中。在 IDA 中搜索其交叉引用会发现全程序只有一处调用且连续调用了两次.text:00401142 push 0Ch ; cchMax .text:00401144 push offset inputName ; lpString .text:00401149 push 3F8h ; nIDDlgItem .text:0040114E push [ebphWnd] ; hDlg .text:00401151 call GetDlgItemTextA .text:00401156 push 0Ch ; cchMax .text:00401158 push offset inputKey ; lpString .text:0040115D push 3F9h ; nIDDlgItem .text:00401162 push [ebphWnd] ; hDlg .text:00401165 call GetDlgItemTextA .text:0040116A mov var_a, 0 .text:00401171 call process_input .text:00401176 jmp short locExit分析这段代码可以得出清晰的结构第一次调用GetDlgItemTextA将控件3F8h的文本读入缓冲区inputName第二次调用将控件3F9h的文本读入缓冲区inputKey随后初始化一个变量为 0作用暂不明先命名为var_a调用process_input函数jmp locExit跳转到退出相关代码这反证了上面的call process_input就是处理用户输入的关键函数。3F8h与3F9h正是界面中 Name 与 Key 输入框的控件 ID至此输入入口定位完成。4. 输入预处理strlength 与异或混淆进入process_input函数IDA 生成的伪 C 代码如下result strlength((int)inputName); if ( v1 7 ) // v1 length of inputName { *(_DWORD *)lenOfName v1; result strlength((int)inputKey); if ( v2 7 ) // v2 length of inputKey { i 0; do { inputName[i] ^ i; i; } while ( i *(_DWORD *)lenOfName ); unk_4031CE i; dword_4031C8 dword_4035FF; initVM(); initVM(); __debugbreak(); JUMPOUT(*(_DWORD *)word_4012CE); } } return result;4.1 strlength以汇编实现求字符串长度伪代码中的strlength()对应一个用汇编手写的字符串长度函数内部采用cld; repne scasb; not ecx; dec ecx的经典组合来计算长度结果为ecx中的值.text:004011C2 push ebp .text:004011C3 mov ebp, esp .text:004011C5 mov edi, [ebparg_0] .text:004011C8 sub ecx, ecx .text:004011CA sub al, al .text:004011CC not ecx .text:004011CE cld .text:004011CF repne scasb .text:004011D1 not ecx .text:004011D3 dec ecx .text:004011D4 leave .text:004011D5 retn 4结合汇编可以看出伪代码中的v1、v2实际就是strlength返回的字符串长度且被拿来与常量7比较。由此可以确定程序对输入的基本约束inputName 与 inputKey 的长度均不得少于 7否则直接返回不进入后续校验逻辑。4.2 输入混淆逐字节异或下标当两个输入都满足长度要求后程序对inputName做一个简单的逐字节变换i 0; do { inputName[i] ^ i; i; } while ( i *(_DWORD *)lenOfName );即inputName的每一个字符与其下标做异或。对应的 Python 实现为def obfuscate(username): s for i in range(len(username)): s chr(ord(username[i]) ^ i) return s这段预处理本质上是对inputName而不是inputKey做混淆混淆后的结果将作为后续虚拟机解释器的输入数据源。此后的若干变量赋值unk_4031CE、dword_4031C8等对分析校验逻辑并不关键可以暂时忽略。5. 注册 SEH异常处理链背后的反调试process_input的末尾调用了initVM并随后__debugbreak()。查看对应汇编这里实际发生的是注册 SEH结构化异常处理并手动触发异常.text:004012B5 push offset seh_handler .text:004012BA push large dword ptr fs:0 .text:004012C1 mov large fs:0, esp .text:004012C8 call initVM .text:004012CD int 3 ; Trap to DebuggerinitVM完成虚拟机启动前的初始化对寄存器、栈指针、程序计数器等赋初值这一点后面会详细展开。这里先关注 SEH 部分将新异常处理函数seh_handler的地址压栈并把fs:0处原有的 TEB 异常链头压栈然后mov large fs:0, esp完成 SEH 注册紧接着使用int 3EXCEPTION_BREAKPOINT异常码0x80000003手动触发断点异常把控制流转交给刚注册的处理函数。关于int 3异常处理的细节可参考仓库中的 Interrupt 3 专题int 3触发断点异常时 Windows 会递减异常地址以指向所认定的CC操作码但 EIP 的值不变——理解这一机制有助于在动态调试中判断异常来源。5.1 按 c 键将数据转为代码在seh_handler位置IDA 起初并未正确识别出代码显示为一堆原始字节数据.text:004012D7 seh_handler db 64h ; DATA XREF: process_input7D o .text:004012D8 dd 58Fh, 0C4830000h, 13066804h, 0FF640040h, 35h, 25896400h .text:004012D8 dd 0 .text:004012F4 dd 1B8h, 0F7C93300h, 0F7C033F1h, 0FFC483E1h, 8F64FDEBh ...处理方式很简单将光标定位到该地址多次按下c键把这些数据强制转换成指令进行识别。转换后的代码如下seh_handler: ; DATA XREF: process_input7D o .text:004012D7 pop large dword ptr fs:0 .text:004012DE add esp, 4 .text:004012E1 push 401306h .text:004012E6 push large dword ptr fs:0 .text:004012ED mov large fs:0, esp .text:004012F4 mov eax, 1 .text:004012F9 xor ecx, ecx .text:004012FB div ecx .text:004012FD xor eax, eax .text:004012FF mul ecx .text:00401301 .text:00401301 loc_401301: ; CODE XREF: .text:00401304 j .text:00401301 add esp, 0FFFFFFFFh .text:00401304 jmp short near ptr loc_4013012 .text:00401306 ; --------------------------------------------------------------------------- .text:00401306 pop large dword ptr fs:0 .text:0040130D add esp, 4 .text:00401310 push 401330h .text:00401315 push large dword ptr fs:0 .text:0040131C mov large fs:0, esp .text:00401323 xor ecx, ecx .text:00401325 mov ebx, [ecx] .text:00401327 xor eax, eax .text:00401329 mul ecx这段代码揭示了层层嵌套的异常链seh_handler内先pop large fs:0恢复上一层 SEH 链add esp, 4清理栈又以同样方式注册了位于401306h的异常处理函数并通过xor ecx, ecx; div ecx手动触发除零异常401330h处注册下一级处理函数seh_handler3最后40135Eh是最后一个注册的异常处理函数——结合上下文可以推测这才是虚拟机的真正主循环vm_main。5.2 反调试花指令跳转到指令中间在loc_401301位置藏着一个经典的反调试技巧.text:00401301 add esp, 0FFFFFFFFh .text:00401304 jmp short near ptr loc_4013012jmp loc_4013012会使得EIP 转向一条指令的中间add esp, 0FFFFFFFFh的机器码中间位置破坏单步跟踪与调试器的指令解码属于「跳转到指令中间」的花指令junk code。处理手段是将00401301 ~ 00401306这段代码nop掉然后在00401306位置创建一个新函数seh_handler2让 IDA 能够正确反汇编后续逻辑。类似的401330h重命名为seh_handler340135Eh重命名为vm_main。至此一条由int 3、除零异常、花指令层层包裹的 SEH 反调试链被完全剥开。关于 SEH 反调试的更多实战案例可对照仓库中的 反调试技术例题其中第 9 种保护同样是借助除零异常 SEH 实现。提示SEH 链与反调试部分推荐在动态调试器中单步跟踪、观察异常分发过程理解会更透彻。6. 恢复堆栈平衡让 vm_main 可被 F5 反编译对重命名后的vm_main创建函数后按下F5IDA 会提示反编译失败——原因是堆栈不平衡sp-analysis failed。此时可以在 IDA 菜单Options - General中勾选右侧的stack pointer选项让反汇编窗口显示每条指令执行时的栈指针偏移.text:004017F2 000 jmp vm_main .text:004017F7 ; --------------------------------------------------------------------------- .text:004017F7 000 push 0 ; uType .text:004017F9 004 push offset aError ; Error .text:004017FE 008 push offset Text ; The key is wrong. .text:00401803 00C push 0 ; hWnd .text:00401805 010 call MessageBoxA .text:0040180A .text:0040180A locret_40180A: ; CODE XREF: vm_main492 j .text:0040180A 000 leave .text:0040180B -04 leave .text:0040180C -08 leave .text:0040180D -0C leave .text:0040180E -10 leave .text:0040180F -14 leave .text:00401810 -18 leave .text:00401811 -1C retn .text:00401811 vm_main endp ; sp-analysis failed观察栈指针偏移可以读出一系列关键信息顶部的jmp vm_main说明虚拟机内部在循环执行MessageBoxA调用参数为Error与The key is wrong.是校验失败时弹出的错误提示问题出在locret_40180A此位置栈指针已是000平衡状态却连续执行了 7 条leave导致栈指针一路跌到-1C最终retn时完全失衡IDA 因此拒绝反编译。恢复方法非常简单0040180A处已经堆栈平衡只需把这一句leave修改为retn使其直接返回即可.text:0040180A locret_40180A: ; CODE XREF: vm_main492 j .text:0040180A 000 retn .text:0040180B ; --------------------------------------------------------------------------- .text:0040180B 004 leave .text:0040180C 004 leave .text:0040180D 004 leave修改完成后再次按F5vm_main就能成功生成伪 C 代码虚拟机的指令分析正式拉开帷幕。7. 虚拟机指令分析还原 opcode 语义虚拟机分析是一个相对枯燥的还原过程需要把反汇编拆成一个个小片段逐一比对每个片段的操作与涉及的寄存器判断「这是什么指令」。本 Crackme 的虚拟机执行的是一套标准的取指-译码-执行循环取指根据vm_pc程序计数器取出当前字节码译码根据opcode用if-else if-else分支区分指令类型——译码过程提供的信息最多是还原的重点执行跳转到对应的指令实现片段执行操作。由于本题 VM 实现的指令数量有限还原过程并不复杂最终得到完整的 opcode 映射表指令opcodepush0x0apop0x0bmov0x0ccmp0x0dinc0x0edec0x0fand0x1bor0x1cxor0x1dcheck0xff可以看到作者只实现了push、pop、mov、cmp、inc、dec、and、or、xor这 9 种常规运算指令外加一个自定义的check校验指令——整套指令集刻意保持精简非常适合作为 VM 分析的入门练手题。7.1 initVM寄存器与虚拟机状态初始化分析后的initVM函数揭示了虚拟机的寄存器布局int initVM() { int result; // eax1 r1 0; r2 0; r3 0; result (unsigned __int8)inputName[(unsigned __int8)cur_index]; r4 (unsigned __int8)inputName[(unsigned __int8)cur_index]; vm_sp 0x32; vm_pc 0; vm_flags_zf 0; vm_flags_sf 0; cur_index; return result; }由此可以确认虚拟机的完整状态模型4 个通用寄存器r1 / r2 / r3 / r4其中r4被初始化为inputName[cur_index]——即经过异或混淆后的用户名当前字符这正是虚拟机要逐字符处理的数据1 个栈指针vm_sp初始化为0x321 个程序计数器vm_pc初始化为02 个标志位vm_flags_zf零标志与vm_flags_sf符号标志供cmp等指令置位cur_index先前在process_input里被命名为var_a的神秘变量现在真相大白——它指向inputName当前正在处理的字符索引每初始化一次 VM 就自增 1。7.2 指令语义的推导方法以cmp指令为例其执行片段大致遵循如下模式从虚拟机栈弹出两个操作数或寄存器值做减法根据结果设置zf/sf标志而push/pop则以vm_sp为基准对自定义栈做读写mov则在寄存器与栈之间搬运数据。分析时只要抓住「哪个片段访问了vm_sp、哪个片段读写r1~r4、哪个片段置位zf/sf」再结合 opcode 分支入口就能把每条指令的语义逐一定义出来。8. check 指令核心校验逻辑的还原指令集中最特殊的是checkopcode0xff它直接对应虚拟机的校验逻辑。还原出的函数如下int __fastcall check(int a1) { char v1; // al1 int result; // eax4 v1 r1; if ( (unsigned __int8)r1 0x21u ) v1 r1 0x21; LOBYTE(a1) cur_index; if ( v1 inputKey[a1] ) { if ( (unsigned __int8)cur_index (unsigned __int8)lenOfName ) result MessageBoxA(0, aGoodJobNowWrit, Caption, 0); else result initVM(); } else { result MessageBoxA(0, Text, Caption, 0); } return result; }逐行解读这段伪代码取出通用寄存器r1的值由前面的 VM 指令计算得出赋给v1若r1 0x21即小于空格字符 的 ASCII 码 33则加上0x21做归一化偏移——这是作者为 Keygen 埋下的一个小调整规则将处理后的值v1与inputKey[cur_index]输入密钥的当前字符比较相等则继续若cur_index lenOfName说明整个inputName已逐字符校验完毕弹出Good job! Now write a keygen.成功提示否则调用initVM()进入下一轮循环处理用户名下一个字符不相等则弹出错误消息框对应前面看到的The key is wrong.。结合前面两处关键细节可以推导出完整校验模型cur_index会在initVM中自增 1而process_input在进入虚拟机之前连续调用了两次initVMunk_4031CE i; opcode vm_pc; initVM(); initVM(); __debugbreak(); JUMPOUT(*(_DWORD *)word_4012CE);这意味着cur_index在正式校验开始前已经等于 2因此inputKey的前 2 个字符可以是任意值不会被校验——这是 Keygen 编写时的一个重要边界条件。虚拟机从第 3 个字符开始对混淆后的inputName[i] ^ i施加mov/cmp/inc/dec/and/or/xor等运算把结果存入r1再由check指令与inputKey的对应字符比对。9. 编写 Keygen验证还原结果完整分析出虚拟机的指令语义与校验逻辑后就可以编写 Keygen 脚本fuelvm_keygen.py其核心思路与校验流程正好互逆对用户名inputName逐字节异或下标得到混淆后的字符序列前 2 位跳过从第 3 个字符开始按还原出的 VM 指令语义mov/cmp/inc/dec/and/or/xor组合正向计算每个字符经虚拟机运算后应落入r1的结果若r1 0x21补上0x21的偏移得到inputKey对应位置应填写的字符拼接任意 2 个前导字符与后续计算字符即得到合法密钥。运行验证$ python2 fuelvm_keygen.py ctf-wiki [*] Password for user ctf-wiki is: 4mRC*TKJI对用户名ctf-wiki生成的密钥为4mRC*TKJI。将此密钥填入 FuelVM.exe 的 Key 输入框点击 Go程序即弹出Good job! Now write a keygen.的成功提示证明虚拟机分析结论完全正确。10. 总结VM 保护逆向的可复用方法论从 FuelVM 这个案例中可以沉淀出一套通用的虚拟机保护逆向流程找入口利用 API 交叉引用如GetDlgItemTextA或关键字符串定位主处理函数这一步与普通逆向无异剥壳破反调试识别int 3、除零异常与 SEH 异常链等反调试技巧结合栈指针视图恢复堆栈平衡让 IDA 能正常反编译核心循环还原 VM 状态模型从initVM之类的初始化函数提炼寄存器、栈指针、程序计数器与标志位的完整布局逐条还原指令围绕「取指-译码-执行」循环比对访问vm_sp、读写通用寄存器、置位标志的代码片段为每个 opcode 定义语义形成指令映射表锁定校验指令重点关注自定义的校验类指令如check还原出与输入比对的确切规则与边界条件如跳过前 N 个字符编写 Keygen 验证按校验逻辑逆向写出密钥生成脚本以成功弹窗作为还原正确性的最终证明。虚拟机保护虽然在反汇编层面制造了障碍但其核心仍是「取指-译码-执行」的确定性计算只要耐心逐条还原 opcode 语义任何自定义指令集都可以被完整复现。本文相关背景知识可继续阅读仓库中的 软件逆向工程简介、Interrupt 3 与 SEH 反调试专题、反调试技术例题 以及 IDA 使用指南 获得更系统的掌握。赞分享文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载相关推荐ctf-wiki 虚拟机逆向分析实战以 FuelVM crackme 还原取指-译码-执行循环与 Keygen 编写ctf wiki 虚拟机逆向分析实战以 FuelVM crackme 还原取指 译码 执行循环与 Keygen 编写 导读 本文是 ctf wiki 逆向工程文档网络安全教程让 2009 年 iMac 跑上最新 macOS5 步 3 个坑OpenCore Legacy Patcher 升级老旧 Mac 完整指南让 2009 年 iMac 跑上最新 macOS5 步 3 个坑OpenCore Legacy Patcher 升级老旧 Mac 完整指南 应用商店打不操作系统固件驱动开发HiddenVM深度解析匿名虚拟机与隐私保护实战指南HiddenVM深度解析匿名虚拟机与隐私保护实战指南 HiddenVM作为一款创新的隐私保护工具实现了在Tails操作系统环境中运行任何桌面操作系统而不会在桌面应用网络上一篇5分钟终极指南让普通鼠标在Mac上超越触控板的完整解决方案下一篇prompt-tuning进阶技巧多任务提示优化与性能提升实用指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表