APC/DPC
APC
线程异步回调
让一段代码在指定线程的上下文中被异步执行
流程
1 | 线程正在运行 |
结构简述
每个线程里有APC状态结构
1 | KAPC_STATE ApcState; |
ApcListHead中包含的是
0 KernelAPC
1 UserAPC
所以每个APC环境中分两个队列,KernelAPC List 和 UserAPC List
这里的队列节点就是常用的KAPC,并呈现双向链表结构
1 | struct _KAPC |
在队列中,排在前面的部分的是没有 NormalRoutine 的 KAPC
然后是有 NormalRoutine 的KAPC,这里 NormalRoutine 是一个函数,会在 KernelRoutine 函数运行后调用
同时KAPC中还包含一个环境的变量 ApcStateIndex 来说明的是当前KAPC处于什么环境
这里需要回到开头,线程中的APC状态结构有
KAPC_STATE ApcState;
KAPC_STATE SavedApcState;
如果说 线程1 挂靠到别的进程中,那么 ApcStateIndex 负责说明,该APC属于那种环境,也就是上述两种KAPC_STATE的哪个
举例:没有挂靠时
ApcState → 1.exe APC队列
SavedApcState → 未使用
挂靠时
SavedApcState = 新线程APC队列
SavedApcState → 1.exe APC队列
内核 API 用法
1 | typedef enum _KAPC_ENVIRONMENT { |
APC注入
待完善… …
DPC
延迟过程调用
把中断处理里耗时的工作延迟到稍后执行,从而让中断处理函数尽快返回
流程
硬件中断时,CPU会进入 中断服务程序(ISR)
ISR的作用是1.确认终端 2.读取关键数据 3.排队一个DPC
那么进行整理得到流程:
1 | 硬件中断 |
特征:不是线程,无线程上下文
从机制上可以发现,DPC和异常是同一等级的,所以在创建KDPC结构体的时候,需要不分页,不然触发分页异常的时候,异常时无法修复的(因为同等级无法修改)
内核 API 用法
这个的声明都是能直接看到的,所以没什么解释的,然后DPC和APC的区别还有就是,DPC可以延时触发
1 |
|
用法
1.正常注册
2.指定核 KeSetTargetProcessorDpc
3.延时定时器
4.多核侵染 KeGenericCallDpc
VT
| 寄存器 | 存的什么 |
|---|---|
| CR0 | 模式开关(保护模式/分页) |
| CR2 | 最近一次缺页的虚拟地址(#PF 时硬件自动填) |
| CR3 | 当前进程页表的物理基址(PML4),切进程就换它 |
| CR4 | 扩展功能开关排(VMXE 在这) |
| CR8 | 中断优先级(TPR) |
HOST 监控者 - r-1 VT虚拟机
GUEST 被监控者 - r0/r3
VT检测
1.MSR寄存器 IA32_FEATURE_CONTROL 表示BIOS是否打开VT开关
2.执行CPUID指令,判断CPU是否支持VT
3.读取CR4,判断是否VT已被占用
| 寄存器 | 存的什么 |
|---|---|
| CR0 | 模式开关(保护模式/分页) |
| CR2 | 最近一次缺页的虚拟地址(#PF 时硬件自动填) |
| CR3 | 当前进程页表的物理基址(PML4),切进程就换它 |
| CR4 | 扩展功能开关排(VMXE 在这) |
| CR8 | 中断优先级(TPR) |
并且对于多核CPU,每个核都需要进行一次检测,代码直接执行是在哪个核上的,需要遍历所有核
API
KeGenericCallDpc(Routine, Context)—— 发起端 向系统里的每个处理器各排一个 DPC,并等待所有 CPU 都执行完才返回。你在主线程里调用它,Routine 就会在每个 CPU 的 DISPATCH_LEVEL 上被同时调起来。Context 会作为 DeferredContext 传给 DPC 例程。
KeSignalCallDpcSynchronize(SystemArgument2)—— DPC 内的“栅栏” 在 DPC 例程内部调用。它表示“我这个 CPU 已经到了同步点”,然后阻塞等待所有其他 CPU 也到达这一点。返回值是 LOGICAL:最后一个到达栅栏的 CPU 得到 TRUE,其余得到 FALSE(可以用它判断“我是不是最后一个”,好做清理工作)。
KeSignalCallDpcDone(SystemArgument1) —— DPC 内的“完成信号” 也在 DPC 例程内部调用,表示“我这个 CPU 干完了”。最后一个 CPU 调用它之后,发起端的 KeGenericCallDpc 才解除等待返回。
1 | VOID MyDpcRoutine(PKDPC Dpc, PVOID DeferredContext, |
代码
1 |
|
初始化VMON
| VMM | VM |
|---|---|
| host | guest |
| root | non root |
VMON 一块至少一个页的内存
x64 CPU 在 VT 概念里有两层运行模式:
- VMX root 操作模式 :全权模式,hypervisor(你的驱动)跑在这里,能看能管一切;
- VMX non-root 模式 :受限模式,被虚拟化的对象(guest)跑在这里,敏感指令会触发 VM-exit 退回 root 模式。
VMXON指令的作用就是:把当前这个 CPU 切进 VMX root 模式,同时给 CPU 注册一块“VMXON region”内存让芯片存 VMX 内部状态。注意两点:
- VMXON 执行完 ,Windows 本身还在这个 CPU 上正常跑(只是变成了跑在 root 模式),系统毫无感觉;
- VMXON ≠ 虚拟化开始。它只是“开门”,真正的 guest 要等你下一步搞完 VMCS 再用
VMLAUNCH拉起来。
内存
保护机制
首先铺垫,在前面学过pe文件格式,且写过反射型注入,有一定知识前置
我们知道,一个软件在运行的时候,会分配一个内存中的基地址(虚拟地址),很好,那么明明是在内存中实体存在的为什么叫虚拟地址?我们还知道,地址是线性的0 1 2 …… 0xffffffffh,线性排布,那么问题来了,我的程序加载到内存中,假如说是0x87654321,欸?此时我有一个指令是mov dword ptr [0] , 1,那么会把内存开始地方的东西改动掉,但是内存最开始的东西肯定是很内核的东西,什么对程序的调用啊,什么的之前在反调试有接触过
自然是不能让你随便动他的,那么怎么才能很安全的做到呢
目前我学到的是分段分页,相当于在原本的0x55555555h的地方一刀斩断,后面又开始0x0h(应该如此,后续回来补充)
嗯不是这样的,是分成一段一段然后通过代码段,数据段来区分,这是段保护机制
页保护估计就是进程线程的吧。。。
101012
这里火哥的课就又开始”架空”起来了,很多新名词完全没有说过,看一下这篇文章-101012部分,火哥的课不行就跳过这一节,里面也说了”Cr3”
插个眼,这里! process 0 0这个指令还不太懂物理地址懂一半,之前搞项目那会学了一点,但是没太深入,我估计后面学页的时候可以相辅相成
页の前章
线性地址对应物理地址
假如我在记事本中写了一串字符”helloworld”,然后用工具比如CE查找到该字符串在内存中的地址00484C58那么可以按照10-10-12的方式解析该虚拟地址的物理地址,所谓101012就是bit位排布,首先把000AE928换成二进制bit位形式0000 0000 0100 1000 0100 1100 0101 1000,然后划分
10 00 0000 0001 1 页目录 单位1*4byte PDE
10 00 1000 0100 84 页表项 单位1*4byte PTE
12 1100 0101 1000 C58 页内偏移 单位1byte
于是就可以找物理地址了,于是就弥补了前文的无知,cr3是什么?cr3就是改进程(书)地址的寄存器,代表了该书,用! process 0 0列举所有cr3的值找到notepad.exe,得到物理基地址03d75000
! dd 03d75000来获取目录物理地址的值,目录中存放的是指针,也不难想到指针指向的地方就是书本内容的地方,比如这个要找的就是! dd 03d75000+14->3cbc7867,这里要去掉867,这个是页属性得到3cbc7000,这就是当前页的起始地址,再接着加上844,得到3d25b867,同理3d25b000,于是就可以看里面的内容了
1 | kd> !db 3d25b000+C58 |
页属性
当直接查看CR3的时候,看到的后三位并不是0,其实这里代表的就是页属性
可以对照该表
P
0代表无效
US
1代表R3可访问,0代表R0才能访问
A
是否被改变过,改变1
G
如果是1的话,是不会刷新缓存值的
没什么可记的,看完课就好
页の终章
挂页
VirtrualAlloc申请地址在赋值前是不会挂物理页的,在memset等操作进行之后才会赋值,又众所周知,0地址是不可赋值的,因为0地址没有挂物理页,那么就会有一种程序,给0地址挂页
1 | // study05.cpp : 定义控制台应用程序的入口点。 |
这个还是很好玩的
页下
这个感觉不太好记,比较散,应该也不容易忘就不急了,记脑子里
TLB
虚拟页帧 物理页帧 attr属性 次数 PCID
本质是缓存,里面可以临时存储最近用过的联系?然后再次使用的时候可以先碰撞TLB,看看有没有现成的联系,没有再拆
x64
火哥52内容
- PML4 :第 4 级页表(顶层)
- PDPTE :Page Directory Pointer Table Entry,页目录指针表项
- PDE :Page Directory Entry,页目录项
- PTE :Page Table Entry,页表项
进程_线程_对象回调
https://blog.csdn.net/csnn2019/article/details/113818969
全局句柄表/私有访问
dq pspoidtable
dt _HANDLE_TABLE pspoidtable中的第一个值
TableCode值为全局句柄表地址
全局句柄表:
00句柄表
01句柄表指针
02句柄表目录指针
00查表
句柄值/表容量 得到 表序号
句柄表通过序号找到表,跳过去
句柄值%表容量 得到表内序号
最后找到句柄位置
模块_进程_线程回调
这里的API根据名字进行监视回调
1 | PsSetCreateProcessNotifyRoutine; |
对象回调
1 | ObRegisterCallbacks |
1 |
|
句柄降权
相关文章:
https://www.52pojie.cn/thread-1770541-1-1.html
探索Windows内核系列——句柄,利用句柄进行进程保护_volatilelowvalue-CSDN博客
https://www.52pojie.cn/thread-1771006-1-1.html
全局句柄表/私有访问
dq pspoidtable
dt _HANDLE_TABLE pspoidtable中的第一个值
TableCode值为全局句柄表地址
全局句柄表:
00句柄表
01句柄表指针
02句柄表目录指针
00查表
句柄值/表容量 得到 表序号
句柄表通过序号找到表,跳过去
句柄值%表容量 得到表内序号
最后找到句柄位置
以上为火哥内核内容,以下为文章对结构的截图

_EPROCESS是进程在内核中内核对象结构
这个知识在进程线程中有涉及
通信
先补一个知识点
CreateFile如果打开了一个”\??\Nul”这样的驱动对象
那么后续的一些API可以触发回调
ReadFile 触发 IRP_MJ_READ 的回调函数,这个是在驱动结构下的MajorFunction里记录的
1 |
驱动逆向
0: kd> bp 0000000000316ADF0: kd> bp 00000000000022B00: kd> bp 000000000031857E0: kd> bp 0000000000319A37!analyze -v 查看蓝屏相关信息
k查看堆栈调用
kv查看堆栈调用详情
?? Driver->Object可以查看设备对象详情
!object Object_Addr可以查看设备地址
dt 链表名 下一个/上一个链表地址即可看结构
dt这个指令可用于看很多种结构,并且dt xxx -r1可以深入看结构下的几层结构
bp手动下断点(好用!!)
u查看目标地址字节码
u eip 查看代码上下文
u rip L200 查看200行代码上文
r 查看寄存器
eb fffff800`23a35000 XX 改字节码
t 单步进入
t 10 走10步
p 单步步过
p 10 走10步
bp <地址> 断点
.reload windbg重新加载模块符号
lmvm xxx lmvm模块名用来查看某个指定模块的详细信息
.writemem
!process 0 0 目标进程 得到句柄
.process /p 句柄 进入进程
!process 句柄 7 列进程线程
dt 结构体名 地址
bl ; 列出所有断点及编号
bc 0 ; 删除 0 号断点
bc * ; 删除所有断点
bd 0 ; 禁用 0 号断点(保留)
be 0 ; 启用 0 号断点
插件
ida - windbg联动
windbg运行如下指令
.load sync 加载插件(.load C:\Users\13621\AppData\Local\Microsoft\WindowsApps\sync.dll)
!sync 启动
跨进程项目类实现
火哥49-51
查询内存
主要是开发吧,直接学习代码就好
大概思路是通过 APC Attach 挂靠到目标进程,然后用 NtQueryVirtualMemory 扫内存
1 |
|
SSDT HOOK
找到SSDT表的方式:
1.是在kernel中查找特定字节码
2.查找KeAddSystemServerTable函数,里面有
1 |
|
查询模块
想要找到特定的字节码或者函数,需要得到模块地址
模块读写,这里其实就是对API进行熟悉编写小项目,火哥做了一个PEB读模块表得到模块基地址的项目
流程上就是APC注入+读取PEB+读取模块链表,只不过会有一些注意事项,这里不在阐述了,到时候可以回来看,是50
创建线程
依然是APC注入+创建线程+shellcode,感觉没什么