最近在crackmes.one上看到一个挺有意思的题目叫CrackIt;),作者是QERR0R。作为一个喜欢挑战逆向工程的人,我决定尝试破解这个程序。下面是我完整的解题过程,包含所有关键步骤和思考路径。
这个程序是一个64位的Linux可执行文件,动态链接且被strip过(去除了符号表)。运行程序时发现它需要一个命令行参数,否则会显示用法提示。我们的目标就是找出能让程序输出"You cracked me!"的正确flag。
提示:在实际逆向工作中,遇到strip过的二进制文件时,字符串引用和函数调用关系是最重要的突破口。
首先我用了几个基础命令来收集程序信息:
bash复制$ file crackit
crackit: ELF 64-bit LSB pie executable, x86-64, dynamically linked, stripped
$ strings crackit | less
确认这是一个64位ELF文件后,我决定使用Ghidra进行静态分析。Ghidra是NSA开源的逆向工程工具,对于这种没有符号表的二进制特别有用。
在Ghidra中加载程序后,我通过搜索字符串"usage: ./crackit
反编译后可以看到函数开头部分:
c复制if (param_1 != 2) {
// 输出usage并退出
}
__s = *(char **)(param_2 + 8); // 获取argv[1]
这里param_1对应argc,param_2对应argv。在64位系统中,指针是8字节,所以param_2+8就是argv[1]的地址。
程序将用户输入存储到一个栈上的std::string对象中(位于local_58)。这个对象使用了短字符串优化(SSO),即小字符串直接存储在对象内部的缓冲区,而不是堆上。
继续向下分析,在函数末尾发现了关键比较代码:
c复制if ((local_b0 == local_80) &&
((local_b0 == 0 || (iVar1 = memcmp(local_68, local_88, local_b0), iVar1 == 0)))) {
std::operator<<(std::cout, "You cracked me!");
} else {
std::operator<<(std::cout, "Try again, You can do it!");
}
这段代码比较了两个字符串:
我们的任务就是找出local_88指向的内容。
通过回溯代码,我发现local_88最初被初始化为一个空字符串:
c复制local_88 = local_78;
local_80 = 0;
local_78[0] = 0;
随后程序进入一个循环,不断向local_88追加字符串片段:
c复制for (puVar6 = puVar2; puVar6 != puVar3; puVar6 = puVar6 + 4) {
std::__cxx11::string::_M_append((char *)&local_88, *puVar6);
}
这里puVar2指向一块通过operator new(0x160)分配的堆内存,由FUN_001016b0函数初始化。puVar3是这个内存区域的结束地址。
关键函数调用如下:
c复制puVar3 = (ulong *)FUN_001016b0(&PTR_DAT_00103d40, _DYNAMIC, puVar2);
这表明数据来源于全局符号PTR_DAT_00103d40。在Ghidra中跳转到00103d40地址,发现这里是一个指针数组,每个指针指向一个以null结尾的字符串片段。
具体内容如下表所示:
| 内存地址 | 指向地址 | 字符串片段 |
|---|---|---|
| 00103d40 | 00102091 | "CTF{" |
| 00103d48 | 00102096 | "My_" |
| 00103d50 | 0010209a | "S3" |
| 00103d58 | 0010209e | "cr3t_" |
| 00103d60 | 001020a3 | "Fl4g" |
| 00103d68 | 001020a8 | "}W0W" |
| 00103d70 | 001020ad | "Y0u" |
| 00103d78 | 001020b1 | "F0und" |
| 00103d80 | 001020b7 | "M3" |
| 00103d88 | 001020ba | "{0r " |
| 00103d90 | 001020bf | "N0t}" |
循环将这些片段按顺序拼接起来,形成完整的flag:
code复制CTF{My_S3cr3t_Fl4g}W0WY0uF0undM3{0r N0t}
在终端中测试这个flag:
bash复制$ ./crackit "CTF{My_S3cr3t_Fl4g}W0WY0uF0undM3{0r N0t}"
You cracked me!
程序输出了成功信息,证明我们的分析是正确的。
字符串引用追踪:在没有符号表的情况下,字符串引用是最可靠的切入点。在Ghidra中可以通过"Search -> For Strings"查找所有字符串,然后查看它们的交叉引用。
函数识别方法:
调用图分析:在Ghidra中可以通过"Window -> Function Graph"查看函数调用关系,帮助理解程序流程。
注意:对于C++程序,要特别注意this指针的传递(在x86-64中通常通过rdi寄存器传递)。
在实际分析过程中,我遇到了几个问题:
std::string的识别:
内存布局理解:
指针运算困惑:
动态调试配合:
伪代码重构:
交叉验证:
Ghidra:
IDA Pro:
Binary Ninja:
GDB:
strace/ltrace:
radare2:
xxd/hexdump:
objdump:
strings:
计算机体系结构:
操作系统原理:
编程语言:
从简单题目开始:
记录分析过程:
参与CTF比赛:
在线平台:
书籍:
社区:
通过这次CrackIt;)的分析,我再次体会到逆向工程既需要扎实的技术基础,也需要耐心和细致的观察力。每个二进制文件都像是一个谜题,而逆向工程师就是解谜者。希望这篇详细的writeup对正在学习逆向工程的朋友有所帮助。