6.3 应用动态内存补丁

应用,动态内存,补丁 · 浏览次数 : 19

小编点评

**动态内存补丁** **原理:** * 动态加载一个进程。 * 暂停进程。 * 解码进程内存。 * 通过内存读写实现对特定内存区域的动态修改。 **步骤:** 1. 打开进程并暂停运行。 2. 读取进程内存数据。 3. 验证内存区域是否一致。 4. 写入修补文件。 5. 运行修补后的程序。 **特征码定位技术:** * 通过搜索特征码(0x85, 0xed, 0x57, 0x74, 0x07)定位内存区域。 **特征替换:** * 通过读取前五个字节,比较内存中是否一致。 * 如果一致,通过 WriteMemory 写入修补文件中的指令。 **结果:** * 替换成功后,内存输出效果将发生变化。 **示例:** 假设我们要替换内存中的 0x0402507 地址以 0x90, 0x90, 0x90, 0x90, 0x90。 **代码示例:** ```c #include #include #include // 函数声明 void OpenExeFile(char *filename, PROCESS_INFORMATION *pi); void ReadMemory(void *addr, int size); void CheckMemory(void *addr, void *cmp_code, int size); void WriteMemory(void *addr, int size); int main() { // 打开进程文件 OpenExeFile("d://lyshark.exe", &pi); // 读取内存数据 ReadMemory(pi.dwProcessId * 0x401000, 5); // 验证内存区域一致 CheckMemory(pi.dwProcessId * 0x401000, cmp_code, 5); // 写入修补文件 WriteMemory(pi.dwProcessId * 0x401000, 5); // 运行修补后的程序 ResumeThread(pi.hThread); // 关闭进程和文件 CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return 0; } ```

正文

动态内存补丁可以理解为在程序运行时动态地修改程序的内存,在某些时候某些应用程序会带壳运行,而此类程序的机器码只有在内存中被展开时才可以被修改,而想要修改此类应用程序动态补丁将是一个不错的选择,动态补丁的原理是通过CreateProcess函数传递CREATE_SUSPENDED将程序运行起来并暂停,此时程序会在内存中被解码,当程序被解码后我们则可以通过内存读写实现对特定区域的动态补丁。

当读者需要手动拉起一个进程时则可以使用OpenExeFile函数实现,该函数调用后会拉起一个进程,并默认暂停在程序入口处,返回一个PROCESS_INFORMATION结构信息;

// 打开进程并暂停运行
PROCESS_INFORMATION OpenExeFile(char *szFileName)
{
    STARTUPINFO si = { 0 };
    PROCESS_INFORMATION pi = { 0 };

    si.cb = sizeof(STARTUPINFO);
    si.wShowWindow = SW_SHOW;
    si.dwFlags = STARTF_USESHOWWINDOW;

    // 创建子线程并默认暂停
    BOOL bRet = CreateProcess(szFileName, 0, 0, 0, 0, CREATE_SUSPENDED, 0, 0, &si, &pi);
    if (bRet == FALSE)
    {
        exit(0);
    }
    ResumeThread(pi.hThread);
    return pi;
}

其中CreateProcess函数的一般格式:

BOOL WINAPI CreateProcess(
  LPCWSTR lpApplicationName,
  LPWSTR lpCommandLine,
  LPSECURITY_ATTRIBUTES lpProcessAttributes,
  LPSECURITY_ATTRIBUTES lpThreadAttributes,
  BOOL bInheritHandles,
  DWORD dwCreationFlags,
  LPVOID lpEnvironment,
  LPCWSTR lpCurrentDirectory,
  LPSTARTUPINFOW lpStartupInfo,
  LPPROCESS_INFORMATION lpProcessInformation
);

下面是函数的参数说明:

  • lpApplicationName:指向一个空字符结束的字符串,指定将要执行的可执行文件的名称。如果lpApplicationNameNULL,那么应该将可执行文件的名称包含在lpCommandLine所指向的字符串中。
  • lpCommandLine:指向一个空字符结束的字符串,该字符串包含了要执行的命令行和参数,用于指定要运行的可执行文件和要传递给该进程的命令行参数。
  • lpProcessAttributes:指向PROCESS_ATTRIBUTES结构体,用于指定新进程的安全描述符。
  • lpThreadAttributes:指向THREAD_ATTRIBUTES结构体,用于指定新进程的主线程的安全描述符。
  • bInheritHandles:一个布尔值,指定新进程是否继承了它的父进程的句柄。
  • dwCreationFlags:指定新进程的创建标志。一般情况下会指定为 0。
  • lpEnvironment:指向一个环境块,用于指定新进程的环境块。如果为NULL,则新进程将继承调用进程的环境块。
  • lpCurrentDirectory:指向一个空字符结束的字符串,该字符串指定新进程的当前工作目录。如果为NULL,则新进程将继承父进程的当前工作目录。
  • lpStartupInfo:指向STARTUPINFO结构体,该结构体指定了新进程的主窗口外观。
  • lpProcessInformation:指向PROCESS_INFORMATION结构体,该结构体返回了新进程的信息,例如新进程的进程标识符和主线程标识符等。

