[网鼎杯 2020 青龙组]singal
本文最后更新于213 天前,其中的信息可能已经过时,如有错误可以直接在文章下留言

最近在学虚拟机逆向的题目,上学期就触碰到的知识点,但是对于当时的我来说太难了,这学期遇到的比赛还是有虚拟机的题目的,而且学长在校内比赛很喜欢出,而且我每次都不会,于是决心好好补一下。先从简单的做起。

IDA打开,找到主函数,vm_operad应该就是虚拟机的函数,这个v4又传入了这个函数,可能是opcode,我们点进该函数当中。

这里有个while的循环语句,switch语句,那肯定就是虚拟机的调度器了,这里的a1就是v4,也就是opcode,而v9作为opcode数组的索引,再结合后面代码它的变化,应该就相当于汇编语言当中的eip寄存器。当eip大于a2的时候,这个函数就会结束,而a2在主函数传入的是数字114,这应该是opcode的大小,我们回到主函数去看一下opcode。

但是大小不止114,其中有很多零。而且每两个数字之间都刚好有三个零,我们Shift+e导出来,然后把零去掉看看。

还要去掉其中的255,发现刚刚好大小是114,我们就得到了opcode,关键的一步!

然后再分析一下虚拟机函数

int __cdecl vm_operad(int *opcode, int a2)
{
  int result; // eax
  char Str[200]; // [esp+13h] [ebp-E5h] BYREF
//这里Str相当于一个开辟的内存空间
  char v4; // [esp+DBh] [ebp-1Dh]
  int v5; // [esp+DCh] [ebp-1Ch]
  int v6; // [esp+E0h] [ebp-18h]
  int v7; // [esp+E4h] [ebp-14h]
  int v8; // [esp+E8h] [ebp-10h]
  int eip_0; // [esp+ECh] [ebp-Ch]
  eip_0 = 0;
  v8 = 0;
  v7 = 0;
  v6 = 0;
  v5 = 0;
//五个重要的变量
  while ( 1 )
  {
    result = eip_0;
    if ( eip_0 >= a2 )
//当eip的值大于opcode的大小,即执行完虚拟机的调度器操作后,结束函数
      return result;
    switch ( opcode[eip_0] )
    {
      case 1:
        Str[v6 + 100] = v4;
        ++eip_0;
        ++v6;
        ++v8;
        break;
//将Str的内存分成了两块,一块存储我们的输入,一块用于存储数据运算后得到的v4
//唯一一个对v8和v6进行运算的情况。
      case 2:
        v4 = opcode[eip_0 + 1] + Str[v8];
        eip_0 += 2;
        break;
      case 3:
        v4 = Str[v8] - LOBYTE(opcode[eip_0 + 1]);
        eip_0 += 2;
        break;
      case 4:
        v4 = opcode[eip_0 + 1] ^ Str[v8];
        eip_0 += 2;
        break;
      case 5:
        v4 = opcode[eip_0 + 1] * Str[v8];
        eip_0 += 2;
        break;
#上面的case 2 到case 5 都是将opcode中的当前取值的下一位和我们的输入进行运算处理,然后赋值给v4
      case 6:
        ++eip_0;
        break;
//没啥用的
      case 7:
        if ( Str[v7 + 100] != opcode[eip_0 + 1] )
        {
          printf("what a shame...");
          exit(0);
        }
        ++v7;
        eip_0 += 2;
        break;
//最后的比较,这里
      case 8:
        Str[v5] = v4;
        ++eip_0;
        ++v5;
        break;
//我们存储在内存当中的输入经过case 2 到case 5中某个处理之后,发生改变。
//唯一一个对v5进行运算的情况
      case 10:
        read(Str);
        ++eip_0;
        break;
//读取一个长度为15的输入,可以点进read函数看
      case 11:
        v4 = Str[v8] - 1;
        ++eip_0;
        break;
      case 12:
        v4 = Str[v8] + 1;
        ++eip_0;
        break;
//case 11到case 12也是对数据进行运算操作
      default:
        continue;
    }
  }
}

