前言

异常常用于动态反调试技术。正常运行的进程发生异常时,在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进行返回

点击收藏 | 0 关注 | 1
登录 后跟帖