CreateProcess 函数返回一个布尔值,表示函数的调用是否成功。如果成功,则返回值为非零,否则返回值为零,并通过调用GetLastError函数获取错误代码。为了使得新进程与父进程独立运行,一般需要用到独立的进程空间和线程,这通常需要在创建新进程之前调用一些Windows系统API函数,如VirtualAlloc、CreateThread等。

接着来看封装过的三个内存读写函数,其中ReadMemory()用于读取进程内存数据,WriteMemory()用于写入内存数据,CheckMemory()则用于验证两个内存空间内的字节是否匹配。

// 读取指定的内存地址
BYTE * ReadMemory(PROCESS_INFORMATION pi, DWORD dwVAddress, int Size)
{
    BYTE bCode = 0;
    BYTE *buffer = new BYTE[Size];

    for (int x = 0; x < Size; x++)
    {
        ReadProcessMemory(pi.hProcess, (LPVOID)dwVAddress, (LPVOID)&bCode, sizeof(BYTE), 0);
        buffer[x] = bCode;
        dwVAddress++;
    }
    return buffer;
}

// 写入内存特征
BOOL WriteMemory(PROCESS_INFORMATION pi, DWORD dwVAddress, unsigned char *ShellCode, int Size)
{
    BYTE *Buff = new BYTE[Size];

    SuspendThread(pi.hThread);
    memset(Buff, *ShellCode, Size);
    VirtualProtectEx(pi.hProcess, (LPVOID)dwVAddress, Size, 0x40, 0);
    BOOL Ret = WriteProcessMemory(pi.hProcess, (LPVOID)dwVAddress, Buff, Size, 0);
    if (Ret != 0)
    {
        ResumeThread(pi.hThread);
        return TRUE;
    }
    return FALSE;
}

// 比较内存中前几个字节是否一致
BOOL CheckMemory(PROCESS_INFORMATION pi, DWORD dwVAddress, BYTE OldCode[], int Size)
{
    BYTE *Buff = new BYTE[Size];
    ReadProcessMemory(pi.hProcess, (LPVOID)dwVAddress, Buff, Size, 0);

    if (!memcmp(Buff, OldCode, Size))
    {
        /*
        for (int x = 0; x < Size; x++)
        {
            printf("内存地址: %x --> 对比地址: %x \n", Buff[x], OldCode[x]);
        }
        */
        return TRUE;
    }
    return FALSE;
}

接下来我们将通过使用特征码定位技术来实现对特定内存区域的定位并实现特征替换,首先我们搜索0x85, 0xed, 0x57, 0x74, 0x07这段特征值,并定位到0x0402507内存区域,如下图所示;

当定位到内存区域后,我们首先通过ReadMemory读取前五个字节的内存,并调用CheckMemory函数用于验证此片内存区域是否时我们需要修改的,如果验证一致则通过调用WriteMemory函数向该内存中写出替换一段0x90, 0x90, 0x90, 0x90, 0x90的指令,最后通过调用ResumeThread恢复线程运行,并以此实现动态内存补丁;

int main(int argc, char *argv[])
{
    // 动态加载进程
    PROCESS_INFORMATION pi = OpenExeFile("d://lyshark.exe");

    // 开始搜索特征码
    char ScanOpCode[5] = { 0x85, 0xed, 0x57, 0x74, 0x07 };

    // 依次传入开始地址,结束地址,特征码,以及特征码长度
    ULONG Address = ScanMemorySignatureCode(pi.dwProcessId, 0x401000, 0x47FFFF, ScanOpCode, 5);
    printf("[*] 找到内存地址 = 0x%x \n", Address);

    // 读取位于Address地址处的5条机器指令
    BYTE *recv_buffer = ReadMemory(pi, Address, 5);
    for (int x = 0; x < 5; x++)
    {
        printf("%x ", recv_buffer[x]);
    }
    printf("\n");

    // 比较Address内存中前5个字节是否一致
    BYTE cmp_code[] = { 0x85, 0xed, 0x57, 0x74, 0x07 };
    BOOL ret = CheckMemory(pi, Address, cmp_code, 5);
    if (ret == TRUE)
    {
        printf("[*] 内存一致,可以进行打补丁 \n");
    }
    else
    {
        printf("[-] 不一致 \n");
    }

    // 写入修补文件
    unsigned char set_buffer[] = { 0x90, 0x90, 0x90, 0x90, 0x90 };
    WriteMemory(pi, Address, set_buffer, 5);

    // 运行修补后的程序
    ResumeThread(pi.hThread);
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);

    system("pause");
    return 0;
}

当调用成功后,读者可自行跳转到0x0402507处的内存区域,观察替换效果,当替换成功后,其内存输出效果如下图所示;

本文作者: 王瑞
本文链接: https://www.lyshark.com/post/fe53d98c.html
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

与6.3 应用动态内存补丁相似的内容:

6.3 应用动态内存补丁

动态内存补丁可以理解为在程序运行时动态地修改程序的内存,在某些时候某些应用程序会带壳运行,而此类程序的机器码只有在内存中被展开时才可以被修改,而想要修改此类应用程序动态补丁将是一个不错的选择,动态补丁的原理是通过`CreateProcess`函数传递`CREATE_SUSPENDED`将程序运行起来并暂停,此时程序会在内存中被解码,当程序被解码后我们则可以通过内存读写实现对特定区域的动态补丁。

活字格性能优化技巧(1)——如何利用数据库主键提升访问性能

本文由葡萄城技术团队于博客园原创并首发转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。 大家都知道,活字格作为企业级低代码开发平台,拥有6大引擎,3大能力,能够高效落地企业级应用。在每年的应用大赛中也能看到很多格友利用活字格做了很多复杂的应用,例如2021年

加速鸿蒙生态共建,蚂蚁mPaaS助力鸿蒙原生应用开发创新

6月21日-23日,2024华为开发者大会(HDC 2024)如期举行。在22日的【鸿蒙生态伙伴SDK】分论坛中,正式发布了【鸿蒙生态伙伴SDK市场】,其中蚂蚁数科旗下移动开发平台mPaaS(以下简称:蚂蚁mPaaS)在论坛中提出,已正式推出鸿蒙原生Beta版能力集,覆盖手机、IoT等终端,企业、机

Java Solon v2.7.6 发布

Java “新的”应用开发框架。开放原子开源基金会,孵化项目。从零开始构建(非 java-ee 架构),有灵活的接口规范与开放生态。

大模型时代,如何快速开发AI应用

本文分享自华为云社区 《【云享问答】第3期:大模型时代,如何快速开发AI应用》,作者:华为云社区精选。 大模型快速普及应用的当下,AI浪潮汹涌而至,对于开发者来说,开发一款属于自己的AI应用并不是遥不可及。华为云AI生态技术专家、中科院计算所博士坐阵,从数据处理、算法开发、模型训练到部署,全方位拆解

[转帖]正则表达式及在Jmeter中的应用

目录 1.正则表达式 1.1 什么是正则表达式 1.2 为什么使用正则表达式 2.语法 2.1 普通字符 2.2 限定符 2.3 非打印字符 2.4 特殊字符 2.5 定位符 2.6 修饰符(标记) 2.7 选择 2.8 运算符优先级 3.常用正则表达式及在线工具 4.Jmeter之正则表达式提取器

软件设计模式系列之十九——中介者模式

@目录1 模式的定义2 举例说明3 结构4 实现步骤5 代码实现6 典型应用场景7 优缺点8 类似模式9 小结 1 模式的定义 中介者模式是一种行为型设计模式,它用于降低对象之间的直接通信,通过引入一个中介者对象来管理对象之间的交互。这种模式有助于减少对象之间的耦合性,使系统更加可维护和扩展。中介者

Redis set数据类型命令使用及应用场景使用总结

转载请注明出处: 目录 1.sadd 集合添加元素 2.srem移除元素 3.smembers 获取key的所有元素 4.scard 获取key的个数 5.sismember 判断member元素是否存在集合key中 6.srandmember key count 从集合key中随机选出count个

Dubbo3应用开发—协议(Dubbo协议、REST协议 、gRPC协议、Triple协议)

协议 协议简介 什么是协议 Client(Consumer端)与Server(Provider端)在传输数据时双方的约定。 Dubbo3中常见的协议 1.dubbo协议[前面文章中使用的都是dubbo协议] 2.rest协议 3.triple协议 4.grpc协议 5.thirft协议 6.webs

服务端应用多级缓存架构方案

## 一:场景 20w的QPS的场景下,服务端架构应如何设计? ## 二:常规解决方案 可使用分布式缓存来抗,比如redis集群,6主6从,主提供读写,从作为备,不提供读写服务。1台平均抗3w并发,还可以抗住,如果QPS达到100w,通过增加redis集群中的机器数量,可以扩展缓存的容量和并发读写能