祥云杯复盘 machine go语言+vm栈混淆

比赛的时候,vm解析器写的有些问题,导致这道题做了大半天没做出来
解题思路

一、获取vm的opcode

IDA载入题目,可以发现大量的未被解析的伪代码
0
使用IDAPython脚本 恢复代码块
import idc 
import idaapi
def upc(begin,end):
    for i in range(begin,end):
        idc.del_items(i)

    for i in range(begin,end):
        idc.create_insn(i)

    for i in range(begin,end):
        idaapi.add_func(i)
    print("Finish!!!")

begin = 0x00000000007A760
end = 0x0000000000C6ED8
upc(begin,end)
之后字符串搜索 code.bin ,定位到关键函数
0
发现这个函数的汇编没有被正常解析为伪代码,尝试patch掉一些代码,看能否显示出伪代码
经过尝试,是这一句jcc指令影响了IDA的跳转
0
patch掉之后,即可看到所有需要被解析的伪代码
 
0
很显然,这是一个vm混淆题目,里面有很多未知的函数,使用IDA自带的Lumina插件可以恢复大部分符号
 
题目流程
程序首先加载 code.bin 文件,取出文件的前0x100字节,使用sha256加密和RSA加密进行了一个校验(不用管这部分)
 
0
之后解压 code.bin文件
 
0
 
解压缩后的code.bin文件在后面就所为vm的opcode执行代码,我们直接dump出来即可
 

二、第一种vm解析器编写思路 

