簡體   English   中英

鈎函數我不知道參數

[英]Hooking a function I don't know the parameters to

可以說有一個具有已知入口點DoStuff的DLL A.DLL ,我以某種方式將其與自己的DLL fakeA.dll ,以便系統調用我的DoStuff 如何編寫這樣的函數,以便它可以在不知道函數參數的情況下調用掛鈎的DLL( A.DLL )的相同入口點? 即我的fakeA.DLL函數看起來像

LONG DoStuff(
// don't know what to put here
)
{
    FARPROC pfnHooked;
    HINSTANCE hHooked;
    LONG lRet;

    // get hooked library and desired function

    hHooked = LoadLibrary("A.DLL");
    pfnHooked = GetProcAddress(hHooked, "DoStuff");

    // how do I call the desired function without knowing the parameters?

    lRet = pfnHooked( ??? );

    return lRet;
}

我目前的想法是參數在堆棧上,所以我猜我將必須有足夠大的堆棧變量(例如大ass struct )來捕獲任何參數,然后將其傳遞給pfnHooked嗎?

// actual arg stack limit is >1MB but we'll assume 1024 bytes is sufficient
typedef struct { char unknownData[1024]; } ARBITARY_ARG; 

ARBITARY_ARG DoStuff(ARBITARY_ARG args){
    ARBITARY_ARG aRet;
    ...
    aRet = pfnHooked(args);

    return aRet;
}

這行得通嗎? 如果是這樣,有沒有更好的方法?


更新:經過一些基本(非結論性)測試后,將任意塊作為參數傳遞確實起作用(這並不奇怪,因為該程序將從堆棧中讀取所需內容)。 但是,收集返回值比較困難,因為返回值太大,可能會導致訪問沖突。 將任意返回大小設置為8個字節(對於x86,可能為4個字節)可能是大多數情況(包括空返回)的解決方案,但這仍然是猜測。 如果我有某種方法可以知道DLL的返回類型(不一定在運行時),那將是很大的事情。

這應該是一個注釋,但meta回答是,您可以在x64 / x86平台上掛接函數而無需知道調用約定和參數。 可以純粹用C完成嗎? 不,它還需要對各種調用約定和Assembly編程有充分的了解。 掛鈎框架將用匯編語言編寫其中的一些內容。

大多數掛鈎框架通過創建一個蹦床來固有地做到這一點,該蹦床將執行流從被調用函數的前導重定向到存根代碼,該存根代碼通常獨立於它所掛鈎的函數。 在用戶模式下,可以確保堆棧始終存在,因此您可以將自己的局部變量也推送到同一堆棧上,只要可以彈出它們並將堆棧恢復到原始狀態即可。

您實際上不需要將現有參數復制到自己的堆棧變量中。 您可以檢查堆棧,一定要閱讀一些有關調用約定的知識,以及在嘗試進行任何操作之前,如何針對匯編中的各種類型的調用在不同體系結構上構建堆棧。

是的,通用鈎子可以做到100%正確-對於具有不同參數計數和調用約定的多個函數來說,這是常見的。 適用於兩個x86 / x64(amd64)平台。

但是為此需要使用很少的asm存根-當然,對於x86 / x64來說會有所不同-但它會非常小-僅幾行代碼-2個小存根過程-一個用於過濾器調用,而一個用於調用后。 但是大多數代碼實現(95%+)將與平台無關並且在c ++中實現 (當然這可以在c上實現,但是可以與c ++相比-c源代碼更大,更難看並且更難實現)

在我的解決方案中,需要為每個掛鈎API分配小的可執行代碼塊(每個掛鈎api一個塊)。 在此塊中-存儲函數名稱,原始地址(或預調用后的傳輸控制的位置-這取決於掛鈎方法)和一條到公共asm預調用存根的相對調用指令。 這種調用的神奇之處不僅在於它可以將控制權轉移到公共存根,而且堆棧中的返回地址將指向自身本身(確定,帶有一些偏移量,但是如果我們將使用c ++和繼承,它將恰好指向某個基礎類,從中派生可執行塊類)。 作為公共precall存根的結果,我們將獲得信息-我們在此處掛接到哪個api調用,然后將此信息傳遞給c ++公共處理程序。

請注意,因為在x64中相對調用只能在[rip-0x80000000, rip+0x7fffffff]范圍內[rip-0x80000000, rip+0x7fffffff]需要在單獨的bss部分中的PE中聲明(分配)此代碼塊,並將此部分標記為RWE 我們不能簡單地使用VirtualAlloc來分配存儲,因為返回的地址可能與我們的通用預調用存根相距太遠。

