环境准备

checksec

Wenzel/checksec.py: Checksec tool in Python, Rich output. Based on LIEF (github.com)

winpwn

1
2
3
4
5
6
7
pip3 install winpwn

pip3 install pefile

pip3 install keystone-engine

pip3 install install capstone

windbg

microsoft store下载

配置到winpwn调试

在HOMEDIR创建.winpwn

填入一下内容,自行更改路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
"debugger":{
"i386": {
"x64dbg": "F:\\ctfTools\\debugTools\\x64debug\\release\\x32\\x32dbg.exe",
"gdb": "F:\\ctfTools\\windows-gdb\\mingw-w64-686\\mingw32\\bin\\gdb.exe",
"windbg": "C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x86\\windbg.exe",
"windbgx": "C:\\Users\\byzero\\AppData\\Local\\Microsoft\\WindowsApps\\Microsoft.WinDbg_8wekyb3d8bbwe\\WinDbgX.exe"
},
"amd64": {
"x64dbg": "F:\\ctfTools\\debugTools\\x64debug\\release\\x64\\x64dbg.exe",
"gdb": "F:\\ctfTools\\windows-gdb\\mingw-w64-64\\mingw64\\bin\\gdb64.exe",
"windbg": "C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x64\\windbg.exe",
"windbgx": "C:\\Users\\byzero\\AppData\\Local\\Microsoft\\WindowsApps\\Microsoft.WinDbg_8wekyb3d8bbwe\\WinDbgX.exe"
}
},
"debugger_init": {
"i386": {
"x64dbg": "",
"gdb": "",
"windbg": ".load E:\\ShareDir\\building\\bywin\\pykd_ext_2.0.0.24\\x86\\pykd.dll;!py -g E:\\ShareDir\\building\\bywin\\byinit.py;",
"windbgx": ".load E:\\ShareDir\\building\\bywin\\pykd_ext_2.0.0.24\\x86\\pykd.dll;!py -g E:\\ShareDir\\building\\bywin\\byinit.py;"
},
"amd64": {
"x64dbg": "",
"gdb": "",
"windbg": ".load E:\\ShareDir\\building\\bywin\\pykd_ext_2.0.0.24\\x64\\pykd.dll;!py -g E:\\ShareDir\\building\\bywin\\byinit.py;",
"windbgx": ".load E:\\ShareDir\\building\\bywin\\pykd_ext_2.0.0.24\\x64\\pykd.dll;!py -g E:\\ShareDir\\building\\bywin\\byinit.py;"
}
}
}

winserver

https://github.com/Ex-Origin/win_server

ProcessExplorer

进程资源管理器 - Sysinternals | Microsoft Learn

vmmap

VMMap - Sysinternals | Microsoft Learn

Metasploit

保护

DEP

即linux下的NX,栈不可执行

GS

即linux下的canary,不太相同的是GS的值最低位不是\x00

绕过

利用未被GS保护的内存模块:GS机制只有在缓冲区大小大于4字节的函数中才存在,可以寻找缓冲区大小不大于4字节的函数

替换掉.data中的cookie值:cookie值存储在.data段中,如果可写就可以覆盖

ASLR

地址随机化,使得共享库,堆栈的地址不固定,在windows10中已默认启用

不过为啥二进制文件会有这个属性?

Dynamic Base

程序编译时可通过/DYNAMICBASE编译选项指示程序是否利用ASLR的功能。

High Entropy VA

这个保护被称为高熵64位地址空间布局随机化,一旦开启,表示此程序的地址随机化的取值空间为64 bit,这会导致攻击者更难去推测随机化后的地址

SEH

结构化异常处理(Structured Exception Handling,简称 SEH)是一种Windows操作系统对错误或异常提供的处理技术。为Windows的程序设计者提供了程序错误或异常的处理途径,使得系统更加健壮。

SafeSEH

为了防止攻击者通过覆盖堆栈上的异常处理函数句柄,从而控制程序执行流程的攻击,在调用异常处理函数之前,对要调用的异常处理函数进行一系列的有效性校验,如果发现异常处理函数不可靠,立即终止异常处理函数的调用。

SEHOP

即结构化异常处理保护(Structured Exception Handling Overwrite Protection),这个保护能够防止攻击者利用结构化异常处理来进行进一步的利用。

