楚慧杯Misc全解

楚慧杯Misc全解

gamego

先双击安装游戏

查看data

发现是RPGmaker

.rvdata2 文件是 RPG Maker VX Ace 游戏的数据文件,需要使用 RPG Maker VX Ace Ruby 脚本

直接dump出所有文本

require 'zlib'

# --- Stub Classes for RGSS ---
class Table
  def self._load(s); new; end
end

class Color
  def self._load(s); new; end
end

class Tone
  def self._load(s); new; end
end

module RPG
  class Map
    attr_accessor :display_name, :events, :width, :height, :data, :tileset_id, :bgm, :bgs, :autoplay_bgm, :autoplay_bgs, :encounter_list, :encounter_step, :parallax_name, :parallax_loop_x, :parallax_loop_y, :parallax_sx, :parallax_sy, :note
  end
  class Event
    attr_accessor :id, :name, :x, :y, :pages
  end
  class Event::Page
    attr_accessor :condition, :graphic, :move_type, :move_speed, :move_frequency, :move_route, :walk_anime, :step_anime, :direction_fix, :through, :priority_type, :trigger, :list
  end
  class EventCommand
    attr_accessor :code, :indent, :parameters
  end
  class MoveRoute; attr_accessor :repeat, :skippable, :wait, :list; end
  class MoveCommand; attr_accessor :code, :parameters; end
  class BGM; attr_accessor :name, :volume, :pitch; end
  class BGS; attr_accessor :name, :volume, :pitch; end
  class SE; attr_accessor :name, :volume, :pitch; end
  class ME; attr_accessor :name, :volume, :pitch; end
  class System; attr_accessor :game_title, :version_id, :japanese, :party_members, :currency_unit, :elements, :skill_types, :weapon_types, :armor_types, :switches, :variables, :boat, :ship, :airship, :title_bgm, :battle_bgm, :battle_end_me, :gameover_me, :sounds, :test_battlers, :test_troop_id, :start_map_id, :start_x, :start_y, :terms, :opt_draw_title, :opt_use_midi, :opt_transparent, :opt_followers, :opt_slip_death, :opt_floor_death, :opt_display_tp, :magic_number, :window_tone; end
  class System::Terms; attr_accessor :basic, :params, :etypes, :commands; end
  class System::TestBattler; attr_accessor :actor_id, :level, :equips; end
  class System::Vehicle; attr_accessor :character_name, :character_index, :bgm, :start_map_id, :start_x, :start_y; end
  class BaseItem; attr_accessor :id, :name, :icon_index, :description, :features, :note; end
  class UsableItem < BaseItem; attr_accessor :scope, :occasion, :speed, :success_rate, :repeats, :tp_gain, :hit_type, :animation_id, :damage, :effects; end
  class Skill < UsableItem; attr_accessor :mp_cost, :tp_cost, :message1, :message2, :required_wtype_id1, :required_wtype_id2; end
  class Item < UsableItem; attr_accessor :itype_id, :price, :consumable; end
  class EquipItem < BaseItem; attr_accessor :price, :etype_id, :params; end
  class Weapon < EquipItem; attr_accessor :wtype_id, :animation_id; end
  class Armor < EquipItem; attr_accessor :atype_id; end
  class Enemy < BaseItem; attr_accessor :battler_name, :battler_hue, :params, :exp, :gold, :drop_items, :actions; end
  class Enemy::DropItem; attr_accessor :kind, :data_id, :denominator; end
  class Enemy::Action; attr_accessor :skill_id, :condition_type, :condition_param1, :condition_param2, :rating; end
  class State < BaseItem; attr_accessor :restriction, :priority, :remove_at_battle_end, :remove_by_restriction, :auto_removal_timing, :min_turns, :max_turns, :remove_by_damage, :chance_by_damage, :remove_by_walking, :steps_to_remove, :message1, :message2, :message3, :message4; end
  class Troop; attr_accessor :id, :name, :members, :pages; end
  class Troop::Member; attr_accessor :enemy_id, :x, :y, :hidden; end
  class Troop::Page; attr_accessor :condition, :span, :list; end
  class Troop::Page::Condition; attr_accessor :turn_ending, :turn_valid, :turn_a, :turn_b, :enemy_valid, :enemy_index, :enemy_hp, :actor_valid, :actor_id, :actor_hp, :switch_valid, :switch_id; end
  class Animation; attr_accessor :id, :name, :animation1_name, :animation1_hue, :animation2_name, :animation2_hue, :position, :frame_max, :frames, :timings; end
  class Animation::Frame; attr_accessor :cell_max, :cell_data; end
  class Animation::Timing; attr_accessor :frame, :se, :flash_scope, :flash_color, :flash_duration; end
  class CommonEvent; attr_accessor :id, :name, :trigger, :switch_id, :list; end
  class Tileset; attr_accessor :id, :name, :mode, :tileset_names, :flags, :note; end
  class AudioFile; attr_accessor :name, :volume, :pitch; end
  class Class; attr_accessor :id, :name, :exp_params, :params, :learnings, :features, :note; end
  class Class::Learning; attr_accessor :level, :skill_id, :note; end
  class Actor; attr_accessor :id, :name, :nickname, :class_id, :initial_level, :max_level, :character_name, :character_index, :face_name, :face_index, :equips, :description, :features, :note; end
