吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 26581|回复: 65
收起左侧

[调试逆向] 【已完结】与字节码解释器比耐心(动态调试)

  [复制链接]
skywilling 发表于 2018-2-20 21:58
本帖最后由 skywilling 于 2018-2-25 11:04 编辑

0x00前言
如题所言,本次的文章可能比较长,但是内容比较详细,适合新手学习,但是同样考验耐心。这次的文章严格说来是我上一篇文章遗留下来的历史问题,上一篇文章请参考【又见AES】第十届全国大学生信息安全大赛之逆向--欢迎来到加基森同时这篇文章中的字节码解释器与【i春秋】第十届全国大学生信息安全大赛之逆向--apk题目》有异曲同工之妙。在文章开始之前,我首先要感谢一下@veritas501大神给的一些灵感

0x01静态分析
本次要分析的代码段位于0x80495C0,如图是IDA还原出的关键伪C代码
1.png
这次我们要分析的就是disasm函数(这是改过的函数名,位于0x80495C0,),在分析这个函数之前,我们需要先分析一下传入的参数v4,图中已高亮显示,很容易看出v4可能是一个长度为4的数组(下标为0的变量存放一个数组的首地址,这个数组是一个指令集,其他3个我们在动态调试中说明)
disasm函数里面是一个有大量分支的switch分支语句,这里截取部分

28.png
0x02动态分析
在这里我是用的是Ubuntu虚拟机(该文件是ELF程序)+IDA,这里输入以0123456789012345为例,直接断点到0x0804964A(call    disasm)
2.png
3.png
此时传入的参数位于栈顶
4.png
然后在Hex View中查看内存
5.png
选中的部分就是v4[0],紧跟的三个不为零的数据分别是v4[1],v4[2],v4[3]
v4[1]存放的地址指向(其实也是一个数组)

7.png
v4[2]存放的地址指向(虽然现在指向数组尾,但是后面会发现这是一个数据临时存放区)
8.png
v4[3]存放的地址指向(待处理的数据)
9.png
v4与其解析为一个数组,不如解析为一个结构体来的简洁明了
[C] 纯文本查看 复制代码
struct code_vm{
    unsigned int *code_ip;
    unsigned int *array;
    unsigned int temp;
    unsigned int *input;
}code_vm;

这时我们把v4转换为code_vm,其中code_ip(等同v4[0])保存指令集的游标,array(等同v4[1])保存一个数组(保存一些数据)的头地址,temp(等同v4[2])保存临时数据,input(等同v4[3])指向待处理的数据头
了解了这些内容,我们开始分析disasm函数

10.png 11.png
进入后,获取code_ip游标处的数据(寄存器ebx即为code_ip)
字节码0x64
此时读取1个字节(0x64)后保存到eax中,然后再减去0xC(十进制12),最后我们得到0x64,然后又与0xF3(0xFF-0xC)比较,这个比较并没有什么重要作用,这里我们可以忽略
12.png
向下单步跟进开始跳转
13.png
接下来获取code_ip+1和code_ip+2处的数据0和0,由于都是0所以不好辨别
继续单步进入call switch_64(同样段名经过了修改)

14.png
这里有个判断,大于7的话就会跳走,但是这里一般不会跳走,先不管它(后面讲解)
往下ecx被赋值4

继续单步往下走进入call get_array(同样改过名字)
15.png
执行操作ecx=ecx-1,此时ecx=3,然后与3比较,大于3就跳转
紧接着下面是mov eax,[eax+4],在执行之前有
29.png 30.png 31.png
此时eax指向图2中指向的位置(code_ip),由于该程序为32位,寄存器都是32位的,也就是4个字节,那么eax+4就是图3指向的位置(array),那么等同于eax=array
继续往下,ecx被赋值,但是赋值的结果与ecx有关,我们定位到dword_80BDA30,
32.png
这是一个长度为4的数组,所以才有了前面与3比较的过程,是为了检测是否数组越界
这里要使用的是下标为3的数据,也就是0xFFFFFFFF,那么就有ecx=0xFFFFFFFF
紧接着就是一个ADD操作
16.png
此时寄存器里的数据如上图,有ecx=ecx and array[edx]即ecx=array[edx]
最后有eax=array[edx],此时段名最好改为get_array,以便识别

