11월에는 npm 패키지인 event-stream이 악성 종속성인 flatmap-stream을 통해 악용되었습니다. 전체 과정은 여기 에 기록되었으며, 이 글의 초점은 이를 JavaScript 역엔지니어링의 사례 연구로 사용하는 것입니다. flatmap-stream과 관련된 3개의 페이로드는 작성하기 쉬울 만큼 간단하면서도 흥미로울 만큼 복잡합니다. 이 글을 이해하기 위해 이 사건의 배경 이야기를 알 필요는 없지만, 세부 내용을 잘 모르는 사람이라면 명확하지 않을 수 있는 가정을 하겠습니다.
대부분의 JavaScript를 리버스 엔지니어링하는 것은 데스크톱 OS에서 실행할 수 있는 이진 실행 파일보다 간단합니다. 결국 소스가 바로 눈앞에 있으니까요. 하지만 이해하기 어렵게 설계된 JavaScript 코드는 종종 의도를 숨기기 위해 몇 번의 난독화 과정을 거칩니다. 이러한 난독화 중 일부는 "축소화"라고 하는 것에서 비롯됩니다. 최소화는 공간 절약을 목적으로 소스의 전체 바이트 수를 최대한 줄이는 프로세스입니다. 이는 변수를 단일 문자 식별자로 줄이고 true와 같은 표현식을 !0과 같이 더 짧지만 동등한 것으로 변환하는 것을 포함합니다. 축소는 웹 브라우저에서 유래되었기 때문에 주로 JavaScript 생태계에서만 나타나는 고유한 기능이며 도구 재사용으로 인해 노드 패키지에서 가끔 볼 수 있으며 보안 조치로서 의도된 것은 아닙니다. 일반적인 축소 및 난독화 기술의 기본적인 역전 방법을 알아보려면 Shape의 축소 해제 도구를 확인하세요. 전용 난독화 패스는 난독화를 위해 설계된 도구에서 제공되거나 개발자가 수동으로 수행할 수 있습니다.
첫 번째 단계는 분석을 위해 격리된 출처를 확보하는 것입니다. flatmap-stream 패키지는 패키지 버전 0.1.1에만 포함된 악성 페이로드를 제외하고는 무해해 보이도록 특별히 제작되었습니다. 버전 0.1.2 와 버전 0.1.1을 비교하거나 두 탭에서 URL을 번갈아가며 확인하면 소스의 변경 사항을 빠르게 확인할 수 있습니다. 이 게시물의 나머지 부분에서는 첨부된 소스를 페이로드 A라고 부르겠습니다. 아래는 페이로드 A의 포맷된 소스입니다.
먼저 중요한 것부터: 절대로 악성 코드를 실행하지 마세요 (절연된 환경 제외). Shift 파서와 JavaScript 변환기를 사용하여 코드를 동적으로 리팩토링하는 데 도움이 되는 도구를 직접 작성했지만 이 게시물의 내용을 따라가려면 Visual Studio Code와 같은 IDE를 사용할 수 있습니다.
자바스크립트를 리버스 엔지니어링할 때는 정신적 잡담을 최소화하는 것이 중요합니다. 이는 즉각적인 가치를 더하지 않는 모든 표현이나 명령문을 제거하고 자동 또는 수동으로 최적화된 모든 코드의 DRYness를 되돌리는 것을 의미합니다. 우리는 머릿속에서 JavaScript를 정적으로 분석하고 실행을 추적하기 때문에 정신적 스택이 깊어질수록 길을 잃을 가능성이 커집니다.
가장 간단한 작업 중 하나는 3번째 줄과 4번째 줄에서와 같이 require 및 process와 같은 전역 속성이 할당된 변수의 최소화를 취소하는 것입니다.
리팩토링 기능을 제공하는 모든 IDE에서 이 작업을 수행할 수 있습니다(일반적으로 이름을 바꾸려는 식별자 위에서 "F2"를 눌러서). 그 후에, 우리는 e라는 함수 정의를 볼 수 있는데, 이는 단순히 16진수 문자열을 디코딩하는 것처럼 보입니다.
첫 번째 흥미로운 코드 줄은 문자열 "2e2f746573742f64617461"을 디코딩하는 함수 e의 결과에서 나오는 파일을 가져오는 것으로 보입니다.
의도적으로 난독화된 JavaScript에서 문자열 값을 숨기는 것은 매우 흔한 일로, 지나가는 사람이 쉽게 볼 수 있는 특히 불길한 문자열이나 속성에 대해 경고를 받지 못하도록 하는 것입니다. 대부분 개발자는 이것이 매우 낮은 장벽이라는 것을 알고 있기 때문에 쉽게 취소할 수 없는 인코딩을 종종 발견하게 되는데, 여기서도 다르지 않습니다. e 함수는 단순히 16진수 문자열을 반전시키는데, 온라인 도구 나 사용자가 편리하게 사용할 수 있는 기능을 이용해 수동으로 반전시킬 수 있습니다. e 함수의 기능을 이해했다고 확신하더라도 악성 파일에서 찾은 입력으로 해당 함수를 실행하지 않는 것이 좋습니다(추출한 경우에도 마찬가지입니다). 공격자가 데이터로 인해 발생하는 보안 취약점을 찾지 못했다는 보장이 없기 때문입니다.
문자열을 반전하면 스크립트에 배포된 npm 패키지에 위치한 './test/data'라는 데이터 파일이 포함되어 있음을 알 수 있습니다.
n을 data로 이름을 바꾸고 e(n[2])에 대한 호출을 e(n[9])로 난독화 해제하면 여기서 다루고 있는 내용이 무엇인지 더 잘 알 수 있습니다.
이러한 문자열이 숨겨진 이유도 쉽게 알 수 있습니다. 간단한 플랫맵 라이브러리에서 암호 해독에 대한 참조를 찾는 것은 무언가가 매우 잘못되었다는 것을 알려주는 확실한 단서가 될 것입니다.
여기서 스크립트가 node.js의 "crypto" 라이브러리를 가져오는 것을 볼 수 있으며 API를 찾은 후 createDecipher의 두 번째 인수인 o가 암호를 해독하는 데 사용되는 비밀번호임을 알 수 있습니다. 이제 우리는 해당 인수와 다음 반환 값을 API에 기반한 합리적인 이름으로 바꿀 수 있습니다. 퍼즐의 새로운 조각을 찾을 때마다 리팩터링이나 주석을 통해 그것을 영원히 간직하는 것이 중요합니다. 사소해 보이는 이름을 바꾼 변수일지라도요. 외국 코드를 몇 시간 동안 살펴보다 보면 작업 내용을 잃어버리거나 주의가 산만해지거나 잘못된 리팩토링으로 인해 되돌아가야 하는 경우가 매우 흔합니다. 리팩터링 중에 git을 사용하여 체크포인트를 저장하는 것도 가치가 있지만 그 결정은 여러분에게 맡기겠습니다. 이제 코드는 다음과 같습니다. e 함수는 더 이상 if (!o) {... 문과 함께 사용되지 않기 때문에 삭제되었습니다. 분석에 가치를 더하지 않기 때문입니다.
또한 f를 newModuleInstance로 이름을 바꾼 것도 눈에 띄실 겁니다. 이렇게 짧은 코드라면 그다지 중요하지 않지만, 수백 줄에 달하는 코드라면 모든 것을 가능한 한 명확하게 설명하는 것이 중요합니다.
이제 페이로드 A의 난독화가 대부분 해제되어 이를 통해 페이로드 A의 기능을 이해할 수 있습니다.
3번째 줄은 외부 데이터를 가져옵니다.
4번째 줄은 환경에서 비밀번호를 가져옵니다. process.env를 사용하면 노드 스크립트 내에서 변수에 액세스할 수 있으며 npm_package_description은 package.json 파일에 정의된 스크립트를 실행할 때 노드의 패키지 관리자인 npm이 설정하는 변수입니다.
5번째 줄에서는 npm_package_description의 값을 비밀번호로 사용하여 decipher 인스턴스를 생성합니다. 즉, 암호화된 페이로드는 이 스크립트가 npm을 통해 실행되고 package.json에 특정 설명 필드가 있는 특정 프로젝트에 대해 실행되는 경우 에만 해독할 수 있습니다. 힘들겠네요.
6번째와 7번째 줄은 외부 파일의 첫 번째 요소를 복호화하고 이를 변수 "decrypted"에 저장합니다.
8~11번째 줄은 새로운 모듈을 생성한 다음 복호화된 데이터를 문서화되지 않은 메서드인 _compile에 공급합니다. 그런 다음 이 모듈은 외부 데이터 파일의 두 번째 요소를 내보냅니다. module.exports는 노드의 한 모듈에서 다른 모듈로 데이터를 노출하는 메커니즘이므로 newModuleInstance.exports(data[1])는 외부 데이터 파일에서 발견된 두 번째 암호화된 페이로드를 노출합니다.
이 시점에서 우리는 package.json에서 찾은 비밀번호로만 복호화 가능한 암호화된 데이터를 가지고 있으며, 이 복호화된 데이터는 _compile 메서드에 입력됩니다. 이제 우리에게는 하나의 문제가 남았습니다. 비밀번호를 알 수 없는 경우 어떻게 데이터를 해독할 수 있을까요? 이건 사소한 질문이 아닙니다. 만약 무차별 대입 공격을 통해 AES256 암호화를 쉽게 풀 수 있다면 npm 패키지가 장악되는 것보다 더 많은 문제가 생길 것입니다. 다행히도 우리가 다루는 것은 완전히 알려지지 않은 비밀번호가 아니라, 어딘가에 package.json에 입력된 문자열일 뿐입니다. package.json 파일은 원래 npm 패키지 메타데이터의 파일 형식으로 만들어졌으므로 공식 npm 레지스트리에서 시작해도 됩니다. 다행히 모든 패키지 메타데이터 의 스트림을 제공하는 npm 패키지가 있습니다.
대상 파일이 npm 패키지에 있다는 보장은 없습니다. 많은 비 npm 프로젝트는 노드 기반 도구의 구성을 저장하기 위해 package.json을 사용합니다. 또한 package.json 설명은 버전마다 다를 수 있지만, 시작하기에 좋은 위치입니다. 여러 개의 키를 사용하여 이 페이로드를 해독하면 난해한 내용이 나올 수 있으므로 이러한 무차별 대입 공격 과정에서 해독된 페이로드의 유효성을 검사할 방법이 필요합니다. Module.prototype._compile 에 공급된 내용이 vm.runInThisContext 에 공급 되므로 출력이 JavaScript라고 합리적으로 가정할 수 있으며 JavaScript 파서를 사용하여 데이터의 유효성을 검사할 수 있습니다. 비밀번호가 실패하거나 성공했지만 파서가 오류를 발생시키는 경우 다음 package.json으로 이동해야 합니다. 편리하게도 Shape Security는 JavaScript 및 Java 환경에서 사용할 수 있는 자체 JavaScript 파서 세트를 구축했습니다. 사용된 무차별 대입 스크립트는 다음과 같습니다.
92.1초 동안 실행하고 740543개의 패키지를 처리한 후, 우리는 "안전한 비트코인 지갑"이라는 비밀번호를 얻었고, 이는 아래에 포함된 페이로드를 성공적으로 디코딩했습니다.
이건 행운이군요. 괴물같이 엄청난 난제였을 수도 있는 문제가 백만 번 미만의 반복만으로 해결되었습니다. 문제의 키가 포함된 영향을 받은 패키지는 결국 비트코인 지갑인 Copay의 클라이언트 애플리케이션으로 밝혀졌습니다. 다음 두 개의 페이로드는 애플리케이션 자체에 더욱 깊이 파고들며, 대상 애플리케이션이 비트코인을 저장하는 것을 중심으로 하고 있다는 점을 감안하면 아마도 어디로 향할지 짐작할 수 있을 것입니다.
이와 같은 주제가 흥미롭고 다른 두 가지 페이로드나 향후 공격에 대한 분석을 읽고 싶다면 이 게시물에 "좋아요"를 누르거나 @jsoverson 에서 Twitter로 알려주세요.