本题考查的是对格式化字符串漏洞的利用以及修改_IO_2_1_stdin来实现任意地址写
题目分析
checksec:
发现保护全开
源码分析:
echo函数:
在该函数中有明显的格式化字符串漏洞,但是格式化字符串的长度有限制,最长只能为7,这使得许多利用格式化字符串进行任意内存写的方式不能使用,不过泄露信息还是能够做到的
对于信息泄露,首先我们需要获取libc的基地址,这样我们才能计算system和/bin/sh在程序中的位置。而本题利用的泄露信息与以往的题目不同,以前一般是泄露某个库函数(read,write,printf等等)的got表地址来计算libc基地址的。但是本题因为开启了PIE以及输入长度的限制,我们难以得到got表在内存中的地址。所以本题是利用 __libc_start_main 来获取libc基地址的:main函数的返回地址为__libc_start_main + 0xf0,获得了main函数的返回地址就能计算出__libc_start_main的地址,也能计算出libc的基地址。而main函数的返回地址是保存在栈上的,根据调试可以发现使用 %19$p 就能得到main的返回地址。同理使用 %13$p就可以得到echo函数的返回地址从而计算出程序的地址。
通过上面的步骤我们能够计算出system和/bin/sh的地址,但是我们还需要修改函数的返回地址才能执行我们的代码。而%12$p处就保存了main函数rbp的地址,该地址+0x8就是main函数的返回地址了。
现在我们已经基本获取到了需要的地址,但是还有一个问题,就是如何把我们的rop链写入。这里就需要用到源代码中的setName函数:
我们先把名字设为aaaa,然后查看栈
可以看到我们设置的名字在%16$的位置,所以我们可以先把名字设为想要写入的地址,然后利用格式化字符串就能向该地址写入数据了。
但是还有一个问题,因为题目的限制,我们能够输入的字符数是有限的,在题目的限制下我们难以把ROP链写到返回地址上,这里我们就需要利用 _IO_2_1_stdin 了。
基础知识
在echo函数中,程序使用了scanf来读取长度,而scanf是从stdin中读取数据的,如果我们能够修改它,就能实现任意内存写入。
_IO_FILE
当Linux新建一个进程时,会自动创建3个文件描述符0、1和2,分别对应标准输入、标准输出和错误输出。C库中与文件描述符对应的是文件指针,与文件描述符0、1和2类似,我们可以直接使用文件指针stdin、stdout和stderr。
下面是stdio.h中的代码
typedef struct _IO_FILE FILE;
/* Standard streams. */
extern struct _IO_FILE *stdin; /* Standard input stream. */
extern struct _IO_FILE *stdout; /* Standard output stream. */
extern struct _IO_FILE *stderr; /* Standard error output stream. */
#ifdef __STDC__
/* C89/C99 say they're macros. Make them happy. */
#define stdin stdin
#define stdout stdout
#define stderr stderr
#endif
从上面的源码可以看出,stdin、stdout和stderr确实是文件指针。而C标准要求stdin、stdout和stderr是宏定义,所以在C库的代码中又定义了同名宏。stdin、stdout和stderr的定义代码如下:
_IO_FILE *stdin = (FILE *) &_IO_2_1_stdin_;
_IO_FILE *stdout = (FILE *) &_IO_2_1_stdout_;
_IO_FILE *stderr = (FILE *) &_IO_2_1_stderr_;
让我们再看看文件的读取过程:
_IO_new_file_underflow 这个函数最终调用了_IO_SYSREAD系统调用来读取文件。在这之前,它做了一些处理
int _IO_new_file_underflow (_IO_FILE *fp)
{
_IO_ssize_t count;
...
if (fp->_flags & _IO_NO_READS)
<