17.png 33.png
跳出get_array段后,往下走又是一个get_array(图1)
这时我们注意到有两个与7比较的指令(同样是检测数组越界,这里就可以推断出array的长度为8),一个是esi,另一个是ebp,看我标出的红箭头,esi就是第一个参数*(code_ip+1),ebp是第二个参数*(code_ip+2),由于都为0所以不是很好分辨
第一个get_array的返回值给了ebx
第二个get_array的返回值存放于eax
后面就是ebx=ebx^eax,即ebx=array[*(code_ip+1)]^array[*(code_ip+2)]
往下走,进入call set_array(同样修改了名字)

18.png 19.png 20.png
这里分别有xor,and,xor,eax异或了两次,所以与eax无关,最后有eax=ecx
edx=array
最后有array[*(code_ip+1)]=array[*(code_ip+1)]^array[*(code_ip+2)]
21.png
code_ip+=3
整理后有
22.png
运行输出和内存数据对比,主要看array,现在对比还不明显
34.png 35.png
字节码0xD9
再次读取一个字节0xD9
23.png
24.png 25.png 26.png
这个就比较简单了

根据分析有temp=*(code_ip+1)
整理后有

27.png
程序输出和内存数据对比
36.png 37.png

字节码0xBF
再往下走就是0xBF
38.png 39.png
继续进入call
40.png 41.png
在这里有edx=*(code_ip+1)
code_ip-=1
ebx=temp
temp=ebx+0x10=temp+0x10
再往下就是set_array
这里不再进入,分析后,有array[edx]=ebx
最后整理后有
42.png
输出结果与内存数据对比
43.png 44.png
向下走又是0xD9
45.png
这里不再赘述,直接给出输出结果和内存数据的对比
46.png 47.png
再次走到switch跳转处,0xBF
48.png
输出结果和内存数据对比
49.png 50.png
字节码0xB7
51.png
再次走到switch跳转处,0xB7
52.png
首先获取了*(code_ip+1),*(code_ip+2)分别为0,2
53.png
一般检测后,调用了get_array
执行后esi=array[*(code_ip+1)]
54.png
这里再次调用get_array
执行后eax=array[*(code_ip+2)]
往下走
55.png
在这里,eax如果小于等于esi都会跳走,这里没有跳走
没有跳走直接将array[7]置0
整理后有
56.png
将输出结果与内存数据对比
57.png 58.png
字节码0x67
走到switch跳转处,0x67
59.png
60.png
根据上图有ecx=array[7]
如果ecx==0,就有code_ip+=5
否则会跳走,这里没有跳走,所以我们不再分析跳走的情况
整理后有
61.png
输出结果与内存数据对比
62.png 63.png
字节码0x65
走到switch跳转处,0x65
64.png
走到关键代码处
65.png
首先eax=*(code_ip+1)=0
进入call
66.png
这里再次遇到get_array
那么就有eax=array[*(code_ip+1)]
往下走
67.png
这里是edx=code_ip-1
往下走
68.png
在这里ecx=edx
所以最终有*(code_ip+4)=eax=array[*(code_ip+1)]
整理后有
69.png
输出结果与内存数据对比
70.png 71.png
字节码0x7E
走到switch跳转处,0x7E
73.png
走到关键代码处

72.png
同样获取*(code_ip+1)和*(code_ip+2)分别是3,0
进入call
74.png
在这里,ebx=input[*(code_ip+2)],从这里就开始对input进行操作了
这里同样注意code_ip+=3
下面调用set_array
通过跟进有array[*(code_ip+1)]=input[*(code_ip+2)]
整理后有
75.png
对比输出结果与内存数据

76.png 77.png
走到switch跳转处,0x65

78.png
对比输出结果和内存数据

79.png 80.png
走到switch跳转处,0x7E
81.png
对比输出结果和内存数据
82.png 83.png
走到switch跳转处,0x64
84.png
对比输出结果和内存数据
85.png 86.png
走到switch跳转处,0x65

