软件安全实验4,内核条件竞争漏洞.主要是linux下的dirty cow,toctou相关缺陷攻击
race_condition
task1
这里提到了一个特殊的密码U6aMy0wojraho
。当我们在/etc/passwd中设置用户密码为这个时,可以直接enter登录,不需要填写。
发现写入到/etc/passwd之后确实可以不用输入密码登录
确实非常神奇。
task2
在做实验之前,我们首先关掉两个保护。一是防止用户把自己的符号链接链接到其他用户的文件上,二是防止root写用户的文件。因为一般用户的文件都是用户自己维护的,root一般能修改的都是系统的文件。
chmod 4755的作用
和chmod 755的区别在于:前面的4确保我们在以seed的身份执行此文件时,具有与所有着相当的权限。后续我们设置chown root也就是把所有者设置为root。
/etc/passwd格式
我们看一个典型的/etc/passwd文件。
1 | root:x:0:0:root:/root:/bin/bash |
这里第一位是root
,表示用户名称。
第二位是x。表示密码在/etc/shadow中。而其实/etc/shadow中存放的是hash过的用户密码。如果这里写入的不是x,也可以直接写入密码。这样OS就会在登陆的时候直接找这里的密码。
第三位是用户标识号。如果设置为0表示用户具有root权限。
第四个是用户组ID。一般只需要知道上述四个即可。
task2.A
这里程序中间暂停了10秒,在这段时间内我们可以将/tmp/XYZ
修改成/etc/passwd
即可。
原理:我们首先检查access文件是否可达的时候,看到的是谁运行的此文件(uid),因此如果我们想要写入/etc/passwd,此处以seed的身份无法通过access
的检查。但是fopen()
检查的是当前可执行文件的拥有者是否有对目标文件写入的权限(euid)。因此,我们用fopen打开/etc/passwd
时,是可以写入的。尤其是当我们用chmod 4755之后,始终可以使用fopen打开/etc/passwd。
于是我们要做的,就是在vlup进行sleep的同时,调用下面的函数,将/tmp/XYZ软连接到/etc/passwd即可。
1 |
|
成功截图如下所示。我们就把上面那个特殊的密码写入了/etc/passwd
之后就可以直接root。注意要先创建一个/tmp/XYZ
,并且把对应权限改为777。(pdf里面写的)
task2.B
我们编写如下程序。首先把/tmp/XYZ
改到/dev/null
,这是为了通过access()
的检查,其次usleep一段时间是为了防止竞争条件变化太快,最后把/tmp/XYZ
链接到/etc/passwd是为了通过条件竞争获取写的可能性。
我们如果想要成功,必须先unlink
到/dev/null
接着通过vulp
的access
。之后回到本文件的unlink到/etc/passwd,最后再回到vulp。可以看出条件竞争还是很苛刻的。
1 | #include <stdio.h> |
将上述程序编译,之后确保/tmp/ABC
文件存在并且拥有者为seed
。首先运行上述.c文件编译的attack
文件,接着运行target_process.sh
。但是尝试了好久也没有成功。期间查看tmp/XYZ
文件类型,发现已经被改为root
拥有。这样将一直无法通过access检查。
思考:看到上面文件大小很大,说明我们已经成功写入了这个文件。说明vulp肯定通过了access的检查。但是注意access只是检查调用者的身份。这里可能会有疑问: 调用者是seed,但是/tmp/XYZ是root拥有的,为什么还能通过access的检验? 做了相关试验后发现,access通过只需要文件用户组ID和调用者ID对的上即可通过。在这里我们以seed身份运行程序,而/tmp/XYZ也是属于用户组seed的(从上面这张图可以看出),因此本来就可以写入。
task2.C
这里解释了为什么之前我们的攻击不会成功。因为/tmp
文件夹有粘滞性质,只有符号链接的拥有者才可以修改文件,即使文件是所有人都可以写的。
作者给出了一个改进方案,就是使用一个原子操作来删掉现有的符号链接(也就是unlink)并且把它换成一个新的符号链接(因为这牵涉到两个不同的系统调用)。之前错误发生的原因是,如果在unlink之后,程序被切换成了vulp
,那么fopen
就会创造一个和自己用户组一样的文件,也就是输入root
的/tmp/XYZ
。所以造成了之前的问题。
那么我们只需要一个原子操作,把Unlink和symlink放在同一时刻执行即可。使用renameat2
系统调用可以原子性的交换他们。注意到我们先把/tmp/ABC
符号链接到/etc/passwd
,之后尝试交换/tmp/XYZ和/tmp/ABC的路径即可。
总结一下上述攻击流程,如下图所示。右边是三种攻击流程,包括了成功,一般的失败情况,和为什么会创建一个root的文件。
1 |
|
关于为什么fd参数为0的解释。
这里是成功的截图。
之后测试sudo test可以得到正确的结果。
task3.a
这里要求我们使用seteuid
来限制用户的权限。
如果在程序执行一开始调用seteuid,我们的effective userID将被设置为和read uid
一样的内容,而uid即为执行程序的人拥有的权限。这里就是seed。因此在fopen
时,进程检查我们的effective userID时,就会发现并不是root,从而失去对/etc/passwd写入的能力,从而完成了防御。同时,也不需要用access来做检查了。
我们把vulp改成如下所示的内容。
1 |
|
发现即使有的时候能够执行到open语句,也无法成功了。即使我们程序依然是4755权限。因为此时我们在打开文件时,权限已经被改为用户(执行者)的,无法修改/etc/passwd。
task3.b
同样的,尝试使用pdf里面给出的。下面这条命令开启了对于符号链接的保护。也就是说我们不能对有粘滞bit的文件夹中的符号链接做解引用。
运行之前没有setuid的程序,发现也无法成功了。
这样的原因可以见网站
这里设置为1之后,就相当于”sticky world-writable”的符号链接文件不允许我们follow,也就是无法通过/tmp文件夹中的文件进入别的地方,只有当符号链接的uid和想要通过这个符号链接访问别的东西的人的uid是一样的时候才行。
这样相当于我们在/tmp文件夹下的所有符号链接都设置了一个seteuid,确保了访问符号链接的用户一定和真实用户是一致的。
在本练习中,漏洞程序是root身份,/tmp的拥有者也是root,但是符号链接的创建者是seed。这就使得上述保护激活,从而不允许root的进程使用符号链接。
局限性:
这个保护只针对tmp文件夹下的符号链接。如果是其他文件夹下面,依然可以成功。推测可能在现实某个场景中,用户只能访问/tmp路径。
保护措施
其实task3讲了一些linux的保护措施。其实也有一些别的,这里来总结一下
- ubuntu的在全局可写文件夹中是否能够使用符号链接的保护。(task3.b)这使得我们在部分情况下不能对有粘滞bit的文件夹中的符号链接做解引用,这样我们就不能在/tmp里面放符号链接了。 ==为什么只限制/tmp?==
具体来说,粘滞符号链接保护如下所示。
执行者 | 目录所有者 | 符号链接所有者 | 是否可以访问 |
---|---|---|---|
seed | seed | seed | 可以 |
seed | seed | root | 不可以 |
seed | root | seed | 可以 |
seed | root | root | 可以 |
root | seed | seed | 可以 |
root | seed | root | 可以 |
root | root | seed | 不可以 |
root | root | root | 可以 |
(黑体部分为,如果我们创建了一个root的/tmp/XYZ时,攻击不能成功的原因。我们不能解引用此符号链接文件了)
- 把检查和使用的操作原子化。如下面的代码,可以实现检查文件和打开的原子化操作。
1 | f = open(file, O_CREAT | O_EXCL); |
- 每次open时,检查read uid而不是effective uid(就相当于上面的seteuid自动执行了)但是这个选项在linux中还没有。
1 | f = open(file, O_WRITE | O_REAL_USER_ID); |
- 利用access和open在代码中多次打开同一个文件,并且确保在所有被打开的文件全部相同的时候,再打开文件。因此攻击者必须确保所有文件同时竞争成功,才能完成攻击。
dirty COW
这是第二个条件竞争漏洞。
task1.1
我们用和pdf中完全一样的输入序列。创建一个用户不可写入的文件。
task1.2
我们首先以只读的方式打开文件(从之前的cat可以看出,这也是可行的)并映射一段内存出来,用来储存文件内容。在这之后,我们分为两步进行这次攻击。
write thread
write首先去内存中找到我们的目标(222222)的偏移(利用strstr),之后利用fseek找到所在位置。并且mmap的内存中写入数据。
1 | void *writeThread(void *arg) |
madvise thread
这个线程主要调用madvise
。看到man madvise中MADV_DONTNEED的含义。
这个意思是,当前这个进程已经用好了这一片内存,在最近的一段时间内不需要在使用了。操作系统可以把这一片内存释放了。
如何成功
当write的syscall发生时,如果我们调用了madvise,就会把write现在正在写的页表丢弃,而如果现在又切换回write,write现在在写的页表就变成了目标文件的内容,也就是只读文件的内容。具体来说,漏洞触发原理和copy on write机制相关。
首先是这两句。我们把只读文件以私有映射的方式映射到mmap出来的一片内存上。注意MAP_PRIVATE
的flag表示我们并不直接对源文件写入,而是创建一片新的内存。
1 | // Open the target file in the read-only mode. |
在创造了这样一篇内存之后,由于复制需要时间,操作系统并不会直接把这一片内容分配一个页面出来,而是等到用户要做修改的时候再复制用户写入的内容,并改变用户进程页表的项目。这样的做法叫做copy on write
。
如果用户丢弃了这样一片空间(使用madvise),那么页表将会被操作系统回收,之前mmap出来的地址将被重新指向写入之前的,也就是没有复制时候的地址。(最关键的地方其实在这里,重新指向写入之前的地址)
如果我们能够想办法在调用write
之后,调用madvise
释放空间,之后再回到write
,此时write指向的页面就是只读文件所在的页面,这样就能造成对只读文件的修改了。流程如下图所示。
思考:为什么写入内存的时候,还是没有检查是否可写呢?
write进行私有写的过程中,一共经过了三步1. 对映射的物理内存做了一份拷贝 2.更新页表,让虚拟内存指向新创建的物理内存 3.写入内存
因为write如果在已知当前内存是COW类型时,会申请新页面、切换页表的过程(也就是1,2)。而最后一部写入内存时,就不会再检查了。因此,我们可以在最后蓝色方块之前,加入一个条件竞争。一旦madvice成功,就使得写入的实际内存变为/etc/passwd。
本质上来说,问题还是系统调用write不是原子的。
可以看到,运行cow_attack
之后,很快只读文件就被修改了。
获得root权限
我们首先创建charlie账户,如下所示。可以看到charlie
只有一个user权限。
接下来尝试通过dirty cow攻击使得charlie拥有root权限。我们要做的只是修改上面”222222”字符串为”1001”,并且修改的目标为”0”即可,同时把打开的只读文件改为/etc/passwd
。这样我们最终将会把charlie
的userID设置为0也就是root。
如上所示,我们就获得了一个root的shell。
exp
最重要的就是查找charlie:x:1001
字符串,并将其替换为charlie:x:0000
。注意不能只把1001
改为0
。这样会导致只能改第一位。也就是把charlie:x:1001
改成了charlie:x:0001
。这样是不能起到攻击效果的。要改一个完整的才行。
1 |
|