动态内存补丁可以理解为在程序运行时动态地修改程序的内存,在某些时候某些应用程序会带壳运行,而此类程序的机器码只有在内存中被展开时才可以被修改,而想要修改此类应用程序动态补丁将是一个不错的选择,动态补丁的原理是通过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:指向一个空字符结束的字符串,指定将要执行的可执行文件的名称。如果
lpApplicationName
为NULL
,那么应该将可执行文件的名称包含在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)) {
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);
BYTE *recv_buffer = ReadMemory(pi, Address, 5); for (int x = 0; x < 5; x++) { printf("%x ", recv_buffer[x]); } printf("\n");
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
处的内存区域,观察替换效果,当替换成功后,其内存输出效果如下图所示;