이 글은 linux의 시스템기능을 주로 간략하게 분석하는데, 관심 있는 친구들은
간단 분석을 참고하면 된다. Linux에서의 시스템 기능은 다음과 같습니다
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); } weak_alias (libc_system, system)
코드는 glibc/sysdeps/posix/system.c에 있습니다. 여기서 system은 libc_system의 약한 별칭이고 libc_system은 do_system의 프런트 엔드 기능입니다. 매개변수 포함 검사를 위해 다음으로 do_system 함수를 살펴보세요.
static int do_system (const char *line) { int status, save; pid_t pid; struct sigaction sa; #ifndef _LIBC_REENTRANT struct sigaction intr, quit; #endif sigset_t omask; sa.sa_handler = SIG_IGN; sa.sa_flags = 0; sigemptyset (&sa.sa_mask); DO_LOCK (); if (ADD_REF () == 0) { if (sigaction (SIGINT, &sa, &intr) < 0) { (void) SUB_REF (); goto out; } if (sigaction (SIGQUIT, &sa, &quit) < 0) { save = errno; (void) SUB_REF (); goto out_restore_sigint; } } DO_UNLOCK (); /* We reuse the bitmap in the 'sa' structure. */ sigaddset (&sa.sa_mask, SIGCHLD); save = errno; if (sigprocmask (SIG_BLOCK, &sa.sa_mask, &omask) < 0) { #ifndef _LIBC if (errno == ENOSYS) set_errno (save); else #endif { DO_LOCK (); if (SUB_REF () == 0) { save = errno; (void) sigaction (SIGQUIT, &quit, (struct sigaction *) NULL); out_restore_sigint: (void) sigaction (SIGINT, &intr, (struct sigaction *) NULL); set_errno (save); } out: DO_UNLOCK (); return -1; } } #ifdef CLEANUP_HANDLER CLEANUP_HANDLER; #endif #ifdef FORK pid = FORK (); #else pid = fork (); #endif if (pid == (pid_t) 0) { /* Child side. */ const char *new_argv[4]; new_argv[0] = SHELL_NAME; new_argv[1] = "-c"; new_argv[2] = line; new_argv[3] = NULL; /* Restore the signals. */ (void) sigaction (SIGINT, &intr, (struct sigaction *) NULL); (void) sigaction (SIGQUIT, &quit, (struct sigaction *) NULL); (void) sigprocmask (SIG_SETMASK, &omask, (sigset_t *) NULL); INIT_LOCK (); /* Exec the shell. */ (void) execve (SHELL_PATH, (char *const *) new_argv, environ); _exit (127); } else if (pid < (pid_t) 0) /* The fork failed. */ status = -1; else /* Parent side. */ { /* 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; } #ifdef CLEANUP_HANDLER CLEANUP_RESET; #endif save = errno; DO_LOCK (); if ((SUB_REF () == 0 && (sigaction (SIGINT, &intr, (struct sigaction *) NULL) | sigaction (SIGQUIT, &quit, (struct sigaction *) NULL)) != 0) || sigprocmask (SIG_SETMASK, &omask, (sigset_t *) NULL) != 0) { #ifndef _LIBC /* glibc cannot be used on systems without waitpid. */ if (errno == ENOSYS) set_errno (save); else #endif status = -1; } DO_UNLOCK (); return status; } do_system
우선, 이 함수는 SIGINT 및 SIGQUIT 신호를 처리하기 위한 일부 신호 처리기를 설정합니다. 여기서는 크게 신경 쓰지 않습니다. 키 코드 세그먼트는 여기에 있습니다
#ifdef FORK pid = FORK (); #else pid = fork (); #endif if (pid == (pid_t) 0) { /* Child side. */ const char *new_argv[4]; new_argv[0] = SHELL_NAME; new_argv[1] = "-c"; new_argv[2] = line; new_argv[3] = NULL; /* Restore the signals. */ (void) sigaction (SIGINT, &intr, (struct sigaction *) NULL); (void) sigaction (SIGQUIT, &quit, (struct sigaction *) NULL); (void) sigprocmask (SIG_SETMASK, &omask, (sigset_t *) NULL); INIT_LOCK (); /* Exec the shell. */ (void) execve (SHELL_PATH, (char *const *) new_argv, environ); _exit (127); } else if (pid < (pid_t) 0) /* The fork failed. */ status = -1; else /* Parent side. */ { /* 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; }
프론트 엔드 함수를 통해 시스템 호출 포크를 호출합니다. 포크가 두 개의 반환 값을 갖는 자식 프로세스를 생성합니다. 자식 프로세스의 pid는 부모 프로세스에 반환되고 자식 프로세스에는 0이 반환됩니다. 따라서 하위 프로세스는 6~24줄의 코드를 실행하고 상위 프로세스는 30~35줄의 코드를 실행합니다.
자식 프로세스의 논리는 SHELL_PATH에 지정된 프로그램을 실행하기 위해 호출되며 매개 변수는 new_argv를 통해 전달되며 환경 변수 는 환경 변수입니다.
SHELL_PATH 및 SHELL_NAME은 다음과 같이 정의됩니다.
#define SHELL_PATH "/bin/sh" /* Path of the shell. */ #define SHELL_NAME "sh" /* Name to give it. */
실제로 /bin/sh -c "command"를 호출하는 하위 프로세스를 생성합니다. 시스템이 전달한 명령을 실행합니다.
실제로 제가 시스템 함수에 대해 연구하는 이유와 초점은 다음과 같습니다.
CTF의 pwn 질문에서 스택 오버플로를 통해 시스템 함수를 호출하면 가끔 실패한다고 마스터들이 말씀하시는 것을 들었습니다. 환경변수가 덮어씌워졌다고 하는데 늘 헷갈렸는데 오늘 자세히 공부해서 드디어 알아냈습니다.
여기서 시스템 기능에 필요한 환경변수는 전역변수인 environ에 저장되는데, 이 변수의 내용은 무엇일까요?
environ은 glibc/csu/libc-start.c에 정의되어 있습니다. 몇 가지 핵심 설명을 살펴보겠습니다.
# define LIBC_START_MAIN libc_start_main
libc_start_main은 _start에 의해 호출되는 함수로, 프로그램 시작 시 일부 초기화 작업이 포함됩니다. 이러한 용어를 이해하지 못하는 경우 이 문서를 읽어보세요. 다음으로 LIBC_START_MAIN 함수를 살펴보세요.
STATIC int LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL), int argc, char **argv, #ifdef LIBC_START_MAIN_AUXVEC_ARG ElfW(auxv_t) *auxvec, #endif typeof (main) init, void (*fini) (void), void (*rtld_fini) (void), void *stack_end) { /* Result of the 'main' function. */ int result; libc_multiple_libcs = &_dl_starting_up && !_dl_starting_up; #ifndef SHARED char **ev = &argv[argc + 1]; environ = ev; /* Store the lowest stack address. This is done in ld.so if this is the code for the DSO. */ libc_stack_end = stack_end; ...... /* Nothing fancy, just call the function. */ result = main (argc, argv, environ MAIN_AUXVEC_PARAM); #endif exit (result); }
Environ의 값은 SHARED를 정의하지 않고 19행에서 정의된 것을 볼 수 있습니다. 시작 프로그램은 LIBC_START_MAIN을 호출하기 전에 먼저 환경 변수와 문자열 을 argv에 저장합니다(실제로는 스택에 저장됩니다). 그런 다음 각 문자열의 주소를 환경 변수에 순차적으로 저장하고 argv의 각 문자열 항목 문자열의 주소와 argc가 스택에 푸시되므로 환경 변수 배열 은 argv 배열 바로 뒤에 빈 주소로 구분되어 위치해야 합니다. 따라서 17행의 &argv[argc + 1] 문은 스택에 있는 환경 변수 배열의 첫 번째 주소를 가져와서 ev에 저장하고 마지막으로 Environ에 저장합니다. 203행에서는 Environ 값을 스택에 푸시하는 기본 함수를 호출합니다. 이 함수는 Environ의 주소를 덮어쓰지 않는 한 스택 오버플로로 덮어쓰여도 문제가 없습니다.
따라서 스택 오버플로의 길이가 너무 길고 오버플로 내용이 Environ 주소의 중요한 내용을 덮을 경우 시스템 함수 호출이 실패합니다. 특정 환경 변수가 오버플로 주소에서 얼마나 떨어져 있는지는 _start를 인터럽트하여 확인할 수 있습니다.
위 내용은 Linux에서의 시스템 기능 분석 소개의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!