APC/DPC

APC

线程异步回调

让一段代码在指定线程的上下文中被异步执行

流程

1
2
3
4
5
6
7
线程正在运行
↓
系统或其他线程向它投递一个 APC
↓
APC 被加入该线程的 APC 队列
↓
线程在合适的时机执行 APC 中的函数

结构简述

每个线程里有APC状态结构

1
2
3
4
5
6
7
8
9
10
11
KAPC_STATE ApcState;
KAPC_STATE SavedApcState;

struct _KAPC_STATE
{
struct _LIST_ENTRY ApcListHead[2]; //0x0
struct _KPROCESS* Process; //0x10
UCHAR KernelApcInProgress; //0x14
UCHAR KernelApcPending; //0x15
UCHAR UserApcPending; //0x16
};

ApcListHead中包含的是

0 KernelAPC

1 UserAPC

所以每个APC环境中分两个队列,KernelAPC List 和 UserAPC List

这里的队列节点就是常用的KAPC,并呈现双向链表结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct _KAPC
{
UCHAR Type; //0x0
UCHAR SpareByte0; //0x1
UCHAR Size; //0x2
UCHAR SpareByte1; //0x3
ULONG SpareLong0; //0x4
struct _KTHREAD* Thread; //0x8
struct _LIST_ENTRY ApcListEntry; //0xc
VOID (*KernelRoutine)(struct _KAPC* arg1, VOID (**arg2)(VOID* arg1, VOID* arg2, VOID* arg3), VOID** arg3, VOID** arg4, VOID** arg5); //0x14
VOID (*RundownRoutine)(struct _KAPC* arg1); //0x18
VOID (*NormalRoutine)(VOID* arg1, VOID* arg2, VOID* arg3); //0x1c
VOID* NormalContext; //0x20
VOID* SystemArgument1; //0x24
VOID* SystemArgument2; //0x28
CHAR ApcStateIndex; //0x2c
CHAR ApcMode; //0x2d
UCHAR Inserted; //0x2e
};

在队列中,排在前面的部分的是没有 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
typedef enum _KAPC_ENVIRONMENT {
OriginalApcEnvironment, //原始环境
AttachedApcEnvironment, //挂靠的进程环境
CurrentApcEnvironment, //当前环境
InsertApcEnvironment //替换时确定环境
} KAPC_ENVIRONMENT;

typedef enum _MODE {
KernelMode,
UserMode,
MaximumMode
} MODE;

VOID KeInitializeApc(
__out PRKAPC Apc, //APC结构
__in PRKTHREAD Thread, //目标线程
__in KAPC_ENVIRONMENT Environment, //环境
__in PKKERNEL_ROUTINE KernelRoutine, //调用的函数
__in_opt PKRUNDOWN_ROUTINE RundownRoutine, //
__in_opt PKNORMAL_ROUTINE NormalRoutine, //在KernelRoutine后的函数
__in_opt KPROCESSOR_MODE ApcMode, //内核还是用户
__in_opt PVOID NormalContext //个数
);

KeInitializeApc(pApc, eThread, OriginalApcEnvironment,
kernelRoutineFunc, NULL, NormalRoutineFunc, KernelMode, (PVOID)1);

KeInsertQueueApc(pApc, pEvent, NULL, 0);

APC注入

待完善… …

DPC

延迟过程调用

把中断处理里耗时的工作延迟到稍后执行,从而让中断处理函数尽快返回

流程

硬件中断时,CPU会进入 中断服务程序(ISR)

ISR的作用是1.确认终端 2.读取关键数据 3.排队一个DPC

那么进行整理得到流程:

1
2
3
4
5
6
7
8
9
10
11
硬件中断
↓
ISR (Interrupt Service Routine)
↓
KeInsertQueueDpc
↓
DPC 被放入 DPC 队列
↓
CPU IRQL 降到 DISPATCH_LEVEL
↓
DPC 执行

特征:不是线程,无线程上下文

从机制上可以发现,DPC和异常是同一等级的,所以在创建KDPC结构体的时候,需要不分页,不然触发分页异常的时候,异常时无法修复的(因为同等级无法修改)

内核 API 用法

这个的声明都是能直接看到的,所以没什么解释的,然后DPC和APC的区别还有就是,DPC可以延时触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include<ntifs.h>

