<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: rudy_candy</title>
    <description>The latest articles on DEV Community by rudy_candy (@rudycandy).</description>
    <link>https://dev.to/rudycandy</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3889369%2F530605a8-564f-4054-9a9b-1d3e58ffc3c6.jpeg</url>
      <title>DEV Community: rudy_candy</title>
      <link>https://dev.to/rudycandy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rudycandy"/>
    <language>en</language>
    <item>
      <title>What people get wrong about penetration testing</title>
      <dc:creator>rudy_candy</dc:creator>
      <pubDate>Mon, 08 Jun 2026 20:48:20 +0000</pubDate>
      <link>https://dev.to/rudycandy/what-people-get-wrong-about-penetration-testing-1pc2</link>
      <guid>https://dev.to/rudycandy/what-people-get-wrong-about-penetration-testing-1pc2</guid>
      <description>&lt;p&gt;Before I became a vulnerability assessor I had the job slightly wrong in my head. If you only know security from films and TV, you probably do too. So here's the reality, including the parts that caught me off guard once I was actually doing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reality is shockingly boring
&lt;/h2&gt;

&lt;p&gt;The picture most people have is someone hammering a keyboard while text streams down the screen and they elegantly break into a system. That's not it.&lt;/p&gt;

&lt;p&gt;Most of the work is taking nearly identical requests, changing one small thing, and comparing how the response differs. Change a parameter, send it, look at the result. Change it again, send, look. Over and over. You intercept a request in a tool like Burp Suite, edit it by hand, and check whether the behavior shifts, one at a time. There's no glamour anywhere in it.&lt;/p&gt;

&lt;p&gt;I'll be honest, at first it felt like a letdown. But noticing those tiny differences turned out to be its own kind of fun, and I got pulled in. These days I think whether you can find that boring work interesting is the real test of fit for the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  I didn't expect writing to be the hard part
&lt;/h2&gt;

&lt;p&gt;This one I genuinely didn't see coming. Finding a vulnerability isn't the end of the job.&lt;/p&gt;

&lt;p&gt;You have to explain where it is, what the problem is, how to reproduce it, and how dangerous it is, in words the other person can act on. That's the report. It doesn't matter how clever the bug is: if the developer reading it can't reproduce it, you get back "is this actually a vulnerability?" The job needs the hands-on skill and the ability to put it into writing. For someone who assumed it was a purely technical job, that was the biggest surprise.&lt;/p&gt;

&lt;h2&gt;
  
  
  You learn you can't say "it's safe"
&lt;/h2&gt;

&lt;p&gt;Here's the one whose weight I only felt after starting. When an assessment turns up no vulnerabilities, you still can't say "this system is safe."&lt;/p&gt;

&lt;p&gt;What you can say is that within the agreed time, scope, and methods, you didn't find anything. The chance you missed something is always there. "No issues within what we checked" and "definitely safe" are completely different statements. The quiet, honest part of holding that line mattered more on the job than any dramatic find.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's still a good job
&lt;/h2&gt;

&lt;p&gt;I've spent this whole piece on what surprised me, but I'm not trying to put you off. After stacking up enough of the boring checks, you hit a moment where something feels slightly off, you pull on that thread, and a real problem is sitting at the end of it. That feeling is hard to get anywhere else. Not glamorous, but genuinely interesting.&lt;/p&gt;

&lt;p&gt;If you're drawn to this work, ask yourself less about the glamour and more about whether you could enjoy the careful, repetitive checking. Get that part right and it's a job you can do for a long time.&lt;/p&gt;




&lt;p&gt;How I got into this work with no background, and the certs and career steps along the way, is something I've written up at length elsewhere. If this was useful, follow along.&lt;/p&gt;

</description>
      <category>security</category>
      <category>cybersecurity</category>
      <category>beginners</category>
      <category>career</category>
    </item>
    <item>
      <title>The skills that actually transfer: what to learn for a long career in IT</title>
      <dc:creator>rudy_candy</dc:creator>
      <pubDate>Mon, 08 Jun 2026 20:47:24 +0000</pubDate>
      <link>https://dev.to/rudycandy/the-skills-that-actually-transfer-what-to-learn-for-a-long-career-in-it-5dpf</link>
      <guid>https://dev.to/rudycandy/the-skills-that-actually-transfer-what-to-learn-for-a-long-career-in-it-5dpf</guid>
      <description>&lt;p&gt;When you're trying to break into a specialized IT role from scratch, "what should I even study?" is a hard question. I was there myself.&lt;/p&gt;

&lt;p&gt;I started as a network engineer and now I do vulnerability assessment. After moving across roles a few times, one thing got clear: skills split fairly cleanly into the ones that transfer and the ones that don't. Here's how I tell them apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hot tool ages out faster than you think
&lt;/h2&gt;

&lt;p&gt;When you're job-hunting, it's tempting to chase whatever is most in demand right now. The tool names that show up in every posting, the framework everyone's talking about. I get it.&lt;/p&gt;

&lt;p&gt;But a thing that's popular is, by definition, a thing that gets replaced in a few years. You learn it, and by the time you have it down the next one is already taking over. Chase only that, and you're chasing forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  What lasts is the ability to understand how things work
&lt;/h2&gt;

&lt;p&gt;The opposite of that is foundation, and foundation lasts. For me it was networking.&lt;/p&gt;

&lt;p&gt;Back as a network engineer I spent my time in Wireshark, looking at traffic one packet at a time, reading what was actually happening on the wire. It was tedious, and at the time I half-doubted it had anything to do with security. But when I moved into vulnerability assessment, that foundation was exactly what carried over. Tools change; the ability to read what's riding on a request and a response doesn't.&lt;/p&gt;

&lt;p&gt;You can always stack tool knowledge on top of a foundation later. Going the other way is much harder. So if you're going to spend time early, spend it on the foundation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick "boring but durable"
&lt;/h2&gt;

&lt;p&gt;The skills that transfer are usually boring. How communication works, OS basics, how data moves. There's no flash to them, and while you study them you don't get much of a sense that they're paying off.&lt;/p&gt;

&lt;p&gt;But you can carry that understanding across roles and across whatever new tool shows up. The only reason I could move from networking into assessment was that the foundation came with me.&lt;/p&gt;

&lt;p&gt;If you're starting out and stuck on what to learn first, I'd pick "the thing that'll still exist in ten years" over "the hottest thing right now." It looks like the long way around. It isn't.&lt;/p&gt;




&lt;p&gt;The full route I took from network engineering into vulnerability assessment is something I've written up at length elsewhere. If this was useful, follow along.&lt;/p&gt;

</description>
      <category>career</category>
      <category>beginners</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>How I pass IT certifications in about 3 months while working full-time</title>
      <dc:creator>rudy_candy</dc:creator>
      <pubDate>Mon, 08 Jun 2026 20:47:21 +0000</pubDate>
      <link>https://dev.to/rudycandy/how-i-pass-it-certifications-in-about-3-months-while-working-full-time-4fih</link>
      <guid>https://dev.to/rudycandy/how-i-pass-it-certifications-in-about-3-months-while-working-full-time-4fih</guid>
      <description>&lt;p&gt;I've picked up a handful of IT certifications while working full-time, usually one to three months each. I'm not unusually smart. I just decide how I'm going to study before I start, and that part does most of the work. Here's the method.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set the finish line as a number
&lt;/h2&gt;

&lt;p&gt;The single thing that helped most was deciding, before I ever booked the exam, the score I had to reach before I was allowed to book it.&lt;/p&gt;

&lt;p&gt;For networking certs I'd run through a question bank several times, then switch to exam-simulation mode and keep going until my score sat around 90 to 95 percent. Only then did I register. Not "I feel about ready," but "I hit the number, so I book it." When the trigger is a number, you stop agonizing over whether you're ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cap the timeline, or it never ends
&lt;/h2&gt;

&lt;p&gt;Studying for a cert expands to fill whatever time you give it. The moment I think "half a year is fine," it tends to never finish.&lt;/p&gt;

&lt;p&gt;So I set a hard limit up front: three months. Once there's a deadline, the daily amount falls out of simple arithmetic. Work backward from the exam date and the per-day load is usually smaller than you feared, even around a full-time job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Passive studying didn't stick for me
&lt;/h2&gt;

&lt;p&gt;This one comes with some regret. Studying by watching videos didn't leave much in my head.&lt;/p&gt;

&lt;p&gt;While the video plays you feel like you understand it. Then you sit down with a real question and your hand stops. What actually stuck was the active loop: try a problem, get it wrong, try again. Output over input. And instead of buying more and more material, finishing one standard resource cover to cover was faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  That's the whole thing
&lt;/h2&gt;

&lt;p&gt;Passing certs around a job isn't about willpower for me. It's about the setup. Decide the finish line as a number, cap the timeline, and keep your hands moving on real problems. That alone gets you forward with limited time.&lt;/p&gt;

&lt;p&gt;If you happen to have a stretch where time comes in big blocks, like when you're still a student, that's when cramming a cert is most efficient. Use it.&lt;/p&gt;




&lt;p&gt;I write the technical notes in long form elsewhere, but the career and "how I actually did it" pieces I keep short like this. If this was useful, follow along.&lt;/p&gt;

</description>
      <category>career</category>
      <category>beginners</category>
      <category>productivity</category>
      <category>learning</category>
    </item>
    <item>
      <title>A Day in the Life of a Vulnerability Assessor in Japan</title>
      <dc:creator>rudy_candy</dc:creator>
      <pubDate>Mon, 08 Jun 2026 19:13:22 +0000</pubDate>
      <link>https://dev.to/rudycandy/a-day-in-the-life-of-a-vulnerability-assessor-in-japan-n0n</link>
      <guid>https://dev.to/rudycandy/a-day-in-the-life-of-a-vulnerability-assessor-in-japan-n0n</guid>
      <description>&lt;p&gt;People picture this job as someone hammering a keyboard and "hacking in." The reality is much quieter. I work as a vulnerability assessor (a web app pentester) in Japan, and most of my day is slow, careful, repetitive work. Here's what it actually looks like, hour by hour, plus a few things that might be specific to how the industry runs here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Morning: I don't touch the keyboard first
&lt;/h2&gt;

&lt;p&gt;The first thing I do isn't launch a tool. It's check the scope of the engagement I'm working on that day.&lt;/p&gt;