end

# --- Analysis Logic ---

def search_object(obj, path, visited = {})
  # Avoid infinite recursion
  return if visited[obj.object_id]
  visited[obj.object_id] = true

  case obj
  when String
    # Handle encoding
    str = obj.dup
    str.force_encoding('UTF-8') rescue str.force_encoding('ASCII-8BIT')
    
    if str.match(/DASCTF\{.*?\}/i)
       puts "\n\n!!!!! FOUND FLAG in #{path}: #{str} !!!!!\n\n"
    elsif str.downcase.include?("dasctf")
       puts "Found 'dasctf' in #{path}: #{str.inspect}"
    elsif str.downcase.include?("getflag")
       puts "Found 'getflag' in #{path}: #{str.inspect}"
    end
    
  when Array
    obj.each_with_index do |item, index|
      search_object(item, "#{path}[#{index}]", visited)
    end
  when Hash
    obj.each do |key, value|
      search_object(key, "#{path}[key:#{key}]", visited)
      search_object(value, "#{path}[#{key}]", visited)
    end
  when RPG::EventCommand
    # Special handling for Event Commands to show context
    obj.parameters.each_with_index do |param, i|
      search_object(param, "#{path}.parameters[#{i}]", visited)
    end
  when Object
    # For generic objects, search instance variables
    obj.instance_variables.each do |var|
      value = obj.instance_variable_get(var)
      search_object(value, "#{path}.#{var}", visited)
    end
  end
end

def decompress_scripts(scripts)
  decompressed_scripts = []
  scripts.each do |script|
    id, name, code = script
    begin
      decompressed_code = Zlib::Inflate.inflate(code)
      decompressed_scripts << [id, name, decompressed_code]
    rescue Zlib::Error
      # Maybe not compressed or other error
      decompressed_scripts << [id, name, code]
    end
  end
  decompressed_scripts
end

def dump_strings(obj, path, visited = {}, file = nil)
  return if visited[obj.object_id]
  visited[obj.object_id] = true

  case obj
  when String
    str = obj.dup
    str.force_encoding('UTF-8') rescue str.force_encoding('ASCII-8BIT')
    msg = "  String at #{path}: #{str.inspect}"
    if file
      file.puts msg
    else
      puts msg
    end
  when Array
    obj.each_with_index { |item, i| dump_strings(item, "#{path}[#{i}]", visited, file) }
  when Hash
    obj.each { |k, v| dump_strings(k, "#{path}[key]", visited, file); dump_strings(v, "#{path}[#{k}]", visited, file) }
  when Object
    obj.instance_variables.each do |var|
      dump_strings(obj.instance_variable_get(var), "#{path}.#{var}", visited, file)
    end
  end
end

# Dump all strings to a file
File.open("rvdata2_dump.txt", "w") do |file|
  puts "Starting analysis... Outputting to rvdata2_dump.txt"
  
  Dir.glob("*.rvdata2").each do |filename|
    puts "Analyzing #{filename}..."
    file.puts "\n\n=== FILE: #{filename} ===\n"

    # 1. Raw binary search (Fallback method)
    begin
      raw_content = File.binread(filename)
      # Search for all occurrences of DASCTF (case insensitive)
      matches = raw_content.scan(/DASCTF.*?/i)
      if !matches.empty?
        file.puts "\n!!!!! FOUND POTENTIAL FLAG in #{filename} (Raw Search) !!!!!"
        puts "\n!!!!! FOUND POTENTIAL FLAG in #{filename} (Raw Search) !!!!!"
        
        # Print MORE context for each match
        raw_content.scan(/.{0,100}DASCTF.{0,200}/im).each do |context|
             file.puts "Context:\n#{context.inspect}\n"
             puts "Context:\n#{context.inspect}\n"
        end
        file.puts "\n"
      end
    rescue => e
      file.puts "  Error reading file #{filename}: #{e}"
    end

    # 2. Object traversal search
    begin
      data = File.open(filename, "rb") { |f| Marshal.load(f) }

      # Special handling for Scripts.rvdata2
      if filename == "Scripts.rvdata2" && data.is_a?(Array)
        file.puts "  Decompressing scripts in #{filename}..."
        data = decompress_scripts(data)
      end
      
      # Dump ALL strings for ALL files
      dump_strings(data, filename, {}, file)
      
    rescue StandardError => e
      file.puts "  Error loading #{filename}: #{e.message}"
    end
  end
