495
技術社區[雲棲]
[原創]再談 unlocker 編程”探險”及工作原理
Unlocker的編程”探險”及工作原理
Unlocker是偶寫的一個文件解鎖小工具,原來GUI用的是C# 2005編寫,功能邏輯用的
是純匯編加少量的C語言編寫。現在為了不依賴於.Net Framework 平台,CUI用VB6.0
重寫,而功能邏輯全部用C語言改寫。
VB6對於GUI的快速開發以及”便攜綠色化”還是比較優秀的一款工具,雖然他對漂亮
的XP皮膚支持有限(比如一些控件無法XP Skin化),甚至有些人會認為她是一款早已
過時的IDE,但目前來說,她還是可以很好的滿足偶的需求,既然可以滿足那麼足以。
[PART0 : 關於文件解鎖方法的淺談]
NT下的文件解鎖,我知道的主要有3種方法,我分別寫了3個函數對應:
extern WINAPI int CloseHandleByDH(DWORD pid,HANDLE hfile);
extern WINAPI int CloseHandleByRT(DWORD pid,HANDLE hfile);
extern WINAPI int CloseHandleByCore(DWORD pid,HANDLE handle);
這些都是NT下編程中較基本的知識點,相信大多數Coder們看到這心中已經明白了。
下麵我逐一作簡單說明:
1. CloseHandleByDH
使用DuplicateHandle的DUPLICATE_CLOSE_SOURCE 選項,該選項的作用是不但將源進
程中的對象句柄“拷貝”到目標進程(其實是將句柄指向Object的連接添加到目標進程
的對象表中。),而且同時關閉源進程中的對象句柄。
這種方法效果還是很好的,可以unlock大多數文件句柄,隻要取得SE_DEBUG特權,
甚至連System進程中一些句柄都可以關閉。很多文件解鎖工具用的都是這種方法。
2. CloseHandleByRT
這種方法的原理是向源進程中插入RemoteThread,同時將遠線程的入口設置為CloseHandle,
並將源進程中要關閉的句柄傳遞給它。以前在匯編中我是寫了一個所謂的naked函數,還
要在源進程中分配地址空間,然後將naked函數copy過去,最後在該函數中調用CloseHandle。其實沒這麼複雜,對於隻有一個參數的 ”知名” API,完全可以用一個CreateRemoteThread
搞定。但這種RemoteThread方法效果不是很好,如果源進程不允許在用戶態插入遠線程
(比如System,smss等),則這種方法就會失效。RemoteThread效果大大不如第一種方法。
3. CloseHandleByCore
前兩種方法對於有些內核對象來說沒有效果-------原湯化原食,這時還得從內核
裏想辦法。所以有了CloseHandleByCore 方法。對於某些內核對象可以簡單的在
Ring0中用ZwClose 關閉,然而另一些內核對象稱之為PERMANENT對象,這種對象
要先使用ZwMakeTemporaryObject將其“轉性”
然後再將其關閉(但據偶觀察還未見
到File類型的永久對象。)。然而在內核中做動作仍需小心,否則”必藍”。這個問題
在後麵還要提及。
以上列出了關閉句柄的幾種方法,還沒說如何獲得活動文件對象的句柄表。偶用的
還是比較“正統”的NtQuerySystemInformation 方法,該函數返回係統中全部活動對
象的信息表,其中每一項結構定義如下:
typedef struct _SYSTEM_HANDLE_INFORMATION {
ULONG ProcessId;
UCHAR ObjectTypeNumber;
UCHAR Flags;
USHORT Handle;
PVOID Object;
ACCESS_MASK GrantedAccess;
}SYSTEM_HANDLE_INFORMATION,*PSYSTEM_HANDLE_INFORMATION;
為了便於VB與C的信息傳遞,偶定義了相關的OpenFile結構:
typedef struct _OPENED_FILE_INFO
{
char ProcessName[MAX_PATH]; //進程名全稱
char FileName[MAX_PATH]; //文件全名稱
HANDLE hFile; //文件句柄
DWORD PID; //進程ID
DWORD Flags; //句柄標誌
DWORD GrantedAccess; //句柄訪問授權
PVOID Object; //對象體指針
int CurrentIndex; //當前句柄項在句柄表中的索引
}OPENED_FILE_INFO,*POPENED_FILE_INFO;
VB6中與其定義的結構是:
Type OPENED_FILE_INFO
ProcessName As String * MAX_PATH
FileName As String * MAX_PATH
hfile As Long
pid As Long
Flags As Long
GrantedAccess As Long
Object As Long
CurrentIndex As Long
End Type
[PART1 : 一個內核態中的嚴重漏洞!]
在用戶模式(UserMode)中,使用不到Object對象指針,但在內核中往往需要傳遞Object
去完成某些操作。這本來也無可厚非,但有一個嚴重的漏洞存在:
在內核中使用該Object時不能確保它是否還處在有效狀態!
前麵用NtQuerySystemInformation 取得係統句柄表,隻是係統在某個時間段裏的“快照”,
誰也沒有保證這些句柄和對象在後麵仍然有效!如果我們在Ring3級中引用一個失效
的句柄,那頂多也就返回無效句柄之類的錯誤。但在Ring0級則情況大有不同,在內
核態(KernelMode)中引用任何無效的內存都有可能引發“嚴重問題”。在編碼過程中
我發現即使ObReferenceObjectByPointer之類的函數返回 STATUS_SUCCESS ,仍不
能確保該對象是一個有效對象,貌似ObReferenceObjectByPointer 不管三七二十一,
隻是簡單的將對象頭(Object_Header)結構中的PointerCount值加1。
經過若幹次的“藍屏”,用Softice總結如下:
若文件對象的引用計數和句柄計數都為零,則基本上可以確定該對象已不
存在了。為了保險係數更高,我又增加判定第3個條件:對象的Type
字段總為0xBAD0????。通過這3點,則可保證該對象已OVER!不用再處理了!
(其實這也不是所謂的“數學證明”式的保證。雖然在各個係統上2K,
XP-SP2,XP-SP3,2K3-SP2 都沒有出問題,但我因未查NT源碼,也不敢拍著胸
脯說在各位的係統上不會出問題,如果我的“保證”哪裏有錯誤,請毫不
猶豫的指出,謝謝!)即有:
- if(poh->PointerCount == 0 &&/
- poh->u0.HandleCount == 0 &&/
- (DWORD)(poh->Type)>>16 == 0xBAD0)
- {
- DbgPrint("[%s] Bad Object!/n",__func__);
- //__asm("int $3");
- }
[PART2 : 另一個"不成熟"的思路]
(另一個思路:這裏說一下另一個思路,如何在內核關閉對象而不用切入到其進程
空間中去?我開始這樣想,隻要該對象不是Bad Object,則若將其句柄計數置0,
引用計數置1,然後調用ObDereferenceObject(pfo)讓對象管理器將它幹掉,這正是
所謂“狠毒”的“借刀殺人” 一招:
- DbgPrint("[%s]Obj : %p ,PointerCount : %u ,"
- "HandleCount : %u ,Flags : %02x/n",/
- __func__,pfo,poh->PointerCount,/
- poh->u0.HandleCount,(UCHAR)(poh->Flags));
- poh->u0.HandleCount = 0;
- poh->PointerCount = 1;
- ObDereferenceObject(pfo);
- bSuccess = true;
至於為什麼會這樣,我想各位即使不看NT內核揭秘之類的書也可以猜出個八九不
離十來。但實際上,這種方法大有問題,我試了幾次都“藍了”。我分析的
原因是(未驗證):盡管對象管理器可能將該對象“Free”了,但所有引用該對
象的進程都不知道他們指向該對象的句柄已經失效了,如果再用這些對象的話,
藍屏也是可想而知的。)
[PART3 : 如何通過文件句柄取得文件名稱]
下麵再來聊聊偶是如何通過文件對象句柄得到文件名字的,這個偶開始參考了
網上一些代碼,他們無外乎采用2種方法:
1
NtQueryObject (如果沒記錯的話)
貌似這2種方法的調用都在用戶態解決戰鬥(意思是他們都在ntdll.dll中導出,
可以在用戶態直接調用,但他們最終要不要進Ring0,則不予考慮。),倒也安全
穩定。其實這其中有點小問題,說小其實也不小,就是他們在枚舉某些非命名管
道時(管道在內部也是以文件對象來實現,如果偶沒理解錯的話,嘿嘿),隻有在
該管道有消息到達時才會返回,這就會發生“無限期”等待的問題。
其實這也好解決,就是將這些函數調用放到一個線程中,然後用
WaitForSingleObject以一個超時來強製其返回,然後終結掉線程。不瞞大家說,
偶開始就是這樣實現的,但這樣會給進程帶來嚴重的內存泄露!因為
可能NtQueryInformationFile在等待前申請了一塊內存,然後等待消息,
這個等待發生在一個Thread中,你在TimeOut時將該線程強行結束,該內存不會
被釋放(釋放內存的代碼得不到執行的機會),即使放在try…finally塊中都無
效。這樣程序執行幾次搜索後,可以看到其占用的物理內存和虛擬內存都直線上
升,虛存“輕易”的就可以突破幾百兆,其“後果”可想而知了。
再有一點:
用NtQueryInformationFile對於某些加載的OCX 文件隻能枚舉到空白文件名。
這一點小缺陷也使得偶“芒刺在背”。
So ,偶的最終解決方法是進內核,直接從File_Object中取得文件名稱,
這樣不會造成任何等待,不會有延時,而且取得的文件名要比前者更準確,
File_Object結構定義如下:
- typedef struct _FILE_OBJECT {
- CSHORT Type;
- CSHORT Size;
- PDEVICE_OBJECT DeviceObject;
- PVPB Vpb;
- PVOID FsContext;
- PVOID FsContext2;
- PSECTION_OBJECT_POINTERS SectionObjectPointer;
- PVOID PrivateCacheMap;
- NTSTATUS FinalStatus;
- struct _FILE_OBJECT *RelatedFileObject;
- BOOLEAN LockOperation;
- BOOLEAN DeletePending;
- BOOLEAN ReadAccess;
- BOOLEAN WriteAccess;
- BOOLEAN DeleteAccess;
- BOOLEAN SharedRead;
- BOOLEAN SharedWrite;
- BOOLEAN SharedDelete;
- ULONG Flags;
- UNICODE_STRING FileName;
- LARGE_INTEGER CurrentByteOffset;
- ULONG Waiters;
- ULONG Busy;
- PVOID LastLock;
- KEVENT Lock;
- KEVENT Event;
- PIO_COMPLETION_CONTEXT CompletionContext;
- KSPIN_LOCK IrpListLock;
- LIST_ENTRY IrpList;
- PVOID FileObjectExtension;
- } FILE_OBJECT, *PFILE_OBJECT;
取文件名的Ring0代碼如下:
- DDKAPI DWORD CoreGetFileName(PFILE_OBJECT pfo,PWSTR pwstr)
- {
- DWORD dwRet = 0;
- KIRQL oldirql;
- if(!pfo || !pwstr)
- {
- PRINT("[%s]error : pfo == NULL or pwstr == NULL!/n",/
- __func__);
- goto QUIT;
- }
- //__asm("int $3");
- KeRaiseIrql(DISPATCH_LEVEL,&oldirql);
- if(MmIsAddressValid(pfo->FileName.Buffer))
- {
- memcpy(pwstr,pfo->FileName.Buffer,pfo->FileName.Length);
- dwRet = pfo->FileName.Length;
- }
- else
- {
- PRINT("[%s]Memory Address %p is Invalid![pObj:%p]/n",/
- __func__,pfo->FileName.Buffer,pfo);
- }
- KeLowerIrql(oldirql);
- QUIT:
- return dwRet;
- }
為了“接應”Ring0中的CoreGetFileName,在Ring3種同樣要有考慮:
- DLLEXP WINAPI int CloseHandleByCore(DWORD pid,HANDLE handle)
- {
- int iRet = 0;
- if(!pid || !handle)
- {
- PRINT("[%s]error : pid == 0 or handle == NULL/n",/
- __func__);
- goto QUIT;
- }
- byte Buf[1024] = {0};
- DWORD *pbuf = (DWORD*)Buf;
- *pbuf = pid;
- *(pbuf+1) = (DWORD)handle;
- if(!CallDrv(&g_shs,IOCTL_Ctl_CloseHnd,Buf,1024,NULL,0))
- {
- PRINT("[%s] CallDrv IOCTL_Ctl_CloseHnd Failed!/n",/
- __func__);
- PrintErr();
- goto QUIT;
- }
- iRet = 1;
- QUIT:
- return iRet;
- }
在關閉了一個對象的句柄後如何確定這一點呢?我寫了一個簡單的C函數解決:
- //檢查是否特定進程中的句柄是否存在。
-
- //返回1代表存在,返回0代表不存在。
-
- DLLEXP WINAPI int FindHandle(DWORD pid,HANDLE hfile)
-
- {
-
- int iRet = 0;
-
- if(!pid || !hfile)
-
- {
-
- PRINT("[%s]error : pid == 0 or hfile == 0/n",/
-
- __func__);
-
- goto QUIT;
-
- }
-
-
- if(!FlushSysInfoList())
-
- {
-
- PRINT("[%s]FlushSysInfoList Failed!/n",__func__);
-
- goto QUIT;
-
- }
-
-
- if(!lpSysInfoList || !FileType)
-
- {
-
- PRINT("[%s]error : lpSysInfoList == NULL or FileType == 0",/
-
- __func__);
-
- goto QUIT;
-
- }
-
-
- int *pcount = (int*)lpSysInfoList;
-
- int count = *pcount;
-
- PSYSTEM_HANDLE_INFORMATION pSHI = (PSYSTEM_HANDLE_INFORMATION)(++pcount);
-
- for(int i = 0;i < count;++i)
-
- {
-
- if(pid == pSHI[i].ProcessId && hfile == pSHI[i].Handle &&/
-
- FileType == (unsigned)(pSHI[i].ObjectTypeNumber))
-
- {
-
- iRet = 1;
-
- break;
-
- }
-
- }
-
- QUIT:
-
- return iRet;
-
- }
取得特定進程的PE文件名相對而且比較容易,同樣可以有多種方法,
比如使用Process32First,Process32Next之類的Win32 API 搞定,偶在這裏采
用了其他方法:GetProcessImageFileName ,但這個API在2K中不能用,
遂換成GetModuleFileNameEx 解決。
為了便於重用還寫了若幹Driver加載函數,分別是:
- extern bool NewDrv(PSvrHnds pSH);
- extern bool DelDrv(PSvrHnds pSH);
- extern bool StartDrv(PSvrHnds pSH);
- extern bool DelDrvForce(void);
- extern bool CallDrv(PSvrHnds pSH,DWORD Ctrl_Code,void *_in,/
- size_t cbin,void *_out,size_t cbout);
他們隻是對Win32 服務API的2次包裝,故省去解釋。
[PART5 : 尾聲+亂彈]
總的來說,這個版本的unlocker比以前用C#寫的功能更強大,效率和穩定性更高,
也更安全。匯編固然強大,但用起來也有繁瑣的地方,VB6固然“落後”但也有她方便
貼心的一麵,而C#固然先進,但我卻偏想找找“麻煩”。難道Gcc就沒有不爽的地方了麼?
有啊!為啥不像VC那樣來個naked函數呢?現在還不得用匯編來做這件苦差啊。
所以世上美沒有十全十美的語言,也沒有十全十美的人,正所謂:
人有悲歡離合,月有陰晴圓缺,此事古難全,但願人長久,千裏共嬋娟。
(嚴重跑題中…)。
csdn上傳的文件,不知為何被取消???隻有先另找個地方臨時上傳一下,
歡迎各位下載挑刺,謝謝:
侯佩|hopy
2008-11-10於電心
最後更新:2017-04-02 00:06:39