Jump to content
Tuts 4 You

Python Obfuscated KeygenMe


musava_ribica
Go to solution Solved by Extreme Coders,

Recommended Posts

musava_ribica

Python Obfuscated KeygenMe


I have provided a zip archive which contains two .py files, as seen in the screenshot. In LicenseChecker.py there is a validation function invoked by main.py where you put the key in the given format.

The python code is obfuscated using a obfuscator I'm currently working on. If you solved the challenge, or have any feedback to give, or need hints/help with reversing without spoiling, please let me know. Thanks.

 

You can see original source code and the solution in the 'Authors Code' and 'Authors Solution', if you wish.

Good luck!


 

  • Like 3
Link to comment
Share on other sites

  • 2 weeks later...
  • Solution
Extreme Coders
Teddy Rogers
This post was recognized by Teddy Rogers!

Extreme Coders was awarded the badge 'Great Content' and 1 points.

The KeygenMe or more appropriately a CrackMe (since it accepts just a single key) is protected with virtualization based obfuscation. This mini writeup describes a way to obtain the correct key without devirtualization.

I - First Steps

There are two files - main.py & LicenseChecker.py of which the latter is additionally minified. To improve readability we can run the file through a beautifier like black to get the following code.

https://gist.github.com/extremecoders-re/35cb06674676afdcf85bd19d0793d6cc

II - Overview

The list variable C holds the bytecode for the VM.

C=[82,26,95,26,95,26,105, .... snip....   ,571,84,572,84,393,129,3,101,84,103,84,573,76,1,134]

The dictionary variable W near the end contains the mappings from the instruction opcode to the corresponding handlers. There are 70 handlers which imply there are the same number of instructions.

W = {
    10: A0,
    179: f,
    36: AT,
    168: g,
# ... snip ...
    162: A9,
    113: A6,
    197: c,
    215: AI,
}

The while loop at the end is the VM fetch-decode-execute loop.

while B.a < L(C):
    try:
        W[C[B.a]]()
    except Z as X:
        A = [X]
        if not G:
            raise X
        P, Ag = G.pop()
        while F:
            Ah, Ai, Aj = F and F[-1] or (0, 0, 0)
            if Ah <= P:
                break
            F.pop()
        B.a = P + Ag
    B.a += 1

There is a similar loop in one of the handlers AW which implies this must be implementing function calls.

Spoiler
def AW():
    B.a += 3
    E = C[B.a - 2]
    J = C[B.a - 1]
    D = B.a

    def H(*P, **Q):
        H.flags & 1 and A.append(P)
        H.flags & 2 and A.append(Q)
        M.append((D, E))
        R = B.a
        B.a = D
        L = D + E
        K = I
        while B.a < L:
            try:
                if W[C[B.a]]():
                    K = A.pop()
                    break
            except Z as N:
                A.append(N)
                if not G:
                    raise N
                J, O = G.pop()
                while F:
                    S, T, U = F and F[-1] or (0, 0, 0)
                    if S > J:
                        F.pop()
                    else:
                        break
                if J < D < B.a < D + E < J + O:
                    M.pop()
                B.a = J + O
                if not D <= B.a < L:
                    return K
            B.a += 1
        else:
            M.pop()
        B.a = R
        return K

    H.flags = J
    B.a = D + E - 1
    A.append(H)

 

III - Simplifying the VM

The VM supports 70 instructions but not all of them are used. Hence we can remove the unused handlers to simplify the code. This can be done manually in a trial and error way or we can also automate it by logging which handler  executes and remove the others.

Eventually we are left with 18 handlers which after renaming are as follows.

W = {
    2: h2,
    19: h19,
    26: h26,
    33: h33,
    41: h41,
    76: h76,
    82: h82,
    84: h84,
    88: h88,
    101: h101,
    109: h109,
    112: h112,
    113: h113,
    117: h117,
    129: h129,
    131: h131,
    134: h134,
    139: h139,
}

Full simplified code: https://gist.github.com/extremecoders-re/8962f5faefcd714ce5336461fe670c06

