远程管理和监控系统入门
远程管理和监控系统入门

引言
#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 浏览器漏洞的“即点即毁”下载、蠕虫、特洛伊木马或后门安装,并在一个通用的命令与控制基础设施下运行。
*** 维基百科
让我们谨慎行事。;)
结构

嗯,整个东西将有一个“三角形”结构。考虑以下组件:
- 主控机——我们将从中建立控制的机器。
- 服务器机——我们将在其中放置我们的服务器(稍后解释这一点)。
- 客户端机——我们将控制/管理其的机器。
主控机 <---> 服务器机 <---> 客户端机
为什么采用这种结构?假设我正在实时远程管理 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. 之前的机器码
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. 之后的机器码
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 个小时了,同时还要完成和测试项目本身。同时,享受我刚刚创建的这张图:)
图片由我、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 Gtk+ Linux 版本 ***
相当简单的界面,功能最少,这使得新手更容易理解,也使得高级程序员更容易定制以满足自己的需求。好吧,我不会“重新发明轮子”,描述我在这两个示例中使用的每种编程语言的细节,而是指向两个主要的网络资源,你可以在其中找到完整的文档并从中学习: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 编译),并具有以下选项:
- [+] 使用
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);
其他网络资源
- Microsoft Developer Network (http://msdn.microsoft.com)
- GNOME Library (http://library.gnome.org/devel/gtk/2.16/ )
- Perl 编程文档 (https://perldoc.perl.net.cn)
历史
- 2009年10月20日:初始版本