Canary5种绕过方式 栈溢出保护及整数保护
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Canary5种绕过方式 栈溢出保护及整数保护相关的知识,希望对你有一定的参考价值。
参考技术A 栈溢出保护及整数保护说点什么
之前觉得学习原理很没有用,不如实践去执行
原理很多、很杂,很没用
真正去做时发现,原理是能做、不能做
从根本上指导实践,减少100%的试错时间
知行合一...
Canary原理
Canary是在栈的尾部插入值,函数返回时检测canary是否改变,判断是否溢出
利用gcc编译:
gcc ... -fstack-protector
如果Canary检查到,会调用__stack_chk_failed函数
绕过:
绕过方法1
至少利用两次栈溢出,才可以使用绕过方法1
V3后代表开启canary,所以v3是canary字节
第一次打印canary,第二次利用
buf距离ebp,10C个字节,v3距离ebp,C个字节,所以buf为100个字节
gdb:
dias main
disas vuln
这几个指令代表是有canary,
gdb可以创建随机字符
检测到栈溢出
利用gdb调试
可以看到,canary的末尾值为00
可以看到,ebp-0xC即为canary值,
0xffc66a68就是ebp的值,
0x08048695就是返回地址,所以canary后要有3个字节才是返回地址
sendline函数会添加回车再发送,所以是0x101个字符
收到的4个字符就是canary,减去回车 0xa就是canary。
注意:回车会导致多输入一个字符
Canary会随机变化,但是fork的函数中canary可以进行爆破
绕过方法2
调用多次的canary值和父进程完全一样,因此可以爆破
先填充0x100个字节,接上cannary值,canary4个字节,所以最后1字节是0x00,前三个字节进行爆破,如果接收到被篡改那么输出信息
绕过方法3
修改该函数的got表即可
最多溢出到canary,不能覆盖到ebp与ret地址,所以要利用格式化字符串漏洞,将getshell函数转换到chk_fail函数的got表,使得栈溢出触发getshell函数。
格式化字符串三点:改写的地址,写入的值,格式化字符串是第几个参数
chk_fail函数之前没调用过,所以是plt的第二条指令
get_shel高字节与chk_fail相同,因此只需要改低字节
%%%dc实现输出addr-4个字符, addr-4是因为stack_chk_fail先输出,是4个字节
最后将前面参数对应的数字,写入到第7个参数(buf对应第几个)对应的地址
绕过方法4
v4:buf大小是0x100
IO_gets就是gets,不限长度输入,以0字符截断,将flag地址进行覆盖
注意:本地的flag内容与远程的flag不相同!
找到chk_fail函数的argv[0]与flag地址的偏移,才能覆盖
64位因此参数传递是通过寄存器,rdi,rsi,rdx,rcx,r8,r9
ELF文件如果较小,会有多重映射
因此用find命令寻找字符串与地址
找到初始地址,0x400d20,也就是要修改的地址
0x218是地址偏移,当执行stack_chk_fail时,就会把new_flag_addr的值输出出来
绕过方法5
canary初始值就是TLS结构中stack_guard,
修改stack_guard就可以绕过canary
输入远大于 char s 数组,可以溢出
gdb中如果没有 符号表(striped),调试不方便 ,但是可以结合IDA与gdb进行调试
栈离TLS距离较近,则可以溢出
所以填充(TLScanary地址 - buf起始地址)的无用值
注意溢出时也会有canary,这个canary也要修改成一样的
这道题没有现成getshell,因此需要第一遍获取system地址,第二遍执行/bin/sh
利用read函数读入system函数,产生栈偏移到data段直接执行system函数,再构造read链,
read函数3个参数,因此填充
pop_rdi_ret能够传递参数
leave指令将rbp移动到rsp,因此栈会偏移
GCC栈溢出保护
逆向过elf程序都知道,GCC的canary,x86_64下从fs:0x28偏移处获取,32位下从gs:0x14偏移处获取。但知道canary如何产生,为什么在这里取的人比较少。
下面以x86_64平台为例,通过glibc源码分析一下。
看第一个问题:为什么从%fs:0x28处取。%fs寄存器被glibc定义为存放tls信息,查看tls结构:
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; int gscope_flag; uintptr_t sysinfo; uintptr_t stack_guard; /* canary,0x28偏移 */ uintptr_t pointer_guard; …… } tcbhead_t;
可以看到%fs:0x28实际取的是当前线程控制块的stack_guard变量,这个变量在线程创建时已经固定。下面看第二个问题,stack_guard如何赋值的。
Linux加载器完成elf加载后,会将入口设置为_start,并在栈上为_start提供入参。_start的代码在sysdeps/x86_64/start.S文件中。
_start从栈上取参数,然后调用__libc_start_main()函数,这个函数也是在main()函数之前执行:
58 ENTRY (_start) 59 /* Clearing frame pointer is insufficient, use CFI. */ 60 cfi_undefined (rip) 61 /* Clear the frame pointer. The ABI suggests this be done, to mark 62 the outermost frame obviously. */ 63 xorl %ebp, %ebp 64 65 /* Extract the arguments as encoded on the stack and set up 66 the arguments for __libc_start_main (int (*main) (int, char **, char **), 67 int argc, char *argv, 68 void (*init) (void), void (*fini) (void), 69 void (*rtld_fini) (void), void *stack_end). 70 The arguments are passed via registers and on the stack: 71 main: %rdi 72 argc: %rsi 73 argv: %rdx 74 init: %rcx 75 fini: %r8 76 rtld_fini: %r9 77 stack_end: stack. */
__libc_start_main()首先以_dl_random这个全局变量为入参,生成canary,然后通过THREAD_SET_STACK_GUARD宏将canary赋值给tls的stack_guard变量。
198 /* Set up the stack checker‘s canary. */ 199 uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random); 200 # ifdef THREAD_SET_STACK_GUARD 201 THREAD_SET_STACK_GUARD (stack_chk_guard); 202 # else 203 __stack_chk_guard = stack_chk_guard; 204 # endif
看下_dl_random哪里来的,在glibc源码中,有2处但实现大致相同:
126 /* Random data provided by the kernel. */ 127 void *_dl_random; 288 case AT_RANDOM: 289 _dl_random = (void *) av->a_un.a_val;
注意av这个变量,逆向跟踪发现其最终来自__libc_start_main()的argv参数。也就是_dl_random是由加载器提供的。而AT_RANDOM表示内核提供了接口,支持canary的随机数生成。可以使用下面命令查看:
[email protected] :~/glibc-2.22$ LD_SHOW_AUXV=1 /bin/true grep AT_RANDOM AT_RANDOM: 0x7fffdaf776e9
看下实际代码中,这个内核接口指的是什么,canary值又如何取。
rand_size = CONFIG_SECURITY_AUXV_RANDOM_SIZE * sizeof(unsigned long); u_rand_bytes = NULL; if (rand_size) { unsigned char k_rand_bytes[CONFIG_SECURITY_AUXV_RANDOM_SIZE * sizeof(unsigned long)]; get_random_bytes(k_rand_bytes, rand_size); u_rand_bytes = (elf_addr_t __user *)STACK_ALLOC(p, rand_size); if (__copy_to_user(u_rand_bytes, k_rand_bytes, rand_size)) return -EFAULT; }
发现在内核中通过get_random_bytes()接口产生,并copy_to_user()到用户空间。而内核中的安全随机数,也推荐使用get_random_bytes()生成。下面看下实现:
//http://lxr.free-electrons.com/source/drivers/char/random.c void get_random_bytes(void *buf, int nbytes) { #if DEBUG_RANDOM_BOOT > 0 if (unlikely(nonblocking_pool.initialized == 0)) printk(KERN_NOTICE "random: %pF get_random_bytes called " "with %d bits of entropy available\n", (void *) _RET_IP_, nonblocking_pool.entropy_total); #endif trace_get_random_bytes(nbytes, _RET_IP_); extract_entropy(&nonblocking_pool, buf, nbytes, 0, 0); } EXPORT_SYMBOL(get_random_bytes);
而看一下read /dev/urandom的内核实现:
static ssize_t urandom_read(struct file *file, char __user *buf, size_t nbytes, loff_t *ppos) { int ret; if (unlikely(nonblocking_pool.initialized == 0)) printk_once(KERN_NOTICE "random: %s urandom read " "with %d bits of entropy available\n", current->comm, nonblocking_pool.entropy_total); nbytes = min_t(size_t, nbytes, INT_MAX >> (ENTROPY_SHIFT + 3)); ret = extract_entropy_user(&nonblocking_pool, buf, nbytes); trace_urandom_read(8 * nbytes, ENTROPY_BITS(&nonblocking_pool), ENTROPY_BITS(&input_pool)); return ret; }
可以看到get_random_bytes()与read /dev/urandom实现是相同的,都是通过extract_entropy*从"entropy pool"中取的随机数。只不过一个在内核空间用,将结果返回到一块内核buffer,一个在用户空间使用,将结果返回到一块用户buffer。
下面再来看下,程序中如何使用这个canary。分析a()函数:
void a() { int a = 3; char str[16]; }
x86_64平台汇编如下:
(gdb) disass a Dump of assembler code for function a: 0x000000000040055d <+0>: push %rbp 0x000000000040055e <+1>: mov %rsp,%rbp 0x0000000000400561 <+4>: sub $0x30,%rsp 0x0000000000400565 <+8>: mov %fs:0x28,%rax 0x000000000040056e <+17>: mov %rax,-0x8(%rbp) 0x0000000000400572 <+21>: xor %eax,%eax 0x0000000000400574 <+23>: movl $0x3,-0x24(%rbp) ;变量重排,a的地址低于str地址 0x000000000040057b <+30>: mov -0x8(%rbp),%rax 0x000000000040057f <+34>: xor %fs:0x28,%rax 0x0000000000400588 <+43>: je 0x40058f <a+50> 0x000000000040058a <+45>: callq 0x400440 <[email protected]> 0x000000000040058f <+50>: leaveq 0x0000000000400590 <+51>: retq End of assembler dump.
可以看到,GCC的栈保护还实现了变量重排。但与微软实现不同,GCC取出canary后并没有与ebp异或,直接放到栈上。也就是说,同一线程中,所有的canary值都是相同的,通过调试验证也中如此:
Breakpoint 1, 0x000000000040056e in a () at 1.c:4 4 void a() { (gdb) p/x $rax $1 = 0xc609d364696f6000 (gdb) c Continuing. Breakpoint 2, 0x00000000004005a2 in b () at 1.c:9 9 void b() { (gdb) p/x $rax $2 = 0xc609d364696f6000 (gdb) c Continuing. Breakpoint 1, 0x000000000040056e in a () at 1.c:4 4 void a() { (gdb) p/x $rax $3 = 0xc609d364696f6000 (gdb)
以上是关于Canary5种绕过方式 栈溢出保护及整数保护的主要内容,如果未能解决你的问题,请参考以下文章
内存保护机制及绕过方案——利用未启用SafeSEH模块绕过SafeSEH