栈溢出进阶(一)

发布于 2023-03-17  430 次阅读


针对于canary的进一步利用手段:

: stack smash

样栗一:

存在明显的栈溢出,跑起来
输入一个很长的字符串,程序报错了,应该是开启了stack保护,但发现程序报错时也把文件名给打印出来了,推测程序检测到攻击者注入数据后调用的函数中包括打印文件名的函数,那么设想把这个打印文件名函数的地址给覆盖成自己想要执行的函数的地址,那么就算程序报错也能打印出flag;但是我们知道文件名并不会存在二进制文件中,也就是说当我们改变二进制文件名时并不会改变二进制文件本身,但是发生栈溢出时,程序报错并把文件名输出出来,推测,文件名在程序一开始执行时就把文件名读到了内存当中,报错时调用了一系列函数其中就包含打印文件名的函数,那么如果我们知道了flag的地址,把flag的地址填充到打印文件名的函数的地址,程序就算G也能打印出flag。

gdb调试看看程序G的时候发生了什么

可以看到当程序检查到Canary被破坏之后,程序call了 stack_chk_fail 函数,

紧接着又call了_fortify_fail函数

看下源码

打印出了前一段报错信息,stack smashing detected

再看fortify_fail函数

调用了_libc_message函数,这个函数功能类似于printf ,两个%s 是格式化字符串,第一个%s对应的是msg,第二个对应的是表达式_libc_argv[0] ?: "<unkonwn>",若非空,则输出_libc_argv[0],若空则输出"<unkonwn>",我们知道main函数其实是由两个参数的,一个为int类型,一个为char类型的字符串数组,_libc_argv[0]就是char类型的数组,0号元素就代表该数组的地址,保存的就是文件名。接下来就是把flag的地址覆盖在这个数组的地址。

拖进ida

点击flag查看地址
然后gdb调试看看输出文件名函数的地址距离我们输入字符串的地址有多远。
目的位置,
输入的位置

计算

exp
成功报错但打印出了flag。

样栗二:

由于flag保存到了栈中,若想利用stack smash的话先泄露栈地址。

进入while循环,判断v7是否大于等于v8,v8是3,v7初始是0,若判断为真,则程序就return ,main函数结束,若为假,则调用

这个函数:是fork一个子进程,如下图:

我们知道,第一次fork的话是父进程fork的子进程,会返回一个子进程的id,则if里边的判断会为假,执行++v7,然后fork的子进程再fork进程,而此时是子进程fork的进程,会返回一个返回值0,则此时if里的判断会为真,则break。

break后继续接下来的程序

肯定是猜不对的。考虑stack smash,但是得先知道栈地址。而前边我们知道是可以fork3次的,可以考虑先泄露libc,再泄露libc中的environ,而environ中保存着一个栈地址。然后计算保存flag的buf到栈地址的偏移,最后就能输出flag。

gdb调试,断电打在gets,运行到比对flag处:

发现我们输入的位置,然后查看栈中情况

用ded8减去我们输入字符串的地址ddb0,为128。(其实这就是相当于不借助ida分析缓冲区大小,直接gdb调试来知道缓冲区大小)

然后泄露libc地址

再次调试,gets下断点,计算environ中保存的栈 到flag的偏移

然后 dee8减去dd80,168的偏移

来通过environ泄露其中保存的栈地址,再减去偏移输出flag。

成功输出flag

注意:这是在libc2.23。

libc2.27下与2.23不一样,会多一个参数false

由于会传进来一个false,所以need_backtrace为false,这 个表达式只会输处unknown。也就是说stack smash在libc2.27已经不行了。

在2.31下也不行,

被砍了一个格式化字符%s。因此,stack smash方法在2.23以后已经不可行了。但libc2.23还是可行的。

二:多进程下的爆破

先来看下fork函数:

pid_t实际作用相当于int , fork的作用就是创建一个新的进程——子进程,这就意味着父进程和子进程的canary的值是一样的。

看一个栗题的源代码。

声明的一个void init函数作用是关闭三个缓冲区,接着是一个backdoor函数 ,func函数存在着的溢出点,但能溢出的不多,大概率是没办法像stack smash一样覆盖到打印文件名的函数地址的。再看main函数,先inits初始化 ,然后设置pid=0, 然后进入while死循环,先fork一下赋值给pid,再判断pid的值,如果小于零就print(error)并且退出死循环;如果pid=0,那么久调用func函数并打印出func;如果pid不等于0也不小于0 ,那就wait, 也就意味着父进程成功创建了子进程,这里的wait函数的作用就是把父进程挂起等待子进程执行,直到子进程运行完毕在重启父进程。

利用思路,我们知道64位程序canary是八个字节,其中最低字节默认为\x00,如下

我们需要爆破的是前七个字节,因为前边提过,子进程会完全复制父进程地址空间的内容,也就是说canary是一样的,每次我们利用子进程进行溢出,每次只溢出一个字节,如果这个字节溢出对的话程序不会崩溃就会向下执行puts good,如此反复直到爆破完七位得到canary,代码如下:
canary初始是'\x00',也就是canary的第八个字节,第一个for循环是爆破七次,也就是七个字节,第二个 for循环就是爆破每个字节位上对应的值,从0到0xff,每次爆破完后先输出good,再puts下次的input your name ,recv一下,检查是否返回good,如果返回good,canary就加一位爆破成功的字节,然后打印出当前的canary,然后跳出本次循环接着爆破下一个字节,爆破完七个字节后就打印出完整的canary。

exp如下:


穿过云层我试着努力向你奔跑