在 Shell 執行 mysql -u root -pSecret123 時,密碼會出現在:
ps -ef、ps aux/proc//cmdline ~/.bash_historyMySQL 客戶端啟動後會嘗試用 0 覆寫 argv 密碼段,但存在競態條件,且背景執行時更明顯。
C 程式啟動時,main(int argc, char *argv[]) 接收命令列參數指針陣列。argv[1] 指向密碼字串在進程記憶體中的位置。直接修改該記憶體區內容,ps 讀取 /proc/PID/cmdline 時即顯示修改後的值。
/* hide_pass.c - 編譯:gcc -o hide_pass hide_pass.c */
#include
#include
#include
#include
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s \n", argv[0]);
return 1;
}
pid_t pid = getpid();
printf("argc = %d, PID = %d\n", argc, pid);
for (int i = 0; i < argc; i++) {
printf("argv[%d] = %s\n", i, argv[i]);
}
printf("覆寫 argv[1] 中... 在另一終端執行: ps -p %d -o cmd\n", pid);
fflush(stdout);
/* 關鍵:用 'x' 覆蓋密碼每個字元 */
memset(argv[1], 'x', strlen(argv[1]));
/* 保持進程存活以便觀察 */
getchar();
return 0;
}
# 終端 1:編譯執行
$ gcc -o hide_pass hide_pass.c
$ ./hide_pass MySecretPassword123
argc = 2, PID = 12345
argv[0] = ./hide_pass
argv[1] = MySecretPassword123
覆寫 argv[1] 中... 在另一終端執行: ps -p 12345 -o cmd
# 終端 2:觀察
$ ps -p 12345 -o cmd
CMD
./hide_pass xxxxxxxxxxxxxxxx # 密碼已被 x 覆蓋
原始碼 client/mysql.cc 中 init_common_variables():
/* 簡化版邏輯 */
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-p") == 0 || strncmp(argv[i], "-p", 2) == 0) {
char *pass = (argv[i][2] ? &argv[i][2] : argv[++i]);
/* 立即複製密碼到內部緩衝區 */
strncpy(internal_password_buffer, pass, sizeof(internal_password_buffer)-1);
/* 覆寫 argv 記憶體 */
memset(pass, 'x', strlen(pass));
break;
}
}
| 方案 | 原理 | 優點 | 缺點 |
|---|---|---|---|
| argv 覆寫 | 修改進程參數記憶體 | 無依賴、通用 | 競態窗口、不支援所有 Unix、無法隱藏環境變數 |
| setproctitle() | BSD/System V 系統調用修改進程標題 | 標準化、可設定任意標題 | 非 POSIX 標準、Linux 需 prctl |
| PR_SET_MM_ARG_START | Linux prctl 修改參數區域 | Linux 專用、精確控制 | 需 CAP_SYS_RESOURCE、複雜 |
| 配置檔 .my.cnf | 從檔案讀取密碼,不經命令列 | 最安全、生產標準 | 需檔案權限管控 |
| Docker/K8s Secrets | 容器編排層注入密碼 | 不落盤、RBAC 控制、輪換友好 | 需容器環境 |
--defaults-extra-file 指向 600 權限配置檔ps、/proc、Shell 歷史,確保無洩露argv 覆寫是 C 語言底層記憶體操作的經典教學案例,展示了進程參數在記憶體中的佈局。但在現代基礎設施下,**「不讓密碼進入命令列」才是正解**,而非「命令列裡藏著密碼再想辦法藏起來」。