题目内容:

经过我国执法部门的努力,终于在今年十月提取出了张纪星(系杜撰名字,与现实人员无关)被捕前布置的监听设备中的加密信息,据本人供述其曾恢复过我国一份绝密情报。

flag为情报所提及的详细时间和地址的md5值,即flag{md5(x年x月x日x时x分于x地)}。
附件下载[1]

解答

主要思路

解压题目发现一个流量包和一个被加密的7z压缩包,推测压缩包的密码应该就是在流量包里面了。

流量包数据识别和提取

流量包打开发现全是udp数据,跟踪某一端口的流量发现,DATA数据全是16进制的

丢AI识别分析一下,claude说这个是RTP数据,可能是音频文件数据。给我写了还原的代码。

这个wav文件还原出来是个音频文件,但是实在是刺啦刺啦的听不太清楚。


降噪之后重新识别一下,能看到一些字眼,但是不全。
尝试导出全端口的,发现音频十分混乱了。
tshark -r Data.pcap -T fields -e frame.number -e udp.payload > udp_payloads.txt
xxd -r -p udp_payloads.txt udp_payloads.bin
.ffmpeg.exe -f s16le -ar 8000 -ac 1 -i udp_payloads.bin output.wav


如果尝试提取全部流量的音频的话,又会有很多杂音,难以去除,而且总的时长达到9小时,其中一些降噪后的音频文件虽然还保留人声但是前后文字就不连贯了。
tshark -r Data.pcap -Y 'udp' -T fields -e udp.srcport -e udp.dstport | sort | uniq -c | sort -nr >ports.txt
tshark -r Data.pcap -Y 'udp.port==51075' -T fields -e udp.payload > udp_payloads_50002.txt
统计发现存在不同端口的通信,一般dst就是src+10000的数字端口。统计共有1301条这样的1对1的端口通信。将其中一条拿出来看。

确实有内容,所以接下来的思路就是写脚本批量提取端口,然后导出为wav文件,丢讯飞里面识别。

# 清理端口文件
sed -i 's/r//g' ports.txt

# 确保输出目录存在

mkdir
 -p payloads

# 运行提取

while
 IFS= read -r port; do
    tshark -r Data.pcap -Y 'udp.port==$port' -T fields -e udp.payload > 'payloads/udp_payloads_$port.txt'
done
 < ports.txt

# 并行加速

parallel -j 8 'tshark -r Data.pcap -Y 'udp.port=={}' -T fields -e udp.payload > payloads/udp_payloads_{}.txt' :::: ports.txt

在后台运行:
nohup ./extract_payloads.sh > extraction.log 2>&1 &
要查看后台任务状态:
tail -f extraction.log

RTP数据转WAV

然后再用批量rtp解码脚本处理这每一个端口的信息

#!/usr/bin/env python3
'''
批量 RTP 载荷解码工具
自动过滤 0xFF 杂音并转换为 WAV 音频文件

用法:
    python batch_rtp_decoder.py udp_payloads_50002.txt
    python batch_rtp_decoder.py input_folder/  # 批量处理文件夹
'''


import
 sys
import
 os
import
 struct
import
 wave
import
 numpy as np
from
 pathlib import Path


# ========== 配置参数 ==========

NOISE_FILTER_ENABLED = True      # 是否启用杂音过滤
NOISE_THRESHOLD = 8               # 连续多少个 0xFF 视为杂音
SAMPLE_RATE = 8000                # 采样率 (Hz)
CHANNELS = 1                      # 声道数
SKIP_RTP_HEADER = True           # 是否跳过 RTP 头部(前12字节)
OUTPUT_DIR = 'decoded_audio'      # 输出目录


def
 hex_string_to_bytes(hex_str):
    '''
    将 tshark 导出的十六进制字符串转换为字节数组
    支持格式: '7e7eff' 或 '7e 7e ff' 或 '7e:7e:ff'
    '''

    # 移除所有非十六进制字符

    hex_str = ''.join(c for c in hex_str if c in '0123456789abcdefABCDEF')
    
    # 转换为字节

    try
:
        return
 bytes.fromhex(hex_str)
    except
 ValueError as e:
        print
(f'⚠️  十六进制解析错误: {e}')
        return
 None


