<?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.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>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\|ctf\|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\|ctf\|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\|flag\|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\|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|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>
    <item>
      <title>Includes picoCTF Writeup</title>
      <dc:creator>rudy_candy</dc:creator>
      <pubDate>Mon, 20 Apr 2026 17:34:24 +0000</pubDate>
      <link>https://dev.to/rudycandy/includes-picoctf-writeup-3eio</link>
      <guid>https://dev.to/rudycandy/includes-picoctf-writeup-3eio</guid>
      <description>&lt;p&gt;HTML source code review is one of those skills that sounds obvious in hindsight, yet I spent a full 30+ minutes chasing the wrong rabbit before I figured that out. This is my writeup for the &lt;strong&gt;picoCTF "Includes"&lt;/strong&gt; challenge — a Web Exploitation Easy problem that taught me more about my own assumptions than it did about hacking. If you landed here hoping to find how a hidden flag lives inside CSS and JavaScript comments, you're in the right place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge Overview: picoCTF "Includes"
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Competition:&lt;/strong&gt; picoCTF&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Challenge Name:&lt;/strong&gt; Includes&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Category:&lt;/strong&gt; Web Exploitation&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Difficulty:&lt;/strong&gt; Easy&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Points:&lt;/strong&gt; 100&lt;/p&gt;

&lt;p&gt;The challenge drops you at a single-page website with a button and some decorative text. The objective: find the flag. No hints. No file downloads. Just a URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  My First Instinct — And Why It Was Wrong
&lt;/h2&gt;

&lt;p&gt;I'll be honest: the moment I saw a web challenge labeled "Easy," my brain jumped straight to SQL injection. I don't know why — probably because SQL injection is the first thing I learned and it feels like a reliable default. I opened the page, saw a text input area, and started typing.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;' OR 1=1 --
" OR "1"="1
admin'--
' UNION SELECT null--
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Nothing. The button didn't even submit anything to a visible endpoint. I checked the Network tab in DevTools — no POST request firing at all. The form was cosmetic, or at least it wasn't wired to a backend I could see.&lt;/p&gt;

&lt;p&gt;Then I pivoted to XSS, reasoning maybe there was a reflection point somewhere.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;script&amp;gt;alert(1)&amp;lt;/script&amp;gt;
&amp;lt;img src=x onerror=alert(1)&amp;gt;
javascript:alert(document.cookie)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Also nothing. The page just sat there looking smug. No reflected output, no pop-ups. I started to feel the slow creep of frustration — you know the feeling when you're trying three things at once and none of them are landing.&lt;/p&gt;

&lt;p&gt;I wasted about 20 minutes here before stepping back and asking myself: &lt;em&gt;why am I jumping to SQL injection on a page that probably doesn 't even have a database?&lt;/em&gt; I had pattern-matched to "web challenge = injection" without actually reading what was in front of me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rabbit Hole Log — 30+ Minutes of Going Nowhere
&lt;/h2&gt;

&lt;p&gt;Here's the honest accounting of what I tried before getting to the actual solution:&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;What I Tried&lt;/th&gt;
&lt;th&gt;Time Spent&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Why It Failed&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;SQL injection via text input&lt;/td&gt;
&lt;td&gt;~10 min&lt;/td&gt;
&lt;td&gt;No response change&lt;/td&gt;
&lt;td&gt;No database backend; form is static&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;XSS payloads in input fields&lt;/td&gt;
&lt;td&gt;~10 min&lt;/td&gt;
&lt;td&gt;No reflection&lt;/td&gt;
&lt;td&gt;Input not rendered back to DOM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Brute-forcing hidden paths (/admin, /flag, /secret)&lt;/td&gt;
&lt;td&gt;~8 min&lt;/td&gt;
&lt;td&gt;404 on everything&lt;/td&gt;
&lt;td&gt;This is a static challenge page, no hidden routes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Checking cookies and local storage for encoded values&lt;/td&gt;
&lt;td&gt;~5 min&lt;/td&gt;
&lt;td&gt;Empty / session cookie only&lt;/td&gt;
&lt;td&gt;Flag isn't stored client-side in that way&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;HTML source view — finally&lt;/td&gt;
&lt;td&gt;2 min&lt;/td&gt;
&lt;td&gt;Found CSS and JS references&lt;/td&gt;
&lt;td&gt;Should have been step 1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Thirty-three minutes. That's how long it took me to do what should have been the first thing. If you're laughing right now, that's fair. I was laughing too — eventually.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Solution: Reading the Source
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: View HTML Source
&lt;/h3&gt;

&lt;p&gt;I hit &lt;code&gt;Ctrl+U&lt;/code&gt; to open the raw HTML source. Right away I could see two external file references: &lt;code&gt;style.css&lt;/code&gt; and &lt;code&gt;script.js&lt;/code&gt;. Nothing unusual about that structure — every web page has CSS and JS. But in a CTF context, "included files" is practically a signpost.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;link rel="stylesheet" href="style.css"&amp;gt;
&amp;lt;script src="script.js"&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The challenge name is literally "Includes." I cannot stress enough how much I should have caught that earlier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Open style.css
&lt;/h3&gt;

&lt;p&gt;I navigated directly to &lt;code&gt;/style.css&lt;/code&gt; by appending it to the base URL. The file loaded. I scrolled through the usual CSS declarations — nothing unusual — until I hit a comment at the bottom of the file:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/* You need to include picoCTF{1nclu51v17y_1of2_ */
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;There it was. Half a flag, wrapped in a developer comment. My first reaction was a small, embarrassed laugh — this was &lt;em&gt;right here&lt;/em&gt; the whole time, in the CSS file, which I hadn't thought to open for over half an hour.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Open script.js
&lt;/h3&gt;

&lt;p&gt;With the first fragment in hand, I opened &lt;code&gt;/script.js&lt;/code&gt;. Same approach — append to base URL, scroll through the file. Near the bottom:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// f7w_2of2_6edef411}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The second fragment. A JavaScript single-line comment hiding in plain sight.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Combine the Fragments
&lt;/h3&gt;

&lt;p&gt;Concatenate the two parts:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;picoCTF{1nclu51v17y_1of2_f7w_2of2_6edef411}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That's the flag. The moment I pasted both strings together I had that very specific CTF feeling — not triumph exactly, more like relief mixed with "oh come on, really?" It's humbling when a 100-point Easy challenge makes you feel genuinely foolish for 30 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Tried SQL Injection and XSS First (And Why That Was a Mistake)
&lt;/h2&gt;

&lt;p&gt;I want to be specific about the reasoning failure here, because it's a pattern worth breaking.&lt;/p&gt;

