VNCTF 2025

入坑CTF以来打过最难的个人赛,打完的表情就像先辈的一张表情包一样

img

比赛期间就出了一题,这才是正统misc吗哈哈(

VN_Lang

不能较真的签到题,附件一个exe,一个莫名其妙的源代码。看起来花里胡哨,实际010一查就出来了

image-20250210180505746

echo_flowers

区块链的基本知识和取证搭配的好题。大概?

给了一个安卓的镜像,先VM开了,不得不吐槽的一点是,操作是真吃力啊。。。

先放个hintimage-20250210180853422

我没有那么多知识储备,所以其实能用的hint也不多,就一个软件本身没有缓存image-20250210180951701

看到干干净净的桌面其实就能感觉到不对劲了哈哈,连文件管理都没有还整什么,果断放弃仿真取证。挂DiskGenius

既然说软件本身没有任何缓存。。那么从哪里入手呢,其实和bashhistory有点相似吧(刚好之前看到过类似的题目)

也就是,从输入法的缓存,或者说,输入的记录入手。

定位交给AIimage-20250210181822981

正好我们是可以找到files这一文件夹的image-20250210181930421

接下来一个个看过去其实也可以,最终是可以定位到sgim_gd_usr.bin这个文件的,当然按理来说我们应该先排查.db,.dat,.log这类文件的image-20250210182036252

最终找到助记词ranch only space define laundry carpet muscle ramp high twenty couch fashion

挂上Metamask,记得一定要用没用使用过metamask的浏览器,因为如果你有账户你要导入一般是会让你直接输入密钥登录的

接下来按照指示输入助记词,重置密码,点击你的账户->账户详情->查看密钥就结束了

ezSignal

C3师傅的旷世之作(不仅指半夜更新附件)

当然其实C3师傅给的hint已经相当多了,或者说,都已经把解题步骤告诉你了哈哈(

image-20250210183042001

直接解压会碰到这个问题,那么有人就要问了为什么呢

查下或者根据提示可以发现包里是有一个名字为空格的文件的,而在Windows系统中这种文件是不会被显示的.当然在不知道这点的前提下,可以观察一下压缩包的结构

image-20250210183450579

可以明显看到frFileNameLength是1,并且文件名显示的是’ ‘

那么将压缩包复制到Ubuntu

unzip ezSignal_fix.zip
mv ' ' 2

再复制回来,010查一下2,image-20250210183802628

问一下AI,知道是一个grc文件,搭配GNU使用,改后缀为grc

image-20250210184040460

喂AI/看hint,这是一个窄带调谐+将复数信号转换为虚部实部分别输出的过程,而我们的任务是逆向一下它,写个Python脚本。(赛后C3说可以Cyber chef,可以Linux。。诶我怎么和我的电脑一个温度了)

喂了半天ChatGPT报错报了一下午+一晚上,活全家了哈哈()

抱着试一试的心情给Deepseek跑了一下,两遍过,最支持国产的一集。。。

import numpy as np
from scipy.io import wavfile
from scipy.signal import butter, lfilter

def read_gr_file(filename, dtype=np.float32, endian="<"):
    """
    读取 GRC 生成的二进制文件(如 blocks_file_sink 的输出)
    - dtype: 数据类型(默认 float32)
    - endian: 字节序(默认小端序 "<",大端序用 ">")
    """
    # 以二进制模式读取文件
    with open(filename, "rb") as f:
        raw_bytes = f.read()
    

# 转换为指定字节序和类型的 numpy 数组

dt = np.dtype(f"{endian}{dtype().dtype.char}")
data = np.frombuffer(raw_bytes, dtype=dt)
return data

def main():

    # 参数配置(与 GRC 一致)

     samp_rate = 48000      # 音频采样率
     if_rate = 192000       # 中频速率(usrp_rate/3 = 576000/3)
     max_dev = 5e3          # FM最大频偏
     endian = "<"           # 字节序(GRC 默认小端序)

try:

    # 1. 读取I/Q数据(二进制模式)

     i_data = read_gr_file("flag1.txt", dtype=np.float32, endian=endian)
     q_data = read_gr_file("flag2.txt", dtype=np.float32, endian=endian)

    # 检查长度一致性

     if len(i_data) != len(q_data):
         raise ValueError("I/Q数据长度不一致!")

    # 2. 合并为复数信号(I + jQ)

     complex_signal = i_data + 1j * q_data

    # 3. FM解调(相位差分法)

     phase = np.unwrap(np.angle(complex_signal))
     demodulated = np.diff(phase) / (2 * np.pi * max_dev) * if_rate
     demodulated = demodulated.astype(np.float32)

    # 4. 重采样到音频采样率(48 kHz)

     demodulated_resampled = demodulated[::4]  # 简单下采样

    # 5. 低通滤波(300-5000 Hz,与GRC一致)

     b, a = butter(4, 5000, fs=samp_rate, btype='low')
     demodulated_resampled = lfilter(b, a, demodulated_resampled)

    # 6. 归一化并保存为WAV

     demodulated_resampled /= np.max(np.abs(demodulated_resampled)) * 1.2
     wavfile.write("flag_recovered.wav", samp_rate, demodulated_resampled)

     print("还原成功!保存为 flag_recovered.wav")

except FileNotFoundError:
    print("错误:未找到 flag1.txt 或 flag2.txt!")
except Exception as e:
    print(f"错误:{str(e)}")

if __name__ == "__main__":
    main()

得到一个92.7MB的wav,那其实一想就是SSTV了,用RX-SSTV跑一下

得到一张阿兹特克码,扫一下得到flag2025-02-09_09.53.19

唉,不能老实做传统misc了,不去折腾研究一些东西感觉永远都只能是入门仔了。也算是吃一堑长一智吧。