网络靶场实战-PE 自注入

news2024/11/27 8:33:08

默认的 Windows API 函数(LoadLibrary、LoadLibraryEx)只能加载文件系统中的外部库,无法直接从内存中加载 DLL,并且无法正确地加载 EXE。有时候,确实需要这种功能(例如,不想分发大量文件或者想增加反汇编的难度)。解决这个问题的常见方法是先将 DLL 写入临时文件,然后从临时文件中导入。当程序终止时,临时文件会被删除。另一种方式是直接将所需的 DLL/EXE 文件数据直接存储在内存中,通过模拟 Windows 映像加载程序的功能,完全从内存中加载,而不需要先存储到磁盘上。

PE 格式简介

为了模拟 Windows 映像加载程序的功能,首先需要了解 Windows 的可执行程序格式(PE 文件格式),由以下几部分组成:

  • DOS 头:包含用于早期 DOS 兼容性的信息,并标识该文件是否是有效的 PE 文件。

  • PE头:包括 PE 标识、文件头和可选头。

  1. PE标识:指示该文件是一个PE(Portable Executable,可移植可执行)文件。

  2. 文件头:包含关于文件本身的基本信息,如文件类型、目标平台、时间戳等。

  3. 可选头:包含更详细的文件信息,如内存布局、入口点地址、数据目录等。

  • 节表:描述了文件中各个节(sections)的属性和位置,每个节都包含特定类型的数据,如代码、数据、导入表等。

  • 节表数据:实际存储文件中的数据和代码,每个节都有自己的属性,如可执行性、可读性、可写性等。

这几部分共同构成了 Windows 可执行文件的结构。下图显示了 PE 结构的简图:

图片

这些组成部分对应的结构体大多可在头文件 winnt.h 中找到。

PE自注入流程

对 PE 文件格式有了一定的了解,现在来看一下 PE 自注入的流程,也就是模拟 Window 加载器的步骤。

  • 分配足够内存用来保存注入的 PE 文件。

  • 拷贝节表数据。

  • 进行基址重定位。

  • 填充导入地址表 (IAT)。

  • 设置节表数据对应内存区域的权限。

  • 转到入口点执行。

这是基本的流程,如果 PE 文件还包含 TLS 回调和异常处理程序,还需要对这些情况进行处理。

PE自注入实现流程

准备工作

获取载荷数据,这里直接采用从文件中读取载荷文件内容到内存中。编写 ReadFileContents 函数从文件中获取载荷数据。

// 从磁盘读取文件内容
BOOL ReadFileContents(LPCSTR cFileName, PBYTE* ppBuffer, PDWORD pdwFileSize)
{
    DWORD dwNumberOfBytesRead = 0;

    HANDLE hFile = CreateFileA(cFileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    DWORD dwFileSize = GetFileSize(hFile, 0);
    PBYTE pBufer = (PBYTE)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwFileSize);
    DWORD dwFileSize = ReadFile(hFile, pBufer, dwFileSize, &dwNumberOfBytesRead, NULL);
    CloseHandle(hFile);

    *ppBuffer = pBufer;
    *pdwFileSize = dwFileSize;
    return ((*ppBuffer != NULL) && (*pdwFileSize != 0x00)) ? TRUE : FALSE;
}

在获取载荷数据后,对载荷文件 (PE文件)进行解析,并创建一个结构体来保存解析后的 PE 相关结构。

typedef struct _PE_HDRS
{
    PBYTE pFileBuffer; // 指向载荷文件缓冲区的指针
    DWORD dwFileSize; // 载荷文件大小
    BOOL bIsDLLFile; // 是否为DLL文件

    PIMAGE_NT_HEADERS pImgNtHdrs; // 指向NT头的指针
    PIMAGE_SECTION_HEADER pImgSecHdr; // 指向节表的指针

    PIMAGE_DATA_DIRECTORY pEntryImportDataDir; // 指向导入数据目录的指针
    PIMAGE_DATA_DIRECTORY pEntryBaseRelocDataDir; // 指向重定位数据目录的指针
    PIMAGE_DATA_DIRECTORY pEntryTLSDataDir; // 指向TLS数据目录的指针
    PIMAGE_DATA_DIRECTORY pEntryExceptionDataDir; // 指向异常数据目录的指针
    PIMAGE_DATA_DIRECTORY pEntryExportDataDir; // 指向导出数据目录的指针
} PE_HDRS, *PPE_HDRS, *LPPE_HDRS;

InitPeStruct 函数负责解析载荷文件进而初始化 PE_HDRS 结构。