&lt;p&gt;SQL injection made sense in my head because I saw an input field and assumed there was a database behind it. That's an assumption without evidence. I should have checked whether the form even submitted a request before crafting payloads for a backend that didn't exist.&lt;/p&gt;

&lt;p&gt;XSS followed from the same mistake: I was looking for a reflection point in a page that wasn't reflecting user input anywhere. I was testing a hypothesis I'd constructed without reading the actual page behavior first.&lt;/p&gt;

&lt;p&gt;The correct mental model for an Easy web challenge should be: &lt;em&gt;start with what the browser already has access to, then escalate&lt;/em&gt;. HTML source → linked resources → cookies/storage → requests → server behavior. I started in the middle and paid for it with half an hour of my time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters in the Real World: Comment Leakage
&lt;/h2&gt;

&lt;p&gt;This challenge isn't contrived. Leaving sensitive information in code comments is a genuine, documented vulnerability class. A few real cases worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API keys in JavaScript files:&lt;/strong&gt; Developers frequently embed API keys in client-side JS for quick prototyping, then ship to production without stripping them. Google Maps API keys, Firebase credentials, and Stripe publishable keys have all been leaked this way. Some of these allow read access to entire databases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS credentials in HTML comments:&lt;/strong&gt; AWS Access Key IDs and Secret Access Keys have been found in HTML comments of public-facing pages. Once extracted, these credentials can be used to access S3 buckets, spin up EC2 instances, or exfiltrate data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internal path disclosure:&lt;/strong&gt; Comments like &lt;code&gt;/* TODO: move /admin/config.php before launch */&lt;/code&gt; reveal server directory structures that help attackers map the application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debug notes left in production:&lt;/strong&gt; Comments such as &lt;code&gt;// password is "admin123" for now&lt;/code&gt; or &lt;code&gt;/* hardcoded until OAuth is ready */&lt;/code&gt; have appeared in real production codebases during security audits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The picoCTF "Includes" challenge distills this exact pattern: split sensitive data across multiple files, leave it in comments, and wait to see who checks. In a real engagement, an attacker running a passive reconnaissance pass over your JS and CSS files would find this in seconds. Automated tools like &lt;a href="https://github.com/GerbenJavado/LinkFinder" rel="noopener noreferrer"&gt;LinkFinder&lt;/a&gt; and &lt;a href="https://github.com/lc/subjs" rel="noopener noreferrer"&gt;subjs&lt;/a&gt; scrape JS files specifically looking for exposed secrets.&lt;/p&gt;

&lt;h2&gt;
  
  
  OWASP Top 10: Security Misconfiguration
&lt;/h2&gt;

&lt;p&gt;This vulnerability maps directly to &lt;strong&gt;OWASP Top 10 A05:2021 — Security Misconfiguration&lt;/strong&gt;. The OWASP category covers scenarios where systems are deployed with unnecessary features enabled, default credentials unchanged, or — as here — error-prone development artifacts left in production code.&lt;/p&gt;

&lt;p&gt;Leaving debug comments, internal notes, or fragment data in unminified CSS/JS files qualifies as misconfiguration because it exposes internal implementation details that serve no function for end users but provide genuine value to an attacker. The fix is straightforward: minify and strip comments from all production assets. A build pipeline using tools like Webpack, Rollup, or esbuild handles this automatically — there's no excuse in 2026 for shipping commented development notes to a production server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beginner Web Challenge Checklist
&lt;/h2&gt;

&lt;p&gt;Based on this challenge (and the 33 minutes I wasted), here's the checklist I now follow before trying anything fancy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;View HTML source (&lt;code&gt;Ctrl+U&lt;/code&gt; or right-click → View Page Source)&lt;/li&gt;
&lt;li&gt;Check all externally linked files: CSS, JS, images, fonts&lt;/li&gt;
&lt;li&gt;Read every comment in every file — CSS &lt;code&gt;/* */&lt;/code&gt;, JS &lt;code&gt;//&lt;/code&gt; and &lt;code&gt;/* */&lt;/code&gt;, HTML &lt;code&gt;&amp;lt;!-- --&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Open browser DevTools → Network tab, reload the page, inspect every request&lt;/li&gt;
&lt;li&gt;Check cookies, localStorage, and sessionStorage for encoded or suspicious values&lt;/li&gt;
&lt;li&gt;Look at the page title, meta tags, and hidden form fields&lt;/li&gt;
&lt;li&gt;Check robots.txt and sitemap.xml for path hints&lt;/li&gt;
&lt;li&gt;Only after all of the above: start testing for injection, XSS, or authentication bypass&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you follow this order, you'll solve "Includes"-style challenges in under two minutes every time. I know this now. You know this now. Let's not lose 33 minutes again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrospective: How I'd Solve This Next Time
&lt;/h2&gt;

&lt;p&gt;If I encountered a challenge like this again, my first move would be to look at the challenge name. "Includes" is not a subtle hint — it's telling you exactly what the mechanic is. Always read the problem title as a potential clue about what technique is being tested.&lt;/p&gt;

&lt;p&gt;Second, I'd open the HTML source before even interacting with the page. No clicking buttons, no typing in forms — just &lt;code&gt;Ctrl+U&lt;/code&gt;, read the markup, note every external resource, and visit each one immediately.&lt;/p&gt;

&lt;p&gt;Third, I'd use a tool like Burp Suite to passively collect all resources the page loads, so I don't miss anything that isn't in an obvious &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag. Dynamic imports, fetch calls, and lazy-loaded resources can hide interesting content that raw source viewing won't catch.&lt;/p&gt;

&lt;p&gt;The core lesson: don't let your instinct for "cool" techniques — injection, XSS, SSRF — blind you to what's already sitting in the DOM.&lt;/p&gt;

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

&lt;p&gt;This problem is part of the picoCTF series. You can see the other problems &lt;a href="https://alsavaudomila.com/category/picoctf-web-exploitation-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/red-picoctf-writeup/" rel="noopener noreferrer"&gt;RED picoCTF Writeup&lt;/a&gt; — another picoCTF challenge involving hidden data&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://alsavaudomila.com/ph4nt0m-1ntrud3r-picoctf-writeup/" rel="noopener noreferrer"&gt;Ph4nt0m 1ntrud3r picoCTF Writeup&lt;/a&gt; — network forensics challenge&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ctf</category>
      <category>picoctf</category>
      <category>webdev</category>
      <category>security</category>
    </item>
    <item>
      <title>steghide in CTF: Extract Flags</title>
      <dc:creator>rudy_candy</dc:creator>
      <pubDate>Mon, 20 Apr 2026 17:29:18 +0000</pubDate>
      <link>https://dev.to/rudycandy/steghide-in-ctf-extract-flags-g8j</link>
      <guid>https://dev.to/rudycandy/steghide-in-ctf-extract-flags-g8j</guid>
      <description>

