这一系列的文章都是与恶意软件相关的课程,因此我们首先从PE和ELF文件的完整结构讲起。
另一个比较重要的概念就是这些恶意程序是通过可理解的汇编代码执行的,而这些不同的代码在二进制层面的架构如何呢?在这篇文章中,我们将在汇编层面认识代码结构。
注意:这篇文章的受众人群默认是对汇编语言有一定的掌握。如果基础不好的好也不必担心,我会以最简单方式来解释这些东西。
代码中声明的用于保存常量或者字符串的东西叫做变量。变量的范围可以是全局变量也可以是局部变量。局部变量保存的是函数内部的值而全局变量可以在代码逻辑中的任何地方使用。当我们深入汇编代码的时候,全局变量表示的都是一些内存的地址,反之局部变量表示的是寄存器ebp的偏移值 。对于不熟悉汇编语言开始的人,汇编语言函数开头的指令类似这样:
push ebp
mov ebp,esp
ebp设置新的函数的栈帧,第二条指令将ebp设置为基址。由于栈的生长方式是向上地址递减,这就意味着局部变量可以通过类似mov eax,[ebp-4]存取,即将ebp-4内的四个字节移动至eax中。局部变量由于不隶属于任意函数,所以如果通过ebp是无法引用这些变量的,而全局变量是指向的是内存地址,例如mov eax,dword_50CF60,全局变量地址为0x50CF60。
If条件语句经常在代码中使用到。一个基本的if条件语句如下:
对应的汇编代码如下:
mov [ebp + var-4],1 mov [ebp+var_8],2 mov eax,[ebp+var_4] cmp eax,[ebp+var_8] jnz short loc_50101B push offset aequalsb;”a & b are equal/n” …… Push offset anotequalb;”a & b are not equal” ……
注意cmp和’jnz-compare’、’jump if no zerm’指令对应于代码中的’if(a==b)。Cmp指令表示减去操作。因此,意思是比较两个变量,如果不相等则跳转至这个内存地址处,并且打印字符串”a&b are not equal”,否则的话,如果变量相等,jnz指令将会被跳过,接着打印字符串”a&b are equal”。如果内嵌多条if语句,你将会看到多个cmp,jnz/jz,接着在cmp后续会打印字符串或者其他的操作。
代码逻辑中的循环语句是用来迭代一些操作,并且一直执行到某一个条件满足为止。For循环是经常被使用到的。
对于For循环搞清楚这4件事:初始化,比较,执行,增加/减少。
for(int i=0;i<10;i++) { Printf(“Current value of i is %d/n”,i); }
对应的汇编代码包含4个部分:
mov [ebp+var_4],0 jmp short loc_102345 loc_987654 mov eax, [ebp+var_4] add eax,1 mov [ebp+var_4],eax loc_102345 cmp [ebp+var_4],Ah jge short loc_23456 mov ecx,[ebp+var_4] push ecx push offset iValue;”Current Value of i is %d/n” call printf add esp, 8 jmp loc_987654
上述汇编代码我们可以看到,第一条指令是初始化,随后一个跳转,和10(Ah)进行比较,如果大于等于10则跳转至loc23456处,否则打印I的值,随后跳转至loc987654,i值加1,。接着再比较((i=1)>=(i=10))。如果不是则值被打印出来再递增。这整个过程再继续一遍。
While循环语句在代码中可以很轻易被追踪到。例如:
int i=0; while(i<10) { printf(“current value of I is %d/n”,i) i++; }
汇编语言
Mov [ebp+var_4], 0 Jmp short loc_12345 Loc_123456: mov eax, [ebp+var_4] add eax,1 mov [ebp_var_4],eax Loc_102345: cmp [ebp+var_4],Ah jge short loc_234567 mov ecx,[ebp+var_4] push ecx push offset iValue,” current value of I is %d/n” add esp,8 jmp loc_1023456
可以看到While循环的汇编代码,和for循环类似。
Switch case 经常被程序使用到,通过值来进行判定。例如:
Switch(i) { Case 1: Printf(“Current Value of I is %d/n”,i+1); break; Case 2: Printf(“Current Value of I is %d/n”,i+1); break; Case 3: Printf(“Current Value of I is %d/n”,i+1); break; Case 4: Printf(“Current Value of I is %d/n”,i+1); break; default: break; }
汇编代码语言中看起来像是一系列的if语句,因为在刚开始的阶段可以看到很多cmp和jmp指令。当这些case没有按照顺序例如case 1 ,case12,case17等等,在这种情况下,汇编代码中就会出现很多if-else语句。变量会紧跟在case1,case2,case3等后面。编译器进行简单的优化如下:
Mov ecx,[ebp+var_4] Sub ecx,1 cmp [ebp+var_8],3 ja loc_12345 mov edx,[ebp+var_8] jmp short loc_987650[edx*4] loc_234564: ….. Jmp loc_12345: loc_234565: ….. Jmp loc_12345: loc_234566: ….. Jmp loc_12345: loc_234567: ….. Jmp loc_12345: loc_12345: // 清楚栈代码 loc_987650 offset loc_234564 //jump table offset loc_234565 offset loc_234566 offset loc_234567
在这里都发生了些什么呢。首先我将相似内存地址的都进行了颜色标示(希望有所帮助!)。初始化case变量,例如“i”,保存在ecx,然后和1相减,为什么呢,这里用到一些跳转表的概念,编译器优化这些代码的时候采用这张跳转表,跳转表中存放的是这些不同case值的地址。因此ecx需要递减1,因为它是作为偏移指针指向这些开始位置为0的跳转表。最大的case最先比较是因为,这个case是默认的设置的case。对于其他变量,将会在跳转表中设置成偏移值。
这篇只是描述一些代码中最基本的特征。在第二部分会讨论一些复杂的结构像数组、结构体和链表。
*参考来源: infosecinstitute ,FB小编老王隔壁的白帽子翻译,转载请注明来自FreeBuf黑客与极客(FreeBuf.COM)