본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 20. 23:03

AI 에이전트, 10분 만에 난독화된 JavaScript를 격파하다

요약

Claude Code와 같은 AI 에이전트가 복잡한 난독화 기술이 적용된 JavaScript 코드를 단 몇 분 만에 분석하고 원본 소스 코드로 복구하는 실험 과정을 다룹니다. 정적 분석의 한계를 극복하기 위해 에이전트가 스스로 런타임 계측(instrumentation) 도구를 작성하여 난독화된 VM의 바이트코드를 추출하는 동적 분석 방식으로 전환하여 성공한 사례를 보여줍니다.

핵심 포인트

  • 단순 챗봇과 달리 실행 능력을 갖춘 AI 에이전트는 복잡한 난독화 로직을 격파할 수 있음
  • 정적 재구현(Static Reimplementation)의 실패를 인지하고 동적 계측(Instrumentation)으로 전략을 수정하는 자율적 문제 해결 능력 확인
  • 난독화 도구가 스스로 코드를 디코딩하도록 유도하는 '런타임 활용' 방식이 핵심 성공 요인
  • Claude Code를 활용하여 커스텀 VM 기반의 9단계 방어 계층을 10분 만에 무력화

이 글은 제가 AfterPack 블로그에서 진행한 실험을 집중적으로 정리한 기록입니다. 네 문단으로 구성된 전체 프롬프트, 실패했던 883줄짜리 스크립트, 바이트코드 역어셈블리(bytecode disassembly), 그리고 두 가지 타겟 모두에서 복구된 소스 코드를 포함합니다. 여기서는 왜 이것이 성공했는지, 그리고 이것이 무엇을 변화시키는지에 대해 다루고자 합니다. 난독화된 코드를 실행할 수 있는 LLM 에이전트는 이를 몇 분 만에 격파합니다. 벤더사가 직접 공개한 데모 파일들을 대상으로 두 종류의 JS 난독화 도구(obfuscators)로부터 각각 10분과 20분 만에 깨끗한 소스 코드를 복구해냈습니다. 그중 하나는 AI가 이를 할 수 없다고 마케팅에서 주장해 온 상용 엔터프라이즈 제품이었습니다. 그 주장은 챗봇(chatbot)에 대해서는 정확할지 모르나, 스크립트를 작성하고 실행하는 에이전트(agent)에 대해서는 그렇지 않습니다. 그래서 저는 Claude Code에 두 개의 난독화된 파일과 각각 하나의 프롬프트를 주고, 실제로 무언가를 실행하도록 내버려 두었습니다.

타겟 1: 9개의 방어 계층을 가진 커스텀 VM
첫 번째 타겟는 1,587줄, 68KB의 난독화된 출력물로 구성되었습니다(이는 오픈 소스 난독화 도구가 VM 모드를 자랑하기 위해 랜딩 페이지에 게시한 13줄짜리 calculatePrice(quantity, unitPrice) 입력값보다 약 194배 큰 규모입니다). 약 10분 만에 소스로 복구되었습니다. 내부에는 약 1,500줄의 커스텀 스택 기반 VM 인터프리터(stack-based VM interpreter)를 감싸고 있는 9개의 조합 가능한 방어 계층이 있었습니다. 프롬프트는 네 개의 짧은 문단이었습니다. "이것을 난독화 해제(deobfuscate)해줘, 필요한 만큼 반복하고, 도움이 되는 임시 파일은 무엇이든 작성해, 가장 유사한 재구성 결과와 사용된 기술에 대한 노트를 줘"라는 내용이었으며, 저는 파일이 어떤 난독화 도구에서 왔는지 전혀 말하지 않았습니다. Claude는 이를 네 번 읽고 구조를 통해 이를 인식한 뒤, 6단계 계획을 작성했습니다.

