点击上方[蓝字],关注我们
免责声明
本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者及本公众号不为此承担任何责任。
文章正文
在本文中,我将向读者展示如何使用C语言编写自己的RDI / sRDI加载器,然后展示如何优化代码使其完全位置无关。
作为安全研究员和恶意软件开发人员,能够使用RDI / sRDI等技术创建自己的加载程序可以帮助避免安全软件的检测,并增加给定恶意软件变体的寿命。植入程序越独特,安全软件检测和分析就越困难,从而使其在其预期用途上更加有效。学习如何使用RDI / sRDI加载等技术编写自己的加载程序是保持竞争优势的关键因素。
TL;DR
最终的代码示例已添加到我的仓库中,但是如果您正在学习,我强烈建议您在进行操作时重新输入所有内容以获得最大的收益。
https://github.com/maliciousgroup/RDI-SRDI?ref=blog.malicious.group
话虽如此,让我们看一下跟随本文所需的先决条件。
先决条件
反射式DLL注入(RDI)和Shellcode反射式DLL注入(sRDI)是攻击者用于将DLL或shellcode加载到进程中的技术,该技术不需要传统的注入方法。RDI由Stephen Fewer于2009年引入,而sRDI由Adam Chester在2016年的DerbyCon会议上介绍。这两位研究人员都因向公众介绍这些技术而受到赞誉。
要完全掌握本文中的示例,必须对PE文件格式及其在Windows中的典型加载方式有深入的了解。
以下列表概述了Windows加载PE文件时采取的几个关键步骤。这些也是我们编写RDI加载器时需要采取的相同步骤。
1.在此步骤中,从文件系统或内存读取DLL文件的二进制表示形式。这通常涉及打开文件,读取其内容并将二进制数据存储在内存中以进行进一步处理。
2.解析DLL文件的PE头以提取重要信息,例如映像大小。PE头是位于DLL文件开头的数据结构,其中包含有关文件的组织和布局的信息,包括不同节的大小。
3.在此步骤中,在目标进程的地址空间中分配内存以容纳DLL的二进制数据。这通常使用VirtualAllocEx函数完成,该函数保留并提交具有适当大小以容纳整个DLL映像的内存块。然后使用PIMAGE_SECTION_HEADER结构进行迭代并将节复制到新分配的内存中。
4.重定位(也称为修补)应用于DLL映像,以调整映像中代码和数据的地址以匹配映像在目标进程中加载的基地址。此步骤涉及迭代PE头中的重定位表,并对分配的内存中相应位置应用必要的地址调整。
5.处理DLL的导入地址表(IAT)以解析和更新对外部函数和数据的导入引用。此步骤涉及迭代IAT并通过将所需模块加载到目标进程中,获取导入函数或数据的地址,并使用已解析的地址更新IAT来解析导入引用。
6.将DLL映像中不同部分的保护设置应用于分配内存中相应的内存页面。这涉及使用VirtualProtectEx函数设置适当的内存保护标志,例如PAGE_EXECUTE_READWRITE用于可执行部分和PAGE_READWRITE用于可写部分。
7.如果DLL具有线程局部存储(TLS)回调,则在目标进程中执行它们。TLS是一种机制,允许进程中的每个线程都具有自己的线程特定数据存储。TLS回调是在创建或终止线程时执行的函数,它们通常用于初始化或清理线程特定数据。
8.最后,将执行权移交给DLL的入口点,即DllMain函数。DllMain是DLL中的特殊函数,在加载或卸载DLL时操作系统会自动调用它,并负责执行特定于DLL的任何必要初始化或清理任务。
为了确保彻底理解上述步骤,重点想象如何使用C代码执行每个任务。如果您还不确定或希望获得有关PE格式功能的更多知识,我强烈建议参加Xeno Kovah的OST2[课程](https://www.youtube.com/playlist?list=PLUFkSN0XLZ- n_Na6jwqopTt1Ki57vMIc3&ref=blog.malicious.group "课程")或观看Dr. Josh Stroschein在YouTube上发布的视频。
从现在开始,我还将假设读者具备了至少一些我将在以下部分中涵盖的主题方面的先前知识。
构建设置
为了开发,我选择使用Jetbrains' CLion作为首选IDE。由于我之前购买了PyCharm Professional,因此我已经拥有Jetbrains帐户,并发现CLion是这项任务的绝佳工具。
在即将到来的构建过程中,我的第一步将涉及创建一个仅用于测试我们的RDI / sRDI加载程序的通用DLL。成功创建DLL后,我将继续构建使用标准Windows API执行测试DLL注入的基本RDI加载程序。最后,我将创建一个离散RDI / sRDI加载程序,其中包括函数哈希、混淆函数指针,并且利用来自ntdll.dll 的Native API。
为了清晰起见,我将把加载器代码分成不同的部分。我发现这是展示标准API与Native API下加载器工作方式最简单的方法。
DLL创建
使用CLion作为IDE时,我的第一步将是创建一个新C项目,名称为dll_poc,如下例所示。
创建dll_poc项目后,您应该看到以下图像,这将允许您开始编辑main.c文件。
以下代码应该放入main.c中,以创建并导出DLL文件中的DllMain()函数。
main.c
#include <windows.h>
#define DLLEXPORT __declspec( dllexport )
DLLEXPORT BOOL DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved);
BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
MessageBoxA(NULL, "DLL PROCESS ATTACH", "Bingo!", 0);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
default:
break;
}
return TRUE;
}
为了确保我们将其编译为DLL而不是EXE,我们需要修改位于项目根目录中的CMakeLists.txt文件。该文件应如下所示。
CMakeLists.txt
cmake_minimum_required(VERSION 3.24)
project(dll_poc C)
set(CMAKE_C_STANDARD 17)
# add_executable(dll_poc main.c)
set(CMAKE_SHARED_LIBRARY_PREFIX "")
add_library(dll_poc SHARED main.c)
现在,两个上述文件都已修改,我们可以继续构建项目,这应该会在通用的cmake-debug-build目录中创建一个dll_poc.dll文件。
创建了dll_poc.dll文件后,我们可以通过使用rundll32快速测试DllMain函数是否正常工作而不出错。
有了DLL,我将把DLL复制到C:/ Temp /以进行简单的测试。
cp .cmake-build-debugdll_poc.dll C:Temp
基本的RDI加载器
将编译后的DLL文件移动到C:/ Temp /后,就可以开始编写加载器了。因此,我将再次创建一个名为dll_loader的新C项目,如下所示。
创建新项目后,在IDE中使用快捷键 Alt + F12打开终端,并创建以下文件夹。根据您在CLion中使用的命令解释器,来决定您将运行哪些命令。
构造所需的文件夹
Powershell
New-Item -ItemType Directory -Force -Path "srcc","srch","srcmasm"
Cmd
mkdir srcc srch srcmasm
一旦设置了上述文件夹,IDE文件浏览器应如下所示。
现在,已创建目录,可以使用以下命令将其中一些空文件填充到各自的目录中。
构造所需的文件
Powershell
New-Item -ItemType File -Path "src/c/peb.c", "src/h/peb.h", "src/masm/peb.masm"
New-Item -ItemType File -Path "src/h/defs.h", "src/h/structs.h"
Cmd
echo.>srccpeb.c
echo.>srchpeb.h
echo.>srcmasmpeb.masm
copy /b srccpeb.c+,, srchpeb.h+,, srcmasmpeb.masm+,
echo.>srchdefs.h
echo.>srchstructs.h
运行上述命令以设置一些空文件后,您应该会看到下图所示的文件布局。
创建了所有必要的文件存根之后,下一步是使用main.c创建基本DLL注入。现在,我们将使用来自ired.team的示例,其中使用我认为“高级”的Windows API函数来确保逻辑可靠。确认后,我们可以继续开发和优化代码。
基本-步骤1
在此步骤中,从文件中读取DLL文件的二进制表示形式...
HANDLE dll = CreateFileA("\??\C:\Temp\dll_poc.dll", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
DWORD64 dll_size = GetFileSize(dll, NULL);
LPVOID dll_bytes = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dll_size);
DWORD out_size = 0;
ReadFile(dll, dll_bytes, dll_size, &out_size, NULL);
此代码使用CreateFileA函数打开要读取的DLL,然后使用HeapAlloc分配一些堆空间,该空间将存储ReadFile正在读取的文件。
基本-步骤2
解析DLL文件的PE头以提取重要信息...
PIMAGE_DOS_HEADER dos_headers = (PIMAGE_DOS_HEADER)dll_bytes;
PIMAGE_NT_HEADERS nt_headers = (PIMAGE_NT_HEADERS)((DWORD_PTR)dll_bytes + dos_headers->e_lfanew);
SIZE_T dllImageSize = nt_headers->OptionalHeader.SizeOfImage;
此代码使用PIMAGE_DOS_HEADER和PIMAGE_NT_HEADERS结构查找DLL文件的大小,该大小存储在nt_headers->OptionalHeader.SizeOfImage成员变量中。
基本-步骤3
在目标进程的地址空间中分配内存以容纳二进制文件...
LPVOID dllBase = VirtualAlloc((LPVOID)ntHeaders->OptionalHeader.ImageBase, dllImageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
DWORD_PTR deltaImageBase = (DWORD_PTR)dllBase - (DWORD_PTR)ntHeaders->OptionalHeader.ImageBase;
memcpy(dllBase, dllBytes, ntHeaders->OptionalHeader.SizeOfHeaders);
PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHeaders);
for (size_t i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++) {
LPVOID sectionDestination = (LPVOID)((DWORD_PTR)dll_base + (DWORD_PTR)section->VirtualAddress);
LPVOID sectionBytes = (LPVOID)((DWORD_PTR)dll_bytes + (DWORD_PTR)section->PointerToRawData);
memcpy(sectionDestination, sectionBytes, section->SizeOfRawData);
section++;
}
此代码负责将DLL的各个节从磁盘上的文件复制到先前为DLL分配的内存块中。它通过使用PIMAGE_SECTION_HEADER和IMAGE_FIRST_SECTION结构来迭代要复制到分配内存中的每个节来实现此目的。
基本-步骤4
加载程序执行任何必要的重定位修复。重定位...
cmake_minimum_required(VERSION 3.24)
project(dll_poc C)
set(CMAKE_C_STANDARD 17)
# add_executable(dll_poc main.c)
set(CMAKE_SHARED_LIBRARY_PREFIX "")
add_library(dll_poc SHARED main.c)
0
此代码负责将DLL映像重新定位到其新基地址。它首先获取重定位数据目录并计算重定位表的RVA。然后,它使用PBASE_RELOCATION_ENTRY和PBASE_RELOCATION_BLOCK结构迭代重定位表中的每个块,并根据重定位类型和偏移量计算要修补的地址。它读取要修补地址处的原始值,将其加上增量映像基址,并将其写回同一位置以更新地址到新位置。该过程确保DLL可以在其新基地址上正确运行。
基本-步骤5
加载程序执行任何必要的导入。导入是对函数的引用...
cmake_minimum_required(VERSION 3.24)
project(dll_poc C)
set(CMAKE_C_STANDARD 17)
# add_executable(dll_poc main.c)
set(CMAKE_SHARED_LIBRARY_PREFIX "")
add_library(dll_poc SHARED main.c)
1
此代码负责加载DLL的导入函数。它首先检索导入目录的地址,然后循环遍历每个导入描述符,通过其名称或序数值加载包含导入函数的库并解析每个导入函数。然后它在适当的thunk表项中设置每个导入函数的地址。
基本-步骤6
最后,加载程序将控制权转移到可执行文件的入口点...
cmake_minimum_required(VERSION 3.24)
project(dll_poc C)
set(CMAKE_C_STANDARD 17)
# add_executable(dll_poc main.c)
set(CMAKE_SHARED_LIBRARY_PREFIX "")
add_library(dll_poc SHARED main.c)
2
此代码片段使用已加载DLL的OptionalHeader字段AddressOfEntryPoint获取DLL入口点的地址。然后,它将此地址转换为类型为DLLEntry的函数指针,该类型表示DLL的入口点。(* DllEntry)语法取消引用函数指针并使用已加载DLL的HINSTANCE、DLL_PROCESS_ATTACH标志以及第三个参数的值0调用入口点函数。最后,代码释放为加载DLL分配的资源,包括关闭对文件的句柄和释放为存储DLL字节而分配的内存。
基本-最后部分
采取上述所有步骤并将其组合起来将如下所示。
cmake_minimum_required(VERSION 3.24)
project(dll_poc C)
set(CMAKE_C_STANDARD 17)
# add_executable(dll_poc main.c)
set(CMAKE_SHARED_LIBRARY_PREFIX "")
add_library(dll_poc SHARED main.c)
3
在main.c中键入代码后,构建解决方案并运行可执行文件。这应该与项目创建时附带的默认CMakeLists.txt一起工作。
正如您在上面看到的那样,此示例有效。但是,这是一个非常基本的RDI注入示例,没有任何优化或混淆,并且在大多数安全环境中都不起作用。看看原因,让我们仔细研究一下二进制文件。
首先让我们运行二进制文件,并在不点击“Ok”按钮关闭它时打开Process Hacker并查看内存。
正如您在上面的图像中看到的那样,我们当前在内存中有一个RWX区域,这是一个明显提示某些可疑事件正在发生。我们需要确保修复这个问题以及即将推出版本中的其他所有内容。
接下来,让我们使用CFF Explorer打开文件,如下所示。
在这里,您可以看到dll_loader.exe文件正在导入带有24个函数的KERNEL32.dll和带有26个函数的msvcrt.dll。您可能会问自己为什么有50个导入函数,而代码本身只使用其中一小部分?这是因为msvcrt.dll和更高级别的Windows API函数也使用底层函数。
最后,让我们使用Sys Internals工具中的strings.exe查看二进制文件。
cmake_minimum_required(VERSION 3.24)
project(dll_poc C)
set(CMAKE_C_STANDARD 17)
# add_executable(dll_poc main.c)
set(CMAKE_SHARED_LIBRARY_PREFIX "")
add_library(dll_poc SHARED main.c)
4
在strings.exe输出中有1490个条目,并且以下是我们想要删除的某些项目示例。
上面输出中所有使用的函数都可以以明文形式轻松查看。这对于任何恶意软件开发人员来说都是一个问题,并且需要处理以使此加载器更隐蔽。
正如您在上面的示例中看到的那样,基本加载器产生了大量信息,还需要大量工作。现在让我们看看编写使用Windows Native API、混淆函数指针、函数名称哈希等方式进行混淆处理RDI注入。
隐蔽加载器
在此版本的加载器中,我们将利用先前生成的文件来存储自定义结构、函数定义和汇编代码以提高RDI加载器的隐蔽性。使用相同的dll_loader项目,暂时注释掉当前main()函数,以便我们可以从头开始重新编写所有内容。首先,我们需要创建一些辅助函数来进行函数哈希和函数地址解析。为此,我们将从peb.c文件中合并CRC哈希函数,并添加一个get_proc_address_by_hash函数,该函数根据提供的DLL解析函数地址。
辅助函数
peb.c
cmake_minimum_required(VERSION 3.24)
project(dll_poc C)
set(CMAKE_C_STANDARD 17)
# add_executable(dll_poc main.c)
set(CMAKE_SHARED_LIBRARY_PREFIX "")
add_library(dll_poc SHARED main.c)
5
如上所示的代码中,crc32b函数接受一个字符串并返回用于存储的哈希值,get_proc_address_by_hash函数接受DLL的基地址和散列函数名称作为输入,并返回函数地址。我们还添加了peb.h头文件,其中包括crc32b所需的变量,如下所示。
peb.h
cmake_minimum_required(VERSION 3.24)
project(dll_poc C)
set(CMAKE_C_STANDARD 17)
# add_executable(dll_poc main.c)
set(CMAKE_SHARED_LIBRARY_PREFIX "")
add_library(dll_poc SHARED main.c)
6
接下来,我们将把旧的main.c中的结构移动到structs.h头文件中,如下所示。
structs.h
cmake_minimum_required(VERSION 3.24)
project(dll_poc C)
set(CMAKE_C_STANDARD 17)
# add_executable(dll_poc main.c)
set(CMAKE_SHARED_LIBRARY_PREFIX "")
add_library(dll_poc SHARED main.c)
7
接下来,我们将向defs.h文件添加一些内容以定义一些常量。随着我们进一步进行,这也是我们所有函数定义的位置。
defs.h
cmake_minimum_required(VERSION 3.24)
project(dll_poc C)
set(CMAKE_C_STANDARD 17)
# add_executable(dll_poc main.c)
set(CMAKE_SHARED_LIBRARY_PREFIX "")
add_library(dll_poc SHARED main.c)
8
现在是时候熟悉一些基本的MASM汇编指令了。由于我们将Native API用于我们的加载器,因此必须知道如何获取ntdll.dll库的地址以在需要时解析Native API函数。虽然这项任务也可以在C语言中完成,但学习汇编语言可以帮助编写C代码,这就是我们将重点关注它的原因。
peb.masm
cmake_minimum_required(VERSION 3.24)
project(dll_poc C)
set(CMAKE_C_STANDARD 17)
# add_executable(dll_poc main.c)
set(CMAKE_SHARED_LIBRARY_PREFIX "")
add_library(dll_poc SHARED main.c)
9
上面的代码正在遍历当前64位进程的PEB(进程环境块)以查找ntdll.dll的地址。这是因为ntdll.dll DLL在所有进程中具有相同的基地址。以下是上述代码中发生的步骤解释说明内容。
1.xor rax,rax:这将rax寄存器的值设置为零,这是在使用寄存器之前初始化寄存器的常见方法。
2.mov rax,gs:[60h]:这从gs段寄存器中检索PEB的地址,gs段寄存器是Windows用于线程本地存储的寄存器。偏移量60h是TIB(线程信息块)中PEB指针的位置,TIB是Windows用于存储有关线程的信息的另一个数据结构。
3.mov rax,[rax + 18h]:这检索PEB的Ldr(Loader)成员的地址,它是指向已加载模块链表的指针。
4.mov rax,[rax + 20h]:这检索链接列表中的第一个条目的地址,该条目对应于进程的主模块(即可执行文件本身)。
5.mov rax,[rax]:这检索主模块的基地址。
6.mov rax,[rax + 20h]:这检索链接列表中的第二个条目的地址,该条目对应于ntdll.dll。
7.ret:这返回rax的值,现在它包含ntdll.dll的基地址。
32位应用程序的指令略有不同,但由于我正在使用64位,因此将使用以上版本。
使用peb.masm创建get_ntdll()函数后,我们需要向peb.h文件添加一行以导出get_ntdll()函数以供使用。
peb.h
cp .cmake-build-debugdll_poc.dll C:Temp
0
如上所示,我们添加了第7行以便从我们的C代码中访问get_ntdll()MASM函数。建了上述辅助函数后,在继续之前要做的最后一件事是设置CMakeLists.txt配置以设置MASM编译以及其他使事情更容易的特性。
CMakeLists.txt
cp .cmake-build-debugdll_poc.dll C:Temp
1
在上述配置中,它将允许我们添加我们的MASM文件以编译并用作C编译过程的一部分。它还允许项目中的每个文件仅按名称添加头文件,而无需添加每个路径。您会注意到我将ml64.exe二进制文件移动到C:/ Temp /目录以进行测试,因此您可以执行相同操作或在类似Visual Studio之类的地方使用ml64.exe存在的完整路径。现在Cmake配置完成了,我们可以转到main.c文件以开始编写和设置在main.c中需要的新功能和结构。在main.c中,我们将使用基本加载器中每个步骤中的逻辑,并将其重写为Native API函数,并使用函数指针混淆。
cp .cmake-build-debugdll_poc.dll C:Temp
2
上面的代码是我们基本加载器中的第1步,该代码基本上只是以读取模式打开文件,在分配内存以存储数据之前读取文件大小,并通过ReadFile函数读取到分配的内存中。
要使用Native API重写这些函数,重要的是清楚地了解需要哪些函数以及如何为每个函数设置函数名称哈希和函数定义。以下函数需要替换上面第1步代码中的函数。
1.RtlInitUnicodeString-需要创建Unicode字符串
2.NtCreateFile-需要替换CreateFileA
3.NtQueryInformationFile-需要获取有关文件的信息
4.NtAllocateVirtualMemory-需要为文件分配内存
5.NtReadFile-需要读取文件内容
为了使这些函数起作用,我们将需要向structs.h头文件添加一些新结构。这是因为Native API调用依赖于需要添加的较低层级数据结构。
1.UNICODE_STRING
2.OBJECT_ATTRIBUTES
3.IO_STATUS_BLOCK
4.FILE_STANDARD_INFORMATION
5.PIO_APC_ROUTINE
由于这是恶意软件开发中的常见主题,我将向您展示我用来获取两个结构和函数定义以及如何哈希名称的定义来源。
我经常参考x64dbg项目中使用的ntdll.dll头文件作为参考点。我认为不必在我的项目中包含整个文件,而是重要的是了解哪些函数使用哪些结构。通过仅添加必要组件,可以减小项目大小并潜在地减少分析范围。这种方法还允许更好地理解正在使用的函数并提供更专注和流畅的开发过程。解决函数定义的第一件事是从上面链接中列出的ntdll.dll头文件中提取函数原型。提取函数原型后,我将其复制如下所示defs.h文件中。
defs.h
cp .cmake-build-debugdll_poc.dll C:Temp
3
如果您正在跟随进行操作,则可以看到我们需要添加哪些结构以支持Native API函数定义,如下图所示。
您可以在IDE中看到诸如PUNICODE_STRING或PIO_STATUS_BLOCK等项目未被解析,因此这意味着我们需要在structs.h文件中填充这些结构,如下所示。
structs.h
cp .cmake-build-debugdll_poc.dll C:Temp
4
现在我们需要为我们当前要使用的函数名称定义函数名称哈希。我们可以使用printf函数以及HASH宏来实现此目的,然后退出,如下所示。
然后,你需要将这些哈希定义在peb.h文件中,以便我们可以在即将使用的代码中使用它们。
cp .cmake-build-debugdll_poc.dll C:Temp
5
函数混淆
现在已经创建了函数哈希和定义,我将向你展示如何混淆函数指针,并在二进制文件中隐藏函数名称。以下示例中,在使用函数混淆时使用Native API函数有三个步骤。
步骤1 :确保有指向ntdll.dll基地址的指针,如果没有则使用get_ntdll()创建它。接着,创建函数名称的void指针,并使用get_proc_address_by_hash解析基地址和我们在前面步骤中创建的哈希,如下所示。
cp .cmake-build-debugdll_poc.dll C:Temp
6
上面的指针当前指向ntdll.dll的基地址和NtCreateFile的基地址。
步骤2 :将函数指针强制转换为其定义类型。
cp .cmake-build-debugdll_poc.dll C:Temp
7
上面的代码将新变量g_nt_create_file强制转换为NtCreateFile_t类型,因为我们在defs.h中定义了它。
步骤3 :像往常一样使用该函数,使用我们的新函数g_nt_create_file。
cp .cmake-build-debugdll_poc.dll C:Temp
8
上面的命令与实际的NtCreateFile函数相同,但使用了混淆的函数指针。现在我们已经涵盖了辅助函数以及如何应用函数混淆,我们可以开始处理新RDI加载器的第1步。
隐蔽步骤第1步
在此步骤中,从文件中读取DLL文件的二进制表示形式...
cp .cmake-build-debugdll_poc.dll C:Temp
9
上面的代码是我们基本加载器第1步的等效代码。此版本包括使用函数混淆和函数名称哈希的Native API函数。你可以看到代码从4行变成了近40行,这还不包括我们添加的结构和定义。
隐蔽步骤第2步
解析DLL文件的PE头以提取重要信息...
New-Item -ItemType Directory -Force -Path "srcc","srch","srcmasm"
0
此代码使用PIMAGE_DOS_HEADER和PIMAGE_NT_HEADERS结构查找DLL文件的大小。
隐蔽步骤第3步
在目标进程的地址空间中分配内存以容纳二进制文件...
New-Item -ItemType Directory -Force -Path "srcc","srch","srcmasm"
1
上面的代码为DLL分配虚拟内存并将DLL头复制到分配的内存中,然后迭代DLL的每个节,将它们的内容复制到分配的内存中。该代码计算分配的内存基址与DLL PE文件头中ImageBase地址之间的差异,并使用memcpy()进行内存复制操作。接下来,代码迭代DLL节,如其PE文件头所定义。
隐蔽步骤第4步
加载器对可执行文件执行任何必要的重定位修复。重定位...
New-Item -ItemType Directory -Force -Path "srcc","srch","srcmasm"
2
此代码负责对DLL映像执行其新基址的重定位。首先获取重定位数据目录并计算重定位表的RVA。然后遍历重定位表中的每个块,并根据重定位类型和偏移量计算要修补的地址,同时使用PBASE_RELOCATION_ENTRY和PBASE_RELOCATION_BLOCK结构。它读取要修补地址处的原始值,将其加上增量映像基址,,并将其写回到相同位置以更新地址到新位置。此过程确保DLL可以在其新基址上正确运行。
隐蔽步骤第5步
加载器执行任何必要的导入操作。导入是对函数的引用...
New-Item -ItemType Directory -Force -Path "srcc","srch","srcmasm"
3
上面的代码处理Windows PE文件中导入库的动态链接。它遍历PE文件中的导入描述符,使用LdrLoadDll函数加载导入库,使用LdrGetProcedureAddress函数检索导入函数的地址,并使用已解析的地址更新导入地址表(IAT)。该代码使用Windows特定的数据类型和结构(例如PIMAGE_IMPORT_DESCRIPTOR和UNICODE_STRING),并利用通过基于哈希查找获得的函数指针动态调用NTDLL库中的函数,NTDLL库提供管理进程、线程和内存等低层级函数。
隐蔽步骤第6步
加载器应用DLL映像中不同节的保护设置...
New-Item -ItemType Directory -Force -Path "srcc","srch","srcmasm"
4
上面的代码迭代DLL节,如其PE文件头所定义。对于每个节,它根据节的特性(例如可执行、可读和可写)计算用于加载节的内存区域的保护标志。然后,它调用通过函数指针获得的NtProtectVirtualMemory()函数,设置适当的内存保护以供节使用。
隐蔽步骤第7步
刷新指令缓存并检查TLS是否有条目可复制...
New-Item -ItemType Directory -Force -Path "srcc","srch","srcmasm"
5
上面的代码在处理IAT后使用NtFlushInstructionCache刷新指令缓存。接下来,代码使用IMAGE_DIRECTORY_ENTRY_TLS常量检查PE文件是否具有其可选头中的TLS(线程本地存储)目录条目。如果有,它将从PE文件中检索TLS目录,其中包含要在线程初始化期间执行的TLS回调函数(AddressOfCallBacks)列表。
隐蔽步骤第8步
最后,加载器将控制权转移到可执行文件的入口点...
New-Item -ItemType Directory -Force -Path "srcc","srch","srcmasm"
6
上面的代码计算DLL中DLLEntry函数的地址,使用适当的参数调用它,从ntdll模块检索并转换NtClose和NtFreeVirtualMemory函数的函数指针,并使用相关参数调用它们。这些操作可能涉及处理DLL初始化、关闭句柄或文件以及释放为DLL分配的虚拟内存。
隐蔽最终操作
将所有代码放在一起,包括所有新的函数定义和哈希,应该看起来像下面这样。
structs.h
New-Item -ItemType Directory -Force -Path "srcc","srch","srcmasm"
7
defs.h
New-Item -ItemType Directory -Force -Path "srcc","srch","srcmasm"
8
peb.h
New-Item -ItemType Directory -Force -Path "srcc","srch","srcmasm"
9
main.c
mkdir srcc srch srcmasm
0
CMakeLists.txt
cp .cmake-build-debugdll_poc.dll C:Temp
1
当你重写了以上所有代码后,应该能够构建和执行它,以获取用于验证它是否有效的消息框。
现在我们知道逻辑是有效的,让我们更仔细地查看二进制文件,首先从下面所示的Process Hacker开始。
正如你所看到的,不再有RWX内存区域,这正是我们希望通过将每个节更新为正确权限而实现的(如上面第6步所示)。接下来,让我们使用CFF Explorer打开二进制文件以检查导入表。
虽然导入表看起来已经改进了,但它还没有达到我们想要的水平。在继续编写我们的sRDI之前,我们必须确保加载器完全是位置无关的。
为了实现这一点,我们可以完全删除msvcrt.dll依赖项,通过修改CMakeLists.txt包括以下行来实现。
mkdir srcc srch srcmasm
2
CMakeLists.txt文件中的上述添加将删除C标准库,并将入口点从main()更改为start()。但是,通过删除msvcrt.dll依赖项将要求我们编写自己的strlen和memcpy函数来替换代码中使用的那些函数。在main.c文件顶部,我们将添加两个替换函数strlen和memcpy,如下所示。
mkdir srcc srch srcmasm
3
搜索并替换每个memcpy为mc函数,并替换每个strlen为sl函数。完成这些更改后,重新构建和运行二进制文件以确保其正常工作。验证你是否收到消息框后,尝试再次在CFF Explorer中打开文件。
Bingo!不需要导入即可运行,并且消息框仍然弹出,显示使用完全位置无关代码加载DLL。现在我们有了一个完全PIC RDI加载器,请看看如何将其转换为sRDI加载器,以便我们不必依赖于磁盘/网络DLL进行加载,并且我们可以直接从存根加载DLL。
隐蔽sRDI加载器
现在我们有了完全位置无关的RDI加载器,可以尝试更改start()中的前几个步骤,以实现从存根加载shellcode而不是打开和读取字节文件。首先要做的是将DLL文件转换为shellcode,由Hasherezade编写的pe_to_shellcode工具非常适合此PoC。
https://github.com/hasherezade/pe_to_shellcode?ref=blog.malicious.group
将dll_poc.dll转换为shellcode dll_poc.bin后,你可以使用xxd工具为你创建头文件。
mkdir srcc srch srcmasm
4
然后你只需将dll.h头文件添加到src/h/文件夹中即可。
头文件仅包括两个变量dll_bin和dll_bin_len,分别存储dll字节及其大小。有了新的dll.h头文件,我们可以返回到start()并更改步骤1以使用shellcode而不是磁盘上的DLL文件。使用dll.h头文件,我们的步骤1从35行代码变为几行代码,如下所示。
替换步骤1
在此步骤中,从头部存根读取DLL文件的二进制表示...
mkdir srcc srch srcmasm
5
为避免打印所有其他步骤,因为代码几乎相同,我将打印新的main.c,显示当使用sRDI而不是RDI注入时,我们从步骤1中删除了33行代码。还有一些微小的更改,但在查看以下代码时应该很明显。
mkdir srcc srch srcmasm
6
然后重新构建并执行加载器,可以看到我们的sRDI能够从头文件加载DLL,而不是从磁盘或网络拉取DLL。
好了,到此为止我展示代码示例。但是,这段代码仍然有很多优化可以使其更隐蔽和逃避检测,例如在头文件中加密DLL字节,并在执行之前解密...或向DLL本身添加一些睡眠混淆等等... 希望本文能够让你对如何将标准Windows API转换为Native代码、如何使用函数混淆、函数名称哈希以及如何反射式地将DLL加载到内存中有一个相当好的理解。
翻译来源: <https://blog.malicious.group/writing-your-own-rdi-srdi-loader-using-c- and-asm>
技术交流
知识星球
致力于红蓝对抗,实战攻防,星球不定时更新内外网攻防渗透技巧,以及最新学习研究成果等。常态化更新最新安全动态。专题更新奇技淫巧小Tips及实战案例。
涉及方向包括Web渗透、免杀绕过、内网攻防、代码审计、应急响应、云安全。星球中已发布 300+ 安全资源,针对网络安全成员的普遍水平,并为星友提供了教程、工具、POC&EXP以及各种学习笔记等等。()
交流群
关注公众号回复“加群”,添加Z2OBot好友,自动拉你加入Z2O安全攻防交流群(微信群)分享更多好东西。(QQ群可直接扫码添加)
关注我们
关注福利:
回复“app" 获取 app渗透和app抓包教程
回复“渗透字典" 获取 针对一些字典重新划分处理,收集了几个密码管理字典生成器用来扩展更多字典的仓库。
回复“书籍" 获取 网络安全相关经典书籍电子版pdf
回复“资料" 获取 网络安全、渗透测试相关资料文档
点个【 在看 】,你最好看
推荐站内搜索:最好用的开发软件、免费开源系统、渗透测试工具云盘下载、最新渗透测试资料、最新黑客工具下载……
还没有评论,来说两句吧...