feat(q-solved): add first q-solved writeup

Signed-off-by: Julien CLEMENT <julien.clement@epita.fr>
This commit is contained in:
Julien CLEMENT 2022-03-25 22:45:24 +01:00
parent 8cbb1a0b89
commit 6f5bbb7ceb
12 changed files with 17645 additions and 6 deletions

@ -7,6 +7,7 @@ paginate = 10
PygmentsCodeFences = true PygmentsCodeFences = true
PygmentsStyle = "monokai" PygmentsStyle = "monokai"
[author] [author]
name = "Julien CLEMENT" name = "Julien CLEMENT"
@ -56,10 +57,6 @@ PygmentsStyle = "monokai"
[params.footer] [params.footer]
author = true author = true
[taxonomies]
category = "blog"
tag = "tags"
series = "series"
[languages] [languages]
[languages.en] [languages.en]
@ -71,7 +68,7 @@ PygmentsStyle = "monokai"
[languages.en.params.logo] [languages.en.params.logo]
logoMark = "juju@synacktiv ~$ " logoMark = "juju@synacktiv ~$ "
logoText = "cryptoratio" logoText = "cryptopouneur"
logoHomeLink = "/" logoHomeLink = "/"
logoCursorColor = "#3884FF" logoCursorColor = "#3884FF"
# or # or
@ -84,3 +81,8 @@ PygmentsStyle = "monokai"
identifier = "posts" identifier = "posts"
name = "Posts" name = "Posts"
url = "/post/" url = "/post/"
[[menu.main]]
identifier = "writeups"
name = "Writeups"
url = "/writeups/"

@ -8,5 +8,5 @@ tags: ["Reverse", "Exploit"]
{{< youtube 5g2eZSST7YE >}} {{< youtube 5g2eZSST7YE >}}
{{< rawhtml >}} {{< rawhtml >}}
<embed src="/post/conf_exploit.pdf" type="application/pdf" width="100%" height="430"> <embed src="/conf_exploit/slides.pdf" type="application/pdf" width="100%" height="430">
{{< /rawhtml >}} {{< /rawhtml >}}

@ -0,0 +1,111 @@
---
title: "Reversing quantum algorithms ~~for ctf points~~ | q-solved - zer0pts 2022"
date: "2022-03-25 18:00:00"
author: "Juju"
tags: ["Reverse", "Quantum", "Writeup", "zer0pts"]
---
# Challenge description
`quantum` `reverse` | `304 pts` `8 solves`
```
I copied the solver of a reversing task from the future. But it doesn't
show the flag forever :thinking:
```
# Given files
{{< code file="/static/q-solved/solve.py" language="py" >}}
[circuit.json](/q-solved/circuit.json)
# TL;DR
The scripts builds a quantum circuit describing an unstructured search
algorithm inspired by the grover's algorithm. Its goal is to find among all
possible inputs, the one(s) with the highest probability to match a predefined
criteria.
It is composed of an `oracle` and a `diffuser`. The oracle is a black-box
function taking a state vector as input and introducing a phase shift in the
target qubit if the input matches the predefined criteria.
The diffuser then performs amplitude amplification using the target qubit's
phase kickback when the oracle matches, thus increasing the probability of a
matching input vector to be measured.
Basically, the output of the circuit is the input matching the criteria
described by the oracle.
All that remains to do for us is to understand what that criteria is.
# Reversing the Oracle
We can see that the oracle is built using the `circuit.json`.
The oracle is composed of 1408 multi-controlled X (MCX) gates, each controlled by 1
or 3 input qubits with with a control state given in the json. Each MCX gate
acts on a dedicated ancilla qubit.
After all 1408 MCX, the circuit adds an other MCX on the target qubit with all
control states set to 0. The target qubit is therefore introduced a phase shift
when all ancillas are in `|0>`.
So we want all ancillas to be `|0>` but it is also their original state. We
therfore have to influence the control qubits of each MCX so that none actually
performs the X gate on any ancilla. This means that among all control qubits of
a MCX, at least one must have a state different from its control state, thus
deactivating the gate.
If we look closely, we can see that the control state of a control qubit is `1`
when the json specifies `False`, and `0` when `True`.
Remember, inputs must be different from their control state specified on the
gate. Therefore a qubit marked as `False`, must take value `|0>` to deactivate
the gate. Similarly, a qubit marked `True` must take value `|1>`.
So we said earlier that the MCX have either 3 or 1 control bits and that at
least 1 of the control qubits must mismatch from their control state.
# POC with trivial qubits
Obviously this results in an equation system but let's see what we get with
only the obvious qubits: the ones controlling an ancilla by themselves.
Indeed if an ancilla is controlled by a single input qubit then this qubit MUST
be different from his control state so the X gate stays disabled. Therefore, any input qubit marked as `False` and as the only control of a gate MUST be set to `|0>` to match the oracle. Same is true for qubits marked as `True` that must be in state `|1>`.
So let's try to set all obvious qubits:
{{< code file="/static/q-solved/poc_flag.py" language="py" >}}
{{< image src="/q-solved/poc_flag.png" style="border-radius: 8px;" >}}
Well, most of them are 0, except, the first byte: `z`
Which is a really good sign that we are indead decoding a flag of the form
`zer0pts{...}`
# Equation system
For MCX with 3 control bits, we simply need to put them in an equation system,
with the trivial qubits.
We will have a total of 1408 equations, 1 for each MCX, each equation basically
saying that at least 1 Qubit must be different from its control state, and
therefore equal to its assigned boolean in the json.
Once the system is solved, we will know the state of all qubits that match the
oracle, which is the one outputed by the quantum circuit. We will then be able
to decode it to get the flag.
# Solve
I used z3 to build and solve the equation system:
{{< code file="/static/q-solved/flag.py" language="py" >}}
Running the script outputs us the equation system, sat indicated that z3 found
a solution to the system and the decoded solution of the system with the flag:
`zer0pts{FLAG_by_Gr0v3r's_4lg0r1thm}`
{{< image src="/q-solved/flag.png" style="border-radius: 8px;" >}}

