APC注入
APC注入
APC机制
线程是不能被杀死 挂起和恢复的,线程在执行的时候自己占据着CPU,别人怎么可能控制他呢?举个极端的例子,如果不调用API,屏蔽中断,并保证代码不出现异常,线程将永久占据CPU。所以说线程如果想结束,一定是自己执行代码把自己杀死,不存在别人把线程结束的情况。
那如果想改变一个线程的行为该怎么办?可以给他提供一个函数,让他自己去调用,这个函数就是APC,即异步过程调用
对于内核APC,APC函数的插入和执行并不是同一个线程,具体点说:在A线程中向B线程插入一个APC,插入的动作是在A线程中完成的,但什么时候执行则由B线程决定。所以叫异步过程调用。
线程切换时,在SwapContext
函数即将执行完成的时候,会判断当前是否有要执行的内核APC,接着将判断的结果存到eax,然后返回,接着找到上一层函数KiSwapContext
函数,这个函数也没有对APC进行处理,而是继续返回,到父函数,会判断KiSwapContext
的返回值,也就是判断当前是否有要处理的内核APC,如果有,则调用KiDeliverApc
进行处理。
系统调用中断或者异常(_KiServiceExit
),会判断是否有要执行的用户APC,如果有的话则会调用KiDeliverApc
函数进行处理,此时KiDeliverApc
第一个参数为1,代表执行用户APC和内核APC。当要执行用户APC之前,先要执行内核APC。
对于应用APC,当产生系统调用、中断或者异常,线程在返回用户空间前都会调用_KiServiceExit
函数,在_KiServiceExit
函数里会判断是否有要执行的用户APC,如果有则调用KiDeliverApc
函数进行处理。
有用户APC要执行的话,就意味着线程要提前返回到用户空间去执行,而且返回的位置不是线程进入内核时的位置,而是返回到真正执行APC的位置每处理一个用户APC就会涉及到:内核—>用户空间—>再回到内核空间。进入内核前,当前上下文会被临时保存以待恢复。
APC注入
dllmain
注入的dll弹一个messageBox就行:
1 | BOOL APIENTRY DllMain( HMODULE hModule, |
KAPC
创建好设备对象和链接对象,设计派遣历程
1 | NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegisterPath) |
派遣例程
默认直接通过
1 | NTSTATUS PassThroughDispatch(PDEVICE_OBJECT DeviceObject, PIRP Irp) |
IRP_MJ_DEVICE_CONTROL自定义
解析IRP堆栈内容,筛选IO控制码,CTL_APC_INJECTION就调用注入函数
1 | NTSTATUS ControlThroughDispatch(PDEVICE_OBJECT DeviceObject, PIRP Irp) |
APC注入主函数
- PsLookupProcessByProcessId通过目标进程ID获取EProcess
- PsGetProcessWow64Process返回值可以判断是不是Wow64进程。
- KeWaitForSingleObject返回值判断是否是僵尸进程。如果是,解引用释放。
- KeStackAttachProcess切换进程上下背景文。
- 自己实现的SeGetModuleBaseByModuleName从目标进程获取Ntdll模块基地址。这里会使用到是否是Wow64进程
- 自己实现的SeGetExportFunctionFromModule从目标模块获取LdrLoadDll导出函数。
- 初始化ShellCode
- 调用自己的ApcInjection开始进行注入。
1 | NTSTATUS ApcInjectionPrepare(IN PINJECTION_INFORMATION InjectionInfo) |
从目标进程通过模块名称获取模块基地址
- 检查EProcess
- 对WOW64和64位进程分别获取指定的PEB,并等待Loader初始化,然后再模块列表中搜索指定的模块名称,如果找到就返回基地址。注意WOW64需要转UNICODE
1 | PVOID SeGetModuleBaseByModuleName(IN PEPROCESS EProcess, IN PUNICODE_STRING ModuleName, IN BOOLEAN IsWow64) |
从目标模块获取目标函数导出地址
PE文件分析
1 | PVOID SeGetExportFunctionFromModule(IN PVOID ModuleBase, IN PCCHAR FunctionName, IN PEPROCESS EProcess, IN PUNICODE_STRING ModuleName) |
获取ShellCode
WOW64
1 | typedef struct _INJECTION_DATA |
x64
1 | PINJECTION_DATA GetNativeCode(IN PVOID LdrLoadDll, IN PUNICODE_STRING DllFullPath) |
开始APC注入
- 获取一个线程,取出EThread
- 插入线程的APC队列中
- 等待完成
1 | NTSTATUS ApcInjection(IN PINJECTION_DATA InjectionData, IN HANDLE TargetProcessID) |
获取活跃线程
- 获取进程的线程列表
- 过滤当前线程,并在SystemProcessInfo里取出一个线程ID获取EThread
1 | NTSTATUS SeLookupProcessThread(IN HANDLE ProcessID, OUT PETHREAD* EThread) |
插入目标线程APC队列
- 分配APC内存
- 初始化APC并分配执行和结束回调例程。APC回调例程中可以设置强制唤醒。执行例程就是之前的ShellCode
1 | NTSTATUS SeQueueUserApc(IN PETHREAD EThread,IN PVOID StartRoutine,IN PVOID Argument1,IN PVOID Argument2,IN PVOID Argument3, |
APC完成回调例程
一般情况
释放结构
1 | VOID KernelApcExcuteCallback( |
强制情况
强行唤醒线程执行APC,释放结构
1 | VOID KernelApcPrepareCallback( |
驱动卸载
1 | VOID DriverUnload(PDRIVER_OBJECT DriverObject) |
Ring3
主函数外的入口
- 输入进程ID
- 获取当前可执行文件的路径,并将DllName与路径拼接起来,得到完整的DLL路径
- 通过链接名打开设备对象
- 初始化结构,发送结构体和控制码
1 |
|
Ring3操作驱动方法类
1 | class CControlDevice |
获取Exe的完整名字
获取当前第一模块(可执行文件)路径,在路径中去掉文件名,再做一些转换
1 | wstring GetExeDirectory() |
1 | wstring GetParentDirectory(const std::wstring& BufferData) |
打开设备对象
通过链接对象获取驱动设备对象
1 | BOOL CControlDevice::SeOpenDeviceObject(WCHAR* DeviceLinkName) |
发送IO控制码
最重要的DeviceIoControl
1 | NTSTATUS CControlDevice::SeDeviceIoControl(PVOID BufferData,ULONG BufferLength,ULONG IoControlCode) |
32位区别基本仅在ShellCode只有32位一种。