以前一直都在用Read/WriteProcessMemory这个API读写内存,也没探究过Windows怎么实现的内存读写,这几天就了解了解这一部分。
打开IDA,拖入KernelBase.dll(因为Kernel32.dll中的ReadProcessMemory会调转到KernelBase这里),定位到API。
BOOL __stdcall ReadProcessMemory(HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T *lpNumberOfBytesRead)
{
NTSTATUS status; // eax
SIZE_T SIZE; // [rsp+30h] [rbp-18h]
status = NtReadVirtualMemory(hProcess, lpBaseAddress, lpBuffer, nSize, &SIZE);
if ( lpNumberOfBytesRead )
*lpNumberOfBytesRead = SIZE;
if ( status >= 0 )
return 1;
sub_180028EA0();
return 0;
}
可以看到直接Call了Ntdll中的NtReadVirtualMemory,这玩意原型如下。
NTSTATUS
NtReadVirtualMemory(
__in HANDLE ProcessHandle,
__in_opt PVOID BaseAddress,
__out_bcount(BufferSize) PVOID Buffer,
__in SIZE_T BufferSize,
__out_opt PSIZE_T NumberOfBytesRead
);
继续跟下去。可以看到Ntdll调用了syscall进入内核的NtReadVirtualMemory
既然有WRK这种东西就不用自己看F5了,直接打开WRK。这玩意是真香~
贴一段代码方便读者们查看。
NTSTATUS
NtReadVirtualMemory(
__in HANDLE ProcessHandle,
__in_opt PVOID BaseAddress,
__out_bcount(BufferSize) PVOID Buffer,
__in SIZE_T BufferSize,
__out_opt PSIZE_T NumberOfBytesRead
)
/*++
Routine Description:
This function copies the specified address range from the specified
process into the specified address range of the current process.
Arguments:
ProcessHandle - Supplies an open handle to a process object.
BaseAddress - Supplies the base address in the specified process
to be read.
Buffer - Supplies the address of a buffer which receives the
contents from the specified process address space.
BufferSize - Supplies the requested number of bytes to read from
the specified process.
NumberOfBytesRead - Receives the actual number of bytes
transferred into the specified buffer.
Return Value:
NTSTATUS.
--*/
{
SIZE_T BytesCopied;
KPROCESSOR_MODE PreviousMode;
PEPROCESS Process;
NTSTATUS Status;
PETHREAD CurrentThread;
PAGED_CODE();
//
// Get the previous mode and probe output argument if necessary.
//
CurrentThread = PsGetCurrentThread ();
PreviousMode = KeGetPreviousModeByThread(&CurrentThread->Tcb);
if (PreviousMode != KernelMode) {
if (((PCHAR)BaseAddress + BufferSize < (PCHAR)BaseAddress) ||
((PCHAR)Buffer + BufferSize < (PCHAR)Buffer) ||
((PVOID)((PCHAR)BaseAddress + BufferSize) > MM_HIGHEST_USER_ADDRESS) ||
((PVOID)((PCHAR)Buffer + BufferSize) > MM_HIGHEST_USER_ADDRESS)) {
return STATUS_ACCESS_VIOLATION;
}
if (ARGUMENT_PRESENT(NumberOfBytesRead)) {
try {
ProbeForWriteUlong_ptr (NumberOfBytesRead);
} except(EXCEPTION_EXECUTE_HANDLER) {
return GetExceptionCode();
}
}
}
//
// If the buffer size is not zero, then attempt to read data from the
// specified process address space into the current process address
// space.
//
BytesCopied = 0;
Status = STATUS_SUCCESS;
if (BufferSize != 0) {
//
// Reference the target process.
//
Status = ObReferenceObjectByHandle(ProcessHandle,
PROCESS_VM_READ,
PsProcessType,
PreviousMode,
(PVOID *)&Process,
NULL);
//
// If the process was successfully referenced, then attempt to
// read the specified memory either by direct mapping or copying
// through nonpaged pool.
//
if (Status == STATUS_SUCCESS) {
Status = MmCopyVirtualMemory (Process,
BaseAddress,
PsGetCurrentProcessByThread(CurrentThread),
Buffer,
BufferSize,
PreviousMode,
&BytesCopied);
//
// Dereference the target process.
//
ObDereferenceObject(Process);
}
}
//
// If requested, return the number of bytes read.
//
if (ARGUMENT_PRESENT(NumberOfBytesRead)) {
try {
*NumberOfBytesRead = BytesCopied;
} except(EXCEPTION_EXECUTE_HANDLER) {
NOTHING;
}
}
return Status;
}
首先这玩意先判断了下当前线程的PreviousMode,如果是用户态就进行一系列参数校验,过滤不合法参数。之后就判断BufferSize参数(Read系列函数是向Buffer中写数据),如果参数正常则根据传过来的ProcessHandle获取该进程的EPROCESS指针。如果成功获取到EPROCESS就调用MmCopyVirtualMemory,并调用ObDereferenceObject减少该EPROCESS指针的引用计数,传参、返回MmCopyVirtualMemory返回的Status。
也就是说NtReadVirtualMemory的核心是MmCopyVirtualMemory。
NTSTATUS
MmCopyVirtualMemory(
IN PEPROCESS FromProcess,
IN CONST VOID *FromAddress,
IN PEPROCESS ToProcess,
OUT PVOID ToAddress,
IN SIZE_T BufferSize,
IN KPROCESSOR_MODE PreviousMode,
OUT PSIZE_T NumberOfBytesCopied
)
{
NTSTATUS Status;
PEPROCESS ProcessToLock;
if (BufferSize == 0) {
ASSERT (FALSE); // No one should call with a zero size.
return STATUS_SUCCESS;
}
ProcessToLock = FromProcess;
if (FromProcess == PsGetCurrentProcess()) {
ProcessToLock = ToProcess;
}
//
// Make sure the process still has an address space.
//
if (ExAcquireRundownProtection (&ProcessToLock->RundownProtect) == FALSE) {
return STATUS_PROCESS_IS_TERMINATING;
}
//
// If the buffer size is greater than the pool move threshold,
// then attempt to write the memory via direct mapping.
//
if (BufferSize > POOL_MOVE_THRESHOLD) {
Status = MiDoMappedCopy(FromProcess,
FromAddress,
ToProcess,
ToAddress,
BufferSize,
PreviousMode,
NumberOfBytesCopied);
//
// If the completion status is not a working quota problem,
// then finish the service. Otherwise, attempt to write the
// memory through nonpaged pool.
//
if (Status != STATUS_WORKING_SET_QUOTA) {
goto CompleteService;
}
*NumberOfBytesCopied = 0;
}
//
// There was not enough working set quota to write the memory via
// direct mapping or the size of the write was below the pool move
// threshold. Attempt to write the specified memory through nonpaged
// pool.
//
Status = MiDoPoolCopy(FromProcess,
FromAddress,
ToProcess,
ToAddress,
BufferSize,
PreviousMode,
NumberOfBytesCopied);
//
// Dereference the target process.
//
CompleteService:
//
// Indicate that the vm operation is complete.
//
ExReleaseRundownProtection (&ProcessToLock->RundownProtect);
return Status;
}
这函数首先判断了下参数,获取当前PROCESS对象,如果是自己的内存向自己内存复制,就讲ProcessToLock赋值自己的PROCESS。然后用ExAcquireRundownProtection给ProcessToLock中的Process加上停运保护。在微软文档中对这玩意的说明如下:
To start sharing an object, the driver that owns the object calls the
ExInitializeRundownProtection routine to initialize run-down
protection on the object. After this call, other drivers that access
the object can acquire and release run-down protection on the object.A driver that accesses the shared object calls the
ExAcquireRundownProtection routine to request run-down protection on
the object. After the access is finished, this driver calls the
ExReleaseRundownProtection routine to release run-down protection on
the object.
也就是说要想共享一个对象,拥有该对象的驱动要调用ExInitializeRundownProtection函数以初始化停运保护机制,在这之后,其他要访问此对象的驱动就可以对其实施和撤销停运保护功能。
要访问共享对象的驱动通过调用ExAcquireRundownProtection函数来请求对该对象的停运保护,当访问结束后,驱动通过调用 ExReleaseRundownProtection 来取消停运保护。
停运机制很适合用于那些经常有效可用但不知何时会突然被删除或替换的共享对象,访问共享对象数据的驱动或者是调用线程在对象被删除后需确保不再尝试访问该对象,否则这些非法访问可能会造成无法预料的行为后果比如数据损坏,更严重点甚至会出现系统崩溃。
这玩意确保进程还在正常状态,没退出。说白了就是比较好使的互斥锁。
#define POOL_MOVE_THRESHOLD 511
如果要读取的大小大于POOL_MOVE_THRESHOLD,就调用MiDoMappedCopy拷贝内存,否则调用MiDoPoolCopy。调用完成后释放对ProcessToLock的停运保护。
NTSTATUS
MiDoMappedCopy (
IN PEPROCESS FromProcess,
IN CONST VOID *FromAddress,
IN PEPROCESS ToProcess,
OUT PVOID ToAddress,
IN SIZE_T BufferSize,
IN KPROCESSOR_MODE PreviousMode,
OUT PSIZE_T NumberOfBytesRead
)
/*++
Routine Description:
This function copies the specified address range from the specified
process into the specified address range of the current process.
Arguments:
FromProcess - Supplies an open handle to a process object.
FromAddress - Supplies the base address in the specified process
to be read.
ToProcess - Supplies an open handle to a process object.
ToAddress - Supplies the address of a buffer which receives the
contents from the specified process address space.
BufferSize - Supplies the requested number of bytes to read from
the specified process.
PreviousMode - Supplies the previous processor mode.
NumberOfBytesRead - Receives the actual number of bytes
transferred into the specified buffer.
Return Value:
NTSTATUS.
--*/
{
KAPC_STATE ApcState;
SIZE_T AmountToMove;
ULONG_PTR BadVa;
LOGICAL Moving;
LOGICAL Probing;
LOGICAL LockedMdlPages;
CONST VOID *InVa;
SIZE_T LeftToMove;
PSIZE_T MappedAddress;
SIZE_T MaximumMoved;
PMDL Mdl;
PFN_NUMBER MdlHack[(sizeof(MDL)/sizeof(PFN_NUMBER)) + (MAX_LOCK_SIZE >> PAGE_SHIFT) + 1];
PVOID OutVa;
LOGICAL MappingFailed;
LOGICAL ExceptionAddressConfirmed;
PAGED_CODE();
MappingFailed = FALSE;
InVa = FromAddress;
OutVa = ToAddress;
MaximumMoved = MAX_LOCK_SIZE;
if (BufferSize <= MAX_LOCK_SIZE) {
MaximumMoved = BufferSize;
}
Mdl = (PMDL)&MdlHack[0];
//
// Map the data into the system part of the address space, then copy it.
//
LeftToMove = BufferSize;
AmountToMove = MaximumMoved;
Probing = FALSE;
//
// Initializing BadVa & ExceptionAddressConfirmed is not needed for
// correctness but without it the compiler cannot compile this code
// W4 to check for use of uninitialized variables.
//
BadVa = 0;
ExceptionAddressConfirmed = FALSE;
while (LeftToMove > 0) {
if (LeftToMove < AmountToMove) {
//
// Set to move the remaining bytes.
//
AmountToMove = LeftToMove;
}
KeStackAttachProcess (&FromProcess->Pcb, &ApcState);
MappedAddress = NULL;
LockedMdlPages = FALSE;
Moving = FALSE;
ASSERT (Probing == FALSE);
//
// We may be touching a user's memory which could be invalid,
// declare an exception handler.
//
try {
//
// Probe to make sure that the specified buffer is accessible in
// the target process.
//
if ((InVa == FromAddress) && (PreviousMode != KernelMode)){
Probing = TRUE;
ProbeForRead (FromAddress, BufferSize, sizeof(CHAR));
Probing = FALSE;
}
//
// Initialize MDL for request.
//
MmInitializeMdl (Mdl, (PVOID)InVa, AmountToMove);
MmProbeAndLockPages (Mdl, PreviousMode, IoReadAccess);
LockedMdlPages = TRUE;
MappedAddress = MmMapLockedPagesSpecifyCache (Mdl,
KernelMode,
MmCached,
NULL,
FALSE,
HighPagePriority);
if (MappedAddress == NULL) {
MappingFailed = TRUE;
ExRaiseStatus(STATUS_INSUFFICIENT_RESOURCES);
}
//
// Deattach from the FromProcess and attach to the ToProcess.
//
KeUnstackDetachProcess (&ApcState);
KeStackAttachProcess (&ToProcess->Pcb, &ApcState);
//
// Now operating in the context of the ToProcess.
//
if ((InVa == FromAddress) && (PreviousMode != KernelMode)){
Probing = TRUE;
ProbeForWrite (ToAddress, BufferSize, sizeof(CHAR));
Probing = FALSE;
}
Moving = TRUE;
RtlCopyMemory (OutVa, MappedAddress, AmountToMove);
} except (MiGetExceptionInfo (GetExceptionInformation(),
&ExceptionAddressConfirmed,
&BadVa)) {
//
// If an exception occurs during the move operation or probe,
// return the exception code as the status value.
//
KeUnstackDetachProcess (&ApcState);
if (MappedAddress != NULL) {
MmUnmapLockedPages (MappedAddress, Mdl);
}
if (LockedMdlPages == TRUE) {
MmUnlockPages (Mdl);
}
if (GetExceptionCode() == STATUS_WORKING_SET_QUOTA) {
return STATUS_WORKING_SET_QUOTA;
}
if ((Probing == TRUE) || (MappingFailed == TRUE)) {
return GetExceptionCode();
}
//
// If the failure occurred during the move operation, determine
// which move failed, and calculate the number of bytes
// actually moved.
//
*NumberOfBytesRead = BufferSize - LeftToMove;
if (Moving == TRUE) {
if (ExceptionAddressConfirmed == TRUE) {
*NumberOfBytesRead = (SIZE_T)((ULONG_PTR)BadVa - (ULONG_PTR)FromAddress);
}
}
return STATUS_PARTIAL_COPY;
}
KeUnstackDetachProcess (&ApcState);
MmUnmapLockedPages (MappedAddress, Mdl);
MmUnlockPages (Mdl);
LeftToMove -= AmountToMove;
InVa = (PVOID)((ULONG_PTR)InVa + AmountToMove);
OutVa = (PVOID)((ULONG_PTR)OutVa + AmountToMove);
}
//
// Set number of bytes moved.
//
*NumberOfBytesRead = BufferSize;
return STATUS_SUCCESS;
}
NTSTATUS
MiDoPoolCopy (
IN PEPROCESS FromProcess,
IN CONST VOID *FromAddress,
IN PEPROCESS ToProcess,
OUT PVOID ToAddress,
IN SIZE_T BufferSize,
IN KPROCESSOR_MODE PreviousMode,
OUT PSIZE_T NumberOfBytesRead
)
/*++
Routine Description:
This function copies the specified address range from the specified
process into the specified address range of the current process.
Arguments:
ProcessHandle - Supplies an open handle to a process object.
BaseAddress - Supplies the base address in the specified process
to be read.
Buffer - Supplies the address of a buffer which receives the
contents from the specified process address space.
BufferSize - Supplies the requested number of bytes to read from
the specified process.
PreviousMode - Supplies the previous processor mode.
NumberOfBytesRead - Receives the actual number of bytes
transferred into the specified buffer.
Return Value:
NTSTATUS.
--*/
{
KAPC_STATE ApcState;
SIZE_T AmountToMove;
LOGICAL ExceptionAddressConfirmed;
ULONG_PTR BadVa;
PEPROCESS CurrentProcess;
LOGICAL Moving;
LOGICAL Probing;
CONST VOID *InVa;
SIZE_T LeftToMove;
SIZE_T MaximumMoved;
PVOID OutVa;
PVOID PoolArea;
LONGLONG StackArray[COPY_STACK_SIZE];
ULONG FreePool;
PAGED_CODE();
ASSERT (BufferSize != 0);
//
// Get the address of the current process object and initialize copy
// parameters.
//
CurrentProcess = PsGetCurrentProcess();
InVa = FromAddress;
OutVa = ToAddress;
//
// Allocate non-paged memory to copy in and out of.
//
MaximumMoved = MAX_MOVE_SIZE;
if (BufferSize <= MAX_MOVE_SIZE) {
MaximumMoved = BufferSize;
}
FreePool = FALSE;
if (BufferSize <= sizeof(StackArray)) {
PoolArea = (PVOID)&StackArray[0];
} else {
do {
PoolArea = ExAllocatePoolWithTag (NonPagedPool, MaximumMoved, 'wRmM');
if (PoolArea != NULL) {
FreePool = TRUE;
break;
}
MaximumMoved = MaximumMoved >> 1;
if (MaximumMoved <= sizeof(StackArray)) {
PoolArea = (PVOID)&StackArray[0];
break;
}
} while (TRUE);
}
//
// Initializing BadVa & ExceptionAddressConfirmed is not needed for
// correctness but without it the compiler cannot compile this code
// W4 to check for use of uninitialized variables.
//
BadVa = 0;
ExceptionAddressConfirmed = FALSE;
//
// Copy the data into pool, then copy back into the ToProcess.
//
LeftToMove = BufferSize;
AmountToMove = MaximumMoved;
Probing = FALSE;
while (LeftToMove > 0) {
if (LeftToMove < AmountToMove) {
//
// Set to move the remaining bytes.
//
AmountToMove = LeftToMove;
}
KeStackAttachProcess (&FromProcess->Pcb, &ApcState);
Moving = FALSE;
ASSERT (Probing == FALSE);
//
// We may be touching a user's memory which could be invalid,
// declare an exception handler.
//
try {
//
// Probe to make sure that the specified buffer is accessible in
// the target process.
//
if ((InVa == FromAddress) && (PreviousMode != KernelMode)){
Probing = TRUE;
ProbeForRead (FromAddress, BufferSize, sizeof(CHAR));
Probing = FALSE;
}
RtlCopyMemory (PoolArea, InVa, AmountToMove);
KeUnstackDetachProcess (&ApcState);
KeStackAttachProcess (&ToProcess->Pcb, &ApcState);
//
// Now operating in the context of the ToProcess.
//
if ((InVa == FromAddress) && (PreviousMode != KernelMode)){
Probing = TRUE;
ProbeForWrite (ToAddress, BufferSize, sizeof(CHAR));
Probing = FALSE;
}
Moving = TRUE;
RtlCopyMemory (OutVa, PoolArea, AmountToMove);
} except (MiGetExceptionInfo (GetExceptionInformation(),
&ExceptionAddressConfirmed,
&BadVa)) {
//
// If an exception occurs during the move operation or probe,
// return the exception code as the status value.
//
KeUnstackDetachProcess (&ApcState);
if (FreePool) {
ExFreePool (PoolArea);
}
if (Probing == TRUE) {
return GetExceptionCode();
}
//
// If the failure occurred during the move operation, determine
// which move failed, and calculate the number of bytes
// actually moved.
//
*NumberOfBytesRead = BufferSize - LeftToMove;
if (Moving == TRUE) {
//
// The failure occurred writing the data.
//
if (ExceptionAddressConfirmed == TRUE) {
*NumberOfBytesRead = (SIZE_T)((ULONG_PTR)(BadVa - (ULONG_PTR)FromAddress));
}
}
return STATUS_PARTIAL_COPY;
}
KeUnstackDetachProcess (&ApcState);
LeftToMove -= AmountToMove;
InVa = (PVOID)((ULONG_PTR)InVa + AmountToMove);
OutVa = (PVOID)((ULONG_PTR)OutVa + AmountToMove);
}
if (FreePool) {
ExFreePool (PoolArea);
}
//
// Set number of bytes moved.
//
*NumberOfBytesRead = BufferSize;
return STATUS_SUCCESS;
}
MiDoMappedCopy和MiDoPoolCopy都是通过KeStackAttachProcess挂靠到进程完成读解除挂靠,挂靠到另外一个进程写入,但MiDoMappedCopy利用的MDL来实现读写,MiDoPoolCopy申请了一片缓冲区作为中转,未利用MDL…
另外读这一片WRK原来,我学到了操作用户空间的内存必须要用try except进行异常处理,降低BSOD的几率.