本文首先是由下面事件促使的,我們觀察到 OutputDebugString() 在管理員和非管理員用戶試圖一起工作或游戲時並不總是能可靠地工作(至少在 Win2000 上)。我們懷疑是一些相關的內核對象的權限問題,此間涉略了相當多不得不寫下來的信息。
請注意,雖然我們使用了“調試器”這一術語,但不是從調試 API 的意義上來使用的:並沒有“單步運行”、“斷點”或者“附着到進程”等能夠在 MS Visual C 或者一些真正的交互開發環境中找到的東西。從某種意義上來說,不論什么實現了協議的程序都是“調試器”。可能是一個很小的命令行工具,或者像來自於 SysInternals 那幫聰明的家伙們的 DebugView 那樣的高級貨。
內容文件夾
應用程序使用方法
<windows.h> 文件聲明了 OutputDebugString() 函數的兩個版本號 - 一個用於 ASCII,一個用於 Unicode - 不像絕大多數 Win32 API 一樣,原始版本號是 ASCII。而大多數的 Win32 API 的原始版本號是 Unicode。
使用一個 NULL 結尾的字符串緩沖區簡單調用 OutputDebugString() 將導致信息出如今調試器中,假設有調試器的話。構建一條信息並發送之的通經常使使用方法是:
sprintf(msgbuf, "Cannot open file %s [err=%ld]/n", fname, GetLastError()); OutputDebugString(msgbuf);
只是在實際環境中我們中的不少人會創建一個前端函數,以同意我們使用 printf 風格的格式化。以下的 odprintf() 函數格式化字符串,確保結尾有一個合適的回車換行(刪除原來的行結尾),而且發送信息到調試器。
#include <stdio.h> #include <stdarg.h> #include <ctype.h> void __cdecl odprintf(const char *format, ...) { char buf[4096], *p = buf; va_list args; va_start(args, format); p += _vsnprintf(p, sizeof buf - 1, format, args); va_end(args); while ( p > buf && isspace(p[-1]) ) *--p = '/0'; *p++ = '/r'; *p++ = '/n'; *p = '/0'; OutputDebugString(buf); }
於是在代碼中使用它就非常easy:
... odprintf("Cannot open file %s [err=%ld]", fname, GetLastError()); ...
協議
在應用程序和調試器之間傳遞數據是通過一個 4KB 大小的共享內存塊完畢的,並有一個相互排斥量和兩個事件對象用來保護對他的訪問。以下就是相關的四個內核對象:
對象名稱 對象類型 DBWinMutex Mutex DBWIN_BUFFER Section (共享內存) DBWIN_BUFFER_READY Event DBWIN_DATA_READY Event
相互排斥量通常一直保留在系統中,其它三個對象僅當調試器要接收信息才出現。其實 - 假設一個調試器發現后三個對象已經存在,它會拒絕執行。
當 DBWIN_BUFFER 出現時,會被組織成下面結構。進程 ID 顯示信息的來源,字符串數據填充這 4K 的剩余部分。依照約定,信息的末尾總是包含一個 NULL 字節。
struct dbwin_buffer { DWORD dwProcessId; char data[4096-sizeof(DWORD)]; };
當 OutputDebugString() 被應用調用時,它運行下面步驟。注意在任何位置的錯誤都將放棄整個事情,調試請求被覺得是什么也不做(不會發送字符串)。
- 打開 DBWinMutex 而且等待,直到我們取得了獨占訪問。
- 映射 DBWIN_BUFFER 段到內存中:假設沒有發現,則沒有調試器在執行,將忽略整個請求。
- 打開 DBWIN_BUFFER_READY 和 DBWIN_DATA_READY 事件對象。就像共享內存段一樣,缺少對象意味着沒有可用的調試器。
- 等待 DBWIN_BUFFER_READY 事件對象為有信號狀態:表示內存緩沖區不再被占用。大部分時候,這一事件對象一被檢查就處於有信號狀態,但等待緩沖區就緒不會超過 10 秒(超時將放棄請求)。
- 復制數據直到內存緩沖區中接近 4KB,再保存當前進程 ID。總是放置一個 NULL 字節到字符串結尾。
- 通過設置 DBWIN_DATA_READY 事件對象告訴調試器緩沖區就緒。調試器從那兒取走它。
- 釋放相互排斥量。
- 關閉事件對象和段對象,但保留相互排斥量的句柄以備后用。
在調試器端會簡單一點。相互排斥量根本不須要,假設事件對象和/或共享內存對象已經存在,則假定其它調試器已經在執行。系統中隨意時刻僅僅能存在一個調試器。
- 創建共享內存段以及兩個事件對象。假設失敗,退出。
- 設置 DBWIN_BUFFER_READY 事件對象,由此應用程序得知緩沖區可用。
- 等待 DBWIN_DATA_READY 事件對象變為有信號狀態。
- 從內存緩沖區中提取進程 ID 和 NULL 結尾的字符串。
- 轉到步驟 2。
這使我們覺得這決不是一種低消耗的發送信息的方法,應用程序的執行速度會受到調試器的左右。
權限問題
我們發現 OutputDebugString() 有時不可靠已經好幾年了,並且我們十分不解為什么微軟這么長時間也沒把它搞好。奇怪的是,問題總是環繞着 DBWinMutex 對象出現,這就須要我們察看許可系統以找出為什么會這么麻煩。
相互排斥量對象會一直存活着直到使用它的最后一個程序關閉其句柄,故而它能在初始創建它的應用程序退出后保留相當長的時間。由於此對象被廣泛地共享,所以它必須被賦予明白的許能夠同意不論什么人使用它。其實,“缺省”許可差點兒從不適用,這一問題被計為在 NT 3.51 和 NT 4.0 中我們觀察到的第一個問題。
當時的修正方法是使用一個廣泛開放的 DACL 創建相互排斥量,以此來同意不論什么人訪問它,可是看樣子在 Win2000 里這些許可被加強了。表面上它看起來是正確的,就像我們在下表中看到的:
SYSTEM MUTEX_ALL_ACCESS Administrators MUTEX_ALL_ACCESS Everybody SYNCHRONIZE | READ_CONTROL | MUTEX_QUERY_STATE
希望發送調試信息的應用僅僅須要等待和獲取該相互排斥量的能力,也即體現為擁有 SYNCHRONIZE 權限。上列的許可對於全部參與的用戶都是全然正確的。
只是假設有人觀察 CreateMutex() 在對象已經存在時的行為,就會發現奇怪的事情。在這樣的情況下,Win32 的表現就好像我們進行了例如以下調用:
OpenMutex(MUTEX_ALL_ACCESS, FALSE, "DBWinMutex");
雖然我們確實僅僅須要 SYNCHRONIZE 訪問,但它還是假定調用者要做不論什么事情(MUTEX_ALL_ACCESS)。由於非管理員沒有這些權限 - 僅有上列的少許 - 相互排斥量不能被打開或者獲取,於是 OutputDebugString() 不做不論什么事情就悄悄地返回了。
甚至將全部的軟件開發都以管理員來執行也不是一個完整的修正方法:假設存在其它的用戶(比如服務)以非管理員執行而許可配置不對,它們的調試信息將會丟失。
我們感覺真正的修正須要微軟為 CreateMutex() 加入�一個參數 - 假設對象已經存在時用於隱含的 OpenMutex() 調用的訪問掩碼。或許某天我們會看到一個 CreateMutexEx(),但在此期間我們必須採用另外的方法。代之以,當對象已經存活於內存中時我們將硬性改變其上的許可配置。
這須要調用 SetKernelObjectSecurity(),下列程序片斷展示一個程序怎樣才干打開相互排斥量並安裝一個新的 DACL。此 DACL 即使在程序退出后也仍然保持着,僅僅要任一其它程序還維護有它(譯者注:應該是指相互排斥量)的句柄。
...
// open the mutex that we're going to adjust
HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, "DBWinMutex");
// create SECURITY_DESCRIPTOR with an explicit, empty DACL
// that allows full access to everybody
SECURITY_DESCRIPTOR sd;
InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION);
SetSecurityDescriptorDacl(
&sd, // addr of SD
TRUE, // TRUE=DACL present
NULL, // ... but it's empty (wide open)
FALSE); // DACL explicitly set, not defaulted
// plug in the new DACL
SetKernelObjectSecurity(hMutex, DACL_SECURITY_INFORMATION, &sd);
...
這一方法明白地走向了正確的道路,但我們還須要找一個地方來放置此邏輯。把它放在一個一經請求即執行的小程序中是能夠的,可是看起來它有可能被中斷。我們的辦法是寫一個 Win32 服務來干這件事情。
我們的 dbmutex 工具完畢的就是這一工作:它在系統引導時啟動,打開或者創建相互排斥量,然后設置對象的安全性以同意廣泛的訪問。然后休眠直到系統關閉,在此過程中保持相互排斥量的打開狀態。它不消耗不論什么 CPU 時間。
實現細節
我們花了非常多時間使用 IDA Pro 深入到 Windows 2000 KERNEL32.DLL 的實現中,我們覺得,對於它在更精確的基礎上究竟是怎樣工作的已經有了良好的掌握。在這兒我們給出 OutputDebugString() 函數的偽代碼(我們沒有編譯過它),以及創建相互排斥量的函數。
我們有益略去了大多數的錯誤檢查:假設事情變糟了,它將釋放全部已分配的資源並退出,就像沒有調試器存在一樣。目的是展示一般行為而不是對代碼的完整的逆向project。
“setup” 函數 - 名字是我們起的 - 創建相互排斥量或者在已經存在時打開它。經過一些努力來設置相互排斥量對象的安全性以使不論什么人都能用它,雖然我們會看到事實上並沒有全然正確地得到它。
胡思亂想
一些人可能會感到這是一個安全性問題,事實上並非。非管理員用戶確實擁有適當使用 OutputDebugString() 的全部權限,只是因為“請求比所需很多其它權限”這一常見問題,一個合理的請求因形成了錯誤的形態而被拒絕了。
但並不像大部分的這樣的問題那樣,這並不是是有意的。大多數的錯誤是開發者顯式請求了很多其它(如“MUTEX_ALL_ACCESS”),而這次的掩碼是由 CreateMutex() 的行為隱含的。這使得假設 Win32 API 不做修改的話更加難於避免。
---
當分析 KERNEL32.DLL 中的 OutputDebugStringA() 時,非管理員怎樣可以有可能去削弱系統變得明顯起來。一旦得到相互排斥量,一個要發送調試信息的應用會等待 DBWIN_BUFFER_READY 事件對象就緒最多十秒鍾,假設超時則放棄。這看起來是一個慎重的防范措施,假設調試系統忙的話,用以避免被餓死。
但在更早的步驟里,等待相互排斥量,沒有這種超時設定。假設系統中的不論什么進程 - 包含非特權進程 - 能夠以請求 SYNCHRONIZE 權限打開此相互排斥量,而且不釋放它,全部其它試圖獲取此相互排斥量的進程將會無限停止完蛋。
我們的研究表明,全部類型的程序都會發送任意的調試信息(比如,MusicMatch Jukebox 就有一個嘮嘮叨叨的鍵盤鈎子),這些線程通過非常少的幾行代碼就能停止住。沒有必要停止整個程序 - 可能還有其它的線程 - 但在實際中,開發者不計划使用 OutputDebugString() 將會是一條拒絕服務之路(譯者注:此句沒有全然明確,請參看原文)。
---
最奇怪的是,我們發現 OutputDebugString() 並不是一個天然的 Unicode 函數。大多數的 Win32 API 具有“真正的”使用了 Unicode 的函數(“W” 版本號),假設調用“A”版本號的函數則它們自己主動從 ASCII 轉換到 UNICODE。
可是,由於 OutputDebugString 把在內存緩沖區中的數據終於是作為 ASCII 傳遞到調試器中的,它們具有相反於常規的 A/W 配對。這就暗示了假設要在 Unicode 程序里發送一個快捷信息到調試器,能夠通過直接調用 “A” 版本號來實現:
OutputDebugStringA("Got here to place X");