一般vm解析器会把代码解析成可读的伪代码或汇编
先说一下我使用的方法,我是把代码解析成了可执行的c语言伪代码
针对于该vm混淆,我记录下来所有关于寄存器以及栈的代码信息,之后用gcc 开O3优化进行处理,观察加密算法
经过尝试,可以发现程序每次检查8字节字符(最后一次是10字节),我们可以每次求出8字节,如此重复四次,即可得到flag
vm提取脚本总如下:(这里只贴最后一次
finn = '''
if (arr_v163[0] !=0)
   printf("error\\n");
else
   printf("continue\\n");
'''

vm_hander = []
fp =  open(r"C:\Users\Administrator\Desktop\Data","r")
Rd = fp.readlines()
for i in range(len(Rd)):
   vm_hander.append(int(Rd[i]))
   # print(Rd[i],end = "")

fps = open(r"C:\Users\Administrator\Desktop\opreattttttttttt","w")
def p(pt):
   global fps
   fps.write(pt+";"+"\n")
   # print(pt)

arr_v163 = [0] * 10000
len_v163 = 0
v172 = [0] * 10000
Anss  =0
scanf_ans = 0 #记录scanf的序列
# flag = "1111111111111111111111111111111111111"
# flag = "734f1698_12345678911111111111111111111"
# flag = "734f1698a5775ec2_12345671111111111111"
# flag = "734f1698a5775ec26cd2e11e_23456781\n"
flag = "734f1698a5775ec26cd2e11ed4791e77\r\n"
S1 = 0
S2 = 0
pc = 0   
p("unsigned long long int arr_v163[100] = {0};\nunsigned  long long int v172[10] = {0};")
while pc < len(vm_hander):

   opcode = vm_hander[pc]   
   if opcode == 0:
      v24 = len_v163;
      v13 = len_v163 + 1;
      len_v163 = v24 + 1;
      
      arr_v163[v24] = vm_hander[pc+1]
      # print(arr_v163[v24])
      # assert 0 
      p(f"arr_v163[{v24}] = {arr_v163[v24]}")
      pc += 2


   elif opcode == 0x1:           #  v26.append(vm_hander[pc+1]])

      v30 = len_v163;
      v13 = len_v163 + 1;

      v28 = vm_hander[pc+1]
      v29 = v172[v28];
      len_v163 = v30 + 1;
      arr_v163[v30] = v29;
      p(f"arr_v163[{v30}] = v172[{v28}]")

      pc += 2

   
   elif opcode == 0x2:
      v34 = vm_hander[pc+1] 
      ans_len = len_v163 - 1
      v35 = arr_v163[len_v163 - 1]
      len_v163 -= 1
      v172[v34] = v35
      p(f"v172[{v34}] = arr_v163[{ans_len}]")
      pc += 2
   
   elif opcode == 0x3:   # 0x18   #len = 2
      v36 = len_v163
      v37 = len_v163 - 1
      len_v163 -= 1
      ans_len = len_v163
      v38 = arr_v163[len_v163]
      v39 = v36 - 2
      v40 = arr_v163[v36 - 2] + v38
      len_v163 = v39 + 1
      arr_v163[v39] = v40
      p(f"arr_v163[{v39}] = arr_v163[{v36 - 2}] + arr_v163[{ans_len}]")

      pc += 1

   elif opcode == 0x4:
      v44 = len_v163;
      v45 = len_v163 - 1;
      ans_len = len_v163 - 1
      v46 = arr_v163[len_v163-1];
      len_v163 -= 1
      v47 = v44 - 2;

      v48 = v46 - arr_v163[v44 - 2];
      len_v163 = v44 - 2;

      len_v163 = v47 + 1;
      arr_v163[v47] = v48;

      p(f"arr_v163[{v47}] = arr_v163[{ans_len}] - arr_v163[{v44 - 2}]")

      pc += 1  # ? 
    
   elif opcode == 0x5:
      v52 = len_v163;
      v53 = len_v163 - 1;
      ans_len = len_v163 - 1
      v54 = arr_v163[len_v163 - 1];
      len_v163 -= 1
      v55 = v52 - 2;
      v56 = arr_v163[v52 - 2];
      len_v163 = v52 - 2;
      v57 = v56 * v54;
      
      len_v163 = v55 + 1;
      arr_v163[v55] = v57;

      p(f"arr_v163[{v55}] = arr_v163[{v52 - 2}] * arr_v163[{ans_len}]")
      pc += 1
     
   elif opcode == 0x6:           # 写
      v61 = len_v163;
      v62 = len_v163 - 1
      ans_len = len_v163 - 1
      v63 = arr_v163[len_v163-1];
      len_v163 -= 1
      v64 = v61 - 2;
      v65 = arr_v163[v61 - 2];
      len_v163 = v61 - 2;

      v66 = v62;
      v67 = v63 // v65;

      len_v163 = v64 + 1;
      arr_v163[v64] = v67;

      p(f"arr_v163[{v64}] = arr_v163[{ans_len}] // arr_v163[{v61 - 2}]")
      assert 0
      pc += 1
      
      

   elif opcode == 0x7:  # len = 3
      v71 = len_v163;
      v72 = len_v163 - 1;
      ans_len = len_v163-1
      v73 = arr_v163[len_v163-1];
      len_v163 -= 1
      v74 = v71 - 2;
      v75 = arr_v163[v71 - 2] ^ v73;
      len_v163 = v71 - 2;
      
      len_v163 = v74 + 1;
      arr_v163[v74] = v75;
      p(f"arr_v163[{v74}] = arr_v163[{v71 - 2}] ^ arr_v163[{ans_len}]")

      pc += 1
     
   elif opcode == 0x8:  # len = 3
      v79 = len_v163;
      v80 = len_v163 - 1;
      len_v163 -= 1
      ans_len = len_v163
      v81 = arr_v163[len_v163];
      v82 = v79 - 2;
      v83 = arr_v163[v79 - 2];
      len_v163 = v82;
      v84 = -(v83 < 0x40) & (v81 << v83);
      len_v163 = v82 + 1;
      arr_v163[v82] = v84;

      p(f"arr_v163[{v82}] = -(arr_v163[{v79 - 2}] < 0x40) & (arr_v163[{ans_len}] << arr_v163[{v79 - 2}])")
      pc += 1
              

   elif opcode == 0x9:
      v88 = len_v163;
      v89 = len_v163 - 1;
      ans_len = len_v163 -  1
      v90 = arr_v163[len_v163-1];
      len_v163 -= 1
      v91 = v88 - 2;
      v92 = arr_v163[v88 - 2];
      len_v163 = v91;
      v93 = -(v92 < 0x40) & (v90 >> v92);
      len_v163 = v91 + 1;
      arr_v163[v91] = v93;

      p(f"arr_v163[{v91}] = -(arr_v163[{v88 - 2}] < 0x40) & (arr_v163[{ans_len}] >> arr_v163[{v88 - 2}])")
      pc += 1
   


   elif opcode == 0xa:        # 2 
      v97 = len_v163
      v98 = len_v163 - 1
      ans_len = len_v163-1
      v99 = arr_v163[len_v163-1]
      len_v163 -= 1
      len_v163 = v98 + 1
      arr_v163[v98] = ~v99
      
      p(f"arr_v163[{v98}] = ~arr_v163[{ans_len}]")
      pc += 1


   elif opcode == 0xb:
      v104 = len_v163
      v105 = len_v163 - 1
      ans_len = len_v163-1
      v106 = arr_v163[len_v163-1]
      len_v163 -= 1
      v107 = v104 - 2

      v108 = arr_v163[v104 - 2] & v106;
      len_v163 = v104 - 2;
      len_v163 = v107 + 1
      arr_v163[v107] = v108
      p(f"arr_v163[{v107}] = arr_v163[{v104 - 2}] & arr_v163[{ans_len}]")
      pc += 1

   elif opcode == 0xc:           #  v26.append(scanf)
      
         if flag[scanf_ans] == "\n":     # 事实 + 检验发现,最后一个 '\n' 字符是在arr_v163[2]的位置被读取的
            arr_v163[2] = ord(flag[scanf_ans])
            scanf_ans += 1
            p(f"arr_v163[2] = 0")
            p(f"scanf(\"%c\",&arr_v163[2])")
         else:
            arr_v163[0] = ord(flag[scanf_ans])
            scanf_ans += 1
            p(f"arr_v163[0] = 0")
            p(f"scanf(\"%c\",&arr_v163[0])")
         # p(f"if (arr_v163[0] < 0x20 || arr_v163[0] >= 0x7f )\n\tFun_Exit(0)")
         S1 += 1
         len_v163 += 1
         pc += 1
      


   elif opcode == 0xd:
      try:
         p(f"//p --> {chr(vm_hander[pc+1])}")
      except:
         print(hex(pc))
      pc += 2

   elif opcode == 0xe:
      len_v163 -= 1
      # p(f"//Pan arr_v163[{len_v163}] == 0")
      Pan = f'''if (arr_v163[{len_v163}])\n\tFun_Exit(0)'''
      p(Pan)


      # if arr_v163[len_v163] & 0xffffffffffffffff:
      #    p(f"//errorrrrrr")
      #    p("//-------------------------------------------------------------------------------------------------#")
      #    fps.close()
      #    exit()
         
      pc +=1
   elif opcode == 0xcc:
      print("what???")
      assert 0
   else:
      assert 0


fps.close()
提取出的代码如下:
 
0
 
19000行代码,人工很难分辨出加密算法
修改成c语言代码后直接开O3优化
0
 
0
可以发现程序被优化到了250行代码,发现加密逻辑比较简单,只是很少简单的逻辑操作,对此,直接angr一把梭
import angr
import sys, time


def found(simgr):
    if simgr.found:
        solution_state = simgr.found[0]
        correct_input = solution_state.posix.dumps(sys.stdin.fileno())
        print(correct_input)
    else:
        raise Exception('not solution')


def main(argv):
    path_to_binary = argv[1]
    p = angr.Project(path_to_binary)

    initial_state = p.factory.entry_state(
        add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                     angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
    )

    simgr = p.factory.simulation_manager(initial_state)

    # good_address =  0x000000000401360       #加上 0x400000 基质
    # again_address = (0x000000000401378)  
    # good_address = 0x00000000040162F
    # again_address = 0x000000000401649
    # good_address = 0x000000000401932
    # again_address = 0x00000000040194C
    good_address = 0x000000000401A99
    again_address = 0x000000000401AB0
    simgr.explore(find=good_address, avoid=again_address)
    found(simgr)


if __name__ == '__main__':
    start = time.time()
    main(sys.argv)
    end = time.time()
    total = end - start
    print(total)
 
0
即可直接得到flag
 
0
 
734f1698a5775ec26cd2e11ed4791e77
这个方法不用考虑,栈啊,寄存器那一堆乱七八糟的关系,在python上就是一个列表的事,十分方便
但也有一些问题,有意这个方法依赖于gcc O3优化以及angr约束求解,我们的vm解析器脚本不允许出现一点问题,不然就GG。angr也解不了一些复杂逻辑的加密算法

三、第二种vm解析器编写思路

用栈和寄存器的形式提取vm混淆代码,这个方法是普遍使用率高同时也是效果最好的方式,我见更多大佬选择这种方式,我们需要先标志好寄存器,栈之类的东西,再提取它们来代表vm的执行
参考山海关战队wp:

 

 但是他们并没有从提取出的代码中找到加密算法的规律(因为提取出的数据太多了...),而是黑盒爆破猜到了规律..

这种方法直击vm混淆的本质,但需要人工去分析加密算法,比较淦

不管怎么样,这两种解析方法都是不错的方法

posted @ 2022-11-12 17:37  TLSN  阅读(111)  评论(0)    收藏  举报