心想随性

  • 首页
  • 关于我
  • 友链
Gality
小菜鸡的垃圾桶
  1. 首页
  2. 二进制
  3. 正文

MemoryModule项目分析(一)

2021年3月24日 46点热度 0人点赞 0条评论

目录

  • 概览
  • 主项目分析
  • 番外:
    • #pragma warning用法
    • extern "C"用法

概览

这篇文章接上一篇文章教程:《从内存中加载DLL》,来分析下该项目的代码,项目目录如下

先介绍一下,封装了加载函数及相关头文件的是MemoryModule.c和MemoryModule.h中,而对于怎么使用这些封装好的函数来从内存中加载dll,作者写了一个例子,在example文件夹中

其中DllLoader通过使用MemoryModule中的函数来完成从内存中加载Dll的一个Demo,至于加载的Dll,就位于SampleDll中,也就是SampleDLL.cpp,只是导出了一个很简单的求和函数:

// example/SampleDLL/SampleDLL.cpp

#include "SampleDLL.h"

extern "C" {

SAMPLEDLL_API int addNumbers(int a, int b)
{
    return a + b;
}

}

所以我们主要来看DllLoader的代码,学习下MemoryModule是怎么使用的,同时对比下正常的使用dll的方式和使用MemoryModule这个项目有什么区别

主项目分析

首先是一些导入和定义:

#include <assert.h>
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
#include <malloc.h>

#include "../../MemoryModule.h"

typedef int (*addNumberProc)(int, int);

#define DLL_FILE TEXT("..\\SampleDLL\\SampleDLL.dll")

能说的不多,有一点稍微说下:

typedef int (*addNumberProc)(int, int);

这一行定义了一个addNumberProc类型,这是一个接受两个整型参数,返回值为整型的函数指针类型

接着我们先看一个正常的 从磁盘上加载dll的方式:

void LoadFromFile(void)
{
    addNumberProc addNumber;
    HRSRC resourceInfo;
    DWORD resourceSize;
    LPVOID resourceData;
    TCHAR buffer[100];

    HINSTANCE handle = LoadLibrary(DLL_FILE);
    if (handle == NULL)
        return;

    addNumber = (addNumberProc)GetProcAddress(handle, "addNumbers");
    _tprintf(_T("From file: %d\n"), addNumber(1, 2));

    resourceInfo = FindResource(handle, MAKEINTRESOURCE(VS_VERSION_INFO), RT_VERSION);
    _tprintf(_T("FindResource returned 0x%p\n"), resourceInfo);

    resourceSize = SizeofResource(handle, resourceInfo);
    resourceData = LoadResource(handle, resourceInfo);
    _tprintf(_T("Resource data: %ld bytes at 0x%p\n"), resourceSize, resourceData);

    LoadString(handle, 1, buffer, sizeof(buffer));
    _tprintf(_T("String1: %s\n"), buffer);

    LoadString(handle, 20, buffer, sizeof(buffer));
    _tprintf(_T("String2: %s\n"), buffer);

    FreeLibrary(handle);
}

如果我们使用正常的方式加载dll,来使用他的导出函数,常规的操作如下:

  1. LoadLibrary 装载dll
  2. GetProcAddress 获得函数地址(这里就可以直接使用了)
  3. FindResource 找到指定资源块
  4. SizeofResource 和 LoadResource 加载资源
  5. LoadString 加载字符串

最后用FreeLibrary将加载进来的dll释放,完成整个的dll从装载到使用再到卸载的全过程. 由于是使用的windowsAPI,所以像是Dll的装载时的展开等都是由系统自动完成的,但用这种方法加载的Dll向系统注册,比较容易暴露我们的行踪,所以我们可以利用下面的方式,手动完成系统完成的所有操作,手动将Dll加载到项目中, 这种手动解析Dll并加载手法就叫做反射

整个DllLoader.c中的主函数只有短短几行:

int main()
{
    LoadFromFile();
    printf("\n\n");
    LoadFromMemory();
    printf("\n\n");
    TestCustomAllocAndFree();
    return 0;
}

LoadFromFile刚刚说过了, 是正常的加载dll的方式, 我们接着看LoadFromMemory函数,如果我们想利用反射的方式从内存中加载Dll,就需要自己实现自己的LoadFromMemory函数