87.png
对比输出结果和内存数据
88.png 89.png
字节码0x77
走到switch跳转处,0x77
90.png
91.png
首先是获取*(code_ip+1)和*(code_ip+2)分别是3,0
走到关键代码处
92.png
这里再次看见了get_array
eax=array[*(code_ip+1)]
93.png
这里有input[*(code_ip+2)]=eax
整理后有
94.png
对比输出结果和内存数据
95.png 97.png
98.png 99.png
到目前为止,我们终于看到了数据被操作,这里可以很容易看出数据处理逻辑是
input[0]=input[0]^input[0x10]
同样也可以得到一个假设,在执行0x77时就会修改input数据
走到switch跳转处,0xD9
96.png
对比输出结果和内存数据
100.png 101.png
走到switch跳转处,0xBF
102.png
对比输出结果和内存数据
103.png 104.png
走到switch跳转处,0xD9
105.png
对比输出结果和内存数据
106.png 107.png
走到switch跳转处,0xBF
108.png
对比输出结果和内存数据
109.png 110.png
字节码0x79
走到switch跳转处,0x79
111.png
走进去看
112.png
获取了*(code_ip+1)和*(code_ip+2)分别是3,0
进入call
113.png
这里调用了两次get_array,有ebx=array[*(code_ip+1)]
eax=array[*(code_ip+2)]
114.png
这里有ebx-=eax
调用set_array,array[*(code_ip+1)]=ebx
整理后有
115.png
对比输出结果和内存数据
116.png 117.png
走到switch跳转处,0x79
118.png
对比输出结果和内存数据
119.png 120.png
走到switch跳转处,0x65
121.png
对比输出结果和内存数据
122.png 123.png
走到switch跳转处,0x7E
124.png
对比输出结果和内存数据
125.png 126.png
走到switch跳转处,0x65
127.png
对比输出结果和内存数据
128.png 129.png
走到switch跳转处,0x7E
130.png
对比输出结果和内存数据
131.png 132.png
这时已将input[0xF],input[0xE]放入了array中
走到switch跳转处,0x64
133.png
对比输出结果和内存数据
134.png 135.png
走到switch跳转处,0x65
136.png
对比输出结果和内存数据
137.png 138.png
走到switch跳转处,0x77
139.png
对比输出结果和内存数据
140.png 142.png
141.png 143.png
这里的数据处理逻辑是input[0xE]^=input[0xF]
所以在这里可以做个假设,假设处理整个逻辑是
input[index]^=input[0x10+index](1)
input[0xE-index]^=input[0xF-index](2)
index是从0开始的,但是不清楚i到那里结束,在这里我们也可以猜想一下
可以看出第一个式子最多可以执行0xF次,第二个式子最多可以执行0xE次
所以i的结束条件可以是0xF,也可以是0xE
毕竟这些都是猜想,需要验证是否正确
这中题目类似于找规律,求归纳式子
那么要找规律就必须至少验证两次循环了
所以我们要验证处理过程是否正确,就需要再跟进一次循环过程
下面继续分析
字节码0x00
走到switch跳转处,0x00
144.png
进入跳转
145.png
这里获取了*(code_ip+1)是0
进入call
146.png
这里面调用了get_array和set_array
分析有
eax=array[*(code_ip+1)]
esi=eax+1
array[*(code_ip+1)]=esi
整理后有
147.png
对比输出结果和内存数据
149.png 148.png
走到switch跳转处,0x00
150.png
对比输出结果和内存数据
151.png 152.png
字节码0xA0
走到switch跳转处,0xA0
153.png
进入跳转
154.png
到这里,eax=0x0FFFFFFAA=-86(十进制)
eax=edx-89
这里就可以理解为回到了循环的开始
整理后有
155.png
对比输出结果和内存数据
156.png 157.png
接下来,我们分析一下执行的流程
程序在0xA0处回到了循环的开始
那么0xA0就可以作为一次循环结束的标志
然后我们通过修改我们编写的程序来进行模拟循环的执行流程
于是我们可以有
159.png
仔细观察,我们可以发现两次循环有相同的执行流程
我们设置可以查看循环3次,4次的输出结果,会更加明显
160.png
这是我们发现第二次,第三次,第四次循环的流程是一模一样的
那么我们假设的归纳式子就是成立的了
但是我们还是不知道结束的标志在哪里
其实细心的朋友可能已经知道了
i应该执行到0xE就结束,为什么呢?
我们仔细分析了12个有用的字节码,而其他的都是根本用不到的
而这里面的操作都是不同的
那么哪个操作码是判断结束的呢?
我们可以大致浏览一下每个字节码的作用
[C] 纯文本查看 复制代码
            case 0u:
                printf("0x00->");
