ES EN
Index

RedTail · Chapter 15

I went back for RedTail, and RedTail already had an owner

I built a cage, ran the miner and pulled from memory the configuration I couldn't decrypt back in chapter 6. I got excited… and walked away with two things I wasn't looking for: that this family was already documented top to bottom —somewhere it never occurred to me to look— and that the piece I thought I'd decrypted was for something else.

In chapter 6 I left off with a promise: "The binary isn't going anywhere. I'll be back." And with a gap in the map — a line that read ??? decrypted and an arrow next to it: this is where I stopped.

I remember where I left it, which is where this starts. I opened RedTail's miner looking for its Monero wallet —where the money goes— and came away with three things: that the wallet isn't in the binary, and isn't there by accident; that the configuration does go inside, embedded and encrypted; and that, sweeping the whole file with a statistical test, the fingerprint of a repeating-key XOR showed up nowhere — from 1 to 40 bytes of key, not a single hit. What I didn't get was the middle piece: what turns those encrypted bytes into the text the miner reads.

Days later I had two things I didn't have then. A cage to run live bugs in without them escaping. And a splinter that chapter 13 had left stuck in me. Let me tell you.

01The lesson I'd already written

The chapter 13 thing went like this. I gutted a bug, thought I was the first to spot one particular gut, and before publishing did what I always do: paste the finding into a search engine. Out came a Chinese report from 2020 with everything of mine in it, six years earlier. I wasn't the first. Not by a long shot.

With that splinter I reread my own chapter 6. And the uncomfortable part wasn't finding a mistake — it was finding, in the last section, this conclusion signed by me:

What I myself wrote in chapter 6"Check first whether someone already came this way. It should have been the first thing, not the last."

The lesson was already written. I had written it. And even so, a few paragraphs above that line, there was another I had never verified: "nobody has published how RedTail's configuration is decrypted".

And look, search I did. I went to Akamai, the reference for this family, and to Malpedia, the catalogue. I found nothing, and sat back happy. What I didn't do was keep searching after the big source agreed with me.

So this time I searched differently: with other words, in other languages and in smaller places. And I got it wrong again — but in a different way.

02It was written down. In Italian.

It didn't take much digging, once I stopped digging where I'd already dug. An Italian technical analysis had the family cut wide open, and it matched piece by piece what Akamai had already told:

  • Its proxy domains, names and all, published. And two more than I'd managed to see.
  • Port 2137 —the one I flagged in chapter 6 as its signature— documented as its mining channel over TLS.
  • Why the wallet is missing. And here I'll be fair to myself, which is easy to overdo in the self-flagellation direction: in chapter 6 I got the what right. I said the wallet isn't in the file and isn't there by design, and I deduced it by looking at what had been amputated from the miner. What I didn't have was the how, and Akamai spells it out: RedTail doesn't point at a public pool — it stands up its own mining infrastructure, its own private proxies and pools, which recognise its miners by the IP they call from. The wallet stays on that side. In the binary it isn't needed.

In other words: my conclusion was good, and the mechanism that explains it had been published for over a year. Which is an elegant way of saying I spent an afternoon deducing something I could have read.

And the other wall, the decryption one —the one I left open with an I'll be back—, wasn't an open mystery either. It was a mystery solved in a language it didn't occur to me to try.

03But I had the cage built

I could have left it there, with the lesson learned for the second time and my face a little red. But I had an itch left — and a tool.

From Trinity, the worm in chapter 13, I still had a lab to run malware without it escaping: an isolated machine, the network cable unplugged, and the process locked inside a fake network —a bogus interface, a route to nowhere— from which not a single packet leaves. I tested it before anything else: from inside that cage, the internet doesn't exist.

And I thought: since I've got it, I'll walk the same path. Not to discover anything the others didn't know —I'd already accepted that— but to see it with my own eyes, from my own decoy. The miner decrypts its config on startup, holds it in memory for a few instants before trying to connect. I just had to look at that moment.