BOOL InitPeStruct(PPE_HDRS pPeHdrs, PBYTE pFileBuffer, DWORD dwFileSize)
{
    if (!pPeHdrs || !pFileBuffer || !dwFileSize)
        return FALSE;

    pPeHdrs->pFileBuffer = pFileBuffer;
    pPeHdrs->dwFileSize = dwFileSize;
    pPeHdrs->pImgNtHdrs = (PIMAGE_NT_HEADERS)(pFileBuffer + ((PIMAGE_DOS_HEADER)pFileBuffer)->e_lfanew);

    if (pPeHdrs->pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
        return FALSE;

    pPeHdrs->bIsDLLFile = (pPeHdrs->pImgNtHdrs->FileHeader.Characteristics & IMAGE_FILE_DLL) ? TRUE : FALSE;
    pPeHdrs->pImgSecHdr = IMAGE_FIRST_SECTION(pPeHdrs->pImgNtHdrs);
    pPeHdrs->pEntryImportDataDir = &pPeHdrs->pImgNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
    pPeHdrs->pEntryBaseRelocDataDir =
        &pPeHdrs->pImgNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
    pPeHdrs->pEntryTLSDataDir = &pPeHdrs->pImgNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS];
    pPeHdrs->pEntryExceptionDataDir =
        &pPeHdrs->pImgNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION];
    pPeHdrs->pEntryExportDataDir = &pPeHdrs->pImgNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];

    return TRUE;
}

分配内存

载荷文件所需的内存使用 VirtualAlloc 进行分配,Windows 提供了函数来保护这些内存块,从而可以为这块内存进行权限设置。OptionalHeader 结构中定义了 PE 文件所需的内存块大小,如果可能的话,必须在 ImageBase 指定的地址上保留该内存块,如果保留的内存与 ImageBase 中给出的地址不同,则需要进行基址重定位。

PBYTE pPeBaseAddress = NULL;

pPeBaseAddress = (PBYTE)VirtualAlloc((LPVOID)(pPeHdrs->pImgNtHdrs->OptionalHeader.ImageBase),
                                           pPeHdrs->pImgNtHdrs->OptionalHeader.SizeOfImage,
                                           MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);

拷贝节表数据

FileHeader 结构 (IMAGE_FILE_HEADER)中给出了节表的数量,通过循环将 PE 文件中的所有节表数据拷贝 (memcpy)到分配的内存中,每个节表数据从 IMAGE_SECTION_HEADER.PointerToRawData 开始复制,复制的大小为 IMAGE_SECTION_HEADER.SizeOfRawData,复制到分配内存基地址偏移为 IMAGE_SECTION_HEADER.VirtualAddress 处。

for (int i = 0; i < pPeHdrs->pImgNtHdrs->FileHeader.NumberOfSections; i++) {
    memcpy(
        (PVOID)(pPeBaseAddress + pPeHdrs->pImgSecHdr[i].VirtualAddress),
        (PVOID)((ULONG_PTR)pPeHdrs->pFileBuffer + pPeHdrs->pImgSecHdr[i].PointerToRawData),
        pPeHdrs->pImgSecHdr[i].SizeOfRawData
    );
}

这里不拷贝的 DOS 头和 PE 头的原因是防止杀软等安全产品将这个作为一个特征点。

下图显示了各节表复制到分配内存中的状态:

基址重定位

将 PE 节表数据拷贝到分配的内存后,下一步是进行基址重定位,当 PE 映像在加载时的地址与其默认基址 (IMAGE_OPTIONAL_HEADER.ImageBase)不同时,需要进行重定位以调整可执行映像中的硬编码地址。在大多数情况下,PE 被映射到与 IMAGE_OPTIONAL_HEADER.ImageBase 不同的地址,因此需要调整 PE 映像中特定的硬编码地址。在 PE 文件中,重定位表位于 ". reloc" 节。下图形象的描述了 PE 为什么要重定位,只要 PE 模块没有加载到默认基址,都会发生重定位。

图片

在重定位的过程中,需要找到需要修复的硬编码数据,计算公式如下:

修复结果 = 需要重定位的数据 + 当前的ImageBase - 以前的ImageBase;

重定位表是由多个重定位块组合而成的,每个块描述了一个 4KB (0x1000) 大小的内存页中,需要进行重定位的位置,每个重定位块都以 IMAGE_BASE_RELOCATION 结构开始,该结构定义如下:

typedef struct _IMAGE_BASE_RELOCATION {
    DWORD VirtualAddress;
    DWORD SizeOfBlock;
    // WORD TypeOffset[1];
} IMAGE_BASE_RELOCATION, *PIMAGE_BASE_RELOCATION;

