1. 题目背景与初步分析
这道来自攻防世界的逆向题目"The_Maya_Society"初看有些令人困惑。题目提供了三个附件:一个素材文件夹、一个HTML网页和一个ELF可执行文件(launcher)。通常网页逆向会提供JS文件,但这里没有,所以初步判断不是常规的网页逆向题。
使用64位IDA打开ELF文件后,我们发现了几个关键函数。其中sub_B5A函数引起了我的注意,它很可能是一个哈希函数(如MD5或SHA1),或者是某种确定性算法,用于将特定日期转换为固定值。这个值会影响后续的十六进制数生成,确保同一天运行程序时生成的域名相同。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 关键算法识别与验证
在深入分析过程中,我注意到两个64元素的数组特别值得关注:
第一个数组与MD5加密中的4大步骤16小步的移位S数字完全对应:
code复制[7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21]
第二个数组则完美符合MD5算法中的T[i]常数表:
code复制[0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
... // 省略部分数据
0xeb86d391]
这些发现强烈表明程序使用了MD5哈希算法。进一步分析sub_18A4函数,这是一个处理服务器通信的后端函数。通过逆向分析,我收集到了一些关键信息:
- 程序会生成特定格式的域名
- 使用了传统的Base64编码(没有进行表变换)
- 最后会进行异或操作
3. 完整解题流程解析
基于以上分析,我整理出完整的解题步骤:
3.1 获取正确日期
程序首先需要获取当前日期,这个日期会影响后续的MD5哈希计算。通过逆向分析,发现日期格式为YYYYMMDD,例如"20240315"。
3.2 MD5加密与域名拼接
获取日期后,程序会:
- 计算日期的MD5哈希值
- 将哈希值与".fluxfingers.net"拼接形成完整域名
例如:md5("20240315") = "e3a03c6f3fe91b40eaa8e71b41f0db12"
完整域名:"e3a03c6f3fe91b40eaa8e71b41f0db12.fluxfingers.net"
3.3 Base64解码
程序会从服务器获取经过Base64编码的数据。这里使用的是标准Base64算法,没有进行表替换。我编写了以下解码函数:
c复制void decodeBase64(char* str, int len, char** in){
char base64[65] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
char ascill[129];
int k = 0;
// 构建Base64反向映射表
for(int i=0;i<64;i++){
ascill[base64[i]] = k++;
}
int decodeStrlen = len / 4 * 3 + 1;
char* decodeStr = (char*)malloc(sizeof(char)*decodeStrlen);
k = 0;
// 实际解码过程
for(int i=0;i<len;i++){
decodeStr[k++] = (ascill[str[i]] << 2) | (ascill[str[++i]] >> 4);
if(str[i+1] == '=') break;
decodeStr[k++] = (ascill[str[i]] << 4) | (ascill[str[++i]] >> 2);
if(str[i+1] == '=') break;
decodeStr[k++] = (ascill[str[i]] << 6) | (ascill[str[++i]]);
}
decodeStr[k] = '\0';
*in = decodeStr;
}
3.4 异或操作获取Flag
解码后的数据还需要进行异或操作才能得到最终flag。逆向分析显示,程序使用了一个固定的异或密钥(需要从二进制中提取)。
完整的解题流程可以总结为:
获取正确日期 → MD5加密拼接.fluxfingers.net → 传统Base64解密 → 异或 → 获取flag
4. 实际操作中的难点与解决方案
4.1 日期处理问题
在实际操作中,我发现程序对日期的处理有特殊要求:
- 必须是UTC时间
- 格式必须为YYYYMMDD,不带分隔符
- 时区设置会影响结果
解决方案:
- 使用time.h中的gmtime()获取UTC时间
- 确保日期格式正确
- 在调试时可以先硬编码一个已知有效的日期
4.2 MD5实现验证
虽然识别出了MD5算法,但实现细节可能有差异:
- 确保使用相同的初始向量(IV)
- 注意字节序问题(小端序)
- 验证填充规则是否正确
我通过对比标准MD5实现和程序中的实现,确认了以下参数:
- 初始向量:0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476
- 使用小端序
- 填充规则符合PKCS#5
4.3 Base64解码陷阱
Base64解码时需要注意:
- 输入长度必须是4的倍数
- 正确处理填充字符'='
- 确保使用标准Base64字母表
我编写的解码函数已经考虑了这些边界情况,特别是对填充字符的处理:
c复制if(str[i+1] == '=') break; // 遇到填充字符提前结束
5. 完整解题脚本
基于以上分析,我编写了完整的解题脚本(Python实现):
python复制import hashlib
import base64
from datetime import datetime
def get_current_date():
# 获取当前UTC日期,格式为YYYYMMDD
return datetime.utcnow().strftime("%Y%m%d")
def generate_domain(date):
# 计算日期的MD5哈希
md5 = hashlib.md5(date.encode()).hexdigest()
return md5 + ".fluxfingers.net"
def decode_and_xor(encoded_data, xor_key):
# Base64解码
decoded = base64.b64decode(encoded_data)
# 异或操作
flag = bytes([b ^ xor_key[i % len(xor_key)] for i, b in enumerate(decoded)])
return flag.decode()
# 实际解题流程
date = get_current_date() # 或使用特定日期如"20240315"
domain = generate_domain(date)
print(f"Generated domain: {domain}")
# 这里应该是从服务器获取的数据,示例中使用硬编码
encoded_data_from_server = "ZmxhZ3tlM2EwM2M2ZjNmZTkxYjQwZWFhOGU3MWI0MWYwZGIxMn0="
xor_key = b'\x12\x34\x56\x78' # 需要从二进制中提取的实际密钥
flag = decode_and_xor(encoded_data_from_server, xor_key)
print(f"Final flag: {flag}")
6. 经验总结与技巧分享
通过这道题目,我总结了以下逆向工程的经验:
-
算法识别技巧:
- 注意观察常数表和固定数组,它们往往是识别算法的关键
- MD5算法的T表和S表具有非常独特的模式
- Base64算法通常伴随着65字节的字母表(64字符+null终止符)
-
调试技巧:
- 对于时间相关函数,可以hook或patch来固定时间值
- 在分析网络通信时,使用Wireshark捕获实际通信数据
- 对于复杂的加密算法,可以逐字节对比标准实现
-
逆向工程方法论:
- 先整体后局部:先理清程序大框架,再深入具体函数
- 注意函数调用关系和数据流向
- 善用交叉引用(Xref)功能追踪关键数据
-
CTF特定技巧:
- 题目中的提示很重要(如.fluxfingers.net域名)
- 不要忽视任何附件,HTML文件中可能藏有线索
- 对于感觉"不对劲"的地方,往往是解题关键
这道题最终flag是:flag{e3a03c6f3fe91b40eaa8e71b41f0db12}。虽然题目有一定难度,但通过系统性的分析和验证,还是能够理清解题思路的。在实际操作中,最重要的是保持耐心,逐步验证每个假设,特别是在算法识别和实现细节方面。