&lt;p&gt;I stared at the &lt;code&gt;steghide extract&lt;/code&gt; prompt for a full minute before I realized the passphrase was sitting right there — encoded twice in the image's own metadata. That was &lt;strong&gt;picoCTF Hidden in Plainsight&lt;/strong&gt; , and it changed how I think about steganography challenges entirely.&lt;/p&gt;

&lt;p&gt;Before that problem, my steghide workflow was: download image, run &lt;code&gt;steghide extract&lt;/code&gt;, type "password," fail, give up. After it, I understood that the real skill isn't knowing steghide's flags — it's knowing &lt;em&gt;where the passphrase is hiding before you even open the tool&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solve That Taught Me Everything: picoCTF Hidden in Plainsight
&lt;/h2&gt;

&lt;p&gt;The challenge gave me a single file: &lt;code&gt;img.jpg&lt;/code&gt;. No hints, no description beyond "can you find it?" I ran &lt;code&gt;file&lt;/code&gt; first, which is always my starting point — not because I expect much, but because sometimes the output surprises you.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ file img.jpg
img.jpg: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, comment: "c3RlZ2hpZGU6Y0VGNmVuZHZjbVE9", baseline, precision 8, 640x640, components 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That &lt;code&gt;comment&lt;/code&gt; field stopped me cold. A JPEG comment containing what's clearly a base64 string — that's not normal. I decoded it immediately:&lt;/p&gt;

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


$ python3 decode.py
steghide:cEF6endvcmQ=
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The output itself was split at a colon: &lt;code&gt;steghide&lt;/code&gt; on the left, another base64 string on the right. The challenge was literally telling me which tool to use and handing me an encoded passphrase. I decoded the right side:&lt;/p&gt;

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


$ python3 decode.py
pAzzword
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Before extracting, I ran &lt;code&gt;steghide info&lt;/code&gt; to confirm data was actually embedded — a habit I've developed after chasing too many false positives:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ steghide info img.jpg
"img.jpg":
  format: jpeg
  capacity: 4.0 KB
Try to get information about embedded data ? (y/n) y
Enter passphrase:
  embedded file "flag.txt":
    size: 34.0 Byte
    encrypted: rijndael-128, cbc
    compressed: yes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;There it was — &lt;code&gt;flag.txt&lt;/code&gt;, 34 bytes, AES-128 encrypted. The passphrase &lt;code&gt;pAzzword&lt;/code&gt; unlocked it:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ steghide extract -sf img.jpg
Enter passphrase:
wrote extracted data to "flag.txt".


picoCTF{h1dd3n_1m4g3_67479645}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The lesson wasn't about steghide — it was about metadata. The passphrase was never "hidden." It was in &lt;code&gt;file img.jpg&lt;/code&gt; output the whole time, double base64-encoded in the comment field. I almost missed it because I was looking for the payload, not the key.&lt;/p&gt;




&lt;h2&gt;
  
  
  What steghide Actually Does — and Why It Matters for CTF
&lt;/h2&gt;

&lt;p&gt;Most tutorials say steghide "hides data using LSB." That's close, but not precise — and the difference matters when you're trying to figure out why &lt;code&gt;binwalk&lt;/code&gt; found nothing but steghide still has data.&lt;/p&gt;

&lt;p&gt;For JPEGs, steghide operates on DCT (Discrete Cosine Transform) coefficients — the numeric values produced during JPEG compression. It swaps pairs of coefficients using a graph-theoretic algorithm so the statistical distribution of the image stays nearly identical to an unmodified JPEG. This is why &lt;code&gt;binwalk&lt;/code&gt; shows nothing: there's no appended data, no file signature to detect. The payload is woven into the image's compression artifacts.&lt;/p&gt;

&lt;p&gt;For WAV files, it modifies the least significant bits of audio samples. The human ear can't detect it, and spectrum analysis in Audacity usually won't show anything either.&lt;/p&gt;

&lt;p&gt;Understanding this tells you when steghide is the right tool: when the file is JPEG or WAV, when &lt;code&gt;binwalk&lt;/code&gt; comes up empty, and when the challenge hints at a password or passphrase being required.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Pre-steghide Checklist
&lt;/h2&gt;

&lt;p&gt;I don't open steghide until I've done three things. This checklist has saved me from multiple dead ends:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Run file — read every field
&lt;/h3&gt;

&lt;p&gt;The Hidden in Plainsight comment field is a perfect example of why. The &lt;code&gt;file&lt;/code&gt; command output is dense and easy to skim past. Force yourself to read the comment field, the OEM-ID, the segment length — anything that looks non-standard.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Check file size vs. resolution
&lt;/h3&gt;

&lt;p&gt;A 640×640 JPEG shouldn't be 2MB. If the file size is disproportionately large for its resolution, steghide (or a similar tool) is almost certainly involved. steghide's capacity field in &lt;code&gt;steghide info&lt;/code&gt; will confirm it.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Try a blank passphrase first
&lt;/h3&gt;

&lt;p&gt;A non-trivial number of Easy-rated steganography challenges embed data with no passphrase at all. Run &lt;code&gt;steghide info&lt;/code&gt; and just press Enter when prompted. If it returns embedded file information, you're done searching for the password.&lt;/p&gt;




&lt;h2&gt;
  
  
  When steghide Fails: My Decision Tree
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Wrong passphrase vs. no data — how to tell
&lt;/h3&gt;

&lt;p&gt;This is the most common point of confusion for beginners. The error messages look different:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Wrong passphrase — data IS embedded, key is wrong
steghide: could not extract any data with that passphrase!

# No data — steghide info returns nothing
steghide: could not extract any data with that passphrase!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The messages are identical. The only way to confirm data is embedded is to run &lt;code&gt;steghide info&lt;/code&gt; first with the correct passphrase — which is circular. In practice: if the challenge involves a JPEG or WAV and you suspect steganography, assume steghide and look harder for the passphrase rather than assuming there's no data.&lt;/p&gt;

&lt;h3&gt;
  
  
  When to switch to Stegseek
&lt;/h3&gt;

&lt;p&gt;If you've exhausted the obvious passphrase sources (metadata, challenge description, filenames, visible strings in the binary) and nothing works, switch to Stegseek for brute force — not Stegcracker.&lt;/p&gt;

