[虚拟机逆向]UNCTF - 2019 EasyVm

[虚拟机逆向]UNCTF - 2019 EasyVm

前言

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

什么是虚拟机逆向

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

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

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

前期准备

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

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

题解

主函数

__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

你可能感兴趣的:(学习笔记,c++,算法,网络安全)