集训(三)-ezvm

0x01 题目来源

暑期训练

0x02 思路

vm保护,在逆向中,关键点主要有:vm_init(初始化),vm_start(虚拟机的启动入口),opcode(操作码),dispatcher(解释器,根据操作码进行操作)

进入,查看反汇编
image
经典switch语句,那么byte_140004000就是储存操作码的数组
但是观察switch与普通的vm不同的是,没有发现指针型或整形变量来替代寄存器,所以寻找类似寄存器的存在.

观察后发现character很可疑,"*(&)",类似于数组的操作方式(地址+偏移量),同时每次都是对其进行操作,与寄存器操作相同,更改类型为数组,改名reg(即为寄存器)
image

接下来就是读取指令

example push指令

image

case==1 时,将reg的值传入参数,判断变量是否为255(overflow一眼模拟栈操作),将某个变量转为char*类型,将v2进行"*4"操作,又是"地址+偏移量"的格式,即stack[n]=reg

example cmp指令

image
if语句,模拟cmp指令,改变v2(相当于汇编指令中的标志符),来为下面的jz以及jnz提供判断

0x03 解题

根据线性扫描来输出整个vm进行的操作(即完整输出指令,不进行跳转)
由于本题中跳转是根据当前地址跳转的,所以输出后还要进行运算,所以在编写时建议标记当前的地址

0x04 转换

#include <iostream>
unsigned char code[] =
{
    9,   1,   1,   9,   2,   2,  18,   1,  20,   1,
   12,   0,   2,  14,  11,  17,  11,   0,   8,  12,
    0,   2,  14, 238,  13, 245,  18,   1,  20,   1,
    1,   0,   2,   2,  18,   1,  20,   1,   1,   0,
    2,   3,  18,   1,  20,   1,   1,   0,   2,   4,
    9,   5,   5,  20,   5,  18,   1,  20,   1,   1,
    0,  12,   5,   2,  14,   2,  13, 241,  18,   1,
   20,   1,   1,   0,   2,   5,  18,   1,  20,   1,
    1,   0,   2,   6,  18,   1,  20,   1,   1,   0,
    2,   7,  18,   1,   9,   1,   1,   1,   0,   2,
    8,   9,   2,   2,  22,  13,   4,   0,   0,   0,
    9,   9,   9,   9,  11,  11,   9,  12,  12,  16,
    9,  10,  10,  12,   9,  10,  14,   7,  20,  10,
   10,   0,   8,  13, 244,   8,  11,   0,  20,   9,
   12,   9,  13,  14,   2,  13, 228,   9,   9,   9,
   16,   9,  10,  10,  12,   9,  10,  14,   7,  20,
   10,  10,   0,   8,  13, 244,   8,  12,   0,  20,
    9,  12,   9,  13,  14,   2,  13, 228,   9,  14,
   14,   9,  15,  15,   3,  15,   3,   1,  12,   2,
    9,  10,   9,   4,   3,   9,   5,   1,  12,   2,
   10,   3,  10,  15,   9,   9,  10,   1,  12,   2,
   10,  11,  10,   5,   3,  10,   6,   9,   9,  10,
    3,  11,   9,   1,  11,   2,   9,  10,   9,   4,
    3,   9,   7,   1,  11,   2,  10,   3,  10,  15,
    9,   9,  10,   1,  11,   2,  10,  11,  10,   5,
    3,  10,   8,   9,   9,  10,   3,  12,   9,  20,
   14,  12,  14,   4,  14,   2,  13, 172,   2,   0,
   12,   0,  11,  15,  16,   2,   0,  12,   0,  12,
   15,   9,  20,   2,  12,   2,  13,  14,   7,  23,
  110,   0,   0,   0,  24,   0,  24,   1,   0,   0,
    0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
    0,   0,   0,   0,   0,   0,   0,   0,   0,   0
};

