<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Research on Zero-Entry</title>
    <link>https://zero-entry.co.za/categories/research/</link>
    <description>Recent content in Research on Zero-Entry</description>
    <image>
      <title>Zero-Entry</title>
      <url>https://zero-entry.co.za/images/about.jpg</url>
      <link>https://zero-entry.co.za/images/about.jpg</link>
    </image>
    <generator>Hugo -- 0.147.7</generator>
    <language>en-us</language>
    <lastBuildDate>Fri, 12 Jun 2026 07:25:21 +0200</lastBuildDate>
    <atom:link href="https://zero-entry.co.za/categories/research/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>RF Pt 2: Fingerprinting the House</title>
      <link>https://zero-entry.co.za/posts/rf-pt-2-fingerprinting-the-house/</link>
      <pubDate>Tue, 02 Jun 2026 00:00:00 +0200</pubDate>
      <guid>https://zero-entry.co.za/posts/rf-pt-2-fingerprinting-the-house/</guid>
      <description>Picking up from the first RF post: mapping what actually lives in the air around the house. Constant emitters, event-driven traffic, rtl_433, and what&amp;#39;s worth logging vs what&amp;#39;s noise.</description>
      <content:encoded><![CDATA[<p>In <a href="../wth-im-doing-rf-now-rtl-sdr-hackrf-one-and-the-dumb-problems-i-hit/">pt 1</a> the goal was just to see the spectrum. Plug things in, fix permissions, log a band, stop being intimidated by waterfalls. This one is about turning that into a map of what actually lives in the air around the house.</p>
<p>Every place has two RF layers. Stuff that&rsquo;s always there and stuff that fires when something happens. Once you can tell them apart, the spectrum stops looking like static and starts looking like an inventory.</p>
<h2 id="the-premise">The premise</h2>
<p>A house broadcasts more than people realise. Weather sensors that ping every 30 seconds. A smart meter that wakes up on its own schedule. The garage door, which is silent until it isn&rsquo;t. Door contacts. A neighbour&rsquo;s tyre pressure sensors driving past. Half of it is licensed ISM band traffic, the rest is whatever the manufacturer thought they could get away with.</p>
<p>I wanted to know, concretely:</p>
<ul>
<li>What devices in my own environment are emitting, and on what frequencies</li>
<li>Which of them are predictable (heartbeats, polling) and which are reactive</li>
<li>What&rsquo;s mine vs what&rsquo;s leaking in from outside</li>
</ul>
<p>No reversing protocols yet. Just identification.</p>
<h2 id="tooling-shift-rtl_433-does-most-of-the-work">Tooling shift: rtl_433 does most of the work</h2>
<p><code>rtl_power</code> from pt 1 is fine for surveying energy. For identification, <code>rtl_433</code> is what you want. It ships with decoders for hundreds of consumer devices on the common ISM bands and will happily print everything it recognises as JSON.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">rtl_433 -F json -M time:iso -M level
</span></span></code></pre></div><p>That&rsquo;s the entire starting command. No frequency argument means it sits on 433.92 MHz. Once it&rsquo;s running, walk around the house and trigger things. Press the doorbell, open a window with a contact sensor, lock and unlock the car from inside. Anything decoded will print a line.</p>
<p>To save it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">rtl_433 -F json -M time:iso -M level 2&gt;/dev/null <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  <span class="p">|</span> tee -a ~/rf/rtl_433_<span class="k">$(</span>date +%F<span class="k">)</span>.jsonl
</span></span></code></pre></div><p>JSONL is the right format here. One event per line, append-only, trivial to grep through later.</p>
<p>For other bands you tell it explicitly:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">rtl_433 -f 315M -F json   <span class="c1"># North American garage doors, TPMS, some fobs</span>
</span></span><span class="line"><span class="cl">rtl_433 -f 868M -F json   <span class="c1"># European alarm sensors, smart meters</span>
</span></span><span class="line"><span class="cl">rtl_433 -f 915M -F json   <span class="c1"># NA ISM, weather stations, some utility meters</span>
</span></span></code></pre></div><p>If you have a HackRF, <code>hackrf_sweep</code> is the wider-net version for finding where activity is in the first place:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">hackrf_sweep -f 300:960 -w <span class="m">100000</span> -1 &gt; sweep.csv
</span></span></code></pre></div><p>One pass from 300 to 960 MHz, 100 kHz bins, single sweep. Open it, see where the energy is, then point rtl_433 at the live bands.</p>
<h2 id="pass-1-the-always-on-layer">Pass 1: the always-on layer</h2>
<p>I left rtl_433 running on 433 MHz for a few hours with nobody touching anything in the house. The point was to see what fires on its own.</p>
<p>What showed up in my environment:</p>
<ul>
<li>A weather station outside that transmits temperature and humidity every 47 seconds, like clockwork. Same device ID every time.</li>
<li>Soil moisture sensors from the garden, less frequent, but every device announces itself with the model name in the decoder output.</li>
<li>A smart meter reading that wasn&rsquo;t mine, coming from the direction of the neighbours&rsquo; wall.</li>
<li>An LPG tank level sensor I&rsquo;d forgotten was wireless.</li>
</ul>
<p>The pattern is heartbeats. Always-on devices want to be polled, so they shout into the void on a fixed interval, and the interval itself is fingerprintable. If a sensor pings every 47 seconds with the same ID, you can plot it on a timeline and the gaps tell you when the device was off or out of range.</p>
<p>A useful question to ask of every constant emitter: do I own this? If not, why can I hear it, and what does that say about how far my receiver is reaching.</p>
<h2 id="pass-2-the-event-driven-layer">Pass 2: the event-driven layer</h2>
<p>This is where you walk around the house being a nuisance. Press every button, open every door, trigger every motion sensor you know about. Have rtl_433 logging the whole time.</p>
<p>What I found:</p>
<ul>
<li>The garage door remote at 433.92 MHz, rolling code, decoder didn&rsquo;t name it but the bit pattern is consistent per press.</li>
<li>A wireless doorbell on 433 that the decoder identified by model.</li>
<li>Two PIR sensors from the alarm panel that wake up on motion and stay quiet otherwise.</li>
<li>Three door contacts that send a short burst on open and another on close.</li>
<li>The TV remote, which is IR not RF, and reminded me that not everything you press is going to show up on an SDR.</li>
</ul>
<p>The car key fob didn&rsquo;t decode cleanly on 433. That one was on 868 in this region, which I only confirmed by re-running rtl_433 on the other band while pressing the unlock button.</p>
<p>Event-driven traffic is the more interesting half because it&rsquo;s tied to physical state changes. A door contact that hasn&rsquo;t transmitted in 12 hours either means the door hasn&rsquo;t moved or the sensor is dead. Both are useful pieces of information if you&rsquo;re the one who owns the network.</p>
<h2 id="building-the-inventory">Building the inventory</h2>
<p>After a couple of sessions I had enough to start a flat inventory. Mine looks like a TSV with these columns:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">freq_mhz    type            label                   cadence         protocol_or_model
</span></span><span class="line"><span class="cl">433.92      heartbeat       weather_station_outside ~47s            Acurate-986
</span></span><span class="line"><span class="cl">433.92      heartbeat       soil_sensor_bed_1       ~5min           Generic-FS20
</span></span><span class="line"><span class="cl">433.92      event           garage_door_remote      on_press        unknown_rolling
</span></span><span class="line"><span class="cl">433.92      event           doorbell_front          on_press        Honeywell-RCWL
</span></span><span class="line"><span class="cl">433.92      event           door_contact_kitchen    on_state_change unknown
</span></span><span class="line"><span class="cl">868.30      heartbeat       smart_meter             ~10s            wmbus
</span></span><span class="line"><span class="cl">868.30      event           car_fob                 on_press        unknown
</span></span><span class="line"><span class="cl">915.00      heartbeat       (neighbour) utility     ~30s            unknown
</span></span></code></pre></div><p>That&rsquo;s enough to do real things with. If something new shows up that isn&rsquo;t on the list, it&rsquo;s either a new device I just installed, a neighbour got something, or somebody&rsquo;s nearby holding a transmitter. Any of those is worth a glance.</p>
<h2 id="a-small-script-for-the-boring-part">A small script for the boring part</h2>
<p>The bit I wanted automated was &ldquo;tell me what&rsquo;s new since yesterday&rdquo;. This is a 20-line script, not a project:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="ch">#!/usr/bin/env python3</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">json</span><span class="o">,</span> <span class="nn">sys</span><span class="o">,</span> <span class="nn">collections</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">seen</span> <span class="o">=</span> <span class="n">collections</span><span class="o">.</span><span class="n">defaultdict</span><span class="p">(</span><span class="nb">int</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="n">sys</span><span class="o">.</span><span class="n">stdin</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">d</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">line</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">except</span> <span class="n">json</span><span class="o">.</span><span class="n">JSONDecodeError</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">continue</span>
</span></span><span class="line"><span class="cl">    <span class="n">key</span> <span class="o">=</span> <span class="p">(</span><span class="n">d</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;model&#34;</span><span class="p">,</span><span class="s2">&#34;?&#34;</span><span class="p">),</span> <span class="n">d</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;id&#34;</span><span class="p">,</span><span class="s2">&#34;?&#34;</span><span class="p">),</span> <span class="nb">round</span><span class="p">(</span><span class="n">d</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;freq&#34;</span><span class="p">,</span><span class="mi">0</span><span class="p">),</span> <span class="mi">2</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">seen</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">dev_id</span><span class="p">,</span> <span class="n">freq</span><span class="p">),</span> <span class="n">count</span> <span class="ow">in</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">seen</span><span class="o">.</span><span class="n">items</span><span class="p">(),</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="o">-</span><span class="n">x</span><span class="p">[</span><span class="mi">1</span><span class="p">]):</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">count</span><span class="si">:</span><span class="s2">6d</span><span class="si">}</span><span class="s2">  </span><span class="si">{</span><span class="n">freq</span><span class="si">:</span><span class="s2">7.2f</span><span class="si">}</span><span class="s2"> MHz  </span><span class="si">{</span><span class="n">model</span><span class="si">:</span><span class="s2">30s</span><span class="si">}</span><span class="s2">  id=</span><span class="si">{</span><span class="n">dev_id</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span></code></pre></div><p>Pipe a day of JSONL through it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat ~/rf/rtl_433_2026-06-01.jsonl <span class="p">|</span> ./rf_summary.py
</span></span></code></pre></div><p>The output is a leaderboard of which device IDs were most active. New entries are obvious. Missing entries are obvious. That&rsquo;s most of what you need for daily checks.</p>
<h2 id="what-surprised-me">What surprised me</h2>
<p>A few things from this round.</p>
<p>The smart meter wasn&rsquo;t mine. It&rsquo;s loud enough to clear the wall. That&rsquo;s not a vulnerability exactly, but consumption telemetry leaving the building in the clear is a thing worth knowing about.</p>
<p>Cars driving past show up. TPMS sensors transmit at 315 or 433 depending on region and you can see them passing if you&rsquo;re logging long enough. None of it is identifying on its own, but the pattern of passes is.</p>
<p>A device I thought was wired was wireless. The LPG tank sensor was installed by a contractor years ago and I had no documentation on it. The decoder gave me the model in one line of JSON.</p>
<p>Most &ldquo;smart&rdquo; devices announce themselves by manufacturer in the protocol header. There&rsquo;s no obscurity here. If it&rsquo;s on a hobbyist decoder list, it&rsquo;s identifiable.</p>
<h2 id="what-im-not-doing">What I&rsquo;m not doing</h2>
<p>Not replaying anything. Not transmitting on the HackRF. Rolling code captures are interesting from a &ldquo;how does this work&rdquo; point of view, but the moment you press the button on a transmitter aimed at someone else&rsquo;s gate you&rsquo;ve crossed a line that isn&rsquo;t worth crossing. The HackRF stays on receive.</p>
<p>Not trying to demodulate anything rtl_433 doesn&rsquo;t already know about. URH (Universal Radio Hacker) is the next step for that and it&rsquo;s its own rabbit hole.</p>
<h2 id="lessons">Lessons</h2>
<p>A few short ones.</p>
<p>Heartbeats are gold. Anything with a fixed cadence is easy to spot, easy to baseline, easy to alarm on when it goes quiet.</p>
<p>The decoder is more useful than the waterfall once you know roughly where things live. Waterfalls are for finding signals. Decoders are for identifying them.</p>
<p>Region matters. ISM bands are different in EU, US, and AU. If a fob doesn&rsquo;t show on 433, it&rsquo;s probably on 315 or 868. Try the other ones before assuming the hardware is broken.</p>
<p>Your inventory will outlive any single script. Keep it in a flat file you can read in ten years.</p>
<h2 id="whats-next">What&rsquo;s next</h2>
<p>A 30-day version of the inventory, with cadence drift tracking. If the weather station&rsquo;s 47-second heartbeat starts arriving every 52 seconds, I want to know about it.</p>
<p>Some long captures on 868 MHz. The smart meter and the car fob are both there and I haven&rsquo;t given that band the time it deserves.</p>
<p>A small dashboard. Probably a static page that reads the JSONL and renders last-seen times per device. Not a project, just a page.</p>
<h2 id="legal">Legal</h2>
<p>Same as before. Receiving on bands I&rsquo;m allowed to receive on, in an environment I own, on equipment I own. No transmitting, no replaying, no decoding anything that isn&rsquo;t mine to look at. RF is the kind of hobby where the line between curious and a problem is short and worth respecting.</p>
]]></content:encoded>
    </item>
    <item>
      <title>30 Days of a Honeypot at Home</title>
      <link>https://zero-entry.co.za/posts/30-days-of-a-honeypot-at-home/</link>
      <pubDate>Sat, 18 Apr 2026 20:30:00 +0200</pubDate>
      <guid>https://zero-entry.co.za/posts/30-days-of-a-honeypot-at-home/</guid>
      <description>Standing up T-Pot on a segmented VLAN behind OPNsense, opening a curated set of ports to a residential IP, and writing down what 30 days of unfiltered internet traffic actually looked like.</description>
      <content:encoded><![CDATA[<p>I finally got around to putting a honeypot on the public side of my home connection. I wasn&rsquo;t trying to catch APTs. I wanted to see what hits a random residential IP when nothing is hiding it.</p>
<p>This is a notes post about standing it up, how it&rsquo;s contained, and what actually showed up in the logs after a month.</p>
<h2 id="why-bother">Why bother</h2>
<p>Most threat intelligence I read describes the internet as a battlefield. Every unpatched device is five minutes from compromise. Every IP gets 30,000 probes a day. The numbers are usually correct. They aren&rsquo;t useful unless you can map them to what your environment looks like.</p>
<p>I wanted my own baseline. Not a vendor&rsquo;s feed, not an aggregated report. What does my ISP-assigned IP attract, right now, and what does the traffic look like when you strip out the marketing spin.</p>
<p>Secondary reason: I wanted to segment the network, and a honeypot is a good forcing function. My homelab had been flat for too long. Nothing makes you VLAN a network faster than hanging something deliberately exposed off it.</p>
<h2 id="threat-model-and-ground-rules">Threat model and ground rules</h2>
<p>Before anything went online, I wrote down what I was willing to tolerate and what I wasn&rsquo;t.</p>
<p>Willing to accept:</p>
<ul>
<li>My IP appearing on scanning blocklists.</li>
<li>My ISP sending me a polite note. (They never did.)</li>
<li>Getting buried in logs I&rsquo;d then have to process.</li>
</ul>
<p>Not willing to accept:</p>
<ul>
<li>Anything the honeypot attracts touching my real LAN.</li>
<li>A honeypot compromise turning into a pivot into anything else I own.</li>
<li>Outbound traffic that looks like I&rsquo;m participating in someone else&rsquo;s botnet.</li>
</ul>
<p>Those three constraints drove every architectural decision that followed.</p>
<h2 id="architecture">Architecture</h2>
<p>The honeypot runs as a single VM on a secondary host, isolated on its own VLAN behind OPNsense. It has one purpose, no shared storage, no shared credentials, and nothing legitimate behind it. If it gets popped, I wipe the VM from snapshot and start again.</p>
<p>The physical picture:</p>
<ul>
<li><strong>Honey-VM</strong>: 4 vCPU, 8 GB RAM, 60 GB disk. Ubuntu 22.04 base, T-Pot on top.</li>
<li><strong>VLAN 66</strong>: dedicated &ldquo;DMZ-lite&rdquo;. No inter-VLAN access. DHCP scoped tight.</li>
<li><strong>OPNsense</strong>: port-forwards a curated set of TCP ports from WAN to the honey-VM.</li>
<li><strong>Suricata</strong> on the OPNsense WAN interface logs everything hitting those ports, independent of what the honeypot itself sees.</li>
<li><strong>Outbound rules on VLAN 66</strong>: no outbound except to a specific syslog collector and to Cloudflare DoH for DNS. No SSH out, no SMB out, no SMTP out, no arbitrary outbound anything.</li>
</ul>
<p>The last point is the one that matters most. A honeypot with open outbound is a honeypot that can participate in the abuse you&rsquo;re trying to study. If Cowrie accepts a shell and the intruder tries to <code>curl</code> a second-stage payload, they should fail at the network, not at the endpoint.</p>
<p>Ports exposed to the internet: 22, 23, 80, 443, 445, 1433, 2222, 3306, 3389, 5060, 5900, 8080. Nothing else.</p>
<h2 id="stack">Stack</h2>
<p>I used T-Pot for the heavy lifting. It&rsquo;s maintained, it aggregates sensible honeypots, and its dashboards are ready out of the box. The components that mattered for me:</p>
<ul>
<li><strong>Cowrie</strong> on 22 and 2222. SSH and Telnet. Logs full session transcripts.</li>
<li><strong>Dionaea</strong> on 445, 1433, 3306, 5060. Protocol emulation for SMB, MSSQL, MySQL, SIP.</li>
<li><strong>Heralding</strong> on anything else that smells like a login prompt.</li>
<li><strong>Honeytrap</strong> as the generic catch-all TCP listener.</li>
<li><strong>Snare / Tanner</strong> serving HTTP decoy content on 80 and 8080.</li>
<li><strong>Elasticsearch + Kibana</strong> for the dashboards, with data shipped out to my own Loki instance as a backup.</li>
</ul>
<p>T-Pot&rsquo;s internal firewalling is fine, but I don&rsquo;t rely on it. The OPNsense rules are the real enforcement. If T-Pot broke tomorrow, nothing on VLAN 66 would suddenly start talking to my real network.</p>
<h2 id="standing-it-up">Standing it up</h2>
<p>Provisioning was uneventful once the segmentation was in place.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># T-Pot install on a fresh Ubuntu 22.04</span>
</span></span><span class="line"><span class="cl">env bash -c <span class="s2">&#34;</span><span class="k">$(</span>curl -sL https://ghst.ly/tpot-install<span class="k">)</span><span class="s2">&#34;</span>
</span></span></code></pre></div><p>The installer handles Docker, pulls the containers, and wires up the reverse proxy for the Kibana side. The one thing I changed was restricting the admin web interface to the management VLAN, reachable only over WireGuard.</p>
<p>SSHD for actual host management got moved to a non-standard port on the management interface. Cowrie owns 22 on the WAN side.</p>
<p>Before opening the firewall, I ran a full scan from an external VPS against the advertised ports to make sure only the intended services responded, and that the banners looked realistic enough to not scream &ldquo;honeypot&rdquo; on the first handshake. A couple of Cowrie defaults were too obvious (the default SSH version string, for one) and needed tweaking. Anyone running OpenCanary fingerprints or Shodan&rsquo;s honeypot detector will still figure it out. Most bots won&rsquo;t.</p>
<h2 id="first-24-hours">First 24 hours</h2>
<p>I expected a slow ramp while DNS caches and scanners noticed the IP. That wasn&rsquo;t the experience.</p>
<p>Within 12 minutes of opening port 22, the first SSH login attempt came in. Inside the first hour: 340 SSH attempts from 47 unique source IPs. By the end of the first day: 8,200 SSH attempts, 1,100 HTTP requests, and around 60 SMB connections.</p>
<p>There is no onboarding period for a public IP. You&rsquo;re in the database already. Opening a port just tells the scanners that something is finally listening.</p>
<h2 id="what-30-days-actually-looked-like">What 30 days actually looked like</h2>
<p>Rough totals at the 30-day mark (Suricata plus T-Pot aggregated):</p>
<ul>
<li><strong>SSH and Telnet attempts</strong>: 412,000 across 22 and 2222, from 14,300 unique source IPs.</li>
<li><strong>HTTP requests to honeypot web roots</strong>: 28,400. Mostly scanner fingerprints and path probes.</li>
<li><strong>SMB connections</strong>: 3,900.</li>
<li><strong>MSSQL login attempts</strong>: 1,100.</li>
<li><strong>RDP connections</strong>: 9,700, almost all from a handful of subnets running NLA probes.</li>
<li><strong>SIP INVITE floods</strong>: two distinct campaigns, one targeting Asterisk defaults, one targeting a specific FreePBX module.</li>
</ul>
<p>Geography is the least interesting dimension and the one vendors love to lead with. Source IPs spread across 90+ countries. That maps to compromised hosts, not operator location. Treating a GeoIP heatmap as a map of threat actors is a mistake.</p>
<p>The credential side is more useful. Top ten SSH username/password combinations over the 30 days, in order:</p>
<ol>
<li><code>root</code> / <code>root</code></li>
<li><code>admin</code> / <code>admin</code></li>
<li><code>root</code> / <code>123456</code></li>
<li><code>root</code> / <code>password</code></li>
<li><code>admin</code> / <code>password</code></li>
<li><code>user</code> / <code>user</code></li>
<li><code>ubnt</code> / <code>ubnt</code></li>
<li><code>pi</code> / <code>raspberry</code></li>
<li><code>root</code> / <code>1234</code></li>
<li><code>support</code> / <code>support</code></li>
</ol>
<p>None of those will surprise anyone who&rsquo;s spent time on this. They confirm what the big honeypot operators have been saying for years: the low end of the attack surface is stuck on the same dictionary it&rsquo;s been stuck on for a decade, because it keeps working.</p>
<h2 id="what-the-payloads-looked-like">What the payloads looked like</h2>
<p>Cowrie logs full session transcripts, which is the part worth reading. Patterns I saw repeatedly:</p>
<ul>
<li><code>uname -a; cat /proc/cpuinfo; free -m</code> as environment fingerprinting before any payload drop. The bot wants to know whether it landed on an ARM router, a MIPS camera, or an x86 box, so it can pull the right binary.</li>
<li>A <code>wget</code> or <code>curl</code> chain pointing at a staging server, usually on port 80 over a direct IP with no DNS. Almost always a short-lived URL, dead within days.</li>
<li>A <code>chmod 777</code> on the downloaded binary, followed by <code>./&lt;binary&gt;</code> and a quick <code>rm</code> to clean up.</li>
<li>Busybox-style commands with shell tricks to survive minimal environments.</li>
</ul>
<p>Three payloads I captured and detonated in an isolated environment later:</p>
<ul>
<li>A Mirai variant targeting MIPS and ARM, with the usual hardcoded C2 list.</li>
<li>A Monero miner compiled for x86_64 with an embedded pool address and worker ID.</li>
<li>An XorDDoS dropper with the &ldquo;encrypted&rdquo; strings still trivially XOR-decodable against a one-byte key.</li>
</ul>
<p>Nothing novel. That&rsquo;s the point. The mass of the internet&rsquo;s attack noise is bots spraying five-year-old payloads at anything that looks like a vulnerable edge device. A residential IP running no listening services would never see this traffic because the TCP connections would simply RST. Exposing ports makes you legible to the layer that scans for this kind of target.</p>
<h2 id="the-quieter-more-interesting-traffic">The quieter, more interesting traffic</h2>
<p>Once you filter out the SSH brute force floor, and it is a floor of roughly 300 to 600 attempts per hour, the rest gets more varied.</p>
<p>Log4Shell probes still show up on HTTP. More than two years after the advisory, JNDI probes are a standing wave. Most point at self-hosted Burp collaborators or long-dead VPS callbacks. Somebody, somewhere, is still paying for a scanner that fires these shots and never checks whether anyone answered.</p>
<p>A handful of requests were clearly scripted against specific CVEs:</p>
<ul>
<li>GPON router authentication bypass (CVE-2018-10561 / 10562). Still hitting in 2026.</li>
<li>Various Ivanti and Fortinet path traversals.</li>
<li>Confluence OGNL injection strings.</li>
<li>Generic WordPress <code>xmlrpc.php</code> pingback fishing.</li>
</ul>
<p>The unusual one: a small cluster of requests that tried to negotiate TLS with an SNI matching a real banking domain. No credentials, no follow-up. Probably a scanner doing inventory for cert transparency lookups. Possibly something less innocent. I don&rsquo;t have enough data to tell, and that&rsquo;s the honest answer.</p>
<h2 id="operational-reality">Operational reality</h2>
<p>A honeypot is not a set-and-forget box. In the first week I burned an evening chasing false positives and another fixing log rotation before a partition filled. The real costs:</p>
<ul>
<li>Disk grows fast. Cowrie session logs plus Elasticsearch indices ate about 18 GB in 30 days. That&rsquo;s cheap, but it isn&rsquo;t free.</li>
<li>Containers drift. Watchtower handles the weekly pull. I still review breaking changes in the T-Pot release notes before merging.</li>
<li>Elasticsearch is memory-hungry and will swap itself into uselessness if you underprovision. 8 GB is the practical floor.</li>
<li>Alerting is where this gets useful. A 500-per-hour SSH baseline is noise. A successful Cowrie shell that persists for more than 30 seconds, or any outbound hit blocked at the OPNsense rule, is signal. Those page me. Everything else goes to a dashboard I check when I feel like it.</li>
</ul>
<p>The alerting config is where most of my ongoing time goes. Without it, the whole thing is a pretty dashboard.</p>
<h2 id="what-id-change">What I&rsquo;d change</h2>
<p>A few things I&rsquo;ll do on the next iteration:</p>
<p>Split the honeypot across two IPs. Run Cowrie on one, everything else on the other. The SSH noise crowds the indices and makes queries slower than they need to be.</p>
<p>Move the dashboards off the honey-VM entirely. Shipping to an external Loki instance is already half the work. The remaining Kibana stack on-box is there for convenience, not necessity.</p>
<p>Add a second outbound-blocking layer inside the VM itself. Defence in depth against a container escape I&rsquo;m still not fully satisfied with.</p>
<p>Log rolling PCAPs on a 7-day window. Right now I only have what Suricata and the honeypots chose to log. Full packet captures would let me revisit sessions I under-investigated at the time.</p>
<h2 id="does-this-change-what-i-do-at-work">Does this change what I do at work</h2>
<p>Partially. It doesn&rsquo;t change how I think about APT-level adversaries. Nothing I saw in 30 days would strain a reasonable environment.</p>
<p>What it changes is how I talk about the baseline. The background radiation of the internet is real, it&rsquo;s measurable, and it doesn&rsquo;t stop. Any machine with an unpatched edge service survives hours, not days. Any default credential on a public interface is already compromised. You just haven&rsquo;t noticed yet.</p>
<p>That isn&rsquo;t a marketing line. That&rsquo;s 412,000 login attempts across 30 days on one residential IP running an obvious honeypot.</p>
<h2 id="closing">Closing</h2>
<p>Segment first. Then break something on purpose, in a place where it can&rsquo;t reach anything you care about. The logs that come back are more honest than any vendor report.</p>
]]></content:encoded>
    </item>
    <item>
      <title>WTH I&#39;m Doing RF Now: RTL-SDR &#43; HackRF One (and the dumb problems I hit)</title>
      <link>https://zero-entry.co.za/posts/wth-im-doing-rf-now/</link>
      <pubDate>Sun, 22 Feb 2026 00:00:00 +0200</pubDate>
      <guid>https://zero-entry.co.za/posts/wth-im-doing-rf-now/</guid>
      <description>First sessions with RTL-SDR and HackRF One: scanning 433 MHz, fixing udev permissions, building a passive logger, and the practical lessons from the first two hours.</description>
      <content:encoded><![CDATA[<p><img loading="lazy" src="/images/Pasted%20image%2020260219202312.png"></p>
<p>I&rsquo;ve started digging into RF, meaning anything noisy in the air that my SDR can see. This is a quick log of the first sessions using an RTL-SDR (cheap, RX-only) and a HackRF One (wider bandwidth, TX-capable, which stays off outside a legal setup).</p>
<p>This isn&rsquo;t a decoding write-up. The goal for now is observation: watch the spectrum, log activity, and build something useful.</p>
<h2 id="the-kit">The kit</h2>
<ul>
<li><strong>RTL-SDR</strong> (RTL2832U + R820T): cheap receive, wide community support, good for learning.</li>
<li><strong>HackRF One</strong>: wider tuning range, bigger bandwidth, better lab potential.</li>
</ul>
<p>Antennas matter more than most people want to admit. A random wire will pick something up, but it&rsquo;ll also mislead you.</p>
<h2 id="what-im-trying-to-do-v1">What I&rsquo;m trying to do (v1)</h2>
<p>Three things:</p>
<ol>
<li>Scan a band, starting with the 433 MHz junk drawer</li>
<li>Log energy and activity over time to CSV</li>
<li>Watch live when a remote is pressed or something triggers</li>
</ol>
<p>No demodulation, no protocol reversing yet. Just what&rsquo;s alive and when.</p>
<h2 id="rtl-sdr-first-scans-and-the-empty-csv-problem">RTL-SDR: first scans and the empty CSV problem</h2>
<p><code>rtl_power</code> is the right starting point for band surveys:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">rtl_power -f 430M:440M:50k -i <span class="m">1</span> -g <span class="m">30</span> -e 15s /tmp/rtl_433_test.csv
</span></span><span class="line"><span class="cl">head -n <span class="m">3</span> /tmp/rtl_433_test.csv
</span></span></code></pre></div><p>Two things showed up immediately:</p>
<ul>
<li>Frequency hops, FFT bins, the tool doing its job.</li>
<li>This line:</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">[R82XX] PLL not locked!
</span></span></code></pre></div><p>It looks bad. In practice it&rsquo;s common with these dongles and the data is often usable anyway. If it looks broken, try adjusting the frequency range slightly, dropping the gain, swapping the USB port or cable, or ruling out power starvation.</p>
<p>The other thing: the numbers move even when you think nothing is happening. Your receiver sees something constantly. The job is separating signal from noise and logging it so you can compare runs over time.</p>
<h2 id="hackrf-the-access-denied-wall">HackRF: the access denied wall</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">hackrf_info
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">hackrf_open() failed: Access denied (insufficient permissions) (-1000)
</span></span></code></pre></div><p>Linux has the hardware, you don&rsquo;t. Fix it with udev rules.</p>
<h3 id="the-fix">The fix</h3>
<p>Add yourself to the plugdev group:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo usermod -aG plugdev <span class="nv">$USER</span>
</span></span><span class="line"><span class="cl"><span class="c1"># log out and back in after this</span>
</span></span></code></pre></div><p>Check whether HackRF udev rules exist for your distro. If not, create them:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo nano /etc/udev/rules.d/53-hackrf.rules
</span></span></code></pre></div><p>Rule:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">SUBSYSTEM==&#34;usb&#34;, ATTR{idVendor}==&#34;1d50&#34;, ATTR{idProduct}==&#34;6089&#34;, MODE=&#34;0666&#34;
</span></span></code></pre></div><p>Reload:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo udevadm control --reload-rules
</span></span><span class="line"><span class="cl">sudo udevadm trigger
</span></span></code></pre></div><p>After that, <code>hackrf_info</code> works.</p>
<h2 id="driver-detach-and-reattach-noise">Driver detach and reattach noise</h2>
<p>RTL tools print this during normal operation:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">Detached kernel driver
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">Reattached kernel driver
</span></span></code></pre></div><p>The SDR tooling takes temporary ownership of the USB device. It becomes a problem when another service grabs the device at the same time, or when you switch tools quickly and the reattach doesn&rsquo;t complete cleanly. Unplug and replug is a valid fix.</p>
<h2 id="current-workflow">Current workflow</h2>
<h3 id="1-quick-band-scan">1. Quick band scan</h3>
<p>Pick a known band, collect a short sample, inspect.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">rtl_power -f 430M:440M:50k -i <span class="m">1</span> -g <span class="m">30</span> -e 60s ./rf_433_1min.csv
</span></span></code></pre></div><h3 id="2-trigger-devices-while-logging">2. Trigger devices while logging</h3>
<p>Press a remote, ring a doorbell, whatever you own and are allowed to test. Watch what shows up.</p>
<h3 id="3-passive-logger">3. Passive logger</h3>
<p>A script that samples a band, writes a CSV row with timestamp and strongest bins, and repeats for hours or days. The goal is trend visibility: activity spikes at these times, at these frequencies.</p>
<h3 id="4-long-runs-in-screen">4. Long runs in screen</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">screen -S rfwatch
</span></span><span class="line"><span class="cl"><span class="c1"># run your loop / script</span>
</span></span><span class="line"><span class="cl"><span class="c1"># detach: Ctrl+A then D</span>
</span></span><span class="line"><span class="cl">screen -r rfwatch
</span></span></code></pre></div><h2 id="lessons-from-the-first-two-hours">Lessons from the first two hours</h2>
<ul>
<li>Antenna placement matters more than gain settings. Move it 30cm and signal becomes noise.</li>
<li>Gain is not volume. Too much gain makes every bin look busy and kills your ability to compare runs.</li>
<li>433 MHz is crowded. Good for learning, bad for clean data.</li>
<li>A CSV you can graph beats ten live waterfall sessions.</li>
<li>Fix udev permissions before you need them, not during a session.</li>
</ul>
<h2 id="rtl-sdr-vs-hackrf">RTL-SDR vs HackRF</h2>
<p><strong>RTL-SDR</strong>: default always-on receiver. Cheap enough to leave plugged in and logging. Good for band surveys and learning.</p>
<p><strong>HackRF One</strong>: used for broader coverage and lab work. TX stays off unless the environment is controlled and the use is legal.</p>
<h2 id="whats-next">What&rsquo;s next</h2>
<ul>
<li>A 24-hour monitor for 433 MHz, then other bands</li>
<li>Lightweight analysis: top active frequencies, time-of-day patterns, spikes worth investigating</li>
<li>Mapping RF fingerprints in the environment: gates, remotes, alarms, sensors. What&rsquo;s constant vs what&rsquo;s event-driven.</li>
</ul>
<h2 id="legal">Legal</h2>
<p>I&rsquo;m monitoring what I&rsquo;m allowed to monitor, on equipment I own, in environments where I have permission. RF gets murky fast if you treat it like network recon without thinking about it first.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
