有了上面的知识,让我们回顾一下先前讨论的问题,首先,用户调用API的recv函数,程序运行到recv的入口地址处,此时堆栈中拥有用户调用recv的参数和用户代码中CALL [recv]的下一条指令的地址。堆栈如下图:
堆栈指针
[ESP]
堆栈的内容
堆栈内容的含义
0x00000100
0
参数
0x000000fc
len
参数
0x000000f8
buf
参数
0x000000f4
S
参数
0x000000f0
RetUserAddress
用户调用recv的下一条指令的地址
然后程序指针EIP被修改为recv入口处的地址,而入口地址处有一条简单的CALL指令,它使程序将recv的第6个字节的地址压入栈中(因为CALL XXXX占用5个字节,第六个字节被认为为返回地址),然后跳转到我们的无参数无返回值的通用替换函数中去了,好了看看现在堆栈中都有些什么?如图:
堆栈指针
[ESP]
堆栈的内容
堆栈内容的含义
0x00000100
0
参数
0x000000fc
len
参数
0x000000f8
buf
参数
0x000000f4
s
参数
0x000000f0
RetUserAddress
用户调用recv的下一条指令的地址
0x00000ec
RetrecvAddress
recv的第六个字节的地址
首先是参数,其次是用户调用recv后的返回值,然后是recv调用我们的替换函数中的返回值,紧接着就像刚才提到的那样,程序将EBP当前内容压入栈中。如图
堆栈指针[ESP]
堆栈的内容
堆栈内容的含义
0x00000100
0
参数
0x000000fc
len
参数
0x000000f8
buf
参数
0x000000f4
S
参数
0x000000f0
RetUserAddress
用户调用recv的下一条指令的地址
0x00000ec
RetrecvAddress
recv的第六个字节的地址
0x000000e8
OldEBP
保存的旧的EBP的内容,然后[EBP]= 0x000000e8
0x000000e4
OldEBX
保存的旧的EBX的内容
0x000000e0
OldESI
保存的旧的ESI的内容
0x000000dc
OldEDI
保存的旧的EDI的内容,此时[ESP]=0x000000dc
此时我们可以看到,[EBP]为保存的ebp的值,现在对我们没有用处,函数返回前用于恢复EBP的值,[EBP+4]是recv函数的CALL XXXX后面指令的地址(也就是第六个字节的地址),我们可以通过将此值减去5来得到recv的入口地址,这样在我们所有hook的api函数的列表中进行检索,就可以匹配出用户调用的是哪一个API函数,从而为后面恢复和再次改变该API的入口5字节做准备,因为调用任何我们需要HOOK的API程序都会进入到这个无返回值无参数的函数,所以通过这种方法找到当前HOOK的是哪一个API,从而可以区分不同的API进行特殊的处理。[EBP+8]保存的是用户调用recv后的返回地址,由于我们执行完替换函数后,应该返回到这个地址,而不应该返回到recv的第6个字节处执行,所以我们还是需要保存下这个值,以便在我们用ret返回前把它压栈从而使程序返回到用户调用recv的下一条指令处继续运行。
我们先定义如下函数:
void CommonFunc(void);
我们现在实现它,请注意参考上面堆栈表格。
void CommonFunc(void)
{
DWORD pdwCall; // recv入口地址
DWORD dwRtAddr; // 我们的函数真正要返回的地址
DWORD* pdwParam; // 第一个参数的地址
DWORD dwParamCount; // 参数个数
DWORD dwParamSize; // 所有参数所占用的大小应该=4* dwParamCount
DWORD dwRt; // 返回值
_asm
{
lea EAX,[EBP+4] // recv入口处第6个字节的地址
mov [pdwCall],EAX
mov EAX,[EBP+8] // 用户调用recv(即call XXXX)后面一条指令的地址
mov [dwRtAddr],EAX
lea EAX, [EBP+12] // 第一个参数的地址!
mov [pdwParam],EAX
}
(*pdwCall) -= 5; // 获得recv入口地址
HOOKINFO *hi = findHookInfo(pdwCall); // 通过原始API的入口地址获得此API的相关信息
memcpy(pdwCall,hi->OrgApi5bytes,5); // 恢复被调用API的前5个字节,使下面的代码可以正常调用
// 下面准备进入用户针对此API的替换函数,现准备参数
dwParamCount = hi->ParamCount; // 得到本API的参数个数
dwParamSize = 4*dwParamCount; // 计算参数所占用大小
DWORD pdwESP;
_asm
{
sub esp,[dwParamSize] // 将栈增加,可以容纳参数
mov [pdwESP],esp // 保存当前栈的地址
}
memcpy(pdwESP,pdwParam,dwParamSize);//将用户传递的参数拷贝到栈中
hi->myAPIFunc(); // 调用用户针对此API的替换函数
_asm
{
mov [dwRt],eax // 保存返回值
}
// 如果是CreateProcess,那么继续hook它
pPi = (PROCESS_INFORMATION*)pdwParam[9];
if(strcmpi(pai->szOrgApiName,"CreateProcessA") != 0 || strcmpi(pai->szOrgApiName,"CreateProcessW") != 0)
{
InjectDll(pPi->dwProcessId,m_szDllPathName);
}
// 下面再次修改原始API的前5个字节
memcpy(pdwCall,JMPCODE,5); // #define JMPCODE 0xE8
DWORD* pdwapi = pdwCall[1];
pdwapi[0] = (DWORD) CommonFunc – (DWORD)pdCall – 5; // CommonFunc函数地址的偏移
// 下面准备返回的操作
_asm
{
add esp,[dwParamSize] // 清理我们为了调用真正的替换函数而分配的堆栈里的参数
// 下面弹出所有保存的寄存器值(按照入栈的逆顺序)
pop EDI // 恢复EDI
pop ESI // 恢复ESI
pop EBX // 恢复EBX
// 我们没有改动过EBP的值,所以EBP指向堆栈中OldEBP的位置
mov ESP,EBP
pop EBP // 恢复EBP
// 由于堆栈中还剩下参数和两个返回地址(我们真正要返回的地址和原始API中的第6个字节的地址),所以我们把这些数据也清除出堆栈
add ESP,8 // 清除两个返回地址
mov ECX,[dwParamSize] // 获得参数的大小
add ESP,ECX // 清除参数
mov EAX,[dwRt] // 设置返回值
// 由于调用ret返回时,程序先从堆栈中取出返回地址,所以我们把要真正返回的地址压入堆栈中
mov EDX,[dwRtAddr] // 设置返回地址
push EDX
ret // 返回
}
}
最后要注意得一点是,如果要执行得API函数是CreateProcess,那么应该把它新开启得进程也HOOK掉。以上我们了解了通用替换函数的原理,那么让我们深入的讨论CHookApi类,并且实现它。
--------------------------------------联系我(liutao_free@sohu.com)
--------------------------------------联系我(liutao_free@sohu.com)
联系我(liutao_free@sohu.com)