IV - Tracing the VMCALL instruction

With 18 handlers left we can now trace the VM. An important thing to note is the obfuscator must have a way to call non-obfuscated external functions such as those from the standard library. If we log the external function it calls, the logic of the crackme would be clear.

The instruction with opcode 76 implements the VMCALL instruction.

def h76():
    vmctx.pc += 1
    E = G.copy()
    D = bc[vmctx.pc]
    F = A.pop() if D & 1 else ()
    H = A.pop() if D & 2 else {}
    I = A.pop()(*(F), **H)
    J = G.copy()
    E == J and A.append(I)

We can introduce a logging statement just before the call as shown. below.

def h76():
    vmctx.pc += 1
    E = G.copy()
    D = bc[vmctx.pc]
    F = A.pop() if D & 1 else ()
    H = A.pop() if D & 2 else {}

    # Logging the external function name and arguments
    print(A[-1].__name__, F, H)

    I = A.pop()(*(F), **H)
    J = G.copy()
    E == J and A.append(I)

V - Retrieving the correct key

Running with the serial and the VMCALL logging in place

verify("ABCDE-FGHIJ-KLMNO-PQRST-UVWXY")

we get a trace, of which the important parts are shown below.

getitem (['ABCDE', 'FGHIJ', 'KLMNO', 'PQRST', 'UVWXY'], 0) {}
getattr ('ABCDE', 'encode') {}
encode () {}
getattr (<module 'hashlib' from '/usr/lib/python3.10/hashlib.py'>, 'md5') {}
openssl_md5 (b'ABCDE',) {}
getattr (<md5 _hashlib.HASH object @ 0x7f335f0850f0>, 'digest') {}
digest () {}
list ((253, 101, 190, 39, 10, 139, 237, 181, 248, 22, 251, 138, 86, 113, 116, 52),) {}
bytes ([253, 101, 190, 39, 10, 139, 237, 181, 248, 22, 251, 138, 86, 113, 116, 52],) {}
eq (b'.\xcd\xde9Y\x05\x1d\x91?a\xb1Ey\xea\x13m', b"\xfde\xbe'\n\x8b\xed\xb5\xf8\x16\xfb\x8aVqt4") {}
not_ (False,) {}

It calculates the md5 of the first word -> openssl_md5("ABCDE") which is then compared to b"\xfde\xbe'\n\x8b\xed\xb5\xf8\x16\xfb\x8aVqt4". This can be converted to hex representation.

>>> print(b"\xfde\xbe'\n\x8b\xed\xb5\xf8\x16\xfb\x8aVqt4".hex())
fd65be270a8bedb5f816fb8a56717434

The MD5 hash can be reversed with any online tool such as https://hashes.com/en/decrypt/hash  &  https://crackstation.net/

image.png.355b433aa2d1c21fd2eb50999dcd5975.png

The first word is thus CONGR.

Re-running with the following key we get another trace.

verify("CONGR-FGHIJ-KLMNO-PQRST-UVWXY")
getitem (['CONGR', 'FGHIJ', 'KLMNO', 'PQRST', 'UVWXY'], 1) {}
getitem ('FGHIJ', 1) {}
eq ('G', 'T') {}

Here we see it taking the second word in the key viz FGHIJ and comparing the second character in the word G with T.
Thus the correct character at that place is T.

Since it stops comparing further letters as soon as a mismatch is found we can only recover the key character by character. However there is a quicker way. ;)

We can override the result of the comparison to true such that all the checks are revealed at once. This can be done by a slight modification to the logging logic.

def h76():
    vmctx.pc += 1
    E = G.copy()
    D = bc[vmctx.pc]
    F = A.pop() if D & 1 else ()
    H = A.pop() if D & 2 else {}

    # Logging the external function name and arguments
    if A[-1].__name__ == "eq":
        print(A[-1].__name__, F, H)
        I = True
    else:
        I = A.pop()(*(F), **H)

    J = G.copy()
    E == J and A.append(I)

