【軟體安全】實驗1——環境變數與 Set-UID 實驗
Task 1:配置環境變數
- 使用
printenv
或env
指令來列印環境變數:
如果只想列印特定的環境變數,如PWD
變數,可以使用printenv PWD
或者env | grep PWD
- 使用
export
和unset
來設定或者取消環境變數
- 使用
export
設定環境變數:
比如現在我使用export
設定一個環境變數MY_VAR
的值為softwaresecurity
可以使用echo $MY_VAR
列印出這個環境變數的值。
- 使用
unset
取消環境變數:
取消變數MY_VAR
。
Task 2:從父程序向子程序傳遞環境變數
- 編譯
myprintenv.c
並執行,將輸出結果列印到檔案output1.txt
中。
- 註釋掉子程序中的
printenv()
,並取消註釋父程序的printenv()
,再次編譯並列印輸出到檔案output2.txt
。
- 使用
diff
命令比較兩個檔案的差異。
結論:由於我在不同的視窗下執行的a.out
和b.out
,因此父子程序只有編譯成的可執行檔名稱和命令列視窗這兩個環境變數不同,其餘的環境變數都是相同的。結論是子程序在繼承父程序的環境變數時,除了檔名和輸出視窗存在差異以外,其他的環境變數都是相同的。
Task 3:環境變數和execve()
- 編譯並執行
myenv.c
發現輸出為空。
- 修改
execve()
函式為execve("/usr/bin/env",argv,environ);
發現列印出了當前程序的環境變數。
-
結論:
execve()
函式的原型是:int execve(const char *pathname, char *const argv[], char *const envp[]);
pathname
: 要執行的程式的路徑。argv
: 引數陣列,以NULL
結尾,包含傳遞給程式的命令列引數。envp
: 環境變數陣列,也以NULL
結尾。
新程式透過
execve()
函式的第三個引數傳遞的environ
變數來獲取環境變數。
Task 4:環境變數和system()
編譯並執行如下程式碼:
#include <stdio.h>
#include <stdlib.h>
int main()
{
system("/usr/bin/env");
return 0;
}
我們使用man system
檢視函式的手冊:
可以看到system()
函式是透過建立一個子程序,執行execl("/bin/sh", "sh", "-c", command, (char *) NULL);
,呼叫程序的環境變數會傳遞給新程式/bin/sh
。
Task 5:環境變數和Set-UID
程式
- 編寫以下程式列印該程序所有的環境變數:
#include <stdio.h>
#include <stdlib.h>
extern char **environ;
void main()
{
int i = 0;
while (environ[i] != NULL) {
printf("%s\n", environ[i]);
i++;
}
}
- 編譯上述程式得到 foo,將其所有者更改為 root,並使其成為一個 Set-UID 程式
// Asssume the program’s name is foo
$ sudo chown root foo
$ sudo chmod 4755 foo
檢視一下foo
的許可權,發現所有者更改為了root。
- 設定以下環境變數:
- PATH
- LD_LIBRARY_PATH
- MY_NAME
然後執行foo
並檢視這些環境變數的值
發現只有在父程序中設定的PATH
和MY_NAME
的環境變數進入子程序,而LD_LIBRARY_PATH
這個環境變數沒有進入子程序。
- 原因:
LD_LIBRARY_PATH
這個環境變數設定的是動態連結器的地址,由於動態連結器的保護機制,雖然在一個root許可權的程式下建立子程序並繼承父程序的環境變數,但由於我們是在普通使用者下修改的LD_LIBRARY_PATH
這個環境變數,所以是無法在子程序中生效的,而PATH
和MY_NAME
則沒有這種保護機制,因此可以被成功設定。
Task 6:PATH
環境變數和Set-UID
程式
先使用以下命令將bin/sh
連結到bin/zsh
,以規避bin/dash
阻止Set-UID程式使用特權執行的策略。
sudo ln -sf /bin/zsh /bin/sh
然後編寫LS.c
檔案,如下所示:
#include<stdio.h>
#include<stdlib.h>
int main(){
system("ls");
return 0;
}
然後編譯,並設定為Set-UID
程式:
可以看出,編譯出來的LS
檔案確實執行了system("ls")
的操作,更改後的檔案所有者確實變成了root
現在我們在普通使用者下設定PATH
環境變數,使用export PATH=/home/seed:$PATH
將/home/seed
新增到環境變數的開頭:
然後我們在/home/seed
下編寫我們的惡意程式碼。
// hack.c
#include<stdio.h>
#include<stdlib.h>
#include <unistd.h>
extern char **environ;
int main(){
uid_t euid = geteuid(); //獲取執行惡意程式碼的程序的euid
printf("euid=%d\n", euid);
printf("You have been hacked!!!!\n");
return 0;
}
然後編譯並命名成ls
:
gcc hack.c -o ls
然後再執行我們的LS
檔案:
發現可以使用Set-UID
程式執行我們的惡意程式碼,並且根據system("id")
的結果來看:euid=0
表示當前程序具有root許可權,表明惡意程式碼是以root許可權執行的。
Task 7:LD_PRELOAD
環境變數和Set-UID
程式
-
觀察環境變數在執行普通程式時如何影響動態載入器/連結器的行為,首先要進行如下配置:
- 構建一個動態連結庫,命名為
mylib.c
,裡面基本上覆蓋了libc裡的sleep()
函式:
#include <stdio.h> void sleep (int s) { /* If this is invoked by a privileged program , you can do damages here! */ printf("I am not sleeping!\n"); }
- 編譯該程式:
gcc -fPIC -g -c mylib.c gcc -shared -o libmylib.so.1.0.1 mylib.o -lc
- 設定
LD_PRELOAD
環境變數的值:
export LD_PRELOAD=./libmylib.so.1.0.1
- 編譯下面的程式
myprog.c
/* myprog.c */ #include <unistd.h> int main() { sleep(1); return 0; }
- 構建一個動態連結庫,命名為
-
完成上述操作後,請在以下條件下執行 myprog,觀察會發生什麼。
- 使 myprog 為一個普通程式,以普通使用者身份執行它。
發現執行的是我們編寫的
sleep
函式。- 使 myprog 為一個 Set-UID 特權程式,以普通使用者身份執行它。
發現等待了一秒後,沒有輸出,說明執行的是libc中的
sleep()
函式。- 使 myprog 為一個 Set-UID 特權程式,在 root 下重新設定 LD_PRELOAD 環境變數,並執行它。
發現執行的是我們編寫的
sleep
函式。- 使myprog成為一個Set_UID user1程式,在另一個使用者帳戶(非root使用者)中再次改變LD_PRELOAD環境變數並執行它
發現等待了一秒後,沒有輸出,說明執行的是libc中的
sleep()
函式。 -
設計一個實驗來找出導致這些差異的原因,並解釋為什麼第二步的行為不同。
修改一下myprog.c
,列印這個程式執行時的程序的uid
、euid
以及LD_PRELOAD
環境變數的值,如下所示:
/* myprog.c */
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
extern char **environ;
int main()
{
sleep(1);
uid_t uid = getuid();
printf("uid=%d(%s) ", uid, getenv("USER"));
uid_t euid = geteuid();
printf("euid=%d\n", euid);
char *preload = getenv("LD_PRELOAD");
printf("LD_PRELOAD: %s\n", preload);
return 0;
}
然後編寫一個shell指令碼,用於測試四種情況的輸出以及當前程序的id,如下所示:
#test.sh
echo "seed,run in seed:"
sudo chown seed myprog
sudo chmod 4755 myprog
export LD_PRELOAD=./libmylib.so.1.0.1
./myprog
echo "root,run in seed:"
sudo chown root myprog
sudo chmod 4755 myprog
./myprog
echo "root,run in root:"
sudo su <<EOF
export LD_PRELOAD=./libmylib.so.1.0.1
./myprog
EOF
echo "user1,run in seed:"
sudo chown user1 myprog
sudo chmod 4755 myprog
export LD_PRELOAD=./libmylib.so.1.0.1
./myprog
這個指令碼可以自動化測試四種情況下的sleep()
函式的執行情況以及列印當前程序的id,執行結果如下:
我們發現:
-
當
myprog
為一個普通程式,以普通使用者身份執行它時,其uid為seed,euid也為seed,LD_PRELOAD環境變數繼承了父程序的,並且執行的是我們編寫的sleep函式。 -
當
myprog
為一個Set-UID程式時,以普通使用者身份執行它時,其uid為seed,euid為root,LD_PRELOAD環境變數沒有繼承父程序的,並且執行的是libc的sleep函式。 -
當
myprog
為一個Set-UID程式時,以root使用者身份執行它時,其uid為root,euid為root,LD_PRELOAD環境變數繼承了父程序的,並且執行的是我們編寫的sleep函式。 -
當
myprog
為一個Set-UID user1程式時,以普通使用者身份執行它時,其uid為seed,euid為user1,LD_PRELOAD環境變數沒有繼承父程序的,並且執行的是libc的sleep函式。
如下表所示:
程式型別 | 執行使用者 | uid | euid | LD_PRELOAD環境變數 | 執行的sleep函式 |
---|---|---|---|---|---|
普通程式 | seed | seed | seed | 繼承父程序 | 我們編寫的 |
Set-UID程式 | seed | seed | root | 沒有繼承父程序 | libc的 |
Set-UID程式 | root | root | root | 繼承父程序 | 我們編寫的 |
Set-UID user1程式 | seed | seed | user1 | 沒有繼承父程序 | libc的 |
結論:
當一個程序的uid
和euid
一致時,子程序才會繼承父程序的環境變數,才會執行我們編寫的sleep()
函式,第二步行為不同的原因是因為它們的uid
和euid
的一致/不一致會導致子程序繼承/不繼承LD_PRELOAD
環境變數,從而導致了sleep()
函式的不同。
Task 8:使用 system() 與 execve() 呼叫外部程式的對比
編寫並編譯catcall.c
,如下所示:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[])
{
char *v[3];
char *command;
if(argc < 2) {
printf("Please type a file name.\n");
return 1;
}
v[0] = "/bin/cat"; v[1] = argv[1]; v[2] = NULL;
command = malloc(strlen(v[0]) + strlen(v[1]) + 2);
sprintf(command , "%s %s", v[0], v[1]);
system(command);
// execve(v[0], v, NULL);
return 0 ;
}
這個程式呼叫了system()
函式執行了/bin/cat [filename]
,可以列印指定檔案的內容。
-
編譯上述程式,使其成為 root 所有的 Set-UID 程式。該程式將使用 system() 來呼叫該命令。如果你是 Bob,你能損害系統的完整性嗎?例如,你可以刪除對你沒有寫許可權的檔案嗎?
- 首先使其成為root所有的 Set-UID 程式:
-
嘗試刪除沒有寫許可權的檔案:
- 首先建立一個seed沒有寫許可權的檔案,我們首先要將資料夾許可權改為seed不可寫,再將test.txt的屬性設為seed不可寫:
- 發現
catcall
有命令注入漏洞,可以呼叫system()
執行其他系統命令:
使用命令
catcall "test.txt;rm test.txt"
成功將沒有寫許可權的test.txt
刪除。
-
註釋掉 system(command) 語句,取消註釋 execve() 語句;程式將使用 execve() 來呼叫命令。 編譯程式,並使其成為 root 擁有的 Set-UID 程式。你在第一步中的攻擊仍然有效嗎?請描述並解釋你的觀察結果。
- 首先建立一個seed沒有寫許可權的檔案:
- 然後再使用命令
catcall "test.txt;rm test.txt"
發現無法刪除
test.txt
,攻擊失效。
原理:
使用system()
函式能成功刪除的原因是system()
函式會建立一個子程序,並呼叫bin/bash
來執行函式的引數,因此執行catcall "test.txt;rm test.txt"
就相當於父程序建立了一個子程序,子程序使用bin/bash
執行bin/cat test.txt;rm test.txt
,由於bash的特性,分號後面會作為下一個命令並執行,而且父程序是一個Set-UID
程式,因此相當於在 root 下執行了rm test.txt
,所以可以刪除檔案。
而使用execve()
函式刪除不了檔案的原因是execve()
函式並不是呼叫bin/bash
來執行函式的引數的,而是透過系統呼叫的方式執行bin/cat test.txt;rm test.txt
,它會把 test.txt;rm test.txt
當作一個檔名,而我們這個目錄下並不存在這個檔案,因此會報錯/bin/cat: 'test.txt;rm test.txt': No such file or directory
Task 9:許可權洩漏
編譯以下程式,將其所有者更改為 root,並使其成為 Set-UID 程式。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
void main()
{
int fd;
char *v[2];
/* Assume that /etc/zzz is an important system file,
* and it is owned by root with permission 0644.
* Before running this program, you should create
* the file /etc/zzz first. */
fd = open("/etc/zzz", O_RDWR | O_APPEND);
if (fd == -1) {
printf("Cannot open /etc/zzz\n");
exit(0);
}
// Print out the file descriptor value
printf("fd is %d\n", fd);
// Permanently disable the privilege by making the
// effective uid the same as the real uid
setuid(getuid());
// Execute /bin/sh
v[0] = "/bin/sh"; v[1] = 0;
execve(v[0], v, 0);
}
我們在/etc
下建立檔案zzz
,並執行cap_leak
檔案描述符(File Descriptor,簡稱 fd)是作業系統中用於管理和操作檔案或其他輸入/輸出資源(如網路連線、管道等)的一個重要概念。當開啟一個檔案時,作業系統會返回一個檔案描述符,後續的讀寫操作都透過這個描述符進行。
此時輸出了zzz
檔案的檔案描述符fd(File Descriptor),並且執行了setuid(getuid())
操作,將程序的uid改為了當前使用者的,也就是將uid設為seed,然後呼叫execve()
函式執行了bin/sh
開啟了一個shell。
我們使用whoami
命令檢視shell的擁有者:
發現擁有者確實是seed
,但是雖然這個程序的有效使用者ID是 seed ,但是該程序仍然擁有特權,我們可以以普通使用者的身份將惡意程式碼寫入/etc/zzz
檔案中,這個過程需要利用檔案描述符fd。
我們可以使用echo "You have been hacked!!" >& 3
,將這段話透過檔案描述符寫入/etc/zzz
:
可以發現成功寫入了檔案。
原理:
雖然程式碼中執行了setuid(getuid())
操作,將程序的uid改為了seed,但是在執行execve(v[0], v, 0)
開啟一個shell時,由於在放棄特權時沒有關閉/etc/zzz
這個檔案,建立的子程序會繼承/etc/zzz
這個檔案的檔案描述符,造成特權洩露,子程序可以利用這個檔案描述符向檔案中寫入內容。