Windows堆比较复杂,希望通过这篇文章对Windows的堆机制有一个系统全面的了解,包括堆分配、堆回收机制,以及堆的缺点,同时明白常用的堆漏洞利用技巧。同时来续上,之前半途而废的文章。
后端堆Unlink
Hello-Win
程序分析
1 | printf("[+] Hello! "); |
存在格式化字符串漏洞,但是输出后,会被检查退出。
1 | puts("[+] Please input size:"); |
在Edit
函数中有一个堆溢出漏洞。同时可以拥有一次 UAF
漏洞。
1 | int show() |
show
时存在一个数据泄露。
利用分析
首先可以调试一下,发现是 NT
堆,有堆溢出漏洞和 UAF
漏洞。那么稳定利用就是依靠 Unlink
攻击。
首先申请 5个堆块,依次释放 chunk2
、chunk4
,这里 释放 chunk4
是为了让 chunk4
进入 Freelist
首部 和 进入 listhead
,使得chunk2
后续合并时可以跳过这段检查。chunk3
是为了防止 chunk2
和 chunk4
合并,chunk5
是为了防止 chunk4
与 top chunk
合并。
然后通过 show(1)
,泄露 chunk2
的 chunk_header
。
然后通过堆溢出,修改 chunk2
的 Flink=&chunk2-8
和 Blink=&chunk2
,然后释放 chunk1
,构造 unlink
攻击即可。
那么这里的问题在于如何泄露 程序基址,以及各 加载库的基址。这里用到一个小技巧,就是 windows
下的程序的 ASLR
,并不是没启动一次程序就会变化一次,而是在一定时间范围内程序基址不改变的。所以,我们可以利用格式化字符串漏洞来泄露地址。然后即使程序退出了,再次运行时,基址也可以不改变。
得到程序基址后,就可以得到 chunk2=&chunk2
,然后得到任意地址读写漏洞。
通过这个任意地址读写漏洞,可以利用导入导出表泄露 kernel.dll
、ntdll.dll
和 ucrtbase.dll
等地址。这里我尝试过直接使用格式化字符串输出栈上残留的这些库的地址,但是 %np
的方式并没有成功,这种方法在windows
下应该并不会成功。而且对于一些与内核相关的dll
,其对于所有程序来说都是共享同一个虚拟地址,ntdll.dll
、 kernel32.dll
。
随后即可利用ROP
、或者 shellcode
来 getshell
。
释放 chunk2
之后:
1 | //chunk2_header |
泄露chunk2
堆头:
1 | 0:000> dq 1de18fb0000+7d0 |
释放chunk4
,可以看到已经将 chunk2
从 ListHint
和 ListHead
的头部移除。
1 | //chunk4 |
Unlink
攻击:
1 | //changed chunk2_header |
任意地址读写到 getshell
首先使用 IAT
表得到各个库的基址,这里选用如下地址泄露 得到 Kernel32.dll
和 ucrtbase.dll
的基址
1 | //Kernel32.dll |
但是,这个程序并没有导入 ntdll.dll
的地址。所以我们需要借助 Kernel32.dll
来泄露 ntdll.dll
的基址:
1 | //kernel32.dll 中的 ntdll.dll函数 |
然后如果考虑使用 ROP
或者 shellcode
,都需要能做到 控制程序指令流。由于没有可以供我们劫持函数指针,只能劫持 ret
地址。那么就需要泄露 栈地址。那么如何泄露返回地址所在的栈地址?
首先为什么不能利用最开始的格式化字符串泄露的栈地址,因为利用格式化字符串后程序退出,再启动时栈地址是改变的,不能够利用。
这里有一种比较通用的方法:
对于 Windows 的程序来说,每个进程都有一个PEB
,每个线程都有一个TEB
,而且他们的相对偏移一般是固定的。那么我们只要知道PEB
的地址,就可以计算出TEB
的地址,从而泄露StackBase
。
1 | 0:000> !teb |
但是PEB
的地址又该怎么查询呢,在ntdll!PebLdr
附近,有一个值可以泄露出PEB
的地址,其调试结果如下:
1 | 0:000> r $peb |
从上面可以看到ntdll!PebLdr
向上偏移0x98
字节的地方存储着PEB
地址的信息,而且这个地址信息和PEB
地址的偏移总是0x240
(不同系统偏移不一样,需要具体调试),所以我们可以利用该地址信息来计算出PEB
的地址。
又因为PEB
和TEB
的地址的偏移是固定的,我们可以计算出babyheap
线程的TEB
的地址然后泄露出该线程的栈基地址。但是,得到栈地址后,又如何确定返回地址所在。由于受到ASLR
影响,main函数的返回地址对于StackBase
来说并不是固定偏移的,这点和Linux
是一样的,
这里由于已经实现了任意地址读和知道程序基地址,可以利用爆破的方法,不断读取栈地址,如果是 main
函数的返回地址即为正确的栈地址。
rop or shellcode
执行 shellcode
如下:
1 | rop = flat([ |
EXP
1 | from pwn import * |
2019-ogeekctf babyheap
程序分析
程序总体逻辑和 Hello-win
很像,漏洞点也是一个堆溢出。
1 | .text:00401478 loc_401478: ; CODE XREF: .text:004011E9↑j |
利用分析
和 Hello-win
唯一不同在于,这是一个32位的 NT
堆。相比 64
位,区别在于 Flink
和 Blink
都是 4字节,而chunk_header
没有变化,仍然为 8
字节。
利用思路,仍然是 Unlink
攻击。不过这道题没有开启 PROCESS_MITIGATION_CHILD_PROCESS_POLICY
保护,所以可以直接通过 ROP
执行 system('cmd.exe')
来 getshell
。
EXP
1 | from pwn import * |
2020-SCTF-EasyWinheap
程序分析
1 | case 2: |
Delete
功能有一个 UAF
漏洞。
1 | *(_DWORD *)(chunk + 8 * id) = (unsigned int)puts | size_1; |
存储堆块地址中,会存储一个 puts
函数指针,在输出时会调用这个指针。
利用分析
32
位,利用 Unlink
劫持 chunk_list
。
先输出 puts|size
的值,得到 plt
的基址。随后和上面差不多。
这里是通过劫持 函数指针 为 system
,布置参数为 cmd.exe
。
EXP
1 | from pwn import * |
后端堆-堆伪造
2019-HITCON-dadadb
程序分析
程序漏洞在add
函数中,对同一个 key
再次使用 add
时,其会现释放 之前的 data_chunk
,然后根据输入的 size
再申请一个 data_chunk
,然后再次读取用户输入,但是读取输入时使用的仍然是之前的 chunk size
,所以这里存在一个堆溢出。如果旧的 chunk size
比新分配的 data_chunk
大,那么就会堆溢出。
1 | int sub_140001340() |
同时输出时,因为也是使用旧的 chunk size
,所以存在一个越界泄露。可以用于泄露数据。
利用分析
由于存在一个堆溢出,那么就很方便的就能泄露 chunk header
,和 heap
地址。
然后这里 有一个新的 得到 ntdll.dll
的方法,在堆内存开头的 _HEAP
结构体的 0x2c0
偏移处,会存储 ntdll!lock
的地址,那么我们只需要通过堆溢出修改 chunk_struct
中的 data_chunk
,就可以泄露 ntdll.dll
的基址,然后就可以泄露 kernel32.dll
和 ucrtbase.dll
和栈基址和程序基址。
这道题的难点,并不在于数据泄露,而在于如何实现 getshell
。虽然,通过堆溢出可以实现 任意地址读,但是并不能实现任意地址写。
这里实现任意地址写的方法是 类似于 Linux
下的 house of sprit
,在栈上或者 bss
段上伪造一个 fake_heap
,修改一个 freed_chunk
的 Flink
指针,将 fake_chunk
链入 Freelist
中,然后申请堆块,直到分配到栈上或 bss
上。
house of sprit
可以在 bss
段上,通过再次输入 pwd
来伪造一个 freed_chunk_header
和一个Flink\ Blink
指针在 FILE
指针之上。
然后通过堆溢出,将这个 fake_chunk
插入当前 Freelist
中。例如选用如下Freelist
链:
1 | Freelist.Flink->chunk1.Flink->chunk2.Flink->chunk3.Flink->Freelist |
此时将 fake_chunk
需要插入 chunk1
和 chunk2
之间,避免插入 Freelist
头部和尾部,因为会对BlockIndexs
有影响
1 | Freelist.Flink->chunk1.Flink->fake_chunk.Flink->chunk2.Flink->chunk3.Flink->Freelist |
所以伪造 fake_chunk
,需要知道一个header
,其size
满足 chunk1<= size <= chunk2
。然后要能够修改 chunk1.Flink
和 chunk2.Blink
,同时还需要知道 chunk1
和chunk2
的地址。
然后,就能通过分配 fake_chunk
的size
将 fake_chunk
申请出来,也就能够修改 FILE
结构体指针的值。
FILE攻击
Windows
的 FILE
结构体中,并不像 Linux
下含有各种指针,所以无法做到直接通过劫持FILE
来劫持控制流。
但是相同点是,都含有一个输入缓冲区,用以缓存输入的内容。其结构体如下:
1 | struct __crt_stdio_stream_data |
如果要实现任意地址读,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
为输入的大小
这里首先在堆中伪造 fake_FILE
的结构,然后将 FILE
指针指向该fake_FILE
即可。这里的想法是劫持返回地址,所以这里的 _base
需要设置为 ret
的 栈地址。
1 | fake_FILE = [ |
getshell
这里 getshell
的方法,是先通过劫持返回地址,rop
执行virtualProtect
将heap
设为可执行,然后执行shellcode
即可。这里解释一下为什么 shellcode
中要在执行 writefile
之前,调用GetStdHandle
,这个函数功能是检索指定标准设备的句柄(标准输入、标准输出或标准错误),因为在windows
下想执行write
和 Linux
下不同,其标准输入输出的句柄并非固定的 0、1、2
,所以这里需要先执行这个函数获取句柄
1 | HANDLE WINAPI GetStdHandle( |
参数如下:
1 | 值 含义 |
这里的另一个思路是直接将 fake_chunk伪造在栈上,这样即可不用攻击 FILE
EXP
1 | #!/usr/bin/python2 |
HITCON 2018 Windows Land
程序分析
1 |
利用分析
1 |
EXP
1 |
- 本文作者: A1ex
- 本文链接: http://yoursite.com/2021/05/21/Windows堆机制学习/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!