剑客
关注科技互联网

逆向工厂(二):静态分析技术

* 本文原创作者:追影人, 本文属FreeBuf原创奖励计划,未经许可禁止转载

前言

[逆向工厂]第一章节中介绍了逆向技术的相关基础知识,其中提到逆向的两种形式:静态分析、动态分析。 本章将对静态分析技术进行讲解,重点阐述静态分析的原理方法,程序的静态结构,常见流程控制语句的静态反编译形态,并且通过实例来掌握利用IDA逆向工具的静态逆向分析技术。

一、静态分析原理与方法

上一篇章介绍到,程序运行前需要将硬盘内编译好的程序文件装载进内存,然后将指令送入CPU执行,此时程序就像“复活”一样,按照指令的“先后顺序”开始工作,这便是程序的“动态”。与其相反,静态分析就是在程序尚未运行的状态时进行逆向分析的行为。但是,静态分析并非在硬盘上直接进行,仍需将文件存入内存进行分析,只是此时程序文件只是单纯作为数据被逆向软件加载。

1、“简单暴力”的静态分析

既然静态分析只是把程序作为数据文件加载进逆向软件,进行查看分析,那岂不是用UltraEidt这类文本编辑软件直接查看即可。

逆向工厂(二):静态分析技术

上图是使用UE打开的某程序,可以看到程序的所有数据,通过查看文件PE头可以获取所有头信息,一些文件信息查看工具也正是利用该原理,直接读取静态文件按照PE格式提取相对应的信息。上图所展示的区域是程序的代码段,这些看似很乱的字节码其实都对应着一条条的汇编语句,在这里UE只是很粗暴的将原始编译数据进行了展示,并没有进行反编译处理。

Q:静态分析中UE这种过于“暴力”的工具可以干什么?

最常用的便是使用UE处理字符串,使用“查找”功能搜索标题关键字。

逆向工厂(二):静态分析技术

在UE中修改标题,保存后再运行,程序效果如下:

逆向工厂(二):静态分析技术

你可以利用这样的手段自己随意修改程序的字串信息包括各种版权信息等(当然小心侵权哈),最重要的是一些程序会将key等重要信息以明文的形式存储,所以使用UE可以找到这些key。这样做的前提是,字串均以明文形式存储,未加密处理,但通常商业程序会通过“壳”保护技术对程序加密保护,关于“壳”方面的知识在后续篇章介绍。

2、“高级”静态分析

相比上面“原生”字节码,童鞋更愿意阅读“高级”点的语言。既然同一种架构平台的字节码与汇编语句是一一对应的,那么根据这个原理便可将“二进制”文件反编译成汇编语言。

逆向工厂(二):静态分析技术

上图是IDA逆向工具反编译结果,相比UE的“暴力”,这个非常人性化,在第三节中重点介绍该软件的一些知识。

相比C/C++这类编译型程序,C#等带有解释运行的程序静态逆向的结果就更为“高级”了,甚至堪比源代码,下图便是某安全公司分析国产敲诈者病毒,该病毒为C#编写。(文章传送门 http://www.freebuf.com/articles/system/114046.html

逆向工厂(二):静态分析技术

可清晰看到程序的执行流程,心疼作者没有用c写,暴露的这么赤裸裸。(其实不管用什么语言写,只要没有刻意去做代码保护,都容易被逆向分析出完整流程。)

二、代码结构

上一章我们讲到helloworld程序,本节将源代码做一些改变后继续。

#include

  
   
 
charfreebuf[30] = {"hello,freebuf!/n"};
voidmain()
{
printf("hello,world!/n");
printf("%s",freebuf);
}

  

将编译好的的程序用IDA打开,代码的整体结构如下图:

逆向工厂(二):静态分析技术

由图可知,程序先是执行__tmainCRTStartup函数,然后执行main函数。

__tmainCRTStartup函数反汇编代码:

逆向工厂(二):静态分析技术

逆向工厂(二):静态分析技术

逆向工厂(二):静态分析技术

__tmainCRTStartup函数中调用main函数,在调用main函数之前,程序已经执行了一部分指令,例如:全局对象的构造函数,一些全局变量、对象和静态变量、对象的空间分配和赋初值,一些初始化代码(如设置环境变量等)。

main函数执行结束后返回到__tmainCRTStartup函数,执行一些收尾工作,如:全局对象的析构函数,释放空间、释放资源使用权等操作。

总结:程序在执行main函数前已经有代码运行,main函数的结束不代表进程的结束。

helloworld程序的区段信息:

逆向工厂(二):静态分析技术

helloworld程序四个区段:

