访问进程私有地址空间--ReadProcessMemory

        ReadProcessMemory函数用于读取其他进程的数据。我们知道自远古时代结束后,user模式下的进程都有自己的地址空间,进程与进程间互不干扰,这叫私有财产神圣不可侵犯。但windows里还真就提供了那么一个机制,让你可以合法的获取别人的私有财产,这就是ReadProcessMemory和WriteProcessMemory。为什么一个进程居然可以访问另一个进程的地址空间呢?因为独立的只是低2G的用户态空间,高2G的内核态空间是所有进程共享的。一段执行中的线程进入内核态后,它可以拿到别人的cr3寄存器,用该cr3替换自己的cr3便完成了地址空间的转换。理论说明完毕,下面来看实现细节:

BOOL  STDCALL  ReadProcessMemory (HANDLE hProcess,

                                                                           LPCVOID lpBaseAddress,

                                                                           LPVOID lpBuffer,

                                                                           DWORD nSize,

                                                                           LPDWORD lpNumberOfBytesRead)

{

  NTSTATUS Status;

  Status = NtReadVirtualMemory( hProcess, (PVOID)lpBaseAddress,lpBuffer, nSize,(PULONG)lpNumberOfBytesRead);

  if (!NT_SUCCESS(Status))

  {

    SetLastErrorByStatus (Status);

    return FALSE;

  }

  return TRUE;

}

        这是用户态ReadProcessMemory的实现,它只做了一件事那就是调用NtReadVirtualMemory。NtReadVirtualMemory函数位于ntdll中,属于所谓的桩函数,作用就是把用户态的函数调用翻译成相应的系统调用,进入内核态。内核中一般有一个相同名字的处理函数,接收到该类型的系统调用后做实际的工作。系统调用的细节按下不表,让我们来看NtReadVirtualMemory到底在做什么事情:

NTSTATUS  STDCALL  NtReadVirtualMemory(IN HANDLE ProcessHandle,

                                                                                    IN PVOID BaseAddress,

                                                                                    OUT PVOID Buffer,

                                                                                    IN ULONG NumberOfBytesToRead,

                                                                                    OUT PULONG NumberOfBytesRead)
{

   NTSTATUS Status;

   PMDL Mdl;

   PVOID SystemAddress;

   PEPROCESS Process;

   DPRINT("NtReadVirtualMemory(ProcessHandle %x, BaseAddress %x, ""Buffer %x, NumberOfBytesToRead %d)\n",ProcessHandle,BaseAddress,

                 Buffer,NumberOfBytesToRead);

   Status = ObReferenceObjectByHandle(ProcessHandle,PROCESS_VM_WRITE,NULL,UserMode,(PVOID*)(&Process),NULL);

   if (Status != STATUS_SUCCESS)

   {

      return(Status);

   }

   。。。

}

        ObReferenceObjectByHandle函数从代表目标进程的handle里获取EPROCESS类型的指针,存放在变量Process中。EPROCESS结构保存了能代表一个进程的几乎所有关键数据,包括我们这里急需的cr3。

struct _EPROCESS

{

  /* Microkernel specific process state. */

  KPROCESS Pcb; /* 000 */

  。。。/*其他*/

}

typedef struct _KPROCESS

{

  /* So it's possible to wait for the process to terminate */

  DISPATCHER_HEADER DispatcherHeader; /* 000 */

   /* 
   * Presumably a list of profile objects associated with this process,
   * currently unused.

  */

   LIST_ENTRY ProfileListHead; /* 010 */

   /*
   * We use the first member of this array to hold the physical address of
   * the page directory for this process.

  */

  PHYSICAL_ADDRESS DirectoryTableBase; /* 018 这是cr3*/

   。。。/*其他*/

}


        接下来是从目标地址里创建一个MDL并将其锁定在主存里:

        Mdl = MmCreateMdl(NULL,Buffer,NumberOfBytesToRead);MmProbeAndLockPages(Mdl,UserMode,IoWriteAccess);

        为什么要创建这个MDL?等会儿再说。然后是最关键的一步,当前线程要当逃兵,叛逃至目标进程里了。。。

        KeAttachProcess(Process);

        执行完KeAttachProcess后,当前线程就成了Process进程所属的线程了,悲剧啊。怎么着咱们就被策反了呢?细节我们等下再看,让我们完成主逻辑先。

        SystemAddress = MmGetSystemAddressForMdl(Mdl);

        memcpy(SystemAddress, BaseAddress, NumberOfBytesToRead);

        KeDetachProcess();

        if (Mdl->MappedSystemVa != NULL)

        {

           MmUnmapLockedPages(Mdl->MappedSystemVa, Mdl);

        }

        MmUnlockPages(Mdl);

        ExFreePool(Mdl);

        ObDereferenceObject(Process);

        *NumberOfBytesRead = NumberOfBytesToRead;

        return(STATUS_SUCCESS);


        attach到目标进程里之后,我们又从之前生成好的MDL里获取一个虚拟地址映射,然后执行memcpy操作。这下为什么要创建MDL的秘密就清楚了,假如我们直接这样

