Unlocker的编程”探险”及工作原理
关键字:文件对象,NT用户态,内核态
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源码,也不敢拍着胸
脯说在各位的系统上不会出问题,如果我的“保证”哪里有错误,请毫不
犹豫的指出,谢谢!)即有:
(另一个思路:这里说一下另一个思路,如何在内核关闭对象而不用切入到其进程
空间中去?我开始这样想,只要该对象不是Bad Object,则若将其句柄计数置0,
引用计数置1,然后调用ObDereferenceObject(pfo)让对象管理器将它干掉,这正是
所谓“狠毒”的“借刀杀人” 一招:
至于为什么会这样,我想各位即使不看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代码如下:
为了“接应”Ring0中的CoreGetFileName,在Ring3种同样要有考虑:
在关闭了一个对象的句柄后如何确定这一点呢?我写了一个简单的C函数解决:
取得特定进程的PE文件名相对而且比较容易,同样可以有多种方法,
比如使用Process32First,Process32Next之类的Win32 API 搞定,偶在这里采
用了其他方法:GetProcessImageFileName ,但这个API在2K中不能用,
遂换成GetModuleFileNameEx 解决。
为了便于重用还写了若干Driver加载函数,分别是:
他们只是对Win32 服务API的2次包装,故省去解释。
[PART5 : 尾声+乱弹]
总的来说,这个版本的unlocker比以前用C#写的功能更强大,效率和稳定性更高,
也更安全。汇编固然强大,但用起来也有繁琐的地方,VB6固然“落后”但也有她方便
贴心的一面,而C#固然先进,但我却偏想找找“麻烦”。难道Gcc就没有不爽的地方了么?
有啊!为啥不像VC那样来个naked函数呢?现在还不得用汇编来做这件苦差啊。
所以世上美没有十全十美的语言,也没有十全十美的人,正所谓:
人有悲欢离合,月有阴晴圆缺,此事古难全,但愿人长久,千里共婵娟。
(严重跑题中…)。
csdn上传的文件,不知为何被取消???只有先另找个地方临时上传一下,
欢迎各位下载挑刺,谢谢:
下载连接
侯佩|hopy
2008-11-10 于电心