&lt;p&gt;The difference is significant: Stegcracker restarts the full steghide process for every attempt. Stegseek interfaces directly with the steghide library, skipping redundant hash recalculation. Against &lt;code&gt;rockyou.txt&lt;/code&gt;, Stegcracker takes hours; Stegseek takes seconds.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ stegseek suspicious.jpg /usr/share/wordlists/rockyou.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  When to abandon steghide entirely
&lt;/h3&gt;

&lt;p&gt;steghide only supports JPEG and WAV. If the file is PNG, BMP, or MP3 — stop. You're looking at a different tool. For PNGs, &lt;code&gt;zsteg&lt;/code&gt; is the equivalent. For formats steghide can't read, &lt;code&gt;steghide info&lt;/code&gt; will error immediately, which is at least a fast negative signal.&lt;/p&gt;


&lt;h2&gt;
  
  
  Tool Selection: When to Use What
&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 types&lt;/th&gt;
&lt;th&gt;Use when&lt;/th&gt;
&lt;th&gt;Skip when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;steghide&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JPEG, WAV&lt;/td&gt;
&lt;td&gt;Password/passphrase is available or suspected; binwalk empty&lt;/td&gt;
&lt;td&gt;File is PNG or any non-JPEG/WAV format&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stegseek&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JPEG&lt;/td&gt;
&lt;td&gt;steghide fails and passphrase is unknown; need brute force&lt;/td&gt;
&lt;td&gt;You have the passphrase already&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;zsteg&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;PNG, BMP&lt;/td&gt;
&lt;td&gt;File is PNG; LSB analysis needed&lt;/td&gt;
&lt;td&gt;File is JPEG or WAV&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;binwalk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Any&lt;/td&gt;
&lt;td&gt;First pass; looking for appended files or embedded signatures&lt;/td&gt;
&lt;td&gt;Stego technique is DCT-based (steghide)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h2&gt;
  
  
  My steghide Workflow
&lt;/h2&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# 1. Static analysis first — read every field
file target.jpg

# 2. Check for embedded data with blank passphrase
steghide info target.jpg
# (press Enter at the prompt)

# 3. If data confirmed, extract
steghide extract -sf target.jpg
# (enter passphrase when prompted)

# 4. If passphrase unknown, check metadata, strings, challenge text
strings target.jpg | grep -v "^.\{1\}$"
exiftool target.jpg

# 5. Last resort: brute force with Stegseek
stegseek target.jpg /usr/share/wordlists/rockyou.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;One install note: &lt;code&gt;steghide&lt;/code&gt; is occasionally missing from modern repositories due to aging dependencies. If &lt;code&gt;apt install steghide&lt;/code&gt; fails, download the &lt;code&gt;.deb&lt;/code&gt; package directly or use a pre-built CTF environment like pwntools Docker images.&lt;/p&gt;




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

&lt;p&gt;steghide only handles JPEG and WAV. When you're looking at a PNG, the tool you want is &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 in CTF&lt;/a&gt;&lt;/strong&gt; for QR/barcode patterns, or check our broader &lt;strong&gt;&lt;a href="https://alsavaudomila.com/ctf-forensics-tools/" rel="noopener noreferrer"&gt;CTF Forensics Tools guide&lt;/a&gt;&lt;/strong&gt; for PNG-specific tools like zsteg and pngcheck.&lt;/p&gt;

&lt;p&gt;If the steganography is audio-based and steghide finds nothing, the signal is likely hidden in the spectrogram rather than the sample data — that's where &lt;strong&gt;&lt;a href="https://alsavaudomila.com/audacity-in-ctf-audio-forensics-techniques-and-common-challenge-patterns/" rel="noopener noreferrer"&gt;Audacity in CTF&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;a href="https://alsavaudomila.com/sox-in-ctf-how-to-analyze-and-manipulate-audio-files/" rel="noopener noreferrer"&gt;SoX in CTF&lt;/a&gt;&lt;/strong&gt; come in.&lt;/p&gt;

&lt;p&gt;The Hidden in Plainsight challenge I solved here is documented in full in the &lt;strong&gt;&lt;a href="https://alsavaudomila.com/hidden-in-plainsight-picoctf-writeup/" rel="noopener noreferrer"&gt;Hidden in Plainsight picoCTF Writeup&lt;/a&gt;&lt;/strong&gt; — including why the double base64 encoding in the metadata is a pattern worth recognizing in future challenges.&lt;/p&gt;

</description>
      <category>ctf</category>
      <category>security</category>
      <category>linux</category>
      <category>forensics</category>
    </item>
    <item>
      <title>Corrupted File picoCTF Writeup</title>
      <dc:creator>rudy_candy</dc:creator>
      <pubDate>Mon, 20 Apr 2026 17:24:10 +0000</pubDate>
      <link>https://dev.to/rudycandy/corrupted-file-picoctf-writeup-446c</link>
      <guid>https://dev.to/rudycandy/corrupted-file-picoctf-writeup-446c</guid>
      <description>&lt;p&gt;Magic bytes corruption, hex editing, and a JPEG hiding in plain sight — this is my full walkthrough of the &lt;strong&gt;picoCTF 2019 "Corrupted File"&lt;/strong&gt; forensics challenge. The &lt;code&gt;file&lt;/code&gt; command returned just "data." No extension, no hint about the format. I spent roughly 25 minutes confused, trying the wrong tools, before two bytes of hexdump finally made everything click. Here's the complete process, including every dead end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge Overview: picoCTF 2019 "Corrupted File"
&lt;/h2&gt;

&lt;p&gt;This challenge is from &lt;strong&gt;picoCTF 2019&lt;/strong&gt; , Forensics category, rated Easy. The setup: you receive a single file with no extension. The challenge description says it's "corrupted." Your job is to figure out what it actually is and recover the hidden flag inside it.&lt;/p&gt;

&lt;p&gt;There are no other hints. No suggested tools. Just a broken file and your terminal.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CTF Name&lt;/td&gt;
&lt;td&gt;picoCTF 2019&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Challenge Name&lt;/td&gt;
&lt;td&gt;Corrupted File&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Category&lt;/td&gt;
&lt;td&gt;Forensics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Difficulty&lt;/td&gt;
&lt;td&gt;Easy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flag&lt;/td&gt;
&lt;td&gt;picoCTF{r3st0r1ng_th3_by73s_b67c1558}&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  First Look: The File Command Returns Nothing Useful
&lt;/h2&gt;

&lt;p&gt;My first instinct with any unknown file is &lt;code&gt;file&lt;/code&gt;. It checks the actual byte content — not the extension — and matches known magic byte patterns. I ran it expecting at least a partial identification:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ file corrupted_file
corrupted_file: data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;"data." That's the &lt;code&gt;file&lt;/code&gt; command's way of saying it matched absolutely nothing in its signature database. Not truncated, not partially identified — just completely unknown. My first thought was that maybe the file was totally destroyed and nothing was recoverable. I sat with that for a moment.&lt;/p&gt;