실제 작업이 시작되기 전 작성된 Claude의 계획입니다. 첫 번째 시도는 자연스러운 방식이었으나 실패했습니다. Claude는 RC4 문자열 복호화(string decryption), base64, 바이너리 역직렬화기(binary deserializer) 등 파이프라인을 정적으로 재구현하는 883줄짜리 deobfuscate.js를 작성했습니다. 이를 통해 약 500개의 암호화된 문자열 호출을 되찾았고, 환경 지문(environment-fingerprint) 값을 복구했습니다. 하지만 그 후 커스텀 zigzag-varint 바이트코드 형식을 만났고, 잘못된 버전 바이트를 선택하여 쓰레기 값(garbage)을 생성했습니다. 외부에서 역직렬화기를 재구현하는 것은 함정이었습니다.

따라서 AI 에이전트는 재구현을 중단하고 계측 (instrumenting)을 시작했습니다. 48줄로 구성된 instrument2.js는 VM (Virtual Machine)에 몇 개의 로깅 훅 (logging hooks)을 삽입하여 난독화된 파일의 수정된 복사본을 만들었고, 해당 복사본을 실행하여 난독화기 (obfuscator)가 런타임 (runtime) 중에 스스로의 바이트코드 (bytecode)를 디코딩하도록 했습니다. 그 결과 함수 이름, 매개변수 (parameters), 지역 변수 (locals), 상수 ([0.15, 100, 1, "calculatePrice"]), 그리고 함수별로 필요한 키 값들(blockKey=54, jumpKey=9643, seKey=4168320119)이 추출되었습니다. 핵심 전환점(pivot)은 이것입니다: 역직렬화기 (deserializer)를 재구현하지 말고, 난독화기가 당신을 위해 그것을 실행하게 하십시오. disassemble.js는 캡처된 22개의 명령어 바이트코드 스트림 (instruction bytecode stream)을 가져와 연산 코드 (opcodes: PUSH_CONST, LOAD_ARG, STORE_LOCAL, MUL, SUB, GT, JMP_FALSE, RETURN ...)의 이름을 지정하고 다음과 같이 재구성했습니다:

function calculatePrice ( price , quantity ) {
const taxRate = 0.15 ;
const threshold = 100 ;
let total = price * quantity ;
if ( total > threshold ) {
total = total * ( 1 - taxRate );
}
return total ;
}
console . log ( calculatePrice ( 10 , 20 )); // → 170

경계값 케이스 (boundary case)를 포함하여 제가 시도한 모든 입력값에 대해 원본과 동일한 170이 결과로 나왔습니다. 완벽하게 보존되지 않는 것은 이름들입니다. quantity와 unitPrice는 price, quantity로 돌아왔고 (동작으로부터 인자 순서가 추론됨), 두 개의 SECRET_* 상수는 taxRate와 threshold로 돌아왔습니다. 이는 코드의 동작 방식으로부터 추론되는 것이지 문자 그대로 저장되어 있는 것이 아니기 때문입니다. 리터럴 값(literal values)인 0.15와 100은 수정되지 않고 그대로 유지되었는데, 이는 VM이 실행을 위해 바이트코드 내에서 이 값들을 필요로 하기 때문입니다. 총 소요 시간은 약 10분이었습니다.

두 번째 목표: 완전히 다른 메커니즘을 가진 상용 엔터프라이즈 난독화기. 상용 엔터프라이즈 난독화기(유료 제품)를 대상으로 동일한 실험을 진행했습니다. 그들이 기본 보호 프로필을 광고하기 위해 배포하는 배경 스프라이트 아틀라스 (sprite-atlas) 모듈인 공개 데모를 사용했습니다. 프롬프트의 형태는 동일했으나, 이번에는 사고 과정이 더 심화되었습니다.

Claude는 파일을 한 번 읽고 구조를 파악했습니다: 네임스페이스 레지스트리 (namespace registry), URL 인코딩된 XOR 키 기반의 문자열 블롭 (string blob), 불투명한 정수 태그로 구성된 3D 인덱스 테이블 (3D index table), 정확히 8번의 호출을 위해 준비된 후 직접 조회 방식으로 퇴화하는 "자가 교체 디코더 (self-replacing decoder)", 그리고 실제 페이로드(payload)인 제어 흐름 평탄화 (control-flow-flattened)가 적용된 BACKGROUND라는 객체를 생성하는 상태 머신 (state machine)까지 말입니다. 이 중 그 어떤 것도 프롬프트에 포함되어 있지 않았습니다. 완전히 다른 프리미티브 (primitives)들이었지만 (바이트코드 VM (bytecode VM), RC4, 안티 디버깅 타이밍 (anti-debug timing) 등이 없음), 동일한 6단계의 흐름이 거의 그대로 적용되었습니다. 그리고 이번에는 계측 피벗 (instrumentation pivot)조차 필요하지 않았습니다. 정적 분석 (Static analysis)만으로 24,620 바이트를 다음과 같이 줄였습니다: var BACKGROUND = { HILLS : { x : 5 , y : 5 , w : 1280 , h : 480 }, SKY : { x : 5 , y : 495 , w : 1280 , h : 480 }, TREES : { x : 5 , y : 985 , w : 1280 , h : 480 }, }; 24KB를 입력하여 5줄을 출력하는 데 약 20분이 걸렸습니다. 왜 이것이 유지되지 않았을까요? 네 가지 구조적 이유가 있으며, 이 부분이 흥미로운데 왜냐하면 이것이 다음에 무엇이 작동하고 무엇이 작동하지 않을지를 예측해주기 때문입니다. 1. 역연산 (inverse)이 번들 (bundle) 내에 존재해야 합니다. 이 계열의 모든 방어 기제는 자체적인 복구 수단을 포함합니다. 암호화된 문자열을 위한 디코더, 옵코드 테이블 (opcode table)을 위한 셔플 키 (shuffle key), 환경 핑거프린트 (environment fingerprint)를 위한 소스 값 등이 그러합니다. 역연산이 없다면 프로그램이 실행될 수 없기 때문입니다. 이는 원칙적으로 이 계열 전체가 복구 가능하다는 것을 의미합니다. 유일한 실제 문제는 비용입니다. 2. 순차적 변환 (Sequential transforms)은 한 번에 하나씩 풀립니다. 레이어들이 t₁ ∘ t₂ ∘ … ∘ tₙ 과 같이 구성될 때, 역연산은 단순히 역순의 합성 (reverse composition)일 뿐입니다. LLM은 개별 변환 계열을 인식하고 역전시키는 데 능숙합니다. 문자열-배열 회전 (string-array rotation), RC4, base64, 상수 기반 XOR (XOR-with-constant), 시드 기반 피셔-예이츠 (seeded Fisher-Yates) 등은 이미 수천 개씩 학습했기 때문입니다. n개의 레이어를 격파하는 데는 대략 n 단위의 작업이 필요할 뿐, 진정으로 인터리브 (interleaved)된 변환에서 얻을 수 있는 2ⁿ 단위의 작업이 필요하지 않습니다. 3. VM은 단일 장애점 (single point of failure)입니다.

