Pwnable.kr 提示之 otp 篇
前前言
本人的个人博客网址:www.QmSharing.space,所有的文章都可以在里面找到,欢迎各位大佬前来参观并留下宝贵的建议,大家一起学习一起成长 :-)
难度分析
本题主要考的是知识面, 它并没有考什么高超的技术, 所以难度来说如果你知道这个知识点, 那么这题简单的不行, 但如果你不知道(和我一样), 那做的就会非常痛苦.
基本检查
- file
otp: 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.24, BuildID[sha1]=f851771b439725c55be4ed4b0e102c2a39f4c196, not stripped
本程序是一个64位动态链接程序, 其他没什么特别
- checksec
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
我发现 Rookiss 系列中很多题目都是这样又有 Canary 又有栈段不可执行保护的. 这样就导致很多技术都难以被应用, 这种题目通常解题的手段不会很多样.
分析
本题久违的给出了程序源码, 那就直接先审计源码:
int main(int argc, char* argv[]){
char fname[128];
unsigned long long otp[2];
// 必须传入 passcode 参数
if(argc!=2){
printf("usage : ./otp [passcode]\n");
return 0;
}
int fd = open("/dev/urandom", O_RDONLY);
if(fd==-1) exit(-1);
// 从 /dev/urandom 读取16字节的随机数
if(read(fd, otp, 16)!=16) exit(-1);
close(fd);
// 格式化输出文件名称
sprintf(fname, "/tmp/%llu", otp[0]);
FILE* fp = fopen(fname, "w");
if(fp==NULL){ exit(-1); }
// 写出至 /tmp/XXX 文件, 写出 8 字节随机数
fwrite(&otp[1], 8, 1, fp);
fclose(fp);
printf("OTP generated.\n");
// 重新打开 /tmp/XXX 读取刚刚写出的内容
unsigned long long passcode=0;
FILE* fp2 = fopen(fname, "r");
if(fp2==NULL){ exit(-1); }
fread(&passcode, 8, 1, fp2);
fclose(fp2);
// 和我们输入的参数进行比较, 正确就 cat flag
if(strtoul(argv[1], 0, 16) == passcode){
printf("Congratz!\n");
system("/bin/cat flag");
}
else{
printf("OTP mismatch\n");
}
// 清理掉临时文件
unlink(fname);
return 0;
}
整个程序逻辑很简单明了, 我的注释已经在关键位置进行了说明. 官网特别提醒, 本题不是竞争问题, 叫我们不要用暴力算法. 经过基本的审计, 再加上程序自身的限制, 你会发现整个程序基本无懈可击. 但我发现以往的程序读取随机数一般是去找 /dev/random 文件读取, 因此我特地去了解了这两个文件的区别, 以求发现注入点. 刚开始, 我得知 urandom 是非阻塞式的伪随机数生成器, 而 random 是"真"随机生成器, 我还在想有没有办法能让系统的熵值降到 0, 然后生成一样的随机数, 从而破解文件内的随机数.
但是, 这里有两个问题: 1. 随机数的熵值在被消耗达到最低值(目标系统设定是64字节, 通过 cat /proc/sys/kernel/random/read_wakeup_threshold
来查看)会阻塞 random, 这样熵值无法通过任何手段保持稳定. 2. 即使能做到控制熵值, urandom 产生可预测的随机数的条件是要系统刚刚启动, 还没有足够的熵来生成 urandom 的种子这个前提, 但我们是没办法让目标系统重启的, 所以破解 urandom 其实是个"不可能"的任务.
这里我其实有参考了一下网上的一些说法, 才觉得这题脑洞真大. 嗯... 我们是不可以预测随机数, 但如果我们让程序写不出任何内容, 那它再读取一个空文件, passcode 肯定是等于0的. 我不得不承认, 这个想法太特么绝了!!! 因为我是提示性文章, 这里我就提示一个 bash 指令 ulimit (做完这题, 我才觉得这个指令原来这么厉害, 以前真的很少用, 除了为了写出 core file). 如果你直接 man ulimit 是不行的, 会展示 ulimit 函数的说明, 这个指令的说明是整合在 bash 的说明中的, 因此你需要 man bash 然后再搜索 ulimit 这个关键字才行.
<div align=center><font color=red> ========== 进一步剧透分界线 ========== </font></div>
这里涉及下一步的提示, 如果你还没有玩转 ulimit 指令的话, 希望能完成上一步后再看这里.
有些人通过上述方法, 成功限制了程序写出文件大小, 但在服务端执行(或者本地), 会发现报错 "File size limit exceeded (core dumped) ", 然后调试 core 文件, 发现是 SIGXFSZ 信号. 其实, 太过具体的成因我现在也不是很明确, 但是, 如果把它作为子进程执行的话, 并不会触发这个 SIGXFSZ 信号, 这里提示一下 Python 的 subprocess 模块.
答案
解答步骤和 Writeup 可以在我的 Github 中找到: otp writeup