Xor Hides In Plain Sight

:date: 2026-01-17 07:16 :tags:

In my last post I introduced the XOR logical operation and discussed some of the ways it is used. A few weeks after adding all of the bitwise logical functions to my programming language, I was nerd sniped by a fantastic opportunity to put them to the test. I received this fascinating spam email.

spam.png

Most of you don't see email in a text client. For those of us who still do, we are largely immune to many forms of graphical obfuscation and Javascript mischief. This means that I can safely take an academic interest in the technical tricks of spammers. Looking over this email I started to have a feeling that it was hiding something and when I got toward the bottom, I couldn't believe my luck! Check out this line.

k += f(j.charCodeAt(c) ^ q.charCodeAt(c % q.length));

If you read my xor introduction you saw that the most common programming language syntax for xor is the circumflex hat: ^. And there it is center stage!

What are they using xor for? That big messy looking variable definition of x gave me an instant suspicion.

let x =
"EAQQGVJeVkA......

It looks to me like they are trying to hide a malware payload in an encrypted string. And the interesting part, the encryption algorithm looks to be based on the xor function.

Yes, it turns out that one of xor's other party tricks is that it serves as a simple low effort encryption algorithm. For encryption it is very fast but also quite weak, which makes it perfect for a job where it just needs to quickly scramble some text to slip it by an adversary known to have no decryption capabilities (normal humans and normal spam filters).

When presented with a (deliberately) messy piece of code today, you fortunately don't have to burn too many brain cells working it out. I gave this code to my robot friend and it was confirmed  —  this code is definitely obfuscating some text using an extremely simple xor cipher.

How does an xor cipher work? We already saw that when you xor numbers you basically get a somewhat (very pseudo) random number. This means that you just need to xor your plain text with a password and you'll not be able to read it. To get the plain text back from the cipher text you just need to xor the cipher text with the password again and the plain text will reemerge. Remember that toggle property? Using a passphrase to toggle between encrypted and decrypted is how this very simple xor cipher works.

Not only can our robot friends help us guess what's going on, in cases like this you also don't even have to trust your robot friends' guesses. I asked it to translate this code into readable Python which I could use to run and decode the payload myself. It did and I did, and we confirmed an xor cipher was exactly what this was.

At this point I got an ambitious stupid idea that cost me several days. I wondered, could I write a decoder for this encrypted text using only my own programming language? It would be a superb test of all the nice new boolean bitwise logic operators I have just implemented. And one of the last programming challenges where our robot friends can provide absolutely no help!

