SHangwendada 发表于 2023-3-6 21:54:02

2019UNCTF easyvm

# [虚拟机逆向]UNCTF - 2019 EasyVm

## 前言

虚拟机逆向在Hgame2023中遇见过,这次刷题中又遇见了,写一篇文章总结一下

## 什么是虚拟机逆向

虚拟机逆向是指对一个运行在虚拟机上的程序进行逆向工程。虚拟机是一种软件层,它模拟了一种计算机架构,允许程序在不同的平台上运行。在虚拟机上运行的程序通常使用一种特定的指令集,这个指令集不同于在物理机器上运行的指令集。

虚拟机逆向包括对虚拟机本身的分析,以及对在虚拟机上运行的程序的分析。对于虚拟机本身的分析,可以探究虚拟机的指令集、内存布局、代码执行流程等方面。对于在虚拟机上运行的程序的分析,可以通过反编译、动态调试等手段获取程序的源代码、调用栈信息、内存映射等信息,以此来理解程序的行为和工作原理。

虚拟机逆向常用于软件逆向工程、漏洞挖掘和安全评估等领域。

## 前期准备

要进行虚拟机逆向,需要具备以下几点准备:

1. 计算机基础知识:逆向是计算机领域的高级技术,需要对计算机的结构和原理有一定的了解。
2. 操作系统和编程语言的基础:要逆向虚拟机,掌握一种或多种编程语言非常有帮助。同时熟悉操作系统的基础知识也是必要的,以便能够在不同的操作系统上进行虚拟机逆向。
3. 调试工具的使用:在虚拟机逆向过程中,需要使用各种调试器和分析工具,例如IDA、OllyDbg等,这需要对这些工具的使用方法有一定的掌握。
4. 熟悉汇编语言:虚拟机的实现常常会涉及到汇编语言,因此熟悉汇编语言是进行虚拟机逆向的必要条件。
5. 拥有调试虚拟机的实践经验:虚拟机逆向需要具有一定的实践经验,了解虚拟机的实现原理和逆向技巧,需要进行大量的实践操作才能熟练掌握。

## 题解

### 主函数

~~~c
__int64 __fastcall main(int a1, char **a2, char **a3)
{
unsigned int (__fastcall ***v3)(_QWORD, void *, void *, char *); // rbx
char s; // BYREF
int v6; //
unsigned __int64 v7; //

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

~~~c
__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看看里面是一些什么东西

~~~assembly
.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

~~~c
__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我们跳转到该地址

~~~ass
.rodata:00000000004010B0 dq offset sub_400C7C
~~~

此处就是调用了sub_400C7C函数,进去看看

~~~c
__int64 __fastcall sub_400C7C(__int64 a1)
{
__int64 result; // rax

result = a1;
++*(a1 + 16);
return result;
}
~~~

对a1地址偏移16进行了一个++操作

~~~ass
: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   |
|   0xAA   |r2 = input   |
|   0xAB   |      func1()      |
|   0xAC   |      func2()      |
|   0xAD   |      func3()      |
|   0xAE   |    判断r5的值   |
|   0xAF   |    判断r5的值   |

然后我们看到函数中的a4就是我们输入的值,然后再看到函数中的a3有一串字符和我们输入的字符长度一样,那么肯定是我们的check字符

### exp

然后我们写一个反编译代码,就是通过之前的指令操作,进行反编译

~~~python
opcode =

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")
   if i == 0xaa:
      print("r2 = input")
   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)")
~~~

输出结果为:

~~~python
r1 = input
r1 -= r3
r2 ^= r1
r1 = 0xcd
r1 ^= r2
fun1()
r2 = r1
if(r5!=0)
r3++
func3()
if(r5!=1)
~~~

然后我们根据该逻辑写一个解密exp

~~~pytho
res =
flag = ''
temp = 0
for i in range(0,32):
    flag += chr((temp ^ res ^ 0xcd) + i)
    temp = res

print(flag)
~~~

得到flag:942a4115be2359ffd675fa6338ba23b6
            
页: [1]
查看完整版本: 2019UNCTF easyvm