Crypto / Crapto Century

This challenge was called crypto century in the associated forum post but the downloadable file was called crapto century which I don't think was a typo. I discussed this challenge with the author and it has its origin in real code found in a real pen test. In the defence of the people who were/are? using this code it appears to pre-date the widespread availability of modern crypto. I suppose it prevents someone from immediately recognizing the meaning of data but it doesn't impart any reliable security at all.

The challenge involved two files, one in ascii encoded binary and one code file. I'm not sure what to call the language it's written in. It generally appears to be SQL, but there is a lot of formatted metadata that I don't recognize. The database it was written for is Gupta SQLBase, so I guess I'd call it SqlBase or Gupta SQLBase or just SQL with the dialect being SQLBase. Anyway the ascii encoded binary isn't very interesting yet so the first thing to do is to look at the code file.

 1Function: Encrypt
 2 Returns
 3     String:
 4 Parameters
 5     String: clearPassword
 6 Static Variables
 7 Local variables
 8     String: binaryNumber
 9     String: preSwapBinaryText
10     String: encryptPassword
11     Number: charASCIINum
12     Number: length
13     Number: origLength
14     Number: index
15     Number: swapPosition
16     Number: position
17 Actions
18     Set length = SalStrLength( clearPassword )
19     Set origLength = length
20     While index < length
21         Set charASCIINum = SalStrLop( clearPassword )
22         Set binaryNumber = libDecimalToBinary( charASCIINum )
23         Set preSwapBinaryText = preSwapBinaryText || binaryNumber
24         Set index = index + 1
25     Set length = SalStrLength( preSwapBinaryText )
26     While SalStrLength( preSwapBinaryText ) < 70
27         Set binaryNumber = libDecimalToBinary( 90 + SalNumberTruncate( SalStrLength( preSwapBinaryText) / 2, 2, 0 ) )
28         Set preSwapBinaryText = preSwapBinaryText || binaryNumber
29     Set swapPosition = 2 + SalStrToNumber( SalStrMidX( preSwapBinaryText, 5, 1 ) )
30     Set length = SalStrLength( preSwapBinaryText )
31     Set index = 0
32     Loop
33         Set encryptPassword = encryptPassword || SalStrMidX( preSwapBinaryText, position, swapPosition - 1 )
34         Set position = position + swapPosition -1
35         Set encryptPassword = encryptPassword || SalStrMidX( preSwapBinaryText, position +1 , 1)
36         Set encryptPassword = encryptPassword || SalStrMidX( preSwapBinaryText, position  , 1)
37         Set position = position + 2
38         If position >= length
39             Break
40     Return encryptPassword

Right at the top in the formatted metadata we can see that this is a function that's part of a larger system, it returns a String and it takes a String called clearPassword. Presumably this code was used to hide passwords that otherwise would have been stored in plaintext. We then have the variable list but I find it's better to just skip ahead and see what the code is doing with those variables. In the code variables are used before being given values so we can safely assume they are automatically set to 0 if they are numbers or empty strings if they are strings.

On line 18 we see a SalStrLop function and then scanning the rest of the code we can see that function is used multiple times and there are a number of other functions starting with Sal. This makes it apparent that there is some sort of library in play that is using Sal as a prefix. Googling any one of these functions will bring you to a developer manual.

After finding the developer manual I found it useful to jot down some notes on what the functions do (I actually didn't bother with SalStrLength and SalStrToNumber which seemed obvious, but I've added them here for quick reference):

  • SalStrLop - take the first character in ascii (by take I mean to remove it form the string and return it)

  • SalStrLength - return the length of an ascii string

  • SalStrToNumber - convert an ascii string to a number

  • SalNumberTruncate (number, precision, scale) number to truncate, number of digits to display from the left, number of digits after decimal place

  • SalStrMidX( source, start, length) (return a string from the middle of the source string with the specified start and length)

