HWS2021——Enigma

前言

这是一道来自华为2021硬件安全冬令营线上赛的一道虚拟机逆向题,比较有特点的是整个虚拟机的操作隐藏在了异常处理函数中,正常用类似 OD调试器不能顺利调试。这种反调试结合虚拟机技巧值得学习。

分析工具:IDA 7.0

1.分析

1.1.整体流程

程序没有加壳,运行之后出现提示:

在这里插入图片描述

根据关键信息,来到主函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  FILE *fout; // [esp+0h] [ebp-74h]
  FILE *fin; // [esp+4h] [ebp-70h]
  signed int i; // [esp+8h] [ebp-6Ch]
  char outbuff[100]; // [esp+Ch] [ebp-68h]

  fin = (FILE *)fopen(&filename, &mod);         // 存放明文的文件名:inp
  if ( !fin )
  {
    printf("Input file not found.\n");
    system("pause");
    exit(0);
  }
  fout = (FILE *)fopen("enc", "w+");
  fscanf(fin, "%s", plaintext);
  sub_4018F0();                                 // 安装异常处理
  memset((__m128i *)outbuff, 0, 0x64u);
  for ( i = 0; i < 32; ++i )
    fprintf((int)&outbuff[2 * i], "%02x", (unsigned __int8)res[i]);
  fwrite(outbuff, fout);
  fclose(fin);
  fclose(fout);
  printf("Success!\n");
  system("pause");
  return 0;
}

主要流程很清晰,首先读取 inp文件,该文件存放的是明文数据,然后创建一个 enc文件,保存加密后的数据。@line:17是重点,留到后面分析。

通过 @line20知道,enc文件里的数据是加密结果的十六进制字符串。

1.2.异常处理分析

void sub_4018F0()
{
  SetUnhandledExceptionFilter(ExceptionsHandler);
  JUMPOUT(unk_401901);
}

该函数设置了一个异常处理函数 E x c e p t i o n s H a n d l e r \textcolor{cornflowerblue}{ExceptionsHandler} ExceptionsHandler,理解这个异常处理函数是解决这道题目的关键,前提是需要了解 S e t U n h a n d l e d E x c e p t i o n F i l t e r \textcolor{cornflowerblue}{SetUnhandledExceptionFilter} SetUnhandledExceptionFilter的定义

LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(
[in] LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
);

typedef LONG (WINAPI *PTOP_LEVEL_EXCEPTION_FILTER)(
 _In_ struct _EXCEPTION_POINTERS *ExceptionInfo
);

调用该函数后,如果一个 未被调试的进程发生异常,并且该异常进入未处理的异常过滤器,该过滤器将调用 lpTopLevelExceptionFilter 参数指定的异常过滤器函数。

显然在调试器里面不能正常的跟踪调试这个 E x c e p t i o n s H a n d l e r \textcolor{cornflowerblue}{ExceptionsHandler} ExceptionsHandler,至少 OD这类的调试器不行,尽管我在 OD设置里忽略了所有异常,但结果依然是程序直接跑飞。不动态调试了行不行?先静态分析一下 E x c e p t i o n s H a n d l e r \textcolor{cornflowerblue}{ExceptionsHandler} ExceptionsHandler

signed int __stdcall ExceptionsHandler(_EXCEPTION_POINTERS *exceptions)
{

  pc = exceptions->ContextRecord->Eip;
  switch ( get_ptr_val(pc + 2) )   // get_ptr_val(int *a1){return *a1}
  {
    case 0:
      val = (unsigned __int8)get_ptr_val(pc + 4);
      reg_id = get_ptr_val(pc + 3);
      Add_Register(&exceptions->ContextRecord->ContextFlags, (_DWORD *)reg_id, val);
      v18 = 5;
      break;
    case 1:
      v3 = (unsigned __int8)get_ptr_val(pc + 4);
      v4 = get_ptr_val(pc + 3);
      Sub_Register_Value(&exceptions->ContextRecord->ContextFlags, (_DWORD *)v4, v3);
      v18 = 5;
      break;
    case 2:
      v5 = get_ptr_val(pc + 3);
      Add_Register(&exceptions->ContextRecord->ContextFlags, (_DWORD *)v5, 1);
      v18 = 4;
      break;
    case 3:
      v6 = get_ptr_val(pc + 3);
      Add_Register(&exceptions->ContextRecord->ContextFlags, (_DWORD *)v6, -1);
      v18 = 4;
      break;
    case 4:
      v7 = (unsigned __int8)get_ptr_val(pc + 4);
      v8 = get_ptr_val(pc + 3);
      And_Register_Value(&exceptions->ContextRecord->ContextFlags, (_DWORD *)v8, v7);
      v18 = 5;
      break;
    case 5:
      v9 = (unsigned __int8)get_ptr_val(pc + 4);
      v10 = get_ptr_val(pc + 3);
      Or_Register_Value(&exceptions->ContextRecord->ContextFlags, (_DWORD *)v10, v9);
      v18 = 5;
      break;
    case 6:
      v11 = (unsigned __int8)get_ptr_val(pc + 4);
      v12 = get_ptr_val(pc + 3);
      Xor_Register_Value(&exceptions->ContextRecord->ContextFlags, (_DWORD *)v12, v11);
      v18 = 5;
      break;
    case 7:
      v13 = get_ptr_val(pc + 4);
      v14 = get_ptr_val(pc + 3);
      Left_Shift_Register_Value(&exceptions->ContextRecord->ContextFlags, (_DWORD *)v14, v13);
      v18 = 5;
      break;
    case 8:
      v15 = get_ptr_val(pc + 4);
      v16 = get_ptr_val(pc + 3);
      Right_Shift_Register_Value(&exceptions->ContextRecord->ContextFlags, (_DWORD *)v16, v15);
      v18 = 5;
      break;
    default:
      v18 = 2;
      break;
  }
  exceptions->ContextRecord->Eip += v18;
  return -1;
}

