3P by GN⁺ | ★ favorite | 댓글과 토론
  • LinkedIn에서 소프트웨어 엔지니어 채용을 제안한 뒤 정상적인 React/Web3 과제로 위장한 비공개 GitHub 저장소를 전달했으며, 실행하면 tailwind.config.js에 숨겨진 JavaScript 악성코드가 개발자 컴퓨터에서 작동함
  • 30,987바이트짜리 설정 파일은 수천 개의 공백 뒤에 27KB 난독화 코드를 숨겼고, 원격 서버에서 AES-256-CBC로 암호화된 2단계 페이로드를 받아 %TEMP%\pack에 저장한 뒤 node pack으로 실행
  • Windows 11 ARM 격리 VM에서 약 10분간 관찰한 결과, 페이로드는 원격 제어 백도어, 브라우저·지갑 탈취기, 재귀 파일 스캐너, 클립보드 감시기라는 네 구성 요소를 가동함
  • Socket.IO를 통해 터미널·SSH·화면·키보드·마우스를 제어하고 Chromium 데이터베이스, 지갑 확장 저장소, SSH 키, 소스 코드, 설정 파일 등을 수집했으며, 파일 업로드 포트에서 2,588건의 요청이 관찰됨
  • 낯선 사람이 보낸 과제 저장소는 비신뢰 코드로 취급해 실제 브라우저 프로필·SSH 키·클라우드 자격 증명·지갑이 없는 일회용 VM이나 컨테이너에서 검사해야 하며, 감염됐다면 네트워크 차단과 증거 보존 후 비밀 정보까지 교체해야 함

정상적인 Web3 채용 과제로 위장한 저장소

  • LinkedIn 사용자 Wayan Adrianshrapnel.com의 소프트웨어 엔지니어 직무를 제안하고, yevhen-o 계정의 비공개 GitHub 저장소 tech-active-workplace-frontend-main을 과제로 전달함
  • BLAIEXS라는 NFT 캠페인 플랫폼은 겉으로는 일반적인 React/Web3 프로젝트였음
    • React 프런트엔드는 브랜드·관리자 대시보드, 캠페인 관리, NFT 생성 흐름 등을 제공함
    • Express API는 인증, 대시보드 데이터, KYB 검사, NFT 생성·클레임, 파일 업로드와 일부 스텁 엔드포인트를 처리함
    • 시드 스크립트는 superadmin·브랜드·소비자 데모 계정, Demo NFT Drop 캠페인, 100개가 제공되는 Demo Collectible NFT를 생성함
  • 실제 과제 범위는 단순한 MetaMask 네트워크 표시 기능이었음
    • src/utils/ethereum.js의 비동기 getChainId()eth_chainId를 호출하고 파싱한 16진수 값 또는 null을 반환하도록 구현함
    • 같은 파일의 getNetworkLabel(chainId)가 기존 NETWORKS 객체에서 읽기 쉬운 네트워크 이름을 찾도록 구현함
    • src/components/Wallet/ConnectWalletButton.js가 지갑 연결 후 두 함수를 호출하도록 수정함
  • 구현을 마치고 개발 서버를 실행했지만 오랫동안 시작되지 않았고, 이상을 감지한 사용자는 인터넷 케이블을 분리함

tailwind.config.js에 숨겨진 실행 코드

  • ls -la에서 tailwind.config.js30,987바이트였지만 편집기에서는 97줄만 보였으며, wc -c로도 같은 크기를 확인함
  • 95번째 줄 부근에는 수천 개의 공백 뒤로 사람이 읽기 어려운 대형 JavaScript 블롭이 숨겨져 있었음
  • 코드는 일반적인 JavaScript 난독화 방식을 사용함
    1. 중요한 문자열을 큰 배열에 저장함
    2. 체크섬이 맞을 때까지 배열을 회전함
    3. 문자열 접근을 디코더 함수 뒤에 숨김
    4. 명확한 이름을 숫자 인덱스와 래퍼 호출로 교체함
  • 두 디코더 중 하나는 Base64 형태로 문자열을 복구하고, 다른 하나는 문자열별 키와 RC4 유사 스트림 암호를 적용했으며, 디코더 인덱스가 맞기 전에 문자열 배열을 회전시킴
  • 악성코드를 실행하지 않고도 다음 순서로 난독화를 해제할 수 있었음
    1. 문자열 배열을 추출함
    2. 예상 체크섬에 도달하도록 배열 회전을 재현함
    3. 디코더 함수를 다시 구현함
    4. b(0x214), c(0x2f1, "key") 같은 호출을 실제 문자열로 치환함
    5. 래퍼 객체와 도우미 함수를 단순화해 실행 의도를 드러냄

