在一次排查系統的驅動的目錄system32/driver目錄文件時,看到一個名字為ProcLaunchMon.sys的驅動
數字簽名微軟官方的
該驅動帶入的時間是2019?年?12?月?7?日,??17:08:33,文件描述是Time Travel Debugging Process Launch Monitor大致明白了就是一個調試工具實事記錄進程活動的驅動。微軟的這個官方頁面有講述了什么是Time Travel Debugging,
https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/time-travel-debugging-overview
本篇的文章的目的是主要研究這個驅動到底做了什么?
接下來就祭出逆向工具ida
微軟對這個模塊是有部分符號文件的,左側的Function windows可以看到他是用c++寫的驅動有一些類名顯示
主要的類名就是ProcessLaunchMonitorClient 、ProcessLaunchMonitorDevice、LegacyDevice
然后再看看導入表有哪些函數,函數并不多主要就兩類: 進程函數和事件函數
PsSetCreateProcessNotifyRoutine
ZwCreateEvent
PsSuspendProcess
PsResumeProcess
ZwDuplicateObject
等等
分析完表面這些明顯的地方,下面就從驅動的入口點分析
最開始的時候會構建一個device 類 LegacyDevice::LegacyDevice
LegacyDevice::LegacyDevice類的大小是0x38,因為驅動,所以一般用c++寫驅動的時候都會用動態內存分配全局結構,ProcessLaunchMonitorDevice::`vftable 是該類的基類,在驅動的虛表rdata節里可以看到該基類的定義
前面第一個是構造與析構函數,后面四個是IO 例程函數DispatchRoutine、DispatchCreate、DispatchClose、DispatchBufferedIoctl
內存構造結束了,就直接調用LegacyDevice::LegacyDevice的構造函數
首先會構造設備名字的字符串
然后創建設備和設備連接
注意該驅動使用的是
WdmlibIoCreateDeviceSecure來創建設備,該函數創建的設備是管理權限才能打開設備。
最后構造函數里會填充MajorFunction結構體,用StaticDispatchRoutine函數覆蓋。
memset64(a2->MajorFunction, (unsigned __int64)LegacyDevice::StaticDispatchRoutine, 0x1Cui64);
自此這個構造類結束。
構造了這個全局類LegacyDevice::LegacyDevice,后接下來就是調用注冊進程回調通知
回調函數是ProcessCreationNotifyRoutine
到這里入口函數就結束,其實過程很簡單,但是精華卻在例程函數里。
要控制利用驅動,首先我們必須CreateFile一個驅動,這是會進入驅動的IRP_MJ_CREATE例程,接下來看上面的提到的StaticDispatchRoutine函數
這個函數很簡單,可能很多人一下子看不懂,從設備信息中獲得DeviceExtension結構然后call v4 + 8的函數,這到底是什么呢,
秘密在剛才那個構造函數里。
在之前的LegacyDevice::LegacyDevice的構造函數里有這么句代碼
(_QWORD )(*DeviceObject)->DeviceExtension=this;
就是把LegacyDevice::LegacyDevice這個全局類的this指針賦值給DeviceObject)->DeviceExtension的結構中,現在明白了這個DeviceExtension里的值是什么了把,對他就是LegacyDevice::LegacyDevice的地址值,那么
((void (fastcall **)(int64, _QWORD ))((_QWORD )v4 + 8i64))(v4, v7);
的代碼的意思就是調用LegacyDevice::LegacyDevice的基類的第二個函數DispatchRoutine
繼續分析進入DispatchRoutine 函數
IRP_MJ_CREATE 會進入第一個函數
ProcessLaunchMonitorDevice::DispatchCreate(this, ((struct _FILE_OBJECT *)iocode + 6));
ProcessLaunchMonitorDevice::DispatchCreate函數里會為每一個Client創建該設備的對象生成一個ProcessLaunchMonitorClient::ProcessLaunchMonitorClient(v6, v7, &v15),該類的內存大小為0x70.
這個類的構造函數一個最主要的功能就是生成一個進程間通訊的Event,賦值給了
v3=(PVOID )((char )this + 56);的便宜的位置。
最后會把這個Process
賦值給了
ProcessLaunchMonitorDevice這個全局類的v10=((_QWORD )this + 6);的位置的LIST_ENTRY的鏈表里。以及當前設備的文件對象的FsContext的上下文里。
打開了設備之后,我們就要通過IRP_MJ_DEVICE_CONTROL IO控制碼是14的之類的去給驅動發IO命令,那么就會經過驅動的
然后再進入ProcessLaunchMonitorDevice::DispatchBufferedIoctl函數
首先該函數會先從之前的我們講的文件對象的FsContext結構中取出之前創建的ProcessLaunchMonitorClient的指針。
接下來會看到里面有一個IO code
Case : 0x224040 恢復進程
Case 0x224044 關閉 ProcessLaunchMonitorClient 并且清楚恢復所有被懸掛的進程
Case 0x2240048 獲取之前ProcessLaunchMonitorClient里創建的進程間通訊的事件。
等等
這些主要的IRP例程的函數大致的邏輯就分析完畢,那該驅動的進程回調函數有什么用呢?這個問題很好,下面就來分析回調通知里的邏輯
當一個進程啟動后就會進入該驅動回調通知
然后就會進入
ProcessLaunchMonitorDevice::HandleProcessCreatedOrDestroyed(gLegacyDevice::gLegacyDevice, Create !=0, v4, v6);
這個函數是主要的處理邏輯
然后就會進入
進入這個函數SuspendResumeProcessById(a4, v13)后就會把進程懸掛起來
可以看到
If(a2)
{
v5=PsSuspendProcess(Object);
}
如果a2這個參數是1的話,直接就懸掛了,然后外面給的參數就是1,那就是只要驅動功能起來了,就直接懸掛了新起來的進程(這個驅動太霸道了), 如果你不發之前我們看到那個IO: 0x224040的控制碼的話,它就一直被懸掛,起不來了,或者這個驅動被關閉也能自動恢復所有被懸掛的進程。
其他一些小的附加的結構體的處理不在具體分析,以上就是該驅動的最主要的功能,當然分析完畢就是驗證我們的分析結果。
主要代碼如下:(具體的iocode 我模糊處理了,避免被惡意亂用)
int main()
{
TCHAR szDriverKey[MAX_PATH]={ 0 };
GetSystemDirectory(szDriverKey,MAX_PATH);
StringCchCat(szDriverKey, MAX_PATH, _T("\\dirver\\ProcLaunchMon.sys"));
if (TRUE)
{
//install driver
LoadDriver(szDriverKey);
}
BOOL CheckServiceOk=FALSE;
if (SUCCEEDED(StringCchPrintf(
szDriverKey,
MAX_PATH,
_T("\\\\.\\%s"),
_T("com_microsoft_xxxxx_ProcLaunchMon")))) //
{
HANDLE hObjectDrv=CreateFile(
szDriverKey,
GENERIC_READ |
GENERIC_WRITE,
0,
0,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
0);
if (hObjectDrv !=INVALID_HANDLE_VALUE)
{
DWORD dwProcessId=-1;
BYTE OutBuffer[4002]={ 0 };
ULONG BytesReturned=0;
DWORD sendrequest=0xxabsdd;
if (DeviceIoControl(
hObjectDrv,
sendrequest,//,
&dwProcessId,
4,
&dwProcessId,
4,
&BytesReturned, 0))
{
ULONG64 Request=0;
sendrequest=0xa2XXXX;
ULONG64 KeyHandle=0;
if (DeviceIoControl(
hObjectDrv,
sendrequest,//
&Request,
8,
&KeyHandle,
8,
&BytesReturned, 0))
{
//Get the kernel process event and wait for the kernel setting event
if (WaitForSingleObject((HANDLE)KeyHandle, INFINITE)==WAIT_OBJECT_0)
{
// int nI=0;
// nI++;
}
while (TRUE)
{
sendrequest=0x125a3X;
typedef struct _GetPidBuffer
{
LARGE_INTEGER Pid;
ULONG32 Other;
}GetPidBuffer;
GetPidBuffer ProcessPid={0};
if (DeviceIoControl(
hObjectDrv,
sendrequest,
&Request,
8,
&ProcessPid,
12,
&BytesReturned,
0))
{
if (BytesReturned && ProcessPid.Pid.LowPart > 0)
{
printf("curent run pid:%d", ProcessPid.Pid.HighPart);
//If the process is not recovered, it will cause denial of service attack. For example,
//if it is security software, it will cause security software failure
sendrequest=0x224040;
if (DeviceIoControl(
hObjectDrv,
sendrequest,
&ProcessPid.Pid.HighPart,
8,
&ProcessPid.Pid.HighPart,
8,
&BytesReturned, 0))
{
}
Sleep(100);
BytesReturned=0;
}
else
{
if (WaitForSingleObject((HANDLE)KeyHandle, INFINITE)==WAIT_OBJECT_0)
{
}
}
}
}
}
}
CloseHandle(hObjectDrv);
hObjectDrv=NULL;
}
}
return 0;
}
1. 加載驅動成功
2. 打開設備成功
3. 開啟功能
后記: 這個曾經上報給微軟的msrc,對方承認是個有待改進的問題的驅動,但是并非是個漏洞,至今已經過去了四個月了,可以到了公布的時間了,如果一個惡意進程已經攻擊進入一個系統后,,并且已經有了管理員權限,他就可以利用這個驅動去控制安全軟件的啟動,甚至失效,這也是很危險的驅動。
IO (Input/Output,輸入/輸出)即數據的讀取(接收)或寫入(發送)操作,通常用戶進程中的一個完整IO分為兩階段:用戶進程空間<-->內核空間、內核空間<-->設備空間(磁盤、網絡等)。IO有內存IO、網絡IO和磁盤IO三種,通常我們說的IO指的是后兩者。
LINUX中進程無法直接操作I/O設備,其必須通過系統調用請求kernel來協助完成I/O動作;內核會為每個I/O設備維護一個緩沖區。
對于一個輸入操作來說,進程IO系統調用后,內核會先看緩沖區中有沒有相應的緩存數據,沒有的話再到設備中讀取,因為設備IO一般速度較慢,需要等待;內核緩沖區有數據則直接復制到進程空間。
所以,對于一個網絡輸入操作通常包括兩個不同階段:
(1)等待網絡數據到達網卡→讀取到內核緩沖區,數據準備好;
(2)從內核緩沖區復制數據到進程空間。
同步才有阻塞和非阻塞之分;
阻塞與非阻塞關乎如何對待事情產生的結果(阻塞:不等到想要的結果我就不走了)
理解進程的狀態轉換
從操作系統層面執行應用程序理解 IO 模型
當調用recv()函數時,系統首先查是否有準備好的數據。如果數據沒有準備好,那么系統就處于等待狀態。當數據準備好后,將數據從系統緩沖區復制到用戶空間,然后該函數返回。在套接應用程序中,當調用recv()函數時,未必用戶空間就已經存在數據,那么此時recv()函數就會處于等待狀態。
阻塞模式給網絡編程帶來了一個很大的問題,如在調用 send()的同時,線程將被阻塞,在此期間,線程將無法執行任何運算或響應任何的網絡請求。這給多客戶機、多業務邏輯的網絡編程帶來了挑戰。這時,我們可能會選擇多線程的方式來解決這個問題。
應對多客戶機的網絡應用,最簡單的解決方式是在服務器端使用多線程(或多進程)。多線程(或多進程)的目的是讓每個連接都擁有獨立的線程(或進程),這樣任何一個連接的阻塞都不會影響其他的連接。
具體使用多進程還是多線程,并沒有一個特定的模式。傳統意義上,進程的開銷要遠遠大于線程,所以,如果需要同時為較多的客戶機提供服務,則不推薦使用多進程;如果單個服務執行體需要消耗較多的 CPU 資源,譬如需要進行大規模或長時間的數據運算或文件訪問,則進程較為安全。
當用戶進程調用了select,那么整個進程會被block,而同時,kernel會“監視”所有select負責的socket,當任何一個socket中的數據準備好了,select就會返回。這個時候用戶進程再調用read操作,將數據從kernel拷貝到用戶進程。
所以,I/O 多路復用的特點是通過一種機制一個進程能同時等待多個文件描述符,而這些文件描述符(套接字描述符)其中的任意一個進入讀就緒狀態,select()函數就可以返回。
區別IO多路復用中的select poll epoll
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); select 函數監視的文件描述符分3類,分別是writefds、readfds、和exceptfds。調用后select函數會阻塞,直到有描述符就緒(有數據 可讀、可寫、或者有except),或者超時(timeout指定等待時間,如果立即返回設為null即可),函數返回。當select函數返回后,可以 通過遍歷fdset,來找到就緒的描述符
int poll (struct pollfd *fds, unsigned int nfds, int timeout); 不同與select使用三個位圖來表示三個fdset的方式,poll使用一個 pollfd的指針實現。pollfd并沒有最大數量限制(但是數量過大后性能也是會下降)。 和select函數一樣,poll返回后,需要輪詢pollfd來獲取就緒的描述符。
epoll是通過事件的就緒通知方式,調用epoll_create創建實例,調用epoll_ctl添加或刪除監控的文件描述符,調用epoll_wait阻塞住,直到有就緒的文件描述符,通過epoll_event參數返回就緒狀態的文件描述符和事件。
epoll操作過程需要三個接口,分別如下: int epoll_create(int size);//創建一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大 生成一個 epoll 專用的文件描述符,其實是申請一個內核空間,用來存放想關注的 socket fd 上是否發生以及發生了什么事件。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
控制某個 epoll 文件描述符上的事件:注冊、修改、刪除。其中參數 epfd 是 epoll_create() 創建 epoll 專用的文件描述符。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待 I/O 事件的發生;返回發生事件數。參數說明:
epfd: 由 epoll_create() 生成的 Epoll 專用的文件描述符;
epoll_event: 用于回傳代處理事件的數組;
maxevents: 每次能處理的事件數;
timeout: 等待 I/O 事件發生的超時值;
(1)select,poll實現需要自己不斷輪詢所有fd集合,直到設備就緒,期間可能要睡眠和喚醒多次交替。而epoll其實也需要調用epoll_wait不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒交替,但是它是設備就緒時,調用回調函數,把就緒fd放入就緒鏈表中,并喚醒在epoll_wait中進入睡眠的進程。雖然都要睡眠和交替,但是select和poll在“醒著”的時候要遍歷整個fd集合,而epoll在“醒著”的時候只要判斷一下就緒鏈表是否為空就行了,這節省了大量的CPU時間。這就是回調機制帶來的性能提升。
(2)select,poll每次調用都要把fd集合從用戶態往內核態拷貝一次,epoll 通過 mmap 把內核空間和用戶空間映射到同一塊內存,省去了拷貝的操作。
tornado 的 IOLoop 模塊 是異步機制的核心,它包含了一系列已經打開的文件描述符和每個描述符的處理器 (handlers)。這些 handlers 就是對 select, poll , epoll等的封裝。(所以本質上說是 IO 復用)
Linux后端服務器開發要學關于IO的哪些知識點
網絡IO是網絡通信的血管,數據是血液。血液的流動是不能離開血管的。