简介:这份PDF文档面向逆向工程初学者和网络安全入门读者,围绕IDA Pro反汇编工具的基础认知与EXE逆向解密实战展开,适合希望建立静态分析入门框架、了解简单样本分析流程的学习者。资源以1个PDF文件交付,约5.5MB,内容覆盖IDA Pro简介、工程新建、IDA View与Hex View、Strings窗口、文件类型识别,以及代码加密和逆向解密案例,并穿插Windows PE基础与CrackMe样本的分析思路。已有1662人学习,说明其在入门工具笔记与实战案例之间具有一定参考价值。读者可从中熟悉IDA Pro常用窗口的观察方式,了解PE文件载入后的代码、数据与字符串线索,理解代码与数据分离、函数标记等基础概念,并借助一元一次方程类CrackMe案例体会简单逆向解密的推理路径,为后续工具进阶和样本分析练习打下基础。内容以图文笔记形式组织,章节由工具认识过渡到案例实战,便于按主题检索和复盘。
1. 三分钟定位到 key,一个下午卡在载入框
一个二十来行的 C 程序,加密逻辑就一句异或,我用 IDA Pro 从打开到看清密钥,前后不到三分钟;但在此之前,我在载入对话框、窗口布局,还有一个叫gcc2_compiled_的怪变量名上耗了整整一个下午。反汇编工具真正的门槛不在“按下 F5”,而在于你得知道 F5 出来的那一屏东西该往哪儿对、对不上时该回退到哪个窗口找证据。这篇就按我自己摸出来的顺序走一遍:怎么把一份 EXE 干净地载入成数据库、几个核心窗口各自解决什么问题、一个异或加密样本怎么被完整逆推回 key,以及新手最容易在哪几个地方原地打转。刚装好 IDA Pro、想正经做一次逆向工程复现的人,照着顺序做一遍就有手感;已经能跑通流程的,可以只看第 4、5 章的判断依据和踩坑记录。
2. 载入一份样本:从双击图标到 idb 数据库落地
拿到一个陌生的 EXE,最稳的做法不是急着按 F5,而是先把“载入”这一步做干净。载入阶段决定了两件事:IDA 用什么处理器架构去解释这段机器码,以及它把哪些区段当作代码、哪些当作数据。这一步错了,后面所有的交叉引用、函数识别、反编译都是建立在沙子上的。我一般会把这个阶段拆成四件事:选对位数、走完首次载入流程、看懂区段列表、搞明白数据库文件到底存了什么。
2.1 32 位还是 64 位:装完之后先做的一次选择
安装完成后桌面上会出现两个入口,分别对应 32 位和 64 位程序的分析。很多人第一次就选错,然后对着报错框发呆。判断依据很简单:用系统自带的文件属性看,或者直接拿 32 位版本去开,开不动就说明是 64 位样本。32 位版本打不开 64 位 PE,会直接告诉你文件格式不被识别;反过来,64 位版本能打开 32 位程序,但反汇编出来的地址宽度、调用约定、部分库函数识别都会和你预期的不一样,新手很容易在这里把call的参数个数数错。
我自己的习惯是:样本位数未知时先用 32 位试,报错再换 64 位,绝不凭“看着像老程序”去猜。演示样本是一个用最普通的 C 编译器生成的 32 位控制台程序,所以后面所有截图式的描述都基于 32 位入口。如果你手上的样本是近几年编译的,八成是 64 位,别在这一步浪费十分钟。
2.2 首次载入的八个动作,一个都别跳
第一次打开会弹一个支持信息框,点确认即可,这个框没什么信息量。接下来是三种入口:新建打开一个标准文件、开一个空白工作区再把文件拖进去、以及从最近使用记录里选。新手建议走第一种,路径清晰,不会有“拖进去之后格式识别错了”的尴尬。
选完文件会进入装载配置窗口,这里默认已经按 PE 格式识别好了,直接确认。随后 IDA 会做两阶段处理:先把代码和数据分开,标记函数、分析参数调用与跳转关系;再尝试识别编译器类型,装载对应的特征文件给函数赋名。这个过程在大样本上可能几十秒,别以为卡死了。接着会弹更新检查之类的提示框,点确认走默认就行,不影响分析。
进到主界面之后,第八个动作是保存:关闭时选择保存数据库,默认选项直接确认。很多人嫌这一步多余,结果第二天打开发现改的名字、写的注释全没了,等于白干。养成“分析告一段落就存一次”的习惯,比事后补要省事得多。
2.3 装载窗口里的五个区段,看懂了才知道程序长什么样
装载配置里列出的那几个区段,是后面所有定位工作的地图:
| 区段 | 含义 | 逆向时用来干什么 |
|---|---|---|
| text | 代码块 | 主逻辑、判断分支、加密循环都在这里 |
| data | 数据块 | 已初始化的全局变量、硬编码常量 |
| rsrc | 资源块 | 图标、对话框模板、部分字符串资源 |
| idata | 输入表 | 程序调用了哪些外部函数,是找关键 API 的入口 |
| edata | 输出表 | 动态库导出函数,做 DLL 分析才用得上 |
新手最常忽略的是 idata。想快速判断一个程序干了什么,先看它导入了哪些函数:出现字符串比较、出现输入函数、出现文件读写,基本就能勾勒出行为轮廓。反过来,如果一个程序几乎不导入任何敏感 API,那它多半是自己实现了相关逻辑,或者加了壳,这两条路后面的分析方式完全不同。区段列表不是装饰,它是你决定“从字符串切入还是从导入表切入”的依据。
2.4 id0、id1、nam、til:数据库四件套到底存了什么
分析完成后本地会生成一组文件,主库加上四个附属文件。它们各自负责一块:一个是二叉树形式的数据库主体,一个是程序字节标识,一个存命名窗口的索引信息,最后一个存本地类型定义。你在界面里做的重命名、加注释、定义结构体、标记数组,全部落在这些文件里,而不是改动了原始 EXE。
这带来一个很实用的结论:下次要继续分析,直接加载数据库文件,而不是重新载入 EXE。重新载入等于从零开始,之前的命名和注释全部作废。我吃过一次亏,改了一晚上的函数名,第二天习惯性双击了 EXE,看到满屏的sub_401000,那一刻的滋味不太好受。从那以后我的目录里永远只有一种入口:先找 idb,找不到再开 EXE。
3. 三个窗口撑起半个逆向现场:IDA View、Hex View、Strings
界面上一堆窗口,真正高频使用的就三个,剩下的是按需调用。判断一个新手有没有入门,看他会不会在这三个窗口之间来回跳:在 Strings 里看到一句提示语,双击跳到 IDA View 的引用位置,怀疑某个常量被改过再去 Hex View 核对原始字节。这条链路走顺了,分析效率会有明显提升。
3.1 IDA View 的三段结构:地址区、OpCode 区、代码区
IDA View 从左到右大致分三段。最左边是地址区,以 PE 文件加载到内存后的虚地址为准,是镜像基址加偏移,常见起点在这个量级;中间是操作码区,默认是隐藏的,需要在选项里的通用设置中把操作码字节数改成 8 才会显示;右边是反汇编代码区,也是主工作区,能高亮,双击函数名或变量名会跳到对应地址。
操作码区为什么要手动开?因为它在排查“ida 软件/插件 识别错误”这类问题时几乎是唯一的证据。当 IDA 把一段数据误判成指令,或者把一条指令的长度算错,你只有看到原始字节才能判断到底是它错了还是你理解错了。我一般会在分析卡住时打开它,对照字节和指令,一眼就能看出是不是解析跑偏。
代码区还有几个必记的操作:给函数或变量改名、给某一行加注释、以及查看交叉引用。改名这件事看着琐碎,实际上是逆向里性价比最高的投入——你给一个sub_401100改成“校验函数”,后面每次跳过来都能秒懂,省下的是反复读汇编的时间。
3.2 Text View 和 Graph View:什么时候该切
IDA View 有两种浏览模式,一种是纯文本的指令流,一种是把条件分支画成方块的图形视图,右键可以互相跳转。我的用法很固定:拿到新函数先看图形视图,方块一多一分叉,控制流立刻清楚;确定要看某条具体指令、某个寄存器的值时,切回文本视图,因为图形视图在密集跳转时会把你绕晕。
图形视图左下角有整个函数的缩略图,函数大了以后靠它快速定位到某个方块。每个方块上方会标注它的起始地址和来源,判断“这个分支是从哪个条件跳过来的”非常直观。第 4 章的实战里,正是靠图形视图一眼看出“长度合法”和“长度非法”两条岔路,才没有在文本流里来回翻页。
3.3 Hex View 与 F2:只读是保护,不是限制
十六进制窗口默认只读,这是防止你手滑改坏字节。按 F2 可以在只读和编辑之间切换,进入编辑态后光标所在的字节区域可以修改。这个功能在补丁式分析里很有用:把一个条件跳转的指令字节改掉,观察程序行为如何变化。
但有两件事必须提醒。第一,你在 Hex View 里的修改只存在于数据库里,不写回原文件,想让补丁生效得走“把修改应用到输入文件”的流程,而且务必在副本上做,原样本留一份干净的对照。第二,改字节前先确认当前是不是在正确的段里,有时候光标停在只读段上,F2 按了没反应,不是软件坏了,是这一段本来就不允许写。这类小坑在第 5 章还会展开说。
3.4 Strings 窗口:从一句话反推一段代码的最短路径
字符串窗口列出的东西很朴素:地址加字符串。但它是逆向里最短的一条捷径。程序只要给用户看过一句话——提示语、报错信息、成功提示——这句话就必然以某种形式存在于文件里,顺着它往上找引用,几乎总能落到关键分支上。
打开方式在视图菜单的子窗口列表里,点开之后可以按长度、按编码过滤。找到目标字符串后双击,会跳到它被引用的那条指令附近,再按一次交叉引用就能看到完整调用点。要注意两点:一是编码,宽字符字符串在默认过滤下可能不显示,需要切换编码;二是死字符串,有些字符串被编译进去但从未被引用,跳过去发现周围全是数据,那就换一个关键词再试。
3.5 导入表、函数窗口、段窗口:按什么顺序看
几个辅助窗口的调用顺序,我总结成一条固定路径:先看函数窗口找到主函数,再看导入表确认程序用了哪些敏感 API,再看段窗口确认.text的起止范围,最后用命名窗口核对有没有遗漏的符号。
函数窗口列出程序调用的所有函数,包括名称、区域、起始位置、长度和标记,主函数通常排在最前面之一,直接进去就能看整体骨架。导入表在判断行为时最有效,一个只调了输入函数和字符串比较的程序,逻辑必然围绕“读入—变换—比较”展开,这个判断能帮你省掉大段无关代码。段窗口看起来最没用,但它能告诉你哪些地址是可执行代码、哪些是数据,避免你对着一段数据硬按 F5。
4. 复现:一个异或加密样本,从字符串到 key 的完整链路
演示样本是自己写的一段 C 程序,逻辑是把用户输入和一段内置密钥按字节异或,结果与一个固定字符串比较,相等就打印成功。听起来简单,但它包含了逆向入门要练的全部动作:找字符串、看控制流、读伪代码、定位常量、反推输入。这一段我按实际操作顺序写,每一步都说清楚“为什么这么走”。
4.1 三条提示语就是三张路标
程序只说了三句话:要求输入密钥、长度不合法时的报错、以及成功提示。这三句话分别对应三段代码:输入点、长度校验、结果比较。最省力的入口是成功提示,因为“成功”这件事在整个程序里必然只有一个出口,从它往上找,一步就能跳到比较语句。
在字符串窗口里定位到成功提示,双击跳到引用位置,会发现它处在一个条件跳转之后。图形视图里这个方块只有两个前驱:一个是比较结果为真直接落下,一个是跳转过来。到这里,程序的主干已经被压缩成一个很小的范围,剩下的工作只是读懂被比较的两个东西分别是什么。
4.2 图形视图定位分支:长度判断和比较的两条岔路
按下反编译快捷键之后,伪代码大致长这样(变量名是工具自动生成的,我保留了原始形态):
int __cdecl main(int argc, const char **argv, const char **envp) { char buf[32]; // 用户输入 char out[40]; // 异或后的结果 int len; // 输入长度 int i; __main(); printf("please input the key:"); scanf("%s", buf); len = strlen(buf); if ( len > 5 && len <= 10 ) // 长度必须落在 6~10 { for ( i = 0; i < len; ++i ) out[i] = gcc2_compiled_[i] ^ buf[i]; // 逐字节异或 if ( !strcmp(out, "123456789") ) // 与固定串比较 printf("You are right, Success.\n"); else printf("Error, please input the right key.\n"); } else { printf("Error, The length of the key is 6~10\n"); } return 0; }这段伪代码里有两个判断点需要留意。第一个是长度条件,编译器的表达方式和源码不一定一致,源码里写的是“小于 6 或大于 10 报错”,反编译出来变成“大于 5 且小于等于 10 才继续”,逻辑等价但读起来别扭,新手容易在这里怀疑自己找错了函数。第二个是比较点,strcmp的第一个参数是异或结果,第二个参数是硬编码字符串,长度九位,说明输入也必须是九位——这就是为什么长度条件卡在 6 到 10 之间,因为最终比较的那串正好是九位。
参数含义上,buf是栈上缓冲区、out是另一块缓冲区、len存长度、i是循环计数。这些名字都是工具给的,无所谓,关键是认清三者关系:输入进buf,逐字节和密钥异或写到out,out再参与比较。只要这个数据流看明白了,后面的推导就是纯算术。
4.3gcc2_compiled_这个变量名:不认识就双击它
伪代码里最奇怪的东西就是那个名字。它不像变量,也不像函数,看起来像工具自己造的词。原因很简单:这是一个位于数据段、没有被识别出明确类型的常量数组,反汇编器给它起了个占位名。遇到这种名字不要绕开,双击它,让视图跳到实际地址。
跳过去之后看到的是数据段里的一段字节。对这段字节按转字符串的快捷键,或者切到十六进制窗口按 ASCII 读,就能拿到明文。演示样本里这一段是九个可打印字符,正好和比较串的九位对上。到这里,密钥和密文都齐了,缺的只是推导方向。
顺便说一个判断技巧:如果跳过去看到的是无法阅读的乱码字节,说明这段常量被压过或者加密过,那就得回去找解密它的那段代码,而不是在这里硬啃。区分“可打印常量”和“加密常量”,是我认为逆向里最需要早点建立起来的直觉之一。
4.4 写脚本反推:已知密钥和密文,求输入
异或的可逆性让这题不需要任何爆破。设密钥为 K、输入为 P、密文为 C,程序做的是P ^ K = C,那么P = C ^ K。用 Python 把两个字符串对齐按字节异或即可:
# 已知:程序内置密钥 key,比较目标 cipher # 目标:反推用户应该输入什么 key = "revstudyx" # 从数据段里读出来的常量 cipher = "123456789" # 伪代码里 strcmp 的第二个参数 assert len(key) == len(cipher), "长度不一致时按较短的截断处理" plain = "".join( chr(ord(c) ^ ord(k)) # 逐字节异或,异或两次即还原 for c, k in zip(cipher, key) ) print("应输入的 key =", plain) print("长度 =", len(plain)) print("可打印 =", plain.isprintable())代码只有几行,但三个细节决定成败。zip是按位置对齐,两个串长度必须一致,不一致时按短的截断,这一点要在断言里体现出来,否则算出来的结果少了字节你还以为是程序错了。ord取的是字符编码值,如果是宽字符或者非 ASCII 常量,要换成字节序列处理,不能直接对字符做运算。最后的可打印性检查是自查手段,正规样本的正确答案通常是可打印字符串,算出一堆控制字符,八成是密钥读错了位或者方向搞反了。
跑出来的结果是一个九位大写字符串,长度落在 6 到 10 之间,符合第一道校验。把它输入原程序,成功提示如期出现,这条链路就闭环了。闭环这个动作非常重要,光算出来不算完,必须让原程序认可,否则你验证的只是自己的算术,不是样本的逻辑。
4.5 为什么这题不能靠猜:异或给了你捷径,也给了你陷阱
同样的样本,如果密钥不是明文放在数据段,而是运行时由某个函数算出来的,反向推导就不成立了,那时要做的变成跟进密钥生成函数,或者用已知明文去倒推。这两种情况的分界线在于:常量是否静态可见。静态可见就老老实实抄出来算,静态不可见就先解决“它从哪来”。
还有一种常见误区是把异或当成加密强度问题来讨论。这里它只是个教学用的变换,目的是让你练“读数据流”这件事,而不是练破译。真正值得记住的是那个判断顺序:先确认数据流方向,再确认常量位置,最后才动笔算。顺序反了,就会变成对着一堆字节试各种组合,纯靠运气。
5. 排查:五个让新手在 IDA 里卡半天的坑
下面这五条,是我自己和身边人反复踩过的。每一条都按“看到什么现象、为什么、怎么解决”写,遇到对应症状时可以直接对号入座。
5.1 载入时报文件格式无法识别
现象:选中 EXE 之后弹出提示,说文件不是可识别的可执行格式,或者干脆没有任何反应。
原因:绝大多数是位数不匹配,用 32 位入口去开 64 位样本;其次是文件本身不完整,拷贝过程中被截断;再次是样本加了壳,头部结构被改写得不像标准 PE。
解决:先换另一个位数的入口重试,一秒就能排除第一种可能。换过之后仍然报错,用十六进制工具看文件头两个字节是不是标准的可执行标记,不是就说明文件损坏,回去重新拿一份。头部正常但依然打不开,基本可以判定是加壳,这时不要硬载入,先做脱壳或者改用动态调试手段观察运行时行为,硬塞进去只会得到一屏无意义的字节。
5.2 反编译快捷键按了没反应
现象:光标停在函数里,按下反编译快捷键,界面毫无变化,或者提示当前函数无法反编译。
原因:三种可能。一是当前光标不在一个被识别为函数的区域内,比如停在数据段;二是样本架构不被反编译器支持,某些精简指令集或非主流平台就是没有反编译能力;三是安装的是不带反编译组件的版本。
解决:先看光标所在位置是不是在代码段范围内,不在就回到函数窗口双击一个明确的函数再试。确认在函数内仍然不行,就看架构,非主流架构只能读汇编,这时候把精力放在图形视图和控制流上,别死磕伪代码。最后核对安装组件,缺组件就去补,别指望换样本能绕过这个问题。
5.3 字符串窗口里搜不到刚才看到的那句话
现象:程序运行时明明打印了某句提示,字符串窗口里却怎么都搜不到,或者搜到一堆乱码。
原因:编码不同。宽字符字符串在默认过滤下不显示,需要切换编码选项;另一种情况是字符串在运行时才拼出来,静态文件里根本不存在完整形态;还有一种是字符串被简单变换过,比如每个字节加了固定偏移。
解决:先在字符串窗口里切换编码再看一遍,多数情况这一下就解决了。仍然没有,就去导入表看有没有字符串拼接类函数,如果有,说明是运行时构造,改从拼接函数往上找调用点。最后一种情况比较费劲,需要找到解密或变换的那段代码,手动还原,这时候前面讲的操作码区就派上用场了。
5.4 改的名字和注释,第二次打开全没了
现象:辛苦一晚上给几十个函数改了名、写了注释,第二天打开满屏还是自动生成的符号。
原因:没有保存数据库,或者保存了但又重新载入了原始 EXE。重命名和注释只存在于数据库文件里,原始样本一个字节都没变。
解决:关程序时走正常保存流程,别直接结束进程;下次打开时选数据库文件,不要选 EXE。另外建议把数据库和样本放在同一个目录、用相近的文件名,减少“找不到上次那份”的概率。这个坑没有任何技术含量,但它的破坏力最大,因为它吞掉的是纯人力投入。
5.5 双击常量跳过去,看到的是一片零
现象:伪代码里某个变量看着像密钥,双击跳过去,数据段里全是零字节,或者只有几个看不出规律的字节。
原因:这个变量在未初始化数据段,程序运行时才会被赋值;也可能它根本不是数据,而是一个地址,你跳到了它指向的位置而不是它本身。
解决:先看它所在的段属性,未初始化段里的东西静态看不到值,只能跟到写入它的代码处去看。判断是不是地址,看它的值和代码段范围是否接近,接近就按指针处理,再跳一层。这两个判断不需要任何插件,看段窗口和地址范围就够。很多人在这里误以为是加壳,绕了一大圈又绕回来。
6. 把 key 跑通、把库存好,这条链路才算真正走完
逆向这件事,最后一步永远是闭环验证。算出输入、输入原程序、看到成功提示,这三件事凑齐了,你才有资格说自己读懂了这段逻辑。中间任何一步跳过,留下的都是自我感觉良好但经不起复现的结论。演示样本里那个怪名字的常量,我第一次是直接猜的,猜了个九位小写串,跑出来一堆不可打印字符,还以为是样本有问题;回头老老实实双击跳过去读字节,五分钟就结束了。这个对比很能说明问题:工具已经把证据摆在那儿了,多数时候卡住人的不是难度,是有没有走过去看。
如果再补一个具体技巧,我会说:动笔写解密脚本之前,先手算一组字节。挑密钥的第一个字符和密文的第一个字符,写出它们的二进制,手动异或一次,看看结果是不是符合预期方向。这一步花两分钟,能挡掉方向搞反、编码取错、长度错位这三类错误,而这三类错误在脚本跑出乱码时最难分辨。手算一组之后,脚本只是把手算过程批量化,你对结果的信任度完全不同。
还有一个习惯我后来强制自己执行:任何一次分析只要超过半小时,立刻保存数据库,并且在样本旁边放一个文本文件,记下当前进度、已经确认的结论、以及下一步想验证什么。听起来像给小学生布置作业,但它救过我两次——一次是隔了一周回来完全忘了思路,打开笔记三分钟接上;一次是中途发现方向错了,笔记里的推理链让我能准确回退到分岔点,而不是从头再来。逆向工程的记忆负担比想象中重,别指望脑子能替你存住所有细节。
需要对着练手的,配套的笔记和那个演示样本一起放在这份资源里,按第 2 章的顺序载入、按第 4 章的顺序推一遍,比自己随便找个程序乱点要快得多。希望帮到你。
本文还有配套的精品资源,点击获取