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ểmChế độ DebugChế độ 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.
DebuggingDễ 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:

  1. 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:

  2. 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
  3. 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
  4. 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ụ mallocmemset và free), xử lý chuỗi (ví dụ strcpy và strlen) và nhập xuất (ví dụ printfwprintf 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.dllapi-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ư printfstrlenstrcat 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__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()#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ết memset 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

  1. Xem thêm Binary Entropy Reduction