Linux系列第一篇!
本期介绍Linux中的权限模型,从 ls -l 的解释一路科普到内核漏洞的利用(什么!)


由于 Linux 设计时是一个多用户系统,可能有很多人共用一个 Linux 系统,因此 Linux 中存在用户用户组的概念,每个用户或者用户组都有一个自己的 id,每个用户可以属于多个用户组。
有了用户之间的区分,就可以为文件设置权限,限制不应该访问的用户的访问,于是就有了权限系统。

用户和用户组这两个抽象的概念,其实主要体现在两个地方:

  • 进程系统:每个进程都有自己所属的用户和用户组
  • 文件系统:每个文件都有自己所属的用户和用户组,以及相应的读写权限设置

可以使用 getuid() 系列系统调用获取当前进程的用户 id,在 shell 里可以直接输入 id 查看当前 shell 进程的用户和用户组 id。(这里先不提及 id 的区别,后面再进行讲解)

文件系统中的权限模型

使用命令 ls -l 可以查看文件的详细信息,比如:

1
2
3
4
5
$ ll
drwxr-xr-x 4 cameudis cameudis 4096 Mar 24 2023 .cargo/
drwx------ 2 cameudis cameudis 4096 Nov 2 10:43 .ssh/
-rw-r--r-- 1 cameudis cameudis 4957 Oct 31 18:41 .bashrc
-rw-r--r-- 1 cameudis cameudis 619 Oct 31 10:11 memo.txt

我们以第一条为例:

文件类型 权限信息 连结数 拥有者 用户组 文件大小 修改日期 文件名
d rwxr-xr-x 4 cameudis cameudis 4096 Mar 24 2023 .cargo/

第一个字段文件类型包括以下这些(从这里也可以看到万物皆文件的思想):

字符 文件类型
- 普通文件
d 目录
| 符号链接
p named pipe
c 字符设备
b 块设备
s socket 文件

我们可以看到,每个文件都会有一个所属的用户、一个所属的用户组。相应地,一个文件的权限设置会有三档:对于所属用户的权限对于所属用户组中用户的权限对于其他用户的权限。在一些权限设置工具 chmod 中,这三者分别简称为 U G O,即 User、Group、Others。

ls -h 看到的信息中,我们看到的 rwxr--r-- 字符串,其实就类似一个 bit vector。前三个字符表示对于所属用户的权限,中间三个表示所属用户组中用户的权限,最后三个字符表示对于其他用户的权限。

比如,.cargo 目录归 cameudis 所有,那么 cameudis 作为拥有者,其权限是 rwx(Read、Write、eXecute);而另一个用户 Jern,若他不属于 cameudis 用户组,那么他的权限是 r--(Read only)。

你可能会好奇,为什么 Linux 下各种目录大小都显示为 4096:这是硬盘中用来存储目录 metadata 信息的大小,这些 metadata 有:
如果你需要计算目录大小,可以使用 du 指令,比如 du -sh /tmp

修改文件权限

一句废话就是:如果想要修改文件的权限,你必须拥有文件的权限。
在命令行中,我们最常使用的修改权限工具是 chmod

chmod 中,最简单的用法就是:chmod <+/-><r/w/x> <filename>,这样会给用户、组和其他人通通加上或减去某个权限,比如:chmod +x a.out 就能让所有人都获取执行该文件的权限。

在此基础上,还可以特别指定某一群体:chmod [ugoa...]<+/-><r/w/x> <filename>。比如 chmod u+x a.out 就可以只给拥有者执行该文件的权限。

不过,根据笔者观察,大家最常用的用法是直接使用数字指定。我们知道每个文件有三组权限,所以可以用三个 3 比特的值来分别表示一个文件的三组权限。在这个 3 比特的值中,约定最高位表示 r,中间一位表示 w,最后一位表示 x。所以,111 就对应 rwx010 就对应 -w-

然后,我们再将其写为 8 进制,111 就会变成 7(如果你硬要说是更大的进制也可以),这样我们就可以用一个阿拉伯数字表示一组权限。

再将其推广一下之后,就可以用三个数字表达三组权限,我们列出一些经常用到的权限作为例子:

权限编码 权限说明
755 rwxr-xr-x
600 rw——-
644 rw-r–r–
777 rwxrwxrwx

chmod 使用这种语法来让我们快速指定权限:chmod 755 ./a.out