其 中 的 函 数 名 是 我 分 析 之 后 给 出 的 定 义 \textcolor{green}{其中的函数名是我分析之后给出的定义}

首先看整体的框架,这相当于一个虚拟机的调度器:

  • @line4pc变量取自发生异常时的位置 + 2 \textcolor{orange}{+2} +2

  • @line5 p c + 2 \textcolor{orange}{pc+2} pc+2就是整个虚拟机的 opcode,两个参数 reg_idval分别在 p c + 3 \textcolor{orange}{ pc+3} pc+3 p c + 4 \textcolor{orange}{pc+4} pc+4的位置

  • @line63:当异常处理结束后,返回的地址为当前异常发生位置 + v 18 \textcolor{orange}{+v18} +v18

触发异常的位置刚好就在函数 s u b _ 4018 F 0 \textcolor{cornflowerblue}{sub\_4018F0} sub_4018F0中,该函数安装完异常处理,紧接着执行一个异常指令并触发异常:

.text:004018FB ; ---------------------------------------------------------------------------
.text:00401901 unk_401901      db 0C7h
.text:00401902                 db 0FFh
.text:00401903                 db    4
.text:00401904                 db    1
.text:00401905                 db    0
.text:00401906                 db  33h ; 3
.text:00401907                 db 0C9h
.text:00401908                 db  83h
.text:00401909                 db 0F9h
.text:0040190A                 db  20h
.text:0040190B                 db  7Dh ; }
.text:0040190C                 db  17h
.text:0040190D                 db 0C7h
.text:0040190E                 db 0FFh
.text:0040190F                 db    0
.text:00401910                 db    1
.text:00401911                 db 11h
.text:00401912                 db 0C7h
.text:00401913                 db 0FFh
.text:00401914                 db    4
.text:00401915                 db    1
.text:00401916                 db 1Fh
...
.text:004019D5                 db 0CCh

异常指令就是 0xC7,通过审计虚拟机的调度器可以知道:0xFFC7就是进入虚拟机的标志。例如上面的代码段中的第一个 0xFFC7后面的 4就是虚拟机的 opcode10opcode对应的 Handler的参数。

下面举一个 Handler进行分析,其他的 Handler分析方法类似。

_DWORD *__cdecl Add_Register(_DWORD *a1, _DWORD *a2, int a3)
{
  _DWORD *result; // eax

  result = a2;
  switch ( (unsigned int)a2 )
  {
    case 1u:
      result = a1;
      a1[44] += a3;                             // eax
      break;
    case 2u:
      result = a1;
      a1[41] += a3;                             // ebx
      break;
    case 3u:
      result = a1;
      a1[43] += a3;                             // ecx
      break;
    case 4u:
      result = a1;
      a1[42] += a3;                             // edx
      break;
    case 5u:
      result = a1;
      a1[40] += a3;                             // esi
      break;
    default:
      return result;
  }
  return result;
}

变量 a1指向的是 ContextFlags,这是结构体_EXCEPTION_POINTERS中的 CONTEXT成员,对 a 1 [ 44 ] \textcolor{orange}{a1[44]} a1[44]的寻址相当于是相对于ContextFlags + 44 ∗ 4 \textcolor{orange}{44*4} 444所在内存的访问。

以下是 CONTEXT成员对应的结构体

typedef struct DECLSPEC_NOINITALL _CONTEXT {
    DWORD ContextFlags;
    DWORD   Dr0;
    DWORD   Dr1;
    DWORD   Dr2;
    DWORD   Dr3;
    DWORD   Dr6;
    DWORD   Dr7;
    FLOATING_SAVE_AREA FloatSave;
    DWORD   SegGs;
    DWORD   SegFs;
    DWORD   SegEs;
    DWORD   SegDs;
    DWORD   Edi;
    DWORD   Esi;
    DWORD   Ebx;
    DWORD   Edx;
    DWORD   Ecx;
    DWORD   Eax;
    DWORD   Ebp;
    DWORD   Eip;
    DWORD   SegCs;              // MUST BE SANITIZED
    DWORD   EFlags;             // MUST BE SANITIZED
    DWORD   Esp;
    DWORD   SegSs;
    BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];

} CONTEXT;

所以不难得出 a 1 [ 44 ] \textcolor{orange}{a1[44] } a1[44]指向的就是 eax寄存器, a 1 [ 41 ] = > e b x \textcolor{orange}{a1[41] => ebx} a1[41]=>ebx a 1 [ 42 ] = > e d x \textcolor{orange}{a1[42] => edx} a1[42]=>edx a 1 [ 43 ] = > e c x \textcolor{orange}{a1[43] => ecx} a1[43]=>ecx a 1 [ 40 ] = > e s i \textcolor{orange}{a1[40] => esi} a1[40]=>esi,到这里自然明白变量 a2起到指定寄存器的作用。所以该函数作用就是指定寄存器与给定立即数相加,我命名为 A d d _ R e g i s t e r \textcolor{cornflowerblue}{Add\_Register} Add_Register

