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-Win32ret-sync-release-windbg-x64 两个插件。

  • 把下载好的 sync.dllsync32.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.dllstackoverflow.exe 并且都运行了 ret_sync 插件,因此这里可以看到 ntdll.dllstackoverflow.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 字节长度的内存中的值。edeweb 同理,只是修改内存长度有区别。

  • 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\nputs 函数在输出完字符串后也会在后面添加 \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 得到下一个模块的起始位置,以一个 VirtualAddressSizeOfBlock 均为 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;

其中 OriginalFirstThunkFirstThunk 分别指向两个由 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 的导出序号
  • AddressOfDataIMAGE_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.dllucrtbase.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:指向当前线程的异常处理器链表的头部。当线程发生异常时,系统会将异常处理器添加到该链表中,以便进行异常处理。
  • StackBaseStackLimit:分别指向线程栈的起始地址和结束地址。这是我们我们泄露栈基址的一个途径。
  • 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 作为代理函数来调用用户定义的处理函数。用户定义的 FilterFuncHandlerFunc 保存在 SCOPETABLE 中(实际调试的 SCOPETABLE 可能是使用了 _EH3_SCOPETABLE_RECORD 因此和前面的 _EH4_SCOPETABLE_RECORD 定义有所不同)。
通过分析汇编可知,MSC 对用户定义的 __try 块进行了编号,每个 __try 的编号为其在 SCOPETABLE 中对应的 SCOPETABLE_RECORD 的下标,对于不在 __try 块的情况编号为 -2(0xFFFFFE)。当代码执行到某个 __try 块中时,会先将栈中的 CPPEH_RECORDTryLevel 更新为当前所在 __try 块的编号。另外, SCOPETABLE 中的 SCOPETABLE_RECORDEnclosingLevel 记录了 __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 进一步完成栈回滚。(这里非常重要,如果有恢复 espold_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_REGISTRATIONnext 为 0xFFFFFFFF ,exceptionhandlerntdll!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 且 HandlerRtlpFinalExceptionHandler
  • 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 可以泄露的地址:

  1. 栈基址:偏移 0x8 的 StackBase 可以泄露栈基址,即栈底地址。
  2. 栈顶地址:偏移 0x10 的 StackLimit 可以泄露栈顶地址。

windows 异常处理

  1. 产生硬件异常通过 IDT 调用异常处理例程, 产生软件异常通过 API 的层层调用产地异常信息。而异常又由于发生位置不同,分为内核异常和用户态异常,二者最后都会靠 kiDispathException 函数来进行异常分发;
  2. 当内核产生异常时,程序处理流程进入到 KiDispatchException 函数,在该函数内备份当前线程 R3 的 TrapFrame(即栈帧的基址)。异常处理首先判断这是否是第一次异常,判断是否存在内核调试器,如果有内核调试器,则把当前的异常信息发送给内核调试器;如果没有内核调试器或者内核调试器没有处该异常 , 则进入步骤 3 ,调用 RtlDispatchException
  3. 内核异常进入 RtlDispatchException 函 数, 如果 RtlDispatchException 函数没有处理该异常,那么将再次尝试将异常发送到内核调试器,如果此时内核调试器仍然不存在或者没有处理该异常,那么此时系统会直接蓝屏;
  4. 如果是用户态异常则经过 KiDispatchException 进行用户态异常分发和处理。如果是第一次分发异常,则调用 DbgKForwardException 将异常分发到内核调试器;如果内核调试器不存在或没有处理异常,则尝试将异常分发给用户态调试器;如果异常被处理,则进入步骤 10 ;如果用户态调试器不存在或未处理异常,则检测是否是第一次处理异常,如果是第一次处理异常则进入第 5 步中的异常数据准备;
  5. 准备一个返回 ntdll!KiUserExceptionDispatcher 函数的应用层调用栈,结束本次 KiDispatchException 函数的运行,调用 KiServiceExit 返回用户层。此时函数栈帧是 ntdll!KiUserExceptionDispatcher 的执行环境,用户态线程从执行 ntdll!KiUserExceptionDispatcher 开始执行。该函数调用 ntdll!RtlDispatchException 进行异常的分发,进入第 6 步
  6. 通过 RtlCallVectoredExceptionHandlers 遍历 VEH 链表尝试查找异常处理函数;如果 VEH 未处理异常。则从 fs[0] 读取 ExceptionList 并开始执行 SEH 函数处理,进入步骤 7
  7. 如果 SEH 没有处理函数处理该异常,则检查用户是否通过 SetUnhandledExceptionFilter 函数注册过进程的异常处理函数,如果用户注册过异常处理函数,调用该异常处理函数,如果异常没有被成功处理或没有自定义的异常处理函数,则进入步骤 3
  8. 如果最后仍没有处理该异常,便会主动调用 NtRaiseException 将该异常重新跑出来,但是此时不是第一次分发,此时 NtRaiseException 流程重新调用了 ntdll!KiDispatchException ,并再次进入用户态异常的处理分支,进入步骤 9
  9. 第二次进入用户态异常处理时,不会再尝试发送到内核调试器,也不会再进行异常分发,而是直接尝试发送到用户态体异常调试器,如果最后异常仍未被处理则进入步骤 11
  10. 异常被处理,调用 NtContine ,将之前保存的 TrapFrame 还原,程序继续从异常处正常运行;
  11. 异常不能被处理,系统调用 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 函数创建堆后,可以使用其他堆相关的函数(如 HeapAllocHeapFree 等)来分配和释放内存块。堆对象可以在不再需要时使用 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);
  • 作用:释放通过动态内存分配函数(如 malloccallocrealloc 等)分配的内存块。
  • 参数:
    • _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 ,存放着 FlinkBlink ,将 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 后对应的 FrontEndHeapUsageDataSegmentInfoArrays 的下标。
  • 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 字段为 0x0034Flags 字段为 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 表示 freed
  • UnusedBytes (&0x3f)始终为 0
  • Flink 指向的是下一个 freed chunk 或 FreeList
  • Blink 指向的是上一个 freed chunk 或 FreeList

