ilovehim的文章

  • 修改指定进程PEB中路径和命令行信息实现进程伪装

    背景所谓的进程伪装,指的修改任意一个指定进程的信息,是它的信息在系统中的显示是另一个进程的信息,这样看来,指定的进程就像是被伪装的进程一样,因为进程信息相同,但实际上,它还是原来的进程,做着原来的进程操作。
    本文就是要介绍进程伪装的技术,实现在 32 位系统和 64 位系统上的进程伪装。现在,我就把实现过程和原理整理成文档,分享给大家。
    函数介绍NtQueryInformationProcess 函数
    [NtQueryInformationProcess可能在Windows的未来版本中更改或不可用]获取指定进程的信息。
    函数声明
    NTSTATUS WINAPI NtQueryInformationProcess( _In_ HANDLE ProcessHandle, _In_ PROCESSINFOCLASS ProcessInformationClass, _Out_ PVOID ProcessInformation, _In_ ULONG ProcessInformationLength, _Out_opt_ PULONG ReturnLength);
    参数

    ProcessHandle [in]要获取信息的进程的句柄。
    ProcessInformationClass [in]要获取的进程信息的类型。 该参数可以是PROCESSINFOCLASS枚举中的以下值之一:




    VALUE
    MEANING




    ProcessBasicInformation
    检索指向PEB结构的指针,该结构可用于确定是否正在调试指定的进程,以及系统用于标识指定进程的唯一值


    ProcessDebugPort
    获取作为进程调试器的端口号的DWORD_PTR值。 非零值表示该进程正在Ring3调试器的控制下运行


    ProcessWow64Information
    确定进程是否在WOW64环境中运行(WOW64是允许基于Win32的应用程序在64位Windows上运行的x86模拟器)


    ProcessImageFileName
    检索包含该进程的映像文件名称的UNICODE_STRING值


    ProcessBreakOnTermination
    检索指示进程是否被视为关键的ULONG值。注意可以在具有SP3的Windows XP中启动该值。 从Windows 8.1开始,应该使用IsProcessCritical


    ProcessSubsystemInformation
    检索指示进程的子系统类型的SUBSYSTEM_INFORMATION_TYPE值。 ProcessInformation参数指向的缓冲区应足够大以容纳单个SUBSYSTEM_INFORMATION_TYPE枚举




    ProcessInformation [out]指向由调用应用程序提供的缓冲区的指针,函数写入请求的信息。 所写信息的大小取决于ProcessInformationClass参数的数据类型:
    PROCESS_BASIC_INFORMATION当ProcessInformationClass参数为ProcessBasicInformation时,ProcessInformation参数指向的缓冲区应足够大,以容纳具有以下布局的单个PROCESS_BASIC_INFORMATION结构:
    typedef struct _PROCESS_BASIC_INFORMATION { PVOID Reserved1; PPEB PebBaseAddress; PVOID Reserved2[2]; ULONG_PTR UniqueProcessId; PVOID Reserved3;} PROCESS_BASIC_INFORMATION;
    UniqueProcessId成员指向该过程的系统唯一标识符。 最好使用GetProcessId函数来检索这些信息。PebBaseAddress成员指向PEB结构。该结构的其他成员保留供操作系统内部使用。
    ProcessInformationLength [in]ProcessInformation参数指向的缓冲区的大小(以字节为单位)。
    ReturnLength [out,optional]指向变量的指针,其中函数返回所请求信息的大小。 如果函数成功,这是由ProcessInformation参数指向缓冲区的信息的大小,但是如果缓冲区太小,这是成功接收信息所需的最小缓冲区大小。

    返回值

    该函数返回一个NTSTATUS成功或错误代码。NTSTATUS错误代码的形式和意义在DDK中提供的Ntstatus.h头文件中列出,并在DDK文档中介绍,内核模式驱动程序架构/设计指南/驱动程序编程技术/日志记录错误。
    注意

    NtQueryInformationProcess函数及其返回的结构在操作系统内部,并可能会从一个版本的Windows更改为另一个版本。 为了保持应用程序的兼容性,最好使用ProcessInformationClass参数描述中提到的公共函数。如果您使用NtQueryInformationProcess,请通过运行时动态链接访问该函数。 如果功能已被更改或从操作系统中删除,这将使您的代码有机会正常响应。 但签名变更可能无法检测。此功能没有关联的导入库。 您必须使用LoadLibrary和GetProcAddress函数动态链接到Ntdll.dll。

    实现原理进程伪装的原理不是很复杂,就是修改指定进程的进程环境块 PEB 中的进程路径以及命令行信息,即可达到进程伪装的效果。
    具体实现原理分析如下:

    首先,我们根据进程的 PID 号打开指定进程,并获取进程的句柄。
    然后,我们要从 ntdll.dll 中获取 NtQueryInformationProcess 函数的导出地址,因为 NtQueryInformationProcess 函数没有关联导入库,所以只能动态获取,这个函数是这个程序功能实现的关键步骤。
    接着,使用 NtQueryInformationProcess 函数获取指定进程的进程基本信息 PROCESS_BASIC_INFORMATION,并从中获取指定进程的进程环境块 PEB。
    然后,我们就可以根据进程环境块中的 ProcessParameters,获取指定进程的 RTL_USER_PROCESS_PARAMETERS 信息,因为PEB的路径信息、命令行信息存储在这个结构体中。
    最后,我们开始对指定进程 PEB 中路径信息、命令行信息进行更改,实现进程伪装。

    编码实现// 修改指定进程的进程环境块PEB中的路径和命令行信息, 实现进程伪装BOOL DisguiseProcess(DWORD dwProcessId, wchar_t *lpwszPath, wchar_t *lpwszCmd){ // 打开进程获取句柄 HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId); if (NULL == hProcess) { ShowError("OpenProcess"); return FALSE; } typedef_NtQueryInformationProcess NtQueryInformationProcess = NULL; PROCESS_BASIC_INFORMATION pbi = { 0 }; PEB peb = { 0 }; RTL_USER_PROCESS_PARAMETERS Param = { 0 }; USHORT usCmdLen = 0; USHORT usPathLen = 0; // 需要通过 LoadLibrary、GetProcessAddress 从 ntdll.dll 中获取地址 NtQueryInformationProcess = (typedef_NtQueryInformationProcess)::GetProcAddress( ::LoadLibrary("ntdll.dll"), "NtQueryInformationProcess"); if (NULL == NtQueryInformationProcess) { ShowError("GetProcAddress"); return FALSE; } // 获取指定进程的基本信息 NTSTATUS status = NtQueryInformationProcess(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL); if (!NT_SUCCESS(status)) { ShowError("NtQueryInformationProcess"); return FALSE; } /* 注意在读写其他进程的时候,注意要使用ReadProcessMemory/WriteProcessMemory进行操作, 每个指针指向的内容都需要获取,因为指针只能指向本进程的地址空间,必须要读取到本进程空间。 要不然一直提示位置访问错误! */ // 获取指定进程进本信息结构中的PebBaseAddress ::ReadProcessMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), NULL); // 获取指定进程环境块结构中的ProcessParameters, 注意指针指向的是指定进程空间中 ::ReadProcessMemory(hProcess, peb.ProcessParameters, &Param, sizeof(Param), NULL); // 修改指定进程环境块PEB中命令行信息, 注意指针指向的是指定进程空间中 usCmdLen = 2 + 2 * ::wcslen(lpwszCmd); ::WriteProcessMemory(hProcess, Param.CommandLine.Buffer, lpwszCmd, usCmdLen, NULL); ::WriteProcessMemory(hProcess, &Param.CommandLine.Length, &usCmdLen, sizeof(usCmdLen), NULL); // 修改指定进程环境块PEB中路径信息, 注意指针指向的是指定进程空间中 usPathLen = 2 + 2 * ::wcslen(lpwszPath); ::WriteProcessMemory(hProcess, Param.ImagePathName.Buffer, lpwszPath, usPathLen, NULL); ::WriteProcessMemory(hProcess, &Param.ImagePathName.Length, &usPathLen, sizeof(usPathLen), NULL); return TRUE;}
    程序测试我们运行一个测试程序 Demon Memory.exe,先使用 Process Explorer 查看它的进程信息:

    现在,我们运行我们的进程伪装程序,修改上述的测试程序 Demon Memory.exe 进程的PEB进程环境块信息,实现进程伪装。程序执行提示成功,我们再运行 Process Explorer 程序查看进程信息,发现测试程序 Demon Memory.exe 进程信息成功伪装了 explorer.exe 资源管理器进程。

    总结这个程序的原理不难理解,但是需要重点注意一个地方,这个地方我在开发这个程序的时候,也犯了相同的错误,就是:一定要区分指针指向的是指定进程的空间还是自己本程序的空间,对于指向其它进程空间,则一律使用 ReadProcessMemory 和 WriteProcessMemory 函数进行数据读写。
    同时,也要注意,如果程序运行在 64 位系统上,那么程序就要编译为 64 位程序;如果程序运行在 32 位系统上,则程序要编译为 32 位程序,否则程序不能伪装成功。
    参考参考自《Windows黑客编程技术详解》一书
    1  留言 2019-01-23 19:44:48
  • 编程实现硬盘型号序列号固件版本号检测

    背景硬盘是计算机文件主要存储的地方,它和我们的生活息息相关。硬盘也有不同的生产厂商,为了能够区别每一块硬盘,所以,硬盘本身就会有型号、序列号、固件版本号等一些列的标识。
    本文主要讲解的就是编程实现,获取计算机硬盘的型号、序列号以及固件版本号的信息。现在,我把实现的过程整理成文档,分享给大家。
    函数介绍DeviceIoControl 函数
    将控制代码直接发送到指定的设备驱动程序,使相应的设备执行相应的操作。
    函数声明
    BOOL WINAPI DeviceIoControl( _In_ HANDLE hDevice, _In_ DWORD dwIoControlCode, _In_opt_ LPVOID lpInBuffer, _In_ DWORD nInBufferSize, _Out_opt_ LPVOID lpOutBuffer, _In_ DWORD nOutBufferSize, _Out_opt_ LPDWORD lpBytesReturned, _Inout_opt_ LPOVERLAPPED lpOverlapped);
    参数

    hDevice [in]要执行操作的设备的句柄。设备通常是卷,目录,文件或流。要检索设备句柄,请使用CreateFile函数。有关详细信息,请参阅备注。dwIoControlCode [in]操作的控制代码。此值标识要执行的具体操作和要执行该操作的设备类型。有关控制代码的列表,请参阅备注。每个控制代码的文档提供了lpInBuffer,nInBufferSize,lpOutBuffer和nOutBufferSize参数的使用细节。lpInBuffer [in,optional]指向输入缓冲区的指针,其中包含执行操作所需的数据。此数据的格式取决于dwIoControlCode参数的值。如果dwIoControlCode指定不需要输入数据的操作,则此参数可以为NULL。nInBufferSize [in]输入缓冲区的大小(以字节为单位)。lpOutBuffer [out,optional]指向要接收操作返回的数据的输出缓冲区的指针。此数据的格式取决于dwIoControlCode参数的值。如果dwIoControlCode指定不返回数据的操作,则此参数可以为NULL。nOutBufferSize [in]输出缓冲区的大小(以字节为单位)。lpBytesReturned [out,optional]指向一个变量的指针,该变量接收存储在输出缓冲区中的数据大小(以字节为单位)。如果输出缓冲区太小,无法接收任何数据,则调用失败,GetLastError返回ERROR_INSUFFICIENT_BUFFER,lpBytesReturned为零。lpOverlapped [in,out,optional]指向OVERLAPPED结构的指针。
    返回值

    如果操作成功完成,则返回值不为零。如果操作失败或待处理,返回值为零。 要获取扩展错误信息,请调用GetLastError。

    实现原理编程实现硬盘信息的获取,主要是通过 DeviceIoControl 发送控制码与物理设备进行交互,获取硬盘信息数据。具体的实现步骤如下所示:

    我们使用 CreateFile 打开物理设备,获取物理设备的句柄,好为接下来与设备进行数据交互。要注意的是物理设备的字符串\\\\.\\PHYSICALDRIVE的写法,不要写错了。
    然后,我们调用 DeviceIoControl 函数,传递 SMART_GET_VERSION 控制码,获取物理设备的版本信息、设备的位掩码和功能掩码。
    接着,我们根据设备的位掩码判断该设备是设备是 ATAPI 驱动器还是 ATA 驱动器。若是 ATAPI 驱动器的话,则接下来使用读取 ATAPI 设备的命令 0xA1 进行下面的操作;若是 ATA 驱动器的话,则接下来使用读取 ATA 设备的命令 0xEC 进行下面的操作。
    继续构造输入参数SENDCMDINPARAMS,调用 DeviceIoControl 函数,传递 SMART_RCV_DRIVE_DATA 控制码,获取硬盘数据信息。
    然后,我们从输出信息中,获取硬盘序列号数据,数据下标为 20-39;硬盘固件版本数据,数据下标为 46-53;硬盘型号数据,数据下标为 54-93。并对数据进行处理后输出显示。
    最后,关闭设备句柄,释放资源。

    经过上面 6 个步骤,我们便可以成功获取并显示硬盘信息了。
    编码实现// 获取硬盘信息BOOL GetDiskInfo(int iDriver){ char szFilePath[MAX_PATH] = { 0 }; HANDLE hDisk = NULL; DWORD dwBytesReturned = 0; GETVERSIONINPARAMS gvopVersionParam = { 0 }; BOOL bRet = FALSE; unsigned int uiIDCmd = 0; SENDCMDINPARAMS InParams = { 0 }; unsigned int uiDrive = 0; BYTE outBuffer[1024] = { 0 }; int i = 0; // 打开硬盘设备 ::wsprintf(szFilePath, "\\\\.\\PHYSICALDRIVE%d", iDriver); hDisk = ::CreateFile(szFilePath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); if (INVALID_HANDLE_VALUE == hDisk) { ShowError("CreateFile"); return FALSE; } // 发送控制代码到指定设备驱动程序, 返回版本信息,功能掩码和设备的位掩码 bRet = ::DeviceIoControl(hDisk, SMART_GET_VERSION, NULL, 0, &gvopVersionParam, sizeof(gvopVersionParam), &dwBytesReturned, NULL); if (FALSE == bRet) { ShowError("DeviceIoControl"); return FALSE; } // 设备的位掩码bIDEDeviceMap位4置为1的话, 该设备是ATAPI驱动器,它是主通道上的主设备 if (0x10 & gvopVersionParam.bIDEDeviceMap) { // 读取ATAPI设备的命令 uiIDCmd = IDE_ATAPI_IDENTIFY; } else { // 读取ATA设备的命令 uiIDCmd = IDE_ATA_IDENTIFY; } // 设置输入 SENDCMDINPARAMS 输入参数 InParams.cBufferSize = IDENTIFY_BUFFER_SIZE; InParams.irDriveRegs.bFeaturesReg = 0; InParams.irDriveRegs.bSectorCountReg = 1; InParams.irDriveRegs.bSectorNumberReg = 1; InParams.irDriveRegs.bCylLowReg = 0; InParams.irDriveRegs.bCylHighReg = 0; InParams.irDriveRegs.bDriveHeadReg = (uiDrive & 1) ? 0xB0 : 0xA0; InParams.irDriveRegs.bCommandReg = uiIDCmd; InParams.bDriveNumber = uiDrive; // 发送控制代码到指定设备驱动程序, 获取硬盘数据信息 bRet = ::DeviceIoControl(hDisk, SMART_RCV_DRIVE_DATA, (LPVOID)(&InParams), sizeof(SENDCMDINPARAMS), (LPVOID)outBuffer, 1024, &dwBytesReturned, NULL); if (FALSE == bRet) { ShowError("DeviceIoControl"); return FALSE; } // 硬盘中的序列号, 型号, 固件版本号 的数据是前后两个字节内容是颠倒的, 所以要转换为正常的顺序 // 获取序列号, 下标20-39 printf("序列号:"); for (i = 20; i < 40; i = i + 2) { printf("%c%c", ((SENDCMDOUTPARAMS *)outBuffer)->bBuffer[i + 1], ((SENDCMDOUTPARAMS *)outBuffer)->bBuffer[i]); } printf("\n"); // 获取固件版本号, 下标46-53 printf("固件版本号:"); for (i = 46; i < 54; i = i + 2) { printf("%c%c", ((SENDCMDOUTPARAMS *)outBuffer)->bBuffer[i + 1], ((SENDCMDOUTPARAMS *)outBuffer)->bBuffer[i]); } printf("\n"); // 获取型号, 下标54-93 printf("型号:"); for (i = 54; i < 94; i = i + 2) { printf("%c%c", ((SENDCMDOUTPARAMS *)outBuffer)->bBuffer[i + 1], ((SENDCMDOUTPARAMS *)outBuffer)->bBuffer[i]); } printf("\n"); // 关闭设备 ::CloseHandle(hDisk); return TRUE;}
    程序测试我们直接以管理员权限运行程序,程序提示运行成功,并能正确读取计算机上硬盘的信息:

    总结这个程序需要注意下面两点:

    一是,程序需要管理员权限才能正常运行。因为我们使用 CreateFile 打开物理设备,这个操作就需要管理员权限或者管理员以上的权限运行,否则会因权限不足而操作失败,获取不到物理设备句柄。
    二是,当我们获取到硬盘数据的时候,并不能直接从中读取序列号、固件版本号、型号的信息,需要我们进行下转换。因为,硬盘存储的数据和我们正常读取的数据不一样,硬盘中的序列号、固件版本号、型号的数据前后两个字节内容是颠倒的,所以,显示输出的时候,我们要对它前后两字节颠倒输出。

    参考参考自《Windows黑客编程技术详解》一书
    1  留言 2018-12-03 19:45:00
  • 根据进程PID读写指定进程的内存数据

    背景如果对外挂有了解的同学,应该知道,修改进程内存应该是外挂入门学习的必修技术点。当然,不单单是外挂程序会修改进程内存数据,还有很多安全类软件也都会有修改进程内存数据的功能,方便分析人员进行分析。
    而且,Windows也提供了相应的进程内存读写的API函数 ReadProcessMemory 和 WriteProcessMemory,我们实现的程序,正是基于这两个函数实现的。
    现在,本文就对这个功能的实现过程进行整理,形成文档,分享给大家。
    函数介绍OpenProcess 函数
    打开现有的本地进程对象。
    函数声明
    HANDLE WINAPI OpenProcess( _In_ DWORD dwDesiredAccess, _In_ BOOL bInheritHandle, _In_ DWORD dwProcessId);
    参数

    dwDesiredAccess [in]访问进程对象。此访问权限针对进程的安全描述符进行检查。此参数可以是一个或多个进程访问权限。如果调用该函数的进程启用了SeDebugPrivilege权限,则无论安全描述符的内容如何,都会授予所请求的访问权限。bInheritHandle [in]若此值为TRUE,则此进程创建的进程将继承该句柄。否则,进程不会继承此句柄。dwProcessId [in]要打开的本地进程的标识符。如果指定的进程是系统进程(0x00000000),则该函数失败,最后一个错误代码为ERROR_INVALID_PARAMETER。如果指定的进程是空闲进程或CSRSS进程之一,则此功能将失败,并且最后一个错误代码为ERROR_ACCESS_DENIED,因为它们的访问限制会阻止用户级代码打开它们。如果您使用GetCurrentProcessId作为此函数的参数,请考虑使用GetCurrentProcess而不是OpenProcess,以提高性能。
    返回值

    如果函数成功,则返回值是指定进程的打开句柄。如果函数失败,返回值为NULL。 要获取扩展错误信息,请调用GetLastError。

    VirtualProtectEx 函数
    更改指定进程的虚拟地址空间中已提交页面的区域上的保护。
    函数声明
    BOOL WINAPI VirtualProtectEx( _In_ HANDLE hProcess, _In_ LPVOID lpAddress, _In_ SIZE_T dwSize, _In_ DWORD flNewProtect, _Out_ PDWORD lpflOldProtect);
    参数

    hProcess [in]要更改内存保护的进程的句柄。句柄必须具有PROCESS_VM_OPERATION权限。有关更多信息,请参阅流程安全和访问权限。lpAddress [in]指向要更改其访问保护属性的页面区域的基址的指针。指定区域中的所有页面必须在使用MEM_RESERVE调用VirtualAlloc或VirtualAllocEx函数时分配的相同保留区域内。这些页面不能跨越通过使用MEM_RESERVE单独调用VirtualAlloc或VirtualAllocEx分配的相邻保留区域。dwSize [in]其访问保护属性更改的区域的大小(以字节为单位)。受影响页面的区域包括从lpAddress参数到(lpAddress + dwSize)范围内包含一个或多个字节的所有页面。这意味着跨越页面边界的2字节范围会导致两个页面的保护属性被更改。flNewProtect [in]内存保护选项。该参数可以是存储器保护常数之一。对于映射视图,此值必须与视图映射时指定的访问保护兼容(请参阅MapViewOfFile,MapViewOfFileEx和MapViewOfFileExNuma)。lpflOldProtect [out]指向变量的指针,该变量接收指定的页面区域中的第一页的先前访问保护。如果此参数为NULL或不指向有效变量,则该函数将失败。
    返回值

    如果函数成功,则返回值不为零。如果函数失败,返回值为零。 要获取扩展错误信息,请调用GetLastError。

    ReadProcessMemory 函数
    在指定的进程中从内存区域读取数据。 要读取的整个区域必须可访问或操作失败。
    函数声明
    BOOL WINAPI ReadProcessMemory( _In_ HANDLE hProcess, _In_ LPCVOID lpBaseAddress, _Out_ LPVOID lpBuffer, _In_ SIZE_T nSize, _Out_ SIZE_T *lpNumberOfBytesRead);
    参数

    hProcess [in]具有正在读取的内存的进程的句柄。 句柄必须具有进程的PROCESS_VM_READ访问权限。lpBaseAddress [in]指向从中读取的指定进程中的基地址的指针。 在发生任何数据传输之前,系统将验证指定大小的基地址和内存中的所有数据都可访问以进行读取访问,如果不可访问,则该函数将失败。lpBuffer [out]指向从指定进程的地址空间接收内容的缓冲区的指针。nSize [in]要从指定进程读取的字节数。lpNumberOfBytesRead [out]指向一个变量的指针,该变量接收传输到指定缓冲区的字节数。 如果lpNumberOfBytesRead为NULL,则该参数将被忽略。
    返回值

    如果函数成功,则返回值不为零。如果函数失败,返回值为0(零)。 要获取扩展错误信息,请调用GetLastError。

    WriteProcessMemory 函数
    在指定的进程中将数据写入内存区域。 要写入的整个区域必须可访问或操作失败。
    函数声明
    BOOL WINAPI WriteProcessMemory( _In_ HANDLE hProcess, _In_ LPVOID lpBaseAddress, _In_ LPCVOID lpBuffer, _In_ SIZE_T nSize, _Out_ SIZE_T *lpNumberOfBytesWritten);
    参数

    hProcess [in]要修改的进程内存的句柄。 句柄必须具有PROCESS_VM_WRITE和PROCESS_VM_OPERATION访问进程。lpBaseAddress [in]指向写入数据的指定进程中的基地址的指针。 在数据传输发生之前,系统会验证指定大小的基地址和内存中的所有数据是否可以进行写入访问,如果不可访问,则该函数将失败。lpBuffer [in]指向缓冲区的指针,其中包含要写入指定进程的地址空间的数据。nSize [in]要写入指定进程的字节数。lpNumberOfBytesWritten [out]指向变量的指针,该变量接收传输到指定进程的字节数。 此参数是可选的。 如果lpNumberOfBytesWritten为NULL,则忽略该参数。
    返回值

    如果函数成功,则返回值不为零。如果函数失败,返回值为0(零)。 要获取扩展错误信息,请调用GetLastError。

    实现过程对进程内存数据的读取过程和写入过程基本上是一样的。
    对于进程数据读取的过程:

    首先,我们先要使用 OpenProcess 函数打开进程,并获取具有 PROCESS_ALL_ACCESS 权限的进程句柄
    然后,我们调用 VirtualProtectEx 更改将要读取内存的页面属性,将其页面属性更改为 PAGE_READWRITE 可读可写属性。因为并不是所有的页面属性都具有可读属性,所以要进行设置,以防万一
    接着,我们便可以调用 ReadProcessMemory 函数来读取指定内存的数据到缓冲区里
    然后,读取完毕之后,我们还要对内存页面属性进行还原,以防程序因为页面属性更改而出现不可预料的错误
    最后,我们关闭进程句柄

    这样,进程内存数据读取就完成了。对于进程内存的写过程,就基本上和读取相似,也是 5 个步骤:

    首先,我们先要使用 OpenProcess 函数打开进程,并获取具有 PROCESS_ALL_ACCESS 权限的进程句柄
    然后,我们调用 VirtualProtectEx 更改将要写入内存的页面属性,将其页面属性更改为 PAGE_READWRITE 可读可写属性。因为并不是所有的页面属性都具有可写属性,所以要进行设置,以防万一
    接着,我们便可以调用 WriteProcessMemory 函数来将指定缓冲区里的数据写入到内存里
    然后,写入完毕之后,我们还要对内存页面属性进行还原,以防程序因为页面属性更改而出现不可预料的错误
    最后,我们关闭进程句柄

    这样,读写操作就完成了。
    编码实现读取进程内存// 读内存BOOL ReadProcessMem(DWORD dwProcessId, PVOID pAddress, PVOID pReadBuf, DWORD dwReadBufferSize){ BOOL bRet = FALSE; DWORD dwRet = 0; // 根据PID, 打开进程获取进程句柄 HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId); if (NULL == hProcess) { ShowError("OpenProcess"); return FALSE; } // 更改页面保护属性 DWORD dwOldProtect = 0; bRet = ::VirtualProtectEx(hProcess, pAddress, dwReadBufferSize, PAGE_READWRITE, &dwOldProtect); if (FALSE == bRet) { ShowError("VirtualProtectEx"); return FALSE; } // 读取内存数据 bRet = ::ReadProcessMemory(hProcess, pAddress, pReadBuf, dwReadBufferSize, &dwRet); if (FALSE == bRet) { ShowError("ReadProcessMemory"); return FALSE; } // 还原页面保护属性 bRet = ::VirtualProtectEx(hProcess, pAddress, dwReadBufferSize, dwOldProtect, &dwOldProtect); if (FALSE == bRet) { ShowError("VirtualProtectEx"); return FALSE; } // 关闭进程句柄 ::CloseHandle(hProcess); return TRUE;}
    写入进程内存// 写内存BOOL WriteProcessMem(DWORD dwProcessId, PVOID pAddress, PVOID pWriteBuf, DWORD dwWriteBufferSize){ BOOL bRet = FALSE; DWORD dwRet = 0; // 根据PID, 打开进程获取进程句柄 HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId); if (NULL == hProcess) { ShowError("OpenProcess"); return FALSE; } // 更改页面保护属性 DWORD dwOldProtect = 0; bRet = ::VirtualProtectEx(hProcess, pAddress, dwWriteBufferSize, PAGE_READWRITE, &dwOldProtect); if (FALSE == bRet) { ShowError("VirtualProtectEx"); return FALSE; } // 写入内存数据 bRet = ::WriteProcessMemory(hProcess, pAddress, pWriteBuf, dwWriteBufferSize, &dwRet); if (FALSE == bRet) { ShowError("ReadProcessMemory"); return FALSE; } // 还原页面保护属性 bRet = ::VirtualProtectEx(hProcess, pAddress, dwWriteBufferSize, dwOldProtect, &dwOldProtect); if (FALSE == bRet) { ShowError("VirtualProtectEx"); return FALSE; } // 关闭进程句柄 ::CloseHandle(hProcess); return TRUE;}
    程序测试首先,我们运行程序,读取 520.exe 进程中 0x400000 地址处的 4 字节数据,成功读取。然后,我们对 520.exe 进程中 0x400000 地址处的 4 字节数据进行更改,更改成功后,在对数据读取一遍,发现数据成功更改。

    总结在读写进程内存数据之前,我们最好使用 VirtualProtect 函数来修改内存对应的页属性保护,给它设置相应的读写权限,这样可以避免一些因为页属性保护而操作失败的情况。而且,操作完成后,还要记得还原页属性保护。
    其中要注意一点的是,低权限进程不能读写高权限进程的内存数据。是因为在调用 OpenProcess 函数打开进程获取句柄的时候,低权限进程就会因为权限不足而执行失败,自然后续操作不能正常执行。所以,可以“以管理员身份运行程序”来读取管理员权限获取比管理员权限低的进程内存数据。
    参考参考自《Windows黑客编程技术详解》一书
    1  留言 2018-11-23 08:51:00

发送私信

岁月极美,在于它必然的流逝

12
文章数
11
评论数
eject