Today, Adobe Acrobat Reader DC for macOS patched three critical vulnerabilities(CVE-2020-9615, CVE-2020-9614, CVE-2020-9613) I reported. The only requirement needed to trigger the vulnerabilities is that Adobe Acrobat Reader DC has been installed. A normal user on macOS(with SIP enabled) can locally exploit this vulnerabilities chain to elevate privilege to the ROOT without a user being aware. In this blog, I will analyze the details of vulnerabilities and show how to exploit them.
The root process has superpowers, it almost can do anything, reading/writing all sensitive files/databases such as Images/Calendars. However in modern macOS, root processes outside of sandbox are rare, most macOS built-in services run within a sandbox. They are no longer the king, they imprison themselves in a cage based on declarative sandbox profile rules.
Good news, popular software with high privileged services are new good target in addition to macOS built-in services, so Adobe Acrobat Reader DC catch my attention.
com.adobe.ARMDC.SMJobBlessHelper within /Library/PrivilegedHelperTools/ is one of the components of Adobe Acrobat Reader DC, responsible for software updating. It runs as root and no-sandbox are applied, and hosts an XPC service named SMJobBlessHelper(com.adobe.ARMDC.SMJobBlessHelper). Most XPC services will check its connection client before doing any actual work, so does SMJobBlessHelper.
SMJobBlessHelper is based on NSXPC, its client checking exists in [SMJobBlessHelper listener:shouldAcceptNewConnection:]. The checking logic is as pseudo-code shows below, gets the client’s PID, and then obtains Bundle ID based on the client’s process path, the client will be trusted if its Bundle ID is “com.adobe.ARMDC”.
1 | pid = [NSXPCConnection processIdentifier]; |
But what is NSBundle, can we trust it?
Apple says it is “A representation of the code and resources stored in a bundle directory on disk.”, so it’s just a directory structure with some well-defined subdirectories/files. The bundle ID is obtained from Contents/Info.plist of the directory structure.
The directory structure is certainly not credible, we can forge any Bundle ID by creating our special bundle directory.
As the pseudo below show, in the updating process before SMJobBlessHelper launch ARMDCHammer, download folder(in bundle’s parent directory) will be moved to /var/folders/zz/xxxxx/T/. Unfortunately after directory moving, the owner of “/var/folders/zz/xxxxx_n0000000000000/T/download” is the root, and normal user DO NOT have access to it. So it means that we can not change it and its subfiles any more.
1 |
|
But, the designer may forget the symlink.
If ./download/ARMDCHammer is a symlink, after being moved to /var/folders/zz/xxxxx/T/download, does the symlink still be valid?
Yes, the symlink is still valid, it can help us to bypass temp directory protection. I can force /var/folders/zz/xxxxx/T/download/ARMDCHammer to link to anywhere.
With the help of vulnerability 2, we can force validateBinary() to check /tmp/test/hello_root.
The logic exists in [SMJobBlessHelper doWork].
1 | if (validateBinary("/tmp/test/hello_root")){ |
validateBinary and launchARMHammer all use program path, and we have writing permission to this path.
So if we can replace the “/tmp/test/hello_root” with our malicious file after validateBinary, launchARMHammer will launch our malicious process.
You may think the race condition window is too narrow to control, I will show the tricks later.
As explained before, NSBundle is not trusted, so we try to forge an NSBundle, with its bundle id is “com.adobe.ARMDC”. For saving time, we copy Adobe’s original bundle from “/Library/Application Support/Adobe/ARMDC/Application/Adobe Acrobat Updater.app”.
1 | echo "copy Adobe Acrobat Updater.app" |
Then compile our NSXPC Exploit client, copy it to Adobe Acrobat Updater.app/Contents/MacOS/
1 | cd /tmp/test/exploit |
Now, SMJobBlessHelper-Exploit, being launched as an NSXPC client, will pass through [SMJobBlessHelper listener:shouldAcceptNewConnection:]’s check.
1 | DoWorkAndLauchHammer(){ |
Symlink can help us, before SMJobBlessHelper moves our download directory, we create a symlink at our download directory.
1 | $ cd /tmp/test |
Then we trigger SMJobBlessHelper’s XPC interface, /tmp/test/exploit/download is moved to /var/folders/zz/xxxxx/T/download.
Now, we can see symlink in /tmp/test/exploit/download/ is still pointing to /tmp/test/hello_root.
1 | $ sudo ls -l /var/folders/zz/xxxxx/T/download |
So, with the help of symlink we change checking logic:
1 | DoWorkAndLauchHammer(){ |
validateBinary use built-in codesign command to check if /var/folders/zz/xxxxx/T/download/ARMDCHammer is valid or not.
1 | validateBinary("/var/folders/zz/xxxxx/T/download/ARMDCHammer"); |
The parameters passed to codesign are as below:
1 | (lldb) po $rcx |
Where can we find the valid ARMDCHammer?
I write a script, which searches the full local disk for ARMDCHammer, and finally gain nothing. But it must exist, isn’t it?
Since it is not on the local drive, it should have been downloaded on demand. I reverse a lot of binary files and found the cute download URL in Acrobat Update Helper.app. Downloading and extracting the archive, in the end, I catch the ARMDCHammer I’m looking for.
1 | $ codesign --verbose --verify -R="identifier ARMDCHammer and anchor trusted and anchor apple generic and certificate leaf[subject.CN] = \"Developer ID Application: Adobe Systems, Inc. (JQ525L2MZD)\"" ~/Downloads/ARMDCContents2/ASSET/Contents/MacOS/ARMDCHammer |
The time window between validateBinary and launchARMHammer is narrow. OPLock can help us to freeze time in Windows, unfortunately, there are no alternatives like that in macOS.
1 | DoWorkAndLauchHammer(){ |
We split our works into three parts, each part uses a separate thread.
Thread 1: Circularly replace files frequently
Symlink in /var/folders/zz/xxxxx/T/download point to /tmp/test/hello_root, so we replace it circularly.
1 | Step 1: move /tmp/test/ARMDCHammer to /tmp/test/hello_root, |
Thread 2: Prepare download directory and symlink frequently
1 | Step 1: create /tmp/test/orig_download directory, create symlink /tmp/test/orig_download/ARMDCHammer pointing to /tmp/test/hello_root |
Thread 3: Trigger NSXPC DoWorkAndLauchHammer interface
1 |
|
The reason we need Thread 1 is obvious, why we need separate Thread 2 and Thread 3?
High-frequency NSXPC interface call requests which make the server busy can increase the probability of success. With multi-threads running, the race condition needs a very short time. In my test, most test cases need only 1~3 seconds, and the best case only takes a blink of time.
(Safari may not play the gif automatically, recommend to use Chrome or Firefox)
The most important part of the vulnerability patch is adding a new function named -[SMJobBlessHelper validatePaths], before validateBinary and launch, it checks the path is a symlink or not. It breaks the only way which must be passed.
1 | bool -[SMJobBlessHelper validatePaths](path){ |
In this blog, I analyzed the three logic vulnerabilities in Adobe Acrobat Reader and show how to exploit them to gain root without sandbox limitation. As an almost per-device required software, its security matters to macOS.
Ping me(@yuebinsun2020) if you have any questions.
Thanks to R3dF09(@R3dF09) for help in the analysis.
]]>COVID-19 outbreak keep me from going out,I have been researching macOS’s Security framework in the past two weeks of homeworking.
In this blog, I will try to analyze Security framework, especially Keychain, and previous vulnerabilities of the Secuirty framework。
Security framework is responsible for providing authentication and authorization, secure data storage and transportation, code signing, encryption/decryption services. Apps can use this services by using API of Security framework directly without knowing or caring about its implementation details.
But what are the components which composes the Security framework, and how the components collaborate with each other?
Unfortunately the official document site did not updating the architecture diagram of this framework from macOS 10.7, but I find a outdated diagram in the book 《Mac OS X Internals》, It can still be used as a reference, Not too much has changed from that time.
Keychain play a significant role in the Security framework, passwords of WiFi and passwords of Safari autofilling are all saved and managed by Keychain.
Keychain was first introduced in Mac OS X 8.6, and was used for storing login credentials of the mail servers for PowerTalk mail system. Today, it improve a lot, many new data types are supported including many kinds of passwords, encryption keys, certificates and private notes. PowerTalk is no longer exists, but Keychain’s clients is replaced by many builtin Apps and thirdparty Apps.
Keychains in iOS and macOS are slightly different.
In iOS, there is only a single Keychain, it can be accessed when the device is unlocked, otherwise it will be locked too.
In macOS, users are allowed to create any number of Keychains for private use, Security framework provide SecKeychain{Create, Delete, Open, …} APIs for macOS users, with this API users can create, delete, open Keychain.
By default, two Keychains are already exist in macOS:
The login Keychain will be unlocked when user login macOS. System keychain, in contrast, is locked and encrypted, its decryption key is stored in /var/db/SystemKey, only root process can access it.
Apple offers Keychain Access.app to common users to view/search/create Keychains and Keychain Items, obtaining sensitive data, such as passwords, will trigger a authentication dialog.
Example code of Apple Documentation as below shows how to store a new website password to Keychain.
1 | static let server = "www.example.com" |
The most important is SecItemAdd API, we will analyze this API step by step to see how it implements.
In the abstract, data stored in query param will deliver to Keychain Service over SecItemAdd API, service will package the data as an Keychain Item, password in query will be encrypted, and the Keychain Item will continue to be written to Keychain database at disk.
From a components perspective, SecItemAdd API is implemented by Security shared library(/System/Library/Frameworks/Security.framework/Versions/A/Security), Security shared library will be loaded into the current App process. After SecItemAdd of Security shared library are called, the query data will be forwarded to XPC Service(com.apple.securityd.xpc) over SECURITYD_XPC macro, this XPC service are hosted by secd process(/usr/libexec/secd), secd run as current user.
When data is send to secd process, according to operation, securityd_xpc_dictionary_handler(simplified for better reading) dispatch message to different internal functions. In our case, query param is passed on to _SecItemAdd directly, and there is also another important param, client of SecurityClient struct, SecurityClient is responsible for identifying client process, subsequently ACL checking is based on this struct. In addition, accessGroups field of SecurityClient is used for implementing the Shared Web Credentials(share credentials between iOS apps and their website counterparts). Apps and Web use Associated Domains Entitlement to create association, more details can read Supporting Associated Domains in Your App
1 | static void securityd_xpc_dictionary_handler(const xpc_connection_t connection, xpc_object_t event) { |
In _SecItemAdd, query data will be translated to Sqlite3 sql statement, and in the end data will be inserted into sqlite3 database. What needs to be pointed out is that password will be encryptd before insert into database, and other non-secret fields will keep plain, this plain fields provide Keychain item searching support.
So far, inserting new website password to Keychain based on SecItemAdd API finished. The newly inserted Keychain item is save to Login Keychain, Login Keychain will be locked when user logout or machine poweroff.
1 | static CFStringRef SecDbItemCopyInsertSQL(SecDbItemRef item, bool(^use_attr)(const SecDbAttr *attr)) { |
Login Keychain is unlocked when user unlock the device, so wo do not see Keychain decryption or unlocking in the previous inserting keychain item.
However System Keychain or private Keychain(user created) will need Keychain encryption/decryption, locking/unlocing, Security Server is responsible for that.
Security Server(/usr/sbin/securityd) is a daemon service process which run as root, as the architecture diagram shows, Security Server offsers CSP/DL plugin for CDSA, namely data encryption and storage.
Security Server host service based on ucsp MIG interface, clients can access the internal Server object through mig interface. Fortunately any process can use this ucsp MIG interface.
By reading the source code, I see many features provided by Security Server:
It is obvious that Security Server(securityd) has many highly privileged operations, and it manages a lot of sensitive data, run as root, so if we can find a vulnerability in this service process, it will have a big impact.
KeySteal is such a vulnerability occurred in securityd, when successfully exploited, any process can access passwords hold by Keychain.
But how can we interact with Security Server(securityd) based on MIG interface?
The ucsp MIG definition file exists in the source code of Security framework(OSX/libsecurityd/mig/ucsp.defs). Unfortunately only a few documentations about MIG can be found, and none of them can be found about interacting with Security Server certainly. In the end, I extract the code snippet which meet our needs from Linus Henze’s KeySteal Exploit, based on the snippet, I create a simple ucsp client accessing Security Server’s ucsp_server_setup interface.
1 |
|
As mentioned above, Security Server is also responsible for managing authentication and authroization.
When client request Security Server to launch authentication/authroization verifying, if Security Server need to interact with the user (enter password) to verify identity, Security Server daemon will talk to Security Agent through XPC communication. The Security Agent, run as current user, pop up the interactive dialog to user, and then passwords typed by user will send back to Security Server, Security Server processing the actual verification. Throughout all the verification process, client DO NOT touch any sensitive information(such as passwords), all it can get is the verification result, true or false, yes or not. This mechanism can avoid the leakage of sensitive information and at the same time it will be transparent to the client when system add new authentication extension.
After we have learned some of the necessary Security framework architecture content above, let’s move on to the Security framework vulnerabilities which occured in macOS 10.14.*, and try to understand how the vulnerability works and what components the vulnerability is in.
What needs to be declared in advance is that Apple do not provide any details about the patched vulnerabilities except short title. The following are my own analysis based on the source code diff, so it also means that the results may not be entirely correct. If you find any mistakes or omissions, please don’t hesitate to point them out.
By comparing the two versions of the source code, vulerability patch of CVE-2019-8604 can be found.
This vulnerablity exists in Security Server Daemon(securityd), when Security Server deal with Keychain database name, ucsp_server_setDbName of mig interface can set any length of dbname to Security Server object, ucsp_server_getDbName of interface will copy the given dbname to name parameter, the name parameter is designed to length PATH_MAX. So, if we pass a overlong dbname, ucsp_server_getDbName will trigger OOB write when memcpy.
1 | --- a/Security-58286.251.4/securityd/src/transition.cpp |
It is quite natural that the patch add a checking to length of dbname, to avode overlong dbname triggering oob memory copying. One more point to explain, both std::string and strlen can and only can be truncated by “\0”, so the way of setDbName and getDbName treating dbname is consistent.
By comparing the two versions of the source code, vulerability patch of CVE-2019-8520 can be found.
This vulnerablity exists also in Security Server Daemon(securityd), when Security Server deal with authroization or authentication, if it need to interact with the user (enter password) to verify identity, it will ask for Security Agent’s help, Security Agent finally pop up a diaglog to user.
The communication between Security Server and Security Agent is based on XPC, when receiving response from Security Agent, in xpcArrayToAuthItemSet, the data(AUTH_XPC_ITEM_VALUE) and the length(AUTH_XPC_ITEM_SENSITIVE_VALUE_LENGTH) are passed by two separate fields. If providing a small data and a big length, sensitiveLength bytes of data will be copied from data to dataCopy, if sensitiveLength exceeds the actual size of data. OOB read will occurs.
1 | --- a/Security-58286.240.4/securityd/src/agentquery.cpp |
The patch add a checking, to ensure that sensitiveLength is not greater than actual size of data.
By comparing the two versions of the source code, vulerability patch of CVE-2019-8526 can be found.
1 |
|
This is the KeySteal vulnerability that I have read paper before, vulnerablity exists in Security Server Daemon(securityd), Security Server provides a feature named Hosting Guest Code. Two problems can be seen from the vulnerability patch:
The first one exists in implementing Hosting Guest Code. When Security Server creeate SecCode object, it use SecCodeCreateWithPID API, this API identifies the client process by pid, so as the comment code in the patch says, there is a problem with PID Reuse. The way fix this problem is replacing SecCodeCreateWithPID with SecCodeCreateWithAuditToken, SecCodeCreateWithAuditToken API identifies the client process by audit token. Reasons and attack methods for pid reuse has already been fully explained in Samuel Groß’s 《Don’t Trust the PID!》
The second is the reference counting problem of mach port. CodeSigningHost::reset() calls destory() to forcibly release mach port. But the destroyed mach port may still be referenced by some data structures, and the mach port in the user-mode process itself is a Mach Port Name, it’s just a number. Since it’s number, there’s a possibility of reuse, so, UAF can be implemented if we can reoccupy it before the next use. It’s easy to fix the problem, replace destory() with a reference-counted version of deallocate().
The vulnerability is described in Apple bulletin that denies service when processing S/MIME messages. Through code comparison, I got a suspected patch, and I am not sure.
1 | --- a/Security-58286.200.222/OSX/libsecurity_smime/lib/smimeutil.c |
I am not very familiar with certificate management and related data structures for the time being, so I will not analyze further.
The above are some of the identified vulnerabilities and patches that I have found so far. Because Apple open source code lags behind version updates, this vulnerabilities exists in 10.14.*.
This blog is what I researched and did during this time about Security Framework. The source code of Security Framework is huge and I only focused on Keychain and Security Server components that have been found to have more vulnerabilities in history。Other components like Auth will be analyzed when I have time, and, of course, I will continue to share with community through this site.
If you find something wrong above, or you are also interested in macOS, welcome to ping me @yuebinsun2020.
[1] Documentation of Security Framework
https://developer.apple.com/documentation/security?language=objc
[2] Apple Open Source Code
[3] KeySteal Vulnerability
https://www.pinauten.de/resources/KeySteal_OBTS_2019.pdf
[4] Keychain Wikipedia
]]>新冠病毒疫情出不了门,在家办公这两周笔者研究了一下 macOS 的 Security Framework。
本文主要分析 Security Framework 尤其是其中 Keychain 的架构,将 Security Framework 近一两年的历史漏洞做个整理。
Security Framework 主要负责为 App 提供认证与授权、安全数据存储与传输(Keychain,App Transport Security)、代码签名、加密解密功能。
第三方 App 通过引用 Security Framework,使用 Apple 提供的 API 就可以直接使用这些功能,不用关心底层实现的细节。
但 Security Framework 都有哪些组件,又是如何构建起来的呢?
官方最近已经不再更新整体的架构图了,在 [Mac OS X Internals] 书里找到了一张整体架构图,目前来看重要组件的变化不是特别大,可以用来参考
Keychain 是 Security Framework 的重要组件,系统中保存的 WiFi 密码、Safari 保存的网站密码等都由 Keychain 组件负责管理。
Keychain 最早在 Mac OS 8.6 版本被引入,用于保存邮件系统(PowerTalk)的邮件服务器的登录凭据。现在的 Keychain 组件已经扩展了很多,可用于保存密码、加密密钥、证书以及 Notes,被 Apple 自身以及众多第三方应用使用。
iOS 与 macOS 系统中的 Keychain 略微有些差异,iOS 中只有一个 Keychain,设备解锁状态时 Keychain 可以访问,设备锁定状态时 Keychain 也处于锁定状态。macOS 则不同,macOS 系统允许用户自己创建任意的 Keychain 用于私有使用,Security Framework 提供了 SecKeychain{Create, Delete, Open,…} API 用于 macOS 用户管理 Keychain。
默认状态下,macOS 系统中存在两个 Keychain:
其中 login Keychain 在 macOS 解锁状态时就会被解密,System.keychain 密钥保存在 /var/db/SystemKey,只有 root 用户可以访问。
具体目前系统中保存的 Keychain 以及存储的信息列表可以通过 macOS 的 Keychain Access.app 应用访问并查看。
Apple 官网文档如下示例代码可以实现向 Kaychain 中存储一个网站的密码。
1 | static let server = "www.example.com" |
其中核心的就是 SecItemAdd 这个 API,接下来我们将一步步分析这个 API 是如何实现的。
抽象的看,保存在 query 变量中的数据通过 SecItemAdd API 传递给 Keychain Service,服务进一步会将 query 数据封装为 Keychain Item,对于其中的 password 则会被加密,Keychain Item 进一步会被保存到磁盘的 Keychain Database。
如果从组件的角度看,SecItemAdd API 由 Security 共享库(Security Framework 的一部分,此处为了与 Security Framework 作区分所以叫共享库,/System/Library/Frameworks/Security.framework/Versions/A/Security)实现,Security 共享库会被加载进当前 App 进程,SecItemAdd API 收到数据后,进一步通过 SECURITYD_XPC 宏,将 API 调用转发至 com.apple.securityd.xpc XPC 服务,该服务位于 secd 进程,secd 以当前用户身份运行。
进入 secd 进程之后,会根据 operation 进入到服务消息分发 handler(securityd_xpc_dictionary_handler)(代码已被精简),对于 SecItemAdd,operation 为 sec_item_add_id,保存新增数据的 query 会被直接传递给 _SecItemAdd,除了 query 还有重要的数据结构 SecurityClient 结构体,SecurityClient 用于在后续的数据处理流程中支持访问控制检查,其中的 accessGroups 用于实现在 Web(Safari)和同一个团队开发的 App 之间共享密码,核心就是 Web 与 App 通过 Associated Domains Entitlement 关联,感兴趣可以参考 Supporting Associated Domains in Your App
1 | static void securityd_xpc_dictionary_handler(const xpc_connection_t connection, xpc_object_t event) { |
_SecItemAdd 内部就会将 query 数据转化为 Sqlite 的数据库增、删、改、查操作,最终实现对我们传递 query 的 item 插入操作。插入 sqlite3 的数据,password 会被加密。同时为了支持搜索,其他一些非私密数据会保持明文,这样可以支持对 keychain 数据库条目的搜索。至此 SecItemAdd API 新增网站密码的流程就结束了。
1 | static CFStringRef SecDbItemCopyInsertSQL(SecDbItemRef item, bool(^use_attr)(const SecDbAttr *attr)) { |
Safari 保存的这部分网站密码会被保存到 login keychain 数据库中,login keychain 等用户注销或者关机等操作时会被加密锁定。
系统的 login Keychain 在系统处于解锁状态时就会自动解锁,所以上面保存网站密码时并没有涉及 keychain 的解密或解锁过程。
然而对于 System Keychain 或者时自己创建的 Keychain,这就涉及到 Keychain 数据库的加解锁、加解密处理,此时就需要 Security Server 的参与。
Security Server(/usr/sbin/securityd) 是一个 root 身份独立运行的 daemon 服务进程,如最上面的整体架构图所示,CDSA 架构中,Security Server 为 CDSA 架构提供了 CSP/DL Plugin,即负责数据的安全加密与存储。
Security Server 通过 ucsp MIG 接口提供服务,用于 client 访问 SecurityServer 内部对象。普通用户进程就可以访问此 MIG 接口。从源码中看这个服务提供了以下功能:
可以看出 root 身份运行的 Security Server(securityd) 提供了很多高权限的敏感操作,同时也管理着大量敏感数据,因此如果可以发现这个服务进程的漏洞,那么影响也将非常大,KeySteal 就是利用该服务的漏洞实现无需密码验证访问 Keychain 保存的密码。
那么如何通过 MIG 接口与他交互呢?
在 Security 的源码中就包含了这个 ucsp MIG 接口的定义文件(OSX/libsecurityd/mig/ucsp.defs)。但很可惜,介绍 MIG 使用的文档很少,直接访问 Security Server 的文档更是没有。最终,我从 Linus Henze 写的 KeySteal Exploit 代码中精简了一个访问 ucsp_server_setup 接口的 Client。
通过 mig 命令行工具生成 ucspUser.c 以及 ucspServer.c 接口定义源码,解决完编译依赖的头文件定义之后,就可以通过如下的示例测试代码访问 ucsp_server_setup 接口。
1 |
|
上面的介绍中提到,Security Server 还负责认证(Authentication)和授权(Authroization)。
当 Client 请求 Security Server 发起认证(Authentication)和授权(Authroization)验证时。如果需要与用户交互(输入密码)以验证身份,Security Server 就会通过 XPC 与 Security Agent(当前用户身份运行)通信,由 Security Agent 负责弹框与用户交互。用户输入的密码凭据信息由 Security Server 接收并管理,Client 只会收到验证或授权结果的消息。这个保证整个验证过程中 Client 不会接触密码等敏感信息,同时,这种机制也可以保证如果系统增加新的身份验证或鉴权扩展时,对 client 是透明的。
了解完了上面的一些必要的系统架构内容外,我们来继续看看 macOS 10.14 版本至今的涉及 Security 框架的漏洞,方便读者朋友了解漏洞的原理以及漏洞所在的组件。
需要说明的是,因为 Apple 官方在每次漏洞修复后并不会提供漏洞的详细信息,所以以下这些都是我根据源码自己分析整理的,这也意味着整理的结果可能不一定正确,如果您发现有错误或疏漏,请不吝指出。
通过对比两个版本之间的源码,发现 CVE-2019-8604 漏洞的补丁。
这个漏洞在 securityd(Security Server Daemon) 中,securityd 提供的 MIG 接口在处理 client 端传递的 dbname 时,只有 assert 检查,而 assert 在 Release 版本是不存在的,因此,client 传递一个超长的字符串(长度超过 PATH_MAX),ucsp_server_getDbName 接口就会触发 memcpy 内存越界拷贝。
1 | --- a/Security-58286.251.4/securityd/src/transition.cpp |
补丁中,在 ucsp_server_{get, set}DbName 中新增对路径名字的检查(checkPathLength),防止超长的 dbName 溢出固定长度(PATH_MAX)的 name。
因为 std::string 与 strlen 都会被且仅能被 “\0” 截断,所以 setDbName 与 getDbName 的处理方式就一致了。
通过对比两个版本之间的源码,发现了 CVE-2019-8520 漏洞的补丁。
该漏洞位于 Security Server Daemon(securityd) 中,securityd(root) 负责处理系统中的管理系统中的 Authroization 和 Authentication,认证或者授权过程中,如果需要与用户交互(输入密码)以验证身份,securityd 就会通过 XPC 与 Security Agent(当前用户身份运行)通信,由 Security Agent 负责弹框与用户交互。
这个漏洞就出现在 securityd 与 Security Agent 的交互过程,securityd 在接收来自 Security Agent 的数据时,通过 XPC 传入 data,data 的长度为 length,另外通过另一个字段传入 sensitivelength,拷贝的时候,从 data 的起始位置拷贝长度为 sensitivelength 的内容到新创建的 dataCopy,因此,如果传入一个超长的 sensitivelength,超过上面传入的 data 的实际长度,将导致 data 的越界拷贝,会越界读取 data 变量之后的内存。
1 | --- a/Security-58286.240.4/securityd/src/agentquery.cpp |
漏洞的修复逻辑就是加了一个对 sensitiveLength 的长度检查,保证 memcpy 的长度不超过 data。
通过比对代码,发现了补丁。
1 |
|
这个漏洞正是之前读过 Paper 的 KeySteal 漏洞,补丁代码位于 securityd(Security Server Daemon) ,securityd 在通过 MIG 实现 Hosting Guest Code 机制时存在问题。
从补丁中可以看出漏洞存在的两个问题:
第一个是实现 Hosting Guest Code 机制,securityd 在创建 SecCode 时,错误地使用 SecCodeCreateWithPID 这个 API,这个 API 根据 pid 标识 Client Process,因此如补丁中的注释代码所说,存在 PID Reuse 的问题。
修复的方式是 SecCodeCreateWithPID 换做 SecCodeCreateWithAuditToken 用 audit token 表示 client。关于 PID 方式有何问题,可以参考之前 Samuel Groß 的 《Don’t Trust the PID!》
第二个是 Mach Port 的引用计数问题,CodeSigningHost::reset() 调用 destory() 导致强制释放 Mach Port,被 destory 的 Mach Port 可能仍然被某些数据结构引用,同时因为用户态进程的 Mach Port 本身是 mach port name,其实就是个 number,既然是 number 就存在被 reuse 的可能。所以,在下次使用之前如果可以导致重新被占用,就可以实现 UAF。补丁修复也很容易,就是 destory 改为引用计数版本的 deallocate()。
这个漏洞 Apple 公告中的描述是处理 S/MIME 消息时拒绝服务,对比代码,得到的了疑似补丁,不敢完全确定
1 | --- a/Security-58286.200.222/OSX/libsecurity_smime/lib/smimeutil.c |
对证书管理及相关的数据结构暂时还不太熟悉,暂时不进一步分析了
上面这些是目前我找到的比较确定的一些漏洞及其补丁,因为 Apple 开源代码非常滞后,所以上面这些主要是 10.14.* 版本中涉及 Security Framework 的漏洞的分析。
以上就是我这段时间研究 Security Framework 并做的分享。因为 Security Framework 比较庞大,我只重点介绍了 Keychain 以及历史上被发现漏洞比较多的 Security Server 组件。其他像 Auth 组件来得及分析,等后续对这些组件有了新的研究,我将继续分享。
如果发现上面的内容有错误,或者您也对 macOS 感兴趣,欢迎联系我 @yuebinsun2020。
[1] Documentation of Security Framework
https://developer.apple.com/documentation/security?language=objc
[2] Apple Open Source Code
[3] KeySteal Vulnerability
https://www.pinauten.de/resources/KeySteal_OBTS_2019.pdf
[4] Keychain Wikipedia
]]>1 | (1648.f78): Access violation - code c0000005 (first chance) |
既然当前 Windows 版本(1607)可以触发崩溃,那我们就下载该版本对应的 ChakraCore 源码。
Windows 10 1607 的发布时间是 2016/08/02,我们从 GitHub ChakraCore 的 Roadmap 上找一个大概 8 月份的源码版本:
https://github.com/Microsoft/ChakraCore/releases/tag/v1.2.0.0
下载其中的 ChakraCore-binaries.zip 二进制文件和 Source code(zip) 源码。
下载后用 ChakraCore-binaries 中的 x64\ch.exe 直接运行 poc.js(去掉其中的 alert 等非 JS 引擎内置函数),测试效果,可以看到 ch.exe 也崩溃了,而且错误代码也是 0xc000005,这也说明:
所以我们完全可以不调试浏览器而只调试 ch.exe 来研究该漏洞,只调试 ch.exe 相对容易而且可以下载并编译源码调试。
从崩溃点的调用栈 OP_NewScObjArray_Impl 函数可以判断,崩溃发生在 OP_NewScObjArray OpCode 的处理过程中。所以很自然地就想到要为 OP_NewScObjArray 处理过程下断点跟踪调试。
Interpreter 模式执行时, Js::InterpreterStackFrame::INTERPRETERLOOPNAME() 内部,OpCode 被 Interpreter 逐一 Read 然后解释执行。
所以如果想为某个特定的 OpCode 下断点,可以先在 Js::InterpreterStackFrame::INTERPRETERLOOPNAME() 内部下断点
1 | 0:000> bp ch!runscript |
断点命中到 lib\runtime\language\interpreterloop.inl 的 Var Js::InterpreterStackFrame::INTERPRETERLOOPNAME()
单步执行,进入 Interpreter While 循环中的 OpCode Read 的过程
1 | INTERPRETER_OPCODE op = ReadByteOp<INTERPRETER_OPCODE>(ip); |
ReadByOp 的函数实现如下:
1 | template<> |
单步执行到 this->scriptContext->byteCodeHistogram[(int)op]++; 当前 c 代码对应的汇编代码为
1 | 0:004> p |
此时 op 已经被赋值为将要处理的 OpCode 值,为此行代码设置条件断点,拦截 NewScObjArray OpCode
注意:
条件断点中使用寄存器而不是本地变量的方式,原因是实测条件断点中使用本地变量 op 下断点不能触发。
由于本条指令执行时,ax 寄存器刚好存储的就是 OpCode 值,所以改为借助 ax 寄存器下断
1 | 0:004> bp . ".if ( @ax == 0n197){} .else {gc}" |
OpCode 各个分支代码都是通过宏生成的,需要一步步展开来看。
Js::InterpreterStackFrame::ReadByteOp
1 |
|
继续跟踪,新得到的 NewScObjArray OpCode 进入 switch case 分支,其具体的分支处理代码由宏实现
1 | // lib\runtime\language\interpreterhandler.inl |
上面 switch case 中, DEF3_WMS 的定义为
1 |
所以宏展开之后为
1 | PROCESS_CALL_COMMON(NewScObjArray, OP_NewScObjArray, CallI, _Medium) |
我们继续看 PROCESS_CALL_COMMON 的定义,也是个宏
1 |
PROCESS_READ_LAYOUT 的定义如下
1 |
继续展开
注意这里的展开用到了 ## 连接符,另外一个就是宏在参数替换的时候是以 token 为单位的,在下面的例子中,就是根据 ## 和标点符号将表达式划分成 token 列表,与参数列表项匹配的 token 将会被替换。
在这里我纠结了很久 m_reader.layout##suffix 到底替换不替换,以及 OpLayout##layout##suffix 为什么不写成 OpLayoutlayout##suffix,后来查资料,比较少的资料提到了替换的基本单元是 token,
也就是 token 内部的匹配项是不替换的。
譬如下面的参数中有 layout,而 token 中有 playout,此时不会将 playout 替换为 pCALLI。
参考: https://www.jianshu.com/p/f8cb57957c03
参考: https://zhuanlan.zhihu.com/p/26978356
全部展开之后的 case 分支为:
1 | case OpCode::NewScObjArray: \ |
ByteCodeReader::CallI_Medium 是通过宏的方式生成的,一步步的展开之后是:
1 |
|
跟进 CallI_Medium 调用,调用至 GetLayout
1 | 0:004> t |
GetLayout 函数的定义如下
1 | template<typename LayoutType> |
此时的 LayoutType 类型定义为: Js::OpLayoutT_CallI<Js::LayoutSizePolicy<1> >
1 | template <typename SizePolicy> |
LayoutType 的大小为 5 字节
1 | 0:004> dv layoutSize |
GetLayout 函数主要是从当前 ip 的位置读取 sizeof(LayoutType) 字节的数据作为 LayoutType 类型的 layoutData 返回,然后 ip 向后偏移 sizeof(LayoutType) 字节
参考上面的 Case OpNewScObj 分支,从 GetLayout 返回之后,进入 OP_NewScObjArray(),OP_NewScObjArray() 继续调用其具体实现 OP_NewScObjArray_Impl()
1 | template <class T> void OP_NewScObjArray(const unaligned T* playout) { OP_NewScObjArray_Impl<T, false>(playout); } |
OP_NewScObjArray_Impl 的 playout 即为通过 GetLayout 得到的 layoutData
在 OP_NewScObjArray_Impl() 内部,它会调用 ProfilingHelpers::ProfiledNewScObjArray() 以实现对 OP_NewScObjArray 的 Profile 支持。
特别需要注意的是它调用 ProfilingHelpers::ProfiledNewScObjArray() 时的参数,这里的 playout 将通过类型转换至 const unaligned OpLayoutDynamicProfile2
1 | SetReg( |
下面看一下 OpLayoutDynamicProfile2 结构体和之前的 Js::OpLayoutT_CallI 有何不同
OpLayoutDynamicProfile2 结构体的定义如下
1 | template <typename LayoutType> |
看来两个结构体之间是继承关系。
因为这里的 OpLayoutDynamicProfile2 继承了 LayoutType(Js::OpLayoutT_CallI<Js::LayoutSizePolicy<1> >)结构体,相当于扩充了结构体的大小,在尾部增加了两个结构体成员(uint16)
上面调试中,LayoutType 的大小为 5 字节,此时 OpLayoutDynamicProfile2 为 9 个字节。
而在 InterpreterStackFrame::OP_NewScObjArray_Impl,调用 ProfilingHelpers::ProfiledNewScObjArray 函数传入的参数正是类型转换之后的 profileId 和 profileId2。
我们继续动态跟踪一下调用时的状态及实际的参数
首先来看一下调用 ProfilingHelpers::ProfiledNewScObjArray 之前 playout 的内存状态
1 | 0:004> dt playout |
注意 playout 内存的前 5 个字节属于 LayoutType(Js::OpLayoutT_CallI<Js::LayoutSizePolicy<1> >),而后面的四个字节 0xfa 0xfe 0xe9 0x09 是 OpLayoutDynamicProfile2 自己派生的两个变量
这里就是问题所在了,我们在理清一下思路。
也就是说,本来我们通过 GetLayout 读取到的 playout 是一个 5 字节的结构体,但是此时类型转换之后, profileId(0xfefa) 和 profileId2(0x09e9) 被作为参数传入 ProfilingHelpers::ProfiledNewScObjArray
下面来跟踪证实一下我们的判断,看一下这两个参数的值
进入 ProfilingHelpers::ProfiledNewScObjArray 函数后,打印两个参数值,发现正是 0xfefa 和 0x09e9
1 | 0:004> |
我们继续跟踪,ProfilingHelpers::ProfiledNewScObjArray 会将 profileId(0xfefa)参数作为 callSiteId 实参传递给 RecordCallSiteInfo()
1 | profileInfo->RecordCallSiteInfo( |
1 | 0:004> |
而 callSiteId(0xfefa)在 RecordCallSiteInfo 内部会作为数组索引访问 this->callSiteInfo。那么此时 this->callSiteInfo 数组的真实长度是多少呢?
这就要看一下 this->callSiteInfo 的初始化过程了,this->callSiteInfo 的初始化是在 DynamicProfileInfo::New()
1 | DynamicProfileInfo* DynamicProfileInfo::New(Recycler* recycler, FunctionBody* functionBody, bool persistsAcrossScriptContexts) |
可以看到 callSiteInfo 数组的长度是通过 functionBody->GetProfiledCallSiteCount() 获得的,也就是 FunctionBody 对象的 profiledCallSiteCount 成员变量的值。
1 | ProfileId GetProfiledCallSiteCount() const { return this->profiledCallSiteCount; } |
那么此时在内存中 callSiteInfo 数组的真实长度是多少呢?也就是 functionBody 的 profiledCallSiteCount 成员的值。通过 functionBody 对象我们来看一下
1 | 0:004> dt functionBody profiledCallSiteCount |
callSiteInfo 数组的长度是 2
在 callSiteInfo 数组长度是 2 的情况下,竟然尝试以 0xfefa 索引访问它,所以当然是内存越界了。
我们继续通过调试器验证一下这个动态的过程
1 | // this->callSiteInfo 的地址为 0x0000024b`c0a100b8 |
实际调试验证最终崩溃点 if (!callSiteInfo[callSiteId].isPolymorphic) 地址是不是我们上一步自己计算的地址
1 | 0:004> p |
截至目前,我们可以总结一下漏洞的根本原因了
在处理 OP_NewScObjArray Opcode 时,通过 GetLayout 得到的 layoutData 本身是个 5 字节长度的 Js::OpLayoutT_CallI<Js::LayoutSizePolicy<1> > 类型,但是在 ProfilingHelpers::ProfiledNewScObjArray() 内部竟然被转换为 9 字节长度的 const unaligned OpLayoutDynamicProfile2
转换之后,越界访问得到的错误 profileId 被当作了 DynamicProfileInfo->callSiteInfo 的数组索引,造成内存越界访问异常。
总结起来,这是个类型混淆导致的越界访问漏洞。
本来希望可以通过 git log 的 -L 参数实现对函数的追踪,不过实际测试发现不起作用。
最后想到的方法是先通过 git log + grep 碰碰运气,然后手动测试
1 | test@test MINGW64 /k/edge/ChakraCore (master) |
发现只有一条 commit 涉及 OP_NewScObjArray_Impl,我们来看一下这个 commit
下面来分别编译补丁前后两个版本测试 poc 能否触发
从 master 分支回退到上面发现的补丁版本(8bd6826aea01ff1af36f2a83fe00c44799ba80cb),即打上疑似补丁的版本
1 | test@test MINGW64 /k/edge/ChakraCore (master) |
VS2017 编译,然后测试
1 | C:\Users\test>"K:\edge\ChakraCore\Build\VcBuild\bin\x64_debug\ch.exe" "K:\edge\OP_NewScObjArray_Type_Confusion\poc\pure_js_poc.js" |
发现没有崩溃,证明这个版本该漏洞已经被修复。
再来测试回退到补丁之前版本
1 | test@test MINGW64 /k/edge/ChakraCore ((8bd6826ae...)) |
VS2017 编译,然后测试
1 | C:\Users\test> |
发现崩溃了,说明这个 commit 8bd6826aea01ff1af36f2a83fe00c44799ba80cb 正是该漏洞的补丁
补丁代码中,OP_NewScObjArray OpCode 的处理函数 OP_NewScObjArray_Impl 不再实现具体的逻辑,改为调用 OP_NewScObject_Impl。
1 | diff --git a/lib/Runtime/Language/InterpreterStackFrame.h b/lib/Runtime/Language/InterpreterStackFrame.h |
1 | template <class T, bool Profiled, bool ICIndex> |
当进入到 OP_NewScObject_Impl 函数的时候,可以发现此时传入的 playout 内存的数据与补丁之前是一致的
1 | 0:004> p |
接下来,因为 OP_NewScObjArray_Impl 调用 OP_NewScObject_Impl 时传入的模板参数 Profiled 为 false。所以会调用 NewScObject_Helper 创建 newInstance 赋值给 playout->Return 寄存器。
注意在这个 OP_NewScObjArray 的处理过程中,已经不像补丁之前的代码存在类型转换,所以漏洞也就不再存在了。
1 | template <class T, bool Profiled> |
总结一下补丁,补丁后的版本,OP_NewScObjArray_Impl 仅仅是 OP_NewScObject_Impl 的封装,不再有之前复杂的判断和逻辑,不再有类型转换,所以漏洞也就被修复了。
GeekPwn 2016 比赛中有一道 Windows 服务漏洞提权题目,该服务程序会创建命名管道(Named Pipe)服务端接收客户端发送的文件路径然后调用 LoadLibrary 加载,但加载之前有一系列的检查过程,我们的最终目标是绕过这些检查,加载我们指定的 DLL并以 SYSTEM 权限执行任意代码。本文笔者逐一分析该服务的各个验证环节及其绕过方法,以及如何组合他们最终启动 SYSTEM 权限的计算器。
服务端的处理逻辑:
服务端创建命名管道,等待客户端的连接。
客户端连接之后,服务端OpenProcess 打开客户端进程句柄,获得客户端进程 Image 文件路径。
验证客户端 Image 文件路径的签名。
签名验证通过之后,创建 Event 事件对象,无限等待 Event 对象直到 Signaled 状态。
通过管道接受客户端发送的 DLL 文件路径。
验证 DLL 文件路径的签名,签名通过之后调用 LoadLibrary 加载 DLL。
LoadLibrary 是我们的目标,但是在抵达目标之前,服务程序中有多层障碍需要绕过:
管道创建时,指定的 DACL 字符串是 “D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRWD;;;WD)”,普通用户没有向管道发送数据的权限。
1 | c:\project\geekpwn_2016>accesschk.exe -v \pipe\GeekPwn2016 |
服务端接收客户端的连接请求之后,调用 OpenProcess 获得客户端进程句柄,然后得到客户端进程 Image 路径,之后会调用 WinVerifyTrust 验证签名。注意有些 PE 文件通过附属 Manifest 文件提供签名信息,而 WinVerifyTrust 验证签名时对这类文件会视为无效。只有签名验证通过之后才会继续下一步。
服务端创建的 “Global\GeekPwn2016” Event 对象,客户端作为普通权限进程是没有权限调用 SetEvent 置为 Signaled 状态的。
客户端通过管道发送的 DLL 路径会传递给 WinVerifyTrust 验证签名,验证通过后才会传递给 LoadLibrary 加载。
前面 accesschk 对管道权限的枚举结果可以发现,普通用户虽然没有 WRITE_DATA 权限,但也有不少权限,挨个查 MSDN,发现 WRITE_DAC 可以帮我们重新改写对象的 DACL 列表。
利用 WRITE_DAC 给 Everyone 组用户添加 WRITE_DATA 权限,网上可以找到代码片段,需要注意的是,调用 SetSecurityDescriptorDacl 之后,只是生成了一个新的满足需求的 SecurityDescriptor 对象,管道对象的 DACL 并没有被修改,需要进一步调用 SetKernelObjectSecurity 并传递 pipe handle 才能真正在管道对象中生效。
DACL 修改之后,还有个地方需要注意,管道对象的 handle 需要关闭且以 WRITE_DATA 权限再次打开才行,因为用于修改 DACL 的 handle 无论如何没有 WRITE_DATA 权限。
服务端的检查代码如上图,先获得客户端进程 PE 文件路径,然后验证 PE 文件路径。所以如果我们可以找个有签名的程序,向其进程注入攻击代码就可以绕过上述检查了。
向有签名的程序注入代码有很多方法,最简单的是创建签名程序作为子进程,本例中笔者选用 SysInternals 工具集中的 procexp.exe,然后调用 WriteProcessMemory 和 CreateRemoteThread 注入代码。直接注入完整程序逻辑比较困难,所以我们注入的是一段加载用于加载 DLL 的 Shellcode,这段 Shellcode 运行时会加载同目录的 hello.dll,剩下的工作都在这个 hello.dll 中完成。
Hello.dll 中调用 CreateFile 和 CreateNamedPipeW 连接服务端管道时,服务端获得进程 PE 路径是 procexp.exe 的路径,WinVerifyTrust 验证签名有效。
服务端创建并且无限期等待 Event 的代码如上图,服务端创建的 Event 普通权限的客户端进程显然是没有权限修改状态的。连接返回失败,GetLastError 返回错误代码 5,提示 Access Denied。想到一个方法是,在服务端创建 Global\GeekPwn2016 Event 之前,抢先创建同名 Event,并设置该 Event 的初始状态为 Signaled。根据 MSDN 中对 CreateEvent 返回值的描述,之后服务端进程调用 CreateEvent 创建时会返回我们之前创建的这个 Event 的句柄。再之后的 WaitForSingleObject() 会直接以成功状态返回(返回值为0)。
服务端接收客户端发来的 DLL 路径后,验证过程如上图。还是调用 WinVerifyTrust 验证签名。这次怎么绕过签名检查呢?经过自己分析对比 LoadLibrary 和 WinVerifyTrust,发现二者在处理路径时存在标准不一致导致的漏洞。传递给 WinVerifyTrust 的路径,会当作全路径直接验证。而据 MSDN 介绍,LoadLibrary 似乎“灵活”很多,如果路径结尾没有扩展名,LoadLibrary 在真正的加载动作之前会自动补上一个 “.dll” 作为最终路径。
利用这个特性就可以绕过上面的检查了。假设我们传递的路径是 c:\test\evil。WinVerifyTrust 会直接验证 c:\test\evil。而 LoadLibrary 发现没有以 .dll 结尾,会自动追加,最终加载 c:\test\evil.dll。
注意,evil.dll 一定要编译成 64 位的,因为服务端主程序 GeekPwn2016.exe 是 64 位的。
成功迫使管道服务端加载 evil.dll 后启动 SYSTEM 权限计算器就很简单了,直接在 DLL Main 中调用 WinExec(“calc.exe”, SW_SHOW); 就好了,当然普通用户桌面中是看不到弹出计算器的,需要 procexp.exe 或者任务管理器查看。
本次的服务端程序中,大部分漏洞或利用技巧都是依赖 MSDN 上一些不太显眼的特性或 Tricks。如果平时能够多收集这样的点,势必对漏洞的发现和利用大有帮助。
附录中会贴出精简后的服务端代码,完整的攻击程序源码会单独以压缩包形式提供。
微软MSDN关于CreateEvent的文档: https://msdn.microsoft.com/zh-cn/library/windows/desktop/ms682396(v=vs.85).aspx
微软 MSDN 关于 LoadLibrary 的文档: https://msdn.microsoft.com/zh-cn/library/windows/desktop/ms684175(v=vs.85).aspx
rekken
2017.07.23
]]>这份文档是对 James Forshaw 2017 年公开的 《Windows Logical EoP Workshop》 逻辑漏洞本地提权 Workshop 的分析调试笔记。
如果方便,建议生成一次系统快照,方便实验结束之后恢复系统环境
只有关闭了这个签名验证的保护,Windows 系统才允许加载用户自己编写的驱动。另外,这个特性是针对 64 位 Windows 8/10 的,如果用的是 32 位系统,可以忽略。
管理员权限运行:
1 | C:\Windows\system32>Bcdedit.exe -set TESTSIGNING ON |
1 | C:\WINDOWS\system32>Bcdedit.exe -set TESTSIGNING ON |
解决方法: 开机启动时,快速按 F2 进入 BIOS,选择 Boot 标签,将 Secure Boot 设置为 Disabled
1 | C:\Windows\system32>sc create workshop binPath= "C:\workshop\Driver\x86\LogicalEoPWorkshopDriver.sys" type= kernel start= demand |
注意每个参数 ‘=’ 的后面都有个空格,sc 命令的原型为:
1 | sc [servername] create Servicename [Optionname= Optionvalues] |
PowerShell 脚本执行策略从最严格到最宽松依次有几个级别:
执行策略的生效范围分为三个层次:
这里设置为最宽松策略,仅影响当前用户:
1 | C:\WINDOWS\system32>powershell -Command "Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Bypass" |
这几个策略需要重启系统才能完全生效
1 | C:\Windows\system32>sc start workshop |
利用 workshop\sandbox-attacksurface-analysis-tools\TokenViewer.exe 可以查看 Token 相关的各类安全属性信息,比如 Integrity Level
1 | PS C:\Users\test> Import-Module C:\workshop\sandbox-attacksurface-analysis-tools\NtObjectManager\NtObjectManager.psd1 |
1 | PS C:\Users\test> Import-Module C:\workshop\sandbox-attacksurface-analysis-tools\NtObjectManager\NtObjectManager.psd1 |
1 | PS C:\Users\test> Get-ChildItem -Recurse NtObject:\ | Where-Object -Property IsSymbolicLink | Format-List |
1 | PS C:\Users\test> Get-NtSymbolicLinkTarget \global??\UMDFCtrlDev-ee7ed3ad-29bf-11e7-a06e-000c29f028f7 |
1 | PS C:\Users\test> cd C:\workshop\sandbox-attacksurface-analysis-tools |
COM 是一种实现方式,应用程序可以通过 COM 接口调用目标组件提供的服务,COM 组件可以注册成进程内(In-Process) 和独立进程实现,相应的,当用户程序调用该接口时,系统会选择将 COM 的实现 DLL 加载进用户进程空间或者是选择拉起独立进程。
漏洞挖掘中,一般只关注独立进程实现的 COM 服务,因为这样的服务如果存在漏洞才有可能被用于沙箱逃逸,In-Process 类型的 COM,即便存在漏洞并成功触发,那也还是在当前进程空间(比如在沙箱进程内部),继承的是当前进程的权限。
服务与 COM 本身没有关系,服务是个独立的概念,通过 Windows Service Manager 管理的后台程序,该程序提供某些功能或者暴露一些接口,有些服务会暴露 COM 接口对外提供服务。
DCOM(COM) 在注册时会拥有一个唯一的标识符 AppID,用户程序可以通过 AppID 指定目标 COM,在比较新的 Windows 版本中,可以为 AppID 额外指定一个名字。
AppID 是谁生成的呢?AppID 本身是 GUID,由 Guidgen.exe 生成,在 COM 注册的时候,需要在注册表中写入这个以 GUID 命名的注册表键到 HKEY_LOCAL_MACHINE\SOFTWARE\Classes\AppID\ ,参考 MSDN AppID 和 What is AppID 以及 分布式组件对象Distribute Component Object Model(DCOM)的配置
打开如下工具,打开之后默认显示注册表的统计信息,包括 AppID 的计数、ProgID 的计数、各类服务 CLSID 的计数等等。
1 | C:\workshop\OleViewDotNet\OleViewDotNet.exe |
过滤出当前进程可以访问的 App
1 | 1. 菜单 -> Registry -> App IDs |
步骤基本同上,在 Apply 之后选择 Specific Process,选择 MicrosoftEdgeCP
在上面过滤出 Edge Content Process 进程可访问组件的基础上,克隆一份然后设置过滤器,过滤出 Service 类型
1 | mode -> Complex -> Apply -> Type = AppId, Field = IsService -> Add,添加过滤器,然后确定 |
此时会过滤出服务类型的 COM 组件
从 RpcServer.exe 打印的信息来看,测试 LoadLibrary 项时,输入 abc.dll,输出信息显示实际加载路径为 C:\Windows\abc.dll,所以,配合 ..\ 可以实现加载任意路径 DLL
1 | C:\Windows\system32>c:\workshop\ExploitTools\RpcServer.exe |
该漏洞所在函数的源码如下:
1 | extern "C" boolean TestLoadLibrary(handle_t hBinding, const wchar_t* name) |
前面几步同上,后面选择 3 Test Load Library with Path Check,继续输入 ..\ 拼接的 DLL 路径
1 | [RPC Tests] |
这次没有成功,RpcServer.exe 提示发现了路径分隔符,经过几次测试发现,RpcServer.exe 会检查输入的路径中是否含有 ‘/‘ 和 ‘' 分隔符,如果有则拒绝加载
1 | TestLoadLibraryCanonical called |
所以我们的目标就是,路径中不出现 ‘/‘和’',但是路径指向的文件又是可控的(在 Tasks、tracing 目录下创建的文件也不行)
在文档中,James Forshaw 给出的方法是 ADS(Alternate Data Steam),在 C:\Windows 目录下,找到可以写入 Data Stream 的路径写入 TestDll32.dll。
他找的的路径是 c:\windows\tracing,经过测试发现普通用户(非管理员)有权限向 c:\windows\tracing 目录写入 ADS
1 | c:\workshop\ExploitTools>CopyFile.exe TestDll32.dll c:\Windows\tracing:xyz.dll |
之后再次测试 Test Load Library with Path Check,路径输入 tracding:xyz.dll,成功!
比 c:\windows\ 下的其他目录多好几个权限
1 | c:\workshop\ExploitTools>..\sandbox-attacksurface-analysis-tools\CheckFileAccess.exe c:\Windows |
如果当前用户属于管理员组(Administrators),那么启动的 shell 进程是具有 limited token,比如访问 c:\windows\system32 会被拒绝,而管理员权限的 shell 进程具有 full token
对比测试 c:\Windows\tracing 和 c:\Windows\Tasks 两个目录
1 | c:\workshop\ExploitTools>CopyFile.exe TestDll32.dll c:\Windows\tracing:xyz.dll |
向 c:\Windows\Tasks 目录创建 ADS 失败了,
对比查看两个文件夹的 permissions
1 | \??\c:\Windows\Tasks\ : 001200AB ListDirectory, AddFile, ReadEa, Traverse, ReadAttributes, ReadControl, Synchronize |
Tasks 目录缺少的 Permission 为:AddSubDirectory、WriteEa、WriteAttributes
剩下这 3 个可疑的权限,如何判断是哪个生效的呢?
鼠标右键新建一个测试文件夹 c:\workshop\test_ads,默认情况下 test_ads 会继承 c:\ 的访问权限。当前用户具有对该文件夹的读/写/修改权限等大部分权限。
文件夹右键->属性->安全->高级->禁用继承->将已经继承的权限转换为当前文件的显式权限->此时就可以编辑各个权限了,针对某个具体的高级权限启用或者禁用
根据实际测试, AddSubDirectory、WriteEa、WriteAttributes 这三个权限,缺少哪个,都不能在 test_ads 文件夹中创建 ADS 和拷贝文件。这一点与 James Forshaw PPT 第 88 页的结果不太一致,他只强调了 AddSubDirectory 权限与 Alternate Data Steam 创建有关。
利用 Sysinternals 工具集中的 streams.exe 工具可以查看
1 | PS C:\Users\test> C:\workshop\Sysinternals\streams.exe C:\Windows\tracing |
目前还没有找到直接删除 ADS 的快捷方法,只看到有文章中提到可以通过 ren(重命名)的方式实现,相当于删除目录(或文件)之后再次创建。
1 | //RpcServer.c |
选择 TOCTOU LoadLibrary 测试项目之后,提示加载失败
1 | Specify library name: Tasks\xyz.dll |
服务端的输出日志提示是 Verify 失败
1 | Error verifying file: Module not in system directory |
查看源码, LoadLibrary 之前调用了系统 API WinVerifyTrust 检测了文件签名
1 | extern "C" boolean TestLoadLibraryTocTou(handle_t hBinding, const wchar_t* lib_path) |
通过分析上面的源码,如果我们可以在 VerifyEmbeddedSignature() 调用时利用符号链接指向系统文件 verified.dll,但是在调用 LoadLibrary 时再修改指向我们可控的文件 evil.dll,那就可以成功了,如果在验证签名之后有机制可以回调通知我们,那就可以精确获得符号链接的替换时机。
当文件名/目录/文件大小/属性/安全属性 发生变动时,可以通过 WaitForMultipleObjects 获得通知
参考 MSDN: FindFirstChangeNotification
这个的问题在于,通知只会在文件变动的时候触发,而 WinVerifyTrust 并不会触发。
上面我一直从’抢时间’的角度来找方法,但是在 WinVerifyTrust() 之后到 LoadLibrary 的时间差很短,上面提到的 Access Callback 也没有找到,所以这个方法比较困难。
回去看了一下 James Forshaw 给的文档,他的利用方法很有意思,他利用的是系统 API WinVerifyTrust 和 LoadLibrary 处理文件名时的差异。
WinVerifyTrust() 接受的就是用户提供的路径,不会做任何处理,而 LoadLibrary 不一样,如果用户指定的路径不是以 dll 结尾,LoadLibrary 会尝试 ‘补’上一个 “.dll”。发现这个差异,利用方法也就有了。
注意 MSDN LoadLibrary 中的这段文档,如果路径不是以 “.dll” 结尾,自动追加上 “.dll”。另外,如果不希望 LoadLibrary 自动追加 “.dll”,那在提供的路径后面追加一个 “.”,这个特性说不定什么时候也能被利用。
1 | If the string specifies a module name without a path and the file name extension is omitted, the function appends the default library extension .dll to the module name. To prevent the function from appending .dll to the module name, include a trailing point character (.) in the module name string. |
既然有了 LoadLibrary 这个比较 “周到” 的特性,那利用方法也就有了。
拷贝携带有效签名的 kernel32.dll 至我们的文件夹 c:\workshop\test\,重命名为 abc,即 c:\workshop\test\abc
1 | c:\workshop\ExploitTools>CopyFile.exe c:\Windows\System32\kernel32.dll c:\workshop\test\abc |
拷贝我们的 evil.dll 至 c:\workshop\test\,重命名为 abc.dll,即 c:\workshop\test\abc.dll
1 | c:\workshop\ExploitTools>CopyFile.exe c:\workshop\ExploitTools\TestDll32.dll c:\workshop\test\abc.dll |
DemoClient.exe 输入 c:\workshop\test\abc
1 | [RPC Tests] |
RpcServer 弹框提示成功
1 | Hello From Process 1712 |
这个 Case 是上一个的加强版,再次输入上面的 c:\workshop\test\abc,RpcServer 端提示错误:
1 | Extension is: |
看来是添加了对扩展名的检查
1 | extern "C" boolean TestLoadLibraryTocTouHardened(handle_t hBinding, const wchar_t* lib_path) |
源码中除了有后缀名 “.dll” 的检查,还有对文件路径 “C:\Windows\System32” 的检查
CreateFile 之后再通过 handle 进一步检查,可以保证 CheckFileIsInSystem 中访问的文件与 CreateFile 中的参数是同一个文件。
这个 Case 利用的利用方法是,使最终 LoadLibrary 的文件与前面检查的不是一个文件。利用到的技术有两个:OpLock 机会锁与符号连接。
创建指向 c:\Windows\System32 的符号链接
1 | c:\workshop\ExploitTools>mklink /J c:\workshop\test c:\windows\system32 |
启动对目标文件的 OpLock,监控文件的读写行为,发生读写行为时会触发阻塞式的回调
1 | c:\workshop\ExploitTools>start ..\symboliclink-testing-tools\SetOpLock.exe c:\Windows\System32\tapi32.dll x |
在 DemoClient 中输入 tapi32.dll 的新路径(也可以为其他文件名,要求 system32 目录下存在)
1 | [RPC Tests] |
此时 RpcServer 中的 CreateFile 会触发 SetOpLock.exe 的监控,由于 RpcServer 中只有一次文件打开动作,后面都是通过 handle 来处理,所以处理的是 c:\windows\system32\tapi32.dll 的 handle。所以此时我们替换了 test 文件夹的符号链接目标,不再链接至 c:\Windows\System32。删除旧 test 文件夹,新建 test 文件夹,并且在其中拷贝一份我们的 evil.dll(修改文件名为 tapi32.dll)。
1 | c:\workshop\ExploitTools> rmdir c:\workshop\test |
在 SetOpLock.exe 标准输入中,输入回车。
成功弹框提权后的 Hello 问候。
OpLock 是冲突锁,当出现对文件操作的冲突时,就会触发 OpLock。
OpLock 本身是用于缓存文件加速网络访问效率而实现的。客户端程序通过 DeviceIoControl 与内核中的 OpLock 驱动模块交互。
客户端应用可以利用这个特性实现对文件的监控,尤其是 Io Code 为 FSCTL_REQUEST_OPLOCK_LEVEL_1 时。
参考 MSDN FSCTL_REQUEST_OPLOCK_LEVEL_1
在本例中,客户端对 tapi32.dll 注册 OpLock 时,服务端 CreateFile 时就会阻塞,此时客户端得到通知,替换 test 目录,不再指向 c:\windows\system32,然后继续执行。
服务端得到 c:\windows\system32\tapi32.dll 的 handle,所以后面判断路径时有效。而 LoadLibrary 时再次使用的 lib_path,但 lib_path 保存的是 c:\workshop\test\tapi32.dll,加载的是我们自己的 Evil.dll。
想到一个与本 Case 无关的问题,CreateFile 成功获得文件 handle 之后,文件能被删除吗?
如果 CreateFile 时指定的 dwShareMode 包含 FILE_SHARE_DELETE,那其他进程就可以删除此文件,只不过删除之后,通过当前 handle 对文件的读写操作就会返回失败。
利用 OpLock 机制,Demo 7 的 TOCTOU Case 也有了新的解法。
1 | extern "C" boolean TestCreateProcess( |
上面代码中,服务端模拟 ALPC 客户端的身份,启动了 notepad.exe 进程。
我们的目标就是绕过限制,启动自己的进程,甚至是启动高权限进程。
RpcImpersonateClient 可以为当前线程创建一个 Impersonation 的身份,使当前服务端线程拥有客户端的 impersonation token,之后当前线程在调用某些 API 时就会以 impersonation token 身份验证权限。
CreateProcess 用于创建新进程,如果一个具有 impersonation token 的进程调用 CreateProcess 时,新进程会继承哪个 Token 呢? server primary token 还是 impersonation token?
可惜,是 server primary token。不过系统也提供了用于指定用户的 CreateProcess 版本 - CreateProcessAsUser。CreateProcessAsUser 可以通过指定 Token,代表用某个用户的身份创建进程。
1 | BOOL WINAPI CreateProcessAsUser( |
所以 RpcImpersonateClient 应该和 CreateProcessAsUser 配套使用,如果和 CreateProcess 一起使用,就容易出现身份不一致的问题。
在本 Case 中,正好可以验证一下,管理员权限打开 Sysinternals 工具集中的 Process Explorer,重复本 Case 中的测试过程,管理员权限打开 RpcServer.exe,普通用户权限打开 DemoClient->Rpc Client Test->ALPC->Test Create Process。
在 Process Explorer 选择展示 Intergrity Level 和 User 列,然后对比 RpcServer、DemoClient、notepad,发现 notepad 进程的启动用户和 Integrity Level 与 RpcServer 完全一致,Intergrity Level 均为 High,而 DemoClient 的 Intergrity Level 为 Medium。
参考:
有了这个问题,看来权限问题就不用解决了,剩下的就看如何来让 CreateProcess 创建的是我们可控的程序,直接替换 c:\system32\notepad.exe 文件肯定是不行的,权限不够,与 Impersonation 结合呢?
对象管理器中,\DosDevices 下面保存着驱动创建的有名字的设备对象(方便暴露给用户态?)。为了隔离不同的用户会话,对象管理器对其中的 \DosDevices、\Windows、\BaseNamedObjects 隔离实例化,有一份全局的,然后每个用户也可以有一份属于自己的,并且自己的那份会 shadow 掉全局的,所以对这几个命名空间的修改只会影响自己。
\GLOBAL??\ 是指向全局 \DosDevices 的符号链接,\DosDevices 命名空间下保存着如 C:、COM1 等设备的符号链接,这些符号链接指向 \Devices 命名空间中的真实设备
??\ 比较特殊,它是个前缀,不是实际的命名空间。当对象管理器发现应用传递的路径以 ?? 为前缀时就会进入到查找私有 \DosDevices 命名空间的流程,首先从进程 EPROCESS 的 DeviceMap 开始,DeviceMap 的 DosDevicesDirectory 指向进程私有的 \DosDevices 命名空间,如果在这个私有 \DosDevices 没找到我们的目标对象,就会继续根据 DeviceMap 的 GlobalDosDevicesDirectory 查找,GlobalDosDevicesDirectory 总是指向 \GLOBAL??\
参考: [Windows Internals 7th Edition 第三章 Seesion Namespace 小结]
1 | // 创建假的 windows 目录 |
本例中,没有执行实验文档中的 CreateMountPoint,发现仍然成功了,猜测教程中执行 CreateMountPoint 的原因是:我们的 c:\demo9\windows 假目录下没有 system32,可能会影响某些程序的执行。所以用 CreateMountPoint 提前创建挂载点,保证 system32 的正常文件访问,某些程序的依赖 dll 能够正常加载。
根据我以 calc.exe 实际测试,发现即便创建了挂载点也不能弹出计算器,会提示并行配置错误,这个暂时不深究了。
如果创建了 MountPoint,如何删除?
1 | c:\workshop\symboliclink-testing-tools>DeleteMountPoint.exe c:\demo9\Windows\system32 |
测试了一下,会失败,而且 Last Error Message 为空,猜测原因是:创建 ??\C:\windows 符号链接时,??\C: 已经是符号链接了,所以不允许为符号链接创建子级符号链接。
看了一下源码,CreateNativeSymlink 是调用 NtCreateSymbolicLinkObject,是在 Object Manager 中创建符号链接。
试着运行了一下 DemoClient 的对应测试项,没有看懂是怎么回事儿,还是根据源码看看目标是什么吧
1 | // RpcServer.cpp |
运行于 RpcServer 的这段代码的实现功能是:从 Rpc Client 进程复制一个 handle 给 Rpc Client 自己。测试时,客户端可以指定希望复制的 handle 值,我们的目标就是泄露 RpcServer 的 handle。
前面几行是获得 Rpc Client 进程的 Process Handle,后面调用 DuplicateHandle 是实际的 handle 复制动作,基本上唯一可控的就是 handle 值了
有两个 handle 属于 pseudo-handle: -1 和 -2,分别是 GetCurrentProcess 和 GetCurrentThread 的返回值。这两个常量 handle 值并不是真正的 handle,而是为了方便程序员对当前进程、当前线程的引用而使用的伪 handle。
GetCurrentThread 返回的 handle 值永远是 -2,kernel32.dll 中反汇编的 GetCurrentThread 的源码
1 | HANDLE __stdcall GetCurrentThread() |
实际上,当前线程的 handle 并不是 -2,实际的 handle 值可以通过调用 KeGetCurrentThread() 获得。
反汇编分析 DuplicateHandle 的源码,它会调用 ObpReferenceProcessObjectByHandle 先获得客户端传递来的 handle 所引用的对象。
结合 WRK 和 IDA 的反编译结果,简化后 ObpReferenceProcessObjectByHandle 的部分代码如下:
1 | NTSTATUS |
上面这段代码非常重要,有点需要注意:
当 Handle 为 GetCurrentProcess()(即 -1)时,返回的 Object 为参数 Process 所引用的对象,后面会介绍,这个 Process 是 RpcClient 进程对象。
当 Handle 为 GetCurrentThread()(即 -2)时,返回的 Object 为 KeGetCurrentThread() 返回的当前线程对象,也就是当前的 RpcServer 中的 Calling Thread 对象。
参数 Process 所引用的 Process 对象哪里来的呢?
1 | BOOL __stdcall DuplicateHandle(HANDLE hSourceProcessHandle, HANDLE hSourceHandle, HANDLE hTargetProcessHandle, LPHANDLE lpTargetHandle, DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwOptions) |
winbase!DuplicateHandle->ntoskrnl!NtDuplicateObject->ntoskrnl!ObDuplicateObject->ntoskrnl!ObpReferenceProcessObjectByHandle
传递给 DuplicateHandle 的 hSourceProcessHandle,在 NtDuplicateObject 中被解引用为进程对象继续向下传递,直到 ObpReferenceProcessObjectByHandle 中作为参数 Process。所以当 Rpc Client 进程传递的 handle 为 -1 时,返回的是 Rpc Client 进程自己的 handle。
当 Rpc Client 进程传递 -2 作为 handle 值给 Rpc Server 时,Rpc Server 根据 handle 值 -2 解引用对象,没有考虑 -2 是个伪 handle,-2 代表 Rpc Server 中的当前线程,然后复制该线程句柄给了 Rpc Client,造成了高权限进程线程句柄的泄露,客户端得到这个句柄之后,就可以实现一些越权操作了。
1 | NTSTATUS HandleIoControl(PIRP Irp) |
从源码和标题来看,这个 case 主要是用户态 DemoClient 利用内核创建文件。那能不能创建 DemoClient 本身没有权限创建的文件呢?比如向 c:\windows 目录
与普通用户态程序调用 CreateFile 创建文件不同的时,ZwCreateFile 要求传入的路径必须是一个包括设备名的全路径。
假如我们要创建文件 c:\windows\helloworld,我们传入 ZwCreateFile 的路径需要是 ??\c:\windows\helloworld 或者 \DosDevices\c:\windows\helloworld,如果是前者,对象管理器会负责转换为后者。(其实传入 \GLOBAL??\c:\windows\helloworld 也可以,这个在 Demo9 中讨论过)
实际测试下面的过程,可以成功向 c:\windows 和 c:\windows\system32 下会创建文件,然而 DemoClient 本身是没有对这些目录的写权限的。新创建的这两个文件的属主用户为当前进程 DemoClient 的用户
1 | [Driver File Tests] |
参考:
选择 1 - Create File 测试时,CreateFile 的 secure 参数为 FALSE,所以创建文件 handle 时指定的 OBJECT_ATTRIBUTES.AttributeFlags 没有置位 OBJ_FORCE_ACCESS_CHECK,缺少 OBJ_FORCE_ACCESS_CHECK ZwCreateFile 时内核缺少安全检查,也就没有对 DemoClient 用户态进程权限的验证。
用户态进程和驱动应该如何共享 handle ?
遵循下面几点:
如果不希望用户态进程得到并访问这个 handle,应该置位 OBJECT_ATTRIBUTES.AttributeFlags 为 OBJ_KERNEL_HANDLE。
handle 由内核态创建然后传递给用户态,尽量不要反过来。从用户态传递过来的 handle,驱动应该认为是不可信的
驱动替用户态进程创建 handle 时,需要置位 OBJ_FORCE_ACCESS_CHECK,以添加必要的安全检查,包括进程权限的检查
与用户态共享的 handle,内核需要使用 ObReferenceObjectByPointer 引入引用计数,防止用户态 close handle 之后驱动再访问引起系统 crash
打开 Sysinternals 的 DebugView.exe 工具,标题栏勾选 [Capture]->[Capture Kernel],抓取驱动的调试日志
1 | [Driver Token Tests] |
DebugView 看到的调试日志分别为:
1 | IsCallerElevated: ImpersonationLevel: None |
1 | // 驱动代码 |
当某个用户隶属于 Administrator 组时,用户启动的进程 token 中会嵌入 Administrator Token,只不过当 UAC 启用时,这个 token 不生效。尽管不生效,我们却可以通过 API GetTokenInformation() 获得这个 Administrator token,调用时 class 设定为 TokenLinkedToken。
既然可以得到 Administrator token,那不是可以直接调用 CreateProcessAsUser() 以 Administrator 身份创建进程了?
不是,还有 Impersonation Level,进程如果没有 SeTcbPrivilege,那 GetTokenInformation() 得到的 Administrator token 的 level 不是 SecurityImpersonation,而是 SecurityIdentification。也就是不能用这个 token 模仿管理员用户的行为,只能根据这个 token 知道 Administrator 有哪些 SID/Privileges。
什么进程会有 SeTcbPrivilege 权限? MSDN 中说底层认证相关的服务有这个权限。
1 | Only low-level authentication services should require this privilege. |
驱动代码中检查 client 权限时,只检查了 token 的 TokenIsElevated 是否置位,却没有检查 token 的 SECURITY_IMPERSONATION_LEVEL。由于 client 没有 SeTcbPrivilege 权限,所以只得到了一个 SecurityIdentification level 的 Administrator token,是没有权限模拟管理员用户进程的。但驱动没有检查这里,所以返回的状态时成功 STATUS_SUCCESS。
漏洞的修复也比较简单,就是如代码中的 secure 判断分支:
1 | if (secure) |
什么时候使用 impersonation token 认证,什么时候使用 primary token?
下列几种情况时,即便存在 impersonation token,也会使用 primary token:
1 | BOOL WINAPI OpenThreadToken( |
1 | C:\workshop\ExploitTools>ExploitDotNetDCOMSerialization.exe 801445a7-c5a9-468d-9423-81f9d13fee9b calc |
这个涉及到很多 COM 的知识,之前完全不懂,等日后有了这方面的基本了解之后再来补上
如果在环境搭建过程中生成了虚拟机快照,提取出有用文件之后,恢复快照最方便。否则就按照下面的步骤恢复。管理员权限运行
1 | C:\WINDOWS\system32>Bcdedit.exe -set TESTSIGNING OFF |
如果修改了 Secure Boot,记得还原。关机重启,快速按 F2 进入 BIOS,选择 Boot 标签,将 Secure Boot 设置为 Enabled。