这里我们其实思路已经比较明了了,上面的opcode,所有的7后面的数据就是要密文,就是要比较的数据,第一个数据是34。我们其实可以尝试一下手搓flag第一位。

读取输入后,eip寄存器加1,当前opcode为4,进入case 4,此时v4为flag的第一位与16进行异或运算,然后eip寄存器加2,当前opcode为8,进入case 8,然后将v4存入Str[v5],此时v5为0,也就是Str[0]所以就是覆盖Str的第一位,这个第一位往后好像一开始存的是我们的flag。执行完case 8之后,v5=1,然后进入case 3,其实就是v4 = v4- opcode[eip_0 + 1],v4=v4 – 5。然后直接进入case 1,将第一位flag处理完后,也就是v4,放入另外一个内存空间当中。

那flag[0]=(34+5)^16=55,这是Ascii编号,转化成字符就是7,我们可以一直这样动态调试手搓。

得到15个算法是

0x22 = (flag[0] ^ 0xa) - 5

0x3f = (flag[1] ^ 0x20) * 3

0x34 = (flag[2] - 2) - 1

0x32 = (flag[3] + 1) ^ 4

0x72 = (flag[4] * 3) - 0x21

0x33 = (flag[5] - 1) - 1 

0x18 = (flag[6] ^ 9) - 0x20

0xa7 = (flag[7] + 0x51) ^ 0x24

0x31 = (flag[8] +1 ) - 1

0xf1 = (flag[9] * 2) + 0x25

0x28 = (flag[10] + 0x36) ^ 0x41

0x84 = (flag[11] + 0x20) * 1

0xc1 = (flag[12] *3) + 0x25

0x1e = (flag[13] ^ 9) - 0x20

0x7a = (flag[14] + 0x41) + 1 

然后写个脚本

b=[34,63,52,50,114,51,24,167,49,241,40,132,193,30,122]
flag=[0]*15
flag[0]=(b[0]+5)^0x10
flag[1]=(b[1]//3)^0x20
flag[2]=(b[2]+3)
flag[3]=(b[3]^4)-1
flag[4]=(b[4]+0x21)//3
flag[5]=b[5]+2
flag[6]=(b[6]+0x20)^9
flag[7]=(b[7]^0x24)-0x51
flag[8]=b[8]
flag[9]=(b[9]-0x25)//2
flag[10]=(b[10]^0x41)-0x36
flag[11]=(b[11]-0x20)
flag[12]=(b[12]-0x25)//3
flag[13]=(b[13]+0x20)^9
flag[14]=(b[14]-1)-0x41
result = ""
for i in range(len(flag)):
        result += chr(flag[i])
print(result)
#输出757515121f3d478

所以flag就是flag{757515121f3d478}

这题用angr也是可以跑的,脚本如下

import angr
import sys
def Go():
    path_to_binary = "C:\\Users\\Daki\Desktop\\signal.exe"
    project = angr.Project(path_to_binary, auto_load_libs=False)
    initial_state = project.factory.entry_state()
    simulation = project.factory.simgr(initial_state)

    print_good_address = 0x4017A5
    simulation.explore(find=print_good_address)

    if simulation.found:
        solution_state = simulation.found[0]
        solution = solution_state.posix.dumps(sys.stdin.fileno()) # 大概意思是dump出输入
        print(solution)
    else:
        raise Exception('Could not find the solution')

if __name__ == "__main__":
    Go()
#输出b'757515121f3d478\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

看了其他大佬的Writeup,看到也有正向爆破的方法,这个应该也是不错的方法,可以学习一下。

这题不是算很常规的虚拟机逆向的题目,但是能让我们明白虚拟机的结构,是干啥的,怎么对这类题目进行分析。

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