1、.text,默认的代码区段,它的内容全是指令代码

逆向工厂(二):静态分析技术

2、.idata,包含其他外来的DLL的函数及数据信息,即输入表,在这里可以看见程序链接dll和调用dll中函数的情况。

逆向工厂(二):静态分析技术

3、.rdata,默认只读数据区块,一般两种情况用到,一是MS的链接器产生EXE文件中用于存放调试目录,二是用于存放说明字符串。

逆向工厂(二):静态分析技术

4、默认的读/写数据块,全局变量,静态变量一般放在这个区段。

逆向工厂(二):静态分析技术

以上是PE最基本的四个区段,还有一些区段描述,如下表所示:

名称 描述
.edata 输出表,当创建一个输出API或数据的可执行文件时,连接器会创建一个.EXP文件,这个.EXP文件包含一个.edata区块,其会被加载到可执行文件中
.rsrc 资源,包括模块的全部资源,如图标,菜单,位图等,这个区块是只读的
.crt 用于C++运行时(CRT)所添加的数据
.tls TLS的意思是线程局部存储器,用于支持通过_declspec(thread)声明的线程局部存储变量的数据,这包括数据的初始化值,也包括运行时所需要的额外变量
.reloc 可执行文件的基址重定位,基址重定位一般仅Dll需要的
.sdata 相对于全局指针的可被定位的 短的读写数据
.pdata 异常表,包含CPU特定的IAMGE_RUNTIME_FUNTION_ENTRY结构数组,DataDirectory中的IMAGE_DIRECTORY_ENTRY_EXCEPTION指向它.

本节概述了反汇编代码的结构与一般组成,有利于大家在逆向分析中对代码的整体把握。

三、IDA Pro与静态分析

“工欲善其事必先利其器”,IDA Pro无疑是静态反汇编工具里最强大的一个,下面就IDA简单介绍。

逆向工厂(二):静态分析技术

1、窗口管理,可以查看反汇编、hex数据、函数列表、字符串、段寄存器、函数调用、函数被调用等情况。

逆向工厂(二):静态分析技术

代码中的字符串:

逆向工厂(二):静态分析技术

区段信息:

逆向工厂(二):静态分析技术

函数调用,查看调用和被调用函数的信息,下图是以helloworld程序中main函数为例:

逆向工厂(二):静态分析技术

2、图表信息,可以查看代码的执行流程图、函数调用图等。

逆向工厂(二):静态分析技术

函数被调用(调用main函数的函数):

逆向工厂(二):静态分析技术

函数调用(main函数调用的其它函数):

逆向工厂(二):静态分析技术

函数执行流程(__tmainCRTStartup函数为例):

逆向工厂(二):静态分析技术

3、交叉引用与添加注释

在出现XREF 的地方就是有交叉引用(如下图);

而在XREF后面的向上箭头,双击它可以跳到它跳转的地方(如下图);

逆向工厂(二):静态分析技术

添加注释

在代码行右击鼠标选择“添加注释”或直接敲击“;”键

逆向工厂(二):静态分析技术

空格键可以将代码与流程图间切换:

逆向工厂(二):静态分析技术

逆向工厂(二):静态分析技术

4、反汇编函数中出现的标识含义

sub_XXXXXX子程序
loc_xxxxxx地址
byte_xxxxxx8位数据
word_xxxxxx16位数据
dword_xxxxxx32位数据
unk_xxxxxx未知的

5、伪代码转换

IDA伪代码转换插件,可以将汇编语言转换为易读的类似于高级语言的伪代码,将光标放置在汇编代码区,敲击F5键就可将汇编语言转换为伪代码。

逆向工厂(二):静态分析技术

逆向工厂(二):静态分析技术

四、流程控制语句的反编译形态

通常,程序在执行时按照代码的先后顺序执行,但一些语句会使得程序跳过某些代码执行,或者重复执行某段代码,这种改变程序执行流程的语句便是“流程控制语句”。流程控制语句用来实现对程序流程的选择、循环、转向和返回等控制,不同语言因语法语义的差别,控制语句也有差异。

逆向分析时,如果能快速识别出流程控制语句,对于梳理程序结构和流程将事半功倍,因此熟悉各类流程控制语句及其对应的反编译代码结构尤为重要。本节重点针对c语言流程控制语言的反编译形态进行讲解。

C语言共包含4大类共9种控制语句:

1、选择语句(If、switch语句)

又称分支语句,该类语句从判断点开始,存在不止一条分支可供程序执行,通过给定的条件进行真假判断或者值判断,从而决定执行两个或多条分支的哪个分支。

