1. 程式人生 > 其它 >羽夏逆向指引——注入

羽夏逆向指引——注入

寫在前面

  此係列是本人一個字一個字碼出來的,包括示例和實驗截圖。可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我

你如果是從中間插過來看的,請仔細閱讀 羽夏逆向指引——序 ,方便學習本教程。

簡述

  在安全領域,你或多或少聽過注入這個名詞,並瞭解高中注入手段:遠端執行緒注入、APC注入、訊息注入、輸入法注入、修改PE結構注入。這一切的一切的目的就是將自己的Dll注入到目標程序實現自己的目的。但是一旦涉及注入自己的可執行程式碼,如果注入Dll

,這種方式在0環是極易被發現的,並不是隱蔽性很好的攻擊方式。如果注入ShellCode執行,執行完後抹除的話,隱蔽性就明顯的提高。下面我們以Dll注入來介紹並以最簡單的方式實現以下它們的功能。

遠端執行緒注入

實現

  既然注入Dll,我們就得寫一個,如下是其程式碼:

#include "pch.h"

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    {
        MessageBox(NULL, L"注入成功!!!By.WingSummer.", L"CnBlog", MB_ICONINFORMATION);
        break;
    }
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

  這個Dll的作用就是注入成功之後進行彈窗提示,表示注入成功!我們開始進行一下知識鋪墊。
  如何載入Dll呢?我們平時載入的時候會呼叫LoadLibrary這個函式,如下是函式原型:

HMODULE WINAPI LoadLibraryW(
    _In_ LPCWSTR lpLibFileName
    );

  既然是注入,肯定不是我們自己呼叫。讓一個程式碼執行就需要執行緒,如果在對方建立執行緒需要使用如下函式:

HANDLE WINAPI CreateRemoteThread(
    _In_ HANDLE hProcess,
    _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
    _In_ SIZE_T dwStackSize,
    _In_ LPTHREAD_START_ROUTINE lpStartAddress,
    _In_opt_ LPVOID lpParameter,
    _In_ DWORD dwCreationFlags,
    _Out_opt_ LPDWORD lpThreadId
    );

  使用LoadLibrary這個函式,需要傳參一個字串地址,而這個地址正好可以用lpParameter提供,但是,這個地址是被注入的程序,我們需要在被注入的程式寫一個字串。可以在被注入程式申請一塊記憶體,其函式原型如下:

LPVOID WINAPI VirtualAllocEx(
    _In_ HANDLE hProcess,
    _In_opt_ LPVOID lpAddress,
    _In_ SIZE_T dwSize,
    _In_ DWORD flAllocationType,
    _In_ DWORD flProtect
    );

  申請好了地址,就需要寫字串,需要用到的函式如下:

BOOL WINAPI WriteProcessMemory(
    _In_ HANDLE hProcess,
    _In_ LPVOID lpBaseAddress,
    _In_reads_bytes_(nSize) LPCVOID lpBuffer,
    _In_ SIZE_T nSize,
    _Out_opt_ SIZE_T* lpNumberOfBytesWritten
    );

  而在其他程式中申請記憶體和寫記憶體都需要相應的程序控制代碼,我們可以開啟程序,需要的函式如下:

HANDLE WINAPI OpenProcess(
    _In_ DWORD dwDesiredAccess,
    _In_ BOOL bInheritHandle,
    _In_ DWORD dwProcessId
    );

  OpenProcess函式的引數dwProcessId表示的是程序ID,這個我們通過輸入的方式進行。
  有了以上知識鋪墊之後,我們就可以寫程式碼了。但是你可能有疑問,LoadLibrary的地址被注入和注入程序是一樣的嗎?當然是的。獲取函式的時候,我們還需要GetProcAddress函式,其函式原型如下:

FARPROC WINAPI GetProcAddress(
    _In_ HMODULE hModule,
    _In_ LPCSTR lpProcName
    );

  具體程式碼實現如下:

#include <iostream>
#include<Windows.h>
using namespace std;

#define DllPath L"*:\\****\\DllTest.dll" //根據自己的 Dll 路徑來定

