CISCN 2021 西南赛区 Reverse Writeup
babyRe
主要加密代码如下
_BYTE *__fastcall encode(const char *a1)
{
_BYTE *v2; // [rsp+18h] [rbp-28h]
signed __int64 v3; // [rsp+20h] [rbp-20h]
int v4; // [rsp+30h] [rbp-10h]
int v5; // [rsp+34h] [rbp-Ch]
__int64 v6; // [rsp+38h] [rbp-8h]
v3 = strlen(a1);
if ( v3 % 3 )
v6 = 4 * (v3 / 3 + 1);
else
v6 = 4 * (v3 / 3);
v2 = malloc(v6 + 1);
v2[v6] = 0;
v5 = 0;
v4 = 0;
while ( v5 < v6 - 2 )
{
v2[v5] = a3zanjvbmdZekol[(unsigned __int8)a1[v4] >> 2];
v2[v5 + 1] = a3zanjvbmdZekol[(16 * a1[v4]) & 0x30 | ((unsigned __int8)a1[v4 + 1] >> 4)];
v2[v5 + 2] = a3zanjvbmdZekol[(4 * a1[v4 + 1]) & 0x3C | ((unsigned __int8)a1[v4 + 2] >> 6)];
v2[v5 + 3] = a3zanjvbmdZekol[a1[v4 + 2] & 0x3F];
v4 += 3;
v5 += 4;
}
if ( v3 % 3 == 1 )
{
v2[v5 - 2] = 61;
v2[v5 - 1] = 61;
}
else if ( v3 % 3 == 2 )
{
v2[v5 - 1] = 61;
}
return v2;
}
容易看出是 base64 变种,编写脚本根据常量表进行替换即可正常解密
import base64
a="3ZAnJVbMd/zEkolRBDW4KUYT0ga1PF9j86qwuXHciCOfr2tLmexGhpSI+NQ5y7sv"
b="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
s="gHe6gIrSlYUqkGPeg4KNo4Vql4g6g4UqgHgHl4JNonBhlbk+och="
c=""
for i in s:
c=c+b[a.find(i)]
print(base64.b64decode(c))
一次脱单的机会
程序主要代码如下
if ( *(float *)&dword_7FF705EB5638 <= 0.0 )
{
if ( IsDebuggerPresent() )
goto LABEL_22;
sub_7FF705EB1020("f");
if ( IsDebuggerPresent() )
goto LABEL_22;
sub_7FF705EB1020("l");
if ( IsDebuggerPresent() )
goto LABEL_22;
sub_7FF705EB1020("ag");
sub_7FF705EB1020("{md5(");
v8 = 0i64;
v13[2] = _mm_load_si128((const __m128i *)&xmmword_7FF705EB35D0);
v13[1] = _mm_load_si128((const __m128i *)&xmmword_7FF705EB35E0);
v13[0] = _mm_load_si128((const __m128i *)&xmmword_7FF705EB35F0);
v13[3] = v13[0];
v13[4] = 0i64;
if ( _mm_cvtsi128_si32(v13[0]) )
{
do
{
sub_7FF705EB1020("%c");
++v8;
}
while ( v13[0].m128i_i32[v8] );
}
if ( IsDebuggerPresent() )
goto LABEL_22;
sub_7FF705EB1020(")");
if ( IsDebuggerPresent() )
goto LABEL_22;
sub_7FF705EB1020("}");
}
在主函数开头下断点,然后修改 ip 跳过 IsDebuggerPresent,直接运行到中间输出的地方即可得到 flag
ag{md5(女神早已不是女神
注意编码要选择 utf8,不要选 gbk
import hashlib
s=hashlib.md5('女神早已不是女神'.encode('utf8')).hexdigest()
print(s)
CryptoMachine
分析异常处理
程序启动时首先调用 TlsCallback 函数
```cpp
char *__stdcall TlsCallback_0(int a1, int a2, int a3)
{
char *result; // eax
DWORD flOldProtect; // [esp+8h] [ebp-Ch] BYREF
LPVOID v5; // [esp+Ch] [ebp-8h]
LPVOID lpAddress; // [esp+10h] [ebp-4h]
if ( a2 == 1 )
{
lpAddress = &loc_4025DB;
VirtualProtect(&loc_4025DB, 5u, 0x40u, &flOldProtect);
*(_BYTE *)lpAddress = 0xE9;
*(_DWORD *)((char *)lpAddress + 1) = (char *)sub_4021D0 - (char *)lpAddress - 5;// jmp sub_4021D0
v5 = SEH_412700;
VirtualProtect(SEH_412700, 1u, 0x40u, &flOldProtect);
*(_DWORD *)v5 = 0x1B8;
*((_WORD *)v5 + 2) = 0xC300; // ret
VirtualProtect(sub_4021D0, 0x30u, 0x40u, &flOldProtect);
*((_BYTE *)sub_4021D0 + 38) = 0xE8; // call unknown_libname_17
*(_DWORD *)((char *)sub_4021D0 + 39) = (char *)unknown_libname_17 - (char *)((char *)sub_4021D0 + 38) - 5;
*((_BYTE *)sub_4021D0 + 43) = 0xE9;
result = (char *)sub_4021D0 + 43;
*((_DWORD *)sub_4021D0 + 11) = (_BYTE *)lpAddress - ((char *)sub_4021D0 + 43);// jmp lpAddress
}
return result;
}
通过 SMC 操作修改 __scrt_common_main_seh 函数,使程序在进入主函数前调用 sub_4021D0 函数
.text:004021D0 sub_4021D0 proc near ; DATA XREF: TlsCallback_0+39↓o
.text:004021D0 ; TlsCallback_0+86↓o ...
.text:004021D0 push offset main
.text:004021D5 push large dword ptr fs:0
.text:004021DC mov large fs:0, esp
.text:004021E3 push offset sub_4021A0
.text:004021E8 push large dword ptr fs:0
.text:004021EF mov large fs:0, esp
.text:004021F6 call _register_thread_local_exe_atexit_callback ; Microsoft VisualC universal runtime
.text:004021FB jmp loc_4025E0
.text:004021FB sub_4021D0 endp
sub_4021D0 函数在 SEH 链中添加虚拟机入口
进入主函数,首先将需要加密的 flag.docx 读入到 block,随后创建 output 文件用来存放加密后的密文
Buffer = 0;
nNumberOfBytesToRead = 0;
CreateFileA(FileName, 0x80000000, 1u, 0, 3u, 0, 0);
stack = malloc(0x100u);
memset(stack, 0, 0x100u);
ReadFile(0, &Buffer, 0, &nNumberOfBytesToRead, 0);
Block = malloc(0x3000u);
memset(Block, 0, 0x3000u);
v3 = (int (__stdcall *)(const char *, unsigned int, int, _DWORD))sub_401820(aCsgbpNdlk);
v4 = (void (__stdcall *)(int, void *, int, int *, _DWORD))sub_401820(aRdcgbljb);
nNumberOfBytesToRead = sub_401820(aWskwacokm);
v5 = v3("flag.docx", 0x80000000, 1, 0);
outfile = ((int (__stdcall *)(const char *, int, int, _DWORD, int, _DWORD, _DWORD))v3)(
"output",
0x40000000,
1,
0,
2,
0,
0);
v4(v5, Block, 12288, &blocklen, 0);
MEMORY[0x44D000] = 0;
MEMORY[0x44D000] = 0 会触发 SEH 调用虚拟机
reg = envp[46];
if ( *reg == 0xB4900D8B )
{
envp[0x2E] = (_DWORD *)((char *)envp[0x2E] - 3);
v23 = (int (__cdecl *)(_DWORD))sub_401820(aSdvvjmgileooiu);
v15 = v23(sub_4018C0);
result = 0;
}
else if ( envp[46] < (_DWORD *)dword_41A8D8 || (unsigned int)envp[46] > 0x41AA5C )
{
result = 1;
}
第一次进入虚拟机时会调用 SetUnhandledExceptionFilter(sub_4018C0) 注册 UnhandledException 异常处理函数 sub_4018C0
第二次进入虚拟机时会返回 1 抛出 UnhandledException 异常进行反调试,在调试状态下不会执行 sub_4018C0 导致程序退出
int __cdecl sub_4018C0(int a1)
{
if ( **(_DWORD **)(*(_DWORD *)(a1 + 4) + 184) == 0x8B0000C6 )
*(_DWORD *)(*(_DWORD *)(a1 + 4) + 184) = dword_41A8D8;
return -1;
}
sub_4018C0 对虚拟机的状态进行初始化,将 ip 指向虚拟机的指令段,由于处理器无法识别虚拟机指令,所以返回时会触发异常再次进入虚拟机
这里把内存中 envp[46] 的值手动修改成 0x0041A8D8 即可正常调试虚拟机
分析虚拟机
虚拟机主要代码如下
switch ( *reg )
{
case 1:
v26 = 0;
envp[46] = reg + 1; // nop
break;
case 0xA0:
mem[reg[1]] = reg[2]; // mem(a1)=a2
envp[46] = reg + 3;
break;
case 0xB0:
v22 = mem[reg[1]];
++sp_;
*((_DWORD *)stack + sp_) = v22; // push(mem(a1))
envp[46] = reg + 2;
break;
case 0xB5:
mem[reg[1]] = *((_DWORD *)stack + sp_--);// mem(a1)=pop()
envp[46] = reg + 2;
break;
case 0xC0:
m1 = mem[reg[1]]; // mem(0)=cmp(mem(a1),mem(a2))
m2 = mem[reg[2]];
mem[0] = 0;
if ( m1 <= m2 )
{
if ( m1 == m2 )
{
mem[0] |= 2u; // 010 mem(a1)==mem(a2)
}
else if ( m1 < m2 )
{
mem[0] |= 1u; // 001 mem(a1)<mem(a2)
}
}
else
{
mem[0] |= 4u; // 100 mem(a1)>mem(a2)
}
envp[46] = reg + 3;
break;
case 0xD0:
mem[reg[1]] ^= mem[reg[2]]; // mem(a1)^=mem(a2)
envp[46] = reg + 3;
break;
case 0xD1:
mem[reg[1]] += mem[reg[2]]; // mem(a1)+=mem(a2)
envp[46] = reg + 3;
break;
case 0xD2:
mem[reg[1]] -= mem[reg[2]]; // mem(a1)-=mem(a2)
envp[46] = reg + 3;
break;
case 0xD3:
++mem[reg[1]]; // mem(a1)++
envp[46] = reg + 2;
break;
case 0xD4:
--mem[reg[1]]; // mem(a1)--
envp[46] = reg + 2;
break;
case 0xE0:
envp[46] = (_DWORD *)(4 * reg[1] + 0x41A8D8);// jmp(a1)
break;
case 0xE1:
if ( (mem[0] & 2) != 0 )
envp[46] = (_DWORD *)(4 * reg[1] + 4303064);// jeq
else
envp[46] = reg + 2;
break;
case 0xE2:
if ( (mem[0] & 2) != 0 ) // jne
envp[46] = reg + 2;
else
envp[46] = (_DWORD *)(4 * reg[1] + 4303064);
break;
case 0xE3:
if ( (mem[0] & 4) != 0 )
envp[46] = (_DWORD *)(4 * reg[1] + 4303064);// ja
else
envp[46] = reg + 2;
break;
case 0xE4:
if ( (mem[0] & 6) != 0 )
envp[46] = (_DWORD *)(4 * reg[1] + 4303064);// jae
else
envp[46] = reg + 2;
break;
case 0xE5:
if ( (mem[0] & 1) != 0 )
envp[46] = (_DWORD *)(4 * reg[1] + 4303064);// jb
else
envp[46] = reg + 2;
break;
case 0xE6:
if ( (mem[0] & 3) != 0 )
envp[46] = (_DWORD *)(4 * reg[1] + 4303064);// jbe
else
envp[46] = reg + 2;
break;
case 0xF0:
v4 = sub_401810(0); // random
v5 = rand();
srand(v5 + v4);
v6 = (unsigned __int8)rand();
v7 = ((unsigned __int8)rand() << 8) | v6;
v8 = ((unsigned __int8)rand() << 16) | v7;
mem[0] = ((unsigned __int8)rand() << 24) | v8;// mem(0)=rand()
envp[46] = reg + 1;
break;
case 0xF1:
mem[4] = (int)stack; // mem(4)=stack
envp[46] = reg + 1;
break;
case 0xF2:
mem[4] = (int)Block; // mem(4)=block
envp[46] = reg + 1;
break;
case 0xF3:
WriteFile = (void (__cdecl *)(int, int, int, char *, _DWORD))sub_401820(aWskwacokm);
buf = mem[reg[1]];
len = mem[reg[2]];
WriteFile(outfile, buf, len, v14, 0); // write(outfile,mem(a1),mem(a2))
envp[46] = reg + 3;
break;
case 0xF4:
v16 = mem[reg[1]];
hCrypto = (PINFORMATIONCARD_CRYPTO_HANDLE)mem[reg[2]];
v17 = mem[reg[3]];
encrypt(hCrypto, v9, v10, v11, v12, v13);
envp[46] = reg + 4;
break;
case 0xF6:
mem[0] = blocklen; // mem(0)=len
envp[46] = reg + 1;
break;
case 0:
free(stack); // exit
free(Block);
_loaddll(0);
break;
}
简单分析可以看出是使用栈和寄存器的虚拟机
用 IDA 的 Export Data 功能导出虚拟机指令,再编写脚本进行翻译
#include <cstdio>
unsigned char str[] =
{
0xA0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xA0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
0x04, 0x00, 0x00, 0x00, 0xA0, 0x00, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0xB0, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0xA0, 0x00, 0x00, 0x00, 0x02, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA0, 0x00, 0x00, 0x00,
0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xC0, 0x00,
0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
0xE4, 0x00, 0x00, 0x00, 0x1D, 0x00, 0x00, 0x00, 0xF0, 0x00,
0x00, 0x00, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xD3, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xE0, 0x00,
0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0xF1, 0x00, 0x00, 0x00,
0xD1, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x03, 0x00,
0x00, 0x00, 0xB0, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
0xB5, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0xB5, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB5, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xB5, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xB5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xB5, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xF2, 0x00,
0x00, 0x00, 0xD1, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0xF4, 0x00, 0x00, 0x00, 0x04, 0x00,
0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0xF3, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0xF3, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0xD1, 0x00, 0x00, 0x00, 0x02, 0x00,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xF6, 0x00, 0x00, 0x00,
0xD1, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xD2, 0x00, 0x00, 0x00, 0x02, 0x00,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xE5, 0x00, 0x00, 0x00,
0x09, 0x00, 0x00, 0x00, 0xF6, 0x00, 0x00, 0x00, 0xD2, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
0xD1, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0xF4, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF3, 0x00,
0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0xF3, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
int main(){
unsigned int *op_=(unsigned int *)str;
unsigned int *op=op_;
while(op-op_<sizeof(str)/4){
printf("%d: ",op-op_);
switch(*op){
case 0:
printf("exit\n");
op++;
break;
case 1:
printf("nop\n");
op++;
break;
case 0xA0:
printf("mem(%d)=%d\n",*(op+1),*(op+2));
op+=3;
break;
case 0xB0:
printf("push(mem(%d))\n",*(op+1));
op+=2;
break;
case 0xB5:
printf("mem(%d)=pop()\n",*(op+1));
op+=2;
break;
case 0xC0:
printf("mem(0)=cmp(mem(%d),mem(%d))\n",*(op+1),*(op+2));
op+=3;
break;
case 0xD0:
printf("mem(%d)^=mem(%d)\n",*(op+1),*(op+2));
op+=3;
break;
case 0xD1:
printf("mem(%d)+=mem(%d)\n",*(op+1),*(op+2));
op+=3;
break;
case 0xD2:
printf("mem(%d)-=mem(%d)\n",*(op+1),*(op+2));
op+=3;
break;
case 0xD3:
printf("mem(%d)++\n",*(op+1));
op+=2;
break;
case 0xD4:
printf("mem(%d)--\n",*(op+1));
op+=2;
break;
case 0xE0:
printf("jmp %d\n",*(op+1));
op+=2;
break;
case 0xE1:
printf("jeq %d\n",*(op+1));
op+=2;
break;
case 0xE2:
printf("jne %d\n",*(op+1));
op+=2;
break;
case 0xE3:
printf("ja %d\n",*(op+1));
op+=2;
break;
case 0xE4:
printf("jae %d\n",*(op+1));
op+=2;
break;
case 0xE5:
printf("jb %d\n",*(op+1));
op+=2;
break;
case 0xE6:
printf("jbe %d\n",*(op+1));
op+=2;
break;
case 0xF0:
printf("mem(0)=rand()\n");
op+=1;
break;
case 0xF1:
printf("mem(4)=stack\n");
op+=1;
break;
case 0xF2:
printf("mem(4)=block()\n");
op+=1;
break;
case 0xF3:
printf("write(outfile,mem(%d),mem(%d))\n",*(op+1),*(op+2));
op+=3;
break;
case 0xF4:
printf("encrypt(mem(%d),mem(%d),mem(%d))\n",*(op+1),*(op+2),*(op+3));
op+=4;
break;
case 0xF6:
printf("mem(0)=blocklen\n");
op+=1;
break;
}
}
}
翻译结果如下
0: mem(2)=0
3: mem(3)=4
6: mem(1)=16
9: push(mem(2))
11: mem(2)=0
14: mem(4)=4
17: mem(0)=cmp(mem(2),mem(4))
20: jae 29
22: mem(0)=rand()
23: push(mem(0))
25: mem(2)++
27: jmp 17
29: mem(4)=stack
30: mem(4)+=mem(3)
33: push(mem(4))
35: mem(5)=pop()
37: mem(0)=pop()
39: mem(0)=pop()
41: mem(0)=pop()
43: mem(0)=pop()
45: mem(2)=pop()
47: mem(4)=block()
48: mem(4)+=mem(2)
51: encrypt(mem(4),mem(5),mem(1))
55: write(outfile,mem(5),mem(1))
58: write(outfile,mem(4),mem(1))
61: mem(2)+=mem(1)
64: mem(0)=blocklen
65: mem(2)+=mem(1)
68: mem(0)=cmp(mem(2),mem(0))
71: mem(2)-=mem(1)
74: jb 9
76: mem(0)=blocklen
77: mem(0)-=mem(2)
80: mem(4)+=mem(1)
83: encrypt(mem(4),mem(5),mem(0))
87: write(outfile,mem(5),mem(1))
90: write(outfile,mem(4),mem(1))
93: exit
进一步化简得到伪代码
offset=0
do{
stack[4:20]=random();
msg=block()+offset
key=&stack[4]
encrypt(msg,key,16)
write(outfile,key,16)
write(outfile,msg,16)
offset+=16
}while(offset<blocklen)
remain=blocklen-offset
msg+=16
encrypt(msg,key,remain)
write(outfile,key,16)
write(outfile,msg,16)
exit
可以看出程序的算法是每次随机生成 128 位的密钥,然后用 encrypt 函数加密明文中 128 位的数据,再将密钥和密文一起输出到文件
分析加密算法
encrypt 函数的主要代码如下
do
{
v17 = 0;
v18 = *(int *)((char *)&dword_418A24 + v16);
v42 = *(int *)((char *)&dword_418A2C + v16);
v40 = *(int *)((char *)&dword_418A28 + v16);
v41 = *(int *)((char *)&dword_418A20 + v16);
v39 = v18;
v43 = (int)&v49 + v16;
v19 = (char *)&v49 + v16;
do
{
v20 = sub_401250(v18, v35.m128i_i32[v17]);
v21 = sub_401250(v42, v37.m128i_i32[v17]) ^ v20;
v22 = sub_401250(v41, *((_DWORD *)&v34 + v17)) ^ v21;
v23 = sub_401250(v40, v36.m128i_i32[v17]);
v18 = v39;
v19 += 4;
++v17;
*((_DWORD *)v19 - 1) = v23 ^ v22;
}
while ( v17 < 4 );
v16 = v48 + 16;
v48 = v16;
}
while ( v16 < 64 );
分析算法特征
-
dword_418A24 处的内容为 [2,3,1,1],对应 AES 中列混合的系数
-
sub_401250 中多次出现异或 0x1B 的操作,0x1B 对应二进制 0b00011011,对应 AES 中 \(GF(2^8)\) 上乘法规定的的不可约多项式 \(m(x)=x^8+x^4+x^3+x^1+1\)
-
encrypt 中第三个参数为 16,对应 AES 中块的大小 16 字节(128位)
虽然程序中没有静态的 sbox 表可供 FindCrypt 插件查询算法,但是通过以上三点可以推断出这里的 encrypt 函数即为 AES 加密算法
编写解密脚本
解密脚本如下
from Crypto.Cipher import AES
with open('output','rb') as f1:
with open('output.docx','wb') as f2:
while(1):
k=f1.read(16)
c=f1.read(16)
cipher=AES.new(k,AES.MODE_ECB)
msg=cipher.decrypt(c)
f2.write(msg)
打开 output.docx 即可得到 flag