glibc中的文件指针漏洞分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/SKY453589103/article/details/86725814

glib版本2.17
在上一篇文章(源码解析glibc中的pclose与fclose函数)中,初步了解到了glibc中的文件指针。
现在我们再来深入分析一下glibc文件指针,并解析一下其漏洞所在。

glibc中的文件结构

先了解一下普通文件的方式(注意3个标准文件描述符的链接顺序)

文件链接示意图

普通文件的操作

在文件libio/stdfile.c中可以找到如下定义:

// DEF_STDFILE 这是个宏定义,里面包含了将标准输入、标准输出、标准出错用链表链接起来的操作
// 感兴趣的可以看看源码,其中用到的FILEBUF_LITERAL宏定义 libio/libioP.h 中
// FILEBUF_LITERAL 只是把文件结构体初始化
DEF_STDFILE(_IO_2_1_stdin_, 0, 0, _IO_NO_WRITES);
DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS);
DEF_STDFILE(_IO_2_1_stderr_, 2, &_IO_2_1_stdout_, _IO_NO_READS+_IO_UNBUFFERED);

struct _IO_FILE_plus *_IO_list_all = &_IO_2_1_stderr_;

再来看看文件打开和关闭时的操作

//----------------------- 文件 打开 -----------------------
// 在调用fopen时,会调用这个函数
// 在文件libio/fileops.c中
void
_IO_new_file_init (fp)
     struct _IO_FILE_plus *fp;
{
  /* POSIX.1 allows another file handle to be used to change the position
     of our file descriptor.  Hence we actually don't know the actual
     position before we do the first fseek (and until a following fflush). */
  fp->file._offset = _IO_pos_BAD;
  fp->file._IO_file_flags |= CLOSED_FILEBUF_FLAGS;

  _IO_link_in (fp);
  fp->file._fileno = -1;
}

/// 在文件libio/genops.c中
void
_IO_link_in (fp)
     struct _IO_FILE_plus *fp;
{
  if ((fp->file._flags & _IO_LINKED) == 0)
    {
      fp->file._flags |= _IO_LINKED;
#ifdef _IO_MTSAFE_IO
      _IO_cleanup_region_start_noarg (flush_cleanup);
      // 对全局链表上锁,线程安全
      _IO_lock_lock (list_all_lock);
      run_fp = (_IO_FILE *) fp;
      _IO_flockfile ((_IO_FILE *) fp);
#endif
	  // 在链表的表头插入,新打开的文件
      fp->file._chain = (_IO_FILE *) _IO_list_all;
      // 因此_IO_list_all 总是指向最新打开的文件
      _IO_list_all = fp;
      // 递增文件描述符的修改次数
      ++_IO_list_all_stamp;
#ifdef _IO_MTSAFE_IO
      _IO_funlockfile ((_IO_FILE *) fp);
      run_fp = NULL;
      _IO_lock_unlock (list_all_lock);
      _IO_cleanup_region_end (0);
#endif
    }
}
//----------------------- 文件 打开 -----------------------

//----------------------- 文件 关闭 -----------------------
// 在函数fclose中或者打开文件失败时会调用
// 在文件libio/genops.c中
void
_IO_un_link (fp)
     struct _IO_FILE_plus *fp;
{
  if (fp->file._flags & _IO_LINKED)
    {
      struct _IO_FILE **f;
#ifdef _IO_MTSAFE_IO
      _IO_cleanup_region_start_noarg (flush_cleanup);
      // 上锁
      _IO_lock_lock (list_all_lock);
      run_fp = (_IO_FILE *) fp;
      _IO_flockfile ((_IO_FILE *) fp);
#endif
      if (_IO_list_all == NULL)
	;
      else if (fp == _IO_list_all)
	{
	  _IO_list_all = (struct _IO_FILE_plus *) _IO_list_all->file._chain;
	  // 递增文件描述符的修改次数
	  ++_IO_list_all_stamp;
	}
      else
    // 不知道为什么源码没有对齐,看起来挺蛋疼的
    // 在已打开的文件列表中找到要取消链接的文件指针,在链表中区中
	for (f = &_IO_list_all->file._chain; *f; f = &(*f)->_chain)
	  if (*f == (_IO_FILE *) fp)
	    {
	      // 指向下一个文件
	      *f = fp->file._chain;
	      // 递增文件描述符的修改次数
	      ++_IO_list_all_stamp;
	      break;
	    }
      fp->file._flags &= ~_IO_LINKED;
#ifdef _IO_MTSAFE_IO
      _IO_funlockfile ((_IO_FILE *) fp);
      run_fp = NULL;
      _IO_lock_unlock (list_all_lock);
      _IO_cleanup_region_end (0);
#endif
    }
}
//----------------------- 文件 关闭 -----------------------

管道文件的打开与关闭