Running once more with the same key as last time we get the full trace as below.

eq (29, 29) {}
eq (5, 5) {}
eq (5, 5) {}
eq (5, 5) {}
eq (5, 5) {}
eq (5, 5) {}
eq (5, 5) {}
eq (b"\xfde\xbe'\n\x8b\xed\xb5\xf8\x16\xfb\x8aVqt4", b"\xfde\xbe'\n\x8b\xed\xb5\xf8\x16\xfb\x8aVqt4") {}
eq ('G', 'T') {}
eq ('O', 'S') {}
eq ('Q', 'O') {}
eq ('J', 'A') {}
eq ('H', 'U') {}
eq ('R', 'H') {}
eq ('K', 'T') {}
eq ('S', 'F') {}
eq ('F', 'A') {}
eq ('L', 'I') {}
eq ('T', 'A') {}
eq ('M', 'O') {}
eq ('I', 'L') {}
eq ('N', 'N') {}
eq ('P', 'Q') {}
eq ('UYXWV', 'BHZQB') {}

From the equality checks we can retrieve the 2nd, 3rd and 4th words in the key. The 1st word has already been retrieved before from the MD5 reversing.

CONGR-ATULA-TIONS-QOHFA-UVWXY

The fifth word is however checked in a different way.

The fifth word in the entered key was UVWXY. However it is checking UYXWV with BHZQB.

U -> B
Y -> H
X -> Z
W -> Q
V -> B

UYXWV is a permutation of the original letters UVWXY. Thus we can simply undo the above mapping in the proper order to get the correct word as shown below.

U -> B
V -> B
W -> Q
X -> Z
Y -> H

The correct last word is BBQZH and thus the complete key:

CONGR-ATULA-TIONS-QOHFA-BBQZH

  • Like 13
  • Thanks 5
Link to comment
Share on other sites

39 minutes ago, Extreme Coders said:

The KeygenMe or more appropriately a CrackMe (since it accepts just a single key) is protected with virtualization based obfuscation. This mini writeup describes a way to obtain the correct key without devirtualization.

I - First Steps

There are two files - main.py & LicenseChecker.py of which the latter is additionally minified. To improve readability we can run the file through a beautifier like black to get the following code.

https://gist.github.com/extremecoders-re/35cb06674676afdcf85bd19d0793d6cc

II - Overview

The list variable C holds the bytecode for the VM.

C=[82,26,95,26,95,26,105, .... snip....   ,571,84,572,84,393,129,3,101,84,103,84,573,76,1,134]

The dictionary variable W near the end contains the mappings from the instruction opcode to the corresponding handlers. There are 70 handlers which imply there are the same number of instructions.

W = {
    10: A0,
    179: f,
    36: AT,
    168: g,
# ... snip ...
    162: A9,
    113: A6,
    197: c,
    215: AI,
}

The while loop at the end is the VM fetch-decode-execute loop.

while B.a < L(C):
    try:
        W[C[B.a]]()
    except Z as X:
        A = [X]
        if not G:
            raise X
        P, Ag = G.pop()
        while F:
            Ah, Ai, Aj = F and F[-1] or (0, 0, 0)
            if Ah <= P:
                break
            F.pop()
        B.a = P + Ag
    B.a += 1

There is a similar loop in one of the handlers AW which implies this must be implementing function calls.

  Hide contents
def AW():
    B.a += 3
    E = C[B.a - 2]
    J = C[B.a - 1]
    D = B.a

    def H(*P, **Q):
        H.flags & 1 and A.append(P)
        H.flags & 2 and A.append(Q)
        M.append((D, E))
        R = B.a
        B.a = D
        L = D + E
        K = I
        while B.a < L:
            try:
                if W[C[B.a]]():
                    K = A.pop()
                    break
            except Z as N:
                A.append(N)
                if not G:
                    raise N
                J, O = G.pop()
                while F:
                    S, T, U = F and F[-1] or (0, 0, 0)
                    if S > J:
                        F.pop()
                    else:
                        break
                if J < D < B.a < D + E < J + O:
                    M.pop()
                B.a = J + O
                if not D <= B.a < L:
                    return K
            B.a += 1
        else:
            M.pop()
        B.a = R
        return K

    H.flags = J
    B.a = D + E - 1
    A.append(H)

 