//                printf("\t%X %X\n",*(codeVM->code_ip+1),*(codeVM->code_ip+2));
                codeVM->array[*(codeVM->code_ip+1)]++;
                codeVM->code_ip+=2;
                break;
            case 0x64u:
                printf("0x64->");
//                printf("\t%X %X\n",*(codeVM->code_ip+1),*(codeVM->code_ip+2));
                codeVM->array[*(codeVM->code_ip+1)]^=codeVM->array[*(codeVM->code_ip+2)];
                codeVM->code_ip+=3;
                break;
            case 0x65u:
                printf("0x65->");
//                printf("\t%X %X\n",*(codeVM->code_ip+1),*(codeVM->code_ip+2));
                *(codeVM->code_ip+4)=codeVM->array[*(codeVM->code_ip+1)];
                codeVM->code_ip+=2;
                break;
            case 0x67u:
                printf("0x65->");
//                printf("\t%X %X\n",*(codeVM->code_ip+1),*(codeVM->code_ip+2));
                if(codeVM->array[7]==0){
                    codeVM->code_ip+=5;
                }
                break;
            case 0x77u:
                printf("0x77->");
//                printf("\t%X %X\n",*(codeVM->code_ip+1),*(codeVM->code_ip+2));
                codeVM->input[*(codeVM->code_ip+2)]=codeVM->array[*(codeVM->code_ip+1)];
                codeVM->code_ip+=3;
                break;
            case 0x79u:
                printf("0x79->");
//                printf("\t%X %X\n",*(codeVM->code_ip+1),*(codeVM->code_ip+2));
                codeVM->array[*(codeVM->code_ip+1)]-=codeVM->array[*(codeVM->code_ip+2)];
                codeVM->code_ip+=3;
                break;
            case 0x7Eu:
                printf("0x7E->");
//                printf("\t%X %X\n",*(codeVM->code_ip+1),*(codeVM->code_ip+2));
                codeVM->array[*(codeVM->code_ip+1)]=codeVM->input[*(codeVM->code_ip+2)];
                codeVM->code_ip+=3;
                break;
            case 0xA0u:
                printf("0xA0->");
                if(i<3) {
                    printf("\n\n");
                }else{
                    return;
                }
//                printf("\t%X %X\n",*(codeVM->code_ip+1),*(codeVM->code_ip+2));
                codeVM->code_ip-=0x100-*(codeVM->code_ip+1);
                i++;
                break;
            case 0xB7u:
                printf("0xB7->");
//                printf("\t%X %X\n",*(codeVM->code_ip+1),*(codeVM->code_ip+2));
                if(codeVM->array[*(codeVM->code_ip+1)]<codeVM->array[*(codeVM->code_ip+2)]){
                    codeVM->array[7]=0;
                }else{
                    return;
                }
                codeVM->code_ip+=3;
                break;
            case 0xBFu:
                printf("0xBF->");
//                printf("\t%X %X\n",*(codeVM->code_ip+1),*(codeVM->code_ip+2));
                codeVM->array[*(codeVM->code_ip+1)]=codeVM->temp;
                codeVM->temp+=0x10;
                codeVM->code_ip+=2;
                break;
            case 0xD9u:
                printf("0xD9->");
//                printf("\t%X %X\n",*(codeVM->code_ip+1),*(codeVM->code_ip+2));
                codeVM->temp=*(codeVM->code_ip+1);
                codeVM->code_ip+=5;
                break;
        }