There is another function libDecimalBinary, but I couldn't find references to it online so I assumed I was meant to figure out what it did from its name and the variables involved in calling it. It takes a number that is acquired by converting an ascii character to a number and generates a string that is then appended to another string, so clearly it takes a decimal number and converts it into binary ascii (like 4 -> "10"). At this point we don't know for sure how libDecimalBinary is handling leading 0s. Do all the values come out padded to 8 bits? Are they all 7 bits as required for ascii? Are some less than 7 bits? "!" for example is 33 in decimal and can be represented with only 6 bits. There is no way to know for sure the best thing to do is note an assumption and come back to it if required. Given the apparent age of the code I assumed it would use 7 bits per ascii character and moved on (this turned out to be correct).

So now that all the functions are covered off we look at the loop from line 20 to line 24. For every character in the password (see lines 18 and 19 for why this is the correct way to interpret the loop,) we take the character in question, convert it to binary ascii and append it to the end of the preSwapBinaryText variable ( note that the || symbol means append in SQL and other common meanings don't fit well with what's going on).

Now the clearPassword string is empty and we have the same information but in ascii binary. The next loop is on lines 26-28. This loop does some math and appends some calculated values to the end of our new binary ascii string, but it only does this until the string is 70 characters long. When I was figuring this code out I decided to ignore and come back to it as the value I wanted would be the first part of the binary ascii string anyway. Under further consideration since the binary ascii string given with the challenge was longer than 70 characters and the length of the string isn't altered by any future code it is guaranteed that this loop never executed at all. So what does it do? nobody cares.

On line 30 we can see swapPosition is 2 plus a one character substring of the binary ascii string starting at position 5. We don't know for sure what this value will be, but we do know that it is either a 0 or a 1 because everything in preSwapBinary is in binary. We could look at the binary value we have and consider how it gets altered to determine if this is a 0 or a 1, but it makes much more sense to just remember to try both.

Then from line 32 to line 39 the code goes through the binary ascii string a bit at a time. Rather than taking chunks of the string using SalStrLop the code uses a position variable to keep track of where in preSwapBinaryText it is working from, takes the characters up to the next swapPosition and then takes the next two characters in swapped order. So a swapPosition of 2 means to swap the 2nd character and every 3rd character after that, a swapPosition of 3 means to swap the 3rd character and every 4th character after that... and that's basically it.

The most difficult part of coding something to reverse this process is making sure to consider off by one errors. Python uses start and end indexes including the start and excluding the end while the code uses position and length so it is kind of annoying but not very complicated. Once all the bits are unswapped we just need to convert back from binary to bytes and look at the ascii. I'm confident there is a better way to do this, but I knew how to hack this together really quickly using eval and 0b notiation in python and in a CTF there are no points for form. The below code will get you the flag. The blob variable is just set to a copy paste of the binary ascii value given as part of the challenge. I just tried do_swap with both 2 and 3 in ipython, but added the solve code in an if __name__ == "__main__" block for the purposes of this write up.

Edit: I updated my description of the algorithm. A swapPosition of 2 doesn't actually swap every 2nd bit because the bits swapped into the 2nd bit location are then skipped so it swaps the 2nd bit then every third bit afterwards and likewise for the 3rd bits. You could also say that it processes chunks of swapPosition+1 bits at a time swapping the swapPosition bit with the swapposition+1 bit. Anyway the code works :P

 1blob = b"1000111000110001000011000111011010011110001100001101011011000011100001110010010100010101101010100110101011010010110010001110111010010101011001010101110100101010001100100111001110001010100010110001010100111010101011001011000101010110100011101011110101000101110"    
 2
 3def unbin(binpass:str):
 4    outpass = ''
 5    while len(binpass) >= 7:
 6        binVal = binpass[:7]
 7        binpass = binpass[7:]
 8        outpass += chr(eval(f"0b{binVal.decode()}"))
 9    return outpass
10
11def do_swap(passwd:str, swapPos:int):
12    outpass = b''
13    while len(passwd) > 0:
14        try:
15            outpass += passwd[:swapPos-1] + passwd[swapPos:swapPos+1] + passwd[swapPos-1:swapPos]
16            passwd = passwd[swapPos+1:]
17        except Exception as e:
18            print(e)
19            break
20    return outpass
21
22
23if __name__ == "__main__":
24    print(unbin(do_swap(blob,3)))