閱讀495 返回首頁    go 技術社區[雲棲]


[原創]再談 unlocker 編程”探險”及工作原理

Unlocker的編程探險及工作原理

 



Unlocker是偶寫的一個文件解鎖小工具,原來GUI用的是C# 2005編寫,功能邏輯用的

是純匯編加少量的C語言編寫。現在為了不依賴於.Net Framework 平台,CUIVB6.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

使用DuplicateHandleDUPLICATE_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;

 

為了便於VBC的信息傳遞,偶定義了相關的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源碼,也不敢拍著胸

脯說在各位的係統上不會出問題,如果我的“保證”哪裏有錯誤,請毫不

猶豫的指出,謝謝!)即有:

 

  1. if(poh->PointerCount == 0 &&/
  2.             poh->u0.HandleCount == 0 &&/
  3.             (DWORD)(poh->Type)>>16 == 0xBAD0)
  4.         {
  5.             DbgPrint("[%s] Bad Object!/n",__func__);
  6.             //__asm("int $3");
  7.         }
             

    [PART2  : 另一個"不成熟"的思路]

(另一個思路:這裏說一下另一個思路,如何在內核關閉對象而不用切入到其進程

空間中去?我開始這樣想,隻要該對象不是Bad Object,則若將其句柄計數置0

引用計數置1,然後調用ObDereferenceObject(pfo)讓對象管理器將它幹掉,這正是

所謂“狠毒”的“借刀殺人” 一招:

 

  1. DbgPrint("[%s]Obj : %p ,PointerCount : %u ,"
  2.               "HandleCount : %u ,Flags : %02x/n",/
  3.               __func__,pfo,poh->PointerCount,/
  4.               poh->u0.HandleCount,(UCHAR)(poh->Flags));
  5.          
  6.           poh->u0.HandleCount = 0;
  7.           poh->PointerCount = 1;
  8.           ObDereferenceObject(pfo);
  9.           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結構定義如下:

    
  1. typedef struct _FILE_OBJECT {
  2.     CSHORT  Type;
  3.     CSHORT  Size;
  4.     PDEVICE_OBJECT  DeviceObject;
  5.     PVPB  Vpb;
  6.     PVOID  FsContext;
  7.     PVOID  FsContext2;
  8.     PSECTION_OBJECT_POINTERS  SectionObjectPointer;
  9.     PVOID  PrivateCacheMap;
  10.     NTSTATUS  FinalStatus;
  11.     struct _FILE_OBJECT  *RelatedFileObject;
  12.     BOOLEAN  LockOperation;
  13.     BOOLEAN  DeletePending;
  14.     BOOLEAN  ReadAccess;
  15.     BOOLEAN  WriteAccess;
  16.     BOOLEAN  DeleteAccess;
  17.     BOOLEAN  SharedRead;
  18.     BOOLEAN  SharedWrite;
  19.     BOOLEAN  SharedDelete;
  20.     ULONG  Flags;
  21.     UNICODE_STRING  FileName;
  22.     LARGE_INTEGER  CurrentByteOffset;
  23.     ULONG  Waiters;
  24.     ULONG  Busy;
  25.     PVOID  LastLock;
  26.     KEVENT  Lock;
  27.     KEVENT  Event;
  28.     PIO_COMPLETION_CONTEXT  CompletionContext;
  29.     KSPIN_LOCK  IrpListLock;
  30.     LIST_ENTRY  IrpList;
  31.     PVOID  FileObjectExtension;
  32. } FILE_OBJECT, *PFILE_OBJECT;

取文件名的Ring0代碼如下:


  1. DDKAPI DWORD CoreGetFileName(PFILE_OBJECT pfo,PWSTR pwstr)
  2. {
  3.     DWORD dwRet = 0;
  4.     KIRQL oldirql;
  5.     if(!pfo || !pwstr)
  6.     {
  7.        PRINT("[%s]error : pfo == NULL or pwstr == NULL!/n",/
  8.            __func__);
  9.        goto QUIT;
  10.     }
  11.     //__asm("int $3");
  12.     KeRaiseIrql(DISPATCH_LEVEL,&oldirql);
  13.     if(MmIsAddressValid(pfo->FileName.Buffer))
  14.     {
  15.        memcpy(pwstr,pfo->FileName.Buffer,pfo->FileName.Length);
  16.        dwRet = pfo->FileName.Length;
  17.     }
  18.     else
  19.     {
  20.        PRINT("[%s]Memory Address %p is Invalid![pObj:%p]/n",/
  21.               __func__,pfo->FileName.Buffer,pfo);
  22.     }
  23.     KeLowerIrql(oldirql);
  24. QUIT:
  25.     return dwRet;
  26. }

 

為了“接應”Ring0中的CoreGetFileName,Ring3種同樣要有考慮:

   

  1. DLLEXP WINAPI int CloseHandleByCore(DWORD pid,HANDLE handle)
  2. {
  3.     int iRet = 0;
  4.     if(!pid || !handle)
  5.     {
  6.        PRINT("[%s]error : pid == 0 or handle == NULL/n",/
  7.            __func__);
  8.        goto QUIT;
  9.     }
  10.    
  11.     byte Buf[1024] = {0};
  12.     DWORD *pbuf = (DWORD*)Buf;
  13.     *pbuf = pid;
  14.     *(pbuf+1) = (DWORD)handle;
  15.    
  16.     if(!CallDrv(&g_shs,IOCTL_Ctl_CloseHnd,Buf,1024,NULL,0))
  17.     {
  18.        PRINT("[%s] CallDrv IOCTL_Ctl_CloseHnd Failed!/n",/
  19.            __func__);
  20.        PrintErr();
  21.        goto QUIT;
  22.     }
  23.     iRet = 1;
  24.    
  25. QUIT:
  26.     return iRet;
  27. }



在關閉了一個對象的句柄後如何確定這一點呢?我寫了一個簡單的C函數解決:


  1.  //檢查是否特定進程中的句柄是否存在。

  2. //返回1代表存在,返回0代表不存在。

  3. DLLEXP WINAPI int FindHandle(DWORD pid,HANDLE hfile)

  4. {

  5.     int iRet = 0;

  6.     if(!pid || !hfile)

  7.     {

  8.        PRINT("[%s]error : pid == 0 or hfile == 0/n",/

  9.            __func__);

  10.        goto QUIT;

  11.     }

  12.    

  13.     if(!FlushSysInfoList())

  14.     {

  15.        PRINT("[%s]FlushSysInfoList Failed!/n",__func__);

  16.        goto QUIT;

  17.     }

  18.    

  19.     if(!lpSysInfoList || !FileType)

  20.     {

  21.        PRINT("[%s]error : lpSysInfoList == NULL or FileType == 0",/

  22.            __func__);

  23.        goto QUIT;

  24.     }

  25.    

  26.     int *pcount = (int*)lpSysInfoList;

  27.     int count = *pcount;

  28.     PSYSTEM_HANDLE_INFORMATION pSHI = (PSYSTEM_HANDLE_INFORMATION)(++pcount);

  29.     for(int i = 0;i < count;++i)

  30.     {

  31.        if(pid == pSHI[i].ProcessId && hfile == pSHI[i].Handle &&/

  32.            FileType == (unsigned)(pSHI[i].ObjectTypeNumber))

  33.        {

  34.            iRet = 1;

  35.            break;

  36.        }

  37.     }

  38. QUIT:

  39.     return iRet;

  40. }


        [PART4  : 一些其他問題]

取得特定進程的PE文件名相對而且比較容易,同樣可以有多種方法,

比如使用Process32FirstProcess32Next之類的Win32 API 搞定,偶在這裏采

用了其他方法:GetProcessImageFileName 但這個API2K中不能用,

遂換成GetModuleFileNameEx 解決。



 

為了便於重用還寫了若幹Driver加載函數,分別是:

    

  1. extern bool NewDrv(PSvrHnds pSH);
  2. extern bool DelDrv(PSvrHnds pSH);
  3. extern bool StartDrv(PSvrHnds pSH);
  4. extern bool DelDrvForce(void);
  5. extern bool CallDrv(PSvrHnds pSH,DWORD Ctrl_Code,void *_in,/
  6.         size_t cbin,void *_out,size_t cbout);

他們隻是對Win32 服務API2次包裝,故省去解釋。

 

        [PART5  : 尾聲+亂彈]

       總的來說,這個版本的unlocker比以前用C#寫的功能更強大,效率和穩定性更高,

也更安全。匯編固然強大,但用起來也有繁瑣的地方,VB6固然“落後”但也有她方便

貼心的一麵,而C#固然先進,但我卻偏想找找“麻煩”。難道Gcc就沒有不爽的地方了麼?

有啊!為啥不像VC那樣來個naked函數呢?現在還不得用匯編來做這件苦差啊。

所以世上美沒有十全十美的語言,也沒有十全十美的人,正所謂:


       人有悲歡離合,月有陰晴圓缺,此事古難全,但願人長久,千裏共嬋娟。

(嚴重跑題中)。

 

 

csdn上傳的文件,不知為何被取消???隻有先另找個地方臨時上傳一下,
歡迎各位下載挑刺,謝謝:


下載連接



                                                    侯佩|hopy

                                                                                                         2008-11-10於電心

最後更新:2017-04-02 00:06:39

  上一篇:go javascript 正則全收錄
  下一篇:go C++流實現內幕---由boost::lexical_cast引發的一個問題