feat(q-solved): add first q-solved writeup
Signed-off-by: Julien CLEMENT <julien.clement@epita.fr>
This commit is contained in:
parent
8cbb1a0b89
commit
6f5bbb7ceb
@ -7,6 +7,7 @@ paginate = 10
|
||||
PygmentsCodeFences = true
|
||||
PygmentsStyle = "monokai"
|
||||
|
||||
|
||||
[author]
|
||||
name = "Julien CLEMENT"
|
||||
|
||||
@ -56,10 +57,6 @@ PygmentsStyle = "monokai"
|
||||
[params.footer]
|
||||
author = true
|
||||
|
||||
[taxonomies]
|
||||
category = "blog"
|
||||
tag = "tags"
|
||||
series = "series"
|
||||
|
||||
[languages]
|
||||
[languages.en]
|
||||
@ -71,7 +68,7 @@ PygmentsStyle = "monokai"
|
||||
|
||||
[languages.en.params.logo]
|
||||
logoMark = "juju@synacktiv ~$ "
|
||||
logoText = "cryptoratio"
|
||||
logoText = "cryptopouneur"
|
||||
logoHomeLink = "/"
|
||||
logoCursorColor = "#3884FF"
|
||||
# or
|
||||
@ -84,3 +81,8 @@ PygmentsStyle = "monokai"
|
||||
identifier = "posts"
|
||||
name = "Posts"
|
||||
url = "/post/"
|
||||
|
||||
[[menu.main]]
|
||||
identifier = "writeups"
|
||||
name = "Writeups"
|
||||
url = "/writeups/"
|
||||
|
@ -8,5 +8,5 @@ tags: ["Reverse", "Exploit"]
|
||||
{{< youtube 5g2eZSST7YE >}}
|
||||
|
||||
{{< 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 >}}
|
||||
|
111
jujure/content/writeups/q-solved.md
Normal file
111
jujure/content/writeups/q-solved.md
Normal file
@ -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;" >}}
|
3
jujure/layouts/shortcodes/code.html
Normal file
3
jujure/layouts/shortcodes/code.html
Normal file
@ -0,0 +1,3 @@
|
||||
{{ $file := .Get "file" | readFile }}
|
||||
{{ $lang := .Get "language" }}
|
||||
{{ (print "```" $lang "\n" $file "\n```") | markdownify }}
|
1
jujure/static/q-solved/circuit.json
Executable file
1
jujure/static/q-solved/circuit.json
Executable file
File diff suppressed because one or more lines are too long
17414
jujure/static/q-solved/circuit_linted.json
Executable file
17414
jujure/static/q-solved/circuit_linted.json
Executable file
File diff suppressed because it is too large
Load Diff
BIN
jujure/static/q-solved/flag.png
Executable file
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
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'))
|
BIN
jujure/static/q-solved/poc_flag.png
Executable file
BIN
jujure/static/q-solved/poc_flag.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
19
jujure/static/q-solved/poc_flag.py
Executable file
19
jujure/static/q-solved/poc_flag.py
Executable file
@ -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
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'))
|
Loading…
Reference in New Issue
Block a user