<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://gtsintzas.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://gtsintzas.github.io/" rel="alternate" type="text/html" /><updated>2026-10-04T16:25:33+00:00</updated><id>https://gtsintzas.github.io/feed.xml</id><title type="html">Ioannis Tsintzas</title><subtitle>Personal site of Ioannis Tsintzas: CTF writeups, projects and more.</subtitle><entry><title type="html">Gone Not Forgotten - Recovering a Deleted Message from SQLite Free Space</title><link href="https://gtsintzas.github.io/gone-not-forgotten" rel="alternate" type="text/html" title="Gone Not Forgotten - Recovering a Deleted Message from SQLite Free Space" /><published>2026-09-28T00:00:00+00:00</published><updated>2026-09-28T00:00:00+00:00</updated><id>https://gtsintzas.github.io/gone-not-forgotten</id><content type="html" xml:base="https://gtsintzas.github.io/gone-not-forgotten"><![CDATA[<h2 id="tldr">TL;DR</h2>
<p>(This is a writeup/walkthrough of a medium difficulty forensics CTF challenge, from H7 CTF Quals 2026.)</p>

<p>When content is deleted from an SQLite database, the content is not usually erased but rather the space used to hold the content is marked as being available for reuse. This can allow deleted content to be recovered by a hacker or by forensic analysis. I showcase a simple CTF example of this.</p>

<h2 id="the-scenario">The Scenario</h2>
<p>We are given the following description.</p>

<blockquote>
  <p>A phone lands on the evidence bench for a harassment case, and the suspect is adamant they never sent anything ugly. Nova Messenger agrees with them: nothing there.</p>

  <p>Deleting a thing and being rid of it were never the same move.</p>
</blockquote>

<p>And two files, <code class="language-plaintext highlighter-rouge">messages.db</code> and a <code class="language-plaintext highlighter-rouge">secure.xml</code> file with these contents</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version='1.0' encoding='utf-8' standalone='yes' ?&gt;</span>
<span class="nt">&lt;map&gt;</span>
    <span class="nt">&lt;string</span> <span class="na">name=</span><span class="s">"obfuscation_key"</span><span class="nt">&gt;</span>n0v4ch4t<span class="nt">&lt;/string&gt;</span>
    <span class="nt">&lt;string</span> <span class="na">name=</span><span class="s">"stored_body_encoding"</span><span class="nt">&gt;</span>hex(xor(body, obfuscation_key))<span class="nt">&lt;/string&gt;</span>
    <span class="nt">&lt;boolean</span> <span class="na">name=</span><span class="s">"biometric_lock"</span> <span class="na">value=</span><span class="s">"true"</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;/map&gt;</span>
</code></pre></div></div>

<p>So, let’s think about this. We have a phone in our hands, and we dig into a messaging app. We find the 2 files mentioned above, and we now want to do some forensics analysis to prove that the suspect did send something ugly.</p>

<p><code class="language-plaintext highlighter-rouge">secure.xml</code> seems to contain encoding information, which will probably be useful down the line, but it doesnt help us a lot by itself. We’re gonna have to dig into the <code class="language-plaintext highlighter-rouge">messages.db</code> file.</p>

<h2 id="analysis">Analysis</h2>
<p>Before doing anything let’s see what we are working with. It is always a good idea to run <code class="language-plaintext highlighter-rouge">file</code> on whatever he have on our hands.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>file messages.db
messages.db: SQLite 3.x database, last written using SQLite version 3039002, file counter 9, database pages 3, cookie 0x1, schema 4, largest root page 3, UTF-8, version-valid-for 9
</code></pre></div></div>

<p>So we have an SQLite3 database. Let’s inspect further and see what tables it contains.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sqlite</span><span class="o">&gt;</span> <span class="p">.</span><span class="n">tables</span>
<span class="n">messages</span>

<span class="n">sqlite</span><span class="o">&gt;</span> <span class="p">.</span><span class="k">schema</span> <span class="s1">'messages'</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">messages</span><span class="p">(</span><span class="n">_id</span> <span class="nb">INTEGER</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span> <span class="n">thread</span> <span class="nb">TEXT</span><span class="p">,</span> <span class="n">sender</span> <span class="nb">TEXT</span><span class="p">,</span> <span class="n">ts</span> <span class="nb">INTEGER</span><span class="p">,</span> <span class="n">body</span> <span class="nb">TEXT</span><span class="p">);</span>
</code></pre></div></div>