논리적인 프로그램은 JavaScript가 아닌 커스텀 바이트코드 (custom bytecode)로 되어 있지만, 바이트코드를 동작으로 변환하는 요소인 인터프리터 (interpreter)가 번들 안에 그대로 들어 있습니다. 디스패치 루프 (dispatch loop)를 한 번만 계측 (instrument)하면, 해당 인터프리터가 실행할 모든 함수가 디코딩됩니다. 안티 디버깅 (anti-debug) 타이밍 체크는 사람이 중단점 (breakpoint)을 찍고 천천히 단계별로 진행할 때만 작동합니다. 자동화된 계측 (instrumentation)은 전체 속도로 실행되므로 이를 절대 트리거하지 않습니다. 4. 기술적 진입 장벽 (skill floor)이 낮아졌습니다. LLM 이전에도 이 중 그 어떤 것도 깨뜨릴 수 없었던 것은 아닙니다. 제어 흐름 평탄화 (control-flow flattening)를 수동으로 역공학 (reversing)하는 방법에 대한 10년 된 논문들도 존재합니다. 변한 것은 누가, 얼마나 빨리 할 수 있느냐입니다. "도구를 갖춘 전문가 역공학자가 며칠 걸릴 일"이 "API 키를 가진 엔지니어가 커피 한 잔 마시는 시간 동안 할 일"이 되었으며, 기존에 역공학자들이 처음부터 직접 만들어내야 했던 고유 명사들은 이제 의미론적 문맥 (semantic context)을 통해 무료로 되살아납니다. webcrack 및 ben-sb/javascript-deobfuscator와 같은 자동화된 난독화 해제기 (deobfuscators)들이 이미 정적 계층 (static layers)을 처리해 왔으며, HumanifyJS는 LLM이 읽기 쉬운 이름을 다시 붙일 수 있음을 보여주었습니다. 코드를 직접 실행하는 에이전트 (agent)는 VM 및 안티 디버깅 측면의 격차를 메웁니다. 이는 Google이 프로덕션용 LLM 난독화 해제기인 CASCADE 및 JsDeObsBench 연구를 통해 나아간 방향과 동일하며, Elastic의 보안 팀과 해당 주제에 대한 RSAC 기사가 다루어 온 동일한 트레이드오프 (trade-off)입니다. 그들이 계속해서 제기하고 저 또한 동의하는 솔직한 주의 사항은 다음과 같습니다: LLM이 복구한 코드는 확신을 가지고 틀릴 수 있으므로, 반드시 검증해야 합니다.

첫 번째 실행에서 나타난 9개의 계층과 각 계층이 무너진 이유는 다음과 같습니다:

계층역할무너진 이유
1. 문자열 배열 + RC4약 500개의 모든 문자열 리터럴 (string literals)이 암호화되어 있으며, 런타임에 U(idx, key)를 통해 디코딩됨자급자족형 - U()를 한 번 평가하여 모든 호출을 정적으로 복호화 가능
2. 문자열 배열 회전 (String array rotation)체크섬 (checksum)이 일치할 때까지 IIFE에 의해 배열이 회전됨시작 시 실행됨. 실행되도록 두고 회전된 배열을 읽으면 됨
3. 환경 핑거프린팅 (Environment fingerprinting, h())내장된 .length 값들과 0x5f3759df를 XOR 연산함해당 값들은 모든 표준 JS 엔진에서 동일함. 실제적인 결합력이 없음
4.
  1. 바이트코드 암호화 (Bytecode encryption (RC4))
    h()로부터 파생된 키로 암호화된 바이트코드 블롭 (Bytecode blob) | h()가 실행되면 복호화는 단 한 줄로 끝남

  2. 바이너리 직렬화 (Binary serialization ( B() ))
    플래그 기반의 조건부 필드를 가진 커스텀 지그재그 가변 정수 (zigzag-varint) 형식 | 이를 재구현하지 말고, 인스트루멘테이션 (instrumentation) 하세요. 난독화 도구가 스스로를 해독하게 만드세요.

  3. 옵코드 셔플링 (Opcode shuffling ( b3 table + per-function seed))
    논리적 옵코드 (opcode)를 셔플된 인덱스에 매핑 | 시드 (seed) 기반의 의사 난수 생성기 (PRNG). 시드와 알고리즘을 확보하면 재구성 가능함

  4. 피연산자 XOR (Operand XOR ( blockKey , jumpKey ))
    모든 옵코드와 점프 타겟 (jump target)이 함수별 키로 XOR 처리됨 | 단순한 XOR 연산. 알려진 평문 (known-plaintext) 하나만 있으면 키를 복구할 수 있음

  5. 스택 값 암호화 (Stack value encryption ( seKey ))
    VM 스택에 푸시될 때 정수 값이 키와 함께 XOR 처리됨 | 정수에만 적용됨. 부동 소수점 (float), 문자열 (string), 객체 (object)는 평문으로 저장됨

  6. 안티 디버깅 타이밍 + VM 인터프리터 (Anti-debug timing + VM interpreter)
    단계별 실행 (stepping) 시 타이밍 체크가 옵코드 테이블을 손상시킴 | 약 1,500라인 규모의 커스텀 VM | 인스트루멘테이션 (instrumentation)은 전체 속도로 실행됨. VM은 그 반대임 - 디스패치 (dispatch)를 한 번만 인스트루멘테이션하면 모든 것을 얻을 수 있음

