
POC for frustrating/defeating Malware Analysts
다음에 제시되는 작업은 악성코드가 특정 피해자에게 "키잉(key)"되도록 하여 악성코드 분석가의 노력을 방해하기 위한 POC입니다.
본 프로젝트에 포함된 아이디어나 코드의 악의적 사용에 대해 어떠한 책임도 지지 않습니다. 저는 이 연구를 정보보안 전문가들의 교육을 더욱 발전시키고, 악성코드 분석가, 리버스 엔지니어, 그리고 블루팀 전체에 추가적인 훈련/생각거리를 제공하기 위해 공유합니다.
악성코드가 피해자에서 처음 실행될 때, 해당 피해자의 환경 데이터를 사용하여 실제 페이로드(RDLL)를 AES 암호화합니다. 이후 악성코드가 실행될 때마다 동일한 환경 정보를 수집하고, 악성코드 내에 바이트 배열로 저장된 페이로드를 AES 복호화하여 실행합니다. 복호화에 실패하거나 페이로드 실행에 실패하면 악성코드는 스스로 삭제됩니다. 리버스 엔지니어와 악성코드 분석가로부터의 보호 기능입니다.


이 프로젝트에 대해 아직 끝났다는 느낌이 들지 않아 상당히 대대적인 재작성을 진행했습니다. 원본 연구 및 트레이드크래프트는 여기에서 확인할 수 있습니다.
주요 변경 사항은 다음과 같습니다:
이 프로젝트의 소스 코드에서 다른 곳에 활용할 수 있는 다양한 요소들이 있습니다. 누군가에게 유용하게 사용되길 바랍니다.
원본 BeatRev 릴리스에는 몇 가지 단점이 있었고, 이를 해결하기로 결정했습니다.
Stage2는 이전에 Stage1의 대체 데이터 스트림(ADS)으로 저장된 독립 실행형 실행 파일이었습니다. 피해자별 AES 암호화와 이후 복호화 및 실행을 달성하기 위해, Stage1이 실행될 때마다 ADS를 읽고, 복호화하고, ADS에 다시 쓰고, CreateProcess를 호출한 다음, Stage2를 다시 암호화하여 디스크의 ADS에 기록했습니다. 이는 많은 I/O 작업이었고, 물론 CreateProcess 호출도 좋지 않았습니다.
우연히 Steven Fewer의 Reflective DLL에 관한 연구를 접하게 되었고, 이것이 적합해 보였습니다. Stage2는 이제 RDLL입니다. 우리가 보호하려는 악성코드/셸코드 실행기/기타 등등은 RDLL 형식으로 포팅되어 Stage1 내에 바이트 배열로 저장되고, 런타임에 복호화되어 Stage1에 의해 실행됩니다. 이는 Version1의 모든 I/O 작업과 CreateProcess 호출을 제거하며 환영할 만한 변화입니다.
Stage1에는 실제로 어떤 종류의 AV 회피 조치도 프로그래밍되어 있지 않았습니다. 이는 의도적인 것으로, 추가 작업이 필요하고 이 연구의 요점이 아니었기 때문입니다. 재작성 과정에서 이를 추가적인 도전 과제로 삼아 API 해싱을 추가하여 Stage1의 Import Address Table에서 함수를 제거했습니다. 이는 탐지에 도움이 되었으며 Stage1은 VirusTotal에서 4/66의 탐지율을 보입니다. Stage1이 이미 실행된 원래 시스템에 키잉되어 있고 AES 암호화로 인해 파일 시그니처가 지속적으로 변경되기 때문에 업로드해도 안전하다고 판단했습니다.
최근에 악성코드 탐지 수단으로 엔트로피에 주목하기 시작했습니다. 실행 파일에 거대한 AES 암호화 바이너리 블롭이 주는 매우 높은 엔트로피를 낮추기 위해 UUID로 저장된 셸코드를 통합하는 방법을 조사했습니다. 바이너리가 문자열 표현으로 저장되기 때문에 실행 파일의 전체 엔트로피가 낮아집니다. 이 기술을 사용하여 Stage0의 엔트로피는 이제 ~6.8, Stage1은 ~4.5입니다(최대 8 기준).
마지막으로, 조작해야 할 모든 조각들로 인해 완전한 Stage0을 통합하고 생성하는 것은 매우 번거로운 작업입니다. 이를 더 쉽게 만들기 위해 빌더 애플리케이션을 만들어 Stage0.c 템플릿 파일, Stage1 스텁, Stage2 스텁, 그리고 원시 셸코드 파일(Stage2가 CobaltStrike 셸코드를 포함하는 셸코드 실행기인 경우를 기준으로 구축됨)을 입력받아 컴파일된 Stage0 페이로드를 생성합니다.
Stephen Fewer의 Reflective DLL 코드에는 Visual Studio 컴파일러 특정 명령어가 포함되어 있습니다. 이 기술을 MingW로 포팅하는 것이 가능할 수 있지만, 제가 그 기술을 가지고 있지 않습니다. 여기서 주요 문제는 CobaltStrike 셸코드(스테이지리스는 ~265K)가 RDLL 내부에 들어가서 컴파일되어야 한다는 것입니다. 이를 해결하고 나머지 프로세스와 잘 통합하기 위해 Stage2 RDLL에 CS 셸코드 크기만큼의 전역 변수 메모리 청크를 포함하도록 작성했습니다. 이 ~265K 메모리 청크에는 컴파일된 바이너리에서 위치를 찾을 수 있는 작은 플레이스홀더가 있습니다. src/Stage2의 코드에는 이미 이것이 추가되어 있습니다.
컴파일되면 이 Stage2 스텁은 kali로 전송되어 실제 CS 셸코드를 해당 메모리 위치에 삽입하는 바이너리 패치가 수행됩니다. 이로써 완전한 Stage2가 생성됩니다.
이전에 설명된 I/O 및 CreateProcess 문제를 피하기 위해, 완전한 Stage2는 또한 Stage0에 의해 컴파일된 Stage1에 패치되어야 합니다. 이는 Stage2가 대상에서 한 번 암호화될 수 있도록 하고 Stage2가 디스크에 별도로 저장되는 것을 방지하기 위해 필요합니다. Stage2에 대해 앞서 설명한 것과 동일한 개념이 대상에서 Stage0에 의해 수행되어 최종 Stage1 페이로드를 조립합니다. 각 스텁 내에서 플레이스홀더를 찾기 위해 memmem 함수가 사용된다는 점에 유의하십시오. 이 함수는 Windows에서 사용할 수 없으므로 사용자 정의 구현이 사용되었습니다. Foxik384의 코드에 감사드립니다.
바이너리 패치를 수행하려면 필요한 메모리를 미리 할당해야 합니다. 이는 연쇄 효과를 가지며, Stage1이 Stage2도 포함할 수 있을 만큼 충분히 커야 합니다. Stage2를 UUID 문자열로 변환하는 추가 단계로 인해 Stage2의 크기가 급증하고 이를 담기 위해 Stage1도 커집니다. 컴파일된 크기가 ~290K인 Stage2 RDLL은 ~1.38M의 Stage0 페이로드와 ~700K의 Stage1 페이로드를 생성합니다.
빌더 애플리케이션은 x64 EXE 생성만 지원합니다. 그러나 약간의 추가 작업을 통해 이론적으로 Stage0을 DLL로 만들고 Stage1도 DLL로 만들어 전체 라이프사이클이 독립 실행형 실행 파일 대신 DLL 하이재킹으로 존재하도록 할 수 있습니다.
이 POC를 사용하는 방법에 대한 지침입니다.