int main()
{
    HMODULE lib = LoadLibrary(L"kernel32.dll");
    if (lib)
    {
        FARPROC loadlib = GetProcAddress(lib, "LoadLibraryW");
        if (loadlib)
        {
            cout << "請輸入注入 PID:";
            DWORD pid;
            cin >> pid;

            HANDLE hprocess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
            if (hprocess)
            {
                LPVOID addr = VirtualAllocEx(hprocess, NULL, 4000, MEM_COMMIT, PAGE_READWRITE);
                if (addr)
                {
                    if (WriteProcessMemory(hprocess, addr, DllPath, sizeof(DllPath), NULL))
                    {
                        HANDLE hthread = CreateRemoteThread(hprocess, NULL, NULL, (LPTHREAD_START_ROUTINE)loadlib, addr, 0, NULL);
                        if (hthread)
                        {
                            cout << "注入成功!!!" << endl;
                            CloseHandle(hthread);
                        }
                        CloseHandle(hprocess);
                    }
                    else
                    {
                        cout << "WriteProcessMemory 失敗!" << endl;
                    }
                }
                else
                {
                    cout << "VirtualAllocEx 失敗!" << endl;
                }
            }
            else
            {
                cout << "OpenProcess 失敗!" << endl;
            }
        }
        else
        {
            cout << "獲取 LoadLibraryW 地址失敗!" << endl;
        }
    }
    else
    {
        cout << "獲取 kernel32.dll 地址失敗!" << endl;
    }
    system("pause");
    return 0;
}

  如下是實驗效果圖:

注意事項

  1. 注意程式的位數,64位程式注入64位的DLL,32位注入32位的。
  2. 如果注入高許可權的程式,請具有相應的許可權。
  3. 如果注入系統服務程序的話,需要通過使用未匯出的函式ZwCreateThreadEx,在ntdll裡面,需要手動獲取。由於會話隔離機制,你無法使用彈窗的形式驗證注入成功。

APC 注入

實現

  APC中文名稱為非同步過程呼叫,它是Windows十分重要的機制,如果想要學習其內部細節,請自行學習 羽夏看Win系統核心APC篇。下面我們重點介紹最小化實現。
  我們利用建立程序的方式來實現,為什麼呢?我們來看一下它的函式原型:

BOOL CreateProcessW(
  [in, optional]      LPCWSTR               lpApplicationName,
  [in, out, optional] LPWSTR                lpCommandLine,
  [in, optional]      LPSECURITY_ATTRIBUTES lpProcessAttributes,
  [in, optional]      LPSECURITY_ATTRIBUTES lpThreadAttributes,
  [in]                BOOL                  bInheritHandles,
  [in]                DWORD                 dwCreationFlags,
  [in, optional]      LPVOID                lpEnvironment,
  [in, optional]      LPCWSTR               lpCurrentDirectory,
  [in]                LPSTARTUPINFOW        lpStartupInfo,
  [out]               LPPROCESS_INFORMATION lpProcessInformation
);

  在最後一個函式中,裡面包含程序控制代碼和主執行緒控制代碼,我向執行緒傳送APC的時候就十分方便。下面我們繼續看QueueUserAPC的函式原型:

DWORD QueueUserAPC(
  [in] PAPCFUNC  pfnAPC,
  [in] HANDLE    hThread,
  [in] ULONG_PTR dwData
);

  下面我們來開始寫程式碼:

#include <iostream>
#include<Windows.h>
using namespace std;

#define DllPath L"*:\\****\\DllTest.dll" //根據自己的 Dll 路徑來定

