异常常用于动态反调试技术。正常运行的进程发生异常时,在SEH(Structured Exception Handling)机制的作用下,OS会接收异常,然后调用进程中注册的SEH处理。但是,若进程正被调试器调试,那么调试器就会先于SEH接收处理。利用该特征可判断进程是正常运行还是调试运行,然后根据不同的结果执行不同的操作,这就是利用异常处理机制不同的反调试原理。
我们以一道题目为例
拿到题目放入IDA中查看程序主逻辑
v7 = 0; memset(&v8, 0, 0x4Fu); v10 = dword_401360; sub_401060("input your flag : ", v6); v5 = (int **)80; sub_4010D0("%s", (unsigned int)&v7); v13 = &v7; v12 = &v8; v13 += strlen(v13); v11 = ++v13 - &v8; if ( v13 - &v8 == 16 )//判断输入的长度是否为16 { v9 = &v4; v5 = &v10; wsprintfA(&v4, "%s", &v10); if ( (unsigned __int8)sub_401860(&v7) ) sub_401060("Good ,u success!\n", v6); else sub_401060("Wrong,u lose!\n", v6); j___fgetchar(); result = j___fgetchar(); } else { sub_401060("wrong u lose\n", v6); result = 0; } return result;
首先程序判断长度是否为16,接着进入401860进行判断。进入该函数进行判断,对输入的字符进行了一系列操作,但是401390的函数的参数是固定值。
bool __cdecl sub_401860(_BYTE *a1) { signed int i; // [esp+0h] [ebp-4h] *a1 *= 2; a1[1] >>= 3; a1[2] >>= 4; a1[3] >>= 88; a1[4]; a1[4] = 0; a1[7] ^= 0xAu; a1[8] += 61; a1[9] /= 8; a1[10] %= 4; a1[11] ^= 0xCu; for ( i = 0; i < 10; ++i ) a1[i] = sub_4017E0(a1[i], a1[i + 1]); a1[12] ^= a1[11]; a1[13] ^= a1[12]; a1[14] ^= a1[13]; a1[15] ^= a1[14]; return sub_401390(i) != 0; }
带着疑问进入401390函数看一下
sub_401390@<eax>(int a1@<eax>, int a2@<edx>, int a3@<ecx>, int a4@<ebx>, int a5@<ebp>, int a6@<edi>, int a7@<esi>, int a8, const char **a9, const char **a10) { *(_DWORD *)(a5 - 4) = a1; *(_DWORD *)(a5 - 8) = a4; *(_DWORD *)(a5 - 12) = a3; *(_DWORD *)(a5 - 16) = a2; *(_DWORD *)(a5 - 20) = a6; *(_DWORD *)(a5 - 24) = a7; if ( *(_DWORD *)(a5 - 4) == 1466715468 && *(_DWORD *)(a5 - 8) == 794374773 && *(_DWORD *)(a5 - 12) == 1664641876 && *(_DWORD *)(a5 - 16) == 727266099 && *(_DWORD *)(a5 - 20) == 1984706644 ) { *(_DWORD *)(a5 - 24); } return main(a8, a9, a10); }
程序实际有四个参数。。。动态调试一下发现程序wsprintf的返回处写入了401360
程序在401360处触发了异常
由于接下来需要解析的dll函数比较多于是我们在windbg中进行调试
1.由于该异常是用户态异常,KiDispatchException会试图将异常分发给用户态的调试器,如果DebugPort不为空,将异常发送给调试子系统,调试子系统将异常发送给调试器,如果处理了异常分发结束。
2.如果调试器没有处理该异常,KiDispatchException修改用户态栈,返回用户层之后执行KiUserExceptionDispatcher,此函数会调用RtlDispatchException来寻找异常处理器,首先遍历VEH,然后遍历SEH。如果RtlDispatchException返回FALSE,并且当前进程在被调试,那么KiUserExceptionDispatcher会调用ZwRaiseException并将FirstChance设置为FALSE,进行第二轮分发。如果没有被调试,结束进程。将异常发送给调试子系统,调试子系统将异常发送给调试器,如果处理了异常分发结束。
3.ZwRaiseException会通过内核服务NtRaiseException把异常传递给KiDispatchException来进行分发。第二次,将异常传递给调试器,如果没有处理将异常分配给ExceptionPort异常端口监听者处理,如果返回FALSE,结束进程。
首先我们先在KiUserDispatcher上下断点。
首先判断是32位异常还是64位异常。继续往下走调用*RtlDisPatchException寻找异常处理器,进入该函数,程序跳转到4016000(程序应该是hook了KiUserExceptionDispatcher中的调用异常handler的call)。
void __usercall sub_401600(int a1@<ecx>, int a2@<ebx>) { int v2; // eax unsigned int v3; // et0 signed int v4; // ecx unsigned int v5; // [esp-24h] [ebp-2Ch] v2 = a1 + 160; if ( *(__int16 **)(a2 + 12) == &word_401372 ) { v3 = __readeflags(); v5 = v3; v4 = 4; do { *(_DWORD *)(a2 + 4 * v4 + 16) = *(_DWORD *)(v2 + 4 * v4) ^ __ROL4__(*(_DWORD *)(v2 + 4 * v4 - 4), 5); --v4; } while ( v4 ); *(_DWORD *)(a2 + 24) ^= *(_DWORD *)(a2 + 20); *(_DWORD *)(a2 + 20) ^= *(_DWORD *)(a2 + 24); *(_DWORD *)(a2 + 24) ^= *(_DWORD *)(a2 + 20); *(_DWORD *)(a2 + 28) ^= *(_DWORD *)(a2 + 20); *(_DWORD *)(a2 + 20) ^= *(_DWORD *)(a2 + 28); *(_DWORD *)(a2 + 28) ^= *(_DWORD *)(a2 + 20); *(_DWORD *)(a2 + 32) ^= *(_DWORD *)(a2 + 24); *(_DWORD *)(a2 + 24) ^= *(_DWORD *)(a2 + 32); *(_DWORD *)(a2 + 32) ^= *(_DWORD *)(a2 + 24); __writeeflags(v5); AddVectoredExceptionHandler(0, Handler); } JUMPOUT(__CS__, (char *)lpAddress + 5); }
将字符串拆分成四组
第四组循环左移5位之后与0x797963异或存入内存中
取第三组循环左移5位与第四组(输入的最后4个)异或存入内存中
取第二组循环左移5位与第三组异或存入内存中
取第一组循环左移5位与第二组异或存入内存中
之后四组数据又相互异或,设这四组数据位 o p q r
p=p^o
o=o^(p^o)=p
p=p^o^p=o
q=q^p
o=p^(q^p)=q
q=(q^p)^q=p
r=r^o
p=o^(r^o)=r
r=(r^o)^r=o
总的来说四个值进行了互换
这里调用了VEH向量
继续向下运行对VEHAdress进行了Decode
执行完之后发现其返回值EAX为0x401570
继续向下走到调用0x401750的函数
跟进VEH函数,在IDA中查看VEH函数
LONG __stdcall Handler(struct _EXCEPTION_POINTERS *ExceptionInfo) { UINT_PTR *v1; // eax UINT_PTR *v2; // ecx _DWORD *v3; // edx int v4; // eax v1 = (UINT_PTR *)sub_401110(0x10u, (int)ExceptionInfo->ExceptionRecord->ExceptionInformation); v2 = ExceptionInfo->ExceptionRecord->ExceptionInformation; *v2 = *v1; v2[1] = v1[1]; v2[2] = v1[2]; v2[3] = v1[3]; v2[4] = v1[4]; v2[5] = v1[5]; v3 = *(_DWORD **)(__readfsdword(0x18u) + 8); dword_41F304 = (int)v3; *v3 = v4; v3[1] = sub_401550; return 0; }
进入函数401110进行base64加密,但是字母表发生了变化
{ unsigned int v2; // ST10_4 _BYTE *v3; // ST1C_4 _BYTE *v4; // ST1C_4 _BYTE *v5; // ST1C_4 _BYTE *v7; // [esp+0h] [ebp-18h] unsigned int v8; // [esp+Ch] [ebp-Ch] unsigned int i; // [esp+10h] [ebp-8h] _BYTE *v10; // [esp+14h] [ebp-4h] _BYTE *v11; // [esp+14h] [ebp-4h] v10 = calloc(4 * (a1 / 3) + 5, 1u); v7 = v10; for ( i = 0; i < 3 * (a1 / 3); i += 3 ) { LOBYTE(v2) = *(_BYTE *)(i + a2 + 2); BYTE1(v2) = *(_BYTE *)(i + a2 + 1); HIWORD(v2) = *(unsigned __int8 *)(i + a2); *v10 = byte_41E8B0[(v2 >> 18) & 0x3F]; v3 = v10 + 1; *v3++ = byte_41E8B0[(v2 >> 12) & 0x3F]; *v3++ = byte_41E8B0[(v2 >> 6) & 0x3F]; *v3 = byte_41E8B0[v2 & 0x3F]; v10 = v3 + 1; } if ( a1 != i ) { LOWORD(v8) = 0; HIBYTE(v8) = 0; if ( a1 - i == 2 ) { BYTE1(v8) = *(_BYTE *)(i + a2 + 1); BYTE2(v8) = *(_BYTE *)(i + a2); *v10 = byte_41E8B0[(v8 >> 18) & 0x3F]; v4 = v10 + 1; *v4 = byte_41E8B0[(v8 >> 12) & 0x3F]; v11 = v4 + 1; *v11 = byte_41E8B0[(v8 >> 6) & 0x3F]; } else { BYTE2(v8) = *(_BYTE *)(i + a2); *v10 = byte_41E8B0[(v8 >> 18) & 0x3F]; v5 = v10 + 1; *v5 = byte_41E8B0[(v8 >> 12) & 0x3F]; v11 = v5 + 1; *v11 = 61; } v11[1] = 61; } return v7; }
gu执行函数至返回,判断VEH是否处理完,若处没有处理完则跳转,继续处理剩余的VEH
继续向下走检验SEHhandler的地址是否合法,如果合法则进行跳转否则一直循环直到倒数第二个SEH进行内部调用call UnhandledExceptionFilter进行处理,如果还不能处理就停止运行:
显然这个SEH是合法的,我们继续跟进RtlExcuteHandlerForException,这里对SEHHandler进行调用
函数执行401550,跟进函数
执行函数setUnhandExceptionFilter用来捕获这个异常,而处理异常的函数就为TopLevelException
signed int sub_401550() { lpTopLevelExceptionFilter = SetUnhandledExceptionFilter(TopLevelExceptionFilter); return 1; }
SEH一次执行完之后,进行判断是否处理好了异常 eax=1,显然没有执行好
之后又执行了下一个SEH,这次SEH执行的程序如下
int __cdecl SEH_4158F0(PEXCEPTION_RECORD ExceptionRecord, PVOID TargetFrame, int a3) { _DWORD *v3; // esi _DWORD *v4; // ebx int v5; // edi int v6; // eax int (__fastcall *v7)(_DWORD, _DWORD); // ecx int *v8; // eax int v9; // ebx int v10; // eax char v11; // cl EXCEPTION_RECORD *v12; // eax void (*v13)(void); // esi PEXCEPTION_RECORD v15; // [esp+Ch] [ebp-1Ch] int v16; // [esp+10h] [ebp-18h] char *v17; // [esp+14h] [ebp-14h] int *v18; // [esp+18h] [ebp-10h] int v19; // [esp+1Ch] [ebp-Ch] _DWORD *v20; // [esp+20h] [ebp-8h] char v21; // [esp+27h] [ebp-1h] v3 = TargetFrame; v21 = 0; v19 = 1; v4 = (_DWORD *)(__security_cookie ^ *((_DWORD *)TargetFrame + 2)); v17 = (char *)TargetFrame + 16; v20 = v4; ValidateLocalCookies(v4, (int)TargetFrame + 16); nullsub_1(a3); if ( ExceptionRecord->ExceptionFlags & 0x66 ) { if ( *((_DWORD *)TargetFrame + 3) != -2 ) { _EH4_LocalUnwind((int)TargetFrame, -2, (int)TargetFrame + 16, (int)&__security_cookie); goto LABEL_21; } } else { v15 = ExceptionRecord; v16 = a3; v5 = *((_DWORD *)TargetFrame + 3); *((_DWORD *)TargetFrame - 1) = &v15; if ( v5 != -2 ) { while ( 1 ) { v6 = v5 + 2 * (v5 + 2); v7 = (int (__fastcall *)(_DWORD, _DWORD))v4[v6 + 1]; v8 = &v4[v6]; v9 = *v8; v18 = v8; if ( v7 ) { v10 = _EH4_CallFilterFunc(v7); v11 = 1; v21 = 1; if ( v10 < 0 ) { v4 = v20; v19 = 0; goto LABEL_21; } if ( v10 > 0 ) { v12 = ExceptionRecord; if ( ExceptionRecord->ExceptionCode == -529697949 && dword_41F32C ) { if ( _IsNonwritableInCurrentImage(&dword_41F32C) ) { v13 = (void (*)(void))dword_41F32C; j_nullsub_1(dword_41F32C, ExceptionRecord, 1); v13(); v3 = TargetFrame; } v12 = ExceptionRecord; } _EH4_GlobalUnwind2(v3, v12); if ( v3[3] != v5 ) _EH4_LocalUnwind((int)v3, v5, (int)(v3 + 4), (int)&__security_cookie); v3[3] = v9; ValidateLocalCookies(v20, (int)(v3 + 4)); _EH4_TransferToHandler(v18[2], v3 + 4); __debugbreak(); JUMPOUT(*(_DWORD *)__vcrt_initialize); } } else { v11 = v21; } v5 = v9; if ( v9 == -2 ) break; v4 = v20; } if ( v11 ) { v4 = v20; LABEL_21: ValidateLocalCookies(v4, (int)v17); return v19; } } } return v19; }
函数很长,然而真正要执行的函数TransferHandler中,但是在本题中由于执行了异常过滤(_EH4_CallFilterFunc),使得SEH没有执行,SEH依然没有执行好
继续分析下一个SEH,为except_handler4 跟进之后为 ntdll!_except_handler4_common ,再进入这个函数找到函数 ntdll!_EH4_CallFilterFunc 跟进这个SEH
再继续向下走进入RtlUserThreadStart+0x398a7 跟进去,继续走再跟进这个函数UnhandledExceptionFilter,而如果这个函数不能处理就停止运行
继续向下运行,这里判断查询是否有DebugPort,如果有调试器则把异常给调试器处理,否则则执行TopLevelExceptionHandle进行处理
程序的正常执行流程必然是没有经过调试,而此处交给了调试器处理,使得程序无法进入正常回调,因此我们需要将此处返回值eax(1)修改为0(r @eax=0)继续向下走,找到了执行了TopLevelExceptionHandler(0x401470)的地方
LONG __stdcall TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *ExceptionInfo) { ExceptionInfo->ContextRecord->Eax = ExceptionInfo->ExceptionRecord->ExceptionInformation[0]; ExceptionInfo->ContextRecord->Ebx = ExceptionInfo->ExceptionRecord->ExceptionInformation[1]; ExceptionInfo->ContextRecord->Ecx = ExceptionInfo->ExceptionRecord->ExceptionInformation[2]; ExceptionInfo->ContextRecord->Edx = ExceptionInfo->ExceptionRecord->ExceptionInformation[3]; ExceptionInfo->ContextRecord->Edi = ExceptionInfo->ExceptionRecord->ExceptionInformation[4]; ExceptionInfo->ContextRecord->Esi = ExceptionInfo->ExceptionRecord->ExceptionInformation[5]; ExceptionInfo->ContextRecord->Eip = (DWORD)sub_401390; SetUnhandledExceptionFilter(lpTopLevelExceptionFilter); RemoveVectoredExceptionHandler(Handler); return -1; }
TopLevelExceptionFilter中先设置各个寄存器的值。
接着设置异常处理函数lpTopLevelExceptionFilter(0x41f308)函数直接返回,并且将VEH删除
此时SEH处理完毕
我们继续走程序走到正常的异常回调
然而前面TOPLevelException函数中有一句 ExceptionInfo->ContextRecord->Eip = (DWORD)sub_401390,设置eip为0x401360
使得NtContinue之后返回401390进行判断
int __usercall sub_401390@<eax>(int a1@<eax>, int a2@<edx>, int a3@<ecx>, int a4@<ebx>, int a5@<ebp>, int a6@<edi>, int a7@<esi>, int a8, const char **a9, const char **a10) { *(_DWORD *)(a5 - 4) = a1; *(_DWORD *)(a5 - 8) = a4; *(_DWORD *)(a5 - 12) = a3; *(_DWORD *)(a5 - 16) = a2; *(_DWORD *)(a5 - 20) = a6; *(_DWORD *)(a5 - 24) = a7; if ( *(_DWORD *)(a5 - 4) == 'WlML' && *(_DWORD *)(a5 - 8) == '/Y2u' && *(_DWORD *)(a5 - 12) == 'c8kT' && *(_DWORD *)(a5 - 16) == '+Y33' && *(_DWORD *)(a5 - 20) == 'vL8T' ) { *(_DWORD *)(a5 - 24); } return main(a8, a9, a10); }
这一段字符进行比较判断。
总体来说程序的加密流程为,4轮异或,再进行base64加密,但是最后加密生成的数据应为18个字符带两个'='字符,解密只能解密前15个字符,但是还需注意的是字母表进行了变换,新字母表为
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/
先进行base64解密
import binascii base64_table='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/' base_encode=str(raw_input(u"请输入解密字符")) counter=base_encode.count("=") length=len(base_encode) encode="" encode_re="" if(counter==2): a=base64_table.find(base_encode[length-4:length-3])#取前六位 a=a<<2 b=base64_table.find(base_encode[length-3:length-2])#取2位 b=b>>4 encode_re=chr(a+b) if(counter==1): a=base64_table.find(base_encode[length-4:length-3])#第一个字符前6位 a=a<<2 b=base64_table.find(base_encode[length-3:length-2])#第二个字符前2位 b=b>>4 encode_re1=chr(a+b) a=base64_table.find(base_encode[length-3:length-2])#第二个字符后4位 a=(a&0xf)<<4 b=base64_table.find(base_encode[length-2:length-1])#第三个字符前4位 b=b>>2 encode_re2=chr(a+b) encode_re=encode_re1+encode_re2 length=length-4 if(counter==0): length=length+4 for i in range(0,length,4):#以4个字符为一组 a=base64_table.find(base_encode[i:i+1])#第一个字符6位 a=a<<2 b=base64_table.find(base_encode[i+1:i+2])#第二个字符前2位 b=b>>4 encode=encode+chr(a+b) a=base64_table.find(base_encode[i+1:i+2])#第二个字符后4位 a=((a&0xf)<<4) b=base64_table.find(base_encode[i+2:i+3])#第三个字符前4位 b=b>>2 encode=encode+chr(a+b) a=base64_table.find(base_encode[i+2:i+3])#取第三个字符后2位 a=(a&3)<<6 b=base64_table.find(base_encode[i+3:i+4])#取第四个字符6位 encode=encode+chr(a+b) encode=encode+encode_re print( binascii.b2a_hex(encode)) print('\n')
得到结果9662f053 6cbfb4af 02df7cbe b7c955?? 最后一个字符的ASCII码无法确定(分别对应着q,r,p,o)
(afb4bf6c)^797963再循环右移5位得到7d7e6e30(}~n0)
(53f06296)^7d7e6e30在循环右移得到31747065(1tpe)
(be7cdf02)^31747065循环右移5位3c78457b(<xE{)
最后一组虽然无法解密但是可以推测位flag
最终flag为flag{Ex<ept10n~}
32位异常正常的处理流程如下
1.KiUserDispatcher中判断是64位异常还是32位异常
2.VEH链处理
3.SEH链处理,倒数第二个SEH CALL UnHandledExceptionFliter (会调用NtQueryInformationProcess查询DebugPort),然后调用TopLevelExceptionFilter处理
4.zwcontinue进行返回