Stack smash

在程序加了 canary 保护之后,如果我们读取的 buffer 覆盖了对应的值时,程序就会报错,而一般来说我们并不会关心报错信息。而 stack smash 技巧则就是利用打印这一信息的程序来得到我们想要的内容。这是因为在程序启动 canary 保护之后,如果发现 canary 被修改的话,程序就会执行 __stack_chk_fail 函数来打印 argv[0] 指针所指向的字符串,正常情况下,这个指针指向了程序名。其代码如下:

1
2
3
4
5
6
7
8
9
10
11
void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
__fortify_fail ("stack smashing detected");
}
void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg)
{
/* The loop is added only to keep gcc happy. */
while (1)
__libc_message (2, "*** %s ***: %s terminated\n",
msg, __libc_argv[0] ?: "<unknown>");
}

所以说如果我们利用栈溢出覆盖 argv[0] 为我们想要输出的字符串的地址,那么在 __fortify_fail 函数中就会输出我们想要的信息。

EKOPARTY PRE-CTF 2015: Smashing the stack for fun and profit

这里结合ekoparty-pre-ctf-2015的一道Pwn题来学习,题目下载地址

在本地运行前,需要在本地先建立一个flag.txt。

运行程序,先给个有趣的地址,然后给你输入用户名;file查看是静态链接文件;checksec发现开启了NX和Canary:

IDA看下main()函数,程序会打开本地的flag.txt文件,然后复制该文件描述符到一个buf中,接着输出一个指向该buf的地址并询问你的用户名,通过read()函数获取0x400长度的用户输入的内容,而0x400数值过大,由此可知read()函数存在栈溢出漏洞:

这样看应该是直接给了获取flag的地址了。

回到之前安全编译选项检测的结果发现,程序是开了Canary的,根据Canary的原理,其在一个函数入口处从fs段内获取一个随机值,一般存到EBP - 0x4(32位)或RBP - 0x8(64位)的位置,如果攻击者利用栈溢出修改到了这个值,导致该值与存入的值不一致,__stack_chk_fail()函数将抛出异常并退出程序。

先试下发送大量字符,查看到抛出异常信息,即调用了Canary实现代码中的__libc_message()将__libc_argv[0]打印了出来,这里__libc_argv[0]为本程序名,同时发现程序只是终止了而非栈溢出时的段错误:

也就是说,只要溢出的内容覆盖了Canary插入的cookie,Canary就会报错,且当前输入200个字符还未达到覆盖到argv[0]的位置。整个栈结构及溢出原理如下图所示:

下面只需要确定溢出的变量地址距离argv[0]的偏移地址。

Method1——使用pattern

之前pattern_create生成的200个字符串并未使程序出现段错误,后面尝试到400个字符串时发生段错误,没有输出Canary出错返回的异常信息而是输出Segmentation fault,即输入字符溢出覆盖了argv[0]地址导致程序无法正常读取该值从而发生错误:

pattern_offset得到偏移量为376:

Method2——断点调试

先在main()函数打断点,运行:

可以看出0x7fffffffe2bd保存的是程序名,其自然就是 argv[0],所以我们修改的内容就是这个地址。同时0x7fffffffdf48处保留着该地址,所以我们真正需要覆盖的地址是0x7fffffffdf48。

此时RBP的地址为0x7fffffffde60;在前面IDA中可以看到变量v8相对于RBP的偏移量为90h,因此可算出v8变量的地址为RBP-90h=0x7fffffffddd0。

最后可以算出v8变量到需要覆盖的地址的偏移量为:|0x7fffffffddd0 - 0x7fffffffdf48|=178h=376(d)

编写payload:

1
2
3
4
5
6
7
8
9
from pwn import *

sh = process("./xpl")
addr = int(sh.recvline().split()[4], 16)

payload = flat(["A" * 376, p64(addr)])
sh.sendline(payload)
result = sh.recvall()
print result

运行getflag: