[虚拟机逆向]UNCTF 您所在的位置:网站首页 unvtfhomepage [虚拟机逆向]UNCTF

[虚拟机逆向]UNCTF

2023-07-29 02:37| 来源: 网络整理| 查看: 265

[虚拟机逆向]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字节)0xA0r1++0xA1r2++0xA2r3++0xA3r1 -= r30xA4r1 ^= r20xA5r2 ^= r10xA6r1 = 0xCD0xA7r2 = r10xA8r3 = 0xCD0xA9r1 = input[r3]0xAAr2 = input[r3]0xABfunc1()0xACfunc2()0xADfunc3()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



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有