65.9K
CodeProject 正在变化。 阅读更多。
Home

远程管理和监控系统入门

starIconstarIconstarIconstarIconstarIcon

5.00/5 (10投票s)

2009年10月21日

CPOL

18分钟阅读

viewsIcon

55134

downloadIcon

2472

远程管理和监控系统入门

Project Poster

引言

#define ARTICLE_STATUS "0.03%"

远程管理是指从远程位置控制计算机的任何方法。

允许远程管理的软件越来越普遍,通常在难以或不切实际地近距离使用系统,或者为了访问本地无法获得的 Web 内容(例如,从英国境外观看 BBC iPlayer)时使用。远程位置可以指隔壁房间的计算机,也可以指地球另一端的计算机。它还可以指合法的和非法的远程管理。

*** 维基百科

我们又见面了,是时候深入探讨比远程 shell 更高级的内容了。

在本文中,我将展示其结构的一种实现方式及其功能的一个小示例。那么,我们跳过不必要的叙述,离主题更近一步。

*** cross

引言和先决条件

要理解这里将要讨论的全部内容,你需要对以下方面有一些基本了解:

  • Linux 和 Windows 系统
  • Windows 和 Linux 的 C/C++ 编程
  • Perl 脚本语言
  • Windows 内核模式驱动程序开发
  • 网络编程
  • 编程内核模式网络客户端 / Winsock Kernel
  • Gtk+ GUI 编程,或
  • C# .NET 编程
  • Windows 内核模式挂钩

我使用了这些工具,并且,我并不强迫你一步一步地跟着我走——我们的主要任务可以用不同的方式完成。

  • Windows Driver Kit 版本 >= 6
  • 物理 Linux 机,配有 gcc 编译器和 Gtk+ GUI(开发+运行时)库,设置良好。
  • 物理 Windows 机,配有 Visual Studio Pro 2008 和 Perl 解释器。
  • VMWare workstation / server
  • 虚拟 Linux 系统,配有 Perl 解释器。
  • 虚拟 Windows 系统(Vista / Server 2008 / 7)
  • ... 某种驱动程序加载器。

最终的“现实生活般”的测试将需要这个。但是,你可以将所有这些打包并混合在一个 Windows 系统(Vista / Server 2008 / 7)中。开始之前有几句话。这个项目——不是什么大而可怕的东西。我曾计划将其做得大 4 倍,但由于许多原因而放弃了。例如,时间不足。如今,我们都生活节奏快,想做很多事情,想学习,想获得更多知识——生命是不够的。在这里,我将展示一个“骨架”,如果你觉得它很有趣、有用,如果它给你带来了一些新想法——那么你就有了一个很好的起点,可以将其做大。另一件事是,远程管理系统通常与恶意软件、例如木马、僵尸网络等相关联。你如何使用我的代码——这取决于你。你可以从中做出合法的东西,而且有很多合法的软件使用相同的功能。也有许多僵尸网络的例子。

虽然“僵尸网络”一词可以指任何一组机器人,例如 IRC 机器人,但通常是指一组受感染的计算机(称为僵尸计算机),它们运行着软件,通常通过利用 Web 浏览器漏洞的“即点即毁”下载、蠕虫、特洛伊木马或后门安装,并在一个通用的命令与控制基础设施下运行。

*** 维基百科

让我们谨慎行事。;)

结构

Structure

嗯,整个东西将有一个“三角形”结构。考虑以下组件:

  • 主控机——我们将从中建立控制的机器。
  • 服务器机——我们将在其中放置我们的服务器(稍后解释这一点)。
  • 客户端机——我们将控制/管理其的机器。
主控机 <---> 服务器机 <---> 客户端机

为什么采用这种结构?假设我正在实时远程管理 100 台机器,无论如何,数量可能会更多。我想知道(或者我需要知道)每台机器上发生了什么。最简单的方法是强制每台机器将状态信息发送到我的家庭电脑。但是,如果我可能没有足够的带宽,或者我的流量有限,而且并非每条消息都至关重要,都需要我的注意——有些消息我可以稍后查看,而且我只想接收实时关键报告呢?为了避免大量“垃圾”流量涌入我的个人网络,我将实施“中间服务器”,即在我与客户端机之间放置一个服务器,由它来决定发送什么给我。

我希望你明白了。那么,我将如何编写所有这些?首先,如果我们要做管理/控制某事,我们需要一个漂亮的图形用户界面。伙计,我可不想处理控制台应用程序,绝对不行。这里我们需要运用我们创建 GUI 界面的得心应手的技能。其次,我们有哪些操作系统?Linux?Windows?嗯,我都有。假设我在 Linux 上工作——我将使用 Gtk+ 作为我的管理控制台的 GUI 库;让我们现在给它命名,例如 R-manager。你喜欢它的新名字吗?;) 如果愿意,你可以使用其他的。好了,现在清楚了。但是那些在 Windows 机器上工作的系统管理员呢?我们不要忘记他们,并为他们编写另一个版本的 R-manager,这次我们将使用 C# .NET。或者你可以编译 Gtk+ 版本在 Windows 上运行——你只需要库。所以我们必须编写 2 个版本的 R-manager,很清楚。

工作名称:R-manager。

那么“中间服务器”呢?顺便说一句——这就是它的名字,我的朋友们,SITM Server。我为这个任务选择了 Perl 脚本语言。前面我说过你可以只使用一个操作系统来测试这个项目,我并没有撒谎。你也可以在 Windows 机器上运行这个服务器——只需要在 Windows 机器上安装 Perl 解释器就可以了!哦,有一件事将不可用——服务器不会作为守护进程运行(在 Windows 术语中是服务),除非你能设置 Daemon Perl 模块。好的。

工作名称:SITM Server。

现在是客户端机。我们的客户端机运行 Windows server 2008。现在我们必须编写一个应用程序,为我们提供对这台机器的某些控制。我本可以使用 Win32 API 来完成,编写一个简单的 ring3 应用程序,那又如何呢?有什么乐趣呢?无聊,我无聊死了……因此,我的客户端应用程序将是一个内核模式网络驱动程序。实际上,这更好!当我们处于内核模式时,我们可以获得更多的乐趣,并且可以完全控制客户端机。这是一个练习 Winsock Kernel, WSK——微软新的 NPI 的好机会。

工作名称:R-client。