其他的 Handler分析方法类似,所以最终得出前面调度器中各个函数的定义。

由于这是通过异常进入虚拟机,所以两个异常标志之间可能还包含有程序代码,这并不属于虚拟机相关的数据,但也不可忽略。于是仿造前面对调度器分析的结果,写了个 python脚本进行辅助分析(代码见文末)。该脚本的作用就是解析虚拟机的指令,并打印每个步骤的操作,将不属于虚拟机相关的数据转换为程序代码。最后得到:

.text:00401901                 dw 0FFC7h               ; 0: eax=eax&0
.text:00401903                 db    4
.text:00401904                 db    1
.text:00401905                 db    0
.text:00401906 ; ---------------------------------------------------------------------------
.text:00401906                 xor     ecx, ecx
.text:00401908
.text:00401908 loc_401908:                             ; CODE XREF: .text:00401922↓j
.text:00401908                 cmp     ecx, 20h
.text:0040190B                 jge     short loc_401924
.text:0040190B ; ---------------------------------------------------------------------------
.text:0040190D                 db 0C7h                 ; 1: eax=eax+17
.text:0040190E                 db 0FFh
.text:0040190F                 db    0
.text:00401910                 db    1
.text:00401911                 db  11h
.text:00401912                 db 0C7h                 ; 2: eax=eax&31
.text:00401913                 db 0FFh
.text:00401914                 db    4
.text:00401915                 db    1
.text:00401916                 db  1Fh
.text:00401917 ; ---------------------------------------------------------------------------
.text:00401917                 mov     dword_457A70[ecx*4], eax
.text:00401917 ; ---------------------------------------------------------------------------
.text:0040191E                 db 0C7h                 ; 3: ecx=ecx+1
.text:0040191F                 db 0FFh
.text:00401920                 db    2
.text:00401921                 db    3
.text:00401922 ; ---------------------------------------------------------------------------
.text:00401922                 jmp     short loc_401908
.text:00401924 ; ---------------------------------------------------------------------------
.text:00401924
.text:00401924 loc_401924:                             ; CODE XREF: .text:0040190B↑j
.text:00401924                 xor     ecx, ecx
.text:00401926
.text:00401926 loc_401926:                             ; CODE XREF: .text:00401956↓j
.text:00401926                 cmp     ecx, 20h
.text:00401929                 jge     short loc_401958
.text:0040192B                 mov     ebx, dword_457A70[ecx*4]
.text:00401932                 mov     edx, dword_457A74[ecx*4]
.text:00401939                 mov     al, byte_457A4C[edx]
.text:0040193F                 mov     byte_4579E0[ebx], al
.text:00401945                 mov     al, byte_457A4C[ebx]
.text:0040194B                 mov     byte_4579E0[edx], al
.text:0040194B ; ---------------------------------------------------------------------------
.text:00401951                 db 0C7h                 ; 4: ecx=ecx+2
.text:00401952                 db 0FFh
.text:00401953                 db    0
.text:00401954                 db    3
.text:00401955                 db    2
.text:00401956 ; ---------------------------------------------------------------------------
.text:00401956                 jmp     short loc_401926
.text:00401958 ; ---------------------------------------------------------------------------
.text:00401958
.text:00401958 loc_401958:                             ; CODE XREF: .text:00401929↑j
.text:00401958                 xor     ecx, ecx
.text:0040195A
.text:0040195A loc_40195A:                             ; CODE XREF: .text:00401992↓j
.text:0040195A                 cmp     ecx, 20h
.text:0040195D                 jge     short loc_401994
.text:0040195F                 mov     bl, byte_4579E0[ecx]
.text:0040195F ; ---------------------------------------------------------------------------
.text:00401965                 db 0C7h                 ; 5: ebx=ebx&31
.text:00401966                 db 0FFh
.text:00401967                 db    4
.text:00401968                 db    2
.text:00401969                 db  1Fh
.text:0040196A                 db 0C7h                 ; 6: ebx=ebx<<3
.text:0040196B                 db 0FFh
.text:0040196C                 db    7
.text:0040196D                 db    2
.text:0040196E                 db    3
.text:0040196F ; ---------------------------------------------------------------------------
.text:0040196F                 mov     esi, ecx
.text:00401971                 inc     esi
.text:00401972                 and     esi, 1Fh
.text:00401975                 mov     dl, byte_4579E0[esi]
.text:0040197B                 and     dl, 0E0h
.text:0040197E                 and     edx, 0FFh
.text:0040197E ; ---------------------------------------------------------------------------
.text:00401984                 db 0C7h                 ; 7: edx=edx>>5
.text:00401985                 db 0FFh
.text:00401986                 db    8
.text:00401987                 db    4
.text:00401988                 db    5
.text:00401989 ; ---------------------------------------------------------------------------
.text:00401989                 or      bl, dl
.text:0040198B                 mov     byte_457A04[ecx], bl
.text:00401991                 inc     ecx
.text:00401992                 jmp     short loc_40195A
.text:00401994 ; ---------------------------------------------------------------------------
.text:00401994
.text:00401994 loc_401994:                             ; CODE XREF: .text:0040195D↑j
.text:00401994                 mov     al, byte_457A04
.text:00401999                 mov     byte_457A28, al
.text:0040199E                 mov     ecx, 1
.text:004019A3
.text:004019A3 loc_4019A3:                             ; CODE XREF: .text:004019CE↓j
.text:004019A3                 cmp     ecx, 20h
.text:004019A6                 jge     short loc_4019D0
.text:004019A8                 mov     bl, byte_457A04[ecx]
.text:004019AE                 mov     esi, ecx
.text:004019AE ; ---------------------------------------------------------------------------
.text:004019B0                 db 0C7h                 ; 7: esi=esi-1
.text:004019B1                 db 0FFh
.text:004019B2                 db    3
.text:004019B3                 db    5
.text:004019B4 ; ---------------------------------------------------------------------------
.text:004019B4                 xor     bl, byte_457A04[esi]
.text:004019BA                 mov     esi, ecx
.text:004019BA ; ---------------------------------------------------------------------------
.text:004019BC                 db 0C7h                 ; 8: esi=esi&3
.text:004019BD                 db 0FFh
.text:004019BE                 db    4
.text:004019BF                 db    5
.text:004019C0                 db    3
.text:004019C1 ; ---------------------------------------------------------------------------
.text:004019C1                 xor     bl, byte ptr aBier[esi] ; "Bier"
.text:004019C7                 mov     byte_457A28[ecx], bl
.text:004019CD                 inc     ecx
.text:004019CE                 jmp     short loc_4019A3
.text:004019D0 ; ---------------------------------------------------------------------------
.text:004019D0
.text:004019D0 loc_4019D0:                             ; CODE XREF: .text:004019A6↑j
.text:004019D0                 pop     edi
.text:004019D1                 pop     esi
.text:004019D2                 pop     ebx
.text:004019D3                 pop     ebp
.text:004019D4                 retn
.text:004019D4 ; ---------------------------------------------------------------------------