@ -0,0 +1,3 @@
{{ $file := .Get "file" | readFile }}
{{ $lang := .Get "language" }}
{{ (print "```" $lang "\n" $file "\n```") | markdownify }}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

BIN
jujure/static/q-solved/flag.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

33
jujure/static/q-solved/flag.py Executable file

@ -0,0 +1,33 @@
#!/usr/bin/env python3
import json
from z3 import *
with open('circuit.json', 'r') as f:
j = json.load(f)
nq = j['memory']
system = [Bool(str(i)) for i in range(nq)]
s = Solver()
for cs in j['circuit']:
term = Or()
for c in cs:
term = Or(term, system[c[1]] == c[0])
s.add(term)
print(s)
print(s.check())
m = s.model()
result_arr = [''] * nq
for d in m.decls():
result_arr[int(d.name())] = '1' if m[d] else '0'
result_arr.reverse()
result = "".join(result_arr)
print(int.to_bytes(int(result, 2), nq//8, 'little'))

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@ -0,0 +1,19 @@
#!/usr/bin/env python3
import json
with open('circuit.json', 'r') as f:
j = json.load(f)
nq = j['memory']
system = ['0'] * nq
for cs in j['circuit']:
if len(cs) == 1:
system[cs[0][1]] = '1' if cs[0][0] else '0'
system.reverse()
result = "".join(system)
print(int.to_bytes(int(result, 2), nq//8, 'little'))

56
jujure/static/q-solved/solve.py Executable file

@ -0,0 +1,56 @@
import gmpy2
from qiskit import QuantumCircuit, execute, Aer
from qiskit.circuit.library import XGate
import json
with open("circuit.json", "r") as f:
circ = json.load(f)
nq = circ['memory']
na = circ['ancilla']
target = nq + na
print("[+] Constructing circuit...")
main = QuantumCircuit(nq + na + 1, nq)
sub = QuantumCircuit(nq + na + 1)
main.x(target)
main.h(target)
for i in range(circ['memory']):
main.h(i)
t = circ['memory']
for cs in circ['circuit']:
ctrl = ''.join(['0' if x else '1' for (x, _) in cs])
l = [c for (_, c) in cs]
sub.append(XGate().control(len(cs), ctrl_state=ctrl), l + [t])
t += 1
sub.append(XGate().control(na, ctrl_state='0'*na),
[i for i in range(nq, nq + na)] + [target])
for cs in circ['circuit'][::-1]:
t -= 1
ctrl = ''.join(['0' if x else '1' for (x, _) in cs])
l = [c for (_, c) in cs]
sub.append(XGate().control(len(cs), ctrl_state=ctrl), l + [t])
sub.h([i for i in range(nq)])
sub.append(XGate().control(nq, ctrl_state='0'*nq),
[i for i in range(nq)] + [target])
sub.h([i for i in range(nq)])
for i in range(round(0.785 * int(gmpy2.isqrt(2**nq)) - 0.5)):
main.append(sub, [i for i in range(na + nq + 1)])
for i in range(nq):
main.measure(i, i)
print("[+] Calculating flag...")
emulator = Aer.get_backend('qasm_simulator')
job = execute(main, emulator, shots=1024)
hist = job.result().get_counts()
result = sorted(hist.items(), key=lambda x: -x[1])[0][0]
print("[+] FLAG:")
print(int.to_bytes(int(result, 2), nq//8, 'little'))