npm 수학 라이브러리에 암호화 로더가 숨겨진 이유
인기 수학 라이브러리 mathjs를 사칭한 npm 패키지 'mathmain'에서 원격 접근 백도어(임플란트)가 발견되었다. 악성 코드는 암호화되어 숨겨져 있으며, 특정 방정식을 풀 때 생성되는 행렬 데이터가 복호화 키로 사용되어야만 활성화된다. 유출된 페이로드는 공격자의 명령을 실행하며 공개 채팅 서비스와 블록체인 네트워크를 명령 채널로 활용한다.
블로그로 돌아가기
npm 수학 라이브러리에 암호화 로더가 왜 필요한가?
보안 | SafeDep 팀 • 2026년 9월 18일 • 11분 읽기
우리는 인기 수학 라이브러리인 mathjs를 복제한 npm 패키지 mathmain 내부에 숨겨진 원격 접근 임플란트를 발견했습니다. 악성 코드는 암호화된 형태로 배포되며, 프로그램이 이 라이브러리로 특정 방정식을 풀 때까지 잠복 상태로 유지됩니다. 그 방정식이 곧 키(key)입니다. 키가 일치하면 패키지는 페이로드를 복호화하여 실행합니다. 이 페이로드는 공격자로부터 명령을 받아 호스트에서 실행하며, 공개 채팅 서비스와 블록체인 네트워크를 명령 채널로 사용합니다. 이 글에서는 로더를 어떻게 발견했는지, 어떻게 복호화했는지, 페이로드가 무엇을 하는지, 그리고 이를 탐지할 수 있는 지표들을 소개합니다.
2026년 9월 17일, 우리는 SafeDep 분석을 통해 mathmain을 조사하기 시작했습니다. 이 패키지는 이름만 다르고 코드가 난독화된 mathjs의 복제본처럼 보였습니다. 솔버(solver)에 추가된 호출 하나가 우리를 로더로 이끌었습니다.
솔버가 타입 체크를 호출
lusolve() 함수의 거의 끝부분에서 CommonJS 빌드에 추가된 호출을 발견했습니다. 솔버는 이미 결과를 계산한 상태였고, 그 후 하삼각행렬(lower triangular matrix)의 데이터를 removeSolveValidation()에 전달하고 있었습니다. 여기서 l은 하삼각행렬을, x는 결과를 담고 있습니다. 솔버는 x를 변경 없이 반환하며, 추가 호출의 반환값을 q에 할당하지만 q는 다시 사용되지 않습니다.
removeSolveValidation()을 따라가 보니 lib/cjs/utils/is.js의 isGraph(x)로 연결되었습니다. 이 파일에는 isMatrix, isNumber 같은 체크 함수들이 포함되어 있는데, 추가된 isGraph() 함수가 코드를 복호화하여 로드하는 역할을 합니다.
isGraph()는 입력을 JSON 문자열로 변환하고 그 문자열을 비밀번호로 사용합니다. 먼저 파일명을 복호화한 다음, 파일 경로와 비밀번호를 event()에 전달하고, 반환된 경로를 require()로 로드합니다. 가독성을 위해 로더 코드 조각에서는 문자열을 복원하고 지역 변수명을 변경했습니다. 글 끝부분의 해시값이 원본 파일을 식별합니다.
행렬 데이터가 비밀번호가 된다
lib/cjs/utils/event.js에서 복호화 함수들을 발견했습니다. 이 함수들은 scrypt를 사용해 비밀번호를 256비트 키로 변환한 후, AES-GCM(Galois/Counter Mode)으로 데이터를 복호화합니다.
암호화된 데이터는 고정된 구조를 가집니다: 16바이트 salt, 12바이트 초기화 벡터(IV), 16바이트 인증 태그(tag), 그 뒤에 암호문이 이어집니다. 패키지는 이 전체 시퀀스를 base64 텍스트로 저장합니다.
솔버를 통한 호출의 경우 비밀번호는 JSON.stringify(L._data)입니다. 호출자는 lusolve()의 객체 형식으로 L을 전달할 수 있으므로, 올바른 비밀번호를 만들어내는 행렬 데이터를 넘겨야 합니다. 우리는 공개된 로더 코드 안에 저장된 비밀번호를 발견하지 못했습니다.
암호화된 파일명은 8바이트의 암호문입니다. 로더 옆에 있으며 파일명도 8바이트인 graph.js를 가리키는 것으로 추정되지만, 비밀번호를 복구하지 못해 확인하지는 못했습니다.
로더가 파일을 작성하고 실행
event() 헬퍼는 파일을 복호화하고, 결과를 디스크에 기록한 뒤 출력 경로를 반환합니다. 파일명에 enc_ 접두사가 없으면, 헬퍼는 암호화된 파일을 복호화된 코드로 덮어씁니다. 그런 다음 isGraph()의 require() 호출이 이를 로드하며, 그 코드는 Node.js 프로세스와 동일한 권한으로 실행됩니다.
세 개의 추가된 파일에는 일반적인 JavaScript 대신 base64 데이터가 담겨 있었습니다:
- lib/cjs/utils/ 하위 graph.js — 암호문 20,918바이트
- lib/cjs/utils/ 하위 fraction.js — 암호문 9,084바이트
- lib/cjs/utils/bignumber/ 하위 type.js — 암호문 1,179,416바이트
공개된 로더에서는 마지막 두 파일에 대한 참조를 찾지 못했습니다. 복호화된 graph.js가 페이로드 섹션에서 보여지듯 이후 단계로 이들을 로드합니다. 매니페스트에서는 설치 훅(install hook)을 발견하지 못했습니다.