SkillAgentSearch skills...

C0ntextomy

CVE-2020-9992 - A design flaw in MobileDevice.framework/Xcode and iOS/iPadOS/tvOS Development Tools allows an attacker in the same network to gain remote code execution on a target device

Install / Use

/learn @c0ntextomy/C0ntextomy
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

c0ntextomy

an informal fallacy and a type of false attribution in which a passage is removed from its surrounding matter in such a way as to distort its intended meaning

Wikipedia - Contextomy

A Proof of Concept demonstrating the vulnerability with a debug session hijack, remote code execution, and sensitive data exfiltration.

Advisory

A design flaw in macOS'/Xcode's MobileDevice.framework and the Development Tools for iOS/iPadOS/tvOS results in clear text communication over the network, despite the service connection setup performing an actual SSL handshake.

For further details about the vulnerability, the affected components and versions refer to advisory.md.

Authors

  • Dany Lisiansky (@danyl931), Independent Security Researcher
  • Nikias Bassen (@pimskeks), Security Researcher and VP of Product Security, ZIMPERIUM zLabs

Exploitation

So we have plain-text remote debugging sessions over the network, how can we exploit this?

In theory, it would be enough to manipulate a single packet to inject or replace a shellcode sent and executed by different lldb operations (see symbol lookup as an example). While this is an impressive goal to meet, it would require us to specially craft and produce device/state dependent shellcodes in real-time which would make it difficult for others to reproduce in different environments. Instead, we opted to a more reliable, device/state agnostic and easily reproducible approach allowing us to attach a second, fully working lldb client to the process while keeping the original client unaware.

Note that we were mostly focused on gaining code execution on the device side - but it's possible to attack the client as well. In fact, we accidentally crashed the client more than once during the implementation of this attack.

We also tried to simulate a real-world environment - an intruded local network inside an enterprise or a small startup which develops an AR/Fitness themed app, an app that communicates with an accessory attached via the lightning port or a tvOS app - all remotely debugged.

Gaining control over the session

One option we first considered was to spoof mDNS records which are broadcasted by network devices to allow service discovery. We decided against it because mDNS records are likely to be cached and it might be difficult to change them after the fact. Instead, we went with an ARP spoofing attack - a classic method that abuses the Address Resolution Protocol to associate IPv4 addresses with a MAC address of a machine we control and allowed us to redirect all victims' traffic through that machine.

Note: While this specifically targets IPv4 networks, it is possible to use other kinds of attacks to target IPv6 networks as well (e.g. by attacking the NDP protocol or by propagating from other network positions). We used this method because it can also be easily delivered by publicly available tools and reproduced by others.

Next, we need to perform an active Man-in-the-Middle (MITM) attack and gain control over the session itself - this is usually done by registering a simple firewall redirection rule to redirect traffic from a known destination address and port to a server the attacker controls. Unfortunately, the destination port of debugserver is unknown since it is dynamically allocated and sent by lockdownd through a (properly) secured connection. Because we couldn't predict the port, we ended up writing an "on-demand" service to dynamically register firewall rules and spawn servers based on TCP SYN packets sent from our victims to each other and gained control over all TCP sessions. (For implementation details refer to the NFQUEUE callback function OnPacket found in /c0ntextomy/src/exploit/exploit.go)

Detecting a gdb-remote session

At this point we reliably gained control over all TCP sessions between our victims, but how can we distinguish between a gdb-remote session and others? Thankfully the protocol defines an easy to identify handshake which initializes the session (as can be seen below) and followed by additional packets that exchange information about the supported server/client features, negotiating compression methods, the remote architecture, and general information about the process being debugged.

Client -> Server: $QStartNoAckMode#b0
Server -> Client: +$OK#9a
Client -> Server: +
Client -> Server: $qSupported:xmlRegisters=i386,arm,mips#12
Server -> Client: $NqXfer:features:read+;PacketSize=20000;qEcho+;SupportedCompressions=lzfse,zlib-deflate,lz4,lzma;DefaultCompressionMinSize=384#00
Client -> Server: $QEnableCompression:type:lzfse;#bf
...

The right point in time to manipulate the session

The session is now in the initialization phase, the client sends various configuration packets, sets internal breakpoints, and so on. If we will try to manipulate it at this stage we are likely to break the session and make the victim notice. So how can we make sure the session is fully initialized? The same way Xcode does. During initialization Xcode enables the async process profiling feature by sending the following packet:

