- Published on
ZKP Writeup
- Authors
- Name
- Jayden Koh
ZKP Writeup
Complete writeup of the ZKP algo challenge from HSCTF 8.
Description
Summary: A 3SAT problem generator generates 3SAT problems, but the server makes some of them nonSAT, our job is to figure out which ones were modded to be nonSAT and which ones are still SAT (not modded). We are given a finite amount of tests to request for a nth clause to see, though after each request, the whole formula is shuffled. We have to solve for
t
number of trials with a total ofm
clauses andn
literals. We need to have a success rate of at least 75% (guessing whether or not the formula was modded or not).3SAT: 3SAT is a type of SAT which always has 3 literals per clause. Each literal is a separate boolean and each clause is a conjunction of literals (3 in the case of 3SAT). A formula is considered SAT if any combination of booleans can make the output
True
. Each literal is connected with the other literals by anOR
gate while each clause is connected by anAND
gate.3SAT Ex:
(T OR T OR F) AND (F OR T OR F) AND (F OR F OR T)
is SAT solvable because the output isT
3SAT Ex:
(T OR T OR F) AND (F OR T OR F) AND (F OR F OR F)
is not SAT solvable because the output isF
Theory: If each clause is connected by an
AND
gate, that means that each internal clause must outputT
, otherwise the whole answer becomesF
. If each clause must beT
then calculating all the possible combinations:
(T OR T OR T) = T
(T OR T OR F) = T
(T OR F OR T) = T
(T OR F OR F) = T
(F OR T OR T) = T
(F OR T OR F) = T
(F OR F OR T) = T
(F OR F OR F) = F
The only combination that doesn't work is FFF
which means all we need to do it look out for FFF
and it is definitely nonSAT.
Solution: The number of literals and clauses are basically irrelevant. We request 10 times for a random clause (1<random_clause<m)
and if the combination (F OR F OR F)
is one of them, we immediately guess false
or nonSAT. If we have reached the end of 10 requests and have not encountered (F OR F OR F)
, we guess either false
or true
. Statistically, that 75% should hold true because guessing at total random gives us 50%, the extra 25% is added by using this formula. If it doesn't work the first time, run it again because it still can fail.
from pwn import *
import random
def attempt():
r = remote('zkp.hsc.tf', 1337)
l = ['true','true','false']
print(r.recvuntil(f'==\n'))
trials = int(r.recvline().decode('utf-8'))
print('trials: ',trials)
for t in range(trials):
n = r.recvline().decode('utf-8').strip()
#print('N: ',n)
m = r.recvline().decode('utf-8').strip()
#print('M: ',m)
permuted = r.recvline().decode('utf-8').strip()
#print('permuted: ',permuted)
i = str(random.randint(0,int(m)//2))
#print('randint: ',i)
r.sendline(i)
literals = r.recvline().decode('utf-8').strip()
#print('first literals: ',literals)
absolute = True
for z in range(15):
r.sendline('next')
#print('sent next')
permuted = r.recvline().decode('utf-8').strip()
#print('permuted: ',permuted)
i = str(random.randint(0,int(m)))
r.sendline(i)
literals = r.recvline().decode('utf-8').strip()
#print(literals,type(literals))
if literals == ': [False, False, False]':
absolute = False
#print('absolutely false')
break
print('finished trial',t,'/',trials)
if absolute == False:
guess = 'false'
else:
guess = random.choice(l)
print('guess',guess)
r.sendline(guess)
#r.sendline(ret)
result = r.recvline().decode('utf-8').strip()
print(result)
try:
result = r.recvline().decode('utf-8').strip()
print(result)
except:
print('one more down')
for x in range(10):
try:
attempt()
except:
pass