VOID dpcCallBack(
_In_ struct _KDPC* Dpc,
_In_opt_ PVOID DeferredContext,
_In_opt_ PVOID SystemArgument1,
_In_opt_ PVOID SystemArgument2
)
{
ExFreePool(Dpc);
DbgPrintEx(77, 0, "[db]: %s\n", __FUNCTION__);
return;
}

VOID DriverUnload(PDRIVER_OBJECT pDriver)
{

}

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pPeg)
{
KDPC *kdpc = ExAllocatePool(NonPagedPool, sizeof(KDPC));

KeInitializeDpc(kdpc, dpcCallBack, NULL);

KeInsertQueueDpc(kdpc, NULL, NULL);

KTIMER* Tkdpc = ExAllocatePool(NonPagedPool, sizeof(KTIMER));

KeInitializeTimer(Tkdpc);

LARGE_INTEGER inTime = { 0 };
inTime.QuadPart = -10000 * 1000;
KeSetTimer(Tkdpc, inTime, kdpc);

pDriver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}

用法

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
VOID MyDpcRoutine(PKDPC Dpc, PVOID DeferredContext,
PVOID SystemArgument1, PVOID SystemArgument2)
{
// 1. 每个 CPU 各自做本地的事(如清各自 CPU 的缓存/TLB)

// 2. 栅栏:等所有 CPU 都干完本地部分
KeSignalCallDpcSynchronize(SystemArgument2);

// 3. 所有 CPU 都到齐后才能做的事(如统一改某个全局内核函数)

// 4. 通知完成
KeSignalCallDpcDone(SystemArgument1);
}

VOID DoWorkOnAllCpus(VOID)
{
KeGenericCallDpc(MyDpcRoutine, NULL); // 阻塞直到所有 CPU 完成
}

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#include<ntifs.h>
#include<intrin.h>
#include<ntifs.h>

#define IA32_FEATURE_CONTROL 0x3A

BOOLEAN VmxIsBIOSSStartVT()
{
ULONG64 control = __readmsr(IA32_FEATURE_CONTROL);

if (((control & 5) == 5))
return TRUE;

if ((control & 1) == 1)
return FALSE;

control |= 5;

__writemsr(IA32_FEATURE_CONTROL, control);

control = __readmsr(IA32_FEATURE_CONTROL);

return (control & 5) == 5;
}

BOOLEAN VmxIsCpuIdSuportVT()
{
ULONG cpuidinfo[4];

__cpuidex(cpuidinfo, 1, 0);

return (cpuidinfo[2] >> 5) & 1;
}

BOOLEAN VmxIsCr4EnableVT()
{
ULONG64 mcr4 = __readcr4();

return ((mcr4 >> 13) & 1) == 0;
}

VOID KeGenericCallDpc(__in PKDEFERRED_ROUTINE Routine, __in_opt PVOID Context);

VOID KeSignalCallDpcDone(__in PVOID SystemArgument1);

LOGICAL KeSignalCallDpcSynchronize(__in PVOID SystemArgument2);

VOID StartVT(
_In_ struct _KDPC* Dpc,
_In_opt_ PVOID DeferredContext,
_In_opt_ PVOID SystemArgument1,
_In_opt_ PVOID SystemArgument2
)
{
do
{
if (!VmxIsCpuIdSuportVT())
{
DbgPrintEx(77, 0, "[db]:cpuid not support vt number = %d\r\n", KeGetCurrentProcessorNumberEx(NULL));
break;
}
if (!VmxIsBIOSSStartVT())
{
DbgPrintEx(77, 0, "[db]:bios disable vt lock number = %d \r\n", KeGetCurrentProcessorNumberEx(NULL));
break;
}
if (!VmxIsCr4EnableVT())
{
DbgPrintEx(77, 0, "[db]:vt 已存在 number = %d\r\n", KeGetCurrentProcessorNumberEx(NULL));
break;
}

DbgPrintEx(77, 0, "[db]:vt number = %d\r\n", KeGetCurrentProcessorNumberEx(NULL));

} while (0);

KeSignalCallDpcSynchronize(SystemArgument2);
KeSignalCallDpcDone(SystemArgument1);
}