在常見的asm 預調用存根代碼中,必須將rcx,rdx,r8,r9寄存器保存為x64 (這是絕對必要的 ),而將ecx,edx寄存器保存為x86。 如果函數使用__fastcall調用約定,則需要這樣做 但是,例如,Windows api幾乎不使用__fastcall-數千個win api中僅存在幾個__fastcall函數(為確保找到並找到此函數,請轉到LIB文件夾並搜索__imp_@字符串(這是__fastcall的通用前綴),然后調用c ++通用處理程序,必須將原始函數的地址(傳遞控制到該地址)返回到存根。存根恢復rcx,rdx,r8,r9 (或ecx,edx )寄存器並跳轉 (但不調用 !)到該地址

如果我們只想過濾預調用,這就是我們所需要的。 但是在大多數情況下,需要過濾器(掛接)和調用后-用於查看/修改函數的返回值和out參數。 這也是可行的,但只需要更多的編碼。

對於鈎子調用,顯然我們必須替換鈎子api的返回地址。 但是我們必須更改寄信人地址嗎? 在哪里保存原始寄信人地址? 為此,我們不能使用全局變量。 甚至不能使用本地線程( __declspec( thread )thread_local ),因為調用可能是必需的。 不能使用易失性寄存器(因為在api調用期間已更改),也不能使用非易失性寄存器-因為在這種情況下,我們將其保存,以便稍后還原-但出現了一個問題-哪里?

這里只有一個(也是不錯的)解決方案-將一小塊可執行內存( RWE )分配給一個普通的調用后asm存根,其中包含一個相對調用指令。 和一些數據-保存的原始返回地址,函數參數(用於在后處理程序中簽出參數)和函數名稱

在這里,還是x64的某個發行者-該塊必須與普通的后期存根(+/- 2GB)相距不遠-因此最好也將此存根分配到單獨的.bss節中(帶有預調用存根)。

有多少需要這個存根? 每個api調用一個(如果我們要控制post調用)。 因此在任何時候都不會超過激活的api調用。 通常說256個預分配的塊-綽綽有余。 即使我們在調用前分配該塊失敗-我們也不會在調用后控制它,而不會崩潰。 而且我們不能為所有掛鈎的api都希望控制調用后調用,而僅對某些調用。

對於非常快速且互鎖的分配/釋放,此塊-需要在其上構建堆棧語義。 通過聯鎖彈出分配,並通過聯鎖推釋放。 並在開始時預初始化(調用指令)此塊(同時將其全部壓入堆棧,以免每次調用前都不重新初始化)

asm中常見的調用后存根非常簡單-在這里我們不需要保存任何寄存器。 我們只需調用C ++后處理程序的塊地址(我們彈出它從棧- 調用指令的結果),並與原來的返回值(RAXEAX)。 嚴格地說-api函數可以返回成對的rax + rdxeax + edx,但Windows api的99.9%+會在單個寄存器中返回值,我假設我們只會鈎住此api。 但是,如果需要的話,也幾乎不能調整代碼來處理此問題(在大多數情況下根本不需要)

C ++調用處理程序恢復原始返回地址(通過使用_AddressOfReturnAddress() ),可以記錄調用和/或修改參數,最后返回到api的原始調用者。 我們的處理程序返回的內容-這將是api調用的最終返回值。 通常我們會返回原始值。

C ++代碼

#if 0 
#define __ASM_FUNCTION __pragma(message(__FUNCDNAME__" proc\r\n" __FUNCDNAME__ " endp"))
#define _ASM_FUNCTION {__ASM_FUNCTION;}
#define ASM_FUNCTION {__ASM_FUNCTION;return 0;}
#define CPP_FUNCTION __pragma(message("extern " __FUNCDNAME__ " : PROC ; "  __FUNCTION__))
#else
#define _ASM_FUNCTION
#define ASM_FUNCTION
#define CPP_FUNCTION
#endif

class CODE_STUB
{
#ifdef _WIN64
    PVOID pad;
#endif
    union
    {
        DWORD code;
        struct  
        {
            BYTE cc[3];
            BYTE call;
        };
    };
    int offset;

public:

    void Init(PVOID stub)
    {
        // int3; int3; int3; call stub
        code = 0xe8cccccc;
        offset = RtlPointerToOffset(&offset + 1, stub);

        C_ASSERT(sizeof(CODE_STUB) == RTL_SIZEOF_THROUGH_FIELD(CODE_STUB, offset));
    }

    PVOID Function()
    {
        return &call;
    }

    // implemented in .asm
    static void __cdecl retstub()  _ASM_FUNCTION;
    static void __cdecl callstub() _ASM_FUNCTION;
};

struct FUNC_INFO
{
    PVOID OriginalFunc;
    PCSTR Name;

    void* __fastcall OnCall(void** stack);
};

struct CALL_FUNC : CODE_STUB, FUNC_INFO
{
};  

C_ASSERT(FIELD_OFFSET(CALL_FUNC,OriginalFunc) == sizeof(CODE_STUB));

struct RET_INFO 
{
    union
    {
        struct  
        {
            PCSTR Name;
            PVOID params[7];
        };

        SLIST_ENTRY Entry;
    };

    INT_PTR __fastcall OnCall(INT_PTR r);
};

struct RET_FUNC : CODE_STUB, RET_INFO 
{
};

C_ASSERT(FIELD_OFFSET(RET_FUNC, Entry) == sizeof(CODE_STUB));

#pragma bss_seg(".HOOKS")

RET_FUNC g_rf[1024];//max call count
CALL_FUNC g_cf[16];//max hooks count

#pragma bss_seg() 

#pragma comment(linker, "/SECTION:.HOOKS,RWE")

class RET_FUNC_Manager 
{
    SLIST_HEADER _head;

public:

    RET_FUNC_Manager()
    {
        PSLIST_HEADER head = &_head;

        InitializeSListHead(head);

        RET_FUNC* p = g_rf;
        DWORD n = RTL_NUMBER_OF(g_rf);

        do 
        {
            p->Init(CODE_STUB::retstub);
            InterlockedPushEntrySList(head, &p++->Entry);
        } while (--n);
    }

    RET_FUNC* alloc()
    {
        return static_cast<RET_FUNC*>(CONTAINING_RECORD(InterlockedPopEntrySList(&_head), RET_INFO, Entry));
    }

    void free(RET_INFO* p)
    {
        InterlockedPushEntrySList(&_head, &p->Entry);
    }
} g_rfm;

void* __fastcall FUNC_INFO::OnCall(void** stack)
{
    CPP_FUNCTION;

    // in case __fastcall function in x86 - param#1 at stack[-1] and param#2 at stack[-2]

    // this need for filter post call only
    if (RET_FUNC* p = g_rfm.alloc())
    {
        p->Name = Name;
        memcpy(p->params, stack, sizeof(p->params));
        *stack = p->Function();
    }

    return OriginalFunc;
}

INT_PTR __fastcall RET_INFO::OnCall(INT_PTR r)
{
    CPP_FUNCTION;

    *(void**)_AddressOfReturnAddress() = *params;

    PCSTR name = Name;
    char buf[8];
    if (IS_INTRESOURCE(name))
    {
        sprintf(buf, "#%04x", (ULONG)(ULONG_PTR)name), name = buf;
    }

    DbgPrint("%p %s(%p, %p, %p ..)=%p\r\n", *params, name, params[1], params[2], params[3], r);
    g_rfm.free(this);
    return r;
}

struct DLL_TO_HOOK 
{
    PCWSTR szDllName;
    PCSTR szFuncNames[];
};

void DoHook(DLL_TO_HOOK** pp)
{
    PCSTR* ppsz, psz;
    DLL_TO_HOOK *p;
    ULONG n = RTL_NUMBER_OF(g_cf);

    CALL_FUNC* pcf = g_cf;

    while (p = *pp++)
    {
        if (HMODULE hmod = LoadLibraryW(p->szDllName))
        {
            ppsz = p->szFuncNames;

            while (psz = *ppsz++)
            {
                if (pcf->OriginalFunc = GetProcAddress(hmod, psz))
                {
                    pcf->Name = psz;
                    pcf->Init(CODE_STUB::callstub);

                    // do hook: pcf->OriginalFunc -> pcf->Function() - code for this skiped
                    DbgPrint("hook: (%p) <- (%p)%s\n", pcf->Function(), pcf->OriginalFunc, psz);

                    if (!--n)
                    {
                        return;
                    }

                    pcf++;
                }
            }
        }
    }
}

asm x64代碼:

extern ?OnCall@FUNC_INFO@@QEAAPEAXPEAPEAX@Z : PROC ; FUNC_INFO::OnCall
extern ?OnCall@RET_INFO@@QEAA_J_J@Z : PROC ; RET_INFO::OnCall

?retstub@CODE_STUB@@SAXXZ proc
    pop rcx
    mov rdx,rax
    call ?OnCall@RET_INFO@@QEAA_J_J@Z
?retstub@CODE_STUB@@SAXXZ endp

?callstub@CODE_STUB@@SAXXZ proc
    mov [rsp+10h],rcx
    mov [rsp+18h],rdx
    mov [rsp+20h],r8
    mov [rsp+28h],r9
    pop rcx
    mov rdx,rsp
    sub rsp,18h
    call ?OnCall@FUNC_INFO@@QEAAPEAXPEAPEAX@Z
    add rsp,18h
    mov rcx,[rsp+8]
    mov rdx,[rsp+10h]
    mov r8,[rsp+18h]
    mov r9,[rsp+20h]
    jmp rax
?callstub@CODE_STUB@@SAXXZ endp

asm x86代碼

extern ?OnCall@FUNC_INFO@@QAIPAXPAPAX@Z : PROC ; FUNC_INFO::OnCall
extern ?OnCall@RET_INFO@@QAIHH@Z : PROC ; RET_INFO::OnCall

?retstub@CODE_STUB@@SAXXZ proc
    pop ecx
    mov edx,eax
    call ?OnCall@RET_INFO@@QAIHH@Z
?retstub@CODE_STUB@@SAXXZ endp

?callstub@CODE_STUB@@SAXXZ proc
    xchg [esp],ecx
    push edx
    lea edx,[esp + 8]
    call ?OnCall@FUNC_INFO@@QAIPAXPAPAX@Z
    pop edx
    pop ecx
    jmp eax
?callstub@CODE_STUB@@SAXXZ endp

您可以從哪里知道我裝飾過的名字,例如?OnCall@FUNC_INFO@@QAIPAXPAPAX@Z 尋找C ++代碼的最開始-用於多個宏-並首次使用#if 1進行編譯,然后在輸出窗口中查找。 希望您能理解(您可能需要使用這個名稱,但不能使用我的名字-裝飾可以有所不同)

以及如何調用void DoHook(DLL_TO_HOOK** pp) 像那樣:

DLL_TO_HOOK dth_kernel32 = { L"kernel32", { "VirtualAlloc", "VirtualFree", "HeapAlloc", 0 } };
DLL_TO_HOOK dth_ntdll = { L"ntdll", { "NtOpenEvent", 0 } };

DLL_TO_HOOK* ghd[] = { &dth_ntdll, &dth_kernel32, 0 };
DoHook(ghd);

可以說有一個帶有已知入口點DoStuff的DLL A.DLL

如果知道入口點DoStuff ,則應將其記錄在某個地方,至少要記錄在某些C頭文件中。 因此,一種可行的方法可能是解析該標頭以獲取其簽名(即DoStuff的C聲明)。 也許您可以用在所有系統頭文件中聲明的所有函數的簽名填充某個數據庫,等等。或者,如果有的話,可以使用調試信息。

如果您在C中調用某個函數但未提供所有必需的參數,則仍將使用調用約定ABI ,並且這些(丟失的)參數將獲得垃圾值(如果調用約定定義了要傳入的參數)一個寄存器,該寄存器內的垃圾;如果約定定義了要在調用堆棧上傳遞的參數,則為該特定調用堆棧插槽內的垃圾)。 因此,您很可能崩潰,並且肯定有一些未定義的行為 (這很可怕 ,因為您的程序似乎可以運行,但仍然非常錯誤)。

