实战三
d3sky
这一题需要了解一些TLS相关知识
TLS回调函数
TLS(Thread Local Storage,线程局部存储)是各线程的独立的存储空间,TLS回调函数是指,每当创建或终止进程的线程时会自动调用执行的函数,且调用执行优先于EP代码,该特征使它可以作为一种反调试技术使用。
为什么要讲这东西呢,因为一开始分析main函数时感觉静态分析比较吃力就想着动态调试分析一下,然后就停在了这里,这是触发了一个访问异常,sub_4C1050是由线程回调函数(TlsCallback_0)调用的
圈起来的那里是判断程序是否处于调试状态,如果是调试状态则执行下面的mov ecx,[eax]
指令。如果不是处于调试状态则走另一个分支,触发一个除零异常,跳转到处理函数之后程序正常运行。对抗方法很简单,直接在IsDebuggerPresent结束之后修改eax的值,将1修改为0。
踩了个坑大家注意一下,如果修改的是跳转逻辑也就是将jz改为jnz,在调试状态程序也是不能正常运行的,因为IsDebuggerPresent的返回值被存储在了[ebp+var1C]处,下面的idiv 除指令除的就是这个位置,试想我们修改了跳转逻辑成功避开了那段会触发访问异常的指令,但是往下运行却没能触发除零异常,自然不能跳转到程序原本的执行流程上去。
分析main函数
这个VM题和前两个又很大的区别,他没有类似mov、xor、push、pop这样的操作,只有一条与非指令opcode[v6] = ~(opcode[v7] & opcode[v8]),而通过与非指令可以实现所有的逻辑运算。
乍一看很唬人,跟着动调几轮就能理清逻辑了,opcode[2]、opcode[7]、opcode[19]是三个标志位。
通过动态调试很容易判断出当opcode[2]为1时,会在控制台输出,当opcode[7]为1时会读取输入,在读取完所有的数据之前,opcode[19]不会为1,结合puts("wrong")可以推测出他是flag检测位。
后面这一组着重分析一下
opcode[0]可以看作是虚拟机的pc指针,而每次循环都会解密三个单位的指令(这里第一次RC4理解为解密更符合逻辑一点,也就是将原始的opcode认为是加密处理过的)。取出来之后,pc指针加三,可以理解为每条指令长度为3。之后会将opcode进行复原。然后执行核心的与非指令。
在调试的过程中通过十六进制窗口那里能得到输入存储的位置
翻译
接下来就是翻译了,和前面两个题目一样,我们需要做的是打印下来,而打印又有两种思路,一是将代码直接copy过来,编译执行一下,打印出所有的与非逻辑。二是借助idapython,由于之前都是用的第一种方法,这道题目就使用idapython来打印指令了。
想要实现的效果就是
1 |
|
根据汇编代码编写idapython脚本
1 2 3 4 5 6 7 8 9 10 11 12 |
|
我们只关心对输入的判断,所以前面的逻辑不用理会,使用idapython在输入结束时提醒
1 2 3 4 5 6 7 8 9 |
|
输入:123456789012345678901234567890123456~
输出如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
观察输出不难发现(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 |
|
最下面可以推测应该是比较数据,opcode[2809]距opcode[2772]正好相差37个单位,是输入的长度。
把与非指令还原成高级指令
1 2 3 4 5 6 7 8 9 10 11 |
|
第一组idapython打印的指令就等价为
1 2 3 4 |
|
结合这里
检测时要保证opcode为0,两个数相同异或结果为0,所以最后一步opcode[19]=cpdata[0]^opcode[18] 就是检测cpdata[0]和opcode[18]是否相等。
那么合理的推测就是:
1 2 3 4 5 6 |
|
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 |
|
明显是错误,有可能是推测错了,经过验证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[19]得到的值是0,又继续往下执行了一组还是正确的,直接拉到最后可以看到执行的逻辑是
1 |
|
循环执行了,更改一下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 |
|
终于结束了!!!
这道题似乎完全不同于前面的那种VM类型的题目,他没有结构体,没有vm_init、没有vm_start等等,但是思想上是一致的,都是用vm提供的指令来还原程序逻辑(只是在这道题目中只有一种指令),都很好的抵抗了静态分析,而且仔细回味一下,opcode[11]、opcode[12]、opcode[17]、opcode[18],其实就是虚拟机的寄存器,也能找到对应的抽象的init、start等。
初识VMP
通过这三个题目我们对虚拟机保护技术有了一定的了解,这项技术运用到实际的生产工作中就有了VMP(VMProtect)。
什么是VMProtect
VMProtect是一个软件保护程序,应用于软件保护和加固领域,VMProtect通过使应用程序的代码和逻辑变得复杂来对抗逆向,主要的保护机制包括:虚拟化、变异、组合保护。
我们主要了解一下虚拟化,VMProtect首先会将受保护的代码转换为等价的虚拟代码片段,然后交由虚拟机执行,该虚拟机是VMProtect嵌入到受保护程序中的,因此受保护的程序不需要第三方库活模块即可运行。更为夸张的是,VMProtect允许使用多个不同的虚拟机来保护同一个应用程序的不同代码片段,这大大增加了破解难度。
优缺点
优点:
- 保护程度高,极难破解(没有绝对安全的程序,但如果破解程序所需的成本大于程序本身,程序自然就安全了)
- 保护后,虚拟机和新命令集将内置到受保护的应用程序中,并且不需要任何其他库和模块即可工作。
- 支持多种语言,以及主流操作系统:Windows、macOS、Linux
缺点:
- 执行效率低(参考上面的题目,一条printf语句就对应十几条vm指令)
- 过于消耗系统资源
之后有时间了应该会深入了解一下VMP,尝试对抗低版本的vmp保护,跟着大佬们分析分析源码。(本来这篇文章的后半部分是尝试逆向一个我自己写的受VMProtect保护的程序的,可是一个54kb的程序直接膨胀到了13mb,一时间有些恍惚)