MacOS LPE CVE-2020-9854
라온화이트햇 핵심연구팀 강인욱
MacOS LPE CVE-2020-9854
MacOS 10.15.5 이하 에서 root 권한을 획득할 수 있는 취약점이 나왔다. 이 취약점에 대해 알아보자
취약점1
첫 번째 취약점은 authd(Security.framework)에서 발생하는 취약점이다. 이 프레임워크는 키 체인 엑세스, 코드 서명 및 권한 부여를 포함한 보안 관련 상항을 관리한다.
사전 등록된 권한과 지원하는 rule은 아래의 URL에서 확인할 수 있다.
해당 rule을 사용하면 rule 자격을 갖기 위해 클라이언트에게 필요한 권한을 매우 상세하게 제어할 수 있다. 일부 권한은 아래와 같이 팝업 대화상자에 비밀번호를 입력해야 한다.
authd 코드를 살펴보면 process.c 에서 재미있는 부분이 있다고 한다. 클라이언트로부터 코드 서명 관련 정보를 가져오는 코드 라인은 아래와 같다.
// ...
status = SecCodeCopySigningInformation(codeRef, kSecCSRequirementInformation, &code_info); // [1]
require_noerr_action(status, done, os_log_debug(AUTHD_LOG, "process: PID %d SecCodeCopySigningInformation failed with %d", proc->auditInfo.pid, (int)status));
// ...
if (CFDictionaryGetValueIfPresent(code_info, kSecCodeInfoEntitlementsDict, &value)) {
if (CFGetTypeID(value) == CFDictionaryGetTypeID()) {
proc->code_entitlements = CFDictionaryCreateCopy(kCFAllocatorDefault, value); // [2]
}
value = NULL;
}
코드에서 보듯이 클라이언트로 부터 데이터를 검색하기 위해 SecCodeCopySigningInformation 를 호출하고 자격을 찾으면 해당 값을 dict로 복사한다.
자 여기서 문제는 Apple 개발자 문서를 보면SecCodeCopySigningInformation의 문제를 알 수 있다고 한다.
If the signing data for the code is corrupt or invalid, this function may fail or it
may return partial data. To ensure that only valid data is returned (and errors are
raised for invalid data), you must successfully call the SecCodeCheckValidity or
SecCodeCheckValidityWithErrors function before calling SecCodeCopySigningInformation.
SecCodeCheckValidity의 기능은 디스크의 클라언트 바이너리를 CDHash와 비교하여 무결성을 확인한다. 하지만 이런 일이 절대 발생하지 않기 때문에 “authd”의 제재를 받지 않고 임의의 권한을 부여할 수 있다.
이제 “authd”가 어떤 권한에 관심이 있는지 알아 내야한다. 이것은 “authd” 내부적으로 프로세스가 필요한 권한을 가지고 있는지 확인하기 위해 사용 하는 함수이다.
bool
process_has_entitlement_for_right(process_t proc, const char * right)
{
bool entitled = false;
require(right != NULL, done);
CFTypeRef rights = NULL;
if (proc->code_entitlements && CFDictionaryGetValueIfPresent(proc->code_entitlements, CFSTR("com.apple.private.AuthorizationServices"), &rights)) { // [3]
if (CFGetTypeID(rights) == CFArrayGetTypeID()) {
CFStringRef key = CFStringCreateWithCStringNoCopy(kCFAllocatorDefault, right, kCFStringEncodingUTF8, kCFAllocatorNull);
require(key != NULL, done);
CFIndex count = CFArrayGetCount(rights);
for (CFIndex i = 0; i < count; i++) {
if (CFEqual(CFArrayGetValueAtIndex(rights, i), key)) {
entitled = true;
break;
}
}
CFReleaseSafe(key);
}
}
done:
return entitled;
}
보시다시피 해당 문자열의 “com.apple.private.AuthorizationServices” 자격을 가진 것을 찾는다. 이는 array-string으로 되어 있다.
트리거하는 법은 아래와 같다.
- 원하는 자격으로 파일을 만든다. (예를 들면 system.install.apple-software )
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.private.AuthorizationServices</key>
<array>
<string>system.install.apple-software</string>
</array>
</dict>
</plist>
- 프로그램을 작성하고 잠시 기다린 후 AuthorizationRef를 생성하고 “authd”에 권한을 요청한다.
- 프로그램을 기다리는 동안, “codesign -f -s - –entitlements entitlements.xml ./test”를 실행한다.
- “authd”로그를 확인한다. 그러면 아래와 같은 것을 볼 수 있다.
왜 프로그램을 실행하기 전에 코드사인을 먼저 해야하는지 궁금하다면 AppleMobileFileIntegretyDaemon 이 데몬 때문이다.
이 데몬은 바이너리의 자격과 서명을 검증하는 데몬인데, Apple이 서명하지 않은 프로그램이므로 권한이 제한되어 있기 때문에 실행할 수 없다.
authorization.plist를 분석하면 해당 권한을 보유하여 기본 사용자가 다음 권한을 얻을 수 있다.
Right Private Framework implementing API
system.install.apple-software // PackageKit.framework/InstallKit.framework
system.preferences.nvram // SystemAdministrator.framework
com.apple.uninstalld.uninstall // Uninstall.framework
com.apple.opendirectoryd.linkidentity
com.apple.ServiceManagement.daemons.modify // ServiceManagement.framework
system.services.directory.configure
com.apple.trust-settings.user
system.install.apple-config-data // PackageKit.framework/InstallKit.framework
system.services.networkextension.filtering
system.install.software.iap // PackageKit.framework/InstallKit.framework
system.install.software.mdm-provided // PackageKit.framework/InstallKit.framework
system.install.apple-software.standard-user // PackageKit.framework/InstallKit.framework
system.services.systemconfiguration.network
com.apple.activitymonitor.kill // Activicymonitor?
com.apple.XType.fontmover.restore
com.apple.security.assessment.update
system.services.networkextension.vpn
com.apple.SoftwareUpdate.scan // SoftwareUpdate.framework/InstallKit.framework
com.apple.SoftwareUpdate.modify-settings // SoftwareUpdate.framework/InstallKit.framework
system.preferences.security.remotepair
우리에게는 system.install.* 권한이 있다. PackageKit.framework 을 분석하면 흥미로운 API가 있음을 알 수 있다. 이 권한은 SIP로 보호되지 않은 위치에 Apple 서명 패키지를 설치할 수 있다.
취약점2
PKG 파일은 기본적으로 설치할 파일, 코드 서명 및 pre/post-install 스크립트를 포함하는 아카이브이다. 이 pkgutil 유틸리티를 사용하면 이러한 아카이브의 내용을 추출할 수 있다.
일반적으로 pre/post-install 스크립트는 “installd”에 의해 root 권한으로 실행된다. 그러나 우리는 Apple이 서명하지 않았기 때문에 악의적인 스크립트를 사용하여 자체 패키지를 만들 수 없다.
그렇다면 Apple이 서명한 패키지를 찾은 경우 어떻게하면 이러한 스크립트 중 하나를 가로챌 수 있을까?
패키지가 Apple에 의해 서명된 경우, 스크립트는 “installd” 의해 실행되지 않고 “system_installd”에 실행 된다.
“system_installd”의 차이점은 “com.apple.rootless.install.heritable” 자격을 가지고 있다. 이 뜻은 모든 자식 프로세스가 SIP 제한 없이 실행된다는 것이다.
그렇다면 서명된 Apple 패키지를 어디서 찾을 수 있을까?
아래의 링크에서 찾을 수 있다.
해당 링크를 보면 macOSPublicBetaAccessUtility.pkg에서 경로가 일치하는 한 높은 권한을 가진 실행 파일을 위조할 수 있는 걸 볼 수 있다.
취약점3
이미 SIP 우회와 루트 권한으로 코드 실행을 할 수 있고 커널 코드 실행을 하기 위한 방법은 “kextutil”이다. 이 유틸리티는 커널 모듈을 로드/언로드 할 수 있다.
SIP이 비활성화된 시스템에서 커널 모듈을 로드하려는 경우 루트로 충분하다. 하지만 SIP이 있는 시스템에서는 Apple이 서명한 확장만 로드할 수 있다.
이론적으로 kextutil이 kext를 한 번만 열어 모든 파일을 로드하고 검사를 수행 한 다음 메모리에서 파일을 로드하면 문제가 없다. 파일 디스크립터를 사용하면 이 문제를 완화 할 수 있지만 실제로는 kext가 확인되는 시점과 커널에 로드되는 시점 사이에 경쟁 조건이 있다.
아래의 방법으로 100% 레이스 컨디션을 성공시킬 수 있다.
- Apple의 서명이된 커널 모듈을 non-SIP protected인 곳(ex. acfs.kext)으로 복사한다.
- kextutl -interactive /tmp/acfs.text를 실행한다. (ktextutil이 자동으로 서명을 검증할 것이다.)
- 바이너리를 덮어쓴다. (ex. mv kernelHax /Library/StagedExtensions/private/tmp/acfs.kext/Contents/MacOS/acfs)
- kextutil에 계속하도록 요청을 보내고 악성코드를 포함한 kext를 커널에 로드 한다.
- kextutil에 한번더 계속하도록 요청을 보내면 커널 모듈이 동작하도록 작동할 것이다.
전체 인스플로잇 링크