处理结果并不能用 F5还原代码,因为堆栈指针异常,代码量也不大,主要涉及 3个循环,那就手动还原吧~

1.3.虚拟机加密代码还原

const unsigned char key[] = "Bier";

void Enc(const unsigned char* plaintext, unsigned char* res) {
	unsigned int table[128] = { 0 };
	unsigned int a = 0;
	unsigned char tmp_table[0x20] = { 0 };
	unsigned char tmp_table2[0x20] = { 0 };

    //.text:00401906 - .text:00401917
	for (int i = 0; i < 0x20; i++) {
		a = (a + 17) % 0x20;
		table[i * 4] = a;
	}
	
    //.text:00401924 - .text:00401956
	for (int i = 0; i < 0x20; i+=2) {
		int q = table[4*i];
		int r = table[4*(i+1)];
		tmp_table[r] = plaintext[q];
		tmp_table[q] = plaintext[r];
	}
	
	//.text:00401958 - .text:00401992
	for (int i = 0; i < 0x20; i++) {
		tmp_table2[i] = ((tmp_table[i] & 0x1F) << 3) | ((tmp_table[(i + 1) % 0x20] & 0xE0) >> 5);
	}
	
    //.text:00401994 - .text:004019CE 
	res[0] = tmp_table2[0];
	for (int i = 1; i < 0x20; i++) {
		res[i]= tmp_table2[i]^ tmp_table2[i-1]^ key[i % 4];
	}
}
  • line10:生成一个 table,用于 line16对明文数据进行位移。
  • line24:实际是将 t m p _ t a b l e [ i ] \textcolor{orange}{tmp\_table[i]} tmp_table[i]的低 5位作为
    t m p _ t a b l e 2 [ i ] \textcolor{orange}{tmp\_table2[i]} tmp_table2[i]的高 5位, t m p _ t a b l e [ ( i + 1 ) % 0 x 20 ] \textcolor{orange}{tmp\_table[(i+1)\%0x20]} tmp_table[(i+1)%0x20]的高 3位作为 t m p _ t a b l e 2 [ i ] \textcolor{orange}{tmp\_table2[i]} tmp_table2[i]的低 3位。
  • line31:简单的异或。

2.破解

根据题目给出的密文数据,最终破解代码:

#include<iostream>
#include<windows.h>

const unsigned char key[] = "Bier";

void Enc(const unsigned char* plaintext, unsigned char* res) {
	unsigned int table[128] = { 0 };
	unsigned int a = 0;
	unsigned char tmp_table[0x20] = { 0 };
	unsigned char tmp_table2[0x20] = { 0 };

	for (int i = 0; i < 0x20; i++) {
		a = (a + 17) % 0x20;
		table[i * 4] = a;
	}

	for (int i = 0; i < 0x20; i+=2) {
		int q = table[4*i];
		int r = table[4*(i+1)];
		tmp_table[r] = plaintext[q];
		tmp_table[q] = plaintext[r];
	}
	
	for (int i = 0; i < 0x20; i++) {
		tmp_table2[i] = ((tmp_table[i] & 0x1F) << 3) | ((tmp_table[(i + 1) % 0x20] & 0xE0) >> 5);
	}
	
	res[0] = tmp_table2[0];
	for (int i = 1; i < 0x20; i++) {
		res[i]= tmp_table2[i]^ tmp_table2[i-1]^ key[i % 4];
	}
	
}

