Windows 二进制原始文件名获取

孙康

在黑客攻击终端主机时,为绕过主机上 EDR 等防护软件的检测识别,可能会将恶意文件更名为主机上系统文件,如果防护软件针对该文件名做了白名单,则可能存在绕过风险。另外,为避免防护软件对危险进程的识别,如禁止 Windows 主机 PowerShell 等进程创建子进程,则可能将 PowerShell 二进制文件更名后启动,以绕过检测。

原始文件名属性介绍

PE (Portable Executable) 文件是 Windows 操作系统中用于可执行文件、对象代码和动态链接库的文件格式。原始文件名属性位于 PE 文件的资源区,属性定位的关键字是OriginalFilename,如下图所示,即 PEview.exe 文件的原始文件名属性定位。由于属性的存储方式是宽字符,所以每个字符占两个字节。

PEview.exe 原始文件名
PEview.exe 原始文件名

当然该属性也可以使用属性窗口查看,如果原始文件名的属性有定义,则会有如下的显示。

PEview.exe 原始文件名
PEview.exe 原始文件名

因为原始文件名属性存放在二进制文件中,所以不受文件下载、拷贝、移动、重命名等常规操作的影响,可以标识二进制的来源,维护版本的一致性。经过统计,约 90% 的系统二进制含原始文件名属性,所以基于原始文件名进行威胁检测识别是可行的。

使用 API 获取原始文件名

使用 Windows API 可以方便地获取二进制原始文件名,API 使用需要链接系统库version.lib。步骤比较简单,需要注意的是,Translation查询的目的是获取文件版本信息资源中的翻译表,包含系列的语言和字符集组合。在获取翻译表后,代码使用翻译表的第一个语言-字符集组合来构建查询信息。翻译表并非是固定的或者和系统相关的,需每次查询原始文件名之前都查询一次。如果翻译表硬编码导致与实际不匹配,则会导致原始文件名查询失败。

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
bool getOriginalFileNameByAPI(const std::string& strFilePath, std::string& strOriginalName) {
if (strFilePath.empty()) {
return false;
}

DWORD dwHandle = 0;
DWORD dwDataSize = GetFileVersionInfoSizeA(strFilePath.c_str(), &dwHandle);
if (dwDataSize == 0) {
return false;
}

std::unique_ptr<BYTE[]> pVersionData(new (std::nothrow) BYTE[dwDataSize]);
if (!pVersionData) {
return false;
}

if (!GetFileVersionInfoA(strFilePath.c_str(), dwHandle, dwDataSize, pVersionData.get())) {
return false;
}

UINT iQuerySize = 0;
DWORD* pTransTable = nullptr;
const char* szQueryTrans = "\\VarFileInfo\\Translation";
if (!VerQueryValueA(pVersionData.get(), szQueryTrans, reinterpret_cast<LPVOID*>(&pTransTable), &iQuerySize) || iQuerySize == 0) {
return false;
}

DWORD dwLangCharset = MAKELONG(HIWORD(pTransTable[0]), LOWORD(pTransTable[0]));
char szSubBlock[50];
sprintf_s(szSubBlock, "\\StringFileInfo\\%08lx\\OriginalFileName", dwLangCharset);

LPVOID lpData = nullptr;
if (!VerQueryValueA(pVersionData.get(), szSubBlock, &lpData, &iQuerySize) || iQuerySize == 0) {
return false;
}

strOriginalName = std::string(static_cast<LPSTR>(lpData)).c_str();
return true;
}

手动解析 PE 文件获取原始文件名

既然原始文件名属性存储在二进制的资源区,那么就可以通过手动解析 PE 文件获取该属性。手动解析相比调用 API 步骤要繁琐一些:首先对打开的文件句柄创建内存映射,以便提升文件内容读取的访问速度并降低内存占用;然后检查文件的 DOS 和 NT 头,判断文件是否为有效的 PE 文件;然后定位资源区,根据标识定位原始文件名所在区域;最后提取原始文件名并将其由宽字符串转化为 UTF-8 格式的字符串。

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
#define ORIGINAL_FILENAME	L"OriginalFilename"
#define ORI_FILENAME_SIZE 17
#define PE_RSRCNAME ".rsrc"

