There aren’t that many homes

I wasn’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.

So I sat there and did the dumb arithmetic. Residential means a real home, a real router, a real broadband line. Homes are finite. There’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?

There 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’s pools, so the same real home shows up under five brand names.

And then there’s the ugly third answer. Some of those “residential” IPs are homes whose owners never agreed to anything. Their device just quietly became somebody else’s exit node. Keep that third one in mind, it shows up again later.

I filed the whole thing away as an interesting shower thought and moved on. Then, completely unrelated, a phone landed in my lap.

A 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 “my phone is haunted” symptoms.

I 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.

First move was the boring one: put the phone’s traffic through Wireshark and just watch. It didn’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 “app woke up to sync.” Just held open the whole time. That port, 1334, only ever showed up right here, in the phone’s own capture.

A phone that keeps one long-lived tunnel open to a datacenter all night isn’t resting. That’s a phone that’s working for someone.

A game from a Telegram channel

So I asked the obvious question: what did you install recently, and where from?

The 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 “cracked game” 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.

I pulled the APK off the device over adb and took it home to look at it properly.

apkscan, or reading APKs without running them

I didn’t want to run this thing. Running unknown malware to see what it does is a great way to learn things you’ll regret. So I ran it through apkscan, a little static-analysis pipeline I’d built for exactly this — triaging sketchy APKs without ever executing them. Everything it does is static; nothing gets a chance to phone home.

It answers three questions I care about:

  1. Can 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.
  2. 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’t the original developer.
  3. What native code is it carrying? Every lib/<abi>/*.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 “there’s a silent background worker in here” flag. Nothing magic — just the questions I’d ask by hand, automated so I don’t skip one at 1 a.m.

This APK lit up the background-worker flag immediately. But the real move was comparing it against a clean copy.

A 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’s different? So I diffed them, file by file. Three files had been injected that didn’t belong.

FileWhat it isWhat it’s for
A decoy .mp3An audio file with no actual payloadAlmost certainly padding, there to make the repack’s file size line up
A bootstrap .dexA handful of Java classesLoads and invokes the native library — the glue
libgojni.soAn ELF shared object, Go-compiledThe 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.

So 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:

build -ldflags="-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"

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’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’t hiding what it is from anyone who bothers to look — it’s hiding from people who never look.

It doesn’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.

Put those together and the picture is clean. The malware turns the phone into a proxy node — SOCKS5 and HTTP both. It’s not rummaging for your photos or your files. It doesn’t care about you at all. It’s renting out your internet connection to strangers, and your phone is the meter. That’s why the battery was warm and the data was gone by morning. It was somebody else’s exit node all night.

The ugly third answer, confirmed. This was a home that never agreed to anything — exactly the option I’d shrugged off on the forum weeks earlier, which was a little eerie.

Two 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.

infected phone(bot)hornmot.comconfig · Cloudflarerelay:13341 · fetch relay addr (TLS)2 · persistent TCP/1334one socket · many channelscustomers' trafficSOCKS5 · 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’d seen on the phone.

Seven bytes and four letters

The traffic on 1334 isn’t HTTP or anything off-the-shelf. It’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:

ByteLetterDirectionMeaning
0x54Tbot → C2Authentication
0x4ENC2 → botOpen new channel
0x57WbidirectionalData transfer
0x43CbidirectionalClose channel

Multiplexed so one infected phone can service many customers’ traffic at once without opening a fresh socket per job. Efficient. Someone built this on purpose.

A password that never expires

The authentication is dead simple. It’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’s the whole credential. The UUID is the account.

Here’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.

Knocking 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’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.

So 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.

So who’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.

It was not ad verification.

The 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’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 “residential proxy service” framing is the cover story. The product is really rate-limit evasion for people attacking mailboxes at scale.

Three 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’s abuse desk ever sees the whole shape of the thing. Send a complaint to the Dutch host and you’ve reported some relays and nothing else.

One 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.

417,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’s window, my own rough estimate.

When 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’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 “there aren’t that many homes” was exactly the wrong instinct. There were plenty. They just hadn’t been asked.

The 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’s EC3, national CERTs, and the relevant hosting abuse teams.

The nice thing about a lazy protocol is that it’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’s the very first thing a bot says on 1334:

# Bot authentication frame, first packet on TCP/1334
alert tcp $HOME_NET any -> $EXTERNAL_NET 1334 (
  msg:"Residential Proxy Botnet - Auth Frame";
  flow:established,to_server;
  content:"|54|"; offset:0; depth:1;
  content:"|04 01|"; 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’t have a maybe. You have a sleeper.

Then I waited, because that’s the only thing you can do.

Months 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.

NCSC-NL challenge coin in a presentation box

A challenge coin from NCSC-NL, the Dutch national CERT - their thanks for the disclosure.

I want to be honest about what that coin does and doesn’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’ll take a challenge coin over credit any day.

Oh — and the phone. It’s fine now. We wiped it. It runs cool, sleeps through the night, and no longer answers to strangers.

Two things I still don’t know

I’ll end on the loose threads, because this is the honest state of it.

First, a real puzzle I haven’t cracked. A surprisingly large share of these residential exits present a TCP TTL consistent with Linux — an initial 64 — even in regions I’d expect to be wall-to-wall iOS and Windows, which don’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’t pinned down. I don’t have the answer yet. It’s the next post.

Second, 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 .

I was just bored on a forum. The math didn’t add up, and it turned out there were about 17 million reasons why. The IP was never the part worth hiding.