如何挂接API
想要挂接API,有两种方法:1、修改代码方法 2、修改模块的输入节。
对于第2种方法的详细讲解,请看《Windows核心编程》,Jeffery Ritcher讲得是最好的。不过比较复杂,需要了解 ImageHlp库函数,
MSDN讲解得很少。
这里主要探讨第一种API挂接方式。
在X86 CPU下,
原理即把API函数的头5个字节修改为一条JMP指令,或者把函数体的第一个字节修改为一条INT指令(这种方法要修改中断向量表,比较复杂)。修改为JMP指令以后,调用API的线程将会执行我们的JMP指令,跳至我们的代码。而我们将原API函数的头5个字节保存到自己的变量中,以便于恢复。通常,把该挂接方式写成一个C++的类,然后用一个类对象对应于一个API的挂接。
下面给出这个类,通过阅读源代码和注释,你可以很清楚的看到这种挂接的原理:
///
FileName ApiHook.h
Description: Hook a Api Function
//
class CApiHook
{
private:
BYTE m_byteOldCode[5];//原API函数的头5个字节
BYTE m_byteNewCode[5];//jmp指令
DWORD m_dwOldFunAddr; //原API函数地址(理解为函数体第一个字节的地址)
public:
BOOL m_bHookIsOK;//该API是否已被挂接
DWORD m_dwOldProtect;//原API函数所在页面的保护属性
public:
CApiHook()
{
}
virtual ~CApiHook()
{
if(m_bHookIsOK) HookSwitch(FALSE);
}
public:
//挂接API
BOOL SetApiHook(LPCTSTR strDllName,LPCTSTR strFunName,DWORD lpFunAddr);
BOOL HookSwitch(BOOL fHook);
};
// strDllName为DLL文件名 strFunName为API函数名,lpFunAddr是自己函数的地址
// (该strFunName指定的函数应当位于strDllName指定的DLL文件中)
BOOL CApiHook::SetApiHook(LPCTSTR strDllName,LPCTSTR strFunName,DWORD lpFunAddr)
{
HMODULE hModule = NULL;
DWORD dwJmpAddr = 0;
//判断是否已经挂接
if(m_bHookIsOK==TRUE) return FALSE;
//获得该DLL模块的句柄
hModule = GetModuleHandle(strDllName);
if (hModule==NULL) return FALSE;
//获取API函数的地址
m_dwOldFunAddr=(DWORD)GetProcAddress(hModule,strFunName);
if (m_dwOldFunAddr==NULL) return FALSE;
//保存旧指令
CopyMemory(m_byteOldCode,(LPCVOID)m_dwOldFunAddr,sizeof(m_byteOldCode));
//将自己的函数体的第一个字节设置为 0xE9
m_byteNewCode[0]=0xE9;
// 拷贝到函数体的第2-10个字节中(一个地址4字节)
// 计算jmp指令的偏移地址: offset = userfun-sysfun- 5
dwJmpAddr = lpFunAddr - (m_dwOldFunAddr + sizeof(m_byteOldCode));
CopyMemory(&m_byteNewCode[1],&dwJmpAddr,sizeof(dwJmpAddr));
HookSwitch(TRUE);
return TRUE;
}
//参数为TURE则修改原DLL函数,为FALSE则将函数改回来
BOOL CApiHook::HookSwitch(BOOL fHook)
{
BOOL bOk = FALSE;
DWORD dwProtect;
//已经挂钩
if (m_bHookIsOK && fHook)
return FALSE;
//开始挂接API
if (fHook==TRUE)
{
VirtualProtect((LPVOID)m_dwOldFunAddr,sizeof(m_byteOldCode),
PAGE_EXECUTE_READWRITE,&m_dwOldProtect);
CopyMemory((void *)m_dwOldFunAddr,m_byteNewCode,sizeof(m_byteNewCode));
m_bHookIsOK = TRUE;
}
else
{
CopyMemory((void *)m_dwOldFunAddr,m_byteOldCode,sizeof(m_byteOldCode));
VirtualProtect((LPVOID)m_dwOldFunAddr,sizeof(m_byteOldCode),m_dwOldProtect,&dwProtect);
m_bHookIsOK = FALSE;
}
return TRUE;
}
// end of file
///
有了这个类,以后挂接API就很方便了。把上面内容放入一个头文件中,例如ApiHook.h,
然后需要使用的时候就写下面两行代码,就完成了挂接:
CApiHook g_ApiHook1;
g_ApiHook1.SetApiHook("user32.dll", "MessageBoxW", hook_messagebox);
停止挂接就调用
g_ApiHook1.HookSwitch(FALSE);
不过,只讲到这里可能无法满足你的需要。我们挂接API,往往要在我们自己的挂接函数中调用原API函数,以保证目标程序的正常运行。
因此,我们可以这样写挂接函数:
int hook_messagebox(HWND hWnd, LPCWSTR lpText,LPCWSTR lpCaption, UINT uType)
{
int nRet=0;
//.code
g_ApiHook1.HookSwitch(FALSE);
nRet = MessageBoxW(hWnd, lpText, lpCaption, uType);
g_ApiHook1.HookSwitch(TRUE);
return nRet;
}
你可能以为这样就OK了,但是,当你调试一下你的程序就会发现,一旦目标程序调用了你的挂接函数,
很快就会弹出一个错误提示,说ESP寄存器不正确,然后进程立即终止。
我在第一次学习该方式挂接API时,被这个问题困扰过。从C代码来看几乎找不到错误。为什么呢?
原因在于我们修改了API函数的代码,破坏了函数的正常运行,使得堆栈不能正确平衡。虽然我们调用了HookSwitch(TRUE)把那5个字节替换了回去,但是此时的堆栈已经与直接调用该API的堆栈不同了,为了把这个问题弄明白我们需要首先复习一下汇编语言知识:
SP, BP, SI, DI 这4个寄存器(80386以后扩展成了 ESP, EBP, ESI, EDI)
既可以用作通用寄存器,又可以存储特定段的偏移地址。
SP 堆栈指针 存放堆栈的偏移地址(SS存放堆栈的段地址)
BP 基址指针 在间接寻址中,用于存放段内偏移的一部分或全部(此时段地址存放于 SS)
SI 源变址寄存器 在间接寻址中,用于存放段内偏移的一部分或全部
在字符串操作中,存放源操作数的段内偏移地址
可存放一般数据
DI 目标变址寄存器 在间接寻址中,用于存放段内偏移的一部分或全部
在字符串操作中,存放目的操作数的段内偏移地址
也可存放一般数据
压栈/出栈操作:
汇编中,栈是先使用高地址,后使用低地址。压栈导致SP减少一个机器字,出栈增加一个机器字。
push 指令是压栈 ESP=ESP-4
pop 指令是出栈 ESP=ESP+4
调用子程序指令
call tag1 这条指令也会执行压栈操作,把该call指令的下一条指令的地址压入栈中。
WINAPI调用方式的特点是,参数从右往左依次进栈,被调用的函数负责清栈。
在C代码被编译为汇编指令后,调用函数的代码实际上是变为以下形式:
C代码: MessageBoxW(1,2,3,4);
汇编: push 4;
push 3;
push 2;
push 1;
call taget;
一个函数在执行完成以后,通过汇编指令: retn 16; (4个参数,每个参数4字节)
清栈并且返回(就是我们在C中看到的 return(x))。
当原API调用时,编译产生的汇编指令已经把API的参数按从右到左的顺序压入了栈中(这里以MessageBoxW为例),此时ESP寄存器减少16,即ESP = ESP - 16。 然后,就是调用子程序的指令 call xxx。 call 指令会把目标函数地址也压入堆栈,此时ESP = ESP - 4。
因此ESP一共移动了20个字节。然后,就调用了我们的JMP指令跳转到我们的函数中,此时,我们的函数其实就直接使用这些已经压入栈的参数了,当我们把5个字节改回去再用C/C++代码调用MessageBoxW时,又会产生那些压栈的代码。但是,我们函数中的参数hWnd, lpCaption, lpText, uType其实是利用 [ESP+偏移](ESP的内容实现拷入EBP,EBP又一次进栈) 来访问的,后面又压入了函数地址和EBP,如果我们仍按照 nRet = MessageBoxW(hWnd, lpText, lpCaption, uType) 写代码会把 EBP + 偏移 压入堆栈,导致真正的API访问堆栈时读取错误的参数。
因此,我们需要修改我们的挂接函数如下:
int WINAPI hook_messagebox(hWnd, lpText, lpCaption, uType)
{
int nRet=0;
//.code
g_ApiHook1.HookSwitch(FALSE);
nRet = MessageBoxW(hWnd, lpText, lpCaption, uType);
g_ApiHook1.HookSwitch(TRUE);
return nRet;
}
其实就是使我们的挂接函数与原API函数的调用约定一致。
汗,研究了半天,原来是因为少了个WINAPI修饰符。。。。。。
我的感想就是,不要自己修改ESP寄存器,很难把值弄正确。
E-mail: smfwuxiao@qq.com