From 9278b8c3cd95c931131d057946db144268ec156a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=8F=84=EB=A1=9C=EB=A1=B1?= Date: Fri, 24 Oct 2025 11:45:58 +0900 Subject: [PATCH] asdf --- src/content/blog/01-whpre-writeup/index.md | 336 ++++++++++++++++++++- src/content/blog/01-whpre-writeup/temp.txt | 215 +++++++++++++ 2 files changed, 548 insertions(+), 3 deletions(-) create mode 100644 src/content/blog/01-whpre-writeup/temp.txt diff --git a/src/content/blog/01-whpre-writeup/index.md b/src/content/blog/01-whpre-writeup/index.md index 725628f..5850d36 100644 --- a/src/content/blog/01-whpre-writeup/index.md +++ b/src/content/blog/01-whpre-writeup/index.md @@ -1,7 +1,337 @@ --- -title: "whitehatcontest 2025 pre writeup(forensics/ai)" +title: "화이트햇콘테스트 2025 pre writeup(forensics/ai)" description: "writing" -date: "Oct 23 2025 " +date: "Oct 23 2025" --- -시간나면작성 +## 1. Introduction +본문서는 2025 WHITEHAT 예선에 참가한 팀 헤일메리의 문제별 write-up으로, **AI Sommelier**와 **Leakage Investigation** 두 문제를 다룬다. (다른 문제는 미작성) + +## 2. AI Sommelier + +### 2.1 Overview +문제 서버는 LLaMA 3.2가 생성한 문장 10개와 Gemma 3가 생성한 문장 10개, 총 20개의 텍스트를 무작위로 제공한다. 참가자는 각 텍스트의 생성 모델을 판별해 제출하면 플래그를 획득한다. + +기본 가정은 언어 모델이 스스로 생성했을 법한 텍스트에 대해 상대적으로 더 높은 로그우도(log-likelihood)를 부여한다는 것이다. 후보 모델을 직접 구동하고 입력 텍스트의 `logprobs`를 관측하면 모델 기원 분류(model attribution)가 가능하다. + +### 2.2 Methodology +입력 $x = (t_1, \ldots, t_n)$ 에 대해 모델 $M$ 의 로그우도 합은 $\log p_M(x) = \sum_{i=1}^{n} \log p_M(t_i \mid t_{:19999") +VLLM_BASE = os.getenv("VLLM_BASE_URL", "http://127.0.0.1:7001") +LLAMA, GEMMA, TOP_K = "llama3.2", "gemma3", 10 + + +def wait_llama(): + for _ in range(240): + r = requests.get(f"{VLLM_BASE}/v1/models", timeout=5) + models = [m.get("id") or m.get("name") for m in r.json().get("data", [])] + if LLAMA in models: + return + time.sleep(0.5) + raise RuntimeError("llama3.2 not ready") + + +def fetch_challenge(): + r = requests.get(f"{CHALLENGE_BASE}/challenge/new", timeout=30) + r.raise_for_status() + js = r.json() + return js["id"], js["texts"] + + +def submit_answers(cid, answers): + r = requests.post( + f"{CHALLENGE_BASE}/challenge/{cid}", + json={"answers": answers}, + timeout=60, + ) + r.raise_for_status() + return r.json() + + +def score_mean_bpb(text): + payload = { + "model": LLAMA, + "messages": [{"role": "user", "content": text}], + "temperature": 0, + "top_p": 1, + "max_tokens": 0, + "echo": True, + "logprobs": True, + "top_logprobs": 0, + } + try: + r = requests.post( + f"{VLLM_BASE}/v1/chat/completions", json=payload, timeout=120 + ) + r.raise_for_status() + items = r.json()["choices"][0]["logprobs"]["content"] + lps = [it["logprob"] for it in items if it.get("logprob") is not None] + except Exception: + alt = { + "model": LLAMA, + "prompt": text, + "temperature": 0, + "top_p": 1, + "max_tokens": 0, + "echo": True, + "logprobs": True, + } + r = requests.post( + f"{VLLM_BASE}/v1/completions", json=alt, timeout=120 + ) + r.raise_for_status() + lps = [ + lp + for lp in r.json()["choices"][0]["logprobs"].get("token_logprobs", []) + if lp is not None + ] + if not lps: + lps = [-999.0] + + s = sum(lps) + mean_lp = s / max(1, len(lps)) + bpb = (-s / math.log(2)) / max(1, len(text.encode("utf-8"))) + return mean_lp, bpb + + +def main(): + wait_llama() + cid, texts = fetch_challenge() + with open(f"./challenge_{cid}.json", "w", encoding="utf-8") as f: + json.dump({"id": cid, "texts": texts}, f, ensure_ascii=False, indent=2) + + scores = [] + for i, text in enumerate(texts, 1): + mean_lp, bpb = score_mean_bpb(text) + scores.append((i - 1, mean_lp, bpb)) + print(f"[llama] #{i}: mean_lp={mean_lp:.4f}, bpb={bpb:.5f}") + time.sleep(0.08) + + order = sorted( + range(len(texts)), + key=lambda idx: (scores[idx][1], -scores[idx][2]), + reverse=True, + ) + llama_idxs = set(order[:TOP_K]) + answers = [LLAMA if i in llama_idxs else GEMMA for i in range(len(texts))] + + print("\n[*] submit:", answers) + print("[=] server:", submit_answers(cid, answers)) + + +if __name__ == "__main__": + main() +``` + +### 2.4 Results +단일 모델(LLaMA 3.2)만으로도 상위 10개 선별 전략이 안정적으로 동작했고, 서버 채점 결과 정확 판정을 받아 플래그를 획득했다. + +### 2.5 Discussion +정석 접근은 두 후보 모델을 모두 서빙하여 표본별 `logprobs`를 직접 비교하는 것이다. 그럼에도 LLaMA 3.2 단일 모델의 로그우도만으로도 충분한 신호를 확보할 수 있었다. + +### 2.6 Runtime Evidence +다음은 실행 로그에서 핵심 부분만 발췌한 결과다(대표 샘플 + 최종 제출/응답). + +```text +id=bfd5641a-ed41-4d0f-8d00-0b51e757cd13, samples=20, schema=for_user +[llama3.2] #1: mean_logprob: -0.6946, bpb: 0.1883, num_tokens: 344 +[llama3.2] #2: mean_logprob: -2.1178, bpb: 0.6121, num_tokens: 342 +... +[llama3.2] #19: mean_logprob: -0.8869, bpb: 0.2508, num_tokens: 318 +[*] submit: ['llama3.2', 'gemma3', 'llama3.2', 'gemma3', 'gemma3', 'llama3.2', 'gemma3', 'gemma3', 'gemma3', 'gemma3', 'llama3.2', 'llama3.2', 'llama3.2', 'llama3.2', 'gemma3', 'llama3.2', 'gemma3', 'gemma3', 'gemma3', 'gemma3'] +[=] server: {'result': 'correct whitehat2025{fee40ba0cde992326f520632f07e5a75}'} +``` + +## 3. Leakage Investigation + +### 3.1 Overview +"LumenGrid Labs" 내부에서 신제품 관련 기밀 정보가 외부로 유출되었다. 의심 직원의 네트워크 트래픽 덤프(`prob.pcap`)를 분석해 실제 유출된 정보(제품명과 출시일)를 복원하는 것이 목표다. + +Flag Format: `whitehat2025{ProductName_ReleaseDate}` + +제공된 자료는 다음과 같다. + +- `prob.pcap`: 네트워크 패킷 캡처 (약 131 MB, 116,822 packets) +- `passwd`: 시스템 계정 정보 파일 (Linux 표준 형식) + +### 3.2 Methodology + +1. **패킷 구조 분석** + Scapy로 전체 패킷 통계를 확인한 결과 대부분의 트래픽은 HTTPS(구글, 마이크로소프트 업데이트 등)로 암호화되어 있었다. 그러나 내부 IP `192.168.110.128`과 외부 IP `198.51.100.23` 사이에서 평문 통신이 지속적으로 관측됐다. + + ```python + from collections import Counter + + from scapy.all import IP, TCP, rdpcap + + pkts = rdpcap("prob.pcap") + print(f"Total packets: {len(pkts)}") + + tcp_ports = Counter() + for p in pkts: + if p.haslayer(TCP): + tcp_ports[p[TCP].sport] += 1 + tcp_ports[p[TCP].dport] += 1 + + print("Top TCP ports:", tcp_ports.most_common(10)) + ``` + + 통계 결과 `198.51.100.23:60000 <-> 192.168.110.128:54321` 연결이 일반 서비스 포트와 달라 주요 분석 대상으로 지정했다. + +2. **평문 문자열 검색** + `strings`로 pcap 파일 내 평문을 추출해 비정상 트래픽의 의미를 파악했다. + + ```bash + strings -n 10 prob.pcap > all_strings.txt + ``` + + 여기에서 "From now on, let's hide our messages inside images." 문장을 발견해 공격자가 이후 스테가노그래피(steganography) 기법을 사용할 것임을 추정했다. + +3. **TCP 스트림 재구성** + 해당 문장을 포함하는 패킷을 기준으로 양단 IP/포트를 식별하고, 동일한 TCP 세션의 페이로드를 시간 순으로 병합해 전체 대화를 복원했다. + + ```python + from scapy.all import IP, Raw, TCP, rdpcap + + pkts = rdpcap("prob.pcap") + needle = b"From now on, let's hide our messages inside images." + + for packet in pkts: + if packet.haslayer(Raw) and needle in bytes(packet[Raw].load): + src, dst = packet[IP].src, packet[IP].dst + sport, dport = packet[TCP].sport, packet[TCP].dport + + stream = [] + for q in pkts: + if q.haslayer(IP) and q.haslayer(TCP): + cond1 = (q[IP].src, q[IP].dst, q[TCP].sport, q[TCP].dport) == ( + src, + dst, + sport, + dport, + ) + cond2 = (q[IP].src, q[IP].dst, q[TCP].sport, q[TCP].dport) == ( + dst, + src, + dport, + sport, + ) + if cond1 or cond2: + stream.append(q) + + data = b"".join( + bytes(q[Raw].load) + for q in sorted(stream, key=lambda x: x.time) + if q.haslayer(Raw) + ) + conversation = data.decode("utf-8", errors="ignore") + print(conversation[:2000]) + break + ``` + + 복원된 대화에는 다음과 같은 지시 사항이 포함되어 있었다. + + ```text + [198.51.100.23] From now on, let's hide our messages inside images. + [192.168.110.128] Inside images? How would anyone know to look for them? + [198.51.100.23] I'll put a four-digit length at the front. Just read up to that. + [192.168.110.128] Got it. How can I notice that an image has a message? + [198.51.100.23] I'll invert the red channel so you can notice. + ``` + + 이를 통해 메시지가 이미지 내부에 숨겨지며, 4자리 길이 프리픽스와 Red 채널 반전이 시그널이라는 점을 확인했다. + +4. **이미지 파일 카빙** + 두 IP 간 스트림을 재조립해 PNG 파일 시그니처가 포함된 블록을 추출했다. + + ```python + from collections import defaultdict + + from scapy.all import IP, Raw, TCP, rdpcap + + pkts = rdpcap("prob.pcap") + ip1, ip2 = "198.51.100.23", "192.168.110.128" + streams = defaultdict(list) + + for packet in pkts: + if packet.haslayer(IP) and packet.haslayer(TCP): + if {packet[IP].src, packet[IP].dst} == {ip1, ip2}: + sid = f"{packet[IP].src}:{packet[TCP].sport}-{packet[IP].dst}:{packet[TCP].dport}" + streams[sid].append(packet) + + for sid, arr in streams.items(): + payload = b"".join( + bytes(p[Raw].load) + for p in sorted(arr, key=lambda x: x[TCP].seq) + if p.haslayer(Raw) + ) + if b"\x89PNG" in payload: + start = payload.find(b"\x89PNG") + blob = payload[start:] + end_marker = b"IEND\xae\x42\x60\x82" + end = blob.find(end_marker) + if end != -1: + blob = blob[: end + len(end_marker)] + name = f"extracted_{sid.replace(':', '_').replace('-', '_')}.png" + with open(name, "wb") as f: + f.write(blob) + print("Saved:", name, len(blob)) + ``` + + 총 8개의 PNG 파일을 복구했으며 모두 Red 채널이 반전된 패턴을 보였다. + +5. **스테가노그래피 분석** + Red 채널 반전이 있는 이미지를 [stylesuxx/steganography](https://stylesuxx.github.io/steganography/) 도구로 확인한 결과 평문 메시지가 존재했고, 다음 정보를 얻었다. + + > The product name is "HelioKey." The release date for this product is "2025-12-25". + +### 3.3 Results +- ProductName: **HelioKey** +- ReleaseDate: **2025-12-25** + +최종 플래그: `whitehat2025{HelioKey_2025-12-25}` + +### 3.4 Discussion +문제는 네트워크 포렌식과 스테가노그래피 분석을 동시에 요구하는 전형적인 정보 유출 탐지 시나리오였다. 암호화된 HTTPS 트래픽 사이에 남아 있던 평문 문자열("hide our messages inside images")이 결정적 단서였고, Red 채널 반전이라는 명시적 신호 덕분에 은닉 데이터의 존재를 빠르게 파악할 수 있었다. diff --git a/src/content/blog/01-whpre-writeup/temp.txt b/src/content/blog/01-whpre-writeup/temp.txt new file mode 100644 index 0000000..8c052d9 --- /dev/null +++ b/src/content/blog/01-whpre-writeup/temp.txt @@ -0,0 +1,215 @@ +1 Introduction +본문서는 2025 WHITEHAT 예선에 참가한팀헤일메리의문항별WriteUp이며, AI sommelier, Leakage +Investigation만을 다룬다(나머진 귀찮음) +2 AI sommelier +2.1 Overview +서버는 llama3.2가생성한텍스트 10개와gemma3가생성한텍스트 10개(총20개)를 무작위로제공한다. +참가자는 각텍스트의생성모델을판별하여 정답을제출하면 플래그를 획득한다. +핵심가정은다음과같다. 언어모델은 자기가 생성했을 법한 텍스트에 대해 상대적으로 더높은 로그우도 +(log-likelihood)를 부여한다. 따라서후보모델을실제로구동한뒤입력텍스트에 대한logprobs를 관측하면 +모델기원분류(model attribution)가가능하다. +2.2 Methodology +입력x = (t1, . . . , tn)에 대해모델M 의로그우도합은 +log pM (x) = +n +i=1 +로정의된다. 길이편향을줄이기위해평균로그우도 +mean +_logprob(x) = 1 +n +n +i=1 +를 사용하고, 근소차이의안정화를 위해바이트 단위교차엔트로피 +−log pM (x) +bpb(x) = +log pM (ti |t all_strings.txt +검색 결과, 다음문장이발견되었다. +From now on, let’s hide our messages inside images. +이문장을통해공격자가이후스테가노그래피(steganography) 기법을사용할것임을유추할수있었다. +4 +3) TCP 스트림 재구성. 해당 문장을포함하는 패킷을기준으로양단 IP/포트를 식별하고, 동일한TCP +세션내의모든페이로드를 시간순서대로병합하여 전체 대화를 복원하였다. +from scapy.all import * +pkts = rdpcap(’prob.pcap’) +needle = b"From now on, let’s hide our messages inside images." +for i, p in enumerate(pkts): +if p.haslayer(Raw) and needle in bytes(p[Raw].load): +src, dst = p[IP].src, p[IP].dst +sport, dport = p[TCP].sport, p[TCP].dport +stream = [] +for q in pkts: +if q.haslayer(IP) and q.haslayer(TCP): +cond1 = (q[IP].src, q[IP].dst, q[TCP].sport, q[TCP].dport) == (src, dst, sport, +dport) +cond2 = (q[IP].src, q[IP].dst, q[TCP].sport, q[TCP].dport) == (dst, src, dport, +sport) +if cond1 or cond2: stream.append(q) +data = b’’.join(bytes(q[Raw].load) for q in sorted(stream, key=lambda x: x.time) if q. +haslayer(Raw)) +print(data.decode(’utf-8’, errors=’ignore’)[:2000]) +break +복원된대화에는 다음과같은내용이포함되어 있었다. +[198.51.100.23] From now on, let’s hide our messages inside images. +[192.168.110.128] Inside images? How would anyone know to look for them? +[198.51.100.23] I’ll put a four-digit length at the front. Just read up to that. +[192.168.110.128] Got it. How can I notice that an image has a message? +[198.51.100.23] I’ll invert the red channel so you can notice. +이를 통해, 메시지는 이미지파일내부에 숨겨지며, Red 채널반전과4자리 길이 프리픽스가삽입된것으로 +확인되었다. +4) 이미지 파일 카빙. 두IP 간스트림 중PNG 파일서명을포함하는 데이터 블록을추출하였다. +from scapy.all import * +from collections import defaultdict +pkts = rdpcap(’prob.pcap’) +ip1, ip2 = "198.51.100.23", "192.168.110.128" +streams = defaultdict(list) +for i, p in enumerate(pkts): +if p.haslayer(IP) and p.haslayer(TCP): +if {p[IP].src, p[IP].dst} == {ip1, ip2}: +sid = f"{p[IP].src}:{p[TCP].sport}-{p[IP].dst}:{p[TCP].dport}" +5 +streams[sid].append(p) +for sid, arr in streams.items(): +payload = b’’.join(bytes(p[Raw].load) for p in sorted(arr, key=lambda x: x[TCP].seq) if p. +haslayer(Raw)) +if b’\x89PNG’ in payload: +start = payload.find(b’\x89PNG’) +blob = payload[start:] +endmk = b’IEND\xae\x42\x60\x82’ +end = blob.find(endmk) +if end != -1: +blob = blob[:end+len(endmk)] +name = f"extracted_{sid.replace(’:’,’_’).replace(’-’,’_’)}.png" +with open(name, ’wb’) as f: +f.write(blob) +print("Saved:", name, len(blob)) +총8개의PNG 이미지파일이추출되었으며, 모두동일한Red 채널반전패턴을보였다. +5) 스테가노그래피분석. Red 채널반전이존재하는 이미지를 분석한결과평문 메시지가존재함을확인 +하였다. (https://stylesuxx.github.io/steganography/ 활용) +발견된메시지에서찾은필요한정보는 다음과같다: +The product name is "HelioKey." The release date for this product is "2025-12-25". +3.3 Results +분석을통해다음정보를 복원하였다. +• ProductName: HelioKey +• ReleaseDate: 2025-12-25 +최종플래그는 다음과같다. +whitehat2025{HelioKey_2025-12-25} +3.4 Discussion +본문제는 네트워크 포렌식과스테가노그래피분석이결합된전형적인정보유출사건분석유형이다. 단 +순한HTTPS 트래픽에 가려져있었지만, 평문 문자열(‘hide our messages inside images‘)이결정적단서가 +되었으며, 이후Red 채널반전이라는 명시적시그널을통해은닉 데이터의존재를 확인할수있었다