무례하기 굴지 않고 머무르기: InlineExecute-Assembly를 통해 포크 앤 런 .NET 실행 피하기

밤늦게 코드를 작성하면서 컴퓨터 화면을 보며 작업하는 남성

좋아하시는 분도 있고 싫어하시는 분도 계시겠지만, 현 시점에서 .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를 구축해야 하는 이유

InlineExecute-Assembly를 구축하는 이유는 매우 간단합니다. 저는 적대적인 시뮬레이션 팀이 진행 중인 .NET 어셈블리를 실행하여 Cobalt Strike를 사용하여 성숙한 환경에서 운영할 때 위에서 언급한 OPSEC 함정 중 일부를 피할 수 있는 방법을 원했습니다. 또한 현재 사용 중인 대부분의 .NET 도구를 수정해야 하므로 추가 개발 시간으로 인해 팀에 부담을 주지 않도록 하는 도구가 필요했습니다. 또한 안정적이어야 했습니다. 복잡한 BOF가 가질 수 있는 최대한의 안정성이 필요했습니다. 환경 내에 몇 안 되는 비콘 중 하나를 잃는 일은 절대 피해야 하기 때문입니다. 기본적으로 Cobalt Strike의 어셈블리 실행 모듈만큼 작업자가 원활하게 사용할 수 있어야 합니다.

Mixture of Experts | 12월 12일, 에피소드 85

AI 디코딩: 주간 뉴스 요약

세계적인 수준의 엔지니어, 연구원, 제품 리더 등으로 구성된 패널과 함께 불필요한 AI 잡음을 차단하고 실질적인 AI 최신 소식과 인사이트를 확인해 보세요.

주요 기능

공용 언어 런타임(CLR) 로드

다소 뻔한 내용일 것입니다. 이게 없었다면 더 나아가지 못했을 것입니다. 농담은 제쳐두고, CLR의 작동 방식과 심층적인 진행 방식에 대한 복잡한 설명은 그 자체로 블로그 게시물이 될 수 있으므로, 비관리 코드를 통해 CLR을 로드할 때 BOF가 무엇을 사용하는지 매우 높은 수준에서 검토해 보겠습니다.

CLR 로드 스크린샷

CLR 로드

위의 간단한 스크린샷에서 보는 바와 같이 BOF가 CLR을 로드하기 위해 수행하는 주요 단계는 다음과 같습니다.

  1. ICLRMetaHost 인터페이스를 검색하는 데 사용할 CLRCreateInstance를 호출합니다.
  2. 그런 다음, ICLRMetaHost - > GetRuntime을 사용하여 요청한 .NET 버전에 대한 런타임 정보를 가져옵니다. 어셈블리가 .NET 버전 3.5 이하로 빌드된 경우 v2.0.50727을 요청하고, 어셈블리가 .NET 4.0 이상으로 빌드된 경우 v4.0.30319를 요청합니다. 실제로 BOF에는 .NET 어셈블리가 자동으로 사용하는 버전을 파악하는 데 도움이 되는 함수가 있지만 이에 대해서는 나중에 다루겠습니다.
  3. 런타임 정보가 확인되면 ICLRRuntimeInfo->IsLoadable을 사용하여 런타임을 프로세스에 로드할 수 있는지 확인합니다. 이때 다른 런타임이 이미 로드되어 있는지 여부를 고려하고, 런타임이 프로세스에서 로드될 수 있는 경우 BOOL 값 fLoadable을 1(true)로 설정합니다.
  4. 모든 것이 확인되면 ICLRRuntimeInfo->GetInterface를 실행하여 CLR을 프로세스에 로드하고 ICorRunTimeHost에 대한 인터페이스를 검색합니다.
  5. 마지막으로, ICorRuntimeHost - > Start를 호출하여 CLR을 시작합니다.

이제 CLR이 초기화되었지만 즐겨 사용하는 .NET 어셈블리를 실제로 실행하기 전에 해야 할 일이 조금 더 있습니다. Microsoft에서는 '애플리케이션이 실행되는 격리된 환경'이라고 설명하는 AppDomain 인스턴스를 만들어야 합니다. 즉, 이는 포스트 익스플로잇 .NET 어셈블리를 로드하고 실행하는 데 사용됩니다.

스크린샷: AppDomain 생성 및 어셈블리 로드/실행

생성 중인 AppDomain 및 로드/실행 중인 어셈블리

