Windows下Hook C++ DLL

关键字: Hook windows dll thiscall fastcall

DLL 导出符号

如果导出的DLL是extern "C"类型的话,那么就只有函数名字,不包含任何返回类型以及参数信息.比如Kernel32.dll中的GetProcAddress.
但是如果是导出一个C++类的话,导出的符号就很复杂,比如下面这种情况.

?Refresh_RequestProcess@CFLRPCApi@@QAEHXZ		@36
?Request@CFLRPCApi@@QAEHHPADH0AAHHHH@Z		@37
``
展开以后对应的就是:

```cpp
class FLRPCLib_API CFLRPCApi
{
public:
	int	Refresh_RequestProcess(void);
	int	Request(int,char *,int,char *,int &,int,int,int);
}

x86下的各种调用约定

以下列内容主要来自维基百科
分类依据主要是清理栈的工作是调用者还是被调用者清理.

调用者清理

在这些约定中,调用者自己清理堆栈上的参数(arguments),这样就允许了可变参数列表的实现,如printf()。

cdecl

cdecl(C declaration,即C声明)是源起C语言的一种调用约定,也是C语言的事实上的标准。在x86架构上,其内容包括:

  1. 函数实参在线程栈上按照从右至左的顺序依次压栈。
  2. 函数结果保存在寄存器EAX/AX/AL中
  3. 浮点型结果存放在寄存器ST0中
  4. 编译后的函数名前缀以一个下划线字符
  5. 调用者负责从线程栈中弹出实参(即清栈)
  6. 8比特或者16比特长的整形实参提升为32比特长。
  7. 受到函数调用影响的寄存器(volatile registers):EAX, ECX, EDX, ST0 - ST7, ES, GS
  8. 不受函数调用影响的寄存器: EBX, EBP, ESP, EDI, ESI, CS, DS
  9. RET指令从函数被调用者返回到调用者(实质上是读取寄存器EBP所指的线程栈之处保存的函数返回地址并加载到IP寄存器)
  10. Visual C++规定函数返回值如果是POD值且长度如果不超过32比特,用寄存器EAX传递;长度在33-64比特范围内,用寄存器EAX:EDX传递;长度超过64比特或者非POD值,则调用者为函数返回值预先分配一个空间,把该空间的地址作为隐式参数传递给被调函数。

GCC的函数返回值都是由调用者分配空间,并把该空间的地址作为隐式参数传递给被调函数,而不使用寄存器EAX。GCC自4.5版本开始,调用函数时,堆栈上的数据必须以16B对齐(之前的版本只需要4B对齐即可)。

考虑下面的C代码片段:

  int callee(int, int, int);
  int caller(void)
  {
      register int ret;
      
      ret = callee(1, 2, 3);
      ret += 5;
      return ret;
  }

在x86上, 会产生如下汇编代码(AT&T 语法):

        .globl  caller
  caller:
        pushl   %ebp
        movl    %esp,%ebp
        pushl   $3
        pushl   $2
        pushl   $1
        call    callee
        addl    $12,%esp
        addl    $5,%eax
        leave
        ret

在函数返回后,调用的函数清理了堆栈。 在cdecl的理解上存在一些不同,尤其是在如何返回值的问题上。结果,x86程序经过不同OS平台的不同编译器编译后,会有不兼容的情况,即使它们使用的都是“cdecl”规则并且不会使用系统调用。某些编译器返回简单的数据结构,长度大致占用两个寄存器,放在寄存器对EAX:EDX中;大点的结构和类对象需要异常处理器的一些特殊处理(如一个定义的构造函数,析构函数或赋值),存放在内存上。为了放置在内存上,调用者需要分配一些内存,并且让一个指针指向这块内存,这个指针就作为隐藏的第一个参数;被调用者使用这块内存并返回指针----返回时弹出隐藏的指针。 在Linux/GCC,浮点数值通过x87伪栈被推入堆栈。像这样:

        sub esp, 8      ; 给double值一点空间
        fld [ebp + x]   ; 加载double值到浮点堆栈上
        fstp [esp]      ; 推入堆栈
        call funct
        add esp, 8

使用这种方法确保能以正确的格式推入堆栈。 cdecl调用约定通常作为x86 C编译器的默认调用规则,许多编译器也提供了自动切换调用约定的选项。如果需要手动指定调用规则为cdecl,编译器可能会支持如下语法:

  return_type _cdecl funct();

其中_cdecl修饰符需要在函数原型中给出,在函数声明中会覆盖掉其他的设置。

syscall

与cdecl类似,参数被从右到左推入堆栈中。EAX, ECX和EDX不会保留值。参数列表的大小被放置在AL寄存器中(?)。 syscall是32位OS/2 API的标准。

参数也是从右到左被推入堆栈。从最左边开始的三个字符变元会被放置在EAX, EDX和ECX中,最多四个浮点变元会被传入ST(0)到ST(3)中----虽然这四个参数的空间也会在参数列表的栈上保留。函数的返回值在EAX或ST(0)中。保留的寄存器有EBP, EBX, ESI和EDI。 optlink在IBM VisualAge编译器中被使用。

被调用者清理

如果被调用者要清理栈上的参数,需要在编译阶段知道栈上有多少字节要处理。因此,此类的调用约定并不能兼容于可变参数列表,如printf()。然而,这种调用约定也许会更有效率,因为需要解堆栈的代码不要在每次调用时都生成一遍。 使用此规则的函数容易在asm代码被认出,因为它们会在返回前解堆栈。x86 ret指令允许一个可选的16位参数说明栈字节数,用来在返回给调用者之前解堆栈。代码类似如下:

 ret 12

pascal

基于Pascal语言的调用约定,参数从左至右入栈(与cdecl相反)。被调用者负责在返回前清理堆栈。 此调用约定常见在如下16-bit 平台的编译器:OS/2 1.x,微软Windows 3.x,以及Borland Delphi版本1.x。

register

Borland fastcall的别名。

stdcall

stdcall是由微软创建的调用约定,是Windows API的标准调用约定。非微软的编译器并不总是支持该调用协议。GCC编译器如下使用:

int __attribute__((__stdcall__ )) func()

stdcall是Pascal调用约定与cdecl调用约定的折衷:被调用者负责清理线程栈,参数从右往左入栈。其他各方面基本与cdecl相同。但是编译后的函数名后缀以符号"@",后跟传递的函数参数所占的栈空间的字节长度。寄存器EAX, ECX和EDX被指定在函数中使用,返回值放置在EAX中。stdcall对于微软Win32 API和Open Watcom C++是标准。

微软的编译工具规定:PASCAL, WINAPI, APIENTRY, FORTRAN, CALLBACK, STDCALL, __far __pascal, __fortran, __stdcall均是指此种调用约定。

fastcall

此约定还未被标准化,不同编译器的实现也不一致。

Microsoft/GCC fastcall

Microsoft或GCC的fastcall约定(也即msfastcall)把第一个(从左至右)不超过32比特的参数通过寄存器ECX/CX/CL传递,第二个不超过32比特的参数通过寄存器EDX/DX/DL,其他参数按照自右到左顺序压栈传递。

Borland fastcall

从左至右,传入三个参数至EAX, EDX和ECX中。剩下的参数推入栈,也是从左至右。 在32位编译器Embarcadero Delphi中,这是缺省调用约定,在编译器中以register形式为人知。 在i386上的某些版本Linux也使用了此约定。

调用者或被调用者清理

thiscall

在调用C++非静态成员函数时使用此约定。基于所使用的编译器和函数是否使用可变参数,有两个主流版本的thiscall。 对于GCC编译器,thiscall几乎与cdecl等同:调用者清理堆栈,参数从右到左传递。差别在于this指针,thiscall会在最后把this指针推入栈中,即相当于在函数原型中是隐式的左数第一个参数。

在微软Visual C++编译器中,this指针通过ECX寄存器传递,其余同cdecl约定。当函数使用可变参数,此时调用者负责清理堆栈(参考cdecl)。thiscall约定只在微软Visual C++ 2005及其之后的版本被显式指定。其他编译器中,thiscall并不是一个关键字(反汇编器如IDA使用__thiscall)。

WINAPI

WINAPI是平台的缺省调用约定。Windows操作系统上默认是StdCall;Windows CE上默认是Cdecl。

Intel ABI

根据Intel ABI,EAX、EDX及ECX可以自由在过程或函数中使用,不需要保留。

x86-64调用约定

x86-64调用约定得益于更多的寄存器可以用来传参。而且,不兼容的调用约定也更少了,不过还是有2种主流的规则。

微软x86-64调用约定

在Windows x64环境下编译代码时,只有一种调用约定,也就是说32位下的各种约定在64位下统一成一种了。

微软x64调用约定使用RCX, RDX, R8, R9四个寄存器用于存储函数调用时的4个参数(从左到右),使用XMM0, XMM1, XMM2, XMM3来传递浮点变量。其他的参数直接入栈(从右至左)。整型返回值放置在RAX中,浮点返回值在XMM0中。少于64位的参数并没有做零扩展,此时高位充斥着垃圾。

在微软x64调用约定中,调用者的一个职责是在调用函数之前(无论实际的传参使用多大空间),在栈上的函数返回地址之上(靠近栈顶)分配一个32字节的“影子空间”;并且在调用结束后从栈上弹掉此空间。影子空间是用来给RCX, RDX, R8和R9提供保存值的空间,即使是对于少于四个参数的函数也要分配这32个字节。

例如, 一个函数拥有5个整型参数,第一个到第四个放在寄存器中,第五个就被推到影子空间之外的栈顶。当函数被调用,此栈用来组成返回值----影子空间32位+第五个参数。

在x86-64体系下,Visual Studio 2008在XMM6和XMM7中(同样的有XMM8到XMM15)存储浮点数。结果对于用户写的汇编语言例程,必须保存XMM6和XMM7(x86不用保存这两个寄存器),这也就是说,在x86和x86-64之间移植汇编例程时,需要注意在函数调用之前/之后,要保存/恢复XMM6和XMM7。

System V AMD64 ABI

此约定主要在Solaris,GNU/Linux,FreeBSD和其他非微软OS上使用。头六个整型参数放在寄存器RDI, RSI, RDX, RCX, R8和R9上;同时XMM0到XMM7用来放置浮点变元。对于系统调用,R10用来替代RCX。同微软x64约定一样,其他额外的参数推入栈,返回值保存在RAX中。 与微软不同的是,不需要提供影子空间。在函数入口,返回值与栈上第七个整型参数相邻。

thiscall 问题

如果要hook的方式是C++ DLL,那么它遵循的就是微软的thiscall调用约定,特点就是:
this指针通过ECX寄存器传递,其余同cdecl约定

比如:
this->Request(arg1,arg2,arg3)
其中的this指针是不通过栈传递的,如果我们当成普通的stdcall来处理一定会崩溃,因为ECX中存储的不是this 指针.

比如刚刚的那个函数,转换成C语言来声明,大致如下:

int	Request(void*this,int,char *,int,char *,int &,int,int,int);

但是其中的第一个参数不通过栈来传递,而是通过ECX来传递. 这是我们做hook的时候要特别注意的问题.

那么怎么解决这个问题呢,一个思路就是通过fastcall来模拟thiscall.

用fastcall 模拟thiscall

fastcall中第一个参数通过ecx,第二个参数通过edx,其他参数则是从右至左顺序压栈.
下面给出一个完整的例子:

#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#include <string>
#include "mhook-lib/mhook.h"

class A
{
private:
    int m_data;
    char* m_sz[20];

public:
    int setMsg(const char* pstr, int data)
    {
        if (pstr != NULL && *(char*)pstr != '\0')
        {
            memcpy(m_sz, pstr, 20);
        }

        m_data = data;

        return 0;
    }


    void showMsg()
    {
        if (m_sz[0] != '\0')
        {
            printf("%s,%d\n", m_sz,m_data);
        }
    }


};



typedef int (__thiscall A::* TYPE_Ptr)(const char* pstr, int data);

//注意这里的第二个参数notUsed一定要保留,它对应的就是fastcall中的edx.
typedef int (__fastcall * TYPE_setMsgPtr)(void* pthis,  void* notUsed, const char*, int);

TYPE_setMsgPtr pNew;


int __fastcall HookSetMsg(void * pThis ,void * notUsed, const char* pstr, int data)
{

    printf("hook new function\n");
    return pNew(pThis, notUsed, pstr, data);
}




TYPE_setMsgPtr pfnSetMsg = NULL;

//实现hook thiscall 的方法,不需要用naked汇编
int main(int argc, char **argv)
{
    A* theA = new A();
    theA->setMsg("hello A!", 12);
    
    theA->showMsg();

    TYPE_Ptr px = &A::setMsg;
    int x = *(int*)&px;

    //printf("%p,%p\n", px, x);

    pNew = (TYPE_setMsgPtr)x;
    Mhook_SetHook((PVOID*)&pNew, HookSetMsg);

    theA->setMsg("hello B!", 14);
    theA->showMsg();

    theA->setMsg("hello C!", 1);
    theA->showMsg();

    return 0;
}

使用EasyHook来Hook任意进程

EasyHook的使用

以下内容来自EasyHook官方教程

injector

#include <iostream>
#include <string>
#include <cstring>

#include <easyhook.h>

int _tmain(int argc, _TCHAR* argv[])
{
	DWORD processId;
	std::wcout << "Enter the target process Id: ";
	std::cin >> processId;

	DWORD freqOffset = 0;
	std::cout << "Enter a frequency offset in hertz (e.g. 800): ";
	std::cin >> freqOffset;

	WCHAR* dllToInject = L"..\\Debug\\BeepHook.dll";
	wprintf(L"Attempting to inject: %s\n\n", dllToInject);

	// Inject dllToInject into the target process Id, passing 
	// freqOffset as the pass through data.
	NTSTATUS nt = RhInjectLibrary(
		processId,   // The process to inject into
		0,           // ThreadId to wake up upon injection
		EASYHOOK_INJECT_DEFAULT,
		dllToInject, // 32-bit
		NULL,		 // 64-bit not provided
		&freqOffset, // data to send to injected DLL entry point
		sizeof(DWORD)// size of data to send
	);

	if (nt != 0)
	{
		printf("RhInjectLibrary failed with error code = %d\n", nt);
		PWCHAR err = RtlGetLastErrorString();
		std::wcout << err << "\n";
	}
	else 
	{
		std::wcout << L"Library injected successfully.\n";
	}

	std::wcout << "Press Enter to exit";
	std::wstring input;
	std::getline(std::wcin, input);
	std::getline(std::wcin, input);
	return 0;
}

hook.dll

#include <easyhook.h>
#include <string>
#include <iostream>
#include <Windows.h>

DWORD gFreqOffset = 0;
BOOL WINAPI myBeepHook(DWORD dwFreq, DWORD dwDuration)
{
	std::cout << "\n    BeepHook: ****All your beeps belong to us!\n\n";
	return Beep(dwFreq + gFreqOffset, dwDuration);
}

// EasyHook will be looking for this export to support DLL injection. If not found then 
// DLL injection will fail.
extern "C" void __declspec(dllexport) __stdcall NativeInjectionEntryPoint(REMOTE_ENTRY_INFO* inRemoteInfo);

void __stdcall NativeInjectionEntryPoint(REMOTE_ENTRY_INFO* inRemoteInfo)
{
	std::cout << "\n\nNativeInjectionEntryPointt(REMOTE_ENTRY_INFO* inRemoteInfo)\n\n" <<
		"IIIII           jjj               tt                dd !!! \n"
		" III  nn nnn          eee    cccc tt      eee       dd !!! \n"
		" III  nnn  nn   jjj ee   e cc     tttt  ee   e  dddddd !!! \n"
		" III  nn   nn   jjj eeeee  cc     tt    eeeee  dd   dd     \n"
		"IIIII nn   nn   jjj  eeeee  ccccc  tttt  eeeee  dddddd !!! \n"
		"              jjjj                                         \n\n";

	std::cout << "Injected by process Id: " << inRemoteInfo->HostPID << "\n";
	std::cout << "Passed in data size: " << inRemoteInfo->UserDataSize << "\n";
	if (inRemoteInfo->UserDataSize == sizeof(DWORD))
	{
		gFreqOffset = *reinterpret_cast<DWORD *>(inRemoteInfo->UserData);
		std::cout << "Adjusting Beep frequency by: " << gFreqOffset << "\n";
	}

	// Perform hooking
	HOOK_TRACE_INFO hHook = { NULL }; // keep track of our hook

	std::cout << "\n";
	std::cout << "Win32 Beep found at address: " << GetProcAddress(GetModuleHandle(TEXT("kernel32")), "Beep") << "\n";

	// Install the hook
	NTSTATUS result = LhInstallHook(
		GetProcAddress(GetModuleHandle(TEXT("kernel32")), "Beep"),
		myBeepHook,
		NULL,
		&hHook);
	if (FAILED(result))
	{
		std::wstring s(RtlGetLastErrorString());
		std::wcout << "Failed to install hook: ";
		std::wcout << s;
	}
	else 
	{
		std::cout << "Hook 'myBeepHook installed successfully.";
	}

	// If the threadId in the ACL is set to 0,
	// then internally EasyHook uses GetCurrentThreadId()
	ULONG ACLEntries[1] = { 0 };

	// Disable the hook for the provided threadIds, enable for all others
	LhSetExclusiveACL(ACLEntries, 1, &hHook);

	return;
}

最关键的是hook的时候要遵循fastcall模拟thiscall的约定.

fastcall 模拟 thiscall


typedef int(__fastcall * TYPE_RequestPtr)(void* pthis,void*edx, int id,char * inBuf,int inLen,char * outBuf,int & outLen,int a1,int a2,int a3);

TYPE_RequestPtr pOldRequest;

int   __fastcall HookRequest(void* pthis,void*edx, int id, char * inBuf, int inLen, char * outBuf, int & outLen, int a1, int a2, int a3)
{
	int ret;
	std::cout << "HookRequest:--------------------------" << pthis << ",id=" << id<<",a1="<<a1<<",a2="<<a2<<",a3="<<a3 << std::endl;
	std::cout << "inLen=" << inLen << ",outLen=" << outLen << std::endl;
	std::cout<<"inbuf=" << (void*)inBuf << ",outbuf=" << (void*)outBuf << std::endl;
	if (inBuf != NULL) {
		std::cout << Hexdump(inBuf, inLen) << std::endl;
	}
	else {
		std::cout << "inBuf is null" << std::endl;	
	}
	ret= pOldRequest(pthis,edx, id, inBuf, inLen, outBuf, outLen, a1, a2, a3);
	std::cout << "outLen=" << outLen <<",ret="<<ret<< std::endl;
	//TestA ta;
	//ta.Request(id, inBuf, inLen, outBuf, outLen, a1, a2, a3);
	//testf(pthis, id, inBuf, inLen, outBuf, outLen, a1, a2, a3);
	if (outBuf != NULL) {
		std::cout << "outBuf: outL" << std::endl;
		std::cout << Hexdump(outBuf, outLen) << std::endl;
	}
	else {
		std::cout << "outBuf is null" << std::endl;
	}
	return ret;
}


pOldRequest =(TYPE_RequestPtr) GetProcAddress(GetModuleHandle(TEXT("FLRPCLib")), "?Request@CFLRPCApi@@QAEHHPADH0AAHHHH@Z");
// Install the hook request
	 result = LhInstallHook(
		pOldRequest,
		HookRequest,
		NULL,
		&hHook2);
本站总访问量 本站访客数人次