异常处理机制流程分析

2019-07-08 约 1421 字 预计阅读 7 分钟

声明:本文 【异常处理机制流程分析】 由作者 playmak3r 于 2019-07-08 10:55:00 首发 先知社区 曾经 浏览数 111 次

感谢 playmak3r 的辛苦付出!

前言

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

关键词:[‘安全技术’, ‘二进制安全’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now