위의 간소화된 스크린샷에서 볼 수 있듯이 BOF가 .NET 어셈블리를 로드하고 호출하기 위해 취하는 주요 단계는 다음과 같습니다.

  1. ICorRuntimeHost->CreateDomain을 사용하여 고유한 AppDomain을 생성합니다.
  2. IUnknown->QueryInterface (pAppDomainThunk)를 사용하여 AppDomain 인터페이스에 대한 포인터를 가져옵니다.
  3. SafeArray를 만들고 .NET 어셈블리 바이트를 복사합니다.
  4. AppDomain->Load_3을 통해 어셈블리를 로드합니다.
  5. Assembly->EntryPoint를 통해 어셈블리의 진입점을 가져옵니다.
  6. 마지막으로 MethodInfo- > Invoke_3을 통해 어셈블리를 호출합니다.

이제 여러분은 비관리형 코드를 통한 .NET 실행에 대한 높은 수준의 이해를 갖게 되었습니다. 하지만 이 정도로는 실용적인 도구를 만드는 데는 한참 모자라니, BOF에서 구현된 몇 가지 기능을 살펴보며 평범한 수준에서 완전히 합법적인 수준으로 끌어올리는 방법을 알아보겠습니다.

콘솔 STDOUT을 Named Pipe 또는 Mail Slot으로 리디렉션: 도구 수정 방지

이것이 왜 중요한지 궁금하실 것입니다. 여러분이 저처럼 시간을 소중히 여기는 사람이라면, 일반적으로 콘솔 표준 출력으로 파이핑되는 모든 데이터가 포함된 문자열을 반환하도록 거의 모든 .NET 어셈블리를 수정하는 데 시간을 소비하고 싶지 않으실 것입니다. 그럴 줄 알았습니다. 이를 방지하려면 표준 아웃풋을 명명된 파이프 또는 메일 슬롯으로 리디렉션하고, 작성된 후 아웃풋을 읽은 다음, 원래 상태로 되돌리면 됩니다. 이렇게 하면 cmd.exe 또는 powerShell.exe에서와 마찬가지로 수정되지 않은 어셈블리를 실행할 수 있습니다. 이제 코드를 살펴보기 전에 @N4k3dTurtl3와 프로세스 실행 어셈블리 및 메일 슬롯에 대한 블로그 게시물에 감사를 표해야 합니다. 원래는 이 기술이 처음 나왔을 때 제 개인 C 임플란트에 이 기술을 구현하게 된 계기였고, 몇 달 후 동일한 기능을 BOF에 이식했습니다. 이제 소품이 제공되었으므로 아래의 명명된 파이프로 stdout을 리디렉션하여 이를 달성하는 간단한 예를 살펴보겠습니다.

스크린샷: 콘솔 표준 아웃풋을 명명된 파이프로 리디렉션하고 다시 되돌리기

콘솔 표준 출력을 명명된 파이프로 리디렉션하고 다시 되돌리기

어셈블리의 .NET 버전 결정

ICLRMetaHost - > GetRuntime을 통해 CLR을 로드할 때 필요한.NET 프레임워크의 버전을 지정해야 했던 것을 기억하시나요? .NET 어셈블리가 컴파일된 버전에 따라 다르다는 것을 기억하시나요? 매번 어떤 버전이 필요한지 수동으로 지정해야 하는 것은 그다지 재미가 없을 것입니다. 다행히도, @b4rtik가 Metasploit 프레임워크용 execute-assembly 모듈에 이를 처리하는 멋진 함수를 구현한 덕분에 저희가 아래에 보이는 저희 도구에 쉽게 구현할 수 있습니다.

스크린샷: .NET 어셈블리를 읽고 CLR을 로드할 때 필요한.NET 버전을 결정하는 데 도움이 되는 함수

.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 버전 변수를 설정하기 위한 if/else 문

AMSI(Antimalware Scan Interface) 패치

.NET 공격 수법에 대해 이야기하면서 AMSI에 대해 이야기하지 않을 수는 없었습니다. AMSI가 무엇이며 이를 우회하는 모든 방법에 대해서는 이미 여러 차례 다뤄진 바 있으므로, 깊이 있게 다루지는 않겠습니다. 다만 BOF를 통해 실행하기로 결정한 내용에 따라 AMSI 패치가 필요한 이유에 대해서는 간략히 설명하겠습니다. 예를 들어, 난독화 없이 Seatbelt를 실행하기로 결정한 경우 출력을 다시 얻지 못하고 비콘이 작동하지 않는다는 것을 금방 알 수 있습니다. 네, 작동을 멈추었습니다. AMSI가 귀하의 어셈블리를 포착하고 악의적이라고 판단하여 너무 시끄러운 하우스 파티처럼 폐쇄했기 때문입니다. 이는 이상적인 상황이 아닙니다. 이제 AMSI에 대해 두 가지 좋은 선택지가 있습니다. 하나는 ConfuserX 또는 Invisibility Cloak 같은 도구로 .NET 도구를 난독화하거나, 다양한 기법으로 AMSI를 비활성화하는 것입니다. 저희 경우에는 RastaMouse의 amsi.dll 메모리를 패치해서 E_INVALIDARG를 반환하고 스캔 결과가 0이 되도록 합니다. 블로그 글에서 지적했듯이, 보통 AMSI_RESULT_CLEAN로 해석됩니다. 아래에서 x64 프로세스용 코드의 간소화된 버전을 살펴보겠습니다.

