希望能通过这次的学习,对Windows 下的基础知识,都有一个初步的了解,以后调试CVE,也能够更有经验了。
环境搭建
checksec
有一个windows
下的 exe,但是我没有安装成功。直接使用 checksec.py脚本检测。
winpwn
可以直接安装 winpwn
1 | pip install winpwn |
调试
使用的 EX 大佬编写的一个类似 socat
的工具 Win Server,能够实现利用 pwntools
脚本连接。
可以直接使用windbg,也可以使用微软商店里的windbg preview(界面更好看)。
这里建议配置一下调试符号表的下载,由于现在墙的限制和windows
对很多符号表已经取消开放了,所以很多系统库文件已经没有符号表了。但是对已有的,加载了符号表后,调试还是很方便的。符号表配置方法如下:
首先在windbg
的symbol file path
中添加符号表本地缓存的路径和远程的符号服务器:
srv*c:\MySymbols*https://msdl.microsoft.com/download/symbols
随后在windbg
命令行中输入:.reload
即可重新加载符号文件(注意:这里由于墙的原因,所以需要梯子)
1 | .sympath // 查看当前符号查找路径 |
windbg
常用的调试命令:
系统模块与PE文件检索:
1 | 0:000> lm // 列出所有模块对应的符号信息 |
进程与线程操作
1 | | // 列出调试进程 |
反汇编指令与内存断点
1 | u // 反汇编当前eip寄存器地址的后8条指令 |
堆栈操作
1 | k // 显示当前调用堆栈 |
其他命令:
1 | dt ntdll!* // 显示ntdll里的所有类型信息 |
Windows Pwn基本知识
函数调用约定
函数调用大多与架构有关,与操作系统无关。
x64
模式下只有一种调用方式 fastcall
,在发生函数调用的时候前4个参数通过寄存器 RCX,RDX,R8,R9
,传递剩下的通过栈传递。函数的返回值保存在 RAX
寄存器下。
- 如果返回值为较大的值(结构体),那么由调用方在栈上分配空间,并将指针通过
RCX
传递给被调用函数,被调用函数通过RAX
返回该指针 - 栈需要十六字节对齐,但是
call
之后会push
八字节的返回地址,但是这样的情况下栈就没办法对齐了,因此所有的非叶子节点调用函数都需要调整栈帧为16n+8
。 - 对于
R8-R15
寄存器,我们可以使用r8, r8d, r8w, r8b
分别代表r8
寄存器的64
位、低32
位、低16
位和低8
位 - 一般情况下
x64
平台中RBP
栈指针被废弃,只作为普通的寄存器使用,所有的栈操作都通过RSP
指针完成。 - 调用者负责清理栈帧,被调用者不用清理栈帧,但是有时候调用者不一定会清理栈帧。这是因为与通过
PUSH
和POP
指令在堆栈中显式添加和移除参数的x86
编译器不同,x64
模式下,编译器会预留足够的堆栈空间,以调用最大目标函数(参数方法)所使用的任何内容。随后,在调用子函数时,它重复使用相同的堆栈区域来设置这些参数,从而实现不用调用者反复清栈的过程
程序保护
ASLR
与 Linux
相同,在程序启动时将 DLL
随机加载到内存中的位置,这将缓解恶意程序的加载。ASLR
在 Windows10
后开始在系统中被配置为默认启用。
High Entropy VA
称为 高熵64位地址空间布局随机化,开启后,表示此程序的地址随机化的取值空间为64bit,这会导致攻击者更难去推测随机化后的地址。
Force Integrity
这个保护被称为强制签名保护,一旦开启,表示此程序加载时需要验证其中的签名,如果签名不正确,程序将会被组织运行。
Isolation
被称为隔离保护,一旦开启,表示此程序加载时将会在一个相对独立的隔离环境中被加载,从而阻止攻击者过度提升权限。
NX/EDP/PAE
NX
指内存页不可运行,操作系统能够将一页或多页内存标记为不可执行,从而防止从该内存区域运行代码,以帮助防止缓冲区溢出。防止代码在数据页面(例如堆、栈和内存池)中运行,在Windows
中常称为 DEP
。
PAE
(物理地址扩展,即 Physical Address Extension
),是一项处理器功能,使 x86
处理器可以在部分 Windows
版本上访问 4GB
以上的物理内存。在基于 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
中启用 EDP
等功能。
SEHOP
即结构化异常处理保护,能够防止攻击者利用结构化异常处理来进行进一步的利用。
CFG
控制流防护,在间接跳转前插入校验代码,检查目标地址的有效性,进而可以组织执行流跳转到预期之外的地点,最终及时有效的进行异常处理。
RFG
返回地址防护,在每个函数头部将返回地址保存到 fs:[rsp](Thread Control Stack)
,并在函数返回前将其与栈上返回地址进行比较,从而有效阻止了这些攻击方式。
SafeSEH
安全结构化异常处理函数,即白名单安全沙箱,事先定义一些异常处理程序,并基于此构造安全结构化异常处理表,程序正式运行后,安全结构化异常处理表之外的异常处理程序将会被阻止运行。
GS
类似 Canary
保护,一旦开启会在返回地址和BP
之前压入一个额外的 Security Cookie
。系统会比较栈中的这个值和原先存放在 .data
中的值做一个比较。
Authenticode
签名保护
.NET
DLL
混淆级保护
SEH机制
SEH
是Windows
操作系统上 对 C/C++ 程序语言做的语法拓展,用于处理异常事件的程序控制结构。异常事件是指打断程序正常执行流程的不在期望之中的硬件、软件事件。硬件异常是CPU
抛出的如 除0、数值溢出等;软件异常是操作系统与程序通过 RaiseException
语句抛出的异常。Windows
拓展了C语言的语法,用 try-except
与 try-finally
语句来处理异常。异常处理程序可以释放已经获取的资源、显示出错信息与程序内部状态供调试、从错误中恢复、尝试重新执行出错的代码或者关闭程序等。一个 __try
语句不能既有 __except
,又有 __finally
。但 try-except
与 try-finally
语句可以嵌套使用。
SHE结构体
TIB结构体
TIB
线程信息块,是保存现成基本信息的数据结构,存在于 x86
的机器上,也被称为是 Win32
的 TEB(Thread Environment Block)
线程环境快。是操作系统为了保存每个现成的私有数据创建的,每个线程都有自己的 TIB/TEB
。
TEB
结构如下:
1 | typedef struct _TEB { |
TIB
结构如下:
1 | // Code in https://source.winehq.org/source/include/winnt.h#2635 |
在这个结构中与异常处理有关的成员是指向 _EXCEPTION_REGISTRATION_RECORD
结构的 Exceptionlist
指针,注意 异常处理链表位于 TIB
的链表头
_EXCEPTION_REGISTRATION_RECORD
结构体
该结构体主要用于描述线程异常处理句柄的地址,多个该结构的链表描述了多个线程异常处理过程的嵌套层次关系,结构内容为:
1 | // Code in https://source.winehq.org/source/include/winnt.h#2623 |
注意:fs
寄存器指向 TEB
结构,Next
指针指向 0xFFFFFFFF
,表示SEH
链结束。
Windwos异常处理流程
之前调试过,但是再看了一下,查漏补缺。
- 产生硬件异常通过
IDT
调用异常处理例程, 产生软件异常通过API
的层层调用产地异常信息。而异常又由于发生位置不同,分为内核异常和用户态异常,二者最后都会靠kiDispathException
函数来进行异常分发; - 当内核产生异常时,程序处理流程进入到
KiDispatchException
函数,在该函数内备份当前线程R3
的TrapFrame
(即栈帧的基址)。异常处理首先判断这是否是第一次异常,判断是否存在内核调试器,如果有内核调试器,则把当前的异常信息发送给内核调试器;如果没有内核调试器或者内核调试器没有处该异常 , 则进入步骤3,调用RtlDispatchException
。 - 内核异常进入
RtlDispatchException
函 数, 如果RtlDispatchException
函数没有处理该异常,那么将再次尝试将异常发送到内核调试器,如果此时内核调试器仍然不存在或者没有处理该异常,那么此时系统会直接蓝屏; - 如果是用户态异常则经过
KiDispatchException
进行用户态异常分发和处理。如果是第一次分发异常,则调用DbgKForwardException
将异常分发到内核调试器;如果内核调试器不存在或没有处理异常,则尝试将异常分发给用户态调试器;如果异常被处理,则进入步骤10;如果用户态调试器不存在或未处理异常,则检测是否是第一次处理异常,如果是第一次处理异常则进入第5步中的异常数据准备; - 准备一个返回
ntdll!KiUserExceptionDispatcher
函数的应用层调用栈,结束本次KiDispatchException
函数的运行,调用KiServiceExit
返回用户层。此时函数栈帧是ntdll!KiUserExceptionDispatcher
的执行环境,用户态线程从执行ntdll!KiUserExceptionDispatcher
开始执行。该函数调用ntdll!RtlDispatchException
进行异常的分发,进入第 6 步; - 通过
RtlCallVectoredExceptionHandlers
遍历VEH
链表尝试查找异常处理函数;如果VEH
未处理异常。则从fs[0]
读取ExceptionList
并开始执行SEH
函数处理,进入步骤7; - 如果
SEH
没有处理函数处理该异常,则检查用户是否通过SetUnhandledExceptionFilter
函数注册过进程的异常处理函数,如果用户注册过异常处理函数,调用该异常处理函数,如果异常没有被成功处理或没有自定义的异常处理函数,则进入步骤3; - 如果最后仍没有处理该异常,便会主动调用
NtRaiseException
将该异常重新跑出来,但是此时不是第一次分发,此时NtRaiseException
流程重新调用了ntdll!KiDispatchException
,并再次进入用户态异常的处理分支,进入步骤9; - 第二次进入用户态异常处理时,不会再尝试发送到内核调试器,也不会再进行异常分发,而是直接尝试发送到用户态体异常调试器,如果最后异常仍未被处理则进入步骤11;
- 异常被处理,调用
NtContine
,将之前保存的TrapFrame
还原,程序继续从异常处正常运行; - 异常不能被处理,系统调用
ntdll!KiDispatchException
调用ZeTerminateProcess
结束进程。
导入表和导出表
Windows
程序没有延迟绑定机制,也就没有 PLT/GOT
表,Windows
程序通过导入表和导出表来调用库函数。
导入表是 为了实现代码重用而设置,通过分析导入表数据,可以获得 PE
文件指令中调用了多少外来函数,以及这些外来函数都存在于哪些动态链接库里等信息。Windows
加载器在运行PE
时,会将导入表中声明的动态链接库一并加载到进程的地址空间,并修正指令代码中调用的函数地址。在数据目录中一共有四种类型的数据与导入表数据有关:导入表、导入函数地址表、绑定导入表、延迟加载导入表。导入表通常位于 .idata
段;
SafeSEH
当异常发生时,异常处理过程 RtlDispatchException
会对 SEH
进行检查:
- 首先检查异常处理链是否在栈上,如果不在栈上程序将终止异常处理;
- 其次检查异常处理
Handler
是否在栈上,如果在栈上程序将终止异常处理。 - 最后检测调用
RtlsValidHandler
检测Handler
有效性。
1 | BOOL RtlIsValidHandler(handler) |
上面伪码里的 ExcuteDispatchEnable
和 ImageDispatchEnable
标志用来控制 Handler
在不可执行内存或者不在异常模块的映射内时,是否可以执行。默认情况下,如果进程 DEP
开启,两位为 0,DEP
关闭两位为1。
通过对上面 SafeSEH
的分析,了解到 SafeSEH
的检测主要分为3类:
SEH
在加载模块的进程空间,检测是否开启了有SEH
标志位,未开启直接返回false
;随后检测是否有SEH Table
,有则检测handler
是否在表中,不在表中则报错返回SEH
在不可执行页中,如果DEP
关闭了,返回True
;如果未关闭,则返回false
;SEH
在加载模块之外,且在可执行页上,则检测是否满足 MEM_EXECUTE_OPTION_IMAGE_DISPATCH_ENABLE标志位,其决定了是否允许在加载模块内存空间外执行。
如果要绕过 SafeSEH
检测,一般都要寻找满足以上三种情况的内存来布置我们自己的 fake_seh_handler
。
当然,还有一种比较实用的方法,就是找一个未开启 SafeSEH
的 dll
来布置我们的fake_seh_handler
。这种方法是简单但比较常见,对于Windows
下的 ASLR
也可以通过寻找未开启的 Dll
中的 gadget
来利用。
Win10 在检查 SEH
时,还会检查 SEH
链,也即覆盖后的 Next
需要指向一个正常的 SEH
,保证 SEH
链的正常。
我们在 RtlDispatchException
(位于Windows\SysWoW64\ntdll.dll)函数中,可以发现对SEH
链表地址的检测:
也就是说,在Win10
下,还得注意 对 栈上SEH next
这个值进行泄露,以获得正确地址。
SEH Scope Table
系统通过 ScopeTable
结构体来实现伪造 __except
和 __finally
函数,其中 _EH4_SCOPETABLE_RECORD
结构体中的 HandlerAddress
对应 __except
函数,FinallyFunc
对应 __finally
函数
1 | struct _EH4_SCOPETABLE { |
当 GSCookieOffset
= -2
表示 GS Cookie
没有启用;EHCookie
是永远存在的,并且访问时用到的偏移都是相对于 %ebp
来计算的,对 security_cookie
的检验方式为:
1 | (ebp+CookieXOROffset) ^ [ebp+CookieOffset] == _security_cookie |
栈中指向 scopetable
的指针也要与 _security_cookie
进行异或计算。
以一个程序为例分析 Scope Table
在栈中的环境:
main
函数的开头,会将 scope_table
先插入 ebp-8
,保存了当前函数中的 __try
块相匹配的 __except
或 __finally
的地址值。
再将系统默认的异常处理回调函数 __except_handler4
插入到 ebp-c
中,当发生异常时会首先调用它。
然后再将 SEH
链表头从 fs[0]
中取出,插入到 ebp-0x10
中;
然后插入了 GS
到 bep-0x1c
中。
当异常时,程序会先调用 __except_handler
,其位于 VCRUNTIME140.dll
如下所示:
1 | int __cdecl _except_handler4_common(unsigned int *CookiePointer, void (__fastcall *CookieCheckFunction)(unsigned int), _EXCEPTION_RECORD *ExceptionRecord, _EXCEPTION_REGISTRATION_RECORD *EstablisherFrame, _CONTEXT *ContextRecord) |
会检查栈中放入的 GS
值,会根据 securityCookies
解密 _EH4_SCOPETABLE
的地址,最终会调用到 _EH4_SCOPETABLE
里面的 FilterFunc
和 FinallyFunc
。
函数栈帧结构如下:
针对 __except_handler
函数,如果我们伪造一个 scope table
,把里面的 FilterFunc
或者 FinallyFunc
改为 system('cmd')
的地址,然后把这个伪造的 scope table
通过溢出覆盖掉原 scope table
,就能够getshell
。
当然由于 栈中存储的 scope table
地址是 _EH4_SCOPETABLE_addr ^ _security_cookie
得来,所以我们也得知道 __security_cookie
的实际值。同时覆盖时,也不可避免覆盖掉 GS Cookie
,next SEH
和 except_handler
,但也必须保证这三个值的正确性。
HITB GSEC BabySTACK
程序分析
程序开启了ASLR,DEP,ControlFlowGuadr
程序总体功能较为简单,而且也给了我们一个栈地址和 一个 plt
地址。还能有10次机会输入和获取指定地址的数据。
当我们输入 no
时,存在一个栈溢出,栈大小为 0x9c
,我们可以输入 0x100
的数据。
此外,程序中还隐藏了一个后门,可以直接执行 system(cmd)
:
利用分析
看到栈溢出,而且有一个后门地址,我们首先应该想到直接覆盖 ret
地址,使 rip
跳转到后门地址。但是这道题的 main
函数并非是以 return
结束,而是以 exit
函数,在执行 exit
函数时,会在当前的栈上新增一个 exit
的栈,我们的覆盖的main
函数返回地址将失效。
然后,经过上面对 Scope Table
的分析,可以发现程序在有 __try
和 __except, __finally
时,发生了异常不会执行 SEH链
,而是先执行 Scope Table
里的 FilterFunc
或者 FinallyFunc
等
我们看到当完成了初始的栈布局后,main
函数的栈环境如下:
此时,ebp=0x19ff28
,我们往上找,能分别找到 Scope_Type
经过与 security_cookie
异或加密、SEH_next
、except_hander
、和 GS_Cookie
。
我们可以看到 SHE_next
此时的值为 0x19ff60
,其地址为 0x19ff18
,我们可以在 TEB
的头部,看到其地址为 0x19ff18
,说明该函数的 SEH
节点已经插入SHE
链表中。
那么,我们此时的方法是在栈上伪造一个 Scope_Table
,然后修改原本的 Scope_Table
的值。覆盖时,注意需要保证 GS_Cookie
的值正确,保证 SEH_next
的值仍然正确,保证 except_handler
的函数地址也正确。
我们的覆盖数据如下:
1 | 'a'*0x10 + fake_scope_table+ padding + GS_Cookie + 'a'*8 + SEH_next + except_handler + fake_scope_table_addr + p32(0) |
我们伪造的 fake_scope_table
数据如下:
1 | fake_scope_table = p32(0x0FFFFFFE4) #GSCookieOffset |
EXP
1 | from pwn import * |
HITB GSEC Babyshellcode
大体是参照 KoshI
师傅的思路,然后自己分析了一下。
程序分析
程序初始使用 init_scmgr
函数构造 buf
,可以看一下这个 dll
文件:
这里主要关心两点:一通过 VirtualAlloc
生成一个堆地址,也就是该地址不在 babyshellcode
的栈空间内;页权限设置为 0x40
拥有 RWX
权限。
此外 scmgr.dll
中还存在一个后门函数,那么我们的大概思路应该是修改 eip
来执行后门函数。
babyshellcode
还实现了 CreateShellcode
、和 RunShellcode
的功能
利用分析
首先我们建立的 shellcode
都是分配在 使用 init_scmgr
分配的堆上,地址为 0x1d0000
,shellcode
的结构体如下所示:
- 泄露
scmgr.dll
基址
上面提到 scmgr.dll
中有一个后门函数,我们想要调用它,首先就必须得知道 scmgr.dll
的基址。在 babyshellcode
中有一个 set
函数,输出了一些加密数据如下:
我们可以动态调试一下,查看每一个加密函数加密的数据是什么,如下图所示,可以看到第一个加密输出的数据就是 scmgr
的地址。也就是我们可以通过这个输出的密文获得 scmgr.dll
的基址。
但是,我们需要解密这个密文。这个加密函数比较复杂,解密太难了。根据KoshI
师傅的方法,是将加密函数dump下来,然后爆破明文地址去比对密文,得到 scmgr.dll
的地址,这样做可以把 比对的过程放到我们本地完成,而不用去触及远程程序,较为合理。但是加解密函数挺复杂的,我逆向太差了,应该自己写不出来的。
- 利用
seh_handler
main
函数中还有一个 RunShellCode
的功能,我们可以查看这个函数的代码如下,发现在这个函数的头部使用了 SEH
,我们很容易就能发现 SEH
的标志。如下所示:
在 EBP
之前,我们也能够看到 seh_handler
和 scope_table
的数据,如下:
然后在 run
函数中,能够直接调用shellcode
执行,但是问题再于执行前将我们的 shellcode
前4位放入了 0xfffffff
。也就是这个调用是必然失败的。
既然,采用了 SEH
,那么就考虑 覆盖 seh_handler
指针指向 后门函数地址,实现 getshell
。
这样做,需要绕过 SafeSEH
,即对 seh_handler
是有检查的。但是,恰好我们的 后门函数地址所在的 scmgr.dll
并没有开启 SafeSEH
,我们成功绕过。
还有一点注意,是 win10
下异常处理时,对于 栈中的 SEH_next
也是会做检查的。所以我们需要先泄露该值。利用程序一开始输入 name时有一个泄露,即可成功。
EXP
1 | from pwn import * |
参考
- 本文作者: A1ex
- 本文链接: http://yoursite.com/2020/10/15/Windows-Pwn学习/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!