由于打ctf的时候经常看见异常处理的题目,虽然知道流程但是原理还是不是很理解,也不会写,所以准备学一下。记录一下本人的学习过程,如有错误,希望各位大佬能指正。

概述

异常是程序在执行期间产生的问题。C++ 异常是指在程序运行时发生的特殊情况,比如尝试除以零的操作。

触发异常后,如果有对应的异常处理就会跳到异常处理去执行,相当于是编写代码的人针对会发生的错误的解决方案。如果没有异常处理或者异常处理程序未成功处理异常,程序就会退出。具体可以看加密与解密的seh异常处理部分。

SEH异常处理

简述

有关数据结构

TIB,TEB

TIB(Thread Information Block 线程信息块) 是保存线程基本信息的数据结构,user mode下位于TEB(Thread Enviroment Block线程环境块)的头部,TEB是操作系统为了保存每个线程的私有数据而创建的,所以每个线程都有TEB。

TIB结构如下

image-20220616193019338

x86平台的user mode上,fs:[0]总是指向TEB,可以通过这个来定位。(x64平台上是gs:[0]指向TEB)

_EXCEPTION_REGISTRATION_RECORD

TEB偏移量为0的结构,是用来对线程的异常进行处理的结构。结构如下

image-20220616194858424

俩元素,第一个是指向下一个结构的指针,第二个是指向handler,处理异常的函数。这个结构层层相连,构成了我们的SEH。SEH就相当于是一个长链表,链表元素就是一个个_EXCEPTION_REGISTRATION_RECORD结构。当运行发生异常,程序就会去fs:[0]里找到TEB,然后在TEB的偏移量为0的地方找到SEH的链表头,然后一个个遍历,直到找到对应的异常处理函数。(其实感觉书上这里有点问题,不是先找VEH吗,不是很懂)

因为TEB是线程的私有的数据结构,所以每个线程都有自己的SEH链,所以我们说,SEH是基于线程的。

_EXCEPTION_POINTERS

如果一个异常发生了,且没有调试器可以进行干预,则操作系统会将控制权交予用户态的异常处理。但是因为内核态和用户态用的是俩不同的栈,所以用户态无法直接获得异常信息。操作系统此时会将仨数据结构放到用户态的栈里面:EXCEPTION_RECORD,CONTEXT,_EXCEPTION_POINTERS。第三个结构有俩指针,第一个指向EXCEPTION_RECORD,第二个指向CONTEXT。这样用户态就可以获得异常信息了。

安装SEH和卸载SEH

由于我们知道SEH本质就是一个链表,所以我们只需要把我们写好的一个_EXCEPTION_REGISTRATION_RECORD结构插入到链表头就行。首先push指向我们handler的地址,然后push fs:[0],此时就成功的创造了一个_EXCEPTION_REGISTRATION_RECORD。最后mov fs:[0],esp,就成功的修改了我们的TEB,相当于插入了一个新的节点。

image-20220616201349427

卸载就是把esp赋值为刚刚存入fs:[0]的(像上图的右下角的那个_EXCEPTION_REGISTRATION_RECORD的next的地址)。然后pop一下保证栈帧平衡,就可以了。

代码

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
#include <windows.h>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
char flag[] = "ek`fzrdg^sdrs|";

VOID test_seh()
{
int* pValue = NULL;
__try
{
printf("into Try.\n");
*pValue = 0x114514;
}
__except (printf("into except"))
{
cout << "into handler" << endl;
for (int i = 0; i < strlen(flag); i++) {
flag[i] += 1;
}
}
}

int main(int argc, char* argv[])
{
test_seh();
cout << flag;
return 0;
}

ida分析

image-20220616204142053

test_seh函数的最开始,先push handler,再push fs:[0]。

看到这个源代码里的try….except块里的代码

image-20220616204701598

下方的except对应这个,ida显示的是__except filter

image-20220616204856744

except里的处理部分的代码呢?在这里

image-20220616204953960

__except($LN8)中的LN8是刚刚的filter对应的label,这里个人感觉就是判断是哪个filter里的处理代码的标志,看label。

验证一下,又加了一点代码

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
#include <windows.h>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
char flag[] = "dj_eyqcfrcqr{";


VOID test_seh()
{
int* pValue = NULL;
__try
{
printf("into __Try.\n");
__try {
*pValue = 0x114514;
}
__except(printf("into into except!\n")) {
cout << "into into handler!" << endl;
for (int i = 0; i < strlen(flag); i++) {
flag[i] += 1;
}
}
*pValue = 0x114514;
}
__except (printf("into __except"))
{
cout << "into handler" << endl;
for (int i = 0; i < strlen(flag); i++) {
flag[i] += 1;
}
}
}

int main(int argc, char* argv[])
{
test_seh();
cout << flag;
return 0;
}

可以看到

image-20220616210755394

except里面的操作都对应的是except filter

