这几天一直在跟着团队的进度做csapp的实验报告,突然想拿题来练手了才发现自己还是有点菜的。这次的任务是做一道简简单单的带canary保护的一题,做了很长时间问了很多师傅,也算把这个方法搞搞明白了,但是不确定能不能完全说的明白,那么下面就开始吧。

canary保护

canary就是一段简简单单的cookie,它一般在上个函数所保存的ebp之前(即靠近栈顶的那个方向)下面一个图能描述一个带canary保护的栈帧,应该还是比较清楚的。

1
2
3
4
5
6
7
8
9
10
11
↑栈顶
buf//函数的缓冲区,一般用于保存局部变量
canary
ebp//上一个函数保存的ebp
eip//返回地址
argument 1
argument 2
argument 3
...
...
↓栈底

如果企图用大量数据覆盖缓冲区并且修改返回地址的数据达到劫持eip的目的那么就会修改canary的值,那么在返回的时候检测到canary的值发生改变后就会直接抛出异常并且停止执行程序。并且每次canary的值都是随机的,普通方法几乎是突破不了的。但是我们可以先想办法泄露canary的值,然后再把canary插入到payload当中,这样的话,就算我溢出了,但是并没有修改canary的值,也就没办法检测到我有没有栈溢出了。在64位的程序当中,canary就是一个七字节的数据带一个\x00字节,并且\x00字节在最低位。

那么回想一下字符串是什么?字符串就是一串连着的字节序,不管它原本在这个地方的定义是什么,我说它是字符串,他就可以是字符串,因为计算机它不管是存什么数据它终归也只是0和1的组成。例如0x61626364它看上去好像是一个int型变量,但是它储存也只是

1
64 63 62 61

如果我把它看成字符串那也没错,它可以代表字符串dcba,它们存储的数据是一模一样的。但是有一个问题:计算机里基本上都是又很多字节连在一起,那如果后面还有很多数据,怎么样才能只表示字符串dcba呢?那就需要一个特殊字节\x00了,识别字符串会从一个字符指针开始,然后依次增大指针的值,只要指针所指向的地址不是\x00字节,那么它就可以是这个字符串中的一员。那如果我输入一个字符串,溢出了但没完全溢出呢?我们把字符串填充地恰到好处,刚好紧挨着canary,那么在之后如果printf这个字符串的话,就会把canary一起输出出来,我们就获取了canary。但是注意,canary最后一个字节是\x00,不会被接受,因此在获取canary的时候注意末尾加上\x00字节。

下面来看道例题:

Bugku - Pwn4

下载文件照例拖进虚拟机checksec检查一下各种保护

发现只开启了canary和堆栈不可执行的保护,堆栈不可执行那么就不能注入代码,一般就直接rop攻击,这没什么好讲的。拖进IDA-F5主函数看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[48]; // [rsp+10h] [rbp-240h] BYREF
char v5[520]; // [rsp+40h] [rbp-210h] BYREF
unsigned __int64 v6; // [rsp+248h] [rbp-8h]

v6 = __readfsqword(0x28u);
init(argc, argv, envp);
write(1, "Welcome!\n", 0x10uLL);
write(1, "Please leave your name(Within 36 Length):", 0x29uLL);
read(0, buf, 0x300uLL);
printf("Hello %s\n", buf);
write(1, "Please leave a message(Within 0x200 Length):", 0x2CuLL);
read(0, v5, 0x300uLL);
printf("your message is :%s \nBye~", v5);
return 0;
}

可以看到这里执行了两次read()函数,并且读取的大小远远超过缓冲区开辟的大小,基本上可以说无限制吧。然后第一次read之后还有一次printf()输出,那很明显,第一次就让你泄露canary,第二次就去执行shell函数,如果不确定缓冲区的大小,那么可以双击buf变量看看和离本函数rbp的相对偏移。

可以看到相对rbp的偏移为-0x240而且canaryrbp-8的位置,那么我们第一次的payload就很容易得知是b'a'*(0x240-8-1)了。然后用recv函数接受它输出的字符串,那么我们知道前面的Hello+sapce以及我们输入的0x237a一共有0x23e个字节,这些都是垃圾数据,在接收完这些数据之后我们再往后接收七个字节,这些数据就是canary。那么分析到这里我们的exp就很好写了

在这里p.sendline对应b'a'*0x237然后你可以直接p.recvline()直接接收完垃圾数据或者是p.recv(0x23e)指定接收数据的长度的,因为p.sendline()会在指定的字符串之后添加一个\n字节,所以你前面只能有0x237 个填充的字节,然后此时p.recvline()在接收到\n字节后停止,这个和之前讲的字符串的判定类似。但是如果直接p.send()的话就可以填充满,并且你只能用第一个方法接收垃圾数据。那这样的话canary就成功被我们泄露并且保存了,接下来就是构造shell函数了。我们shift+F12查看字符串,一般pwn题的突破口都在这里,甚至逆向题也是如此。

查到bin/sh(0x601068)字符串,并且system函数直接在表里可以查到0x400660,但是往后面调用了system函数以及堆了那个参数之后才注意到这是64位程序,所以我们很轻(jian)松(nan)地可以得知这题肯定要用rop去传参给rdi寄存器了,32位和64位的传参差异我就不过多赘述了,前面博客很多次提到过了。那么这个时候我们先写出要执行的汇编代码

1
2
movq $0x601068,%rdi
ret

但是一般来说,带一个那么多位立即数的且指令一模一样的是不可能直接让你找到的。此时肯定不能莽撞,只可智取,我们因为栈上的数据我们可以随意写(只是不能写代码,写了也不能执行啊),那么我们重新看一看栈的结构

1
2
3
canary
rbp
rip

我们如果在rip位置放一个pop %rdi的话,那么rip下方的数据就能直接被传出来,然后后面再放上system函数就完成了整个提权函数的构造了,那么我们的payload就直接是:

1
payload=...+canary+fake_rbp+pop_rdi+bin_sh_addr+system_addr

找rop碎片的方法在前面的attacklab中有提到很多的,大家可以翻翻我前面的博客,所以我们完整的exp就可以出炉了:

今天真的学到了很多知识,非常的开心,但是对于初入ctf的小白我来说,还有很长的路要走。加油!