2、循环语句(do while、while、for语句)

程序进入该语句后,重复执行循环体内代码,当满足某种条件后跳出循环语句执行后续代码。

3、转向语句(break、continue、goto语句)

该类语句可打断程序当前执行的循环体,或者跳到指定的任意标记位处继续执行。

4、返回语句,return语句。

返回语句通常用于函数调用过程中的函数返回。

为深入理解掌握各类控制语句在反编译结果的形态,现编写各类控制语句的源码,生成对应程序,再利用IDA反编译,观察其形态。

1、if语言

If条件判断语句通常有三种结构

逆向工厂(二):静态分析技术

下面是三种结构分别对应的源码、反编译形态、伪代码(通过IDA F5伪代码插件获得)。

If结构

逆向工厂(二):静态分析技术

If else双路结构

逆向工厂(二):静态分析技术

If else多路结构

逆向工厂(二):静态分析技术

上图可以看到在条件判断语句中,汇编代码通过cmp、test等比较语句进行条件的判断,然后通过jmp、jle、jns等跳转语句进行流程跳转。童鞋们会注意到,在源码中,printf函数出现在不同分支中,但是在反编译结果中,printf并没有出现在多个分支,而是分支中压入不同的打印参数,统一跳转到401033处调用prinf函数,这种情况是因为在程序生成过程中,编译器根据源码进行优化处理,减少不必要重复,精简程序,缩小体积。

图中伪代码部分可以看出,除变量名称外,伪代码和源代码竟然非常接近甚至一模一样,而在平常的逆向过程中,ida F5伪代码插件经常会用到,可以大大提高分析效率。图三中伪代码结构并非和源代码一样,而是嵌套形式的双路结构,这是因为程序在编译后是不可逆的,并不能恢复成初始状态。

2、Switch语句

源代码:

int a;

scanf_s("%d", &a);

switch (a)

{

case 0:

printf("input:0/n");

break;

case 1:

printf("input:1/n");

break;

case 2:

printf("input:2/n");

break;

default:

printf("input:other/n");

break;

}

printf("the program end/n");

反编译结果:

text:00401009 push offset Format ; "%d"
.text:0040100E call ds:scanf_s
.text:00401014 mov eax, [ebp+var_4]
.text:00401017 add esp, 8
.text:0040101A sub eax, 0
.text:0040101D jz short loc_401040
.text:0040101F dec eax
.text:00401020 jz short loc_401039
.text:00401022 mov esi, ds:printf
.text:00401028 dec eax
.text:00401029 jz short loc_401032
.text:0040102B push offset aInputOther ; "input:other/n"
.text:00401030 jmp short loc_40104B
.text:00401032 ; ---------------------------------------------------------------------------
.text:00401032
.text:00401032 loc_401032: ; CODE XREF: _main+29j
.text:00401032 push offset aInput2 ; "input:2/n"
.text:00401037 jmp short loc_40104B
.text:00401039 ; ---------------------------------------------------------------------------
.text:00401039
.text:00401039 loc_401039: ; CODE XREF: _main+20j
.text:00401039 push offset aInput1 ; "input:1/n"
.text:0040103E jmp short loc_401045
.text:00401040 ; ---------------------------------------------------------------------------
.text:00401040
.text:00401040 loc_401040: ; CODE XREF: _main+1Dj
.text:00401040 push offset aInput0 ; "input:0/n"
.text:00401045
.text:00401045 loc_401045: ; CODE XREF: _main+3Ej
.text:00401045 mov esi, ds:printf
.text:0040104B
.text:0040104B loc_40104B: ; CODE XREF: _main+30j
.text:0040104B ; _main+37j
.text:0040104B call esi ; printf
.text:0040104D add esp, 4
.text:00401050 push offset aTheProgramEnd ; "the program end/n"
.text:00401055 call esi ; printf
.text:00401057 add esp, 4
.text:0040105A xor eax, eax
.text:0040105C pop esi
.text:0040105D mov esp, ebp
.text:0040105F pop ebp
.text:00401060 retn
.text:00401060 _main endp

反编译结果可见,switch语句也进行了同样优化,多个分支共同调用同一处printf函数。

伪代码如下

int __cdecl main(int argc, const char **argv, const char **envp)
{
void (*v3)(const char *, ...);
const char *v5;
int v6;
scanf_s("%d", &v6);
if ( !v6 )
{
v5 = "input:0/n";
goto LABEL_8;
}
if ( v6 == 1 )
{
v5 = "input:1/n";
LABEL_8:
v3 = (void (*)(const char *, ...))printf;
printf(v5);
goto LABEL_9;
}
v3 = (void (*)(const char *, ...))printf;
if ( v6 == 2 )
printf("input:2/n");
else
printf("input:other/n");
LABEL_9:
v3("the program end/n");
return 0;
}