void LoadFromMemory(void)
{
    void *data;
    size_t size;
    HMEMORYMODULE handle;
    addNumberProc addNumber;
    HMEMORYRSRC resourceInfo;
    DWORD resourceSize;
    LPVOID resourceData;
    TCHAR buffer[100];

    data = ReadLibrary(&size);
    if (data == NULL)
    {
        return;
    }

    handle = MemoryLoadLibrary(data, size);
    if (handle == NULL)
    {
        _tprintf(_T("Can't load library from memory.\n"));
        goto exit;
    }

    addNumber = (addNumberProc)MemoryGetProcAddress(handle, "addNumbers");
    _tprintf(_T("From memory: %d\n"), addNumber(1, 2));

    resourceInfo = MemoryFindResource(handle, MAKEINTRESOURCE(VS_VERSION_INFO), RT_VERSION);
    _tprintf(_T("MemoryFindResource returned 0x%p\n"), resourceInfo);

    resourceSize = MemorySizeofResource(handle, resourceInfo);
    resourceData = MemoryLoadResource(handle, resourceInfo);
    _tprintf(_T("Memory resource data: %ld bytes at 0x%p\n"), resourceSize, resourceData);

    MemoryLoadString(handle, 1, buffer, sizeof(buffer));
    _tprintf(_T("String1: %s\n"), buffer);

    MemoryLoadString(handle, 20, buffer, sizeof(buffer));
    _tprintf(_T("String2: %s\n"), buffer);

    MemoryFreeLibrary(handle);

exit:
    free(data);
}

先是data = ReadLibrary(&size); 我们看该函数的定义:

void* ReadLibrary(size_t* pSize) {
    size_t read;
    void* result;
    FILE* fp;

    fp = _tfopen(DLL_FILE, _T("rb"));  //打开Dll文件
    if (fp == NULL)
    {
        _tprintf(_T("Can't open DLL file \"%s\"."), DLL_FILE);
        return NULL;
    }

    fseek(fp, 0, SEEK_END);  //设置文件指针到文件末尾
    *pSize = static_cast<size_t>(ftell(fp)); //获取指针位置(相当于是获取了文件大小)
    if (*pSize == 0)
    {
        fclose(fp);
        return NULL;
    }

    result = (unsigned char *)malloc(*pSize); //开辟文件大小的空间
    if (result == NULL)
    {
        return NULL;
    }

    fseek(fp, 0, SEEK_SET); //重新设置文件指针到文件开头
    read = fread(result, 1, *pSize, fp); //将文件内容逐字节的读入到刚刚开辟的内存中
    fclose(fp); //关闭文件指针
    if (read != *pSize) //如果没有完全读入到内存
    {
        free(result);
        return NULL;
    }

    return result; //返回内存的地址
}

其中*pSize = static_cast<size_t>(ftell(fp));是C++新引入的一种写法, static_cast是一个关键字,用于显示类型转换, 表示该转换为良性转换,一般不会导致意外发生,风险很低。用法为:

xxx_cast<newType>(data)

同类的关键字说明如下:

关键字说明
static_cast用于良性转换,一般不会导致意外发生,风险很低。
const_cast用于 const 与非 const、volatile 与非 volatile 之间的转换。
reinterpret_cast高度危险的转换,这种转换仅仅是对二进制位的重新解释,不会借助已有的转换规则对数据进行调整,但是可以实现最灵活的 C++ 类型转换。
dynamic_cast借助 RTTI,用于类型安全的向下转型(Downcasting)。

ReadLibrary函数只是简单的完成了将文件复制到一块内存中的操作,没什么可说的,接着逐步来到了重点MemoryLoadLibrary函数,该函数位于MemoryModule.c中,函数定义如下:

HMEMORYMODULE MemoryLoadLibrary(const void *data, size_t size)
{
    return MemoryLoadLibraryEx(data, size, MemoryDefaultAlloc, MemoryDefaultFree, MemoryDefaultLoadLibrary, MemoryDefaultGetProcAddress, MemoryDefaultFreeLibrary, NULL);
}

而MemoryLoadLibraryEx则是一个关键的函数, 比较长,我们一段一段看, 首先, 是参数:

HMEMORYMODULE MemoryLoadLibraryEx(const void *data, size_t size,
    CustomAllocFunc allocMemory,
    CustomFreeFunc freeMemory,
    CustomLoadLibraryFunc loadLibrary,
    CustomGetProcAddressFunc getProcAddress,
    CustomFreeLibraryFunc freeLibrary,
    void *userdata)

其中,data和size是调用者提供的, 刚刚分析过, 分别代表被复制进内存中的Dll的地址起始指针 和 该块地址的大小(文件的大小), 而后传递的是自定类型的函数指针,定义分别如下:

//MemoryModule.h

typedef LPVOID (*CustomAllocFunc)(LPVOID, SIZE_T, DWORD, DWORD, void*);
typedef BOOL (*CustomFreeFunc)(LPVOID, SIZE_T, DWORD, void*);
typedef HCUSTOMMODULE (*CustomLoadLibraryFunc)(LPCSTR, void *);
typedef FARPROC (*CustomGetProcAddressFunc)(HCUSTOMMODULE, LPCSTR, void *);
typedef void (*CustomFreeLibraryFunc)(HCUSTOMMODULE, void *);

而作为参数传递的,则是定义在MemoryModule.h中的几个函数:

其具体定义在MemoryModule.c中,注释也写得很清楚,默认的函数实现是直接调用系统API的:

把这几个函数作为参数传递进MemoryLoadLibrary,主要是为了修补,先接着看:

#ifdef _WIN64 //如果是Win64,则定义blockedMemory 
    POINTER_LIST *blockedMemory = NULL;
#endif

    if (!CheckSize(size, sizeof(IMAGE_DOS_HEADER))) { // 确保文件大小大于IMAGE_DOS_HEADER得长度
        return NULL;
    }
    dos_header = (PIMAGE_DOS_HEADER)data; //dos_header 指向data首地址,也就是PIMAGE_DOS_HEADER结构得首地址
    if (dos_header->e_magic != IMAGE_DOS_SIGNATURE) { //根据dos_header得e_magic字段判断文件类型,应该为0x5A4D, 也就是"MZ"
        SetLastError(ERROR_BAD_EXE_FORMAT);
        return NULL;
    }

    if (!CheckSize(size, dos_header->e_lfanew + sizeof(IMAGE_NT_HEADERS))) { //确保文件大小大于DOS头+PE头的大小(dos_header->e_lfanew存有PE头相对于基址的偏移地址
        return NULL;
    }
    old_header = (PIMAGE_NT_HEADERS)&((const unsigned char *)(data))[dos_header->e_lfanew]; //取PE头地址放入old_header变量
    if (old_header->Signature != IMAGE_NT_SIGNATURE) { // 判断PE头的Signature,应为0x00004550,也就是"PE00"
        SetLastError(ERROR_BAD_EXE_FORMAT);
        return NULL;
    }

    if (old_header->FileHeader.Machine != HOST_MACHINE) { //判断PE头中的FileHeader项的Machine字节是否符合当前的机器位数(该项指出dll可以运行的位数 32位/64位)
        SetLastError(ERROR_BAD_EXE_FORMAT);
        return NULL;
    }

    if (old_header->OptionalHeader.SectionAlignment & 1) { 判断OptionalHeader的SectionAlignment项是否是2的倍数(该项标示PE文件内区块的对齐大小,一般为0x200或0x1000)
        // Only support section alignments that are a multiple of 2
        SetLastError(ERROR_BAD_EXE_FORMAT);
        return NULL;
    }

上面的代码中有两点需要说的:

  1. old_header = (PIMAGE_NT_HEADERS)&((const unsigned char *)(data))[dos_header->e_lfanew]
    这里使用了数组的方式来取值,由于[]的优先级比&高,所以这里是先把data转换成const unsigned char *的指针,起始也就是一个const unsigned char 的数组,这个dos_header->e_lfanew中存的就是PE头相对于dos头的偏移,所以直接[dos_header->e_lfanew], 相当于是从开始往后取了偏移个字节数的地址的值,想取地址还需要加一步&,此时指针就指向了PE头,转换成相应的类型即可
  2. 关于HOST_MACHINE ,该宏定义被写在了开头:
#ifdef _WIN64
#define HOST_MACHINE IMAGE_FILE_MACHINE_AMD64
#else
#define HOST_MACHINE IMAGE_FILE_MACHINE_I386
#endif

接着就是

section = IMAGE_FIRST_SECTION(old_header);

先看IMAGE_FIRST_SECTION这个函数,其实这是一个宏定义,该宏被定义再winnt.h中,如下:


#define IMAGE_FIRST_SECTION( ntheader ) ((PIMAGE_SECTION_HEADER)        \
    ((ULONG_PTR)(ntheader) +                                            \
     FIELD_OFFSET( IMAGE_NT_HEADERS, OptionalHeader ) +                 \
     ((ntheader))->FileHeader.SizeOfOptionalHeader   \
    ))

FIELD_OFFSET也是个宏,用于将第二个参数相对于第一个参数的偏移求出,所以这个IMAGE_FIRST_SECTION得到的地址就是Dll的基地址+OptionalHeader到基地址的偏移+OptionalHeader的长度,即取到了OptionalHeader后紧跟着的一块区域,如下图的PE文件结构,其实也就是区块的地址,符合宏的名字映像第一个区块地址

这里面有个比较有意思的东西在于FIELD_OFFSET的定义:

