# HKCERT-CTF 2020 Sign in Please 請登入 (9) Sign in Please 請登入 500 Points / 1 Solves # Author Blackb6a # Solver T0022 Member 3 # Description I have implemented a secure authentication system. You can't eavesdrop the passwords, can you? 我發明了一個安全的身份驗證系統。反正沒有人能竊聽密碼吧。 nc tertiary.pwnable.hk 50001 # Files chall.py # Analyze Basically, the challenge is asking for the answer (auth) to the hash given the pbox and the salt, which we do not know in advance. Also, we are given the function (spy) to test out the hash calculated using our input (pbox and salt) and the unknown password. The whole string to test is 20-byte long, which composes of the random password (16 bytes) generated at the beginning of the connection and the salt (4 bytes), and the string gets shuffled by the given pbox. python def permutate(payload, pbox): return bytes([payload[x] for x in pbox])  Simply speaking, it is returning [payload[pbox[0]], payload[pbox[1]], payload[pbox[2]], ..., payload[pbox[len(pbox) - 1]]] in the form of bytes. Notice that the permutate process only retrieves the characters with the index given in the pbox, it does not check for the length of the pbox, or any repetition in the pbox. So initially I would think that it would be useful using a rainbow table to recover the password. The 16-byte password is generated by encoding a random 12-byte integer using base64, so only the 64 characters used in base64 ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") are possible to appear in the password part. For example, if we send $[0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1]$ as the pbox, it is supposed to permutate the payload to a 2-distinct(1-if-duplicated)-character-only string ($[p[0], p[1], p[0], p[1], ...]$), which generated hash can be checked against our rainbow table of every "2-character combination repeated 10 times" (only $64×64=4096$ combinations) to obtain the first 2 characters of the password. We have only 10 iterations available to test, so at least 2 characters have to be recovered per iteration to recover the password. A program is created to test this type of pbox, but 'no way!' in the exception part appears. To find out the issues, we can see that there is a verification check to the pbox. python assert len(set(pbox)) == 20 assert len(salt) == 4  It means that we can only use 4 characters for the salt, and pbox should contain exactly 20 different integers as indices. I was wondering if I can use different integers to obtain the same element from the payload array. The only possible integers I can think of is negative indices for counting from the end (e.g. 2 and -18 corresponding to the same element in a 20-byte payload) However, we only have control over the 4-byte salt. So at most we can use 8 bytes for the known characters in the payload, and the remaining 12 bytes are in the password, thus at least 6 elements have to be loaded from the password part. (e.g. pbox = $[0, 1, 2, 3, 4, 5, 16, 17, 18, 19, -1, -2, -3, -4, -15, -16, -17, -18, -19, -20]$) To create a rainbow table for 6 characters, we have to store $64^6 = 2^{36} = 68719476736$ values which takes so much time which makes this method impractical. So the idea of negative indices is abandoned. So it seems that our last resort is the SHA256 algorithm. SHA256, which generates a 256-bit hash as indicated in the name, algorithm breaks the payload into 512-bit (64-byte) blocks. The initial hash array is a 256-bit constant split into 8 parts for calculation. The initial hash array for the next block is the hash calculated from the previous block. As it seems that the default hashlib library does not provide the feature to use custom initial hash values, so I crafted my SHA256 library according to the SHA-2 documentation. Before the calculation, the payload is first padded to the length of a multiple of 512 bits (64 bytes). A '1' bit is appended to the payload first, then '0' bits are appended until there are still 64 bits to a multiple of 512 ($\text{length} \equiv 448\ (\text{mod }512)$), then the 64-bit representation of the length of the payload (in bits) is added at the end. For the default 20-byte payload, the padded payload should appear as below:  +---------------------+------------------+-------------------------------+------------------+ | the 20-byte payload | 128 (0b10000000) | 42-bytes of null (0b00000000) | 160 (0b10100000) | +---------------------+------------------+-------------------------------+------------------+  where $160\text{ bits} = 20 × 8\text{ bits}$, and 128 is equivalent to a '1' bit and 7 '0' bits. Therefore, if a length extension attack is carried out to calculate the hash, we have to use the second block. Also, the content of the first block should not be modified in order to have a known initial hash. which means, we need chr(128), chr(0) and chr(160) in the salt in order to have the pbox-permutated result containing these bytes. The order is not important for the pbox, so by default we can use $[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]$ as after pbox-permuatation the order of the payload remains the same (and so the first 16-byte is the password we want, which is convenient to us). Therefore, we carry out our first test using $[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]$ as the pbox, and the salt explained below (which is not important in this stage). (Notice that the assert check only checks for 20 distinct values in pbox, but not repeating elements, so we can definitely send a pbox of more than 20 elements as the input.) The hash returned is the initial hash array of the 2nd block in SHA256. To construct the 2nd block that contains our desired value, we would like to use 2 bytes from the password as the start of this block, so each time we can recover 2 bytes of the password after comparing the returned hash from the system to our rainbow table. The structure of the 2nd block:  +----+----+ | p1 | p2 | +----+----+  where $p1$ and $p2$ are the tested part of the password. The padded block should appear as below: (Notice that in total we have to send a $512 + 2 × 8 = 210_{16}$ bits string).  +----+----+------------------+-------------------------------+----------------+-----------------+ | p1 | p2 | 128 (0b10000000) | 59-bytes of null (0b00000000) | 2 (0b00000010) | 16 (0b00010000) | +----+----+------------------+-------------------------------+----------------+-----------------+  The block size is $1 + 1 + 1 + 59 + 1 + 1 = 64\text{ bytes}\ (= 512\text{ bits})$ Therefore, the rainbow table is generated using the tested hash received from the server as the initial hash, and $[c1] + [c2] + [128] + [0] × 59 + [2, 16]$ , where $[c1]$ and $[c2]$ are substituted by each possible pairs of characters in base64, as the padded payload. Notice that to achieve that, we have to disable the padding procedure of the SHA256 function, which also seems to be impossible with the default python hashlib library. After generating the hash table, the next step is to send the pbox and salt. The supposed content of the payload after permutated by the pbox is $p1$ and $p2$ appended just after the first block. Also, as mentioned before, the pbox can be much longer as long as exactly 20 different elements are found in it. Besides, the salt has to be crafted carefully so we can have chr(128), chr(0) and chr(160) in the permutated payload. My chosen salt is $[0, 160, 2, 128]$ (2 is not used as the input actually) The custom pbox is created as follows to construct desired pre-padded payload: $[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]$ + $[19]$ (corresponding to 128) + $[16]$ (corresponding to null bytes) $×\ 42$ + $[17]$ (corresponding to 160) + $[n, n+1]$ (the two characters in password we crack) in these 8 iterations, we have to test all the 16 characters in password, so n corresponds to $[0, 2, 4, 6, 8, 10, 12, 14]$ in these 8 iterations. After receiving the 8 hashes and comparing them to the rainbow table, the password is recovered, and we have only 1 allowed iteration left. Therefore, we have to do the authentication this time. The system provides a random pbox and salt. We can directly copy the functions (parse_pbox and permutate) in the given source code to calculate the permutated payload and the required hash value, and send it to the server. After that, the flag should be shown if nothing is wrong. Solved. ## Example Output  AKACgA== b'\x00\xa0\x02\x80' 4 [x] Opening connection to tertiary.pwnable.hk on port 50001 [x] Opening connection to tertiary.pwnable.hk on port 50001: Trying 18.163.58.67 [+] Opening connection to tertiary.pwnable.hk on port 50001: Done b'[cmd] ' b'[pbox] ' b'[salt] ' b'935229df4a638142892d4d18601258858fc1bee30fbec9ce6fa28e5a47d39edd' b'[cmd] ' b'[pbox] ' b'[salt] ' b'[cmd] ' b'[pbox] ' b'[salt] ' b'[cmd] ' b'[pbox] ' b'[salt] ' b'[cmd] ' b'[pbox] ' b'[salt] ' b'[cmd] ' b'[pbox] ' b'[salt] ' b'[cmd] ' b'[pbox] ' b'[salt] ' b'[cmd] ' b'[pbox] ' b'[salt] ' b'[cmd] ' b'[pbox] ' b'[salt] ' b'[cmd] ' b'[pbox] ' b'[salt] ' b'[hash] ' b'[flag] hkcert20{d0_y0u_feel_s3cur3_wh3n_s19nin9_in?}\n' [*] Closed connection to tertiary.pwnable.hk port 50001  The source code is given in the Walkthrough Part. # Walkthrough (Codes) sign in.py (the comments are used to test whether my library works properly) python from pwn import * import base64, sha256, os import hashlib # rainbow table salt = bytes([0, 160, 2, 128]) salt64 = base64.b64encode(salt).decode() # test extension #print(sha256.sha_256("a"*1)) #print(sha256.sha_256(b"a" + bytes([128]) + b"\0" * 61 + bytes([8]) + b"b", True, True)) #print(sha256.sha_256(b"b" + bytes([128]) + b"\0" * 60 + b"\2" + bytes([8]), False, True, i=sha256.sha_256("a"*1))) #passwordt = base64.b64encode(os.urandom(12)) #print(passwordt + salt) #init1 = sha256.sha_256(passwordt + salt, byts = True) #print(sha256.bytes2string(init1)) #init2 = sha256.sha_256(passwordt + salt + bytes([128]) + b'\0' * 42 + bytes([160]) + b'ab', byts = True) #print(sha256.bytes2string(init2)) #print(sha256.bytes2string(sha256.sha_256(b'ab' + bytes([128]) + b'\0' * 59 + bytes([2, 16]), pads = False, byts = True, i=init1))) print(salt64) print(base64.b64decode(salt64)) print(len(base64.b64decode(salt64))) rainbow = [""] * (64*64) def parse_pbox(payload): return list(map(int, payload[1:-1].split(','))) def permutate(payload, pbox): return bytes([payload[x] for x in pbox]) per = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" r = remote('tertiary.pwnable.hk', 50001) password = "" # get 1 block hash print(r.recvuntil('[cmd] ')) r.sendline("spy") print(r.recvuntil('[pbox] ')) r.sendline(str([i for i in range(20)])) print(r.recvuntil('[salt] ')) r.sendline(salt64) r.recvuntil('[hash] ') password = r.recvuntil('\n')[:-1] print(password) # break password into bytes password = [int(password[i:i+8],16) for i in range(0,len(password),8)] # rainbow table for ic in range(64): for jc in range(64): i = per[ic] j = per[jc] rainbow[ic * 64 + jc] = sha256.bytes2string(sha256.sha_256((i + j).encode() + bytes([128]) + b'\0' * 59 + bytes([2, 16]), False, True, password)) real = "" for j in range(8): print(r.recvuntil('[cmd] ')) r.sendline("spy") print(r.recvuntil('[pbox] ')) r.sendline(str([i for i in range(20)] + [19] + [16] * 42 + [17] + [j*2, j*2+1])) print(r.recvuntil('[salt] ')) r.sendline(salt64) r.recvuntil('[hash] ') result = r.recvuntil('\n')[:-1] ind = rainbow.index(result.decode()) real += per[ind // 64] + per[ind%64] print(r.recvuntil('[cmd] ')) r.sendline("auth") print(r.recvuntil('[pbox] ')) pbox = parse_pbox(r.recvuntil('\n')[:-1].decode()) print(r.recvuntil('[salt] ')) salt = base64.b64decode(r.recvuntil('\n')[:-1]) print(r.recvuntil('[hash] ')) permutated_password = permutate(real.encode() + salt, pbox) r.sendline(hashlib.sha256(permutated_password).hexdigest()) print(r.recvuntil('\n')) r.close()  sha256.py python initial_hash_values=[ 0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a, 0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19 ] sha_256_constants=[ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 ] def int_to_bytes(x: int) -> bytes: return x.to_bytes((x.bit_length() + 7) // 8, 'big') def int_from_bytes(xbytes: bytes) -> int: return int.from_bytes(xbytes, 'big') def pad(a, pads=True, byts=False): le = len(a) << 3 if byts: a = int_from_bytes(a) else: a = int_from_bytes(a.encode()) if pads: a = (a << 1) + 1 a = (a << (512 - (le + 65)%512)) a = (a << 64) + le else: if le%512 > 0: a = (a << (512 - le%512)) s = hex(a)[2:] s = ("0" * 128 + s)[-((len(s)-1)-(len(s)-1)%128+128):] #print(s) return [int(s[i:i+8],16) for i in range(0,len(s),8)] INT_MAX = (1 << 32) - 1 def rr(i, p): return (INT_MAX & (i << (32-p))) | (i >> p) def sha_256(s, pads=True, byts=False, i=initial_hash_values): ha = i.copy() W = [0] * 64 bi = pad(s, pads, byts) for i in range(0, len(bi), 16): a, b, c, d, e, f, g, h = ha for j in range(64): if j < 16: W[j] = bi[j+i] #print(j, W[j]) else: w = W[j-15] s0 = rr(w, 7) ^ rr(w, 18) ^ (w >> 3) w = W[j-2] s1 = rr(w, 17) ^ rr(w, 19) ^ (w >> 10) # print(s0, s1) W[j] = INT_MAX & (W[j-16] + s0 + W[j-7] + s1) # print(W[j]) # input() S0 = rr(a, 2) ^ rr(a, 13) ^ rr(a, 22) S1 = rr(e, 6) ^ rr(e, 11) ^ rr(e, 25) ch = (e & f) ^ ((INT_MAX - e) & g) temp1 = INT_MAX & (h + S1 + ch + sha_256_constants[j] + W[j]) #print(temp1) maj = (a&b)^(a&c)^(b&c) temp2 = INT_MAX & (S0 + maj) #print(temp2) h, g, f, e, d, c, b, a = g, f, e, INT_MAX & (d + temp1), c, b, a, INT_MAX & (temp1 + temp2) #print(a,b,c,d,e,f,g,h) #input() ha = [INT_MAX & (ha[k] + [a,b,c,d,e,f,g,h][k]) for k in range(8)] return ha def bytes2string(b): s = "" for i in range(8): s += ("0" * 8 + hex(b[i])[2:])[-8:] return s  # Provided Source File(s) chall.py python # Challenge written by Mystiz. # Signature: PHNjcmlwdD5jb25zb2xlLmxvZygnVEhJUyBDSEFMTEVOR0UgSVMgV1JJVFRFTiBGT1IgSEtDRVJUIENURiBBTkQgSVMgTk9UIEZPUiBGUkFOS0lFIExFVU5HIFRPIFBMQUdBUklaRS4nKTwvc2NyaXB0Pg== import base64 import hashlib import os import random from secret import flag def parse_pbox(payload): return list(map(int, payload[1:-1].split(','))) def permutate(payload, pbox): return bytes([payload[x] for x in pbox]) class Server: def __init__(self, password): assert len(password) == 16 self.password = password def preauth(self): pbox = list(range(20)) salt = os.urandom(4) random.shuffle(pbox) return pbox, salt def auth(self, pbox, salt, hashed_password): permutated_password = permutate(self.password + salt, pbox) return hashlib.sha256(permutated_password).hexdigest() == hashed_password class Client: def __init__(self, password): self.password = password def spy(self, pbox, salt): assert len(set(pbox)) == 20 assert len(salt) == 4 password = self.password permutated_password = permutate(password + salt, pbox) hashed_password = hashlib.sha256(permutated_password).hexdigest() print(f'[hash] {hashed_password}') def main(): password = base64.b64encode(os.urandom(12)) s = Server(password) c = Client(password) for _ in range(10): command = input('[cmd] ') try: if command == 'spy': pbox = parse_pbox(input('[pbox] ')) salt = base64.b64decode(input('[salt] ')) c.spy(pbox, salt) elif command == 'auth': pbox, salt = s.preauth() print(f'[pbox] {pbox}') print(f'[salt] {base64.b64encode(salt).decode()}') hashed_password = input('[hash] ') if not s.auth(pbox, salt, hashed_password): raise Exception('No!') print(f'[flag] {flag}') except: print('no way!') if __name__ == '__main__': main()  # Flag hkcert20{d0_y0u_feel_s3cur3_wh3n_s19nin9_in?} Do you feel secure when signing in?