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}{其中的函数名是我分析之后给出的定义} 其中的函数名是我分析之后给出的定义
首先看整体的框架,这相当于一个虚拟机的调度器:
-
@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个循环,那就手动还原吧~
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)} [32∗42,127∗42),完全能够接受。得到 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前辈制作的吧,膜拜~