void Dec(const unsigned char* cipher, unsigned char* res) {
	unsigned int table[128] = { 0 };
	unsigned int a = 0;
	unsigned char tmp_table[0x20] = { 0 };
	unsigned char tmp_table2[0x20] = { 0 };

	for (int i = 0; i < 0x20; i++) {
		a = (a + 17) % 0x20;
		table[i * 4] = a;
	}

	tmp_table2[0] = cipher[0];
	for (int i = 1; i < 0x20; i++) {
		tmp_table2[i] = cipher[i] ^ tmp_table2[i - 1] ^ key[i % 4];
	}

	for (int i = 2; i < 0x20*2; i++) {
		tmp_table[i%0x20] = (tmp_table2[(i - 1)%0x20] & 7) << 5 | (tmp_table2[i%0x20] >> 3);
	}

	for (int i = 0; i < 0x20; i += 2) {
		int q = table[4 * i];
		int r = table[4 * (i + 1)];
		res[q] = tmp_table[r];
		res[r] = tmp_table[q];
	}
}

int main() {
	unsigned char plaintext[0x20] = { 0x93,0x8b,0x8f,0x43,0x12,0x68,0xf7,0x90,0x7a,0x4b,0x6e,0x42,0x13,0x01,0xb4,0x21,0x20,0x73,0x8d,0x68,0xcb,0x19,0xfc,0xf8,0xb2,0x6b,0xc4,0xab,0xc8,0x9b,0x8d,0x22 };
	unsigned char res[0x20] = { 0 };

	Dec(plaintext, res);

	for (int i = 0; i < 0x20; i++)
		printf("%c", res[i]);
	printf("\n");
	system("pause");
	return 0;
}

IDA python脚本:

#coding:utf-8
from idaapi import*

step=0

def ChoiceRegister(id):
    if id==1:
        return 'eax'
    elif id==2:
        return 'ebx'
    elif id==3:
        return 'ecx'
    elif id==4:
        return 'edx'
    elif id==5:
        return 'esi'
    else:
        return str(id)

def Add_Register(ip,id,v):
    global step
    reg = ChoiceRegister(id)
    s =  str(step)+': '+reg+'='+reg+'+'+str(v)
    idc.MakeComm(ip,s)
    print(s)
    step+=1

def Sub_Register_Value(ip,id,v):
    global step
    reg = ChoiceRegister(id)
    s =  str(step)+': '+reg+'='+reg+'-'+str(v)
    idc.MakeComm(ip,s)
    print(s)
    step+=1

def And_Register_Value(ip,id,v):
    global step
    reg = ChoiceRegister(id)
    s =  str(step)+': '+reg+'='+reg+'&'+str(v)
    idc.MakeComm(ip,s)
    print(s)
    step+=1
    
def Or_Register_Value(ip,id,v):
    global step
    reg = ChoiceRegister(id)
    s =  str(step)+': '+reg+'='+reg+'|'+str(v)
    idc.MakeComm(ip,s)
    print(s)
    step+=1


def Xor_Register_Value(ip,id,v):
    global step
    reg = ChoiceRegister(id)
    s =  str(step)+': '+reg+'='+reg+'^'+str(v)
    idc.MakeComm(ip,s)
    print(s)
    step+=1


def Left_Shift_Register_Value(ip,id,v):
    global step
    reg = ChoiceRegister(id)
    s =  str(step)+': '+reg+'='+reg+'<<'+str(v)
    idc.MakeComm(ip,s)
    print(s) 
    step+=1
    
def Right_Shift_Register_Value(ip,id,v):
    global step
    reg = ChoiceRegister(id)
    s =  str(step)+': '+reg+'='+reg+'>>'+str(v)
    idc.MakeComm(ip,s)
    print(s)

def Dispatcher(op):
    pc=0
    ip=0
    c=[]
    base = 0x401901
    while pc+4<len(op):
        if op[pc]!=0xC7 and op[pc+1]!=0xFF:
            idaapi.create_insn(base+pc)
            print("func = "+hex(base+pc))
            for i in range(pc,len(op)):
                if op[i]==0xC7 and op[i+1]==0xFF:
                    pc=i
                    break
                    
        v = op[pc+4]
        id=op[pc+3]
        w = op[pc+2]
        if w == 0:
            Add_Register(base+pc,id,v)
            pc+=5
            
        elif w==1:
            Sub_Register_Value(base+pc,id,v)
            pc+=5
        elif w==2:
            Add_Register(base+pc,id,1)
            pc+=4
        elif w==3:
            Sub_Register_Value(base+pc,id,1)
            pc+=4
        elif w==4:
            And_Register_Value(base+pc,id,v)
            pc+=5
        elif w==5:
            Or_Register_Value(base+pc,id,v)
            pc+=5
        elif w==6:
            Xor_Register_Value(base+pc,id,v)
            pc+=5
        elif w==7:
            Left_Shift_Register_Value(base+pc,id,v)
            pc+=5
        elif w==8:
            Right_Shift_Register_Value(base+pc,id,v)
            pc+=5  
        else:
            pc+=2
    