块的剩余部分是一个重定位数据项的数组,这个数据项占 2 个字节 (16 bit),其中高 4 位表示该重定位数据项的类型,低12位表示重定位数据项偏移 (相对于当前重定位块的偏移),由低 12 位与 VirtualAddress 相加就是需要进行重定位数据项的 RVA 地址。这个重定位数据项可以用下述结构表示:

typedef struct _BASE_RELOCATION_ENTRY {
  WORD Offset : 12; // 低12位:大小为12Bit的重定位数据项偏移,Offset就是相对于VirtualAddress的一个偏移。
  WORD Type : 4; // 高4位:大小为4Bit的重定位数据项类型值,Type是一个类型,一般情况下是 3,说明需要重定位
} BASE_RELOCATION_ENTRY, * PBASE_RELOCATION_ENTRY;

重定位类型由如下几种,可在 winnt.h 头文件中找到,详细的描述信息可在官方文档中获取。

//
// Based relocation types.
//

#define IMAGE_REL_BASED_ABSOLUTE 0
#define IMAGE_REL_BASED_HIGH 1
#define IMAGE_REL_BASED_LOW 2
#define IMAGE_REL_BASED_HIGHLOW 3
#define IMAGE_REL_BASED_HIGHADJ 4
#define IMAGE_REL_BASED_MIPS_JMPADDR 5
#define IMAGE_REL_BASED_MIPS_JMPADDR16 9
#define IMAGE_REL_BASED_IA64_IMM64 9
#define IMAGE_REL_BASED_DIR64 10

为了执行重定位,需要遍历所有重定位块,获取所有重定位数据项,根据其类型和偏移,进行必要修复,PerformBaseReloc 函数实现了这个过程。

BOOL PerformBaseReloc(PIMAGE_DATA_DIRECTORY pEntryBaseRelocDataDir, ULONG_PTR pPeBaseAddress, ULONG_PTR pOldAddress)
{
  PIMAGE_BASE_RELOCATION pImgBaseRelocation = (PIMAGE_BASE_RELOCATION)(pPeBaseAddress + pEntryBaseRelocDataDir->VirtualAddress);
  ULONG_PTR uDeltaOffset = pPeBaseAddress - pOldAddress;
  PBASE_RELOCATION_ENTRY pEntry = NULL;

  // 遍历重定位块
  while (pImgBaseRelocation->VirtualAddress)
  {
    pEntry = (PBASE_RELOCATION_ENTRY)(pImgBaseRelocation + 1);

    while ((PBYTE) pEntry != (PBYTE) pImgBaseRelocation + pImgBaseRelocation->SizeOfBlock)
    {
      // 处理重定位数据项
      switch (pEntry->Type)
      {
      case IMAGE_REL_BASED_DIR64:
        *((ULONG_PTR*)(pPeBaseAddress + pImgBaseRelocation->VirtualAddress + pEntry->Offset)) += uDeltaOffset;
        break;
      case IMAGE_REL_BASED_HIGHLOW:
        *((DWORD*)(pPeBaseAddress + pImgBaseRelocation->VirtualAddress + pEntry->Offset)) += (DWORD) uDeltaOffset;
        break;
      case IMAGE_REL_BASED_HIGH:
        *((WORD*)(pPeBaseAddress + pImgBaseRelocation->VirtualAddress + pEntry->Offset)) += HIWORD (uDeltaOffset);
        break;
      case IMAGE_REL_BASED_LOW:
        *((WORD*)(pPeBaseAddress + pImgBaseRelocation->VirtualAddress + pEntry->Offset)) += LOWORD (uDeltaOffset);
        break;
      case IMAGE_REL_BASED_ABSOLUTE:
        break;
      default:
        return FALSE;
      }
      // 定位下一个重定位数据项
      pEntry++;
    }

    // 定位下一个重定位块
    pImgBaseRelocation = (PIMAGE_BASE_RELOCATION) pEntry;
  }

  return TRUE;
}

填充 IAT

Windows 映像加载程序在处理 PE 映像的时候,其中一个步骤就是,加载所需的 DLL,并从中获取导入函数的地址写入其导入地址表(IAT)中,这个填充的过程需要手动的实现。

PE 文件的导入表记录当前程序使用了哪些模块的哪些函数,如果使用了多个模块,相应的也会存在多张导入表,最终会以一个全 0 的导入表结构结尾。当 PE 文件没有加载到内存时,IAT 和 INT 中保存的通常序号或导入名称结构的 RVA,一旦程序被加载到内存中,Windows 映像加载程序就会为 IAT 填充函数的真实地址,填充的过程如下可以使用下面的图进行描述:

图片

