0x00前言
如题所言,本次的文章可能比较长,但是内容比较详细,适合新手学习,但是同样考验耐心。这次的文章严格说来是我上一篇文章遗留下来的历史问题,上一篇文章请参考 《 【又见AES】第十届全国大学生信息安全大赛之逆向--欢迎来到加基森 》 ,同时这篇文章中的字节码解释器与 《 【i春秋】第十届全国大学生信息安全大赛之逆向--apk题目》 有异曲同工之妙。在文章开始之前,我首先要感谢一下@ veritas501大神给的一些灵感 。
0x01静态分析
本次要分析的代码段位于0x 80495C0,如图是IDA还原出的关键伪C代码
这次我们要分析的就是disasm函数(这是改过的函数名,位于 0x 80495C0, ),在分析这个函数之前,我们需要先分析一下传入的参数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] 纯文本查看 复制代码 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] 纯文本查看 复制代码 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(icode_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)]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],里面又是什么呢?我们同样把它们输出
<font face="微软雅黑"><font size="4"><font color="#000000">这样的话就十分清晰了,那么就有index
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有帐号?立即注册
x
楼主热帖