III - Simplifying the VM

The VM supports 70 instructions but not all of them are used. Hence we can remove the unused handlers to simplify the code. This can be done manually in a trial and error way or we can also automate it by logging which handler  executes and remove the others.

Eventually we are left with 18 handlers which after renaming are as follows.

W = {
    2: h2,
    19: h19,
    26: h26,
    33: h33,
    41: h41,
    76: h76,
    82: h82,
    84: h84,
    88: h88,
    101: h101,
    109: h109,
    112: h112,
    113: h113,
    117: h117,
    129: h129,
    131: h131,
    134: h134,
    139: h139,
}

Full simplified code: https://gist.github.com/extremecoders-re/8962f5faefcd714ce5336461fe670c06

IV - Tracing the VMCALL instruction

With 18 handlers left we can now trace the VM. An important thing to note is the obfuscator must have a way to call non-obfuscated external functions such as those from the standard library. If we log the external function it calls, the logic of the crackme would be clear.

The instruction with opcode 76 implements the VMCALL instruction.

def h76():
    vmctx.pc += 1
    E = G.copy()
    D = bc[vmctx.pc]
    F = A.pop() if D & 1 else ()
    H = A.pop() if D & 2 else {}
    I = A.pop()(*(F), **H)
    J = G.copy()
    E == J and A.append(I)

We can introduce a logging statement just before the call as shown. below.

def h76():
    vmctx.pc += 1
    E = G.copy()
    D = bc[vmctx.pc]
    F = A.pop() if D & 1 else ()
    H = A.pop() if D & 2 else {}

    # Logging the external function name and arguments
    print(A[-1].__name__, F, H)

    I = A.pop()(*(F), **H)
    J = G.copy()
    E == J and A.append(I)

V - Retrieving the correct key

Running with the serial and the VMCALL logging in place

verify("ABCDE-FGHIJ-KLMNO-PQRST-UVWXY")

we get a trace, of which the important parts are shown below.

getitem (['ABCDE', 'FGHIJ', 'KLMNO', 'PQRST', 'UVWXY'], 0) {}
getattr ('ABCDE', 'encode') {}
encode () {}
getattr (<module 'hashlib' from '/usr/lib/python3.10/hashlib.py'>, 'md5') {}
openssl_md5 (b'ABCDE',) {}
getattr (<md5 _hashlib.HASH object @ 0x7f335f0850f0>, 'digest') {}
digest () {}
list ((253, 101, 190, 39, 10, 139, 237, 181, 248, 22, 251, 138, 86, 113, 116, 52),) {}
bytes ([253, 101, 190, 39, 10, 139, 237, 181, 248, 22, 251, 138, 86, 113, 116, 52],) {}
eq (b'.\xcd\xde9Y\x05\x1d\x91?a\xb1Ey\xea\x13m', b"\xfde\xbe'\n\x8b\xed\xb5\xf8\x16\xfb\x8aVqt4") {}
not_ (False,) {}

It calculates the md5 of the first word -> openssl_md5("ABCDE") which is then compared to b"\xfde\xbe'\n\x8b\xed\xb5\xf8\x16\xfb\x8aVqt4". This can be converted to hex representation.

>>> print(b"\xfde\xbe'\n\x8b\xed\xb5\xf8\x16\xfb\x8aVqt4".hex())
fd65be270a8bedb5f816fb8a56717434

The MD5 hash can be reversed with any online tool such as https://hashes.com/en/decrypt/hash  &  https://crackstation.net/

image.png.355b433aa2d1c21fd2eb50999dcd5975.png

The first word is thus CONGR.

Re-running with the following key we get another trace.

