刚刚开始接触 pwn 没多久,在大佬的指引下尝试做了这个题目,虽说参考了WP才做出来,但是作为 pwn 的第一个实战题目,还是很值得纪念的!
打开题目页面,会看到如下提示:
既然给出了ssh链接,那么就在linux下尝试连接一下:
在输入了密码 guest 之后成功登陆服务器。
使用 pwd 命令来看一下现在在哪个目录:
接着使用 ls -l 命令来查看一下当前目录都有哪些文件:
可以看到目录下有名为flag的文件,尝试看看能否打开它:
很遗憾,当前的用户并没有权限打开它。
目录下还有另外两个文件,passcode和passcode.c
很明显,其中一个是C语言源码文件,而另一个就是编译过的C程序了。
我们来运行一下这个程序:
要求用户输入名字和passcode1,但是输入passcode1之后就会报出段错误,什么是段错误?
1 | 段错误就是指访问的内存超出了系统所给这个程序的内存空间.--百度百科 |
关于段错误后面还有很专业的解释,这里不去详细解读,单看这第一句就可以知道,程序访问了本不应该访问到的内存空间(或者是根本不存在的内存空间)。
既然知道了错误的原因,不妨看一下源码中是什么导致了错误的发生。
打开 passcode.c 文件:
为了浏览方便,把源码复制到本地文件中。
很明显的错误,在读取passcode1和passcode2的scanf函数处,并没有加 & 取地址符号,但为什么读取name的scanf也没有加取地址符号,但没有出现错误呢?
原因是 name 是一个char型数组,它本来就代表一个地址,而passcode1和passcode2是int型变量,如果不加取地址符号的话,由于passcode1和passcode2中是随机值,我们的输入会被存到这个随机值所指向的地址,而这个地址可能是系统不允许访问的,或者是根本不存在的,所以会出现段错误(学过C语言的同学应该印象很深刻)。
好了,错误的原因找到了,下面来分析一下程序逻辑。
首先在main函数中可以看到,连续调用了函数welcome和login,这里需要知道一个知识点,如果两个函数是连续调用的,那么它们的栈基地址(ebp)是一样的。
在welcome函数中,程序声明了一个长度为100的char型数组,并要求用户输入名字,这里就很奇怪了,按照道理来说一般的名字不可能有100个字符之长。
随后进入了login函数,要求用户输入passcode1 和 passcode2,并在随后的条件判断中,passcode1的值应该是338150 而 passcode2 的值应该是13371337(关于1337大家可以百度一下 ^_^).
只有这样,程序才能调用system函数去打开flag文件,但是我们又无法正常输入,这时就需要借助栈溢出来拿flag了。
正片开始!!
开始之前先介绍一下栈溢出的‘四大天王’:
- shellcode
- rop
- return2libc
- hijack GOT
通过观察程序的源代码,有两种思路去拿到flag:
1. 可以发现,在welcome函数中的name数组十分可疑,它所包含的空间足够去编写一段shellcode了,所以第一种方式就是编写一段SHELLCODE直接运行 cat flag 命令即可,但是shellcode拿系统权限有2个大前提,即没有开启ASLR(如果开启了,我们可以通过‘滑雪橇’的方法绕过)而且需要栈上的数据拥有可执行权限(这个就无法绕过了。PS.可能是我太菜。。。。)。 2. 就是通过溢出name数组从而淹没passcode1和passcode2并将其中的值修改为对应的正确值即可,但是这种方法需要程序未开启栈溢出保护。
使用python来看一下这个程序是否满足这些条件:
很不幸,程序开启了NX(数据执行保护),那么写shellcode的方法就行不通了,而且程序开启了栈溢出保护,这也就意味着,四大天王中的3个倒下了。
但是,我们还有希望,依然可以通过覆写函数GOT表的方式来达到目的!
什么是GOT表呢? 在Linux中,为了优化用户体验和更好的CPU利用率,系统提供了GOT表(全局函数表)和PLT表(局部/内部函数表)
PLT和GOT相互对应。
那么覆写GOT表对溢出攻击有什么帮助呢?
通过上面的介绍,我们知道,程序如果想要调用有关的系统函数,就需要从GOT表中得到目标函数在内存中的地址,如果可以找到一种方法将GOT表中某个函数的地址修改为SHELLCODE地址,或者修改为包含我们希望程序执行的代码的地址(如,将printf函数的地址修改为system函数的地址,这样,一旦程序再次调用printf函数,就会被GOT表引导调用了system函数),那么就间接控制了程序流程,从而拿到我们想要的东西。
这道题目中,我们想要的,就是令函数执行system(“/bin/cat flag”)这条语句,那么可以将GOT表中某个函数的地址修改为这条指令的地址,从而拿到flag。
溢出方法已经确定了,下面来分析该从哪里进行溢出,显而易见,welcome函数中的name数组具有得天独厚的位置:
- 主函数中连续调用了welcome和login函数,导致它们拥有相同的基地址。
- name数组空间很大。
基于以上两点,溢出思路就是,找到name数组在栈中的位置,接着找到passcode1在栈中的位置(注意哦,这里由于name空间很大,不需要溢出数组,也就不会触发栈溢出保护机制了),通过在name数组中填写一定长度的字符,覆盖到passcode1之前,由于在获取用户输入的时候没有加入取地址符号,我们可以将passcode1的值覆盖成GOT表中printf函数的值(因为在scanf(passcode1)之后紧接着就是一句printf),而通过scanf输入system(“/bin/cat flag”)的地址,这样,GOT表中printf函数的地址就被修改成目标代码的地址,当程序再次调用printf函数时,就会运行目标代码,我们就拿到flag了。
具体操作
首先使用objdump查看一下程序的反汇编代码(也可以把源码拷贝下来,GCC编译之后使用IDA查看),
首先找到name数组和passcode1相对于基地址的位置:
name:
passcode1:
name相对于ebp偏移量为 ebp-0x70,passcode1相对于ebp的偏移量为 ebp-0x10。
0x70-0x10=96,也就是说,name中要先填充96个任意的字符来达到passcode1之前,再取出GOT表中printf函数的地址来覆盖passcode1,接着程序会运行到scanf这里,我们输入目标代码的地址就好啦。
先找到目标代码的地址:
0x80485e3,这里需要把这个值转为十进制(134514147),这是因为,scanf函数使用 %d 来接受输入。
接着来查看一下printf函数在GOT表中的地址:
0x0804a000。
下面我们来构造payload
payload = padding + printf_addr + ‘\n’ + system_addr + ‘\n’
使用pwntools来写payload:
拿到flag:Sorry mom.. I got confused about scanf usage :(
稍微总结一下
这是第一个pwn的实战题目,以前总觉得理论知识很简单,很枯燥,但是到了实际操作却感到比较费力,却也十分有趣,正如荀况的一句:不闻不若闻之,闻之不若见之,见之不若知之,知之不若行之。