The first method of testing for this is to check is to create a tweak using Theos. To get all the necessary information, I use a combination of other tools, namely:
Examining the changes, we find that they’re all in request type 5, corresponding toGUESTRPC_FINALIZE. The user controls the argument which is& 0x21and passed toguestrpc_close_backdoor.
Control ofa3allows us to go down the first branch in a previously inaccessible manner, letting us free the buffer ata1+0x8, which corresponds to the buffer used internally to store the reply data passed back to the user. However, this same buffer will also be freed with command type 6,GUESTRPC_CLOSE, resulting in a controlled double free which we can turn into use-after-free. (The other patch nop’d out code responsible for NULLing out the reply buffer, which would have prevented this codepath from being exploited.)
Given that the bug is very similar to a traditional CTF heap pwnable, we can already envision a rough path forward, for which we’ll fill in details shortly:
Obtain a leak, ideally of thevmware-vmxbinary text section
Use tcache to allocate a chunk on top of a function pointer
Obtainripandrdicontrol and invokesystem("/usr/bin/xcalc &")
Heap internals and obtaining a leak
Firstly, it should be stated that the vmx heap appears to have little churn in a mostly idle VM, at least in the heap section used for guestrpc requests. This means that the exploit can relatively reliable even if the VM has been running for a bit or if the user was previously using the system.
In order to obtain a heap leak, we’ll perform the following series of operations
Allocate three channels [A], [B], and [C]
Send theinfo-setcommmand to channel [A], which allows us to store arbitrary data of arbitrary size (up to a limit) in the host heap.
Open channel [B] and issue ainfo-getto retrieve the data we just set
Issue the reply length and reply read commands on channel [B]
Invoke the buggy finalize command on channel [B], freeing the underlying reply buffer
Invokeinfo-geton channel [C] and receive the reply length, which allocates a buffer at the same address we just
Close channel [B], freeing the buffer again
Read out the reply on channel [C] to leak our data
Eachvmware-vmxprocess has a number of associated threads, including one thread per guest vCPU. This means that the underlying glibc heap has both the tcache mechanism active, as well as several different heap arenas. Although we can avoid mixing up our tcache chunks by pinning our cpu in the guest to a single core, we still cannot directly leak alibcpointer because only themain_arenain the glibc heap resides there. Instead, we can only leak a pointer to our individual thread arena, which is less useful in our case.
[#0] Id 1, Name: "vmware-vmx", stopped, reason: STOPPED [#1] Id 2, Name: "vmx-vthread-300", stopped, reason: STOPPED [#2] Id 3, Name: "vmx-vthread-301", stopped, reason: STOPPED [#3] Id 4, Name: "vmx-mks", stopped, reason: STOPPED [#4] Id 5, Name: "vmx-svga", stopped, reason: STOPPED [#5] Id 6, Name: "threaded-ml", stopped, reason: STOPPED [#6] Id 7, Name: "vmx-vcpu-0", stopped, reason: STOPPED <-- our vCPU thread [#7] Id 8, Name: "vmx-vcpu-1", stopped, reason: STOPPED [#8] Id 9, Name: "vmx-vcpu-2", stopped, reason: STOPPED [#9] Id 10, Name: "vmx-vcpu-3", stopped, reason: STOPPED [#10] Id 11, Name: "vmx-vthread-353", stopped, reason: STOPPED . . . .
To get around this, we’ll modify the above flow to spray some other object with a vtable pointer. I came acrossthis writeupby Amat Cama which detailed his exploitation in 2017 using drag-n-drop and copy-paste structures, which are allocated when you send a guestrpc command in the host vCPU heap.
Therefore, I updated the above flow as follows to leak out a vtable/vmware-vmx-bss pointer
Allocate four channels [A], [B], [C], and [D]
Send theinfo-setcommmand to channel [A], which allows us to store arbitrary data of arbitrary size (up to a limit) in the host heap.
Open channel [B] and issue ainfo-getto retrieve the data we just set
Issue the reply length and reply read commands on channel [B]
Invoke the buggy finalize command on channel [B], freeing the underlying reply buffer
Invokeinfo-geton channel [C] and receive the reply length, which allocates a buffer at the same address we just
Close channel [B], freeing the buffer again
Sendvmx.capability.dnd_versionon channel [D], which allocates an object with a vtable on top of the chunk referenced by [C]
Read out the reply on channel [C] to leak the vtable pointer
One thing I did notice is that the copy-paste and drag-n-drop structures appear to only allocate their vtable-containing objects once per guest execution lifetime. This could complicate leaking pointers inside VMs where guest tools are installed and actively being used. In a more reliable exploit, we would hope to create a more repeatable arbitrary read and write primtive, maybe with these heap constructions alone. From there, we could trace backwards to leak our vmx binary.
※ Non-Volatile Register란 자식 함수를 호출하고 리턴된 후에 값이 호출 전과 동일하게 유지되는 레지스터를 의미합니다. 따라서 모든 함수에서는 Non-Volatile Register를 사용하기 전에 스택에 백업받고 리턴하기 전에 복원해주는 작업을 프롤로그/에필로그에서 수행합니다.
3) 스택 프레임 포인터(EBP)의 용도 변화
x86에서는 스택 베이스포인터(ebp)와 스택 포인터(esp)를 이용해 각 프레임 별로 사용중인 스택 영역을 확인할 수 있었습니다.
하지만 x64에서는 RBP 레지스터가 더이상 스택 프레임 포인터로 사용되지 않고 일반적인 목적으로 사용됩니다.
당연히 x64 함수에서는 더이상 다음과 같은 함수 프롤로그도 볼수 없겠군요. ^^a
push ebp
mov ebp,esp
..
2. 함수 호출규약의 변화
1) x86 : 스택 기반 아규먼트 전달
x86은 _cdecl이나 _stdcall 같은 호출 규약을 주로 사용했죠. 그래서 x86에서는 대부분의 함수 호출시 아규먼트가 스택을 통해 전달되었습니다. 이부분은 자료가 많으니 자세히 설명하기 보다는 어셈코드를 간단히 살펴보겠습니다.
일단 보시면... 5~7번째 아규먼트가 스택에 저장되고, 1~4번째 아규먼트는 RCX(ECX), RDX(EDX), R8, R9에 저장된 후 call되는 것을 확인할 수 있습니다. (좀 특이한 점은... x86과 달리 5~7번째 아규먼트가 스택에 push되지 않고 스택의 특정 주소로 mov 된다는 점인데 이것은 x64의 스택이 일단 함수의 프롤로그가 끝난 이후로는 늘어나거나 줄어들지 않는다는 특성 때문입니다.)
이렇게 전달된 아규먼트는 Callee 측에서 다음과 같이 레지스터를 이용해 꺼태올 수 있습니다.
x86에서는 일단 함수의 프롤로그 (push ebp, mov ebp esp...) 가 끝나고 나면 함수가 리턴할 때까지 ebp는 바뀌지 않지만 esp는 수시로 바뀝니다. (예를 들면, 자식함수 호출과정에서 파라메터 push할때...) 하지만x64에서는 일단 함수의 프롤로그가 끝나고 나면 함수가 리턴될 때까지 RSP가 바뀌지 않습니다.이 얘기는 뭐냐면...함수의 프롤로그에서 해당 함수에서 필요한 모든 스택 공간이 한꺼번에 확보된다는 것을 의미합니다.
예를 들어 어떤 함수가 다음과 같이 자식 함수를 두번 호출한다고 가정하면... 자식 함수를 두번 호출하는데 그 자식 함수들의 아규먼트가 5개, 7개이니 wmain의 프롤로그에서는 한번에 7개분의 스택 공간을 한꺼번에 확보해 버린다는 거죠. ^^
(1) x64에서 처음 네개의 파라메터는 레지스터를 통해 전달되지만, Caller 함수에서는 x86에서처럼 이 네개의 파라메터를 저장할 공간을 스택에 확보해놓는데 이 공간을 Parameter Homing Space라고 합니다.
위에서 언급한 바와 같이 x64의 각 함수에서 사용할 모든 스택 공간은 프롤로그에서 일괄 확보되므로 Parameter Homing Space 역시 프롤로그에서 확보됩니다.
(2) Callee에서는 레지스터 RCX, RDX, R8, R9를 통해 아규먼트를 전달받지만 필요에 따라 레지스터에 들어있는 아규먼트 값을 스택에 백업받는데, 이때 Caller에서 확보해놓은 Parameter Homing Space를 사용합니다. 즉, Parameter Homing Space는 Caller가 Callee를 위해 미리 준비해주는 메모리 공간이라고 볼수 있죠..
Parameter Homing Space가 필요한 이유는... 아마도... 이런게 아닐까 싶네요.
. 아규먼트 관련 레지스터를 다른 목적으로 사용하기 위해 백업받아야 하는 경우
. Callee 내에서 또다른 자식함수를 호출하기 위해 RCX, RDX, R8, R9를 사용해야 할 경우... Callee에서 리턴한 후에도 계속 아규먼트를 사용하기 위해 스택상의 어딘가에 백업받아야 함
. 어셈의 인스트럭션에 따라 아규먼트에 레지스터가 아닌 주소 값으로 접근해야 하는 경우가 있기 때문
만약 어떤 함수가 다른 자식함수를 5번 호출한다면... 백업할 공간을 각 자식함수에서 준비한다면 메모리 할당을 5번 해야되는 반면에 부모함수가 준비해주면 1번만 하면 되기 때문에... 이런 게 있는게 아닐까 하는 개인적인 상상을 해봅니다. (스택 메모리 할당은 그냥 레지스터에서 값을 빼는 단순한 작업이기 때문에 무슨 효과가 있을까 싶긴 하네요. ㅎㅎ)
3) 내부적으로 전혀 다른 함수를 호출하지 않는 함수(Leaf Function)의 경우 Parameter Homning space를 할당하지 않습니다. 하지만 자식함수를 하나라도 호출하는 함수라면(Non-Leaf Function) 자식함수 중 가장 많은 아규먼트가 4개 이하더라도 기본적으로 4개의 파라메터에 해당하는 Homing Space(8 * 4 = 32바이트)가 할당됩니다.
4) Parameter Homing Space는 Callee에서 다른 목적으로 사용될 수도 있습니다. 예를 들면 Callee가 RCX, RDX...가 아닌 다른 비휘발성 레지스터를 백업받는 데 그 공간을 사용할 수도 있는거죠.
The privilege escalation vulnerability actually exists withinendpointsecuritydand theSystemExtensions.frameworkit depends on. All of the communication above, between daemons, takes place using a low level system IPC mechanism calledXPC. TheSystemExtensions.frameworkprovides aOSSystemExtensionPointListenerclass used byendpointsecuritydto listen for the XPC activation requestssysextdsends. When theendpointsecurityddaemon starts up it does the following:
Apple’s Patch
With the release ofmacOS 10.15.1, Apple has patched this vulnerability. If we disassemble and reconstruct the code for[OSSystemExtensionPointListener listener:shouldAcceptNewConnection:]we can see the changes that they applied: