总分5500, 总排名:38 / 2460 ,组内排名:6 / 389
今年应该是第四次打hackergame了,前两次码力不强同时也是在试水,第三年摸到了hackergame校内三等奖的最后一名,今年摸到了二等奖的最后一名,明年可以摸到一等奖的最后一名吗(bushi
因为有点懒所以随便记一下解法吧(?
先不说关于我从零开始独自在异世界转生成某大厂家的 LLM 龙猫女仆这件事可不可能这么离谱,发现 Hackergame 内容审查委员会忘记审查题目标题了ごめんね,以及「这么长都快赶上轻小说了真的不会影响用户体验吗🤣」
因为是本 writeup 唯一有含金量的地方所以放到前面了
flag1 直接生成配合gpt等大模型还算能猜出来的所以不表,直接flag2
本质是一个猜词游戏,但需要猜的符合分布,所以直接dfs配合混淆条件进行剪枝,可以得到符合条件的解
先把 qwen 的 conv_template 抠出来之后换成 prompt 的形式(这一步也可以在 llama_cpp 里面打断点得到),然后带着温度来一步步预测下一个token来dfs确保可以获得多样性的结果
import random
from llama_cpp import Llama
llm = Llama(
model_path="llm_censored_docker/qwen2.5-3b-instruct-q8_0.gguf",
n_ctx=1024,
seed=random.SystemRandom().randint(0, 2**64),
verbose=False,
)
prompt = "<|im_start|>system\nYou are a professional CTF player.<|im_end|>\n<|im_start|>user\nWrite a short article for Hackergame 2024 (中国科学技术大学 (University of Science and Technology of China) 第十一届信息安全大赛) in English. The more funny and unreal the better. About 500 words.<|im_end|>\n<|im_start|>assistant\n"
def tok(t):
return llm.tokenize(
t.encode("utf-8"),
add_bos=False,
special=True,
)
prompt = tok(prompt)
target_chars = "hkrgameofustcx "
def dfs(prompt, text, depth=5):
if "x" not in text:
# 如果没有需要替换的字符,直接返回完整的 prompt
return text
line = "NOT FOUND"
for _ in range(depth):
token = llm.create_completion(
prompt, max_tokens=1
)["choices"][0]["text"]
if len(token) > len(text):
continue
comp_text, next_text = text[: len(token)], text[len(token) :]
if all([g in target_chars if r == "x" else g == r for g, r in zip(token, comp_text)]):
next_prompt = prompt + tok(token)
line = dfs(next_prompt, next_text, depth)
if line != "NOT FOUND":
return token + line
return "NOT FOUND"
# 读取文件并处理每一行
with open(
"llm_censored_docker/censor_lv2/after.txt", "r"
) as f:
texts_after = f.readlines()
for line in texts_after:
if "x" in line:
for depth in range(5, 101, 5):
next = dfs(prompt, line, depth)
if next != "NOT FOUND":
line = next
break
else:
print("NOT FOUND")
exit(1)
print(line, end="")
prompt = prompt + tok(line)
问题来了,这样获得的文本虽然符合条件但大概率是不符合 hash 的
实际上观察多次生成后文本的diff,发现两个文本之间的不同之处仅仅是几个词的变化,比如 our <-> the
, show off <-> showcase
,因此只需要生成足够多的文章,然后对不同之处排列一遍找到 hash 正确的值即可
import os
import hashlib
os.chdir("llm_censored_docker/censor_lv2")
num = 6 # 只用了六篇输出
file_list = [f"before{i}.txt" for i in range(1, num + 1)]
texts = []
for file in file_list:
texts.append(open(file, "r").read())
diff = {}
# {index: set(diff_char_ses)}
# 生成diff表
for i in range(1, num):
origin_text = texts[i]
for j in range(i + 1, num):
diff_text = texts[j]
diff_index = [
index for index, (c1, c2) in enumerate(zip(origin_text, diff_text)) if c1 != c2
]
# width小于5的diff合并
width = 5
diff_merge = []
last_index_begin = -1000
last_index_end = -1000
for index in diff_index:
if index - last_index_end > width:
if last_index_begin != -1000:
diff_merge.append((last_index_begin, last_index_end))
last_index_begin = index
last_index_end = index
if last_index_begin != -1000:
diff_merge.append((last_index_begin, last_index_end))
for begin, end in diff_merge:
if begin not in diff:
diff[begin] = set()
diff[begin].add(origin_text[begin : end + 1])
diff[begin].add(diff_text[begin : end + 1])
sha = "f0d1d40fdef63ea6a6dc97ba78a59512deb07ad9ecad1e3fd16c83151d51fe58"
getsha = lambda text: hashlib.sha256(text.encode()).hexdigest()
diff = [[key, list(value)] for key, value in diff.items()]
num_options = len(diff)
# 遍历diff表
def generate_and_compare(texts, diff, target_sha):
for i in range(2**num_options):
binary_repr = bin(i)[2:].zfill(num_options) # 生成二进制表示
# 创建一个复制的原始文本
modified_text = texts[0]
# 遍历所有diff项并根据binary_repr选择每个diff项的内容
for i, (index, diff_options) in enumerate(diff):
# binary_repr[index] 取值为 '0' 或 '1'
choice = int(binary_repr[i]) # '0' -> 0, '1' -> 1
option = diff_options[choice] # 根据选择选择diff的选项
# 用选项替换文本中的相应部分
modified_text = modified_text[:index] + option + modified_text[index + len(option) :]
# 计算修改后的文本的SHA-256哈希
current_sha = getsha(modified_text)
# 如果哈希值匹配目标哈希,则输出文本
if current_sha == target_sha:
print(f"Match found with binary {binary_repr}!")
print(modified_text)
return modified_text # 返回匹配的文本
print("No match found.")
return None # 没有找到匹配的文本
generate_and_compare(texts, diff, sha)
很快啊,马上来做了,我一直都是旅行照片的粉丝啊.jpg
百度地图搜了一下科里科气科创驿站,只有一个红点(右下角)离科大很近,即可秒了东校区西门
在leo酱的 动态 里找到了随机宅舞的预告,但是恰好有acg音乐会,那就得到了答案2024/05/19
好难一道题,谷歌图解不出来,但是谷歌图解告诉我们上面的路的中央线叫“彩虹划线”,配合右下角关键词“六安”找到一篇新(旧)闻,说是中央公园,在六安地图上搜到了中央森林公园,所以就是答案
下一个问题是谷歌搜图强势区,略过不表
同样谷歌搜图,把框限定在左下角的火车上,就可以确定车型(下图右上角wiki),还告诉我们是怀密线
本来以为这里是车站啥的,然后在全景图上找了半天车站也没找到相似的走向,最后在阿b里面怀密线相关的vlog中找到了相关信息,视频的4:23中找到了和照片两个建筑的布局相同的类似建筑,通过地图验证后可以认为是正确的,那么就可以找到最近的医院,照片应该也是在医院顶楼拍的
感觉今年的旅行照片多多少少有点散了,各部分没有很强的关联性,还是比较喜欢一次旅游能留下的那种比较成体系的照片(
明明看到一个简直是秒解的编码形式,但为什么传不上去,多洗爹
总之试了一下15min用模拟键盘输入的方法传base64文件传不了500k这样的数据,纯传需要45min左右
所以最后还是只传了一个45k的qrcode的python库,base64之后大小58k,大概四分钟能传上去,然后将文件转化三千五百左右的二维码录屏解码得到文件,正好快到十五分钟,真是极极又限限啊
# encoder
import base64
import qrcode
import time
def encode_file_to_terminal_qr(filepath, chunk_size=200):
# 读取二进制文件并进行Base64编码
with open(filepath, "rb") as f:
b64_data = base64.b64encode(f.read()).decode("utf-8")
# 将Base64字符串分块,并生成控制台QR码
for i in range(0, len(b64_data), chunk_size):
print(f"--- Frame {i // chunk_size} / {len(b64_data)//chunk_size +1} ---\n")
chunk = b64_data[i : i + chunk_size]
qr = qrcode.QRCode()
qr.add_data(chunk)
qr.print_ascii(invert=True) # 在控制台打印二维码
# 可以加入延时,确保录屏每帧都能记录到
time.sleep(0.1)
filepath = "/secret"
print("begin output for 3s after")
time.sleep(3)
encode_file_to_terminal_qr(filepath)
# decoder
import cv2
from pyzbar.pyzbar import decode
from PIL import Image
import base64
import numpy as np
def extract_qr_data_from_video(video_path, output_file):
# 初始化视频读取对象
cap = cv2.VideoCapture(video_path)
last_data = None # 保存上一个QR码内容
last_image = None
output_data = [] # 收集非重复的QR码内容
frame_count = 0
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
# 转换帧为灰度图像以便解析QR码
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 假设 gray 是当前帧,last_image 是上一帧
if last_image is not None:
diff = cv2.absdiff(gray, last_image) # 计算两帧的绝对差异
non_zero_diff = np.count_nonzero(diff) # 计算非零差异的像素数
ratio = non_zero_diff / gray.size
last_image = gray
if ratio < 0.1:
frame_count += 1
continue
else:
last_image = gray
pil_img = Image.fromarray(gray)
# 解析QR码
decoded_objects = decode(gray)
# 如果检测到QR码
if decoded_objects:
qr_data = decoded_objects[0].data.decode("utf-8")
# 若当前QR码内容与上次不同,添加到输出列表
if qr_data != last_data:
output_data.append(qr_data)
last_data = qr_data # 更新last_data
print(f"Frame {frame_count}: QR data added.")
else:
print(f"Frame {frame_count}: Duplicate QR data skipped.")
frame_count += 1
cap.release()
# 将收集的所有QR码数据连接成完整的Base64字符串
full_b64_data = ''.join(output_data)
# Base64解码并保存为原始文件
binary_data = base64.b64decode(full_b64_data)
with open(output_file, "wb") as f:
f.write(binary_data)
print(f"File has been reconstructed and saved as {output_file}")
video_path = "/hackergame/无法获得的秘密/QQ2024118-164211_1.mp4" # 没错是qq录屏
output_file = "/hackergame/无法获得的秘密/output.jpg"
extract_qr_data_from_video(video_path, output_file)
模拟键盘输入的方法还会时不时出现输入错位的情况,很申必,还得时不时sleep一下防止太快催人跑,做得很折磨
挺多题甚至都不是大模型辅助,都是直接通过逼问大模型问出来的,在这里稍微列个表(
- Node.js is Web Scale:来自【通义千问】“JavaScript对象的
__proto__
属性允许修改对象的原型链。攻击者可以利用这一点来修改cmds
对象的原型,从而影响所有对象的行为。” - 看不见的彼方:交换空间(flag1):来自【gpt-4o】,帮我直接把socket通信和相关的cpp代码做完了,但flag2还是需要自己手改一下,不能直接问出来
- 链上转账助手(flag1、2):没看懂题目,也没看懂解答,但【gpt-4o】也干了
- 不太分布式的软总线(flag1、2):本来就是一知半解,但【gpt-4o】也告诉了我两个解法,第三个在
get_flag3.cpp
上面改也是勉强出来了
好玩,爱玩,多玩。期待明年的hackergame喵