이것이 실제로 위협하는 것
내가 계속해서 되돌아오는 사례는 상수 (constants)와 제어 흐름 (control flow)이 지적 재산 (IP)인 경우입니다: 라이선스 확인 함수, 유료/무료 전환 게이트, 사용자가 기능을 사용할 수 있는지 결정하는 토큰 검증기 등입니다. 여기서 전체 방어 전략은 "읽기 어렵게 만드는 것"이었으나, 읽는 데 드는 비용이 불과 몇 자릿수(orders of magnitude)만큼 급감했습니다.

트레이딩 전략 코드, 랭킹 로직, 부정행위 방지 휴리스틱 (anti-fraud heuristics), 추천 알고리즘 - 브라우저로 전달되는 함수라면 무엇이든 동일한 범주에 속합니다. 브라우저 게임의 안티 치트 (anti-cheat) 및 DRM도 마찬가지입니다. 기존의 위협 모델은 공격자가 레이어당 며칠씩 작업해야 한다고 가정했으나, 이제 치트 개발자들은 몇 주 뒤처지는 대신 패치 주기 내에서 반복 작업을 수행할 수 있습니다. (아직 LLM 기반의 치트 개발에 대한 공개적인 증거를 보지는 못했지만, 비용의 기울기는 방어자에게 불리한 방향을 가리키고 있습니다.) 많은 난독화 구매 결정은 공격자가 레이어당 며칠간의 전문가 작업이 필요하다는 전제하에 가격이 책정되었습니다.

과거에는 어느 정도 수고를 들일 용의가 있는 공격자를 상대로 1년 정도의 시간적 여유를 벌어다 주었던 방어 기제들이, 이제는 Claude 계정을 가진 사람이라면 누구에게나 단 몇 시간 만에 뚫려버립니다. 제가 아직 테스트하지 않은 부분, 즉 유료 기능들은 별개의 문제입니다. 벤더(Vendor)들이 자신들의 기본 설정을 광고하기 위해 선정한 두 가지 제품과 두 개의 데모 파일만 확인했을 뿐, 그 위에 얹혀 있는 유료 기능들—환경 기반 실행 잠금(environment-bound execution locks), 자기 방어적 무결성 검사(self-defending integrity checks), 안티 디버깅 트랩(anti-debugging traps), 도메인 기반 실행 게이트(domain-bound execution gates)—은 아직 건드리지 않았습니다. 이러한 기능들은 각각 실질적인 장애물을 추가합니다. 또한 번들 전체에 역연산(inverses)을 흩뿌리고 얽히게 만들어 껍질을 벗기듯 층별로 분리할 수 없게 만드는 기술은 별도의 실험 과제입니다. 두 가지 모두 후속 연구를 진행할 가치가 있습니다. 하지만 상용 제품을 포함한 주류 JavaScript 난독화 도구들의 기본 프로필에 대해서는, 구조적인 주장이 유효하다고 생각합니다. 즉, 이들은 2026년의 유능한 LLM(대규모 언어 모델)을 상대로 유의미할 만큼 복구 비용을 높이지 못한다는 것입니다. (저는 몇 주 전, 압축(minified)된 코드 역시 결코 제대로 숨겨진 것이 아니었다는 이와 유사한 주제에 대해 글을 쓴 적이 있습니다.) 그리고 맞습니다, 저는 AfterPack에서 다른 전제를 바탕으로 현대적인 난독화 도구를 구축하고 있습니다. 바로 Rust를 사용하여 역연산들을 번들 전체에 매우 깊게 흩뿌리고 얽히게 함으로써, 복구 과정이 선형적(linear)이 아닌 조합론적(combinatorial, n^m)이 되도록 변환하는 것입니다. JavaScript는 여전히 실행되며 역연산 또한 여전히 존재하지만

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0