6.3 应用动态内存补丁

动态内存补丁可以理解为在程序运行时动态地修改程序的内存,在某些时候某些应用程序会带壳运行,而此类程序的机器码只有在内存中被展开时才可以被修改,而想要修改此类应用程序动态补丁将是一个不错的选择,动态补丁的原理是通过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处的内存区域,观察替换效果,当替换成功后,其内存输出效果如下图所示;