填充 IAT 需要遍历导入表,这个过程需要使用两个循环来进行枚举,外层循环枚举所有的 DLL,内层循环枚举所导入的该 DLL 的所有函数名及函数地址。

BOOL FixIAT(PIMAGE_DATA_DIRECTORY pEntryImportDataDir, PBYTE pPeBaseAddress) {

  PIMAGE_IMPORT_DESCRIPTOR pImgImportDesc = NULL;

  // 外层循环枚举所有的 DLL
  for (SIZE_T i = 0; i < pEntryImportDataDir->Size; i += sizeof(IMAGE_IMPORT_DESCRIPTOR))
  {
    pImgImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)(pPeBaseAddress + pEntryImportDataDir->VirtualAddress + i);
    if (pImgImportDesc->OriginalFirstThunk == NULL && pImgImportDesc->FirstThunk == NULL)
      break;

    LPSTR cDllName = (LPSTR)(pPeBaseAddress + pImgImportDesc->Name);
    ULONG_PTR uOriginalFirstThunkRVA = pImgImportDesc->OriginalFirstThunk;
    ULONG_PTR uFirstThunkRVA = pImgImportDesc->FirstThunk;
    SIZE_T ImgThunkSize = 0;
    HMODULE hModule = NULL;

    hModule = LoadLibraryA(cDllName);

    // 内层循环枚举所导入的该 DLL 的所有函数名及函数地址
    while (TRUE)
    {
      PIMAGE_THUNK_DATA pOriginalFirstThunk = (PIMAGE_THUNK_DATA)(pPeBaseAddress + uOriginalFirstThunkRVA + ImgThunkSize);
      PIMAGE_THUNK_DATA pFirstThunk = (PIMAGE_THUNK_DATA)(pPeBaseAddress + uFirstThunkRVA + ImgThunkSize);
      PIMAGE_IMPORT_BY_NAME pImgImportByName = NULL;
      ULONG_PTR pFuncAddress = NULL;

      if (pOriginalFirstThunk->u1.Function == NULL && pFirstThunk->u1.Function == NULL) {
        break;
      }

      if (IMAGE_SNAP_BY_ORDINAL(pOriginalFirstThunk->u1.Ordinal)) {
        if (!(pFuncAddress = (ULONG_PTR)GetProcAddress(hModule, (LPCSTR)(IMAGE_ORDINAL(pOriginalFirstThunk->u1.Ordinal))))) {
          return FALSE;
        }
      }
      else {
        pImgImportByName = (PIMAGE_IMPORT_BY_NAME)(pPeBaseAddress + pOriginalFirstThunk->u1.AddressOfData);
        // 获取函数地址
        pFuncAddress = (ULONG_PTR)GetProcAddress(hModule, pImgImportByName->Name);
      
      }

      // 进行填充
      pFirstThunk->u1.Function = (ULONGLONG)pFuncAddress;

      // 定位下一个项
      ImgThunkSize += sizeof(IMAGE_THUNK_DATA);
    }
  }

  return TRUE;
}

设置节表数据对应内存区域的权限

在执行完重定位和填充了 IAT 之后,需要根据 IMAGE_SECTION_HEADER.Characteristics 字段设置正确的内存权限。

BOOL SetMemoryPermission(ULONG_PTR pPeBaseAddress, PIMAGE_NT_HEADERS pImgNtHdrs, PIMAGE_SECTION_HEADER pImgSecHdr)
{
  for (DWORD i = 0; i < pImgNtHdrs->FileHeader.NumberOfSections; i++)
  {
    DWORD dwProtection = 0;
    DWORD dwOldProtection = 0;

    if (!pImgSecHdr[i].SizeOfRawData || !pImgSecHdr[i].VirtualAddress)
      continue;

    if (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_WRITE)
      dwProtection = PAGE_WRITECOPY;

    if (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_READ)
      dwProtection = PAGE_READONLY;

    if ((pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_WRITE) && (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_READ))
      dwProtection = PAGE_READWRITE;

    if (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_EXECUTE)
      dwProtection = PAGE_EXECUTE;

    if ((pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_EXECUTE) && (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_WRITE))
      dwProtection = PAGE_EXECUTE_WRITECOPY;

    if ((pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_EXECUTE) && (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_READ))
      dwProtection = PAGE_EXECUTE_READ;

    if ((pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_EXECUTE) && (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_WRITE) && (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_READ))
      dwProtection = PAGE_EXECUTE_READWRITE;

    VirtualProtect((PVOID)(pPeBaseAddress + pImgSecHdr[i].VirtualAddress), pImgSecHdr[i].SizeOfRawData, dwProtection, &dwOldProtection);
  
  }

  return TRUE;
}

