续之前的Windows,最近应该会学习一波Windows的Pwn题。但是,之前还有几个比赛的题还没做完,tcl,做题太慢了。
基础知识
库介绍
ntdll.dll:描述了 windows 本地
NTAPI的接口,是重要的Windows NT内核级文件。位于Kernel32和user32.dll中的所有win32 API最终都是调用Ntdll.dll中的函数实现的。kernel32.dll:是32位动态链接库文件,属于内核级文件,控制系统的内存管理、数据的输入输出操作和中断处理;
User32.dll:是
Windows用户界面相关应用的程序接口,用于包括Windows处理,基本用户界面等特征,如创建窗口和发送消息。Gdi32.dll:是存放在
Windows系统文件夹中的一个动态链接库,是Windows下图形用户界面的应用拓展;ucrtbased.dll:是Runtime Library组件,即 VC 运行库中的相关动态链接库文件,例如 vs中的各种集成开发环境都会需要该组件才能够运行。
Windows下函数调用
Windows的 x64下只有一种函数调用约定,即 __fastcall,其他调用约定的关键字会被忽略,也即 ABI只有 __fastcall
一个函数在调用时,前四个参数是从左至右依次存放于 RCX、RDX、R8、R9寄存器里面,剩下的参数通过栈传递,从右至左顺序入栈。注意这里的参数寄存器与Linux不同。
如果是 int f(double a, double b, double c, double d, double e, double f) 这样的函数,前四个浮点类型的参数从左到右由 XMM0,XMM1,XMM2,XMM3 依次传递,剩下的参数通过栈传递,从右至左顺序入栈。
被调用函数的返回值64位以内,仍然会被放入 RAX; 如果是 浮点值,则返回值存放入 XMM0.
注意:更大的返回值(比如结构体),由调用方在栈上分配空间,并由 RCX持有该空间的指针并传递给被调用函数,因此整型参数使用的寄存器依次右移一格,实际只可利用 RDX,R8, R9 3个寄存器,其余参数通过栈传递。函数调用结束后,RAX 返回该空间指针。
2020 QWB Overflow
程序分析

程序功能很简单,就三次溢出。
利用分析
- 泄露 GS_Cookie
由于栈上存在 GS_Cookie,所以当我们覆盖返回地址实现 ROP时,会由于覆盖了 GS_Cookie而导致程序出错,所以我们需要先泄露 GS_Cookie,然后ROP将该位置填上正确的 GS_Cookie;如下图所示,三个箭头的位置从上至下分别是 GS_Cookie,rbp和返回地址。

- 泄露程序基址和
ntdll.dll地址
windows下的 ASLR机制与linux不同,PIE_base 和 dll_base 其低 2 bytes均为0,而且在短时间是不会变化的,经过我测试发现程序基址只要不重启应该都不会改变。
泄露程序基址,可以通过直接泄露返回地址,即可得到当前程序的基址;
泄露 ntdll.dll基址,我们可以看一下执行到 main函数时 程序的函数调用栈,如下所示,可以看到在main之前分别调用了 Kernel32和ntdll。也就是说这几个库的函数栈一定在当前 main函数的调用栈下面,我们可以从上面的那张图也看到,在main函数的返回地址之后出现了一个 0x7fff20c67c24地址,这个地址就是 Kernel32库里的地址,证明了我们函数调用栈的结果。

我们从 main函数继续向下查找,可以看到 能找到 ntdll库函数的地址,那么我们可以成功泄露 Ntdll的地址。

泄露 ntdll的地址是为了方便我们使用 gadget:
- 泄露
ucrtbased.dll基址和security_cookie
接着就要泄露 ucrtbased.dll的基址,这个库是 VC程序执行必须要有的库,里面放入了很多 VC程序的API调用接口,类似于 Glibc.so。
我们可以通过他找到 system地址和 cmd.exe字符串。


这个库的地址,不能在 Main函数下方找不到,不能够直接泄露。其泄露思路和 glibc下的常见泄露思路是一致的,Linux是使用puts函数输出 got表的地址,即可泄露 Libc地址;Windows下之前提过没有 got表,但是有导入导出表,我们也可以通过 Puts函数输出导入导出表里的地址,来泄露 各个库的基址。
如下所示,我们在导入表里可以找到 read函数,其由 ucrtbased.dll导入。

我们动态调试,可以发现程序中该导入表的地址已经存储了 ucrtbased.dll中 read函数的真正位置。

