[虚拟机逆向]UNCTF - 2019 EasyVm
前言
虚拟机逆向在Hgame2023中遇见过,这次刷题中又遇见了,写一篇文章总结一下
什么是虚拟机逆向
虚拟机逆向是指对一个运行在虚拟机上的程序进行逆向工程。虚拟机是一种软件层,它模拟了一种计算机架构,允许程序在不同的平台上运行。在虚拟机上运行的程序通常使用一种特定的指令集,这个指令集不同于在物理机器上运行的指令集。
虚拟机逆向包括对虚拟机本身的分析,以及对在虚拟机上运行的程序的分析。对于虚拟机本身的分析,可以探究虚拟机的指令集、内存布局、代码执行流程等方面。对于在虚拟机上运行的程序的分析,可以通过反编译、动态调试等手段获取程序的源代码、调用栈信息、内存映射等信息,以此来理解程序的行为和工作原理。
虚拟机逆向常用于软件逆向工程、漏洞挖掘和安全评估等领域。
前期准备
要进行虚拟机逆向,需要具备以下几点准备:
- 计算机基础知识:逆向是计算机领域的高级技术,需要对计算机的结构和原理有一定的了解。
- 操作系统和编程语言的基础:要逆向虚拟机,掌握一种或多种编程语言非常有帮助。同时熟悉操作系统的基础知识也是必要的,以便能够在不同的操作系统上进行虚拟机逆向。
- 调试工具的使用:在虚拟机逆向过程中,需要使用各种调试器和分析工具,例如IDA、OllyDbg等,这需要对这些工具的使用方法有一定的掌握。
- 熟悉汇编语言:虚拟机的实现常常会涉及到汇编语言,因此熟悉汇编语言是进行虚拟机逆向的必要条件。
- 拥有调试虚拟机的实践经验:虚拟机逆向需要具有一定的实践经验,了解虚拟机的实现原理和逆向技巧,需要进行大量的实践操作才能熟练掌握。
题解
主函数
__int64 __fastcall main(int a1, char **a2, char **a3)
{
unsigned int (__fastcall ***v3)(_QWORD, void *, void *, char *); // rbx
char s[96]; // [rsp+10h] [rbp-80h] BYREF
int v6; // [rsp+70h] [rbp-20h]
unsigned __int64 v7; // [rsp+78h] [rbp-18h]
v7 = __readfsqword(0x28u);
memset(s, 0, sizeof(s));
v6 = 0;
v3 = (unsigned int (__fastcall ***)(_QWORD, void *, void *, char *))operator new(0x28uLL);
sub_400C1E(v3, a2);
puts("please input your flag:");
scanf("%s", s);
if ( strlen(s) != 32 )
{
puts("The length of flag is wrong!");
puts("Please try it again!");
}
if ( (**v3)(v3, &unk_602080, &unk_6020A0, s) )
{
puts("Congratulations!");
printf("The flag is UNCTF{%s}", s);
}
return 1LL;
}
可以发现主函数非常的简洁就是做了长度的判断,然后还有一个v3作为一个函数指针然后将输入的函数作为传入参数进行了一些判断
分析完毕,我们主要目标就是跟进这个函数指针,查看对传入的字符串做了一些什么操作。
首先我们看到sub_400C1E这个函数是对v3进行了操作的然后传入参数为a2,我们先分析该函数对v3指针做了一些什么操作
sub_400C1E
__int64 __fastcall sub_400C1E(__int64 a1)
{
__int64 result; // rax
*(_QWORD *)a1 = off_4010A8;
*(_QWORD *)(a1 + 8) = 0LL;
*(_BYTE *)(a1 + 16) = 0;
*(_BYTE *)(a1 + 17) = 0;
*(_BYTE *)(a1 + 18) = 0;
*(_DWORD *)(a1 + 20) = 0;
*(_QWORD *)(a1 + 24) = 0LL;
result = a1;
*(_QWORD *)(a1 + 32) = 0LL;
return result;
}
可以看到这个就是以a1为基地址,然后对一些偏移量进行了赋值操作,我们点开这个off_4010A8看看里面是一些什么东西
.rodata:00000000004010A8 06 08 40 00 00 00 00 00 off_4010A8 dq offset sub_400806 ; DATA XREF: sub_400C1E+8↑o
.rodata:00000000004010B0 7C 0C 40 00 00 00 00 00 dq offset sub_400C7C
.rodata:00000000004010B8 9A 0C 40 00 00 00 00 00 dq offset sub_400C9A
.rodata:00000000004010C0 B8 0C 40 00 00 00 00 00 dq offset sub_400CB8
.rodata:00000000004010C8 D6 0C 40 00 00 00 00 00 dq offset sub_400CD6
.rodata:00000000004010D0 FA 0C 40 00 00 00 00 00 dq offset sub_400CFA
.rodata:00000000004010D8 1E 0D 40 00 00 00 00 00 dq offset sub_400D1E
.rodata:00000000004010E0 42 0D 40 00 00 00 00 00 dq offset sub_400D42
.rodata:00000000004010E8 56 0D 40 00 00 00 00 00 dq offset sub_400D56
.rodata:00000000004010F0 70 0D 40 00 00 00 00 00 dq offset sub_400D70
.rodata:00000000004010F8 84 0D 40 00 00 00 00 00 dq offset sub_400D84
.rodata:0000000000401100 B0 0D 40 00 00 00 00 00 dq offset sub_400DB0
.rodata:0000000000401108 DC 0D 40 00 00 00 00 00 dq offset sub_400DDC
.rodata:0000000000401110 56 0E 40 00 00 00 00 00 dq offset sub_400E56
.rodata:0000000000401118 D0 0E 40 00 00 00 00 00 dq offset sub_400ED0
是一堆函数的地址表,那么显然,该虚拟机就是通过a1进行取址然后调用函数,对栈空间,寄存器之类的东西进行操控,我们首先看到第一个函数
sub_400806
__int64 __fastcall sub_400806(__int64 a1, __int64 a2, __int64 a3, __int64 a4)
{
*(a1 + 8) = a2 + 9;
*(a1 + 24) = a3;
*(a1 + 32) = a4;
while ( 2 )
{
switch ( **(a1 + 8) )
{
case 0xA0:
(*(*a1 + 8LL))(a1);
continue;
case 0xA1:
(*(*a1 + 16LL))(a1);
continue;
case 0xA2:
(*(*a1 + 24LL))(a1);
*(a1 + 8) += 11LL;
continue;
case 0xA3:
(*(*a1 + 32LL))(a1);
*(a1 + 8) += 2LL;
continue;
case 0xA4:
(*(*a1 + 40LL))(a1);
*(a1 + 8) += 7LL;
continue;
case 0xA5:
(*(*a1 + 48LL))(a1);
++*(a1 + 8);
continue;
case 0xA6:
(*(*a1 + 56LL))(a1);
*(a1 + 8) -= 2LL;
continue;
case 0xA7:
(*(*a1 + 64LL))(a1);
*(a1 + 8) += 7LL;
continue;
case 0xA8:
(*(*a1 + 72LL))(a1);
continue;
case 0xA9:
(*(*a1 + 80LL))(a1);
*(a1 + 8) -= 6LL;
continue;
case 0xAA:
(*(*a1 + 88LL))(a1);
continue;
case 0xAB:
(*(*a1 + 96LL))(a1);
*(a1 + 8) -= 4LL;
continue;
case 0xAC:
(*(*a1 + 104LL))(a1);
continue;
case 0xAD:
(*(*a1 + 112LL))(a1);
*(a1 + 8) += 2LL;
continue;
case 0xAE:
if ( *(a1 + 20) )
return 0LL;
*(a1 + 8) -= 12LL;
continue;
case 0xAF:
if ( *(a1 + 20) != 1 )
{
*(a1 + 8) -= 6LL;
continue;
}
return 1LL;
default:
puts("cmd execute error");
return 0LL;
}
}
}
分析之后发现是一个典型的while+switch,利用传入的参数进行寻址。我们通过动态调试来查看指令运行的先后顺序。然后把表抄下来,发现是如下结果
0xA9u 0xA3u 0xA5u 0xA6u 0xA4u 0xABu 0xA7u 0xAEu 0xA2u 0xADu 0xAFu
然后我们再分析如何得出每一个语句是干啥的
这里我只举例一点,其他的都是类似操作。
我们首先输入32个字符躲避长度判断,通过断点跳转来到该函数
我们分析0xA0指令的操作方式
首先我们需要看到a1中存储的到底是什么东西。
unsigned char ida_chars[] =
{
0xA8, 0x10, 0x40
};
通过经验就可以发现这是小端序存储的一段地址为0x4010A8,那么我们就要知道该虚拟机的基地址为0x4010A8,看到0xA0
偏移量为8也就是0x4010b0我们跳转到该地址
.rodata:00000000004010B0 dq offset sub_400C7C
此处就是调用了sub_400C7C函数,进去看看
__int64 __fastcall sub_400C7C(__int64 a1)
{
__int64 result; // rax
result = a1;
++*(a1 + 16);
return result;
}
对a1地址偏移16进行了一个++操作
[heap]:00000000013BFEC0 db 0
本身该处是0,那么之前可以看到sub_400C1E函数对一堆偏移量进行了置0操作,这里猜测他们都是寄存器
那么a1+16也就是寄存器r1,a1+17就是寄存器r2
那么结合上面在总结一下就可以得到如下指令表
操作码 | 对应指令集合 |
---|---|
*(a1+16) | 寄存器r1(占1字节) |
*(a1+17) | 寄存器r2(占1字节) |
*(a1+18) | 寄存器r3(占1字节) |
*(a1+19) | 寄存器r4(占1字节) |
*(a1+20) | 寄存器r5(占4字节) |
0xA0 | r1++ |
0xA1 | r2++ |
0xA2 | r3++ |
0xA3 | r1 -= r3 |
0xA4 | r1 ^= r2 |
0xA5 | r2 ^= r1 |
0xA6 | r1 = 0xCD |
0xA7 | r2 = r1 |
0xA8 | r3 = 0xCD |
0xA9 | r1 = input[r3] |
0xAA | r2 = input[r3] |
0xAB | func1() |
0xAC | func2() |
0xAD | func3() |
0xAE | 判断r5的值 |
0xAF | 判断r5的值 |
然后我们看到函数中的a4就是我们输入的值,然后再看到函数中的a3有一串字符和我们输入的字符长度一样,那么肯定是我们的check字符
exp
然后我们写一个反编译代码,就是通过之前的指令操作,进行反编译
opcode = [0xa9, 0xa3, 0xa5, 0xa6, 0xa4, 0xab, 0xa7, 0xae, 0xa2, 0xad, 0xaf]
for i in opcode:
if i == 0xa0:
print("r1++")
if i == 0xa1:
print("r2++")
if i == 0xa2:
print("r3++")
if i == 0xa3:
print("r1 -= r3")
if i == 0xa4:
print("r1 ^= r2")
if i == 0xa5:
print("r2 ^= r1")
if i == 0xa6:
print("r1 = 0xcd")
if i == 0xa7:
print("r2 = r1")
if i == 0xa8:
print("r3 = 0xcd")
if i == 0xa9:
print("r1 = input[r3]")
if i == 0xaa:
print("r2 = input[r3]")
if i == 0xab:
print("fun1()")
if i == 0xac:
print("func2()")
if i == 0xad:
print("func3()")
if i == 0xae:
print("if(r5==0)")
if i == 0xaf:
print("if(r5!=1)")
输出结果为:
r1 = input[r3]
r1 -= r3
r2 ^= r1
r1 = 0xcd
r1 ^= r2
fun1()
r2 = r1
if(r5!=0)
r3++
func3()
if(r5!=1)
然后我们根据该逻辑写一个解密exp
res = [0xF4, 0x0A, 0xF7, 0x64, 0x99, 0x78, 0x9E, 0x7D, 0xEA, 0x7B, 0x9E, 0x7B, 0x9F, 0x7E, 0xEB, 0x71, 0xE8, 0x00, 0xE8, 0x07, 0x98, 0x19, 0xF4, 0x25, 0xF3, 0x21, 0xA4, 0x2F, 0xF4, 0x2F, 0xA6, 0x7C]
flag = ''
temp = 0
for i in range(0,32):
flag += chr((temp ^ res[i] ^ 0xcd) + i)
temp = res[i]
print(flag)
得到flag:942a4115be2359ffd675fa6338ba23b6