【CTF对抗-VM逆向,一篇就够了(下)】此文章归类为:CTF对抗。
title: VM逆向,一篇就够了(下)
接着上一篇文章,我们继续了解一些不一样vm。
d3sky
这一题需要了解一些TLS相关知识
TLS(Thread Local Storage,线程局部存储)是各线程的独立的存储空间,TLS回调函数是指,每当创建或终止进程的线程时会自动调用执行的函数,且调用执行优先于EP代码,该特征使它可以作为一种反调试技术使用。
为什么要讲这东西呢,因为一开始分析main函数时感觉静态分析比较吃力就想着动态调试分析一下,然后就停在了这里,这是触发了一个访问异常,sub_4C1050是由线程回调函数(TlsCallback_0)调用的
指令在尝试读取地址0x00处的数据,导致程序不能正常长运行。
汇编页面长这个样子,
圈起来的那里是判断程序是否处于调试状态,如果是调试状态则执行下面的mov ecx,[eax]
指令。如果不是处于调试状态则走另一个分支,触发一个除零异常,跳转到处理函数之后程序正常运行。对抗方法很简单,直接在IsDebuggerPresent结束之后修改eax的值,将1修改为0。
踩了个坑大家注意一下,如果修改的是跳转逻辑也就是将jz改为jnz,在调试状态程序也是不能正常运行的,因为IsDebuggerPresent的返回值被存储在了[ebp+var1C]处,下面的idiv 除指令除的就是这个位置,试想我们修改了跳转逻辑成功避开了那段会触发访问异常的指令,但是往下运行却没能触发除零异常,自然不能跳转到程序原本的执行流程上去。
这个VM题和前两个又很大的区别,他没有类似mov、xor、push、pop这样的操作,只有一条与非指令opcode[v6] = ~(opcode[v7] & opcode[v8]),而通过与非指令可以实现所有的逻辑运算。出题师傅的博客有详细的介绍d3sky出题小记 - 云之君's Blog (yunzh1jun.com)
乍一看很唬人,跟着动调几轮就能理清逻辑了,opcode[2]、opcode[7]、opcode[19]是三个标志位。
通过动态调试很容易判断出当opcode[2]为1时,会在控制台输出,当opcode[7]为1时会读取输入,在读取完所有的数据之前,opcode[19]不会为1,结合puts("wrong")可以推测出他是flag检测位。
后面这一组着重分析一下
注释解释的应该很清楚了,RC4_back是RC_4的加/解密部分
opcode[0]可以看作是虚拟机的pc指针,而每次循环都会解密三个单位的指令(这里第一次RC4理解为解密更符合逻辑一点,也就是将原始的opcode认为是加密处理过的)。取出来之后,pc指针加三,可以理解为每条指令长度为3。之后会将opcode进行复原。然后执行核心的与非指令。
在调试的过程中通过十六进制窗口那里能得到输入存储的位置
查看这个地址,可以看到他在tls回调函数里就被初始化了
接下来就是翻译了,和前面两个题目一样,我们需要做的是打印下来,而打印又有两种思路,一是将代码直接copy过来,编译执行一下,打印出所有的与非逻辑。二是借助idapython,由于之前都是用的第一种方法,这道题目就使用idapython来打印指令了。
想要实现的效果就是
1 | opcode[ 11 ] = ~(opcode[ 20 ]&opcode[ 20 ]) opcode[ 11 ] = ~( 1 & 1 ) |
根据汇编代码编写idapython脚本
1 2 3 4 5 6 7 8 9 10 11 12 | import idc var_8 = - 8 var_C = - 0x0C v6 = idc.get_reg_value( 'eax' ) ebp_ = idc.get_reg_value( 'ebp' ) v7 = idc.get_wide_word(ebp_ + var_8) v8 = idc.get_wide_word(ebp_ + var_C) opcode_base = 0xBE4018 value0 = idc.get_wide_word(opcode_base + v7 * 2 ) #这里需要注意,v7要乘2,因为opcode是word数组 value1 = idc.get_wide_word(opcode_base + v8 * 2 ) print ( 'opcode[%d]=~(opcode[%d]&opcode[%d])' % (v6,v7,v8), ' opcode[%d]=~( %d & %d )' % (v6,value0,value1)) |
我们只关心对输入的判断,所以前面的逻辑不用理会,使用idapython在输入结束时提醒
1 2 3 4 5 6 7 8 9 | import idc var_24 = - 0x24 ebp_ = idc.get_reg_value( 'ebp' ) num = idc.get_wide_word(ebp_ + var_24) if (num = = 0x24 ): print ( '-------------------------------------END-------------------------------------' ) print ( '-------------------------------------END-------------------------------------' ) print ( '-------------------------------------END-------------------------------------' ) |
输入:123456789012345678901234567890123456~
输出如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | -------------------------------------END------------------------------------- opcode[7]=~(opcode[8]&opcode[8]) opcode[7]=~( 126 & 126 ) opcode[2808]=~(opcode[7]&opcode[7]) opcode[2808]=~( 65409 & 65409 ) opcode[11]=~(opcode[2772]&opcode[2772]) opcode[11]=~( 49 & 49 ) //取反 opcode[11]=~(opcode[2773]&opcode[11]) opcode[11]=~( 50 & 65486 ) opcode[12]=~(opcode[2773]&opcode[2773]) opcode[12]=~( 50 & 50 ) opcode[12]=~(opcode[2772]&opcode[12]) opcode[12]=~( 49 & 65485 ) opcode[17]=~(opcode[12]&opcode[11]) opcode[17]=~( 65534 & 65533 ) opcode[11]=~(opcode[2774]&opcode[2774]) opcode[11]=~( 51 & 51 ) opcode[11]=~(opcode[2775]&opcode[11]) opcode[11]=~( 52 & 65484 ) opcode[12]=~(opcode[2775]&opcode[2775]) opcode[12]=~( 52 & 52 ) opcode[12]=~(opcode[2774]&opcode[12]) opcode[12]=~( 51 & 65483 ) opcode[18]=~(opcode[12]&opcode[11]) opcode[18]=~( 65532 & 65531 ) opcode[11]=~(opcode[17]&opcode[17]) opcode[11]=~( 3 & 3 ) opcode[11]=~(opcode[18]&opcode[11]) opcode[11]=~( 7 & 65532 ) opcode[12]=~(opcode[18]&opcode[18]) opcode[12]=~( 7 & 7 ) opcode[12]=~(opcode[17]&opcode[12]) opcode[12]=~( 3 & 65528 ) opcode[18]=~(opcode[12]&opcode[11]) opcode[18]=~( 65535 & 65531 ) opcode[11]=~(opcode[2809]&opcode[2809]) opcode[11]=~( 36 & 36 ) opcode[11]=~(opcode[18]&opcode[11]) opcode[11]=~( 4 & 65499 ) opcode[12]=~(opcode[18]&opcode[18]) opcode[12]=~( 4 & 4 ) opcode[12]=~(opcode[2809]&opcode[12]) opcode[12]=~( 36 & 65531 ) opcode[19]=~(opcode[12]&opcode[11]) opcode[19]=~( 65503 & 65535 ) |
观察输出不难发现(49、50、51、52,正是前四个输入1234的ascii),应该是把我们的输入按照四字节一组进行加密的。也能看出,我们的输入存储在从opcode[2772]这个位置。
与非运算十六进制观察更方便一些
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | -------------------------------------END------------------------------------- opcode[7]=~(opcode[8]&opcode[8]) opcode[7]=~( 7e & 7e ) opcode[2808]=~(opcode[7]&opcode[7]) opcode[2808]=~( ff81 & ff81 ) //保存最后一位输入至opcode[2808] opcode[11]=~(opcode[2772]&opcode[2772]) opcode[11]=~( 31 & 31 ) //opcode[11]=~(input[0]&input[0]) opcode[11]=~(opcode[2773]&opcode[11]) opcode[11]=~( 32 & ffce ) //opcode[11]=~(input[1]&opcode[11]) opcode[12]=~(opcode[2773]&opcode[2773]) opcode[12]=~( 32 & 32 ) //opcode[12]=~(input[1]&input[1]) opcode[12]=~(opcode[2772]&opcode[12]) opcode[12]=~( 31 & ffcd ) //opcode[12]=~(input[0]&opcode[12]) opcode[17]=~(opcode[12]&opcode[11]) opcode[17]=~( fffe & fffd ) //opcode[17]=~(opcode[12]&opcode[11]) opcode[11]=~(opcode[2774]&opcode[2774]) opcode[11]=~( 33 & 33 ) //opcode[11]=~(input[2]&input[2]) opcode[11]=~(opcode[2775]&opcode[11]) opcode[11]=~( 34 & ffcc ) //opcode[11]=~(input[3]&opcode[11]) opcode[12]=~(opcode[2775]&opcode[2775]) opcode[12]=~( 34 & 34 ) //opcode[12]=~(input[3]&input[3]) opcode[12]=~(opcode[2774]&opcode[12]) opcode[12]=~( 33 & ffcb ) //opcode[12]=~(input[2]&opcode[12]) opcode[18]=~(opcode[12]&opcode[11]) opcode[18]=~( fffc & fffb ) //opcode[18]=~(opcode[12]&opcode[11]) opcode[11]=~(opcode[17]&opcode[17]) opcode[11]=~( 3 & 3 ) //opcode[11]=~(opcode[17]&opcode[17]) opcode[11]=~(opcode[18]&opcode[11]) opcode[11]=~( 7 & fffc ) //opcode[11]=~(opcode[18]&opcode[11]) opcode[12]=~(opcode[18]&opcode[18]) opcode[12]=~( 7 & 7 ) //opcode[12]=~(opcode[18]&opcode[18]) opcode[12]=~(opcode[17]&opcode[12]) opcode[12]=~( 3 & fff8 ) //opcode[12]=~(opcode[17]&opcode[12]) opcode[18]=~(opcode[12]&opcode[11]) opcode[18]=~( ffff & fffb ) //opcode[18]=~(opcode[12]&opcode[11]) //注意下面是进行比较 opcode[11]=~(opcode[2809]&opcode[2809]) opcode[11]=~( 24 & 24 ) //opcode[11]=~(cpdata[0]&cpdata[0]) opcode[11]=~(opcode[18]&opcode[11]) opcode[11]=~( 4 & ffdb ) //opcode[11]=~(opcode[18]&opcode[11]) opcode[12]=~(opcode[18]&opcode[18]) opcode[12]=~( 4 & 4 ) //opcode[12]=~(opcode[18]&opcode[18]) opcode[12]=~(opcode[2809]&opcode[12]) opcode[12]=~( 24 & fffb ) //opcode[12]=~(cpdata[0]&opcode[12]) opcode[19]=~(opcode[12]&opcode[11]) opcode[19]=~( ffdf & ffff ) //opcode[19]=~(opcode[12]&opcode[11]) |
最下面可以推测应该是比较数据,opcode[2809]距opcode[2772]正好相差37个单位,是输入的长度。
把与非指令还原成高级指令
1 2 3 4 5 6 7 8 9 10 11 | opcode[ 11 ] = ~( input [ 0 ]& input [ 0 ]) / / 将 input [ 0 ]取反 0000 0000 0011 0001 - - > 1111 1111 1100 1110 opcode[ 11 ] = ~( input [ 1 ]&opcode[ 11 ]) / / 将 input [ 1 ]与取反之后的 input [ 0 ]进行与操作 与操作保留都是 1 的位,而~ input [ 0 ]的 1 是由 0 变来的 1100 1110 & 0011 0010 - - > 0000 0010 这一步就是将 input [ 0 ]为 0 且 input [ 1 ]为 1 的位 置 1 0000 0010 然后取反(有点异或的意思) 1111 1101 opcode[ 12 ] = ~( input [ 1 ]& input [ 1 ]) / / input [ 1 ]取反 0011 0010 - - 1100 1101 opcode[ 12 ] = ~( input [ 0 ]&opcode[ 12 ]) / / 将 input [ 0 ]为 1 且 input [ 1 ]为 0 的位 置 1 0000 0001 然后取反 1111 1110 opcode[ 17 ] = ~(opcode[ 12 ]&opcode[ 11 ]) / / 保留都是 1 的位 1111 1100 然后取反 0000 0011 就是 input [ 0 ]^ input [ 1 ]的结果 最后一条指令就是将 input [ 0 ]为 0 且 input [ 1 ]为 1 的位置 1 同时将 input [ 0 ]为 1 且 input [ 1 ]为 0 的位置 1 合并一下就是相异为 1 相同为 0 上面六条等价为高级指令 opcode[ 17 ] = input [ 0 ]^ input [ 1 ] |
第一组idapython打印的指令就等价为
1 2 3 4 | opcode[ 17 ] = input [ 0 ]^ input [ 1 ] opcode[ 18 ] = input [ 2 ]^ input [ 3 ] opcode[ 18 ] = opcode[ 17 ]^opcode[ 18 ] opcode[ 19 ] = cpdata[ 0 ]^opcode[ 18 ] |
结合这里
检测时要保证opcode为0,两个数相同异或结果为0,所以最后一步opcode[19]=cpdata[0]^opcode[18] 就是检测cpdata[0]和opcode[18]是否相等。
那么合理的推测就是:
1 2 3 4 5 6 | cpdata[ 0 ] = = input [ 0 ]^ input [ 1 ]^ input [ 2 ]^ input [ 3 ] cpdata[ 1 ] = = input [ 1 ]^ input [ 2 ]^ input [ 3 ]^ input [ 4 ] ………… cpdata[ 33 ] = = input [ 33 ]^ input [ 34 ]^ input [ 35 ]^ input [ 36 ] ?????这里数据不够了 不知道是循环到 input [ 0 ]那里还是继续往后取数据,先写一个解出前面的试试 cpdada[ 36 ] = input [] |
z3脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | from z3 import * # 创建符号变量 input = [BitVec( 'input_%d' % i, 16 ) for i in range ( 37 )] # 已知的结果 cpdata = [ 0x0024 , 0x000B , 0x006D , 0x000F , 0x0003 , 0x0032 , 0x0042 , 0x001D , 0x002B , 0x0043 , 0x0078 , 0x0043 , 0x0073 , 0x0030 , 0x002B , 0x004E , 0x0063 , 0x0048 , 0x0077 , 0x002E , 0x0032 , 0x0039 , 0x001A , 0x0012 , 0x0071 , 0x007A , 0x0042 , 0x0017 , 0x0045 , 0x0072 , 0x0056 , 0x000C , 0x005C , 0x004A , 0x0062 , 0x0053 , 0x0033 ] # 添加异或约束 constraints = [] for i in range ( 34 ): constraints.append(cpdata[i] = = ( input [i] ^ input [i + 1 ] ^ input [i + 2 ] ^ input [i + 3 ])) # 创建解决器 solver = Solver() # 添加约束 solver.add(constraints) # 求解 if solver.check() = = sat: model = solver.model() solution = [model[ input [i]].as_long() for i in range ( 37 )] print ( "Solution found:" ) print (solution) else : print ( "No solution found." ) #[80, 65, 50, 7, 127, 39, 80, 11, 78, 87, 15, 61, 38, 108, 52, 13, 101, 119, 81, 32, 78, 72, 8, 60, 69, 107, 0, 95, 78, 83, 85, 13, 121, 119, 15, 93, 111] |
明显是错误,有可能是推测错了,经过验证z3打印的结果是满足cpdata[0]==input[0]^input[1]^input[2]^input[3] 的,那我们将其patch进去,就能得到第二组比较的方法,将得到的全部数据patch进去就能打印到与cpdata[34]的比较
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | opcode[ 7 ] = ~(opcode[ 8 ]&opcode[ 8 ]) opcode[ 7 ] = ~( 7e & 7e ) opcode[ 2808 ] = ~(opcode[ 7 ]&opcode[ 7 ]) opcode[ 2808 ] = ~( ff81 & ff81 ) opcode[ 11 ] = ~(opcode[ 2772 ]&opcode[ 2772 ]) opcode[ 11 ] = ~( 50 & 50 ) / / input [ 0 ] opcode[ 11 ] = ~(opcode[ 2773 ]&opcode[ 11 ]) opcode[ 11 ] = ~( 41 & ffaf ) / / input [ 1 ] opcode[ 12 ] = ~(opcode[ 2773 ]&opcode[ 2773 ]) opcode[ 12 ] = ~( 41 & 41 ) opcode[ 12 ] = ~(opcode[ 2772 ]&opcode[ 12 ]) opcode[ 12 ] = ~( 50 & ffbe ) opcode[ 17 ] = ~(opcode[ 12 ]&opcode[ 11 ]) opcode[ 17 ] = ~( ffef & fffe ) opcode[ 11 ] = ~(opcode[ 2774 ]&opcode[ 2774 ]) opcode[ 11 ] = ~( 32 & 32 ) / / input [ 2 ] opcode[ 11 ] = ~(opcode[ 2775 ]&opcode[ 11 ]) opcode[ 11 ] = ~( 7 & ffcd ) / / input [ 3 ] opcode[ 12 ] = ~(opcode[ 2775 ]&opcode[ 2775 ]) opcode[ 12 ] = ~( 7 & 7 ) opcode[ 12 ] = ~(opcode[ 2774 ]&opcode[ 12 ]) opcode[ 12 ] = ~( 32 & fff8 ) opcode[ 18 ] = ~(opcode[ 12 ]&opcode[ 11 ]) opcode[ 18 ] = ~( ffcf & fffa ) opcode[ 11 ] = ~(opcode[ 17 ]&opcode[ 17 ]) opcode[ 11 ] = ~( 11 & 11 ) opcode[ 11 ] = ~(opcode[ 18 ]&opcode[ 11 ]) opcode[ 11 ] = ~( 35 & ffee ) opcode[ 12 ] = ~(opcode[ 18 ]&opcode[ 18 ]) opcode[ 12 ] = ~( 35 & 35 ) opcode[ 12 ] = ~(opcode[ 17 ]&opcode[ 12 ]) opcode[ 12 ] = ~( 11 & ffca ) opcode[ 18 ] = ~(opcode[ 12 ]&opcode[ 11 ]) opcode[ 18 ] = ~( ffff & ffdb ) opcode[ 11 ] = ~(opcode[ 2809 ]&opcode[ 2809 ]) opcode[ 11 ] = ~( 24 & 24 ) opcode[ 11 ] = ~(opcode[ 18 ]&opcode[ 11 ]) opcode[ 11 ] = ~( 24 & ffdb ) opcode[ 12 ] = ~(opcode[ 18 ]&opcode[ 18 ]) opcode[ 12 ] = ~( 24 & 24 ) opcode[ 12 ] = ~(opcode[ 2809 ]&opcode[ 12 ]) opcode[ 12 ] = ~( 24 & ffdb ) opcode[ 19 ] = ~(opcode[ 12 ]&opcode[ 11 ]) opcode[ 19 ] = ~( ffff & ffff ) opcode[ 11 ] = ~(opcode[ 2773 ]&opcode[ 2773 ]) opcode[ 11 ] = ~( 41 & 41 ) / / input [ 1 ] opcode[ 11 ] = ~(opcode[ 2774 ]&opcode[ 11 ]) opcode[ 11 ] = ~( 32 & ffbe ) / / input [ 2 ] opcode[ 12 ] = ~(opcode[ 2774 ]&opcode[ 2774 ]) opcode[ 12 ] = ~( 32 & 32 ) opcode[ 12 ] = ~(opcode[ 2773 ]&opcode[ 12 ]) opcode[ 12 ] = ~( 41 & ffcd ) opcode[ 17 ] = ~(opcode[ 12 ]&opcode[ 11 ]) opcode[ 17 ] = ~( ffbe & ffcd ) opcode[ 11 ] = ~(opcode[ 2775 ]&opcode[ 2775 ]) opcode[ 11 ] = ~( 7 & 7 ) / / input [ 3 ] opcode[ 11 ] = ~(opcode[ 2776 ]&opcode[ 11 ]) opcode[ 11 ] = ~( 35 & fff8 ) / / input [ 4 ] opcode[ 12 ] = ~(opcode[ 2776 ]&opcode[ 2776 ]) opcode[ 12 ] = ~( 35 & 35 ) opcode[ 12 ] = ~(opcode[ 2775 ]&opcode[ 12 ]) opcode[ 12 ] = ~( 7 & ffca ) opcode[ 18 ] = ~(opcode[ 12 ]&opcode[ 11 ]) opcode[ 18 ] = ~( fffd & ffcf ) opcode[ 11 ] = ~(opcode[ 17 ]&opcode[ 17 ]) opcode[ 11 ] = ~( 73 & 73 ) opcode[ 11 ] = ~(opcode[ 18 ]&opcode[ 11 ]) opcode[ 11 ] = ~( 32 & ff8c ) opcode[ 12 ] = ~(opcode[ 18 ]&opcode[ 18 ]) opcode[ 12 ] = ~( 32 & 32 ) opcode[ 12 ] = ~(opcode[ 17 ]&opcode[ 12 ]) opcode[ 12 ] = ~( 73 & ffcd ) opcode[ 18 ] = ~(opcode[ 12 ]&opcode[ 11 ]) opcode[ 18 ] = ~( ffbe & ffff ) opcode[ 11 ] = ~(opcode[ 2810 ]&opcode[ 2810 ]) opcode[ 11 ] = ~( b & b ) opcode[ 11 ] = ~(opcode[ 18 ]&opcode[ 11 ]) opcode[ 11 ] = ~( 41 & fff4 ) opcode[ 12 ] = ~(opcode[ 18 ]&opcode[ 18 ]) opcode[ 12 ] = ~( 41 & 41 ) opcode[ 12 ] = ~(opcode[ 2810 ]&opcode[ 12 ]) opcode[ 12 ] = ~( b & ffbe ) opcode[ 19 ] = ~(opcode[ 12 ]&opcode[ 11 ]) opcode[ 19 ] = ~( fff5 & ffbf ) ………… opcode[ 11 ] = ~(opcode[ 2806 ]&opcode[ 2806 ]) opcode[ 11 ] = ~( 60 & 60 ) / / input [ 34 ] opcode[ 11 ] = ~(opcode[ 2807 ]&opcode[ 11 ]) opcode[ 11 ] = ~( 43 & ff9f ) / / input [ 35 ] opcode[ 12 ] = ~(opcode[ 2807 ]&opcode[ 2807 ]) opcode[ 12 ] = ~( 43 & 43 ) opcode[ 12 ] = ~(opcode[ 2806 ]&opcode[ 12 ]) opcode[ 12 ] = ~( 60 & ffbc ) opcode[ 17 ] = ~(opcode[ 12 ]&opcode[ 11 ]) opcode[ 17 ] = ~( ffdf & fffc ) opcode[ 11 ] = ~(opcode[ 2808 ]&opcode[ 2808 ]) opcode[ 11 ] = ~( 7e & 7e ) / / input [ 36 ] opcode[ 11 ] = ~(opcode[ 2772 ]&opcode[ 11 ]) opcode[ 11 ] = ~( 41 & ff81 ) / / input [ 0 ] opcode[ 12 ] = ~(opcode[ 2772 ]&opcode[ 2772 ]) opcode[ 12 ] = ~( 41 & 41 ) opcode[ 12 ] = ~(opcode[ 2808 ]&opcode[ 12 ]) opcode[ 12 ] = ~( 7e & ffbe ) opcode[ 18 ] = ~(opcode[ 12 ]&opcode[ 11 ]) opcode[ 18 ] = ~( ffc1 & fffe ) opcode[ 11 ] = ~(opcode[ 17 ]&opcode[ 17 ]) opcode[ 11 ] = ~( 23 & 23 ) opcode[ 11 ] = ~(opcode[ 18 ]&opcode[ 11 ]) opcode[ 11 ] = ~( 3f & ffdc ) opcode[ 12 ] = ~(opcode[ 18 ]&opcode[ 18 ]) opcode[ 12 ] = ~( 3f & 3f ) opcode[ 12 ] = ~(opcode[ 17 ]&opcode[ 12 ]) opcode[ 12 ] = ~( 23 & ffc0 ) opcode[ 18 ] = ~(opcode[ 12 ]&opcode[ 11 ]) opcode[ 18 ] = ~( ffff & ffe3 ) opcode[ 11 ] = ~(opcode[ 2843 ]&opcode[ 2843 ]) opcode[ 11 ] = ~( 62 & 62 ) opcode[ 11 ] = ~(opcode[ 18 ]&opcode[ 11 ]) opcode[ 11 ] = ~( 1c & ff9d ) opcode[ 12 ] = ~(opcode[ 18 ]&opcode[ 18 ]) opcode[ 12 ] = ~( 1c & 1c ) opcode[ 12 ] = ~(opcode[ 2843 ]&opcode[ 12 ]) opcode[ 12 ] = ~( 62 & ffe3 ) opcode[ 19 ] = ~(opcode[ 12 ]&opcode[ 11 ]) opcode[ 19 ] = ~( ff9d & ffe3 ) |
不出所料,第一组opcode[19]得到的值是0,又继续往下执行了一组还是正确的,直接拉到最后可以看到执行的逻辑是
1 | cpdata[ 34 ] = = input [ 34 ]^ input [ 35 ]^ input [ 36 ]^ input [ 0 ] |
循环执行了,更改一下z3脚本即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | from z3 import * # 创建符号变量 input = [BitVec( 'input_%d' % i, 8 ) for i in range ( 37 )] # 已知的结果 cpdata = [ 0x0024 , 0x000B , 0x006D , 0x000F , 0x0003 , 0x0032 , 0x0042 , 0x001D , 0x002B , 0x0043 , 0x0078 , 0x0043 , 0x0073 , 0x0030 , 0x002B , 0x004E , 0x0063 , 0x0048 , 0x0077 , 0x002E , 0x0032 , 0x0039 , 0x001A , 0x0012 , 0x0071 , 0x007A , 0x0042 , 0x0017 , 0x0045 , 0x0072 , 0x0056 , 0x000C , 0x005C , 0x004A , 0x0062 , 0x0053 , 0x0033 ] # 添加异或约束 constraints = [] for i in range ( 34 ): constraints.append(cpdata[i] = = ( input [i] ^ input [i + 1 ] ^ input [i + 2 ] ^ input [i + 3 ])) # 创建解决器 solver = Solver() # 添加约束 solver.add(constraints) solver.add(cpdata[ 34 ] = = ( input [ 34 ] ^ input [ 35 ] ^ input [ 36 ] ^ input [ 0 ])) solver.add(cpdata[ 35 ] = = ( input [ 35 ] ^ input [ 36 ] ^ input [ 0 ] ^ input [ 1 ])) solver.add(cpdata[ 36 ] = = ( input [ 36 ] ^ input [ 0 ] ^ input [ 1 ] ^ input [ 2 ])) solver.add( input [ 36 ] = = 126 ) #题目中对最后一位的限制 # 求解 if solver.check() = = sat: model = solver.model() solution = [model[ input [i]].as_long() for i in range ( 37 )] print ( "Solution found:" ) print (solution) else : print ( "No solution found." ) #[65, 95, 83, 105, 110, 57, 49, 101, 95, 73, 110, 83, 55, 114, 85, 99, 116, 105, 48, 78, 95, 86, 105, 82, 84, 117, 97, 49, 95, 77, 52, 99, 104, 105, 110, 51, 126] #A_Sin91e_InS7rUcti0N_ViRTua1_M4chin3~ |
终于结束了!!!
这道题似乎完全不同于前面的那种VM类型的题目,他没有结构体,没有vm_init、没有vm_start等等,但是思想上是一致的,都是用vm提供的指令来还原程序逻辑(只是在这道题目中只有一种指令),都很好的抵抗了静态分析,而且仔细回味一下,opcode[11]、opcode[12]、opcode[17]、opcode[18],其实就是虚拟机的寄存器,也能找到对应的抽象的init、start等。
参考链接:
出题师傅的博客 d3sky出题小记 - 云之君's Blog (yunzh1jun.com)
pz师傅的视频讲解【D^3CTF2023】D3sky & D3rc4_哔哩哔哩_bilibili
通过这三个题目我们对虚拟机保护技术有了一定的了解,这项技术运用到实际的生产工作中就有了VMP(VMProtect)。
VMProtect是一个软件保护程序,应用于软件保护和加固领域,VMProtect通过使应用程序的代码和逻辑变得复杂来对抗逆向,主要的保护机制包括:虚拟化、变异、组合保护。
我们主要了解一下虚拟化,VMProtect首先会将受保护的代码转换为等价的虚拟代码片段,然后交由虚拟机执行,该虚拟机是VMProtect嵌入到受保护程序中的,因此受保护的程序不需要第三方库活模块即可运行。更为夸张的是,VMProtect允许使用多个不同的虚拟机来保护同一个应用程序的不同代码片段,这大大增加了破解难度。
优点:
缺点:
之后有时间了应该会深入了解一下VMP,尝试对抗低版本的vmp保护,跟着大佬们分析分析源码。(本来这篇文章的后半部分是尝试逆向一个我自己写的受VMProtect保护的程序的,可是一个54kb的程序直接膨胀到了13mb,一时间有些恍惚)
参考链接:
VMProtect Software » VMProtect » Docs (vmpsoft.com)
【镖客熊猫江】从破解一个vmp壳的程序到vmp原理详解_哔哩哔哩_bilibili
更多【CTF对抗-VM逆向,一篇就够了(下)】相关视频教程:www.yxfzedu.com