将未知的音频数据流转换为 wav 或类似格式

逆向工程 解密
2021-06-29 14:30:51

我正在尝试从 dota2 游戏文件中获取评论(解说员语音)。我设法解析了游戏文件并选择了我认为的语音数据。这是一种奇怪的格式 (CSVCMsg_VoiceData),其结构如下:

type CSVCMsg_VoiceData struct {
Client                   *int32            `protobuf:"varint,1,opt,name=client" json:"client,omitempty"`
Proximity                *bool             `protobuf:"varint,2,opt,name=proximity" json:"proximity,omitempty"`
Xuid                     *uint64           `protobuf:"fixed64,3,opt,name=xuid" json:"xuid,omitempty"`
AudibleMask              *int32            `protobuf:"varint,4,opt,name=audible_mask" json:"audible_mask,omitempty"`
VoiceData                []byte            `protobuf:"bytes,5,opt,name=voice_data" json:"voice_data,omitempty"`
Caster                   *bool             `protobuf:"varint,6,opt,name=caster" json:"caster,omitempty"`
Format                   *VoiceDataFormatT `protobuf:"varint,7,opt,name=format,enum=VoiceDataFormatT,def=1" json:"format,omitempty"`
SequenceBytes            *int32            `protobuf:"varint,8,opt,name=sequence_bytes" json:"sequence_bytes,omitempty"`
SectionNumber            *uint32           `protobuf:"varint,9,opt,name=section_number" json:"section_number,omitempty"`
UncompressedSampleOffset *uint32           `protobuf:"varint,10,opt,name=uncompressed_sample_offset" json:"uncompressed_sample_offset,omitempty"`
XXX_unrecognized         []byte            `json:"-"`

}

这在读取数据时似乎有效。从逻辑上讲,当给出以下内容时,我可能正在寻找结构的 VoiceData 部分:

“格式”:0 “voice_data”: “UZ + ACgEAEAELgD4EQgEWAKV4mxnepfmhxKCQxAnKVNaHhKRXPIsmAH5RjXmJV0u + WTmrvgyCKxcraehjo / ZeKcFjksXQZEeOju4hLNv / MAB9KA7ww14Vc0ndYPB7dDXoXTexuxcW0Jg / diMgdH5ijWhe02Ch48KX86qJZYFyZV81AH76qCgh9AXliMdyWEgWTMbRD6xMX37WJALrXlSnxymIloSq2KGwXCcMXzQiSQIrcLVNfqdNJACCluFOIRKPmugUvsLZmnD04X0xhpAuNkwJECK4t51MBOWNWJlCAIDyZlJwWI45EPTjBB6yKyGOclu96qBV2MhFAh1d2J7WDZwe6YxOVu / BGkGcur9qTP85ZRfjANoiQxQrWvpoHFBFBy0AfX6k8XvbSwrk2nUAEP3P6kcmXORKUNKeu8HDnOUflQqtA5AkkTiun77fZrqnimIfWg ==”, “sequence_bytes”:23598094, “的section_number”:1, “SAMPLE_RATE”:16000

我能拉语音数据出来,像这样: UZ + ACgEAEAELgD4EQgEWAKV4mxnepfmhxKCQxAnKVNaHhKRXPIsmAH5RjXmJV0u + WTmrvgyCKxcraehjo / ZeKcFjksXQZEeOju4hLNv / MAB9KA7ww14Vc0ndYPB7dDXoXTexuxcW0Jg / diMgdH5ijWhe02Ch48KX86qJZYFyZV81AH76qCgh9AXliMdyWEgWTMbRD6xMX37WJALrXlSnxymIloSq2KGwXCcMXzQiSQIrcLVNfqdNJACCluFOIRKPmugUvsLZmnD04X0xhpAuNkwJECK4t51MBOWNWJlCAIDyZlJwWI45EPTjBB6yKyGOclu96qBV2MhFAh1d2J7WDZwe6YxOVu / BGkGcur9qTP85ZRfjANoiQxQrWvpoHFBFBy0AfX6k8XvbSwrk2nUAEP3P6kcmXORKUNKeu8HDnOUflQqtA5AkkTiun77fZrqnimIfWg ==

然而,这是我碰到了一些障碍的地方。此数据的格式未知。我试图对可能的格式进行一些研究,我发现 Steam 在 2011 年开始使用 SILK 编解码器处理语音数据 - 但是当尝试将此数据写入文件并使用 opus 打开它时(我相信它支持SILK) opus 解码器告诉我它无法打开文件 - 所以我不是 100% 相信它是 Silk 编解码器。识别音频数据不是我有丰富经验的事情 - 所以任何建议都会很棒。

我注意到结构中有一个 VoiceDataFormatT 部分,但我能找到的唯一定义是:

type VoiceDataFormatT int32

这似乎没有太大帮助!:/

编辑 1: 根据用户 Ian Cook 的建议,我已将 base64 中的数据解码为以下内容(作为十六进制转储):

BB 3F 80 0A 01 00 10 01 0B 80 3E 04 42 01 16 00 A5 78 9B 19 DE A5 F9 A1 C4 A0 90 C4 09 CA 54 D6 87 84 A4 87 8 9 5 B 9 5 B 7 9 D B AB BE 0C 82 2B 17 2B 69 E8 63 A3 F6 5E 29 C1 63 92 C5 D0 64 47 8E 8E EE 21 2C DB FF 30 00 7D 28 0E F0 C3 5E 6 7 D 3 7 D 3 E 5 B 7 5 E 3 B 17 16 D0 98 3F 76 23 20 74 7E 62 8D 68 5E D3 60 A1 E3 C2 97 F3 AA 89 65 81 72 65 5F 35 00 7E FA A8 28 5 7 4 F A8 28 5 7 4 F 8 C 8 5 C 8 4 F 7 F 5F 7E D6 24 02 EB 5E 54 A7 C7 29 88 96 84 AA D8 A1 B0 5C 27 0C 5F 34 22 49 02 2B 70 B5 4D 7E A7 4D 24 06 1 9 A 24 06 1 9 A 24 06 E 1 9 A 24 06 E 1 9 F4 E1 7D 31 86 90 2E 36 4C 09 10 22 B8 B7 9D 4C 04 E5 8D 58 99 42 00 80 F2 66 52 70 58 8E 39 10 F4 1 B7 9D 4C 04 E5 8D 58 99 42 00 80 F2 66 52 70 58 8E 39 10 F4 1 B7 E3 2 5 5 B C 5 E3 2 5 04 B 5 8 B 1D 5D D8 9E D6 0D 9C 1E E9 8C 4E 56 EF C1 1A 41 9C BA BF 6A 4C FF 39 65 17 E3 00 DA 22 43 14 2B 5A FA 68 1C50 45 07 2D 00 7D 7E A4 F1 7B DB 4B 0A E4 DA 75 00 10 FD CF EA 47 26 5C E4 4A 50 D2 9E BB C1 C3 9C E5 1F 90 3 9 DFA 6 FAA 6 BA 3 8A 62 1F 5A

我仍然不知道这些信息是什么 - 我已经尝试使用 ffmpeg(假设是 pcm)将其转换为 wav 文件,但它仍然以白噪声的形式出现。

编辑 2: 所以我想到如果我包含更多数据样本可能会有所帮助 - 可以在此处找到解码的数据十六进制(每个样本由换行符分隔):

粘贴箱

我注意到每个似乎都以以下十六进制开头:

BB 3F 80 0A 01 00 10 01 0B 80 3E 04

翻译成:

»?€ €>

我仍然不知道如何将其转换为音频数据。

编辑 3: 我已经将更多的数据转储上传到以下 pastebin(更多数据),它不是一个完整的转储,因为它大约 15mb 并且当我尝试粘贴时 pastebin 崩溃了!

数据文件是一个 dota2 演示文件(扩展名 .dem),它是我使用 GoLang 和 Manta 重放解析(在这里找到解析的 protobuf 消息的集合这允许我提取任何类型的消息,我选择 OnCSVCMsg_VoiceData,它返回 m.Audio.VoiceData 的形式:CSVCMsg_VoiceData(我在上面显示的结构)。

编辑 4

这是(最后)带有连接的 voiceData 消息的文件的链接

是 protobuff消息原始文件的链接

2个回答

有3种类型“帧”的,我想3个脚轮
BB 3F 80 0A 01 00 10 01 0B 80 3E 04 42 01(@为0x0)
D8 76 DD 02 01 00 10 01 0B 80 3E 04 FA 01(@ 0x5f0)
67 7D 11 05 01 00 10 01 0B 80 3E 04 7E 01 (@ 0x44ccf)

第一个例子:
BB 3F 80 0A 脚轮标识符
01 通道号 单声道
80 3E = 0x3e80 =16000 速率
42 01 = 0x142 丝数据的大小

在大小之后的 0x142 字节是丝绸文件的数据,
只需添加丝绸头 #!SILK_V3
23 21 53 49 4C 4B 5F 56 33

我使用 Silk_v3_decoder.exe(?某些 python 脚本可以做到)
Silk_v3_decoder.exe in.hex out.pcm -Fs_API 16000
然后
ffmpeg -f s16le -ar 16000 -ac 1 -i out.pcm out.wav

一帧代表很短的时间,所以所有的数据都必须连接起来
(如无毛熊所说)

注意:在“帧”的末尾有 4 个字节可以是校验和

TL; 博士

  1. 每个section n表示一个单独的数据流
  2. sequence_bytes值指示解码时应放置帧的顺序。
  3. voice_data是base64编码
    1. 解码后的数据是一个 SILK 编码的帧,但有以下例外:
      1. 前 14 个字节
      2. 最后 4 个字节
  4. 要解码数据,您必须执行以下操作:
    1. 对于每个section n,根据n的值按升序对n的结构进行排序sequence_bytes
    2. De-base64 每个结构的 voice_data
    3. 从每个结构中提取 SILK 负载(即删除前 14 个字节和最后 4 个字节)并将它们全部连接在一起(同样,必须按顺序基于sequence_bytes
    4. 在生成的文件前加上#!SILK_V3(SILK 标头)
    5. 您现在有一个可以解码的有效 SILK 文件(详情如下)

长版

使用您发布示例数据,我必须做的第一件事是用 a 替换最后一个逗号]以使其成为有效的 JSON。

我最初使用 shell 脚本将结构从 JSON 转换为 SILK,但为了提高效率,我在 Python 中重新实现了转换。

import json
import base64
import sys

def main():
    if len(sys.argv) < 2:
        print("Usage: python3", sys.argv[0], "<CSVCMsg_VoiceData json file>")
        exit(1)

    with open(sys.argv[1], 'r') as infile:
        json_data = json.load(infile)

    # Create dictionary with section number as the key and list of
    # that section's structs as the value
    section_dict = {}
    for obj in json_data:
        sec_num = obj['section_number']
        if sec_num not in section_dict:
            section_dict[sec_num] = []
        section_dict[sec_num].append(obj)

    # Create SILK file for each section number stream
    for section in section_dict.keys():
        filename=f"section_{section}.slk"
        print(f"Generating SILK file {filename} for section {section}...")
        with open(filename, 'wb') as outfile:
            # SILK header
            outfile.write(b"#!SILK_V3")
            # Sort frames in ascending order based on sequence_bytes value
            for frame in sorted(section_dict[section], key=lambda x : x['sequence_bytes']):
                decoded = base64.b64decode(frame['voice_data'])
                # strip first 14 bytes and last 4 bytes before writing
                outfile.write(decoded[14:-4])

if __name__ == '__main__':
    main()

为了解码 SILK,我使用了官方 SDK(这是 Gordon Freeman 链接的解码器构建在其之上的)。SDK 可以从这个链接下载,我是从这个页面找到的

下载 SDK 后,我将其解压缩,进入名为 的目录SILK_SDK_SRC_FIX_v1.0.9,然后运行make(我在 Kali 上,但几乎任何 Linux 变体都应该没问题)。

一旦make完成,你留下了一对夫妇的可执行文件; 我们唯一关心的是decoder

只需decoder在上面生成的 SILK 有效负载上运行,你就会得到一个 pcm 文件,你可以用它做任何你想做的事。例如,./decoder section_12.slk section_12.pcm输出文件的频率为 22050 Hz。

向@Gordon Freeman 致敬,他指出标头不是我最初怀疑的 18 个字节,并且最后 4 个字节不是 SILK 有效负载的一部分。

旧的 shell 脚本

对于后代,这里是我如何使用 shell 脚本将 JSON 转换为 SILK 文件。

我使用以下脚本来提取数据,对它进行 de-base64,并将每个结构的数据放在自己的文件中。

#!/bin/bash

# Write each decoded VoiceData to a file with the naming convention
# <sequence_bytes>_<section_number>
write_data ()
{
    filename=`echo $1 | cut -d_ -f1,2`
    data=`echo $1 | cut -d_ -f3`
    echo -n "$data" | base64 -d > $filename
}
export -f write_data
jq -r '.[] | "\(.sequence_bytes)_\(.section_number)_\(.voice_data)"' dota2CasterParse.json | xargs -I '{}' bash -c "write_data '{}'"

然后我使用以下脚本为每个部分创建一个 SILK 文件:

#!/bin/bash

section_numbers=$(ls [0-9]*_[0-9]* | cut -d_ -f2 | sort -u)

for section in $section_numbers; do
    output="section_${section}_voiceData.slk"
    echo -n '#!SILK_V3' > $output
    for i in $(ls *_${section} | sort -n); do
        dd bs=1 skip=14 count=$(($(stat -c "%s" $i)-18)) if=$i of=$output conv=notrunc oflag=append
    done
done