SEEDLab —— 環境變數與 Set-UID 實驗

Smera1d0發表於2024-12-07

【軟體安全】實驗1——環境變數與 Set-UID 實驗

Task 1:配置環境變數

  1. 使用printenvenv指令來列印環境變數:
image-20240925085104482 image-20240925085246669

​ 如果只想列印特定的環境變數,如PWD變數,可以使用printenv PWD或者env | grep PWD

image-20240925085533581
  1. 使用exportunset來設定或者取消環境變數
  • 使用export設定環境變數:

​ 比如現在我使用export設定一個環境變數MY_VAR的值為softwaresecurity

image-20240925090946612

​ 可以使用echo $MY_VAR列印出這個環境變數的值。

  • 使用unset取消環境變數:

​ 取消變數MY_VAR

image-20240925091213705

Task 2:從父程序向子程序傳遞環境變數

  1. 編譯myprintenv.c並執行,將輸出結果列印到檔案output1.txt中。
image-20240925091905373
  1. 註釋掉子程序中的printenv(),並取消註釋父程序的printenv(),再次編譯並列印輸出到檔案output2.txt
image-20240925093523967
  1. 使用diff命令比較兩個檔案的差異。

image-20240925093608802

結論:由於我在不同的視窗下執行的a.outb.out,因此父子程序只有編譯成的可執行檔名稱命令列視窗這兩個環境變數不同,其餘的環境變數都是相同的。結論是子程序在繼承父程序的環境變數時,除了檔名和輸出視窗存在差異以外,其他的環境變數都是相同的。

Task 3:環境變數和execve()

  1. 編譯並執行myenv.c
image-20240925095029968

發現輸出為空。

  1. 修改execve()函式為execve("/usr/bin/env",argv,environ);
image-20240925101911467

發現列印出了當前程序的環境變數。

  1. 結論:

    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;
}
image-20240925110330803

我們使用man system檢視函式的手冊:

image-20240925110655775

可以看到system()函式是透過建立一個子程序,執行execl("/bin/sh", "sh", "-c", command, (char *) NULL);,呼叫程序的環境變數會傳遞給新程式/bin/sh

Task 5:環境變數和Set-UID程式

  1. 編寫以下程式列印該程序所有的環境變數:
#include <stdio.h>
#include <stdlib.h>

extern char **environ;
void main()
{
int i = 0;
while (environ[i] != NULL) {
	printf("%s\n", environ[i]);
	i++;
	}
}

  1. 編譯上述程式得到 foo,將其所有者更改為 root,並使其成為一個 Set-UID 程式
// Asssume the program’s name is foo
$ sudo chown root foo
$ sudo chmod 4755 foo

檢視一下foo的許可權,發現所有者更改為了root。

image-20240926141026903
  1. 設定以下環境變數:
  • PATH
  • LD_LIBRARY_PATH
  • MY_NAME

image-20240925114140972

然後執行foo並檢視這些環境變數的值

image-20240925115029191

發現只有在父程序中設定的PATHMY_NAME的環境變數進入子程序,而LD_LIBRARY_PATH這個環境變數沒有進入子程序。

  1. 原因:

LD_LIBRARY_PATH這個環境變數設定的是動態連結器的地址,由於動態連結器的保護機制,雖然在一個root許可權的程式下建立子程序並繼承父程序的環境變數,但由於我們是在普通使用者下修改的LD_LIBRARY_PATH這個環境變數,所以是無法在子程序中生效的,而PATHMY_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程式:

image-20240926144129959

可以看出,編譯出來的LS檔案確實執行了system("ls")的操作,更改後的檔案所有者確實變成了root

現在我們在普通使用者下設定PATH環境變數,使用export PATH=/home/seed:$PATH/home/seed 新增到環境變數的開頭:

image-20240926145925741

然後我們在/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檔案:

image-20240928091157480

發現可以使用Set-UID程式執行我們的惡意程式碼,並且根據system("id")的結果來看:euid=0表示當前程序具有root許可權,表明惡意程式碼是以root許可權執行的。

