Google CTF Quals 2018 - JS Safe

JavaScript Anti-Debug

General problem definition

You stumbled upon someone's "JS Safe" on the web. It's a simple HTML file that can store secrets in the browser's localStorage. This means that you won't be able to extract any secret from it (the secrets are on the computer of the owner), but it looks like it was hand-crafted to work only with the password of the owner...

The assignment was a Javascript file, which needs the Flag as input.

Getting to the flag-check

The function x is a oneliner, which does a few nasty things. There are some anti-debugging measures, for example a very long loop, that calls the debugger every iteration. Another overwrites the toString method of the object source, which would loop endlessly if printed.

Removing these codeparts causes the whole program to break, because the function hashes itself at position x = h(str(x));. This use of x does not use the parameter of the function, as one is the ASCII x and the other une is a Unicode cyrillic x.

In the function there are 2 subfunctions defined. The first one c xors a string with a second one(repeating if it is too small) and the second function h does a simple hash function over a string. source = /Ӈ#7ùª9¨M¤ŸÀ.áÔ¥6¦¨¹.ÿÓÂ.։£JºÓ¹WþʖmãÖÚG¤…¢dÈ9&òªћ#³­1᧨/; is an interesting line, as it creates a regex object. In the end return eval('eval(c(source,x))') is executed and should return true if the flag was entered correctly. Notice, that the input х was never used only the function x.

Now we looked at the nested eval statement. To find out what it results in, we reimplemented the whole function in python and evaluated c(source,x) there:

import string
a = 1000
b = 0
byteord = lambda b: bytes([b])
def hash(unicodestr):
    global a
    global b

    for char in unicodestr:
        a = (a + ord(char)) % 65521
        b = (b + a) % 65521

    print("end a:", a)
    print("end b:", b)
    return b.to_bytes(2, 'big') + a.to_bytes(2, 'big')

def crypt(enc, key):
    """
    enc: encrypted (unicode)
    key: key (bytes)
    """
    c = ""

    for i in range(len(enc)):
        c += chr(ord(enc[i]) ^ key[i % len(key)])

    return c
code = "function x(х){...}"
enc_code = "Ӈ#7ùª9¨M¤ŸÀ.áÔ¥6¦¨¹.ÿÓÂ.։£JºÓ¹WþʖmãÖÚG¤…¢dÈ9&òªћ#³­1᧨"

hashed_code = hash(code)
crypt_code = crypt(enc_code, hashed_code)

print(list(hashed_code))
print(crypt_code)

As a result we get the string: х==c('¢×&Ê´cʯ¬$¶³´}ÍÈ´T—©Ð8ͳÍ|Ԝ÷aÈÐÝ&þJ',h(х))//᧢. Finally the parameter x(our flag) is used.

Getting the Hash

The string х==c('¢×&Ê´cʯ¬$¶³´}ÍÈ´T—©Ð8ͳÍ|Ԝ÷aÈÐÝ&þJ',h(х))//᧢ is again evaluated. Now we just need to find an input were this condition is true. Looking down where the input to the function is checked we can see, that the regex /^CTF{([0-9a-zA-Z_@!?-]+)}$/ is used to check the flag, so we know that only digits, lowercase- and uppercase characters and _@!?- can be part of the flag. Now we can check for every possible byte in the 4 byte hash if every byte xored with every 4th byte in the '¢×&Ê´cʯ¬$¶³´}ÍÈ´T—©Ð8ͳÍ|Ԝ÷aÈÐÝ&þJ'-string is in our possible charset. This reduces the possible hashes that are possible.

# crypt_code is "х==c('¢×&Ê´cʯ¬$¶³´}ÍÈ´T—©Ð8ͳÍ|Ԝ÷aÈÐÝ&þJ',h(х))//᧢"
the_string = crypt_code[6:-10]
possibleset = string.ascii_letters + string.digits + "_@!?-"

for hashind in range(4):
    print("-----------------")
    for p in possibleset:
        testsol = ord(the_string[hashind]) ^ ord(p)
        for j in range(hashind + 4, len(the_string), 4):
            if chr(testsol ^ ord(the_string[j])) not in possibleset:
                break
        else:
            print(testsol)

As a result we get

-----------------
253
-----------------
149
153
-----------------
21
-----------------
249

as the output. That means there are only 2 possible hashes : [253, 149, 21, 249] and [253, 153, 21, 249]. Now we can just use the crypt function with the hashes and the '¢×&Ê´cʯ¬$¶³´}ÍÈ´T—©Ð8ͳÍ|Ԝ÷aÈÐÝ&þJ' string and we get our possible flags:

print(">" + crypt(crypt_code[6:-10], [253, 149, 21, 249]))
#>_B3x7!v3R91ON!h45!AnTE-4NXi-abt1-H3bUk_
print(">" + crypt(crypt_code[6:-10], [253, 153, 21, 249]))
#>_N3x7-v3R51ON-h45-AnTI-4NTi-ant1-D3bUg_

Only the second of the two actually looks readable, so the flag is CTF{_N3x7-v3R51ON-h45-AnTI-4NTi-ant1-D3bUg_}


Navigation