1. 所有SEH结构体必须存在栈上
2. 所有SEH结构体必须四字节对齐
3. 所有SEH结构体中处理异常的函数必须不在栈上
4. 检测整个SEH链中最后一个结构体,其next指针必须指向0xffffffff,且其异常处理函数必须是ntdll!FinalExceptionHandler
5. 攻击者将SEH指针劫持到堆空间中运行shellcode。
6. 有了SEHOP机制以后,由于ASLR的存在,攻击者很难将伪造的SEH链表的最后一个节点指到
   ntdll!FinalExceptionHandler上所以在检测最后一个节点的时候会被SEHOP机制发现异常

绕过方式

泄露栈信息并修复SEH chain

Force Integrity

这个保护被称为强制签名保护,一旦开启,表示此程序加载时需要验证其中的签名,如果签名不正确,程序将会被阻止运行。

Control Flow Guard

控制Flow防护 (CFG) 是一项高度优化的平台安全功能,旨在打击内存损坏漏洞。 通过严格限制应用程序可以从何处执行代码,利用漏洞(如缓冲区溢出)执行任意代码会更加困难。

这项技术通过在间接跳转前插入校验代码,检查目标地址的有效性,进而可以阻止执行流跳转到预期之外的地点, 最终及时并有效的进行异常处理,避免引发相关的安全问题

Return Flow Guard

即返回地址防护(Return Flow Guard),这项技术会在每个函数头部将返回地址保存到fs:[rsp](Thread Control Stack),并在函数返回前将其与栈上返回地址进行比较,从而有效阻止了这些攻击方式。

Isolation

这个保护被称为隔离保护,一旦开启,表示此程序加载时将会在一个相对独立的隔离环境中被加载,从而阻止攻击者过度提升权限。

Authenticode

签名保护

栈调用约定

32位是栈传参

64位是Microsoft x64

在发生函数调用的时候前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指针完成。
  • 调用者负责清理栈帧,被调用者不用清理栈帧,但是有时候调用者不一定会清理栈帧。这是因为与通过 PUSHPOP 指令在堆栈中显式添加和移除参数的x86 编译器不同,x64 模式下,编译器会预留足够的堆栈空间,以调用最大目标函数(参数方法)所使用的任何内容。随后,在调用子函数时,它重复使用相同的堆栈区域来设置这些参数,从而实现不用调用者反复清栈的过程

常见dll

  • ntdll.dll:ntdll.dll是重要的Windows NT内核级文件。描述了windows本地NTAPI的接口。当Windows启动时,ntdll.dll就驻留在内存中特定的写保护区域,使别的程序无法占用这个内存区域。是Windows系统从ring3到ring0的入口,位于Kernel32.dll和user32.dll中的所有win32 API 最终都是调用ntdll.dll中的函数实现的。ntdll.dll中的函数使用SYSENTRY进入ring0,函数的实现实体在ring0中
  • kernel32.dll:kernel32.dll是非常重要的32位动态链接库文件,属于内核级文件。它控制着系统的内存管理、数据的输入输出操作和中断处理,当Windows启动时,kernel32.dll就驻留在内存中特定的写保护区域,使别的程序无法占用这个内存区域
  • KernelBase.dll:系统文件kernelbase.dll是存放在Windows系统文件夹中的重要文件,通常情况下是在安装操作系统过程中自动创建的,对于系统正常运行来说至关重要
  • ucrtbase.dll:在介绍ucrtbase.dll前先看一下msvcrt.dll是啥,msvcrt.dll是微软在windows操作系统中提供的C语言运行库执行文件(Microsoft Visual C Runtime Library),其中提供了printf,malloc,strcpy等C语言库函数的具体运行实现,这个和libc.so很像。ucrtbase.dll其实就是把msvcrt.dll拆开了,主要的c运行时的代码放在了ucrtbase.dll

工具使用

windbg

符号

  • .sympath查看当前符号路径

  • .reload为当前所有加载的模块加载符号

  • dt type查看符号的定义

  • dt type [address]查看某处某个符号

  • x module!*,查看模块的全部符号
    • 如果没有出现输出,请输入 .reload /f 以尝试强制加载符号。使用 !sym Noise 显示附加符号加载信息。

内存查看

  • k堆栈结构,只能看到栈的调用层次
  • d address,查看某处的内存
    • dd address,差不多,更好看一点
  • dps address,将某处视作堆栈(真正实用的查看栈)
  • u address,将某处内存视作代码查看

执行与断点

  • g开始执行
    • gu执行到当前函数结束
  • p,单步过
  • t,单步进

  • bp address,软件断点

  • bu address,硬件断点
  • ba optione size address设置处理器断点(通常称为数据断点), 当访问指定内存时会触发该断点。

    • optione可以为e(执行),w(写),r(读),i(i/o)
    • size可以为1,2,4,8(8只能在64位程序),当optione为e时,size必须为1
  • bl查看所有断点

  • bd idx,取消某个断点

模块

lm列出所有模块

线程

!teb当前线程TEB结构体

Metasploit

gadget

msfpescan -f file_address option

option可以为

  • -j,寻找寄存器跳转指令
  • -p,寻找pop-pop-ret类gadget

shellcode

SEH

在windows的利用中SEH结构体是十分重要的,而且相对较为复杂

所以专门讲一讲

TEB与TIB

在了解SEH之前,我们先了解两个概念:TEB和TIB

TEB(Thread Environment Block.线程环境块),系统在此TEB中保存频繁使用的线程相关的数据。位于用户地址空间。进程中的每个线程都有自己的一个TEB。一个进程的所有TEB都以堆栈的方式,存放在线性内存空间中,每4KB为一个完整的TEB,不过该内存区域是向下扩展的。在用户模式下,当前线程的TEB位于独立的4KB段,可通过CPU的FS寄存器来访问该段,一般存储在[FS:0]。在用户态下WinDbg中可用命令$thread取得TEB地址。

这是一个TEB的组成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
typedef struct _TEB {
PVOID Reserved1[12];
PPEB ProcessEnvironmentBlock;
PVOID Reserved2[399];
BYTE Reserved3[1952];
PVOID TlsSlots[64];
BYTE Reserved4[8];
PVOID Reserved5[26];
PVOID ReservedForOle;
PVOID Reserved6[4];
PVOID TlsExpansionSlots;
} TEB, *PTEB;

0:000> dt nt!_TEB
ntdll!_TEB
+0x000 NtTib : _NT_TIB
+0x01c EnvironmentPointer : Ptr32 Void
+0x020 ClientId : _CLIENT_ID
+0x028 ActiveRpcHandle : Ptr32 Void
+0x02c ThreadLocalStoragePointer : Ptr32 Void
+0x030 ProcessEnvironmentBlock : Ptr32 _PEB
+0x034 LastErrorValue : Uint4B
+0x038 CountOfOwnedCriticalSections : Uint4B
+0x03c CsrClientThread : Ptr32 Void
+0x040 Win32ThreadInfo : Ptr32 Void
+0x044 User32Reserved : [26] Uint4B
+0x0ac UserReserved : [5] Uint4B
+0x0c0 WOW32Reserved : Ptr32 Void
+0x0c4 CurrentLocale : Uint4B
+0x0c8 FpSoftwareStatusRegister : Uint4B
+0x0cc ReservedForDebuggerInstrumentation : [16] Ptr32 Void
+0x10c SystemReserved1 : [26] Ptr32 Void
+0x174 PlaceholderCompatibilityMode : Char
+0x175 PlaceholderHydrationAlwaysExplicit : UChar
+0x176 PlaceholderReserved : [10] Char
+0x180 ProxiedProcessId : Uint4B
+0x184 _ActivationStack : _ACTIVATION_CONTEXT_STACK
+0x19c WorkingOnBehalfTicket : [8] UChar
+0x1a4 ExceptionCode : Int4B
+0x1a8 ActivationContextStackPointer : Ptr32 _ACTIVATION_CONTEXT_STACK
+0x1ac InstrumentationCallbackSp : Uint4B
+0x1b0 InstrumentationCallbackPreviousPc : Uint4B
+0x1b4 InstrumentationCallbackPreviousSp : Uint4B
+0x1b8 InstrumentationCallbackDisabled : UChar
+0x1b9 SpareBytes : [23] UChar
+0x1d0 TxFsContext : Uint4B
+0x1d4 GdiTebBatch : _GDI_TEB_BATCH
+0x6b4 RealClientId : _CLIENT_ID
+0x6bc GdiCachedProcessHandle : Ptr32 Void
+0x6c0 GdiClientPID : Uint4B
+0x6c4 GdiClientTID : Uint4B
+0x6c8 GdiThreadLocalInfo : Ptr32 Void
+0x6cc Win32ClientInfo : [62] Uint4B
+0x7c4 glDispatchTable : [233] Ptr32 Void
+0xb68 glReserved1 : [29] Uint4B
+0xbdc glReserved2 : Ptr32 Void
+0xbe0 glSectionInfo : Ptr32 Void
+0xbe4 glSection : Ptr32 Void
+0xbe8 glTable : Ptr32 Void
+0xbec glCurrentRC : Ptr32 Void
+0xbf0 glContext : Ptr32 Void
+0xbf4 LastStatusValue : Uint4B
+0xbf8 StaticUnicodeString : _UNICODE_STRING
+0xc00 StaticUnicodeBuffer : [261] Wchar
+0xe0c DeallocationStack : Ptr32 Void
+0xe10 TlsSlots : [64] Ptr32 Void
+0xf10 TlsLinks : _LIST_ENTRY
+0xf18 Vdm : Ptr32 Void
+0xf1c ReservedForNtRpc : Ptr32 Void
+0xf20 DbgSsReserved : [2] Ptr32 Void
+0xf28 HardErrorMode : Uint4B
+0xf2c Instrumentation : [9] Ptr32 Void
+0xf50 ActivityId : _GUID
+0xf60 SubProcessTag : Ptr32 Void
+0xf64 PerflibData : Ptr32 Void
+0xf68 EtwTraceData : Ptr32 Void
+0xf6c WinSockData : Ptr32 Void
+0xf70 GdiBatchCount : Uint4B
+0xf74 CurrentIdealProcessor : _PROCESSOR_NUMBER
+0xf74 IdealProcessorValue : Uint4B
+0xf74 ReservedPad0 : UChar
+0xf75 ReservedPad1 : UChar
+0xf76 ReservedPad2 : UChar
+0xf77 IdealProcessor : UChar
+0xf78 GuaranteedStackBytes : Uint4B
+0xf7c ReservedForPerf : Ptr32 Void
+0xf80 ReservedForOle : Ptr32 Void
+0xf84 WaitingOnLoaderLock : Uint4B
+0xf88 SavedPriorityState : Ptr32 Void
+0xf8c ReservedForCodeCoverage : Uint4B
+0xf90 ThreadPoolData : Ptr32 Void
+0xf94 TlsExpansionSlots : Ptr32 Ptr32 Void
+0xf98 MuiGeneration : Uint4B
+0xf9c IsImpersonating : Uint4B
+0xfa0 NlsCache : Ptr32 Void
+0xfa4 pShimData : Ptr32 Void
+0xfa8 HeapData : Uint4B
+0xfac CurrentTransactionHandle : Ptr32 Void
+0xfb0 ActiveFrame : Ptr32 _TEB_ACTIVE_FRAME
+0xfb4 FlsData : Ptr32 Void
+0xfb8 PreferredLanguages : Ptr32 Void
+0xfbc UserPrefLanguages : Ptr32 Void
+0xfc0 MergedPrefLanguages : Ptr32 Void
+0xfc4 MuiImpersonation : Uint4B
+0xfc8 CrossTebFlags : Uint2B
+0xfc8 SpareCrossTebBits : Pos 0, 16 Bits
+0xfca SameTebFlags : Uint2B
+0xfca SafeThunkCall : Pos 0, 1 Bit
+0xfca InDebugPrint : Pos 1, 1 Bit
+0xfca HasFiberData : Pos 2, 1 Bit
+0xfca SkipThreadAttach : Pos 3, 1 Bit
+0xfca WerInShipAssertCode : Pos 4, 1 Bit
+0xfca RanProcessInit : Pos 5, 1 Bit
+0xfca ClonedThread : Pos 6, 1 Bit
+0xfca SuppressDebugMsg : Pos 7, 1 Bit
+0xfca DisableUserStackWalk : Pos 8, 1 Bit
+0xfca RtlExceptionAttached : Pos 9, 1 Bit
+0xfca InitialThread : Pos 10, 1 Bit
+0xfca SessionAware : Pos 11, 1 Bit
+0xfca LoadOwner : Pos 12, 1 Bit
+0xfca LoaderWorker : Pos 13, 1 Bit
+0xfca SkipLoaderInit : Pos 14, 1 Bit
+0xfca SpareSameTebBits : Pos 15, 1 Bit
+0xfcc TxnScopeEnterCallback : Ptr32 Void
+0xfd0 TxnScopeExitCallback : Ptr32 Void
+0xfd4 TxnScopeContext : Ptr32 Void
+0xfd8 LockCount : Uint4B
+0xfdc WowTebOffset : Int4B
+0xfe0 ResourceRetValue : Ptr32 Void
+0xfe4 ReservedForWdf : Ptr32 Void
+0xfe8 ReservedForCrt : Uint8B
+0xff0 EffectiveContainerId : _GUID