Task 7:LD_PRELOAD環境變數和Set-UID程式

  1. 觀察環境變數在執行普通程式時如何影響動態載入器/連結器的行為,首先要進行如下配置:

    1. 構建一個動態連結庫,命名為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");
    }
    
    1. 編譯該程式:
    gcc -fPIC -g -c mylib.c
    gcc -shared -o libmylib.so.1.0.1 mylib.o -lc
    
    1. 設定LD_PRELOAD環境變數的值:
    export LD_PRELOAD=./libmylib.so.1.0.1
    
    1. 編譯下面的程式myprog.c
    /* myprog.c */
    #include <unistd.h>
    int main()
    {
    sleep(1);
    return 0;
    }
    
  2. 完成上述操作後,請在以下條件下執行 myprog,觀察會發生什麼。

    • 使 myprog 為一個普通程式,以普通使用者身份執行它。
    image-20240926155532626

    發現執行的是我們編寫的sleep函式。

    • 使 myprog 為一個 Set-UID 特權程式,以普通使用者身份執行它。
    image-20240926160631925

    發現等待了一秒後,沒有輸出,說明執行的是libc中的sleep()函式。

    • 使 myprog 為一個 Set-UID 特權程式,在 root 下重新設定 LD_PRELOAD 環境變數,並執行它。
    image-20240926161005509

    發現執行的是我們編寫的sleep函式。

    • 使myprog成為一個Set_UID user1程式,在另一個使用者帳戶(非root使用者)中再次改變LD_PRELOAD環境變數並執行它
    image-20240926162005224

    發現等待了一秒後,沒有輸出,說明執行的是libc中的sleep()函式。

  3. 設計一個實驗來找出導致這些差異的原因,並解釋為什麼第二步的行為不同。

修改一下myprog.c,列印這個程式執行時的程序的uideuid以及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,執行結果如下:

image-20240928090101443

我們發現:

  • 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的

結論:

當一個程序的uideuid一致時,子程序才會繼承父程序的環境變數,才會執行我們編寫的sleep()函式,第二步行為不同的原因是因為它們的uideuid的一致/不一致會導致子程序繼承/不繼承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],可以列印指定檔案的內容。

  1. 編譯上述程式,使其成為 root 所有的 Set-UID 程式。該程式將使用 system() 來呼叫該命令。如果你是 Bob,你能損害系統的完整性嗎?例如,你可以刪除對你沒有寫許可權的檔案嗎?

    1. 首先使其成為root所有的 Set-UID 程式:
    image-20240928093318801
    1. 嘗試刪除沒有寫許可權的檔案:

      • 首先建立一個seed沒有寫許可權的檔案,我們首先要將資料夾許可權改為seed不可寫,再將test.txt的屬性設為seed不可寫:
      image-20240928172847966
      • 發現catcall有命令注入漏洞,可以呼叫system()執行其他系統命令:
      image-20240928173223367

      ​ 使用命令catcall "test.txt;rm test.txt"成功將沒有寫許可權的test.txt刪除。

  2. 註釋掉 system(command) 語句,取消註釋 execve() 語句;程式將使用 execve() 來呼叫命令。 編譯程式,並使其成為 root 擁有的 Set-UID 程式。你在第一步中的攻擊仍然有效嗎?請描述並解釋你的觀察結果。

    1. 首先建立一個seed沒有寫許可權的檔案:
    image-20240928172847966
    1. 然後再使用命令catcall "test.txt;rm test.txt"
    image-20240928173712409

    ​ 發現無法刪除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。

image-20240928233806262

我們使用whoami命令檢視shell的擁有者:

image-20240928233936581

發現擁有者確實是seed,但是雖然這個程序的有效使用者ID是 seed ,但是該程序仍然擁有特權,我們可以以普通使用者的身份將惡意程式碼寫入/etc/zzz檔案中,這個過程需要利用檔案描述符fd。

我們可以使用echo "You have been hacked!!" >& 3,將這段話透過檔案描述符寫入/etc/zzz

image-20240928235100876

可以發現成功寫入了檔案。

原理

雖然程式碼中執行了setuid(getuid())操作,將程序的uid改為了seed,但是在執行execve(v[0], v, 0) 開啟一個shell時,由於在放棄特權時沒有關閉/etc/zzz這個檔案,建立的子程序會繼承/etc/zzz這個檔案的檔案描述符,造成特權洩露,子程序可以利用這個檔案描述符向檔案中寫入內容。

相關文章