写memcpy:

        memcpy(Buffer, BaseAddress, NumberOfBytesToRead);

        看着好像没什么问题,其实问题很大!Buffer所代表的地址应该是前一个进程空间里的,但现在确实新进程空间里的,根本不是一回事。我们费劲拷贝过去的数据,其实位于错误的内存里,等KeDetachProcess执行完切回原来的进程空间后,这些数据就全丢了,找都没地方找去。所以我们应该先从Buffer里生成一个MDL,切换进程完成后再从该MDL里反生成一个Virtual Address,然后memcpy就可以正确的将数据拷贝到该去的地方了。完成内存拷贝后,KeDetachProcess函数又将我们的线程从Process进程转回原来的进程,这下好,数据也偷到了,组织也回归了,原来这家伙是个间谍啊。。。现在我们可以来看看KeAttachProcess函数到底做了什么事情了。核心行为很明确,那就是替换cr3,但是细节到底如何呢:

VOID  STDCALL  KeAttachProcess (PEPROCESS Process)

{

   KIRQL oldlvl;

   PETHREAD CurrentThread;

   PULONG AttachedProcessPageDir;

   ULONG PageDir;

   DPRINT("KeAttachProcess(Process %x)\n",Process);

   CurrentThread = PsGetCurrentThread();

   if (CurrentThread->OldProcess != NULL)

   {

     DbgPrint("Invalid attach (thread is already attached)\n");

     KEBUGCHECK(0);

   }

   KeRaiseIrql(DISPATCH_LEVEL, &oldlvl);

   KiSwapApcEnvironment(&CurrentThread->Tcb, &Process->Pcb);

}

        这里我们把当前的IRQL提升到了DPC level,为的就是防止线程切换。然后调用KiSwapApcEnvironment把当前的apc队列也贴到目标进程里,按下不表。
/* The stack of the current process may be located in a page which is
not present in the page directory of the process we're attaching to.
That would lead to a page fault when this function returns. However,
since the processor can't call the page fault handler 'cause it can't
push EIP on the stack, this will show up as a stack fault which will
crash the entire system.
To prevent this, make sure the page directory of the process we're
attaching to is up-to-date. */

   AttachedProcessPageDir = ExAllocatePageWithPhysPage(Process->Pcb.DirectoryTableBase);

   MmUpdateStackPageDir(AttachedProcessPageDir, &CurrentThread->Tcb);

   ExUnmapPage(AttachedProcessPageDir);


        接下来如注释所说,Process->Pcb.DirectoryTableBase所代表的数据很有可能正在硬盘里的,物理如何也要保证它在内存里,因为函数返回时要做栈操作,如果Process->Pcb.DirectoryTableBase在硬盘上,栈操作就会引起page fault,而处理page fault前又必须要push eip,悲剧就要发生了。同样的,stack base 和 stack top这两哥们也一定得在内存里,MmUpdateStackPageDir做的就是这个事情。

  CurrentThread->OldProcess = PsGetCurrentProcess();

  CurrentThread->ThreadsProcess = Process;

  PageDir = Process->Pcb.DirectoryTableBase.u.LowPart;

  DPRINT("Switching process context to %x\n",PageDir);

  Ke386SetPageTableDirectory(PageDir);

  KeLowerIrql(oldlvl);


        最后做的事情就简单了,把当前线程的ThreadsProcess换成新的,再把当前的cr3换成Process->Pcb.DirectoryTableBase.u.LowPart。一番梳妆打扮后,敌人就分不清咱的身份了。       

        至此为止,ReadProcessMemory函数分析完毕。个人觉得有几个细节是需要注意的:第一呢,lpBaseAddress和lpBuffer所在的进程空间是不同的。第二呢,KeRaiseIrql和KeLowerIrql这两个函数一定要限制在进程空间切换的函数内,绝对不能把memcpy放在它们中间,因为KeRaiseIrql之后page fault就没法处理了,而memcpy不产生page fault那是不可能的,想都不要想。

