赞
踩
这是一道来自华为2021硬件安全冬令营线上赛的一道虚拟机逆向题,比较有特点的是整个虚拟机的操作隐藏在了异常处理函数中,正常用类似 OD调试器不能顺利调试。这种反调试结合虚拟机技巧值得学习。
分析工具:IDA 7.0
程序没有加壳,运行之后出现提示:
根据关键信息,来到主函数:
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文件里的数据是加密结果的十六进制字符串。
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 );
- 1
- 2
- 3
- 4
- 5
- 6
- 7
调用该函数后,如果一个 未被调试的进程发生异常,并且该异常进入未处理的异常过滤器,该过滤器将调用 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}{其中的函数名是我分析之后给出的定义} 其中的函数名是我分析之后给出的定义
首先看整体的框架,这相当于一个虚拟机的调度器:
@line4: pc变量取自发生异常时的位置 + 2 \textcolor{orange}{+2} +2
@line5 : p c + 2 \textcolor{orange}{pc+2} pc+2就是整个虚拟机的 opcode,两个参数 reg_id和 val分别在 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就是虚拟机的 opcode,1和 0是 opcode对应的 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}
44∗4所在内存的访问。
以下是 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个循环,那就手动还原吧~
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]; } }
根据题目给出的密文数据,最终破解代码:
#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)
这是一道 Win32逆向题,比较值得学习的是反调试技术
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; }
从上面的分析可以看出这是个标准的 Win32桌面应用编程,窗口的所有响应事件均在 W i n P r o c \textcolor{cornflowerblue}{WinProc} WinProc函数中。
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字节
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回调函数,这个函数会先于主函数运行,所以这个回调中必有乾坤!
重新加载程序,断点打在 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
然后调用 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变量。
接下来可以分析如何破解这个校验了。
回顾 @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)} [32∗42,127∗42),完全能够接受。得到 sum之后,就可以逐字符进行枚举 input数据了。
#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; }
这道题根据 PE节名为pizza猜测应该是pizza前辈制作的吧,膜拜~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。