The same line I moved in chapter 13Yes, this is running a live bug, and yes, it goes against the rule I started this log with. I move it the same way I moved it in chapter 13, and for the same reason: in a cage, with no network, observing isn't releasing. And here, on top of that, it's a miner, not a worm: this binary doesn't spread by itself — its thing is to mine and talk to its infrastructure. (That the campaign delivering it uses exploits and stolen credentials is another story, and not the one running here.) In a fake network, that call dies against a painted wall.

04What I saw in memory

I launch the miner inside the cage and lose sight of it in two seconds. Because the first thing it does isn't mine: it's disappear. It detaches from the terminal that launched it and shows up in the process table under another name. Where the file name should go it reads php-fpm: pool www — a web-server process about as boring as they come, the sort nobody scanning a ps runs a finger over. Among its other disguises it carries /bin/mariadbd and /usr/sbin/ip: a database and one of the system's own network tools.

A trick of the trade, for whoever comes afterBy name you won't find it, because it made the name up. I found it by the user I launched it as: a process can change its label, but not its owner.

And here's the beauty of the cage: no rush. The bug is already running, it's already decrypted the configuration so it can use it, and it still hasn't managed to talk to anyone. So I freeze it dead —a stop signal, and the process stays mid-sentence— and I read its memory from outside, calmly, through the same place the system lets you look at any process of your own.

And there it is. In the clear. The same thing that on disk is encrypted:

decrypted config · pulled from the process's memory
"url": "proxies.identities.network:2137"
"url": "proxies.insanecppdev.com:2137"
"url": "proxies.insanitycpp.cx:2137"

The three proxies, on 2137. Exactly what the reports said — but coming out of my bug, on my machine, with my own hands. It's not a scoop; it's something else. It's the difference between believing something and having seen it.

A warning if you're going to note those names down: they rotate. A public tracker listed, that same day, different domains from the same family. These three are the ones my sample carried, not RedTail's eternal list.

And with the configuration came a detail I wasn't expecting. The only connection the miner attempts on startup doesn't go to any of its proxies: it goes to port 853 of an IP. 853 is the usual port for DNS over TLS, so everything points that way — whether the connection really carries DoT inside I haven't opened up to check; what I have is the destination and the port. Instead of asking "what IP does proxies.insanecppdev.com have?" in plain view of everyone —its provider, the household firewall, anyone watching the traffic—, it asks over an encrypted channel. It doesn't just hide who it talks to: it hides who it asks about it.

I went to check whose IP that was before getting excited —lesson learned, and this time in time— and just as well. That IP answers as dns.njal.la: the public resolver of Njalla, a Swedish privacy-oriented service that plenty of legitimate people use. It's not an IP I can attribute to RedTail's infrastructure — it's a third-party service the bug asks. That is: it doesn't run its own DNS. It leans on someone else's, privacy-oriented, and wraps the query inside an encrypted channel.

Why this is NOT an indicatorI was about to note that IP down as "RedTail infrastructure". It would have been one of those mistakes that get copied: dns.njal.la is used by thousands of people who have nothing to do with this. Flagging it would be like reporting the phone company because a criminal made a call. The good data point isn't the IP — it's the technique: RedTail resolves its domains over an anonymous, encrypted DNS. That does portray the operator.

05And on top of that, the table that wasn't

And now my own mistake, the one that really stings: nobody did this one to me, I did it to myself. Before pulling out the cage I'd tried the clean route —finding the decryptor inside the binary— and thought I'd found it.

A fine entropy sweep —measuring the binary's "disorder" in small windows, not big ones like in chapter 6— turned up a 256-byte table hidden among the data, a perfect permutation of every possible value. A substitution table: the typical piece of a homemade cipher. My first reflex was to call it an "S-box" — and that name already presumes a cipher role I hadn't proven.

02468entropy (bits/byte)0x465a00 · 7.6720,312 windows of 256 B · 5.2 MB →
The whole binary, measured in 256-byte windows. Twenty thousand chunks, all with normal entropy… and a single needle. That's the hidden table.