伪代码通过if else语句、goto语句配合完成整个过程,从这点便可证明switch在底层实现上是与if else语句相同。

3、While、do while和for语句

逆向工厂(二):静态分析技术

当满足执行条件时,程序进入while循环体,不断重复执行循环体内代码,直到条件为假时离开循环体。从反编译结果可以看到,通过inc语句对变量i的值进行自加,通过cmp对比i是否小于0×8,通过对比结果再决定是否跳回循环体的第一条指令处401010。在伪代码中可以看到是以do while结构展示,下面我们再看看do while语句。

与while条件为真才进入循环体不同,do while语句是先进入循环体然后再判断条件,以决定是否重复执行循环体。

逆向工厂(二):静态分析技术

从上面的结果可以看出,do while和while语句的反编译结果一致,这似乎与我们上面提到的不同有出入。这个例子中,i的初始值为0,即第一次执行循环体时,i<10为真,因此程序在进行编译时进行了优化,导致其二进制结构和do while一致。

for语句也是拥有循环体和判断条件,经常搬砖的同学都晓得for语句是可以转换为while语句,在编译器眼里也如此,for语句在编译过程中和while语句一致,反编译的结果也如上图。

4、Break和continue语句

Break和congtinue语句一般用在while循环体中,break用于跳出while循环体,continue用于结束本次循环进入下次循环。下图可以看到break语句在条件判断成功后,jmp到循环体后面的语句。

逆向工厂(二):静态分析技术

Continue语句执行后,会终止本轮循环体的执行,跳转到循环体的判断语句,通过判断后再决定是否进入下轮循环。

逆向工厂(二):静态分析技术

5、goto语句

Goto语句可以调到任意标签地址处,因这种跳转破坏了程序的结构性,让代码的执行顺序杂乱无章,一般在软件开发中不推荐使用这种跳转。下图中伪代码部分再次看出程序的不可逆性,编译器在编译连接时,根据程序的整体结构和部分流程进行综合优化编译,同样的一段源码也许在不同编译器或者不同工程中,生成的编译结果均有差异。

逆向工厂(二):静态分析技术

6、return语句

Return语句用于被调函数执行完毕后,返回到主调函数继续执行,通常返回时可带一个返回值。在学习return前我们有必要了解程序调用函数的机制。

函数通常分为无返回值函数(void类型)和有返回值函数,前者在编程时无须写return,而后者必须以return+返回值 的形式作为结尾语句。根据函数的来源,分为系统函数(由系统DLL提供)和自定义函数(用户自己编写),在调用函数时,均通过call语句跳转到函数的入口进行执行,看起来和jmp语句等价,但是call语句会将下一句的位置记录自动保存下来,等函数执行完毕后,调用rent语句,返回至刚才记录的位置继续执行。

Call语句都是以call+函数地址 的形式存在,逆向工具为提高可读性,通常会将调用系统函数的地方替换成该函数名称,从而一目了然确定调用对象。

逆向工厂(二):静态分析技术

因程序编译过程中会丢掉函数名等符号信息,所以逆向结果是无法知道用户自定义函数名称的,故反编译的结果如下图所示,由逆向工具的自定义名称代替。

逆向工厂(二):静态分析技术

下图为用户自定义函数401362的内部,可以直观看到,其内部调用了MessageBeep和MessageBoxA两个函数。

逆向工厂(二):静态分析技术

在上述多个例子中,我们都会看到在调用函数call语句前面,通常会紧跟一个或多个push语句,push所入栈的数值即函数的参数。

MessageBeep函数:

BOOL MessageBeep(UINT uType);//该函数用来播放一个波形声音,声音类型由参数uType决定。

结合上图,可见在401362处push 0即为参数uType的值。

MessageBox函数:

Int WINAPI MessageBox(HWND hWnd,LPCTSTR lpText,LPCTSTR lpCaption,UINT uType);//显示一个模态对话框。

上图中401369-401375处便是压入的4个参数。

自定义函数401362执行到末尾时,便执行40137D处的retn进行返回,这句汇编代码即对应源码中的return语句,如果需要返回具体数值,则retn前会将返回值存入eax寄存器。有关函数调用过程中的具体问题及所涉及的堆栈平衡知识,逆向工厂将在后续动态分析章节中详细介绍。

五、代码分析示例

为了清楚地了解静态分析,下面就以一个crackme小程序作为例子进行分析,该程序主要是验证输入的序列号是否正确。