在 free 完一块 chunk 后,会将该 chunk 放到 FreeLists 中,并会按照大小决定插在 Freelists 中的位置。

VirtualAlloc Chunk

VirtualAlloc Chunk 的 chunk head 为 _HEAP_VIRTUAL_ALLOC_ENTRY

  • FlinkBlink:分别指向前⼀个和后⼀个 mmap 出来的 chunk(不管是 in use 还是 freed)
  • Size:unused size,而且没有右移
  • Unusedbytes:恒为 4,用来判断 VirtualAlloc Chunk。(注意,_HEAP.Encoding 对应 Unusedbytes 的位置通常为 0 ,因此很多地方在未解密 _HEAP_ENTRY 的时候直接判断 Unusedbytes
BlocksIndex (_HEAP_LIST_LOOKUP)

_HEAPBlocksIndex 指向一个类型为 _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 将上一个 BlocksIndexArraySize 作为 BaseIndex
  • ListHead (Ptr64 _LIST_ENTRY):指向 _HEAPFreeList
  • ListsInUseUlong (Ptr64 Uint4B):用在判断 ListHint 中是否有适合大小的 chunk ,是一个 bitmap 。
  • ListHint (Ptr64 Ptr64 _LIST_ENTRY):十分重要,用来指向对应大小的 chunk array ,其目的就在于更快速找到适合大小的 chunk ,0x10 大小为一个间隔。

ListInUseUlongListHint 组成了一个位图选择,能更快速的找到合适大小的堆块。并且 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 。(这个操作实际是在后面进行的,不过为了方便理解写在这里)
  • 遍历 BlocksIndex 链表,找到第一个 ArraySize 大于 SizeBlocksIndex ,然后找到对应的 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),再进行合并。
  • 合并完之后更新 SizePreviousSize ,判断一下 Size 较大的情况,然后把合并好的 chunk 插入到 ListHint 中;插入时也会对 FreeList 进行检查(但是此检查不会触发 abort ,原因在于没有做 unlink 写入)。

