1. 切换目标进程的CR3
通常,跨进程读写内存,用到ReadProcessMemory, WriteProcessMemory, 但需要进程句柄,如果目标进程受到保护,可能获得进程句柄会失败.
ReadProcessMemory最后会调用到KeStackAttachProcess附加到目标进程上切换进程环境进行拷贝的, 所以想到拿到目标进程的虚拟内存内容,可以将目标进程的页目录基地址放入CR3中即可.
首先要获得目标进程的cr3寄存器,即页目录基地址(开启PAE, 页目录指针表),
每个进程在内核里都有一个EPROCESS结构.
nt!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x06c ProcessLock : _EX_PUSH_LOCK
+0x070 CreateTime : _LARGE_INTEGER
+0x078 ExitTime : _LARGE_INTEGER
+0x080 RundownProtect : _EX_RUNDOWN_REF
…….
Pcb中就有我们想要得到的CR3
nt!_KPROCESS
+0x000 Header : _DISPATCHER_HEADER
+0x010 ProfileListHead : _LIST_ENTRY
+0x018 DirectoryTableBase : [2] Uint4B
+0x020 LdtDescriptor : _KGDTENTRY
+0x028 Int21Descriptor : _KIDTENTRY
………那只需要获得目标进程EPROCESS就可以得到CR3了
遍历EPROCESS里的ActiveProcessLinks 的链表获取指定进程的EPROCESS
// 获得当前进程EPROCESS信息
ULONG uEprocess = 0;
__asm
{
mov eax, fs:[0x124] // _ethread
mov eax, [eax+0x44] // _kprocess
mov uEprocess, eax
}
KdPrint(("EPROCESS: 0x%08x\n", uEprocess));
LIST_ENTRY ListHead;
InitializeListHead(&ListHead);
ULONG uFirstEprocess = uEprocess;
ULONG uCount = 0;
PLIST_ENTRY pActiveProcessLinks;
ProcessInfoList *pProcssList = NULL;
ULONG uNameOffset = GetPlantformDependentInfo(FILE_NAME_OFFSET);
ULONG uPidOffset = GetPlantformDependentInfo(PROCESS_ID_OFFSET);
ULONG uLinkOffset = GetPlantformDependentInfo(PROCESS_LINK_OFFSET);
ULONG uExitTime = GetPlantformDependentInfo(EXIT_TIME_OFFSET);
// 遍历链表获得进程信息
do
{
pProcssList=
(ProcessInfoList *)ExAllocatePool(PagedPool, sizeof(ProcessInfoList));
if (pProcssList == NULL)
{
status = STATUS_INSUFFICIENT_RESOURCES;
break;
}
PLARGE_INTEGER ExitTime;
ExitTime = (PLARGE_INTEGER)(uEprocess + uExitTime);
if (ExitTime->QuadPart == 0)
{
if (*(int *)(uEprocess + uPidOffset) <= 0)
{
pProcssList->ProcInfo.uProcessId = 0;
pProcssList->ProcInfo.uEprocess = uEprocess;
pProcssList->ProcInfo.uCR3 = *(PULONG)(uEprocess + 0x18);
RtlCopyMemory(pProcssList->ProcInfo.pszImageFileName, "Idle", 16);
InsertHeadList(&ListHead, &pProcssList->ListEntry);
KdPrint(("PID: %d, EPROCESS: 0x%08x, FileName: %s, CR3: 0x%08x\n",
pProcssList->ProcInfo.uProcessId,
pProcssList->ProcInfo.uEprocess,
pProcssList->ProcInfo.pszImageFileName,
pProcssList->ProcInfo.uCR3));
}
else
{
pProcssList->ProcInfo.uEprocess = uEprocess;
pProcssList->ProcInfo.uCR3 = *(PULONG)(uEprocess + 0x18);
pProcssList->ProcInfo.uProcessId = *(PULONG)(uEprocess + uPidOffset);
RtlCopyMemory(pProcssList->ProcInfo.pszImageFileName,
(PVOID)(uEprocess + uNameOffset),
16);
InsertHeadList(&ListHead, &pProcssList->ListEntry);
KdPrint(("PID: %d, EPROCESS: 0x%08x, FileName: %s, CR3: 0x%08x\n",
pProcssList->ProcInfo.uProcessId,
pProcssList->ProcInfo.uEprocess,
pProcssList->ProcInfo.pszImageFileName,
pProcssList->ProcInfo.uCR3));
}
uCount++;
}
pActiveProcessLinks = (PLIST_ENTRY)(uEprocess + uLinkOffset);
uEprocess = (ULONG)pActiveProcessLinks->Blink - uLinkOffset;
if (uEprocess == uFirstEprocess)
{
break;
}
} while (uEprocess != 0);
引用了北极星2003大哥的GetPlantformDependentInfo 获取EPROCESS的成员偏移
下面是读写内存的:
_try
{
WriteMemoryInfo *pInfo =
(WriteMemoryInfo *)ExAllocatePool(PagedPool, sizeof(WriteMemoryInfo));
RtlCopyMemory(pInfo, pIoBuffer, sizeof(WriteMemoryInfo));
PVOID pWrite = ExAllocatePool(PagedPool, pInfo->nWriteSize);
RtlCopyMemory(pWrite, pInfo->pData, pInfo->nWriteSize);
//pInfo->pData = (PBYTE)ExAllocatePool(PagedPool, pInfo->nWriteSize);
ULONG uOldCr3 = 0;
ULONG uCurrentCr3 = *(PULONG)(pInfo->nEprocess + 0x18);
if (pInfo->nMemoryAddr == 0)
{
status = STATUS_UNSUCCESSFUL;
break;
}
__asm
{
mov eax, cr3
mov uOldCr3, eax
mov eax, uCurrentCr3
mov cr3, eax
}
KIRQL oldIrql;
oldIrql = KeRaiseIrqlToDpcLevel();
__asm
{
cli
push eax
mov eax, cr0
and eax, not 10000h
mov cr0, eax
}RtlCopyMemory((PVOID)pInfo->nMemoryAddr,
pWrite, pInfo->nWriteSize);__asm
{
mov eax, CR0
or eax, 10000h
mov cr0,eax
pop eax
sti
}KeLowerIrql(oldIrql);
__asm
{
mov eax, uOldCr3
mov cr3, eax
}
uOutSize = pInfo->nWriteSize;
if (pInfo != NULL)
{
ExFreePool(pInfo);
pInfo = NULL;
}
// Return success
status = STATUS_SUCCESS;
}
__except(1)
{
status = STATUS_UNSUCCESSFUL;
}
__try
{
ReadMemoryInfo *pInfo =
(ReadMemoryInfo *)ExAllocatePool(PagedPool, sizeof(ReadMemoryInfo));
RtlCopyMemory(pInfo, pIoBuffer, sizeof(ReadMemoryInfo));
ULONG uOldCr3 = 0;
ULONG uCurrentCr3 = *(PULONG)(pInfo->nEprocess + 0x18);
if (pInfo->nMemoryAddr == 0)
{
status = STATUS_UNSUCCESSFUL;
break;
}
__asm
{
mov eax, cr3
mov uOldCr3, eax
mov eax, uCurrentCr3
mov cr3, eax
}
RtlCopyMemory(pIoBuffer,
(PVOID)pInfo->nMemoryAddr ,pInfo->nReadSize);
uOutSize = pInfo->nReadSize;
__asm
{
mov eax, uOldCr3
mov cr3, eax
}
if (pInfo != NULL)
{
ExFreePool(pInfo);
pInfo = NULL;
}
// Return success
status = STATUS_SUCCESS;
}
__except(1)
{
status = STATUS_UNSUCCESSFUL;
}
2. 根据分页机制,进行手工转换,得到虚拟地址的映射的物理地址,读其物理地址得到目标进程虚拟地址的内容.详见附件分析.
获得了CR3,,接下来,就是根据分页机制,把虚拟地址转换为物理地址勒.
在转换之前要判断是否开启PAE,非PAE和开启PAE的转换有所不同
控制寄存器CR4的第5位标记是否开启PAE
// 获得CR4的值
__asm
{
_emit 0x0F
_emit 0x20
_emit 0xE0
mov uCR4, eax
}
未开启PAE情况
通过CR3寄存器定位到页目录的基地址
线性地址的高10位作为获取页目录表项的索引, 获得一个页目录的一个表项
注: windows的保护实现基本不使用分段机制,主要是通过分页机制来实现保护,这里的就线性地址等于虚拟地址.
// 获得页目录表项(PDE)
dwPageDirIndex = (dwVirtualAddr & 0xffc00000) >> 22;
DWORD dwPageDirEntry = ReadPageDirEntryNoPAE(dwPageDirIndex);
if (dwPageDirEntry == 0)
{
return;
}
根据PDE获得页表基地址或者页基地址
当没有开启PAE时,有两种PDE格式, 分别指向4KB的页表,和4MB的内存页
27893
27894
页目录项的第7位判断页大小
// 获得页大小
DWORD CReadMemoryDlg::GetPageSizeNoPAE(DWORD dwAddr)
{
if ((dwAddr & 0x00000080) == 0x00000080)
{
return MBSIZE;
}
else
{
return KBSIZE;
}
}
4KB时, 页目录项的高20位为页表基地址.线性地址的12位到21位作为选取页表的一个表项.(PTE)
27895
dwPageTableIndex = (dwVirtualAddr & 0x0003ff000) >> 12;
DWORD dwPageTableBaseAddr = dwPageDirEntry & 0xfffff000;
// 获得页表
DWORD dwPageTable = ReadPageTableNoPAE(dwPageTableBaseAddr,
dwPageTableIndex);
if (dwPageTable == 0)
{
return;
}
if (IsPresentNoPAE(dwPageTable) == FALSE)
{
return;
}
取PTE的高20位,作为内存页的基地址,线性地址的低12作为页中偏移得到物理地址
// 页的基地址
DWORD dwPageBaseAddr = dwPageTable & 0xfffff000;
dwPageOffset = dwVirtualAddr & 0x00000fff;
这时获得的物理地址就是想要的目标进程虚拟地址的映射的物理地址
PVOID pReadBuf = new BYTE[dwReadSize];
BOOL bRet = ReadPageMemoryNoPAE(pReadBuf,
dwPageOffset,
dwReadSize,
dwPageBaseAddr);
if (bRet == FALSE)
{
return;
}
4MB时,页目录项的高10位作为页的基地址, 线性地址的低22位在物理地址在页中的偏移
27896
// 线性地址的低22位是页内偏移
DWORD dwPageOffsetMB = dwVirtualAddr & 0x003fffff;
// 高10位,是页基地址
DWORD dwPageBaseAddr = dwPageDirEntry & 0xffc00000;
这时获得的物理地址就是想要的目标进程虚拟地址的映射的物理地址
PVOID pReadBuf = new BYTE[dwReadSize];
BOOL bRet = ReadPageMemoryPAE(pReadBuf,
dwPageOffsetMB,
dwReadSize,
dwPageBaseAddr);
if (bRet == FALSE)
{
return;
}
开启PAE情况
CR4中的物理地址扩展(PAE)标志可以开启PAE机制,将物理地址从32位扩展到36位.
当开始PAE机制后,处理器支持两种尺寸的页:4KB和2Mb的页.
增加了页目录指针表项.
表项的大小从32位增加到了64位
表项中的物理基地址扩展到了24位
寄存器CR3中的高位页目录基地址被换为27位的页目录指针表基地址
27897
通过CR3寄存器定位到页目录指针表起始地址,取线性地址的高2位作为选取页目录指针表项的索引
DWORD dwDirPointerTableIndex = (dwVirtualAddr & 0xc0000000) >> 30;
DWORD dwDirPointerTableBaseAddr = m_nDirBase & 0xffffffe0;
// 获得页目录指针表
__int64 nPageDirPointerTable = ReadPageDirPAE(dwDirPointerTableIndex,
dwDirPointerTableBaseAddr);
if (nPageDir == 0)
{
return;
}
取线性地址的第21位到29位作为页目录索引,页目录指针表项的第12位到第35位为页目录基地址
27898
// 页目录基地址
DWORD dwDirBaseAddr = (DWORD)(nPageDirPointerTable &0x0000000ffffff000);
// 页目录项索引
DWORD dwPageDirIndex = (dwVirtualAddr & 0x3fe00000) >> 21;
__int64 nPageDirEntry = ReadPageDirEntryPAE(dwPageDirIndex, dwDirBaseAddr);
if (nPageDirEntry == 0)
{
return;
}
根据页目录项的第7位判断页大小,2MB,还是4KB
// 获得页大小
DWORD CReadMemoryDlg::GetPageSizePAE(__int64 nAddr)
{
if ((nAddr & 0x0000000000000080) == 0x0000000000000080)
{
return MBSIZE;
}
else
{
return KBSIZE;
}
}
4KB时,页目录项的第12位到35位作为页表基地址的高24位.取线性地址第12位到20位作为在页表中的偏移
27899
// 获得页表
DWORD dwPageTableBaseAddr = (DWORD)(nPageDirEntry & 0x0000000ffffff000);
DWORD dwPageTableIndex = (dwVirtualAddr & 0x001ff000) >> 12;
__int64 nPageTable = ReadPageTablePAE(dwPageTableIndex, dwPageTableBaseAddr);
if (nPageTable == 0)
{
return;
}
通过页表项的第12位到第35位作为页基地址的高24位,线性地址的低12为作为在页中的偏移
// 读取页内容
DWORD dwPageBaseAddr = (DWORD)(nPageTable & 0x0000000ffffff000);
DWORD dwPageOffsetKb = dwVirtualAddr & 0x00000fff;
PVOID pReadBuf = new BYTE[dwReadSize];
BOOL bRet = ReadPageMemoryPAE(pReadBuf,
dwPageOffsetKb,
dwReadSize,
dwPageBaseAddr);
if (bRet == FALSE)
{
return;
}
所得到的内容就是目标进程虚拟地址内存的数据
2MB时, 页目录项的第21位到35位作为页的基地址的高位,取线性地址的第0位到第20位做为物理地址在页中的偏移
27900
DWORD dwPageOffsetMB = dwVirtualAddr & 0x000fffff;
// 高-35位
DWORD dwDirBaseAddr = (DWORD)(nPageDirEntry & 0x00000007fff00000);
PVOID pReadBuf = new BYTE[dwReadSize];
BOOL bRet = ReadPageMemoryPAE(pReadBuf,
dwPageOffsetMB,
dwReadSize,
dwDirBaseAddr);
if (bRet == FALSE)
{
return;
}
所得到的内容就是目标进程虚拟地址内存的数据