VOID EnableVT()
{
KeGenericCallDpc(StartVT, NULL);

}

VOID DriverUnload(PDRIVER_OBJECT pDriver)
{

}

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pPeg)
{
EnableVT();
pDriver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}

初始化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 内部状态。注意两点:

  1. VMXON 执行完 ,Windows 本身还在这个 CPU 上正常跑(只是变成了跑在 root 模式),系统毫无感觉;
  2. 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
2
3
4
5
6
7
8
9
kd> !db 3d25b000+C58
#3d25bc58 68 00 65 00 6c 00 6c 00-6f 00 77 00 6f 00 72 00 h.e.l.l.o.w.o.r.
#3d25bc68 6c 00 64 00 31 00 31 00-00 00 00 00 00 00 00 00 l.d.1.1.........
#3d25bc78 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
#3d25bc88 00 00 00 00 00 00 00 00-08 00 00 08 58 82 00 00 ............X...
#3d25bc98 50 87 48 00 70 e9 47 00-06 00 00 06 5a 82 00 00 P.H.p.G.....Z...
#3d25bca8 50 87 48 00 70 e9 47 00-00 00 00 00 04 00 3b 00 P.H.p.G.......;.
#3d25bcb8 9b a1 37 7b 40 82 00 00-90 4a 48 00 68 09 48 00 ..7{@....JH.h.H.
#3d25bcc8 61 00 72 00 73 00 f6 ff-89 a1 36 68 4e 82 00 08 a.r.s.....6hN...

页属性

当直接查看CR3的时候,看到的后三位并不是0,其实这里代表的就是页属性

可以对照该表

P

0代表无效

US

1代表R3可访问,0代表R0才能访问

A

是否被改变过,改变1

G

如果是1的话,是不会刷新缓存值的

没什么可记的,看完课就好

页の终章

挂页

VirtrualAlloc申请地址在赋值前是不会挂物理页的,在memset等操作进行之后才会赋值,又众所周知,0地址是不可赋值的,因为0地址没有挂物理页,那么就会有一种程序,给0地址挂页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// study05.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include<Windows.h>

int _tmain(int argc, _TCHAR* argv[])
{
int *a = 0;

int * x = (int* )VirtualAlloc(NULL,0x100,MEM_COMMIT,PAGE_EXECUTE_READWRITE);
memset(x,0,0x100);
printf("%x\n",x);
system("pause");
//手动挂页
*a=100;
printf("%x\n",*x);
system("pause");
return 0;
}

这个还是很好玩的

页下

这个感觉不太好记,比较散,应该也不容易忘就不急了,记脑子里

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
2
3
4
5
6
7
8
PsSetCreateProcessNotifyRoutine;
PsSetCreateProcessNotifyRoutineEx;

PsSetCreateThreadNotifyRoutine;
PsSetLoadImageNotifyRoutine;

PsRemoveLoadImageNotifyRoutine;
PsRemoveCreateThreadNotifyRoutine;

对象回调

1
ObRegisterCallbacks
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include<ntifs.h>

PVOID RegistrationHandle = NULL;

typedef struct _LDR_DATA_TABLE_ENTRY
{
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
USHORT LoadCount;
USHORT TlsIndex;
union
{
LIST_ENTRY HashLinks;
struct
{
PVOID SectionPointer;
ULONG CheckSum;
};
};
union
{
ULONG TimeDateStamp;
PVOID LoadedImports;
};
PVOID EntryPointActivationContext;
PVOID PatchInformation;
} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;

OB_PREOP_CALLBACK_STATUS PreProcess(
_In_ PVOID RegistrationContext,
_Inout_ POB_PRE_OPERATION_INFORMATION OperationInformation
)
{


return OB_PREOP_SUCCESS;
}

VOID PostProcess(
_In_ PVOID RegistrationContext,
_In_ POB_POST_OPERATION_INFORMATION OperationInformation
)
{

}