1단계 드로퍼의 동작

  • 해제된 코드는 child_process, os, fs, path, crypto를 불러오고 임시 디렉터리에 axiossocket.io-client를 설치함
  • 프로세스 수준의 uncaughtExceptionunhandledRejection 처리기는 오류를 무시하도록 구성됨
  • 원격 페이로드 실행 흐름은 다음과 같음
    • UID는 59e605dd78fb2aafccd1b622f06a00ca
    • http://45.146.252.17/api/service/59e605dd78fb2aafccd1b622f06a00ca에서 데이터를 가져옴
    • UID와 문자열 saltcrypto.scryptSync에 넣어 32바이트 키를 파생함
    • 응답을 base64_iv:base64_ciphertext 형식으로 분리하고 aes-256-cbc로 복호화함
    • 평문을 임시 디렉터리의 pack 파일에 기록함
    • child_process.execSync("node pack", { windowsHide: true, cwd: os.tmpdir() })로 실행함
  • 자체 기능을 수행하기보다 다른 악성 소프트웨어를 내려받아 실행하는 멀웨어 드로퍼
  • 다운로드한 코드에는 서명 검증, 해시 허용 목록, 출처 고정, 경로 제한, 실행 방지 가드가 없어서 원격 엔드포인트가 Tailwind 설정을 불러온 운영체제 계정의 권한으로 코드를 실행할 수 있음

격리 VM에서 수행한 동적 분석

  • 호스트 시스템을 노출하지 않도록 UTM으로 일회용 Windows 11 ARM VM을 구성함
    • 메모리 4096 MiB
    • 디스크 64 GiB
    • shared/NAT 네트워킹
    • 공유 폴더 비활성화
  • 여러 관점에서 악성코드 동작을 수집하도록 도구를 설치함
    • Wireshark: 패킷과 C2·유출 트래픽 수집
    • Sysinternals Sysmon: 지속 가능한 Windows 이벤트 원격 측정
    • Procmon: 실행 중 프로세스·레지스트리·파일시스템 활동 수집
  • 악성코드의 수집 대상을 확인하기 위해 미끼 데이터를 배치함
    • SSH 키 쌍
    • 비공개 GitHub 저장소
    • 암호화폐 지갑
    • 브라우저 데이터
    • 정보 탈취기가 노릴 만한 기타 파일
  • VM에서 yarn start를 실행하고 약 10분 동안 관찰한 뒤 run1-full.pcapng, run1-sysmon.evtx, stage2-pack.bin을 확보함

네트워크에서 확인된 감염 흐름

  • 45.146.252.17로 향한 HTTP 요청은 포트별로 역할이 나뉘어 있었음
    • 포트 80: 1단계 페이로드 가져오기, 호스트 등록, 로그 전송
    • 포트 7641: Socket.IO 명령·제어 백도어
    • 포트 7646: 파일 업로드 2,588건
    • 포트 7649: 더 크지만 수가 적은 브라우저 데이터 업로드 3건
  • 첫 요청은 GET /api/service/59e605dd78fb2aafccd1b622f06a00ca였으며, 응답은 Content-Length: 150897base64_iv:base64_ciphertext 형식이었음
  • tailwind.config.js는 응답을 AES-256-CBC로 복호화하고 평문을 %TEMP%\pack에 쓴 뒤 node pack으로 실행함
  • 확보한 2단계 페이로드의 특성은 다음과 같음
    • SHA-256: 68a64d8c015c06fd70bcb8c5878c1e430da827dd00b62f8e6ef69e76bb94de5b
    • 크기: 113136바이트
    • 형식: 한 줄로 구성된 ASCII JavaScript
  • 복호화된 pack도 다시 난독화돼 있었지만 첫 단계와 같은 기법을 사용해 동일한 절차로 해제할 수 있었음