It was identical, byte for byte, across the bug's five architectures — when I wrote this I counted four, because my version of UPX couldn't get the wrapper off the RISC-V binary and I left it out. A newer version opens it without complaint, and the table is in there too. Same thing again: the fault was my tool's, not the sample's. That places it in the common trunk of all four —part of what they carry inside, not what each platform's compiler adds—, though it doesn't prove they wrote it: it could come from some library they drag along.

00112233445566778899AABBCCDDEEFF256 cells · 256 distinct values · zero repeats
The table I found, drawn cell by cell (colour is the value). All 256 possible values, each once: a permutation. It smelled of encryption. It wasn't.

And I found the routine that uses it: a mix of chained adds and XORs. I convinced myself that this was the config's cipher, and licked my lips.

Well, no. When I finally searched properly, the Italian analysis said RedTail's configuration is decrypted with something quite different: a pseudo-random number generator. One of those that, given a starting number, spit out a stream of bytes that looks random but is always the same. That stream is the key, and with it the cipher comes undone.

And they gave a clue to recognise it: the constant 0x6c078965, the one the Mersenne Twister —the MT19937, a classic generator— uses when setting up its initial state. I went looking for it in my binary. There it was, crouching at 0x1c409.

Here I'll split hairsWhich is exactly what I didn't do in chapter 6. From now on it's worth separating three things that aren't worth the same. What I've seen myself: the configuration in memory, the names it disguises itself with, the 256-byte table, and that constant in my sample. What I've read: that the configuration is decrypted with that generator — they say so; I haven't followed the routine to the end. And what I deduce putting the two together: that this table has nothing to do there. It fits well. But deducing isn't having seen, and that distinction is the one I lacked the first time.

And isn't it RC4?

It's the question that had to be asked: a 256-byte permutation moved around with adds and XOR looks exactly like RC4. But this time I didn't trust the resemblance — I looked in the binary. And it doesn't fit, in three places.

The table lives in a read-only segment (R-X, no write bit), and RC4 needs to rewrite its state on every byte it produces (swap(S[i],S[j])): there, it physically can't run. And in the whole binary nobody writes to it — of the three only times it's touched, all three are reads, and all three fall in the same loop:

the table in the binary · permissions and references
va 0x8659e0   R-X segment   ; read-only, no writes

3 references in 962,205 instructions, all 3 reads:
  0x6c82e9   movzx eax, byte [rax + 0x8659e0]
  0x6c8309   movzx eax, byte [rax + 0x8659e0]
  0x6c832a   movzx eax, byte [rax + 0x8659e0]
not one swap, not one write, in the whole file

And the shape of the code isn't RC4's either —which is swap plus a single XOR against the data—: what's there is h = T[h ⊕ c] chained, one forward pass adding and one backward pass with XOR. Zero swaps. It's a chained substitution, Pearson style, and the loop hangs off two descriptors that only change parameters — the shape of a library's algorithm table, not of hand-written code (a stone's throw away, in .rodata, there's a wall of libuv strings). So the honest label is the dullest one: a 256-byte substitution table. Which library and for exactly what, I don't know — the binary is stripped, no symbols, and it's not enough for me to identify it. And I'm not going to make it up, which is exactly what this chapter is about.

And now the part I wasn't expecting: this absolves me of something else. In chapter 6 I spent an afternoon proving the configuration is not encrypted with a repeating-key XOR — I swept the whole file for the statistical fingerprint that cipher leaves, trying keys from 1 to 40 bytes, and it came out clean. It was the only thing in that chapter I hadn't read anywhere, so I confess that, this late in the day, I feared this would knock that down too.

Well, it doesn't knock it down: it turns it around. A generator like this produces a deterministic stream of bytes —always the same if you give it the same starting number— that can be used as a key. And if decryption means applying that stream byte by byte with XOR, then it's still XOR. What falls isn't the XOR: it's the short-repeating-key hypothesis. A stream like that takes an absurdly long time to loop back on itself —incomparably longer than this file's five megabytes— so a test looking for repeats every 1 to 40 bytes had nothing to find.