#define FIELD_OFFSET(type, field)    ((LONG)(LONG_PTR)&(((type *)0)->field))

这里用到了(type *)0这种写法,这里是好像是在0地址处存储了一个type类型的结构体,然后取到了指定成员的地址,由于起始地址是0,所以自然取到的成员地址就等于该成员在结构体内的偏移,这种写法我也是第一次见,这里记录下

ok,言归正传,接着说:

optionalSectionSize = old_header->OptionalHeader.SectionAlignment;
for (i=0; i<old_header->FileHeader.NumberOfSections; i++, section++) { //NumberOfSections存储了所有区块的个数
    size_t endOfSection;
    if (section->SizeOfRawData == 0) {
        // Section without data in the DLL
        endOfSection = section->VirtualAddress + optionalSectionSize;
    } else {
        endOfSection = section->VirtualAddress + section->SizeOfRawData;
    }

    if (endOfSection > lastSectionEnd) {
        lastSectionEnd = endOfSection;
    }
}

optionalSectionSize取到了SectionAlignment,这里存放的是区块被装入内存时的大小,需要该值以用于后续的展开,然后遍历所有区块,找到区块大小等于0的区块,该区块标志着区块的结束,最终endOfSection指向的是最后一个区块的结尾的地址(这里的地址是RVA)。

GetNativeSystemInfo(&sysInfo);

用于获取系统信息,一般是用于判断系统位数,以执行不同的代码

alignedImageSize = AlignValueUp(old_header->OptionalHeader.SizeOfImage, sysInfo.dwPageSize); //dwPageSize指定页面的大小和页面保护和委托的颗粒,也就是被 VirtualAlloc 函数使用的页大小
    if (alignedImageSize != AlignValueUp(lastSectionEnd, sysInfo.dwPageSize)) { //判断文件大小是否等于最后一个区块前所有部分的大小(二者都取整后的)
        SetLastError(ERROR_BAD_EXE_FORMAT);
        return NULL;
    }

AlignValueUp是自定义的一个宏:

static inline size_t AlignValueUp(size_t value, size_t alignment) {
return (value + alignment - 1) & ~(alignment - 1);
}

这里取到了value的向上取整(加了一个页大小,然后末尾清零了)的值

至此位置,前期的一些准备工作已经完成了,接下来就要步入真正的内容了。

敬请期待下一篇~

番外:

看项目过程中遇到的一些知识

#pragma warning用法

#if _MSC_VER
// Disable warning about data -> function pointer conversion
#pragma warning(disable:4055)
 // C4244: conversion from 'uintptr_t' to 'DWORD', possible loss of data.
#pragma warning(error: 4244)
// C4267: conversion from 'size_t' to 'int', possible loss of data.
#pragma warning(error: 4267)

#define inline __inline
#endif

#ifdef _WIN64
#define HOST_MACHINE IMAGE_FILE_MACHINE_AMD64
#else
#define HOST_MACHINE IMAGE_FILE_MACHINE_I386
#endif

_MSC_VER是一个MSVC的内置宏,定义了编译器的版本,所以这段的意思就是,如果是用微软的C编译器编译该段程序,则设定#pragma warning等

这里涉及的#pragma warning的用法如下:

#pragma warning(disable:4055)//不显示4055号警告信息
#pragma warning(error:4244)//把4244号警告信息作为一个错误。

然后定义了inline,至于这里每一个错误号分别对应什么错误,注释已经给的很清楚了

extern "C"用法

#ifdef __cplusplus
extern "C" {
#endif
...
...
#ifdef __cplusplus
}
#endif

的方式来确保编译器不会对其中的函数名进行重命名,每一个函数的用法都有详细的注释

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: MemoryModule 反射Dll
最后更新:2021年3月24日

Gality

不困于心,不惑于行

点赞
< 上一篇

文章评论

取消回复

Gality

不困于心,不惑于行

分类目录
  • CTF
  • C语言
  • PHP
  • Python
  • 二进制
  • 取证
  • 安装/踩坑
  • 开发
  • 读书
  • 运维
最新 热点 随机
最新 热点 随机
MemoryModule项目分析(一) 从内存中加载Dll “人类是大尺度的地衣”——读《看不见的森林》有感 花式找回WordPress密码 Python写二进制文件 Python多线程
MemoryModule项目分析(一)
配置Pycharm远程开发 Python多线程 从内存中加载Dll 记一次Windows蓝屏的解决 Volatility使用指南 花式找回WordPress密码

COPYRIGHT © 2020 心想随性. ALL RIGHTS RESERVED.

THEME KRATOS MADE BY VTROIS