现在的 ctf 真有意思 ( ̄ε ̄@)
最近刚好翻到了之前参加线下赛的一道虚拟机逆向,而且还原出来程序逻辑很简单,刚好仔仔细细地分析一下汇编
题目描述
听说VMP的代码保护能力很强大?(提交dart{}内的内容即可)
逆向分析
又是虚拟机逆向,直接来看 main() 函数
main()
_BOOL8 __fastcall main(int a1, char **a2, char **a3)
{
std::ostream *v3; // rax
__int64 v4; // rax
_BYTE *v5; // r12
__int64 v6; // rdx
_QWORD *v7; // rbx
__int64 v8; // rax
__int64 v9; // rcx
char *v10; // rdi
_BOOL4 v11; // r12d
__int64 (__fastcall *v13)(); // rax
char v14; // [rsp+Fh] [rbp-149h] BYREF
void *v15; // [rsp+10h] [rbp-148h] BYREF
unsigned __int64 v16; // [rsp+18h] [rbp-140h]
_BYTE v17[16]; // [rsp+20h] [rbp-138h] BYREF
void *v18; // [rsp+30h] [rbp-128h] BYREF
unsigned __int64 v19; // [rsp+38h] [rbp-120h]
__int64 v20; // [rsp+40h] [rbp-118h]
char v21; // [rsp+48h] [rbp-110h] BYREF
int v22; // [rsp+84h] [rbp-D4h]
void *v23; // [rsp+88h] [rbp-D0h]
__int64 v24; // [rsp+90h] [rbp-C8h]
__int64 v25; // [rsp+98h] [rbp-C0h]
__int64 v26; // [rsp+A0h] [rbp-B8h]
__int64 v27; // [rsp+A8h] [rbp-B0h]
_QWORD *v28; // [rsp+B0h] [rbp-A8h]
__int64 v29; // [rsp+B8h] [rbp-A0h]
__int64 v30; // [rsp+C0h] [rbp-98h]
__int64 v31; // [rsp+C8h] [rbp-90h]
_QWORD *v32; // [rsp+D0h] [rbp-88h]
void *v33[4]; // [rsp+D8h] [rbp-80h] BYREF
__int16 v34; // [rsp+F8h] [rbp-60h]
char v35; // [rsp+FAh] [rbp-5Eh]
void *v36[2]; // [rsp+100h] [rbp-58h] BYREF
char v37; // [rsp+110h] [rbp-48h] BYREF
__int64 v38; // [rsp+120h] [rbp-38h]
unsigned __int64 v39; // [rsp+128h] [rbp-30h]
v39 = __readfsqword(0x28u);
v3 = (std::ostream *)std::operator<<<std::char_traits<char>>(&std::cout, "Enter key: ", a3);
std::ostream::flush(v3);
v17[0] = 0;
v16 = 0;
v4 = *(_QWORD *)(std::cin[0] - 24);
v15 = v17;
v5 = *(_BYTE **)((char *)&std::cin[30] + v4);
if ( !v5 )
std::__throw_bad_cast();
if ( v5[56] )
{
v6 = (unsigned int)(char)v5[67];
}
else
{
std::ctype<char>::_M_widen_init(*(__int64 *)((char *)&std::cin[30] + v4));
v6 = 10;
v13 = *(__int64 (__fastcall **)())(*(_QWORD *)v5 + 48LL);
if ( v13 != std::ctype<char>::do_widen )
v6 = (unsigned int)((char (__fastcall *)(_BYTE *, __int64, __int64))v13)(v5, 10, 10);
}
std::getline<char,std::char_traits<char>,std::allocator<char>>(std::cin, &v15, v6);
if ( v16 <= 0x29 )
std::string::_M_replace_aux(&v15, v16, 0, 42 - v16, 0);
LODWORD(v20) = 0;
v23 = 0;
v25 = 0;
v26 = 0;
v27 = 0;
v28 = 0;
v29 = 0;
v30 = 0;
v31 = 0;
v32 = 0;
v24 = 8;
v23 = (void *)operator new(0x40u);
v7 = (char *)v23 + ((4 * v24 - 4) & 0xFFFFFFFFFFFFFFF8LL);
v8 = operator new(0x200u);
*v7 = v8;
v28 = v7;
v26 = v8;
v27 = v8 + 512;
v32 = v7;
v30 = v8;
v31 = v8 + 512;
v25 = v8;
v29 = v8;
memset(v33, 0, 24);
v33[3] = init_proc;
v34 = 0;
v35 = 0;
v36[0] = &v37;
v36[1] = 0;
v37 = 0;
v38 = 0;
v14 = 0;
sub_1E60(v33, 0, 4096, &v14);
v9 = 16;
v10 = &v21;
v19 = 1966;
while ( v9 )
{
*(_DWORD *)v10 = 0;
v10 += 4;
--v9;
}
v18 = &unk_3040;
v20 = 0xFFC00000000LL;
std::string::_M_assign(v36, &v15);
LOBYTE(v34) = 1;
v38 = 0;
do
{
if ( (unsigned int)v20 >= v19 )
break;
sub_1890(&v18);
}
while ( (_BYTE)v34 );
v11 = v22 != 1;
sub_1DD0(&v18);
if ( v15 != v17 )
operator delete(v15);
return v11;
}
看到 "Enter key: " 基本确定找对位置了,随后定位到下面的一个循环

