前言
异常常用于动态反调试技术。正常运行的进程发生异常时,在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分析
这里调用了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
SEH分析
继续向下走检验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
call UnhandledExceptionFilter
再继续向下走进入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进行返回