if __name__ == '__main__':
    op = [0x0C7,0x0FF,0x4,0x1,0x0,0x33,0x0C9,0x83,0x0F9,0x20,0x7D,0x17,0x0C7,0x0FF,0x0,0x1,0x11,0x0C7,0x0FF,0x4,0x1,0x1F,0x89,0x4,0x8D,0x70,0x7A,0x45,0x0,0x0C7,0x0FF,0x2,0x3,0x0EB,0x0E4,0x33,0x0C9,0x83,0x0F9,0x20,0x7D,0x2D,0x8B,0x1C,0x8D,0x70,0x7A,0x45,0x0,0x8B,0x14,0x8D,0x74,0x7A,0x45,0x0,0x8A,0x82,0x4C,0x7A,0x45,0x0,0x88,0x83,0x0E0,0x79,0x45,0x0,0x8A,0x83,0x4C,0x7A,0x45,0x0,0x88,0x82,0x0E0,0x79,0x45,0x0,0x0C7,0x0FF,0x0,0x3,0x2,0x0EB,0x0CE,0x33,0x0C9,0x83,0x0F9,0x20,0x7D,0x35,0x8A,0x99,0x0E0,0x79,0x45,0x0,0x0C7,0x0FF,0x4,0x2,0x1F,0x0C7,0x0FF,0x7,0x2,0x3,0x8B,0x0F1,0x46,0x83,0x0E6,0x1F,0x8A,0x96,0x0E0,0x79,0x45,0x0,0x80,0x0E2,0x0E0,0x81,0x0E2,0x0FF,0x0,0x0,0x0,0x0C7,0x0FF,0x8,0x4,0x5,0x0A,0x0DA,0x88,0x99,0x4,0x7A,0x45,0x0,0x41,0x0EB,0x0C6,0x0A0,0x4,0x7A,0x45,0x0,0x0A2,0x28,0x7A,0x45,0x0,0x0B9,0x1,0x0,0x0,0x0,0x83,0x0F9,0x20,0x7D,0x28,0x8A,0x99,0x4,0x7A,0x45,0x0,0x8B,0x0F1,0x0C7,0x0FF,0x3,0x5,0x32,0x9E,0x4,0x7A,0x45,0x0,0x8B,0x0F1,0x0C7,0x0FF,0x4,0x5,0x3,0x32,0x9E,0x0F0,0x68,0x45,0x0,0x88,0x99,0x28,0x7A,0x45,0x0,0x41,0x0EB,0x0D3,0x5F,0x5E,0x5B,0x5D,0x0C3]
    Dispatcher(op)

在这里插入图片描述


2018网鼎杯——Give_a_try

前言

这是一道 Win32逆向题,比较值得学习的是反调试技术

1.分析

1.1.运行测试

在这里插入图片描述

1.2.代码逆向分析

1.2.1.主函数
int __stdcall WMain(int a1, int a2, int a3, int a4)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  v7 = 0x30;
  v8 = 3;
  v9 = WndProc;
  v10 = 0;
  v11 = 0x1E;
  v12 = a1;
  v15 = 0x10;
  v16 = 0;
  v17 = "DLGCLASS";
  v13 = LoadIconA(0, 0x7F00);
  v18 = v13;
  v14 = LoadCursorA(0, 0x7F00);
  RegisterClassExA(&v7);
  CreateDialogParamA(hinstance, 0x3E8, 0, WndProc, 0);
  ShowWindow(hwdn, 1);
  UpdateWindow(hwdn);
  while ( GetMessageA(&v5, 0, 0, 0) )
  {
    TranslateMessage(&v5);
    DispatchMessageA(&v5);
  }
  return v6;
}
  • line:17注册了一个窗口类,并在 line:18创建一个对话框,并制定一个窗口处理程序 W i n P r o c \textcolor{cornflowerblue}{WinProc} WinProc
  • line:21 建立窗口消息循环

从上面的分析可以看出这是个标准的 Win32桌面应用编程,窗口的所有响应事件均在 W i n P r o c \textcolor{cornflowerblue}{WinProc} WinProc函数中。

1.2.2.窗口处理程序逆向分析
int __stdcall WndProc(int a1, int a2, int a3, int a4)
{
  char input; // [esp+0h] [ebp-100h]

  switch ( a2 )
  {
    case 0x110:
      hwdn = a1;
      break;
    case 0x111:
      if ( (unsigned __int16)a3 == 1001 )
      {
        GetDlgItemTextA(a1, 1002, &input, 255);
        check(&input);
      }
      break;
    case 0x10:
      DestroyWindow(a1);
      break;
    case 2:
      PostQuitMessage(0);
      break;
    default:
      return DefWindowProcA(a1, a2, a3, a4);
  }
  return 0;
}
  • line:11 这个地方就是处理按钮响应事件的

  • line:13 获取编辑框中的数据到 input变量中,最大长度为 255字节

