初识APC

零、

(1)、APC是一个线程提供给另一个线程一个函数让它去调用

(2)、同步与异步概念

同步与异步

当您同步执行某项任务时,您将等待它完成,然后再转到另一项任务。当您异步执行某项任

务时,您可以在它完成之前转移到另一个任务。

(3)、海哥提的几个小问题

1、谁插入的APC:

别的线程

2、插入到哪里

两个双向链表(在_KAPC_STATE结构体里)

3、谁执行的APC

自己这个线程

4、什么时候执行的

执行KiInsertQueueApc()函数的时候   //以我目前的知识得到的理解是这样的//

一、分析TerminateThread/SuspendThread是如何实现的(从三环开始分析)  //我只分析了TerminateThread函数//

(1)、

 

 

 调用号为0x102h

(2)、KeInitializeApc()函数是NtTerminateThread里面的函数,初始化APC

第一个参数是ExAllocatePoolWithTag函数的返回值,即开辟的一块空间的初始地址
第二个参数是上上个函数传进来的ThreadHandle,是线程结构体(一开始我还以为是句柄号,后来我见这个参数在ObReferenceObjectByHandle()函数中做了两个参数,我才意识到,这个句柄号可能在这个函数里面被转化成线程结构体了)
第三个参数填的0,不知道是什么
第四个参数,PsExitSpecialApc函数地址(应该是判断是否为特殊APC)
第五个参数,ExFreeCallBack函数地址
第六个参数,PspExitNormalApc函数地址(应该是判断是否为普通APC)
第七个参数,填的0,不知道是什么
第八个参数,返回码

 

 

 结合部分伪代码:

 

 

 

 

 

 再结合_KAPC结构体

 

 

 

 

 

 可以发现这个KeInitializeApc函数就是在构造_KAPC结构体

(3)、分析 KeInsertQueueApc()函数,该函数执行插入APC的操作

(4)、KeInsertQueueApc()函数里面有一个KiInsertQueueApc函数执行真正的插入APC函数

char __fastcall KiInsertQueueApc(int a1, int a2)
第一个参数是_KAPC结构体
第二个参数写死为2

这个函数的逆向分析在下面,与第三个任务的一部分重了

二、自己编写代码向某个线程中插入一个用户APC

DWORD QueueUserAPC(

  [in] PAPCFUNC  pfnAPC,  //APC函数地址

  [in] HANDLE    hThread,   //线程//

  [in] ULONG_PTR dwData  //APC函数的参数

);

// 使用API插入APC.cpp : Defines the entry point for the console application. // //PAPCFUNC 回调函数 参考链接:// //https://docs.microsoft.com/en-us/windows/win32/api/winnt/nc-winnt-papcfunc// #include "stdafx.h" #include <windows.h> void InsertApcQue(); void __stdcall Papcfunc( ULONG_PTR);// HANDLE hThread; DWORD WINAPI ThreadProc( LPVOID lpParameter) { while(1) { printf("等待插入APC...\n"); SleepEx(1000,TRUE); } return 0; } int main(int argc, char* argv[]) { hThread = CreateThread(0,0,(unsigned long (__stdcall *)(void *))ThreadProc,0,0,0); if(!hThread) { printf("CreateThread错误.错误码是%d\n",GetLastError()); } Sleep(3000); InsertApcQue(); getchar(); return 0; } void InsertApcQue() { if(!QueueUserAPC((PAPCFUNC)Papcfunc,hThread,NULL)) { printf("QueueUserAPC错误码是:%d\n",GetLastError()); } } void __stdcall Papcfunc(ULONG_PTR ) { int i = 3; while(i--) { printf("插入APC成功\n"); } }

实验结果如下:

 

 

 

 

三、自己分析APC的插入过程

我逆向了一手QueueUserAPC函数

(1)、

QueueUserAPC函数功能是将用户模式异步过程调用(APC) 对象添加到指定线程的 APC 队列。
一、 DWORD __stdcall QueueUserAPC(PAPCFUNC pfnAPC, HANDLE hThread, ULONG_PTR dwData)
第一个参数是插入的APC函数的指针
第二个参数是线程句柄
第三个参数是传给APC函数的参数
QueueUserAPC调用了NtQueueUserAPC函数

先找到ZwQueueUserAPC

 

不知道为啥,这里都写的KiSystemService
(2)、 进入ntoskrnl.exe,找NtQueueApcThread函数(这里省略了)

NTSTATUS __stdcall NtQueueApcThread(HANDLE ThreadHandle, PKNORMAL_ROUTINE ApcRoutine, PVOID NormalContext, PVOID SystemArgument1, PVOID SystemArgument2)
第一个参数为线程结构体句柄
第二个参数为BaseDispatchAPC(3环函数)用户APC总入口
第三个参数是APC函数的函数地址
第四个参数是APCC函数的参数
第五个参数未知,好像也是要传递的参数 参考 https://repnz.github.io/posts/apc/user-apc/ ,作者还说,在x64下有隐藏属性