verify("CONGR-FGHIJ-KLMNO-PQRST-UVWXY")
getitem (['CONGR', 'FGHIJ', 'KLMNO', 'PQRST', 'UVWXY'], 1) {}
getitem ('FGHIJ', 1) {}
eq ('G', 'T') {}

Here we see it taking the second word in the key viz FGHIJ and comparing the second character in the word G with T.
Thus the correct character at that place is T.

Since it stops comparing further letters as soon as a mismatch is found we can only recover the key character by character. However there is a quicker way. ;)

We can override the result of the comparison to true such that all the checks are revealed at once. This can be done by a slight modification to the logging logic.

def h76():
    vmctx.pc += 1
    E = G.copy()
    D = bc[vmctx.pc]
    F = A.pop() if D & 1 else ()
    H = A.pop() if D & 2 else {}

    # Logging the external function name and arguments
    if A[-1].__name__ == "eq":
        print(A[-1].__name__, F, H)
        I = True
    else:
        I = A.pop()(*(F), **H)

    J = G.copy()
    E == J and A.append(I)

Running once more with the same key as last time we get the full trace as below.

eq (29, 29) {}
eq (5, 5) {}
eq (5, 5) {}
eq (5, 5) {}
eq (5, 5) {}
eq (5, 5) {}
eq (5, 5) {}
eq (b"\xfde\xbe'\n\x8b\xed\xb5\xf8\x16\xfb\x8aVqt4", b"\xfde\xbe'\n\x8b\xed\xb5\xf8\x16\xfb\x8aVqt4") {}
eq ('G', 'T') {}
eq ('O', 'S') {}
eq ('Q', 'O') {}
eq ('J', 'A') {}
eq ('H', 'U') {}
eq ('R', 'H') {}
eq ('K', 'T') {}
eq ('S', 'F') {}
eq ('F', 'A') {}
eq ('L', 'I') {}
eq ('T', 'A') {}
eq ('M', 'O') {}
eq ('I', 'L') {}
eq ('N', 'N') {}
eq ('P', 'Q') {}
eq ('UYXWV', 'BHZQB') {}

From the equality checks we can retrieve the 2nd, 3rd and 4th words in the key. The 1st word has already been retrieved before from the MD5 reversing.

CONGR-ATULA-TIONS-QOHFA-UVWXY

The fifth word is however checked in a different way.

The fifth word in the entered key was UVWXY. However it is checking UYXWV with BHZQB.

U -> B
Y -> H
X -> Z
W -> Q
V -> B

UYXWV is a permutation of the original letters UVWXY. Thus we can simply undo the above mapping in the proper order to get the correct word as shown below.

U -> B
V -> B
W -> Q
X -> Z
Y -> H

The correct last word is BBQZH and thus the complete key:

CONGR-ATULA-TIONS-QOHFA-BBQZH

This is what I call an actual solution. It explains every step in a proper manner so we can understand each and every action taken by you to solve it. 

 

Thank you.

  • Like 1
  • Thanks 1
Link to comment
Share on other sites

8 hours ago, Extreme Coders said:

The KeygenMe or more appropriately a CrackMe (since it accepts just a single key) is protected with virtualization based obfuscation. This mini writeup describes a way to obtain the correct key without devirtualization.

I - First Steps

There are two files - main.py & LicenseChecker.py of which the latter is additionally minified. To improve readability we can run the file through a beautifier like black to get the following code.

https://gist.github.com/extremecoders-re/35cb06674676afdcf85bd19d0793d6cc

II - Overview

The list variable C holds the bytecode for the VM.

C=[82,26,95,26,95,26,105, .... snip....   ,571,84,572,84,393,129,3,101,84,103,84,573,76,1,134]

The dictionary variable W near the end contains the mappings from the instruction opcode to the corresponding handlers. There are 70 handlers which imply there are the same number of instructions.

W = {
    10: A0,
    179: f,
    36: AT,
    168: g,
# ... snip ...
    162: A9,
    113: A6,
    197: c,
    215: AI,
}

The while loop at the end is the VM fetch-decode-execute loop.

while B.a < L(C):
    try:
        W[C[B.a]]()
    except Z as X:
        A = [X]
        if not G:
            raise X
        P, Ag = G.pop()
        while F:
            Ah, Ai, Aj = F and F[-1] or (0, 0, 0)
            if Ah <= P:
                break
            F.pop()
        B.a = P + Ag
    B.a += 1

There is a similar loop in one of the handlers AW which implies this must be implementing function calls.

  Reveal hidden contents
def AW():
    B.a += 3
    E = C[B.a - 2]
    J = C[B.a - 1]
    D = B.a

    def H(*P, **Q):
        H.flags & 1 and A.append(P)
        H.flags & 2 and A.append(Q)
        M.append((D, E))
        R = B.a
        B.a = D
        L = D + E
        K = I
        while B.a < L:
            try:
                if W[C[B.a]]():
                    K = A.pop()
                    break
            except Z as N:
                A.append(N)
                if not G:
                    raise N
                J, O = G.pop()
                while F:
                    S, T, U = F and F[-1] or (0, 0, 0)
                    if S > J:
                        F.pop()
                    else:
                        break
                if J < D < B.a < D + E < J + O:
                    M.pop()
                B.a = J + O
                if not D <= B.a < L:
                    return K
            B.a += 1
        else:
            M.pop()
        B.a = R
        return K

    H.flags = J
    B.a = D + E - 1
    A.append(H)

 

III - Simplifying the VM

The VM supports 70 instructions but not all of them are used. Hence we can remove the unused handlers to simplify the code. This can be done manually in a trial and error way or we can also automate it by logging which handler  executes and remove the others.

Eventually we are left with 18 handlers which after renaming are as follows.

W = {
    2: h2,
    19: h19,
    26: h26,
    33: h33,
    41: h41,
    76: h76,
    82: h82,
    84: h84,
    88: h88,
    101: h101,
    109: h109,
    112: h112,
    113: h113,
    117: h117,
    129: h129,
    131: h131,
    134: h134,
    139: h139,
}

Full simplified code: https://gist.github.com/extremecoders-re/8962f5faefcd714ce5336461fe670c06

IV - Tracing the VMCALL instruction

With 18 handlers left we can now trace the VM. An important thing to note is the obfuscator must have a way to call non-obfuscated external functions such as those from the standard library. If we log the external function it calls, the logic of the crackme would be clear.

The instruction with opcode 76 implements the VMCALL instruction.

def h76():
    vmctx.pc += 1
    E = G.copy()
    D = bc[vmctx.pc]
    F = A.pop() if D & 1 else ()
    H = A.pop() if D & 2 else {}
    I = A.pop()(*(F), **H)
    J = G.copy()
    E == J and A.append(I)

We can introduce a logging statement just before the call as shown. below.

def h76():
    vmctx.pc += 1
    E = G.copy()
    D = bc[vmctx.pc]
    F = A.pop() if D & 1 else ()
    H = A.pop() if D & 2 else {}

    # Logging the external function name and arguments
    print(A[-1].__name__, F, H)

    I = A.pop()(*(F), **H)
    J = G.copy()
    E == J and A.append(I)

V - Retrieving the correct key

Running with the serial and the VMCALL logging in place

verify("ABCDE-FGHIJ-KLMNO-PQRST-UVWXY")

we get a trace, of which the important parts are shown below.

getitem (['ABCDE', 'FGHIJ', 'KLMNO', 'PQRST', 'UVWXY'], 0) {}
getattr ('ABCDE', 'encode') {}
encode () {}
getattr (<module 'hashlib' from '/usr/lib/python3.10/hashlib.py'>, 'md5') {}
openssl_md5 (b'ABCDE',) {}
getattr (<md5 _hashlib.HASH object @ 0x7f335f0850f0>, 'digest') {}
digest () {}
list ((253, 101, 190, 39, 10, 139, 237, 181, 248, 22, 251, 138, 86, 113, 116, 52),) {}
bytes ([253, 101, 190, 39, 10, 139, 237, 181, 248, 22, 251, 138, 86, 113, 116, 52],) {}
eq (b'.\xcd\xde9Y\x05\x1d\x91?a\xb1Ey\xea\x13m', b"\xfde\xbe'\n\x8b\xed\xb5\xf8\x16\xfb\x8aVqt4") {}
not_ (False,) {}