在文件libio/iopopen.c 可以找到管理 管道文件 的链表结构

struct _IO_proc_file
{
  struct _IO_FILE_plus file;
  /* Following fields must match those in class procbuf (procbuf.h) */
  _IO_pid_t pid;
  struct _IO_proc_file *next;
};
static struct _IO_proc_file *proc_file_chain;

管道文件的打开与关闭跟普通文件都是一样会操作链表,这里就不在重复说明了。

文件指针中的漏洞

文件断链时的死循环漏洞

这种漏洞比较容易检测到,具体的表现为了在关闭文件句柄时,一直卡在fclose函数中,无法跳出。

int main()
{
    FILE *fpReader_1 = fopen("1.txt","w");
    FILE *fpReader_2 = fopen("2.txt","w");
    if(NULL == fpReader_1 || NULL == fpReader_2)
    {
        printf("open file error \n");
    }
    // 在执行下面的操作之前,_chain 指向的是fpReader_1 
    // 执行下面的操作之后,打开的文件链表中形成了一个环
    fpReader_2->_chain = fpReader_2;
    // fclose函数会调用unlink函数,以便将fpReader_1指向的文件从链表中剔除。
    // unlink函数会从表头(这里是fpReader_2)开始一个个往下找。
    // 由于next指针(即变量_chain)被替换,从而形成环,导致for循环无法终止。
    if (fclose(fpReader_1) != 0)
    {
        printf("%s \n", strerror(errno));
    }
    else
    {
        printf("close success\n");
    }
	
	return 0;
}

虚表执行漏洞

在 2.24版本之后,添加了地址检查函数,如果覆盖了虚表,则会报如下错误:
Fatal error: glibc detected an invalid stdio handle
Aborted (core dumped)

上篇文章讲到,无论是虚表中的__close函数在关闭文件指针时是一定会被执行的。而虚表又只是一个结构体指针,指向的内容可以是动态分配的内容(默认的虚表都被保护的内存地址中),因此我们可以通过覆盖虚表来实现一些操作。

// 这里为了增加可读性,我仿造了一个 IO_jump_t 出来,部分的函数指针类型会对不上,但这不影响测试。
// 当然也可以通过指针进行操作,但我认为可读性不高。
typedef int (*MY_IO_close_t)(_IO_FILE *);
struct MY_IO_jump_t {
  size_t __dummy;
  size_t __dummy2;
  MY_IO_close_t __finish;
  MY_IO_close_t __overflow;
  MY_IO_close_t __underflow;
  MY_IO_close_t __uflow;
  MY_IO_close_t __pbackfail;
  MY_IO_close_t __xsputn;
  MY_IO_close_t __xsgetn;
  MY_IO_close_t __seekoff;
  MY_IO_close_t __seekpos;
  MY_IO_close_t __setbuf;
  MY_IO_close_t __sync;
  MY_IO_close_t __doallocate;
  MY_IO_close_t __read;
  MY_IO_close_t __write;
  MY_IO_close_t __seek;
  MY_IO_close_t __close;
  MY_IO_close_t __stat;
  MY_IO_close_t __showmanyc;
  MY_IO_close_t __imbue;
};
// 仿造一个 IO_FILE_plus 结构体,提高可读性
struct MY_IO_FILE_plus {
  _IO_FILE file;
  const struct MY_IO_jump_t *vtable;
};

// 用于测试的函数,会覆盖原有的close动作
int test_close(_IO_FILE *fp) {
  // 注意!!!!!!!!!!!
  // 这里是 rm 操作
  // 千万不要改成 rm -rf /
  // !!!!!!!!!!!!!!!!!!!!!
  FILE *p = popen("rm -rf test.txt", "re");!!! // 去掉这3个感叹号即可编译成功
  pclose(p);
  return 0;
}

int main() {
  for (unsigned int i = 0; i < 10; ++i) {
    FILE *fpReader;
    fpReader = fopen("1.txt", "w");
    if (NULL == fpReader) {
      printf("open file error \n");
      break;
    }
    struct MY_IO_FILE_plus *p = (struct MY_IO_FILE_plus *)fpReader;
    struct MY_IO_jump_t *pNewJump =
        (struct MY_IO_jump_t *)malloc(sizeof(struct MY_IO_jump_t));
    // 复制原有的虚表,保留其他操作,只改变close动作
    memcpy(pNewJump, p->vtable, sizeof(MY_IO_jump_t));
    pNewJump->__close = test_close;
    p->vtable = pNewJump;
    printf("close\n");
    if (fclose(fpReader) == -1) {
      printf("%s \n", strerror(errno));
    } else {
      printf("close success\n");
    }
    sleep(1);
  }

  return 0;
}

通过上面代码,我们可以实现删库跑路的动作。

猜你喜欢

转载自blog.csdn.net/SKY453589103/article/details/86725814