&lt;p&gt;Then I looked at the challenge name again: &lt;em&gt;Corrupted File&lt;/em&gt;. Of course it was corrupted. That was the point. Something had been deliberately broken, and that something was almost certainly the header — the magic bytes that tell every tool on your system what type of file it's looking at.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running strings — and finding more than I expected
&lt;/h3&gt;

&lt;p&gt;Before touching any hex editor, I ran &lt;code&gt;strings&lt;/code&gt; to see if any readable content survived the corruption:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ strings corrupted_file | head -20
JFIF
Exif
...
picoCTF{r3st0r1ng_th3_by73s_b67c1558}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Two things jumped out immediately. First: &lt;strong&gt;JFIF&lt;/strong&gt;. That's the JPEG File Interchange Format marker — it's specific to JPEG files and always appears just after the SOI header. Second, the flag was right there, readable as plain text near the end of the output. I could have submitted it and moved on.&lt;/p&gt;

&lt;p&gt;But I didn't, because I wanted to understand what was actually broken. Submitting the flag without understanding the fix felt like cheating myself out of the lesson.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why JPEG? How I Ruled Out Every Other Format
&lt;/h2&gt;

&lt;p&gt;When you see an unidentified file, the first question is: what format is it supposed to be? I didn't guess JPEG randomly — I reasoned through it by eliminating alternatives.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JFIF = JPEG, full stop.&lt;/strong&gt; The string "JFIF" doesn't appear in PNG, GIF, PDF, or ZIP files. It's specific to JPEG. Once I saw it in strings output, there was no ambiguity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PNG ruled out immediately.&lt;/strong&gt; PNG files contain the ASCII string "PNG" in bytes 1–3 of their magic number (&lt;code&gt;89 50 4E 47&lt;/code&gt;). Even if the first byte were corrupted, "PNG" would still show up in &lt;code&gt;strings&lt;/code&gt;. It didn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GIF ruled out.&lt;/strong&gt; GIF headers start with ASCII "GIF87a" or "GIF89a." Completely readable in &lt;code&gt;strings&lt;/code&gt;. Absent here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF ruled out.&lt;/strong&gt; PDFs begin with &lt;code&gt;%PDF&lt;/code&gt;, also readable ASCII. Not present.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ZIP ruled out.&lt;/strong&gt; ZIP files start with "PK" in ASCII. Not present either.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The only format consistent with seeing "JFIF" in the strings output was JPEG. The body of the file was fine — it was just the two-byte SOI marker at the very beginning that had been tampered with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rabbit Hole: 20 Minutes of Wrong Approaches
&lt;/h2&gt;

&lt;p&gt;I didn't go straight to hexdump. I'm being honest here: I fumbled around for a while trying things that seemed reasonable but weren't actually going to fix anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Attempt 1: Renaming and opening directly (~5 minutes)
&lt;/h3&gt;

&lt;p&gt;My immediate reaction was to rename it to &lt;code&gt;corrupted_file.jpg&lt;/code&gt; and try to open it in an image viewer. I figured maybe it just needed the extension. The image viewer opened, showed a loading spinner for a fraction of a second, then gave me an error: "Invalid or unsupported image format." Of course. File extensions are just labels — they don't affect how the actual bytes are interpreted. A JPEG viewer checks the magic bytes first, and &lt;code&gt;FF D8&lt;/code&gt; was not there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Attempt 2: exiftool (~5 minutes)
&lt;/h3&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ exiftool corrupted_file
ExifTool Version Number         : 12.76
File Name                       : corrupted_file
File Type                       : JPEG
File Type Extension             : jpg
MIME Type                       : image/jpeg
Image Width                     : 400
Image Height                    : 300
Color Components                : 3
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This was surprising. Exiftool identified it as JPEG — dimensions and everything — even without the correct magic bytes. That's because exiftool doesn't rely solely on the first two bytes; it looks for JFIF/Exif markers deeper in the file structure. Useful confirmation that the file was JPEG, but it didn't repair anything. I still couldn't view it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Attempt 3: binwalk (~10 minutes)
&lt;/h3&gt;

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

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             Unknown data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Binwalk relies on magic byte signatures at known offsets. With the first two bytes wrong, it couldn't match anything. I tried &lt;code&gt;binwalk -e&lt;/code&gt; (extract), &lt;code&gt;binwalk --dd&lt;/code&gt;, different flags — nothing. Eventually I accepted that no automated tool was going to save me here. I needed to look at the raw bytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  hexdump: Finding the Two Wrong Bytes
&lt;/h2&gt;

&lt;p&gt;I finally did what I should have done ten minutes earlier:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ hexdump -C corrupted_file | head -3
00000000  5c 78 ff e0 00 10 4a 46  49 46 00 01 01 00 00 01  |\.....JFIF......|
00000010  00 01 00 00 ff db 00 43  00 08 06 06 07 06 05 08  |.......C........|
00000020  07 07 07 09 09 08 0a 0c  14 0d 0c 0b 0b 0c 19 12  |................|
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The first two bytes: &lt;code&gt;5C 78&lt;/code&gt;. Everything from byte 2 onward looked like legitimate JPEG data — &lt;code&gt;FF E0&lt;/code&gt; is the APP0 marker, and there's "JFIF" right in the ASCII column at bytes 6–9.&lt;/p&gt;

&lt;p&gt;So what is &lt;code&gt;5C 78&lt;/code&gt;? In ASCII, &lt;code&gt;5C&lt;/code&gt; is a backslash (&lt;code&gt;\&lt;/code&gt;) and &lt;code&gt;78&lt;/code&gt; is the letter &lt;code&gt;x&lt;/code&gt;. Together: &lt;code&gt;\x&lt;/code&gt;. That's the hex escape prefix used in Python, C, and most scripting languages. When you write &lt;code&gt;\xff\xd8&lt;/code&gt; in code, the &lt;code&gt;\x&lt;/code&gt; parts are supposed to be interpreted by the language and produce the binary bytes &lt;code&gt;FF D8&lt;/code&gt;. But here, the &lt;code&gt;\x&lt;/code&gt; prefix had been written out as literal ASCII characters instead of being processed. So instead of the binary JPEG SOI marker, the file had the text "&lt;code&gt;\x&lt;/code&gt;" followed by the rest of the original sequence.&lt;/p&gt;

&lt;p&gt;A correct JPEG SOI (Start of Image) header looks like this:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FF D8 FF E0 00 10 4A 46 49 46 ...
^^^^^
SOI marker — the two bytes that identify this as JPEG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The fix: replace bytes 0 and 1 with &lt;code&gt;FF D8&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Aha" Moment: \x as a Fingerprint of the Corruption
&lt;/h2&gt;

