舒默哦 发表于 2021-4-17 00:13

一道反调试小题分析

本帖最后由 舒默哦 于 2021-4-17 01:47 编辑

这道题在网上下的,也不知道谁发的,文件我发上来了,想要分析的朋友可以下载下来搞搞,再来看我的帖子,对照下看是否有出入。

https://static.52pojie.cn/static/image/hrline/1.gif
## 正文


这是32位的一个控制台程序,启动后界面如下:



主函数如下:





当程序走到wsprintfA函数会引起异常,v10里面存的是sub_401360函数地址。
异常来自这个函数,在OD里跟进这个函数查看:
执行到401372产生了异常:


再次点击运行时,程序产生第二次异常卡住了。正常打开程序后,输入随机16位字符,也会走这个地方并且产生异常,程序并不会卡死,会提醒输入的flag错误。
那么。暂时可以推断该程序是注册了一个VEH或者注册了一个最顶层异常处理函数,因为在主函数中没有看见结构化异常处理的相关代码。## 分析反调试:

### 用户层异常流程:

先说明下异常在用户层的分发流程:
KiUserExceptionDispatcher->RtlDispatchException->VEH-> 没有VEH处理则 调用SEH。没有SEH,则查看是否有最顶层异常处理函数,没有程序就会exitprocess。
注意:用户态的异常如果在非调试状态下的话仅仅只有一轮的分发,而只有在调试状态下才会进行第二轮,再次判断调试器是否要接手异常。如果注册了最顶层异常处理函数,有调试器存在的情况下,顶级异常处理函数就会被跳过,否则就会被顶级处理函数接管。

由上面产生异常时,分析得出的结论:该程序可能注册了一个VEH或者注册了一个最顶层异常处理函数。那么在哪儿注册的呢?
首先查看是否有TLS表的存在:


TLS不存在,那么可以暂时在KiUserExceptionDispatcher下断,程序产生异常后,一步一步往下跟,看看有什么情况。

运气好,这儿看到RtlDispatchException函数的函数头被HOOK了:


接下来,可以用IDA打开分析:

再次跟进Handler:

跟进sub_401550函数,发现还注册了顶层异常处理函数。






### 过反调试:


既然知道注册了顶层异常处理函数,那么在NtQueryInformationProcess下断,时机是产生异常时下断。NtQueryInformationProcess会断下两次。
第一次的协议号是0x22,过滤掉,第二次协议号是0x7,与调试有关的,修改返回值,整个流程就通了。


第二次调用NtQueryInformationProcess返回时候,把sf置1,跳过inc esi这条指令。

随意输入16个字符,程序不会被卡住,正常退出了:

## 分析加密算法:

这个程序把整个加密过程放到了异常处理的流程中了,具体放到了sub_401600函数和handler函数里面了。### 1、先分析输入数据的流向:

产生异常前,输入的字符串数据分别被保存ebx、edx、ecx、esi中,此外eax=0x797963。




产生异常后,KiUserExceptionDispatcher->RtlDispatchException->jmp sub_401600




其中a1是_CONTEXT结构体,a2是EXCEPTION_RECORD。
a1+0xA0在_CONTEXT结构体中,恰好对应的是esi寄存器。


数据经过处理后被存放到了EXCEPTION_RECORD的ExceptionInformation中。



_EXCEPTION_RECORD 结构体如下:               

#define EXCEPTION_MAXIMUM_PARAMETERS 15 // maximum number of exception parameters
type struct _EXCEPTION_RECORD               
{                                                               
      DWORD ExceptionCode;                              //异常代码
      DWORD ExceptionFlags;                              //异常状态
      struct _EXCEPTION_RECORD* ExceptionRecord;      //下一个异常
      PVOID ExceptionAddress;                              //异常发生地址
      DWORD NumberParameters;                        //附加参数个数
      ULONG_PTR ExceptionInformation
      ;                //附加参数指针      
}



经过一些流程后,最后调用顶层异常处理函数,把加密后的flag放到eax、ebx、edx、ecx、esi、edi寄存器里。


