Exception Handling in CTF

简单说一下异常处理在ctf中是如何体现的

SEH VEH

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
#define _CRT_SECURE_NO_DEPRECATE
#include <stdio.h>
#include <windows.h>
#include <tlhelp32.h>
#include <stdlib.h>

typedef PVOID(NTAPI* FnAddVectoredExceptionHandler)(ULONG, _EXCEPTION_POINTERS*);
FnAddVectoredExceptionHandler MyAddVectoredExceptionHandler;

LONG NTAPI VectExcepHandler(PEXCEPTION_POINTERS pExcepInfo)
{
if (pExcepInfo->ExceptionRecord->ExceptionCode == 0xC0000094)//除0异常
{
//将除数修改为2
pExcepInfo->ContextRecord->Ecx = 2;
//或者修改发生异常的代码的Eip idiv ecx长度2字节 从下一行开始执行
//pExcepInfo->ContextRecord->Eip = pExcepInfo->ContextRecord->Eip + 2;
return EXCEPTION_CONTINUE_EXECUTION;//已处理
}
return EXCEPTION_CONTINUE_SEARCH;//未处理
}

int main()
{
HMODULE hModule = GetModuleHandle(L"Kernel32.dll");
MyAddVectoredExceptionHandler = (FnAddVectoredExceptionHandler)::GetProcAddress(hModule, "AddVectoredExceptionHandler");
//参数1表示插入VEH链的头部, 0插入到VEH链的尾部
MyAddVectoredExceptionHandler(0, (_EXCEPTION_POINTERS*)&VectExcepHandler);
//构造除0异常
int val = 0;
_asm
{
xor edx, edx
xor ecx, ecx
mov eax, 100
idiv ecx
mov val, eax //结果在eax
}
printf("val = %d\n", val);
getchar();
}

上面是VEH

image-20200826144944366

主要是除0后调试器不会自动跟进异常处理函数中会直接跑起来

出题人往往在异常处理函数中写一些反调试

例题如SCTF2019某道逆向,一个AES,异常处理函数中判断是否调试然后改变密文

但那题是SEH不是VEH,总体形式很像

写起来不难,在异常处理函数中下断点就行,下面来介绍如何找到异常处理函数,要学会逆向首先得知道正向如何写

手动挂入SEH

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
#define _CRT_SECURE_NO_DEPRECATE
#include <stdio.h>
#include <windows.h>
#include <tlhelp32.h>
#include <stdlib.h>

struct _EXCEPTION
{
struct _EXCEPTION* Next;
DWORD Handler;
};

EXCEPTION_DISPOSITION _cdecl MyEexception_handler
(
_In_ struct _EXCEPTION_RECORD* _ExceptionRecord, //异常结构体
_In_ void* _EstablisherFrame, //SEH结构体地址
_Inout_ struct _CONTEXT* _ContextRecord, //存储异常发生时的各种寄存器的值 栈位置等
_Inout_ void* _DispatcherContext
)
{
if (_ExceptionRecord->ExceptionCode == EXCEPTION_INT_DIVIDE_BY_ZERO)
{
_ContextRecord->Ecx = 2;
return ExceptionContinueExecution;
}

return ExceptionContinueSearch;
}


int main()
{
DWORD temp;
_EXCEPTION Exception;//必须在当前线程的堆栈中

//fs[0]-> Exception
_asm
{
mov eax, fs: [0]
mov temp, eax
lea ecx, Exception
mov fs : [0] , ecx
}
//为SEH成员赋值
Exception.Next = (_EXCEPTION*)temp;
Exception.Handler = (DWORD)&MyEexception_handler;

//创建异常
int val = 0;
_asm
{
xor edx, edx
xor ecx, ecx
mov eax, 4
idiv ecx
mov val, eax //结果在eax
}
printf("val = %d\n", val);
//摘除刚插入的SEH
_asm
{
mov eax, temp
mov fs : [0] , eax
}
getchar();
}

上面就是手动挂seh代码,因为有safeseh保护vs2005以上版本都不能手动挂了,有绕过方式直接hook 系统seh处理函数即可。但这和本文讲的无关,关闭safeseh即可

image-20200826151313656

image-20200826151353283

image-20200826151459554

总的来说是要关注对fs:0的操作,seh挂入就是通过挂入fs:0,然后将Exception.Next改为原始fs:0指向