DWORD Signature::FindSignature(HANDLE hProcess, std::string markCode, DWORD memBeginAddr, DWORD memEndAddr, DWORD retAddr[], int deviation, bool isAll)//deviation 偏移量 { //1F ?? 3B 44 -> 1f?3b44 -> 1f 3f 3b 44 ( 3f=='?' ) if (!markCode.empty()) { //去除所有空格 int index = 0; while ((index = markCode.find(' ', index)) >= 0)markCode.erase(index, 1); //删掉头部通配符 if (markCode[0] == 0x3F && markCode[1] == 0x3F)markCode.erase(0, 2); //特征码长度不能为单数 if (markCode.length() % 2 != 0) return 0; //特征码长度 int len = markCode.length() / 2; //Sunday算法模板数组的长度 int nSundayLen = len; //将特征码转换成byte型 BYTE *pMarkCode = new BYTE[len]; for (int i = 0; i < len; i++) { std::string tempStr = markCode.substr(i * 2, 2); if (tempStr == "??") { pMarkCode[i] = 0x3F; //0x3F == '?' if (nSundayLen == len) nSundayLen = i; } else pMarkCode[i] = (BYTE)strtoul(tempStr.c_str(), 0, 16); } //--------------------------end-------------------------// //Sunday算法模板数组赋值,+1防止特征码出现FF时越界 int aSunday[0xFF + 1] = { 0 }; for (int i = 0; i < nSundayLen; i++)aSunday[pMarkCode[i]] = i + 1; //起始地址 const DWORD dwBeginAddr = memBeginAddr; //结束地址 const DWORD dwEndAddr = memEndAddr; //当前读取的内存块地址 DWORD dwCurAddr = dwBeginAddr; //存放内存数据的缓冲区 BYTE *pMemBuffer = NULL; //计算参数retAddr[]数组的长度,该参数传入前一定要清0 int nArrayLength = 0; for (int i = 0;; i++) { if (*(retAddr + i) != 0) { nArrayLength = i; break; } } //偏移量 int nOffset; //数组下标:内存、特征码、返回地址, nCount=找到次数? int i = 0, j = 0, nCount = 0; //内存信息 MEMORY_BASIC_INFORMATION mbi; //扫描内存 while (dwCurAddr < dwEndAddr) { //查询地址空间中内存地址的信息 memset(&mbi, 0, sizeof(MEMORY_BASIC_INFORMATION));//mbi置零 if (::VirtualQueryEx(hProcess, (LPCVOID)dwCurAddr, &mbi, sizeof(mbi)) == 0) { //int error = GetLastError(); goto end; } //过滤内存空间, 根据内存的状态和保护属性进行过滤 //一般扫描(读写及执行)即可,速度极快,扫不到的话在尝试添加(读写)这一属性 if (MEM_COMMIT == mbi.State && //已分配的物理内存 //MEM_PRIVATE == mbi.Type || //私有内存,不被其他进程共享 //MEM_IMAGE == mbi.Type && //镜像 //PAGE_READONLY == mbi.Protect || //只读 PAGE_EXECUTE_READ == mbi.Protect || //读及执行 PAGE_READWRITE == mbi.Protect || //读写 PAGE_EXECUTE_READWRITE == mbi.Protect) //读写及执行 { //申请动态内存 if (pMemBuffer) { delete[] pMemBuffer; pMemBuffer = NULL; } pMemBuffer = new BYTE[mbi.RegionSize]; //读取进程内存 ReadProcessMemory(hProcess, (LPCVOID)dwCurAddr, pMemBuffer, mbi.RegionSize, 0); i = 0; j = 0; while (j < len) { nextAddr: if (pMemBuffer[i] == pMarkCode[j] || pMarkCode[j] == 0x3F) { i++; j++; } else { nOffset = i - j + nSundayLen; //判断偏移量是否大于缓冲区 if (nOffset > (int)mbi.RegionSize - len) break; //判断 aSunday模板数组 里有没有 内存偏移后的值,有则回溯,否则+1 if (aSunday[pMemBuffer[nOffset]]) { i = nOffset - aSunday[pMemBuffer[nOffset]] + 1; j = 0; } else { i = nOffset + 1; j = 0; } } } if (j == len) { //计算找到的目标地址: //特征码地址 = 当前内存块基址 + i偏移 - 特征码长度 //目标地址 = 特征码地址 + 偏移距离 //CALL(E8)跳转的地址 = E8指令后面的4个字节地址 + 下一条指令地址(也就是目标地址 + 5) retAddr[nCount] = dwCurAddr + i - len + deviation; /* if (isCall) { DWORD temp; memcpy(&temp, &pMemBuffer[i - len + deviation + 1], 4); retAddr[nCount] += 5; retAddr[nCount] += temp; } */ if (++nCount >= nArrayLength) { //传入的数组下标越界就结束搜索 goto end; } if (isAll) { i = i - len + 1; j = 0; goto nextAddr; } else { goto end; } } dwCurAddr += mbi.RegionSize; //取下一块内存地址 } else { dwCurAddr += mbi.RegionSize; } } end: //释放内存 if (pMemBuffer) { delete[] pMemBuffer; pMemBuffer = NULL; } delete[] pMarkCode; pMarkCode = NULL; return nCount; } return 0; }帮我优化这个函数
最新发布
07-12
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值