1. 从零开始:拿到题目文件与初步检查
大家好,我是老张,一个在安全圈摸爬滚打了十来年的老逆向。今天咱们来一起盘一盘BUUCTF平台上的一道经典逆向题——[ACTF新生赛2020]rome。这道题特别适合刚入门逆向的朋友,它没有复杂的混淆和壳,核心就是考察你对基础加密算法的识别和爆破能力。很多新手朋友一看到“逆向”两个字就发怵,觉得要懂汇编、懂底层,其实没那么玄乎。咱们今天就用这道题,手把手带你走一遍从拿到文件到最终爆出flag的完整流程,保证你看完就能上手操作。
首先,咱们得把题目文件拿到手。在BUUCTF平台上下载附件,你会得到一个名为“rome.exe”的可执行文件。拿到任何未知可执行文件,第一步千万别急着双击运行,尤其是在自己的主力机上。我的习惯是,先把它丢到虚拟机或者沙箱环境里。不过对于CTF题目,第一步更常规的操作是进行“文件指纹识别”。这里我用的是Exeinfo PE这个小工具,它轻量又免费,是逆向分析的好帮手。把“rome.exe”拖进Exeinfo PE的窗口,工具会快速分析文件头信息。几秒钟后,结果就出来了:这是一个32位的PE文件,并且最重要的是,它显示为“Microsoft Visual C++”编译,而且无壳。
看到“无壳”这两个字,我心里就踏实了一大半。壳(Pack)就像是给程序穿上的“盔甲”,有压缩壳、加密壳等等,它的存在会干扰我们的静态分析,需要先进行脱壳处理。这道题没加壳,意味着我们可以直接用静态分析工具打开它,查看最原始的代码逻辑,这大大降低了入门门槛。所以,对于逆向新手来说,遇到无壳程序,就等于成功了一半。确认了是32位无壳程序后,我们接下来就要请出逆向分析领域的“瑞士军刀”——IDA Pro了。因为程序是32位的,所以我们打开IDA 32位版本(也就是常说的IDA32),准备开始真正的分析之旅。
2. 静态分析核心:用IDA Pro定位关键逻辑
打开IDA32,把“rome.exe”拖进去。IDA会先进行自动分析,包括识别函数、字符串、数据结构等,这个过程需要一点时间,耐心等待进度条走完。分析完成后,我们会看到IDA的反汇编窗口,里面满是汇编指令,对于新手来说可能有点眼花。别慌,我们有“捷径”可走。
逆向分析中,字符串常量往往是重要的突破口。程序在运行中输出的提示信息、进行比较的密钥、加密后的flag等,常常以明文或某种编码形式存储在程序的静态数据区。在IDA中,我们可以快速查看所有这些字符串。按下快捷键 Shift + F12,就能打开“Strings window”(字符串窗口)。这里列出了程序里所有的字符串常量。我快速浏览了一下,一眼就看到了一个非常可疑的字符串:Qsw3sj_lz4_Ujw@l。这串字符看起来毫无规律,既不像英文单词,也不像常规的Base64编码,非常符合CTF题目中“加密后的flag”的特征。我双击这行字符串,IDA会自动跳转到该字符串在数据段(通常是.rdata段)的存储位置。
接下来,我们需要找到是哪段代码在使用这个字符串。在数据窗口,这个字符串的地址上通常会显示对其的交叉引用(xref)。我们可以在字符串地址上按 X 键,或者在右键菜单中选择“List cross-references to...”。这时,IDA会弹出一个窗口,显示所有引用了这个地址的代码位置。不出所料,只有一个函数引用了它。我们双击这个引用,IDA就会跳转到引用该字符串的汇编代码处。
通常,引用关键字符串的代码就在程序的主逻辑函数附近,甚至可能就是主函数本身。我们按 F5 键,IDA强大的Hex-Rays插件就会将当前函数的汇编代码,反编译成非常易读的C语言伪代码。这简直是逆向工程师的福音,它把晦涩的汇编指令转换成了接近源码的逻辑,极大提升了分析效率。F5之后,一个清晰的函数结构就展现在我们面前。我习惯先快速浏览一下这个函数的整体结构:看到了明显的循环、条件判断以及对那个可疑字符串的字符数组访问(比如 v12[i]),这基本可以确定,我们找对地方了,这就是处理flag加密/解密的核心函数,很可能就是main函数。
3. 深入代码:逆向解析加密算法
现在,我们聚焦到IDA F5生成的伪代码上。我们的目标是把这段代码的逻辑彻底搞清楚。伪代码里定义了一个字符数组,内容正是我们之前看到的 Qsw3sj_lz4_Ujw@l,我们给它起个名字叫 encrypted_flag。然后是一个双重循环结构。
外层循环 for (i = 0; i < 17; ++i) 很容易理解:它遍历 encrypted_flag 字符串的每一个字符,字符串长度是16(索引0到15),但循环条件是 i < 17,这可能是因为字符串末尾还有一个结束符 \0,循环也把它包含了进去,不过在实际比较时,\0字符通常不会匹配我们设定的可打印字符范围,所以不影响结果。
内层循环 for ( j = 32; j <= 126; ++j ) 是关键。它遍历了所有ASCII码中的可打印字符(从空格32到波浪号126)。这意味着,程序在尝试对encrypted_flag中的每一个字符,用所有可能的可打印字符进行一种变换,然后看变换后的结果是否与encrypted_flag中的那个字符相等。
那么变换规则是什么呢?看内层循环里的两个if判断:
if ( j > 64 && j <= 90 )
{
j = (j - 51) % 26 + 65;
}
if ( j > 96 && j <= 122 )
{
j = (j - 79) % 26 + 97;
}
这里要注意,代码里直接对循环变量 j 进行了修改,在分析时我们最好引入一个临时变量(比如 x)来理解,避免混淆。
第一个if判断:如果 j(即我们尝试的字符的ASCII码)在大写字母‘A’(65)到‘Z’(90)之间(注意条件是 j > 64 && j <= 90),就执行 x = (x - 51) % 26 + 65。这其实是一个典型的凯撒移位变种。(x - 65) 可以得到字母在字母表中的序号(0-25),这里减51再模26,等效于先减 51 % 26 = 25,也就是减25。因为模26运算下,减25等于加1。所以这个变换实际上是:将大写字母在字母表中向后移动1位(Z移动后变成A)。+65 是将数字序号重新转换回ASCII码。
第二个if判断:如果 j 在小写字母‘a’(97)到‘z’(122)之间,执行 x = (x - 79) % 26 + 97。同理,79 % 26 = 1,所以这个变换是:将小写字母在字母表中向后移动1位。
总结一下算法逻辑:对于encrypted_flag中的每个字符,程序会遍历所有可打印字符。对于每个被遍历的字符,如果是英文字母,就将其在字母表内循环后移一位(大写归大写,小写归小写),数字和符号则保持不变。然后,用变换后的字符与encrypted_flag中的对应字符进行比较。如果相等,那么当前遍历的这个原始字符(变换前的 j),就是flag中对应位置的真实字符!
这实际上是一个解密算法的逆向。我们已知加密后的字符串 encrypted_flag,加密规则是“将原文中的英文字母后移一位”。那么解密规则自然就是“将密文中的英文字母前移一位”。程序这里用爆破的方式(遍历所有可能输入)来反向寻找原文。
4. 编写爆破脚本:将分析转化为答案
分析清楚了算法,我们就有多种方法得到flag。最直接的是心算或笔算解密:看到密文字母,就将其前移一位(‘B’变‘A’,‘b’变‘a’),非字母字符不变。但作为CTF练习,同时也是为了应对更复杂的变换,编写一个爆破脚本是更通用、更可靠的方法。而且,题目原始文章给出的就是一段C语言爆破代码,我们也来亲手实现一下,并理解其原理。
根据上面的分析,我们可以用Python快速写一个脚本。思路和伪代码一致:准备密文,遍历每个字符,对于每个可能输入(32-126),应用那个“后移一位”的变换,然后比较。
encrypted = "Qsw3sj_lz4_Ujw@l"
flag = ""
for enc_char in encrypted:
for possible_ascii in range(32, 127): # 遍历所有可打印字符
x = possible_ascii
# 如果是大写字母,后移一位
if 65 <= possible_ascii <= 90: # 'A' to 'Z'
x = (possible_ascii - 65 + 1) % 26 + 65
# 如果是小写字母,后移一位
elif 97 <= possible_ascii <= 122: # 'a' to 'z'
x = (possible_ascii - 97 + 1) % 26 + 97
# 如果变换后的字符等于密文中的字符
if chr(x) == enc_char:
flag += chr(possible_ascii) # 则原始字符就是flag的一部分
break # 找到后跳出内层循环,处理下一个密文字符
print("Flag is: flag{" + flag + "}")
运行这个脚本,瞬间就能得到输出:Flag is: flag{Cae3ar_th4_Gre@t}。
等等,这里有个细节!我们对比一下原始文章给出的C代码。它的变换公式是 (x - 51) % 26 + 65 和 (x - 79) % 26 + 97。我们刚才分析,这等价于“后移一位”。但我们自己写的Python脚本,直接用了“后移一位”的逻辑 (ord-65+1)%26+65。这两者数学上是等价的吗?我们来验证一下:对于大写字母‘C’,ASCII是67。按照C代码:(67-51)=16,16%26=16,16+65=81 即‘Q’。按照我们的“后移一位”逻辑:‘C’后移一位是‘D’,ASCII是68,不对。哦,这里我犯了个错误!密文是Q,原始文章代码是用j(可能输入)去变换,然后看是否等于密文。所以这个变换是从可能输入到密文的。如果输入是‘C’(67),变换后得到了‘Q’(81)。这可不是后移一位,这是一个特定的映射。
让我们重新审视:(67 - 51) % 26 + 65 = (16) % 26 + 65 = 16 + 65 = 81。所以这个公式把‘C’映射成了‘Q’。这更像是一个凯撒加密的变种,但密钥不是1。(x - 51) % 26 等价于 (x - (51%26)) % 26 即 (x - 25) % 26。在模26运算中,减25等于加1。所以 (x - 25) % 26 + 65 等价于 (x + 1) % 26 + 65?不对,(x - 25) mod 26 和 (x + 1) mod 26 是相等的。因为 -25 ≡ 1 (mod 26)。所以,(x - 51) % 26 + 65 确实等价于 (x + 1) % 26 + 65!也就是说,对于大写字母,它执行了 (x + 1) % 26 + 65。这不是“后移一位”,这是“前移一位”吗?(x+1) 才是后移。我们验证:输入‘C’(67),(67+1)=68,68%26=16? 等等,68除以26余16,16+65=81,确实是‘Q’。所以这个公式是:先给字母序号加1(后移一位),然后模26,最后加65转回ASCII。这正是标准的凯撒加密(移位为1)!只不过它用了一个绕一点的写法 -51。
同理,小写字母的 (x - 79) % 26 + 97,因为 -79 ≡ -1 ≡ 25 (mod 26),所以等价于 (x + 25) % 26 + 97,也就是 (x - 1) % 26 + 97?这里有点绕。我们验算:对于小写字母‘a’(97),(97-79)=18,18%26=18,18+97=115 即‘s’。如果按(x-1)算,‘a’前移一位是‘’,不对。我们直接计算 (97 + 25) % 26 + 97 = (122) % 26 + 97 = 18 + 97 = 115,也是‘s’。所以小写部分的 -79等价于+25,而 +25在模26下等价于-1`。所以小写字母的变换是 前移一位。
结论:这个加密算法对大写字母后移一位,对小写字母前移一位,对数字符号不变。那么解密算法(即从密文反推原文)就应该是:大写字母前移一位,小写字母后移一位。我们根据这个正确的解密逻辑,修改Python脚本:
encrypted = "Qsw3sj_lz4_Ujw@l"
flag = ""
for enc_char in encrypted:
found = False
for possible_ascii in range(32, 127):
x = possible_ascii
# 加密过程(用于与密文比较)
if 65 <= possible_ascii <= 90: # 大写字母,加密时后移
x = (possible_ascii - 65 + 1) % 26 + 65
elif 97 <= possible_ascii <= 122: # 小写字母,加密时前移
x = (possible_ascii - 97 - 1) % 26 + 97 # 注意是-1
# 比较
if chr(x) == enc_char:
flag += chr(possible_ascii)
found = True
break
if not found: # 处理非字母字符(如数字、符号)
flag += enc_char
print("Flag is: flag{" + flag + "}")
运行后,我们得到 Flag is: flag{Cae3ar_th4_Gre@t}。和原始文章的结果一致。对于数字‘3’、‘4’和符号‘_’、‘@’,它们在加密过程中没有变化,所以在爆破时,内层循环会直接命中(当 possible_ascii 等于其本身ASCII时,x不变,等于密文字符),因此也能正确还原。
5. 算法还原与深度理解:凯撒密码的变种
通过上面的爆破,我们拿到了flag。但作为一个学习者,我们不应该止步于此。我们应该能正面推导出加解密算法,而不仅仅是暴力破解。从爆破过程中,我们反向推导出了加密规则:大写字母后移1位,小写字母前移1位。这是一个带大小写区分的、移位方向相反的凯撒密码变种。
标准的凯撒密码是所有的字母都朝同一个方向移动固定的位置。而这道题玩了个小花样,让大小写字母的移动方向相反。这可能是为了增加一点迷惑性,但本质上依然是单表替换密码,安全性非常弱,通过爆破或频率分析很容易破解。
为什么我们能用爆破?因为密钥空间极小。对于每个字符,如果是字母,其可能性只有26种(大小写分开算);即使遍历所有可打印字符,也才95种可能。对于长度仅为16的字符串,总的尝试次数最多是 95^16,看起来很大,但因为我们是对每个字符独立进行爆破(算法中没有跨字符的依赖),所以实际复杂度是 O(n*m),其中n是字符串长度,m是字符集大小(95)。在计算机上,这几乎是瞬间完成的。
在真正的逆向工程中,我们遇到的情况会比这复杂得多。但这道题提供了一个完美的入门范例:定位关键字符串 -> 反编译主函数 -> 分析算法逻辑 -> 编写解密脚本。这个流程是通用的。当你掌握了这个流程,再遇到更复杂的保护(如混淆、壳、反调试、多线程),你就有了一套基础的应对框架:先想办法脱壳、去混淆,让代码可读,然后再套用这个分析流程。
6. 举一反三:扩展思考与实战建议
做完这道题,我们可以再深入思考几个问题,这对提升逆向能力很有帮助。
第一,如果加密算法更复杂怎么办? 比如,移位量不是固定的1,而是依赖于字符在字符串中的位置(i),或者依赖于一个密钥字符串。我们的爆破方法依然有效,但需要调整内层循环。如果算法是可逆的,我们更推荐直接编写解密函数,而不是爆破。例如本题,我们可以直接写出解密算法:遍历密文,大写字母减1,小写字母加1。这样效率更高,代码也更优雅。爆破是在算法复杂或难以直接逆向时的“万能钥匙”。
第二,如何验证自己的分析? 一个很好的方法是动态调试。我们可以用x32dbg或OllyDbg附加到“rome.exe”进程,在关键比较指令处下断点,单步跟踪寄存器和内存值的变化,亲眼看到加密/解密过程。动态调试能直观地验证静态分析的结论。对于这道题,你可以在内存中看到 encrypted_flag 字符串,然后跟踪循环,观察每个候选字符经过计算后是否与密文字符相等。这个过程能让你对程序运行时的行为有更深的理解。
第三,关于IDA的使用技巧。 本题我们用了 Shift+F12 查字符串和 F5 反编译,这是最常用的两个功能。除此之外,你还需要熟悉重命名变量(按N键)、添加注释(按冒号:键)、修改函数原型(按Y键)等操作。良好的命名和注释能让伪代码的可读性大大提升。比如,把 v12 重命名为 encrypted_flag,把循环变量 i 重命名为 index,一眼就能看懂代码在做什么。
第四,CTF逆向题的常见套路。 像“rome”这类题目,往往暗示了加密算法。“rome”(罗马)很容易让人联想到“凯撒”(Caesar),因为凯撒密码起源于古罗马。所以,看到题目名、函数名、字符串中的一些提示(比如本题flag里的“Cae3ar”),要大胆猜测可能用了古典密码。常见的还有“base64”、“xor”、“rabbit”、“tea”等关键词,都可能直接指向了所用的加密算法。
最后,给新手朋友一点实战建议:逆向工程就像解谜,需要耐心和细心。不要被庞大的汇编代码吓倒,总是从入口点(main函数)、输入输出(字符串、文件、网络)和关键判断(if、switch、比较指令)入手。多动手,多写脚本,哪怕最开始只是照猫画虎。像今天这样,分析一道题,就彻底吃透它的算法、它的漏洞、它的解题思路,积累下来,你的能力就会在不知不觉中快速增长。这道“[ACTF新生赛2020]rome”就是一个非常棒的起点,它几乎涵盖了入门逆向所需的所有基础步骤。希望这篇详细的拆解能帮你打开逆向世界的大门。
1050



被折叠的 条评论
为什么被折叠?



