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

作者: skywilling 分类: ELF逆向,逆向 发布时间: 2018-05-29 11:43

转自https://www.52pojie.cn/thread-700728-1-1.html

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

0x01静态分析
本次要分析的代码段位于0x80495C0,如图是IDA还原出的关键伪C代码

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


0x02动态分析
在这里我是用的是Ubuntu虚拟机(该文件是ELF程序)+IDA,这里输入以0123456789012345为例,直接断点到0x0804964A(call    disasm)


此时传入的参数位于栈顶

然后在Hex View中查看内存

选中的部分就是v4[0],紧跟的三个不为零的数据分别是v4[1],v4[2],v4[3]
v4[1]存放的地址指向(其实也是一个数组)


v4[2]存放的地址指向(虽然现在指向数组尾,但是后面会发现这是一个数据临时存放区)

v4[3]存放的地址指向(待处理的数据)

v4与其解析为一个数组,不如解析为一个结构体来的简洁明了

[C] 纯文本查看 复制代码
1
2
3
4
5
6
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函数

 
进入后,获取code_ip游标处的数据(寄存器ebx即为code_ip)
字节码0x64
此时读取1个字节(0x64)后保存到eax中,然后再减去0xC(十进制12),最后我们得到0x64,然后又与0xF3(0xFF-0xC)比较,这个比较并没有什么重要作用,这里我们可以忽略

向下单步跟进开始跳转

接下来获取code_ip+1和code_ip+2处的数据0和0,由于都是0所以不好辨别
继续单步进入call switch_64(同样段名经过了修改)


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

继续单步往下走进入call get_array(同样改过名字)

执行操作ecx=ecx-1,此时ecx=3,然后与3比较,大于3就跳转
紧接着下面是mov eax,[eax+4],在执行之前有
  
此时eax指向图2中指向的位置(code_ip),由于该程序为32位,寄存器都是32位的,也就是4个字节,那么eax+4就是图3指向的位置(array),那么等同于eax=array
继续往下,ecx被赋值,但是赋值的结果与ecx有关,我们定位到dword_80BDA30,

这是一个长度为4的数组,所以才有了前面与3比较的过程,是为了检测是否数组越界
这里要使用的是下标为3的数据,也就是0xFFFFFFFF,那么就有ecx=0xFFFFFFFF
紧接着就是一个ADD操作

此时寄存器里的数据如上图,有ecx=ecx and array[edx]即ecx=array[edx]
最后有eax=array[edx],此时段名最好改为get_array,以便识别

 
跳出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(同样修改了名字)

 
这里分别有xor,and,xor,eax异或了两次,所以与eax无关,最后有eax=ecx
edx=array
最后有array[*(code_ip+1)]=array[*(code_ip+1)]^array[*(code_ip+2)]

code_ip+=3
整理后有

运行输出和内存数据对比,主要看array,现在对比还不明显
 
字节码0xD9
再次读取一个字节0xD9

 
这个就比较简单了

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


程序输出和内存数据对比
 

字节码0xBF
再往下走就是0xBF
 
继续进入call
 
在这里有edx=*(code_ip+1)
code_ip-=1
ebx=temp
temp=ebx+0x10=temp+0x10
再往下就是set_array
这里不再进入,分析后,有array[edx]=ebx
最后整理后有

输出结果与内存数据对比
 
向下走又是0xD9

这里不再赘述,直接给出输出结果和内存数据的对比
 
再次走到switch跳转处,0xBF

输出结果和内存数据对比
 
字节码0xB7

再次走到switch跳转处,0xB7

首先获取了*(code_ip+1),*(code_ip+2)分别为0,2

一般检测后,调用了get_array
执行后esi=array[*(code_ip+1)]

这里再次调用get_array
执行后eax=array[*(code_ip+2)]
往下走

在这里,eax如果小于等于esi都会跳走,这里没有跳走
没有跳走直接将array[7]置0
整理后有

将输出结果与内存数据对比
 
字节码0x67
走到switch跳转处,0x67


根据上图有ecx=array[7]
如果ecx==0,就有code_ip+=5
否则会跳走,这里没有跳走,所以我们不再分析跳走的情况
整理后有

输出结果与内存数据对比
 
字节码0x65
走到switch跳转处,0x65

走到关键代码处

首先eax=*(code_ip+1)=0
进入call

这里再次遇到get_array
那么就有eax=array[*(code_ip+1)]
往下走

