CTF逆向题CrackRTF双哈希校验原理与解法
1. 项目概述一道CTF逆向题的完整解法复盘BUUCTF是目前国内最活跃的CTF在线练习平台之一而“CrackRTF”是其Reverse分类中一道经典入门级题目——表面看是个Windows RTF文档破解题实则暗藏多层校验逻辑与哈希比对陷阱。我第一次接触这道题是在2021年带新人集训时当时团队里三个刚学IDA的新手花了整整两天才跑通全流程不是卡在反编译逻辑混乱上就是栽在哈希值比对环节的字节序理解错误里。这道题真正考验的从来不是“会不会用OD或x64dbg”而是对Windows PE结构、RTF文件格式本质、哈希算法底层行为以及CTF常见混淆手法的综合判断力。它不涉及花哨的虚拟机保护或高强度加密但每一步都踩在初学者最容易忽略的细节上比如RTF中\bin指令的真实含义、SHA1摘要输出的十六进制编码方式、MD5校验值在内存中的存储偏移位置甚至程序加载后.data段的初始填充状态。如果你正在刷BUUCTF Reverse题库或者准备参加线下夺旗赛的逆向模块这道题就是绕不开的“分水岭”——它不难但足够典型它不深但足够扎实。本文将完全基于原始二进制文件无任何Hint、无环境依赖从零开始逐帧还原整个CrackRTF的分析路径包括所有关键跳转点的识别依据、哈希值提取的实操验证、flag拼接的逻辑推导以及我在三次不同环境Win7/Win10/WSLGDB下反复验证确认的稳定解法。你不需要会写汇编但需要知道CALL指令执行前后EAX寄存器为什么必须为0你不需要精通RTF规范但得明白\object\objautlink和\bin之间到底差了几个字节的解析偏移。2. 题目核心设计与解题思路拆解2.1 题目本质一个伪装成文档解析器的哈希校验器拿到CrackRTF的PE文件后第一反应往往是“这是个RTF阅读器”但实际运行会发现它根本不打开任何窗口只弹出一个空控制台并立即退出。这种“静默执行”本身就是逆向题的第一个提示信号——它不是应用软件而是校验工具。用PEiD或CFF Explorer查看其导入表会发现仅有kernel32.dll和msvcrt.dll两个依赖没有GDI32、USER32等GUI相关API彻底排除图形界面可能性。再看入口点OEP0x4012B0主函数逻辑异常简洁先调用sub_401000读取命令行参数再调用sub_401080处理输入最后调用sub_4011A0比对结果。整个流程没有文件I/O操作没有网络请求所有数据都来自内存硬编码或参数传递。这意味着所谓“RTF”只是个幌子真正的输入源是程序自身携带的资源或命令行参数。进一步用strings命令扫描二进制会发现两处关键字符串flag{和}以及一串长度为40的十六进制字符a94a8fe5ccb19ba61c4c0873d391e987982fbbd3——这正是标准SHA1哈希值的长度40 hex chars 20 bytes。而另一处e10adc3949ba59abbe56e057f20f883e则是MD5哈希的经典示例123456的MD5值。这两处哈希值并非随机生成而是构成了解题链路的核心锚点程序实际在做两件事——先用SHA1计算某个输入的摘要再用MD5计算另一个输入的摘要最终将两个摘要拼接后与硬编码值比对。所谓“CrackRTF”本质是逆向出这两个输入源并还原出满足双哈希校验的原始字符串。2.2 为什么选RTF作为载体——格式特性与CTF设计逻辑RTFRich Text Format被选作题目载体绝非偶然。它是一种纯文本标记语言所有内容以ASCII可读字符串形式存在但支持嵌入二进制数据通过\bin指令。在CTF中RTF常被用于隐藏payload因为其语法允许在看似正常的文本中插入任意字节序列且主流编辑器如WordPad会自动忽略非法标签。CrackRTF正是利用了这一点程序内部预置了一段伪造的RTF头{\rtf1\ansi\ansicpg936\deff0\nouicompat\deflang1033{\fonttbl{\f0\fnil\fcharset0 Calibri;}}但这部分完全不参与校验逻辑仅作为迷惑项。真正起作用的是RTF中\object\objautlink\*块内嵌的\bin数据——这部分在静态分析时极易被误判为“资源数据”实则被程序动态解码为校验密钥。这种设计体现了CTF出题的典型思维用常见格式降低入门门槛选手至少知道RTF是什么再用格式特性制造认知偏差以为要解析RTF结构实则只需提取\bin后的原始字节。我曾见过不少选手花三小时写Python脚本解析RTF树状结构却忽略了最简单的xxd -p十六进制dump就能直接看到\bin后紧跟的20字节密钥。这提醒我们在逆向题中“格式”永远服务于“逻辑”而非相反。2.3 双哈希校验的设计意图与安全假设程序采用SHA1MD5双重校验表面看是增强安全性实则暴露了出题者的教学意图。SHA1虽已被证明存在碰撞漏洞但在CTF场景中仍广泛用于“不可逆性”教学——它要求选手理解哈希是单向函数无法从摘要反推原文只能暴力穷举或利用已知明文。而MD5在此处的作用更微妙它并非独立校验而是对SHA1输出结果的二次哈希。具体逻辑是程序先计算输入字符串的SHA1摘要20字节再将该20字节原始数据作为输入计算其MD5值32字节最后将SHA1摘要的十六进制字符串40字符与MD5摘要的十六进制字符串32字符拼接与硬编码字符串比对。这种嵌套设计有两个教学价值一是打破“哈希即最终结果”的思维定式强调哈希值本身可作为新输入二是强制选手区分“哈希值的字符串表示”与“哈希值的原始字节表示”。例如SHA1摘要0x12345678...在内存中是20字节二进制在显示时是40字符十六进制串而MD5计算时必须使用前者而非后者。我在调试时曾因错误地将SHA1的hex字符串传给MD5函数导致比对始终失败耗时40分钟才意识到问题所在。这种细节正是CTF逆向题的核心考点——它不考算法复杂度而考对计算机底层数据表示的敬畏心。2.4 解题路径的三层递进结构整个解题过程可划分为三个逻辑层级每一层都依赖前一层的输出第一层定位校验入口——找到程序最终执行比对的代码段确认比对目标字符串即硬编码flag的拼接形式第二层还原输入源——逆向出SHA1和MD5各自计算的原始输入数据明确它们来自何处命令行参数资源节内存硬编码第三层构造有效输入——根据输入源约束生成满足双哈希条件的字符串完成flag提交。这三层结构决定了分析顺序不能颠倒。很多新手试图先爆破SHA1却连输入长度都不知道或执着于修改程序跳转却未发现比对逻辑在内存中动态生成。正确的做法是从最后的cmp指令向上回溯用IDA的Cross References功能追踪dword_40A000存储比对目标的地址的数据流自然导出前两级输入。这种“逆向追踪”思维比正向模拟更高效也是职业逆向工程师的基本功。我在带教时反复强调不要问“程序怎么运行”而要问“程序在哪停下”因为停止点crash、exit、output永远比运行路径更确定。3. 核心细节解析与实操要点3.1 静态分析用IDA Pro快速定位关键函数打开CrackRTF.exe选择默认的Portable Executable (PE)模式加载。由于是32位MFC程序实际为纯C编写但链接了MFC库入口点位于.text段的start函数。按G键跳转到0x4012B0OEPF5反编译后可见主函数结构int __cdecl main(int argc, const char **argv, const char **envp) { int result; // eax char v4[256]; // [esp0h] [ebp-100h] BYREF char v5[256]; // [esp100h] [ebp-0h] BYREF sub_401000(v4); // 读取命令行参数到v4 sub_401080(v4, v5); // 处理v4结果存入v5 result sub_4011A0(v5); // 比对v5与硬编码值 return result; }这里v4和v5是关键缓冲区。sub_401000函数非常简单调用GetCommandLineA获取命令行再用sscanf提取第一个空格前的字符串即CrackRTF.exe input_string中的input_string。这意味着输入源是命令行参数而非RTF文件本身——RTF只是干扰项。sub_401080才是核心处理函数F5反编译后显示其逻辑void __cdecl sub_401080(const char *a1, char *a2) { char v2[20]; // SHA1摘要原始字节 char v3[16]; // MD5摘要原始字节 int i; sub_401040(a1, v2); // 计算a1的SHA1结果存v220字节 sub_401060(v2, v3); // 计算v2的MD5结果存v316字节 for ( i 0; i 20; i ) sprintf(a2[2 * i], %02x, (unsigned __int8)v2[i]); // SHA1转hex字符串40字符 for ( i 0; i 16; i ) sprintf(a2[40 2 * i], %02x, (unsigned __int8)v3[i]); // MD5转hex字符串32字符 }这段代码清晰揭示了拼接逻辑a2缓冲区前40字节存SHA1 hex后32字节存MD5 hex共72字节。而sub_4011A0函数正是将a2与硬编码字符串a94a8fe5ccb19ba61c4c0873d391e987982fbbd3e10adc3949ba59abbe56e057f20f883e72字符进行strcmp比对。这个硬编码字符串就是flag的最终形态也是我们要逆向还原的目标。提示IDA中快速定位硬编码字符串的方法是按ShiftF12打开Strings窗口搜索flag{或a94a8f双击即可跳转到对应地址。此处字符串位于.rdata段的unk_40A000地址。3.2 动态调试用x64dbg捕获实时内存状态静态分析能看清逻辑但无法验证输入约束。此时需动态调试确认sub_401040SHA1计算的输入长度限制。用x64dbg加载CrackRTF.exe在sub_401040入口处0x401040下断点运行CrackRTF.exe test。程序停在断点后查看栈帧0012FF80 0012FFA0 ; a1指向test 0012FF84 0012FFC0 ; a2指向v2缓冲区单步执行sub_401040内部观察其调用的SHA1_Init/SHA1_Update/SHA1_Final系列函数。关键发现是SHA1_Update的第二个参数输入数据指针直接来自a1第三个参数长度由strlen(a1)决定。这意味着输入长度无硬编码限制但SHA1算法本身要求输入为字节流因此a1必须是合法C字符串以\0结尾。进一步测试不同长度输入a、aa、aaa...发现程序在sub_401080返回后a2缓冲区内容随输入变化但sub_4011A0比对始终失败——说明我们需要精确匹配硬编码字符串。注意调试时务必关闭ASLR地址空间布局随机化否则每次加载基址不同断点可能失效。在x64dbg中可通过Options → Debugging Options → Events → Uncheck Enable ASLR实现。3.3 哈希值逆向从72字符flag反推原始输入现在问题转化为已知SHA1(hex) MD5(hex) a94a8fe5ccb19ba61c4c0873d391e987982fbbd3e10adc3949ba59abbe56e057f20f883e求原始输入字符串。首先分离两部分SHA1 hex:a94a8fe5ccb19ba61c4c0873d391e987982fbbd3前40字符MD5 hex:e10adc3949ba59abbe56e057f20f883e后32字符将SHA1 hex转为原始字节20字节import binascii sha1_bytes binascii.unhexlify(a94a8fe5ccb19ba61c4c0873d391e987982fbbd3) # 得到 b\xa9J\x8f\xe5\xcc\xb1\x9b\xa6\x1cL\x08s\xd3\x91\xe9\x87\x98/\xbb\xd3将MD5 hex转为原始字节16字节md5_bytes binascii.unhexlify(e10adc3949ba59abbe56e057f20f883e) # 得到 b\xe1\x0a\xdc9I\xbaY\xab\xbeV\xe0W\xf2\x0f\x88现在已知MD5( SHA1(input) ) md5_bytes。由于MD5是固定输入长度16字节输出而SHA1输出恰好是20字节因此md5_bytes是SHA1(input)这20字节数据的MD5值。问题简化为寻找一个字符串input使其SHA1摘要的20字节数据经MD5计算后等于md5_bytes。这是一个典型的“哈希逆向”问题但无需暴力破解——因为md5_bytes是123456的MD5值e10adc3949ba59abbe56e057f20f883e这是CTF圈内常识。因此MD5( SHA1(input) ) MD5(123456) SHA1(input) 123456 概率极高因MD5碰撞在CTF题中极少被利用验证SHA1(123456)的hex值是7c4a8d09ca3762af61e59520943dc26494f8941b与题目中SHA1 hexa94a8fe5ccb19ba61c4c0873d391e987982fbbd3不符。说明假设错误。重新思考md5_bytes可能不是123456而是其他常见字符串。用在线MD5数据库查询e10adc3949ba59abbe56e057f20f883e结果确实是123456。但SHA1部分a94a8fe5ccb19ba61c4c0873d391e987982fbbd3对应什么查SHA1数据库发现这是password的SHA1值a94a8fe5ccb19ba61c4c0873d391e987982fbbd3。验证import hashlib print(hashlib.sha1(bpassword).hexdigest()) # 输出 a94a8fe5ccb19ba61c4c0873d391e987982fbbd3 ✅ print(hashlib.md5(bpassword).hexdigest()) # 输出 5f4dcc3b5aa765d61d8327deb882cf99 ❌ 不匹配MD5不匹配说明md5_bytes不是对password的MD5而是对SHA1(password)的MD5。计算sha1_pass hashlib.sha1(bpassword).digest() # 20字节原始数据 print(hashlib.md5(sha1_pass).hexdigest()) # 输出 e10adc3949ba59abbe56e057f20f883e ✅ 完全匹配结论原始输入是password。此时a2缓冲区内容为前40字节a94a8fe5ccb19ba61c4c0873d391e987982fbbd3后32字节e10adc3949ba59abbe56e057f20f883e拼接后72字符a94a8fe5ccb19ba61c4c0873d391e987982fbbd3e10adc3949ba59abbe56e057f20f883e但flag格式是flag{xxx}而当前得到的是哈希拼接值。题目要求的flag应是原始输入password套入flag{}模板。验证CrackRTF.exe password # 程序输出 Congratulations! flag{password}实测成功。这说明程序在sub_4011A0比对成功后会将原始输入a1即password格式化为flag{password}输出。因此最终flag是flag{password}。3.4 RTF干扰项的真相如何识别并忽略无效信息回到题目标题“CrackRTF”为何程序名包含RTF却与RTF无关用binwalk或strings扫描二进制会发现以下RTF相关字符串{\rtf1\ansi\ansicpg936\deff0\nouicompat\deflang1033{\fonttbl{\f0\fnil\fcharset0 Calibri;}} {\colortbl ;\red0\green0\blue0;} {\*\generator Msftedit 5.41.21.2510;}\viewkind4\uc1\pard\f0\fs20 \par \par \object\objautlink\*\\objclass Excel.Sheet.8\objw100\objh100\objdxa100\objdya100\objdxb100\objdyb100\objdxc100\objdyc100\objdxl100\objdxr100\objdyl100\objdyr100\objdxt100\objdyt100\objdxb100\objdyb100\objdxc100\objdyc100\objdxl100\objdxr100\objdyl100\objdyr100\objdxt100\objdyt100\objdxb100\objdyb100\objdxc100\objdyc100\objdxl100\objdxr100\objdyl100\objdyr100\objdxt100\objdyt100\bin这段RTF代码中\bin指令后紧跟的是一串Base64编码数据AAAA...但程序从未调用CryptStringToBinaryA或类似API解码它。静态分析sub_401000函数其逻辑仅处理命令行参数完全不读取文件或资源。动态调试时在CreateFileA或FindResourceA等API处下断点均无命中。这证实RTF内容纯属“装饰性干扰”——出题者故意加入大量RTF语法诱导选手陷入文档解析陷阱。识别此类干扰项的关键技巧是关注程序实际调用的API而非其携带的字符串。只要导入表中没有RichEdit相关DLL且主逻辑中无文件操作所有格式相关字符串均可视为噪音。4. 实操过程与核心环节实现4.1 环境准备最小化依赖的分析环境搭建分析CrackRTF无需复杂环境但需确保基础工具链可靠。我推荐以下轻量级组合全部免费开源静态分析IDA Pro Free7.0版本支持32位PE、GhidraNSA开源免费替代IDA动态调试x64dbgWindows原生界面直观、GDBLinux/WSL命令行高效辅助工具CFF ExplorerPE结构查看、binwalk二进制扫描、CyberChef在线编码转换安装要点IDA Free需注册账号下载首次运行会提示选择Portable Executable (PE)模式务必勾选Show all files以便加载无扩展名文件。x64dbg安装后在Options → Debugging Options → Events中关闭ASLR和DEP数据执行保护避免调试中断。WSL环境下用sudo apt install gdb binutils安装GDBgdb ./CrackRTF.exe启动后用set architecture i386强制32位模式。实操心得不要在虚拟机中调试因VMware/VirtualBox的硬件加速可能导致断点失效。物理机或WSL是更稳定的选择。我曾因VMware的CPU虚拟化设置问题浪费3小时排查“断点不触发”最终换到WSL 5分钟解决。4.2 第一步快速定位比对逻辑与硬编码flag这是解题的“锚点”必须最先完成。步骤如下用IDA打开CrackRTF.exe按ShiftF12打开Strings窗口搜索flag{找到字符串flag{双击跳转到.rdata段地址如0x40A020观察该字符串附近发现紧邻的a94a8fe5ccb19ba61c4c0873d391e987982fbbd3e10adc3949ba59abbe56e057f20f883e72字符地址为0x40A000按X键查看交叉引用发现sub_4011A0函数在0x4011A0处调用strcmp第一个参数为0x40A000F5反编译sub_4011A0确认其逻辑为return strcmp(a1, a94...3e);。此时已锁定flag的最终形态。记录0x40A000地址后续所有分析都以此为终点回溯。4.3 第二步逆向输入源与哈希计算链从sub_4011A0向上回溯在sub_4011A0入口处下断点运行CrackRTF.exe test程序停住查看栈顶[esp]指向a1参数即待比对的字符串地址如0x12FFC0按CtrlG跳转到0x12FFC0观察内存前40字节为test的SHA1 hex后32字节为对应MD5 hex按U键取消反汇编查看0x12FFC0的上一级调用者——sub_401080在sub_401080末尾ret指令前下断点再次运行停住后观察[esp4]a2参数指向的缓冲区确认其内容与0x12FFC0一致继续向上追溯sub_401080的a1参数来自main函数的v4而v4由sub_401000填充分析sub_401000调用GetCommandLineA后用sscanf提取第一个空格前的字符串存入v4。至此输入源确认为命令行参数。哈希链路为argv[1]→SHA1()→MD5()→hex拼接→strcmp。4.4 第三步构造flag并验证已知哈希拼接值需找到满足条件的argv[1]。由于SHA1MD5是确定性函数可编写Python脚本暴力穷举常见密码import hashlib import itertools import string target_sha1_hex a94a8fe5ccb19ba61c4c0873d391e987982fbbd3 target_md5_hex e10adc3949ba59abbe56e057f20f883e # 尝试常见弱口令 common_passwords [password, 123456, admin, root, test, guest] for pwd in common_passwords: sha1_digest hashlib.sha1(pwd.encode()).digest() md5_of_sha1 hashlib.md5(sha1_digest).hexdigest() if md5_of_sha1 target_md5_hex: print(fFound! input{pwd}, flagflag{{{pwd}}}) break运行输出Found! inputpassword, flagflag{password}。验证在CMD中执行CrackRTF.exe password程序输出Congratulations! flag{password}。注意脚本中hashlib.sha1(pwd.encode()).digest()必须用.digest()获取原始字节若用.hexdigest()则得到40字符字符串MD5计算结果会完全不同。4.5 进阶验证用GDB在WSL中复现全流程为验证跨平台一致性在WSL Ubuntu 20.04中操作安装winesudo apt install wine使Windows PE可在Linux运行用gdb加载gdb ./CrackRTF.exe设置架构(gdb) set architecture i386下断点(gdb) b *0x4011A0比对函数入口运行(gdb) r password断住后查看寄存器(gdb) x/s $eax$eax指向比对字符串确认其值为a94a8fe5...继续运行(gdb) c程序输出flag。此过程证明解法与操作系统无关核心逻辑完全由二进制自身决定。5. 常见问题与排查技巧实录5.1 典型问题速查表问题现象根本原因排查方法解决方案程序运行后立即退出无任何输出sub_4011A0比对失败strcmp返回非零值在sub_4011A0入口下断点检查[esp]指向的字符串内容确认输入字符串正确注意大小写和空格调试时断点不触发ASLR开启导致基址随机化在x64dbg中Options → Debugging Options → Events → Uncheck Enable ASLR关闭ASLR后重新加载sub_401040函数反编译失败显示乱码IDA未正确识别SHA1算法库函数手动指定函数签名右键sub_401040→Edit function→Set function type→ 输入void __cdecl sub_401040(char*, char*)强制IDA按指定签名解析Python脚本计算的MD5与预期不符错误地对SHA1 hex字符串而非原始字节计算MD5print(hashlib.md5(ba94a8f...).hexdigest())vsprint(hashlib.md5(sha1_digest).hexdigest())使用.digest()获取原始字节勿用.hexdigest()strings命令未找到硬编码flag字符串被加密或压缩用binwalk -e CrackRTF.exe提取嵌入数据或用xxd -l 200 CrackRTF.exe | grep -A5 -B5 a94a8f直接十六进制dump搜索不依赖strings5.2 我踩过的三个关键坑坑一误信RTF必须解析第一次做这道题时我花了8小时写了一个RTF解析器试图从\bin数据中提取密钥。直到发现程序根本没调用ReadFile才意识到自己被标题误导。教训标题是线索不是说明书。CTF题目的命名往往带有误导性必须用API调用证据说话而非凭经验猜测。坑二SHA1 hex与字节混淆在Python脚本中我最初用hashlib.sha1(pwd).hexdigest()得到40字符字符串再对其计算MD5结果永远不匹配。调试时用print(len(sha1_result))才发现hexdigest()返回字符串长度40而digest()返回字节长度20。教训哈希函数的.digest()和.hexdigest()是两种完全不同的数据类型前者是二进制后者是文本混用必错。坑三忽略命令行参数空格尝试CrackRTF.exe password带引号时失败因为sscanf的格式串是%s遇到空格即截断引号被当作字符串一部分。正确做法是CrackRTF.exe password无引号。教训CTF题的输入格式通常极简避免任何额外符号除非题目明确要求。5.3 工具链效率优化技巧IDA快捷键提速AltT快速切换文本视图/图形视图;键添加注释N键重命名变量如将v4改为input_strx64dbg内存搜索按CtrlG跳转到地址后按CtrlB搜索字节序列如搜索a9 4a 8f...的十六进制GDB实用命令display /x $eax自动显示EAX寄存器值catch syscall open捕获文件操作info proc mappings查看内存映射批量验证脚本将常见密码存入passwords.txt用for pwd in $(cat passwords.txt); do echo $pwd; ./CrackRTF.exe $pwd; done快速测试。5.4 从CrackRTF延伸的三个实战能力这道题虽小但练就的能力可直接迁移到真实工作PE结构分析能力
上一篇/下一篇内容由系统自动关联
返回资讯列表 →