编程实践4(内存代码注入)

题目要求:

设计和实现一个软件,其功能如下:
1、显示所有的进程列表;
2、选中一个进程,显示该进程的所有IAT中的函数;
3、选中一个IAT函数,实现Hooking和 inline Hooking,在Hooking的函数中显示”组号:姓名”。

显示所有进程列表

  1. 函数CreateToolhelp32Snapshot用于获取指定进程的快照或者所有进程的快照。并且返回快照的句柄。
  2. 函数Process32First用于获取进程快照中的第一个进程的信息,并且将这个进程的信息保存在一个PROCESSENTRY32的结构体中。在这个结构体中保存着进程的ID号和名称。
  3. 函数Process32Next用于获取快照中下一个进程的信息。我们循环调用这个函数,那么可以遍历进程快照中的所有进程。
    源程序如下:
#include <windows.h> 
#include<iostream>
#include <tlhelp32.h>
#include <stdio.h>
using namespace std;
DWORD GetProcessId(char*myprocess)//枚举进程函数
{
    DWORD Pid = -1;
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    //创建系统快照
    PROCESSENTRY32 lPrs;
    ZeroMemory(&lPrs, sizeof(lPrs));
    lPrs.dwSize = sizeof(lPrs);
    char *targetFile = myprocess;
    bool flag = false;
    if (Process32First(hSnap, &lPrs)) {//取得系统快照里第一个进程信息
        //printf("name\t\t\tpid\t\tsize\n");
        printf("%-50s%u\t\t%u\n", lPrs.szExeFile, lPrs.th32ProcessID, lPrs.dwSize);
        if (strstr(targetFile, lPrs.szExeFile))//判断进程信息是否是explorer.exe
        {
            Pid = lPrs.th32ProcessID;
            flag = true;
        }
    }
    while (1)
    {
        ZeroMemory(&lPrs, sizeof(lPrs));
        lPrs.dwSize = (&lPrs, sizeof(lPrs));
        if (!Process32Next(hSnap, &lPrs))//继续枚举进程信息
        {
            break;
        }
        if (strstr(targetFile, lPrs.szExeFile))//判断进程信息是否是explorer.exe
        {
            Pid = lPrs.th32ProcessID;
            flag = true;

        }
        printf("%-50s%u\t\t%u\n", lPrs.szExeFile, lPrs.th32ProcessID, lPrs.dwSize);

    }
    if(flag)
        return Pid;
    else return -1;
}
int main() {
    printf("start.....\n");
    char myprocess[50] = "cloudmusic.exe";
    DWORD myPid = GetProcessId(myprocess);
    printf("%u\n", myPid);
    getchar();
    return 0;
}

显示进程所有的IAT函数

首先获得模块的句柄:

// 取得主模块的模块句柄(即进程模块基地址)
 HMODULE hModule = ::GetModuleHandleA(NULL);

然后把进程基址赋给pDosHeader,即起始基址就是PE的IMAGE_DOS_HEADER

IMAGE_DOS_HEADER* pDosHeader = (IMAGE_DOS_HEADER*)hModule;

然后定位到PE header
基址hMod加上IMAGE_DOS_HEADER结构的e_lfanew成员到达IMAGE_NT_HEADERS
NT文件头的前4字节是文件签名("PE00" 字符串),然后是20字节的IMAGE_FILE_HEADER结。即到达IMAGE_OPTIONAL_HEADER结构的地址,获取了一个指向IMAGE_OPTIONAL_HEADER结构体的指针。

IMAGE_OPTIONAL_HEADER* pOpNtHeader = (IMAGE_OPTIONAL_HEADER*)((BYTE*)hModule + pDosHeader->e_lfanew + 24);

定位导入表:
通过IMAGE_OPTIONAL_HEADER结构中的DataDirectory结构数组中的第二个成员中的VirturalAddress字段定位到IMAGE_IMPORT_DESCRIPTOR结构的起始地址即获得导入表中第一个IMAGE_IMPORT_DESCRIPTOR结构的指针(导入表首地址)

