本文最后更新于 2025-05-28T15:54:53+08:00
隐写
这不是什么新奇的技术,早有APT组织利用过
windows在计算PE文件的哈希时会跳过文件末尾的数字签名数据,也就是WIN_CERTIFICATE
结构体,该结构体由可选头的安全目录指向
可以在这里下载数字签名标准文档:https://learn.microsoft.com/zh-cn/windows/win32/debug/pe-format#references
1 2 3 4 5 6 7
| typedef struct _WIN_CERTIFICATE { DWORD dwLength; WORD wRevision; WORD wCertificateType; BYTE bCertificate[ANYSIZE_ARRAY]; } WIN_CERTIFICATE, *LPWIN_CERTIFICATE;
|
其中,dwLength
必须是8的整倍数,表示整个结构体的长度(包括结构体头部和它本身)
原理很简单,在数字签名末尾夹带数据,并对应调整结构体大小就行
由于dwLength
必须是8的整倍数,所以夹带的数据长度也得是8的整倍数

然后调整结构体大小,要调整的地方有两个,一个就是文件末尾的WinCertificate.dwLength
,还有一个可选头安全目录中的大小OptionalHeader.DataDirArray[4].size
上文夹带了8字节数据,所以这两处地方对应增大8就行


然后保存文件,同时数字签名未失效

检测
事实上,这里还存在一个“隐式”的长度可以用来检测是否被夹带了恶意数据
可以知道:数字签名是 PKCS#7 结构,该结构的顶层是一个 SEQUENCE,整个结构都嵌套在这个顶层 SEQUENCE 中,这个顶层 SEQUENCE 的大小就是整个签名结构的大小

图中的4个字节是顶层 SEQUENCE 头(不定长),遵循TLV结构(Tag-Length-Value)
其中,
30
表示这是一个 SEQUENCE 类型(Tag)
82
是长度前缀,表示接下来的两个字节表示长度
41 08
是长度,以大端序表示(Length)
也就是说,顶层 SEQUENCE 的 Value 部分有16648字节大,加上 SEQUENCE 头的4个字节、WIN_CERTIFICATE头的8个字节,填充的4字节,总长就是16664字节
16664和实际长度16672差了8字节,也就是上文夹带的数据长度
编写一个粗糙的POC代码:

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 118 119 120 121 122 123 124 125 126 127 128 129 130
| #include <windows.h> #include <iostream> #include <filesystem> #include <fstream> #include <wintrust.h>
void hexdump(const void* data, size_t size) { const unsigned char* ptr = (const unsigned char*)data; for (size_t i = 0; i < size; i += 16) { printf("%08zx ", i);
for (size_t j = 0; j < 16; j++) { if (i + j < size) printf("%02x ", ptr[i + j]); else printf(" "); if (j == 7) printf(" "); }
printf(" ");
for (size_t j = 0; j < 16 && i + j < size; j++) { unsigned char c = ptr[i + j]; printf("%c", isprint(c) ? c : '.'); }
printf("\n"); } }
static void check(const char* pe) { printf("\n检测 %s\n", pe);
std::ifstream file(pe, std::ios::binary); if (!file) { printf("无法打开文件\n"); return; } auto fileSize = std::filesystem::file_size(pe); if (fileSize < 200) { printf("非有效PE文件\n"); return; }
std::unique_ptr<char[]> fileBuffer = std::make_unique<char[]>(fileSize); file.read(fileBuffer.get(), fileSize);
PIMAGE_DOS_HEADER dos_header = (PIMAGE_DOS_HEADER)fileBuffer.get(); PIMAGE_NT_HEADERS32 nt_header_32 = (PIMAGE_NT_HEADERS32)(fileBuffer.get() + dos_header->e_lfanew); PIMAGE_NT_HEADERS64 nt_header_64 = (PIMAGE_NT_HEADERS64)(fileBuffer.get() + dos_header->e_lfanew);
if (dos_header->e_magic != IMAGE_DOS_SIGNATURE || nt_header_32->Signature != IMAGE_NT_SIGNATURE) { printf("非有效PE文件\n"); return; }
IMAGE_DATA_DIRECTORY securityDir; if (nt_header_32->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) { securityDir = nt_header_64->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY]; } else if (nt_header_32->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) { securityDir = nt_header_32->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY]; } else { printf("非有效PE文件\n"); return; }
if (securityDir.VirtualAddress == 0 || securityDir.Size == 0) { printf("文件没有数字签名\n"); return; }
if (securityDir.VirtualAddress + securityDir.Size > fileSize) { printf("数字签名损坏\n"); return; }
const char* securityDataStart = fileBuffer.get() + securityDir.VirtualAddress; const char* securityDataEnd = securityDataStart + securityDir.Size;
LPWIN_CERTIFICATE cert = (LPWIN_CERTIFICATE)securityDataStart;
if (cert->bCertificate[0] != 0x30) { printf("数字签名损坏\n"); return; }
BYTE seq_length_tag = *(cert->bCertificate + 1); uint64_t seq_length = 0;
if (seq_length_tag <= 0x7F) { seq_length = seq_length_tag; } else if ((seq_length_tag >= 0x81) && (seq_length_tag <= 0x88)) { BYTE length_bytes = seq_length_tag & 0x7F; if (length_bytes > 8) { printf("数字签名过长\n"); return; }
const BYTE* pLength = cert->bCertificate + 2; for (int i = 0; i < length_bytes; i++) { seq_length = (seq_length << 8) | (*pLength++); } } else { printf("数字签名损坏\n"); return; }
uint64_t expected_cert_length = ((seq_length + 4 + 8) + 7) / 8 * 8; if (expected_cert_length < cert->dwLength) { uint64_t length = cert->dwLength - expected_cert_length; printf("检测到数据夹带,总长 %lld 字节\n", length); hexdump(securityDataStart + expected_cert_length, length); } else { printf("未检测到数据夹带\n"); } }
int main() { check("C:\\Users\\ADMIN\\Desktop\\msvcp140.dll"); check("C:\\Users\\ADMIN\\Desktop\\msvcp140_raw.dll"); }
|