如何實現對打印的監控,微軟提出的一種解決方案就是時刻檢測放到打印隊列中的打印任務,發現有任務出現,就從中篩選出來提供給調用者。對此功能的實現,微軟的確公開了一套完整的代碼,並且能夠實現我們基本想要的功能,但是在實現功能之餘,我又進行了更深一層的研究和測試,通過Hook win32k.sys內的打印相關的4個函數就完美地實現了打印監控功能。
我們先分析一下當系統完成一次打印任務需要調用的幾個核心函數(對我們的程序有重要作用的),如表1所示。
|
Gdi32.dll |
Win32k.sys |
開始打印任務 |
StartDoc |
NtGdiStartDoc |
打印每一頁 |
StartPage |
NtGdiStartPage |
結束每一頁 |
EndPage |
NtGdiEndPage |
結束打印任務 |
EndDoc |
NtGdiEndDoc |
表1
其中的StartDoc和EndDoc是來控制每次打印任務的開始和結束;StartPage和EndPage控制每一頁的打印,我的理解是需要打印的文檔有多少頁,StartPage和EndPage函數對就要調用多少次,實際上對函數調用測試也驗證了這一點。
既然已經清楚了函數的功能和對應關係,想必如何實現應該有了點思路吧?對了,就是在這幾個函數上動手腳——Hook!到底是Ring3下的Hook還是Ring0下的Hook就應該是仁者見仁了,本文通過在Ring0下的Hook來實現打印監控的功能!
下面是我們目前要解決的問題,我將逐個擊破。
1) 如何定位到KeServiceDescriptorTableShadow?
2)在不同的操作系統版本下,Win32k.sys內的函數索引不同,我們該如何解決?
2) Win32k.sys的特殊性,並不是在每個進程都有映射,該如何進行Shadow Hook?
4)如何捕獲到打印相關的信息?
如何定位到KeServiceDescriptorTableShadow,黑防以前的文章中都有描述,這裏我只簡單的說明一下。定位的思路是在KeAddSystemServiceTable函數中具體的信息,如何定位網上已給出了一套完整的代碼,自己在Windows多個版本中測試時發現可以兼容!
nt!KeAddSystemServiceTable:
805a11d4 8bffmov edi,edi
805a11d6 55push ebp
805a11d7 8becmov ebp,esp
805a11d9 837d1803cmp dword ptr [ebp+18h],3
805a11dd 7760ja nt!KeAddSystemServiceTable+0x6b (805a123f)
805a11df 8b4518mov eax,dword ptr [ebp+18h]
805a11e2 c1e004shl eax,4
805a11e5 83b800c7558000cmpdword ptr nt!KeServiceDescriptorTable (8055c700)[eax],0
805a11ec 7551jnent!KeAddSystemServiceTable+0x6b (805a123f)
805a11ee 8d88c0c65580leaecx,nt!KeServiceDescriptorTableShadow (8055c6c0)[eax]
805a11f4 833900cmpdword ptr [ecx],0
805a11f7 7546jnent!KeAddSystemServiceTable+0x6b (805a123f)
具體實現定位的代碼如下:
ULONG GetAddressOfShadowTable()
//得到SSDT Shadow的函數地址
{
unsigned int i;
unsigned char *p;
unsigned int dwordatbyte;
p = (unsigned char*) KeAddSystemServiceTable;//該函數沒有文檔化,使用時需導出
for(i = 0; i < 4096; i++, p++)
{
__try
{
dwordatbyte = *(unsigned int*)p;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
return 0;
}
if(MmIsAddressValid((PVOID)dwordatbyte))
{
if(memcmp((PVOID)dwordatbyte, &KeServiceDescriptorTable, 16) == 0)
{
if((PVOID)dwordatbyte == &KeServiceDescriptorTable)
{
continue;
}
return dwordatbyte;
}
}//在KeAddSystemServiceTable中搜索匹配得到KeServiceDescriptorTableShadow
}
return 0;
}
既然定位KeServiceDescriptorTableShadow的代碼能夠在Windows的多個版本下準確的定位,我們的Hook也應該儘量做到最大的兼容性。不過有一個問題,就是在Shadow中的每個函數的具體索引並不能像SSDT那樣可以輕鬆得到,我們可以通過不同的操作系統版本來給出具體的數值,畢竟只有4個函數,而我們做的只是判斷一下當前運行的版本,然後填入正確的索引值即可!同時,這裏似乎還隱含着一個問題,我們待會兒再說。先看看如何判斷當前版本。
VOID GetFunIndex()
{
ULONG majorVersion, minorVersion;
PsGetVersion( &majorVersion, &minorVersion, NULL, NULL );//這個函數是核心!
if ( majorVersion == 5 && minorVersion == 2 )
{
DbgPrint("Running on Windows 2003");
//進行數據的修正
}
else if ( majorVersion == 5 && minorVersion == 1 )
{
DbgPrint("Running on Windows XP");
//進行數據的修正
}
else if ( majorVersion == 5 && minorVersion == 0 )
{
DbgPrint("Running on Windows 2000");
//進行數據的修正
}
}
通過該函數就可以輕鬆地實現我們對不同版本的操作系統兼容性的要求,具體的數值接下來會用表格的形式呈現出來!
到了這裏,我們應該就可以進行Hook了,但是如何確保win32k.sys已經映射到了我們的進程空間內呢?否則如果我們直接操作,後果就是BSOD啊!其實這個很簡單,因爲我們直接在DriverEntry函數中實現了這一系列的功能,也就是當前運行的進程空間處於SYSTEM進程內,而在SYSTEM進程內沒有映射win32k.sys,所以我們對Shadow的Hook導致藍屏也顯而易見啦!所以,在我們Hook之前先Attache到一個確信映射了該文件的進程內就是必須的了,而Csrss.sys就是我們的首選!現在,我們的問題明確了,就是找到csrss.exe進程後Attach!我使用了遍歷的方式來實現,不過只要能實現就OK啦!
ULONG GetCsrssProcessId()//如果找到,返回PID,沒有找到,返回
{
NTSTATUS m_status=STATUS_SUCCESS;
HANDLE m_process_id=0;//返回的ID
char m_name[16]={0};//進程的名字
ULONG m_index=0;
PEPROCESS m_eprocess;//進程的對象
for (m_index=0;m_index<65535;m_index+=4)
{
m_status=PsLookupProcessByProcessId((HANDLE)m_index,&m_eprocess);
if(NT_SUCCESS(m_status))
{
//檢測一下當前得到的是不是活動進程
strncpy(m_name,(char *)((ULONG)m_eprocess+m_name_offset),16);
//m_name_offset就是上面提到的隱含問題,不過如果是通過另外的方式定位的話,這個問題就不存在了
if (_stricmp(m_name,"csrss.exe")==0)
{
//表明已經得到了該進程的PID
DbgPrint("獲得到的PID爲:%d\n",m_index);
return m_index;
}
}
}
return 0;
}
如果Hook掉提到的那4個函數,方法和SSDT的Hook完全相同,這兒我就簡單的貼出代碼吧。
VOID TryToHookFun()//這兒執行的前提是已經Attach到了進程
{
PEPROCESS m_eprocess;
KIRQL m_irql;
NTSTATUS m_status=STATUS_SUCCESS;
ULONG m_process_id=0;
KAPC_STATE m_apc_state;
m_process_id=GetCsrssProcessId();
m_status=PsLookupProcessByProcessId((HANDLE)m_process_id,&m_eprocess);
if(NT_SUCCESS(m_status))
{
m_irql=KeRaiseIrqlToDpcLevel();
//下面開始附加進程
KeStackAttachProcess(m_eprocess,&m_apc_state);//附加到csrss.exej進程
CloseProtected();//關閉保護---這個代碼黒防到處都是!
……
//這裏實現的就是對Shadow函數的具體替換
RecoverProtected();
KeUnstackDetachProcess(&m_apc_state);//取消進程的附加
KeLowerIrql(m_irql);
}
}
表2是函數和進程名在不同版本下的具體數值或偏移量。
|
Windows 2000 |
Windows XP |
Windows 2003 |
NtGdiStartDoc索引 |
280 |
290 |
289 |
NtGdiStartPage索引 |
281 |
291 |
290 |
NtGdiEndPage索引 |
126 |
131 |
131 |
NtGdiEndDoc索引 |
125 |
130 |
130 |
ImageFileName偏移 |
0x1fc |
0x174 |
0x164 |
表2
至此,我們的前3個問題終於完美解決了,開始我們的打印監控了!如上所說,這4個函數的配合使用就可以輕鬆獲得打印信息!打印的開始時間、文檔名稱等信息在NtGdiStartDoc中獲得;打印的頁數從NtGdiStartPage的調用次數中獲得;結束時間在NtGdiEndDoc中獲得!根據這4個函數的參數來看,都有一個HDC 參數,是不是可以根據這個HDC來捕獲到打印內容呢?這個我還沒有去測試,至少當前可以捕獲到的信息已經滿足一定的要求了,有興趣的可以試試!
對於這4個函數如何配合使用,應該說是仁者見仁,智者見智,本文我只是簡單實現了打印監控,然後按照一定格式寫入了文件;如果讀取,就在Ring3層寫個程序把文件解析顯示即可,同樣達到了內核和用戶層的交互,畢竟打印監控沒有實時性的要求!