int main()
{
    HMODULE lib = LoadLibrary(L"kernel32.dll");
    if (lib)
    {
        FARPROC loadlib = GetProcAddress(lib, "LoadLibraryW");
        if (loadlib)
        {
            cout << "建立記事本程序開始實驗,按任意鍵繼續……";
            cin.get();

            WCHAR app[] = L"notepad.exe";
            STARTUPINFO info = { sizeof(STARTUPINFO) };
            PROCESS_INFORMATION pi;
            BOOL ret = CreateProcess(NULL, app, NULL, NULL, NULL, 0, NULL, NULL, &info, &pi);

            if (ret)
            {
                LPVOID addr = VirtualAllocEx(pi.hProcess, NULL, 4000, MEM_COMMIT, PAGE_READWRITE);
                if (addr)
                {
                    if (WriteProcessMemory(pi.hProcess, addr, DllPath, sizeof(DllPath), NULL))
                    {
                        if (QueueUserAPC((PAPCFUNC)loadlib, pi.hThread, (ULONG_PTR)addr))
                        {
                            WaitForSingleObjectEx(pi.hThread, -1, TRUE);    //觸發 APC
                            cout << "注入成功!!!" << endl;
                        }
                    }
                    else
                    {
                        cout << "WriteProcessMemory 失敗!" << endl;
                    }
                }
                else
                {
                    cout << "VirtualAllocEx 失敗!" << endl;
                }
            }
            else
            {
                cout << "建立程序失敗!" << endl;
            }

            CloseHandle(pi.hProcess);
            CloseHandle(pi.hThread);
        }
        else
        {
            cout << "獲取 LoadLibraryW 地址失敗!" << endl;
        }
    }
    else
    {
        cout << "獲取 kernel32.dll 地址失敗!" << endl;
    }
    system("pause");
    return 0;
}

  效果圖如下:

注意事項

  1. 注意程式的位數,64位程式注入64位的DLL,32位注入32位的。
  2. 裡面的相關細節請學習我在文中提到的教程,這些東西並不是一言兩語就能說明白的。

訊息注入

  在Windows中大部分的應用程式都是基於訊息機制的,它們都有一個訊息過程函式,根據不同的訊息完成不同的功能。Windows作業系統提供的鉤子機制就是用來截獲和監視系統中這些訊息的。按照鉤子作用的範圍不同,它們又可以分為區域性鉤子和全域性鉤子。區域性鉤子是針對某個執行緒的;而全域性鉤子則是作用於整個系統的基於訊息的應用。全域性鉤子需要使用DLL檔案,在DLL中實現相應的鉤子函式。
  至於為什麼全域性鉤子必須是DLL,簡單思考就可以得到答案,因為我們需要對任何GUI程序進行掛鉤,既然到使用者程序只有DLL能做到。
  我們需要使用SetWindowsHookEx函式進行掛鉤,如下是其函式原型:

HHOOK WINAPI SetWindowsHookEx(
 _In_ int idHook,
 _In_ HOOKPROC lpfn,
 _In_ HINSTANCE hMod,
 _In_ DWORD dwThreadId)

  第一個引數就是表示要安裝的鉤子程式的型別,第二個是處理函式,第三個是包含由lpfn引數指向的鉤子過程的DLL控制代碼,最後一個引數是與鉤子程式關聯的執行緒識別符號,如果此引數為0,則鉤子過程與系統中所有執行緒相關聯。
  在作業系統中安裝全域性鉤子後,只要程序接收到可以發出鉤子的訊息,全域性鉤子的DLL檔案就會由作業系統自動或強行地載入到該程序中。因此,設定全域性鉤子可以達到DLL注入的目的。建立一個全域性鉤子後,在對應事件發生的時候,系統就會把DLL載入到發生事件的程序中,這樣,便實現了DLL注入。
  為了能夠讓DLL注入到所有的程序中,程式設定WH_GETMESSAGE訊息的全域性鉤子。下面我們開始實現DLL

#include "pch.h"

// 共享記憶體
#pragma data_seg("shared")
HHOOK g_hHook = NULL;
#pragma data_seg()
#pragma comment(linker, "/SECTION:shared,RWS")

#define EXPORT extern "C" __declspec(dllexport)

HMODULE ghModule;

// 鉤子回撥函式
LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam)
{
    return ::CallNextHookEx(g_hHook, code, wParam, lParam);
}

EXPORT BOOL SetGlobalHook()
{
    g_hHook = ::SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, ghModule, 0);
    if (NULL == g_hHook)
    {
        return FALSE;
    }
    return TRUE;
}