一般出题是的异常处理都不是像上面一样手动挂的,都是利用编译器支持的SEH

也就是try except

1
2
3
4
5
6
7
8
_try                               	1) 挂入链表
{

}
_except(过滤表达式) 2) 异常过滤
{
异常处理程序 3) 异常处理程序
}

编译器扩展SEH

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
#define _CRT_SECURE_NO_DEPRECATE
#include <stdio.h>
#include <windows.h>
#include <tlhelp32.h>
#include <stdlib.h>
int main()
{
int val = 0;
__try
{
_asm
{
xor edx, edx
xor ecx, ecx
mov eax, 100
idiv ecx
mov val, eax //结果在eax
}
}
_except(GetExceptionCode() == 0xC0000094 ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
printf("Exception Handling in CTF\n");
}
printf("val = %d\n", val);
getchar();
}

image-20200826153328810

这种在ctf中就比较常见了

当然还能换个形式

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
#define _CRT_SECURE_NO_DEPRECATE
#include <stdio.h>
#include <windows.h>
#include <tlhelp32.h>
#include <stdlib.h>

int ExceptFilter(LPEXCEPTION_POINTERS pExceptionInfo)
{
if (pExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_INT_DIVIDE_BY_ZERO)
{
pExceptionInfo->ContextRecord->Ecx = 2;
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}

int main()
{
int val;
_try
{
_asm
{
xor edx,edx
xor ecx,ecx
mov eax,100
idiv ecx
mov val,eax
}
}//GetExceptionInformation获取异常结构指针
_except(ExceptFilter(GetExceptionInformation()))
{
printf("Exception-Handling-in-CTF\n");
}
printf("val = %d\n", val);
getchar();
}

image-20200826153355707

注意上面这段代码,下面来介绍下这段代码具体干了些啥

1
2
3
4
typedef struct _EXCEPTION_REGISTRATION_RECORD {
struct _EXCEPTION_REGISTRATION_RECORD *Next;
PEXCEPTION_ROUTINE Handler;
} EXCEPTION_REGISTRATION_RECORD;

每写一个try except,编译器会自动帮我们挂入一个_EXCEPTION_REGISTRATION_RECORD结构体,写一个还行,如果写多个嵌套try except,那就得挂多个结构体,很占空间,所以就有了拓展 _EXCEPTION_REGISTRATION_RECORD结构体

1
2
3
4
5
6
7
struct _EXCEPTION_REGISTRATION{
struct _EXCEPTION_REGISTRATION *prev;
void (*handler)(PEXCEPTION_RECORD, PEXCEPTION_REGISTRATION, PCONTEXT, PEXCEPTION_RECORD);
struct scopetable_entry *scopetable;
int trylevel;
int _ebp;
};

image-20200826160415927

image-20200826160901647

1
2
3
4
5
6
struct scopetable_entry
{
DWORD previousTryLevel //上一个try{}结构编号
PDWRD lpfnFilter //过滤函数的起始地址
PDWRD lpfnHandler //异常处理程序的地址
}

在ida中查看

image-20200826161117320

loc_9D108A

image-20200826161205821

即是过滤函数

image-20200826161223519

loc_9D10C1就是except中的代码了

image-20200826161244822

写一个try可能看不出有啥用,嵌套几个即可

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
#define _CRT_SECURE_NO_DEPRECATE
#include <stdio.h>
#include <windows.h>
#include <tlhelp32.h>
#include <stdlib.h>

int ExceptFilter(LPEXCEPTION_POINTERS pExceptionInfo)
{
if (pExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_INT_DIVIDE_BY_ZERO)
{
pExceptionInfo->ContextRecord->Ecx = 2;
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}

int main()
{
int val;
_try
{
_asm
{
xor edx,edx
xor ecx,ecx
mov eax,100
idiv ecx
mov val,eax
}
_try
{
_asm
{
xor edx,edx
xor ecx,ecx
mov eax,50
idiv ecx
mov val,eax
}
_try
{
_asm
{
xor edx,edx
xor ecx,ecx
mov eax,10
idiv ecx
mov val,eax
}
}//GetExceptionInformation获取异常结构指针
_except(ExceptFilter(GetExceptionInformation()))
{
printf("Exception-Handling-in-CTF\n");
}
}//GetExceptionInformation获取异常结构指针
_except(ExceptFilter(GetExceptionInformation()))
{
printf("Exception-Handling-in-CTF\n");
}
}//GetExceptionInformation获取异常结构指针
_except(ExceptFilter(GetExceptionInformation()))
{
printf("Exception-Handling-in-CTF\n");
}
_try
{
_asm
{
xor edx, edx
xor ecx, ecx
mov eax, 10
idiv ecx
mov val, eax
}
}//GetExceptionInformation获取异常结构指针
_except(ExceptFilter(GetExceptionInformation()))
{
printf("Exception-Handling-in-CTF\n");
}

printf("val = %d\n", val);
getchar();
}

image-20200826162439938

在进入不同异常处理时都会赋值不同try level

image-20200826162057326

操作系统根据根据trylevel 选择scopetable数组然后调用scopetable数组中对应的lpfnFilter函数,如果lpfnFilter函数返回0 向上遍历 直到previousTryLevel=-1。

未处理异常

这种在ctf出现的话就比上面稍微难一些

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
#define _CRT_SECURE_NO_DEPRECATE
#include <stdio.h>
#include <windows.h>
#include <tlhelp32.h>
#include <stdlib.h>

long _stdcall ExceptFilter(LPEXCEPTION_POINTERS pExceptionInfo)
{
if (pExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_INT_DIVIDE_BY_ZERO)
{
pExceptionInfo->ContextRecord->Ecx = 2;
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}

int main()
{
SetUnhandledExceptionFilter(ExceptFilter);
int val;
_try
{
_asm
{
xor edx,edx
xor ecx,ecx
mov eax,100
idiv ecx
mov val,eax
}
}//GetExceptionInformation获取异常结构指针
_except(UnhandledExceptionFilter(GetExceptionInformation()))
{
//printf("Exception-Handling-in-CTF\n");
}
printf("val = %d\n", val);
getchar();
}

直接运行没啥问题

image-20200826164241033

但是调试的话就会闪退

在这种情况下只有程序不在调试时才会去处理异常

1
2
3
4
5
6
7
UnhandledExceptionFilter的执行流程:

1.通过NtQueryInformationProcess查询当前进程是否正在被调试,如果是,返回EXCEPTION_CONTINUE_SEARCH(0),此时会进入第二轮分发
2.如果没有被调试:
查询是否通过SetUnhandledExceptionFilter注册处理函数 如果有就调用
如果没有通过SetUnhandledExceptionFilter注册处理函数 弹出窗口 让用户选择终止程序还是启动即时调试器
如果用户没有启用即时调试器,那么该函数返回EXCEPTION_EXECUTE_HANDLER

NtQueryInformationProcess

这个函数是Ntdll.dll中一个原生态API,它用来提取一个给定进程的信息。它的第一个参数是进程句柄,第二个参数告诉我们它需要提取进程信息的类型。为第二个参数指定特定值并调用该函数,相关信息就会设置到第三个参数。第二个参数是一个枚举类型,其中与反调试有关的成员有ProcessDebugPort(0x7)、ProcessDebugObjectHandle(0x1E)和ProcessDebugFlags(0x1F)。例如将该参数置为ProcessDebugPort,如果进程正在被调试,则返回调试端口,否则返回0

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
BOOL CheckDebug()
{
int debugPort = 0;
HMODULE hModule = LoadLibrary("Ntdll.dll");
NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess");
NtQueryInformationProcess(GetCurrentProcess(), 0x7, &debugPort, sizeof(debugPort), NULL);
return debugPort != 0;
}

BOOL CheckDebug()
{
HANDLE hdebugObject = NULL;
HMODULE hModule = LoadLibrary("Ntdll.dll");
NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess");
NtQueryInformationProcess(GetCurrentProcess(), 0x1E, &hdebugObject, sizeof(hdebugObject), NULL);
return hdebugObject != NULL;
}

BOOL CheckDebug()
{
BOOL bdebugFlag = TRUE;
HMODULE hModule = LoadLibrary("Ntdll.dll");
NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess");
NtQueryInformationProcess(GetCurrentProcess(), 0x1E, &bdebugFlag, sizeof(bdebugFlag), NULL);
return bdebugFlag != TRUE;
}

总结

image-20200826164943966

文章目录
  1. 1. SEH VEH
    1. 1.1. 手动挂入SEH
    2. 1.2. 编译器扩展SEH
    3. 1.3. 未处理异常
      1. 1.3.1. NtQueryInformationProcess
  2. 2. 总结
|