Just a simple query challenge, but can you solve it in time?
This is a remote challenge, you can connect with:
nc subarraysums101.challs.teamitaly.eu 29001
Authors: Carmen Casulli <@Dandelion>, Lorenzo Demeio <@Devrar>
In this challenge the server generates a secret as an array of
Let
-
First observation:
$u$ is the greatest positive number such that its query result$q_u$ will be positive while$q_{u+1}$ will be$0$ , therefore we could find$u$ with a binary search or also by just asking for all query results from$1$ to$n$ , until we find the value with the aforementioned property. -
Second observation: After we've found
$u$ , we can get information on the secret from at most$u+1$ queries (from$q_0$ to$q_u$ ), as we already know that the result for any query$q_x$ with$x>u$ will be$0$ . -
Third observation: The number of
$0$ and$1$ in the sequence will be approximately$\frac{n}{2}$ which is also the number of queries we can perform; therefore we will assume$u \lt \frac{n}{2}$ , start asking for all queries from$q_1$ until we find$q_x \gt 0$ and$q_{x+1} = 0$ (which lets us deduce$U=x$ ), and store their results.
If the assumption of the third observation doesn't hold, we will reach the maximum number of queries before we encounter
def getU():
u = 0
while(u<n):
if num_queries == n//2:
return -1
if(get_query(u+1) == 0):
break
u += 1
return u
We now define
Let
Notice that some
Let
-
First observation:
$z + u = n$ . -
Second observation:
$z = {\sum}_{i=0}^u x_i$
Thus we have our first equation in the variables$x_i$ :
We can now observe that all query results can be written as equations in these variables, take as an example
Similarly, to get
In the general case, for
We don't like all those
Let us also rewrite the first equation we had:
Therefore our complete system is:
As we've already mentioned, the query
This information is redundant because we already know
Now give the system to your favorite SMT solver and you'll get the solution - but watch out for the time limit (later in Implementation).
The values of
To reconstruct the secret we shall first write
# assuming the array result contains y_i
maybeRes = [0]*(result[0]-1)
for i in range(1, u+1):
maybeRes += [1] + [0]*(result[i]-1)
Let's discuss a few implementation details of the solution we have proposed.
First of all, the choice of the solver. The system of equations can be solved by any generic SMT solver, but most won't do it within the time limit given the set of constraints. For this purpose we've decided to use cvc5, whose python API have the same syntax as Z3's. We have also tried using Z3, but it was too slow.
For some instancies of the secret the solver was still taking too long, so we also set its time limit to setOption("tlimit-per", 20000)
and we added the assumption that all
To cover all these situation, our code must connect to the server multiple times, until it finds a secret that satisfies all assumptions that we have made, that must be solvable by cvc5 within the time limit, plus we need to be lucky enough to get the correct solutions, and not its inverse.
Checking that
Here is the complete code of the solution:
from pwn import remote, process
from cvc5 import *
from cvc5.pythonic import *
import os
online = True
n = 128
queries_ans = [-1]*(n+1)
num_queries = 0
HOST = os.environ.get("HOST", "todo.challs.todo.it")
PORT = int(os.environ.get("PORT", 34001))
def initConn():
global io, num_queries
io = remote(HOST, PORT)
def get_query(k):
global num_queries
if(queries_ans[k] == -1):
io.sendlineafter(b'> ', str(k).encode())
queries_ans[k] = int(io.recvline(False).decode())
num_queries += 1
return queries_ans[k]
def getU():
u = 0
while(u<n):
if num_queries == n//2:
return -1
if(get_query(u+1) == 0):
break
u += 1
return u
def findSol(u, target):
# nota: non si puo' distinguere una stringa dall'inversa
sol = Solver()
sol.setOption("tlimit-per", 20000)
ys = [BitVec(f'y_{i}', 8) for i in range(u+1)]
for y in ys:
sol.add(y >= 1, y <= 9)
sol.add(sum(ys) == n+1)
sums = [0 for i in range(u+1)]
for k in range(0, u):
# query(u-k)
q = u-k
sums[q] = 0
for i in range(0, k+1):
sums[q] += ys[i]*ys[u-k+i]
sol.add(sums[q] == get_query(q))
print("here")
res = sol.check()
if(res != sat):
print(res)
print("Something went wrong")
return None
m = sol.model()
solutions = 0
status = sat
return [m[y].as_long() for y in ys]
def checkResult(u, result, target=[]):
maybeRes = [0]*(result[0]-1)
for i in range(1, u+1):
maybeRes += [1] + [0]*(result[i]-1)
if num_queries < n//2:
for _ in range(n//2 - num_queries):
try:
io.sendlineafter(b"> ", b"0")
except EOFError:
print("Timeout")
return False
if not online:
return maybeRes == target
else:
try:
io.sendlineafter(b": ", (''.join(str(x) for x in maybeRes)).encode())
except EOFError:
print("Timeout")
return False
ans = io.recvline().decode()
if "Nope" in ans:
print("Nope")
return False
else:
print(ans)
return True
def main():
global num_queries, queries_ans
retry = True
while retry:
num_queries = 0
queries_ans = [-1]*(n+1)
initConn()
target = []
if not online:
target = io.recvline(False)[1:-1].decode().split(', ')
target = [int(t) for t in target]
u = getU()
if u == -1:
io.close()
continue
if not online:
offlineChecking(target, u)
print("Finding sol")
result = findSol(u, target)
if result == None:
io.close()
continue
if not checkResult(u, result, target):
io.close()
continue
retry = False
print("Query usate:", num_queries)
if __name__ == '__main__':
main()
flag{z3_d03snt_s0lv3_3v3rything...0r_do3s_1t?}