LockBox Write-up

By
Febna V M
Published on
20 Nov 2021
15 min read
DOMECTF2021

Story

Edward received a lockbox with a message

Hello, Ed. I hope you received the box. I set the key to be sent after one year. However, you may attempt to break into it before receiving the key.

Solution

This challenge is built in Python.

On executing the given file in a terminal, it will be prompted to enter a key.

lockbox1

We can see that it returns some output while giving an input. This means that the input is being validated based on certain conditions within the code.

We can use reverse engineering tools such as Ghidra, objdump, etc to reverse the executables and to find out the functioning of the code.

On checking the reverse-engineered code, we can arrive at the conclusion that:

As the first step, the code validates the length of the key entered with the code given below:

        if(len(password[::-2]) != 16 or len(password[21:]) != 11): 
      		  print("Woah, you're not even close!!") 
       		  return False 
   

From the code, we can conclude that the required key length is 32 and if we do not meet it, it will return a false statement.

Here, the key is divided into 4 blocks of equal length, and 8 characters long.

(Analyzing the code)

Block1

        block1 = 'domectf'.join([chr(0x92 - ord(password[c])) 
        for c in range(0, int(pwlen / 4))]) 
            if "".join([c for c in block1[::8]]) != '=N"Y < 8:F': 
                return False 
        else: 
            print("Hey..you are close!!") 
   

We can break the functioning of the above-given code into 4 pieces for simplification:

  1. The Unicode code of each character from block1 is subtracted from 0x92 (which is equal to 146 in decimal).

  2. Then the string ‘domectf’ is attached to each character.

  3. And then, every 8th character of the resultant block1 is taken and combined to form a string and is matched with =N"Y<8:F

  4. Then it will return a false statement if both the strings aren’t a match.

Now we have got a string to compare with the proceeding blocks.

        block1 =   
        cipher = '=N"Y < 8:F'
        for i in range(len(cipher)): 
            x = 146 - ord(cipher[i]) 
            block1 += chr(x)  
   

In the given code, the program is retrieving the first block of the key from =N"Y < 8:F. This is by taking the decimal value of each character in the string and then comparing and subtracting it from 146.

With this, the first block, Dp9VZXL, can be found.

Block2

Certain operations are done on block2 prior to the next check.

        block2 = [ord(c) - 0x1C if 2 * ord(c) < 0xA0 
        else (ord(c) + 0x1C if (2 * ord(c)) > 0xC8 else ord(c)) 
            for c in password[int(pwlen / 4): int(2 * pwlen / 4)]] 
                keys = [102, -247, 54, -231, -35, -8, -88, -168] 
            for i in range(0, len(block2)): 
  	            if(0 if i == len(block2) - 1 else block2[i + 1]) != (2 * block2[i]) + keys[i]: 
                    print("Oops...!, try again! " + str(i)) 
                    return False 
    

By analyzing the above-mentioned code, we can understand its functioning as that an array (keys = [102, -247, 54, -231, -35, -8, -88, -168]) is given and it checks whether the sum of twice the ASCII value of each character in block2 and the element in the array with the same index returns the ASCII value of the next character in block2. And this loop will execute until it reaches the last character within the block.

        keys = [102, -247, 54, -231, -35, -8, -88, -168] 
        block2array = [0, 0, 0, 0, 0, 0, 0, 84] 
        for i in range(len(block2array) - 1, 0, -1): 
            block2array[i - 1] = (block2array[i] - keys[i - 1]) // 2 
            block2 = "" 
        for i in range(0, len(block2array)): 
            if 80 < int(block2array[i]) < 100: 
                block2array[i] = chr(block2array[i]) 
            elif (chunk2array[i] + 0x1C) > 0X64: 
                block2array[i] = chr(chunk2array[i] - 0X1c) 
            elif (chunk2array[i] - 0x1C) < 0x50: 
                block2array[i] = chr(chunk2array[i] + 0x1C) 
                block2 +=block2array[i] 
    

The code finds block2 of our key and hence, we can conclude that block2 is 1tElEKVT.

Block3

        broken_key = ['\x18', ')', '!', '0', '\x11', '/', 'D', 'A'] 
        block3 = [chr(ord(broken_key[i]) + 0X20) for i in range(len(broken_key))]
    