转到入口点执行

基本步骤完成后,如果载荷文件不存在异常目录和 TLS 目录,那么就只需要跳转到入口点执行了。如果需要注入执行的文件是 EXE 文件,通过以下代码来执行。

typedef BOOL(WINAPI* MAIN)();

MAIN pMain = (MAIN)EntryPoint;
pMain();

如果需要注入执行的文件是 DLL 文件,可按照如下方式运行。

typedef BOOL(WINAPI* DLLMAIN)(HINSTANCE, DWORD, LPVOID);

DLLMAIN pDllMain = (DLLMAIN)EntryPoint;
pDllMain((HINSTANCE)pPeBaseAddress, DLL_PROCESS_ATTACH, NULL);

到这里,能够将 PE 程序的加载执行了,但是如果目标 PE 程序存在异常表和 TLS 表,那么还需要对其进行处理,不然自注入的 PE 程序可能存在问题。

注册异常处理程序

PE 文件头可选映像头中数据目录表的第 4 个成员指向异常处理表,它保存在 PE 文件中,通常在".pdata"区段。其 RVA 指向的是一个 IMAGE_IA64_RUNTIME_FUNCTION_ENTRY 的结构体,其结构如下:

typedef struct _IMAGE_RUNTIME_FUNCTION_ENTRY {
  DWORD BeginAddress;
  DWORD EndAddress;
  union {
    DWORD UnwindInfoAddress;
    DWORD UnwindData;
  } DUMMYUNIONNAME;
} RUNTIME_FUNCTION, *PRUNTIME_FUNCTION, _IMAGE_RUNTIME_FUNCTION_ENTRY, *_PIMAGE_RUNTIME_FUNCTION_ENTRY;

在进行 PE 自注入的过程中,可以使用 RtlAddFunctionTable 这个 API 来向运行时函数表中添加函数表条目,具体过程如下:

BOOL RegisterExceptionFunctionTable(PPE_HDRS pPeHdrs, ULONG_PTR pPeBaseAddress)
{
    if (!pPeHdrs)
        return FALSE;

    if (pPeHdrs->pEntryExceptionDataDir->Size == 0)
        return FALSE;

    PIMAGE_RUNTIME_FUNCTION_ENTRY pImgRuntimeFuncEntry = (PIMAGE_RUNTIME_FUNCTION_ENTRY)(pPeBaseAddress + pPeHdrs->pEntryExceptionDataDir->VirtualAddress);
    RtlAddFunctionTable(pImgRuntimeFuncEntry, (pPeHdrs->pEntryExceptionDataDir->Size / sizeof(IMAGE_RUNTIME_FUNCTION_ENTRY)), pPeBaseAddress);

    return TRUE;
}

执行 TLS 回调

Thread Local Storage(TLS)是一种线程本地存储的机制。它用于为每个线程分配独立的内存空间,使得每个线程都可以独立地访问和修改自己的局部变量,而不会与其他线程的变量发生冲突。在多线程程序中,每个线程都有自己的栈、寄存器等资源,但是全局变量是共享的,可能会导致并发访问的竞争条件。为了解决这个问题,TLS 提供了一种线程专用的存储区域,使得每个线程都可以拥有自己的变量副本,互不干扰。

TLS 回调是指 PE 文件的 TLS 目录中指定的一组回调函数。这些回调在创建线程之前由 Windows 映像加载器执行,这意味着 TLS 回调可以在 PE 的入口点之前运行。TLS 回调用于在程序或线程的主要逻辑开始之前初始化特定于线程的资源或配置。TLS 回调机制也可以用于反调试技术。

如果需要自注入的 PE 载荷包含 TLS 回调,可通过检索 TLS 目录( IMAGE_TLS_DIRECTORY )在入口点之前执行它们。这个目录包含一个由 PIMAGE_TLS_CALLBACK 函数指针定义的回调函数数组。IMAGE_TLS_DIRECTORY 结构可在 winnt.h 头文件中找到,其中 IMAGE_TLS_DIRECTORY64 结构如下,它描述 64 位程序 TLS 的相关信息。

typedef struct _IMAGE_TLS_DIRECTORY64 {
    ULONGLONG StartAddressOfRawData;
    ULONGLONG EndAddressOfRawData;
    ULONGLONG AddressOfIndex; // PDWORD
    ULONGLONG AddressOfCallBacks; // PIMAGE_TLS_CALLBACK *;
    DWORD SizeOfZeroFill;
    union {
        DWORD Characteristics;
        struct {
            DWORD Reserved0 : 20;
            DWORD Alignment : 4;
            DWORD Reserved1 : 8;
        } DUMMYSTRUCTNAME;
    } DUMMYUNIONNAME;

} IMAGE_TLS_DIRECTORY64;

