On the first day of WWDC22 Apple released a very exciting beta version of iOS16. I was waiting for that, and indeed, Apple exceeded all expectations here. Folks on Twitter already covered many of the awesome new security features, hardenings, and attack surface reduction, so I wouldn’t repeat that here. I do encourage you to check that out. For your convenience, I wrote here a (very) partial list:
mach_msg2
, while deprecating mach_msg
- ref.And in general, this Twitter thread is amazing.
The thing I wanted to cover here is something new I noticed in IOSurfaceRootUserClient
and IOGPUDeviceUserClient
.
As I’ve said before, I do not like to repeat highly documented and common knowledge stuff. Therefore I won’t explain here what IOKit is, how it was designed and how it works. There are many (many) resources on that, which I linked in a previous blogpost. That blogpost also covers the high-level of what happens when an EL0 process interacts with a kernel driver through its userclient.
If you’ll take a look at IOSurfaceRootUserClient
, you’ll noticed the IOSurfaceRootUserClient::sMethodDescs
table is now duplicated, and now we actually have two tables, one after the other - the first one is IOSurfaceRootUserClient::sMethodDescs
, and the second one is IOSurfaceRootUserClient::sMethodDescsRestricted
. Same thing goes for IOGPUDeviceUserClient
- we have IOGPUDeviceUserClient::sDeviceMethods
, followed by IOGPUDeviceUserClient::sDeviceMethodsRestricted
.
Interesting. The difference between these tables (the “regular” and the “restricted”) is that some of the methods in the “restricted” versions are replaced with the function s_restricted
:
IOGPUDeviceUserClient
the function is IOGPUDeviceUserClient::s_restricted
IOSurfaceRootUserClient
the function is IOSurfaceRootUserClient::s_restricted
And, no surprises there, both of the functions are identical:
FFFFFFF0097B6A00 IOSurfaceRootUserClient::s_restricted(IOSurfaceRootUserClient*, void *, IOExternalMethodArguments *)
FFFFFFF0097B6A00 MOV W0, #0xE00002E2
FFFFFFF0097B6A08 RET
FFFFFFF00952B4E0 IOGPUDeviceUserClient::s_restricted(IOGPUDeviceUserClient*, void *, IOExternalMethodArguments *)
FFFFFFF00952B4E0 MOV W0, #0xE00002E2
FFFFFFF00952B4E8 RET
As a reminder - 0xE00002E2
is kIOReturnNotPermitted
.
Well, I guess we can assume what’s going on here (don’t worry, we will reverse it, because assuming stuff is the root of all evil). It seems the approach is “OK, so some processes need access to certain functionalities of IOSurfaceRoot
or IOGPUDevice
, but they don’t need access to all the functionalities. Let’s give each entity exactly what it needs”.
It’s not the only way to achieve such attack surface reduction, and it’s not the first time Apple has done something like this. You could simply add new entitlements and check at the beginning of certain external methods handlers if the required entitlement is set. Tielei noted it has happened before in IOMFB, and actually, iOS 16 added another function to IOMFB - IOMobileFramebufferUserClient::s_create_gain_map
, and it’s gated in this way by the entitlement "com.apple.private.gain-map-access"
.
However, it’s cool to see this generic change, of simply creating “reduce exposed” versions of this kernel components.
First of all, some details:
The tables come one after the other.
The metadata of the structure describing the method (for IOGPUDeviceUserClient
it’s IOExternalMethodDispatch
) - everything besides the function
field, which is the function pointer itself, remains the same. So, you will see all the I/O sizes as the “original” method at this index, but with the function
pointer points to s_restricted
.
As a reminder, here is IOExternalMethodDispatch
:
typedef IOReturn (*IOExternalMethodAction)(OSObject * target, void * reference,
IOExternalMethodArguments * arguments);
struct IOExternalMethodDispatch {
IOExternalMethodAction function;
uint32_t checkScalarInputCount;
uint32_t checkStructureInputSize;
uint32_t checkScalarOutputCount;
uint32_t checkStructureOutputSize;
};
And as an example (partial copy-paste from IOGPUDeviceUserClient::sDeviceMethodsRestricted
):
...
FFFFFFF00A01D798 IOExternalMethodDispatch_s <IOGPUDeviceUserClient::s_create_mtlfence(IOGPUDeviceUserClient*,void *,IOExternalMethodArguments *),\
FFFFFFF00A01D798 0, 0, 0, 4>
FFFFFFF00A01D7B0 IOExternalMethodDispatch_s <IOGPUDeviceUserClient::s_destroy_mtlfence(IOGPUDeviceUserClient*,void *,IOExternalMethodArguments *),\
FFFFFFF00A01D7B0 1, 0, 0, 0>
FFFFFFF00A01D7C8 IOExternalMethodDispatch_s <IOGPUDeviceUserClient::s_create_mtlevent(IOGPUDeviceUserClient*,void *,IOExternalMethodArguments *),\
FFFFFFF00A01D7C8 0, 0, 2, 0>
FFFFFFF00A01D7E0 IOExternalMethodDispatch_s <IOGPUDeviceUserClient::s_destroy_mtlevent(IOGPUDeviceUserClient*,void *,IOExternalMethodArguments *),\
FFFFFFF00A01D7E0 1, 0, 0, 0>
FFFFFFF00A01D7F8 IOExternalMethodDispatch_s <IOGPUDeviceUserClient::s_restricted(IOGPUDeviceUserClient*,void *,IOExternalMethodArguments *),\
FFFFFFF00A01D7F8 0, 0, 0, 0x30>
FFFFFFF00A01D810 IOExternalMethodDispatch_s <IOGPUDeviceUserClient::s_restricted(IOGPUDeviceUserClient*,void *,IOExternalMethodArguments *),\
FFFFFFF00A01D810 0, 0xFFFFFFFF, 0, 0xFFFFFFFF>
FFFFFFF00A01D828 IOExternalMethodDispatch_s <IOGPUDeviceUserClient::s_get_allocated_size(IOGPUDeviceUserClient*,void *,IOExternalMethodArguments *),\
FFFFFFF00A01D828 0, 0, 1, 0>
FFFFFFF00A01D840 IOExternalMethodDispatch_s <IOGPUDeviceUserClient::s_set_command_queue_notification_queue(IOGPUDeviceUserClient*,void *,IOExternalMethodArguments *),\
FFFFFFF00A01D840 2, 0, 0, 0>
FFFFFFF00A01D858 IOExternalMethodDispatch_s <IOGPUDeviceUserClient::s_submit_command_buffers(IOGPUDeviceUserClient*,void *,IOExternalMethodArguments *),\
FFFFFFF00A01D858 4, 0xFFFFFFFF, 0, 0>
FFFFFFF00A01D870 IOExternalMethodDispatch_s <IOGPUDeviceUserClient::s_set_priority_and_background(IOGPUDeviceUserClient*,void *,IOExternalMethodArguments *),\
FFFFFFF00A01D870 1, 0xC, 1, 0>
FFFFFFF00A01D888 IOExternalMethodDispatch_s <IOGPUDeviceUserClient::s_restricted(IOGPUDeviceUserClient*,void *,IOExternalMethodArguments *),\
FFFFFFF00A01D888 1, 4, 0, 0>
FFFFFFF00A01D8A0 IOExternalMethodDispatch_s <IOGPUDeviceUserClient::s_restricted(IOGPUDeviceUserClient*,void *,IOExternalMethodArguments *),\
FFFFFFF00A01D8A0 0, 0, 2, 0>
FFFFFFF00A01D8B8 IOExternalMethodDispatch_s <IOGPUDeviceUserClient::s_restricted(IOGPUDeviceUserClient*,void *,IOExternalMethodArguments *),\
FFFFFFF00A01D8B8 1, 0, 0, 0>
FFFFFFF00A01D8D0 IOExternalMethodDispatch_s <IOGPUDeviceUserClient::s_restricted(IOGPUDeviceUserClient*,void *,IOExternalMethodArguments *),\
FFFFFFF00A01D8D0 2, 0, 0, 0>
FFFFFFF00A01D8E8 IOExternalMethodDispatch_s <IOGPUDeviceUserClient::s_restricted(IOGPUDeviceUserClient*,void *,IOExternalMethodArguments *),\
FFFFFFF00A01D8E8 1, 0, 2, 0>
FFFFFFF00A01D900 IOExternalMethodDispatch_s <IOGPUDeviceUserClient::s_restricted(IOGPUDeviceUserClient*,void *,IOExternalMethodArguments *),\
FFFFFFF00A01D900 2, 0, 0, 0>
FFFFFFF00A01D918 IOExternalMethodDispatch_s <IOGPUDeviceUserClient::s_restricted(IOGPUDeviceUserClient*,void *,IOExternalMethodArguments *),\
FFFFFFF00A01D918 2, 0, 0, 0>
FFFFFFF00A01D930 IOExternalMethodDispatch_s <IOGPUDeviceUserClient::s_restricted(IOGPUDeviceUserClient*,void *,IOExternalMethodArguments *),\
FFFFFFF00A01D930 1, 0, 1, 0>
FFFFFFF00A01D948 IOExternalMethodDispatch_s <IOGPUDeviceUserClient::s_set_resource_owner_identity(IOGPUDeviceUserClient*,void *,IOExternalMethodArguments *),\
FFFFFFF00A01D948 2, 0, 0, 0>
FFFFFFF00A01D960 IOExternalMethodDispatch_s <IOGPUDeviceUserClient::s_restricted(IOGPUDeviceUserClient*,void *,IOExternalMethodArguments *),\
FFFFFFF00A01D960 3, 0, 1, 0>
...
Now, let’s see how the externalMethod
function chooses which table to use. I expect to see new entitlement involved, and it would be nice to be familiar with them. The relevant code is very straightforward. Let’s check it out.
FFFFFFF00952B97C ADRL X8, IOGPUDeviceUserClient::sDeviceMethodsRestricted
FFFFFFF00952B984 LDRB W9, [X0,#0x103]
FFFFFFF00952B988 ; sizeof(IOExternalMethodDispatch)
FFFFFFF00952B988 MOV W10, #0x18
FFFFFFF00952B98C ; set X8 to the function from sDeviceMethodsRestricted
FFFFFFF00952B98C UMADDL X8, W1, W10, X8
FFFFFFF00952B990 ADRL X11, IOGPUDeviceUserClient::sDeviceMethods
FFFFFFF00952B998 ; set X10 to the function from sDeviceMethods
FFFFFFF00952B998 UMADDL X10, W1, W10, X11
FFFFFFF00952B99C ; choose between X10 and X8 based on offset 0x103 from the userclient object
FFFFFFF00952B99C CMP W9, #0
FFFFFFF00952B9A0 CSEL X3, X10, X8, EQ ; dispatch
By looking for this value, we can easily see the following code from IOGPUDeviceUserClient::deviceUserClientStart
:
FFFFFFF00952B5F4 BL IOGPUDevice::isRestrictedClient(void)
FFFFFFF00952B5F8 STRB W0, [X19,#0x103]
(while X19
is this
). Ok, let’s see what IOGPUDevice::isRestrictedClient
does:
FFFFFFF009540E8C IOGPUDevice::isRestrictedClient(void)
FFFFFFF009540E8C ADRL X1, aComApplePrivat_72 ; "com.apple.private.gpu-restricted"
FFFFFFF009540E94 B IOGPUDevice::doesEntitlementExist(char const*)
Great! So, now we know that:
0x103
in IOGPUDeviceUserClient
means “isRestricted”."com.apple.private.gpu-restricted"
entitlement will have to use the restricted userclient.Same steps - check out the following code from IOSurfaceRootUserClient::externalMethod
:
FFFFFFF0097BA1D0 IOSurfaceRootUserClient::externalMethod(unsigned int, IOExternalMethodArgumentsOpaque *)
FFFFFFF0097BA1D0 ; DATA XREF: com.apple.iokit.IOSurface:__const:FFFFFFF00A0831C8↓o
FFFFFFF0097BA1D0 ADRL X8, IOSurfaceRootUserClient::sMethodDescsRestricted
FFFFFFF0097BA1D8 LDRB W9, [X0,#0x11F]
FFFFFFF0097BA1DC ADRL X10, IOSurfaceRootUserClient::sMethodDescs
FFFFFFF0097BA1E4 CMP W9, #0
FFFFFFF0097BA1E8 ; select between IOSurfaceRootUserClient::sMethodDescsRestricted and IOSurfaceRootUserClient::sMethodDescs
FFFFFFF0097BA1E8 CSEL X3, X10, X8, EQ
FFFFFFF0097BA1EC MOV W4, #0x35 ; '5'
FFFFFFF0097BA1F0 MOV X5, X0
FFFFFFF0097BA1F4 MOV X6, #0
FFFFFFF0097BA1F8 B IOUserClient2022::dispatchExternalMethod(uint,IOExternalMethodArgumentsOpaque *,IOExternalMethodDispatch2022 const*,ulong,OSObject *,void *)
Searching for this offset (0x11F
), we get to the following code from IOSurfaceRootUserClient::init
:
FFFFFFF0097B6A58 ADRL X2, aComApplePrivat_79 ; "com.apple.private.iosurfaceinfo"
FFFFFFF0097B6A60 MOV X1, X20 ; task
FFFFFFF0097B6A64 BL IOSurfaceRootUserClient::taskHasEntitlement(task *,char const*)
FFFFFFF0097B6A68 ; has iosurfaceinfo entitlemment
FFFFFFF0097B6A68 STRB W0, [X19,#0x11A]
FFFFFFF0097B6A6C ADRL X2, aComApplePrivat_80 ; "com.apple.private.gpu-restricted"
FFFFFFF0097B6A74 MOV X1, X20 ; task
FFFFFFF0097B6A78 BL IOSurfaceRootUserClient::taskHasEntitlement(task *,char const*)
FFFFFFF0097B6A7C ; has gpu-restricted entitlement
FFFFFFF0097B6A7C STRB W0, [X19,#0x11F]
FFFFFFF0097B6A80 ADRL X2, aComApplePrivat_81 ; "com.apple.private.IOSurface.protected-a"...
FFFFFFF0097B6A88 MOV X1, X20 ; task
FFFFFFF0097B6A8C BL IOSurfaceRootUserClient::taskHasEntitlement(task *,char const*)
FFFFFFF0097B6A90 ; has IOSurface.protected-access entitlement
FFFFFFF0097B6A90 STRB WZR, [X19,#0x11B]
Awesome, now we know some of the new entitlements:
"com.apple.private.iosurfaceinfo"
"com.apple.private.gpu-restricted"
"com.apple.private.IOSurface.protected-access"
You probably noticed that IOSurfaceRootUserClient::externalMethod
calls IOUserClient2022::dispatchExternalMethod
(it basically wraps that now). There are more userclients that behave this way - their externalMethod
function simply calls to IOUserClient2022::dispatchExternalMethod
, with the relevant external method table and arguments. This is also new, and it was added in the beta version of iOS 16 from June. But let’s leave this for another time.
Let’s see if some interesting processes are more restricted using these new userclients. Let’s look at com.apple.WebKit.WebContent
in the beta version:
saaramar@Saars-Air com.apple.WebKit.WebContent.xpc % codesign -dvv --entitlements - ./com.apple.WebKit.WebContent
Executable=/Users/saaramar/Documents/projects/restricted_userclient_blogpost/com.apple.WebKit.WebContent.xpc/com.apple.WebKit.WebContent
Identifier=com.apple.WebKit.WebContent
Format=bundle with Mach-O thin (arm64e)
CodeDirectory v=20400 size=756 flags=0x2(adhoc) hashes=13+7 location=embedded
Signature=adhoc
Info.plist entries=29
TeamIdentifier=not set
Sealed Resources version=2 rules=9 files=0
Internal requirements count=0 size=12
[Dict]
[Key] dynamic-codesigning
[Value]
[Bool] true
[Key] com.apple.private.memorystatus
[Value]
[Bool] true
[Key] com.apple.private.verified-jit
[Value]
[Bool] true
[Key] com.apple.private.pac.exception
[Value]
[Bool] true
[Key] com.apple.QuartzCore.secure-mode
[Value]
[Bool] true
[Key] com.apple.private.gpu-restricted
[Value]
[Bool] true
...
Indeed! WebContent now has the "com.apple.private.gpu-restricted"
entitlement, which means it can’t get access to all the IOGPUDevice
external method. Another binary is com.apple.WebKit.GPU
.
In addition, it seems that /usr/libexec/backboardd
has the "com.apple.private.IOSurface.protected-access"
entitlement.
Apple has done a fantastic work on attack surface reductions. It’s not the first time flows to certain functionalities in the kernel are being gated by entitlements, and it probably won’t be the last.
I hope you enjoyed reading this short write-up.
Thanks,
Saar Amar