Summarize this content to 500 words
이 블로그에서는 타이머를 사용하여 호출 스택을 스푸핑하는 PoC 기술을 소개합니다. 임플란트가 잠들기 전에 타이머를 대기시켜 호출 스택을 가짜로 덮어쓴 다음 실행을 다시 시작하기 전에 원본을 복원할 수 있습니다. 따라서 수면 중에 임플란트에 속한 메모리를 마스킹할 수 있는 것과 같은 방식으로 메인 스레드의 호출 스택도 마스킹할 수 있습니다. 또한 이 접근 방식은 다른 호출 스택 스푸핑 접근 방식의 일반적인 X64 스택 해제의 복잡성을 처리할 필요가 없습니다.
호출 스택 문제
공격자의 관점에서 핵심 메모리 회피 문제는 임플란트가 일반적으로 주입된 코드에서 작동한다는 것입니다(모든 모듈 비우기 접근 방식 무시). 따라서 최신 감지의 핵심 중 하나는 백업되지 않은(또는 ‘부동’) 메모리에 속하는 스레드 생성을 모니터링하는 것입니다. 이것 블로그 Elastic에 의한 것은 EDR 관점에서 비정상적인 스레드 감지 측면에서 최신 기술에 대한 좋은 근사치입니다.
그러나 이 문제가 공격자에게 시사하는 또 다른 의미는 모든 임플란트의 API 호출이 지원되지 않는 메모리에서도 발생한다는 것입니다. 특정 API 호출 시 호출 스택을 검사하거나 실행 중인 스레드(예: 휴면 중인 스레드)를 사전에 검사하여 지원되지 않는 메모리에 대한 반환 주소를 통해 의심스러운 호출 스택을 식별할 수 있습니다.
이것은 역사적으로 최신 EDR 스택(내 경험상)에서 엄청난 양의 집중/연구를 받지 못한 하나의 감지 영역입니다. 그러나 이것은 다음과 같은 오픈 소스 도구의 출시로 바뀌기 시작했습니다. 헌트-슬리핑-비콘지원되지 않는 영역이 있는 호출 스택을 찾기 위해 “휴면” 스레드를 사전에 검사합니다. 이것은 명백히 의심스러운 활동에 대한 높은 신뢰도 신호를 제공합니다. 따라서 EDR에 가치가 있으며 공격자가 회피 TTP에서 심각하게 고려해야 할 사항입니다.
미사용 호출 스택 검사
공격자의 관점에서 해결해야 할 첫 번째 문제는 이러한 유형의 검사를 우회할 수 있도록 휴면 스레드의 호출 스택을 조작하는 방법입니다. 이는 실제 스레드 자체 또는 일부 외부 메커니즘(APC 등)을 통해 수행될 수 있습니다.
일반적으로 이를 “미사용 스푸핑”이라고 합니다(여기서 Kyle Avery에게 이 용어는 우수한 블로그 메모리 스캐너 피하기). 이 문제를 해결하기 위한 첫 번째 공개 시도는 mgeeky의 ThreadStackSpoofer스택의 마지막 반환 주소를 덮어씁니다.
참고로 이 문제에 접근하는 반대 방법은 스레드나 호출 스택이 전혀 존재하지 않는 것입니다. 데스슬립. 이 기술의 단점은 지원되지 않는 스레드가 반복적으로 생성될 가능성이 있다는 것입니다(정확한 구현에 따라 다름). 이는 현대 환경에서 훨씬 더 큰 악입니다. 다만, 향후 사용 하드웨어 스택 보호 EDR 공급업체가 이러한 유형의 접근 방식을 불가피하게 만들 수 있습니다.
실행 중 호출 스택 검사 – 사용자 모드
두 번째 문제는 실행 중 호출 스택 검사입니다., 사용자 모드 또는 커널 모드에서 구현될 수 있습니다. 사용자 모드 구현 측면에서 이것은 일반적으로 일반적으로 남용되는 함수를 후킹하고 호출이 시작된 위치를 확인하기 위해 스택을 탐색하는 것을 포함합니다. 지원되지 않는 메모리를 찾으면 의심할 가능성이 높습니다. 이에 대한 확실한 예는 WinInet 함수를 호출하는 삽입된 쉘코드 스테이저입니다. MalMemDetect 이 탐지 기술을 보여주는 오픈 소스 프로젝트의 좋은 예입니다.
이러한 시나리오의 경우 다음과 같은 기술 RET 주소 스푸핑 일반적으로 호출 스택에서 지원되지 않는 주소의 증거를 제거하기에 충분합니다. 높은 수준에서 여기에는 스택의 마지막 반환 주소를 수동으로 교체하고 대상 함수가 트램펄린 가젯(예: jmp rbx)으로 반환되도록 리디렉션하는 대상 함수 주위에 작은 어셈블리 하네스를 삽입하는 작업이 포함됩니다.
추가적으로, 있다 사일런트문워크 영리한 동기화 해제 접근 방식을 사용합니다(본질적으로 X64 스택 해제 코드에 구축된 ROP 가젯). 이렇게 하면 함수 호출의 출처를 동적으로 숨길 수 있으며 유사하게 이러한 기본 감지 휴리스틱을 우회합니다. 운영자에게 가장 중요한 점은 이 두 기술 모두 작동 중인 스레드 자체에서 수행할 수 있으며 외부 메커니즘이 필요하지 않다는 것입니다.
opsec 관점에서 이 블로그에서 참조된 많은 기술이 비정상적인 호출 스택을 생성할 수 있다는 점에 유의하는 것이 중요합니다. 이것이 문제인지 여부는 대상 환경과 사용 중인 보안 제어에 따라 다릅니다. 주요 고려 사항은 작업에 의해 생성된 호출 스택이 어딘가(예: 커널, 다음 섹션 참조)에 기록되고 이벤트/경고에 추가되는지 여부입니다. 이 경우 훈련된 눈(예: 위협 사냥꾼/IR)에게 의심스러워 보일 수 있습니다.
이를 입증하기 위해 SilentMoonWalk의 비동기화 스택 스푸핑 기술을 예로 들 수 있습니다(다른 기술은 구현에 따라 다를 수 있으므로 이것은 약간 더 쉬운 사용 사례입니다). 이전에 언급했듯이 이 기술은 특정 스택 감기 작업을 구현하는 함수를 찾아야 합니다(X64 스택 풀기의 전체 개요는 이 블로그의 범위를 벗어납니다. 이것 추가 읽기를 위한 우수한 CodeMachine 기사).
예를 들어 첫 번째 프레임은 항상 다음을 수행해야 합니다. UWOP_SET_FPREG 작업두번째 UWOP_PUSH_NONVOL (rbp) 등은 아래 windbg에서 설명합니다.
0:000> knf
# Memory Child-SP RetAddr Call Site
00 0000001d`240feb98 00007ffe`b622d831 win32u!NtUserWaitMessage+0x14
(…)
08 40 0000001d`240ff140 00007ffe`b483b576 KERNELBASE!CreatePrivateObjectSecurity+0x31
09 40 0000001d`240ff180 00007ffe`b48215a5 KERNELBASE!Internal_EnumSystemLocales+0x406
0a 3e0 0000001d`240ff560 00007ffe`b4870e22 KERNELBASE!SystemTimeToTzSpecificLocalTimeEx+0x25
0b 680 0000001d`240ffbe0 00007ffe`b6d87614 KERNELBASE!PathReplaceGreedy+0x82
0c 100 0000001d`240ffce0 00007ffe`b71826a1 KERNEL32!BaseThreadInitThunk+0x14
0d 30 0000001d`240ffd10 00000000`00000000 ntdll!RtlUserThreadStart+0x21
0:000> .fnent KERNELBASE!PathReplaceGreedy+0x82
Debugger function entry 000001cb`dda19c60 for:
(00007ffe`b4870da0) KERNELBASE!PathReplaceGreedy+0x82 | (00007ffe`b4871050) KERNELBASE!SortFindString
(…)
06: offs 13, unwind op 3, op info 2 UWOP_SET_FPREG.
0:000> .fnent KERNELBASE!SystemTimeToTzSpecificLocalTimeEx+0x25
Debugger function entry 000001cb`dda19c60 for:
(00007ffe`b4821580) KERNELBASE!SystemTimeToTzSpecificLocalTimeEx+0x25 | (00007ffe`b482182c) KERNELBASE!AddTimeZoneRules
(…)
08: offs b, unwind op 0, op info 5 UWOP_PUSH_NONVOL reg: rbp.
이 출력은 스푸핑된 SilentMoonwalk 스레드(knf)의 호출 스택과 호출 스택(PathReplaceGreedy / SystemTimeToTzSpecificLocalTimeEx)에서 발견된 두 함수의 해제 작업(.fnent)을 보여줍니다.
중요한 점은 이로 인해 적법한 코드 경로에 대해 결코 발생하지 않는(따라서 비정상적인) 호출 스택이 생성된다는 것입니다. 따라서 KERNELBASE!PathReplaceGreedy는 KERNELBASE!SystemTimeToTzSpecificLocalTimeEx … 등을 호출하지 않습니다. 또한 EDR은 절전 스레드의 사전 검색 중에 이 해제 코드 패턴을 자체적으로 검색하려고 시도할 수 있습니다. 다시 말하지만 이것이 문제인지 여부는 전적으로 제어/원격 측정에 달려 있지만 운영자로서 우리가 사용할 수 있는 모든 기술의 장단점을 이해하는 것은 항상 가치가 있습니다.
마지막으로 ‘깨끗한’ 호출 스택으로 API를 호출하는 간단한 방법은 다른 작업을 수행하는 것입니다. 일반적인 예는 OS에서 제공하는 모든 콜백 유형 기능을 사용하는 것입니다(스레드 생성 시작 주소 추론을 우회하는 경우에도 동일하게 적용됨). 대부분의 콜백에 대한 제한은 일반적으로 하나의 인수만 제공할 수 있다는 것입니다. 좋은 연구 이에 대한 방법을 보여줍니다).
실행 중 호출 스택 검사 – 커널 모드
사용자 모드 호출 스택은 커널 콜백 기능(즉, 프로세스 생성, 스레드 생성/종료, 핸들 액세스 등) 중에 인라인으로 캡처될 수 있습니다. 예를 들어 SysMon 드라이버는 다음을 사용합니다. Rtl도보액자체인 모든 프로세스 액세스 이벤트에 대한 사용자 모드 호출 스택을 수집하기 위해(예: 오픈프로세스 핸들을 얻기 위해). 따라서 이 기능을 사용하면 LSASS에 대한 핸들을 열려고 시도하는 백업되지 않은 메모리/주입된 코드(‘UNKNOWN’)를 쉽게 발견할 수 있습니다. 예를 들어, 이 고안된 시나리오에서는 다음과 유사한 호출 스택을 얻게 됩니다.
0:020> knf
# Memory Child-SP RetAddr Call Site
00 0000004c`453cf428 00007ffd`7f1006fe ntdll!NtOpenProcess
01 8 0000004c`453cf430 00007ff6`98fe937f KERNELBASE!OpenProcess+0x4e
02 70 0000004c`453cf4a0 000002ad`c3fd1121 000002ad`c3fd1121 (UNKNOWN)
또한 이제 다음이 가능합니다. 호출 스택 수집 와 더불어 ETW 위협 인텔리전스 공급자. 호출 스택 주소는 확인되지 않지만(즉, EDR은 기호를 확인하기 위해 자체 내부 프로세스 모듈 캐시를 유지해야 함) 본질적으로 EDR 공급업체가 거의 실시간 호출 스택을 캡처할 수 있는 가능성을 제공합니다(여기서 기호는 비동기식으로 확인됨). 따라서 이는 핵심적으로 커널에 캡처된 사용자 모드 후킹을 직접 대체하는 것으로 볼 수 있습니다. 백업되지 않은/직접 API 호출이 민감한 기능 (VirtualAlloc/QueueUserApc/SetThreadContext/VirtualProtect 등) 감지하기가 쉽지 않습니다.
이러한 시나리오는 실행 중 호출 스택 스푸핑에 대한 내 이전 연구의 일부에 대한 전제였습니다. https://github.com/WithSecureLabs/CallStackSpoofer. 그만큼 아이디어 호출이 지원되지 않는 메모리에서 시작되었다는 사실을 숨기기 위해 가짜 상태로 초기화할 수 있는 새 스레드에 대한 API 호출을 오프로드하는 것이었습니다. 내 원래 PoC는 이 아이디어를 OpenProcess에 적용했지만 이미지 로드 등에 쉽게 적용할 수 있습니다.
여기서 핵심 요구 사항은 어느 임의의 호출 스택이 스푸핑될 수 있으므로 위협 사냥꾼이 호출 스택이 포함된 경고를 검토하더라도 여전히 다른 스레드와 구분할 수 없는 것처럼 보입니다. 이 접근 방식의 단점은 새 스레드를 생성해야 하고 이 스푸핑된 스레드를 가장 잘 처리하는 방법과 하드 코딩/정적 호출 스택에 대한 의존도였습니다.
호출 스택 마스킹
콜 스택 스푸핑에 대한 현재 연구 상태를 간략하게 검토한 후 이 블로그에서는 새로운 콜 스택 스푸핑 기술인 콜 스택 마스킹을 시연합니다. 이 블로그 게시물에 소개된 PoC는 외부 메커니즘(타이머)을 통해 휴면 스레드의 호출 스택을 마스킹하여 미사용 스푸핑 문제를 해결합니다.
과거에 이 주제를 조사하는 동안 저는 스택 스푸핑을 수행하는 TTP를 생성하기 위해 X64 스택 풀기의 복잡성을 파악하는 데 많은 시간을 보냈습니다. 이러한 복잡성은 위에서 논의한 다른 여러 기술에도 존재합니다. 그러나 이러한 복잡함을 처리할 필요 없이 호출 스택을 스푸핑/마스킹하는 훨씬 간단한 방법이 있다는 생각이 들었습니다.
모든 종류의 대기를 수행하는 일반 스레드를 고려하면 정의에 따라 대기가 충족될 때까지 자체 스택을 수정할 수 없습니다. 또한 스택은 항상 읽기/쓰기가 가능합니다. 따라서 타이머를 사용하여 다음을 수행할 수 있습니다.
현재 스레드 스택의 백업 생성
가짜 스레드 스택으로 덮어쓰기
실행 재개 직전에 원래 스레드 스택 복원
모든 타이머 개체를 사용할 수 있지만 편의상 C5Spider의 PoC를 기반으로 PoC를 만들었습니다. Ekko 수면 난독화 기술.
남은 과제는 대상 스레드가 잠든 후 RSP 값을 계산하는 것입니다. 이것은 컴파일러 내장 함수를 사용하여 달성할 수 있습니다(_AddressOfReturnAddress) 현재 프레임의 Child-SP를 얻습니다. 이것이 있으면 예상되는 다음 두 프레임(예: KERNELBASE!WaitForSingleObjectEx 및 ntdll!NtWaitForSingleObject)의 총 스택 사용률을 빼서 절전 시간에 예상되는 RSP 값을 찾을 수 있습니다.
마지막으로 마스킹된 스레드를 가능한 한 사실적으로 보이게 하기 위해 기존(및 합법적인) 스레드의 시작 주소와 호출 스택을 복사할 수 있습니다.
PoC || GTFO
PoC는 여기에서 찾을 수 있습니다. https://github.com/Cobalt-Strike/CallStackMasker.
PoC는 정적 및 동적의 두 가지 모드로 작동합니다. 정적 모드에는 Process Explorer를 통해 spoolsv.exe에서 찾은 하드 코딩된 호출 스택이 포함되어 있습니다. 이 스레드는 아래와 같으며 KERNELBASE!WaitForSingleObjectEx를 통해 ‘Wait:UserRequest’ 상태에 있는 것으로 볼 수 있습니다.
아래 스크린샷은 정적 호출 스택 마스킹을 보여줍니다. 마스킹된 스레드의 시작 주소와 호출 스택은 위의 spoolsv.exe에서 식별된 스레드와 동일합니다.
정적 모드의 명백한 단점은 우리가 여전히 하드 코딩된 호출 스택에 의존하고 있다는 것입니다. 이 문제를 해결하기 위해 PoC는 동적 호출 스택 마스킹도 구현합니다. 이 모드에서는 호스트에서 액세스 가능한 모든 스레드를 열거하고 원하는 대상 상태(예: WaitForSingleObjectEx를 통한 UserRequest)에서 하나를 찾습니다. 적합한 스레드 스택이 발견되면 이를 복사하여 휴면 스레드를 마스킹하는 데 사용합니다. 마찬가지로 PoC는 복제된 스레드의 시작 주소를 다시 한 번 복사하여 마스킹된 스레드가 합법적으로 보이도록 합니다.
‘–동적’ 플래그로 PoC를 실행하면 아래와 같이 흉내낼 다른 스레드의 호출 스택을 찾습니다.
위에서 식별된 대상 프로세스(taskhostw.exe / 4520), 스레드(5452) 및 호출 스택은 Process Explorer에서 아래에 표시됩니다.
이제 CallStackMasker에 속한 메인 스레드의 호출 스택과 시작 주소를 검사하면 모방된 스레드와 동일하다는 것을 알 수 있습니다.
다음은 explorer.exe에서 스푸핑까지 shcore.dll 기반 스레드 호출 스택을 동적으로 찾는 CallStackMasker의 또 다른 예입니다.
아래 스크린샷은 실제 ‘unmasked’ 호출 스택을 보여줍니다.
현재 PoC는 WaitForSingleObject만 지원하지만 WaitForMultipleObjects에 대한 지원을 추가하는 것은 간단합니다.
마지막으로 이 PoC는 타이머-대기열 타이머를 사용합니다. 이전에 시연한 것처럼 메모리에 열거할 수 있습니다. https://github.com/WithSecureLabs/TickTock. 그러나 이 잠재적인 탐지 기회를 피하기 위해 완전한 커널 타이머를 사용하도록 이 PoC를 수정할 수 있습니다.