32 位程序 TLS 结构如下:

typedef struct _IMAGE_TLS_DIRECTORY32 {
    DWORD StartAddressOfRawData;
    DWORD EndAddressOfRawData;
    DWORD AddressOfIndex; // PDWORD
    DWORD AddressOfCallBacks; // PIMAGE_TLS_CALLBACK *
    DWORD SizeOfZeroFill;
    union {
        DWORD Characteristics;
        struct {
            DWORD Reserved0 : 20;
            DWORD Alignment : 4;
            DWORD Reserved1 : 8;
        } DUMMYSTRUCTNAME;
    } DUMMYUNIONNAME;

} IMAGE_TLS_DIRECTORY32;

通过 IMAGE_TLS_DIRECTORY.AddressOfCallBacks 成员,可以检索 PIMAGE_TLS_CALLBACK 函数指针的数组。PIMAGE_TLS_CALLBACK 类型的函数指针指向一个 TLS 回调函数,其函数原型为:

typedef VOID (NTAPI *PIMAGE_TLS_CALLBACK) (
    PVOID hModule,
    DWORD dwReason,
    PVOID pContext
);

TLS 回调函数的构造方式类似于 DLL 的 DllMain 函数,它接受一个 dwReason 参数,根据该参数决定在 TLS 回调函数内执行什么操作。下面是一个示例,演示了如何构造 TLS 回调函数:

VOID NTAPI MyTlsCallback(PVOID DllHandle, DWORD Reason, PVOID Reserved) {
    switch (Reason) {
        case DLL_PROCESS_ATTACH: // DLL加载时
            break;
        case DLL_THREAD_ATTACH: // 线程创建时
            break;
        case DLL_THREAD_DETACH: // 线程终止时
            break;
        case DLL_PROCESS_DETACH: // DLL卸载时
            break;
        default:
            // 其他情况下
            break;
    }
}

在执行 TLS 回调函数时,需要指定执行的原因,例如 DLL_PROCESS_ATTACH(DLL 加载时执行)等。但是,由于是自己写的加载器,在调用 TLS 回调函数之前,无法确定 dwReason 参数的值。

解决办法是使用一个固定的标志,但这限制了 TLS 回调函数的能力,但它仍然优于完全忽略 TLS 回调函数。下面的代码片段通过获取 TLS 表中的回调函数地址,并传递 DLL_PROCESS_ATTACH 标志来执行它们。

BOOL ExecuteTlsCallbacks(PPE_HDRS pPeHdrs, ULONG_PTR pPeBaseAddress)
{
    if (!pPeHdrs)
        return FALSE;

    if (pPeHdrs->pEntryTLSDataDir->Size == 0)
        return FALSE;

    PIMAGE_TLS_DIRECTORY pImgTlsDirectory = (PIMAGE_TLS_DIRECTORY)(pPeBaseAddress + pPeHdrs->pEntryTLSDataDir->VirtualAddress);
    PIMAGE_TLS_CALLBACK* pImgTlsCallback = (PIMAGE_TLS_CALLBACK*)(pImgTlsDirectory->AddressOfCallBacks);
    CONTEXT ctx = {0};

    // 指向TLS回调,传递固定标志 DLL_PROCESS_ATTACH
    for (int i = 0; pImgTlsCallback[i] != NULL; i++) {
        pImgTlsCallback[i]((LPVOID)pPeBaseAddress, DLL_PROCESS_ATTACH, &ctx);
    }
}

还有一种需求就是不仅仅需要执行入口点代码,如果针对一个 DLL,它通常存在导出函数,有时候需要这个自注入的加载器拥有能够执行该导出函数的能力。这可通过遍历 PE 载荷的导出表来获取该导出函数的地址,下面的代码片段通过解析 PE 载荷的导出表来获取载荷导出函数的地址。

PVOID GetExportedFunctionAddress(PIMAGE_DATA_DIRECTORY pEntryExportDataDir, ULONG_PTR pPeBaseAddress, LPCSTR cFunctionName)
{
    PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pPeBaseAddress + pEntryExportDataDir->VirtualAddress);
    PDWORD pENT = (PDWORD)(pPeBaseAddress + pImgExportDir->AddressOfNames);
    PDWORD pEAT = (PDWORD)(pPeBaseAddress + pImgExportDir->AddressOfFunctions);
    PWORD pEOT = (PWORD)(pPeBaseAddress + pImgExportDir->AddressOfNameOrdinals);

    for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++)
    {
        LPCSTR pFunctionName = (LPCSTR)(pPeBaseAddress + pENT[i]);
        PVOID pFunctionAddress = (PVOID)(pPeBaseAddress + pEAT[pEOT[i]]);

        if (strcmp(cFunctionName, pFunctionName) == 0)
        {
            return pFunctionAddress;
        }
    }

    return NULL;
}

