flatmap-stream 페이로드 A
11월에 npm 패키지 event-stream이 악성 종속성인 flatmap-stream을 통해 공격당했습니다. 이 사건의 전말은 여기에 정리되어 있으며, 이 글에서는 이를 JavaScript 리버스 엔지니어링의 사례 연구로 활용하고자 합니다. flatmap-stream과 관련된 세 가지 페이로드는 글로 설명하기에 충분히 단순하면서도 흥미를 끌 만큼 복잡합니다. 이 글을 이해하는 데 사건의 배경을 반드시 알아야 하는 것은 아니지만, 세부 내용을 어느 정도 알고 있지 않다면 명확하지 않을 수 있는 몇 가지 사항을 전제로 설명하겠습니다.
대부분의 JavaScript를 리버스 엔지니어링하는 작업은 데스크톱 OS에서 실행할 수 있는 바이너리 실행 파일을 분석하는 것보다 더 간단합니다. 무엇보다 소스가 바로 눈앞에 있기 때문입니다. 하지만 이해하기 어렵도록 설계된 JavaScript 코드는 그 의도를 감추기 위해 여러 단계의 난독화를 거치는 경우가 많습니다. 이러한 난독화 중 일부는 공간을 절약하기 위해 소스의 전체 바이트 수를 최대한 줄이는 과정인 ‘최소화(minification)’에서 비롯됩니다. 여기에는 변수 이름을 한 글자 식별자로 줄이고 true 같은 표현식을 더 짧지만 동일한 의미의 !0 같은 표현식으로 바꾸는 작업이 포함됩니다. 최소화는 웹 브라우저에서 유래한 JavaScript 생태계에서 주로 나타나는 고유한 특성이며, 도구를 재사용하면서 node 패키지에서도 간혹 볼 수 있지만 보안 조치를 목적으로 하지는 않습니다. 일반적인 최소화 및 난독화 기법을 기본적으로 되돌리려면 Shape의 unminify 도구를 확인해 보세요. 별도의 난독화 단계는 난독화 전용 도구를 통해 적용되거나 개발자가 직접 수행할 수 있습니다.
첫 번째 단계는 분석을 위해 분리된 소스 코드를 확보하는 것입니다. flatmap-stream 패키지는 패키지의 단 하나의 버전인 0.1.1에만 악성 페이로드가 포함되어 있다는 점을 제외하면 정상적으로 보이도록 의도적으로 제작되었습니다. 버전 0.1.2와 버전 0.1.1의 차이를 비교하거나, 두 탭에서 URL을 번갈아 확인하기만 해도 소스 코드의 변경 사항을 빠르게 파악할 수 있습니다. 이 글의 나머지 부분에서는 추가된 소스 코드를 페이로드 A라고 부르겠습니다. 아래는 형식을 정리한 페이로드 A의 소스 코드입니다.

무엇보다 먼저, 악성 코드는 절대로 실행하지 마세요 (격리된 환경은 예외입니다). 저는 Shift 파서 및 JavaScript 변환기 모음을 사용해 코드를 동적으로 리팩터링하는 데 도움이 되는 도구를 직접 만들었지만, 이 글을 따라 해 보는 데는 Visual Studio Code 같은 IDE를 사용해도 됩니다.
JavaScript를 리버스 엔지니어링할 때는 머릿속에서 동시에 처리해야 할 내용을 최소화하는 것이 중요합니다. 즉, 당장 도움이 되지 않는 표현식이나 문을 제거하고, 자동 또는 수동으로 최적화된 코드에 적용된 DRY 원칙도 되돌려야 합니다. JavaScript를 정적으로 분석하면서 실행 과정을 머릿속으로 추적해야 하므로, 머릿속의 스택이 깊어질수록 길을 잃기 쉽습니다.
가장 간단하게 할 수 있는 작업 중 하나는 3번과 4번 줄처럼 require 및 process와 같은 전역 속성이 할당된 변수의 축약된 이름을 원래 의미를 알 수 있는 형태로 바꾸는 것입니다.
리팩터링 기능을 제공하는 IDE라면 무엇이든 사용할 수 있습니다(일반적으로 이름을 바꾸려는 식별자 위에서 “F2” 키를 누르면 됩니다). 그러면 16진수 문자열을 단순히 디코딩하는 것으로 보이는 함수 정의 e가 나타납니다.

처음으로 눈에 띄는 코드 줄은 함수 e가 문자열 "2e2f746573742f64617461"을 디코딩한 결과로 얻은 파일을 가져오는 것으로 보입니다.
의도적으로 난독화된 JavaScript에서는 누군가 대충 훑어보더라도 특히 불길해 보이는 문자열이나 속성이 그대로 드러나 경계심을 갖지 않도록 모든 리터럴 문자열 값을 숨기는 경우가 매우 흔합니다. 대부분의 개발자는 이것이 아주 낮은 수준의 장벽이라는 점을 알고 있으므로 쉽게 되돌릴 수 있는 인코딩이 적용된 경우가 많으며, 여기서도 마찬가지입니다. e 함수는 단순히 16진수 문자열을 뒤집으며, 이 작업은 온라인 도구 또는 직접 만든 편의 함수를 사용해 수동으로 수행할 수 있습니다. e 함수가 무엇을 하는지 확실히 이해하고 있더라도 악성 파일에서 발견된 입력값으로 이 함수를 실행하지 않는 것이 좋습니다(함수를 따로 추출한 경우에도 마찬가지입니다). 공격자가 해당 데이터에 의해 유발되는 보안 취약점을 찾아내지 않았다고 보장할 수 없기 때문입니다.
해당 문자열을 뒤집어 보면 스크립트가 배포된 npm 패키지에 포함된 데이터 파일인 './test/data'를 불러오고 있음을 알 수 있습니다.

n을 data로 이름을 바꾸고 e(n[2])부터 e(n[9])까지의 호출을 난독화 해제하고 나니, 여기서 다루고 있는 것이 무엇인지 좀 더 명확하게 보이기 시작합니다.

이 문자열들이 왜 숨겨졌는지도 쉽게 알 수 있습니다. 단순한 flatmap 라이브러리에서 복호화와 관련된 참조가 하나라도 발견된다면 무언가 크게 잘못되었다는 명백한 증거가 될 테니까요.
여기서 스크립트가 node.js의 “crypto” 라이브러리를 가져오고 있으며, API를 찾아본 결과 createDecipher의 두 번째 인수, 즉 여기서 o가 복호화에 사용되는 비밀번호라는 것을 알 수 있습니다. 이제 API를 바탕으로 이 인수와 뒤이어 나오는 반환 값들의 이름을 의미에 맞게 바꿀 수 있습니다. 퍼즐의 새로운 조각을 찾을 때마다, 사소해 보이는 변수 이름 변경이라도 리팩터링이나 주석을 통해 그 내용을 반드시 기록해 두는 것이 중요합니다. 낯선 코드를 몇 시간씩 파고들다 보면 어디까지 살펴봤는지 잊거나, 주의가 흐트러지거나, 잘못된 리팩터링 때문에 되돌아가야 하는 일이 흔히 발생합니다. git을 사용해 리팩터링 도중 체크포인트를 저장하는 것도 유용하지만, 그 여부는 여러분의 판단에 맡기겠습니다. 이제 코드는 다음과 같습니다. 더 이상 사용되지 않는 e 함수는 삭제했고, 분석에 도움이 되지 않는 if (!o) {... 문도 함께 제거했습니다.

또한 f의 이름을 newModuleInstance로 변경한 것도 확인할 수 있습니다. 이처럼 짧은 코드에서는 크게 중요하지 않지만, 코드가 수백 줄에 달할 수 있는 경우에는 모든 내용을 최대한 명확하게 표현하는 것이 중요합니다.
이제 페이로드 A의 난독화가 대부분 해제되었으므로, 코드를 차근차근 살펴보며 어떤 작업을 수행하는지 이해할 수 있습니다.
3번째 줄에서는 외부 데이터를 가져옵니다.
4번째 줄은 환경에서 비밀번호를 가져옵니다. process.env 를 사용하면 Node 스크립트 내에서 변수에 접근할 수 있으며, npm_package_description 은 Node의 패키지 관리자인 npm이 package.json 파일에 정의된 스크립트를 실행할 때 설정하는 변수입니다.
5번째 줄에서는 npm_package_description의 값을 비밀번호로 사용하여 복호화 인스턴스를 생성합니다. 즉, 이 스크립트가 npm을 통해 실행되고 package.json에 특정 description 필드가 있는 특정 프로젝트에서 실행되는 경우에만 암호화된 페이로드를 복호화할 수 있습니다. 쉽지 않겠네요.

6번째와 7번째 줄에서는 외부 파일의 첫 번째 요소를 복호화하여 “decrypted” 변수에 저장합니다.
8~11행에서는 새 모듈을 생성한 다음, 복호화된 데이터를 문서화되지 않은 메서드인 _compile에 전달합니다. 그런 다음 이 모듈은 외부 데이터 파일의 두 번째 요소를 내보냅니다. module.exports는 한 모듈의 데이터를 다른 모듈에 노출하는 Node의 메커니즘이므로, newModuleInstance.exports(data[1])는 외부 데이터 파일에서 발견된 두 번째 암호화 페이로드를 노출합니다.

이 시점에서 우리에게는 어딘가의 package.json에서 찾을 수 있는 비밀번호로만 복호화할 수 있고, 복호화된 데이터가 _compile 메서드에 전달되는 암호화된 데이터가 있습니다. 이제 한 가지 문제가 남습니다. 비밀번호를 모르는 데이터를 어떻게 복호화할 수 있을까요? 이는 간단한 문제가 아닙니다. aes256 암호화를 무차별 대입으로 쉽게 풀 수 있었다면 npm 패키지 하나가 탈취되는 것보다 훨씬 더 큰 문제가 생겼을 것입니다. 다행히 가능한 비밀번호의 범위가 완전히 미지인 것은 아니며, 단지 어딘가의 package.json에 입력된 적이 있는 임의의 문자열일 뿐입니다. package.json 파일은 원래 npm 패키지 메타데이터를 위한 파일 형식으로 만들어졌으므로 공식 npm 레지스트리부터 살펴보는 것이 좋겠습니다. 다행히 모든 패키지 메타데이터를 스트림으로 제공하는 npm 패키지가 있습니다.

대상 파일이 npm 패키지에 들어 있다고 보장할 수는 없습니다. npm이 아닌 프로젝트에서도 Node 기반 도구의 구성을 저장하는 데 package.json을 사용하는 경우가 많고, package.json의 description은 버전마다 달라질 수 있지만, 그래도 시작점으로 삼기에는 좋습니다. 여러 키로 이 페이로드를 복호화할 수 있지만 그 결과가 알아볼 수 없는 문자열일 수도 있으므로, 무차별 대입 과정에서 복호화된 페이로드가 올바른지 검증할 방법이 필요합니다. 지금 다루는 데이터는 Module.prototype._compile에 전달되고, 이는 다시 다음으로 전달됩니다 vm.runInThisContext. 따라서 출력이 JavaScript라고 합리적으로 추정할 수 있으며, 여러 JavaScript 파서 중 하나를 사용해 데이터를 검증할 수 있습니다. 비밀번호가 맞지 않거나, 비밀번호는 맞더라도 파서에서 오류가 발생하면 다음 package.json으로 넘어가야 합니다. 다행히 Shape Security는 JavaScript 및 Java 환경에서 사용할 수 있는 자체 JavaScript 파서 세트를 구축했습니다. 사용한 무차별 대입 스크립트는 다음과 같습니다:

이 작업을 92.1초 동안 실행하여 740543개의 패키지를 처리한 결과, 비밀번호인 “A Secure Bitcoin Wallet”을 찾아냈으며, 이 비밀번호로 아래에 포함된 페이로드를 성공적으로 디코딩할 수 있습니다:

운이 좋았습니다. 자칫 엄청난 규모의 무차별 대입 문제가 될 수도 있었지만, 실제로는 100만 회 미만의 반복만으로 해결되었습니다. 문제의 키가 포함된 패키지는 비트코인 지갑 Copay의 클라이언트 애플리케이션이었습니다. 다음 두 페이로드에서는 애플리케이션 자체를 더 깊이 파고듭니다. 대상 애플리케이션이 비트코인 보관을 중심으로 한다는 점을 고려하면, 이야기가 어디로 흘러갈지 짐작하실 수 있을 것입니다.
이와 같은 주제에 관심이 있고 나머지 두 페이로드나 향후 공격에 대한 분석도 읽고 싶다면, 이 게시물에 꼭 ‘좋아요’를 누르거나 트위터 @jsoverson로 알려주세요.
작성자 소개
관련 블로그 게시물
성평등을 향한 여정: 사이버 보안 분야에서 성공적인 커리어를 개척하는 방법에 관한 Jenn Gile과의 Q&A
F5의 ‘성평등을 향한 여정’ 시리즈 네 번째 Q&A에서는 Rachael Shah와 NGINX 제품 마케팅 책임자 Jenn Gile이 사이버 보안 직종에 대한 오해를 바로잡고, 사이버 보안 분야의 성별 격차를 해소하기 위한 조언과 그 이점에 대해 이야기합니다.
TLS 검사에서 피터 파커 원칙 다루기
“큰 힘에는 큰 책임이 따른다.” 볼테르와 처칠도 이와 비슷한 말을 남겼지만, 이 문구는 스파이더맨 코믹스에서 벤 삼촌의 말로 가장 널리 알려져 있습니다. 물론 이 문구가 문화적으로 널리 통용되는 이유 중 하나는 TLS 검사를 비롯해 수많은 상황과 주제에 적용할 수 있기 때문입니다.
Venafi와 F5를 통한 머신 아이덴티티 보호
‘머신 아이덴티티’란 간단히 말해 자동화된 프로세스가 자신의 신원을 식별할 수 있도록 하고, 다른 자동화 시스템과 프로세스가 관련 리소스에 적절한 수준의 접근 권한을 부여하도록 하는 방법을 의미합니다. 이 개념 자체는 새로운 것이 아닙니다. 비교적 새로운 점은 이를 구현해야 하는 규모입니다.
마이너와 미니언: 데이터 유출만이 비용을 초래하는 것은 아닙니다
물론 데이터 유출은 더 큰 비용을 초래할 수 있고(적어도 지금은), 실패 사실을 세상에 드러낼 수밖에 없게 합니다(통지 의무가 있으니까요). 하지만 보안을 단순히 데이터 노출 문제로만 봐서는 안 됩니다. 요즘은 데이터가...
내 클라우드에서 나가!
F5의 Jay Kelley가 전하는 최신 소식에는 Microsoft Office 365용 신규 ID 및 액세스 솔루션을 배포할 때 고려해야 할 사항이 정리되어 있습니다.
Shift(웹 앱) 보안 레터
웹 앱 보안을 개발 초기 단계로 앞당기세요. F5 블로그에서 DevOps 파이프라인 초기에 보안을 통합하여 취약점을 예방하고 시간을 절약하는 방법을 알아보세요.