[{"content":"There aren\u0026rsquo;t that many homes I wasn\u0026rsquo;t hunting anything. I was bored, a couple coffees deep, scrolling the proxy section of a forum I had no real business being on. The same ad kept sliding past me, worded a dozen slightly different ways from a dozen different shops: millions of residential IPs, any city, pick a country, billed by the gigabyte, pennies each. Millions. From basically every storefront on the page.\nSo I sat there and did the dumb arithmetic. Residential means a real home, a real router, a real broadband line. Homes are finite. There\u0026rsquo;s a hard, physical ceiling on how many living rooms exist in a given city. So where do millions of residential IPs come from, sold and resold across all these shops at once?\nThere are two honest answers. One: people opt in. There are legit bandwidth-sharing SDKs bundled into free apps, and if you actually read the consent screen, you traded your spare upload for a free flashlight or whatever. Two: providers buy and resell each other\u0026rsquo;s pools, so the same real home shows up under five brand names.\nAnd then there\u0026rsquo;s the ugly third answer. Some of those \u0026ldquo;residential\u0026rdquo; IPs are homes whose owners never agreed to anything. Their device just quietly became somebody else\u0026rsquo;s exit node. Keep that third one in mind, it shows up again later.\nI filed the whole thing away as an interesting shower thought and moved on. Then, completely unrelated, a phone landed in my lap.\nA phone with a fever Someone I know had an Android phone acting possessed. They handed it to me the way you hand someone a sick pet — half embarrassed, half worried. The battery ran warm even sitting on the nightstand doing nothing. Cellular data was evaporating overnight — gigabytes gone by morning, no video, no downloads, nothing that explained it. Classic \u0026ldquo;my phone is haunted\u0026rdquo; symptoms.\nI asked if I could poke at it. They said go for it. That yes is the reason any of this exists — the owner handed the phone over, and that phone is the only device I touched in the whole story. Everything after it is just me at home, taking files apart on my own machines.\nFirst move was the boring one: put the phone\u0026rsquo;s traffic through Wireshark and just watch. It didn\u0026rsquo;t take long. Underneath the normal noise there was one connection that never went away — a persistent TCP session to a datacenter IP, on a weird high port, held open 24 hours a day. Not bursty. Not \u0026ldquo;app woke up to sync.\u0026rdquo; Just held open the whole time. That port, 1334, only ever showed up right here, in the phone\u0026rsquo;s own capture.\nA phone that keeps one long-lived tunnel open to a datacenter all night isn\u0026rsquo;t resting. That\u0026rsquo;s a phone that\u0026rsquo;s working for someone.\nA game from a Telegram channel So I asked the obvious question: what did you install recently, and where from?\nThe answer was a Telegram channel for free cracked games. Not one random APK — a whole distribution operation, repackaged versions of popular titles, posted on a schedule. Each \u0026ldquo;cracked game\u0026rdquo; was really a delivery vehicle. And the clever bit, the reason nobody suspects anything, is that the game actually works. You install it, it launches, it plays. The person got their free game and never had a reason to look closer. The malware was a passenger, and the game was a very convincing driver.\nI pulled the APK off the device over adb and took it home to look at it properly.\napkscan, or reading APKs without running them I didn\u0026rsquo;t want to run this thing. Running unknown malware to see what it does is a great way to learn things you\u0026rsquo;ll regret. So I ran it through apkscan, a little static-analysis pipeline I\u0026rsquo;d built for exactly this — triaging sketchy APKs without ever executing them. Everything it does is static; nothing gets a chance to phone home.\nIt answers three questions I care about:\nCan this thing live in the background and call out? It reads the manifest shape — foreground services, boot receivers, WorkManager / JobService, anything designed to survive reboots and keep a process alive. Who signed it? It hashes the signing certificate (SHA-256), which clusters repacks together and flags the lazy tells — AOSP debug keys, test keys. A real app signed with a debug or test key is a giant flashing sign that something got rebuilt by someone who isn\u0026rsquo;t the original developer. What native code is it carrying? Every lib/\u0026lt;abi\u0026gt;/*.so gets hashed, with its exports, imports, DT_NEEDED, and JNI surface pulled out. On top of that it has some heuristics: fingerprints for a few known commercial bandwidth-sharing SDKs, and a generic \u0026ldquo;there\u0026rsquo;s a silent background worker in here\u0026rdquo; flag. Nothing magic — just the questions I\u0026rsquo;d ask by hand, automated so I don\u0026rsquo;t skip one at 1 a.m.\nThis APK lit up the background-worker flag immediately. But the real move was comparing it against a clean copy.\nA game that needed a Go runtime The best trick with a repackaged app is that the original still exists. You can grab the clean, legitimate version of the same game and lay it right next to the trojanized one. That turns a needle-in-a-haystack problem into a much smaller question: what\u0026rsquo;s different? So I diffed them, file by file. Three files had been injected that didn\u0026rsquo;t belong.\nFile What it is What it\u0026rsquo;s for A decoy .mp3 An audio file with no actual payload Almost certainly padding, there to make the repack\u0026rsquo;s file size line up A bootstrap .dex A handful of Java classes Loads and invokes the native library — the glue libgojni.so An ELF shared object, Go-compiled The actual malware That last row is the one that made me sit up. libgojni.so is a native library compiled from Go. A mobile game does not need a Go runtime bolted into it — there is no legitimate reason for a match-three or a racing game to ship an embedded Go binary. But Go is exactly what you reach for when you want to write a networking daemon and drop it onto something. The name is meant to look like boring JNI plumbing. It is anything but.\nSo I ran strings on it, half expecting the usual stripped-binary nothing. The nice thing about Go is it bakes its build metadata straight into the binary. Out fell this — and it basically spells out what the thing is:\nbuild -ldflags=\u0026#34;-s -w -X maxgit.net/sdk/proxy-android-lib-tiny/internal/proxy.version=v1.2.4 -X maxgit.net/sdk/proxy-android-lib-tiny/internal/proxy.buildTime=2025-06-17_09:06:19 -X maxgit.net/sdk/proxy-android-lib-tiny/internal/proxy.serversStr=hornmot.com\u0026#34; The SDK names itself: proxy-android-lib-tiny, version v1.2.4. That version number matters more than it looks — a v1.2.4 is a maintained product with a version history behind it, not some one-off someone banged out on a weekend. Its source lives on a private git host, maxgit.net. And there\u0026rsquo;s a hardcoded config domain sitting right there in the build flags: hornmot.com. The word proxy appears six times across those three flags. This isn\u0026rsquo;t hiding what it is from anyone who bothers to look — it\u0026rsquo;s hiding from people who never look.\nIt doesn\u0026rsquo;t want your files. It wants your connection. The Go import list closed the case on intent. Two dependencies mattered: github.com/armon/go-socks5, a SOCKS5 server, and github.com/elazarl/goproxy, an HTTP/HTTPS proxy that can intercept requests.\nPut those together and the picture is clean. The malware turns the phone into a proxy node — SOCKS5 and HTTP both. It\u0026rsquo;s not rummaging for your photos or your files. It doesn\u0026rsquo;t care about you at all. It\u0026rsquo;s renting out your internet connection to strangers, and your phone is the meter. That\u0026rsquo;s why the battery was warm and the data was gone by morning. It was somebody else\u0026rsquo;s exit node all night.\nThe ugly third answer, confirmed. This was a home that never agreed to anything — exactly the option I\u0026rsquo;d shrugged off on the forum weeks earlier, which was a little eerie.\nTwo hops and a port nobody uses Watching it connect, the C2 path is two hops, not one. First the malware reaches out to hornmot.com — sitting behind Cloudflare, over TLS — and that server hands back a relay address. Then it opens the persistent connection to that relay, on TCP 1334, and holds it open forever. The domain is just a directory that points you at the muscle. Kill the domain and the relay list moves; the relay does the real work.\ninfected phone (bot) hornmot.com config \u0026#183; Cloudflare relay :1334 1 \u0026#183; fetch relay addr (TLS) 2 \u0026#183; persistent TCP/1334 one socket \u0026#183; many channels customers' traffic SOCKS5 \u0026#183; HTTP CONNECT The bot never talks to just one server: a config domain hands out the relay, and a single persistent socket to :1334 carries every customer. That long-lived 1334 tunnel was the exact thing I\u0026rsquo;d seen on the phone.\nSeven bytes and four letters The traffic on 1334 isn\u0026rsquo;t HTTP or anything off-the-shelf. It\u0026rsquo;s a custom binary multiplexing protocol — one connection carrying many logical channels at once. Frames start with a 7-byte header, and there are four message types, each a single ASCII letter:\nByte Letter Direction Meaning 0x54 T bot → C2 Authentication 0x4E N C2 → bot Open new channel 0x57 W bidirectional Data transfer 0x43 C bidirectional Close channel Multiplexed so one infected phone can service many customers\u0026rsquo; traffic at once without opening a fresh socket per job. Efficient. Someone built this on purpose.\nA password that never expires The authentication is dead simple. It\u0026rsquo;s a single 19-byte T frame: the type byte, a 16-byte v4 UUID that was minted once when the malware installed itself, and two flag bytes. That\u0026rsquo;s the whole credential. The UUID is the account.\nHere\u0026rsquo;s the part that mattered later: the server never invalidates a token. I tried a UUID from a months-old sample, fully expecting it to be long dead, and it authenticated fine. No rotation, no expiry, no revocation. Once a device is enrolled, its identity is good more or less forever.\nKnocking on a live relay After auth, the C2 runs keepalive rounds. It opens a channel and sends a T frame carrying ping; the bot answers Tpong within a second or two; then it starts routing real customer traffic through W frames. The bot itself is protocol-agnostic — it just relays raw bytes and doesn\u0026rsquo;t know or care what they mean. I watched three customer shapes come through: a full SOCKS5 handshake, an HTTP CONNECT, and a raw pre-negotiated TLS tunnel.\nSo I tried it myself. Using the UUID straight from the capture, I authenticated to a live relay. Token accepted. Ping, pong, right on schedule. And then the relay started handing me real customer traffic — exit-IP verification checks, and fresh SOCKS5 handshakes originating from other customers of the service. That was the moment it stopped being a dead sample on my desk. The botnet was operational, right now, with paying users on it — and for those few minutes, I was briefly, uncomfortably, a node in it.\nSo who\u0026rsquo;s actually renting these homes? Residential proxies have plenty of dull, legitimate uses — ad verification, price scraping, that world. So I put mitmproxy in front of the exit and looked at what customers were actually pushing through.\nIt was not ad verification.\nThe dominant pattern, by a wide margin, was IMAP credential brute-forcing. Waves of simultaneous IMAPS connections on port 993 — hammering Microsoft 365 edge servers, Google\u0026rsquo;s mail infrastructure, and a spread of third-party mail providers. Plaintext IMAP on 143 too. And a side of telnet on 23, the old IoT-scanning knock. This is large-scale credential stuffing, laundered through home IPs specifically to dodge the per-IP rate limits and blocklists that would instantly stop it coming from a datacenter. The \u0026ldquo;residential proxy service\u0026rdquo; framing is the cover story. The product is really rate-limit evasion for people attacking mailboxes at scale.\nThree countries, no single abuse desk The infrastructure was deliberately smeared across jurisdictions. The storefront — sx.org — plus a linked seller portal sat behind Cloudflare. The relays lived on a single European hosting ASN in the Netherlands. The backend and the private git host, maxgit.net, resolved to a Russian provider. Sales in one place, muscle in another, brains in a third. None of that is an accident. Spreading it across three countries is the whole point. No single country\u0026rsquo;s abuse desk ever sees the whole shape of the thing. Send a complaint to the Dutch host and you\u0026rsquo;ve reported some relays and nothing else.\nOne more wrinkle worth flagging: the same relays and the same device pool were sold under more than one brand. sx.org was just the door I happened to walk in through. The sales front that showed up later in the takedown news was a different shopfront entirely — same machinery underneath, different sign over the entrance.\n417,000 or 17 million The storefront dashboard I could see advertised roughly 417,000 proxies online at any given moment. That was my keyhole number — one snapshot, through one shop\u0026rsquo;s window, my own rough estimate.\nWhen the operation was eventually taken down, the figure in the Dutch press (NCSC-NL , security.nl , Tweakers ) was about 17 million devices over its lifetime. Those aren\u0026rsquo;t contradictory numbers. One is how many exits are live this second; the other is how many phones and gadgets rotated through the pool across its whole run. Same thing, two scales. Which is when it landed on me that \u0026ldquo;there aren\u0026rsquo;t that many homes\u0026rdquo; was exactly the wrong instinct. There were plenty. They just hadn\u0026rsquo;t been asked.\nThe report, and the coin None of this is useful sitting in my notes. So I wrote it up properly — a full technical report: the protocol spec, the infrastructure map, indicators of compromise, and ready-to-deploy Suricata signatures. It went in for the attention of Europol\u0026rsquo;s EC3, national CERTs, and the relevant hosting abuse teams.\nThe nice thing about a lazy protocol is that it\u0026rsquo;s a loud protocol. The auth frame and the Tping/Tpong keepalive are distinctive enough to alert on directly, and the auth frame especially — it\u0026rsquo;s the very first thing a bot says on 1334:\n# Bot authentication frame, first packet on TCP/1334 alert tcp $HOME_NET any -\u0026gt; $EXTERNAL_NET 1334 ( msg:\u0026#34;Residential Proxy Botnet - Auth Frame\u0026#34;; flow:established,to_server; content:\u0026#34;|54|\u0026#34;; offset:0; depth:1; content:\u0026#34;|04 01|\u0026#34;; offset:17; depth:2; sid:1000001; rev:1; ) T at offset 0, then those two flag bytes sitting right after the UUID. If your network is speaking that, you don\u0026rsquo;t have a maybe. You have a sleeper.\nThen I waited, because that\u0026rsquo;s the only thing you can do.\nMonths later, the operation went down. The relays stopped answering. The Dutch press wrote it up with that ~17 million figure. And NCSC-NL, the Dutch national CERT, sent me a challenge coin as a thank-you for the disclosure.\nA challenge coin from NCSC-NL, the Dutch national CERT - their thanks for the disclosure.\nI want to be honest about what that coin does and doesn\u0026rsquo;t mean. I did not take this thing down. Takedowns at this scale are the work of law enforcement and CERTs across multiple countries, coordinated over months, and the overwhelming majority of that effort is invisible to some guy analyzing a phone at his kitchen table. Lots of people saw this operation from lots of angles. What I can truthfully say is small and specific: my report went in, and the coin came back. I\u0026rsquo;ll take a challenge coin over credit any day.\nOh — and the phone. It\u0026rsquo;s fine now. We wiped it. It runs cool, sleeps through the night, and no longer answers to strangers.\nTwo things I still don\u0026rsquo;t know I\u0026rsquo;ll end on the loose threads, because this is the honest state of it.\nFirst, a real puzzle I haven\u0026rsquo;t cracked. A surprisingly large share of these residential exits present a TCP TTL consistent with Linux — an initial 64 — even in regions I\u0026rsquo;d expect to be wall-to-wall iOS and Windows, which don\u0026rsquo;t look like that. Something is normalizing the stack. Maybe a transparent relay in the path, maybe a fleet of rooted devices, maybe NAT doing something I haven\u0026rsquo;t pinned down. I don\u0026rsquo;t have the answer yet. It\u0026rsquo;s the next post.\nSecond, a smaller, nastier point I keep coming back to. A residential proxy hides your IP address. Fine. But the shape of your traffic — the timing, the sizes, the rhythm — survives being encrypted and pushed out onto the air, and that shape is its own kind of fingerprint. That one gets its own post .\nI was just bored on a forum. The math didn\u0026rsquo;t add up, and it turned out there were about 17 million reasons why. The IP was never the part worth hiding.\n","permalink":"https://vitalii-zaiats.com/posts/residential-proxy-botnet/","summary":"I got bored on a forum doing arithmetic on \u0026lsquo;millions of residential IPs\u0026rsquo;, ended up reverse-engineering a botnet\u0026rsquo;s C2 from one infected phone, and reported it. The whole messy thread — and where it led.","title":"17 million devices for rent: tracing a residential-proxy botnet from one cracked game"},{"content":"This one is pure curiosity — a weekend project with no product behind it. I wanted to see, with my own hands, whether a claim I keep repeating in my day job is actually true: that encryption hides content, not shape, and that a flow\u0026rsquo;s byte sizes and timing survive TLS, WPA2, NAT, and a proxy hop all the way onto the air.\nIt\u0026rsquo;s a fun thing to say. It\u0026rsquo;s a different thing to sit at a bench, drive a pattern through a doubly-encrypted link, and watch it come out the other side in radio frames you can\u0026rsquo;t decrypt. So I did — and then spent most of the weekend not on the physics, which worked on the first honest try, but on the far more stubborn problem of a monitor radio that refused to show me half the frames. Everything below is on my own devices, my own network, my own accounts.\nThe thesis, concretely Take a connection that\u0026rsquo;s encrypted twice: a TLS tunnel inside an 802.11 link secured with WPA2. Two properties leak straight through both layers:\nBurst size. CCMP (WPA2\u0026rsquo;s cipher) is a stream mode — ciphertext length equals plaintext length plus a fixed header and MIC. A 100-byte write is a bigger frame than a 20-byte write, encrypted or not. TLS adds a constant record overhead; a proxy relays byte-for-byte. Timing. The cadence of when bursts leave the far end survives the whole path. Latency adds jitter; it doesn\u0026rsquo;t erase rhythm. So if a server emits a recognizable rhythm of sizes and gaps, that rhythm should reappear in the WiFi frames of whatever device pulls the flow — no key, no decryption, just a passive sniffer.\nwhat the server sends what the radio leaks sizes \u0026#183; timing same shape TLS + WPA2 + proxy content encrypted, unreadable The content is encrypted twice over; the envelope — how many bytes, and when — is not. I encoded a Morse-like code into a plain TCP flow: a long burst for 1, a short one for 0, one fixed time unit apart, with a five-mark preamble as a sync marker. The bytes are filler. Only the envelope matters.\npreamble payload = 1 1 1 0 1 0 0 1 0 1 1 0 1 0 █ █ █ █ █ · █ █ █ ▂ █ ▂ ▂ █ ▂ █ █ ▂ █ ▂ └ 5 long ┘ █ long burst = 1 ▂ short burst = 0 What held up (r = 0.7–0.9) The core result was unambiguous. Driving the code 11101001011010 through the target and correlating the air capture against it:\nTrigger on the target r (correlation) ON/OFF contrast ICMP ping, paced 0.89 ×42 UDP echo 0.89 ×72 TCP SYN → RST (closed port) 0.92 ×19 The shape provably survives onto the air. The cleanest trigger turned out to be TCP SYN → RST: a closed port makes the target\u0026rsquo;s own kernel emit a reset per SYN, so the rhythm rides its uplink with nothing installed and no open port. (One trigger died: UDP → ICMP port-unreachable, because the OS rate-limits ICMP errors down into the noise. Worth knowing which primitives the kernel throttles — that one dropped to r ≈ −0.1, pure noise.)\nOne real capture. Orange (top) is the code I pushed down the wire; blue (bottom) is what the sniffer heard over the air. The highlighted ON-bits line up with the bursts — the shape crossed the encryption. r = 0.70, ON/OFF contrast 17×.\nThe monitor was the hard part Nobody warns you that the physics is the easy part. Most of the weekend went to fighting my own radio, which kept dropping the frames I needed.\nI sniffed with a MediaTek MT7612U — a common, cheap USB monitor adapter, WiFi 5. It could not demodulate the high-rate downstream data frames. Not \u0026ldquo;sometimes.\u0026rdquo; In a three-second capture under a full flood it decoded on the order of sixteen data frames — from every station on the channel combined, not just my target. What it caught by the thousand were the low-rate control frames: block-acks, CTS, the ACKs the AP fires back at low, robust rates.\nI did the obvious things. Reloaded the driver. Forced HT20, then HT40. Checked for stray monitor flags. None of it moved the data-frame count off the floor. This is a known failure mode — a monitor stuck effectively in \u0026ldquo;no-HT\u0026rdquo; mode silently drops every HT/VHT data frame while happily logging the legacy control traffic — and on a bleeding-edge kernel the driver simply would not come out of it.\nSo I did the only thing left: I correlated on what I could see. The control frames the AP sends back toward the target are triggered almost 1:1 by the target\u0026rsquo;s own activity — every uplink burst earns a block-ack, every transmit opportunity a CTS. Ride those and you\u0026rsquo;re reading the acknowledgements of a flow you can\u0026rsquo;t decode at all. It worked (that\u0026rsquo;s the r=0.70 figure above), but with a caveat I want to be honest about: those frames are transmitted by the AP, so their signal strength points at the router, not the target. The rhythm is the target\u0026rsquo;s; the RSSI is the AP\u0026rsquo;s.\n(And a smaller, dumber problem, because a lab notebook should include the dumb ones: my first pass at counting frames was pure fiction, because I let awk split on whitespace and control frames have no transmitter-address field — the empty column collapsed and every row shifted one to the left. Half an hour of confidently wrong numbers before I switched to splitting on tabs. The measurement lies in more ways than one.)\nThere\u0026rsquo;s a deeper asymmetry underneath all this, and it\u0026rsquo;s the genuinely interesting finding. A sniffer sitting next to the target hears the target\u0026rsquo;s uplink almost perfectly — single stream, robust low rate. But the downlink from the AP is rate-adapted and beamformed toward the target\u0026rsquo;s antennas, at a high MCS. A monitor a few centimetres away, at different antennas and different multipath, can\u0026rsquo;t separate it — so it drops the very frames carrying the content. You are trying to decode a signal that was physically shaped for someone else\u0026rsquo;s antennas. The better the victim\u0026rsquo;s link, the more the eavesdropper loses. And this holds no matter what gear I use. It\u0026rsquo;s baked into how the channel works.\nWhich brings me to the one experiment I most wanted to run and couldn\u0026rsquo;t. The cleanest, most hands-off version of this attack is a plain one-way UDP flood at the target — you don\u0026rsquo;t even need it to answer. And the pattern is radiated: a one-way flood arrives as downlink data frames addressed to the target\u0026rsquo;s MAC, in exactly the rhythm you sent. It\u0026rsquo;s right there in the air, attributable, no cooperation required. My radio just can\u0026rsquo;t demodulate it. The signal exists; the receiver doesn\u0026rsquo;t. So that\u0026rsquo;s where it stalls, and it stalls on hardware, not on the physics. My radio just can\u0026rsquo;t hear it yet.\nTeaching a network to recognize the rhythm Cross-correlation is fine at high SNR and brittle at low. So for fun — and because the whole point of a bench project is to overdo it — I trained a small siamese 1D-CNN with an InfoNCE contrastive loss and heavy noise augmentation. A learned correlator instead of a hand-rolled one: one encoder eats the wire code, the other eats the air envelope, and the network learns to pull matching pairs together and shove mismatched ones apart.\nThe trick to not fooling myself was to never let it see the real data during training. I built a generator that produces synthetic wire/air pairs from first principles — the OOK code, the control-frame bursts, timing jitter, dropped bits, background traffic, a clock offset between the two capture machines — and seeded its parameters from the summary statistics of the one real run. Seeded well enough that the synthetic reproduced the real capture at r=0.69 against the real 0.70, same ON/OFF contrast, same RSSI spread. Then I trained only on synthetic and tested against the real capture the network had never seen. (All of it on a spare box with a GPU; the model is tiny and the data is generated on the fly, so \u0026ldquo;training\u0026rdquo; is a couple of minutes.)\nRetrieval: given the wire code, find its air capture among 128 — top-1 96% at low noise, and it beat classical cross-correlation by 5–7 points as noise rose. The augmentation earned its keep exactly where the hand-rolled method falls apart. Sim2real: the real capture, never seen in training, retrieved at rank #1 of 301 in both directions — wire finds its air, air finds its wire. Detection, the one I actually care about: AUC for beacon-present vs beacon-absent was 0.999. At a threshold tuned to a 1% false-alarm rate it flagged real beacons 97.8% of the time. And a real empty channel — a capture with no beacon at all — scored well below the threshold and was correctly rejected. The correlator does not hallucinate a signal into silence. That last property is the whole ballgame, and it\u0026rsquo;s easy to prove to yourself visually. Here\u0026rsquo;s the detector across three back-to-back captures on one time axis: an empty channel, then beacon A, then a different code B.\nThree real captures, one time axis: nothing, then beacon A, then a different code B. The envelope is flat when there\u0026rsquo;s no beacon, and traces whichever code I drive — including telling two codes apart. This is the detector not hallucinating.\nSo the machinery was sound and noise-robust. Which, if you\u0026rsquo;ve done any of this, is exactly the state of mind that gets you into trouble.\nThe part where I fooled myself The moment your tool is good, you start pushing it at problems it isn\u0026rsquo;t good enough for — and it will happily tell you it succeeded.\nI went after the hard version: a much fainter, more indirect way to nudge the target\u0026rsquo;s airtime, and finding the device among a crowd of stations that all randomize their MACs. The signal got faint. To line things up I added a lag search — slide the code against each candidate station until the correlation peaks — and up popped a station at r ≈ 0.41 with a plausible delivery lag. It looked like a hit. I was drafting the paragraph.\nThen I ran the control, and it fell apart:\nRe-correlating the same air data against a deliberately wrong code produced the same ~0.42 for the top stations. A wrong code scoring as well as the right one means the correlation isn\u0026rsquo;t real. With the lag search constrained to physically sane values, the \u0026ldquo;winning\u0026rdquo; station became a device that wasn\u0026rsquo;t even part of the experiment, at a negative lag — airtime arriving before the code that supposedly caused it. Impossible. The ~0.41 was an artifact. A lag search over a short code in noisy, multi-station traffic will manufacture a ~0.4 peak out of nothing if you let it — it has enough degrees of freedom to find some alignment that looks convincing. Contrast that with the IP floods above: r = 0.7–0.9, contrast ×17–84, surviving any null control you throw at them, at zero lag, on the station you expected. The difference is signal-to-noise, and no amount of clever search makes bandwidth appear that isn\u0026rsquo;t there.\nSo I threw the faint-signal result away. My initial gut (\u0026ldquo;this\u0026rsquo;ll drown in noise\u0026rdquo;) was right, and I\u0026rsquo;d briefly talked myself out of it because the number looked good. The lesson is cheap to state and expensive to learn: never trust a lag-searched correlation without a wrong-code control. It\u0026rsquo;s now the first thing I run, not the last. The r=0.41 that turned out to be nothing taught me more than the clean r=0.9 did.\nThe honest ledger What this is, and what it isn\u0026rsquo;t:\nProved: shape (size + timing) survives TLS + WPA2 onto the air; a driven pattern reappears in a device\u0026rsquo;s frames at r = 0.7–0.9; a zero-install trigger (SYN→RST) works; the learned correlator generalizes sim2real and rejects empty channels. The measurement is fragile, and the chipset is why. The physics is robust; my receiver is not. A cheap WiFi-5 monitor sees the acknowledgements but not the content, and can\u0026rsquo;t touch the one-way downlink case at all. Every number here is bottlenecked by hardware I\u0026rsquo;m about to replace. A proximity primitive, not a map. Even where it works, this needs a passive radio in range — tens of metres. It locates a device you\u0026rsquo;re already near; it does not find anyone from a desk, and driving a beacon through a proxy lights up whichever exit you were routed to, not one you chose. The physics works; the targeting doesn\u0026rsquo;t. Scope: own devices, own network, own accounts, passive capture of frame size and timing only. Measuring the shape of your own traffic is one thing; anything that touches a device you don\u0026rsquo;t own is another, and not part of this. Where I stopped: a shopping list So this is where the weekend actually ended — not on a grand conclusion, but on a parts order. The physics is proven and the correlator works; the thing standing between me and the experiments I couldn\u0026rsquo;t run — the one-way downlink, decoding an ax phone, seeing content instead of just acknowledgements — is the receiver. So the next move is hardware, and I did the homework so I don\u0026rsquo;t buy the wrong thing twice.\nA WiFi-6 monitor, first. A MediaTek mt7921au adapter (the ALFA AWUS036AXML is the easy one to find) demodulates HT/VHT/HE data frames — the exact thing my MT7612U couldn\u0026rsquo;t. That\u0026rsquo;s what lets me finally see the one-way downlink → target frames directly, and target modern ax devices instead of being blind to them. This one upgrade unblocks the entire stuck half of the project. The caveat I\u0026rsquo;m carrying forward: verify it actually decodes data frames before trusting it — flood the target, count the data frames, make sure it\u0026rsquo;s thousands and not sixteen. I got burned once. An SDR, second, for the other half. For raw energy detection of the airtime pattern (no decode needed — just watch the channel\u0026rsquo;s power trace the code) and for transmitting, a HackRF covers 2.4 GHz and up. Worth stating the mistake I nearly made: a plain RTL-SDR does not work here — it tops out around 1.7 GHz and can\u0026rsquo;t even tune to WiFi\u0026rsquo;s 2.4 GHz. And if I later want to crack the beamformed two-stream downlink that defeats a single-antenna receiver, that\u0026rsquo;s a two-channel radio (BladeRF / USRP) — a different, more expensive weekend, and I\u0026rsquo;m not there yet. That\u0026rsquo;s the honest stopping point. So the physics is proven and the correlator\u0026rsquo;s trained and validated, false positive included. All that\u0026rsquo;s left is a receiver that can actually see the data frames, and that\u0026rsquo;s on order. The next post is what happens when that radio shows up.\nWhy a proxy person finds this interesting A residential proxy is an application-layer disguise: it changes the IP the server sees. It does nothing about the fact that the exit is a physical radio on a physical link, and that the flow\u0026rsquo;s shape crosses every layer of the disguise. It\u0026rsquo;s the same principle as the rest of my work — a TCP fingerprint that contradicts the browser\u0026rsquo;s claimed OS, a STUN allocation that leaks the real IP. You can spoof the one field you control. You leak at every layer you don\u0026rsquo;t.\nBut mostly I did this because it was a good puzzle — and the two things I\u0026rsquo;m taking away from it are that the shape really does survive, and that my own null control caught me before I published a number that wasn\u0026rsquo;t there. Those are the ones worth writing down.\n","permalink":"https://vitalii-zaiats.com/posts/wire-air-correlation/","summary":"A weekend bench experiment: drive a size/timing pattern through a doubly-encrypted, proxied flow and watch it reappear in a device\u0026rsquo;s WiFi frames. The physics held at r=0.9 on my own hardware — and then the real fight was with a cheap monitor adapter that decoded almost none of the frames I actually needed. A lab notebook that ends in a shopping list, false positive included.","title":"Wire-to-WiFi correlation"},{"content":"In the earlier teardown I pulled apart a class of SOCKS5 proxy that demands a password on TCP but relays UDP for anybody — no ASSOCIATE, no source check, no memory of who you are. Once I stopped staring at the reflection mechanics, the load-bearing detail was this: a relay that forwards a lone, unsolicited SOCKS5-wrapped datagram is stateless on the UDP path. And stateless forwarders compose. Which raises an obvious, slightly gremlin-brained follow-up — what happens if you nest the headers and point these things at each other?\nSo I spent a session finding out. Chaining relays end to end, looping a single relay through itself, hunting for where the loop leaks, and finally paying real money for a \u0026ldquo;proper\u0026rdquo; commercial proxy to check whether the by-the-book RFC build is any safer. Usual disclaimer up front: I publish signals, not weapons. Anything volumetric below was rebuilt and measured inside the same isolated Docker network from the first post — a /28 with no uplink, nothing leaves the bridge, not one third-party box touched. Every figure is measured and then deliberately deflated, because the punchline hasn\u0026rsquo;t moved: these relays are worse at the frightening thing than the napkin math promises, and better as detection targets.\nNesting is free The SOCKS5 UDP request header (RFC 1928 §7) is tiny and self-describing: 2 reserved + 1 fragment + 1 address-type + 4 (IPv4) + 2 port = 10 bytes, then the payload. A relay reads exactly one header, forwards the rest to whatever address it names, and forgets the whole transaction. So to route through N relays you don\u0026rsquo;t need a protocol — you just stack N headers, innermost-last, and let the relays unwrap themselves:\nsent to P1 : [hdr→P2][hdr→P3]…[hdr→P_N][hdr→target][DATA] P1 → P2 : [hdr→P3]…[hdr→P_N][hdr→target][DATA] … P_N → target: [DATA] Each hop strips its ten bytes and forwards a smaller packet. There\u0026rsquo;s nothing clever going on; the relays do all the labour, peeling their own layer like an onion. Standing several of these stateless builds up in the lab and chaining them, the forward path was almost boringly reliable — runs of 15–20 hops delivered 100% of 500 datagrams. I\u0026rsquo;d already been burned once by a hairpin artifact in the first teardown, so this time I checked the exit IP actually matched the intended last hop before believing anything. It did. The chain was honoring the path, not quietly collapsing to a gateway.\nThe amplification that isn\u0026rsquo;t The intuition most people have here is wrong: \u0026ldquo;I send a 1-byte payload wrapped in 100 bytes of headers — that\u0026rsquo;s ×100 amplification!\u0026rdquo; It is exactly backwards. The headers get peeled, not multiplied. You transmit the fattest packet (101 B); the target receives the runt (1 B). At the destination it\u0026rsquo;s de-amplification — you spend ~100× more than you deliver.\n1 \u0026#183; you send to P1\u0026#8594;P2\u0026#8594;P3\u0026#8594;tgtDATA2 \u0026#183; P1 peels \u0026#8594; P2\u0026#8594;P3\u0026#8594;tgtDATA3 \u0026#183; P2 peels \u0026#8594; P3\u0026#8594;tgtDATA4 \u0026#183; target gets DATADATA Nesting headers doesn't multiply your traffic — it divides it. You send the fat packet; the target gets the runt. The only thing that actually grows is total aggregate work across the chain, and only modestly. Sum the shrinking packets and you get ≈ 5N² bytes of relay traffic for your ≈ 10N-byte input — an amplification of N/2 — but that traffic lands on the relays, spread out, not on any victim:\nchain depth N relay-side traffic amplification (N/2) 50 ~12.5 KB ×25 100 ~50 KB ×50 147 (MTU cap) ~108 KB ×73 ← ceiling And it\u0026rsquo;s capped. 147 ten-byte headers exactly fill one 1472-byte UDP payload, so a single un-fragmented packet tops out around ×73 no matter how you distribute the hops. What chaining buys you is source obfuscation — your traffic laundered behind a line of residential-looking IPs — not bytes on a target. That distinction matters, because the literature keeps filing UDP relays under \u0026ldquo;amplifier.\u0026rdquo; As a reflector aimed at a victim they\u0026rsquo;re bad. As a way to hide where a packet came from, they\u0026rsquo;re excellent. Two very different things.\nOne relay is a whole chain: the self-loop This is where it got fun. Needing N genuinely independent relays that all chain is fragile — pools rot, hosts drop. But a stateless forwarder will cheerfully forward to its own address, so you can build arbitrary depth out of a single relay by pointing every header back at it:\n[hdr→P][hdr→P][hdr→P]…[hdr→target][DATA] sent to P P receives, peels, forwards to P, receives, peels, forwards to P… all the way down until the innermost header finally releases the payload to the target. In the isolated lab I watched a single high-port relay build self-loop 140 times at 100% delivery, no anti-loop guard anywhere in sight. Depth had decoupled completely from relay count. A single box, one packet, and it ran 140 hops deep.\nNow — this is a genuine amplifier, and it\u0026rsquo;s the first honest one I\u0026rsquo;ve found in this whole saga. But read the fine print on who it hits. The self-loop is the same N/2 ≈ ×70 at N=140, and every multiplied byte is the relay flooding itself. One small packet makes the box perform 140 sequential forwards; in my run that was 500 × 140 = 70,000 self-forwards spat out from a 0.03 s burst. The victim of this amplifier is the relay and its provider\u0026rsquo;s bandwidth, full stop — not some external target. It\u0026rsquo;s a self-DoS / resource-exhaustion primitive, quadratic in depth, MTU-capped. Which is another way of saying: a relay that self-loops with no hop limit is a clean, catalogable vulnerability. Basically it announces itself.\nWhy \u0026ldquo;lossless\u0026rdquo; localhost still drops Then I pointed the loop headers at 127.0.0.1 instead of the relay\u0026rsquo;s public IP — and delivery cratered to ~28%. My first reaction was the obviously-wrong one: how does localhost drop packets? It doesn\u0026rsquo;t. The loopback wire is lossless; that\u0026rsquo;s the entire point of it. What\u0026rsquo;s actually happening is the relay drowning its own UDP receive buffer. A 140-deep self-loop at 500 packets rams ~70,000 datagrams into the relay\u0026rsquo;s single listener faster than its SOCKS handler can drain them; the socket buffer overflows and the kernel throws the rest on the floor. Textbook receiver overflow — except the relay is the one inflicting it, on itself. The quadratic self-traffic bites its own buffer, and deeper loops drop harder. The 28% isn\u0026rsquo;t wire loss at all. It\u0026rsquo;s the relay\u0026rsquo;s own self-DoS overflowing its buffer. I spent a good while confused by that number before it clicked that the drop was the finding.\nThe loopback leak is an SSRF primitive That 127.0.0.1 experiment buried a much bigger result under all the buffer noise: the relay forwarded UDP to its own loopback at all. Which is worth pausing on: Some of these relays will happily relay a datagram to 127.0.0.1, into RFC 1918 space, or at a cloud metadata address — reaching services that are only ever supposed to be reachable from inside the host. A stateless UDP relay with no destination egress filter is a UDP SSRF: point the innermost header at an internal resolver, an SNMP daemon, a 169.254.169.254 metadata endpoint, and the relay obligingly delivers on your behalf.\nThe contrast between builds is itself the signal. The :1080 pool sitting on Alibaba and Huawei Cloud tended to block loopback and internal destinations — basic SSRF hygiene — while still chaining freely out to other public relays. The high-port relays self-looped into internal space without a second thought. Two populations, two postures, and \u0026ldquo;will it forward to 169.254.169.254?\u0026rdquo; is a single-packet question that sorts one from the other.\nThe paid proxy fails the source check A correct SOCKS5 proxy treats the TCP control connection as the root of trust: you authenticate, you issue UDP ASSOCIATE, and the relay you get back is pinned to your source — RFC 1928 §6 is explicit that the relay must drop datagrams that didn\u0026rsquo;t come from the associating client. Because TCP can\u0026rsquo;t be spoofed (the handshake needs return packets), a properly source-pinned relay is simply not reflectable. That pinning is supposed to be the entire defense.\nSo I bought one. A paid commercial residential proxy — real USER:PASS, real TCP ASSOCIATE, three distinct IPs for front, relay, and exit, the works — specifically to test whether it actually enforces §6. The probe is minimal and I\u0026rsquo;m the paying customer, so nobody else is involved: open the association over TCP, then inject a single datagram into the relay endpoint from a different source (an open relay\u0026rsquo;s exit IP), and watch a STUN oracle for a reply.\nassociate: OK -\u0026gt; relay endpoint \u0026lt;proxy\u0026gt;:5114 inject via open relay (foreign source IP) -\u0026gt; proxy relay port reply: 2 SOCKS5 headers peeled, STUN reflexive = proxy egress RESULT: the proxy accepted UDP from a non-client source — §6 NOT enforced It reflected. The paid, authenticated, association-based proxy honored a datagram that never came from the client who paid for the association. The TCP gate authorizes setup — and then, the moment the relay port is open, this build stops caring who fills it. That collapses the one structural reason an ASSOCIATE proxy was ever supposed to be safe. And it means reflectability isn\u0026rsquo;t a property of \u0026ldquo;open vs. paid\u0026rdquo; at all. It\u0026rsquo;s a property of whether this specific build re-validates source on every datagram. Most, it turns out, don\u0026rsquo;t.\nWhat all of this is actually for None of it is a flood. The honest summary:\nChaining launders origin; it de-amplifies at the target. Self-looping amplifies N/2, capped ~×73, aimed squarely at the relay itself. Loopback forwarding is an SSRF reach into the host\u0026rsquo;s internal services. Missing §6 source-pinning turns even a paid proxy into a reflector. Every one of those is weak as an attack and sharp as a fingerprint. That\u0026rsquo;s the whole value for me: each behavior is a single-packet, deterministic probe that scores a node without abusing anyone.\nProbe Question Signal Self-reference forward Forwards to its own IP? loop limit? self-DoS-able Loopback / RFC1918 dst Forwards to 127.0.0.1/internal? UDP SSRF Foreign-source inject Accepts UDP from a non-client source? reflectable (§6 fail) MTU sweep Where does egress drop? DF/PMTUD fingerprint Self-loop depth How deep before buffer overflow? capacity / build fingerprint A relay\u0026rsquo;s vulnerabilities are its identity. The box that self-loops to 140, forwards to its own loopback, and skips the source check isn\u0026rsquo;t just exploitable, it\u0026rsquo;s easy to tell apart from a well-built proxy next to it. And that\u0026rsquo;s what lets you separate a residential-proxy exit from an actual residential user.\nFor operators If you run a SOCKS5 UDP relay and want off this list, the fixes are unglamorous and they all point the same way: re-validate the datagram source against the association on every packet (RFC 1928 §6 — not just once at setup); drop loopback, RFC 1918, link-local and metadata destinations at the relay; refuse to forward to your own ingress address, and cap forward depth; and don\u0026rsquo;t bind the UDP relay somewhere an unauthenticated datagram can reach it without a live, source-matched association. The reflection, the SSRF, and the self-loop all close the instant the relay stops trusting a bare datagram.\nThat\u0026rsquo;s the through-line across both posts. I sell signals, not verdicts — and a proxy that flunks these probes is about the loudest signal there is.\n","permalink":"https://vitalii-zaiats.com/posts/chaining-socks5-udp-relays/","summary":"Final follow-up post debunking the \u0026ldquo;open UDP relay = DDoS amplifier\u0026rdquo; fear: chaining launders origin but de-amplifies at the target, self-looping only DoSes the relay itself, loopback forwarding is a UDP SSRF, and even a paid commercial proxy fails the RFC 1928 §6 source check — every behavior reframed as a single-packet detection fingerprint.","title":"Chaining SOCKS5 UDP relays: self-loops, SSRF, and the amplification that isn't"},{"content":"Open-proxy measurement is supposed to be a solved problem. That\u0026rsquo;s the received wisdom, and honestly it\u0026rsquo;s the wisdom I started with: you take a list, you sweep :1080, you send a SOCKS5 greeting, and you write down which servers complete a no-authentication handshake. ZMap, Masscan, ProxyBroker, every academic survey I\u0026rsquo;ve read — they all hinge on that first TCP exchange. If the box asks you for a password, it\u0026rsquo;s closed. Next.\nI want to tell you about the blind spot in that gate, because it swallowed a whole population of proxies and I only noticed by accident.\nA SOCKS5 proxy can demand a password on TCP CONNECT and still relay UDP datagrams for absolutely anybody — no auth, no UDP ASSOCIATE, no check that the datagram even came from a client who logged in. To a handshake scanner that box looks locked. On the UDP side it\u0026rsquo;s a wide-open window around the side of the house.\nI found one of these by fluke, took it apart packet by packet over a weekend, and then went looking for siblings. There are a lot of them — not a number I\u0026rsquo;m going to quote, but a whole population — sitting on Alibaba and Huawei cloud, on the default SOCKS port, invisible to every scanner that asks the front door for a password first.\nThis is the teardown. Standing rule for this blog: I publish signals, not weapons. Everything reflective below was measured and then deliberately defanged, and the reality is almost backwards from the scary story: these relays are lousy at the attack you\u0026rsquo;d worry about, but easy to spot. The debunk is the interesting part, so keep reading.\nThe lie the password tells A correct SOCKS5 proxy treats the TCP control connection as the root of trust. You authenticate, you issue UDP ASSOCIATE, the proxy hands back a relay endpoint bound to your source, and that relay lives and dies with the TCP connection that spawned it. RFC 1928 spells all of this out; there\u0026rsquo;s nothing exotic about doing it right.\nThe broken build just… skips it on the UDP path. The password is real on TCP and theatre on UDP. I confirmed the bypass with the cheapest possible test — a single SOCKS5-wrapped UDP datagram fired straight at the proxy\u0026rsquo;s port, no TCP session, no auth, no ASSOCIATE, and see if the thing forwards:\nsending 30B wrapped STUN to \u0026lt;relay\u0026gt;:\u0026lt;port\u0026gt; with NO tcp / NO auth / NO associate REPLY in 28ms -\u0026gt; exit 178.x.x.x:36874 =\u0026gt; OPEN UDP RELAY (auth bypass) It forwarded. One shot, 28ms, and there\u0026rsquo;s my reflected STUN answer coming back through a proxy that would have slammed the door on the exact same client over TCP.\nclient SOCKS5 proxy :1080 target TCP CONNECT one UDP datagram no TCP \u0026#183; no auth \u0026#183; no ASSOCIATE \u0026#10007; password required \u0026#10003; forwards for anyone scanner checks TCP \u0026#8594; \u0026quot;closed\u0026quot; Same box, two doors. The scanner knocks on the locked one and writes the house down as empty. That asymmetry is the load-bearing fact for the entire post, so let me say why it matters and not just that it\u0026rsquo;s rude. TCP can\u0026rsquo;t be spoofed — the handshake needs return packets to land on the real source, so a relay that insists on ASSOCIATE over a live TCP session is simply not reflectable. A stateless one that forwards a lone, unsolicited UDP datagram is. The whole story lives in that difference.\nWhat the relay actually is, measured to death Before I generalized a single thing, I characterized one open relay exhaustively. Pure stdlib tooling, STUN as the truth oracle, no third party touched harder than a public STUN server answering a single query.\nIt egresses from its own host. The STUN-reflexive exit IP came back identical to the proxy front and identical to the UDP relay endpoint. One box does ingress, relay, and egress — there is no exit isolation at all. Contrast that with a paid commercial residential proxy I tested for a baseline, where I measured three distinct IPs: the SOCKS front, the relay, and a bound exit that honored the customer\u0026rsquo;s IP selection. The broken build is one machine wearing one hat.\nIts mapping is endpoint-independent — a cone. Inside a single association I probed five STUN servers across five distinct networks (Google, Cloudflare, Nextcloud, sipgate, metered) and got back the same egress port every time. Different concurrent associations each draw their own random egress port, and there\u0026rsquo;s no cross-association prediction to be had. Textbook cone NAT behavior, which matters for how you\u0026rsquo;d reason about the reflection surface.\nIts egress filtering is restricted, not full-cone — and this one embarrassed me. My first pass read as full-cone, and I got briefly excited about it, and I was wrong. What I\u0026rsquo;d actually caught was a hairpin artifact: a packet that reached the relay\u0026rsquo;s egress port from the relay\u0026rsquo;s own IP — an internal loopback short-circuit — not from the outside world. When I sent a genuinely external, unsolicited datagram at that egress port, it got dropped. So inbound to a relayed port is filtered down to peers the relay has already spoken to. The lesson I\u0026rsquo;m passing on so you don\u0026rsquo;t burn the afternoon I did: measure your reflection surface from the outside, never through a hairpin, or the box will happily lie to you about how open it is.\nIt cannot fragment outbound, and this is the most consequential limit of the whole thing. I pushed wrapped payloads of increasing size through the relay to a server I control:\nthrough relay: 1470 ok 1471 ok 1472 ok | 1473 ✗ 2000 ✗ ... 60000 ✗ direct control: 2000 ok 4000 ok 8000 ok (my server reassembles fine) That\u0026rsquo;s a razor cliff at payload 1472 — exactly one MTU (1472 + 8 UDP + 20 IP = 1500). The relay cheerfully reassembles fragmented inbound traffic, because the kernel does that before the relay ever reads the bytes. But the instant it has to emit a datagram larger than the link MTU, the packet is gone. Almost certainly DF is set on the egress socket — Linux default PMTUD — so sendto() returns EMSGSIZE and the thing is dropped without a sound. Anything over ~1472 bytes that needs to reach a victim simply vanishes. Keep that in mind — it matters later when I get to the debunk.\nIt drops empty payloads. A 0-byte inner payload is silently discarded; the minimum forwardable unit is one byte. Put the three quirks together — reassembles ingress, DF-drops anything past 1472 on egress, drops empty — and you\u0026rsquo;ve got a tidy behavioral fingerprint that needs no auth and no abuse to collect.\nOne sighting unrolls a whole block The single relay I\u0026rsquo;d been poking wasn\u0026rsquo;t a loner; it belonged to a block, and the block followed a rule I could read off the wire. Given one seed IP:port, the next host\u0026rsquo;s port is the previous port plus two:\n2.27.114.2 :46587 2.27.114.8 :46599 2.27.114.3 :46589 ... 2.27.114.4 :46591 2.27.114.14 :46611 \u0026lt;- block ends 2.27.114.5 :46593 2.27.114.15 : (dead) It\u0026rsquo;s just arithmetic: port = 46583 + 2 × last_octet, holding across a contiguous run that fills the usable addresses of a /28 (.2–.14, skipping network, gateway, and broadcast). The +2 step is nothing clever — sparse allocation, one port per host. But the consequence is that a single open endpoint hands you its entire /28. From one sighting you reconstruct the block; a feather-light sweep maps the pool. File that under \u0026ldquo;why this is an enumeration story,\u0026rdquo; because it comes back around when we talk detection.\nSo can you actually weaponize it? Mostly, no. Here\u0026rsquo;s the part I care about most, and it\u0026rsquo;s where I take the scary framing apart with a ruler. The fear writes itself: \u0026ldquo;open UDP relay equals DDoS reflector.\u0026rdquo; The reality I measured is a 1:1 forwarder with a header tax and a one-packet ceiling. It launders traffic. It does not multiply it. Let me show my work.\nThe header tax. Every datagram the relay forwards carries a 10-byte SOCKS5 UDP header, and it\u0026rsquo;s paid on both legs. Ten bytes you have to send can only ever lower an amplification ratio — you can\u0026rsquo;t conjure gain out of overhead you\u0026rsquo;re adding yourself. I measured this against real DNS and real NTP:\namplifier request response raw factor via SOCKS5 relay DNS TXT 39 B 960 B 24.6× 19.8× NTP monlist 8 B 48,200 B (100 pkts) 6025× 2733× The smaller the request, the more brutal the tax — NTP\u0026rsquo;s 8-byte monlist request paying an extra 10 bytes nearly halves the effective factor. That\u0026rsquo;s the opposite of what a reflector is supposed to do for you.\nThe MTU ceiling makes it worse still. Because the relay can\u0026rsquo;t fragment on egress, the deliverable response is capped at roughly 1462 bytes — one MTU minus the SOCKS5, UDP, and IP headers. Picture a 4 KB DNSKEY answer, exactly the high-amplification record an attacker would reach for: it arrives at the relay, gets faithfully reassembled, and then gets dropped on the way out. Over the cliff you don\u0026rsquo;t get a truncated packet, you get nothing. So the relay quietly enforces a \u0026ldquo;stay single-packet\u0026rdquo; rule, capping any DNS reflection through it at somewhere around 30×, single packet, or zero. The relay is sabotaging the attack it\u0026rsquo;s supposedly enabling.\nAnd the relay is the bottleneck. Whatever amplified traffic exists has to leave through the relay\u0026rsquo;s own cloud uplink, at 1:1 packet rate. A real attacker just skips the relay and hits the amplifier directly — same bytes, no chokepoint. The only thing the relay actually buys anyone is anonymity, and even that\u0026rsquo;s a trade rather than a win, for reasons the fingerprint section is about to make embarrassing.\nBuilding the reflection where nobody can get hurt I wanted to watch the full reflection chain end to end without touching a single third party, so I built the whole thing inside an isolated Docker network — 10.99.0.0/24, no uplink, nothing leaves the bridge. Everything below happened on my laptop and reached exactly no one:\nattacker ──spoof src=victim──\u0026gt; badproxy ──\u0026gt; dnsmasq (fat TXT) ──\u0026gt; badproxy ──\u0026gt; victim 10.99.0.10 10.99.0.20 10.99.0.40 10.99.0.30 Four containers: an attacker that forges the victim\u0026rsquo;s source IP with scapy, the deliberately-broken stateless relay, a real dnsmasq serving a fat TXT record as the amplifier, and a victim that just prints whatever lands on it. Here\u0026rsquo;s a run:\ndnsmasq query[TXT] amp.lab from 10.99.0.20 \u0026lt;- sees the PROXY, not attacker, not victim badproxy got 25B from 10.99.0.30:7777 (src NOT checked) -\u0026gt; forwarding to 10.99.0.40:53 badproxy REFLECTED 499B reply -\u0026gt; 10.99.0.30:7777 (the spoofed source!) victim \u0026lt;- 509B from 10.99.0.20:1080 \u0026lt;- amplified answer lands on the victim Two things fell out of this, and they\u0026rsquo;re both visibility stories rather than firepower stories.\nThe relay launders the spoof away from the amplifier. Look at what dnsmasq logged: a query from 10.99.0.20 — the proxy — a completely legitimate-looking lookup. Not the spoofed victim, not the attacker. In direct reflection the resolver would see the spoofed victim\u0026rsquo;s address and its anti-spoofing machinery could kick in — DNS cookies, response-rate limiting, TCP fallback. With the relay in front, the resolver sees a clean, ordinary client and none of those defenses ever fire. The spoofing only ever exists on the attacker→relay hop — and on that hop the real defense, BCP38 egress filtering, still catches it. In my lab the spoof \u0026ldquo;works\u0026rdquo; only because an isolated bridge has no BCP38 to enforce. On the actual internet, that gate is what these relays keep running into.\nThe victim gets handed a confession. What actually lands on the victim is SOCKS5-framed UDP — the payload prefixed with the relay\u0026rsquo;s reconstructed source header:\n00 00 00 01 | 0a 63 00 28 | 00 35 | \u0026lt;real DNS response: amp.lab TXT ...\u0026gt; └RSV+FRAG┘ATYP└10.99.0.40─┘└ 53 ─┘ The DNS server\u0026rsquo;s own address, 10.99.0.40:53, is sitting right there in the header of every reflected packet. A defender parsing a single datagram can say \u0026ldquo;this is laundered DNS reflection, original source port 53,\u0026rdquo; even though the outer IP is just some proxy. So yes, the attacker\u0026rsquo;s gain is real — a residential-or-cloud-looking source instead of 8.8.8.8, which slips past IP-reputation checks and naive \u0026ldquo;drop source-port 53\u0026rdquo; filters. But it\u0026rsquo;s paid for with a brand-new, self-incriminating payload signature and an enumerable pool of source IPs to boot. Some anonymity.\nThe one mechanism that genuinely multiplies — loops Everything above is a poor multiplier of bytes. There is exactly one exception, and it\u0026rsquo;s the genuinely dangerous idea in this whole post, because it multiplies in time instead.\nHere\u0026rsquo;s the trick. A SOCKS5 UDP response header (RSV RSV FRAG ATYP ADDR PORT) is byte-for-byte a valid request header. They\u0026rsquo;re the same shape. So when one open relay wraps a reply and delivers it to another open relay\u0026rsquo;s listen port, the second relay doesn\u0026rsquo;t read it as \u0026ldquo;a reply\u0026rdquo; — it reads it as a fresh forward request and acts on it. Point two of these relays at each other and you get a perpetual ping-pong. This is the application-layer \u0026ldquo;Loop DoS\u0026rdquo; class (CISPA, 2024); what follows is the SOCKS5-UDP instance of it.\nSame isolated style as before — proxy A, proxy B, an echo server, and an attacker that fires one spoofed packet (src = B:1080) and then walks away:\nattacker fired ONE spoofed bootstrap: src=B:1080 -\u0026gt; A:1080 ...then exited [A] forward #1..#21: from B:1080 -\u0026gt; echo \u0026#39;hi\u0026#39; [B] forward #1..#20: from A:1080 -\u0026gt; echo \u0026#39;hi\u0026#39; [echo] hit #1..#41 proxy A forwards: 21 proxy B forwards: 20 echo hits: 41 One packet produced 41 echo hits and 41 inter-proxy datagrams, and — this is the unsettling bit — the loop kept running entirely after the attacker was gone. A\u0026rsquo;s forwards #4 through #21 all happened with nobody there driving them. It\u0026rsquo;s a clean listen-to-listen ping-pong (:1080↔:1080), and it only stopped because I had capped the echo server. Uncapped, it runs forever — until a single UDP packet somewhere happens to drop. The amplification here isn\u0026rsquo;t in bytes, it\u0026rsquo;s in persistence: one forged datagram turns into unbounded traffic that saturates both relays\u0026rsquo; uplinks and hammers the responder on every hop. Compose that with the enumerable /28 pool and, in principle, you could seed a whole field of independent loops, one packet apiece.\nThis is the genuinely scary bit — except the internet has mostly killed it off already, kind of by accident. And I mean genuinely starved, not \u0026ldquo;mitigated on paper\u0026rdquo;:\nThe loop needs a responder that replies to its own reply, fed back to it. Only \u0026ldquo;answer literally anything\u0026rdquo; services do that — echo, chargen, QOTD, daytime — and those have been off by default for decades. The fuel is essentially extinct. Modern protocols are silent on purpose. WireGuard answers only a cryptographically valid handshake (cookie-based anti-DoS) and drops everything else without a peep. QUIC and DTLS withhold any reply until they\u0026rsquo;ve verified you aren\u0026rsquo;t spoofing. Feed the relay\u0026rsquo;s forward into any of them and you get back nothing — no reflection, no loop. That silence is a stated design goal, and it turns out to be exactly the right defense. DNS and NTP don\u0026rsquo;t sustain it in this topology. Feed a resolver its own response packet and it just drops it on the floor; the loop dies inside a single hop. So here\u0026rsquo;s the honest verdict on the scariest mechanism I found: the loop genuinely multiplies, it\u0026rsquo;s trivially seeded, and it composes beautifully with the enumerable pool — and its real-world fuel is gone, and the protocols people actually run today refuse to play. It runs forever in my lab, but out on the real internet there\u0026rsquo;s nothing to feed it. Strip it away and the residual risk collapses right back to weak one-shot reflection, and the relays settle back into being what they always were: a laundering and detection-signal story, not a volumetric weapon.\nWhy every standard scanner reports these as secure Having understood one relay cold, I went and looked at a large working list of SOCKS endpoints, and a whole population turned out to be sitting on port 1080, in netblocks that were not the least bit ambiguous:\n47.79/80/81/83/236/237, 8.218/8.219 → Alibaba Cloud (Aliyun), including their international regions 121.36, 124.71 → Huawei Cloud; plus China Telecom / Unicom ranges Not residential — Chinese cloud ECS, default SOCKS port, the same sequential /28 layout everywhere, all pointing at one widely-deployed proxy build gone wrong.\nNow the part that made me think this was worth writing up. The vulnerability class is not a secret. There\u0026rsquo;s an IETF draft to deprecate SOCKS5 FRAG that flatly notes UDP ASSOCIATE is reachable \u0026ldquo;prior to, or during, the establishment of a fully authenticated session,\u0026rdquo; and it explicitly wants the proxy barred from being \u0026ldquo;an amplification vector.\u0026rdquo; TURN\u0026rsquo;s security literature describes the identical shape — auth on the control channel, no per-packet auth on the data path — and calls TURN servers \u0026ldquo;the modern equivalent of those misconfigured open proxies.\u0026rdquo; There are SOCKS auth-bypass advisories and malicious no-auth ASSOCIATE modules floating around already. None of this is new.\nBut look at how we measure open proxies. The big internet surveys, the ZMap and Masscan sweeps, the tooling everyone cites — they all gate on a no-authentication TCP handshake. And these proxies require auth on TCP. So every conventional scanner connects, gets asked for a password, dutifully marks the host \u0026ldquo;closed / auth-required,\u0026rdquo; and moves on. The open-UDP-relay behavior is only visible if you probe the UDP path regardless of TCP auth — send the wrapped datagram and watch for a forward, which is precisely the check nobody\u0026rsquo;s list makes.\nThe result is a population that\u0026rsquo;s systematically undercounted. It\u0026rsquo;s a known bug class hiding inside a measurement-methodology gap. Everyone\u0026rsquo;s looking at the locked front door and nobody\u0026rsquo;s checking the open UDP window around the side.\nWhat I\u0026rsquo;d actually do with this: detect, don\u0026rsquo;t touch I don\u0026rsquo;t sell verdicts, I sell signals, and this is a clean one. The relay\u0026rsquo;s behavior is a multi-part fingerprint you can collect without auth and without abusing anything:\nforwards a wrapped UDP datagram with no ASSOCIATE (the bypass itself) reassembles inbound fragments but DF-drops anything \u0026gt;1472 B on egress drops empty payloads, minimum forwardable unit one byte egress IP == ingress IP (single box, no exit isolation) /28-block, +2-per-host port allocation → enumerable from one sighting Any host matching that profile is infrastructure, not an eyeball. And a host that also shows up sourcing amplifier-shaped traffic is a high-confidence \u0026ldquo;abuse relay.\u0026rdquo; For a model consuming a feed, \u0026ldquo;this IP is a stateless open UDP relay on cloud\u0026rdquo; is exactly the sort of observable that should move a score — even though I\u0026rsquo;d never tell anyone to go fire packets at it.\nThe right end of this is detection and notification: catalogue the pool, feed the fingerprint into scoring, and quietly point the cloud abuse desks at the misconfiguration. Not use. These are other people\u0026rsquo;s broken machines, and the useful move for an operator on the receiving end is boring and effective — enforce ASSOCIATE and per-packet source binding on the UDP path so it inherits the same trust as the TCP session, and set BCP38 egress filtering upstream so a spoofed source never leaves the network in the first place. Fix either and the relay stops being reflectable.\nHonest limits I\u0026rsquo;d rather undersell this than get caught overclaiming, so:\nThe deep teardown is one box. The +2//28 pattern and the cloud attribution come from the list, but I have not exhaustively re-probed every one of them, and I\u0026rsquo;m not going to mass-fire UDP at third-party infrastructure to pad a number. The lab work stayed in the lab. \u0026ldquo;Chinese cloud\u0026rdquo; here means Chinese cloud providers — Alibaba, Huawei, including their overseas regions. It is a statement about where the boxes physically sit, not a claim about who operates each individual proxy. The amplification figures are measured against real resolvers and a real monlist mock; field factors will vary with the record and the path. Treat the table as a ceiling I actually observed, not a promise. The reflection only functions where source spoofing is possible at all. On the real internet BCP38 is the gate, and it holds far more often than it fails — which is the entire reason these relays are a laundering curiosity and not a volumetric weapon. The interesting thing was never \u0026ldquo;open relay can DDoS.\u0026rdquo; It can\u0026rsquo;t, much — I spent a good chunk of this post proving that with tables. The interesting thing is a few thousand misconfigured boxes that every standard scanner reports as secure, each one leaking an open UDP forwarder that\u0026rsquo;s trivially fingerprinted the moment you stop asking the TCP front door for a password. That gap — between what the handshake says and what the UDP path actually does — is the signal.\n","permalink":"https://vitalii-zaiats.com/posts/socks5-udp-open-relays/","summary":"Final version of the SOCKS5 open-UDP-relay teardown: de-quantified the scan-list phrasing, aligned a log comment to first-person voice, and thinned one forward-reference scaffold, with all tables, numbers, RFC references, and the \u0026ldquo;signals, not weapons\u0026rdquo; defensive framing preserved verbatim.","title":"Auth on TCP, open on UDP: the SOCKS5 relays scanners can't see"},{"content":"There\u0026rsquo;s one question about residential proxies I can never quite let go of: where do the exit nodes actually come from? The providers all say the same thing — \u0026ldquo;real users who opted in.\u0026rdquo; I\u0026rsquo;ve poked at that claim before, and every time I come away thinking the device is real enough, but I\u0026rsquo;ve never seen much proof that the users actually agreed to any of it.\nSo this time I tried working the supply chain from the inside out. Instead of asking who the exits are, I asked what they do — specifically, what a pool\u0026rsquo;s own IPs sit around downloading all day. That thread pulled all the way to a single cracked driving game.\nStart with what the pool downloads First I needed a pile of exit IPs. I harvested a batch from a mid-tier residential provider — rotating, country-targeted, nothing exotic — and looked each address up in public BitTorrent-attribution data. Just: has this IP been seen in a swarm, and for what?\nThe picture was not subtle. A big chunk of the exits had torrent history, and roughly one in five had pulled down an Android .apk at some point. That is wildly above whatever you\u0026rsquo;d expect from a random scoop of consumer IPs. And when I ranked the torrents by how many distinct pool IPs touched each one, the top of the list was basically wall-to-wall cracked game mods.\nNow, I want to be careful here, because this is the part that\u0026rsquo;s easy to oversell. This isn\u0026rsquo;t proof anyone\u0026rsquo;s infected. It\u0026rsquo;s a behavioral fingerprint of the crowd. The people who sideload pirated games are the same people who happily install a sketchy \u0026ldquo;free\u0026rdquo; app next to it — that correlation exists with or without malware. But it\u0026rsquo;s still a map. It tells you exactly which APKs are worth opening first, and that\u0026rsquo;s all I wanted out of it.\nGet a second opinion from the DHT Attribution data is one source, and one source makes me nervous. I wanted something independent that couldn\u0026rsquo;t be poisoned by the same upstream. So for the most pool-correlated titles I went and watched the swarms myself, live.\nThat meant a BitTorrent DHT get_peers crawl: bootstrap off the public routers, walk toward the infohash, and scoop up the peers who are downloading that exact file right now. No tracker, no torrent client, just raw KRPC/bencode over UDP. It\u0026rsquo;s a lovely little protocol to talk to by hand.\nThen the real question — I classified every swarm peer by ASN and geo and asked whether these were the same machines the provider was renting out to me. You almost never nail the identical IP; leases churn, and the pool cycles through hundreds of thousands of addresses, so exact matches are a coin toss at best. Instead I tested the granularities that actually mean something: same ISP/ASN, same /16, same /24. For each peer\u0026rsquo;s country I harvested fresh pool exits and compared.\nIt held up. Swarm peers and pool exits matched ISP-for-ISP, and in the dense markets they lined up right down to the same /24. Even better as a sanity check: a peer sitting on a datacenter/seedbox ASN was correctly missing from the pool, which checks out — the pool is supposed to be residential, and that\u0026rsquo;s a datacenter box, and it wasn\u0026rsquo;t there. Two independent signals, download attribution and live swarm membership, both pointing at the same consumer networks.\nThe other channel: an installer that just asks Not everything in the pool gets there through a hidden payload. The same attribution thread turned up a second recruitment channel that hides nothing at all — and it isn\u0026rsquo;t a game, it\u0026rsquo;s the torrent client itself.\nThe pool\u0026rsquo;s IPs kept leaving a distinctive footprint in public torrent-attribution data — the signature of one particular consumer torrent client, showing up across the exits again and again. That pointed straight at the client, so I installed it on a throwaway VM and watched what its setup actually did.\nThe installer offered to add two commercial bandwidth-sharing SDKs, bundled right into setup as opt-in add-ons. Not a trojan — you \u0026ldquo;agree,\u0026rdquo; somewhere in the flow — but the outcome is the same as any of this: your connection quietly becomes someone\u0026rsquo;s residential exit node, resold by the gigabyte. The consent is real in the way installer-bundle and EULA consent is always real, which is to say it\u0026rsquo;s buried in a flow nobody reads, sitting next to the \u0026ldquo;yes, next, next\u0026rdquo; you click to reach your download.\nThat\u0026rsquo;s the honest-looking end of the same business. There\u0026rsquo;s no pirated game or hidden library here — just a free tool that signs up its own users at install time and feeds them into the pool.\nGrab the whole game, five times over The single most pool-correlated title turned out to be one very popular driving game. So I did the dumb, thorough thing: I grabbed five different builds from five different places — an established APK mirror, a couple of \u0026ldquo;mod\u0026rdquo; sites, a content-locker page, and the legit packaged version — and diffed the lot against each other.\nFour of the five were boring, which is the best possible outcome. Normal engine, normal analytics, normal native libraries. The real game, repacked, nothing tucked away.\nThe fifth was not boring. It was the only build carrying a stack of extra native libraries none of the others had. Buried in there was a small .so with an entropy of 7.84 — that\u0026rsquo;s an encrypted blob, basically noise — sitting behind a custom loader and some anti-analysis libraries from a commercial Android packer family. That\u0026rsquo;s what gave it away. Nobody wraps a harmless little library in an encrypted, anti-debugging loader. You go to that trouble for exactly one reason. That payload was deliberately injected and deliberately hidden, by precisely one repacker in the set.\nThe nice thing is I didn\u0026rsquo;t have to guess which build was dirty. Four clean references told me, byte for byte, which libraries didn\u0026rsquo;t belong in the fifth. The diff did the accusing for me.\nWatch it run Static analysis walks straight into a wall the moment it hits an encrypted payload — there\u0026rsquo;s nothing left to read. So I put the booby-trapped build on a sandboxed device behind a transparent gateway and just watched what it said on the wire.\nWhat came out was ad fraud, and a lot of it:\nA whole swarm of gibberish throwaway domains — random word-salad names that rotate constantly to stay ahead of blocklists. The clustering is what gave them up. Four of them sat on the same /24 as a domain named, with no shame whatsoever, effectivecpmnetwork.com. So: the serving edges of one CPM network. And the offers were the top-of-market popunder vertical — adult-cam CPA, affiliate click-tracker and all. I also fingerprinted every TLS Client Hello with JA3, because that tends to reveal who\u0026rsquo;s really making the connections. It split into two clean stacks. One was the device\u0026rsquo;s own platform/Conscrypt stack — the OS and the well-behaved apps. The other was a Chromium/WebView stack that shattered into dozens of different fingerprints, per-connection extension randomization, which is the unmistakable signature of ad creatives rendering inside a WebView.\nThe honest ledger This is the point where I have to slow down, because it\u0026rsquo;s an easy story to overstate.\nWhat I\u0026rsquo;m confident about: the build is an ad-fraud / adult-CPA carrier. I sat there and watched it serve cam-site popunders through a recognizable CPM network. And it is, provably, the only one of five builds that injects an encrypted, packer-protected native payload the clean builds simply don\u0026rsquo;t have.\nWhat I\u0026rsquo;m not confident about: that the encrypted payload is proxyware. It\u0026rsquo;s the obvious guess — this whole thing started from a pool built out of devices that download exactly these mods, and the payload is concealed in precisely the way a residential-proxy SDK would be. But I haven\u0026rsquo;t decrypted it. During my whole capture window it just sat there, silent. A packed payload that never wakes up tells you nothing from the outside, no matter how long you sniff. Actually settling \u0026ldquo;proxy or not\u0026rdquo; means unpacking it at runtime, and that\u0026rsquo;s still on the bench.\nSo two things are true at once, and I\u0026rsquo;m not going to smear them together to make a better headline. The pirated game is a confirmed ad-fraud vehicle. And it carries a deliberately hidden native payload of unknown function, riding on the exact device profile that fills a residential proxy pool. Correlation isn\u0026rsquo;t a confession. It is, however, an extremely specific spot to keep digging.\npoolexitstorrentattributionDHTswarmdiff 5buildsrun \u0026amp;watch Five independent observables — the verdict only uses what each stage actually showed. The pipeline is what I actually care about here: pool exits, torrent attribution, DHT swarm confirmation, multi-sourcing the sample, static diff against clean builds, then runtime observation. Every stage is its own independent observable, and the conclusion only leans on what each stage actually showed me — no more.\nPassive enumeration and public attribution only; I report ISP/ASN/subnet aggregates, never individual subscriber addresses. All sample analysis is static and sandboxed. The legitimate, unmodified game is unaffected — the injected payload lives only in one specific cracked repack.\n","permalink":"https://vitalii-zaiats.com/posts/from-torrent-to-boobytrapped-game/","summary":"I chased a residential proxy pool\u0026rsquo;s own download habits down to one cracked driving game, and found a build that\u0026rsquo;s a confirmed ad-fraud carrier hiding an encrypted payload I still can\u0026rsquo;t prove is proxyware.","title":"From a proxy pool's torrents to a booby-trapped game"},{"content":"This spring I sent a coordinated disclosure to NCSC-NL, the Dutch national CERT. The gist: a handful of residential IP addresses, scattered across several of the big Dutch ISPs, were being sold as residential proxy exit nodes. Every one of them was a hacked CCTV camera. And I\u0026rsquo;d bet almost anything the people who own those cameras have no idea their driveway monitor is currently laundering someone else\u0026rsquo;s credential-stuffing traffic.\nThis is the public writeup, so let me get the disclaimer out of the way first: the actual IP addresses aren\u0026rsquo;t in here. They point at real, still-vulnerable boxes in real homes. Those went to the CERT and they stay with the CERT. What I can talk about is how you go looking for something like this, and why it exists in the first place.\nFrom \u0026ldquo;puppy cam\u0026rdquo; to \u0026ldquo;exit node\u0026rdquo; All the devices carried a TCP/IP fingerprint that lines up with the NETSurveillance/XMEye/Sofia family — those cheap ARM and MIPS CCTV DVR/NVR boxes that sell for about €50 under a dozen interchangeable brand names. You\u0026rsquo;ve seen them. Maybe you own one.\nThe trip from \u0026ldquo;camera on the porch\u0026rdquo; to \u0026ldquo;node in a botnet\u0026rdquo; is embarrassingly short:\nThe firmware ships with default credentials and a web interface sitting on port 80. Two well-known unauthenticated RCE bugs (CVE-2017-7921, CVE-2018-10088) have been getting popped in the wild since 2018. Somebody scans big IP ranges, exploits the box, and drops a small statically-linked ARM/MIPS Linux binary onto it. That binary quietly registers the device as a residential proxy exit node with one or more commercial proxy services. The camera now carries strangers\u0026rsquo; traffic on the side, alongside whatever it was already doing. home CCTV:80 \u0026#183; default credsunauth RCECVE-2017-7921proxy SDKdroppedexit nodefor rentthe owner sees nothing \u0026#8212; someone else\u0026#39;s abuse now rides their home IP From driveway camera to exit node in four steps — and the owner never sees a thing. And the owner sees nothing. The dashboard still loads. The app still shows the puppy. There is no blinking light for \u0026ldquo;your home connection is now the residential-looking front end of a credential-stuffing campaign.\u0026rdquo;\nFinding them without ever touching them I want to be really clear about this, because it\u0026rsquo;s the whole point: I never touched a single camera. Everything I did happened on my side of the wire.\nI run an active prober. It buys access to commercial residential proxy pools and pushes a tiny keyed identifier out through each provider\u0026rsquo;s network. On the other end, a raw-socket SYN sniffer catches the inbound TCP handshake coming back from whatever exit IP the pool handed me, and pulls the TCP/IP fingerprint out of it — TTL, MSS, window size, the ordering of the TCP options, the DF flag. No HTTP, no login, no poking at the device. Just watching how it says hello.\nAnd this particular hello is distinctive. The signature: option ordering MSS,NOP,NOP,TS,NOP,WS, with no SACK, WS≈28240, MSS=1412, an initial TTL of 64 landing 9–15 hops out, DF set. The missing SACK is the giveaway. Nothing modern negotiates a connection without it — that\u0026rsquo;s an old embedded Linux 2.6.x kernel talking, which is exactly what this class of camera runs. I lined the fingerprints up against public banner data, and the dashboard titles on the affected hosts confirmed the device family. No guessing required at that stage.\nThe Dutch slice was small enough that you could actually remediate it device by device, which is why it made a clean disclosure case — a tractable problem you can hand to a CERT and expect action on. But I don\u0026rsquo;t want to oversell that. The same fingerprint matches thousands of IPs across dozens of countries. The Netherlands was just the piece small enough to do something about. Taiwan, South Korea, and the US each turned up hundreds. That\u0026rsquo;s not a coordinated-disclosure email anymore; that\u0026rsquo;s an ISP-scale notification problem.\nWhy the person who owns the camera should actually care \u0026ldquo;Your IP shows up in abuse logs\u0026rdquo; is the boring version. The real reasons are worse:\nThat web interface is reachable by anyone routing through the proxy — the live feed, the recordings, the admin controls, all of it. Whatever the abuser sends through your line — and some of it is genuinely illegal — comes out on your IP, with a timestamp. When a subpoena follows that trail, it ends at your house, not theirs. The same RCE that installed the proxy SDK can install anything else. A DDoS bot that pins your uplink to the ceiling whenever its operator feels like it, for one. What I actually asked for The ask to NCSC-NL was deliberately modest: notify the affected ISPs so they can reach the customers, and get those customers to factory-reset, update firmware, and set credentials that aren\u0026rsquo;t the factory default. Standard stuff, and it runs through NCSC-NL\u0026rsquo;s normal process.\nI floated one bigger idea as a policy question, well above the pay grade of any single CERT: ISPs could consider blocking the known proxy-SDK signature ports outbound from residential lines, the same way they\u0026rsquo;ve blocked open SMTP relay for years. That one\u0026rsquo;s a conversation, not a request. The first two are the actual disclosure.\nIf you\u0026rsquo;re buying \u0026ldquo;residential\u0026rdquo; IPs, read this part This next part is the uncomfortable one. A meaningful chunk of the IPs you can pay to route through right now are compromised IoT devices — CCTV cameras, Android TV boxes, routers — owned by people who never agreed to any of it. Across the pools I fingerprinted, the IoT share of the \u0026ldquo;residential\u0026rdquo; supply was over a quarter.\nAnd the useful thing is that this device class is structurally different at the TCP layer. You can spot it before a single HTTP request goes out, before any JavaScript runs — just from how it opens a connection. So when a proxy vendor tells you their network is \u0026ldquo;ethically sourced,\u0026rdquo; there\u0026rsquo;s actually a way to check: you can measure the fingerprint spread yourself instead of taking the sales pitch at face value.\n","permalink":"https://vitalii-zaiats.com/posts/coordinated-disclosure-cctv-proxies/","summary":"How I passively fingerprinted hacked Dutch CCTV cameras being resold as residential proxy exit nodes — and reported them to NCSC-NL without ever touching a single box.","title":"Coordinated disclosure: CCTV cameras selling their owners' bandwidth"},{"content":"Open any residential proxy provider\u0026rsquo;s landing page and you\u0026rsquo;ll see the same thing: a photo of an iPhone, some copy about \u0026ldquo;real mobile IPs from real user devices.\u0026rdquo; So when I fingerprinted 1.93 million residential proxy exit nodes, that\u0026rsquo;s what I expected to find. Phones. Lots of iPhones.\nThere were almost none. And the reason turned out to be more interesting than \u0026ldquo;someone\u0026rsquo;s lying in the marketing.\u0026rdquo;\nThe number that didn\u0026rsquo;t add up In the US, iOS is somewhere around 57% of the mobile market. Roughly one in every two phones. If a proxy pool were genuinely built from opt-in users letting an app share their bandwidth, iPhones should be all over the data — you couldn\u0026rsquo;t avoid them.\nHere\u0026rsquo;s what I actually saw:\nCountry iOS market share Proxy-pool iOS Under-representation United States 57% 0.1% 570× United Kingdom 51% 2.0% 25× France 30% 0.2% 150× Brazil 15% 0.1% 150× Read that right-hand column again. In the US, iPhones show up in these pools 570 times less often than they show up in the real world. Globally, iOS is under 0.2% of residential exit nodes. That\u0026rsquo;s not a dip in the numbers. An entire platform is basically missing.\nMy first instinct was the flattering one: maybe iPhone owners are just more principled, less likely to install some sketchy \u0026ldquo;get paid for your unused bandwidth\u0026rdquo; app. That\u0026rsquo;s a nice story. It\u0026rsquo;s also wrong.\nThe wall Apple built by accident iPhones aren\u0026rsquo;t missing because their owners have better ethics. They\u0026rsquo;re missing because the operating system makes running a proxy exit node borderline impossible, and it does this without ever trying to. Three things stack up:\nBackground execution limits. iOS aggressively suspends any background process that keeps chewing CPU or network. A proxy exit node has to be the opposite of that — always on, always relaying traffic. iOS looks at that behavior and kills it within minutes. The whole business model requires the one thing the OS won\u0026rsquo;t allow. The sandbox. Every app lives in its own isolated box. Opening a listening socket that pulls third-party WAN traffic in and shoves it back out through your LAN — which is literally what an exit node does — is exactly the kind of thing the sandbox exists to prevent. App Store review. Even if you somehow engineered around the first two, a bundled bandwidth-sharing SDK trips the performance and privacy guidelines. Apps caught carrying one get pulled. So it\u0026rsquo;s not one lock. It\u0026rsquo;s three, and you\u0026rsquo;d have to pick all of them. Android, by contrast, is far more permissive — which is why the pool is full of it. That asymmetry is the entire chart above.\nThen what am I actually buying when I buy \u0026ldquo;mobile\u0026rdquo;? This is the part that reframed things for me. If a provider sells you \u0026ldquo;US Mobile\u0026rdquo; and the fingerprints come back 99% Linux and Android, 0.1% iOS — the word \u0026ldquo;mobile\u0026rdquo; barely means anything at that point. What you\u0026rsquo;re really getting is some blend of:\nCheap Android phones with the background restrictions turned off or the bootloader unlocked; Rooted devices where an SDK just walks past the OS restrictions it was told to respect; Compromised Android TV boxes — mobile-class chips sitting on fixed-line ISPs, which from a distance look like phones; Wholesale tunnels, where the traffic gets routed through a carrier via a plain Linux gateway and quietly borrows the \u0026ldquo;mobile\u0026rdquo; reputation on the way through. None of that is an iPhone in someone\u0026rsquo;s pocket. The iPhone in the ad is the one device that essentially can\u0026rsquo;t be there.\nThe ghost is the signal The empty column turns out to be useful, though, not just a letdown.\nBecause real iOS is so vanishingly rare in these pools, finding a genuine iOS TCP fingerprint on a residential IP becomes one of the more trustworthy \u0026ldquo;there is an actual human here\u0026rdquo; signals you can get. If a request says it\u0026rsquo;s an iPhone by user-agent and the TCP stack backs that up — the telltale IP ID = 0, Apple\u0026rsquo;s particular option layout — the odds that it\u0026rsquo;s a proxy are tiny. The rarity works in your favor.\nThe inverse is where it gets fun. An \u0026ldquo;iPhone\u0026rdquo; user-agent riding on a Linux or Android TCP fingerprint is a flare going up. The top layer is telling you one story and the layer underneath is telling you a different one, and the lower layer is the honest one. Somebody dressed up their traffic and forgot the kernel doesn\u0026rsquo;t play along.\nThat gap — what a connection claims to be versus what its stack actually is — is the thing you actually check. An attacker owns the user-agent string; they can type anything they want in there. What they don\u0026rsquo;t own is the kernel that shook hands with you. The kernel just answers straight — it has no idea the user-agent is lying for it.\n","permalink":"https://vitalii-zaiats.com/posts/the-ios-proxy-ghost/","summary":"Turns out the iPhone on every proxy provider\u0026rsquo;s landing page is the one device that basically can\u0026rsquo;t be in the pool — and that absence is exactly what makes a real iOS fingerprint a trustworthy \u0026ldquo;human here\u0026rdquo; signal.","title":"The iOS proxy ghost: why you can't find an iPhone in a residential pool"},{"content":"The pitch for residential proxies is always the same: your traffic goes out through \u0026ldquo;real user devices.\u0026rdquo; Real phones, real laptops, real people at home. That\u0026rsquo;s the whole selling point — the exit looks like a human because it supposedly is one.\nI wanted to check. Not the marketing, not the user-agent string the seller hands you — those are trivially dressed up. I wanted to look at a layer the seller doesn\u0026rsquo;t really control: the TCP handshake itself. When a machine opens a connection, its kernel stamps the SYN packet with a handful of tuning values — options, window scale, TTL, MSS. Those come from the OS, not from whatever the operator wants you to believe is running.\nSo I sat on the other end and listened. 319,706 connections through residential proxy exit nodes, SYN parameters captured off each one. Then I went looking for what the stack was quietly saying.\nAlmost none of them are phones or laptops Here\u0026rsquo;s the OS split:\nLinux/Android — 313,300 (98.0%) Windows — 5,604 (1.8%) BSD/iOS — 719 (0.2%) Unknown — 83 (0.03%) Think about what that mix would have to be. If this pool were actually a crowd of ordinary people\u0026rsquo;s devices, the mix would look roughly like the consumer market — call it a third Windows, a quarter Android, a quarter iOS, some macOS scattered in. That\u0026rsquo;s what \u0026ldquo;real users\u0026rdquo; looks like from the outside.\nBut that\u0026rsquo;s not what this is. It\u0026rsquo;s 98% Linux, which looks a lot more like a server room with some proxy-SDK Android phones bolted on than an actual neighborhood. The shape gives it away before you\u0026rsquo;ve looked at a single other field.\nWhen \u0026ldquo;diverse residential users\u0026rdquo; all share one kernel default I broke the SYNs down by full fingerprint to see how varied they really were. The single most common one covers 54.6% of every probe:\nOS: Linux/Android Options: MSS,SACK,TS,NOP,WS MSS: 1460 Window Scale: 12 TTL: 64 That\u0026rsquo;s a stock Linux kernel TCP stack. Nothing exotic — the defaults you\u0026rsquo;d get out of the box. The second most common fingerprint (15.8%) is byte-for-byte the same except Window Scale = 6, which is just an older kernel or a different sysctl knob.\nAdd those two together and you\u0026rsquo;re past 70% of the pool sharing one of two near-identical kernel defaults. \u0026ldquo;Diverse residential users\u0026rdquo; don\u0026rsquo;t do that. Real households run a mess of different OSes, versions, and hardware — the variety is the whole point of a residential pool. When more than half of them handshake in lockstep, they aren\u0026rsquo;t diverse and, frankly, they aren\u0026rsquo;t users.\nThe MSS quietly narrates the wiring The maximum segment size is my favorite field here, because it leaks the physical link sitting under the connection. The kernel sets MSS based on the path\u0026rsquo;s MTU, and different kinds of links carve off different amounts of overhead. So the number tells you what the exit is actually plugged into:\n1460 (73%) — plain Ethernet 1452 (8.8%) — PPPoE, i.e. DSL 1412 (6.9%) — VPN / tunnel overhead 1440 (3.1%) — some ISP-specific MTU The PPPoE slice is the honest part of the picture — 1452 is what real residential DSL looks like, and that\u0026rsquo;s genuinely someone\u0026rsquo;s home line.\nThe 1412 slice is the one that made me lean in. That extra overhead is the signature of a tunnel: the traffic isn\u0026rsquo;t arriving straight off a home link, it\u0026rsquo;s coming through something first. That lines up with wholesale gateways re-exporting traffic — one operator taking connections and pushing them back out, borrowing the residential reputation on the way through. The \u0026ldquo;home\u0026rdquo; IP is real enough; the hop before it isn\u0026rsquo;t a home at all.\nIt all funnels through a handful of networks Last thing I checked was where these exits actually live, by ASN. If this were a broad grassroots population of consenting people, you\u0026rsquo;d expect them smeared across thousands of small networks all over the place.\nInstead they pile up in a few large ISPs — mostly big US cable providers. The single top ASN by itself accounts for roughly 10% of all probed exits, with tens of thousands of unique IPs under it. A pool that clusters that hard isn\u0026rsquo;t a wide sweep of volunteers. It\u0026rsquo;s whichever networks the recruitment malware happened to find most fertile.\nThe pattern that keeps showing up Every layer told the same story the one before it did, and it\u0026rsquo;s the same lesson I keep running into: you can spoof the one field you control — the exit IP — and you leak at every layer you don\u0026rsquo;t. The seller can hand you a clean residential address and a convincing user-agent all day. But the kernel underneath still stamps the SYN, and that stamp gives away the real OS no matter what the seller claims. That\u0026rsquo;s a layer the operator doesn\u0026rsquo;t get to dress up, and when you actually read it, the \u0026ldquo;real user devices\u0026rdquo; turn out to be a rack of Linux boxes.\n","permalink":"https://vitalii-zaiats.com/posts/tcp-fingerprinting-residential-proxies/","summary":"Probed 319,706 residential-proxy exits by their TCP SYN fingerprints — 98% Linux, two kernel defaults covering 70%+, MSS leaking the real link, and a handful of big ISPs — showing the \u0026ldquo;real user devices\u0026rdquo; are mostly a rack of Linux boxes.","title":"TCP-fingerprinting 300K residential proxy IPs: 98% are Linux"},{"content":"You can\u0026rsquo;t read what\u0026rsquo;s inside a TLS tunnel. Not without sitting in the middle with your own certificate, and I wasn\u0026rsquo;t doing that. So for a while I assumed encrypted traffic was a dead end — if I can\u0026rsquo;t see the payload, what\u0026rsquo;s left?\nTurns out: the shape. And the shape talks.\nWhen someone runs credential stuffing through a proxy, the tunnel stays sealed but the outline of what\u0026rsquo;s flowing through it is loud. You don\u0026rsquo;t need the contents. You need the silhouette.\nEvery failure looks the same A failed login is about as predictable as it gets. It fails identically. Same error, same JSON shape, same status code, same body length, every single time. The server doesn\u0026rsquo;t get creative when it says no.\nThrough TLS, that sameness survives. Encryption scrambles the bytes but not the number of them, so a wall of failed logins turns into a wall of connections with nearly identical bytes_recv. Once I noticed that, I couldn\u0026rsquo;t un-notice it.\nOne target API gave me around 1,000 connections in 15 minutes, and the response-size distribution looked like this:\nbytes_recv count 5042 494 4821 148 4820 130 5041 71 Over 80% of the responses landed within 200 bytes of each other. That\u0026rsquo;s not what a person browsing looks like. A human loads different pages with different content at different sizes — the variance is all over the place. This was a tight little cluster, and that tightness ended up being the single strongest signal I had.\nTurning \u0026ldquo;it looks uniform\u0026rdquo; into a number Eyeballing a table is fine for one target. I wanted something I could point a script at, so I reached for the coefficient of variation — CV = stdev / mean. It just tells you how spread out the sizes are relative to their average.\nA CV below 0.05 means the responses are basically clones of each other, which is exactly what credential stuffing produces. Normal traffic sits above 0.5. That\u0026rsquo;s a full order of magnitude of daylight between the two, which is a comfortable place to draw a line.\nI didn\u0026rsquo;t want to hang everything on one metric though, so I stacked a few observables into a rough score:\nResponse uniformity — CV \u0026lt; 0.05 → 40 points Request uniformity — CV \u0026lt; 0.10 → 15 points Single JA3 across many connections → 25 points High connection rate — \u0026gt; 1/s → 15 points Volume — \u0026gt; 500 connections → 15 points Anything over 30 flags the target. Nothing fancy — the response uniformity does most of the heavy lifting on its own, and the rest is corroboration. Run against my dataset, this cleanly picked out automated stuffing against a major social API, a stock-trading app, several kids\u0026rsquo; games, a payments wallet, and a dozen banks. Same score, one dataset — it caught all of them.\nJA3 doesn\u0026rsquo;t tell you if — it tells you how many The other thing I could see through the tunnel was the JA3 fingerprint, which is basically a hash of how the client negotiated its TLS handshake. And JA3 answers a different question than I expected. It won\u0026rsquo;t tell you whether you\u0026rsquo;re under attack, but it will tell you how many tools are behind it.\nTwo examples sat right next to each other and made the point better than I could.\nOne trading API took around 1,400 connections with exactly one JA3. One. A single script, one TLS stack, hammering the endpoint over and over without bothering to hide.\nA major identity provider\u0026rsquo;s login endpoint took 64 connections with 60 unique JA3s — someone randomizing their TLS fingerprint on nearly every connection, specifically to dodge the kind of detection I\u0026rsquo;m describing here.\nBoth are credential stuffing. The JA3 spread only tells you how much effort the attacker put into blending in, not whether they\u0026rsquo;re attacking. And the funny thing is, the evasion actually gives them away. Real client populations don\u0026rsquo;t mint a brand-new TLS stack for every single request. 60 fingerprints across 64 connections is the opposite of blending in — it stands right out. When you work that hard to look random, that\u0026rsquo;s what gives you away.\nWhich lands more or less where all of this work lands for me: encryption hides the content, not the shape. And the shape of a machine doing the same thing 1,400 times in a row is a lot more regular — a lot more honest, in its way — than anything a human leaves behind.\n","permalink":"https://vitalii-zaiats.com/posts/detecting-credential-stuffing-through-tls/","summary":"Encrypted traffic still leaks its silhouette: near-identical response sizes (CV \u0026lt; 0.05) plus JA3 counts turn a sealed TLS tunnel into a credential-stuffing detector.","title":"Detecting credential stuffing through encrypted traffic"},{"content":" I\u0026rsquo;m Vitalii Zaiats — a security engineer based in Ukraine, working at the seam between network fingerprinting, malware analysis, and threat intelligence.\nMost of what I do starts from the same instinct: the top layer of a system is the one an attacker controls, and it\u0026rsquo;s usually lying. The layers underneath — the TCP stack, the TLS handshake, the shape of a flow, the native code inside an app — tend to tell the truth. This blog is where I write those investigations up, including the parts where I was wrong.\nWhat I work with Reverse engineering — Android APK triage, Go/native .so analysis, custom C2 protocol reconstruction, live protocol verification. Network fingerprinting — TCP/IP, JA3/JA4, HTTP/2 \u0026amp; HTTP/3, STUN/WebRTC leak detection; distinguishing automation and proxies from real users. Detection \u0026amp; tooling — Suricata/Snort signatures, traffic-shape analysis, and small purpose-built tools (Python, Go, C, some ML with PyTorch). Selected work Residential-proxy botnet — reverse-engineered and disclosed. Took an Android proxy-malware family and its C2 protocol apart from a single infected-device sample, mapped the infrastructure, and submitted a technical report (IOCs + detection rules) for the attention of Europol EC3, national CERTs, and hosting abuse teams. → Read the write-up apkscan — static APK triage pipeline. Batch-analyses cracked APKs for bundled proxy/malware SDKs from the manifest, signing cert, and native code — without executing them. → code Wire↔air correlation research. A bench study of whether a flow\u0026rsquo;s shape survives encryption onto WiFi — including the false positive my own null control caught. → Read the write-up More open source. layerid-fingerprint-c — a dependency-free C99 device-fingerprinting / anti-tamper SDK; layerid-id — a self-hosted OAuth2 / OIDC identity provider (FastAPI + Vue); fps — a multi-layer (TCP/TLS/HTTP2/HTTP3) network-fingerprinting service. Get in touch I\u0026rsquo;m open to security-engineering and threat-intelligence roles. The fastest way to reach me is email . My code is on GitHub (key repos also linked under Selected work above), and I\u0026rsquo;m on LinkedIn .\nDownload my CV ","permalink":"https://vitalii-zaiats.com/about/","summary":"Who I am and what I work on.","title":"About"}]