{
  "version": "https://jsonfeed.org/version/1",
  "title": "Forensics on LLBBL Blog",
  "icon": "https://avatars.micro.blog/avatars/2023/40/125738.jpg",
  "home_page_url": "https://llbbl.blog/",
  "feed_url": "https://llbbl.blog/feed.json",
  "items": [
      {
        "id": "http://llbbl.micro.blog/2026/08/26/your-pdf-remembers-what-you.html",
        "title": "Your PDF Remembers What You Deleted",
        "content_html": "<p>A PDF does not have a current version. It has a stack of them, and the one your reader shows you is whichever one is on top.</p>\n<p>That is not a bug, but a clause in the specification, it is the reason PDFs can be signed and annotated without invalidating anything that came before, and it is also why a court filing with black rectangles over the sensitive parts keeps handing those parts to anyone who runs <code>pdftotext</code>.</p>\n<p>I built a 619-byte PDF by hand to show the mechanism, because the mechanism is small enough to fit in a blog post.</p>\n<hr>\n<h2 id=\"read-it-from-the-bottom\">Read It From the Bottom</h2>\n<p>Here is the entire tail of that file:</p>\n<pre tabindex=\"0\"><code>xref\n0 6\n0000000000 65535 f \n0000000015 00000 n \n0000000064 00000 n \n0000000121 00000 n \n0000000247 00000 n \n0000000366 00000 n \ntrailer\n&lt;&lt; /Size 6 /Root 1 0 R &gt;&gt;\nstartxref\n436\n%%EOF\n</code></pre><p>A parser opens this file by seeking to the end, finding <code>%%EOF</code>, reading the number above it, and jumping to byte 436. That is the cross-reference table. Every line in it is a ten-digit zero-padded byte offset for one object. Object 4 lives at byte 247. Object 5 lives at byte 366. The trailer then says the document catalog is object 1, and the parser follows the graph from there.</p>\n<p>Nothing is read sequentially. There is no &ldquo;parse the header, then stream forward.&rdquo; The header is four bytes of version number and a comment containing four deliberately non-ASCII bytes, present only so that any transport handling the file is forced to treat it as binary.</p>\n<p>If you read the <a href=\"https://llbbl.blog/2026/08/17/zip-files-are-read-backwards.html\">ZIP post</a> earlier this month, this should feel familiar. Both formats put their index at the end so that appending is cheap. Both formats therefore have the same structural property: the index is authoritative, and the bytes it doesn&rsquo;t point at are still sitting right there in the file.</p>\n<hr>\n<h2 id=\"the-black-box-is-a-rect\">The Black Box Is a Rect</h2>\n<p>Start with the failure, the one that has been in the news.</p>\n<p>My page has a single content stream with a text-drawing operator in it:</p>\n<pre tabindex=\"0\"><code>BT /F1 12 Tf 20 60 Td (CONFIDENTIAL: Q3 layoffs begin March 14) Tj ET\n</code></pre><p>To redact it, I append a filled black rectangle covering the same coordinates:</p>\n<pre tabindex=\"0\"><code>0 0 0 rg 15 52 270 24 re f\n</code></pre><p>On screen that is a black bar. The text is underneath it and completely invisible. Then:</p>\n<pre tabindex=\"0\"><code>$ pdftotext overlay.pdf -\nCONFIDENTIAL: Q3 layoffs begin March 14\n</code></pre><p>The rectangle is a drawing instruction. <code>Tj</code> is a different drawing instruction. Text extraction never rasterizes anything, so it never learns that one shape happens to sit on top of another. It walks the content stream, collects the text operators, and hands them over.</p>\n<p>This is what Paul Manafort&rsquo;s lawyers filed in January 2019. The blacked-out passages came straight back out with copy and paste, and what came out was that Manafort had shared 2016 campaign polling data with Konstantin Kilimnik. A corrected version went up quickly.</p>\n<p>It is also the <a href=\"https://www.cbsnews.com/news/unredacted-tsa-manual-leaked-online/\">93-page TSA screening manual</a> posted to a federal contracting site in December 2009, with black boxes over the tolerances used by airport metal and explosive detectors and over the special handling rules for CIA officers, diplomats, and law enforcement. Same mistake, ten years earlier, and the second time nobody could claim the technique was novel.</p>\n<hr>\n<h2 id=\"doing-it-properly-and-still-losing\">Doing It Properly and Still Losing</h2>\n<p>Now the interesting case. This time I actually replace the content stream. New object 4, no text operator, only the rectangle. The old object stays where it is, because PDF&rsquo;s editing model appends:</p>\n<pre tabindex=\"0\"><code>$ pdftotext redacted.pdf -\n$\n</code></pre><p>Empty. The text is gone from the document as the parser understands it. This is an edit, not an overlay, and every viewer will agree the page contains a black bar and nothing else.</p>\n<pre tabindex=\"0\"><code>$ strings redacted.pdf | grep -i confidential\nBT /F1 12 Tf 20 60 Td (CONFIDENTIAL: Q3 layoffs begin March 14) Tj ET\n</code></pre><p>The file is 812 bytes. The first 616 of them are the original document, untouched, including its own <code>xref</code>, its own trailer, and its own <code>%%EOF</code>. The new revision was appended after that:</p>\n<pre tabindex=\"0\"><code>$ grep -abo &#39;%%EOF&#39; redacted.pdf\n613:%%EOF\n806:%%EOF\n</code></pre><p>Two end-of-file markers in one file. The second trailer carries a <code>/Prev</code> key pointing back at the first cross-reference table:</p>\n<pre tabindex=\"0\"><code>trailer\n&lt;&lt; /Size 6 /Root 1 0 R /Prev 436 &gt;&gt;\nstartxref\n695\n%%EOF\n</code></pre><p>So recovering the unredacted document is not forensics. It is <code>head</code>:</p>\n<pre tabindex=\"0\"><code>$ head -c 616 redacted.pdf &gt; recovered.pdf\n$ pdftotext recovered.pdf -\nCONFIDENTIAL: Q3 layoffs begin March 14\n</code></pre><p>That is a valid, complete, openable PDF. Not a fragment, not a carved string. The original revision was always a self-contained file; the redaction just parked another file behind it.</p>\n<p>You can see why the format works this way. Digital signatures need the signed byte range to stay byte-identical, so an annotation or a form fill has to append rather than rewrite. Incremental save is also why a 400-page PDF takes a moment to annotate instead of a minute. The design is coherent. It just means that &ldquo;save&rdquo; and &ldquo;erase&rdquo; are unrelated operations, and most software offers you the first one while you are thinking about the second.</p>\n<hr>\n<h2 id=\"hard-to-get-right\">Hard to Get Right</h2>\n<p>In 2021 Supriya Adhatarao and Cédric Lauradoux collected 39,664 PDF files published by 75 security agencies across 47 countries and looked at what was still in them. Seven of the 75 agencies had made any attempt at sanitization at all. Of the files those seven had sanitized, <a href=\"https://arxiv.org/abs/2103.02707\">65% still contained sensitive information</a>.</p>\n<p>These are security agencies. Sanitizing documents is a thing they have written policies about. The success rate among the small minority that tried was roughly one in three.</p>\n<p>The reason is not incompetence. It is that the safe operation and the obvious operation are different operations, and the file gives you no feedback about which one you performed. Both produce a document with a black bar on it.</p>\n<hr>\n<h2 id=\"what-to-do-about-it\">What To Do About It</h2>\n<ul>\n<li><strong>Never redact by drawing.</strong> If the feature lives in the same menu as shapes, highlights, and stamps, it is a shape. A real redaction tool deletes the content stream operators; an annotation tool adds one on top.</li>\n<li><strong>Flatten and rewrite the whole file afterward.</strong> <code>qpdf --linearize in.pdf out.pdf</code> rebuilds the document as a single revision and drops what nothing points at. If the output still has two <code>%%EOF</code> markers, the collapse did not happen.</li>\n<li><strong>Check your work with <code>strings</code> and <code>pdftotext</code>.</strong> Both take ten seconds. Between them they would have caught every failure in this post.</li>\n<li><strong>Count the <code>%%EOF</code> markers</strong> on any PDF you receive, not just the ones you send. Use <code>grep -ac '%%EOF' file.pdf</code>; without <code>-a</code>, grep decides the file is binary and reports nothing at all.</li>\n<li><strong>Export to images and re-OCR</strong> when the stakes are high enough. It destroys the text layer along with everything else, which is the point.</li>\n<li><strong>Strip metadata separately.</strong> Author names, the software that produced the file, and the local file path of the original are in the <code>/Info</code> dictionary and the XMP packet, and none of that is touched by redacting page content.</li>\n</ul>\n<p>The formats in the first half of this series failed by being unclear about what their bytes meant. PDF is not unclear about anything. It says precisely where every object is and precisely which ones are current, and it says it in a structure that keeps the non-current ones exactly where they were. The file is honest. It is the word &ldquo;redacted&rdquo; that is doing the lying.</p>\n<h2 id=\"sources\">Sources</h2>\n<ul>\n<li><a href=\"https://www.iso.org/standard/51502.html\">ISO 32000-1:2008</a> — the PDF 1.7 specification; clause 7.5.4 covers the cross-reference table, 7.5.6 covers incremental updates</li>\n<li><a href=\"https://www.iso.org/standard/75839.html\">ISO 32000-2:2020</a> — PDF 2.0</li>\n<li><a href=\"https://arxiv.org/abs/2103.02707\">Adhatarao &amp; Lauradoux, &ldquo;Exploitation and Sanitization of Hidden Data in PDF Files&rdquo;</a> — 39,664 files from 75 security agencies; 7 attempted sanitization, 65% of those still leaked</li>\n<li><a href=\"https://www.loc.gov/preservation/digital/formats/fdd/fdd000318.shtml\">Library of Congress format description for the PDF family</a> — history and revision structure</li>\n<li><a href=\"https://www.buzzfeednews.com/article/zoetillman/paul-manafort-redacted-konstanin-kilimnik-lying\">BuzzFeed News on the Manafort filing</a> — January 2019</li>\n<li><a href=\"https://www.cbsnews.com/news/unredacted-tsa-manual-leaked-online/\">CBS News on the unredacted TSA manual</a> — December 2009</li>\n</ul>\n<blockquote>\n<p>I&rsquo;d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at <a href=\"https://micro.blog/llbbl?remote_follow=1\">@logan@llbbl.blog</a>.</p>\n</blockquote>\n",
        "date_published": "2026-08-26T10:00:00-05:00",
        "url": "https://llbbl.blog/2026/08/26/your-pdf-remembers-what-you.html",
        "tags": ["Programming","security","File-formats","Pdf","Forensics"]
      }
  ]
}