TIB(Thread Information Block.线程信息块),是保存线程基本信息的数据结构。是TEB的第一个成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef struct _NT_TIB{
struct _EXCEPTION_REGISTRATION_RECORD *Exceptionlist; // 指向当前线程的 SEH
PVOID StackBase; // 当前线程所使用的栈的栈底
PVOID StackLimit; // 当前线程所使用的栈的栈顶
PVOID SubSystemTib; // 子系统
union {
PVOID FiberData;
ULONG Version;
};
PVOID ArbitraryUserPointer;
struct _NT_TIB *Self; //指向TIB结构自身
} 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

其中的_EXCEPTION_REGISTRATION_RECORD *Exceptionlist就是指向当前线程的SEH的指针。

那么这个_EXCEPTION_REGISTRATION_RECORD就是SEH的结构体,具体来说长这个样子:

1
2
3
4
5
6
7
8
9
10
//  Code in https://source.winehq.org/source/include/winnt.h#2623

typedef struct _EXCEPTION_REGISTRATION_RECORD{
struct _EXCEPTION_REGISTRATION_RECORD *Next; // 指向下一个结构的指针
PEXCEPTION_ROUTINE Handler; // 当前异常处理回调函数的地址
}EXCEPTION_REGISTRATION_RECORD;

0:000> dt ntdll!_EXCEPTION_REGISTRATION_RECORD
+0x000 Next : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x004 Handler : Ptr32 _EXCEPTION_DISPOSITION

TEB存放于fs段开头位置,那么fs[0]即为TIB,TIB第一个字段就保存了SEH链表的头部指针。而SEH链表中其他的节点存储在栈中。如下图:

接下来确实看一下其在内存中的存储,有三种办法

使用!exchain

1
2
3
4
5
6
7
8
9
10
0:000> !exchain
0019fa38: ntdll!_except_handler4+0 (7742af30)
CRT scope 0, filter: ntdll!LdrpDoDebuggerBreak+2e (77461a95)
func: ntdll!LdrpDoDebuggerBreak+32 (77461a99)
0019fc9c: ntdll!_except_handler4+0 (7742af30)
CRT scope 0, func: ntdll!LdrpInitializeProcess+1e57 (7745c1f9)
0019fcf4: ntdll!_except_handler4+0 (7742af30)
CRT scope 0, filter: ntdll!_LdrpInitialize+3d9f8 (77453f3f)
func: ntdll!_LdrpInitialize+3da0b (77453f52)
Invalid exception stack at ffffffff

使用FS段寄存器

1
2
0:000> ?poi(fs:[0])
Evaluate expression: 1702456 = 0019fa38

使用TEB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0:000> !teb
TEB at 0030b000
ExceptionList: 0019fa38
StackBase: 001a0000
StackLimit: 0019d000
SubSystemTib: 00000000
FiberData: 00001e00
ArbitraryUserPointer: 00000000
Self: 0030b000
EnvironmentPointer: 00000000
ClientId: 000016a4 . 00000e00
RpcHandle: 00000000
Tls Storage: 004a4bd0
PEB Address: 00308000
LastErrorValue: 0
LastStatusValue: 0
Count Owned Locks: 0
HardErrorMode: 0

SEH工作原理

在线程初始化的时候,会自动在栈中安装一个SEH结构体,作为默认异常处理,他的next就是0xFFFFFFFFF,而这个异常程序大家应该都很熟悉,就是windows程序崩溃时那个弹窗,打印出来出错函数地址。

如果程序中使用了try、excpt、assert来处理异常信息,那么编译器就会在栈中压入一个SEH结构体,同时插入链表中。

当出现异常的时候,操作系统会先中断程序,然后从TIB中取出第一个SEH结构体(也就是最近的SEH结构),使用其中的handler处理这个异常。

如果这个异常处理函数处理不了这个异常,那么就顺着next往下找别的异常处理函数,直到找到一个可以处理这个异常的函数或者到底部,也就是弹出错误窗口然后杀死线程。