LFH 堆

当同一个大小的堆块分配次数过多的时候,除了从后端堆分配所需堆块外,还会额外分配一块很大的内存供前端堆使用,之后再次分配该大小的堆块的时候会从前端堆分配。

相关数据结构

FrontEndHeap(_LFH_HEAP)

_LFH_HEAP 是前端堆管理的核心结构,可以通过 _HEAPFrontEndHeap 成员指针访问。

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_HEAPBuckets 是一个长度为 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_HEAPSegmentInfoArray 是一个长度为 129 的 _HEAP_LOCAL_SEGMENT_INFO 结构体指针数组。

_HEAP_LOCAL_SEGMENT_INFO 主要成员解释如下:

  • LocalData (_HEAP_LOCAL_DATA):指向 _LFH_HEAP->LocalData ,方便从 SegmentInfo 找回 _LFH_HEAP
  • BucketIndexbuckets 中对应位置的 SizeIndex
  • ActiveSubsegment (_HEAP_SUBSEGMENT):指向当前分配 chunk 使用的 SubSegmentSubSegment 用于管理 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

  • 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
        • BusyBitmapRandomOffset 偏移处开始,SearchWidthMask 范围内,找到第一个空闲 chunk 的下标 FreeChunkOffset
        • FreeChunkOffset 加上 RandomOffset 并关于 64 取模得到在 BusyBitmap 中的真实偏移。
        • 更新 BusyBitmap ,在要取出的 chunk 对应位置置位。
        • 计算并返回空闲 chunk 在整个 UserBlock 中的下标。
      • 缓存这次查找到空闲 chunk 的下标到 ActiveSubsegment->AggregateExchg 中。
      • 通过 UserBlocks->EncodedOffsets 计算出空闲 chunk 的具体位置。
      • 通过判断 UnusedBytes 记录未使用大小的位置(&0x3F)是否为 0 来检查是不是已释放的堆块,如果是说明出错。
      • 设置 chunk 的 UnusedBytesUnusedBytes >= 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 函数初始化 HeapSubsegmentUserBlock
      • 初始化 UserBlock 中的每个 chunk 的 SubSegmentCodePreviousSizeUnusedBytes
      • 初始化 UserBlockSubSegmentBusyBitmapBitmapDataEncodedOffsets 等。
      • 初始化 HeapSubsegmentBlockSizeBlockCountLocalInfoSizeIndexUserBlocks 等。
    • 将创建的 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 决定红黑树的指针是否加密(默认不加密)。加密方法是当前节点的指针异或当前节点的地址,对于 RootEncodedRoot = 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
    • AllocateRtlpHpSegVsAllocate
    • FreeRtlpHpSegLfhVsFree
    • CommitRtlpHpSegLfhVsCommit
    • DecommitRtlpHpSegLfhVsDecommit
    • 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
    • 依次调用 RtlpHpSegVsAllocateRtlpHpSegLfhVsCommit 使用 Segment Allocation 分配一个新的 VSSubsegment
    • 初始化 VSSubsegment->Size 为整个VSSubsegment 大小减去 0x30 的头部然后右移 4 位 。
    • 初始化 VSSubsegment->Signature = VSSubsegment->Size ^ 0x2BED
    • 初始化 VSSubsegment 中的 chunk。 VSSubsegment 初始时只有一个 chunk,这里要将 chunk 头清零,然后设置 UnsafeSizeVSSubsegment->Size 并加密 chunk 头部。
    • 检查 VSContext->SubsegmentList.Blink.Flink = VSContext->SubsegmentList ,如果检查通过则将新创建的 VSSubsegmentSubsegmentList.Blink 插入到 SubsegmentList 链表中。注意这里涉及到的指针都是加密的。
    • 将新申请的 VSSubsegment 中的 chunk 插入到 VSContext->FreeChunkTree 中然后重新在红黑树中搜索合适的 chunk 。
  • 通过查找到的 chunk 找到 VSSubsegment 然后校验 (VSSubsegment->Signature ^ VSSubsegment->Size ^ 0x2BED) & 0x7FFF == 0
  • 调用 RtlpHpVsChunkSplit 将 chunk 从红黑树中取出并切掉多余的 chunk ,然后将多余的 chunk 插入到 VSContext->FreeChunkTree 中。在这一过程中初始化了申请的 chunk 和切下来的 chunk 的 UnsafeSizeAllocatedEncodedSegmentPageOffset 和切下来的 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 == 0xA2E64EADA2E64EADHeapSegContextsHeap->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 并校验这个 VSSubsegmentSignature ,以此来校验 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 合并,需要更新更新 FinalChunkUnsafeSize 和后一个 chunk (如果存在)的 UnsafePrevSize
    • 如果合并完之后整个 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
    • AllocateRtlpHpSegLfhAllocate
    • FreeRtlpHpSegLfhVsFree
    • CommitRtlpHpSegLfhVsCommit
    • DecommitRtlpHpSegLfhVsDecommit
    • ExtendContextRtlpHpSegLfhExtendContext
  • 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 。
