how system work

system 함수가 과연 어떻게 동작할까?

C
int
__libc_system (const char *line)
{
  if (line == NULL)
    /* Check that we have a command processor available.  It might
       not be available after a chroot(), for example.  */
    return do_system ("exit 0") == 0;

  return do_system (line);
}

먼저, 기본 게이트이다. do_system 을 실행시키면서 명령어가 비어있는지 검사한다.

do_system

C
static int
do_system (const char *line)
{
  int status;
  pid_t pid;
  struct sigaction sa;
#ifndef _LIBC_REENTRANT
  struct sigaction intr, quit;
#endif
  sigset_t omask;
  sigset_t reset;

  sa.sa_handler = SIG_IGN;
  sa.sa_flags = 0;
  __sigemptyset (&sa.sa_mask);

  DO_LOCK ();
  if (ADD_REF () == 0)
    {
      /* sigaction can not fail with SIGINT/SIGQUIT used with SIG_IGN.  */
      __sigaction (SIGINT, &sa, &intr);
      __sigaction (SIGQUIT, &sa, &quit);
    }
  DO_UNLOCK ();

  __sigaddset (&sa.sa_mask, SIGCHLD);
  /* sigprocmask can not fail with SIG_BLOCK used with valid input
     arguments.  */
  __sigprocmask (SIG_BLOCK, &sa.sa_mask, &omask);

  __sigemptyset (&reset);
  if (intr.sa_handler != SIG_IGN)
    __sigaddset(&reset, SIGINT);
  if (quit.sa_handler != SIG_IGN)
    __sigaddset(&reset, SIGQUIT);

  posix_spawnattr_t spawn_attr;
  /* None of the posix_spawnattr_* function returns an error, including
     posix_spawnattr_setflags for the follow specific usage (using valid
     flags).  */
  __posix_spawnattr_init (&spawn_attr);
  __posix_spawnattr_setsigmask (&spawn_attr, &omask);
  __posix_spawnattr_setsigdefault (&spawn_attr, &reset);
  __posix_spawnattr_setflags (&spawn_attr,
			      POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSIGMASK);

  status = __posix_spawn (&pid, SHELL_PATH, 0, &spawn_attr,
			  (char *const[]){ (char*) SHELL_NAME,
					   (char*) "-c",
					   (char *) line, NULL },
			  __environ);
  __posix_spawnattr_destroy (&spawn_attr);

  if (status == 0)
    {
      /* Cancellation results in cleanup handlers running as exceptions in
	 the block where they were installed, so it is safe to reference
	 stack variable allocate in the broader scope.  */
#if defined(_LIBC_REENTRANT) && defined(SIGCANCEL)
      struct cancel_handler_args cancel_args =
      {
	.quit = &quit,
	.intr = &intr,
	.pid = pid
      };
      __libc_cleanup_region_start (1, cancel_handler, &cancel_args);
#endif
      /* Note the system() is a cancellation point.  But since we call
	 waitpid() which itself is a cancellation point we do not
	 have to do anything here.  */
      if (TEMP_FAILURE_RETRY (__waitpid (pid, &status, 0)) != pid)
	status = -1;
#if defined(_LIBC_REENTRANT) && defined(SIGCANCEL)
      __libc_cleanup_region_end (0);
#endif
    }

  DO_LOCK ();
  if (SUB_REF () == 0)
    {
      /* sigaction can not fail with SIGINT/SIGQUIT used with old
	 disposition.  Same applies for sigprocmask.  */
      __sigaction (SIGINT, &intr, NULL);
      __sigaction (SIGQUIT, &quit, NULL);
      __sigprocmask (SIG_SETMASK, &omask, NULL);
    }
  DO_UNLOCK ();

  return status;
}

다음과 같은데 주요 flow는 다음과 같다.

  1. 부모 쪽 준비
  • SIGINT/SIGQUIT(Ctrl-C/Ctrl-)를 일시적으로 무시(SIG_IGN) 하여, 사용자가 system()으로 실행한 외부 명령이 신호를 받아도 부모는 죽지 않게 한다.
  • SIGCHLD를 블록(mask)해서 spawn 직후 ~ waitpid 사이의 경쟁 상태(race)를 막는다.

참고로 여기서 mask란, 시그널 마스크를 의미한다.

현재 스레드가 어떤 시그널을 일시적으로 막고(block) 있을지를 의미한다.

막힌 시그널은 보류(pending) 상태가 되고, 마스크를 풀면 그때 전달된다.

  1. 자식 생성 (posix_spawn + 속성)
  • 자식 프로세스(셸)는
    • 부모의 원래 마스크(omask) 를 사용(= 부모가 일시적으로 추가한 SIGCHLD 블록을 자식에 물려주지 않음)
    • SIGINT/SIGQUIT의 디폴트 동작(SIG_DFL)을 복원하여, 사용자가 Ctrl-C를 치면 자식은 정상적으로 종료되도록 함
  • 실행 파일: SHELL_PATH(보통 /bin/sh) , 인자: SHELL_NAME, -c, <line>, 환경: __environ.

여기서 다음과 같은 명령어가 실행된다. sh -c ( line ) __환경 즉, 내가 system(”/bin/sh”)을 하면 내부적으로 sh -c “/bin/sh” 이 실행된다.

추가적으로 fork-exec 방식은 잘 쓰지 않는다. 대신 같은 결과지만, 성능과 문제점을 개선한 posix_spawn

해당 블로그를 인용하자면,

C
앞서 언급한대로 fork-exec에서 COW가 도입되고 성능적인 제약은 사라졌지만 fork-exec는 여전히 문제점을 가지고 있었다. 그것은 Signal mask나 파일, 스케줄링 정책/우선순위가 상속되는 점과 스왑영역 확보등의 문제였다. 특히 파일(or 소켓)이나 스케줄링 정책/우선순위의 상속 문제는 매우 민감하여 예기치 못한 작동을 발생시킬 소지가 있었다.

결국 POSIX 표준에서 fork-exec를 대체할 기능으로 posix_spawn을 제안하게 되었고 1999년 IEEE-1003.1d에 정식 채택되었다. (POSIX 표준에 대한 이야기는 여기에서 살펴보자)

ps 명령어로 흐름을 보면,

C

root        77  0.0  0.2   2360  1456 ?        S    15:18   0:00 ./dreamsanta_present
ctf         78  0.0  0.1   2612   536 ?        S    15:18   0:00 sh -c /bin/sh
ctf         79  0.0  0.1   2612   600 ?        S    15:18   0:00 /bin/sh

다음과 같다.

dreamsanta_present 은 root로 실행되고 그 안에서

sh -c /bin/sh 이 커널에 의해 posix_spawn 함수 실행 중에 실행되고 이 이후에,

/bin/sh 이 우리가 사용하는 셸이 된다.

  1. 대기 & 정리
  • waitpid(pid, &status, 0)로 자식 종료까지 대기.
  • 끝나면 부모의 SIGINT/SIGQUIT 핸들러와 신호 마스크를 원래대로 복구.
  • status(자식 종료 상태)를 반환(에러면 -1).

근데 여기서 중요한 점이 하나있다. 바로 posix_spawn ( 즉, 내부의 fork() 기능 ) 이후에 취약점이 존재하는데