LONG __stdcall TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *ExceptionInfo)
{
ExceptionInfo->ContextRecord->Eax = ExceptionInfo->ExceptionRecord->ExceptionInformation;
ExceptionInfo->ContextRecord->Ebx = ExceptionInfo->ExceptionRecord->ExceptionInformation;
ExceptionInfo->ContextRecord->Ecx = ExceptionInfo->ExceptionRecord->ExceptionInformation;
ExceptionInfo->ContextRecord->Edx = ExceptionInfo->ExceptionRecord->ExceptionInformation;
ExceptionInfo->ContextRecord->Edi = ExceptionInfo->ExceptionRecord->ExceptionInformation;
ExceptionInfo->ContextRecord->Esi = ExceptionInfo->ExceptionRecord->ExceptionInformation;
ExceptionInfo->ContextRecord->Eip = (DWORD)sub_401390;
SetUnhandledExceptionFilter(lpTopLevelExceptionFilter);
RemoveVectoredExceptionHandler(Handler);
return -1;
}

EIP修正为sub_401390:
下面sub_401390函数的主要功能:
1、最终把输入的flag作比较,都匹配成功了则返回1,失败返回0。
2、修改返回地址,把EIP改为了main函数的地址。



最后返回main函数0x401B6E处继续执行。


### 2、分析加密流程

输入的flag的会经过三次加密,最终和一个字符串对比,这个字符串是 "LMlWu2Y/Tk8c33Y+T8Lv0a==";匹配成功则flag输入正确。
第一次加密:

第二次加密:

第三次加密:Handler->sub_401110

_BYTE *__cdecl sub_401110(unsigned int a1, int *__shifted(EXCEPTION_RECORD,0x14) a2)
{
_BYTE *v3; //
unsigned int v4; //
unsigned int v5; //
unsigned int i; //
_BYTE *v7; //
_BYTE *v8; //
_BYTE *v9; //
_BYTE *v10; //
_BYTE *v11; //

v7 = calloc(4 * (a1 / 3) + 5, 1u);
v3 = v7;
for ( i = 0; i < 3 * (a1 / 3); i += 3 )
{
    LOBYTE(v4) = *((_BYTE *)ADJ(a2)->ExceptionInformation + i + 2);
    BYTE1(v4) = *((_BYTE *)ADJ(a2)->ExceptionInformation + i + 1);
    HIWORD(v4) = *((unsigned __int8 *)ADJ(a2)->ExceptionInformation + i);
    *v7 = byte_41E8B0[(v4 >> 18) & 0x3F];
    v8 = v7 + 1;
    *v8++ = byte_41E8B0[(v4 >> 12) & 0x3F];
    *v8++ = byte_41E8B0[(v4 >> 6) & 0x3F];
    *v8 = byte_41E8B0;
    v7 = v8 + 1;
}
if ( a1 != i )
{
    LOWORD(v5) = 0;
    HIBYTE(v5) = 0;
    if ( a1 - i == 2 )
    {
      BYTE1(v5) = *((_BYTE *)ADJ(a2)->ExceptionInformation + i + 1);
      BYTE2(v5) = *((_BYTE *)ADJ(a2)->ExceptionInformation + i);
      *v7 = byte_41E8B0[(v5 >> 18) & 0x3F];
      v9 = v7 + 1;
      *v9 = byte_41E8B0[(v5 >> 12) & 0x3F];
      v10 = v9 + 1;
      *v10 = byte_41E8B0[(v5 >> 6) & 0x3F];
    }
    else
    {
      BYTE2(v5) = *((_BYTE *)ADJ(a2)->ExceptionInformation + i);
      *v7 = byte_41E8B0[(v5 >> 18) & 0x3F];
      v11 = v7 + 1;
      *v11 = byte_41E8B0[(v5 >> 12) & 0x3F];
      v10 = v11 + 1;
      *v10 = 61;
    }
    v10 = 61;
}
return v3;
}




其中byte_41E8B0是一个字符串:



### 逆算法

逆算法,我直接贴代码了:

#define _CRT_SECURE_NO_WARNINGS
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>
#include <iostream>
#include <string>