好的,我们知道要做什么。我们知道这是一项简单的任务。我们这样做是为了好玩和练习。这里的例子将代表最少的功能,基本的东西,可以说是一个骨架。让我们进入下一章。

R-client

那么,我们将实现什么功能呢?让我们做一些简单的,如下所示:R-client(驱动程序)初始化后,它将等待我们的“命令”来保护自己不被卸载,方法是设置对单个函数的挂钩——NtUnloadDriver。如果用户尝试使用上述函数卸载我们的驱动程序——R-client 将向我们发送一条消息,告知卸载尝试。很简单,不是吗?然后我们将编写这样一个小程序并实际测试它。所以我们的驱动程序将接收我们的消息并发送消息给我们。我的意思是,发送给 SITM Server。在某些情况下,它将使用简单的 rot47 密码加密出站消息并解密入站消息。它将为此使用 UDP 协议,并且将同时充当客户端和服务器。最后,它将挂钩NtUnloadDriver。现在我们对我们的 R-client 驱动程序有了完整的了解。

  • 基于网络的通信系统
  • 加密/解密
  • 内核模式挂钩

让我们从最后一个开始。

*** NtUnloadDriver 内核模式挂钩 ***

我们将通过在原始NtUnloadDriver函数入口点插入一个无条件 5 字节的跳转来实现这一点,这将导致NtUnloadDriver的执行重定向到我们的函数,假设是:NewNtUnloadDriver。然后我们的NewNtUnloadDriver将决定下一步做什么。我不会深入细节,假设你具备所需的知识,但让我们澄清一些事情。
我们的函数非常简单。

NTSTATUS __stdcall NewZwUnloadDriver(IN PUNICODE_STRING DriverServiceName){
	WCHAR Path[1024];
	RtlZeroMemory(Path, sizeof(Path)); 		// here we will
					 	// reg path of driver service
	swprintf(Path, L"%wZ", DriverServiceName); 	// store it actually
	if(wcscmp(Path,
	  L"\\Registry\\Machine\\SYSTEM\\CurrentControlSet\\Services\\test.sys") == 0){
	//^ if registry path  = our driver path ->
         //access denied and send message to server
	DbgPrint("R-client: Access denied!\n");
	SendMessage("rcli!Something tried to unload me");
	return STATUS_ACCESS_DENIED;
	} else { // just execute original NtUnloadDriver
	DbgPrint("DriverServiceName: %wZ", DriverServiceName);
	return OriginalZwUnloadDriver(DriverServiceName); // jump to original
					//entry point stored in call buffer
	}
}

通过阅读代码中的注释,你可以看到它是如何工作的。我将把进一步的解释分成以下几点:

  • 识别原始函数结构
  • 之前的机器码
  • 挂钩实现
  • 之后的机器码
  • 结论

1. NtUnloadDriver C 代码

NTSTATUS NTAPI NtUnloadDriver (IN PUNICODE_STRING DriverServiceName){
return IopUnloadDriver(DriverServiceName, FALSE);
}

2. 之前的机器码

Machine code before

3. 挂钩实现

==> 定位“jmp”插入空间
==> 计算 Callgate
==> 内存分配
==> 保存原始函数地址
==> 插入跳转到我们的新函数
=> 禁用内核内存保护
=> 插入“jmp”
=> 启用内存保护

为了完成这项任务,我们使用了 Length-Disassembler Engine(by (C)ZOMbiE,包含在项目中)。

// locating place to set our 5-byte jump

while (CollectedSpace < 5){
	GetInstLenght(Inst, &Size); // getting instructions length 
				// thanks ZOMbiE's engine
	(unsigned long)Inst += Size; // next instruction
	CollectedSpace += Size; // update space
}

// memory allocation
CallGateSize = CollectedSpace + 5;
CallGate = (void *)ExAllocatePool(NonPagedPool, CallGateSize);

// clear memory with nopes (0x90)

memset(CallGate, 0x90, CallGateSize);
memcpy(CallGate, Addr, CollectedSpace);
memset(Addr, 0x90, CollectedSpace);

// generate jump

*((unsigned long *)(((unsigned long)CallGate + CollectedSpace) + 0x1)) =
(((unsigned long)Addr + 0x5) - 	((unsigned long)CallGate + CollectedSpace) - 0x5);
*((unsigned char *)((unsigned long)CallGate + CollectedSpace)) = 0xe9;
*((unsigned long *)(((unsigned long)Addr) + 0x1)) = (((unsigned long)NewFunc) -
((unsigned long)Addr) 	- 0x5);
*((unsigned char *)((unsigned long)Addr)) = 0xe9;

Typedefs

typedef NTSTATUS (* _ZwUnloadDriver)(IN PUNICODE_STRING DriverServiceName);
_ZwUnloadDriver OriginalZwUnloadDriver;

最后一步

// disable kernel memory protection

__asm
{
	cli
	mov		eax,cr0
	mov		CR0Reg,eax
	and		eax,0xFFFEFFFF
	mov		cr0,eax
}

// Hook

OriginalZwUnloadDriver =
(_ZwUnloadDriver)SetHook(
(PVOID)KeServiceDescriptorTable.ServiceTableBase[*(PULONG)((ULONG)(ZwUnloadDriver)+1)],
NewZwUnloadDriver
);

// Enable kernel memory protection

__asm
{
	mov		eax,CR0Reg
	mov		cr0,eax
	sti
}

4. 之后的机器码

Machine code after

5. 结论

最后,我们有了原始函数的地址,并且可以随时从我们的NewUnloadDriver跳转到它。有了这个模板,以及 ZOMbiE 的反汇编引擎,你就可以实现自己的内核模式挂钩。你可以隐藏文件、文件夹,禁止访问任何地方,以你想要的方式操纵远程系统。我选择在这个项目中实现这种技术,因为它最适合展示我们如何创建简单的监控例程。在本文包含的源文件中,你可以找到一个测试工具,它会尝试使用NtUnloadDriver卸载驱动程序,所以你只需要编译它并进行测试。总的来说,它看起来是这样的:

int main(){
    BOOL en;
    UNICODE_STRING u_str;
    WCHAR RegUniPath[MAX_PATH] =
	L"\\Registry\\Machine\\SYSTEM\\CurrentControlSet\\Services\\test.sys";
    NtFunctionsInit();
    RtlAdjustPrivilege(10, TRUE, AdjustCurrentProcess, &en);
    RtlInitUnicodeString(&u_str, RegUniPath);
    if(NtUnloadDriver(&u_str) != STATUS_SUCCESS)MessageBox(0, "Unable to unload driver!",
	"ERROR", 	MB_ICONERROR);
    return 0;
}
*** 加密/解密 ***

嗯,这几乎是我们项目的一个不必要的部分,因为还有很多东西没有展示,但是……

char *code_rot(char *cod_dec){
    char *p = cod_dec;

while(*p) {
        if(*p >= '!' && *p <= 'O')
        *p = ((*p + 47) % 127);
        else if(*p >= 'P' && *p <= '~')
        *p = ((*p - 47) % 127);
        p++;
    }
return cod_dec;
}

很简单,我的朋友们。:)

*** 基于网络的通信系统 ***

Winsock Kernel (WSK) 是一个内核模式网络编程接口 (NPI)。通过 WSK,内核模式软件模块可以使用与用户模式 Winsock2 支持的相同套接字编程概念执行网络 I/O 操作。WSK NPI 支持熟悉的套接字操作,如套接字创建、绑定、连接建立和数据传输(发送和接收)。然而,尽管 WSK NPI 支持与用户模式 Winsock2 大部分相同的套接字编程概念,但它是一个全新的、不同的接口,具有独特的功能,例如使用 IRP 和事件回调来提高性能的异步 I/O。

*** MSDN

作为介绍的一部分,请允许我介绍一个由 (C)MaD 提供的小型框架,它由以下函数包装器组成,使内核套接字编程比以往任何时候都更容易。

NTSTATUS NTAPI SocketsInit();
VOID NTAPI SocketsDeinit();

PWSK_SOCKET NTAPI
CreateSocket(
    __in ADDRESS_FAMILY	AddressFamily,
    __in USHORT			SocketType,
    __in ULONG			Protocol,
    __in ULONG			Flags
    );

NTSTATUS NTAPI
CloseSocket(
	__in PWSK_SOCKET WskSocket
	);

NTSTATUS NTAPI
Connect(
	__in PWSK_SOCKET	WskSocket,
	__in PSOCKADDR		RemoteAddress
	);

PWSK_SOCKET NTAPI
SocketConnect(
	__in USHORT		SocketType,
	__in ULONG		Protocol,
	__in PSOCKADDR	RemoteAddress,
	__in PSOCKADDR	LocalAddress
	);

LONG NTAPI
Send(
	__in PWSK_SOCKET	WskSocket,
	__in PVOID			Buffer,
	__in ULONG			BufferSize,
	__in ULONG			Flags
	);

LONG NTAPI
SendTo(
	__in PWSK_SOCKET	WskSocket,
	__in PVOID			Buffer,
	__in ULONG			BufferSize,
	__in_opt PSOCKADDR	RemoteAddress
	);

LONG NTAPI
Receive(
	__in  PWSK_SOCKET	WskSocket,
	__out PVOID			Buffer,
	__in  ULONG			BufferSize,
	__in  ULONG			Flags
	);

LONG NTAPI
ReceiveFrom(
	__in  PWSK_SOCKET	WskSocket,
	__out PVOID			Buffer,
	__in  ULONG			BufferSize,
	__out_opt PSOCKADDR	RemoteAddress,
	__out_opt PULONG	ControlFlags
	);

NTSTATUS NTAPI
Bind(
	__in PWSK_SOCKET	WskSocket,
	__in PSOCKADDR		LocalAddress
	);

PWSK_SOCKET NTAPI
Accept(
	__in PWSK_SOCKET	WskSocket,
	__out_opt PSOCKADDR	LocalAddress,
	__out_opt PSOCKADDR	RemoteAddress
   );

我们将不会在这里深入探讨 Winsock Kernel 的最深层方面,因为它不是重点。要在我们的示例中实现任何类型的面向连接的套接字使用 WSK,我们首先需要:

  • 注册 winsock 内核应用程序
  • 创建套接字
  • 将套接字绑定到本地传输地址

R-Client 在网络通信中使用 UDP 协议与 SITM Server 通信,那么我们的套接字将是一个数据报套接字。绑定到本地地址后,驱动程序会创建一个系统线程,该线程在循环中接收数据报,并根据来自服务器的传入消息执行某些操作。例如:1.收到消息“hi”--->发送消息“Hello :)”。最后让我们看一些代码。;)

// Initialization of networking

NTSTATUS InitNetworking(){
NTSTATUS	Status = STATUS_UNSUCCESSFUL;
SOCKADDR_IN LocalAddress;

Status = SocketsInit(); // socket init
if (!NT_SUCCESS(Status)) {	// if failed
	DbgPrint("SocketsInit() failed with status 0x%08X\n", Status);
	PsTerminateSystemThread(Status);
} else DbgPrint("Kernel Sockets Initialized successfully!\n");

g_ServerSocket = CreateSocket
    (AF_INET, SOCK_DGRAM, IPPROTO_UDP, WSK_FLAG_DATAGRAM_SOCKET); // socket creation
if (g_ServerSocket == NULL) {
	DbgPrint("CreateSocket() returned NULL\n");
	PsTerminateSystemThread(Status);
} else DbgPrint ("Socket created!\n");

// just like Winsock2 API
LocalAddress.sin_family		= AF_INET;
LocalAddress.sin_addr.s_addr	= inet_addr(RCLI_ADDR);
LocalAddress.sin_port		= HTONS(SERVER_PORT);

Status = Bind(g_ServerSocket, (PSOCKADDR)&LocalAddress); // binding to local address
if (!NT_SUCCESS(Status)) { // if failed ...
	DbgPrint("Bind() failed with status 0x%08X\n", Status);
	CloseSocket(g_ServerSocket);
	g_ServerSocket = NULL;
	PsTerminateSystemThread(Status);
} else DbgPrint("Binded to local address...\n");

return STATUS_SUCCESS;
}

注意* g_ServerSocket是我们全局变量,现在是我们崭新闪亮的、已初始化的数据报套接字,供以后使用。就这样!现在 R-Client 驱动程序可以使用g_ServerSocket接收和发送消息,直到它关闭。最后,需要一些新鲜空气。让我们进入网络……