VOID DriverUnload(PDRIVER_OBJECT pDriver)
{
if(RegistrationHandle) ObUnRegisterCallbacks(RegistrationHandle);
}

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pPeg)
{

OB_CALLBACK_REGISTRATION CallbackRegistration = { 0 };
CallbackRegistration.Version = ObGetFilterVersion;
CallbackRegistration.OperationRegistrationCount = 1;
CallbackRegistration.RegistrationContext = ((PVOID)123456);
RtlInitUnicodeString(&CallbackRegistration.Altitude, L"12345");

OB_OPERATION_REGISTRATION OperationRegistration = { 0 };
OperationRegistration.ObjectType = *PsProcessType;
OperationRegistration.Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE;

OperationRegistration.PreOperation = PreProcess;
OperationRegistration.PostOperation = PostProcess;

CallbackRegistration.OperationRegistration = &OperationRegistration;

PLDR_DATA_TABLE_ENTRY ex = (PLDR_DATA_TABLE_ENTRY)pDriver->DriverSection;
ex->Flags |= 0x20;

NTSTATUS st = ObRegisterCallbacks(&CallbackRegistration, &RegistrationHandle);

DbgPrintEx(77, 0, "[db]: st:0x%x", st);

pDriver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}

句柄降权

相关文章:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#define IRP_MJ_CREATE                   0x00
#define IRP_MJ_CREATE_NAMED_PIPE 0x01
#define IRP_MJ_CLOSE 0x02
#define IRP_MJ_READ 0x03
#define IRP_MJ_WRITE 0x04
#define IRP_MJ_QUERY_INFORMATION 0x05
#define IRP_MJ_SET_INFORMATION 0x06
#define IRP_MJ_QUERY_EA 0x07
#define IRP_MJ_SET_EA 0x08
#define IRP_MJ_FLUSH_BUFFERS 0x09
#define IRP_MJ_QUERY_VOLUME_INFORMATION 0x0a
#define IRP_MJ_SET_VOLUME_INFORMATION 0x0b
#define IRP_MJ_DIRECTORY_CONTROL 0x0c
#define IRP_MJ_FILE_SYSTEM_CONTROL 0x0d
#define IRP_MJ_DEVICE_CONTROL 0x0e
#define IRP_MJ_INTERNAL_DEVICE_CONTROL 0x0f
#define IRP_MJ_SHUTDOWN 0x10
#define IRP_MJ_LOCK_CONTROL 0x11
#define IRP_MJ_CLEANUP 0x12
#define IRP_MJ_CREATE_MAILSLOT 0x13
#define IRP_MJ_QUERY_SECURITY 0x14
#define IRP_MJ_SET_SECURITY 0x15
#define IRP_MJ_POWER 0x16
#define IRP_MJ_SYSTEM_CONTROL 0x17
#define IRP_MJ_DEVICE_CHANGE 0x18
#define IRP_MJ_QUERY_QUOTA 0x19
#define IRP_MJ_SET_QUOTA 0x1a
#define IRP_MJ_PNP 0x1b
#define IRP_MJ_PNP_POWER IRP_MJ_PNP // Obsolete....
#define IRP_MJ_MAXIMUM_FUNCTION 0x1b

驱动逆向

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

L 内存dump

​

!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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include"function.h"
#include"tool.h"

NTSTATUS RW_QueryMemory(ULONG64 pid, PCHAR VirtualAddress, PMyMEMORY_BASIC_INFOMATION info)
{
PEPROCESS Process = NULL;

PMyMEMORY_BASIC_INFOMATION mTemp = { 0 };

NTSTATUS st = PsLookupProcessByProcessId(pid, &Process); //通过PID找到进程

if (!NT_SUCCESS(st))
{
return st;
}

if (PsGetProcessExitStatus(Process) != STATUS_PENDING)
{
return STATUS_UNSUCCESSFUL;
}

KAPC_STATE kApcState = { 0 };

KeStackAttachProcess(Process, &kApcState); //挂靠过去

MODE o_mode = SetPreviousMode(KeGetCurrentThread(), KernelMode); //此时的线程是r3层, 调用 NtQueryVirtualMemory 的时候会检测环境, 需要手动改线程的目标位

do
{
MEMORY_BASIC_INFORMATION m_info = { 0 };
SIZE_T retSize = 0;
st = NtQueryVirtualMemory(NtCurrentProcess(), VirtualAddress, MemoryBasicInformation, &m_info, sizeof(m_info), retSize); //查内存

if (!NT_SUCCESS(st))
{
break;
}

mTemp->AllocationBase = m_info.AllocationBase;
mTemp->AllocationProtect = m_info.AllocationProtect;
mTemp->BaseAddress = m_info.AllocationProtect;
mTemp->Protect = m_info.Protect;
mTemp->RegionSize = m_info.RegionSize;
mTemp->State = m_info.State;
mTemp->Type = m_info.Type;

} while (0);

SetPreviousMode(KeGetCurrentThread(), UserMode);

KeUnstackDetachProcess(&kApcState);

if (MmIsAddressValid(info)) //注意这里, 一开始的参数是原进程的地址, 挂靠到 B 进程是没有地址的, 所以需要中转一下
{
memcpy(info, &mTemp, sizeof(MyMEMORY_BASIC_INFOMATION));
}

return mTemp->RegionSize != 0 ? STATUS_SUCCESS : STATUS_UNSUCCESSFUL;
}

