)
前陣子帶著一批入門MISC的選手刷題我發(fā)現(xiàn)一個特別有意思的現(xiàn)象流量包分析在很多人眼里是最“勸退”的題型——一個幾十MB的pcap文件扔過來Wireshark打開滿屏都是包根本不知道從哪起手。但刷到后面大家都會認同一個結論流量包題目難不在技術而在“信息定位”。出題人把flag藏來藏去無非是那么幾個地方你只要掌握對應的提取思路再配合Python腳本去做重復勞動效率和正確率都能上來一大截。這篇文章我把最近實戰(zhàn)中總結的5種隱藏技巧完整拆開來講從HTTP響應里的圖片尾巴到DNS請求里的一串可疑字符再到TCP流中手撿文件頭每一個都會給出可以直接復用的Python腳本。適合正在準備CTF比賽、想進階MISC方向的選手也適合做流量分析但想提升效率的安全工程師。文章里所有腳本都是我在實際刷題環(huán)境里剪出來改過的能跑、能改照著抄基本就能應對大半流量包題目。1. 先摸清流量包里藏信息的三種路子流量分析題能玩的點其實繞不開三類信息載體。先把這三類搞清楚做題的時候就有了地圖看到一個pcap不會兩眼一抹黑。1.1 文件對象、協(xié)議元數(shù)據(jù)、載荷暗語這三類信息載體第一類是文件對象。HTTP協(xié)議在傳輸過程中最常見的載荷就是文件——圖片、壓縮包、PDF、甚至直接就是一段exe。這些文件通過HTTP響應體傳過來肉眼在Wireshark里看不到完整結構但用工具提取出來后可以做文件層面的隱寫分析。很多題目的flag不是直接寫在包里的而是藏在某個被傳輸?shù)膱D片的LSB位里或者附加在文件尾部。所以看到HTTP上傳下載的痕跡第一反應就是“把文件撈出來”。第二類是協(xié)議元數(shù)據(jù)。不需要看載荷內(nèi)容光看協(xié)議字段本身就能藏信息。最典型的就是DNS查詢里的子域名出題人把編碼后的flag拆成一段段子域名字符串塞進DNS請求里還有HTTP的頭部字段、TCP的序列號、TLS握手的SNI字段都有被改寫的可能。這類信息的特征是“藏得淺但隱蔽”因為一般分析者懶得逐條看協(xié)議字段。第三類是載荷暗語。TCP或UDP的payload可能是一整段經(jīng)過編碼的字符串比如base64、十六進制或者直接就是一段加密后的密文需要先解碼、再解密、再分析。有些題目甚至直接把壓縮包拆成多個切片分散在多個包里需要按序拼回去。這一類最考驗解碼和恢復能力也是腳本發(fā)揮最大價值的地方。1.2 為什么用 Python Scapy而不是純 WiresharkWireshark當然適合做人工邏輯分析它的圖形界面、過濾器、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、PillowPIL、pyzipper這三個庫就夠了后面腳本都基于它們。解析大部分pcap和圖片隱寫操作都不用額外再裝什么重型依賴。2. 從流量包摳flag的5種隱藏技巧這五種技巧是我從一類類題目里篩出來的基本是流量包MISC題里出現(xiàn)頻率最高的五種藏法。每一種我都會講清楚“藏在哪里”、“怎么發(fā)現(xiàn)”、“怎么提取”有基礎的朋友可以直接跳到對應小節(jié)看腳本思路。2.1 技巧一HTTP響應里的文件先提取再檢查圖片尾巴和LSB這種題目的pcap里會有一個明顯的HTTP GET請求請求某個圖片或者壓縮包響應體里就是這個文件。難點從來不在“拿到文件”而在于“拿到文件之后做什么”。我的標準流程是這樣的先用tshark把HTTP響應里的文件數(shù)據(jù)導出來或者用Python腳本遍歷HTTP層抓http.file_data字段。拿到文件后先用file命令看真實類型用binwalk掃一遍有沒有附加數(shù)據(jù)再用strings拽一遍看有沒有明文字符串。如果這三步都沒有結果就要對圖片本身做隱寫分析檢查尾部是否被追加了數(shù)據(jù)處理LSB隱寫留意EXIF信息或PNG的tEXt注釋塊。我實際遇到過一道題pcap里傳輸了一張PNG表面上看是一張普通的二維碼。但binwalk顯示文件尾部有額外數(shù)據(jù)把尾部的一段十六進制轉ASCII后直接就是flag{...}。這種屬于出題人給你送的溫暖。更常見的是尾部沒有額外數(shù)據(jù)但圖片用StegSolve打開后在Red plane 0、Green plane 0通道能看到一行若隱若現(xiàn)的字符這就是LSB隱寫。處理這類情況建議優(yōu)先用zsteg工具一鍵檢測比手動提取RGB值快得多。這個技巧的核心經(jīng)驗是不要在拿到文件的那一刻就以為題做完了文件本身就是分析對象。2.2 技巧二DNS查詢子域名里的變形字符串DNS請求里藏數(shù)據(jù)在真實攻擊中也存在攻擊者會把竊取的數(shù)據(jù)拆成一段段字符串放在域名前綴里發(fā)出去繞過高延遲的HTTP通道。CTF題目里直接把這種思路改成了“把flag編碼后藏在子域名里”。分析方法第一步是過濾出所有DNS查詢請求也就是dns.qr 0的包。然后在Wireshark里看dns.qname字段如果你發(fā)現(xiàn)大量請求指向同一個根域名但每個請求的子域名部分是一串無意義字符基本就是中招了。這時候把子域名前綴全部拼接到一起再根據(jù)字符集形態(tài)選擇解碼方式。解碼方式的選擇是有門道的如果拼接后的字符串全是十六進制字符0-9和a-f第一優(yōu)先嘗試hex解碼如果字符串長度是32的倍數(shù)、字符集包含大寫字母A-Z和數(shù)字2-7那就是base32用Python的base64.b32decode直接解如果是大小寫混合的base64要注意DNS解析會把域名統(tǒng)一轉成小寫所以原大寫字母會被破壞這種情況下可以觀察字符串是否包含/等base64特征字符如果包含就嘗試手動補全大小寫后再做base64解碼。我遇到的一道練習題比較常規(guī)域名是example.com前綴是幾段等長的字符串拼接后長度能被8整除通過base32解碼直接拿到了flag。還有一道進階題把flag分成了多段放到了多次DNS請求里順序不是按時間排列的還需要根據(jù)請求序號或者子域名里的序號字段重新排序這個坑要在做題時特別注意。2.3 技巧三TCP流里手撿magic number提文件有些題目不走HTTP直接把一個文件拆成原始數(shù)據(jù)塞進TCP流里。這時候Wireshark的“Follow TCP Stream”雖然能看內(nèi)容但文件是二進制的話界面里全是亂碼得依賴腳本做自動提取。我從實戰(zhàn)中總結出來一個很實用的搜索思路遍歷pcap中所有TCP載荷然后用Python的bytes.find()在整段拼接后的流數(shù)據(jù)里搜索各種文件頭特征也叫magic number。比如PK開頭的是ZIP壓縮包\x89PNG\r\n\x1a\n是PNG圖片\xff\xd8\xff是JPEG%PDF是PDF文檔7z\xbc\xaf\x27\x1c是7z壓縮包。一旦命中就把從這個位置開始的文件數(shù)據(jù)切出來保存。但這個操作有個隱含的坑TCP是流協(xié)議同一個連接的數(shù)據(jù)包可能在傳輸過程中亂序還可能重傳。如果直接按pcap里的包順序拼接數(shù)據(jù)提取出來的文件大概率是壞的。正確做法是以四元組源IP、源端口、目的IP、目的端口為維度做流重組并且按TCP序列號排序后再拼。腳本里我會用字典按流存數(shù)據(jù)、再按seq排序這樣提取出的文件才是完整的。如果提取出來的是加密ZIP就進入下一個常見的處理鏈路先用zipfile測試是否能直接解壓如果不能去pcap里搜口令——這里就引出后面的技巧五了。2.4 技巧四兩張圖做像素運算還原flag流量里出現(xiàn)兩張大小一致、肉眼看起來完全相同的圖片這種題目的flag通常就藏在“兩張圖的差別”里。常見的操作包括異或、相減、疊加、灰度通道拆分等。原理其實很簡單如果出題人把一張干凈的圖片跟一張寫有flag的圖片做了異或處理得到的結果就是一張充滿噪點的“密圖”。解題時把那兩張原始圖重新做異或噪點會抵消flag信息就浮現(xiàn)出來了。做題的人只需要把兩張圖逐像素取出RGB值按位置做異或運算再把結果寫回一張新圖片即可。我自己寫腳本的時候優(yōu)先用Pillow庫通過Image.open()讀取getdata()拿到全部像素再用生成器表達式做運算最后putdata()寫回。這里必須注意兩點一是圖片尺寸必須一致否則逐像素運算會報錯如果不一致可以先用resize()統(tǒng)一尺寸二是通道數(shù)要一致有些圖是RGBA四通道直接跟RGB三通道做異或會出問題需要先轉成RGBA或者RGB格式。還有一種變體題目是兩張圖中一張是原始圖另一張是附加了flag的噪點圖直接相減就能出結果。這時候可以把自己的運算函數(shù)參數(shù)化成op模式靈活選擇XOR、SUB、ADD三種運算。2.5 技巧五加密壓縮包的口令藏在會話或其他載荷里流量包題里提取出ZIP后往往發(fā)現(xiàn)它是加密的這時候如果直接上爆破效率低不說成功率也不高。出題人不可能弄一個高強度密碼讓你硬剛密碼幾乎一定藏在pcap的某個角落。我常用的檢索方式是遍歷所有包的payload把可打印字符串提取出來然后用正則搜索包含password、pass、key、口令、密碼這些關鍵詞的上下文。很多時候密碼就在一段聊天流量里比如IRC消息、HTTP POST參數(shù)、或者某人通過FTP傳的txt文件里。也有變態(tài)一點的題目密碼被拆成幾段分散在多個UDP包的載荷末尾需要先拼出來再用。判斷加密方式是另一個關鍵細節(jié)傳統(tǒng)ZipCrypto加密的ZIP標準庫zipfile可以嘗試用密碼解壓但如果ZIP使用的是AES加密特征是壓縮包里有AES標志zipfile不支持需要改用pyzipper庫。我第一次遇到AES加密ZIP時在zipfile上報RuntimeError: File is encrypted換到pyzipper后用同樣密碼秒解。還有一個實用信息有些加密ZIP會有一個明文“提示”文件比如注釋里寫了“密碼是生日”那就要關注流量里的日期信息如果寫了“密碼是MAC地址”那就去看ARP或DHCP包里的MAC信息。出題人不會無緣無故寫提示看到就直接去對應位置找。3. 能直接跑的Python腳本集這部分直接放代碼。所有腳本我都拆成了獨立函數(shù)你完全可以只拷貝自己需要的部分pcap路徑改成自己的文件名就能跑。3.1 pcap讀取與流重組框架讀pcap最基礎的就是rdpcap但大文件容易內(nèi)存爆掉這時候建議用PcapReader做流式讀取一個包一個包地處理既能控內(nèi)存又能做流重組。下面這個函數(shù)會把整個pcap里的TCP載荷按四元組拆分、按序列號排序最終返回每個TCP流的完整字節(jié)流。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(keylambda x: x[0]) yield key, b.join(data for _, data in chunks)這個函數(shù)是用生成器的形式寫好的遍歷的時候直接for key, data in reassemble_tcp_streams(a.pcap):就可以。排序時注意只按seq排對于超大文件的亂序可能需要處理回繞但CTF題目一般不會考到那么深。3.2 從HTTP流自動提取文件HTTP下的提取我建議不要自己寫HTTP解析器直接用Scapy的TCP載荷加簡單的頭部分割就夠了。下面的腳本會識別HTTP響應體把文件數(shù)據(jù)摳出來并用HTTP頭里的filename字段作為文件名保存到指定目錄。import os, re from scapy.all import PcapReader, TCP, IP def extract_http_files(pcap_path, outdirfiles): os.makedirs(outdir, exist_okTrue) 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(bHTTP/) or bHTTP/1.1 200 in payload[:40]: headers, _, body payload.partition(b\r\n\r\n) if not body: continue fname_match re.search( rbfilename?([^;])?, headers ) if fname_match: fname fname_match.group(1).decode(errorsignore) files[fname] files.get(fname, b) body elif bContent-Type: image in headers: fname fimg_{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包就要結合上面的流重組來做。穩(wěn)妥一點是把所有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_domainexample.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 fhex - {bytes.fromhex(s).decode(errorsignore)} except ValueError: pass # 2. base32 try: return fbase32 - {base64.b32decode(s.upper()).decode(errorsignore)} except Exception: pass # 3. base64 try: return fbase64 - {base64.b64decode(s * (-len(s) % 4)).decode(errorsignore)} except Exception: pass return no result s collect_dns_queries(dns.pcap) print(s) print(try_decode(s))如果DNS查詢里有多個不同根域名就把root_domain參數(shù)改成一個不含的域名或者直接把整段域名拼接起來再試著解碼。有一個容易被忽略的點DNS請求可能重復出現(xiàn)重試機制會產(chǎn)生相同的查詢所以收集時用set去重最好。3.4 從TCP流提取magic bytes文件這個腳本重點展示如何搜索文件頭。我定義了一組常見格式的magic bytes遍歷之前生成的流數(shù)據(jù)用bytes.find定位文件頭然后把文件頭開始處的數(shù)據(jù)全部保存。如果文件長度未知就先把流尾部到文件頭之間的數(shù)據(jù)都截出來再根據(jù)流結束位置判斷文件是否完整。MAGIC { zip: bPK\x03\x04, zip_spanned: bPK\x07\x08, png: b\x89PNG\r\n\x1a\n, jpg: b\xff\xd8\xff, pdf: b%PDF, gz: b\x1f\x8b, 7z: b7z\xbc\xaf\x27\x1c, rar: bRar!\x1a\x07, } def extract_by_magic(data, outdirextracted): import os os.makedirs(outdir, exist_okTrue) result [] for name, magic in MAGIC.items(): pos data.find(magic) if pos ! -1: chunk data[pos:] path os.path.join(outdir, fstream_{name}_{len(result)}.bin) with open(path, wb) as f: f.write(chunk) result.append((name, path)) return result # 配合流重組函數(shù)使用 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”就說明切割長度不對需要手動調(diào)整。有些圖片文件后面還會跟著別的數(shù)據(jù)所以切的時候只取到magic number開始的區(qū)域即可后續(xù)內(nèi)容可以在文件層面繼續(xù)分析。3.5 像素運算還原圖片最后這個函數(shù)用來做像素級運算。傳入兩張圖片路徑和一個運算模式返回運算結果圖片。注意它會把兩張圖轉換成同一個尺寸和模式再逐像素處理。from PIL import Image def pixel_merge(img1_path, img2_path, opxor, out_pathmerged.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循環(huán)也夠用。4. 常見問題與排查竅門實錄流量包分析翻車最多的不是思路不對而是細節(jié)沒處理好。我把刷題和帶人過程中遇到的高頻問題列出來按“現(xiàn)象、原因、解法”的方式整理成速查表方便你卡殼時快速對照。4.1 我會遇到的5個高頻坑第一個坑是DNS域名大小寫問題。DNS協(xié)議在傳輸過程中域名會被統(tǒng)一轉成小寫如果出題人用base64編碼flag大小寫字母一混合解碼必掛。我現(xiàn)在的做法是優(yōu)先試hex和base32因為這兩種編碼不依賴大小寫混合能規(guī)避大部分協(xié)議干擾。第二個坑是TCP亂序。直接用pcap原始順序拼文件經(jīng)常會拼出損壞文件。不要偷懶一定要按序列號排序重組。我之前在某個數(shù)據(jù)包中直接提取ZIP解壓時報BadZipFile最后發(fā)現(xiàn)是流重組沒按seq排序片段倒序了。這個坑很多人都會踩。第三個坑是Scapy層解析問題。有時候pcap文件不是標準的以太網(wǎng)幀比如某些采集環(huán)境是Linux cooked captureSLL封裝這時候包沒有Ether層直接pkt[IP]會報錯。我的建議是寫解析腳本時先判斷pkt.haslayer(IP)再做IP層訪問不要假設一定有Ethernet。第四個坑是加密ZIP的AES誤判。zipfile解不開加密包時不要直接認定密碼錯誤。先看壓縮包里的加密算法如果提示AE-x這類參數(shù)說明是AES加密必須用pyzipper或7-Zip命令行。我當時在某道題卡了半個多小時后來發(fā)現(xiàn)根本原因是用錯了解壓庫。第五個坑是大pcap內(nèi)存溢出。rdpcap把整個pcap讀入內(nèi)存如果文件快1GB機器就會卡死。優(yōu)先用PcapReader迭代讀取這樣每條包只是臨時占用內(nèi)存。對刷題來說基本不會爆但處理真實流量樣本時必須養(yǎng)成這個習慣。4.2 排查思路速查表現(xiàn)象可能原因解決思路提取的文件打開后報損壞TCP流未按序列號重組按四元組分組后按seq排序再拼接base64解碼出來是亂碼DNS域名被轉小寫先嘗試hex、base32或手動恢復大小寫zipfile解壓報File is encryptedZipCrypto或AES加密用pyzipper或7z命令行確認密碼兩張圖片像素運算報錯尺寸或通道數(shù)不一致先resize、統(tǒng)一convert模式Scapy訪問IP層報錯pcap基于SLL封裝用pkt.haslayer(IP)做保護或加載SLL層圖片用StegSolve打開沒東西隱寫不在可見通道用zsteg自動掃描LSB檢查圖片尾部數(shù)據(jù)找不到任何文件頭數(shù)據(jù)被編碼后再傳輸先嘗試對整個TCP流做base64/hex解碼再搜索DNS子域名拼接后無法解碼請求順序錯亂或重復用請求序號排序或者用set去重這個速查表是我實際使用頻率最高的復盤清單刷題遇到卡殼時先查一遍比自己瞎試快得多。我在做流量包題目時最深的體會是耐心比技術重要腳本比肉眼可靠。很多隱藏信息用Wireshark手動翻也能看到但一個大pcap里可能混著幾千個包手翻既慢又容易漏腳本跑一遍就不會漏。所以我現(xiàn)在處理流量包的基本套路都是先Wireshark快速定位可疑協(xié)議再用Python腳本做批量提取和重組最后對提取出的文件做隱寫檢查。這套流程幫我解決了不少難題也希望這篇內(nèi)容能給你省下一些踩坑的時間。最后分享一個小經(jīng)驗刷流量分析題一定要養(yǎng)成積累腳本的習慣把HTTP提取、DNS解碼、文件頭搜索這些基礎函數(shù)保存好下次套用就會很輕松。