image-20220616210902828

image-20220616210955176

而从这个except filter上方的label可以找到对应的处理代码。

思路

所以以后我再遇到这种seh的题,首先定位filter,然后根据filter的label找到对应的处理代码,然后下断进行分析。

VEH异常处理

在VEH里面放自己的异常处理函数感觉在实战中还是蛮常见的,比如无痕hook那种,硬件断点加VEH,十分好用,一定要好好学。

分析

VEH和SEH的区别

VEH和SEH最大的几个区别就是

  • 异常发生时,VEH会先于SEH获得控制权(不过还是没有调试器优先级高)
  • VEH是全局的链表,SEH是基于线程的,基于函数的。每个函数有自己的seh handler,而VEH在整个进程的范围都是有效的,可以捕获和处理所有线程的异常
  • VEH不需要栈展开,SEH需要经过栈展开来将栈里面存放的数据进行一个释放,而VEH并不基于栈,所以不需要

处理流程

用户产生异常后,内核函数KiDispatchException会修改eip为KiUserExceptionDispatcher,然后就啥也不干,所以主要的异常处理都是在KiUserExceptionDispatcher函数中的。

KiUserExceptionDispatcher

在这里插入图片描述

首先是进入RtIDispatchException函数,这个函数的作用是找到对应的异常处理函数。如果返回的是true,意味着异常被处理,那就皆大欢喜,直接进入ZwContinue函数,将eip进行一个修正,然后回到出现异常或者新的地方执行。如果没有被处理就跳转然后进行二次分发,和加密与解密上的描述相同

image-20220615224640309

RtIDispatchException

在这里插入图片描述

先从VEH链表中遍历,如果没有就遍历SEH链表。

汇总

感觉逻辑也不难,整个流程大概如下:

  • 捕获异常信息
  • 交予KiDispatchException,修正ip为KiUserExceptionDispatcher
  • KiUserExceptionDispatcher中调用RtIDispatchException
  • RtIDispatchException按照先VEH后SEH的顺序遍历链表,查找对应的异常处理
  • 没找到就二次分发,找到就调用然后返回KiUserExceptionDispatcher
  • 调用ZwContinue,进入ring0,修正eip,返回到ring3.
  • 从修正后的eip开始执行

代码

试着写了一个简单的代码

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
#include <csignal>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <stdio.h>
#include <windows.h>
using namespace std;
char input[32];
char flag[] = "flag{catch_test}";
typedef PVOID(NTAPI* FnAddVectoredExceptionHandler)(ULONG, _EXCEPTION_POINTERS*);
FnAddVectoredExceptionHandler MyAddVectoredExceptionHandler;
LONG NTAPI VectExcepHandler(PEXCEPTION_POINTERS pExcepInfo)
{
if (pExcepInfo->ExceptionRecord->ExceptionCode == 0xC0000094)
{
pExcepInfo->ContextRecord->Eip = pExcepInfo->ContextRecord->Eip + 2;
for (int i = 0; i < strlen(input); i++) {
input[i] += 1;
}
return EXCEPTION_CONTINUE_EXECUTION;
}

return EXCEPTION_CONTINUE_SEARCH;
}
void set_veh_catch()
{
HMODULE hModule = GetModuleHandle(L"Kernel32.dll");
MyAddVectoredExceptionHandler = (FnAddVectoredExceptionHandler)::GetProcAddress(hModule, "AddVectoredExceptionHandler");
MyAddVectoredExceptionHandler(0, (_EXCEPTION_POINTERS*)&VectExcepHandler);
}
int main()
{
set_veh_catch();
std::cout << "input the flag";
cin >> input;
for (int i = 0; i < strlen(input); i++) {
if (flag[i] & 1) {
__asm {
xor edx,edx
xor ecx,ecx
idiv ecx
}
}
input[i] ^= 0x12;
}
std::cout << input;
}

通过除零异常来抛出异常,从而跳到我们写的异常处理函数进行执行,对input进行操作后set ip后返回。此处idiv ecx指令机器码为2,所以eip + 2。

AddVectoredExceptionHandler是官方提供的添加VEH异常处理函数的一个函数,第一个参数如果是0就是添加到链表尾部,1的话就是头部。第二个参数是我们自个儿写的异常处理函数。

ida分析

image-20220616211800438

image-20220616211811087

可以看到还是比较明显的,可能还需要整个无痕hook之类的才比较实用,不然拖进ida很容易就被一眼看出来了。

然后没啥不同的地方了,注册后遇到对应的异常就会跳到异常处理函数,要分析的话在异常处理下断即可。

总结

异常处理还是比较神奇,也能用异常处理来实现很多牛逼的操作,这个基础还是得打好。

参考

https://blog.csdn.net/weixin_30917213/article/details/96341398

https://blog.csdn.net/weixin_42052102/article/details/83540134

加密与解密第四版