为什么空密码能够取得你的ROOT权限?
背景
为防止你还不知道这个消息,目前出现了一个足以影响到最新版macOS(High Sierra)的严重的安全漏洞。任何一个掌握了此漏洞的人都可以凭此使用空密码或是他们自己选的密码登录ROOT账户。
显然,这个漏洞刚刚才被披露出来,危害还不大,在苹果公司的内部开发者论坛中,此漏洞是用来帮助那些有登陆问题的用户的:
但是,当Lemi Orhan Ergin (@lemiorhan)发表了推特称:“我们注意到macOS(High Sierra)存在着严重的安全问题”之后,这个漏洞赢得了更广泛的关注。
我对这个漏洞感到相当的好奇,因此决定对macOS进行逆向,以追查其根源。在这篇博文中,我公布了我的发现,并揭示了这个bug的底层原因。
所以,不要再啰嗦了,让我们马上进入正题!
深层挖掘
首先,让我们看看发生在高级别的事情。当用户(或攻击者)尝试登录到当前未启用的帐户(即root)时,出于某种未知的原因,系统将天真地使用该用户指定的任意密码(即使该密码为空)创建该帐户。那么用户(或攻击者)就可以轻易地登录到该帐户:
这两个步骤的过程解释了为什么要执行这个攻击,你必须敲击enter键或点击“解锁”两次:
目前已被证实,如果用户启用了屏幕共享等服务,这种攻击也可以远程执行!
当然,在不提供任何形式认证的情况下,不应该允许用户随意启用帐户,特别是具有全部强大功能的root帐户(通过远程方式!)。所以,到底发生了什么?到了对macOS系统进行深挖的时候,让我们看看幕后发生了什么!
当用户(或攻击者)试图向一个账户进行认证时,这个操作由'opendirectory'守护进程(opendirectoryd)处理。通过调试这个守护进程,我们可以查看此守护进程接收到一个mach XPC认证信息时发生的函数调用的顺序:
# ps aux | grep opendirectoryd
root 70 /usr/libexec/opendirectoryd
lldb -p 70
...
(lldb) bt
* frame #0: opendirectoryd`od_verify_crypt_password
frame #1: PlistFile`___lldb_unnamed_symbol26$$PlistFile
frame #2: PlistFile`odm_RecordVerifyPassword
frame #3: opendirectoryd`___lldb_unnamed_symbol37$$opendirectoryd
frame #4: opendirectoryd`___lldb_unnamed_symbol313$$opendirectoryd
我们将从odm_RecordVerifyPassword函数开始。这个函数在PlistFile二进制文件中被实现。这个包(库)从/System/Library/OpenDirectory/Modules/PlistFile.bundle路径下被动态加载到opendirectoryd进程中:
(lldb) image list
[ 0] 50686B40-3B06-347D-B906-DCEF1D9F10E1 0x00000001041e5000 /usr/libexec/opendirectoryd
...
[188] A38BC5A0-67AA-3D75-89AD-57A7DF6D20BE 0x000000010447f000 /System/Library/OpenDirectory/Modules/PlistFile.bundle/Contents/MacOS/PlistFile
在odm_RecordVerifyPassword 函数上设置一个断点,我们可以转存它的参数(通过RDI,RSI,RDX,RCX传递进来的):
Process 70 stopped
* thread #15, stop reason = breakpoint 1.1
PlistFile`odm_RecordVerifyPassword:
-> 0x10448e50b: pushq %rbp
(lldb) po $rdi
<OS_od_module: 0x7fcb0dc29110>
(lldb) po $rsi
<OS_od_connection: 0x7fcb0dc26cb0>
(lldb) po $rdx
<OS_od_request: 0x7fcb0dc78d30>
(lldb) po $rcx
<OS_od_moduleconfig: 0x7fcb0dc203b0>
来看看它的反编译,我们可以看到它调用了另一个函数:sub_18f1
sub_18f1(&var_818, odconnection_get_context(rbx), r13);
传递给此函数(R13)的最后一个参数是一个字典,其中包含用户(或攻击者)正尝试向其进行身份验证的帐户信息:
(lldb) po $r13
{
"dsAttrTypeStandard:AppleMetaNodeLocation" = (
"/Local/Default"
);
"dsAttrTypeStandard:GeneratedUID" = (
"FFFFEEEE-DDDD-CCCC-BBBB-AAAA00000000"
);
"dsAttrTypeStandard:Password" = (
"*"
);
"dsAttrTypeStandard:RecordName" = (
root,
"BUILTIN\\Local System"
);
"dsAttrTypeStandard:RecordType" = (
"dsRecTypeStandard:Users"
);
"dsAttrTypeStandard:UniqueID" = (
0
);
}
请注意 dsAttrTypeStandard 键的值:*。我们稍后会看到这个值!
接下来,odm_RecordVerifyPassword函数会调用另一个辅助函数:sub_826b,它依次调用sub_5192。反编译这个函数出现的一串字符串显示它将从用户(或攻击者)正在尝试登录的帐户读取“shadowhash data”。这个'shadowhash数据'存储在'dsAttrTypeNative:ShadowHashData' 键中:
可以使用命令dscl . -read / Users / <user>通过终端查看用户的“shadowhash”,或直接从路径/ private / var / db / dslocal / nodes / Default / users / <user>下读取:
$ dscl . -read /Users/user
...
AuthenticationAuthority: ;ShadowHash;HASHLIST:
<SALTED-SHA512-PBKDF2,SRP-RFC5054-4096-SHA512-PBKDF2>
;Kerberosv5;;user@LKDC:SHA1.F69BF62F41274B2B983399C0D143CD33961... GeneratedUID: A39EF7FC-E5B1-46B8-AB47-3C2B7DA49425
应该注意的是,对于已启用的帐户,如用户帐户,sub_519函数将成功读取...因为这个“shadowhash”数据确实存在。但是,对于已禁用的帐户(例如正在作为目标的root帐户),此信息不存在:
$ dscl . -read /Users/root | grep ShadowHash | wc
0 0 0
当“shadowhash”数据不存在时,sub_5192函数将失败(返回0x0)。这会导致在sub_826b函数中执行“else”子句:
rax = sub_5192(var_98, r15, r12, r14, &var_88, &var_80);
if (rax != 0x0) {
//found shadow hash data
}
//no shadow hash data found
else {
//read 'dsAttrTypeStandard:Password'
rax = odproplist_get_array(r12, *_kODAttributeTypePassword);
...
var_41 = 0x0;
var_54 = 0x1388;
if (od_verify_crypt_password(var_70, rax, var_60, &var_54, &var_41) != 0x0) {
//upgrade password
sub_13d00(arg7, var_60);
sub_14324(var_70, var_A0, var_68, var_50, r15, var_60, arg7);
...
在'else'语句中,代码首先从kODAttributeTypePassword(dsAttrTypeStandard:Password)键中读取值。然后,它调用od_verify_crypt_password函数来验证用户(或攻击者)传入的密码是否与该帐户的密码相匹配。
例如,如果尝试使用密码“hunter2”登录(已被禁用的)根帐户,则使用“*”(根帐户的dsAttrTypeStandard:Password值)和“hunter2”调用od_verify_crypt_password:
Process 70 stopped
* thread #12, stop reason = breakpoint 1.1
opendirectoryd`od_verify_crypt_password:
(lldb) po $rdi
<OS_od_request: 0x7fcb0f2625e0>
(lldb) po $rsi
<__NSCFArray 0x7fcb0f2511b0>(
*
)
(lldb) po $rdx
hunter2
如果我们继续调用,它会返回一个非零值(al = 0x1)....意味着成功?有趣!
(lldb) reg read al
al = 0x01
由于返回了非零值,并且没有执行其他检查,所以代码将执行假定提供有效密码的逻辑(尽管真实情况并非如此!)具体来说,会调用各种方法,如sub_13d00。正如在反编译中显示的调试日志语句,它们将执行从crypt密码到shadowhash或securetoken的升级:
"found crypt password in user-record - upgrading to shadowhash or securetoken"
如果我们查找一下这些“升级”子程序(比如sub_13d00)是如何被调用的话,那么我们会发现其实是我们提供的密码(即'hunter2')起了作用:
Process 70 stopped
* thread #10, stop reason = breakpoint 2.1
PlistFile`___lldb_unnamed_symbol26$$PlistFile:
-> 0x104487552 <+743>: callq 0x104492d00
0x104487557 <+748>: subq $0x8, %rsp
0x10448755b <+752>: movq -0x70(%rbp), %rdi
0x10448755f <+756>: movq -0xa0(%rbp), %rsi
(lldb) po $rsi
hunter2
这个新的“用户指定的”值随后被转换为shadowhash / securetoken,然后保存到帐户(例如root账户)中。 因此,用户(或攻击者)便可以登录,因为他们可以用指定的密码访问对应帐户!
让我们回顾一下。 当用户(或攻击者)尝试使用任何密码(包括空白)对帐户进行身份验证时:
对于被禁用的帐户(即没有“shadowhash”数据的),macOS将尝试执行升级
在此升级过程中,od_verify_crypt_password返回一个非零值,并且不执行其他检查,因此代码假定升级成功
接下来“新”用户提供的密码被成功更新(“shadowhash / securetoken”)并为对应帐户保存下来
这解释了(很大程度上)为什么可以使用任意(或空白)密码激活和访问root帐户。
剩下唯一的问题是(除了这个bug是如何通过High Sierra版本中的QA测试发布出来的),即为什么od_verify_crypt_password函数没有失败? 或者如果失败了,为什么没有被发现? 现在我们来仔细看看。
顾名思义,od_verify_crypt_password应该验证用户(或攻击者)指定的密码是否对帐户有效。 例如,当我们尝试使用'hunter2'来验证禁用的root帐户时,od_verify_crypt_password应该果断地告诉我们GTFO(滚开)。
od_verify_crypt_password函数直接在“opendirectory”守护进程(opendirectoryd)中实现。 如前所述,它被PlistFile包,特别是'sub_826b'函数调用:
//sub_826b
//check password and upgrade if necessary
if (od_verify_crypt_password(var_70, rax, var_60, &var_54, &var_41) != 0x0)
{
//upgrade password
sub_13d00(arg7, var_60);
sub_14324(var_70, var_A0, var_68, var_50, r15, var_60, arg7);
}
我们已经注意到它被调用时所使用的各种参数,如帐户的密码散列和用户/攻击者指定的密码。 但是,第四个参数(var_54)也是很重要的! 在被调用之前,它被设置为0x1388:
var_54 = 0x1388;
if (od_verify_crypt_password(var_70, rax, var_60, &var_54, &var_41) != 0x0){
...
在osstatus.com上查找0x1388(十进制数5000),显示这个值对应于'kODErrorCredentialsInvalid':
要执行现实的密码验证,od_verify_crypt_password函数会调用crypt_verify。帐户密码散列值(例如,对于被禁用的root帐户;'*')、所提供的密码(例如'hunter2')及var_54(被传递给od_verify_crypt_password的参数)都会被传递给此函数。这个参数同时被保存到R14寄存器中,当且仅当某些字符串比较结果为真时,该参数被设置为0x0:
int _crypt_verify(int arg0, int arg1, int arg2, int arg3) {
r12 = arg3;
r14 = arg2;
...
if (strcmp(&var_130, r13) == 0x0) {
*(int32_t *)r14 = 0x0;
}
通过静态和动态分析,我们可以确定(如预期的那样),这个字符串比较是将提供的密码的散列值与帐户的实际密码的散列值进行比较。 换句话说,它正在验证密码(散列)匹配:
Process 70 stopped
opendirectoryd`crypt_verify:
-> 0x104243f3c <+965>: callq 0x104249e8a; symbol stub for: strcmp
(lldb) x/s $rdi
0x70000665eec0: "*.dAJ47YHEIRE"
(lldb) x/s $rsi
0x7fcb0ddcb851: "*"
这些字符串显然不匹配,所以strcmp函数不会成功(即返回值不会== 0x0)。 因此,我们正在跟踪的被传入的参数不会被设置为0x0。
在这一点上,我们对这个参数的作用有了清晰的认识。 它是一个指向一个变量的指针,当且仅当密码(散列)匹配时,才从crypt_verify函数中设置为0x0的od_verify_crypt_password传入。 因此,我们可以想象下面的伪代码:
//verify
// 'match' will be set to 0x0 if verification is ok!
int match = kODErrorCredentialsInvalid;
od_verify_crypt_password(accountHash, providedPassword, &match, ...);
....
//verify by checking hashes
// 'match' will be set to 0x0 if verification is ok!
if(strcmp(providedPWHash, accountPWHash, user) == 0x0) {
*match = 0x0;
}
正如我们前面指出的,只有od_verify_crypt_password的返回值会被检查....而不是实际验证的结果!(即 所谓“匹配”的变量,var_54)。
这可以通过检查以下的反编译来确认,这段反编译代码显示了对od_verify_crypt_password的调用。
请注意,所谓“匹配”变量(var_54)在调用之后从未被检查。 相反,升级函数(sub_13d00,sub_14324)被错误地调用:
苹果官方的回应
发布这个博客不久之后,苹果发布了一个针对macOS 10.13和10.13.1的补丁。 该补丁可以从苹果的支持网站直接下载:
或者,它应该自动显示为安全更新(在macOS应用商店中):
该错误被分配为CVE-2017-13872,苹果在安全发布说明中指出,这仅仅是“证书验证中存在的逻辑错误”。 他们指出,他们的补丁“改进了凭证验证”。
你可能想知道他们是如何修补这个bug的? ...我们在这个博客中发现的根本问题是正确的吗?
比较未打补丁和修补的PlistFile二进制文件,我们可以看到苹果添加了代码来检测无效的凭据(即,当未经身份验证的攻击者试图设置root密码时):
特别要说的是,如预期的那样,他们现在在调用od_verify_crypt_password之后也会检查所谓“匹配”变量的结果:
lea rbx, qword [rbp+var_54] ;load addr of 'match' in rbx
mov rcx, rbx ;move into arg4 for call
call imp___stubs__od_verify_crypt_password
mov ecx, dword [rbx] ;get value of 'match'
test ecx, ecx ;is it 0x0?
jne noMatch ;no, then bail!
因此,我们的分析是正确的!
不幸的是,与其他的苹果补丁一样,这个补丁似乎还带来了一些严重的问题。首先,它破坏了不同用户的文件共享:
正如我指出的(谢谢@alvarnell),这种不兼容性很快就被一个新的补丁修复了(将构建带到17C1003)。
更糟糕的是,据Wired报道,如果macOS 10.13上的用户使用该补丁,然后升级到macOS 10.13.1,则会重新引入该错误:
聪明的Pepijn Bruienne(@bruienne)指出,这可能是由于苹果公司既“没有碰撞内部版本号”,也没有“将 #iamroot bug的补丁放到10.13.1中:
用户还报告说,在应用补丁后,需要重新启动!
我的好朋友托马斯·里德(@thomasareed)发表了一篇很好的文章,全面总结了苹果公司在这个补丁上的失误。
结论
那么,这是一个总结!在本博客中,为了在Apple发布补丁之前揭示了时下臭名昭著的#iamroot bug的根本原因,我们逆向了“opendirectory”守护进程的各种组件!
我们确定苹果公司忘记检查一个必要的变量(保存有一个账户验证结果)的值。
一旦一个补丁被发布,我们将会逆向它,以确认我们的发现是正确的。万岁!
本文由看雪翻译小组 jasonk龙莲 编译,来源objective-see
转载请注明来自看雪社区
热门阅读
点击阅读原文/read,
更多干货等着你~