从“生成”的这个事实来看,我们可以猜想客户端程序可能本身绑有一段自定义的二进制数据,这段二进制数据实质上就是一个服务端的模板。在用户进行了设置工作之后,客户端就会将设置完成的这些特定数据填充到这个模板之中,然后再通过写文件的操作将这段已经配置完毕的二进制数据模板生成一个特定的服务端程序,如下图所示:
打个比方来说,邮局有很多空白的信封。你在买下一个信封之后,在信封上填写邮政编码、收件人地址和姓名,然后再将你的信件装入信封糊好,这就成为了你所特有的一封信件了。——希望我这么解释能够让你明白我下面将要在操作的大致过程。
下面我要实现一个名为“MsgBox生成器”的演示程序,你可以在客户端进行MessageBox的标题和文本的设置,然后程序就会在C:\下生成一个名为MsgBox.exe的“Hello, World”程序,这个弹出的MessageBox就是你先前在客户端所设置的那样。运行界面如下图:
好了,那么我先来设计生成服务端的功能。为了十足地模仿木马程序,我使用Win32 ASM来编写服务端程序,代码如下:
.386
.model flat, stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
.da
szTitle db "Hello", 0
szText db "Hello, World", 0
.co
start:
invoke MessageBox, NULL, addr szText, addr szTitle, MB_OK or MB_ICONINFORMATION
invoke ExitProcess, NULL
end start
如你所见,这正是Iczelion在他的Win32汇编教程中编写的那个“Hello, World”,你可以利用MASM32将这段源代码编译链接,生成一个MsgBox.exe,在此我就利用这个程序来充当我要生成的服务端。
现在需要考虑的问题是,如何将这个MsgBox.exe嵌入到客户端程序中?一个最好想,也是最直观的办法是利用一个BYTE数组来放置MsgBox.exe的二进制数据(CIH病毒就是这样,它专门准备了一个PE文件头),然后再使用API函数CreateFile将这段二进制数据写成文件。部分代码如下:
BYTE bySrvData[] = {
0x4d, 0x5a, 0x90, 0x00, 0x03, 0x00, 0x00, 0x00,
0x04, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00,
0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 由于篇幅略去以下所有数据-_-#...
};
BOOL CreateServer()
{
HANDLE hFile;
DWORD dwWritten;
hFile = CreateFile( "C:\\MsgBox.exe", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL );
if( hFile == NULL )
return
FALSE;
WriteFile( hFile, (LPCVOID)bySrvData,
sizeof( bySrvData ), &dwWritten, NULL );
CloseHandle( hFile );
return
TRUE;
}
好了,代码到此为止。虽然它可以胜利完成任务,但是你一定不愿意编写这样的代码——对于MsgBox.exe的数据部分,它实在太冗长了(我算了一笔账,对于编译完成的MsgBox.exe,它只有不到3KB的大小,但是如果把它的二进制数据像上面那样保存成文本文件,这个文本文件就会拥有15KB的体积)。况且,这仅仅是一个“Hello, World!”程序,那么换成真正的木马服务端程序又会怎么样呢?而且,很多的服务端是用高级语言编写的,它们动辄会拥有几十KB的体积!
现在我们只能别无选择地换一个角度考虑如何来解决这个问题:除了用BYTE数组,还有别的方法可以将MsgBox.exe放入客户端程序中吗?
答案是肯定的,那就是利用EXE文件的自定义资源。在解说如何使用自定义资源之前,我先大体介绍一下我要使用的几个API:
FindResource:查找一个资源。由于我们是要将这个服务端作为EXE的自定义资源形式放在EXE之中,所以在释放它之前必须首先找到它才可以。
SizeofResource:获得资源的尺寸,装载资源时使用。
LoadResource:装载资源,将资源的二进制数据装载到内存中。
LockResource:锁定资源,将内存中的资源数据锁定。
好了,你可以通过查阅MSDN来了解这几个函数的详细功能及参数,这里我就不赘述了。我的整个思路如下:
将MsgBox.exe作为客户端程序的二进制资源一同编译。
在生成MsgBox.exe服务端的时候,使用以上的几个API函数读取这一段二进制资源数据。
将这段二进制数据保存为文件。
现在我来实现第1步。首先你将MsgBox.exe改名为MsgBox.bin作为一个二进制文件,并将这个文件放入客户端源代码的文件夹下。然后,向客户端的资源脚本(.rc文件)中导入这个二进制资源,如下图所示:
这时候,VC会弹出一个对话框提示,如下图:
你可以在“Resource Type”中随意填写你的资源类型,这个类型名称就是我们将要在FindResource函数的第三个参数lpType中使用的资源类型,我这里以“Server”为例。
这样,我就能够以资源的方式来使用这段数据了。我的代码如下:
BOOL CreateServer()
{
HRSRC hResInfo;
HGLOBAL hResData;
DWORD dwSize, dwWritten;
HANDLE hFile;
// 查找所需的资源
hResInfo = FindResource( NULL, MAKEINTRESOURCE(IDR_SERVER), "Server" );
if( hResInfo == NULL )
return
FALSE;
// 获得资源尺寸
dwSize = SizeofResource( NULL, hResInfo );
// 装载资源
hResData = LoadResource( NULL, hResInfo );
if( hResData == NULL )
return
FALSE;
// 写文件
hFile = CreateFile( "C:\\MsgBox.exe", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL );
if( hFile == NULL )
return
false;
WriteFile( hFile, (LPCVOID)LockResource( hResData ), dwSize, &dwWritten, NULL );
CloseHandle( hFile );
return
TRUE;
}
到这里,如何生成服务端的问题就已经解决了,现在我来讨论如何自定义服务端的数据。我还是以一个例子来解释这个问题,下面是一段代码,也就是一个MessageBox的调用过程:
TCHAR strContext[] = "Hello, World!";
TCHAR strTitle[] = "Hello";
MessageBox( NULL, strContext, strTitle, MB_OK );
以上代码相对应的汇编代码为:
push 0
; 最后一个参数MB_OK
lea eax,[ebp-18h]
push eax
; 第3个参数strTitle
lea ecx,[ebp-10h]
push ecx ;
第2个参数strContext
push 0
; 第1个参数NULL
call dword ptr [__imp__MessageBoxA@16 (0042a2ac)]
; 函数调用
也就是说,MessageBox的调用过程是传入了两个字符串地址,那么我们只需要将字符串所指向的内容进行改变,就可以达到修改MessageBox显示结果的目的了。
那么,又如何来确定在MsgBox.exe中那两个字符串的位置呢?在这里,我玩弄了一个小把戏,就是将原来MsgBox.asm的.da
.da
szTitle db 100 dup('A')
szText db 100 dup('B')
你也许会对这段代码颇为不解。不过我还是请你先按照上面的步骤将这个新的源文件编译链接,然后再改名为MsgBox.bin,最后导入到客户端的资源中。现在,你可以打开那个二进制资源MsgBox.bin看一看,相信你一定会在某个位置找到像下面这样的东西:
这样一来,你就会很容易地发现,标题的相对偏移量是0x800,文本的相对偏移量是0x864——这也就是我定义那一连串'A'和'B'的用意了。
现在,就可以编写我在本文开始所提到的“MsgBox生成器”了,它的核心代码如下:
HRSRC hResInfo;
HGLOBAL hResData;
DWORD dwSize, dwWritten;
LPBYTE p;
HANDLE hFile;
TCHAR szTitle[100], szText[100];
// 查找所需的资源
hResInfo = FindResource( NULL, MAKEINTRESOURCE( IDR_SERVER ), "Server" );
if( hResInfo == NULL )
{
MessageBox( hDlg, "查找资源失败!", "错误", MB_OK | MB_ICONINFORMATION );
break
;
}
// 获得资源尺寸
dwSize = SizeofResource( NULL, hResInfo );
// 装载资源
hResData = LoadResource( NULL, hResInfo );
if( hResData == NULL )
{
MessageBox( hDlg, "装载资源失败!", "错误", MB_OK | MB_ICONINFORMATION );
break
;
}
// 为数据分配空间
p = (LPBYTE)GlobalAlloc( GPTR, dwSize );
if( p == NULL )
{
MessageBox( hDlg, "分配内存失败!", "错误", MB_OK | MB_ICONINFORMATION );
break
;
}
// 复制资源数据
CopyMemory( (LPVOID)p, (LPCVOID)LockResource( hResData ), dwSize );
// 获取标题和文本,并复制数据
GetDlgItemText( hDlg, IDC_EDT_TITLE, szTitle, 100 );
GetDlgItemText(hDlg, IDC_EDT_TEXT, szText, 100);
CopyMemory( (LPVOID)( p + 0x800 ), (LPCVOID)szTitle, 100 );
CopyMemory( (LPVOID)( p + 0x864 ), (LPCVOID)szText, 100 );
// 创建文件,写数据
hFile = CreateFile( "C:\\MsgBox.exe", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL );
if( hFile != NULL )
WriteFile( hFile, (LPCVOID)p, dwSize, &dwWritten, NULL );
else
{
MessageBox( hDlg, "创建文件失败!", "错误", MB_OK | MB_ICONINFORMATION );
GlobalFree( (HGLOBAL)p );
break
;
}
// 收尾工作,释放资源
CloseHandle( hFile );
GlobalFree( (HGLOBAL)p );
到这里,这一技术的核心部分基本上就已经讲完了。对于一个实际的木马程序服务端而言,你只需要把MsgBox的标题和文本替换为相应的端口号和邮件地址就可以了。另外,对于用C/C++、Delphi等高级语言所编写的服务端模板,它的尺寸肯定会大大超过一个汇编写成的程序,这样的话查找要替换的字符串或者数值就肯定会花些时间了。
本文配套代码用SDK编写,在Windows XP Professional + VC 6.0下编译通过。