(3)、 插入APC的函数在KeInsertQueueApc种
char __stdcall KeInsertQueueApc(_DWORD *a1, int a2, int a3, int a4)
第一个参数是构造的_KAPC结构体地址,
第二个参数是上一个函数传递的参数(上一个函数的倒数第二个参数)
第三个也是上一个函数传递的参数(上一个函数的倒数第一个参数)
第四个是0



(4)、 真正执行插入APC的是KiInsertQueueApc函数

char __fastcall KiInsertQueueApc(int a1, int a2)
第一个参数是构造的_KAPC结构体地址,
第二个参数是上一个函数的第四个参数,即0

这里我只是对部分必要代码进行了分析:

.text:0040E379 8B FF mov edi, edi .text:0040E37B 55 push ebp .text:0040E37C 8B EC mov ebp, esp .text:0040E37E 51 push ecx .text:0040E37F 8B C1 mov eax, ecx ; ecx,eax : 构造的_KAPC结构体 .text:0040E381 80 78 2E 00 cmp [eax+_KAPC.Inserted], 0 .text:0040E385 8B 48 08 mov ecx, [eax+_KAPC.Thread] ; ecx,KTHREAD .text:0040E388 89 55 FC mov [ebp-4], edx .text:0040E38B 0F 85 D4 86 03 00 jnz loc_446A65 .text:0040E391 80 78 2C 03 cmp [eax+_KAPC.ApcStateIndex], 3 ; ApcStateIndex 显示当前状态 什么状态:0:正常1:挂靠状态 .text:0040E391 ; .text:0040E391 ; 与 KTHREAD(+0x165) 的属性同名,但含义不一样: .text:0040E391 ; ApcStateIndex 有四个值: .text:0040E391 ; 0:原始环境; 1:挂靠环境 2:当前环境 3:插入APC时的当前环境 .text:0040E391 ; 正常情况下:ApcStatePointer[0] 指向 ApcState;ApcStatePointer[1] 指向 SavedApcState .text:0040E391 ; 挂靠情况下:ApcStatePointer[0] 指向 SavedApcState;ApcStatePointer[1] 指向 ApcState .text:0040E395 0F 84 CE 86 03 00 jz loc_446A69 .text:0040E39B .text:0040E39B loc_40E39B: ; CODE XREF: KiInsertQueueApc(x,x)+386F9↓j .text:0040E39B 83 78 1C 00 cmp [eax+KAPC.NormalRoutine], 0 ; 指向我们所提供的APC函数 .text:0040E39F 0F BE 50 2C movsx edx, [eax+KAPC.ApcStateIndex] .text:0040E3A3 53 push ebx .text:0040E3A4 56 push esi .text:0040E3A5 57 push edi .text:0040E3A6 8B BC 91 38 01 00 00 mov edi, [ecx+edx*4+138h] ; ApcStatePointer 结构体的领域,指向ApcState ,一般来说ApcStatePointer[ApcStateIndex]指向ApcState .text:0040E3AD 8A 50 2D mov dl, [eax+KAPC.ApcMode] ; 内核APC置0,用户APC置1 .text:0040E3B0 0F 85 7E 0C 00 00 jnz loc_40F034 ; 如果NormalRoutine指向的APC函数不为NULL,则跳转,这里我们直接跳转,只分析跳转后的代码
跳转到这里:
text:0040F034 loc_40F034: ; CODE XREF: KiInsertQueueApc(x,x)+37↑j .text:0040F034 84 D2 test dl, dl .text:0040F036 74 0D jz short loc_40F045 ; 如果是内核APC,跳转 .text:0040F038 81 78 14 DE B7 4A 00 cmp [eax+KAPC.KernelRoutine], offset _PsExitSpecialApc@20 ; PsExitSpecialApc(x,x,x,x,x) .text:0040F03F 0F 84 EB 78 01 00 jz loc_426930 ; 如果是特殊APC,跳转(特殊APC > 内核APC > 用户APC) .text:0040F045 .text:0040F045 loc_40F045: ; CODE XREF: KiInsertQueueApc(x,x)+CBD↑j .text:0040F045 0F BE DA movsx ebx, dl ; 这里处理用户APC和内核APC .text:0040F048 8D 3C DF lea edi, [edi+ebx*8] ; edi是APCState结构体,如果是用户APC,ebx为1,走APCState 的第二个双向链表,反之,走第一个 .text:0040F04B 8B 5F 04 mov ebx, [edi+4] ; edi指向APCstate的双向链表的Flink的地址... .text:0040F04B ; ebx = APCState._LIST_ENTRY.Blink .text:0040F04E 8D 70 0C lea esi, [eax+KAPC.ApcListEntry] ; esi = KAPC.ApcListEntry.Flink 我记的这KAPC是构造的..而且当时也没有给他的0xc成员初始化... .text:0040F051 89 3E mov [esi], edi ; KAPC.ApcListEntry.Flink = &ApcState.LIST_ENTRY.Flink .text:0040F053 89 5E 04 mov [esi+4], ebx ; KAPC.ApcListEntry.Blink = ApcState.LIST_ENTRY.Blink .text:0040F053 ; 上面这两个就好像在给KAPC初始化一样 .text:0040F056 89 33 mov [ebx], esi ; APCState._LIST_ENTRY.Blink.Flink = &KAPC.ApcListEntry.Flink .text:0040F058 89 77 04 mov [edi+4], esi ; ApcState.LIST_ENTRY.Blink = &KAPC.ApcListEntry.Flink .text:0040F05B E9 76 F3 FF FF jmp loc_40E3D6 ; 以上命令执行后正好插入了一个APC队列 .text:0040F060 ; ---------------------------------------------------------------------------