可能有些情况待注入 PE 载荷执行需要命令行参数,这就需要自注入的加载能够用于 Patch 注入载荷命令行参数的能力,这可通过解析 PEB 结构中命令行相关字段来实现。

检测

自注入的 PE 载荷在运行时会暴露在内存中,可使用 Pesieve 或者 Moneta 等内存扫描工具针对被加载的 PE 载荷进行运行时检测。

总结

本文首先简单的介绍了 PE 文件结构,之后列出了 PE 自注入的流程,随后根据这个流程进行了具体的实现,并进行了简单的规避操作(擦除 PE 特征),最后提出了检测方式。

图片

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Redis入门到通关之解决Redis缓存一致性问题

文章目录 ☃️概述☃️数据库和缓存不一致采用什么方案☃️代码实现☃️其他 ☃️概述 由于我们的 缓存的数据源来自于数据库, 而数据库的 数据是会发生变化的, 因此,如果当数据库中 数据发生变化,而缓存却没有同步, 此时就会有 一致性问题存在, 其后果是: 用户使用缓存中的过…

Python 数据结构和算法实用指南(二)

原文&#xff1a;zh.annas-archive.org/md5/66ae3d5970b9b38c5ad770b42fec806d 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 第四章&#xff1a;列表和指针结构 我们已经在 Python 中讨论了列表&#xff0c;它们方便而强大。通常情况下&#xff0c;我们使用 Python…

【C语言__基础概念__复习篇8】

目录 前言 一、C语言是什么 二、C语言的发展历史 三、编译器的选择 3.1 编译和链接 3.2 编译器的对比 3.3 VS如何使用 四、main函数 五、关键字 六、字符和ASCII编码 七、字符串和\0 八、转义字符 九、注释 十、数据类型 10.1 数据类型的介绍 10.2 数据类型大小的计…

有哪些网站设计教程

网站设计教程是帮助人们学习如何设计和开发网站的资源&#xff0c;它们提供了从基础知识到高级技巧的全方位指导。无论您是初学者还是经验丰富的开发者&#xff0c;都可以从这些教程中获益。下面是一些广受欢迎的网站设计教程&#xff0c;它们涵盖了各种技术和工具&#xff1a;…

Linux之进程控制进程终止进程等待进程的程序替换替换函数实现简易shell

文章目录 一、进程创建1.1 fork的使用 二、进程终止2.1 终止是在做什么&#xff1f;2.2 终止的3种情况&&退出码的理解2.3 进程常见退出方法 三、进程等待3.1 为什么要进行进程等待&#xff1f;3.2 取子进程退出信息status3.3 宏WIFEXITED和WEXITSTATUS&#xff08;获取…

【C++题解】1345. 玫瑰花圃

问题&#xff1a;1345. 玫瑰花圃 类型&#xff1a;基本运算、小数运算 题目描述&#xff1a; 有一块nn&#xff08;n≥5&#xff0c;且 n 是奇数&#xff09;的红玫瑰花圃&#xff0c;由 nn 个小正方形花圃组成&#xff0c;现要求在花圃中最中间的一行、最中间的一列以及 4 个…

SpringBoot多数据源基于mybatis插件(三)

SpringBoot多数据源基于mybatis插件&#xff08;三&#xff09; 1.主要思路2.具体实现 1.主要思路 MyBatis的插件机制允许你在MyBatis的四大对象&#xff08;Executor、StatementHandler、ParameterHandler和ResultSetHandler&#xff09;的方法执行前后进行拦截&#xff0c;并…

考察自动化立体库应注意的几点

导语 大家好&#xff0c;我是智能仓储物流技术研习社的社长&#xff0c;老K。专注分享智能仓储物流技术、智能制造等内容。 整版PPT和更多学习资料&#xff0c;请球友到知识星球 【智能仓储物流技术研习社】自行下载 考察自动化立体仓库的关键因素&#xff1a; 仓库容量&#x…

linux 中ifconfig 无法使用

1、先看问题 2、搜索 ifconfig 命令&#xff0c;看下该命令在哪 yum search ifconfig 可以看到ifconfig命令在 net-tools.x86_64这个包里。 3、下面开始安装&#xff0c;执行下面的命令 yum install net-tools.x86_64 4、查看是否安装成功 ifconfig 看到上面的ip就说明可以用了…

