有了上面的知识,让我们回顾一下先前讨论的问题,首先,用户调用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)
|