// 解除安裝鉤子
EXPORT BOOL UnsetGlobalHook()
{
    if (g_hHook)
    {
        ::UnhookWindowsHookEx(g_hHook);
    }
    return TRUE;
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    {
        ghModule = hModule;
        break;
    }
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

  上面程式碼實現了全域性鉤子的設定、鉤子回撥函式的實現以及全域性鉤子的解除安裝,這些操作都需要用到全域性鉤子的控制代碼作為引數。而全域性鉤子是以DLL形式載入到其他程序空間中的,而且程序都是獨立的,所以任意修改其中一個記憶體裡的資料是不會影響另一個程序的。那麼,如何將鉤子控制代碼傳遞給其他程序呢?為了解決這個問題,這裡採用的方法是在DLL中建立共享記憶體。
  共享記憶體是指突破程序獨立性,多個程序共享同一段記憶體。在DLL中建立共享記憶體,就是在DLL中建立一個變數,然後將DLL載入到多個程序空間,只要一個程序修改了該變數值,其他程序DLL中的這個值也會改變,就相當於多個程序共享一個記憶體。
  在上面的程式碼中,使用#pragma data_seg建立了一個名為shared的資料段,然後使用/section:shared,RWSshared資料段設定為可讀、可寫、可共享的共享資料段。
  下面我們實現載入全域性鉤子的程式:

#include <iostream>
#include<Windows.h>
using namespace std;

#define DllPath L"E:\\VsProject\\C++\\DllTest\\x64\\Debug\\DllTest.dll"

typedef BOOL(*SetGlobalHook)();
typedef BOOL (*UnsetGlobalHook)();

int main()
{
    HMODULE lib = LoadLibrary(DllPath);
    if (lib)
    {
        SetGlobalHook sethook = (SetGlobalHook)GetProcAddress(lib, "SetGlobalHook");
        UnsetGlobalHook unsethook = (UnsetGlobalHook)GetProcAddress(lib, "UnsetGlobalHook");
        if (sethook&&unsethook)
        {
            if (sethook())
            {
                cout << "已被 Hook ,按任意鍵取消 Hook ……" << endl;
                cin.get();
                unsethook();
            }
        }
        else
        {
            cout << "獲取函式失敗!!!" << endl;
        }
    }
    else
    {
        cout << "載入全域性 Hook 失敗!!!" << endl;
    }
    system("pause");
    return 0;
}

  然後我們載入鉤子之後,啟動新的記事本,就可以發現DLL被注入了。

輸入法注入

  IME輸入法實際就是一個DLL檔案,只不過字尾為IME罷了,需要匯出必要的介面供系統載入輸入法時呼叫。我們可以在此IME檔案的DllMain函式的入口通過呼叫LoadLibrary函式來載入需要注入的DLL
  對於IME,必須匯出如下函式:

ImeConversionList           //將字串/字元轉換成目標字串/字元 
ImeConfigure                //設定ime引數  
ImeDestroy                  //退出當前使用的IME  
ImeEscape                   //應用軟體訪問輸入法的介面函式  
ImeInquire                  //啟動並初始化當前ime輸入法  
ImeProcessKey               //ime輸入鍵盤事件管理函式  
ImeSelect                   //啟動當前的ime輸入法  
ImeSetActiveContext         //設定當前的輸入處於活動狀態  
ImeSetCompositionString     //由應用程式設定輸入法編碼  
ImeToAsciiEx                //將輸入的鍵盤事件轉換為漢字編碼事件  
NotifyIME                   //ime事件管理函式  
ImeRegisterWord             //向輸入法字典註冊字串  
ImeUnregisterWord           //刪除被註冊的字串  
ImeGetRegisterWordStyle  
ImeEnumRegisterWord  

  其中最重要的就是ImeInquire函式,當切換到此輸入法時此函式就會被呼叫啟動並初始化輸入法。引數lpIMEInfo用於輸入對輸入法初始化的內容結構,引數lpszUIClass為輸入法的視窗類。lpszUIClass對應的視窗類必須已註冊,我們應該在DllMain入口處註冊此視窗類,我們來看一下函式原型:

BOOL WINAPI ImeInquire(LPIMEINFO lpIMEInfo,LPTSTR lpszUIClass,LPCTSTR lpszOption);

  由於實現起來還是比較複雜的,其原理就是用輸入法弄個殼,安裝好,被觸發到然後執行目的碼,具體就不實現了。

下一篇

  羽夏逆向指引——符號