把 WerEnc.dll LOLBin 改成攻击者控制的加密原语(BYOK)

导语:WerEnc.dll 是 Windows 10 1607+ 和 Windows 11 自带的”Windows 错误报告转储编码库”,系统 32 目录下、微软签名、有两个导出函数 EncryptDumpFile(序号 1)和 EncryptDumpStream(序号 2),做 AES-256 + RSA-4096 的混合加密。0xsp 研究员 Mr.Z 写了一个 PoC,把 WerEnc 内部嵌入的 RSA 公钥在内存中替换成攻击者自己生成的 4096-bit 密钥,从此 WerEnc 加密的产物密钥完全由攻击者掌控——密文格式和正常 WER 工件在表面上看不出区别,目前没有公开的检测规则能拦住加载这个 DLL。


引言

每台 Windows 10 1607+ 和 Windows 11 机器的 System32 下都自带 WerEnc.dll——全称”Windows Error Reporting Dump Encoding Library”。微软签名,导出两个函数:EncryptDumpFile(序号 1)和 EncryptDumpStream(序号 2)。

WerEnc.dll 在 WerFaultSecure.exe 中被加载

这两个函数对进程崩溃转储做 AES-256 加密,session key 用 RSA 包装到内嵌的 4096-bit 公钥上。WerFaultSecure.exe 在 dump PPL 进程时会加载 WerEnc.dll(感谢 @TwoSevenOneT 提供这条线索)。

过去几年,恶意软件和红队都习惯用 advapi32!SystemFunction033——也就是 RtlEncryptMemory 的 RC4 模式——给载荷加密。EDR 现在已经把这套标记了。WerEnc 提供的是正经的 CNG(下一代加密 API)AES-256 + RSA-4096,产物长得跟合法加密的 WER 工件一模一样,目前没有公开检测规则能识别对 WerEnc.dll 的加载

研究过程中最难的部分是:WerEnc 把 session key 包装到了微软内嵌的公钥上,意思是说输出 blob 只有微软能解开。但这不是终点——我们做了 BYOK(Bring Your Own Key,自带密钥)攻击,把内存里的密钥直接换掉。

WerEnc.dll 内部细节

找到调用约定

网上查不到、问 Opus 4.6 / GLM 5.3 也查不到这两个导出的 API 签名。AI 在某些上下文里说”它是 WerFaultSecure.exe 的依赖,用来加密崩溃转储后再上传微软”——到这里就断线了。

因为不知道确切签名,我们写了个探针(research/WerEnc/werenc_probe.c),在向量化异常处理(vectored exception handler)的保护下枚举每一种可能的调用签名。setjmp 检查点捕获非法访问违规,打一条消息,继续尝试下一个猜测。

探测 EncryptDumpFile 时,传 LPCWSTR 文件路径字符串没用,直接返回错误 0x80070006(无效句柄)。这个函数压根不检查输入是不是合法路径,直接把参数当文件句柄。所以正确用法是先 CreateFileW 打开两个文件,把 HANDLE 传过去。

另一边,探测 EncryptDumpStream 时,传裸内存 buffer 会直接崩——函数期望的是 COM 接口指针,会调它们的虚表。从 prologue 反汇编确认它吃三个参数不是两个:(IStream, IStream, PVOID reserved)。要做内存里加密,先要用 CreateStreamOnHGlobal 把 buffer 包成 stream,再调用。

替换密钥

WerEnc.dll 的导入表很少,CNG 调用只有 BCryptGenRandomBCryptSetPropertyBCryptImportKeyPairBCryptEncrypt——这是一套标准的混合加密流程:生成密钥、设置加密模式、导入公钥、加密数据。既然它导入了一个公钥,这个密钥必然嵌在 DLL 自己的某个位置。

用进程调试器跟函数调用链,就能定位到要找的”魔法密钥”:

RSA 密钥 blob 在 WerEnc.dll .rdata 段的位置

基于这个事实,在映射到内存的 DLL 镜像里扫 RSA1 魔法数(0x31415352),找到唯一一处匹配,落在 RVA 0x5610。这是个标准 CNG 公钥 blob:

BCRYPT_RSAKEY_BLOB (public), 539 bytes total:
  Magic:         RSA1 (0x31415352)
  BitLength:     4096
  cbPublicExp:   3 bytes
  cbModulus:     512 bytes
  Exponent:      01 00 01 (65537)
  Modulus:       512 bytes (Microsoft's)

替换能这么直白有两个原因:

  • BCryptImportKeyPair 上打一个断点,触发了一次加密调用。断点命中时,blob 指针参数直接指向 WerEnc.dll 的 .rdata 段 RVA 0x5610,不在堆里、也不在栈副本上。意思是 WerEnc 函数直接从它自己的映射镜像里读密钥——如果我们在调用前把那 539 字节改掉,BCryptImportKeyPair 导入的就是我们的密钥而不是微软的。
  • blob 格式固定 24 字节头 + 3 字节 exponent + 512 字节 modulus。CNG 用 BCryptGenerateKeyPair 生成新的 4096-bit RSA 密钥对时,导出的 RSAPUBLICBLOB 用的也是 3 字节编码的 65537(01 00 01)加 512 字节 modulus。我们的密钥和微软的密钥产出的 blob 维度完全一致。不需要调尺寸、改字段,直接 memcpy 539 字节就是完整的替换
/* 扫描 RSA1 blob 的代码 */
for (off = 0; off + sizeof(BCRYPT_RSAKEY_BLOB) < imageSize; off++) {
    if (*(ULONG*)(base + off) == 0x31415352) {
        hdr = (BCRYPT_RSAKEY_BLOB*)(base + off);
        if (hdr->BitLength == 4096 && hdr->cbPublicExp == 3
            && hdr->cbModulus == 512)
            blob = base + off;
    }
}

VirtualProtect(blob, 539, PAGE_READWRITE, &oldProtect);
memcpy(blob, ourPublicBlob, 539);
VirtualProtect(blob, 539, oldProtect, &oldProtect);

/* 现在调用 EncryptDumpFile:session key 包到我们的公钥上 */
EncryptDumpFile(hIn, hOut);

替换完成后,WerEnc 后续产出的每一个工件的密钥材料都包到了我们的公钥上。

容器格式

测试时我们加密了一个 21 字节的小文件,看看产物大小。结果输出是 1,659 字节,布局如下:64 (header) + 539 (key blob) + 512 (RSA wrap 1) + 512 (RSA wrap 2) + 32 (AES content) = 1659:

偏移大小内容
0x00016格式常量 A(所有工件相同)
0x01016格式常量 B(所有工件相同)
0x0204version = 2
0x0244公钥偏移 = 0x40
0x0284原始明文大小(仅文件变种)
0x02C4reserved
0x0304公钥 blob 大小 = 539
0x0344RSA 块大小 = 512
0x0384AES 密钥大小 = 32
0x03C4reserved
0x040539RSA1 blob(替换后是我们的)
0x25B512RSA 包装的 AES-256 session key
0x45B512RSA 包装的内容 IV
0x65BNAES-256-CBC 密文(对齐到 16 字节边界)

头部这六条 DWORD 在 0x20 起始的位置很好解:version、offset、原始大小、blob 大小、RSA 块大小、AES 密钥大小,每个值跟实际文件布局都能交叉验证。

0x45B 处的第二个 512 字节块一开始不明显。我们尝试了大约 60 种 key/IV/mode 组合直接当 AES 解,全是噪声。但它恰好 512 字节(RSA-4096 一个块),用我们的私钥 RSA 一解,出来 16 字节——就是内容 IV。

这个结论由下文讲的 IAT hook 抓 CNG 调用链进一步坐实。

CNG IAT hooks

分析容器结构只帮我们走到一半。第二块 RSA 到底存的是什么、加密时具体 CNG 参数是哪些,这两个问题还得现场抓。

我们对 WerEnc.dll 的导入地址表(IAT)做 hook——把目标 BCrypt 函数的 IAT 条目重定向到轻量日志包装器,记录每次传给 CNG API 的参数、标志位、buffer,然后再转发给真实的实现。发布的 PoC 配合 --capture 标志就能启用:

static NTSTATUS WINAPI Hook_BCryptGenRandom(void *hAlg, BYTE *buf,
                                            ULONG cb, ULONG flags)
{
    NTSTATUS st = g_realGenRand(hAlg, buf, cb, flags);
    if (g_cap && st == 0) CapHex("[GenRandom]", buf, cb);
    return st;
}

再次加密、用这些 hook 抓日志后,完整流水线就出来了:

BCryptGenRandom(32)          ->  session key K
BCryptGenRandom(16)          ->  content IV
BCryptSetProperty(CBC)       ->  把 AES 设成 CBC 模式
BCryptImportKeyPair(blob)    ->  RSA handle(blob = 我们的,+0x5610)
BCryptEncrypt(K, 32)         ->  K 的 512 字节 RSA 包装
BCryptEncrypt(IV, 16)        ->  IV 的 512 字节 RSA 包装
BCryptEncrypt(content)       ->  AES-256-CBC 密文

WerEnc.dll 每次加密产出两个随机值——32 字节 key + 16 字节 IV,各自单独 RSA 包装,然后用 AES-CBC 加密内容。两个 RSA 块都进容器

不过在解密阶段(测了多个文件变种、不同输入大小)我们观察到:代码路径不同,RSA padding 模式也不同——EncryptDumpFileBCRYPT_PAD_PKCS1,但 EncryptDumpStream 用的是 OAEP-SHA256。配上对应替换 blob 的私钥,完整解密流程是:

container -> 抽出 wrap1(0x25B 处 512 字节)
          -> RSA 解密(依次试 PKCS#1 / OAEP-SHA256 / OAEP-SHA1)
          -> session key K(32 字节)

          -> 抽出 wrap2(0x45B 处 512 字节)
          -> RSA 解密(同上 padding 序列)
          -> content IV(16 字节)

          -> 抽出密文(0x65B 之后)
          -> AES-256-CBC 解密,key=K、iv=IV
          -> 按头部 0x28 字段裁回原始长度
          -> 明文

把它做成实战 stager

基于上面的发现,我们做了一个能跑的工具,能做这些事:

解密预加密载荷

载荷以 .byok.enc 容器形式到达,操作员用 werenc_byok.exe --encrypt 预先加密。stager 识别 WerEnc v2 头部(0 偏移处 32 字节常量),跳过加密、直接解密执行。

内存加密载荷

当载荷以明文形式到达(没预先加密),stager 可以先用 WerEnc.dll 的 DumpFileStream 在内存里加密,再解密执行。这对那些没法预先加密的载荷很有用——动态生成、从另一阶段拉过来之类的。如果载荷本身是已经加密好的,PoC 的头部检测会跳过加密步骤直接进入解密模式:

stager 流程——header 检测决定跳过加密或走完整加密-解密路径

Beacon 模式

这是个扩展功能,用来演示从受密码保护的 C2 服务器拉取加密密钥的能力。攻击链:

  • 操作员先跑一个 key server(werenc_keyserver.py),通过 HTTP 提供 BYOK 密钥文件,以单长度前缀 blob 形式传输。
  • beacon 首次 check-in 拉取 blob,把两个密钥缓存到内存里,BYOK-patch WerEnc.dll 一次,然后进入 sleep 循环。
  • 两个线程并行跑:第一个线程独立运行 shellcode/载荷;第二个线程(Wrapping 线程)管加密/sleep/解密节奏。
Beacon 模式架构——C2 提供密钥,stager 在内存中轮换 key/IV 做休眠加密

PoC 代码和使用说明在 GitHub 仓库可取。


关于作者

Mr.Z,0xsp 安全研究与发展实验室(SRD)创始人,致力于攻击性安全研究和开源项目维护。X 账号 @zux0x3a


附注

© 版权声明
THE END
喜欢就支持一下吧
点赞15 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容