NTSTATUS SendMessage(char *message){
	NTSTATUS	Status = STATUS_UNSUCCESSFUL;
	SOCKADDR_IN ServerAddr, LocalAddress;
	char encoded[1024];
	RtlZeroMemory(encoded, sizeof(encoded));
	strcpy(encoded, message);
	code_rot(encoded);	// encrypting message with rot47 cipher, just for example

	ServerAddr.sin_family		= AF_INET;
	ServerAddr.sin_addr.s_addr	= inet_addr(SERVER_ADDR);	// address
	ServerAddr.sin_port		= HTONS(6666); // SITM Server port

	Status = SendTo(g_ServerSocket, encoded,
			strlen(encoded), (PSOCKADDR)&ServerAddr);
	if (!NT_SUCCESS(Status)) {
		DbgPrint("Could not send data! Status 0x%08X\n", Status);
		PsTerminateSystemThread(Status);
	} else DbgPrint("Data sent!\n");

	return STATUS_SUCCESS;
	}

static VOID UdpServer(PVOID Context){
	..................
	SendMessage(status); // datagram, sent here, contains some initial information
	// based on which SITM decides if our machine is
         // already registered in a system or not,
	// it contains binded R-Client's port, etc, etc,
         // whatever information we would like to send
	// during startup
	........................................
	......................................
	while(1){
	Status = ReceiveFrom(g_ServerSocket, response, 1024, NULL, NULL);
	// a loop
	if(strcmp(response, "hook") == 0){ // if we got order to setup hook
	DbgPrint("Setting up hooks...\n");
	KeDelayExecutionThread(KernelMode,FALSE,&Interval ); // take a breath
	    SetHooks();	// set it
		SendMessage("rcli!NtUnloadDriver Hooked"); // send message
		} else if(strcmp(response, "hi") == 0){  // just say hi to master :)
		KeDelayExecutionThread(KernelMode,FALSE,&Interval );
		SendMessage("rcli!Hello :)"); // send
		}
		..............
	}
	.................

你可能会问关于消息开头的“rcli!”。嗯,这会告知服务器消息来自 R-Client,感叹号是分隔符,其余部分是实际消息。

__Begin: (Receive ==> Process ==> Send) goto __Begin;

呼,完成了。R-Client 的解释达到了最后一步。

#undef ARTICLE_STATUS #define ARTICLE_STATUS "43%"

中间服务器

Perl 是一种高级、通用、解释型、动态编程语言。Perl 最初由 Larry Wall(一位以系统管理员身份为 NASA 工作,精通语言学的人)于 1987 年开发,作为一种通用 Unix 脚本语言,旨在简化报告处理。此后,它经历了许多变化和修订,并在程序员中广受欢迎。Larry Wall 继续监督核心语言的开发,以及其即将推出的版本 Perl 6。Perl 借鉴了 C、shell 脚本、AWK 和 sed 等其他编程语言的特性。该语言提供了强大的文本处理功能,并且不受许多当代 Unix 工具的任意数据长度限制,便于轻松处理文本文件。它还用于图形编程、系统管理、网络编程、需要数据库访问的应用程序以及 Web 上的 CGI 编程。由于其灵活性和适应性,Perl 被昵称为“编程语言的瑞士军刀”。

*** 维基百科

我认为这个描述不言而喻,并解释了选择 Perl 作为即将到来的任务的编程语言的决定。需要立即编写一个快速稳定的脚本——Perl 是你的朋友。Perl 是我的第一门编程语言,它一直陪伴我至今,直到我死去。

print " Hi there :) \n"; # 我们很棒!

现在,集中注意力。我们不会在这个脚本上浪费太多时间,因为我们想要它快速,并且我们想要它立刻对吧?我也不再重复它的任务。首先,在 R-Client 的初始消息出现后,SITM Server 会在 MYSQL 数据库中检查——是否有关于刚刚连接的远程机的记录。如果是一台机器,我的意思是,如果只有一台远程机器在监控之下——我认为没有必要玩数据库。但如果我们谈论的是 100/1000 台机器,甚至 10 台——我认为有必要。嗯,我的问题是,你已经创建了你的数据库了吗?

	my $dbhost = "localhost";
	my $dbuser = "root";
	my $dbpass = "";
	my $table = "machines";
	my $db = "project";

sub SetDB {
	my $nodb = undef;
	my $dbh_create = DBI->connect("dbi:mysql:$nodb:$dbhost",$dbuser,$dbpass) ;
						# connect to database
	my $sql_create = "create database $db";	#query to execute
	my $sth_create = $dbh_create->prepare($sql_create); # prepare query
	$sth_create->execute or die "MYSQL error while creating Database!
					($DBI::errstr)\n"; # execute it or die
	# we are not going anywhere without our database
	my $dbh = DBI->connect("dbi:mysql:$db:$dbhost",$dbuser,$dbpass) ;


	my $sql = "create table `$table` (".
				  "`id` int(10) not null auto_increment,".
				  "`ip_as_name` varchar(150) not null default '',".
				  "`port` varchar(50) not null default '',".
				  " primary key(`id`),".
				  " unique key `id`(`id`)".
				  ") type=MyISAM comment='' AUTO_INCREMENT=1 ;";
	my $sth = $dbh->prepare($sql); # prepare table creation query

	$sth->execute or die "MYSQL error while creating table!
				($DBI::errstr)\n"; # execute it or die
	#not going anywhere neither if the above function failed
	print "MYSQL Database setup finished\nDetailes:\nDatabase Host:
					$dbhost\nDatabase User: $dbuser\
	Database Password: $dbpass\nTable: $table\n"; # print some stats
	return "DBCREATED"; # return some value, in some case we may need it later
}

这些是我的默认设置,默认字段和行。我们还远未完成。

sub FetchMachines {
	my ($ip_addr) = @_;  # that is IN parameter
	my $online;
	my $container; my $num; my $i;
	$container .= "machines$J";
	my $dbh = DBI->connect("dbi:mysql:$db:$dbhost",$dbuser,$dbpass) ;
	my $sq = "select count(*) from $table";
	my $st = $dbh->prepare($sq);
	$st->execute or print("MYSQL error while fetching machines list!
							($DBI::errstr)");
	while (@row = $st->fetchrow_array){
	$container .= $row[$i++];
	}

	my $sql = "select * from $table";
	my $sth = $dbh->prepare($sql);
	$sth->execute or print("MYSQL error while fetching machines list!
							($DBI::errstr)");
	$container .= "$J"; # $J is a splitter '!'
	while (@row = $sth->fetchrow_array) {
	$container .= $row[1].$online;
	$container .= "$J";
	}
	&SendStatus ( "mcs", $J, "$container", $ip_addr, $SEND_PORT);
	# $SEND_PORT – is a binded port of administrator's machine
}

上述例程执行一个简单的任务:

  • 从数据库获取机器
  • 获取数据库中的机器数量
  • 注意*:在我们的例子中,我们一直在谈论一台中单一的机器;)

然后它将收集到的数据发送给……例如,我。

sub AddMachine {
	my ($ip_as_name, $port) = @_; # input parameters
	my $trash = "Server~#";
	my $add_err = "($DBI::errstr) while registering new machine!($ip_as_name)";
	my $check_err = "MYSQL error ($DBI::errstr)
			while checking new machine ($ip_as_name)!";
	print "registering ".$ip_as_name."\n";
	my $dbh = DBI->connect("dbi:mysql:$db:$dbhost",$dbuser,$dbpass) ;
	my $sql_ex = "select * from $table where ip_as_name = '$ip_as_name'";
	my $sth_ex = $dbh->prepare($sql_ex);
	$sth_ex->execute or &SendStatus("nfo", $J, "$trash$J$check_err", $MASTER_IP);
	if($sth_ex->rows == 1){
		print "This machine is registered!\n".$sth_ex->rows."\n";
		return "here";
	} else {
	my $sql = "insert into $table (ip_as_name, port) values
						('$ip_as_name', '$port')";
	my $sth = $dbh->prepare($sql);
	$sth->execute or &SendStatus( "nfo", $J,
		"$trash$J$add_err", $MASTER_IP) and return "error";
	return "registered";
	}
}

这个程序将机器添加到数据库并检查机器是否已存储在数据库中。那么那个神秘的“SendStatus”有什么作用呢?给你。

sub SendStatus {
	my($header, $splitter, $message, $ipaddress, $port) = @_; # Input params
	socket(SOCKET, PF_INET, SOCK_DGRAM, $proto) or print
					"Cannot create socket!\n";
	#^ socket creation
	$ipaddr   = inet_aton($ipaddress); # ip addr
	$portaddr = sockaddr_in($port, $ipaddr); # port and ip
	my $msg = "$header$splitter$message";	# combined message
	send(SOCKET, $msg, 0, $portaddr); # send data
	shutdown(SOCKET, 0);                # I/we have stopped reading data
	shutdown(SOCKET, 1);                # I/we have stopped writing data
	shutdown(SOCKET, 2);                # I/we have stopped using this socket
	print $msg."\n";
	return;
}

顺便说一句,如果我讲得太快,我很抱歉,但我假设你的知识水平足够高,可以立即理解代码,并且不关心注释……你准备好进行最后的冲刺了吗?在我们稍作休息并讨论几件事情之后。主要函数就在前方,它完成了所有工作。它从一台机器接收并发送到另一台机器,处理数据,总之,欢迎来到深渊;]开玩笑:)

