You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
`LD_PRELOAD=./hook.so ./validate_harness payload.json` exfiltrates the internal flag and confirms the crash oracle without patching the binary.
115
114
5.**Shrink the fuzz space.** Disassembly exposed an XOR key reused across the flag comparison, meaning the first seven bytes of `flag` were known. Only fuzz the nine unknown bytes.
116
-
6.**Embed fuzz bytes inside a valid JSON envelope.** The AFL harness reads exactly nine bytes from `stdin`, copies them into the flag suffix, and hard-codes every other field (constants, tree depths, arithmetic preimage). Any malformed read simply exits, so AFL spends cycles on meaningful testcases.
115
+
6.**Embed fuzz bytes inside a valid JSON envelope.** The AFL harness reads exactly nine bytes from `stdin`, copies them into the flag suffix, and hard-codes every other field (constants, tree depths, arithmetic preimage). Any malformed read simply exits, so AFL spends cycles on meaningful testcases:
117
116
118
117
<details>
119
-
<summary>AFL-friendly harness for structured JSON</summary>
118
+
<summary>Minimal AFL harness</summary>
120
119
121
120
```c
122
121
#include<stdint.h>
@@ -154,7 +153,62 @@ int main(void) {
154
153
155
154
This workflow lets you triage anti-debug-protected JNI validators quickly, leak secrets when needed, then fuzz only the meaningful bytes, all without touching the original APK.
156
155
157
-
### Related pages
156
+
## Pointer-Keyed Hash Table Pointer Leaks on Apple Serialization
157
+
158
+
### Requirements & attack surface
159
+
160
+
- A service accepts attacker-controlled property lists (XML or binary) and calls `NSKeyedUnarchiver.unarchivedObjectOfClasses` with a permissive allowlist (e.g., `NSDictionary`, `NSArray`, `NSNumber`, `NSString`, `NSNull`).
161
+
- The resulting objects are reused and later serialized again with `NSKeyedArchiver` (or iterated in deterministic bucket order) and sent back to the attacker.
162
+
- Some key type in the containers uses pointer values as its hash code. Before March 2025, `CFNull`/`NSNull` fell back to `CFHash(object) == (uintptr_t)object`, and deserialization always returned the shared-cache singleton `kCFNull`, giving a stable kernel-shared pointer without memory corruption or timing.
163
+
164
+
### Controllable hashing primitives
165
+
166
+
- **Pointer-based hashing:** `CFNull`’s `CFRuntimeClass` lacks a hash callback, so `CFBasicHash` uses the object address as the hash. Because the singleton lives at a fixed shared-cache address until reboot, its hash is stable across processes.
167
+
- **Attacker-controlled hashes:** 32-bit `NSNumber` keys are hashed through `_CFHashInt`, which is deterministic and attacker-controllable. Picking specific integers lets the attacker choose `hash(number) % num_buckets` for any table size.
168
+
- **`NSDictionary` implementation:** Immutable dictionaries embed a `CFBasicHash` with a prime bucket count chosen from `__CFBasicHashTableSizes` (e.g., 23, 41, 71, 127, 191, 251, 383, 631, 1087). Collisions are handled with linear probing (`__kCFBasicHashLinearHashingValue`), and serialization walks buckets in numeric order; therefore, the order of serialized keys encodes the bucket index that each key finally occupied.
169
+
170
+
### Encoding bucket indices into serialization order
171
+
172
+
By crafting a plist that materializes a dictionary whose buckets alternate between occupied and empty slots, the attacker constrains where linear probing can place `NSNull`. For a 7-bucket example, filling even buckets with `NSNumber` keys produces:
173
+
174
+
```text
175
+
bucket: 0 1 2 3 4 5 6
176
+
occupancy: # _ # _ # _ #
177
+
```
178
+
179
+
During deserialization the victim inserts the single `NSNull` key. Its initial bucket is `hash(NSNull) % 7`, but probing advances until hitting one of the open indices {1,3,5}. The serialized key order reveals which slot was used, disclosing whether the pointer hash modulo 7 lies in {6,0,1}, {2,3}, or {4,5}. Because the attacker controls the original serialized order, the `NSNull` key is emitted last in the input plist so the post-reserialization ordering is solely a function of bucket placement.
180
+
181
+
### Resolving exact residues with complementary tables
182
+
183
+
A single dictionary only leaks a range of residues. To determine the precise value of `hash(NSNull) % p`, build **two** dictionaries per prime bucket size `p`: one with even buckets pre-filled and one with odd buckets pre-filled. For the complementary pattern (`_ # _ # _ # _`), the empty slots (0,2,4,6) map to residue sets {0}, {1,2}, {3,4}, {5,6}. Observing the serialized position of `NSNull` in both dictionaries narrows the residue to a single value because the intersection of the two candidate sets yields a unique `r_i` for that `p`.
184
+
185
+
The attacker bundles all dictionaries inside an `NSArray`, so a single deserialize → serialize round trip leaks residues for every chosen table size.
186
+
187
+
### Reconstructing the 64-bit shared-cache pointer
188
+
189
+
For each prime `p_i ∈ {23, 41, 71, 127, 191, 251, 383, 631, 1087}`, the attacker recovers `hash(NSNull) ≡ r_i (mod p_i)` from the serialized ordering. Applying the Chinese Remainder Theorem (CRT) with the extended Euclidean algorithm yields:
so the combined residue uniquely equals the 64-bit pointer to `kCFNull`. The Project Zero PoC iteratively combines congruences while printing intermediate moduli to show convergence toward the true address (`0x00000001eb91ab60` on the vulnerable build).
196
+
197
+
### Practical workflow
198
+
199
+
1.**Generate crafted input:** Build the attacker-side XML plist (two dictionaries per prime, `NSNull` serialized last) and convert it to binary format.
2.**Victim round trip:** The victim service deserializes with `NSKeyedUnarchiver.unarchivedObjectOfClasses` using the allowed classes set `{NSDictionary, NSArray, NSNumber, NSString, NSNull}` and immediately re-serializes with `NSKeyedArchiver`.
206
+
3.**Residue extraction:** Converting the returned plist back to XML reveals the dictionary key ordering. A helper such as `extract-pointer.c` reads the object table, determines the index of the singleton `NSNull`, maps each dictionary pair back to its bucket residue, and solves the CRT system to recover the shared-cache pointer.
207
+
4.**Verification (optional):** Compiling a tiny Objective-C helper that prints `CFHash(kCFNull)` confirms the leaked value matches the real address.
208
+
209
+
No memory safety bug is required—simply observing serialization order of pointer-keyed structures yields a remote ASLR bypass primitive.
0 commit comments