def
 remove_rtp_header(data):
    '''
    移除 RTP 头部(标准为12字节,可能有CSRC)
    '''

    if
 len(data) < 12:
        return
 data
    
    # 检查 RTP 版本(前2位应该是 10,即版本2)

    version = (data[0] >> 6) & 0x03
    if
 version != 2:
        # 可能不是标准 RTP 包,返回原数据

        return
 data
    
    # 获取 CSRC 数量(第1个字节的低4位)

    csrc_count = data[0] & 0x0F
    header_size = 12 + (csrc_count * 4)
    
    # 检查是否有扩展头部

    has_extension = (data[0] >> 4) & 0x01
    if
 has_extension and len(data) > header_size + 4:
        # 读取扩展长度

        ext_length = struct.unpack('!H', data[header_size+2:header_size+4])[0]
        header_size += 4 + (ext_length * 4)
    
    return
 data[header_size:]


def
 filter_noise(byte_data):
    '''
    过滤连续的 0xFF 杂音
    检测连续 NOISE_THRESHOLD 个或以上的 0xFF,并替换为 0x7E(中值)
    '''

    if
 not NOISE_FILTER_ENABLED:
        return
 byte_data
    
    data = bytearray(byte_data)
    length = len(data)
    silenced_count = 0
    
    i = 0
    while
 i < length:
        if
 data[i] == 0xFF:
            # 检测连续 0xFF 的长度

            consecutive = 1
            j = i + 1
            while
 j < length and data[j] == 0xFF:
                consecutive += 1
                j += 1
            
            # 如果连续数量达到阈值,替换为静音(0x7E 或 0x80)

            if
 consecutive >= NOISE_THRESHOLD:
                for
 k in range(i, j):
                    data[k] = 0x80  # 使用中值作为静音
                silenced_count += consecutive
                i = j
            else
:
                i += 1
        else
:
            i += 1
    
    return
 bytes(data), silenced_count


def
 convert_to_pcm(byte_data):
    '''
    将字节数据转换为 16-bit PCM 采样
    0xFF -> 最大正值, 0x7E -> 最大负值, 其他 -> 线性映射
    '''

    pcm_data = []
    
    for
 byte in byte_data:
        if
 byte == 0xFF:
            pcm_data.append(32767)   # 最大正值
        elif
 byte == 0x7E:
            pcm_data.append(-32768)  # 最大负值
        else
:
            # 线性映射 0-255 到 -32768~32767

            pcm_data.append((byte - 128) * 256)
    
    return
 np.array(pcm_data, dtype=np.int16)


def
 create_wav_file(pcm_data, output_path, sample_rate=8000):
    '''
    创建 WAV 文件
    '''

    with
 wave.open(output_path, 'wb') as wav_file:
        wav_file.setnchannels(CHANNELS)
        wav_file.setsampwidth(2)  # 16-bit
        wav_file.setframerate(sample_rate)
        wav_file.writeframes(pcm_data.tobytes())


def
 process_payload_line(hex_line):
    '''
    处理单行载荷数据
    返回: (pcm_data, stats) 或 None
    '''

    # 转换十六进制到字节

    byte_data = hex_string_to_bytes(hex_line)
    if
 byte_data is None or len(byte_data) == 0:
        return
 None
    
    # 跳过 RTP 头部

    if
 SKIP_RTP_HEADER:
        byte_data = remove_rtp_header(byte_data)
    
    # 过滤杂音

    byte_data, silenced_count = filter_noise(byte_data)
    
    # 转换为 PCM

    pcm_data = convert_to_pcm(byte_data)
    
    stats = {
        'original_bytes'
: len(byte_data),
        'silenced_count'
: silenced_count,
        'samples'
: len(pcm_data),
        'duration'
: len(pcm_data) / SAMPLE_RATE
    }
    
    return
 pcm_data, stats


def
 process_single_file(input_file, output_dir):
    '''
    处理单个载荷文件
    每行是一个 UDP 包的载荷(十六进制)
    '''

    print
(f'n{'='*60}')
    print
(f'📄 处理文件: {input_file}')
    print
(f'{'='*60}')
    
    # 读取所有载荷行

    try
:
        with
 open(input_file, 'r', encoding='utf-8') as f:
            lines = [line.strip() for line in f if line.strip()]
    except
 Exception as e:
        print
(f'❌ 读取文件失败: {e}')
        return
 False
    
    print