sub UdpListener{
	my $response;
	my $dec;
	my $binded = 0;
	my $trash = "Server~#";
	my $rcli_sign = "R-Client~#";
	my $rcli_here = "reg_ok";
	my $rcli_regok = "registered";
	my $xXx = $gpp1[rand(@gpp1)]." ".$gpp2[rand(@gpp2)]."
					".$gpp3[rand(@gpp3)]."\n".$help;
	my $check_err = "Cannot execute mysql query
			while checking machine($remoteaddress)!\n";
	__init:
	$response = "";
	my $q=CGI->new();
	socket(SOCKET, PF_INET, SOCK_DGRAM, $proto) or print
				"Cannot create listening socket!\n";
	setsockopt(SOCKET, SOL_SOCKET, SO_REUSEADDR, 1) ;

	$ipaddr   = inet_aton($VMWareIp);
	$portaddr = sockaddr_in($LISTEN_PORT, $ipaddr);
	bind(SOCKET, $portaddr) or die "Could not bind! $!";
	if($binded == 0){
		print "Binded\n";
	} else {
		print "Rebinded\n";
	}
	my $remoteaddress = $q->remote_addr();
	while(1){
	$binded = 1;
	$dec = "";
	recv(SOCKET,$response,1000,0);
	$dec = rot47($response);
	print $dec."\n";
	@words = split(/$J/, $dec);

	if($words[0] eq "?"){
		print "Help information sent\n";
		&SendStatus("nfo", $J, "$trash$J$xXx", $MASTER_IP, $SEND_PORT);
		DestroySocket(SOCKET); goto __init;
		} elsif($words[0] eq "machine"){
		print $words[1]." ".$words[2]."\n";
		} elsif(index($words[0], "stats") > -1 ||
				index($words[0], "statistics") > -1){
		&FetchMachines($MASTER_IP);
		DestroySocket(SOCKET); goto __init;
		} elsif($words[0] eq "die"){
		&SendStatus("nfo", $J, "$trash$J$death", $MASTER_IP);
		DestroySocket(SOCKET);
		system("clear");
		die "\nMaster ordered me to die ;(\n";
		} elsif($words[0] eq "rcli"){
			if($words[1] eq "status"){
			my $MACH_PORT = $words[2];
			$MACHINE_ADDR = $words[3];
			my $dbh = DBI->connect
			("dbi:mysql:$db:$dbhost",$dbuser,$dbpass) ; my $data;
			my $sql = "select * from $table
				where ip_as_name = '$MACHINE_ADDR'";
			my $sth = $dbh->prepare($sql);
			$sth->execute or &SendStatus
				("nfo", $J, "$trash$J$check_err") and goto __init;
				if($sth->rows == 1){
				print "Machine ($MACHINE_ADDR) is here.\n";
				&SendStatus("", "", "$rcli_here",
						$MACHINE_ADDR, $MACH_PORT);
				DestroySocket(SOCKET); goto __init;
				} else {
				print "Machine ($MACHINE_ADDR)
					is not detected in our DB, adding...";
				my $status = AddMachine
					($MACHINE_ADDR, $MACH_PORT);
					if($status eq "registered"){
					&SendStatus("", "",
					"$rcli_regok", $MACHINE_ADDR, $MACH_PORT);
					DestroySocket(SOCKET); goto __init;
					} else {
					die "i dont want to check
						for any errors =/";
					}
				}
			} else {
				my $rcli_msg = $words[1];
				&SendStatus("nfo", $J,
				"$rcli_sign$J$rcli_msg\n", $MASTER_IP, $SEND_PORT);
				DestroySocket(SOCKET); goto __init;
			}
		} elsif($words[0] eq "2rcli"){
		my $MACHINE_ADDR = $words[1];
		my $rcli_msg = $words[2];
		&SendStatus("", "", "$rcli_msg", $MACHINE_ADDR, $MACHINE_PORT);
		DestroySocket(SOCKET); goto __init;
		} else {
		&SendStatus("nfo", $J, "$trash$J$whatever\n",
					$MASTER_IP, $SEND_PORT);
		DestroySocket(SOCKET); goto __init;
		}
	}
	}

