Shctf

shr1mp 发布于 11 天前 22 次阅读


签到

略,扫二维码关注公众号

不止二维码

拿到一张二维码,扫进去后没啥内容(但是通过pz发现是LSB隐写)

拿到stegsolve里,发现0通道的二维码有变化,故而单独将RGB0通道三个图片拿出来

调整不同通道得到不同的二维码,解出三个flag部分

FLAG_PART_1: SHCTF{55a23d24-
FLAG_PART_2: ABBB/AABBB/AAAAA/BBBBB/ABBBBA/BBBBA/B/AABBB/ABBB
FLAG_PART_3: MkZkbDg3ZlY3ZEQxalNGenQyZUFYT3E0NmRrTXFV

part2是莫斯密码

B705-4E7B

part3是Base系列

故而得到

SHCTF{55a23d24-B705-4E7B-942e-bdd}

Evan

给的图片后面有个zip文件。提取出来,伪加密。解开后就是flag

打开flag.txt得到:

SHCTF{Evan_1s_s0_h4nds0me!}

提问前请先搜索

很多人认为 CTF 只是关于“攻击”的艺术,但其实它更是关于“学习”的艺术

这里没有复杂的代码,也没有高深的算法,只有一篇你需要反复研读的文章

如果你无法通过搜索解决报错,你就无法通过提问获得帮助

请静下心来仔细阅读,你会有收获的

tips: 可能会分不清大写O和数字0

文章里面藏了flag,直接复制粘贴下来

Office

Office我只推荐MS,WPS是什么,我不知道

首先doc文件里面

IRy1m2qYkmewkTqDrneCoTCQoUiFqm7zqoeRoT7DqDCAqm7QsTqRuT3PqjWUt5e7

给了个doc文件,改后缀名位zip,找到里面有个异常文件

复制alphabet里面的内容

+/0-6a-zA-Z7-9=

这个是Base编码的码表,拿去cyberchef

得到flag

资源平权!

苕皮哥在网上下载资源时遇到了无良资源站,从某克网盘下了一个小时,结果发现压缩包的解压密码竟然还要支付 1 个比特币???

给了个zip,里面有exe文件,利用经典的exe进行明文爆破,

C:\Users\q1388\Desktop\工具\bkcrack-1.8.1-win32>bkcrack -C 2.zip -c flag.exe -p exe -o 64
bkcrack 1.8.1 - 2025-10-25
[17:53:01] Z reduction using 56 bytes of known plaintext
100.0 % (56 / 56)
[17:53:01] Attack on 140645 Z values at index 71
Keys: 60101051 4cba82cb 48eac20c
33.5 % (47060 / 140645)
Found a solution. Stopping.
You may resume the attack with the option: --continue-attack 47060
[17:53:17] Keys
60101051 4cba82cb 48eac20c

bkcrack -C 2.zip -c flag.exe -k 60101051 4cba82cb 48eac20c  -U 22.zip 123
bkcrack 1.8.1 - 2025-10-25
[17:56:59] Writing unlocked archive 22.zip with password "123"
100.0 % (1 / 1)
Wrote unlocked archive.

打开exe得到flag

薇薇安的美照

给了个jpg文件,末尾有flag字样

Base64解码后是

1_8_74_20_7_92_16_5_18_8_7

这里试了很多次,都不对,想到还有解密,最后才试到化学元素这上边,将数字转化为化学元素:

SHCTF{H_O_W_CA_N_U_S_B_AR_O_N}

滴答滴答

给了个Wav

先拿到一个flag,但是这个flag是错的

继续往下听,拿到flag

SHCTF{Radio_is_just_too_much_fun}

Open my puff

给了一个png和一个txt

txt文件里面内存有点大,但是能看见的就只有几个字符

keyA:12345678 keyB:qwertyui keyC:asdfghjk

看到另外一个png文件末尾结合题目名字,直到用openpuff

用openpuff找到隐写

解出来一个zip文件,里面有没有加密的flag.txt

niimmccw????zfip

还有一个加密的flag.zip,里面也有个flag.txt。这里8个连续字节+4刚好满足明文攻击条件

而且里面的zip加密算法和压缩方法符合条件

里面的flag.txt:

尝试用外层flag.txt的内容进行明文攻击

6e69696d6d636377(偏移0)<------------niimmccw
          {偏移12<--------------????}
7a666970<----------------------------zfip
bkcrack -C 1234.zip -c flag.txt  -x 0 6e69696d6d636377  -x 12 7a666970
bkcrack -C 1234.zip -c flag.txt -k 4543d810 f89b3d67 531a63b0 -U 111.zip 123

打开txt

niimmccwcnsizfip

All these eyes from the side under blue lights

Making you confused, I'm selfish, I know

That now's not the time, I'm sure they wouldn't mind

If you'd wanna leave and follow me home

this is flag   :   SHCTF{N3ur4l_Gl1tch_1n_Th3_5yst3m}            

得到flag

奇怪的数据

给了一个txt,里面都是(xx,xx,xx)类型,(0,0,0)和(255,255,255),感觉像是像素

from PIL import Image
import math

# ==== 1. 修改这里:输入文件名 ====
input_file = "flag.txt"  # 你的数据文件名
# =============================

# 读取数据
pixels = []
with open(input_file, "r", encoding="utf-8") as f:
    for line_num, line in enumerate(f, 1):
        if line.strip():
            # 移除末尾分号,分割像素
            parts = line.strip().rstrip(';').split(';')
            for part in parts:
                if part.startswith('('):
                    try:
                        r, g, b = map(int, part[1:-1].split(','))
                        pixels.append((r, g, b))
                    except:
                        pass

# 自动计算尺寸
total = len(pixels)
h = int(math.sqrt(total))
while total % h != 0 and h > 1:
    h -= 1
w = total // h

# 生成图片
img = Image.new("RGB", (w, h))
img.putdata(pixels[:w*h])
img.save("output.png")

print(f"完成")
print(f"输入文件: {input_file}")
print(f"像素数量: {total}")
print(f"图片尺寸: {w}×{h}")
print(f"输出文件: output.png")

得到了一个二维码

再经过Base64解出flag

Base64Encryption

附件解压后是这几样东西:

  • Readme.txt(明文)
  • Readme.txt.enc(加密后)
  • png.png.enc
  • flag.zip.enc

题目一句话:把 Base64 的字符表打乱了,别人解不开就算“加密”。

先确认:.enc 到底是什么

打开 Readme.txt,里面除了题面,还多了一行 64 字符的串(暂时不管它):

看我把Base64的字符表全都打乱了!只要别人解不开,那就是加密?
b4CYzZ3RWg7pBuTyVmGrxaHhjtQMUqEno5XJscD/1d892vO+Pfk6NewlFLSKiI0A

再看 Readme.txt.enc,明显是一串「看起来像 base64」的字符,而且长度刚好是 196,正好对应 147 字节明文的 base64 长度:

  • base64 输出长度 = 4 * ceil(n/3)
  • 147 字节 → 4 * 49 = 196

所以基本可以确定:.enc 就是 自定义字符表的 Base64

Readme.txtReadme.txt.enc 是一对现成的「明文 → 加密后」:

  1. 先用标准 base64 对 Readme.txt 编码,得到 std_b64
  2. 再把 Readme.txt.enc 当作 custom_b64
  3. 逐位对齐:std_b64[i] -> custom_b64[i],就能得到大部分字符映射
# quick_chinese.py
import sys
def quick_discover():
    """快速发现中文文件的Base64编码表"""
    
    with open('readme.txt', 'rb') as f1, open('readme.txt.enc', 'r', encoding='utf-8') as f2:
        plain = f1.read()  # 二进制读取
        encoded = f2.read().replace('\n', '').replace(' ', '').rstrip('=')
    
    print(f"明文: {len(plain)} 字节")
    print(f"编码: {len(encoded)} 字符")
    
    # 转换为二进制
    binary = ''.join(f'{b:08b}' for b in plain)
    
    # 初始化表
    table = ['?'] * 64
    
    # 快速处理
    for i, char in enumerate(encoded):
        if i * 6 >= len(binary):
            break
        start = i * 6
        chunk = binary[start:start+6].ljust(6, '0')
        idx = int(chunk, 2)
        if idx < 64:
            table[idx] = char
    result = ''.join(table)
    # 显示结果
    print(f"\n发现的表: {result}")
    print(f"发现: {64 - result.count('?')}/64")
    # 保存
    with open('table_result.txt', 'w', encoding='utf-8') as f:
        f.write(result)
    print("已保存到 table_result.txt")
    return result
if __name__ == "__main__":
    quick_discover()

这一轮能拿到 59 个字符的映射,还差 5 个没出现过

?yr8RIkJwCtaTNdBYifW7Zo6UbmqPG9?pSc/?0hxg52jALDl?enV1+EM4Hv?3Fsu

cyberchef里试出其中两位

k1="Kyr8RIkJwCtaTNdBYifW7Zo6UbmqPG9OpSc/X0hxg52jALDlQenV1+EM4Hvz3Fsu“
k2="Kyr8RIkJwCtaTNdBYifW7Zo6UbmqPG9OpSc/z0hxg52jALDlQenV1+EM4HvX3Fsu“
k3="Kyr8RIkJwCtaTNdBYifW7Zo6UbmqPG9XpSc/O0hxg52jALDlQenV1+EM4Hvz3Fsu“
k4="Kyr8RIkJwCtaTNdBYifW7Zo6UbmqPG9XpSc/z0hxg52jALDlQenV1+EM4HvO3Fsu“
k5="Kyr8RIkJwCtaTNdBYifW7Zo6UbmqPG9zpSc/X0hxg52jALDlQenV1+EM4HvO3Fsu“
k6="Kyr8RIkJwCtaTNdBYifW7Zo6UbmqPG9zpSc/O0hxg52jALDlQenV1+EM4HvX3Fsu“

最终试出来:

Kyr8RIkJwCtaTNdBYifW7Zo6UbmqPG9zpSc/O0hxg52jALDlQenV1+EM4HvX3Fsu

得到了一个

import numpy as np
import cv2
def reconstruct_qr_advanced(gray: np.ndarray, N: int, m: int, ox: int = 0, oy: int = 0, margin_ratio: float = 0.35):
    """
    重建干净的二维码 - 核心算法
    原理:采样每个模块时忽略边缘(斜线干扰区),只用中心区域投票
    
    Args:
        gray: 灰度图像
        N: QR码模块数(如版本10=57)
        m: 每个模块的像素大小
        ox, oy: 偏移量用于对齐网格
        margin_ratio: 边缘忽略比例(关键!0.35表示忽略35%边缘)
    """
    size = N * m
    sub = gray[oy:oy + size, ox:ox + size]
    
    margin = int(m * margin_ratio)
    mat = np.zeros((N, N), dtype=np.uint8)
    
    for r in range(N):
        for c in range(N):
            block = sub[r*m:(r+1)*m, c*m:(c+1)*m]
            # 关键:只取中心区域,忽略边缘的斜线干扰
            core = block[margin:-margin, margin:-margin] if margin > 0 else block
            if core.size == 0:
                core = block
            # 中值投票决定黑白
            mat[r, c] = 0 if np.median(core) < 128 else 255
    # 放大并添加静区便于扫描
    scale = 8
    img_bin = np.kron(mat, np.ones((scale, scale), dtype=np.uint8))
    q = 4 * scale
    out = 255 * np.ones((img_bin.shape[0] + 2*q, img_bin.shape[1] + 2*q), dtype=np.uint8)
    out[q:-q, q:-q] = img_bin
    return out
def auto_repair_qr(image_path: str):
    """自动搜索最佳参数修复二维码"""
    img = cv2.imread(image_path, cv2.IMREAD_COLOR)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    h, w = gray.shape
    
    # 裁剪到二维码区域
    det = cv2.QRCodeDetector()
    ok, pts = det.detect(img)
    if ok:
        pts = pts[0].astype(int)
        x0, y0 = pts[:,0].min()-10, pts[:,1].min()-10
        x1, y1 = pts[:,0].max()+10, pts[:,1].max()+10
        gray = gray[y0:y1, x0:x1]
        h, w = gray.shape
    
    # 暴力搜索版本、偏移、margin
    for version in range(1, 41):
        N = 21 + 4*(version-1)  # 模块数公式
        m = round(h / N)
        if not (4 <= m <= 60):
            continue
            
        size = N * m
        if size > h or size > w:
            continue
            
        for oy in range(min(5, m//3)):
            for ox in range(min(5, m//3)):
                for margin_ratio in [0.15, 0.25, 0.35]:
                    rebuilt = reconstruct_qr_advanced(gray, N, m, ox, oy, margin_ratio)
                    data, _, _ = cv2.QRCodeDetector().detectAndDecode(rebuilt)
                    if data:
                        return rebuilt, data
    return None, None
# 使用示例
result, content = auto_repair_qr("png.png")
cv2.imwrite("repaired.png", result)
print(f"修复成功: {content}")  # password: base64_15_n0t_3ncrypt10n

解出来:

  • 斜线干扰是抗锯齿产生的中间灰度像素,位于模块边缘
  • 解决方案:每个模块只采样中心65%区域(margin_ratio=0.35),完全避开边缘干扰
  • 成功参数:版本10(57×57模块),模块大小~20px,偏移(4,1),忽略35%边缘

通过二维码得到:

password: base64_15_n0t_3ncrypt10n

解压zip,里面的txt再次解密

SHCTF{fbf655a2-0661-4665-ac56-2331ca65e887} 

获取 SHSolver 之路

题面一句话提示“提升QQ等级”,而靶机返回的是一张非常高的 JPG。把图像缩小或旋转后能看到规律性的“QQ等级图标”阵列(皇冠/太阳/月亮/星星)。进一步观察发现:

  • 整张图是一个固定 9 列、933 行的网格。
  • 每一行的图标都靠左连续排列,右侧为空白,没有夹杂空格。
  • 每种图标在单行里最多出现 3 个,且“皇冠”最多 1 个。

这和 QQ 等级展示规则完全一致:

  • 1 皇冠 = 4 太阳 = 16 月亮 = 64 星星。
  • 由此想到了每一行可以看作是“以 4 进制计数”的 7-bit ASCII 值。

思路:

  1. 定位编码规则
  • 题面关键词“QQ等级”直接指向 QQ 等级图标系统。
  • 图标数量上限(3、3、3、1)与 QQ 等级的进位规则吻合。
  1. 图像切分
  • 二值化后按行扫描,找到所有“连续有像素”的行段,得到 933 个行块。
  • 按列扫描,得到 9 个列块。
  1. 图标分类
  • 对每个格子裁剪并缩放为 16x16 二值图。
  • 用汉明距离聚类,稳定得到 4 个类型(对应 4 种图标)。
  1. 确定图标顺序
  • 遍历 4 种图标的排列,找到能让所有行都保持“从高到低有序”的唯一顺序。
  • 最终顺序为:皇冠 -> 太阳 -> 月亮 -> 星星。
  1. 解码与还原
  • 每行计数:皇冠*64 + 太阳*16 + 月亮*4 + 星星*1 得到 ASCII。
  • 文本中有一行提示“remove all spaces”,下一行是带空格的 Base64。
  • 去空格后 Base64 解码得到反向字符串,再整体反转即可得到 flag。

代码核心逻辑:

1. 二值化图像 (阈值50)                 
     arr = (img > 50).astype(uint8)                     
                 │
                 ▼
2. 投影分割找网格                       
     行投影: arr.sum(axis=1) > 0        
     列投影: arr.sum(axis=0) > 0        
                                             → 得到 row_runs, col_runs          
                 │
                 ▼
3. 聚类识别4种符号                      
     每个单元格 → 缩放到16×16 → 展平256维  
     汉明距离<30归为同类                   
                                             → 得到4类: A,B,C,D                  
                 │
                 ▼
4. 拓扑排序确定权重顺序                  
     枚举4!种排列,验证每行符号顺序约束       
                                             → 确定 [?, ?, ?, ?] 对应 [64,16,4,1] 
                 │
                 ▼
5. 每行解码为1字节                      
     val = count(A)*64 + count(B)*16      
           + count(C)*4 + count(D)*1      
                                             → bytes(values) → 文本               
                 │
                 ▼
6. 提取Base64并反转                     
     找"gift"行的下一行 → 去空格 → b64decode 
                                              → 结果字符串反转 = flag              
import base64
import itertools
from pathlib import Path

import numpy as np
import requests
from PIL import Image

# Leave BASE_URL empty to avoid leaking target info.
# You can fill it manually (domain:port, no scheme) or place the image as work/challenge.jpg.
BASE_URL = ""

OUT_DIR = Path(__file__).resolve().parent
IMG_PATH = OUT_DIR / "challenge.jpg"
THRESH = 50
CLUSTER_DIST = 30
GRID_SIZE = (16, 16)


def _get_runs(mask):
    runs = []
    start = None
    for i, v in enumerate(mask):
        if v and start is None:
            start = i
        elif not v and start is not None:
            runs.append((start, i - 1))
            start = None
    if start is not None:
        runs.append((start, len(mask) - 1))
    return runs


def _load_image():
    if BASE_URL:
        url = BASE_URL.strip()
        if not url.startswith(("http://", "https://")):
            url = "http://" + url
        r = requests.get(url, timeout=20)
        r.raise_for_status()
        IMG_PATH.write_bytes(r.content)
    if not IMG_PATH.exists():
        raise SystemExit("No image found. Fill BASE_URL or put challenge.jpg in work/")
    return Image.open(IMG_PATH).convert("L")


def _cluster_cells(arr, row_runs, col_runs):
    reps = []
    items = []
    for li, (rs, re) in enumerate(row_runs):
        for ci, (cs, ce) in enumerate(col_runs):
            cell = arr[rs : re + 1, cs : ce + 1]
            if cell.sum() < 10:
                continue
            im = Image.fromarray((cell * 255).astype(np.uint8)).resize(GRID_SIZE, Image.NEAREST)
            data = (np.array(im) > 0).astype(np.uint8).flatten()
            if not reps:
                reps.append(data)
                items.append([(li, ci)])
                continue
            dists = [np.count_nonzero(data != r) for r in reps]
            m = min(dists)
            if m <= CLUSTER_DIST:
                idx = dists.index(m)
                items[idx].append((li, ci))
            else:
                reps.append(data)
                items.append([(li, ci)])
    return reps, items

def _infer_order(row_strings, labels):
    for perm in itertools.permutations(labels):
        rank = {c: i for i, c in enumerate(perm)}
        ok = True
        for s in row_strings:
            if not s:
                continue
            prev = -1
            for ch in s:
                r = rank[ch]
                if prev != -1 and r < prev:
                    ok = False
                    break
                prev = r
            if not ok:
                break
        if ok:
            return perm
    return None

def main():
    img = _load_image()
    arr = (np.array(img) > THRESH).astype(np.uint8)
    row_runs = _get_runs(arr.sum(axis=1) ; 0)
    col_runs = _get_runs(arr.sum(axis=0) ; 0)

    reps, items = _cluster_cells(arr, row_runs, col_runs)
    if len(reps) != 4:
        raise SystemExit(f"Expected 4 icon types, got {len(reps)}")
    order = sorted(range(len(reps)), key=lambda i: len(items[i]), reverse=True)
    labels = ["A", "B", "C", "D"]
    pos2type = {}
    for t, ri in enumerate(order):
        for pos in items[ri]:
            pos2type[pos] = labels[t]
    row_strings = [
        "".join(pos2type.get((li, ci), ".") for ci in range(len(col_runs))).rstrip(".")
        for li in range(len(row_runs))
    ]

    # Find the left-to-right order of symbols in each row.
    sym_order = _infer_order(row_strings, labels)
    if not sym_order:
        raise SystemExit("Failed to infer symbol order")

    weights = [64, 16, 4, 1]
    weight_map = {sym_order[i]: weights[i] for i in range(4)}

    values = []
    for s in row_strings:
        val = 0
        for sym in labels:
            val += s.count(sym) * weight_map[sym]
        values.append(val)
    text = bytes(values).decode("latin1")
    lines = text.splitlines()
    b64_line = None
    for i, line in enumerate(lines):
        if "gift" in line and i + 1 < len(lines):
            b64_line = lines[i + 1]
            break
    if not b64_line:
        raise SystemExit("Failed to locate base64 line")
    b64 = b64_line.replace(" ", "")
    raw = base64.b64decode(b64)
    flag = raw.decode()[::-1]
    print(flag)


if __name__ == "__main__":
    main()

[阶段3] 珍贵的Signature

给了个doc,打不开

后缀名改为zip,发现是伪加密,打开word/_rels/doc

Base64解密:得到一个bmp

发现有单图盲水印

U0hDVEZ7N2hhbmtfeTB1X2Ywcl9sMWsxbmdfTHNjY2N9

base64解码:

SHCTF{7hank_y0u_f0r_l1k1ng_Lsccc}
这个人很菜,但是在学
最后更新于 2026-02-10