先看完整版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
#include <iostream>
#include <windows.h>

using namespace std;


// 在注册表中设置开机自启动
bool set_startup_registry(const wstring app_path) {
HKEY hKey;
wstring sub_key = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run";
wstring value_name = L"calc";

if (RegCreateKeyEx(HKEY_CURRENT_USER, sub_key.c_str(), 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL) != ERROR_SUCCESS) {
return false;
}

if (RegSetValueEx(hKey, value_name.c_str(), 0, REG_SZ, (const BYTE*)app_path.c_str(), (app_path.length() + 1)* sizeof(wchar_t)) != ERROR_SUCCESS) {
RegCloseKey(hKey);
return false;
}

RegCloseKey(hKey);
return true;
}

int main() {
wstring app_path = L"C:\\Windows\\System32\\calc.exe";

// 在注册表中设置开机自启动
if (set_startup_registry(app_path)) {
cout << "设置开机自启成功!" << endl;
}
else {
cout << "设置开机自启失败!" << endl;
}


return 0;
}


这个程序通过调用windows api在注册表中写入开机自启的程序路径来实现类似红队的权限维持操作,这里我们实现的是开机自动启动calc.exe,也就是计算器

前置知识:

Windows注册表是一个用于存储操作系统、应用程序及用户配置信息的中心化数据库。注册表包含了关于硬件、系统设置、用户偏好、文件关联等方面的信息。许多程序依赖注册表来保存和读取其配置。

注册表的结构是分层的,类似于文件系统。在最顶层,有五个预定义的根键(root keys),它们是:

  1. HKEY_CLASSES_ROOT:包含了文件关联、对象链接和嵌入以及注册表类型库等信息。这个键实际上是HKEY_LOCAL_MACHINE\Software\Classes的别名。
  2. HKEY_CURRENT_USER:包含了当前登录用户的配置信息。这个键实际上是HKEY_USERS\当前用户SID的别名。
  3. HKEY_LOCAL_MACHINE:包含了计算机硬件和软件的配置信息。
  4. HKEY_USERS:包含了所有用户的配置信息。每个用户都有一个与其安全标识符(SID)相关联的子键。
  5. HKEY_CURRENT_CONFIG:包含了当前硬件配置集的信息。这个键实际上是HKEY_LOCAL_MACHINE\System\CurrentControlSet\Hardware Profiles\Current的别名。

根键下面有子键(subkeys),子键可以继续包含子键,形成一个树状结构。每个键可以包含多个键值(values),键值是具有名称和数据的实体。数据可以有不同的类型,如字符串(REG_SZ)、二进制数据(REG_BINARY)或者整数(REG_DWORD)等。

在本例中,我们使用了HKEY_CURRENT_USER这个根键,表示我们要设置当前登录用户的配置信息。

Windows API 提供了两种字符版本的函数:一种是基于 char 类型(多字节字符)的 ANSI 函数,另一种是基于 wchar_t 类型(宽字符)的 Unicode 函数。对于许多 Windows API 函数,实际上有两个版本。一个是以 “A” 结尾的 ANSI 版本,另一个是以 “W” 结尾的 Unicode 版本。例如,MessageBox 有两个版本:MessageBoxA(ANSI)和 MessageBoxW(Unicode)。

当您在项目中包含 <Windows.h> 时,它会根据项目设置自动选择 ANSI 或 Unicode 函数。默认情况下,许多开发环境(如 Visual Studio)将 Unicode 设置为默认,因为 Unicode 支持更广泛的字符集,可以很好地处理各种语言和字符。

宽字符和正常字符串的区别主要在于字符大小和编码:

  1. 大小:char 类型通常占用一个字节(8 位),而 wchar_t 类型通常占用两个字节(16 位)。这意味着宽字符可以表示更多的字符。
  2. 编码:char 类型通常用于表示基于多字节的字符编码,如 ASCII 或 UTF-8。wchar_t 类型通常用于表示宽字符编码,如 UTF-16。UTF-16 编码支持更广泛的字符集,因此适用于多种语言和字符。

因此,使用宽字符(Unicode)的主要优势是支持更广泛的字符集,能够更好地处理多语言环境。

因为vs2022默认是Unicode编码,所以此程序中的string,char都替换为了wstring(宽字符版string)和wchat_t(宽字符版char),如果不改在visual stdio中可能会报错

c_str() 是 C++ std::stringstd::wstring 类的成员函数。它用于返回一个指向以空字符(null-terminator)结尾的 C 风格字符串的指针。换句话说,它返回一个指向字符串中第一个字符的指针,这个字符串以 ‘\0’ 字符结尾。

在很多情况下,一些 C 或 C++ 函数接受一个 C 风格字符串作为参数。例如,RegCreateKeyEx 函数接受一个 LPCSTR(对于 ANSI 版本)或 LPCWSTR(对于 Unicode 版本)类型的参数,这是一个指向 C 风格字符串的指针。在这种情况下,您可以使用 c_str() 函数从 std::stringstd::wstring 获取一个 C 风格字符串,以便将其传递给这些函数。

代码分析:

set_startup_registry函数

1
2
3
HKEY hKey;
wstring sub_key = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run";
wstring value_name = L"calc";

HKEY 是 Windows API 中的一个类型,用于表示注册表中的键。这个变量将用于存储新创建的键的句柄

sub_key用来表示要操作的子键的路径,SOFTWARE\Microsoft\Windows\CurrentVersion\Run路径是一个常用的自动启动程序的注册表键路径。

value_name是这个的名称,我们可以自定义,这里我就用calc表示

1
2
3
if (RegCreateKeyEx(HKEY_CURRENT_USER, sub_key.c_str(), 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL) != ERROR_SUCCESS) {
return false;
}

RegCreateKeyEx函数用来创建注册表键,HKEY_CURRENT_USER表示当前键的根键,在前置知识中已经知道这代表当前用户, sub_key表示子键路径,由于函数接受的是一个指针,所以需要用c_str来把string转换为指向这个字符串的指针,&hKey是将当前键的句柄赋予hKey,用于对接下来键值操作,若成功创建则返回ERROR_SUCCESS常量

1
2
3
4
5
if (RegSetValueEx(hKey, value_name.c_str(), 0, REG_SZ, (const BYTE*)app_path.c_str(), (app_path.length() + 1)* sizeof(wchar_t)) != ERROR_SUCCESS) {
RegCloseKey(hKey);
return false;
}

RegSetValueEx用来设置注册表的键值,hKey表示设置的是hKey这个句柄对应的键,value_name表示键名加上c_str返回指向value_name的指针,app_path表示要设置的值,c_str()同样返回对应指针,然后用强制类型转换为BYTE型指针,

app_path.length() + 1表示值的长度,+1是因为考虑到结尾的’\0’结束符,再乘以wchar_t的大小是因为用的是宽字符,所以要用宽字符的字节大小来计算

REG_SZ表示值的类型是字符串

1
RegCloseKey(hKey);

最后关闭hKey句柄

主函数main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main() {
wstring app_path = L"C:\\Windows\\System32\\calc.exe";

// 在注册表中设置开机自启动
if (set_startup_registry(app_path)) {
cout << "设置开机自启成功!" << endl;
}
else {
cout << "设置开机自启失败!" << endl;
}


return 0;
}

主函数很简单,设置要自启的程序路径,也就是注册表键的值,然后执行修改注册表操作的函数,显示结果

结语

开始免杀的学习,好久没用过C++了而且之前也就是控制台交互,现在直接是和windows api进行交互,而且没接触过win api的知识,一上来感觉很吃力,遇到了很多根本不认识的函数和数据类型,还要考虑编码问题(ansi和unicode),对于本人来说是一个很大的挑战,接下来就要开始汇编指令的学习,希望能早日跨进免杀的门槛