写拷贝是一种内存权限,是3环用的
使用 !vad EPROCESS.VadRoot 查看内存权限,主要关注的地址是0x77d507ea(MessageBoxA)
下方蓝条范围内,是执行_写拷贝权限
什么是写拷贝,就是你要往里面修改数据的时候,就会拷贝一份内容放入另一个内存且自己独占,并修改独占的内存,不会直接修改原来的内存
大概就是我不喜欢流水线大批量生产的东西,于是我自己定制了一份只有自己能用的
别人用 自己用 内存123 内存567 +-------+ +--------------+ |内容1 | ---> |修改后的内容1 | |内容2 | |内容2 | |内容3 | |内容3 |思路
当异常产生时,就会转入异常处理函数,判断当前操作是否合法。我们之前学到了缺页的时候会触发异常,读写权限不对的时候也会出现异常,但是只要不触发异常,他就不会检测这个操作是否合法。
而罪魁祸首居然是PTE没有 r/w权限(0B 0000 0010)的位置
相信大家已经轻车熟路了。刚才没注意,代码里没写MessageBoxA,所以用不到这个函数,所以PTE表里不需要加载这块物理内存,所以没有执行MessageBoxA之前,PTE是空的
我们知道call后会push EIP
所以call MessageBoxA后的地址应该是
MessageBoxA(参数1, 参数2, 参数3, 参数4) EIP <-- ESP 参数1 +0x4 参数2 +0x8 参数3 +0xC 参数4 +0x10
我的shell code 非常简单,只是把第4个参数改了
shell code 到OD里写出人能看懂的语句,然后照抄机器码就行
#include0环代码#include #include #define IN_BUFFER_MAXLENGTH 0x10 #define OUT_BUFFER_MAXLENGTH 0x10 //宏定义之获取一个32位的宏控制码 参数:设备类型(鼠标,键盘...Unkonwn);0x000-0x7FF保留,0x800-0xfff随便填一个;数据交互类型(缓冲区,IO,其他);对这个设备的权限 #define OPER1 CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) #define OPER2 CTL_CODE(FILE_DEVICE_UNKNOWN,0x900,METHOD_BUFFERED,FILE_ANY_ACCESS) #define SYMBOLICLINK_NAME "\\.\MyTestDriver" HANDLE g_hDevice; //全局驱动句柄 //打开驱动服务句柄 //3环链接名:\\.\AABB BOOL Open(PCHAR pLinkName) { //在3环获取设备句柄 TCHAR szBuffer[10] = { 0 }; //CreateFile 打开的是内核的设备对象 g_hDevice = CreateFile(pLinkName, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); if (g_hDevice != INVALID_HANDLE_VALUE) return TRUE; else return FALSE; } //dword写到byte[]里,由于要用两次,所以写了函数 //主要用来填充跳转地址的 void writeDword2Byte(BYTE* dst, DWORD src){ dst[0] = src >> 0; dst[1] = src >> 8; dst[2] = src >> 16; dst[3] = src >> 24; } int main(int argc, char* argv[]) { //通过这个jmp,跳转到要执行的shell code BYTE shellCode[] = {0xe9, 00, 00, 00, 00}; //jump xxxx to shell code (hook) //玩完记得弄回去 BYTE shellCode_writeback[5]; // restore //真正的shell code BYTE shellCode_run[] = { 0xC7, 0x44, 0x24, 0x10, 0x01, 0x00, 0x00, 0x00, //mov dword ptr[esp + 0x10], 1 0x55, //push ebp 0x8B, 0xEC, //mov ebp, esp 由于hook覆盖了两条语句,所以要写回来 0xe9, 00, 00, 00, 00}; //jmp back DWORD dwInBuffer[2], szOutBuffer[4]; DWORD Sz0utBuffer = 0; DWORD ByteReturned = 0; DWORD my_PID; DWORD msg_addr; BYTE* run_addr; DWORD offset_jmp; DWORD offset_jmpback; BYTE *pMsg_addr; int i; //C代码声明要放最前面,不然报错 //MessageBoxA( NULL, "before run", NULL, 0); //观察PTE用 msg_addr = (DWORD) MessageBoxA; //这个拿去做加减运算 pMsg_addr = (BYTE*)MessageBoxA; //这个拿去写入 //给ring 0传递的PID my_PID = (DWORD)GetCurrentProcessId(); printf("addr: %XtPID: %X", msg_addr, my_PID); //保存写回去的地址 memcpy(shellCode_writeback, pMsg_addr, sizeof(shellCode)); //申请可执行内存,这里申请的内存不能被其他进程访问,所以要想完美完成实验, //需要在高2G申请内存,并使其能被用户进程访问 run_addr = (BYTE*)VirtualAlloc(NULL, sizeof(shellCode_run), MEM_COMMIT, PAGE_EXECUTE_READWRITE); // offset = dst - (src + codeLength) offset_jmp = (DWORD)run_addr - (msg_addr + 5); writeDword2Byte(shellCode + 1, offset_jmp); //这里加上sizeof(shellCode)是为了防止跳到自己写的jmp上死循环 offset_jmpback = msg_addr - ((DWORD)run_addr + sizeof(shellCode_run)) + sizeof(shellCode); writeDword2Byte(shellCode_run + sizeof(shellCode_run)-4, offset_jmpback); //将要执行的 shell code 写入可执行内存 memcpy(run_addr, shellCode_run, sizeof(shellCode_run)); //准备冻手,准备冻手 //1.通过符号链接,打开设备 if (!Open(SYMBOLICLINK_NAME)) { printf("设备对象打开失败!!!"); getchar(); return 0; } //传入这俩 dwInBuffer[0] = msg_addr; dwInBuffer[1] = my_PID; //2.测试通信 DeviceIoControl(g_hDevice, OPER2, dwInBuffer, IN_BUFFER_MAXLENGTH, szOutBuffer, OUT_BUFFER_MAXLENGTH, &ByteReturned, NULL); //写入jmp xxxx memcpy(pMsg_addr, shellCode, sizeof(shellCode)); //3.关闭设备 getchar(); CloseHandle(g_hDevice); //玩完了写回去 memcpy(pMsg_addr, shellCode_writeback, sizeof(shellCode)); return 0; }
由于0环代码通信部分又臭又长,是我从网上扒下来的。感谢这些热心网友提供的代码(不记得从哪里扒的了,只是一直扒扒到能用)
这里指提供关键代码
//ProcessID:3环进程的PID
//addr: 要修改权限的地址
NTSTATUS changePageAttribute(HANDLE ProcessId, DWORD32 addr)
{
NTSTATUS Status = STATUS_SUCCESS;
PEPROCESS pEProcess = NULL;
//恢复状态用的,我也不太清楚这个
KAPC_STATE ApcState = { 0 };
DWORD32* pte;
_asm int 3;
//根据之前所学的公式直接套用即可
pte = (DWORD32*)(((addr >> 9) & 0x7FFFF8) + 0xC0000000);//x的pte
//获取EPROCESS结构体,很重要
Status = PsLookupProcessByProcessId((HANDLE)ProcessId, &pEProcess);
if (!NT_SUCCESS(Status) && !MmIsAddressValid(pEProcess)) { return STATUS_UNSUCCESSFUL; }
//加上这个try不会蓝屏,但是有些问题依然要重启的
__try {
//进程挂靠,就是把CR3改得和3环应用一样的,这样就能直接读3环应用的地址了
//有上层的API干嘛要自己搞
KeStackAttachProcess(pEProcess, &ApcState);
//好吧,其实0环做的所有事情就只有这一件,加上读写属性
*pte |= 0x2; //0000 0010
//结束挂靠
KeUnstackDetachProcess(&ApcState);
}
__except (EXCEPTION_EXECUTE_HANDLER) {
KeUnstackDetachProcess(&ApcState);
Status = STATUS_UNSUCCESSFUL;
}
ObDereferenceObject(pEProcess);
return Status;
}
如果觉得麻烦,完全可以在0环修改GDT表或者IDT表,弄个门,然后让3环调用,然后复用以前的代码
若果你对上面的函数不满意,还有两个更底层的进程挂靠函数可以用
甚至你可以在
EPROCESS[0]的地方找到KPROCESS
KPROCESS[0x18]的地方找到DirectoryTableBase,这就是CR3
然后就用汇编代码mov CR3, mycr3,改完PTE在弄回去
先执行驱动,在执行程序
好吧,其他进程调用MessageBoxA会直接崩溃,这说明我成功了,但也失败了
写保护过了,但是 shell code 没有成功,总归是有些失落的
原因是virtualAlloc申请的内存并不能被其他进程拥有。所以接下来要别人执行自己的shellcode,就需要申请高2G的内存,这样线性地址就是固定的了。
然后再把高2G的PTE改写一下,使他可以被用户程序访问
懒得搞了,有机会的话,会来补更的。
失败了但也有收获不是吗


![(15)[驱动开发]过写拷贝 (15)[驱动开发]过写拷贝](http://www.mshxw.com/aiimages/31/1020416.png)
