上次发帖介绍了一下64位Shellcode异常处理的(链接:),有朋友发消息说想看一下32位的,所以抽空做了一点尝试。
Win32的异常处理最经典的是Matt Pietrek写的文章,这是一篇翻译的:。
32位的异常挂接比较简单,下面的代码是从上文复制过来的:
DWORD scratch;
EXCEPTION_DISPOSITION
__cdecl
_except_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext )
{
unsigned i;
// Indicate that we made it to our exception handler
printf( "Hello from an exception handler\n" );
// Change EAX in the context record so that it points to someplace
// where we can successfully write
ContextRecord->Eax = (DWORD)&scratch;
// Tell the OS to restart the faulting instruction
return ExceptionContinueExecution;
}
int main()
{
DWORD handler = (DWORD)_except_handler;
__asm
{
// 创建 EXCEPTION_REGISTRATION 结构:
push handler // handler函数的地址
push FS:[0] // 前一个handler函数的地址
mov FS:[0],ESP // 装入新的EXECEPTION_REGISTRATION结构
}
__asm
{
mov eax,0 // EAX清零
mov [eax], 1 // 写EAX指向的内存从而故意引发一个错误
}
printf( "After writing!\n" );
__asm
{
// 移去我们的 EXECEPTION_REGISTRATION 结构记录
mov eax,[ESP] // 获取前一个结构
mov FS:[0], EAX // 装入前一个结构
add esp, 8 // 将 EXECEPTION_REGISTRATION 弹出堆栈
}
return 0;
}
原理其实就是将自己的异常处理函数挂接到TEB的异常列表里面。
但Win32如果想在ShellCode里面使用自己的异常处理,其实还是很困难的,原因在于Safe SEH。比如说上面的代码,如果使用VS2017编译为release版本,默认情况下,异常回调根本不会触发。所以比如说你将ShellCode注入到记事本,异常函数根本不会触发,记事本闪崩。网上也有一些解决方案,比如说使用向量异常接管,但这也失去了SEH的意义。详细介绍可以参考这篇文章: ,代码下载:
。当然,如果你是自己的Loader,那么可以通过修改编译选项来解决,但也失去了Shellcode的意义---因为写成Dll然后内存加载更好。
32位异常,不能在代码里面使用try...except语句了,一旦使用,编译器就会在编译的时候加入挂接函数了。必须像上面那样自己挂接。这个是跟64位最大不同的地方。另外,还有一个地方需要提一下的,就是异常处理的时候,展开时会调用到RtlUnwind函数,这个函数是会清除除了EBP和ESP外的所有寄存器的,所以一定要先保存。
例如(伪代码):
EXCEPTION_DISPOSITION
__cdecl
_except_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext )
{
if (ExceptionRecord->ExceptionFlags&&( cUnwinding || cUnwindingForExit))
return EXCEPTION_CONTINUE_SEARCH;//如果处于展开阶段,则不处理。
ExceptionRecord->ExceptionFlags ||= cUnwinding;
__asm
{
push ebp
mov ebp, esp
push ebx
push esi
push edi
//RtlUnwind(EstablisherFrame,pReturnAddress,ExceptionRecord,NULL);//展开
push dword ptr [retVal]
push dword ptr [ExceptionRecord]
push offset _returnAddress
push dword ptr [pEstablisherFrame]
mov eax, [RtlUnwind]
call eax
pop edi
pop esi
pop ebx
}
_returnAddress:
asm
{
MOV EBP,[EDI].TExcFrame.hEBP//恢复发生异常前保存的EBP
MOV EBX,[EDI].TExcFrame.desc//这个就是异常处理函数跳转前的地址,内容是:jmp _except_handler
ADD EBX,TExcDesc.instructions//加上5个字节,就等于Filter的地址了
JMP EBX //跳到异常处理后的地址继续执行
}
}
顺便提一下64位的入门资料,因为有朋友问过。
64位的异常可以参考这篇文章:,代码如下:
// sample code to demonstrate Windows RtlAddFunctionTable API
// (c) 2019 Peter Meerwald-Stadler, pmeerw@pmeerw.net
// 64-bit only, compile with: cl rtlft.c /link /fixed
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include "windows.h"
typedef uint8_t UBYTE;
typedef uint16_t USHORT;
typedef union _UNWIND_CODE {
struct {
UBYTE CodeOffset;
UBYTE UnwindOp : 4;
UBYTE OpInfo : 4;
};
USHORT FrameOffset;
} UNWIND_CODE, *PUNWIND_CODE;
typedef struct _UNWIND_INFO {
UBYTE Version : 3;
UBYTE Flags : 5;
UBYTE SizeOfProlog;
UBYTE CountOfCodes;
UBYTE FrameRegister : 4;
UBYTE FrameOffset : 4;
UNWIND_CODE UnwindCode[1];
/* UNWIND_CODE MoreUnwindCode[((CountOfCodes + 1) & ~1) - 1];
* OPTIONAL ULONG ExceptionHandler;
* OPTIONAL ULONG ExceptionData[]; */
} UNWIND_INFO, *PUNWIND_INFO;
typedef struct {
uint8_t code[0x1000];
RUNTIME_FUNCTION function_table[1];
UNWIND_INFO unwind_info[1];
} DYNSECTION;
static EXCEPTION_DISPOSITION handler(PEXCEPTION_RECORD ExceptionRecord, ULONG64 EstablisherFrame, PCONTEXT ContextRecord, PDISPATCHER_CONTEXT DispatcherContext) {
printf("handler!\n");
ContextRecord->Rip += 3;
return ExceptionContinueExecution;
}
int main() {
int ret;
RUNTIME_FUNCTION *q;
DYNSECTION *dynsection = VirtualAlloc(NULL, 0x2000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
uint8_t *code = dynsection->code;
size_t p = 0;
code[p++] = 0xb8; // mov rax, 42
code[p++] = 0x2a;
code[p++] = 0x00;
code[p++] = 0x00;
code[p++] = 0x00;
code[p++] = 0xc6; // mov byte [rax], 0 -- raises exception!
code[p++] = 0x00;
code[p++] = 0x00;
code[p++] = 0xc3; // ret
size_t trampoline = p;
code[p++] = 0x48; // mov rax,
code[p++] = 0xb8;
size_t patch_handler_address = p;
code[p++] = 0x00; // address to handler patched here
code[p++] = 0x00;
code[p++] = 0x00;
code[p++] = 0x00;
code[p++] = 0x00;
code[p++] = 0x00;
code[p++] = 0x00;
code[p++] = 0x00;
code[p++] = 0xff; // jmp rax
code[p++] = 0xe0;
DWORD64 dyn_base = 0;
q = RtlLookupFunctionEntry((DWORD64) code, &dyn_base, NULL);
printf("lookup 'code' %p %llx\n", q, dyn_base); // no function table entry
DWORD64 image_base = 0;
q = RtlLookupFunctionEntry((DWORD64) main, &image_base, NULL);
printf("lookup 'main' %p %llx\n", q, image_base); // there is a function table entry
dyn_base = (DWORD64) dynsection;
UNWIND_INFO *unwind_info = dynsection->unwind_info;
unwind_info[0].Version = 1;
unwind_info[0].Flags = UNW_FLAG_EHANDLER;
unwind_info[0].SizeOfProlog = 0;
unwind_info[0].CountOfCodes = 0;
unwind_info[0].FrameRegister = 0;
unwind_info[0].FrameOffset = 0;
*(DWORD *) &unwind_info[0].UnwindCode = (DWORD64) &code[trampoline] - dyn_base;
RUNTIME_FUNCTION *function_table = dynsection->function_table;
function_table[0].BeginAddress = (DWORD64) &code[0] - dyn_base; // set RVA of dynamic code start
function_table[0].EndAddress = (DWORD64) &code[trampoline] - dyn_base; // RVA of dynamic code end
function_table[0].UnwindInfoAddress = (DWORD64) unwind_info - dyn_base; // RVA of unwind info
*(DWORD64 *) &code[patch_handler_address] = (DWORD64) handler; // VA of handler
printf("main VA %016llx\n", (DWORD64) main);
printf("code VA %016llx\n", (DWORD64) code);
printf("function table VA %016llx\n", (DWORD64) function_table);
printf("unwind info VA %016llx\n", (DWORD64) unwind_info);
printf("handler VA %016llx\n", (DWORD64) handler);
printf("RUNTIME_FUNCTION begin RVA %08x, end RVA %08x, unwind RVA %08x\n",
function_table[0].BeginAddress, function_table[0].EndAddress,
function_table[0].UnwindInfoAddress);
printf("UNWIND_INFO handler RVA %08x\n", *(DWORD *) &unwind_info[0].UnwindCode);
if (!RtlAddFunctionTable(function_table, 1, dyn_base)) {
printf("RtlAddFunctionTable() failed, exit.\n");
exit(EXIT_FAILURE);
}
q = RtlLookupFunctionEntry((DWORD64) code, &dyn_base, NULL);
printf("lookup 'code' %p %llx\n", q, dyn_base); // should return address of function table entry
uint64_t (*call)() = (uint64_t (*)()) code;
uint64_t result = (*call)();
printf("result = %llx\n", result);
if (!RtlDeleteFunctionTable(function_table)){
printf("RtlDeleteFunctionTable() failed, exit.\n");
exit(EXIT_FAILURE);
}
return EXIT_SUCCESS;
}
从这个代码入手,有一个初步的认识后,自己写一个会产生异常的函数,用它来替换上面例子的异常代码和异常表,就可以调试自己的异常处理函数了。全部OK后,全部集成到ShellCode里面即可。