<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>코드스키</title>
    <link>https://gorani-kr.tistory.com/</link>
    <description>보안, 시스템, 개발 삽질을 직접 재현하고 기록하는 컴공생의 기술 블로그</description>
    <language>ko</language>
    <pubDate>Mon, 10 Aug 2026 21:04:39 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>코드스키</managingEditor>
    <image>
      <title>코드스키</title>
      <url>https://tistory1.daumcdn.net/tistory/6601514/attach/b7e2e90733ae4e529690fa654af100a2</url>
      <link>https://gorani-kr.tistory.com</link>
    </image>
    <item>
      <title>대용량 강의 영상을 안정적으로 백업하는 병렬 다운로드 파이프라인 짜기</title>
      <link>https://gorani-kr.tistory.com/43</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;cover-n14.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lxTbS/dJMcadbJZoi/SiOQjJ1aN6t0xEIM0wnfTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lxTbS/dJMcadbJZoi/SiOQjJ1aN6t0xEIM0wnfTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lxTbS/dJMcadbJZoi/SiOQjJ1aN6t0xEIM0wnfTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlxTbS%2FdJMcadbJZoi%2FSiOQjJ1aN6t0xEIM0wnfTk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-filename=&quot;cover-n14.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 수강하는 온라인 강의가 수백 개 있는데, 재생이 자주 버벅여서 내 자료를 로컬에 보존해두고 싶었다. 수백 개를 하나씩 받는 건 말이 안 되고, 그렇다고 대충 스크립트를 돌리면 중간에 끊기거나 같은 걸 또 받거나 용량이 터진다. 그래서 &quot;끊겨도 이어받고, 이미 받은 건 건너뛰고, 끝났는지 한눈에 아는&quot; 파이프라인을 설계했다. 특정 사이트 얘기가 아니라 대용량 배치 다운로드를 어떻게 견고하게 만드는가에 대한 기록이다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bxBQPX/dJMcaasGT6h/AAAAAAAAAAAAAAAAAAAAAGZQe6J7WbrRsOm3HdpD1LGpcP2PaQOfZV1gEBHNZ8r9/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=746oY%2BDrvlLyNaHvNRbO%2F%2FYit4U%3D&quot; alt=&quot;대용량 파일을 안전하게 병렬 백업하는 다운로드 파이프라인 대표 이미지&quot; /&gt;
&lt;figcaption&gt;대용량 파일을 안전하게 병렬 백업하는 다운로드 파이프라인 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;먼저 목록과 메타데이터를 확보한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작은 &quot;무엇을 받을 것인가&quot;의 완전한 목록이다. 나는 강의 목록 페이지의 정적 HTML을 파싱해서 각 항목의 식별자를 뽑았다. 여기서 하나 배운 건, 페이지가 아코디언처럼 접혀 있어도 DOM에는 전체 항목이 이미 다 들어있는 경우가 많다는 거다. 굳이 펼치는 클릭을 흉내 내지 않아도, 정적으로 로드된 목록만 긁으면 전량을 얻을 수 있었다. 개수가 사이트가 보여주는 통계와 정확히 맞는지 대조해서 파싱이 빠짐없이 됐는지 검증했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 파일 정보는 목록에 다 없어서, 항목별로 메타데이터를 한 번씩 조회했다. 이건 동시성을 적당히 걸어서 배치로 처리했다. 조회 실패가 0건인지 확인하고 다음 단계로 넘어갔다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;용량을 미리 정확히 계산한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대용량 작업에서 제일 위험한 건 용량 추정을 대충 하는 거다. 나도 처음엔 샘플 하나의 재생시간과 비트레이트로 전체를 어림했다가 크게 틀렸다. 화면 움직임이 많은 영상은 파일당 2GB에 달할 정도로 편차가 컸기 때문이다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/wvJ9V/dJMcaf8tULp/AAAAAAAAAAAAAAAAAAAAAEyRDH4VU9KHHciWIivDmZB77l-vwrmRXahxvGansMZ5/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=nMGkJ5%2Bzr2ADuVzdyOQ94XF%2B4aM%3D&quot; alt=&quot;MDN HTTP Range 헤더 문서&quot; /&gt;
&lt;figcaption&gt;MDN HTTP Range 헤더 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/cedJCL/dJMcaf8tULq/AAAAAAAAAAAAAAAAAAAAAPX_V4bZG3hJYZptusQtSktp0u7k0LF2UVO_A_S7YbEk/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=GgJaBk1Xur6LlRknioYi3JVfd0Q%3D&quot; alt=&quot;RFC 9110의 Range 필드 명세&quot; /&gt;
&lt;figcaption&gt;RFC 9110의 Range 필드 명세&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 추정 대신 실측을 했다. 모든 항목에 대해 &lt;code&gt;Range: bytes=0-0&lt;/code&gt; 헤더로 딱 첫 바이트만 요청하면, 서버가 응답 헤더에 &lt;code&gt;Content-Range: bytes 0-0/TOTAL&lt;/code&gt; 형태로 전체 크기를 알려준다. 이 TOTAL만 파싱해서 다 더하면 실제 전체 용량이 나온다. 이렇게 계산한 값이 실제 다운로드 후 디스크 사용량과 거의 정확히 맞았다. 미리 정확한 총량을 알면 받을 디스크에 여유가 있는지, 얼마나 걸릴지 판단할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8-way 병렬로, 이어받기 가능하게&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다운로드는 집에 있는 리눅스 박스에서 돌렸다. 샌드박스나 노트북보다 홈 네트워크가 훨씬 빨라서, SSH로 붙어 백그라운드로 굴리는 게 효율적이었다. URL과 저장 경로를 담은 매니페스트를 만들어 서버로 전송하고, 목록을 여덟 조각으로 나눠서(&lt;code&gt;split -n l/8&lt;/code&gt;) 각 조각을 백그라운드 서브셸에서 순차적으로 받게 했다. 즉 8개가 동시에 돌면서 각자 자기 몫을 처리하는 구조다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/cDCaau/dJMb99NXWfO/AAAAAAAAAAAAAAAAAAAAAAPVTioS01ZeTM_xjMA7GbSE368VXEz4um1BPW0mwQrV/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=TGPJwtyr%2B0pxxqitIfKko6WeGgE%3D&quot; alt=&quot;GNU Coreutils split 명령 공식 문서&quot; /&gt;
&lt;figcaption&gt;GNU Coreutils split 명령 공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 파일 다운로드에는 재시도와 이어받기를 넣었다. curl의 resume(&lt;code&gt;-C -&lt;/code&gt;) 옵션을 쓰면 중간에 끊긴 파일을 처음부터 다시 받지 않고 이어서 받는다. 공식 문서에 나오듯 &lt;code&gt;-C -&lt;/code&gt;는 curl이 스스로 어디까지 받았는지 알아내게 하고, &lt;code&gt;-C 400&lt;/code&gt;처럼 정확한 바이트를 지정할 수도 있다. 이 옵션은 &lt;code&gt;--range&lt;/code&gt;와 동시에 쓸 수 없다는 점도 문서에서 확인했다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;curl -C - -o &quot;$dest&quot; &quot;$url&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이미 받아둔 파일이 일정 크기 이상이면 스킵하게 했다. 이 스킵 로직 덕분에 스크립트를 중간에 다시 돌려도 안전했다. 실제로 한 번은 실행 명령이 로컬에서 중단된 것처럼 보였는데, 서버 프로세스는 이미 돌고 있어서 재실행했더니 이미 받은 수백 개를 SKIP으로 건너뛰고 나머지만 받았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;완료를 알리는 마커와 진행 로그&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장시간 배치의 마지막 조각은 &quot;지금 어디까지 됐나&quot;를 빨리 아는 장치다. 청크별로 OK/FAIL을 로그에 쌓고, 전체가 끝나면 상태 파일에 완료 표시를 남기게 했다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;printf 'ALL_DONE %s\n' &quot;$(date -Is)&quot; &amp;gt; status.txt&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;돌아왔을 때 이 마커 파일 하나만 확인하면 끝났는지 아닌지 즉시 알 수 있고, 진행 중이면 로그의 OK 카운트와 누적 용량을 보면 된다. 다운받은 파일은 랜덤 샘플을 골라 &lt;code&gt;file&lt;/code&gt; 명령으로 실제 정상 미디어인지 검증했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 대용량 배치 다운로드의 견고함은 화려한 기술이 아니라 몇 가지 방어에서 나온다. 목록을 완전하게 확보하고, 용량을 추정이 아닌 실측으로 잡고, 이어받기와 스킵으로 재실행에 안전하게 만들고, 완료 마커로 상태를 빨리 읽게 하는 것. 이 네 가지를 갖추니 &quot;걸어두고 나중에 결과만 확인하는&quot; 작업이 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 이런 자동화는 내가 정당하게 접근 권한을 가진 내 자료를 개인적으로 보존하는 용도에 한정해서 썼다. 남의 콘텐츠나 접근 권한이 없는 자료에 이 방식을 적용하는 건 다른 문제다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Range&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MDN: HTTP Range 헤더&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://httpwg.org/specs/rfc9110.html#field.range&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RFC 9110 Range 필드&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://curl.se/docs/manpage.html#-C&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;curl -C/--continue-at&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gnu.org/software/coreutils/manual/html_node/split-invocation.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GNU Coreutils split&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발</category>
      <category>Automation</category>
      <category>Bash</category>
      <category>CURL</category>
      <category>infra</category>
      <category>ssh</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/43</guid>
      <comments>https://gorani-kr.tistory.com/43#entry43comment</comments>
      <pubDate>Wed, 29 Jul 2026 10:40:22 +0900</pubDate>
    </item>
    <item>
      <title>얼굴인증 시스템에 5가지 적대적 공격을 걸고 방어를 우회해본 기록</title>
      <link>https://gorani-kr.tistory.com/42</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;cover-n13.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kMqdi/dJMcac42WPj/EEQfSpQc3pLaGE4vJTEWqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kMqdi/dJMcac42WPj/EEQfSpQc3pLaGE4vJTEWqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kMqdi/dJMcac42WPj/EEQfSpQc3pLaGE4vJTEWqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkMqdi%2FdJMcac42WPj%2FEEQfSpQc3pLaGE4vJTEWqk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-filename=&quot;cover-n13.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한이음 ICT 멘토링에서 공격&amp;middot;방어&amp;middot;개발 3인이 팀을 이뤄 얼굴인증 시스템의 적대적 공격과 방어를 다루는 프로젝트를 했다. 나는 공격 파트를 맡았다. 여기서는 실제로 어떤 공격을 만들었고, 방어팀이 붙인 방어를 어떤 조건에서 뚫었는지, 숫자와 함께 정리한다. 사용한 모델과 데이터셋은 모두 공개 연구용 자원이고, 실제 서비스나 타인의 인증 시스템을 공격한 것은 아니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bNSchB/dJMcagsQ92C/AAAAAAAAAAAAAAAAAAAAAJkej2akS92OCqi2cjwWx0ittE4m7qaFDwJ3BkYqkUf6/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=a7J3gCKYNGE%2BiYNh9Wj8oewAYd4%3D&quot; alt=&quot;얼굴인증 시스템에 5가지 적대적 공격을 걸고 방어를 우회해본 기록 대표 이미지&quot; /&gt;
&lt;figcaption&gt;얼굴인증 시스템에 5가지 적대적 공격을 걸고 방어를 우회해본 기록 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파이프라인 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공격은 크게 두 갈래로 나눴다. 하나는 LFW 얼굴 10인 분류 모델에 대한 targeted 공격 5종 비교, 다른 하나는 FaceNet 기반 얼굴 검증(1:1 인증)에 adaptive 공격을 걸어 방어팀의 입력 변환 방어를 우회하는 실험이다. 전체 흐름은 이렇게 잡았다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2040&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bbcIK1/dJMcaalOkn0/AAAAAAAAAAAAAAAAAAAAAPVQBGGstcAZ1-q6q2YluBpV8hv6uLDZIbT3MztRoGQ3/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=D73uOQ24zh1lymyrgEjr6o9zjDA%3D&quot; alt=&quot;공격 파트 파이프라인: FaceNet 인증 baseline &amp;rarr; 표적 사칭 공격 생성 &amp;rarr; 방어 내성 평가 &amp;rarr; 공격 포렌식&quot; /&gt;
&lt;figcaption&gt;공격 파트 파이프라인: FaceNet 인증 baseline &amp;rarr; 표적 사칭 공격 생성 &amp;rarr; 방어 내성 평가 &amp;rarr; 공격 포렌식&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Source 얼굴과 Target(사칭하려는) 얼굴을 넣고 baseline 유사도를 먼저 재고, 그 다음 공격으로 유사도를 임계값 위로 밀어올린다. 방어가 붙어 있으면 그 위에서 다시 판정하고, 마지막에 threshold margin&amp;middot;L2/Linf&amp;middot;query 수 같은 값을 남겨 포렌식 자료로 정리했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5가지 공격을 같은 조건으로 비교했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResNet-50 기반 얼굴 분류 모델(LFW-10, ImageNet 사전학습 후 파인튜닝)에 FGSM, PGD, Square Attack, JSMA 변형, ZOO-style 공격을 모두 targeted 설정으로 구현했다. 테스트셋 223장 전체에 대해 평가했고, 원본을 모델이 맞힌 샘플(170/223, clean accuracy 76.23%)을 기준으로 공격 성공률을 쟀다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th&gt;공격&lt;/th&gt;
&lt;th&gt;설정&lt;/th&gt;
&lt;th&gt;Target ASR (clean 기준)&lt;/th&gt;
&lt;th&gt;평균 L2&lt;/th&gt;
&lt;th&gt;평균 query&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FGSM&lt;/td&gt;
&lt;td&gt;eps=0.005&lt;/td&gt;
&lt;td&gt;27.65%&lt;/td&gt;
&lt;td&gt;1.89&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PGD&lt;/td&gt;
&lt;td&gt;eps=0.030, steps=10&lt;/td&gt;
&lt;td&gt;100.00%&lt;/td&gt;
&lt;td&gt;3.56&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Square&lt;/td&gt;
&lt;td&gt;eps=0.050, queries=300&lt;/td&gt;
&lt;td&gt;59.41%&lt;/td&gt;
&lt;td&gt;10.76&lt;/td&gt;
&lt;td&gt;190.77&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSMA 변형&lt;/td&gt;
&lt;td&gt;theta=0.050, k=200&lt;/td&gt;
&lt;td&gt;99.41%&lt;/td&gt;
&lt;td&gt;1.97&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ZOO-style&lt;/td&gt;
&lt;td&gt;eps=0.050, queries=2000&lt;/td&gt;
&lt;td&gt;7.06%&lt;/td&gt;
&lt;td&gt;0.58&lt;/td&gt;
&lt;td&gt;1891.51&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;white-box 계열(모델 gradient에 접근 가능)인 PGD와 JSMA 변형이 각각 100%, 99.41%로 거의 모든 표적 사칭에 성공했다. 반면 black-box 계열은 갈렸다. Square Attack은 평균 190회 남짓한 질의로 59.41% 성공률을 냈는데, ZOO-style은 그보다 10배 많은 질의(1891회)를 쓰고도 7.06%에 그쳤다. 같은 black-box라도 gradient를 유한차분으로 근사하는 방식(ZOO)이 무작위 섭동을 진화시키는 방식(Square)보다 이 설정에서는 훨씬 비효율적이었다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1030&quot; data-origin-height=&quot;300&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/dz9y04/dJMcaalOkoi/AAAAAAAAAAAAAAAAAAAAAJbo198qriqGzT-OpyyPuBOIXYMtRfprMz1TVBBAUnOB/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=2hyD95kDcj68%2B7UT%2B%2FcwiOlUIpw%3D&quot; alt=&quot;PGD eps=0.005 공격 예시: 소스 이미지를 최소한으로 흔들어 타깃 얼굴에 대한 FaceNet 유사도를 끌어올림&quot; /&gt;
&lt;figcaption&gt;PGD eps=0.005 공격 예시: 소스 이미지를 최소한으로 흔들어 타깃 얼굴에 대한 FaceNet 유사도를 끌어올림&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PGD 예시 이미지를 보면 사람 눈에는 원본과 거의 구별이 안 가는데, Perturbation 채널만 따로 보면 이미지 전체에 저주파 노이즈가 고르게 퍼져 있는 게 보인다. eps를 낮게 잡아도(0.005) 반복 최적화(PGD)를 쓰면 표적 얼굴 쪽으로 임베딩을 충분히 밀어붙일 수 있다는 뜻이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;방어를 우회하는 게 진짜 재미있던 부분&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방어팀은 JPEG 압축, bit-depth 축소, Gaussian smoothing 세 가지 입력 변환 방어를 붙였다. 일반 PGD를 그대로 방어에 통과시키면 어떻게 되는지 먼저 봤다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2039&quot; data-origin-height=&quot;588&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/85JQK/dJMcafnfIvY/AAAAAAAAAAAAAAAAAAAAAJL9rMfFcxw4NR4dYgU2TjwF_tTnN40XlRu7b9t-s6w8/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=F2VGjqWwj06wP1uoF58MMONlIto%3D&quot; alt=&quot;5개 공격 x 3개 방어 조합의 방어 성공률과 복구율 히트맵&quot; /&gt;
&lt;figcaption&gt;5개 공격 x 3개 방어 조합의 방어 성공률과 복구율 히트맵&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대체로 방어 성공률이 80~93%대로 높았다. 특히 JSMA 변형과 PGD는 세 방어 모두에서 84~93%대로 잘 막혔다. 그런데 이건 공격이 방어의 존재를 모르는 상태에서 만든 결과였다. 그래서 &quot;공격이 방어를 알고 있다면 어떻게 될까&quot;를 따로 실험했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Adaptive PGD로 Gaussian Smoothing을 무력화하다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 PGD 옵티마이저를 두 가지 방식으로 돌렸다. 하나는 방어를 모르는 Plain PGD, 다른 하나는 공격 루프 안에 Gaussian smoothing을 미리 넣어서 &quot;블러를 견디는 방향으로&quot; 최적화하는 Adaptive PGD다. FaceNet(vggface2) 임베딩과 코사인 유사도 임계값 0.4797을 기준으로 100쌍씩 같은 조건에서 비교했다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th&gt;eps&lt;/th&gt;
&lt;th&gt;공격&lt;/th&gt;
&lt;th&gt;raw 성공&lt;/th&gt;
&lt;th&gt;방어 후 성공&lt;/th&gt;
&lt;th&gt;우회율&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.005&lt;/td&gt;
&lt;td&gt;Plain&lt;/td&gt;
&lt;td&gt;57/100&lt;/td&gt;
&lt;td&gt;2/100&lt;/td&gt;
&lt;td&gt;3.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.005&lt;/td&gt;
&lt;td&gt;Adaptive&lt;/td&gt;
&lt;td&gt;3/100&lt;/td&gt;
&lt;td&gt;0/100&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.010&lt;/td&gt;
&lt;td&gt;Plain&lt;/td&gt;
&lt;td&gt;86/100&lt;/td&gt;
&lt;td&gt;2/100&lt;/td&gt;
&lt;td&gt;2.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.010&lt;/td&gt;
&lt;td&gt;Adaptive&lt;/td&gt;
&lt;td&gt;6/100&lt;/td&gt;
&lt;td&gt;4/100&lt;/td&gt;
&lt;td&gt;66.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.020&lt;/td&gt;
&lt;td&gt;Plain&lt;/td&gt;
&lt;td&gt;100/100&lt;/td&gt;
&lt;td&gt;4/100&lt;/td&gt;
&lt;td&gt;4.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.020&lt;/td&gt;
&lt;td&gt;Adaptive&lt;/td&gt;
&lt;td&gt;25/100&lt;/td&gt;
&lt;td&gt;24/100&lt;/td&gt;
&lt;td&gt;96.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Plain PGD는 eps를 아무리 키워도 smoothing 방어에 거의 다 걸렸다(우회율 2~4%). 반면 Adaptive PGD는 eps=0.020에서 raw 성공률 자체는 25%로 낮았지만, 방어를 통과한 뒤에도 24개가 살아남아 우회율이 96%까지 뛰었다. 방어 없는 조건만 보면 Adaptive가 Plain보다 훨씬 약해 보이는데(raw 25 vs 100), 방어가 붙는 순간 우위가 완전히 뒤집혔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 결과에서 얻은 결론은 &quot;smoothing 하나로 막았다&quot;고 안심하면 안 된다는 것이었다. 공격자가 방어의 존재를 알고 그 방어를 손실 함수에 포함시켜 최적화하면(adaptive), 단일 입력 변환 방어는 상당히 쉽게 뚫린다. 다층 방어나 생성형 정화(DAE, DiffPure 계열)처럼 adaptive 공격 기준으로도 평가된 방어를 검토할 필요가 있다는 게 팀 결론이었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;역할을 분명히 하면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 공격 5종의 구현&amp;middot;평가와 adaptive bypass 분석을 맡았다. Gaussian/bit-depth/JPEG 방어 구현과 방어 측 평가 파이프라인은 방어 담당 팀원이, 통합 대시보드와 산출물 정리는 개발 담당 팀원이 맡았다. 위 수치는 전부 우리 팀이 직접 돌린 실험 결과이고, 벤치마크 재현 조건(같은 100쌍, 같은 steps/alpha)을 두 조건에서 동일하게 유지한 것이 결과의 신뢰도를 만들었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/1412.6572&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;FGSM 원 논문 &amp;mdash; Goodfellow et al., Explaining and Harnessing Adversarial Examples&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/1706.06083&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PGD 원 논문 &amp;mdash; Madry et al., Towards Deep Learning Models Resistant to Adversarial Attacks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/1503.03832&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;FaceNet 원 논문 &amp;mdash; Schroff et al.&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>보안</category>
      <category>AdversarialML</category>
      <category>FaceNet</category>
      <category>FGSM</category>
      <category>PGD</category>
      <category>보안</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/42</guid>
      <comments>https://gorani-kr.tistory.com/42#entry42comment</comments>
      <pubDate>Wed, 29 Jul 2026 10:38:07 +0900</pubDate>
    </item>
    <item>
      <title>취약점 심각도를 정직하게 매기기 &amp;mdash; RCE라고 부풀리지 않으려고 한 일들</title>
      <link>https://gorani-kr.tistory.com/41</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;cover-n12.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zrwH0/dJMcaccT5mU/A5D77XH0QLTSo0yA5VIi5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zrwH0/dJMcaccT5mU/A5D77XH0QLTSo0yA5VIi5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zrwH0/dJMcaccT5mU/A5D77XH0QLTSo0yA5VIi5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzrwH0%2FdJMcaccT5mU%2FA5D77XH0QLTSo0yA5VIi5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-filename=&quot;cover-n12.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;퍼징으로 크래시를 찾으면 잠깐 &amp;ldquo;이거 RCE 아닐까?&amp;rdquo;라는 생각이 든다. 하지만 크래시와 코드 실행 사이에는 큰 간격이 있다. 네트워크 파서 버그들을 제보하면서 실제로 확인한 영향만 남기고 심각도를 계속 낮춰 잡았던 과정을 정리한다. 공개가 끝난 사례의 평가 방법만 다루고, 미공개 취약점과 재현 입력은 싣지 않는다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bgnV9Z/dJMcaiqDSWU/AAAAAAAAAAAAAAAAAAAAAE-h8CH1_mHZoEwqPrLucxsP8IQVLBik_FOD8Zvk5v7D/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=p4lsqw8BK1LeLSZgz5y9jmbOmLE%3D&quot; alt=&quot;RCE로 부풀리지 않는 취약점 영향도와 CVSS 평가 대표 이미지&quot; /&gt;
&lt;figcaption&gt;RCE로 부풀리지 않는 취약점 영향도와 CVSS 평가 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Out-of-bounds read를 RCE라고 부르지 않았다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;찾은 버그의 중심은 버퍼 경계 밖을 읽는 OOB read였다. 비정상 종료가 재현되면 가용성 영향은 설명할 수 있다. 인접 메모리가 외부 응답이나 로그로 실제 노출되는 경로가 확인되면 기밀성 영향도 논의할 수 있다. 하지만 &amp;ldquo;읽기 범위를 벗어났다&amp;rdquo;는 사실만으로 공격자가 값을 쓰거나 명령 포인터를 제어했다고 말할 수는 없다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bcSPmO/dJMcaiKUri1/AAAAAAAAAAAAAAAAAAAAALGAnMHBFMRmyCeOyrLiD7-k7fV_qsc1Xhm1INMEzD6I/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=y1X9e9dKzX7QvwZjsbf%2FG4GTJNM%3D&quot; alt=&quot;MITRE CWE-125 Out-of-bounds Read 정의&quot; /&gt;
&lt;figcaption&gt;MITRE CWE-125 Out-of-bounds Read 정의&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 보고서에는 확인된 것을 짧게 썼다. &amp;ldquo;OOB read가 발생하고 프로세스가 종료된다. 코드 실행과 외부 정보 노출은 확인하지 못했다.&amp;rdquo; 가능성을 크게 쓰는 대신 증거가 생기면 그때 영향 범위를 넓히는 방식이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ASan 빌드와 일반 최적화 빌드를 같이 봤다&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bgabqj/dJMcaiRKXar/AAAAAAAAAAAAAAAAAAAAALcXlOEIzUwhJFARGDGeRchh_sNQlQVMx-3M6CR0qglC/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=QyKZpufOFoP9E%2Fead9WzZu3SADM%3D&quot; alt=&quot;Clang AddressSanitizer 공식 문서&quot; /&gt;
&lt;figcaption&gt;Clang AddressSanitizer 공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AddressSanitizer는 경계 밖 접근 위치와 호출 스택을 찾는 데 강하다. 반면 실제 배포 바이너리와 메모리 배치가 달라질 수 있다. 같은 입력을 ASan 디버그 빌드와 일반 &lt;code&gt;-O2&lt;/code&gt; 빌드에 넣었다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th&gt;결과&lt;/th&gt;
&lt;th&gt;해석&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;두 빌드 모두 결정적으로 크래시&lt;/td&gt;
&lt;td&gt;가용성 영향 근거가 강함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ASan만 abort, 일반 빌드는 정상 종료&lt;/td&gt;
&lt;td&gt;버그는 맞지만 배포 영향은 추가 검증 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;일반 빌드에서 출력만 달라짐&lt;/td&gt;
&lt;td&gt;데이터 무결성&amp;middot;논리 영향으로 별도 평가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sanitizer 리포트 한 장만으로 &amp;ldquo;원격 DoS가 확정됐다&amp;rdquo;고 쓰지 않았다. 실제 입력 경로가 네트워크에서 도달하는지, 데몬이 죽는지, supervisor가 즉시 복구하는지도 같이 봤다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CVSS 벡터를 숫자보다 먼저 적었다&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/cxU56G/dJMcaiqDSWV/AAAAAAAAAAAAAAAAAAAAAFInp-EerK_N1TQ_CbLZuCAL1EOhE4a1l_GDfGIqAIFs/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=xHjQ5RiOmwiBsxKMRPB1Hsk9ULU%3D&quot; alt=&quot;FIRST CVSS v3.1 공식 계산기&quot; /&gt;
&lt;figcaption&gt;FIRST CVSS v3.1 공식 계산기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점수를 먼저 정하고 벡터를 맞추면 원하는 숫자 쪽으로 해석이 끌린다. 공격 벡터, 복잡도, 필요한 권한과 사용자 상호작용, 범위 변화, C/I/A 영향을 문장으로 정한 뒤 계산기에 넣었다. 특히 네트워크 패킷으로 도달한다는 사실과 아무 인증 없이 도달한다는 사실은 다른 지표다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;C&lt;/b&gt;: 외부로 실제 데이터가 노출됐는가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;I&lt;/b&gt;: 파싱 결과나 저장 상태를 공격자가 바꿀 수 있는가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;A&lt;/b&gt;: 단일 요청이 프로세스&amp;middot;서비스 가용성을 얼마나 깨는가&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;버전 대조와 중복 검색을 붙였다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취약 버전과 이전 릴리스, 최신 브랜치에 같은 최소 입력을 적용했다. 특정 커밋 이후에만 재현되면 도입 범위를 좁힐 수 있고, 옛 CVE와 함수&amp;middot;입력 형식이 다르다면 그 근거를 적을 수 있다. 공개 이슈, 커밋, NVD, GitHub Security Advisory를 검색했지만 비공개 중복까지 알 수는 없다는 한계도 명시했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;패치의 성공 기준은 &amp;ldquo;안 죽음&amp;rdquo; 하나가 아니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경계 값을 clamp하거나 길이를 확인하는 패치를 넣은 뒤 같은 PoC가 ASan&amp;middot;일반 빌드에서 모두 살아남는지 확인했다. 정상 입력 회귀 테스트와 기존 출력도 비교했다. 크래시만 막고 정상 레코드를 버리거나 카운트를 바꾸면 안전한 수정이라고 하기 어렵다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;제보에 남긴 증거&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대상 버전&amp;middot;커밋&amp;middot;빌드 옵션&lt;/li&gt;
&lt;li&gt;최소 입력의 크기와 해시&lt;/li&gt;
&lt;li&gt;ASan 스택과 일반 빌드 결과&lt;/li&gt;
&lt;li&gt;버전별 재현 표&lt;/li&gt;
&lt;li&gt;패치 전후&amp;middot;정상 입력 회귀 결과&lt;/li&gt;
&lt;li&gt;중복 검색 범위와 확인하지 못한 부분&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;퍼징 자체보다 이 평가 과정이 더 오래 걸렸다. 크래시를 크게 부르는 건 쉽지만, 어디까지 확인했고 어디부터 모르는지 정확히 쓰는 쪽이 결국 메인테이너와 CNA의 신뢰를 얻었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.first.org/cvss/calculator/3.1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;FIRST CVSS v3.1 Calculator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.first.org/cvss/v3.1/specification-document&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CVSS v3.1 Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://clang.llvm.org/docs/AddressSanitizer.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Clang AddressSanitizer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cwe.mitre.org/data/definitions/125.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CWE-125&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같은 크래시도 배치 방식에 따라 벡터가 달라진다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 인증 없이 외부 네트워크에서 한 패킷으로 데몬을 매번 종료할 수 있고 서비스가 자동 복구되지 않는다면 &lt;code&gt;AV:N/AC:L/PR:N/UI:N&lt;/code&gt;과 높은 가용성 영향을 검토할 수 있다. 반대로 로컬 파일을 관리자가 직접 열어야 재현되고 프로세스가 즉시 재시작된다면 공격 벡터&amp;middot;권한&amp;middot;가용성 평가는 달라진다. &amp;ldquo;네트워크 파서에서 발견했다&amp;rdquo;만으로 AV:N을 고정하면 안 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;보고서 문장을 이렇게 나눴다&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;문장 예시&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;관찰&lt;/td&gt;
&lt;td&gt;ASan이 함수 X의 OOB read를 보고하고 프로세스가 abort했다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;재현&lt;/td&gt;
&lt;td&gt;최신 커밋에서 동일 입력으로 5회 모두 재현됐다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;영향&lt;/td&gt;
&lt;td&gt;해당 입력이 실제 서비스 경로에 도달할 때 가용성 저하가 가능하다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;미확인&lt;/td&gt;
&lt;td&gt;메모리 내용의 외부 노출과 코드 실행은 확인하지 못했다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관찰과 영향을 한 문장에 섞지 않으면 과장이 줄어든다. 특히 sanitizer의 진단명은 버그 종류이지 곧바로 원격 공격 결과가 아니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;제보 전 마지막 질문&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;PoC가 최신 기본 브랜치에서도 재현되는가&lt;/li&gt;
&lt;li&gt;외부 입력이 해당 코드까지 실제로 도달하는가&lt;/li&gt;
&lt;li&gt;프로세스&amp;middot;서비스&amp;middot;시스템 중 어디까지 영향이 가는가&lt;/li&gt;
&lt;li&gt;정보 노출과 코드 실행을 직접 관찰했는가&lt;/li&gt;
&lt;li&gt;패치가 정상 입력을 깨뜨리지 않는가&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같이 읽기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기준으로 정리한 실제 공개 제보 흐름은 &lt;a href=&quot;https://gorani-kr.tistory.com/15&quot;&gt;AFL++ 퍼징과 GHSA 10건 제보 기록&lt;/a&gt;, 메인테이너와 비공개로 협업한 과정은 &lt;a href=&quot;https://gorani-kr.tistory.com/31&quot;&gt;GitHub 책임 제보와 PVR이 없을 때의 대응&lt;/a&gt;에 적었다.&lt;/p&gt;</description>
      <category>보안</category>
      <category>AddressSanitizer</category>
      <category>CVSS</category>
      <category>CWE-125</category>
      <category>fuzzing</category>
      <category>취약점제보</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/41</guid>
      <comments>https://gorani-kr.tistory.com/41#entry41comment</comments>
      <pubDate>Tue, 28 Jul 2026 11:00:39 +0900</pubDate>
    </item>
    <item>
      <title>SSH 끊어도 죽지 않는 퍼징 캠페인 만들기 (nohup은 왜 죽었나)</title>
      <link>https://gorani-kr.tistory.com/40</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;cover-n11.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnOoD2/dJMb99UMigO/qE3Z85c48TvAsh8c1wbEZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnOoD2/dJMb99UMigO/qE3Z85c48TvAsh8c1wbEZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnOoD2/dJMb99UMigO/qE3Z85c48TvAsh8c1wbEZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnOoD2%2FdJMb99UMigO%2FqE3Z85c48TvAsh8c1wbEZ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-filename=&quot;cover-n11.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;집 Linux 박스에서 AFL++ 캠페인을 며칠씩 돌리고, 맥북을 덮었다가 다음 날 결과를 확인하고 싶었다. 그런데 SSH로 띄운 퍼저가 연결을 끊은 뒤 자꾸 사라졌다. 처음에는 코어를 많이 써서 OOM이 난 줄 알았고, 실제 원인은 프로세스가 SSH 세션에서 충분히 분리되지 않은 실행 방식이었다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/x1Li7/dJMb99NXj6x/AAAAAAAAAAAAAAAAAAAAAF0NNs_BZyhPU0Zn3yKCxS_HnaK8XC_UgSKAwCKBcx9p/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=Vq0Ap%2F%2F%2FJD7qg%2FjVNJqCeYknr2w%3D&quot; alt=&quot;SSH가 끊겨도 유지되는 AFL++ 장기 퍼징 캠페인 구성 대표 이미지&quot; /&gt;
&lt;figcaption&gt;SSH가 끊겨도 유지되는 AFL++ 장기 퍼징 캠페인 구성 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;먼저 OOM인지 세션 종료인지 구분했다&lt;/h2&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;journalctl -k --since today | grep -Ei 'oom|killed process'
dmesg -T | grep -Ei 'oom|out of memory'
ps -o pid,ppid,sid,tty,stat,cmd -p &amp;lt;PID&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OOM killer 기록이 없고 SSH 종료 시점과 프로세스 종료가 맞아떨어지면 세션&amp;middot;터미널 신호를 본다. 단순히 &amp;ldquo;프로세스가 없다&amp;rdquo;만 보면 자원 부족과 세션 종료를 구분하기 어렵다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;nohup이 하는 일과 하지 않는 일&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/b9BC6F/dJMcaiqDSWJ/AAAAAAAAAAAAAAAAAAAAAD-HMZI-A-0PzEzZ1in6QVUyUocqSgpoCnnmlbHCspcr/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=ukzpj0vdNFKDUoU0GQRwPzKYb8Q%3D&quot; alt=&quot;GNU Coreutils nohup 공식 문서&quot; /&gt;
&lt;figcaption&gt;GNU Coreutils nohup 공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;nohup&lt;/code&gt;은 SIGHUP을 무시하게 하고 표준 출력&amp;middot;에러를 파일로 돌린다. 하지만 프로세스를 완전한 서비스로 만들거나 실패 시 재시작해주지는 않는다. 셸 스크립트 안에서 여러 워커를 띄우고 일부 스트림이 터미널에 남아 있으면 기대와 다른 수명 문제가 생길 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;nohup ./run-fuzzers.sh &amp;gt;campaign.log 2&amp;gt;&amp;amp;1 &amp;lt;/dev/null &amp;amp;
disown&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 조합은 간단한 배치에는 충분하지만, 재부팅 후 자동 복구와 자원 제한까지 필요하면 systemd 쪽이 명확하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;setsid로 새 세션을 만든다&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/btfc5m/dJMcaae8TfQ/AAAAAAAAAAAAAAAAAAAAAAjFLVv7ouHb6TNqlR8Ppd8dqbkFkXxHInO1lgNR5hqe/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=g63Wc5U0xE1yLx%2BRAz5U9%2F2yMLA%3D&quot; alt=&quot;Linux setsid(1) 매뉴얼&quot; /&gt;
&lt;figcaption&gt;Linux setsid(1) 매뉴얼&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;setsid -f sh -c '
  exec afl-fuzz -i seeds -o out -M main -- ./target @@
' &amp;gt;main.log 2&amp;gt;&amp;amp;1 &amp;lt;/dev/null&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;setsid -f&lt;/code&gt;는 새 세션을 만들기 위해 fork하고 호출 셸의 제어 터미널에서 분리한다. 예전에는 PPID가 1인지 하나만 봤지만, 시스템에 subreaper가 있으면 다른 부모로 재귀속될 수 있다. &lt;code&gt;SID&lt;/code&gt;와 &lt;code&gt;TTY=?&lt;/code&gt;, 실제 프로세스가 SSH 종료 뒤에도 남는지를 같이 확인하는 편이 정확하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AFL++ 병렬 워커는 하나의 출력 디렉터리를 공유한다&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bp5i42/dJMcaiqDSWI/AAAAAAAAAAAAAAAAAAAAAH6acoMkQhiPWtIGiMmrJuTN9KQb3hBDpljvHj9eW7hI/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=VEYxN1YbhdEJS4FczBgzglRgMjE%3D&quot; alt=&quot;AFL++ 공식 Parallel Fuzzing 문서&quot; /&gt;
&lt;figcaption&gt;AFL++ 공식 Parallel Fuzzing 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;afl-fuzz -i seeds -o sync -M main -- ./target @@
afl-fuzz -i seeds -o sync -S s01 -p fast -- ./target @@
afl-fuzz -i seeds -o sync -S s02 -p explore -- ./target @@&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마스터와 세컨더리는 같은 &lt;code&gt;-o&lt;/code&gt; 동기화 디렉터리를 사용하고 서로 다른 ID를 가져야 한다. 코어 수와 워커 수를 기계적으로 같게 두지는 않았다. 대상 프로그램의 메모리 사용량과 시스템 load, exec/s가 떨어지는 지점을 보고 여유 코어를 남겼다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;돌아왔을 때 5초 안에 상태를 읽게 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 워커의 &lt;code&gt;fuzzer_stats&lt;/code&gt;에서 &lt;code&gt;last_update&lt;/code&gt;, &lt;code&gt;execs_done&lt;/code&gt;, &lt;code&gt;execs_per_sec&lt;/code&gt;, &lt;code&gt;saved_crashes&lt;/code&gt;를 모으고, 전체 작업이 끝났을 때는 완료 마커를 남겼다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;printf 'ALL_DONE %s
' &quot;$(date -Is)&quot; &amp;gt; status.txt&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 SSH 명령이 타임아웃이나 aborted로 끝나도 서버 프로세스는 이미 실행 중일 수 있다. 같은 캠페인을 다시 띄우기 전에 서버의 PID, 로그 증가, 출력 디렉터리를 확인해야 중복 실행을 피할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;장기 운영은 systemd가 더 낫다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;며칠 이상 반복 실행하거나 재부팅 복구가 필요하면 unit 파일로 옮겼다. &lt;code&gt;Restart=on-failure&lt;/code&gt;, &lt;code&gt;WorkingDirectory&lt;/code&gt;, 로그 정책, CPU&amp;middot;메모리 제한을 명시할 수 있다. 일회성 실험은 setsid, 운영에 가까운 캠페인은 systemd라는 선을 잡으니 관리가 단순해졌다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://aflplus.plus/docs/parallel_fuzzing/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AFL++ Parallel Fuzzing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man1/setsid.1.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;setsid(1)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gnu.org/software/coreutils/manual/html_node/nohup-invocation.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GNU nohup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;systemd.service&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;중복 실행을 막는 시작 스크립트&lt;/h2&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/usr/bin/env bash
set -euo pipefail
cd /srv/fuzz/campaign

if [[ -f campaign.pid ]] &amp;amp;&amp;amp; kill -0 &quot;$(cat campaign.pid)&quot; 2&amp;gt;/dev/null; then
  echo &quot;already running: $(cat campaign.pid)&quot;
  exit 1
fi

setsid -f sh -c '
  echo $$ &amp;gt; campaign.pid
  exec ./run-workers.sh &amp;gt; logs/campaign.log 2&amp;gt;&amp;amp;1
'

sleep 1
cat campaign.pid
ps -o pid,ppid,sid,tty,stat,cmd -p &quot;$(cat campaign.pid)&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PID 파일만 믿으면 이전 비정상 종료의 낡은 파일 때문에 막힐 수 있어 &lt;code&gt;kill -0&lt;/code&gt;로 실제 프로세스 존재를 같이 확인했다. 캠페인 디렉터리와 출력 경로도 절대 경로로 고정해 SSH를 어디서 실행했는지에 따라 결과가 흩어지지 않게 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;watcher가 경고한 조건&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;last_update&lt;/code&gt;가 일정 시간 이상 갱신되지 않음&lt;/li&gt;
&lt;li&gt;exec/s가 0 또는 장시간 급락&lt;/li&gt;
&lt;li&gt;워크 PID가 사라졌는데 완료 마커가 없음&lt;/li&gt;
&lt;li&gt;디스크 여유 공간이 임계값 아래로 내려감&lt;/li&gt;
&lt;li&gt;새 크래시는 생겼지만 triage 큐가 처리되지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;watcher는 퍼저를 무조건 다시 띄우지 않았다. 디스크 부족이나 잘못된 대상 바이너리 때문에 죽었다면 자동 재시작이 같은 실패를 반복하기 때문이다. 상태를 기록하고 알림을 남긴 뒤 사람이 원인을 확인하도록 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같이 읽기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 인프라에서 얻은 크래시를 중복 제거하고 공개 제보까지 연결한 과정은 &lt;a href=&quot;https://gorani-kr.tistory.com/15&quot;&gt;AFL++ 네트워크 파서 퍼징과 GHSA 제보 기록&lt;/a&gt;에 정리했다.&lt;/p&gt;</description>
      <category>보안</category>
      <category>AFL++</category>
      <category>fuzzing</category>
      <category>linux</category>
      <category>setsid</category>
      <category>systemd</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/40</guid>
      <comments>https://gorani-kr.tistory.com/40#entry40comment</comments>
      <pubDate>Tue, 28 Jul 2026 10:59:51 +0900</pubDate>
    </item>
    <item>
      <title>리버싱 과제 보고서에서 AI 티를 걷어내고 증거 중심으로 다시 쓰기</title>
      <link>https://gorani-kr.tistory.com/39</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;cover-n10.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qwPsZ/dJMcabE2r1X/kK53yevaaSpWlTVP70Md71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qwPsZ/dJMcabE2r1X/kK53yevaaSpWlTVP70Md71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qwPsZ/dJMcabE2r1X/kK53yevaaSpWlTVP70Md71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqwPsZ%2FdJMcabE2r1X%2FkK53yevaaSpWlTVP70Md71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-filename=&quot;cover-n10.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메신저 분석 리버싱 과제를 하면서 가장 오래 걸린 건 분석 결과를 보고서로 옮기는 일이었다. AI로 만든 초안은 문장은 매끄러웠지만, 실제로 확인한 것과 추측을 구분하지 못했고 학생 보고서라기보다 홍보 문구처럼 보였다. 결국 문장을 꾸미는 대신 증거 없는 주장을 지우고 다시 썼다. 과제용으로 허가된 대상만 분석했으며 실제 메시지&amp;middot;키&amp;middot;계정&amp;middot;프로토콜 값은 이 글에 싣지 않는다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/dRWbiV/dJMb99NXj5d/AAAAAAAAAAAAAAAAAAAAAJwPPPQKuSICCwkrFuvC3T1-oO6-DXXOFaOIlsGMOh1w/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=vv65cko1UtTApMCc2ZNqpzeuU14%3D&quot; alt=&quot;리버싱 과제 보고서를 AI 문장 대신 증거 중심으로 다시 쓴 과정 대표 이미지&quot; /&gt;
&lt;figcaption&gt;리버싱 과제 보고서를 AI 문장 대신 증거 중심으로 다시 쓴 과정 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;도구를 열기 전에 프로그램 구조부터 봤다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대상은 Windows 데스크톱 클라이언트였다. 먼저 MFC 애플리케이션의 메시지 처리 구조, PE 파일의 섹션과 import, 네트워크 통신 흐름을 정리했다. 배경을 모른 채 문자열과 패킷만 보면 눈에 띄는 값을 의미 있는 증거로 착각하기 쉽다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/u5isU/dJMb99NXj5G/AAAAAAAAAAAAAAAAAAAAAM6nQZ5spojVQ6nx0bke4gaRMt3m5FBirGBlI3MhL9ry/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=saxb9HZ6bry8I0SxxoXkFiWWs00%3D&quot; alt=&quot;Microsoft Learn MFC 데스크톱 애플리케이션 문서&quot; /&gt;
&lt;figcaption&gt;Microsoft Learn MFC 데스크톱 애플리케이션 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보고서 첫 장에는 분석 도구 목록보다 &amp;ldquo;무엇을 확인하려는가&amp;rdquo;를 적었다. 정적 분석에서는 함수와 문자열&amp;middot;import 관계를 보고, 동적 분석에서는 실제 실행 경로와 통신 시점을 확인한다. 한 도구의 결과를 다른 도구가 보완하도록 역할을 정했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ARM Windows와 x86 분석 환경을 억지로 섞지 않았다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 컴퓨터는 ARM 맥이고 Parallels의 Windows도 ARM64다. x86 프로그램 자체는 에뮬레이션으로 실행되지만 동적 후킹 도구가 프로세스 구조를 기대한 방식으로 읽지 못하는 경우가 있었다. 정적 분석과 문서 작업은 맥에서, x86 동적 분석은 별도 x86 Windows PC에서 진행했다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;553&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/AwjuR/dJMb99NXj53/AAAAAAAAAAAAAAAAAAAAAG_yFXZfAJz-_Z6LHqfrYJuaAMWPSlHPMRsvH-79L_aj/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=%2BhnkYWo72bUggVrrSpvXQe3ffDk%3D&quot; alt=&quot;Hex-Rays IDA 공식 문서&quot; /&gt;
&lt;figcaption&gt;Hex-Rays IDA 공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 환경 차이를 실패처럼 숨기지 않고 보고서의 분석 조건에 적었다. 어떤 캡처가 어느 컴퓨터와 아키텍처에서 나온 것인지 표시하니, 같은 절차가 다른 환경에서 재현되지 않을 때 원인을 설명할 수 있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;패킷 캡처는 보이는 값을 전부 공개하는 일이 아니다&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/VPGBJ/dJMcaiqDSWH/AAAAAAAAAAAAAAAAAAAAANa8c0a1BoT4j-txiIB3yh3k_laHvo-fB2hX29J1HDvJ/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=WbOvyEy3J6HZebaCDZvorkuo9zY%3D&quot; alt=&quot;Wireshark 공식 User&amp;rsquo;s Guide&quot; /&gt;
&lt;figcaption&gt;Wireshark 공식 User&amp;rsquo;s Guide&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통신 흐름을 설명하기 위해 Wireshark 캡처를 사용했지만 원본 화면을 그대로 붙이지 않았다. 계정 식별자, 실제 메시지, 토큰&amp;middot;키처럼 과제 설명에 불필요한 값은 가리고, 패킷 방향&amp;middot;길이&amp;middot;시점처럼 주장에 필요한 부분만 남겼다. 캡션에는 &amp;ldquo;이 화면이 무엇을 증명하는가&amp;rdquo;를 한 문장으로 적었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PE 구조는 추측을 줄이는 기준이 됐다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열 주소가 어느 섹션에 있는지, import가 실제로 존재하는지, 디스어셈블 결과가 파일 오프셋과 가상주소 중 무엇을 쓰는지 공식 PE 문서와 대조했다. &amp;ldquo;아마 암호화 함수일 것이다&amp;rdquo;처럼 이름만 보고 쓴 문장은 호출 관계나 실행 캡처가 없으면 삭제했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AI 티를 걷어낸 실제 기준&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초안에는 &amp;ldquo;플랫폼 독립성과 범용성을 증명한다&amp;rdquo;, &amp;ldquo;최종 평가의 핵심 메시지 복원 증명&amp;rdquo; 같은 문장이 있었다. 거창하지만 무엇을 했는지 알 수 없는 표현이었다. 아래 규칙으로 통째로 다시 썼다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;분석했다&amp;rdquo; 뒤에는 파일&amp;middot;함수&amp;middot;패킷&amp;middot;캡처 중 하나의 근거를 붙인다.&lt;/li&gt;
&lt;li&gt;정적 분석에서 추정한 것과 실행으로 확인한 것을 구분한다.&lt;/li&gt;
&lt;li&gt;실패한 시도와 환경 제약도 삭제하지 않는다.&lt;/li&gt;
&lt;li&gt;캡처 번호와 본문 주장이 서로 연결되지 않으면 둘 중 하나를 뺀다.&lt;/li&gt;
&lt;li&gt;&amp;ldquo;완벽히&amp;rdquo;, &amp;ldquo;범용적&amp;rdquo;, &amp;ldquo;증명했다&amp;rdquo; 같은 표현은 범위가 맞는지 다시 본다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주장-증거 표를 먼저 만들었다&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th&gt;주장&lt;/th&gt;
&lt;th&gt;필요한 증거&lt;/th&gt;
&lt;th&gt;상태&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;특정 함수가 해당 흐름에서 호출됨&lt;/td&gt;
&lt;td&gt;정적 호출 관계 + 동적 breakpoint&lt;/td&gt;
&lt;td&gt;둘 다 있으면 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;특정 시점에 통신 발생&lt;/td&gt;
&lt;td&gt;프로세스 동작 + 패킷 시간&lt;/td&gt;
&lt;td&gt;상관관계로 표현&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;보호 기능을 우회함&lt;/td&gt;
&lt;td&gt;전후 실행 결과&lt;/td&gt;
&lt;td&gt;없으면 주장 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표를 만들고 나니 분량이 줄어도 보고서는 더 단단해졌다. 사람이 쓴 것처럼 보이게 만드는 가장 확실한 방법은 말투를 일부러 흐트러뜨리는 게 아니라, 실제로 확인한 과정과 한계를 구체적으로 쓰는 것이었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/cpp/mfc/mfc-desktop-applications&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Microsoft MFC Desktop Applications&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.hex-rays.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Hex-Rays IDA Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wireshark.org/docs/wsug_html_chunked/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Wireshark User&amp;rsquo;s Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/windows/win32/debug/pe-format&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Microsoft PE Format&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최종 보고서 목차도 분석 순서대로 바꿨다&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;범위와 윤리&lt;/b&gt;: 허가된 대상&amp;middot;제외한 데이터&amp;middot;재현 환경&lt;/li&gt;
&lt;li&gt;&lt;b&gt;분석 질문&lt;/b&gt;: 무엇을 확인하려 했는지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;정적 분석&lt;/b&gt;: 파일 정보, import, 함수 관계, 추정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동적 분석&lt;/b&gt;: breakpoint, 실행 경로, 패킷 시점&lt;/li&gt;
&lt;li&gt;&lt;b&gt;증거 연결&lt;/b&gt;: 캡처 번호와 주장 대응&lt;/li&gt;
&lt;li&gt;&lt;b&gt;한계&lt;/b&gt;: 아키텍처&amp;middot;도구&amp;middot;재현 범위&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도구별로 장을 나누면 IDA에서 본 것과 Wireshark에서 본 것이 따로 놀았다. 질문별로 정적&amp;middot;동적 증거를 묶으니 &amp;ldquo;그래서 이 캡처가 무슨 뜻인가&amp;rdquo;가 더 잘 보였다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;캡처 검수 체크리스트&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;창 제목과 시간대가 분석 환경 설명과 맞는가&lt;/li&gt;
&lt;li&gt;주소&amp;middot;함수명&amp;middot;패킷 번호가 본문 인용과 일치하는가&lt;/li&gt;
&lt;li&gt;계정, 메시지, 키, 토큰이 가려졌는가&lt;/li&gt;
&lt;li&gt;강조 박스가 원본 값을 가리거나 왜곡하지 않는가&lt;/li&gt;
&lt;li&gt;캡션만 읽어도 캡처의 증명 범위를 알 수 있는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 초안은 문장 연결에는 도움이 됐지만 실제 캡처 번호와 환경을 대신 확인해주지는 않았다. 마지막 검수는 PDF로 렌더링한 뒤 모든 페이지를 눈으로 넘기며 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같이 읽기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ARM 맥과 x86 Windows 사이에서 실제 동적 분석이 막힌 이유는 &lt;a href=&quot;https://gorani-kr.tistory.com/32&quot;&gt;ARM 맥에서 x86 Windows 리버싱 환경 꾸리기&lt;/a&gt;에 더 자세히 적었다.&lt;/p&gt;</description>
      <category>학습&amp;middot;과제</category>
      <category>IDA</category>
      <category>wireshark</category>
      <category>기술보고서</category>
      <category>리버싱</category>
      <category>증거기반</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/39</guid>
      <comments>https://gorani-kr.tistory.com/39#entry39comment</comments>
      <pubDate>Tue, 28 Jul 2026 10:59:08 +0900</pubDate>
    </item>
    <item>
      <title>EDR 탐지 아이디어를 검증 가능한 결과로 만든 팀 프로젝트 회고</title>
      <link>https://gorani-kr.tistory.com/38</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;cover-n9.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bx7qvV/dJMcaixtk2G/sxJkLk5JJmikq91eubsda1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bx7qvV/dJMcaixtk2G/sxJkLk5JJmikq91eubsda1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bx7qvV/dJMcaixtk2G/sxJkLk5JJmikq91eubsda1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbx7qvV%2FdJMcaixtk2G%2FsxJkLk5JJmikq91eubsda1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-filename=&quot;cover-n9.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안 동아리 4인 팀으로 Linux EDR 에이전트를 만들었다. eBPF로 프로세스&amp;middot;파일&amp;middot;네트워크&amp;middot;메모리&amp;middot;DNS&amp;middot;네임스페이스 이벤트를 관찰하고, 규칙과 이벤트 상관 분석으로 의심 행위를 잡는 프로젝트다. 여기서는 기능을 나열하기보다 &amp;ldquo;탐지 아이디어&amp;rdquo;를 발표에서 검증 가능한 결과로 바꾼 과정을 정리한다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/3Aq6M/dJMcab59Inq/AAAAAAAAAAAAAAAAAAAAAFTq3gXEJhw0LWfhprp19YythnBI0i1p30mlJ_TmGmWA/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=zZ4qOyOsE43XNQUid8JNyMUrUno%3D&quot; alt=&quot;eBPF EDR 탐지 아이디어를 검증 가능한 결과로 만든 팀 프로젝트 대표 이미지&quot; /&gt;
&lt;figcaption&gt;eBPF EDR 탐지 아이디어를 검증 가능한 결과로 만든 팀 프로젝트 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;내 역할부터 구분했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 시스템 영역과 팀 리드를 맡았다. 커널 이벤트를 받아 처리하는 에이전트 쪽, 탐지 규칙 검증과 벤치마크, 격주 보고서와 일정 조율이 내 담당이었다. 프론트엔드&amp;middot;데모 화면&amp;middot;발표자료는 다른 팀원들의 기여가 컸고 최종 발표 준비는 나와 다른 팀원이 함께 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 프로젝트 회고에서 모든 기능을 &amp;ldquo;내가 만들었다&amp;rdquo;고 쓰면 결과는 화려해 보여도 기록의 신뢰가 사라진다. 그래서 저장소 전체 기능과 내가 직접 맡은 부분을 나눠 적었다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/buVIHf/dJMb99NXjZf/AAAAAAAAAAAAAAAAAAAAAKw9lzbf8-DsZJ2sL1DiW3oc2tI7pXrBfaXdnvfGa-4k/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=OrvJmTtUauWuAuJuT7xOFlpL9LA%3D&quot; alt=&quot;공개된 edr-agent GitHub 저장소&quot; /&gt;
&lt;figcaption&gt;공개된 edr-agent GitHub 저장소&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;아이디어에 재현 시나리오를 붙였다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반에는 &amp;ldquo;이런 행위를 탐지하면 좋겠다&amp;rdquo;는 아이디어가 계속 나왔다. 하지만 발표에서 필요한 것은 규칙 이름이 아니라 실제 이벤트가 발생했고 에이전트가 그 규칙을 발화했다는 증거다. 그래서 탐지 항목마다 최소 재현 시나리오를 붙였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 임시 디렉터리에서 실행한 프로세스가 외부 연결을 만드는 흐름이라면, 파일 생성&amp;middot;실행&amp;middot;connect 이벤트가 같은 PID와 시간 창에서 연결되는지 확인한다. 단일 이벤트 한 줄만으로 공격이라고 단정하기보다 여러 이벤트의 순서가 맞을 때 상관 규칙을 발화하도록 했다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bWY7n4/dJMcab59Iny/AAAAAAAAAAAAAAAAAAAAAOWnov6pcEu39u9zSnSvjOpKYFoAuP5gXIytOxdIK35J/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=TfrqRCSGUXmMlYoqH4oBuXXN8p8%3D&quot; alt=&quot;eBPF Docs 공식 문서&quot; /&gt;
&lt;figcaption&gt;eBPF Docs 공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;커널에서 다 하지 않았다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;eBPF 프로그램은 검증기와 실행 비용 제약을 받는다. 커널 쪽에는 이벤트 수집과 최소한의 파싱을 두고, 규칙 매칭&amp;middot;중복 억제&amp;middot;상관 분석&amp;middot;위협 인텔리전스 대조는 유저스페이스로 넘겼다. 현재 공개 저장소 기준으로 6개 BPF 프로그램이 ring buffer를 통해 이벤트를 보내고, 유저스페이스에서 28개 패턴을 처리한다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/eBCbzF/dJMb99NXjZL/AAAAAAAAAAAAAAAAAAAAAISuBv4VWc2y9V_HT--7S7_Q9_-f1AOUYb8aRZXqR-ob/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=ngGV9wWARFeJoGx6%2FaA7D2LteM4%3D&quot; alt=&quot;Linux 커널 BPF 공식 문서&quot; /&gt;
&lt;figcaption&gt;Linux 커널 BPF 공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 분리는 기능을 더 넣기 위해서만 필요한 게 아니었다. 규칙을 YAML로 바꾸고, 정상 프로세스 화이트리스트를 조정하고, 이벤트가 너무 많이 들어올 때 중복 알림을 줄이는 작업을 커널 재빌드 없이 할 수 있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;탐지가 되면 오버헤드도 봐야 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EDR은 한 번 실행하고 끝나는 도구가 아니라 계속 켜두는 프로그램이다. 규칙이 잘 잡혀도 CPU와 메모리 사용량이 크거나 이벤트 손실이 심하면 실사용하기 어렵다. 그래서 탐지 시나리오와 함께 아래 항목을 기록했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기능을 끈 baseline과 에이전트 실행 상태 비교&lt;/li&gt;
&lt;li&gt;이벤트를 많이 발생시켰을 때 CPU&amp;middot;메모리 변화&lt;/li&gt;
&lt;li&gt;ring buffer 손실과 초당 처리량&lt;/li&gt;
&lt;li&gt;같은 재현을 반복했을 때 규칙 발화 일관성&lt;/li&gt;
&lt;li&gt;화이트리스트 적용 전후의 오탐 변화&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;규칙 엔진을 공개된 관점과 비교했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Falco처럼 공개된 런타임 보안 도구가 규칙의 조건&amp;middot;출력&amp;middot;우선순위를 어떻게 분리하는지도 참고했다. 그대로 복제하지 않고, 우리 에이전트의 이벤트 구조에서 설정 파일로 바꿔야 할 값과 코드에 남겨야 할 로직을 구분하는 데 썼다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;격주 보고서가 개발 로그 역할을 했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현이 적었던 회차도 회의에서 정한 역할, 막힌 이유, 다음 검증 항목을 보고서에 남겼다. 발표 직전에는 이 기록이 작업 분배와 누락 확인표가 됐다. 누가 어떤 기능을 맡았고 어떤 증거가 준비됐는지 바로 찾을 수 있었기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트에서 가장 크게 배운 건 탐지 아이디어를 내는 것보다 주장을 재현으로 바꾸는 일이 어렵다는 점이었다. 이벤트를 실제로 만들고, 알림을 확인하고, 오버헤드와 오탐까지 같이 기록해야 비로소 &amp;ldquo;동작한다&amp;rdquo;고 말할 수 있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식&amp;middot;프로젝트 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/no-carve-only-pizza/edr-agent&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;edr-agent GitHub 저장소&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.ebpf.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;eBPF Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/bpf/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Linux Kernel BPF&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://falco.org/docs/concepts/rules/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Falco Rules&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;탐지 규칙 검증 표를 이렇게 남겼다&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;기록 내용&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;재현 명령&lt;/td&gt;
&lt;td&gt;통제된 VM에서 발생시킨 최소 행위&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기대 이벤트&lt;/td&gt;
&lt;td&gt;exec, file, connect 등 커널 이벤트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기대 규칙&lt;/td&gt;
&lt;td&gt;단일 규칙 또는 상관 규칙 ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;관찰 결과&lt;/td&gt;
&lt;td&gt;발화 여부&amp;middot;타임스탬프&amp;middot;PID 연결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;정상 대조군&lt;/td&gt;
&lt;td&gt;같은 도구의 정상 사용에서 오탐 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공격 명령 한 번으로 알림이 떴다고 끝내지 않았다. 정상 업데이트, 디버거, 컨테이너 런타임처럼 비슷한 이벤트를 만드는 대조군을 같이 실행했다. 이 과정에서 &lt;code&gt;sshd&lt;/code&gt;, &lt;code&gt;apt&lt;/code&gt;, &lt;code&gt;dockerd&lt;/code&gt;, &lt;code&gt;gdb&lt;/code&gt; 같은 정상 프로세스의 노이즈를 확인하고 화이트리스트와 상관 조건을 조정했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;벤치마크 숫자를 낼 때 고정한 조건&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;같은 커널&amp;middot;같은 빌드&amp;middot;같은 이벤트 생성 스크립트 사용&lt;/li&gt;
&lt;li&gt;warm-up 뒤 일정 시간 측정&lt;/li&gt;
&lt;li&gt;baseline, 수집만 활성화, 전체 규칙 활성화 세 상태 비교&lt;/li&gt;
&lt;li&gt;평균값뿐 아니라 반복 측정의 변동 범위 기록&lt;/li&gt;
&lt;li&gt;처리량이 올라갈 때 ring buffer 손실도 함께 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자가 예쁘게 나온 한 번만 고르지 않고 조건과 반복 횟수를 보고서에 남겼다. 그래야 다음 팀원이 규칙을 추가한 뒤 같은 방법으로 회귀를 확인할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같이 읽기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탐지와 마찬가지로 퍼징 결과도 주장보다 재현 증거가 중요했다. 공개 제보 과정은 &lt;a href=&quot;https://gorani-kr.tistory.com/15&quot;&gt;AFL++로 네트워크 포렌식 파서를 퍼징해 GHSA까지 간 기록&lt;/a&gt;에 정리했다.&lt;/p&gt;</description>
      <category>보안</category>
      <category>detection</category>
      <category>eBPF</category>
      <category>EDR</category>
      <category>LinuxSecurity</category>
      <category>TeamProject</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/38</guid>
      <comments>https://gorani-kr.tistory.com/38#entry38comment</comments>
      <pubDate>Tue, 28 Jul 2026 10:58:18 +0900</pubDate>
    </item>
    <item>
      <title>흩어진 AI 대화 수백 개를 검색 가능한 개인 지식베이스로 묶기</title>
      <link>https://gorani-kr.tistory.com/37</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Claude, Codex, Cursor 같은 도구를 번갈아 쓰다 보니 대화가 수백 개 쌓였다. 문제는 저장 여부가 아니라 찾는 일이었다. &amp;ldquo;그때 결정한 SSH 방식이 뭐였지?&amp;rdquo;라고 떠올라도 어느 도구에서 언제 얘기했는지 기억이 안 났다. 그래서 대화 로그를 한곳에 모으고, 키워드와 의미 검색을 같이 쓰는 개인 지식베이스를 만들었다. 대화 원문과 개인정보는 공개하지 않고 구조만 정리한다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/OfaaF/dJMb99NXjYY/AAAAAAAAAAAAAAAAAAAAAJSlR2VyLo7Eah7cmDeujR0LudJErGrNl_hBoTAKUkkw/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=dr2cCg6l4ICfT6IwlGRiIC2u10E%3D&quot; alt=&quot;여러 AI 대화를 검색 가능한 개인 지식베이스로 묶은 과정 대표 이미지&quot; /&gt;
&lt;figcaption&gt;여러 AI 대화를 검색 가능한 개인 지식베이스로 묶은 과정 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원본 로그와 현재 결론을 분리했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 export한 대화를 한 폴더에 넣고 전부 색인했다. 검색 결과는 금방 소음투성이가 됐다. 같은 프로젝트 이야기가 날짜마다 반복되고, 중간에 폐기된 결정도 최신 결론처럼 섞였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 문서를 두 층으로 나눴다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;날짜별 세션 로그&lt;/b&gt;: 언제 어떤 도구에서 무슨 일을 했는지 추적하는 원본 기록&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주제별 정리 노트&lt;/b&gt;: 프로젝트의 현재 상태&amp;middot;결정&amp;middot;다음 행동을 한곳에 모은 문서&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;질문이 들어오면 먼저 정리 노트를 보고, 근거가 더 필요할 때만 세션 로그로 내려간다. 로그는 append-only에 가깝게 보존하고, 정리 노트는 최신 상태가 보이도록 갱신한다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/xtpwf/dJMb99NXjY9/AAAAAAAAAAAAAAAAAAAAABBdRQsp-I8TojGcgc13O0IhFZIYbvyl84Pl4TiWfnPa/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=gUzY%2FuESwgRyI0C0F0aJap%2F9rWQ%3D&quot; alt=&quot;Markdown Guide의 기본 문법 문서 &amp;mdash; 노트 저장 형식의 기반&quot; /&gt;
&lt;figcaption&gt;Markdown Guide의 기본 문법 문서 &amp;mdash; 노트 저장 형식의 기반&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;키워드 검색과 의미 검색을 같이 쓴 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트명, 함수명, 파일 경로처럼 정확한 문자열은 키워드 검색이 잘 잡는다. 반대로 &amp;ldquo;원격 작업을 끊어도 계속 돌린 방법&amp;rdquo;처럼 당시 표현을 기억하지 못하는 질문은 임베딩 검색이 유리하다. 한쪽만 쓰면 빈틈이 생겼다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/pfhk7/dJMb99NXjYZ/AAAAAAAAAAAAAAAAAAAAAOMEYbninEjSagrXAEWWuegIUZ2IQXdLz_x3Aiqr7DE-/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=kkDcE2p2KFbzhXOYoL79ZUPT1XQ%3D&quot; alt=&quot;SQLite FTS5 공식 문서 &amp;mdash; 로컬 키워드 검색 계층&quot; /&gt;
&lt;figcaption&gt;SQLite FTS5 공식 문서 &amp;mdash; 로컬 키워드 검색 계층&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키워드 계층은 SQLite FTS5로 두고, 의미 검색 결과와 순위를 합쳤다. 희귀한 고유명사는 FTS 점수를 높게 보고, 자연어로 비슷한 기억을 찾을 때는 임베딩 결과가 보완한다. 인덱스가 망가져도 원본 Markdown은 그대로 남기 때문에 다시 구축할 수 있다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/uAqIw/dJMcab59Inh/AAAAAAAAAAAAAAAAAAAAAKFK1OyNLpelERnCM07uPNxNctOWwuUEL3eCBQ1OqHht/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=lYXUbIKnORjp6y9sAHzU62A1ydA%3D&quot; alt=&quot;Ollama 임베딩 모델 안내 &amp;mdash; 로컬 의미 검색 구성 참고&quot; /&gt;
&lt;figcaption&gt;Ollama 임베딩 모델 안내 &amp;mdash; 로컬 의미 검색 구성 참고&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;너무 잘게 쪼개면 검색이 오히려 나빠졌다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에는 대화 한두 문장마다 청크를 만들었다. 그러면 비슷한 조각이 결과 상단을 점령하고, 하나의 결정이 나온 앞뒤 맥락을 읽기 어려웠다. 이후 세션 요약과 문서 섹션 단위로 압축하고, 결과에는 원문 경로와 날짜를 함께 붙였다. 검색 점수만 보여주는 대신 &amp;ldquo;어느 기록에서 나온 말인지&amp;rdquo;를 바로 열 수 있게 한 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;도구마다 같은 검색 규칙을 적용한다&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/HC5x3/dJMb99NXjY5/AAAAAAAAAAAAAAAAAAAAAIVmA3cctVS2J2QVyUIY7M9wbVSJVaAyMDFB0ZoBhWLq/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=aw%2B4vZdBST%2FoFOVcSXwIuQmdCmk%3D&quot; alt=&quot;Model Context Protocol 공식 문서&quot; /&gt;
&lt;figcaption&gt;Model Context Protocol 공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장소를 하나 만들고 끝내면 새 도구가 그 기억을 사용하지 않는다. 그래서 &amp;ldquo;과거 작업&amp;middot;사람&amp;middot;결정&amp;middot;경로가 필요한 요청이면 먼저 검색한다&amp;rdquo;는 규칙을 문서로 만들고, MCP 도구로 같은 검색 함수를 노출했다. 어느 에이전트를 쓰더라도 같은 정리 노트와 로그를 본다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기억이 충돌할 때의 규칙&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오래된 대화와 현재 결정이 다르면 현재 사용자의 말이 가장 우선이다. 노트에서는 과거 결론을 몰래 지우기보다 언제 무엇으로 바뀌었는지 남긴다. 최신 상태는 문서 상단에서 명확히 표시하고, 이유가 필요하면 변경 이력으로 내려간다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개인 지식베이스에서 더 중요한 것&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비밀번호&amp;middot;쿠키&amp;middot;토큰&amp;middot;복구 코드는 색인 전에 제거한다.&lt;/li&gt;
&lt;li&gt;외부에서 가져온 문서는 출처와 검증 상태를 따로 기록한다.&lt;/li&gt;
&lt;li&gt;검색 결과가 없으면 과거를 추측해 만들지 않는다.&lt;/li&gt;
&lt;li&gt;중요한 결정에는 날짜와 근거를 남긴다.&lt;/li&gt;
&lt;li&gt;인덱스는 파생물로 보고 원본 Markdown을 백업한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 만들고 나니 대화가 많아질수록 부담이 커지는 대신, 이전 시행착오를 다시 쓰는 일이 줄었다. 완벽한 기억 장치라기보다 &amp;ldquo;현재 결론에서 원본까지 되짚을 수 있는 검색 가능한 작업 기록&amp;rdquo;에 가깝다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.sqlite.org/fts5.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;SQLite FTS5&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ollama.com/blog/embedding-models&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Ollama Embedding Models&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://modelcontextprotocol.io/docs/getting-started/intro&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Model Context Protocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.markdownguide.org/basic-syntax/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Markdown Basic Syntax&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제로 둔 디렉터리 역할&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;SecondBrain/
├── 00_Inbox/          # 분류 전 메모
├── 10_Projects/       # 프로젝트별 현재 상태
├── 20_Areas/          # 계속 관리할 영역
├── 30_Knowledge/      # 재사용 지식
├── 40_Decisions/      # 결정과 변경 이력
├── 50_Conversations/  # 날짜별 도구 세션 로그
└── AI_CONTEXT/        # 검색&amp;middot;기억 운영 규칙&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폴더 번호는 중요도 순서가 아니라 문서 역할을 고정하기 위한 장치다. 새 정보가 들어오면 Inbox나 세션 로그에 먼저 남기고, 반복해서 쓸 가치가 생겼을 때만 프로젝트&amp;middot;지식 노트로 승격한다. 모든 대화를 정리 노트로 만들면 또 다른 소음이 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;검색 결과를 답으로 바로 쓰지 않았다&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;query &amp;rarr; FTS 후보 + embedding 후보
      &amp;rarr; 점수 병합&amp;middot;중복 제거
      &amp;rarr; 상위 문서 원문 확인
      &amp;rarr; 최신 상태&amp;middot;날짜&amp;middot;출처 비교
      &amp;rarr; 답변 또는 노트 갱신&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색 결과는 &amp;ldquo;어디를 읽을지&amp;rdquo; 정하는 후보일 뿐이다. 오래된 로그가 최신 정리 노트와 충돌하면 현재 상태를 다시 확인한다. 검색이 비어 있을 때는 기억을 만들어내지 않고 모른다고 남긴다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같이 읽기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 에이전트의 실행 상태 자체를 한 화면에서 본 과정은 &lt;a href=&quot;https://gorani-kr.tistory.com/33&quot;&gt;AI 에이전트 로컬 관제 대시보드 제작기&lt;/a&gt;에 정리했다.&lt;/p&gt;</description>
      <category>개발</category>
      <category>MCP</category>
      <category>Secondbrain</category>
      <category>SQLite</category>
      <category>임베딩</category>
      <category>하이브리드검색</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/37</guid>
      <comments>https://gorani-kr.tistory.com/37#entry37comment</comments>
      <pubDate>Tue, 28 Jul 2026 10:56:49 +0900</pubDate>
    </item>
    <item>
      <title>코드스키 소개</title>
      <link>https://gorani-kr.tistory.com/pages/%EC%BD%94%EB%93%9C%EC%8A%A4%ED%82%A4-%EC%86%8C%EA%B0%9C</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드스키는 제가 직접 만들고 깨뜨리고 다시 고치면서 배운 내용을 기록하는 기술 블로그입니다. 보안, 시스템, 개발 환경처럼 실제로 손을 움직여야 알 수 있는 이야기를 주로 다룹니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;운영자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숭실대학교 AI소프트웨어학부에서 공부하고 있으며, 교내 보안 동아리 ASC와 화이트햇스쿨 4기에서 활동하고 있습니다. Linux 시스템 보안, 취약점 분석, 퍼징, 리버싱과 보안 제품 개발에 관심이 많습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주로 쓰는 내용&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오픈소스 퍼징과 책임 있는 취약점 제보&lt;/li&gt;
&lt;li&gt;펌웨어&amp;middot;리버싱&amp;middot;CTF 분석 기록&lt;/li&gt;
&lt;li&gt;Spring Boot와 시스템 프로그래밍 프로젝트&lt;/li&gt;
&lt;li&gt;macOS, Linux, Windows 개발 환경에서 겪은 문제 해결&lt;/li&gt;
&lt;li&gt;보안 행사와 도구를 직접 써본 후기&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원칙&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 확인하지 않은 결과를 경험한 것처럼 쓰지 않습니다. 명령어와 설정은 가능한 한 다시 재현하고, 공식 문서와 원문 링크를 함께 남깁니다. 진행 중인 대회 문제나 아직 공개할 수 없는 취약점 정보는 올리지 않습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;링크&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/no-carve-only-pizza&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.linkedin.com/in/%EB%8F%84%ED%98%84-%EB%B0%95-6a3035407&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gorani-kr.tistory.com/rss&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RSS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/pages/%EC%BD%94%EB%93%9C%EC%8A%A4%ED%82%A4-%EC%86%8C%EA%B0%9C</guid>
      <pubDate>Mon, 27 Jul 2026 16:05:28 +0900</pubDate>
    </item>
    <item>
      <title>가민 Fenix 7에 KBO 실시간 점수 띄우는 워치페이스 만들기</title>
      <link>https://gorani-kr.tistory.com/35</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;가민 Fenix 7 Pro Solar를 쓰는데, 야구 볼 때 폰을 매번 꺼내는 게 귀찮았다. 손목에서 우리 팀 점수가 실시간으로 보이면 좋겠다 싶어서 워치페이스를 직접 만들기로 했다. 만들다 보니 워치 플랫폼의 제약이 생각보다 빡빡해서, 워치 혼자로는 안 되고 폰을 끼워야 했던 이야기다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/dVwZR4/dJMcaccTHRC/AAAAAAAAAAAAAAAAAAAAANLxd85pAY6bBSqFfwY1EVQlRZ-JRlHrExqYxcHTs_Y7/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=O6TiNBFsOtDYDD6TpHLhXnWpbm0%3D&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FdVwZR4%2FdJMcaccTHRC%2FAAAAAAAAAAAAAAAAAAAAANLxd85pAY6bBSqFfwY1EVQlRZ-JRlHrExqYxcHTs_Y7%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1785509999%26allow_ip%3D%26allow_referer%3D%26signature%3DO6TiNBFsOtDYDD6TpHLhXnWpbm0%253D&quot; alt=&quot;Garmin Fenix 7 KBO 실시간 점수 워치페이스 제작기 대표 이미지&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; /&gt;
&lt;figcaption&gt;Garmin Fenix 7 KBO 실시간 점수 워치페이스 제작기 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Connect IQ 워치페이스의 기본&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가민 워치 앱은 Connect IQ SDK로 만든다. 워치페이스는 그중에서도 시계 화면 자체를 그리는 종류인데, 시간&amp;middot;날짜 같은 기본 정보에 원하는 걸 얹어서 그린다. SDK를 설치하고 대상 기기를 fenix7pro로 잡은 다음, 시간과 날짜를 그리는 기본 골격부터 세웠다. 여기까지는 순조로웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;900&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/RH7Y0/dJMcahZx359/AAAAAAAAAAAAAAAAAAAAACYa-X0Cie_fn_nImam3f290zkan9NCm485UYWZvno7_/img.jpg?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=%2Bf4iQFQl7%2FAyYmTBNimPb7z5zE8%3D&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FRH7Y0%2FdJMcahZx359%2FAAAAAAAAAAAAAAAAAAAAACYa-X0Cie_fn_nImam3f290zkan9NCm485UYWZvno7_%2Fimg.jpg%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1785509999%26allow_ip%3D%26allow_referer%3D%26signature%3D%252Bf4iQFQl7%252FAyYmTBNimPb7z5zE8%253D&quot; alt=&quot;Garmin Fenix 7 Pro Solar 실제 착용 화면&quot; width=&quot;1200&quot; height=&quot;900&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;900&quot; /&gt;
&lt;figcaption&gt;Garmin Fenix 7 Pro Solar 실제 착용 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 &quot;실시간 점수&quot;였다. 워치페이스가 스스로 인터넷에서 점수를 계속 긁어오게 만들고 싶었는데, 여기서 플랫폼 제약에 부딪혔다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5분 실행 제한이라는 벽&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Connect IQ 워치페이스는 백그라운드 실행 시간이 제한된다. 대략 5분 단위의 제약이 있어서, 워치페이스가 혼자 계속 네트워크를 폴링하면서 실시간 데이터를 받아오는 구조를 만들 수가 없다. 배터리를 아끼려는 플랫폼의 의도라 이해는 되는데, 실시간 스코어보드를 원하는 입장에선 벽이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 방향을 틀었다. 워치가 직접 데이터를 가져오는 대신, 폰이 데이터를 가져와서 워치로 밀어주는 구조로 갔다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;안드로이드 컴패니언으로 우회&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폰 쪽에 안드로이드 컴패니언 앱을 붙였다. 이 앱이 15초마다 라이브 점수를 폴링하고, 경기가 진행 중일 때 그 값을 워치로 &lt;code&gt;sendMessage&lt;/code&gt;를 통해 전송한다. 워치페이스는 폰에서 오는 메시지를 받는 수신부만 두고, 받은 점수를 화면에 그린다. 즉 무거운 네트워크 작업은 폰이 하고, 워치는 표시만 담당한다. 이렇게 나누니 5분 제한을 우회하면서도 손목에서 거의 실시간에 가까운 점수를 볼 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경기 중이 아닐 때는 굳이 야구 UI를 띄우지 않게 했다. 경기가 시작된 상태(STARTED)일 때만 스코어를 표시하고, 평소에는 일반 시계 화면으로 둔다. 응원팀 점수는 눈에 띄게 강조 색으로 처리했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;화면 디자인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기왕 새로 만드는 김에 기존 페이스를 그대로 복제하기보다 내 취향대로 다시 디자인했다. 상단에 한글 요일과 날짜를 넣고, 강조 색 헤더, 정리된 데이터 그리드, 분 단위 진행 링을 얹었다. 경기 중에는 그 위에 라이브 스코어 라인이 뜬다. 가독성을 손목에서 확인하면서 몇 번 조정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/cTHQ3F/dJMcahFfosX/AAAAAAAAAAAAAAAAAAAAANgS65q86_mZDDPe7oJc8Qf1O3txNZxzNlNBSBTJNTIv/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=uI2fm7vDWvbhLn11wCqTAmMfF1Y%3D&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FcTHQ3F%2FdJMcahFfosX%2FAAAAAAAAAAAAAAAAAAAAANgS65q86_mZDDPe7oJc8Qf1O3txNZxzNlNBSBTJNTIv%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1785509999%26allow_ip%3D%26allow_referer%3D%26signature%3DuI2fm7vDWvbhLn11wCqTAmMfF1Y%253D&quot; alt=&quot;Garmin Connect IQ 공식 사이트&quot; width=&quot;2800&quot; height=&quot;1800&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; /&gt;
&lt;figcaption&gt;Garmin Connect IQ 공식 사이트&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사이드로드에서 또 막힘&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다 만들고 워치에 올리는 단계에서 마지막 함정이 있었다. macOS에서 Fenix 7을 USB로 꽂아도 &lt;code&gt;/Volumes/GARMIN&lt;/code&gt; 같은 일반 저장장치로 마운트되지 않았다. 이 기기는 대용량 저장장치가 아니라 MTP 방식으로 붙기 때문이다. 그래서 파인더로는 파일을 못 넣고, OpenMTP나 libmtp 같은 MTP 도구를 써야 했다. 가민 Express가 떠 있으면 충돌하니 완전히 종료한 다음, 빌드한 &lt;code&gt;.prg&lt;/code&gt; 파일을 워치의 &lt;code&gt;GARMIN/Apps/&lt;/code&gt; 경로에 복사하는 식으로 사이드로드했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/1rvgk/dJMcahFfos5/AAAAAAAAAAAAAAAAAAAAAKLTH0u4Frw9H2jB0hT6p_9S-6Yt6a6cUtv6RwMkmRRr/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=jZfQL3nZiuekqzUZIq5fM7RuJjU%3D&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2F1rvgk%2FdJMcahFfos5%2FAAAAAAAAAAAAAAAAAAAAAKLTH0u4Frw9H2jB0hT6p_9S-6Yt6a6cUtv6RwMkmRRr%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1785509999%26allow_ip%3D%26allow_referer%3D%26signature%3DjZfQL3nZiuekqzUZIq5fM7RuJjU%253D&quot; alt=&quot;OpenMTP &amp;mdash; macOS에서 Garmin 워치를 MTP 기기로 인식해 파일을 옮기는 도구&quot; width=&quot;2800&quot; height=&quot;1800&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; /&gt;
&lt;figcaption&gt;OpenMTP &amp;mdash; macOS에서 Garmin 워치를 MTP 기기로 인식해 파일을 옮기는 도구&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 워치페이스 하나 만드는데 넘어야 할 벽이 세 개였다. 워치 플랫폼의 5분 실행 제한(폰 컴패니언으로 우회), 실시간성(15초 폴링 + sendMessage), 그리고 macOS에서의 MTP 사이드로드. 손목에서 점수 보는 소소한 목표였는데, 플랫폼 제약을 이해하고 역할을 나누는 좋은 연습이 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덧붙이면 점수 데이터를 어디서 가져오는지는 그 출처의 이용 조건을 확인하고 쓰는 게 맞다. 개인용으로 손목에 띄우는 것과 재배포는 다른 문제다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Garmin Connect IQ &amp;mdash; Watch Face, 백그라운드 실행 제약&lt;/li&gt;
&lt;li&gt;Connect IQ 앱 간 메시지 전달(&lt;code&gt;sendMessage&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;macOS에서 MTP 기기 접근(OpenMTP/libmtp), &lt;code&gt;.prg&lt;/code&gt; 사이드로드 경로&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.garmin.com/connect-iq/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Garmin Connect IQ 공식 사이트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://openmtp.ganeshrvel.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;OpenMTP&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;워치&amp;middot;폰 메시지 API 확인&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bdA3rc/dJMcafHxIZM/AAAAAAAAAAAAAAAAAAAAAMfvEc220JemNp-O_IDHgsed46t71fOaapkRIJwQBth1/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=JjXmOsEeed1oQapH4WbyTXmqEDM%3D&quot; alt=&quot;Garmin Connect IQ Toybox.Communications API 문서&quot; /&gt;
&lt;figcaption&gt;Garmin Connect IQ Toybox.Communications API 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴패니언 구조를 잡을 때는 워치 앱과 모바일 쪽이 어떤 메시지 형식을 주고받는지 Communications API에서 다시 확인했다. 폰이 점수를 가져오고 워치는 받은 상태만 그리는 경계를 유지하면 워치페이스의 실행 시간과 배터리 제약을 덜 건드릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.garmin.com/connect-iq/api-docs/Toybox/Communications.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Toybox.Communications&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발</category>
      <category>Android</category>
      <category>ConnectIQ</category>
      <category>Garmin</category>
      <category>KBO</category>
      <category>watchface</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/35</guid>
      <comments>https://gorani-kr.tistory.com/35#entry35comment</comments>
      <pubDate>Mon, 27 Jul 2026 15:12:21 +0900</pubDate>
    </item>
    <item>
      <title>바이브코딩으로 만든 중고거래 서비스를 제출 가능한 수준까지 보안 강화하기</title>
      <link>https://gorani-kr.tistory.com/34</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;시큐어코딩 수업 과제로 중고거래 플랫폼을 만들었다. 처음엔 AI한테 대충 시켜서 빠르게 굴러가는 걸 만들었는데(요즘 말로 바이브코딩), 막상 제출하려고 다시 보니 &quot;이거 그대로 내면 안 되겠다&quot; 싶은 게 잔뜩 보였다. 최소 요구사항은 채웠지만 보안 약점이 여기저기 있었고, UI와 문서도 대충 짜맞춘 티가 났다. 그걸 하나씩 뜯어고쳐 제출 가능한 상태로 만든 과정이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWMwEb/dJMcaixsVZb/0QJ1dS5ljzRV2SyN0Dcujk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWMwEb/dJMcaixsVZb/0QJ1dS5ljzRV2SyN0Dcujk/img.png&quot; data-alt=&quot;Spring Boot 중고거래 서비스 시큐어코딩 개선 과정 대표 이미지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWMwEb/dJMcaixsVZb/0QJ1dS5ljzRV2SyN0Dcujk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWMwEb%2FdJMcaixsVZb%2F0QJ1dS5ljzRV2SyN0Dcujk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Spring Boot 중고거래 서비스 시큐어코딩 개선 과정 대표 이미지&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Spring Boot 중고거래 서비스 시큐어코딩 개선 과정 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스택과 최초 상태&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 구성은 Spring Boot 3, Java 21, Thymeleaf, Spring Security, JPA다. 상품 등록&amp;middot;조회, 사용자, 관심상품, 그리고 CarrotCoin이라는 모의 지갑 기능이 있었다. 지갑은 실제 가상자산 연동이 아니라 모의 결제 흐름이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;750&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyh3CH/dJMcafneCIj/VPC4E06XcJG43KpZ0p2bx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyh3CH/dJMcafneCIj/VPC4E06XcJG43KpZ0p2bx1/img.png&quot; data-alt=&quot;Spring Boot로 구현한 중고거래 서비스 UsedCarrot 홈 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyh3CH/dJMcafneCIj/VPC4E06XcJG43KpZ0p2bx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcyh3CH%2FdJMcafneCIj%2FVPC4E06XcJG43KpZ0p2bx1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Spring Boot로 구현한 중고거래 서비스 UsedCarrot 홈 화면&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;750&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;750&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Spring Boot로 구현한 중고거래 서비스 UsedCarrot 홈 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 기능은 슬라이드의 최소 요구사항 대비 대체로 구현돼 있었다. 문제는 디테일이었다. 상세 페이지의 버튼이 역할별로 분리가 안 돼 있어서 누가 눌러도 같은 동작이 나오고, 마이페이지와 관심상품 사이 내비게이션이 끊겨 있었다. 초기에 만든 요구사항 문서(SRS 등)와 실제 구현이 어긋나 있었고, 판매자 상태를 바꾸는 드롭다운이 필요 이상으로 노출돼 있었다. 이런 게 전형적인 바이브코딩 냄새다. 돌아가긴 하는데, 왜 이렇게 됐는지 설명이 안 되는 부분들.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;보안 약점을 찾아 고치기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과제의 진짜 목표는 &quot;개발 과정에서 확인한 보안 약점이 무엇이고 어떻게 고쳤는가&quot;를 보이는 거였다. 그래서 대충 고치지 않고 제대로 파고들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 신경 쓴 건 권한과 접근 제어였다. 판매자 상태 변경 같은 민감한 동작이 UI에 과하게 노출돼 있으면, 화면에서 안 보이게 하는 걸로는 부족하다. 서버 쪽에서 그 요청을 할 권한이 있는지 실제로 검증해야 한다. Spring Security를 쓰고 있으니 인증&amp;middot;인가 규칙을 다시 잡고, 화면 노출과 서버 검증을 분리했다. 화면에서 버튼을 숨기는 건 편의일 뿐이고, 진짜 방어는 요청을 처리하는 컨트롤러/서비스 계층에 있어야 한다는 원칙을 지켰다. Spring Security의 메서드 시큐리티(&lt;code&gt;@PreAuthorize&lt;/code&gt; 등)를 서비스 계층에 붙여서, 컨트롤러를 우회해서 호출되더라도 같은 권한 검증을 통과해야 하게 만들었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;900&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjNHZZ/dJMcahL2jiD/SNokB96TtKda3wURtkOvr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjNHZZ/dJMcahL2jiD/SNokB96TtKda3wURtkOvr0/img.png&quot; data-alt=&quot;역할별로 분리한 상품 상세 화면 &amp;amp;mdash; 구매자 시점에서는 판매자 상태 변경 UI가 노출되지 않음&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjNHZZ/dJMcahL2jiD/SNokB96TtKda3wURtkOvr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjNHZZ%2FdJMcahL2jiD%2FSNokB96TtKda3wURtkOvr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;역할별로 분리한 상품 상세 화면 &amp;mdash; 구매자 시점에서는 판매자 상태 변경 UI가 노출되지 않음&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;900&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;900&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;역할별로 분리한 상품 상세 화면 &amp;mdash; 구매자 시점에서는 판매자 상태 변경 UI가 노출되지 않음&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버튼 역할 미분리처럼 보이는 UI 문제도 사실 보안과 연결된다. 하나의 동작이 여러 맥락에서 재사용되면 의도치 않은 경로로 상태가 바뀔 수 있다. 역할을 나누고, 각 동작이 필요한 검증을 거치게 정리했다. 모의 지갑/온체인 결제 흐름도 값 검증과 상태 전이를 다시 봤다. 모의라도 결제는 결제라, 금액이나 상태를 신뢰할 수 없는 입력으로 다뤄야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;900&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dh9vgj/dJMcahL2jiH/73tUZAq5J54kd4F7THyepK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dh9vgj/dJMcahL2jiH/73tUZAq5J54kd4F7THyepK/img.png&quot; data-alt=&quot;Sepolia 테스트넷 기반 모의 지갑 결제 내역 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dh9vgj/dJMcahL2jiH/73tUZAq5J54kd4F7THyepK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdh9vgj%2FdJMcahL2jiH%2F73tUZAq5J54kd4F7THyepK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Sepolia 테스트넷 기반 모의 지갑 결제 내역 화면&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;900&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;900&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Sepolia 테스트넷 기반 모의 지갑 결제 내역 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전반적으로 &quot;믿을 수 없는 입력&quot;이라는 관점을 코드 전체에 적용했다. 사용자가 보내는 값, URL 파라미터, 폼 데이터를 그대로 믿지 않고 서버에서 다시 검증하는 것. 시큐어코딩 수업의 핵심이 결국 이거였다. 관리자 대시보드 쪽에는 감사 로그(audit log)도 남기게 했는데, 로그인&amp;middot;로그아웃&amp;middot;지갑 연결&amp;middot;메시지 전송 같은 이벤트를 시간&amp;middot;사용자&amp;middot;결과와 함께 기록해두면 나중에 &quot;누가 언제 무엇을 했는지&quot;를 서버 쪼기 없이 바로 확인할 수 있다는 게 실무적으로 크게 느껴졌다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;900&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5Fk2v/dJMcahL2jiL/dfEEAqyfR6RaBmlFvEFdCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5Fk2v/dJMcahL2jiL/dfEEAqyfR6RaBmlFvEFdCk/img.png&quot; data-alt=&quot;관리자 대시보드 &amp;amp;mdash; 사용자/상품/신고/지갑 거래/감사 로그 관리&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5Fk2v/dJMcahL2jiL/dfEEAqyfR6RaBmlFvEFdCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5Fk2v%2FdJMcahL2jiL%2FdfEEAqyfR6RaBmlFvEFdCk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;관리자 대시보드 &amp;mdash; 사용자/상품/신고/지갑 거래/감사 로그 관리&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;900&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;900&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;관리자 대시보드 &amp;mdash; 사용자/상품/신고/지갑 거래/감사 로그 관리&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1070&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yqBoR/dJMcacRr1U6/8dLu2rj0tc7YoIwPG6QPQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yqBoR/dJMcacRr1U6/8dLu2rj0tc7YoIwPG6QPQK/img.png&quot; data-alt=&quot;로그인&amp;amp;middot;지갑 연결&amp;amp;middot;메시지 전송 이벤트를 기록하는 감사 로그(Audit Log)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yqBoR/dJMcacRr1U6/8dLu2rj0tc7YoIwPG6QPQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyqBoR%2FdJMcacRr1U6%2F8dLu2rj0tc7YoIwPG6QPQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;로그인&amp;middot;지갑 연결&amp;middot;메시지 전송 이벤트를 기록하는 감사 로그(Audit Log)&quot; loading=&quot;lazy&quot; width=&quot;1070&quot; height=&quot;1440&quot; data-origin-width=&quot;1070&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;로그인&amp;middot;지갑 연결&amp;middot;메시지 전송 이벤트를 기록하는 감사 로그(Audit Log)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xhzGs/dJMcacRr1Vm/KM2ZcvGrCBeDou0uunLsu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xhzGs/dJMcacRr1Vm/KM2ZcvGrCBeDou0uunLsu0/img.png&quot; data-alt=&quot;Spring Security 공식 문서 &amp;amp;mdash; Method Security&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xhzGs/dJMcacRr1Vm/KM2ZcvGrCBeDou0uunLsu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxhzGs%2FdJMcacRr1Vm%2FKM2ZcvGrCBeDou0uunLsu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Spring Security 공식 문서 &amp;mdash; Method Security&quot; loading=&quot;lazy&quot; width=&quot;2800&quot; height=&quot;1800&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Spring Security 공식 문서 &amp;mdash; Method Security&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;증거 기반 보고서 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과제는 GitHub 공개 저장소에 코드를 올리고 README에 실행 방법을 명시하는 것, 그리고 개발 전 과정과 보안 약점 수정 내역을 담은 보고서를 제출하는 것이었다. 나는 보고서를 말로만 채우지 않고 증거로 채우고 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 각 기능과 수정 지점을 실제로 로그인해서 화면을 캡처하고, 정상 관리자 화면&amp;middot;결제 흐름&amp;middot;지갑 연결 같은 걸 스크린샷으로 남겼다. 그걸 표지&amp;middot;목차&amp;middot;아키텍처&amp;middot;결제 흐름&amp;middot;표&amp;middot;캡션&amp;middot;페이지 번호까지 통일해서 22쪽짜리 제출용 PDF로 만들었다. 자동 캡처에서 생긴 긴 빈 배경은 PDF 전용 사본에서 잘라내고, 원본 스크린샷은 건드리지 않았다. 캡처가 실제로 렌더링되는지, 이미지가 깨지지 않는지 전 페이지를 눈으로 검수했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하고 나니 보고서가 &quot;했다고 주장하는 문서&quot;가 아니라 &quot;한 걸 보여주는 문서&quot;가 됐다. 바이브코딩으로 빠르게 시작하는 건 좋지만, 제출까지 가려면 결국 왜 이렇게 만들었는지를 하나하나 설명할 수 있어야 한다는 걸 이 과제로 배웠다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Security &amp;mdash; 인증/인가와 메서드 보안&lt;/li&gt;
&lt;li&gt;OWASP &amp;mdash; 서버 사이드 입력 검증, 접근 제어&lt;/li&gt;
&lt;li&gt;시큐어코딩 관점의 &quot;신뢰할 수 없는 입력&quot; 원칙&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같이 읽기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검증하지 않은 입력이 실제 명령 실행 취약점으로 이어진 사례는 &lt;a href=&quot;https://gorani-kr.tistory.com/14&quot;&gt;ipTIME N2V CVE-2025-55423 Command Injection 분석&lt;/a&gt;에 정리했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/reference/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring Security Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring Security &amp;mdash; Method Security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;OWASP Authorization Cheat Sheet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;OWASP Input Validation Cheat Sheet&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발</category>
      <category>Java</category>
      <category>Project</category>
      <category>securecoding</category>
      <category>springboot</category>
      <category>WebSecurity</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/34</guid>
      <comments>https://gorani-kr.tistory.com/34#entry34comment</comments>
      <pubDate>Mon, 27 Jul 2026 15:11:31 +0900</pubDate>
    </item>
    <item>
      <title>AI 에이전트 4종을 한 화면에서 보려고 로컬 관제 대시보드를 만들었다</title>
      <link>https://gorani-kr.tistory.com/33</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 코딩 에이전트를 여러 개 동시에 쓴다. Codex, Claude Code, Cursor, Antigravity를 상황에 따라 갈아타는데, 문제는 지금 어느 세션이 일하는 중이고 어느 세션이 내 입력을 기다리는지 한눈에 안 보인다는 거였다. 창을 하나씩 열어서 확인하는 게 은근히 성가셔서, 이 네 개를 한 화면에 픽셀 사무실처럼 띄워주는 로컬 대시보드를 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/oSFZX/dJMcagsQaRn/AAAAAAAAAAAAAAAAAAAAAACFeTAUfKBGQlz08HpQfYAU2Xz2Vbz1OS8U919N7FEu/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=rIV4v8bY0bwCHa8ZwVEwN%2FAn7Co%3D&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FoSFZX%2FdJMcagsQaRn%2FAAAAAAAAAAAAAAAAAAAAAACFeTAUfKBGQlz08HpQfYAU2Xz2Vbz1OS8U919N7FEu%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1785509999%26allow_ip%3D%26allow_referer%3D%26signature%3DrIV4v8bY0bwCHa8ZwVEwN%252FAn7Co%253D&quot; alt=&quot;AI 에이전트 4종 로컬 관제 대시보드 대표 이미지&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; /&gt;
&lt;figcaption&gt;AI 에이전트 4종 로컬 관제 대시보드 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;모델 호출도 훅도 없이, 로그만 읽는다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설계에서 처음 정한 원칙은 &quot;읽기 전용&quot;이었다. 각 에이전트한테 훅을 심거나 API를 호출하지 않는다. 대신 이 도구들이 로컬에 남기는 JSONL 세션 파일을 그냥 읽는다. Codex는 Codex대로, Claude Code는 Claude대로, Cursor와 Antigravity도 각자 세션 기록을 파일로 떨군다. 대시보드는 그 파일들을 주기적으로 파싱해서 각 세션의 현재 상태만 뽑아 픽셀 화면에 배치한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식의 장점은 에이전트 동작에 전혀 개입하지 않는다는 거다. 모델을 부르지 않으니 토큰도 안 먹고, 훅이 없으니 에이전트가 느려지거나 깨질 일도 없다. 순수하게 관찰만 하는 계층이다. localhost의 작은 포트 하나에 웹 화면을 띄워서 브라우저로 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;750&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/Bj4Ft/dJMcacYhEMh/AAAAAAAAAAAAAAAAAAAAAIwgPtmtGSZOo4FamIEnfZ5O_yK_TKuIejVWGlZSa9L_/img.jpg?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=zX2Tq27UuImqpYGK61iEVMxX8i4%3D&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FBj4Ft%2FdJMcacYhEMh%2FAAAAAAAAAAAAAAAAAAAAAIwgPtmtGSZOo4FamIEnfZ5O_yK_TKuIejVWGlZSa9L_%2Fimg.jpg%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1785509999%26allow_ip%3D%26allow_referer%3D%26signature%3DzX2Tq27UuImqpYGK61iEVMxX8i4%253D&quot; alt=&quot;Codex, Claude, Cursor, Antigravity 세션을 한 화면에 표시한 Agent Control Room 실행 화면&quot; width=&quot;1200&quot; height=&quot;750&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;750&quot; /&gt;
&lt;figcaption&gt;Codex, Claude, Cursor, Antigravity 세션을 한 화면에 표시한 Agent Control Room 실행 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;working / waiting / done을 구분하는 게 핵심이자 함정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대시보드의 가치는 결국 상태 분류의 정확도에서 나온다. 세션이 셋 중 뭔지를 맞혀야 한다. 지금 실제로 작업 중(working)인지, 내 명령이나 승인을 기다리는(waiting) 상태인지, 아니면 끝난(done) 상태인지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 이게 다 틀렸다. 끝난 세션도, 유휴 상태인 세션도 전부 &quot;대기중(waiting)&quot;으로 떴다. 원인을 파보니 각 도구의 세션 로그를 끝에서부터 읽어 마지막 이벤트로 상태를 판정하는데, 그 판정 조건이 엉성했다. 예를 들어 어떤 세션은 마지막 기록이 도구 호출(tool_use)에서 끊겨 있었다. 실제로는 작업이 끝났는데, 로그만 보면 &quot;도구를 부르다 멈춘&quot; 상태라 대시보드가 &quot;아직 뭔가 하는 중&quot; 또는 &quot;대기 중&quot;으로 오해한 거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 각 도구별 파서의 분류 조건을 손봤다. 실제로 사용자 입력이나 권한 승인을 기다리는 interactive 상태만 waiting으로 잡고, 완료된 세션은 done으로 매핑했다. &lt;code&gt;stop_reason&lt;/code&gt;이나 마지막 이벤트 타입을 도구마다 다르게 해석해야 해서, 도구별로 조건을 따로 뒀다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;tool_use에서 끊긴 트랜스크립트를 위한 idle 휴리스틱&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 남는 애매한 경우가 있었다. Cursor 세션이 tool_use로 끊긴 채 남아 있으면, 이게 진짜 지금 도구를 실행하는 중인지 아니면 그냥 그 상태로 끝나버린 건지 로그만으로는 구분이 안 됐다. 마지막 이벤트가 같으니까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기엔 시간 기반 휴리스틱을 넣었다. 마지막 이벤트로부터 일정 시간(나는 5분으로 뒀다)이 지나도록 아무 변화가 없으면 done으로 본다는 규칙이다. 대략 이런 형태다.&lt;/p&gt;
&lt;pre style=&quot;background: #1e1e1e; color: #d4d4d4; padding: 12px; border-radius: 6px; overflow-x: auto;&quot;&gt;&lt;code&gt;CURSOR_ACTIVE_IDLE_SEC = 300  # 5분

def classify_cursor_session(last_event, now):
    if last_event.type != &quot;tool_use&quot;:
        return status_from_event(last_event)
    idle = now - last_event.timestamp
    if idle &amp;gt; CURSOR_ACTIVE_IDLE_SEC:
        return &quot;done&quot;   # tool_use에서 끊겼지만 충분히 오래 조용했다면 종료로 간주
    return &quot;working&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드에 &lt;code&gt;CURSOR_ACTIVE_IDLE_SEC&lt;/code&gt; 같은 임계값을 두고, tool_use로 끊긴 트랜스크립트라도 그만큼 idle이면 완료로 처리했다. 이걸 넣고 나서야 실제로 도는 세션 하나만 working으로 남고 나머지는 제대로 done으로 정리됐다. 테스트 케이스를 몇 개 만들어서 이 분류가 흔들리지 않는지 확인했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로그인하면 알아서 떠 있게&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 이걸 매번 손으로 실행하는 게 귀찮아서 자동 실행을 걸었다. macOS라 launchd로 로그인 시 자동으로 서버가 뜨게 해두고, 자동 실행용 사본을 별도 위치에 뒀다. 이제 컴퓨터를 켜면 대시보드가 알아서 떠 있고, 브라우저 탭 하나로 네 에이전트의 상태를 훑는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/d5a80g/dJMb991A16I/AAAAAAAAAAAAAAAAAAAAAG54sg8jjXb-rfgYzzmyagbqS_e88sa2O_9sZakqQBQh/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=n7NmkiVtSYAEsA43Inb3i1JG6F8%3D&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2Fd5a80g%2FdJMb991A16I%2FAAAAAAAAAAAAAAAAAAAAAG54sg8jjXb-rfgYzzmyagbqS_e88sa2O_9sZakqQBQh%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1785509999%26allow_ip%3D%26allow_referer%3D%26signature%3Dn7NmkiVtSYAEsA43Inb3i1JG6F8%253D&quot; alt=&quot;Apple Developer 공식 문서 &amp;mdash; launchd로 로그인 시 데몬/에이전트 자동 실행 설정&quot; width=&quot;2800&quot; height=&quot;1800&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; /&gt;
&lt;figcaption&gt;Apple Developer 공식 문서 &amp;mdash; launchd로 로그인 시 데몬/에이전트 자동 실행 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만들고 나서 느낀 건, 이런 관제 도구의 어려움은 데이터를 모으는 게 아니라 &quot;상태를 어떻게 정의하느냐&quot;에 있다는 거였다. working과 done의 경계, waiting의 진짜 의미, 로그가 중간에 끊겼을 때의 처리. 결국 도메인(각 에이전트가 로그를 남기는 방식)을 정확히 이해해야 분류가 맞아떨어졌다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;launchd로 macOS 로그인 시 자동 실행 등록&lt;/li&gt;
&lt;li&gt;각 에이전트 CLI의 로컬 세션 JSONL 구조(도구별 상이)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같이 읽기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 AI 도구의 역할과 비용 차이를 정리한 글은 &lt;a href=&quot;https://gorani-kr.tistory.com/24&quot;&gt;ChatGPT Plus, GitHub Copilot, Google Colab Pro 뭐가 달라?&lt;/a&gt;에서 이어진다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/overview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Claude Code 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://openai.com/codex/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;OpenAI Codex&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Apple Developer &amp;mdash; Creating Launch Daemons and Agents (launchd)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/cApdy9/dJMb991A16B/AAAAAAAAAAAAAAAAAAAAANbt0XFERdqER56JaRrJNBWUGYm45lNsnd-t49pevBGY/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=o0Q3TYyEnwhhuzkRzD1VpNrlFtA%3D&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FcApdy9%2FdJMb991A16B%2FAAAAAAAAAAAAAAAAAAAAANbt0XFERdqER56JaRrJNBWUGYm45lNsnd-t49pevBGY%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1785509999%26allow_ip%3D%26allow_referer%3D%26signature%3Do0Q3TYyEnwhhuzkRzD1VpNrlFtA%253D&quot; alt=&quot;Claude Code 공식 문서 홈&quot; width=&quot;2800&quot; height=&quot;1800&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; /&gt;
&lt;figcaption&gt;Claude Code 공식 문서 홈&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Codex 쪽 공식 문서&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/ddKgYZ/dJMcadpiGnn/AAAAAAAAAAAAAAAAAAAAAOObPQ7E1icu7OK33ZQn_lxZj9dWwjLd7gRpG9_MYbXT/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=pp%2BXyLKhhsglzpG9hT6Xjnq2H8g%3D&quot; alt=&quot;OpenAI Codex 공식 개발자 문서&quot; /&gt;
&lt;figcaption&gt;OpenAI Codex 공식 개발자 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 에이전트의 상태를 한 화면에 모으더라도, 지원 기능과 실행 모델은 공식 문서가 바뀔 때 함께 갱신해야 한다. 대시보드는 제품 기능을 대신 설명하는 게 아니라 로컬 프로세스 상태를 관찰하는 도구로 범위를 제한했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.openai.com/codex/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;OpenAI Codex 문서&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발</category>
      <category>ai</category>
      <category>codex</category>
      <category>cursor</category>
      <category>Dashboard</category>
      <category>Tooling</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/33</guid>
      <comments>https://gorani-kr.tistory.com/33#entry33comment</comments>
      <pubDate>Mon, 27 Jul 2026 15:09:57 +0900</pubDate>
    </item>
    <item>
      <title>ARM 맥에서 x86 윈도우 리버싱 환경 꾸리기 (Parallels의 한계)</title>
      <link>https://gorani-kr.tistory.com/32</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;맥북을 메인으로 쓰면서 보안 공부를 하다 보면, 리버싱 실습에서 벽에 부딪힌다. 분석 대상이 x86 윈도우 프로그램인데 내 맥은 ARM이다. Parallels로 윈도우를 돌려도 그게 ARM 윈도우라, x86 동적 분석이 매끄럽지 않다. 처음엔 &quot;에뮬레이션이 다 되는 거 아닌가&quot; 싶었는데, 실제로 후킹 도구를 붙여보면서 왜 안 되는지, 그리고 결국 어떻게 환경을 정리했는지를 남긴다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Parallels ARM 윈도우의 한계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parallels로 애플 실리콘 맥에 윈도우를 깔면 ARM64 버전의 윈도우가 깔린다. 일상적인 윈도우 앱은 x86 에뮬레이션으로 그럭저럭 돌아가지만, 보안 공부에서 하는 동적 분석은 얘기가 다르다. Frida 같은 후킹 도구로 x86 프로세스에 붙어서 함수 호출을 가로채고 메모리를 들여다보려고 하면, 아키텍처가 어긋나서 잘 안 되는 경우가 많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유를 찾아보니 단순히 &quot;에뮬레이션이라 느려서&quot;가 아니라 구조 자체의 문제였다. Windows 11 on Arm은 x86/x64 코드를 실행할 때 &lt;b&gt;Prism&lt;/b&gt;이라는 에뮬레이터가 x86 명령어 블록을 그때그때 Arm64 명령어로 JIT 컴파일한다. 이렇게 번역된 코드 블록은 캐시에 저장돼서, 같은 코드를 다시 실행할 때는 번역을 또 하지 않고 캐시된 걸 재사용한다. x86 쪽 시스템 콜과 파일/레지스트리 접근은 &lt;a href=&quot;https://learn.microsoft.com/en-us/windows/arm/apps-on-arm-x86-emulation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;WOW64 레이어&lt;/a&gt;가 맡아서, x64용 WOW64가 x86 코드를 감싸는 것과 비슷한 방식으로 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 Frida 같은 동적 계측 도구가 하는 일이 이 캐시 구조와 정면으로 부딪힌다는 거다. Frida의 &lt;code&gt;Interceptor.attach&lt;/code&gt;는 원래 대상 함수의 진입부에 트램폴린(작은 점프 코드)을 심어서 실행 흐름을 가로챈다. 그런데 이게 네이티브 x86 위에서는 &quot;그 함수가 메모리 어디에 있고, 그 자리의 바이트를 어떻게 바꿀지&quot;가 명확한 반면, 에뮬레이션 위에서는 Frida가 보는 코드와 실제로 CPU가 실행하는 코드가 다른 레이어에 있다. Frida는 x86 프로세스의 가상 메모리에 패치를 심지만, 실제 실행은 이미 Arm64로 번역되어 캐시된 블록 위에서 일어난다. 패치 시점과 캐시 갱신 시점이 어긋나면 후킹이 아예 안 걸리거나, 걸렸다가도 캐시가 다시 채워지면서 무효화되는 일이 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정적 분석은 그나마 낫다. IDA나 Ghidra로 바이너리를 열어서 어셈블리를 읽는 건 파일을 디스크에서 읽어서 해석하는 작업이라 아키텍처를 가리지 않는다. 맥에서도 똑같이 된다. 하지만 실제로 프로그램을 실행시켜서 런타임 동작을 관찰하는 x86 동적 분석은, 캐시&amp;middot;번역 레이어가 끼어들지 않는 진짜 x86 환경이 필요했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/P5kon/dJMb998iQGH/QbP4apaoI1IW9RVBr8Tq7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/P5kon/dJMb998iQGH/QbP4apaoI1IW9RVBr8Tq7K/img.png&quot; data-alt=&quot;ARM 맥 x86 윈도우 리버싱 환경 구성 대표 이미지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/P5kon/dJMb998iQGH/QbP4apaoI1IW9RVBr8Tq7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FP5kon%2FdJMb998iQGH%2FQbP4apaoI1IW9RVBr8Tq7K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;ARM 맥 x86 윈도우 리버싱 환경 구성 대표 이미지&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ARM 맥 x86 윈도우 리버싱 환경 구성 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;945&quot; data-origin-height=&quot;213&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRLMrk/dJMcag7pH1w/kSpn40vI9lXTzyxSrhQs1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRLMrk/dJMcag7pH1w/kSpn40vI9lXTzyxSrhQs1K/img.png&quot; data-alt=&quot;Windows Task Manager 세부 정보 탭 Architecture 열에 표시된 ARM64 (x64 compatible) &amp;amp;mdash; 출처: Microsoft Learn&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRLMrk/dJMcag7pH1w/kSpn40vI9lXTzyxSrhQs1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRLMrk%2FdJMcag7pH1w%2FkSpn40vI9lXTzyxSrhQs1K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Windows Task Manager 세부 정보 탭 Architecture 열에 표시된 ARM64 (x64 compatible) &amp;mdash; 출처: Microsoft Learn&quot; loading=&quot;lazy&quot; width=&quot;945&quot; height=&quot;213&quot; data-origin-width=&quot;945&quot; data-origin-height=&quot;213&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Windows Task Manager 세부 정보 탭 Architecture 열에 표시된 ARM64 (x64 compatible) &amp;mdash; 출처: Microsoft Learn&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Task Manager로 에뮬레이션 여부 확인하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 눈으로 확인하는 방법이 있다. 윈도우 Task Manager의 세부 정보 탭에는 Architecture 열이 있는데, 네이티브 Arm64 앱은 그냥 &lt;code&gt;ARM64&lt;/code&gt;로 뜨고 x86/x64 앱이 에뮬레이션으로 돌아가고 있으면 &lt;code&gt;ARM64 (x64 compatible)&lt;/code&gt; 같은 식으로 표시된다. 이 열을 한 번 띄워보면 지금 돌리고 있는 프로세스가 진짜 네이티브인지, 에뮬레이션 위에서 돌고 있는지가 바로 보인다. 리버싱 대상 프로그램을 실행해놓고 이 열을 확인하는 걸 습관으로 들이면, &quot;지금 내가 보는 게 진짜 x86 실행인가&quot;를 헷갈리지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 근본적인 문제도 있다. 마이크로소프트 문서에 따르면 에뮬레이션은 &lt;b&gt;유저 모드 코드만 지원하고 드라이버는 지원하지 않는다.&lt;/b&gt; 커널 모드 컴포넌트는 반드시 Arm64로 네이티브 컴파일되어야 한다. 그런데 커널 레벨 후킹이나 드라이버 기반 안티치트 분석, 커널 디버거를 쓰는 종류의 분석은 이 조건 때문에 ARM 윈도우 위에서는 원천적으로 막힌다. 유저 모드 후킹이 캐시 문제로 불안정한 것과는 또 다른, 더 단단한 벽이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 x86은 집 PC에서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론은 x86이 꼭 필요한 작업은 집에 있는 x86 윈도우 PC에서 하는 거였다. 지뢰찾기 동적 분석이나 x86 메신저 프로그램 분석 같은 건 그 PC에서 돌렸다. 문제는 그러면 분석 파일이 집 컴퓨터에만 남는다는 거다. 다른 작업은 다 맥에서 하는데 이것만 물리적으로 다른 기계에 묶여 있으니, 매번 옮겨서 관리하기가 번거로웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 파일 동기화로 풀었다. 집 PC와 맥 사이에 분석 파일을 오가게 해서, 어느 쪽에서 작업하든 최신 파일을 볼 수 있게 했다. 큰 파일이 오갈 때는 홈 네트워크가 빠르니 그쪽에서 처리하는 게 유리했다. 핵심은 &quot;x86 실행은 x86 기계에서, 문서&amp;middot;정리&amp;middot;정적 분석은 맥에서&quot;라는 역할 분리를 명확히 하고, 그 사이를 동기화로 잇는 것이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;864&quot; data-origin-height=&quot;579&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Grhd4/dJMcadiEdWA/hZk5LkT4GYQShZ09RpJ7NK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Grhd4/dJMcadiEdWA/hZk5LkT4GYQShZ09RpJ7NK/img.png&quot; data-alt=&quot;Parallels Desktop으로 Apple Silicon 맥에 Windows 11 설치를 완료한 화면 &amp;amp;mdash; 출처: Parallels 공식 KB&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Grhd4/dJMcadiEdWA/hZk5LkT4GYQShZ09RpJ7NK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGrhd4%2FdJMcadiEdWA%2FhZk5LkT4GYQShZ09RpJ7NK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Parallels Desktop으로 Apple Silicon 맥에 Windows 11 설치를 완료한 화면 &amp;mdash; 출처: Parallels 공식 KB&quot; loading=&quot;lazy&quot; width=&quot;864&quot; height=&quot;579&quot; data-origin-width=&quot;864&quot; data-origin-height=&quot;579&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Parallels Desktop으로 Apple Silicon 맥에 Windows 11 설치를 완료한 화면 &amp;mdash; 출처: Parallels 공식 KB&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1358&quot; data-origin-height=&quot;1203&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ttqm1/dJMcadCKAWB/ASQsvTgbybyzTFuhpGFF7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ttqm1/dJMcadCKAWB/ASQsvTgbybyzTFuhpGFF7k/img.png&quot; data-alt=&quot;Parallels Control Center에 나란히 뜬 Windows 11과 macOS 가상머신 &amp;amp;mdash; 출처: Parallels 공식 KB&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ttqm1/dJMcadCKAWB/ASQsvTgbybyzTFuhpGFF7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fttqm1%2FdJMcadCKAWB%2FASQsvTgbybyzTFuhpGFF7k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Parallels Control Center에 나란히 뜬 Windows 11과 macOS 가상머신 &amp;mdash; 출처: Parallels 공식 KB&quot; loading=&quot;lazy&quot; width=&quot;1358&quot; height=&quot;1203&quot; data-origin-width=&quot;1358&quot; data-origin-height=&quot;1203&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Parallels Control Center에 나란히 뜬 Windows 11과 macOS 가상머신 &amp;mdash; 출처: Parallels 공식 KB&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실전에서 부딪혀 본 사례 &amp;mdash; 고전 지뢰찾기 패치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 환경 문제를 가장 확실하게 느낀 건 수업 과제로 받은 고전 지뢰찾기(winmine.exe)를 패치할 때였다. 32비트 PE로 된 오래된 프로그램이라 요즘 프로그램과 달리 난독화나 패킹이 없어서, 리버싱 입문 대상으로는 오히려 좋았다. 목표는 &quot;지뢰를 눌러도 지지 않게&quot; 승패 분기를 무력화하는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접근은 정적 분석과 동적 분석을 같이 썼다. 디스어셈블러로 코드를 열어서 칸을 클릭했을 때 호출되는 흐름을 따라가며, 지뢰 여부를 검사하고 게임 오버로 이어지는 조건 분기를 어셈블리에서 찾았다. 그다음 실제로 프로그램을 실행하면서 디버거로 그 지점에 브레이크포인트를 걸고, 칸을 눌렀을 때 정말 그 경로로 가는지 대조했다. 정적으로 짐작한 분기가 실제 실행에서도 그 자리인지 확인하는 과정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 이 동적 확인 단계에서 ARM 윈도우의 한계를 체감했다. 브레이크포인트를 걸고 실행 흐름을 관찰하는 작업이 에뮬레이션 위에서는 캐시된 번역 블록 때문에 예상과 다르게 동작하는 경우가 있었고, 그래서 이 검증은 결국 x86 PC로 옮겨서 다시 했다. 분기를 특정한 뒤에는 그 지점의 바이트 몇 개를 바꿔서 패배 경로를 타지 않게 만들고, 패치 전후로 실제 실행 결과를 대조해서 내가 건드린 지점이 정말 그 로직이었다는 걸 증명했다. 분석 대상은 수업에서 허가된 과제용 바이너리였고, 여기서는 접근 방법과 환경 문제를 남기는 게 목적이라 패치 바이트나 파일 자체는 다루지 않는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;도구 환경을 분리해서 관리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리버싱 도구는 종류가 많고 서로 요구하는 게 다르다. 정리하면서 쓴 기준은 이렇다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정적 분석(IDA, Ghidra, 문자열/PE 헤더 확인) &amp;mdash; 아키텍처 무관, 맥에서 처리&lt;/li&gt;
&lt;li&gt;동적 계측(Frida, 후킹, 런타임 트레이싱) &amp;mdash; 네이티브 x86 환경 필요, 집 PC에서 처리&lt;/li&gt;
&lt;li&gt;디버거 기반 브레이크포인트/스텝 실행 &amp;mdash; 캐시 영향을 받을 수 있어 x86 PC에서 처리&lt;/li&gt;
&lt;li&gt;언패커, 특수 목적 도구 &amp;mdash; 대상에 따라 다르므로 그때그때 확인 후 배치&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 한 환경에 다 섞으면 충돌하거나 어느 게 어디서 되는지 헷갈린다. &quot;이 도구는 어느 환경에서 쓰는가&quot;를 먼저 정하고, 그 기준대로 도구를 배치하는 걸로 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;돌아보면 맥 사용자가 보안 공부를 하려면 이 x86 문제를 한 번은 정면으로 겪게 된다. ARM 윈도우로 다 될 것 같지만 동적 분석에서 막히고, 그 원인이 단순 성능 문제가 아니라 JIT 번역 캐시와 유저 모드 한정 에뮬레이션이라는 구조적인 이유라는 걸 알고 나면, 오히려 왜 진짜 x86 환경이 필요한지가 명확해진다. 처음부터 &quot;정적은 맥, 동적 x86은 x86 기계, 그 사이는 동기화&quot;라는 그림을 잡아두면 시행착오를 줄일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 실습에 쓴 분석 대상은 수업에서 과제용으로 제공되고 허가된 것들이었다. 환경을 어떻게 꾸렸는지가 이 글의 요지고, 특정 프로그램의 분석 결과나 제공된 바이너리 자체를 배포하려는 건 아니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Apple Silicon에서 Parallels ARM Windows와 x86/x64 에뮬레이션(Prism)&lt;/li&gt;
&lt;li&gt;Frida 등 동적 계측 도구가 JIT 번역 캐시와 부딪히는 지점&lt;/li&gt;
&lt;li&gt;정적 분석(IDA/Ghidra) vs 동적 분석 환경 분리, 커널 모드 에뮬레이션 미지원&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같이 읽기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맥에서 윈도우 PC를 원격 분석 환경으로 쓸 때 겪은 키 입력 문제는 &lt;a href=&quot;https://gorani-kr.tistory.com/26&quot;&gt;Parsec으로 맥&amp;rarr;윈도우 원격접속할 때 Cmd+W 누르면 파섹이 꺼지는 문제&lt;/a&gt;에 따로 정리했다. 퍼징으로 찾은 크래시를 실제로 제보까지 이어간 과정은 &lt;a href=&quot;https://gorani-kr.tistory.com/31&quot;&gt;오픈소스 취약점을 GitHub에 책임 있게 제보하기&lt;/a&gt;에 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://kb.parallels.com/125375&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Parallels: Windows 11 on Apple silicon Mac&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/windows/arm/apps-on-arm-x86-emulation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Microsoft Learn &amp;mdash; How emulation works on Arm (Prism, WOW64)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/windows/arm/arm64ec&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Microsoft Learn &amp;mdash; Arm64EC for Windows 11 apps on Arm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://frida.re/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Frida &amp;mdash; Dynamic instrumentation toolkit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pNbfM/dJMcahrONtJ/Q5wcTfYz4D9bh1tU5dV591/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pNbfM/dJMcahrONtJ/Q5wcTfYz4D9bh1tU5dV591/img.png&quot; data-alt=&quot;Frida 공식 홈페이지 &amp;amp;mdash; 동적 계측 도구 소개&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pNbfM/dJMcahrONtJ/Q5wcTfYz4D9bh1tU5dV591/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpNbfM%2FdJMcahrONtJ%2FQ5wcTfYz4D9bh1tU5dV591%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;Frida 공식 홈페이지 &amp;mdash; 동적 계측 도구 소개&quot; loading=&quot;lazy&quot; width=&quot;2800&quot; height=&quot;1800&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Frida 공식 홈페이지 &amp;mdash; 동적 계측 도구 소개&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>환경설정</category>
      <category>ARM</category>
      <category>Frida</category>
      <category>macos</category>
      <category>reversing</category>
      <category>windows</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/32</guid>
      <comments>https://gorani-kr.tistory.com/32#entry32comment</comments>
      <pubDate>Mon, 27 Jul 2026 15:06:57 +0900</pubDate>
    </item>
    <item>
      <title>오픈소스 취약점을 GitHub에 책임 있게 제보하기 &amp;mdash; PVR이 없을 때 겪은 일들</title>
      <link>https://gorani-kr.tistory.com/31</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;퍼징으로 크래시를 재현하고 근본 원인까지 특정하고 나면, 그다음이 진짜 시작이다. 이걸 어디에 어떻게 알릴 것인가. 나는 네트워크 파서 몇 개에서 메모리 버그를 찾아 GitHub Security Advisory와 공개 이슈로 제보했는데, 그 과정에서 저장소마다 제보 창구가 제각각이라는 걸 몸으로 배웠다. 정리해두면 나중의 나나 비슷한 걸 처음 해보는 사람한테 도움이 될 것 같아서 남긴다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비공개 제보(PVR)와 공개 이슈는 다른 트랙이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub에는 Private Vulnerability Reporting(PVR)이라는 기능이 있다. 저장소의 Security 탭에서 &quot;Report a vulnerability&quot; 버튼을 누르면 메인테이너하고만 보이는 비공개 advisory 초안을 만들 수 있다. 이게 활성화돼 있으면 취약점을 공개하기 전에 조용히 수정할 시간을 줄 수 있어서, 책임 있는 제보의 정석 경로다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 이 기능이 저장소마다 켜져 있기도 하고 아니기도 하다는 점이다. 내가 제보하려던 프로젝트 중 하나는 PVR이 잘 켜져 있어서 advisory를 비공개로 제출했고, 다른 두 개는 &lt;code&gt;/security/advisories/new&lt;/code&gt;에 접속하면 404가 떴다. 더 헷갈렸던 건, 그중 하나는 SECURITY.md 파일에 &quot;GitHub의 Security reporting 기능을 사용하라&quot;고 안내까지 해뒀는데 실제로는 그 기능이 꺼져 있었다는 거다. 문서와 실제 설정이 어긋난 상태였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ScTmR/dJMb99ULVit/0qLT5pnakoKQ9ItuOTI2FK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ScTmR/dJMb99ULVit/0qLT5pnakoKQ9ItuOTI2FK/img.png&quot; data-alt=&quot;오픈소스 취약점 책임 있는 제보 절차 대표 이미지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ScTmR/dJMb99ULVit/0qLT5pnakoKQ9ItuOTI2FK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FScTmR%2FdJMb99ULVit%2F0qLT5pnakoKQ9ItuOTI2FK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;오픈소스 취약점 책임 있는 제보 절차 대표 이미지&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;오픈소스 취약점 책임 있는 제보 절차 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3200&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A6PRw/dJMcaiEdCOZ/ttYwzug8B5bKf1A99i0eGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A6PRw/dJMcaiEdCOZ/ttYwzug8B5bKf1A99i0eGk/img.png&quot; data-alt=&quot;GitHub 공식 문서 &amp;amp;mdash; 보안 취약성 비공개 보고(PVR)는 SECURITY.md와 별개 기능&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A6PRw/dJMcaiEdCOZ/ttYwzug8B5bKf1A99i0eGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA6PRw%2FdJMcaiEdCOZ%2FttYwzug8B5bKf1A99i0eGk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;GitHub 공식 문서 &amp;mdash; 보안 취약성 비공개 보고(PVR)는 SECURITY.md와 별개 기능&quot; loading=&quot;lazy&quot; width=&quot;3200&quot; height=&quot;1800&quot; data-origin-width=&quot;3200&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;GitHub 공식 문서 &amp;mdash; 보안 취약성 비공개 보고(PVR)는 SECURITY.md와 별개 기능&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 얻은 첫 교훈은 이거다. SECURITY.md만 믿지 말고, 제보 전에 &lt;code&gt;/security/advisories/new&lt;/code&gt; URL로 직접 들어가서 폼이 실제로 뜨는지 확인해라. 폼이 안 뜨면 PVR이 꺼진 거고, 이때는 선택지가 갈린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PVR이 없는 저장소는 결국 공개 GitHub Issue로 갈 수밖에 없다. 다만 이건 순간부터 전 세계에 공개되는 거라, 심각도를 먼저 냉정하게 따져야 한다. 내가 찾은 건 전부 읽기 기반(OOB read)의 저심각 DoS라 원격 코드 실행 같은 위험이 없었고, 그래서 공개 이슈로 올려도 악용 위험보다 수정 유도 효과가 크다고 판단했다. 만약 쓰기 primitive나 RCE 가능성이 조금이라도 보였다면 공개 이슈는 절대 안 됐을 거고, 메인테이너 이메일이나 CNA를 통한 비공개 경로를 찾았어야 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;750&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c59bzN/dJMcafneCIK/F7ktzQLb3aDmAvgCkvAfQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c59bzN/dJMcafneCIK/F7ktzQLb3aDmAvgCkvAfQk/img.png&quot; data-alt=&quot;PVR이 없는 오픈소스 프로젝트에 GitHub 이슈로 취약점을 제보한 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c59bzN/dJMcafneCIK/F7ktzQLb3aDmAvgCkvAfQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc59bzN%2FdJMcafneCIK%2FF7ktzQLb3aDmAvgCkvAfQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;PVR이 없는 오픈소스 프로젝트에 GitHub 이슈로 취약점을 제보한 화면&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;750&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;750&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;PVR이 없는 오픈소스 프로젝트에 GitHub 이슈로 취약점을 제보한 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;advisory 폼을 채우는 실무 팁&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PVR로 advisory를 쓸 때 몇 가지 편의 기능이 있다. Severity 항목에서 &quot;Assess severity using CVSS&quot;를 고르면 벡터 문자열만 입력해도 Low/Moderate/High 등급과 점수를 자동으로 계산해준다. 내 경우 원격에서 서비스를 죽일 수 있는 건 &lt;code&gt;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H&lt;/code&gt;로 7.5가 나왔고, 파일을 열어야 트리거되는 로컬 파서 버그는 그보다 낮은 5.5대가 나왔다. 벡터를 손으로 계산하지 말고 이 기능에 맡기는 게 정확하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4000&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Qy7T7/dJMcahegtS5/d809Skd4Nh2fkU0m2VyKG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Qy7T7/dJMcahegtS5/d809Skd4Nh2fkU0m2VyKG0/img.png&quot; data-alt=&quot;FIRST.org CVSS v3.1 계산기 &amp;amp;mdash; 실제 제보에 쓴 벡터로 7.5(High) 산출&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Qy7T7/dJMcahegtS5/d809Skd4Nh2fkU0m2VyKG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQy7T7%2FdJMcahegtS5%2Fd809Skd4Nh2fkU0m2VyKG0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;FIRST.org CVSS v3.1 계산기 &amp;mdash; 실제 제보에 쓴 벡터로 7.5(High) 산출&quot; loading=&quot;lazy&quot; width=&quot;4000&quot; height=&quot;1800&quot; data-origin-width=&quot;4000&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;FIRST.org CVSS v3.1 계산기 &amp;mdash; 실제 제보에 쓴 벡터로 7.5(High) 산출&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dOwkBB/dJMcaalNqsr/cukwR1Oht7nwHgUcdNYG9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dOwkBB/dJMcaalNqsr/cukwR1Oht7nwHgUcdNYG9k/img.png&quot; data-alt=&quot;CWE-125: Out-of-bounds Read 공식 정의 다이어그램 &amp;amp;mdash; MITRE CWE&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dOwkBB/dJMcaalNqsr/cukwR1Oht7nwHgUcdNYG9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdOwkBB%2FdJMcaalNqsr%2FcukwR1Oht7nwHgUcdNYG9k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;CWE-125: Out-of-bounds Read 공식 정의 다이어그램 &amp;mdash; MITRE CWE&quot; loading=&quot;lazy&quot; width=&quot;2800&quot; height=&quot;1800&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;CWE-125: Out-of-bounds Read 공식 정의 다이어그램 &amp;mdash; MITRE CWE&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CWE 필드는 번호로 검색하면 자동완성이 뜬다. OOB read면 &quot;CWE-125&quot;를 치면 바로 후보가 나온다. 분류를 정확히 달아두면 메인테이너가 이해하기 훨씬 빠르다. CWE-125 공식 정의를 보면 &quot;의도한 버퍼의 끝(또는 시작) 너머의 데이터를 읽는다&quot;는 게 핵심이고, 그 결과로 비인가 데이터 노출&amp;middot;크래시&amp;middot;예기치 못한 동작으로 이어진다고 명시돼 있다. 내가 겪은 DoS(크래시)가 정확히 이 흐름에 들어맞았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PoC 첨부에서 한 번 막혔다. GitHub 코멘트에 &lt;code&gt;.pcap&lt;/code&gt; 파일을 그대로 올리려니까 &quot;File type not supported&quot;라고 거부당했다. 바이너리 확장자 일부는 직접 첨부가 안 된다. 이건 &lt;code&gt;.zip&lt;/code&gt;으로 압축해서 올리면 통과한다. 압축한 다음에는 원본과 압축본의 SHA-256을 코멘트에 같이 적어뒀다. 나중에 메인테이너가 받은 파일이 내가 보낸 것과 같은지 검증할 수 있게 하려는 목적이었고, 실제로 첨부가 업로드 완료 전에 코멘트가 먼저 게시돼서 링크가 깨진 적이 있어서 재업로드 후 해시를 다시 확인해야 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;보고서에 꼭 담아야 하는 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 쓴 제보 초안은 대체로 이런 뼈대였다. 영향받는 버전과 커밋 해시, 취약점 클래스(CWE), 요약, 재현 절차, 취약 코드의 파일&amp;middot;라인과 한 줄짜리 근본 원인, 최소화한 PoC의 크기와 해시, 그리고 영향 범위. 마지막에 &quot;이건 OOB read이고 DoS이며 RCE는 아니다&quot;를 명시적으로 박아뒀다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 마지막 한 줄이 생각보다 중요하다. 심각도를 부풀리면 메인테이너 신뢰를 잃는다. 보안 커뮤니티에서 과장은 금방 티가 나고, 한 번 &quot;이 사람 오버한다&quot;는 인상을 주면 그다음 제보가 안 읽힌다. 오히려 &quot;여기까지는 확인했고 여기부터는 확인 못 했다&quot;를 정확히 적는 게 훨씬 신뢰를 준다. 패치 제안이 있으면 검증된 한 줄 diff까지 같이 제시하고, &quot;원하시면 PR 보내겠다&quot;고 열어두면 대화가 매끄럽게 이어진다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과와 남은 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비공개로 제출한 쪽은 메인테이너가 검토 후 advisory를 published로 전환하면서 제보자 크레딧을 accepted 해줬고, 공개 이슈로 올린 쪽도 정상적으로 접수됐다. 한쪽은 이미 수정 커밋까지 반영됐다. CVE 번호는 CNA/메인테이너 판단 영역이라 아직 배정 대기 상태다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;돌이켜보면 제보라는 게 그냥 &quot;버그 찾았어요&quot;를 보내는 일이 아니었다. 어느 창구가 열려 있는지 확인하고, 심각도를 정직하게 매기고, 상대가 검증할 수 있는 형태로 증거를 포장하고, 과장 없이 쓰는 것. 크래시를 재현하는 것보다 이쪽이 훨씬 더 &quot;실무&quot;에 가까웠다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같이 읽기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 AFL++로 크래시를 찾고 GHSA 제보까지 이어간 과정은 &lt;a href=&quot;https://gorani-kr.tistory.com/15&quot;&gt;AFL++로 네트워크 포렌식 파서 퍼징해서 GHSA 10건 제보&amp;middot;게재까지&lt;/a&gt;에 정리했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/ko/code-security/how-tos/report-and-fix-vulnerabilities/report-privately&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GitHub Docs &amp;mdash; 보안 취약성 비공개 보고&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.first.org/cvss/calculator/3.1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;FIRST.org &amp;mdash; CVSS v3.1 Calculator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cwe.mitre.org/data/definitions/125.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CWE-125: Out-of-bounds Read&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>보안</category>
      <category>cve</category>
      <category>disclosure</category>
      <category>GHSA</category>
      <category>github</category>
      <category>Security</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/31</guid>
      <comments>https://gorani-kr.tistory.com/31#entry31comment</comments>
      <pubDate>Mon, 27 Jul 2026 15:06:11 +0900</pubDate>
    </item>
    <item>
      <title>Boolean Blind SQLi로 한 글자씩 데이터 뽑아내기 &amp;mdash; WHS 워게임 5단계</title>
      <link>https://gorani-kr.tistory.com/30</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;화이트햇스쿨 2주차 웹해킹 기초 시간에 멘토님이 워게임 링크를 하나 공유했다. whs.sakuya.kr, Blind SQL Injection 1단계부터 5단계까지 풀어보는 사이트였다. 로그인부터 하고 시작하는 구조였는데, 단계가 올라갈수록 필터가 하나씩 더 붙는 식이라 처음엔 쉽다가 뒤로 갈수록 손이 많이 갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blind SQLi는 에러 메시지나 데이터를 직접 화면에 보여주지 않는 쿼리 인젝션이다. 대신 참/거짓에 따라 화면이 다르게 나오거나 응답 시간이 달라지는 걸 이용해서 데이터를 한 글자씩 추측해나간다. 이번 워게임은 Boolean 기반이라 응답 시간이 아니라 화면에 뜨는 결과(로그인 성공/실패, 참/거짓 메시지)가 참거짓 신호였다. 예를 들어 &lt;code&gt;admin' AND SUBSTRING(password,1,1)='a&lt;/code&gt; 같은 조건을 걸어서 결과가 참으로 나오면 비밀번호 첫 글자가 a라는 걸 알 수 있는 식이다. 이걸 알파벳과 숫자 조합으로 26~62번씩, 비밀번호 길이만큼 반복하면 전체 값을 뽑아낼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/cL9TF7/dJMcaixsYr1/AAAAAAAAAAAAAAAAAAAAAEUDY8zKEkDhrSe-ZUOsoAJzO7N_u9AExT2oFwUTm-VC/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=p8dl9v4w%2Fyf%2Bat0Jp%2FGWtdSrc2Q%3D&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FcL9TF7%2FdJMcaixsYr1%2FAAAAAAAAAAAAAAAAAAAAAEUDY8zKEkDhrSe-ZUOsoAJzO7N_u9AExT2oFwUTm-VC%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1785509999%26allow_ip%3D%26allow_referer%3D%26signature%3Dp8dl9v4w%252Fyf%252Bat0Jp%252FGWtdSrc2Q%253D&quot; alt=&quot;Boolean Blind SQLi 워게임 대표 이미지&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; /&gt;
&lt;figcaption&gt;Boolean Blind SQLi 워게임 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1~2단계: 교과서대로 되는 구간&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1~2단계는 필터가 거의 없어서 교과서에 나오는 방식 그대로 통했다. PortSwigger Academy의 설명과 거의 똑같은 원리다. 조건이 참인 요청과 거짓인 요청을 각각 보내서 응답이 어떻게 달라지는지만 확인하면 된다. 아래 이미지의 예시처럼 &lt;code&gt;AND '1'='1&lt;/code&gt;은 참이라 정상 응답이 오고, &lt;code&gt;AND '1'='2&lt;/code&gt;는 거짓이라 다른 응답이 온다. 이 참/거짓 한 비트를 반복해서 물어보는 게 Blind SQLi의 전부다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;2600&quot; data-origin-height=&quot;2160&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/H3a6V/dJMcaixsYr9/AAAAAAAAAAAAAAAAAAAAAFFSsMQop81vWES1aMUyu2yGgdu6WjwrOCxD9tTj67I7/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=Lgfh%2BZu%2BT7pM3mUoF34%2FI74lkKw%3D&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FH3a6V%2FdJMcaixsYr9%2FAAAAAAAAAAAAAAAAAAAAAFFSsMQop81vWES1aMUyu2yGgdu6WjwrOCxD9tTj67I7%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1785509999%26allow_ip%3D%26allow_referer%3D%26signature%3DLgfh%252BZu%252BT7pM3mUoF34%252FI74lkKw%253D&quot; alt=&quot;PortSwigger Academy &amp;mdash; 조건에 따라 응답이 달라지는 Blind SQLi 예시 (공식 문서)&quot; width=&quot;2600&quot; height=&quot;2160&quot; data-origin-width=&quot;2600&quot; data-origin-height=&quot;2160&quot; /&gt;
&lt;figcaption&gt;PortSwigger Academy &amp;mdash; 조건에 따라 응답이 달라지는 Blind SQLi 예시 (공식 문서)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3단계부터: 필터를 피해가는 표현 찾기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 3단계부터였다. &lt;code&gt;or&lt;/code&gt;라는 단어 자체를 막아놔서 &lt;code&gt;||&lt;/code&gt;로 바꿔야 했고, &lt;code&gt;=&lt;/code&gt; 기호도 막혀서 &lt;code&gt;IN&lt;/code&gt;으로 우회해야 했다. 이런 WAF 우회는 처음엔 감이 안 왔는데, 결국 SQL 문법에서 같은 의미를 다르게 표현하는 방법을 찾는 게임이라는 걸 알고 나니 패턴이 보이기 시작했다. &lt;code&gt;AND&lt;/code&gt; 대신 &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;, &lt;code&gt;SUBSTRING&lt;/code&gt; 대신 &lt;code&gt;MID&lt;/code&gt;나 &lt;code&gt;SUBSTR&lt;/code&gt;처럼 동의어가 있는 함수들도 마찬가지였다. 아래는 실제로 참고한 PortSwigger의 SQL Injection Cheat Sheet 일부인데, 같은 동작을 DBMS마다 다른 문법으로 표현할 수 있다는 걸 잘 보여준다. 필터를 하나 피하고 나면 그 필터가 정확히 어떤 키워드를 문자열 매칭으로 걸렀는지가 거꾸로 보이는 느낌이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;2600&quot; data-origin-height=&quot;1800&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/U8ozT/dJMcafU3GAW/AAAAAAAAAAAAAAAAAAAAAOSxdxnBe6hpvsjA0hHCSrohznG4N-_MJhKb_DyT4fs8/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=kWCAZ3PgN1zgB7Im0jgQbk50zhU%3D&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FU8ozT%2FdJMcafU3GAW%2FAAAAAAAAAAAAAAAAAAAAAOSxdxnBe6hpvsjA0hHCSrohznG4N-_MJhKb_DyT4fs8%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1785509999%26allow_ip%3D%26allow_referer%3D%26signature%3DkWCAZ3PgN1zgB7Im0jgQbk50zhU%253D&quot; alt=&quot;PortSwigger SQL Injection Cheat Sheet &amp;mdash; DBMS별 문자열/서브스트링 문법 비교 (공식 문서)&quot; width=&quot;2600&quot; height=&quot;1800&quot; data-origin-width=&quot;2600&quot; data-origin-height=&quot;1800&quot; /&gt;
&lt;figcaption&gt;PortSwigger SQL Injection Cheat Sheet &amp;mdash; DBMS별 문자열/서브스트링 문법 비교 (공식 문서)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid #ddd; padding: 6px; text-align: left;&quot;&gt;막힌 표현&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #ddd; padding: 6px; text-align: left;&quot;&gt;우회 표현&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #ddd; padding: 6px; text-align: left;&quot;&gt;비고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #ddd; padding: 6px;&quot;&gt;&lt;code&gt;or&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #ddd; padding: 6px;&quot;&gt;&lt;code&gt;||&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #ddd; padding: 6px;&quot;&gt;단어 자체를 문자열 필터링&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #ddd; padding: 6px;&quot;&gt;&lt;code&gt;=&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #ddd; padding: 6px;&quot;&gt;&lt;code&gt;IN (...)&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #ddd; padding: 6px;&quot;&gt;등호 기호 차단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #ddd; padding: 6px;&quot;&gt;&lt;code&gt;and&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #ddd; padding: 6px;&quot;&gt;&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #ddd; padding: 6px;&quot;&gt;동일 원리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #ddd; padding: 6px;&quot;&gt;&lt;code&gt;SUBSTRING()&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #ddd; padding: 6px;&quot;&gt;&lt;code&gt;MID()&lt;/code&gt;, &lt;code&gt;SUBSTR()&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #ddd; padding: 6px;&quot;&gt;MySQL 동의어 함수&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5단계: 이진 탐색으로 자동화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5단계에서는 손으로 한 글자씩 시도하는 게 도저히 불가능한 수준이라 자동화 스크립트(ch5.py)를 짰다. 무식하게 알파벳 순서대로 26번씩 시도하는 대신 이진 탐색을 썼다. 문자의 아스키 코드 값을 기준으로 &lt;code&gt;&amp;gt; 중간값&lt;/code&gt;인지 물어보고, 참이면 위쪽 절반, 거짓이면 아래쪽 절반으로 범위를 좁혀나가는 방식이다. 알고리즘만 놓고 보면 이렇게 정리된다.&lt;/p&gt;
&lt;pre style=&quot;background: #1e1e1e; color: #d4d4d4; padding: 12px; border-radius: 6px; overflow-x: auto;&quot;&gt;&lt;code&gt;def find_char(oracle, position, lo=32, hi=126):
    # oracle(cond)는 조건이 참인지 서버에 물어보고 True/False를 반환
    while lo &amp;lt; hi:
        mid = (lo + hi) // 2
        cond = f&quot;ASCII(SUBSTR(password,{position},1)) &amp;gt; {mid}&quot;
        if oracle(cond):
            lo = mid + 1
        else:
            hi = mid
    return chr(lo)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아스키 코드 범위(32~126, 약 95개)가 넓지 않아서 한 글자당 대략 6~7번 요청이면 값을 확정할 수 있었고, 전체 비밀번호를 뽑는 데 대략 160번 정도의 요청이 들었다. 순차 탐색으로 했으면 한 글자에 최대 90번 넘게 요청을 보내야 했을 걸 생각하면 확실히 체감되는 차이였다. log₂(95) &amp;asymp; 6.6이라는 계산이 실제 요청 횟수로 그대로 나타난 셈이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자동화가 진짜 벽이었다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 워게임을 풀면서 새삼 느낀 건, Blind SQLi가 어려운 이유가 SQL 문법을 몰라서가 아니라 자동화 없이는 사람이 감당할 수 없는 규모의 반복 작업이라는 점이었다. 조건 하나 만들고, 요청 보내고, 결과 보고, 다음 조건 만들고를 손으로 하는 건 1~2단계까지가 한계였다. 자동화 스크립트를 짜는 순간부터 문제가 완전히 다른 성격이 됐다. SQL 페이로드를 정확히 짜는 것보다, 요청-응답 루프를 안정적으로 도는 스크립트를 만드는 쪽에 더 공을 들이게 됐다. 세션 쿠키 유지, 응답 파싱 기준(어떤 문자열이 뜨면 참인지)을 정확히 잡는 것, 그리고 요청이 하나 실패했을 때 재시도하는 처리까지 신경 쓸 게 늘어난다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사라질 워게임을 어떻게 남길까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아쉬운 점은 이 사이트가 곧 종료될 예정이라는 거였다. 멘토님이 공유해준 Notion write-up과 내가 짠 스크립트는 남아 있지만, 실제 서버가 내려가면 오프라인으로 다시 연습할 방법이 없다. PHP와 DB 소스가 따로 공개돼 있지 않으면 로컬 미러를 만들기도 어렵다. 예전에 다른 멘토님이 공유해준 mobilehacking.kr CTF는 로컬 미러를 따로 만들어둔 적이 있는데, 이번에도 비슷하게 미리 보존해뒀어야 했나 싶다. 워게임 사이트들이 언젠가 사라진다는 걸 당연하게 여기고, 풀 수 있을 때 스크립트와 페이로드를 잘 정리해두는 습관을 들여야겠다는 생각이 들었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같이 읽기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 DevTools만으로 숨은 단서를 추적한 다른 CTF 풀이도 있다. &lt;a href=&quot;https://gorani-kr.tistory.com/25&quot;&gt;Cascading the Seven Seas: 브라우저 DevTools로 숨겨진 플래그 찾기&lt;/a&gt;.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://portswigger.net/web-security/sql-injection/blind&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PortSwigger &amp;mdash; Blind SQL Injection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://portswigger.net/web-security/sql-injection/cheat-sheet&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PortSwigger &amp;mdash; SQL Injection Cheat Sheet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/05-Testing_for_SQL_Injection&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;OWASP WSTG &amp;mdash; Testing for SQL Injection&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;2000&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bHbDcS/dJMcadCKBev/AAAAAAAAAAAAAAAAAAAAACfezbRNWl8pIcg0IXVAEfriilEk6KF5KEVg6rEecDXP/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=Dp%2BcOpo5n0Uj528Pc5G8ZrrAwvg%3D&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FbHbDcS%2FdJMcadCKBev%2FAAAAAAAAAAAAAAAAAAAAACfezbRNWl8pIcg0IXVAEfriilEk6KF5KEVg6rEecDXP%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1785509999%26allow_ip%3D%26allow_referer%3D%26signature%3DDp%252BcOpo5n0Uj528Pc5G8ZrrAwvg%253D&quot; alt=&quot;OWASP Web Security Testing Guide &amp;mdash; Testing for SQL Injection (WSTG-INPV-05)&quot; width=&quot;2800&quot; height=&quot;2000&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;2000&quot; /&gt;
&lt;figcaption&gt;OWASP Web Security Testing Guide &amp;mdash; Testing for SQL Injection (WSTG-INPV-05)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기법 전체 맥락&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/JaaDY/dJMcacYh01A/AAAAAAAAAAAAAAAAAAAAAKEbFgjgGYC-JXIJHr9gyJtiueZHwHAFzg_POgHUa1VM/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=%2BMP0wEgcXDZil4ykxbpEJc1c7hc%3D&quot; alt=&quot;PortSwigger Web Security Academy SQL injection 개요&quot; /&gt;
&lt;figcaption&gt;PortSwigger Web Security Academy SQL injection 개요&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Boolean blind는 SQL injection의 한 형태다. 응답 본문에 데이터가 직접 나오지 않을 때 참&amp;middot;거짓 차이를 관찰한다는 점이 핵심이며, 실서비스에서는 허가된 환경 외에 적용하면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://portswigger.net/web-security/sql-injection&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PortSwigger SQL injection&lt;/a&gt;&lt;/p&gt;</description>
      <category>보안</category>
      <category>BlindSQLi</category>
      <category>SQLi</category>
      <category>Wargame</category>
      <category>WEB</category>
      <category>WHS</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/30</guid>
      <comments>https://gorani-kr.tistory.com/30#entry30comment</comments>
      <pubDate>Sun, 26 Jul 2026 10:41:27 +0900</pubDate>
    </item>
    <item>
      <title>AIxCC 우승 시스템 ATLANTIS&amp;middot;Crete 로컬 설치 후기</title>
      <link>https://gorani-kr.tistory.com/29</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;CODEGATE 키노트를 듣고 집에 돌아와서도 계속 머릿속에 남는 게 있었다. 발표자가 말한 그 시스템, 그러니까 DARPA AIxCC 결선에서 우승한 자율 취약점 탐지&amp;middot;패치 시스템이 실제로 존재한다면 코드를 한 번 보고 싶었다. 슬라이드로 아키텍처 다이어그램만 보는 것과 실제 코드 구조를 뜯어보는 건 전혀 다른 얘기니까.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddnqSg/dJMcacKPeib/BFiKUrefrBAz9ZNMKlGlK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddnqSg/dJMcacKPeib/BFiKUrefrBAz9ZNMKlGlK0/img.png&quot; data-alt=&quot;AIxCC 우승 시스템 ATLANTIS 클론 후기 대표 이미지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddnqSg/dJMcacKPeib/BFiKUrefrBAz9ZNMKlGlK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FddnqSg%2FdJMcacKPeib%2FBFiKUrefrBAz9ZNMKlGlK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;AIxCC 우승 시스템 ATLANTIS 클론 후기 대표 이미지&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;AIxCC 우승 시스템 ATLANTIS 클론 후기 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오픈소스로 풀린 우승 시스템&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 이게 진짜 오픈소스로 풀렸는지 확인해야 했다. 검색해보니 DARPA가 결선에 오른 7개 팀의 CRS(Cyber Reasoning System)를 전부 OSI 승인 라이선스로 공개하겠다고 발표했고 이미 4개 팀은 공개된 상태였다. Georgia Tech 뉴스와 OpenSSF 팟캐스트로 교차 확인까지 해봤다. 우승팀은 Team Atlanta였다. Georgia Tech, KAIST, Samsung Research, POSTECH이 함께한 팀이고 시스템 이름은 ATLANTIS. 상금의 절반인 200만 달러를 Georgia Tech SSLab에 기부해서 오픈소스 유지보수를 지원하고 있다고 했다. Linux Foundation, OpenSSF와 함께 OSS-CRS라는 이니셔티브도 진행 중이라고.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mE12S/dJMcacKPeiO/gJJAASl7Q55GnB1i7akL4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mE12S/dJMcacKPeiO/gJJAASl7Q55GnB1i7akL4k/img.png&quot; data-alt=&quot;DARPA AIxCC 결선 1위 Team Atlanta, 상금 400만 달러 &amp;amp;mdash; 출처: AIxCC 공식 사이트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mE12S/dJMcacKPeiO/gJJAASl7Q55GnB1i7akL4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmE12S%2FdJMcacKPeiO%2FgJJAASl7Q55GnB1i7akL4k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;DARPA AIxCC 결선 1위 Team Atlanta, 상금 400만 달러 &amp;mdash; 출처: AIxCC 공식 사이트&quot; loading=&quot;lazy&quot; width=&quot;2800&quot; height=&quot;1800&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;DARPA AIxCC 결선 1위 Team Atlanta, 상금 400만 달러 &amp;mdash; 출처: AIxCC 공식 사이트&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ck7YuX/dJMcadQl3Z9/FlOnobK6CPIQAnN1jL0eLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ck7YuX/dJMcadQl3Z9/FlOnobK6CPIQAnN1jL0eLk/img.png&quot; data-alt=&quot;DEF CON 현장에서 우승 상금 400만 달러 수표를 든 Team Atlanta &amp;amp;mdash; 출처: Team Atlanta 공식 블로그&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ck7YuX/dJMcadQl3Z9/FlOnobK6CPIQAnN1jL0eLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fck7YuX%2FdJMcadQl3Z9%2FFlOnobK6CPIQAnN1jL0eLk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;DEF CON 현장에서 우승 상금 400만 달러 수표를 든 Team Atlanta &amp;mdash; 출처: Team Atlanta 공식 블로그&quot; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;1800&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;DEF CON 현장에서 우승 상금 400만 달러 수표를 든 Team Atlanta &amp;mdash; 출처: Team Atlanta 공식 블로그&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3840&quot; data-origin-height=&quot;2160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uEu6m/dJMcacKPeju/LKckq8t2XvXk77yeVOkHuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uEu6m/dJMcacKPeju/LKckq8t2XvXk77yeVOkHuk/img.png&quot; data-alt=&quot;GitHub에 공개된 Team Atlanta ATLANTIS 저장소 &amp;amp;mdash; example-crs-webservice / architecture / appendix 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uEu6m/dJMcacKPeju/LKckq8t2XvXk77yeVOkHuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuEu6m%2FdJMcacKPeju%2FLKckq8t2XvXk77yeVOkHuk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;GitHub에 공개된 Team Atlanta ATLANTIS 저장소 &amp;mdash; example-crs-webservice / architecture / appendix 구조&quot; loading=&quot;lazy&quot; width=&quot;3840&quot; height=&quot;2160&quot; data-origin-width=&quot;3840&quot; data-origin-height=&quot;2160&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;GitHub에 공개된 Team Atlanta ATLANTIS 저장소 &amp;mdash; example-crs-webservice / architecture / appendix 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;저장소 구조 훑어보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장소는 MIT 라이선스였다. &lt;code&gt;--depth 1&lt;/code&gt;로 얕게 클론했는데도 4.5기가바이트가 나왔다. 구조를 살펴보니 크게 세 덩어리였다. CRS 본체인 &lt;code&gt;example-crs-webservice&lt;/code&gt;, Azure&amp;middot;Terraform&amp;middot;Kubernetes&amp;middot;Tailscale로 짜인 배포 인프라인 &lt;code&gt;example-crs-architecture&lt;/code&gt;, 그리고 부속 도구 모음인 &lt;code&gt;example-crs-appendix&lt;/code&gt;. 대회용 배포 인프라 의존성이 워낙 커서 로컬에서 전체를 그대로 돌리는 건 무리였다. 대신 아키텍처와 에이전트 프롬프트, 워크플로우 코드를 읽는 레퍼런스로만 쓰기로 하고, 그 중에서 비교적 독립적으로 실행 가능해 보이는 컴포넌트 하나를 골랐다. 내부명 Crete라고 불리는 &lt;code&gt;crs-patch&lt;/code&gt;였다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;vendor 패키지의 업스트림 버그 두 개&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;집에 있는 우분투 서버로 옮겨서 &lt;code&gt;uv sync&lt;/code&gt;부터 돌렸다. 예상대로 한 번에 되지는 않았다. vendor로 들어있던 &lt;code&gt;third_party/aider&lt;/code&gt; 패키지의 setuptools 패키징에 버그가 두 개 있었다. 서브패키지 include 패턴이 빠져 있어서 일부 모듈이 설치 과정에서 누락됐고, 리소스 데이터 파일도 패키지에 포함이 안 되고 있었다. 둘 다 Crete 프로젝트만의 문제가 아니라 vendor된 aider 자체의 업스트림 버그였다. 직접 패치해서 두 문제를 다 해결하고 나서야 setup 스크립트가 끝까지 돌아갔다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스모크 테스트에서 막힌 지점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 스모크 테스트를 돌려보려고 했는데 여기서 막혔다. 대회 전용 OSS-Fuzz 픽스처인 &lt;code&gt;aixcc/c/mock-c&lt;/code&gt;가 비공개였다. &lt;code&gt;aixcc-finals/oss-fuzz-aixcc&lt;/code&gt;와 &lt;code&gt;tob-challenges/oss-fuzz-aixcc&lt;/code&gt; 두 저장소를 다 확인해봤지만 어디에도 없었다. Crete의 LLM 모듈, insighter나 writeup 같은 부분도 Docker와 OSS-Fuzz 환경에 강하게 결합돼 있어서 가볍게 우회할 수 있는 수준이 아니었다. 여기서 전체 재현은 포기하기로 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;대신 미니 데모를 직접 짜다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 Crete 없이 독립적으로 돌아가는 미니 데모를 하나 직접 짰다. 크래시 로그를 받으면 root_cause 분석, skeptic consensus 검증, patch 생성까지 3단계로 이어지는 파이프라인이고 Anthropic SDK만 써서 구현했다. 키노트에서 들었던 &quot;익스플로잇 관점 vs 반박 관점 컨센서스&quot; 아이디어를 최소 형태로 흉내낸 셈이다. 테스트 데이터는 지어내지 않고 내가 실제로 갖고 있던 걸 썼다. 이전에 nfcapd sequencer에서 찾은 use-after-free의 실제 ASAN 크래시 로그와 관련 소스 파일(ipfix.c, nfxV3.c)로 한 번 돌려봤다. 그런데 결과를 다 보기 전에 Anthropic Console API 크레딧이 0원이 됐다. claude.ai 구독과 콘솔 API 과금이 별개라는 걸 이번에 처음 제대로 알았다. 크레딧을 충전하고 나서 마저 돌려볼 생각이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;덤으로 얹은 IDA Pro MCP&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 작업을 하는 김에 IDA Pro용 MCP 플러그인도 설치했다. mrexodia가 만든 ida-pro-mcp를 Claude Code 플러그인 마켓플레이스로 설치하고 idalib를 전역 활성화했다. IDA와 Claude Code를 재시작해야 반영되는데, 이건 리버싱 작업할 때 IDA 안의 함수&amp;middot;구조체 정보를 에이전트가 직접 조회할 수 있게 해주는 도구라 나중에 리버싱 과제나 CVE 분석에서 꽤 쓸모가 있을 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfPZic/dJMcadQl30q/ZqVGShre5wTZXGYVfKeihK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfPZic/dJMcadQl30q/ZqVGShre5wTZXGYVfKeihK/img.png&quot; data-alt=&quot;IDA Pro와 Claude Code를 연결하는 mrexodia/ida-pro-mcp 저장소&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfPZic/dJMcadQl30q/ZqVGShre5wTZXGYVfKeihK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfPZic%2FdJMcadQl30q%2FZqVGShre5wTZXGYVfKeihK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;IDA Pro와 Claude Code를 연결하는 mrexodia/ida-pro-mcp 저장소&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1400&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1400&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;IDA Pro와 Claude Code를 연결하는 mrexodia/ida-pro-mcp 저장소&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;완전 재현은 실패, 그래도 남은 것들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 목표했던 &quot;우승 시스템을 완전히 재현해서 돌려보기&quot;는 못 했다. 대회용 인프라와 비공개 픽스처 때문에 애초에 로컬에서 통째로 돌아가게 만들어진 물건이 아니었다. 하지만 그 과정에서 얻은 게 더 많았다. 실제로 패키지를 빌드하다가 업스트림 버그 두 개를 발견해서 고쳤고, 슬라이드에서만 봤던 컨센서스 아이디어를 내 손으로 최소 구현까지 해봤다. 발표만 듣고 끝냈으면 절대 몰랐을 디테일들이다. 다음엔 크레딧을 채우고 crash_to_patch 스크립트를 끝까지 돌려서 실제로 얼마나 쓸만한 패치가 나오는지 확인해볼 생각이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같이 읽기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시스템을 처음 접했던 CODEGATE 키노트 현장 기록은 &lt;a href=&quot;https://gorani-kr.tistory.com/28&quot;&gt;CODEGATE 2026 키노트 &amp;mdash; AI가 취약점을 찾고 고치기까지 6개월&lt;/a&gt;에 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Team-Atlanta/aixcc-afc-atlantis&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Team Atlanta &amp;mdash; ATLANTIS 저장소 (GitHub)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://aicyberchallenge.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;DARPA AI Cyber Challenge 공식 사이트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://team-atlanta.github.io/blog/post-afc/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Team Atlanta 공식 블로그 &amp;mdash; AIxCC Final and Team Atlanta&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mrexodia/ida-pro-mcp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;mrexodia/ida-pro-mcp (GitHub)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>보안</category>
      <category>AIxCC</category>
      <category>atlantis</category>
      <category>crete</category>
      <category>ida-mcp</category>
      <category>opensource</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/29</guid>
      <comments>https://gorani-kr.tistory.com/29#entry29comment</comments>
      <pubDate>Sun, 26 Jul 2026 10:41:17 +0900</pubDate>
    </item>
    <item>
      <title>CODEGATE 2026 키노트 &amp;mdash; AI가 취약점을 찾고 고치기까지 6개월</title>
      <link>https://gorani-kr.tistory.com/28</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;코엑스 그랜드볼룸 102-104호에 도착했을 때는 이미 자리가 거의 다 차 있었다. CODEGATE 2026 금요일 세션 1, 10시 반 키노트를 듣기 위해서였다. 발표자는 DARPA AIxCC(AI Cyber Challenge) 결선 우승팀 출신이고 지금은 Microsoft에서 MDC라는 프로젝트를 하고 있다고 자기소개를 했다. CVE 퍼징을 계속 하고 있는 입장에서 놓칠 수 없는 발표였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/b16LCf/dJMcacRr08f/AAAAAAAAAAAAAAAAAAAAAEGi5lGVyTRppHN0eNvWH4YQtt8YQHJX-EveTxw7Y7SU/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=Lhi24Lkyg%2B2VKbvtXrZqSWzMJ5w%3D&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2Fb16LCf%2FdJMcacRr08f%2FAAAAAAAAAAAAAAAAAAAAAEGi5lGVyTRppHN0eNvWH4YQtt8YQHJX-EveTxw7Y7SU%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1785509999%26allow_ip%3D%26allow_referer%3D%26signature%3DLhi24Lkyg%252B2VKbvtXrZqSWzMJ5w%253D&quot; alt=&quot;CODEGATE 2026 키노트 후기 대표 이미지&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; /&gt;
&lt;figcaption&gt;CODEGATE 2026 키노트 후기 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AIxCC, 10~20년 주기 챌린지의 최신판&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AIxCC는 DARPA가 10~20년 주기로 여는 사회적 임팩트 챌린지 계열의 최신판이라고 한다. 후쿠시마 이후 재난로봇 대회였던 로보틱스 챌린지, 10년 전 자동 해킹을 겨뤘던 Cyber Grand Challenge의 뒤를 잇는다. 백악관과 프론티어 AI랩, MS&amp;middot;Google 같은 빅테크가 공동 출자해서 3년간 진행됐고 결선은 작년 데프콘에서 열렸다. 목표는 명확했다. 자율 시스템이 대규모 오픈소스 코드베이스에서 버그를 찾고, 거기서 그치지 않고 패치까지 직접 생성하는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/coaA3e/dJMb99ULXKI/AAAAAAAAAAAAAAAAAAAAAFn4tEJV6H3G8AmMWMO1WLpqsX4A6ddgzdDxNl33kLyg/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=t1Gl69RNygqotq39E3WRxk6S43k%3D&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FcoaA3e%2FdJMb99ULXKI%2FAAAAAAAAAAAAAAAAAAAAAFn4tEJV6H3G8AmMWMO1WLpqsX4A6ddgzdDxNl33kLyg%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1785509999%26allow_ip%3D%26allow_referer%3D%26signature%3Dt1Gl69RNygqotq39E3WRxk6S43k%253D&quot; alt=&quot;CODEGATE 2026 공식 홈페이지 &amp;mdash; The Path to Top 3 Global&quot; width=&quot;2800&quot; height=&quot;1800&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; /&gt;
&lt;figcaption&gt;CODEGATE 2026 공식 홈페이지 &amp;mdash; The Path to Top 3 Global&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;숫자로 본 결선&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자가 인상적이었다. 준결선에 100팀이 등록해 42팀이 완주했고, 결선에 오른 7팀 전원이 AI 전공이 아니라 시큐리티 도메인 지식 기반 팀이었다고 한다. 발표자는 이 부분을 강조했다. AI를 잘 쓰는 것보다 보안 도메인 지식이 있는 상태에서 AI를 쓰는 게 효율을 극대화한다는 얘기였다. 발표팀은 결선 1위로 400만 달러의 상금을 받았고, AI가 낸 버그 리포트의 정확도(트루 포지티브율)가 약 91%였다고 한다. 열 개 중 아홉 개가 진짜 버그였다는 뜻이다. 대회 전체로 보면 주최 측이 검증용으로 넣은 합성 취약점 63개 중 54개를 참가 시스템들이 찾아냈고, 실제 오픈소스에서도 C/C++ 6개와 Java 12개, 모두 18개의 새로운 버그를 발견했다. 그중 ATLANTIS가 각 언어에서 3개씩, 총 6개를 찾았다고 한다. 첫 버그를 찾기까지 걸린 시간은 45분. DARPA가 공개한 결선 통계에서 태스크 하나당 평균 비용은 약 152달러였는데, 외부 펜테스트 업체가 수개월 동안 수행하는 일과 비교하면 자동화의 경제성이 확실히 보였다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;아키텍처: reachability, 앙상블, 하니스, 컨센서스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발표에서 가장 오래 필기한 부분은 아키텍처 얘기였다. 첫째, reachability, 그러니까 도달 가능성 증명이 진짜 버그와 AI가 지어낸 헛소리(slop)를 가르는 기준이라고 했다. 정적 분석기는 &quot;이 라인에 버그가 있을 것 같다&quot;까지만 말해줄 뿐 실제로 그 라인에서 크래시가 나는지는 증명하지 못한다. AIxCC의 핵심은 유저스페이스 인풋으로 실제 크래시를 재현해서 정적 분석 결과를 검증하는 거였다. 이 얘기를 듣는데 내가 CVE 프로젝트에서 지켜온 원칙이 그대로 떠올랐다. UAF를 찾았다고 바로 RCE라고 주장하지 않고, 실제로 재현된 것만 승격시키는 방식. 이름은 몰랐지만 이미 하고 있던 방법이 대회 우승 방법론과 정확히 같은 방향이었다는 게 신기했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째는 앙상블이 단일 모델이나 단일 에이전트보다 항상 우월하다는 얘기였다. 발표팀은 67개의 서로 다른 백엔드 모델 조합 에이전트를 썼는데, 각 에이전트가 잘 잡는 버그 타입이 다르기 때문에 합치면 개별 최고 성능을 능가한다고 했다. 셋째는 아키텍처, 그러니까 하니스가 모델 성능보다 중요하다는 것. 같은 모델이라도 하니스를 바꾸면 결과가 확 달라지고, 최신 프론티어 모델이 아니어도 좋은 하니스만 있으면 준수한 성능이 나온다고 했다. 넷째는 서브에이전트 오프로딩과 팀 컨센서스로 false positive를 걸러내는 방식이었는데, 이게 특히 흥미로웠다. Microsoft MDC의 프로덕션 시스템에서는 서로 다른 persona를 가진 에이전트들이 토론을 해서 컨센서스에 도달해야 진짜 버그로 리포트된다고 한다. 한쪽 persona는 &quot;이건 왜 중요한 취약점인가&quot;를 주장하는 익스플로잇 관점이고, 다른 persona는 &quot;이건 의도된 동작이다, 이미 아는 이슈다&quot;라며 반박하는 개발자 방어 관점이다. 두 관점이 부딪혀서 살아남은 것만 최종 리포트로 올라간다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MDC, Windows TCP/IP 스택에서 검증된 파이프라인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MDC 얘기도 따로 시간을 들여 설명했다. 올해 6월 빌드 컨퍼런스에서 사티아 나델라가 직접 소개했던 프로젝트라고 한다. 기존 퍼저 기반 시스템을 AI 기반으로 전환하면서, 개발자와 보안 리서처의 실제 행동을 모방하는 아키텍처로 설계했다고 했다. 코드의 과거 버그 이력을 분석해서 threat modeling을 하고, 100~150개의 특화 에이전트를 컨텍스트별로 활성화해서 스캔하고, 멀티persona 토론으로 검증한 뒤에야 true-positive 리포트가 나오는 파이프라인이다. 첫 실사용 고객이 Windows 팬테스팅 그룹의 TCP/IP 스택이었는데, 유료 펜테스트로도 이미 버그를 못 찾던 &quot;안정적인&quot; 코드였다고 한다. 시스템이 30개 버그 리포트를 냈을 때 처음엔 팀 내부에서도 AI가 헛소리를 하는 거라고 의심했는데, 개발자가 직접 확인해보니 90% 이상이 진짜였다고 했다. 2025년에 발견한 버그의 70% 이상이 배포 후 3개월 안에 발견됐다는 수치도 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/B70yW/dJMcafgr0tk/AAAAAAAAAAAAAAAAAAAAAI73NDlW2fGOUxyaXsC6jiCanOhRez72r0PxLGQ1MIQZ/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=pYc4Bod%2BLwgaiTQpVDdgIfsiFhA%3D&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FB70yW%2FdJMcafgr0tk%2FAAAAAAAAAAAAAAAAAAAAAI73NDlW2fGOUxyaXsC6jiCanOhRez72r0PxLGQ1MIQZ%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1785509999%26allow_ip%3D%26allow_referer%3D%26signature%3DpYc4Bod%252BLwgaiTQpVDdgIfsiFhA%253D&quot; alt=&quot;DARPA AIxCC 결선 1위 Team Atlanta, 상금 400만 달러 &amp;mdash; 출처: AIxCC 공식 사이트&quot; width=&quot;2800&quot; height=&quot;1800&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; /&gt;
&lt;figcaption&gt;DARPA AIxCC 결선 1위 Team Atlanta, 상금 400만 달러 &amp;mdash; 출처: AIxCC 공식 사이트&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발표를 듣고 나서 이 프로젝트명을 다시 검색해봤는데, 공식 문서에서는 코드네임 &lt;b&gt;MDASH&lt;/b&gt;(Multi-model Agentic Scanning Harness)로 표기되고 있었다. Microsoft Security 블로그에 올라온 발표 글에서도 100개 이상의 특화 에이전트가 Windows 코드베이스에서 16개의 신규 취약점(치명적 RCE 4건 포함)을 찾아낸 사례를 소개하고 있는데, 키노트에서 들은 숫자들과 결이 같다. 발표에서 들은 이름과 표기가 조금 다를 수 있어 확인 차 남긴다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&quot;6개월 만에 현실이 됐다&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발표 마지막은 이런 얘기로 마무리됐다. AI로 취약점을 찾고 고치는 일이 예전 DARPA 챌린지들처럼 10~20년 걸릴 먼 미래가 아니라 6개월 만에 현실이 됐다고. 지금 취약점 연구에 AI를 안 쓰고 있다면 지금 당장 시작하라는 메시지였다. 듣는 내내 내가 하고 있는 CVE 퍼징 프로젝트와, 이번 학기에 시작할 화이트햇스쿨 팀 프로젝트(LLM을 활용한 소프트웨어 취약점 분석)가 이 발표의 방법론과 거의 단계별로 겹친다는 생각이 계속 들었다. 취약점 후보 좁히기는 reachability 검증으로, 동적 분석으로 트리거하는 부분은 정적분석과 퍼징의 하이브리드로, PoC와 보고서 품질 관리는 멀티persona 컨센서스 절차로 그대로 매핑할 수 있을 것 같다. 팀 프로젝트 수행계획서를 쓸 때 이 발표 내용을 방법론 섹션에 인용해야겠다고 생각하며 노트를 정리했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같이 읽기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키노트를 듣고 집에 와서 직접 ATLANTIS 저장소를 클론하고 로컬에서 재현해본 기록은 &lt;a href=&quot;https://gorani-kr.tistory.com/29&quot;&gt;AIxCC 우승 시스템 ATLANTIS&amp;middot;Crete 로컬 설치 후기&lt;/a&gt;에 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.codegate.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CODEGATE 공식 사이트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://aicyberchallenge.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;DARPA AI Cyber Challenge 공식 사이트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.microsoft.com/en-us/security/blog/2026/05/12/defense-at-ai-speed-microsofts-new-multi-model-agentic-security-system-finds-16-new-vulnerabilities/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Microsoft Security Blog &amp;mdash; Defense at AI speed (codename MDASH)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/XRnke/dJMcaazrhCH/AAAAAAAAAAAAAAAAAAAAAIDD4gk2tZecCqPvJjyvIGlsxFmVcfKf-xrm_Fec2bO4/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=VIMbInAv6JeMHzvHNtdSIymAQ6I%3D&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FXRnke%2FdJMcaazrhCH%2FAAAAAAAAAAAAAAAAAAAAAIDD4gk2tZecCqPvJjyvIGlsxFmVcfKf-xrm_Fec2bO4%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1785509999%26allow_ip%3D%26allow_referer%3D%26signature%3DVIMbInAv6JeMHzvHNtdSIymAQ6I%253D&quot; alt=&quot;Microsoft Security 공식 블로그 &amp;mdash; codename MDASH 발표 글 (작성자: Taesoo Kim, VP of Agentic Security)&quot; width=&quot;2800&quot; height=&quot;1800&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; /&gt;
&lt;figcaption&gt;Microsoft Security 공식 블로그 &amp;mdash; codename MDASH 발표 글 (작성자: Taesoo Kim, VP of Agentic Security)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;팀 구성도 원 출처에서 확인&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/dIaurA/dJMcacYh01u/AAAAAAAAAAAAAAAAAAAAANRjtW8xfIdVFMILRSnLCsvWpTIxgbpWLbteP-YLvUf_/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=YeCAl841OiFI02BpyUMVGeZrdDY%3D&quot; alt=&quot;AIxCC 우승팀 Team Atlanta 공식 GitHub 조직&quot; /&gt;
&lt;figcaption&gt;AIxCC 우승팀 Team Atlanta 공식 GitHub 조직&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키노트에서 들은 시스템 이름과 구성은 Team Atlanta가 공개한 저장소&amp;middot;조직 자료를 함께 확인했다. 발표 메모와 공개 자료가 다를 때는 공식 공개 명칭을 본문에 우선 반영했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/Team-Atlanta&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Team Atlanta GitHub&lt;/a&gt;&lt;/p&gt;</description>
      <category>행사&amp;middot;후기</category>
      <category>ai-security</category>
      <category>AIxCC</category>
      <category>codegate</category>
      <category>DARPA</category>
      <category>microsoft</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/28</guid>
      <comments>https://gorani-kr.tistory.com/28#entry28comment</comments>
      <pubDate>Sun, 26 Jul 2026 10:41:07 +0900</pubDate>
    </item>
    <item>
      <title>Parsec 원격 PC에서 npm 없을 때 Node.js 설치하고 재부팅 없이 파섹 유지하기</title>
      <link>https://gorani-kr.tistory.com/27</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Parsec으로 원격 접속한 Windows PC에서 프로젝트를 열었는데 &lt;code&gt;npm&lt;/code&gt;이 없어서 바로 막혔다. 예전에는 Node.js MSI를 설치하고 재부팅한 뒤 Windows 자동 로그인이 되어야 Parsec이 다시 붙는다고 정리했지만, 다시 확인해보니 그건 너무 거친 설명이었다. 대부분은 재부팅 없이 새 터미널만 열면 되고, 재부팅 뒤 로그인 화면에 접속할 수 있는지는 &lt;b&gt;Parsec 설치 방식&lt;/b&gt;에 달려 있다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/PtGFz/dJMcac42h9I/AAAAAAAAAAAAAAAAAAAAAJYFV7cgBmrhKmco5Q4DLTwH1ZhHkA7RW0eUMvAmt3Aw/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=M2E49aFqmNXqqEbbvJNKaT12D2o%3D&quot; alt=&quot;Parsec 원격 Windows PC에 Node.js를 설치할 때 확인할 항목&quot; /&gt;
&lt;figcaption&gt;Parsec 원격 Windows PC에 Node.js를 설치할 때 확인할 항목&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;먼저 진짜 Node.js가 없는지 확인&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;node --version
npm --version
where node
where npm&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;node&lt;/code&gt;는 나오는데 &lt;code&gt;npm&lt;/code&gt;만 안 나오면 설치 자체보다 PATH나 손상된 npm 구성을 의심해야 한다. 둘 다 나오지 않으면 Node.js 설치로 넘어간다. 원격 PC에 여러 프로젝트가 있고 버전을 자주 바꿔야 한다면 MSI 하나를 고정 설치하기보다 nvm-windows 같은 버전 관리 도구가 낫다. Microsoft의 Windows 개발 가이드도 버전 변화가 빠르다는 이유로 버전 관리자를 권한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;가장 단순한 방법: 공식 LTS 설치&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/ba5rmN/dJMcadiEAoN/AAAAAAAAAAAAAAAAAAAAACWsLRMZD-2qHsY3xoXwDvzgi9j1VcS9jRTGTHdwz5Ym/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=Kszbde6CVThpZFa16CjZNKsgTzU%3D&quot; alt=&quot;Node.js 공식 다운로드 페이지의 Windows 설치 선택지&quot; /&gt;
&lt;figcaption&gt;Node.js 공식 다운로드 페이지의 Windows 설치 선택지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 버전만 쓸 PC라면 Node.js 공식 페이지에서 LTS Windows Installer를 내려받아 설치하는 게 가장 빠르다. MSI의 Custom Setup에서 &lt;b&gt;Add to PATH&lt;/b&gt;가 포함되어 있는지 확인한다. 설치가 끝난 직후 이미 열려 있던 PowerShell이나 CMD는 이전 환경변수를 계속 들고 있으므로, 먼저 터미널을 완전히 닫고 새로 연다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;node -v
npm -v
npm config get prefix&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 버전이 뜨면 끝이다. 시스템 전체 재부팅은 보통 필요 없다. 새 터미널에서도 안 된다면 &lt;code&gt;C:Program Files
odejs&lt;/code&gt;가 PATH에 있는지 확인하고, 다른 Node 설치나 사용자별 npm 경로가 충돌하는지도 본다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;native modules 도구는 무조건 체크할 필요가 없다&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bAYu5b/dJMcaiqDScg/AAAAAAAAAAAAAAAAAAAAAMNpFXWEFpWAUQ8ScQUoNvQKPMt4GoscPc_Lw_ukDzpj/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=oWPCm5pxQzO%2FHEkyv7SU3gMFU%2FQ%3D&quot; alt=&quot;Microsoft Learn의 Windows Node.js 개발 환경 안내&quot; /&gt;
&lt;figcaption&gt;Microsoft Learn의 Windows Node.js 개발 환경 안내&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSI에서 &amp;ldquo;Automatically install the necessary tools&amp;rdquo; 같은 선택지가 보일 수 있다. 이건 모든 JavaScript 프로젝트의 필수 항목이 아니다. &lt;code&gt;node-gyp&lt;/code&gt;로 C/C++ 네이티브 애드온을 빌드해야 할 때 Python과 Visual Studio Build Tools가 필요하다. 순수 JavaScript 패키지만 쓰는데 미리 설치하면 다운로드와 디스크 사용량만 커질 수 있다. 프로젝트 의존성에서 native addon 오류가 실제로 날 때 설치해도 된다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bsPpA8/dJMcac42h9L/AAAAAAAAAAAAAAAAAAAAALMq-5vLT-4rKcSCBU690y4um8GQS5QEv-U7VDJM4ZI0/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=LK19HG88lIoZwkTh47bQkXZudCk%3D&quot; alt=&quot;npm 공식 문서의 Node.js와 npm 설치 방법&quot; /&gt;
&lt;figcaption&gt;npm 공식 문서의 Node.js와 npm 설치 방법&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;재부팅이 필요해졌다면 Parsec 설치 방식을 본다&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/ciRe1u/dJMcac42h9M/AAAAAAAAAAAAAAAAAAAAAP_UEohMOYNrMquIj-9XJoltQDLtyUN4Gmk62YorzpS-/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=rhR8ZPfjJddCeMicXPSNQ9%2F%2Fxxk%3D&quot; alt=&quot;Parsec 공식 Windows 앱 문서의 Per User와 Per Computer 설치 설명&quot; /&gt;
&lt;figcaption&gt;Parsec 공식 Windows 앱 문서의 Per User와 Per Computer 설치 설명&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parsec Windows 앱은 Per User와 Per Computer 설치가 다르다. 공식 문서에 따르면 &lt;b&gt;Per Computer 설치&lt;/b&gt;는 Windows 로그인 화면 접근을 지원한다. 서비스가 올라오면 자동 로그인 없이도 로그인 화면에 접속할 수 있다는 뜻이다. 반대로 사용자 세션에만 설치했다면 로그인 전에는 호스트가 나타나지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 원격 PC를 재부팅하기 전에는 아래 순서로 본다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Node 설치 때문에 정말 재부팅이 필요한지 먼저 확인한다. 대개 새 터미널이면 충분하다.&lt;/li&gt;
&lt;li&gt;Parsec이 Per Computer로 설치되어 있는지 확인한다.&lt;/li&gt;
&lt;li&gt;Windows 시작 후 Parsec 호스트가 자동으로 뜨는지 이전에 한 번 시험한다.&lt;/li&gt;
&lt;li&gt;BitLocker PIN, BIOS 화면, 네트워크 단절처럼 Parsec이 닿지 못하는 구간이 없는지 확인한다.&lt;/li&gt;
&lt;li&gt;가능하면 다른 원격 경로나 물리적으로 도와줄 사람을 확보한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Windows 자동 로그인은 편하지만 계정이 열린 채 부팅되는 보안 비용이 있다. &amp;ldquo;Parsec을 다시 붙이기 위해 무조건 켜야 한다&amp;rdquo;는 식으로 권장하면 안 된다. Per Computer 설치와 로그인 화면 접근을 먼저 확인하는 쪽이 안전하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최종 체크&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 새 PowerShell에서
node -v
npm -v

# 프로젝트에서
npm ci
npm test&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제는 npm 설치 자체보다 원격 PC에서 재부팅을 어떻게 안전하게 다룰지가 핵심이었다. 설치 &amp;rarr; 새 터미널 &amp;rarr; PATH 확인 순서로 좁히면 불필요한 재부팅을 피할 수 있고, 정말 재부팅해야 할 때도 Parsec 설치 범위를 알고 있으면 원격지를 잃어버릴 가능성이 줄어든다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://nodejs.org/en/download&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Node.js 다운로드&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/windows/dev-environment/javascript/nodejs-on-windows&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Microsoft Learn: Windows에 Node.js 설치&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.npmjs.com/downloading-and-installing-node-js-and-npm/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;npm 설치 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://support.parsec.app/hc/en-us/articles/32381199341716-Parsec-App-for-Windows&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Parsec App for Windows&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>환경설정</category>
      <category>nodejs</category>
      <category>NPM</category>
      <category>parsec</category>
      <category>windows</category>
      <category>원격접속</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/27</guid>
      <comments>https://gorani-kr.tistory.com/27#entry27comment</comments>
      <pubDate>Sun, 26 Jul 2026 10:40:58 +0900</pubDate>
    </item>
    <item>
      <title>Parsec으로 맥&amp;rarr;윈도우 원격접속할 때 Cmd+W 누르면 파섹이 꺼지는 문제</title>
      <link>https://gorani-kr.tistory.com/26</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Parsec으로 맥에서 윈도우 PC에 접속해 크롬을 쓰다가 &lt;code&gt;Cmd+W&lt;/code&gt;를 눌렀는데, 원격 크롬 탭이 아니라 Parsec 연결이 통째로 끊겼다. 처음에는 macOS가 창 닫기 단축키를 가로챈다고만 생각했지만 공식 문서를 다시 보니 이유가 하나 더 있었다. Parsec macOS 클라이언트의 기본 &lt;b&gt;Disconnect&lt;/b&gt; 단축키 자체가 &lt;code&gt;Command+Q/W&lt;/code&gt;로 잡혀 있었다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/cnhGd0/dJMcagNeJPC/AAAAAAAAAAAAAAAAAAAAAKW_VMJ6IY5ecP4QWmDhGDew34yodEXtbPFTwXl6_Kcu/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=zt00QG0jV6PMd01yXVPXADHRXEQ%3D&quot; alt=&quot;macOS Parsec에서 Cmd+W로 연결이 끊기는 원인과 해결 대표 이미지&quot; /&gt;
&lt;figcaption&gt;macOS Parsec에서 Cmd+W로 연결이 끊기는 원인과 해결 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Command와 Control이 섞이는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parsec은 macOS에서 Windows 호스트로 접속할 때 Command와 Ctrl의 기능을 바꿔주는 옵션(&lt;code&gt;client_macos_key_swap=true&lt;/code&gt;)을 기본으로 쓴다. 그래서 &lt;code&gt;Cmd+C/V&lt;/code&gt;가 원격 Windows의 &lt;code&gt;Ctrl+C/V&lt;/code&gt;처럼 자연스럽게 작동한다. 하지만 Parsec 자체의 클라이언트 단축키는 별도로 존재한다. 복사&amp;middot;붙여넣기는 호스트로 전달되는데 &lt;code&gt;Cmd+W&lt;/code&gt;만 연결 종료로 처리되는 이유다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/Au4hH/dJMcaiddyXO/AAAAAAAAAAAAAAAAAAAAAItTwR1iwiBSvg2lLOXB74WgjhquxD76PjXNumKJzl0B/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=6%2BRWEtZ1IGvH624Syh5iA6lpziU%3D&quot; alt=&quot;Unity Parsec 공식 제품 페이지의 원격 입력 기능 소개&quot; /&gt;
&lt;figcaption&gt;Unity Parsec 공식 제품 페이지의 원격 입력 기능 소개&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th&gt;하고 싶은 동작&lt;/th&gt;
&lt;th&gt;맥에서 누를 키&lt;/th&gt;
&lt;th&gt;결과&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;복사/붙여넣기&lt;/td&gt;
&lt;td&gt;Cmd+C / Cmd+V&lt;/td&gt;
&lt;td&gt;원격 Windows의 Ctrl+C / Ctrl+V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;원격 크롬 탭 닫기&lt;/td&gt;
&lt;td&gt;Control+W&lt;/td&gt;
&lt;td&gt;호스트에 Ctrl+W 전달&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parsec 연결 종료&lt;/td&gt;
&lt;td&gt;기본값 Cmd+Q/W&lt;/td&gt;
&lt;td&gt;클라이언트 Disconnect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Immersive Mode 전환&lt;/td&gt;
&lt;td&gt;Cmd+Shift+I&lt;/td&gt;
&lt;td&gt;시스템 키 전달 범위 확대&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;가장 먼저 할 일: Hotkeys에서 Disconnect를 바꾸기&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/cYjRlZ/dJMcafHxIp3/AAAAAAAAAAAAAAAAAAAAADxw3xTCC3WEldTMHUYkC1uJj3JvQO-Gsw2g2O5BDKan/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=3IT1%2Bp1K%2BXsX5Y0tqRrHrQgJocs%3D&quot; alt=&quot;Parsec 공식 Configure Hotkeys 문서&quot; /&gt;
&lt;figcaption&gt;Parsec 공식 Configure Hotkeys 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임시로는 맥 키보드의 &lt;code&gt;Control+W&lt;/code&gt;를 눌러 원격 탭을 닫으면 된다. 자주 실수한다면 Parsec 설정의 &lt;b&gt;Hotkeys&lt;/b&gt;에서 Disconnect 키를 다른 조합으로 바꾸는 편이 낫다. 공식 문서에는 macOS 기본값이 &lt;code&gt;Command+Option+&lt;/code&gt; 또는 &lt;code&gt;Command+Q/W&lt;/code&gt;로 표시되고, 설정 화면의 Reset to Default로 원상복구할 수 있다고 적혀 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 건 &amp;ldquo;macOS 단축키를 무조건 우회하는 비법&amp;rdquo;을 찾는 게 아니라 Parsec 클라이언트 단축키와 원격 호스트로 보낼 키를 겹치지 않게 만드는 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Immersive Mode는 언제 필요한가&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/20WIq/dJMcafHxIp4/AAAAAAAAAAAAAAAAAAAAADejHhpbx95_fcoJFOXfJh6VHGyyQEKM0m8N5_FM82j3/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=GxPXBlOMnNyohz2CCKKpBI%2FKzO8%3D&quot; alt=&quot;Parsec 고급 설정의 Command/Ctrl 교환과 Immersive Mode 설명&quot; /&gt;
&lt;figcaption&gt;Parsec 고급 설정의 Command/Ctrl 교환과 Immersive Mode 설명&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Cmd+Tab&lt;/code&gt;, &lt;code&gt;Windows+R&lt;/code&gt;, Alt 계열 키처럼 로컬 운영체제가 먼저 처리하는 조합을 원격 PC로 보내려면 Immersive Mode가 도움이 된다. Parsec 고급 설정에서는 키보드와 마우스를 모두 잡는 모드, 키보드만 잡는 모드, 마우스만 잡는 모드를 구분한다. macOS에서는 접근성 권한도 허용해야 Command 조합을 제대로 캡처할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 Immersive Mode를 켠다고 Parsec 자체의 종료 단축키가 사라지는 것은 아니다. 이 글의 &lt;code&gt;Cmd+W&lt;/code&gt; 문제는 Hotkeys에서 Disconnect 조합을 확인하는 게 먼저고, Immersive Mode는 그다음 단계다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;F1~F12가 안 먹는 경우&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/rc39W/dJMcagNeJPK/AAAAAAAAAAAAAAAAAAAAAOYtWzV9BoX5U9xwpF5GfKiVCFMq1jE646_f2mrPqErk/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=w0060pDJ9HXeDs4VtU1KRIwS%2BJ0%3D&quot; alt=&quot;Apple 공식 문서의 기능 키 동작 변경 안내&quot; /&gt;
&lt;figcaption&gt;Apple 공식 문서의 기능 키 동작 변경 안내&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맥 키보드의 F1~F12가 밝기나 음량 조절로 먼저 동작하면 &lt;code&gt;Fn&lt;/code&gt;을 함께 누르거나 시스템 설정에서 기능 키 기본 동작을 바꿔야 한다. Parsec 공식 문서도 macOS function key 문제가 보이면 Apple의 기능 키 설정을 먼저 확인하라고 안내한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;내가 정착한 설정&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원격 Windows의 일반 Ctrl 단축키는 맥의 Cmd로 사용&lt;/li&gt;
&lt;li&gt;원격 탭 닫기는 &lt;code&gt;Control+W&lt;/code&gt;로 구분&lt;/li&gt;
&lt;li&gt;Parsec Disconnect는 잘못 누르기 어려운 조합으로 변경&lt;/li&gt;
&lt;li&gt;Alt+Tab, Win+R을 자주 쓰는 세션에서만 Immersive Mode 활성화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인을 macOS 하나로만 보면 계속 헷갈린다. Parsec의 Command/Ctrl 교환, 자체 Hotkeys, macOS 접근성&amp;middot;기능 키 설정을 세 층으로 나눠 보면 어느 단계에서 키가 소비됐는지 금방 찾을 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://support.parsec.app/hc/en-us/articles/32381778420372-Configure-Hotkeys&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Parsec Configure Hotkeys&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://support.parsec.app/hc/en-us/articles/32381443626516-All-Advanced-Configuration-Options&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Parsec Advanced Configuration Options&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://support.apple.com/en-us/102439&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Apple: 기능 키 사용 방법&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>환경설정</category>
      <category>MAC</category>
      <category>parsec</category>
      <category>단축키</category>
      <category>원격접속</category>
      <category>키매핑</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/26</guid>
      <comments>https://gorani-kr.tistory.com/26#entry26comment</comments>
      <pubDate>Sun, 26 Jul 2026 10:40:46 +0900</pubDate>
    </item>
    <item>
      <title>CTF - Cascading the Seven Seas: 브라우저 DevTools로 숨겨진 플래그 찾기</title>
      <link>https://gorani-kr.tistory.com/25</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;웹 CTF에서 화면만 보고 버튼을 누르다 보면 정작 단서는 CSS에 숨어 있는 경우가 있다. &amp;ldquo;Cascading the Seven Seas&amp;rdquo;를 보자마자 CSS(Cascading Style Sheets)를 의심했고, Chrome DevTools의 Sources 전체 검색으로 범위를 줄였다. 이 글은 현재 문제의 플래그나 정답을 공개하지 않고, 다른 웹 문제에도 그대로 쓸 수 있는 조사 순서만 정리한다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bkHHBU/dJMcagzBkUc/AAAAAAAAAAAAAAAAAAAAAObkhdedF3rhP0_hGkljWpZPOPaQKCHHqE4FNcthQOr7/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=qtR5oOE3vvU1NZepjU38kaVKxjg%3D&quot; alt=&quot;웹 CTF에서 CSS 단서를 찾는 Chrome DevTools 조사 흐름&quot; /&gt;
&lt;figcaption&gt;웹 CTF에서 CSS 단서를 찾는 Chrome DevTools 조사 흐름&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Elements만 보지 말고 로드된 리소스 전체를 본다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 화면의 DOM만 펼쳐보다가 단서가 없으면 &amp;ldquo;소스에 아무것도 없다&amp;rdquo;고 착각하기 쉽다. 실제로는 인라인 &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt;, 동적으로 추가된 스타일시트, &lt;code&gt;@import&lt;/code&gt;로 연결된 파일, 가상 요소의 &lt;code&gt;content&lt;/code&gt; 등 여러 층에 문자열이 나뉘어 있을 수 있다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/enIdew/dJMcadpiFtU/AAAAAAAAAAAAAAAAAAAAADnKbQSNidH8qsYkVsMGtnzfoQWQrz-ApC9E2_LX9tRR/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=fL%2FwhubF6KY8vZ3%2BvxVma5PX3jA%3D&quot; alt=&quot;Chrome DevTools 공식 Search 문서의 전체 리소스 검색 기능&quot; /&gt;
&lt;figcaption&gt;Chrome DevTools 공식 Search 문서의 전체 리소스 검색 기능&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Chrome DevTools에서 &lt;code&gt;Ctrl+Shift+F&lt;/code&gt;(macOS는 &lt;code&gt;Cmd+Option+F&lt;/code&gt;)를 누르면 현재 페이지가 불러온 리소스 전체를 검색한다. 파일 트리를 한 개씩 여는 것보다 빠르다. 처음에는 플래그 접두사 하나만 찾기보다 아래처럼 구조를 드러내는 키워드를 번갈아 본다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@import
content:
::before
::after
--flag
part
hidden
display: none&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열이 여러 조각이면 검색 결과의 파일 경로와 줄 번호를 먼저 기록한다. 발견 순서대로 붙이는 게 정답 순서라는 보장은 없기 때문이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Sources와 Network의 역할을 나눠 쓴다&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/BQuU2/dJMcagzBkUC/AAAAAAAAAAAAAAAAAAAAAMM5nAHMEfRWMkpK5EpdDCuYD83uLIL6n4So55ORaGsP/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=FTuFeSwLZrwJN3ggTciybSBs97Q%3D&quot; alt=&quot;Chrome DevTools Sources 패널 공식 안내&quot; /&gt;
&lt;figcaption&gt;Chrome DevTools Sources 패널 공식 안내&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Sources&lt;/b&gt;는 로드된 파일 내용과 인라인 스크립트&amp;middot;스타일을 읽기 좋다. &lt;b&gt;Network&lt;/b&gt;는 어떤 요청이 실제로 나갔는지, 리다이렉트가 있었는지, CSS가 캐시에서 왔는지 확인하는 데 낫다. 스타일 파일이 목록에 없다고 느껴지면 Network에서 CSS 필터를 걸고 새로고침한다. 응답 본문과 Initiator를 함께 보면 어느 문서가 그 파일을 불렀는지 알 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;가상 요소는 DOM 텍스트와 다르게 보인다&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/c9xk0K/dJMcagzBkUG/AAAAAAAAAAAAAAAAAAAAANkXDTT6ryd84yLi0odyk0f5uwGuzhXQ-4gqaop9pufP/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=sMYXfFclQBRl%2Bg0E9Tm9V1%2B2Sq4%3D&quot; alt=&quot;MDN의 ::before 가상 요소 문서&quot; /&gt;
&lt;figcaption&gt;MDN의 ::before 가상 요소 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;::before&lt;/code&gt;와 &lt;code&gt;::after&lt;/code&gt;는 HTML에 텍스트 노드가 없어도 화면에 내용을 그릴 수 있다. Elements에서 선택한 요소의 Styles 또는 Computed를 보면 가상 요소 규칙이 따로 나타난다. JavaScript의 &lt;code&gt;element.textContent&lt;/code&gt;만 출력해서는 이 문자열을 놓칠 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.ocean::after {
  content: var(--fragment);
  opacity: 0;
  position: absolute;
  left: -9999px;
}&lt;/code&gt;&lt;/pre&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/7IEuo/dJMcagzBkUI/AAAAAAAAAAAAAAAAAAAAAHY1YPYfDQfzqNXQ6Nekle5Lxp4p_jtqsC8mSztMWAgS/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=5xnJp6tXWO8svi1dUZfM3yezA0s%3D&quot; alt=&quot;MDN content 속성 문서 &amp;mdash; 가상 요소에 생성 콘텐츠를 넣는 방식&quot; /&gt;
&lt;figcaption&gt;MDN content 속성 문서 &amp;mdash; 가상 요소에 생성 콘텐츠를 넣는 방식&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;content&lt;/code&gt;는 일반적으로 가상 요소와 함께 쓰인다. 화면에서 투명하거나 뷰포트 밖으로 밀려 있어도 CSS 원문에는 값이 남는다. CSS 변수(&lt;code&gt;--fragment&lt;/code&gt;)를 거쳐 값이 들어간다면 변수 정의 위치까지 따라가야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@import 체인은 Network로 순서를 확인한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스타일시트 안의 &lt;code&gt;@import&lt;/code&gt;가 다른 도메인이나 다음 단계 파일을 부르면 &amp;ldquo;cascading&amp;rdquo;이라는 제목 자체가 파일 연결 구조를 암시할 수 있다. 이때 무작정 URL을 추측하지 않고, Network의 요청 순서와 Initiator를 따라간다. 같은 파일이 중복 로드되거나 캐시 때문에 보이지 않으면 Disable cache를 켠 상태로 새로고침하는 것도 도움이 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;내가 쓰는 최소 체크리스트&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;페이지 소스와 Elements에서 인라인 style/script 확인&lt;/li&gt;
&lt;li&gt;전체 검색으로 접두사&amp;middot;&lt;code&gt;content&lt;/code&gt;&amp;middot;&lt;code&gt;@import&lt;/code&gt; 검색&lt;/li&gt;
&lt;li&gt;Network의 CSS/JS 응답과 Initiator 확인&lt;/li&gt;
&lt;li&gt;&lt;code&gt;::before&lt;/code&gt;, &lt;code&gt;::after&lt;/code&gt;, CSS 변수, 숨김 속성 확인&lt;/li&gt;
&lt;li&gt;발견한 조각은 파일&amp;middot;줄&amp;middot;의존 관계와 함께 기록&lt;/li&gt;
&lt;li&gt;문제 답과 플래그는 대회 정책을 확인한 뒤 공개&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제에서 가장 크게 남은 건 특정 문자열이 아니라 검색 습관이었다. 파일 하나씩 눈으로 훑기보다 브라우저가 이미 로드한 범위를 먼저 검색하면, CSS와 DOM의 경계를 이용한 문제를 훨씬 빠르게 구조화할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같이 읽기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력에 따라 응답이 달라지는 웹 문제를 분석한 과정은 &lt;a href=&quot;https://gorani-kr.tistory.com/30&quot;&gt;Boolean Blind SQLi로 한 글자씩 데이터 뽑아내기&lt;/a&gt;에 따로 정리했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.chrome.com/docs/devtools/search/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Chrome DevTools Search&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.chrome.com/docs/devtools/sources/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Chrome DevTools Sources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/::before&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MDN ::before&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/content&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MDN content&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>보안</category>
      <category>css</category>
      <category>CTF</category>
      <category>devtools</category>
      <category>WEB</category>
      <category>브라우저보안</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/25</guid>
      <comments>https://gorani-kr.tistory.com/25#entry25comment</comments>
      <pubDate>Sat, 25 Jul 2026 13:59:00 +0900</pubDate>
    </item>
    <item>
      <title>ChatGPT Plus, GitHub Copilot, Google Colab Pro 뭐가 달라?</title>
      <link>https://gorani-kr.tistory.com/24</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;셋 다 이름에 AI가 붙어서 비슷한 구독처럼 보이지만, 실제로 돈을 내는 대상이 완전히 다르다. C++ 알고리즘과 보안 공부를 병행하면서 셋을 같이 알아봤다가, 대화 모델&amp;middot;IDE 도구&amp;middot;클라우드 계산 환경을 한 줄에 놓고 비교하면 안 된다는 걸 알게 됐다. 2026년에는 GitHub Copilot 과금 방식까지 바뀌어서 예전 비교 글도 그대로 믿기 어렵다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/HNICs/dJMcabygOGg/AAAAAAAAAAAAAAAAAAAAAM4zWaxV8af_EbN5F8zhraufGtH24AdNSm1ZZffX4ZZ-/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=5ojR%2BM2BHb0oqN%2BJwUb72QB0E9E%3D&quot; alt=&quot;ChatGPT&amp;middot;GitHub Copilot&amp;middot;Google Colab 역할 비교 대표 이미지&quot; /&gt;
&lt;figcaption&gt;ChatGPT&amp;middot;GitHub Copilot&amp;middot;Google Colab 역할 비교 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한 문장으로 나누면 이렇다&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th&gt;서비스&lt;/th&gt;
&lt;th&gt;주로 사는 것&lt;/th&gt;
&lt;th&gt;내가 쓸 상황&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT&lt;/td&gt;
&lt;td&gt;대화&amp;middot;추론&amp;middot;문서/코드 분석&lt;/td&gt;
&lt;td&gt;개념 질문, 오류 원인 정리, 코드 리뷰&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Copilot&lt;/td&gt;
&lt;td&gt;IDE 안의 코드 완성&amp;middot;에이전트 기능&lt;/td&gt;
&lt;td&gt;반복 코드 작성, 저장소 맥락에서 수정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Colab&lt;/td&gt;
&lt;td&gt;브라우저 노트북과 원격 CPU/GPU&lt;/td&gt;
&lt;td&gt;Python 실험, ML 학습, 재현 가능한 노트북&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니까 &amp;ldquo;어느 AI가 가장 똑똑한가&amp;rdquo;보다 &amp;ldquo;지금 병목이 생각 정리인지, 코딩 속도인지, 계산 자원인지&amp;rdquo;를 먼저 봐야 한다. 이 구분만 해도 필요 없는 중복 구독을 많이 줄일 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ChatGPT는 설명과 탐색에 가깝다&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/1odhU/dJMcacD4etK/AAAAAAAAAAAAAAAAAAAAAGXofJ4H-vU1b4924iSDzU6xQx-IsjvpC79fJCgClnji/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=dVwB6ADEs2abTriuUuakHe5fM9Q%3D&quot; alt=&quot;OpenAI 공식 ChatGPT 요금제 페이지&quot; /&gt;
&lt;figcaption&gt;OpenAI 공식 ChatGPT 요금제 페이지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatGPT는 대화형 작업 공간이다. 알고리즘의 시간복잡도를 다른 방식으로 설명해달라고 하거나, 크래시 로그와 코드 일부를 같이 놓고 의심할 지점을 좁힐 때 편하다. 다만 ChatGPT 구독과 OpenAI API 사용료는 별개다. 웹에서 Plus를 구독했다고 개인 스크립트의 API 호출이 무료가 되는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안 공부에서는 답을 바로 붙여넣는 것보다 &amp;ldquo;이 입력을 신뢰하면 어떤 경계가 깨지는가&amp;rdquo;, &amp;ldquo;이 크래시를 재현하려면 어떤 관찰값을 더 모아야 하는가&amp;rdquo;처럼 사고 과정을 확인하는 용도로 쓰는 편이 낫다. 생성된 명령어와 취약점 판단은 공식 문서나 실제 실행 결과로 다시 확인해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GitHub Copilot은 편집기 안에서 시간을 줄인다&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/w9Ffa/dJMcajpsMHw/AAAAAAAAAAAAAAAAAAAAAH_hv2SnQ8r9vLqVB02RjMAByOQEQ7QyEF452okcVX90/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=8RYhVm7aCWvqbahvfVdcIZzmh2E%3D&quot; alt=&quot;GitHub Copilot 공식 기능 소개 페이지&quot; /&gt;
&lt;figcaption&gt;GitHub Copilot 공식 기능 소개 페이지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Copilot의 장점은 현재 열어둔 코드와 저장소 흐름을 보면서 제안을 이어간다는 점이다. 테스트 골격, 반복되는 DTO나 파서 분기, 리팩터링 초안처럼 정답보다 타이핑 양이 문제일 때 효과가 크다. 반대로 처음 배우는 알고리즘을 통째로 생성시키면 왜 그 코드가 맞는지 모른 채 통과하기 쉽다. 나는 자동완성을 받은 뒤 적어도 입력 범위, 예외 처리, 복잡도는 직접 다시 보는 기준을 잡았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2026년 6월부터 GitHub는 Copilot의 프리미엄 요청 단위 대신 &lt;i&gt;GitHub AI Credits&lt;/i&gt; 기반 사용량 과금으로 전환했다. 기본 코드 완성과 Next Edit 제안은 포함되지만 모델&amp;middot;에이전트 사용량을 볼 때는 예전의 &amp;ldquo;월 몇 회&amp;rdquo; 비교표보다 현재 공식 플랜을 보는 게 정확하다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/cpyHHl/dJMcabLWJnZ/AAAAAAAAAAAAAAAAAAAAAP0MorIV_aa_gZGDGJO8d-P8FZg8VXjRRW8RsVdX0-JZ/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=qQPr%2BCrgUBAW23TkBP0cCUDni4E%3D&quot; alt=&quot;GitHub 공식 문서의 검증된 학생 Copilot 무료 이용 안내&quot; /&gt;
&lt;figcaption&gt;GitHub 공식 문서의 검증된 학생 Copilot 무료 이용 안내&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검증된 GitHub Education 학생은 Copilot 기능을 무료로 이용할 수 있다. 학교 메일만 있다고 자동 적용되는 것은 아니고 GitHub Education의 학생 인증을 마쳐야 하며, 자격은 주기적으로 재검토된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Colab은 지능보다 실행 환경이 핵심이다&lt;/h2&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bCVtDc/dJMcajpsMHz/AAAAAAAAAAAAAAAAAAAAALt8eMaq6ve42mhp9SB84bdWa7EtwlC87zmD3M4DGlJ_/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=KaHmcd7FJZUFV1fZG%2FRobhxI2Ac%3D&quot; alt=&quot;Google Colab 공식 노트북 첫 화면&quot; /&gt;
&lt;figcaption&gt;Google Colab 공식 노트북 첫 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Colab은 브라우저에서 Jupyter 노트북을 실행하고 필요할 때 원격 CPU&amp;middot;GPU&amp;middot;TPU를 쓰는 서비스다. 코드 제안 기능도 들어오고 있지만, 내가 비용을 고민할 이유는 여전히 런타임 시간과 가속기다. 무료 버전으로 작은 실험부터 돌려보고, 세션이 자주 끊기거나 계산 자원이 꾸준히 필요할 때 Pro&amp;middot;Pro+&amp;middot;Pay As You Go를 검토하는 순서가 맞다. 특정 GPU가 항상 배정되는 서비스는 아니므로 과제나 시연 직전에 하드웨어를 단정해두면 곤란하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;지금 고른다면&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개념을 이해하고 설계&amp;middot;디버깅 대화를 길게 이어가고 싶다 &amp;rarr; ChatGPT&lt;/li&gt;
&lt;li&gt;VS Code나 JetBrains에서 실제 코드 작성 시간을 줄이고 싶다 &amp;rarr; GitHub Copilot&lt;/li&gt;
&lt;li&gt;Python 노트북과 GPU 실행 환경이 필요하다 &amp;rarr; Google Colab&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 개를 동시에 결제하기보다 무료 플랜으로 병목을 확인하는 게 먼저다. 내 경우 보안 공부에서는 설명과 코드 검토가 중요해서 대화형 도구의 체감이 컸고, Copilot은 학생 혜택을 활용할 수 있을 때 붙이는 쪽이 합리적이었다. Colab 유료 플랜은 실제로 GPU 사용량이 늘어났을 때 결정해도 늦지 않았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://chatgpt.com/pricing/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ChatGPT 요금제&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/features/copilot&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GitHub Copilot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/copilot/how-tos/manage-your-account/free-access-with-copilot-student&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GitHub Copilot Student 무료 이용&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.blog/news-insights/company-news/github-copilot-is-moving-to-usage-based-billing/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GitHub Copilot 사용량 과금 전환 안내&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://colab.research.google.com/signup&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Google Colab 유료 서비스&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발</category>
      <category>ai-tools</category>
      <category>ChatGPT</category>
      <category>colab</category>
      <category>github-copilot</category>
      <category>개발환경</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/24</guid>
      <comments>https://gorani-kr.tistory.com/24#entry24comment</comments>
      <pubDate>Sat, 25 Jul 2026 13:58:11 +0900</pubDate>
    </item>
    <item>
      <title>선형대수학 &amp;mdash; Ax=b의 해는 'p + 동차해'다 (Theorem 6과 평행이동의 기하)</title>
      <link>https://gorani-kr.tistory.com/23</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/bxlkpe/dJMcag7p2AG/AAAAAAAAAAAAAAAAAAAAAH0Md8Wjsr3C-2CG2jokW4SmRVk_kqVuBC7T-vMC66RI/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=GQw1T%2FoMIZzjrdJuwCkEO4uAIaA%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/bxlkpe/dJMcag7p2AG/AAAAAAAAAAAAAAAAAAAAAH0Md8Wjsr3C-2CG2jokW4SmRVk_kqVuBC7T-vMC66RI/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=GQw1T%2FoMIZzjrdJuwCkEO4uAIaA%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bxlkpe/dJMcag7p2AG/AAAAAAAAAAAAAAAAAAAAAH0Md8Wjsr3C-2CG2jokW4SmRVk_kqVuBC7T-vMC66RI/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=GQw1T%2FoMIZzjrdJuwCkEO4uAIaA%3D&quot; alt=&quot;Ax=b 해의 구조 p + 동차해 대표 이미지&quot; /&gt;
&lt;figcaption&gt;Ax=b 해의 구조 p + 동차해 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Ax = b&lt;/code&gt;를 풀었더니 답이 무수히 많이 나오는 경우가 있다. 이걸 어떻게 한 줄로 깔끔하게 표현하느냐가 이번 과제의 핵심이었는데, 답은 &quot;아무 해 하나 + 동차 해 전부&quot;라는 형태로 분해된다는 거였다. 이걸 한 번 익혀두니 동차/비동차 풀이가 사실상 같은 작업이 됐고, 나중에 미분방정식에서도 거의 똑같은 패턴이 또 나온다고 해서 신기했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동차(homogeneous)는 &lt;code&gt;Ax = 0&lt;/code&gt;처럼 우변이 모두 0인 경우고, 비동차(nonhomogeneous)는 &lt;code&gt;Ax = b&lt;/code&gt;처럼 우변에 0이 아닌 값이 있는 경우다. 동차는 무조건 &lt;code&gt;x = 0&lt;/code&gt;이라는 자명해를 가지니까, 여기서 궁금한 건 &quot;해가 있냐 없냐&quot;가 아니라 &quot;자명해 말고 다른 해가 있느냐&quot;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;David Lay 1.5절 Theorem 6은 이렇게 말한다. &lt;code&gt;Ax = b&lt;/code&gt;가 어떤 &lt;code&gt;b&lt;/code&gt;에 대해 해를 가진다고 하고, &lt;code&gt;p&lt;/code&gt;를 그 해 하나(특수해)라고 하면, &lt;code&gt;Ax = b&lt;/code&gt;의 해 집합은 &lt;code&gt;w = p + vₕ&lt;/code&gt; 형태의 모든 벡터의 집합이다. 여기서 &lt;code&gt;vₕ&lt;/code&gt;는 동차 방정식 &lt;code&gt;Ax = 0&lt;/code&gt;의 임의의 해다. 풀어서 말하면 비동차의 일반해는 비동차의 특수해 하나에 동차의 일반해 전부를 더한 것과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이게 성립하는지는 증명이 짧아서 직접 따라가 보는 게 빠르다. &lt;code&gt;p&lt;/code&gt;가 &lt;code&gt;Ax = b&lt;/code&gt;의 한 해라고 하면 &lt;code&gt;Ap = b&lt;/code&gt;고, &lt;code&gt;vₕ&lt;/code&gt;가 &lt;code&gt;Ax = 0&lt;/code&gt;의 한 해라고 하면 &lt;code&gt;Avₕ = 0&lt;/code&gt;이다. &lt;code&gt;w = p + vₕ&lt;/code&gt;를 &lt;code&gt;A&lt;/code&gt;에 넣으면 &lt;code&gt;A(p + vₕ) = Ap + Avₕ = b + 0 = b&lt;/code&gt;가 되니까 &lt;code&gt;w&lt;/code&gt;도 &lt;code&gt;Ax = b&lt;/code&gt;의 해다. 반대로 &lt;code&gt;w&lt;/code&gt;가 &lt;code&gt;Ax = b&lt;/code&gt;의 어떤 해라고 하면 &lt;code&gt;A(w - p) = Aw - Ap = b - b = 0&lt;/code&gt;이라서 &lt;code&gt;w - p&lt;/code&gt;는 &lt;code&gt;Ax = 0&lt;/code&gt;의 해가 되고, &lt;code&gt;vₕ = w - p&lt;/code&gt;로 두면 &lt;code&gt;w = p + vₕ&lt;/code&gt; 형태로 항상 쓸 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기하학적으로 보면 이게 왜 자연스러운지 더 잘 보인다. &lt;code&gt;Ax = 0&lt;/code&gt;의 해 집합은 항상 원점을 지나는 직선이나 평면이고, &lt;code&gt;Ax = b&lt;/code&gt;의 해 집합은 그걸 &lt;code&gt;p&lt;/code&gt;만큼 통째로 평행 이동한 것이다. 벡터 &lt;code&gt;p&lt;/code&gt;는 원점에서 그 평면이나 직선이 어디로 옮겨졌는지 알려주는 이정표고, 방향(&lt;code&gt;vₕ&lt;/code&gt;)은 그대로인 채 시작점만 옮겨진 셈이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6번과 20번 비교하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과제 6번은 동차 방정식이었다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; x +  3y -  5z = 0
 x +  4y -  8z = 0
-3x -  7y +  9z = 0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가우스 소거법으로 풀면 RREF가&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ 1   0   4 | 0 ]
[ 0   1  -3 | 0 ]
[ 0   0   0 | 0 ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 나오고, &lt;code&gt;x = -4z&lt;/code&gt;, &lt;code&gt;y = 3z&lt;/code&gt;, &lt;code&gt;z&lt;/code&gt;는 자유 변수라서 매개변수 벡터로 정리하면 &lt;code&gt;x = z&amp;middot;(-4, 3, 1)&lt;/code&gt;이다. 원점을 지나는 직선이고 방향은 &lt;code&gt;(-4, 3, 1)&lt;/code&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;20번은 계수가 똑같고 우변만 비동차였다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; x +  3y -  5z =  4
 x +  4y -  8z =  7
-3x -  7y +  9z = -6&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계수 행렬이 6번과 같으니 행 연산 결과도 거의 같고 우변만 따라온다. RREF는&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ 1   0   4 | -5 ]
[ 0   1  -3 |  3 ]
[ 0   0   0 |  0 ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;x₁ = -5 - 4z&lt;/code&gt;, &lt;code&gt;x₂ = 3 + 3z&lt;/code&gt;, &lt;code&gt;z&lt;/code&gt;는 자유라서 &lt;code&gt;x = (-5, 3, 0) + z&amp;middot;(-4, 3, 1)&lt;/code&gt;로 정리된다. 특수해 &lt;code&gt;p = (-5, 3, 0)&lt;/code&gt;이고 동차해 &lt;code&gt;vₕ&lt;/code&gt;는 6번에서 구한 것과 정확히 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6번은 원점을 지나는 직선, 20번은 같은 직선을 &lt;code&gt;p = (-5, 3, 0)&lt;/code&gt;만큼 평행 이동한 직선이다. 방향 벡터가 &lt;code&gt;(-4, 3, 1)&lt;/code&gt;로 똑같으니 두 직선은 평행하다. 과제에서 두 문제를 비교하라던 게 바로 이 결론이었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;헷갈렸던 것들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특수해 &lt;code&gt;p&lt;/code&gt;는 하나로 정해지는 게 아니다. 해가 무수히 많을 때 그중 아무거나 골라도 &lt;code&gt;p&lt;/code&gt;로 쓸 수 있는데, 보통 자유 변수에 0을 대입한 해가 가장 깔끔하다. 20번에서 &lt;code&gt;z = 0&lt;/code&gt;을 대입하면 &lt;code&gt;p = (-5, 3, 0)&lt;/code&gt;이 나오지만 &lt;code&gt;z = 1&lt;/code&gt;을 잡아서 &lt;code&gt;p' = (-9, 6, 1)&lt;/code&gt;을 골라도 똑같이 정답이다. 다만 형태가 더 지저분해질 뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동차 시스템은 항상 일관적(consistent)이다. 비동차는 &lt;code&gt;0 = nonzero&lt;/code&gt; 모순이 나와서 해가 없을 수 있지만, 동차는 우변이 다 0이라 그런 모순이 절대 안 나온다. 최소한 자명해 &lt;code&gt;x = 0&lt;/code&gt;은 항상 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자유 변수 개수가 곧 동차해의 차원이다. &lt;code&gt;Ax = 0&lt;/code&gt;을 풀어서 자유 변수가 1개면 직선, 2개면 평면, 3개면 3차원 부분공간 식이다. &lt;code&gt;Ax = b&lt;/code&gt;의 해 집합 차원도 똑같은데, 위치만 평행 이동한 거니까 당연한 얘기다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Ax = b&lt;/code&gt;의 해가 유일하다는 것과 &lt;code&gt;Ax = 0&lt;/code&gt;의 해가 자명해뿐이라는 건 같은 말이다. &lt;code&gt;Ax = b&lt;/code&gt;의 해가 두 개(&lt;code&gt;w₁&lt;/code&gt;, &lt;code&gt;w₂&lt;/code&gt;)라면 &lt;code&gt;w₁ - w₂&lt;/code&gt;는 동차해이면서 0이 아닌 벡터고, 반대로 동차해가 자명해뿐이라면 &lt;code&gt;w - p = 0&lt;/code&gt;이라 &lt;code&gt;w = p&lt;/code&gt;로 유일해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 패턴은 미분방정식에서도 그대로 나온다. &lt;code&gt;y'' + p(x)y' + q(x)y = f(x)&lt;/code&gt; 같은 비동차 미분방정식의 일반해도 &lt;code&gt;y(x) = y_c(x) + y_p(x)&lt;/code&gt;로 분해되는데, &lt;code&gt;y_c&lt;/code&gt;는 동차 방정식의 일반해(complementary solution), &lt;code&gt;y_p&lt;/code&gt;는 비동차의 특수해(particular solution)다. 선형대수에서 익힌 직관이 그대로 넘어간다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;David C. Lay, &lt;i&gt;Linear Algebra and Its Applications&lt;/i&gt;, 1.5절 Theorem 6&lt;/li&gt;
&lt;li&gt;Mathematics LibreTexts &amp;mdash; The General Solution of a Linear System&lt;/li&gt;
&lt;li&gt;미분방정식 교재의 Nonhomogeneous Linear Equations 챕터 (같은 정리의 연속체 버전)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 모든 해가 p + v 꼴인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Ap=b&lt;/code&gt;인 특수해 p 하나를 잡고 다른 해 x를 빼면 &lt;code&gt;A(x-p)=0&lt;/code&gt;이다. 따라서 x-p는 null space에 있고, 모든 해는 &lt;code&gt;p+v&lt;/code&gt;(단, &lt;code&gt;v&amp;isin;N(A)&lt;/code&gt;)로 쓸 수 있다. 비동차 해집합은 원점을 지나는 부분공간 자체가 아니라 null space를 p만큼 평행이동한 집합이다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/OOJef/dJMcacYh0QO/AAAAAAAAAAAAAAAAAAAAAJEoWUJRjUExKKAebJq9zrVERq4xifM-o7JHH93Hk4yU/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=0zMuIRkYdLvCHQxT4%2F4JuTxPG1I%3D&quot; alt=&quot;MIT 18.06 Ax=0과 특수해 강의&quot; /&gt;
&lt;figcaption&gt;MIT 18.06 Ax=0과 특수해 강의&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/cz53Xw/dJMcadpiGda/AAAAAAAAAAAAAAAAAAAAAEXfuyB8b68etOaNPvbw4AhFmKjftPl3A7vKcHkAs5SM/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=SZNThBmXdLT2CABHXZfVcHgiarQ%3D&quot; alt=&quot;LibreTexts 선형시스템 해집합 문서&quot; /&gt;
&lt;figcaption&gt;LibreTexts 선형시스템 해집합 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bkQj00/dJMcadpiGdc/AAAAAAAAAAAAAAAAAAAAACiPMji2vPTXMr7uJHhjJA5q_YeRLuiKDIAtGgdEqRvH/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=%2BIngiVs%2B8sBpZaSNEPQ%2Fh2hy0Sc%3D&quot; alt=&quot;3Blue1Brown Essence of Linear Algebra 자료&quot; /&gt;
&lt;figcaption&gt;3Blue1Brown Essence of Linear Algebra 자료&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bCBx7G/dJMcacYh0QU/AAAAAAAAAAAAAAAAAAAAAAaKvrubongRxdu7MOtmDlYWlDgfy4unVp9bQ44vcSLt/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=50GeJwUflNmbat2fBAhvWdMFofA%3D&quot; alt=&quot;Wolfram MathWorld Null Space&quot; /&gt;
&lt;figcaption&gt;Wolfram MathWorld Null Space&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특수해는 하나로 고정되지 않는다. 다른 특수해를 골라도 둘의 차이가 null space 원소라서 전체 집합은 같아진다. 이 사실을 알고 나니 매개변수 해를 구할 때 &amp;ldquo;특수해를 예쁘게 골라도 되는 이유&amp;rdquo;가 명확해졌다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-7-solving-ax-0-pivot-variables-special-solutions/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MIT Solving Ax=0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.3blue1brown.com/topics/linear-algebra&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;3Blue1Brown Linear Algebra&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mathworld.wolfram.com/NullSpace.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MathWorld Null Space&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>학습&amp;middot;과제</category>
      <category>homogeneous</category>
      <category>LinearAlgebra</category>
      <category>ParticularSolution</category>
      <category>Theorem6</category>
      <category>선형대수</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/23</guid>
      <comments>https://gorani-kr.tistory.com/23#entry23comment</comments>
      <pubDate>Sat, 25 Jul 2026 13:57:31 +0900</pubDate>
    </item>
    <item>
      <title>선형대수학 &amp;mdash; 피벗 위치로 벡터들이 R^n을 생성하는지(Span) 판단하기</title>
      <link>https://gorani-kr.tistory.com/22</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/bWK2xM/dJMcaa7jk8W/AAAAAAAAAAAAAAAAAAAAAGi3iq0EnY9zHQqNgF-_Fx85edMGKJBgaRtK_REE-8Ml/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=kwdJiRN%2FP2%2BnFG5o0lxm%2FgAgGwM%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/bWK2xM/dJMcaa7jk8W/AAAAAAAAAAAAAAAAAAAAAGi3iq0EnY9zHQqNgF-_Fx85edMGKJBgaRtK_REE-8Ml/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=kwdJiRN%2FP2%2BnFG5o0lxm%2FgAgGwM%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bWK2xM/dJMcaa7jk8W/AAAAAAAAAAAAAAAAAAAAAGi3iq0EnY9zHQqNgF-_Fx85edMGKJBgaRtK_REE-8Ml/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=kwdJiRN%2FP2%2BnFG5o0lxm%2FgAgGwM%3D&quot; alt=&quot;피벗 위치로 Span과 Rank를 판단하는 글 대표 이미지&quot; /&gt;
&lt;figcaption&gt;피벗 위치로 Span과 Rank를 판단하는 글 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선형대수 과제를 풀다 보면 &quot;이 벡터들이 공간 전체를 채울 수 있냐&quot;는 질문이 계속 나온다. 처음엔 이게 왜 중요한지 잘 몰랐는데, 알고 보니 머신러닝에서 특징 벡터가 충분한지 따질 때도, 그래픽스에서 기저 벡터로 모든 점을 표현할 수 있는지 볼 때도 결국 같은 질문이더라. David Lay 교재 1.4절 Theorem 4가 이 질문에 대한 답을 정리해주는데, 학교 시험에 거의 매 학기 나온다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Span이 뭔지부터 다시 짚고 가면, 벡터 집합 &lt;code&gt;{v₁, v₂, ..., vₙ}&lt;/code&gt;의 span은 그 벡터들의 모든 가능한 선형 결합으로 만들어지는 집합이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Span{v₁, ..., vₙ} = { c₁v₁ + c₂v₂ + ... + cₙvₙ | cᵢ &amp;isin; ℝ }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;R&amp;sup3;&lt;/code&gt;을 span한다는 건 이 벡터들을 적당히 더하고 빼고 곱해서 &lt;code&gt;R&amp;sup3;&lt;/code&gt;의 어떤 점이든 만들 수 있다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Theorem 4가 말하는 건, &lt;code&gt;A&lt;/code&gt;가 &lt;code&gt;m &amp;times; n&lt;/code&gt; 행렬일 때 다음 네 문장이 모두 같은 말이라는 거다. 하나가 참이면 나머지도 다 참이고, 하나가 거짓이면 다 거짓이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(a) 모든 &lt;code&gt;b &amp;isin; ℝᵐ&lt;/code&gt;에 대해 &lt;code&gt;Ax = b&lt;/code&gt;는 해를 가진다&lt;br /&gt;(b) 모든 &lt;code&gt;b &amp;isin; ℝᵐ&lt;/code&gt;은 &lt;code&gt;A&lt;/code&gt;의 열들의 선형 결합이다&lt;br /&gt;(c) &lt;code&gt;A&lt;/code&gt;의 열들이 &lt;code&gt;ℝᵐ&lt;/code&gt;을 생성(span)한다&lt;br /&gt;(d) &lt;code&gt;A&lt;/code&gt;는 모든 행에 피벗 위치를 가진다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(a)~(c)는 사실 같은 말을 다르게 표현한 것뿐이고, 진짜 쓸모 있는 건 (d)다. 계산으로 직접 확인할 수 있는 조건이 이거 하나이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 &quot;모든 행에 피벗&quot;이 핵심 조건인지는 이렇게 생각하면 이해가 됐다. &lt;code&gt;A&lt;/code&gt;를 사다리꼴로 만들었을 때 어떤 행이 전부 0이라면, 그 행은 &lt;code&gt;0x₁ + 0x₂ + ... = ?&lt;/code&gt; 꼴이 된다. 첨가행렬 &lt;code&gt;[A | b]&lt;/code&gt;로 같이 처리했다면 그 행은 &lt;code&gt;0 = (b의 변형된 값)&lt;/code&gt; 형태가 되고, 우변이 0이 아니면 모순이라 해가 없어진다. 반대로 &lt;code&gt;A&lt;/code&gt;의 모든 행에 피벗이 박혀 있으면 어떤 행 연산을 해도 &lt;code&gt;b&lt;/code&gt; 쪽에 모순적인 값이 만들어질 자리가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 자연스럽게 따라오는 게 있는데, &lt;code&gt;m &amp;times; n&lt;/code&gt; 행렬에서 피벗은 행에도 열에도 한 개씩만 있을 수 있어서 피벗 개수의 최대값은 &lt;code&gt;min(m, n)&lt;/code&gt;이다. 그래서 열보다 행이 많은(세로로 긴) 행렬은 모든 행에 피벗을 박는 게 애초에 불가능하고, &lt;code&gt;Rᵐ&lt;/code&gt;을 생성할 수 없다. &lt;code&gt;R&amp;sup3;&lt;/code&gt;에 있는 벡터 2개로는 절대 &lt;code&gt;R&amp;sup3;&lt;/code&gt;을 생성할 수 없다는 게 바로 이 케이스인데, 평면에 있는 두 화살표로 3차원 공간을 채울 수 없다고 생각하면 직관적으로 와닿는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;과제 1.3절 22번으로 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 벡터들이 &lt;code&gt;ℝ&amp;sup3;&lt;/code&gt;을 생성하는지 보는 문제였다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;v₁ = ( 0,  0, -2)
v₂ = ( 0, -3,  8)
v₃ = ( 4, -1, -5)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터를 열로 가지는 행렬을 만들면&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A = [  0   0   4 ]
    [  0  -3  -1 ]
    [ -2   8  -5 ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1행이 &lt;code&gt;0 0 4&lt;/code&gt;로 시작해서 피벗 잡기가 애매하길래 행 교환부터 했다. &lt;code&gt;R₁ &amp;harr; R₃&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ -2   8  -5 ]
[  0  -3  -1 ]
[  0   0   4 ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러면 이미 사다리꼴이고, 1행에 -2, 2행에 -3, 3행에 4로 모든 행에 피벗이 박혀 있다. 그러니 Theorem 4의 (d)가 참이고 (c)도 참, 이 벡터들은 &lt;code&gt;ℝ&amp;sup3;&lt;/code&gt;을 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3&amp;times;3 행렬이면 행렬식 한 번 계산해도 되는데, &lt;code&gt;det(A) &amp;ne; 0&lt;/code&gt;이면 모든 행에 피벗이 있다는 뜻과 같다. 행렬식 단원 끝나고 나면 이 방법이 더 빠르더라.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;계속 헷갈렸던 부분들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;열에 피벗&quot;과 &quot;행에 피벗&quot;을 자꾸 헷갈렸다. 이 둘은 서로 다른 정리와 연결된다. 행에 피벗이 있으면 열들이 &lt;code&gt;Rᵐ&lt;/code&gt;을 span하는 거고(Theorem 4), 열에 피벗이 있으면 열들이 선형 독립이라는 거다(Theorem 1.7). &lt;code&gt;m &amp;times; n&lt;/code&gt; 행렬에서 &lt;code&gt;m &amp;gt; n&lt;/code&gt;인 좁고 긴 행렬은 모든 열엔 피벗을 박을 수 있어도 모든 행은 못 하니까 독립일 수는 있어도 span은 못 한다. &lt;code&gt;m &amp;lt; n&lt;/code&gt;인 넓고 짧은 행렬은 반대로 span은 가능해도 독립은 아닐 수 있다. &lt;code&gt;m = n&lt;/code&gt;이면 둘 다 되거나 둘 다 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첨가행렬 &lt;code&gt;[A | b]&lt;/code&gt;에서 &quot;마지막 열에 피벗&quot;이라는 표현도 처음엔 헷갈렸는데, 이건 &lt;code&gt;b&lt;/code&gt; 쪽에 피벗이 생긴다는 뜻이고 그러면 &lt;code&gt;0 = nonzero&lt;/code&gt; 모순이라 해가 없다는 얘기다. Theorem 4의 (d)는 이게 절대 일어나지 않게 보장하는 조건인 셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Span{v₁, v₂}가 v₁과 v₂ 딱 두 벡터만 들어있는 집합이 아니라는 것도 은근 헷갈렸다. 두 벡터의 가능한 모든 선형 결합, 즉 무수히 많은 벡터의 집합이다. 두 벡터가 &lt;code&gt;R&amp;sup3;&lt;/code&gt;에 있고 서로 평행하지 않다면 그 span은 원점을 지나는 평면이 된다. 참고로 선형 결합에서 모든 계수를 0으로 두면 항상 0벡터가 나오니까, 모든 span은 0벡터를 포함한다. 어떤 집합이 부분공간인지 확인할 때 제일 먼저 체크하는 게 이 부분이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;David C. Lay, &lt;i&gt;Linear Algebra and Its Applications&lt;/i&gt;, 1.4절 Theorem 4&lt;/li&gt;
&lt;li&gt;Mathematics LibreTexts &amp;mdash; Spanning Sets and Pivot Columns&lt;/li&gt;
&lt;li&gt;Northwestern University 강의노트 &amp;mdash; Theorem 6 (열공간의 기저로서의 피벗 열)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Span 판정은 모든 b에 대해 풀리는지 묻는 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;열벡터들이 R^n을 생성한다는 말은 임의의 &lt;code&gt;b&amp;isin;R^n&lt;/code&gt;에 대해 &lt;code&gt;Ax=b&lt;/code&gt;가 풀린다는 뜻이다. 행 소거 후 모든 행에 피벗이 있으면 각 출력 좌표 방향을 빠짐없이 만들 수 있다. 열의 개수보다 피벗이 어느 행에 놓이는지가 핵심이다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/dfefkJ/dJMcadpiGcH/AAAAAAAAAAAAAAAAAAAAANBg18G6HhOAqs9Q_Qsc86WCn5WSAZP9I-8Q_izCNAAR/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=erk1UmbnAEZKdy0Kw3CcLKcVgUo%3D&quot; alt=&quot;MIT 18.06 Column Space and Nullspace 강의&quot; /&gt;
&lt;figcaption&gt;MIT 18.06 Column Space and Nullspace 강의&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/uZW7n/dJMcaa7jk8X/AAAAAAAAAAAAAAAAAAAAAHmzhGTqbWCLPFUad8eKOjOfDT6S9CcTdiku_6eN1ltA/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=KD%2BIWhOcHEfp%2FacUrPucfdPyrk8%3D&quot; alt=&quot;Khan Academy Vectors and Spaces 과정&quot; /&gt;
&lt;figcaption&gt;Khan Academy Vectors and Spaces 과정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/B4D6Q/dJMcadpiGcU/AAAAAAAAAAAAAAAAAAAAAKXAhRDpjfDnUmHTbp0ZUQ8pwnLFKd3HjiwImNDu3FPX/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=YECEarIKVN85xpJMBvqB96YUxIo%3D&quot; alt=&quot;LibreTexts Spanning Sets and Linear Independence&quot; /&gt;
&lt;figcaption&gt;LibreTexts Spanning Sets and Linear Independence&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/50cbY/dJMcag7p2AE/AAAAAAAAAAAAAAAAAAAAAGAbdAkaqE_lG2bV2gw5sTbdna9G4iWEgCuFVEIfmODR/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=OkKhDN5ILSy7%2BUk8thqBlsSXTDw%3D&quot; alt=&quot;NumPy matrix_rank 공식 문서&quot; /&gt;
&lt;figcaption&gt;NumPy matrix_rank 공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수치 행렬의 rank는 부동소수점 허용오차에 따라 달라질 수 있다. 정수&amp;middot;유리수 과제는 정확한 행 연산으로 판단하고, NumPy 결과는 피벗 개수 검산으로 사용했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;판단 순서&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;벡터를 행이 아니라 열로 세워 A를 만든다.&lt;/li&gt;
&lt;li&gt;A를 행 사다리꼴로 만든다.&lt;/li&gt;
&lt;li&gt;모든 행에 피벗이 있는지 본다.&lt;/li&gt;
&lt;li&gt;그렇다면 열들이 R^n을 span한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-6-column-space-and-nullspace/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MIT Column Space and Nullspace&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://numpy.org/doc/stable/reference/generated/numpy.linalg.matrix_rank.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;numpy.linalg.matrix_rank&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>학습&amp;middot;과제</category>
      <category>LinearAlgebra</category>
      <category>pivot</category>
      <category>span</category>
      <category>Theorem4</category>
      <category>선형대수</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/22</guid>
      <comments>https://gorani-kr.tistory.com/22#entry22comment</comments>
      <pubDate>Sat, 25 Jul 2026 13:56:56 +0900</pubDate>
    </item>
    <item>
      <title>선형대수학 &amp;mdash; 첨가행렬과 가우스 소거법으로 연립방정식 풀기</title>
      <link>https://gorani-kr.tistory.com/21</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/dKCCYW/dJMcaa7jk8O/AAAAAAAAAAAAAAAAAAAAABtgPXBHf0VKjYXj9BpQYNNsXeOVZJmIhC9M-RFTHSax/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=nLIp6GQlfiMQqAG1nSiiGJcWXU4%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/dKCCYW/dJMcaa7jk8O/AAAAAAAAAAAAAAAAAAAAABtgPXBHf0VKjYXj9BpQYNNsXeOVZJmIhC9M-RFTHSax/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=nLIp6GQlfiMQqAG1nSiiGJcWXU4%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/dKCCYW/dJMcaa7jk8O/AAAAAAAAAAAAAAAAAAAAABtgPXBHf0VKjYXj9BpQYNNsXeOVZJmIhC9M-RFTHSax/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=nLIp6GQlfiMQqAG1nSiiGJcWXU4%3D&quot; alt=&quot;첨가행렬과 가우스 소거법 대표 이미지&quot; /&gt;
&lt;figcaption&gt;첨가행렬과 가우스 소거법 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연립방정식을 풀 때마다 변수 x₁, x₂, x₃를 매번 다시 쓰고 &lt;code&gt;+&lt;/code&gt;랑 &lt;code&gt;=&lt;/code&gt;을 다 그리는 게 시간 낭비라는 생각이 들었다. 행렬로 옮기면 숫자만 다루면 되고, 행 연산 몇 번이면 답이 나온다. 가우스 소거법이 이 작업의 표준 절차이고, 선형대수 1단원 전체가 결국 이걸 변주한 거라는 걸 이번 학기에 정리하면서 알게 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 학기에 시스템 프로그래밍도 같이 듣고 있는데, 컴파일러가 코드를 토큰화하는 거랑 연립방정식을 행렬로 옮기는 게 사실 같은 발상이라는 게 재밌었다. 둘 다 &quot;기호를 잘 정리하면 계산이 쉬워진다&quot;는 얘기를 하고 있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;첨가행렬이 뭔가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연립방정식에서 변수랑 등호를 다 떼고 숫자만 표로 정리한 게 첨가행렬이다. 왼쪽은 계수, 오른쪽은 우변 상수, 그 사이를 세로선으로 구분한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; x₁ +  x₂ + 5x₃ =  0
       x₂ + 9x₃ =  0
            7x₃ = -7&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시스템을 첨가행렬로 옮기면 이렇게 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ 1  1  5 |  0 ]
[ 0  1  9 |  0 ]
[ 0  0  7 | -7 ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽 부분만 떼면 계수 행렬이고, 우변 상수까지 붙은 전체가 첨가행렬이다. 세로선은 사실 안 그어도 행렬이 성립하는데, &quot;여기부터 오른쪽이 우변이다&quot;라고 시각적으로 표시해두는 용도다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기본 행 연산&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첨가행렬을 풀기 좋은 모양으로 바꿀 때 쓰는 작업이 세 가지 있고, 이 셋은 해를 바꾸지 않는다는 게 보장된다. 두 행을 교환하는 것, 한 행에 0이 아닌 상수를 곱하는 것, 한 행에 다른 행의 배수를 더하는 것. 연립방정식에서 &quot;두 식을 빼서 변수 하나를 소거&quot;하던 걸 그대로 행렬 위에서 하는 셈이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사다리꼴과 피벗&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;행 연산으로 도달하려는 목표는 행 사다리꼴(row echelon form)이다. 정의는 두 가지다. 모든 0행은 맨 아래에 있어야 하고, 각 0이 아닌 행의 leading entry(왼쪽에서 첫 번째로 0이 아닌 수)가 위 행의 leading entry보다 오른쪽에 있어야 한다. 이 leading entry가 피벗이다. 사다리꼴은 대략 이런 모양이다(■가 피벗, *는 아무 값).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ ■ * * * | * ]
[ 0 ■ * * | * ]
[ 0 0 ■ * | * ]
[ 0 0 0 0 | 0 ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 한 단계 더 나아간 게 기약 행 사다리꼴(RREF)인데, 조건이 두 개 더 붙는다. 각 피벗 위치의 값이 정확히 1이어야 하고, 피벗이 있는 열에서 피벗 외 다른 값은 전부 0이어야 한다. RREF까지 만들면 답이 사실상 그냥 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;절차를 정리하면, 가장 왼쪽의 0이 아닌 열(피벗 열)을 찾고, 필요하면 행 교환으로 그 열의 피벗을 위로 끌어올리고, 그 아래 행들을 전부 0으로 만든 다음 다음 행&amp;middot;다음 열로 내려가며 반복한다. 사다리꼴이 되면 RREF로 가기 위해 위쪽도 0으로 만들고 피벗을 1로 정규화한다. 가우스 소거법은 사다리꼴까지, 가우스-조던 소거법은 RREF까지다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제로 풀어보면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학교 과제에 있던 문제를 하나 가져왔다. 다음 연립방정식은 우변이 전부 0이라 동차(homogeneous) 시스템이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; x +  y + 3z =  0
 x + 4y - 8z =  0
-3x - 7y + 9z =  0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첨가행렬로 옮기면:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[  1   3  -5 | 0 ]
[  1   4  -8 | 0 ]
[ -3  -7   9 | 0 ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;R₂ &amp;rarr; R₂ - R₁&lt;/code&gt;, &lt;code&gt;R₃ &amp;rarr; R₃ + 3R₁&lt;/code&gt;을 적용하면 1열이 정리된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[  1   3  -5 | 0 ]
[  0   1  -3 | 0 ]
[  0   2  -6 | 0 ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;R₃ &amp;rarr; R₃ - 2R₂&lt;/code&gt;로 2열까지 정리하면:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[  1   3  -5 | 0 ]
[  0   1  -3 | 0 ]
[  0   0   0 | 0 ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지가 사다리꼴이다. 마지막 줄이 &lt;code&gt;0 = 0&lt;/code&gt;으로 의미 없는 식이 됐는데, 이건 자유 변수가 있다는 뜻이다. &lt;code&gt;R₁ &amp;rarr; R₁ - 3R₂&lt;/code&gt;로 한 단계 더 가면 RREF가 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[  1   0   4 | 0 ]
[  0   1  -3 | 0 ]
[  0   0   0 | 0 ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;피벗이 있는 1열(x)과 2열(y)이 기본 변수, 피벗이 없는 3열(z)이 자유 변수다. 여기서 &lt;code&gt;x + 4z = 0&lt;/code&gt;이니 &lt;code&gt;x = -4z&lt;/code&gt;, &lt;code&gt;y - 3z = 0&lt;/code&gt;이니 &lt;code&gt;y = 3z&lt;/code&gt;, z는 자유롭게 움직인다. 매개변수 벡터로 정리하면:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[x]       [-4]
[y] = z &amp;middot; [ 3]
[z]       [ 1]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해 집합은 &lt;code&gt;(-4, 3, 1)&lt;/code&gt; 방향으로 뻗어나가는, 원점을 지나는 직선이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;헷갈렸던 부분들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첨가행렬 &lt;code&gt;[A | b]&lt;/code&gt;의 가장 오른쪽 열, 즉 &lt;code&gt;b&lt;/code&gt; 자리에 피벗이 생긴다는 게 무슨 뜻인지 처음엔 감이 안 왔다. &lt;code&gt;[0 0 ... 0 | nonzero]&lt;/code&gt; 같은 행이 만들어졌다는 의미인데, 식으로 풀면 &lt;code&gt;0 = 5&lt;/code&gt; 같은 모순이 된다. 이건 시스템이 불능(inconsistent)이라 해가 존재하지 않는다는 뜻이다. 반대로 계수 행렬 A의 모든 행에 피벗이 박혀 있으면 우변 b가 어떤 값이든 마지막 열까지 피벗이 밀려날 일이 없어서 항상 해가 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사다리꼴까지만 만들고 back-substitution으로 푸는 게 가우스 소거법, RREF까지 끝까지 정리해서 답을 바로 보이게 하는 게 가우스-조던 소거법이다. 시험에서는 보통 RREF까지 요구하는 편인데 풀이 과정이 깔끔하게 남기 때문인 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;피벗이 있는 열의 변수가 기본 변수, 없는 열의 변수가 자유 변수다. 자유 변수는 아무 값이나 넣어도 되고 기본 변수는 자유 변수에 대한 식으로 표현되는데, 자유 변수가 하나라도 있으면 해가 무수히 많아진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우변이 전부 0이면 동차, 아니면 비동차인데 동차 시스템은 항상 자명해(x=0)를 가져서 절대 불능이 될 수 없다. 자유 변수가 있느냐 없느냐로 &quot;자명해만 있느냐, 무수히 많느냐&quot;가 갈린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고한 자료는 David C. Lay의 Linear Algebra and Its Applications(학교 교재 1.1~1.3절), 위키피디아의 Gaussian elimination 항목(역사와 알고리즘 의사코드), Mathematics LibreTexts의 Gaussian Elimination 챕터다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;행 연산에서 끝내지 않고 해의 상태까지 보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첨가행렬을 기약 행 사다리꼴로 만드는 건 목적이 아니라 해의 구조를 읽기 위한 과정이다. 피벗 열과 자유변수를 표시하고, &lt;code&gt;[0 0 ... 0 | c]&lt;/code&gt;에서 &lt;code&gt;c&amp;ne;0&lt;/code&gt;인 행이 생기는지 확인하면 유일해&amp;middot;무수히 많은 해&amp;middot;해 없음이 구분된다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/c5odwQ/dJMcaa7jk8Q/AAAAAAAAAAAAAAAAAAAAAESDfsLmkTICo0CgCIxjX1pSTnnU3eXz7wQ4cx2YhR4C/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=EH1aUtflEQBcAoirPxg397zgRqQ%3D&quot; alt=&quot;MIT OpenCourseWare 18.06 Linear Algebra&quot; /&gt;
&lt;figcaption&gt;MIT OpenCourseWare 18.06 Linear Algebra&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/nnnBg/dJMcacYh0Qw/AAAAAAAAAAAAAAAAAAAAAMAN_Z9ij8P2Rk_IfyE4NsOUHqkglsYwcmyw3esp5viy/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=rO6Yg8AXaApEEmAdfT17ph%2BLbJk%3D&quot; alt=&quot;LibreTexts Row Reduction 학습 문서&quot; /&gt;
&lt;figcaption&gt;LibreTexts Row Reduction 학습 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/qy8Yp/dJMcaa7jk8T/AAAAAAAAAAAAAAAAAAAAAIgpX8XpvYAZoj1upVv-6s_YOt9xHJ_GaCBTwdbeNxz8/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=eMKgaT32yOc3k1Ua0sIjfjMkk%2Bo%3D&quot; alt=&quot;NumPy linalg.solve 공식 문서&quot; /&gt;
&lt;figcaption&gt;NumPy linalg.solve 공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bDAknS/dJMcacYh0QC/AAAAAAAAAAAAAAAAAAAAAIZdOlOa9imXgIvtdNy7NzqEFsX6wDUlCYjBRWIotn7z/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=zWuxOzl8rJ1j9SgJrym4NJSyUAE%3D&quot; alt=&quot;Wolfram MathWorld Gaussian Elimination&quot; /&gt;
&lt;figcaption&gt;Wolfram MathWorld Gaussian Elimination&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계산 검산에 NumPy를 쓸 수는 있지만 &lt;code&gt;linalg.solve&lt;/code&gt;는 정사각&amp;middot;full-rank 계수를 전제로 한다. 수업 문제에서는 먼저 손으로 피벗과 모순 행을 판정하고, 수치 계산은 그 뒤 검산용으로 두는 게 좋았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MIT OCW 18.06&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://numpy.org/doc/stable/reference/generated/numpy.linalg.solve.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;numpy.linalg.solve&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mathworld.wolfram.com/GaussianElimination.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Gaussian Elimination&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>학습&amp;middot;과제</category>
      <category>AugmentedMatrix</category>
      <category>GaussianElimination</category>
      <category>LinearAlgebra</category>
      <category>선형대수</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/21</guid>
      <comments>https://gorani-kr.tistory.com/21#entry21comment</comments>
      <pubDate>Sat, 25 Jul 2026 13:56:15 +0900</pubDate>
    </item>
    <item>
      <title>터미널에서 Java 컴파일하기 (javac -d bin과 패키지 디렉토리 구조)</title>
      <link>https://gorani-kr.tistory.com/20</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/1TEBm/dJMcaaGfpEA/AAAAAAAAAAAAAAAAAAAAAPZOFvOd4GOrqsdzJmELpy6rhuso-HYm2o5ArcWsZ-Eg/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=YP%2FZnkqS0m7oGLtYjG%2Bwh9UWpxs%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/1TEBm/dJMcaaGfpEA/AAAAAAAAAAAAAAAAAAAAAPZOFvOd4GOrqsdzJmELpy6rhuso-HYm2o5ArcWsZ-Eg/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=YP%2FZnkqS0m7oGLtYjG%2Bwh9UWpxs%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/1TEBm/dJMcaaGfpEA/AAAAAAAAAAAAAAAAAAAAAPZOFvOd4GOrqsdzJmELpy6rhuso-HYm2o5ArcWsZ-Eg/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=YP%2FZnkqS0m7oGLtYjG%2Bwh9UWpxs%3D&quot; alt=&quot;javac -d bin과 Java 패키지 구조 대표 이미지&quot; /&gt;
&lt;figcaption&gt;javac -d bin과 Java 패키지 구조 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평소에 IntelliJ로 자바를 짜다 보니 빌드 버튼만 눌렀지 안에서 무슨 일이 일어나는지는 몰랐다. 그런데 과제 제출 형식이 &lt;code&gt;.java&lt;/code&gt; 파일만 압축해서 올리는 거라서, IDE 없이 터미널에서 컴파일하고 실행까지 되는지 미리 확인해야 했다. 이 김에 &lt;code&gt;javac&lt;/code&gt;/&lt;code&gt;java&lt;/code&gt; 명령을 제대로 정리해두기로 했는데, 함정이 두 개 있었다. 하나는 &lt;code&gt;package&lt;/code&gt; 선언이 들어가면 디렉토리 구조가 그거랑 일치해야 한다는 거고, 다른 하나는 &lt;code&gt;-d&lt;/code&gt; 옵션으로 출력 디렉토리를 따로 지정하는 패턴이었다. 처음에는 &lt;code&gt;error: file not found&lt;/code&gt;만 계속 떠서 30분을 날렸다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;제일 단순한 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패키지 선언 없는 한 파일짜리부터 보면 이렇다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// Hello.java
public class Hello {
    public static void main(String[] args) {
        System.out.println(&quot;Hello, world!&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;javac Hello.java   # &amp;rarr; Hello.class 생성
java Hello         # &amp;rarr; Hello, world!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;javac&lt;/code&gt;가 &lt;code&gt;.java&lt;/code&gt;를 컴파일해서 같은 폴더에 &lt;code&gt;.class&lt;/code&gt;를 만들고, &lt;code&gt;java Hello&lt;/code&gt;로 실행한다. 클래스 이름은 확장자를 빼고 적어야 한다. &lt;code&gt;java Hello.class&lt;/code&gt;라고 쓰면 안 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;함정 하나 &amp;mdash; 패키지 선언과 디렉토리 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바는 &lt;code&gt;package&lt;/code&gt; 선언이 강제하는 디렉토리 구조가 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// src/com/myapp/Main.java
package com.myapp;

public class Main {
    public static void main(String[] args) {
        System.out.println(&quot;Hello from package!&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일이 &lt;code&gt;package com.myapp;&lt;/code&gt;라고 선언했다면, 파일이 있는 디렉토리가 정확히 &lt;code&gt;com/myapp/Main.java&lt;/code&gt;여야 한다. 아니면 컴파일은 통과해도 실행할 때 클래스를 못 찾는다는 에러가 뜬다. 내가 처음에 저지른 실수가 이거였다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;~/java/study$ javac -d bin src/com/assignment01/Main.java
error: file not found: src/com/assignment01/Main.java&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 에러는 그 경로에 파일이 진짜 없다는 뜻이었고, 원인은 &lt;code&gt;cd&lt;/code&gt;를 잘못된 위치에서 한 거였다. 컴파일은 항상 프로젝트 루트, 그러니까 &lt;code&gt;src/&lt;/code&gt;와 &lt;code&gt;bin/&lt;/code&gt;이 보이는 위치에서 해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;함정 둘 &amp;mdash; &lt;code&gt;-d&lt;/code&gt; 옵션이 실제로 하는 일&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 &lt;code&gt;javac&lt;/code&gt;는 &lt;code&gt;.class&lt;/code&gt; 파일을 &lt;code&gt;.java&lt;/code&gt; 파일과 같은 디렉토리에 만든다. 소스랑 빌드 결과물이 섞이는 게 싫으면 &lt;code&gt;-d&lt;/code&gt; 옵션으로 출력 폴더를 지정한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;javac -d bin src/com/myapp/Main.java&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 편한 이유는 &lt;code&gt;-d&lt;/code&gt;가 패키지 구조까지 자동으로 만들어준다는 거다. 위 명령을 실행하면 &lt;code&gt;bin/&lt;/code&gt; 안에 &lt;code&gt;com/myapp/Main.class&lt;/code&gt;가 자동으로 생긴다. &lt;code&gt;bin/com/myapp/&lt;/code&gt;을 미리 만들어둘 필요가 없다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;프로젝트 루트
├── src/
│   └── com/myapp/
│       └── Main.java
└── bin/                  &amp;larr; 컴파일 결과
    └── com/myapp/
        └── Main.class&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행할 때 또 걸리는 부분&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일이 끝나고 실행하려는데 여기서도 헷갈렸다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 안 됨
java bin/com/myapp/Main.class
java Main

# 이렇게
java -cp bin com.myapp.Main&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;-cp bin&lt;/code&gt;(또는 &lt;code&gt;-classpath bin&lt;/code&gt;)은 클래스 파일을 찾을 루트 디렉토리를 지정하는 거고, &lt;code&gt;com.myapp.Main&lt;/code&gt;은 패키지를 점으로 구분한 풀 클래스명이다. 슬래시가 아니라 점이고, &lt;code&gt;.class&lt;/code&gt; 확장자는 안 붙인다. &lt;code&gt;java&lt;/code&gt;가 클래스를 찾는 방식은 클래스패스의 루트(&lt;code&gt;bin&lt;/code&gt;)에서 시작해서 패키지 이름을 폴더 경로로 바꾸고 그 안에서 &lt;code&gt;Main.class&lt;/code&gt;를 찾는 식이라서, 내가 직접 그 디렉토리에 들어가서 실행할 필요가 없다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;여러 파일 프로젝트로 넘어가면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실습 과제 폴더 구조는 대략 이렇게 생겼다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;assignment/
├── src/
│   └── com/assignment01/
│       ├── Main.java
│       ├── Calculator.java
│       └── Helper.java
└── bin/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Main.java&lt;/code&gt;가 &lt;code&gt;Calculator&lt;/code&gt;와 &lt;code&gt;Helper&lt;/code&gt;를 쓴다고 하면, 같은 패키지 안에 있는 경우는 import 없이도 되고, 다른 패키지면 &lt;code&gt;import com.assignment01.Calculator;&lt;/code&gt; 식으로 명시한다. 세 개를 일일이 컴파일할 필요 없이 와일드카드를 쓰면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;javac -d bin src/com/assignment01/*.java&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서브패키지가 여러 개면 와일드카드만으로 부족한데, Linux/macOS라면 &lt;code&gt;find&lt;/code&gt;가 깔끔하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;find src -name &quot;*.java&quot; &amp;gt; sources.txt
javac -d bin @sources.txt&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@sources.txt&lt;/code&gt;는 argument file 패턴이다. &lt;code&gt;javac&lt;/code&gt;는 &lt;code&gt;@&lt;/code&gt;로 시작하는 인자를 만나면 그걸 파일로 보고 안의 내용을 명령줄 인자로 펼쳐서 처리하는데, 이 방식이면 파일 수가 수백 개여도 명령줄 길이 제한에 안 걸린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행은 똑같이 &lt;code&gt;java -cp bin com.assignment01.Main&lt;/code&gt;이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자잘하게 헷갈렸던 점들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;javac&lt;/code&gt;는 사실 똑똑해서, &lt;code&gt;Main.java&lt;/code&gt;만 컴파일 명령을 줘도 그 안에서 참조하는 &lt;code&gt;Calculator&lt;/code&gt;, &lt;code&gt;Helper&lt;/code&gt;를 알아서 같이 컴파일해준다. 단, 그 파일들 위치를 알 수 있어야 하는데 &lt;code&gt;-sourcepath&lt;/code&gt; 옵션으로 소스 루트를 지정해주면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;javac -d bin -sourcepath src src/com/assignment01/Main.java&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서드파티 JAR를 쓸 때는 &lt;code&gt;-cp&lt;/code&gt;에 같이 명시해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;javac -cp lib/mysql-connector.jar -d bin src/com/myapp/Main.java
java -cp bin:lib/mysql-connector.jar com.myapp.Main&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 JAR면 Linux/macOS는 &lt;code&gt;:&lt;/code&gt;, Windows는 &lt;code&gt;;&lt;/code&gt;로 구분한다. 이 차이 때문에 OS 간 스크립트를 짤 때 종종 사고가 난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학교에서 받은 과제 텍스트가 EUC-KR이거나 UTF-8 BOM이 섞여 있을 때가 있는데, IntelliJ에서는 파일 우하단 인코딩 표시를 클릭해서 바꿀 수 있고, 터미널에서는 &lt;code&gt;-encoding&lt;/code&gt; 옵션으로 명시하면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;javac -encoding UTF-8 -d bin src/com/myapp/*.java&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;package&lt;/code&gt; 선언이 없는 클래스는 default package에 속하는데, 다른 패키지의 클래스에서 default package 클래스를 import할 수 없다는 제약이 있어서 좀 큰 프로젝트라면 처음부터 패키지를 잡아두는 게 낫다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;-d bin&lt;/code&gt;은 &lt;code&gt;bin&lt;/code&gt; 디렉토리가 이미 존재해야 동작하고 자동으로 만들어주진 않는다. 이거 모르고 에러를 본 적이 있어서, 그냥 미리 만들어두는 습관을 들였다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir -p bin&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고한 자료는 Oracle 공식 javac 매뉴얼, Baeldung의 &quot;Compile All Java Classes in Directory Structure with javac&quot;와 &quot;Compiling Java *.class Files with javac&quot;, 그리고 Berkeley CS61BL의 Command Line for Packages 문서다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;소스 경로와 클래스 출력 경로를 구분하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패키지가 선언된 Java 파일을 컴파일할 때 &lt;code&gt;-d bin&lt;/code&gt;은 클래스 파일의 루트 디렉터리를 지정한다. javac가 패키지 이름에 맞춰 &lt;code&gt;bin/com/example/Main.class&lt;/code&gt;를 만든다. 실행할 때는 파일 경로가 아니라 클래스패스 루트와 완전한 클래스 이름을 준다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/WRkcn/dJMcaiddzM5/AAAAAAAAAAAAAAAAAAAAAKTLE-gO4caa7iVv_c_YMD_AgLmxn8sOgLgi1y5zqrdi/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=pXBp%2B3UqCbG8sVD7bCcvn3KLvXo%3D&quot; alt=&quot;Oracle javac 공식 문서&quot; /&gt;
&lt;figcaption&gt;Oracle javac 공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/YSlT5/dJMcaiYpB8b/AAAAAAAAAAAAAAAAAAAAAFEkdPVakt_zDgOvT4dK-oU2AxoP-NSh3xzkhe-uR9p5/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=rjXtSwI6FRMpzUsdMKjRCN5zZAE%3D&quot; alt=&quot;dev.java 패키지 학습 문서&quot; /&gt;
&lt;figcaption&gt;dev.java 패키지 학습 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/c4XWea/dJMcaaGfpEC/AAAAAAAAAAAAAAAAAAAAAKVG4W401-t6dV1NHnF8bODn6LIdbSGAF-N-JkGEMNs2/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=4R2ZpczIDKQNpZxNNgVTVL8a10c%3D&quot; alt=&quot;Oracle class path 문서&quot; /&gt;
&lt;figcaption&gt;Oracle class path 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/cmY659/dJMcaaMXyZY/AAAAAAAAAAAAAAAAAAAAAB1PlwGYXaScadmmDNwOGihOW8O1LRlwLBPXVwrHJgZ6/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=tcOwu7zWrsDxxzscIEXy2KYMaP4%3D&quot; alt=&quot;Oracle jar 명령 공식 문서&quot; /&gt;
&lt;figcaption&gt;Oracle jar 명령 공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;pre&gt;&lt;code&gt;javac -d bin src/com/example/Main.java
java -cp bin com.example.Main
jar --create --file app.jar -C bin .&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ClassNotFoundException&lt;/code&gt;이 나면 클래스 파일이 없는지만 보지 말고 &lt;code&gt;-cp&lt;/code&gt;가 패키지 디렉터리의 부모를 가리키는지 확인한다. 이 기준을 잡고 나니 IDE 밖에서 컴파일해도 디렉터리 구조가 덜 헷갈렸다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/25/docs/specs/man/javac.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;javac&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.java/learn/packages/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Java Packages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/25/docs/specs/man/jar.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;jar&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발</category>
      <category>commandline</category>
      <category>Java</category>
      <category>javac</category>
      <category>linux</category>
      <category>package</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/20</guid>
      <comments>https://gorani-kr.tistory.com/20#entry20comment</comments>
      <pubDate>Fri, 24 Jul 2026 07:44:00 +0900</pubDate>
    </item>
    <item>
      <title>사각형 충돌 판정에서 원 충돌까지 (AABB의 원리와 일반화)</title>
      <link>https://gorani-kr.tistory.com/19</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/btOeqN/dJMcaiddzKh/AAAAAAAAAAAAAAAAAAAAAFgvFUfWVb0zo-nglnn4a3Ns1TxxjWpNdbOG1n67Tm6a/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=7hqgph7WbPBZ%2F%2BAS1%2F6BkudH2BY%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/btOeqN/dJMcaiddzKh/AAAAAAAAAAAAAAAAAAAAAFgvFUfWVb0zo-nglnn4a3Ns1TxxjWpNdbOG1n67Tm6a/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=7hqgph7WbPBZ%2F%2BAS1%2F6BkudH2BY%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/btOeqN/dJMcaiddzKh/AAAAAAAAAAAAAAAAAAAAAFgvFUfWVb0zo-nglnn4a3Ns1TxxjWpNdbOG1n67Tm6a/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=7hqgph7WbPBZ%2F%2BAS1%2F6BkudH2BY%3D&quot; alt=&quot;AABB와 원 충돌 판정 대표 이미지&quot; /&gt;
&lt;figcaption&gt;AABB와 원 충돌 판정 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 실습 문제 중에 이런 게 있었다. 2차원 평면에서 두 점 &lt;code&gt;(x1, y1)&lt;/code&gt;, &lt;code&gt;(x2, y2)&lt;/code&gt;로 만들어지는 직사각형이 &lt;code&gt;(100, 100)&lt;/code&gt;, &lt;code&gt;(200, 200)&lt;/code&gt;으로 이루어진 직사각형과 충돌하는지 판별하라는 문제. 답은 맞췄는데, 풀고 나서 같은 로직을 원 대 원으로 일반화해보고 싶어졌다. 두 케이스의 공통 원리를 알면 원-사각형처럼 다른 조합도 다 풀린다는 걸 이번에 이해했다. 게임 개발에서 진짜 자주 쓰는 개념인데 처음 직관이 안 잡히면 의외로 헤매기 쉬운 부분이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AABB 사각형 충돌&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AABB, Axis-Aligned Bounding Box는 축에 평행한 사각형을 말한다. 회전 없이 가로세로 축에 딱 맞춰진 사각형이라는 뜻이고, 게임에서 제일 흔한 충돌 모양으로 쓰이는 이유는 단순히 계산이 압도적으로 빠르기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 사각형 A와 B가 충돌하는 조건을 바로 생각하려 하면 헷갈리는데, 반대로 &quot;안 겹치는 경우&quot;부터 생각하면 정리가 쉽다. 두 사각형이 안 겹치려면 A가 완전히 B의 왼쪽에 있거나, 완전히 오른쪽에 있거나, 완전히 위에 있거나, 완전히 아래에 있어야 한다. 이 네 가지 중 하나라도 참이면 안 겹치고, 반대로 넷 다 거짓이어야 겹친다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수식으로 옮기면 사각형 A가 &lt;code&gt;(Ax1, Ay1)&lt;/code&gt;-&lt;code&gt;(Ax2, Ay2)&lt;/code&gt;이고 B가 &lt;code&gt;(Bx1, By1)&lt;/code&gt;-&lt;code&gt;(Bx2, By2)&lt;/code&gt;일 때:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;충돌 = (Ax1 &amp;lt; Bx2) &amp;amp;&amp;amp; (Ax2 &amp;gt; Bx1) &amp;amp;&amp;amp; (Ay1 &amp;lt; By2) &amp;amp;&amp;amp; (Ay2 &amp;gt; By1)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네 조건이 모두 참이면 겹친 거다. Java로 옮기면:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static boolean rectIntersect(
    double ax1, double ay1, double ax2, double ay2,
    double bx1, double by1, double bx2, double by2)
{
    return ax1 &amp;lt; bx2 &amp;amp;&amp;amp; ax2 &amp;gt; bx1
        &amp;amp;&amp;amp; ay1 &amp;lt; by2 &amp;amp;&amp;amp; ay2 &amp;gt; by1;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제에서 B가 &lt;code&gt;(100,100)-(200,200)&lt;/code&gt;으로 고정이면 이렇게 단순화된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static boolean collidesWithFixed(double x1, double y1, double x2, double y2) {
    return x1 &amp;lt; 200 &amp;amp;&amp;amp; x2 &amp;gt; 100
        &amp;amp;&amp;amp; y1 &amp;lt; 200 &amp;amp;&amp;amp; y2 &amp;gt; 100;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 하나 빠뜨려서 막혔던 부분이 있다. 사용자가 입력하는 두 점이 항상 좌상단-우하단 순서라는 보장이 없다는 거다. &lt;code&gt;1 2 300 200&lt;/code&gt;을 입력하면 &lt;code&gt;(1,2)&lt;/code&gt;-&lt;code&gt;(300,200)&lt;/code&gt;으로 순서가 맞지만, &lt;code&gt;300 200 1 2&lt;/code&gt;처럼 뒤집어서 입력하면 &lt;code&gt;x1 = 300&lt;/code&gt;, &lt;code&gt;x2 = 1&lt;/code&gt;이 되어 &lt;code&gt;x1 &amp;lt; x2&lt;/code&gt; 자체가 거짓이 되면서 위 조건식이 무너진다. 해결은 입력받자마자 작은 값과 큰 값을 정렬해두는 거다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;double xMin = Math.min(x1, x2);
double xMax = Math.max(x1, x2);
double yMin = Math.min(y1, y2);
double yMax = Math.max(y1, y2);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 정리한 &lt;code&gt;xMin&lt;/code&gt;/&lt;code&gt;xMax&lt;/code&gt;/&lt;code&gt;yMin&lt;/code&gt;/&lt;code&gt;yMax&lt;/code&gt;를 충돌 판정식에 넣으면 입력 순서와 무관하게 잘 동작한다. 이거 안 해놓고 &quot;1 2 300 200 입력했는데 왜 충돌 안 되냐&quot;고 한참 헤맸다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원 대 원은 훨씬 간단하다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 원이 겹치는지는 사각형보다 코드가 짧다. 두 원 중심 사이 거리가 반지름의 합보다 작거나 같으면 겹친다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;distance = &amp;radic;((x2 - x1)&amp;sup2; + (y2 - y1)&amp;sup2;)
중심 거리 &amp;le; r1 + r2  &amp;rarr;  충돌&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 작은 최적화가 하나 들어가는데, 제곱근 연산이 비싸기 때문에 둘 다 제곱한 채로 비교한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(x2 - x1)&amp;sup2; + (y2 - y1)&amp;sup2;  vs  (r1 + r2)&amp;sup2;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좌변이 우변보다 작으면 충돌이고, 이러면 제곱근을 한 번도 호출하지 않아도 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static boolean circleIntersect(
    double x1, double y1, double r1,
    double x2, double y2, double r2)
{
    double dx = x2 - x1;
    double dy = y2 - y1;
    double distSq = dx * dx + dy * dy;
    double rSum = r1 + r2;
    return distSq &amp;lt;= rSum * rSum;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음수 좌표가 들어와도 문제없는데, &lt;code&gt;(x2 - x1)&lt;/code&gt;이 음수여도 제곱하면 양수가 되기 때문이다. 거리 자체가 원래 부호에 영향을 안 받는 양수 값이라 그렇다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;두 케이스를 관통하는 원리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사각형 충돌과 원 충돌을 따로 보면 완전히 다른 알고리즘처럼 보이는데 본질은 같다. 두 도형이 &quot;분리 가능한가&quot;를 보는 거다. 분리할 수 있으면 충돌이 아니고, 못 하면 충돌이다. 사각형끼리는 X축이나 Y축으로 분리할 수 있는지 보고, 원끼리는 두 중심을 잇는 직선 위에서 분리할 수 있는지, 즉 중심 거리가 반지름 합보다 큰지를 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 일반화된 원리가 분리축 정리(Separating Axis Theorem, SAT)다. 회전한 사각형이나 임의의 다각형 충돌도 같은 원리로 풀 수 있는데, 적당한 축을 골라서 두 도형을 그 위에 투영하고 투영된 구간이 겹치는지 확인하면 된다. 게임 엔진의 충돌 시스템 대부분이 이 위에 만들어져 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자잘하게 걸렸던 것들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경계가 정확히 맞닿은 상태를 충돌로 볼지 말지는 문제마다 다르다. 수업 문제는 지문을 잘 읽고 &lt;code&gt;&amp;lt;&lt;/code&gt;와 &lt;code&gt;&amp;lt;=&lt;/code&gt;를 구분해야 했는데, 보통 게임에서는 정확히 닿은 건 충돌 안 한 걸로 처리하는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좌표를 정수로 받으면 거리 계산에서 오버플로 위험이 있다. 값이 충분히 크면 &lt;code&gt;int * int&lt;/code&gt;가 int 범위를 넘을 수 있어서, &lt;code&gt;long&lt;/code&gt;으로 캐스팅하거나 처음부터 &lt;code&gt;double&lt;/code&gt;을 쓰는 게 안전하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;long dx = (long)x2 - x1;  // 안전&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사각형 표현 방식도 두 가지가 있다. &lt;code&gt;(x1, y1)-(x2, y2)&lt;/code&gt;처럼 두 코너 점으로 표현하는 방식과, &lt;code&gt;(x, y, w, h)&lt;/code&gt;처럼 좌상단 점과 가로세로 크기로 표현하는 방식. 서로 변환은 가능한데 충돌 코드 모양이 조금 다르다. &lt;code&gt;java.awt.Rectangle&lt;/code&gt;이나 MonoGame, Pygame 같은 라이브러리는 대체로 후자를 쓴다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;return ax &amp;lt; bx + bw &amp;amp;&amp;amp; ax + aw &amp;gt; bx
    &amp;amp;&amp;amp; ay &amp;lt; by + bh &amp;amp;&amp;amp; ay + ah &amp;gt; by;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;논리는 똑같고 좌표 계산만 살짝 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원과 사각형의 충돌은 이 둘의 중간쯤 되는 난이도다. 핵심 트릭은 사각형 위에서 원 중심에 가장 가까운 점을 찾고, 그 점과 원 중심 사이 거리가 반지름보다 작은지 보는 거다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;double closestX = Math.max(rectX, Math.min(circleX, rectX + rectW));
double closestY = Math.max(rectY, Math.min(circleY, rectY + rectH));
double dx = circleX - closestX;
double dy = circleY - closestY;
return (dx * dx + dy * dy) &amp;lt; radius * radius;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Math.min&lt;/code&gt;/&lt;code&gt;Math.max&lt;/code&gt; 조합이 클램프 연산이다. 원 중심을 사각형 경계 안쪽으로 당겨놓고, 그 결과 점이 원 안에 있는지 보는 방식인데 처음 보면 잘 안 와닿다가 그림으로 그려보면 바로 이해된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고한 자료는 LearnOpenGL의 Collision detection 문서(AABB와 원-사각형 클램프 트릭), MDN Web Docs의 2D collision detection, MonoGame 문서 12장(제곱 거리 비교 트릭), 위키피디아의 Separating Axis Theorem 항목이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;충돌 판정은 모양보다 거리의 조건이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AABB는 각 축의 구간이 모두 겹치는지 보고, 원은 중심 거리의 제곱이 반지름 합의 제곱보다 작은지 본다. 사각형과 원은 원의 중심을 사각형 내부의 가장 가까운 점으로 clamp한 뒤 그 점과 중심 거리를 계산하면 된다. 세 식을 같은 &amp;ldquo;분리되는 조건이 있는가&amp;rdquo; 관점으로 보면 일반화가 쉬웠다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/Vfj41/dJMcaiddzKD/AAAAAAAAAAAAAAAAAAAAAL1aHbpczuNjos4UHQ6Q1yj7lKxT7u97UW1nD-0v1xJQ/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=meuoDnuNWT4zCk7D7SEld44spkc%3D&quot; alt=&quot;MDN 2D collision detection 문서&quot; /&gt;
&lt;figcaption&gt;MDN 2D collision detection 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/nEOBV/dJMcaiddzK9/AAAAAAAAAAAAAAAAAAAAAKfMlflrUp9PLuX9W9XdWOD0qdH8NzBLRrzA6j0mQz6_/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=Lm6tmMF2M6mSBx4NA5L8wDwJmto%3D&quot; alt=&quot;MDN 3D collision detection의 bounding volume 설명&quot; /&gt;
&lt;figcaption&gt;MDN 3D collision detection의 bounding volume 설명&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/SlnNU/dJMcaiddzLA/AAAAAAAAAAAAAAAAAAAAAPGf7kQnHH-CgK7iHr4lvhZ8hYDVOy-HS7auZNKCbjVl/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=uDCtn4H1ExM569jRMK%2FvztuIm%2Bo%3D&quot; alt=&quot;Wolfram MathWorld Circle 문서&quot; /&gt;
&lt;figcaption&gt;Wolfram MathWorld Circle 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/BNRr0/dJMcaiddzL8/AAAAAAAAAAAAAAAAAAAAAP6rol1_PTthle2DVt_J2C2eGYT7jIR3uMHSQgdgOF-U/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=bXaZtU8PIJc1cKt0w9Bq2ArNPFs%3D&quot; alt=&quot;LearnOpenGL 2D collision detection 예제&quot; /&gt;
&lt;figcaption&gt;LearnOpenGL 2D collision detection 예제&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;pre&gt;&lt;code&gt;closestX = clamp(circleX, rectLeft, rectRight)
closestY = clamp(circleY, rectTop, rectBottom)
dx = circleX - closestX
dy = circleY - closestY
collides = dx*dx + dy*dy &amp;lt;= radius*radius&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제곱근을 생략하고 제곱 거리끼리 비교하면 결과는 같고 계산은 단순해진다. 접촉을 충돌로 볼지 여부에 따라 &lt;code&gt;&amp;lt;&lt;/code&gt;와 &lt;code&gt;&amp;lt;=&lt;/code&gt;를 의도적으로 고른다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Games/Techniques/2D_collision_detection&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MDN 2D collision detection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mathworld.wolfram.com/Circle.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MathWorld Circle&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learnopengl.com/In-Practice/2D-Game/Collisions/Collision-detection&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;LearnOpenGL Collision detection&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발</category>
      <category>AABB</category>
      <category>algorithm</category>
      <category>collision</category>
      <category>Java</category>
      <category>Math</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/19</guid>
      <comments>https://gorani-kr.tistory.com/19#entry19comment</comments>
      <pubDate>Fri, 24 Jul 2026 07:43:51 +0900</pubDate>
    </item>
    <item>
      <title>Java `public static void main(String[] args)` 진짜 의미 + Scanner 입력받기</title>
      <link>https://gorani-kr.tistory.com/18</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/rqaY3/dJMcahrO8rT/AAAAAAAAAAAAAAAAAAAAAC82gTYnK-kwKTNB14FwSjqiVFlpqfdc7Cwq4UcExccw/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=AnEt%2FaoHc8x01HihsijznJms%2FrM%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/rqaY3/dJMcahrO8rT/AAAAAAAAAAAAAAAAAAAAAC82gTYnK-kwKTNB14FwSjqiVFlpqfdc7Cwq4UcExccw/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=AnEt%2FaoHc8x01HihsijznJms%2FrM%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/rqaY3/dJMcahrO8rT/AAAAAAAAAAAAAAAAAAAAAC82gTYnK-kwKTNB14FwSjqiVFlpqfdc7Cwq4UcExccw/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=AnEt%2FaoHc8x01HihsijznJms%2FrM%3D&quot; alt=&quot;Java main 시그니처와 Scanner 입력 대표 이미지&quot; /&gt;
&lt;figcaption&gt;Java main 시그니처와 Scanner 입력 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 실습 첫날, IntelliJ에 코드를 짜다가 두 번째 줄에 빨간 줄이 그어졌다. 디버깅도 안 됐다. 살펴보니 &lt;code&gt;static void main(String args[])&lt;/code&gt;라고 적어놓고 &lt;code&gt;public&lt;/code&gt;을 빠뜨린 게 원인이었다. 그동안 그냥 외워서 쓰던 &lt;code&gt;public static void main(String[] args)&lt;/code&gt;인데, 다섯 단어나 되는 이 한 줄에서 각 단어가 왜 거기 있어야 하는지 한 번도 제대로 생각해본 적이 없다는 걸 그때 깨달았다. C를 다루다가 Java로 넘어오니 더 헷갈렸다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한 단어씩 뜯어보면&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void main(String[] args) {
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;public&lt;/code&gt;부터 보자. JVM이 이 메서드를 클래스 외부에서 호출해야 하는데, JVM은 내가 짠 클래스 내부의 일부가 아니라 완전히 바깥에 있는 시스템이다. 그래서 외부에서 접근 가능하도록 &lt;code&gt;public&lt;/code&gt;이어야 한다. &lt;code&gt;private static void main(...)&lt;/code&gt;으로 짜면 컴파일은 되는데 실행할 때 JVM이 main을 못 찾아서 &quot;Main method not found&quot; 에러가 뜬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 &lt;code&gt;static&lt;/code&gt;이 사실 제일 중요한 키워드다. 일반 인스턴스 메서드는 객체를 먼저 만들어야 호출 가능한데, 프로그램이 막 시작하는 순간에는 아직 어떤 객체도 존재하지 않는다. JVM이 &lt;code&gt;Main&lt;/code&gt; 클래스를 메모리에 로드했을 뿐 &lt;code&gt;new Main()&lt;/code&gt;을 한 적은 없다. &lt;code&gt;static&lt;/code&gt; 메서드는 객체 없이 클래스 이름으로 바로 호출할 수 있어서, JVM이 &lt;code&gt;Main.main(args)&lt;/code&gt;처럼 클래스를 통해 직접 부를 수 있게 된다. &lt;code&gt;static&lt;/code&gt;이 빠지면 &lt;code&gt;non-static method main cannot be referenced from a static context&lt;/code&gt; 에러가 난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;void&lt;/code&gt;는 반환값이 없다는 뜻인데, JVM이 정한 시그니처가 그렇다. C랑 비교하면 재밌는 차이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// C
int main(int argc, char *argv[]) {
    return 0;  // 종료 코드
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// Java
public static void main(String[] args) {
    // 반환 안 함
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C는 종료 상태 코드를 &lt;code&gt;return 0;&lt;/code&gt;으로 넘기는데 Java는 &lt;code&gt;void&lt;/code&gt;라 그게 안 된다. 그럼 종료 코드는 어떻게 주냐면 &lt;code&gt;System.exit(1);&lt;/code&gt; 같은 걸 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;main&lt;/code&gt;은 키워드가 아니라 그냥 이름이다. JVM이 시작점으로 찾는 이름이 정확히 &lt;code&gt;main&lt;/code&gt;이라 이렇게 쓰는 거지 언어 차원에서 예약된 단어는 아니다. &lt;code&gt;mainn&lt;/code&gt;이나 &lt;code&gt;Main&lt;/code&gt;으로 바꾸면 컴파일은 되지만 JVM이 실행할 때 못 찾는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 &lt;code&gt;String[] args&lt;/code&gt;는 JVM이 사용자가 명령줄에 적은 인자들을 문자열 배열로 넘겨주는 자리다. &lt;code&gt;args&lt;/code&gt;라는 이름은 관습일 뿐 강제는 아니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java Main hello world 123&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 실행하면 &lt;code&gt;args[0]&lt;/code&gt;은 &lt;code&gt;&quot;hello&quot;&lt;/code&gt;, &lt;code&gt;args[1]&lt;/code&gt;은 &lt;code&gt;&quot;world&quot;&lt;/code&gt;, &lt;code&gt;args[2]&lt;/code&gt;는 &lt;code&gt;&quot;123&quot;&lt;/code&gt;, &lt;code&gt;args.length&lt;/code&gt;는 3이 된다. C와 비교하면 &lt;code&gt;argc&lt;/code&gt;가 따로 없다는 게 차이인데, Java 배열은 자기 길이를 알고 있어서 &lt;code&gt;args.length&lt;/code&gt;로 바로 접근하면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Scanner로 입력받기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main 안에서 키보드 입력을 받을 때는 보통 &lt;code&gt;Scanner&lt;/code&gt;를 쓴다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.print(&quot;이름을 입력하세요: &quot;);
        String name = scanner.next();

        System.out.print(&quot;나이를 입력하세요: &quot;);
        int age = scanner.nextInt();

        System.out.println(name + &quot;님은 &quot; + age + &quot;살입니다.&quot;);

        scanner.close();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Scanner&lt;/code&gt;는 &lt;code&gt;java.util&lt;/code&gt; 패키지에 있어서 맨 위에 import를 박아야 한다. IntelliJ에서는 &lt;code&gt;Scanner&lt;/code&gt;라고 치고 Alt+Enter를 누르면 자동으로 추가해준다. &lt;code&gt;next()&lt;/code&gt;는 공백 전까지 한 단어, &lt;code&gt;nextLine()&lt;/code&gt;은 줄 끝까지, &lt;code&gt;nextInt()&lt;/code&gt;/&lt;code&gt;nextDouble()&lt;/code&gt;/&lt;code&gt;nextBoolean()&lt;/code&gt;은 각각 int&amp;middot;double&amp;middot;boolean을 읽는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 처음에 진짜 헷갈렸던 게 &lt;code&gt;next()&lt;/code&gt;/&lt;code&gt;nextInt()&lt;/code&gt; 다음에 &lt;code&gt;nextLine()&lt;/code&gt;을 부르는 조합이다. &lt;code&gt;nextInt()&lt;/code&gt;는 공백이나 개행 직전까지만 읽고 멈추기 때문에, 입력 버퍼에 개행 문자가 그대로 남는다. 그 상태에서 &lt;code&gt;nextLine()&lt;/code&gt;을 부르면 남아있던 개행을 즉시 읽어서 빈 문자열을 반환하고 끝나버린다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int age = scanner.nextInt();        // 25 입력 &amp;rarr; age=25, 버퍼에 \n 남음
String name = scanner.nextLine();   // 즉시 빈 문자열 반환! 입력 못 받음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결은 &lt;code&gt;nextInt()&lt;/code&gt; 다음에 더미로 &lt;code&gt;nextLine()&lt;/code&gt;을 한 번 더 불러서 남은 개행을 처리해주는 거다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int age = scanner.nextInt();
scanner.nextLine();  // 남은 개행 처리
String name = scanner.nextLine();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그밖에 헷갈렸던 것들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;String[] args&lt;/code&gt;와 &lt;code&gt;String args[]&lt;/code&gt;는 둘 다 문법적으로 맞다. 배열 표기 &lt;code&gt;[]&lt;/code&gt;가 타입 뒤에 붙느냐 변수명 뒤에 붙느냐의 차이일 뿐이고, C/C++에서는 변수명 뒤가 자연스럽지만 Java 컨벤션은 타입 뒤(&lt;code&gt;String[] args&lt;/code&gt;)를 권장한다. 가변 인자 문법인 &lt;code&gt;String... args&lt;/code&gt;도 되는데 main에서는 거의 안 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;public&lt;/code&gt;과 &lt;code&gt;static&lt;/code&gt;의 순서를 바꿔서 &lt;code&gt;static public void main(...)&lt;/code&gt;으로 써도 컴파일은 된다. 다만 다들 &lt;code&gt;public static void&lt;/code&gt;로 쓰니까 그냥 관습을 따르는 게 가독성에 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IntelliJ에서 빨간 줄이 뜨는 흔한 원인을 정리하면, &lt;code&gt;public&lt;/code&gt; 빠뜨림, &lt;code&gt;static&lt;/code&gt; 빠뜨림, &lt;code&gt;String[] args&lt;/code&gt; 빠뜨림, 클래스 이름과 파일명 불일치(&lt;code&gt;public class Main&lt;/code&gt;인데 파일명이 &lt;code&gt;Main.java&lt;/code&gt;가 아닌 경우), 세미콜론 빠뜨림 정도다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main도 메서드라서 오버로딩이 가능한데, JVM이 시작점으로 인식하는 건 정확히 &lt;code&gt;public static void main(String[])&lt;/code&gt; 하나뿐이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Main {
    public static void main(String[] args) {
        main(42);          // 우리가 직접 호출
        main(&quot;hello&quot;);     // 우리가 직접 호출
    }
    public static void main(int n) { System.out.println(&quot;int: &quot; + n); }
    public static void main(String s) { System.out.println(&quot;String: &quot; + s); }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM은 &lt;code&gt;String[]&lt;/code&gt;를 받는 main을 호출하고, 그 안에서 우리가 다른 오버로드를 직접 부를 수 있다. 실제로는 거의 안 쓰는 패턴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고한 자료는 DigitalOcean의 &quot;Understanding public static void main(String[] args) in Java&quot;, GeeksforGeeks의 Java main() Method 설명, Oracle Java Language Specification의 메서드 선언 부분, 그리고 Coderanch 포럼에서 main 시그니처가 왜 그렇게 생겼는지 다룬 글이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;언어 문법과 실행기 규칙을 나눠 보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;public static void main(String[] args)&lt;/code&gt;는 단순 주문이 아니라 Java 실행기가 시작 메서드를 찾는 규칙과 연결된다. &lt;code&gt;static&lt;/code&gt;이라 객체 생성 없이 호출할 수 있고, &lt;code&gt;String[] args&lt;/code&gt;에는 명령행 인자가 들어온다. Scanner는 표준 입력 스트림을 토큰 단위로 읽는 별도 도구다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/MZC57/dJMcahrO8rV/AAAAAAAAAAAAAAAAAAAAABP1j19SbrLwd57v0sGPQO5CZpMpHl9_crxkFFry_DlL/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=eF0CfLlPFkQA3Kii%2FroPSUgow8w%3D&quot; alt=&quot;Oracle java launcher 공식 문서&quot; /&gt;
&lt;figcaption&gt;Oracle java launcher 공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/taoWK/dJMcaiYpB5u/AAAAAAAAAAAAAAAAAAAAAF2PyEvuxq5iCbqMHy6sO_I0fEaEa8pKkR7ZDhXSw7IE/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=pmrtszPAHSlaHhAyEcLe2EICADE%3D&quot; alt=&quot;Java Scanner API 문서&quot; /&gt;
&lt;figcaption&gt;Java Scanner API 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/L5mRq/dJMcab59HWw/AAAAAAAAAAAAAAAAAAAAANKEuWkTMQ1K0G3jMxqWVYdGLNwCvFGgwLiWnB4Zobow/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=6sVUx2HnsktLYU9IWEeNXCOgU5k%3D&quot; alt=&quot;Java Virtual Machine Specification&quot; /&gt;
&lt;figcaption&gt;Java Virtual Machine Specification&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/cD5QVH/dJMcahrO8rX/AAAAAAAAAAAAAAAAAAAAAEy1g5dg4rP9vt007RhYNI1TOe08GA7XT_kjrP5JF4W1/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=3hRCgMxB%2B0Ein7Z6Zn28qCsLg7Q%3D&quot; alt=&quot;dev.java 공식 학습 페이지&quot; /&gt;
&lt;figcaption&gt;dev.java 공식 학습 페이지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;nextInt()&lt;/code&gt; 뒤에 &lt;code&gt;nextLine()&lt;/code&gt;을 바로 호출하면 남은 줄바꿈을 읽는 문제도 토큰 입력과 줄 입력의 경계를 알면 설명된다. 입력 방식을 섞을 때는 한 줄을 읽고 직접 변환하거나, 남은 개행을 소비하는 코드를 의도적으로 둔다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/25/docs/specs/man/java.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;java launcher&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Scanner.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Scanner API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.java/learn/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;dev.java Learn&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발</category>
      <category>Beginner</category>
      <category>Intellij</category>
      <category>Java</category>
      <category>Scanner</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/18</guid>
      <comments>https://gorani-kr.tistory.com/18#entry18comment</comments>
      <pubDate>Fri, 24 Jul 2026 07:43:43 +0900</pubDate>
    </item>
    <item>
      <title>C 컴파일의 4단계와 링커 &amp;mdash; gcc -c는 뭐고 -o는 뭐가 다른가</title>
      <link>https://gorani-kr.tistory.com/17</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/dge11a/dJMcab59HWo/AAAAAAAAAAAAAAAAAAAAADnUxlCIBQrmUOi5JIcnGvg8MG4pyMo9pOaPAHfbwN-K/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=Fy2DHIUbXv13T%2BVW%2BudxXSJ3jao%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/dge11a/dJMcab59HWo/AAAAAAAAAAAAAAAAAAAAADnUxlCIBQrmUOi5JIcnGvg8MG4pyMo9pOaPAHfbwN-K/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=Fy2DHIUbXv13T%2BVW%2BudxXSJ3jao%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/dge11a/dJMcab59HWo/AAAAAAAAAAAAAAAAAAAAADnUxlCIBQrmUOi5JIcnGvg8MG4pyMo9pOaPAHfbwN-K/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=Fy2DHIUbXv13T%2BVW%2BudxXSJ3jao%3D&quot; alt=&quot;C 컴파일 4단계와 링커 대표 이미지&quot; /&gt;
&lt;figcaption&gt;C 컴파일 4단계와 링커 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템 프로그래밍 수업에서 컴파일러, 링커, POSIX 같은 단어가 한꺼번에 쏟아진 날이 있었다. 그동안은 그냥 &lt;code&gt;gcc main.c&lt;/code&gt;만 치고 살았는데, 수업 자료에 &lt;code&gt;gcc -c&lt;/code&gt;, &lt;code&gt;gcc -o&lt;/code&gt;, &lt;code&gt;gcc -S&lt;/code&gt;, &lt;code&gt;gcc -E&lt;/code&gt;가 따로 나오는 걸 보고 처음으로 이게 뭔 차이인지 궁금해졌다. 나중에 알고 보니 이유는 단순했다. gcc는 사실 컴파일러 그 자체가 아니라 컴파일러 드라이버였고, 내부적으로 네 단계짜리 도구 체인을 순서대로 불러주는 놈이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 시간에 &quot;요즘 컴파일러는 LLVM 기반&quot;이라는 얘기도 들었는데, gcc랑 clang 차이까지 이번 기회에 같이 정리해봤다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;hello.c&lt;/code&gt;가 실행 파일이 되기까지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 줄짜리 &lt;code&gt;hello.c&lt;/code&gt;를 실행 파일로 만드는 데도 사실 네 단계를 거친다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hello.c
   │  ① Preprocessing  (cpp)
   ▼
hello.i
   │  ② Compilation    (cc1)
   ▼
hello.s        &amp;larr; 어셈블리 코드
   │  ③ Assembly       (as)
   ▼
hello.o        &amp;larr; 오브젝트 파일 (relocatable machine code)
   │  ④ Linking        (ld)
   ▼
hello          &amp;larr; 실행 파일&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;gcc hello.c -o hello&lt;/code&gt; 한 줄로 이 네 단계가 순서대로 다 돌아간다. 중간에 멈추고 싶으면 그때 옵션을 따로 준다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;옵션&lt;/th&gt;
&lt;th&gt;어디서 멈추나&lt;/th&gt;
&lt;th&gt;출력&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-E&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;전처리 후&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.i&lt;/code&gt; (전처리된 C 코드)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-S&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;컴파일 후&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.s&lt;/code&gt; (어셈블리)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;어셈블 후&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.o&lt;/code&gt; (오브젝트 파일)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(없음)&lt;/td&gt;
&lt;td&gt;링크까지&lt;/td&gt;
&lt;td&gt;실행 파일&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전처리는 &lt;code&gt;#include&lt;/code&gt;, &lt;code&gt;#define&lt;/code&gt;, &lt;code&gt;#if&lt;/code&gt; 같은 걸 처리하는 단계다. &lt;code&gt;#include &amp;lt;stdio.h&amp;gt;&lt;/code&gt;를 만나면 진짜로 그 헤더 파일 전체를 그 자리에 끼워넣는다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcc -E hello.c -o hello.i&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이거 한 번 돌려보면 결과 파일이 수천 줄로 불어난다. stdio.h 안의 선언이 죄다 박혀서다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 컴파일 단계에서 전처리된 코드를 어셈블리로 번역한다. 우리가 흔히 &quot;컴파일러가 하는 일&quot;이라고 생각하는 게 사실 이 단계다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcc -S hello.c -o hello.s&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;hello.s&lt;/code&gt;를 열어보면 &lt;code&gt;pushq %rbp&lt;/code&gt;, &lt;code&gt;movq %rsp, %rbp&lt;/code&gt; 같은 진짜 어셈블리가 보인다. x86-64 리눅스 환경이면 인텔 문법이 아니라 AT&amp;amp;T 문법이 기본으로 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째는 어셈블. 어셈블리를 진짜 기계어로 바꾼다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcc -c hello.c -o hello.o&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 와도 아직 실행은 안 된다. 오브젝트 파일은 &quot;재배치 가능한 기계어&quot;라서 함수 주소가 아직 확정 안 됐다. &lt;code&gt;printf&lt;/code&gt; 같은 외부 함수 주소도 비어있는 상태다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 링킹이 그 빈칸을 채운다. 오브젝트 파일들을 모아서 실행 파일로 만드는 단계인데, 이게 아마 처음 들으면 제일 헷갈리는 부분일 거다. 링커가 하는 일은 크게 두 가지다. 하나는 심볼 해소로, 한 파일에서 부른 함수가 다른 파일이나 라이브러리에 있으면 그걸 찾아 연결하는 거다. &lt;code&gt;printf&lt;/code&gt; 호출이 libc 안의 진짜 &lt;code&gt;printf&lt;/code&gt;로 이어지는 게 이 과정이다. 다른 하나는 재배치인데, 오브젝트 파일 안에서 가짜 주소로 남아있던 함수 위치에 실제 주소를 박아넣는 작업이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcc hello.o -o hello&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 파일을 나눠서 컴파일하나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;main.c&lt;/code&gt;, &lt;code&gt;util.c&lt;/code&gt;, &lt;code&gt;math.c&lt;/code&gt; 세 파일짜리 프로젝트는 한 줄로도 컴파일된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcc main.c util.c math.c -o app&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 실제 프로젝트에서는 이렇게 잘 안 한다. 파일 하나만 고쳤는데 매번 전부 다시 컴파일하는 게 시간 낭비이기 때문이다. 그래서 각 파일을 오브젝트로 따로 만들고 마지막에 링크만 다시 돌리는 패턴을 쓴다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcc -c main.c -o main.o
gcc -c util.c -o util.o
gcc -c math.c -o math.o
gcc main.o util.o math.o -o app&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;util.c&lt;/code&gt;만 고쳤으면 &lt;code&gt;util.o&lt;/code&gt;만 새로 만들고 링크 단계만 다시 하면 된다. Makefile이 자동화하는 게 딱 이 부분이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-makefile&quot;&gt;app: main.o util.o math.o
    gcc main.o util.o math.o -o app

main.o: main.c
    gcc -c main.c -o main.o

util.o: util.c
    gcc -c util.c -o util.o

math.o: math.c
    gcc -c math.c -o math.o&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;make&lt;/code&gt;는 각 타깃의 의존성을 보고 바뀐 파일만 다시 빌드한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;gcc, clang, LLVM은 무슨 관계인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업에서 &quot;요즘은 gcc 말고 LLVM 쓴다&quot;는 말을 듣고 찾아봤다. GCC는 1987년부터 있던 GNU 프로젝트고, C&amp;middot;C++&amp;middot;Fortran 등을 다 지원하는 모놀리식 구조다. LLVM은 컴파일러 자체가 아니라 컴파일러를 만들기 위한 라이브러리 모음, 그러니까 컴파일러 인프라다. Clang은 그 LLVM 위에 올라간 C/C++/Objective-C 프론트엔드고, 우리가 평소에 &quot;LLVM 쓴다&quot;고 할 때는 대체로 Clang을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLVM의 핵심은 프론트엔드/미들엔드/백엔드 분리다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;C 소스   ──[Clang 프론트엔드]──┐
C++ 소스 ──[Clang 프론트엔드]──┤
Rust 소스──[rustc 프론트엔드]──┼──&amp;gt; LLVM IR ──[옵티마이저]──&amp;gt; LLVM IR ──[백엔드]──&amp;gt; 기계어
Swift 소스─[Swift 프론트엔드]──┘                                                ├──&amp;gt; x86
                                                                                ├──&amp;gt; ARM
                                                                                └──&amp;gt; RISC-V&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 언어가 같은 LLVM IR로 모이고, 하나의 옵티마이저와 하나의 백엔드 세트를 공유한다. N개 언어 곱하기 M개 아키텍처만큼 컴파일러가 필요한 게 아니라 N+M개로 줄어드는 구조다. GCC는 모놀리식이라 새 언어나 새 아키텍처를 추가하려면 내부 구조를 잘 알아야 하는데, 이 진입장벽 때문에 Apple도 GCC를 버리고 LLVM/Clang으로 갈아탔고(Clang이 사실상 Apple 프로젝트다), Rust나 Swift 같은 신생 언어들도 처음부터 LLVM 위에서 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실용적으로는 명령줄에서 &lt;code&gt;gcc&lt;/code&gt;를 &lt;code&gt;clang&lt;/code&gt;으로 바꿔도 &lt;code&gt;-c&lt;/code&gt;, &lt;code&gt;-o&lt;/code&gt;, &lt;code&gt;-Wall&lt;/code&gt; 같은 옵션이 거의 그대로 통하고, 성능은 케이스마다 미세하게 갈리며, Clang이 에러 메시지가 더 친절하다는 평이 많다. 참고로 macOS에서 &lt;code&gt;gcc&lt;/code&gt;를 쳐도 사실은 Clang이 불려나온다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제로 헷갈렸던 것들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;-o&lt;/code&gt;와 &lt;code&gt;-c&lt;/code&gt;가 배타적인 옵션인 줄 알았는데 아니었다. &lt;code&gt;-o&lt;/code&gt;는 그냥 &quot;출력 파일 이름&quot;을 지정하는 옵션이라 &lt;code&gt;-c&lt;/code&gt;, &lt;code&gt;-S&lt;/code&gt;, &lt;code&gt;-E&lt;/code&gt; 어느 단계에서든 같이 쓸 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcc -c hello.c -o hello.o     # 컴파일만 + 출력 이름 지정
gcc hello.o -o hello          # 링크 + 출력 이름 지정&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;clang&lt;/code&gt;을 쳤는데 명령을 못 찾은 적도 있는데, Ubuntu/Kali는 기본으로 gcc만 깔려 있고 clang은 따로 설치해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt install clang&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헤더 파일 &lt;code&gt;hello.h&lt;/code&gt;만 따로 &lt;code&gt;gcc -c hello.h&lt;/code&gt; 식으로 컴파일하는 건 안 된다. 헤더는 컴파일 단위가 아니라 &lt;code&gt;.c&lt;/code&gt; 파일이 &lt;code&gt;#include&lt;/code&gt;로 끌어와서 전처리 단계에 끼워 넣는 대상이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수학 함수(&lt;code&gt;sin&lt;/code&gt;, &lt;code&gt;cos&lt;/code&gt; 등)를 쓸 때는 &lt;code&gt;libm&lt;/code&gt;을 따로 링크해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcc main.c -o app -lm&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;-lm&lt;/code&gt;은 명령줄 끝에 두는 게 안전하다. 링커는 왼쪽에서 오른쪽으로 심볼을 해소하기 때문에, &lt;code&gt;-lm&lt;/code&gt;을 앞에 두면 그 시점에 아직 등장하지 않은 심볼이라 그냥 무시되고 나중에 missing reference 에러로 돌아온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고한 자료는 GCC 공식 매뉴얼의 Overall Options 섹션, &lt;code&gt;man gcc&lt;/code&gt;, LLVM 공식 문서의 아키텍처 개요, Tufts CS40의 &quot;Stages of Program Building&quot;, 그리고 Jonathan Cook의 &quot;C/C++: Basic Compiling and Linking&quot;이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;옵션을 단계별 산출물과 연결하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;-E&lt;/code&gt;는 전처리 결과, &lt;code&gt;-S&lt;/code&gt;는 어셈블리, &lt;code&gt;-c&lt;/code&gt;는 아직 링크하지 않은 오브젝트 파일을 만든다. &lt;code&gt;-o&lt;/code&gt;는 단계를 정하는 옵션이 아니라 출력 파일 이름을 지정한다. 이 둘을 구분하면 &amp;ldquo;gcc -c와 -o 중 무엇을 써야 하나&amp;rdquo;라는 질문 자체가 풀린다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/SyP1A/dJMcahrO8rO/AAAAAAAAAAAAAAAAAAAAAFqzgLScB3PExuaHZpozoIBL2gNFKNnRNAte0ucL21Rm/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=%2Bd%2BNE29dywZ7T5UokbINtp5bEes%3D&quot; alt=&quot;GCC Overall Options 공식 문서&quot; /&gt;
&lt;figcaption&gt;GCC Overall Options 공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/NNVPg/dJMcaiYpB3c/AAAAAAAAAAAAAAAAAAAAAOMxmbVQjMQinAYuMOU4k2-k96nrBEhlOF38-qRoU5sR/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=eEQMx%2FeCfjlgdsmWLE5YriAKCV4%3D&quot; alt=&quot;Clang 사용자 매뉴얼&quot; /&gt;
&lt;figcaption&gt;Clang 사용자 매뉴얼&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/cv1CNL/dJMcab59HWr/AAAAAAAAAAAAAAAAAAAAAAClQ-HlKhLn1NgSMDqxnnUDkSLCjebrfnkuWgVoYK8N/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=bdV%2F9IripPDvfZMIYKEJaHpbTzo%3D&quot; alt=&quot;GNU linker ld 공식 문서&quot; /&gt;
&lt;figcaption&gt;GNU linker ld 공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bqkxAq/dJMcahrO8rQ/AAAAAAAAAAAAAAAAAAAAAI_SnyvC-nIsRdZR2thIquRJevc1UuVT5E6HJ_TGeRFp/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=iIPUKBUsvwxPGD8BVDY%2B5AGXJTc%3D&quot; alt=&quot;C 번역 단계 정리&quot; /&gt;
&lt;figcaption&gt;C 번역 단계 정리&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;pre&gt;&lt;code&gt;gcc -E main.c -o main.i
gcc -S main.i -o main.s
gcc -c main.s -o main.o
gcc main.o -o app&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 오류를 볼 때도 전처리&amp;middot;컴파일&amp;middot;어셈블&amp;middot;링크 중 어느 단계에서 멈췄는지 먼저 분류하면 메시지가 훨씬 읽기 쉬워진다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GCC Overall Options&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://clang.llvm.org/docs/UsersManual.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Clang User's Manual&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://sourceware.org/binutils/docs/ld/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GNU ld&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발</category>
      <category>C</category>
      <category>Compiler</category>
      <category>gcc</category>
      <category>linker</category>
      <category>systemprogramming</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/17</guid>
      <comments>https://gorani-kr.tistory.com/17#entry17comment</comments>
      <pubDate>Fri, 24 Jul 2026 07:43:30 +0900</pubDate>
    </item>
    <item>
      <title>드림핵 시스템 해킹 입문 &amp;mdash; 무료 자료만으로 시작하는 루트</title>
      <link>https://gorani-kr.tistory.com/16</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/bsIShc/dJMcaixtjS1/AAAAAAAAAAAAAAAAAAAAACqfs8xTJCLxfVFbhAvN12B-r0nQ8z8-GEQE22ovHkFZ/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=pYwYSwuyD9PcfihCQvBHWOXFQew%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/bsIShc/dJMcaixtjS1/AAAAAAAAAAAAAAAAAAAAACqfs8xTJCLxfVFbhAvN12B-r0nQ8z8-GEQE22ovHkFZ/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=pYwYSwuyD9PcfihCQvBHWOXFQew%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bsIShc/dJMcaixtjS1/AAAAAAAAAAAAAAAAAAAAACqfs8xTJCLxfVFbhAvN12B-r0nQ8z8-GEQE22ovHkFZ/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=pYwYSwuyD9PcfihCQvBHWOXFQew%3D&quot; alt=&quot;드림핵 시스템 해킹 입문 학습 루트 대표 이미지&quot; /&gt;
&lt;figcaption&gt;드림핵 시스템 해킹 입문 학습 루트 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해킹 동아리에 들어가고 나서 드림핵으로 시스템 해킹 공부를 시작했다. 강의를 들어가려고 로그인했더니 950코인을 내라는 문구가 떠서 잠깐 당황했는데, 알고 보니 드림핵이 2025년 5월부터 유료 구독제로 바뀐 뒤였다. 다행히 Tier 0 Path는 여전히 무료고, 무료로 풀 수 있는 워게임도 수백 개나 남아 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해킹 공부를 처음 시작하는 사람한테 어디서부터 하냐고 물으면 열에 아홉은 드림핵을 추천한다. 이유는 단순하다. 한국어 콘텐츠가 있어서 처음 한두 달은 모국어로 개념을 잡는 게 압도적으로 빠르고, 강의 &amp;rarr; 실습 &amp;rarr; CTF로 이어지는 구조가 짜여 있어서 다른 워게임 사이트처럼 문제만 덜렁 던져주지 않는다. 티어 시스템도 도움이 되는데, 처음 시작할 때 가장 어려운 게 사실 &quot;내 실력에 맞는 문제가 뭔지 모르는 것&quot;인데 이걸 플랫폼이 대신 정해준다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;유료화 이후에도 무료로 쓰는 법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025년 5월 2일부로 드림핵이 유료 구독제(Starter / Pro / Expert)로 바뀌면서, 코인 모아서 강의 사는 방식은 사라졌다. 근데 무료 회원도 Tier 0 Path와 무료로 분류된 워게임은 그대로 풀 수 있고, 입문자가 봐야 할 &quot;Dream Beginners&quot; 같은 기초 콘텐츠도 무료 영역에 포함돼 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 가입하면 코인 950개 어쩌고 하는 문구를 보고 결제해야 하나 싶어지는데, 무시해도 된다. 로그인해서 강의 메뉴의 &quot;Dream Beginners&quot; Path부터 듣고, 그다음 System Hacking이나 Web Hacking 입문 강의로 넘어가고, 워게임 메뉴에서 필터로 &quot;무료&quot;를 체크하면 문제 수백 개가 뜬다. 1~2 레벨부터 차근차근 풀면 되고, 막히면 그 문제의 다른 사람 풀이를 보면 된다. 다만 너무 일찍 보면 실력이 안 느니까 30분에서 1시간은 혼자 붙잡고 있다가 보는 게 낫다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코인 결제가 필요한 강의가 분명 있긴 한데, 입문 단계에서는 굳이 손댈 필요가 없다. 한 학기쯤 무료 자료로 진도를 빼면서 자기가 어느 분야로 갈지 정한 다음에 결제를 고려하는 게 효율적이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시스템, 웹, 리버싱 중에 뭘 고를까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드림핵에는 큰 트랙이 세 개 있다. System Hacking(Pwnable)은 메모리 취약점, 버퍼오버플로우, ROP, 힙 익스플로잇을 다루고 C와 어셈블리 지식이 필요한데, CTF의 꽃이라 불리는 영역이다. Web Hacking은 SQL Injection, XSS, CSRF, SSRF, 인증 우회 위주고 HTML/JS/HTTP 기초만 있으면 진입할 수 있어서 입문자한테 가장 친절한 편이다. Reverse Engineering은 바이너리 분석과 디버거(gdb, IDA, Ghidra) 활용이 핵심이라 어셈블리 읽는 게 관건이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템 해킹부터 시작한 건 학교에서 시스템 프로그래밍 수업을 들으면서 C 기본기가 있었고 어셈블리도 배우는 중이라 자연스럽게 이어졌기 때문이다. 웹은 다른 곳에도 자료가 많아서 굳이 드림핵이 아니어도 됐고, 리버싱은 도구 세팅에 시간이 걸릴 것 같아서 일단 미뤄뒀다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;처음 한 달에 반복해서 나오는 개념들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템 해킹 입문 단계에서 이게 안 잡혀 있으면 워게임 풀 때마다 멈추게 되는 개념들이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 레이아웃부터. 스택은 위에서 아래로(높은 주소에서 낮은 주소로) 자라고, 힙은 반대로 자란다. 지역 변수는 스택에 잡히는데, 이걸 넘치게 쓰면 위쪽에 있는 저장된 RBP나 리턴 주소를 덮어쓸 수 있다는 게 버퍼오버플로우의 핵심 직관이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;높은 주소 ┌─────────────┐
         │   Stack     │  &amp;larr; 함수 호출, 지역 변수
         │     &amp;darr;       │
         │             │
         │     &amp;uarr;       │
         │   Heap      │  &amp;larr; malloc, 동적 할당
         ├─────────────┤
         │ BSS / Data  │  &amp;larr; 전역 변수
         ├─────────────┤
         │   Text      │  &amp;larr; 코드(명령어)
낮은 주소 └─────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 현대 바이너리는 그냥 BOF가 되지 않는다. 기본적으로 몇 가지 보호 기법이 켜져 있어서다. NX(No-eXecute)는 스택을 실행 불가능하게 만들어서 셸코드를 스택에 올려도 실행이 안 되고(그래서 ROP로 우회한다), ASLR은 라이브러리&amp;middot;스택&amp;middot;힙 주소를 매번 랜덤 배치해서 정보 유출(leak)로 우회해야 하고, Stack Canary는 함수 진입 시 스택에 랜덤 값을 박아두고 리턴 직전에 검증해서 덮어쓰면 그냥 abort시켜버린다. PIE는 코드 자체를 랜덤 주소에 두는 거고 역시 leak이 필요하다. 이 네 개가 끝없이 나온다. 처음에는 일부러 보호 기법을 전부 끈(&lt;code&gt;gcc -fno-stack-protector -no-pie -z execstack ...&lt;/code&gt;) 바이너리로 BOF의 원리부터 익히고, 그다음에 보호 기법을 하나씩 켜가면서 우회법을 배우는 순서가 맞는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도구는 gdb에 pwndbg 확장을 얹은 걸 기본으로 쓰고 있다. pwndbg가 스택&amp;middot;레지스터&amp;middot;디스어셈블을 한 화면에 정리해서 보여줘서 처음 배울 때 거의 필수다. 익스플로잇 짤 때는 pwntools(&lt;code&gt;from pwn import *&lt;/code&gt;)가 표준이고, 바이너리에 어떤 보호 기법이 켜져 있는지는 &lt;code&gt;checksec&lt;/code&gt; 한 줄로 확인한다. 디스어셈블러는 처음엔 무료인 Ghidra로 충분하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;처음 시작하는 사람한테&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드림핵 켜고 950코인 보고 좌절했던 얘기를 친구한테 했더니 &quot;그냥 무료 필터 켜고 풀면 되는데 ㅋㅋ&quot;라는 답이 돌아왔고, 그 말이 맞았다. 입문자한테 진짜 필요한 건 결제가 아니라 그냥 꾸준히 앉아 있는 시간이다. 책 사서 처음부터 끝까지 읽고 시작하려고 하면 시작 자체를 못 하게 되는 것 같다. 가입하고 Tier 0 Path 클릭해서 강의 하나 들으면서 모르는 단어를 검색하는 것만 반복해도 한 달 뒤엔 뭔가 풀리는 게 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 학기 목표는 Pwnable 1~3 레벨 워게임을 다 풀고 시스템 해킹 강의 한 코스를 완주하는 거다. 화이트햇 스쿨 모집까지 남은 기간 동안 어디까지 갈 수 있는지가 지금 관심사다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dreamhack 공식 &amp;mdash; Dream Beginners Path&lt;/li&gt;
&lt;li&gt;드림핵 유료 구독 서비스 관련 공지 (2025년 5월)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;무료 자료로 시작할 때도 순서가 필요했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 많이 푸는 것보다 C 실행 구조, 어셈블리, 디버거, 메모리 보호기법을 한 줄로 연결하는 게 먼저였다. 드림핵 Path의 전체 구성을 지도처럼 보고, 무료로 열린 단원과 워게임을 골라 작은 실습을 반복했다. 막힌 명령어는 pwntools 문서와 Linux man page에서 원래 의미를 확인했다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/oXayw/dJMcaixtjS2/AAAAAAAAAAAAAAAAAAAAAACYrzF978c_NdNKtBRr2osV87LlnwMXcPYpN1dbQlgU/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=Z3gNqDZhclfMQK6Tdgs1VMJWjUE%3D&quot; alt=&quot;Dreamhack System Hacking Basics 학습 Path&quot; /&gt;
&lt;figcaption&gt;Dreamhack System Hacking Basics 학습 Path&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/cwUx9x/dJMcadXcmCY/AAAAAAAAAAAAAAAAAAAAAEG7mr0UInEN3GiMNkoXOKDHIvR80R2J0Gt-wauZfSp7/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=Yox5vF7b5sH07YSbLOkzDjKb200%3D&quot; alt=&quot;Dreamhack 워게임 목록&quot; /&gt;
&lt;figcaption&gt;Dreamhack 워게임 목록&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/PztDE/dJMcaixtjS3/AAAAAAAAAAAAAAAAAAAAAEgLMNWhr2QgFL7G_f48ZP6zm3aMFP3ZYpsxd4g2m0Y-/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=K%2FjArG835IBrXL88%2B3KKkh2NEyQ%3D&quot; alt=&quot;pwntools 공식 문서&quot; /&gt;
&lt;figcaption&gt;pwntools 공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/cEaAZv/dJMcadXcmC3/AAAAAAAAAAAAAAAAAAAAAFQgUJtz35gQddVwg22Fra8tkIBka9fQYw1e82zSe5lB/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=EfiAyoDf3mPNG7Lro5iZptiQafU%3D&quot; alt=&quot;Linux man-pages 프로젝트 문서 색인&quot; /&gt;
&lt;figcaption&gt;Linux man-pages 프로젝트 문서 색인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;내가 잡은 학습 루프&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;짧은 C 프로그램을 직접 컴파일하고 보호기법을 확인한다.&lt;/li&gt;
&lt;li&gt;gdb에서 입력 전후 레지스터와 스택을 본다.&lt;/li&gt;
&lt;li&gt;익스플로잇 스크립트는 pwntools로 자동화하되 각 I/O를 설명할 수 있게 한다.&lt;/li&gt;
&lt;li&gt;풀이를 본 뒤에는 새 바이너리에서 같은 원리를 다시 적용한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무료&amp;middot;유료 여부는 자주 바뀌므로 특정 강의가 계속 무료라고 단정하지 않았다. Path 전체를 한 번에 결제하기보다 공개 자료로 기초를 확인한 다음, 설명이 필요한 구간에 맞춰 강의나 Unit을 고르는 편이 효율적이었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://dreamhack.io/lecture/paths/system-hacking-fundamental&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Dreamhack System Hacking Path&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dreamhack.io/wargame&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Dreamhack Wargame&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.pwntools.com/en/stable/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;pwntools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Linux man-pages&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>보안</category>
      <category>dreamhack</category>
      <category>pwnable</category>
      <category>systemhacking</category>
      <category>보안공부</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/16</guid>
      <comments>https://gorani-kr.tistory.com/16#entry16comment</comments>
      <pubDate>Fri, 24 Jul 2026 07:43:21 +0900</pubDate>
    </item>
    <item>
      <title>AFL++로 네트워크 포렌식 파서 퍼징해서 GHSA 10건 제보&amp;middot;게재까지</title>
      <link>https://gorani-kr.tistory.com/15</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/Fs1pE/dJMcafU32Hn/AAAAAAAAAAAAAAAAAAAAAL4b58FlVOJt1Lk0lfRx5uEAq0LtamNhMr8t22Gv1UzH/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=9ccE2UTfm%2BOIQ47ByEe%2FCcdfUWY%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/Fs1pE/dJMcafU32Hn/AAAAAAAAAAAAAAAAAAAAAL4b58FlVOJt1Lk0lfRx5uEAq0LtamNhMr8t22Gv1UzH/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=9ccE2UTfm%2BOIQ47ByEe%2FCcdfUWY%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/Fs1pE/dJMcafU32Hn/AAAAAAAAAAAAAAAAAAAAAL4b58FlVOJt1Lk0lfRx5uEAq0LtamNhMr8t22Gv1UzH/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=9ccE2UTfm%2BOIQ47ByEe%2FCcdfUWY%3D&quot; alt=&quot;AFL++ 네트워크 파서 퍼징과 GHSA 제보 대표 이미지&quot; /&gt;
&lt;figcaption&gt;AFL++ 네트워크 파서 퍼징과 GHSA 제보 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-filename=&quot;ghsa-published.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1600&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/pcvGm/dJMcafneCIG/AAAAAAAAAAAAAAAAAAAAAIjNRVtAs0wrmhz7iw_74TJTbbgPkESHWhBHIC8od8S3/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=dtVbKhLYw32r8IzUqia06b0kCAY%3D&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FpcvGm%2FdJMcafneCIG%2FAAAAAAAAAAAAAAAAAAAAAIjNRVtAs0wrmhz7iw_74TJTbbgPkESHWhBHIC8od8S3%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1785509999%26allow_ip%3D%26allow_referer%3D%26signature%3DdtVbKhLYw32r8IzUqia06b0kCAY%253D&quot; alt=&quot;nfdump 취약점이 GitHub Security Advisory로 공개된 화면&quot; width=&quot;1280&quot; height=&quot;1600&quot; data-filename=&quot;ghsa-published.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1600&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포트폴리오에 쓸 CVE를 하나 노리고 시작한 작업이다. 목표는 한 달 안에 CVE나 보안 기여 실적 1건. 취업이나 BoB 지원용으로 &quot;실제 오픈소스에서 취약점을 찾아 제보한 경험&quot;만큼 강한 카드가 없다고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 타깃 선정이었다. 아무거나 퍼징하면 십중팔구 이미 남이 다 훑어놓은 코드다. 특히 이미지나 폰트, &lt;code&gt;libpcap&lt;/code&gt; 자체 같은 유명 파서는 구글 OSS-Fuzz가 24시간 상시 퍼징 중이라 얕은 크래시는 이미 다 잡혀 있다. 그래서 방향을 이렇게 잡았다. 도메인은 네트워크 포렌식/DFIR 파서로. 내 EDR&amp;middot;침해대응 서사와도 붙고, 패킷에서 무슨 일이 있었는지 재구성하는 도구라 신뢰할 수 없는 입력(pcap)을 많이 먹는다는 이유였다. OSS-Fuzz에 등재된 프로젝트는 제외하기로 했는데(libyal, suricata, zeek, yara, pcapplusplus, sleuthkit 등은 실제로 등재 목록 1,366개를 받아서 배제 확인했다), 대신 자체 파싱 코드가 있고 pcap을 파일로 바로 먹으며(&lt;code&gt;-r file&lt;/code&gt;), 아직 활발히 유지보수되는 프로젝트를 선호 조건으로 뒀다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;셋업&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;퍼징 셋업 자체는 단순하다. 12코어 리눅스 박스에서 &lt;code&gt;AFL_USE_ASAN=1 CC=afl-clang-fast CXX=afl-clang-fast++&lt;/code&gt;로 계측 빌드를 하는데, &lt;code&gt;afl-clang-fast&lt;/code&gt;가 커버리지 계측을, AddressSanitizer가 메모리 오류 탐지를 담당한다. 빌드 후엔 &lt;code&gt;nm&lt;/code&gt;이나 &lt;code&gt;strings&lt;/code&gt;로 &lt;code&gt;__afl_*&lt;/code&gt;, &lt;code&gt;__asan_*&lt;/code&gt; 심볼이 실제로 들어갔는지 꼭 확인해야 한다. 이거 안 하면 계측 안 된 바이너리를 몇 시간씩 헛돌리게 된다. 시드 코퍼스는 각 프로젝트의 &lt;code&gt;tests/*.pcap&lt;/code&gt;을 그대로 썼는데, 프로젝트가 자기 테스트로 넣어둔 pcap이 제일 좋은 시드다. 실행은 &lt;code&gt;afl-fuzz -i seeds -o out -M main ... -- ./target -r @@&lt;/code&gt; 형태로, &lt;code&gt;@@&lt;/code&gt;가 입력 파일 경로로 치환된다. 코어가 많아서 &lt;code&gt;-M main&lt;/code&gt; 하나에 &lt;code&gt;-S&lt;/code&gt; 보조 인스턴스 여러 개를 explore/exploit/fast 스케줄로 다르게 걸어 병렬로 돌렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tcpflow는 시드를 정상 실행해도 종료 시 &lt;code&gt;exit=1&lt;/code&gt;이 떴는데, 버그인 줄 알고 놀랐다가 알고 보니 LeakSanitizer가 잡는 양성 스타트업 누수(스캐너 로딩)였다. &lt;code&gt;ASAN_OPTIONS=detect_leaks=0&lt;/code&gt;으로 LSan을 꺼야 dry-run이 막히지 않았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1차 &amp;mdash; tcpflow, tcpreplay, pcapfix&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 개를 병렬로 돌렸다. pcapfix는 퍼저가 35초 만에 고유 크래시 1건을 찾았는데, &lt;code&gt;fix_pcapng&lt;/code&gt;(pcapng.c)에서 파일이 제어하는 길이값을 검증 없이 할당 크기로 써서 &lt;code&gt;malloc&lt;/code&gt;이 NULL을 반환하고 거기에 &lt;code&gt;fread&lt;/code&gt;가 write하는 SEGV였다. tcpflow는 크래시가 139건 넘게 나왔고, 심지어 번들 테스트 파일 &lt;code&gt;simson.pcap&lt;/code&gt; 자체가 ASAN에서 크래시했다. &lt;code&gt;tcpip::print_packet&lt;/code&gt;(tcpip.cpp:383)의 heap over-read를 포함해 3개 사이트가 나왔다. tcpreplay는 크래시가 0이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &quot;CVE 3개다&quot; 하고 신나면 안 된다. 진짜 일은 이게 known-ness가 있는지 확인하는 거였다. pcapfix는 확인해보니 중복이었다. open 이슈 #28(Honggfuzz)이 내가 퍼징한 바로 그 버전(1.1.7)에서 정확히 같은 &lt;code&gt;0xfffffffffffffffc&lt;/code&gt; 할당 크래시를 이미 보고해뒀고, #38도 같은 영역이라 CVE 가치가 없었다. tcpflow는 미확정 후보로 남았다. 기존 이슈(#216은 다른 &lt;code&gt;futimes&lt;/code&gt; 크래시, #247은 다른 함수의 로직 버그)와는 안 겹치고 published CVE도 없었지만, tcpflow는 과거 AFL 퍼징 이력이 있어서(이슈에 &lt;code&gt;crashes/&lt;/code&gt; 디렉토리 흔적) 비공개로 이미 알려졌을 가능성을 배제할 수 없었다. 그래서 novel 확정이라고는 말하지 않기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 얻은 교훈은 크래시 개수는 아무 의미가 없다는 거였다. 접근성 좋은 타깃일수록 쉬운 크래시는 이미 남이 다 봤다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2차 &amp;mdash; softflowd, justniffer, ngrep&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기준을 &quot;OSS-Fuzz 미등재 + 최근 퍼징 이력 적음 + 유지보수 중 + 자체 코드&quot;로 바꾸고 골랐다. 그리고 인프라 버그 하나를 고쳤는데, 1차 때 퍼저들이 자꾸 죽었던 원인이 OOM이 아니라 detach 실패였다. &lt;code&gt;nohup&lt;/code&gt;만으로 띄운 인스턴스는 SSH 세션이 끊길 때 SIGHUP으로 같이 죽었는데, &lt;code&gt;setsid -f&lt;/code&gt;로 완전히 부모에서 떼어내니(PPID=1) 살아남았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;justniffer는 크래시가 34건 나왔지만 전부 번들된 &lt;code&gt;libnids-1.21&lt;/code&gt;(2010년산 3rd-party 라이브러리)의 IP 조각 재조립 버그(ip_fragment.c:417)였다. justniffer 자체 코드가 아니고 이 계열은 이미 잘 알려져 있어서 가치가 낮았다. 자체 포맷 파서 오버플로도 있었는데 그건 pcap이 아니라 사용자가 주는 &lt;code&gt;-l&lt;/code&gt; 포맷 인자로 트리거되는 거라 원격 악용성이 낮았다. ngrep은 크래시가 0이었다(17분/53사이클/커버리지 10%에서 정체).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;softflowd에서 유력한 후보가 나왔다. &lt;code&gt;datalink_check&lt;/code&gt;(softflowd.c:1213)에서 frametype 바이트를 읽는데, &lt;code&gt;caplen&lt;/code&gt;을 &lt;code&gt;skiplen&lt;/code&gt;에 대해서만 검사(:1185)하고 실제로 인덱싱하는 &lt;code&gt;ft_off + vlan_size + ft_len&lt;/code&gt; 범위는 검사하지 않는다. 16바이트짜리 짧은 패킷을 주면 버퍼 끝 뒤를 읽는다(CWE-125). softflowd 자체 코드고 근본원인이 명확하며 무관한 이슈뿐이고 유지보수 중(v1.1.1)이었다. 52KB 크래시를 1,468B PoC로 최소화해서 재현을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서의 교훈은 자체 코드냐 번들 라이브러리냐를 반드시 구분해야 한다는 거였다. justniffer 34건은 화려해 보여도 남의 라이브러리 버그라 내 크레딧이 안 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3차 &amp;mdash; nfdump&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기준을 더 밀어붙여서 pcap 대신 자체 바이너리 포맷을 파싱하는, 아직 거의 안 퍼징된, 그런데 지금도 활발히 유지보수되는 파서를 찾았다. 골라진 게 nfdump(넷플로우/nfcapd 수집&amp;middot;분석 도구, phaag/nfdump, v1.7.8, 2026-06 커밋)였다. ngrep은 커버리지가 10%에서 막혀서 CmpLog(비교 연산을 학습해 매직바이트&amp;middot;길이 필드를 뚫는 AFL++ 기능)를 붙여 재개했는데, 58사이클을 더 돌려도 크래시가 0이라 표면이 얕아 깨끗하다고 결론 냈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nfdump는 12분 만에 크래시가 35건 나왔다. 심볼라이즈해서 dedup하니 32건이 한 곳에 몰렸는데, &lt;code&gt;AddVrfNameRecord&lt;/code&gt;/&lt;code&gt;AddIfNameRecord&lt;/code&gt;(ifvrf.c)였다. 인터페이스/VRF 이름 배열 레코드를 파싱하는데, 파일에서 읽은 &lt;code&gt;numElements&lt;/code&gt;&amp;middot;&lt;code&gt;elementSize&lt;/code&gt;를 레코드 크기 대비 검증 없이 그대로 루프&amp;middot;스트라이드로 써서 &lt;code&gt;*ingress&lt;/code&gt;(uint32)를 데이터 블록 뒤로 읽는다(READ of size 4).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;known-ness를 확인했다. 알려진 nfdump 버퍼오버플로 CVE(CVE-2019-1010057)는 &lt;code&gt;nfx.c&lt;/code&gt;, &lt;code&gt;nffile_inline.c&lt;/code&gt;, &lt;code&gt;minilzo.c&lt;/code&gt;이고 2019년에 이미 수정됐다. &lt;code&gt;ifvrf.c&lt;/code&gt;의 이 함수들은 어떤 CVE나 이슈, OSS-Fuzz 리포트와도 연결되지 않았다. 스택도 깔끔했다. &lt;code&gt;AddVrfNameRecord (ifvrf.c:106) &amp;larr; process_data (nfdump.c:755) &amp;larr; main&lt;/code&gt;. 즉 &lt;code&gt;nfdump -r &amp;lt;악성파일&amp;gt;&lt;/code&gt; 파일 파싱 주경로에서 바로 터지는, 최신 버전 자체 코드의 미보고 OOB read였다. 298바이트 PoC로 3/3 재현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;novelty는 타깃 신선도에 거의 비례한다는 걸 여기서 확실히 느꼈다. 남들이 이미 pcap을 퍼징한 도구(tcpflow) 대신, 자체 바이너리 포맷을 파싱하면서 최근 퍼징 흔적이 없는 도구(nfdump)로 갔더니 확신도 높은 후보가 바로 나왔다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;pcap 파서에서 OOB read가 반복되는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 타깃(tcpflow, softflowd)에서 나온 버그의 근본 원인이 사실상 같다. 패킷이 말하는 길이를 실제로 캡처된 길이(&lt;code&gt;caplen&lt;/code&gt;)로 검증하지 않는다는 것이다. pcap의 각 레코드에는 원래 패킷 길이(&lt;code&gt;len&lt;/code&gt;)와 실제 저장된 길이(&lt;code&gt;caplen&lt;/code&gt;)가 따로 있다. 정상 캡처면 보통 같지만, 공격자가 만든 pcap은 헤더에 &quot;이 패킷은 1500바이트야&quot;라고 써놓고 실제로는 16바이트만 넣을 수 있다. 파서가 헤더의 길이나 프로토콜 구조체 크기를 믿고 그만큼 읽으면 libpcap이 할당한 버퍼(예: 2048B) 끝을 넘어간다. tcpflow는 TCP 페이로드 &lt;code&gt;length&lt;/code&gt;를 &lt;code&gt;caplen&lt;/code&gt;으로 안 자르고 &lt;code&gt;print_packet&lt;/code&gt;까지 넘겼고, &lt;code&gt;caplen &amp;lt; sizeof(ether_header)&lt;/code&gt;일 때 부호 없는 뺄셈이 언더플로되면서 거대한 길이가 됐다. softflowd는 frametype 오프셋(&lt;code&gt;ft_off + vlan_size + ft_len&lt;/code&gt;)이 &lt;code&gt;caplen&lt;/code&gt;을 넘는지 안 봤다. 방어는 전부 같다. 읽기 직전에 필요한 오프셋 + 크기가 &lt;code&gt;caplen&lt;/code&gt; 이하인지 확인하고, 아니면 패킷을 스킵하는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 찾은 건 전부 read 오버플로다. 버퍼 뒤를 몇 바이트 읽고 크래시가 나는 거라 프로그램이 죽으니 DoS이고, 인접 힙 메모리가 출력에 섞이면 경미한 정보 노출 가능성은 있다. 하지만 쓰기(write) primitive가 없으면 코드 실행(RCE)으로 이어지지 않는다. 리포트에 &quot;RCE 가능&quot;이라고 부풀려 쓰면 메인테이너 신뢰를 잃고, 심각도 과장은 보안 커뮤니티에서 바로 티가 난다. 그래서 초안에 &quot;OOB read, DoS, RCE 아님&quot;을 명시적으로 박아뒀다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크래시가 났을 때 CVE 후보인지 거르려면 순서가 있다. 먼저 ASAN &lt;code&gt;#0&lt;/code&gt; 프레임(file:line)으로 유니크한 크래시 지점만 세서 사이트별로 dedup한다(139건이 3개 사이트일 수 있다). 그다음 크래시 파일이 프로젝트 소스인지 번들된 오래된 라이브러리인지 본다. 원격이나 파일로 트리거되는지, 아니면 사용자가 직접 주는 인자로만 되는지도 확인한다(후자는 악용성이 낮다). 프로젝트 이슈 트래커와 CVE, 과거 퍼징 흔적을 검색해서 같은 사이트나 클래스가 이미 보고됐는지 본다. 마지막으로 &lt;code&gt;afl-tmin&lt;/code&gt;으로 PoC를 줄여 근본 원인을 특정한다. 이 다섯 단계를 안 거치면 크래시 더미일 뿐 제보 가능한 버그가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 퍼징 돌리면 CVE가 뚝딱 나오겠지 싶었는데, 실제로는 크래시를 내는 것보다 그 크래시가 가치 있는지 정직하게 깎아내리는 작업이 대부분이었다. pcapfix에서 35초 만에 크래시라고 흥분했다가 기존 이슈 #28을 발견하고 김이 샜을 때, 이게 진짜 보안 리서치의 질감이구나 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제보 이후에는 예상보다 훨씬 큰 결과가 나왔다. nfdump에서 찾은 문제는 최종적으로 9건 모두 GitHub Security Advisory로 공개됐다. 원격 nfcapd/IPFIX 경로에서 발생하는 5건은 UAF 2건, SIGFPE 1건, CPU&amp;middot;디스크 고갈 2건으로 각각 CVSS 7.5를 받았다. &lt;code&gt;nfdump -r&lt;/code&gt; 파일 파서에서 찾은 NBAR&amp;middot;ifvrf OOB, 구버전 v1 ident stack OOB, appendix log-flood DoS 4건은 각각 CVSS 5.5로 게재됐다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원격 nfcapd/IPFIX: &lt;code&gt;GHSA-35rg-4vr7-fmr2&lt;/code&gt;, &lt;code&gt;GHSA-h97c-j4xh-mrwh&lt;/code&gt;, &lt;code&gt;GHSA-9q9x-c3p9-pp6r&lt;/code&gt;, &lt;code&gt;GHSA-4x68-h7hg-9gh4&lt;/code&gt;, &lt;code&gt;GHSA-7rq7-vj35-pw96&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;파일 파서: &lt;code&gt;GHSA-5c4g-q9hx-6x4m&lt;/code&gt;, &lt;code&gt;GHSA-qrg4-hf97-rg7g&lt;/code&gt;, &lt;code&gt;GHSA-jfw9-v5p4-pxw2&lt;/code&gt;, &lt;code&gt;GHSA-7vhp-748h-jjx6&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;justniffer도 처음에는 번들된 libnids 문제라서 가치가 낮다고 판단했지만, 후속 분석에서 IP 조각 재조립 경로의 heap over-read를 별도로 정리해 &lt;code&gt;GHSA-3gvq-jf6c-94v8&lt;/code&gt;로 제보했다. 이 건은 CVSS 8.2로 공개됐고 v0.6.14에서 수정됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;softflowd와 tcpflow는 저장소에 Private Vulnerability Reporting이 활성화돼 있지 않아 공개 GitHub Issue로 제출했다. 이 과정에서 초기 후보와 최종 제출 내용도 달라졌다. tcpflow에서 처음 본 세 사이트 중 &lt;code&gt;dl_ethernet&lt;/code&gt;은 현재 코드에 이미 방어 로직이 있어 제외했고, 나머지 두 사이트를 같은 근본 원인인 &lt;code&gt;caplen&lt;/code&gt; 미검증 문제로 묶어 &lt;code&gt;simsong/tcpflow#277&lt;/code&gt;에 제출했다. softflowd는 후속 퍼징에서 MPLS 라벨 스택 깊이를 제한하지 않아 &lt;code&gt;flow_compare()&lt;/code&gt;가 배열 밖을 읽고 flow-table 집계까지 망가뜨리는 문제를 확인해 &lt;code&gt;irino/softflowd#67&lt;/code&gt;로 제출했다. 두 이슈 모두 공개돼 있으며, 현재 확인한 영향은 OOB read와 DoS&amp;middot;데이터 무결성 문제까지다. RCE는 주장하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 목표는 CVE나 보안 기여 1건이었다. 실제 결과는 published GHSA 10건과 공개 이슈 2건이 됐다. 다만 크래시가 많이 나왔다고 그대로 성과가 된 것은 아니었다. 중복을 버리고, 번들 라이브러리와 자체 코드를 구분하고, 재현되지 않는 사이트를 제외하고, 심각도를 과장하지 않는 과정이 퍼징 자체보다 오래 걸렸다. CVE ID는 아직 배정되지 않았지만, 메인테이너가 내용을 검토해 advisory로 공개하고 실제 수정까지 반영했다는 점이 더 확실한 결과로 남았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 이제 두 가지다. 남은 CVE ID 배정 여부를 추적하는 것, 그리고 이번에 반복해서 확인한 &quot;파일이 말하는 길이 vs 실제 캡처/버퍼 길이&quot; 미검증 패턴을 EDR 쪽 탐지&amp;middot;안전 파싱 관점과 연결하는 것. malformed 입력에서 뭐가 터지는지는 결국 공격 표면 이야기라 서사가 이어진다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AFL++ 공식 문서 &amp;mdash; instrumentation, &lt;code&gt;-M&lt;/code&gt;/&lt;code&gt;-S&lt;/code&gt; 병렬, &lt;code&gt;afl-tmin&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;AddressSanitizer &amp;mdash; &lt;code&gt;ASAN_OPTIONS&lt;/code&gt;(&lt;code&gt;detect_leaks&lt;/code&gt;, &lt;code&gt;abort_on_error&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;CWE-125: Out-of-bounds Read&lt;/li&gt;
&lt;li&gt;Google OSS-Fuzz &amp;mdash; 등재 프로젝트 목록(중복 회피용 기준)&lt;/li&gt;
&lt;li&gt;게재된 결과: phaag/nfdump GHSA 9건(published), onotelli/justniffer GHSA-3gvq-jf6c-94v8(published, fixed v0.6.14), irino/softflowd#67(공개 이슈), simsong/tcpflow#277(공개 이슈)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실험은 전부 본인 소유의 격리된 리눅스 환경에서, 공개된 오픈소스와 그 자체 테스트 데이터로만 수행했다. 발견한 버그는 메인테이너에게 책임 있게 제보했고, exploit이 아니라 크래시 재현용 최소 PoC만 다뤘다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/AFLplusplus/AFLplusplus&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AFL++ 공식 저장소&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://google.github.io/oss-fuzz/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Google OSS-Fuzz 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/advisories&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GitHub Advisory Database&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제보 채널을 고르는 과정은 &lt;a href=&quot;https://gorani-kr.tistory.com/31&quot;&gt;PVR이 없을 때 취약점을 제보한 경험&lt;/a&gt;에 따로 적었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로컬 퍼징에서 공개 제보까지 연결된 도구들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AFL++가 입력을 변형해 경로를 넓히는 역할을 했다면, OSS-Fuzz는 오픈소스 프로젝트의 지속 퍼징 운영 모델을 보여줬고, GitHub Repository Security Advisory는 수정 전 비공개 협업과 CVE 요청을 연결하는 창구였다. 세 단계가 서로 다른 문제를 푼다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bxBR3N/dJMcac42ixt/AAAAAAAAAAAAAAAAAAAAAMkSVwN4J4WaX8V6f5w2I6vCQnjDsIjB7rZjD0faSYYq/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=jN%2BUB80f4qv8ZKPpR5R2%2FwjFOXs%3D&quot; alt=&quot;AFL++ 공식 GitHub 저장소&quot; /&gt;
&lt;figcaption&gt;AFL++ 공식 GitHub 저장소&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bhCV0N/dJMcac42ixB/AAAAAAAAAAAAAAAAAAAAANcneNdabHqnvl0ggs2wazuc7POT4ezOpouxbMKBb-40/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=QKOqu74j%2FFBIxkgQ7KDDCZWGeVo%3D&quot; alt=&quot;Google OSS-Fuzz 공식 문서&quot; /&gt;
&lt;figcaption&gt;Google OSS-Fuzz 공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/cfYyKS/dJMcaixtjS0/AAAAAAAAAAAAAAAAAAAAANkbpyjdOTr5wzWnHGugFOLgccnmvy061ZYRHau4z0Op/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=f9sOe0K%2Bs5jfOhz7qbO4bZrmn%2FY%3D&quot; alt=&quot;GitHub Repository Security Advisory 공식 문서&quot; /&gt;
&lt;figcaption&gt;GitHub Repository Security Advisory 공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크래시 수를 성과로 세기보다 고유한 root cause인지, 최신 커밋에서 재현되는지, sanitizer 스택과 최소 입력이 있는지를 먼저 확인했다. 동일 원인의 변형은 한 건으로 묶고, 관리자에게는 영향과 재현 환경을 과장 없이 전달했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/AFLplusplus/AFLplusplus&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AFL++&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://google.github.io/oss-fuzz/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;OSS-Fuzz&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/code-security/security-advisories/working-with-repository-security-advisories/about-repository-security-advisories&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GitHub Repository Security Advisories&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>보안</category>
      <category>AddressSanitizer</category>
      <category>AFL++</category>
      <category>fuzzing</category>
      <category>GHSA</category>
      <category>pcap</category>
      <category>Security</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/15</guid>
      <comments>https://gorani-kr.tistory.com/15#entry15comment</comments>
      <pubDate>Thu, 23 Jul 2026 20:17:35 +0900</pubDate>
    </item>
    <item>
      <title>ipTIME N2V 공유기 CVE-2025-55423 분석 &amp;mdash; UPnP Relay에 박힌 Command Injection</title>
      <link>https://gorani-kr.tistory.com/14</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/bek0wv/dJMcafU32GL/AAAAAAAAAAAAAAAAAAAAAJFtwUSlOjPENZDeWXfUxyAS5hmZICyXxJrjx0KAA9bA/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=3eYAxB2w3VLYEY3rp8jUAv8PYm4%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/bek0wv/dJMcafU32GL/AAAAAAAAAAAAAAAAAAAAAJFtwUSlOjPENZDeWXfUxyAS5hmZICyXxJrjx0KAA9bA/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=3eYAxB2w3VLYEY3rp8jUAv8PYm4%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bek0wv/dJMcafU32GL/AAAAAAAAAAAAAAAAAAAAAJFtwUSlOjPENZDeWXfUxyAS5hmZICyXxJrjx0KAA9bA/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=3eYAxB2w3VLYEY3rp8jUAv8PYm4%3D&quot; alt=&quot;CVE-2025-55423 Command Injection 분석 대표 이미지&quot; /&gt;
&lt;figcaption&gt;CVE-2025-55423 Command Injection 분석 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안동아리에서 한 학기짜리 프로젝트로 IoT/임베디드 기기 해킹을 잡았다. 팀은 4명, 리버싱 한 명, 웹해킹 한 명, 프론트 한 명, 그리고 나. 타깃 기기를 뭘로 할지 고민하다가 집에 안 쓰고 굴러다니던 ipTIME 공유기 N2V가 떠올라서 그걸로 정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기기를 정하고 나면 제일 먼저 하는 게 모델명으로 알려진 CVE부터 검색해보는 거다. 그렇게 검색하자마자 CVE-2025-55423이 떴는데, 영향받는 모델 목록 163종 안에 N2V가 정확히 들어 있었다. 분석 대상은 이 글 전체에서 내 소유의 N2V 공유기이고, 격리된 테스트 LAN에서만 다뤘다는 걸 먼저 밝혀둔다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;취약점이 뭔지부터&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CVE-2025-55423은 OS Command Injection(CWE-78)이다. 인증 없이 네트워크에 인접한 공격자가 root 권한으로 임의 명령을 실행할 수 있다. 취약 함수는 옛 펌웨어 기준 &lt;code&gt;/lib/libcgi.so&lt;/code&gt;의 &lt;code&gt;upnp_relay()&lt;/code&gt;이고, 새 펌웨어에서는 &lt;code&gt;/lib/libesysapi.so&lt;/code&gt;의 &lt;code&gt;upnp_relay_add()&lt;/code&gt; / &lt;code&gt;upnp_relay_rm()&lt;/code&gt;이다. 163개 모델이 영향을 받는데, N2V가 속한 EoS(단종) 라인은 패치가 안 나온다. 벤더 권장도 &quot;기기 폐기 또는 포트포워딩 릴레이 기능 비활성화&quot;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기능 자체를 먼저 이해해야 했다. ipTIME 펌웨어 9.90.4 이후 추가된 UPnP Relay는 이런 상황을 자동화한다. ISP 공유기 아래에 ipTIME 공유기가 2단으로 물려 있고, 그 아래 내 PC가 있다고 하자. 내 PC의 앱이 UPnP로 ipTIME 공유기에 포트를 열어달라고 요청하면, ipTIME이 그 요청을 받아서 자기 NAT에 룰을 추가한다. 근데 외부에서 보면 그 포트는 여전히 ISP 공유기에 막혀 있다. 그래서 ipTIME이 상위 공유기(ISP 라우터)에도 같은 룰을 자동으로 등록해주는 게 UPnP Relay다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 상위 공유기에 SOAP 요청을 보내야 하는데, 그 요청의 엔드포인트가 &lt;code&gt;controlURL&lt;/code&gt;이다. 즉 LAN에 있는 누군가가 UPnP로 포트포워딩을 요청하면, ipTIME은 그 요청에서 뽑아낸 &lt;code&gt;controlURL&lt;/code&gt;을 가지고 상위 공유기에 또 SOAP 요청을 날린다. 문제는 이 값이 검증 없이 처리된다는 데 있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;취약한 코드 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공개된 분석에 따르면 옛 펌웨어의 &lt;code&gt;upnp_relay()&lt;/code&gt;는 대략 이런 식이었다(의사코드).&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void upnp_relay(...) {
    char *control_url = parse_soap_action(request);
    char cmd[1024];
    snprintf(cmd, sizeof(cmd),
        &quot;curl -s -X POST %s -d '...'&quot;,
        control_url);  // 검증 없음
    system2(cmd);      // 그대로 셸에 던짐
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;controlURL&lt;/code&gt;은 LAN에 있는 디바이스가 보낸 UPnP/SSDP 응답에서 가져오는 값인데, 어떤 검증이나 이스케이핑도 없이 &lt;code&gt;system()&lt;/code&gt; 계열 함수에 그대로 들어간다. 세미콜론이나 파이프 같은 셸 메타문자가 그대로 통과한다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론상 공격자가 SSDP 응답을 위조해서 &lt;code&gt;controlURL&lt;/code&gt;에 &lt;code&gt;http://192.168.0.1/ctl/IPConn;reboot&lt;/code&gt; 같은 값을 실어 보내면, ipTIME은 그 값을 그대로 명령어 문자열에 끼워넣어 실행하게 된다. 세미콜론 뒤의 &lt;code&gt;reboot&lt;/code&gt;이 root 권한으로 실행되는 구조다. 공개된 분석 자료에는 SSDP 가짜 응답, ARP/DHCP 스푸핑을 조합해 라우터가 공격자를 상위 공유기로 착각하게 만드는 전체 공격 체인이 단계별로 나와 있는데, 이 글에서는 취약점의 근본 원인과 방어 원리 위주로만 정리하고 구체적인 공격 절차는 옮기지 않기로 했다. 어차피 지금 N2V는 패치가 안 나오는 EoS 상태라 상세 절차를 공개하는 게 도움보다 위험이 더 크다고 판단했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;패치는 어떻게 됐나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벤더는 EoS가 아닌 23개 제품군에는 패치를 배포했다. 패치된 펌웨어에서는 &lt;code&gt;system()&lt;/code&gt; 대신 &lt;code&gt;execvpe()&lt;/code&gt; / &lt;code&gt;execvp()&lt;/code&gt;를 쓴다. 이게 핵심이다. &lt;code&gt;system()&lt;/code&gt;은 인자를 통째로 셸(&lt;code&gt;/bin/sh -c&lt;/code&gt;)에 넘기기 때문에 메타문자가 해석되지만, &lt;code&gt;exec*()&lt;/code&gt; 계열은 인자를 배열로 받아서 셸을 거치지 않는다. &lt;code&gt;controlURL&lt;/code&gt; 값이 들어가도 그냥 하나의 문자열 인자로만 취급될 뿐, 셸이 그걸 다시 해석하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;// 패치 후 (대략)
char *argv[] = {&quot;curl&quot;, &quot;-s&quot;, &quot;-X&quot;, &quot;POST&quot;, control_url, &quot;-d&quot;, &quot;...&quot;, NULL};
execvpe(&quot;curl&quot;, argv, envp);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 커맨드 인젝션을 막는 정석이다. 우리집 N2V는 EoS라 이 패치를 못 받고, 벤더가 대놓고 &quot;버리거나 기능을 꺼라&quot;라고 하는 이유가 여기 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 이런 게 생기는지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;system()&lt;/code&gt;, &lt;code&gt;popen()&lt;/code&gt;, &lt;code&gt;eval&lt;/code&gt;, &lt;code&gt;os.system()&lt;/code&gt; &amp;mdash; 이름은 다 다르지만 사용자 입력을 셸 명령 문자열의 일부로 끼워넣는 패턴은 거의 다 위험하다. 막는 방법은 두 가지뿐이다. 셸을 아예 거치지 않는 API(&lt;code&gt;execve()&lt;/code&gt;, &lt;code&gt;posix_spawn()&lt;/code&gt;, Python의 &lt;code&gt;subprocess.run([...])&lt;/code&gt; 리스트 형태)를 쓰거나, 입력을 화이트리스트로 검증하고 이스케이핑하는 것. 후자는 까먹기 쉬워서 항상 전자가 권장된다. 이 CVE는 둘 다 안 한 케이스였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 UPnP 자체가 LAN 내부 기기들이 서로 신뢰한다는 전제로 설계돼 있다는 점도 짚을 만하다. 인증 없이 SSDP 응답으로 임의 &lt;code&gt;controlURL&lt;/code&gt;을 실어보낼 수 있고, 라우터는 그걸 의심 없이 처리한다. LAN이 안전한 공간이라는 가정이 깨지는 순간(악성 IoT 기기, 게스트 와이파이, 손님 노트북 하나만 있어도) UPnP는 곧바로 위험 자산이 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 단계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 CVE를 미리 알고 시작한 건 사실 운이 좋았던 쪽이다. 만약 알려진 취약점이 없었다면 binwalk으로 펌웨어를 까서 직접 함수부터 찾았어야 했을 거고, 실제로 다음 단계로 그 과정을 그대로 재현해보고 있다. 패치 전후 펌웨어를 각각 받아서 &lt;code&gt;libcgi.so&lt;/code&gt;와 &lt;code&gt;libesysapi.so&lt;/code&gt;를 Ghidra로 열어보고, &lt;code&gt;system2()&lt;/code&gt; 호출이 &lt;code&gt;execvpe()&lt;/code&gt;로 어떻게 바뀌었는지 디스어셈블 단위로 비교해보는 게 다음 회차 목표다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CVE-2025-55423 (NVD, 공개 취약점 리포지토리)&lt;/li&gt;
&lt;li&gt;ipTIME 공식 패치 노트&lt;/li&gt;
&lt;li&gt;OWASP &amp;mdash; Command Injection 방어 패턴(&lt;code&gt;exec*&lt;/code&gt; vs &lt;code&gt;system&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같이 읽기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취약점 분석 전에 같은 기기의 펌웨어를 binwalk로 추출한 과정은 &lt;a href=&quot;https://gorani-kr.tistory.com/12&quot;&gt;binwalk으로 ipTIME N2V 펌웨어 까보기&lt;/a&gt;에 정리했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2025-55423&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;NVD CVE-2025-55423&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2025-55423&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CVE.org CVE-2025-55423 레코드&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공개 기록과 방어 관점에서 다시 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취약점 번호와 심각도는 블로그 표현이 아니라 NVD&amp;middot;CVE 레코드의 현재 상태를 기준으로 다시 확인해야 한다. 제품별 영향 버전과 패치 여부도 제조사 펌웨어 페이지에서 보는 편이 안전하다. 아래 자료는 공격 재현 절차가 아니라 공개 기록과 입력 검증 원칙을 확인하기 위한 참고 화면이다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/cbOYFH/dJMcac42iwV/AAAAAAAAAAAAAAAAAAAAACIx3o7TIKGkdD_oE6TvU7c6XfwsAyvLYZ3xZ48S2U3R/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=eNdHfAhZsVJBuVoMCsy9kENS7H0%3D&quot; alt=&quot;NVD CVE-2025-55423 취약점 상세 화면&quot; /&gt;
&lt;figcaption&gt;NVD CVE-2025-55423 취약점 상세 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/nNLCC/dJMcaixtjST/AAAAAAAAAAAAAAAAAAAAAG5JXa6N6afH1v445jT1JlqhMjKVlBxY8GKGBUHGnkPo/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=BBD7c3D7tZSsRVLhOmYk2k%2FQjp0%3D&quot; alt=&quot;CVE.org CVE-2025-55423 공식 레코드&quot; /&gt;
&lt;figcaption&gt;CVE.org CVE-2025-55423 공식 레코드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/ONAcd/dJMcaixtjSV/AAAAAAAAAAAAAAAAAAAAAO-1sIR02-3An0o7AKcGs8DqONpuZCI2k-kae5LKQz8l/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=5CnPmC5qPMVDhOw1GnsGuil1A7I%3D&quot; alt=&quot;OWASP Command Injection 설명&quot; /&gt;
&lt;figcaption&gt;OWASP Command Injection 설명&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bcy4wt/dJMcabkQpkj/AAAAAAAAAAAAAAAAAAAAALj7AuTpr4ck9isrZWMeNsqX_w-411PfyIg-YOVzxd7o/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=Pz9n%2FrzIzbhJbJ2UF4a3ccwY%2FqE%3D&quot; alt=&quot;ipTIME 공식 펌웨어 다운로드 페이지&quot; /&gt;
&lt;figcaption&gt;ipTIME 공식 펌웨어 다운로드 페이지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방어의 핵심은 셸 명령 문자열에 사용자 입력을 이어붙이지 않는 것이다. 외부 프로세스가 꼭 필요하면 인자를 배열로 전달하고, 허용 목록과 타입&amp;middot;길이 검증을 적용한다. 공유기는 관리 인터페이스를 외부에 노출하지 않고 최신 펌웨어를 유지하는 기본 조치도 함께 필요하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2025-55423&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;NVD CVE-2025-55423&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2025-55423&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CVE.org 레코드&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://owasp.org/www-community/attacks/Command_Injection&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;OWASP Command Injection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://iptime.com/iptime/?page_id=126&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ipTIME 다운로드&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>보안</category>
      <category>cve</category>
      <category>IOT</category>
      <category>iptime</category>
      <category>Security</category>
      <category>Upnp</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/14</guid>
      <comments>https://gorani-kr.tistory.com/14#entry14comment</comments>
      <pubDate>Thu, 23 Jul 2026 20:13:56 +0900</pubDate>
    </item>
    <item>
      <title>C로 비트스트림 파싱해서 7가지 타입(char ~ double)으로 출력하기 (시스템 프로그래밍 과제)</title>
      <link>https://gorani-kr.tistory.com/13</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/yo4sB/dJMcacKPzXP/AAAAAAAAAAAAAAAAAAAAAGZ3cqy8vNNqbga_HhtG68N2jlVbRiE-JZdUABqe0jnE/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=7XZyraieInacUSzCulnx21Avw%2BE%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/yo4sB/dJMcacKPzXP/AAAAAAAAAAAAAAAAAAAAAGZ3cqy8vNNqbga_HhtG68N2jlVbRiE-JZdUABqe0jnE/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=7XZyraieInacUSzCulnx21Avw%2BE%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/yo4sB/dJMcacKPzXP/AAAAAAAAAAAAAAAAAAAAAGZ3cqy8vNNqbga_HhtG68N2jlVbRiE-JZdUABqe0jnE/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=7XZyraieInacUSzCulnx21Avw%2BE%3D&quot; alt=&quot;C 비트스트림 파싱과 7가지 타입 출력 대표 이미지&quot; /&gt;
&lt;figcaption&gt;C 비트스트림 파싱과 7가지 타입 출력 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템 프로그래밍 수업 과제로 받은 문제인데, 요약하면 &lt;code&gt;input&lt;/code&gt; 파일에 들어 있는 0/1 비트스트림을 처음부터 끝까지 읽어서 앞에서부터 순서대로 &lt;code&gt;signed char&lt;/code&gt; &amp;rarr; &lt;code&gt;ASCII&lt;/code&gt; &amp;rarr; &lt;code&gt;unsigned char&lt;/code&gt; &amp;rarr; &lt;code&gt;signed int&lt;/code&gt; &amp;rarr; &lt;code&gt;unsigned int&lt;/code&gt; &amp;rarr; &lt;code&gt;float&lt;/code&gt; &amp;rarr; &lt;code&gt;double&lt;/code&gt; 이렇게 7가지 타입으로 해석해서 &lt;code&gt;printf&lt;/code&gt;로 출력하라는 거였다. 조건은 출력에 &lt;code&gt;printf&lt;/code&gt;만 쓸 것(&lt;code&gt;puts&lt;/code&gt; 쓰면 형식이 안 맞아서 감점), 파일 크기를 미리 모르니 동적 할당을 쓸 것, Makefile 포함해서 학번 붙인 파일명으로 제출할 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과제가 진짜 물어보는 건 두 가지다. 메모리에 있는 같은 비트열을 어떻게 다른 타입으로 해석하느냐(type punning), 그리고 C 표준 타입의 크기와 표현 방식을 정확히 아는지.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;첫 번째 함정 &amp;mdash; 입력은 비트가 아니라 문자다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 헷갈렸던 게, 파일에 들어 있는 게 진짜 비트 단위 데이터가 아니라 &lt;code&gt;'0'&lt;/code&gt;과 &lt;code&gt;'1'&lt;/code&gt; 문자가 ASCII로 그대로 적혀 있는 텍스트라는 점이었다. 즉 한 비트가 실제로는 1바이트를 차지한다. 파일에 &lt;code&gt;&quot;01000001&quot;&lt;/code&gt;이라고 8글자가 적혀 있으면, 그게 의미하는 건 &lt;code&gt;0 1 0 0 0 0 0 1&lt;/code&gt; 8비트, 곧 1바이트다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 제일 먼저 할 일이 이 텍스트를 읽어서 진짜 바이트 배열로 압축하는 거였다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;char ch;
unsigned char byte = 0;
int bit_count = 0;

while (fread(&amp;amp;ch, 1, 1, fp) == 1) {
    if (ch != '0' &amp;amp;&amp;amp; ch != '1') continue;  // 개행 등 스킵
    byte = (byte &amp;lt;&amp;lt; 1) | (ch - '0');
    bit_count++;
    if (bit_count == 8) {
        buffer[idx++] = byte;
        byte = 0;
        bit_count = 0;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0/1 문자 8개를 모아서 1바이트로 만드는 거고, 비트 시프트로 한 자리씩 밀면서 OR로 새 비트를 끼워넣는 게 정석이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동적 할당 &amp;mdash; 파일 크기를 모를 때&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 크기를 미리 모르니까 &lt;code&gt;malloc(1024)&lt;/code&gt;처럼 임의로 잡아둘 수가 없다. 두 가지 방법을 놓고 고민했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;fseek&lt;/code&gt;으로 끝까지 갔다가 &lt;code&gt;ftell&lt;/code&gt;로 위치를 확인하는 방법은 깔끔하긴 한데, 텍스트 파일에 &lt;code&gt;\r\n&lt;/code&gt; 같은 게 섞여 있으면 실제 의미 있는 바이트 수랑 어긋날 수 있어서 좀 불안했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;fseek(fp, 0, SEEK_END);
long size = ftell(fp);
fseek(fp, 0, SEEK_SET);
char *buf = malloc(size);
fread(buf, 1, size, fp);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 더 안전한 쪽인 &lt;code&gt;realloc&lt;/code&gt;으로 동적 확장하는 방법을 썼다. 용량이 차면 두 배씩 늘리는 식이라 입력 형태가 좀 변형돼도 유연하게 대응된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;size_t cap = 16, len = 0;
unsigned char *buf = malloc(cap);
while (...읽기...) {
    if (len == cap) {
        cap *= 2;
        buf = realloc(buf, cap);
    }
    buf[len++] = byte;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;진짜 핵심 &amp;mdash; 같은 메모리를 7가지 타입으로 읽기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과제의 본체는 같은 바이트 영역을 그대로 두고 타입만 바꿔서 읽어내는 거다. 처음엔 캐스팅이 더 간단해 보였다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// 이렇게 하면 안 된다
float f = *(float*)p;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 컴파일도 되고 보통은 잘 동작하는데, 문제는 C 표준의 strict aliasing 규칙을 위반한다는 점이다. 컴파일러는 서로 다른 타입의 포인터가 같은 메모리를 가리키지 않는다고 가정하고 최적화를 하기 때문에, 위 코드는 &lt;code&gt;-O2&lt;/code&gt; 이상에서 예측 못 한 결과를 낼 수 있다. C11 표준 6.5절 7항을 찾아보니 객체에 저장된 값은 호환되는 타입의 lvalue로만 접근할 수 있고, 예외는 character 타입(&lt;code&gt;char&lt;/code&gt;, &lt;code&gt;unsigned char&lt;/code&gt;)뿐이라고 명시돼 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 결국 &lt;code&gt;memcpy&lt;/code&gt;를 썼다. 내부적으로 &lt;code&gt;unsigned char *&lt;/code&gt;로 바이트를 복사하기 때문에 strict aliasing 문제를 피할 수 있고, 현대 컴파일러는 작은 &lt;code&gt;memcpy&lt;/code&gt; 호출을 레지스터 이동으로 최적화해줘서 성능 손해도 거의 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 타입의 크기는 &lt;code&gt;signed char&lt;/code&gt;와 &lt;code&gt;unsigned char&lt;/code&gt;가 1바이트, &lt;code&gt;signed int&lt;/code&gt;와 &lt;code&gt;unsigned int&lt;/code&gt;와 &lt;code&gt;float&lt;/code&gt;이 4바이트, &lt;code&gt;double&lt;/code&gt;이 8바이트로 총 23바이트가 필요했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;unsigned char *p = buffer;

signed char sc;
memcpy(&amp;amp;sc, p, sizeof(sc));
printf(&quot;%d\n&quot;, sc);
p += sizeof(sc);

char asc;
memcpy(&amp;amp;asc, p, sizeof(asc));
printf(&quot;%c\n&quot;, asc);
p += sizeof(asc);

unsigned char uc;
memcpy(&amp;amp;uc, p, sizeof(uc));
printf(&quot;%u\n&quot;, uc);
p += sizeof(uc);

signed int si;
memcpy(&amp;amp;si, p, sizeof(si));
printf(&quot;%d\n&quot;, si);
p += sizeof(si);

unsigned int ui;
memcpy(&amp;amp;ui, p, sizeof(ui));
printf(&quot;%u\n&quot;, ui);
p += sizeof(ui);

float f;
memcpy(&amp;amp;f, p, sizeof(f));
printf(&quot;%.6f\n&quot;, f);
p += sizeof(f);

double d;
memcpy(&amp;amp;d, p, sizeof(d));
printf(&quot;%.6lf\n&quot;, d);
p += sizeof(d);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;union으로도 같은 걸 할 수 있긴 하다. C에서는 합법인데(C++에서는 아니다), trap representation 문제가 있어서 &lt;code&gt;memcpy&lt;/code&gt;가 더 안전하다는 쪽으로 정리했다. &lt;code&gt;-fno-strict-aliasing&lt;/code&gt;으로 GCC의 strict aliasing 검사를 아예 꺼버리는 방법도 있고 실제로 Linux 커널이 이렇게 하는데, 학교 과제 수준에서는 굳이 쓸 이유가 없어 보였다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;형식 지정자와 헷갈리는 것들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;printf&lt;/code&gt;만 쓰라고 한 이유가 형식 지정자가 출력의 정밀도를 결정하기 때문이었다. &lt;code&gt;signed char&lt;/code&gt;와 &lt;code&gt;signed int&lt;/code&gt;는 &lt;code&gt;%d&lt;/code&gt;, &lt;code&gt;unsigned char&lt;/code&gt;와 &lt;code&gt;unsigned int&lt;/code&gt;는 &lt;code&gt;%u&lt;/code&gt;, ASCII 문자는 &lt;code&gt;%c&lt;/code&gt;, &lt;code&gt;float&lt;/code&gt;은 &lt;code&gt;%f&lt;/code&gt;, &lt;code&gt;double&lt;/code&gt;은 &lt;code&gt;%lf&lt;/code&gt; 또는 &lt;code&gt;%f&lt;/code&gt;를 쓴다. 채점이 소수점 자릿수까지 본다면 &lt;code&gt;%.4f&lt;/code&gt;처럼 정밀도를 고정해두는 게 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔디안 문제도 신경 써야 했다. &lt;code&gt;signed int&lt;/code&gt;, &lt;code&gt;unsigned int&lt;/code&gt;, &lt;code&gt;float&lt;/code&gt;, &lt;code&gt;double&lt;/code&gt;처럼 여러 바이트를 차지하는 타입은 어떤 바이트가 가장 의미 있는 비트인지가 엔디안으로 결정된다. x86과 ARM 대부분은 little endian이라 가장 낮은 주소에 LSB가 오는데, &lt;code&gt;[0x41, 0x42, 0x43, 0x44]&lt;/code&gt; 4바이트를 int로 읽으면 &lt;code&gt;0x44434241&lt;/code&gt;이 된다. 내 x86 Linux 환경에서 &lt;code&gt;memcpy&lt;/code&gt;로 읽으면 자연스럽게 little endian으로 해석되니까, 입력 데이터가 같은 엔디안 기준으로 만들어졌어야 결과가 맞는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C 표준이 &lt;code&gt;int&lt;/code&gt;를 정확히 32비트라고 못박지 않는다는 것도 새삼 알게 됐다. &lt;code&gt;int &amp;gt;= 16&lt;/code&gt;, &lt;code&gt;long &amp;gt;= 32&lt;/code&gt;만 보장한다. 실제로는 거의 모든 현대 시스템에서 &lt;code&gt;int&lt;/code&gt;가 4바이트지만, 이식성을 생각하면 &lt;code&gt;&amp;lt;stdint.h&amp;gt;&lt;/code&gt;의 &lt;code&gt;int32_t&lt;/code&gt;, &lt;code&gt;uint32_t&lt;/code&gt; 같은 고정 크기 타입을 쓰는 게 안전하다는 얘기도 여기저기서 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;float&lt;/code&gt;과 &lt;code&gt;double&lt;/code&gt;은 IEEE 754 방식이라, 임의의 4바이트를 &lt;code&gt;float&lt;/code&gt;으로 해석하면 경우에 따라 NaN이나 Infinity가 나올 수도 있다. 과제 입력이 의도된 값이라 그럴 일은 없었지만, 디버그 출력을 걸어두면 확인하기 편하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Makefile은 채점 점수가 따로 있어서 아주 간단하게 만들었다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-makefile&quot;&gt;CC = gcc
CFLAGS = -Wall -O2
TARGET = 20233051

$(TARGET): $(TARGET).c
    $(CC) $(CFLAGS) -o $(TARGET) $(TARGET).c

clean:
    rm -f $(TARGET)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탭으로 들여써야 하는 걸 깜빡하고 스페이스로 넣었다가 &lt;code&gt;make&lt;/code&gt;가 &quot;missing separator&quot; 에러를 뱉어서 30분을 날렸다. 사소한 거지만 은근히 자주 걸리는 함정 같다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;C11 표준 6.5절 7항 &amp;mdash; strict aliasing 규칙&lt;/li&gt;
&lt;li&gt;cppreference &amp;mdash; &lt;code&gt;memcpy&lt;/code&gt;, &lt;code&gt;&amp;lt;stdint.h&amp;gt;&lt;/code&gt; 고정 크기 타입&lt;/li&gt;
&lt;li&gt;IEEE 754 &amp;mdash; &lt;code&gt;float&lt;/code&gt;/&lt;code&gt;double&lt;/code&gt; 표현 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;타입 크기와 출력 형식은 따로 검증하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비트스트림을 7가지 타입으로 바꾸는 과제에서 가장 위험한 부분은 &amp;ldquo;내 컴퓨터에서는 이 크기였다&amp;rdquo;를 표준처럼 쓰는 것이다. C의 산술 타입 크기는 구현에 따라 달라질 수 있고, &lt;code&gt;printf&lt;/code&gt; 형식 지정자 불일치는 정의되지 않은 동작으로 이어질 수 있다. &lt;code&gt;sizeof&lt;/code&gt;, 고정폭 정수형, 엔디언, 부동소수점 표현을 따로 확인했다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bAYMGi/dJMcagsQxuI/AAAAAAAAAAAAAAAAAAAAAPgxOhbl3Ne5UEMe0rcDiY5Hcf1P1TLiB5U6GDSrehZa/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=Oh0daq6V6LY1YGoeuCGzxX%2Bkwms%3D&quot; alt=&quot;cppreference C 산술 타입 문서&quot; /&gt;
&lt;figcaption&gt;cppreference C 산술 타입 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/rs5PS/dJMcacKPzXU/AAAAAAAAAAAAAAAAAAAAABhPMFvDONt4KSlsWm_BvXKy4c9oCTdZ1EGhnrYg-e9S/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=OGdjBJ%2Bde0pS84jaei%2FXuRD6RYc%3D&quot; alt=&quot;POSIX printf 함수 명세&quot; /&gt;
&lt;figcaption&gt;POSIX printf 함수 명세&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/A6FQz/dJMcacKPzXV/AAAAAAAAAAAAAAAAAAAAANBO8HYjZ7Qjhb1VOi_AQVO5tifuqdfECpo_d1wcNSiG/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=cWUp%2FnFHDDn7qWpyUugmQPh9uRM%3D&quot; alt=&quot;IEEE 754 부동소수점 표준 페이지&quot; /&gt;
&lt;figcaption&gt;IEEE 754 부동소수점 표준 페이지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/b3Jzbg/dJMcagsQxuM/AAAAAAAAAAAAAAAAAAAAALMJurazQYXyU8wcmPvA5YOis-SnE_LWLvUGR8wGGUBm/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=F%2FORMC6mXaWgtaUk%2B4ASOq82Uh8%3D&quot; alt=&quot;GCC 기타 내장 함수 공식 문서&quot; /&gt;
&lt;figcaption&gt;GCC 기타 내장 함수 공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdint.h&amp;gt;
#include &amp;lt;inttypes.h&amp;gt;

uint32_t value = 0;
printf(&quot;%&quot; PRIu32 &quot;
&quot;, value);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고정폭 타입은 파일 포맷처럼 비트 수가 계약인 경우에 특히 유용하다. 부동소수점은 바이트 배열을 포인터 캐스팅으로 억지 해석하기보다 &lt;code&gt;memcpy&lt;/code&gt;로 옮긴 뒤 대상 형식과 엔디언을 명시하는 편이 안전하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://en.cppreference.com/w/c/language/arithmetic_types.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;C arithmetic types&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pubs.opengroup.org/onlinepubs/9799919799/functions/printf.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;POSIX printf&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://standards.ieee.org/ieee/754/6210/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;IEEE 754&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>학습&amp;middot;과제</category>
      <category>Bitstream</category>
      <category>C</category>
      <category>Memory</category>
      <category>systemprogramming</category>
      <category>TypePunning</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/13</guid>
      <comments>https://gorani-kr.tistory.com/13#entry13comment</comments>
      <pubDate>Thu, 23 Jul 2026 20:07:40 +0900</pubDate>
    </item>
    <item>
      <title>binwalk으로 ipTIME N2V 펌웨어 까보기 (squashfs 추출까지)</title>
      <link>https://gorani-kr.tistory.com/12</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/bfxwQp/dJMcafU32Gd/AAAAAAAAAAAAAAAAAAAAACoJHwf4eW7IN8oZjFIjWCQ84yPVFmV7Kq1s3fMntCo7/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=6MMUL5zTg1yAn%2FJa0H7BdKAS2eA%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/bfxwQp/dJMcafU32Gd/AAAAAAAAAAAAAAAAAAAAACoJHwf4eW7IN8oZjFIjWCQ84yPVFmV7Kq1s3fMntCo7/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=6MMUL5zTg1yAn%2FJa0H7BdKAS2eA%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bfxwQp/dJMcafU32Gd/AAAAAAAAAAAAAAAAAAAAACoJHwf4eW7IN8oZjFIjWCQ84yPVFmV7Kq1s3fMntCo7/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=6MMUL5zTg1yAn%2FJa0H7BdKAS2eA%3D&quot; alt=&quot;ipTIME 펌웨어 binwalk&amp;middot;SquashFS 분석 대표 이미지&quot; /&gt;
&lt;figcaption&gt;ipTIME 펌웨어 binwalk&amp;middot;SquashFS 분석 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CVE-2025-55423 분석을 직접 재현해보기로 하고 제일 먼저 한 일이 펌웨어를 손에 넣는 거였다. 펌웨어가 단일 ELF 바이너리 하나면 편하겠지만, 임베디드 라우터 펌웨어는 거의 다 부트로더, 커널 이미지, 루트 파일시스템(squashfs 등) 세 덩어리가 한 파일에 통째로 들어 있고 각각 압축까지 돼 있다. 그래서 &lt;code&gt;cat&lt;/code&gt;이나 &lt;code&gt;strings&lt;/code&gt;로 열어봐야 알아먹을 수가 없다. 이걸 표준적으로 분석하는 도구가 binwalk이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kali Linux에는 binwalk이 기본으로 들어 있다. 없으면 &lt;code&gt;sudo apt install -y binwalk squashfs-tools&lt;/code&gt;로 깔면 되는데, 여기서 &lt;code&gt;squashfs-tools&lt;/code&gt;를 같이 까는 게 중요하다. 라우터 펌웨어의 루트 파일시스템은 거의 항상 squashfs라, binwalk이 squashfs 시그니처를 찾아도 실제로 풀려면 &lt;code&gt;unsquashfs&lt;/code&gt; 바이너리가 있어야 한다. 이게 없으면 시그니처는 찾아놓고 추출 단계에서 조용히 실패한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ipTIME 공식 사이트에서 N2V용 펌웨어 &lt;code&gt;n2v_ml_12_168.bin&lt;/code&gt;(12.16.8 버전, 패치 안 된 취약 버전)을 받아서 시작했다. &lt;code&gt;file&lt;/code&gt; 명령으로 먼저 확인해봤는데 결과가 그냥 &lt;code&gt;data&lt;/code&gt;로 나왔다. 이건 표준 시그니처에 안 걸린다는 뜻일 뿐이지 내용이 없다는 얘기가 아니다. 라우터 펌웨어는 벤더마다 자체 헤더로 시작하는 경우가 많아서 &lt;code&gt;file&lt;/code&gt; 명령은 여기서 거의 도움이 안 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;binwalk으로 시그니처 스캔&lt;/h2&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;binwalk n2v_ml_12_168.bin&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;binwalk은 파일을 처음부터 끝까지 훑으면서 알려진 시그니처(매직 넘버)를 찾는다. uImage 헤더, gzip/lzma 압축 데이터, squashfs 파일시스템, ELF 헤더 같은 것들. 출력이 대략 이렇게 나왔다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;DECIMAL    HEXADECIMAL    DESCRIPTION
-----------------------------------------------------------
0          0x0            &amp;lt;벤더 헤더&amp;gt;
112        0x70           LZMA compressed data, ...
1310864    0x140090       Squashfs filesystem, little endian,
                          version 4.0, compression: lzma, ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 보는 순간 펌웨어 구조가 눈에 그려졌다. 처음에 헤더, 그다음 LZMA로 압축된 커널 이미지, 그리고 1.31MB 오프셋에 squashfs 파일시스템.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;dd&lt;/code&gt;로 오프셋을 직접 잘라낼 수도 있지만 &lt;code&gt;-e&lt;/code&gt; 옵션으로 자동 추출하는 게 훨씬 편하다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;binwalk -e n2v_ml_12_168.bin&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 디렉토리에 &lt;code&gt;_n2v_ml_12_168.bin.extracted/&lt;/code&gt; 폴더가 생기고, 그 안에 &lt;code&gt;squashfs-root/&lt;/code&gt;라는 폴더가 나온다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;cd _n2v_ml_12_168.bin.extracted/squashfs-root
ls
# bin  dev  etc  home  lib  mnt  proc  root  sbin  sys  tmp  usr  var  www&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라우터의 진짜 루트 디렉토리 구조가 그대로 드러나는 걸 보고 좀 신기했다. 이 안의 모든 파일이 실제로 그 라우터에서 돌아가는 파일 그대로다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어디부터 봐야 하나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분석할 때 첫 번째로 보는 자리가 대충 정해져 있다. &lt;code&gt;/etc/passwd&lt;/code&gt;와 &lt;code&gt;/etc/shadow&lt;/code&gt;에는 종종 하드코딩된 계정이나 백도어 계정의 해시가 그대로 박혀 있다. 우리 N2V는 여기서 특별한 게 안 나왔다. &lt;code&gt;/www/&lt;/code&gt;에는 라우터 관리 페이지의 HTML/CGI 코드가 전부 들어 있어서, cgi-bin 안의 바이너리를 보면서 인증 우회나 command injection 가능성을 살필 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 진짜 필요했던 건 &lt;code&gt;/lib/&lt;/code&gt;였다. CVE-2025-55423의 취약 함수 &lt;code&gt;upnp_relay()&lt;/code&gt;가 있다고 알려진 &lt;code&gt;libcgi.so&lt;/code&gt;가 여기 있다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;file lib/libcgi.so
# ELF 32-bit LSB shared object, MIPS, MIPS32 rel2 version 1 (SYSV), ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MIPS 32비트 빌드. ipTIME 공유기 대부분이 MIPS라 예상했던 대로다. 이건 호스트(x86)에서 그냥 실행은 못 하고, Ghidra/IDA로 정적 분석하거나 &lt;code&gt;qemu-mipsel-static&lt;/code&gt;으로 사용자 모드 에뮬레이션을 걸어야 한다. 이번 글은 추출까지가 목표라 여기서 끊고, &lt;code&gt;libcgi.so&lt;/code&gt;를 Ghidra로 디컴파일한 내용은 다음 회차에 정리할 생각이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;함정 두 가지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;binwalk을 돌렸는데 squashfs-root가 안 생기는 경우가 있었다. 원인은 두 가지였는데, 하나는 &lt;code&gt;squashfs-tools&lt;/code&gt;가 안 깔려서였다. binwalk이 squashfs를 발견은 했지만 추출 단계에서 &lt;code&gt;unsquashfs&lt;/code&gt;를 못 찾아 실패하는 거였고, 출력을 잘 보면 &lt;code&gt;WARNING: extractor 'unsquashfs' not found&lt;/code&gt;가 뜬다. 다른 하나는 어떤 펌웨어는 한 겹 더 안에 압축이 또 있는 경우인데, 이럴 땐 &lt;code&gt;-e&lt;/code&gt; 대신 &lt;code&gt;-Me&lt;/code&gt;(matryoshka 모드)로 재귀 추출을 걸면 된다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;binwalk -Me n2v_ml_12_168.bin&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;binwalk이 실제로 하는 일&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;binwalk은 본질적으로 시그니처 스캐너다. 파일시스템&amp;middot;압축 포맷&amp;middot;바이너리 헤더의 매직 넘버 데이터베이스를 갖고 있고, 입력 파일을 1바이트씩 밀면서 패턴이 나오는지 확인한다. 발견되면 오프셋을 출력하고, &lt;code&gt;-e&lt;/code&gt; 옵션이면 그 시그니처에 맞는 추출 도구를 호출해서 풀어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연히 한계도 있다. 시그니처 기반이라 암호화된 펌웨어는 그냥 랜덤 데이터로 보여서 못 푼다. 펌웨어가 암호화됐는지 확인하려면 &lt;code&gt;binwalk -E&lt;/code&gt;로 엔트로피 분석을 걸어보면 된다. 전체 구간이 0.97~1.0 근처로 평탄하면 암호화나 강한 압축 신호고, 정상 펌웨어는 헤더&amp;middot;코드 영역 엔트로피가 낮고 압축 영역만 높아서 그래프가 들쭉날쭉하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 SOHO 라우터는 부트로더로 U-Boot, 커널로 Linux(MIPS 또는 ARM), 루트 파일시스템으로 SquashFS(가끔 JFFS2/UBIFS), 압축으로 LZMA/gzip/xz를 쓰는 구성이다. OpenWRT 같은 오픈 펌웨어로 갈아탈 수 있는 라우터가 많은 것도 결국 이 구조가 표준 임베디드 Linux 위에서 돌아가기 때문이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;binwalk이 명령어 한 줄로 라우터의 내장을 통째로 까볼 수 있게 해준다는 게 처음엔 좀 비현실적으로 느껴졌다. 동시에 이 정도로 쉽게 열리는 게 라우터 보안의 현주소라는 것도 같이 느꼈다. 펌웨어가 평문으로 배포되고, 시그니처도 표준이고, 추출 도구도 다 무료 공개돼 있으니까. 벤더가 못 막은 건지 아니면 분석 가능하게 일부러 둔 건지는 모르겠지만, 어느 쪽이든 분석하는 입장에선 나쁘지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 단계는 Ghidra 환경 잡고 &lt;code&gt;libcgi.so&lt;/code&gt; 디컴파일하는 거다. 분석 대상 펌웨어는 본인 소유 ipTIME N2V 공유기용 공식 펌웨어고, 분석은 격리된 Kali Linux VM에서만 진행하고 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;binwalk 공식 깃허브 (ReFirmLabs/binwalk)&lt;/li&gt;
&lt;li&gt;OWASP FSTM Stage 4 &amp;mdash; Extracting the Filesystem&lt;/li&gt;
&lt;li&gt;ArchWiki &amp;mdash; squashfs-tools&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 분석&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;펌웨어를 추출한 뒤 확인한 실제 취약점은 &lt;a href=&quot;https://gorani-kr.tistory.com/14&quot;&gt;ipTIME N2V CVE-2025-55423 Command Injection 분석&lt;/a&gt;에서 이어진다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ReFirmLabs/binwalk&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ReFirmLabs binwalk 저장소&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.kernel.org/doc/html/latest/filesystems/squashfs.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Linux SquashFS 문서&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;도구가 나눈 역할&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;binwalk이 서명과 오프셋을 찾아주는 단계, SquashFS 도구가 파일시스템을 실제로 푸는 단계, &lt;code&gt;hexdump&lt;/code&gt;로 헤더와 경계를 직접 확인하는 단계를 나눠두면 자동 추출 실패 때 원인을 찾기 쉽다. 자동화 결과만 믿지 않고 오프셋과 파일 형식을 한 번 더 확인하는 것이 펌웨어 분석의 재현성을 높였다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/RD7Qo/dJMcagsQxuw/AAAAAAAAAAAAAAAAAAAAALmeQwVcFk3tbINwMVPHa2pikG9e2BaEaXUX4d8WiMje/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=ucXR5FQQs6N1ThAi%2BypKggBfUss%3D&quot; alt=&quot;ReFirmLabs binwalk 공식 GitHub 저장소&quot; /&gt;
&lt;figcaption&gt;ReFirmLabs binwalk 공식 GitHub 저장소&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/E5Y0Y/dJMcahFfHEX/AAAAAAAAAAAAAAAAAAAAAOHE6hOMf3LRZXxP4pdnFM0YCaekQH-Oi_XXn6SXhqS2/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=jRhcZFpB53vI2UPVy5uobunTNOc%3D&quot; alt=&quot;SquashFS 공식 도구 저장소&quot; /&gt;
&lt;figcaption&gt;SquashFS 공식 도구 저장소&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/IboRq/dJMcagsQxuB/AAAAAAAAAAAAAAAAAAAAAEkZulSWHhh7hRnkVzLYLKXCarmfxHPbR5Zmj_NdTAkt/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=%2Be8VtHXH3suFJR2JaYJgv9Mo7fo%3D&quot; alt=&quot;Linux 커널 SquashFS 문서&quot; /&gt;
&lt;figcaption&gt;Linux 커널 SquashFS 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bVFSbU/dJMcagsQxuE/AAAAAAAAAAAAAAAAAAAAAJqZzSpVuDVFVk8zaFkqk9Fgm7qSYZwApmdz6VC-99b1/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=nmMqG0NCzwnzbC%2BdZ1826gTXuLk%3D&quot; alt=&quot;Linux hexdump 매뉴얼&quot; /&gt;
&lt;figcaption&gt;Linux hexdump 매뉴얼&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추출 결과 체크리스트&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원본 펌웨어 해시와 파일 크기를 기록한다.&lt;/li&gt;
&lt;li&gt;binwalk이 표시한 오프셋과 파일시스템 형식을 남긴다.&lt;/li&gt;
&lt;li&gt;추출 디렉터리의 심볼릭 링크가 작업 경로 밖을 가리키지 않는지 본다.&lt;/li&gt;
&lt;li&gt;분석은 네트워크와 분리한 컨테이너&amp;middot;VM에서 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ReFirmLabs/binwalk&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;binwalk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/plougher/squashfs-tools&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;squashfs-tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/filesystems/squashfs.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Linux SquashFS 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man1/hexdump.1.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;hexdump(1)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>보안</category>
      <category>binwalk</category>
      <category>Firmware</category>
      <category>iptime</category>
      <category>Kali</category>
      <category>Security</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/12</guid>
      <comments>https://gorani-kr.tistory.com/12#entry12comment</comments>
      <pubDate>Thu, 23 Jul 2026 20:07:30 +0900</pubDate>
    </item>
    <item>
      <title>iTerm2에서 SSH로 Linux 접속할 때 한글 깨짐 + zsh/Powerline 테마 잡기</title>
      <link>https://gorani-kr.tistory.com/10</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/PlK3W/dJMcafAI7UW/AAAAAAAAAAAAAAAAAAAAALKAHgSiobFd1oZwDrCNvQrlP_QqYsqG0ycqQGxO-kWE/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=GKrUnIFScsJ%2BFcUHTjKNkHX8270%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/PlK3W/dJMcafAI7UW/AAAAAAAAAAAAAAAAAAAAALKAHgSiobFd1oZwDrCNvQrlP_QqYsqG0ycqQGxO-kWE/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=GKrUnIFScsJ%2BFcUHTjKNkHX8270%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/PlK3W/dJMcafAI7UW/AAAAAAAAAAAAAAAAAAAAALKAHgSiobFd1oZwDrCNvQrlP_QqYsqG0ycqQGxO-kWE/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=GKrUnIFScsJ%2BFcUHTjKNkHX8270%3D&quot; alt=&quot;iTerm2 SSH 한글 로케일과 zsh Powerline 설정 대표 이미지&quot; /&gt;
&lt;figcaption&gt;iTerm2 SSH 한글 로케일과 zsh Powerline 설정 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;집에 새로 맞춘 우분투 PC를 메인 작업 환경으로 쓰려고 하는데 노트북은 맥북 에어다. 책상에서 같은 모니터를 쓰면 좋겠지만 자리를 자주 옮기다 보니, 맥북에서 iTerm2로 우분투에 SSH 접속하는 게 제일 편했다. &lt;code&gt;ssh dohyun@&amp;lt;리눅스IP&amp;gt;&lt;/code&gt;로 접속까지는 잘 됐는데, 두 가지가 계속 거슬렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나는 우분투 쪽에서 한글이 들어간 파일명이나 메시지가 표시될 때 폭이 너무 넓게 잡혀서 줄이 밀려 보이는 문제였다. &lt;code&gt;한글파일.txt&lt;/code&gt;가 &lt;code&gt;한 글 파 일 . t x t&lt;/code&gt;처럼 사이가 벌어진 느낌으로 보였다. 다른 하나는 우분투 기본 bash 프롬프트가 너무 단조로워서 &lt;code&gt;pwd&lt;/code&gt;도 매번 쳐야 하는 게 불편했다는 거였다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한글 폭이 이상하게 렌더링되는 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 우분투 쪽 로케일이 잘못된 줄 알았다. &lt;code&gt;locale&lt;/code&gt;을 쳐보니 &lt;code&gt;ko_KR.UTF-8&lt;/code&gt;로 잘 잡혀 있었고, 폰트 문제도 아니었다. 한글 자체는 잘 보였고, 다만 한 글자가 차지하는 셀 폭이 두 칸이 아니라 그 이상처럼 렌더링됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;찾아보니 iTerm2의 &quot;ambiguous-width&quot; 설정 때문이었다. 유니코드 명세상 어떤 문자(그리스 문자나 일부 기호)는 폭이 모호하게 정의돼 있어서, 터미널이 그걸 1칸으로 그릴지 2칸으로 그릴지 선택할 수 있다. iTerm2는 기본적으로 이걸 double-width로 그리게 돼 있는데, CJK 환경에서는 이게 한글과 충돌해서 폭 계산이 꼬이는 것 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iTerm2 &amp;rarr; Settings &amp;rarr; Profiles &amp;rarr; Text에서 &quot;Treat ambiguous-width characters as double-width&quot; 체크를 해제하니 바로 해결됐다. 같은 메뉴 옆에 있는 &quot;Use Unicode version 9+ widths&quot;는 켜두는 게 좋다. 이모지나 최신 유니코드 글리프의 폭 계산을 더 정확하게 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 하나 더 알아둔 게 있다. macOS는 한글 파일명을 NFD(분리형, ㅎ+ㅏ+ㄴ+ㄱ+ㅡ+ㄹ)로 저장하고 Linux는 NFC(조합형, 한글)를 쓰는데, 이 때문에 어떤 상황에서는 한글이 자모 단위로 쪼개져 보이는 일이 생긴다고 한다. iTerm2의 &quot;Unicode normalization form&quot; 옵션은 기본값(None)으로 두는 게 무난했다. 굳이 NFC로 강제했다가 다른 문제가 생길 수 있어서 건드리지 않았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;bash에서 zsh로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 bash가 너무 밋밋해서 zsh로 갈아탔다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt install -y zsh
chsh -s $(which zsh)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;chsh&lt;/code&gt;로 기본 셸을 바꾸고 로그아웃 후 다시 로그인했다. 이어서 oh-my-zsh를 설치했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh -c &quot;$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치가 끝나면 &lt;code&gt;~/.zshrc&lt;/code&gt;가 자동으로 생기고, 여기서 &lt;code&gt;ZSH_THEME&lt;/code&gt; 항목을 바꾸면 테마가 바뀐다. 처음엔 agnoster 테마를 골랐다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ZSH_THEME=&quot;agnoster&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;source ~/.zshrc&lt;/code&gt; 하고 새 터미널을 띄웠는데 화살표 모양 글리프가 두부(□)로 떴다. 여기서 30분을 날렸다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Powerline 폰트 함정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;agnoster 테마는 Powerline 글리프(화살표 모양)를 쓰는데, 이건 Powerline 패치가 들어간 폰트가 있어야 그려진다. 일반 폰트에는 그 글리프가 없어서 두부로 표시된 거였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 쉬운 해결책은 Nerd Fonts(Powerline과 아이콘 글리프를 다 박아넣은 패치판) 폰트를 쓰는 거였다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir -p ~/.local/share/fonts
cd ~/.local/share/fonts
wget https://github.com/ryanoasis/nerd-fonts/releases/download/v3.1.1/Meslo.zip
unzip Meslo.zip
fc-cache -fv&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이걸 우분투에 깔아놓고 나서야 깨달았다. SSH 클라이언트는 맥북의 iTerm2니까, 폰트는 우분투가 아니라 iTerm2 쪽에 깔아야 했다. 터미널에서 글자를 실제로 그리는 건 클라이언트 쪽이다. 맥에서는 이렇게 설치했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;brew tap homebrew/cask-fonts
brew install --cask font-meslo-lg-nerd-font&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iTerm2 &amp;rarr; Settings &amp;rarr; Profiles &amp;rarr; Text &amp;rarr; Font에서 MesloLGS NF로 폰트를 바꾸니 화살표가 살아났다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;색감과 프롬프트 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;색감도 바꾸고 싶어서 Dracula 컬러 스킴을 깔았다. iTerm2용 컬러 프리셋은 draculatheme.com/iterm에서 받을 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/dracula/iterm.git ~/dracula-iterm&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iTerm2 &amp;rarr; Settings &amp;rarr; Profiles &amp;rarr; Colors &amp;rarr; Color Presets &amp;rarr; Import에서 &lt;code&gt;Dracula.itermcolors&lt;/code&gt;를 선택하고, 다시 Color Presets에서 Dracula를 적용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 하나 더 손봤다. agnoster 테마는 기본적으로 &lt;code&gt;user@hostname&lt;/code&gt;을 항상 표시하는데, 노트북에서 SSH로 들어가면 이게 길어져서 가독성이 떨어졌다. &lt;code&gt;~/.zshrc&lt;/code&gt;에 아래 한 줄을 추가하면 해결된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;DEFAULT_USER=&quot;dohyun&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;agnoster는 현재 사용자가 DEFAULT_USER와 같으면 user@host 부분을 그리지 않는다. SSH로 접속한 경우엔 자동으로 다시 표시되기 때문에 원격/로컬 구분도 알아서 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;돌아보면 원격 터미널의 한글 폭 문제는 거의 다 클라이언트 쪽 ambiguous-width 설정 때문이었고, Powerline 화살표가 두부로 뜨는 건 100% 폰트 문제였다. 둘 다 iTerm2 설정 몇 번 만지는 걸로 끝났다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고한 자료:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;iTerm2 공식 FAQ &amp;mdash; CJK characters and double-width 설정&lt;/li&gt;
&lt;li&gt;iTerm2 Documentation &amp;mdash; Text Profile Preferences&lt;/li&gt;
&lt;li&gt;Nerd Fonts 프로젝트 (ryanoasis/nerd-fonts)&lt;/li&gt;
&lt;li&gt;oh-my-zsh GitHub&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문자 인코딩과 글꼴 문제를 따로 확인하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH에서 한글이 깨지는 문제와 Powerline 기호가 네모로 보이는 문제는 겉보기만 비슷하다. 전자는 로컬&amp;middot;원격의 UTF-8 로케일과 터미널 환경변수를 보고, 후자는 실제로 선택한 글꼴에 필요한 glyph가 들어 있는지 봐야 한다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/locQ5/dJMcaasGehV/AAAAAAAAAAAAAAAAAAAAAPxUb2PYntnRtIMdWxiulcUjgVb_H-qPQkTqxLDnfRY6/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=DhdMbrKTtHbdxKRd15jDVf3hvZk%3D&quot; alt=&quot;iTerm2 공식 문서&quot; /&gt;
&lt;figcaption&gt;iTerm2 공식 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/nnu2w/dJMcafAI7UY/AAAAAAAAAAAAAAAAAAAAANJ1g4PnwSdqsAuuJpiUd8ofElTbrutGpeipprPdMErj/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=CQruP6aeWjcHH1QWnkI7OfueMvk%3D&quot; alt=&quot;OpenSSH 공식 매뉴얼 목록&quot; /&gt;
&lt;figcaption&gt;OpenSSH 공식 매뉴얼 목록&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/KTvgK/dJMb99NXjka/AAAAAAAAAAAAAAAAAAAAAPTg4sg4Rchd4InFVT_eA603hg3QIdNTGNyW9rJn8fEa/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=6CoKuk30Jn7GNouniJXuQzGdORs%3D&quot; alt=&quot;Z shell 공식 사이트&quot; /&gt;
&lt;figcaption&gt;Z shell 공식 사이트&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bxoVop/dJMcaasGeh0/AAAAAAAAAAAAAAAAAAAAAHpe0Kx2OpmpZqfdmQFPhkGnv1va9GGswcBHvHBMEavU/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=6zYcKjMb5FaFfK%2F3z613UZPx6sI%3D&quot; alt=&quot;Powerline fonts 공식 GitHub 저장소&quot; /&gt;
&lt;figcaption&gt;Powerline fonts 공식 GitHub 저장소&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;pre&gt;&lt;code&gt;# 로컬과 원격 양쪽에서 비교
locale
printf '%s
' &quot;$LANG&quot; &quot;$LC_ALL&quot;

# 실제 셸 확인
printf '%s
' &quot;$SHELL&quot;
ps -p $$ -o comm=&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로케일이 정상인데 프롬프트 아이콘만 깨지면 인코딩을 다시 설치할 문제가 아니다. iTerm2 프로필의 Normal/Non-ASCII Font와 zsh 테마가 요구하는 글꼴을 맞춘다. 반대로 파일명과 명령 출력의 한글까지 깨지면 원격 서버 로케일부터 확인한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://iterm2.com/documentation.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;iTerm2 Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.openssh.com/manual.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;OpenSSH Manuals&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zsh.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Zsh&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/powerline/fonts&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Powerline fonts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>환경설정</category>
      <category>iterm2</category>
      <category>linux</category>
      <category>macos</category>
      <category>ssh</category>
      <category>zsh</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/10</guid>
      <comments>https://gorani-kr.tistory.com/10#entry10comment</comments>
      <pubDate>Wed, 22 Jul 2026 15:45:59 +0900</pubDate>
    </item>
    <item>
      <title>새 PC에 Windows + Ubuntu 듀얼부팅 처음 깔아보기 (Rufus, BIOS, 미할당 256GB)</title>
      <link>https://gorani-kr.tistory.com/9</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/URRtG/dJMcaf8tiH1/AAAAAAAAAAAAAAAAAAAAAMZd8B4ia7LSSoDjPlGB78GkzyO1ZW2lwJQpA_wClfQh/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=9vhf5pRe8ZD5Byh2KqvihqzuiZ4%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/URRtG/dJMcaf8tiH1/AAAAAAAAAAAAAAAAAAAAAMZd8B4ia7LSSoDjPlGB78GkzyO1ZW2lwJQpA_wClfQh/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=9vhf5pRe8ZD5Byh2KqvihqzuiZ4%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/URRtG/dJMcaf8tiH1/AAAAAAAAAAAAAAAAAAAAAMZd8B4ia7LSSoDjPlGB78GkzyO1ZW2lwJQpA_wClfQh/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=9vhf5pRe8ZD5Byh2KqvihqzuiZ4%3D&quot; alt=&quot;Windows와 Ubuntu 듀얼부팅 설치 대표 이미지&quot; /&gt;
&lt;figcaption&gt;Windows와 Ubuntu 듀얼부팅 설치 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PC를 새로 맞추면서 처음부터 듀얼부팅을 염두에 뒀다. 윈도우를 설치할 때 일부러 SSD에서 256GB를 미할당으로 남겨뒀고, 보안 공부랑 시스템 프로그래밍 과제는 Ubuntu에서 돌릴 생각이었다. 가상머신은 이미 써봤지만 호스트 자원을 다 쓸 수 있는 네이티브 환경이 필요한 작업도 있어서 듀얼부팅으로 결정했다. 부팅 USB는 16GB짜리를 새로 샀다. 요즘 우분투 ISO가 6GB 정도라 4GB로는 안 들어간다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Rufus로 부팅 USB 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ubuntu 24.04 LTS ISO를 공식 사이트에서 받고 Rufus를 켰다. 여기서 첫 번째 함정이 있다. 파티션 스킴을 GPT, 타깃 시스템을 UEFI로 맞춰야 한다는 점이다. 요즘 메인보드는 거의 다 UEFI 모드라, MBR/Legacy로 만들면 부팅 자체가 안 된다. 내 시스템이 UEFI인지는 윈도우에서 Win+R로 msinfo32를 실행해서 &quot;BIOS 모드&quot; 항목을 보면 확인할 수 있다. UEFI라고 뜨면 GPT로 만들면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rufus 세팅은 이랬다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Device: USB 드라이브 선택&lt;/li&gt;
&lt;li&gt;Boot selection: Ubuntu ISO 선택&lt;/li&gt;
&lt;li&gt;Partition scheme: GPT&lt;/li&gt;
&lt;li&gt;Target system: UEFI (non CSM)&lt;/li&gt;
&lt;li&gt;나머지는 기본값&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;START를 누르면 &quot;ISOHybrid image detected&quot;라는 창이 뜨는데, 여기서 &quot;Write in ISO Image mode&quot;를 선택했다. DD 모드로 쓰면 USB 파티션 테이블을 통째로 덮어써서 일부 환경에서 인식이 이상해질 수 있다는데, ISO 모드가 더 안전하다고 해서 그쪽으로 골랐다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;BIOS 진입과 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 쓰는 B850I 보드(ASRock 계열)는 부팅할 때 Del 키를 연타하면 BIOS로 들어간다. 보드마다 다르지만 보통 F2, F12, Del, Esc 중 하나다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BIOS에서는 부팅 모드와 순서를 먼저 확인했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;UEFI 모드 유지 &amp;mdash; CSM/Legacy 모드는 끄고 UEFI로 맞췄다. 어떤 보드는 CSM 메뉴가 아예 안 보이는데, 이미 비활성 상태라면 그대로 두면 된다.&lt;/li&gt;
&lt;li&gt;Fast Boot 끄기 &amp;mdash; USB 부팅 메뉴로 진입할 시간을 확보하고, 듀얼부팅 환경에서 생길 수 있는 문제를 줄이기 위해 껐다.&lt;/li&gt;
&lt;li&gt;Boot Order에서 설치 USB를 먼저 선택하기.&lt;/li&gt;
&lt;li&gt;Secure Boot는 일단 유지 &amp;mdash; Ubuntu 24.04 공식 이미지와 커널은 Secure Boot를 지원한다. 내 환경에서 서명되지 않은 드라이버나 커널 모듈 때문에 설치가 막힐 때만 임시로 끄고, 설치 후 다시 켜는 편이 안전하다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSM 메뉴는 한참 못 찾았는데, ASRock B850I는 Advanced &amp;rarr; Boot 메뉴를 깊이 들어가야 보이거나 아예 없는 경우도 있는 것 같다. 안 보이면 그냥 넘어가도 됐다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Ubuntu 설치 마법사&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;USB로 부팅하면 GRUB 메뉴가 뜨고 &quot;Try or Install Ubuntu&quot;를 선택하면 된다. 언어, 키보드, Wi-Fi(유선이면 자동 인식) 화면은 다 넘기고, 핵심은 &quot;Installation type&quot; 화면이다. 여기서 보통 옵션이 세 개 뜬다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Install Ubuntu alongside Windows Boot Manager &amp;mdash; 가장 쉬운 방법. 미할당 공간을 자동으로 잡아준다.&lt;/li&gt;
&lt;li&gt;Erase disk and install Ubuntu &amp;mdash; 절대 누르면 안 된다. 윈도우가 통째로 날아간다.&lt;/li&gt;
&lt;li&gt;Something else &amp;mdash; 수동으로 파티션을 잡는 방법.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 옵션이 뜰 줄 알았는데 안 떴다. 미할당 공간이 있는데도 알아서 인식을 못 하는 경우가 종종 있다고 한다. 그래서 Something else로 직접 잡았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션 목록에서 free space(unallocated) 256GB짜리를 찾아 + 버튼으로 파티션을 만들었다. 구성은 이렇게 잡았다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/ (루트 파티션): 약 220GB, ext4, mount point &lt;code&gt;/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;swap: 약 16GB (RAM과 비슷하게 잡았는데, 최대 절전모드를 안 쓸 거면 4~8GB로도 충분하다)&lt;/li&gt;
&lt;li&gt;/home: 따로 안 만들었다. 어차피 한 디스크에서 쓰니 분리할 이유가 없었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EFI 파티션은 윈도우가 이미 만들어둔 게 있어서 별도로 만들지 않았다. UEFI 시스템에서는 ESP(EFI System Partition)가 디스크에 하나만 있으면 되고, 윈도우 설치 때 이미 잡혀 있으니 설치 마법사가 자동으로 인식해서 GRUB을 거기에 박아준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Device for boot loader installation&quot;은 파티션이 아니라 디스크 전체(예: &lt;code&gt;/dev/nvme0n1&lt;/code&gt;, 숫자 없음)로 지정해야 한다는 것도 기억해둘 만하다. 파티션(&lt;code&gt;/dev/nvme0n1p3&lt;/code&gt; 같은)으로 지정하면 부트로더가 망가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Install Now를 누르고 시간대, 사용자명, 비밀번호를 설정하니 10분쯤 걸려서 끝났다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;부팅하면 GRUB에서 OS를 고른다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치가 끝나고 USB를 빼고 재부팅하면 이런 메뉴가 뜬다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Ubuntu
Advanced options for Ubuntu
Windows Boot Manager&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방향키로 고르고 엔터를 누르면 된다. 기본 선택은 Ubuntu고, 10초 안에 아무것도 안 누르면 그게 자동으로 부팅된다. 기본을 윈도우로 바꾸고 싶으면 Ubuntu 안에서 이렇게 하면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo nano /etc/default/grub
# GRUB_DEFAULT=0 &amp;rarr; GRUB_DEFAULT=2 (메뉴 순서대로 0부터)
sudo update-grub&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BIOS 만지는 시간까지 다 합쳐서 한 시간쯤 걸렸다. 돌이켜보면 Rufus는 GPT + UEFI + ISO 모드로 굽고, Erase disk를 누르지 않고 Something else로 미할당 공간을 직접 지정하는 게 핵심이었다. Secure Boot는 무조건 끄는 설정이 아니라 설치 이미지와 드라이버 상황을 보고 판단해야 한다. 미할당 공간만 미리 남겨뒀다면 나머지는 크게 어렵지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고한 자료:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Ubuntu 공식 &amp;mdash; Install Ubuntu desktop (UEFI/Secure Boot 항목)&lt;/li&gt;
&lt;li&gt;Rufus FAQ &amp;mdash; ISOHybrid image와 ISO mode vs DD mode&lt;/li&gt;
&lt;li&gt;ArchWiki &amp;mdash; GRUB 부트로더 설치 옵션&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설치 전에 다시 볼 네 가지 화면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;듀얼부팅은 USB를 만드는 단계보다 디스크와 부팅 방식을 잘못 판단했을 때 복구 비용이 크다. Windows 디스크 관리에서 실제 미할당 공간을 확인하고, Rufus의 파티션 방식과 펌웨어의 UEFI 모드를 맞춘 뒤 Ubuntu 설치 화면에서 대상 파티션을 다시 확인하는 순서를 기준으로 잡았다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bOgfOS/dJMcaf8tiIh/AAAAAAAAAAAAAAAAAAAAALfQ8KB8OegYc2bfyMuAg7aPOKuqZaxQdi5RdArATYvj/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=P7547Wu1cgmWp1k1GH98WSNSAQg%3D&quot; alt=&quot;Ubuntu 공식 데스크톱 설치 튜토리얼&quot; /&gt;
&lt;figcaption&gt;Ubuntu 공식 데스크톱 설치 튜토리얼&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/le4oy/dJMcafAI7UO/AAAAAAAAAAAAAAAAAAAAANz-Q11nUFfsq3pbZPM9xDdUNM0wvixclqXOqwdAgd6H/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=Fw71yX4%2Fe2Gxpj5AAil5vwdYpWE%3D&quot; alt=&quot;Rufus 공식 사이트 &amp;mdash; 부팅 USB 제작 도구&quot; /&gt;
&lt;figcaption&gt;Rufus 공식 사이트 &amp;mdash; 부팅 USB 제작 도구&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/7YQZa/dJMcaf8tiIQ/AAAAAAAAAAAAAAAAAAAAADVlfBCmYvzDiIlyxpyUVesynS9bRjCqDz12j8o8JAFw/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=inErsHmbIIyczC1JtO3S4PUKM6I%3D&quot; alt=&quot;Microsoft Learn의 Windows 디스크 관리 개요&quot; /&gt;
&lt;figcaption&gt;Microsoft Learn의 Windows 디스크 관리 개요&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/AR2fm/dJMcafAI7UQ/AAAAAAAAAAAAAAAAAAAAAOM7a34UATuahFXe98rTzHY3xY_qmoeFuOBc70TfjUym/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=o0DMf%2BATVYCZHwTK8VcK7r3qVsA%3D&quot; alt=&quot;UEFI Forum 공식 사양 페이지&quot; /&gt;
&lt;figcaption&gt;UEFI Forum 공식 사양 페이지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 데이터가 있는 디스크라면 설치 전 백업이 먼저다. BitLocker가 켜져 있으면 복구 키를 확보하고, Windows 빠른 시작과 Secure Boot 지원 여부도 배포판&amp;middot;드라이버 상황에 맞춰 확인한다. &amp;ldquo;미할당 256GB가 보인다&amp;rdquo;는 것과 &amp;ldquo;그 공간이 내가 설치하려는 물리 디스크에 있다&amp;rdquo;는 것은 다른 확인이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://ubuntu.com/tutorials/install-ubuntu-desktop&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Ubuntu Desktop 설치&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://rufus.ie/en/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Rufus&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/windows-server/storage/disk-management/overview-of-disk-management&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Windows Disk Management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://uefi.org/specifications&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;UEFI Specifications&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>환경설정</category>
      <category>bios</category>
      <category>DualBoot</category>
      <category>linux</category>
      <category>rufus</category>
      <category>ubuntu</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/9</guid>
      <comments>https://gorani-kr.tistory.com/9#entry9comment</comments>
      <pubDate>Wed, 22 Jul 2026 15:44:19 +0900</pubDate>
    </item>
    <item>
      <title>Kali Linux에 fcitx5로 한글 입력기 잡기 + 한영 전환 키 매핑</title>
      <link>https://gorani-kr.tistory.com/8</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/btAMai/dJMcacD4eIw/AAAAAAAAAAAAAAAAAAAAAPgNo0DBmg9RArG4kZ9lO77-ZYaYO7r6yZi9ib2mQ-0D/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=6r5kXO%2FoR9otiMU%2BAXXO8XIqK%2FA%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/btAMai/dJMcacD4eIw/AAAAAAAAAAAAAAAAAAAAAPgNo0DBmg9RArG4kZ9lO77-ZYaYO7r6yZi9ib2mQ-0D/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=6r5kXO%2FoR9otiMU%2BAXXO8XIqK%2FA%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/btAMai/dJMcacD4eIw/AAAAAAAAAAAAAAAAAAAAAPgNo0DBmg9RArG4kZ9lO77-ZYaYO7r6yZi9ib2mQ-0D/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=6r5kXO%2FoR9otiMU%2BAXXO8XIqK%2FA%3D&quot; alt=&quot;Kali Linux fcitx5 한글 입력과 한영 키 설정 대표 이미지&quot; /&gt;
&lt;figcaption&gt;Kali Linux fcitx5 한글 입력과 한영 키 설정 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kali에 보안 프로젝트 환경을 만들면서 한글 입력이 안 되는 게 계속 거슬렸다. 처음엔 단순하게 생각했다. Settings에서 Region &amp;amp; Language 들어가 한국어만 추가하면 끝나는 줄 알았다. 추가는 됐는데 한영 전환 자체가 안 먹었다. 게다가 어떤 시스템 다이얼로그에서는 한글이 두부(□□□)로 깨져 보였다. 두 가지 문제가 겹쳐 있었던 거다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;키보드 레이아웃과 입력기는 다른 물건이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한참 찾아보고 나서야 헷갈렸던 지점을 알았다. GNOME Settings의 입력 소스에 한국어를 추가하는 건 그냥 키보드 레이아웃을 추가하는 거였다. 자판 배치만 한국식으로 바뀔 뿐, 한글 조합을 해주는 게 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한글 입력에는 IME(Input Method Engine)가 따로 필요하다. ㅎ + ㅏ를 눌러서 &quot;하&quot;로 조합하는 처리는 키보드 레이아웃이 하는 일이 아니다. 자모를 받아서 음절로 묶어주는 별도 프로그램이 있어야 하고, 그 역할을 fcitx5나 ibus 같은 IME 프레임워크가 담당한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;패키지부터 깔았다&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt update
sudo apt install -y fcitx5 fcitx5-hangul fcitx5-configtool im-config&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각 하는 일은 이렇다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;fcitx5&lt;/code&gt; &amp;mdash; 입력기 프레임워크 본체&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fcitx5-hangul&lt;/code&gt; &amp;mdash; 한글 조합 엔진(libhangul 기반)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fcitx5-configtool&lt;/code&gt; &amp;mdash; GUI 설정 도구&lt;/li&gt;
&lt;li&gt;&lt;code&gt;im-config&lt;/code&gt; &amp;mdash; 시스템 기본 입력기 지정 유틸&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 입력기를 fcitx5로 지정하는 건 한 줄이면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;im-config -n fcitx5&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러면 다음 로그인부터 시스템이 fcitx5를 입력기로 띄운다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;빠뜨리기 쉬운 환경 변수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기가 진짜 놓치기 쉬운 부분이었다. GTK 앱과 Qt 앱이 어떤 입력기를 쓸지는 환경 변수로 결정된다. &lt;code&gt;/etc/environment&lt;/code&gt;에 다음을 추가했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;GTK_IM_MODULE=fcitx
QT_IM_MODULE=fcitx
XMODIFIERS=@im=fcitx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/etc/environment&lt;/code&gt;에 박으면 시스템 전체에 적용돼서 X 세션이든 Wayland든 다 잡힌다. 사용자 단위로만 적용하고 싶으면 &lt;code&gt;~/.profile&lt;/code&gt;이나 &lt;code&gt;~/.xprofile&lt;/code&gt;에 &lt;code&gt;export&lt;/code&gt;로 넣어도 된다. 여기서 한 번 재로그인해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Hangul 입력기 추가와 전환 키 매핑&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;fcitx5-configtool&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정창을 열면 왼쪽에 &quot;현재 입력기&quot;, 오른쪽에 &quot;사용 가능한 입력기&quot;가 뜬다. 한국어가 목록에 안 보이면 &quot;현재 언어만 표시&quot; 체크를 해제하면 된다. 이거 모르고 한참 헤맸다. Hangul을 찾아서 왼쪽으로 옮기고 Apply를 누르면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 그다음이었다. 한국 키보드는 오른쪽 Alt 옆에 한/영 키가 따로 있는데, 일반 PC 키보드에는 그 키가 없다. 그러니 어떤 키를 한영 전환에 쓸지 직접 지정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방법은 두 가지다. 하나는 fcitx5 설정에서 단축키로 지정하는 방법으로, Global Options 탭의 Trigger Input Method 항목에서 원하는 키를 추가하면 된다. 나는 Hangul 키와 Shift+Space를 같이 등록했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 방법은 오른쪽 Alt를 한글 키로 리매핑하는 것이다(X11 환경 한정).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;xmodmap -e 'remove mod1 = Alt_R'
xmodmap -e 'keycode 108 = Hangul'
xmodmap -e 'remove control = Control_R'
xmodmap -e 'keycode 105 = Hangul_Hanja'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오른쪽 Alt를 한글 키로, 오른쪽 Ctrl을 한자 키로 바꾸는 방식이다. &lt;code&gt;~/.xprofile&lt;/code&gt;에 넣어두면 로그인할 때마다 자동 적용된다. 다만 Wayland에서는 xmodmap이 먹지 않아서, 그 환경에서는 keyd 같은 걸 따로 써야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리매핑이 잘 됐는지는 &lt;code&gt;xev&lt;/code&gt;를 띄워두고 키를 눌러보면 확인할 수 있다. 콘솔에 &lt;code&gt;keysym 0xff31, Hangul&lt;/code&gt; 같은 게 뜨면 성공이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한글 깨짐은 결국 폰트 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한글이 □□□로 보이는 건 단순히 한글 글꼴이 안 깔려서였다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt install -y fonts-nanum fonts-nanum-coding fonts-noto-cjk&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치하고 재로그인하니 깨짐이 사라졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 순서를 정리하면, fcitx5와 fcitx5-hangul을 깔고, im-config로 기본 입력기를 지정하고, 환경 변수를 등록한 다음, 한영 전환 키까지 매핑하고, 마지막에 한글 폰트를 깔아야 완성된다. 이 중 하나만 빠져도 한글 입력이 제대로 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고한 자료:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ArchWiki &amp;mdash; Localization/Korean (xmodmap 매핑 예시 포함)&lt;/li&gt;
&lt;li&gt;fcitx5-hangul GitHub &amp;mdash; Default qwerty 레이아웃 강제 옵션 설명&lt;/li&gt;
&lt;li&gt;Fcitx 공식 위키 &amp;mdash; Hangul addon 동작 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;입력기와 키보드 레이아웃을 분리해서 보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한글이 안 써질 때 키보드 레이아웃만 바꾸다 보면 계속 제자리다. XKB는 물리 키와 키 심볼 매핑을 담당하고, fcitx5는 조합 중인 글자를 애플리케이션에 전달한다. 한영 키 이벤트가 들어오는지, fcitx5가 세션에 떠 있는지, GTK/Qt 애플리케이션이 입력기 모듈을 보고 있는지를 따로 확인해야 한다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/cRUPd8/dJMcacD4eIx/AAAAAAAAAAAAAAAAAAAAAAU_D98dlAhUz31k6f-seT-Cs_XVjoA6olrrtseeJwzp/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=MAKPHTsw%2FCAm9l49Zt8Y%2F8pWR3A%3D&quot; alt=&quot;Kali Linux 공식 XFCE FAQ&quot; /&gt;
&lt;figcaption&gt;Kali Linux 공식 XFCE FAQ&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/EL0O2/dJMcafAI7UE/AAAAAAAAAAAAAAAAAAAAAMyc9NSZo40C4pTlJ68nVERMNSaRg7OpmzR9loM9RmTE/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=ushWwhfDhc70HZznwhGvv8vdyVM%3D&quot; alt=&quot;Fcitx 5 공식 Setup 문서&quot; /&gt;
&lt;figcaption&gt;Fcitx 5 공식 Setup 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/crNKte/dJMcafAI7UI/AAAAAAAAAAAAAAAAAAAAALveED1qgM8nQTM4-CAUV8j4qS868Z9zZe4TXbmnxEIS/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=sPL48dbU0ipf7gRM1f0OqZIX5SU%3D&quot; alt=&quot;ArchWiki Fcitx5 문서 &amp;mdash; 환경변수와 프론트엔드 점검&quot; /&gt;
&lt;figcaption&gt;ArchWiki Fcitx5 문서 &amp;mdash; 환경변수와 프론트엔드 점검&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bBHb0u/dJMcaf8tiHQ/AAAAAAAAAAAAAAAAAAAAABFBBt8_v70lEOiOlSyw2R1MAQcTumTAb9nlobeqo930/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=v3Uk7hUCLrV0qpGxMLTJturTeuA%3D&quot; alt=&quot;freedesktop.org X Keyboard Configuration 프로젝트&quot; /&gt;
&lt;figcaption&gt;freedesktop.org X Keyboard Configuration 프로젝트&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;pre&gt;&lt;code&gt;fcitx5-diagnose
ps aux | grep fcitx5
xev&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;xev&lt;/code&gt;에서 키 이벤트는 보이는데 전환이 안 되면 fcitx5 단축키 설정을 보고, 특정 앱에서만 입력이 안 되면 그 앱의 GTK/Qt/Wayland 실행 환경을 본다. 이 순서로 나누니 설정 파일을 무작정 여러 번 덮어쓰는 일을 줄일 수 있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://fcitx-im.org/wiki/Setup_Fcitx_5&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Fcitx 5 Setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wiki.archlinux.org/title/Fcitx5&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ArchWiki Fcitx5&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.freedesktop.org/wiki/Software/XKeyboardConfig/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;X Keyboard Configuration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>환경설정</category>
      <category>Fcitx5</category>
      <category>Kali</category>
      <category>KOREAN</category>
      <category>linux</category>
      <category>Troubleshooting</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/8</guid>
      <comments>https://gorani-kr.tistory.com/8#entry8comment</comments>
      <pubDate>Wed, 22 Jul 2026 15:42:54 +0900</pubDate>
    </item>
    <item>
      <title>Ubuntu 24.04에 카카오톡 설치하다 결국 포기한 이유 (Bottles + Wine + fcitx5)</title>
      <link>https://gorani-kr.tistory.com/7</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/IcNql/dJMcaijUuBr/AAAAAAAAAAAAAAAAAAAAAHW5u-cgxGmM97WBn1XnxLWUhc4P06f9F2yn9HTc2W2z/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=F3ka%2BCIsY6AipNtMBAI8%2FCNymDI%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/IcNql/dJMcaijUuBr/AAAAAAAAAAAAAAAAAAAAAHW5u-cgxGmM97WBn1XnxLWUhc4P06f9F2yn9HTc2W2z/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=F3ka%2BCIsY6AipNtMBAI8%2FCNymDI%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/IcNql/dJMcaijUuBr/AAAAAAAAAAAAAAAAAAAAAHW5u-cgxGmM97WBn1XnxLWUhc4P06f9F2yn9HTc2W2z/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=F3ka%2BCIsY6AipNtMBAI8%2FCNymDI%3D&quot; alt=&quot;Ubuntu에서 Bottles와 Wine으로 카카오톡을 시도한 기록 대표 이미지&quot; /&gt;
&lt;figcaption&gt;Ubuntu에서 Bottles와 Wine으로 카카오톡을 시도한 기록 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ubuntu 24.04 LTS를 메인으로 쓰다 보니 카카오톡이 아쉬운 순간이 계속 생겼다. 폰을 두고 PC만 켜놓을 때 알림을 놓치는 게 은근히 신경 쓰였다. 카카오톡은 공식 리눅스 앱도, 공식 웹 메신저도 없다. 결국 윈도우용 카톡을 Wine으로 직접 돌리거나, Bottles라는 Wine 래퍼를 거치는 정도가 현실적인 선택지였다. 일단 설치가 조금이라도 편해 보이는 Bottles부터 시도했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설치까지는 순조로웠다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bottles는 Flatpak으로 설치한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;flatpak install flathub com.usebottles.bottles&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 bottle을 만들 때 환경을 &quot;Application&quot;으로 두고, 카톡 설치파일(&lt;code&gt;KakaoTalkSetup.exe&lt;/code&gt;)을 받아서 &quot;Run Executable&quot;로 실행했다. 윈도우 설치 마법사가 그대로 뜨고 다음, 다음 누르니 설치가 깔끔하게 끝났다. 여기까지는 평화로웠다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행하니 한글이 안 들어간다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인까지는 됐는데, 입력창에 한글을 못 쳤다. 영어는 들어가는데 한글만 안 되는 상황. 시스템에 fcitx5를 아예 안 깔아둬서 그런가 싶어 입력기부터 잡았다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt install fcitx5 fcitx5-hangul fcitx5-configtool
im-config -n fcitx5&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 입력기를 fcitx5로 지정하고, &lt;code&gt;~/.profile&lt;/code&gt;에 환경 변수도 넣었다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export GTK_IM_MODULE=fcitx
export QT_IM_MODULE=fcitx
export XMODIFIERS=@im=fcitx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재로그인 후 fcitx5 설정 도구에서 Hangul을 입력 메소드 목록에 추가했다. 텍스트 에디터나 브라우저 같은 시스템 앱에서는 한글이 잘 들어갔다. 그런데 카톡은 여전히 영어만 받았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Wine 환경 변수를 카톡에도 전달&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bottles는 Flatpak으로 격리돼서 돌아가기 때문에 호스트의 환경 변수가 자동으로 전달되지 않는다. Bottle 설정의 &quot;Environment Variables&quot; 항목에 위 세 줄을 직접 넣어야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GTK_IM_MODULE=fcitx
QT_IM_MODULE=fcitx
XMODIFIERS=@im=fcitx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카톡을 다시 켰다. 입력창에서 한글 자모는 입력이 되는 것 같았다(ㅎ을 누르면 ㅎ이 보인다). 그런데 조합이 완성되는 순간 입력창이 이상하게 굴면서 메시지 전송 버튼이 활성화되지 않았다. 여기서 멘탈이 좀 나갔다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;진짜 원인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;찾아보니 이게 Bottles 이슈 트래커에 그대로 올라와 있는 알려진 버그였다(#4165). 정리하면 이렇다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Wine의 IME 이벤트 핸들러는 한글 조합을 받을 때 &lt;code&gt;WM_IME_COMPOSITION&lt;/code&gt; 이벤트를 발생시킨다.&lt;/li&gt;
&lt;li&gt;영어는 &lt;code&gt;WM_CHAR&lt;/code&gt; 이벤트로 들어온다.&lt;/li&gt;
&lt;li&gt;카톡 입력창은 &lt;code&gt;WM_CHAR&lt;/code&gt; 기준으로만 &quot;텍스트 변경&quot;을 감지한다.&lt;/li&gt;
&lt;li&gt;그래서 한글을 먼저 입력하면 placeholder(&quot;메시지 입력&quot;)가 사라지지 않고 전송 버튼이 계속 회색 상태로 남는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워크어라운드는 좀 황당했다. 영어 한 글자를 먼저 치고, 백스페이스로 지우고, 그다음에 한글을 입력하면 그 뒤로는 정상 동작한다. 문제는 이걸 메시지 칠 때마다 해야 한다는 거였다. 그렇게까지 쓸 이유가 없었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결국 다 지웠다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 와서 결론을 냈다. Bottles에서 만든 prefix를 통째로 삭제하고, fcitx5 환경 변수도 원복하고, 데스크탑 런처도 정리했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Bottles UI에서 bottle을 삭제하거나
rm -rf ~/.var/app/com.usebottles.bottles/data/bottles/bottles/KakaoTalk

# 억지로 만든 데스크탑 아이콘도 정리
rm ~/.local/share/applications/KakaoTalk.desktop
update-desktop-database ~/.local/share/applications/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스에서 카카오톡을 PC 앱처럼 안정적으로 쓰는 방법은 지금(2026년 4월 기준) 사실상 없는 것 같다. Wine 쪽 IME 처리가 해결되어야 하는 문제라 사용자가 설정 몇 줄로 끝낼 수 있는 영역도 아니었다. 지금은 필요할 때 scrcpy로 안드로이드 화면을 미러링해서 쓴다. 억지로 Wine 설정을 붙잡고 있는 것보다는 이쪽이 훨씬 덜 번거로웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고한 자료:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;bottlesdevs/Bottles 이슈 #4165 &amp;mdash; Korean Input Issue: Placeholder Text Not Disappearing&lt;/li&gt;
&lt;li&gt;Arch Wiki &amp;mdash; Fcitx5 설정 (환경 변수 항목)&lt;/li&gt;
&lt;li&gt;kimlulz/Katalk_linux &amp;mdash; Bottles 기반 설치 가이드 (참고용, 같은 IME 한계 존재)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 이 조합이 계속 불안정했나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bottles는 Wine 접두사와 러너를 관리해주는 도구이고, Wine은 Windows API를 Linux 위에서 다시 구현한다. 카카오톡이 공식 Linux 클라이언트를 제공하는 구조가 아니기 때문에, 설치가 끝났다는 사실만으로 알림&amp;middot;한글 입력&amp;middot;업데이트까지 정상이라고 보장되지 않는다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/ub2Fo/dJMcaijUuBI/AAAAAAAAAAAAAAAAAAAAAP1xmmTihi5I-9EY5BKITRRB5ET6XJQ8R6SeXwAuxxNl/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=mTZ2Rv25JP0gHdmjfPC83R6UcTM%3D&quot; alt=&quot;카카오 공식 서비스 페이지의 카카오톡 안내&quot; /&gt;
&lt;figcaption&gt;카카오 공식 서비스 페이지의 카카오톡 안내&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/kAgek/dJMcafAI7Um/AAAAAAAAAAAAAAAAAAAAAO6mhQ7dN8FV0GniwOxh63rORgg94M0nhYQTTvhyHJVq/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=DSR%2FgAWPmFA%2BgZvbtiVG2ERhdfU%3D&quot; alt=&quot;Bottles 공식 사이트 &amp;mdash; Wine 환경을 병 단위로 관리&quot; /&gt;
&lt;figcaption&gt;Bottles 공식 사이트 &amp;mdash; Wine 환경을 병 단위로 관리&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bwZHCm/dJMcaijUuCc/AAAAAAAAAAAAAAAAAAAAAI7gbWTroG0R3WAK9F2YCN5y-1Erk5-TRec_8lFn8r6Y/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=2cfxJNGGQEbZ4HHe6KlyKBEw%2FD4%3D&quot; alt=&quot;WineHQ 공식 사이트&quot; /&gt;
&lt;figcaption&gt;WineHQ 공식 사이트&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bHI3cN/dJMcaijUuCv/AAAAAAAAAAAAAAAAAAAAAHZ3Q26nSYOmOiHx9p5euxzHKTN9IUinZDMPTK86wRzM/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=1%2BTw1SRUlcg81FiEnJ9%2FUL%2F6mEM%3D&quot; alt=&quot;Fcitx 5 공식 위키&quot; /&gt;
&lt;figcaption&gt;Fcitx 5 공식 위키&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실패는 &amp;ldquo;명령어 하나를 더 찾으면 된다&amp;rdquo;기보다 지원 경계가 겹친 문제였다. Wine 러너, 입력기 환경변수, 애플리케이션 업데이트 중 하나만 바뀌어도 다시 깨질 수 있었다. 메신저처럼 알림과 자동 업데이트가 중요한 프로그램은 브라우저&amp;middot;휴대폰&amp;middot;가상머신 같은 대안을 먼저 비교하는 편이 유지보수 비용이 낮았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;확인할 공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.kakaocorp.com/page/service/service/KakaoTalk&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;KakaoTalk 서비스 안내&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://usebottles.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Bottles&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.winehq.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;WineHQ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://fcitx-im.org/wiki/Fcitx_5&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Fcitx 5&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>환경설정</category>
      <category>Bottles</category>
      <category>Fcitx5</category>
      <category>kakaotalk</category>
      <category>ubuntu</category>
      <category>Wine</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/7</guid>
      <comments>https://gorani-kr.tistory.com/7#entry7comment</comments>
      <pubDate>Wed, 22 Jul 2026 15:41:29 +0900</pubDate>
    </item>
    <item>
      <title>Kali Linux VMware에서 마우스 커서 사라지고 165Hz도 안 잡힐 때</title>
      <link>https://gorani-kr.tistory.com/6</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/cGsfy8/dJMcah6rrds/AAAAAAAAAAAAAAAAAAAAAHN4vPUBZ1TgdDKHBauchXXhGA5rEql-BvUPCkX8Q2JY/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=R9%2FCRQ4gsDkyec4eR8vlecC5NwY%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dna/cGsfy8/dJMcah6rrds/AAAAAAAAAAAAAAAAAAAAAHN4vPUBZ1TgdDKHBauchXXhGA5rEql-BvUPCkX8Q2JY/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=R9%2FCRQ4gsDkyec4eR8vlecC5NwY%3D&quot; data-lightbox=&quot;lightbox&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/cGsfy8/dJMcah6rrds/AAAAAAAAAAAAAAAAAAAAAHN4vPUBZ1TgdDKHBauchXXhGA5rEql-BvUPCkX8Q2JY/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=R9%2FCRQ4gsDkyec4eR8vlecC5NwY%3D&quot; alt=&quot;Kali VMware 마우스 커서와 165Hz 문제 해결 대표 이미지&quot; /&gt;
&lt;figcaption&gt;Kali VMware 마우스 커서와 165Hz 문제 해결 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안동아리 프로젝트로 공유기 분석을 해야 해서 Kali Linux 2025.4를 VMware에 새로 깔았다. 설치는 별문제 없이 끝났는데, 로그인하자마자 마우스 커서가 안 보였다. 클릭도 되고 드래그도 되는데 커서 아이콘만 화면에 안 그려지는 상태. 메뉴 하나 찾으려고 화면을 더듬더듬 클릭하고 다녀야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거기에 하나 더, 평소 쓰는 모니터가 165Hz인데 VM 안에서는 무조건 60Hz로 고정돼 있었다. View 메뉴의 &quot;Fit Guest Now&quot; 옵션도 회색으로 비활성화라 손댈 수가 없었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;X 서버 설정을 의심하다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 Kali 안에서 뭔가 잘못된 줄 알았다. &lt;code&gt;/etc/X11/xorg.conf&lt;/code&gt;를 만들어서 &lt;code&gt;HWcursor&lt;/code&gt; 옵션을 &lt;code&gt;off&lt;/code&gt;로 두면 된다는 글이 많길래 그대로 따라 해봤다. 하드웨어 커서 대신 소프트웨어 커서를 강제로 쓰게 만드는 방법이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo nano /etc/X11/xorg.conf&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Section &quot;Device&quot;&lt;/code&gt; 안에 &lt;code&gt;Option &quot;HWcursor&quot; &quot;off&quot;&lt;/code&gt;를 넣고 재부팅했는데 안 됐다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;open-vm-tools 재설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kali 버그 트래커(0009336)를 뒤져보니 같은 이슈가 이미 올라와 있었다. &lt;code&gt;apt full-upgrade&lt;/code&gt; 후에 커서가 사라지는 게 알려진 버그였고, 공식 권장 해결법은 &lt;code&gt;open-vm-tools-desktop&lt;/code&gt;을 다시 깔라는 거였다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt update
sudo apt install -y open-vm-tools open-vm-tools-desktop
sudo reboot&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸로 호스트-게스트 통합은 다시 잡혔는데, 커서는 여전히 안 돌아왔다. 여기서만 30분을 날렸다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;진짜 원인은 가상 하드웨어 호환성이었다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VM마다 &quot;가상 하드웨어 버전&quot;이라는 게 있다. 여기서 에뮬레이트되는 그래픽카드, 마우스, 메모리 모델 같은 걸 정의한다. Kali 커널이랑 &lt;code&gt;vmwgfx&lt;/code&gt; 드라이버는 최신인데 VM의 가상 하드웨어 정의가 예전 버전이면, 새 드라이버가 예전 가상 GPU에서 지원 안 되는 기능을 호출하다가 커서 렌더링이 깨지는 모양이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결은 생각보다 간단했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;VM 종료 (스냅샷 하나 떠두면 안전하다)&lt;/li&gt;
&lt;li&gt;VMware Workstation에서 해당 VM 우클릭 &amp;rarr; Change Hardware Compatibility&lt;/li&gt;
&lt;li&gt;마법사에서 가장 최신 호환성 버전 선택 &amp;rarr; Next &amp;rarr; Finish&lt;/li&gt;
&lt;li&gt;VM 부팅&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부팅하자마자 커서가 바로 돌아왔다. 참고로 이 옵션은 VMware Workstation Pro에만 있고 Player에는 없다. 다행히 Workstation Pro가 개인 사용은 무료라 그냥 옮겨 타면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;165Hz는 결국 포기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커서를 잡고 나니 주사율도 욕심이 났다. &quot;Fit Guest Now&quot;가 회색이던 것도 &lt;code&gt;open-vm-tools-desktop&lt;/code&gt;을 재설치하면서 같이 풀렸는데, 문제는 그다음이었다. 호환성 업그레이드까지 마친 후에도 주사율이 60Hz 이상 올라가질 않았다. VMware Communities랑 VirtualBox 포럼을 한참 뒤져본 결론은, VM 게스트가 호스트의 고주사율을 그대로 받는 건 사실상 불가능하다는 거였다. &lt;code&gt;xrandr&lt;/code&gt;로 커스텀 모드라인을 만들어 144Hz를 강제로 등록해봐도 화면은 그대로 60Hz로 돌아간다. 진짜 고주사율을 쓰려면 GPU 패스스루가 필요한데, 그러려면 호스트가 Linux여야 하고 GPU도 따로 있어야 한다. 보안 공부용 환경 만드는 거였으니 거기까지 갈 일은 없어서 그냥 접었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면, Kali에서 커서가 사라지는 문제는 게스트 OS 안에서 헤매기 전에 VM의 하드웨어 호환성 버전부터 의심하는 게 빠르다. 165Hz는 VM 구조상 애초에 안 되는 거니까 일찍 포기하는 게 정신건강에 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고한 자료:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kali Linux Bug Tracker #9336 &amp;mdash; &lt;code&gt;apt full-upgrade&lt;/code&gt; 후 커서 사라지는 이슈&lt;/li&gt;
&lt;li&gt;VMware Communities &amp;mdash; Linux 게스트의 &quot;Fit Guest Now&quot; 동작 조건&lt;/li&gt;
&lt;li&gt;&lt;code&gt;open-vm-tools&lt;/code&gt; 공식 저장소 (vmware/open-vm-tools)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 문서로 다시 확인한 지점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커서 통합과 화면 크기&amp;middot;주사율 문제를 한 덩어리로 보지 않는 게 중요했다. 게스트 통합은 open-vm-tools, 실제 출력 모드 확인은 XRandR, VMware 제품 쪽 권장 설치 방식은 Broadcom 문서로 나눠 확인할 수 있다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/wP6Za/dJMcaijUuAs/AAAAAAAAAAAAAAAAAAAAANHNMp5mh2vtggkHqqQj8HDeK9tSEr8og86mHCGb8lVh/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=N7jc%2BjBcyMNJpH8p37Ah7W5%2FpQw%3D&quot; alt=&quot;Kali 공식 VMware 게스트 설치 문서&quot; /&gt;
&lt;figcaption&gt;Kali 공식 VMware 게스트 설치 문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/ctHnyi/dJMcah6rrdw/AAAAAAAAAAAAAAAAAAAAADT1L2HZnHS4jfvmlzOh27p9nFW6GTpzkF6giiAP0a-p/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=kEP8KIsXP7%2BFCR4wTFQ4VcywRkI%3D&quot; alt=&quot;VMware open-vm-tools 공식 GitHub 저장소&quot; /&gt;
&lt;figcaption&gt;VMware open-vm-tools 공식 GitHub 저장소&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/cMDXTP/dJMcah6rrdx/AAAAAAAAAAAAAAAAAAAAAB7MEsxTk8ymEaIG7PO955AbehK6Zf9KlS_6ilZnR89l/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=iGzZ%2BzBzI7m6gZja9OuUfpaP7EA%3D&quot; alt=&quot;X.Org xrandr 매뉴얼 &amp;mdash; 현재 출력 모드와 주사율 확인&quot; /&gt;
&lt;figcaption&gt;X.Org xrandr 매뉴얼 &amp;mdash; 현재 출력 모드와 주사율 확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bHAqDs/dJMcadJzF8S/AAAAAAAAAAAAAAAAAAAAAB3JHkXFWx_vkq3BiMwdZjvUNrvpFdQm8jGbY_AbT9fy/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=HWox8zXyUf4uIVM1xkXgb0Bfv3I%3D&quot; alt=&quot;Broadcom 공식 open-vm-tools 설치 안내&quot; /&gt;
&lt;figcaption&gt;Broadcom 공식 open-vm-tools 설치 안내&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;xrandr&lt;/code&gt;에 165Hz 모드 자체가 없으면 데스크톱 설정만 만져서는 해결되지 않는다. 반대로 모드는 보이는데 선택 후 돌아온다면 VMware 가상 디스플레이 제한과 호스트 설정을 같이 봐야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공식 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.kali.org/docs/virtualization/install-vmware-guest-vm/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Kali Linux: VMware 게스트 설치&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/vmware/open-vm-tools&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;open-vm-tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.x.org/releases/current/doc/man/man1/xrandr.1.xhtml&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;XRandR 매뉴얼&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://knowledge.broadcom.com/external/article/315654/installing-open-vm-tools.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Broadcom open-vm-tools 안내&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>환경설정</category>
      <category>Kali</category>
      <category>linux</category>
      <category>Troubleshooting</category>
      <category>vm</category>
      <category>vmware</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/6</guid>
      <comments>https://gorani-kr.tistory.com/6#entry6comment</comments>
      <pubDate>Wed, 22 Jul 2026 15:39:21 +0900</pubDate>
    </item>
    <item>
      <title>해당 연재를 시작하며</title>
      <link>https://gorani-kr.tistory.com/3</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;나는 22-23 시즌에 처음 스키를 탔다. 삼수를 끝내고 대학에 들어가기 전 겨울, 유럽 여행 중 융프라우 곤돌라에서 아래 설원을 내려가는 스키어들을 봤다. 그때는 기술 이름도 장비 차이도 몰랐지만 &amp;ldquo;언젠가는 저렇게 타보고 싶다&amp;rdquo;는 생각이 선명하게 남았다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/b7fRxu/dJMcagTWCq6/AAAAAAAAAAAAAAAAAAAAAIzWufvm6f01PgylLfoY2oPnX4noipNUhGfmLo9hITa1/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=wJ%2BndRoRA9xzXxUg54rM3a201DE%3D&quot; alt=&quot;스키 입문부터 강사 준비까지 연재 소개 대표 이미지&quot; /&gt;
&lt;figcaption&gt;스키 입문부터 강사 준비까지 연재 소개 대표 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/tXbXb/dJMcaa7jlaI/AAAAAAAAAAAAAAAAAAAAAIpL98aFgf749fWV-agOTQdtw7Q4-OKB8bgx6O6b2cyG/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=jPvM5i%2FwmPkA9Tc97%2FWOrPVRr58%3D&quot; alt=&quot;처음 스키에 관심을 갖게 된 융프라우 스키 지역 공식 페이지&quot; /&gt;
&lt;figcaption&gt;처음 스키에 관심을 갖게 된 융프라우 스키 지역 공식 페이지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;처음에는 친구들 따라 탔다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;귀국한 뒤 고등학교 친구들과 스키장에 가면서 실제로 입문했다. 첫 시즌은 제대로 된 강습보다는 주변에서 알려주는 대로 따라 타는 수준이었다. 자세가 맞는지, 장비가 나한테 맞는지도 몰랐다. 그래도 속도를 조절하고 방향을 바꿀 때의 감각이 재미있어서 다음 시즌을 기다리게 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;23-24 시즌에는 대학 교양으로 개설된 생활스포츠(스키) 강좌를 수강했다. 겨울 스키캠프가 있다는 이유로 신청했지만, 수업을 통해 턴과 하중 이동을 말로 설명하는 법을 처음 배웠다. 이전에는 내려오기만 하면 된다고 생각했는데 자세를 의식하고 타기 시작하니 같은 슬로프도 전혀 다르게 느껴졌다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/pUEBj/dJMcaa7jlaJ/AAAAAAAAAAAAAAAAAAAAAIv-a6u-ZuYV8tj3q-C_DwICuzV0YPLi3dS7dvEZkh-H/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=fnyLIEoyFYto9rLDlywxPcz0jkQ%3D&quot; alt=&quot;FIS 공식 알파인 스키 경기&amp;middot;결과 페이지&quot; /&gt;
&lt;figcaption&gt;FIS 공식 알파인 스키 경기&amp;middot;결과 페이지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;군 복무 중 맞은 다음 시즌&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;24-25 시즌은 군 복무 중이었다. 휴가를 맞춰 친구들과 몇 번 스키장에 갔지만 시즌 전체를 보내기에는 시간이 짧았다. 그래서 다음 겨울에는 단순히 몇 번 방문하는 것보다 아예 스키장 안에서 생활하며 더 많이 타고 싶다는 생각을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시 패트롤로 일하던 친구에게 스키장 근무 이야기를 들었고, 봄부터 스키강사를 준비하는 방향을 잡았다. 여름에는 장비를 미리 알아보기 시작했다. 문제는 검색하면 홍보 글은 많은데, 초보 입장에서 왜 그 길이와 플렉스를 골라야 하는지, 강사 준비에는 무엇이 필요한지까지 이어지는 기록이 적다는 것이었다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;blob:https://gorani-kr.tistory.com/d2142568-c7cd-4aaf-b8fe-68025ba2f2e7&quot; alt=&quot;PSIA-AASI 공식 스키&amp;middot;스노보드 강습 단체 페이지&quot; /&gt;
&lt;figcaption&gt;PSIA-AASI 공식 스키&amp;middot;스노보드 강습 단체 페이지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 연재에 남길 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시리즈에는 내가 실제로 알아보고 결정한 과정을 남기려 한다. 장비 이름만 나열하기보다 당시 실력과 목표에서 왜 그 선택을 했는지, 써본 뒤 생각이 어떻게 바뀌었는지를 같이 적는다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스키&amp;middot;부츠&amp;middot;폴을 고를 때 본 기준과 사이즈&lt;/li&gt;
&lt;li&gt;렌탈 장비와 개인 장비에서 체감한 차이&lt;/li&gt;
&lt;li&gt;강습을 받으며 고친 자세와 연습 순서&lt;/li&gt;
&lt;li&gt;스키강사 준비 과정과 실제 현장에서 필요했던 것&lt;/li&gt;
&lt;li&gt;리조트 이용 전 확인할 슬로프&amp;middot;운영 정보&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2800&quot; data-origin-height=&quot;1800&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/cBnQfd/dJMcagTWCrd/AAAAAAAAAAAAAAAAAAAAAErnbMxqJMpnSpnXx_bwbcQIXZ0FEWDsqUw5i7bNIMts/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1785509999&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=3qGROcdjTtoMFVTK1tSQ2LK1Arg%3D&quot; alt=&quot;국내 스키장 운영 정보를 확인할 수 있는 모나 용평 공식 페이지&quot; /&gt;
&lt;figcaption&gt;국내 스키장 운영 정보를 확인할 수 있는 모나 용평 공식 페이지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터넷에 없는 정보를 억지로 일반화하지는 않으려고 한다. 내가 탄 환경과 장비, 당시 수준을 가능한 한 구체적으로 남기고, 자격&amp;middot;기술 기준처럼 공식 확인이 필요한 부분은 관련 단체와 리조트 자료를 연결할 생각이다. 몇 시즌 뒤 다시 읽었을 때 실력과 판단이 어떻게 바뀌었는지 보이는 기록이면 충분하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고한 공식 사이트&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.jungfrau.ch/en-gb/jungfrau-ski-region/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Jungfrau Ski Region&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.fis-ski.com/DB/alpine-skiing/calendar-results.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;FIS Alpine Skiing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://psia-aasi.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PSIA-AASI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.yongpyong.co.kr/eng/index.do&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Mona Yongpyong&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>스키/스키장비셋업</category>
      <category>스키</category>
      <category>스키장비</category>
      <author>코드스키</author>
      <guid isPermaLink="true">https://gorani-kr.tistory.com/3</guid>
      <comments>https://gorani-kr.tistory.com/3#entry3comment</comments>
      <pubDate>Tue, 1 Jul 2025 14:38:16 +0900</pubDate>
    </item>
  </channel>
</rss>