经过分析我们可以看到在字节码0xB7处,有一个判断,我们可以看一下它需要的参数
我们单独输出0xB7的参数
161.png
这是执行三次循环后的输出,我们发现参数是固定的,都是0和2
那么array[0]和array[2],里面又是什么呢?我们同样把它们输出
162.png
这样的话就十分清晰了,那么就有index<0xF
结合我们归纳的式子,整理后有
[C] 纯文本查看 复制代码
for(i=0;i<0xF;i++){
       input[i]^=input[0x10+i];
       input[0xE-i]^=input[0xF-i];
 }

这时我们可以和程序运行的结果进行对比
163.png 164.png
到这里这个字节码解释器就分析完毕了
0x03总结
当你看到这里时就会明白,文章题目为什么是和字节码解释器比耐心了,这篇文章跟进了程序的31个字节码,而这只是程序循环的第一个循环。搞逆向需要耐心,这句话一点都不错。我说一下,我对这个字节码解释器的理解,这个字节码解释器一共有12个不同的字节码,每个字节码后面都紧跟1到2个操作数,可以形象的理解为操作码+操作数的组合,也就是说每个字节码和它需要的参数共同构成了一条或多条汇编指令。循着这条路走,就有了@veritas501大神的思路,根据每个字节码,分别输出对应的汇编指令,在通过分析汇编指令,编写模拟C程序。文章可能存在不足之处,欢迎指正。

附件中包含代码和程序!!!
附件:https://pan.baidu.com/s/1o9qF97W 密码:cvs9
版权声明:允许转载,但是一定要注明出处

免费评分

参与人数 23吾爱币 +25 热心值 +22 收起 理由
acelite + 1 + 1 谢谢@Thanks!
chkds + 1 + 1 大佬真心秀啊
ws5483 + 1 + 1 我很赞同!
Xyzkst + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
q739806133 + 1 + 1 谢谢@Thanks!
xxpl123 + 1 + 1 谢谢@Thanks!
tobias + 1 + 1 用心讨论,共获提升!
virusPPP + 1 + 1 很赞&amp;#128077;
SomnusXZY + 1 + 1 热心回复!
cyay + 1 + 1 鼓励转贴优秀软件安全工具和文档!
aristotlez + 1 + 1 谢谢@Thanks!
飘荡的心 + 1 + 1 谢谢@Thanks!
我要做大神 + 1 + 1 谢谢@Thanks!
龟仔龟龟 + 1 + 1 鼓励转贴优秀软件安全工具和文档!
Ganlv + 1 + 1 用心讨论,共获提升!
Hackerpro + 1 + 1 谢谢@Thanks!
躲在角落看繁华 + 1 鼓励转贴优秀软件安全工具和文档!
malno + 1 + 1 我很赞同!
wmsuper + 2 + 1 我很赞同!
夏雨微凉 + 2 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
Ravey + 1 + 1 谢谢@Thanks!
哈哈嘿 + 1 + 1 谢谢@Thanks!
Tomatoman + 1 + 1 用心讨论,共获提升!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

~零度 发表于 2018-3-21 15:18
liphily 发表于 2018-2-27 18:07
我记得组成原理上讲,系统并不知道自己要读取的是操作指令还是数值,还是有个固定的读取模式来着了。。。。 ...

读取数据不也是一条指令吗
kong1235 发表于 2018-2-20 22:04
Tomatoman 发表于 2018-2-20 22:04
ScareCrowL 发表于 2018-2-20 23:02
mark一下,等更新
chen4321 发表于 2018-2-21 05:57 来自手机
坐等更新,大佬威武
此用户无法显示 发表于 2018-2-22 12:39
蒙蔽进来,懵逼回去
chenjingyes 发表于 2018-2-23 00:30
谢谢楼主分享  等待续哦
Hmily 发表于 2018-2-24 17:29
discuz对 【i】字符识别太恶心了,把文章全搞斜了。。。
Ganlv 发表于 2018-2-25 09:50
精品,虽然我没解过汇编层次的vm保护,但是的确知道,研究机器码就是一个体力活。
linuxprobe 发表于 2018-2-25 13:32
想这么高深的代码,这里面的人有能读懂的没啊?
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-15 13:59

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表