PKWCTF新生MISC-“看看就好”"

admin 2026-10-16 08:17:25 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

PKWCTF新生MISC-“看看就好”

原创

玄网安全 opis
玄网安全 opis

玄网安全

2026年9月27日 18:02
浙江

在小说阅读器读本章

去阅读

在公众号小说中沉浸阅读

MISC1:你瞅啥

解题过程

附件结构:

  • browser_cache/cache.db:SQLite 缓存库
  • network/traffic.pcapng:本机回环 HTTP 流量
  • screenshots/:后台截图,其中 喵喵喵.png 为隐写载体

cache_meta 中有提示 same path, same time, different echo。request_log 记录了 /api/echo?id=101 到 id=160,response_hash 为 hidden_in_pcap。

pcap 中同一接口返回 JSON:{\"echo\":\"<base64>\",\"id\":\"...\",\"msg\":\"hello user\"}。解码后噪声为 noise:<id>,有效记录为 log_NN:<hex>。按序号拼出:

key=echo_key_2026;target=喵喵喵.png;method=red_lsb

对 喵喵喵.png 取红色通道 LSB(像素行优先、字节 MSB first),截到 cipher=...::END::,再用重复密钥 echo_key_2026 XOR 密文。

#!/usr/bin/env python3
from __future__ import annotations

import base64
import json
import re
from pathlib import Path

from PIL import Image

ROOT = Path(r\"E:\\opendata\\challenge\\浮生-challenge\")
PCAP = ROOT / \"network\" / \"traffic.pcapng\"
IMG_DIR = ROOT / \"screenshots\"

def bits_to_bytes(bits: list[int]) -> bytes:
    out = bytearray()
    for i in range(0, len(bits) - 7, 8):
        b = 0
        for j in range(8):
            b = (b << 1) | bits[i + j]
        out.append(b)
    return bytes(out)

def repeating_xor(data: bytes, key: bytes) -> bytes:
    return bytes(d ^ key[i % len(key)] for i, d in enumerate(data))

def main() -> None:
    raw = PCAP.read_bytes()
    bodies = re.findall(rb'\\{\"echo\":\"[^\"]+\",\"id\":\"[^\"]+\",\"msg\":\"[^\"]+\"\\}', raw)

    logs: dict[int, str] = {}
    for body in bodies:
        obj = json.loads(body)
        echo = base64.b64decode(obj[\"echo\"]).decode(\"utf-8\")
        m = re.fullmatch(r\"log_(\\d+):([0-9a-fA-F]{2})\", echo)
        if m:
            logs[int(m.group(1))] = m.group(2)

    instruction = bytes(int(logs[i], 16) for i in sorted(logs)).decode(\"utf-8\")
    fields = dict(part.split(\"=\", 1) for part in instruction.split(\";\"))
    key = fields[\"key\"].encode(\"utf-8\")
    target = IMG_DIR / fields[\"target\"]

    pixels = list(Image.open(target).convert(\"RGB\").getdata())
    payload = bits_to_bytes([p[0] & 1 for p in pixels])
    cipher_hex = re.search(rb\"cipher=([0-9a-fA-F]+)::END::\", payload).group(1)
    flag = repeating_xor(bytes.fromhex(cipher_hex.decode()), key).decode(\"utf-8\")
    print(flag)

if __name__ == \"__main__\":
    main()

输出:

PKWCTF{cache_never_lies_but_logs_do}

dashboard.png / error.png 以及噪声 echo 是干扰项,可忽略。

MISC5:奥马哈

摘要

动态 nc 服务连续给出 5 局 Pot-Limit Omaha 翻牌圈后局面,要求在 15 秒内计算 k1ne 在河牌上能严格击败三名对手的 outs 数量。核心是枚举剩余 32 张未知牌,并按“手牌恰好 2 张 + 公共牌恰好 3 张”评估成牌。

解题过程

服务地址:nc nc1.ctfplus.cn 15185。每局公开:

  • k1ne 的 4 张手牌
  • 3 名对手(cq / F1iAz / ddn)各 4 张手牌
  • 4 张公共牌(Turn)
  • 剩余未知牌:52 - 4 - 12 - 4 = 32

Outs 定义:河牌发出后,k1ne 的最终牌型必须严格大于所有对手;平局不计入。

第 1 步:实现奥马哈成牌比较

标准 5 张牌型从强到弱:同花顺 > 四条 > 葫芦 > 同花 > 顺子 > 三条 > 两对 > 一对 > 高牌。A 可作为 1 组成 A-2-3-4-5。

奥马哈强制组合:从 4 张手牌中选恰好 2 张,从 5 张公共牌中选恰好 3 张,取所有 C(4,2)*C(5,3)=60 种组合中的最大牌型。比较时用可排序元组(牌型等级 + 主牌 + kickers)。

第 2 步:枚举河牌计数 outs 并连打 5 轮

对每张剩余河牌,分别计算四人的最佳奥马哈牌型,统计 hero > max(villains) 的张数,在 15 秒超时前提交整数。五轮全部正确后给出 flag。

#!/usr/bin/env python3
import re
from itertools import combinations
from collections import Counter
from pwn import *

RANK_MAP = {c: i + 2 for i, c in enumerate(\"23456789TJQKA\")}
SUIT_MAP = {\"♠\": 0, \"♥\": 1, \"♣\": 2, \"♦\": 3}
ALL_CARDS = [(r, s) for r in range(2, 15) for s in range(4)]

def parse_card(tok):
    tok = tok.strip().strip(\"[],\")
    return (RANK_MAP[tok[0]], SUIT_MAP[tok[1]])

def parse_card_list(inner):
    return [parse_card(p) for p in inner.split(\",\") if p.strip()]

def eval_5(cards):
    ranks = sorted((c[0] for c in cards), reverse=True)
    suits = [c[1] for c in cards]
    is_flush = len(set(suits)) == 1
    cnt = Counter(ranks)
    counts = sorted(cnt.values(), reverse=True)
    by_freq = sorted(cnt.keys(), key=lambda r: (cnt[r], r), reverse=True)
    unique = sorted(set(ranks), reverse=True)
    is_straight, straight_high = False, 0
    if len(unique) == 5:
        if unique[0] - unique[4] == 4:
            is_straight, straight_high = True, unique[0]
        elif unique == [14, 5, 4, 3, 2]:
            is_straight, straight_high = True, 5
    if is_straight and is_flush:
        return (8, straight_high)
    if counts == [4, 1]:
        return (7, by_freq[0], by_freq[1])
    if counts == [3, 2]:
        return (6, by_freq[0], by_freq[1])
    if is_flush:
        return (5, *ranks)
    if is_straight:
        return (4, straight_high)
    if counts == [3, 1, 1]:
        kickers = sorted((r for r in ranks if r != by_freq[0]), reverse=True)
        return (3, by_freq[0], *kickers)
    if counts == [2, 2, 1]:
        pairs = sorted((r for r, c in cnt.items() if c == 2), reverse=True)
        kicker = next(r for r, c in cnt.items() if c == 1)
        return (2, pairs[0], pairs[1], kicker)
    if counts == [2, 1, 1, 1]:
        kickers = sorted((r for r in ranks if r != by_freq[0]), reverse=True)
        return (1, by_freq[0], *kickers)
    return (0, *ranks)

def omaha_best(hole, board):
    best = None
    for h in combinations(hole, 2):
        for b in combinations(board, 3):
            score = eval_5(h + b)
            if best is None or score > best:
                best = score
    return best

def count_outs(hero, villains, board):
    known = set(hero) | set(board)
    for v in villains:
        known.update(v)
    outs = 0
    for river in (c for c in ALL_CARDS if c not in known):
        full = board + [river]
        hs = omaha_best(hero, full)
        if all(hs > omaha_best(v, full) for v in villains):
            outs += 1
    return outs

def parse_round(text):
    def grab(pat):
        return parse_card_list(re.search(pat, text).group(1))
    return (
        grab(r\"k1ne[^\
]*\\[([^\\]]+)\\]\"),
        [
            grab(r\"cq[^\
]*\\[([^\\]]+)\\]\"),
            grab(r\"F1iAz[^\
]*\\[([^\\]]+)\\]\"),
            grab(r\"ddn[^\
]*\\[([^\\]]+)\\]\"),
        ],
        grab(r\"Board[^\
]*\\[([^\\]]+)\\]\"),
    )

def main():
    r = remote(\"nc1.ctfplus.cn\", 15185, timeout=20)
    for _ in range(5):
        data = r.recvuntil(b\"(15s) > \", timeout=18)
        hero, villains, board = parse_round(data.decode(\"utf-8\", errors=\"replace\"))
        r.sendline(str(count_outs(hero, villains, board)).encode())
    print(r.recvall(timeout=8).decode(\"utf-8\", errors=\"replace\"))
    r.close()

if __name__ == \"__main__\":
    main()

实战五轮答案分别为 32 / 3 / 8 / 0 / 4,服务返回:

[+] Incredible! You stacked them all!
[+] Here is your flag: PKWCTF{2dea34f3-8ecc-46e5-8067-992ae3505a7a}

注意:recvuntil(b\"> \") 会在规则文本 Full House > Flush 处提前截断,必须匹配完整提示 (15s) >。

MISC6:黑白之间

附件 1.png 是一张 19×19 围棋棋盘。空交叉点是分隔符,黑子和白子按行读成二进制,黑为 0、白为 1。

解题过程

第 1 步:定位棋子

图片只有少量纯色。黑子中心是 (16, 16, 16),白子中心是接近纯白的像素。对这两类连通块取质心,再把横坐标、纵坐标各自聚类,得到 19×19 的交叉点。结果是 110 颗黑子、122 颗白子,其余交叉点为空。

第 2 步:按行解码

空点不编码。从左到右、从上到下只读取有棋子的点:黑子记为 0,白子记为 1。232 bit 正好是 29 字节,按 8 bit 一组转成 ASCII。

import numpy as np
from PIL import Image
from scipy import ndimage

PNG_PATH = r\"1.png\"

def stone_centers(mask):
    labels, count = ndimage.label(mask)
    centers = []
    for i in range(1, count + 1):
        ys, xs = np.where(labels == i)
        centers.append((float(xs.mean()), float(ys.mean())))
    return centers

def cluster(values, tolerance=8):
    values = sorted(values)
    groups = [[values[0]]]
    for value in values[1:]:
        if value - np.mean(groups[-1]) <= tolerance:
            groups[-1].append(value)
        else:
            groups.append([value])
    return [float(np.mean(group)) for group in groups]

def nearest(value, lines):
    return int(np.argmin(np.abs(np.array(lines) - value)))

def main():
    arr = np.array(Image.open(PNG_PATH))
    black = stone_centers(np.all(arr < 40, axis=2))
    white = stone_centers(np.all(arr > 230, axis=2))
    xs = cluster([x for x, _ in black + white])
    ys = cluster([y for _, y in black + white])
    grid = np.full((len(ys), len(xs)), -1, np.int8)
    for x, y in black:
        grid[nearest(y, ys), nearest(x, xs)] = 0
    for x, y in white:
        grid[nearest(y, ys), nearest(x, xs)] = 1

    bits = [int(v) for row in grid for v in row if v != -1]
    out = bytearray()
    for i in range(0, len(bits) - 7, 8):
        byte = 0
        for bit in bits[i : i + 8]:
            byte = (byte << 1) | bit
        out.append(byte)
    print(out.decode(\"ascii\"))

if __name__ == \"__main__\":
    main()

依赖:numpy、Pillow、scipy。输出:

PKWCTF{b1ack_is_0_whit3_1S_1}

MISC7:正在发动鬼脑

附件 电脑也是脑.zip 的注释是密钥,data.txt 用四种零宽字符携带一份 zlib 压缩的像素编码器说明。按说明把可见字符还原成黑白图,图中的文字就是 flag。

解题过程

第 1 步:取出密钥和编码器说明

ZIP 全局注释是 53d26425,即 4 字节密钥。data.txt 中夹有 U+200B、U+200C、U+200D、U+2060。按这个顺序各对应 00、01、10、11,高位在前拼成字节。结果以 ZW1 开头,偏移 7 处是 zlib,解压后得到编码器规则。

规则要点:

  • 忽略换行和这四种零宽字符,可见字符每 9 个一组。
  • 每组用 SHA256(K || uint32_be(组号) || 字符) 对 62 个字母数字排序。前 31 个表示黑(1),其余表示白(0)。
  • 校验位在 s % 9,初始 s = 93。去掉校验位后得到 8 个数据位 v,再执行 s = (33*s + v + 组号) % 256。
  • 数据位按 8×8 小块存放。块来源下标 t = (5*k + 173) % 块数,奇数块的对角扫描倒序,旋转为 r = (7*t + 3) % 4。
  • 还原后的像素必须匹配规则中的 SHA256。

第 2 步:逆变换并读图

216576 个数据位等于 3384 个小块,排成 94×36。按上面的规则逆置换、逆扫描、逆旋转后,白底黑字图片的 SHA256 为 de46c7564c271c524cf4ff6df51309e49711b23fbb78cde7a11f3e1a879940e9,与规则一致。图片内容是:

PKWCTF{F4q_m3rm3r_z3n_m
3_zh3_m3_hu41}
import hashlib
import struct
import zipfile
import zlib

import numpy as np
from PIL import Image

ZIP_PATH = r\"电脑也是脑.zip\"
OUT_PATH = r\"brain_flag.png\"

ZW = {\"\\u200b\": 0, \"\\u200c\": 1, \"\\u200d\": 2, \"\\u2060\": 3}
ALPHABET = \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\"
IGNORE = set(\"\\r\
\\u200b\\u200c\\u200d\\u2060\")

def zw_to_zlib(text):
    bits = []
    for ch in text:
        if ch not in ZW:
            continue
        value = ZW[ch]
        bits.append((value >> 1) & 1)
        bits.append(value & 1)
    raw = bytearray()
    for i in range(0, len(bits) - 7, 8):
        byte = 0
        for bit in bits[i : i + 8]:
            byte = (byte << 1) | bit
        raw.append(byte)
    start = raw.find(b\"\\x78\\xda\")
    return zlib.decompress(bytes(raw[start:]))

def char_bit(key, group, ch):
    scores = []
    prefix = key + struct.pack(\">I\", group)
    for item in ALPHABET:
        digest = hashlib.sha256(prefix + item.encode(\"ascii\")).digest()
        scores.append((digest, item))
    scores.sort()
    order = [item for _, item in scores]
    return 1 if ch in order[:31] else 0

def recover_bits(text, key):
    visible = [ch for ch in text if ch not in IGNORE]
    state = 93
    bits = []
    for group, start in enumerate(range(0, len(visible), 9)):
        chunk = visible[start : start + 9]
        nine = [char_bit(key, group, ch) for ch in chunk]
        check_at = state % 9
        eight = nine[:check_at] + nine[check_at + 1 :]
        value = 0
        for bit in eight:
            value = (value << 1) | bit
        bits.extend(eight)
        state = (33 * state + value + group) % 256
    return bits

def diagonal_scan(size=8):
    order = []
    for s in range(2 * size - 1):
        coords = [(x, s - x) for x in range(size) if 0 <= s - x < size]
        if s % 2:
            coords.reverse()
        order.extend(coords)
    return order

def rotate(rotation, x, y, size=8):
    if rotation == 0:
        return x, y
    if rotation == 1:
        return y, size - 1 - x
    if rotation == 2:
        return size - 1 - x, size - 1 - y
    return size - 1 - y, x

def pixels(bits, tiles_x=94, tiles_y=36):
    tile_count = tiles_x * tiles_y
    tiles = [bits[i * 64 : (i + 1) * 64] for i in range(tile_count)]
    source = [None] * tile_count
    for k, tile in enumerate(tiles):
        t = (5 * k + 173) % tile_count
        source[t] = tile
    scan = diagonal_scan()
    image = np.zeros((tiles_y * 8, tiles_x * 8), np.uint8)
    for t, tile in enumerate(source):
        seq = scan[::-1] if t % 2 else scan
        rotation = (7 * t + 3) % 4
        canvas = np.zeros((8, 8), np.uint8)
        for bit, (x, y) in zip(tile, seq):
            sx, sy = rotate(rotation, x, y)
            canvas[sy, sx] = bit
        ty, tx = divmod(t, tiles_x)
        image[ty * 8 : (ty + 1) * 8, tx * 8 : (tx + 1) * 8] = canvas
    return image

def main():
    with zipfile.ZipFile(ZIP_PATH) as zf:
        text = zf.read(\"data.txt\").decode(\"utf-8\")
        key = bytes.fromhex(zf.comment.decode(\"ascii\"))
    image = pixels(recover_bits(text, key))
    digest = hashlib.sha256(image.reshape(-1).astype(np.uint8).tobytes()).hexdigest()
    assert digest == \"de46c7564c271c524cf4ff6df51309e49711b23fbb78cde7a11f3e1a879940e9\"
    Image.fromarray((1 - image) * 255).save(OUT_PATH)
    print(\"PKWCTF{F4q_m3rm3r_z3n_m3_zh3_m3_hu41}\")

if __name__ == \"__main__\":
    main()

依赖:numpy、Pillow。把 ZIP_PATH 改成附件解压后的 电脑也是脑.zip。脚本校验像素哈希并保存 brain_flag.png,图中文字与打印的 flag 相同。

PKWCTF{F4q_m3rm3r_z3n_m3_zh3_m3_hu41}


PKWCTF新生MISC-“看看就好”" 网络安全文章

PKWCTF新生MISC-“看看就好”"

本文为PKWCTF新生赛MISC方向题解,涵盖图片隐写、文件分离、pyjail沙箱逃逸、流量分析与LSB隐写等题型。通过foremost分离、Python沙箱绕过、SQLite缓存与pcap分析、奥马哈扑克outs枚举等方法获取flag,展示了CTF杂项常见解题思路与工具链。
2026腾讯游戏安全初赛Android题解" 网络安全文章

2026腾讯游戏安全初赛Android题解"

本文是2026腾讯游戏安全初赛安卓逆向题解,涉及Godot引擎游戏逆向。通过Frida脚本dump加壳so文件,定位并提取AES密钥,解密.gdc脚本,修改逻辑后重打包APK。最终通过hook函数调用链定位加密函数地址,展示了完整的逆向分析流程。
为可信智能时代筑基:2026CSA大中华区大会暨AI+安全大会聚焦基础设施安全与产业实践" 网络安全文章

为可信智能时代筑基:2026CSA大中华区大会暨AI+安全大会聚焦基础设施安全与产业实践"

2026CSA大中华区大会暨AI+安全大会聚焦可信智能时代筑基,探讨AI基础设施安全、智能体风险及安全治理。大会发布AI安全评估体系(AISM与STARforAI)、CAINIP认证课程及创新实践案例集,强调从技术、标准、人才到产业实践推进AI安全能力建设,推动安全从事后响应走向事前预判。
评论:0   参与:  0