Chinaunix首页 | 论坛 | 博客
  • 博客访问: 579601
  • 博文数量: 190
  • 博客积分: 10937
  • 博客等级: 上将
  • 技术积分: 2205
  • 用 户 组: 普通用户
  • 注册时间: 2009-04-07 11:28
文章分类

全部博文(190)

文章存档

2012年(1)

2011年(27)

2010年(20)

2009年(142)

我的朋友

分类: WINDOWS

2011-01-05 11:28:16

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

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


}

 

        [PART4  : 一些其他问题]
取得特定进程的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次包装,故省去解释。

阅读(803) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~