<p>Alright. A single “messages” table. Seems reasonable, coming out of a file called messages.db. Let’s the the rows.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sqlite</span><span class="o">&gt;</span> <span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="s1">'messages'</span><span class="p">;</span>
<span class="mi">1</span><span class="o">|</span><span class="k">general</span><span class="o">|</span><span class="n">alice</span><span class="o">|</span><span class="mi">1710001000000</span><span class="o">|</span><span class="n">lunch</span> <span class="k">at</span> <span class="mi">1</span><span class="o">?</span>
<span class="mi">2</span><span class="o">|</span><span class="k">general</span><span class="o">|</span><span class="n">bob</span><span class="o">|</span><span class="mi">1710001100000</span><span class="o">|</span><span class="n">sure</span><span class="p">,</span> <span class="n">the</span> <span class="n">usual</span> <span class="n">spot</span>
<span class="mi">3</span><span class="o">|</span><span class="k">general</span><span class="o">|</span><span class="n">alice</span><span class="o">|</span><span class="mi">1710001200000</span><span class="o">|</span><span class="n">cool</span><span class="p">,</span> <span class="n">see</span> <span class="n">you</span>
<span class="mi">5</span><span class="o">|</span><span class="n">ops</span><span class="o">|</span><span class="n">mallory</span><span class="o">|</span><span class="mi">1710002050000</span><span class="o">|</span><span class="n">burn</span> <span class="n">this</span> <span class="k">after</span> <span class="n">you</span> <span class="k">read</span> <span class="n">it</span>
<span class="mi">6</span><span class="o">|</span><span class="k">general</span><span class="o">|</span><span class="n">bob</span><span class="o">|</span><span class="mi">1710002100000</span><span class="o">|</span><span class="n">anyone</span> <span class="n">seen</span> <span class="n">the</span> <span class="n">build</span><span class="o">?</span>
<span class="mi">7</span><span class="o">|</span><span class="k">general</span><span class="o">|</span><span class="n">carol</span><span class="o">|</span><span class="mi">1710002200000</span><span class="o">|</span><span class="n">ci</span> <span class="k">is</span> <span class="n">green</span> <span class="n">now</span>
</code></pre></div></div>

<p>Okay. Now we have a much better idea of what is going on. We notice that message with _id 4 is missing, and message 5 is saying “burn this after you read it”</p>

<p>That is suspicious, to say the least. We are obviously gonna have to try and recover message 4.</p>

<h2 id="recovery">Recovery</h2>
<p>It is mentioned in several pages of SQLite docs that deleted data may still be intact. <a href="https://www.sqlite.org/recovery.html">Example 1</a>, <a href="https://sqlite.org/lang_vacuum.html">Example 2</a></p>

<p>So let’s try the simplest of things first, before doing anything sophisticated. Good old <a href="https://man7.org/linux/man-pages/man1/strings.1.html">strings</a>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ strings messages.db
SQLite format 3
Ktablemessagesmessages
CREATE TABLE messages(_id INTEGER PRIMARY KEY, thread TEXT, sender TEXT, ts INTEGER, body TEXT)
+generalcarol
ci is green now,
9generalbob
W anyone seen the build?1
Copsmallory
burn this after you read it
yopsmallory
26073560251303150a564f025a5d521658054357545d064c5f000b%
'generalalice
cool, see you*
5generalbob
sure, the usual spot#
#generalalice
@lunch at 1?
</code></pre></div></div>

<p>Now, we immediately notice something interesting. Right after the suspicious message, we see this string: <code class="language-plaintext highlighter-rouge">26073560251303150a564f025a5d521658054357545d064c5f000b%</code></p>

<p>This could very well be the deleted message 4. But it is obviously encoded/encrypted in some way. This is the time for secure.xml to shine.</p>

<p>Looking back at the xml:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;string</span> <span class="na">name=</span><span class="s">"obfuscation_key"</span><span class="nt">&gt;</span>n0v4ch4t<span class="nt">&lt;/string&gt;</span>
<span class="nt">&lt;string</span> <span class="na">name=</span><span class="s">"stored_body_encoding"</span><span class="nt">&gt;</span>hex(xor(body, obfuscation_key))<span class="err">&lt;</span>/
</code></pre></div></div>

<p>We know that both hex and xor are reversible operations. Let’s try to decode the mysterious string.</p>

<p>The hypothesis is that message 4 is encrypted as:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ct = hex(xor(pt, n0v4ch4t))
</code></pre></div></div>

<p>Therefore the plaintext should be</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pt = xor(fromhex(ct), n0v4ch4t)
</code></pre></div></div>

<p>Let’s put this into the test. We can simply use <a href="https://gchq.github.io/CyberChef/">CyberChef</a>, a tool that will do all decoding for us!</p>

<p><img src="/assets/posts/gone-not-forgotten/image.png" alt="Yes" /></p>

<p>That was it! We got ourselves a flag!</p>

<p>flag: <code class="language-plaintext highlighter-rouge">H7CTF{7adf9695fb655c752810}</code></p>

<h2 id="takeaways">Takeaways</h2>
<p>This is a beginner friendly challenge, but we can learn a few things from it.</p>

<ul>
  <li>
    <p>Don’t blindly trust an app showing no data, especially if you have a reason to suspect that something might be there. Always dig deeper.</p>
  </li>
  <li>
    <p>Just because something is deleted, doesn’t mean it’s truly gone. To be safe, set  <a href="https://sqlite.org/pragma.html#pragma_secure_delete">PRAGMA secure_delete=ON</a> so that deleted content actually gets overwritten with zeros.</p>
  </li>
</ul>]]></content><author><name></name></author><category term="Forensics, H7CTF 2026" /><summary type="html"><![CDATA[TL;DR (This is a writeup/walkthrough of a medium difficulty forensics CTF challenge, from H7 CTF Quals 2026.)]]></summary></entry><entry><title type="html">Hello World!</title><link href="https://gtsintzas.github.io/hello-world" rel="alternate" type="text/html" title="Hello World!" /><published>2026-08-10T00:00:00+00:00</published><updated>2026-08-10T00:00:00+00:00</updated><id>https://gtsintzas.github.io/hello-world</id><content type="html" xml:base="https://gtsintzas.github.io/hello-world"><![CDATA[<p>Hello world! This is the first post. Still figuring out things. I am getting there :P</p>]]></content><author><name></name></author><category term="General" /><summary type="html"><![CDATA[Hello world! This is the first post. Still figuring out things. I am getting there :P]]></summary></entry></feed>