_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 (a232,aN)BlockSize b   ( b ≤ a , b ∈ N ) b \ (b\le a,b\in \mathbb{N}) b (ba,bN)Shift 32 32 32ReciprocalBlockSize ⌈ 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 =a 232a×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 (0r<b,kN) 则原式等于 ⌊ 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} bab×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} kN ,因此 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×(232a)+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 (tN) 满足 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 来获取 BlockSizeRtlpBucketBlockSizes[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 不在链表中
  • WitheldBlockCount:当 LfhContext->Config.WitholdPageCrossingBlocks 置 1 即不允许有跨页 BlockBlock 不为 2 的整数次幂时这个值用于统计 LfhSubsegment 中的跨页块的数量。
  • BlockOffsets (_HEAP_LFH_SUBSEGMENT_ENCODED_OFFSETS):被加密为 EncodedData = RtlpHpHeapGlobals.LfhKey ^ BlockOffsets ^ (Subsegment >> 12)
    • BlockSize:LFH Subsegment 中每一个 LFH block 的大小。
    • FirstBlockOffset:第一个 Block 相对于 LFH Subsegment 起始地址的偏移。
  • CommitUnitShift1 << 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 初始化。
    • 调用 RtlpHpSegLfhExtendContextBucket 及其相关结构申请内存。
    • 调用 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 为该 AffinitySlotAffinitySlot 数组中的下标。
      • 初始化 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
      • 依次调用 RtlpHpSegLfhAllocateRtlpHpSegLfhVsCommitLfhSubsegment 申请内存。
      • 调用 RtlpHpLfhSubsegmentInitialize 函数初始化 LfhSubsegment
        • 根据 BlockSizeSubsegmentSize 计算出 BlockCountBlockOffset (BlockSize,FirstBlockOffset) ,然后初始化 LfhSubsegment->FreeCountLfhSubsegment->BlockCountBlockCount 以及 LfhSubsegment->BlockOffsets ,注意这里 BlockOffsets 是被加密的。
        • 根据 CommitUnitSizeSubsegmentSize 计算出 LfhSubsegment->CommitUnitCountLfhSubsegment->CommitUnitShiftLfhSubsegment->CommitStateOffset
        • 初始化 LfhSubsegment->Location 为 2 。
        • 初始化 LfhSubsegment->BlockBitmap 为全 0 ,关于 8 字节对齐的位置全部位置 1 。
        • 如果 LfhContext->Config.WitholdPageCrossingBlocks 即不允许有跨页 BlockLfhSubsegment 的大小超过 0x1000 则需要进行一些调整来避免跨页的 Block ,不过这个选项默认不开启。
        • 最后初始化一下对应的 RtlpLowFragHeapRandomDat
      • 更新 Bucket->TotalSubsegmentCount++Bucket->TotalBlockCount += LfhSubsegment->BlockCount
    • 初始化 LfhSubsegment->Owner 指向对应的 AffinitySlot->State
    • 设置 LfhSubsegment->Location 为 0 ,然后将新创建的 LfhSubsegmentBlink 加入到 AffinitySlot->State.AvailableSubsegmentList 中,加入时有一个双向链表检查。
    • 如果 AffinitySlot->State.AvailableSubsegmentCount > 8 会从 AffinitySlot->State.AvailableSubsegmentListFlink 取出一个 LfhSubsegment 并将其 Location 置为 2 ,Owner 值为 NULL 。
    • 因为此时 AffinitySlot->State.AvailableSubsegmentList 不为空,因此会尝试从其 Flink 找到一个 LfhSubsegment 用作内存分配,更新 LfhSubsegment->FreeCount--
    • 通过 BucketIndex 查询全局数组 RtlpSearchWidth 得到随机范围 SearchWith
    • 判断 LFHContext->Config.DisableRandomization 是否置位来决定 RandomOffset 是否为 0(不随机)。因为随机化默认开启,因此 DisableRandomization 不置位,会通过 TEB->HeapDataRtlpLowFragHeapRandomData 数组中选取一个随机值作为 RandomOffset 并更新 TEB->HeapData
    • LfhSubsegment->BlockBitmapLfhSubsegment->FreeHint 所在的 BitMap 开始循环遍历 BlockBitmap ,直到找到一个有空闲块的 BitMapbusy 位不为 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
    • BitMapRandomOffset 偏移处开始,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 ,这里的 FirstBlockOffsetLfhSubsegment->BlockOffsets 解密后的高 2 字节。
  • 通过 LfhContext->Buckets[RtlpLfhBucketIndexMap[(BlockSize + 15) >> 4]] 找到对应的 Bucket ,这里 BlockSizeLfhSubsegment->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 为释放的 BlockLfhSubsegment 中的下标,是在前面判断判断 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 需要从 AvailableSubsegmentListFlink 取出一个 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 找到 PageSegmentPageSegment = BlockPtr & SegmentMask
  • UnitShift:一个 PageDescriptor 维护的内存的大小关于 2 取对数,用于计算 BlockPtr 所在 Page 对应的 PageDescriptor 的下标:Index = BlockPtr >> UnitShift
  • PagePerUnitShift:一个 PageDescriptor 维护的内存的的内存页数(即大小除以 0x1000)关于 2 取对数。
  • FirstDescriptorIndex:第一个 PageDescriptorSegContext 中的下标。
  • LfhContext:指向 Segment Heap 的 LfhContext
  • VsContext:指向 Segment Heap 的 VsContext
  • Heap:指向所属的 Segment Heap 。
  • SegmentListHead:指向 PageSegment 的双向链表。
  • SegmentCountPageSegment 的数量。
  • 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 处于已分配状态时结构如下:

  • TreeSignaturePageRangeDescriptor 的签名,值为恒为 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
  • 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)~EncodedCommittedPageCountBlock 中提交的页面数。只在 Block 的开头对应的 PageRangeDescriptor 才有。
    • UniCount (1byte)
      • 如果是 Block 的开头对应的 PageRangeDescriptor ,那么 UniCount Block 的大小,用 Page 的数量来表示。
      • 如果不是 Block 的开头对应的 PageRangeDescriptor ,那么 UniCount PageBlock 中的偏移。