&lt;p&gt;Which domains and screens are in scope, and where am I not allowed to go? I read through whatever the client shared in the pre-engagement hearing (in Japan there's usually a fairly formal kickoff and a signed scope document), and I confirm the test accounts work. Getting sloppy here is how you end up touching a system that wasn't in scope, which is a real incident, not a small mistake. The whole job rests on one rule: only touch what you were given permission to touch. So this check comes before anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Late morning: crawl the app to build a map
&lt;/h2&gt;

&lt;p&gt;Once the scope is clear, I walk through the whole target like a normal user. Log in, move between screens, fill in forms, submit them. The entire time, Burp Suite is quietly recording every request in the background.&lt;/p&gt;

&lt;p&gt;At this stage I'm not hunting for bugs yet. I'm building a map: what features exist, and where does this app send and receive data? I also count the requests to estimate how much testing the day will actually take.&lt;/p&gt;

&lt;h2&gt;
  
  
  Afternoon: change one request, watch what changes
&lt;/h2&gt;

&lt;p&gt;This is the core of the work. I take the recorded requests one at a time, change a parameter, and look at how the response differs. Then again. And again.&lt;/p&gt;

&lt;p&gt;Honestly, it's tedious. You repeat almost the same action hundreds of times against different targets, and the dramatic moment almost never comes. But when a response behaves differently than you expected, that "wait, there's something here" instinct kicks in, and it gets sharper the more reps you put in. Whether you can find that tedium interesting is, I think, the real test of fit for this job.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually turns up
&lt;/h2&gt;

&lt;p&gt;This is the question I get most: "Do you find dramatic stuff like SQL injection all the time?"&lt;/p&gt;

&lt;p&gt;In my experience, the textbook SQL injection you learn on day one isn't that common on modern sites. Frameworks tend to handle it. What I actually run into is quieter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Broken access control (IDOR)
&lt;/h3&gt;

&lt;p&gt;This is the one I hit most. Change an ID in the URL or request from your own to someone else's, and you can see their data.&lt;/p&gt;

&lt;p&gt;It happens because the app checks whether you're logged in, but forgets to check whether you're allowed to see that particular record. During development, people test with their own data only, so it slips through. In a test, I usually set up two accounts and drop one account's ID into the other's request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Misconfiguration and information leakage
&lt;/h3&gt;

&lt;p&gt;A directory that's exposed when it shouldn't be. An error page that spills the server's internals or a stack trace. A dev file left behind in production. These "forgot to clean up" issues come up constantly.&lt;/p&gt;

&lt;p&gt;It's less a vulnerability than leftover mess. I find it by deliberately triggering errors and watching whether the response leaks more than it should, or by knocking on common paths to see what answers back.&lt;/p&gt;

&lt;h3&gt;
  
  
  Outdated, unpatched components
&lt;/h3&gt;

&lt;p&gt;A library or middleware with a known vulnerability, still running an old version. "It works, so nobody updated it." If a version number shows up in a response header or an error page, you can often guess that the version has a known issue from there.&lt;/p&gt;




&lt;p&gt;Line these up and the pattern is clear: the single flashy bug is rarer than the holes that grow out of day-to-day operations. Permission checks that are too loose, cleanup that never happened, updates that got pushed off. That gap between the textbook and the field is the part I didn't expect when I started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evening: find it, prove it, put it into words
&lt;/h2&gt;

&lt;p&gt;Finding something isn't the end. The job runs until you've captured how to reproduce it as evidence and written it up in a form that belongs in a report.&lt;/p&gt;

&lt;p&gt;Explaining it in language the client understands matters as much as finding it. "This is dangerous" tells them nothing. What happened, what could leak, how to fix it. Whether you can write that is what separates assessors. And in Japan the report is often the actual deliverable the client pays for, so it carries real weight.&lt;/p&gt;

&lt;h2&gt;
  
  
  So: quiet, but deep
&lt;/h2&gt;

&lt;p&gt;A vulnerability assessor's day is far more low-key than people imagine. Check the scope, build the map, keep testing requests, put what you find into words. That loop, over and over.&lt;/p&gt;

&lt;p&gt;But the feeling of catching a "wait, this is off" inside all that quiet checking is hard to get from other work. That's what keeps me in it.&lt;/p&gt;




&lt;p&gt;I'm an ex-network engineer who moved into security here in Japan. I'm starting to write about real-world pentesting and what this industry looks like from the inside. If that's interesting, follow along.&lt;/p&gt;

</description>
      <category>security</category>
      <category>cybersecurity</category>
      <category>career</category>
      <category>beginners</category>
    </item>
    <item>
      <title>strings Command in CTF: Hidden Data Guide</title>
      <dc:creator>rudy_candy</dc:creator>
      <pubDate>Mon, 20 Apr 2026 18:01:46 +0000</pubDate>
      <link>https://dev.to/rudycandy/strings-command-in-ctf-hidden-data-guide-4n9g</link>
      <guid>https://dev.to/rudycandy/strings-command-in-ctf-hidden-data-guide-4n9g</guid>
      <description>&lt;h1&gt;
  
  
  picoCTF Ph4nt0m 1ntrud3r — Network Forensics Writeup
&lt;/h1&gt;

&lt;p&gt;Category: Forensics | Difficulty: Easy | Competition: picoCTF&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge Overview
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;picoCTF Ph4nt0m 1ntrud3r&lt;/strong&gt; challenge drops you into a classic network forensics scenario: you receive a PCAP file and need to figure out what a mystery attacker was smuggling across the wire. No binary exploitation, no cryptographic math — just you, Wireshark, and a packet capture that hides a fragmented flag across multiple packets. I'll be honest: I thought this would take me ten minutes. It took closer to ninety, mostly because I spent the first half-hour confidently doing the wrong thing.&lt;/p&gt;

&lt;p&gt;This writeup covers the full investigation: my initial wrong approach, the rabbit hole I fell into, the exact Wireshark filters I used, the Python decoder I wrote, and what I'd do differently if I had to solve this again from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  My First (Wrong) Approach — And Why I Chose It
&lt;/h2&gt;

&lt;p&gt;When I opened &lt;code&gt;evidence.pcap&lt;/code&gt; in Wireshark, my gut reaction was to look at DNS traffic. In a lot of CTF forensics problems, exfiltration happens over DNS because defenders often under-monitor it. Long, weirdly-encoded subdomains are a classic data-hiding technique. I filtered on &lt;code&gt;dns&lt;/code&gt; immediately and started staring at query names, convinced I was about to find something like &lt;code&gt;cGljb0NURg==.attacker.evil&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There was nothing. Some boring A-record lookups, nothing that looked hand-crafted. I switched to HTTP, thinking maybe the flag was in a User-Agent header or a URL parameter. Still nothing suspicious. I then tried &lt;code&gt;tcp contains "picoCTF"&lt;/code&gt; as a raw string search — also empty. At this point I had burned roughly 35 minutes and had zero leads.&lt;/p&gt;

&lt;p&gt;The reason I kept chasing these paths is that they work in a lot of other CTF challenges, and pattern-matching from past experience can be a trap. I was looking for the shape of a problem I'd solved before instead of reading the actual data in front of me.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Rabbit Hole: Manual Packet Inspection
&lt;/h3&gt;

&lt;p&gt;After the DNS and HTTP dead ends, I started scrolling through packets manually, reading payloads one by one. This is exactly the kind of approach that sounds thorough but is actually just slow. I found a few packets with short string payloads and tried to read them as ASCII flags. One of them had what looked like a partial "picoC" — I got excited, copied it out wrong because I was working from a hex dump, and spent another fifteen minutes trying to figure out why my "flag" was garbled nonsense.&lt;/p&gt;

&lt;p&gt;That manual copy failure was the moment I finally stopped and thought about the problem differently. If the data is fragmented and Base64-encoded, manual copying from hex dumps is going to produce errors every single time. I needed to sort by something structural and then automate the extraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up the Investigation Environment
&lt;/h2&gt;

&lt;p&gt;Tools used for this challenge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wireshark 4.x (packet capture analysis)&lt;/li&gt;
&lt;li&gt;Python 3.11 (Base64 decoding script)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tshark&lt;/code&gt; (command-line Wireshark for batch extraction)&lt;/li&gt;
&lt;li&gt;A Linux terminal with &lt;code&gt;base64&lt;/code&gt; utility for quick spot checks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing exotic. The point of forensics challenges at this level is usually that the tools are simple and the insight is the hard part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Digging Into the PCAP with Wireshark
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Initial Triage: What Does This Traffic Even Look Like?
&lt;/h3&gt;

&lt;p&gt;After abandoning the manual scroll approach, I went back to basics. In Wireshark, I opened the Statistics menu and ran &lt;strong&gt;Protocol Hierarchy&lt;/strong&gt; first. This gives you an instant breakdown of what protocols are present in the capture without requiring you to guess. The capture was mostly TCP with a small cluster of short application-layer payloads that didn't map cleanly to any known protocol — that asymmetry was the first real signal.&lt;/p&gt;

&lt;p&gt;Next I sorted packets by &lt;strong&gt;Length&lt;/strong&gt; (ascending). This is a move I wish I'd made at the start. The attacker's fragments were short — consistently around 12–16 bytes of payload — while the rest of the traffic had normal-sized packets. That uniform small size is unusual and stands out immediately once you sort by length.&lt;/p&gt;

&lt;h3&gt;
  
  
  Applying Wireshark Filters
&lt;/h3&gt;

&lt;p&gt;Once I had a hypothesis — short payloads, possibly Base64 — I used the following display filter to isolate TCP segments with small application data:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tcp.len &amp;gt; 0 and tcp.len &amp;lt; 20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This filtered down to a manageable set of packets. Looking at the Follow TCP Stream output on one of them:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Wireshark &amp;gt; Right-click packet &amp;gt; Follow &amp;gt; TCP Stream

Stream content (ASCII view):
cGljb0NURg==
ezF0X3c0cw==
bnRfdGg0dA==
XzM0c3lfdA==
YmhfNHJfOQ==
NjZkMGJmYg==
fQ==
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Seven short strings. Every single one ends in &lt;code&gt;=&lt;/code&gt; or &lt;code&gt;==&lt;/code&gt;. That trailing equals sign is the unmistakable fingerprint of Base64 padding — it appears when the input length isn't a multiple of three bytes. Seeing it once might be coincidence. Seeing it seven times in a row is a pattern that can only mean one thing.&lt;/p&gt;

&lt;p&gt;I also used &lt;code&gt;tshark&lt;/code&gt; from the command line to extract these payloads more cleanly, which avoids the copy-paste errors I'd been making earlier:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tshark -r evidence.pcap -Y "tcp.len &amp;gt; 0 and tcp.len &amp;lt; 20" -T fields -e data.text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Output:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cGljb0NURg==
ezF0X3c0cw==
bnRfdGg0dA==
XzM0c3lfdA==
YmhfNHJfOQ==
NjZkMGJmYg==
fQ==
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Clean extraction, no manual copying. This is what I should have done from the beginning instead of scrolling through hex dumps by hand.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Importance of Timestamp Order
&lt;/h3&gt;

&lt;p&gt;One subtlety worth noting: network packets don't necessarily arrive in the order they were sent. TCP handles reordering at the transport layer, but if you're extracting application-layer fragments manually, you need to sort by the original timestamp — not by the order Wireshark received them. In this challenge the packets happened to arrive in sequence, but in a real incident response scenario, out-of-order fragments are a deliberate anti-forensics technique. Always sort by time first, then extract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recognizing the Base64 Pattern
&lt;/h2&gt;

&lt;p&gt;Before writing the decoder, I did a quick sanity check on the first fragment using the command line:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ echo "cGljb0NURg==" | base64 --decode
picoCTF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That was the moment everything clicked. &lt;code&gt;picoCTF&lt;/code&gt; — the first fragment is literally the competition name and flag prefix. The attacker (or in this case, the challenge author) split the flag at a seven-character boundary and encoded each chunk separately. The decode confirms: I have the right data, I have the right encoding, and I just need to concatenate all seven decoded strings.&lt;/p&gt;

&lt;p&gt;Let me be specific about that feeling: it's genuinely satisfying after 35 minutes of wrong guesses to see a word you recognize come out of a decoder. Not triumphant — more like the relief when you finally find your keys after tearing the house apart. The work isn't done yet but now at least you know what you're doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing the Decoder Script
&lt;/h2&gt;

&lt;p&gt;With all seven fragments confirmed, the decoder is straightforward:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import base64

# Fragments extracted from PCAP via tshark, sorted by timestamp
cipher = [
    "cGljb0NURg==",   # fragment 1
    "ezF0X3c0cw==",   # fragment 2
    "bnRfdGg0dA==",   # fragment 3
    "XzM0c3lfdA==",   # fragment 4
    "YmhfNHJfOQ==",   # fragment 5
    "NjZkMGJmYg==",   # fragment 6
    "fQ=="            # fragment 7
]

plain = ""
for i, c in enumerate(cipher):
    decoded = base64.b64decode(c).decode("utf-8")
    print(f"Fragment {i+1}: {c!r:20s} =&amp;gt; {decoded!r}")
    plain += decoded

print()
print("Assembled flag:", plain)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Execution output:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 decode_flag.py
Fragment 1: 'cGljb0NURg=='      =&amp;gt; 'picoCTF'
Fragment 2: 'ezF0X3c0cw=='      =&amp;gt; '{1t_w4s'
Fragment 3: 'bnRfdGg0dA=='      =&amp;gt; 'nt_th4t'
Fragment 4: 'XzM0c3lfdA=='      =&amp;gt; '_34sy_t'
Fragment 5: 'YmhfNHJfOQ=='      =&amp;gt; 'bh_4r_9'
Fragment 6: 'NjZkMGJmYg=='      =&amp;gt; '66d0bfb'
Fragment 7: 'fQ=='              =&amp;gt; '}'

Assembled flag: picoCTF{1t_w4snt_th4t_34sy_tbh_4r_966d0bfb}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Flag: &lt;code&gt;picoCTF{1t_w4snt_th4t_34sy_tbh_4r_966d0bfb}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The flag text itself is a small joke by the challenge author — "it wasn't that easy, tbh" — which I found funnier after spending 90 minutes on what is technically an "Easy" challenge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Trial Process Table
&lt;/h2&gt;

&lt;p&gt;Here is every approach I tried during this challenge, in order:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Command / Filter&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Why it failed / succeeded&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Filter DNS traffic&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dns&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only standard A-record lookups, nothing encoded&lt;/td&gt;
&lt;td&gt;Wrong assumption — exfiltration wasn't DNS-based&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Filter HTTP traffic&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No suspicious headers or URL params&lt;/td&gt;
&lt;td&gt;Wrong protocol assumption from past CTF patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Raw string search for flag prefix&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tcp contains "picoCTF"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No matches&lt;/td&gt;
&lt;td&gt;Flag was Base64-encoded, not plaintext — search missed it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Manual hex dump scroll&lt;/td&gt;
&lt;td&gt;(manual, no filter)&lt;/td&gt;
&lt;td&gt;Found short payloads but copied incorrectly&lt;/td&gt;
&lt;td&gt;Human error in transcribing hex; garbled output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Protocol hierarchy check&lt;/td&gt;
&lt;td&gt;Statistics &amp;gt; Protocol Hierarchy&lt;/td&gt;
&lt;td&gt;Identified anomalous short TCP payloads&lt;/td&gt;
&lt;td&gt;Right direction — structural anomaly visible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Sort by packet length&lt;/td&gt;
&lt;td&gt;Column sort in Wireshark UI&lt;/td&gt;
&lt;td&gt;Small cluster of 12–16 byte payloads visible&lt;/td&gt;
&lt;td&gt;Attacker's fragments isolated from normal traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Filter short TCP payloads&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tcp.len &amp;gt; 0 and tcp.len &amp;lt; 20&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Seven packets isolated&lt;/td&gt;
&lt;td&gt;Correct filter; exact fragments found&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Follow TCP stream&lt;/td&gt;
&lt;td&gt;Right-click &amp;gt; Follow &amp;gt; TCP Stream&lt;/td&gt;
&lt;td&gt;All seven Base64 strings visible in sequence&lt;/td&gt;
&lt;td&gt;Confirmed data and order; saw "=" padding pattern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;tshark command-line extraction&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tshark -r evidence.pcap -Y "tcp.len &amp;gt; 0 and tcp.len &amp;lt; 20" -T fields -e data.text&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Clean list of seven Base64 fragments&lt;/td&gt;
&lt;td&gt;No manual copy error; clean input for Python script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Quick spot decode&lt;/td&gt;
&lt;td&gt;`echo "cGljb0NURg=="&lt;/td&gt;
&lt;td&gt;base64 --decode`&lt;/td&gt;
&lt;td&gt;&lt;code&gt;picoCTF&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Python decoder script&lt;/td&gt;
&lt;td&gt;&lt;code&gt;python3 decode_flag.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full flag assembled: &lt;code&gt;picoCTF{1t_w4snt_th4t_34sy_tbh_4r_966d0bfb}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;All fragments decoded and concatenated correctly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Technical Deep Dive — Why Attackers Fragment Data This Way
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Data Fragmentation as an Evasion Technique
&lt;/h3&gt;

&lt;p&gt;This challenge models a real attacker behavior: splitting exfiltrated data into small chunks to evade detection. Signature-based intrusion detection systems (IDS) look for known patterns — if a full flag string or a recognizable file header appears in a single packet, an alert fires. But if that same data is split into seven fragments of 8–12 bytes each, each encoded in Base64 (which looks like random alphanumeric noise to a pattern matcher), the same IDS might let every packet through individually.&lt;/p&gt;

&lt;p&gt;Base64 encoding adds another layer of deniability. It transforms binary or text data into a character set that looks like ordinary web traffic — Base64 appears constantly in legitimate email attachments, image data URIs, and API tokens. A network defender scanning for "weird-looking traffic" might not flag short Base64 strings without specific tuning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real-World Network Forensics Parallels
&lt;/h3&gt;

&lt;p&gt;In professional incident response and digital forensics, Wireshark and &lt;code&gt;tshark&lt;/code&gt; are standard tools that security operations center (SOC) analysts and DFIR (Digital Forensics and Incident Response) specialists use daily. The workflow in this challenge — capture traffic, identify anomalous patterns, extract and decode payloads — mirrors what a real analyst does when investigating suspected data exfiltration.&lt;/p&gt;

&lt;p&gt;Some concrete real-world parallels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;APT exfiltration campaigns&lt;/strong&gt; often use DNS tunneling or HTTP with Base64-encoded payloads in headers — the same encoding technique used here, just over a different protocol&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Malware command-and-control (C2) traffic&lt;/strong&gt; frequently uses short, regular beacons with encoded payloads; identifying the "attacker's packets" by their unusual size and periodicity is a standard detection heuristic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network traffic analysis (NTA) tools&lt;/strong&gt; like Zeek/Bro and Suricata implement exactly the kind of length-based filtering we did manually here — they flag short TCP streams with encoded payloads as potential exfiltration candidates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DFIR tools&lt;/strong&gt; like NetworkMiner automate the extraction of payloads from PCAP files, doing at scale what we did by hand in this challenge&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The skills this challenge teaches — statistical anomaly detection in traffic, protocol filter construction, payload extraction, encoding recognition — are directly transferable to entry-level SOC analyst work. This isn't just a CTF puzzle; it's a stripped-down version of a real investigation workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Base64 Specifically?
&lt;/h3&gt;

&lt;p&gt;Base64 is not encryption — it provides no confidentiality. Anyone who sees the encoded string can decode it trivially. The reason it shows up in CTF challenges and in real attacks is that it solves a different problem: binary data compatibility. Network protocols, email systems, and web applications are often designed to handle text. Base64 encodes arbitrary binary data as printable ASCII characters, making it safe to embed in text-only contexts. Attackers use it not to hide data from sophisticated defenders, but to get it through infrastructure that would otherwise mangle or block binary payloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reflection — How I Would Solve This Faster Next Time
&lt;/h2&gt;

&lt;p&gt;Looking back at this challenge with the benefit of knowing the answer, my 90-minute solve breaks down roughly as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;35 minutes: chasing DNS and HTTP false leads&lt;/li&gt;
&lt;li&gt;20 minutes: manual hex dump scrolling and failed copy attempts&lt;/li&gt;
&lt;li&gt;15 minutes: realizing I should check protocol hierarchy and sort by length&lt;/li&gt;
&lt;li&gt;10 minutes: applying the right filter and extracting fragments&lt;/li&gt;
&lt;li&gt;10 minutes: writing and running the decoder&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first 55 minutes were waste. Here's the checklist I'd follow if I had to do this again from the start:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Protocol Hierarchy first, always.&lt;/strong&gt; Before applying any filters, run Statistics &amp;gt; Protocol Hierarchy. This takes 10 seconds and tells you exactly what you're dealing with.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sort by packet length before scrolling.&lt;/strong&gt; Attackers' fragments usually stand out by size. Sort ascending, look for clusters of unusually short or unusually long packets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use tshark for extraction, not manual copy.&lt;/strong&gt; The moment you're copy-pasting hex or ASCII from Wireshark by hand, you're introducing errors. Automate extraction from the start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spot-test the encoding before writing a full script.&lt;/strong&gt; A one-liner (&lt;code&gt;echo "..." | base64 --decode&lt;/code&gt;) confirms your hypothesis before you invest time scripting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't assume past patterns apply.&lt;/strong&gt; DNS exfiltration, HTTP header hiding — these work in many challenges. But the first thing to do is read the actual data, not apply heuristics from previous problems.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If I applied this checklist, I think I could solve this challenge in under 15 minutes. The solution is genuinely straightforward — the difficulty is resisting the urge to jump to conclusions and doing the unglamorous structural analysis first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wireshark filter to isolate short TCP payloads:&lt;/strong&gt; &lt;code&gt;tcp.len &amp;gt; 0 and tcp.len &amp;lt; 20&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tshark command to extract payload text:&lt;/strong&gt; &lt;code&gt;tshark -r evidence.pcap -Y "tcp.len &amp;gt; 0 and tcp.len &amp;lt; 20" -T fields -e data.text&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Base64 tell:&lt;/strong&gt; trailing &lt;code&gt;=&lt;/code&gt; or &lt;code&gt;==&lt;/code&gt; padding in all fragments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lesson:&lt;/strong&gt; Start with protocol-level statistics, not protocol assumptions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-world connection:&lt;/strong&gt; This workflow (size anomaly detection → payload extraction → encoding analysis) is standard network forensics practice in SOC and DFIR environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;picoCTF Ph4nt0m 1ntrud3r is a well-constructed introductory forensics challenge because it teaches a real investigative pattern, not just a trick. The "easy" rating is accurate once you know what to look for — but getting to that moment of knowing takes most of the work.&lt;/p&gt;

</description>
      <category>ctf</category>
      <category>security</category>
      <category>linux</category>
      <category>forensics</category>
    </item>
    <item>
      <title>pngcheck in CTF: How to Analyze and Repair PNG Files</title>
      <dc:creator>rudy_candy</dc:creator>
      <pubDate>Mon, 20 Apr 2026 18:01:43 +0000</pubDate>
      <link>https://dev.to/rudycandy/pngcheck-in-ctf-how-to-analyze-and-repair-png-files-84o</link>
      <guid>https://dev.to/rudycandy/pngcheck-in-ctf-how-to-analyze-and-repair-png-files-84o</guid>
      <description>&lt;h1&gt;
  
  
  🔍 pngcheck CTF Tutorial: How to Analyze Corrupted PNG Files and Find Hidden Chunks
&lt;/h1&gt;

&lt;p&gt;Searching for "pngcheck CTF" or "how to fix corrupted PNG forensics" usually returns tool documentation or terse Writeups that skip the thinking. This article is different: it's a walkthrough of how I actually use pngcheck in CTF PNG forensics challenges — including the 30 minutes I wasted on steganography tools before I learned to validate structure first. If you're stuck on a corrupted PNG challenge and wondering what pngcheck is showing you, this guide will get you unstuck.&lt;/p&gt;

&lt;h2&gt;
  
  
  This Article at a Glance
&lt;/h2&gt;

&lt;p&gt;pngcheck is a command-line PNG validation tool that reads a PNG file's internal chunk structure and reports exactly what's wrong — or what's hidden — at the byte level. In CTF forensics, it's the fastest way to diagnose a corrupted PNG, find non-standard chunks, and decide whether you're dealing with a structural fix challenge or a steganography challenge. By the end of this article, you'll know when to run it, what its output means, and — just as importantly — when to put it down and switch tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction: The PNG Challenge Where I Used Every Tool Except the Right One
&lt;/h2&gt;

&lt;p&gt;CTF forensics challenges love PNG files. They're binary, they have a well-documented structure, and there are a dozen ways to hide data inside them without visually changing the image. The problem for beginners is that a corrupted or manipulated PNG doesn't announce itself — it just fails to open, or opens fine while hiding something in the chunk data you never look at.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pngcheck&lt;/code&gt; is the tool that makes the invisible visible. It reads the raw PNG chunk stream and validates every piece of the structure: the magic header bytes, the IHDR dimensions, each IDAT chunk's CRC, the IEND terminator, and anything else lurking in between. It won't decrypt anything or extract hidden images — but it will tell you precisely where the file is broken, where extra data is hiding, and what every chunk in the file actually contains.&lt;/p&gt;

&lt;p&gt;The challenge that taught me this was picoCTF's &lt;strong&gt;Corrupted File&lt;/strong&gt; — a PNG that wouldn't open, a description that said "I tried to open this image but something seems off," and me spending 30 minutes going down the wrong path completely. My first instinct was steganography. I ran &lt;code&gt;zsteg challenge.png&lt;/code&gt;, got output I didn't understand, tried every channel combination. Nothing. I tried &lt;code&gt;stegsolve&lt;/code&gt; and clicked through every filter. Still nothing. I even tried &lt;code&gt;strings&lt;/code&gt; and grepped for &lt;code&gt;picoCTF{&lt;/code&gt;. The flag wasn't there because the image wasn't a steganography challenge. It was a broken CRC challenge. One &lt;code&gt;pngcheck -v&lt;/code&gt; would have shown me the answer in 2 seconds. The reason I didn't run it first: I associated PNG challenges with hidden pixel data and never considered that the file structure itself was the puzzle.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is pngcheck? (And What It Isn't)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What pngcheck actually does
&lt;/h3&gt;

&lt;p&gt;A PNG file is a sequence of chunks. Each chunk has a type (4-byte name like &lt;code&gt;IHDR&lt;/code&gt;, &lt;code&gt;IDAT&lt;/code&gt;, &lt;code&gt;tEXt&lt;/code&gt;), a length, data, and a CRC checksum. &lt;code&gt;pngcheck&lt;/code&gt; reads every chunk in order, validates the CRC, checks that required chunks are present in the right order, and reports anything unexpected.&lt;/p&gt;

&lt;p&gt;The basic output looks like this:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ pngcheck challenge.png
OK: challenge.png (800x600, 24-bit RGB, non-interlaced, 92.3%).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;And verbose output — which is what you actually want in CTF — looks like this:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ pngcheck -v challenge.png
File: challenge.png (153847 bytes)
  chunk IHDR at offset 0x0000c, length 13
    800 x 600 image, 24-bit RGB, non-interlaced
  chunk tEXt at offset 0x00025, length 36, keyword: Comment
  chunk IDAT at offset 0x00057, length 8192 (OK)
  chunk IDAT at offset 0x02065, length 8192 (OK)
  chunk IEND at offset 0x25819, length 0
No errors detected in challenge.png (5 chunks, 92.3% compression).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  What pngcheck cannot do
&lt;/h3&gt;

&lt;p&gt;This is the part beginners miss. pngcheck is a validator and inspector — it is not an extractor or a decoder. It will tell you that a &lt;code&gt;tEXt&lt;/code&gt; chunk exists with keyword "Comment," but it won't show you the content of that comment in basic mode. It will tell you there's data after the IEND chunk, but it won't extract it. It validates structure; everything else needs another tool.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;pngcheck can do it?&lt;/th&gt;
&lt;th&gt;Use this instead&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Find broken CRC&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List all chunks and offsets&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detect extra data after IEND&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extract hidden text from tEXt chunks&lt;/td&gt;
&lt;td&gt;⚠️ Partial (shows keyword only)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;strings&lt;/code&gt;, &lt;code&gt;exiftool&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detect LSB steganography&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zsteg&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extract embedded files&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;&lt;code&gt;binwalk -e&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fix broken CRC&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;hex editor + manual CRC calculation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repair corrupted IHDR dimensions&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;hex editor + &lt;code&gt;pngcheck&lt;/code&gt; to verify fix&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  When to Use pngcheck in CTF
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Problem description keywords that should trigger pngcheck
&lt;/h3&gt;

&lt;p&gt;I've developed a reflex: if a PNG challenge mentions any of these, pngcheck runs first before anything else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"The image is corrupted" or "won't open"&lt;/li&gt;
&lt;li&gt;"Something is wrong with the file"&lt;/li&gt;
&lt;li&gt;"Check the structure" or "check the chunks"&lt;/li&gt;
&lt;li&gt;"The file passes validation but something is off"&lt;/li&gt;
&lt;li&gt;Any hint involving CRC, chunk, header, or IHDR&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  pngcheck vs zsteg vs binwalk — When to Use Which
&lt;/h3&gt;

&lt;p&gt;The trap I fell into on Corrupted File — and then again on two other challenges before I finally learned — was running steganography tools on a PNG that had a structural problem. My reasoning at the time: "It's a PNG challenge, so it's probably steganography." That assumption is wrong about half the time. Here's the decision logic I've built since then:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use pngcheck when:&lt;/strong&gt; the file won't open, the challenge mentions "corruption," "chunks," or "structure," or you want to enumerate what chunks exist before doing anything else. pngcheck answers the question: &lt;em&gt;is this file structurally valid?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use zsteg when:&lt;/strong&gt; pngcheck reports no errors and the image opens normally. zsteg checks for LSB-encoded data hidden in pixel channels — it operates entirely at the pixel level and doesn't care about chunk structure. If pngcheck says the file is clean, zsteg is your next move.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use binwalk when:&lt;/strong&gt; you suspect an entirely different file is embedded somewhere inside the PNG, or when pngcheck reports extra data after IEND and you want to extract it cleanly. binwalk signature-scans the raw bytes regardless of format.&lt;/p&gt;

&lt;p&gt;The decision order that works for me: &lt;code&gt;pngcheck -fvp&lt;/code&gt; first, always. If it passes → &lt;code&gt;zsteg&lt;/code&gt;. If it fails with extra data → &lt;code&gt;binwalk -e&lt;/code&gt;. If it fails with CRC → hex editor to patch. This sequence alone has saved me from countless Rabbit Holes. (For a deeper look at binwalk, see CTF Forensics: How to Use binwalk to Extract Hidden Files.)&lt;/p&gt;
&lt;h2&gt;
  
  
  Basic Usage With Thinking
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Step 1 — Basic validation: is it broken at all?
&lt;/h3&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ pngcheck challenge.png
CRC error in chunk IHDR (computed 4a3f2c1b, expected 00000000)
ERRORS DETECTED in challenge.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If this returns an error, you have a structural problem. The chunk name tells you where to look. IHDR error = broken header. IDAT error = broken image data. CRC mismatch = someone modified a byte somewhere. The next question is whether it was intentional (challenge design) or accidental (corrupted file).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 — Verbose output: read every chunk
&lt;/h3&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ pngcheck -v challenge.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This is the command I run on every PNG challenge now, even before checking if it opens. The verbose output shows chunk names, offsets, lengths, and CRC status. I'm looking for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any chunk with a CRC error&lt;/li&gt;
&lt;li&gt;Non-standard chunk names (anything that isn't IHDR/IDAT/IEND/tEXt/zTXt/gAMA/etc.)&lt;/li&gt;
&lt;li&gt;Chunks in wrong order (IDAT before IHDR is invalid)&lt;/li&gt;
&lt;li&gt;Content after IEND&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3 — Maximum detail: zlib and compression info
&lt;/h3&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ pngcheck -fvp challenge.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;-f&lt;/code&gt; flag forces pngcheck to continue checking even after errors (useful when multiple chunks are broken). The &lt;code&gt;-p&lt;/code&gt; flag prints the contents of non-critical chunks including text. This is how I found a base64-encoded flag sitting in a &lt;code&gt;tEXt&lt;/code&gt; chunk with keyword "Author" — the image opened perfectly, the flag was in plain sight in the chunk data, and I'd wasted 20 minutes on pixel-level steg before checking this.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Most Common CTF Scenarios
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Scenario 1: Broken CRC — The Most Common Trap
&lt;/h3&gt;

&lt;p&gt;A challenge author modifies a chunk's data without recalculating the CRC. pngcheck catches it immediately:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ pngcheck -v challenge.png
  chunk IHDR at offset 0x0000c, length 13
    800 x 600 image, 24-bit RGB, non-interlaced
CRC error in chunk IHDR (computed 4a3f2c1b, expected 1a2b3c4d)
ERRORS DETECTED in challenge.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The CRC mismatch means the IHDR data was modified after the CRC was set. In CTF, this almost always means the image dimensions were changed — the real dimensions were replaced with smaller values to crop out the hidden content. The flag is in the part of the image that was "hidden" by reducing the reported height or width.&lt;/p&gt;

&lt;p&gt;To fix it: find the correct CRC value for the real IHDR data, then patch the file. Here's the Python approach I use:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import struct, zlib

with open("challenge.png", "rb") as f:
    data = f.read()

# IHDR chunk data is at bytes 12-28 (after 8-byte signature + 4-byte length + 4-byte type)
ihdr_data = data[12:29]  # 4 (length) + 4 (IHDR) + 13 (data) = offset 12 to 28
chunk_type_and_data = data[16:29]  # just "IHDR" + 13 bytes of data

correct_crc = zlib.crc32(chunk_type_and_data) &amp;amp; 0xFFFFFFFF
print(f"Correct CRC: {correct_crc:#010x}")

# Patch: replace bytes 29-33 with correct CRC
patched = data[:29] + struct.pack("&amp;gt;I", correct_crc) + data[33:]
with open("fixed.png", "wb") as f:
    f.write(patched)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;After patching, run &lt;code&gt;pngcheck fixed.png&lt;/code&gt; to confirm it passes, then open the image. If the dimensions were manipulated, the fixed file will render at the real size and reveal the hidden area. This pattern is so common in picoCTF and beginner CTFs that I now check for dimension mismatches as the first instinct whenever I see an IHDR CRC error.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 2: Hidden Custom Chunks
&lt;/h3&gt;

&lt;p&gt;PNG allows custom (ancillary) chunks. They're valid PNG — most image viewers ignore unknown chunks silently. pngcheck lists them:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ pngcheck -v challenge.png
  chunk IHDR at offset 0x0000c, length 13
  chunk IDAT at offset 0x00025, length 8192 (OK)
  chunk IEND at offset 0x25801, length 0
  chunk flAg at offset 0x25815, length 42
    (unknown ancillary chunk)
No errors detected in challenge.png (4 chunks).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That &lt;code&gt;flAg&lt;/code&gt; chunk after IEND is not standard. The data inside it is the flag. pngcheck found it in one command. Without it, I'd be running steganography tools on an image that wasn't hiding anything in its pixels at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 3: Data Appended After IEND
&lt;/h3&gt;

&lt;p&gt;The IEND chunk is supposed to be the last chunk in a PNG. Data after it is technically invalid, but most image viewers load the image anyway and ignore the trailing bytes. pngcheck flags it:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ pngcheck challenge.png
invalid chunk name "" (00 00 00 00)
ERRORS DETECTED in challenge.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Or with verbose:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  chunk IEND at offset 0x25801, length 0
additional data after IEND chunk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;From the offset of IEND, you can calculate exactly where the appended data starts and extract it with &lt;code&gt;dd&lt;/code&gt; or &lt;code&gt;binwalk&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Mistakes and Rabbit Holes
&lt;/h2&gt;

&lt;p&gt;The Corrupted File mistake was the first one. The second was a different challenge where pngcheck reported "No errors detected" — so I assumed I'd checked everything and moved to pixel-level steganography. I spent another 20 minutes on &lt;code&gt;zsteg&lt;/code&gt; before going back and running &lt;code&gt;pngcheck -fvp&lt;/code&gt;. The &lt;code&gt;-p&lt;/code&gt; flag I'd skipped revealed a &lt;code&gt;tEXt&lt;/code&gt; chunk with keyword "flag" containing a base64 string. It was sitting there in plain text the whole time. The file was clean structurally — the data was just hidden in a chunk I hadn't looked at.&lt;/p&gt;

&lt;p&gt;The third mistake: I saw an IHDR CRC error, correctly identified that dimensions had been manipulated, patched the CRC — and then stopped. The image rendered at the new larger size, but I didn't look at what was in the newly revealed area carefully enough. The flag was written in white text on a white background in the bottom 50 pixels that had been hidden. I would have seen it immediately if I'd opened the image in a tool that let me invert colors. Lesson: when you fix a CRC and the image changes size, the newly revealed area is where to look first.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mistake&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;th&gt;How to avoid it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Running steganography tools on a structurally broken PNG&lt;/td&gt;
&lt;td&gt;Tools either fail or give garbage output&lt;/td&gt;
&lt;td&gt;Always run &lt;code&gt;pngcheck -v&lt;/code&gt; first; fix structure before any steg analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stopping at "No errors detected" without &lt;code&gt;-p&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Miss flag stored in tEXt chunk content&lt;/td&gt;
&lt;td&gt;Always use &lt;code&gt;-fvp&lt;/code&gt; — "no errors" doesn't mean "nothing hidden"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixing CRC without examining what changed&lt;/td&gt;
&lt;td&gt;Fix the structure but miss the actual clue in the revealed area&lt;/td&gt;
&lt;td&gt;After patching, look at the newly visible area of the image first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ignoring ancillary chunk names&lt;/td&gt;
&lt;td&gt;Miss flag stored in custom chunk like &lt;code&gt;flAg&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Read every chunk name in verbose output; unusual names are the challenge&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Full Trial Process Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Open in image viewer&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;❌ Won't open / blank&lt;/td&gt;
&lt;td&gt;Structural problem suspected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Basic pngcheck&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pngcheck challenge.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ CRC error in IHDR&lt;/td&gt;
&lt;td&gt;IHDR was modified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Verbose pngcheck&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pngcheck -v challenge.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ Offset + expected CRC shown&lt;/td&gt;
&lt;td&gt;Dimensions likely manipulated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Check IHDR bytes in hex editor&lt;/td&gt;
&lt;td&gt;hex editor at offset 0x10&lt;/td&gt;
&lt;td&gt;✅ Width/height values confirmed wrong&lt;/td&gt;
&lt;td&gt;Restore real dimensions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Recalculate CRC and patch&lt;/td&gt;
&lt;td&gt;Python CRC32 + hex editor&lt;/td&gt;
&lt;td&gt;✅ pngcheck now passes&lt;/td&gt;
&lt;td&gt;Image opens at correct dimensions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Full verbose check on fixed file&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pngcheck -fvp fixed.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ Flag visible in tEXt chunk&lt;/td&gt;
&lt;td&gt;Challenge solved&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Command Reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;When to Use&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pngcheck file.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Basic validation&lt;/td&gt;
&lt;td&gt;Quick first check&lt;/td&gt;
&lt;td&gt;Shows pass/fail only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pngcheck -v file.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Verbose chunk listing&lt;/td&gt;
&lt;td&gt;Always, after basic check&lt;/td&gt;
&lt;td&gt;Shows all chunk names, offsets, CRC status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pngcheck -fvp file.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full detail including text chunk contents&lt;/td&gt;
&lt;td&gt;When looking for hidden data in chunks&lt;/td&gt;
&lt;td&gt;-f forces continuation past errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pngcheck -c file.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Show chunk names only&lt;/td&gt;
&lt;td&gt;Quick structural scan&lt;/td&gt;
&lt;td&gt;Less detail than -v&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pngcheck -t file.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Print tEXt/zTXt chunk content&lt;/td&gt;
&lt;td&gt;When verbose output shows text chunks&lt;/td&gt;
&lt;td&gt;Useful for reading hidden comments&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Beginner Tips
&lt;/h2&gt;

&lt;h3&gt;
  
  
  My personal pngcheck workflow for every PNG challenge
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;pngcheck -fvp challenge.png&lt;/code&gt; immediately — even before trying to open the file&lt;/li&gt;
&lt;li&gt;Read every line of output. Non-standard chunk names are red flags&lt;/li&gt;
&lt;li&gt;If there's a CRC error in IHDR, check the dimensions with a hex editor before doing anything else&lt;/li&gt;
&lt;li&gt;If it passes cleanly, note any &lt;code&gt;tEXt&lt;/code&gt;, &lt;code&gt;zTXt&lt;/code&gt;, or &lt;code&gt;iTXt&lt;/code&gt; chunks — extract their content&lt;/li&gt;
&lt;li&gt;Check for data after IEND&lt;/li&gt;
&lt;li&gt;Only switch to steganography tools (&lt;code&gt;zsteg&lt;/code&gt;, &lt;code&gt;stegsolve&lt;/code&gt;) after structural analysis is complete&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Installing pngcheck
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Debian/Ubuntu/Kali&lt;br&gt;
sudo apt install pngcheck
&lt;h1&gt;
  
  
  macOS
&lt;/h1&gt;

&lt;p&gt;brew install pngcheck&lt;br&gt;
&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  What You Learn From Using pngcheck&lt;br&gt;
&lt;/h2&gt;

&lt;p&gt;If you want to go further with the pattern this article teaches — validate structure before running analysis tools — the same mindset applies to disk images with &lt;a href="https://alsavaudomila.com/mount-in-ctf-disk-image-mounting-and-common-challenge-patterns/" rel="noopener noreferrer"&gt;mount and mmls&lt;/a&gt;, to ZIP archives with &lt;a href="https://alsavaudomila.com/zip2john-in-ctf-extracting-zip-passwords-and-common-challenge-patterns/" rel="noopener noreferrer"&gt;zip2john&lt;/a&gt;, and to PDFs with &lt;a href="https://alsavaudomila.com/pdfdumper-in-ctf-extracting-pdf-content-and-common-challenge-patterns/" rel="noopener noreferrer"&gt;pdfdumper&lt;/a&gt;. Every binary format has a structure validator. Find it before running extraction tools.&lt;/p&gt;

&lt;p&gt;Using pngcheck teaches you to read binary file structure before running tools. The PNG chunk format is one of the clearest examples of how a file format works internally — fixed headers, typed chunks, CRC integrity checks, a defined terminator. Once you understand why pngcheck exists and what it's validating, you start applying the same thinking to every binary challenge: what does the spec say this file should contain, and what does this specific file actually contain?&lt;/p&gt;

&lt;p&gt;That gap between spec and reality is where CTF flags live. pngcheck is the tool that measures that gap for PNG files.&lt;/p&gt;

&lt;p&gt;In real-world forensics, the same principle applies. Investigators validate file format integrity to detect tampering — a modified PNG with a recalculated CRC might look valid to an image viewer but will show the modification timestamp inconsistency at the chunk level. CTF PNG challenges are teaching you actual forensic thinking, not just CTF tricks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;p&gt;This article is part of the &lt;strong&gt;Forensics Tools&lt;/strong&gt; series. You can see the other tools covered in the series here: &lt;a href="https://alsavaudomila.com/ctf-forensics-tools/" rel="noopener noreferrer"&gt;CTF Forensics Tools: The Ultimate Guide for Beginners&lt;/a&gt;. Introducing the &lt;strong&gt;pngcheck&lt;/strong&gt; command, how to use it in CTF, and common patterns used in problems.&lt;/p&gt;

&lt;p&gt;Here are related articles from &lt;a href="https://alsavaudomila.com/" rel="noopener noreferrer"&gt;alsavaudomila.com&lt;/a&gt; that complement what you've learned here about pngcheck:&lt;/p&gt;

&lt;p&gt;Once pngcheck confirms that a PNG file is structurally clean and you still suspect hidden data, the next tool to reach for is zsteg. It operates entirely at the pixel level, scanning LSB-encoded channels that pngcheck cannot see. &lt;a href="https://alsavaudomila.com/zsteg-in-ctf-detect-and-extract-hidden-data-from-images/" rel="noopener noreferrer"&gt;zsteg in CTF: Detect and Extract Hidden Data from Images&lt;/a&gt; walks through exactly when and how to use it, including the patterns that distinguish a clean image from one carrying hidden payloads.&lt;/p&gt;

&lt;p&gt;When pngcheck reports extra data after the IEND chunk, the right tool to extract it is binwalk. Rather than manually calculating offsets with &lt;code&gt;dd&lt;/code&gt;, binwalk signature-scans the raw bytes and pulls out embedded files automatically — regardless of filesystem or format boundaries. &lt;a href="https://alsavaudomila.com/binwalk-in-ctf-how-to-analyze-binaries-and-extract-hidden-files/" rel="noopener noreferrer"&gt;binwalk in CTF: How to Analyze Binaries and Extract Hidden Files&lt;/a&gt; explains how to read its output and avoid the common mistake of extracting too aggressively.&lt;/p&gt;

&lt;p&gt;If pngcheck reveals a &lt;code&gt;tEXt&lt;/code&gt; or &lt;code&gt;iTXt&lt;/code&gt; chunk with suspicious content, exiftool can read the full metadata embedded across all chunk types — including GPS data, creation timestamps, and author fields that pngcheck shows as keywords but doesn't display in full. &lt;a href="https://alsavaudomila.com/exiftool-in-ctf-how-to-analyze-metadata-and-find-hidden-data/" rel="noopener noreferrer"&gt;exiftool in CTF: How to Analyze Metadata and Find Hidden Data&lt;/a&gt; covers how metadata fields become hiding places in CTF challenges and what to look for.&lt;/p&gt;

</description>
      <category>ctf</category>
      <category>security</category>
      <category>linux</category>
      <category>forensics</category>
    </item>
    <item>
      <title>Scan Surprise picoCTF Writeup</title>
      <dc:creator>rudy_candy</dc:creator>
      <pubDate>Mon, 20 Apr 2026 17:50:26 +0000</pubDate>
      <link>https://dev.to/rudycandy/scan-surprise-picoctf-writeup-4447</link>
      <guid>https://dev.to/rudycandy/scan-surprise-picoctf-writeup-4447</guid>
      <description>&lt;p&gt;picoCTF forensics challenges come in all shapes, but Scan Surprise from the General Skills category is one where the difficulty isn't the technique — it's knowing which tool exists. I solved this in under two minutes once I figured that out, but getting there took longer than I'd like to admit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;p&gt;The challenge gives you a ZIP file. Unzip it and you get a &lt;code&gt;flag.png&lt;/code&gt;. Open it and you're looking at a QR code.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ unzip challenge.zip
Archive:  challenge.zip
   creating: home/ctf-player/drop-in/
 extracting: home/ctf-player/drop-in/flag.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Okay, it's a QR code. First instinct: pull out my phone and scan it. That works — phone cameras read QR codes fine. But in a CTF environment where you're working in a terminal, there's a cleaner way, and figuring that out is the whole point of this challenge.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Part Where I Wasted Time
&lt;/h2&gt;

&lt;p&gt;I knew QR codes existed as a challenge type in CTF forensics, but I didn't know there was a dedicated command-line decoder for them. My first thought was to write a Python script using &lt;code&gt;opencv&lt;/code&gt; or &lt;code&gt;pyzbar&lt;/code&gt;. I started down that path:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# What I tried first (unnecessary)
pip install pyzbar
pip install Pillow

# Then started writing:
from pyzbar.pyzbar import decode
from PIL import Image
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;A few minutes in, I stopped and searched for "qr code cli linux" — and immediately found &lt;code&gt;zbarimg&lt;/code&gt;. It's a standalone command-line tool that reads QR codes and barcodes from image files directly. No Python, no script, no library imports needed.&lt;/p&gt;

&lt;p&gt;That's the actual "surprise" in this challenge: there's already a tool for this. Once you know &lt;code&gt;zbarimg&lt;/code&gt; exists, the challenge collapses into a single command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing zbarimg
&lt;/h2&gt;

&lt;p&gt;If you don't have it already:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Debian/Ubuntu (including picoCTF's webshell environment)
sudo apt install zbar-tools

# Verify it's working
zbarimg --version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The package is &lt;code&gt;zbar-tools&lt;/code&gt;, not &lt;code&gt;zbarimg&lt;/code&gt; — that's the command name, not the package. This tripped me up the first time I tried to install it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ cd home/ctf-player/drop-in/
$ zbarimg flag.png
QR-Code:picoCTF{p33k_@\_b00\_3f7cf1ae}
scanned 1 barcode symbols from 1 images in 0 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Flag: &lt;strong&gt;picoCTF{p33k_@_b00_3f7cf1ae}&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The flag text — &lt;code&gt;p33k_@_b00&lt;/code&gt; — is leet speak for "peek-a-boo." The challenge name "Scan Surprise" is a nod to that. Small detail, but it made me smile when I noticed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Challenge Is Actually Testing
&lt;/h2&gt;

&lt;p&gt;Scan Surprise isn't testing your knowledge of QR code internals or image processing. It's testing tool awareness — specifically, whether you know that &lt;code&gt;zbarimg&lt;/code&gt; exists.&lt;/p&gt;

&lt;p&gt;This comes up more than you'd think in CTF forensics. A lot of time gets lost not because the technique is hard, but because competitors don't know a purpose-built tool exists and end up writing scripts from scratch. Knowing the toolbox matters as much as knowing the theory.&lt;/p&gt;

&lt;p&gt;Why not just use a phone camera? In a competition, you want reproducible, copy-pasteable output in your terminal — not a screenshot of your phone screen. &lt;code&gt;zbarimg&lt;/code&gt; gives you that. It also becomes essential when challenges deliberately break QR codes in ways that confuse phone cameras but can be fixed with preprocessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Harder Versions of This Challenge Look Like
&lt;/h2&gt;

&lt;p&gt;Scan Surprise is the baseline — a clean QR code that zbarimg reads without any preprocessing. Once you've seen this pattern, you'll encounter versions where it's deliberately harder:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inverted colors&lt;/strong&gt; — the QR code has white modules on a black background. zbarimg returns "0 barcodes" because the ISO standard assumes dark-on-light. Fix: &lt;code&gt;convert -negate&lt;/code&gt; before scanning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low resolution&lt;/strong&gt; — the image is too small for the decoder to reliably parse. Fix: upscale with &lt;code&gt;convert -resize&lt;/code&gt; using &lt;code&gt;-filter point&lt;/code&gt; to keep edges sharp.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QR code inside a video&lt;/strong&gt; — a QR code appears in a frame of a video file. Fix: extract frames with ffmpeg, then run zbarimg on the frames.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In all of those cases, the tool is still zbarimg — you just have to preprocess the image first. Scan Surprise establishes the foundation; the harder variants are the same workflow with an extra step in front.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;p&gt;Here are related articles from &lt;a href="https://alsavaudomila.com/" rel="noopener noreferrer"&gt;alsavaudomila.com&lt;/a&gt; that build on this challenge:&lt;/p&gt;

&lt;p&gt;If you want to go deeper on &lt;code&gt;zbarimg&lt;/code&gt; itself — including the full diagnostic workflow for when it returns "0 barcodes" — the tool guide at &lt;a href="https://alsavaudomila.com/zbarimg-in-ctf-qr-barcode-decoding-techniques-and-common-challenge-patterns/" rel="noopener noreferrer"&gt;zbarimg in CTF&lt;/a&gt; covers each failure mode with working commands.&lt;/p&gt;

&lt;p&gt;For a broader map of which forensics tools to use depending on file type, &lt;a href="https://alsavaudomila.com/ctf-forensics-tools/" rel="noopener noreferrer"&gt;CTF Forensics Tools: The Ultimate Guide for Beginners&lt;/a&gt; covers the full decision process from file identification through to extraction.&lt;/p&gt;

</description>
      <category>ctf</category>
      <category>picoctf</category>
      <category>linux</category>
      <category>security</category>
    </item>
    <item>
      <title>CTF Audio Challenges: A Practical SoX Combat Guide</title>
      <dc:creator>rudy_candy</dc:creator>
      <pubDate>Mon, 20 Apr 2026 17:50:23 +0000</pubDate>
      <link>https://dev.to/rudycandy/ctf-audio-challenges-a-practical-sox-combat-guide-1lc0</link>
      <guid>https://dev.to/rudycandy/ctf-audio-challenges-a-practical-sox-combat-guide-1lc0</guid>
      <description>

&lt;p&gt;― Decision Log: Turning an Inaudible WAV into a Flag ―&lt;/p&gt;

&lt;h2&gt;
  
  
  Facing the Problem: Why I Judged This Audio "Meaningless to Play"
&lt;/h2&gt;

&lt;h3&gt;
  
  
  First Impression of the Distributed Audio File (CTF Context)
&lt;/h3&gt;

&lt;p&gt;It was a Saturday evening CTF. The problem title was "Silent Message" and a single file was attached: &lt;code&gt;message.wav&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I downloaded it, double-clicked. Windows Media Player opened. Hit play.&lt;/p&gt;

&lt;p&gt;Static. Pure white noise for about 5 seconds, then silence.&lt;/p&gt;

&lt;p&gt;My first thought: "Corrupted file?" But this is CTF. Nothing is ever corrupted by accident. I closed the player and stared at the filename for a moment.&lt;/p&gt;

&lt;p&gt;In that instant, I made a decision: &lt;strong&gt;" Playing this normally won't get me anywhere."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why could I make that judgment so quickly? Because I'd wasted 40 minutes on a similar problem two months earlier, listening to static on repeat with headphones, convinced I was "missing something subtle." I wasn't. The information was just stored in a completely different dimension.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Basis for Immediately Discarding the "Just Listen" Approach
&lt;/h3&gt;

&lt;p&gt;Here's what I knew from the problem context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Problem category&lt;/strong&gt; : Listed under "Forensics" not "Audio Analysis"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File size&lt;/strong&gt; : 441KB for 5 seconds—that's suspiciously standard (44100Hz × 2 bytes × 1 channel × 5 sec)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Problem description&lt;/strong&gt; : "The message is there, you just need to hear it differently"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last line was the tell. Not "listen carefully" but "hear it &lt;em&gt;differently&lt;/em&gt;." In CTF language, that's code for: "The playback parameters are wrong."&lt;/p&gt;

&lt;p&gt;I've learned to read these hints. When a problem says:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Listen carefully" → Likely steganography or obscured speech&lt;/li&gt;
&lt;li&gt;"Hear it differently" → Parameter manipulation needed&lt;/li&gt;
&lt;li&gt;"Something's off" → Structural problem with the file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was clearly the second type.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initial Hypotheses I Formed at This Point
&lt;/h3&gt;

&lt;p&gt;Standing at the starting line, I had three hypotheses:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hypothesis 1: Sampling rate mismatch&lt;/strong&gt; The file header claims one rate, but the data was recorded at another. Classic CTF trick. If recorded at 22050Hz but labeled as 44100Hz, it would play at double speed—unintelligible squeaks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hypothesis 2: Channel-based hiding&lt;/strong&gt; Maybe it's stereo and one channel is empty noise while the other has data. Or left/right channels need to be XORed together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hypothesis 3: Frequency domain information&lt;/strong&gt; The "sound" might be meaningless, but a spectrogram could reveal text or images.&lt;/p&gt;

&lt;p&gt;I needed to test these fast. But which tool?&lt;/p&gt;

&lt;h2&gt;
  
  
  First Approach and Failure: Why I Didn't Use SoX
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why I Considered Other Tools (Audacity / ffmpeg) First
&lt;/h3&gt;

&lt;p&gt;My initial instinct was Audacity. I'd used it before, knew where the menus were, and most importantly: &lt;strong&gt;I could see what I was doing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For Hypothesis 3 (spectrogram), Audacity was the obvious choice. I opened the file.&lt;/p&gt;

&lt;p&gt;The waveform appeared—flat line with occasional noise spikes. I switched to spectrogram view (Ctrl+Shift+Y in my muscle memory).&lt;/p&gt;

&lt;p&gt;Nothing. Just uniform noise across all frequencies. No hidden text, no patterns, no images.&lt;/p&gt;

&lt;p&gt;Okay, Hypothesis 3 out. But this took 2 minutes including load time.&lt;/p&gt;

&lt;p&gt;For Hypotheses 1 and 2, I could use Audacity's effect menus:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Effect → Change Speed&lt;/li&gt;
&lt;li&gt;Tracks → Stereo Track to Mono&lt;/li&gt;
&lt;li&gt;Effect → Equalize&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But here's where I hesitated. To test Hypothesis 1 properly, I'd need to try multiple sampling rates: 22050, 16000, 11025, maybe 8000. In Audacity, that's:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Effect → Change Speed → Calculate ratio → Apply&lt;/li&gt;
&lt;li&gt;Listen&lt;/li&gt;
&lt;li&gt;Undo&lt;/li&gt;
&lt;li&gt;Repeat with different ratio&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each cycle: 20-30 seconds.&lt;/p&gt;

&lt;p&gt;I sat there, cursor hovering over the Effect menu, and thought: "There has to be a faster way."&lt;/p&gt;

&lt;h3&gt;
  
  
  The Point Where I Judged "This Isn't It"
&lt;/h3&gt;

&lt;p&gt;I tried one speed change in Audacity: 0.5x (simulating if the file was actually 22050Hz).&lt;/p&gt;

&lt;p&gt;Result: Slow static. Still meaningless.&lt;/p&gt;

&lt;p&gt;The problem wasn't that Audacity couldn't do it. The problem was the &lt;strong&gt;feedback loop was too slow&lt;/strong&gt;. Each test required:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Menu navigation&lt;/li&gt;
&lt;li&gt;Parameter input via dialog box&lt;/li&gt;
&lt;li&gt;Processing time (even if short)&lt;/li&gt;
&lt;li&gt;Manual playback&lt;/li&gt;
&lt;li&gt;Mental note-taking of what I tried&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I needed to test maybe 10 different configurations. At 30 seconds per test, that's 5 minutes minimum—and that's if I don't get lost or forget what I already tried.&lt;/p&gt;

&lt;p&gt;I closed Audacity.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Would Have Happened If I Hadn't Chosen SoX Here
&lt;/h3&gt;

&lt;p&gt;Looking back, if I'd stuck with Audacity, one of two things would have happened:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario A: I 'd have solved it, but slowly&lt;/strong&gt; Eventually, I'd have hit the right combination and heard the flag. But it might have taken 15-20 minutes instead of the 3 minutes it actually took with SoX.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario B: I 'd have given up&lt;/strong&gt; More likely, after trying 3-4 combinations manually, I'd have convinced myself "it's not a sampling rate problem" and moved to a different hypothesis. Wrong direction, wasted time.&lt;/p&gt;

&lt;p&gt;The danger with GUI tools in CTF isn't that they can't solve problems—it's that they make you give up on correct hypotheses too early because the iteration cost is too high.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Turning Point: The Decisive Condition That Made Me Deploy SoX
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The CTF-Specific Checklist: "When These Conditions Align, Use SoX"
&lt;/h3&gt;

&lt;p&gt;I've developed a mental checklist over time. When I can tick 3+ boxes, I reach for SoX:&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Problem hints at parameter manipulation&lt;/strong&gt; (sampling rate, speed, channels) ✅ &lt;strong&gt;Need to test multiple values systematically&lt;/strong&gt; ✅ &lt;strong&gt;GUI tool feedback loop feels too slow&lt;/strong&gt; ✅ &lt;strong&gt;File format is standard&lt;/strong&gt; (WAV, not some obscure codec) ✅ &lt;strong&gt;Time pressure&lt;/strong&gt; (other problems to solve, limited CTF duration)&lt;/p&gt;

&lt;p&gt;This problem hit all five.&lt;/p&gt;

&lt;p&gt;The moment I realized "I need to try 5+ sampling rates quickly" was the moment I decided: SoX.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I Abandoned GUI and Chose CLI
&lt;/h3&gt;

&lt;p&gt;Here's the honest truth: I don't love command-line tools. GUIs are comfortable. You can see your options, click around, explore.&lt;/p&gt;

&lt;p&gt;But in CTF, comfort is the enemy of speed.&lt;/p&gt;

&lt;p&gt;With SoX, I could write:&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for rate in 8000 11025 16000 22050 32000 44100; do
  sox message.wav -r $rate "test_${rate}.wav"
done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Six files generated in under 3 seconds. Then I could just play them all:&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for f in test_*.wav; do
  echo "Playing $f"
  play "$f"
done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Linear playback, no menu navigation, no remembering what I tried. The command history is my lab notebook.&lt;/p&gt;

&lt;p&gt;This is why I chose CLI: &lt;strong&gt;not because it 's better at audio processing, but because it's better at rapid experimentation.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Misconceptions and Anxiety at First Deployment
&lt;/h3&gt;

&lt;p&gt;That said, I wasn't confident.&lt;/p&gt;

&lt;p&gt;The first time I used SoX in a CTF (different problem, months earlier), I spent 10 minutes fighting with it because I didn't understand the option syntax. I kept trying:&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sox input.wav output.wav -r 22050
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Nothing changed. No error messages, just… no effect. I thought SoX was broken or I had the wrong version installed.&lt;/p&gt;

&lt;p&gt;Turns out, the &lt;code&gt;-r&lt;/code&gt; option has to come &lt;em&gt;before&lt;/em&gt; the output filename:&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sox input.wav -r 22050 output.wav
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This kind of thing—option ordering, global vs. effect syntax—was completely non-obvious to me as a beginner. The man page didn't help; it's comprehensive but overwhelming.&lt;/p&gt;

&lt;p&gt;So even as I decided "SoX is the right tool," part of me was thinking: "Am I going to waste 15 minutes debugging syntax again?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I Actually Got Stuck: Traps Every SoX Beginner Steps On
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The "Cognitive Mismatch" That Happened on First Operation
&lt;/h3&gt;

&lt;p&gt;I created my test files with the for-loop above. Played &lt;code&gt;test_22050.wav&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Clear human voice. Success on the second try.&lt;/p&gt;

&lt;p&gt;But here's the thing—I almost dismissed it.&lt;/p&gt;

&lt;p&gt;The voice said: "The password is echo charlie tango…"&lt;/p&gt;

&lt;p&gt;I thought: "Wait, that's not a flag. Flags are &lt;code&gt;flag{...}&lt;/code&gt; format."&lt;/p&gt;

&lt;p&gt;I started to move on to the next test file, then stopped. Re-read the problem description: "The message is there."&lt;/p&gt;

&lt;p&gt;Not "the flag." The &lt;em&gt;message&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This was a two-stage problem. The audio gives you a password, you use that password to decrypt something else (there was a &lt;code&gt;.enc&lt;/code&gt; file I'd ignored).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trap&lt;/strong&gt; : I was so focused on "find the flag" that I almost missed "find the message." SoX did exactly what it was supposed to—I almost threw away the correct answer because my mental model was wrong.&lt;/p&gt;

&lt;p&gt;This happens more than I'd like to admit. The tool works; my assumptions don't.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Changing Options Didn't Change Results
&lt;/h3&gt;

&lt;p&gt;Earlier in my SoX learning curve (different problem), I tried:&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sox input.wav output.wav rate 16000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Played output.wav. No change.&lt;/p&gt;

&lt;p&gt;Tried again:&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sox input.wav output.wav rate 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Still no change. I checked file sizes—they were different, so &lt;em&gt;something&lt;/em&gt; happened. But when I played them, identical to the original.&lt;/p&gt;

&lt;p&gt;I was mystified for 20 minutes.&lt;/p&gt;

&lt;p&gt;The problem: I was using &lt;code&gt;rate&lt;/code&gt; as an effect, which does sample rate &lt;em&gt;conversion&lt;/em&gt; (resampling the existing data). What I actually wanted was to &lt;em&gt;reinterpret&lt;/em&gt; the existing samples at a different rate, which requires the &lt;code&gt;-r&lt;/code&gt; option:&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sox input.wav -r 16000 output.wav
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The lesson&lt;/strong&gt; : SoX has two philosophies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Global options&lt;/strong&gt; (&lt;code&gt;-r&lt;/code&gt;, &lt;code&gt;-c&lt;/code&gt;): "Interpret the data this way"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Effects&lt;/strong&gt; (&lt;code&gt;rate&lt;/code&gt;, &lt;code&gt;channels&lt;/code&gt;): "Transform the data"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For CTF sampling rate tricks, you almost always want global options, not effects. But if you don't know this distinction, you'll burn time on operations that do nothing useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Operations I Should Have Abandoned at This Point
&lt;/h3&gt;

&lt;p&gt;In that earlier problem where &lt;code&gt;rate&lt;/code&gt; wasn't working, I tried:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Different &lt;code&gt;rate&lt;/code&gt; values (8000, 11025, 16000…)&lt;/li&gt;
&lt;li&gt;Adding quality options (&lt;code&gt;rate -h&lt;/code&gt;, &lt;code&gt;rate -m&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Checking if &lt;code&gt;dither&lt;/code&gt; affected it&lt;/li&gt;
&lt;li&gt;Reading forums about sample rate conversion algorithms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this mattered because I was using the wrong approach entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The abandonment rule I developed&lt;/strong&gt; : If 3 attempts with parameter variations don't change the perceptible output, it's not a parameter problem—it's a conceptual problem. Stop tweaking, start reading.&lt;/p&gt;

&lt;p&gt;In this case, 5 minutes with the man page (searching for "sample rate") would have saved me 15 minutes of flailing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Worked / What Disappointed (Combat Comparison)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Settings That Worked: Why This Parameter Hit Hard
&lt;/h3&gt;

&lt;p&gt;For the "Silent Message" problem, the winning command was:&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sox message.wav -r 22050 output.wav
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why did this work?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The file header claimed 44100Hz, but the actual recording was done at 22050Hz. When played as 44100Hz, it ran at 2x speed—too fast to understand, sounded like noise.&lt;/p&gt;

&lt;p&gt;Re-interpreting as 22050Hz slowed it to the correct speed.&lt;/p&gt;

&lt;p&gt;But here's the critical part: &lt;strong&gt;I didn 't just get lucky.&lt;/strong&gt; The file size was the tell:&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ls -lh message.wav
# 441000 bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;441000 bytes = 220500 samples × 2 bytes/sample (16-bit) 220500 samples at 44100Hz = 5 seconds 220500 samples at 22050Hz = 10 seconds&lt;/p&gt;

&lt;p&gt;The problem description said nothing about file length, but I timed the audio: 5 seconds of noise. If the hidden message was "normal speech speed," it probably needed &lt;em&gt;more&lt;/em&gt; than 5 seconds to say anything meaningful.&lt;/p&gt;

&lt;p&gt;So 22050Hz (doubling the duration to 10 seconds) was a strong hypothesis.&lt;/p&gt;

&lt;h3&gt;
  
  
  Differences in "Appearance and Sound" When Changing Values
&lt;/h3&gt;

&lt;p&gt;I made a systematic test:&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for rate in 11025 16000 22050 32000 44100 88200; do
  sox message.wav -r $rate "test_${rate}.wav"
  echo "Testing ${rate}Hz..."
  play "test_${rate}.wav" 2&amp;gt;/dev/null
  sleep 1
done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;11025Hz&lt;/strong&gt; : Very slow, deep voice, but comprehensible words&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;16000Hz&lt;/strong&gt; : Slow, slightly lower pitch, also comprehensible&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;22050Hz&lt;/strong&gt; : Normal speech speed—&lt;strong&gt;clear winner&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;32000Hz&lt;/strong&gt; : Too fast, words blur&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;44100Hz&lt;/strong&gt; : Original—unintelligible&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;88200Hz&lt;/strong&gt; : Extremely fast squeaks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern was obvious. Below 22050Hz, I could understand the words but the speech was unnaturally slow. Above 22050Hz, too fast. At 22050Hz exactly, natural cadence.&lt;/p&gt;

&lt;p&gt;This is why systematic testing matters. If I'd only tried 16000Hz, I might have thought "close enough" and missed subtle details in the message.&lt;/p&gt;

&lt;h3&gt;
  
  
  Settings I Expected to Work but Did Nothing
&lt;/h3&gt;

&lt;p&gt;In an earlier problem, I was convinced the trick was channel manipulation. The file was stereo, so I tried:&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Extract left channel
sox stereo.wav left.wav remix 1

# Extract right channel  
sox stereo.wav right.wav remix 2

# Mix both channels
sox stereo.wav -c 1 mono.wav
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;```

Played all three. All sounded identical—just noise.

I wasted 10 minutes trying different channel operations: swapping left/right, inverting one channel, isolating frequency bands per channel.

Nothing.

Eventually checked the file with `soxi`:
```
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Channels: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;It was mono the whole time. The file extension was &lt;code&gt;.wav&lt;/code&gt; and I &lt;em&gt;assumed&lt;/em&gt; stereo because many WAV files are. I never verified.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson&lt;/strong&gt; : &lt;code&gt;soxi&lt;/code&gt; first, assumptions later. One command (&lt;code&gt;soxi input.wav&lt;/code&gt;) would have saved me those 10 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rabbit Hole Chronicle: Dangerous Forks in This Audio Problem
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Trap of Drowning Time in Spectrograms
&lt;/h3&gt;

&lt;p&gt;Even after solving "Silent Message" with sampling rate changes, I felt uneasy. "That was too easy," I thought. "Maybe there's a &lt;em&gt;second&lt;/em&gt; flag hidden in the spectrogram?"&lt;/p&gt;

&lt;p&gt;I generated one:&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sox message.wav -n spectrogram -o spec.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Opened the image. Stared at it for 5 minutes, looking for patterns.&lt;/p&gt;

&lt;p&gt;Nothing obvious, but I zoomed in. Enhanced contrast in GIMP. Adjusted gamma. Rotated 90 degrees (I've seen upside-down text before).&lt;/p&gt;

&lt;p&gt;15 minutes gone.&lt;/p&gt;

&lt;p&gt;Then I snapped out of it. The problem was marked as 100 points—easy tier. If there were two flags, it would be marked higher. I was inventing complexity that wasn't there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The psychology&lt;/strong&gt; : After solving a problem "too easily," your brain invents reasons to doubt the solution. Especially in CTF, where you're trained to expect tricks within tricks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt; : Check the problem's point value. Check if anyone else has solved it (if scoreboards are visible). If 20 people solved it in 5 minutes, you're probably done. Move on.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Psychology of Continuing Noise Reduction
&lt;/h3&gt;

&lt;p&gt;In a different problem (not "Silent Message"), I had an audio file with voice buried under noise. I tried:&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sox noisy.wav clean.wav noisered profile.prof 0.21
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;It helped. The voice became slightly clearer.&lt;/p&gt;

&lt;p&gt;So I thought: "What if I do it again?"&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sox clean.wav cleaner.wav noisered profile.prof 0.21
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;And again:&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sox cleaner.wav cleanest.wav noisered profile.prof 0.21
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;By the third iteration, the "voice" was unrecognizable. I'd removed so much signal along with the noise that the message was destroyed.&lt;/p&gt;

&lt;p&gt;But I kept going. "Maybe one more time…"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt; Because each iteration showed &lt;em&gt;some&lt;/em&gt; change. The file sounded different. My brain interpreted "different" as "progress."&lt;/p&gt;

&lt;p&gt;It wasn't progress. It was destruction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The escape&lt;/strong&gt; : Set a rule before starting: "I'll try this effect twice at most. If it doesn't clearly help by attempt two, abandon it." Write the rule down. Stick to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Moment When Continuing to Use SoX Becomes the Failure
&lt;/h3&gt;

&lt;p&gt;"Silent Message" was perfect for SoX. But I've had problems where SoX was the wrong tool and I didn't realize until I'd wasted 30 minutes.&lt;/p&gt;

&lt;p&gt;Example: A problem with an MP3 file that had metadata steganography—flag hidden in ID3 tags, not in the audio data itself.&lt;/p&gt;

&lt;p&gt;I spent ages trying:&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sox hidden.mp3 -r 22050 test.wav
sox hidden.mp3 output.wav reverse
sox hidden.mp3 output.wav speed 0.5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Nothing worked because I was operating on the wrong layer. SoX processes &lt;em&gt;audio data&lt;/em&gt;. Metadata isn't audio data.&lt;/p&gt;

&lt;p&gt;The solution was:&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ffmpeg -i hidden.mp3
# (Shows metadata in output)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;or&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;exiftool hidden.mp3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The recognition point&lt;/strong&gt; : If you've tried 5+ different SoX operations across different categories (sampling rate, channels, speed, effects) and &lt;em&gt;nothing&lt;/em&gt; changes the perceptible output, the problem isn't in the audio domain. It's structural, metadata-based, or you're completely off-track.&lt;/p&gt;

&lt;p&gt;That's when you stop using SoX and reassess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thought Progression to Flag Identification (Reproducible Search Order)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Hypothesis → Operation → Result → Next Hypothesis
&lt;/h3&gt;

&lt;p&gt;Here's the mental flowchart I followed for "Silent Message":&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Initial state&lt;/strong&gt; : WAV file, plays as static&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hypothesis 1&lt;/strong&gt; : "Static = high-frequency noise, maybe lowpass filter helps"&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sox message.wav filtered.wav lowpass 4000
play filtered.wav
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt; : Still static, just quieter &lt;strong&gt;Judgment&lt;/strong&gt; : Wrong direction, abandon lowpass approach&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hypothesis 2&lt;/strong&gt; : "File metadata lies about sampling rate"&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;soxi message.wav
# Sample Rate: 44100
# Duration: 5 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;File size: 441KB ≈ 220500 samples &lt;strong&gt;Reasoning&lt;/strong&gt; : 5 seconds feels short for a message. Try reinterpreting as 22050Hz → 10 seconds&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sox message.wav -r 22050 output.wav
play output.wav
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt; : Clear voice! &lt;strong&gt;Judgment&lt;/strong&gt; : Hypothesis confirmed, proceed to decode message&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hypothesis 3&lt;/strong&gt; : (Not needed—already solved)&lt;/p&gt;

&lt;p&gt;Total time: Under 3 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key principle&lt;/strong&gt; : Each hypothesis is &lt;em&gt;falsifiable&lt;/em&gt;. "Lowpass might help" → test → no → discard. Don't dwell. Move to next hypothesis.&lt;/p&gt;

&lt;h3&gt;
  
  
  Confirmation Judgment Derived from Flag Format
&lt;/h3&gt;

&lt;p&gt;The voice said: "The password is echo charlie tango foxtrot bravo alpha two zero two four"&lt;/p&gt;

&lt;p&gt;I transcribed: &lt;code&gt;ectfba2024&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;But the problem said "submit the flag." Flags have format &lt;code&gt;flag{...}&lt;/code&gt; or similar.&lt;/p&gt;

&lt;p&gt;Checked problem description again: "The flag is obtained by using the password to decrypt the file."&lt;/p&gt;

&lt;p&gt;There was an attached &lt;code&gt;secret.enc&lt;/code&gt;. I tried:&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openssl enc -d -aes-256-cbc -in secret.enc -out secret.txt -k ectfba2024
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Output: &lt;code&gt;flag{sampling_rate_lies}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That 's&lt;/strong&gt; the flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The confirmation process&lt;/strong&gt; :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Audio gives "password" → Not directly the flag&lt;/li&gt;
&lt;li&gt;Problem gives encrypted file → Flag is inside&lt;/li&gt;
&lt;li&gt;Decrypt with password → Obtain actual flag&lt;/li&gt;
&lt;li&gt;Flag matches expected format → Confirmed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If I'd submitted &lt;code&gt;ectfba2024&lt;/code&gt; directly, I'd have gotten "Wrong answer." Understanding the &lt;strong&gt;flag submission format&lt;/strong&gt; and &lt;strong&gt;multi-stage problem structure&lt;/strong&gt; was as critical as solving the audio part.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Deviating from This Order Leads to Getting Lost
&lt;/h3&gt;

&lt;p&gt;I've seen people (including past-me) mess up by:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 1&lt;/strong&gt; : Trying everything simultaneously&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open Audacity, look at spectrogram&lt;/li&gt;
&lt;li&gt;Run SoX sampling rate changes&lt;/li&gt;
&lt;li&gt;Try steganography tools&lt;/li&gt;
&lt;li&gt;Check metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result: Information overload, can't track what worked&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 2&lt;/strong&gt; : Not recording what you tried&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Wait, did I already try 16000Hz?"&lt;/li&gt;
&lt;li&gt;"Was this the file before or after I applied the effect?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result: Repeated work, confusion&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 3&lt;/strong&gt; : Ignoring problem context&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Solve the audio to get &lt;code&gt;ectfba2024&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Submit it directly without reading "use it to decrypt"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result: Correct step, wrong conclusion&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt; : Linear progression with documentation.&lt;/p&gt;

&lt;p&gt;My actual terminal history for "Silent Message":&lt;/p&gt;

&lt;p&gt;bash&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# 1. Initial recon
file message.wav
soxi message.wav
play message.wav

# 2. First hypothesis - lowpass
sox message.wav filtered.wav lowpass 4000
play filtered.wav
# (nope)

# 3. Second hypothesis - sampling rate
sox message.wav -r 22050 output.wav
play output.wav
# (yes!)

# 4. Decrypt
openssl enc -d -aes-256-cbc -in secret.enc -out secret.txt -k ectfba2024
cat secret.txt
# flag{sampling_rate_lies}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;```

Clean, linear, reproducible. That's how you avoid getting lost.

## Next Time I See Similar Conditions: Action Guidelines

### Decision Criteria Summary for Using SoX

I reach for SoX when:

1. **Problem hints suggest parameter tricks**
   - Keywords: "sounds wrong," "too fast," "can't hear," "hidden message"
   - File format: Standard WAV/FLAC, not exotic codecs

2. **Need systematic parameter exploration**
   - Test multiple sampling rates: 8k, 11k, 16k, 22k, 32k, 44k, 48k
   - Test channel operations: L/R split, mono conversion
   - Test time operations: reverse, speed changes

3. **Time is constrained**
   - Other unsolved problems waiting
   - GUI iteration feels too slow
   - Need to automate multiple tests

4. **Command-line environment available**
   - Can pipe outputs, use loops
   - Terminal history = automatic documentation

### Decision Line for Not Using / Abandoning Midway

I abandon SoX and switch tools when:

1. **5 different operations produce identical output**
   - Likely wrong problem domain
   - Switch to metadata tools (`exiftool`, `ffmpeg -i`)

2. **Visual inspection needed**
   - Need to see spectrogram clearly
   - Need to manually select waveform regions
   - Switch to Audacity

3. **Complex signal processing required**
   - FFT analysis, correlation, custom algorithms
   - Switch to Python (librosa, scipy)

4. **File format unsupported**
   - Exotic codecs, video with audio
   - Switch to ffmpeg for conversion first

### Timing for Switching to Other Tools

My typical workflow:
```
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Start: SoX (3-5 minutes)
  ↓
Sampling rate, channels, speed, reverse → Any change?
  ↓ Yes                    ↓ No
Keep using SoX         Switch to Audacity
(refine parameters)    (visual inspection)
  ↓                        ↓
Flag found?            See patterns?
  ↓ Yes                   ↓ Yes              ↓ No
Submit              Process with       Switch to metadata
                    Python/SoX         or steganography tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Time limits&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SoX phase: Max 5 minutes. If no progress, switch.&lt;/li&gt;
&lt;li&gt;Audacity phase: Max 10 minutes for visual inspection.&lt;/li&gt;
&lt;li&gt;If nothing after 15 minutes total on audio: Problem might not be audio-focused. Re-read problem description.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example decision points&lt;/strong&gt; :&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Minute 2&lt;/em&gt; : "Tried 6 sampling rates with SoX, heard voice at 22050Hz" → Stay with SoX, refine &lt;em&gt;Minute 5&lt;/em&gt; : "Tried sampling rates, channels, speed, reverse—all sound identical" → Switch to Audacity, check spectrogram &lt;em&gt;Minute 15&lt;/em&gt; : "Spectrogram shows nothing, SoX operations did nothing" → This isn't an audio problem. Check file metadata, steganography, encryption.&lt;/p&gt;

&lt;p&gt;The key is having predetermined time boxes. Without them, you'll sink 45 minutes into one tool because "just one more thing to try…"&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;When I started doing CTF audio challenges, I thought success meant "finding the right tool." I'd see writeups that said "use SoX" or "use Audacity" and think: "Oh, I need to learn that tool better."&lt;/p&gt;

&lt;p&gt;Wrong mindset.&lt;/p&gt;

&lt;p&gt;Success isn't about tools—it's about &lt;strong&gt;decision timing&lt;/strong&gt;. Knowing when to use SoX, when to abandon it, when to switch. The tool is just an instrument for testing hypotheses.&lt;/p&gt;

&lt;p&gt;"Silent Message" taught me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Decide fast&lt;/strong&gt; : 30 seconds to judge if normal playback is viable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test systematically&lt;/strong&gt; : Loop through parameters, don't guess randomly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recognize dead ends&lt;/strong&gt; : 3-5 attempts with no change = wrong direction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document as you go&lt;/strong&gt; : Command history is your lab notebook&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Know the win condition&lt;/strong&gt; : Flag format, submission requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SoX isn't magic. It's just really good at one specific thing: rapidly converting audio files with different parameter interpretations. When that's what you need, nothing beats it. When it's not, you're just wasting time.&lt;/p&gt;

&lt;p&gt;The real skill is knowing which situation you're in.&lt;/p&gt;

&lt;p&gt;Now when I see an audio problem, I don't think "which tool should I use?" I think: "What's my hypothesis, and what's the fastest way to test it?"&lt;/p&gt;

&lt;p&gt;Usually, that answer is SoX. But only if I'm asking the right question.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Further Reading Section Summary&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The "Further Reading" section introduces related articles from &lt;strong&gt;alsavaudomila.com&lt;/strong&gt; that complement the use of SoX by focusing on other essential command-line tools for CTF challenges. Below are the three featured articles with their context and links:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://alsavaudomila.com/ffmpeg-in-ctf-how-to-analyze-and-manipulate-audio-video-files/" rel="noopener noreferrer"&gt;FFmpeg in CTF: How to Analyze and Manipulate Audio/Video Files&lt;/a&gt;&lt;/strong&gt; This article is introduced as a companion to the SoX guide, focusing on &lt;strong&gt;FFmpeg&lt;/strong&gt;. It explains how to handle not only audio but also video files, which is crucial when flags are hidden within multimedia formats or require specific encoding/decoding techniques.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://alsavaudomila.com/dd-in-ctf-disk-imaging-extraction-and-common-challenge-patterns/" rel="noopener noreferrer"&gt;dd in CTF: Disk Imaging, Extraction, and Common Challenge Patterns&lt;/a&gt;&lt;/strong&gt; The second link points to a guide on the &lt;strong&gt;dd&lt;/strong&gt; command. In the context of forensics, this article explores how to create disk images and extract hidden data from raw files, providing a broader perspective on data recovery beyond simple audio analysis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://alsavaudomila.com/fdisk-in-ctf-partition-analysis-and-common-challenge-patterns/" rel="noopener noreferrer"&gt;fdisk in CTF: Partition Analysis and Common Challenge Patterns&lt;/a&gt;&lt;/strong&gt; The final recommendation focuses on &lt;strong&gt;fdisk&lt;/strong&gt; , a tool for partition table manipulation. It teaches readers how to analyze disk structures and identify hidden partitions where secret information might be stored, rounding out the technical skills needed for comprehensive CTF forensics.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ctf</category>
      <category>security</category>
      <category>linux</category>
      <category>forensics</category>
    </item>
    <item>
      <title>fdisk in CTF: Partition Analysis and Common Challenge Patterns</title>
      <dc:creator>rudy_candy</dc:creator>
      <pubDate>Mon, 20 Apr 2026 17:45:14 +0000</pubDate>
      <link>https://dev.to/rudycandy/fdisk-in-ctf-partition-analysis-and-common-challenge-patterns-ne5</link>
      <guid>https://dev.to/rudycandy/fdisk-in-ctf-partition-analysis-and-common-challenge-patterns-ne5</guid>
      <description>&lt;h2&gt;
  
  
  The Day fdisk Showed Me I Was Looking at the Wrong Layer
&lt;/h2&gt;

&lt;p&gt;There's a specific kind of CTF frustration that comes from staring at a disk image and knowing the flag is in there, but having no idea where to even start looking. I hit that wall hard on a picoCTF forensics challenge — one of the "Disk, disk, sleuth!" variants — where I spent close to an hour trying every file-level tool I knew before someone in Discord mentioned &lt;code&gt;fdisk&lt;/code&gt; and completely changed how I was thinking about the problem.&lt;/p&gt;

&lt;p&gt;I had been approaching it all wrong. I was reaching for &lt;code&gt;binwalk&lt;/code&gt;, &lt;code&gt;strings&lt;/code&gt;, &lt;code&gt;foremost&lt;/code&gt; — tools that look at content embedded inside files. But this challenge had a &lt;em&gt;partition table&lt;/em&gt; , and the flag was sitting in a partition that no filesystem tool would touch because it was never mounted anywhere. The moment I ran &lt;code&gt;fdisk -l disk.img&lt;/code&gt; and actually read the output — three partitions, one with a suspicious type ID I'd never seen before — I realized I'd been scanning the surface of the image while the answer was structured one layer deeper.&lt;/p&gt;

&lt;p&gt;That's what this article is about. Not just the &lt;code&gt;fdisk -l&lt;/code&gt; syntax — that takes thirty seconds to learn — but the &lt;em&gt;mindset shift&lt;/em&gt; that makes fdisk genuinely useful in CTF forensics. Understanding partition boundaries, sector math, and why tools like Autopsy miss things that live in unallocated space is the difference between solving this class of challenges quickly and wandering through them for hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  fdisk Syntax: The One Command That Actually Matters
&lt;/h2&gt;

&lt;p&gt;Unlike &lt;code&gt;dd&lt;/code&gt;, which has a handful of critical parameters to memorize, fdisk in CTF basically comes down to one command:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fdisk -l disk.img
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;-l&lt;/code&gt; flag lists partition information: partition numbers, start and end sectors, total sector count, sector size, and filesystem type identifiers. Everything else you need for CTF work — the sector-to-byte math, the gap calculations, the dd extraction commands — flows from reading this output correctly.&lt;/p&gt;

&lt;p&gt;Here's what typical output looks like on a CTF disk image:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ fdisk -l disk.img
Disk disk.img: 100 MiB, 104857600 bytes, 204800 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes

Device      Boot  Start    End  Sectors  Size  Id  Type
disk.img1         2048   43007   40960   20M  83  Linux
disk.img2        43008  122879   79872   39M   7  HPFS/NTFS/exFAT
disk.img3       124928  204799   79872   39M  8e  Linux LVM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The fields that matter for CTF: &lt;strong&gt;Start&lt;/strong&gt; (first sector of the partition), &lt;strong&gt;Sectors&lt;/strong&gt; (total sector count), and &lt;strong&gt;Id&lt;/strong&gt; (partition type code). Notice the gap between disk.img2 ending at 122879 and disk.img3 starting at 124928 — those 2048 unallocated sectors are 1MB of space that no filesystem tool will touch unless you're explicitly looking for it. Challenge authors love that gap.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Sector-to-Byte Calculation (Do This Once, Do It Right)
&lt;/h3&gt;

&lt;p&gt;Everything in fdisk output is in sectors. Everything in dd (the extraction tool you'll pair with fdisk) is configurable in whatever unit you choose. The simplest approach: set &lt;code&gt;bs=512&lt;/code&gt; in dd, and the sector values from fdisk map directly to &lt;code&gt;skip&lt;/code&gt; and &lt;code&gt;count&lt;/code&gt; without any multiplication.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Extract partition 2 — sector values from fdisk map directly to dd parameters
$ dd if=disk.img of=partition2.img bs=512 skip=43008 count=79872
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If you ever need raw byte offsets — for a hex editor or a tool that takes byte positions — the math is: byte offset = sector × 512. disk.img2 starts at sector 43008, which is byte 22,020,096. But in practice, I almost always use &lt;code&gt;bs=512&lt;/code&gt; and let the sector numbers do the work directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  One Detail That Trips Up Beginners Every Time
&lt;/h3&gt;

&lt;p&gt;When extracting with dd, the &lt;code&gt;count&lt;/code&gt; parameter is the &lt;strong&gt;Sectors&lt;/strong&gt; column from fdisk output — not End minus Start, and not the End value itself. fdisk gives you the total sector count directly in that column; use it. Using the End sector as count will extract from the wrong position entirely, and dd won't warn you — it'll just silently produce an incorrect image. Always double-check: &lt;code&gt;count&lt;/code&gt; = Sectors column value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rabbit Hole: The Hour I Wasted Before Running fdisk
&lt;/h2&gt;

&lt;p&gt;Here's exactly what I tried before reaching for fdisk — I want to be honest about this because I think it maps to what most beginners attempt:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ file disk.img
disk.img: DOS/MBR boot record; partition 1 : ID=0x83, start-CHS (0x0,32,33),
end-CHS (0x14,223,19), startsector 2048, 40960 sectors; partition 2 : ID=0x07...
# Okay, partitions exist. Let me just mount it...

$ sudo mount -o loop disk.img /mnt/disk
mount: /mnt/disk: wrong fs type, bad option, bad superblock on /dev/loop0
# Mounting the raw image without an offset doesn't work on partitioned images.
# I spent 15 minutes trying different mount flags here.

$ strings disk.img | grep -i "flag&amp;amp;#124;ctf&amp;amp;#124;pico"
(no output)
# Flag was inside an unmounted filesystem, not raw ASCII in the image

$ binwalk disk.img

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             DOS/MBR boot record
1048576       0x100000        Linux EXT2 filesystem data
# Saw the EXT2 hit and tried extracting it with dd — got a partial image that
# mounted but only contained the first partition's content. Missed partition 3.

$ foremost -i disk.img -o output/
# 12 minutes later: recovered some JPEG files, none relevant
# foremost carved by signature without partition context — wrong tool here

$ sudo autopsy &amp;amp;
# Waited 8 minutes for the browser GUI
# Autopsy found files in partition 1 and 2 — nothing in partition 3
# It had silently skipped partition 3 due to the unrecognized type ID (0x8e)
# I didn't know that's what happened until much later
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That Autopsy session was the specific failure point. Partition 3 had type ID &lt;code&gt;8e&lt;/code&gt; (Linux LVM), which in this challenge was a deliberate mislabel — the author set an unusual partition type to make standard tools skip over it while still keeping the actual content as a mountable ext2 filesystem. Autopsy didn't parse it as a recognized filesystem, reported it as empty, and I had no reason to dig further because Autopsy had "scanned everything."&lt;/p&gt;

&lt;p&gt;The fix took about ninety seconds once I ran fdisk properly. Saw three partitions, noticed 8e was suspicious, extracted it with dd, ran &lt;code&gt;file&lt;/code&gt; on the result — it came back as ext2. Mounted it. Flag was in &lt;code&gt;/home/user/flag.txt&lt;/code&gt;. The entire challenge hinged on knowing that partition type IDs are just labels and can't be trusted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five CTF Patterns Where fdisk Is the Right Starting Point
&lt;/h2&gt;

&lt;p&gt;After working through multiple forensics disk image challenges, the scenarios where fdisk matters fall into recognizable patterns. Here's what each looks like and where beginners typically get stuck:&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 1: Partition Extraction with dd
&lt;/h3&gt;

&lt;p&gt;The most common pattern. fdisk shows a partition with content; you extract it with dd and analyze the result. The calculation is straightforward but error-prone if you're rushing.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ fdisk -l disk.img
...
Device      Boot  Start    End  Sectors  Size  Id  Type
disk.img1         2048   43007   40960   20M  83  Linux
disk.img2        43008  204799  161792   79M  83  Linux

# Extract partition 2: skip=Start, count=Sectors (not End)
$ dd if=disk.img of=part2.img bs=512 skip=43008 count=161792

$ file part2.img
part2.img: Linux rev 1.0 ext2 filesystem data

$ mkdir /tmp/mnt &amp;amp;&amp;amp; sudo mount part2.img /tmp/mnt
$ ls /tmp/mnt
flag.txt  home/  lost+found/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The mistake I made early: confusing the &lt;strong&gt;End&lt;/strong&gt; column with the count. End is the last sector number, not the total sector count. fdisk gives you the total directly in the &lt;strong&gt;Sectors&lt;/strong&gt; column — that's your &lt;code&gt;count&lt;/code&gt; value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 2: Hidden or Mislabeled Partitions
&lt;/h3&gt;

&lt;p&gt;CTF authors add partitions with misleading type IDs, or use unusual IDs that standard tools don't recognize and quietly skip. The tell is an unfamiliar type in the &lt;code&gt;Type&lt;/code&gt; column — things like "Unknown", "W95 FAT32 (LBA)", or "Linux LVM" on an image that's clearly not a production server.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ fdisk -l tricky.img
...
Device       Boot  Start    End  Sectors  Size  Id  Type
tricky.img1        2048   20479   18432    9M  83  Linux
tricky.img2       20480   40959   20480   10M  7f  Unknown

# Type 0x7f is unusual — extract and verify what's actually there
$ dd if=tricky.img of=hidden_part.img bs=512 skip=20480 count=20480

$ file hidden_part.img
hidden_part.img: Linux rev 1.0 ext2 filesystem data
# Despite the "Unknown" label — it's actually ext2

$ sudo mount hidden_part.img /tmp/hidden
$ find /tmp/hidden -name "flag*"
/tmp/hidden/secret/flag.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The rule I follow now: never trust the &lt;code&gt;Type&lt;/code&gt; label. A partition's type ID is a single byte that anyone can set to anything. Always extract and run &lt;code&gt;file&lt;/code&gt; on unknown or suspicious types before writing them off.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 3: Unallocated Space Between Partitions
&lt;/h3&gt;

&lt;p&gt;Gaps between partition boundaries are invisible to filesystem tools — they don't belong to any partition — but they can contain deleted files, embedded data, or raw flag strings. Spot them by checking whether each partition's End+1 equals the next partition's Start.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ fdisk -l gappy.img
...
Device       Boot  Start    End  Sectors  Size  Id  Type
gappy.img1         2048   20479   18432    9M  83  Linux
gappy.img2        24576  204799  180224   88M  83  Linux

# Gap: sectors 20480 to 24575 = 4096 sectors = 2MB of unallocated space
# That's not alignment padding — 2MB is a deliberate gap in CTF context

$ dd if=gappy.img of=gap.bin bs=512 skip=20480 count=4096

$ strings gap.bin | grep -i "flag&amp;amp;#124;ctf&amp;amp;#124;pico"
picoCTF{h1dd3n_1n_th3_g4p_a7b2c3}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Normal partition alignment padding is typically 2048 sectors (1MB) between sector 0 and the first partition. Gaps larger than that — especially gaps in the middle of the image — are almost always intentional in CTF challenges. Also check the tail: if the last partition ends well before the disk's total sector count, that trailing space is another standard hiding spot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 4: Filesystem Identification for Targeted Analysis
&lt;/h3&gt;

&lt;p&gt;Different partition types contain different kinds of artifacts. Knowing what you're dealing with before you start digging saves significant time. NTFS partitions have Windows event logs, USN journals, and alternate data streams. Linux ext2/3/4 partitions have &lt;code&gt;.bash_history&lt;/code&gt;, shadow files, and syslog. Swap partitions sometimes contain memory fragments — including plaintext credentials or flag strings that were briefly loaded into RAM.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ fdisk -l multi.img
...
Device      Boot  Start    End  Sectors  Size  Id  Type
multi.img1         2048   43007   40960   20M  83  Linux       # ext2/3/4
multi.img2        43008  122879   79872   39M   7  NTFS        # Windows filesystem
multi.img3       122880  143359   20480   10M  82  Linux swap  # memory fragments

# Swap partition: strings can find in-memory artifacts
$ dd if=multi.img of=swap.img bs=512 skip=122880 count=20480
$ strings swap.img | grep -i "password&amp;amp;#124;flag&amp;amp;#124;secret" | head -20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Swap partitions are an underrated source in CTF disk imaging challenges. I've found plaintext flags in swap regions on two separate challenges where the filesystem partitions had nothing — the flag had been used in a process that paged memory to swap before the image was captured.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 5: Corrupted or Malformed Partition Tables
&lt;/h3&gt;

&lt;p&gt;Sometimes fdisk can't read the partition table cleanly — overlapping sectors, impossible values, a corrupted MBR signature. This is itself a clue: the challenge is about table forensics, not just extracting a known partition. When fdisk fails, switch to &lt;code&gt;mmls&lt;/code&gt; from Sleuth Kit.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ fdisk -l corrupted.img
GPT: not present
MBR: not present
# fdisk can't find a valid partition structure

# mmls is more tolerant of malformed tables
$ mmls corrupted.img
DOS Partition Table
Offset Sector: 0
Units are in 512-byte sectors

      Slot      Start        End          Length       Description
000:  Meta      0000000000   0000000000   0000000001   Primary Table (#0)
001:  -------   0000000000   0000002047   0000002048   Unallocated
002:  000:000   0000002048   0000043007   0000040960   Linux (0x83)
003:  -------   0000043008   0000045055   0000002048   Unallocated
004:  000:001   0000045056   0000204799   0000159744   Unknown Type (0xcc)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;mmls explicitly labels unallocated regions and parses tables that fdisk gives up on. The key difference: fdisk relies on the MBR/GPT signature being intact to even begin reading. mmls reads the raw sector data more defensively and will reconstruct partial partition entries even from a damaged table. The partition with type 0xcc was the payload in the challenge above — fdisk returned nothing, but mmls found it and gave me exact Start and Length values I could feed directly to dd. When fdisk says "no partition table found" on a file that &lt;code&gt;file&lt;/code&gt; identifies as a boot record, switch to mmls immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  fdisk vs Other Tools: How I Actually Decide
&lt;/h2&gt;

&lt;p&gt;fdisk reads partition tables. It doesn't carve files, parse filesystems, or recover deleted data — and reaching for it when those are your actual needs wastes time. Here's how I make the call in practice:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;My First Choice&lt;/th&gt;
&lt;th&gt;Why Not fdisk?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Disk image with unknown structure&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;fdisk -l&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Corrupted or unreadable partition table&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;mmls&lt;/strong&gt; (Sleuth Kit)&lt;/td&gt;
&lt;td&gt;fdisk gives up on malformed tables; mmls keeps going and shows unallocated regions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browse filesystem contents visually&lt;/td&gt;
&lt;td&gt;Autopsy / sudo mount&lt;/td&gt;
&lt;td&gt;fdisk doesn't open or navigate filesystems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Carve files without a filesystem&lt;/td&gt;
&lt;td&gt;foremost or binwalk&lt;/td&gt;
&lt;td&gt;fdisk only reads partition boundaries, not file signatures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extract a specific partition&lt;/td&gt;
&lt;td&gt;fdisk → then &lt;strong&gt;dd&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;fdisk identifies the boundaries; dd extracts — they're a matched pair&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detect unallocated gaps&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;fdisk -l&lt;/strong&gt; (manual gap math)&lt;/td&gt;
&lt;td&gt;mmls labels gaps explicitly if you want them surfaced automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NTFS-specific artifacts (ADS, USN journal)&lt;/td&gt;
&lt;td&gt;Autopsy or ntfsinfo&lt;/td&gt;
&lt;td&gt;fdisk identifies the partition type but can't read NTFS structures&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The failure mode I keep seeing in beginners: running Autopsy directly on the raw disk image and trusting that it finds everything. Autopsy works at the filesystem level — it will find files in recognized partitions, but it silently skips partitions with unusual type IDs and won't surface anything in unallocated gaps. Run fdisk first, identify which partitions are worth investigating, then point Autopsy at the specific extracted images rather than the raw disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Trial Process Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Why it failed / succeeded&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;File identification&lt;/td&gt;
&lt;td&gt;file disk.img&lt;/td&gt;
&lt;td&gt;DOS/MBR boot record&lt;/td&gt;
&lt;td&gt;Confirmed it's a partitioned disk — useful, but told me nothing about what was inside each partition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Direct mount&lt;/td&gt;
&lt;td&gt;sudo mount -o loop disk.img /mnt&lt;/td&gt;
&lt;td&gt;mount: wrong fs type&lt;/td&gt;
&lt;td&gt;Mounting the raw image without an offset doesn't work on partitioned images — wasted 15 minutes trying different flags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;String search&lt;/td&gt;
&lt;td&gt;strings disk.img&lt;/td&gt;
&lt;td&gt;grep flag&lt;/td&gt;
&lt;td&gt;No output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Auto-carve&lt;/td&gt;
&lt;td&gt;foremost -i disk.img -o out/&lt;/td&gt;
&lt;td&gt;JPEGs recovered, no flag&lt;/td&gt;
&lt;td&gt;foremost uses file signatures without partition context — carved false positives from the wrong region&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Autopsy full scan&lt;/td&gt;
&lt;td&gt;autopsy (GUI)&lt;/td&gt;
&lt;td&gt;Files in partitions 1–2, nothing in partition 3&lt;/td&gt;
&lt;td&gt;Autopsy silently skipped partition 3 due to unrecognized type ID — I didn't know it had done this&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;fdisk scan&lt;/td&gt;
&lt;td&gt;fdisk -l disk.img&lt;/td&gt;
&lt;td&gt;Three partitions visible; type 0x8e on partition 3 looks suspicious&lt;/td&gt;
&lt;td&gt;Turning point — saw the full partition layout for the first time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Extract partition 3&lt;/td&gt;
&lt;td&gt;dd if=disk.img of=p3.img bs=512 skip=124928 count=79872&lt;/td&gt;
&lt;td&gt;Clean image extracted&lt;/td&gt;
&lt;td&gt;fdisk sector values mapped directly to dd skip/count — no calculation needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Verify filesystem type&lt;/td&gt;
&lt;td&gt;file p3.img&lt;/td&gt;
&lt;td&gt;Linux rev 1.0 ext2 filesystem data&lt;/td&gt;
&lt;td&gt;Despite the "Linux LVM" label from fdisk, it's actually ext2 — the partition ID was a deliberate mislabel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Mount and explore&lt;/td&gt;
&lt;td&gt;sudo mount p3.img /tmp/p3 &amp;amp;&amp;amp; ls /tmp/p3&lt;/td&gt;
&lt;td&gt;flag.txt present in root&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Read flag&lt;/td&gt;
&lt;td&gt;cat /tmp/p3/flag.txt&lt;/td&gt;
&lt;td&gt;picoCTF{…}&lt;/td&gt;
&lt;td&gt;Should have run fdisk at step 1 — everything before step 6 was wasted time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why Partition-Level Thinking Matters Beyond CTF
&lt;/h2&gt;

&lt;p&gt;Partition tables exist below the filesystem layer — they're the first structure on any block storage device, before any filesystem driver gets involved. This is why forensic investigators care about them: data in unallocated sectors, between partitions, or in partitions that were never mounted still shows up in a raw partition table scan, even if every filesystem tool reports nothing found.&lt;/p&gt;

&lt;p&gt;In real incident response, deleted partitions are a common evidence source. An attacker can delete a partition containing exfiltrated data or tooling, but the bytes remain on disk until overwritten. fdisk and mmls often detect the ghost of a former partition entry — the data may be recoverable even after the partition record is gone. CTF challenge authors use the same principle: they create data structures at the partition layer specifically because most beginners only look at the filesystem layer.&lt;/p&gt;

&lt;p&gt;The insight that shifts how you think about disk forensics: a disk image is not a folder. It's a byte sequence with structure at multiple layers — the partition table at the outermost level, filesystems within each partition, individual file structures within those filesystems. Each layer can hide data from the others. fdisk gives you visibility into the outermost layer, which is precisely the one beginners tend to skip.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I'd Solve It Faster Next Time
&lt;/h2&gt;

&lt;p&gt;My first-three-minutes workflow for any disk image challenge now — hard-won from running the wrong tools in the wrong order too many times:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Step 1: What kind of image is this?
file target.img

# Step 2: What partitions exist, and are any suspicious?
fdisk -l target.img
# Look for: unusual type IDs, gaps between partitions, trailing unallocated space

# Step 3: Note any gaps
# gap = next_Start - current_End - 1 (in sectors)
# If gap &amp;gt; 2048 sectors, extract it:
dd if=target.img of=gap.bin bs=512 skip=&amp;lt;end_of_prev_part+1&amp;gt; count=&amp;lt;gap_size&amp;gt;
strings gap.bin | grep -i "flag&amp;amp;#124;ctf"

# Step 4: Extract each partition that looks interesting
dd if=target.img of=partN.img bs=512 skip=&amp;lt;Start&amp;gt; count=&amp;lt;Sectors&amp;gt;

# Step 5: Verify what's actually in each extracted partition
file partN.img
# Don't trust the fdisk type label — verify with file every time

# Step 6: Mount or analyze
sudo mount partN.img /tmp/mnt
ls -la /tmp/mnt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;I run &lt;code&gt;fdisk -l&lt;/code&gt; before anything else now — before binwalk, before strings, before Autopsy. It takes two seconds and immediately tells me whether I'm dealing with a partitioned image (where the partition structure is the puzzle) or a raw binary (where I should switch to binwalk and dd). That decision used to cost me twenty minutes of trying the wrong tools. Now it costs two seconds.&lt;/p&gt;

&lt;p&gt;One specific thing to watch: compare the total sector count at the top of fdisk output against the End sector of the last partition. If the last partition ends significantly before the total, that trailing space is another common hiding spot — extract it the same way you'd extract a gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;p&gt;For an overview of the full forensics toolkit that fdisk fits into, &lt;a href="https://alsavaudomila.com/ctf-forensics-tools/" rel="noopener noreferrer"&gt;CTF Forensics Tools: The Ultimate Guide for Beginners&lt;/a&gt; covers when to reach for each tool — fdisk, dd, binwalk, foremost, Autopsy, and Sleuth Kit all have distinct roles, and understanding that division of labor is half the battle in disk imaging challenges.&lt;/p&gt;

&lt;p&gt;Here are related articles from &lt;a href="https://alsavaudomila.com/" rel="noopener noreferrer"&gt;alsavaudomila.com&lt;/a&gt; that pair directly with this topic:&lt;/p&gt;

&lt;p&gt;fdisk finds partition boundaries; &lt;a href="https://alsavaudomila.com/dd-in-ctf-forensics/" rel="noopener noreferrer"&gt;dd&lt;/a&gt; extracts from them. These two tools are a matched pair in every disk imaging challenge — the article on dd covers the extraction workflow in detail, including the sector math, the &lt;code&gt;conv=notrunc&lt;/code&gt; flag, and how to handle the count calculation without making mistakes.&lt;/p&gt;

&lt;p&gt;Once fdisk identifies a suspicious partition and dd extracts it, &lt;a href="https://alsavaudomila.com/sleuth-kit-autopsy-ctf/" rel="noopener noreferrer"&gt;Sleuth Kit and Autopsy&lt;/a&gt; handle the filesystem investigation layer — browsing file trees, recovering deleted files, and reading metadata that a simple mount won't surface. The article also covers &lt;code&gt;mmls&lt;/code&gt; as a more forensically accurate alternative to fdisk when partition tables are corrupted or unusual.&lt;/p&gt;

&lt;p&gt;For challenges where the disk image contains embedded files in addition to (or instead of) a partition structure, the &lt;a href="https://alsavaudomila.com/binwalk-in-ctf/" rel="noopener noreferrer"&gt;binwalk guide&lt;/a&gt; explains how to read scan output accurately, which offsets to trust, and when manual dd extraction beats the auto-extract mode — a natural complement to the partition-level analysis that fdisk handles.&lt;/p&gt;

</description>
      <category>ctf</category>
      <category>security</category>
      <category>linux</category>
      <category>forensics</category>
    </item>
    <item>
      <title>binwalk in CTF: Spot False Positives Fast</title>
      <dc:creator>rudy_candy</dc:creator>
      <pubDate>Mon, 20 Apr 2026 17:44:42 +0000</pubDate>
      <link>https://dev.to/rudycandy/binwalk-in-ctf-spot-false-positives-fast-7fp</link>
      <guid>https://dev.to/rudycandy/binwalk-in-ctf-spot-false-positives-fast-7fp</guid>
      <description>&lt;p&gt;&lt;strong&gt;binwalk&lt;/strong&gt; is a binary analysis tool that scans files for embedded signatures — ZIPs inside PNGs, compressed firmware blobs, appended archives. In CTF forensics it's usually one of the first tools you reach for. But the real skill isn't running it; it's reading the output correctly. While working on the "Digging for Treasure" challenge at BurnerCTF 2025, I ran binwalk and watched more than ten detections scroll across the screen. My first thought was "this is a goldmine." Thirty minutes later, when I realized every single one of them was a false positive, I learned firsthand just how dangerous it is to blindly trust tool output.&lt;/p&gt;

&lt;p&gt;Rather than listing binwalk commands one by one, this article focuses on the noise problem you actually face in CTF scenarios and the decision-making process for finding the signal that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Output from BurnerCTF 2025: Separating Noise from Signal
&lt;/h2&gt;

&lt;p&gt;Here is the actual output from running binwalk on the "Digging for Treasure" challenge.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ binwalk treasure.png

DECIMAL       HEXADECIMAL     DESCRIPTION
------------------------------------------------------------------------
0             0x0             PNG image, 1536 x 1024, 8-bit/color RGB, non-interlaced
3860          0xF14           Certificate in DER format (x509 v3), header length: 4, sequence length: 1573
5440          0x1540          Certificate in DER format (x509 v3), header length: 4, sequence length: 1746
7193          0x1C19          Certificate in DER format (x509 v3), header length: 4, sequence length: 1455
8688          0x21F0          Object signature in DER format (PKCS header length: 4, sequence length: 5983
8857          0x2299          Certificate in DER format (x509 v3), header length: 4, sequence length: 1573
10634         0x298A          Certificate in DER format (x509 v3), header length: 4, sequence length: 1716
12354         0x3042          Certificate in DER format (x509 v3), header length: 4, sequence length: 1421
15075         0x3AE3          Zlib compressed data, default compression
2706518       0x294C56        TIFF image data, big-endian, offset of first image directory: 8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;At first glance, it looks like the PNG contains six DER certificates, zlib data, and an embedded TIFF. In reality, every detection from offset 3860 through 15075 was a false positive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why So Many DER Certificates Get Detected
&lt;/h3&gt;

&lt;p&gt;PNG files store image data compressed with zlib inside IDAT chunks. Within that compressed binary data, patterns can appear that happen to match the magic bytes of a DER certificate (sequences starting with &lt;code&gt;30 82&lt;/code&gt;). Since binwalk determines file types through binary signature matching without considering context, it reports every byte sequence that looks certificate-like.&lt;/p&gt;

&lt;p&gt;The two bytes of the DER sequence tag (&lt;code&gt;0x30&lt;/code&gt;) and the length field (&lt;code&gt;0x82&lt;/code&gt;) appear frequently in compressed data. This is the root cause of the false positive flood inside IDAT chunks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Criteria for Finding Real Signals
&lt;/h3&gt;

&lt;p&gt;In the output above, the only detection actually worth investigating was the last line: the TIFF at offset &lt;code&gt;0x294C56&lt;/code&gt; (2,706,518 bytes). Here is the reasoning behind that call.&lt;/p&gt;

&lt;p&gt;First, check the context of each offset. Detections clustered near the beginning of the file (within the range where IDAT chunks exist) are likely false positives inside compressed data. On the other hand, an isolated detection of a different format near the end of the file — close to the total file size — is likely data appended after the file's end.&lt;/p&gt;

&lt;p&gt;Next, compare against the file size. Run &lt;code&gt;ls -la treasure.png&lt;/code&gt; to check the file size and see whether 2,706,518 bytes corresponds to near the end of the file. If data exists beyond the PNG's native IEND chunk, it was clearly appended.&lt;/p&gt;

&lt;p&gt;Also check the consistency of detected formats. If six DER certificates are detected in a row and they are densely packed within small offset intervals, you should assume binwalk is scanning through a single compressed data block.&lt;/p&gt;

&lt;h2&gt;
  
  
  The -e Option Trap: Avoiding Extraction Chaos
&lt;/h2&gt;

&lt;p&gt;Using binwalk's extraction option &lt;code&gt;-e&lt;/code&gt; dumps everything detected into an &lt;code&gt;_extracted/&lt;/code&gt; folder. But running it against output riddled with false positives generates a flood of useless files and turns the folder into a mess.&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# A common mistake&lt;br&gt;
$ binwalk -e treasure.png
&lt;h1&gt;
  
  
  Result: _extracted/ gets filled with unwanted files
&lt;/h1&gt;

&lt;p&gt;$ ls _extracted/treasure.png.extracted/&lt;br&gt;
F14         F14.der&lt;br&gt;
1540        1540.der&lt;br&gt;
1C19        1C19.der&lt;br&gt;
21F0        21F0.der&lt;br&gt;
2299        2299.der&lt;br&gt;
298A        298A.der&lt;br&gt;
3042        3042.der&lt;br&gt;
3AE3        3AE3.zlib&lt;br&gt;
294C56.tiff&lt;br&gt;
294C56&lt;/p&gt;
&lt;h1&gt;
  
  
  Manual extraction from a specific offset using dd
&lt;/h1&gt;
&lt;h1&gt;
  
  
  Extract the TIFF from offset 0x294C56 (2706518 bytes)
&lt;/h1&gt;

&lt;p&gt;$ dd if=treasure.png bs=1 skip=2706518 of=extracted.tiff&lt;/p&gt;
&lt;h1&gt;
  
  
  Or use binwalk's --dd option to extract only a specific type
&lt;/h1&gt;

&lt;p&gt;$ binwalk --dd='tiff image:tiff' treasure.png&lt;br&gt;
&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Core binwalk Commands and When to Use Them&lt;br&gt;
&lt;/h2&gt;
&lt;br&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Scan a file for signatures&lt;br&gt;
$ binwalk file.bin
&lt;h1&gt;
  
  
  Detailed entropy analysis (-B)
&lt;/h1&gt;

&lt;p&gt;$ binwalk -B file.bin&lt;/p&gt;
&lt;h1&gt;
  
  
  Output an entropy graph (high-entropy regions = likely encrypted or compressed data)
&lt;/h1&gt;

&lt;p&gt;$ binwalk -E file.bin&lt;/p&gt;
&lt;h1&gt;
  
  
  Extract detected files (watch out for false positives)
&lt;/h1&gt;

&lt;p&gt;$ binwalk -e file.bin&lt;/p&gt;
&lt;h1&gt;
  
  
  Recursive extraction (re-scan extracted files)
&lt;/h1&gt;

&lt;p&gt;$ binwalk -Me file.bin&lt;/p&gt;
&lt;h1&gt;
  
  
  Search for a specific signature only
&lt;/h1&gt;

&lt;p&gt;$ binwalk -R "\x50\x4b\x03\x04" file.bin   # Search for ZIP signature&lt;/p&gt;
&lt;h1&gt;
  
  
  Scan a firmware image (common in CTF hardware/misc challenges)
&lt;/h1&gt;

&lt;p&gt;$ binwalk firmware.bin&lt;/p&gt;
&lt;h1&gt;
  
  
  Recursive extraction when the image contains filesystems like squashfs or cpio
&lt;/h1&gt;

&lt;p&gt;$ binwalk -Me firmware.bin&lt;br&gt;
&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  How Magic Numbers and Binary Signatures Work&lt;br&gt;
&lt;/h2&gt;

&lt;p&gt;Understanding how binwalk identifies file types is key to spotting false positives. Most file formats have a fixed signature (magic number) at the start of the file. PNG, for example, uses &lt;code&gt;89 50 4E 47 0D 0A 1A 0A&lt;/code&gt; (&lt;code&gt;\x89PNG\r\n\x1a\n&lt;/code&gt;), and ZIP uses &lt;code&gt;50 4B 03 04&lt;/code&gt; (&lt;code&gt;PK\x03\x04&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;binwalk matches signature patterns from its internal database against every offset in a file using a sliding window. This approach is powerful, but in regions with byte sequences that look random — like compressed or encrypted data — coincidental matches happen all the time. In the case of DER format, the sequence tag &lt;code&gt;0x30 0x82&lt;/code&gt; appears frequently in binary data that has nothing to do with certificates, and binwalk reports every single one of those matches.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Not to Use binwalk: Choosing the Right Tool
&lt;/h2&gt;

&lt;p&gt;If LSB steganography is suspected, use &lt;code&gt;zsteg&lt;/code&gt;. The technique of hiding data in the least significant bits of PNG pixels cannot be detected by binwalk's signature scanning — binwalk found nothing on the "RED" picoCTF challenge where zsteg recovered the flag immediately. For metadata inspection, use &lt;code&gt;exiftool&lt;/code&gt;; GPS coordinates, comment fields, and custom XMP tags are invisible to binwalk because they don't appear as binary signatures. To diagnose file corruption, reach for &lt;code&gt;pngcheck&lt;/code&gt; or the &lt;code&gt;file&lt;/code&gt; command first — running binwalk on a deliberately corrupted PNG often produces misleading output.&lt;/p&gt;

&lt;p&gt;binwalk's full signature database and source are maintained at the &lt;a href="https://github.com/ReFirmLabs/binwalk" rel="noopener noreferrer"&gt;ReFirmLabs/binwalk GitHub repository&lt;/a&gt;. The &lt;code&gt;src/binwalk/magic/&lt;/code&gt; directory lists every pattern binwalk scans for, which is useful when you need to understand why a specific false positive is being triggered.&lt;/p&gt;

&lt;h2&gt;
  
  
  binwalk vs foremost vs strings: Comparison Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Recommended Tool&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Suspected embedded file in a different format&lt;/td&gt;
&lt;td&gt;binwalk -e&lt;/td&gt;
&lt;td&gt;Signature scanning with automatic extraction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Appended data at the end of a file&lt;/td&gt;
&lt;td&gt;binwalk (check offset) + dd&lt;/td&gt;
&lt;td&gt;Identify the exact extraction point, then extract manually&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recovering files from a disk image&lt;/td&gt;
&lt;td&gt;foremost&lt;/td&gt;
&lt;td&gt;File carving that ignores filesystem structure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Searching for printable strings in a binary&lt;/td&gt;
&lt;td&gt;strings&lt;/td&gt;
&lt;td&gt;Fast search for flag strings or config values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analyzing filesystem structure in firmware&lt;/td&gt;
&lt;td&gt;binwalk -Me&lt;/td&gt;
&lt;td&gt;Recursive extraction unpacks squashfs/cramfs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LSB steganography suspected&lt;/td&gt;
&lt;td&gt;zsteg&lt;/td&gt;
&lt;td&gt;binwalk does not detect bit-level manipulation of pixel data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flag hidden in EXIF metadata&lt;/td&gt;
&lt;td&gt;exiftool&lt;/td&gt;
&lt;td&gt;Metadata fields do not appear as binary signatures&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Practical binwalk Workflow for CTF
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Step 1: Start with the file command to get basic info&lt;br&gt;
$ file challenge.png
&lt;h1&gt;
  
  
  Step 2: Check the file size (you'll compare this against offsets later)
&lt;/h1&gt;

&lt;p&gt;$ ls -la challenge.png&lt;/p&gt;
&lt;h1&gt;
  
  
  Step 3: Scan with binwalk
&lt;/h1&gt;

&lt;p&gt;$ binwalk challenge.png&lt;/p&gt;
&lt;h1&gt;
  
  
  Step 4: Interpret the output
&lt;/h1&gt;
&lt;h1&gt;
  
  
  - Focus on detections at offsets close to the file size
&lt;/h1&gt;
&lt;h1&gt;
  
  
  - Be skeptical of DER/certificate detections clustered near the beginning
&lt;/h1&gt;
&lt;h1&gt;
  
  
  - Prioritize isolated detections of a different format at a unique offset
&lt;/h1&gt;
&lt;h1&gt;
  
  
  Step 5: Manually extract only from promising offsets
&lt;/h1&gt;

&lt;p&gt;$ dd if=challenge.png bs=1 skip=2706518 of=candidate.tiff&lt;/p&gt;
&lt;h1&gt;
  
  
  Step 6: Use strings to search for text if needed
&lt;/h1&gt;

&lt;p&gt;$ strings candidate.tiff | grep -i "ctf&amp;amp;#124;flag"&lt;br&gt;
&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Tips for Reducing binwalk False Positives&lt;br&gt;
&lt;/h2&gt;

&lt;p&gt;Using entropy analysis (the &lt;code&gt;-E&lt;/code&gt; option) lets you visually map high-entropy regions (compressed or encrypted data) within a file. Combining it with the &lt;code&gt;strings&lt;/code&gt; command is also effective — if you spot file header strings like "JFIF", "Exif", or "PK", use those offsets as a starting point.&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Inspect bytes around offset 0x294C56&lt;br&gt;
$ xxd challenge.png | grep -A 3 "00294c"
&lt;h1&gt;
  
  
  Or use dd to check the bytes around that area
&lt;/h1&gt;

&lt;p&gt;$ dd if=challenge.png bs=1 skip=2706510 count=32 | xxd&lt;br&gt;
&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Further Reading&lt;br&gt;
&lt;/h2&gt;

&lt;p&gt;For more CTF forensics tools and decision guides, see the &lt;a href="https://alsavaudomila.com/ctf-forensics-tools-the-complete-guide-for-beginners/" rel="noopener noreferrer"&gt;CTF Forensics Tools: The Complete Guide&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ctf</category>
      <category>security</category>
      <category>linux</category>
      <category>forensics</category>
    </item>
    <item>
      <title>CTF Forensics Tools: The Ultimate Guide for Beginners</title>
      <dc:creator>rudy_candy</dc:creator>
      <pubDate>Mon, 20 Apr 2026 17:39:36 +0000</pubDate>
      <link>https://dev.to/rudycandy/ctf-forensics-tools-the-ultimate-guide-for-beginners-5gn2</link>
      <guid>https://dev.to/rudycandy/ctf-forensics-tools-the-ultimate-guide-for-beginners-5gn2</guid>
      <description>&lt;p&gt;When I first started picoCTF forensics challenges, I had a folder full of installed tools and no idea which one to open first. Every challenge felt like staring at a locked box with twenty keys on the table. The problem wasn't a lack of tools — it was not knowing the decision process behind picking the right one.&lt;/p&gt;

&lt;p&gt;This page is what I wish had existed when I started. Not a list of tools with feature descriptions, but a map of &lt;em&gt;when&lt;/em&gt; to reach for each one and — just as importantly — when to put it down and try something else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step Zero: Identify What You're Dealing With
&lt;/h2&gt;

&lt;p&gt;Before touching any specialized tool, run these two commands on every unknown file:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ file challenge.bin
challenge.bin: Zip archive data, at least v2.0 to extract

$ xxd challenge.bin | head -5
00000000: 504b 0304 1400 0000 0800 ...  PK..........
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code&gt;file&lt;/code&gt; reads magic bytes and tells you the actual format regardless of the extension. &lt;code&gt;xxd&lt;/code&gt; shows the raw hex so you can spot a corrupted header immediately. I've lost count of how many times a file named &lt;code&gt;data.png&lt;/code&gt; turned out to be a ZIP or a disk image — the magic bytes &lt;code&gt;50 4B 03 04&lt;/code&gt; (PK) are a dead giveaway for ZIP regardless of what the filename says.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;file&lt;/code&gt; says "data" or gives something unexpected, that's your first clue. See the &lt;a href="https://alsavaudomila.com/corrupted-file-picoctf-writeup/" rel="noopener noreferrer"&gt;Corrupted File writeup&lt;/a&gt; for a real example of this — the challenge handed me a PNG with a broken magic byte, and &lt;code&gt;file&lt;/code&gt; was what made that obvious.&lt;/p&gt;

&lt;h2&gt;
  
  
  By File Type: Which Tool to Reach For
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Disk Images (.img, .dd, raw)
&lt;/h3&gt;

&lt;p&gt;Disk image challenges are a category where picking the wrong tool first wastes a lot of time. Here's the order I follow now:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://alsavaudomila.com/fdisk-in-ctf-partition-analysis-and-common-challenge-patterns/" rel="noopener noreferrer"&gt;fdisk&lt;/a&gt;&lt;/strong&gt; — read the partition table first. Tells you how many partitions exist and their offsets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://alsavaudomila.com/dd-in-ctf-disk-imaging-extraction-and-common-challenge-patterns/" rel="noopener noreferrer"&gt;dd&lt;/a&gt;&lt;/strong&gt; — carve out individual partitions by byte offset for closer inspection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://alsavaudomila.com/mount-in-ctf-disk-image-mounting-and-common-challenge-patterns/" rel="noopener noreferrer"&gt;mount&lt;/a&gt;&lt;/strong&gt; — only after you know the partition layout. Mounting blindly often fails; fdisk tells you the offset you need for the &lt;code&gt;-o&lt;/code&gt; flag.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Rabbit Hole I fell into early on: jumping straight to &lt;code&gt;mount&lt;/code&gt; without checking the partition table. If the image has multiple partitions, mount defaults to the first one and you might miss the flag entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Audio Files (.wav, .mp3, .flac)
&lt;/h3&gt;

&lt;p&gt;Audio forensics challenges almost always hide data in one of three places: the spectrogram, the waveform LSBs, or metadata. Your first move should always be the spectrogram.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://alsavaudomila.com/audacity-in-ctf-audio-forensics-techniques-and-common-challenge-patterns/" rel="noopener noreferrer"&gt;Audacity&lt;/a&gt;&lt;/strong&gt; — open the file and switch to spectrogram view immediately. If there's a visual message hidden in the frequency domain, you'll see it in seconds. This is the tool I open first for any audio challenge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://alsavaudomila.com/sox-in-ctf-how-to-analyze-and-manipulate-audio-files/" rel="noopener noreferrer"&gt;SoX&lt;/a&gt;&lt;/strong&gt; — when I need to script audio analysis or batch-process files. Also useful for speed/pitch manipulation when a challenge hints that audio has been distorted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://alsavaudomila.com/ffmpeg-in-ctf-how-to-analyze-and-manipulate-audio-video-files/" rel="noopener noreferrer"&gt;FFmpeg&lt;/a&gt;&lt;/strong&gt; — for video files or when a challenge mixes audio and video. Also my go-to when a file won't open in Audacity due to codec issues — FFmpeg can transcode it first.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Image Files (.png, .jpg, .bmp)
&lt;/h3&gt;

&lt;p&gt;Image steganography is one of the most common forensics categories. The approach depends on whether the file is structurally intact or corrupted.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://alsavaudomila.com/pngcheck-in-ctf-how-to-analyze-and-repair-png-files/" rel="noopener noreferrer"&gt;pngcheck&lt;/a&gt;&lt;/strong&gt; — run this first on any PNG. It validates chunk integrity and will immediately flag if something is wrong with the file structure. A challenge with a "broken" PNG almost always has an intentionally modified chunk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://alsavaudomila.com/steghide-in-ctf-how-to-hide-and-extract-data-from-files/" rel="noopener noreferrer"&gt;steghide&lt;/a&gt;&lt;/strong&gt; — for JPEG/BMP files that might have data embedded with a passphrase. If the challenge gives you a password hint, steghide is usually involved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://alsavaudomila.com/binwalk-in-ctf-how-to-analyze-binaries-and-extract-hidden-files/" rel="noopener noreferrer"&gt;binwalk&lt;/a&gt;&lt;/strong&gt; — when the image looks clean but is suspiciously large. Scans for embedded files and compressed data appended after the image end.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One pattern I've noticed: if a PNG passes pngcheck cleanly but still feels suspicious, look at the IDAT chunk data and palette entries. Some challenges inject data there that doesn't break the structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Documents and Archives
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://alsavaudomila.com/pdfdumper-in-ctf-extracting-pdf-content-and-common-challenge-patterns/" rel="noopener noreferrer"&gt;pdfdumper&lt;/a&gt;&lt;/strong&gt; — PDFs are containers. pdfdumper extracts embedded objects, JavaScript, and hidden streams that you'd never see just opening the file normally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://alsavaudomila.com/zip2john-in-ctf-extracting-zip-passwords-and-common-challenge-patterns/" rel="noopener noreferrer"&gt;zip2john&lt;/a&gt;&lt;/strong&gt; — for password-protected ZIPs. The key thing here is identifying the encryption type first: ZipCrypto is crackable with zip2john + hashcat/john, but AES-256 encryption requires the actual password. I wrote about this distinction in detail in the &lt;a href="https://alsavaudomila.com/zip2john-in-ctf-extracting-zip-passwords-and-common-challenge-patterns/" rel="noopener noreferrer"&gt;zip2john article&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  QR Codes and Barcodes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://alsavaudomila.com/zbarimg-in-ctf-qr-barcode-decoding-techniques-and-common-challenge-patterns/" rel="noopener noreferrer"&gt;zbarimg&lt;/a&gt;&lt;/strong&gt; — the fastest way to decode QR/barcodes from the command line. The Scan Surprise challenge in picoCTF is a straightforward example — see the &lt;a href="https://alsavaudomila.com/scan-surprise-picoctf-writeup/" rel="noopener noreferrer"&gt;writeup&lt;/a&gt; for how it plays out in practice.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My First-Pass Workflow
&lt;/h2&gt;

&lt;p&gt;When I get a new forensics challenge, this is the sequence I actually follow:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# 1. What is this file?
file challenge.*
xxd challenge.* | head -30

# 2. Anything embedded?
binwalk challenge.*
strings challenge.* | grep -i flag

# Example output that changes my approach:
$ strings mystery.dat | grep -i flag
picoCTF{hidden_in_plain_sight_3a9f2}
# Done in 10 seconds. Sometimes it's that simple.

# 3. Branch based on file type
# → disk image: fdisk → dd → mount
# → audio: Audacity spectrogram → sox/ffmpeg
# → image: pngcheck → steghide
# → archive: zip2john (check encryption type first)
# → PDF: pdfdumper
# → QR/barcode: zbarimg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;strings&lt;/code&gt; pass in step 2 sounds too simple to mention, but I've found flags in plaintext embedded in binary files more than once. Never skip it before reaching for a specialized tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Rabbit Holes in Forensics CTF
&lt;/h2&gt;

&lt;p&gt;Things I've learned to check before going deep on a tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wrong file type assumption&lt;/strong&gt; — the extension lies. Always check magic bytes with &lt;code&gt;file&lt;/code&gt; and &lt;code&gt;xxd&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple layers&lt;/strong&gt; — extracting one file from a ZIP and stopping. There's often another layer inside.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mounting without reading partition offsets&lt;/strong&gt; — mount fails or mounts the wrong partition when you skip fdisk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AES-256 ZIP + zip2john&lt;/strong&gt; — zip2john cannot crack AES-256 encrypted ZIPs. If you're seeing $zip2$* in john output, you need the actual password, not a dictionary attack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spectrogram at wrong scale&lt;/strong&gt; — if Audacity's spectrogram looks like noise, zoom in on the frequency range 1–4kHz. Flags are sometimes hidden in a narrow band that's invisible at default zoom.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tool Reference Index
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;File Type&lt;/th&gt;
&lt;th&gt;When to Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://alsavaudomila.com/fdisk-in-ctf-partition-analysis-and-common-challenge-patterns/" rel="noopener noreferrer"&gt;fdisk&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Disk image&lt;/td&gt;
&lt;td&gt;Read partition table before anything else&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://alsavaudomila.com/dd-in-ctf-disk-imaging-extraction-and-common-challenge-patterns/" rel="noopener noreferrer"&gt;dd&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Disk image&lt;/td&gt;
&lt;td&gt;Carve partitions by byte offset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://alsavaudomila.com/mount-in-ctf-disk-image-mounting-and-common-challenge-patterns/" rel="noopener noreferrer"&gt;mount&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Disk image&lt;/td&gt;
&lt;td&gt;Browse filesystem after fdisk gives you the offset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://alsavaudomila.com/audacity-in-ctf-audio-forensics-techniques-and-common-challenge-patterns/" rel="noopener noreferrer"&gt;Audacity&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Audio&lt;/td&gt;
&lt;td&gt;Spectrogram analysis — open first for any audio file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://alsavaudomila.com/sox-in-ctf-how-to-analyze-and-manipulate-audio-files/" rel="noopener noreferrer"&gt;SoX&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Audio&lt;/td&gt;
&lt;td&gt;Scripted analysis, speed/pitch manipulation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://alsavaudomila.com/ffmpeg-in-ctf-how-to-analyze-and-manipulate-audio-video-files/" rel="noopener noreferrer"&gt;FFmpeg&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Audio/Video&lt;/td&gt;
&lt;td&gt;Video files, codec issues, format conversion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://alsavaudomila.com/pngcheck-in-ctf-how-to-analyze-and-repair-png-files/" rel="noopener noreferrer"&gt;pngcheck&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;Validate chunk integrity on any PNG challenge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://alsavaudomila.com/steghide-in-ctf-how-to-hide-and-extract-data-from-files/" rel="noopener noreferrer"&gt;steghide&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;JPEG/BMP&lt;/td&gt;
&lt;td&gt;Extract passphrase-protected embedded data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://alsavaudomila.com/binwalk-in-ctf-how-to-analyze-binaries-and-extract-hidden-files/" rel="noopener noreferrer"&gt;binwalk&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Any binary&lt;/td&gt;
&lt;td&gt;Detect and extract embedded files; watch for false positives in PNG IDAT chunks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://alsavaudomila.com/zbarimg-in-ctf-qr-barcode-decoding-techniques-and-common-challenge-patterns/" rel="noopener noreferrer"&gt;zbarimg&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;QR/Barcode&lt;/td&gt;
&lt;td&gt;Fastest CLI decoder for QR and barcode images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://alsavaudomila.com/pdfdumper-in-ctf-extracting-pdf-content-and-common-challenge-patterns/" rel="noopener noreferrer"&gt;pdfdumper&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;PDF&lt;/td&gt;
&lt;td&gt;Extract hidden streams, embedded objects, JavaScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://alsavaudomila.com/zip2john-in-ctf-extracting-zip-passwords-and-common-challenge-patterns/" rel="noopener noreferrer"&gt;zip2john&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;ZIP archive&lt;/td&gt;
&lt;td&gt;Password hash extraction (ZipCrypto only — check encryption type first)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;p&gt;Here are related writeups from &lt;a href="https://alsavaudomila.com/" rel="noopener noreferrer"&gt;alsavaudomila.com&lt;/a&gt; that show these tools in action on real picoCTF problems:&lt;/p&gt;

&lt;p&gt;If you want to see how disk forensics tools chain together in practice, the &lt;a href="https://alsavaudomila.com/disko-1-picoctf-writeup/" rel="noopener noreferrer"&gt;DISKO 1 writeup&lt;/a&gt; walks through a full fdisk → dd → mount workflow on an actual picoCTF challenge.&lt;/p&gt;

&lt;p&gt;For a real example of corrupted file identification using magic bytes and pngcheck, the &lt;a href="https://alsavaudomila.com/corrupted-file-picoctf-writeup/" rel="noopener noreferrer"&gt;Corrupted File writeup&lt;/a&gt; covers exactly how a broken PNG header gets diagnosed and fixed.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://alsavaudomila.com/scan-surprise-picoctf-writeup/" rel="noopener noreferrer"&gt;Scan Surprise writeup&lt;/a&gt; is the most straightforward QR code challenge in picoCTF — a good first read if you've never used zbarimg before.&lt;/p&gt;

&lt;p&gt;For network forensics (Wireshark/tshark), which isn't covered above, the &lt;a href="https://alsavaudomila.com/ph4nt0m-1ntrud3r-picoctf-writeup/" rel="noopener noreferrer"&gt;Ph4nt0m 1ntrud3r writeup&lt;/a&gt; goes deep on packet filtering and Base64 fragment extraction across TCP streams.&lt;/p&gt;

</description>
      <category>ctf</category>
      <category>security</category>
      <category>linux</category>
      <category>forensics</category>
    </item>
    <item>
      <title>RED picoCTF Writeup</title>
      <dc:creator>rudy_candy</dc:creator>
      <pubDate>Mon, 20 Apr 2026 17:34:27 +0000</pubDate>
      <link>https://dev.to/rudycandy/red-picoctf-writeup-2f27</link>
      <guid>https://dev.to/rudycandy/red-picoctf-writeup-2f27</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This is my writeup for the picoCTF challenge &lt;strong&gt;RED&lt;/strong&gt; — a forensics puzzle centered on LSB steganography hidden inside a PNG image. I used &lt;strong&gt;exiftool&lt;/strong&gt; to find a suspicious poem in the metadata, decoded an acrostic clue pointing to &lt;strong&gt;zsteg&lt;/strong&gt; , and extracted a Base64-encoded flag from the LSB layer. Fair warning: I hit a wall installing zsteg before I even got started.&lt;/p&gt;




&lt;h2&gt;
  
  
  Challenge Overview
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;CTF:&lt;/strong&gt; picoCTF&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Challenge:&lt;/strong&gt; RED&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Category:&lt;/strong&gt; Forensics&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Difficulty:&lt;/strong&gt; Easy&lt;/p&gt;

&lt;p&gt;The challenge description was minimal — almost taunting:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;RED, RED, RED, RED&lt;br&gt;&lt;br&gt;
Download the image: red.png&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A single PNG. No hints. Just red. I had no idea what I was walking into.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step-by-Step Walkthrough
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Start with &lt;code&gt;file&lt;/code&gt; — Because Assumptions Kill
&lt;/h3&gt;

&lt;p&gt;My first instinct with any unknown file is to run &lt;code&gt;file&lt;/code&gt;. Not because I expect fireworks, but because I've been burned before. Once I tried to open a "PNG" that was actually a ZIP in disguise — and I wasted 20 minutes confused why my image viewer crashed. Never again.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ file red.png
red.png: PNG image data, 128 x 128, 8-bit/color RGBA, non-interlaced
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Legitimate PNG, 128×128 pixels, RGBA color space. Nothing suspicious here — which somehow made it more suspicious. A 128×128 image isn't exactly a high-resolution photo. Something was packed inside.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: &lt;code&gt;exiftool&lt;/code&gt; — Where Things Got Weird
&lt;/h3&gt;

&lt;p&gt;When the file itself looks clean, I dig into metadata. &lt;code&gt;exiftool&lt;/code&gt; reads EXIF and other embedded data that the image itself doesn't display visually. Most of the time it's boring — camera model, GPS coordinates, timestamps. This time it was not boring at all.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ exiftool red.png
ExifTool Version Number         : 13.25
File Name                       : red.png
Directory                       : .
File Size                       : 796 bytes
File Type                       : PNG
Image Width                     : 128
Image Height                    : 128
Bit Depth                       : 8
Color Type                      : RGB with Alpha
Poem                            : Crimson heart, vibrant and bold,
Hearts flutter at your sight.
Evenings glow softly red,
Cherries burst with sweet life.
Kisses linger with your warmth.
Love deep as merlot.
Scarlet leaves falling softly,
Bold in every stroke.
Image Size                      : 128x128
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;A &lt;strong&gt;Poem&lt;/strong&gt; field. I had never seen that metadata field before. My first reaction was genuine confusion — who puts a poem in a PNG? But then I read it again. Something about the structure felt deliberate. Too deliberate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: The CHECKLSB Acrostic — I Almost Missed It
&lt;/h3&gt;

&lt;p&gt;I copied the poem into a text editor and read it a few times looking for something — a URL, a hex string, anything obviously encoded. Nothing. I almost moved on to running &lt;code&gt;strings&lt;/code&gt; on the raw file when something made me slow down. The poem felt constructed rather than natural. Every line started clean and capitalized. That's not how poems flow when they're written to be felt — that's how they flow when they're written to hide something.&lt;/p&gt;

&lt;p&gt;I read the first letters vertically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;C&lt;/strong&gt; rimson heart, vibrant and bold,&lt;br&gt;&lt;br&gt;
&lt;strong&gt;H&lt;/strong&gt; earts flutter at your sight.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;E&lt;/strong&gt; venings glow softly red,&lt;br&gt;&lt;br&gt;
&lt;strong&gt;C&lt;/strong&gt; herries burst with sweet life.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;K&lt;/strong&gt; isses linger with your warmth.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;L&lt;/strong&gt; ove deep as merlot.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;S&lt;/strong&gt; carlet leaves falling softly,&lt;br&gt;&lt;br&gt;
&lt;strong&gt;B&lt;/strong&gt; old in every stroke.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;C-H-E-C-K-L-S-B.&lt;/strong&gt; CHECKLSB.&lt;/p&gt;

&lt;p&gt;I genuinely laughed. Not because it was funny, but because I almost didn't see it. I had been looking for something technical — encoded characters, unusual Unicode, anything that looked like data — when the answer was a first-grade word puzzle hiding in plain sight. An acrostic: a message formed by the first letters of each line. And "CHECKLSB" is not a cryptic phrase. It's an instruction. Check the LSB — the Least Significant Bit layer of the image. The poem was telling me exactly what to do next.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Installing &lt;code&gt;zsteg&lt;/code&gt; — The Part That Tripped Me Up
&lt;/h3&gt;

&lt;p&gt;LSB steganography in PNG images — my tool of choice is &lt;code&gt;zsteg&lt;/code&gt;. So I typed &lt;code&gt;sudo apt install zsteg&lt;/code&gt; and got nothing. Package not found. I tried &lt;code&gt;apt search zsteg&lt;/code&gt;. Still nothing. Checked Snap. No luck there either.&lt;/p&gt;

&lt;p&gt;Turns out zsteg is not in the standard APT repositories at all. It's a Ruby gem, and you need to install it through RubyGems after setting up the Ruby development environment and ImageMagick bindings. I didn't know this going in and lost probably 10 minutes trying different apt variants before looking it up.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ sudo apt update
$ sudo apt install ruby ruby-dev imagemagick libmagickwand-dev
$ sudo gem install zsteg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;libmagickwand-dev&lt;/code&gt; dependency is the one that catches people. The gem won't compile without it. Once that's in place, &lt;code&gt;gem install zsteg&lt;/code&gt; works cleanly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Running &lt;code&gt;zsteg&lt;/code&gt; — The Payload Surfaces
&lt;/h3&gt;

&lt;p&gt;With zsteg installed, I ran it on the image:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ zsteg red.png
meta Poem           .. text: "Crimson heart, vibrant and bold,\nHearts flutter at your sight.\nEvenings glow softly red,\nCherries burst with sweet life.\nKisses linger with your warmth.\nLove deep as merlot.\nScarlet leaves falling softly,\nBold in every stroke."
b1,rgba,lsb,xy      .. text: "cGljb0NURntyM2RfMXNfdGgzX3VsdDFtNHQzX2N1cjNfZjByXzU0ZG4zNTVffQ==cGljb0NURntyM2RfMXNfdGgzX3VsdDFtNHQzX2N1cjNfZjByXzU0ZG4zNTVffQ=="
b1,rgba,msb,xy      .. file: OpenPGP Public Key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;There it is. The &lt;code&gt;b1,rgba,lsb,xy&lt;/code&gt; channel contains a Base64-encoded string — twice concatenated, but that's just the decoder reading past the end of the actual data. The real payload is the first copy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Capture the Flag
&lt;/h2&gt;

&lt;p&gt;The Base64 string &lt;code&gt;cGljb0NURntyM2RfMXNfdGgzX3VsdDFtNHQzX2N1cjNfZjByXzU0ZG4zNTVffQ==&lt;/code&gt; needed one more step. I wrote a quick Python snippet:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import base64
cipher = "cGljb0NURntyM2RfMXNfdGgzX3VsdDFtNHQzX2N1cjNfZjByXzU0ZG4zNTVffQ=="
plain = base64.b64decode(cipher).decode()
print(plain)


$ python3 a.py
picoCTF{r3d_1s_th3_ult1m4t3_cur3_f0r_54dn355_}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That moment when the flag prints cleanly to stdout — it never gets old. After the frustration of the zsteg installation, seeing a clean &lt;code&gt;picoCTF{...}&lt;/code&gt; string felt disproportionately satisfying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flag:&lt;/strong&gt; &lt;code&gt;picoCTF{r3d_1s_th3_ult1m4t3_cur3_f0r_54dn355_}&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Full Trial Process Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Why it failed or succeeded&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Identify file type&lt;/td&gt;
&lt;td&gt;&lt;code&gt;file red.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Valid PNG, 128×128, RGBA&lt;/td&gt;
&lt;td&gt;Succeeded — confirmed the file is a genuine PNG, not disguised as something else&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Read metadata&lt;/td&gt;
&lt;td&gt;&lt;code&gt;exiftool red.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Found embedded Poem field&lt;/td&gt;
&lt;td&gt;Succeeded — unusual Poem field stood out immediately as non-standard metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Decode acrostic&lt;/td&gt;
&lt;td&gt;Manual reading of first letters&lt;/td&gt;
&lt;td&gt;CHECKLSB&lt;/td&gt;
&lt;td&gt;Succeeded — once you look for it, the pattern is obvious; easy to miss on first glance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4a&lt;/td&gt;
&lt;td&gt;Install zsteg via apt&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sudo apt install zsteg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Package not found&lt;/td&gt;
&lt;td&gt;Failed — zsteg is not in the standard APT repositories; requires RubyGems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4b&lt;/td&gt;
&lt;td&gt;Install Ruby dependencies&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sudo apt install ruby ruby-dev imagemagick libmagickwand-dev&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All packages installed&lt;/td&gt;
&lt;td&gt;Succeeded — &lt;code&gt;libmagickwand-dev&lt;/code&gt; is required for the gem to compile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4c&lt;/td&gt;
&lt;td&gt;Install zsteg via gem&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sudo gem install zsteg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;zsteg installed&lt;/td&gt;
&lt;td&gt;Succeeded — once dependencies are in place, gem install works cleanly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Run LSB steganography scan&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zsteg red.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Base64 string in b1,rgba,lsb,xy&lt;/td&gt;
&lt;td&gt;Succeeded — CHECKLSB hint pointed directly to the right channel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Decode Base64&lt;/td&gt;
&lt;td&gt;&lt;code&gt;python3 a.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;picoCTF{r3d_1s_th3_ult1m4t3_cur3_f0r_54dn355_}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Succeeded — standard Base64 decoding, no further obfuscation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Command Explanations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  exiftool
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;exiftool&lt;/code&gt; reads metadata embedded in image files — things like camera settings, GPS data, copyright information, and (in this case) custom fields that challenge authors have planted. It reads dozens of metadata formats across hundreds of file types. Running it with no flags on a file gives you a full dump of everything embedded. It's usually one of the first things I run on a forensics challenge image because metadata is cheap to hide things in and often overlooked.&lt;/p&gt;

&lt;h3&gt;
  
  
  zsteg
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;zsteg&lt;/code&gt; is a Ruby-based tool specifically designed to detect hidden data in PNG and BMP files using steganographic techniques. It checks multiple bit-plane combinations (b1 through b8), color channel orderings (rgb, rgba, bgr, etc.), bit orders (lsb, msb), and scan directions (xy, yx). The output line &lt;code&gt;b1,rgba,lsb,xy&lt;/code&gt; means: bit depth 1, RGBA channels in that order, least significant bit first, scanning left-to-right then top-to-bottom. That specific combination is one of the most common LSB hiding methods, which is why it showed up first.&lt;/p&gt;

&lt;h3&gt;
  
  
  base64 (Python)
&lt;/h3&gt;

&lt;p&gt;Base64 is an encoding scheme — not encryption. It converts binary data into ASCII text using a 64-character alphabet (A-Z, a-z, 0-9, +, /). The &lt;code&gt;=&lt;/code&gt; padding at the end is a giveaway. Python's &lt;code&gt;base64.b64decode()&lt;/code&gt; reverses this. Because it's encoding rather than encryption, no key is needed — if you recognize the format, you can decode it immediately. In CTF challenges, Base64 often appears as a final layer after the actual hiding mechanism has been defeated.&lt;/p&gt;




&lt;h2&gt;
  
  
  Beginner Tips
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Always run&lt;code&gt;file&lt;/code&gt; first.&lt;/strong&gt; A file extension can lie. The &lt;code&gt;file&lt;/code&gt; command reads magic bytes at the start of the file — that's the truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;exiftool&lt;/code&gt; before anything else on image challenges.&lt;/strong&gt; Custom metadata fields like "Poem" won't show up in normal image viewers. You need to ask for them explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read slowly.&lt;/strong&gt; The acrostic in this challenge is easy to miss if you skim. Treat every piece of embedded text as potentially meaningful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;zsteg is not in apt.&lt;/strong&gt; Save yourself 10 minutes of confusion: it requires &lt;code&gt;ruby-dev&lt;/code&gt; and &lt;code&gt;libmagickwand-dev&lt;/code&gt; before &lt;code&gt;gem install zsteg&lt;/code&gt; will work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Double-check Base64 strings before decoding.&lt;/strong&gt; zsteg sometimes reads past the end of the actual data and duplicates it. Compare the two halves — if they're identical, use just one copy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep a tool checklist.&lt;/strong&gt; For PNG forensics: &lt;code&gt;file&lt;/code&gt; → &lt;code&gt;exiftool&lt;/code&gt; → &lt;code&gt;strings&lt;/code&gt; → &lt;code&gt;binwalk&lt;/code&gt; → &lt;code&gt;zsteg&lt;/code&gt; → &lt;code&gt;pngcheck&lt;/code&gt;. Working through a checklist beats staring blankly at a file.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What You Learned / Takeaways
&lt;/h2&gt;

&lt;p&gt;This challenge is a clean three-layer puzzle: metadata hiding (the poem in exiftool), linguistic encoding (the acrostic spelling CHECKLSB), and steganographic hiding (LSB data in the RGBA channel). Each layer points to the next. It's a well-designed beginner challenge because no layer requires specialized knowledge — just methodical thinking and knowing which tools to reach for.&lt;/p&gt;

&lt;p&gt;The zsteg installation issue is worth dwelling on. "Not in apt" is a common pattern for niche security tools. When a standard package manager comes up empty, the usual next steps are: check if it's a Python package (&lt;code&gt;pip install&lt;/code&gt;), a Ruby gem (&lt;code&gt;gem install&lt;/code&gt;), a Go tool (&lt;code&gt;go install&lt;/code&gt;), or a manual build from GitHub. Knowing the tool's ecosystem tells you where to look.&lt;/p&gt;

&lt;p&gt;On the LSB technique itself: each color channel in an RGBA pixel is stored as an 8-bit number. The least significant bit — the rightmost 1 in the binary representation — contributes almost nothing to the visible color. A red value of 254 (11111110) and 255 (11111111) are visually indistinguishable. Flip those last bits across every pixel in a 128×128 RGBA image and you have 128 × 128 × 4 = 65,536 bits of hidden storage — enough to hold meaningful text. The image looks completely normal. No tools would flag it in transit. That's the point.&lt;/p&gt;

&lt;p&gt;This is not just a CTF puzzle technique. I've read incident reports where malware communicated with command-and-control servers by exfiltrating data hidden in PNG images uploaded to public file-sharing sites — traffic that looked like ordinary image uploads to any network monitor not specifically checking for steganographic content. Media companies use the same underlying math for digital watermarking: embedding traceable identifiers in image files to identify the source of a leak. Both sides of the security industry use this. Knowing how to detect it matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If I solved this again:&lt;/strong&gt; I'd go straight from exiftool to zsteg. The CHECKLSB acrostic is a strong enough hint that I wouldn't spend time on binwalk or strings first. I'd also pre-verify the zsteg installation before the challenge clock starts — tool installation under time pressure is avoidable friction.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;p&gt;This problem is part of the picoCTF Forensics series. You can see the other problems &lt;a href="https://alsavaudomila.com/category/picoctf-forensics-easy/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For more Forensics Tools, check out &lt;a href="https://alsavaudomila.com/ctf-forensics-tools/" rel="noopener noreferrer"&gt;CTF Forensics Tools: The Ultimate Guide for Beginners&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here are related articles from &lt;a href="https://alsavaudomila.com/" rel="noopener noreferrer"&gt;alsavaudomila.com&lt;/a&gt; that complement this challenge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://alsavaudomila.com/steghide-in-ctf-how-to-hide-and-extract-data-from-files/" rel="noopener noreferrer"&gt;steghide in CTF: How to Hide and Extract Data from Files&lt;/a&gt; — another steganography tool commonly used in CTF challenges&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://alsavaudomila.com/pngcheck-in-ctf-how-to-analyze-and-repair-png-files/" rel="noopener noreferrer"&gt;pngcheck in CTF: How to Analyze and Repair PNG Files&lt;/a&gt; — when PNG structure itself is the puzzle&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ctf</category>
      <category>picoctf</category>
      <category>forensics</category>
      <category>security</category>
    </item>
  </channel>
</rss>