Here, the third block of the key is converted into another array by subtracting 0x20 (which is equal to 32 in decimal) from the ASCII value of each character. This returns a string.

Hence, finding block3 is a lot simpler than you thought. That is by adding 32 to each ASCII value and then converting it back to a character from its Unicode codes.

Block4

Finding this block might look like a harder one. But in fact, it is a lot easier if you recall base conversion methodology.

The final block of code looks like:

        block4 = password[int(3 * pwlen / 4):] 
        code =             193467235523314909552858807952825905707585690687056221482407486215822962362081901228539668218909561858261716398442106581207560945004074744751468435208638309807448792333157151318042827086034349204606797072878208156712781015117915286904063496924417175941657872595681280 
        for i in range(0, len(block4)): 
            if(ord(block4[i]) < 0x96): 
                code -= (512 ** (0x96 - ord(block4[i])) * i) 
            if code == 0: 
                print("Key accepted!") 
                print("Here is your flag..." + "domectf{" + password + "}") 
                return True 
            else: 
                print("Quite wrong indeed!") 
                return False 
    

The functioning of code is:

A large number is given and on each iteration, the value obtained by multiplying the index of the value of each 512 to the power of 0x96 minus the Unicode value of each character in the block, is subtracted from the large number.

TRUE expression is returned if the code becomes zero.

i.e:

    Let the decimal array for block4 be = [x0, x1, x2, x3, x4, x5, x6, x7] 

    Code = 512 **(150  x0) * 0 + 512 **(150  x1) * 1+ 512 **(150  x2) * 2 +  

	512 **(150  x3) * 3 + 512 **(150  x4) * 4 + 512 **(150  x5) * 5 + 

	512 **(150  x6) * 6 + 512 **(150  x7) * 7
    

Here it is kept on adding 512 to the power of the value of 0x96 (150) minus the value of character is multiplied with the index value of character for every iteration. The resultant value is the large number code.

Simply let us understand this with an example of conversion of (153)16 to base 10 using the equation:

3 * (160) + 5 * (161) + 1 * (162)

From this, we can conclude that the large number code is a decimal number, which is obtained by converting a number with base 512 and 0, 1, 2, 3, 4, 5, 6, 7 as individual digits in it.

And 150 – x0, 150 – x1, ……, 150 – x7 will be the position of these digits, and the remaining digits are 0s.

The large number code is:

code = 193467235523314909552858807952825905707585690687056221482407486215822962362081901228539668218909561858261716398442106581207560945004074744751468435208638309807448792333157151318042827086034349204606797072878208156712781015117915286904063496924417175941657872595681280

And with the code given below,

        def numberToBase(n, b): 
        if n == 0: 
            return [0] 
            digits = [] 
            while n: 
                digits.append(int(n % b)) 
                n //= b 
                return digits[: : -1] 
            truenum = numberToBase(code, 512) 
    

We can write the large number code as

code = 512 **(150 – 0) * 0 + 512 **(150 – 1) * 0+ …......+ 512 **(150 – x2) * 2 + 512 **(150 – x3) * 3 + 512 **(150 – x4) * 4 + 512 **(150 – x5) * 5 +512 **(150 – x6) * 6 + 512 **(150 – x7) * 7 + …+ 512 ** (150-150) *0

And the output of all the digits together will give the base 512 to the version of our large number code, which looks like

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 1, 0, 0, 0, 3, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

And in considering the character value of the positions of numbers from 1 to 7, it will return the string SvWfQ4X.

Since there is more number of 0s, its position is unknown. Hence, we need to brute-force the character at position 0 from the list of combinations of a-z, A-Z, and 0-9.

Capturing the flag

The FLAG domectf{UDp9VZXL1tElEKVT8IAP1OdarSvWfQ4X} can be obtained by combining all the blocks we have derived.

Automated human-like penetration testing for your web apps & APIs
Teams using Beagle Security are set up in minutes, embrace release-based CI/CD security testing and save up to 65% with timely remediation of vulnerabilities. Sign up for a free account to see what it can do for you.

Written by
Febna V M
Febna V M
Cyber Security Engineer
Experience the Beagle Security platform
Unlock one full penetration test and all Advanced plan features free for 10 days
Find surface-level website security issues in under a minute
Free website security assessment
Experience the power of automated penetration testing & contextual reporting.