end

puts "Analysis complete. Check rvdata2_dump.txt for full data."

image

image

得到

DASCTF{1168cb17-31ff-43b7--b586-8414d383afce}

Time_and_chaos_1

首先观察这些图片,肉眼看全是噪声,没什么规律。但是查看文件属性时发现,它们的修改时间 是不同的,而且不是顺序的。这很可能是一个排序的依据。

第一部分:Time Accumulation
这部分逻辑是把图片按照 st_mtime(修改时间)排序,然后提取每张图蓝色通道 (Blue Channel) 的第 6 位 (bit index 5),按照一定的衰减系数叠加。

第二部分:Bit Plane Reconstruction
这部分是把 8 张图按文件名顺序(1-8)排列,分别提取红通道 (Red Channel) 的第 6 位,然后把这 8 个 bit 拼成一个字节 (Byte)。。

import argparse
from pathlib import Path
import numpy as np
from PIL import Image

def solve_images():
    # 设定输入目录
    input_dir = Path(".")
    files = [input_dir / f"{i}.png" for i in range(1, 9)]
    
    # --- 任务 1: 生成 Accumulation 图 ---
    # 关键点:按修改时间排序
    ordered_by_time = sorted(files, key=lambda p: p.stat().st_mtime)
    
    # 初始化累加器
    first_img = np.array(Image.open(ordered_by_time[0]).convert("RGB"))
    acc = np.zeros(first_img.shape[:2], dtype=np.float32)
    
    decay = 0.85
    inject = 0.15
    
    for p in ordered_by_time:
        img = np.array(Image.open(p).convert("RGB"))
        # 提取 Blue 通道的第 6 bit (0x20)
        bit = ((img[:,:,2] >> 5) & 1).astype(np.float32)
        # 模拟时间衰减叠加
        acc = acc * decay + bit * 255.0 * inject
        
    # 保存结果
    res_acc = np.clip(acc, 0, 255).astype(np.uint8)
    Image.fromarray(res_acc, mode="L").save("solved_chaos.png")
    print("[+] Generated solved_chaos.png")

    # --- 任务 2: 生成 Rebuild 图 ---
    # 关键点:按文件名顺序 1-8
    imgs = [np.array(Image.open(p).convert("RGB")) for p in files]
    
    # 提取 Red 通道的第 6 bit
    bits = [((im[:,:,0] >> 5) & 1).astype(np.uint8) for im in imgs]
    
    # 组合 bits: 1.png -> MSB, 8.png -> LSB
    out = np.zeros(bits[0].shape, dtype=np.uint8)
    for i, b in enumerate(bits):
        out |= b << (7 - i)
        
    # 归一化以便查看
    out_f = out.astype(np.float32)
    norm = ((out_f - out_f.min()) / (out_f.max() - out_f.min()) * 255.0).astype(np.uint8)
    
    Image.fromarray(norm, mode="L").save("solved_rebuild.png")
    print("[+] Generated solved_rebuild.png")

if __name__ == '__main__':
    solve_images()

image

DASCTF{Logistic_and

接下来处理 flag.txt。打开文件看似只有一些毫无逻辑的文字(“也许是有用的东西”),中间夹杂着 "666"。

但是光标移动的时候感觉不对劲,有明显的卡顿,且文件大小比显示的字符要大。这显然是零宽字符隐写 (Zero-Width Steganography)

with open("flag.txt", "r", encoding="utf-8") as f:
    content = f.read()
    # 打印非打印字符的 Unicode 编码
    print([hex(ord(c)) for c in content if ord(c) > 0x2000])

零宽字符解密得到
image

_time_fly}
DASCTF{Logistic_and_time_fly}

老妈的故事书

解压拿到两个文件一个word一个pdf,在pdf尾巴发现提示

There are two positons that require 4,one is to crack is love,the other???

word密码:love
image

根据pdf里面的论文隐写方法可在简体/繁体之间切换的汉字
image

使用zhconv提取

import re
import zhconv

with open("document.xml", encoding="utf-8") as fp:
    xml_raw = fp.read()

corpus = "".join(re.findall(r"<w:t[^>]*>([^<]*)</w:t>", xml_raw))
print(f"Total chars: {len(corpus)}")

markers = []
vi = 0
for c in corpus:
    s_form = zhconv.convert(c, "zh-hans")
    t_form = zhconv.convert(c, "zh-hant")
    if s_form == c and t_form == c:
        continue
    if s_form != c:
        markers.append(vi)
    vi += 1

dists = [markers[0]]
for i in range(1, len(markers)):
    dists.append(markers[i] - markers[i - 1] - 1)

print(f"Segments ({len(dists)}): {dists}")

nibbles = "".join(hex(n)[2:] for n in dists)
payload = bytes.fromhex(nibbles if len(nibbles) % 2 == 0 else nibbles[:-1])
print(f"Flag: {payload.decode('ascii', errors='replace')}")#!/usr/bin/env python3
"""
Chinese Text Steganography - HESM Extraction
pip install zhconv
"""
import os
from xml.etree import ElementTree as ET
import zhconv

DIR = os.path.dirname(os.path.abspath(__file__))
XML_PATH = os.path.join(DIR, "document.xml")
WML = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"


def read_xml_text(path):
    tree = ET.parse(path)
    return "".join(node.text or "" for node in tree.iter(f"{WML}t"))


def extract_hesm(text):
    segments = []
    gap = -1
    for ch in text:
        to_s = zhconv.convert(ch, "zh-hans")
        to_t = zhconv.convert(ch, "zh-hant")
        if to_s != ch or to_t != ch:
            gap += 1
            if to_s != ch:
                segments.append(gap)
                gap = -1
    return segments


def decode_segments(segs):
    hex_str = "".join(format(v, "x") for v in segs)
    raw = bytes(int(hex_str[i:i+2], 16) for i in range(0, len(hex_str) - len(hex_str) % 2, 2))
    return hex_str, raw


def main():
    text = read_xml_text(XML_PATH)
    print(f"[*] Text length: {len(text)}")

    segs = extract_hesm(text)
    print(f"[*] {len(segs)} segments: {segs}")

    hex_str, raw = decode_segments(segs)
    print(f"[*] Hex: {hex_str}")
    print(f"\n[+] Flag: {raw.decode('ascii', errors='replace')}")


if __name__ == "__main__":
    main()
DASCTF{024bb015-5578-4181-9d28-e2f7d10bac4a}

generate_key

加载镜像

直接lovelymemLuxe的ai分析一波

image

找到三个有用的线索

generate_key 文件被IE浏览器打开
key.zip 文件被WinRAR打开
字符串:ASHFK4567315

分别用vol3导出这两个文件

两个都是加密zip

上面得到的ASHFK4567315可以作为密码解压generate_key,得到一个二进制文件

key.zip里有很多1kb的文件,可以猜到是crc爆破

4字节crc爆破

import zipfile
import zlib
import itertools
import string
import io

def crack_crc32(target_crc, length):
    charset = string.ascii_letters + string.digits + string.punctuation + " "
    for candidate in itertools.product(charset.encode(), repeat=length):
        data = bytes(candidate)
        if zlib.crc32(data) == target_crc:
            return data.decode('utf-8', errors='ignore')
    return None

def main():
    zip_path = "key.zip"
    results = {}
    
    print("开始 CRC 爆破...")
    
    with zipfile.ZipFile(zip_path, 'r') as z:
        for info in z.infolist():
            if info.filename.endswith('.zip'):
                # 读取子 zip 文件的数据
                sub_zip_data = z.read(info.filename)
                with zipfile.ZipFile(io.BytesIO(sub_zip_data), 'r') as sub_z:
                    for sub_info in sub_z.infolist():
                        if sub_info.filename.endswith('.txt'):
                            target_crc = sub_info.CRC
                            size = sub_info.file_size
                            print(f"正在破解 {sub_info.filename} (CRC32: {hex(target_crc)}, Size: {size})...")
                            
                            cracked_content = crack_crc32(target_crc, size)
                            
                            if cracked_content:
                                print(f"  -> 成功: {cracked_content}")
                                # 提取数字索引用于排序,例如 "10.txt" -> 10
                                index = int(sub_info.filename.split('.')[0])
                                results[index] = cracked_content
                            else:
                                print(f"  -> 失败: 未找到匹配内容")

    print("\n--- 最终结果 ---")
    final_string = ""
    # 按索引顺序拼接
    for i in sorted(results.keys()):
        print(f"{i}.txt: {results[i]}")
        final_string += results[i]
        
    print(f"\n拼接后的字符串: {final_string}")
    
    with open("result.txt", "w") as f:
        f.write(final_string)
    print("结果已保存到 result.txt")