약 6개월 전, AV/EDR 회피에 관해 악성코드에 대해 많은 것을 배우고 해왔지만, 리버스 엔지니어링/악성코드 분석을 회피하거나 무력화하는 것에는 거의 신경을 쓰지 않았다는 사실을 깨달았습니다. 여기에는 몇 가지 좋은 이유가 있었습니다:
그럼에도 불구하고 흥미로운 사고 실험이었고, 악성코드 분석에 대해 실제로 알고 있는 몇몇 동료들에게 아이디어를 얻을 수 있었습니다. 이는 AV/EDR 회피와는 전혀 다른 차원의 도전처럼 보였고, 한번 시도해보기로 결정했습니다.
저의 초기 전제는 악성코드가 처음 실행될 때 해당 피해자 머신에 어떻게든 "키잉"되고, 이후 실행 시도 시 대상 환경에서 어떤 것을 평가하여 악성코드 내의 값과 일치하는지 비교한다는 것이었습니다. 이 두 요소가 일치하면 정상적으로 실행됩니다. 일치하지 않으면 (예: 샘플이 악성코드 분석가의 샌드박스로 전송된 경우), 악성코드는 스스로 삭제됩니다 (다시 한번 LloydLabs의 delete-self-poc 작업에 크게 의존합니다).
이 "키"는 피해자 컴퓨터에 "고유한" 것이어야 합니다. 이상적으로는 여러 정보의 조합이며, 추가로 난독화됩니다. 예를 들어, 컴퓨터의 호스트 이름과 설치된 RAM 용량을 수집할 수 있습니다. 이 두 값을 연결한 다음(예: Client018192MB) 사용자 정의 함수를 사용하여 해싱하여 숫자를 생성할 수 있습니다(예: 5343823956).
수집할 정보에는 수많은 선택지가 있지만, 블루팀원이 쉽게 스푸핑할 수 있는 값은 무엇인지 고려해야 합니다. 예를 들어 MAC 주소는 피해자에 대한 매력적인 "고유" 식별자로 보일 수 있지만, MAC 주소는 리버스 엔지니어가 샌드박스를 원래 피해자와 일치시키기 위해 수동으로 쉽게 설정할 수 있습니다. 이상적으로 선택되고 열거되는 값은 리버스 엔지니어가 자신의 환경에서 복제하기 어려운 값이어야 합니다.
자체 삭제 매직을 통해, 악성코드는 자신을 버퍼로 읽어들이고, 플레이스홀더 변수를 찾아 이 숫자로 대체한 다음, 스스로 삭제하고, 수정된 악성코드를 동일한 위치에 디스크에 다시 쓸 수 있습니다. Main의 if/else 문과 결합하여, 다음에 악성코드가 실행될 때 이전에 실행된 적이 있음을 감지하고 호스트 이름과 RAM 용량을 다시 수집하여 해싱된 숫자를 생성합니다. 그런 다음 첫 번째 실행 동안 악성코드에 저장된 숫자(5343823956)와 비교하여 평가됩니다. 일치하면(악성코드가 원래 실행된 동일한 머신에서 실행되는 경우) 정상적으로 실행되지만, 다른 값이 반환되면 다시 자체 삭제 함수를 호출하여 디스크에서 자신을 제거하고 악성코드 분석가로부터 작성자를 보호합니다.
이것은 이론상 좋은 아이디어처럼 보였지만, 실제 악성코드 분석 및 리버스 엔지니어링 경험이 있는 동료와 상담할 때까지였습니다. 리버스 엔지니어는 악성코드의 조건문(if ValueFromFirstRun != GetHostnameAndRAM())을 관찰할 수 있고, 예상 값이 조건문의 한쪽에 하드코딩되어 있으므로 레지스터에 예상 값을 포함하도록 수정하여 전체 보호 메커니즘을 완전히 우회할 수 있다고 들었습니다.
이 새로운 지식은 사고 실험을 완전히 탈선시켰고, 어차피 이런 기능을 사용할 필요가 없었기 때문에 프로젝트는 약 6개월 동안 중단되었습니다.
이 프로젝트는 이후 6개월 동안 몇 번 다시 떠올랐지만, 리버싱/악성코드 분석에 대한 새로운 지식을 얻지 못했고 여전히 그러한 기능이 필요하지 않았기 때문에 매번 스쳐 지나가는 생각에 불과했습니다. 며칠 전 다시 아이디어가 떠올랐고, 여전히 그 두 가지 요소는 실제로 변하지 않았지만, 이번에는 좀 더 많은 지식이 있었고 아이디어를 놓을 수 없었습니다.
앞서 언급한 하드코딩 값 문제를 염두에 두고, 궁극적으로 다단계 설계를 선택하기로 결정했습니다. 각각 Stage0, Stage1, Stage2라고 부르겠습니다.
Stage0: 설정. 초기 감염 시 실행되고 이후 삭제됨
Stage1: 실행기. 악성코드가 실행될 때마다 이후에 실행됨
Stage2: 페이로드. 보호하려는 실제 악성코드. 프로세스를 생성하고 셸코드를 주입하여 Beacon을 반환함
Stage0은 공격자가 대상에 전달하는 최초 실행 파일입니다. Stage0은 Stage1과 Stage2를 AES 암호화된 바이트 배열로 포함합니다. 이는 전송 중인 악성코드를 보호하거나, 방어자가 어떻게든 Stage0의 사본을 손에 넣는 경우(그래서는 안 되지만)를 대비하기 위함입니다. AES Key와 IV는 Stage0 내에 포함되어 있으므로 실제로는 유능한 블루팀원으로부터 Stage1이나 Stage2를 보호하지 못합니다.
Stage0은 다음 작업을 수행합니다:
이 이벤트 시퀀스가 종료되면 Stage0은 종료됩니다. 2단계에서 디스크에서 삭제되었고 메모리에서 더 이상 실행되지 않기 때문에 Stage0은 사실상 사라집니다. 이 기술에 대한 사전 지식 없이는 나머지 악성코드 라이프사이클이 이미 혼란스러운 상황보다 훨씬 더 혼란스러울 것입니다.
4단계에서 프로세서 이름과 Microsoft ProductID가 수집됩니다. ProductID는 레지스트리에서 검색되며 이 값은 수동으로 수정할 수 있어 블루팀원이 샌드박스를 대상 환경과 일치시킬 수 있는 쉬운 기회를 제공합니다. 수집되는 환경 정보에 따라 이것은 더 쉬워지거나 더 어려워질 수 있습니다.
Stage1은 Stage0에 의해 드롭되었으며 Stage0과 정확히 동일한 위치에 존재합니다(이름 포함). Stage2는 Stage1의 ADS로 저장됩니다. 공격자/지속성이 이후에 악성코드를 실행하면 Stage1을 실행하는 것입니다.
Stage1은 다음 작업을 수행합니다:
Stage2가 덮어쓰기 가능하려면 반드시 종료되어야 합니다. 자체 삭제 트릭은 이미 ADS인 파일에는 작동하지 않는 것으로 보입니다. 자체 삭제 기술은 실행 파일의 기본 데이터 스트림 이름을 바꾸는 데 의존하기 때문입니다. Stage2는 이상적으로 인젝트(inject) 또는 스폰+인젝트(spawn+inject) 실행 파일이어야 합니다.
Stage1이 동일한 피해자에서 실행되고 있지 않음을 감지하고 위협 행위자를 보호하기 위해 자신/Stage2를 삭제할 수 있는 지점은 두 곳입니다. 첫 번째는 수집된 환경 정보를 사용하여 Stage2를 복호화한 후 실행 파일 헤더를 확인하는 것입니다. 이론상 이 단계는 리버스 엔지니어에 의해 우회될 수 있지만, 좋은 첫 번째 검사입니다. 두 번째 보호 지점은 CreateProcess 호출의 결과입니다. Stage2가 제대로 복호화되지 않아 실패하면 악성코드도 유사하게 삭제됩니다. 이 호출의 결과는 리버스 엔지니어에 의한 삭제를 방지하기 위해 수정될 수도 있지만, 이는 Stage2가 암호화되어 접근 불가능하다는 사실을 바꾸지 않습니다.
Stage2는 악성코드 체인의 핵심입니다. 자체적으로 완전한 셸코드 실행기/악성코드입니다. 우리가 한 방식으로 암호화하고 보호함으로써, 최종 상태 악성코드의 동작은 리버스 엔지니어와 악성코드 분석가로부터 훨씬 더 잘 난독화되고 보호됩니다. 개발 중에는 CobaltStrike 셸코드를 포함하는 기존 셸코드 실행기 중 하나를 사용했지만, 공격자가 실행하고 보호하려는 어떤 것이든 될 수 있습니다.
그렇다면 이러한 악성코드 라이프사이클로 실제로 무엇을 달성할 수 있을까요? 몇 가지 흥미로운 특이점이 있습니다.
대체 데이터 스트림은 NTFS 파일 시스템에만 고유한 기능입니다. 이는 초기 감염 후 대부분의 악성코드 전송 방식이 Stage2를 생략하고 손실시킨다는 것을 의미합니다. Stage2는 Stage1의 ADS이기 때문입니다. Stage2를 보존하려면 샘플을 전송하는 데 특별한 주의를 기울여야 합니다. 그렇지 않으면 많은 리버스 엔지니어와 악성코드 분석가가 무슨 일이 일어나고 있는지 매우 혼란스러워할 것입니다. RAR 아카이브는 ADS를 보존할 수 있으며 7Z 및 Peazip과 같은 도구는 파일과 해당 ADS를 추출할 수 있습니다.
앞서 언급했듯이, 이 라이프사이클을 사용하는 악성코드가 블루팀원에게 도달할 때쯤이면 Stage1 상태여야 합니다. Stage0은 왔다 갔으며 Stage2는 이미 Stage0이 수집한 환경 정보로 암호화되어 있습니다. Stage0이 존재했다는 사실조차 모르면 라이프사이클을 이해하고 Stage2를 복호화하는 데 상당한 불확실성이 추가됩니다.
이론상(다시 말하지만 리버싱 경험이 없기 때문에), Stage1은 (블루팀원이 계속 자체 삭제되는 여러 복사본을 처리한 후) 리버싱될 수 있어야 하며, Stage1이 대상 시스템에서 수집하는 정보를 식별할 수 있어야 합니다. 잘 조직된 대응이 제공된다면, 블루팀은 악성코드가 나온 피해자를 식별하고 해당 정보를 수집하여 프로그램에 주입함으로써 Stage2를 복호화하는 AES Key/IV로 적절히 변환할 수 있어야 합니다. 그러나 여기에는 리버스 엔지니어의 상대적 기술 수준과 피해자 머신이 해당 정보를 복구할 수 있도록 사용 가능해야 하는 등 많은 "만약"이 있습니다.
애플리케이션 화이트리스트는 이 라이프사이클을 상당히 방해할 수 있습니다. Stage0/Stage1은 DLL로 사이드 로드될 수 있지만, ADS로서의 Stage2는 몇 가지 문제를 제기할 것으로 의심됩니다. AWL에 대한 악성코드를 테스트할 환경이 없고 이를 DLL 형식으로 포팅하는 데 신경 쓰지 않았기 때문에 확실히 말할 수는 없습니다. 이러한 문제를 해결할 창의적인 방법이 있을 것이라고 확신합니다.
또한 디스크에 드롭하고 CreateProcess를 호출하는 것보다 Stage2를 실행하는 더 스마트한 방법이 있다고 확신합니다. 실행 파일을 수동으로 매핑하거나 Donut과 같은 도구를 사용하여 셸코드로 변환하는 것이 합리적인 아이디어처럼 보입니다.
개발 중에 Stage1과 Stage2를 입력받아 기능적인 Stage0을 생성하는 Builder 애플리케이션을 만들었습니다. 그러나 이는 제공되지 않습니다. 대신 Stage1의 소스 코드 대부분을 제공할 것입니다. Stage1은 블루팀원에게 가장 가시적인 부분이기 때문입니다. Stage0은 독자의 연습 과제로 제외되며, Stage2는 실행하고 보호하려는 모든 독립 실행형 실행 파일입니다. 이 POC는 관심 있는 독자의 노력과 재량에 따라 추가로 연구될 수 있습니다.I will be providing a compiled copy of this malware as Dropper64.exe. Dropper64.exe is compiled for x64. Dropper64.exe is Stage0; it contains Stage1 and Stage2. On execution, Stage1 and Stage2 will drop to disk but will NOT automatically execute, you must run Dropper64.exe(now Stage1) again. Stage2 is an x64 version of calc.exe. I am including this for any Blue Teamers who want to take a look at this, but keep in mind in an incident response scenario 99& of the time you will be getting Stage1/Stage2, Stage0 will be gone.
이것은 긴 주말을 소모한 재미있는 개인 프로젝트였습니다. 디버거와 디스어셈블러에 경험이 있었다면 훨씬 더 발전되고 완성도가 높았을 것이라고 확신하지만, 가진 도구로 최선을 다하는 수밖에 없습니다. Blue Teamers와 다른 Malware Devs의 의견을 듣고 싶습니다. 실제 APT들이 하는 일에 비해 여기서 바퀴를 불필요하게 복잡하게 재발명했다고 확신하지만, 그 과정에서 몇 가지를 배웠습니다. 읽어주셔서 감사합니다!