引言
在上两篇文章中1、2,我们已经解决了stack0、stack1和stack2挑战,今天,我们将为读者介绍如何解决protostar stack3。在解决前三个挑战的时候,我们使用了二进制文件的源代码,来识别缓冲区溢出发生的位置并加以利用。然而,对于stack3来说,我们虽然也有源代码,但是,我们却不打算使用它,相反,我们将使用一些实用的技术来解决这个挑战。那我们为什么要这么做呢?在现实情况下,我们基本上是没有机会得到目标程序的源代码,对吧?
如果您还没有读过我之前撰写的关于缓冲区溢出的文章,建议您先阅读它们。
./Stack3
让我们先来考察一下这个程序,看看它是做什么的。
./stack3
如图所示,我们没有看到任何输出内容,所以,看来我们应该给它提供一个参数。为了检测该程序是否含有缓冲区溢出漏洞,我们可以为其传递100个字符的参数,看看它有什么反应:
python -c "print 'A' * 100" | ./stack3
这时,我们看到一个segfault错误,这说明发生了缓冲区溢出;同时,我们还看到这样一行内容:“calling function pointer , jumping to 0x41414141”。
现在,我们已经大体知道发生了什么情况:其中有一个函数指针,它会根据函数给定的内存地址来执行函数。由于该内存地址存储在一个变量中,因此,当发生缓冲区溢出时,我们就可以覆盖或者说重写该变量了。我们看到,函数指针正在调用地址0x41414141,其中0x41是“A”的十六进制表示。所以,接下来我们要做两件事:第一件事是弄清楚缓冲区溢出是在哪里发生的,虽然上面给这个程序输入了一个100个字符的参数,但是我们并不知道缓冲区的确切大小;第二件事是找到我们需要执行的函数的内存地址。如何完成这两件事情呢?下面将为读者详细介绍。
确定缓冲区的大小
为了简单起见,这里将使用Kali box系统来完成这个程序的编译和测试工作。
Metasploit提供了两个分别名为pattern_create和pattern_offset的脚本,它们位于kali系统中的/usr/share/metasploit-framework/tools/exploit目录中。
pattern_create可以帮助我们创建一个指定长度的惟一字符串,就这里来说,我们将创建一个包含100个字符的模式(pattern)。
./pattern_create.rb -l 100
现在,让我们在GDB中运行该程序,对于我而言,使用的是gdb-peda。
首先,我们需要在main函数中设置一个断点。
break *main
这样一来,程序就会在main()函数的第一条指令之后中断。
然后,让我们运行该程序。
现在,它在断点处如期停止运行。之后,我们按c键,让它继续运行,并向其传递我们指定的参数。
这时,可以看到发生了segfault错误,并可以看到该错误发生的具体位置,即0x63413163。
现在,我们将使用pattern_offset来了解0x63413163处存放的内容。
./pattern_offset -l 100 -q 63413163
如图所示,与偏移64处的内容是完全匹配的,这意味着缓冲区大小为64个字符,大于这个阀值就会发生溢出。
查找函数的内存地址
如果我们在GDB中运行info functions命令,就能看到所有函数及其内存地址,此外,我们也可以使用objdump来完成这一任务。但是,我们要找的函数是什么呢?
info functions
如图所示,这里有很多函数,但我们最感兴趣的一个函数是“win”,并且,它在我的Kali Box系统上的地址与在protostar机器上的地址并不相同。于是,我们返回protostar机器,并通过objdump来定位它。
objdump -d stack3
如图所示,我们得到的地址为0x08048424。
利用漏洞
现在,我们可以轻松地构建相应的漏洞利用程序了——我们已经知道缓冲区长度为64个字符,所以,我们可以传递一个函数的地址,然后通过函数指针来执行它。
python -c "print 'A' * 64 + '\x24\x84\x04\x08'" | ./stack3
如图所示,这里的输出内容为“Code flow changed successfull”。
上面,我们已经在不借助源代码的情况下搞定了这个挑战,接下来,我们不妨看看这个程序的源代码到底长啥样:
#include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <string.h> void win() { printf("code flow successfully changed\n"); } int main(int argc, char **argv) { volatile int (*fp)(); char buffer[64]; fp = 0; gets(buffer); if(fp) { printf("calling function pointer, jumping to 0x%08x\n", fp); fp(); } }
我们看到,该程序先定义了一个函数win(),然后,又定义了函数main(),并在该函数中定义了一个函数指针,创建了一个长度为64个字符的缓冲区,并将其值设置为0。之后,它会接受我们提供的参数,并将其存储在该缓冲区中。最后一条if语句会检查函数指针的值是否发生了改动,即是否依然为0;如果不为0的话,就根据新值来调用相应的函数。
本文到此结束,希望对大家有所帮助!