この記事は主に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)
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
#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; }
まず、フロントエンド関数がシステムを呼び出します。 fork を呼び出して子プロセスを生成します。このうち、fork の戻り値は 2 つあり、親プロセスに対しては子プロセスの pid を返し、子プロセスに対しては 0 を返します。したがって、子プロセスは 6 ~ 24 行のコードを実行し、親プロセスは 30 ~ 35 行のコードを実行します。
子プロセスのロジックは非常に明確であり、SHELL_PATH で指定されたプログラムを実行するために execve が呼び出され、パラメーターは new_argv を介して渡され、環境変数
はグローバル変数 environ です。SHELL_PATHとSHELL_NAMEは次のように定義されています
#define SHELL_PATH "/bin/sh" /* Path of the shell. */ #define SHELL_NAME "sh" /* Name to give it. */
実際には、システムに渡されたコマンドを実行するために
を呼び出すサブプロセスが生成されます。 。 実際に私がシステム関数を研究した理由と焦点は次のとおりです:
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); }
19 行目で environ の値が SHARED を定義せずに定義されていることがわかります。スタートアップ プログラムは LIBC_START_MAIN を呼び出す前に、まず
文字列
配列 は空のアドレスで区切って argv 配列のすぐ後ろに配置する必要があります。したがって、17 行目の &argv[argc + 1] ステートメントは、スタック上の環境変数配列の最初のアドレスを取得し、それを ev に保存し、最後に environ に保存します。 203 行目は、environ の値をスタックにプッシュする main 関数を呼び出します。environ のアドレスが上書きされない限り、これがスタック オーバーフローによって上書きされても問題はありません。 そのため、スタックオーバーフローの長さが大きすぎて、オーバーフローの内容が環境内のアドレスの重要な内容を覆っている場合、システム関数の呼び出しは失敗します。特定の環境変数がオーバーフロー アドレスからどの程度離れているかは、_start で中断することで確認できます。
以上がLinux でのシステム機能の簡単な分析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。