admin 管理员组文章数量: 887021
环境搭建
checksec
winchecksec
winchecksec 是 windows 版的 checksec ,不过有时候结果不太准确。
checksec(x64dbg)
x64dbg 的插件 checksec 检查效果比较准确,并且可以连同加载的 dll 一起检测。
将 release 的插件按 32 和 64 位分别放到 x32dbg 和 x64dbg 的 plugins 目录,如果找不到 plugins 目录则打开调试器然后关闭就出现了。
winpwn
winpwn 是 windows 平台上类似 pwntools 的 python 库,使用这个库可以更方便的编写 exp 。
winpwn 支持如下功能:
1. process
+ process("./pwn")
+ process(["./pwn","argv[1]","argv[2]"])
+ p.readm(addr,n) # read process memory
+ p.writem(addr,con="") # write process memory
2. remote
+ remote("127.0.0.1", 65535)
3. context
+ context.timeout=512
+ context.debugger="gdb" # or "windbg" or "x64dbg"
+ context.endian="little"
+ context.log_level="" # or "debug"
+ context.terminal=[ ]
+ context.newline="\r\n"
+ context.arch="i386" # or "amd64"
+ content.pie=None
+ context.dbginit=None # used to set debugger init script
+ context.windbg=None # set debugger path, or use .winpwn to find debugger path
+ context.windbgx=None
+ content.gdb=None
+ context.x64dbg=None
+ context.nocolor=None # if set, will print non-colorful output to terminal
4. dbg: windbgx, windbg, gdb, x64dbg
+ windbgx.attach(p,script="bp 0x401000")
+ windbg.attach(p,script="bp 0x401000")
+ gdb.attach(p, script="b *0x401000")
+ x64dbg.attach(p) #can not parse script file yet
5. disable PIE:
+ PIE(exe_fpath="")
+ NOPIE(exe_fpath="")
6. asm/disasm:
+ asm("push ebp")
+ disasm("\x55")
7. winfile(fpath="./main.exe"):
+ winfile.symbols["CreateProcessA"] # return symbol's IAT/EAT offset of CreateProcessA by image base
8. wincs(ip,port)
+ wincs(ip=None,port=512): run a server to asm/disasm in remote machine for client where does not install keystone/capstone
+ wincs(ip='123.123.123.123',512): create a client to connet to server
+ wincs.asm(asmcode='push ebp')
+ wincs.disasm(machinecode='\x55')
安装
这里我使用的是 python2 版本的 winpwn 。
pip install pefile
pip install keystone-engine
pip install capstone
pip install winpwn
添加功能
winpwn 中缺失了一些 pwntools 中的功能,这里我们修改 winpwn 库添加一下(修改好的项目)。
-
添加 sendline/sendlineafter
-
添加 search ,这里 rebase 决定是否按照 ImageBase 进行重定位。
from winpwn import * context.arch = 'i386' pe = winfile("./stackoverflow.exe", rebase=True) print hex(pe.search(asm('push eax'), executable=True).next()) # 0x4117cd
-
symbols 返回符号地址而不是导入/出表地址
-
设置默认调试器路径,可以在
context.py
中设置调试器路径,这样就不用每次写脚本都设置了。这里我的调试器都添加到环境变量了,所以不需要写完整路径。gdb = "gdb" windbg = "WinDbgX" windbgx = "WinDbgX" x64dbg = "x64dbg" if arch == "amd64" else "x32dbg"
-
添加 info,success,fail 的 log 功能与 pwntools 类似。
windbg
安装 windbg
直接在微软商店下载 WinDbg Preview 。
如果 WinDbgX
已经添加到环境变量就可以使用在 winpwn 脚本中用 windbg 附加调试进程。
context.windbgx = 'WinDbgX'
windbgx.attach(p)
在 windbg 中 选择 setting->Debugging settings ,在 Default symbol path 一栏填上,其中 c:\mysymbols
可以换成其他路径。
srv*C:\mysymbols*https://msdl.microsoft/download/symbols
这样 windbg 下载的调试符号就可以保存到对应路径下,下一次调试就不用重新下载符号了。
另外 WinDbg 支持安装插件,比如 windbg-scripts 可以让 WinDbg 支持 !telescope
命令。
将 windbg-scripts 项目下载下来后,修改项目目录下的 Minfest
目录下的 config.xml
,将其中的 LocalCacheRootFolder
的路径改为 Minfest
目录的绝对路径。之后在 WinDbg 命令行中输入 .settings load c:\path\where\cloned\windbg-scripts\Manifest\config.xml
和 .settings save
然后重启 WinDbg 就可以使用 !telescope
命令。
0:004> !telescope 0x0168fdf4
Populating the VA space with modules..
Populating the VA space with TEBs & thread stacks..
Populating the VA space with the PEB..
0x0168fdf4|+0x0000: 0x76f335f9 (ntdll.dll (.text)) -> jmp ntdll!_DbgUiRemoteBreakin@4+0x42 (76f33602) ; xor eax,eax ; inc eax
0x0168fdf8|+0x0004: 0xe8b095a5 (Unknown)
0x0168fdfc|+0x0008: 0x76f335c0 (ntdll.dll (.text)) -> push 8 ; push 76F937F8h ; call ntdll!__SEH_prolog4 (76f17810)
0x0168fe00|+0x000c: 0x76f335c0 (ntdll.dll (.text)) -> push 8 ; push 76F937F8h ; call ntdll!__SEH_prolog4 (76f17810)
0x0168fe04|+0x0010: 0x00000000 (Unknown)
0x0168fe08|+0x0014: 0x0168fdf8 (Stack) -> 0xe8b095a5 (Unknown)
0x0168fe0c|+0x0018: 0x00000000 (Unknown)
0x0168fe10|+0x001c: 0x0168fe78 (Stack) -> 0x0168fe90 (Stack) -> 0xffffffff (Unknown)
0x0168fe14|+0x0020: 0x76efe8b0 (ntdll.dll (.text)) -> mov edi,edi ; push ebp ; mov ebp,esp
0x0168fe18|+0x0024: 0x9f215c7d (Unknown)
@$telescope(0x0168fdf4)
ret_sync 实现 ida 和 windbg 联动调试
ret_sync 可以把 windbg 的调试位置同步到 ida 上,配置方法如下:
-
将 ret_sync 项目下的
ext_ida
文件夹中的文件都复制到 IDA 的 plugins 文件夹下。 -
到这个网址下载
ret-sync-release-windbg-Win32
和ret-sync-release-windbg-x64
两个插件。 -
把下载好的
sync.dll
和sync32.dll
( 32 位文件夹下的sync.dll
改名为sync32.dll
),复制到C:\Users\username\AppData\Local\Microsoft\WindowsApps
文件夹下(与 WinDbg 在同一目录)。旧版windbg插件安装,参考 -
把要调试的 exe 和 dll 都用 ida 打开,然后都选择 Edit->plugins->ret_sync 。
如果正常的话在 ida 的命令行中会看到如下输出:[sync] default idb name: stackoverflow.exe [sync] sync enabled [sync] cmdline: "C:\Python27\python.exe" -u "C:\Program Files\IDA_Pro_7.7\plugins\retsync\broker.py" --idb "stackoverflow.exe" [sync] module base 0x400000 [sync] hexrays #7.7.0.220118 found [sync] broker started [sync] plugin loaded [sync] << broker << connected to dispatcher [sync] << broker << dispatcher msg: add new client (listening on port 4730), nb client(s): 2
-
打开 WinDbg,选择 文件->Launch executable 或者附加进程进入调试状态,然后输入
!load sync
,如果是调试 32 位程序则输入的是!load sync32
。0:000> !load sync32 [sync] DebugExtensionInitialize, ExtensionApis loaded
sync 常用命令如下:
!synchelp
查看帮助!sync
完成ida 和 WinDbg 同步。结合 ida 的 Synchronize with 就可以实现 windbg 与 ida 反编译结果的同步。
!idblist
查看已建立连接的 idb 。因为前面我打开了ntdll.dll
和stackoverflow.exe
并且都运行了 ret_sync 插件,因此这里可以看到ntdll.dll
和stackoverflow.exe
。0:000> !sync [sync] No argument found, using default host (127.0.0.1:9100) [sync] sync success, sock 0x76c [sync] probing sync [sync] sync is now enabled with host 127.0.0.1 0:000> !idblist [0] ntdll.dll [1] stackoverflow.exe
可以在 exp 脚本上写好同步命令,这样附加进程后就能与 ida同步。
windbgx.attach(p,"!load sync32\n!sync\nbp 00700000+0119B7\n")
常用命令
windbg 的 LocalHelp 可以查看帮助文档。
寄存器
r
查看寄存器状态和当前运行指令0:000> r eax=00000210 ebx=003ac000 ecx=001ef9d0 edx=00000000 esi=001efbd4 edi=001efda8 eip=007119b7 esp=001efbd4 ebp=001efda8 iopl=0 nv up ei pl nz ac pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000216 stackoverflow+0x119b7: 007119b7 ff1574b17100 call dword ptr [stackoverflow+0x1b174 (0071b174)] ds:002b:0071b174={ucrtbased!getchar (7aebcf70)}
r @rax=1234
可以修改寄存器的值
地址
-
lmi
查看进程加载的各个模块。通过这个命令可以获得模块的加载基址。0:000> lmi start end module name 00700000 00720000 stackoverflow C (no symbols) 753e0000 75652000 KERNELBASE (deferred) 75b20000 75c10000 KERNEL32 (pdb symbols) C:\Windows\System32\KERNEL32.DLL 76e80000 7702f000 ntdll (pdb symbols) C:\Windows\SYSTEM32\ntdll.dll 7add0000 7adee000 VCRUNTIME140D (deferred) 7adf0000 7af94000 ucrtbased (private pdb symbols) C:\Windows\SYSTEM32\ucrtbased.dll
-
!address
查看更详细的段信息,类似 pwndbg 的vmmap
功能。0:000> !address Mapping file section regions... Mapping module regions... Mapping PEB regions... Mapping TEB and stack regions... Mapping heap regions... Mapping page heap regions... Mapping other regions... Mapping stack trace database regions... Mapping activation context regions... BaseAddr EndAddr+1 RgnSize Type State Protect Usage ----------------------------------------------------------------------------------------------- + 0 60000 60000 MEM_FREE PAGE_NOACCESS Free + 60000 63000 3000 MEM_MAPPED MEM_COMMIT PAGE_READONLY MappedFile "\Device\HarddiskVolume3\Windows\System32\l_intl.nls" + 63000 70000 d000 MEM_FREE PAGE_NOACCESS Free + 70000 80000 10000 MEM_MAPPED MEM_COMMIT PAGE_READWRITE MappedFile "PageFile" + 80000 83000 3000 MEM_MAPPED MEM_COMMIT PAGE_READONLY MappedFile "\Device\HarddiskVolume3\Windows\System32\l_intl.nls" + 83000 90000 d000 MEM_FREE PAGE_NOACCESS Free + 90000 af000 1f000 MEM_MAPPED MEM_COMMIT PAGE_READONLY Other [API Set Map] ...
-
!address 地址
查看某个地址所在段信息。0:000> !address 75bd0000 Usage: Image Base Address: 75bd0000 End Address: 75bd1000 Region Size: 00001000 ( 4.000 kB) State: 00001000 MEM_COMMIT Protect: 00000004 PAGE_READWRITE Type: 01000000 MEM_IMAGE Allocation Base: 75b20000 Allocation Protect: 00000080 PAGE_EXECUTE_WRITECOPY Image Path: C:\Windows\System32\KERNEL32.DLL Module Name: KERNEL32 Loaded Image Name: C:\Windows\System32\KERNEL32.DLL Mapped Image Name: More info: lmv m KERNEL32 More info: !lmi KERNEL32 More info: ln 0x75bd0000 More info: !dh 0x75b20000 Content source: 1 (target), length: 1000
断点
-
bp 地址
在某地址处下断点,另外常见命令如bp ucrtbased!system
可以在system
函数下断点。 -
bp <address> "<condition>"
在某地址处下条件断点,例如bp 00401234 "eax==0"
。 -
bl
查看断点,直接点击Disable
来暂时停用断点,点击Clear
清除断点。0:000> bp ucrtbased!system 0:000> bl 0 e Disable Clear 007119b7 0001 (0001) 0:**** stackoverflow+0x119b7 1 e Disable Clear 7ae6b8f0 [minkernel\crts\ucrt\src\desktopcrt\exec\system.cpp @ 79] 0001 (0001) 0:**** ucrtbased!system
内存
-
dq
八字节查看,dd
四字节查看,dw
两字节查看,dc
一字节查看。0:000> dc 001efbd4 001efbd4 00711348 00711348 003ac000 cccccccc H.q.H.q...:..... 001efbe4 cccccccc cccccccc cccccccc cccccccc ................ 001efbf4 cccccccc cccccccc cccccccc cccccccc ................ 001efc04 cccccccc cccccccc cccccccc cccccccc ................ 001efc14 cccccccc cccccccc cccccccc cccccccc ................ 001efc24 cccccccc cccccccc cccccccc cccccccc ................ 001efc34 cccccccc cccccccc cccccccc cccccccc ................ 001efc44 cccccccc cccccccc cccccccc cccccccc ................
-
eq <address> <value>
修改 8 字节长度的内存中的值。ed
,ew
,eb
同理,只是修改内存长度有区别。 -
u 地址
查看某地址处的汇编,u
查看程序运行位置的汇编,uf
会一值反汇编到 ret 指令。0:000> u 00700000+119b7 stackoverflow+0x119b7: 007119b7 ff1574b17100 call dword ptr [stackoverflow+0x1b174 (0071b174)] 007119bd 3bf4 cmp esi,esp 007119bf e85df8ffff call stackoverflow+0x11221 (00711221) 007119c4 33c0 xor eax,eax 007119c6 52 push edx 007119c7 8bcd mov ecx,ebp 007119c9 50 push eax 007119ca 8d15ec197100 lea edx,[stackoverflow+0x119ec (007119ec)]
-
dt structure [address]
把 address 当成 structure 类型的结构体解析,如果不加 address 就会单纯打印出结构体。
例如已知 4 个堆,想查看第一个 heap 的 _HEAP ,因为 _HEAP 就在 heap 的开头,所以第一个 heap 的 _HEAP 就是 23c9cb00000 。0:001> !heap Heap Address NT/Segment Heap 23c9cb00000 NT Heap 23c9c9d0000 NT Heap 23c9e530000 NT Heap 23c9e990000 NT Heap 0:001> dt _heap 23c9cb00000 ntdll!_HEAP +0x000 Segment : _HEAP_SEGMENT +0x000 Entry : _HEAP_ENTRY +0x010 SegmentSignature : 0xffeeffee +0x014 SegmentFlags : 2 +0x018 SegmentListEntry : _LIST_ENTRY [ 0x0000023c`9cb00120 - 0x0000023c`9cb00120 ] +0x028 Heap : 0x0000023c`9cb00000 _HEAP +0x030 BaseAddress : 0x0000023c`9cb00000 Void +0x038 NumberOfPages : 0xff +0x040 FirstEntry : 0x0000023c`9cb00740 _HEAP_ENTRY +0x048 LastValidEntry : 0x0000023c`9cbff000 _HEAP_ENTRY +0x050 NumberOfUnCommittedPages : 0xce +0x054 NumberOfUnCommittedRanges : 1 +0x058 SegmentAllocatorBackTraceIndex : 0 ...
-
s -a 7adf0000 L100000 "cmd.exe"
搜索字符串-a
表示搜索 ascii 码6a450000
表示搜索起始位置L100000
表示搜索范围是 100000 字节"cmd.exe"
表示搜索内容为 “cmd.exe”
效果如下:
0:000> s -a 7adf0000 L100000 "cmd.exe" 7ae360ec 63 6d 64 2e 65 78 65 00-69 00 73 00 6c 00 65 00 cmd.exe.i.s.l.e.
调试
g
继续运行p
步过t
步入gu
步出k
查看 trace back
线程
~*
用来查看所有线程的信息,可以用来获取 TEB 基址。0:000> ~* . 0 Id: b7d0.3784 Suspend: 1 Teb: 00000063`08c76000 Unfrozen Start: dadadb+0x1125 (00007ff6`6cb51125) Priority: 0 Priority class: 32 Affinity: ffffffff 1 Id: b7d0.dc20 Suspend: 1 Teb: 00000063`08c78000 Unfrozen Start: ntdll!TppWorkerThread (00007ff9`02ca5080) Priority: 0 Priority class: 32 Affinity: ffffffff 2 Id: b7d0.9a54 Suspend: 1 Teb: 00000063`08c7c000 Unfrozen Start: ntdll!TppWorkerThread (00007ff9`02ca5080) Priority: 0 Priority class: 32 Affinity: ffffffff
~#
显示最初导致异常的线程(或在调试器附加到进程时处于活动状态)。0:000> ~# . 0 Id: b7d0.3784 Suspend: 1 Teb: 00000063`08c76000 Unfrozen Start: dadadb+0x1125 (00007ff6`6cb51125) Priority: 0 Priority class: 32 Affinity: ffffffff
~[线程编号]s
:调试的时候切换线程,例如~0s
表示切换到 0 号线程,这里的编号即前面~*
显示在前面的 0,1,2 。
运算
? 0074fbf4 - 74fa68
可以进行简单运算。
0:000> ? 0074fbf4 - 74fa68
Evaluate expression: 396 = 0000018c
查看符号
x ucrtbased!_read
打印 read
函数的地址和其他信息。这个命令支持通配符,比如 x ucrtbased!*read
。
0:000> x ucrtbased!_read
7af371d0 ucrtbased!_read (int, void *, unsigned int)
0:000> x ucrtbased!_system
7ae6b8f0 ucrtbased!system (char *)
0:000> x ucrtbased!*system
7ae6b910 ucrtbased!_wsystem (wchar_t *)
7ae88730 ucrtbased!_o__wsystem (wchar_t *)
7ae6b8f0 ucrtbased!system (char *)
7ae8adb0 ucrtbased!_o_system (char *)
gadget 搜索工具
linux 平台的 gadget 搜索工具实际上是支持 PE 文件的,为了方便起见,我把这些工具安装在 wsl 中。这里推荐安装 wsl1 ,因为 wsl2 会与虚拟机中的一些设置冲突。
ROPGadget
安装方法如下:
git clone https://github/JonathanSalwan/ROPgadget.git
cd ROPgadget
sudo python3 setup.py install
使用方法如下,这样搜索到的 gadget 都写入了 rop 文件中。
ROPgadget --binary ntdll.dll > rop
ropper
安装方法如下:
- 在 pypi 的 ropper 官网上下载 ropper
- 运行安装脚本完成 ropper 安装
python setup.py install
使用方法如下,个人感觉 ropper 搜的全一些。
ropper --file ntdll.dll --nocolor > rop
远程环境
在做 windows pwn 题目时需要搭一个接近远程环境的环境。
系统获取
通常题目会提供一个系统版本截图,例如下图所示系统版本为 1809 17763.615
。
搜索这个版本号发现该系统的相关信息,从中可以获取到该版本系统的发布日期。
在一个收集 Windows 系统下载的网站上搜索时间相近的版本,比如这里我下载的 2019 年 8 月份发布的 Windows Server 2019 。(最好想办法搞一个 迅雷会员,不然下到一半会失败 )
(现在这个网站已经不让下载了,可以用这个网站代替)
搭完后,win+r
,输入 winver
查看版本 1809 17763.678
,非常接近题目给的 1809 17763.615
。
通过对比发现 ntdll.dll
的关键结构偏移都相同。
AppJailLauncher
AppJailLauncher 可以将一个 windows 程序的 IO 映射到一个端口上并且能无限重启。
例如下面的命令可以将 stackoverflow_32.exe
的 IO 映射到 22333 端口上,之后用 nc 命令连对应 IP 的 22333 端口上。
> .\AppJailLauncher.exe /nojail /port:22333 /timeout:2000000 stackoverflow_32.exe
Listening for incoming connections on port 22333...
注意要关闭 Windows 防火墙。
调试环境
为了很好的利用 winpwn 库的便捷性,我直接将调试环境搭建在虚拟机中。
由于该操作系统版本无应用商店,因此我们下载 Windows SDK 来安装 WinDbg 。在安装时可以只勾选调试器选项。
调试发现符号偏移基本一致(我用题目提供的相关 dll 可以正确计算出虚拟机中 dll 的基址)。
替换 dll
在一些情况下把题目的 dll 和题目的 exe 程序放在同一目录下可以完成替换,不过由于 dll 的加载是按照导入表搜索的过程,因此可能会出现 A.dll 导入一个目录下不存在的 B.dll ,而操作系统找到的 B.dll 在系统目录下因此 B.dll 加载了系统目录下的 C.dll 而没有加载 exe 程序所在目录下的 C.dll 。为了解决这一问题,最直接的办法是想办法找一个使用 dll 的版本与题目所给的 dll 版本相差不大的操作系统然后将题目 dll 替换系统目录下的 dll 实现 dll 替换。
首先看 dll 的签名信息,里面会有一个签名时间。这个时间跟操做系统的发布时间比较接近(大概有5个月左右的误差,这个误差是可接受的)。
同样在这个收集 Windows 系统下载的网站上搜索时间相近的版本,这次用到还是 2019 年 8 月发布的 Windows Server 2019 。
另外操作系统的内部版本号与 dll 版本号的倒数第二个数字相同,可以通过搜索内部版本号确定操作系统版本。
系统原 dll 和要替换的 dll 的版本差别如下(版本号中 .
分隔的数字中如果只有最后一个数字不同那么就可以替换):
正常情况下我们没有权限去操作系统目录下的 dll 也就无法完成 dll 替换,但是有如下方法可以完成。
正常右键查看 dll 属性,property → security → Edit → 选择 users
发现发现权限栏是灰色的,无法修改。
打开 property → security → Advanced → Change
,然后填入一个存在的用户名,比如创建系统时注册的用户名就行(一般来说这个用户名同时也是当前登录的用户名,我的用户名是 winpwn),然后点击 ok 。
此时,打开 property → security → Edit → 选择 users
,发现权限栏变成黑色,选择 Full control ,然后 ok 。
此时,就可以改名了(但是无法删除),把dll改成其他名字后,就可以把题目环境同名 dll 复制到 system32 或 sysWOW64 文件夹下了。
重启系统之后 dll 被成功替换,并且程序可以成功运行。
按照题目提供的 dll 的偏移可以打通。
可能存在的问题
0x1a 问题
在Windows的命令行窗口(控制台)中,\x1a
代表结束符(End of Text character),输入包含 \x1a
导致程序 EOF 。注意,接收 \x1a
不会导致程序 EOF 。
具体表现为交互卡在某个地方,而且这个地方可能是输入 \x1a
后的某一步。
要想解决上述问题除了避免输入出现 \x1a
外还可以通过任意地址写修改 ucrtbase.dll
中的 __pioinfo
实现绕过。
__pioinfo
是一个 __crt_lowio_handle_data
类型的结构体指针。其指向的 __crt_lowio_handle_data
结构体在进程默认堆上,并且每次重启 __crt_lowio_handle_data
进程相对于进程默认堆基址的偏移相同。
__crt_lowio_handle_data
结构体的定义如下:
struct __declspec(align(8)) __crt_lowio_handle_data
{
_RTL_CRITICAL_SECTION lock;
__int64 osfhnd;
__int64 startpos;
unsigned __int8 osfile;
__crt_lowio_text_mode textmode;
char _pipe_lookahead[3];
unsigned __int8 unicode : 1;
unsigned __int8 utf8translations : 1;
unsigned __int8 dbcsBufferUsed : 1;
char mbBuffer[5];
};
其中偏移 0x38 的 osfile
决定着程序的输入流模式,当我们把 osfile
改为 0xc1(也可以是 0x09。这里建议是 0x9 ,经调试 0xc1 只能读一个 0x1a)就可以把输入流模式从字符流改为二进制流,从而实现任意字符读入。
回车问题
Windows 中的回车是 \r\n
,因此如果看到一个程序写的是 printf("%p\n",value);
那么实际输出的回车不是 \n
而是 \r\n
,puts
函数在输出完字符串后也会在后面添加 \r\n
。
不过如果需要输入回车那么 \n
和 \r\n
都可以。如果规定只能输入一个字符那么只能是 \n
。
另外如果输入的地址等数据包含 \x0a
那么远程程序接收数据的时候会被 \x0a
截断导致数据接收不完整。
基础知识
windows 函数调用约定
下面是一些常见的调用约定,实际情况不同类型的编译器具体传参规则会有所不同,需要具体分析。
x86
__cdecl | __stdcall | __fastcall | __thiscall | |
---|---|---|---|---|
参数传递顺序 | 从右到左 | 从右到左 | 使用寄存器和栈 | 使用寄存器和栈 |
平衡栈者 | 调用者 | 函数 | 函数 | 函数 |
- VARARG 表示参数的个数可以是不确定的,如果使用 VARARG 参数类型,就是调用程序平衡栈,否则按照默认方式平衡栈。
__fastcall
传参规则为前两个参数通过 ecx 和 edx 传递,之后的参数通过栈传递。__thiscall
传参规则为 ecx 传递 this 指针,其余参数按照从右到左顺序入栈。
x64
__thiscall
传参规则为 rcx 传递 this 指针,前三个参数通过 rdx、r8、r9 传递,剩余参数布置在栈中。- 其他类型的函数调用传参规则为前四个参数通过 rcx、rdx、r8、r9,剩余参数布置在栈中。
- 栈平衡由调用者完成。
这里需要着重强调一下 windows 64 位函数调用的堆栈。
在函数调用前前 4 个参数放在寄存器中,第 5 个参数开始依次从 [rsp + 0x20]
位置处开始存放。进入调用的函数后会将寄存器中的参数存放到返回地址后空缺的位置上。
PE 文件格式
这里以 32 位 PE 文件为例,64 位除数据长度外变化不大。
其中 PE 头结构如下:
IMAGE_DOS_HEADER
WORD e_magic
:“MZ”标记,用于判断是否为可执行文件DWORD e_lfanew
:PE 头相对于文件的偏移,用于定位 PE 文件
IMAGE_NT_HEADERS32
DWORD Signature
:“PE”标记,标记IMAGE_NT_HEADERS
起始位置
IMAGE_FILE_HEADER
WORD Machine
:程序运行的CPU型号,0x0 为任何处理器;0x14C 为 i386 及后续处理器WORD NumberOfSections
:文件中存在的节的总数,如果要新增节或者合并节 就要修改这个值DWORD TimeDateStamp
:时间戳,文件的创建时间(和操作系统的创建时间无关),编译器填写的DWORD PointerToSymbolTable
DWORD NumberOfSymbols
WORD SizeOfOptionalHeader
:可选 PE 头的大小,32 位 PE 文件默认 E0h,64 位 PE 文件默认为 F0h ,大小可以自定义WORD Characteristics
:每个位有不同的含义,可执行文件值为 10F 即 0 1 2 3 8 位置 1
IMAGE_OPTIONAL_HEADER32
-
WORD Magic
:说明文件类型,10B 为 32 位下的 PE 文件;20B 为 64 位下的 PE 文件 -
BYTE MajorLinkerVersion
-
BYTE MinorLinkerVersion
-
DWORD SizeOfCode
:所有代码节的和,必须是FileAlignment
的整数倍,编译器填的,没用 -
DWORD SizeOfInitializedData
:已初始化数据大小的和,必须是FileAlignment
的整数倍,编译器填的,没用 -
DWORD SizeOfUninitializedData
:未初始化数据大小的和,必须是FileAlignment
的整数倍,编译器填的,没用 -
DWORD AddressOfEntryPoint
:程序入口 -
DWORD BaseOfCode
:代码开始的基址,编译器填的,没用 -
DWORD BaseOfData
:数据开始的基址,编译器填的,没用 -
DWORD ImageBase
:内存镜像基址 -
DWORD SectionAlignment
:内存对齐 -
DWORD FileAlignment
:文件对齐 -
WORD MajorOperatingSystemVersion
-
WORD MinorOperatingSystemVersion
-
WORD MajorImageVersion
-
WORD MinorImageVersion
-
WORD MajorSubsystemVersion
-
WORD MinorSubsystemVersion
-
DWORD Win32VersionValue
-
DWORD SizeOfImage
:内存中整个 PE 文件的映射的尺寸,可以比实际的值大,但必须是SectionAlignment
的整数倍 -
DWORD SizeOfHeaders
:所有头 + 节表按照文件对齐后的大小,否则加载会出错 -
DWORD CheckSum
:校验和,一些系统文件有要求,用来判断文件是否被修改 -
WORD Subsystem
-
WORD DllCharacteristics
-
DWORD SizeOfStackReserve
:初始化时保留的堆栈大小 -
DWORD SizeOfStackCommit
:初始化时实际提交的大小 -
DWORD SizeOfHeapReserve
:初始化时保留的堆大小 -
DWORD SizeOfHeapCommit
:初始化时实践提交的大小 -
DWORD LoaderFlags
-
DWORD NumberOfRvaAndSizes
:目录项数目 -
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]
:
我们所了解的 PE 分为头和节,在每个节中,都包含了我们写的一些代码和数据,但还有一些非常重要的信息是编译器替我们加到 PE 文件中的,这些信息可能存在在任何可以利用的地方,而数据目录表存储了这些信息的位置和大小。
DataDirectory
是一个长度为 16 的IMAGE_DATA_DIRECTORY
类型数组,相关定义如下:#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16 typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; //内存偏移 DWORD Size; //大小 } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
这个数组前 15 项的下标宏以及含义如下,第 16 项保留未使用。
IMAGE_DIRECTORY_ENTRY_EXPORT
:导出表IMAGE_DIRECTORY_ENTRY_IMPORT
:导入表IMAGE_DIRECTORY_ENTRY_RESOURCE
:资源表IMAGE_DIRECTORY_ENTRY_EXCEPTION
:异常信息表IMAGE_DIRECTORY_ENTRY_SECURITY
:安全证书表IMAGE_DIRECTORY_ENTRY_BASERELOC
:重定位表IMAGE_DIRECTORY_ENTRY_DEBUG
:调试信息表IMAGE_DIRECTORY_ENTRY_COPYRIGHT
:版权所有表IMAGE_DIRECTORY_ENTRY_GLOBALPTR
:全局指针表IMAGE_DIRECTORY_ENTRY_TLS
:TLS 表IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG
:加载配置表IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT
:绑定导入表IMAGE_DIRECTORY_ENTRY_IAT
:IAT 表IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT
:延迟导入表IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR
:COM 信息表
重定位表
重定位表在程序的加载基址不是 ImageBase
时用来修复代码访问字符串,全局变量等数据时使用的地址。
重定位表是一个由 IMAGE_BASE_RELOCATION + 数据
结构组成的数组。
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
} IMAGE_BASE_RELOCATION ,* PIMAGE_BASE_RELOCATION;
在内存中的结构如下图所示:
每个块用来记录一个内存页中需要重定位的位置。
VirtualAddress
表示这该内存页的地址SizeOfBlock
表示该块的大小,即(SizeOfBlock - 8) / 2
为具体项的数量- 如果某一项的高 4 位为 0b0011 则该项的低 12 位为需要重定位的位置在该内存页中的偏移
- 重定位时从需要重定位的位置取出 4 字节长度的数据,将其减去
ImageBase
然后加上模块加载基址,最后将得到的结果写入重定位的位置。 - 重定位表通过一个模块的起始位置加上
SizeOfBlock
得到下一个模块的起始位置,以一个VirtualAddress
和SizeOfBlock
均为 0 的模块为结束标志。
导出表
导出表是一个 IMAGE_EXPORT_DIRECTORY
结构:
导入表
导入表是一个 IMAGE_IMPORT_DESCRIPTOR
结构组成的数组。
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //RVA 指向IMAGE_THUNK_DATA结构数组
};
DWORD TimeDateStamp; //时间戳
DWORD ForwarderChain; //RVA,指向IMAGE_THUNK_DATA结构数组
DWORD Name; //RVA,指向dll名字,该名字已0结尾
DWORD FirstThunk; //RVA,指向IMAGE_THUNK_DATA结构数组
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
其中 OriginalFirstThunk
和 FirstThunk
分别指向两个由 IMAGE_THUNK_DATA
结构组成的数组 INT 和 IAT 。
typedef struct _IMAGE_THUNK_DATA32 {
union {
PBYTE ForwarderString;
PDWORD Function;
DWORD Ordinal; //序号
PIMAGE_IMPORT_BY_NAME AddressOfData; //指向IMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
IMAGE_THUNK_DATA
可以存储多种类型的数据:
Function
:函数地址Ordinal
:函数在其所在 dll 的导出序号AddressOfData
:IMAGE_IMPORT_BY_NAME
类型的结构,该结构主要用于存储函数名
IMAGE_IMPORT_BY_NAME
定义如下,其中 Name
是一个以 \x00
结尾的字符串,长度不确定。
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; //可能为空,编译器决定 如果不为空 是函数在导出表中的索引
BYTE Name[1]; //函数名称,以0结尾
} IMAGE_IMPORT_BY_NAME;
在 PE 文件还未加载到内存时,整个导入表及其相关结构状态如下:
其中 INT 表和 IAT 表中的内容相同。根据最高位为 1 还是 0 决定 IMAGE_THUNK_DATA
中的内容是 Ordinal
还是 AddressOfData
。
当 PE 文件加载到内存中时,IAT 表会被修为函数地址。
节表
节表是由 IMAGE_SECTION_HEADER
构成的数组,数组中元素数量为 IMAGE_FILE_HEADER
中的 NumberOfSections
。
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
Name
:8 个字节 一般情况下是以"\0"结尾的 ASCII 码字符串来标识的名称,内容可以自定义。
注意:该名称并不遵守必须以"\0"结尾的规律,如果不是以"\0"结尾,系统会截取 8 个字节的长度进行处理。Misc.VirtualSize
:该节在内存中没有对齐前的真实尺寸,该值可以不准确。VirtualAddress
:节区在内存中的偏移地址。加上ImageBase
才是在内存中的真正地址。SizeOfRawData
:节在文件中对齐后的尺寸。PointerToRawData
:节区在文件中的偏移。PointerToRelocations
:在 obj 文件中使用 对 exe 无意义。PointerToLinenumbers
:行号表的位置,调试的时候使用。NumberOfRelocations
:在 obj 文件中使用,对 exe 无意义。NumberOfLinenumbers
:行号表中行号的数量,调试的时候使用。Characteristics
:节的属性。
常见 dll
ntdll.dll
- 包含未公开 API
- 系统调用入口
- 各版本间不同
kernel32.dll
- 堆,虚拟内存,文件 I/O 相关的 API
- 多数函数只是 ntdll 函数的封装
- API 几乎不会修改
mscrtxxx.dll
/ucrtbase.dll
- 类似 linux 中的 glibc
dll 之间的函数调用关系如下图所示:
mscrtxxx.dll
和 ucrtbase.dll
的区别:
-
mscrtxxx.dll
:Microsoft Visual C++ 运行时库,包含了用于支持早期的 Visual C++ 版本的函数和变量。 -
ucrtbase.dll
:Universal C 运行时库,是 Windows 10 中默认的 C 运行时库。它包含了许多标准 C 库函数的实现,以及一些新的安全函数,可以提高代码的安全性和可靠性。
常见结构
PEB
PEB(Process Environment Block)是 Windows 操作系统中的一个数据结构,它包含了进程的上下文信息。每个进程都有一个唯一的 PEB,它被存储在进程的用户模式地址空间中。
PEB 与 TEB 的相对偏移固定,使用 .process
或者 r $peb
查看进程的 PEB 地址,随后使用 dt _PEB peb_addr
查看进程的 PEB 信息。
0:000> .process
Implicit process is now 00995000
0:000> r $peb
$peb=00995000
0:000> dt _PEB 00995000
ntdll!_PEB
+0x000 InheritedAddressSpace : 0 ''
+0x001 ReadImageFileExecOptions : 0 ''
+0x002 BeingDebugged : 0x1 ''
+0x003 BitField : 0x4 ''
...
!peb
查看 PEB 的具体内容,其中 Ldr 的地址为76facb00,即 ntdll!pebldr
地址。
0:000> !peb
PEB at 00995000
InheritedAddressSpace: No
ReadImageFileExecOptions: No
BeingDebugged: Yes
ImageBaseAddress: 00700000
NtGlobalFlag: 0
NtGlobalFlag2: 0
Ldr 76facb00
...
0:000> dc ntdll!pebldr
76facb00 00000030 00000001 00000000 00e12360 0...........`#..
76facb10 00e18418 00e12368 00e18420 00e12278 ....h#.. ...x"..
76facb20 00e182d0 00000000 00000000 00000000 ................
76facb30 00000002 00000000 00000000 00000000 ................
76facb40 00000000 00000000 00000000 00000000 ................
76facb50 00000000 00000000 00000000 00000000 ................
76facb60 00000000 00000000 00000000 00000000 ................
76facb70 00000000 00000000 00000000 00000000 ................
PEB 结构在 Windows Pwn 中的作用主要是泄露 TEB 地址,程序基址,以及通过修改其中的 ProcessHeap
完成对进程默认堆的切换。
TEB
TEB(Thread Environment Block)是 Windows 操作系统中的一个线程私有的数据结构,用于存储线程相关的信息。每个线程都有一个对应的 TEB 。32 位程序 FS 寄存器指向当前线程的 TEB ,64 位程序 GS 寄存器指向当前线程的 TEB 。
使用 r $teb
查看进程的 TEB 地址,!teb
可以查看 TEB 详细信息。
0:000> r $teb
$teb=00998000
0:000> !teb
TEB at 00998000
ExceptionList: 00affcf0
StackBase: 00b00000
StackLimit: 00afd000
SubSystemTib: 00000000
FiberData: 00001e00
ArbitraryUserPointer: 00000000
Self: 00998000
EnvironmentPointer: 00000000
ClientId: 00003638 . 00000ae0
RpcHandle: 00000000
Tls Storage: 00e1acb8
PEB Address: 00995000
LastErrorValue: 0
LastStatusValue: 0
Count Owned Locks: 0
HardErrorMode: 0
TEB 的开头是一个 NT_TIB
结构,具体如下:
0:000> dt _nt_tib
ntdll!_NT_TIB
+0x000 ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x004 StackBase : Ptr32 Void
+0x008 StackLimit : Ptr32 Void
+0x00c SubSystemTib : Ptr32 Void
+0x010 FiberData : Ptr32 Void
+0x010 Version : Uint4B
+0x014 ArbitraryUserPointer : Ptr32 Void
+0x018 Self : Ptr32 _NT_TIB
TEB 结构在 Windows Pwn 中的作用是泄露栈地址。
NT_TIB
中一些重要的字段的解释:
ExceptionList
:指向当前线程的异常处理器链表的头部。当线程发生异常时,系统会将异常处理器添加到该链表中,以便进行异常处理。StackBase
和StackLimit
:分别指向线程栈的起始地址和结束地址。这是我们我们泄露栈基址的一个途径。Self
:指向当前 TEB 的指针。对于任何 TEB,该字段的值应该等于 TEB 的地址。
SEH
SEH(Structured Exception Handling,结构化异常处理)是 Windows 操作系统中的一种异常处理机制。
异常处理需要注册异常,即在异常处理链表中添加 _EXCEPTION_REGISTRATION_RECORD
节点,代码如下:
push offset SEHandler
push fs:[0]
mov fs:[0], esp
如果程序当前的函数执行完毕需要卸载当前函数中注册的 SEH 处理程序,代码如下:
mov esp, dword ptr fs:[0]
pop dword ptr fs:[0]
_EXCEPTION_REGISTRATION_RECORD
中的 Next
指向上一个 _EXCEPTION_REGISTRATION_RECORD
结构,Handler
指向异常处理的代码。
MSC 在 32 位模式对异常处理链表的节点 _EXCEPTION_REGISTRATION_RECORD
被扩充为 CPPEH_RECORD
(具体与编译器版本有关),其成员 _EH3_EXCEPTION_REGISTRATION
结构是对原始的 SEH 结构 _EXCEPTION_REGISTRATION_RECORD
的扩充。
typedef struct _EH4_SCOPETABLE_RECORD {
int EnclosingLevel;
void *FilterFunc;
void *HandlerFunc;
} *PSCOPETABLE_ENTRY;
struct _EH4_SCOPETABLE {
DWORD GSCookieOffset;
DWORD GSCookieXOROffset;
DWORD EHCookieOffset;
DWORD EHCookieXOROffset;
struct _EH4_SCOPETABLE_RECORD ScopeRecord[];
};
struct _EH3_EXCEPTION_REGISTRATION {
struct _EH3_EXCEPTION_REGISTRATION *Next;
PVOID ExceptionHandler;
PSCOPETABLE_ENTRY ScopeTable;
DWORD TryLevel;
};
struct CPPEH_RECORD {
DWORD old_esp;
EXCEPTION_POINTERS *exc_ptr;
struct _EH3_EXCEPTION_REGISTRATION registration;
};
MSC编译器引入了_try
、_except
、_finally
关 完成异常处理,使用方法如下:
__try {
/*可能产生异常的代码*/
} __except (/*异常筛选代码*/ FilterFunction(GetExceptionCode(), GetExceptionInformation())) {
/*异常处理代码*/
ExceptionHandler();
}
__try {
/*可能产生异常的代码*/
} __finally {
/*终结处理代码*/
FinallyHandler();
}
FilterFunction
由用户定义用来筛选异常,返回值有如下三种:
// Defined values for the exception filter expression
#define EXCEPTION_EXECUTE_HANDLER 1
#define EXCEPTION_CONTINUE_SEARCH 0
#define EXCEPTION_CONTINUE_EXECUTION (-1)
EXCEPTION_EXECUTE_HANDLER
:表示该异常在预料之中,直接执行下面的ExceptionHandler
。EXCEPTION_CONTINUE_SEARCH
:表示不处理该异常,请继续寻找其他处理程序。EXCEPTION_CONTINUE_EXECUTION
:表示该异常已被修复,请回到异常现场再次执行。
ExceptionHandler
处理完异常后,需要返回如下返回值:
// Exception disposition return values
typedef enum _EXCEPTION_DISPOSITION
{
ExceptionContinueExecution,
ExceptionContinueSearch,
ExceptionNestedException,
ExceptionCollidedUnwind
} EXCEPTION_DISPOSITION;
ExceptionContinueExecution
:表示异常已经被处理,程序可以继续执行。此时,程序会从发生异常的地址处继续执行,而不会跳转到异常处理程序中。ExceptionContinueSearch
:表示异常未被处理,程序应该继续搜索异常处理程序。当多个异常处理程序都可以处理同一个异常时,该枚举值可以用于指示程序继续搜索下一个异常处理程序。ExceptionNestedException
:表示在处理当前异常时,又发生了一个异常。此时,程序会跳转到新的异常处理程序中,处理新的异常。ExceptionCollidedUnwind
:表示发生了一些不可恢复的错误,无法继续执行当前线程。此时,线程的栈会被展开,所有的异常处理程序都会被调用,直到找到一个可以处理当前异常的异常处理程序。如果没有找到这样的异常处理程序,程序将终止。
以下面这段代码为例(SEH.exe,SEH.pdb):
#include <iostream>
#include <windows.h>
int main() {
__try {
__try {
__try {
// 可能会引发异常的代码
*(int *) nullptr = 1;
} __except (EXCEPTION_CONTINUE_SEARCH) {
// 处理异常
puts("Handler 2");
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
// 处理异常
puts("Handler 1");
}
__try {
int x = 0;
x /= x;
} __finally {
// 处理异常
puts("Handler 3");
}
} __except (EXCEPTION_CONTINUE_EXECUTION) {
puts("Handler 4");
}
return 0;
}
在不考虑异常处理函数后汇编代码如下:
.text:00411810 push ebp ; ebp
.text:00411811 mov ebp, esp
.text:00411813 push 0FFFFFFFEh ; 在 __try 块之外,因此 TryLevel = -2
.text:00411815 push offset stru_4191F8 ; PSCOPETABLE_ENTRY ScopeTable
.text:0041181A push offset __except_handler4 ; PVOID ExceptionHandler
.text:0041181F mov eax, large fs:0
.text:00411825 push eax ; struct _EH3_EXCEPTION_REGISTRATION *Next
.text:00411826 add esp, -0D4h ; 提升栈顶
.text:0041182C push ebx
.text:0041182D push esi
.text:0041182E push edi
.text:0041182F
.text:0041182F __$EncStackInitStart_1:
.text:0041182F lea edi, [ebp+var_24]
.text:00411832 mov ecx, 3
.text:00411837 mov eax, 0CCCCCCCCh
.text:0041183C rep stosd
.text:0041183E
.text:0041183E __$EncStackInitEnd_1:
.text:0041183E mov eax, ___security_cookie
.text:00411843 xor [ebp+CPPEH_RECORD.registration.ScopeTable], eax ; ScopeTable ^= ___security_cookie
.text:00411846 xor eax, ebp
.text:00411848 push eax ; canary = ebp ^ ___security_cookie
.text:00411849 lea eax, [ebp+CPPEH_RECORD.registration]
.text:0041184C mov large fs:0, eax ; fs:0 -> struct _EH3_EXCEPTION_REGISTRATION registration;
.text:00411852 mov [ebp+CPPEH_RECORD.old_esp], esp ; DWORD old_esp
.text:00411855 mov ecx, offset _51A925CF_ConsoleApplication1@cpp ; JMC_flag
.text:0041185A call j_@__CheckForDebuggerJustMyCode@4 ; __CheckForDebuggerJustMyCode(x)
.text:0041185F mov [ebp+CPPEH_RECORD.registration.TryLevel], 0 ; 进入 __try0
.text:00411866 mov [ebp+CPPEH_RECORD.registration.TryLevel], 1 ; 进入 __try1
.text:0041186D mov [ebp+CPPEH_RECORD.registration.TryLevel], 2 ; 进入 __try2
.text:00411874 mov large dword ptr ds:0, 1 ; 异常代码
.text:0041187E mov [ebp+CPPEH_RECORD.registration.TryLevel], 1 ; 退出 __try2
.text:00411885 jmp short loc_4118AB
...
.text:004118AB loc_4118AB:
.text:004118AB mov [ebp+CPPEH_RECORD.registration.TryLevel], 0 ; 退出 __try1
.text:004118B2 jmp short loc_4118DB ; 进入 __try3
...
.text:004118DB loc_4118DB:
.text:004118DB mov [ebp+CPPEH_RECORD.registration.TryLevel], 3 ; 进入 __try3
.text:004118E2 mov [ebp+x], 0
.text:004118E9 mov eax, [ebp+x]
.text:004118EC cdq
.text:004118ED idiv [ebp+x] ; 异常代码
.text:004118F0 mov [ebp+x], eax
.text:004118F3 mov [ebp+CPPEH_RECORD.registration.TryLevel], 0 ; 退出 __try3
.text:004118FA call $LN20 ; 调用 __finally
.text:004118FA
.text:004118FF ; ---------------------------------------------------------------------------
.text:004118FF
.text:004118FF loc_4118FF:
.text:004118FF jmp short $LN23
.text:004118FF
.text:00411901 ; ---------------------------------------------------------------------------
.text:00411901
.text:00411901 $LN20:
.text:00411901 ; __finally // owned by 4118DB
.text:00411901 mov esi, esp
.text:00411903 push offset aHandler3 ; "Handler 3"
.text:00411908 call ds:__imp__puts
.text:00411908
.text:0041190E add esp, 4
.text:00411911 cmp esi, esp
.text:00411913 call j___RTC_CheckEsp
.text:00411913
.text:00411918
.text:00411918 $LN21:
.text:00411918 retn
.text:00411918 ; } // starts at 4118F3
.text:00411918
.text:00411919 ; ---------------------------------------------------------------------------
.text:00411919
.text:00411919 $LN23:
.text:00411919 mov [ebp+CPPEH_RECORD.registration.TryLevel], 0FFFFFFFEh ; 退出 __try0
.text:00411920 jmp short loc_411947
...
.text:00411947 loc_411947:
.text:00411947 xor eax, eax
.text:00411949 mov ecx, [ebp+CPPEH_RECORD.registration.Next]
.text:0041194C mov large fs:0, ecx ; 卸载 SEH
.text:00411953 pop ecx
.text:00411954 pop edi
.text:00411955 pop esi
.text:00411956 pop ebx
.text:00411957 add esp, 0E4h
.text:0041195D cmp ebp, esp
.text:0041195F call j___RTC_CheckEsp
.text:0041195F
.text:00411964 mov esp, ebp
.text:00411966 pop ebp
.text:00411967 retn
通过调试发现相关结构在内存中状态如下:
在 MSC 扩展的 SEH 中,处理函数使用 _except_handler4
作为代理函数来调用用户定义的处理函数。用户定义的 FilterFunc
和 HandlerFunc
保存在 SCOPETABLE
中(实际调试的 SCOPETABLE
可能是使用了 _EH3_SCOPETABLE_RECORD
因此和前面的 _EH4_SCOPETABLE_RECORD
定义有所不同)。
通过分析汇编可知,MSC 对用户定义的 __try
块进行了编号,每个 __try
的编号为其在 SCOPETABLE
中对应的 SCOPETABLE_RECORD
的下标,对于不在 __try
块的情况编号为 -2(0xFFFFFE)。当代码执行到某个 __try
块中时,会先将栈中的 CPPEH_RECORD
的 TryLevel
更新为当前所在 __try
块的编号。另外, SCOPETABLE
中的 SCOPETABLE_RECORD
的 EnclosingLevel
记录了 __try
块外层包裹的 __try
块的编号,这样 _except_handler4
进行异常处理的时候就可以按正确的顺序调用处理函数。
注意,_except_handler4 中有一个栈的回滚操作,因此当程序执行到注册在 ScopeTable 中的函数时所在的栈帧是注册该函数所在的栈帧。
v6[0] = (int)ExceptionRecord;
v6[1] = (int)ContextRecord;
v16->Handler = (_EXCEPTION_DISPOSITION (__stdcall *)(_EXCEPTION_RECORD *, void *, _CONTEXT *, void *))v6;
for ( i = v16[2].Handler;
i != (_EXCEPTION_DISPOSITION (__stdcall *)(_EXCEPTION_RECORD *, void *, _CONTEXT *, void *))-2;
i = EnclosingLevel ) // 遍历 ScopeTable
{
v12 = &ScopeTable->ScopeRecord[(_DWORD)i];
FilterFunc = (int (*)(void))ScopeTable->ScopeRecord[(_DWORD)i].FilterFunc;
EnclosingLevel = (_EXCEPTION_DISPOSITION (__stdcall *)(_EXCEPTION_RECORD *, void *, _CONTEXT *, void *))v12->EnclosingLevel;
if ( FilterFunc )
{
v10 = _EH4_CallFilterFunc(FilterFunc); // 调用 Filter 函数
v17 = 1;
if ( v10 < 0 )
{
v7 = 0;
break;
}
if ( v10 > 0 ) // Filter 函数返回值为 EXCEPTION_EXECUTE_HANDLER ,因此调用 Handler 函数
{
if ( ExceptionRecord->ExceptionCode == 0xE06D7363
&& _pDestructExceptionObject
&& _IsNonwritableInCurrentImage(&_pDestructExceptionObject) )
{
Target = (unsigned int)_pDestructExceptionObject;
_pDestructExceptionObject(ExceptionRecord, 1);
}
_EH4_GlobalUnwind2(&v16[1], ExceptionRecord);
if ( v16[2].Handler != i )
_EH4_LocalUnwind((int)&v16[1], (int)i, (int)FramePointer, (int)CookiePointer); //栈回滚
v16[2].Handler = EnclosingLevel;
ValidateLocalCookies(CookieCheckFunction, ScopeTable, FramePointer);
_EH4_TransferToHandler(v12->HandlerFunc, FramePointer); // 调用 Handler 函数
}
}
}
之后在异常处理函数中还会用 old_esp
替换 esp 进一步完成栈回滚。(这里非常重要,如果有恢复 esp
为 old_esp
的操作则说明栈帧恢复到注册异常时的栈,异常处理函数准备直接跳转到发生异常的函数的结尾卸载 SEH 然后直接返回,此时 handler 的返回值即为异常函数的返回值,这种情况也对应着 __expect(...){...}
中没有调用用户定义的异常处理函数而是直接把代码写在 {...}
中而没有返回值的情况。否则说明 handler 在其所在的栈帧中分析处理异常,返回值为异常处理的结果。)
.text:004018BD ; __except(loc_4018B7) // owned by 401869
.text:004018BD mov esp, [ebp+ms_exc.old_esp]
触发异常后,输入 !exchain
可以查看 seh chain(有一种错误说法是 TryLevel
设为 0 后就可以用 !exchain
查看,实际上必须是触发异常后查看的 chain 才是 seh chain)
EXCEPTION_REGISTRATION
依次连接,最后一个 EXCEPTION_REGISTRATION
的 next
为 0xFFFFFFFF ,exceptionhandler
为 ntdll!FinalExceptionHandler
。
0:000> !exchain
0056fcf4: ConsoleApplication1!_except_handler4+0 (00591c70)
CRT scope 2, filter: ConsoleApplication1!main+77 (00591887)
func: ConsoleApplication1!main+7a (0059188a)
CRT scope 1, filter: ConsoleApplication1!main+a4 (005918b4)
func: ConsoleApplication1!main+aa (005918ba)
CRT scope 0, filter: ConsoleApplication1!main+112 (00591922)
func: ConsoleApplication1!main+116 (00591926)
0056fd70: ConsoleApplication1!_except_handler4+0 (00591c70)
CRT scope 0, filter: ConsoleApplication1!__scrt_common_main_seh+1a3 (00591f93)
func: ConsoleApplication1!__scrt_common_main_seh+1be (00591fae)
0056fde8: ntdll!_except_handler4+0 (76efe8b0)
CRT scope 0, filter: ntdll!__RtlUserThreadStart+40 (76eeb760)
func: ntdll!__RtlUserThreadStart+d3 (76eeb7f3)
0056fe00: ntdll!FinalExceptionHandlerPad53+0 (76f18685)
Invalid exception stack at ffffffff
常见保护
DEP
- 类似 Linux 上的 NX 保护,可以理解为内存的可写和可执行不共存。
- 绕过方法
- ROP 调用 VirtualProtect (类似于 Linux 的 mprotect)
ASLR
- 模块加载基址随机而不是按照 ImageBase 加载,每次重启靶机才会改变,而不是每次运行程序时改变。
- TEB/PEB/heap/stack 的基址每次运行程序都会改变
- 一些内核相关的 dll 例如 ntdll.dll 和 kernel32.dll 在所有进程中基址相同
- 绕过方法
- 泄露地址
- 一些 dll 的加载基址在所有进程都相同,因此可以在另一个进程中泄露基址。
- 模块加载基址每次重启才会改变,因此只要靶机不重启不必每次运行程序时泄露基址。
- 爆破
- 由于在 32 位程序中地址只随机 8 字节,因此爆破有 1/256 的几率成功。
- 泄露地址
GS
windows 版的 canary
在开启 GS 保护的程序的头尾部会有如下代码:
.text:4B36C225 mov edi, edi
.text:4B36C227 push ebp
.text:4B36C228 mov ebp, esp
.text:4B36C22A sub esp, 58h
.text:4B36C22D mov eax, ds:___security_cookie
.text:4B36C232 xor eax, ebp
.text:4B36C234 mov [ebp-4], eax
...
.text:4B36C27E mov ecx, [ebp-4]
...
.text:4B36C282 xor ecx, ebp ; StackCookie
...
.text:4B36C285 call @__security_check_cookie@4 ; __security_check_cookie(x)
.text:4B36C28A leave
.text:4B36C28B retn
其中 __security_check_cookie
函数内容如下,其主要作用是比较 StackCookie
与 ___security_cookie
是否相等。
.text:4B2F83C0 ; void __fastcall __security_check_cookie(uintptr_t StackCookie)
.text:4B2F83C0 @__security_check_cookie@4 proc near
.text:4B2F83C0 cmp ecx, ds:___security_cookie
.text:4B2F83C6 jnz short loc_4B2F83CB
.text:4B2F83C8 retn 0
.text:4B2F83CB ; ---------------------------------------------------------------------------
.text:4B2F83CB loc_4B2F83CB: ; CODE XREF: __security_check_cookie(x)+6↑j
.text:4B2F83CB jmp ___report_gsfailure
.text:4B2F83CB @__security_check_cookie@4 endp
___security_cookie
位于程序模块中的 .data
段中,可读写。在程序入口调用 _security_init_cookie
函数完成该值的初始化。
void __cdecl _security_init_cookie()
{
uintptr_t cookie; // rax
unsigned __int64 v1; // [rsp+30h] [rbp+10h] BYREF
struct _FILETIME SystemTimeAsFileTime; // [rsp+38h] [rbp+18h] BYREF
LARGE_INTEGER PerformanceCount; // [rsp+40h] [rbp+20h] BYREF
cookie = _security_cookie;
if ( _security_cookie == 0x2B992DDFA232i64 )
{
SystemTimeAsFileTime = 0i64;
GetSystemTimeAsFileTime(&SystemTimeAsFileTime);
v1 = (unsigned __int64)SystemTimeAsFileTime;
v1 ^= GetCurrentThreadId();
v1 ^= GetCurrentProcessId();
QueryPerformanceCounter(&PerformanceCount);
cookie = ((unsigned __int64)&v1 ^ v1 ^ PerformanceCount.QuadPart ^ ((unsigned __int64)PerformanceCount.LowPart << 32)) & 0xFFFFFFFFFFFFi64;
if ( cookie == 0x2B992DDFA232i64 )
cookie = 0x2B992DDFA233i64;
_security_cookie = cookie;
}
qword_140005000 = ~cookie;
}
绕过方法:
- 泄露
- SEH
CheckStackVars
这个保护是在函数返回前调用 _RTC_CheckStackVars
函数检查栈中的局部变量的前后 4 字节是否被修改,通常在 Debug 版程序中会出现。
以 x64 版本程序为例,通常在函数开头的汇编代码如下:
text:0000000140011900 push rbp
.text:0000000140011902 push rdi
.text:0000000140011903 sub rsp, 208h
.text:000000014001190A lea rbp, [rsp+20h]
.text:000000014001190F mov rdi, rsp
.text:0000000140011912 mov ecx, 82h
.text:0000000140011917 mov eax, 0CCCCCCCCh
.text:000000014001191C rep stosd
此时的栈结构如下,其中有一个局部变量 buffer[0x100]
。
在函数结束时的汇编代码如下:
.text:0000000140011982 lea rcx, [rbp-20h] ; Esp
.text:0000000140011986 lea rdx, Fd ; Fd
.text:000000014001198D call j__RTC_CheckStackVars
.text:000000014001198D
.text:0000000140011992 mov eax, edi
.text:0000000140011994 lea rsp, [rbp+1E8h]
.text:000000014001199B pop rdi
.text:000000014001199C pop rbp
.text:000000014001199D retn
可以看到函数在结束时调用了 CheckStackVars
,函数原型如下:
void __fastcall RTC_CheckStackVars(void *Esp, _RTC_framedesc *Fd)
其中 Esp
等于上图中的 ESP 寄存器的值, Fd
为一个保存在 .rdata
段的一个 _RTC_framedesc
结构体,该结构体的相关定义如下:
struct _RTC_vardesc
{
int addr;
int size;
char *name;
};
struct _RTC_framedesc
{
int varCount;
_RTC_vardesc *variables;
};
在程序中 _RTC_framedesc
相关结构状态如下:
varCount
表示_RTC_vardesc
结构数量,也是该函数中需要检查的局部变量个数。variables
是一个_RTC_vardesc
结构体指针,指向一个元素个数为varCount
的结构体数组。_RTC_vardesc
结构体描述了该函数中的一个需要检查的局部变量的相关信息。addr
:变量起始地址相对于 RSP 的偏移。size
:变量大小。name
:指向变量名称的字符串,用于打印错误信息。
CheckStackVars
函数定义如下,这个函数遍历 _RTC_vardesc
描述的所有局部变量,检查变量的前后 4 字节是否被修改(即是否不是 0xCCCCCCCC)。
void __fastcall RTC_CheckStackVars(void *Esp, _RTC_framedesc *Fd)
{
int count; // ebx
__int64 index; // rdi
_RTC_vardesc *variables; // rdx
__int64 addr; // rcx
void *retaddr; // [rsp+28h] [rbp+0h]
count = 0;
if ( Fd->varCount > 0 )
{
index = 0i64;
do
{
variables = Fd->variables;
addr = variables[index].addr;
if ( *(_DWORD *)((char *)Esp + addr - 4) != 0xCCCCCCCC// 检查变量前面 4 字节是否为 0xCCCCCCCC
|| *(_DWORD *)((char *)Esp + addr + variables[index].size) != 0xCCCCCCCC )// 检查变量后面 4 字节是否为 0xCCCCCCCC
{
_RTC_StackFailure(retaddr, variables[index].name);
}
++count;
++index;
}
while ( count < Fd->varCount );
}
}
在进行栈溢出相关利用时注意在检查的位置填充 \xcc
即可绕过。
SEHOP
在 ntdll!RtlDispatchException
中有对 SEH 链表的检查(ntdll.dll,ntdll.dll.idb):
RtlpGetStackLimits(&StackLimit, &StackBase);
ExceptionList = NtCurrentTeb()->NtTib.ExceptionList;
ProcessInformation = 0;
if ( ZwQueryInformationProcess((HANDLE)0xFFFFFFFF, ProcessExecuteFlags, &ProcessInformation, 4u, 0) < 0 )
ProcessInformation = 0;
if ( (ProcessInformation & 0x40) != 0 || RtlpIsValidExceptionChain(ExceptionList, StackLimit, StackBase) )// SEHOP
{
LABEL_11:
RegistrationPointerForCheck = ExceptionList;
NestedRegistration = 0;
while ( RegistrationPointerForCheck != (_EXCEPTION_REGISTRATION_RECORD *)-1 )// -1 表示 SEH 链结束
{
if ( (unsigned int)RegistrationPointerForCheck < StackLimit
|| (unsigned int)&RegistrationPointerForCheck[1] > StackBase// SEH 节点不在栈中
|| ((unsigned __int8)RegistrationPointerForCheck & 3) != 0// SEH 节点的位置没有 4 字节对齐
|| (Handler = RegistrationPointerForCheck->Handler, (unsigned int)Handler < StackBase)
&& StackLimit <= (unsigned int)Handler
|| !RtlIsValidHandler(Handler, ProcessInformation, pContext) )// safeSEH
{
pExcptRec->ExceptionFlags |= EXCEPTION_STACK_INVALID;// EXCEPTION_STACK_INVALID
goto DispatchExit;
}
...
其中 RtlpIsValidExceptionChain
内容如下:
char __fastcall RtlpIsValidExceptionChain(
_EXCEPTION_REGISTRATION_RECORD *ExceptionList,
unsigned int StackLimit,
unsigned int StackBase,
int StackLimita)
{
unsigned int stackBase; // ebx
int stackLimit; // eax
_EXCEPTION_DISPOSITION (__stdcall *Handler)(_EXCEPTION_RECORD *, void *, _CONTEXT *, void *); // edx
stackBase = StackBase;
stackLimit = StackLimit;
while ( ExceptionList != (_EXCEPTION_REGISTRATION_RECORD *)-1 )
{
if ( stackLimit > (unsigned int)ExceptionList )
return 0;
if ( (unsigned int)ExceptionList >= stackBase - 8 )
return 0;
if ( ((unsigned __int8)ExceptionList & 3) != 0 )
return 0;
Handler = ExceptionList->Handler;
if ( (unsigned int)Handler < stackBase && StackLimit <= (unsigned int)Handler )
return 0;
if ( ExceptionList->Next == (_EXCEPTION_REGISTRATION_RECORD *)-1 )
{
stackBase = StackBase;
if ( (NtCurrentTeb()->SameTebFlags & 0x200) != 0 && Handler != RtlpFinalExceptionHandler )
return 0;
}
stackLimit = (int)&ExceptionList[1];
ExceptionList = ExceptionList->Next;
}
return 1;
}
主要检查 SEH 是否满足如下条件:
- SEH 节点在栈中
- SEH节点指向的
Handler
不在栈中 - SEH 节点地址 4 字节对齐
- SEH 最后一个节点的
Next
为 -1 且Handler
为RtlpFinalExceptionHandler
- SEH 节点的
Next
指向的下一个节点的地址一定大于当前节点
只要泄露栈地址就可以伪造 SEH 链表绕过 SEHOP 检查
SafeSEH
在 ntdll!RtlDispatchException
中调用 RtlIsValidHandler
进一步检查 SEH 链表,伪代码如下:
BOOL RtlIsValidHandler(handler) {
if (handler image has a SafeSEH table) {
if (handler found in the table)
return TRUE;
else
return FALSE;
}
if (ExecuteDispatchEnable|ImageDispatchEnable bit set in the process flags)
return TRUE;
if (handler is on a executeable page) {
if (handler is in an image) {
if (image has the IMAGE_DLLCHARACTERISTICS_NO_SEH flag set)
return FALSE;
if (image is a .NET assembly whith the ILonly flag set)
return FALSE;
return TRUE;
}
if (handler is not in an image) {
if (ImageDispatchEnable bit set in the process flags)
return TRUE;
else
return FALSE;
}
}
if (handler is on a non-executable page) {
if (ExecuteDispatchEnable bit set in the process flags)
return TRUE;
else
raise ACCESS_VIOLATION;
}
}
绕过方法:
- 将
Handler
覆盖指向有 SEH 但没有 SafeSEH 保护的 Image 即可绕过。
CFG
即 Control Flow Guard ,为函数指针创建白名单,每次调用前都会检查。
.text:00000001400017BD mov rbx, [rdi+Node.FuncPtr]
.text:00000001400017C4 mov rcx, rbx
.text:00000001400017C7 call cs:__guard_check_icall_fptr
.text:00000001400017CD mov rcx, rdi
.text:00000001400017D0 call rbx
其中函数指针 __guard_check_icall_fptr
位于不可写的 .rdata
段,默认初始化为 ntdll!LdrpValidateUserCallTarget
函数。
void __fastcall LdrpValidateUserCallTarget(unsigned __int64 FuncPtr)
{
__int64 BitMap; // rdx
unsigned __int64 Offset; // rax
BitMap = CFGBitMap[FuncPtr >> 9];
Offset = FuncPtr >> 3;
if ( (FuncPtr & 0xF) != 0 )
{
Offset &= ~1ui64;
if ( !_bittest64(&BitMap, Offset) )
{
LABEL_6:
LdrpHandleInvalidUserCallTarget();
return;
}
LABEL_5:
if ( _bittest64(&BitMap, Offset | 1) )
return;
goto LABEL_6;
}
if ( !_bittest64(&BitMap, Offset) )
goto LABEL_5;
}
绕过方法:
- ROP
- SEH Handler
PROCESS_MITIGATION_CHILD_PROCESS_POLICY
PROCESS_MITIGATION_CHILD_PROCESS_POLICY
是Windows操作系统中的一项安全功能。该功能允许管理员指定如何创建子进程以及它们从其父进程继承哪些安全设置。该功能可用于防止子进程继承某些安全设置,例如创建新进程或访问某些系统资源的能力。
可用于配置 PROCESS_MITIGATION_CHILD_PROCESS_POLICY
的几个选项,包括:
NoChildProcessCreation
:防止创建子进程。ParentProcess
:允许子进程继承与其父进程相同的安全设置。ChildProcessRestricted
:将子进程的安全设置限制为其父进程安全设置的子集。
可使用如下命令查询 PROCESS_MITIGATION_CHILD_PROCESS_POLICY
是否已开启(在管理员权限的 Powershell 中查询):
Get-ProcessMitigation -Name 程序名
看到与 PROCESS_MITIGATION_CHILD_PROCESS_POLICY
相关的输出,则表示该保护功能已启用。如果没有相关的输出,则表示该保护功能未启用。(开启并关闭保护也可以查询)
比赛时我们无法查询远程环境的保护是否开启,不过题目会提供远程的启动脚本,其中可能会有 PROCESS_MITIGATION_CHILD_PROCESS_POLICY
保护的开启命令。
可以使用如下命令开启 ChildProcessRestricted
保护,效果是不能执行 system("cmd.exe")
,只能 ORW 获取 flag 。
Set-ProcessMitigation -Name 程序名 -Enable DisallowChildProcessCreation
常见地址泄露方法
通过导入表泄露
dll 的基址通常通过另一个模块的导入表泄露,具体各种 dll 之间的导入表关系可以参考前面常见 dll 之间的调用关系。
通过堆泄露
如果每次重启程序我们都有一次堆基址泄露和一次任意地址读,那么我们可以通过泄露堆上的数据来泄露相关地址。
-
ntdll 基址
_HEAP
偏移 0x2c0 的地址处存放着一个ntdll.dll
的地址,我们可以通过任意地址读泄露&_HEAP + 0x2c0
处存储的ntdll.dll
地址,从而泄露ntdll.dll
基址。这里要注意泄露的ntdll.dll
地址与ntdll.dll
基址偏移不固定,通常需要采用下面这种方法获取 ntdll 的基址。ntdll.address = (arbitrary_address_read(heap_base + 0x2c0, True) - 0x15f000) & ~0xFFFF
-
程序基址
在
ntdll!LdrpInitializeProcess
函数中有如下代码:__int64 __fastcall LdrpAllocateModuleEntry(__int64 a1) { ... Heap = RtlAllocateHeap(LdrpHeap, (NtdllBaseTag + 0x40000) | 8u, 288i64); ... return Heap; } void __fastcall LdrpInsertDataTableEntry(void *a1) { ... qword_1801653D0 = (__int64)a1; ... } v63 = LdrpAllocateModuleEntry(v137); ... LdrpImageEntry = v63; ... v76 = ProcessEnvironmentBlock->ImageBaseAddress; v70 = LdrpImageEntry; ... *(_QWORD *)(v70 + 48) = v76; LdrpInsertDataTableEntry(v70);
可以看到,程序在在一个堆地址
v70 + 48
的地方写了一个程序基址。而v70
是一个堆地址,存储在 ntdll 的全局变量qword_1801653D0
上。由于LdrpAllocateModuleEntry
的调用是在默认堆创建不久之后调用的,因此这个堆地址相对于默认堆的基址偏移固定且比较靠近默认堆基址(偏移不超过 16 bit,不会受堆地址随机化影响)。因此我们可以先用一次任意地址读泄露
qword_1801653D0
存储的数据,根据qword_1801653D0
的低 16 bit 泄露其与堆基址之间的偏移。之后再次启动程序,就可以在堆上对应位置泄露出程序基址。
通过 PEB 泄露
首先我们需要知道如何泄露 PEB 地址。
在 ntdll.dll
中的 ntdll!LdrpInitializeProcess
函数中,有如下代码:
TEB = NtCurrentTeb();
PEB = (int)TEB->ProcessEnvironmentBlock;
通过查找我们发现 ntdll.dll
中有不少全局变量存放了 PEB 的地址,并且这些变量自从写入 PEB 相关地址后就没有修改过,因此我们可以通过这些变量泄露 PEB 地址。
dword_4B3A0C0C = PEB + 540;
*(_BYTE *)(PEB + 540) |= 1u;
*(_DWORD *)(PEB + 532) = PEB + 528;
*(_DWORD *)(PEB + 528) = PEB + 528;
TlsBitMap = 64;
dword_4B3A0BD4 = PEB + 68;
*(_BYTE *)(PEB + 68) |= 1u;
TlsExpansionBitMap = 1024;
dword_4B3A0BBC = PEB + 340;
*(_BYTE *)(PEB + 340) |= 1u;
PEB 可以泄露的地址:
- 程序基址:偏移 0x10 的
ImageBaseAddress
为程序基址。 - TEB 基址:通常与 PEB 偏移固定,在已知 PEB 基址的情况下可以推算出 TEB 基址。
通过 TEB 泄露
PEB 和 TEB 的相对偏移固定,并且在 WIndows 大版本相同的情况下偏移是一样的,因此在泄露 PEB 地址后 TEB 的地址也可以确定。
注意,一个线程对应一个 TEB 因此要想获取主线程对应的 TEB 地址需要让 WinDbg 段在主程序上然后 r $teb
查看 TEB 地址。
TEB 可以泄露的地址:
- 栈基址:偏移 0x8 的
StackBase
可以泄露栈基址,即栈底地址。 - 栈顶地址:偏移 0x10 的
StackLimit
可以泄露栈顶地址。
windows 异常处理
- 产生硬件异常通过 IDT 调用异常处理例程, 产生软件异常通过 API 的层层调用产地异常信息。而异常又由于发生位置不同,分为内核异常和用户态异常,二者最后都会靠
kiDispathException
函数来进行异常分发; - 当内核产生异常时,程序处理流程进入到
KiDispatchException
函数,在该函数内备份当前线程 R3 的TrapFrame
(即栈帧的基址)。异常处理首先判断这是否是第一次异常,判断是否存在内核调试器,如果有内核调试器,则把当前的异常信息发送给内核调试器;如果没有内核调试器或者内核调试器没有处该异常 , 则进入步骤 3 ,调用RtlDispatchException
。 - 内核异常进入
RtlDispatchException
函 数, 如果RtlDispatchException
函数没有处理该异常,那么将再次尝试将异常发送到内核调试器,如果此时内核调试器仍然不存在或者没有处理该异常,那么此时系统会直接蓝屏; - 如果是用户态异常则经过
KiDispatchException
进行用户态异常分发和处理。如果是第一次分发异常,则调用DbgKForwardException
将异常分发到内核调试器;如果内核调试器不存在或没有处理异常,则尝试将异常分发给用户态调试器;如果异常被处理,则进入步骤 10 ;如果用户态调试器不存在或未处理异常,则检测是否是第一次处理异常,如果是第一次处理异常则进入第 5 步中的异常数据准备; - 准备一个返回
ntdll!KiUserExceptionDispatcher
函数的应用层调用栈,结束本次KiDispatchException
函数的运行,调用KiServiceExit
返回用户层。此时函数栈帧是ntdll!KiUserExceptionDispatcher
的执行环境,用户态线程从执行ntdll!KiUserExceptionDispatcher
开始执行。该函数调用ntdll!RtlDispatchException
进行异常的分发,进入第 6 步; - 通过
RtlCallVectoredExceptionHandlers
遍历 VEH 链表尝试查找异常处理函数;如果 VEH 未处理异常。则从fs[0]
读取ExceptionList
并开始执行 SEH 函数处理,进入步骤 7; - 如果 SEH 没有处理函数处理该异常,则检查用户是否通过
SetUnhandledExceptionFilter
函数注册过进程的异常处理函数,如果用户注册过异常处理函数,调用该异常处理函数,如果异常没有被成功处理或没有自定义的异常处理函数,则进入步骤 3 ; - 如果最后仍没有处理该异常,便会主动调用
NtRaiseException
将该异常重新跑出来,但是此时不是第一次分发,此时NtRaiseException
流程重新调用了ntdll!KiDispatchException
,并再次进入用户态异常的处理分支,进入步骤 9 ; - 第二次进入用户态异常处理时,不会再尝试发送到内核调试器,也不会再进行异常分发,而是直接尝试发送到用户态体异常调试器,如果最后异常仍未被处理则进入步骤 11 ;
- 异常被处理,调用
NtContine
,将之前保存的TrapFrame
还原,程序继续从异常处正常运行; - 异常不能被处理,系统调用
ntdll!KiDispatchException
调用ZwTerminateProcess
结束进程。
windows IO_FILE
Windows 的 FILE
结构体定义在 ucrtbase.dll
中,在使用 IDA 打开 ucrtbase.dll
时会根据调试信息表 IMAGE_DIRECTORY_ENTRY_DEBUG
中的 pdb 信息下载相关符号,由于 ucrtbase.dll
为 Release 版,因此没有 FILE
结构体的具体定义。不过通过对比 Debug 版的 ucrtbased.dll
我们发现 FILE
结构体实际上是 __crt_stdio_stream_data
。
__crt_stdio_stream_data
相关定义如下,该结构体大小为 0x58 。
struct _RTL_CRITICAL_SECTION {
_RTL_CRITICAL_SECTION_DEBUG *DebugInfo;
int LockCount;
int RecursionCount;
void *OwningThread;
void *LockSemaphore;
unsigned __int64 SpinCount;
};
struct __crt_stdio_stream_data {
union {
FILE _public_file;
char* _ptr; // 当前结构指针
}
char *_base; // 输入缓冲区基址
int _cnt; // 没有被读出的缓冲区剩余大小
int _flags;
int _file; // 文件描述符
int _charbuf; // Local buffer
int _bufsiz; // buffer size
char *_tmpfname;
_RTL_CRITICAL_SECTION _lock; // lock
};
如果要实现任意地址读,fwrite
:
- 设置
_file
文件描述符为stdout
输出符 - 设置
_flag
为_IOWRITE | IOBUFFER_USER | _IOUPDATE
- 设置
_cnt=0
- 设置
_base& _ptr
指向读取的地址 - 设置
_bufsize
为输出的大小
如果要实现任意地址写,fread
:
- 设置
_file
文件描述符为stdin
输出符 - 设置
_flag
为_IOALLOCATED | _IOBUFFER_USER
- 设置
_cnt=0
- 设置
_base& _ptr
指向写入的地址 - 设置
_bufsize
为输入的大小
程序在每次执行如下代码时会在进程的默认堆中申请一个 0x60 大小的 chunk 并将其填充为 __crt_stdio_stream_data
结构体然后将该结构体地址写入 Stream
中。
fopen_s(&Stream, "magic.txt", "rb");
如果我们能够劫持 Stream
指针或者 UAF 修改 __crt_stdio_stream_data
结构体就可以在执行下面这段代码时实现任意地址写。
fread_s(buffer, size, 1ui64, size, Stream);
具体伪造方式如下,主要操作是把 _base
指向要写入数据的地址,_file
设为 0 即标准输入。
fake_FILE = ''
fake_FILE += p64(0) # _ptr
fake_FILE += p64(target_addr) # _base
fake_FILE += p32(0) # _cnt
fake_FILE += p32(0x2080) # _flags
fake_FILE += p32(0) # _file = stdin(0)
fake_FILE += p32(0) # _charbuf
fake_FILE += p64(0x200) # _bufsiz
fake_FILE += p64(0) # _tmpfname
fake_FILE += p64(0xffffffffffffffff) # DebugInfo
fake_FILE += p32(0xffffffff) # LockCount
fake_FILE += p32(0) # RecursionCount
fake_FILE += p64(0) # OwningThread
fake_FILE += p64(0) # LockSemaphore
fake_FILE += p64(0) # SpinCount
windows 堆基础
Windows 堆概述
Windows 堆类型
- 在 Win10 和 Win Server2016 版本之前,只有一种堆类型 NT Heap
- 在 Win10 和 Win Server2016 之后,引入了 Segment Heap(段堆)
- 在之后版本中,除了 UWP 程序之外 一般都继续使用 NT Heap 进行堆管
UWP(Universal Windows Platform)是 Win10 引入的一种新的应用程序开发模型,他们采用了一套共享的 API 。所以采用 UWP开发的程序,可以在所有 Win10 设备上运行。
要想区分是一个正常程序还是 UWP 程序有以下方法:
- 打开任务管理器,查看其中打开的程序能否展开,如果可以,且其中一个是 Runtime Broker , 另外一个是应用本身,那么就是 UWP 应用。
- 在开始菜单右键应用,点击更多,查看其中有没有应用设置 。有的话就是 UWP 应用。
- 在开始菜单,找到 UWP 应用并右键,打开应用设置,查看版本信息。
Windows 用户态进程堆空间
每个进程的堆包含两种类型:
- Process Heap(默认),整个进程共享的堆,它包括两个部分:
- default heap ,其地址信息会存放于 _PEB 的 ProcessHeap 中,在调用 malloc 等函数的时候会用到。
- crtheap,但是其本质一样是 default ,封装了一些别的信息,存放于 crt_heap 中。
- Private Heap,通过 HeapCreate 创建的堆。
普通进程堆空间:
- 默认堆
- 用于向进程的会话 Csrss.exe 实例传递大参数的共享堆。这是由 CsrClientConnectToServer 函数创建的,该函数在 ntdll.dll 完成的进程初始化早期执行。
- 由 Microsoft C 运行库创建的堆。该堆是由 C/C++ 内存分配函数(如malloc 、free 等)内部使用的堆。
UWP 应用程序进程除了普通进程堆空间包含的堆外还包含 Segment Heap 段堆。
堆管理常见函数
HeapCreate
WINBASEAPI HANDLE WINAPI HeapCreate (DWORD flOptions, SIZE_T dwInitialSize, SIZE_T dwMaximumSize);
- 作用:创建一个新的堆对象。
- 参数:
flOptions
:堆的选项标志。可以是以下标志的组合:HEAP_GENERATE_EXCEPTIONS
:在内存不足时引发异常。HEAP_NO_SERIALIZE
:多线程访问堆时不进行同步。
dwInitialSize
:堆的初始大小(以字节为单位)。如果为 0 ,则系统会选择一个默认的初始大小。dwMaximumSize
:堆的最大大小(以字节为单位)。如果为 0 ,则堆的大小受系统的限制。
- 返回值:
- 如果操作成功,返回堆对象的句柄;
- 如果操作失败,返回 NULL 。
HeapCreate
函数用于创建一个新的堆对象,该堆对象提供了一种用于内存分配和管理的机制。堆是进程专用的内存区域,用于动态分配和释放内存块。通过使用堆,可以有效地管理不同大小的内存块,并提供多线程访问的同步机制。
使用 HeapCreate
函数创建堆后,可以使用其他堆相关的函数(如 HeapAlloc
、HeapFree
等)来分配和释放内存块。堆对象可以在不再需要时使用 HeapDestroy
函数进行销毁。
HeapAlloc/HeapFree
WINBASEAPI LPVOID WINAPI HeapAlloc (HANDLE hHeap, DWORD dwFlags, SIZE_T dwBytes);
- 作用:在指定的堆中分配指定大小的内存块。
- 参数:
hHeap
:要分配内存的堆的句柄。此句柄通常由HeapCreate函数创建。dwFlags
:内存分配的标志。可以是以下标志的组合:HEAP_ZERO_MEMORY
:分配的内存块被初始化为零。HEAP_GENERATE_EXCEPTIONS
:在分配内存时发生错误时生成异常。HEAP_NO_SERIALIZE
:禁用堆的同步机制,使多线程访问堆时不同步。
dwBytes
:要分配的内存块的大小(以字节为单位)。
- 返回值:
- 如果分配成功,返回指向分配的内存块的指针;
- 如果分配失败,返回 NULL 。
HeapAlloc
函数用于在指定的堆中分配内存块。通过传入合适的堆句柄,可以在特定的堆对象上进行内存分配和管理操作。分配的内存块可以是可变大小的,并且可以根据需要进行零初始化。
需要注意的是,HeapAlloc
函数是在指定的堆上进行内存分配,而不是全局堆或本地堆。因此,使用 HeapAlloc
函数的前提是必须先通过 HeapCreate
函数创建堆对象,并获取相应的堆句柄。
WINBASEAPI WINBOOL WINAPI HeapFree(HANDLE hHeap, DWORD dwFlags, LPVOID lpMem)
- 作用:释放指定堆中的内存块。
- 参数:
hHeap
:要释放内存的堆的句柄。dwFlags
:释放内存的标志。可以是以下标志的组合:HEAP_NO_SERIALIZE
:禁用堆的同步机制,使多线程访问堆时不同步。
lpMem
:要释放的内存块的指针。
- 返回值:
- 如果操作成功,返回 TRUE ;
- 如果操作失败,返回 FALSE 。
HeapFree
函数用于释放指定堆中的内存块,将之前分配的内存返回给堆以供重用。通过传入适当的堆句柄和内存块指针,可以释放特定堆中的特定内存块。
VirtualAlloc/VirtualFree
WINBASEAPI LPVOID WINAPI VirtualAlloc (LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect);
- 作用:为进程保留或提交指定大小的虚拟内存区域。
- 参数:
lpAddress
:要保留或提交的虚拟内存区域的首字节地址。可以指定为NULL,表示由系统选择地址。dwSize
:要保留或提交的虚拟内存区域的大小(以字节为单位)。flAllocationType
:内存分配的类型标志。可以是以下标志的组合:MEM_COMMIT
:提交虚拟内存区域。MEM_RESERVE
:保留虚拟内存区域。MEM_RESET
:将虚拟内存区域的内容重置为零。MEM_RESET_UNDO
:撤消对虚拟内存区域的重置操作。
flProtect
:内存保护标志,指定分配的内存区域的访问权限和保护级别。
- 返回值:
- 如果操作成功,返回分配的虚拟内存区域的首字节地址;
- 如果操作失败,返回 NULL 。
VirtualAlloc
函数用于在进程的虚拟地址空间中分配或提交虚拟内存区域。虚拟内存可以用于多种目的,例如分配堆内存、映射文件等。通过指定不同的标志,可以控制对虚拟内存的保留、提交和重置操作,并指定相应的内存保护级别。
需要注意的是,VirtualAlloc
函数操作的是虚拟内存,而非物理内存。分配的虚拟内存区域在使用之前需要进行显式的提交操作(使用 MEM_COMMIT
标志),否则访问该内存区域将导致访问冲突异常。此外,释放虚拟内存区域的操作通常使用 VirtualFree
函数。
WINBASEAPI WINBOOL WINAPI VirtualFree(LPVOID lpAddress, SIZE_T dwSize, DWORD dwFreeType)
- 作用:释放指定区域的虚拟内存。
- 参数:
lpAddress
:要释放的虚拟内存区域的起始地址。dwSize
:要释放的虚拟内存区域的大小(以字节为单位)。dwFreeType
:释放内存的类型。可以是以下常量之一:MEM_DECOMMIT
:取消提交内存,将内存区域标记为未提交状态。MEM_RELEASE
:释放内存,将内存区域标记为不再使用。
- 返回值:
- 如果操作成功,返回 TRUE ;
- 如果操作失败,返回 FALSE 。
LocalAlloc/LocalFree
WINBASEAPI HLOCAL WINAPI LocalAlloc (UINT uFlags, SIZE_T uBytes);
- 作用:在本地堆中分配指定大小的内存块。
- 参数:
uFlags
:内存分配的标志。可以是以下标志的组合:LPTR
:返回一个指向分配的内存块的指针,并将内存内容初始化为零。LMEM_FIXED
:返回一个固定的指针,表示分配的内存块。LMEM_ZEROINIT
:分配的内存块被初始化为零。
uBytes
:要分配的内存块的大小(以字节为单位)。
- 返回值:
- 如果分配成功,返回一个指向分配的内存块的指针(如果使用了
LMEM_FIXED
标志)或句柄(如果使用了LPTR
标志)。 - 如果分配失败,返回 NULL 。
- 如果分配成功,返回一个指向分配的内存块的指针(如果使用了
LocalAlloc
函数用于在本地堆中分配内存。本地堆是进程私有的内存区域,只能由相应进程访问。通过指定不同的标志,可以选择返回指针或句柄来表示分配的内存块。分配的内存块可以是固定的(使用指针)或可移动的(使用句柄)。
需要注意的是,LocalAlloc
函数已经过时,不推荐在新的应用程序中使用。现代的 Windows 应用程序通常使用 HeapAlloc
或其他更高级的内存分配函数来进行内存管理。
WINBASEAPI HLOCAL WINAPI LocalFree(HLOCAL hMem)
- 作用:释放指定的本地内存块。
- 参数:
hMem
:要释放的本地内存块的句柄。
- 返回值:
- 如果操作成功,返回 NULL ;
- 如果操作失败,返回输入的句柄
hMem
。
GlobalAlloc/GlobalFree
WINBASEAPI HGLOBAL WINAPI GlobalAlloc (UINT uFlags, SIZE_T dwBytes);
- 作用:在全局堆中分配指定大小的内存块。
- 参数:
uFlags
:内存分配的标志。可以是以下标志的组合:GMEM_FIXED
:返回一个固定的指针,表示分配的内存块。GMEM_MOVEABLE
:返回一个可移动的句柄,表示分配的内存块。GMEM_ZEROINIT
:分配的内存块被初始化为零。GMEM_DISCARDABLE
:分配的内存块可被丢弃。
dwBytes
:要分配的内存块的大小(以字节为单位)。
- 返回值:
- 如果分配成功,返回一个指向分配的内存块的句柄(如果使用了
GMEM_MOVEABLE
标志)或指针(如果使用了GMEM_FIXED
标志)。 - 如果分配失败,返回 NULL 。
- 如果分配成功,返回一个指向分配的内存块的句柄(如果使用了
GlobalAlloc
函数用于在全局堆中分配内存。全局堆是所有进程可访问的公共内存区域。通过指定不同的标志,可以选择返回指针或句柄来表示分配的内存块。分配的内存块可以是固定的(使用指针)或可移动的(使用句柄)。
需要注意的是,GlobalAlloc
函数已经过时,不推荐在新的应用程序中使用。现代的 Windows 应用程序通常使用 HeapAlloc
或其他更高级的内存分配函数来进行内存管理。
WINBASEAPI HGLOBAL WINAPI GlobalFree(HGLOBAL hMem)
- 作用:释放指定的全局内存块。
- 参数:
hMem
:要释放的全局内存块的句柄。
- 返回值:
- 如果操作成功,返回 NULL ;
- 如果操作失败,返回输入的句柄
hMem
。
malloc/free
void *__cdecl malloc(size_t _Size)
- 作用:在堆上分配指定大小的内存块。
- 参数:
_Size
:要分配的内存块的大小(以字节为单位)。
- 返回值:
- 如果分配成功,返回指向分配的内存块的指针;
- 如果分配失败,返回 NULL 。
void __cdecl free(void* _Memory);
- 作用:释放通过动态内存分配函数(如
malloc
、calloc
、realloc
等)分配的内存块。 - 参数:
_Memory
:要释放的内存块的指针。
- 返回值:无。
常用堆调试命令
-
!heap
打印当前进程所有堆0:001> !heap Heap Address NT/Segment Heap 23c9cb00000 NT Heap 23c9c9d0000 NT Heap 23c9e530000 NT Heap 23c9e990000 NT Heap
-
!heap -h
可以查看当前进程所创建的堆空间0:001> !heap -h Index Address Name Debugging options enabled 1: 233708f0000 Segment at 00000233708f0000 to 00000233709ef000 (00012000 bytes committed) 2: 23370730000 Segment at 0000023370730000 to 0000023370740000 (00001000 bytes committed)
-
!heap -x address
打印包含 address 的堆块的相关信息
申请的堆块:0:001> !heap -x 0000023C9CB2FE80 Entry User Heap Segment Size PrevSize Unused Flags ------------------------------------------------------------------------------------------------------------- 0000023c9cb2fe40 0000023c9cb2fe50 0000023c9cb00000 0000023c9cb00000 140 1010 c busy
将这个堆块释放后:
0:001> !heap -x 0000023C9CB2FE80 Entry User Heap Segment Size PrevSize Unused Flags ------------------------------------------------------------------------------------------------------------- 0000023c9cb2fe40 0000023c9cb2fe50 0000023c9cb00000 0000023c9cb00000 140 1010 0 free
-
!heap -i address
显示 address 对应堆块的详细信息,注意这里的 address 指 Entry ,即堆块的起始地址0:001> !heap -i 0000023c9cb2fe40 Detailed information for block entry 0000023c9cb2fe40 Assumed heap : 0x0000023c9cb00000 (Use !heap -i NewHeapHandle to change) Header content : 0x84D7A09E 0x0000546B (decoded : 0x14000014 0x00000101) Owning segment : 0x0000023c9cb00000 (offset 0) Block flags : 0x0 (free ) Total block size : 0x14 units (0x140 bytes) Previous block size: 0x101 units (0x1010 bytes) Block CRC : OK - 0x14 Free list entry : OK Previous block : 0x0000023c9cb2ee30 Next block : 0x0000023c9cb2ff80
-
!heap -v address
检查堆是否损坏,address 为 heap 地址。例如伪造FreeList
链表后可以用这个命令测试是否能通过检查。0:000> !heap -v 247254a0000 HEAPEXT: Unable to get address of ntdll!RtlpHeapInvalidBadAddress. Index Address Name Debugging options enabled 1: 247254a0000 Segment at 00000247254a0000 to 000002472559f000 (0000f000 bytes committed) Flags: 00000002 ForceFlags: 00000000 Granularity: 16 bytes Segment Reserve: 00100000 Segment Commit: 00002000 DeCommit Block Thres: 00000400 DeCommit Total Thres: 00001000 Total Free Size: 00000197 Max. Allocation Size: 00007ffffffdefff Lock Variable at: 00000247254a02c0 Next TagIndex: 0000 Maximum TagIndex: 0000 Tag Entries: 00000000 PsuedoTag Entries: 00000000 Virtual Alloc List: 247254a0110 Uncommitted ranges: 247254a00f0 FreeList[ 00 ] at 00000247254a0150: 00000247254ad730 . 00000247254a7f40 Unable to read nt!_HEAP_FREE_ENTRY structure at fffffffffffffff0 (6 blocks) ##CORRUPTION FOUND at 0x254AC470 PreviousSize field does not match Size field in previous entry Entry->PreviousSize == 0x9 PreviousEntry->Size == 0x41 ##CORRUPTION FOUND at 0x254AC560 PreviousSize field does not match Size field in previous entry Entry->PreviousSize == 0x9 PreviousEntry->Size == 0x9 ##The above errors were found in segment at 0x254A0000
NT Heap
具体过程分析见 ntdll.dll(NtHeap).i64 。
Windows 的 NT Heap 的调用关系如下图所示,NT Heap 分为前端堆(LFH堆)和后端堆两部分。
后端堆
当 LFH 没有启用的时候,我们通过后端堆来分配内存。
相关数据结构
_HEAP
_HEAP
是堆管理的最核心结构,和 linux glibc 的 main_arena
作用类似。每一个 HEAP 都有一个 _HEAP
结构,存在于该 HEAP 的开头。
_HEAP
的定义如下:
0:001> dt _HEAP 233708f0000
ntdll!_HEAP
+0x000 Segment : _HEAP_SEGMENT
+0x000 Entry : _HEAP_ENTRY
+0x010 SegmentSignature : 0xffeeffee
+0x014 SegmentFlags : 2
+0x018 SegmentListEntry : _LIST_ENTRY [ 0x00000233`708f0120 - 0x00000233`708f0120 ]
+0x028 Heap : 0x00000233`708f0000 _HEAP
+0x030 BaseAddress : 0x00000233`708f0000 Void
+0x038 NumberOfPages : 0xff
+0x040 FirstEntry : 0x00000233`708f0740 _HEAP_ENTRY
+0x048 LastValidEntry : 0x00000233`709ef000 _HEAP_ENTRY
+0x050 NumberOfUnCommittedPages : 0xed
+0x054 NumberOfUnCommittedRanges : 1
+0x058 SegmentAllocatorBackTraceIndex : 0
+0x05a Reserved : 0
+0x060 UCRSegmentList : _LIST_ENTRY [ 0x00000233`70901fe0 - 0x00000233`70901fe0 ]
+0x070 Flags : 2
+0x074 ForceFlags : 0
+0x078 CompatibilityFlags : 0
+0x07c EncodeFlagMask : 0x100000
+0x080 Encoding : _HEAP_ENTRY
+0x090 Interceptor : 0
+0x094 VirtualMemoryThreshold : 0xff00
+0x098 Signature : 0xeeffeeff
+0x0a0 SegmentReserve : 0x100000
+0x0a8 SegmentCommit : 0x2000
+0x0b0 DeCommitFreeBlockThreshold : 0x400
+0x0b8 DeCommitTotalFreeThreshold : 0x1000
+0x0c0 TotalFreeSize : 0xc2
+0x0c8 MaximumAllocationSize : 0x00007fff`fffdefff
+0x0d0 ProcessHeapsListIndex : 1
+0x0d2 HeaderValidateLength : 0x2c0
+0x0d8 HeaderValidateCopy : (null)
+0x0e0 NextAvailableTagIndex : 0
+0x0e2 MaximumTagIndex : 0
+0x0e8 TagEntries : (null)
+0x0f0 UCRList : _LIST_ENTRY [ 0x00000233`70901fd0 - 0x00000233`70901fd0 ]
+0x100 AlignRound : 0x1f
+0x108 AlignMask : 0xffffffff`fffffff0
+0x110 VirtualAllocdBlocks : _LIST_ENTRY [ 0x00000233`708f0110 - 0x00000233`708f0110 ]
+0x120 SegmentList : _LIST_ENTRY [ 0x00000233`708f0018 - 0x00000233`708f0018 ]
+0x130 AllocatorBackTraceIndex : 0
+0x134 NonDedicatedListLength : 0
+0x138 BlocksIndex : 0x00000233`708f02e8 Void
+0x140 UCRIndex : (null)
+0x148 PseudoTagEntries : (null)
+0x150 FreeLists : _LIST_ENTRY [ 0x00000233`708fa920 - 0x00000233`70901790 ]
+0x160 LockVariable : 0x00000233`708f02c0 _HEAP_LOCK
+0x168 CommitRoutine : 0x36d0c67f`cfd1ec4e long +36d0c67fcfd1ec4e
+0x170 StackTraceInitVar : _RTL_RUN_ONCE
+0x178 CommitLimitData : _RTL_HEAP_MEMORY_LIMIT_DATA
+0x198 FrontEndHeap : 0x00000233`707f0000 Void
+0x1a0 FrontHeapLockCount : 0
+0x1a2 FrontEndHeapType : 0x2 ''
+0x1a3 RequestedFrontEndHeapType : 0x2 ''
+0x1a8 FrontEndHeapUsageData : 0x00000233`708f2ea0 -> 0
+0x1b0 FrontEndHeapMaximumIndex : 0x402
+0x1b2 FrontEndHeapStatusBitmap : [129] "p"
+0x238 Counters : _HEAP_COUNTERS
+0x2b0 TuningParameters : _HEAP_TUNING_PARAMETERS
EncodeFlagMask
:Heap 初始化后会设置为 0x100000 ,用于判断是否要加密该 heap 空间中每个堆的 chunk_header 。Encoding
(_Heap_Entry
):用于与 chunk_header 做异或的 cookies;所有分配的 chunk 的 chunk_header 都会与Encoding
进行异或,然后在存入内存中。VirtualAllocdBlocks
:一个双向链表的 dummy head ,存放着Flink
和Blink
,将VirtualAllocate
出来的 chunk 链接起来。BlocksIndx
(_Heap_LIST_LOOKUP
):Back-End 中用于管理后端管理器中的 chunk 。FreeList
(_Heap_Entry
):连接 Back-End 中的所有 free chunk ,类似 unsorted bin 。FrontEndHeap
:指向管理 FrontEnd 的 heap 结构。FrontEndHeapUsageData
:指向一个对应各大小 chunk 的数组,记录各种大小 chunk 的使用次数,到达某个程度时会开启该对应大小 chunk 的 Front-End 分配器。如果开启 LFH 后对应的FrontEndHeapUsageData
是SegmentInfoArrays
的下标。FrontEndHeapStatusBitmap
:非常重要。是一个 bitmap 数组,每一项长度为 1 字节,用来记录某个 size 是否开启了 LFH 。判断方式是_HEAP.FrontEndHeapStatusBitmap[(size >> 4) >> 3] & (1 << ((size >> 4) & 7))
是否为 1 ,如果是 1 则说明对应 size 开启了 LFH 。
chunk head
后端段的 chunk head,其分为三种情况:
- Allocated Chunk(
_HEAP_ENTRY
):已分配堆 - Freed Chunk(
_HEAP_ENTRY
):已释放堆 - VirtualAlloc Chunk(
_HEAP_VIRTUAL_ALLOC_ENTRY
):使用 VirtualAlloc 分配的堆
_HEAP_VIRTUAL_ALLOC_ENTRY
和 _HEAP_ENTRY
两种结构定义如下:
0:004> dt _Heap_VIRTUAL_ALLOC_ENTRY
ntdll!_HEAP_VIRTUAL_ALLOC_ENTRY
+0x000 Entry : _LIST_ENTRY
+0x010 ExtraStuff : _HEAP_ENTRY_EXTRA
+0x020 CommitSize : Uint8B
+0x028 ReserveSize : Uint8B
+0x030 BusyBlock : _HEAP_ENTRY
0:004> dt _HEAP_ENTRY
ntdll!_HEAP_ENTRY
+0x000 UnpackedEntry : _HEAP_UNPACKED_ENTRY
+0x000 PreviousBlockPrivateData : Ptr64 Void
+0x008 Size : Uint2B
+0x00a Flags : UChar
+0x00b SmallTagIndex : UChar
+0x008 SubSegmentCode : Uint4B
+0x00c PreviousSize : Uint2B
+0x00e SegmentOffset : UChar
+0x00e LFHFlags : UChar
+0x00f UnusedBytes : UChar
+0x008 CompactHeader : Uint8B
+0x000 ExtendedEntry : _HEAP_EXTENDED_ENTRY
+0x000 Reserved : Ptr64 Void
+0x008 FunctionIndex : Uint2B
+0x00a ContextValue : Uint2B
+0x008 InterceptorValue : Uint4B
+0x00c UnusedBytesLength : Uint2B
+0x00e EntryOffset : UChar
+0x00f ExtendedBlockSignature : UChar
+0x000 ReservedForAlignment : Ptr64 Void
+0x008 Code1 : Uint4B
+0x00c Code2 : Uint2B
+0x00e Code3 : UChar
+0x00f Code4 : UChar
+0x00c Code234 : Uint4B
+0x008 AgregateCode : Uint8B
Allocated Chunk
Allocated Chunk 的 chunk head 为 _HEAP_ENTRY
,结构如下图所示:
PreviousBlockPrivateData
:8 字节,可为前一块 chunk 的 data ,因为 chunk 必须对齐。Size
: chunk 的大小,为实际大小右移 4bit 后的值。比如大小为 0x80 的 chunk 的Size
值为 0x8 。Flags
: 表示该chunk的状态:HEAP_ENTRY_BUSY(01)
堆块处于占用状态HEAP_ENTRY_EXTRA_PRESENT(02)
该块存在额外的描述_HEAP_ENTRY_EXTRA
HEAP_ENTRY_FILE_PATTERN(03)
使用固定模式填充堆块HEAP_ENTRY_VIRTUAL_ALLOC(08)
通过 virtual allocation 虚拟分配的堆块HEAP_ENTRY_LAST_ENTRY(10)
表示是该段的最后一个堆块
SmallTagIndex
: 前 3 个字节异或后的值,用于验证。PreviousSize
: 前⼀个 chunk 的大小,为实际大小右移 4bit 后的值。SegmentOffset
: 在某种情况下用来寻找 Heap 的。SegmentOffset = heap_entry->UnpackedEntry.SegmentOffset; if ( SegmentOffset ) Heap = ((heap_entry & 0xFFFFFFFFFFFF0000ui64) - (SegmentOffset << 16) + 0x10000);
Unusedbytes
:整个 chunk 的大小减去用户 malloc 的大小,因为如果 chunk 是在使用状态Unusedbytes
一定不为 0 ,因此可以判断 chunk 是否空闲(&0x3F 是否为 0)。另外这个值还有一个 0x80 的标志位也可以用来判断 chunk 的状态是前端堆还是后端堆。
如下图所示,chunk head 在内存中是加密的,要想获取原本的 chunk head 需要异或上 Encoding
。
另外可以看到解密后的 chunk head 的 Size
字段为 0x0034
,Flags
字段为 0x1
,因此 SmallTagIndex = LOBYTE(Size) ^ BYTE1(Size) ^ Flags = 0x35
。
Freed Chunk
Freed Chunk 的 chunk head 同样为 _HEAP_ENTRY
,不过由于是释放状态,因此会被链到 FreeList
链表中,因此在 _HEAP_ENTRY
后多了一个 _LIST_ENTRY
结构,如下图所示:
_LIST_ENTRY
定义如下:
0:004> dt _LIST_ENTRY
ntdll!_LIST_ENTRY
+0x000 Flink : Ptr64 _LIST_ENTRY
+0x008 Blink : Ptr64 _LIST_ENTRY
需要特别说明 Freed Chunk 的一些字段:
Flags
为 0 表示 freedUnusedBytes
(&0x3f)始终为 0Flink
指向的是下一个 freed chunk 或FreeList
Blink
指向的是上一个 freed chunk 或FreeList
在 free 完一块 chunk 后,会将该 chunk 放到 FreeLists
中,并会按照大小决定插在 Freelists
中的位置。
VirtualAlloc Chunk
VirtualAlloc Chunk 的 chunk head 为 _HEAP_VIRTUAL_ALLOC_ENTRY
。
Flink
和Blink
:分别指向前⼀个和后⼀个 mmap 出来的 chunk(不管是 in use 还是 freed)Size
:unused size,而且没有右移Unusedbytes
:恒为 4,用来判断 VirtualAlloc Chunk。(注意,_HEAP.Encoding
对应Unusedbytes
的位置通常为 0 ,因此很多地方在未解密_HEAP_ENTRY
的时候直接判断Unusedbytes
)
BlocksIndex (_HEAP_LIST_LOOKUP)
_HEAP
的 BlocksIndex
指向一个类型为 _HEAP_LIST_LOOKUP
的结构体,定义如下。该结构用来管理各种不同大小的 freed chunk ,能快速的找到合适的 chunk 。
0:005> dt _HEAP_LIST_LOOKUP 0x00000233`708f02e8
ntdll!_HEAP_LIST_LOOKUP
+0x000 ExtendedLookup : 0x00000233`708f36b0 _HEAP_LIST_LOOKUP
+0x008 ArraySize : 0x80
+0x00c ExtraItem : 0
+0x010 ItemCount : 0xa
+0x014 OutOfRangeItems : 0
+0x018 BaseIndex : 0
+0x020 ListHead : 0x00000233`708f0150 _LIST_ENTRY [ 0x00000233`708f7e80 - 0x00000233`70902300 ]
+0x028 ListsInUseUlong : 0x00000233`708f0320 -> 0xdc
+0x030 ListHints : 0x00000233`708f0330 -> (null)
ExtendedLookup (Ptr64 _HEAP_LIST_LOOKUP)
:指向下一个BlocksIndex
,通常下一个BlocksIndex
会管理更大的 chunk 。ArraySize (Uint4B)
:该结构会管理最大 chunk 的大小 + 0x10 。上面例子中ArraySize
为 0x80 但由于右移实际是 0x800 。ItemCount (Uint4B)
:4 字节,目前该结构所管理的 chunk 数。OutofRangeItems (Uint4B)
:超出该结构所管理大小的 chunk 的数量。BaseIndex (Uint4B)
:该结构所管理的 chunk 的起始 index ,将(Aligned(size) >> 4) - BaseIndex
作为ListHint
中查找的下标。通常下一个BlocksIndex
将上一个BlocksIndex
的ArraySize
作为BaseIndex
。ListHead (Ptr64 _LIST_ENTRY)
:指向_HEAP
的FreeList
。ListsInUseUlong (Ptr64 Uint4B)
:用在判断ListHint
中是否有适合大小的 chunk ,是一个 bitmap 。ListHint (Ptr64 Ptr64 _LIST_ENTRY)
:十分重要,用来指向对应大小的 chunk array ,其目的就在于更快速找到适合大小的 chunk ,0x10 大小为一个间隔。
ListInUseUlong
和 ListHint
组成了一个位图选择,能更快速的找到合适大小的堆块。并且 BlocksIndex
通过 ExtendedLookUp
由组成了一个快速查找的 BlocksIndex
链表。
分配机制
Allocate (RtlAllocateHeap)
判断 Size
大小是否正常,然后对 Size
进行对齐操作:Size = ((Size ? Size : 1) + Heap->AlignRound) & Heap->AlignMask = ((Size ? Size : 1) + 0x17) & ~0xF
。另外还有 Index
的值:Index = Size >> 4
。
首先判断 Heap->Segment.SegmentSignature
,如果值为 0xDDEEDDEE 说明是 Segment Heap ,需要单独处理。否则为 Nt Heap ,值为 0xFFEEFFEE ,继续进行下一步操作。
将 Size
按照大小分成 3 种,分别进行不同操作分配内存。
- Size ≤ 0x4000
- 0x4000 < size ≤ 0xff000
- Size > 0xff000
Size ≤ 0x4000
-
检查是否有该
Size
对应的FrontEndHeapStatusBitmap
,判断是否启动了LFH- 如果有,就通过LFH分配,即调用
RtlpLowFragHeapAllocFromContext
函数分配内存。 - 如果没有,就在对应的
FrontEndHeapUsageData
的值加上 0x21 ,如果该值超过 0xff00 或者与 0x1f 相与后值大于 0x10(FrontEndHeapUsageData
的低 5 bit 记录的是 malloc 次数减 free 次数,高 11 bit 记录的是 malloc 的次数)就启动 LFH ,即将FrontEndHeapStatusBitmap
对应位置置 1 。(这个操作实际是在后面进行的,不过为了方便理解写在这里)
- 如果有,就通过LFH分配,即调用
-
遍历
BlocksIndex
链表,找到第一个ArraySize
大于Size
的BlocksIndex
,然后找到对应的ListHint
,即BlocksIndex->ListHints[Size - BlocksIndex->BaseIndex]
。调用RtlpAllocateHeap
函数分配内存。 -
查看对应的
ListHint
中是否有值(也就是否有对应 size 的 freed chunk):-
如果刚好有值,就检查该 chunk 的
Flink
是否是同样 size 的 chunk :- 若是则将
Flink
写到对应的ListHint
中。 - 若否则清空对应
ListHint
。
最后将该 chunk 从
Freelist
中 unlink 出来。 - 若是则将
-
如果对应的
ListHint
中本身就没有值,就从比较大的ListHint
中找:- 如果找到了,就以上述同样的方式处理该
ListHint
,并 unlink 该 chunk ,之后对其进行切割,剩下的重新放入FreeList
,如果可以放进ListHint
就会放进去,再 encode header 。 - 如果没较大的
ListHint
也都是空的,那么尝试ExtendedHeap
加大堆空间,再从 extend 出来的 chunk 拿,接着一样切割,放回ListHIint
,encode header 。
- 如果找到了,就以上述同样的方式处理该
-
0x4000 < size ≤ 0xff000
除了没有 LFH 相关操作外,其余都和第一种情况一样。
size > 0xff000
直接调用 ZwAllocateVirtualMemroy
进行分配,类似于 linux 下的 mmap
直接给一大块地址,并且插入 _HEAP->VirtualAllocdBlocks
中。
Free (RtlFreeHeap)
- 调用
RtlpValidateHeapEntry
对要释放的 chunk 进行一系列的检查:- 释放的
_HEAP_ENTRY
是否为 NULL - 释放的
_HEAP_ENTRY
地址是否关于 0x10 对齐 - 通过
UnusedBytes & 0x3F
是否为 0 判断_HEAP_ENTRY
是否已被释放过 - 检查校验位
SmallTagIndex
- 如果
UnusedBytes
为 4 即通过ZwAllocateVirtualMemroy
分配的内存,则判断整个_HEAP_VIRTUAL_ALLOC_ENTRY
是否关于 0x1000 对齐 - 如果
UnusedBytes
不为 4 则通过SegmentOffset
找到_HEAP
然后判断_HEAP_ENTRY
是否在[Heap->Segment.FirstEntry, Heap->Segment.LastValidEntry)
范围内
- 释放的
- 调用
RtlFreeHeap
,进而调用RtlpFreeHeapInternal
,通过Heap->Segment.SegmentSignature
判断是否为 Segment Heap ,如果是则单独处理,否则继续执行。 - 判断地址是否关于 0x10 对齐以及通过
UnusedBytes & 0x3F
是否为 0 判断_HEAP_ENTRY
是否已被释放过。 - 根据
UnusedBytes
是否小于 0 (0x80 是否置位)判断是否是 LFH 堆,如果不是则调用后端堆释放的核心函数RtlpFreeHeap
。 - 解密
_HEAP_ENTRY
并校验SmallTagIndex
,根据 chunk 大小找到对应的BlocksIndex
。 - 根据
UnusedBytes
是否为 4 判断是否是通过ZwAllocateVirtualMemroy
分配的内存。如果是则检查该 chunk 的_HEAP_ENTRY->Flink->Blink == _HEAP_ENTRY->Blink->Flink == &_HEAP_ENTRY
并从_HEAP->VirtualAllocdBlocks
中移除,接着使用RtlpSecMemFreeVirtualMemory
将 chunk 整个 munmap 掉。 - 如果 chunk 大小在 LFH 堆的范围内(
_HEAP_ENTRY->Size < _HEAP->FrontEndHeapMaximumIndex
),会将对应的FrontEndHeapUsageData -= 1
(并不是0x21)。 - 接着判断前后的 chunk 是否是 freed 的状态(根据
_HEAP_ENTRY.Flags
的 1 是否置位判断),如果是的话就检查前后的 freed chunk (校验SmallTagIndex
以及_HEAP_ENTRY->Flink->Blink == _HEAP_ENTRY->Blink->Flink == &_HEAP_ENTRY
)然后将前后的 freed chunk 从FreeList
中 unlink 下来(与上面的方式一样更新ListHint
),再进行合并。 - 合并完之后更新
Size
和PreviousSize
,判断一下Size
较大的情况,然后把合并好的 chunk 插入到ListHint
中;插入时也会对FreeList
进行检查(但是此检查不会触发 abort ,原因在于没有做 unlink 写入)。
LFH 堆
当同一个大小的堆块分配次数过多的时候,除了从后端堆分配所需堆块外,还会额外分配一块很大的内存供前端堆使用,之后再次分配该大小的堆块的时候会从前端堆分配。
相关数据结构
FrontEndHeap(_LFH_HEAP)
_LFH_HEAP
是前端堆管理的核心结构,可以通过 _HEAP
的 FrontEndHeap
成员指针访问。
0:000> dt _LFH_HEAP 0x0000016c`cc6f0000
ntdll!_LFH_HEAP
+0x000 Lock : _RTL_SRWLOCK
+0x008 SubSegmentZones : _LIST_ENTRY [ 0x0000016c`cc7d4ca0 - 0x0000016c`cc7d4ca0 ]
+0x018 Heap : 0x0000016c`cc7d0000 Void
+0x020 NextSegmentInfoArrayAddress : 0x0000016c`cc6f1440 Void
+0x028 FirstUncommittedAddress : 0x0000016c`cc6f2000 Void
+0x030 ReservedAddressLimit : 0x0000016c`cc7b3000 Void
+0x038 SegmentCreate : 2
+0x03c SegmentDelete : 0
+0x040 MinimumCacheDepth : 0
+0x044 CacheShiftThreshold : 0
+0x048 SizeInCache : 0
+0x050 RunInfo : _HEAP_BUCKET_RUN_INFO
+0x060 UserBlockCache : [12] _USER_MEMORY_CACHE_ENTRY
+0x2a0 MemoryPolicies : _HEAP_LFH_MEM_POLICIES
+0x2a4 Buckets : [129] _HEAP_BUCKET
+0x4a8 SegmentInfoArrays : [129] (null)
+0x8b0 AffinitizedInfoArrays : [129] (null)
+0xcb8 SegmentAllocator : (null)
+0xcc0 LocalData : [1] _HEAP_LOCAL_DATA
Heap (_HEAP)
:指向对应的_HEAP
Buckets (_HEAP_BUCKET)
:用来寻找配置大小对应到 Block 大小的阵列结构SegmentInfoArray (_HEAP_LOCAL_SEGMENT_INFO)
:不同大小对应到不同的_HEAP_LOCAL_SEGMENT_INFO
结构,主要管理对应到的 SubSegment 的结构LocalData (_HEAP_LOCAL_DATA)
:主要可以关注其中的LowFragHeap
成员,该成员指向_LFH_HEAP
本身,通常用来找回_LFH_HEAP
。
Buckets(_HEAP_BUCKET)
相关定义如下:
0:000> dx -r1 (*((ntdll!_HEAP_BUCKET *)0x16ccc6f02a8))
(*((ntdll!_HEAP_BUCKET *)0x16ccc6f02a8)) [Type: _HEAP_BUCKET]
[+0x000] BlockUnits : 0x2 [Type: unsigned short]
[+0x002] SizeIndex : 0x1 [Type: unsigned char]
[+0x003 ( 0: 0)] UseAffinity : 0x0 [Type: unsigned char]
[+0x003 ( 2: 1)] DebugFlags : 0x0 [Type: unsigned char]
[+0x003] Flags : 0x0 [Type: unsigned char]
0:000> dq 0x16ccc6f02a4
0000016c`cc6f02a4 00010002`00000001 00030004`00020003
0000016c`cc6f02b4 00050006`00040005 00070008`00060007
0000016c`cc6f02c4 0009000a`00080009 000b000c`000a000b
0000016c`cc6f02d4 000d000e`000c000d 000f0010`000e000f
0000016c`cc6f02e4 00110012`00100011 00130014`00120013
0000016c`cc6f02f4 00150016`00140015 00170018`00160017
0000016c`cc6f0304 0019001a`00180019 001b001c`001a001b
0000016c`cc6f0314 001d001e`001c001d 001f0020`001e001f
在 _LFH_HEAP
中 Buckets
是一个长度为 129 的 _HEAP_BUCKET
结构体数组。_HEAP_BUCKET
主要成员解释如下:
BlockUnits (Uint2B)
: 要分配出去的一个 block 大小右移 4 bit ,也就是SegmentInfoArrays (UChar)
中的_HEAP_SUBSEGMENT
结构的BlockSize
。SizeIndex
:在buckets
中的下标,也就是SegmentInfoArrays
对应位置的BucketIndex
。
SegmentInfoArray(_HEAP_LOCAL_SEGMENT_INFO)
相关定义如下:
0:000> dx -r1 ((ntdll!_HEAP_LOCAL_SEGMENT_INFO *)0x16ccc6f12c0)
((ntdll!_HEAP_LOCAL_SEGMENT_INFO *)0x16ccc6f12c0) : 0x16ccc6f12c0 [Type: _HEAP_LOCAL_SEGMENT_INFO *]
[+0x000] LocalData : 0x16ccc6f0cc0 [Type: _HEAP_LOCAL_DATA *]
[+0x008] ActiveSubsegment : 0x16ccc7d4cc0 [Type: _HEAP_SUBSEGMENT *]
[+0x010] CachedItems [Type: _HEAP_SUBSEGMENT * [16]]
[+0x090] SListHeader [Type: _SLIST_HEADER]
[+0x0a0] Counters [Type: _HEAP_BUCKET_COUNTERS]
[+0x0a8] LastOpSequence : 0x1 [Type: unsigned long]
[+0x0ac] BucketIndex : 0x7 [Type: unsigned short]
[+0x0ae] LastUsed : 0x0 [Type: unsigned short]
[+0x0b0] NoThrashCount : 0x0 [Type: unsigned short]
在 _LFH_HEAP
中 SegmentInfoArray
是一个长度为 129 的 _HEAP_LOCAL_SEGMENT_INFO
结构体指针数组。
_HEAP_LOCAL_SEGMENT_INFO
主要成员解释如下:
LocalData (_HEAP_LOCAL_DATA)
:指向_LFH_HEAP->LocalData
,方便从SegmentInfo
找回_LFH_HEAP
。BucketIndex
:buckets
中对应位置的SizeIndex
ActiveSubsegment (_HEAP_SUBSEGMENT)
:指向当前分配 chunk 使用的SubSegment
,SubSegment
用于管理UserBlock
分配 chunk 。CachedItems (_HEAP_SUBSEGMENT)
:长度为 16 的_HEAP_SUBSEGMENT
结构体指针数组,存放对应该SegmentInfo
且还有可以分配 chunk 的SubSegment
。当ActiveSubsegment
中的 chunk 用完时,会从这里选择空闲 chunk 最多的_HEAP_SUBSEGMENT
结构替换掉ActiveSubsegment
。
ActiveSubsegment(_HEAP_SUBSEGMENT)
相关定义如下:
0:000> dt 0x16ccc7d4cc0 _HEAP_SUBSEGMENT
ntdll!_HEAP_SUBSEGMENT
+0x000 LocalInfo : 0x0000016c`cc6f12c0 _HEAP_LOCAL_SEGMENT_INFO
+0x008 UserBlocks : 0x0000016c`cc7d3c90 _HEAP_USERDATA_HEADER
+0x010 DelayFreeList : _SLIST_HEADER
+0x020 AggregateExchg : _INTERLOCK_SEQ
+0x024 BlockSize : 8
+0x026 Flags : 0
+0x028 BlockCount : 0x1f
+0x02a SizeIndex : 0x7 ''
+0x02b AffinityIndex : 0 ''
+0x024 Alignment : [2] 8
+0x02c Lock : 7
+0x030 SFreeListEntry : _SINGLE_LIST_ENTRY
LocalInfo (_HEAP_LOCAL_SEGMENT_INFO)
:指回对应的SegmentInfoArray (_HEAP_LOCAL_SSEGMENT_INFO)
。UserBlock (_HEAP_USERDATA_HEADER)
:指向该子段所管理的用户数据头(_HEAP_USERDATA_HEADER
)的指针。DelayFreeList (_SLIST_HEADER)
:用于延迟释放的单向链表头。AggregateExchg (_INTERLOCK_SEQ)
:其中的主要成员Depth
记录了堆分配空间剩余的堆块个数,管理对应到的 UserBlock 中还有多少 freed chunk, LFH 用这个判断是否还从该 UserBlock 分配。BlockSize
:表示该子段管理的内存块大小。Flags
:用于标识该子段的一些属性。BlockCount
:表示该子段中空闲内存块的数量。SizeIndex
:表示该子段所管理的内存块大小对应的索引值。AffinityIndex
:用于在多处理器系统中进行性能优化的一个指示器。Alignment
:表示该子段所管理的内存块的对齐方式。Lock
:用于保护该子段的互斥锁。SFreeListEntry (_SINGLE_LIST_ENTRY)
:用于单向链表的一个节点,目前没见过这个字段非 0 的情况。
AggregateExchg(_INTERLOCK_SEQ)
0:000> dx -r1 (*((ntdll!_INTERLOCK_SEQ *)0x16ccc7d4ce0))
(*((ntdll!_INTERLOCK_SEQ *)0x16ccc7d4ce0)) [Type: _INTERLOCK_SEQ]
[+0x000] Depth : 0x1c [Type: unsigned short]
[+0x002 (14: 0)] Hint : 0x3 [Type: unsigned short]
[+0x002 (15:15)] Lock : 0x0 [Type: unsigned short]
[+0x002] Hint16 : 0x3 [Type: unsigned short]
[+0x000] Exchg : 196636 [Type: long]
Depth
:该UserBlock
的 freed chunk 的数量Lock
:锁
UserBlocks(_HEAP_USERDATA_HEADER)
0:017> dx -r1 ((ntdll!_HEAP_USERDATA_HEADER *)0x20793447a10)
((ntdll!_HEAP_USERDATA_HEADER *)0x20793447a10) : 0x20793447a10 [Type: _HEAP_USERDATA_HEADER *]
[+0x000] SFreeListEntry [Type: _SINGLE_LIST_ENTRY]
[+0x000] SubSegment : 0x2079341f200 [Type: _HEAP_SUBSEGMENT *]
[+0x008] Reserved : 0x20793406330 [Type: void *]
[+0x010] SizeIndexAndPadding : 0xc [Type: unsigned long]
[+0x010] SizeIndex : 0xc [Type: unsigned char]
[+0x011] GuardPagePresent : 0x0 [Type: unsigned char]
[+0x012] PaddingBytes : 0x0 [Type: unsigned short]
[+0x014] Signature : 0xf0e0d0c0 [Type: unsigned long]
[+0x018] EncodedOffsets [Type: _HEAP_USERDATA_OFFSETS]
[+0x020] BusyBitmap [Type: _RTL_BITMAP_EX]
[+0x030] BitmapData [Type: unsigned __int64 [1]]
-
SubSegment (_HEAP_SUBSEGMENT)
:指回对应的SubSegment
-
EncodedOffsets
:用来验证 chunk header 是否被修改过,由下面 4 个值异或:RtlpLFHKey
:进程创建时初始化的一个 8 字节随机数UserBlock
的地址UserBlock
对应的LowFragHeap
的地址sizeof(UserBlocks) | ((0x10 * BlockIndex) << 16)
在释放一个 LFH chunk 时,NT Heap 会通过
UserBlock ^ RtlpLFHKey ^ _SegmentInfoArray->EncodedOffsets ^ LowFragHeap
计算出sizeof(UserBlocks) | ((0x10 * BlockIndex) << 16)
的值,进而计算出 chunk 的地址与要释放的 chunk 的地址进行比较,从而验证 chunk header 是否被修改过。 -
BusyBitmap
:记录UserBlock
中在使用的 chunk 的 bitmap -
Block
:LFH 返回给使用者的 chunk
chunk/block(_HEAP_ENTRY)
-
SubSegmentCode
:用来计算UserBlock
的地址,是下面 4 个值的异或:- chunk 对应的
_HEAP
地址的低 4 字节 RtlpLFHKey
的低 4 字节- chunk 地址右移 4 bit
- chunk 与其所在的
UserBlock
的距离左移 12 bit
在代码中通常采用
&chunk_head->UnpackedEntry.PreviousBlockPrivateData - ((RtlpLFHKey ^ Heap ^ chunk_head->UnpackedEntry.SubSegmentCode ^ (chunk_head >> 4)) >> 12
来找到其所在的UserBlock
。 - chunk 对应的
-
PreviousSize
:该 chunk 在UserBlock
中的 index 左移 8 bit -
SegmentOffset
:通常为 0 ,没有用。 -
UnusedBytes
:在空闲 chunk 中为 0x80,在使用的chunk 中为UnusedBytes >= 0x3F ? 0xBF : (UnusedBytes | 0x80)
。
分配机制
Allocate
这里仅考虑连续分配相同大小 chunk 的情况
- 第 17 次 malloc:
- 在
RtlpAllocateHeap
函数中heap->FrontEndHeapUsageData[AlignedIndex]
加上 0x21 后满足(FrontEndHeapUsageData & 0x1Fu) > 0x10 || FrontEndHeapUsageData > 0xFF00u
:- 会调用
RtlpGetLFHContext
获取该大小对应于SegmentInfoArray
中的下标,然而此时前端堆未初始化(heap->FrontEndHeap
为 NULL),因此RtlpGetLFHContext
返回 -1,此时会将heap->CompatibilityFlags
的 0x20000000u 置位,表示表示下次 allocate 时会去初始化 LFH 堆。
- 会调用
- 继续通过后端堆申请 chunk 。
- 在
- 第 18 次 malloc:
- 由于
heap->CompatibilityFlags
的 0x20000000u 置位,在RtlpAllocateHeap
函数中会调用RtlpPerformHeapMaintenance
函数,进而调用RtlpActivateLowFragmentationHeap
函数创建 LFH 堆。 - 在
RtlpActivateLowFragmentationHeap
函数中:- 调用
RtlpExtendFrontEndUsageArray
函数扩展FrontEndUsageData
大小 - 调用
RtlpExtendListLookup
函数在heap->BockIndex->ExtendedLookup
为 NULL 时创建一个BlockIndex
并将地址写到ExtendedLookup
上。 - 调用
RtlpCreateLowFragHeap
创建一个 LFH 堆并将地址写到heap->FrontEndHeap
上。
- 调用
- 在
RtlpAllocateHeap
函数中heap->FrontEndHeapUsageData[AlignedIndex]
加上 0x21 后满足(FrontEndHeapUsageData & 0x1Fu) > 0x10 || FrontEndHeapUsageData > 0xFF00u
:- 调用
RtlpGetLFHContext
获取该大小对应于SegmentInfoArray
中的下标,如果没有创建对应的SegmentInfoArrays
就调用RtlpInitializeSegmentInfoForBucket
创建对应的SegmentInfoArrays
。 - 在
FrontEndHeapUsageData
对应位置写入SegmentInfoArray
的下标并更新FrontEndHeapStatusBitmap
。
- 调用
- 继续通过后端堆申请 chunk 。由于前面创建结构会申请一些堆块,所以造成了第 18 次开始 chunk 申请不连续的假象。
- 由于
- 第 19 次 malloc:
- 在
RtlpAllocateHeapInternal
函数中因为FrontEndHeapStatusBitmap
对应位置被置位,因此会调用RtlpLowFragHeapAllocFromContext
在 LFH 堆中进行分配。 - 在
RtlpLowFragHeapAllocFromContext
函数中判断SegmentInfoArray->ActiveSubsegment
是否不为 NULL。 如果不为 NULL:- 取出随机范围
RtlpSearchWidth[BucketIndex]
,即待会申请 chunk 时的随机选取范围SearchWidth
。 - 取出随机种子数组
RtlpLowFragHeapRandomData
的随机下标TEB->HeapData
并且更新TEB->HeapData
。 - 调用
RtlpLfhFindClearBitAndSet
函数获取空闲 chunk 的下标:- 从
ActiveSubsegment->AggregateExchg
中缓存的位置开始按 64 bit 一组循环遍历UserBlocks->BusyBitmap
,直到找到有空闲 chunk 的一组。 - 根据前面传进来的随机数计算出在这个组中查找空闲 chunk 的起始下标
RandomOffset = (SearchWidth * LowFragHeapRandomData) >> 7
。 - 设置
SearchWidthMask
为 -1,如果查找范围SearchWidth
小于 64 时为了确保一定能找到空闲 chunk 需要在BusyBitmap
中用 bsf 指令找到一个空闲 chunk 的位置FirstOffset
,然后SearchWidthMask
设置为((1i64 << SearchWidth) - 1) << FirstOffset
确保一定能覆盖到空闲 chunk 。另外还会将RandomOffset
加上FirstOffset
。 - 在
BusyBitmap
的RandomOffset
偏移处开始,SearchWidthMask
范围内,找到第一个空闲 chunk 的下标FreeChunkOffset
。 - 将
FreeChunkOffset
加上RandomOffset
并关于 64 取模得到在BusyBitmap
中的真实偏移。 - 更新
BusyBitmap
,在要取出的 chunk 对应位置置位。 - 计算并返回空闲 chunk 在整个
UserBlock
中的下标。
- 从
- 缓存这次查找到空闲 chunk 的下标到
ActiveSubsegment->AggregateExchg
中。 - 通过
UserBlocks->EncodedOffsets
计算出空闲 chunk 的具体位置。 - 通过判断
UnusedBytes
记录未使用大小的位置(&0x3F)是否为 0 来检查是不是已释放的堆块,如果是说明出错。 - 设置 chunk 的
UnusedBytes
为UnusedBytes >= 0x3F ? 0xBF : (UnusedBytes | 0x80)
。 - 返回申请的 chunk 的 User Data 部分的地址。
- 取出随机范围
- 由于
SegmentInfoArray
刚刚创建还没有创建_HEAP_SUBSEGMENT
,因此上面的判断不通过,会进入创建和初始化_HEAP_SUBSEGMENT
的流程。 - 尝试更换
ActiveSubsegment
,在SegmentInfoArray->CachedItems
中遍历,找到一个Depth
最大的(即空闲 chunk 最多的)_HEAP_SUBSEGMENT
。如果找到了就将其替换到SegmentInfoArray->ActiveSubsegment
然后跳转至RtlpLowFragHeapAllocFromContext
函数开头尝试重新分配。不过这里由于还没创建_HEAP_SUBSEGMENT
因此会跳转到创建_HEAP_SUBSEGMENT
的流程。 - 调用
RtlpAllocateUserBlock
函数为UserBlock
申请一块内存。 - 调用
RtlpLowFragHeapAllocateFromZone
函数为HeapSubsegment
申请一块内存。 - 调用
RtlpSubSegmentInitialize
函数初始化HeapSubsegment
及UserBlock
- 初始化
UserBlock
中的每个 chunk 的SubSegmentCode
,PreviousSize
,UnusedBytes
。 - 初始化
UserBlock
的SubSegment
,BusyBitmap
,BitmapData
,EncodedOffsets
等。 - 初始化
HeapSubsegment
的BlockSize
,BlockCount
,LocalInfo
,SizeIndex
,UserBlocks
等。
- 初始化
- 将创建的
HeapSubsegment
替换到SegmentInfoArray->ActiveSubsegment
, 然后跳转至RtlpLowFragHeapAllocFromContext
函数开头尝试重新分配。
- 在
Free
- 在
RtlpFreeHeapInternal
函数中首先会检查释放的内存地址是否对齐 0x10 。 - 通过
_HEAP_ENTRY->UnpackedEntry.UnusedBytes & 0x3F
是否为 0 判断 chunk 是否已被释放。 - 通过
_HEAP_ENTRY->UnpackedEntry.SubSegmentCode
找到对应的UserBlock
进而找到HeapSubsegment
。 - 通过
UserBlock->EncodedOffsets
再尝试找回_HEAP_ENTRY
从而校验有无恶意修改。 - 将
_HEAP_ENTRY.UnusedBytes
设置为 0x80 。 - 将
UserBlocks->BusyBitmap.Buffer
中释放的 chunk 对应的位复位。
Segment Heap
具体过程分析见 ntdll.dll(SegmentHeap).i64 。
Segment Heap 分为如下几个部分:
-
Frontend Allocation
- Variable Size Allocation
- Low Fragmentation Heap
-
Backend Allocation
- Segment Allocation
-
Large Block Allocation
如果想对某个特定进程,开启Segment Heap分配机制,可以为该进程创建如下注册表,设置 FrontEndHeapDebugOptions = 0x8
。
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\(executable)
FrontEndHeapDebugOptions = (DWORD)
Bit 2 (0x04): Disable Segment Heap
Bit 3 (0x08): Enable Segment Heap
如果想对整个系统开启 Segment Heap 机制,可以设置注册表:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Mamager\Segment Heap
Enabled = (DWORD)
0 : Disable Segment Heap
(Not 0): Enable Segment Heap
VS 堆
相关数据结构
_SEGMENT_HEAP
0:000> dt _SEGMENT_HEAP 1c48bd70000
ntdll!_SEGMENT_HEAP
+0x000 EnvHandle : RTL_HP_ENV_HANDLE
+0x010 Signature : 0xddeeddee
+0x014 GlobalFlags : 0x1000
+0x018 Interceptor : 0
+0x01c ProcessHeapListIndex : 4
+0x01e AllocatedFromMetadata : 0y0
+0x020 CommitLimitData : _RTL_HEAP_MEMORY_LIMIT_DATA
+0x020 ReservedMustBeZero1 : 0
+0x028 UserContext : (null)
+0x030 ReservedMustBeZero2 : 0
+0x038 Spare : (null)
+0x040 LargeMetadataLock : 0
+0x048 LargeAllocMetadata : _RTL_RB_TREE
+0x058 LargeReservedPages : 0
+0x060 LargeCommittedPages : 0
+0x068 Tag : 0
+0x070 StackTraceInitVar : _RTL_RUN_ONCE
+0x080 MemStats : _HEAP_RUNTIME_MEMORY_STATS
+0x0d8 GlobalLockCount : 0
+0x0dc GlobalLockOwner : 0
+0x0e0 ContextExtendLock : 0
+0x0e8 AllocatedBase : 0x000001c4`8bd70840 ""
+0x0f0 UncommittedBase : 0x000001c4`8bd71000 "--- memory read error at address 0x000001c4`8bd71000 ---"
+0x0f8 ReservedLimit : 0x000001c4`8bdc0000 "--- memory read error at address 0x000001c4`8bdc0000 ---"
+0x100 ReservedRegionEnd : 0x000001c4`8bdc0000 "--- memory read error at address 0x000001c4`8bdc0000 ---"
+0x108 CallbacksEncoded : _RTL_HP_HEAP_VA_CALLBACKS_ENCODED
+0x140 SegContexts : [2] _HEAP_SEG_CONTEXT
+0x2c0 VsContext : _HEAP_VS_CONTEXT
+0x380 LfhContext : _HEAP_LFH_CONTEXT
EnvHandle (RTL_HP_ENV_HANDLE)
:Segment Heap 的环境句柄。Signature
:区分堆类型的签名,对于 Segment Heap 总是 0xDDEEDDEE 。LargeAllocMetadata (_RTL_RB_TREE)
:large blocks 管理信息的红黑树。LargeReservedPages
:对于 large blocks 分配保留的页面。LargeCommittedPages
:large blocks 分配时被提交的 页面。AllocatedBase
:指向_SEGMENT_HEAP
结构体的底部,用于分配后续的 LFH 结构体等。SegContexts (_HEAP_SEG_CONTEXT)
:与 Segment 有关的管理结构体。VsContext (_HEAP_VS_CONTEXT)
:Frontend Allocation 中 Variable Size Allocation 的核心结构体,跟踪 variable size allocation 分配状态。LfhContext ( _HEAP_LFH_CONTEXT)
:Frontend Allocation 中 Low Fragmentation Heap 的核心结构体,跟踪 LFH 分配状态。
RtlpHpHeapGlobals(_RTLP_HP_HEAP_GLOBALS)
在 Segment Heap 中,许多数据和指针都被加密了。RtlpHpHeapGlobals
用于存放加密用的一些 key 和其他信息。
0:000> dt _RTLP_HP_HEAP_GLOBALS
ntdll!_RTLP_HP_HEAP_GLOBALS
+0x000 HeapKey : Uint8B
+0x008 LfhKey : Uint8B
+0x010 FailureInfo : Ptr64 _HEAP_FAILURE_INFORMATION
+0x018 CommitLimitData : _RTL_HEAP_MEMORY_LIMIT_DATA
+0x038 Flags : Uint4B
+0x038 FlagsBits : <unnamed-tag>
HeapKey
:8 字节随机数,用于 VS Allocator 和 Segment Allocator 中的数据加密。LfhKey
:8 字节随机数,用于 LowFragmentationHeap 中的数据加密。
_HEAP_VS_CONTEXT
管理 VS 分配的结构体
0:000> dx -r1 (*((ntdll!_HEAP_VS_CONTEXT *)0x1c48bd702c0))
(*((ntdll!_HEAP_VS_CONTEXT *)0x1c48bd702c0)) [Type: _HEAP_VS_CONTEXT]
[+0x000] Lock : 0x1 [Type: unsigned __int64]
[+0x008] LockType : HeapLockPaged (0) [Type: _RTLP_HP_LOCK_TYPE]
[+0x010] FreeChunkTree [Type: _RTL_RB_TREE]
[+0x020] SubsegmentList [Type: _LIST_ENTRY]
[+0x030] TotalCommittedUnits : 0x3 [Type: unsigned __int64]
[+0x038] FreeCommittedUnits : 0x0 [Type: unsigned __int64]
[+0x040] DelayFreeContext [Type: _HEAP_VS_DELAY_FREE_CONTEXT]
[+0x080] BackendCtx : 0x380 [Type: void *]
[+0x088] Callbacks [Type: _HEAP_SUBALLOCATOR_CALLBACKS]
[+0x0b0] Config [Type: _RTL_HP_VS_CONFIG]
[+0x0b4] Flags : 0x0 [Type: unsigned long]
Lock
:锁LockType (_RTLP_HP_LOCK_TYPE)
:锁的类型,有 3 种:HeapLockPaged
HeapLockNonPaged
HeapLockTypeMax
FreeChunkTree (_RTL_RB_TREE)
:管理空闲 chunk 的红黑树。红黑树按照 chunk 大小维护,较大的 chunk 在左,较小的 chunk 在右。Root
:指向红黑树的根节点。Encoded
:根据最低比特是否为 1 决定红黑树的指针是否加密(默认不加密)。加密方法是当前节点的指针异或当前节点的地址,对于Root
为EncodedRoot = Root ^ FreeChunkTree
。
SubsegmentList
:所有的 VS Subsegment 链表,实际存储的是SubsegmentList
地址异或指向的 VS Subsegment 的地址。DelayFreeContext (_HEAP_VS_DELAY_FREE_CONTEXT)
:VsContext->Config
决定是否开启(用户态默认不开启,内核态默认开启),当开启时释放的 chunk 会先放到DelayFreeContext
这个单向链表中,当链表中的 chunk 达到一定数量的时候才会集中释放。BackendCtx
:指向 VS 堆的后端堆分配器,即_SEGMENT_HEAP.SegContexts (_HEAP_SEG_CONTEXT)
。这个指针异或了_HEAP_VS_CONTEXT
的地址。Callbacks
:用于管理 VS SubSegments 函数指针集合,函数指针都经过加密RtlpHpHeapGlobals.HeapKey ^ VsContext_addr ^ func_ptr
。Allocate
:RtlpHpSegVsAllocate
Free
:RtlpHpSegLfhVsFree
Commit
:RtlpHpSegLfhVsCommit
Decommit
:RtlpHpSegLfhVsDecommit
ExtendContext
:NULL
Config (_RTL_HP_VS_CONFIG)
:用于表示 VS 分配器的属性。PageAlignLargeAllocs
: 用户态默认关闭。FullDecommit
EnableDelayFree
:用户态默认关闭。
_HEAP_VS_SUBSEGMENT
管理 VS SubSegment 的结构体
0:000> 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
Listentry
:每一个 VS SubSegment 是VsContext.SubsegmentList
链表的一个节点 。实际存储的地址还要再异或Listentry
本身的地址。CommitBitmap
:VS SubSegment 中 pages 的提交位图。Size
:VS SubSegment 的大小(去掉 0x30 大小的头部),右移 4 位。Signature
:用于检查 VS SubSegment ,通过Size ^ 0x2BED
计算。
chunk head
Variable Size Allocation 分为 2 种情况:
- Allocated Chunk(
_HEAP_VS_CHUNK_HEADER
):已分配的 chunk - Freed Chunk(
_HEAP_VS_CHUNK_FREE_HEADER
):已释放的 chunk
Allocated Chunk
Allocated Chunk 的 chunk head 为 _HEAP_VS_CHUNK_HEADER
,结构如下图所示:
MemoryCost
:只有空闲堆块才会使用。UnsafeSize
:堆块Size
,右移 4 位。UnsafePrevSize
:前一个堆块Size
,右移 4 位。Allocated
:表示堆块是否空闲,已分配恒为 0x1 。EncodedSegmentPageOffset
:chunk 所在 page 在 VS Subsegment 中的索引,用于查找 VS Subsegment 。这个值是被加密的:EncodedSegmentPageOffset = SegmentPageOffset ^ (int8)chunk address ^ (int8)RtlpHpHeapGlobals.HeapKey
。解密后的SegmentPageOffset
通过下面的代码寻找_HEAP_VS_SUBSEGMENT
。VSSubsegment = (_HEAP_VS_SUBSEGMENT *)(((unsigned __int64)__pChunkHeader - (unsigned int)(SegmentPageOffset << 12)) & 0xFFFFFFFFFFFFF000ui64);
UnusedBytes
:用于表示堆块有未被使用的内存。
chunk head 的前 8 字节进行了加密:chunk header = chunk header ^ chunk address ^ RtlpHpHeapGlobals.HeapKey
。
Freed Chunk
Allocated Chunk 的 chunk head 为 _HEAP_VS_CHUNK_FREE_HEADER
,结构如下图所示:
MemoryCost
:表示 chunk 被申请的时候会有多少 page 被提交。Node (_RTL_BALANCED_NODE)
Children[2] (Left/Right)
:左右子树节点ParentValue
:父节点
chunk head 的前 8 字节进行了加密,加密方式和 Allocated Chunk 相同。
分配机制
Allocate
- 在
RtlpAllocateHeapInternal
函数中,首先判断Heap->Signature == 0xDDEEDDEE
确定Heap
是_SEGMENT_HEAP
类型的。 - 如果
(RtlpHpAppCompatFlags & 2) != 0
:- 如果申请的内存大小
size
不超过 0xFEFF8 则将size
加上 0x10,否则加上 0x40。
- 如果申请的内存大小
- 如果
Size <= 0x4000 - 16
那么需要检查对应的 LFH 是否开启。- 查找
RtlpLfhBucketIndexMap[(Size + 15) >> 4]
得到Size
对应的Heap->LfhContext->Buckets[]
中的下标。 - 如果
Bucket
指针最低位不为 1 说明 LFH 已开启,直接进行 LFH 分配。 - 否则在
Bucket
的第 2 个 WORD 加上 0x21 。 - 判断
Bucket
的第 2 个 WORD 是否满足与 0x1F 大于 0x10 或者大于 0xFF00(和 Nt Heap 判断条件相同),如果满足则调用RtlpHpLfhBucketActivate
初始化Bucket
然后后续用 LFH 分配。
- 查找
- 判断
Size
是否大于 0x20000 ,如果大于则使用 Large Block Allocation 分配内存。 - 否则调用核心函数
RtlpHpVsContextAllocateInternal
使用 Variable Size Allocation 分配内存。 - 通过
((Size + 15) >> 4) + 1
计算出ChunkIndex
。 - 在红黑树
VSContext->FreeChunkTree
中搜索大于ChunkIndex
的最小的 chunk 。 - 如果找不到合适的 chunk 会调用
RtlpHpVsSubsegmentCreate
函数使用 Segment Allocation 分配一个新的VSSubsegment
。- 依次调用
RtlpHpSegVsAllocate
和RtlpHpSegLfhVsCommit
使用 Segment Allocation 分配一个新的VSSubsegment
。 - 初始化
VSSubsegment->Size
为整个VSSubsegment
大小减去 0x30 的头部然后右移 4 位 。 - 初始化
VSSubsegment->Signature = VSSubsegment->Size ^ 0x2BED
。 - 初始化
VSSubsegment
中的 chunk。VSSubsegment
初始时只有一个 chunk,这里要将 chunk 头清零,然后设置UnsafeSize
为VSSubsegment->Size
并加密 chunk 头部。 - 检查
VSContext->SubsegmentList.Blink.Flink = VSContext->SubsegmentList
,如果检查通过则将新创建的VSSubsegment
从SubsegmentList.Blink
插入到SubsegmentList
链表中。注意这里涉及到的指针都是加密的。 - 将新申请的
VSSubsegment
中的 chunk 插入到VSContext->FreeChunkTree
中然后重新在红黑树中搜索合适的 chunk 。
- 依次调用
- 通过查找到的 chunk 找到
VSSubsegment
然后校验(VSSubsegment->Signature ^ VSSubsegment->Size ^ 0x2BED) & 0x7FFF == 0
。 - 调用
RtlpHpVsChunkSplit
将 chunk 从红黑树中取出并切掉多余的 chunk ,然后将多余的 chunk 插入到VSContext->FreeChunkTree
中。在这一过程中初始化了申请的 chunk 和切下来的 chunk 的UnsafeSize
,Allocated
,EncodedSegmentPageOffset
和切下来的 chunk 的下一个相邻 chunk 的UnsafePreSize
。
Free
- 在
RtlpFreeHeapInternal
函数中,首先判断Heap->Signature == 0xDDEEDDEE
确定Heap
是_SEGMENT_HEAP
类型的。 - 如果
BlockPtr
最低 16 比特为 0 则判定为 Large Block 堆分配。 - 通过
Heap->SegContexts[0].SegmentMask & BlockPtr
找到所在的HeapPageSegment
,然后校验HeapPageSegment->Signature ^ HeapPageSegment ^ HeapSegContexts ^ RtlpHpHeapGlobals.HeapKey == 0xA2E64EADA2E64EAD
(HeapSegContexts
即Heap->SegContexts[0]
)。 - 通过
HeapPageSegment + sizeof(_HEAP_PAGE_RANGE_DESCRIPTOR) * ((BlockPtr - HeapPageSegment) >> HeapSegContext->UnitShift)
找到BlockPtr
对应的_HEAP_PAGE_RANGE_DESCRIPTOR
。 - 通过
HeapPageSegment + (( HeapPageRangeDesctiptor - HeapPageSegment ) / sizeof(_HEAP_PAGE_RANGE_DESCRIPTOR)) << HeapSegContext->UnitShift
找到对应的 Subsegment 。 - 获取
HeapPageRangeDesctiptor
的属性HeapPageRangeDesctiptor->RangeFlags
,根据RangeFlags & 0xC
是否等于 8 判断是 Low Fragmentation Heap 还是 Variable Size Allocation 分配的。如果是 Variable Size Allocation 分配的 chunk 会调用RtlpHpVsContextFree
函数完成 chunk 的释放。- 通过
(VSSubsegment->Signature ^ VSSubsegment->Size ^ 0x2BED) & 0x7FFF ==0
校验VSSubsegment->Signature
。 - 通过
ChunkHeader.Allocated
判断 chunk 是否已被释放来防止 double free 。 - 将
ChunkHeader.EncodedSegmentPageOffset
解密成SegmentPageOffset
,然后通过SegmentPageOffset
找到VSSubsegment
并校验这个VSSubsegment
的Signature
,以此来校验SegmentPageOffset
。 - 再次通过
Allocated
判断 double free 。 - 调用
RtlpHpVsChunkCoalesce
函数来合并释放的 chunk 的前后空闲 chunk 。- 更新 chunk 的
Allocated
为 0 (free)。 - 如果
UnsafePrevSize
不为 0 说明有前一个空闲 chunk 。找到前一个空闲 chunk 并判断该 chunk 是否已被释放。如果前一个相邻 chunk 也是释放状态就将该 chunk 从红黑树中取出并记录合并完的 chunk 头位置FinalChunk
和合并完的大小MergedChunkSize
。 - 如果当前 chunk 不是
VSSubsegment
中的最后一个 chunk 那么找到当前 chunk 的后一个相邻 chunk 并判断该 chunk 是否已被释放。如果后一个相邻 chunk 也是释放状态就将该 chunk 从红黑树中取出并更新合并完的大小MergedChunkSize
。 - 如果
MergedChunkSize
不等于合并前的 chunk 大小说明发生了 chunk 合并,需要更新更新FinalChunk
的UnsafeSize
和后一个 chunk (如果存在)的UnsafePrevSize
。
- 更新 chunk 的
- 如果合并完之后整个
VSSubsegment
都是空闲的则先调用RtlpHpVsSubsegmentCleanup
函数将VSSubsegment
从_HEAP_VS_CONTEXT.SubsegmentList
链表中取出,取出前会进行双向链表的检查。之后调用RtlpHpVsSubsegmentFree
最终调用VSContext->Callbacks.Free
函数释放整个VSSubsegment
。 - 否则将合并完的 chunk 插入到
FreeChunkTree
中。
- 通过
- 如果在 LFH 范围且未开启 LFH (即对应
Buckets
为初始化)则将对应LfhContext->Bucket
减 1 (与 Nt Heap 相同)。
LFH 堆
相关数据结构
与之前的内存管理不同,这里 LFH 堆分配的 chunk 没有 chunk 头,并被称为 Block
。
_HEAP_LFH_CONTEXT
0:000> dx -r1 (*((ntdll!_HEAP_LFH_CONTEXT *)0x2bef8360380))
(*((ntdll!_HEAP_LFH_CONTEXT *)0x2bef8360380)) [Type: _HEAP_LFH_CONTEXT]
[+0x000] BackendCtx : 0x2bef8360140 [Type: void *]
[+0x008] Callbacks [Type: _HEAP_SUBALLOCATOR_CALLBACKS]
[+0x030] AffinityModArray : 0x7ff902da7733 : 0x1 [Type: unsigned char *]
[+0x038] MaxAffinity : 0x20 [Type: unsigned char]
[+0x039] LockType : 0x0 [Type: unsigned char]
[+0x03a] MemStatsOffset : -768 [Type: short]
[+0x03c] Config [Type: _RTL_HP_LFH_CONFIG]
[+0x040] BucketStats [Type: _HEAP_LFH_SUBSEGMENT_STATS]
[+0x048] SubsegmentCreationLock : 0x0 [Type: unsigned __int64]
[+0x080] Buckets [Type: _HEAP_LFH_BUCKET * [129]]
BackendCtx
:指向 LFH 堆的后端堆分配器,即_SEGMENT_HEAP.SegContexts (_HEAP_SEG_CONTEXT)
。与_HEAP_VS_CONTEXT.BackendCtx
不同的是这个指针没有被加密。Callbacks (_HEAP_SUBALLOCATOR_CALLBACKS)
:用于负责申请释放 LFH SubSegments 所需内存的函数指针集合,函数指针都经过加密:RtlpHpHeapGlobals.HeapKey ^ LFHContext_addr ^ func_ptr
。Allocate
:RtlpHpSegLfhAllocate
Free
:RtlpHpSegLfhVsFree
Commit
:RtlpHpSegLfhVsCommit
Decommit
:RtlpHpSegLfhVsDecommit
ExtendContext
:RtlpHpSegLfhExtendContext
Config (_RTL_HP_LFH_CONFIG)
:用于表示 LFH 管理堆块的属性。MaxBlockSize
:决定多大的堆块适用于 LFH 分配。WitholdPageCrossingBlocks
:是否有跨页块。DisableRandomization
:是否关闭 LFH 分配随机化。
Buckets (_HEAP_LFH_BUCKET )
:Buckets
指针数组,与 V8 区分 Obj 指针和 Smi 相似,这个值通过最低位区分为_HEAP_LFH_BUCKET
结构体和单纯的计数作用。- 如果 LFH 启动,每个
Bucket
存储了对应Size
的_HEAP_LFH_BUCKET
结构体地址。 - 如果 LFH 未启动,每个
Bucket
低 2 字节恒为 0x0001 ,高 2 字节存储了当前Size
堆块的分配次数,每分配一次加 0x21,每释放一次减 1 。
- 如果 LFH 启动,每个
_HEAP_LFH_BUCKET
只有在启用 LFH 时,才会分配 Buckets
及其相关结构。LFH 分配器使用该结构体来管理与 Size
相对应的块。
0:000> dt _HEAP_LFH_BUCKET
ntdll!_HEAP_LFH_BUCKET
+0x000 State : _HEAP_LFH_SUBSEGMENT_OWNER
+0x038 TotalBlockCount : Uint8B
+0x040 TotalSubsegmentCount : Uint8B
+0x048 ReciprocalBlockSize : Uint4B
+0x04c Shift : UChar
+0x04d ContentionCount : UChar
+0x050 AffinityMappingLock : Uint8B
+0x058 ProcAffinityMapping : Ptr64 UChar
+0x060 AffinitySlots : Ptr64 Ptr64 _HEAP_LFH_AFFINITY_SLOT
State (_HEAP_LFH_SUBSEGMENT_OWNER)
:用于记录Buckets
的状态。TotalBlockCount
:这个Bucket
中 LFH Subsegments 中所有的Block
的数量。TotalSubsegmentCount
:这个Bucket
中 LFH Subsegments 的数量。ReciprocalBlockSize
:如果BlockSize
不是 2 的整数次幂,这个值在判断释放的 block 相对于第一个 block 的距离BlockOffset
是否关于BlockSize
对齐时会被用到。判断的方法为看释放的BlockPtr
是否满足((ReciprocalBlockSize * BlockOffset) >> Shift) * BlockSize == BlockOffset
,下面会给出该方法正确性的证明。Shift
:如果BlockSize
不是 2 的整数次幂,这个值为 0x20 ,否则为__builtin_ctz(BlockSize)
。AffinitySlots (_HEAP_LFH_AFFINITY_SLOT)
: 存储了当前Bucket
的 subsegment 管理信息。默认只有一个。
下面证明
BlockOffset
关于BlockSize
对齐当且仅当((ReciprocalBlockSize * BlockOffset) >> Shift) * BlockSize == BlockOffset
:
为了方便表述,不妨设BlockOffset
为 a ( a ≤ 2 32 , a ∈ N ) a \ (a\le 2^{32},a\in \mathbb{N}) a (a≤232,a∈N) ,BlockSize
为 b ( b ≤ a , b ∈ N ) b \ (b\le a,b\in \mathbb{N}) b (b≤a,b∈N) ,Shift
为 32 32 32 则ReciprocalBlockSize
为 ⌈ 2 32 b ⌉ \left \lceil \frac{2^{32}}{b} \right \rceil ⌈b232⌉
则原命题成立等价为如下等式成立 ⌊ a × ⌈ 2 32 b ⌉ 2 32 ⌋ × b = a \left \lfloor \frac{a\times \left \lceil \frac{2^{32}}{b} \right \rceil }{2^{32}} \right \rfloor \times b =a232a×⌈b232⌉ ×b=a 当且仅当 a m o d b = 0 a \mod b =0 amodb=0
令 2 32 = b × k + r ( 0 ≤ r < b , k ∈ N ) 2^{32}=b\times k+r\ (0\le r< b,k\in \mathbb{N}) 232=b×k+r (0≤r<b,k∈N) 则原式等于 ⌊ a × k + a × ⌈ r b ⌉ b × k + r ⌋ × b \left \lfloor \frac{a\times k +a \times \left \lceil \frac{r}{b} \right \rceil }{b\times k+r} \right \rfloor \times b ⌊b×k+ra×k+a×⌈br⌉⌋×b
- 当 a a a , b b b 满足 a m o d b = 0 a \mod b =0 amodb=0 时
当 r = 0 r = 0 r=0 时原式等价为 ⌊ a b ⌋ × b \left \lfloor \frac{a}{b} \right \rfloor \times b ⌊ba⌋×b显然成立。
当 r ≠ 0 r \ne 0 r=0 时,原式等价为 ⌊ a × k + a b × k + r ⌋ × b \left \lfloor \frac{a\times k +a }{b\times k+r} \right \rfloor \times b ⌊b×k+ra×k+a⌋×b 因为 a m o d b = 0 a \mod b =0 amodb=0 ,因此原命题成立等价为如下不等式成立
a b ≤ a × k + a b × k + r < a + b b \frac{a}{b} \le \frac{a\times k +a }{b\times k+r} <\frac{a+b}{b} ba≤b×k+ra×k+a<ba+b
首先显然有如下不等式成立: a × k + a b × k + r > a × k + a b × k + b \frac{a\times k+a}{b \times k+r} >\frac{a\times k+a}{b \times k+b} b×k+ra×k+a>b×k+ba×k+a 由于 k ∈ N k\in \mathbb{N} k∈N ,因此 a × k + a b × k + r > a b \frac{a\times k+a}{b \times k+r}>\frac{a}{b} b×k+ra×k+a>ba 而不等式 a × k + a b × k + r < a + b b \frac{a \times k+a}{b\times k+r}<\frac{a+b}{b} b×k+ra×k+a<ba+b 可以化简为 b × ( 2 32 − a ) + a × r > 0 b\times (2^{32}-a)+a\times r>0 b×(232−a)+a×r>0 显然也成立- 当 a a a , b b b 满足 a m o d b ≠ 0 a \mod b \ne 0 amodb=0 时显然不存在一个数 t ( t ∈ N ) t\ (t \in \mathbb{N}) t (t∈N) 满足 t × b = a t \times b = a t×b=a ,因此原式一定不成立
综上,原命题成立
_HEAP_LFH_SUBSEGMENT_OWNER
0:000> dt _HEAP_LFH_SUBSEGMENT_OWNER
ntdll!_HEAP_LFH_SUBSEGMENT_OWNER
+0x000 IsBucket : Pos 0, 1 Bit
+0x000 Spare0 : Pos 1, 7 Bits
+0x001 BucketIndex : UChar
+0x002 SlotCount : UChar
+0x002 SlotIndex : UChar
+0x003 Spare1 : UChar
+0x008 AvailableSubsegmentCount : Uint8B
+0x010 Lock : Uint8B
+0x018 AvailableSubsegmentList : _LIST_ENTRY
+0x028 FullSubsegmentList : _LIST_ENTRY
IsBucket
:用来区分该结构是在Bucket
上还是在AffinitySlots
上,如果是Bucket
中的State
则该位置 1 。BucketIndex
:当前Bucket
的编号,通常可以利用这个值查找全局数组RtlpBucketBlockSizes
来获取BlockSize
:RtlpBucketBlockSizes[State.BucketIndex]
AvailableSubsegmentCount
:目前可用于分配的的 LFH Subsegments 数量。AvailableSubsegmentList
:指向下一个可用的 LFH subsegment 。FullSubsegmentList
:指向下一个全被使用的 LFH subsegment ,目前没有发现该链表使用的地方。
_HEAP_LFH_AFFINITY_SLOT
0:000> dt _HEAP_LFH_AFFINITY_SLOT
ntdll!_HEAP_LFH_AFFINITY_SLOT
+0x000 State : _HEAP_LFH_SUBSEGMENT_OWNER
+0x038 ActiveSubsegment : _HEAP_LFH_FAST_REF
State
:类似Bucket
中的State
,不过这个主要用来管理 Subsegment ,LfhSubsegment.Owner
通常会指向这个结构。ActiveSubsegment
:指向当前正在使用的 Subsegment 。
_HEAP_LFH_SUBSEGMENT
与 Nt Heap 中的 UserBlock
中的非常相似,但每个块没有 chunk 头。主要通过 Buckets->AffinitySlots
管理。
一旦没有足够的内存,LFH Subsegment 将从 Buckets->State
获取 LFH Subsegment 。首先尝试从 AvailableSubsegmentList
中获取,如果没有可用的子段,将从后端分配器分配一个新的 LFH Subsegment 。
0:000> dt _HEAP_LFH_SUBSEGMENT
ntdll!_HEAP_LFH_SUBSEGMENT
+0x000 ListEntry : _LIST_ENTRY
+0x010 Owner : Ptr64 _HEAP_LFH_SUBSEGMENT_OWNER
+0x010 DelayFree : _HEAP_LFH_SUBSEGMENT_DELAY_FREE
+0x018 CommitLock : Uint8B
+0x020 FreeCount : Uint2B
+0x022 BlockCount : Uint2B
+0x020 InterlockedShort : Int2B
+0x020 InterlockedLong : Int4B
+0x024 FreeHint : Uint2B
+0x026 Location : UChar
+0x027 WitheldBlockCount : UChar
+0x028 BlockOffsets : _HEAP_LFH_SUBSEGMENT_ENCODED_OFFSETS
+0x02c CommitUnitShift : UChar
+0x02d CommitUnitCount : UChar
+0x02e CommitStateOffset : Uint2B
+0x030 BlockBitmap : [1] Uint8B
ListEntry
:指向前(后)一个 LFH Subsegment 。与 VS Subsegment 不同的是这里的指针不加密。Owner (_HEAP_LFH_SUBSEGMENT_OWNER)
:指向管理 LFH Subsegment 的结构体,具体来说是指向对应的AffinitySlots.State
。FreeCount
:LFH Subsegment 中空闲Block
的数量。BlockCount
:LFH Subsegment 中Block
的数量。FreeHint
:释放的Block
中的最小下标。Location
:标记该 LFH Subsegment 所在的位置。- 0:
AvailableSubsegmentList
- 1:
FullSubsegmentList
- 2:表示 FLH Subsegment 不在链表中
- 0:
WitheldBlockCount
:当LfhContext->Config.WitholdPageCrossingBlocks
置 1 即不允许有跨页Block
且Block
不为 2 的整数次幂时这个值用于统计LfhSubsegment
中的跨页块的数量。BlockOffsets (_HEAP_LFH_SUBSEGMENT_ENCODED_OFFSETS)
:被加密为EncodedData = RtlpHpHeapGlobals.LfhKey ^ BlockOffsets ^ (Subsegment >> 12)
。BlockSize
:LFH Subsegment 中每一个 LFH block 的大小。FirstBlockOffset
:第一个Block
相对于 LFH Subsegment 起始地址的偏移。
CommitUnitShift
:1 << CommitUnitShift
为一个CommitUnit
的大小。CommitUnitCount
:LFH Subsegment 中划分的CommitUnit
的数量。CommitStateOffset
:记录每个CommitUnit
状态的uint16_t
数组相对于 LFH Subsegment 的偏移。如果一个跨多个CommitUnit
上的Block
被申请出去则这个Block
所覆盖的CommitUnit
对应的CommitState
会加 1 。BlockBitmap
:每个 LFH 块的状态由该块位图中的 2 个比特表示。- bit 0:is busy bit
- bit 1:unused bytes
Block
:分配器返回给用户的内存。对于已分配的Block
如果UnusedBytes
不为 0 会把Block
在最后 2 字节作为UnusedBytes
,如果UnusedBytes
为 1则将最后 2 字节置为 0x8000(实际上相当于只将最后的 1 字节置为 0x80)。
分配机制
Allocate
- 在
RtlpAllocateHeapInternal
中首先判断Size <= 0x4000 -16
,如果满足条件说明在 LFH 堆的分配范围。 - 在 LFH Context 中找到对应的
Buckets
指针,根据其最低 1 比特是否置位判断 LFH 是否已初始化,如果最低 1 比特未置位说明 LFH 已初始化,直接调用RtlpHpLfhSlotAllocate
进行 LFH 分配。 - 否则将
Buckets
指针的第二个 2 字节加 0x21 然后判断是否满足与 0x1F 大于 0x10 或者大于 0xFF00(和 Nt Heap 判断条件相同),如果满足则调用RtlpHpLfhBucketActivate
初始化。- 调用
RtlpHpSegLfhExtendContext
为Bucket
及其相关结构申请内存。 - 调用
RtlpHpLfhBucketInitialize
函数初始化Bucket
。- 调用
RtlpHpLfhOwnerInitialize
初始化Bucket->State
。- 将
Bucket->State.IsBucket
置 1 。 - 初始化
Bucket->State.BucketIndex
。 - 初始化
Bucket->State.AvailableSubsegmentList
为空链表。 - 初始化
Bucket->State.FullSubsegmentList
为空链表。
- 将
- 通过全局数组
RtlpBucketBlockSizes
获取BlockSize
。 - 判断
BlockSize
是否是 2 的整数次幂。- 如果
BlockSize
不是 2 的整数次幂则初始化Bucket->Shift
为__builtin_ctz(LFHContext->Config.MaxBlockSize) + 18 = 32
,初始化Bucket->ReciprocalBlockSize
为(BlockSizes - 1 + (1i64 << Shift)) / _BlockSizes
。 - 否则初始化
Bucket->Shift
为__builtin_ctz(BlockSizes)
。
- 如果
- 调用
- 初始化
Bucket->AffinitySlots
指向AffinitySlot
数组。然后初始化AffinitySlot
数组中的每一项(实际只有 1 项)指向具体的_HEAP_LFH_AFFINITY_SLOT
结构,更新Bucket->State.SlotCount
并调用RtlpHpLfhOwnerInitialize
函数初始化AffinitySlot->State
。- 初始化
AffinitySlot->State.SlotCount
为该AffinitySlot
在AffinitySlot
数组中的下标。 - 初始化
AffinitySlot->State.BucketIndex
。 - 初始化
AffinitySlot->State.AvailableSubsegmentList
为空链表。 - 初始化
AffinitySlot->State.FullSubsegmentList
为空链表。
- 初始化
LfhContext->Buckets[BucketIndex]
指向初始化的Bucket
。
- 调用
- 接下来会调用 LFH 堆分配的核心函数
RtlpHpLfhSlotAllocate
。- 根据
AffinitySlot->State.AvailableSubsegmentCount
是否为 0 判断是否有空闲的LfhSubsegment
。如果有直接进行后续分配操作,否则需要先创建LfhSubsegment
。 - 如果
AffinitySlot->State.AvailableSubsegmentList
为空则调用RtlpHpLfhSubsegmentCreate
函数创建LfhSubsegment
。- 依次调用
RtlpHpSegLfhAllocate
和RtlpHpSegLfhVsCommit
为LfhSubsegment
申请内存。 - 调用
RtlpHpLfhSubsegmentInitialize
函数初始化LfhSubsegment
。- 根据
BlockSize
和SubsegmentSize
计算出BlockCount
和BlockOffset (BlockSize,FirstBlockOffset)
,然后初始化LfhSubsegment->FreeCount
和LfhSubsegment->BlockCount
为BlockCount
以及LfhSubsegment->BlockOffsets
,注意这里BlockOffsets
是被加密的。 - 根据
CommitUnitSize
和SubsegmentSize
计算出LfhSubsegment->CommitUnitCount
,LfhSubsegment->CommitUnitShift
和LfhSubsegment->CommitStateOffset
。 - 初始化
LfhSubsegment->Location
为 2 。 - 初始化
LfhSubsegment->BlockBitmap
为全 0 ,关于 8 字节对齐的位置全部位置 1 。 - 如果
LfhContext->Config.WitholdPageCrossingBlocks
即不允许有跨页Block
且LfhSubsegment
的大小超过 0x1000 则需要进行一些调整来避免跨页的Block
,不过这个选项默认不开启。 - 最后初始化一下对应的
RtlpLowFragHeapRandomDat
。
- 根据
- 更新
Bucket->TotalSubsegmentCount++
和Bucket->TotalBlockCount += LfhSubsegment->BlockCount
。
- 依次调用
- 初始化
LfhSubsegment->Owner
指向对应的AffinitySlot->State
。 - 设置
LfhSubsegment->Location
为 0 ,然后将新创建的LfhSubsegment
从Blink
加入到AffinitySlot->State.AvailableSubsegmentList
中,加入时有一个双向链表检查。 - 如果
AffinitySlot->State.AvailableSubsegmentCount > 8
会从AffinitySlot->State.AvailableSubsegmentList
的Flink
取出一个LfhSubsegment
并将其Location
置为 2 ,Owner
值为 NULL 。 - 因为此时
AffinitySlot->State.AvailableSubsegmentList
不为空,因此会尝试从其Flink
找到一个LfhSubsegment
用作内存分配,更新LfhSubsegment->FreeCount--
。 - 通过
BucketIndex
查询全局数组RtlpSearchWidth
得到随机范围SearchWith
。 - 判断
LFHContext->Config.DisableRandomization
是否置位来决定RandomOffset
是否为 0(不随机)。因为随机化默认开启,因此DisableRandomization
不置位,会通过TEB->HeapData
在RtlpLowFragHeapRandomData
数组中选取一个随机值作为RandomOffset
并更新TEB->HeapData
。 - 从
LfhSubsegment->BlockBitmap
中LfhSubsegment->FreeHint
所在的BitMap
开始循环遍历BlockBitmap
,直到找到一个有空闲块的BitMap
(busy
位不为 1)。特别的,如果LfhSubsegment->BlockCount * 2 < 64
(这里乘 2 是因为 2 个 比特对应一个Block
的状态),那么BlockBitmap
中不足一个BitMap
(这里定义一个BitMap
为一个 8 字节长度数据),因此该BitMap
一定包含空闲块且需要更新SearchWith = min(SearchWith , LfhSubsegment->BlockCount * 2)
。 - 初始化
RandomOffset = ((SearchWidth * RandomOffset) >> 7) & 0x1FFFFFE
。 - 设置
SearchMask
为 0x5555555555555555 ,如果查找范围SearchWidth
小于 64 时为了确保一定能找到空闲 chunk 需要在BitMap
中用找到第一个空闲Block
的位置FirstOffset
,然后SearchWidthMask
设置为(((1i64 << SearchWidth) - 1) << FirstOffset) & 0x5555555555555555i64
确保一定能覆盖到空闲 chunk 。另外还会将RandomOffset
加上FirstOffset
。 - 在
BitMap
的RandomOffset
偏移处开始,SearchWidthMask
范围内,找到第一个空闲Block
的下标FreeChunkOffset
。 - 将
FreeChunkOffset
加上RandomOffset
并关于 64 取模得到在BitMap
中的真实偏移。 - 更新
BitMap
,在要取出的Block
的对应位置置位。 - 计算空闲
Block
在整个LfhSubsegment
中的下标FreeBlockIndex
。 - 如果
Block
横跨多个CommitUnit
则将这些CommitUnit
对应的CommitState
都加 1 。 - 如果
UnusedBytes
不为 0 会把Block
在最后 2 字节作为UnusedBytes
,如果UnusedBytes
为 1则将最后 2 字节置为 0x8000(实际上相当于只将最后的 1 字节置为 0x80)。最后返回找到的Block
。
- 根据
Free
- 在
RtlpFreeHeapInternal
函数中根据(HeapPageRangeDesctiptor->RangeFlags & 0xC) == 8
判断释放的Block
属于 LFH 堆,因此调用 LFH 堆释放的核心函数RtlpHpLfhSubsegmentFreeBlock
。 - 计算
BlockPtr
相对于LfhSubsegment
中第一个Block
的偏移BlockOffset = BlockPtr - FirstBlockOffset - LfhSubsegment
,这里的FirstBlockOffset
是LfhSubsegment->BlockOffsets
解密后的高 2 字节。 - 通过
LfhContext->Buckets[RtlpLfhBucketIndexMap[(BlockSize + 15) >> 4]]
找到对应的Bucket
,这里BlockSize
是LfhSubsegment->BlockOffsets
解密后的低 2 字节。 - 判断
BlockOffset
是否能被BlockSize
整除。如果Bucket->ReciprocalBlockSize
非 0 说明BlockSize
不是 2 的整数次幂,需要根据((BlockOffset * ReciprocalBlockSize) >> Shift) * BlockSize
是否等于BlockOffset
来判断。如果Bucket->ReciprocalBlockSize
为 0 说明BlockSize
是 2 的整数次幂,可以通过BlockOffset & ((1 << Shift) - 1)
来判断。 - 更新
LfhSubsegment->FreeHint = min(LfhSubsegment->FreeHint, BlockIndex)
,这里BlockIndex
为释放的Block
在LfhSubsegment
中的下标,是在前面判断判断BlockOffset
是否能被BlockSize
整除的时候计算出的。 - 更新
LFHSubsegment.BlockBitmap
。 - 如果
Block
横跨多个CommitUnit
则将这些CommitUnit
对应的CommitState
都减 1 。 - 更新
LfhSubsegment->FreeCount++
。 - 如果
LfhSubsegment->FreeCount == LfhSubsegment->BlockCount
说明LfhSugsegment
需要从原本所在的链表中取出。如果LfhSubsegment->FreeCount == 1
即原先FreeCount
为 0 则需要将LfhSubsegment
放到LfhSubsegment->Owner->AvailableSubsegmentList
中。否则直接返回。 - 如果没有立即返回则说明需要转移
LfhSubsegment
的位置。首先需要将LfhSubsegment
从原本所在的链表中取出,取出前有双向链表检查。 - 如果
LfhSugsegment
需要插入新的链表中(这里通常为AvailableSubsegmentList
)则从新的链表的Blink
插入,在插入之前有双向链表检查。 - 更新
LfhSugsegment->Location
。 - 如果
LfhSubsegment->Owner->AvailableSubsegmentCount > 8
需要从AvailableSubsegmentList
的Flink
取出一个LfhSubsegment
并将其Location
标记为 2 。 - 如果有从
AvailableSubsegmentList
中取出的LfhSubsegment
并且Location == 2
则需要将其Owner
置为 NULL 。 - 如果
LfhSubsegment->FreeCount != LfhSubsegment->BlockCount
即取出的LfhSubsegment
不完全空闲则将LfhSubsegment
重新放回AvailableSubsegmentList
中,放回过程有双向链表检查。 - 否则需要将
LfhSubsegment
释放。首先要更新Bucket->TotalBlockCount -= LfhSubsegment->BlockCount
以及更新Bucket->TotalSubsegmentCount--
,然后调用RtlpHpSegLfhVsFree
释放LfhSubsegment
。
后端堆
相关数据结构
_HEAP_SEG_CONTEXT
段分配的核心结构,用于管理由段分配器分配的内存,并在堆中记录段分配器的所有信息和结构。
0:002> dx -r1 (*((ntdll!_HEAP_SEG_CONTEXT *)0x23aac050140))
(*((ntdll!_HEAP_SEG_CONTEXT *)0x23aac050140)) [Type: _HEAP_SEG_CONTEXT]
[+0x000] SegmentMask : 0xfffffffffff00000 [Type: unsigned __int64]
[+0x008] UnitShift : 0xc [Type: unsigned char]
[+0x009] PagesPerUnitShift : 0x0 [Type: unsigned char]
[+0x00a] FirstDescriptorIndex : 0x2 [Type: unsigned char]
[+0x00b] CachedCommitSoftShift : 0x7 [Type: unsigned char]
[+0x00c] CachedCommitHighShift : 0x4 [Type: unsigned char]
[+0x00d] Flags [Type: <unnamed-tag>]
[+0x010] MaxAllocationSize : 0x7f000 [Type: unsigned long]
[+0x014] OlpStatsOffset : -160 [Type: short]
[+0x016] MemStatsOffset : -192 [Type: short]
[+0x018] LfhContext : 0x23aac050380 [Type: void *]
[+0x020] VsContext : 0x23aac0502c0 [Type: void *]
[+0x028] EnvHandle [Type: RTL_HP_ENV_HANDLE]
[+0x038] Heap : 0x23aac050000 [Type: void *]
[+0x040] SegmentLock : 0x0 [Type: unsigned __int64]
[+0x048] SegmentListHead [Type: _LIST_ENTRY]
[+0x058] SegmentCount : 0x1 [Type: unsigned __int64]
[+0x060] FreePageRanges [Type: _RTL_RB_TREE]
[+0x070] FreeSegmentListLock : 0x0 [Type: unsigned __int64]
[+0x078] FreeSegmentList [Type: _SINGLE_LIST_ENTRY [2]]
SegmentMask
:用于从BlockPtr
找到PageSegment
:PageSegment = BlockPtr & SegmentMask
。UnitShift
:一个PageDescriptor
维护的内存的大小关于 2 取对数,用于计算BlockPtr
所在Page
对应的PageDescriptor
的下标:Index = BlockPtr >> UnitShift
。PagePerUnitShift
:一个PageDescriptor
维护的内存的的内存页数(即大小除以 0x1000)关于 2 取对数。FirstDescriptorIndex
:第一个PageDescriptor
在SegContext
中的下标。LfhContext
:指向 Segment Heap 的LfhContext
。VsContext
:指向 Segment Heap 的VsContext
。Heap
:指向所属的 Segment Heap 。SegmentListHead
:指向PageSegment
的双向链表。SegmentCount
:PageSegment
的数量。FreePageRanges
:维护空闲的 Subsegment 的红黑树,树的节点为PageSegment.DescArray
中的元素。与 VS 堆的FreeChunkTree
相似。FreeSegmentList
:存放空闲的PageSegment
。
_HEAP_PAGE_SEGMENT
0:003> dt _HEAP_PAGE_SEGMENT 0x23026a00000
ntdll!_HEAP_PAGE_SEGMENT
+0x000 ListEntry : _LIST_ENTRY [ 0x00000230`26970188 - 0x00000230`26970188 ]
+0x010 Signature : 0xe5cd4705`0e7c5890
+0x018 SegmentCommitState : (null)
+0x020 UnusedWatermark : 0 ''
+0x000 DescArray : [256] _HEAP_PAGE_RANGE_DESCRIPTOR
ListEntry
:连接链表中的前后PageSegment
。Signature
:用来检验PageSegment
是否有效,通过PageSegment ^ SegContext ^ RtlpHpHeapGlobals.HeapKey ^ 0xA2E64EADA2E64EAD
计算。DescArray (_HEAP_PAGE_RANGE_DESCRIPTOR)
:数组中的每个元素对应描述PageSegment
中一个内存页的状态。
_HEAP_PAGE_RANGE_DESCRIPTOR
页面描述符指示页面段中每个页面的状态(已分配或已释放)和信息(页面是否为块的开始、块的大小等)。它可以被划分为已分配和释放。释放状态下的页面范围描述符将存储在自由页面范围中,这是一个 rbtree 结构。
_HEAP_PAGE_RANGE_DESCRIPTOR
处于已分配状态时结构如下:
TreeSignature
:PageRangeDescriptor
的签名,值为恒为 0xCCDDCCDD 。只在Block
的开头对应的PageRangeDescriptor
才有。UnusedBytes
:申请的块未使用的部分的大小。RangeFlag
:表示页的状态。- Bit 1:allocted bit
- Bit 2:block header bit
- Bit 3:Commited
- LFH:
RangeFlag & 0xc = 8
- VS:
RangeFlag & 0xc = 0xc
- LFH:
CommitedPageCount
:表示相应页面中提交的页数。key (_HEAP_DESCRIPTOR_KEY)
:存储与PageRangeDescriptor
对应的页面的一些相关信息。0:003> dt _HEAP_DESCRIPTOR_KEY ntdll!_HEAP_DESCRIPTOR_KEY +0x000 Key : Uint4B +0x000 EncodedCommittedPageCount : Pos 0, 16 Bits +0x000 LargePageCost : Pos 16, 8 Bits +0x000 UnitCount : Pos 24, 8 Bits
EncodedCommittedPageCount (2bytes)
:~EncodedCommittedPageCount
是Block
中提交的页面数。只在Block
的开头对应的PageRangeDescriptor
才有。UniCount (1byte)
:- 如果是
Block
的开头对应的PageRangeDescriptor
,那么UniCount
为Block
的大小,用Page
的数量来表示。 - 如果不是
Block
的开头对应的PageRangeDescriptor
,那么UniCount
为Page
在Block
中的偏移。
- 如果是
_HEAP_PAGE_RANGE_DESCRIPTOR
处于已释放状态时结构如下:
TreeNode (_RTL_BALANCED_NODE)
:Left
:指向大小小于当前PageRangeDescriptor
对应Block
的Block
对应的PageRangeDescriptor
。Right
:指向大小大于当前PageRangeDescriptor
对应Block
的Block
对应的PageRangeDescriptor
。ParentValue
:指向父节点,指针最低 1 比特表示是否加密。
Key(_HEAP_DESCRIPTOR_KEY)
:与已分配的Block
对应的PageRangeDescriptor
相同,不过UniCount
为Page
在Block
中的偏移。
分配机制
Allocate
VS 堆,LFH 堆和用户申请内存都有可能从后端堆分配内存,这里选择用户申请内存时的过程进行分析,其余过程基本一致。
- 在
RtlpAllocateHeapInternal
函数中,如果中首先判断Size <= 0x4000 - 16
,不在 LFH 堆的范围内,所以不考虑 FLH 堆的分配。 - 由于
Size > 0x20000
因此不考虑 VS 堆的分配。 - 由于不满足
Size > Heap->SegContexts[1].MaxAllocationSize
即Size > 0x7f0000
,因此不考虑 Large Block 堆的分配。 - 根据
Size
是否小于等于Heap->SegContexts[0].MaxAllocationSize
决定是使用Heap->SegContexts[0]
还是Heap->SegContexts[1]
作为SegContexts
。这两个结构的区别主要在于一个PageDescriptor
维护的内存的大小,Heap->SegContexts[0]
对应的是 0x1000 ,而Heap->SegContexts[1]
对应的是 0x10000 。然后调用核心分配函数RtlpHpSegAlloc
进行内存分配。- 调用
RtlpHpSegPageRangeAllocate
函数获取合适的Block
对应的PageRangeDescriptor
。- 计算
UnitCount
等于PageCount
除以(1 << HeapSegContext->PagesPerUnitShift)
向上取整。 - 尝试从红黑树
FreePageRanges
中找合适的Block
对应的PageRangeDescriptor
。- 如果找到了合适的
Block
就会将该PageRangeDescriptor
从红黑树中取出,然后初始化PageRangeDescriptor->TreeSignature = 0xCCDDCCDD
。 - 如果没有找到合适的
Block
- 首先调用
RtlpHpSegSegmentAllocate
申请一个新的PageSegment
。 - 然后调用
RtlpHpSegSegmentInitialize
初始化新申请的PageSegment
。- 首先找到第一个
PageDescriptor
:FirstDescriptor = (NewPageSegment + sizeof(_HEAP_PAGE_RANGE_DESCRIPTOR) * HeapSegContext->FirstDescriptorIndex)
。 - 设置
FirstDescriptor->UnitOffset = -HeapSegContext->FirstDescriptorIndex
。因为PageDescriptor
是 0x100 个,而FirstDescriptor
是DescArray
数组中的第 3 个,FirstDescriptorIndex = 2
,因此-FirstDescriptorIndex = 0x100 - 2 = 0xfe
,即整个Block
的大小。 - 设置
FirstDescriptor->RangeFlags |= 2u
,即 allocted bit 置位。 - 设置
FirstDescriptor->key.EncodedCommittedPageCount
为 0xffff 。 - 设置
FirstDescriptor->TreeSignature = 0xCCDDCCDD
。 - 设置
PageSegment.DescArray
的UnitOffset
为~FirstDescriptorIndex
,即该PageDescriptor
在整个Block
中的下标。注意此时内存只申请了DescArray
,后面的Page
还没有申请,都是无效地址。
- 首先找到第一个
- 调用
RtlpHpSegHeapAddSegment
函数。- 初始化
PageSegment->Signature = PageSegment ^ RtlpHpHeapGlobals.HeapKey ^ HeapSegContext ^ 0xA2E64EADA2E64EADui64
。 - 将新申请的
PageSegment
从Blink
插入到SegContext->SegmentListHead
中。在插入前有双向链表检查。 - 最后
++HeapSegContext->SegmentCount
。
- 初始化
- 首先调用
- 如果找到了合适的
- 调用
RtlpHpSegPageRangeSplit
从获取的Block
中切下多余的部分。- 如果找到的
Block
大小恰好合适就直接返回 NULL 。 - 对于切下来的
Block
:- 第一个
PageRangeDescriptor
的RangeFlags
的 2 置位(block header bit)。 - 第一个
PageRangeDescriptor
的UnitOffset
设置为该Block
的PageRangeDescriptor
数量。 - 第一个
PageRangeDescriptor
的TreeSignature
设置为 0xCCDDCCDD 。 - 第一个
PageRangeDescriptor
的EncodedCommittedPageCount
设置为该Block
所有的CommittedPageCount
之和取反 。 - 最后一个
PageRangeDescriptor
的UnitOffset
设置为PageRangeDescriptor
在Block
中的下标。
- 第一个
- 对于保留的
Block
:- 第一个
PageRangeDescriptor
的UnitOffset
设置为该Block
的PageRangeDescriptor
数量。 - 第一个
PageRangeDescriptor
的EncodedCommittedPageCount
设置为该Block
所有的CommittedPageCount
之和取反(通过原有的EncodedCommittedPageCount
与切下来的Block
的EncodedCommittedPageCount
计算) 。 - 最后一个
PageRangeDescriptor
的UnitOffset
设置为PageRangeDescriptor
在Block
中的下标。
- 第一个
- 最后返回切下来的
Block
。
- 如果找到的
- 如果存在切下来的多余
Block
就调用RtlpHpSegFreeRangeInsert
将多余部分放回FreePageRanges
中。 - 对于申请到的
Block
,将其中第一个PageRangeDescriptor
的RangeFlags
或上 1(allocted bit)和HIBYTE(Flags) & 0xC
(由参数决定是LfhSubsegment
,VsSubsegment
还是用户申请的内存);将其中最后一个PageRangeDescriptor
的RangeFlags
或上 1(allocted bit)。 - 对于申请到的
Block
,将其中除第一个和最后一个的PageRangeDescriptor
的RangeFlags
或上 1(allocted bit),UnitOffset
设置为该PageRangeDescriptor
在Block
中的下标。 - 最后返回申请到的
Block
对应的第一个PageRangeDescriptor
。
- 计算
- 调用
RtlpHpSegPageRangeCommit
判断申请到的Block
中是否有需要提交的页面,如果有会调用RtlpHpSegMgrCommit ->RtlpHpAllocVA->MmAllocatePoolMemory
来分配页面。并且更新PageRangeDescriptor
中的CommittedPagecount
。 - 最后返回
((_PageRangeDescriptor & HeapSegContext->SegmentMask) + ((_PageRangeDescriptor - (_PageRangeDescriptor & HeapSegContext->SegmentMask)) >> 5 << HeapSegContext->UnitShift))
即对应申请到的内存的起始地址。
- 调用
Free
VS 堆,LFH 堆和用户释放内存都有可能导致后端堆释放内存,这里选择用户释放内存时的过程进行分析,其余过程基本一致。
- 在
RtlpFreeHeapInternal
函数中,如果BlockPtr
等于其所在Block
的开头说明是后端堆分配,因此会调用RtlpHpSegPageRangeShrink
释放内存。- 将要释放的内存中除了第一个和最后一个的
PageRangeDescriptor
的RangeFlags
的 allocted bit 复位 - 将要释放的内存中第一个
PageRangeDescriptor
的RangeFlags
与上 0xF3 ,即将 VS 或 LFH 相关标志位清除。 - 调用
RtlpHpSegPageRangeCoalesce
函数尝试合并前后空闲的Block
。- 如果不是最后一个
Block
(DescriptorIndex + PageRangeDescriptor->UnitOffset < 0x100
)尝试向后合并。首先找到后一个PageRangeDescriptor
,如果后一个PageRangeDescriptor
的RangeFlags
的 allocted bit 位没有置位说明空闲,记为NextPageRangeDescriptor
。 - 如果不是第一个
Block
(DescriptorIndex > SegContext->FirstDescriptorIndex
)尝试向前合并。首先找释放的Block
的第一个PageRangeDescriptor
的前一个PageRangeDescriptor
,然后判断其RangeFlags
的 block header bit 是否置位来确定是否是其所在Block
的第一个PageRangeDescriptor
,如果不是就根据其UnitOffset
找到第一个PageRangeDescriptor
。如果前一个PageRangeDescriptor
的RangeFlags
的 allocted bit 位没有置位说明空闲,记为PrevPageRangeDescriptor
。 - 如果找到了
PrevPageRangeDescriptor
则将其合并到释放的block
中。- 调用
RtlpHpSegFreeRangeRemove
将该PageRangeDescriptor
从FreePageRanges
中取出。 PrevPageRangeDescriptor->UnitOffset += PageRangeDescriptor->UnitOffset
PrevPageRangeDescriptor->Key.CommittedPageCount = ~(~(PrevPageRangeDescriptor->Key.CommittedPageCount) + ~(PageRangeDescriptor->Key.CommittedPageCount))
PageRangeDescriptor->RangeFlags &= (PageRangeDescriptor->UnitOffset <= 1u) - 4
PrevPageRangeDescriptor[(unsigned int)PrevPageRangeDescriptor->UnitOffset - 1].UnitOffset = PrevPageRangeDescriptor->UnitOffset - 1
- 记当前的
PageRangeDescriptor
为PrevPageRangeDescriptor
。
- 调用
PageRangeDescriptor->RangeFlags |= 0x11u
- 如果找到了
NextPageRangeDescriptor
则将其合并到释放的block
中。- 调用
RtlpHpSegFreeRangeRemove
将该PageRangeDescriptor
从FreePageRanges
中取出。 PageRangeDescriptor[(unsigned int)PageRangeDescriptor->UnitOffset - 1].RangeFlags &= ~1u
PageRangeDescriptor->UnitOffset += NextPageRangeDescriptor->UnitOffset
PageRangeDescriptor->Key.CommittedPageCount = ~(~(PageRangeDescriptor->Key.CommittedPageCount) + ~(NextPageRangeDescriptor->Key.CommittedPageCount))
NextPageRangeDescriptor->RangeFlags &= ~2u
PageRangeDescriptor[PageRangeDescriptor->UnitOffset - 1].RangeFlags |= 1u
PageRangeDescriptor[PageRangeDescriptor->UnitOffset - 1].UnitOffset = PageRangeDescriptor->UnitOffset - 1
- 调用
- 最后将合并完的
Block
的第一个PageRangeDescriptor
的RangeFlags
与上 0xEE ,最后一个PageRangeDescriptor
的RangeFlags
的 allocted bit 复位。
- 如果不是最后一个
- 将合并完的
Block
插入到FreePageRanges
中,完成释放。
- 将要释放的内存中除了第一个和最后一个的
LB 堆
相关数据结构
_HEAP_LARGE_ALLOC_DATA
0:000> dt _HEAP_LARGE_ALLOC_DATA
ntdll!_HEAP_LARGE_ALLOC_DATA
+0x000 TreeNode : _RTL_BALANCED_NODE
+0x018 VirtualAddress : Uint8B
+0x018 UnusedBytes : Pos 0, 16 Bits
+0x020 ExtraPresent : Pos 0, 1 Bit
+0x020 GuardPageCount : Pos 1, 1 Bit
+0x020 GuardPageAlignment : Pos 2, 6 Bits
+0x020 Spare : Pos 8, 4 Bits
+0x020 AllocatedPages : Pos 12, 52 Bits
TreeNode
:红黑树_SEGMENT_HEAP.LargeAllocMetadata
上的节点。Left
:指向一个VirtualAddress
小于当前节点的节点。Right
:指向一个VirtualAddress
大于当前节点的节点。ParentValue
:指向父节点。最低 1 比特决定是否加密。
VirtualAddress
:LargeBlock
的地址,低 16 比特为UnusedBytes
。AllocatedPages
:分配的内存页数量。
分配机制
Allocate
- 在
RtlpAllocateHeapInternal
函数中,由于Size > Heap->SegContexts[1].MaxAllocationSize
即Size > 0x7f0000
则调用 LB 堆核心函数RtlpHpLargeAlloc
分配内存。- 调用
RtlpHpMetadataAlloc
函数为LargeAllocData
结构分配内存。 - 计算
ReservedSize
,通常是Size + 0x1000
。 - 调用
RtlpHpAllocVA
函数分配一段虚拟内存空间,注意此时还没有分配物理页。 - 调用
RtlpHpAllocVA
函数分配实际内存。 - 初始化
LargeAllocData
结构体。 - 将
LargeAllocData
插入到LargeAllocMetadata
中。 - 更新
Heap->LargeReservedPages
和Heap->LargeCommittedPages
。 - 将分配的内存的起始地址返回。
- 调用
Free
- 在
RtlpAllocateHeapInternal
函数中,如果BlockPtr
最低 16 比特为 0 则判定为 Large Block 堆分配,调用 LB 堆的核心释放函数RtlpHpLargeFree
。- 根据
BlockPtr
在LargeAllocMetadata
中找到对应的LargeAllocData
。 - 调用
RtlRbRemoveNode
函数将该LargeAllocData
从LargeAllocMetadata
中取出。 - 调用
RtlpHpFreeVA
函数把BlockPtr
指向的内存释放掉。 - 更新
Heap->LargeReservedPages
和Heap->LargeCommittedPages
。 - 调用
RtlpHpMetadataFree
释放对应的LargeAllocData
。
- 根据
版权声明:本文标题:windows pwn 基础知识 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.freenas.com.cn/jishu/1726377234h948167.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论