It calculates the md5 of the first word -> openssl_md5("ABCDE") which is then compared to b"\xfde\xbe'\n\x8b\xed\xb5\xf8\x16\xfb\x8aVqt4". This can be converted to hex representation.

>>> print(b"\xfde\xbe'\n\x8b\xed\xb5\xf8\x16\xfb\x8aVqt4".hex())
fd65be270a8bedb5f816fb8a56717434

The MD5 hash can be reversed with any online tool such as https://hashes.com/en/decrypt/hash  &  https://crackstation.net/

image.png.355b433aa2d1c21fd2eb50999dcd5975.png

The first word is thus CONGR.

Re-running with the following key we get another trace.

verify("CONGR-FGHIJ-KLMNO-PQRST-UVWXY")
getitem (['CONGR', 'FGHIJ', 'KLMNO', 'PQRST', 'UVWXY'], 1) {}
getitem ('FGHIJ', 1) {}
eq ('G', 'T') {}

Here we see it taking the second word in the key viz FGHIJ and comparing the second character in the word G with T.
Thus the correct character at that place is T.

Since it stops comparing further letters as soon as a mismatch is found we can only recover the key character by character. However there is a quicker way. ;)

We can override the result of the comparison to true such that all the checks are revealed at once. This can be done by a slight modification to the logging logic.

def h76():
    vmctx.pc += 1
    E = G.copy()
    D = bc[vmctx.pc]
    F = A.pop() if D & 1 else ()
    H = A.pop() if D & 2 else {}

    # Logging the external function name and arguments
    if A[-1].__name__ == "eq":
        print(A[-1].__name__, F, H)
        I = True
    else:
        I = A.pop()(*(F), **H)

    J = G.copy()
    E == J and A.append(I)

Running once more with the same key as last time we get the full trace as below.

eq (29, 29) {}
eq (5, 5) {}
eq (5, 5) {}
eq (5, 5) {}
eq (5, 5) {}
eq (5, 5) {}
eq (5, 5) {}
eq (b"\xfde\xbe'\n\x8b\xed\xb5\xf8\x16\xfb\x8aVqt4", b"\xfde\xbe'\n\x8b\xed\xb5\xf8\x16\xfb\x8aVqt4") {}
eq ('G', 'T') {}
eq ('O', 'S') {}
eq ('Q', 'O') {}
eq ('J', 'A') {}
eq ('H', 'U') {}
eq ('R', 'H') {}
eq ('K', 'T') {}
eq ('S', 'F') {}
eq ('F', 'A') {}
eq ('L', 'I') {}
eq ('T', 'A') {}
eq ('M', 'O') {}
eq ('I', 'L') {}
eq ('N', 'N') {}
eq ('P', 'Q') {}
eq ('UYXWV', 'BHZQB') {}

From the equality checks we can retrieve the 2nd, 3rd and 4th words in the key. The 1st word has already been retrieved before from the MD5 reversing.

CONGR-ATULA-TIONS-QOHFA-UVWXY

The fifth word is however checked in a different way.

The fifth word in the entered key was UVWXY. However it is checking UYXWV with BHZQB.

U -> B
Y -> H
X -> Z
W -> Q
V -> B

UYXWV is a permutation of the original letters UVWXY. Thus we can simply undo the above mapping in the proper order to get the correct word as shown below.

U -> B
V -> B
W -> Q
X -> Z
Y -> H

The correct last word is BBQZH and thus the complete key:

CONGR-ATULA-TIONS-QOHFA-BBQZH

Amazing write-up, thank you!!

  • Like 1
  • Thanks 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...