软件漏洞分析入门(五)

异常机制探索

异常实现机制

程序在运行的时候,经常会遇到各种各样的错误,为了程序以及系统遇到错误的时候也保持好的健壮性,异常处理机制应运而生

异常处理机制简单来说,就是当异常发生的时候,系统先抛出异常,程序先检查一遍,看看自己有没有能处理这种异常的函数,能处理固然好,如果不能处理的话就继续传给系统,即默认异常处理,随机终止进程

SEH,Structure Exception Handler,异常处理结构体,是一种 Windows 在异常处理中用到的一种数据结构。SEH 包含一个 SEH 链表指针以及应该异常处理函数句柄,栈中的多个 SEH 通过链表指针在栈内由栈顶向栈底串成单向链表,位于链表最顶端的 SEH 通过 TEB(线程环境块)0 字节偏移处的指针标识。下面我们分别实验几种不同的异常处理器的注册方式

使用C语言添加SEH

C语言中添加一个异常处理器非常简单,只用 __try 将要检查的代码包起来,在 __except 中编写异常处理代码,形如:

__try{
    
    
// 监控的代码
}	
__except ( expression ){
    
    
// 异常处理代码
}	

下面是使用C语言注册异常处理器的一个代码实例

#include <stdio.h> 
#include <windows.h> 
int main(){
    
    
int a =0, b =1, c =0;
 __try
{
    
    
  c = b / a;//触发除零异常
}

__except(EXCEPTION_EXECUTE_HANDLER)
{
    
    
  MessageBox(NULL, TEXT("integer divide by zero."),
  //弹窗提示除0异常
  TEXT("ERROR"), MB_OK);
   //scanf("%d",&a);;
   a =1; //修正除0错误
   c = b / a;
}
printf("b / a = %d\n", c);
printf("sucess\n");
system("pause");
return 0;
}

弹窗如图所示

image-20220604213204502

抛出异常之后自动处理异常,解决掉除0的问题

image-20220604213244880

使用内联汇编块注册SEH

下面是使用汇编嵌入式注册SEH的实例代码

#include <stdio.h>
#include <windows.h>

char pw[10] = "SEH";
char input[1024] = {
    
    0};

//异常处理函数
EXCEPTION_DISPOSITION
__cdecl
_except_handler( struct _EXCEPTION_RECORD *ExceptionRecord,
                void * EstablisherFrame,
                struct _CONTEXT *ContextRecord,
                void * DispatcherContext )
{
    
    
	//异常处理函数逻辑
	if(strcmp(input, pw) == 0){
    
    
		printf("ok, you are here. Congratulation!\n");
		system("pause");
	    exit(0);//完成口令匹配后结束进程——否则因为没有对异常代码进行处理,所以该进程会一直在触发异常和异常处理之间循环。
	}
	else{
    
    
		printf("ok, you are here. But your pw is wrong!\n");
	}
	
	
	//返回,告知os继续执行异常代码
	return ExceptionContinueExecution;
}


int main(){
    
    

	int a = 1, c = 0;

	int res;

	printf("please input password : ");
	scanf("%s", input);

	DWORD handler = (DWORD)_except_handler;

	//注册异常处理函数
	__asm{
    
    
		push handler;
		push FS:[0];
		mov FS:[0], ESP;
	}

	//设计的除零异常
	res = a / c;

	//如果不处理异常则会运行至此
	printf("What a pity, you found a wrong way.\n");
	system("pause");
	return -1;
}

我们把汇编注册SEH的代码块拎出来

DWORD handler = (DWORD)_except_handler;
//注册异常处理函数
__asm{
	push handler;
	push FS:[0];
    mov FS:[0], ESP;
}

结合上面的完整代码,可以看出来,这段汇编的作用就是把 handler 这个 DWORD 数据类型给压入栈,也就是把我们上面写的异常处理函数地址先压入栈,然后把SEH链表的表头压入栈,然后再在把表头 FS:[0] 修改为当前ESP的地址 ,这个指针 FS:[0] 指向的位置就是 TEB(线程环境块) 的首地址,相当于是在SEH链表里面插入一个程序自定义的异常处理函数

实际上我们如果用 VS2010 编译这个代码,这个内联的SEH注册汇编代码块会注册失败,我们不管输入什么都会回显 What a pity, you found a wrong way.

image-20220604234536126

这是因为 VS2010 使用的平台工具集默认是 v100,就算往前选一个历史版本也是v90,而我们需要的编译器版本是 VC6.0,我们转到 VC++ 6.0 编译一下相同的代码

image-20220604235131012

