文章总结: 本文详细解析了HackTheBox量子安全CTF题目QLotto的解题过程。首先分析server.py代码,发现系统使用量子电路生成随机数,其中0号线为lotto、1号线为test,通过RXX等量子门实现纠缠。关键漏洞在于参数检查使用Python负数索引可绕过,且RXX(pi/2)门能使两线路产生相反纠缠态。解题思路是利用H门抵消初始状态,通过HZH等效X门翻转0号线状态,再用RXX(pi/2,-2,-1)制造相反纠缠,使test与lotto数字按位取反。最后提供逆向解码脚本,根据test数字计算出lotto中奖号码。 综合评分: 85 文章分类: CTF,量子安全,漏洞分析,代码审计,逆向分析
量子安全 quantum ctf Qlotto Hack the box
枫林路大砍刀 枫林路大砍刀
看雪学苑
2026年2月7日 18:00 上海
01
代码分析server.py
首先,拿到了一个server.py文件,我们先来分析代码:
#
这道题也是一上来是定义了一个类,然后引用了包和设置aer
from qiskit import QuantumCircuit, ClassicalRegister, transpile
from scipy.stats import binomtest
from qiskit_aer import Aer
from math import pi
#from my_secret import JACKPOT
class QuantumLotto:
def __init__(self):
self.backend = Aer.get_backend("qasm_simulator")
degrees_to_radians和generate_circuit
这里主要是第一个是角度的转换
然后是生成电路,和上一篇一样,门的输入范式等等,参数转化为int
上一篇:量子安全 quantum ctf Global Hyperlink Zone Hack the box
这里一共生成了两条线路,并且0号最开始加了x门
def degrees_to_radians(self, degrees: int):
return degrees * (pi / 180)
def generate_circuit(self, instructions: str):
circuit = QuantumCircuit(2)
circuit.h(0)
instructions = instructions.split(";")
for instr in instructions:
parts = instr.split(":")
if len(parts) != 2:
print(f"[Dealer] The move '{instr}' isn't recognized at this table. Expected format: <gate>:<params>")
return None
gate, params = parts
try:
params = [ int(p) for p in params.split(",") ]
except:
print("[Dealer] Only number cards are allowed at this table.")
return None
然后是这道题可以用到的门:H,S,T,ZZ,RXX,RYY,RZZ
if len(params) == 1:
if any(n >= circuit.num_qubits for n in params):
print(f"[Dealer] Card numbers must be less than {circuit.num_qubits}")
return None
if gate == "H": circuit.h(params[0])
elif gate == "S": circuit.s(params[0])
elif gate == "T": circuit.t(params[0])
elif gate == "Z": circuit.z(params[0])
else:
print(f"[Dealer] The 1-qubit move '{gate}' isn't recognized at this table.")
return None
…………
phase = self.degrees_to_radians(params[0])
if gate == "RXX": circuit.rxx(phase, params[1], params[2])
elif gate == "RYY": circuit.ryy(phase, params[1], params[2])
elif gate == "RZZ": circuit.rzz(phase, params[1], params[2])
else:
print(f"[Dealer] The 3-qubit move '{gate}' isn't recognized at this table.")
return None
接下来这道题对于参数进行了检查:第一个检查是这个参数不能为0,包括我们的角度,和作用的门的index。
这个检查可以绕过,并且需要绕过,因为我们可能会对0号线路进行一些操作,绕过原因是因为python的数组索引可以用负数来倒数,比如这道题一共两条量子电路,-2为0就是倒数第二个,-1为1倒数第一个。
if any(p == 0 for p in params):
print("[Dealer] Hey, don't tamper with the house card — that's forbidden.")
return None
第二个检查是对于三个参数的门,例如RXX,后两个参数即为门的index不能是一个门,也就是不能RXX(….,1,1)
这个检查不用绕过,因为我们本来用这种门也不会让两个index一样。
if params[1] == params[2]:
print("[Dealer] Control and target cards must be different.")
return None
validate_entropy
这个代码是对于线路0进行测量,验证是不是随机分布的,验证方法为测量很多次,然后统计1和0出现的概率是不是50%
def validate_entropy(self, base_circuit, shots = 100_000):
circuit = base_circuit.copy()
circuit.add_register(ClassicalRegister(1))
circuit.measure(0, 0)
compiled = transpile(circuit, self.backend)
results = self.backend.run(compiled, shots = shots).result()
counts = results.get_counts()
binomial_test = binomtest(counts.get('0', 0), n = shots, p = 0.5, alternative = 'two-sided')
if binomial_test.pvalue < 0.01:
return False
return True
extract_numbers
这里是memory是每次测量的结果,这个函数是把测量的结果每6个当作一个数字,并且每次测量是两条线路0和1,他把0号线路作为lotto,把1号线路作为test。然后每6个二进制数字变成一个十进制数字并且模42+1。
def extract_numbers(self, memory):
print(memory)
lotto_numbers = []
testing_numbers = []
for i in range(0, len(memory), 6):
bits = memory[i : i + 6]
lotto_number = ""
testing_number = ""
for testing_bit, lotto_bit in bits:
lotto_number += str(lotto_bit)
testing_number += str(testing_bit)
lotto_number = int(lotto_number, 2) % 42 + 1
testing_number = int(testing_number, 2) % 42 + 1
lotto_numbers.append(lotto_number)
testing_numbers.append(testing_number)
return lotto_numbers, testing_numbers
run_lotto
接下来是跑我们地电路,跑36次每次测量,并且0号线路要满足之前分布条件的函数的分布。之后因为每6个数字为一组,所以我们的test有6个十进制数字,lotto也有6个十进制数字。
def run_lotto(self, instructions, shots = 36):
circuit = self.generate_circuit(instructions)
if not circuit:
return None
if not self.validate_entropy(circuit):
print("[Dealer] The draw fizzles... not enough quantum energy in your play.")
return None
circuit.measure_all()
print(circuit)
compiled = transpile(circuit, self.backend)
results = self.backend.run(compiled, shots = shots, memory = True).result()
return self.extract_numbers(results.get_memory())
main
这里还是老样子让我们给系统电路门,接下来是运行电路,给我们展示test线路的6个数字,让我猜测lotto的六个数字,如果猜对了就能获得flag。
def main():
print("""
╔═════════════════════╗
║ ⚛ Welcome to the QLotto table ⚛ ║
╠═════════════════════╣
║ Minimum bet : 100,000 credits ║
║ Provider : Qubitrix™ ║
╚═════════════════════╝
""")
lotto = QuantumLotto()
instructions = input("[Dealer] Place your quantum moves : ")
numbers = lotto.run_lotto(instructions)
if not numbers:
return
lotto_numbers, testing_numbers = numbers
if lotto_numbers == testing_numbers:
print("[Dealer] Trying to mirror the house's numbers, are we?")
return
print(f"[Dealer] Your draws are: {testing_numbers}")
guess_numbers = input("[Dealer] Place your six bets on the table : ")
try:
guess_numbers = [ int(n) for n in guess_numbers.split(",") ]
except:
print("[Dealer] Your wagers must be integers.")
return
if len(guess_numbers) != 6 or any(n < 1 or n > 42 for n in guess_numbers):
print("[Dealer] Place six bets on the table, numbered 1 through 42.")
return
if guess_numbers == lotto_numbers:
print("The table erupts in chaos — you've cracked the QLotto!")
print(f"[Dealer] Your jackpot:")
else:
print(f"[Dealer] Oh, that's a shame, the numbers were {lotto_numbers}")
#
02
量子计算相关知识
H,X,CNOT门上篇文章讲过了,这里讲新的RXX门,也是本题的重点
这个门可以让两条线路陷入纠缠,RYY和RZZ类似,本题目不用先不讲解。
这里可以尝试给角度theta带入几个特殊值来看看都是啥:
theta为0:
和I是一样的,没有变化
theta为pi/2:
作用后效果:
让两条线路陷入纠缠,有相同相反的,如果两条线路一样,那么会进入相同的纠缠,即为0号为0时1号也为0,观测一个线路坍缩后可直接推另一个,比如观测0号为0,就可以知道1号一定为0,因为整体坍缩到了00态,11的可能性无线趋于0。
同理,如果两个线路初始情况不一样,一个0一个1,那也会陷入相反的叠加,观测一个线路坍缩后可直接推另一个,比如观测0号为0,就可以知道1号一定为1,因为这个是相反的并且整体坍缩到了01态,10的可能性无线趋于0.===>这正是我们需要的,因为既要保证通过test能推测出来lotto,又要保证test和lotto不一样。
theta为pi:
带相位的纠缠
03
解题
根据上述的分析最后构造的电路如图,首先因为0号线路最开始有个H门,我们先给一个H门抵消掉,此时0号和1号线路都是0态。
然后我们要转变一下其中一个,这里0号或者1号都行,我就在0号了,本来应该是用X门,但是这道题不能用X门,所以我们采用X门的等效电路即为HZH,通过矩阵演算可以知道X=HZH,此时0号是1态,1号是0态。
最后我们直接使用RXX(pi/2,-2,-1)来按照刚才的办法让两条线路纠缠。
接下来debug一下可以看到lotto和test的二进制是正好相反的,我们逆向解码即可。
最后附上逆向解码脚本:
def solve_qlotto(testing_numbers):
"""
根据 Testing Numbers 计算 Lotto Numbers。
原理:Lotto bits = ~Testing bits (按位取反)
推导公式:Lotto = ((21 - (Test - 1)) % 42) + 1
"""
lotto_numbers = []
for t in testing_numbers:
# 公式推导:
# Raw_Lotto = 63 - Raw_Test
# Final_Lotto = (Raw_Lotto % 42) + 1
# Final_Lotto = ((63 - Raw_Test) % 42) + 1
# = ((21 - Raw_Test) % 42) + 1
# Raw_Test 与 (t-1) 同余 42,所以可以直接替换
val = ((21 - (t - 1)) % 42) + 1
lotto_numbers.append(val)
return lotto_numbers
if __name__ == "__main__":
print("--- QLotto Solver ---")
print("请先在服务器输入量子指令: H:0;RXX:90,0,1;H:0;Z:0;H:0")
try:
user_input = input("请输入服务器返回的 draws 数组 (例如 25,21,10...): ")
# 处理可能的方括号
clean_input = user_input.replace('[', '').replace(']', '')
if not clean_input.strip():
print("输入为空")
exit()
testing_nums = [int(x.strip()) for x in clean_input.split(',')]
if len(testing_nums) != 6:
print(f"警告: 输入了 {len(testing_nums)} 个数字,通常需要 6 个。")
winning_nums = solve_qlotto(testing_nums)
print("\n[+] 计算出的必胜数字 (复制粘贴回服务器):")
print(",".join(map(str, winning_nums)))
except ValueError:
print("[-] 输入格式错误,请确保输入的是逗号分隔的数字。")
#
看雪ID:枫林路大砍刀
https://bbs.kanxue.com/user-home-1008959.htm
*本文为看雪论坛优秀文章,由 枫林路大砍刀 原创,转载请注明来自看雪社区
往期推荐
从ANGR-CTF项目入手ANGR和符号执行技术
AI时代-逆向工作者该如何用好这一利器
EXIF解析缓冲区溢出漏洞分析与利用
从C到Pwn:栈溢出漏洞利用实战入门
Android-ARM64的VMP分析和还原
球分享
球点赞
球在看
点击阅读原文查看更多
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:看雪学苑 枫林路大砍刀 枫林路大砍刀《量子安全 quantum ctf Qlotto Hack the box》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论