Description
Our astronaut gained access to a key satellite and intercepted an encrypted message. The forensics team also recovered a file that looks like a custom encryption protocol. We’re sure that these two elements are linked. Please can you help us reveal the contents of the secret message?
When we extract the LunaCrypt.zip file we found two files:
- LunaCrypt.py → The script used for the encryption
- output.txt → The encrypted flag
We have to look at the code to decrypt the message.
Analyzing LunaCrypt.py
The output
We take a look at the function that writes the ouput:
1
output = [f"{str(ord(v))} {str(ord(FLAGS[i]))}" for i, v in enumerate(CHARS)]
We can see that the output is written by pairs of numbers. The first number is an encrypted character (ct) and the second number is the flag used to encrypt it.
We take a look at the FLAG list generation:
1
2
def AppendFlag(flag):
FLAGS.append(strchr(bitxor(flag, 0x4A)))
Here we can see that the flags written in the output are xored with the key 0x4A.
We need to keep this in mind in order to get the flags used for the characters encryption.
The encryption
If we take a look to the function used to encrypt the characters:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def EncryptCharacter(char):
char = ValidateChar(char)
flag = GenerateFlag()
if CheckFlag(flag, FL_SWAPBYTES):
char = ESwapChar(char)
if CheckFlag(flag, FL_NEGATE):
char = NegateChar(char)
if CheckFlag(flag, FL_XORBY6B):
char = XorBy6B(char)
if CheckFlag(flag, FL_XORBY3E):
char = XorBy3E(char)
return char, flag
We can see that the first thing done is a character validation:
1
2
3
4
5
6
strbyt = lambda x, y=0: ord(x[y])
def ValidateChar(char):
if type(char) is str and len(char) == 1:
char = strbyt(char)
return char
The only thing that does the ValidateChar() function is return the ordinal of the character if the character has length 1, otherwise returns the character.
Then, the EncryptCharacter() function generates the flag. The flag generation is completely random and several flags may be activated at the same time.
Finally, the EncryptCharacter() function, checks if the flags FL_SWAPBYTES, FL_NEGATE, FL_XORBY6B and FL_XORBY3E are activated and, depending on which ones are activated, the encryption functions are applied to the character.
As several flags can be activated simultaneously, and the encrypting operations are applied in order, a character can undergo several encryptions. We need to keep this in mind in order to reverse this part.
Decrypting the message
Reading the output
As the output is written by pairs of numbers, and the first number is the encrypted character, and the second number is the encrypted flag, we can read the output and separate the encrypted characters and the encrypted flags.
Decrypting the flags
As the flags are encrypted xoring the flag with the 0x4A key, by the properties of XOR, we can obtain the plaintext flags only by xoring the encrypted flags with the same key.
1
pt ⊕ key = ct → ct ⊕ key = pt
Reversing the EncryptCharacter() function
Since there can be several flags active at the same time, and the encryption functions are applied in order, the only thing we need to do is to reverse the order of the functions applied:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def DecryptCharacter(char, flag):
char = ValidateChar(char)
if CheckFlag(flag, FL_XORBY3E):
char = XorBy3E(char)
if CheckFlag(flag, FL_XORBY6B):
char = XorBy6B(char)
if CheckFlag(flag, FL_NEGATE):
char = NegateChar(char)
if CheckFlag(flag, FL_SWAPBYTES):
char = InvertESwapChar(char)
if type(char) is int:
char = strchr(char)
return char
Reversing XorBy3E, XorBy6B and NegateChar
As these functions are only xorings (remember the XOR properties: pt ⊕ key = ct → ct ⊕ key = pt
) and a negation, the inverse of these functions are the same functions.
Reversing ESwapChar() function
This is the tricky part of the challenge. Once you understand what it does, reversing it is pretty straightforward (although I spent more time on it than I’d like to assume…).
1
2
3
4
5
6
def ESwapChar(char):
char = ValidateChar(char)
THIS_MSB = bitext(char, 4, 4)
THIS_LSB = bitext(char, 0, 4)
return strchr(bitbor(bitxor(THIS_MSB, 0x0D), bitxor(bitlst(THIS_LSB, 4), 0xB0)))
What does this function do?
The first thing this function does is validate the character (we’ve seen before that this returns the ordinal of the character if the character has length 1, otherwise returns the character).
Then declares two variables (in capital letters!? Really? ¬¬!): THIS_MSB and THIS_LSB
Lastly, this function applies some operations to THIS_MSB and THIS_LSB to get the encrypted char.
Let’s simplify the encrypt function a bit:
1
2
3
4
5
6
7
8
9
* strchr = lambda x: chr(x)
* bitbor = lambda x, y: x | y → or
* bitxor = lambda x, y: x ^ y → xor
* bitlst = lambda x, y: x << y → Left shift, moves x to the left y places
return strchr(bitbor(bitxor(THIS_MSB, 0x0D), bitxor(bitlst(THIS_LSB, 4), 0xB0))) →
→ ct = strchr(bitbor(bitxor(THIS_MSB, 0x0D), bitxor(bitlst(THIS_LSB, 4), 0xB0))) →
→ ct = chr(OR[XOR(THIS_MSB, 0x0D), XOR(bitlst(THIS_LSB, 4), 0xB0)])
→ ct = chr[(THIS_MSB ⊕ 0x0D) | (bitlst(THIS_LSB, 4) ⊕ 0xB0)]
We can see that the operation is a chr() applied over an OR between two XORs, so we’ll need to start by reversing the two XORs.
Left XOR: XOR(THIS_MSB, 0x0D)
Ok, as this part it’s a simpe XOR between THIS_MSB and 0x0D, what does THIS_MSB do?
1
2
3
4
5
6
7
8
* bitrst = lambda x, y: x >> y → Right shift, moves x to the right y places
* bitext = lambda x, y, z=1: bitrst(x, y) & int(math.pow(2, z) - 1) = # the value of z will be overriden by the value of the caller function
= (x >> y) & int(math.pow(2, z) - 1)
* THIS_MSB = bitext(char, 4, 4) =
= (char >> 4) & int(math.pow(2, 4) - 1) = # SWAP! (char >> 4) converts the 4 most significant bits of the plaintext (pt) char into the 4 least significant bits of the encrypted char (ct)
= (char >> 4) & int(16 - 1) =
= (char >> 4) & 15
Let’s see it in action:
1
2
3
4
5
6
7
8
9
pt = char = 'A' = 0100 **0001**
0000 **0100** char >> 4 → SWAP!
AND 0000 1111 15*
-------------
0000 0100 (char >>4) & 15 = THIS_MSB
XOR 0000 1101 0x0D
-------------
0000 1001 THIS_MSB ⊕ 0x0D = left XOR
*As (0&0) = 0 and (0&1) = 1 and we are working only with the four least significant bits, this function does nothing and we can skip it to reverse this part
Right XOR: XOR(bitlst(THIS_LSB, 4), 0xB0))
This part it’s another simple XOR between bitlst(THIS_LSB, 4) and 0xB0. So, what does bitlst(THIS_LSB, 4) do?
1
2
3
4
5
6
7
8
9
10
11
12
* THIS_LSB = bitext(char, 0, 4)
* bitext = (x >> y) & int(math.pow(2, z) - 1) # the value of z will be overriden by the value of the caller function
→ THIS_LSB = (char >> 0) & int(math.pow(2, 4) - 1) =
= char & int(math.pow(2, 4) - 1) =
= char & 15
* bitlst = lambda x, y: x << y # SWAP! Left shift, moves x to the left y places
bitlst(THIS_LSB, 4) = bitext(char, 0, 4) =
= THIS_LSB << 4 = # SWAP! << 4 converts the 4 least significant bits of the plaintext char (pt) into the 4 most significant bits of the encrypted char (ct)
= (char & 15) << 4
Let’s see it in action:
1
2
3
4
5
6
7
8
9
10
11
pt = char = 'A' = 0100 0001
0100 0001 char
AND 0000 1111 15 *
-------------
0000 **0001** char & 15 = THIS_LSB
**0001** 0000 bitlst(THIS_LSB) = THIS_LSB << 4 # SWAP!
XOR 1011 0000 0xB0
-------------
1010 0000 bitlst(THIS_LSB) ⊕ 0xB0 = right XOR
*As (0&0) = 0 and (0&1) = 1 and we are working only with the four least significant bits, this function does nothing and we can skip it to reverse this part
ct = chr(OR[XOR(THIS_MSB, 0x0D), XOR(bitlst(THIS_LSB, 4), 0xB0)])
1
2
3
ct = chr(OR[XOR(THIS_MSB, 0x0D), XOR(bitlst(THIS_LSB, 4), 0xB0)])
= chr(OR[XOR(THIS_MSB, 0x0D), XOR(bitlst(THIS_LSB, 4), 0xB0)])
= chr(OR(left XOR, right XOR))
1
2
3
4
0000 1001 left XOR
OR 1010 0000 right XOR
---------
1010 1001 ct = ©
Reversing the Left XOR: XOR(THIS_MSB, 0x0D)
We part from ct = 1010 1001, and we know that the Left XOR give rise to the four least significant bits of the encrypted character (ct), so, first, we will clear the four most significant bits of the ct:
1
0000 1001 ct masked # Four most significant bits cleared
Now, we need to reverse the XOR with 0x0D, in order to do this (and by the properties of XOR: pt ⊕ key = ct → ct ⊕ key = pt
) we only need to XOR it with 0x0D again:
1
2
3
4
0000 1001 ct masked
XOR 0000 1101 0x0D
---------
0000 0100 pt upper bits without swapping
Finally, we need to reverse the swapping to make the four least significant bits of the ct becomes again the four most significant bits of the pt:
1
0100 0000 pt upper bits without swapping << 4 = pt upper bits
Reversing the Right XOR: XOR(bitlst(THIS_LSB, 4), 0xB0))
We part from ct = 1010 1001, and we know that the Right XOR give rise to the four most significant bits of the encrypted character (ct), so, first, we will clear the four least significant bits of the ct:
1
1010 0000 ct masked (four least significant bits cleared)
Now, we need to reverse the XOR with 0xB0, in order to do this (and by the properties of XOR: pt ⊕ key = ct → ct ⊕ key = pt
) we only need to XOR it with 0xB0 again:
1
2
3
4
1010 0000 ct masked
XOR 1011 0000 0xB0
---------
0001 0000 pt lower bits without swapping # = bitlst(THIS_LSB, 4) = THIS_LSB << 4
Finally, we need to reverse the swapping to make the four most significant bits of the ct becomes again the four least significant bits of the pt:
1
0000 0001 pt lower bits without swapping >> 4 = pt lower bits
Reversing the OR (getting the pt): chr(OR[XOR(THIS_MSB, 0x0D), XOR(bitlst(THIS_LSB, 4), 0xB0)])
To reverse the OR we only need to apply the OR again:
1
2
3
4
0100 0000 pt upper bits
OR 0000 0001 pt lower bits
---------
0100 0001 = pt = 'A'
And… We have our pt!
The full InvertESwapChar() function:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def InvertESwapChar(ct):
# Left side -> Revert (THIS_MSB xor 0x0D)
pt_upper = ValidateChar(ct)
# Convert 4 upper bits to 0 (Masking pt_upper)
for i in range(4, 8):
pt_upper = pt_upper & ~(1 << i)
pt_upper = pt_upper ^ 0x0D
pt_upper = pt_upper << 4
# Right side -> Revert (bitlst(THIS_LSB, 4) xor 0xB0)
pt_lower = ValidateChar(ct)
# Convert 4 lower bits to 0 (Masking pt_lower)
for i in range(4):
pt_lower = pt_lower & ~(1 << i)
pt_lower = pt_lower ^ 0xB0 # = bitlst
pt_lower = pt_lower >> 4 # bitlst >> 4
pt = pt_upper | pt_lower
return chr(pt)
To solve this challenge, I did a python script that you can see at: luna_decrypt.py
Enjoy! ;)