注意,还有一点是需要注意的地方,即windows下也没有了 plt表,如果我们想调用一个函数需要直接使用含有call该函数指令的 Gadget来调用。
例如 Linux下,我们要泄露 read_got地址,rop如下:
1 | p64(p_rdi_ret)+p64(read_got)+p64(puts_plt) |
但是,在windows下,我们需要首先找到 含有 call puts指令的 gadget,如下所示:

1 | p64(p_rcx_ret)+p64(read_import)+p64(call_puts) |
然后,我们还需要泄露 security_cookie这个值是存储在 程序地址空间中的一个随机值,作用就是每次新生成栈时用来和rsp异或生成 新的 GS_Cookie的值。由于每次执行main,rsp都会抬高 0x130。拿到这个值了,我们才能够在再次执行 Main函数时绕过 GS_Cookie验证:
getshell
最后getshell的方法,就是执行 system('cmd.exe')函数了,ROP如下:
1 | p64(p_rcx_ret)+p64(cmd_exe_addr)+p64(p_rdi_rsi_ret)+p64(0)+p64(0)p64(system_addr) |
EXP
这题目,我环境有点怪,我执行程序时,直接一下三次输出输入执行完,程序退出了。程序就没有停下来让我输入的时候。不知道是啥原因,所以也没测自己的EXP有没有问题。
1 | from pwn import * |
Windows 堆基础
之前分析的几题,都是针对 windows下的 栈溢出漏洞。对于 堆漏洞还未有涉及。我们首先来补充一点 windows下的堆基础知识。
Windows NT 堆
Windows下的 NT堆已经被研究的比较透彻,下面主要是参考 0day安全总结的。
堆数据结构
堆块:和 linux下chunk类似,包含堆头和数据区。堆头的数据结构如下(以32位为例):
1 | 0 1 2 3 4 5 6 7 8(byte) |
其中Flags是标识堆块状态:01表示使用。Flink和 Blink相当于 Linux中的 fd和 bk指针,如果堆块处于使用状态会被数据区占用。
堆表:堆表一般位于堆区的起始位置,用于索引堆区中的所有堆块的重要信息,包括堆块的位置、堆块的大小、空闲还是占用等。堆表分为两类:
- 空表
堆区一开始的堆表区中有一个128项的指针数组,被称为空表索引(Freelist array)。该数组的每一项包括两个指针,用于标识一条空表。也就是每一个 size的空表是一个双链表指针,而且都是从后往前取,遵循 LIFO原则,这和 Linux下的 smallbin 和 Largebin类似。其中 free[0]类似 unsortedbin链表,存储的空闲堆块是按照size从小到大 从前往后排列。其余的 空表链表,都是类似 smallbin,每一个链表表示一个 size,存储的堆块都是相同size。
- 快表
快表是 Windows用来加速堆块分配而采用的一种堆表,这是一种单向链表。快表也有128条,每个快表最多有4个结点。每个快表的节点的flag都被设为使用态,其和 Linux下的 fastbin机制十分类似。
堆管理策略
堆块分配
NT堆分配可分为三类:快表分配、普通空表分配和 零号空表分配:
快表分配:找到大小匹配的空闲堆块,将其状态修改位占用态,从链表中 Unlink,最后返回一个指向堆块块身的指针给程序使用;
普通空表分配:优先寻找大小最合适的空闲块分配,若失败则依次寻找次优的空闲块分配,即最小的能够满足要求的空闲块;
零号空表分配:其按照大小升序链着不同大小的堆块,分配时从后向前查找,找到最后一个最合适大小的chunk分配出去。
堆块释放
将堆块状态改为空闲,链入相应的堆表。所有的释放块都链入堆表的末尾,分配的时候也从堆表末尾拿。
堆块合并
堆块合并是将两个相邻的空闲堆块从链表中卸下,合并堆块,调整合并后大块的块首信息,再将新块重新链入空闲链表。

