本文首发于“合天智汇”公众号 作者:xiaoyuer
前情提要:
逆向入门分析实战(一):https://www.toutiao.com/i6805482528477020684/
逆向入门分析实战(二):https://www.toutiao.com/i6806187940901814792/
通过之前两篇文章,针对恶意代码为了确保自身只有一个实例在运行进行了正向开发和逆向分析。这种现象在恶意代码中非常常见,现在对上次的内容进行一个简要的回顾和扩展:
使用ida pro对恶意代码进行反汇编时会发现如下特征:
1、可以找到使用了windows的api函数 CreateMutex,该函数其中一个参数为互斥变量名。
2、在调用CreateMutex函数之后,通常会调用GetLastError函数,返回值与ERROR_ALREADY_EXISTS相同,即会使用cmp指令对返回值eax和ERROR_ALREADY_EXISTS对应的常量(16进制的B7,10进制的183)进行对比。
当然,本篇文章不是为了继续讲这个,现在我们应该更加深入的分析其他恶意代码常见的手法,这次分析恶意代码常见的获取计算机基本信息的函数,同样是先通过正向开发,然后进行逆向分析。
通常,恶意代码比较关注的计算机基本信息包括计算机名,计算机用户名,计算机的版本。下面我们就以获取这三个基本信息为例,进行正向开发和逆向分析。
一 正向开发,获取计算机基本信息
首先,我们需要掌握几个知识点:
1、GetComputerName函数,该函数有两个参数,第一个参数是一个缓冲区,用来接收计算机名。第二个参数指定该缓冲区的大小。对于经常使用Python进行编程的人来说,可能觉得有点奇怪,因为以Python语言的风格可能会是这样的:
computerName=GetComputerName()
函数无需传递参数,返回值即为计算机名。但是对于windows api很多函数来说,都会是这种风格,用某一个参数用来接收返回值,习惯就好。
2、GetUserName与GetComputerName函数用法十分类似。
3、GetVersionEx是用来获取计算机版本的函数,该函数只有一个参数,我乍一看觉得这个函数还挺简单,肯定和上面两个函数一样直接把返回值即计算机的版本返回到这个参数里了,当我仔细去看MSDN文档时发现,呵,参数居然是lpVersionInfo,这是什么破东西?经过仔细调研发现,这是一个指向OSVERSIONINFO结构体的指针,好吧,当初学C语言的时候就觉得指针这玩意贼烦,现在又来了。那就好好再学习一下指针吧!这个指针指向OSVERSIONINFO结构体,而这个结构体就是用来承载返回值系统版本的。也就是我们先创建一个OSVERSIONINFO结构体,之后把结构体的指针作为参数传入GetVersionEx函数即可,然后再从OSVERSIONINFO结构体中读取相应的系统版本。
接下来看代码:
这段代码中,在主函数中先声明子函数,然后调用子函数,之后使用getchar函数,用来获取一个键盘输入。为什么加这个getchar函数?主要是因为如果不加这个,有的时候,当你直接双击这个程序时,可能命令行一闪而过让你看不清输出的内容。
子函数中,首先是获取计算机名,创建了一个szComputerName字符数组用来接收计算机名,数组大小为MAXBYTE,为一个常量,通常为256,当然不同操作系统版本对应的大小可能不太一样,在后面逆向的时候我们可以查看。
之后,同理获取计算机用户名。最后获取系统版本,其中if语句里的条件可能看不太懂,这个主要是OSVERSIONINFO结构体的成员变量需要查阅MSDN即可。dwMajorVersion就对应不同的系统版本。当对应的值为6并且dwMinorVersion为1则是windows7或者windows server 2008 R2。如果想了解更多关于系统版本的内容可以查看MSDN:
https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-osversioninfoa
之后编译,运行:
使用windows7时的效果:
该主机计算机名确实为PC:
版本为windows 7:
而使用windows 10时的效果:
二、逆向分析:
将程序拖入ida pro:
首先调用puts将字符串输出,之后调用getSystemInfo函数,这就是我们前面编写的子函数。双击进入发现有很多行汇编代码,如果我们现阶段就要把每一行都弄得很明白,那要花很长时间,学习很多知识,所以和上次一样,我们要分清主次,把所调用的函数分析清楚即可,等后续自己“功力深厚”一些,再努力把每一行汇编代码都分析清楚。我们选中call,会发现所有的call指令都变黄了,这样便于我们分析,为了便于描述,将每一行汇编的地址调出来,点击options->General->Line prefixes,将其勾选:
1、逆向分析获取计算机名和用户名
此部分内容对应的汇编代码如下:
查看0040175E处的call命令,此处调用GetComputerNameA函数,之前提到过后面多出一个A指的是当前使用的为ASCII环境。
在这个函数前面有两个备注分别是lpBuffer和nSize这两个其实就是GetComputerNameA的参数,我们之前学习的时候,一般是使用push将参数入栈,此处怎么不是push?其实是一样的,仔细查看汇编指令push即可知道,其实push指令将参数入栈后,栈顶指针esp便会指向该参数,而此处使用的是mov将参数的值赋值给esp所指的地址空间,本质上是一样的。至于为何将nSize赋值给esp+4所指的空间,这是因为栈的增长方向以及参数所占的内存大小有关,此处简要介绍一下,因为标准调用约定中需要从右往左入栈,需要先将nSize参数入栈,再将lpBuffer参数入栈。根据栈的特点,先入栈的参数地址高,后入栈的参数地址低。即栈是一种由高地址向低地址扩展的数据结构。有兴趣的可以进一步查阅push指令和函数调用约定相关的资料。
执行完0040175E处的call命令返回值便会存储在lpBuffer对应的缓冲区内,此处即ebp+Buffer所指的空间内。之后调用printf函数,进行格式化输出,其中同样涉及到esp+4,原因也是因为需要将GetComputerNameA获取到的计算机名先入栈,再将“computer name is %s \r\n”入栈。如果你熟悉格式化字符串漏洞的话,你会有一种很熟悉的感觉,当然如果你已经掌握了这里的知识,可以深入去了解格式化字符串漏洞的相关内容。
获取用户名的逆向分析过程与获取计算机名的过程类似,此处不过多介绍了。
2、逆向分析获取操作系统版本
这段内容对应的汇编代码如下:
首先查看00401812处的代码,94h对应10进制为148,这个数值便是sizeof(OSVERSIONINFO)的返回值,即OSVERSIONINFO结构体的大小。之后使用lea指令,它是Load Effective Address的缩写,即加载有效地址,将该结构体所在的地址赋值给eax,之后将eax中保存的地址空间存入esp所指的空间中,整个过程即可完成结构体指针入栈工作。
然后调用GetVersionEXA函数,返回值将会放置在结构体中。之后将该结构体中的dwMajorVersion成员变量与6对比,dwMinorVersion与1对比,之后使用jnz命令来决定是否跳转,之前的文章将提及过,该命令是jump not zero的缩写,当不等于0时跳转。如果此处不是很熟悉,建议查阅之前的文章,掌握cmp和jnz以及zf标志寄存器的相关知识。再之后便使用puts输出字符串。
总结:
获取这些基本的信息其实很简单,只需调用几个windows API即可,对恶意代码进行逆向分析也很容易定位到是否在获取这些基本信息。而如果我们通过对这些windows API调用的过程进行深入分析,便可以进一步掌握C语言、汇编语言和数据结构等相关的原理,比如指针,lea指令、栈的工作原理,或者进一步学习格式化字符串漏洞。
参考书籍:
《C++黑客编程揭秘与防范》冀云著,第1版,2012.6--北京,人民邮电出版社
《C++反汇编与逆向分析技术揭秘》钱松林,赵海旭著--北京:机械工业出版社,2011年9月。
《恶意代码分析实战》 (美)Michael Sikorski / Andrew Honig 著,诸葛建伟,姜辉,张光凯译 -- 北京:电子工业出版社,2014年4月,原书名: Practical Malware Analysis: The Hands-On Guide to Dissecting Malicious Software。
《汇编语言》王爽 著--2版,北京:清华大学出版社,2008年4月。
《逆向工程核心原理》,李承远著--北京,人民邮电出版社,2014年第1版。
声明:笔者初衷用于分享与普及网络知识,若读者因此作出任何危害网络安全行为后果自负,与合天智汇及原作者无关!