停止。是的,就是这样……暂时 ;) 整个脚本使用了 3 个 Perl 模块:

  • Socket (use Socket;)
  • DBI (use DBI;)
  • CGI (use CGI;)

很容易注意到 Socket 模块在这里是最重要且最常用的。假设你熟悉至少 BSD 套接字——没有什么新鲜事。你可能对 DBI 模块以及它的作用有一些考虑。DBI 模块使你的 Perl 应用程序能够透明地访问多种数据库类型。你可以连接到 MySQL、MSSQL、Oracle、Informix、Sybase、ODBC 等,而无需了解每种数据库的底层接口。DBI 定义的 API 将适用于所有这些数据库类型以及更多类型。你可以同时连接到不同类型的多个数据库,并轻松地在它们之间移动数据。DBI 层允许你以简单而强大的方式做到这一点。

*** 来自 dbi.perl.org

简而言之,DBI 为你的应用程序提供了与不同类型数据库交互的支持。在我们的例子中——是 MYSQL 数据库。有 5 个主要函数:

  • connect——连接到数据库
  • prepare——准备 SQL 查询
  • execute——执行 SQL 查询
  • disconnect——断开与数据库的连接
  • do——在没有数据返回时更快地执行 SQL 查询的方法

示例

my $dbh = DBI->connect("dbi:mysql:$db:$dbhost",$dbuser,$dbpass) ;
my $sq = "select count(*) from $table";
my $st = $dbh->prepare($sq);
$st->execute;

SITM 服务器使用此模块在数据库中添加机器并将其保留在那里,也用于检查它是否存在。

while (@row = $sth->fetchrow_array) {
$container .= $row[1].$online;
$container .= "$J";
}

这个例子处理返回的数据。另一个模块是 CGI。CGI.pm 是一个稳定、完整且成熟的解决方案,用于处理和准备 HTTP 请求和响应。主要功能包括处理表单提交、文件上传、读取和写入 Cookie、查询字符串生成和操作,以及处理和准备 HTTP 标头。还包含一些 HTML 生成实用程序。CGI.pm 在普通的CGI.pm环境中性能非常好,并且内置支持 mod_perl 和 mod_perl2 以及 FastCGI。它拥有超过 10 年的开发和完善,汇集了数十位贡献者的意见,并已在数千个网站上部署。CGI.pm 自 Perl 5.4 版本以来一直包含在 Perl 分发版中,并已成为事实上的标准。

*** 来自 perldoc.perl.org

可以说,它在这里被初始化,但实际上并未被使用。

my $q=CGI->new();
my $remoteaddress = $q->remote_addr();

这两行代码负责检索已连接远程机的 IP 地址,但对于我们的项目——它们将什么都不做,因为它们不会获取 LAN IP 或虚拟子网 IP。你可以使用 CGI 来处理真实服务器,并且每个服务器都有一个外部 IP 地址。

@words = split(/$J/, $dec);

在这里,你可以观察到我之前谈到的关于分隔符和消息部分的事实。现在我们有 @words 数组中的令牌,例如,如果第一个令牌等于 rcli,脚本就知道应该将实际消息发送到主控 IP。如果令牌等于 2rcli——消息应该发送到远程客户端,依此类推。天哪,我需要抽根烟喝杯咖啡——我写这篇论文已经写了大约 20 个小时了,同时还要完成和测试项目本身。同时,享受我刚刚创建的这张图:)

my small art :)

图片由我、autodesk 3ds max、Adobe Photoshop、gimp 和 Windows 画图制作。
#undef ARTICLE_STATUS #define ARTICLE_STATUS "67%"

R-manager

R-Manager——我们的监控和控制应用程序,具有图形用户界面,位于我们的个人机器上,有 Linux 和 Windows 版本。好吧,是时候揭开面纱,向你展示它们的样子了。

*** R-Manager C# .NET Windows 版本 ***

R-Manager C# .NET Windows Edition

*** R-Manager Gtk+ Linux 版本 ***

R-Manager Gtk+ Linux Edition

相当简单的界面,功能最少,这使得新手更容易理解,也使得高级程序员更容易定制以满足自己的需求。好吧,我不会“重新发明轮子”,描述我在这两个示例中使用的每种编程语言的细节,而是指向两个主要的网络资源,你可以在其中找到完整的文档并从中学习:Microsoft Developers Network 和 Gnome Library。同样,这里的网络设计也是如此:我们的应用程序绑定数据报套接字,接收和发送消息。在上方的屏幕截图中,缺少了一个图形界面元素。一个网络初始化按钮。当我决定启动它时,我将把它留给你——只需点击按钮。对于 Gtk+ 版本——这个按钮叫做“Network”,对于 C# 版本——Initialize(红色);按钮会消失。让我们看看发生了什么。

Gtk+
int run_network_function(){
	gtk_widget_hide(GTK_WIDGET(run_network)); // here we are hiding button
	//===============================================
	// Creating udp listener
	pthread_t thread_id;	// pthread id
	thdata thread_data;	// pointer to thread structure
	CLEAR(thread_data.local_port); // ZeroMemory
	const char *LocalPort = gtk_entry_get_text
		(GTK_ENTRY(local_port_ent)); // get local port from entry
	// widget, by the way, Gtk+ Entry = C# TextBox
	strcpy(thread_data.local_port, LocalPort); // fill thread structure
	pthread_create (&thread_id, NULL, &UdpListener, &thread_data); // create thread
	//===============================================
}
C# .NET
private void Button_Click(object sender, System.EventArgs e)
        {
            Thread trd = new Thread
		(new ThreadStart(this.ThreadTask)); // initialize thread class
            trd.IsBackground = true;	// run in background
            trd.Start();	// start
            start_server.Hide();	// hide button
        }

让我们仔细看看每个线程中发生了什么。

