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.

Then we will get
```
ISTOLETHISIDEAFROMTHESWITSHEREENDSTHEFLAGSOTHEORIGINALIDEAWASACTUALLYUSINGCBCMODEWITHVIGENERECIPHERFORTHOSEOFYOUWHOHAVEPLAYEDLAKECTFOFLASTYEARYOUSHOULDKNOWANYWAYSBECAREFULWHENUSINGNONCE
```
Reading the message, we will see `"HERE ENDS THE FLAG"`, so we stop right before it.
flag: `firebird{ISTOLETHISIDEAFROMTHESWITS}`