네 개 프로그램으로 구성된 2단계 페이로드

  • pack은 단일 스크립트가 아니라 다음 네 프로그램을 실행하는 작은 프로세스 트리였음
    1. scdata 명령·제어 백도어를 임시 디렉터리에 기록하고 실행함
    2. ldata 브라우저·지갑 탈취기를 임시 디렉터리에 기록하고 실행함
    3. 재귀 파일 스캐너를 node -e로 메모리에서 직접 실행함
    4. 클립보드 감시기를 node -e로 메모리에서 직접 실행함
  • 주요 설정은 UID 59e605dd78fb2aafccd1b622f06a00ca, 사용자 키 308, 호스트 45.146.252.17, Socket.IO 포트 7641, 키 포트 7648, 브라우저 업로드 포트 7649, 파일 업로드 포트 7646이었음

scdata: 대화형 원격 제어 백도어

  • scdata%TEMP%에 파일로 기록된 뒤 npm i axios socket.io-client ... && node scdata로 실행되며, 장시간 동작하는 자식 프로세스로 남음
  • 실행 후 자신을 정상적인 프로세스처럼 보이도록 구성함
  • 원격 제어 기능에 필요한 추가 패키지도 설치함
    • socket.io-client, ssh2, node-pty: 터미널과 SSH 유사 기능
    • sharp, screenshot-desktop, clipboardy, @nut-tree-fork/nut-js: 스크린샷, 클립보드, 마우스 이동·클릭·스크롤, 키보드 입력
  • Socket.IO 이벤트는 원격 제어 범위를 직접 보여줌
    • 터미널: start-terminal, terminal-input, terminal-resize, stop-terminal, command
    • 호스트 확인과 캡처: whour, capture
    • 입력 제어: mouseMove, mouseClick, mouseScroll, keyTap, keyCombo
    • 클립보드: copyText, pasteText
    • SSH와 종료: start_ssh, ssh_input, kill
  • PCAP에서는 포트 7641의 폴링·WebSocket 핸드셰이크와 최초 서버 이벤트 whour가 확인됐고, Sysmon에서는 node.exe scdata, 시스템 정보 조회 PowerShell, 두 그룹의 npm 패키지 설치가 차례로 포착됨
  • 단순 정보 탈취를 넘어 공격자에게 셸과 데스크톱 제어 기능까지 제공함

ldata: 브라우저와 지갑 데이터 탈취기

  • ldata%TEMP%에 기록되고 npm i axios && node ldata로 실행되며, 프로세스 제목은 npm-cache
  • 브라우저 프로필과 지갑 확장 저장소를 수집해 http://45.146.252.17:7649/upload로 보내고, LevelDB 상태는 /cldbs에서 확인함
  • 대상 브라우저는 운영체제별로 폭넓게 설정돼 있음
    • Windows: Chrome, Edge, Brave, LT Browser
    • macOS: Chrome, Brave, Opera, LT Browser, Edge와 로컬 로그인 키체인
    • Linux: 대응하는 Chromium 계열 프로필 폴더
  • Default 또는 Profile* 디렉터리에서 다음 Chromium 데이터베이스를 업로드함
    • Login Data
    • Login Data For Account
    • Web Data
  • 이후 Local Extension Settings/<extension-id>를 탐색하며, 하드코딩된 지갑 확장 ID에는 MetaMask의 nkbihfbeogaeaoehlefnkodbefgpgknn도 들어 있음
  • 첫 8개 지갑 확장 경로는 LevelDB 디렉터리를 임시 폴더로 복사해 개별 파일을 업로드하고, 서버의 cldbs 응답에 따라 완료 여부나 재시도를 결정함
  • 포트 7649에서 확인된 세 업로드는 다음 데이터베이스였음
    • Login_Data.sqlite: 51,200바이트
    • Login_Data_For_Account.sqlite: 51,200바이트
    • Web_Data.sqlite: 262,144바이트
  • 실험 데이터에는 저장된 비밀번호나 카드 행이 없었지만 자동완성 값 6개와 브라우저 메타데이터가 포함돼 있었음
  • Chrome 계열 브라우저는 일부 필드를 로컬에서 암호화하므로 데이터베이스만으로 항상 평문 비밀을 복구할 수는 없음
    • 다만 운영체제 비밀·쿠키·확장 저장소·잠금 해제된 브라우저와 결합하면 자격 증명 탈취 파이프라인의 일부가 됨
  • 필요한 브라우저 데이터와 LevelDB를 업로드하거나 서버가 완료로 표시하면 종료되며, 운영자의 별도 명령을 기다리지 않음