bool getOriginalFileName(const std::string &strFilePath, std::string &strOriginalName) {
// Custom deleter for HANDLE
struct HandleDeleter {
void operator()(HANDLE handle) {
if (handle != NULL && handle != INVALID_HANDLE_VALUE) {
CloseHandle(handle);
}
}
};

// Custom deleter for memory-mapped file view
struct MapViewDeleter {
void operator()(LPVOID ptr) {
if (ptr != NULL) {
UnmapViewOfFile(ptr);
}
}
};

// Use smart pointers with custom deleters for automatic resource management
std::unique_ptr<void, HandleDeleter> hFile(CreateFileA(strFilePath.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL), HandleDeleter());
if (hFile.get() == INVALID_HANDLE_VALUE) {
return false;
}
std::unique_ptr<void, HandleDeleter> hFileMapping(CreateFileMappingA(hFile.get(), NULL, PAGE_READONLY, 0, 0, NULL), HandleDeleter());
if (hFileMapping.get() == NULL) {
return false;
}
std::unique_ptr<void, MapViewDeleter> lpBaseAddress(MapViewOfFile(hFileMapping.get(), FILE_MAP_READ, 0, 0, 0), MapViewDeleter());
if (lpBaseAddress.get() == NULL) {
return false;
}

// Check the DOS and NT header
PIMAGE_DOS_HEADER dosHeader = reinterpret_cast<PIMAGE_DOS_HEADER>(lpBaseAddress.get());
if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
return false;
}
PIMAGE_NT_HEADERS ntHeaders = reinterpret_cast<PIMAGE_NT_HEADERS>(reinterpret_cast<BYTE *>(
lpBaseAddress.get()) + dosHeader->e_lfanew);
if (ntHeaders->Signature != IMAGE_NT_SIGNATURE) {
return false;
}

PIMAGE_SECTION_HEADER sectionHeaders = IMAGE_FIRST_SECTION(ntHeaders);

// Find the .rsrc section by VirtualAddress
for (int i = 0; i < ntHeaders->FileHeader.NumberOfSections; ++i) {
PIMAGE_SECTION_HEADER sectionHeader = &sectionHeaders[i];
if (strncmp(reinterpret_cast<const char *>(sectionHeader->Name), PE_RSRCNAME, IMAGE_SIZEOF_SHORT_NAME) == 0) {
// Search for the originalFilename in the .rsrc section
DWORD dwRsrcOffset = sectionHeader->PointerToRawData;
const BYTE *pRsrcBegin = reinterpret_cast<BYTE *>(lpBaseAddress.get()) + dwRsrcOffset;
const BYTE *pRsrcEnd = pRsrcBegin + sectionHeader->SizeOfRawData;
const BYTE *pFound = std::search(pRsrcBegin, pRsrcEnd, reinterpret_cast<const BYTE *>(ORIGINAL_FILENAME),
reinterpret_cast<const BYTE *>(ORIGINAL_FILENAME) + ORI_FILENAME_SIZE * sizeof(wchar_t));

if (pFound != pRsrcEnd && (pFound - sizeof(wchar_t) * 2) >= pRsrcBegin) {
// Found the skip size and name length
const wchar_t *ptrSkip = reinterpret_cast<const wchar_t *>(pFound - sizeof(wchar_t) * 1);
const wchar_t *ptrLength = reinterpret_cast<const wchar_t *>(pFound - sizeof(wchar_t) * 2);

// Skip the key to get to the value
const wchar_t *ptrName = reinterpret_cast<const wchar_t *>(
pFound + (ORI_FILENAME_SIZE - 1 + *ptrSkip) * sizeof(wchar_t));

// Convert the wstring to string and assign it to originalName
std::wstring wstrName(ptrName, *ptrLength);
int iSize = WideCharToMultiByte(CP_UTF8, 0, wstrName.data(), wstrName.size(), NULL, 0, NULL, NULL);
strOriginalName.resize(iSize);
WideCharToMultiByte(CP_UTF8, 0, wstrName.data(), wstrName.size(), &strOriginalName[0], iSize, NULL, NULL);
return true;
}
break;
}
}

return false;
}

检查文件名是否发生篡改

获取到原始文件名之后,可以以此分析当前二进制文件名是否发生篡改。经过对 Windows 系统二进制的分析,发现绝大多数二进制的文件名和原始文件名匹配,但需要经过一些匹配条件的过滤。如下代码进行了系列过滤:忽略大小写的影响;忽略原始文件名.mui后缀;忽略原始文件名的前后"号;原始文件名.dynlink后缀等价于.dll; 忽略原始文件名不带后缀。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bool checkNameMatch(std::string strFileName, std::string strOriginalName)
{
std::transform(strFileName.begin(), strFileName.end(), strFileName.begin(), ::tolower);
std::transform(strOriginalName.begin(), strOriginalName.end(), strOriginalName.begin(), ::tolower);

strOriginalName = strOriginalName.substr(0, strOriginalName.find(".mui"));
if (strOriginalName.front() == '"')
{
strOriginalName.erase(0, 1);
strOriginalName.pop_back();
}
strOriginalName = strOriginalName.substr(0, strOriginalName.find(".dynlink"));

if (strFileName == strOriginalName || strFileName.find(strOriginalName.c_str()) == 0)
{
return true;
}
return false;
}

上述的过滤步骤不可任意调换,因为有类似"Abc.dynlink".MUI的原始文件名,其真实名称为abc.dll。Windows 开发规范并没有要求第三方开发者开发的二进制必须实现原始文件名属性,所以以上的篡改识别方案并不具有普适性,针对的主要是系统二进制。

  • Title: Windows 二进制原始文件名获取
  • Author: 孙康
  • Created at : 2024-05-12 12:00:00
  • Updated at : 2024-08-22 16:46:20
  • Link: https://conradsun.github.io/2024/0560b05398.html
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments