From a61cb758d83c69329a3fb61c99764e92b7cbe02a Mon Sep 17 00:00:00 2001 From: zzh1996 Date: Thu, 5 Nov 2020 04:17:22 +0800 Subject: [PATCH] add 5 writeups --- official/从零开始的 HTTP 链接/README.md | 30 +++++ official/永不溢出的计算器/README.md | 28 ++++ official/永不溢出的计算器/src/Dockerfile | 4 + official/永不溢出的计算器/src/server.py | 67 ++++++++++ official/自复读的复读机/README.md | 44 +++++++ official/自复读的复读机/src/Dockerfile | 4 + official/自复读的复读机/src/checker.py | 41 ++++++ official/自复读的复读机/src/runner.py | 1 + official/超简单的世界模拟器/README.md | 55 ++++++++ official/超简单的世界模拟器/src/Dockerfile | 3 + official/超简单的世界模拟器/src/game.py | 105 +++++++++++++++ official/超简单的世界模拟器/src/solve.py | 71 +++++++++++ official/超自动的开箱模拟器/README.md | 36 ++++++ official/超自动的开箱模拟器/src/Dockerfile | 3 + .../src/unboxing_simulator.py | 120 ++++++++++++++++++ 15 files changed, 612 insertions(+) create mode 100644 official/永不溢出的计算器/src/Dockerfile create mode 100644 official/永不溢出的计算器/src/server.py create mode 100644 official/自复读的复读机/src/Dockerfile create mode 100644 official/自复读的复读机/src/checker.py create mode 100644 official/自复读的复读机/src/runner.py create mode 100644 official/超简单的世界模拟器/src/Dockerfile create mode 100644 official/超简单的世界模拟器/src/game.py create mode 100644 official/超简单的世界模拟器/src/solve.py create mode 100644 official/超自动的开箱模拟器/src/Dockerfile create mode 100644 official/超自动的开箱模拟器/src/unboxing_simulator.py diff --git a/official/从零开始的 HTTP 链接/README.md b/official/从零开始的 HTTP 链接/README.md index bb1effd..76df9fd 100644 --- a/official/从零开始的 HTTP 链接/README.md +++ b/official/从零开始的 HTTP 链接/README.md @@ -1 +1,31 @@ # 从零开始的 HTTP 链接 + +0 号端口是保留端口,大部分应用程序会认为 0 号端口无效从而拒绝连接。但是,TCP 数据包头部的端口字段是两个字节,可以表示 0~65535 的端口号,所以构造 0 号端口的数据包是完全可行的。 + +经过我的测试,在大多数网络环境下,包括校园网、家庭宽带、国内外的云服务器等,即使经过了家用路由器的 NAT,0 号端口的流量也都是可以正常通过的。少数网络环境可能封禁了 0 号端口的流量,此时你可能需要更换一个网络环境或者从云服务器(包括一些免费的持续集成和沙箱环境等)发起连接。 + +## 简单的解法 + +在 Linux 下,安装 socat,然后运行 `socat TCP-LISTEN:20000,fork,reuseaddr TCP:202.38.93.111:0`,再从浏览器访问 localhost:20000 即可。 + +要注意,如果你在用 Linux 虚拟机,很多虚拟机软件的 NAT 实现不能正确处理 0 号端口,你需要把虚拟机的网络设置从 NAT 模式改为桥接模式。 + +## 其他理论上可行的解法 + +- 直接在 Linux 上使用 socket 编程来连接,socket 相关的函数没有限制 0 号端口的使用 +- 使用比较古老版本的 curl 工具,curl 的较旧版本遇到 0 号端口时不会报错退出 +- 修改浏览器源代码直接去掉相关限制 +- 使用 scapy 来直接构造数据包 +- 使用 iptables 等工具修改目标端口 +- 修改 Linux 内核 + +## 花絮:服务器是如何部署的? + +服务器上这个网站在 20000 端口提供服务,然后我加了两条 iptables: + +``` +iptables -t nat -I PREROUTING -p tcp --dport 20000 -j REDIRECT --to-port 0 +iptables -t nat -I PREROUTING -p tcp --dport 0 -j REDIRECT --to-port 20000 +``` + +相当于把 20000 端口的流量与 0 端口的流量互换了一下 diff --git a/official/永不溢出的计算器/README.md b/official/永不溢出的计算器/README.md index 92a7505..9d5384b 100644 --- a/official/永不溢出的计算器/README.md +++ b/official/永不溢出的计算器/README.md @@ -1 +1,29 @@ # 永不溢出的计算器 + +这是一个在 mod n 意义下提供加减乘除和乘方开方运算的计算器。 + +flag 对应的字符串在 mod n 意义下计算 65537 次方,就是 `e=65537` 的 RSA 加密。 + +如果我们想解密 flag,需要对 n 进行分解。 + +我们知道,计算 mod n 的加减乘除和乘方,并不需要知道 n 的分解,知道 n 本身已经足够可以进行这些计算,所以这些计算并不能提供任何额外的信息。唯一可能可以提供额外信息的计算就是开平方的运算。 + +此时,搜索能力较强的同学可能已经找到了[这篇文章](https://crypto.stackexchange.com/questions/34061/factoring-large-n-given-oracle-to-find-square-roots-modulo-n)。 + +解法其实很简单。 + +首先我们通过计算 `0 - 1` 可以得到 n - 1 的值,从而知道 n。 + +当我们选择一个 0 ~ n 范围内随机的整数 x 后,我们计算 x ^ 2 mod n,然后发给服务器开平方得到 y。 + +每一个能够在 mod n 意义下开平方(二次剩余)的非零整数都有 4 个平方根,分别是 a、b、n-a、n-b。 + +如果我们拿到的 y 不巧是 x 或者 n-x 的话(1/2 的概率),我们没有得到任何有用信息,这时我们重试就可以。 + +如果我们拿到的 y 是另外两个数的话,此时我们有 x^2 = y^2 mod n,也就是 n | x^2 - y^2,也就是 n | (x+y)(x-y)。此时 x+y 和 x-y 都不是 n 的倍数,而 (x+y)(x-y) 却是 n 的倍数,说明 x+y 和 x-y 中分别包含一个 n 的真因数。此时计算 gcd(n, x+y) 即可得到 n 的一个真因数,从而分解 n。 + +最后,使用标准的 RSA 解密公式就可以解密得到 flag 了。 + +## 花絮 + +这道题是根据一道原理相同的 ACM 题改编的 diff --git a/official/永不溢出的计算器/src/Dockerfile b/official/永不溢出的计算器/src/Dockerfile new file mode 100644 index 0000000..62764d5 --- /dev/null +++ b/official/永不溢出的计算器/src/Dockerfile @@ -0,0 +1,4 @@ +FROM python:3.8 +RUN python3 -m pip install -U sympy +COPY server.py / +CMD ["/usr/local/bin/python3", "-u", "/server.py"] diff --git a/official/永不溢出的计算器/src/server.py b/official/永不溢出的计算器/src/server.py new file mode 100644 index 0000000..8902d19 --- /dev/null +++ b/official/永不溢出的计算器/src/server.py @@ -0,0 +1,67 @@ +from sympy import sqrt_mod, nextprime +from sympy.ntheory.modular import crt +import random +import os +import hashlib +import math + +flag = int.from_bytes(open("flag", "rb").read(), "big") +token = os.environ["hackergame_token"] +secret = "43c5cf58ed8872cbbfb9625f0dcaf6e0" + +p = int.from_bytes(hashlib.sha512((secret + "1" + token).encode()).digest(), "big") +q = int.from_bytes(hashlib.sha512((secret + "2" + token).encode()).digest(), "big") +while True: + p = nextprime(p) + q = nextprime(q) + n = p * q + if math.gcd(65537, (p - 1) * (q - 1)) == 1: + break + +print("This is a calculator mod n") +print("You can try: a + b, a - b, a * b, a / b, a ^ b, sqrt(a)") +print("Be aware of the spaces around operators") +print("flag ^ 65537 =", pow(flag, 65537, n)) + +while True: + line = input("> ") + line = line.strip() + try: + if line.startswith("sqrt(") and line.endswith(")"): + a = int(line[5:-1]) % n + a1l = sqrt_mod(a, p, True) + a2l = sqrt_mod(a, q, True) + ans = [] + for a1 in a1l: + for a2 in a2l: + r = int(crt([p, q], [a1, a2])[0]) % n + assert pow(r, 2, n) == a + ans.append(r) + if ans: + print(min(ans)) + else: + print("Math error") + else: + a, op, b = line.split() + a = int(a) + b = int(b) + if op == "+": + print((a + b) % n) + elif op == "-": + print((a - b) % n) + elif op == "*": + print((a * b) % n) + elif op == "/": + try: + print((a * pow(b, -1, n)) % n) + except ValueError: + print("Math error") + elif op == "^": + try: + print(pow(a, b, n)) + except ValueError: + print("Math error") + else: + print("Invalid input") + except ValueError: + print("Invalid input") diff --git a/official/自复读的复读机/README.md b/official/自复读的复读机/README.md index dff41cd..4010df5 100644 --- a/official/自复读的复读机/README.md +++ b/official/自复读的复读机/README.md @@ -1 +1,45 @@ # 自复读的复读机 + +## 解题思路 + +使用搜索引擎搜索“输出自己的程序”或者类似的词,可以查到这类程序叫做 Quine。可以很容易在网上查到很多 Python 3 的 Quine,例如: + +`exec(s:='print("exec(s:=%r)"%s)')` + +还有 + +`s='s=%r;print(s%%s)';print(s%s)` + +等等。 + +这道题要求输出代码的逆序以及代码的哈希,我们可以修改上面的 Quine: + +输出自己逆序的程序:`exec(s:='print(("exec(s:=%r)"%s)[::-1])')`(把 print 的内容用括号括起来然后逆序即可) + +但这样提交之后有一个问题,就是输出比代码多了一个 `\n`,这是由于输入的代码结尾没有换行符而 `print` 输出的内容结尾会自带换行符,我们只需要让 `print` 不输出换行符,加一个 `,end=""` 即可。 + +对于第二问,我们把 print 的内容用 Python 自带的计算 sha256 的函数包起来即可。 + +## 答案 + +第一问(每行都是一个可能的构造): + +`exec(s:='print(("exec(s:=%r)"%s)[::-1],end="")')` + +`s='s=%r;print((s%%s)[::-1],end="")';print((s%s)[::-1],end="")` + +第二问(每行都是一个可能的构造): + +`exec(s:='print(__import__("hashlib").sha256(("exec(s:=%r)"%s).encode()).hexdigest(),end="")')` + +`exec(s:='import hashlib;print(hashlib.sha256(("exec(s:=%r)"%s).encode()).hexdigest(),end="")')` + +`import hashlib;s='import hashlib;s=%r;print(hashlib.sha256((s%%s).encode()).hexdigest(),end="")';print(hashlib.sha256((s%s).encode()).hexdigest(),end="")` + +## 其他 + +要注意的是,这道题的程序是使用标准输入读入代码然后用 `exec()` 执行的,所以并不能使用 `print(open(__file__).read())` 之类输出自己源代码文件的方案。 + +你可以使用 `import os; os.system('ls')` 之类的代码来在服务器上任意执行命令,但是进程是以低权限运行的,这种方法不能读到 flag。 + +你也可以通过读取当前进程的内存来找到自己的源代码然后输出,理论上完全可行,但我没有实现,如果有选手做到了可以提 PR 来分享给大家。 diff --git a/official/自复读的复读机/src/Dockerfile b/official/自复读的复读机/src/Dockerfile new file mode 100644 index 0000000..9234b2e --- /dev/null +++ b/official/自复读的复读机/src/Dockerfile @@ -0,0 +1,4 @@ +FROM python:3.8 +COPY checker.py / +COPY runner.py / +CMD ["/usr/local/bin/python3", "-u", "/checker.py"] diff --git a/official/自复读的复读机/src/checker.py b/official/自复读的复读机/src/checker.py new file mode 100644 index 0000000..177b7e7 --- /dev/null +++ b/official/自复读的复读机/src/checker.py @@ -0,0 +1,41 @@ +import subprocess +import hashlib + +if __name__ == "__main__": + code = input("Your one line python code to exec(): ") + print() + if not code: + print("Code must not be empty") + exit(-1) + p = subprocess.run( + ["su", "nobody", "-s", "/bin/bash", "-c", "/usr/local/bin/python3 /runner.py"], + input=code.encode(), + stdout=subprocess.PIPE, + ) + + if p.returncode != 0: + print() + print("Your code did not run successfully") + exit(-1) + + output = p.stdout.decode() + + print("Your code is:") + print(repr(code)) + print() + print("Output of your code is:") + print(repr(output)) + print() + + print("Checking reversed(code) == output") + if code[::-1] == output: + print(open("/root/flag1").read()) + else: + print("Failed!") + print() + + print("Checking sha256(code) == output") + if hashlib.sha256(code.encode()).hexdigest() == output: + print(open("/root/flag2").read()) + else: + print("Failed!") diff --git a/official/自复读的复读机/src/runner.py b/official/自复读的复读机/src/runner.py new file mode 100644 index 0000000..4107d3c --- /dev/null +++ b/official/自复读的复读机/src/runner.py @@ -0,0 +1 @@ +exec(input()) diff --git a/official/超简单的世界模拟器/README.md b/official/超简单的世界模拟器/README.md index 3d70d8c..8a6f9d1 100644 --- a/official/超简单的世界模拟器/README.md +++ b/official/超简单的世界模拟器/README.md @@ -1 +1,56 @@ # 超简单的世界模拟器 + +这道题**手工构造**与**写代码暴力搜索**都可以解决。 + +使用搜索引擎搜索“生命游戏”或“Game of Life”都可以找到很多相关的资料,其中会提到生命游戏的演化规则和一些有趣的构造。 + +## 手工构造解法 + +为了消除右上角的方块,我们只要放置一个水平移动的“太空船”即可: + +``` +000000000000000 +001111000000000 +010001000000000 +000001000000000 +010010000000000 +000000000000000 +000000000000000 +000000000000000 +000000000000000 +000000000000000 +000000000000000 +000000000000000 +000000000000000 +000000000000000 +000000000000000 +``` + +这样可以得到第一个 flag。 + +消除第二个方块有点困难,因为(比较小的)飞行器只能沿着对角线方向和水平竖直方向飞行,我们的可控制区域比较小,无法让飞行器移动后与方块碰撞。 + +所以我们可以找一些会扩散比较大的初始状态,例如[这个链接](https://www.conwaylife.com/wiki/Methuselah)里面讲的例子。 + +一个可行的例子: + +``` +000000000000000 +000000000000000 +000000000000000 +000000000000000 +000000000000000 +000000000000000 +000000011000000 +000000001100000 +000000011000000 +000000010000000 +000000000000000 +000000000000000 +000000000000000 +000000000000000 +000000000000000 +``` +## 暴力解法 + +直接随机生成 0/1 矩阵,大概几十次就可以找到一个,Python 代码见[solve.py](src/solve.py) diff --git a/official/超简单的世界模拟器/src/Dockerfile b/official/超简单的世界模拟器/src/Dockerfile new file mode 100644 index 0000000..8f26c22 --- /dev/null +++ b/official/超简单的世界模拟器/src/Dockerfile @@ -0,0 +1,3 @@ +FROM python:3.8 +COPY game.py / +CMD ["/usr/local/bin/python3", "-u", "/game.py"] diff --git a/official/超简单的世界模拟器/src/game.py b/official/超简单的世界模拟器/src/game.py new file mode 100644 index 0000000..c52ff05 --- /dev/null +++ b/official/超简单的世界模拟器/src/game.py @@ -0,0 +1,105 @@ +import time + + +MAP_SIZE = 50 +CONTROL_SIZE = 15 +STEP = 200 +INTERVAL = 0.1 +FLAGS = [(5, 45), (25, 45)] + + +def flag_range(flag): + x, y = flag + for i in range(2): + for j in range(2): + yield x + i, y + j + + +class Game: + def __init__(self, W, H): + self.W = W + self.H = H + self.map = [[0 for _ in range(W)] for _ in range(H)] + + for flag in FLAGS: + for x, y in flag_range(flag): + self.map[x][y] = 1 + + def print(self): + buf = "" + for i in range(self.H): + for j in range(self.W): + flag = any((i, j) in flag_range(f) for f in FLAGS) + if self.map[i][j]: + buf += "\x1b[48;5;0m" + buf += "\x1b[38;5;15m" + else: + buf += "\x1b[48;5;15m" + buf += "\x1b[38;5;0m" + if flag: + buf += "[]" + else: + buf += " " + buf += "\x1b[0m" + buf += "\n" + return buf + + def step(self): + new = [[0 for _ in range(self.W)] for _ in range(self.H)] + for i in range(self.H): + for j in range(self.W): + cnt = 0 + for io in -1, 0, 1: + for jo in -1, 0, 1: + if 0 <= i + io < self.H: + if 0 <= j + jo < self.W: + if io != 0 or jo != 0: + if self.map[i + io][j + jo]: + cnt += 1 + if cnt == 3: + new[i][j] = 1 + elif cnt == 2: + new[i][j] = self.map[i][j] + else: + new[i][j] = 0 + self.map = new + + +game = Game(50, 50) + +print(game.print()) + +print(f"Your {CONTROL_SIZE}x{CONTROL_SIZE} 0/1 matrix at upper left corner: ") +for i in range(CONTROL_SIZE): + line = input() + if not line: + break + assert len(line) <= CONTROL_SIZE and set(line) <= {"0", "1"} + for j in range(len(line)): + game.map[i][j] = int(line[j]) + +print("\033c", end="") +print(game.print()) +print("Let's begin...") +time.sleep(1) + +for i in range(STEP): + time.sleep(INTERVAL) + game.step() + buf = game.print() + print("\033c", end="") + print(i + 1) + print(buf) + +cnt = 0 +for i, flag in enumerate(FLAGS): + if all(not game.map[x][y] for x, y in flag_range(flag)): + cnt += 1 +if cnt == 1: + print("You destroyed one block, flag 1:") + print(open("flag1").read()) +elif cnt == 2: + print("You destroyed two blocks, flag 2:") + print(open("flag2").read()) +else: + print("Flag block not destroyed") diff --git a/official/超简单的世界模拟器/src/solve.py b/official/超简单的世界模拟器/src/solve.py new file mode 100644 index 0000000..ef2caa2 --- /dev/null +++ b/official/超简单的世界模拟器/src/solve.py @@ -0,0 +1,71 @@ +import random + +MAP_SIZE = 50 +CONTROL_SIZE = 15 +STEP = 200 +FLAGS = [(5, 45), (25, 45)] + + +def flag_range(flag): + x, y = flag + for i in range(2): + for j in range(2): + yield x + i, y + j + + +class Game: + def __init__(self, W, H): + self.W = W + self.H = H + self.map = [[0 for _ in range(W)] for _ in range(H)] + + for flag in FLAGS: + for x, y in flag_range(flag): + self.map[x][y] = 1 + + def step(self): + new = [[0 for _ in range(self.W)] for _ in range(self.H)] + for i in range(self.H): + for j in range(self.W): + cnt = 0 + for io in -1, 0, 1: + for jo in -1, 0, 1: + if 0 <= i + io < self.H: + if 0 <= j + jo < self.W: + if io != 0 or jo != 0: + if self.map[i + io][j + jo]: + cnt += 1 + if cnt == 3: + new[i][j] = 1 + elif cnt == 2: + new[i][j] = self.map[i][j] + else: + new[i][j] = 0 + self.map = new + +random.seed(2020) +while True: + game = Game(MAP_SIZE, MAP_SIZE) + for i in range(CONTROL_SIZE): + for j in range(CONTROL_SIZE): + game.map[i][j] = random.randrange(2) + s = '' + for line in game.map[:CONTROL_SIZE]: + for i in line[:CONTROL_SIZE]: + s += str(i) + s += '\n' + + last = game.map + for i in range(STEP): + game.step() + if game.map == last: + break + last = game.map + + cnt = 0 + for i, flag in enumerate(FLAGS): + if all(not game.map[x][y] for x, y in flag_range(flag)): + cnt += 1 + print("flags =", cnt) + if cnt: + print(s) diff --git a/official/超自动的开箱模拟器/README.md b/official/超自动的开箱模拟器/README.md index d3e9d42..a0a326a 100644 --- a/official/超自动的开箱模拟器/README.md +++ b/official/超自动的开箱模拟器/README.md @@ -1 +1,37 @@ # 超自动的开箱模拟器 + +分析源代码可知,题目是做这样的一件事情: + +在 128 轮模拟中,每一轮你的 Brainfuck 语言程序会与一个开箱模拟器进行交互。 + +你的程序输出 1、2、3 分别表示向左移动、向右移动、开箱。 + +你有 64 次开箱机会,开箱后你的程序的输入将得到开箱的结果。 + +每一轮中你都需要在 64 次机会内成功找到包含 target 数字的箱子,而 target 数字在这 128 轮中各不相同。 + +箱子是随机排列打乱的,每一轮的顺序完全相同,但是你的程序无法在轮与轮之间携带信息。 + +这个问题被称为百囚徒问题([100 prisoners problem](https://en.wikipedia.org/wiki/100_prisoners_problem))。 + +看似不可能的挑战,其实只要每个人像链表一样把打开箱子的内容作为下一个打开的箱子的编号,就可以以非常高的概率成功。 + +这是因为,把箱子内数字的置换拆解成一堆不相交的循环,只要最大的循环的阶小于等于 64,所有人就都可以成功,而这样的概率大约是 0.3 左右。 + +具体的数学证明可自行查阅“百囚徒问题”相关资料。 + +接下来我们要做的事情就是用 Brainfuck 语言实现这个开箱逻辑,这里有个坑在于箱子内的数字是 1~128,而箱子本身的下标是 0~127。 + +如何编写 Brainfuck 代码可以参考相关的资料,这里我手工编写的代码如下: + +`+> >+>++>+++<<< <[> -[>.< -] , - [>>.<< -] >>>.<<< <]` + +它的功能是循环做以下事情:输入一个数(上一个箱子里的数),减一得到 n,然后把位置移动到最左(输出很多次 1),再向右移动 n 次(循环,每次数字减一的同时输出 2,直到数字变为 0),最后开箱(输出 3) + +多提交几次即有非常大的概率可以拿到 flag。 + +## 花絮 + +我本来想把百囚徒困境出成一个简单的交互题,但这是做不到的,因为我们需要选手提交一个“策略”,这个“策略”需要在不同的环境下被执行很多次,并且不同次之间不能传递任何信息。如果直接让选手来开箱,那么无法做到不同次之间不传递信息。所以我让选手提交图灵完备的 Brainfuck 语言代码来表示“策略”,模拟器也比较好实现。 + +使用向左向右移动和开箱操作,而不是直接让程序输出开箱的位置,是故意的设计。这是想让选手真正理解这个问题之后再进行尝试,而不是随便试几个简单的符号组合(甚至在不理解 Brainfuck 各种符号的含义的情况下)就把题目解出了。 diff --git a/official/超自动的开箱模拟器/src/Dockerfile b/official/超自动的开箱模拟器/src/Dockerfile new file mode 100644 index 0000000..22b560d --- /dev/null +++ b/official/超自动的开箱模拟器/src/Dockerfile @@ -0,0 +1,3 @@ +FROM python:3.8 +COPY unboxing_simulator.py / +CMD ["/usr/local/bin/python3", "-u", "/unboxing_simulator.py"] diff --git a/official/超自动的开箱模拟器/src/unboxing_simulator.py b/official/超自动的开箱模拟器/src/unboxing_simulator.py new file mode 100644 index 0000000..35f0aaf --- /dev/null +++ b/official/超自动的开箱模拟器/src/unboxing_simulator.py @@ -0,0 +1,120 @@ +from collections import deque +from random import SystemRandom + + +class BF: + def __init__(self, code): + self.code = code + self.output = deque() + self.input = deque() + self.data = [0] + self.codeptr = 0 + self.dataptr = 0 + self.generate_map() + + def generate_map(self): + self.map = {} + stack = [] + for i, op in enumerate(self.code): + if op == "[": + stack.append(i) + elif op == "]": + if not stack: + print("Error: Brackets not matching") + exit(-1) + pos = stack.pop() + self.map[pos] = i + self.map[i] = pos + if stack: + print("Error: Brackets not matching") + exit(-1) + + def step(self): + op = self.code[self.codeptr] + if op == ">": + self.dataptr += 1 + if self.dataptr == len(self.data): + self.data.append(0) + elif op == "<": + if self.dataptr: + self.dataptr -= 1 + elif op == "+": + self.data[self.dataptr] += 1 + self.data[self.dataptr] %= 256 + elif op == "-": + self.data[self.dataptr] -= 1 + self.data[self.dataptr] %= 256 + elif op == "[": + if self.data[self.dataptr] == 0: + self.codeptr = self.map[self.codeptr] + elif op == "]": + if self.data[self.dataptr] != 0: + self.codeptr = self.map[self.codeptr] + elif op == ".": + self.output.append(self.data[self.dataptr]) + elif op == ",": + if not self.input: + print("Error: No input") + exit(-1) + self.data[self.dataptr] = self.input.popleft() + self.codeptr += 1 + + def run_till_output(self): + while self.codeptr < len(self.code): + self.step() + if self.output: + return self.output.popleft() + return None + + +class Game: + def __init__(self, nboxes, nguess): + self.nboxes = nboxes + self.nguess = nguess + rnd = SystemRandom() + self.boxes = list(range(1, nboxes + 1)) + rnd.shuffle(self.boxes) + + def play_one_round(self, target, code): + bf = BF(code) + bf.input.append(target) + box_key = 0 + guesses = 0 + while guesses < self.nguess: + out = bf.run_till_output() + if out is None: + print("Error: No enough output after BF ended") + exit(-1) + if out == 1: # move left + if box_key > 0: + box_key -= 1 + elif out == 2: # move right + if box_key < self.nboxes - 1: + box_key += 1 + elif out == 3: # open current box + guesses += 1 + box_value = self.boxes[box_key] + print(f"- Guess {guesses}/{self.nguess}: {box_key}->{box_value}") + if box_value == target: + print("Target found") + return True + bf.input.append(box_value) + print(f"Target not found after {self.nguess} guesses") + return False + + def play(self, code): + for i in range(1, self.nboxes + 1): + print(f"Running round {i}/{self.nboxes}, target = {i}") + if not self.play_one_round(i, code): + return False + return True + + +if __name__ == "__main__": + code = input("Your unboxing BF code: ") + game = Game(128, 64) + if game.play(code): + print("You win") + print(open("flag").read()) + else: + print("You lose")