After understanding the challenge script, I know that 1) `add()` and `sub()` is from Vigenere Cipher 2) how `ctf()` change the nonce is first adding 1 to the last character, then flip the string. The details are not important, I am just interested in knowing that the process doesn't involve any random component, so the changes to the nonce is reproducible 3) in `vigenere_ctr_enc()`, first two steps in the for loop can be combined to ctxt += (b + nonce + key), where the addition here is the Vigenere Cipher's addition 4) the value of `nonce` is given in the first block in `CTXT` 5) the value of `KEY` is constant Then, I start solving it. 1. From the challenge script, I know that except the first block in `CTXT`, `CTXT_blocks[i] = message_blocks[i - 1] + current nonce + KEY`. As mentioned, the change to `nonce` is reproducible, therefore I can easily remove the value of `current nonce` from `CTXT_blocks[i]`.What remained in each block `i` will be `message_blocks[i - 1] + KEY`, which is simply `message_blocks[i - 1] + a constant`. ```python def get_blocks(s): blocks = [] i = 0 while(i + BLOCKLENGTH<len(s)): blocks.append(s[i:i+BLOCKLENGTH]) i = i + BLOCKLENGTH blocks.append(s[i:len(s)]) return(blocks) def ctr(num): decimal_num = 0 power = len(num) - 1 for digit in num: decimal_num += (ord(digit) - ord('A')) * (MOD ** power) power -= 1 decimal_num += 1 result = "" while decimal_num > 0: remainder = decimal_num % MOD result = chr(remainder + ord('A')) + result decimal_num //= MOD return(result) def add(block1,block2): assert(len(block1)<= len(block2)) assert(len(block2)<= BLOCKLENGTH) b1upper = block1.upper() b2upper = block2.upper() b1 = [ ord(b1upper[i])-SHIFT for i in range(len(block1))] b2 = [ ord(b2upper[i])-SHIFT for i in range(len(block1))] s = [ (b1[i] + b2[i]) % MOD for i in range(len(block1))] slist = [ chr(s[i]+SHIFT) for i in range(len(block1))] sum = ''.join(slist) return(sum) def sub(block1,block2): assert(len(block1)<= len(block2)) assert(len(block2)<= BLOCKLENGTH) b1upper = block1.upper() b2upper = block2.upper() b1 = [ ord(b1upper[i])-SHIFT for i in range(len(block1))] b2 = [ ord(b2upper[i])-SHIFT for i in range(len(block1))] s = [ (b1[i] - b2[i]) % MOD for i in range(len(block1))] slist = [ chr(s[i]+SHIFT) for i in range(len(block1))] sum = ''.join(slist) return(sum) SHIFT = 65 MOD = 26 BLOCKLENGTH = 20 CTXT = "ULDGWMFOXRNTKTVECMMNPQPAJYLJPIXJQUTYRQIQLQSURMZGYUTTPMHOHJAKNQKFFYGTPWXTMFWKHELLZYYFSUDNFKHOZAQIFQDPLUEFFPAILDTXQWWWKIGSVPPTMMWQMODAIBCODZTDSYUQBFSMLSILAZZHVXNTHPUASMZQBBSQZIKHQCLQFQXQAUJGMKACTYBBVMCXUMJOC" # everything above is direcly copied from the challenge script def RemoveNonce(ctxt): blocks = get_blocks(ctxt) nonce = blocks[0] ptxt = "" for i in range(1, len(blocks)): ptxt += sub(blocks[i], nonce) nonce = ctr(nonce) return(ptxt) CTXT_without_nonce = RemoveNonce(CTXT) print(''.join(get_blocks(CTXT_without_nonce))) ``` Then, we will get the output string ``` VFMUNMGVSRKQGBYUPEWDRFPOVAUSBDGAFTMKFXOWTFHZJMBFSFKACMBGFSZWFNVZWIYZITUVPHVEDERZRJBZJDVUOMGEGDBSIWUBBEMNQARCPXQHYIHKBNHLYNRKFTNYOBVSQGEDTLBANERUWAUCEKFXPPPDOQZWLFUKEIESPTNJJFGXTAQCABGIG ``` Block `i` in this string is `message_blocks[i] + KEY`. As the `KEY` is a constant value, what we get is a simple Vigenere Cipher. 2. We can break the Vigenere Cipher using online tool, e.g. https://guballa.de/vigenere-solver. ![image](https://hackmd.io/_uploads/Byu5535Ka.png) Then we will get ``` ISTOLETHISIDEAFROMTHESWITSHEREENDSTHEFLAGSOTHEORIGINALIDEAWASACTUALLYUSINGCBCMODEWITHVIGENERECIPHERFORTHOSEOFYOUWHOHAVEPLAYEDLAKECTFOFLASTYEARYOUSHOULDKNOWANYWAYSBECAREFULWHENUSINGNONCE ``` Reading the message, we will see `"HERE ENDS THE FLAG"`, so we stop right before it. flag: `firebird{ISTOLETHISIDEAFROMTHESWITS}`