IMAGE_IMPORT_DESCRIPTOR* pImportDesc = (IMAGE_IMPORT_DESCRIPTOR*)((BYTE*)hModule + pOpNtHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

通过两重循环输出IAT函数,外层函数是对模块循环,内层循环对一个模块里面的所有函数进行循环输出:

while (pImportDesc->FirstThunk)
 {
  char* pszDllName = (char*)((BYTE*)hModule + pImportDesc->Name);
  printf("模块名称:%s\n", pszDllName);
  DWORD n = 0;
  //一个IMAGE_THUNK_DATA就是一个导入函数
  IMAGE_THUNK_DATA* pThunk = (IMAGE_THUNK_DATA*)((BYTE*)hModule + pImportDesc->OriginalFirstThunk);
  while (pThunk->u1.Function)
  {
   //取得函数名称
   char* pszFuncName = (char*)((BYTE*)hModule+pThunk->u1.AddressOfData+2); //函数名前面有两个..
   printf("function name:%-25s,  ", pszFuncName);
   //取得函数地址
   PDWORD lpAddr = (DWORD*)((BYTE*)hModule + pImportDesc->FirstThunk) + n; //从第一个函数的地址,以后每次+4字节
   printf("addrss:%X\n", lpAddr);
   n++; //每次增加一个DWORD
   pThunk++;
  }
  printf("\n");
  pImportDesc++;
 }

实现Inline hooking

Inline hooking是一种拦截目标函数调用的方法,主要用于抗病毒软件,沙箱和恶意软件。 一般的想法是将函数重定向到我们自己的函数,以便在函数执行之前和/或之后执行处理。 这可能包括:检查参数,匀场,记录,欺骗返回的数据和过滤呼叫。 Rootkit倾向于使用钩子来修改从系统调用返回的数据,以隐藏其存在,而安全软件则使用它们来防止/监视潜在的恶意操作。
hooking通过直接修改目标函数(内联修改)中的代码来放置,通常通过用跳转覆盖前几个字节; 这允许在函数进行任何处理之前重定向执行。 大多数引擎引擎使用32位相对跳转,占用5个字节的空间。
如何实现:
将使用一个基于trampoline的钩子,它允许我们截取功能,同时仍然可以调用原件。这个钩子由3部分组成:

  1. Hook - 一个5字节的相对跳转,写入目标函数以挂接它,跳转将从挂钩函数跳转到我们的代码。
  2. proxy这是我们指定的函数(或代码),钩子放在目标函数上将跳转到。
  3. trampoline用于绕过钩子,所以我们可以正常地调用挂钩功能。

使用trampoline的原因是:
假设我们要钩住MessageBoxA,从代理功能中打印出参数,然后显示消息框:为了显示消息框,我们需要调用MessageBoxA(它们重定向到我们的代理函数,这反过来又调用MessageBoxA)。 显然从我们的代理函数中调用MessageBoxA将导致无限递归,并且程序由于堆栈溢出而最终崩溃。
我们可以简单地从代理函数中取消MessageBoxA,调用它,然后重新挂接它; 但是如果多个线程同时调用MessageBoxA,这将导致竞争条件,并可能导致程序崩溃。
相反,我们可以做的是存储MessageBoxA的前5个字节(这些被我们的钩子覆盖),然后当我们需要调用非挂钩的MessageBoxA时,我们可以执行存储的前5个字节,然后是5个字节的跳转 MessageBoxA(直接挂钩)。
只要前5个字节不是相对指令,它们可以在任何地方执行。
在这个例子中,函数的前5个字节组成了3个指令mov edi,edi; push ebp; mov ebp,esp,但是,例如,如果第一个指令是10个字节长,我们只存储5个字节trampoline将执行一半的指令,导致程序爆炸。 为了解决这个问题,我们必须使用反汇编来获取每个指令的长度。 最好的情况是前n个指令总共5个字节,最糟糕的情况是如果第一个指令是4个字节,而第二个指令是16个(x86指令的最大长度),则必须存储20个字节(4 + 16),这意味着trampoline的大小必须是25个字节(空间最多可达20个字节的指令,5个字节跳回挂钩的功能)。重要的是,返回跳转必须跳转到挂接的函数n个字节,其中n是我们存储在trampoline中的指令。

代码编写

首先,我们需要定义代理函数。我们把hooking函数重定向。对于这个例子,我们只需要在显示消息框之前打印出参数。

int WINAPI NewMessageBoxA(HWND hWnd, LPCSTR lpText, LPCTSTR lpCaption, UINT uType)
{
    printf("MessageBoxA called!ntitle: %sntext: %snn", lpCaption, lpText);
    return OldMessageBoxA(hWnd, lpText, lpCaption, uType);
}

int WINAPI NewMessageBoxW(HWND hWnd, LPWSTR lpText, LPCTSTR lpCaption, UINT uType)
{
    printf("MessageBoxW called!ntitle: %wsntext: %wsnn", lpCaption, lpText);
    return OldMessageBoxW(hWnd, lpText, lpCaption, uType);
}

OldMessageBox只是一个typedef,它将指向25个字节的可执行内存,该挂钩功能将存储trampoline。

typedef int (WINAPI *TdefOldMessageBoxA)(HWND hWnd, LPCSTR lpText, LPCTSTR lpCaption, UINT uType);
typedef int (WINAPI *TdefOldMessageBoxW)(HWND hWnd, LPWSTR lpText, LPCTSTR lpCaption, UINT uType);
TdefOldMessageBoxA OldMessageBoxA = (TdefOldMessageBoxA)VirtualAlloc(NULL, 25, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
TdefOldMessageBoxW OldMessageBoxW = (TdefOldMessageBoxW)VirtualAlloc(NULL, 25, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

现在对于hooking函数,我们将具有以下参数:

  • name - 挂钩功能的名称。
  • dll - 目标函数所在的dll。
  • proxy - 指向代理函数的指针(NewMessageBox)。
  • original - 指向25字节的可执行内存的指针,存储trampoline。
  • length - 指向一个变量的指针,该变量接收存储在trampoline中的指令值。

在hooking函数内部,我们将获取目标函数的地址,然后使用Hacker Dissasembler Engine(HDE32)来拆分每个指令并获取长度,直到我们有5个或更多个字节值得整个指令(hde32_disasm返回长度的第一个参数指向的指令)。

LPVOID FunctionAddress;
DWORD TrampolineLength = 0;
 
FunctionAddress = GetProcAddress(GetModuleHandleA(dll), name);
if(!FunctionAddress)
 return FALSE;
 
//拆分每个指令的长度,直到我们有5个以上的字节值
while(TrampolineLength < 5)
{
 LPVOID InstPointer = (LPVOID)((DWORD)FunctionAddress + TrampolineLength);
 TrampolineLength += hde32_disasm(InstPointer, &disam);
}

为了构建实际的trampoline,我们首先将目标函数中的TrampolineLength字节复制到trampoline缓冲区(传递给参数“original”中的函数),然后我们将复制的字节用n字节附加到目标函数中,n是Trampoline的长度。

相对跳转是距离跳转结束的距离,即:(destination - (source + 5))。 跳跃的来源将是trampoline地址+trampoline长度,目的地将是hooking函数+trampoline长度。

DWORD src = ((DWORD)FunctionAddress + TrampolineLength);
DWORD dst = ((DWORD)original + TrampolineLength + 5);
BYTE jump[5] = {0xE9, 0x00, 0x00, 0x00, 0x00};

//将n个字节从目标函数存储到trampoline中
memcpy(original, FunctionAddress, TrampolineLength);
 
//设置跳转的第二个字节(偏移量),跳转到我们想要的位置
*(DWORD *)(jump+1) = src - dst;
 
//将跳转复制到trampoline的末端
memcpy((LPVOID)((DWORD)original+TrampolineLength), jump, 5);

在我们可以编写跳转函数之前,我们需要确保内存是可写的(通常不可以),我们通过使用VirtualProtect将保护设置为PAGE_EXECUTE_READWRITE来实现。

//确保该功能是可写的
DWORD OriginalProtection;
if(!VirtualProtect(FunctionAddress, 8, PAGE_EXECUTE_READWRITE, &OriginalProtection))
 return FALSE;

要摆放hooking,我们需要做的就是创建一个从目标函数跳转到代理的跳转,然后我们可以用它覆盖目标的前5个字节。 为了避免在写入跳转时调用函数的任何风险,我们必须一次性写完。而atomic functions只能用于基础2(2,4,8,16等)的大小; 我们的跳转是5个字节,我们可以复制的最接近的大小是8,所以我们必须制作一个自定义函数SafeMemcpyPadded,它将源缓冲区用来从目的地的字节缓冲到8个字节,这样最后3个字节保持不变 复制后。

cmpxchg8b将edx:eax中保存的8个字节与目标进行比较,如果它们相等,则复制ecx:ebx中保存的8个字节,我们将edx:eax设置为目标字节,以使副本始终发生。

//构建并写hooking
*(DWORD *)(jump+1) = (DWORD)proxy - (DWORD)FunctionAddress - 5;
SafeMemcpyPadded(FunctionAddress, Jump, 5);

void SafeMemcpyPadded(LPVOID destination, LPVOID source, DWORD size)
{
 BYTE SourceBuffer[8];
 
 if(size > 8)
  return;
 
 //使用来自目的地的字节来填充源缓冲区
 memcpy(SourceBuffer, destination, 8);
 memcpy(SourceBuffer, source, size);
 
 __asm 
 {
  lea esi, SourceBuffer;
  mov edi, destination;
 
  mov eax, [edi];
  mov edx, [edi+4];
  mov ebx, [esi];
  mov ecx, [esi+4];
 
  lock cmpxchg8b[edi];
 }
}

现在要做的就是恢复页面保护。 刷新指令缓存,并将length参数设置为Trampoline的长度。

//恢复页面保护
VirtualProtect(FunctionAddress, 8, OriginalProtection, &OriginalProtection);
 
//清楚CPU指令缓存
FlushInstructionCache(GetCurrentProcess(), FunctionAddress, TrampolineLength);
 
*length = TrampolineLength;
return TRUE;

hooking功能可以这样简单地调用。

DWORD length;
HookFunction("user32.dll", "MessageBoxA", &NewMessageBoxA, OldMessageBoxA, &length);

通过从OldMessageBox(Trampoline)将length字节复制到hooking函数来完成脱钩。

代码链接:http://pan.baidu.com/s/1bo0ZabH

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,817评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,329评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,354评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,498评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,600评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,829评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,979评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,722评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,189评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,519评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,654评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,940评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,762评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,993评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,382评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,543评论 2 349

推荐阅读更多精彩内容