☰
CTF逆向实战:从RC4到Base64的easyRE完整解析
2026/10/7 12:29:22 网站建设 项目流程

1. 拿到文件后的第一轮判断:先跑起来,别急着反编译

刷到[2019红帽杯]的easyRE时,我原本以为是道送分题——毕竟名字就叫easyRE,逆向嘛,无非是把文件丢进IDA,看几个函数,写个脚本,flag就出来了。真正动手才发现,这道题把“逆向基本功”串得非常紧凑:算法识别、密钥提取、动态调试,甚至还有一手静态分析陷阱。所以这篇文章不是单纯贴答案,而是把我当时在比赛里分析这道题的完整路径捋一遍,重点讲清楚每一步为什么这么做,给同样卡在RC4、Base64变体或者动态调试上的同学留个参考。

1.1 先别打开IDA,先看这个文件是什么

拿到附件后的第一件事,我习惯先跑三个基础命令:

file easyRE checksec --file= easyRE strings easyRE | head -50

在我复现的版本里,file输出大概是:

easyRE: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=..., not stripped

这条信息很关键:64位、动态链接、而且not stripped,意味着符号表还在,main、printf、strcmp这些函数名全部留着,IDA和GDB解析起来会省很多力气。

checksec则告诉我们防护情况。我当时看到的典型结果是:

项目结果说明
Archamd64-64-little小端序,脚本里读写数据要按小端处理
NXNX enabled栈不可执行,但不影响这道题
PIEPIE disabled基址固定,静态地址可以直接用
RELROPartial RELROGOT可写,不过本题跟它关系不大
CanaryNo canary found栈上没金丝雀,函数里如果有缓冲区溢出可以玩,但本题是逻辑校验

看到PIE disabled我比较安心,因为IDA里显示的地址和运行时地址一致,下断点、看内存都方便。如果开了PIE,后续还得换算基址。

1.2 跑一遍,观察交互行为

命令行的ELF程序,直接运行一次永远不亏:

./easyRE

程序会输出一句Input your flag:,然后等着你输入。随便输一个字符串,我输了test_flag_here,程序回了一句Wrong!就退出了。

到这里,虽然还不知道内部逻辑,但信息已经不少:

  • 输入是标准输入,不是命令行参数;
  • 输出有明确的对/错提示,说明存在一个字符串比较或者返回值判断;
  • 大概率是先处理输入,再和某个固定密文比较。

这种“先收集行为,再决定从哪里下手”的习惯很重要。很多新手一上来就F5,对着伪代码发懵,其实先跑一次能帮你确认输入输出接口,后面静态分析时你会更快找到关键分支。

2. 静态分析咬住主逻辑:从输入比较点到三个关键函数

2.1 IDA定位main,别在入口函数里绕圈

用IDA打开后,因为程序没有strip,main直接在函数列表里。双击之后,Hex-Rays立刻给出伪代码。我这里直接说最常见的结构:

int __cdecl main(int argc, const char **argv, const char **envp) { char input[64]; unsigned int len; printf("Input your flag:"); scanf("%s", input); len = strlen(input); if ( len != 24 ) { puts("Wrong!"); return 0; } sub_400A80(input, len); // 第一段处理 sub_400B20(input, len); // 第二段处理 if ( !strcmp(input, "expected_data") ) puts("Right!"); else puts("Wrong!"); return 0; }

不同题目地址和变量名会有差异,但这条主干几乎不会变:输入 → 长度检查 → 各种变换 → 与常量比较。看到这个结构后,真正的分析任务就是弄清楚sub_400A80和sub_400B20到底在做什么。

2.2 从交叉引用和字符串快速锁定关键位置

我一般会在IDA里按Shift+F12打开字符串窗口,看有没有Right、Wrong、Input your flag之类的字符串。然后双击Right!所在的地址,按Ctrl+X查看交叉引用,会直接跳到main里的比较分支。

这个方法比漫无目的地翻函数列表高效得多。反编译时如果发现某个函数很可疑,也可以先看它被谁调用。比如sub_400A80只被main调用,那它就是核心处理函数;如果还被一堆系统函数调用,那可能是库函数,不用管。

2.3sub_400A80刚进去时看到的东西

进入sub_400A80后,Hex-Rays可能给出一堆类似v4[v5] = S[v6]的操作。不要被变量名吓到,我当时的做法是逐行读,并记录循环特征:

  • 有没有256或& 0xFF?
  • 有没有for ( i = 0; i <= 255; ++i )?
  • 有没有两个索引互相交换的代码?
  • 有没有xor?

如果同时出现,基本可以判断是RC4。RC4在C语言里的实现太典型了:初始化阶段先把S[i]=i,然后用密钥打乱S盒;加密/解密阶段再用另一个循环生成密钥流,逐字节异或。静态代码里那个256几乎就是最显眼的标签。

sub_400B20则更直接。我在伪代码里看到类似byte_6010C0的全局数组,点进去是一段64个字符的表,而表内容恰好是ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/。看到这串字符,想都不用想,这是标准Base64码表。

这样一来,整体逻辑就浮出水面了:

输入(input) → RC4加密(密钥是程序内嵌的某个key) → Base64编码 → 与内存里预存的密文比较

验证通过的条件是变换后的结果等于预存密文。逆向时只需要反着做:先对预存密文做Base64解码,再用同一个密钥做RC4解密。

3. RC4和Base64的识别:用“指纹”而不是靠猜

3.1 为什么一眼看出RC4:256字节S盒是最大指纹

很多CTF题不直接调OpenSSL,而是把RC4的实现贴在代码里。RC4是一个流密码,加解密完全对称,算法核心只有两部分:

  1. KSA(密钥调度算法):初始化S盒为0..255,再用密钥打乱。
  2. PRGA(伪随机数生成算法):生成密钥流,与明文逐字节异或。

对应到反编译代码,我会找这些特征:

// KSA for (i = 0; i <= 255; i++) S[i] = i; j = 0; for (i = 0; i <= 255; i++) { j = (j + S[i] + key[i % key_len]) & 0xFF; swap(S[i], S[j]); } // PRGA i = j = 0; for (k = 0; k < data_len; k++) { i = (i + 1) & 0xFF; j = (j + S[i]) & 0xFF; swap(S[i], S[j]); data[k] ^= S[(S[i] + S[j]) & 0xFF]; }

只要看到& 0xFF和swap组合,基本就是RC4。我见过有的题目为了混淆,把swap写成三次异或,但256循环和& 0xFF很难隐藏。你在IDA里可以用Alt+T搜十六进制FF配合汇编查找,也可以直接看Hex-Rays里有没有i & 0xFF。

3.2 密钥藏在哪:一个容易被忽略的全局数组

RC4的密钥不一定是char *key = "abc"这种干净形式。这道题的密钥是通过另一个函数生成的,甚至可能被打散放在全局数组里。如果静态代码里能看到明显的密钥字符串,那直接提取;如果看不到,就在动态调试时用GDB在RC4初始化位置打一个断点,看key指针指向的内存区域。

我在写解密脚本时,习惯先把密钥提取成十六进制数组,因为它可能是不可见字符。标准的RC4解密脚本如下:

def rc4(data: bytes, key: bytes) -> bytes: S = list(range(256)) j = 0 for i in range(256): j = (j + S[i] + key[i % len(key)]) & 0xFF S[i], S[j] = S[j], S[i] i = j = 0 out = bytearray() for ch in data: i = (i + 1) & 0xFF j = (j + S[i]) & 0xFF S[i], S[j] = S[j], S[i] out.append(ch ^ S[(S[i] + S[j]) & 0xFF]) return bytes(out)

这里必须强调:RC4加解密共用一个函数,加密用rc4(明文, key),解密也是rc4(密文, key),不用写两个版本。

3.3 Base64解码的细节:标准表与魔改表

标准Base64码表大家都熟:

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

如果静态分析里看到的全局数组和这个完全一致,直接用Python的base64.b64decode就行。但很多赛题喜欢魔改码表,比如把+和/换成别的字符,或者把大小写字母顺序调换。遇到这种情况,需要手动把码表抽出来,替换到Python的解码逻辑里。

我当时判断这是标准表后,先做一个实验:直接把预存密文base64.b64decode,发现解出来的数据不是乱码,而是一段长达几十字节的二进制数据。再把这二进制数据送入rc4(),输出就是可读字符串了。这说明编码和解码顺序判断正确。

如果Base64魔改了码表,解码脚本要写成这样:

import base64 custom_table = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" standard_table = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" # 如果custom_table等于standard_table,直接标准解码

实际上,可以先在IDA里复制Base64处理函数使用的码表地址,再看那段数据。通过对比码表差异,就能快速确定是否有魔改。

3.4 从IDA里导出密文,别一个个手敲

拿到密文后,不要手抄十六进制,效率太低还容易错。我的操作是:在IDA的Hex View里选中目标数据,然后右键选择Export data,或者直接Shift+E,选择C数组格式,复制出来粘贴到Python脚本里。生成的效果是:

unsigned char enc[] = { 0x73, 0x2A, 0x5C, 0x2B, 0x66, 0x1D, 0x0E, 0x4F, 0x38, 0x1B, 0x41, 0x1E, 0x13, 0x5A, 0x49, 0x24, ... };

如果密文不是全局变量而是运行时计算出来的,那就需要从动态调试里dump,这一步放在下一节。

4. 动态调试验证与避开静态分析的雷

4.1 在strcmp处下断点,直接看比较双方

当静态分析已经给出完整的解密路径后,最好用GDB验证一遍,防止被花指令或伪代码误导。我的调试过程通常是:

gdb ./easyRE set disassembly-flavor intel break *0x400B?? # main里调用strcmp前的位置 run < input.txt x/s $rsi # 查看比较的右侧参数 x/s $rdi # 查看左侧参数

在x86-64 SysV调用约定里,strcmp的两个参数分别放在rdi和rsi。如果程序运行到比较点,rsi指向的是一段已经被Base64变换后的字符串,rdi指向待比较的常量,那么静态分析的判断就对了。

如果直接在strcmp的地址下断点,rsi和rdi不一定就是用户输入和常量,因为有些编译器可能把strcmp的调用优化成内联的rep cmps。不过这道题是动态链接,函数调用通常不会被内联,所以直接在strcmp上打断点也有效:

break strcmp run x/s $rdi x/s $rsi

4.2 动态调试时地址对不上?PIE又在搞事情

如果checksec显示PIE enabled,那么IDA里的静态地址和运行时的实际地址是两回事。GDB里默认开了ASLR,每次调试地址都变。最省事的办法:

set disable-randomization on start info files # 查看入口地址,计算偏移

也可以用pwndbg或gef的vmmap看加载基址,然后用IDA 静态地址 + 基址偏移算出真实断点地址。如果像我复现的这道题一样PIE disabled,那就不存在这个问题,直接对地址下断点即可。

4.3 静态分析里常见的“假分支”和反调试干扰

红帽杯easyRE这类题,有时候会在main里插入一个“看起来是正确路径”的假分支。比如某一段代码先对输入做异或^ 0x66,再和字符串flag{...}比较,如果你的脚本只逆这个异或,会得到一个看似合理的flag,但运行程序验证时却是Wrong!。

原因可能是:

  • 真正的比较逻辑藏在异常处理里,比如使用__cxa_throw触发异常,然后在异常处理函数里完成真实校验;
  • 程序用SMC(自修改代码),运行时改写自身指令,静态IDA看到的只是一堆垃圾字节;
  • 花指令把反编译器骗了,Hex-Rays显示的代码不是实际执行的代码。

遇到这种情况,最好以动态调试为准,直接从比较点回看数据流。不要过分相信静态反编译结果。

4.4 GDB脚本dump内存,省去手录二进制

如果程序运行时才生成密文,可以用GDB脚本把内存里的数据导出来:

dump binary memory enc.bin 0x601080 0x6010A0

然后用Python读取:

data = open('enc.bin', 'rb').read()

这个小技巧看起来简单,却在比赛时能省下大量时间。尤其是数据长度超过几百字节时,手抄粘贴不仅慢而且极易出错。

5. 如果不想纯逆算法:符号执行和暴力枚举的适用边界

5.1 用angr直接求解,为什么我最后没选它

现在很多人在拿到这类题目时,会想到直接用符号执行工具,比如angr。思路非常直接:程序读入一个flag,经过一系列处理后比较,我们把比较条件变成约束,让求解器找出一组满足条件的输入即可。

angr脚本可以写得非常短:

import angr proj = angr.Project('./easyRE') state = proj.factory.entry_state() simgr = proj.factory.simulation_manager(state) simgr.explore(find=0x400XXX, avoid=0x400YYY) if simgr.found: print(simgr.found[0].posix.dumps(0))

但实际操作中很容易遇到问题:程序内部是RC4和Base64,这两个算法循环多、分支多,符号执行会把状态空间撑爆;而且如果程序有反调试或自修改代码,angr默认的静态执行根本到不了真实校验逻辑。我试过一次,求解跑了十分钟还没结果,后来果断放弃,回归手写算法逆向。

5.2 暴力枚举不是万能,但可以配合长度剪枝

如果密钥很短或者只在有限的字符集里枚举,暴力枚举也是一种选择。比如题目是简单的逐字节异或,那根本不用逆算法,直接爆破:

enc = [...] # 密文 for key in range(256): res = bytes([c ^ key for c in enc]) if b'flag' in res: print(res)

但对于RC4这种流密码,密钥长度稍微一长,暴力枚举就是天文数字。所以选择爆破前,先评估一下密钥空间。RC4的密钥空间通常在128位以上,不可能枚举;只能逆出密钥或者从程序里提取。

5.3 这道题的最优解:算法识别加脚本逆算

最终我选择的路径是:

  1. 静态分析识别RC4和Base64;
  2. 从程序里提取密钥和密文;
  3. 用Python脚本做逆运算;
  4. 用GDB在比较处验证结果。

整个过程半小时内完成。符号执行、动态插桩这类高级手段很好,但没必要。比赛中时间有限,够用且可控的解法就是好解法。

6. 从easyRE里提炼的通用经验:工具、脚本和心态

6.1 常用工具清单

做这类逆向题,我的基础工具箱就这几样:

工具用途优先级
IDA Pro 7.x静态反编译、Hex-Rays必装
file / checksec / strings基础信息收集必用
GDB + pwndbg动态调试、内存dump必用
Python3 + pwntools编写解密脚本、交互输入必用
angr特殊情况下的符号执行备用
CyberChefBase64、Hex快速转换辅助

6.2 写解密脚本时的几个坑

我在这里把最容易踩的坑集中列一下:

  • 字节序:从IDA导出的DWORD/QWORD数组,转成Python bytes时要注意小端序。推荐直接导出C数组,而不是导出十六进制字符串再手动处理。
  • Key长度:使用RC4时,密钥可能是变长的,必须从程序里提取准确长度。有的程序会在RC4函数前调用strlen,此时密钥末尾不能有多余的\x00。
  • 输入截断:CTF程序经常用scanf("%s")读输入,这意味着如果flag本身包含空格或特殊字符,输入会被截断。分析时要注意比较成功的flag里是否有不可见字符。
  • Base64顺序:先RC4再Base64,和先Base64再RC4,逆运算顺序完全不同。不要想当然,一定要从主函数逻辑里确认调用顺序。

6.3 比赛里的战术选择

复盘这道easyRE时,我最大的感受是:逆向题考的不是你会多少高级工具,而是你能不能快速把“输入到输出”的变换关系拎清楚。算法识别靠经验,经验靠多刷题总结。RC4看256,Base64看码表,异或看常数,AES看S盒和轮数,这些指纹背下来之后,很多题目在静态分析阶段就能破掉一大半。

另外,不要在某个点上死磕太久。如果静态分析看了二十分钟还在原地打转,果断开动态调试,或者用GDB单步跟一次数据流,往往比反复读伪代码更高效。

最后分享一个我个人的习惯:每做一道逆向题,我会把题目的变换流程图和最终解密脚本存进自己的笔记。因为CTF的考点翻来覆去就那么几类,第一次手动分析RC4用了四十分钟,第二次再遇到同类题目,十分钟就能搞定。easyRE这道题虽然名字带easy,但作为练习逆向基本功的样例,性价比真的很高。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询