if __name__ == "__main__":
    main()

U2FsdGVkX19N/id+O8PA1l9SuuQ4JiS7edG9Og8TaTXUIakkm1gHQ/X77iR4IpKy

得到一串PBE的密文

接着分析二进制文件

bash -lc readelf -x .rodata /mnt/data/generate_key && echo '---DATA---' && readelf -x .data /mnt/data/generate_key

程序在 .data 段里藏了一段初始字符串

ZIKT001NIKH7WZYGQWZZH

进制里有 3 个连续的变换函数,按这个顺序套上去:

bac,b5c,b1c

这三个函数的核心逻辑分别是:

image

image

image

总结来说就是

// bac
x = ((c ^ 0x5A) + 0x33 - (c & 0x0F)) & 0xFF

// b5c
x = ((c + 7) ^ (i % 2 ? 0x11 : 0x22)) & 0xFF

// b1c
x = c ^ 0x33

ZIKT001NIKH7WZYGQWZZH 依次做这三步后,得到的字节序列十六进制正好是

21 66 51 66 B5 86 B5 62 55 62 55 82 51 12 25 72 55 62 21 12 55

exp:

def bac(data: bytes) -> bytes:
    out = bytearray()
    for c in data:
        x = ((c ^ 0x5A) + 0x33 - (c & 0x0F)) & 0xFF
        out.append(x)
    return bytes(out)

def b5c(data: bytes) -> bytes:
    out = bytearray()
    for i, c in enumerate(data):
        x = ((c + 7) ^ (0x11 if (i % 2) else 0x22)) & 0xFF
        out.append(x)
    return bytes(out)

def b1c(data: bytes) -> bytes:
    out = bytearray()
    for c in data:
        x = c ^ 0x33
        out.append(x)
    return bytes(out)

def main():
    s = b"ZIKT001NIKH7WZYGQWZZH"

    stage1 = bac(s)
    stage2 = b5c(stage1)
    stage3 = b1c(stage2)

    print("origin :", s.decode())
    print("stage1 :", stage1.hex().upper())
    print("stage2 :", stage2.hex().upper())
    print("key    :", stage3.hex().upper())

if __name__ == "__main__":
    main()
21665166B586B56255625582511225725562211255

得到

key为0x21665166b586b562556255825112257255622112550a

我们可以

openssl enc -d -aes-256-cbc -a -md md5 -p

或者直接

image

得到

DASCTF{HEEEKAHSZKH26}

SAM_and_Steg

从题目名 SAM_and_Steg 就能先想到两条线:

  1. SAM + SYSTEM 可以用来做 Windows 本地账户信息提取
  2. Steg 提示文件里可能还藏了隐写或额外数据
hexdump -C system | tail

这时候会发现文件尾部不是很像正常的 hive 结束方式,说明后面可能拼接了别的东西

grep/xxd 找偏移
xxd -p system | tr -d '\n' | grep -abo 'ffd8ff'

发现jpg图片,提取出来

with open("system", "rb") as f:
    data = f.read()

start = data.rfind(b"\xff\xd8\xff")
end = data.find(b"\xff\xd9", start) + 2

with open("out.jpg", "wb") as f:
    f.write(data[start:end])

image

SYSTEMSAM hive 离线提取后,拿到管理员这一条哈希

Administrator:500:aad3b435b51404eeaad3b435b51404ee:476b4dddbbffde29e739b618580adb1e:::

hashcat爆破

image

得到

!checkerboard1

用SilentEye解决jpg隐写

image

得到AES256

然后拿文件之前找到的p@s4w0rd 解密
image

openssl enc -d -aes-256-cbc -in AES256 -out aes.dec -pass pass:p@s4w0rd

最后再解压得到flag

image

DASCTF{aa28f51d-0f54-4286-af3c-86a14fbab4a4}
posted @ 2026-03-17 22:04  Alexander17  阅读(100)  评论(0)    收藏  举报