通常处理完异常后,需要执行展开(Unwind)操作,该操作先通知目标结点前的各异常处理函数释放资源,然后将之前的SEH链全部删除。该操作通常由各高级语言Rtl模块来完成,Win32汇编操作时既可以不展开,也可以手工展开,还可以使用RtlUnwind函数展开。

unwind

当一个函数注册一个SEH的时候,通常都会干这些事:

1
2
3
4
push    一堆附加数据
push offset _Handler
push fs:[0] ;next
mov fs:[0],esp ;make head -> new seh

当触发异常调用SEH机制时,每个异常函数都需要四个重要的参数:

  1. pExcept:指向EXCEPTION_RECORD的结构体的指针,其中包含了异常相关信息,如地址、异常类型等。
  2. pFrame:指向栈中的SEH结构体
  3. pContext:指向context结构体,包含了所有寄存器状态信息。
  4. pDispatch:不知道干嘛的

在执行处理函数前,系统会将上述参数压栈,然后调用异常处理函数。

异常处理函数结束时有两个返回值:

0代表处理成功,返回原来程序发生异常的地方,继续执行。

1代表失败,那么就继续顺着SEH链表往后找可以处理这个异常的函数。

当系统找到了可以处理异常的函数后,系统会将已经遍历过的异常处理函数在调用一边,这个过程就是unwind操作。

其目的就是通知前面失败的SEH,系统已经处理完了异常,然后将前面失败的SEH从链表里面删除。

那么为什么需要unwind操作呢?

如果说程序通过层层的调用在SEH链表中找到了一个可以成功处理的handler,那么这时异常被处理成功返回,此时如果直接根据context恢复现场,会涉及到许多压栈操作,那么这些压栈操作就会破坏原来的SEH链表信息,fs[0]指向一个错误地址,程序将发生异常。

具体unwind做了什么呢?有兴趣的可以参考下这篇文章:Windows异常世界历险记(二)——Win32用户层下SEH机制之对RtlUnwind的逆向分析-CSDN博客

safeSEH

既然SEH存储在栈上,那么我们可以通过栈溢出修改SEH handler函数指针为shellcode地址,然后触发异常,函数进入SEH handler,就可以执行shellcode了。

为了针对这一种攻击手法,就有了safe SEH保护措施,那么safe SEH都做了哪些检查呢?

  1. 检查异常处理链是否存在于当前程序栈中,如果不是,就终止异常处理函数调用。
  2. 检查异常处理函数指针是否指向栈中,如果指向,终止异常处理函数调用。
  3. 前面两个都通过后,调用新的函数RtlIsValidHandler,对异常处理函数做一个有效性验证。

那么RtlIsValidHandler又做了哪些检查

  1. 判断程序设置IMAGE_DLLCHARACTERISTICS_NO_SEH标识。设置了,异常就忽略,函数返回校验失败。
  2. 检测程序是否包含SEH表。如果包含,则将当前异常处理函数地址与该表进行匹配,匹配成功返回校验成功,否则失败。
  3. 判断 程序是否设置ILonly标识。设置了,标识程序只包含.NET编译人中间语言,函数直接返回校验失败
  4. 判断异常处理函数是否位于不可执行页(non-executable page)上。若位于,校验函数将检测DEP是否开启,如若系统未开启DEP则返回校验成功;否则程序抛出访问违例的异常

如果异常处理函数的地址没有包含在加载模块的内存空间。校验函数将直接执行DEP相关检测,函数将依次进行如下检验:

  1. 判断异常处理函数是否位于不可执行页(non-executable page)上。若位于,校验函数将检测DEP是否开启,如若系统未开启DEP则返回校验成功;否则程序抛出访问违例的异常
  2. 判断系统是否允许跳转到加载模块的内存空间外执行,如允许则返回校验成功;否则返回校验失败

其伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
BOOL RtlIsValidHandler(handler)
{
if (handler is in image){ //在加载模块内存空间内
if (image has the IMAGE_DLLCHARACTERISTICS_NO_SEH flag ser)
return FALSE;
if (image has a SafeSEH table) //含有安全SEH表,说明程序启用SafeSEH
if (handler found in the table) // 异常处理函数地址出现在安全SEH表中
return TRUE;
else // 异常处理函数未出现在安全SEH表中
return FALSE;
if (image is a .NET assembly with the ILonly flag set) //只包含IL
return FALSE;
}
if (handler is on a non-executable page){ // 跑到不可执行页上
if (ExecuteDispatchEnable bit set in the process flags) //DEP关闭
return TRUE;
else
raise ACESS_VIOLATION; //抛出访问违例异常
}
if (handler is not in an image){ // 在加载模块内存之外,并且在可执行页上
if (ImageDispatchEnable bit set in the process flags) // 允许在加载模块内存空间外执行
return TRUE;
else
return FALSE;
}
return TRUE; //前面所有条件都满足就允许这个异常处理函数执行
}

那么,如果我们想绕过safe SEH来攻击SEH的话,如何绕过呢?

  1. 异常处理函数位于加载模块内存范围之外,DEP关闭
  2. 异常处理函数位于加载模块内存范围之内,相应模块未启用SafeSEH(安全SEH表为空),同时相应模块不是纯IL
  3. 异常处理函数位于加载模块范围之内,相应模块启用SafeSEH(安全SEH表不为空),异常处理函数地址包含在安全SEH表中

其中的DEP就是类似于linux中的NX,即堆栈数据段不可执行。

第一种情况还是比较简单的,在模块外的地址空间写shellcode或者找一个跳板跳到shellcode即可。

第二种情况,可以利用未开启safe SEH的模块中找到一条跳转指令跳到shellcode。

第三种情况有两种方式,一是清空SEH表,欺骗系统未开启safeSEH,二是将我们的指令注册到SEH表中(难度比较大)。

除了以上三种方式,有更为简单的攻击手法:

1.不攻击SEH

2.如果SEH异常处理函数指向堆区域,及时安全校验发现SEH已经不可信,仍然会调用其已经被修改的异常处理函数,所以只需要将shellcode搞到堆即可绕过。

SEHOP

针对于SEH攻击,SEHOP(SEH Overwrite Protection)横空出世。

SEHOP主要任务就是来检测SEH链表的完整性,在调用handler之前系统会先遍历链表,看一下最后一个节点是否为系统固定的最终处理函数,如果是,那么皆大欢喜;不是的话,那么不进行异常处理,程序退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if (process_flags & 0x40 == 0)  // 如果没有SEH记录则不进行检测
{
if (record != 0xFFFFFFFF) // 开始检测
{
do
{
if (record < stack_bottom || record > stack_top) // SEH 记录必须位于栈中
goto corruption;
if ((char *)record + sizeof(EXCEPTION_REGISTRATION) > stack_top) // SEH 记录结构需完全在栈中
goto corruption;
if ((record & 3) != 0) // SEH记录必须4字节对齐
goto corruption;
handler = record->handler;
if (handler >= stack_bottom && handler < stack_top) // 异常处理函数地址不能位于栈中
goto corruption;
record = record->next;
} while (record != 0xFFFFFFFF); // 遍历S.E.H链
}
if ((TEB->word_at_offset_0xFCA & 0x200) != 0)
{
if (handler != &FinalExceptionHandler) // 核心检测,地球人都知道,不解释了
goto corruption;
}
}

所以相应的绕过方法就是伪造一个SEH链,修复SEH链完整性

SEHscopetable

scopetable指向了一个用于描述函数中所有__try代码块的数组。在SEH4中,scopetable是一个被加密过后的scopetable的地址(xor cookie)

filterfunc指向异常过滤函数(__except中的表达式),handlerfunc指向except代码块。

如果filterdunc是NULL,那么Handlerfunc就指向__finally代码块。

具体有多少个try,体现在trylevel中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct _EH4_SCOPETABLE {
DWORD GSCookieOffset;
DWORD GSCookieXOROffset;
DWORD EHCookieOffset;
DWORD EHCookieXOROffset;
_EH4_SCOPETABLE_RECORD ScopeRecord[1];
};


struct _EH4_SCOPETABLE_RECORD {
DWORD EnclosingLevel;
long (*FilterFunc)();
union {
void (*HandlerAddress)();
void (*FinallyFunc)();
};
};

在函数开始时,回先保存上个函数的ebp,然后将try level、加密后的scope table、sehhandler、seh next、异常指针、esp指针以及gs压栈,gs就是类似于canary(security cookie xor ebp)的东西,。

scopetable加密的方式就是异或一下securitycookie。

针对 __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,但也必须保证这三个值的正确性。