写拷贝是什么
写拷贝是一种内存权限,是3环用的
使用 !vad EPROCESS.VadRoot
查看内存权限,主要关注的地址是0x77d507ea(MessageBoxA)
下方蓝条范围内,是执行_写拷贝权限
什么是写拷贝,就是你要往里面修改数据的时候,就会拷贝一份内容放入另一个内存且自己独占,并修改独占的内存,不会直接修改原来的内存
大概就是我不喜欢流水线大批量生产的东西,于是我自己定制了一份只有自己能用的
别人用 自己用
内存123 内存567
+-------+ +--------------+
|内容1 | ---> |修改后的内容1 |
|内容2 | |内容2 |
|内容3 | |内容3 |
思路
当异常产生时,就会转入异常处理函数,判断当前操作是否合法。我们之前学到了缺页的时候会触发异常,读写权限不对的时候也会出现异常,但是只要不触发异常,他就不会检测这个操作是否合法。
而罪魁祸首居然是PTE没有 r/w权限(0B 0000 0010)的位置
相信大家已经轻车熟路了。刚才没注意,代码里没写MessageBoxA,所以用不到这个函数,所以PTE表里不需要加载这块物理内存,所以没有执行MessageBoxA之前,PTE是空的
3环代码
我们知道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里写出人能看懂的语句,然后照抄机器码就行
#include<windows.h>
#include<winioctl.h>
#include<stdio.h>
#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: %X\tPID: %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环代码
由于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改写一下,使他可以被用户程序访问
懒得搞了,有机会的话,会来补更的。
失败了但也有收获不是吗