_HEAP_PAGE_RANGE_DESCRIPTOR 处于已释放状态时结构如下:

  • TreeNode (_RTL_BALANCED_NODE)
    • Left:指向大小小于当前 PageRangeDescriptor 对应 BlockBlock 对应的 PageRangeDescriptor
    • Right:指向大小大于当前 PageRangeDescriptor 对应 BlockBlock 对应的 PageRangeDescriptor
    • ParentValue:指向父节点,指针最低 1 比特表示是否加密。
  • Key(_HEAP_DESCRIPTOR_KEY):与已分配的 Block 对应的 PageRangeDescriptor 相同,不过 UniCountPageBlock 中的偏移。
分配机制
Allocate

VS 堆,LFH 堆和用户申请内存都有可能从后端堆分配内存,这里选择用户申请内存时的过程进行分析,其余过程基本一致。

  • RtlpAllocateHeapInternal 函数中,如果中首先判断 Size <= 0x4000 - 16 ,不在 LFH 堆的范围内,所以不考虑 FLH 堆的分配。
  • 由于 Size > 0x20000 因此不考虑 VS 堆的分配。
  • 由于不满足 Size > Heap->SegContexts[1].MaxAllocationSizeSize > 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
            • 首先找到第一个 PageDescriptorFirstDescriptor = (NewPageSegment + sizeof(_HEAP_PAGE_RANGE_DESCRIPTOR) * HeapSegContext->FirstDescriptorIndex)
            • 设置 FirstDescriptor->UnitOffset = -HeapSegContext->FirstDescriptorIndex。因为 PageDescriptor 是 0x100 个,而 FirstDescriptorDescArray 数组中的第 3 个,FirstDescriptorIndex = 2,因此 -FirstDescriptorIndex = 0x100 - 2 = 0xfe,即整个 Block 的大小。
            • 设置 FirstDescriptor->RangeFlags |= 2u ,即 allocted bit 置位。
            • 设置 FirstDescriptor->key.EncodedCommittedPageCount 为 0xffff 。
            • 设置 FirstDescriptor->TreeSignature = 0xCCDDCCDD
            • 设置 PageSegment.DescArrayUnitOffset ~FirstDescriptorIndex ,即该 PageDescriptor 在整个 Block 中的下标。注意此时内存只申请了 DescArray ,后面的 Page 还没有申请,都是无效地址。
          • 调用 RtlpHpSegHeapAddSegment 函数。
            • 初始化 PageSegment->Signature = PageSegment ^ RtlpHpHeapGlobals.HeapKey ^ HeapSegContext ^ 0xA2E64EADA2E64EADui64
            • 将新申请的 PageSegmentBlink 插入到 SegContext->SegmentListHead 中。在插入前有双向链表检查。
            • 最后 ++HeapSegContext->SegmentCount
      • 调用 RtlpHpSegPageRangeSplit 从获取的 Block 中切下多余的部分。
        • 如果找到的 Block 大小恰好合适就直接返回 NULL 。
        • 对于切下来的 Block
          • 第一个 PageRangeDescriptorRangeFlags 的 2 置位(block header bit)。
          • 第一个 PageRangeDescriptorUnitOffset 设置为该 BlockPageRangeDescriptor 数量。
          • 第一个 PageRangeDescriptorTreeSignature 设置为 0xCCDDCCDD 。
          • 第一个 PageRangeDescriptorEncodedCommittedPageCount 设置为该 Block 所有的 CommittedPageCount 之和取反 。
          • 最后一个 PageRangeDescriptorUnitOffset 设置为 PageRangeDescriptorBlock 中的下标。
        • 对于保留的 Block
          • 第一个 PageRangeDescriptorUnitOffset 设置为该 BlockPageRangeDescriptor 数量。
          • 第一个 PageRangeDescriptorEncodedCommittedPageCount 设置为该 Block 所有的 CommittedPageCount 之和取反(通过原有的 EncodedCommittedPageCount 与切下来的 BlockEncodedCommittedPageCount 计算) 。
          • 最后一个 PageRangeDescriptorUnitOffset 设置为 PageRangeDescriptorBlock 中的下标。
        • 最后返回切下来的 Block
      • 如果存在切下来的多余 Block 就调用 RtlpHpSegFreeRangeInsert 将多余部分放回 FreePageRanges 中。
      • 对于申请到的 Block ,将其中第一个 PageRangeDescriptorRangeFlags 或上 1(allocted bit)和 HIBYTE(Flags) & 0xC(由参数决定是 LfhSubsegmentVsSubsegment 还是用户申请的内存);将其中最后一个 PageRangeDescriptorRangeFlags 或上 1(allocted bit)。
      • 对于申请到的 Block ,将其中除第一个和最后一个的 PageRangeDescriptorRangeFlags 或上 1(allocted bit),UnitOffset 设置为该 PageRangeDescriptorBlock 中的下标。
      • 最后返回申请到的 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 释放内存。
    • 将要释放的内存中除了第一个和最后一个的 PageRangeDescriptorRangeFlags 的 allocted bit 复位
    • 将要释放的内存中第一个 PageRangeDescriptorRangeFlags 与上 0xF3 ,即将 VS 或 LFH 相关标志位清除。
    • 调用 RtlpHpSegPageRangeCoalesce 函数尝试合并前后空闲的 Block
      • 如果不是最后一个 BlockDescriptorIndex + PageRangeDescriptor->UnitOffset < 0x100)尝试向后合并。首先找到后一个 PageRangeDescriptor ,如果后一个 PageRangeDescriptorRangeFlags 的 allocted bit 位没有置位说明空闲,记为 NextPageRangeDescriptor
      • 如果不是第一个 BlockDescriptorIndex > SegContext->FirstDescriptorIndex)尝试向前合并。首先找释放的 Block 的第一个 PageRangeDescriptor 的前一个 PageRangeDescriptor ,然后判断其 RangeFlags 的 block header bit 是否置位来确定是否是其所在 Block 的第一个 PageRangeDescriptor ,如果不是就根据其 UnitOffset 找到第一个 PageRangeDescriptor 。如果前一个 PageRangeDescriptorRangeFlags 的 allocted bit 位没有置位说明空闲,记为 PrevPageRangeDescriptor
      • 如果找到了 PrevPageRangeDescriptor 则将其合并到释放的 block 中。
        • 调用 RtlpHpSegFreeRangeRemove 将该 PageRangeDescriptorFreePageRanges 中取出。
        • 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
        • 记当前的 PageRangeDescriptorPrevPageRangeDescriptor
      • PageRangeDescriptor->RangeFlags |= 0x11u
      • 如果找到了 NextPageRangeDescriptor 则将其合并到释放的 block 中。
        • 调用 RtlpHpSegFreeRangeRemove 将该 PageRangeDescriptorFreePageRanges 中取出。
        • 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 的第一个 PageRangeDescriptorRangeFlags 与上 0xEE ,最后一个 PageRangeDescriptorRangeFlags 的 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 比特决定是否加密。
  • VirtualAddressLargeBlock 的地址,低 16 比特为 UnusedBytes
  • AllocatedPages:分配的内存页数量。