注意:针对NT 堆的最常见的攻击还是对 unlink攻击。
Windows10 堆
Windows10下的堆机制更加复杂,其分为两种:NT堆和 Segment Heap。下面主要讲解NT堆,主要参考 Angel Boy 的这个 Slides。如果要了解 Segment Heap机制,可以参考K0Shi的这篇文章。
NT堆又可以分为:后端堆(Back-end)和前端堆(Front-End)
堆函数
HeapCreate
创建一个只有调用进程才能访问的私有堆。进程从虚拟地址空间里保留出一个连续的块,并且为这个块特定的初始部分分配物理空间。
HANDLE HEAPCreate(DWORD flOptions, DWORD dwInitialSize, DWORD dwMaxmunSize)
参数:
flOptions:堆的可选属性,会影响堆的函数操作,指定标记有:
- HEAP_NO_SERIALIAZE:指定当函数从堆里分配和释放空间时不互斥(不适用互斥锁)。当不指定该标记时默认为使用互斥。序列化允许多个线程操作同一个堆而不会错误。这个标记是可忽略的;
- HEAP_SHARED_READONLY:这个标记指定这个堆只能由创建它的进程进行写操作,对其他进程是只读的。如果调用者不是可靠的,调用将会失败,错误代码
ERROR_ACCESS_DENIDE,为了使用该标记,运行在 Kernel mode(核心状态)是必须的
dwInitialsize:堆的初始大小,单位为 Bytes,其大小会向上舍入直到下一个
page boundary。若需得到主机的页大小,使用 GetSystemInfo函数。dwMaxmumSize:非零时指定这个堆的最大大小,也是向上舍入到一个
Page boundary,然后为这个堆再进程的虚拟地址里保留舍入后大小的块。如果函数HeapAlloc和HeapReAlloc分配的空间超过参数dwInitialSize指定的大小,系统会分配额外的空间给该堆直到这个堆的最大大小;
返回值:
- 成功:返回新创建的对指针
- 失败:Null指针
- 调用函数
GetLaseError获得更多的错误信息
注意:
- 生成的是私有堆,只有调用进程才能够访问,会在虚拟空间内创建一个块,实现对增长
HeapAlloc请求空间超过当前页大小,物理空间足够,则会从保留的空间里附加- 一个 DLL 创建了一个私有堆,这个私有堆是在调用该DLL的进程的地址空间内,且仅该进程可访问
- 系统会使用私有堆的一部分空间去储存堆的结构信息
HeapAlloc
1 | LPVOID HeapAlloc( |
hHeap:分配堆的句柄,可以通过
HeapCreate()函数或GetProcessHeap()函数获得dwFlags:堆分配时的可选参数:
- HEAP_GENERATE_EXCEPTIONS:分配错误将会抛出异常
- HEAP_NO_SERIALIZE:不使用连续存取
- HEAP_ZERO_MEMORY:将分配的内存全部清零
dwBytes:要分配堆的字节数
HeapFree
1 | BOOL HeapFree( |
Heap 数据结构
_HEAP
每个堆段都有一个 _HEAP结构,其是管理该堆段的核心数据结构,位于该堆段的头部。
每个 Heap 有一个 HEAP结构,一个 heap 结构有多个 heap_segment。
1 | heap结构{ |
首先使用 .process 查看进程的 PEB 地址,随后使用 td _PEB peb_addr查看进程的 PEB信息,如下所示:

进程的 PEB信息十分全面,我们重点关注如下结构信息:
1 | 0x30 ProcessHeap: 默认的堆地址 |

根据ProcessHeaps可以查看当前进程所申请堆地址,如下图所示,程序总共申请了三个堆地址。

通过该方法的查看结果与我们直接使用 !heap -h命令查看的结果一致:

还有一个注意事项是:实现 !heap命令查看堆布局,可以发现进程中的堆都是 NT HEAP,未开启 win10下的新机制 Segment Heap,这对于研究win堆是需要区分的。

上述查询的地址,每一个都是一个 Heap结果,我们对该地址可以直接使用 dt _HEAP heap_addr查看,如下图所示。也就是 HEAP结构是在每一个 通过 Heap_Create创建的堆起始地址。同时每个 HEAP结构是由每个堆的 0号堆段和一个特殊结构拼接而成,特殊结构中的 Heap结构是用来保存该堆段的资产及必要信息。

在这个结构体重点关注:
1 | 0x40 FirstEntry 第一个堆头地址 |
后端堆管理
_Heap_ENTRY(Chunk)
如果没有接触过 windbg调试堆的,可以参考一下这篇教程。
_HEAP_ENTRY可以分为 Allocated chunk、Freed chunk和 VirtualAlloc chunk三类。下面这图只表现了 Freed chunk,但是 Allocated chunk除了 Flink和Blink其他也类似。

其堆头数据结构如下:
1 | PreviousBlockPrivateData 基本上可为前一块chunk的data,因为chunk必须对齐0x10 |
其中flag的值对应的结果如下
01-HEAP_ENTRY_BUSY堆块处于占用状态02-HEAP_ENTRY_EXTRA_PRESENT该块存在额外的描述03-HEAP_ENTRY_FILE_PATTERN使用固定模式填充堆块08-HEAP_ENTRY_VIRTUAL_ALLOC虚拟分配的堆块virtual allocation10-HEAP_ENTRY_LAST_ENTRY表示是该段的最后一个堆块
类似于 Linux下的 chunk。前 8 字节保存结构信息,类似 chunk 头,但是 windows 为了安全性,堆前 8 字节进行了加密。加密方式:与 Heap 结构 0x80偏移处 16(64位) 个字节异或,以此防止堆溢出。

可以看到堆头的数据和 Encoder异或后的结果,低位 两字节 0x0013是当前堆块的大小(64位单位是 0x10对齐,所以就是0x130字节),flags是7,Perv_size是 0x0d。我们分析的结果和图中最后直接显示的一致。
FreeLists
当我们对一个释放一个堆块后,其结构如下。可以看到

其Flink 指向了 下一个空闲堆块,Blink指向了 Freelist,注意:此处的 Flink 和 Blink 都是指向了freed chunk的 数据区,并非堆头,这里和 tcache 有点类似 。当释放一个堆块,其会按照size插入 Freelist中,Freelist中的堆块是按照从小到大排列。

Windows下通过堆头部的BlocksIndex的成员变量(单向链表)起到Linux中smallbin/largebin的效果,快速找到相应大小的释放的堆块,其结构体如下,
1 | 0:000> dt _HEAP_LIST_LOOKUP |
分配机制
基本按照size大小,分为三种,0day安全里描述为 小块、大块和巨块。
Size <= 0x4000:
分配都会在
RtlpAllocateHeap,然后会在chunk size中的
FrontEndHeapStatusBitmap是否启用LFH如果没有,会对对应的
FrontEndHeapUsageData加上 0x21,并且检查值是否超过0xff00或者 & 0x1f 后超过 0x10 通过条件就会启用 LFH
然后回判断对应的
ListHind是否有值,会以ListHint中的chunk为优先:- 如果有适合的
chunk在ListHint上则移除ListHint,并且判断该chunk的 Flink 大小是否也为同样size - 如果是,则将
ListHint填上Flink,不是则清空;最后则unlink该chunk把这块chunk从Linked list中移除返还给使用者,并将header xor回去。 - 如果没有合适的,从比较大的
ListHint中找,有找到比较大的后将该chunk从ListHint中移除,然后重复步骤5的处理ListHint。然后切割该chunk,剩下的重新加入Freelist,尝试将其大小放入ListHint。最后将切好的chunk传给使用者,并对header加密。
- 如果有适合的
如果
FreeList中都没有:- 尝试
ExtengdHeap加大heap空间 - 再从
extend出来的chunk拿 - 接着后面一样切割,放回
ListHint,xor header
- 尝试
0x4000 < size <= 0xff000
除了没有对 LFH 相关的操作,其余和 0x4000 一样
Size > 0xff000 (VitualMemoryThreshod << 4)
- 直接使用
ZwAllocateVirtualMemory- 类似
mmap直接给一大块,并且回插入_HEAP->VirtualAllocdBlocks这个Linkd list中,这个linked list是连接该Heap VirtualAllocated出来的区块用的
- 类似
释放机制
size<= 0xff0000
检查
alignment、利用Unused byte判断该chunk状态- 如果是 非 LFH下,会对对应的
FrontEndHeapUsageData减一,接着会判断前后的chunk是否为 freed,是的话就合并; - 合并前后堆块,就是 unlink操作,并从 ListHint 移除,移除方式与前面相同,看下一块是不是同样大小,是的话就补上
ListHInt
- 如果是 非 LFH下,会对对应的
合并完后,
update size & prevsize,然后会看看是不是最前跟最后,是就插入;不是就会从ListHint中插入,并且update ListHint,插入时也会对 linked list 做检查
size > 0xff000
- 检查该
chunk的 linked list并从_HEAP->VirtualAllocdBlocks移除 - 接着使用
RtlpSecMemFreeVirtualMemory将 chunk 整个munmap掉
Unlink攻击
知道了上面的释放堆块是用双链表指针前后相连,并且也会存在相邻空闲堆块合并的操作。那么也就和 Linux一样 存在 unlink攻击。我以 Angel Boy Slides 中的 Unlink 攻击,合并讲解 Unlink 时中会做的步骤。

初始堆布局如上图所示,申请了 5个堆块,我们的chunk_list 依次为 P Q R S T。依次释放 Q 和 S 堆块,此时 Freelist 的链表为:
1 | Freelist->Flink = S |
然后修改 Q 的 Flink 为 &Q -0x8,Blink 为 &Q,这里的 &Q 是值得 Q 在 chunk_list 里的地址,并非是 Q的 chunk 地址。
- Free(P)
然后我们 释放 chunk P,此时会检查P前后堆块时否空闲,会找到 后一个 堆块 Q 处于空闲状态
- 检查Q
然后会先对Q的header 进行解密,检查 header是否正确;然后会对 Q的 Flink 和 Blink 执行如下检查:
1 | Flink = chunk->flink |
此时,我们修改的值是能够成功满足的:
1 | *((&Q-8)->Blink) == *(&Q-8+8) = Q |
我们也就能成功过掉check。
- Find BlockIndex
然后就会对 ListHint进行处理,由于此时 ListHint 中存储的是后释放 S堆块,并不是堆块 Q,所以也就不会对 ListHint 进行处理。可以以此绕过对 ListHint 的检测。
- Unlink
执行经典的 Unlink函数,成功改了chunk_list里 &Q地址 的值为 &Q。
1 | Q->Blink->Flink = Q->Flink |
- 更新 P size
随后就是对 P的size 进行更新,然后根据新的 size 判断是不是在freelist中的最前或最后,是就插入;不是就还需要在 LinkHint 中去寻找合适的位置插入。
- 自此,我们实现了 Q 的值为 &Q,我们就可以成功控制 chunk_list。
TSCTF hellowin
这道题和 Angel Boy 当时分享的一道题的思路是十分相似的。
程序分析

首先程序存在一个 格式化字符串漏洞,可以输入 10字节的数据,可以泄露数据。

然后有一个堆溢出漏洞,可以直接修改下一个chunk的值。
利用分析
- 格式化字符串泄露地址
首先利用格式化字符串直接泄露程序的 plt 基址 和栈地址 以及 ucrtbased.dll 的基址。虽然泄露完后程序会退出,但是这个点已提到过,再次运行程序基址不会改变。
- Unlink实现任意地址写数据
然后,我们就需要利用 Unlink 漏洞,来实现任意地址写。程序中存在一个 chunk_list,且delete堆块后其地址仍然不变。如果我们能够劫持 chunk_list 里的值,就能够实现任意地址写。
方法和 上面介绍相同:
初始堆布局:申请 5 个0x58堆块,先释放 chunk2。
修改chunk2:通过chunk1的堆溢出修改chunk2 的Flink 和 Blink 分别为
&chunk_list+0x8和&chunk_list+0x10,此时的&chunk_list+0x10地址的值正好为 chunk2 的地址;释放 chunk4:释放chunk4的原因是 为了将 chunk4 放入 ListHint中,但是不释放 chunk3 的原因是为了 防止chunk3和chunk2堆合并,不释放chunk5也是为了防止 和 freed chunk合并;
释放chunk1:触发 chunk2的 unlink。经过 unlink 函数后, 此时
&chunk_list+0x10的值为&chunk_list+0x10
此时我们就可以通过 edit chunk2 实现对 chunk_list 的值的修改,实现 任意地址写。

- 泄露地址信息
然后,为了实现ROP执行ORW,我们需要泄露 ntdll.dll 和 Kernel32.dll 的地址信息。这道题有一个容易点就是可以直接通过 将 chunk3 改为我们想知道的 地址,再通过 Show chunk3函数将我们想知道的地址的值输出。这样就可以结合每个dll里的 IAT表,不断泄露每个dll的基址。
注意:这里有一个巨坑,就是在将 Kernel32.dll里的 有关 ntdll的 iat地址写入chunk3,其地址千万不要有 0a出现,否则就会写不进去。这导致我后面一直出错。
其次,我们需要获取函数的返回堆栈地址。我们可以采用爆破的方式,不断爆破栈数据。知道输出的栈数据是 main函数的返回地址,这样我们即可确定该 栈地址存储的是一个 返回地址。我们后续写 ROP,即可在此栈地址处开始写。
- ROW
如果我们能够直接获得 ucrtbased.dll,就可以直接调用该库里面的 ROW函数来得到 flag。
但是,如果不知道 ucrtbased.dll的信息,我们可以选择更通用的方法。这个方法我在之前进行 Adobe漏洞分析时其实也看到过真实漏洞利用也是有这样利用的。
EXP
1 | from pwn import * |
- 本文作者: A1ex
- 本文链接: http://yoursite.com/2020/10/30/TSCTF-helloWin/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!