GRIC
This is the second challenge from the Crypto section of CTF SG 2021. The challenge needs us to find out the checksum letter when given a 10 digit number.
Statement
NRIC goes Global!
nc chals.ctf.sg 10101
The server code is provided
Observation
The basic idea on how the checksum works is that it takes every digit[i]
, multiply it with constants[i]
and sum it together modulo 17 (length of sbox).
Let this final answer be x
Then the checksum letter will be sbox[x]
.
More formally,
\[\texttt{sbox}[( \sum_{i=0}^{9} \texttt{digit[i]} \cdot \texttt{constants[i]})\ \underline{mod} \ 17]\]On every connection to the server, it will random shuffle the sbox and generate random constants.
LEN_GRIC = 10 # For whoever
constants = [0] * LEN_GRIC
sbox = list('ABCDEFGHJKLMRTXYZ')
def setup():
global constants, sbox
constants = [random.randint(0, 17) for x in range(LEN_GRIC)]
random.shuffle(sbox)
The server also provided us the ability to query the checksum of a number for 25 times.
To get the flag, we need to answer 100 checksum question randomly generated by the server.
Solution
First, notice that if we query G1000000000
, we can get the value of sbox[constants[0]]
.
If the value of constants[0]
is 1
, Then we can restore sbox[1..9]
by just querying
sbox[1] = checkSum(G1000000000)
sbox[2] = checkSum(G2000000000)
sbox[3] = checkSum(G3000000000)
...
sbox[9] = checkSum(G9000000000)
Now we have a problem because we can’t retrive sbox[10..16]
.
But what if the value of constants[1]
is also 1
?
Then now we got constants[0] = 1
and constants[1] = 1
We can then retrive the content of sbox with just 16 queries.
sbox[1] = checkSum(G1000000000)
sbox[2] = checkSum(G2000000000)
sbox[3] = checkSum(G3000000000)
...
sbox[9] = checkSum(G9000000000)
sbox[10] = checkSum(G9100000000)
sbox[11] = checkSum(G9200000000)
...
sbox[16] = checkSum(G9700000000)
The only one character that is left out will then be sbox[0]
Okay awesome, now we can retrive sbox if constants[0] = 1
and constants[1] = 1
.
We can generalize this to constants[i] = 1
and constants[j] = 1
. Where i
and j
not necessarily has to be 0 and 1.
However, we still have no idea what constants[2..16]
is.
Therefore we need to have 9 query.
c0 = checkSum(G1000000000) # This has been queried in the previous part
c1 = checkSum(G0100000000)
c2 = checkSum(G0010000000)
c3 = checkSum(G0001000000)
...
c9 = checkSum(G0000000001)
If we have already known the sbox, we can easily retrive back the constants.
constants[0] = sbox.index(c0)
constants[1] = sbox.index(c1)
constants[2] = sbox.index(c2)
constants[3] = sbox.index(c3)
...
constants[9] = sbox.index(c9)
Note that constants[i] = constants[j]
if and only if \(c_{i} = c_{j}\)
So the main idea on how to solve this solution is we first query with 10 queries,
c0 = checkSum(G1000000000)
c1 = checkSum(G0100000000)
c2 = checkSum(G0010000000)
c3 = checkSum(G0001000000)
...
c9 = checkSum(G0000000001)
And be hopeful that there will be a pair (i,j)
where i != j
such that \(c_{i} = c_{j}\) , constants[i] = 1
and constants[j] = 1
We can retrive back the sbox and constants with the above method with another 15 queries.
If it fails, we try again and be more hopeful.
It will pass the test in slightly less than 1/17
chance.
Here’s the python code for the complete solution.
from pwn import *
ok = False
sbox = list('ABCDEFGHJKLMRTXYZ')
while (not ok) :
s = 'A'
r = remote('chals.ctf.sg', 10101)
arr = []
for i in range(0,10):
r.recvuntil('\n\n')
r.sendline(b'G')
r.recvline()
r.recvline()
k = 1 << i
gric = 'G' + format(k, '010b')
r.sendline(gric.encode())
c = r.recvline().split()[-1][-1]
if (c in arr):
s = c
ok = True
arr.append(c)
if (not ok):
continue
arr.reverse()
ss = [0, chr(s)]
i1 = arr.index(s)
i2 = i1 + 1 + arr[i1+1:].index(s)
for i in range(2,17):
r.recvuntil('\n\n')
r.sendline(b'G')
r.recvline()
r.recvline()
gric = list('G' + '0'*10)
gric[i1+1] = str(min(i,9))
gric[i2+1] = str(max(i-9,0))
r.sendline("".join(gric).encode())
c = chr(r.recvline().split()[-1][-1])
ss.append(c)
for i in sbox:
if (i not in ss):
ss[0] = i
break
print(ss)
if (ss.count(ss[1]) > 1):
ok = False
continue
constants = [ss.index(chr(i)) for i in arr]
print(constants)
r.recvuntil('\n\n')
r.sendline(b'C')
r.recvline()
for i in range(100):
gric = r.recvline().split()[-1]
count = 0
for j in range(1,11):
count += int(gric[j]) * constants[j-1]
count %= 17
r.sendline(ss[count])
res = r.recvline()
print(res)
if (res.split()[0] == b'Nope,'):
ok = False
r.close()
break
print(r.recvline())
print(r.recvline())
flag : CTFSG{NRIC_m3meS_F0R_GRIC_7eENs}