(f'📦 共找到 {len(lines)} 个 UDP 载荷包')
    
    # 合并所有有效载荷

    all_pcm_data = []
2025 第九届强网杯初赛-MISC-谍影重重 6.0 WP by 静安
    total_silenced = 0
    valid_packets = 0
    
    for
 idx, line in enumerate(lines, 1):
        result = process_payload_line(line)
        if
 result:
            pcm_data, stats = result
            all_pcm_data.append(pcm_data)
            total_silenced += stats['silenced_count']
            valid_packets += 1
            
            if
 idx % 100 == 0:
                print
(f'   处理进度: {idx}/{len(lines)} ({idx/len(lines)*100:.1f}%)')
    
    if
 not all_pcm_data:
        print
('❌ 没有找到有效的音频数据')
        return
 False
    
    # 合并所有 PCM 数据

    merged_pcm = np.concatenate(all_pcm_data)
    
    # 生成输出文件名

    input_name = Path(input_file).stem
    output_path = os.path.join(output_dir, f'{input_name}.wav')
    
    # 保存 WAV 文件

    create_wav_file(merged_pcm, output_path, SAMPLE_RATE)
    
    # 统计信息

    duration = len(merged_pcm) / SAMPLE_RATE
    silence_ratio = (total_silenced / len(merged_pcm) * 100) if len(merged_pcm) > 0 else 0
    
    print
(f'n✅ 转换完成!')
    print
(f'   📊 统计信息:')
    print
(f'      有效数据包: {valid_packets}/{len(lines)}')
    print
(f'      总采样数: {len(merged_pcm):,}')
    print
(f'      音频时长: {duration:.2f} 秒 ({duration/60:.2f} 分钟)')
    print
(f'      采样率: {SAMPLE_RATE} Hz')
    print
(f'      杂音过滤: {'已启用' if NOISE_FILTER_ENABLED else '已禁用'}')
    if
 NOISE_FILTER_ENABLED:
        print
(f'      静音处理: {total_silenced:,} 字节 ({silence_ratio:.2f}%)')
    print
(f'   💾 输出文件: {output_path}')
    
    return
 True


def
 process_directory(input_dir, output_dir):
    '''
    批量处理目录中的所有 .txt 文件
    '''

    input_path = Path(input_dir)
    txt_files = list(input_path.glob('*.txt'))
    
    if
 not txt_files:
        print
(f'❌ 在 {input_dir} 中没有找到 .txt 文件')
        return

    
    print
(f'n🔍 找到 {len(txt_files)} 个文件待处理')
    
    success_count = 0
    for
 idx, txt_file in enumerate(txt_files, 1):
        print
(f'n[{idx}/{len(txt_files)}] ', end='')
        if
 process_single_file(str(txt_file), output_dir):
            success_count += 1
    
    print
(f'n{'='*60}')
    print
(f'🎉 批处理完成!')
    print
(f'   成功: {success_count}/{len(txt_files)} 个文件')
    print
(f'   输出目录: {output_dir}')
    print
(f'{'='*60}')


def
 main():
    if
 len(sys.argv) < 2:
        print
('用法:')
        print
('  单文件: python batch_rtp_decoder.py udp_payloads_50002.txt')
        print
('  批量:   python batch_rtp_decoder.py input_folder/')
        print
('n配置参数 (在脚本顶部修改):')
        print
(f'  NOISE_FILTER_ENABLED = {NOISE_FILTER_ENABLED}')
        print
(f'  NOISE_THRESHOLD = {NOISE_THRESHOLD}')
        print
(f'  SAMPLE_RATE = {SAMPLE_RATE}')
        print
(f'  SKIP_RTP_HEADER = {SKIP_RTP_HEADER}')
        sys.exit(1)
    
    input_path = sys.argv[1]
    
    # 创建输出目录

    os.makedirs(OUTPUT_DIR, exist_ok=True)
    
    # 判断是文件还是目录

    if
 os.path.isfile(input_path):
        process_single_file(input_path, OUTPUT_DIR)
    elif
 os.path.isdir(input_path):
        process_directory(input_path, OUTPUT_DIR)
    else
:
        print
(f'❌ 路径不存在: {input_path}')
        sys.exit(1)


if
 __name__ == '__main__':
    main()

批量提取

这个切片能听到说了时间大概是后天下午十五点半?整个音频一直有杂音,有种做英语听力的感觉。但是这个处理速度真的是太捉急了,25分钟给出了40个文件,假设一个端口要1分钟,1301个大约需要21小时,优化一下输出。

# 一次读取,按端口分类输出,大约20分钟
#!/bin/bash


# UDP payload 批量提取脚本

# 一次性读取pcap文件,按端口分类保存payload


# 创建输出目录

mkdir
 -p payloads

echo
 '开始提取UDP payloads...'
echo
 '时间: $(date)'

# 一次性提取所有UDP数据并按端口分类

tshark -r Data.pcap -Y 'udp' -T fields -e udp.srcport -e udp.dstport -e udp.payload |
awk '{
    # $1 是源端口, $2 是目的端口, $3 是payload
    if ($3 != '') {
        ports[$1] = ports[$1] $3 'n'
        ports[$2] = ports[$2] $3 'n'
    }
}
END {
    for (port in ports) {
        file = 'payloads/udp_payloads_' port '.txt'
        printf '%s', ports[port] > file
        close(file)
    }
    print '共提取了 ' length(ports) ' 个端口的数据' > '/dev/stderr'
}'


echo
 '提取完成!'
echo
 '时间: $(date)'
echo
 '文件保存在 payloads/ 目录'
ls
 -lh payloads/ | wc -l

20分钟处理完成了,我文件夹下有1301个wav文件,每个平均1分钟,但是每个文件基本都是0到10秒有一段语音,20秒到30秒有一段内容,其他时候是没有意义的背景音,写个脚本,只提取每个wav文件的0-10和20-31秒的内容,按照顺序拼接为一整段的内容。我的ffmpeg是安装在windows下面所以这里就用powershell命令了,另存为ps1就行。后来又发现不一定都是0-10和20-31秒有人说话的内容,也有在30秒才开始说话的,让脚本智能的判断一下有没有说话,没有就不要合并,只截取说话的部分内容来合并。

# 简化版:提取前60秒中所有有声片段(保留开头)
$inputDir
 = 'decoded_audio'
$tempDir
 = 'temp_segments'
$outputFile
 = 'merged_audio_smart.wav'
$ffmpeg
 = 'D:Programsffmpeg-2025-03-31-git-35c091f4b7-full_buildbinffmpeg.exe'

New-Item
 -ItemType Directory -Force -Path $tempDir | Out-Null
Remove-Item
 -Path 'filelist.txt' -ErrorAction SilentlyContinue

Write-Host
 '开始智能提取有声片段...' -ForegroundColor Green

$wavFiles
 = Get-ChildItem -Path $inputDir -Filter '*.wav' | Sort-Object Name
$count
 = 0
$segmentIndex
 = 0