1.2.3.校验流程分析
int __stdcall check(char *input)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  if ( strlen(input) != 42 )
    return MessageBoxA(0, aThinkAgain, 0, 0);
  sum = 0;
  v3 = *input;
  v4 = input + 1;
  while ( v3 )
  {
    sum += v3;
    v3 = *v4++;
  }                                             
                                          
                                               
  srand(ReturnLength ^ sum);                    // 正确的ReturnLength = 0x31333359
  for ( i = 0; i != 42; ++i )
  {
    v6 = (unsigned __int8)input[i] * rand();
    v7 = v6 * (unsigned __int64)v6 % 0xFAC96621;
    v8 = v7 * (unsigned __int64)v7 % 0xFAC96621;
    v9 = v8 * (unsigned __int64)v8 % 0xFAC96621;
    v10 = v9 * (unsigned __int64)v9 % 0xFAC96621;
    v11 = v10 * (unsigned __int64)v10 % 0xFAC96621;
    v12 = v11 * (unsigned __int64)v11 % 0xFAC96621;
    v13 = v12 * (unsigned __int64)v12 % 0xFAC96621;
    v14 = v13 * (unsigned __int64)v13 % 0xFAC96621;
    v15 = v14 * (unsigned __int64)v14 % 0xFAC96621;
    v16 = v15 * (unsigned __int64)v15 % 0xFAC96621;
    v17 = v16 * (unsigned __int64)v16 % 0xFAC96621;
    v18 = v17 * (unsigned __int64)v17 % 0xFAC96621;
    v19 = v18 * (unsigned __int64)v18 % 0xFAC96621;
    v20 = v19 * (unsigned __int64)v19 % 0xFAC96621;
    v21 = v20 * (unsigned __int64)v20 % 0xFAC96621;
    if ( v6 % 0xFAC96621 * (unsigned __int64)v21 % 0xFAC96621 != ans[i] )
      break;
  }
  if ( i >= 42 )
    result = MessageBoxA(0, aCorrect, aCongrats, 0);
  else
    result = MessageBoxA(0, aIncorrect, 0, 0);
  return result;
}
  • 校验流程非常清晰,输入的数据长度要求是 42字节

  • line:10循环中将输入的数据进行求和,结果保存在变量 sum中。

  • 使用 sum异或一个动态生成的变量 ReturnLength作为随机数种子

  • line:18的每一轮循环进行的计算相当于是 ( i n p u t [ i ] ∗ r a n d ( ) ) 2 16 + 1   m o d   0 x F A C 96621 \textcolor{orange}{(input[i]*rand())^{2^{16}+1}\ mod\ 0xFAC96621} (input[i]rand())216+1 mod 0xFAC96621,结果和 a n s [ i ] \textcolor{orange}{ans[i]} ans[i]进行比较,如果每一轮的比较都相等,则判定输入正确。

为了获取 ReturnLength,我试图对齐交叉引用,发现有好几个地方对其读写的,而且这些地方都加了花指令,导致 IDA不能正常分析。为了快速获取 ReturnLength的值,直接动态调试吧,就不需要去分析这些代码了。

将断点打在地址 00401146,对应上面源码的第 17行,在输入符合长度要求的数据之后,程序并没有断下来。看来是暗藏了玄机。此时注意到程序中有 TLS回调函数,这个函数会先于主函数运行,所以这个回调中必有乾坤!

1.2.4.反调试分析

重新加载程序,断点打在 TLS回调中,地址是 00402000,结果很严重,桌面都卡死了。看来还是得先对 TLS回调进行一点静态分析,TLS回调函数也加了花指令,花指令的形式比较简单,主要是通过 c a l l   o f f s e t \textcolor{orange}{call\ offset} call offset a d d [ e s p + n ] , m ; r e t r n \textcolor{orange}{add [esp+n], m;retrn} add[esp+n],m;retrn这种形式去控制 eip指针,IDA没法正确分析函数。我并没有死磕这些花指令,我再次看向函数导入表,发现有两个关键系统函数引起了我的注意: N t Q u e r y I n f o r m a t i o n P r o c e s s \textcolor{cornflowerblue}{NtQueryInformationProcess} NtQueryInformationProcess N t S e t I n f o r m a t i o n T h r e a d \textcolor{cornflowerblue}{NtSetInformationThread} NtSetInformationThread,我通过对这两个函数进行交叉引用的时候就发现了程序暗藏的玄机。

首先程序会调用 N t S e t I n f o r m a t i o n T h r e a d \textcolor{cornflowerblue}{NtSetInformationThread} NtSetInformationThread设置主线程不向调试器发送调试信息,使得之前我用 OD调试程序时断不下。

pizza:0040207A                 call    GetCurrentThread
pizza:0040207F                 call    loc_402085
pizza:0040207F ; ---------------------------------------------------------------------------
pizza:00402084                 db 81h
pizza:00402085 ; ---------------------------------------------------------------------------
pizza:00402085
pizza:00402085 loc_402085:                             ; CODE XREF: sub_402022+5D↑j
pizza:00402085                 add     dword ptr [esp+0], 6
pizza:00402089                 retn
pizza:0040208A                 push    0               ; ThreadInformationLength
pizza:0040208C                 push    0               ; ThreadInformation
pizza:0040208E                 push    11h             ; ThreadInformationClass
pizza:00402090                 push    eax             ; ThreadHandle
pizza:00402091                 call    NtSetInformationThread
pizza:00402096                 call    loc_40209C
  • 这种反调试手段也很好过。重新载入程序,在 0040208E下断,断下时将 11改成其他数值即可,然后保存修改,生成新的可执行文件。

然后调用 N t Q u e r y I n f o r m a t i o n P r o c e s s \textcolor{cornflowerblue}{NtQueryInformationProcess} NtQueryInformationProcess,查询 DebugPort信息,处于调试状态的应用程序,其DebugPort值不为 0

