早上起来看到昨天的笔记还摊在桌上上面写着“反汇编的三种死法”觉得自己起这个系列标题真是起对了。《Re从零开始的逆向生活》——不是异世界转生是二进制世界里反复去世、反复重来。今天是我接触逆向工程的第07day按计划该开始碰第一个完整的小程序了不再是零散地学指令、拖文件、点按钮而是把前面几天的东西串起来完整地走一遍“拿到程序→静态分析→算法还原→动态验证”的流程。这篇文章就记录一下第7天的完整做题过程包括我踩的坑、看走眼的跳转、以及最后写出注册机时的那种“终于闭环了”的感觉。如果你也是刚开始学逆向的新手或者已经学过一段时间但总感觉知识点是散的可以参考我这天的思路和节奏说不定能帮你把碎片拼起来。1. 第七天该学什么定目标比闷头敲工具重要1.1 前六天的技能铺垫从认识寄存器到会看汇编既然标题叫“从零开始”那就先说说前六天我到底积累了什么不然第七天做的事情会像是空中楼阁。第一天和第二天基本是在跟寄存器较劲。什么EAX、EBX、ECX、EDX、ESP、EBP一开始真的分不清哪个是哪个。我的土办法是把寄存器想象成桌子上的格子EAX是“计算结果暂存格”ECX经常被用来数数EDX则像除法时候的“余数专用格”。虽然不完全准确但初期够用。第三天开始读汇编指令。mov、lea、push、pop、call、ret、jmp、jnz这些高频指令我找了个简单的C程序看着源码编译出来的汇编一行行对照着看。这个阶段不要贪多能把一个函数调用从参数压栈到返回的过程讲清楚就算过关。第四天和第五天重点学工具。IDA Pro和x64dbg是两把主力武器。有人说“工具就是武器趁手最重要”但我的感受是如果看不懂汇编工具再智能也没用。所以工具其实只是辅助核心能力还是读指令和推测意图。第六天做了几个非常小的题目比如那种“if (input 123456) printf(success)”的入门CrackMe已经学会在IDA里看伪代码、在x64dbg里下断点改跳转了。但那些题说白了是“被剧透”的题——一开始就知道关键行在哪里。第七天我想挑战一下自己拿到一个完全没看过的程序没有任何提示从头到尾走一遍。1.2 选第一个实战目标的标准可玩、可见、不劝退从零开始最忌讳一上来就挑硬骨头啃。我见过有人第一天就开着OD去跟商业保护壳跟了两天直接心态爆炸弃坑。所以第七天我对“实战目标”立了三个标准第一程序本身要小。最好是一个几百KB的单文件exe没有一堆DLL依赖这样静态分析时不会陷入“导入表比代码还长”的尴尬。第二逻辑要够“正统”。也就是要有清晰的输入、处理、判断、输出这种流程最好是有明显的错误提示字符串可以当作切入点。花指令、混淆、反调试这些东西等把正常流程跑通再碰第一次没必要给自己加那么多戏。第三最好是“有答案可验证”的题。比如CTF平台上的新手反向题或者经典的教学版CrackMe。不是为了分数而是为了确认自己最后给出的用户名和密码确实是对的。对的程序会弹出success错的就弹出wrong这种反馈对新手就是最强的定心丸。最后我选了一个叫做“老张的注册机练习”的教学程序——当然这个名字是我自己起的实际上就是一个很典型的用户名序列号验证程序。界面里有一个用户名输入框、一个序列号输入框按钮一按就提示注册成功或注册失败。逻辑很简单但“简单”和“一眼看穿”之间还有距离足够我练手了。1.3 今天用到的工具清单与分工这几天我总结出一个道理工具不是越多越好关键是要有清晰的分工。第七天我只用了四个工具刚好对应四个阶段文件识别用Detect It Easy简称DIE快速看编译器、语言、壳信息。静态分析用IDA Pro核心战场看汇编、看伪代码、看交叉引用。动态调试用x64dbg验证静态分析的结论观察寄存器与内存的实际变化。编写注册机用Python不需要编译改起来快做算法验证特别方便。很多人会把Ghidra也拉进来比较。说实话Ghidra的伪代码能力也挺强但IDA用顺手之后我第七天还是以IDA为主。这里没有谁绝对更好的问题哪个工具让你最快理解程序哪个就是当前阶段最好的工具。2. 静态定位从PE头到关键函数的一连串“线索链”2.1 拿到文件先别急着拖进IDA文件识别与壳检测很多新手拿到程序的第一个动作就是把exe直接拖进IDA这个习惯说不上错但不够冷静。我现在的习惯是先把文件丢给DIE看一眼。DIE能告诉我们三件事文件是什么架构的32位还是64位、什么语言写的C、C、易语言、Delphi等、有没有加壳。这三件事直接决定后面怎么走。第七天这个程序一查32位Microsoft Visual C编译无壳。看到这个结果我心里就有底了。VC编译的程序特征很明显入口点会调用GetCommandLineA之类的API函数序言经常是push ebp / mov ebp, esp字符串往往以UTF-8或ANSI存储IDA打开后自动识别的函数也比较规整。另外提醒一句32位和64位的分析思路差异不小。32位程序参数通常压栈传递而64位程序前几个参数会走寄存器对应不同平台调用约定。第一次实战建议选32位程序因为栈结构直观看着更好理解。如果DIE显示“壳”那一栏是UPX或者“保护”那就要先考虑脱壳。第七天我不想节外生枝所以特意选了个无壳程序先让整个流程顺畅跑一遍。2.2 字符串就是逆向的“路标”拖进IDA之后我没有直接按F5看伪代码而是先打开字符串列表ShiftF12。为什么先看字符串因为在绝大多数正常编写的程序里提示信息是不会凭空消失的就像电梯里的楼层指示牌虽然不能直接告诉你电梯的电路怎么走但至少能告诉你它打算停在第几层。这个程序里我一下就看到了两个关键字符串Register Success! Key Error!一个成功提示一个失败提示。任何一个正常程序都不会把这两句话写在无关紧要的位置。选中“Register Success!”按X查看交叉引用IDA会列出哪些代码引用了这段字符串。这里出现了一个最关键的函数sub_401000。从字符串跳到函数再到函数里看逻辑——这一套流程就是逆向里最常见的“线索链”打法先找一个确定性锚点沿着锚点往代码深处走而不是漫无目的地从入口点逐行读。后者当然也能走到终点但对新手来说太容易迷路。2.3 逆向阅读汇编从main到核心判断分支打开sub_401000的汇编视图第一眼看到的是标准的函数序言后面跟着一堆mov和call。我习惯性地按了一下F5IDA给了一坨伪代码。这里必须说一个很多教程没提的细节伪代码是IDA根据汇编“猜”出来的偶尔会猜错。看伪代码可以帮助快速建立整体印象但遇到可疑逻辑时必须回到汇编视图逐条核对。我仔细看这个函数的整体结构大致是这样的逻辑从输入框控件读取内容通常通过GetDlgItemTextA或GetWindowTextA这里看到调用了GetDlgItemTextA接收两个输入用户名和序列号。调用某个子函数对用户名进行处理返回值存到局部变量里。把处理结果和序列号字符串对比判断是否相等。根据对比结果弹窗显示“Register Success!”或者“Key Error!”。伪代码里最扎眼的是一句if ( v3 (unsigned __int8)sub_401050((int)user_buf, v6) )看到这个v3我之前的猜测基本被证实程序把用户输入的序列号转成了某种数值v3然后跟“用户名经过一个函数处理后得到的数值”做比较。关键就看sub_401050怎么算了。到这一步静态分析的任务从“找到目的”转变为“解剖算法”。3. 算法还原与注册机编写把逻辑从“黑盒”变成“白盒”3.1 从伪代码里抽取出可复述的算法接下来所有注意力都集中到sub_401050上。这是一个没有调用任何其他函数、干巴巴做计算的子程序伪代码不算长大概十几行。我把它整理成了下面这样1. 遍历用户名字符串的每一个字符。 2. 对于每个字符取出其ASCII码。 3. 用一个局部变量初始化为一个固定值比如0x12345678。 4. 每次循环里执行 result (result ^ (unsigned __int8)ch) * 0x8088405; 5. 循环结束后把result的高位右移24位后返回只保留一个字节。这个结构实在是太经典了本质上就是某种哈希或者校验算法。第一次看可能觉得“乘一个很大的数”很神秘但实际上很多注册机练习程序都是把名字做一个自定义变换最后跟序列号比对。还原算法的核心就是把这个变换过程搞明白并且用另一种语言复现出来。我当时盯着* 0x8088405想了一会儿0x8088405这个数不是随机选的它可能是编译器优化后的“乘法魔术”——为了将除法优化成乘法。但在这个程序里不需要细究数学原理只要确保计算流程一致结果一致就能写出能用的注册机。3.2 小端字节序、字符编码与计算过程的坑在写注册机之前我先说两个坑第七天我在这两个坑上分别花了差不多半小时。第一个坑是字节序。程序是32位的内存里多字节数字以小端方式存放也就是说低字节在前。在Python里复现公式时如果用字节数组直接拼int会出现结果跟程序对不上的情况。正确做法是先用struct.unpack(I)把小端字节序转换出来或者干脆把每个字节单独取出来运算。用一句话概括Python的int不关心内存序但是程序的计算结果关心。第二个坑是字符编码。Windows API读出来的输入框内容默认是ANSI编码。如果用户名里出现了中文或者高ASCII字符就不能简单当成日常的UTF-8逐个字符去跑。第七天我用的是全英文用户名没什么问题但如果你练手的题目涉及中文输入就一定要先确认程序里到底拿的是宽字符还是单字节字符。拿错编码前面的公式写得再对注册机也算不出正确序列号。下面是我最终写出的注册机脚本的核心部分import struct def calc_key(username: str) - int: # 初始值来自反汇编中的具体立即数 result 0x12345678 0xFFFFFFFF for ch in username.encode(latin1): # 模拟32位无符号整数运算 result (result ^ ch) 0xFFFFFFFF result (result * 0x08088405) 0xFFFFFFFF # 原程序最终返回的是最高字节 key (result 24) 0xFF return key if __name__ __main__: username input(用户名: ).strip() key calc_key(username) print(序列号(十进制):, key)为什么用 0xFFFFFFFF因为C语言的unsigned int是32位无符号溢出自动截断而Python的整数是不会溢出的。如果不手动做截断乘着乘着数字就跑到天上去最后一字节直接不对。这一点是写这类注册机最容易翻车的地方做过一次之后就会长记性。3.3 用Python把算法翻译成注册机脚本有了脚本我输入用户名“admin”跑了一下得到序列号十进制值是144。当时我心里有点没底这么简单的数字真的能是注册码吗我还以为会输出一长串。但程序的计算就是这样的它只取了最终结果的最高字节所以序列号范围天然就在0到255之间。这也提醒我一个很重要的事实不要拿“符合人类直觉”去衡量程序逻辑。程序只要能自洽地完成验证它就是对的。写注册机的人要做的不是“觉得合理”而是“精确复现”。后来验证的时候我在程序界面输入用户名“admin”序列号填“144”点击注册弹窗显示“Register Success!”。那一瞬间还挺有成就感的。第七天我终于完整地走通了一条“从黑盒到白盒”的链路。4. 动态调试验证眼见为实但眼见也会骗人4.1 用x64dbg下断点观察程序真实走向静态分析得出的结论哪怕逻辑再完整也一定要用动态调试验证一遍。不是不信任IDA而是想亲眼看着程序在运行时的实际值是怎么变化的。我用x64dbg打开这个程序在“cmp”指令所在地址下了一个断点然后回到程序界面输入用户名和序列号点击按钮。断点一触发窗口停住我仔细看寄存器区域的值。EAX里存着序列号字符串解析后的结果EDX里存着用户名经过函数处理后得到的值。我拿计算器算了一下两个值确实相等。这时候心里那块石头才彻底放下静态分析没有看错跳转方向伪代码没有误导我注册机真的生效了。还有一个小技巧在x64dbg里观察字符串时右键选择“转到内存/转储”可以看到输入框内容在内存里的原始存放形式。我特意看了“admin”这个字符串在内存里的字节序列确认是0x61 0x64 0x6D 0x69 0x6E跟ASCII表完全对得上。这种眼见为实的感觉比任何截图都更让人踏实。4.2 调试环境中的常见“假象”与应对不过动态调试也不是永远直来直去的一个很典型的“假象”是在x64dbg里看到某个跳转指令是jz结果为0则跳转但实际运行到一个断点时标志寄存器的值跟静态分析时想象的完全不同。为什么因为前面的指令可能悄悄修改了标志位或者某个call内部返回后把寄存器状态给改了。所以不要只看“跳转指令”更要把跳转前的比较指令和标志位一起看。另一个“假象”是伪代码里的变量名。IDA起的一些变量名比如v1、v2、v3是根据分配顺序来的并不是真实语义。新手容易看到v3就以为“第三个变量”但很可能它才是核心计算结果。所以我的习惯是先用伪代码建大局观真正抠细节时回到汇编把每一个mov、lea、xor都落实到底。第七天我还做了一件笨但有效的事把关键内存区域在x64dbg里加了硬件断点。当程序运行到某一条指令要去写这块内存时会立刻停下来。硬件断点就是“内存被改动就报警”的探测器这个技巧在处理数据被多次修改的场景下特别好用。以后如果碰见变量被复杂逻辑多次改写也算有一个趁手的工具兜底。4.3 第七天的避坑清单把今天踩过的坑和操作经验整理成一份清单方便后面复习拿到程序先做文件识别不要直接拖进IDA。壳和语言决定了后续思路。字符串是定位关键代码的最快入口优先看交叉引用少在入口点那里做无用功。伪代码仅供参考遇到复杂运算要回到汇编核对尤其要注意寄存器位宽和符号类型。Python写注册机时必须模拟无符号32位整数的溢出截断。忘了 0xFFFFFFFF结果一定错。字符编码问题躲不开Windows下注意ANSI和宽字符的区别。动态调试时要结合标志寄存器判断跳转方向不能只看指令助记符。别用“人类直觉”评价程序的算法合理与否只关注它是否自洽。列完之后我发现其实这些坑大多不是“技术不会”造成的而是“想当然”造成的。第七天最大的收获不是懂了那个公式而是学会了一个方法论先验证再下结论。5. 结尾想说的几句实在话第七天走完这个流程我的心态跟前几天不太一样了。前几天经常觉得自己学了一堆招式没地方使就像是背会了九九乘法表但从来不做应用题。今天算是第一次把“招式”变成“打法”虽然动作还笨拙但至少知道一个完整的问题该怎么拆解了。还有一点我觉得值得分享学逆向最容易产生的错觉是“看得懂会了”。看教程时觉得ID A 按个F5就能出伪代码真到自己手里才发现连一个字符串交叉引用都要找半天。所以如果你也在学这个方向我特别建议你尽早找一个小题目从头到尾走一遍。哪怕只是那种“最简单CrackMe”的题也会让你对工具和指令的熟悉度提升一大截。到最后再分享一句今天记录在笔记边上的话逆向这里面慢就是快宁可一步步验证也别凭感觉跳着走。第七天的路走得不快但每一步都踩实了这才是我想要的“从零开始”。明天开始第八天我准备试试把加壳程序脱掉或者找一个带简单反调试的题目练手到时候有新发现再回来继续更新。
阅读完成 · 觉得有帮助?