# How to decrypt two images encrypted using XOR with the same key?

I am trying to solve the following puzzle from CryptoHack involving two images encrypted with the same key:

I've hidden two cool images by XOR with the same secret key so you can't see them!

If I compare both images using an online image comparison tool I can see traces of a new image containing what seems to be the solution to the puzzle. However, I don't think this is the intended way to solve this puzzle.

So I tried to write a program to XOR the two encrypted images together, because the XORed keys should cancel out that way. I XORed both files byte by byte and saved the result as a PNG file. But this new PNG won't even open - my image viewer says it's a faulty file.

This is my Python code:

from binascii import unhexlify

with open("lemur.png", mode='rb') as fl:

with open("flag.png", mode='rb') as ff:

d = b''
for b1, b2 in zip(lemur, flag):
d += bytes([b1^b2])

with open("new.png", mode='wb') as fn:
fn.write(d)


One thing I noticed is that the lemur.png file is one byte longer than the other one. The above program stops XOR at the length of the smaller file. I also tried appending the last byte of the bigger file to the end. But even that doesn't help.

Is my approach incorrect, and if not, what is the right way to compare the two images?

• No, of course it's wrong. Every PNG file starts with the byte sequence 89 50 4E 47 0D 0A 1A 0A. If you XOR two PNG files together in the way you're suggesting, you'll end up with a file that starts with 00 00 00 00 00 00 00 00. If it doesn't start with a PNG header, then it's not a PNG file. – r3mainer Feb 23 at 19:55
• @r3mainer - so what approach should be taken to solve this puzzle? – user93353 Feb 23 at 21:22
• I would imagine you need to XOR the pixel values, not the raw image file data. Try using an image manipulation library to access the pixel data. – r3mainer Feb 23 at 22:49
• That's why you need to use an image manipulation library. XORing compressed image data is not going to work – r3mainer Feb 24 at 9:32
• I can't vote to close this question because it has an active bounty, but I do feel that it's off-topic here. Puzzling or Stack Overflow might be better places to ask this. (Before asking on Puzzling, please read their help page, though.) – Ilmari Karonen Feb 26 at 13:13

PNG is a compressed image format. This means that, unlike with uncompressed formats like BMP or netpbm, the bytes stored on disk in the PNG file do not correspond directly to the bytes of actual image data shown on the screen when you view the image.

In addition to the compressed image data, PNG files also contain a header with distinctive "signature" byte sequence, allowing programs to recognize the file as a PNG image regardless of its name, and several metadata chunks storing additional information about the image such as its width and height, and possibly e.g. a color profile. Each chunk, including the actual image data chunk, also includes a CRC-32 checksum to verify its correctness.

What all this means is that if you XOR a PNG file with something (anything!) else byte by byte, or modify its bytes directly in any other way, the result will almost certainly not be a valid PNG file. If the modification was very minor, some image viewers might still recognize the file as PNG and attempt to display it even though the checksums don't match. But if you do something like XOR the entire file with another PNG file, the result will not even be recognizable as PNG.

So, given that:

• the files you've been given are clearly valid PNG image files,
• the hint you've been given implies that they were valid images also before encryption, and
• the hint says that they've been "XORed with the same key",

what could this possibly mean?

Well, I see only one likely possibility: the encryption must've been done by XORing the raw RGB image data with a (pseudo)random key, and only then saving the resulting image in PNG format.

So, basically, what you'll need to do is to use an image loading library to load and decode the PNG images into a 2D array of pixel values first, and then XOR those pixel values together. And then you'll either need to save the XORed pixel data as PNG (or in some other image format) again for viewing or just make your program display it directly on the screen.

I'll leave actually implementing this in Python as an exercise. If you have any questions about that part, Stack Overflow is the right place for those.

As mentioned by @r3mainer, I get this result from the code below using PIL library.

from PIL import Image, ImageChops
im1 = Image.open('lemur.png')
im2 = Image.open('flag.png')

im3 = ImageChops.add(ImageChops.subtract(im2, im1), ImageChops.subtract(im1, im2))
im3.show()
#im3.save("Your_path/im3.png")


I alter the code found on this link to be more explainable.

My code calculate abs(a-b) while the description of a XOR b using arithmetic in this link is (a-b)^2. They produce the same value when a and b is either 0 or 1.

Normally, a picture has RGB color channel which each of them is treated as 8-bit integer. This code, ofcourse, doesn't give the result as R3 = R1 XOR R2 (if R1 from R channel of picture 1 and so on). But I code this way because I couldn't find any decent arithmetic expression that can really substitue the XOR operation and that I check with this link and it can produce the same result so I think it should works.

• While this works well enough to reveal the hidden flag, what you're computing is not the bitwise XOR of the image data. The equivalence $|a - b| = (a - b)^2 = a \oplus b$ only holds when $a$ and $b$ are single bits (i.e. either 0 or 1), not when they're multi-bit numbers. However, since bitwise XOR can be interpreted as "borrowless subtraction" (or, equivalently, as "carryless addition") in binary, it turns out that $|a - b|$ is often fairly close (by various measures, such as numerical difference or Hamming distance) to $a \oplus b$. – Ilmari Karonen Feb 26 at 14:57