pizza:0040213F                 call    GetCurrentProcess
pizza:00402144                 mov     ebx, eax
pizza:00402146                 call    sub_40214C
pizza:00402146 sub_40213F      endp
pizza:00402146
pizza:00402146 ; ---------------------------------------------------------------------------
pizza:0040214B byte_40214B     db 83h
pizza:0040214C
pizza:0040214C ; =============== S U B R O U T I N E =======================================
pizza:0040214C
pizza:0040214C
pizza:0040214C sub_40214C      proc near               ; CODE XREF: sub_40213F+7↑j
pizza:0040214C                 add     dword ptr [esp+0], 6
pizza:00402150                 retn
pizza:00402150 sub_40214C      endp
pizza:00402150
pizza:00402151
pizza:00402151 ; =============== S U B R O U T I N E =======================================
pizza:00402151
pizza:00402151
pizza:00402151 sub_402151      proc near
pizza:00402151                 push    offset ReturnLength ; ReturnLength
pizza:00402156                 push    4               ; ProcessInformationLength
pizza:00402158                 push    offset ReturnLength ; ProcessInformation
pizza:0040215D                 push    7               ; ProcessInformationClass
pizza:0040215F                 push    ebx             ; ProcessHandle
pizza:00402160                 call    NtQueryInformationProcess
pizza:00402165                 cmp     eax, 0
pizza:00402168                 cmovb   edi, esi
pizza:0040216B                 call    loc_402171
pizza:0040216B ; ---------------------------------------------------------------------------
pizza:00402170                 db 68h
pizza:00402171 ; ---------------------------------------------------------------------------
pizza:00402171
pizza:00402171 loc_402171:                             ; CODE XREF: sub_402151+1A↑j
pizza:00402171                 add     dword ptr [esp+0], 6
pizza:00402175                 retn

程序将DebugPort的查询结果保存到 ReturnLength全局变量中,这就是我们想要获取的。但这还没完,后面会判断 ReturnLength只有为 0时,程序才会将正确的数值赋值给 ReturnLength变量。

  • 想要获取正确的 ReturnLength值,最简单的方法就是先运行程序,再用调试器附加,最后得到正确的 ReturnLength取值为 0x31333359

接下来可以分析如何破解这个校验了。

1.3.破解校验算法

回顾 @1.2.3校验流程分析,最关键的校验算法是 ( i n p u t [ i ] ∗ r a n d ( ) ) 2 16 + 1   m o d   0 x F A C 96621 \textcolor{orange}{(input[i]*rand())^{2^{16}+1}\ mod\ 0xFAC96621} (input[i]rand())216+1 mod 0xFAC96621,这个算法的逆过程就涉及到 离散对数问题,而目前该问题仍属于数学界的难题,还没有办法解决,所以这部分的算法,只能用爆破的方式。但目前变量 sum未知,正确的 input数据只知道前五个字符是 flag{,我们需要获得正确的 sum,才能够往下进行破解。因为 input规定都是可见字符,所以对 sum的枚举空间在 [ 32 ∗ 42 , 127 ∗ 42 ) \textcolor{orange}{ [32*42,127*42)} [3242,12742),完全能够接受。得到 sum之后,就可以逐字符进行枚举 input数据了。

1.4.脚本

#include<stdio.h>
#include<stdlib.h>

int ans[42] = {
	0x63B25AF1,0x0C5659BA5,0x4C7A3C33,0x0E4E4267,0x0B611769B
	,0x3DE6438C,0x84DBA61F,0x0A97497E6,0x650F0FB3,0x84EB507C
	,0x0D38CD24C,0x0E7B912E0,0x7976CD4F,0x84100010,0x7FD66745
	,0x711D4DBF,0x5402A7E5,0x0A3334351,0x1EE41BF8,0x22822EBE
	,0x0DF5CEE48,0x0A8180D59,0x1576DEDC,0x0F0D62B3B,0x32AC1F6E
	,0x9364A640,0x0C282DD35,0x14C5FC2E,0x0A765E438,0x7FCF345A
	,0x59032BAD,0x9A5600BE,0x5F472DC5,0x5DDE0D84,0x8DF94ED5
	,0x0BDF826A6,0x515A737A,0x4248589E,0x38A96C20,0x0CC7F61D9
	,0x2638C417,0x0D9BEB996 };

 int Calc(unsigned int v) {
	 unsigned __int64 a = v;

	  for (int i = 0; i < 16; i++)
		  a = (a * a) % 0xFAC96621;
	 return (a * v) % 0xFAC96621;
}

int  GetSum() {

	const char* prefix = "flag{";

	int c = 0;

	for (int sum = 32*42; sum < 127*42; sum++) {
		srand(0x31333359 ^ sum);
		for (int i = 0; i < 5; i++) {
			if (Calc(prefix[i] * rand()) == ans[i]) {
				c++;
			}
		}
		if (c == 5)return sum;
		c = 0;
	}
	return 0;
}

void Bruteforce(int sum) {
	int r = 0;
	srand(0x31333359 ^ sum);

	for (int i = 0; i < 42; i++) {
		r = rand();
		for (char c = 32; c < 127; c++) {
			if (Calc((c *r ) )== ans[i]) {
				printf("%c", c);
				break;
			}
		}
	}
	printf("\n");
}

int main() {
	Bruteforce(GetSum());
	//flag{wh3r3_th3r3_i5_@_w111-th3r3_i5_@_w4y}
	system("pause");

	return 0;
}

1.5.演示

在这里插入图片描述

后话

这道题根据 PE节名为pizza猜测应该是pizza前辈制作的吧,膜拜~

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