재귀 파일 스캐너와 클립보드 감시기

  • 재귀 파일 스캐너는 별도 파일을 만들지 않고 node -e로 실행되며 사용자 홈 디렉터리부터 탐색함
    • Windows에서는 Get-CimInstance Win32_LogicalDisk로 드라이브 문자를 열거하고 각 드라이브 루트도 검사함
    • 수집 파일 패턴에는 *.env*, *.md, *.pem, *.ini, *.secret, *.json, *.js, *.ts, *.csv, *.txt, *.doc, *.docx, *.pdf, *.xlsx, .zsh_history, .bash_history가 포함됨
    • ~/.ssh, ~/.aws, ~/.azure, ~/.config, ~/.foundry는 자동으로 관심 폴더로 취급함
    • metamask, bitcoin, btc, solana, secret phrase, private key 같은 이름도 찾음
    • 결과는 http://45.146.252.17:7646/upload로 전송함
  • 미끼 환경에서는 id_ed25519, id_ed25519.pub, History.txt, seed.js, password.js, tokens.js, ConnectWalletButton.js, ExportWallet.js, RegisterWallet.js, tailwind.config.js, aptos-cli.json 등이 실제로 업로드됨
  • ldata가 브라우저와 지갑 저장소를 전담한다면 파일 스캐너는 SSH 키·설정·소스 코드·로컬 비밀·셸 기록·프로젝트 파일을 광범위하게 수집함
  • 클립보드 감시기node -e로 실행되며 프로세스 제목은 npm-compiler.log
    • 몇 초 기다린 뒤 반복해서 클립보드를 읽음
    • macOS에서는 pbpaste, Windows에서는 powershell -NoProfile -NonInteractive Get-Clipboard를 사용함
    • 변경된 값은 http://45.146.252.17/api/service/makelog로 보냄
  • 형식을 해석하지 않아도 사용자가 복사하는 비밀번호, 복구 문구, 개인 키, API 토큰, 일회용 코드, GitHub URL, 지갑 주소, 배포 비밀을 그대로 포착할 수 있음

채용 과제를 실행하기 전 지켜야 할 원칙

  • 낯선 사람이 보낸 과제 저장소는 안전성이 확인되기 전까지 비신뢰 실행 코드로 취급해야 함
  • yarn install, npm install, yarn build, yarn start 전에 다음 항목을 검토해야 함
    • package.json
    • 수명 주기 스크립트
    • 설정 파일
    • 명백한 의존성 훅
  • 이 사례의 악성코드는 사람들이 지나치기 쉬운 tailwind.config.js에 숨어 있었으며, 설정 파일·의존성·빌드 도구도 모두 코드로 실행될 수 있음
  • 면접 프로젝트는 실제 비밀이 마운트되지 않은 일회용 VM이나 컨테이너에서 실행해야 함
    • 개인 브라우저 프로필
    • 비밀번호 관리자
    • SSH 키
    • 암호화폐 지갑
    • GitHub 토큰
    • 클라우드 자격 증명
    • 은행 세션
    • 기본 이메일 계정
  • 계정이나 지갑이 필요한 과제에는 자금이 없고 다른 서비스에서 재사용하지 않은 새 테스트 신원을 사용해야 함
  • Web3 과제라면 테스트넷만 사용하고, 무해해 보여도 실제 지갑을 알 수 없는 프로젝트에 연결하지 않아야 함

