ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

流量包分析必备:5种隐藏信息提取技巧与Python脚本实战

流量包分析必备:5种隐藏信息提取技巧与Python脚本实战

前阵子带着一批入门MISC的选手刷题,我发现一个特别有意思的现象:流量包分析在很多人眼里是最“劝退”的题型——一个几十MB的pcap文件扔过来,Wireshark打开满屏都是包,根本不知道从哪起手。但刷到后面大家都会认同一个结论:流量包题目难不在技术,而在“信息定位”。出题人把flag藏来藏去,无非是那么几个地方,你只要掌握对应的提取思路,再配合Python脚本去做重复劳动,效率和正确率都能上来一大截。

这篇文章我把最近实战中总结的5种隐藏技巧完整拆开来讲,从HTTP响应里的图片尾巴,到DNS请求里的一串可疑字符,再到TCP流中手捡文件头,每一个都会给出可以直接复用的Python脚本。适合正在准备CTF比赛、想进阶MISC方向的选手,也适合做流量分析但想提升效率的安全工程师。文章里所有脚本都是我在实际刷题环境里剪出来改过的,能跑、能改,照着抄基本就能应对大半流量包题目。

1. 先摸清流量包里藏信息的三种路子

流量分析题能玩的点,其实绕不开三类信息载体。先把这三类搞清楚,做题的时候就有了地图,看到一个pcap不会两眼一抹黑。

1.1 文件对象、协议元数据、载荷暗语这三类信息载体

第一类是文件对象。HTTP协议在传输过程中最常见的载荷就是文件——图片、压缩包、PDF、甚至直接就是一段exe。这些文件通过HTTP响应体传过来,肉眼在Wireshark里看不到完整结构,但用工具提取出来后,可以做文件层面的隐写分析。很多题目的flag不是直接写在包里的,而是藏在某个被传输的图片的LSB位里,或者附加在文件尾部。所以看到HTTP上传下载的痕迹,第一反应就是“把文件捞出来”。

第二类是协议元数据。不需要看载荷内容,光看协议字段本身就能藏信息。最典型的就是DNS查询里的子域名,出题人把编码后的flag拆成一段段子域名字符串,塞进DNS请求里;还有HTTP的头部字段、TCP的序列号、TLS握手的SNI字段,都有被改写的可能。这类信息的特征是“藏得浅但隐蔽”,因为一般分析者懒得逐条看协议字段。

第三类是载荷暗语。TCP或UDP的payload可能是一整段经过编码的字符串,比如base64、十六进制,或者直接就是一段加密后的密文,需要先解码、再解密、再分析。有些题目甚至直接把压缩包拆成多个切片分散在多个包里,需要按序拼回去。这一类最考验解码和恢复能力,也是脚本发挥最大价值的地方。

1.2 为什么用 Python + Scapy,而不是纯 Wireshark

Wireshark当然适合做人工逻辑分析,它的图形界面、过滤器、Follow TCP Stream都是神器。但它有两个痛点:一是大pcap文件手动点着找太慢,二是重复性的提取操作每次都重新点一遍就很折磨。

所以我一般的主流程是用Wireshark做第一轮“人眼侦查”,确认可疑方向后,再用Python的Scapy或者dpkt库做第二轮批量挖掘。Scapy的rdpcap能把整个pcap读成包列表,每个包都能按层去索引,比如pkt[TCP].payload直接拿TCP载荷,配合pkt[IP].src、pkt[DNS].qd.qname这些字段就能快速过滤。如果你更喜欢命令行的方式,tshark做文本导出也很顺手,但遇到复杂的解码、重组逻辑,最终还是Python顺手。

关于工具链,建议安装Scapy、Pillow(PIL)、pyzipper这三个库就够了,后面脚本都基于它们。解析大部分pcap和图片隐写操作都不用额外再装什么重型依赖。

2. 从流量包抠flag的5种隐藏技巧

这五种技巧是我从一类类题目里筛出来的,基本是流量包MISC题里出现频率最高的五种藏法。每一种我都会讲清楚“藏在哪里”、“怎么发现”、“怎么提取”,有基础的朋友可以直接跳到对应小节看脚本思路。

2.1 技巧一:HTTP响应里的文件先提取,再检查图片尾巴和LSB

这种题目的pcap里会有一个明显的HTTP GET请求,请求某个图片或者压缩包,响应体里就是这个文件。难点从来不在“拿到文件”,而在于“拿到文件之后做什么”。

我的标准流程是这样的:先用tshark把HTTP响应里的文件数据导出来,或者用Python脚本遍历HTTP层抓http.file_data字段。拿到文件后先用file命令看真实类型,用binwalk扫一遍有没有附加数据,再用strings拽一遍看有没有明文字符串。如果这三步都没有结果,就要对图片本身做隐写分析:检查尾部是否被追加了数据,处理LSB隐写,留意EXIF信息或PNG的tEXt注释块。

我实际遇到过一道题,pcap里传输了一张PNG,表面上看是一张普通的二维码。但binwalk显示文件尾部有额外数据,把尾部的一段十六进制转ASCII后直接就是flag{...}。这种属于出题人给你送的温暖。更常见的是尾部没有额外数据,但图片用StegSolve打开后在Red plane 0、Green plane 0通道能看到一行若隐若现的字符,这就是LSB隐写。处理这类情况建议优先用zsteg工具一键检测,比手动提取RGB值快得多。

这个技巧的核心经验是:不要在拿到文件的那一刻就以为题做完了,文件本身就是分析对象。

2.2 技巧二:DNS查询子域名里的变形字符串

DNS请求里藏数据在真实攻击中也存在,攻击者会把窃取的数据拆成一段段字符串放在域名前缀里发出去,绕过高延迟的HTTP通道。CTF题目里直接把这种思路改成了“把flag编码后藏在子域名里”。

分析方法第一步是过滤出所有DNS查询请求,也就是dns.qr == 0的包。然后在Wireshark里看dns.qname字段,如果你发现大量请求指向同一个根域名,但每个请求的子域名部分是一串无意义字符,基本就是中招了。这时候把子域名前缀全部拼接到一起,再根据字符集形态选择解码方式。

解码方式的选择是有门道的:如果拼接后的字符串全是十六进制字符(0-9和a-f),第一优先尝试hex解码;如果字符串长度是32的倍数、字符集包含大写字母A-Z和数字2-7,那就是base32,用Python的base64.b32decode直接解;如果是大小写混合的base64,要注意DNS解析会把域名统一转成小写,所以原大写字母会被破坏,这种情况下可以观察字符串是否包含+/等base64特征字符,如果包含就尝试手动补全大小写后再做base64解码。

我遇到的一道练习题比较常规:域名是example.com,前缀是几段等长的字符串,拼接后长度能被8整除,通过base32解码直接拿到了flag。还有一道进阶题把flag分成了多段放到了多次DNS请求里,顺序不是按时间排列的,还需要根据请求序号或者子域名里的序号字段重新排序,这个坑要在做题时特别注意。

2.3 技巧三:TCP流里手捡magic number提文件

有些题目不走HTTP,直接把一个文件拆成原始数据塞进TCP流里。这时候Wireshark的“Follow TCP Stream”虽然能看内容,但文件是二进制的话界面里全是乱码,得依赖脚本做自动提取。

我从实战中总结出来一个很实用的搜索思路:遍历pcap中所有TCP载荷,然后用Python的bytes.find()在整段拼接后的流数据里搜索各种文件头特征,也叫magic number。比如PK开头的是ZIP压缩包,\x89PNG\r\n\x1a\n是PNG图片,\xff\xd8\xff是JPEG,%PDF是PDF文档,7z\xbc\xaf\x27\x1c是7z压缩包。一旦命中,就把从这个位置开始的文件数据切出来保存。

但这个操作有个隐含的坑:TCP是流协议,同一个连接的数据包可能在传输过程中乱序,还可能重传。如果直接按pcap里的包顺序拼接数据,提取出来的文件大概率是坏的。正确做法是以四元组(源IP、源端口、目的IP、目的端口)为维度做流重组,并且按TCP序列号排序后再拼。脚本里我会用字典按流存数据、再按seq排序,这样提取出的文件才是完整的。

如果提取出来的是加密ZIP,就进入下一个常见的处理链路:先用zipfile测试是否能直接解压,如果不能,去pcap里搜口令——这里就引出后面的技巧五了。

2.4 技巧四:两张图做像素运算还原flag

流量里出现两张大小一致、肉眼看起来完全相同的图片,这种题目的flag通常就藏在“两张图的差别”里。常见的操作包括异或、相减、叠加、灰度通道拆分等。

原理其实很简单:如果出题人把一张干净的图片跟一张写有flag的图片做了异或处理,得到的结果就是一张充满噪点的“密图”。解题时把那两张原始图重新做异或,噪点会抵消,flag信息就浮现出来了。做题的人只需要把两张图逐像素取出RGB值,按位置做异或运算,再把结果写回一张新图片即可。

我自己写脚本的时候优先用Pillow库,通过Image.open()读取,getdata()拿到全部像素,再用生成器表达式做运算,最后putdata()写回。这里必须注意两点:一是图片尺寸必须一致,否则逐像素运算会报错,如果不一致可以先用resize()统一尺寸;二是通道数要一致,有些图是RGBA(四通道),直接跟RGB(三通道)做异或会出问题,需要先转成RGBA或者RGB格式。

还有一种变体题目是两张图中一张是原始图,另一张是附加了flag的噪点图,直接相减就能出结果。这时候可以把自己的运算函数参数化成op模式,灵活选择XOR、SUB、ADD三种运算。

2.5 技巧五:加密压缩包的口令藏在会话或其他载荷里

流量包题里提取出ZIP后往往发现它是加密的,这时候如果直接上爆破,效率低不说,成功率也不高。出题人不可能弄一个高强度密码让你硬刚,密码几乎一定藏在pcap的某个角落。

我常用的检索方式是:遍历所有包的payload,把可打印字符串提取出来,然后用正则搜索包含password、pass、key、口令、密码这些关键词的上下文。很多时候密码就在一段聊天流量里,比如IRC消息、HTTP POST参数、或者某人通过FTP传的txt文件里。也有变态一点的题目,密码被拆成几段分散在多个UDP包的载荷末尾,需要先拼出来再用。

判断加密方式是另一个关键细节:传统ZipCrypto加密的ZIP,标准库zipfile可以尝试用密码解压;但如果ZIP使用的是AES加密(特征是压缩包里有AES标志),zipfile不支持,需要改用pyzipper库。我第一次遇到AES加密ZIP时在zipfile上报RuntimeError: File is encrypted,换到pyzipper后用同样密码秒解。

还有一个实用信息:有些加密ZIP会有一个明文“提示”文件,比如注释里写了“密码是生日”,那就要关注流量里的日期信息;如果写了“密码是MAC地址”,那就去看ARP或DHCP包里的MAC信息。出题人不会无缘无故写提示,看到就直接去对应位置找。

3. 能直接跑的Python脚本集

这部分直接放代码。所有脚本我都拆成了独立函数,你完全可以只拷贝自己需要的部分,pcap路径改成自己的文件名就能跑。

3.1 pcap读取与流重组框架

读pcap最基础的就是rdpcap,但大文件容易内存爆掉,这时候建议用PcapReader做流式读取,一个包一个包地处理,既能控内存又能做流重组。下面这个函数会把整个pcap里的TCP载荷按四元组拆分、按序列号排序,最终返回每个TCP流的完整字节流。

from scapy.all import PcapReader, TCP, IP def reassemble_tcp_streams(pcap_path): streams = {} with PcapReader(pcap_path) as packets: for pkt in packets: if not (pkt.haslayer(TCP) and pkt[TCP].payload): continue key = ( pkt[IP].src, pkt[TCP].sport, pkt[IP].dst, pkt[TCP].dport ) streams.setdefault(key, []).append( (pkt[TCP].seq, bytes(pkt[TCP].payload)) ) for key, chunks in streams.items(): chunks.sort(key=lambda x: x[0]) yield key, b"".join(data for _, data in chunks)

这个函数是用生成器的形式写好的,遍历的时候直接for key, data in reassemble_tcp_streams("a.pcap"):就可以。排序时注意只按seq排,对于超大文件的乱序可能需要处理回绕,但CTF题目一般不会考到那么深。

3.2 从HTTP流自动提取文件

HTTP下的提取我建议不要自己写HTTP解析器,直接用Scapy的TCP载荷加简单的头部分割就够了。下面的脚本会识别HTTP响应体,把文件数据抠出来,并用HTTP头里的filename字段作为文件名,保存到指定目录。

import os, re from scapy.all import PcapReader, TCP, IP def extract_http_files(pcap_path, outdir="files"): os.makedirs(outdir, exist_ok=True) files = {} with PcapReader(pcap_path) as packets: for pkt in packets: if not (pkt.haslayer(TCP) and pkt[TCP].payload): continue payload = bytes(pkt[TCP].payload) # 只挑HTTP响应 if payload.startswith(b"HTTP/") or b"HTTP/1.1 200" in payload[:40]: headers, _, body = payload.partition(b"\r\n\r\n") if not body: continue fname_match = re.search( rb'filename="?([^";]+)"?', headers ) if fname_match: fname = fname_match.group(1).decode(errors="ignore") files[fname] = files.get(fname, b"") + body elif b"Content-Type: image" in headers: fname = f"img_{len(files)}.dat" files[fname] = files.get(fname, b"") + body for name, data in files.items(): with open(os.path.join(outdir, name), "wb") as f: f.write(data) return list(files.keys())

这个脚本的局限是:它假设一个包就是完整的HTTP响应体,如果文件跨多个TCP包,就要结合上面的流重组来做。稳妥一点是把所有HTTP响应流先重组再切分,我一般习惯直接先跑流重组,再对每个流用相同逻辑提取,这样对大文件更保险。

3.3 DNS可疑字符串解码器

DNS解码脚本的核心只有两步:收集所有查询域名,然后剥掉根域名后缀。很多题目的根域名是固定的,比如example.com,所以我会把收集到的域名先按最后的两个标签去掉,剩下前面的标签拼接成一个字符串,再做hex、base32、base64三种尝试。

import base64, binascii from scapy.all import PcapReader, DNS def collect_dns_queries(pcap_path, root_domain="example.com"): labels = [] with PcapReader(pcap_path) as packets: for pkt in packets: if pkt.haslayer(DNS) and pkt[DNS].qr == 0: qname = pkt[DNS].qd.qname.decode().rstrip(".") domain = "." + root_domain if qname.endswith(domain): prefix = qname[: -len(domain)].replace(".", "") labels.append(prefix) return "".join(labels) def try_decode(s): # 1. hex try: return f"hex -> {bytes.fromhex(s).decode(errors='ignore')}" except ValueError: pass # 2. base32 try: return f"base32 -> {base64.b32decode(s.upper()).decode(errors='ignore')}" except Exception: pass # 3. base64 try: return f"base64 -> {base64.b64decode(s + '=' * (-len(s) % 4)).decode(errors='ignore')}" except Exception: pass return "no result" s = collect_dns_queries("dns.pcap") print(s) print(try_decode(s))

如果DNS查询里有多个不同根域名,就把root_domain参数改成一个不含的域名,或者直接把整段域名拼接起来再试着解码。有一个容易被忽略的点:DNS请求可能重复出现,重试机制会产生相同的查询,所以收集时用set去重最好。

3.4 从TCP流提取magic bytes文件

这个脚本重点展示如何搜索文件头。我定义了一组常见格式的magic bytes,遍历之前生成的流数据,用bytes.find定位文件头,然后把文件头开始处的数据全部保存。如果文件长度未知,就先把流尾部到文件头之间的数据都截出来,再根据流结束位置判断文件是否完整。

MAGIC = { "zip": b"PK\x03\x04", "zip_spanned": b"PK\x07\x08", "png": b"\x89PNG\r\n\x1a\n", "jpg": b"\xff\xd8\xff", "pdf": b"%PDF", "gz": b"\x1f\x8b", "7z": b"7z\xbc\xaf\x27\x1c", "rar": b"Rar!\x1a\x07", } def extract_by_magic(data, outdir="extracted"): import os os.makedirs(outdir, exist_ok=True) result = [] for name, magic in MAGIC.items(): pos = data.find(magic) if pos != -1: chunk = data[pos:] path = os.path.join(outdir, f"stream_{name}_{len(result)}.bin") with open(path, "wb") as f: f.write(chunk) result.append((name, path)) return result # 配合流重组函数使用 for key, stream_data in reassemble_tcp_streams("capture.pcap"): found = extract_by_magic(stream_data) for name, path in found: print(key, name, path)

搜索出来的文件直接用file命令判断是否完整,如果报“ truncated”就说明切割长度不对,需要手动调整。有些图片文件后面还会跟着别的数据,所以切的时候只取到magic number开始的区域即可,后续内容可以在文件层面继续分析。

3.5 像素运算还原图片

最后这个函数用来做像素级运算。传入两张图片路径和一个运算模式,返回运算结果图片。注意它会把两张图转换成同一个尺寸和模式再逐像素处理。

from PIL import Image def pixel_merge(img1_path, img2_path, op="xor", out_path="merged.png"): im1 = Image.open(img1_path).convert("RGB") im2 = Image.open(img2_path).convert("RGB") if im1.size != im2.size: im2 = im2.resize(im1.size) p1 = im1.load() p2 = im2.load() out = Image.new("RGB", im1.size) po = out.load() for y in range(im1.size[1]): for x in range(im1.size[0]): r1, g1, b1 = p1[x, y] r2, g2, b2 = p2[x, y] if op == "xor": po[x, y] = (r1 ^ r2, g1 ^ g2, b1 ^ b2) elif op == "sub": po[x, y] = (abs(r1 - r2), abs(g1 - g2), abs(b1 - b2)) elif op == "add": po[x, y] = (min(r1 + r2, 255), min(g1 + g2, 255), min(b1 + b2, 255)) else: raise ValueError("unknown op") out.save(out_path)

跑起来可能有点慢,图片如果太大的话可以先把尺寸压小一点,或者用NumPy向量化运算加速。对于像素级隐写,图片分辨率一般不会太高,纯Python循环也够用。

4. 常见问题与排查窍门实录

流量包分析翻车最多的不是思路不对,而是细节没处理好。我把刷题和带人过程中遇到的高频问题列出来,按“现象、原因、解法”的方式整理成速查表,方便你卡壳时快速对照。

4.1 我会遇到的5个高频坑

第一个坑是DNS域名大小写问题。DNS协议在传输过程中域名会被统一转成小写,如果出题人用base64编码flag,大小写字母一混合,解码必挂。我现在的做法是优先试hex和base32,因为这两种编码不依赖大小写混合,能规避大部分协议干扰。

第二个坑是TCP乱序。直接用pcap原始顺序拼文件经常会拼出损坏文件。不要偷懒,一定要按序列号排序重组。我之前在某个数据包中直接提取ZIP,解压时报BadZipFile,最后发现是流重组没按seq排序,片段倒序了。这个坑很多人都会踩。

第三个坑是Scapy层解析问题。有时候pcap文件不是标准的以太网帧,比如某些采集环境是Linux cooked capture(SLL)封装,这时候包没有Ether层,直接pkt[IP]会报错。我的建议是写解析脚本时先判断pkt.haslayer(IP),再做IP层访问,不要假设一定有Ethernet。

第四个坑是加密ZIP的AES误判。zipfile解不开加密包时,不要直接认定密码错误。先看压缩包里的加密算法,如果提示AE-x这类参数说明是AES加密,必须用pyzipper或7-Zip命令行。我当时在某道题卡了半个多小时,后来发现根本原因是用错了解压库。

第五个坑是大pcap内存溢出。rdpcap把整个pcap读入内存,如果文件快1GB,机器就会卡死。优先用PcapReader迭代读取,这样每条包只是临时占用内存。对刷题来说基本不会爆,但处理真实流量样本时必须养成这个习惯。

4.2 排查思路速查表

现象可能原因解决思路
提取的文件打开后报损坏TCP流未按序列号重组按四元组分组后按seq排序,再拼接
base64解码出来是乱码DNS域名被转小写先尝试hex、base32,或手动恢复大小写
zipfile解压报File is encryptedZipCrypto或AES加密用pyzipper或7z命令行,确认密码
两张图片像素运算报错尺寸或通道数不一致先resize、统一convert模式
Scapy访问IP层报错pcap基于SLL封装用pkt.haslayer(IP)做保护或加载SLL层
图片用StegSolve打开没东西隐写不在可见通道用zsteg自动扫描LSB,检查图片尾部数据
找不到任何文件头数据被编码后再传输先尝试对整个TCP流做base64/hex解码再搜索
DNS子域名拼接后无法解码请求顺序错乱或重复用请求序号排序,或者用set去重

这个速查表是我实际使用频率最高的复盘清单,刷题遇到卡壳时先查一遍,比自己瞎试快得多。

我在做流量包题目时,最深的体会是:耐心比技术重要,脚本比肉眼可靠。很多隐藏信息用Wireshark手动翻也能看到,但一个大pcap里可能混着几千个包,手翻既慢又容易漏,脚本跑一遍就不会漏。所以我现在处理流量包的基本套路都是:先Wireshark快速定位可疑协议,再用Python脚本做批量提取和重组,最后对提取出的文件做隐写检查。这套流程帮我解决了不少难题,也希望这篇内容能给你省下一些踩坑的时间。最后分享一个小经验:刷流量分析题一定要养成积累脚本的习惯,把HTTP提取、DNS解码、文件头搜索这些基础函数保存好,下次套用就会很轻松。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进