typedef unsigned char   uint8;
#define _BYTEunsigned char
#define _WORDunsigned word
#define LOBYTE_(w)         ((BYTE)(((DWORD_PTR)(w)) & 0xff))
#define HIBYTE_(w)         ((BYTE)((((DWORD_PTR)(w)) >> 8) & 0xff))
#define BYTEn(x, n)   (*((_BYTE*)&(x)+n))
#define WORDn(x, n)   (*((_WORD*)&(x)+n))
#define BYTE0(x)   BYTEn(x,0)         // byte 0 (counting from 0)添加此宏定义
#define BYTE1(x)   BYTEn(x,1)         // byte 1 (counting from 0)
#define BYTE2(x)   BYTEn(x,2)
#define BYTE3(x)   BYTEn(x,3)
#define BYTE4(x)   BYTEn(x,4)

const char* byte_41E8B0 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/";
void reversecalc(char* incs)
{

    /*----------------------------------------------------------------------------------*/
    /*      逆算法第一步      ,也就是逆加密的第三步                                                   */
    /*----------------------------------------------------------------------------------*/
    std::string s = byte_41E8B0;
    //const char* str = "WlML/Y2uc8kT+Y33vL8T ==a0";
    const char* str = "LMlWu2Y/Tk8c33Y+T8Lv0a==";
    char ttt = str;
    //int incs = { 0 };
    //char* tempstr = (char*)str;
    char tv = { 0 };
    int xtemp = 0;
    int xtemp1 = 0;
    int xtemp2 = 0;
    int xtemp3 = 0;
    int i = 0;
    int j = 0;
    for (; i < strlen(str)-4; i+=4)
    {
      xtemp = s.find(str, 0);
      xtemp <<= 18;
      xtemp1 = s.find(str, 0);
      xtemp1 <<= 12;
      xtemp2 = s.find(str, 0);
      xtemp2 <<= 6;
      xtemp3 = s.find(str, 0);
      
      xtemp = (xtemp | xtemp1 | xtemp2 | xtemp3);

      BYTE byte_1, byte_2, byte_3, byte_4;
      unsigned intret;
      //int 类型 占4个字节,32位
      byte_1 = (xtemp & 0xff000000) >> 24;
      byte_2 = (xtemp & 0x00ff0000) >> 16;
      byte_3 = (xtemp & 0x0000ff00) >> 8;
      byte_4 = xtemp & 0x000000ff;
      incs = byte_2;
      incs = byte_3;
      incs = byte_4;
    }

    xtemp = s.find(str, 0);
    xtemp <<= 18;
    xtemp >>= 16;
    xtemp1 = s.find(str, 0);
    xtemp1 <<= 12;
    incs = xtemp;



    /*----------------------------------------------------------------------------------*/
    /*      逆算法第二步      ,也就是逆加密的第二步                                        */
    /*----------------------------------------------------------------------------------*/

    EXCEPTION_RECORD a2 = { 0};

    for (int i = 0; i < 4; i++)
    {
      a2.ExceptionInformation = *((int*)incs+i);
    }
   

    a2.ExceptionInformation ^= a2.ExceptionInformation;
    a2.ExceptionInformation ^= a2.ExceptionInformation;
    a2.ExceptionInformation ^= a2.ExceptionInformation;
    a2.ExceptionInformation ^= a2.ExceptionInformation;
    a2.ExceptionInformation ^= a2.ExceptionInformation;
    a2.ExceptionInformation ^= a2.ExceptionInformation;
    a2.ExceptionInformation ^= a2.ExceptionInformation;
    a2.ExceptionInformation ^= a2.ExceptionInformation;
    a2.ExceptionInformation ^= a2.ExceptionInformation;



    /*----------------------------------------------------------------------------------*/
    /*      逆算法第三步      ,也就是逆加密的第一步                                           */
    /*----------------------------------------------------------------------------------*/
    const char* v2 = "1234567890123456cyy";
    int tv2 = { 0,0xbe517013,0x34d7f69e,0x70133315 ,0xc6dfff05 };
    int temp = 0;
    for (int i = 4; i > 0; --i)
    {
      temp = a2.ExceptionInformation;
      if (i==4)
      {
            temp ^= *(DWORD*)((char*)v2 + i * 4);//(DWORD)v2;
      }
      else
      {
            temp ^=tv2;
      }

      __asm {
            mov eax, temp
            ror eax, 5
            mov temp, eax
      }
      tv2 = temp;
    }


    printf("%s\n", tv2 + 1);
    return;
}

