這篇文章主要簡單分析了linux下system函數,具有一定的參考價值,有興趣的夥伴們可以參考一下
簡單分析了linux下system函數的相關內容,具體內容如下
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; }
先透過前端函式呼叫系統呼叫系統呼叫fork產生一個子進程,其中fork有兩個回傳值,對父進程回傳子進程的pid,對子進程回傳0。所以子進程執行6-24行程式碼,父進程執行30-35行程式碼。
子程序的邏輯非常清晰,呼叫execve執行SHELL_PATH指定的程序,參數透過new_argv傳遞,環境變數為全域變數environ。
其中SHELL_PATH和SHELL_NAME定義如下
#define SHELL_PATH "/bin/sh" /* Path of the shell. */ #define SHELL_NAME "sh" /* Name to give it. */
其實就是產生一個子程序呼叫/bin/sh -c "指令"##來執行向system傳入的命令。
下面其實是我研究system函數的原因與重點:
在CTF的pwn題中,透過堆疊溢出呼叫system函數有時會失敗,聽師傅們說是環境變數被覆蓋,但是一直都是懵懂,今天深入學習了一下,總算搞懂了。
在這裡system函數需要的環境變數儲存在全域變數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); }
我們可以看到,在沒有define SHARED的情況下,在第19行定義了environ的值。啟動程式呼叫LIBC_START_MAIN之前,會先將環境變數和argv中的字串保存起來(其實是儲存到堆疊上),然後依序將環境變數中各項字串的位址,argv中各項目字串的位址和argc入棧,所以環境變數陣列一定位於argv陣列的正後方,以一個空位址間隔。所以第17行的&argv[argc + 1]語句就是取環境變數數組在堆疊上的首位址,保存到ev中,最後儲存到environ中。第203行呼叫main函數,會將environ的值入棧,這個被棧溢位覆蓋掉沒什麼問題,只要保證environ中的位址處不被覆蓋即可。
所以,當堆疊溢出的長度過大,溢出的內容覆蓋了environ中地址中的重要內容時,呼叫system函數就會失敗。具體環境變數距離溢出位址有多遠,可以透過在_start中下斷查看。
以上是關於system函數在linux下的分析介紹的詳細內容。更多資訊請關注PHP中文網其他相關文章!