&lt;p&gt;When I saw &lt;code&gt;5C 78&lt;/code&gt; in the hexdump, I stared at it for a moment. Then it hit me. Backslash-x. &lt;em&gt;That 's escape notation.&lt;/em&gt; The person who wrote the challenge (or the script that generated it) had likely worked with the bytes as a Python string like &lt;code&gt;b'\xff\xd8\xff\xe0...'&lt;/code&gt;, and something went wrong in the encoding — the escape sequences got written as ASCII text rather than as binary.&lt;/p&gt;

&lt;p&gt;It's such a specific kind of corruption that it immediately told me this was intentional challenge design. It's not random bit-flipping or file truncation — it's a precise two-byte substitution that takes the exact magic number and replaces it with its own escape-notation representation. Elegant, in a devious sort of way.&lt;/p&gt;

&lt;p&gt;Once I understood what had happened, the fix was obvious and I felt slightly embarrassed it took me 20 minutes to get there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Surgical Repair with hexedit
&lt;/h2&gt;

&lt;p&gt;First, copy the file. This is non-negotiable — never edit the original:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ cp corrupted_file fixed_file
$ hexedit fixed_file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;In hexedit, the display shows hex values on the left and their ASCII equivalents on the right. The cursor starts at position 0x00. I typed &lt;code&gt;FF&lt;/code&gt; — the display updated immediately, showing the changed byte. Then &lt;code&gt;D8&lt;/code&gt; for the second byte. Then &lt;code&gt;Ctrl+X&lt;/code&gt; to save and exit.&lt;/p&gt;

&lt;p&gt;The hexdump before and after, for direct comparison:&lt;/p&gt;

&lt;h3&gt;
  
  
  Before — corrupted header:
&lt;/h3&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;00000000  5c 78 ff e0 00 10 4a 46  49 46 00 01 01 00 00 01  |\.....JFIF......|
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  After — repaired header:
&lt;/h3&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;00000000  ff d8 ff e0 00 10 4a 46  49 46 00 01 01 00 00 01  |......JFIF......|
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Two bytes changed. The rest of the file: completely untouched. The JFIF marker at bytes 6–9 had been there all along, waiting for the SOI marker to precede it correctly.&lt;/p&gt;

&lt;p&gt;Confirmation with &lt;code&gt;file&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ file fixed_file
fixed_file: JPEG image data, JFIF standard 1.01, resolution (DPI), density 1x1, segment length 16, baseline, precision 8, 400x300, components 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;From "data" to a fully described JPEG image with dimensions, resolution, and encoding details. That output felt disproportionately satisfying for what was, mechanically, a two-byte fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Flag
&lt;/h2&gt;

&lt;p&gt;Opening the repaired image revealed the flag printed on it:&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;picoCTF{r3st0r1ng_th3_by73s_b67c1558}&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Full Trial Process: Every Step, Honest Results&lt;br&gt;
&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;Command&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Why it failed / What I learned&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;&lt;code&gt;file corrupted_file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"data" — unidentified&lt;/td&gt;
&lt;td&gt;Magic bytes at offset 0 don't match any known signature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Renamed to .jpg, opened in image viewer&lt;/td&gt;
&lt;td&gt;Error: invalid image format&lt;/td&gt;
&lt;td&gt;File extensions are labels only; viewers check actual header bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;`strings corrupted_file&lt;/td&gt;
&lt;td&gt;head -20`&lt;/td&gt;
&lt;td&gt;Found "JFIF" and the flag as plaintext&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;exiftool corrupted_file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Identified as JPEG (400×300)&lt;/td&gt;
&lt;td&gt;Exiftool uses deeper format parsing; useful confirmation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;binwalk corrupted_file&lt;/code&gt; (multiple flags)&lt;/td&gt;
&lt;td&gt;"Unknown data" — nothing extracted&lt;/td&gt;
&lt;td&gt;Binwalk also uses magic bytes at offset 0; dead end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;`hexdump -C corrupted_file&lt;/td&gt;
&lt;td&gt;head -3`&lt;/td&gt;
&lt;td&gt;Bytes 0–1 are &lt;code&gt;5C 78&lt;/code&gt; (ASCII "&lt;code&gt;\x&lt;/code&gt;")&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cp corrupted_file fixed_file &amp;amp;&amp;amp; hexedit fixed_file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Changed &lt;code&gt;5C 78&lt;/code&gt; → &lt;code&gt;FF D8&lt;/code&gt; at offset 0&lt;/td&gt;
&lt;td&gt;Two-byte repair of the JPEG SOI marker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;code&gt;file fixed_file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Valid JPEG, 400×300, 3 components&lt;/td&gt;
&lt;td&gt;Repair successful; format fully identified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Opened fixed_file in image viewer&lt;/td&gt;
&lt;td&gt;Flag visible in image&lt;/td&gt;
&lt;td&gt;Challenge complete&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Real-World Relevance: Magic Byte Attacks Beyond CTF
&lt;/h2&gt;

&lt;p&gt;Magic byte manipulation isn't just a CTF trick. It's a real attack technique with documented abuse cases — and understanding how it works makes you a better analyst on both sides of a security investigation.&lt;/p&gt;

&lt;h3&gt;
  
  
  File upload bypass attacks
&lt;/h3&gt;

&lt;p&gt;Many web applications validate uploaded files by checking only the magic bytes — the assumption being that if the first few bytes say "JPEG," the file is a JPEG. Attackers exploit this by prepending valid JPEG magic bytes to a PHP shell, a Python script, or any other executable payload. The server's validator sees &lt;code&gt;FF D8&lt;/code&gt; and passes the check. The file is stored. A direct HTTP request to the upload URL executes the code.&lt;/p&gt;

&lt;p&gt;This attack class has appeared in CVEs against WordPress plugins, PHP image processing libraries (particularly when &lt;code&gt;getimagesize()&lt;/code&gt; is misused as a security check), and custom file upload handlers that never perform server-side content inspection beyond the header.&lt;/p&gt;

&lt;h3&gt;
  
  
  Email gateway evasion
&lt;/h3&gt;

&lt;p&gt;Some email security gateways block attachments based on their detected file type. Malware authors have swapped the magic bytes of Windows executables (which start with &lt;code&gt;4D 5A&lt;/code&gt;, "MZ") to make them resemble PDFs or Office documents. The gateway scans the header, sees a "document," and passes the attachment. The recipient's system — which may do additional verification — then handles it correctly as an executable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Polyglot files
&lt;/h3&gt;