其中APC的插入在这里执行:

 

过程就像下图所示

 

 

 

 

 

 

 

 

xt:0040E3D6 0F BE 78 2C movsx edi, [eax+KAPC.ApcStateIndex] .text:0040E3DA C6 40 2E 01 mov [eax+KAPC.Inserted], 1 ; 当前的APC结构体是否已经插入到APC队列 .text:0040E3DE 0F B6 B1 65 01 00 00 movzx esi, [ecx+_ETHREAD.Tcb.ApcStateIndex] .text:0040E3E5 3B FE cmp edi, esi .text:0040E3E7 5F pop edi .text:0040E3E8 5E pop esi .text:0040E3E9 5B pop ebx .text:0040E3EA 75 1C jnz short loc_40E408 ; 判断ETHREAD与KAPC的ApcStateIndex是否相等 .text:0040E3EC 84 D2 test dl, dl .text:0040E3EE 0F 85 6C 0C 00 00 jnz loc_40F060 ; 用户APC跳走 .text:0040E3F4 8A 51 2D mov dl, [ecx+_KTHREAD.State] ; 1就绪,2执行 .text:0040E3F7 80 FA 02 cmp dl, 2 .text:0040E3FA C6 41 49 01 mov [ecx+_KTHREAD.ApcState.KernelApcPending], 1 ; 是否有正在等待执行的内核APC函数存在置为 1 ,不存在为 0. .text:0040E3FE 75 76 jnz short loc_40E476 .text:0040E400 B1 01 mov cl, 1 .text:0040E402 FF 15 80 06 40 00 call ds:__imp_@HalRequestSoftwareInterrupt@4 ; HalRequestSoftwareInterrupt(x) .text:0040E408 .text:0040E408 loc_40E408: ; CODE XREF: KiInsertQueueApc(x,x)+71↑j .text:0040E408 ; KiInsertQueueApc(x,x)+100↓j ... .text:0040E408 B0 01 mov al, 1 .text:0040E40A C9 leave .text:0040E40B C3 retn .text:0040E40B @KiInsertQueueApc@8 endp

(5)、插入APC的核心代码就是那个链表的插入...

 

参考:

APC与DPC

https://osfva.com/20210810000027-windows_dpc_apc%E7%9A%84%E4%BD%9C%E7%94%A8%E5%92%8C%E5%8C%BA%E5%88%AB/

 

屏蔽级别:

https://zh.wikipedia.org/wiki/IRQL

 

APC的API介绍

https://zh.wikipedia.org/wiki/%E5%BC%82%E6%AD%A5%E8%BF%87%E7%A8%8B%E8%B0%83%E7%94%A8

 

SleepEx函数

挂起当前线程,直到满足指定条件。发生以下情况之一时,将恢复执行:

  • 调用 I/O 完成回调函数。
  • 异步过程调用 (APC) 排队等待线程。
  • 超时间隔已过。
  • DWORD SleepEx(
  •   [in] DWORD dwMilliseconds,
  •   [in] BOOL  bAlertable
  • );

链接:https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-sleepex

https://blog.csdn.net/b2292486308/article/details/72832203

 

PS:

在THREAD偏移0x248的位置对应了线程的九种状态

   +0x248 CrossThreadFlags : Uint4B

   +0x248 Terminated       : Pos 0, 1 Bit

   +0x248 DeadThread       : Pos 1, 1 Bit

   +0x248 HideFromDebugger : Pos 2, 1 Bit

   +0x248 ActiveImpersonationInfo : Pos 3, 1 Bit

   +0x248 SystemThread     : Pos 4, 1 Bit

   +0x248 HardErrorsAreDisabled : Pos 5, 1 Bit

   +0x248 BreakOnTermination : Pos 6, 1 Bit

   +0x248 SkipCreationMsg  : Pos 7, 1 Bit

   +0x248 SkipTerminationMsg : Pos 8, 1 Bit

 

 在逆向中遇到了一些函数或结构体:

1、

CONTAINING_RECORD这个宏我们之前使用过,就是通过结构体的对象的成员地址与成员名来获取对象的基址

2、

KeAcquireInStackQueuedSpinLockRaiseToSynch这个函数来自HAL

HAL,硬件抽象层,不知道是干什么的

那么KeReleaseInStackQueuedSpinLock应该就是释放排队自旋锁

 


__EOF__

本文作者_TLSN
本文链接https://www.cnblogs.com/lordtianqiyi/articles/15747190.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   TLSN  阅读(260)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示