光网络中的低偏SOA与无源波导单片集成

----翻译自Aref Rasoulzadeh Zali等人2021年撰写的文章 摘要 在光通信系统中&#xff0c;非常需要可以通过简单工艺与无源光路单片集成的低偏振相关半导体光放大器&#xff08;SOA&#xff09;。然而&#xff0c;尽管已经报道了几种SOA&#xff0c;但在InP平台中将偏振无关的体…

ROS学习笔记(12)AEB和TTC的实现

0.前提 在自动驾驶领域有许多关于驾驶安全的措施AEB和TTC就是为了驾驶安全而设计出来的。在这篇文章中我会讲解我对AEB和TTC算法的一些理解。本期ROS学习笔记同时也是ros竞速小车的学习笔记&#xff0c;我会将我的部分代码拿出来进行讲解&#xff0c;让大家更好的理解ttc和aeb…

html接入高德地图

1.申请key key申请地址&#xff1a;https://console.amap.com/dev/key/app 官方文档 https://lbs.amap.com/api/javascript-api-v2/summary 2.html接入示例 需要将YOUR_KEY替换成自己的key <!doctype html> <html> <head><meta charset"utf-…

未来计算机的发展趋势是什么?

未来计算机的发展趋势是多方面的,涵盖了硬件、软件、体系结构以及计算范式等多个层面。以下是一些预期的趋势: 1. 量子计算: 随着量子理论的不断成熟和技术的进步,量子计算机将可能解决传统计算机难以处理的问题,比如药物发现、材料科学、复杂系统模拟等领域。量子计算的…

VMWare Ubuntu压缩虚拟磁盘

VMWare中ubuntu会越用越大&#xff0c;直到占满预分配的空间 即使系统里没有那么多东西 命令清理 开机->open Terminal sudo vmware-toolbox-cmd disk shrink /关机-> 编辑虚拟机设置->硬盘->碎片整理&压缩 磁盘应用 开机->disk usage analyzer(应用) …

Java面试:算法常用面试题汇总

1.说一下什么是二分法&#xff1f;使用二分法时需要注意什么&#xff1f;如何用代码实现&#xff1f; 二分法查找&#xff08;Binary Search&#xff09;也称折半查找&#xff0c;是指当每次查询时&#xff0c;将数据分为前后两部分&#xff0c;再用中值和待搜索的值进行比较&…

格灵深瞳,实现核心能力高强度保护与灵活交付

格灵深瞳&#xff0c;AI领域的领先企业&#xff0c;借助泰雷兹圣天诺技术&#xff0c;实现核心能力高强度保护与灵活交付&#xff0c;引领行业风向&#xff0c;安策信息助力AI行业企业实现产品核心能力保护、销售模式创新以及软件产品的灵活交付。 格灵深瞳&#xff0c;AI领域的…

生成人工智能体:人类行为的交互式模拟论文与源码架构解析(2)——架构分析 - 核心思想环境搭建技术选型

4.架构分析 4.1.核心思想 超越一阶提示&#xff0c;通过增加静态知识库和信息检索方案或简单的总结方案来扩展语言模型。 将这些想法扩展到构建一个代理架构&#xff0c;该架构处理检索&#xff0c;其中过去的经验在每个时步动态更新&#xff0c;并混合与npc当前上下文和计划…

算法与数据结构要点速学——通用 DS/A 流程图

通用 DS/A 流程图 这是一个流程图&#xff0c;可以帮助您确定应该使用哪种数据结构或算法。请注意&#xff0c;此流程图非常笼统&#xff0c;因为不可能涵盖每个场景。 请注意&#xff0c;此流程图仅涵盖 LICC 中教授的方法&#xff0c;因此排除了像 Dijkstra 等更高级的算法。…

ruoyi单体+react+antdesign

基于ruoyi vue和Ruoyi-React实现的快速开发工具。 源码地址&#xff1a;GitHub - hebian1994/ruoyi-react-single: use ruoyi to generage java backend code and reacr front end code 前端&#xff1a;基于ant-design-pro 后端&#xff1a;单体springboot项目(非cloud)mysq…

Windows版PHP7.4.9解压直用(免安装-绿色-项目打包直接使用)

安装版和解压版 区别 安装版: 安装方便&#xff0c;下一步------下一步就OK了&#xff0c;但重装系统更换环境又要重新来一遍&#xff0c;会特别麻烦解压版&#xff08;推荐&#xff09;&#xff1a; 这种方式&#xff08;项目打包特别方便&#xff09;能更深了解mysql的配置&…