SSDT HOOK

找到SSDT表的方式:

1.是在kernel中查找特定字节码

2.查找KeAddSystemServerTable函数,里面有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include"ssdthook.h"

#include<intrin.h>
#include"tool.h"

SSDTStruct* g_SSDT = NULL;
SSDTStruct* g_SSSDT = NULL;

SSDTStruct* SSDTfind()
{
if (g_SSDT) return g_SSDT;
static SSDTStruct* SSDT = 0;
if (!SSDT)\
{
#ifndef _WIN64
//x86 code
UNICODE_STRING routineName;
RtlInitUnicodeString(&routineName, L"KeServiceDescriptorTable");
SSDT = (SSDTStruct*)MmGetSystemRoutineAddress(&routineName);
#else

//x64 code
ULONG_PTR kernelBase = NULL;
SIZE_T kernelSize = NULL;

kernelSize = GetNtOsModule(&kernelBase);

if (kernelBase == 0 || kernelSize == 0)
return NULL;

//Find KiSystemServiceStart
const unsigned char KiSystemServiceStartPattern[] = { 0x8B, 0xF8, 0xC1, 0xEF, 0x07, 0x83, 0xE7, 0x20, 0x25, 0xFF, 0x0F, 0x00, 0x00 };
const ULONG signatureSize = sizeof(KiSystemServiceStartPattern);
BOOLEAN found = FALSE;
ULONG KiSSSOffset;
for (KiSSSOffset = 0; KiSSSOffset < kernelSize - signatureSize; KiSSSOffset++)
{
if (RtlCompareMemory(((unsigned char*)kernelBase + KiSSSOffset), KiSystemServiceStartPattern, signatureSize) == signatureSize)
{
found = TRUE;
break;
}
}
if (!found)
return NULL;

// lea r10, KeServiceDescriptorTable
ULONG_PTR address = kernelBase + KiSSSOffset + signatureSize;
LONG relativeOffset = 0;
if ((*(unsigned char*)address == 0x4c) &&
(*(unsigned char*)(address + 1) == 0x8d) &&
(*(unsigned char*)(address + 2) == 0x15))
{
relativeOffset = *(LONG*)(address + 3);
}
if (relativeOffset == 0)
return NULL;

SSDT = (SSDTStruct*)(address + relativeOffset + 7);
#endif
}

g_SSDT = SSDT;
return SSDT;
}

ULONG64 GetSSDTFunc(ULONG funcIndex)
{
SSDTStruct* ssdt = SSDTfind();
if (ssdt != NULL)
{
LONG64 offset1 = ssdt->pServiceTable[funcIndex];

offset1 = (offset1 >> 4);

ULONG64 myfunc = (ULONG64)ssdt->pServiceTable + offset1;
KdPrint(("find ssdt func %p index %x\r\n", myfunc, funcIndex));
return myfunc;
}

KdPrint(("not find ssdt table \r\n"));
return 0;
}

查询模块

想要找到特定的字节码或者函数,需要得到模块地址

模块读写,这里其实就是对API进行熟悉编写小项目,火哥做了一个PEB读模块表得到模块基地址的项目

流程上就是APC注入+读取PEB+读取模块链表,只不过会有一些注意事项,这里不在阐述了,到时候可以回来看,是50

创建线程

依然是APC注入+创建线程+shellcode,感觉没什么

​