Which is the honest summary of that afternoon: I wasn't wrong to look for XOR. I was wrong to look for a short key.

The table has a nameI closed that line saying I didn't know which library it came from and that I wasn't going to make it up. There's nothing left to make up.

Those 256 bytes sit verbatim inside OpenSSL, Nettle and libgcrypt. They are the PITABLE of the RC2 cipher, the one defined by RFC 2268 — and RedTail links OpenSSL statically, so it drags it along without meaning to.

And it matches what I described without knowing what it was: RC2's key schedule is, word for word, one pass forward adding and another backward with XOR. That those three reads are exactly RC2's key expansion is the only thing I'm inferring here; that the table is RC2's is measured.

✅ And it doesn't dismantle anything: it confirms. The table isn't RedTail's and doesn't decrypt the configuration, which was the conclusion. And ruling out RC4 was right too — the hunch that it "looked exactly like RC4" was off by a single digit: RC2, not RC4.
The difference, which is the lessonMy table exists, it's real and it's in there. But it doesn't decrypt the config — and, as I've just seen, it probably isn't even its own, but from a library it drags along. I took it for the main cipher because it fit what I wanted to find. I found a real piece, and stuck on it the label that suited me. That's how a mistake slips in: not by inventing a data point, but by wanting the data point you have to be the one you were after.

06Three weeks on

The three proxies, todayThe three proxies are still alive and resolve to 31.56.209.165 and 130.12.180.51. (The root domains don't resolve: only the proxies.* subdomains exist.) This campaign hasn't gone anywhere.

And looking at where those addresses live turned up a thread I hadn't seen, joining two chapters ten apart.

The mining proxy's IP, 31.56.209.165, and the delivery server from Chapter 5, 217.60.195.113, sit in different ranges — but they share the same network number and the same registry name: AS209373, SWISSNET. (And here the two facts agree, without the holder-versus-announcer tangle that turned up in Chapter 11.)

Which is to say: the place that hands out the binary and the place that binary sends the money to are with the same provider. No chapter had connected those two pieces, and it only shows up by comparing 5 with 15. Which is exactly what Chapter 4 was celebrating: saving and comparing pays.

The missing piece of the Mersenne TwisterUp above I published the constant the generator is seeded with, 0x6c078965. Verifying chapters 5 and 6 turned up the other half: the complete implementation of the generator, at 0x466cb0, with all four textbook constants. They're two different functions and they fit together — one seeds, the other generates.

One detail that points somewhere: that function has a single consumer in the whole binary. A general-purpose library PRNG would have dozens.

⚠️ What it's used for is not established. It's a lead, not a conclusion: I'm not saying that's the byte stream that decrypts the configuration, because I haven't followed it to the end.
And something that stings a littleI got the encrypted-DNS finding by switching the miner on in the cage. It turns out it had been sitting there free since 30 July in an automated VirusTotal report. Two different routes, the same result — except one cost building a lab and the other cost looking. The configuration in memory did require detonating it; this didn't.

Chapter 6 ended by saying that recognising a wall is part of the craft. I still think so. Only now I know there are three kinds of wall: the one that can't be knocked down, the one that only costs hours… and the one someone already knocked down while you were banging your head against the one next to it.

RedTail had an owner from the start. The name 2137gang came in the very domains my sample carried, and a handful of security firms had had the family cut wide open since 2024. I arrived in 2026, with a freshly built cage and a lesson I'd already written to myself.

I came back, as I said I would. I don't bring the scoop I wanted: I bring the full map, the missing piece put in by others, and a mistake of my own told before anyone else tells it. That'll do.

To be continued — the decoy's still on. And me, from now on, with the search box open before the first line — and not closing it because the first place agreed with me. 🍯

Comments