通过修改代码挂接API

如何挂接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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值