现在的位置: 首页 > 综合 > 正文

[原创]再谈 unlocker 编程”探险”及工作原理

2013年11月29日 ⁄ 综合 ⁄ 共 7470字 ⁄ 字号 评论关闭

Unlocker的编程探险及工作原理

 关键字:文件对象,NT用户态,内核态


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 使用 NtQueryInformationFile

    2 或者使用 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

抱歉!评论已关闭.