CTF-All-In-One/doc/4.12_stack_chk_fail.md
2018-04-14 17:14:24 +08:00

200 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 4.12 利用 __stack_chk_fail
- [回顾 canary](#回顾-canary)
- [libc 2.23](#libc-2.23)
- [CTF 实例](#ctf-实例)
- [libc 2.25](#libc-2.25)
## 回顾 canary
在章节 4.4 中我们已经知道了有一种叫做 canary 的漏洞缓解机制,用来判断是否发生了栈溢出。
这一节我们来看一下,在开启了 canary 的程序上,怎样利用 `__stack_chk_fail` 泄漏信息。
一个例子:
```c
#include <stdio.h>
void main(int argc, char **argv) {
printf("argv[0]: %s\n", argv[0]);
char buf[10];
scanf("%s", buf);
// argv[0] = "Hello World!";
}
```
我们先注释掉最后一行:
```text
$ gcc chk_fail.c
$ python -c 'print "A"*50' | ./a.out
argv[0]: ./a.out
*** stack smashing detected ***: ./a.out terminated
Aborted (core dumped)
```
可以看到默认情况下 `argv[0]` 是指向程序路径及名称的指针,然后错误信息中打印出了这个字符串。
然后解掉注释再来看一看:
```
$ python -c 'print "A"*50' | ./a.out
argv[0]: ./a.out
*** stack smashing detected ***: Hello World! terminated
Aborted (core dumped)
```
由于程序中我们修改 `argv[0]`,此时错误信息就打印出了 `Hello World!`。是不是很神奇。
main 函数的反汇编结果如下:
```
gef➤ disassemble main
Dump of assembler code for function main:
0x00000000004005f6 <+0>: push rbp
0x00000000004005f7 <+1>: mov rbp,rsp
=> 0x00000000004005fa <+4>: sub rsp,0x30
0x00000000004005fe <+8>: mov DWORD PTR [rbp-0x24],edi
0x0000000000400601 <+11>: mov QWORD PTR [rbp-0x30],rsi
0x0000000000400605 <+15>: mov rax,QWORD PTR fs:0x28
0x000000000040060e <+24>: mov QWORD PTR [rbp-0x8],rax
0x0000000000400612 <+28>: xor eax,eax
0x0000000000400614 <+30>: mov rax,QWORD PTR [rbp-0x30]
0x0000000000400618 <+34>: mov rax,QWORD PTR [rax]
0x000000000040061b <+37>: mov rsi,rax
0x000000000040061e <+40>: mov edi,0x4006f4
0x0000000000400623 <+45>: mov eax,0x0
0x0000000000400628 <+50>: call 0x4004c0 <printf@plt>
0x000000000040062d <+55>: lea rax,[rbp-0x20]
0x0000000000400631 <+59>: mov rsi,rax
0x0000000000400634 <+62>: mov edi,0x400701
0x0000000000400639 <+67>: mov eax,0x0
0x000000000040063e <+72>: call 0x4004e0 <__isoc99_scanf@plt>
0x0000000000400643 <+77>: mov rax,QWORD PTR [rbp-0x30]
0x0000000000400647 <+81>: mov QWORD PTR [rax],0x400704
0x000000000040064e <+88>: nop
0x000000000040064f <+89>: mov rax,QWORD PTR [rbp-0x8]
0x0000000000400653 <+93>: xor rax,QWORD PTR fs:0x28 # 检查 canary 是否相同
0x000000000040065c <+102>: je 0x400663 <main+109> # 相同
0x000000000040065e <+104>: call 0x4004b0 <__stack_chk_fail@plt> # 不相同
0x0000000000400663 <+109>: leave
0x0000000000400664 <+110>: ret
End of assembler dump.
```
所以当 canary 检查失败的时候,即产生栈溢出,覆盖掉了原来的 canary 的时候,函数不能正常返回,而是执行 `__stack_chk_fail()` 函数,打印出 `argv[0]` 指向的字符串。
## libc 2.23
Ubuntu 16.04 使用的是 libc-2.23,其 `__stack_chk_fail()` 函数如下:
```c
// debug/stack_chk_fail.c
extern char **__libc_argv attribute_hidden;
void
__attribute__ ((noreturn))
__stack_chk_fail (void)
{
__fortify_fail ("stack smashing detected");
}
```
调用函数 `__fortify_fail()`
```c
// debug/fortify_fail.c
extern char **__libc_argv attribute_hidden;
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>");
}
libc_hidden_def (__fortify_fail)
```
`__fortify_fail()` 调用函数 `__libc_message()` 打印出错误信息和 `argv[0]`
还有一个错误信息输出到哪儿的问题,再看一下 `__libc_message()`
```c
// sysdeps/posix/libc_fatal.c
/* Abort with an error message. */
void
__libc_message (int do_abort, const char *fmt, ...)
{
va_list ap;
int fd = -1;
va_start (ap, fmt);
#ifdef FATAL_PREPARE
FATAL_PREPARE;
#endif
/* Open a descriptor for /dev/tty unless the user explicitly
requests errors on standard error. */
const char *on_2 = __libc_secure_getenv ("LIBC_FATAL_STDERR_");
if (on_2 == NULL || *on_2 == '\0')
fd = open_not_cancel_2 (_PATH_TTY, O_RDWR | O_NOCTTY | O_NDELAY);
if (fd == -1)
fd = STDERR_FILENO;
```
环境变量 `LIBC_FATAL_STDERR_` 通过函数 `__libc_secure_getenv` 来读取,如果该变量没有被设置或者为空,即 `\0``NULL`,错误信息 stderr 会被重定向到 `_PATH_TTY`,该值通常是 `/dev/tty`,因此会直接在当前终端打印出来,而不是传到 stderr。
## CTF 实例
CTF 中就有这样一种题目,需要我们把 `argv[0]` 覆盖为 flag 的地址,并利用 `__stack_chk_fail()` 把flag 给打印出来。
实例可以查看章节 6.1.13 和 6.1.14。
## libc 2.25
最后我们来看一下 libc-2.25 里的 `__stack_chk_fail`
```c
extern char **__libc_argv attribute_hidden;
void
__attribute__ ((noreturn))
__stack_chk_fail (void)
{
__fortify_fail_abort (false, "stack smashing detected");
}
strong_alias (__stack_chk_fail, __stack_chk_fail_local)
```
它使用了新函数 `__fortify_fail_abort()`,这个函数是在 [BZ #12189](https://sourceware.org/git/?p=glibc.git;a=commit;h=ed421fca42fd9b4cab7c66e77894b8dd7ca57ed0) 这次提交中新增的:
```c
extern char **__libc_argv attribute_hidden;
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>"));
}
void
__attribute__ ((noreturn))
__fortify_fail (const char *msg)
{
__fortify_fail_abort (true, msg);
}
libc_hidden_def (__fortify_fail)
libc_hidden_def (__fortify_fail_abort)
```
函数 `__fortify_fail_abort()` 在第一个参数为 `false` 时不再进行栈回溯,直接以打印出字符串 `<unknown>` 结束,也就没有办法输出 `argv[0]` 了。
就像下面这样:
```
$ python -c 'print("A"*50)' | ./a.out
argv[0]: ./a.out
*** stack smashing detected ***: <unknown> terminated
Aborted (core dumped)
```