Introduction
Việc thay đổi các cấu hình mặc định của compiler trong Visual Studio có thể giúp thay đổi file nhị phân được biên dịch chẳng hạn như làm giảm kích thước hoặc entropy1.
Release Vs Debug Options
Một vài sự khác nhau giữa việc biên dịch ở chế độ Release và chế độ Debug:
Đặc điểm | Chế độ Debug | Chế độ Release |
---|---|---|
Hiệu suất (Performance) | Chậm hơn do không được tối ưu. | Nhanh hơn do trình biên dịch tối ưu mã nguồn. |
Debugging | Dễ debug hơn, có các file Program Database (.pdb ) giúp debugger hiển thị thông tin chi tiết về biến, hàm, số dòng. | Ít thông tin debug hơn, tối ưu hóa có thể làm thay đổi cách thực thi mã nguồn. |
Triển khai (Deployment) | Thường yêu cầu DLL của Visual Studio nên khó chạy trên máy không có Visual Studio. | Tương thích cao hơn, có thể chạy độc lập mà không cần DLL của Visual Studio. |
Xử lý ngoại lệ (Exception handling) | Khi có ngoại lệ, Visual Studio dừng thực thi và hiển thị thông tin chi tiết. | Ngoại lệ có thể gây crash và không có thông tin chi tiết do tối ưu hóa làm mất thông tin debug. |
Trong các phần bên dưới, ta sẽ sử dụng đoạn code sau để làm ví dụ:
#include <stdio.h>
int main()
{
printf("Hello World!\n");
}
Ta gọi chương trình của đoạn code trên là HelloWorld.
Default Compiler Settings
Tuy nhiên, có một số vấn đề xảy ra nếu ta sử dụng các cấu hình mặc định của trình biên dịch:
-
Compatibility: một số ứng dụng build ở chế độ Release vẫn xảy ra lỗi sau khi chạy trên máy không có Visual Studio:
-
CRT Imported Functions: có một vài hàm mà ta không sử dụng nhưng Visual Studio lại tự động thêm vào IAT khi biên dịch chương trình. Ví dụ, chương trình HelloWorld chỉ nên có một import của
printf
(về bản chất là__stdio_common_vfprintf
) nhưng file binary đầu ra của Visual Studio lại có rất nhiều import function (kết quả hiển thị đã bị cắt bớt):C:\Users\MaldevUser\Desktop\HelloWorld\x64\Release>dumpbin.exe /IMPORTS HelloWorld.exe Microsoft (R) COFF/PE Dumper Version 14.42.34438.0 Copyright (C) Microsoft Corporation. All rights reserved. Dump of file HelloWorld.exe File Type: EXECUTABLE IMAGE Section contains the following imports: VCRUNTIME140.dll 140002080 Import Address Table 140002A00 Import Name Table 0 time date stamp 0 Index of first forwarder reference 1C __current_exception_context 1B __current_exception 8 __C_specific_handler 3E memset 3C memcpy api-ms-win-crt-stdio-l1-1-0.dll 140002178 Import Address Table 140002AF8 Import Name Table 0 time date stamp 0 Index of first forwarder reference 3 __stdio_common_vfprintf 0 __acrt_iob_func 1 __p__commode 54 _set_fmode api-ms-win-crt-runtime-l1-1-0.dll 1400020E0 Import Address Table 140002A60 Import Name Table 0 time date stamp 0 Index of first forwarder reference 15 _c_exit 67 terminate 40 _seh_filter_exe 42 _set_app_type 3C _register_onexit_function 1E _crt_atexit 16 _cexit 5 __p___argv
-
Size: kích thước của file binary đầu ra khá lớn. Ví dụ, chương trình HelloWorld có kích thước khoảng 11KB:
C:\Users\MaldevUser\Desktop\HelloWorld\x64\Release>dir Volume in drive C has no label. Volume Serial Number is 0450-6721 Directory of C:\Users\MaldevUser\Desktop\HelloWorld\x64\Release 03/07/2025 08:26 PM <DIR> . 03/07/2025 08:26 PM <DIR> .. 03/07/2025 08:26 PM 11,264 HelloWorld.exe
-
Debugging Information: việc biên dịch ở chế độ Release vẫn sinh ra file binary có chứa thông tin liên quan đến việc debug và các chuỗi khác mà có thể được dùng để làm signature. Ví dụ, hình bên dưới liệt kê (một phần) các chuỗi có trong chương trình HelloWorld:
The CRT Library
Thư viện CRT (hay Microsoft C Run-Time Library) là một tập các hàm và macro low-level thực hiện các tác vụ cơ bản cho các chương trình C/C++. Nó bao gồm các hàm quản lý bộ nhớ (ví dụ malloc
, memset
và free
), xử lý chuỗi (ví dụ strcpy
và strlen
) và nhập xuất (ví dụ printf
, wprintf
và scanf
).
Các DLL của CRT library thường có tên là vcruntimeXXX.dll
với XXX
là số phiên bản. Ví dụ, vcruntime140.dll
là của Visual Studio 2015, vcruntime141.dll
là của Visual Studio 2017, etc. Ngoài ra còn có các DLL khác cũng liên quan đến thư viện CRT chẳng hạn như api-ms-win-crt-stdio-l1-1-0.dll
, api-ms-win-crt-runtime-l1-1-0.dll
và api-ms-win-crt-locale-l1-1-0.dll
. Lý do mà các DLL này xuất hiện ở trong IAT như trên là do chúng sẽ được liên kết động trong quá trình thực thi.
Solving Compatibility Issues
Theo mặc định, khi biên dịch một chương trình, option Runtime Library ở trong Visual Studio sẽ có giá trị là “Multi-threaded DLL (/MD)“. Khi sử dụng giá trị này, các DLL của thư viện CRT sẽ được liên kết động vào chương trình trong lúc chạy. Nếu máy không có các DLL này, chương trình sẽ bị crash. Đây chính là nguyên nhân dẫn đến vấn đề liên quan đến tính tương thích và sự xuất hiện của các unused import function ở trong IAT như đã đề cập ở phần Default Compiler Settings.
Để các DLL của thư viện CRT được liên kết tĩnh trong lúc biên dịch và giảm thiểu các unused import function ở trong IAT, ta có thể chuyển option Runtime Library thành “Multi-threaded (/MT)”:
Info
Cấu hình này giúp giải quyết vấn đề 1 và vấn đề 2 của phần Default Compiler Settings nhưng lại khiến cho vấn đề thứ 3 về kích thước file binary trầm trọng hơn.
Multi-threaded (/MT)
Khi sử dụng “Multi-threaded (/MT)”, hàm __stdio_common_vfprintf
của chương trình HelloWorld sẽ được include trực tiếp vào vùng .text
của file binary:
Chứ không được liên kết động từ ucrtbase.dll
trong lúc chạy:
Tất nhiên, việc sử dụng ” Multi-threaded (/MT)” sẽ làm kích thước của chương trình HelloWorld tăng lên đáng kể:
C:\Users\MaldevUser\Desktop\HelloWorld\x64\Release>dir
Volume in drive C has no label.
Volume Serial Number is 0450-6721
Directory of C:\Users\MaldevUser\Desktop\HelloWorld\x64\Release
03/07/2025 08:26 PM <DIR> .
03/07/2025 08:26 PM <DIR> ..
03/07/2025 09:56 PM 138,752 HelloWorld.exe
03/07/2025 09:56 PM 3,723,264 HelloWorld.pdb
Đồng thời, IAT của chương trình cũng có nhiều Windows API hơn (kết quả hiển thị đã bị cắt bớt):
C:\Users\MaldevUser\Desktop\HelloWorld\x64\Release>dumpbin /IMPORTS HelloWorld.exe
Microsoft (R) COFF/PE Dumper Version 14.42.34438.0
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file HelloWorld.exe
File Type: EXECUTABLE IMAGE
Section contains the following imports:
KERNEL32.dll
140016000 Import Address Table
14001FE88 Import Name Table
0 time date stamp
0 Index of first forwarder reference
4F5 RtlCaptureContext
4FD RtlLookupFunctionEntry
504 RtlVirtualUnwind
5E6 UnhandledExceptionFilter
5A4 SetUnhandledExceptionFilter
232 GetCurrentProcess
5C4 TerminateProcess
3A8 IsProcessorFeaturePresent
470 QueryPerformanceCounter
233 GetCurrentProcessId
237 GetCurrentThreadId
30A GetSystemTimeAsFileTime
38A InitializeSListHead
3A0 IsDebuggerPresent
2F1 GetStartupInfoW
295 GetModuleHandleW
503 RtlUnwindEx
27D GetLastError
564 SetLastError
149 EnterCriticalSection
3E0 LeaveCriticalSection
123 DeleteCriticalSection
386 InitializeCriticalSectionAndSpinCount
5D6 TlsAlloc
5D8 TlsGetValue
Tuy nhiên, như có thể thấy, chương trình không còn phụ thuộc vào các DLL của thư viện CRT nữa và có thể chạy ở trên máy không có Visual Studio.
Additional Compiler Changes
Thay vì liên kết tĩnh thư viện CRT, Chúng ta có thể không phụ thuộc vào nó để làm giảm kích thước của file binary cũng như là loại bỏ các unused import function và các thông tin debug. Cách làm này giải quyết cả 4 vấn đề của phần Default Compiler Settings.
Ta sẽ cấu hình Visual Studio như sau để loại bỏ CRT:
Disable C++ Exceptions
Option Enable C++ Exceptions giúp chương trình có thể lan truyền ngoại lệ bị quăng ra bởi code nhằm hiển thị root cause. Tuy nhiên, do không còn sử dụng thư viện CRT nên option này là không cần thiết.
Disable nó đi như sau:
Disable Whole Program Optimization
Việc disable chức năng Whole Program Optimization có thể giúp ngăn cản compiler thực hiện các tối ưu mà khiến cho call stack bị thay đổi.
Disable Debug Info
Option Generate Debug Info và option Generate Manifest cần được disable để loại bỏ các thông tin debug.
Info
Cấu hình này giúp giải quyết được vấn đề 4 của phần Default Compiler Settings.
Ignore All Default Libraries
Gán giá trị của option Ignore All Default Libraries thành “Yes (/NODEFAULTLIB)” sẽ giúp loại bỏ các thư viện mặc định của hệ thống bao gồm cả thư viện CRT.
Khi đó, ta cần phải tự cung cấp các hàm mà chương trình cần chẳng hạn như printf
. Nếu không, ta sẽ gặp các lỗi biên dịch như sau:
Setting Entry Point Symbol
Lỗi đầu tiên (“LNK2001 - unresolved external symbol mainCRTStartup
”) cho biết rằng compiler không thể tìm thấy symbol mainCRTStartup
, là entry point của chương trình khi được biên dịch với thư viện CRT. Để giải quyết vấn đề này, ta sẽ thay đổi entry point thành hàm main
:
Disable Security Check
Nếu gặp lỗi “LNK2001 - unresolved external symbol __security_check_cookie
” như sau:
Thì có nghĩa là compiler không thể tìm thấy symbol __security_check_cookie
. Đây là symbol dùng để thực hiện stack cookie check giúp ngăn chặn buffer overflow. Để giải quyết lỗi này, ta gán giá trị của option Security Check thành “Disable Security Check (/Gs-)”:
Disable SDL Checks
Khi security check bị disabled, ta có thể gặp warning sau:
Warning “D9025 - overriding ‘/sdl’ with ‘/GS-’” có thể được giải quyết bằng cách disable SDL (Security Development Lifecycle) checks:
Replacing CRT Library Functions
Ta chỉ còn lại 2 lỗi sau:
Hai lỗi này xuất phát từ việc sử dụng hàm của thư viện CRT (printf
) nhưng chúng ta lại không liên kết nó vào chương trình. Thông thường, khi loại bỏ thư viện CRT, ta cần phải tự viết lại các hàm của nó chẳng hạn như printf
, strlen
, strcat
và memcpy
. May mắn thay, ta có thể sử dụng các hàm được viết sẵn của VX-API chẳng hạn như StringCompare
để thay thế cho hàm strcmp
.
Replacing printf
Để thay thế printf
, ta sẽ sử dụng macro sau:
#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 ); \
} \
}
Macro trên nhận vào 2 đối số:
STR
: format string.__VA_ARGS__
hay...
: các đối số mà ta cần in ra console.
Có thể thấy, macro PRINTA
cấp phát vùng nhớ buf
với kích thước 1024 bytes và sau đó sử dụng hàm wsprintfA
để ghi các đối số từ __VA_ARGS__
vào buffer sử dụng format string (STR
). Sau đó, nó gọi hàm WriteConsole
của Windows API để ghi chuỗi đã được format ra console với handle của console được lấy từ GetStdHandle
. Chương trình HelloWorld mà không phụ thuộc vào thư viện CRT:
#include <Windows.h>
#include <stdio.h>
#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 ); \
} \
}
int main() {
PRINTA("Hello World ! \n");
return 0;
}
Building a CRT Library Independent Malware
Khi build malware mà không sử dụng thư viện CRT, ta cần chú ý những điểm sau:
Intrinsic Function Usage
Một vài hàm và macro của Visual Studio sử dụng các hàm của thư viện CRT. Ví dụ, macro ZeroMemory
sử dụng hàm memset
của thư viện CRT. Khi không sử dụng thư viện CRT, ta cần thay thế hàm ZeroMemory
bằng một hàm khác chẳng hạn như hàm CopyMemoryEx
sau:
PVOID CopyMemoryEx(_Inout_ PVOID Destination, _In_ CONST PVOID Source, _In_ SIZE_T Length)
{
PBYTE D = (PBYTE)Destination;
PBYTE S = (PBYTE)Source;
while (Length--)
*D++ = *S++;
return Destination;
}
Một giải pháp khác là tự viết một phiên bản khác của memset
và cấu hình compiler để ZeroMemory
sử dụng nó.
Để làm được điều này, ta cần khai báo hàm memset
sử dụng keyword extern
và calling convention là __cdecl
như sau:
// The `extern` keyword sets the `memset` function as an external function.
extern void* __cdecl memset(void*, int, size_t);
Việc sử dụng keyword extern
cho memset
giúp hàm này có thể được truy cập bởi tất cả các hàm của Visual Studio. Trong khi đó, do thư viện CRT sử dụng calling convention của memset
là __cdecl
nên ta cũng phải sử dụng nó.
Sau đó, sử dụng hai chỉ thị trình biên dịch của Microsoft là #pragma intrinsic()
và #pragma function()
cho memset
như sau:
// The `#pragma intrinsic(memset)` and #pragma function(memset) macros are Microsoft-specific compiler instructions.
// They force the compiler to generate code for the memset function using a built-in intrinsic function.
#pragma intrinsic(memset)
#pragma function(memset)
Với:
-
#pragma intrinsic(memset)
giúp chỉ định cho compiler biếtmemset
là một intrinsic function. Khi biên dịch một intrinsic function, trình biên dịch sẽ thay thế lời gọi hàm đến hàm bằng các chỉ thị hợp ngữ đã được biên dịch và tối ưu của hàm, giống với inline function. Điều này giúp giảm thiểu việc gọi hàm bao gồm nhiều bước chẳng hạn như push các tham số vào stack, thực thi hàm và sau đó pop các tham số ra khỏi stack. Ví dụ, xét inline function sau:inline void swap(int *m, int *n) { int tmp = *m; *m = *n; *n = tmp; }
Lời gọi hàm của nó ở trong code chẳng hạn như
swap(&x, &y);
sẽ được trình biên dịch thay thế thành:int tmp = x; x = y; y = tmp;
Intrinsic function khác inline function ở chỗ nó thay thế lời gọi hàm bằng các chỉ thị hợp ngữ chứ không phải các dòng code mã nguồn.
-
#pragma function(memset)
đảm bảo trình biên dịch sử dụng định nghĩa hàm ở trong file mã nguồn để biên dịch thay vì sử dụng định nghĩa hàm của thư viện CRT.
Cuối cùng, ta viết lại hàm memset
như sau:
void* __cdecl memset(void* Destination, int Value, size_t Size) {
// logic similar to memset's one
unsigned char* p = (unsigned char*)Destination;
while (Size > 0) {
*p = (unsigned char)Value;
p++;
Size--;
}
return Destination;
}
Sau khi viết lại hàm memset
, ta có thể sử dụng nó thông qua ZeroMemory
một cách bình thường như sau:
#include <Windows.h>
int main() {
PVOID pBuff = HeapAlloc(GetProcessHeap(), 0, 0x100);
if (pBuff == NULL)
return -1;
// this will use our version of 'memset' instead of CRT's Library version
ZeroMemory(pBuff, 0x100);
HeapFree(GetProcessHeap(), 0, pBuff);
return 0;
}
Hiding The Console Window
Malware không cần phải mở cửa sổ console trong khi chạy do hành động này rất đáng ngờ và cho phép người dùng có khả năng kết thúc chương trình bằng cách đóng cửa sổ console.
Để giải quyết vấn đề này, ta có thể cấu hình để Visual Studio biên dịch chương trình như là một chương trình có giao diện người dùng (GUI program) bằng cách gán giá trị của option SubSystem là “Windows (/SUBSYSTEM:WINDOWS)”:
Demo
Việc loại bỏ thư viện CRT giúp giảm kích thước của file binary:
Đồng thời, nó cũng loại bỏ các hàm không dùng đến ở trong IAT:
File binary chứa ít chuỗi hơn và không có thông tin debug:
Resources
Footnotes
-
Xem thêm Binary Entropy Reduction ↩