从2019年7月开始,我来到了一个陌生的专业——网络空间安全。初入安全领域,是非常痛苦和难受的,要学的东西太多、涉及面太广,但好在自己通过分享100篇“网络安全自学”系列文章,艰难前行着。感恩这一年相识、相知、相趣的安全大佬和朋友们,如果写得不好或不足之处,还请大家海涵!
接下来我将开启新的安全系列,叫“安全攻防进阶篇”,也是免费的100篇文章,作者将更加深入的去研究恶意样本分析、逆向分析、内网渗透、网络攻防实战等,也将通过在线笔记和实践操作的形式分享与博友们学习,希望能与您一起进步,加油~
- 推荐前文:网络安全自学篇系列-100篇
前文作者讲解了OllyDbg和Cheat Engine工具逆向分析用法,完成植物大战僵尸的游戏辅助器,包括修改阳光值和自动拾取阳光两个功能。这篇文章将继续带领大家来学习科锐钱林松老师在华中科技大学的分享视频,详细讲解条件语句和循环语句源码还原及流程控制逆向。流程控制逆向的具体任务包括:
- 分支结构的分析要点
if-else - 循环结构的分析要点
do-while、while、for
话不多说,让我们开始新的征程吧!您的点赞、评论、收藏将是对我最大的支持,感恩安全路上一路前行,如果有写得不好或侵权的地方,可以联系我删除。基础性文章,希望对您有所帮助,作者目的是与安全人共同进步,也强烈推荐大家去看看钱老师的视频,加油~
作者的github资源:
软件安全:https://github.com/eastmountyxz/Software-Security-Course
其他工具:https://github.com/eastmountyxz/NetworkSecuritySelf-study
Windows-Hacker:https://github.com/eastmountyxz/Windows-Hacker-Exp
声明:本人坚决反对利用教学方法进行犯罪的行为,一切犯罪行为必将受到严惩,绿色网络需要我们共同维护,更推荐大家了解它们背后的原理,更好地进行防护。(参考文献见后)
前文回顾:
[安全攻防进阶篇] 一.什么是逆向分析、逆向分析应用及经典扫雷游戏逆向 (1)
[安全攻防进阶篇] 二.如何学好逆向分析、逆向路线推荐及吕布传游戏逆向案例 (2)
[安全攻防进阶篇] 三.OllyDbg和Cheat Engine工具逆向分析植物大战僵尸游戏 (3)
一.C++逆向条件结构基础入门
大家写过相关的算法吗?
加密代码中会涉及循环和分支,你要识别算法,首先就是需要将它的算法处理流程识别出来。当我们还原出等价的高级代码之后,就没有逆向分析人员的事情了,因为接下来涉及到密码学、数学相关人员的工作,逆向人员把加密的代码还原出来后就应该扔给研究密码学的数学家,他们负责玩数学对抗,而逆向关注的是编译原理和代码还原。同时,逆向还涵盖了识别对象、识别算法、识别优化、识别虚函数对象的继承关系等等,这里主要结合项目相关的加密和解密进行讲解。接着作者准备穿插着VC++6.0和VS2019两个版本进行讲解。
1.单分支结构分析
第一步,通过VC++6.0编写一个最简单的程序,创建工程名称为“RE_SF”。
运行结果如下图所示,可以看到“Hello World”。
第二步,编写单分支结构的相关代码。
#include "stdafx.h"
int main(int argc, char* argv[])
{
if (argc > 8 )
{
printf("argc > 8\r\n");
}
return 0;
}
接着选择“Win32 Release”编译运行代码。
第三步,接着用OD软件打开EXE文件。
此时创建的工程目录分布如下图所示。
OllyDbg打开之后显示如下图所示的界面,程序入口地址是0x00401051。
- 程序入口:0x00401051
第四步,首先我们需要定位main函数,其方法非常简单,找到有三个PUSH的下面就是main函数,因为main函数有三个参数(argc、argv[],、envp[])。接着按F2下断点。
- 主函数:0x00401100
继续按下F7跟进,会看到一个单分支结构。那么,单分支结构它有什么特点呢?
- 进入主函数:0x00401000
第五步,先给大家普及单分支语句的代码定式基础知识。
在高级语言中单分支代码定式如下:
//程序语言
if(...)
{
...
}
//代码定式
IF_BEGIN:
jxx IF_END
...
IF_END:
...
为什么需要记住这个代码定式呢?
因为对于流程控制的识别,我们关键是要找到IF语句的作用域(上界和下界),上界在jxx的位置,称之为IF_BEGIN。接着有个jxx条件跳转,跳转到目标且没有其他的特征,这种就称之为单分支的代码定式。
回到我们的汇编代码,拿到这个代码之后,发现存在一个箭头指向跳转目标,这样就出现了IF模块的上界和下界,条件判断作为IF的上界,条件跳转的目标作为IF下界,通过这种套路方式来还原代码。
第六步,分析嵌套的单分支语句。
假设我们的判断中再嵌套一层或增加一个分支,又该怎么判断呢?对于我们还原代码的人来说,不用管它,你把上下界圈出来,然后递归解决。所有流程问题只要找到上下界,剩下的问题就变成了顺序结构,再看着代码一条条还原即可。
#include "stdafx.h"
#include <stdlib.h>
int main(int argc, char* argv[])
{
if (argc > 8 )
{
printf("argc > 8\r\n");
if (argc < 80)
{
printf("argc < 80\r\n");
}
}
system("pause");
return 0;
}
接着运行程序生成新的EXE程序,然后通过OD软件打开分析,给主函数下个断点然后进入主函数显示如下图所示的界面。
- 主函数内容:0x00401000
第七步,同样的方法将两层单分支语句的上下界圈出来。
我们发现这两个判断的下界重合了,都是跳转到0x00401029位置,这就明显是个嵌套。
总结下IF语句的特点:
- 观察它的条件跳(上下界)
- 条件跳的目标上面的代码没有其他特征,即“ADD ESP, 4"
那么,怎么还原出高级代码呢?
第八步,通过汇编代码还原出高级代码。
还原代码需要进行反条节操作,并且学会查询相关指令。比如JLE、JGE是什么意思呢?
- JLE(jump if less or equal,or not greater):汇编语言中的条件转移指令,小于或等于则条件转移
- JGE:大于或等于转移指令
注意,在还原的时候需要做反条件操作。那么,什么叫反条件呢?具体解释如下:
- JLE:小于等于跳转 --> 代码还原就是“不小于等于”,即:大于跳转
- JGE:大于等于跳转 --> 代码还原就是“不大于等于”,即:小于跳转
反条件
因为当我们满足这个条件的时候它会跳转到另一个地方(结束地方),它没有执行具体的代码;所以如果我们想要执行模块中的代码,就需要反条件处理。即汇编的语义和高级语言的语义是反的,高级语言的语义是满足条件则执行语句块,而汇编的语义是满足条件不执行语句块。
接着我们继续看触发跳转的代码,它是通过CMP比较来触发的。
- CMP ESI, 8
ESI是通过参数传递过来的,然后和8进行不小于等于的比较 - CMP ESI, 50
ESI和50进行不大于等于的比较
此时,我们再将单分支步骤简单归纳如下:
- (1) 通过反汇编代码序列,匹配代码定式;
- (2) 如果是单分支if结果,则将条件转义指令jxx做反条件还原
第九步,接着我们换个工具用VS2019打开我们的代码,生成新的Release版本。
然后删除本地的Release资源,生成一个新的Release方案。
第十步,用IDA Pro打开可执行EXE程序进行分析。
同样,使用IDA也是可以进行逆向分析的,打开新生成的逆向分析工具如下所示。
右键选择“Text View”查看源代码。
找到main函数,然后点击“_main”位置高亮显示。
按下“N”键可以对函数进行重命名,如下图所示。
注意,前面分享的识别方法和编译器版本、编程语言(C++、VB)等都没有关系,它是编译原理的问题。接着我们重点还是回归到代码上去,点击“loc_401039”函数高亮,同样的方法划分出这个单分支的上界和下界,并且嵌套了一个单分支,最终还原出源代码。
所以,不论使用VC++6.0或VS编译工具,还是使用IDA或OD分析工具,它们还原代码的原理及方法都是一样的。在实际项目中,不论你用什么分析工具,最终能分析出结果就好。
2.双分支结构分析
第一步,编写双分支代码。
第二步,普及双分支语句的代码定式基础知识。
在高级语言中双分支代码定式如下。该代码序列关键是发现jxx后,需要检查目标看看下面有没有一个jmp指令,如果有个jmp且是往下跳的,if-else就成立了。
//程序语言
if(...)
{
...
}
else
{
...
}
//代码定式
IF_BEGIN:
jxx ELSE_BEGIN
...
IF_END:
jmp ELSE_END
ELSE_BEGIN:
...
ELSE_END:
...
第三步,接着生成新的exe文件,用OD打开分析。
同样的方法进入主函数,然后F7单步步入0x00401000位置,如下图所示。
核心代码及其跳转如下图所示。
- JLE --> 0x0040100E:PUSH操作
- JMP --> 0x00401013:CALL操作
双分支结构特点:
- jxx的目标处上一行指令为jmp,而且是往高地址去的jmp(往下跳)。如果是循环,后面会讲到它可能往上跳。
确定上下界之后,生成如下图所示的if模块和else模块,同样的反条件处理还原代码。
注意,这里有一个小小的优化,编译原理中的代码外提。它是什么意思呢?
假设有个节点A,现在有了流程分支B1和B2,B1完成后执行C,B2完成后也会执行C。编译器为了减小代码的节点,因为代码节点越多,代码越长,就做了等价流程的代码外提优化,从而汇总到C,少了一个节点。
- 编译器会视情况减少节点的优化
- 编译器也会增加节点来减小路径的优化
第四步,采用同样的方法用IDA工具分析还原代码,其效果也一样。
接着你可能会疑问这两个PUSH是干啥呢?**
- push offset aArgc8 ; “argc > 8\r\n”
- push offset aArgc8_0 ; “argc <= 8\r\n”
C语言中没有标准的高级语法对应汇编中的PUSH操作,说明它有代码优化了,就是代码外提操作。它们有个公共的函数调用被提到下面去了,就是下图所示的两行代码,这个时候我们要把它放回去方便还原。
接着我们复制汇编代码至C语言中进行还原,方便大家理解。代码如下,此时增加了if和else的上下界,但发现两个push无法还原。
同时将代码外提部分分别放到if和else模块中,就能实现最终代码还原,如下图所示。最后删除掉多余的汇编注释即可。
继续还原条件判断内容,JLE小于等于换成大于8就好。在真实环境中,还会遇到双分支中有循环或条件嵌套的问题,不要担心,找到上下界继续分析即可。
#include "stdafx.h"
#include <stdlib.h>
int main(int argc, char* argv[])
{
//.text:00401000 cmp [esp+argc], 8
//.text:00401005 jle short loc_40100E
if (argc > 8)
{
//.text:00401007 push offset aArgc8 ; "argc > 8\r\n"
//.text:0040100C jmp short loc_401013
//.text:00401013 call sub_4010C6
printf("argc > 8\r\n");
//.text:00401018 add esp, 4
}
//.text:0040100E ; ---------------------------------------------------------------------------
//.text:0040100E
else
{
//.text:0040100E loc_40100E: ; CODE XREF: _main+5↑j
//.text:0040100E push offset aArgc8_0 ; "argc <= 8\r\n"
//.text:00401013 call sub_4010C6
printf("argc <= 8\r\n");
//.text:00401018 add esp, 4
}
//.text:00401013
//.text:00401013 loc_401013: ; CODE XREF: _main+C↑j
//.text:0040101B push offset aPause ; "pause"
system("pause");
return 0;
}
二.C++逆向循环结构基础入门
1.do-while结构分析
循环包括do-while、while和for三种,你会觉得哪一种消息最高呢?do-while是三种循环中效率最高的,由于其无条件先执行一次,所以大家很少使用,但其效率很高。
基本语法
先执行,再判断。先执行一遍循环操作,若符合条件,循环操作继续执行,否则退出循环。
do{
循环操作语句;
}whlie(循环条件);
第一步,我们编写一个1加到100的循环代码,这次直接使用Debug版本。
#include "stdafx.h"
#include <stdlib.h>
int main(int argc, char* argv[])
{
int n = 1;
int nSum = 0;
//do-while 执行一次
do {
nSum = nSum + n;
n++;
} while(n <= 100);
printf("%d", nSum);
system("pause");
return 0;
}
第二步,通过OD打开运行的EXE程序“RE_XH.exe”。
- 程序入口地址:0x00401260
第三步,往下查找代码,发现3个PUSH后(参数)就是主函数,然后F2添加断点并F7步入主函数。
- 主函数:CALL RE_XH.00401005
第四步,分析汇编代码。
这里存在一个JLE跳转,如果条件跳往上跳就是do-while循环。
循环肯定会往上走,否则构成不了循环,它需要反复执行同一代码段。如果跳转的目标没有检查条件,就是do-while循环。简单总结下识别do-while循环步骤:
- 识别代码定式
- 如果是do循环,则按jxx同条件还原等价高级代码
注意,同条件的就只有do-while结构。在do-while循环中,它跟汇编的语义是一样的,只有当条件满足则流程更新到循环的起始地点,所以它是正条件还原。而前面的if-else判断都是反条件。
//程序语言
do
{
...
}while(xxx);
//代码定式
DO_BEGIN:
...
jxx DO_BEGIN
DO_END:
...
2.while结构分析
基本语法
先判断,再执行。
whlie(循环条件){
循环操作语句;
}
第一步,我们编写一个1加到100的循环代码。
#include "stdafx.h"
#include <stdlib.h>
int main(int argc, char* argv[])
{
int n = 1;
int nSum = 0;
while (n <= 100)
{
nSum = nSum + n;
n++;
}
printf("%d", nSum);
system("pause");
return 0;
}
第二步,分析while循环的代码定式。
注意,该循环的Debug版本和Release版本存在差异,接下来会对比分析。我们先给出代码定式,如下所示。
//程序语言
while(xxx)
{
...
}
//代码定式
WHILE_BEGIN:
jxx WHILE_END
...
jmp WHILE_BEGIN
WHILE_END:
...
while循环的条件跳是往上跳的,它需要反复执行同一代码段。
第三步,通过OD打开运行的EXE程序“RE_XH.exe”。
- 程序入口地址:0x00401260
第四步,往下查找代码,发现3个PUSH后(参数)就是主函数,然后F2添加断点并F7步入主函数。
- 主函数:CALL RE_XH.00401005
第五步,分析汇编代码。
这里存在一个JG跳转,它有点像if语句,下面还有一个JMP,有点像if-else指令,但是它的跳转是地址减量跳或往上跳,所以它是循环。
这时会发现while循环比刚才的多了一个跳转。我们会过计算机组成原理,当处理器执行跳转指令时,流水线会暂时挂起失效,本来流水线在取指令时已经准备预读后面的代码了,结果在译码过程中是个跳转,后面的代码预读就会出错,然后做流水线清理工作。所以,while循环有两跳对流水线的伤害比do-while大。
第六步,接着我们用高版本VS2019编译一个Release版本,并用IDA进行分析,看看高版本有什么优化。
查看Text View,我们定位到main函数之后,看看它做的优化。它把循环的起点对齐到十六进制10的倍数地址,中间会用nop进行空指令填充。同时,它的汇编循环体变成了do-while循环。
注意,前面的VC++ Debug版本用IDA工具打开如下图所示。上图和下图同样都是while循环,但低版本可以看到JG(往下跳)和JMP(往上跳)两个跳转,典型的while循环;而高版本的却修改成了do-while循环的形式。
问题1:由于do-while循环会执行一次循环体,难道它不担心编译器出错吗?
其实它比较的数值是常量,常量可以在编译期间预置其结果的,其实编译器在第一次的判断时先进行了一次常量传播,令n等于1,即判断的是 while(1<=100),比较1和100的关系条件必成立。
问题2:那么,如果将100替换成变量,编译器还能识别吗?或者会报错?
此时的编译器会将其进行转换,变成如下图所示的形式再执行do-while循环。其中if(n<=argc)条件判断嵌套一个循环。
对应的汇编代码如下,其中“jl short loc_401016”往下跳,还原成一个单分支,循环里面还有一个跳转“jle short loc_40100F”往上跳,满足do-while循环,最终还原成if加do-while,或者你知道有这个优化,直接还原成带变量的while循环也可以。
但需要注意,能不能把do-while直接还原成while循环,还需要看看这两个条件有没有相关性。如果有相关性才能还原,比如外层判断是文件的打开状态,while是迭代n值,这种情况不能还原。下图可以看到if和循环都是EAX参数比较,所以具有相关性。
3.for结构分析
下面开始分析for循环结构。
for(表达式1;表达式2;表达式3;)
{
语句;
}
第一步,我们编写一个for循环代码。
#include "stdafx.h"
#include <stdlib.h>
int main(int argc, char* argv[])
{
int nSum = 0;
for (int n = 1; n<=argc; n++)
{
nSum = nSum + n;
}
printf("%d", nSum);
system("pause");
return 0;
}
第二步,编译生成新的Debug版可执行程序。
第三步,通过IDA打开运行的EXE程序“RE_XH.exe”。
产出了三个跳转代码,如下图所示。
其代码定式如下所示,可以看到JMP、JG和JMP三个跳转。注意,for循环中FOR_STEP地址是低于BODY执行体的地址的。
第四步,分析汇编代码。
首先MOV进行初始化赋值1,接着JMP跳转到比较部分,比较不成立则JG直接跳出循环,否则执行循环体BODY内容,接着继续JMP跳转上去执行n++操作。
注意,由于Release版本都被编译器优化成了do-while循环,所以我们需要在Debug版下进行对比。
第五步,通过VS2019生成Release版本,然后用IDA打开代码对比。
IDA打开如下图所示,发现和do-while一样,高版本做了一点小处理,每次循环总次数增加了4(add eax,4),从而提升效率。
三.总结
写到这里,这篇文章就介绍完毕,希望对您有所帮助,最后进行简单的总结下。
- 条件语句逆向分析
- 循环语句逆向分析
学安全一年,认识了很多安全大佬和朋友,希望大家一起进步。这篇文章中如果存在一些不足,还请海涵。作者作为网络安全初学者的慢慢成长路吧!希望未来能更透彻撰写相关文章。同时非常感谢参考文献中的安全大佬们的文章分享,深知自己很菜,得努力前行。
很多朋友问我如何学逆向分析?
下面给出推荐的学习路线和安全书籍。软件逆行其实就是搬砖活,你需要的是任性和基本功。可能大佬们会有很多技巧,但我希望你能扎扎实实去躺过那些坑,会看懂代码,会写代码,然后IDA和OD工具(倚天屠龙)用好,每天泡在代码中,肯定能行的。你应该这样学习:
1.多敲代码,重视实战;
2.程序不是写出来的,是调出来的;
3.根据自己兴趣和市场需求做一定规模的项目。
下图开发和逆向项目非常推荐你去完成,开发远控软件有助于你分析木马,CAD软件能提升你C++分析能力,做一个调制器或许反调试就不再那么难,自制一个小操作系统、小编译器、任务管理器,或许逆向原理就懂了。
最后给出了这一年我在网络安全、系统安全和机器学习看过的书,如果你想把AI更好的融入安全领域,看看这些书籍还是挺不错,我也厚着脸皮把自己写的两本Python数据分析书放了进来,哈哈~
网络安全:
系统安全:
AI+安全:
编程没有捷径,逆向也没有捷径,它们都是搬砖活,少琢磨技巧,干就对了。什么时候你把攻击对手按在地上摩擦,你就赢了,也会慢慢形成了自己的安全经验和技巧。加油吧,少年希望这个路线对你有所帮助,共勉。
参考资料:
真心推荐大家好好看看这些视频和文章,感恩这些大佬!前非常推荐钱老师的视频,感谢华科UP主。
[1] 科锐逆向的钱林松老师受华中科技大学邀请- “逆向分析计算引导”
[2] C语言逆向工程之游戏辅助开发 - C语言Plus
来源:oschina
链接:https://my.oschina.net/u/4266968/blog/4462223