以往做过的开启了canary保护的题目 一般都是通过格式化字符串泄露 从而来绕过canary 最近在学习免杀的时候 意外了解了canary的生成机制 从而就有了今天的这一篇文章 总体下来还是收获颇丰
什么是canary
由于c语言并没有检查数组的下标 所以其存在溢出的可能性 诱发了栈溢出漏洞 可以使得攻击者任意的控制程序执行流 对此 canary机制有效预防了栈溢出的操作
其通过在栈帧的bp寄存器间隔一个字长(往低地址方向)的地方存放了一串随机数(末位为\x00 目的是防止被printf等函数泄露)
在函数结束前 进行了一个异或检查 如果发现此地址处的canary被修改了 那么则判定为发生了栈溢出的行为
则会跳转到**___stack_chk_fail**函数 该函数负责输出错误信息并且终止程序
函数栈帧在形成初期 从 fs:0x28 赋值 并将其入栈
函数结束前进行异或判定 如果结尾为0 就通过jz指令跳转到 leave|ret 指令处返回父栈帧
否则就调用**___stack_chk_fail**函数结束程序
而在libc2.23及以下的版本中 ___stack_chk_fail函数会根据argv[0]存放的程序路径来输出下面这样的错误信息
|
而argv[0]位于当前栈帧的栈底 可以通过溢出篡改其为flag 从而获取flag
这里直接在源码中修改argv[0] 看看效果
|
不过 要注意的是 其输出的是路径 也就是字符串 而非输出该路径对应文件的内容
接着我们更换一下libc 文件 使其为libc2.27再次尝试 源码不变
可以发现其直接默认输出unknown了
同时这里发现了一个有趣的现象 到达一定长度后 溢出的数据会被当作指令执行 这就需要进一步研究了
不过由于我实在是太心急把这篇文章写出来 所以暂时是先咕咕了 后续会开一个新篇章研究这个现象
上述稍微跑题了一下 说回canary 栈上的canary是由 fs:28h 处提供的值 我们对这个地址朔源一下
fs是一个寄存器 其指向当前活动线程的TEB结构
TEB是一个线程环境块 进程中每个线程都对应着一个TEB结构体 其存储了线程的各种信息
不同的偏移对应着不同的信息
000 指向SEH链指针 |
但是fs中存放的TEB地址需要经过解析之后才能显示
调用pthread_self函数就可以获取到TEB的位置
可以在对应位置找到canary 而canary判断是否被更改 是将栈上的和这里的进行比较
如果我们修改了TEB上的canary 那么栈上的canary就可以很轻易的被我们绕过
那么TEB上的canary又是从何而来呢 这就要从内核的_dl_random说起了
其是一个地址 用来指向内核中存储随机数的地方
该随机数初始化于动态链接之前
其存放于auxiliary vector 数组中 该数组是用来辅助程序运行的数据数组
其在dl_main函数中被调用
ElfW(Addr) |
接着是__libc_start_main函数 其生成canary的源码如下
// sysdeps\unix\sysv\linux\dl-osinfo.h |
canary的值和dl_random一致 只不过在最后一个字节强制使其为\x00
接着来到_libc_start_init函数
/* Set up the stack checker's canary. */ |
如果设置了THREAD_SET_STACK_GUARD宏 那么canary就会被设置进入线程局部存储
PS:一直没有搞清楚TEB TCB pthread三者的关系 上述的描述可能存在很大问题 更详细的源码级别可以看这篇博客 以我的水平很难对其进行复述
浅析 Linux 程序的 Canary 机制 | Kiprey’s Blog
在gdb中我们修改其值为0 接下一路n到函数结束前的canary判断
此时只要rcx寄存器中的值与fs:0x28中的值相同 就会触发je指令 正常结束栈帧
但是显然 此时rcx寄存器保存的是在函数最开始入栈的旧canary值 而此时fs:0x28处的值已经被我们修改为0 如果此时进行xor 显然结果是不会为0
我们再次更改一下rcx寄存器的值 并且执行这一步异或
其二进制形式为1001000110
ZF标志位是1 那么jz指令就会跳转
于是就不会触发__stack_chk_fail函数 所以我们成功绕过了canary
上述的绕过是基于修改主TCB中的canary 还有一种办法是修改子线程的TCB 这里不做说明