좋아하시는 분도 있고 싫어하시는 분도 계시겠지만, 현 시점에서 .NET 공격 수법이 예상보다 조금 더 오래 지속될 것이라는 점은 놀라운 일이 아닙니다. .NET Framework는 Microsoft 운영 체제의 필수적인 부분이며, 최신 .NET 릴리스는 .NET Core입니다. Core는 .NET을 Linux 및 macOS에도 제공하는 .NET Framework의 크로스 플랫폼 후속 버전입니다. 이로 인해 이제 .NET은 공격자와 레드 팀 사이에서 포스트 익스플로잇 공격 수법에 그 어느 때보다 인기를 얻고 있습니다. 이 블로그에서는 작업자가 Cobalt Strike를 통해 진행 중인 .NET 어셈블리를 실행할 수 있는 새로운 Beacon Object File(BOF)과 포크 앤 런(fork and run) 기법을 사용하는 기존의 기본 제공 어셈블리 실행 모듈에 대해 자세히 살펴봅니다.
인기 있는 적대 시뮬레이션 소프트웨어인 Cobalt Strike는 PowerShell의 기능이 향상됨에 따라 레드팀이 PowerShell 툴링에서 C#으로 전환하는 추세를 인식했으며, 2018년 Cobalt Strike 버전 3.11부터 어셈블리 실행 모듈을 도입했습니다. 이를 통해 운영자는 포스트 익스플로잇 .NET 어셈블리를 메모리에서 실행하여 해당 도구를 디스크에 드롭하는 추가 위험 없이 어셈블리의 기능을 활용할 수 있었습니다. 비관리 코드를 통해 메모리에 .NET 어셈블리를 로드하는 기능은 릴리스 당시에는 새로운 기능이나 알려지지 않은 기능이 아니었지만, Cobalt Strike가 이 기능을 주류로 도입했고 포스트 익스플로잇 공격 수법을 위한 .NET의 기능을 계속 높이는 데 도움이 되었다고 말할 수 있습니다.
Cobalt Strike의 실행 어셈블리 모듈은 새로운 희생 프로세스를 생성하고, 포스트 익스플로잇 악성 코드를 새 프로세스에 주입하고, 악성 코드를 실행하고, 완료되면 새 프로세스를 종료하는 포크 앤 런 기법을 사용합니다. 여기에는 장점과 단점이 모두 있습니다. 포크 앤 런 방식의 이점은 실행이 Beacon 임플란트 프로세스 외부에서 발생한다는 것입니다. 이는 포스트 익스플로잇 조치 과정에서 문제가 발생하거나 발각될 경우, 임플란트가 살아남을 가능성이 훨씬 높아진다는 것을 의미합니다. 간단히 말해서, 이는 임플란트의 전반적인 안정성을 향상시키는 데 큰 도움이 됩니다. 그러나 보안 공급업체가 이러한 포크 앤 런 동작을 포착함에 따라 이제 Cobalt Strike가 인정한 바와 같이 OPSEC에 비용이 많이 드는 패턴이 추가되었습니다.
2020년 6월에 릴리스된 버전 4.1부터 Cobalt Strike는 이 문제를 해결하는 데 도움이 되는 새로운 기능인 Beacon Object File(BOF)을 도입했습니다. BOF를 사용하면 운영자는 비콘 이식과 동일한 프로세스 내에서 메모리의 개체 파일을 실행하여 위에서 설명한 잘 알려진 실행 패턴이나 cmd.exe/powerShell.exe 사용과 같은 기타 OPSEC 오류를 방지할 수 있습니다. BOF의 내부 작동 방식에 대해서는 다루지 않겠지만, 다음은 인사이트를 얻을 수 있는 몇 가지 블로그 게시물입니다.
위 블로그들을 읽어보셨다면, BOF가 우리가 기대했던 구원의 손길이 아니었음을 이제야 깨달았을 것입니다. 그리고 그 멋진 .NET 도구들을 모두 다시 작성하여 BOF로 바꾸겠다는 꿈을 꾸셨다면, 그 꿈은 이제 산산조각이 났습니다. 죄송합니다. 하지만 제 생각에는 BOF가 제공할 수 있는 몇 가지 훌륭한 점이 있고, 최근에 BOF로 할 수 있는 일의 한계를 뛰어넘는 데 많은 재미와 약간의 좌절감도 느꼈지만 희망을 잃지 않았습니다. 첫째, LSASS와 같은 프로세스의 완전한 인메모리 덤프를 수행한 후 기존 비콘 통신 채널을 통해 다시 전송하는 CredBandit을 생성하는 것입니다. 오늘 자주 사용하는 .NET 툴을 수정하지 않고도 비콘 프로세스 내에서 .NET 어셈블리를 실행하는 데 사용할 수 있는 InlineExecute-Assembly를 출시합니다. 제가 BOF를 작성한 이유, 몇 가지 주요 기능, 주의 사항, 적대적 시뮬레이션/레드팀을 수행할 때 BOF가 어떻게 유용할 수 있는지 자세히 살펴보겠습니다.
업계 뉴스레터
Think 뉴스레터를 통해 AI, 자동화, 데이터 등 가장 중요하고 흥미로운 업계 동향에 대한 최신 소식을 받아보세요. IBM 개인정보 보호정책을 참조하세요.
구독한 뉴스레터는 영어로 제공됩니다. 모든 뉴스레터에는 구독 취소 링크가 있습니다. 여기에서 구독을 관리하거나 취소할 수 있습니다. 자세한 정보는 IBM 개인정보 보호정책을 참조하세요.
InlineExecute-Assembly를 구축하는 이유는 매우 간단합니다. 저는 적대적인 시뮬레이션 팀이 진행 중인 .NET 어셈블리를 실행하여 Cobalt Strike를 사용하여 성숙한 환경에서 운영할 때 위에서 언급한 OPSEC 함정 중 일부를 피할 수 있는 방법을 원했습니다. 또한 현재 사용 중인 대부분의 .NET 도구를 수정해야 하므로 추가 개발 시간으로 인해 팀에 부담을 주지 않도록 하는 도구가 필요했습니다. 또한 안정적이어야 했습니다. 복잡한 BOF가 가질 수 있는 최대한의 안정성이 필요했습니다. 환경 내에 몇 안 되는 비콘 중 하나를 잃는 일은 절대 피해야 하기 때문입니다. 기본적으로 Cobalt Strike의 어셈블리 실행 모듈만큼 작업자가 원활하게 사용할 수 있어야 합니다.
다소 뻔한 내용일 것입니다. 이게 없었다면 더 나아가지 못했을 것입니다. 농담은 제쳐두고, CLR의 작동 방식과 심층적인 진행 방식에 대한 복잡한 설명은 그 자체로 블로그 게시물이 될 수 있으므로, 비관리 코드를 통해 CLR을 로드할 때 BOF가 무엇을 사용하는지 매우 높은 수준에서 검토해 보겠습니다.
CLR 로드
위의 간단한 스크린샷에서 보는 바와 같이 BOF가 CLR을 로드하기 위해 수행하는 주요 단계는 다음과 같습니다.
이제 CLR이 초기화되었지만 즐겨 사용하는 .NET 어셈블리를 실제로 실행하기 전에 해야 할 일이 조금 더 있습니다. Microsoft에서는 '애플리케이션이 실행되는 격리된 환경'이라고 설명하는 AppDomain 인스턴스를 만들어야 합니다. 즉, 이는 포스트 익스플로잇 .NET 어셈블리를 로드하고 실행하는 데 사용됩니다.
생성 중인 AppDomain 및 로드/실행 중인 어셈블리
위의 간소화된 스크린샷에서 볼 수 있듯이 BOF가 .NET 어셈블리를 로드하고 호출하기 위해 취하는 주요 단계는 다음과 같습니다.
이제 여러분은 비관리형 코드를 통한 .NET 실행에 대한 높은 수준의 이해를 갖게 되었습니다. 하지만 이 정도로는 실용적인 도구를 만드는 데는 한참 모자라니, BOF에서 구현된 몇 가지 기능을 살펴보며 평범한 수준에서 완전히 합법적인 수준으로 끌어올리는 방법을 알아보겠습니다.
이것이 왜 중요한지 궁금하실 것입니다. 여러분이 저처럼 시간을 소중히 여기는 사람이라면, 일반적으로 콘솔 표준 출력으로 파이핑되는 모든 데이터가 포함된 문자열을 반환하도록 거의 모든 .NET 어셈블리를 수정하는 데 시간을 소비하고 싶지 않으실 것입니다. 그럴 줄 알았습니다. 이를 방지하려면 표준 아웃풋을 명명된 파이프 또는 메일 슬롯으로 리디렉션하고, 작성된 후 아웃풋을 읽은 다음, 원래 상태로 되돌리면 됩니다. 이렇게 하면 cmd.exe 또는 powerShell.exe에서와 마찬가지로 수정되지 않은 어셈블리를 실행할 수 있습니다. 이제 코드를 살펴보기 전에 @N4k3dTurtl3와 프로세스 실행 어셈블리 및 메일 슬롯에 대한 블로그 게시물에 감사를 표해야 합니다. 원래는 이 기술이 처음 나왔을 때 제 개인 C 임플란트에 이 기술을 구현하게 된 계기였고, 몇 달 후 동일한 기능을 BOF에 이식했습니다. 이제 소품이 제공되었으므로 아래의 명명된 파이프로 stdout을 리디렉션하여 이를 달성하는 간단한 예를 살펴보겠습니다.
콘솔 표준 출력을 명명된 파이프로 리디렉션하고 다시 되돌리기
ICLRMetaHost - > GetRuntime을 통해 CLR을 로드할 때 필요한.NET 프레임워크의 버전을 지정해야 했던 것을 기억하시나요? .NET 어셈블리가 컴파일된 버전에 따라 다르다는 것을 기억하시나요? 매번 어떤 버전이 필요한지 수동으로 지정해야 하는 것은 그다지 재미가 없을 것입니다. 다행히도, @b4rtik가 Metasploit 프레임워크용 execute-assembly 모듈에 이를 처리하는 멋진 함수를 구현한 덕분에 저희가 아래에 보이는 저희 도구에 쉽게 구현할 수 있습니다.
.NET 어셈블리를 읽고 CLR을 로드할 때 필요한 .NET 버전을 결정하는 데 도움이 되는 함수
기본적으로 이 함수는 어셈블리 바이트를 전달할 때 해당 바이트를 읽고 76 34 2E 30 2E 33 30 33 31 39의 16진수 값을 찾습니다. 이 값을 ASCII로 변환하면 v4.0.30319가 됩니다. 익숙하게 보이길 바랍니다. 어셈블리를 읽을 때 해당 값이 발견되면 함수는 1 또는 true를 반환하고, 찾을 수 없으면 0 또는 false를 반환합니다. 이를 사용하여 아래 코드 예제와 같이 1/true 또는 0/false가 반환되는지 여부와 함께 로드할 버전을 쉽게 결정할 수 있습니다.
.NET 버전 변수를 설정하기 위한 if/else 문
.NET 공격 수법에 대해 이야기하면서 AMSI에 대해 이야기하지 않을 수는 없었습니다. AMSI가 무엇이며 이를 우회하는 모든 방법에 대해서는 이미 여러 차례 다뤄진 바 있으므로, 깊이 있게 다루지는 않겠습니다. 다만 BOF를 통해 실행하기로 결정한 내용에 따라 AMSI 패치가 필요한 이유에 대해서는 간략히 설명하겠습니다. 예를 들어, 난독화 없이 Seatbelt를 실행하기로 결정한 경우 출력을 다시 얻지 못하고 비콘이 작동하지 않는다는 것을 금방 알 수 있습니다. 네, 작동을 멈추었습니다. AMSI가 귀하의 어셈블리를 포착하고 악의적이라고 판단하여 너무 시끄러운 하우스 파티처럼 폐쇄했기 때문입니다. 이는 이상적인 상황이 아닙니다. 이제 AMSI에 대해 두 가지 좋은 선택지가 있습니다. 하나는 ConfuserX 또는 Invisibility Cloak 같은 도구로 .NET 도구를 난독화하거나, 다양한 기법으로 AMSI를 비활성화하는 것입니다. 저희 경우에는 RastaMouse의 amsi.dll 메모리를 패치해서 E_INVALIDARG를 반환하고 스캔 결과가 0이 되도록 합니다. 블로그 글에서 지적했듯이, 보통 AMSI_RESULT_CLEAN로 해석됩니다. 아래에서 x64 프로세스용 코드의 간소화된 버전을 살펴보겠습니다.
AmsiScanBuffer의 인메모리 패치
스크린샷에서 볼 수 있듯이, 단순히 다음을 수행하기만 하면 됩니다.
이를 도구에 구현하면 아래와 같이 –amsi 플래그를 사용하여 AMSI 감지를 우회하는 기본 버전의 Seatbelt.exe를 실행할 수 있습니다.
InlineExecute-Assemby AMSI 바이패스 예제
다행스럽게도 방어자에게는 ETW를 사용하여 악성 .NET 공격 수법을 포착할 때 AMSI가 도와줄 수 있는 그 이상의 것이 있습니다. 안타깝게도 AMSI와 마찬가지로 이 역시 공격자가 우회하기 매우 쉬우며, @xpn은 이를 어떻게 우회할 수 있는지에 대한 멋진 연구를 수행했습니다. 아래에서 ETW를 패치하여 완전히 비활성화하는 방법에 대한 간단한 예를 살펴보겠습니다.
EtwEventWrite의 인메모리 패치
위 스크린샷에서 볼 수 있듯이 단계는 AMSI를 패치한 방법과 거의 동일하므로 이 단계에 대해서는 다루지 않겠습니다. 아래에서 –etw 플래그를 실행하기 전과 후의 스크린샷을 확인할 수 있습니다.
–etw 플래그와 함께 inlineExecute-Assembly를 실행하기 전에 Process Hacker를 사용하여 PowerShell.exe 속성 보기
–etw 플래그를 사용하여 인라인-실행-어셈블리 실행
Process Hacker를 사용하여 inlineExecute-Assembly 실행 후 동일한 PowerShell.exe 속성 보기
기본적으로 생성된 AppDomain, Named Pipe 또는 Mail Slot은 기본값인 'totesLegit'을 사용합니다. 이러한 값은 제공된 공격자 스크립트에서 변경하거나 명령줄 플래그를 통해 즉시 변경하여 테스트 중인 환경에 더 잘 어울리도록 변경할 수 있습니다. 명령줄을 통해 변경하는 예시는 다음과 같습니다.
고유한 AppDomain 이름과 고유한 명명된 파이프 이름을 사용하는 InlineExecute-Assembly 예시
고유한 AppDomain 이름 ChangedMe 예시
고유하게 명명된 파이프 LookAtMe 예시
성공적인 실행이 완료된 후 제거되는 AppDomain 예시
성공적인 실행이 완료된 후 명명된 파이프가 제거되는 예시
이 섹션은 GitHub 리포지토리에서 언급한 내용을 거의 반복하지만, 이 도구를 사용할 때 염두에 두어야 할 몇 가지 사항을 반복해서 설명하는 것이 중요하다고 생각했습니다.
다음은 몇 가지 방어 고려 사항입니다.