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는 다음과 같다.
- 부모 쪽 준비
- SIGINT/SIGQUIT(Ctrl-C/Ctrl-)를 일시적으로 무시(SIG_IGN) 하여, 사용자가 system()으로 실행한 외부 명령이 신호를 받아도 부모는 죽지 않게 한다.
- SIGCHLD를 블록(mask)해서 spawn 직후 ~ waitpid 사이의 경쟁 상태(race)를 막는다.
참고로 여기서 mask란, 시그널 마스크를 의미한다.
현재 스레드가 어떤 시그널을 일시적으로 막고(block) 있을지를 의미한다.
막힌 시그널은 보류(pending) 상태가 되고, 마스크를 풀면 그때 전달된다.
- 자식 생성 (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 이 우리가 사용하는 셸이 된다.
- 대기 & 정리
- waitpid(pid, &status, 0)로 자식 종료까지 대기.
- 끝나면 부모의 SIGINT/SIGQUIT 핸들러와 신호 마스크를 원래대로 복구.
- status(자식 종료 상태)를 반환(에러면 -1).
근데 여기서 중요한 점이 하나있다. 바로 posix_spawn ( 즉, 내부의 fork() 기능 ) 이후에 취약점이 존재하는데