Gtk+
void * UdpListener(void *ptr){
	thdata *data;
	data = (thdata *) ptr;
	char *t; int i;
	int sock, sockh;
	char message[10000];
	char *token[256];
	struct sockaddr_in our_addr, serv_addr;
	socklen_t length;
	char *machine_name;
	int machines_count;
	char xXx[1024];
	char new_line[] = "\n";
	bzero(&our_addr,sizeof(our_addr));
	our_addr.sin_family = AF_INET;
	our_addr.sin_addr.s_addr=htonl(INADDR_ANY);
	our_addr.sin_port=htons(atoi(data->local_port));
	sock=socket(AF_INET,SOCK_DGRAM,0);	// initialize Datagram socket
	bind(sock, (struct sockaddr *)&our_addr,
		sizeof(our_addr)); // bind to local address
	printf("udp server started!\n");
	while(1){ // one big loop
	length = sizeof(serv_addr);
	sockh = recvfrom(sock,message,10000,0,(struct sockaddr *)&serv_addr,&length);
	// receiving message from SITM server
	message[sockh] = 0;
	t = strtok(message, "!");
	for(i = 0; t; t = strtok(NULL,"!"), i++)
		token[i] = t; // tokenizing incoming message
	if(strcmp(token[0], "mcs") == 0){ 	// here we are getting machine ip
		machines_count = atoi(token[2]); // number of machines - omitted
		machine_name = token[3]; 	// name of the machine,
					// its IP address in our case
		gtk_list_store_append (store_ex, &iter_ex); //append iter in "datagrid"
		gtk_list_store_set (store_ex, &iter_ex,	 // update with new value
			  COLUMN_MACHINE, machine_name,
			  -1);
		} else if (strcmp(token[0], "nfo") == 0){ // standard message
		strcpy(xXx, token[1]); // prepare message
		strcat(xXx, " ");
		strcat(xXx, token[2]);
		Update_Buffer_And_Scroll_To_End
			(log_view, xXx); // Update terminal windows with new
	// message
		}
		CLEAR(message); // ZeroMemory
		continue; // continue the game :)
		}
	}

古老而优秀的 UNIX 套接字。

C# .NET
private void ThreadTask()
        	{
            int recv;
            String TimeAndDate = "";
            DateTime thisDate = DateTime.Now;
            TimeAndDate = String.Format("{0:G}", thisDate); // get local time
            byte[] data = new byte[1024];
            String LocalPort = local_port_ent.Text;	// get port from texbox
            int LoPo = System.Int32.Parse(LocalPort);	// pars port
            IPEndPoint ipep = new IPEndPoint
			(IPAddress.Any, LoPo); // init  IPEndPoint class

            Socket newsock = new Socket(AddressFamily.InterNetwork,
                            SocketType.Dgram, ProtocolType.Udp); // init socket class
            newsock.Bind(ipep);	// bind socket

            IPEndPoint sender = new IPEndPoint(IPAddress.Any, 0);
            EndPoint Remote = (EndPoint)(sender);
            termo_label.Text += TimeAndDate + " UDP Local Server Started!\n";
            while (true)
            {
                String response = "";
                recv = newsock.ReceiveFrom(data, ref Remote); // receive message
                System.Text.ASCIIEncoding enc = new System.Text.ASCIIEncoding();
                response = enc.GetString(data);
                Array.Clear(data, 0, data.Length); // then clear
                char[] separator = {'!'};
                string[] token = response.Split(separator); // "tokenize" message
	// and then processing data, like in Gtk+ example
                if (String.Compare(token[0], "mcs") == 0)
                {
                    machine_name.Text = token[3]; //update machine name
                }
                else if (String.Compare(token[0], "nfo") == 0)
                {
                    termo_label.Text += token[1] +
			" " + token[2]; //update terminal window
                }
	// scroll terminal window down
                SendMessage(terminal_pannel.Handle, WM_VSCROLL,
				(IntPtr)SB_PAGEDOWN, IntPtr.Zero);
            }
}

让我们看看按下“Send”按钮时发生了什么。注意:你根本不需要按下发送按钮,你可以在消息输入/文本框字段中键入时直接按 ENTER。

Gtk+
int TransmitData(){
	int result;
	regex_t  rx;
	regmatch_t   *matches;
	char buf[] = "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)
		\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.
		(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.
		(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)";
	// ip address regular expression
	bool Send2Machine;
	char MainMessage[1024];
	char no_serv_err[] = "\nServer IP not specified! Terminating task...\n";
	printf("TransmitData called!\n");
	CLEAR(MainMessage);
	// getting needed values from Entry widgets
	const char *MachineName = gtk_entry_get_text(GTK_ENTRY(machine_ent));
	const char *MessageEnt = gtk_entry_get_text(GTK_ENTRY(message_ent));
	const char *ServerAddr = gtk_entry_get_text(GTK_ENTRY(serv_host_ent));
	const char *ServerPort = gtk_entry_get_text(GTK_ENTRY(serv_port_ent));
	const char *LocalPort = gtk_entry_get_text(GTK_ENTRY(local_port_ent));
	bool server_check_button_status =
	gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(server_check_button));
	// get server check button state
	if(strcmp(MessageEnt, "clear") == 0){ // if our message is simply 'clear'
		clear_output();	// then clear terminal window
				// without sending any data
		return 1;	// like in linux terminal
		}

	if(strcmp(MessageEnt, "exit") == 0) exit(0); 	// same like above,
						// in this case exit

	result = regcomp( &rx, buf, REG_EXTENDED ); 	// compile regular
						// expression pattern
	matches = (regmatch_t *)malloc( (rx.re_nsub + 1) *
			sizeof(regmatch_t) ); // allocate memory

	if(strcmp(ServerAddr, "") == 0){ // if no server address
	Update_Buffer_And_Scroll_To_End(log_view, no_serv_err);
	return 1;
	}
	result = regexec( &rx, ServerAddr, rx.re_nsub + 1,
			matches, 0 ); // validate server address
	if (!result) { // if OK
	printf("Valid IP!\n");
	} else { // update terminal window about an error
	Update_Buffer_And_Scroll_To_End(log_view, "Invalid server IP\n");
	return 1;
	}

	if(strcmp(MachineName, "") != 0) { 	// if we have some value
					// inside machine entry widget
	Send2Machine = true; // we will send our message through SITM to machine
	if(server_check_button_status == true){ // but if we using quick
					// switch to server and it is active
		Send2Machine = false; // cancel

		goto next; // skip validation of machine's ip address
		}
		result = regexec( &rx, MachineName,
			rx.re_nsub + 1, matches, 0 ); // validate machine's ip
		if (!result) printf("Valid machine's IP!\n"); // good
		 else { // bad
		Update_Buffer_And_Scroll_To_End(log_view, "Invalid Machine IP!\n");
		return 1;
		}
	} else {
	Send2Machine = false;
	}
	next:

		if(Send2Machine){ // if message is going to machine
		printf("sending data to machine!\n");
		strcpy(MainMessage, "2rcli!");
		strcat(MainMessage, MachineName);
		strcat(MainMessage, "!");
		strcat(MainMessage, MessageEnt);
		char *RottedMainMessage = code_rot_ex(MainMessage);
		UdpSender((char *)ServerAddr, (char *)ServerPort, RottedMainMessage);
		CLEAR(MainMessage);
		} else { // if message is only for server
		strcpy(MainMessage, MessageEnt);
		char *RottedMainMessage = code_rot_ex(MainMessage);
		printf("machine name is null - sending data to server!\n");
		UdpSender((char *)ServerAddr, (char *)ServerPort, RottedMainMessage);
		// call udp sender function
		CLEAR(MainMessage); // ZeroMemory
		}
}

int UdpSender(char *addr, char *port, char *message){
	// basic things everyone should know
	int sock, sockh;
	char recv_msg[10000];
	struct sockaddr_in serv_ddr;
	bzero(&serv_ddr,sizeof(serv_ddr));
	serv_ddr.sin_family = AF_INET;
	serv_ddr.sin_addr.s_addr=inet_addr(addr);
	serv_ddr.sin_port=htons(atoi(port));
	sock = socket(AF_INET,SOCK_DGRAM,0);
	      sendto(sock,message,strlen(message),0,
             (struct sockaddr *)&serv_ddr,sizeof(serv_ddr));
	return 0;
}
C# .NET

这里我们做的事情和之前的函数一样,所以跳过注释。

public bool IsValidIP(string addr)
	{
	    string pattern = @"\b(25[0-5]|2[0-4][0-9]|[01]?
		[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.
		(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.
		(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b";
	    Regex check = new Regex(pattern);
	    bool valid = false;
	    valid = check.IsMatch(addr, 0);
	    return valid;
	}

private void Send_Function(object sender, System.EventArgs e) {
            byte[] data = new byte[1024];
            bool SendToMachine = false;
            String DestMSG;
            String message;
            String dest_ip = serv_host_ent.Text;
            String MachineIp = machine_name_ent.Text;
            if (String.Compare(dest_ip, "") == 0)
            {
                termo_label.Text += "\nServer IP not specified! Terminating task...\n";
                return;
            }
            if (String.Compare(MachineIp, "") != 0)
            {
                SendToMachine = true;
                if (goto_server_hax.Checked)
                {
                    SendToMachine = false;
                    goto __next;
                }
                if (IsValidIP(MachineIp) == false)
                {
                    termo_label.Text += "\nMachine IP is not valid!
						Terminating task...\n";
                    return;
                }
            }

            __next:
            if (IsValidIP(dest_ip) == false)
            {
                termo_label.Text += "\nIP is not valid! Terminating task...\n";
                return;
            }
            IPEndPoint ipep = new IPEndPoint(
                IPAddress.Parse(dest_ip), 6666);
            Socket server = new Socket(AddressFamily.InterNetwork,
               SocketType.Dgram, ProtocolType.Udp);

            DestMSG = message_entry.Text;
            if (SendToMachine)
                message = "2rcli!" + MachineIp + "!" + DestMSG;
            else message = DestMSG;

            if (String.Compare(message, "") == 0) {
                termo_label.Text += "\nNo message! Terminating task...\n";
                return;
                }
            message_entry.Text = "";
            if (!SendToMachine)
                termo_label.Text += "\n" +
		Environment.MachineName + "~# " + message + "\n";
            else
                termo_label.Text += "\n" +
		Environment.MachineName + "~# " + DestMSG + "\n";

            if (String.Compare(DestMSG, "exit") == 0)
            {
                Quit_Function(sender, e);
                return;
            }
            if (String.Compare(DestMSG, "clear") == 0)
            {
                termo_label.Text = "";
                return;
            }

            data = Encoding.ASCII.GetBytes(Rot47c(message));
            server.SendTo(data, data.Length, SocketFlags.None, ipep);
            SendMessage(terminal_pannel.Handle, WM_VSCROLL,
				(IntPtr)SB_PAGEDOWN, IntPtr.Zero);
            return;
        }

其余代码主要与图形界面有关。最后,几乎不重要的一点是,我想提供一个简单的驱动程序加载器来加载 R-Client 驱动程序,当然,如果你没有自己的。嗯,它是用 Gtk+ 编写的(为 Windows 编译),并具有以下选项:

Driver Loader Dload

  • [+] 使用ZwSetSystemInformation加载驱动程序
  • [+] 使用NtLoadDriver加载驱动程序
  • [+] 通过服务控制管理器加载驱动程序
  • [+] 卸载驱动程序
  • [+] 删除驱动程序文件
  • [+] 删除驱动程序注册表项
  • [+] 将驱动程序加载例程注入到另一个进程中
  • [+] 使用CreateRemoteThread注入
  • [+] 使用RtlCreateUserThread注入

假设它是本项目的一部分——DLoad的源代码和二进制文件已包含在内。

#undef ARTICLE_STATUS #define ARTICLE_STATUS "98.5%"

结束语

为你做的事情,我的朋友:

  • 正确配置 R-client、R-manager、SITM 服务器并进行编译。
  • 运行 SITM 服务器。
  • 运行 R-manager。
  • 加载 R-client。
  • 阅读代码。
  • 我可能忘记了什么,因此可能会出现 bug(我是一个人,有时会犯错)。

本文仅用于教育目的和娱乐。我感谢你阅读我写的所有内容,并感谢 CodeProject 团队审查和发布我的文章。谁知道呢,也许会有这篇论文和整个项目的扩展版。最好的问候,你永远的 cross / csrss。

#undef ARTICLE_STATUS #define ARTICLE_STATUS "100%" PsTerminateSystemThread(0);

其他网络资源

历史

  • 2009年10月20日:初始版本
© . All rights reserved.