从上次学习windows pwn已经过去半年了,学习Windows一直在我的todo list中。所以打算5月详细全面的学习一下Windows的漏洞利用。希望对以下知识有全面了解:Windows的常用保护机制,及绕过方法;Windows漏洞利用的基础知识点;Windows栈溢出的漏洞利用手法;Windows heap的运行机制;Windows heap的利用手法;最后希望能够调试几个Windows应用层漏洞和提权漏洞,并且对Windows 内核有初步了解。
Windows基础知识
Windows程序保护
ASLR:与Linux的PIE相同,指地址随机化,将在程序启动时将DLL随机的加载到内存中的未知,自Windows 10开始已经在系统中被配置为默认启动;
High Entropy VA:高熵64位地址空间布局随机化,开启后标识此程序的随机化取值空间为64 bit,这会导致攻击者更难去推测随机化后的地址;
Force Integrity:强制签名保护,开启后标识程序加载时需要验证其中的前命,如果签名不正确,程序将会被阻止运行;
Isolation:隔离保护,开启后表示此程序加载时将会在一个相对独立的隔离环境中被加载,从而阻止攻击者过度提升权限;
NX/DEP/PAE:NX保护指的是内存页不可运行。属于系统级的内存保护功能,能够将一页或多页标记为不可执行,从而防止从该内存区域运行代码,以帮助防止利用缓冲区溢出。防止代码在数据页面(例如堆、栈和内存池)中运行,在Windows中常称为DEP。
PAE指物理地址拓展,PAE是一项处理器功能,使x86处理器可以在部分windows版本上访问4 GB以上的物理内存。在基于x86的系统上运行的某些32位版本的Windows Server可以使用PAE访问最多64 GB或128 GB的物理内存,具体取决于处理器的物理地址大小。使用PAE,操作系统将从两级线性地址转换为三级地址转换。两级线性地址转换将线性地址拆分为3个独立的字段索引到内存表中,三级地址转换将其拆分为4个独立的字段:一个2位字段,两个9位字段和一个12位字段。PAE模式下的页表条目(PTE)和页目录条目(PDE)的大小从32位增加到64位。附加位允许操作系统PTE或PDE引用4 GB以上的物理内存,同时PAE将允许在基于x86的系统上运行32位windows中启用DEP等功能。
SEHOP
:即结构化异常处理保护(structured Exception Handling Overwrite Protection),这个保护能够防止攻击者利用结构化异常处理来进行进一步的利用。CFG
:即控制流防护这项技术通过在间接跳转前插入校验代码,检查目标地址的有效性,进而可以阻止执行流跳转到预期之外的地点,最终及时有效的进行异常处理,避免引发相关的安全问题。RFG:即返回地址防护,在每个函数头部将返回地址保存到
fs:[rsp](thread control stack)
,并在函数返回前将其与栈上返回地址进行比较,从而有效阻止攻击;SafeSEH:安全结构化异常处理(Safe Structured Exception Handlers),白名单版的安全沙箱,定义一些异常处理程序,并基于此构造安全结构化异常处理表,程序运行后,安全结构化异常处理表之外的异常处理程序将会被阻止运行;
GS:类似于Linux中的Canary保护,开启后,会在返回地址和BP之前压入一个额外的
Security Cookie
,系统会比较栈中的这个值和原先存放在.data
中的值做一个比较,如果两者不吻合,则说明发生了栈溢出;Authenticode:签名保护;
.NET:DLL混淆级保护
栈溢出覆盖返回地址
首先以一道最简单的栈溢出来稍微熟悉一下windows
的一些传参技巧和调试技巧。
1 | void __cdecl wrong(char *input) |
在wrong
函数中调用 strcpy
实现了一个栈溢出。
所以只需要覆盖返回地址即可。
由于是32位的,所以使用了栈传参。如果是64位的,则依次使用 rcx\rdx\r8\r9
传参。
EXP
1 | from pwn import * |
栈溢出之泄露StackCookie
inCTF-warmup
程序分析
存在一个栈溢出漏洞,并且存在一个函数后门。栈溢出覆盖函数返回地址进入后门即可。
1 | int __cdecl main_0(int argc, const char **argv, const char **envp) |
EXP
1 | #!/usr/bin/python2 |
栈溢出之劫持Scope_table
除了最简单的栈溢出,现在Windows的栈溢出都需要劫持SEH。
结构化异常处理SEH
结构化异常处理是Windows操作系统上对c/c++程序语言做的语法拓展,用于处理异常事件的程序控制结构。异常事件是指打断程序正常执行流程的不在期望之中的硬件、软件事件。硬件异常是CPU抛出的如 除0、数值溢出等;软件异常是操作系统与程序通过RaiseException
语句抛出的异常。Microsoft
拓展了c语言的语法,用 try-except
与 try-finally
语句来处理异常。异常处理程序可以释放已经获取的资源、显示出错信息与程序内部状态供调试、从错误中恢复、尝试重新执行出错的代码或者关闭程序等。一个 try
语句不能既有 __except
,又有 __finally
。但 try-except
与 try-finally
语句可以嵌套使用。
SHE相关结构体
TIB结构
TIB(Thread Information Block
,线程信息块),是保存线程信息的数据结构,存在于 x86
机器上,也被称为 win32
的 TEB(Thread Environment Block,线程环境块)
。TIB/TEB
是操作系统为了保存每个线程的私有数据创建的,每个线程都有自己的 TIB/TEB
。
TEB
结构位于 Windows.h
内容如下:
1 | typedef struct _TEB { |
TIB
结构如下:
1 | // Code in https://source.winehq.org/source/include/winnt.h#2635 |
在这个结构体中有异常处理有关的成员是指向 _EXCEPTION_REGISTRATION_RECORD
结构的 Exceptionlist
指针。
_EXCEPTION_REGISTRATION_RECORD结构体
该结构体主要用于描述线程异常处理句柄的地址,多个该结构的链表描述了多个线程异常处理过程的嵌套层次关系。
结构内容为:
1 | // Code in https://source.winehq.org/source/include/winnt.h#2623 |
导入表和导出表
Windows
程序没有延迟绑定机制也没有 PLT/GOT
表,但是 Windows
程序显然也是要调用所谓的库函数的,windows
下的函数库是 DLL
文件,类似于 unix
下的 libc
文件,程序调用库函数需要借助的就是导入表和导出表。
导入表是 PE
数据组织中的一个很重要的组成部分,是为实现代码重用而设置的。通过分析导入表数据,可以获得诸如 PE
文件的指令中调用了多少外来函数,以及这些外来函数都存在于哪些动态链接库里等信息。Windows
加载器在运行 PE
时会将导入表中声明的动态链接库一并加载到进程的地址空间,并修正指令代码中调用的函数地址。在数据目录中一共有四种类型的数据与带入表数据有关:导入表、导入函数地址表、绑定导入表、延迟加载导入表。
程序中,导入表的地址通常位于 .idata
段:
1 | .idata:00409160 extrn __IAT_start__:dword |
HITB GSEC BABYSTACK
程序分析
1 | int __cdecl __noreturn main(int argc, const char **argv, const char **envp) |
程序逻辑比较简单,有10次任意地址读的机会,以及一次溢出。
那么,就是一个很简单的栈溢出漏洞。但是,可以发现程序最后没有使用ret
来退出,而是使用了一个 exit(0)
函数。那么对于传统的覆写返回地址来劫持就失效了。
利用分析
所以,这里就需要思考能否劫持其他的虚表函数。正如上面所介绍的windows
自己引入了一个虚表函数 SEH
。如果能够劫持 SEH
的入口地址,然后自己伪造一个 SEH
的虚函数表,最后去触发就能劫持程序流程了。但是劫持 SEH
,需要满足各项检查。这里一一说明。
先分析一下函数最开始的压入栈的部分:
1 | .text:004010B0 ; __unwind { // __except_handler4 |
这里首先了解几个概念:
Scope table:保存当前函数中 __try
块相匹配的 __except
或 __finally
的地址值。
1 | .rdata:00403688 stru_403688 dd 0FFFFFFE4h ; GSCookieOffset |
其 C结构如下:
1 | struct _EH4_SCOPETABLE { |
其中 FilterFunc
与 FinallyFunc
是我们自定义的 __except
或 __finally
函数的地址。
fs寄存器:fs
寄存器是指向上面所讲的 TEB结构,所以上面 lea eax, [ebp-0x10]
与 mov large fs:0, eax
指令就是在栈中插入一个 SEH
异常处理结构体到 TIB
顶部,__except_handler4
是添加的系统默认异常处理回调函数,当发生异常时会首先执行它。
1 | int __cdecl _except_handler4(int a1, int a2, int a3, int a4) |
嵌套调用了 except_handler4_common
函数,该函数首先会检查栈上的 GS
值,然后根据 securityCookies
解密 _EH4_SCOPETABLE
的地址,最终会调用到 _EH4_SCOPETABLE
里面的 FilterFunc
与 FinallyFunc
函数,也就是我们自定义的 __except
或 __finally
函数的地址。如果能够伪造一个 _EH4_SCOPETABLE
结构,里面的FilterFunc
函数指针写成自己的,其他字段不改变,覆盖栈中的 _EH4_SCOPETABLE_addr
为伪造地址,就能实现任意地址函数调用。
1 | void __cdecl ValidateLocalCookies(void (__fastcall *cookieCheckFunction)(unsigned int), _EH4_SCOPETABLE *scopeTable, char *framePointer) |
最终函数栈帧如下:
那么伪造结构如下:
1 | fake_scope_table{ |
所以总体利用步骤如下:
- 泄露必要数据
泄露一些在栈上,我们能够轻松得到的数据:GS Cookie
、Next_SEH_Frame
、SEH_Handler
、GS_Cookie
、security_cookie
。
- 伪造 fake_scope_table
然后需要伪造 fake_scope_table
,这里主要需要得到一个 system('cmd')
函数的地址。这里简单化了,题目中就有一个 system('cmd')
的后门:
1 | .text:0040138B jnz short loc_40139B |
所以伪造的 fake_scope_table
如下:
1 | p32(0xffffffe4) //GSCookieOffset |
- 然后完整的ROP如下
1 | rop += fake_scope_table |
- 如何触发
这里主要讲清楚如何触发 FilterFunc
或者 FinalFunc
。
这里去触发调用 Scope_table
,是在输出一个地址的值时,输入一个立即数,会爆出一个内存访问错误。这里即会先去执行 except
里的 FilterFunc
。
EXP
1 |
|
栈溢出之绕过Safe SEH
上面主要针对 Scope_table
进行利用,但是一般程序会开启 SafeSEH
。首先先了解 SafeSEH
的保护机理,明白上面的方法有哪里不足,其次明白绕过 SafeSEH
的方法以及适用条件。
SEH结构分析
SEH
的全名为异常处理结构体(Structure Exception Handler)是Windows特有的一种异常机制,每一个 SEH
包含两个 DWORD
指针:Next SEH Record
和 Exception Handler
。
SEH
需要通过 try/catch
之类的异常处理函数生成,同时 SEH
结构是存放在栈中的(仅32位环境)。
SEH结构入栈分析
通过 push offset __except_handler3
将处理结构异常函数的地址入栈,通过 mov eax, large fs:0
将上一个 SEH
链的地址入栈,最后将自己的 SEH
链接放入 fs:[0]
作为链首,和我们上面分析的原理类似:
1 | .text:004010B0 ; __unwind { // __except_handler4 |
SEH利用
SEH的简化流程图如下:
SEH
是在栈中的也即有可能被我们写入的数据覆盖,其次 Handler
指向的是我们的异常处理函数,会造成 EIP
的跳转。所以基础的思路就是通过 覆盖 SE Handler
,然后触发异常(除0),使得程序跳转到我们的 shellcode
。
SafeSEH保护机制
- 检查异常处理链是否位于当前程序的栈中,如果不在当前栈中,程序将终止异常处理函数的调用;
- 检查异常处理函数指针是否指向当前程序的栈中,如果指向当前栈中,程序讲终止异常处理函数的调用;
- 在前面两项检查之后,程序调用全新的函数
RtlIsValidHandler()
来对异常处理函数的有效性进行验证
- 首先该函数判断异常处理函数地址是不是在加载模块的内存空间,如果属于加载模块的内存空间,检验函数将一次进行如下判断:
- 判断程序设置
IMAGE_DLLCHARACTERISTIC_NO_SEH
标识,设置了就忽略异常,函数返回校验失败 - 检测程序是否包含
SEH
表,如果包含,则将当前异常处理函数地址与该表进行匹配,匹配成功返回校验成功,否则失败 - 判断程序是否设置
ILonly
标识,设置了标识程序只包含.NET
编译人中间语言,函数直接返回校验失败 - 判断异常处理函数是否位于不可执行页(
non-executable page
)上,若位于校验函数将检测DEP
是否开启,如若系统未开启DEP
则返回校验成功,否则程序抛出访问违例的异常
- 判断程序设置
- 如果异常处理函数的地址没有包含在加载模块的内存空间,检验函数将直接执行DEP相关检测,函数将依次进行如下检验:
- 判断异常处理函数是否位于不可执行页(
non-executable page
)上,若位于校验函数将检测DEP
是否开启,如若系统未开启DEP
则返回校验成功,否则程序抛出访问违例的异常 - 判断系统是否允许跳转到加载模块的内存空间外执行,如允许则返回校验成功,否则返回校验失败
- 判断异常处理函数是否位于不可执行页(
RtlIsValidHandler()
函数的伪代码如下:
1 | BOOL RtlIsValidHandler(handler) |
SafeSEH的缺点
- 异常处理函数位于加载模块内存之外,
DEP
关闭; - 异常处理函数位于加载模块内存范围之内,相应模块未启用
SafeSEH
(安全SEH表为空),同时相应模块不是纯IL
- 异常处理函数位于加载模块范围之内,相应模块启用
SafeSEH
(安全SEH表不为空),异常处理函数地址包含在安全SEH
表中
分析三种情况可行性:
- 只考虑
SafeSEH
,不考虑DEP
干扰,需要在加载模块内存范围之外找到一个跳板指令就可以转入shellcode
中执行; - 第2种情况,可以利用未启用的
SafeSEH
模块中的指令作为跳板,转入shellcode
执行; - 第3种情况,可以考虑:a. 清空安全SEH表,造成该模块未启用
SafeSEH
假象;b. 将指令注册到安全SEH
表中。
其他的方法:
- 不攻击SEH,使用覆盖返回地址或者虚函数表等信息;
- 利用SEH的安全校验的严重缺陷,如果
SEH
中的异常函数指针指向堆区,即使安全校验发现SEH
不可信,仍会调用其已修改过的异常处理函数。(这种方法目前在windows10上不可行,因为windows加入了一个新的检查 MEM_EXECUTE_OPTION_IMAGE_DISPATCH_ENABLE,它决定了是否允许在加载模块内存空间外执行)
绕过
通过覆盖 NEXT
指针和 SE handler
,使用未开启 Safe SEH
模块的 gadgets
绕过 SafeSEH
。
覆盖 SEH handler
使得程序执行这段 gadget
,最终跳转到 shellcode
。
向后跳转0x6字节的方案
如下布置栈帧,注意 JMP SHORT 0x6
是通过机器码 EB 06 90 90
(90 为 NOPs不影响程序执行)实现。
1 | Before OverFlow[ ][ NEXT RECORD ][ SE Handler ][ ] |
覆盖 SE handler
指向一段 POP POP ret
的 gadgets
,当发生程序错误,调用 SEH
会让程序跳转到 POP POP ret
指令的未知,当执行到 ret
的时候,会发现此时 ESP
正好指向 JMP SHORT 0x6
,程序执行这个代码,使得 EIP
向后调转 0x6
个字节,跳入在缓冲区后方的 shellcode
,成功执行代码。
这里为什么 执行完 SE handler
还能跳转到 JMP SHORT 0x6
之前执行呢?原因在于 当异常发生时,异常分发器创建自己的栈帧,会把 EH Handler
成员压入新创的栈帧中(作为函数起始的一部分),在 EH
结构中有一个域是 EstablisherFrame
,这个域指向异常注册记录(next seh)的地址并被压入栈中,当一个例程被调用的时候被压入的这个值都是位于 ESP+8
的地方。
如果用 pop pop ret
的地址覆盖 SEH Handler
,第一个 pop
将弹出栈顶的 4字节,接下来 pop
继续从栈中弹出 4 字节,最后的 ret
将把此时 esp
所指向栈顶中的值 next SEH
的地址放到 EIP
中。
HITB GSEC Babyshellcode
在上面虽然提到了一种使用 jmp_gadget
来绕过 Safe SEH
的方法,但是在 windows 10
下是否还能使用,这里需要进一步研究。但是这里可以先使用 未开启 Safe SEH
保护的 dll
中的 地址来绕过。
程序分析
在 init
函数中,会调用 init_scmgr
函数。并将 init_scmgr
函数的地址赋值给 chunk_addr
。
1 | setvbuf(v2, 0, 4, 0); |
在 init_scmgr
函数中,会调用 VirtualAlloc
创建一个可执行的堆空间。并且输出堆地址,返回也为堆地址。
1 | int init_scmgr() |
随后在输入name
处有一个栈溢出和数据泄露。通过 格式化函数,可以泄露部分数据。
1 | puts("leave your name"); |
在 Run
函数中还存在一个栈溢出,通过 memcpy
拷贝会导致栈溢出。然后会将shellcode
前4字节赋值为 0xffffffff
,随后执行shellcode
,这里执行shellcode
肯定会报错,因为shellcode
前4字节 为 0xffffffff
。
1 | int Run() |
利用分析
这里首先想到的方法就是直接利用栈溢出覆盖返回地址。但是这里有一个连锁问题,我们栈溢出只有两个地方:一个是输入name
,一个是run
。但是 输入 name
处我们需要用于泄露数据,例如 __security_cookie
;那么只能在 run
处栈溢出,但是此处栈溢出时,必然会触发一个异常,那么就会进入 seh
检查执行,如果我们直接将 next_seh
和 handler
函数以及 scope_table
都覆盖出错,那么就是直接报异常错误,还是执行不到 ret
处。
所以这里还是要考虑 seh
的攻击方法。上面提到针对 safe seh
有三种攻击方法,第三种实现困难不考虑。
首先第一种,找到一个不在当前执行模块内的地址,且为开启 DEP
,那么这里就是堆地址了,而且程序一开始也泄露了堆地址,虽然堆上的 shellcode
前4字节被覆盖为了 0xffffffff
,但是依然可以将 handler
覆盖为 shellcode_addr+4
的地址。但是这种方法在Windows7
上有效,对于 windows10
已经失效,因为开启了 MEM_EXECUTE_OPTION_IMAGE_DISPATCH_ENABLE,它决定了是否允许在加载模块内存空间外执行
所以就只有第2条,找到一个未开启 SafeSEH
的模块地址。这里在 scmgr.dll
中有一个后门函数。并且该模块也没有开启 SafeSEH
。
1 | int getshell_test() |
所以这里可以把 hander
覆盖为 getshell_test
地址。但是这里还需要注意把 next_seh
覆盖正确。原因在于Win10
里面多了一个Check
函数ntdll!RtlpIsValidExceptionChain
,这个函数会去获得当前seh chain
的prev
域的值,并将其与 当前栈上的 next_seh
比较。所以这里只需要修复 next_seh
即可。而 泄露数据 只有在程序一开始的输入 Name
的地方,这里有一个疑惑,即我们没有执行 run
函数,那么 main
一开始是没有异常处理流程的,那么栈上会有这个 next_seh
数据吗。经过我调试发现在 执行main
之前会执行 ?__scrt_common_main_seh@@YAHXZ
函数,其中一块是会调用 __SEH_prolog4
函数,其中会将 next_seh
压入栈中,而且并不会更新当前 Exception_Registration
的 seh
的值:
1 | .text:004023B0 push offset __except_handler4 |
所以,可以利用 name
溢出,来泄露位于main
函数 栈帧下方的 __SEH_prolog4
的 next_seh
函数。
EXP
1 | from pwn import * |
栈溢出执行 ROP
基础知识
Windows
下利用栈溢出执行rop
来 getshell
,有一些和 Linux
相通的知识点这里先做个介绍总结。
GS_Cookie
在windows
结束 ret
之前,会进行一个检查 __security_check_cookie
1 | .text:0000000140001000 ; __unwind { // __GSHandlerCheck |
可以看到程序首先会使用全局变量 __security_cookie
与 rsp
进行异或,生成一个 StackCookie
放到 rbp+0x18
的位置。在执行 ret
之前,会将 Stack_Cookie
与 rsp
进行异或,然后调用 __security_check_cookie
检查是否结果是否等于 __security_cookie
。
__security_cookie
这个全局变量,是存储在程序的 data
段上:
1 | .data:0000000140003008 __security_cookie dq 2B992DDFA232h ; DATA XREF: main+9↑r |
所以,利用栈溢出时,要么能泄露 StackCookie
,溢出时对齐进行修复;要么能泄露 __security_cookie
和 rsp
,对其进行修复。
库函数调用
Linux
执行 rop
,有两个关键点合适的 gadget
和准确的 库函数地址,这两点在 Linux
下都可以通过 glibc
来泄露寻找。但是对于 windows
则需要分开寻找。
ntdll.dll
是Windows
系统从ring3
到ring0
的入口。位于Kernel32.dll
和user32.dll
中的所有win32 API
最终都是调用ntdll.dll
中的函数实现的。ntdll.dll
中的函数使用SYSENTRY
进入ring0
,函数的实现实体在ring0
中。所以如果这个函数是每个程序启动基本都会调用的,可以考虑从中寻找我们需要的 gadget
。
然后是寻找库函数地址, ucrtbased.dll
这个库是 VC
程序执行必须要有的库,里面放入了很多 VC
程序的API
调用接口,类似于 Glibc.so
。所以可以考虑从 ucrtbased.dll
寻找需要调用的 函数地址。
但是,windows
的传参方式和函数调用都与 Linux
有一些差别。
Windows
64位下 参数也通过寄存器传递,依次通过 rcx\ rdx\ r8\ r9
。其次函数调用 也不能直接指向 函数实现的入口地址,又因为 windows
下没有 plt
表,所以调用程序内的函数,要指向 call func
的地址,例如要调用 puts
函数,需要使用如下 gadget
:
1 | .text:00000001400010A6 call cs:puts |
所以,我们想要实现库函数调用,也不可以直接指向库函数地址,而是要找到此类地址:
1 | .text:00000000000ABBA0 jmp common_system_char_ |
所以可能需要泄露 ntdll.dll
和 ucrtbased.dll
的基址。
Babyrop
程序分析
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
一个栈溢出漏洞,在name
偏移 0x64
位置处有 ucrtbase.dll
的地址残留,泄露地址,通过 rop
执行 system("cmd.exe")
即可。
EXP
1 | from pwn import * |
2020 强网杯 easyoverflow
程序分析
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
程序漏洞很简单,存在3次,任意数据泄露,并且都可以执行栈溢出。
利用分析
根据上面分析,要执行栈溢出,首先需要泄露 StackCookie
地址,而这里由于后续需要多次利用栈溢出并执行main
函数,所以这里如果只泄露 StackCookie
后续栈溢出再执行 main
,还得泄露 StackCookie
的值。所以这里选择先泄露 栈地址 和 __security_cookie
的值。
栈地址很好泄露,rbp
即可泄露,而 __security_cookie
需要先泄露 程序基址。可以通过 返回地址泄露。
然后第三次,可以布置栈溢出,rop
如下:
1 | padding; |
然后再次执行main
函数,可以再次泄露 ntdll.dll
,因为 在 执行 main
函数之前会先调用 ntdll.dll
,所以在main
函数的栈帧的高位会存储 ntdll.dll
的地址。通过溢出泄露该 ntdll.dll
地址。
随后即可得到 pop_rcx_r
和 pop_rbx_r
的地址。然后再泄露一次 StackCookie2
;
即可布置第2次 rop
如下:
1 | padding; |
这里的 puts_addr
选用main
中的:
1 | .text:00000000000010A6 call cs:puts |
执行完 puts
后,我们得到 __security_cookie
的值, 会再次进入循环判断,所以这里需要使用 pop_rbx_r
将循环标志置为1。
这里先通过异或得到当前的rsp
,然后 __security_cookie^(rsp+0x160)
得到StackCookie3
然后再次进入一次循环,这里即可再次布置 rop
:
1 | padding; |
这里即可泄露 read
的地址,然后得到 ucrtbase.dll
的基址,从而得到 system
和 cmd.exe
的地址。最后布置rop:
1 | padding; |
EXP
1 | from pwn import * |
2019-SUCTF-babyStack
程序分析
程序一开始就给了栈地址和程序基地址,然后覆盖返回地址已经不成功。这里只有考虑 seh
覆盖。这里从伪代码中看不出程序的漏洞,是因为有部分花指令隐藏:
1 | text:00001982 58 pop eax |
在程序要结束前,会进行一次除法错误,这里可以构造一个除0异常,然后就会进入异常处理的流程。我们查看main
函数注册的异常:
1 | .rdata:00006808 E4 FF FF FF 00 00 00 00 C0 FF+stru_6808 dd 0FFFFFFE4h ; GSCookieOffset |
这里的except
操作对应如下代码:
1 | .text:000019B5 B8 01 00 00 00 mov eax, 1 |
而 finally
操作对应如下代码:
1 | .text:000019BB 8B 65 E8 mov esp, [ebp+ms_exc.old_esp] |
而 funcs
对应的函数如下:
1 | void __noreturn funcs() |
存在10次数据泄露,也可以构造栈溢出,并且可以构造异常错误。
利用分析
这里首先需要分析,发生异常时,如何确定 调用except
还是finally
:
1 |
EXP
1 | from pwn import * |
CFG绕过
Insomni’hack Teaser 2017 Easywin
程序分析
1 | int attak() |
程序漏洞很简单,在堆上有一个函数指针,可以通过edit
覆盖该指针。
利用分析
这里泄露地址的方法,就是通过 attack
的格式化字符串,此时栈上会有 ucrtbase.dll
的残留。所以可以通过格式化字符串泄露出来,虽然执行 attack
后,会结束程序,但是 ucrtbase.dll
的基址是不变的。
然后就通过覆盖函数指针,为 system
即可。这里这个函数调用,虽然有一个 CFG
检查,但是这里执行 system
是可以的。这里后续需要研究一下 CFG
的绕过。
CFG
简单来说,CFG通过在间接跳转前插入校验代码,检查目标地址是否合法,从而可以组成程序控制流被劫持到非预期的地方。
而从细节上讲,比如:
1 | .text:00007FF6862A17BD mov rbx, [rdi+200h] |
这里需要call rbx
,不过首先需要通过__guard_check_icall_fptr
对rbx
的位置进行一个校验。
而在win10里,这个__guard_check_icall_fptr
的实现其实就是ntdll!LdrpValidateUserCallTarget
:
1 | unsigned __int64 __fastcall LdrpValidateUserCallTarget(unsigned __int64 func_addr) |
整个 check
流程如下:
func_addr >> 9
得到对应CFGBitmap
数组的下标,这里的原因是:CFGBitmap
原理是8 bytes对应1 bit,换句话说,就是8 bytes的虚拟地址空间是用CFGBitmap
中的1 bit标记的;所以这里需要右移3。CFGBitmap
数组是以QWORD
单位存的,故bitmap
也是以8 bytes也就是64 bit取的,进行判断的时候下标最大为2^6-1
,即需要6 bit表示下标;所以这里需要右移6。所以最后
func_addr >> 9
才得到对应CFGBitmap
数组的下标。综上,被check的
func_addr
实际上被分为了三个部分:1
2
3
4
5+---------------------+------------------+--------+
| 55 bits | 6 bits | 3 bits |
+---------------------+------------------+--------+
| offset in CFGBitmap | offset in bitmap | left |
+---------------------+------------------+--------+
如果
func_addr
的低4 bits不为0,也就是func_addr
不是0x10对齐的,就会同时检查两个bit,一个是bit_offset & 0xFFFFFFFFFFFFFFFE
,一个是bit_offset | 1
,举个例子:如果
func_addr = 0x101
,那么检查bitoffset = 0x20
以及``bitoffset = 0x21`;如果
func_addr = 0x10F
,那么检查bitoffset = 0x20
以及``bitoffset = 0x21`;换句话说,在
func_addr
没有0x10对齐的情况下,最后判断的相应的bitmap
中的bit是同样的,故结果也是同样的。
且只有在这两个bit均为1的情况下,才能通过检查,否则都会判为无效,从而转入异常处理,也不会进行跳转。
如果
func_addr
的低4 bits为0,也就是func_addr
是0x10对齐的,那么会先后检查bit_offset
和bit_offset | 1
,举个例子:- 如果
func_addr = 0x100
,那么检查bitoffset = 0x20
;如果此时bit_offset
对应的bit为0,则再给一次机会,检查bit_offset | 1
是否为1;如果为1,那么目标地址有效,否则无效。
换句话说,在
func_addr
是0x10对齐的情况下,只要bit_offset
或bit_offset | 1
的其中一个对应的bit是1,那么目标地址都是有效的。- 如果
EXP
1 | #!/usr/bin/python2 |
- 本文作者: A1ex
- 本文链接: http://yoursite.com/2021/05/03/Windows-栈溢出学习/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!