网站首页 > 资源文章 正文
有关虚拟机分析部分, 我们以一道简单的 crackme 来进行讲解.
对应的crackme可以点击此处下载: FuelVM.exe
对应的keygenme可以点击此处下载: fuelvm_keygen.py
对应的IDA数据库可以点击此处下载: FuelVM.idb
本题作者设计了一个具有多种指令的简单虚拟机. 我们使用 IDA 来进行分析. 并为了方便讲解, 我对反汇编出的一些变量重新进行了命名.
运行程序?
我们运行程序 FuelVM.exe. 界面如下所示
在这个界面中, 我们看到右两个输入框, 一个用于输入用户名 Name, 另一个则用于输入密钥 Key. 还有两个按钮, Go 用于提交输入, 而 Exit 则用于退出程序.
获取用户输入?
那么我们就可以从这里入手. 程序想获取用户输入, 需要调用的一个 API 是GetDlgItemTextA()
UINT GetDlgItemTextA(
HWND hDlg,
int nIDDlgItem,
LPSTR lpString,
int cchMax
);
获取的输入字符串会保存在lpString里. 那么我们就可以打开 IDA 查找有交叉引用GetDlgItemTextA()的地方.
.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
如上, IDA 只有这里调用过GetDlgItemTextA并且调用了两次分别获取inputName和inputKey. 随后初始化了一个变量为 0, 因为还不明白这个变量的作用, 因此先重命名为var_a. 之后进行了一次函数调用并 jmp 跳转. 因为 jmp 跳转位置的代码是一些退出程序的代码, 因此我们可以断定上面的这个 call, 是在调用处理用户输入的函数. 因此将 jmp 的位置重命名为locExit, 函数则重命名为process_input.
处理用户输入?
我们进入process_input函数, 该函数仅仅对输入字符串进行了很简单的处理.
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;
首先是这个strlength()函数. 函数使用cld; repne scasb; not ecx; dec ecx来计算字符串长度并将结果保存在ecx里. 是汇编基础知识就不多介绍. 所以我们将该函数重命名为strlength
.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
而在 IDA 生成的伪 C 代码处有v1和v2, 我对其进行了注解, 可以看汇编, 里面是使用ecx与7进行比较, 而ecx是字符串的长度, 于是我们可以知道, 这里对输入的要求是: inputName 和 inputKey 的长度均不少于 7
当inputName和inputKey长度均不少于 7 时, 那么就可以对输入进行简单的变换. 以下是一个循环
i = 0;
do
{
inputName[i] ^= i;
++i;
}
while ( i <= *(_DWORD *)&lenOfName );
对应的 python 代码即
def obfuscate(username):
s = ""
for i in range(len(username)):
s += chr(ord(username[i]) ^ i)
return s
函数之后对一些变量进行了赋值 (这些并不重要, 就忽略不讲了.)
注册 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 Debugger
initVM完成的是一些虚拟机启动前的初始化工作 (其实就是对一些寄存器和相关的部分赋初值), 我们之后来讨论. 这里我们关注的是 SEH 部分. 这里注册了一个 SEH 句柄, 异常处理函数我重命名为seh_handler, 并之后使用int 3手动触发异常. 而在seh_handler位置, IDA 并未正确识别出对应的代码
.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
我们可以点击相应位置按下c键, 将这些数据转换成代码进行识别. (我们需要按下多次 c 键进行转换), 得到如下代码.
如下, 在seh_handler位置, 又用类似的方法注册了一个位于401306h的异常处理函数, 并通过xor ecx,ecx; div ecx手动触发了一个除0异常. 而在loc_401301位置, 这是一个反调试技巧, jmp loc_401301+2会使得EIP转向一条指令中间, 使得无法继续调试. 所以我们可以将00401301~00401306部分的代码nop掉, 然后在00401306位置创建一个新函数seh_handler2
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
类似的, 还有401330h重命名为seh_handler3, 而40135Eh是最后一个注册的异常处理函数, 我们可以推测这才是虚拟机真正的 main 函数, 因此我们将40135Eh重命名为vm_main. (有关 SEH 和反调试的部分, 可以推荐大家自己去动态调试一番弄清楚)
恢复堆栈平衡?
我们创建了一个vm_main函数 (重命名后还需要创建函数, IDA 才能识别), 然后按下F5提示失败, 失败的原因则是由于堆栈不平衡导致的. 因此我们可以点击 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_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
我们来到最下显示不平衡的位置. 最上的jmp vm_main表明虚拟机内在执行一个循环. 而MessageBoxA的调用则是显示最后弹出的错误信息. 而在locret_40180A位置处, 经过多次 leave 堆栈严重不平衡, 因此我们需要手动恢复堆栈平衡.
这里也很简单, 在0040180A位置已经堆栈平衡了 (000), 因此我们只需要将这一句leave修改为retn就可以了. 如下这样
.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
然后你就可以发现vm_main可以 F5 生成伪 C 代码了.
虚拟机指令分析?
说实话, 虚拟机的分析部分是一个比较枯燥的还原过程, 你需要比对各个小部分的操作来判断这是一个怎样的指令, 使用的是哪些寄存器. 像这个 crackme 中, vm 进行的是一个取指-译码-执行的循环. 译码过程可给予我们的信息最多, 不同的指令都会在这里, 根据它们各自的opcode, 使用if-else if-else分支进行区分. 实际的还原过程并不复杂, 但有可能会因为虚拟机实现的指令数量而显得有些乏味.
最后分析出的结果如下:
opcode | value |
push | 0x0a |
pop | 0x0b |
mov | 0x0c |
cmp | 0x0d |
inc | 0x0e |
dec | 0x0f |
and | 0x1b |
or | 0x1c |
xor | 0x1d |
check | 0xff |
我们再来看分析后的initVM函数
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;
}
这里有 4 个通用寄存器 (r1/r2/r3/r4), 1 个sp指针和 1 个pc指针, 标志zf和sf. 先前我们不知道的var_a也被重命名为cur_index, 指向的是inputName当前正在处理的字符索引.
对于 VM 实现的多个指令我们就不再多说, 重点来看下check部分的操作.
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;
}
如果r1中的值跟inputKey[cur_index]相等, 那么会继续判断是否已经检查完了整个inputName, 如果没有出错且比对结束, 那么就会弹出Good job! Now write a keygen.的消息框. 否则会继续initVM进入下一轮循环.(出错了当然是弹出消息框提示错误了.)
cur_index会在initVM中自增 1, 那么还记得之前在process_input里有执行 2 次initVM吗. 因为有执行 2 次initVM, 所以我们的inputKey的前 2 位可以是任意字符.
unk_4031CE = i;
opcode = vm_pc;
initVM();
initVM();
__debugbreak();
JUMPOUT(*(_DWORD *)&word_4012CE);
故而我们分析完了整个虚拟机, 便可以开始着手编写Keygen.
对应的keygenme可以点击此处下载: fuelvm_keygen.py
$ python2 fuelvm_keygen.py ctf-wiki
[*] Password for user 'ctf-wiki' is: 4mRC*TKJI
对应的IDA数据库可以点击此处下载: FuelVM.idb
- 上一篇: 中间相遇攻击 - MITM?
- 下一篇: 软件逆向:脱壳esp定律,以及原理
猜你喜欢
- 2025-01-13 美剧中常见的几个动词词组
- 2025-01-13 CAD多张图纸如何导出在一个pdf,介绍两种方法都可以试一试
- 2025-01-13 英译汉短篇小说——(11-2)
- 2025-01-13 神奇的Hook技术,一文读懂移动App逆向入门之Frida框架的简单使用
- 2025-01-13 笑点不一样,怎么做朋友!和老外怎么聊“哈哈”?
- 2025-01-13 记录一次某MFC软件算法逆向之旅
- 2025-01-13 六,网络安全.OllyDbg动态分析工具基础用法及Crakeme逆向破解
- 2025-01-13 老外说 I’m dead,可不是“我死了”
- 2025-01-13 软件逆向:IDA静态分析
- 2025-01-13 软件逆向:脱壳esp定律,以及原理
你 发表评论:
欢迎- 05-1430个在线地图瓦片URL分享
- 05-1425个在线地图瓦片URL分享
- 05-14PixelStyle for Mac(mac照片编辑器)
- 05-14一篇文章带你了解CSS3 3D 转换知识
- 05-14我们的世界是假的?马斯克:我们生活在高文明模拟的矩阵游戏中
- 05-14【视觉AI的基石】斯坦福大学笔记!带你吃透卷积神经网络 (CNN)
- 05-14潮流 | 通过AAPE的新价目,无虑入荷未来主义服饰
- 05-14身份证查询服务API:准确识别身份证信息
- 最近发表
- 标签列表
-
- 电脑显示器花屏 (79)
- 403 forbidden (65)
- linux怎么查看系统版本 (54)
- 补码运算 (63)
- 缓存服务器 (61)
- 定时重启 (59)
- plsql developer (73)
- 对话框打开时命令无法执行 (61)
- excel数据透视表 (72)
- oracle认证 (56)
- 网页不能复制 (84)
- photoshop外挂滤镜 (58)
- 网页无法复制粘贴 (55)
- vmware workstation 7 1 3 (78)
- jdk 64位下载 (65)
- phpstudy 2013 (66)
- 卡通形象生成 (55)
- psd模板免费下载 (67)
- shift (58)
- localhost打不开 (58)
- 检测代理服务器设置 (55)
- frequency (66)
- indesign教程 (55)
- 运行命令大全 (61)
- ping exe (64)
本文暂时没有评论,来添加一个吧(●'◡'●)