分配机制
Allocate
  • RtlpAllocateHeapInternal 函数中,由于 Size > Heap->SegContexts[1].MaxAllocationSizeSize > 0x7f0000 则调用 LB 堆核心函数 RtlpHpLargeAlloc 分配内存。
    • 调用 RtlpHpMetadataAlloc 函数为 LargeAllocData 结构分配内存。
    • 计算 ReservedSize ,通常是 Size + 0x1000
    • 调用 RtlpHpAllocVA 函数分配一段虚拟内存空间,注意此时还没有分配物理页。
    • 调用 RtlpHpAllocVA 函数分配实际内存。
    • 初始化 LargeAllocData 结构体。
    • LargeAllocData 插入到 LargeAllocMetadata 中。
    • 更新 Heap->LargeReservedPagesHeap->LargeCommittedPages
    • 将分配的内存的起始地址返回。
Free
  • RtlpAllocateHeapInternal 函数中,如果 BlockPtr 最低 16 比特为 0 则判定为 Large Block 堆分配,调用 LB 堆的核心释放函数 RtlpHpLargeFree
    • 根据 BlockPtrLargeAllocMetadata 中找到对应的 LargeAllocData
    • 调用 RtlRbRemoveNode 函数将该 LargeAllocDataLargeAllocMetadata 中取出。
    • 调用 RtlpHpFreeVA 函数把 BlockPtr 指向的内存释放掉。
    • 更新 Heap->LargeReservedPagesHeap->LargeCommittedPages
    • 调用 RtlpHpMetadataFree 释放对应的 LargeAllocData

本文标签: 基础知识 Windows pwn