스크린샷: AmsiScanBuffer의 인메모리 패치

AmsiScanBuffer의 인메모리 패치

스크린샷에서 볼 수 있듯이, 단순히 다음을 수행하기만 하면 됩니다.

  1. AMSI.DLL 로드 및 AmsiScanBuffer에 대한 포인터 가져오기
  2. 메모리 보호 변경
  3. amsiPatch[] 바이트의 패치
  4. 메모리 보호 기능을 원래 상태로 되돌리기

이를 도구에 구현하면 아래와 같이 –amsi 플래그를 사용하여 AMSI 감지를 우회하는 기본 버전의 Seatbelt.exe를 실행할 수 있습니다.

스크린샷: InlineExecute-Assemby AMSI 바이패스 예시

InlineExecute-Assemby AMSI 바이패스 예제

ETW(Event Tracing for Windows) 패치

다행스럽게도 방어자에게는 ETW를 사용하여 악성 .NET 공격 수법을 포착할 때 AMSI가 도와줄 수 있는 그 이상의 것이 있습니다. 안타깝게도 AMSI와 마찬가지로 이 역시 공격자가 우회하기 매우 쉬우며, @xpn은 이를 어떻게 우회할 수 있는지에 대한 멋진 연구를 수행했습니다. 아래에서 ETW를 패치하여 완전히 비활성화하는 방법에 대한 간단한 예를 살펴보겠습니다.

스크린샷: EtwEventWrite의 인메모리 패치

EtwEventWrite의 인메모리 패치

위 스크린샷에서 볼 수 있듯이 단계는 AMSI를 패치한 방법과 거의 동일하므로 이 단계에 대해서는 다루지 않겠습니다. 아래에서 –etw 플래그를 실행하기 전과 후의 스크린샷을 확인할 수 있습니다.

스크린샷: –etw 플래그와 함께 inlineExecute-Assembly를 실행하기 전에 Process Hacker를 사용하여 PowerShell.exe 속성 보기

–etw 플래그와 함께 inlineExecute-Assembly를 실행하기 전에 Process Hacker를 사용하여 PowerShell.exe 속성 보기

스크린샷: –etw 플래그를 사용하여 인라인-실행-어셈블리 실행

–etw 플래그를 사용하여 인라인-실행-어셈블리 실행

스크린샷: Process Hacker를 사용하여 inlineExecute-Assembly를 실행한 후 동일한 PowerShell.exe 속성 보기

Process Hacker를 사용하여 inlineExecute-Assembly 실행 후 동일한 PowerShell.exe 속성 보기

고유 AppDomains, 명명된 파이프, 메일 슬롯

기본적으로 생성된 AppDomain, Named Pipe 또는 Mail Slot은 기본값인 'totesLegit'을 사용합니다. 이러한 값은 제공된 공격자 스크립트에서 변경하거나 명령줄 플래그를 통해 즉시 변경하여 테스트 중인 환경에 더 잘 어울리도록 변경할 수 있습니다. 명령줄을 통해 변경하는 예시는 다음과 같습니다.

Beacon에서 InlineExecute-Assembly --dotnetassembly /root/Desktop/MessageBoxCS.exe 명령의 실행을 보여주는 터미널 스크린샷. 아웃풋에는 inlineExecute-Assembly 실행, 호스트 호출 완료(16319 바이트 전송), 아웃풋 ‘Hello From .NET!’ 수신, 완료 메시지 ‘inlineExecute-Assembly 완료’ 등의 상태 메시지가 포함됩니다.

고유한 AppDomain 이름과 고유한 명명된 파이프 이름을 사용하는 InlineExecute-Assembly 예시

스크린샷: 고유한 앱 도메인 이름 ChangedMe 예시

고유한 AppDomain 이름 ChangedMe 예시

스크린샷: 고유한 명명된 파이프 LookAtMe 예시

고유하게 명명된 파이프 LookAtMe 예시

