思路
对于目标运行中的EXE程序,如notepad.exe,使用inline hook劫持其kernel32.dll中的writefile函数:
- 找到notepad.exe的进程PID,通过进程PID获取进程模块,通过进程模块获取程序加载的imagebase。
- 在获得imagebase后,就可以像类似分析PE文件一样,先找PE头,然后到可选头,里面读取data directory的第2个成员,是IDT表的RVA和大小。
- 在IDT中找到对应的kernel32.dll,去kernel32.dll项的的INT中找到函数writefile,记录下标;拿下标在kernel32.dll项的IAT表中找到writefile在内存中的地址,记录下该地址。
- 把数据进行注入,之后对shellcode进行微调整,主要是最后一段调整。最后一段里面要包含writefile里面的前几个字节的内容,最后是跳转指令E9,跳转的偏移也要进行调整。
- 对wirtefile的地址处前几个字节进行改写,头5字节是一个跳转指令,跳转到shellcode。
系统结构
包括inlinehook.exe、和notepad.exe(可以是其它的文件)。inlinehook.exe在notepad.exe执行时,注入shellcode,并进行inlinehook,hook掉目标函数如writefile。在操作notepad.exe时,如果按ctrl+s或者点击“文件->保存”,就会在notepad.exe中调用writefile函数,就会触发shellcode。系统结构如下图3-1所示。
shellcode
C语言版
#include <windows.h>
#include <stdio.h>
typedef void(*DLLWITHLIB)(void);
typedef HINSTANCE(*LOADP)(char*);
typedef LPTSTR(*GETP)(HINSTANCE,char*);
typedef void(*MSGBOX)(int,char*,char*,int);
int main()
{
//在inlinehook中执行,获得当时LoadLibraryA和GetProcAddress在内存中地址
HINSTANCE LibHandle = LoadLibrary("KERNEL32.dll");
//获取DLL中导出函数地址
LPTSTR getaddr1 = (LPTSTR)GetProcAddress(LibHandle, "LoadLibraryA");
//获取DLL中导出函数地址
LPTSTR getaddr2 = (LPTSTR)GetProcAddress(LibHandle, "GetProcAddress");
//加载user32.dll,里面有MessageBox
HINSTANCE LibHandle1=(*(LOADP)getaddr1)("USER32.dll");
//获取DLL中导出函数MessageBoxA的地址
LPTSTR getaddr3 = (*(GETP)getaddr2)(LibHandle1, "MessageBoxA");
//调用MessageBox弹框
(*(MSGBOX)getaddr3)(0,"组员","组号",0);
return 0;
}
在inlinehook.exe中,执行LoadLibraryA()和GetProcAddress(),获取当时kernel32.dll里这两个函数的地址,然后就可以在shellcode中使用call来进行使用。使用它们,在目标函数中加载user32.dll,进而调用MessageBoxA函数,实现弹框。
汇编版
shellcode包括一开始进行压栈,保存通用寄存器和EFLAGS;最后也要对它们的值进行恢复。调用LoadLibraryA加载user32.dll,调用GetProcAddress获取MessageBoxa的地址。然后调用MessageBoxA弹窗,最后将损伤了的目标函数的前几个字节,损伤了几个,就填入几个,剩下的空位用NOP填。接着E9进行跳转,每次到跳转到目标函数处的起始第5字节,但是起始第5字节后面可能会写入几个NOP。如损伤了7个字节,这7个字节的内容都写在shellcode里面了,执行过了,所以目标函数处的第6、7字节填为NOP,否则会执行失败。
里面有几个地方是需要每次执行都要填写的,主要是B8、BB、68后的地址,有的是函数地址,有的是数据中的字符串的地址。以及指令填在NOP,E9跳转每次都不一样,也需要填写。
跳转关系图
以inlinehook程序notepad.exe的WriteFile函数为例。如图3-3所示,当运行了inlinehook.exe后,当在notepad.exe中要调用WriteFile时,程序仍然会先运行到0x7C810E17处,但是在该地址处的指令不再是PUSH 18,而变成了JMP shellcode,程序跳转到shellcode处执行。shellcode中的内容在JMP前有7字节,之所以不是5字节,是因为7字节才是完整的两条指令,如果只在shellcode中留下5个字节,之后会发生执行错误。
在shellcode中最后是跳转到0x7C810E1B处,也就是原先的函数的第6字节,而这里填写了2个NOP指令,因为原本在这里的指令,被放到shellcode中执行了。
函数调用关系
函数和对应需要“损伤”字节
源代码
#include <sys/stat.h>
#include <windows.h>
#include <Tlhelp32.h>
#include <stdio.h>
#include <string.h>
#include <stdint.h>
//节表
struct section_header{
uint8_t name[8];//名字
uint32_t virtualsize;
uint32_t RVA;//内存中地址
uint8_t size_of_raw_data[4];//节大小
uint32_t file_address;//文件中地址,RVA-file_addr得到本节中delta
uint8_t notimportant[12];
uint8_t character[4];
};
//IDT项
struct IDT_entry{
uint32_t INT_pointer;//指向INT表
uint32_t timestamp;
uint32_t forwarderchain;
uint32_t dll_name_pointer;//指向DLL名字
uint32_t IAT_pointer;//指向IAT表
};
//hint-name table
struct hint_name{
uint16_t hint;//函数序号
char name[50];//函数名
};
//地址和PID
struct PID_and_VA{
uint32_t pid;
uint32_t VA;
};
//获取文件字节数
int file_size(char* filename)
{
struct stat statbuf;
stat(filename,&statbuf);
int size=statbuf.st_size;
return size;
}
uint32_t uint8to32(uint8_t fouruint8[4]) {
return *(uint32_t*)fouruint8;
}
uint16_t uint8to16(uint8_t fouruint8[2]) {
return *(uint16_t*)fouruint8;
}
//打印十六进制字符,小端序
void printf_hex_buff(char * buff,int num){
int i=0;
for(i=0;i<num;i++)
printf("%X ",(uint8_t)buff[i]);
printf("\n");
}
void printf_hex_buff(uint8_t * buff,int num){
int i=0;
for(i=0;i<num;i++)
printf("%X ",buff[i]);
printf("\n");
}
//提升权限
BOOL EnableDebugPrivilege()
{
HANDLE hToken;
LUID Luid;
TOKEN_PRIVILEGES tp;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))return FALSE;
if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &Luid))
{
CloseHandle(hToken);
return FALSE;
}
tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = Luid;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if (!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL))
{
CloseHandle(hToken);
return FALSE;
}
CloseHandle(hToken);
return TRUE;
}
int find_ID(char * PEname)
{
char szStrTemp[50];
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32);
int PID=0;
HANDLE hProcessShot,oneProcess;
hProcessShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); //获取进程列表,最后一个参数是0
if (hProcessShot == INVALID_HANDLE_VALUE)
{
return 0;
}
// 创建系统当前进程快照
if (Process32First(hProcessShot,&pe32))//获取下一个进程快照
{
for (int i = 0;Process32Next(hProcessShot, &pe32);i++)
{
if(!stricmp(PEname,pe32.szExeFile))
{
printf("PID: %d %s\n",pe32.th32ProcessID,pe32.szExeFile);
PID= pe32.th32ProcessID;
}
}
}
//遍历进程快照
CloseHandle(hProcessShot);
return PID;
}
PVOID GetProcessImageBase(DWORD dwProcessId)
{
PVOID pProcessImageBase = NULL;
MODULEENTRY32 me32 = { 0 };
me32.dwSize = sizeof(MODULEENTRY32);
// 获取指定进程全部模块的快照
HANDLE hModuleSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcessId);
if (INVALID_HANDLE_VALUE == hModuleSnap)
{
return pProcessImageBase;
}
// 获取快照中第一条信息
BOOL bRet = Module32First(hModuleSnap, &me32);
if (bRet)
{
// 获取加载基址
pProcessImageBase = (PVOID)me32.modBaseAddr;
}
// 关闭句柄
CloseHandle(hModuleSnap);
return pProcessImageBase;
}
int find_function_VA(char * PEname,char *hook_dll_name,char*hook_function_name,struct PID_and_VA* the_PID_and_VA)
{
//char *hook_dll_name="KERNEL32.DLL";//要hook的函数在哪个DLL里面
//char *hook_function_name="WriteFile";//要hook的函数名
//char * PEname="notepad.exe";
PVOID pProcessImageBase;
int pid=find_ID(PEname);//PID
uint32_t imagebase=(uint32_t)GetProcessImageBase(pid);//image base
HANDLE hProcess;//handle for process
uint8_t tempbuff[1024];//临时存放的
memset(tempbuff,0,1024);
DWORD bufnum;//每次读写的实际大小
uint32_t IDT_directory[2];//[0]表示RVA,[1]表示size
uint32_t pindex=0;//文件偏移
uint32_t RVA=0;//RVA
uint32_t EXEheader_offset=0;
int IDT_entry_num=0;
char dll_name[50];//读取dll名字
char function_name[50];//读取函数名字
uint32_t dll_name_addr=0;
uint32_t function_name_addr=0;
struct IDT_entry* the_IDT_entry=NULL;
int dll_index=0;//是第几个index是要找的DLL?
uint32_t INT_addr=0;
struct hint_name the_hint_name={0};//hint_name table
uint32_t hint_name_addr;//hint_name_addr
uint32_t IAT_seri_num=0;//目标函数在IAT中的下标号
uint32_t hook_function_VA=0;//hook函数内存地址
uint32_t hook_function_IAT=0;
if(pid!=0)
printf("\n\nfind %s! It's PID is %d\n",PEname,pid);
else{
printf("no %s running or find failed!",PEname);
}
the_PID_and_VA->pid=pid;
printf("image base is 0x%X\n",imagebase);
//"打开"进程
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if(hProcess == NULL)
{
fprintf(stderr,"\nOpen process error : %d",GetLastError());
goto bad;
}
//读取第一部分PE头
if(ReadProcessMemory(hProcess, imagebase, tempbuff, 1024, &bufnum)){
if(bufnum != 1024)
{
printf("\nRead PE failed!\n");
goto bad;
}
}
//对于tempbuff是文件偏移!注意RVA和index偏移
EXEheader_offset=*(uint32_t*)&tempbuff[0x3C];
printf("EXE header offset at %X\n",EXEheader_offset);
//加上0x18到达可选头,这里有文件大小,全部复制, EXEheader_offse+0x50是文件大小
//imagesize=*(uint32_t*)&tempbuff[EXEheader_offset+0x50];
//printf("imagesize is %X\n",imagesize);
//读取IAT目录成员
if(ReadProcessMemory(hProcess, imagebase+EXEheader_offset+0x80, IDT_directory, 8, &bufnum)){
if(bufnum != 8)
{
printf("\nRead IAT directory failed!\n");
goto bad;
}
}
//printf("RVA address : IDT at %X, it size is %X\n",IDT_directory[0],IDT_directory[1]);
//uint8_t *imagebuffer =(uint8_t*)malloc(sizeof(imagesize));
//free(imagebuffer);
IDT_entry_num=IDT_directory[1]/20;//计算需要多少个IDT项
//读取IDT表项
the_IDT_entry = (struct IDT_entry*)malloc(IDT_directory[1]);
if(ReadProcessMemory(hProcess, imagebase+IDT_directory[0], the_IDT_entry, IDT_directory[1], &bufnum)){
if(bufnum != IDT_directory[1])
{
printf("\nRead IDT directory failed!\n");
goto bad;
}
}
//列出DLL名字
for(int i=0;i<IDT_entry_num;i++){
dll_name_addr=the_IDT_entry[i].dll_name_pointer;
//printf("dll_name_addr is %X\n",dll_name_addr);
memset(dll_name,0,sizeof(dll_name));
if(dll_name_addr!=0)//如果的确有这一项,为0表示为空
{
if(ReadProcessMemory(hProcess, imagebase+dll_name_addr, dll_name, sizeof(dll_name), &bufnum)){
if(bufnum != sizeof(dll_name))
{
printf("\nRead dll_name failed!\n");
goto bad;
}
//printf("%s\n",dll_name);
//如果找到了要hook的函数所在的DLL,结束
if(!stricmp(dll_name,hook_dll_name))
{
//printf("found target dll!\n");
dll_index=i;
break;
}
}
}
}
//在INT中找函数名,INT内存基址是这里
INT_addr=imagebase+the_IDT_entry[dll_index].INT_pointer;
//只要没有读到末尾,都得读
for(int i=0;;i++)
{
if(ReadProcessMemory(hProcess, INT_addr+4*i, &hint_name_addr, 4, &bufnum))
{
if(bufnum != 4)
{
printf("\nRead hint_name_addr failed!\n");
goto bad;
}
//printf("hint_name_addr is %X\n",hint_name_addr);
if(hint_name_addr==0)//读完了
{
printf("can not found %s in %s!\n",hook_dll_name,hook_function_name);
break;
}
memset(&the_hint_name,0,sizeof(the_hint_name));
if(ReadProcessMemory(hProcess, imagebase+hint_name_addr, &the_hint_name, sizeof(the_hint_name), &bufnum))
{
if(bufnum != sizeof(the_hint_name))
{
printf("\nRead hint_name failed!\n");
goto bad;
}
//如果找到了
if(!stricmp(hook_function_name,the_hint_name.name))
{
//printf("%X %s\n",the_hint_name.hint,the_hint_name.name);
IAT_seri_num=i;
break;
}
}
}
}
//在IAT中获得目标函数在内存中的地址
if(ReadProcessMemory(hProcess, imagebase+the_IDT_entry[dll_index].IAT_pointer+4*IAT_seri_num, &hook_function_VA, 4, &bufnum))
{
if(bufnum != 4)
{
printf("\nRead hook_function_addr failed!\n");
goto bad;
}
}
printf("target function address is at %X\n",hook_function_VA);
the_PID_and_VA->VA=hook_function_VA;
good:
free(the_IDT_entry);
CloseHandle(hProcess);
//system("pause");
return 0;
bad:
free(the_IDT_entry);
CloseHandle(hProcess);
//system("pause");
return -1;
}
int main(){
char bigbuff[4096]={0};//用来临时读取的
int pid=0;
char exepath[100]={0};
HANDLE hProcess;
DWORD dwHasWrite;
LPVOID lpRemoteBuf;
//uint8_t buff[7] = {0};
uint8_t find_inst[20]={0};//观察目标位置有什么
char addr[4]={0};
struct PID_and_VA the_PID_and_VA={0};
uint32_t tempaddr=0;
uint32_t function_addr=0;//目标函数在内存中位置
char string_data[]=
"\x55\x53\x45\x52\x33\x32\x2E\x64"
"\x6C\x6C\x00\x4D\x65\x73\x73\x61"
"\x67\x65\x42\x6F\x78\x41\x00\xB5"
"\xDA\x31\x34\xD7\xE9\x00\x32\x30"
"\x31\x36";
char shellcode1[]=
"\x9C\x60\x90\x90\xB8\x7B\x1D\x80"
"\x7C\xBB\x00\x94\x00\x01\x53\x90"
"\xFF\xD0\x90\x90\x90\xBB\x10\x94"
"\x00\x01\x53\x50\x90\xB8\x30\xAE"
"\x80\x7C\xFF\xD0\x90\x90\x90\x90"
"\x90\x90\x6A\x00\x90\x90\x90\x68"
"\x30\x94\x00\x01\x90\x90\x90\x68"
"\x20\x94\x00\x01\x90\x90\x90\x90"
"\x90\x6A\x00\xFF\xD0\x61\x9D\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\xE9\x12\x34\x56\x78";//大小为84
char exename[30]={0};
char dllname[30]={0};
char function_name[30]={0};
int buffsize=0;
printf("\nplease enter exe name: ");
scanf("%s",exename);
printf("\nplease enter dll name: ");
scanf("%s",dllname);
printf("\nplease enter function name: ");
scanf("%s",function_name);
printf("\nplease enter buffsize[destory buffsize bytes,5/7bytes maybe]: ");
scanf("%d",&buffsize);
uint8_t *buff=(uint8_t *)malloc(buffsize);
HINSTANCE LibHandle = LoadLibrary("KERNEL32.dll");
printf("LERNEL32 Address = 0x%x \n",LibHandle);
LPTSTR getaddr1 = (LPTSTR)GetProcAddress(LibHandle, "LoadLibraryA"); //获取加载函数的地址在getaddr1
printf("LoadLibraryA Address = 0x%x \n",getaddr1);
LPTSTR getaddr2 = (LPTSTR)GetProcAddress(LibHandle, "GetProcAddress"); //GetProcAddress的在getaddr2
printf("GetProcAddress Address = 0x%x \n",getaddr2);
if (!EnableDebugPrivilege())
{
fprintf(stderr,"Add Privilege error\n");
goto bad;
}
else{
printf("Add Privilege success!\n");
}
//要hook的函数在IAT中的地址
//if(!find_function_VA("notepad.exe","kernel32.dll","writefile",&the_PID_and_VA))
if(!find_function_VA(exename,dllname,function_name,&the_PID_and_VA))
function_addr=the_PID_and_VA.VA;
else{
printf("no such exe!");
}
//"打开"进程
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, the_PID_and_VA.pid);
if(hProcess == NULL)
{
fprintf(stderr,"\nOpen process error : %d",GetLastError());
goto bad;
}
dwHasWrite=0;
//读取要hook的函数的前5个字节
if(ReadProcessMemory(hProcess, function_addr, find_inst, sizeof(find_inst), &dwHasWrite))
{
if(dwHasWrite != sizeof(find_inst))
{
printf("\nOpen hook target function failed,length failed\n");
goto bad;
}
printf("first 20 bytes of the target function is : ");
printf_hex_buff(find_inst, sizeof(find_inst));
}else
{
printf("\nOpen hook target function failed :%d",GetLastError());
goto bad;
}
if(ReadProcessMemory(hProcess, function_addr, buff, buffsize, &dwHasWrite))
{
if(dwHasWrite != buffsize)
{
printf("\nOpen hook target function failed,length failed\n");
goto bad;
}
printf("first %d bytes of the target function is : ",buffsize);
printf_hex_buff(buff,sizeof(buff));
}else
{
printf("\nOpen hook target function failed :%d",GetLastError());
goto bad;
}
dwHasWrite=0;
//写入字符串数据,起始地址是lpRemoteBuf
lpRemoteBuf = VirtualAllocEx(hProcess, NULL, sizeof(string_data), MEM_COMMIT, PAGE_READWRITE);
if(WriteProcessMemory(hProcess, lpRemoteBuf, string_data, sizeof(string_data), &dwHasWrite))
{
//printf("write size is %d\n",dwHasWrite);
if(dwHasWrite != sizeof(string_data))
{
VirtualFreeEx(hProcess,lpRemoteBuf,sizeof(string_data), MEM_RELEASE);
printf("\nwrite data length wrong!\n");
goto bad;
}
}else
{
printf("\nwrite data falied : %d",GetLastError());
goto bad;
}
tempaddr = (uint32_t)lpRemoteBuf; //数据区首地址是lpRemoteBuf
addr[0]=((char*)&tempaddr)[0];
addr[1]=((char*)&tempaddr)[1];
addr[2]=((char*)&tempaddr)[2];
addr[3]=((char*)&tempaddr)[3];
printf("data block first addr: ");
printf_hex_buff((uint8_t*)&lpRemoteBuf,4);
//printf_hex_buff((uint8_t*)&shellcode2,sizeof(shellcode2));
//填入字符串的 VA 地址
*(uint32_t*)&shellcode1[5] = (uint32_t)getaddr1;//LoadlibraryA的地址填入
*(uint32_t*)&shellcode1[10] = (uint32_t)((lpRemoteBuf)+0);//填入user32.dll的地址
*(uint32_t*)&shellcode1[22] = (uint32_t)((lpRemoteBuf)+11);//填入messagebox的地址
*(uint32_t*)&shellcode1[30] = (uint32_t)getaddr2;//填入GetProAddress地址
*(uint32_t*)&shellcode1[48] = (uint32_t)((lpRemoteBuf)+23);//标题
*(uint32_t*)&shellcode1[56] = (uint32_t)((lpRemoteBuf)+30);//内容
//填那5字节的目标对象 71--82 12个
for(int i=0;i<12;i++)
{
if(i<buffsize)
shellcode1[71+i]=buff[i];
else
shellcode1[71+i]=0x90;
}
//写入shellcode
lpRemoteBuf = VirtualAllocEx(hProcess, NULL, sizeof(shellcode1), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
tempaddr = ((uint32_t)lpRemoteBuf)+83;//E9的位置
tempaddr = function_addr+ 5 - (tempaddr + 5 );
*(uint32_t *)&shellcode1[84]=tempaddr;
printf("shellcode at %X, E9 at %X, target at %X, jump distance tmp is %X\n",(uint32_t)lpRemoteBuf,((uint32_t)lpRemoteBuf)+83,function_addr,tempaddr);
printf("shellcode is : \n");
printf_hex_buff(shellcode1,sizeof(shellcode1));
dwHasWrite=0;
if(WriteProcessMemory(hProcess, lpRemoteBuf, shellcode1, sizeof(shellcode1), &dwHasWrite))
{
if(dwHasWrite != sizeof(shellcode1))
{
VirtualFreeEx(hProcess,lpRemoteBuf,sizeof(shellcode1),MEM_COMMIT);
printf("WriteProcessMemory failed\n");
goto bad;
}
}else
{
printf("\nWriteProcessMemory failed: %d",GetLastError());
goto bad;
}
//实现Inline hook
tempaddr = (uint32_t)lpRemoteBuf;
tempaddr = tempaddr - (function_addr + 5 );
buff[0] = 0xe9;
*(uint32_t*)&buff[1] = (uint32_t)tempaddr;
if(buffsize>5){
for(int i=5;i<buffsize;i++)
buff[i]=0x90;
}
dwHasWrite=0;
if(WriteProcessMemory(hProcess, function_addr, buff, buffsize, &dwHasWrite))
{
if(dwHasWrite!=buffsize)
{
printf("\nWriteProcessMemory failed: %d",GetLastError());
goto bad;
}
printf("Hook success!\n");
}
else{
printf("\nWriteProcessMemory failed: %d",GetLastError());
}
good:
free(buff);
CloseHandle(hProcess);
system("pause");
return 0;
bad:
free(buff);
CloseHandle(hProcess);
system("pause");
return -1;
}
结果
测试记事本的writefile函数【kernel32.dll中】
先在桌面新建一个txt文件,然后用notepad.exe打开它。运行inlinehook.exe程序,hook掉kernel32.dll里的WriteFile函数。接着,按ctrl+s,可以看到弹出了消息框,如图4-5(a)所示,点击确定,关闭记事本。再次打开文件,发现文件内容被保存,如图4-5(b)所示。说明hook并没有干扰记事本原有的功能。
后记
- 可以不用提升权限
- 如果目标函数前几个字节,本来是一个通过相对寻址的指令,比如E8/E9这些跳转指令,上面的代码需要更改,把E8后面四字节调整成在shellcode中的内存地址访问的偏移。
- 可以通过一些自动化的方法,调一些库,判断应该替换几个字节,每次多取一个字节,判断是不是一个完整的汇编指令。