冷知识:
在著名动漫《新世纪福音战士新剧场版》中,明日香操纵 EVA 二号机进入野兽模式时,使用的指令是 “Code 777”,这说明 EVA 二号机运行的是 Linux 操作系统⊂彡☆))∀`)
chmod 777

常见的文件权限
目录权限通常设置为755。其中7表示rwx,5表示rx。这里,x权限用于进入目录,r权限用于读取目录;换句话说,若去掉某个目录dir的x权限,则cd dir会报错;若去掉r保留x,则可以进入这个目录,但在目录中运行ls会出错;没有w权限,表示不能在目录中删除或新建文件。注意,删掉一个文件并不需要该文件的w权限,而只需要文件所在目录的w权限。一个文档文件的权限通常设置为422,即没有x权限。符号链接文件的权限为777,因为真正起作用的是链接所指向文件的权限。(来自银杏书

文件的权限被修改,对已被打开的文件会立即生效么?
考虑如下情况:在进程 A 打开某个文件时,该文件具有可写权限,因此进程 A 以可读可写权限打开了文件;然后,文件的权限被拥有者修改为只读,那么之后当进程 A 对文件进行写操作时,会成功还是失败呢?根据前一段的描述,进程 A 会一直拥有对文件的写权限,直到关闭该文件。若系统希望对文件的权限更新立即生效,则需要在更新权限的同时,遍历所有打开文件的 fd 并做相应的处理,例如直接关闭所有权限不匹配的 fd,这样进程 A 下次进行文件操作时就会出现错误。(来自银杏书

进程系统中的权限模型

每个进程都会有 UID 和 GID,且相关数据会继承给子进程(当然满足条件就可以修改自己的 UID 和 GID,只要符合一些要求,可以到对应系统调用的 man page 中查看具体要求):

1
2
3
4
5
6
7
$ cat getuid.c
int main() { printf("UID: %d\n", getuid()); }
$ gcc -w -o getuid ./getuid.c
$ id
uid=1000(cameudis) gid=1000(cameudis) groups=1000(cameudis),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),117(netdev),1001(docker)
$ ./getuid
UID: 1000

一个进程并不是只有一个 UID(用户 ID)和一个 GID(用户组 ID),而是根据不同用途有多个。

在操作系统为进程准备的结构体中,一个进程包含如下几种 ID:

  • Effective(eUID、eGID):大多数权限检查都使用这两个 ID。
  • Real(UID、GID):真正的 ID,可能与 eUID、eGID 不同,用作信号检查等。
  • Saved:用于切换的 UID/GID,在临时降权的时候用到。

eUID 和 eGID 最为常用,进程是否能够打开文件等权限检查都使用 eUID 和 eGID,因此 id 指令默认显示的也是 eUID 和 eGID。(不过可以用 -r 参数指定显示 real UID/GID)。

SUID & SGID

之所以需要区分 effective ID 和 read ID,是因为在某些场景中,需要区分这两个 ID。我们设想这样一个场景(纯虚构,细节问题不要在意):

Jern 安装了一个 Web 服务器软件(假设服务器软件的所有者和用户组都是 Jern),想要让运维 Cameudis 也能够执行该软件,因此他就把软件文件的权限设置为 r-x
Cameudis 开心地启动!了 Web 服务器,但是访问网站时发现无法正常访问网页。

原来由 Cameudis 执行的 Web 服务器进程,其 eUID 和 eGID 都是 Cameudis 的,因此这个进程没办法访问 Jern 放在目录中的 html 网页文件!

为了解决场景中的这一问题,一个方法就是再给 Cameudis 目录中所有的文件的权限。但这种方法的缺陷在于,万一程序还需要访问未知位置的一些目录,我们可能不能一直及时地给 Cameudis 权限。
另一个方法就是将 Cameudis 加入 Jern 所在的用户组。这种方法挺好的,不过需要具体情况具体分析下加入之后有没有潜在危害。

此外,还有一种 Linux 提供的方法,这种机制允许用户在文件中加入 SUIDSGID 权限位(就和 RWX 一样),如下所示:

  • SUID (Set-UID):当前文件被执行时,以文件拥有者 UID 作为 eUID 而不是父进程的 eUID。
  • SGID (Set-GID):当前文件被执行时,以文件拥有组 GID 作为 eGID 而不是父进程的 eGID。

这里说当前文件被执行,显然默认了文件是可执行文件。不可执行文件被设置这两个位是可行的但并没有意义。

因此,Jern 可以给 Web 服务器的程序文件加上 SUID bit,这样 Cameudis 执行 Web 服务器时,服务器进程会以 Jern 的 eUID 运行,从而就能够访问所有 Jern 本人可以访问的文件。

一个具体的例子就是 sudo 程序,我们可以这样查看其权限信息:

1
2
3
4
which sudo       # which 让shell查找某个程序的具体位置
/usr/bin/sudo
$ ll /usr/bin/sudo
-rwsr-xr-x 1 root root 166056 Apr 4 2023 /usr/bin/sudo*

可以看到,sudo 的 U 权限是 rws,这里的 s 就表示 SUID。我们在执行 sudo 时,会以 root 用户的 eUID 执行,从而能够访问高权限的资源。

使用 chmod 给文件加 SUID 和 SGID 的方法:
chmod u+s <file>
chmod g+s <file>

Sticky bit

Sticky bit 主要用于目录,对于标记为 Sticky 的目录中的文件,只有文件的所有者与目录的所有者才能重命名或删除文件,其他行为则照常。

通常来说,我们用 Sticky bit 来保护一些共享的文件夹,这里的共享是指多个用户都会在这个文件夹中处理文件。比如,/tmp 文件夹就常常被置为 Sticky,来防止普通用户删除或者移动其他用户的文件:

1
2
$ ll /
drwxrwxrwt 74 root root 36864 Nov 6 14:09 tmp/

至于给非目录的普通文件置 Sticky bit,各类 Unix 系统的对待方式都不一样,比如 Linux 就是直接忽略置 Sticky 的文件。

使用 chmod 给文件加 Sticky 的方法:
chmod o+t <file>

特殊的存在 root

以上说的各种权限限制,都对 root 用户无效。作为 Linux 系统中的真神,root 用户和用户组都拥有特殊的 ID 0。通常为了使用 root 的力量,我们会借用 sudo 这个 SUID 程序。

root 用户可以:

  • 打开任何文件,包括 /proc 中一些高权限的文件如 kallsym
  • 执行任何程序
  • 切换到任何其他用户
  • 调试任何程序
  • 关机、重启
  • 加载设备驱动等内核模块
  • ……

简单来说,root 用户可以控制整个系统

由于 root 的力量过于强大,所以任何一个略有安全意识的人,在平时都不会以 root 用户的身份执行指令,除非必要。相关反例实在太多了,几乎每个默认 root 用户登录的人都会因此出现一些问题。(笑死)

既然 root 的力量如此强大,那么可想而知,如果黑客拿到了我们机器的 root 权限,那会是多么可怕的一场安全灾难。因此,接下来我们学习如何作为黑客拿到 root 权限。

权限提升(提权)

提权,一般就是指黑客将他们权限从普通用户提高到 root 的一类攻击,通常的提权流程是这样的:

  1. 在系统上初步站稳脚跟,比如通过有漏洞的程序拿到一个 shell(pwn!)
  2. 找到一个可以利用的高权限服务
  3. 利用那个高权限服务,借助它拿到权限

什么是可以利用的高权限服务呢?

  1. SUID 程序就是一种高权限的服务,如果它存在漏洞的话,我们就可以通过利用漏洞来达成提权,比如 sudo 就有过非常多的 CVE,可以攻击 sudo 的漏洞来拿到 root 权限。
  2. 有一些不必要有 SUID 的程序,如果能够以 root 权限运行的话,会带来令人意想不到的安全风险。
  3. 操作系统内核显然是最高权限运行的服务了,如果内核存在漏洞,同样可以帮助我们达成提权。这就是传说中的内核漏洞利用

前者比较容易理解,就是普通的用户态程序利用而已,因此本文中我们主要介绍后两者。

SUID 提权

如果你发现 mv 程序是 SUID 的,你可以做到哪些事情?
看起来我们只能移动一些文件,但实际上,每个常见 Linux 程序的功能都可能非常强大。比如就算是简单的 mv 指令,也可以做到彻底的提权。

mv 的提权方法,可以参考The Dark Side of mv Command. mv, short for MOVE has been one of the… | by Nikhil Jagtap | WorkIndia.in | Medium

更多 binary 的提权方法,可以见 GTFOBins

强烈推荐读者去 pwn.college 实地打几道题来试试看。

如果 /bin/sh 作为一个 SUID 程序运行,即 eUID 和 rUID 不同,那么它会主动降权限,将 eUID 设置成 rUID。这就是一种应对 SUID 提权的非常简单的缓解措施。
遇到这种情况,只要加上 -p 参数即可。

内核漏洞

我们知道,操作系统不过就是一个用户程序与资源的管理器而已,它同样也是一个由程序员写成的程序,操作系统也会有漏洞。

我们平常说的操作系统,通常包括了许多东西,比如桌面系统。不过,这里我们要关注的是一个操作系统真正重要的东西——内核kernel,台湾称为核心)。

内核是一个运行在更高级别的程序,我们刚刚提到的各种机制,包括文件系统、进程系统,这些系统的实现统统位于内核之中。比如我们刚刚提到了 eUID、Real UID,这些东西统统都是内核中为每个进程准备的结构体中的一个字段。

如果读者学过 OS,那么就会知道用户态程序和内核交互的最常见的方法就是通过系统调用。因此,如果系统调用涉及的某段代码中存在漏洞,我们就能从用户程序攻击内核。(这就是为什么内核 pwn 的 exp 都是一个 C 程序,自己编写软件来攻击内核显然是最方便的)

提权是攻击内核最常见的目的之一。如果我们控制了内核,比如劫持了控制流,我们就可以去调用内核中存在的函数,将当前进程的权限提高至 root。

具体来说,我们一般会控制内核执行 commit_creds(prepare_kernel_cred(0)),其中 prepare_kernel_cred(0) 会创建一个各个字段都是 0 的 cred 结构体,然后 commit_creds() 可以将当前进程的 cred 替换为参数。我们前面知道 root 拥有特殊的 id —— 0,因此调用完这两个函数后,就可以将进程权限切换为 root
此后进程就可以想干啥就干啥了,比如起一个 root shell。

当然,这里只是一个小科普,内核漏洞笔者还没有入门,希望读者里能有未来挖掘或利用内核漏洞的大能|∀`)


参考资料:pwn.college