这里是edx=code_ip-1
往下走

在这里ecx=edx
所以最终有*(code_ip+4)=eax=array[*(code_ip+1)]
整理后有

输出结果与内存数据对比
 
字节码0x7E
走到switch跳转处,0x7E

走到关键代码处

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

  
走到switch跳转处,0x65

 
对比输出结果和内存数据

  
走到switch跳转处,0x7E
 
对比输出结果和内存数据
 
走到switch跳转处,0x64

对比输出结果和内存数据
  
走到switch跳转处,0x65

 
对比输出结果和内存数据
 
字节码0x77
走到switch跳转处,0x77


首先是获取*(code_ip+1)和*(code_ip+2)分别是3,0
走到关键代码处

这里再次看见了get_array
eax=array[*(code_ip+1)]

这里有input[*(code_ip+2)]=eax
整理后有

对比输出结果和内存数据
 
 
到目前为止,我们终于看到了数据被操作,这里可以很容易看出数据处理逻辑是
input[0]=input[0]^input[0x10]
同样也可以得到一个假设,在执行0x77时就会修改input数据
走到switch跳转处,0xD9

对比输出结果和内存数据
 
走到switch跳转处,0xBF
 
对比输出结果和内存数据
 
走到switch跳转处,0xD9

对比输出结果和内存数据
 
走到switch跳转处,0xBF

对比输出结果和内存数据
 
字节码0x79
走到switch跳转处,0x79

走进去看

获取了*(code_ip+1)和*(code_ip+2)分别是3,0
进入call

这里调用了两次get_array,有ebx=array[*(code_ip+1)]
eax=array[*(code_ip+2)]

这里有ebx-=eax
调用set_array,array[*(code_ip+1)]=ebx
整理后有

对比输出结果和内存数据
 
走到switch跳转处,0x79

对比输出结果和内存数据
 
走到switch跳转处,0x65

对比输出结果和内存数据
 
走到switch跳转处,0x7E

对比输出结果和内存数据
 
走到switch跳转处,0x65

对比输出结果和内存数据
 
走到switch跳转处,0x7E

对比输出结果和内存数据
 
这时已将input[0xF],input[0xE]放入了array中
走到switch跳转处,0x64

对比输出结果和内存数据
 
走到switch跳转处,0x65

对比输出结果和内存数据
 
走到switch跳转处,0x77

对比输出结果和内存数据
 
 
这里的数据处理逻辑是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

进入跳转

这里获取了*(code_ip+1)是0
进入call

这里面调用了get_array和set_array
分析有
eax=array[*(code_ip+1)]
esi=eax+1
array[*(code_ip+1)]=esi
整理后有

对比输出结果和内存数据
 
走到switch跳转处,0x00

对比输出结果和内存数据
 
字节码0xA0
走到switch跳转处,0xA0

进入跳转

到这里,eax=0x0FFFFFFAA=-86(十进制)
eax=edx-89
这里就可以理解为回到了循环的开始
整理后有

对比输出结果和内存数据
 
接下来,我们分析一下执行的流程
程序在0xA0处回到了循环的开始
那么0xA0就可以作为一次循环结束的标志
然后我们通过修改我们编写的程序来进行模拟循环的执行流程
于是我们可以有
 
仔细观察,我们可以发现两次循环有相同的执行流程
我们设置可以查看循环3次,4次的输出结果,会更加明显

这是我们发现第二次,第三次,第四次循环的流程是一模一样的
那么我们假设的归纳式子就是成立的了
但是我们还是不知道结束的标志在哪里
其实细心的朋友可能已经知道了
i应该执行到0xE就结束,为什么呢?
我们仔细分析了12个有用的字节码,而其他的都是根本用不到的
而这里面的操作都是不同的
那么哪个操作码是判断结束的呢?
我们可以大致浏览一下每个字节码的作用

[C] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
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
67
68
69
70
71
72
73
74
75
76
77
78
            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的参数

这是执行三次循环后的输出,我们发现参数是固定的,都是0和2
那么array[0]和array[2],里面又是什么呢?我们同样把它们输出

这样的话就十分清晰了,那么就有index<0xF
结合我们归纳的式子,整理后有

[C] 纯文本查看 复制代码
1
2
3
4
for(i=0;i<0xF;i++){
       input[i]^=input[0x10+i];
       input[0xE-i]^=input[0xF-i];
 }

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

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

发表评论

电子邮件地址不会被公开。 必填项已用*标注