苹果*OS
认证系统简介(一/三)
提出疑问
某日做可信认证登录功能的时候,遇到一个问题,怎样把自己制作的PAM
模块加入到MacOS
的登录流程中去,可以使用自己的PAM
模块登录到MacOS
,相信很多2B
的企业会用到这个功能。
本质上微软的Active Directory
域控制也是同样的功能,由于域控制太过于有名,所以苹果已经在用户与群组→网络账户服务器→加入
界面集成了Active Deirectory
,并且集成到了苹果自己的open directory
服务中去了。
我们想要实现的这个功能,是要在苹果本身的PAM
验证机制上,再加一层我们自己的PAM
,我们的PAM
模块也要验证通过,才可以进入MacOS
系统。
这就涉及到了搞清楚苹果PAM
模块认证的流程和机制等问题。
查找资料
Pluggable Authentication Module ,字面意思就是“插入式认证模块”,那么可拔插就是其基本特征,还有一个特征就是模块化,他是UN*X
系统认证API的抽象化和模块化。有了PAM
机制之后,我们不再受限于古老的/etc/passwd
和/etc/group
,而是可以采用“可拔插”的第三方认证模块、甚至是外部模块,来执行和返回认证结果,并可以通过对模块的hook
操作,进行日志记录、审计或者强制策略执行等任务。PAM机制最大的意义在于,通过配置认证文件的方式,引入单独的认证模块,解除了认证过程和认证逻辑的耦合,提高可用性、复用性和安全性。
虽然*OS
系统中没有采用PAM
机制,但是MacOS
中大量应用了PAM机制。当然,不仅仅是MacOC
,Linux
、Debian
、Solars
等流行系统中都大量应用了PAM机制,PAM机制在这些系统中都是通用的,而且PAM的文档做的也特别好,大家可以去看它的man
的page。
PAM模块非常简洁易用,从开发者的角度来说,只要在代码里调用PAM
的pam API
就好了。开发者加入libpam.dylib
的依赖之后,调用pam API
,PAM的库文件就会找到其相应的PAM模块配置文件,然后加载配置文件里相应的认证库,认证库里就有回调函数,用来返回结果给PAM库。这样就实现了认证过程与认证逻辑的分离,认证逻辑是看不到认证过程的,我们可以用下图来描述一下这个过程。
/etc/pam.d/service
是PAM的配置文件,配置文件里包含了认证规则,也就是auth
、acct
、session
、passwd
等关键字。pam_xxx.so
为认证库,这里才是真正的认证过程的实现。用户程序的认证过程通过PAM配置文件来查找认证过程,认证过程将结果返回给libpam.dylib
,最终回到用户程序。
函数类
PAM的模块库会导出四种类型的API,也就是四种类型的函数类。
类型 |
NAME |
类别 |
功能 |
类型一 |
auth |
认证类API |
提供认证函数,来对调用者的凭证进行认证,根据认证结果返回可供内核识别的UID值; |
类型二 |
account |
账户策略管理类API |
提供用户策略管理和执行的相关函数; |
类型三 |
session |
会话控制类API |
为认证通过的用户创建会话。在会话进行时,模块会提供可供PAM调用的回调函数,用来加载用户默认配置等功能; |
类型四 |
password |
凭证类API |
提供凭证管理功能,用户可以创建、删除、修改凭证。凭证的范围不仅仅是指文字密码,还有证书等等。 |
流程控制语句
虽然对于PAM来说,验证模块内部的执行过程是不可见的,但是验证模块终归会有返回值。如果验证通过,返回值为0,如果不通过,则会有一个错误代码(error code
)。有了错误代码,我们就可以与位运算(与、或、取反、异或,等等)相结合进行按位叠加,进行位运算的规则就由流程控制语句来决定。
语句 |
含义 |
功能 |
requisite |
一票否决 |
如果该语句指定的模块返回错误码,则让PAM立刻停止运行,返回认证失败; |
required |
部分否决 |
如果该语句指定的模块返回错误码,虽然PAM还会继续运行剩余语句,但是仍会返回认证失败; |
sufficient |
立即通过 |
如果该语句指定的模块返回0(验证通过),则让PAM立刻停止运行,返回认证通过; |
binding |
部分通过 |
如果该语句指定的模块返回0(验证通过),虽然PAM还会继续运行剩余语句,但是仍会返回认证成功; |
optional |
无关紧要 |
该语句指定的模块不会对接下来的语句结果产生任何影响; |
总结一下就是:
语句 |
requisited |
required |
optional |
binding |
sufficient |
模块返回 |
失败 |
失败 |
任意结果 |
成功 |
成功 |
PAM运行状态 |
立即停止 |
不会停止 |
不会停止 |
不会停止 |
立即停止 |
PAM返回结果 |
失败 |
失败 |
取决剩余语句结果 |
成功 |
成功 |
将函数类和流程控制语句结合在一起,就是PAM的配置文件了。由于配置文件就是最普通的文本文件,(而不是花哨的XML
或者json
之流,PS:那个年代貌似也没有json
等风骚的格式)因此非常易于阅读,笔者MacOS 10.12
的配置文件是这样的:
$ cd /etc/pam.d
$ ls
authorization authorization_la cups login.term screensaver screensaver_la su
authorization_aks checkpw ftpd other screensaver_aks smbd sudo
authorization_ctk chkpasswd login passwd screensaver_ctk sshd
$ cat login
# login: auth account password session
auth optional pam_krb5.so use_kcminit
auth optional pam_ntlm.so try_first_pass
auth optional pam_mount.so try_first_pass
auth required pam_opendirectory.so try_first_pass
account required pam_nologin.so
account required pam_opendirectory.so
password required pam_opendirectory.so
session required pam_launchd.so
session required pam_uwtmp.so
session optional pam_mount.so
在/etc/pam.d
文件夹里存在着颇多的配置文件,我们查看login
这个配置文件,可见其每一行由函数类、流程控制语句和.so
认证模块所组成。对的,你没看错,即使是在MacOS
的Darwin
系统上,也是.so
的格式,也就是说这些模块在UN*X
系统上是同样可以运行的。虽然.so
文件需要提供全路径,如果只提供文件名,则MacOS
会默认去/usr/lib/pam
文件夹去取.so
文件。
总结
稍微总结一下MacOS
中默认提供的那些.so
模块文件,这些模块也都是开源的。
模块 |
提供的函数类 |
/etc/pam.d 用户 |
功能 |
pam_aks.so.2 |
auth |
authorization_aks<br>screensaver_aks |
AppleKeyStore接口,目前暂未使用 |
pam.deny.so.2 |
all |
other |
直接返回失败 |
pam_env.so.2 |
auth<br>session |
无 |
设置(取消)环境变量 |
pam.group.so.2 |
account |
screensaver<br>su |
组设定 |
pam_krb5.so.2 |
all |
authorization<br>login<br>sshd<br>screensaver |
Kerberos 5 接口<br>Active Directory接口 |
pam_launchd.so.2 |
session |
login<br>rshd<br>sshd<br>su |
launchd接口 |
pam_localauthorization.so.2 |
auth |
authorization_la<br>screensaver_la |
本地认证 |
pam_mount.so.2 |
auth<br>session |
login<br>sshd |
自动挂载磁盘、用户分区等 |
pam_nologin.so.2 |
account |
login<br>rshd<br>sshd |
如果存在/etc/nologin 文件,则直接返回失败 |
pam_ntlm.so.2 |
auth |
authorization<br>login<br>sshd |
Windows NTLM 接口 |
pam_opendirectory.so.2 |
auth<br>account<br>passwd |
authorization<br>checkpw,chkpasswd,cups<br>ftpd,login,passwd<br>rshd,screensaver<br>sshd,su,sudo |
opendirectory数据库接口 |
pam_permit.so.2 |
account<br>auth<br>session<br> |
cups,ftpd,su<br>passwd,rshd<br>chkpasswd,cups,ftpd<br>passwd,smbd,sudo |
直接返回成功 |
pam_rootok.so.2 |
auth |
su |
如果getuid==0 则返回通过 |
pam_sacl.so.2 |
account |
smbd,sshd |
服务访问控制列表 |
pam_self.so.2 |
account |
screensaver |
验证目标账户名与程序用户名是否相同 |
pam_smartcard.so.2 |
auth |
authorization_ctk<br>screensaver_ctk |
智能卡支持接口 |
pam_uwtmp.so.2 |
session |
login |
将登陆记录写进utmpx数据库里 |
当然,我们还可以给验证模块来传递参数,参数写在.so
文件的后面就好,当然这些参数都是随.so
验证模块指定的,具体可以看man
命令,那里有详细的文档。参数可以是uid
、gid
、文件路径等等,我们用su
命令的PAM模块来举个例子:
$ cat /etc/pam.d/su
# su: auth account session
auth sufficient pam_rootok.so
auth required pam_opendirectory.so
account required pam_group.so no_warn group=admin,wheel ruser root_only fail_safe
account required pam_opendirectory.so no_check_shell
password required pam_opendirectory.so
session required pam_launchd.so
我们从上往下看:
pam_rootok.so
:auth
和sufficient
,按照字面意思就是认证立即通过的意思,也就是如果以root
用户身份登陆,那么立即通过,没有运行后续语句的必要;
pam_opendirectory.so
:required
是充分非必要条件,如果失败则整个结果都是失败。su
命令会把密码传输给pam_opendirectory
模块,如果失败,则su
命令失败;
pam_group.so
:所有带required
字段都是指充分非必要条件,no_warn
、group=admin,wheel
等参数,则表示如果su
命令的调用者想要切换到root
,则自身必须是admin
组或者wheel
组里面的;
pam_opendirectory.so
:这个模块第二次被调用,用来进行account
账户验证,no_check_shell
的意思是无需验证是否存在一个shell
,这就是为什么bin
或者daemon
这样的用户也可以运行su
命令;
pam_opendirectory.so
:这个模块第三次被调用,用来验证password
密码,将确认密码的任务交给opendirectoryd
,如果没有这句话,验证密码则又要去检查/etc/master.passwd
文件了;
pam_launchd.so
:上面的验证都通过了之后,即将开启会话session
时,调用pam_launchd.so
将会话移动到用户自己的launchd
命名空间内。
我们再举个例子,比如说我们要让任何用户都可以肆意使用su
命令,也不需要输入密码,可以把这句话:auth sufficient pam_permit.so
加到/etc/pam.d/su
文件的开头,这样任何情况下使用su
命令都会成功,哪怕是su
到root
也可以,也不需要密码,哪怕root
用户没有启用也没关系,详见HT204012。同样,如果将auth required pam_deny.so
作为文件第一行的话,那么任何情况下调用su
都会失败,哪怕是root
用户来调用也不行,因为pam_deny.so
会拒绝所有请求。
拓展
认证是系统安全保障的基础,认证的关键是搞清楚执行某项操作的用户具体是谁。用户使用凭证(账号加密码)登录到系统,然后开始进行会话。该会话的中的所有操作,都属于该用户,都在该用户的权限和策略下执行。
在这里需要强调一下,在内核的最底层,UNX系统只看得见用户ID和组ID,是看不见用户名的。"root"
这个用户名是没有任何意义的,其UID
的0
才是真正起到作用的。内核也不明白什么叫做凭证,以及这些凭证是否有效,比如说login
和password
这些操作的含义内核是不知道的,内核只会按照用户模式的代码来返回结果。也就是说,由UID(UNX内核空间对象)到用户名(用户所熟知的账户名)的映射是至关重要的,以及其凭证是否有效,这些就构成了系统的认证子系统。下面来看看MacOS
也好,这些类UNIX系统从UNIX系统继承下来的古老的认证机制。
password files (* OS)
在UN*X
的“远古时期”,UN*X
使用一个密码表文件——/etc/passwd
来存储用户信息和密码,参数之间使用冒号隔开。BSD 4.3
和MacOS
也采用了这种方法,该文件被命名为/etc/master.passwd
,格式也稍作调整:
name:passwd:uid:gid:class:change:expire:gecos:/path/to/home/dir:shell
使用单文件的认证系统无疑是非常危险的,黑客入侵系统后的第一件事情就是dump这个文件,暴力破解passwd
的域,然后重建缓存,尝试登陆。而且这个格式也存在很多缺陷,所以即使是MacOS
也只是仅仅把这种单文件认证方式用在单用户登陆的模式上。并且保持着对master.passwd
文件的更新,在MacOS 10.12
系统上还给他增加了很多守护进程的用户定义,比如_ctkd
还有_applepay
等等。
因为在OS系统上还残留着该认证方式的应用,所以我们这里还是需要再多提一下。/etc/master.passwd
文件是`OS`系统根文件系统(rootfilesystem)的一部分,定义了如下图所示的用户:
虽然该文件最初的用途是提供UID
到用户名的对应关系,我们可以看到大多数用户并不需要一个密码(* ),而且也不需要一个shell
(/usr/bin/false
)。当然,最重要的root
和mobile
用户并不是这样,他们的shell
被设置为/bin/sh
,可能是因为苹果觉得即使这样设置也没关系吧,因为发行版的iOS
系统上也不会有/bin/sh
的二进制文件存在,更加不会有login(1)
或者sshd(8)
这样的二进制工具存在了,没有这些工具,会话(session
)更是无从谈起。
在越狱机器上,我们拥有了/bin/sh
和sshd
之后,getpwent(3)
就会去查/etc/master.passwd
文件,root
和mobile
的初始密码就是上图中的/smx7MYTQIi2M
,其明文就是alpine
(这是初代的iPhoneOS的编译代号)。
PS:
曾经出现过针对iPhone
越狱设备的SSH蠕虫
,原理就是尝试连接越狱过的iPhone
的22
端口,并以默认的root
和alpine
作为用户名和密码进行登录,然后感染该设备。因此我们越狱之后要做的第一件事情,就是将默认密码改掉,换成一个强口令。当然最好还是生成一个密码对,每次使用秘钥文件登录,这样我们每次登录时都无需再输入密码,也只有拥有该秘钥的设备,才能够登录iPhone
设备,这样是最安全的。
SetUID and GetGID(MacOS)
setuid
和setgid
也是历史遗留的概念,可执行文件的上这两个权限位可以让执行用户迅速地了解到该二进制文件的所有人和所在组。这听上去好像有点烦,我们来具体解释一下。
但我们要更换自己地身份时,我们可以调用标准UN*X
的su(1)
命令(然后su
会检查PAM
规则,这个后面再说),su
命令会在内部调用setuid
这个系统调用。所以切换用户这个操作,一个特权操作,不是每一个普通用户,都可以操作成功的,得是root
用户才能成功。
这跟现状肯定是不符合的,因为我们一直在普通用户的身份下使用su
命令,这里面UN*X
系统做了什么事情呢?为了在普通用户的身份下setuid
也可以成功,UN*X
系统会让su
命令在被执行时,假定已经拥有了root
的权限。实现的方式是通过chown
将后续二进制文件的拥有者改为root
,再通过chmod u+s
这个命令,来执行该二进制文件。
这种机制其实问题比较大,但是初期的UN*X
系统却大量使用了这种机制,比如passwd
,因为这个命令必须要编辑/etc/passwd
和shadow
文件,但是又只能是root
才能编辑这两个文件,所以也使用了跟su
一样的机制。这种机制的核心问题,就是对su
命令也好、passwd
命令也好,对这些命令的绝对信任;但是,我们也清楚,绝对的权利,就会导致绝对的浪费(低效甚至无效)和绝对的腐败(内部溃烂,户枢不蠹)。
对于这种非常“不优雅”的机制,发生问题也是迟早的事情,有些命令可以轻易地被符号链接或者条件竞争所绕过,执行任意文件修改;有些则存在字符串溢出漏洞,轻易达成任意代码执行。像setuid
和setgid
这种机制其本身就不应该存在,但是这些却是UN*X
构建之初就存在的组建,想要拿掉无异于重写系统,我们还是得与之共存。
Darwin
一直在减少setuid
和getuid
机制的使用。在引进opendirectory
之后,passwd
命令终于不再遭受setuid
和getuid
的荼毒了。其他命令也或多或少在引入XPC
和entitlement
(沙盒)之后,减少了对setuid
和getuid
的依赖。就拿最近发生的事情来看,Install.framework's runner
和SystemAdministration.framework's readconfig
的setdui
标志位已经在10.11
和10.12
中被移除了,只剩下下图中的这几个命令还需要依赖setuid
和getuid
了(原因被标注在右侧#
号内。)
JialindeMacBook-Air:~ jialinchen$ find / -user root -perm -4000 2> /dev/null
/bin/ps #系统进程统计
/System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/MacOS/ARDAgent
/usr/bin/at #连接到atd守护进程
/usr/bin/atq #计划作业
/usr/bin/atrm #移除计划作业
/usr/bin/batch #功能性设置
/usr/bin/crontab #历史遗留问题(必须编辑/usr/lib/cron/tabs)
/usr/bin/login #必须依赖setuid
/usr/bin/newgrp #历史遗留问题(必须编辑/etc/group)
/usr/bin/quota #历史遗留问题(编辑配额文件)
/usr/bin/su #历史遗留问题,必须依赖setuid
/usr/bin/sudo #历史遗留问题,必须依赖setuid
/usr/bin/top #提权进行系统进程统计
/usr/libexec/authopen #打开系统内的任意文件
/usr/libexec/security_authtrampoline #执行特权命令
/usr/sbin/traceroute #历史遗留问题,直接操作接口
/usr/sbin/traceroute6 #历史遗留问题,直接操作接口
虽然“榜单”已经很小了,但问题其实还是很大,举个例子来看,一直到10.10.5
版本的动态链接器/usr/lib/dyld
,当跟有setuid
的二进制配合使用时,在特定情况下可以触发提权漏洞,导致任意代码执行(后面有时间可以细聊下)。总之,苹果是时候将历史的裹脚布脱下来,换上特步或者阿迪王,继续“不走寻常路”!
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!