LLM 분석으로부터 함수 이름 숨기기
문제
vmObfuscation: true를 켜고 validateLicense 같은 함수가 들어 있는 파일을 난독화했더니, 난독화된 출력에
validateLicense라는 텍스트가 그대로 남아 있는 것을 발견했습니다. 본문은 사라지고 바이트코드로 바뀌었지만,
이름 자체는 누구나 볼 수 있는 곳에 그대로 있습니다.
이것이 실제 코드베이스 전체에 걸쳐 반복되면 validateLicense, decryptPayload, processPayment,
checkSubscription 같은 함수 이름 목록이 생깁니다. LLM은 프로그램이 무엇을 하는지 이해하기 위해 바이트코드를
해독할 필요가 없습니다. 이름만으로도 모듈의 동작을 자신 있고 정확하게 요약해 낼 수 있습니다. 바이트코드는
불투명하지만 목차는 그렇지 않습니다.
VM 난독화가 이 이름들을 유지하는 이유
vmTargetFunctionsMode: 'root'(기본값)에서 난독화 도구는 모든 루트 수준 함수의 본문을 VM 바이트코드로
변환하지만 이름은 의도적으로 건드리지 않습니다. 루트 수준 함수 선언은 의미상 주변 스코프의 바인딩입니다.
스크립트라면 전역 객체이고, 모듈이라면 모듈 스코프입니다
(내보내지 않은 최상위 함수는 전역이 아니라 모듈 스코프에 속하지만, 파일 안에서는 여전히 루트 수준입니다). 난독화 도구는 이 함수를 누가 참조하는지 알 방법이
없으므로 안전하게 이름을 바꿀 수 없습니다. 다른 번들, 인라인 <script>, HTML onclick="validateLicense(...)"
속성, 동적인 window['validateLicense'] 조회 등이 참조할 수 있기 때문입니다.
그래서 기본값이 택하는 절충은 구현은 보호하고 공개 인터페이스는 유지하는 것입니다. 이렇게 하면 통합이 깨지지 않지만,
LLM이 모든 진입점의 색인을 공짜로 얻게 되기도 합니다. 이런 일이 발생하면 난독화 결과에
VMGlobalFunctionNamesNotRenamed 경고가 보고되며, 읽을 수 있는 상태로 남은 이름들이 나열됩니다.
LLM을 활용한 리버스 엔지니어링에서 이것이 중요한 이유
수백 줄의 바이트코드 디스패치를 마주한 사람 공격자는 대개 포기합니다. 같은 파일을 받은 LLM은 바이트코드를 공략하려 들지조차 않습니다. 이름을 읽고, 보이는 몇 안 되는 문자열 리터럴과 대조한 다음, 다음과 같은 내용을 내놓습니다.
"이 모듈은 유료 기능에 대한 접근을 제어합니다. validateLicense는 서명된 토큰을 검증하고, checkExpiry는
만료된 라이선스를 거부하며, activateFeature는 검사를 통과하면 UI의 잠금을 해제합니다. 복호화 헬퍼는
decryptPayload에 있습니다."
이 요약만으로도 공격자는 VM을 전혀 건드리지 않고 표적 우회를 계획할 수 있습니다. 유출되는 것은 이름입니다.
해결 방법: 코드를 IIFE로 감싸기
이 유출을 없애는 가장 간단하고 확실한 방법은 민감한 함수를 스코프 트리의 한 단계 아래로 내리는 것입니다. 다른 함수 안에서 선언된 함수는 루트 수준이 아니므로, 난독화 도구는 다른 문장과 마찬가지로 자유롭게 이름을 바꾸고 선언을 바이트코드에 흡수할 수 있습니다.
IIFE(Immediately-Invoked Function Expression, 즉시 실행 함수 표현식)는 이를 위한 가장 가벼운 방법입니다. 한 번 실행되고 이름으로 아무것도 노출하지 않는 감싸는 함수 하나만 추가됩니다.
변경 전 - 이름이 노출됨
VM 난독화 후에도 validateLicense와 checkExpiry는 모두 출력에 이름이 그대로 남습니다.
변경 후 - 이름이 IIFE 뒤에 숨겨짐
이제 두 함수 선언은 모두 IIFE 본문 안에 있습니다. 루트 수준의 구성 요소는 IIFE 자체뿐이며, 익명 IIFE에는 유출될 이름이 없습니다. VM 난독화 후에는 함수 이름이 바뀌고 본문은 바이트코드로 변환됩니다.
달라진 점. 함수들에 더 이상 전역으로 접근할 수 없습니다(바로 이것이 이름이 유출되던 원인이었습니다). 여전히
호출할 수는 있지만 같은 IIFE 안에서만 가능합니다. 외부에서 validateLicense를 실제로 호출해야 하는 무언가가 있었다면
이제는 그 함수에 접근할 수 없습니다. 아래의 트램펄린 패턴을 참고하세요.
그런 것이 없었다면 잃은 것은 없습니다.
내보낸 이름, 예약된 식별자, 문자열, 관찰 가능한 동작은 여전히 보일 수 있습니다. 감싼 뒤에는 통합을 테스트하세요. 특히 전역, 모듈 내보내기, 함수 이름을 검사하는 코드를 확인하세요.
알아 둘 트레이드오프: IIFE 본문에 직접 eval이 있거나, 본문이 동적인 new Function(...) / Function(...) 호출이 있으면
런타임에 만들어지는 소스가 난독화 도구가 이름을 바꾼 식별자를 참조할 수 있으므로, VM 난독화는 그 함수 전체와 그 안에 중첩된
모든 것을 제외합니다(VMDynamicCodeSkipped 경고). 그러면 방금 IIFE 안으로 옮긴 코드는 일반 난독화로 되돌아가
바이트코드 보호를 잃습니다. 간접 eval((0, eval)(...))을 사용하거나, 선택지는
VM 난독화에서의 직접 eval을 참고하세요.
함수가 정말로 전역이어야 한다면?
함수가 실제로 공개 진입점인 경우도 있습니다. 인라인 이벤트 핸들러, JSONP 콜백, 서드파티 SDK 훅 등입니다. 두 가지 선택지가 있습니다.
얇은 트램펄린만 노출하고 로직은 IIFE 안에 두세요. IIFE 스코프의 구현을 호출하는 것이 유일한 역할인 작은 전역 래퍼를 선언하세요. 트램펄린 이름은 여전히 유출되지만 아무런 의미 정보도 담고 있지 않으며(
__entry1같은 이름을 붙이세요), 의미 있는 로직은 모두 숨겨진 채로 남습니다.호출 지점을 다시 작성하세요. 인라인
onclick="validateLicense(...)"가 필요로 하기 때문에만 전역이 존재한다면, 인라인 핸들러를 IIFE 안에서 호출하는addEventListener로 바꾸세요. HTML은 더 이상 함수 이름을 언급하지 않고, 함수는 더 이상 전역일 필요가 없으며, 유출은 완전히 사라집니다.
최상위 변수 초기화 식: vmWrapTopLevelInitializers
파일의 루트에 있는 것은 함수 선언만이 아닙니다. 문자열 상수, 설정 객체, 조회 테이블 같은 최상위 변수 초기화 식도
평범한 JavaScript로 남아 있으면 출력에서 똑같이 읽힙니다. const API_BASE = '/api/v2/license' 같은 한 줄은
function validateLicense만큼이나 많은 것을 LLM에 알려 줍니다.
vmWrapTopLevelInitializers 옵션(boolean, 기본값 false, 현재 VM 프리셋에서는 켜져 있음)은 대상이 되는 최상위
초기화 식을 IIFE로 감싸서, 값이 소스에 리터럴로 남는 대신 런타임에 VM 바이트코드로 계산되도록 합니다. root 모드에서 평범한
JavaScript로 남는 초기화 식은 VMTopLevelInitializerNotVirtualized 경고로 보고됩니다.
옵션을 사용하지 않을 때
vmWrapTopLevelInitializers: true를 사용할 때
바인딩 이름(MY_STRING)은 함수 이름과 같은 이유로 여전히 루트 수준에 남습니다. 파일 밖의 무언가가 참조할 수 있기
때문입니다. 하지만 이 바인딩이 담는 _값_은 이제 VM이 만들어 내므로 더 이상 읽을 수 있는 텍스트로 나타나지 않습니다.
vmTargetFunctionsMode가 'root'(기본값)이고 vmAsyncExecutor가 꺼져 있을 때만 효과가 있습니다. comment 모드에서는
이 옵션이 아무 효과가 없으며 경고도 보고되지 않습니다. root 모드에서 vmAsyncExecutor(async 함수만 가상화함)를 사용할 때도
아무 효과가 없으며, 빌드는 일반 JavaScript로 남은 초기화 코드에 대해 VMTopLevelInitializerNotVirtualized를 보고합니다.
파일에 VM이 가상화할 함수가 없으면 결과에 VMNoFunctionsToVirtualize가 보고됩니다. 보호하려는 코드를 함수로 감싸거나,
comment 모드를 사용해 민감한 함수를 명시적으로 선택하세요.
이것만으로 충분하지 않은 경우
- 다른 모듈에서 가져온 이름. 여러 파일을 번들링할 때 한 모듈이 다른 모듈이 가져갈 수 있도록
validateLicense를 내보낸다면, 번들러는 루트 수준 함수가 보이는 것과 같은 방식으로 번들 출력에 그 이름을 남겨 둡니다. 번들 자체를 IIFE로 감싸거나(대부분의 번들러가 지원합니다), 내보내기를 IIFE 안으로 옮기고 의미 없는 트램펄린으로 다시 노출하세요. - 런타임은 여전히 관찰할 수 있습니다. 난독화는 코드를 이해하고 수정하는 데 필요한 노력을 늘리지만, 리버스 엔지니어링이 불가능하다고 보장할 수는 없습니다. 비밀 값과 최종적인 보안 판단은 서버에 두세요.
renameGlobals부터 꺼내 들지 마세요. 이 옵션은 존재하고 루트 수준 식별자의 이름을 바꾸기도 하지만, 그 식별자 중
어느 것이 파일 밖(다른 번들, 인라인 HTML, 동적 조회)에서 참조되는지 알 방법이 없습니다. 이 옵션을 켜면 미묘한 방식으로
통합이 깨지는 경우가 많습니다. IIFE로 감싸는 편이 더 안전합니다. 전역의 이름은 전혀 바꾸지 않고, 필요 없던 전역을
만들지 않을 뿐입니다.