逆向工厂(二):静态分析技术

反汇编后的代码框架与函数:

逆向工厂(二):静态分析技术

逆向工厂(二):静态分析技术

由以上两图可知,start、sub_401159、sub_40120B三个函数是主要功能函数。

start函数反汇编代码及伪代码:

逆向工厂(二):静态分析技术

逆向工厂(二):静态分析技术

逆向工厂(二):静态分析技术

这个就是start函数就是windows编程里的Winmain函数,该函数定义了窗口控件、接受windows消息函数、消息循环等。其中lpfnWndProc 就是指向接受windows消息函数的指针,由反汇编代码可知,lpfnWndProc指向sub_401159。

sub_401159反汇编代码及伪代码:

逆向工厂(二):静态分析技术

逆向工厂(二):静态分析技术

该函数就是windows编程里通常定义的LRESULTCALLBACKWndProc(HWNDhwnd,UINTmessage,WPARAMwParam,LPARAMlParam) 函数了。该函数有三个消息处理过程,一个是msg==273,另一个是msg==2,还有一个缺省处理函数DefWindowProcA。当消息类型为273时,获取窗口中的输入内容,调用sub_40120B函数进行验证,根据返回结果弹出对话框进行提示;当消息类型为2时,向消息队列发送消息,该消息类型为0;缺省函数 DefWindowProcA为应用程序没有处理的任何窗口消息提供缺省的处理,该函数确保每一个消息得到处理。

下面看一下序列号验证函数sub_40120B的反汇编代码及伪代码:

逆向工厂(二):静态分析技术

逆向工厂(二):静态分析技术

逆向工厂(二):静态分析技术

由代码看出,该校验函数中没有明存的字符串,对输入的字符串进行处理后才进行比较验证的。字符串的处理过程:

汇编代码:

loc_401221: //eax存储输入的字符串地址,edx初始为0
mov bl, [eax]//拷贝一个字符串到bl寄存器
rol ebx, 8//将ebx中的值左移八位
add edx, ebx //将ebx中加到edx上
inc eax //eax自加1
cmp byte ptr [eax], 0 //判断是否还有字符
jnz short loc_401221 //如果有,继续循环

这段代码的伪代码如下:

v1 = String;
if ( !String[0] )
goto LABEL_16;
v2 = 0;
v3 = 0;
do
{
LOBYTE(v2) = *v1;
v2 = __ROL4__(v2, 8);
v3 += v2;
++v1;
}
while ( *v1 );

字符串处理完成后调用wsprintf函数将字符串处理结果按十六进制输出到缓冲区,该函数原型wsprintf(缓冲区,格式,要格式化的值),反汇编代码:

push edx //要格式化的值
push offset aLx ; "%lX" //格式,十六进制长整型
push offset byte_4020BF ; LPSTR //缓冲区
call wsprintfA //调用wsprintf
mov ebx, offset byte_4020BF //将缓冲区地址给ebx

edx寄存器长32位,以十六进制格式化后,结果长度为8。代码中的字符串为38h、44h、43h、41h、46h、33h、36h、38h,即“ 8DCAF368”。将输出的缓冲区的字符串与“8DCAF368”比较,相同返回1,不同返回0。至此,程序分析结束。

破解这个程序较为简单,以两种破解方式为例:

1、根据代码处理字符串的过程以及要对比的字符串内容(8DCAF368),生成相应的输入字符串。

2、sub_40120B函数在处理完成后,对eax寄存器赋值后作为函数的返回值:

逆向工厂(二):静态分析技术

输入正确时eax置1,输入错误时eax置0。将分支loc_40127D中的xor eax,eax修改为

mov eax,1,使得sub_40120B返回值恒为1,这样无论输入什么内容,都会显示正确弹框。

写在最后

本章逆向工厂重点讲述静态逆向分析技术,程序代码结构,常用的流程控制语句的反编译形态,并且结合IDA分析CM程序执行流程。童鞋们似乎发现借用IDA等静态分析工具,可以从宏观上清晰掌握程序架构,快速分析程序逻辑,但是如果目标程序过大或者逻辑结构十分复杂时,就很难通过静态分析来梳理其流程,或者当程序采用一些保护技术对代码数据进行加密或混淆后,静态分析工具就无法展现程序的真实面貌,而此时就需要借用另一项技术——动态分析技术。相关内容敬请关注逆向工厂,如果您有什么好的建议或意见,欢迎留言交流。

* 本文原创作者:追影人, 本文属FreeBuf原创奖励计划,未经许可禁止转载

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址