ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

深入解析lena151第6课:从爆破到算法还原的Crackme分析流程

深入解析lena151第6课:从爆破到算法还原的Crackme分析流程 如果你也是顺着tuts4you上lena151系列一路摸过来的八成会在tutorial06这里多停一阵。前面几课基本都是在教“怎么把一个程序跑起来、怎么下断点、怎么找字符串、怎么改跳转”很多人到这一课之前都还挺有信心。结果第六课画风一转程序不让你简单地跳过验证了它开始跟你玩真正的算法。老实说我第一次在调试器里看到一连串mul、div、shl、xor的指令时心里是发毛的完全不知道从哪里下手。这篇我就用最笨、但绝对可复制的方式把这类Crackme的完整分析流程拆开讲清楚。需要先说明的是这类教程里的练习程序都是作者专门发布出来给大家学习软件保护原理用的属于合法授权的教学样本可以放心折腾。学习重点是理解程序内部的数据流动和验证逻辑而不是追求“破解”这个动作本身。1. 先把背景说清楚tuts4you、lena151、tutorial06到底是什么1.1 这套系列在逆向圈里的分量tuts4you是一个很老的逆向工程学习社区上面有大量教程和练习用的Crackme资源。而“lena151”这个名字老逆向玩家基本都听说过这是作者lena发布的一整套入门教学视频/文档合集编号一般都被大家直接拿来当课程目录用。之所以叫“151”常见说法是这套合集整理出来的文件编号或者压缩包序号大家叫着叫着就成代号了实际上是包含了几十课渐进式的逆向教学内容。这个系列牛在它“真的从零开始教”。第一课可能就是最简单的消息框、一个jmp跳过验证到后面慢慢会接触到各种花指令、反调试、壳、算法还原等。它的课程结构非常适合没有系统学过编译原理、汇编的人因为每课都只引入一个新知识点并且用一个独立的Crackme来验证。而tutorial06在整个系列里是一个比较微妙的位置。前面的课程教了你“程序大概长什么样”从这一课开始它希望你把注意力从“找关键跳转”转移到“理解验证代码本身”上。说得直白点前几课你只要会找jz/jnz就能过关从第六课开始光会找跳转不够了你必须看懂这行跳转是基于什么条件产生的。1.2 第六课在整条学习路线里的位置我见过很多新手卡在这一课不是因为难度突然爆炸而是因为之前的习惯太“肌肉记忆”了打开程序、搜索字符串、看到Try Again、回溯到关键跳转、改掉、完事。到了tutorial06这套流程往往还管用但它会给你一种“我好像会了”的错觉——因为字符串照样能搜到跳转照样能改程序照样能激活。但教程的真正目的在于如果只是改跳转你根本没学到这一课想教的东西。第六课想让你学会的是解密一个用户名/序列号校验算法的完整路径。也就是说程序把用户输入的用户名经过一系列运算得到一个机器计算出来的序列号再跟你输入的序列号比较。你要做的不是跳过这个比较而是把这个运算过程完整还原出来变成你自己的注册机。这也是为什么很多人回看这个系列的时候会把tutorial06当成第一个真正的分水岭。会跟算法的人和只会爆破的人从这一课开始出现明显的差距。2. 拿到程序以后先别急着点“Check”2.1 用检测工具确认编译特征和是否有壳不管题目多简单我拿到一个不认识的Crackme第一件事永远是查一下它的基本信息。这里的“查”不是随便看一眼图标、界面而是用工具确认几个关键点程序是32位还是64位有没有加壳加了什么壳;大概率是用什么编译器写的VC6、Delphi、VB、MinGW等。老教程里常用的工具是PEiD现在大家用得更多的可能是Detect it EasyDIE或者Exeinfo PE。DIE的识别率更高而且会顺带告诉你编译器的特征字节是什么这对接下来的调试策略很有用。举个例子如果识别结果是“Microsoft Visual C 6.0”那你基本可以猜到程序内部是调用GetDlgItemTextA之类的标准API来读取输入比较时大概率用lstrcmpA或strcmp。如果识别结果是“Delphi”那就要注意Delphi的字符串比较有自己的内部函数比如LStrCmp。这些信息决定了你后面断点该怎么下、字符串该用ASCII还是Unicode来搜。在这一步我还习惯顺手看一眼节区表。正常VC编译的程序通常有.text、.rdata、.data等几个标准节区如果看到什么.UPX0、.aspack、.nsp0之类的奇怪名字说明被加壳了那就得先去脱壳。tutorial06这个阶段一般不会故意刁难你大概率是无壳的但如果以后自己找额外练习做这个习惯能省很多时间。2.2 动态调试器选择OllyDbg还是x64dbg老教程全程用的是OllyDbg 1.10它至今仍然是很多32位Windows程序的调试利器界面不花哨插件生态成熟特别是字符串搜索、命令行插件、脚本支持都很方便。它的缺点是年久失修在Win10/11上偶尔会抽风而且不支持64位。如果你手上的Crackme是64位的那就别折腾OllyDbg了直接上x64dbg。x64dbg可以理解成OllyDbg的现代继任者界面逻辑和快捷键跟OllyDbg高度类似比如F7单步步入、F8单步步过、F9运行、CtrlG跳到表达式地址。从OllyDbg切到x64dbg基本没有学习成本。我个人现在默认用x64dbg多一点哪怕调试32位程序也完全支持。原因有三个界面能自适应高DPI窗口不会糊成一团自带更好的图模块和搜索功能插件系统更新也更活跃。如果你完全没接触过这两个工具直接学x64dbg就好不用回头去走老路。2.3 跑一下程序摸清交互逻辑在真正动调试器之前花两分钟把程序当作普通软件点一遍弄清楚它有几个输入框、按钮按下去会发生什么。比如这个程序可能是这样打开后是一个对话框上面有“Name”输入框和“Serial”输入框最下面是一个“Check”按钮随便输入一个名字test序列号填123点按钮弹出一个消息框写着类似“Wrong Serial”的文字。这个过程不是瞎玩它帮你在脑子里建立一个基本模型程序从两个窗口控件里读文本经过某个计算再判断匹配与否最后根据结果弹出不同的消息框。后面调试时你的目标就是找到“读文本”之后、“弹消息框”之前的中间那段代码。这里有一个非常实用的经验如果输入框是对话框控件你几乎可以用一句通用断点搞定问题那就是给读取控件内容的API下断点。常见的API是GetDlgItemTextAANSI版或GetDlgItemTextWUnicode版如果是非对话框程序则会用GetWindowTextA/W。在这些函数入口下断然后点按钮程序一般会乖乖停在读取用户输入的那一瞬间。3. 三种定位验证代码的路径总有一条适合你3.1 最直接的方法字符串搜索定位打开OllyDbg或x64dbg把程序加载进调试器让系统断点跑完、停在程序入口之后右键选择“搜索所有引用字符串Search for All Referenced Text Strings”或者直接按快捷键在字符串列表里找跟校验结果相关的文字。以“Wrong Serial”为例搜到以后双击进去你会看到类似这样的反汇编伪代码结构0040137A push 0 0040137C push offset Wrong Serial 00401381 call MessageBoxA 00401386 jmp short 00401390 00401388 push 0 0040138A push offset Good Boy 0040138F call MessageBoxA这两个分支中间一定会有一天没有名字的跳转指令比如00401355 jnz short 0040137A这个jnz就叫做“关键跳转”。它的意思很直白如果条件不成立就跳到“Wrong Serial”那条路上条件成立就继续往下走到“Good Boy”。所以很多新手教程会教你直接把这个jnz改成jz或者改成jmp 00401388也就是所谓的爆破。但我必须说如果你只是这样做就关掉程序那你这一课白练了。字符串搜索真正的价值是帮你快速拿到一个定位支点知道失败分支在哪里之后你回看代码找到它的前一条比较指令再顺着比较指令向上回溯自然就能看到算法的入口在哪。3.2 API断点看不到字符串时的备选方案有时候程序可能不使用明文字符串来提示错误比如把字符串加密了、动态解密后再弹窗。这时候字符串搜索帮不上忙你可以改用API断点。在调试器里对MessageBoxA/MessageBoxW下断点然后运行程序点击Check按钮。每次程序准备弹消息框都会断下来。这时候你查看调用栈就能看到是哪一段代码触发了这个消息框然后在这个调用者的上一两层找到Compare相关指令。另一种很有用的API是lstrcmpA/lstrcmpW。有些简单的序列号校验程序会事先把正确的序列号硬编码在数据区输入序列号之后直接调用字符串比较函数。给它下个断点让它断下来然后看两个参数寄存器32位下通常是EAX和EDX指向的缓冲区一个是用户输入的序列号另一个就是程序期望的正确答案。这就是分分钟出结果的情况。3.3 直接硬跟从读输入函数返回到算法区并不是所有Crackme都弹消息框让你有字符串可搜有些程序做得很“闷”按钮按下去没有任何输出只是在条件满足时改变窗口标题或标题栏状态。这时候上面两种方法可能会失灵最稳的办法就是从读取输入的API开始硬跟。我们在输入框附近对GetDlgItemTextA下断点后点击Check程序断在这个API入口。此时栈顶上就是返回地址你在调试器里执行“运行到返回”OllyDbg/x64dbg都是快捷键CtrlF9就能直接返回到调用它的上层代码处。从这里开始逐条往下F8单步走观察每一条指令对寄存器和内存的改动。你会看到程序把用户输入的字符串取出、转换成某个数值、做运算、再和另一个数值比较整个算法区全部暴露在眼前。硬跟最考验耐心但也是掌握指令语义最快的路径。对于tutorial06这个阶段我建议即使用了前两种方法定位到了关键跳转也还是要再用第三种思路把从读取输入到比较的整段代码完整看一遍否则你的“算法还原”能力永远提升不了。4. 实操还原一个典型的name/serial算法为了把跟随调试器的过程说透我先声明一下下面用来拆解的算法原型直接来源于同类课程的典型模型结构上跟tutorial06要分析的Crackme非常接近但你实际拿到的题目细节肯定不太一样。重点在于掌握分析方法而不是背下来一个固定答案。反复练几道题之后就明白了。假设程序逻辑是这样用户名Name输入admin序列号Serial随便填12345。点击按钮后弹“Wrong Serial”。我现在的目标不是修改跳转而是写出一个函数给任意用户名计算出程序认可的序列号。4.1 在调试器里记录初始状态先把程序加载进x64dbg对GetDlgItemTextA下断点。在输入框里填好用户名admin序列号随便填123点击Check。程序断下来按CtrlF9执行到返回回到上层代码004013A0 mov edx, dword ptr [ebp-0x414] ; 这是输入的用户名字符串 004013A7 push edx 004013A8 call GetDlgItemTextA 004013AD lea ecx, dword ptr [ebp-0x208] ; 另一个缓冲区用于序列号 004013B4 push ecx 004013B5 push 500 004013BA push IDC_SERIAL 004013BF push hWin 004013C5 call GetDlgItemTextA到这里[ebp-0x414]是用户名缓冲区的起始地址[ebp-0x208]是序列号缓冲区的起始地址。我习惯在数据窗口里给这两个地址下内存访问断点但当前阶段没必要直接F8往下走就能看到算法指令。紧接着你会看到类似这样的代码为了讲清楚逻辑我把真实的地址简化了004013CB lea esi, dword ptr [ebp-0x414] ; esi指向用户名字符串 004013D1 xor ebx, ebx ; ebx清零这里大概率是累加器 004013D3 mov edi, 0x7A ; edi122初始值 004013D9 push esi 004013DA call strlenA ; 计算用户名长度 004013DF add esp, 4 004013E2 test eax, eax ; 判断名字是否为空 004013E4 je short 00401440 ; 为空就跳去失败分支 004013E6 mov ecx, eax ; ecx 长度作为循环次数 004013E8 xor eax, eax这个结构太典型了一个累加器清零、一个初始值、拿到字符串长度作为循环计数器。看到这里基本可以确认算法核心就在接下来的循环体里。4.2 逐条进入循环观察每轮数据变化继续F8进入一个循环结构大概是004013EA movsx eax, byte ptr [esi] ; 取当前字符并符号扩展到32位 004013ED imul eax, eax, 0x1F ; 字符值 * 31 004013F0 add ebx, eax ; 累加到ebx 004013F2 inc esi ; 指针移到下一个字符 004013F3 dec ecx ; 计数器减一 004013F4 jnz short 004013EA ; 如果没结束就继续循环这个算法很简单每个字符的ASCII码乘以31再全部加起来结果存在ebx里。之后代码把ebx与某个常量做运算最后和用户输入的序列号比较。这里先停一下我解释几个新手容易懵的点第一movsx eax, byte ptr [esi]这条指令的意思是“把缓冲区当前字节取出来按符号扩展到32位放进eax”。为什么用movsx而不是movzxmovzx是无符号扩展高位补0movsx是符号扩展如果字节最高位是1高位全部补1。在纯ASCII用户名值范围0x20~0x7E的情况下两者结果一样。但如果你用户名里输入了中文编码UTF-8的每个字节大于0x80表示出来是有符号负数movsx会把每个字节当成负数参与运算。这就是很多注册机在碰到中文用户名时算出来不对的原因之一。第二imul eax, eax, 0x1F是一条三操作数乘法指令eax eax * 0x1F。乘以31在密码学里是一个经典操作因为31是质数能有效散列字符分布很多字符串哈希都喜欢用31这个数Java的String.hashCode就是每步h 31 * h ch。看到乘31、累加、再乘以别的质数基本可以推断程序在实现一个字符串哈希函数。循环结束之后代码继续简单处理一下004013F6 imul ebx, ebx, 0x17 ; ebx ebx * 23 004013F9 add ebx, 0xD7 ; ebx ebx 215 004013FC mov eax, ebx 004013FE cdq ; 把eax符号扩展到edx为有符号除法做准备 004013FF mov ecx, 9999 00401404 idiv ecx ; eax eax / 9999, edx eax % 9999 00401406 cmp edx, [ebp-0x208] ; 用取模结果的低16位和用户输入比较看到cdqidiv这对组合就要意识到程序用的是有符号除法。前面的累加结果如果因为乘法溢出导致最高位为1idiv通常会直接触发Divide by zero或Integer overflow异常程序直接崩掉。这也是新手实战中经常遇到“明明算法还原得很对跑起来却报整除异常”的原因。继续看最后一步通常是把你输入的序列号字符串转成数值然后和计算结果比较00401409 push [ebp-0x208] 0040140C call atoi ; 把用户输入的序列号字符串转成int 00401411 add esp, 4 00401414 cmp eax, edx 00401416 je short Good Boy分支 00401418 jmp short Wrong Serial分支至此整个验证逻辑已经水落石出。4.3 把汇编翻译成高级语言把上面这段汇编翻译成C语言或Python难度并不高重点是要把每一个运算步骤都映射到高级语言里尤其是类型def calc_serial(name): total 0 for ch_byte in name.encode(utf-8): # 注意这里模拟的是movsx需要把字节看作有符号char signed_byte ch_byte if ch_byte 0x80 else ch_byte - 0x100 total signed_byte * 31 total * 23 total 215 # 有符号除法取余数 result total % 9999 return result这里有个细节Python的%行为对被除数是负数时结果是非负余数而C语言里的%在C99之前是“商向零取整余数的符号跟随被除数”。如果total是负数两种结果可能不一样。所以写注册机时最好用C或C#来验证或者自己额外处理符号问题。这个坑我是实实在在踩过的一开始用Python写出了个在其他用户名上总是差上几个数或符号不对的注册码折腾半天才发现是除法语义的差异。4.4 写一个简易注册机验证还原的对错有了算法注册机就只是顺手的事。你可以用任何语言实现只要类型和运算顺序不出错def keygen(name): total 0 for ch_byte in name.encode(utf-8): signed_byte ch_byte if ch_byte 0x80 else ch_byte - 0x100 total signed_byte * 31 total total * 23 215 # 模仿C语言有符号除法对负数的截断行为 result abs(total) % 9999 if total 0: result -result return str(result)然后把计算结果填回Crackme的序列号输入框点Check弹 “Good Boy” 基本就说明还原正确。如果不正确先不要怀疑算法步骤优先检查这几点累加器是否被清零汇编里有没有xor ebx, ebx初始值是多少是0还是某个常量每次循环是处理了一个字节还是两个字节Unicode 程序会按两个字节取字符最后是比较余数、比较商、还是比较两者拼接后的字符串用户输入的序列号是被atoi转成整数再比较还是原样字符串比较。5. 爆破和注册机不是二选一而是有明确的学习优先级5.1 为什么教程不让你只改跳转在tutorial06这个阶段如果只是把关键跳转改掉就算完成任务那你其实“浪费”了这一课最精华的部分。爆破教会你的是“程序在哪个点决定了成与败”这是定位能力而算法还原教会你的是“程序为什么在这一点上做这个决定”这是分析能力。两者第一步是一样的但从这里开始分岔。我见过一些朋友Crackme做了几百个但是拿到一个没见过的新程序还是只会那三板斧搜字符串、改跳转、收工。遇到程序把字符串隐藏起来或者校验逻辑复杂一点就断片。这种状态说明基本功不牢因为所有外在的花活最终都要落到“看懂代码在算什么”上。反过来如果你每一题都尽量还原算法、写成注册机哪怕程序本身很简单你也会慢慢对常见的代码模式形成肌肉记忆比如怎么识别哈希、怎么识别base64解码、怎么识别文件校验。到了进入实战研究阶段这种“读懂逻辑”的能力远比“改掉一个跳转”的值钱得多。5.2 什么时候爆破是合理的当然我也不是说不许爆破。实际工作中有些场景就是需要快速验证逻辑的比如你只是想知道这个保护机制是不是一个简单的用户名/序列号比对那改一下跳转跑通确认判断3分钟就够。又比如你在调自己写的程序只想临时绕过一个校验去看后面的功能爆破明显比重写一段逻辑更省事。但职业习惯上爆破永远只是临时手段不是最终交付。因为在真实研究中你跟一个保护对抗最后的产物应该是一份准确说明“校验如何工作、如何被绕过”的分析报告而不是一句“我把jnz改成jmp了”。所以我的建议是每个Crackme先爆破了它再把算法还原出来先跑通再彻底理解。这两步都做到才算真正吃透一题。6. 我在这类题目上反复踩过的一些坑6.1 符号扩展和除法溢出的问题前面已经提过movsx、cdq、idiv这三个关键词。它们连在一起最容易出问题的场景就是用户名里出现非ASCII字符。在中文Windows系统下如果用户名字符串按GBK或UTF-8编码存储往往会有高位为1的字节movsx会把它扩展为负数。如果那个字符刚好是个“负的大数”乘法之后累加值可能溢出接着cdq/idiv就可能异常。我曾经在一个类似的Crackme上用英文用户名一切正常换成一个中文用户名程序直接崩溃。当时第一反应是程序本身有Bug后来仔细跟才发现正是符号扩展导致累加器变成很大的正数和负数交替翻转数值溢出后的高位刚好触发了除法溢出。这种事在分析报告里写出来会显得你考虑得很全面。6.2 下断点下错版本怎么都断不下来32位Crackme往往用ANSI API也就是GetWindowTextA、MessageBoxA这种带A结尾的64位程序默认用GetWindowText/MessageBox没有A/W后缀。新手容易在64位程序里傻傻地下MessageBoxA断点结果老断不下来还以为程序有反调试。另一个坑是程序可能不是用标准API弹窗而是直接在自己的对话框资源里显示文本这种情况你下MessageBox断点当然没用。但没关系字符串搜索一般还是能搜到资源里的文本所以字符串搜索依然是第一选择。6.3 序列号比较时到底是比较字符串还是比较数值很多Crackme会把输入的序列号直接用atoi转成整数然后和计算结果比较。这样做会带来一个明显行为序列号前面允许有0比如0042和42是等价的。这时候如果你写注册机生成的是42填进去0042也能通过。但另一些程序不转数值直接用lstrcmp比较字符串那么0042和42就不相等。判断方法是看代码里有没有call atoi或类似sscanf的调用也有程序用__vbaStrCmp之类的运行时函数那就跟字符串比较有关。这种差别看起来小实际影响注册机的返回格式所以一定要先看比较指令前面的转换步骤。6.4 一个让我少走很多弯路的小习惯最后想分享一下我个人摸索出来的一个习惯每找到一个关键跳转不要马上改它先在调试器里把“它为什么在这里跳”想明白。方法很简单在跳转指令的上一行设置断点运行到那里后看比较指令比较的是哪两个寄存器再看这两个寄存器的值分别从哪里来。做上十道题之后你会发现自己分析问题的速度提升非常明显。那时再回来看tutorial06你会觉得它更像一个思维转变的开始而不是一道普通的练习题。
返回列表