스크린샷: 성공적인 실행이 완료된 후 제거되는 AppDomain 예시

성공적인 실행이 완료된 후 제거되는 AppDomain 예시

스크린샷: 실행이 성공적으로 완료된 후 명명된 파이프가 제거되는 예시

성공적인 실행이 완료된 후 명명된 파이프가 제거되는 예시

주의 사항

이 섹션은 GitHub 리포지토리에서 언급한 내용을 거의 반복하지만, 이 도구를 사용할 때 염두에 두어야 할 몇 가지 사항을 반복해서 설명하는 것이 중요하다고 생각했습니다.

  1. 최대한 안정적으로 만들려고 노력했지만, 절대 충돌이 발생하지 않고 비콘이 작동을 멈추지 않는다는 보장은 없습니다. 문제가 발생해도 비콘이 작동하는 포크 앤 런이라는 추가적인 사치를 누릴 수 없습니다. 이것이 바로 BOF와의 절충점입니다. 그렇기 때문에 어셈블리가 제대로 작동하는지 미리 테스트하는 것이 얼마나 중요한지 아무리 강조해도 지나치지 않습니다.
  2. BOF는 프로세스 중에 실행되고 실행 중에 비콘을 대신하므로 장기 실행 어셈블리에 사용하기 전에 이를 고려해야 합니다. 결과를 가져오는 데 오랜 시간이 걸리는 작업을 실행하기로 선택한 경우, 결과가 반환되고 어셈블리 실행이 완료될 때까지 더 많은 명령을 실행할 수 있는 비콘이 활성화되지 않습니다. 이 또한 절전 모드 설정을 준수하지 않습니다. 예를 들어, 절전 모드가 10분으로 설정되어 있고 BOF를 실행하면 BOF 실행이 완료되는 즉시 결과를 얻을 수 있습니다.
  3. PE를 메모리에 로드하는 도구(예: SafetyKatz)를 수정하지 않는 한, 이러한 도구는 비콘의 작동을 멈출 가능성이 높습니다. 이러한 도구 중 다수는 종료하기 전에 희생 프로세스의 콘솔 출력을 보낼 수 있기 때문에 어셈블리 실행과 함께 잘 작동합니다. 진행 중인 BOF를 통해 종료하면 프로세스가 종료되어 비콘이 종료됩니다. 작동하도록 수정할 수 있지만, OPSEC에 적합하지 않은 다른 항목이 프로세스에 로드되어 제거되지 않을 수 있으므로 어셈블리 실행을 통해 이러한 유형의 어셈블리를 실행하는 것이 좋습니다.
  4. 어셈블리에서 Environment.Exit를 사용하는 경우 프로세스와 비콘을 종료하므로 이를 제거해야 합니다.
  5. 명명된 파이프와 메일 슬롯은 고유해야 합니다. 데이터를 수신하지 못했고 비콘이 여전히 작동 중이라면, 문제는 다른 명명된 파이프 또는 메일 슬롯 이름을 선택해야 할 가능성이 높습니다.

방어 고려 사항

다음은 몇 가지 방어 고려 사항입니다.

  1. AMSI 및 ETW 메모리 패치를 수행할 때 PAGE_EXECUTE_READWRITE를 사용합니다. 이는 의도적으로 수행된 것이며 PAGE_EXECUTE_READWRITE의 메모리 보호 기능을 갖춘 메모리 범위를 가진 프로그램이 거의 없기 때문에 위험 신호로 간주해야 합니다.
  2. 생성된 Named Pipe의 기본 이름은 'totesLegit'입니다. 이는 의도적으로 수행되었으며, 서명 감지 기능을 사용하여 이를 식별할 수 있습니다.
  3. 생성된 메일 슬롯의 기본 이름은 'totesLegit'입니다. 이는 의도적으로 수행되었으며, 서명 감지 기능을 사용하여 이를 식별할 수 있습니다.
  4. 로드된 AppDomain의 기본 이름은 'totesLegit'입니다. 이는 의도적으로 수행되었으며, 서명 감지 기능을 사용하여 이를 식별할 수 있습니다.
  5. .NET의 악의적 사용 탐지를 위한 유용한 팁 (@bohops 사용) 여기, (F-Secure 사용) 여기, 여기
  6. CLR을 로드해서는 안 되는 비관리형 프로세스와 같이 의심스러운 프로세스에 .NET CLR이 로드되는지 확인합니다.
  7. 이벤트 추적에 대해 자세히 알아봅니다.
  8. 다른 알려진 Cobalt Strike Beacon IOC 또는 C2 송신/통신 IOC를 찾습니다.