# Exploiting aCropalypse: Recovering Truncated PNGs

Android Markup screenshot editor wrote cropped PNGs without O_TRUNC, leaving original pixels in the file tail; truncated zlib streams can be partially recovered to un-crop redacted screenshots.

- Payout: Research
- Severity: high
- Program: Google VRP
- Write-up by: David Buchanan
- Disclosed: 2023-03-18
- CVE: CVE-2023-21036
- Write-up quality: 8/10
- Original: https://www.da.vidbuchanan.co.uk/blog/exploiting-acropalypse.html
- Library page: https://bugbountylibrary.com/r/blog%3Ada-vidbuchanan-co-uk-blog-exploiting-acropalypse-html
- Tags: area:mobile domain:mobile impact:data-leak impact:pii-leak kind:research platform:research sev:high tech:android technique:reverse-engineering technique:source-review vuln:info-disclosure

**Target:** Android Markup screenshot editor / PNG file writer
**Root cause:** Markup opened the output file with parseMode("w") instead of "wt", so O_TRUNC was not applied; writing a smaller cropped image left the original file's tail intact.
**Steps:**
1. Crop and save a screenshot in Android Markup; observe the file retains trailing bytes of the original PNG.
2. Parse the truncated PNG's IDAT zlib stream from the leftover tail.
3. For each bit-offset, check if it looks like the start of a dynamic Huffman block; skip otherwise.
4. Pre-initialise zlib state with 32KB of 'X' to satisfy backreferences before the start.
5. Decompress from that bit offset; if output looks plausible, return it; else continue on error.
6. Reconstruct PNG rows from recovered decompressed data to view the un-cropped region.
**How found:** Simon asked abstractly whether a zlib stream missing its first ~0x10000 bytes but with 0x100000 trailing bytes could be decompressed; Retr0id reasoned it was hard generally but feasible given the specific scenario, then found via a zlib-parsing script that the Huffman tree is re-specified every ~16KB, reducing the problem to locating a dynamic-Huffman block start.
**Exploitation:** Attacker obtains a cropped screenshot shared by a victim (e.g. via Discord) and runs the recovery tool to decompress the leftover tail, revealing content the victim cropped out such as a postal address.
**Insight:** When a compressed stream is truncated at the front, dynamic Huffman trees re-specified per block (~16KB in zlib) let you brute-force bit-offsets for a valid block start; pre-seeding the decompressor window with filler bytes satisfies backreferences and makes missing regions visually obvious.

## Proof of concept (copied exactly from the original report)

**Pseudocode for brute-forcing zlib block start**

```
for each bit-offset:
 if it doesn't look like the start of a dynamic huffman block:
 skip this offset
 
 try decompressing from that offset:
 if the decompressed data looks plausible:
 return decompressed data!
 catch decompression errors:
 continue
```

**Pre-initializing zlib window with 'X' bytes**

```
To work around this, I pre-initialised zlib's state with 32KB of the ascii character 'X'. This was an arbitrary choice, but it's convenient because it means that any of these "made up" byte values will show up as a uniform grey colour in the final PNG.
```

