编辑“︁
虚拟机分析
”︁
跳转到导航
跳转到搜索
警告:
您没有登录。如果您进行任何编辑,您的IP地址会公开展示。如果您
登录
或
创建账号
,您的编辑会以您的用户名署名,此外还有其他益处。
反垃圾检查。
不要
加入这个!
{{Ctf_Wiki}} = 虚拟机分析 = 有关虚拟机分析部分, 我们以一道简单的crackme来进行讲解. 对应的<code>crackme</code>可以点击此处下载: [https://github.com/ctf-wiki/ctf-challenges/blob/master/reverse/vm/fuelvm/FuelVM.exe FuelVM.exe] 对应的<code>keygenme</code>可以点击此处下载: [https://github.com/ctf-wiki/ctf-challenges/blob/master/reverse/vm/fuelvm/fuelvm_keygen.py fuelvm_keygen.py] 对应的<code>IDA数据库</code>可以点击此处下载: [https://github.com/ctf-wiki/ctf-challenges/blob/master/reverse/vm/fuelvm/FuelVM.idb FuelVM.idb] 本题作者设计了一个具有多种指令的简单虚拟机. 我们使用IDA来进行分析. 并为了方便讲解, 我对反汇编出的一些变量重新进行了命名. == 运行程序 == 我们运行程序 FuelVM.exe. 界面如下所示 [[File:./figure/start.png|start.png]] 在这个界面中, 我们看到右两个输入框, 一个用于输入用户名Name, 另一个则用于输入密钥Key. 还有两个按钮, Go用于提交输入, 而Exit则用于退出程序. == 获取用户输入 == 那么我们就可以从这里入手. 程序想获取用户输入, 需要调用的一个API是<code>GetDlgItemTextA()</code> <syntaxhighlight lang="c">UINT GetDlgItemTextA( HWND hDlg, int nIDDlgItem, LPSTR lpString, int cchMax );</syntaxhighlight> 获取的输入字符串会保存在<code>lpString</code>里. 那么我们就可以打开IDA查找有交叉引用<code>GetDlgItemTextA()</code>的地方. <syntaxhighlight lang="asm">.text:00401142 push 0Ch ; cchMax .text:00401144 push offset inputName ; lpString .text:00401149 push 3F8h ; nIDDlgItem .text:0040114E push [ebp+hWnd] ; hDlg .text:00401151 call GetDlgItemTextA .text:00401156 push 0Ch ; cchMax .text:00401158 push offset inputKey ; lpString .text:0040115D push 3F9h ; nIDDlgItem .text:00401162 push [ebp+hWnd] ; hDlg .text:00401165 call GetDlgItemTextA .text:0040116A mov var_a, 0 .text:00401171 call process_input .text:00401176 jmp short locExit</syntaxhighlight> 如上, IDA只有这里调用过<code>GetDlgItemTextA</code>并且调用了两次分别获取<code>inputName</code>和<code>inputKey</code>. 随后初始化了一个变量为0, 因为还不明白这个变量的作用, 因此先重命名为<code>var_a</code>. 之后进行了一次函数调用并jmp跳转. 因为jmp跳转位置的代码是一些退出程序的代码, 因此我们可以断定上面的这个call, 是在调用处理用户输入的函数. 因此将jmp的位置重命名为<code>locExit</code>, 函数则重命名为<code>process_input</code>. == 处理用户输入 == 我们进入<code>process_input</code>函数, 该函数仅仅对输入字符串进行了很简单的处理. <syntaxhighlight lang="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;</syntaxhighlight> 首先是这个<code>strlength()</code>函数. 函数使用<code>cld; repne scasb; not ecx; dec ecx</code>来计算字符串长度并将结果保存在<code>ecx</code>里. 是汇编基础知识就不多介绍. 所以我们将该函数重命名为<code>strlength</code> <syntaxhighlight lang="asm">.text:004011C2 arg_0 = dword ptr 8 .text:004011C2 .text:004011C2 push ebp .text:004011C3 mov ebp, esp .text:004011C5 mov edi, [ebp+arg_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 .text:004011D5 strlength endp</syntaxhighlight> 而在IDA生成的伪C代码处有<code>v1</code>和<code>v2</code>, 我对其进行了注解, 可以看汇编, 里面是使用<code>ecx</code>与<code>7</code>进行比较, 而<code>ecx</code>是字符串的长度, 于是我们可以知道, 这里对输入的要求是: ''inputName 和 inputKey 的长度均不少于 7'' 当<code>inputName</code>和<code>inputKey</code>长度均不少于7时, 那么就可以对输入进行简单的变换. 以下是一个循环 <syntaxhighlight lang="c"> i = 0; do { inputName[i] ^= i; ++i; } while ( i <= *(_DWORD *)&lenOfName );</syntaxhighlight> 对应的python代码即 <syntaxhighlight lang="python">def obfuscate(username): s = "" for i in range(len(username)): s += chr(ord(username[i]) ^ i) return s</syntaxhighlight> 函数之后对一些变量进行了赋值(这些并不重要, 就忽略不讲了.) <span id="注册seh"></span> == 注册SEH == <syntaxhighlight lang="asm">.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 Debugger</syntaxhighlight> <code>initVM</code>完成的是一些虚拟机启动前的初始化工作(其实就是对一些寄存器和相关的部分赋初值), 我们之后来讨论. 这里我们关注的是SEH部分. 这里注册了一个SEH句柄, 异常处理函数我重命名为<code>seh_handler</code>, 并之后使用<code>int 3</code>手动触发异常. 而在<code>seh_handler</code>位置, IDA并未正确识别出对应的代码 <pre>.text:004012D7 seh_handler db 64h ; DATA XREF: process_input+7Do .text:004012D8 dd 58Fh, 0C4830000h, 13066804h, 0FF640040h, 35h, 25896400h .text:004012D8 dd 0 .text:004012F4 dd 1B8h, 0F7C93300h, 0F7C033F1h, 0FFC483E1h, 8F64FDEBh .text:004012F4 dd 5, 4C48300h, 40133068h, 35FF6400h, 0 .text:0040131C dd 258964h, 33000000h, 33198BC9h, 83E1F7C0h, 0FDEBFFC4h .text:0040131C dd 58F64h, 83000000h, 5E6804C4h, 64004013h, 35FFh, 89640000h .text:0040131C dd 25h, 0C033CC00h, 0C483E1F7h, 83FDEBFFh, 4035FF05h, 0D8B0200h .text:0040131C dd 4035FFh, 3000B1FFh, 58F0040h, 4031C8h, 31C83D80h, 750A0040h .text:0040131C dd 0B1FF4176h, 403000h, 31C8058Fh, 3D800040h, 4031C8h</pre> 我们可以点击相应位置按下<code>c</code>键, 将这些数据转换成代码进行识别. (我们需要按下多次c键进行转换), 得到如下代码. 如下, 在<code>seh_handler</code>位置, 又用类似的方法注册了一个位于<code>401306h</code>的异常处理函数, 并通过<code>xor ecx,ecx; div ecx</code>手动触发了一个<code>除0异常</code>. 而在<code>loc_401301</code>位置, 这是一个反调试技巧, <code>jmp loc_401301+2</code>会使得<code>EIP</code>转向一条指令中间, 使得无法继续调试. 所以我们可以将<code>00401301~00401306</code>部分的代码<code>nop</code>掉, 然后在<code>00401306</code>位置创建一个新函数<code>seh_handler2</code> <pre>seh_handler: ; DATA XREF: process_input+7Do .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:00401304j .text:00401301 add esp, 0FFFFFFFFh .text:00401304 jmp short near ptr loc_401301+2 .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</pre> 类似的, 还有<code>401330h</code>重命名为<code>seh_handler3</code>, 而<code>40135Eh</code>是最后一个注册的异常处理函数, 我们可以推测这才是虚拟机真正的main函数, 因此我们将<code>40135Eh</code>重命名为<code>vm_main</code>. (有关SEH和反调试的部分, 可以推荐大家自己去动态调试一番弄清楚) == 恢复堆栈平衡 == 我们创建了一个<code>vm_main</code>函数(重命名后还需要创建函数, IDA才能识别), 然后按下<code>F5</code>提示失败, 失败的原因则是由于堆栈不平衡导致的. 因此我们可以点击IDA菜单项<code>Options->General</code>在右侧勾选<code>stack pointer</code>. 这样就会显示出对应的栈指针. <pre>.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_main+492j .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</pre> 我们来到最下显示不平衡的位置. 最上的<code>jmp vm_main</code>表明虚拟机内在执行一个循环. 而<code>MessageBoxA</code>的调用则是显示最后弹出的错误信息. 而在<code>locret_40180A</code>位置处, 经过多次leave堆栈严重不平衡, 因此我们需要手动恢复堆栈平衡. 这里也很简单, 在<code>0040180A</code>位置已经堆栈平衡了(000), 因此我们只需要将这一句<code>leave</code>修改为<code>retn</code>就可以了. 如下这样 <pre>.text:0040180A locret_40180A: ; CODE XREF: vm_main+492j .text:0040180A 000 retn .text:0040180B ; --------------------------------------------------------------------------- .text:0040180B 004 leave .text:0040180C 004 leave .text:0040180D 004 leave</pre> 然后你就可以发现<code>vm_main</code>可以F5生成伪C代码了. == 虚拟机指令分析 == 说实话, 虚拟机的分析部分是一个比较枯燥的还原过程, 你需要比对各个小部分的操作来判断这是一个怎样的指令, 使用的是哪些寄存器. 像这个crackme中, vm进行的是一个<code>取指-译码-执行</code>的循环. <code>译码</code>过程可给予我们的信息最多, 不同的指令都会在这里, 根据它们各自的<code>opcode</code>, 使用<code>if-else if-else</code>分支进行区分. 实际的还原过程并不复杂, 但有可能会因为虚拟机实现的指令数量而显得有些乏味. 最后分析出的结果如下: {| class="wikitable" |- ! opcode ! value |- | push | 0x0a |- | pop | 0x0b |- | mov | 0x0c |- | cmp | 0x0d |- | inc | 0x0e |- | dec | 0x0f |- | and | 0x1b |- | or | 0x1c |- | xor | 0x1d |- | check | 0xff |} 我们再来看分析后的<code>initVM</code>函数 <syntaxhighlight lang="c">int initVM() { int result; // eax@1 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; }</syntaxhighlight> 这里有4个通用寄存器(<code>r1/r2/r3/r4</code>), 1个<code>sp</code>指针和1个<code>pc</code>指针, 标志<code>zf</code>和<code>sf</code>. 先前我们不知道的<code>var_a</code>也被重命名为<code>cur_index</code>, 指向的是<code>inputName</code>当前正在处理的字符索引. 对于VM实现的多个指令我们就不再多说, 重点来看下<code>check</code>部分的操作. <syntaxhighlight lang="c">int __fastcall check(int a1) { char v1; // al@1 int result; // eax@4 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; }</syntaxhighlight> 如果<code>r1</code>中的值跟<code>inputKey[cur_index]</code>相等, 那么会继续判断是否已经检查完了整个<code>inputName</code>, 如果没有出错且比对结束, 那么就会弹出<code>Good job! Now write a keygen.</code>的消息框. 否则会继续<code>initVM</code>进入下一轮循环.(出错了当然是弹出消息框提示错误了. ) <code>cur_index</code>会在<code>initVM</code>中自增1, 那么还记得之前在<code>process_input</code>里有执行2次<code>initVM</code>吗. 因为有执行2次<code>initVM</code>, 所以我们的<code>inputKey</code>的前2位可以是任意字符. <syntaxhighlight lang="c"> unk_4031CE = i; opcode = vm_pc; initVM(); initVM(); __debugbreak(); JUMPOUT(*(_DWORD *)&word_4012CE);</syntaxhighlight> 故而我们分析完了整个虚拟机, 便可以开始着手编写<code>Keygen</code>. 对应的<code>keygenme</code>可以点击此处下载: [https://github.com/ctf-wiki/ctf-challenges/blob/master/reverse/vm/fuelvm/fuelvm_keygen.py fuelvm_keygen.py] <syntaxhighlight lang="bash">$ python2 fuelvm_keygen.py ctf-wiki [*] Password for user 'ctf-wiki' is: 4mRC*TKJI</syntaxhighlight> 对应的<code>IDA数据库</code>可以点击此处下载: [https://github.com/ctf-wiki/ctf-challenges/blob/master/reverse/vm/fuelvm/FuelVM.idb FuelVM.idb] [[Category: Reverse_Overview]]
摘要:
请注意,所有对gamedev的贡献均可能会被其他贡献者编辑、修改或删除。如果您不希望您的文字作品被随意编辑,请不要在此提交。
您同时也向我们承诺,您提交的内容为您自己所创作,或是复制自公共领域或类似自由来源(详情请见
Gamedev:著作权
)。
未经许可,请勿提交受著作权保护的作品!
取消
编辑帮助
(在新窗口中打开)
该页面使用的模板:
Template:Ctf Wiki
(
编辑
)
导航菜单
个人工具
未登录
讨论
贡献
创建账号
登录
命名空间
页面
讨论
不转换
不转换
简体
繁體
大陆简体
香港繁體
澳門繁體
大马简体
新加坡简体
臺灣正體
查看
阅读
编辑
查看历史
更多
搜索
导航
首页
最近更改
随机页面
MediaWiki帮助
工具
链入页面
相关更改
特殊页面
页面信息