int main()
{
    char a2 = { 0 };
    reversecalc(a2);
    system("pause");
    return 0;
}

运行之后最终得到:flag{Ex<ept10n~}

把flag{Ex<ept10n~}贴到窗口,成功了!


## 复盘

先找sub_401600函数的来源,是谁调用了。


sub_401600在sub_401680里引用了其地址。

再次查看sub_401680的来源,来源于sub_401000():

再追sub_401000的来源:

来到了.rdata数据区。看到const _PVFV First ,就知道是_initterm函数调用sub_401000,_initterm函数先于在main执行,这个函数的功能是初始化全局变量。
那么,程序大概是这样子:

//点号代表省略
......
int sub_401680()
{
    .......
}
.......
#pragma init_seg(".CRT$XCC")
int g_hook = sub_401680();

int main()
{
    .......
}


https://static.52pojie.cn/static/image/hrline/4.gif


复盘程序的整个流程:

1、_initterm先于main执行,调用sub_401680(),完成RtlDispatchException函数的HOOK。
2、执行main函数,输入flag之后,产生异常:KiUserExceptionDispatcher->RtlDispatchException->jmp sub_401600。
3、在sub_401600函数里面,注册了VEH,在VEH处理函数里面又注册了顶层异常处理函数,然后返回RtlDispatchException里继续执行。
此时,还没有注册了顶层异常处理函数,因为还没有调用VEH。
4、RtlDispatchException调用VEH查看异常是否被处理了,此时,在VEH函数里注册了顶层异常处理函数,返回RtlDispatchException,
调用UnhandledExceptionFilter查看程序是否在被调试,被调试则交给调试器,没有调试器就调用SetUnhandledExceptionFilter()注册的函数来处理,最终修改EIP返回main()。https://static.52pojie.cn/static/image/hrline/4.gif



跃龙门 发表于 2021-5-7 13:23

优秀, 反手一个收藏, 等哪年想起来了再学习哈哈哈

丿一叶知秋丶 发表于 2021-4-27 19:38

每到一个新的机制是,调试器都有一次处理的机会

哈哈先生No.5 发表于 2021-4-27 11:59

{:1_893:}学习学习,每天进步一小点

natsu27 发表于 2021-5-1 15:31

感谢分享,学习中

cpj1203 发表于 2021-4-30 01:15

慢慢研究吧!

舒默哦 发表于 2021-4-17 00:14

附件在这儿。

darkye 发表于 2021-5-24 20:47


没基础的熟悉三环和内核的异常流程就可以了,就是学起来有点枯燥

nmy124 发表于 2021-4-19 21:22

谢谢楼主分析,试着学习,再次谢谢

Lxx55 发表于 2021-11-9 22:53

分析得头头是到

alongzhenggang 发表于 2021-4-17 07:47

想要看懂理解需要哪些基础

舒默哦 发表于 2021-4-17 10:14

alongzhenggang 发表于 2021-4-17 07:47
想要看懂理解需要哪些基础
熟悉三环和内核的异常流程就可以了{:301_992:}

she383536296 发表于 2021-4-17 10:28

学习中,对我这样的新手来说有点难度

alongzhenggang 发表于 2021-4-17 10:36

舒默哦 发表于 2021-4-17 10:14
熟悉三环和内核的异常流程就可以了

纯小白:@没基础的,不过谢谢,去试着学习

哥比彩砖还炫 发表于 2021-4-18 14:27

火哥牛逼!

舒默哦 发表于 2021-4-18 17:47

哥比彩砖还炫 发表于 2021-4-18 14:27
火哥牛逼!

{:301_983:}火锅是谁

酱鸭腿 发表于 2021-4-19 21:07

学习中,对我这样的新手来说有点难度
页: [1] 2 3 4
查看完整版本: 一道反调试小题分析