canary绕过
仅供个人学习参考
canary保护
Canary 的意思是金丝雀,来源于英国矿井工人用来探查井下气体是否有毒的金丝雀笼子。工人们每次下井都会带上一只金丝雀。如果井下的气体有毒,金丝雀由于对毒性敏感就会停止鸣叫甚至死亡,从而使工人们得到预警。
我们知道,通常栈溢出的利用方式是通过溢出存在于栈上的局部变量,从而让多出来的数据覆盖 ebp、eip 等,从而达到劫持控制流的目的。栈溢出保护是一种缓冲区溢出攻击缓解手段,当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来让 shellcode 能够得到执行。当启用栈保护后,函数开始执行的时候会先往栈底插入 cookie 信息,当函数真正返回的时候会验证 cookie 信息是否合法 (栈帧销毁前测试该值是否被改变),如果不合法就停止程序运行 (栈溢出发生)。攻击者在覆盖返回地址的时候往往也会将 cookie 信息给覆盖掉,导致栈保护检查失败而阻止 shellcode 的执行,避免漏洞利用成功。在 Linux 中我们将 cookie 信息称为 Canary。
由于 stack overflow 而引发的攻击非常普遍也非常古老,相应地一种叫做 Canary 的 mitigation 技术很早就出现在 glibc 里,直到现在也作为系统安全的第一道防线存在。
Canary 不管是实现还是设计思想都比较简单高效,就是插入一个值在 stack overflow 发生的高危区域的尾部。当函数返回之时检测 Canary 的值是否经过了改变,以此来判断 stack/buffer overflow 是否发生。
Canary 与 Windows 下的 GS 保护都是缓解栈溢出攻击的有效手段,它的出现很大程度上增加了栈溢出攻击的难度,并且由于它几乎并不消耗系统资源,所以现在成了 Linux 下保护机制的标配
leak canary
利用格式化字符串leak
我们知道canary的值位于栈上,距离bp若干个偏移的位置,若程序中存在格式化字符串漏洞,且知晓偏移,我们就可以读出canary的值,绕过该保护
附件:https://raw.githubusercontent.com/GNchen1/Pages/main/Img/binary_200
可以看到存在栈溢出,且有格式化字符串漏洞,且有后门函数
通过汇编我们可以得知canary的地址在esp+3Ch处
exp如下:
1 | from pwn import * |
覆盖截断字符获取Canary
canary以\x00结尾,本意是阻止read,write等函数将其读出,倘若程序中存在栈溢出漏洞,我们可以试着覆盖\x00,从而读出canary
在出现类似以下漏洞时可以试着利用
1 | read(0, buf, 0x200); |
爆破canary
对于 Canary,虽然每次进程重启后的 Canary 不同 (相比 GS,GS 重启后是相同的),但是同一个进程中的不同线程的 Canary 是相同的, 并且 通过 fork 函数创建的子进程的 Canary 也是相同的,因为 fork 函数会直接拷贝父进程的内存。我们可以利用这样的特点,彻底逐个字节将 Canary 爆破出来。
下面是爆破canary的模板
1 | from pwn import * |
例题:https://raw.githubusercontent.com/GNchen1/Pages/main/Img/bin1 纯粹爆破题
exp如下:
1 | from pwn import * |
劫持__stack_chk_fail函数
还是这张图,我们可以看到若canary不对则会执行_stack_chk_fail函数,我们也可以修改_stack_chk_fail的got表,当程序执行_stack_chk_fail函数时实际上执行了后门函数(如果got表可修改且有后门函数的话)
需要利用格式化字符串漏洞
SSP leak
SSP(Stack Smashing Protect) Leak,故意触发canary保护,并且修改输出的变量argv[0]的内容来实现任意地址读
执行完_stack_chk_fail函数,程序退出,屏幕上留下一行
*** stack smashing detected ***:[XXX] terminated
[xxx]是程序的名字,那么一定是由 call __stack_chk_fail 函数输出的,而且,程序的名字一定是个来自外部的变量(毕竟ELF格式里面可没有保存程序名)。那么,我们什么时候输入过程序名呢?char *argv[] 是main函数的参数,argv[0]存储的就是程序名,且这个argv[0]就存在于栈上。想明白这个过程,那就很容易理解这个攻击方式了,既然输出的内容是一个外部变量,如果我们利用栈溢出覆盖argv[0]的指针为我们想要输出的字符串的地址,那么就实现了任意地址读取攻击。这里贴上 call __stack_chk_fail 函数的源代码,以助于更好理解这个攻击方式:
1 | void __attribute__ ((noreturn)) __stack_chk_fail (void) |
如果程序可供输入的数据大小足够大,那我们只需要无脑填入足够大数量的目标地址就行(总有一个会覆盖到argv[0])
那么究竟填几个才能刚好覆盖到argv[0]呢
这就需要计算栈顶到指向argv[0]的指针的距离
在gdb中用argv可以看到指向argv[0]的地址,如图为0x7fffffffdfb8
TSL(线程局部存储)攻击
线程局部存储(TLS),是一种变量的存储方法,每一个线程都会有一个副本,这个变量在它所在的线程内是全局可访问的,但是不能被其他线程访问到,这样就保持了数据的线程独立性。而熟知的全局变量,是所有线程都可以访问的,这样就不可避免需要锁来控制,增加了控制成本和代码复杂度。
创建线程的时候会创建一个TLS(Thread Local Storage),该TLS会存储canary的值,而TLS会保存在stack高地址的地方
主线程中的TLS通常位于mmap映射出来的地址空间里,而位置也比较随机,覆盖的可能性不大;子线程通常也是mmap出来的,子线程中的TLS则位于线程栈的顶部
tcbhead_t结构体
1 | typedef struct |
如果可以溢出的数据够大,可以尝试去修改canary的值
利用数组下标越界绕过canary
c语言对于数组不做边界检查,我们可以利用此漏洞直接往后续某个内存地址写入目标地址,直白点就是可以直接修改retaddr的内容
利用条件:可以对数组第i个元素进行写入操作,i为我们自由设定
1 | scanf("%d",&i); |