文章总结: 这篇文章详细解析了一道名为消失的岛屿的CTF密码学题目,通过逆向工程分析了一个使用魔改Base64编码和字符映射变换的加密算法,文章使用radare2工具进行静态分析,深入理解了charEncrypt函数的五种转换规则,最终设计出完整的解密方案并提供了Python脚本,成功解密得到flag,是学习CTF密码学和逆向工程的优秀案例。 综合评分: 90 文章分类: CTF,二进制安全,WEB安全,逆向分析,密码学
CTF密码学题目深度解析:消失的岛屿(LostIslands)
原创
破镜安全
破镜安全
2025年12月15日 08:00 四川
CTF密码学题目深度解析:消失的岛屿(LostIslands)
前言
在CTF(Capture The Flag)竞赛中,密码学题目往往需要我们综合运用逆向工程、密码学知识和编程能力。本文将详细分析一道名为”消失的岛屿”(LostIslands)的密码学题目,从零开始,一步步带领读者完成从逆向分析到成功解密的全过程。
这道题目巧妙地结合了Base64编码和多层字符映射变换,是学习密码学和逆向工程的优秀案例。文章将详细讲解每一步的分析思路、使用的工具和技术原理,确保即使是初学者也能够理解并掌握。
一、题目初探:了解我们面对的是什么
1.1 题目文件基本信息
拿到题目后,我们首先需要了解文件的基本属性。使用Linux系统自带的file命令:
$ file LostIslands.exe
LostIslands.exe: PE32 executable (console) Intel 80386, for MS Windows, 12 sections
这个命令告诉我们:
- 这是一个Windows可执行文件(PE32格式)
- 是32位程序(Intel 80386架构)
- 是控制台程序(console),意味着它运行在命令行界面
- 包含12个节(sections)
我们还可以计算文件的MD5哈希值,用于验证文件完整性:
$ md5sum LostIslands.exe
8ef175759b0abbfd8f613fa428c78cb9 LostIslands.exe
1.2 提取可读字符串:寻找线索
在逆向分析的第一步,我们通常使用strings命令提取程序中的可读字符串。这个命令可以快速找到程序中硬编码的文本信息,往往能提供重要线索。
$ strings LostIslands.exe | grep -E "(flag|base64|TABLE|tuvwx)" | head -20
执行这个命令后,我们发现了几个关键信息:
发现1:一个不寻常的字符串
tuvwxTUlmnopqrs7YZabcdefghij8yz0123456VWXkABCDEFGHIJKLMNOPQRS9+/
这个字符串长度为64个字符,包含大小写字母、数字以及特殊字符”+”和”/”。熟悉Base64编码的读者会立即意识到:标准的Base64码表也是64个字符!但这个码表的顺序明显被打乱了,这是一个魔改的Base64码表。
标准Base64码表应该是:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
发现2:疑似密文
!NGV%,$h1f4S3%2P(hkQ94==
这个字符串有几个Base64的典型特征:
- 末尾有两个等号(==),这是Base64的填充符号
- 长度为24个字符(Base64编码后的长度通常是4的倍数)
- 包含各种奇怪的字符
但它显然不是标准Base64编码,因为标准Base64只包含字母、数字、+、/和=,而这里出现了”!”、”%”、”,”、”$”等字符。
发现3:相关函数名
_flag
base64_encode
_base64_encode
charEncrypt
这些函数名透露了程序的核心功能:
base64_encode:进行Base64编码charEncrypt:进行字符加密/转换_flag:可能与flag相关
发现4:用户交互信息
please enter Serial:
Success
Please Try Again
error
这说明程序会提示用户输入一个Serial(序列号),然后进行验证。
1.3 初步分析总结
通过简单的字符串提取,我们已经得到了重要信息:
- 程序使用了魔改的Base64编码
- 存在一个需要验证的密文
- 有一个名为charEncrypt的函数进行字符转换
- 程序会读取用户输入并进行验证
现在,我们需要深入分析程序的逻辑,理解加密算法的具体实现。
二、深入分析:使用radare2逆向工程
2.1 为什么选择radare2
在逆向工程中,我们有多种工具可选:
- IDA Pro:功能强大但商业软件,价格昂贵
- Ghidra:NSA开源的反编译器,功能完善
- radare2:开源、免费、命令行工具,功能强大
本文选择radare2进行分析,它可以帮助我们:
- 反汇编程序代码
- 分析程序结构
- 查看函数调用关系
- 追踪数据流
2.2 定位关键函数
首先,我们使用radare2分析程序,找出所有函数:
$ r2 -q -c "aaa; afl" LostIslands.exe | grep -E "(main|charEncrypt|base64)"
命令解释:
-
r2:启动radare2 -
-q:安静模式(减少输出) -
-c "aaa; afl":执行两个命令 -
aaa:分析所有函数(analyze all) -
afl:列出所有函数(list functions)
输出结果:
0x004013c0 15 136 dbg.charEncrypt
0x00401448 8 474 dbg.base64_encode
0x00401622 6 224 dbg.main
这个输出给了我们三个关键信息:
| 函数名 | 地址 | 大小(字节) | 分析 | | — | — | — | — | | charEncrypt | 0x004013c0 | 136 | 相对较小,可能是简单的字符转换 | | base64_encode | 0x00401448 | 474 | 较大,包含完整的编码逻辑 | | main | 0x00401622 | 224 | 主函数,协调整体流程 |
函数大小可以帮助我们判断其复杂度。charEncrypt只有136字节,说明它的逻辑相对简单,是个突破口。
2.3 分析charEncrypt函数:核心加密逻辑
现在,让我们深入分析charEncrypt函数。这个函数名暗示它负责字符的加密转换。
$ r2 -q -c "aaa; s dbg.charEncrypt; pdf" LostIslands.exe
命令解释:
s dbg.charEncrypt:跳转(seek)到charEncrypt函数pdf:打印反汇编代码(print disassemble function)
2.3.1 函数序言:建立栈帧
0x004013c0: push ebp ; 保存调用者的栈帧基址
0x004013c1: mov ebp, esp ; 建立新的栈帧
0x004013c3: sub esp, 0x10 ; 在栈上分配16字节的局部变量空间
0x004013c6: mov dword [var_4h], 0x404064 ; 将地址0x404064存入局部变量
让我解释这段代码在做什么:
栈帧的概念:在x86架构中,每个函数调用都会在栈上建立一个”栈帧”,用于存储:
- 函数参数
- 返回地址
- 局部变量
关键发现:0x404064这个地址! 这正是我们之前发现的魔改Base64码表的存储位置。程序将这个地址保存到局部变量中,说明函数会用到这个码表。
2.3.2 读取码表字符
0x004013cd: mov edx, dword [arg_8h] ; edx = 函数参数(索引值)
0x004013d0: mov eax, dword [var_4h] ; eax = 码表地址 (0x404064)
0x004013d3: add eax, edx ; eax = 码表地址 + 索引
0x004013d5: movzx eax, byte [eax] ; 从计算出的地址读取一个字节(字符)
0x004013d8: movsx eax, al ; 将字节符号扩展为32位整数
0x004013db: mov dword [arg_8h], eax ; 将字符的ASCII值保存回参数位置
这段代码的逻辑:
- 从参数中获取一个索引值
- 用这个索引在魔改码表中查找对应的字符
- 将字符的ASCII值保存起来
为什么要这样做?这是典型的数组访问操作:char = table[index]。函数接收一个索引,返回码表中该位置的字符,然后对这个字符进行转换。
2.3.3 第一种转换:大写字母(A-Z)
0x004013de: cmp dword [arg_8h], 0x40 ; 比较:字符 > 64 (ASCII '@')
0x004013e2: jle 0x4013fa ; 如果 <= 64,跳过这段代码
0x004013e4: cmp dword [arg_8h], 0x5a ; 比较:字符 <= 90 (ASCII 'Z')
0x004013e8: jg 0x4013fa ; 如果 > 90,跳过这段代码
0x004013ea: mov eax, 0x9b ; eax = 155
0x004013ef: sub eax, dword [arg_8h] ; eax = 155 - 字符的ASCII值
0x004013f2: mov dword [arg_8h], eax ; 保存转换结果
0x004013f5: mov eax, dword [arg_8h]
0x004013f8: jmp 0x401446 ; 跳转到函数返回
让我详细解释这段代码的逻辑:
条件判断:
- 第一个判断:字符 > 64(即 >= 65)
- 第二个判断:字符 <= 90
- 综合起来:65 <= 字符 <= 90
查看ASCII表,我们知道:
- ‘A’ = 65
- ‘Z’ = 90
所以这段代码处理的是大写字母A-Z。
转换公式:
new_char = 155 - old_char
让我们验证几个例子:
- ‘A’ (65) → 155 – 65 = 90 → ‘Z’
- ‘T’ (84) → 155 – 84 = 71 → ‘G’
- ‘Z’ (90) → 155 – 65 = 65 → ‘A’
为什么用155?这是一个精心设计的数字。注意:65 + 90 = 155。这个公式实际上是在做镜像映射:
- A ↔ Z
- B ↔ Y
- C ↔ X
- …
这是一种对称的转换,大写字母表被”翻转”了。
2.3.4 第二种转换:小写字母(a-z)
0x004013fa: cmp dword [arg_8h], 0x60 ; 比较:字符 > 96 (ASCII '`')
0x004013fe: jle 0x40140f ; 如果 <= 96,跳过
0x00401400: cmp dword [arg_8h], 0x7a ; 比较:字符 <= 122 (ASCII 'z')
0x00401404: jg 0x40140f ; 如果 > 122,跳过
0x00401406: subl dword [arg_8h], 0x40 ; 字符 = 字符 - 64
0x0040140a: mov eax, dword [arg_8h]
0x0040140d: jmp 0x401446 ; 跳转到函数返回
条件判断:
- 97 <= 字符 <= 122
- 这对应小写字母 ‘a’ (97) 到 ‘z’ (122)
转换公式:
new_char = old_char - 64
让我们验证几个例子:
- ‘a’ (97) → 97 – 64 = 33 → ‘!’ (感叹号)
- ‘t’ (116) → 116 – 64 = 52 → ‘4’
- ‘u’ (117) → 117 – 64 = 53 → ‘5’
- ‘z’ (122) → 122 – 64 = 58 → ‘:’
为什么减64?64正好是大写字母’@’的ASCII值。减去64后:
- ‘a’变成了33(’!’)
- ‘z’变成了58(’:’)
小写字母被映射到了可打印的特殊字符区域。这种映射使得结果看起来像是随机字符,增加了破解难度。
2.3.5 第三种转换:数字(0-9)
0x0040140f: cmp dword [arg_8h], 0x2f ; 比较:字符 > 47 (ASCII '/')
0x00401413: jle 0x401424 ; 如果 <= 47,跳过
0x00401415: cmp dword [arg_8h], 0x39 ; 比较:字符 <= 57 (ASCII '9')
0x00401419: jg 0x401424 ; 如果 > 57,跳过
0x0040141b: add dword [arg_8h], 0x32 ; 字符 = 字符 + 50
0x0040141f: mov eax, dword [arg_8h]
0x00401422: jmp 0x401446 ; 跳转到函数返回
条件判断:
- 48 <= 字符 <= 57
- 这对应数字 ‘0’ (48) 到 ‘9’ (57)
转换公式:
new_char = old_char + 50
让我们验证几个例子:
- ‘0’ (48) → 48 + 50 = 98 → ‘b’
- ‘7’ (55) → 55 + 50 = 105 → ‘i’
- ‘9’ (57) → 57 + 50 = 107 → ‘k’
为什么加50?加上50后,数字被映射到了小写字母区域:
- ‘0’变成了’b’
- ‘9’变成了’k’
这种映射使得原本的数字在最终密文中变成了字母,进一步混淆了原始数据。
2.3.6 第四种转换:特殊字符’+’
0x00401424: cmp dword [arg_8h], 0x2b ; 比较:字符 == 43 (ASCII '+')
0x00401428: jne 0x401436 ; 如果不等于43,跳过
0x0040142a: mov dword [arg_8h], 0x77 ; 字符 = 119 (ASCII 'w')
0x00401431: mov eax, dword [arg_8h]
0x00401434: jmp 0x401446 ; 跳转到函数返回
转换规则:
'+' (43) → 'w' (119)
为什么单独处理’+’?在标准Base64编码中,’+’是码表的第62个字符。这里将它固定映射为’w’,确保在最终密文中不会出现’+’字符。
2.3.7 第五种转换:特殊字符’/’
0x00401436: cmp dword [arg_8h], 0x2f ; 比较:字符 == 47 (ASCII '/')
0x0040143a: jne 0x401443 ; 如果不等于47,跳过
0x0040143c: mov dword [arg_8h], 0x79 ; 字符 = 121 (ASCII 'y')
转换规则:
'/' (47) → 'y' (121)
为什么单独处理’/’?在标准Base64编码中,’/’是码表的第63个字符。将它映射为’y’,同样是为了混淆最终结果。
2.3.8 charEncrypt函数总结
通过完整的汇编分析,我们完全理解了charEncrypt函数的5种转换规则:
| 序号 | 输入字符类型 | ASCII范围 | 汇编特征 | 转换公式 | 实例 |
| — | — | — | — | — | — |
| 1 | 大写字母 | 65-90 | mov $0x9b; sub | chr(155 – ascii) | T(84) → G(71) |
| 2 | 小写字母 | 97-122 | subl $0x40 | chr(ascii – 64) | t(116) → 4(52) |
| 3 | 数字 | 48-57 | add $0x32 | chr(ascii + 50) | 7(55) → i(105) |
| 4 | ‘+’ | 43 | mov $0x77 | 固定为’w'(119) | + → w |
| 5 | ‘/’ | 47 | mov $0x79 | 固定为’y'(121) | / → y |
设计目的分析:
- 混淆原始数据:通过不同的数学变换,使得原始字符与转换后字符之间的关系不明显
- 保持可打印性:所有转换后的字符都是可打印字符,便于存储和传输
- 增加分析难度:使用不同的变换规则处理不同类型的字符,增加了逆向分析的复杂度
2.4 分析main函数:程序主流程
了解了charEncrypt的工作原理后,我们需要理解程序的整体流程。让我们分析main函数:
$ r2 -q -c "aaa; s dbg.main; pdf" LostIslands.exe
2.4.1 用户输入部分
0x00401630: mov dword [esp], str.please_enter_Serial:
0x00401637: call sym._printf ; 打印提示信息
0x0040163c: lea eax, [str1] ; 获取输入缓冲区地址
0x00401640: mov dword [size], eax
0x00401644: mov dword [esp], 0x4040ba ; " %s" 格式字符串
0x0040164b: call sym._scanf ; 读取用户输入
这段代码很直观:
- 打印提示信息”please enter Serial:”
- 准备一个缓冲区
- 使用scanf读取用户输入的字符串
2.4.2 输入验证
0x00401650: lea eax, [str1] ; 获取输入字符串地址
0x00401657: call sym._strlen ; 计算字符串长度
0x0040165c: cmp eax, 0x31 ; 比较长度是否 <= 49
0x0040165f: jbe 0x40166d ; 如果 <= 49,继续执行
0x00401661: mov dword [esp], str.error ; 否则输出"error"
长度检查:程序检查输入长度是否不超过49字节(0x31 = 49)。这是为了防止缓冲区溢出。
2.4.3 内存分配
0x0040166d: mov dword [size], 0x400 ; 大小 = 1024字节
0x00401675: mov dword [esp], 1 ; 元素数量 = 1
0x0040167c: call sym._calloc ; 分配内存
0x00401681: mov dword [base64_str], eax ; 保存指针
程序调用calloc分配了1024字节的内存,用于存储Base64编码后的结果。
为什么是1024字节?Base64编码会使数据量增加约33%。如果输入最多49字节,编码后大约65字节,1024字节足够了。
2.4.4 Base64编码
0x00401685: lea eax, [str1] ; 输入字符串地址
0x00401689: mov dword [esp], eax ; 参数1:输入
0x0040168c: call sym._strlen ; 获取长度
0x00401691: mov dword [var_8h], eax ; 参数2:长度
0x00401695: mov eax, dword [base64_str]
0x00401699: mov dword [size], eax ; 参数3:输出缓冲区
0x004016a4: call dbg.base64_encode ; 调用编码函数
这里调用了base64_encode函数,传入三个参数:
- 输入字符串
- 输入长度
- 输出缓冲区
2.4.5 密文比较
0x004016a9: mov dword [str], str._NGV% ; "!NGV%,$h1f4S3%2P(hkQ94=="
0x004016b1: mov eax, dword [base64_str] ; 编码结果
0x004016b5: mov dword [size], eax
0x004016b9: mov eax, dword [str] ; 硬编码的密文
0x004016bd: mov dword [esp], eax
0x004016c0: call sym._strcmp ; 字符串比较
0x004016c5: test eax, eax ; 检查比较结果
0x004016c7: jne 0x4016d7 ; 如果不相等,跳转到失败分支
0x004016c9: mov dword [esp], str.Success ; 相等则输出"Success"
关键逻辑:
- 将用户输入进行魔改Base64编码
- 将编码结果与硬编码密文
!NGV%,$h1f4S3%2P(hkQ94==比较 - 如果相等,输出”Success”;否则输出”Please Try Again”
2.5 分析base64_encode函数:编码实现
base64_encode函数实现了完整的Base64编码流程,但使用了经过charEncrypt转换的码表。
0x00401448: push ebp
0x00401449: mov ebp, esp
0x0040144b: push ebx
0x0040144c: sub esp, 0x14 ; 分配局部变量空间
函数的核心循环逻辑(简化说明):
- 读取3个字节的输入
0x00401462: mov edx, dword [var_8h] ; i = 当前位置
0x00401465: mov eax, dword [arg_8h] ; 输入数据地址
0x00401468: add eax, edx ; 当前字节地址
0x0040146a: movzx eax, byte [eax] ; 读取一个字节
- Base64编码转换(3字节→4字符)
- 将3个字节(24位)分成4组,每组6位
- 每组6位的值范围是0-63,正好对应Base64码表的索引
- 调用charEncrypt转换
0x0040148f: call dbg.charEncrypt ; 第1次调用
...
0x004014cd: call dbg.charEncrypt ; 第2次调用
...
0x00401533: call dbg.charEncrypt ; 第3次调用
在编码过程中,每个Base64索引值都会通过charEncrypt函数转换。
- 处理填充
0x004014e4: mov byte [eax], 0x3d ; 写入 '=' 填充符
工作流程总结:
输入数据
↓
标准Base64编码算法(3字节→4索引)
↓
用索引从魔改码表TABLE1取字符
↓
对每个字符调用charEncrypt转换
↓
生成最终密文
三、理解加密流程:两层混淆
通过对三个关键函数的深入分析,我们现在可以完整理解这道题目的加密流程。
3.1 加密流程图解
用户输入明文:"KanXue2019ctf_st"
↓
第一步:标准Base64编码原理
将每3个字节转换为4个6位索引
↓
第二步:使用魔改码表TABLE1查找
TABLE1 = "tuvwxTUlmnopqrs7YZabcdefghij8yz0123456VWXkABCDEFGHIJKLMNOPQRS9+/"
根据6位索引值在TABLE1中取字符
↓
第三步:对TABLE1中的每个字符应用charEncrypt
经过5种变换规则处理
↓
第四步:生成最终码表TABLE2
TABLE2 = "45678GF,-./0123iBA!"#$%&'()*j9:bcdefghEDC+ZYXWVUTSRQPONMLKJIHkwy="
↓
最终密文:"!NGV%,$h1f4S3%2P(hkQ94=="
3.2 为什么这样设计?双层混淆的巧妙之处
这道题目使用了双层混淆策略:
第一层:魔改Base64码表
- 打乱了标准码表的顺序
- 使得无法直接用标准Base64解码
第二层:charEncrypt字符转换
- 对魔改码表再次进行字符级别的转换
- 即使找到了魔改码表TABLE1,也无法直接解密
- 必须理解charEncrypt的转换规则
这种设计的优点:
- 增加破解难度:需要逆向两层算法
- 隐藏真实码表:程序中存储的TABLE1不是真正使用的TABLE2
- 考察综合能力:需要逆向分析、密码学和编程三方面能力
3.3 生成真正的编码表TABLE2
现在我们理解了charEncrypt的工作原理,可以生成真正使用的码表TABLE2。
让我们手工计算TABLE1的前几个字符:
TABLE1[0] = ‘t’ (116)
- 小写字母规则:116 – 64 = 52 → ‘4’
- TABLE2[0] = ‘4’
TABLE1[1] = ‘u’ (117)
- 小写字母规则:117 – 64 = 53 → ‘5’
- TABLE2[1] = ‘5’
TABLE1[2] = ‘v’ (118)
- 小写字母规则:118 – 64 = 54 → ‘6’
- TABLE2[2] = ‘6’
TABLE1[5] = ‘T’ (84)
- 大写字母规则:155 – 84 = 71 → ‘G’
- TABLE2[5] = ‘G’
TABLE1[17] = ‘7’ (55)
- 数字规则:55 + 50 = 105 → ‘i’
- TABLE2[17] = ‘i’
TABLE1[62] = ‘+’ (43)
- 特殊字符规则:固定为 ‘w’ (119)
- TABLE2[62] = ‘w’
TABLE1[63] = ‘/’ (47)
- 特殊字符规则:固定为 ‘y’ (121)
- TABLE2[63] = ‘y’
完整的TABLE2(通过程序计算):
TABLE2 = "45678GF,-./0123iBA!"#$%&'()*j9:bcdefghEDC+ZYXWVUTSRQPONMLKJIHkwy="
四、设计解密方案:逆向思维
理解了加密流程后,我们需要设计解密方案。
4.1 解密的关键思路
加密流程:
明文 → 标准Base64索引 → TABLE1取字符 → charEncrypt转换 → TABLE2字符 → 密文
解密流程(逆向):
密文 → 在TABLE2中查找位置 → 得到标准Base64索引 → TABLE0取字符 → 标准Base64密文 → Base64解码 → 明文
为什么可以这样解密?
关键理解:
- Base64编码的本质是索引映射
- 每个密文字符在TABLE2中的位置,就是原始的Base64索引值
- 用这个索引值去查标准码表TABLE0,就得到了标准Base64
- 标准Base64可以直接解码
4.2 详细解密步骤
步骤1:生成TABLE2
TABLE1 = "tuvwxTUlmnopqrs7YZabcdefghij8yz0123456VWXkABCDEFGHIJKLMNOPQRS9+/"
TABLE2 = ""
for char in TABLE1:
ascii_val = ord(char)
if 65 <= ascii_val <= 90: # 大写字母
TABLE2 += chr(155 - ascii_val)
elif 97 <= ascii_val <= 122: # 小写字母
TABLE2 += chr(ascii_val - 64)
elif 48 <= ascii_val <= 57: # 数字
TABLE2 += chr(ascii_val + 50)
elif ascii_val == 43: # '+'
TABLE2 += chr(119) # 'w'
elif ascii_val == 47: # '/'
TABLE2 += chr(121) # 'y'
TABLE2 += '=' # 添加填充字符
步骤2:准备标准Base64码表
TABLE0 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
步骤3:密文映射
cip = "!NGV%,$h1f4S3%2P(hkQ94=="
cip_origin = ""
for char in cip:
# 在TABLE2中找到密文字符的位置(索引)
index = TABLE2.find(char)
# 用这个索引从TABLE0中取出标准Base64字符
cip_origin += TABLE0[index]
让我们详细追踪第一个字符:
- 密文字符:’!’
- 在TABLE2中查找:TABLE2.find(‘!’) = 18
- 在TABLE0中取字符:TABLE0[18] = ‘S’
逐字符处理:
| 密文 | TABLE2位置 | TABLE0字符 | 说明 | | — | — | — | — | | ! | 18 | S | 索引18对应标准base64的’S’ | | N | 54 | 2 | | | G | 5 | F | | | V | 46 | u | | | % | 22 | W | | | , | 7 | H | | | $ | 21 | V | | | h | 37 | l | |
完整映射后得到:S2FuWHVlMjAxOWN0Zl9zdA==
步骤4:标准Base64解码
import base64
flag = base64.b64decode("S2FuWHVlMjAxOWN0Zl9zdA==")
print(flag.decode('utf-8')) # 输出:KanXue2019ctf_st
五、编写完整解密脚本
5.1 完整Python脚本
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import base64
print("=" * 70)
print("消失的岛屿 CTF 题目解密脚本")
print("=" * 70)
# 步骤1:从程序中提取的魔改base64码表TABLE1
TABLE1 = "tuvwxTUlmnopqrs7YZabcdefghij8yz0123456VWXkABCDEFGHIJKLMNOPQRS9+/"
print("\n[步骤1] 提取的魔改Base64码表(TABLE1)")
print(f"TABLE1 = {TABLE1}")
print(f"长度:{len(TABLE1)} 字符")
# 步骤2:根据charEncrypt函数的5种变换规则,生成最终码表TABLE2
print("\n[步骤2] 应用charEncrypt变换规则生成TABLE2")
print("变换规则:")
print(" 1. 大写字母(A-Z): chr(155 - ascii)")
print(" 2. 小写字母(a-z): chr(ascii - 64)")
print(" 3. 数字(0-9): chr(ascii + 50)")
print(" 4. '+': chr(119) = 'w'")
print(" 5. '/': chr(121) = 'y'")
TABLE2 = ""
for i in range(len(TABLE1)):
char = TABLE1[i]
ascii_val = ord(char)
if 65 <= ascii_val <= 90: # 大写字母A-Z
new_char = chr(155 - ascii_val)
TABLE2 += new_char
elif 97 <= ascii_val <= 122: # 小写字母a-z
new_char = chr(ascii_val - 64)
TABLE2 += new_char
elif 48 <= ascii_val <= 57: # 数字0-9
new_char = chr(ascii_val + 50)
TABLE2 += new_char
elif ascii_val == 43: # '+'符号
new_char = chr(119) # 'w'
TABLE2 += new_char
elif ascii_val == 47: # '/'符号
new_char = chr(121) # 'y'
TABLE2 += new_char
TABLE2 += '=' # 添加Base64填充字符
print(f"\nTABLE2 = {TABLE2}")
print(f"长度:{len(TABLE2)} 字符(包含填充符'=')")
# 步骤3:标准base64码表TABLE0
TABLE0 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
print("\n[步骤3] 标准Base64码表(TABLE0)")
print(f"TABLE0 = {TABLE0}")
# 步骤4:从程序中提取的密文
cip = "!NGV%,$h1f4S3%2P(hkQ94=="
print("\n[步骤4] 程序中硬编码的密文")
print(f"密文 = {cip}")
print(f"长度:{len(cip)} 字符")
# 步骤5:将密文从TABLE2映射回标准base64(TABLE0)
print("\n[步骤5] 密文字符映射过程")
print("=" * 70)
cip_origin = ""
for i, char in enumerate(cip):
# 在TABLE2中找到密文字符的位置(这就是Base64索引)
index = TABLE2.find(char)
# 使用该索引从标准base64码表TABLE0中取出对应字符
standard_char = TABLE0[index]
cip_origin += standard_char
print(f"密文[{i:2d}]: '{char}' → TABLE2位置 {index:2d} → TABLE0字符 '{standard_char}'")
print("=" * 70)
print(f"\n还原后的标准Base64密文:{cip_origin}")
# 步骤6:使用标准base64解码
print("\n[步骤6] 标准Base64解码")
flag_bytes = base64.b64decode(cip_origin)
flag = flag_bytes.decode('utf-8')
print("=" * 70)
print(f"解密成功!FLAG = {flag}")
print("=" * 70)
5.2 脚本执行结果
$ python solve.py
======================================================================
消失的岛屿 CTF 题目解密脚本
======================================================================
[步骤1] 提取的魔改Base64码表(TABLE1)
TABLE1 = tuvwxTUlmnopqrs7YZabcdefghij8yz0123456VWXkABCDEFGHIJKLMNOPQRS9+/
长度:64 字符
[步骤2] 应用charEncrypt变换规则生成TABLE2
变换规则:
1. 大写字母(A-Z): chr(155 - ascii)
2. 小写字母(a-z): chr(ascii - 64)
3. 数字(0-9): chr(ascii + 50)
4. '+': chr(119) = 'w'
5. '/': chr(121) = 'y'
TABLE2 = 45678GF,-./0123iBA!"#$%&'()*j9:bcdefghEDC+ZYXWVUTSRQPONMLKJIHkwy=
长度:65 字符(包含填充符'=')
[步骤3] 标准Base64码表(TABLE0)
TABLE0 = ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=
[步骤4] 程序中硬编码的密文
密文 = !NGV%,$h1f4S3%2P(hkQ94==
长度:24 字符
[步骤5] 密文字符映射过程
======================================================================
密文[ 0]: '!' → TABLE2位置 18 → TABLE0字符 'S'
密文[ 1]: 'N' → TABLE2位置 54 → TABLE0字符 '2'
密文[ 2]: 'G' → TABLE2位置 5 → TABLE0字符 'F'
密文[ 3]: 'V' → TABLE2位置 46 → TABLE0字符 'u'
密文[ 4]: '%' → TABLE2位置 22 → TABLE0字符 'W'
密文[ 5]: ',' → TABLE2位置 7 → TABLE0字符 'H'
密文[ 6]: '$' → TABLE2位置 21 → TABLE0字符 'V'
密文[ 7]: 'h' → TABLE2位置 37 → TABLE0字符 'l'
密文[ 8]: '1' → TABLE2位置 12 → TABLE0字符 'M'
密文[ 9]: 'f' → TABLE2位置 35 → TABLE0字符 'j'
密文[10]: '4' → TABLE2位置 0 → TABLE0字符 'A'
密文[11]: 'S' → TABLE2位置 49 → TABLE0字符 'x'
密文[12]: '3' → TABLE2位置 14 → TABLE0字符 'O'
密文[13]: '%' → TABLE2位置 22 → TABLE0字符 'W'
密文[14]: '2' → TABLE2位置 13 → TABLE0字符 'N'
密文[15]: 'P' → TABLE2位置 52 → TABLE0字符 '0'
密文[16]: '(' → TABLE2位置 25 → TABLE0字符 'Z'
密文[17]: 'h' → TABLE2位置 37 → TABLE0字符 'l'
密文[18]: 'k' → TABLE2位置 61 → TABLE0字符 '9'
密文[19]: 'Q' → TABLE2位置 51 → TABLE0字符 'z'
密文[20]: '9' → TABLE2位置 29 → TABLE0字符 'd'
密文[21]: '4' → TABLE2位置 0 → TABLE0字符 'A'
密文[22]: '=' → TABLE2位置 64 → TABLE0字符 '='
密文[23]: '=' → TABLE2位置 64 → TABLE0字符 '='
======================================================================
还原后的标准Base64密文:S2FuWHVlMjAxOWN0Zl9zdA==
[步骤6] 标准Base64解码
======================================================================
解密成功!FLAG = KanXue2019ctf_st
======================================================================
5.3 验证解密结果
我们可以验证解密结果是否正确:
import base64
# 将明文编码为标准Base64
plaintext = "KanXue2019ctf_st"
standard_base64 = base64.b64encode(plaintext.encode()).decode()
print(f"标准Base64编码:{standard_base64}")
# 输出:S2FuWHVlMjAxOWN0Zl9zdA==
确认无误!我们成功解密了这道题目。
六、技术总结与学习要点
6.1 核心技术点回顾
1. Base64编码原理
- 将二进制数据编码为可打印字符
- 每3个字节(24位)转换为4个字符(每个6位)
- 使用64个字符的码表进行映射
- 不足3字节时用’=’填充
2. 魔改Base64的常见手法
- 打乱码表顺序
- 使用自定义码表
- 叠加其他变换算法
3. 字符映射变换
- 利用ASCII值的数学关系
- 不同类型字符使用不同变换规则
- 保持可逆性或单向性
4. 双层混淆策略
- 第一层:码表替换
- 第二层:字符转换
- 大幅增加破解难度
6.2 逆向分析方法总结
第一步:信息收集
- 使用file、strings等基础工具
- 快速获取程序特征和关键字符串
- 建立初步假设
第二步:静态分析
- 使用反汇编工具(IDA、radare2、Ghidra)
- 定位关键函数
- 分析函数调用关系
第三步:代码分析
- 逐行分析汇编代码
- 理解算法逻辑
- 识别关键常数和魔数
第四步:算法还原
- 将汇编代码还原为高级语言逻辑
- 绘制流程图
- 理解整体工作流程
第五步:编写解密脚本
- 根据理解的算法编写逆向程序
- 验证结果
- 优化代码
6.3 学习建议
对于初学者:
- 先学习Base64编码的基本原理
- 熟悉常用的逆向工具(radare2、IDA)
- 掌握基本的x86汇编语言
- 练习Python编程
对于进阶者:
- 深入学习密码学理论
- 研究更复杂的混淆技术
- 学习动态分析方法(调试器使用)
- 了解反反调试技术
6.4 常见错误与注意事项
错误1:直接尝试标准Base64解码
- 问题:看到”==”就以为是标准Base64
- 教训:要检查字符集是否符合标准
错误2:忽略字符映射变换
- 问题:找到魔改码表就直接解码
- 教训:要分析程序是否有额外的变换
错误3:手动计算容易出错
- 问题:在纸上计算ASCII值转换
- 教训:编写程序自动化处理
错误4:忘记添加填充字符
- 问题:生成的TABLE2缺少’=’
- 教训:Base64码表必须是65个字符
6.5 拓展思考
如果题目变得更复杂:
- 多轮变换:多次应用charEncrypt
- 动态码表:根据密钥生成不同的码表
- 混合加密:Base64 + AES + 自定义变换
- 反调试:增加反调试代码
- 代码混淆:使用花指令、虚假跳转等
实际应用场景:
- 软件保护:序列号验证系统
- 数据传输:自定义编码协议
- 恶意代码:C&C通信加密
- 游戏保护:存档加密
七、工具使用指南
7.1 radare2 常用命令速查
# 基本操作
r2 file # 打开文件
aaa # 分析所有(analyze all)
afl # 列出所有函数
s address # 跳转到地址
pdf # 打印当前函数反汇编
V # 进入可视模式
q # 退出
# 信息查看
ii # 列出导入函数
is # 列出符号
iz # 列出字符串
afvd # 列出函数变量
# 搜索
/ string # 搜索字符串
/x 90 90 # 搜索十六进制
7.2 Python Base64 库使用
import base64
# 编码
plaintext = "Hello"
encoded = base64.b64encode(plaintext.encode())
print(encoded) # b'SGVsbG8='
# 解码
decoded = base64.b64decode(encoded)
print(decoded.decode()) # Hello
# 处理URL安全的Base64
url_encoded = base64.urlsafe_b64encode(plaintext.encode())
url_decoded = base64.urlsafe_b64decode(url_encoded)
7.3 ASCII 值速查表
| 范围 | 描述 | 示例 | | — | — | — | | 0-31 | 控制字符 | \n, \r, \t | | 32 | 空格 | ‘ ‘ | | 33-47 | 标点符号 | !, “, #, $, %, & | | 48-57 | 数字 | 0-9 | | 58-64 | 标点符号 | :, ;, <, =, >, ?, @ | | 65-90 | 大写字母 | A-Z | | 91-96 | 标点符号 | [, , ], ^, _, ` | | 97-122 | 小写字母 | a-z | | 123-126 | 标点符号 | {, |, }, ~ |
八、总结
通过对”消失的岛屿”这道CTF题目的完整分析,我们学习了:
- 逆向工程基础
- 如何使用radare2进行静态分析
- 如何阅读和理解x86汇编代码
- 如何定位和分析关键函数
- 密码学知识
- Base64编码的原理和实现
- 魔改编码的常见手法
- 多层混淆的设计思路
- 问题解决能力
- 从混乱的汇编代码中提取算法逻辑
- 设计逆向解密方案
- 编写自动化解密脚本
- 技术细节
- 字符映射变换的5种规则
- 双层码表的生成和使用
- ASCII值的数学关系应用
这道题目虽然不算复杂,但涵盖了CTF密码学和逆向工程的核心知识点。通过系统的学习和实践,我们不仅解决了这道题目,更重要的是掌握了一套完整的分析方法论。
在实际的安全研究工作中,我们经常会遇到各种各样的加密和混淆技术。保持好奇心,善用工具,耐心分析,相信每个人都能成为优秀的安全研究员。
最终FLAG:flag{KanXue2019ctf_st}
附录
A. 完整解密脚本
详见本文第五章节的完整Python脚本。
B. 相关资源
工具下载:
- radare2: https://rada.re/
- IDA Pro: https://hex-rays.com/
- Ghidra: https://ghidra-sre.org/
- Python: https://www.python.org/
学习资源:
- Base64编码原理:https://zh.wikipedia.org/wiki/Base64
- x86汇编教程:https://www.cs.virginia.edu/~evans/cs216/guides/x86.html
- CTF Wiki:https://ctf-wiki.org/
题目来源:
- 看雪论坛:https://www.kanxue.com/
- 题目年份:2019年看雪CTF比赛
感谢您的阅读,希望这篇文章能帮助您更好地理解CTF密码学题目的分析方法。如有任何疑问或建议,欢迎交流讨论!
查看原文:《CTF密码学题目深度解析:消失的岛屿(LostIslands)》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论