0

I've reversed the following algorithm from a challenge binary I'm investigating:

def encrypt(plain):
    l = len(plain)
    a = 10
    cipher = ""

    for i in range(0, l):
        if i + a < l - 1:
            cipher += chr( xor(plain[i], plain[i+a]) )
        else:
            cipher += chr( xor(plain[i], plain[a]) )

        if ord(plain[i]) % 2 == 0: a += 1 # even
        else: a -= 1 # odd

    return cipher

from binascii import hexlify
print hexlify(encrypt("this is a test string"))

Essentially, it XORs each character with another character in the string, offset by a. a initial value is 10, as the function iterates over the characters in the string, a +=1 if the character's value is even or a -= 1 if it's odd.

I've worked out in my head how to reverse this cipher and retrieve plain text, it would require the use of a recursive function to find out which character offsets are even/odd in the original string. IE: Given the properties of XOR % 2, we now that if cipher[0] is odd then either plain[0] or plain[10] is odd, but not both. Similarly if cipher[0] is even then both plain[0] and plain[10] are even, or both are odd. From there a recursive algorithm should be able to work the rest.

Once we know which characters in plaintext are even/odd, reversing the rest is trivial. I've spent a few hours working this out, but now I'm at loss implementing it.

I've used basic recursive algorithms in the past but never anything that "branches out" to solve something like this.

Given a cipher string resulting from this function, how could we use a recursive algorithm to determine the parity of each character in the original plain string?

EDIT: Sorry just to be clear and in response to a comment, after scratching my head on this for a few hours I thought the recursion strategy outlined above would be the only way to solve this. If not I'm open to any hints/assistance to solving the title question.

Juicy
  • 11,840
  • 35
  • 123
  • 212
  • You probably need to try something yourself and post that code here. – President James K. Polk Oct 04 '15 at 00:59
  • @JamesKPolk I wish I could but my attempts at this really led to nowhere and I'm afraid posting my attempts would just make this already long question look messier. I'm trying to punch slightly above my weight here although I hope to learn from any possible answers or hints. – Juicy Oct 04 '15 at 01:02
  • Well, does your answer need to use recursion, or do you just suspect that's the best way to proceed? – President James K. Polk Oct 04 '15 at 01:13
  • I'm pretty sure your cipher doesn't always have an unambiguous decryption. For instance, a plain text consisting of a single letter repeated will always create a ciphertext that is all zeros, regardless of what letter was in the plaintext. At best you can use the parities of the ciphertext to rule out some plaintext parity sequences. But, consider an all-even ciphertext. It could be created by an all-even or an all odd plaintext, and there's no way for you to tell which it was (without cracking them both and seeing if one is nonsense). – Blckknght Oct 04 '15 at 01:36
  • I updated my answer below with better code, just wanted to let you know. And due to Blckknight's comment, I made sure the solution will return all possible parities that could produce the cipher text. Not sure how you would guess from there. – Caleb Mauer Oct 04 '15 at 21:12

1 Answers1

1

You can solve this problem with what is known as recursive backtracking. Make an assumption then go down that path until you have decrypted the string or you've reached a contradiction. When you reach a contradiction you then return failure and the calling function will try the next possibility. If you return success, then return success to the caller.

I'm sorry but I couldn't resist trying to solve this. Here's what I came up with:

# Define constants for even/odd/notset so we can use them in a list of
# assumptions about parity.
even = 0
odd = 1
notset = 2

# Define success and failure so that success and failure can be passed
# as a result.
success = 1
failure = 0

def tryParity(i, cipher, a, parities, parityToSet):
    newParities = list(parities)
    for j, p in parityToSet:
        try:

            if parities[j] == notset:
                newParities[j] = p
            elif parities[j] != p:
                # Failure due to contradiction.
                return failure, []

        except IndexError:
            # If we get an IndexError then this can't be a valid set of values for the parity.
            # Error caused by a bad value for "a".
            return failure, []

    # Update "a" based on parity of i
    new_a = a+1 if newParities[i] == even else a-1

    return findParities(i+1,cipher,new_a,newParities)


def findParities(i, cipher, a, parities):
    # Start returning when you've reached the end of the cipher text.
    # This is when success start bubbling back up through the call stack.
    if i >= len(cipher):
        return success, [parities] # list of parities

    # o stands for the index of the other char that would have been XORed.
    # "o" for "other"
    o = i+a if i + a < len(cipher)-1 else a

    result = None
    resultParities = []
    toTry = []

    # Determine if cipher[index] is even or odd
    if ord(cipher[i]) % 2 == 0:
        # Try both even and both odd
        toTry = (((i,even),(o,even)),
                 ((i,odd),(o,odd)))

    else:
        # Try one or the other even, one or the other odd
        toTry = (((i,odd),(o,even)),
                 ((i,even),(o,odd)))


    # Try first possiblity, if success add parities it came up with to result
    resultA, resultParA = tryParity(i, cipher, a, parities, toTry[0])

    if resultA == success:
        result = success
        resultParities.extend(resultParA)

    # Try second possiblity, if success add parities it came up with to result
    resultB, resultParB = tryParity(i, cipher, a, parities, toTry[1])

    if resultB == success:
        result = success
        resultParities.extend(resultParB)

    return result, resultParities

def decrypt(cipher):
    a = 10
    parities = list([notset for _ in range(len(cipher))])

    # When done, possible parities will contain a list of lists,
    # where the inner lists have the parity of each character in the cipher.
    # Comes back with mutiple results because each 
    result, possibleParities = findParities(0,cipher,a,parities)

    # A print for me to check that the parities that come back match the real parities
    print(possibleParities)
    print(list(map(lambda x: 0 if ord(x) % 2 == 0 else 1, "this is a test string")))

    # Finally, armed with the parities, decrypt the cipher. I'll leave that to you.
    # Maybe more recursion is needed

# test call
decrypt(encrypt("this is a test string"))

It seems to work, but I didn't try it on any other inputs.

This solution only gives you the parities, I left the decryption of the characters up to you. They could probably be done together, but I wanted to concentrate on answering your question as asked. I used Python 3 because it's what I have installed.

I'm a beginner in this area also. I recommend reading a Peter Norvig book. Thanks for the tough question.

Caleb Mauer
  • 662
  • 6
  • 11