APC机制详解
文章目錄
- APC的本質
- APC隊列
- APC結構
- APC相關函數
- KiServiceExit
- KiDeliveApc
- 備用APC隊列
- ApcState的含義
- 掛靠環境下的ApcState的含義
- 其他APC相關成員
- ApcStatePointer
- ApcStateIndex
- ApcStatePointer與ApcStateIndex組合尋址
- ApcQueueable
- APC掛入過程
- KAPC結構
- 掛入流程
- KeInitializeApc
- ApcStateIndex
- KiInsertQueueApc
- 內核APC的執行過程
- 執行點:線程切換
- 執行點:系統調用 中斷或者異常(_KiServiceExit)
- KiDeliverApc函數分析
- 總結
- 用戶APC的執行過程
- 執行用戶APC時的堆棧操作
- KiDeliverApc函數分析
- KiInitializeUserApc函數分析:備份CONTEXT
- KiInitializeUserApc函數分析:堆棧圖
- KiInitializeUserApc函數分析:準備用戶層執行環境
- ntdll.KiUserApcDispatcher函數分析
- 總結
APC的本質
線程是不能被殺死 掛起和恢復的,線程在執行的時候自己占據著CPU,別人怎么可能控制他呢?舉個極端的例子,如果不調用API,屏蔽中斷,并保證代碼不出現異常,線程將永久占據CPU。所以說線程如果想結束,一定是自己執行代碼把自己殺死,不存在別人把線程結束的情況。
那如果想改變一個線程的行為該怎么辦?可以給他提供一個函數,讓他自己去調用,這個函數就是APC,即異步過程調用
APC隊列
我們現在需要討論是的,如果我給某一個線程提供一個函數,那么這個函數掛在哪里?答案是APC隊列,先來看一下當前線程的結構體
kd> dt _KTHREAD ntdll!_KTHREAD +0x040 ApcState : _KAPC_STATE線程結構體KTHREAD0x40的位置的成員是一個子結構體ApcState,也就是APC隊列
kd> dt _KAPC_STATE nt!_KAPC_STATE+0x000 ApcListHead //2個APC隊列 用戶APC和內核APC +0x010 Process //線程所屬進程或者所掛靠的進程+0x014 KernelApcInProgress //內核APC是否正在執行+0x015 KernelApcPending //是否有正在等待執行的內核APC+0x016 UserApcPending //是否有正在等待執行的用戶APC_KAPC_STATE的第一個成員是兩個APC隊列,每一個成員都是一個雙向鏈表,這個雙向鏈表就是APC隊列。
APC一共有兩個,一個是用戶態APC隊列,一個是內核態的APC隊列,里面存儲的都是APC函數。
你想讓線程執行某些操作的時候,就可以提供一個函數,掛到這個鏈表里,在某一個時刻,當前線程會檢查當前的函數列表,當里面有函數的時候,就會去調用。這樣就相當于改變了線程的行為。
現在我們知道了如果想改變線程的行為,需要提供一個函數掛到線程的APC隊列里,準確的說是提供一個APC,接下里需要了解APC的結構。
APC結構
kd> dt _KAPC ntdll!_KAPC+0x000 Type : UChar+0x001 SpareByte0 : UChar+0x002 Size : UChar+0x003 SpareByte1 : UChar+0x004 SpareLong0 : Uint4B+0x008 Thread : Ptr32 _KTHREAD+0x00c ApcListEntry : _LIST_ENTRY+0x014 KernelRoutine : Ptr32 void +0x018 RundownRoutine : Ptr32 void +0x01c NormalRoutine : Ptr32 void +0x020 NormalContext : Ptr32 Void+0x024 SystemArgument1 : Ptr32 Void+0x028 SystemArgument2 : Ptr32 Void+0x02c ApcStateIndex : Char+0x02d ApcMode : Char+0x02e Inserted : UChar其中最重要的是+0x01c NormalRoutine的這個成員,通過這個成員可以找到你提供的APC函數。
現在我們知道了提供APC需要遵循的格式,以及存到線程的位置,但是還有另外的問題,當前的線程什么時候會執行所提供的APC函數
如果想要解決這個問題,需要知道一個內核函數:KiServiceExit
APC相關函數
KiServiceExit
這個函數是系統調用 異常或中斷返回用戶空間的必經之路
KiDeliveApc
負責執行APC函數
備用APC隊列
kd> dt _KTHREAD ntdll!_KTHREAD +0x040 ApcState : _KAPC_STATE +0x170 SavedApcState : _KAPC_STATE在線程結構體0x40的位置是APC隊列,在0x170的位置也有一個APC隊列,這兩個成員的結構是完全一樣的
ApcState的含義
線程隊列中的APC函數都是與進程相關聯的,具體點說:A進程的T線程中所有的APC函數,要訪問的內存地址都是A進程的。
但線程是可以掛靠到其他的進程:比如A進程的線程T,通過修改CR3,就可以訪問B進程的地址空間,即所謂的進程掛靠。
當T線程掛靠B進程后,APC隊列中存儲的仍然是原來的APC。具體點說,比如某個APC函數要讀取地址為0x12345678的數據,如果此時進行讀取,讀到的將是B進程的地址空間,這樣邏輯就錯誤了。
為了避免混亂,在T線程掛靠B進程時,會將ApcState中的值暫時存儲到SavedApcState中,等回到原進程A時,再將APC隊列恢復
所以,SavedApcState又稱為備用APC隊列
掛靠環境下的ApcState的含義
在掛靠環境下,也是可以將線程APC隊列插入APC的,那這種情況下,使用的是哪個APC隊列呢?
A進程的T線程掛靠B進程,A是T的所屬進程,B是T的掛靠進程
- ApcState:B進程相關的APC函數
- SavedApcState:A進程相關的APC函數
在正常情況下,當前進程就是所屬進程A,如果是掛靠情況下,當前進程就是掛靠進程B
其他APC相關成員
ApcStatePointer
+0x168 ApcStatePointer : [2] Ptr32 _KAPC_STATE在KTHREAD結構體的0x168的位置的成員是一個指針數組,有兩個指針,每一個指針都指向一個ApcState
為了操作方便,KTHREAD結構體中定義了一個指針數組ApcStatePointer,長度為2。
正常情況下:
? ApcStatePointer[0]指向ApcState
? ApcStatePointer[1]指向SavedApcState
掛靠情況下:
? ApcStatePointer[0]指向SavedApcState
? ApcStatePointer[1]指向ApcState
ApcStateIndex
+0x134 ApcStateIndex : UCharApcStateIndex用來標識當前線程處于什么狀態:0正常狀態 1掛靠狀態
ApcStatePointer與ApcStateIndex組合尋址
正常情況下,向ApcState隊列插入APC時:
? ApcStatePointer[0]指向ApcState,此時ApcStateIndex的值為0
? ApcStatePointer[ApcStateIndex]指向ApcState
掛靠情況下,向ApcState隊列中插入APC時:
? ApcStatePointer[1]指向ApcState,此時ApcStateIndex的值為1
? ApcStatePointer[ApcStateIndex]指向ApcState
總結:
無論什么環境下,ApcStatePointer[ApcStateIndex]指向的都是ApcState,ApcState則總是表示線程當前使用的APC狀態
ApcQueueable
+0x0b8 ApcQueueable : Pos 5, 1 BitApcQueueable用于表示是否可以向線程的APC隊列中插入APC。
當線程正在執行退出的代碼時,會將這個值設置為0,如果此時執行插入APC的代碼,在插入函數中會判斷這個值的狀態,如果為0,則插入失敗。
APC掛入過程
無論是正常狀態還是掛靠狀態,都要有兩個APC隊列,一個內核隊列,一個用戶隊列。每當要掛入一個APC函數時,不管是用戶隊列還是內核隊列,內核都要準備一個KAPC的數據結構,并且將這個KAPC結構掛到相應的APC隊列中。
KAPC結構
kd> dt _KAPC nt!_KAPC+0x000 Type //類型 APC類型為0x12+0x002 Size //本結構體的大小 0x30+0x004 Spare0 //未使用 +0x008 Thread //目標線程 +0x00c ApcListEntry //APC隊列掛的位置+0x014 KernelRoutine //指向一個函數(調用ExFreePoolWithTag 釋放APC)+0x018 RundownRoutine//略 +0x01c NormalRoutine //用戶APC總入口 或者 真正的內核apc函數+0x020 NormalContext //內核APC:NULL 用戶APC:真正的APC函數+0x024 SystemArgument1//APC函數的參數 +0x028 SystemArgument2//APC函數的參數+0x02c ApcStateIndex //掛哪個隊列,有四個值:0 1 2 3+0x02d ApcMode //內核APC 用戶APC+0x02e Inserted //表示本apc是否已掛入隊列 掛入前:0 掛入后 1- Type :類型。在Windows里,任何一種內核對象都有一個編號,這個編號用來標識你是屬于哪一種類型,APC本身也是一種內核對象,它也有一個編號,是0x12
- Size:這個成員指的是當前的KAPC的結構體的大小
- Thread:每一個線程都有自己的APC隊列,這個成員指定了APC屬于哪一個線程
- ApcListEntry:APC隊列掛的位置,是一個雙向鏈表,通過這個雙向鏈表可以找到下一個APC
- KernelRoutine:指向一個函數(調用ExFreePoolWithTag 釋放APC)。當我們的APC執行完畢以后,當前的KAPC本身的這塊內存,會由KernelRoutine指定的函數來釋放
- NormalRoutine:如果當前是內核APC,通過這個值找到的就是真正的內核APC函數;如果當前的APC是用戶APC,那么這個位置指向的是用戶APC總入口,通過這個總入口可以找到所有用戶提供的APC函數
- NormalContext:如果當前是內核APC,通過這個值為空;如果當前的APC是用戶APC,那么這個值指向的是真正的用戶APC函數
- SystemArgument1 SystemArgument2 APC函數的參數
- ApcStateIndex:當前的APC要掛到哪個隊列
- ApcMode:當前的APC是用戶APC還是內核APC
- Inserted:當前的APC結構體是否已經插入到APC隊列
掛入流程
KeInitializeApc
VOID KeInitializeApc (IN PKAPC Apc,//KAPC指針IN PKTHREAD Thread,//目標線程IN KAPC_ENVIRONMENT TargetEnvironment,//0 1 2 3四種狀態IN PKKERNEL_ROUTINE KernelRoutine,//銷毀KAPC的函數地址IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL,IN PKNORMAL_ROUTINE NormalRoutine,//用戶APC總入口或者內核apc函數IN KPROCESSOR_MODE Mode,//要插入用戶apc隊列還是內核apc隊列IN PVOID Context//內核APC:NULL 用戶APC:真正的APC函數 )KeInitializeApc函數的作用就是給當前的KAPC結構體賦值
ApcStateIndex
與KTHREAD(+0x134)的屬性同名,但含義不一樣:
ApcStateIndex有四個值:
- 0 原始環境->插入到當前線程的所屬進程APC隊列,不管是否掛靠都插入到當前線程的所屬進程。
- 1 掛靠環境
- 2 當前環境->插入到當前進程的APC隊列,如果沒有掛靠,當前進程則是父進程,如果掛靠了,當前進程就是掛靠進程
- 3 插入APC時的當前環境->線程隨時處于切換狀態 當值為3時,在插入APC之前會判斷當前線程是否處于掛靠狀態 再進行APC插入
KiInsertQueueApc
內核APC的執行過程
APC函數的插入和執行并不是同一個線程,具體點說:
在A線程中向B線程插入一個APC,插入的動作是在A線程中完成的,但什么時候執行則由B線程決定。所以叫異步過程調用。
內核APC函數與用戶APC函數的執行時間和執行方式也有區別。我們先來了解內核APC的執行過程
執行點:線程切換
IDA打開ntkrnlpa,找到SwapContext函數
在這個函數即將執行完成的時候,會判斷當前是否有要執行的內核APC,接著將判斷的結果存到eax,然后返回
接著找到上一層函數KiSwapContext函數繼續跟進
這個函數也沒有對APC進行處理,而是繼續返回,繼續跟進父函數
返回到這里,會判斷KiSwapContext的返回值,也就是判斷當前是否有要處理的內核APC,如果有,則調用KiDeliverApc進行處理。
這個函數有三個參數,第一個參數如果是0,就意味著KiDeliverApc在執行的時候只會處理內核APC,第一個參數如果是1,KiDeliverApc除了處理內核APC以外,還會處理用戶APC
流程總結:
執行點:系統調用 中斷或者異常(_KiServiceExit)
找到_KiServiceExit函數,這里會判斷是否有要執行的用戶APC,如果有的話則會調用KiDeliverApc函數進行處理,此時KiDeliverApc第一個參數為1,代表執行用戶APC和內核APC。
當要執行用戶APC之前,先要執行內核APC
KiDeliverApc函數分析
無論是執行內核APC還是執行用戶APC都會調用KiDeliverApc函數,接下來分析KiDeliverApc函數主要做了什么事情
首先這里會取出內核APC列表,然后執行跳轉
接著判斷第一個鏈表是否為空(內核APC隊列),如果不為空則跳轉
跳轉以后,首先得到KACP的首地址,然后取出KACP結構體的各個參數,放到局部變量里
在這里,因為我們要處理的是內核APC,所以NormalRoutine代表是內核APC函數地址,這里會判斷內核APC函數地址是否為空,不為空的話則進行跳轉
跳轉以后,先判斷是否有正在執行內核APC,然后判斷是否禁用內核APC,接著將APC從內核隊列中摘除。
接著先調用KAPC.KernelRoutine指定的函數 釋放KAPC結構體占用的空間
然后將ApcState.KernelApcInProgress 設置為1 標識正在執行內核APC。
接著將三個參數壓入棧里,開始執行真正的內核APC函數
執行完畢以后,將ApcState.KernelApcInProgress 置0,接著再次判斷內核APC隊列,開始下一輪循環
內核APC執行流程總結:
總結
用戶APC的執行過程
當產生系統調用 中斷或者異常,線程在返回用戶空間前都會調用_KiServiceExit函數,在_KiServiceExit函數里會判斷是否有要執行的用戶APC,如果有則調用KiDeliverApc函數進行處理
執行用戶APC時的堆棧操作
處理用戶APC要比處理內核APC復雜的多,因為用戶APC函數要在用戶空間執行,這里涉及到大量的換棧操作:
當線程從用戶層進入內核層時,要保留原來的運行環境,比如各種寄存器 棧的位置等等,然后切換成內核的堆棧,如果正常返回,恢復堆棧環境即可
但如果有用戶APC要執行的話,就意味著線程要提前返回到用戶空間去執行,而且返回的位置不是線程進入內核時的位置,而是返回到真正執行APC的位置
每處理一個用戶APC就會涉及到:內核—>用戶空間—>再回到內核空間
執行用戶APC最為關鍵的就是理解堆棧操作的細節
KiDeliverApc函數分析
KiDeliverApc函數會push三個參數,第一個參數如果為0,代表只處理內核APC,如果為1,代表處理用戶APC和內核APC。
也就是說內核APC是無論如何都會執行的。
取出內核APC隊列之后會再次取出用戶APC隊列,并判斷用戶APC隊列是否為空
.text:00426063 cmp [ebp+arg_0], 1接著判斷KiDeliverApc第一個參數是否為1 如果不是1 說明不處理用戶APC,直接返回
.text:00426069 cmp byte ptr [esi+4Ah], 0 ;+0x4A=UserApcPending 表示是否正在執行用戶APC,為0說明正在執行的用戶APC,繼續往下走
.text:0042606F mov byte ptr [esi+4Ah], 0先將UserApcPending置0,表示當前正在執行用戶APC
.text:00426073 lea edi, [eax-0Ch]-0xC 得到KPCR首地址
.text:00426076 mov ecx, [edi+1Ch] ; +0x1C=NormalRoutine 用戶APC總入口 .text:00426079 mov ebx, [edi+14h] ; +0x14=KernelRoutine 釋放APC的函數 .text:0042607C mov [ebp+var_4], ecx .text:0042607F mov ecx, [edi+20h] ; +0x20 NormalContext 用戶APC:真正的APC函數 .text:00426082 mov [ebp+var_10], ecx .text:00426085 mov ecx, [edi+24h] ; +0x24 SystemArgument1 .text:00426088 mov [ebp+var_C], ecx .text:0042608B mov ecx, [edi+28h] ; SystemArgument2接著取出KAPC結構體的成員,放到局部變量里保存
.text:00426091 mov ecx, [eax] ; -------------------------- .text:00426093 mov eax, [eax+4] .text:00426096 mov [eax], ecx ; 鏈表操作 將用戶APC從鏈表中移除 .text:00426098 mov [ecx+4], eax ; --------------------------然后將當前的用戶APC從鏈表中摘除
.text:004260B7 push eax .text:004260B8 push edi .text:004260B9 call ebx ; 調用KAPC.KernelRoutine 釋放KAPC結構體內存接著調用調用KAPC.KernelRoutine指定的函數, 釋放KAPC結構體內存
到這里為止,用戶APC和內核APC的處理方式就發生了變化。
如果是內核APC這里會直接調用APC入口函數,執行內核APC,但是用戶APC執行的方式不一樣。當前的堆棧處于0環,而用戶APC需要在三環執行。
.text:004260CA push [ebp+var_8] .text:004260CD push [ebp+var_C] .text:004260D0 push [ebp+var_10] .text:004260D3 push [ebp+var_4] .text:004260D6 push [ebp+arg_8] .text:004260D9 push [ebp+arg_4] .text:004260DC call _KiInitializeUserApc接著這里調用了KiInitializeUserApc函數,接下來就要研究一下這個函數是如何實現的
用戶APC執行流程總結:
KiInitializeUserApc函數分析:備份CONTEXT
線程進0環時,原來的運行環境(寄存器棧頂等)保存到_Trap_Frame結構體中,如果要提前返回3環去處理用戶APC,就必須修改_Trap_Frame結構體,因為此時Trap_Frame中存儲的EIP是從三環進零環時保存的EIP,而不是用戶APC函數的地址
比如:進0環時的位置存儲在EIP中,現在要提前返回,而且返回的并不是原來的位置,那就意味著必須要修改EIP為新的返回位置,還有堆棧ESP也要修改為處理APC需要的堆棧。那原來的值怎么辦?處理完APC后該如何返回原來的位置呢?
KiInitializeUserApc要做的第一件事就是備份:
將原來_Trap_Frame的值備份到一個新的結構體中(CONTEXT),這個功能由其子函數KeContextFromKframes來完成
找到KiInitializeUserApc函數,首先調用了KeContextFromKframes,將Trap_Frame備份到Context
第一個參數ebx是Trap_Frame結構體首地址,第三個參數ecx是CONTEXT結構體首地址
那么問題在于CONTEXT結構體存到哪?肯定不能存到當前函數的局部變量里。Windows想了一個辦法,把這個結構體和APC需要的參數,直接存到三環的堆棧里
KiInitializeUserApc函數分析:堆棧圖
.text:00429EFC mov esi, [ebp+var_224] ; 2E8-224=C4 剛好是CONTEXT結構體ESP的偏移 .text:00429F02 and esi, 0FFFFFFFCh ; 進行4字節對齊 .text:00429F05 sub esi, eax ; 在0環直接修改3環的棧 將用戶3環的棧減0x2DC個字節首先esi是CONTEXT結構體里ESP的偏移,也就是三環的堆棧,然后進行4字節對齊。
接著將用戶3環的棧減0x2DC個字節,此時三環的堆棧被拉伸,為什么是2DC個字節呢?
因為CONTEXT結構體的大小加上用戶APC所需要的4個參數正好是2DC個字節,如下圖:
.text:00429F16 lea edi, [esi+10h]此時的esi指向的是-2DC的位置,也就是上圖的NormalRoutine,+10降低堆棧,將指針指向SystemArgument2
.text:00429F19 mov ecx, 0B3h .text:00429F1E lea esi, [ebp+var_2E8] .text:00429F24 rep movsd這幾行代碼將CONTEXT復制到了三環的堆棧
.text:00429FAC push 4 .text:00429FAE pop ecx .text:00429FAF add eax, ecx .text:00429FB1 mov [ebp+var_2EC], eax .text:00429FB7 mov edx, [ebp+arg_C] .text:00429FBA mov [eax], edx .text:00429FBC add eax, ecx .text:00429FBE mov [ebp+var_2EC], eax .text:00429FC4 mov edx, [ebp+arg_10] .text:00429FC7 mov [eax], edx .text:00429FC9 add eax, ecx .text:00429FCB mov [ebp+var_2EC], eax .text:00429FD1 mov edx, [ebp+arg_14] .text:00429FD4 mov [eax], edx .text:00429FD6 add eax, ecx .text:00429FD8 mov [ebp+var_2EC], eax ; 修正3環堆棧棧頂接著這幾行代碼就是將APC函數執行時需要的4個值壓入到3環的堆棧
KiInitializeUserApc函數分析:準備用戶層執行環境
當KiInitializeUserApc將CONTEXT和執行用戶APC所需要的4個值備份到3環的堆棧時,就開始準備用戶層的執行環境了
.text:00429F2D push 23h .text:00429F2F pop eax ; eax=0x23 .text:00429F30 mov [ebx+78h], eax ; 修改Trap_Frame中的SS .text:00429F33 mov [ebx+38h], eax ; 修改Trap_Frame中的DS .text:00429F36 mov [ebx+34h], eax ; 修改Trap_Frame中的ES .text:00429F39 mov dword ptr [ebx+50h], 3Bh ; 修改Trap_Frame中的FS .text:00429F40 and dword ptr [ebx+30h], 0 ; 修改Trap_Frame中的GS首先修改段寄存器 SS DS FS GS
.text:00429F78 mov [ebx+70h], eax ; 修改Trap_Frame中的EFLAGS接著修改EFLAGS寄存器
.text:00429F97 mov [ebx+74h], eax ; 修改Trap_Frame中的ESP .text:00429F9A mov ecx, _KeUserApcDispatcher .text:00429FA0 mov [ebx+68h], ecx ; 修改Trap_Frame中的EIP然后修改ESP和EIP。這個EIP就是執行用戶APC時返回到3環的位置。
這個位置是固定的,是一個全局變量:KeUserApcDispatcher。這個值在系統啟動的時候已經賦值好了,是3環的一個函數:ntdll.KiUserApcDispatcher
然后回到3環,由KiUserApcDispatcher執行用戶APC
總結:
ntdll.KiUserApcDispatcher函數分析
找到KiUserApcDispatcher函數,結合上面的堆棧圖我們可以得知,esp+0x10的位置就是CONTEXT指針
此時的ESP指向的是NormalRoutine,pop eax將NormalRoutine賦值給了eax,然后call eax開始處理用戶APC的總入口
處理完用戶的APC函數之后,會調用ZwContinue,這個函數的意義在于:
總結
總結
- 上一篇: 进程线程007 进程挂靠与跨进程读写内存
- 下一篇: CPU和软件模拟异常的执行流程