备用APC与分析 NtReadVirtualMemory 在挂靠时如何备份与恢复Apc队列
零、ApcStatePointer与ApcStateIndex组合寻址
一、
函数分析流程为:NtReadVirtualMemory->MmCopyVirtualMemory->MiDoMappedCopy->KeStackAttachProcess->KiAttachProcess
KiAttachProcess应该才是真正执行进程挂靠的函数
二、KiAttachProcess函数
int __stdcall KiAttachProcess(int a1, int a2, char a3, int a4)
a1 :KTHREAD结构体
a2:KPROCESS结构体
a3: KeRaiseIrqlToDpcLevel()函数的返回值。不知道这个函数是干嘛的,应该是提权到Dpc权限
a4:结合后文应该是_KAPC_STATE 结构体(SavedApcState)
.text:00413166 8B FF mov edi, edi .text:00413168 55 push ebp .text:00413169 8B EC mov ebp, esp .text:0041316B 53 push ebx .text:0041316C 8B 5D 0C mov ebx, [ebp+arg_4] ; ebx :KPROCESS .text:0041316F 66 FF 43 60 inc word ptr [ebx+60h] .text:00413173 56 push esi .text:00413174 8B 75 08 mov esi, [ebp+arg_0] ; esi : KTHREAD .text:00413177 57 push edi .text:00413178 FF 75 14 push [ebp+arg_C] ; 按K显示数字 .text:0041317B 8D 7E 34 lea edi, [esi+_ETHREAD.Tcb.ApcState] ; edi : &KTHREAD.ApcState .text:0041317B ; _KAPC_STATE结构体 .text:0041317E 57 push edi .text:0041317F E8 71 00 00 00 call _KiMoveApcState@8 ; // KiMoveApcState 的功能是将参数1的APC队列复制一份到参数2 .text:0041317F ; // 这里这么做的原因是 attach 前要把父进程的 APC 队列保存起来,保存到 SavedApcState .text:0041317F ; . .text:0041317F ; . .text:00413184 89 7F 04 mov [edi+4], edi ; KTHREAD._KAPC_STATE.ApcListHead[0].Blink = &KTHREAD._KAPC_STATE.ApcListHead[0].Flink .text:00413187 89 3F mov [edi], edi ; KTHREAD._KAPC_STATE.ApcListHead[0].Flink = &KTHREAD._KAPC_STATE.ApcListHead[0].Flink .text:00413187 ; . .text:00413189 8D 46 3C lea eax, [esi+(_KTHREAD.ApcState.ApcListHead.Flink+8)] ; eax = &KTHREAD._KAPC_STATE.ApcListHead[1].Flink .text:0041318C 89 40 04 mov [eax+4], eax ; . .text:0041318C ; KTHREAD._KAPC_STATE.ApcListHead[1].Blink = &KTHREAD._KAPC_STATE.ApcListHead[1].Flink .text:0041318F 89 00 mov [eax], eax ; KTHREAD._KAPC_STATE.ApcListHead[1].Flink = &KTHREAD._KAPC_STATE.ApcListHead[1].Flink .text:0041318F ; 功能就是让表头指向自己 .text:0041318F ; . .text:0041318F ; . .text:00413191 8D 86 4C 01 00 00 lea eax, [esi+_KTHREAD.SavedApcState] ; eax: &KTHREAD.SavedApcState[0].Flink .text:00413197 39 45 14 cmp [ebp+arg_C], eax ; 当时进入KiAttachProcess这个函数之前, .text:00413197 ; KeStackAttachProcess函数有个判断,判定如果不是挂靠状态,第四 .text:00413197 ; 个参数传进来的是SavedApcState .text:00413197 ; . .text:00413197 ; . .text:0041319A 89 5E 44 mov [esi+_KTHREAD.ApcState.Process], ebx ; KTHREAD.ApcState.EPROCESS = EPROCESS(NEXT),这个EPROCESS是我们要挂靠的EPROCESS .text:0041319D C6 46 48 00 mov [esi+_KTHREAD.ApcState.KernelApcInProgress], 0 ; +0x014 KernelApcInProgress : UChar //内核APC是否正在执行// .text:0041319D ; +0x015 KernelApcPending : UChar //是否有正在等待执行的内核APC// .text:0041319D ; +0x016 UserApcPending : UChar //是否有正在等待执行的用户APC// .text:0041319D ; . .text:0041319D ; . .text:004131A1 C6 46 49 00 mov [esi+_KTHREAD.ApcState.KernelApcPending], 0 .text:004131A5 C6 46 4A 00 mov [esi+_KTHREAD.ApcState.UserApcPending], 0 ; 把他们全部赋值为0,说明我们不再管那些正在等待执行的用户或内核APC,不再管内核APC是否在执行 .text:004131A9 75 13 jnz short loc_4131BE .text:004131AB 89 86 38 01 00 00 mov [esi+_KTHREAD.ApcStatePointer], eax ; ApcStatePointer[0] = &KTHREAD.SavedApcState[0].Flink .text:004131AB ; ApcStatePointer[1] = &KTHREAD._KAPC_STATE.ApcListHead[0].Flink .text:004131AB ; . .text:004131AB ; . .text:004131B1 89 BE 3C 01 00 00 mov [esi+(_KTHREAD.ApcStatePointer+4)], edi .text:004131B7 C6 86 65 01 00 00 01 mov [esi+_KTHREAD.ApcStateIndex], 1 ; .//表示现在已经 attach .text:004131B7 ; 看这里这个ApcStateIndex被赋值为1,结合上面 .text:004131B7 ; 的ApcStatePointer正好验证了ApctatePointer[ApcStateIndex] .text:004131B7 ; 永远指向ApcState的结论 .text:004131B7 ; 正常情况下,ApcStatePointer[0]指向ApcState .text:004131B7 ; ,ApcStatePointer[1]指向SavedApcState,挂靠情况下,反之 .text:004131BE .text:004131BE loc_4131BE: ; CODE XREF: KiAttachProcess(x,x,x,x)+43↑j .text:004131BE 80 7B 65 00 cmp [ebx+_EPROCESS.Pcb.State], 0 ; ebx :eprocess .text:004131BE ; KPROCESS.state 8种进程状态,0代表运行 .text:004131C2 0F 85 68 2D 03 00 jnz loc_445F30 .text:004131C8 8D 73 40 lea esi, [ebx+_EPROCESS.Pcb.ReadyListHead] ; &KPROCESS.ReadyListHead.Flink 记录该进程中处于就绪状态但尚未被加入全局就绪链表的线程. .text:004131CB .text:004131CB loc_4131CB: ; CODE XREF: KiAttachProcess(x,x,x,x)+32DC0↓j .text:004131CB 8B 06 mov eax, [esi] ; eax = KPROCESS.ReadyListHead.Flink .text:004131CD 3B C6 cmp eax, esi ; 如果没有处于就绪状态但尚未被加入全局就绪链表的线程就停止循环 .text:004131CF 0F 85 38 2D 03 00 jnz loc_445F0D .text:004131D5 8B 45 14 mov eax, [ebp+arg_C] .text:004131D8 FF 70 10 push dword ptr [eax+10h] .text:004131DB 53 push ebx .text:004131DC E8 E3 18 FF FF call _KiSwapProcess@8 ; 进程的切换(CR3的切换) .text:004131E1 8A 4D 10 mov cl, [ebp+arg_8] .text:004131E4 E8 93 15 FF FF call @KiUnlockDispatcherDatabase@4 ; KiUnlockDispatcherDatabase(x) .text:004131E9
三、挂靠前的备份:
在这里有一个KiMoveApcState(a1,a2)这样的函数,作用是将参数1的值备份到参数2
至于恢复:
在 MiDoMappedCopy 函数下有一个 KeUnstackDetachProcess 函数(a1),在这个函数里面,同样有KiMoveApcState函数:
恢复APC队列
四、挂靠的实现:
在这里KTHREAD.APC.EPROCESS的值改变
这个函数执行进程的切换
五、下面有一些小问题还未解决
在逆向的时候我发现:
在KeStackAttachProcess 下有一些代码:
.text:0041A5A3 8B FF mov edi, edi .text:0041A5A5 55 push ebp .text:0041A5A6 8B EC mov ebp, esp .text:0041A5A8 56 push esi .text:0041A5A9 57 push edi .text:0041A5AA 64 A1 24 01 00 00 mov eax, large fs:124h ; KTHREAD .text:0041A5B0 8B F0 mov esi, eax ; esi :KTHREAD .text:0041A5B2 64 A1 94 09 00 00 mov eax, large fs:994h ; KPCR 偏移0x994的位置...是DpcRoutineActive 不知道干啥的 .text:0041A5B2 ; 查资料发现这条if语句的意思是:如果当前线程正在执行 DPC,则 .text:0041A5B2 ; 蓝屏。 .text:0041A5B8 85 C0 test eax, eax .text:0041A5BA 0F 85 EF B9 02 00 jnz loc_445FAF .text:0041A5C0 8B 7D 08 mov edi, [ebp+BugCheckParameter1] .text:0041A5C3 39 7E 44 cmp [esi+44h], edi ; 这条If语句的意思是:如果尝试 attach 自己,那么设置ApcState->Process 等于1 .text:0041A5C6 74 34 jz short loc_41A5FC .text:0041A5C8 FF 15 94 06 40 00 call ds:__imp__KeRaiseIrqlToDpcLevel@0 ; KeRaiseIrqlToDpcLevel() .text:0041A5CE 80 BE 65 01 00 00 00 cmp byte ptr [esi+165h], 0 ; esi是Thread 判断是否已经属于挂靠状态...//考虑二重挂靠了属于是 .text:0041A5CE ; if (Thread->ApcStateIndex != 0) { .text:0041A5CE ; // 当前线程已经 attach 了一个进程 .text:0041A5CE ; KiAttachProcess(Thread, Process, OldIrql, ApcState); .text:0041A5CE ; .text:0041A5CE ; } else { .text:0041A5CE ; // 当前线程的所属进程就是创建线程的进程 .text:0041A5CE ; KiAttachProcess(Thread, Process, OldIrql, &Thread->SavedApcState); .text:0041A5CE ; ApcState->Process = NULL; .text:0041A5CE ; } .text:0041A5D5 88 45 08 mov byte ptr [ebp+BugCheckParameter1], al
在这里,考虑了三种情况
1.attach自己的进程
2.已经处于挂靠环境了,还要再挂靠,即多次挂靠(别的进程)
3.第一次挂靠(别的进程)
当处于第一种情况时,不执行KiAttachProcess函数
当初于第三种情况时,执行下面代码,最后一个参数直接传递的SaveApcState
当处于第二种情况时,执行:
传递的a2其实就是要接受的SavedApcState...
也就是说上面的分析只是基于第二种情况的分析,第三种和第一种情况的还原代码还没有分析到.......
留个小坑
PS:进程的8种状态:
一些函数的功能:
1.ObDereferenceObject : ObDereferenceObject 返回的EPROCESS会被引用一次,进行解引用.
2.ExAcquireRundownProtection :确保进程还活着
__EOF__

本文链接:https://www.cnblogs.com/lordtianqiyi/articles/15751048.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现