int main() {
    using namespace std;
    int pc=0;int v2;
    unsigned int reg[64] = {};
    while (1) {
      if (code[pc]) {
       switch ( code[pc] )
      {
        case 1:
          printf("pc=%d push(reg[%d])\n",pc,code[pc + 1]);
        pc += 2;
        continue;
        case 2:
          printf("pc=%d pop(reg[%d])\n",pc,code[pc + 1]);
        pc += 2;
        continue;
        case 3:
          printf("pc=%d reg[%d]+=reg[%d]\n",pc,code[pc + 1],code[pc + 2]);
        pc += 3;
        continue;
        case 4:
          printf("pc=%d reg[%d]-=reg[%d]\n" ,pc,code[pc + 1],code[pc + 2]);
        pc += 3;
        continue;
        case 5:
          printf("pc=%d reg[%d]*=reg[%d]\n",pc,code[pc + 1],code[pc + 2]);
        pc += 3;
        continue;
        case 6:
          printf("pc=%d reg[%d]%%=reg[%d]\n",pc,code[pc + 1],code[pc + 2]);
        pc += 3;
        continue;
        case 7:
          printf("pc=%d reg[%d]&=reg[%d]\n",pc,code[pc + 1],code[pc + 2]);
        pc += 3;
        continue;
        case 8:
          printf("pc=%d reg[%d]|=reg[%d]\n",pc,code[pc + 1],code[pc + 2]);
        pc += 3;
        continue;
        case 9:
          printf("pc=%d reg[%d]^=reg[%d]\n",pc,code[pc + 1],code[pc + 2]);
        pc += 3;
        continue;
        case 10:
          printf("pc=%d reg[%d]<<=%d\n",pc,code[pc + 1],code[pc + 2]);
        pc += 3;
        continue;
        case 11:
          printf("pc=%d reg[%d]>>=%d\n",pc,code[pc + 1],code[pc + 2]);
        pc += 3;
        continue;
        case 12:
          v2 = 1;
         printf("pc=%d cmp reg[%d] reg[%d]\n",pc,code[pc + 1],code[pc + 2]);
        pc += 3;
        continue;
        case 13:
          printf("pc=%d pc+=%d\n",pc,(char)code[pc + 1] + 2);
         pc +=  2;
        continue;
        case 14:
          printf("pc=%d if equal pc+=%d\n",pc,(char)code[pc+1]+2);
        pc += 2;
        continue;
        case 15:
          printf("pc=%d if not equal pc+=%d\n",pc,(char)code[pc+1]+2);
        pc += 2;
        continue;
        case 16:
          printf("pc=%d rge[0] = getchar()\n",pc);
        ++pc;
        continue;
        case 17:
          printf("pc=%d putchar(reg[0])\n",pc);
        ++pc;
        continue;
        case 18:
          printf("pc=%d reg[0] = aPlzShowMeYourF[reg[%d]]\n",pc,code[pc + 1]);
        pc += 2;
        continue;
        case 19:
          printf("pc=%d aPlzShowMeYourF[reg[%d]] = reg[0]\n",pc,code[pc + 1]);
        pc += 2;
        continue;
        case 20:
          printf("pc=%d ++reg[%d]\n",pc,code[pc + 1]);
        pc += 2;
        continue;
        case 21:
          printf("pc=%d --reg[%d]\n",pc,code[pc + 1]);
        pc += 2;
        continue;
        case 22:
          printf("pc=%d reg[%d] = *(_DWORD *)&%d \n",pc,code[pc + 1],code[pc + 2]);
        pc += 6;
        continue;
        case 23:
          printf("pc=%d pc = *(_DWORD *)&%d\n",pc,code[pc + 1]);
         pc += 5;
        continue;
        case 24:
          printf("pc=%d return %d\n",pc, code[pc + 1]);
          pc += 2;
          break;
         default:
           return 0;
      }
      }
    }
    return 0;
}
//最终的输出存在问题,在输出return 1时地址对不上,反正其他的都对我就不管了~

0x05 寻找规律

在解释器转换完毕后还要对输出的一大堆汇编指令寻找规律,从而理解程序的输出,输入,以及加密算法,最终达到解密的目的
贴两个关键的