&lt;p&gt;A polyglot file is simultaneously valid in two different formats. The most well-known CTF example is a file that is both a valid JPEG (because it starts with &lt;code&gt;FF D8&lt;/code&gt;) and a valid ZIP archive (because the ZIP end-of-central-directory record appears at the end and doesn't interfere with JPEG parsing). Different tools interpret the same file completely differently. This has been used in real attacks to bypass content filters that only check one format's markers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anti-forensic header corruption
&lt;/h3&gt;

&lt;p&gt;Malware samples sometimes deliberately corrupt their own PE headers or remove their magic bytes to confuse automated sandboxes. If a sandbox can't identify the file type, it may skip format-specific behavioral analysis. This buys the malware time in environments where human review only happens for samples that automated systems flag clearly.&lt;/p&gt;

&lt;h3&gt;
  
  
  MIME sniffing and the nosniff header
&lt;/h3&gt;

&lt;p&gt;Browsers can disagree with servers about file types. A server might declare &lt;code&gt;Content-Type: image/jpeg&lt;/code&gt;, but if the bytes look like HTML, some browsers will sniff the content and render it as HTML — potentially executing embedded JavaScript. This is why &lt;code&gt;X-Content-Type-Options: nosniff&lt;/code&gt; is a security best practice in HTTP headers. Understanding magic bytes is fundamental to understanding why that header exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Magic Bytes Cheat Sheet for CTF Forensics
&lt;/h2&gt;

&lt;p&gt;Having these memorized saves significant time in forensics challenges:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File Type&lt;/th&gt;
&lt;th&gt;Magic Bytes (Hex)&lt;/th&gt;
&lt;th&gt;ASCII / Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JPEG&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FF D8 FF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Non-printable; followed by APP0 &lt;code&gt;FF E0&lt;/code&gt; or APP1 &lt;code&gt;FF E1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;&lt;code&gt;89 50 4E 47 0D 0A 1A 0A&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.PNG....&lt;/code&gt; — "PNG" is readable in strings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GIF&lt;/td&gt;
&lt;td&gt;&lt;code&gt;47 49 46 38&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GIF8&lt;/code&gt; — followed by "7a" or "9a"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PDF&lt;/td&gt;
&lt;td&gt;&lt;code&gt;25 50 44 46&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;%PDF&lt;/code&gt; — fully readable ASCII&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ZIP / JAR / DOCX&lt;/td&gt;
&lt;td&gt;&lt;code&gt;50 4B 03 04&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PK..&lt;/code&gt; — many formats are ZIP-based&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ELF (Linux binary)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;7F 45 4C 46&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.ELF&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows PE (.exe/.dll)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;4D 5A&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;MZ&lt;/code&gt; — from Mark Zbikowski, original DOS designer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLite database&lt;/td&gt;
&lt;td&gt;&lt;code&gt;53 51 4C 69 74 65 20 66&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SQLite f&lt;/code&gt; — starts "SQLite format 3"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Beginner Tips for Magic Byte Challenges
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start with&lt;code&gt;file&lt;/code&gt;, always.&lt;/strong&gt; If it returns "data," the header is broken. That's your diagnostic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run&lt;code&gt;strings&lt;/code&gt; before reaching for a hex editor.&lt;/strong&gt; Format-specific strings like "JFIF," "PNG," "%PDF," or "GIF8" appear in even heavily corrupted files and tell you the intended type in seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check only the first 8–16 bytes.&lt;/strong&gt; Magic bytes live at the very beginning. &lt;code&gt;hexdump -C file | head -2&lt;/code&gt; is almost always sufficient to find header corruption.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy before editing.&lt;/strong&gt; Always: &lt;code&gt;cp original working_copy&lt;/code&gt;. Run all your edits on the copy. You may need the original as a reference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;hexedit navigation:&lt;/strong&gt; Arrow keys to move, just type hex digits to overwrite. &lt;code&gt;Ctrl+X&lt;/code&gt; saves and exits. &lt;code&gt;Ctrl+C&lt;/code&gt; cancels without saving.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Python one-liner if hexedit feels uncomfortable:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;data = open('corrupted_file', 'rb').read()&lt;br&gt;
fixed = b'\xff\xd8' + data[2:]&lt;br&gt;
open('fixed_file', 'wb').write(fixed)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This replaces the first two bytes with the correct JPEG SOI marker and writes the result to a new file. Three lines. No hex editor needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently Next Time
&lt;/h2&gt;

&lt;p&gt;The biggest mistake I made was spending time on tools before looking at the raw bytes. The pattern for magic byte corruption challenges is straightforward once you've done it once:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;file&lt;/code&gt; — does it identify the format? If it returns "data," the header is wrong.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;strings&lt;/code&gt; — what format-specific strings appear? This tells you the intended file type without opening a hex editor.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;hexdump -C file | head -3&lt;/code&gt; — find the exact bytes at the start and compare to the known magic number for that format.&lt;/li&gt;
&lt;li&gt;Make a copy, open in hexedit (or write a Python one-liner), and fix the bytes.&lt;/li&gt;
&lt;li&gt;Verify with &lt;code&gt;file&lt;/code&gt; again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's a five-step process I could now execute in under two minutes. At the time it took me about 25. The difference is just pattern recognition — and that comes from working through enough of these challenges to stop second-guessing the most obvious approach.&lt;/p&gt;

&lt;p&gt;I'd also spend five minutes before any forensics CTF round memorizing the common magic bytes: JPEG, PNG, GIF, ZIP, PDF. Having that table in your head means you recognize a corrupted header instantly from the hexdump, rather than having to stop and look things up mid-solve.&lt;/p&gt;

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

&lt;p&gt;This problem is part of the picoCTF 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/red-picoctf-writeup/" rel="noopener noreferrer"&gt;RED picoCTF Writeup&lt;/a&gt; — another forensics challenge involving PNG file analysis with zsteg and exiftool, where file format understanding is equally critical&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://alsavaudomila.com/scan-surprise-picoctf-writeup/" rel="noopener noreferrer"&gt;Scan Surprise picoCTF Writeup&lt;/a&gt; — working with PNG format and QR code extraction in a CTF context&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ctf</category>
      <category>picoctf</category>
      <category>forensics</category>
      <category>security</category>
    </item>
    <item>
      <title>Crack the Power picoCTF Writeup</title>
      <dc:creator>rudy_candy</dc:creator>
      <pubDate>Mon, 20 Apr 2026 17:24:07 +0000</pubDate>
      <link>https://dev.to/rudycandy/crack-the-power-picoctf-writeup-1ng4</link>
      <guid>https://dev.to/rudycandy/crack-the-power-picoctf-writeup-1ng4</guid>
      <description>&lt;p&gt;picoCTF Crack the Power is an RSA cryptography challenge where the key insight isn't just knowing the attack — it's reading the numbers closely enough to know which version of the attack you're dealing with before you write a single line of code. I didn't do that at first, and it cost me about 40 minutes.&lt;/p&gt;

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

&lt;p&gt;The challenge provides a &lt;code&gt;message.txt&lt;/code&gt; file with three values:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;n = 557838419289674163475248835907844438905982230212026010035453573...
e = 20
c = 640637430810406857500566702096274079797813783756285040245597839...369140625
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;RSA with &lt;code&gt;e = 20&lt;/code&gt;. The hint says the primes are large, and factorization is not the intended approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Tried First (and Why It Was Wrong)
&lt;/h2&gt;

&lt;p&gt;Seeing &lt;code&gt;e = 20&lt;/code&gt; and "large primes," I immediately thought: low-exponent RSA attack. I knew two variants exist — the direct root case and the k-iteration case — but instead of figuring out which one applied here, I just reached for the general solution that handles both:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# olddecript.py — my first attempt, works but misses the point
from Crypto.Util.number import *
import gmpy2

i = 1
while True:
    m, ok = gmpy2.iroot(c + i * N, e)
    if ok:
        break
    i += 1
print(long_to_bytes(m))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This ran and produced the flag at &lt;code&gt;i = 1&lt;/code&gt;, meaning the loop exited on the first iteration. Which meant… &lt;code&gt;i = 1&lt;/code&gt; wasn't even needed. The loop was unnecessary. I had written the mini-rsa solution for a problem that didn't require it.&lt;/p&gt;

&lt;p&gt;That bothered me enough to go back and figure out why &lt;code&gt;i = 0&lt;/code&gt; would have worked just as well — and that's where the actual learning happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the Numbers Before Writing Code
&lt;/h2&gt;

&lt;p&gt;The two cases of RSA low-exponent attack differ by one condition:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No wrapping&lt;/strong&gt; (&lt;code&gt;m^e &amp;lt; N&lt;/code&gt;): the modular reduction never fires, so &lt;code&gt;c = m^e&lt;/code&gt; exactly. Direct iroot works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrapping&lt;/strong&gt; (&lt;code&gt;m^e ≥ N&lt;/code&gt;): &lt;code&gt;c = m^e mod N&lt;/code&gt;, meaning &lt;code&gt;m^e = c + k·N&lt;/code&gt; for some small k. You need to iterate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Looking at the ciphertext again:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;c = ...369140625
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The last 9 digits are &lt;code&gt;369140625&lt;/code&gt;. Now check what happens when you compute &lt;code&gt;5^20&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; 5**20
95367431640625
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The last 9 digits of &lt;code&gt;5^20&lt;/code&gt; are &lt;code&gt;431640625&lt;/code&gt;. And &lt;code&gt;369140625&lt;/code&gt; ends with the same suffix pattern — multiples of powers of 5 propagate their trailing digit structure predictably. This is a hint that &lt;code&gt;c&lt;/code&gt; is a raw integer power of a plaintext that ends in 5, not a reduced modular value. If the modular reduction had fired, those trailing digits would look arbitrary.&lt;/p&gt;

&lt;p&gt;The cleaner check: if &lt;code&gt;m^e &amp;lt; N&lt;/code&gt;, then &lt;code&gt;c &amp;lt; N&lt;/code&gt; should hold. Comparing the number of digits in c versus n in this challenge confirms c is smaller — no wrapping occurred.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import gmpy2
from Crypto.Util.number import long_to_bytes

e = 20
c = 640637430810406857500566702096274079797813783756285040245597839...369140625

m, exact = gmpy2.iroot(c, e)

print("Exact:", exact)
print(long_to_bytes(m))


$ python3 decript.py
Exact: True
b'picoCTF{t1ny_e_2fe2da79}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code&gt;exact = True&lt;/code&gt; confirms it: &lt;code&gt;c&lt;/code&gt; was a perfect 20th power, no wrapping, no iteration needed.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  How to Distinguish the Two Cases Quickly
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;No wrapping (this challenge)&lt;/th&gt;
&lt;th&gt;Wrapping (mini-rsa pattern)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compare digit count of c vs n&lt;/td&gt;
&lt;td&gt;c has fewer digits than n&lt;/td&gt;
&lt;td&gt;c has similar digit count as n&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gmpy2.iroot(c, e) exact flag&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;td&gt;False&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Required technique&lt;/td&gt;
&lt;td&gt;Direct iroot&lt;/td&gt;
&lt;td&gt;k-iteration: iroot(c + k·N, e)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loop exits at&lt;/td&gt;
&lt;td&gt;k=0 (no loop needed)&lt;/td&gt;
&lt;td&gt;k=1 or small value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The fastest approach in practice: try &lt;code&gt;gmpy2.iroot(c, e)&lt;/code&gt; first and check the exact flag. If it's &lt;code&gt;True&lt;/code&gt;, you're done. If it's &lt;code&gt;False&lt;/code&gt;, you need the k-iteration loop. No need to analyze trailing digits beforehand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters Beyond CTF
&lt;/h2&gt;

&lt;p&gt;The "no wrapping" case happens in practice when RSA is implemented without proper padding and the message is short relative to the modulus. A 20-byte flag encrypted with &lt;code&gt;e = 20&lt;/code&gt; and a 2048-bit modulus will almost always satisfy &lt;code&gt;m^e &amp;lt; N&lt;/code&gt;, making the encryption trivially reversible. This is why modern RSA uses OAEP or PKCS#1 v1.5 padding — padding inflates the effective message size to make &lt;code&gt;m^e &amp;gt; N&lt;/code&gt; guaranteed, forcing modular reduction and making direct root extraction impossible.&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 complement this challenge:&lt;/p&gt;

&lt;p&gt;For the wrapping case — where &lt;code&gt;m^e ≥ N&lt;/code&gt; and direct iroot fails — the &lt;a href="https://alsavaudomila.com/mini-rsa-picoctf-writeup/" rel="noopener noreferrer"&gt;Mini RSA writeup&lt;/a&gt; covers the k-iteration technique in detail, including why floating-point cube root methods silently fail and how gmpy2 handles it correctly.&lt;/p&gt;

&lt;p&gt;For a broader overview of RSA attack categories in CTF, &lt;a href="https://alsavaudomila.com/ctf-forensics-tools/" rel="noopener noreferrer"&gt;CTF Forensics Tools: The Ultimate Guide for Beginners&lt;/a&gt; includes the cryptography tool decision flow alongside forensics tools.&lt;/p&gt;

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