运行编译好的程序,随便输入一个错误的 777777

image-20220604235624804

这里是因为进入异常处理函数里面的错误分支之后没有中断,所以一直循环,我们输入正确密码 SEH

image-20220604235838528

SetUnhandledExceptionFilter函数设置自定义的异常处理器

为了使程序逻辑比普通的反调试更加复杂,混淆,更难被调试,我们可以使用 SetUnhandledExceptionFilter() 函数设置自定义异常处理器,这个函数的作用是将默认异常处理器设置为自定义的异常处理器,而且只有在非调试状态下才会被调用,下面贴出实例代码

#include <stdio.h>
#include <windows.h>

char pw[10] = "SEH";
char input[1024] = {
    
    0};

EXCEPTION_DISPOSITION
__cdecl
_except_handler( struct _EXCEPTION_RECORD *ExceptionRecord,
                void * EstablisherFrame,
                struct _CONTEXT *ContextRecord,
                void * DispatcherContext )
{
    
    
	if(strcmp(input, pw) == 0){
    
    
		printf("ok, you are here. Congratulation!\n");
	}
	else{
    
    
		printf("ok, you are here. But your pw is wrong!\n");
	}
	system("pause");
	exit(0);


	return ExceptionContinueExecution;
}

int main(){
    
    

	int a = 1, c = 0;

	int res;

	printf("please input password : ");
	scanf("%s", input);

	SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER )_except_handler);

	res = a / c;

	printf("What a pity, you found a wrong way.\n");
	system("pause");
	return -1;
}

同样使用 VC++ 6.0编译 输入错误的密码 777777 显示错误

image-20220605020410794

输入正确密码 SEH

image-20220605020455957

逆向SEH实例

上面我们复现了几种异常处理流程实现的机制,现在我们站到程序的对立面,假如我们不知道这个程序的正确密码,如何逆向破解出密码呢?

我们先用第二个实例编译程序尝试逆向,我们先把程序拖到 IDA 里面

image-20220605045616462

会发现默认主函数里面是不包含异常处理里面的函数的,而且主函数里面也没有跳转到其他代码块的控制流

我们尝试用 x32dbg 动态调试一下,一直运行到发生除0异常,程序中止

image-20220605204931568

这个时候看一程序的 SEH 链

image-20220605205715882

由于 SEH 链表是从表头开始添加的,所以这里的第一个 SEH 地址就是程序自定义的异常处理函数,我们直接进去看看,会发现这一段就是程序自定义的异常处理流程,其中 strcmp 函数比较的就是我们要找的密码 SEH

image-20220605210205338

接下来我们继续尝试逆向一下用 SetUnhandledExceptionFilter 函数反调试的程序

我们直接用 x32dbg 开始动态调试,同样一直运行到除0异常发生

image-20220605211007209

同样进入SEH链视角,但这次我们发现除了默认的异常处理 SEH,程序自己注册的 SEH 并没有出现在 SEH链里面,为什么呢?

image-20220605211549839

我们前文提到过,SetUnhandledExceptionFilter 函数只有在非调试状态下才会被调用,而我们用 x32dbg 进行调试,程序是以 debug 模式运行,自然也就不会转到用 SetUnhandledExceptionFilter 函数注册的异常处理函数里面

那么如何解决这一问题呢,我们可以用 release版本直接运行这个程序,然后手动给他加个 int 3 断点,再用动态调试的软件 ollydbg 或者 x32dbg attach 到程序上,这个时候就可以在触发异常的时候查看真正的SEH链了

不过这道题说实话用不到这一招,我们直接在 x32dbg 里面搜索密码错误的那个字符串 ok, you are here. But your pw is wrong!

image-20220605213018903

直接搜索到了这些字符串对应的位置

image-20220605213226250

我们直接进入这段隐藏起来的流程,依然能找到正确的密码

image-20220605213303144

既然都用了字符串搜索,IDA能不能也做到静态搜索密码呢?

答案是可以的,我们查看字符串,进入这段异常处理流程

image-20220605213523432

交叉引用一下找到对应的汇编位置

image-20220605213807332

由于这个程序设计的时候密码以明文存放,我们可以在内存里面轻松找到

image-20220605213822084

众所周知,SEH存放在栈中,也就是说,只要构造巧妙,我们可以设计一段数据恰好淹没SEH异常函数的调用地址,让地址指向ShellCode,当异常发生时程序会把ShellCode当初异常处理函数进行调用,这就是为啥SEH这么重要

猜你喜欢

转载自blog.csdn.net/SimoSimoSimo/article/details/125742484