译者:shan66
预估稿费:200RMB
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
传送门
操纵KESS:提升权限
漏洞
在调查KESS的KLAM.SYS驱动程序时,我们发现一段有趣的代码:
void *__thiscall create_module_list(PEB_LDR_DATA *peb_ldr, unsigned int *out_buf_ptr,
unsigned int *out_buf_sz_ptr)
{
void *result; // eax@1
PVOID buf; // ebx@1
char *module_list; // edi@2
int i; // eax@3
int module_list_entry; // esi@9
size_t strlen; // ecx@11
void *v9; // [sp+10h] [bp-30h]@1
int buf_1; // [sp+1Ch] [bp-24h]@9
unsigned int offs; // [sp+20h] [bp-20h]@9
unsigned int sz; // [sp+24h] [bp-1Ch]@1
CPPEH_RECORD ms_exc; // [sp+28h] [bp-18h]@2
result = 0xC000000D;
v9 = 0xC000000D;
buf = 0;
sz = 0;
*out_buf_sz_ptr = 0;
*out_buf_ptr = 0;
if ( peb_ldr10
{
ms_exc.registration.TryLevel = 0;
module_list = &peb_ldr->InMemoryOrderModuleList;
if ( *module_list != module_list )
{
for ( i = *module_list; i != module_list; i = *i )
sz += 0x24 + *(i + 0x1C) + 0xA;
if ( sz )
{
sz += 0x1008;
buf = ExAllocatePoolWithTag(PagedPool, sz + 0x1000, ‘imLK’);
}
if ( buf )
{
buf_1 = buf;
offs = 0;
for ( module_list_entry = *module_list; module_list_entry != module_list;
module_list_entry = *module_list_entry )
{
offs += 12;
offs += 12;
offs += 12;
strlen = *(module_list_entry + 28);
offs += strlen + 10;
if ( offs > sz )
break;
memcpy_special_0(strlen, 3, &buf_1, *(module_list_entry + 0x20));// see at src
buf!
memcpy_special(4u, 7, &buf_1, (module_list_entry + 16));
memcpy_special(4u, 8, &buf_1, (module_list_entry + 24));
memcpy_special(4u, 9, &buf_1, (module_list_entry + 20));
}
offs += 8;
memcpy_special(0, 0, &buf_1, 0);
sz = offs;
}
}
ms_exc.registration.TryLevel = -2;
if ( buf )
{
*out_buf_ptr = buf;
*out_buf_sz_ptr = sz;
v9 = 0;
}
result = v9;
}
return result;
}
接下来,会调用ExAllocatePoolWithTag,并请求分配大小为sz + 0x1000的内存空间。
之后,会再次从头到尾处理该列表,并且对于每个模块,都将存放列表数据的结构添加到已分配的缓冲区中。然后,将缓冲区中的偏移量与sz值(而不是sz + 0x1000 – 参见(3))进行比较,以防止发生溢出。
最后,为缓冲区添加8个以上的零,并返回其指针和大小。
实际上,这个缓冲区大小的计算过程有可能发生一个整数溢出。为此,可以:
1.构造一个InMemoryOrderModuleList_fake链接列表,使得第一次循环中,字符串与剩余其他内容的长度之和等于0xffffdff8;
2.将其放在PEB中;
3.通过这个函数触发列表审核;
4.在步骤(2)的操作算法中,该函数将被分配一个值sz + = 0x1008(Sz = 0xFFFFDFF + 0x1008 = = 0xFFFFF000);
5.调用ExAllocatePoolWithTag,表示大小的参数Sz_arg = sz + 0x1000(sz_arg = 0xFFFFF000 + 0x1000 = = 0)——将分配一个长度为零的缓冲区;
6.然后,InMemoryOrderModuleList_fake的数据将被复制到这个缓冲区,直到偏移值 offs > sz(sz == 0xFFFFF000),这远大于前面分配的缓冲区长度,即零字节:)
对于sz的任何取值,我们都可以为其构建一个InMemoryOrderModuleList_fake。在0xFFFFDFF8 <= sz <0xFFFFEFF8的范围内,我们可以分配大小为0到0x1000字节的缓冲区,然后再设法溢出。
溢出控制
要进行溢出,我们需要解决下面的问题:
1.数据将被复制到缓冲区,直到偏移值 offs> sz,其中sz接近最大的无符号整数,即约4 GB——这时候就应该停下来。
2.让驱动程序在我们的exploit上执行这个函数。
3.把处于我们控制之下的对象放在内存池中,以避免对随机内核对象造成损害。
控制溢出规模
复制4GB的数据简直就是DoS。
第一个想法是在执行易受攻击的函数时替换InMemoryOrderModuleList指针,以使sz的值被溢出;并且在第二次运行时,该列表就会获得一个更适合填充所分配的内存的大小。
实际上,这个想法是可以实现的。当驱动程序处理我们的InMemoryOrderModuleList列表时,它不会阻塞进程,我们可以对另一个指针进行写操作,让它指向PEB。不过,很难抓住合适的替换时机,所以我们只好通过循环,将一个值改为另一个,并希望有好运气。这种方法是可行的,但是非常不稳定。
此外,我们还偶然发现了另一种方便的方法。我们已经注意到,当将数据字符串从指向模块路径的无效指针复制到缓冲区到时,系统没有崩溃蓝屏。关键是,在函数的反编译列表中看不到这些,因为有一个处理程序来处理异常:
PAGE:B1879590 loc_B1879590: ; DATA XREF:
.rdata:off_B184CDD0no
PAGE:B1879590 Mov esp, [ebp+ms_exc.old_esp] ; Exception handler
0 for function B17E8452
PAGE:B1879593 Mov ebx, [ebp+P]
PAGE:B1879596 Test ebx, ebx
PAGE:B1879598 Jz short loc_B18795AB
PAGE:B187959A Push 0 ; Tag
PAGE:B187959C Push ebx ; P
PAGE:B187959D Call ds:ExFreePoolWithTag
PAGE:B18795A3 Xor ebx, ebx
PAGE:B18795A5 Mov [ebp+P], ebx
PAGE:B18795A8 And [ebp+sz], ebx
PAGE:B18795AB
当代码运行到memcpy-rep的mov时,ESI寄存器包含一个指向无法读取的存储器的指针,从而将控制权移交到这里;缓冲区被释放;从函数返回,然后继续。我们可以准备好要复制到缓冲区的数据,以便当我们需要使用PAGE_NOACCESS属性阻塞该内存页时,数据的末尾正好是模块路径字符串。 这样,我们可以准确可靠地控制溢出的长度和内容。
触发函数
据说,如果发现某种恶意软件将自身注入到某些进程中的时候,KESS服务就会调用该函数。然而,当扫描大量不同的样本时,发现并没有触发它。
通过运行内存扫描来执行这个存在漏洞的函数是很方便的方法:
kavshell.exe scan /memory
但是,没有管理员权限的用户无法做到这一点。我们已经逆向了kavshell.exe,证实运行扫描的权限检查位于KESS服务中,而不是在其界面中,所以我们无法通过提交这种扫描请求而绕过权限检查。KESS开始运行时会为每个进程调用该函数,所以您还需要停止它的权限。您可以通过发送一个样本来重新启动服务,这个示例会导致扫描引擎在解压缩时发生DoS。
进行扫描的时间是系统开始时。该exploit可以放入当前用户可用的自动运行区域,然后重启机器。一旦exploit运行,就会启动扫描并影响我们的进程。但是,为了在内存池中进行喷射来控制分配的顺序,这需要更多的时间;并且,如实验所示,这段时间对我们来说太长了。我们将执行以下操作:所有进程都进行相应的扫描——从最新的进程到最旧的进程。该exploit会启动500个处于挂起模式的计算器程序,当KESS内核处理这些进程的时候,我们就获得了比较长的一段时间。
分页池喷射
为了把我们控制的对象放到溢出的缓冲区之后,我们可以设法生成所需的内核内存状态。我们需要知道,易受攻击的缓冲区需要分配到分页池中。这有点麻烦,原因如下:
当覆盖回调函数时,可用于快速获取控制权的对象被分配到了非分页池中了,具体示例可以看这里。
存在多个页面池,分配程序会来回切换以平衡负载。
要了解更多详细信息,强烈建议您深入研究Windows内核分配程序的体系结构。
填充
通过连接调试器并检查分页池的PoolDescriptor.ListHeads列表,请注意,KESS内存扫描是在Windows启动之后进行的,这时候分配的大小与密集的系统初始化过程无关。
例如,我们可以分配一个大小为0x400字节的块,因为系统不太可能在我们的攻击过程中分配和释放相同大小的块,也就不会在喷射中引发错误而影响exploit的可靠性了。您可以通过创建命名对象(例如事件)在分页池中分配这些块。对象名称字符串由WCHAR字符组成,然后将其放入KESS驱动程序用于创建模块列表的同一个页面池中。我们可以设置易受攻击的缓冲区的大小以及用作事件名称的字符串的大小,以使它们进入相同的PoolDescriptor.ListHeads列表。
我们还需要在各个分页池中分配一个块数组。实验表明,在exploit中逐个创建对象会导致相同的池被重用以存储对象的名称字符串,因此,在存在漏洞的驱动程序函数中调用分配程序时,返回的内存块可能源自系统多个页面池中的任何一个。在我们的exploit中,每创建1000个对象后,我们就会添加一个很小的延迟,这个时间通常足以让分配程序切换页面池索引。这样,所有的页面池都将被填满。
接下来,我们通过喷射制造大小为单个内存块的孔洞,从而破坏之前创建的一些对象。指定大小的空闲块被返回给PoolDescriptor.ListHeads列表,然后等待KESS驱动程序去分配。
利用溢出
由于exploit的这部分内容与具体的架构有关,所以这里将使用32位Windows 7 作为目标系统。
在页面池中,可以通过不同的方法来利用溢出漏洞。我们使用的方法是覆盖下一个内存块中的Poolindex。当内存块被释放时,该值被用为PoolDescriptor的索引,指向释放的内存块所要添加到的ListHead。
PoolDescriptor含有PendingFrees列表,即等待添加到ListHeads的块。
首先,ExFreePoolWithTag在其自身内部调用ExDeferredFreePool来释放PendingFrees:合并空闲的相邻块并将它们附加到所需大小的ListHead中。然后,控制从ExDeferredFreePool返回给ExFreePoolWithTag,释放的块将通过该函数添加到PendingFrees中。
当覆盖PoolIndex的值大于系统创建的页面池(在我们的例子中为5)的数量时,对ExDeferredFreePool的调用将采用NULL值——数组中未初始化的地址,当创建页面池时会将这些地址添加到PoolDescriptor。
ExDeferredFreePool必须解除NULL引用,并根据其算法使用PendingFrees和该无效地址结构中的其他成员进行工作。
对于Windows 7来说,NULL指针解引用是必须使用的选项。使用NtAllocateVirtualMemory系统API,我们可以从NULL开始选择内存页,这使得该这片内存非常便于进行读写操作。在这些内存页上,我们可以精心伪造一个PoolDescriptor,让所有成员都具有合适的值,以使ExDeferredFreePool可正常工作。
ExDeferredFreePool将使用我们伪造的PoolDesciptor,并传入PendingFrees列表。它将从该列表中获取一个内存块,检查它和相邻内存块的头部,并将内存块的地址插入到我们的页面池对应的ListHead中。这就是整个行动的关键。
为了将一个表项添加到链接列表中,ExDeferredFreePool中的代码将使用存储在我们的描述符中的指针,并根据指针(从Pendingfrees中释放的内存块的地址)从我们的描述符中写入相应的值。
这样,我们已经把相应缓冲区后面的分配头部覆盖了,从而可以向任意地址写入任意的值了。
充分利用向任意地址执行写操作的能力
在这个基础上,我们可以在一些回调函数中记录shellcode地址,并实现内核模式的执行。这里有一个经典的令牌窃取shellcode,它可以浏览系统中的进程列表,取访问令牌的地址,将我们的进程的访问权限升级到最大的“NT AUTHORITY / SYSTEM”。
但是,我们将采用其他的方法。实际上,有一个NtQuerySystemInformationsystem API,它可以将系统中所有处理程序的信息写入SystemHandleInformation参数。 对于每个处理程序,其引用的内核对象的地址讲被全部公开。 我们可以使用以下方式获取进程的令牌的处理程序:
OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &htoken)
该对象就是我们要覆盖的目标,因为它包含了定义我们在系统中的访问权限的字段。
只要在正在寻找其地址的令牌中设置一个位,就可以直接赋予我们的进程以SeDebugPrivilege特权,而根本无需劫持内核代码控制流。
利用SeDebugPrivilege
我们的进程由于具备了系统范围级别的调试权限,因此可以读取、写入和执行其他进程中的代码,包括系统进程。通过WriteProcessMemory和CreateRemoteThread,我们对任何进程都可以轻松地进行注入,而不是仅限于有限的用户进程。
还有一些小的细节:
从Windows 7开始,我们无法通过CreateRemoteThread在自己的会话之外的进程中创建新线程。所以,理想的目标是KESS服务进程:)
在会话内部,winlogon也可以用,这时我们的代码能够获取到“NT AUTHORITY/SYSTEM”令牌。
有人会想,对于HIPS来说,最关心的难道不就是通过WriteProcessMemory和CreateRemoteThread进行注入吗?然而,KESS并没有从事这方面的行为分析,而是采取了完全容忍的态度。
因此,您完全能够利用KESS的漏洞来提升权限。
完整的攻击向量
从该产品中发现的安全漏洞来看,我们认为攻击者完善的计划大致如下:
1.在ATM机的塑料面板上打一个孔;然后,访问USB总线。当然,如果可以直接接触到计算机本身就再好不过了。
2.连接键盘模拟器,打开记事本,输入一个base64编码的zip文件,其中存放供将来使用的各种工具。保存该文件。
3.键入将上面的文件解码为二进制形式的VBS脚本,运行脚本,并解压文件。
4.将exploit保存到自动运行的注册表中。重新启动计算机。
5.您必须选择最佳选项,以便您不必在文件中携带任何附加组件。
5.1。如果目标机器运行的是Windows XP,则使用我们提供的脚本运行NTSD调试器。通过脚本控制该调试器,将shellcode注入到某个进程中,例如Calc。
5.2。如果目标机器运行的是较高版本的操作系统,则通过另一个脚本运行PowerShell,该脚本使用VirtualAlloc、WriteProcessMemory和CreateThread在其内存中运行shellcode。
6.将Shellcode读入内存并运行exploit的主要部分,以利用KESS驱动程序漏洞来提升权限。
7. 如果exploit一切顺利的话,攻击者将以“NT AUTHORITY/SYSTEM”权限进入系统。这样就可以运行该攻击中用到的最后一部分代码了。我们可以通过两种方式将命令发送到吐钞器:
7.1。自己填写数据包,并将其发送给驱动程序,让其发送到设备。
7.2。使用通用和有文档说明的XFS界面,这样与设备通信时,就不用考虑具体的硬件类型了。这是在大多数已知的ATM恶意软件中所使用的方式,这些软件包括:Tyupkin、Atmitch、GreenDispenser和Suceful。
小结
我们的分析表明,不包含内置保护机制或不包含OS设计中隐含的保护机制的软件非常容易被攻击者绕过,因为其所在系统的属性与该软件蕴含的安全属性不兼容 。因为,操作系统不是为了维护这种边界而设计的。
将可执行文件静态过滤为可信和不可信(或恶意)文件的方法,并不能杜绝机器状态被操纵的情况发生。
此外,由这种特权实体进行文件分类的复杂逻辑,实际上扩大了被攻击面,因为这样的话,其代码中的常见缺陷会给系统引入更多的漏洞。
Exploit演示视频