감염 여부를 확인하는 지표

  • macOS·Linux에서는 다음 항목을 우선 확인함
    • node, pack, scdata, ldata, npm-compiler, vhost.ctl 관련 실행 프로세스
    • 45.146.252.17 또는 포트 7641, 7646, 7649와의 연결
    • 임시 디렉터리, Downloads, Desktop, Documents, Library 아래의 pack, scdata, ldata, vhost.ctl
    • */.npm/vhost.ctl 표시 파일
    • 내려받은 프로젝트 안의 IP, UID, /api/service/makelog, node scdata, node ldata, node pack 문자열
  • Windows에서는 다음 항목을 확인함
    • node, npm, cmd, powershell 프로세스 가운데 명령줄에 pack, scdata, ldata, node -e, IP 주소, Get-Clipboard가 있는 항목
    • 45.146.252.17 또는 원격 포트 7641, 7646, 7649로 열린 TCP 연결
    • %TEMP% 아래의 pack, scdata, ldata, vhost.ctl, .npm\vhost.ctl
    • 면접 저장소를 복제한 디렉터리 안의 알려진 C2 문자열
  • 살아 있는 Node 프로세스, 해당 IP 연결, pack·scdata·ldata 산출물 중 하나라도 발견되면 시스템이 노출된 것으로 간주해야 함
  • 이 지표는 분석한 샘플에 특화된 1차 점검 사항이며 완전한 포렌식 절차는 아님

감염 시스템 정리와 비밀 교체

  • 가장 먼저 컴퓨터를 네트워크에서 분리하고, 감염 환경에서 계속 탐색하거나 디버깅하거나 새 비밀을 복사하지 않아야 함
  • 증거가 필요하다면 삭제 전에 다음 자료를 보존함
    • 프로세스 목록
    • 네트워크 연결
    • 의심스러운 파일
    • 브라우저 기록
    • 악성 저장소
  • 이후 node pack, node scdata, node ldata, node -e, npm-compiler, vhost.ctl 관련 프로세스를 종료하고 임시 디렉터리의 pack, scdata, ldata, .npm/vhost.ctl을 제거함
  • 일회용 VM이고 정상적인 Node 작업을 보존할 필요가 없다면 macOS·Linux의 pkill node나 Windows의 전체 node 프로세스 강제 종료를 사용할 수 있음
  • 악성 저장소와 node_modules를 삭제하되, 추가 분석이 필요하면 ZIP 사본을 오프라인에 보존함
  • 파일 삭제만으로는 충분하지 않으며 다음 비밀 정보를 교체·폐기해야 함
    • SSH 키
    • GitHub·npm 토큰
    • 클라우드 키와 API 키
    • 배포 비밀
    • 브라우저에 저장된 비밀번호
    • 활성 로그인 세션
  • 지갑 시드나 개인 키가 감염 시스템에 닿았을 가능성이 있다면 깨끗한 장치에서 새 지갑을 만들고 자금을 이전해야 함

개발 도구의 신뢰 경계를 악용한 공격

  • 전통적인 백신 제품은 이 저장소를 탐지하지 않았고, 과제 해결에 사용된 인기 AI 코딩 에이전트도 프로젝트에 숨겨진 악성코드를 식별하지 못함
  • Tailwind는 JavaScript 설정 파일을 Node 모듈로 평가하므로, 개발 도구·빌드 스크립트·편집기 통합·Tailwind CLI·번들러·CI 작업이 설정을 불러오는 순간 95번째 줄의 IIFE가 실행됨
  • 정상적인 Tailwind 설정은 94번째 줄까지였고, 그 뒤에 약 27KB의 난독화 코드가 추가돼 있었음
  • 다운로드된 페이로드는 설정을 불러온 사용자와 같은 운영체제 권한으로 실행됨
    • 로컬 파일, 브라우저 프로필, 저장 자격 증명, 지갑 확장 데이터, SSH 키, 환경 변수, 패키지 토큰, 클라우드 자격 증명, 소스 코드, 클립보드에 접근할 수 있음
    • CI에서 실행되면 배포 비밀, 빌드 산출물, 레지스트리 자격 증명, 프로덕션 접근 토큰도 노출될 수 있음
  • 필요한 수정은 tailwind.config.js에서 난독화된 JavaScript 블롭을 완전히 제거하는 것

댓글과 토론