由于是虚拟机逆向,因此循环里面的 sub_1890() 应该就是在逐字节解释执行字节码
那么 v18,也就是 unk_3040 应该就是实际执行的字节码,而 v19 就应该是字节码的长度(1996 bytes)
跟进分析 sub_1890()
sub_1890()
unsigned __int64 __fastcall sub_1890(__int64 *a1)
{
unsigned __int64 v2; // rax
unsigned __int64 v3; // rdi
__int64 v4; // r8
unsigned __int64 v5; // rsi
int v6; // ecx
unsigned __int8 v7; // al
int v9; // eax
unsigned __int64 v10; // rsi
int v11; // ecx
unsigned __int8 v12; // si
__int64 v13; // r9
unsigned __int64 v14; // rsi
unsigned __int64 v15; // rax
__int64 v16; // rsi
unsigned __int8 v17; // r10
int v18; // eax
unsigned __int64 v19; // rax
int v20; // eax
unsigned __int64 v21; // rcx
int v22; // eax
std::ostream *v23; // rdi
int v24; // eax
int v25; // eax
unsigned __int64 v26; // rsi
int v27; // ecx
unsigned __int8 v28; // si
__int64 v29; // r9
unsigned __int64 v30; // rsi
unsigned __int64 v31; // rax
__int64 v32; // rsi
unsigned __int8 v33; // r10
int v34; // eax
unsigned __int64 v35; // rax
unsigned __int64 v36; // rax
int v37; // esi
int v38; // eax
unsigned __int8 v39; // cl
unsigned int v40; // ecx
unsigned int v41; // eax
unsigned __int64 v42; // r9
int v43; // eax
int v44; // eax
int v45; // esi
__int64 v46; // r10
unsigned __int64 v47; // rcx
int v48; // eax
int v49; // r9d
int v50; // eax
int v51; // ecx
char v52; // [rsp+7h] [rbp-11h] BYREF
unsigned __int64 v53; // [rsp+8h] [rbp-10h]
v53 = __readfsqword(0x28u);
v2 = *((unsigned int *)a1 + 4);
v3 = a1[1];
if ( v3 <= v2 )
return v53 - __readfsqword(0x28u);
v4 = *a1;
v5 = (unsigned int)(v2 + 1);
v6 = v2;
*((_DWORD *)a1 + 4) = v5;
v7 = *(_BYTE *)(v4 + v2);
if ( v7 == 107 )
{
if ( v3 <= v5 )
{
v38 = 0;
v37 = 0;
}
else
{
v36 = (unsigned int)(v6 + 2);
*((_DWORD *)a1 + 4) = v36;
v37 = *(unsigned __int8 *)(v4 + v5);
if ( v3 <= v36 )
{
v39 = v37;
v38 = 0;
}
else
{
*((_DWORD *)a1 + 4) = v6 + 3;
v38 = *(unsigned __int8 *)(v4 + v36);
v39 = v37 | v38;
}
if ( v39 > 0xFu )
return v53 - __readfsqword(0x28u);
}
v40 = *((_DWORD *)a1 + v37 + 6);
v41 = *((_DWORD *)a1 + v38 + 6);
*((_BYTE *)a1 + 201) = v40 == v41;
*((_BYTE *)a1 + 202) = v40 < v41;
return v53 - __readfsqword(0x28u);
}
if ( v7 <= 0x6Bu )
{
if ( v7 != 60 )
{
if ( v7 <= 0x3Cu )
{
if ( v7 == 25 )
{
if ( v3 <= v5 )
{
v32 = 0;
v29 = 0;
v34 = 0;
}
else
{
*((_DWORD *)a1 + 4) = v6 + 2;
v29 = *(unsigned __int8 *)(v4 + v5);
v30 = (unsigned int)(v6 + 2);
if ( v3 <= v30 )
{
v33 = v29;
v32 = 0;
v34 = 0;
}
else
{
v31 = (unsigned int)(v6 + 3);
*((_DWORD *)a1 + 4) = v31;
v32 = *(unsigned __int8 *)(v4 + v30);
v33 = v29 | v32;
if ( v3 <= v31 )
{
v34 = 0;
}
else
{
*((_DWORD *)a1 + 4) = v6 + 4;
v34 = *(unsigned __int8 *)(v4 + v31);
}
}
if ( v33 > 0xFu )
return v53 - __readfsqword(0x28u);
}
v35 = (unsigned int)(*((_DWORD *)a1 + v32 + 6) + v34);
if ( v35 < a1[24] )
*((_DWORD *)a1 + v29 + 6) = *(unsigned __int8 *)(a1[21] + v35);
return v53 - __readfsqword(0x28u);
}
if ( v7 == 26 )
{
if ( v3 <= v5 )
{
v13 = 0;
v16 = 0;
v18 = 0;
}
else
{
*((_DWORD *)a1 + 4) = v6 + 2;
v13 = *(unsigned __int8 *)(v4 + v5);
v14 = (unsigned int)(v6 + 2);
if ( v3 <= v14 )
{
v17 = v13;
v16 = 0;
v18 = 0;
}
else
{
v15 = (unsigned int)(v6 + 3);
*((_DWORD *)a1 + 4) = v15;
v16 = *(unsigned __int8 *)(v4 + v14);
v17 = v13 | v16;
if ( v3 <= v15 )
{
v18 = 0;
}
else
{
*((_DWORD *)a1 + 4) = v6 + 4;
v18 = *(unsigned __int8 *)(v4 + v15);
}
}
if ( v17 > 0xFu )
return v53 - __readfsqword(0x28u);
}
v19 = (unsigned int)(*((_DWORD *)a1 + v16 + 6) + v18);
if ( v19 < a1[24] )
*(_BYTE *)(a1[21] + v19) = *((_DWORD *)a1 + v13 + 6);
return v53 - __readfsqword(0x28u);
}
}
else if ( v7 == 78 )
{
if ( v3 <= v5 )
{
v27 = 0;
v25 = 0;
}
else
{
*((_DWORD *)a1 + 4) = v6 + 2;
v25 = *(unsigned __int8 *)(v4 + v5);
v26 = (unsigned int)(v6 + 2);
if ( v3 <= v26 )
{
v28 = v25;
v27 = 0;
}
else
{
*((_DWORD *)a1 + 4) = v6 + 3;
v27 = *(unsigned __int8 *)(v4 + v26);
v28 = v25 | *(_BYTE *)(v4 + v26);
}
if ( v28 > 0xFu )
return v53 - __readfsqword(0x28u);
}
*((_DWORD *)a1 + v25 + 6) ^= *((_DWORD *)a1 + v27 + 6);
}
else if ( v7 == 94 )
{
if ( v3 <= v5 )
{
v9 = 0;
v11 = 0;
}
else
{
*((_DWORD *)a1 + 4) = v6 + 2;
v9 = *(unsigned __int8 *)(v4 + v5);
v10 = (unsigned int)(v6 + 2);
if ( v3 <= v10 )
{
v12 = v9;
v11 = 0;
}
else
{
*((_DWORD *)a1 + 4) = v6 + 3;
v11 = *(unsigned __int8 *)(v4 + v10);
v12 = v9 | *(_BYTE *)(v4 + v10);
}
if ( v12 > 0xFu )
return v53 - __readfsqword(0x28u);
}
*((_DWORD *)a1 + v9 + 6) = *((_DWORD *)a1 + v11 + 6);
}
return v53 - __readfsqword(0x28u);
}
v46 = (unsigned int)v5;
if ( v3 <= (unsigned int)v5 )
{
v47 = (unsigned int)(v6 + 4);
v49 = 0;
if ( v3 <= v47 )
{
v48 = 0;
goto LABEL_66;
}
}
else
{
LODWORD(v5) = v6 + 2;
v47 = (unsigned int)(v6 + 5);
v48 = 0;
*((_DWORD *)a1 + 4) = v5;
v49 = *(unsigned __int8 *)(v4 + v46);
if ( v3 <= v47 )
goto LABEL_65;
v46 = (unsigned int)v5;
}
v50 = (*(unsigned __int8 *)(v4 + (unsigned int)(v5 + 1)) << 8)
| *(unsigned __int8 *)(v4 + v46)
| (*(unsigned __int8 *)(v4 + v47) << 24);
v51 = *(unsigned __int8 *)(v4 + (unsigned int)(v5 + 2));
*((_DWORD *)a1 + 4) = v5 + 4;
v48 = (v51 << 16) | v50;
LABEL_65:
if ( (unsigned __int8)v49 > 0xFu )
return v53 - __readfsqword(0x28u);
LABEL_66:
*((_DWORD *)a1 + v49 + 6) = v48;
return v53 - __readfsqword(0x28u);
}
if ( v7 == 0xE3 )
{
v42 = (unsigned int)(v6 + 4);
v43 = 0;
if ( v3 > v42 )
{
v44 = *(unsigned __int8 *)(v4 + v5)
| (*(unsigned __int8 *)(v4 + (unsigned int)(v6 + 3)) << 16)
| (*(unsigned __int8 *)(v4 + (unsigned int)(v6 + 2)) << 8);
v45 = *(unsigned __int8 *)(v4 + v42);
*((_DWORD *)a1 + 4) = v6 + 5;
v43 = (v45 << 24) | v44;
}
if ( *((_BYTE *)a1 + 201) )
*((_DWORD *)a1 + 4) = v43;
return v53 - __readfsqword(0x28u);
}
if ( v7 > 0xE3u )
{
if ( v7 == 0xFE )
{
v24 = 0;
if ( v3 > v5 )
{
*((_DWORD *)a1 + 4) = v6 + 2;
v24 = *(unsigned __int8 *)(v4 + v5);
}
*((_DWORD *)a1 + 21) = v24;
}
else if ( v7 == 0xFF )
{
*((_BYTE *)a1 + 200) = 0;
}
return v53 - __readfsqword(0x28u);
}
if ( v7 != 0xA1 )
{
if ( v7 == 0xA3 )
{
if ( v3 <= v5 )
{
v20 = 0;
LABEL_32:
v21 = a1[30];
if ( v21 >= a1[27] )
{
*((_DWORD *)a1 + v20 + 6) = 0;
}
else
{
a1[30] = v21 + 1;
*((_DWORD *)a1 + v20 + 6) = *(unsigned __int8 *)(a1[26] + v21);
}
return v53 - __readfsqword(0x28u);
}
*((_DWORD *)a1 + 4) = v6 + 2;
v20 = *(unsigned __int8 *)(v4 + v5);
if ( (unsigned __int8)v20 <= 0xFu )
goto LABEL_32;
}
return v53 - __readfsqword(0x28u);
}
if ( v3 <= v5 )
{
v22 = 0;
}
else
{
*((_DWORD *)a1 + 4) = v6 + 2;
v22 = *(unsigned __int8 *)(v4 + v5);
if ( (unsigned __int8)v22 > 0xFu )
return v53 - __readfsqword(0x28u);
}
v52 = *((_DWORD *)a1 + v22 + 6);
v23 = (std::ostream *)std::__ostream_insert<char,std::char_traits<char>>(&std::cout, &v52, 1);
return std::ostream::flush(v23);
}
反编译出来的伪 c 代码是多个 if - else 嵌套形成的 vm 解释器,自定义的 opcode 如下
说明:
sub_1890为一个 16 寄存器(v0–v15)+ 标志位(EQ/LT)+ 运行标志的字节码 VM 分发器。指令操作码即字节码首字节;寄存器号参数须为 0~15;多字节立即数均为小端序。
19 <dst:u8> <base:u8> <off:u8> | |||
1A <src:u8> <base:u8> <off:u8> | |||
3C <dst:u8> <imm:u32> | |||
4E <a:u8> <b:u8> | |||
5E <dst:u8> <src:u8> | |||
6B <a:u8> <b:u8> | |||
A1 <dst:u8> | |||
A3 <dst:u8> | |||
E3 <target:u32> | |||
FE <imm:u8> | |||
FF |
有了映射表,再写个 ida python 脚本把字节码 unk_3040,也就是 0x3040 地址处的 1966 bytes 的数据 dump 下来就可以还原程序自定义指令的汇编代码了
需要 dump 数据脚本的朋友可以私信联系我,需要将字节码还原为汇编的脚本的朋友可以看这篇文章
[NCTF 2026] VM Encryptor(虚拟机逆向)
其实我可以把工具链接直接贴出来的,但我就是想给之前的文章引个流
( ̄▽ ̄)σ
分析汇编代码
结果很长,这里仅展示关键部分
读取用户输入
0000 | 3C 03 00 00 00 00 | <..... | |
0006 | A3 04 | .. | |
0008 | 1A 04 03 00 | .... | |
000C | A3 04 | .. | |
000E | 1A 04 03 01 | .... | |
0012 | A3 04 | .. | |
0014 | 1A 04 03 02 | .... | |
0018 | A3 04 | .. |
刚开始是向 0x03 寄存器中写入 0,应该只是一步初始化
随后一直到 0xFE 都是下面的循环:
通过 IN指令读取 1 byte 的用户输入存储到0x04寄存器通过 STORE指令读取0x04寄存器的低 8 位存储在内存0x03地址后的指定偏移处
简单来说就是读取并存储用户输入
异或加密
这一段也是一个循环,这里仅展示前 3 个循环
0102 | 3C 09 01 00 00 00 | <..... | |
0108 | 19 0A 03 00 | .... | |
010C | 3C 0B 6C 00 00 00 | <.l... | |
0112 | 4E 0A 0B | N.. | |
0115 | 3C 0B 08 00 00 00 | <..... | |
011B | 6B 0A 0B | k.. | |
011E | E3 29 01 00 00 | .)... | |
0123 | 3C 09 00 00 00 00 | <..... | |
0129 | 19 0A 03 01 | .... | |
012D | 3C 0B B4 00 00 00 | <..... | |
0133 | 4E 0A 0B | N.. | |
0136 | 3C 0B D5 00 00 00 | <..... | |
013C | 6B 0A 0B | k.. | |
013F | E3 4A 01 00 00 | .J... | |
0144 | 3C 09 00 00 00 00 | <..... | |
014A | 19 0A 03 02 | .... | |
014E | 3C 0B 7C 00 00 00 | <.|... | |
0154 | 4E 0A 0B | N.. | |
0157 | 3C 0B 0E 00 00 00 | <..... | |
015D | 6B 0A 0B | k.. | |
0160 | E3 6B 01 00 00 | .k... |
一直到 0x667 都是如下循环:
通过 MOVI32指令向0x09寄存器中存储 0 或 1(除了第一个循环是 1,其他都是 0,在这里这个寄存器只是一个标志位)通过 LOAD指令读取内存0x03地址指定偏移处的值(也就是刚刚读取的用户输入)加载到0x0A寄存器中通过 MOVI32指令加载一个指定的异或值到0x0B寄存器中通过 XOR指令异或0x0A和0x0B寄存器中的值,并将结果存储到0x0A寄存器中通过 MOVI32指令加载一个指定的目标值到0x0B寄存器中通过 CMP指令比较0x0A寄存器中的异或结果和0x0B寄存器中的目标值通过 JEQ指令,若结果不相同则直接跳转至下一个循环的第 2 步,否则执行第 1 步将标志位0x09寄存器中的值置为 0
简单来说就是将用户输入与内置的字符串逐字节异或,并将结果与目标字符串比较
判断
为了便于理解,把最后一个循环也贴出来
064B | 3C 09 00 00 00 00 | <..... | |
0651 | 19 0A 03 29 | ...) | |
0655 | 3C 0B 3A 00 00 00 | <.:... | |
065B | 4E 0A 0B | N.. | |
065E | 3C 0B 47 00 00 00 | <.G... | |
0664 | 6B 0A 0B | k.. | |
0667 | E3 72 06 00 00 | .r... | |
066C | 3C 09 00 00 00 00 | <..... | |
0672 | 3C 0D 01 00 00 00 | <..... | |
0678 | 6B 09 0D | k.. | |
067B | E3 E3 06 00 00 | ..... |
执行完最后一个循环的 CMP 后,若结果不同则将 0x09 寄存器置 0,若结果相同则不变
如此实现了只要有一个字节不一样,标志位 0x09 寄存器就会被置为 0,否则保持为 1
随后通过 MOVI32 指令将 0x0D 寄存器置为 1
最后的 CMP 指令:
若标志位 0x09寄存器与0x0D寄存器相等(即0x09寄存器为 1,每个字符都相同),则跳转至0x6E3输出Correct! Flag verified.若标志位 0x09寄存器与0x0D寄存器不相等(即0x09寄存器为 0,存在不相同的字符),则不跳转,直接输出Wrong key!
解密
解密很简单,把目标值与内置字符串逐字节异或就能拿到 flag
key = [0x6c, 0xb4, 0x7c, 0xfc, 0x07, 0x96, 0x2a, 0x92, 0x60, 0x3f]
obj = [0x08, 0xd5, 0x0e, 0x88, 0x7c, 0xae, 0x1a, 0xf0, 0x50, 0x07]
for a, b in zip(obj, key):
print(chr(a ^ b), end='')
'''
输出内容:
dart{80b08
'''
这里我只把内置字符串和目标字符串的前几个字符抄了下来,懒得全抄了
这两个字符串并不是连续分布在字节码中的,所以不好直接 dump,估计只能通过正则匹配一个一个提取出来了
反正从输出内容也能看出思路没问题,现场比赛的时候老老实实一个一个抄就行了哈哈哈
╮( ̄▽ ̄)╭