TECH 으로 돌아가기
TECH HACKER NEWS 오늘 7분 읽기 22 READS

수학 방정식이 열쇠였다: mathjs 사칭 npm 패키지에 숨은 원격제어 임플란트

수학 방정식이 열쇠였다: mathjs 사칭 npm 패키지에 숨은 원격제어 임플란트
SOURCE IMAGE · HACKER NEWS

오픈소스 공급망 공격이 점점 정교해지고 있다. SafeDep는 2026년 9월 17일 분석에서 npm에 올라온 mathmain 패키지가 널리 쓰이는 mathjs 라이브러리를 그대로 베끼되 이름만 바꾸고 코드를 난독화했다는 점을 포착했다. 겉보기에는 평범한 수학 계산 라이브러리지만, 그 안에는 암호화된 원격제어 임플란트가 잠들어 있었다. 이 악성코드는 설치만으로는 작동하지 않고, 특정 방정식을 라이브러리로 풀어야만 깨어난다. 그 방정식의 계산 결과가 곧 복호화 열쇠다.

잠들어 있는 로더

단서는 선형방정식 풀이 함수 lusolve()의 끝부분에 있었다. 솔버가 결과를 이미 계산한 뒤, 하삼각행렬(lower triangular matrix) 데이터를 removeSolveValidation()이라는 추가 호출에 넘긴다. 반환값은 변수 q에 담기지만 이후 어디에서도 쓰이지 않는다. 이 호출은 다시 is.js 안의 isGraph() 함수로 이어지는데, 이 파일은 원래 isMatrix, isNumber 같은 타입 검사 함수가 모여 있는 곳이다. 공격자는 그 사이에 코드를 복호화하고 로드하는 함수를 끼워 넣었다.

isGraph()는 입력값을 JSON 문자열로 바꾼 뒤 그 문자열을 비밀번호로 사용한다. 먼저 파일 이름을 복호화하고, 그 경로와 비밀번호를 event() 헬퍼에 넘긴 다음 require()로 로드한다. 복호화에는 scrypt로 비밀번호를 256비트 키로 늘린 뒤 AES-GCM(Galois/Counter Mode) 방식을 쓴다. 암호화된 데이터는 16바이트 솔트, 12바이트 초기화 벡터, 16바이트 인증 태그에 이어 암호문이 오는 고정 구조를 base64로 저장한다. 비밀번호가 틀리면 인증 검증 단계에서 실패해 파일을 쓰지 않고 멈춘다. 그래서 단순 import나 다른 데이터를 넣은 평범한 lusolve() 호출로는 이 코드에 결코 도달하지 못한다.

열쇠는 파스칼 행렬

로더 어디에도 비밀번호는 저장돼 있지 않았다. SafeDep는 파일 이름 하나에 1만6922가지, 다섯 개의 암호문 전체에 533가지 후보를 대입해 봤지만 모두 실패했다. 결국 정답은 흔한 값이나 테스트 행렬이 아니라 특정 행렬 하나였다. JFrog가 먼저 그 입력을 보고했고, SafeDep가 mathmain 파일에 대해 재현했다. 열쇠는 3×3 파스칼 행렬 A를 lusolve()에 넣었을 때 나오는 LU 분해의 하삼각 인수 L, 즉 [[1,0,0],[1,1,0],[1,0.5,1]]을 JSON 문자열로 바꾼 값이었다. 이 비밀번호로 파일명은 graph.js로, 그리고 세 개의 페이로드 파일이 모두 복호화됐다. mathmain의 페이로드는 math-universe의 것과 바이트 단위로 일치했다.

페이로드가 하는 일

복호화된 코드는 작은 원격제어 임플란트를 이룬다. 1단계 graph.js는 os·fs로 호스트 정보를 읽고, Node crypto로 X25519 키쌍을 만들며, child_process로 셸 명령을 실행한다. 또 번들된 ethers 라이브러리로 Base Sepolia 테스트넷의 스마트 컨트랙트를 읽고, Slack의 chat.postMessage와 텔레그램 API로 보고한다. 실제 명령 에이전트는 fraction.js다. 이 파일은 실행 시점에 Slack 봇 토큰과 채널 ID를 복호화하며, 환경변수 CHAT_PASSWORD가 없으면 종료된다. 값이 있으면 10초마다 Slack의 conversations.history를 폴링해 운영자가 보낸 메시지를 셸 명령으로 실행한다. 명령 자체는 패키지에 저장되지 않고 런타임에 Slack에서 읽어오므로, 레지스트리 사본만 봐서는 실제로 무엇을 실행했는지 알 수 없다.

실무자가 새겨야 할 점

이 사건에서 특히 눈여겨볼 대목은 '기본 버전 확인'의 함정이다. 9월 17일 npm이 mathmain의 기본 릴리스로 제공한 버전에는 로더가 없었고, 문제의 코드는 1.0.1에만 들어 있었다. 기본 버전만 검사했다면 놓쳤을 것이다. 또한 mathsbase와 math-universe가 연결한 공개 GitHub 저장소에는 로더가 없었다. 즉 공개 소스에는 정상 코드를 두고, npm에 게시하는 순간 악성 코드를 끼워 넣은 것이다. 소스 저장소가 깨끗하다는 사실만으로 배포된 아티팩트를 신뢰할 수 없다는 뜻이다. 다운로드 수치 역시 신뢰하기 어렵다. 9월 17일에는 레지스트리 전체 다운로드가 0으로 보고됐고, 세 패키지 모두 공개 의존 프로젝트나 jsDelivr 트래픽이 거의 없어 실제 설치 규모를 가늠할 수 없었다.

두 배포판(mathmain·math-universe와 별도 버전)은 동일한 Alchemy 프로젝트 키 D2-TbkB2m05WXSnSDOCDI를 공유했고, 이것이 두 배포를 한 운영자로 묶는 단서가 됐다. 다만 SafeDep는 누가 로더를 넣었는지, 게시자 계정이 탈취된 것인지까지는 확인하지 못했다. 공격은 두 부분으로 나뉜다. 한 패키지가 암호화된 페이로드를 품고, 두 번째 패키지나 감염된 호출자가 트리거 행렬로 솔버를 실행해 열쇠를 공급한다. 신뢰받는 인기 라이브러리를 사칭하면서 활성화 조건을 코드 밖 호출자에게 떠넘긴 이 구조 때문에, SafeDep의 GitHub·의존성 검색으로도 그 호출자를 찾지 못했다. 결국 패키지나 해시가 일치한다고 해서 코드가 실제로 실행됐다는 증거가 되지는 않는다. 방어 관점에서는 lock 파일 단위의 정확한 버전 검증, 게시 아티팩트와 소스 대조, 그리고 os·child_process·crypto를 동시에 끌어들이는 수상한 호출 패턴 탐지를 병행하는 것이 현실적인 대응이다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://safedep.io/mathmain-encrypted-loader/
SHARE
NEXT · CHOOSE

변화를 읽었다면,
내가 만들 수익 구조를 고릅니다.

정보를 더 모으는 데서 멈추지 않고, 광고·외주·판매·중개·구독 중 내 상황에 맞는 출발점을 정해보세요.

21가지 수익 구조 살펴보기
처리 중...