Client -> Server: $QSetEnableAsyncProfiling;enable:1;interval_usec:1000000;scan_type:0xfffffbff;#fd
Server -> Client: $OK#00

This configures debugserver to periodically send profiling packets (as seen below) and utilized by Xcode to present telemetry information about system resources used by the process.

Server -> Client: $Anum_cpu:2;host_user_ticks:92538;host_sys_ticks:0;host_idle_ticks:685745;elapsed_usec:1583325511341698;task_used_usec:0;thread_used_id:3c1e;thread_used_usec:323080;thread_used_name:;thread_used_id:3ceb;thread_used_usec:1753;thread_used_name:;thread_used_id:3cec;thread_used_usec:2464;thread_used_name:;thread_used_id:3ced;thread_used_usec:139;thread_used_name:;thread_used_id:3cee;thread_used_usec:67;thread_used_name:;thread_used_id:3cef;thread_used_usec:2798;thread_used_name:636f6d2e6170706c652e75696b69742

When Xcode receives the first profiling packet it considers the process as running, presents the debugging UI, and allows the user to manually interrupt it.

Preparing the session for a second client whilst keeping the original client unaware

Now that we found the right point in time for the session takeover we need to prepare the session to accept a second client. First, we wait for the profiling packet to arrive and save it so we could send it back later on. Next, we send a process interrupt packet (with the value of \x03 - surprisingly equivalent to the SIGQUIT signal) to stop the process. This is important because when lldb tries to connect it expects the process to be stopped. However, the process interruption itself sends state packets back to the client. Since we want the original client to be unaware of the hijack we separate it from the session and instead start replaying the profiling packet we saved earlier back to the original client.

Once debugserver finished sending all state packets the session is ready to accept a second client, almost.

Attacker joins the party

We are in control of the session, the process is now stopped, and everything is supposed to be ready. But how do we join a second client?

Our first attempt was to spawn a server, use the gdb-remote HOST:PORT command to connect the second client, and start forwarding the packets straight to debugserver. Unfortunately, the new client was unaware of the already initialized session and tried to perform a handshake. Because debugserver was already initialized it simply ignored our new client and it failed to connect.

(lldb) gdb-remote HOST:PORT
error: failed to get reply to handshake packet

On the second attempt we waited for the new client to perform the handshake, but instead of forwarding the packets straight to debugserver we first manually handled the handshake by sending the expected replies back to the client. This time we were greeted with the familiar lldb shell. But does it work? Yes, yes it does.

(lldb) gdb-remote HOST:PORT
(lldb) th ba
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
  * frame #0: 0x00000001896bb5f4 libsystem_kernel.dylib`mach_msg_trap + 8
    frame #1: 0x00000001896baa60 libsystem_kernel.dylib`mach_msg + 72
    frame #2: 0x0000000189862068 CoreFoundation`__CFRunLoopServiceMachPort + 216
    frame #3: 0x000000018985d188 CoreFoundation`__CFRunLoopRun + 1444
    frame #4: 0x000000018985c8bc CoreFoundation`CFRunLoopRunSpecific + 464
    frame #5: 0x00000001936c8328 GraphicsServices`GSEventRunModal + 104
    frame #6: 0x000000018d8f26d4 UIKitCore`UIApplicationMain + 1936
    frame #7: 0x000000010008e2e4 project`main + 132
    frame #8: 0x00000001896e7460 libdyld.dylib`start + 4

At this stage, we have a full session takeover in place, and basically just pipe packets between debugserver and the new client. For each packet forwarded to debugserver, we also send back the saved profiling packet to the original client to keep it happy. At the same time we ignore all packets coming from the original client.

A more practical way to join a second client

On our previous attempt, we spawned a server and manually connected a new client using the gdb-remote HOST:PORT command. While it works, it also makes it considerably difficult to handle. Since we already know Xcode passes a raw socket to the lldb-rpc-server process we knew there is a more practical way to achieve this. After digging deeper we learned that gdb-remote was implemented as a process connect plugin and indeed supports an additional option that accepts file descriptors. It is possible to access this option both through lldb's API (using the ConnectRemote method of the class SBTarget) and also str

View on GitHub
GitHub Stars74
CategoryDevelopment
Updated20d ago
Forks11

Languages

C

Security Score

80/100

Audited on Mar 8, 2026

No findings