前前言
本人的个人博客网址:www.QmSharing.space,所有的文章都可以在里面找到,欢迎各位大佬前来参观并留下宝贵的建议,大家一起学习一起成长 :-)
难度分析
本题属于综合题, 是有一定难度的, 但利用的漏洞我们在之前的题目中基本都见到过, 所以也不算特别的难. 这题唯一一个之前涉及不多的就是 canary 技术, 不过它也并不是很难理解, 后面我会大概介绍一下. 这题总体是特别有意思的一道题, 难度与乐趣具备, 很值得你花点时间去钻研一下.
基本检查
- file
hash: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=89ebf47881a82f5a991199ae381f8284a46e0500, not stripped
基本的动态链接的32位程序, 和之前大多数题目一样.
- checksec
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
嗯... 也是有 canary 和栈段不可执行保护, 看起来"好像"又不能进行栈溢出了. 其他没什么值得注意的地方了.
遇到的问题
可能有些人会遇到和我一样的问题, 就是在直接执行 hash 程序时, 出现以下报错:
./hash: error while loading shared libraries: libcrypto.so.1.0.0: cannot open shared object file: No such file or directory
这种类型的报错我之前见过的不少, 本质原因就是程序在进行外部链接时找不到所需的库, 导致程序无法执行, 那么问题就是这个 libcrypto.so.1.0.0 是什么库来的.
我大概 apt search 了一下, 就是下面这个东西:
libssl1.0.0/xenial-updates,xenial-security,now 1.0.2g-1ubuntu4.14 amd64 [installed]
Secure Sockets Layer toolkit - shared libraries
应该是个 SSL 的工具库, 但是可以看到我好像已经装了(不知道你们是不是), 怎么会报错呢? 其实问题也很简单, 你运行的这个程序是32位的, 而你安装的却是64位的动态库, 能运行才怪呢对吧. 因此, 你需要安装 32 位的 ssl 库:
sudo apt install libssl1.0.0:i386
然后你再运行程序应该就不会有问题了.
分析
进入到 Rookiss 这个级别, 已经基本不给你源码进行分析了, 所以是考察你的逆向能力. 这里我就稍微贴一下一些关键位置的逆向伪代码, 其余的你就自己逆向后进行分析吧.
int __cdecl main(int argc, const char **argv, const char **envp)
{
// 前面省略变量定义和缓冲区设定
puts("- Welcome to the free MD5 calculating service -");
v3 = time(0); // 用当前时间做随机数的种子, 这里稍微留意一下哈
srand(v3);
v6 = my_hash(); // 这个是生成验证码(captcha)的逻辑
printf("Are you human? input captcha : %d\n", v6);
__isoc99_scanf("%d", &v5);
if ( v6 != v5 ) // 输错验证直接结束
{
puts("wrong captcha!");
exit(0);
}
puts("Welcome! you are authenticated.");
puts("Encode your data with BASE64 then paste me!");
process_hash(); // 这里就是将输入进行 BASE64 解码后再生成 MD5
puts("Thank you for using our service.");
system("echo `date` >> log"); // 这里就简单的将指令 date 的结果附加到 log 后面, 但我觉得这就是为了提供 system 函数给你用
return 0;
}
整个程序逻辑很简单, 就是把你的输入转换成 MD5, 展示后把当前系统时间写入到 log. 继续研究, 我当时选择先看 process_hash 函数:
unsigned int process_hash()
{
// 省略 v0-v4 的变量定义
int v5; // [esp-20Ch] [ebp-20Ch]
unsigned int v6; // [esp-Ch] [ebp-Ch] 这个是 canary
v6 = __readgsdword(0x14u); // 写入 canary 值
memset(&v5, 0, 0x200u);
while ( getchar() != 10 )
;
memset(g_buf, 0, sizeof(g_buf)); // 初始化 g_buf
fgets(g_buf, 1024, stdin); // 从标准输入读取1024字节的值
memset(&v5, 0, 0x200u);
v1 = Base64Decode(g_buf, &v5, v0); // 将输入进行 BASE64 解码, 结果写入到 v5, v1是解码长度
v3 = calc_md5(&v5, v1, v2); // 利用 v5 的值进行 MD5 生成, 结果赋值给 v3
printf("MD5(data) : %s\n", v3);
free(v3);
return __readgsdword(0x14u) ^ v6; // 这里就是检测 canary 值是否被改变, 改变则证明出现了栈溢出
}
这里不管你用什么方法(输入大量数据或直接分析代码), 你会发现 1024 字节的 BASE64 值理论上解码能得到 (1024*3/4=768)字节的数据, 但你看清楚, 这个 v5 仅仅分配了 512(0x200) 字节给它, 那么这里必然存在了栈溢出, 即 v5 就是注入点. 但讽刺的是, 这个函数被 canary 保护着, 这和我们之前做过的题目好像有点不同啊. 那么我觉得, 这题应该就是要我们进行某种操作来绕过 canary 值了, 如果你看到这里还是不知道什么是 canary 的话, 你应该马上停止阅读, 并打开 canary 原理先进行学习后, 再往下阅读.
绕过 canary 有不少, 但是鉴于本题的情况, 覆盖 canary 或者劫持 got 的方法应该是不太好实现的, 因此我觉得这很大可能是一个要泄漏 canary 值的题目, 因为如果你知道了 canary 值, 那么在整个程序的运行周期内, 这个值都不会改变, 你就可以在注入时在它应该存在的地方把它插入, 让程序以为自己运行并没有出错, 但实际我们已经进行了栈溢出攻击了.
但是, 哪里有办法让我们泄漏出这个 canary 值呢? 因为不知道 canary 的情况下, 我们也不可能改变程序的走向, 而且 Base64 解码和 MD5 生成逻辑中也没什么可疑的地方(可以说我并不想去看那里的代码 (* ̄ω ̄)). 然后我就去翻翻 my_hash 函数, 嗯... 发现了这样一个有趣的地方:
int my_hash()
{
signed int i; // [esp-38h] [ebp-38h]
// 省略 v2 到 v9 的定义, 我这里的变量计数是从 v2 开始的, 我也不知道为什么, 你们知道就好
unsigned int v10; // [esp-Ch] [ebp-Ch] 这个是 canary
v10 = __readgsdword(0x14u);
for ( i = 0; i <= 7; ++i )
*(&v2 + i) = rand(); // 为 v2-v9 分别赋值随机数
return v6 - v8 + v9 + v10 + v4 - v5 + v3 + v7; // 返回 captcha, 嗯... v10 怎么在里面
}
可以看到, 这个返回值的计算中, 有把 canary 算进去了. 这... 岂不是我知道了其它7个值, 我就知道 canary 是什么了吗? 但是其它七个值是随机数啊, 怎么可能破解呢? 我当初在这里也卡了一段时间, 但是, 如果你还记得之前那到 random 的题目, 你就应该知道, 随机数生成算法生成的并不是真正的随机数, 只是根据一个种子进行的计算出伪随机数, 如果你有同样种子, 你就能生成一模一样的随机数了.
这里我感觉应该是本题最难的地方之一了, 你除了需要知道随机数并不是真正随机的这个原理外, 你还要知道程序运行时系统的时间戳是多少. 但这里题目当时有给我们提示, 说这个程序执行的服务器和网站的服务器是同一个. 那么这样就好办了, 我可以在执行程序的时候, 立刻向 Pwnable.kr 网站发一个数据包, 根据结果返回的世界来判断程序当时执行的时间(其实还有一个方法, 就是先通过之前一些题目的提供的账号, ssh 到服务器, 然后在执行程序时, 顺便获得 date +%s
指令的值, 这个值与程序执行的时间基本是一致的). 提示到这里, 本题最难的地方应该已经解决了, 剩下的就是普通的栈溢出利用了, 如何重定向以执行 system 函数, 我相信大家都懂了, 如果大家有什么问题, 欢迎大家在下方留言, 大家一起交流一下 : )
答案
解答步骤和 Writeup 可以在我的 Github 中找到: md5-calculator writeup