但是,也請查看libffi 一旦知道(在運行時)將傳遞給某個任意函數的內容,就可以構造對它的調用,並傳遞正確的數目和參數類型。

我目前的想法是參數在堆棧上

我認為這是錯誤的(至少在許多x86-64系統上)。 一些參數通過寄存器傳遞。 了解有關x86調用約定的信息

這行得通嗎?

不,它不起作用,因為某些參數是通過寄存器傳遞的,並且調用約定取決於被調用函數的簽名(浮點值可能在不同的寄存器中傳遞,或者始終在堆棧中傳遞;可變參數具有特定的調用約定;等等...)

順便說一句,最近的一些C 優化編譯器能夠進行尾部調用優化,這可能會使事情復雜化。

沒有標准的方法可以執行此操作,因為在傳遞參數時,許多事情(例如調用約定,指針大小等)都很重要。 您將必須閱讀所用平台的ABI並編寫一個實現,我擔心再次用C語言無法實現。您將需要一些內聯匯編。

一種簡單的方法是(對於X86_64這樣的平台)-

MyDoStuff:
    jmpq *__real_DoStuff

這個鈎子什么也沒做,只是調用了實函數。 如果要在掛機時執行任何有用的操作,則必須在調用之前保存恢復某些寄存器的內容(再次保存的內容取決於ABI)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM