728x90

내부 LLM 게이트웨이 하나 굴려본 사람이라면 이 얘기가 남 일 같지 않을 거다. Claude Code가 ANTHROPIC_BASE_URL을 커스텀 엔드포인트로 돌렸을 때, 시스템 프롬프트에 들어가는 "오늘 날짜" 문장 하나를 조용히 바꿔서 요청 안에 분류 신호를 숨긴다는 분석이 나왔다. 눈으로는 거의 구분이 안 되는 유니코드 문자로 말이다.

결론부터 말하면, 대부분의 사용자는 이 경로가 아예 안 켜진다. 하지만 자체 게이트웨이나 프록시, 모델 라우터를 태워서 Claude Code를 쓰는 조직이라면 얘기가 다르다. 이 글에서는 원리를 뜯어보고, 실무에서 내 트래픽에 이런 마커가 붙는지 직접 확인하는 방법까지 정리한다.

1. 왜 지금 화제인가

스테가노그래피(steganography)는 암호화와 다르다. 암호화는 "이건 비밀이다"라는 걸 대놓고 보여주고 내용만 못 읽게 한다. 스테가노그래피는 비밀이 있다는 사실 자체를 숨긴다. 평범해 보이는 이미지의 최하위 비트에 데이터를 심거나, 문장의 공백·문장부호에 정보를 인코딩하는 식이다.

이번 사안이 딱 그 케이스다. Claude Code 2.1.196 바이너리에는 시스템 프롬프트로 들어가는 날짜 문자열을 바꾸는 함수가 있다. 기본형은 이렇다.

Today's date is 2026-06-30.

겉으로는 그냥 오늘 날짜를 알려주는 평범한 안내 문장이다. 그런데 특정 조건에서 이 문장의 두 지점이 바뀐다.

  • Today's의 아포스트로피(')
  • 날짜 구분자(-/)

여기서 "왜 문제냐"가 핵심이다. 파일시스템 접근하고, 셸 명령 실행하고, 패키지 설치하고, 커밋까지 푸시하는 개발자 도구가 사용자에게 알리지 않고 보이지 않는 표식으로 분류 정보를 요청에 심고 있다는 것. 목적이 "중국 연구소의 모델 증류(distillation) 탐지"라 해도, 구현 방식이 조용하고 은밀하다는 게 신뢰를 흔든다는 지적이다.

2. 동작 원리: 마커가 어떻게 심기나

트리거 조건

모든 게 ANTHROPIC_BASE_URL에서 시작한다. 이건 Claude Code의 API 기본 URL을 오버라이드하는 환경 변수다. 로직을 순서대로 보면:

  1. ANTHROPIC_BASE_URL설정 안 됨 → 함수 조기 반환(early return). 평범한 Today's date is 2026-06-30. 유지.
  2. 설정됐지만 호스트가 api.anthropic.com → 마찬가지로 조기 반환.
  3. 그 외의 경우에만 아래 검사를 수행한다.
    • 시스템 시간대가 Asia/Shanghai 또는 Asia/Urumqi인지
    • 호스트명이 디코딩된 도메인 목록과 일치하거나 그 하위 도메인인지
    • 호스트명에 특정 AI 연구소 키워드가 들어있는지

무엇이 인코딩되나

시간대가 상하이/우루무치면 날짜 구분자가 바뀐다.

2026-06-30  →  2026/06/30

호스트명 분류 결과는 아포스트로피에 인코딩된다. 이게 진짜 교묘하다.

일반:                          '        (U+0027)
알려진 도메인:                  ’        (U+2019)
연구소 키워드:                  ʼ        (U+02BC)
알려진 도메인 + 연구소 키워드:  ʹ        (U+02B9)

모노스페이스 폰트에서 이 네 개를 눈으로 구분할 수 있는가? 나는 못 한다. 그게 포인트다. 로그를 흘려봐도 "그냥 아포스트로피네" 하고 넘어간다.

도메인 목록은 왜 안 보였나

도메인·키워드 목록은 그냥 문자열로 박혀있지 않다. base64로 저장한 다음 XOR 키 91로 디코딩한다. 그래서 바이너리를 strings로 훑어도 deepseek, zhipu 같은 게 바로 안 나온다. 디코딩된 연구소 키워드에는 이런 게 있다.

deepseek, moonshot, minimax, zhipu, bigmodel,
baichuan, stepfun, 01ai, dashscope, volces

도메인 목록은 더 크다. 중국 기업 도메인(baidu.com, bytedance.net, alibaba-inc.com), AI 회사 도메인(moonshot.ai, stepfun-inc.com), 그리고 프록시·리셀러·게이트웨이 도메인(proxyai.com, anyrouter.top, claude-code-hub.app, yunwu.ai, zenmux.ai)까지 포함된다.

이 마커는 에이전트 컨텍스트를 구성할 때 currentDate 값으로 들어가고, 같은 컨텍스트에는 조건에 따라 사용자 이메일이나 프로젝트 정보도 포함될 수 있다. 즉 모델로 전송되는 시스템 컨텍스트의 일부가 된다. Anthropic 백엔드가 이 값을 파싱할 가능성도 남아있다는 게 원 분석의 지적이다.

비유하자면 배송 송장에 찍힌 날짜 도장 같은 거다. 대부분 사람에겐 그냥 "2026/06/30"이지만, 도장 잉크 색과 슬래시 각도만 보면 창고 시스템은 "이 물건은 어느 경로로 왔고 어느 창구에서 검수 대상인지"를 안다. 받는 사람은 색 차이를 눈치채지 못한다.

3. 실무 관점: 언제 이걸 만나고 어떻게 확인하나

영향받는 시나리오

정상적이고 합법적인데도 이 경로가 켜지는 경우가 문제다. 실제로 우리가 게이트웨이를 두는 이유들이다.

  • 내부 LLM 게이트웨이로 감사·비용 관리
  • 로컬 프록시로 Claude Code가 Anthropic에 뭘 보내는지 검증(데이터 유출 확인)
  • 프롬프트 난이도에 따라 모델 동적 선택
  • 프로젝트별 여러 Anthropic 계정 전환
  • 자격 증명·PII·회사 기밀 필터링

이런 세팅에서 게이트웨이 호스트명이 우연히 목록의 키워드나 도메인 패턴에 걸리거나, 서버 시간대가 상하이/우루무치면 마커가 붙는다. 원 분석은 "이상하지만 합법적인 설정을 쓰는 일반 개발자가 오히려 더 쉽게 지문 채집 대상이 된다"고 본다. 진짜 작정한 공격자는 호스트명 바꾸고 시간대 바꾸고 바이너리 패치하면 그만이기 때문이다.

내 트래픽에 마커가 붙는지 직접 확인하기

가장 확실한 건 프록시 앞단에서 실제로 나가는 요청 바디를 뜯어보는 거다. mitmproxy를 세워서 system 필드의 날짜 문장을 유니코드 코드포인트 단위로 확인한다.

# 1) mitmproxy 실행
mitmproxy --listen-port 8080 --set flow_detail=3

# 2) Claude Code를 프록시 태워서 실행 (테스트용 커스텀 URL)
export ANTHROPIC_BASE_URL="https://my-gateway.internal.example.com"
export HTTPS_PROXY="http://127.0.0.1:8080"
claude "hello"

캡처한 요청 바디에서 날짜 문장만 뽑아 아포스트로피 문자의 정체를 확인한다. 파이썬 한 줄이면 된다.

python3 - <<'PY'
s = "Today’s date is 2026/06/30."   # 캡처한 실제 문자열을 붙여넣기
for ch in s:
    if not ch.isascii():
        print(f"non-ASCII: {ch!r}  U+{ord(ch):04X}")
PY

출력이 이렇게 나오면 마커가 붙은 거다.

non-ASCII: '’'  U+2019

U+2019면 "알려진 도메인" 분류, U+02BC면 "연구소 키워드", U+02B9면 둘 다 걸린 거다. 아무 출력도 없고(모두 ASCII) 날짜가 - 구분자면 정상 경로다.

흔한 함정

함정 1: 프록시 TLS 인증서를 안 넣어서 캡처가 아예 안 됨. Claude Code는 Node 기반이라 시스템 신뢰 저장소가 아니라 Node의 CA를 본다. mitmproxy CA를 안 걸면 이런 에러로 요청 자체가 실패한다.

Error: unable to get local issuer certificate
    at TLSSocket.onConnectSecure (node:_tls_wrap:1678:34)
    code: 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY'

해결은 mitmproxy CA를 Node에 명시적으로 물려주는 거다.

export NODE_EXTRA_CA_CERTS="$HOME/.mitmproxy/mitmproxy-ca-cert.pem"
claude "hello"

함정 2: 시간대를 컨테이너에서 신경 안 씀. CI/에이전트를 상하이 리전 노드에서 돌리면 TZ=Asia/Shanghai가 잡혀서 날짜 구분자가 슬래시로 바뀔 수 있다. 컨테이너 TZ를 명시적으로 UTC로 고정하는 게 재현성 측면에서도 낫다.

# TZ 확인
$ date +%Z
CST
# 명시적 고정
$ export TZ=UTC
$ date +%Z
UTC

함정 3: 로그 grep으로 못 잡는다. 로그 파이프라인에서 grep "Today's date" 걸어도 아포스트로피가 ASCII가 아니면 매칭이 안 된다. 이런 오탐(false negative)이 진짜 골치다.

# 이렇게 하면 U+2019 버전은 안 걸린다
$ grep "Today's date" request.log
# (매칭 없음)

# 코드포인트 무관하게 잡으려면 패턴을 느슨하게
$ grep -P "Today.s date is \d{4}" request.log

대응 옵션과 트레이드오프

  • 프록시에서 정규화(normalize). 게이트웨이 미들웨어에서 시스템 프롬프트의 날짜 문장을 강제로 ASCII '-로 치환한다. 가장 확실하지만, 남이 만든 요청 바디를 손대는 거라 파싱 로직이 바뀌면 유지보수 비용이 든다.
  • 호스트명·시간대 회피. 원 분석대로 호스트명을 목록에 안 걸리게 바꾸고 시간대를 UTC로 두면 조기 반환된다. 단, 이건 "탐지 신호를 무력화"하는 거라 Anthropic 약관 관점에서 애매할 수 있으니 조직 정책 확인이 필요하다.
  • 바이너리 패치/래핑. 이론상 가능하지만 서명된 바이너리를 건드리는 순간 업데이트마다 재작업이고 지원도 못 받는다. 실무에선 비추천이다.
  • 그냥 오픈소스 클라이언트로 갈아타기. HN 반응 중엔 Codex CLI가 FOSS라 이런 은닉 동작 가능성이 낮다는 의견이 있었다. 다만 "클라이언트가 오픈소스면 서버에서 더 숨긴다는 뜻일 뿐"이라는 반론도 있으니 만능은 아니다.

참고로 이게 무조건 "악성"이냐에 대해선 의견이 갈린다. "약관 위반 사용자를 걸러내면서 정상 사용자엔 방해 안 되는 방법"이라는 옹호론과, "PII를 안 수집한다는 보장이 없고, 접근 패턴을 공개하지 않고 지문 채취하는 것 자체가 부정직"이라는 비판론이 팽팽하다. 판단은 각자의 몫이지만, 내 게이트웨이 트래픽에 뭐가 붙는지는 내가 아는 게 맞다는 데는 이견이 없을 거다.

4. 정리

한 줄 요약: Claude Code는 ANTHROPIC_BASE_URL이 공식 엔드포인트가 아닐 때, 호스트명·시간대를 검사해 시스템 프롬프트 날짜 문장의 아포스트로피와 구분자를 보이지 않게 바꿔 분류 신호를 심는다. 대부분은 비활성이지만, 커스텀 엔드포인트를 쓰면 켜진다.

  • 확인해야 할 사람: 자체 LLM 게이트웨이·프록시·모델 라우터를 통해 Claude Code를 운영하는 인프라/백엔드 엔지니어.
  • 언제: 커스텀 ANTHROPIC_BASE_URL을 쓰거나, 상하이/우루무치 시간대 노드에서 에이전트를 돌리는 순간부터.
  • 바로 할 것: mitmproxy로 요청 바디의 날짜 문장을 캡처해 유니코드 코드포인트를 확인하고, 필요하면 게이트웨이에서 정규화. 로그 grep은 코드포인트 무관 패턴으로 바꿀 것.

단정할 수 없는 부분(백엔드가 실제로 이 값을 파싱하는지, PII 수집 여부 등)은 여전히 남아있다. 확실한 건 "내 도구가 내가 모르는 뭔가를 요청에 심고 있다"는 사실이고, 이건 신뢰 문제로 직결된다. 프록시 뒤에서 운영한다면 오늘 한 번 캡처 떠서 내 눈으로 확인하는 걸 권한다.

참고 자료

728x90

+ Recent posts