foreach
 ($file in $wavFiles) {
    $count
++
    Write-Host
 '[$count/$($wavFiles.Count)] 处理: $($file.Name)'
    
    $outputSegment
 = '$tempDirsegment_${count}.wav'
    
    # 修改参数:start_periods=0 表示不删除开头的静音

    # 只删除结尾的静音

    & $ffmpeg -i $file.FullName -t 60 `
        -af
 'silenceremove=start_periods=0:stop_periods=-1:stop_threshold=-40dB:stop_duration=1' `
        $outputSegment
 -y -loglevel error
    
    # 检查输出文件是否有效(大于1KB表示有内容)

    if
 ((Test-Path $outputSegment) -and ((Get-Item $outputSegment).Length -gt 1000)) {
        $segmentIndex
++
        Add-Content
 -Path 'filelist.txt' -Value 'file '$($outputSegment -replace '\','/')''
        Write-Host
 '  -> 已提取有声片段' -ForegroundColor Green
    } else {
        Write-Host
 '  -> 无有效内容,跳过' -ForegroundColor Gray
        Remove-Item
 -Path $outputSegment -ErrorAction SilentlyContinue
    }
}

Write-Host
 '`n共提取 $segmentIndex 个有声片段,开始合并...' -ForegroundColor Green

if
 ($segmentIndex -gt 0) {
    & $ffmpeg -f concat -safe 0 -i filelist.txt -c copy $outputFile -y
    Write-Host
 '完成!输出: $outputFile' -ForegroundColor Green
} else {
    Write-Host
 '未检测到有声片段!' -ForegroundColor Red
}



得到一个4小时的音频,真能讲。刚好压着讯飞的5小时赠送的时长包。

开始做阅读理解

难崩,比女的几点到咖啡馆更炸裂的中文听力题目。

关键内容40259端口提取

发现一段比较突兀的数字组合,出现在合并后的轨道第52分钟,翻原本的端口文件一个一个听,是第udp_payloads_40259的,为了验证,重新提取一遍

tshark -r Data.pcap -Y 'udp.port==40259' -T fields -e udp.payload > udp_payloads_40259.txt

但是这个声音真的难绷,听不清压根。采样率: 6000 Hz,和8000对比着听,得到如下数字
6665 1466 3145 1427 1616 6142 1466 0701 4566 6160 1411 4514 2607 7146 6660 1421 4371 6565 1421 4470

中间有一个7和1分不清,还是想不出来这个数字是什么的密码组合,然后官方给了个提示。

Tip

题目提示本题依托于架空的时间线,取材自真实历史事件,请关注地点信息。

但是目前为止并没有什么用。期间还尝试挂着hash爆破的脚本直接爆7z压缩包的密码,跑了13小时也没出结果。

八进制转字符

比赛结束后看别的大佬的WP补完最后的结果:

2025-强网杯-WriteUp By N0wayBack提到这串数字正确的内容应该是

651 466 3145 1427 1616 6142 1466 0701 4566 6160 1411 4514 2607 1146 6660 1421 4371 6565 1421 4470

是八进制,换成二三二三排列的格式

651466314514271616614214660701456661601411451426071146666014214371656514214470
echo
 $'651466314514271616614214660701456661601411451426071146666014214371656514214470'



这段录音还有粤语,所以识别的时候要勾上粤语识别。

识别结果如下:

情报信息分析

查询到双鲤湖在金门,所以重点就是搜索金门,双鲤湖的重点历史时间,一般金门的历史时间都和台湾有关,所以也要搜索两岸关系的主题。

大概能确定这个年月日是1949年10月24日,因为考虑到这个架空背景是个谍报,所以这个截获的电报是在正式战役之前,文中提到甘四茶叶就是指24号。

所以,年月日时分的信息是:1949年10月24日8时45分于双鲤湖西岸南山茶铺
echo -n '1949年10月24日8时45分于双鲤湖西岸南山茶铺' | md5sum | awk '{print $1}'

[!success] flag
flag{2a97dec80254cdb5c526376d0c683bdd}

参赛总结

这题因为我已经参加过3届强网杯(都在划水),知道谍影重重这个系列比较好玩,所以一开始就是一头猛扎谍影重重,抓间谍张纪星。还记得有一年的一题是提取机场的,就是找了半天4个机场都试过了,我还去翻了航空手册机场对应的缩写也都不是答案,后来发现不是哪个思路。谍影重重系列是比较好玩的,大间谍张纪星年年被抓年年逃。预言一波明年还有谍影重重7.0,可以拍网安连续剧了都。

对于RTP音频的处理得益于紧急充了一个claude pro会员的助力,在识别和处理pacp数据上非常贴心,几乎是开赛一小时后10点的时候我就解出来wav文件在分析了,大概是12点的时候跑出来批量的1301个wav文件,使用加速后的代码20分钟完成,此时这个题目还没有人作答,我还想着能冲一血。

没想到卡死在数字识别上,由于音质真的是非常的战损风啊,背景还有消除不掉的刺啦刺啦声音,非常污染耳朵。我尝试用Audacity软件做降噪和标准化处理还是有几个数字听不清。把录音文件发给其他组员听也是听出来的数字不一样。尤其是1和7在声音里面真的很难识别。

对于得到的数字组合,尝试了几种常见的编码都不能确定是。有组员观察发现了没有数字8推测是8进制(距离成功最近的一次),但是因为前期听错了数字组合,用8进制没有解出正确的答案,所以也就放弃了。

引用链接

[1] 附件下载: https://ichunqiu-fujian.oss-cn-beijing./ba5ab86a661f0c8904d5bd2d6732cbd4/%E8%B0%8D%E5%BD%B1%E9%87%8D%E9%87%8D6.0_0e603d136d7edd20ad6008a0c5d03eab.7z