input 实现
pc=123 cmp reg[9] reg[10]
pc=126 if equal pc+=9   //jmp 135
pc=128 ++reg[10]        //reg10==1
pc=130 reg[0]<<=8       //reg0==0
pc=133 pc+=-10
pc=135 reg[11]|=reg[0]      //第一个字符给reg11的低位
pc=138 ++reg[9]
pc=140 cmp reg[9] reg[13]       //reg13==4
pc=143 if equal pc+=4
pc=145 pc+=-26      //jmp 119
//先读取4个字节,把flag的第一位赋给reg11的低位,以此类推reg11存储的是flag的1-4字节(由低位到高位)
crypto 实现
pc=178 reg[14]^=reg[14]     //reg14=0
pc=181 reg[15]^=reg[15]
pc=184 reg[15]+=reg[3]      //reg15=-1640531527
pc=187 push(reg[12])
pc=189 pop(reg[9])          //reg9=reg12(后四个字符)
pc=191 reg[9]<<=4
pc=194 reg[9]+=reg[5]       //reg9<<=4,reg9+=1406675993
pc=197 push(reg[12])
pc=199 pop(reg[10])         //reg10=reg12(后四个字符)
pc=201 reg[10]+=reg[15]     //reg10-=1640531527
pc=204 reg[9]^=reg[10]      //reg9^=reg10
pc=207 push(reg[12])
pc=209 pop(reg[10])
pc=211 reg[10]>>=5          //reg10=reg12,reg10>>5
pc=214 reg[10]+=reg[6]      //reg10+=816345593
pc=217 reg[9]^=reg[10]      //reg9^=reg10
pc=220 reg[11]+=reg[9]      //reg11+=reg9
//对前四个字节进行加密,其中的reg12来自于上一次进行加密
crypto中的循环
pc=261 cmp reg[14] reg[4]       //reg4=32 循环32次
pc=264 if equal pc+=4
pc=266 pc+=-82
最后的判断
pc=270 cmp reg[0] reg[11]
pc=273 if not equal pc+=18
pc=275 pop(reg[0])          //reg0=-1599809085
pc=277 cmp reg[0] reg[12]
pc=280 if not equal pc+=11
pc=282 ++reg[2]
pc=284 cmp reg[2] reg[13]
//轮流出栈比较加密后的前八个字节

0x06 EXP

#include <iostream>

int num[]={-1433586645,89260403,-1158616304,714172226,-768283605,-1059298339,-1599809085,-1667336015};

int main() {
    using namespace std;
    int i=7;
    
    while (i>0) {
        int e=0;

        for (int k=0;k<32;k++) {
            e-=1640531527;
        }

        for (int j=0;j<32;j++) {
            num[i-1]-=((((unsigned int)num[i])>>5)+1538544130)^(num[i]+e)^((num[i]<<4)+473825587);
            num[i]-=((((unsigned int)num[i-1])>>5)+816345593)^(num[i-1]+e)^((num[i-1]<<4)+1406675993);
            e+=1640531527;
        }
        i-=2;
    }
    printf(reinterpret_cast<char *>(num));
    return 0;
}

最后输出的flag是四字节一个单位,倒置,详细自寻查看,可以通过python进行简单转换

str = "VM?}mp13y_s13Al1_a_RT_1t{1snflag"
fs = [str[i * 4:i * 4 + 4] for i in range(0, len(str) // 4)]
print("".join(fs[::-1]))

魏祖,古希腊掌管python脚本的神!~

0x07 总结

收获很多的一道题
1.拷贝数据的时候少了一个3导致最终的flag乱码(迫害了别人一个小时时间,我要成为cv糕手!)
2.更新了对于vm的印象,并非有变量来直接替代寄存器,其实数组也是一种很有效的方式
3.在进行reverse时,使用线性扫描,并且在跳转时要考虑自身指令的长度
4.最后位运算的加密也让人眼前一亮
5.对于题中的变量,一定要注意数据格式,如DWORD的类型不能按照char类型进行导出
综上,这道题是一道很有意义的vm保护,难度以及知识点把握的都很恰当(对我这样的新人来说),受益匪浅.

posted @ 2024-07-16 16:21  Cia1lo  阅读(14)  评论(0编辑  收藏  举报