Fast forward many days of working on this and here we go. (Don't worry about the details unless you like this sort of thing.)

'' ------------- Define variables.'' #
''EAQQGVJeVkIWDxEFVldAC1AIFRgCRQYCEgAnVVBfXFtCGlAVVlYdRFQxO084YFIPRExLAj8SGRUWVV0OThpGB1sCA1kZGQIHAk1FWFleVkIbQVAMXBlaFFwBCFhGHlhpRkVCGVJeVkIYQVAPXVZaHhsHBVJJEAIPCgoVFEFdSRhYU0cIXlVBD1oIRh9aPUNDRkUFVVpFF0ZXXFUDVkwbB1ECSREAWw8MEUgPVlFTVUYRGwprGRQVRlIKDkFPRAINAgcNQRtTXVEeFVANVVtCS0YFE18RQxBET15oGRUSGVJaXUZPSlVbAlcJGRgAUwdLQQQOVVpFFEVZQkQRShlBCRgDElUARwZOFQQMXVddQRIfCTtBGRQVAVkJFhgSVg0HBAoaF1RWXR0RU10NVkMYAFoUDEVGHlhpRkVCGVJeVkIYQUMCGQkVB0EJAx5GVitETUcwCRcZHlZ+FRoBdAJVTRIqGBFKFVoWRE5FY20VEhdkQRNKWVViBh5BOwNGHEQvC0JJG3teGx4Ra2ZGElRvFlVNQ1RTFUhBEg1AEhd+VBcdEF0LGx8XAmZEShRZX0FIRAcnGx4Vd0ERGVEzCFQeRGcQQx1GYg1ETQU4fFUZG29xEBpDY3MXTRc/URRKEDkVQU5FWwYVEhJaAhZKHntjQR4GI3UBHEE1I0dJG1lnGx5WfkYBElQIW1VPSkxaPUNDRkUFVVpFF0ZCS10EF1dGFWEDGUJBCkNEFgoRUEFbVlsMElcIQVFRXRUPD0UEQ1lDVhUaAhVFUFFCWgtBCAQFQw5GCVMIUAsXXEVTCQUXAhVUXUMFXEYPRgUWGQ1BWgIRAQwMAxUCSU0NQlAFXV1bAQ9GUUYZDEMMEAAQX1ldTg8WWlgFXVFbXRUcTF8PUwYbXEVbAAwLAAwNFQprGRQVRlEJAkMMUg0XSBIQUEFXEVJaXUZIAj4VRhVGBVkCQg4GCBFMW1pWQBtfXF8ES3xhK3lGXBZDFVhpRkVCGVFdWkBbV18VF1ZaAkxIAEYRUg0HJQ0LVVEaXllZRRha'' |U sto
''feb95295621a945f5fa6a7cc''               ''key'' #
dup len                                    ''length of key'' #
U len swap /                               ''size of X / size of key'' #
ceil *                                     ''pad out beyond (ceil) what is necessary'' #
|i sto                                     ''i is sufficiently repeated key'' #
'' ------------- Helpful abbreviation.'' #
|[2 base::!] |b sto
'' ------------- Make a lookup table.'' #
''ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'' |b64map sto
'' ------------- Go through each quad and pull out a padded 24bit binary number.'' #
U len 4 / range 4 * 1 +  ''Generate a quad index list: 1 5 9 13 ... 1037 '' #
|[ 4 range + |[U swap get b64map swap pos -- b dup len 6 swap - ''0'' * swap +::!] for + + + ::!] for
'' ------------- Put the 24bit binaries in a list'' #
U len 4 / 2list
'' ------------- From 24 bits pull 3 chars'' #
[] |X sto
|[ dup dup b 16 bsr rot b 8 bsr 255 band rot b 255 band 3 2list X swap :+ |X sto ::!] for
'' ------------- Iterate through ciphertext, apply XOR, save plaintext P'' #
'''' |P sto
X len range 1 +
|[dup X swap get swap i swap get num bxor chr P swap + |P sto::!] for
P

The hard part was actually doing an initial decoding from base64, a common encoding format used to protect binary data that is transported in readable text characters. With Python, you can just import base64 and it's easy, and in Javascript it's even easier  —  note the atob function in the spam code (which is alphanumeric to binary). Of course I had to implement my own which was another great test of my new boolean logic functions.

But once the message is converted back into (unreadable) binary it really was as simple as doing an XOR operation on each cipher text byte with each character of the passphrase; when you run out of passphrase characters, just go back and loop to the passphrase beginning again (in the original JS done with: c % q.length). This is very low security. The passphrase is included right there, it is the string that starts with feb95....

Ok, so what is this spam mail trying to get a promiscuous mail client's Javascript "feature" to run? Here's the output of my Geogad version.

payload.png

I'm keeping the dangerous code confined to images for you  —  to create mischief you'll need to type it in. But we can see that it seems to be doing a lot of allow functions, presumably to lower your normal defenses. I'm no JavaScript web security expert so I asked my robot friend to tell me what this is doing. (Again, ignore the details if you're not interested - I wasn't!)

Yikes. None of that sounds pleasant.

One final mystery is the glow.src definition. First of all, I'm baffled by their exuberant use of mixed quote styles (but of course I am a snob when it comes to text quoting). It seems to be another text string encoded with base64. Since I had to work it all out anyway, here is a general base64 decoder written in Geogad that decodes this text.

[ ''aH'' ''R0'' ''cH'' ''M6'' ''Ly'' ''9u'' ''ZX'' ''Rs'' ''aW'' ''Z5'' ''Lm'' ''Nl'' ''YW'' ''Zp''
''b2'' ''th'' ''Lm'' ''lj'' ''dS'' ''8h'' ''bE'' ''Nt'' ''R1'' ''Rv'' ''Un'' ''ZE'' ''ZG'' ''ZG''
''Y0'' ''Zv'' ''b3'' ''l0'' ''OV'' ''BC'' ''VE'' ''lU'' ''Lw'' ''AA'' ]
'''' swap |[+::!] for |U sto
|[2 base::!] |b sto
''ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'' |b64map sto
U len 4 / range 4 * 1 +
|[ 4 range + |[U swap get b64map swap pos -- b dup len 6 swap - ''0'' * swap +::!] for + + + ::!] for
U len 4 / 2list
[] |X sto
|[ dup dup b 16 bsr rot b 8 bsr 255 band rot b 255 band 3 2list X swap :+ |X sto ::!] for
X |[chr::!] for X len 2list '''' swap |[+::!] for

When we run that we can see the URL this malware reports to.

glowsrc.png

Obviously opening this URL is a bad idea. I think this .icu TLD is a beautiful example of how fucked DNS is these days. I was really curious what "icu" was an abbreviation for, but I have come to believe it is meant to be read as "I see you". Totally not creepy at all!

Amusingly, when first presented with this problem, my robot friend went on to confidently say it had preemptively worked out the binary to ascii translation itself and proceeded to implicate a completely hallucinated Cuban URL.

fakeglow.png

I guess that when you have a very sophisticated LLM that can walk through the code with you and still fail to properly get the right answer, it demonstrates why this spam tactic might still be effective. And that demonstrates why it can be useful to have your own tools for doing bitwise math operations so you can decode it yourself!