1년에 여러 번, Elastic Security Lab 연구원들은 혼자 또는 팀으로 원하는 프로젝트를 자유롭게 선택하고 파고들 수 있습니다. 이 시간을 내부적으로 '온-위크' 프로젝트라고 합니다. 이 글은 최근 온주간 세션에서 자세히 살펴본 Microsoft에서 개발한 시간 여행 디버깅 (TTD) 기술에 초점을 맞춘 시리즈 중 첫 번째 글입니다.
몇 년 동안 공개되었음에도 불구하고 인포섹 커뮤니티 내에서 TTD와 그 잠재력에 대한 인식은 크게 과소평가되어 있습니다. 이 2부로 구성된 시리즈가 프로그램 디버깅, 취약점 연구 및 악용, 멀웨어 분석에 TTD가 어떻게 유용하게 활용될 수 있는지에 대해 알려드리는 데 도움이 되기를 바랍니다.
이 연구에는 먼저 TTD의 내부 작동 방식을 이해한 다음, 이를 통해 만들 수 있는 몇 가지 흥미로운 적용 가능한 용도를 평가하는 작업이 포함되었습니다. 이 게시물에서는 연구자들이 TTD를 심층적으로 분석하는 방법론과 몇 가지 흥미로운 결과를 공유합니다. 두 번째 파트에서는 맬웨어 분석 및 Elastic Security와의 통합을 목적으로 TTD의 적용 가능한 사용에 대해 자세히 설명합니다.
배경
시간 여행 디버깅은 Microsoft Research에서 개발한 도구로, 사용자가 실행을 기록하고 바이너리의 사용자 모드 런타임을 자유롭게 탐색할 수 있습니다. TTD 자체는 두 가지 기술에 의존합니다: 바이너리 번역을 위한 Nirvana와 추적 읽기/쓰기 프로세스를 위한 iDNA입니다. Windows 7부터 사용 가능한 TTD의 내부 구조는 공개적으로 사용 가능한 문서에 처음 자세히 설명되어 있습니다. 그 이후로 Microsoft와 독립 연구자들은 이 문제를 매우 자세히 다루었습니다. 따라서 두 기술의 내부를 자세히 살펴보지는 않겠습니다. 대신, Elastic 연구원들은 TTD 구현을 작동시키는 에코시스템, 즉 실행 파일, DLL 및 드라이버를 조사했습니다. 이로 인해 TTD는 보호된 프로세스와 같은 특수한 경우에 의도한 대로 작동하기 위해 일부 (문서화되지 않은) 기술을 활용하기 때문에 TTD뿐만 아니라 Windows 자체에 대한 흥미로운 발견이 이루어졌습니다.
그렇다면 왜 TTD를 조사하는 것일까요? 순수한 호기심 외에도 이 기술의 의도된 용도 중 하나는 프로덕션 환경의 버그를 발견하는 것일 가능성이 높습니다. 버그를 트리거하거나 재생산하기 어려운 경우, '한 번 기록하면 항상 재생되는' 유형의 환경이 있으면 이러한 어려움을 보완하는 데 도움이 되는데, 이것이 바로 WinDbg와 결합된 TTD가 구현하는 방식입니다.
WinDbg와 같은 디버깅 도구는 일반적으로 일반 텍스트로 이해하기 쉬운 추가 정보를 제공하기 때문에 Windows 구성 요소를 리버스할 때 항상 엄청난 정보 소스가 되어 왔습니다. 디버깅 도구(특히 디버거)는 기본 운영 체제와 협력해야 하며, 여기에는 디버깅 인터페이스 및/또는 OS에서 이전에 공개되지 않은 기능이 포함될 수 있습니다. TTD는 이 패턴을 따릅니다.
높은 수준의 개요
TTD는 먼저 애플리케이션이 실행하는 모든 명령을 추적하는 기록을 생성하여 데이터베이스(.run 접미사)에 저장하는 방식으로 작동합니다. 기록된 트레이스는 WinDbg 디버거를 사용하여 마음대로 재생할 수 있으며, 처음 액세스하면 .run 파일을 사용하여 데이터베이스를 더 빠르게 탐색할 수 있습니다. 임의 프로세스의 실행을 추적할 수 있도록 TTD는 온디맨드 활동 기록을 담당하는 DLL을 삽입하여 프로세스를 생성하여 기록할 수 있지만 이미 실행 중인 프로세스에 첨부할 수도 있습니다.
TTD는 MS 스토어에서 WinDbg 프리뷰 패키지의 일부로 무료로 다운로드할 수 있습니다. 이 글에서 집중적으로 다룰 x64 아키텍처의 경우 C:\Program Files\WindowsApps\Microsoft.WinDbg_<version></version>_<arch>__8wekyb3d8bbwe\amd64\ttd
에 있는 독립형 컴포넌트이며, WinDbg Preview(일명 WinDbgX)에서 직접 사용할 수 있습니다. x86 및 arm64 버전도 MS 스토어에서 다운로드할 수 있습니다.
패키지는 두 개의 EXE 파일(TTD.exe 및 TTDInject.exe)로 구성됩니다. 및 소수의 DLL. 이 연구는 너바나/iDNA와 관련이 없는 모든 것을 담당하는 주요 DLL에 초점을 맞춥니다(예 세션 관리, 드라이버 통신, DLL 주입 등을 담당): ttdrecord.dll
참고: 이 연구는 대부분 2018 버전(1.9.106.0)과 1.9.106.0 버전(1.9.106.0)의 두 가지 ttdrecord DLL을 사용하여 수행되었습니다. SHA256=aca1786a1f9c96bbe1ea9cef0810c4d164abbf2c80c9ecaf0a1ab91600da6630), and early 2022 version (10.0.19041.1 SHA256=1FF7F54A4C865E4FBD63057D5127A73DA30248C1FF28B99FF1A43238071CBB5C). 이전 버전에는 더 많은 심볼이 있어 리버스 엔지니어링 프로세스 속도를 높이는 데 도움이 되는 것으로 나타났습니다. 그런 다음 구조와 함수 이름을 최신 버전에 맞게 다시 조정했습니다. 따라서 최신 버전에서 재현하려는 경우 여기에 설명된 구조 중 일부가 동일하지 않을 수 있습니다. _
TTD 기능 살펴보기
명령줄 매개 변수
독자들은 TTD.exe가 기본적으로 ttdrecord!ExecuteTTTracerCommandLine의 래퍼 역할을 한다는 점에 유의해야 합니다:
HRESULT wmain()
{
v28 = 0xFFFFFFFFFFFFFFFEui64;
hRes = CoInitializeEx(0i64, 0);
if ( hRes >= 0 )
{
ModuleHandleW = GetModuleHandleW(L"TTDRecord.dll");
[...]
TTD::DiagnosticsSink::DiagnosticsSink(DiagnosticsSink, &v22);
CommandLineW = GetCommandLineW();
lpDiagnosticsSink = Microsoft::WRL::Details::Make<TTD::CppToComDiagnosticsSink,TTD::DiagnosticsSink>(&v31, DiagnosticsSink);
hRes = ExecuteTTTracerCommandLine(*lpDiagnosticsSink, CommandLineW, 2i64);
[...]
위 코드 발췌의 마지막 줄에는 마지막 인수로 정수를 받는 ExecuteTTTracerCommandLine 호출이 나와 있습니다. 이 인수는 원하는 추적 모드에 해당합니다: - 0 -> 풀트레이싱모드, - 1 -> 무제한 추적 및 - 2 -> 스탠드얼론(공개 버전의 TTD.exe에 하드코딩된 모드).
TTD를 전체 추적 모드로 강제로 실행하면 프로그램 및 서비스에 대한 프로세스 재부모(-parent) 및 재부팅할 때까지 자동 추적(-onLaunch) 등의 일부 숨겨진 기능을 포함한 사용 가능한 옵션이 표시됩니다.
TTDRecord.dll의 전체 옵션 세트를 덤프하면 다음과 같은 흥미로운 숨겨진 명령줄 옵션이 발견되었습니다:
-persistent Trace programs or services each time they are started (forever). You must specify a full path to the output location with -out.
-delete Stop future tracing of a program previously specified with -onLaunch or -persistent. Does not stop current tracing. For -plm apps you can only specify the package (-delete <package>) and all apps within that package will be removed from future tracing
-initialize Manually initialize your system for tracing. You can trace without administrator privileges after the system is initialized.
너바나를 설정하는 과정에서는 TTD가 대상 _EPROCESS에 InstrumentationCallback 필드를 설정해야 합니다. 이는 (문서화되어 있지 않지만 알려진) NtSetInformationProcess(ProcessInstrumentationCallback) 시스템 호출(값이 40인 ProcessInstrumentationCallback)을 통해 이루어집니다. 보안에 영향을 미칠 수 있으므로 이 시스콜을 호출하려면 상승된 권한이 필요합니다. 흥미롭게도 -initialize 플래그는 TTD를 Windows 서비스로 배포할 수 있음을 암시하기도 합니다. 이러한 서비스는 추적 요청을 임의의 프로세스로 프록시하는 역할을 담당합니다. 이를 실행하고 결과 오류 메시지를 확인하여 확인할 수 있습니다:
TTDService.exe 의 존재를 확인하는 증거를 쉽게찾을 수 있지만 이 파일은 공개 패키지의 일부로 제공되지 않았으므로 TTD가 서비스로 실행될 수 있다는 점을 제외하고는 이 게시물에서 다루지 않겠습니다.
TTD 프로세스 주입
설명한 대로 TTD 추적 파일은 독립 실행형 바이너리 TTD.exe 또는 서비스 TTDService.exe(비공개)를 통해 만들 수 있으며, 둘 다 권한이 있는 컨텍스트에서 실행해야 합니다. 그러나 이는 런처일 뿐이며 레코딩 DLL(TTDRecordCPU.dll)을 주입하는 것은 다른 프로세스의 작업입니다: TTDInject.exe입니다.
TTDInject.exe는 TTD.exe보다 눈에 띄게 큰 또 다른 실행 파일이지만, 추적 세션 준비라는 매우 간단한 목적을 가지고 있습니다. 지나치게 단순화된 보기에서 TTD.exe는 먼저 일시 중단된 상태에서 녹화할 프로세스를 시작합니다. 그러면 TTDInject.exe가 스폰됩니다, 세션을 준비하는 데 필요한 모든 인수를 전달합니다. 앞서 언급한 추적 모드에 따라 TTDInject가 프로세스를 직접 스폰할 수도 있으므로 가장 일반적인 동작(즉, TTD.exe에서 스폰되는 경우)에 대해 설명합니다.
TTDInject는 기록된 프로세스에서 TTDLoader!InjectThread를 실행하는 스레드를 생성하고, 다양한 유효성 검사 후 모든 프로세스 활동을 기록하는 라이브러리인 TTDRecordCPU.dll을 차례로 로드합니다.
그 시점부터 실행 중에 발생한 모든 명령어, 메모리 액세스, 트리거된 예외 또는 CPU 상태가 기록됩니다.
TTD의 일반적인 워크플로우를 이해하고 나니 세션 초기화 후에는 조작이 거의 또는 전혀 불가능하다는 것이 분명해졌습니다. 따라서 ttdrecord.dll이 지원하는 인수에 더 많은 주의를 기울였습니다. C++ 맹글링 함수 형식 덕분에 함수 이름 자체에서 많은 중요 정보를 검색할 수 있어 명령줄 인수 구문 분석이 비교적 간단합니다. 발견한 흥미로운 플래그 중 하나는 PplDebuggingToken이었습니다. 이 플래그는 숨겨져 있으며 무제한 모드에서만 사용할 수 있습니다.
이 플래그의 존재는 즉시 의문을 불러일으켰습니다: TTD는 Windows 7 및 8과 Windows 8.1+에서 처음 설계되었습니다. 프로세스에 보호 수준이라는 개념이 추가되어 보호 수준이 같거나 낮은 프로세스에 대해서만 핸들을 열 수 있도록 지정했습니다. 커널의 _EPROCESS 구조체에 있는 단순한 바이트이므로 사용자 모드에서 직접 수정할 수 없습니다.
보호 수준 바이트의 값은 잘 알려져 있으며 아래 표에 요약되어 있습니다.
로컬 보안 기관 하위 시스템(lsass.exe) 를 보호된 프로세스 표시등으로 실행하도록 구성하여 호스트에 대한 최대 권한을 얻은 침입자의 범위를 제한할 수 있습니다. 커널 수준에서 작동하므로 사용자 모드 프로세스는 아무리 권한이 있더라도 lsass에 대한 핸들을 열 수 없습니다.
그러나 PplDebuggingToken 플래그는 그렇지 않은 것으로 보입니다. 그런 플래그가 존재한다면 모든 펜테스터/레드팀원들의 꿈일 것입니다. 보호된 프로세스에 주입하여 기록하고 메모리를 덤프할 수 있는 (마법 같은) 토큰이 바로 그것입니다. 명령줄 구문 분석기는 명령 플래그의 내용이 단순한 와이드 문자열임을 암시하는 것 같습니다. PPL 백도어일까요?
PPL 디버깅 토큰 추적하기
ttdrecord.dll로 돌아갑니다, 명령줄 옵션은 파싱되어 TTD 세션을 만드는 데 필요한 모든 옵션과 함께 컨텍스트 구조에 저장됩니다. 이 값은 여러 위치에서 추적할 수 있는데, 흥미로운 것은 TTD::InitializeForAttach 내에 있으며, 그 동작은 다음 의사 코드에서 단순화되어 있습니다:
ErrorCode TTD::InitializeForAttach(TtdSession *ctx)
{
[...]
EnableDebugPrivilege(GetCurrentProcess()); // [1]
HANDLE hProcess = OpenProcess(0x101040u, 0, ctx->dwProcessId);
if(hProcess == INVALID_HANDLE_VALUE)
{
goto Exit;
}
[...]
HMODULE ModuleHandleW = GetModuleHandleW(L"crypt32.dll");
if ( ModuleHandleW )
pfnCryptStringToBinaryW = GetProcAddress(ModuleHandleW, "CryptStringToBinaryW"); // [2]
if ( ctx->ProcessDebugInformationLength ) // [3]
{
DecodedProcessInformationLength = ctx->ProcessDebugInformationLength;
DecodedProcessInformation = std::vector<unsigned char>(DecodedProcessInformationLength);
wchar_t* b64PplDebuggingTokenArg = ctx->CmdLine_PplDebugToken;
if ( *pfnCryptStringToBinaryW )
{
if( ERROR_SUCCESS == pfnCryptStringToBinaryW( // [4]
b64PplDebuggingTokenArg,
DecodedProcessInformationLength,
CRYPT_STRING_BASE64,
DecodedProcessInformation.get(),
&DecodedProcessInformationLength,
0, 0))
{
Status = NtSetInformationProcess( // [5]
NtGetCurrentProcess(),
ProcessDebugAuthInformation,
DecodedProcessInformation.get(),
DecodedProcessInformationLength);
}
[...]
이 함수는 현재 프로세스([1])에 대해 SeDebugPrivilege 플래그를 활성화하고 연결할 프로세스의 핸들([2])을 가져온 후 문자열 연산을 수행하는 데 사용되는 내보낸 일반 함수인 crypt32!CryptStringToBinaryW를 확인합니다. 이 경우 명령줄([3], [4])에서 제공된 경우 PplDebuggingToken 컨텍스트 옵션의 base64 인코딩된 값을 디코딩하는 데 사용됩니다. 그런 다음 디코딩된 값을 사용하여 시스템 호출 NtSetInformationProcess(ProcessDebugAuthInformation) ([5])를 호출합니다. 이 토큰은 다른 곳에서는 사용되지 않는 것 같아서 해당 시스템 호출을 면밀히 조사하게 되었습니다.
프로세스 정보 클래스 ProcessDebugAuthInformation이 RS4에 추가되었습니다. ntoskrnl을 간략히 살펴보면 이 시스콜은 단순히 코드 무결성 드라이버 DLL인 ci.dll에 있는 CiSetInformationProcess에 버퍼를 전달한다는 것을 알 수 있습니다. 그런 다음 버퍼는 완전히 제어된 인수를 사용하여 ci!CiSetDebugAuthInformation에 전달됩니다.
다음 다이어그램은 TTD의 실행 흐름에서 이러한 일이 발생하는 부분을 개략적으로 요약한 것입니다.
CiSetDebugAuthInformation의 실행 흐름은 매우 간단합니다. base64로 디코딩된 PplDebuggingToken과 그 길이가 포함된 버퍼가 구문 분석 및 유효성 검사를 위한 인수로 ci!SbValidateAndParseDebugAuthToken에 전달됩니다. 유효성 검사가 성공하고 몇 가지 추가 유효성 검사가 끝나면 시스템 호출을 수행하는 프로세스에 대한 핸들(여전히 시스템 호출 nt!NtSetInformationProcess를 처리하고 있음을 기억하세요)이 프로세스 디버그 정보 객체에 삽입된 다음 전역 목록 항목에 저장됩니다.
하지만 그게 어떻게 흥미로운가요? 이 목록은 단 한 곳, 즉 ci!CiCheckProcessDebugAccessPolicy에서만 액세스할 수 있고 이 함수는 NtOpenProcess syscall 중에 도달하기 때문입니다. 그리고 앞서 새로 발견된 플래그의 이름에서 알 수 있듯이 해당 목록에 PID가 있는 모든 프로세스는 보호 수준 적용을 우회하게 됩니다. 이는 해당 목록에 액세스 중단점을 설정하여 KD 세션에서 실제로 확인되었습니다(저희 버전의 ci.dll에서는 ci+364d8에 위치했습니다). 또한 LSASS에서 PPL을 활성화하고 NtOpenProcess 시스콜을 트리거하는 간단한 PowerShell 스크립트를 작성했습니다:
nt!PspProcessOpen에서 nt!PsTestProtectedProcessIncompatibility에 대한 호출을 중단하면 PowerShell 프로세스가 lsass.exe를 대상으로 시도하는 것을 확인할 수 있습니다, PPL 프로세스입니다:
이제 호출의 반환값을 nt!PsTestProtectedProcessIncompatibility로 강제 전송하여 PplDebuggingToken 인수가 수행하는 작업에 대한 초기 이론을 확인합니다:
(CI!CiCheckProcessDebugAccessPolicy만 호출하는) nt!PsTestProtectedProcessIncompatibility 호출 다음 명령에서 중단하고 반환 값을 0 (앞서 언급한 것처럼 1 값은 호환되지 않음을 의미함)로 강제 설정합니다:
성공! PPL임에도 불구하고 LSASS에 대한 핸들을 확보하여 저희의 가설을 확인했습니다. 요약하면, '유효한 값'을 찾을 수 있다면(곧 자세히 살펴보겠습니다) ci!CiSetDebugAuthInformation()의 SbValidateAndParseDebugAuthToken() 검사를 통과하고 범용 PPL 바이패스를 갖게 되는 것입니다. 이것이 사실이라고 하기에는 너무 좋게 들린다면 대부분 그렇기 때문이지만, 이를 확인하려면 CI.dll이 수행하는 작업을 더 잘 이해해야 합니다.
코드 무결성 정책 이해
AppLocker에서 사용하는 것과 같은 코드 무결성에 기반한 제한은 사람이 읽을 수 있는 형식의 XML 파일인 정책을 통해 적용될 수 있습니다. 정책에는 기본 정책과 추가 정책의 두 가지 유형이 있습니다. 기본 정책이 어떤 모습인지에 대한 예는 "C:\Windows\schemas\CodeIntegrity\ExamplePolicies" 에서 XML 형식으로 확인할 수 있습니다. 기본 정책의 XML 형식은 다음과 같습니다( "C:\Windows\schemas\CodeIntegrity\ExamplePolicies\AllowAll.xml")에서 가져온 것으로, 우리가 관심 있는 대부분의 세부 사항을 일반 텍스트로 명확하게 보여줍니다.
<?xml version="1.0" encoding="utf-8"?>
<SiPolicy xmlns="urn:schemas-microsoft-com:sipolicy">
<VersionEx>1.0.1.0</VersionEx>
<PolicyID>{A244370E-44C9-4C06-B551-F6016E563076}</PolicyID>
<BasePolicyID>{A244370E-44C9-4C06-B551-F6016E563076}</BasePolicyID>
<PlatformID>{2E07F7E4-194C-4D20-B7C9-6F44A6C5A234}</PlatformID>
<Rules>
<Rule><Option>Enabled:Unsigned System Integrity Policy</Option></Rule>
<Rule><Option>Enabled:Advanced Boot Options Menu</Option></Rule>
<Rule><Option>Enabled:UMCI</Option></Rule>
<Rule><Option>Enabled:Update Policy No Reboot</Option></Rule>
</Rules>
<!--EKUS-- >
<EKUs />
<!--File Rules-- >
<FileRules>
<Allow ID="ID_ALLOW_A_1" FileName="*" />
<Allow ID="ID_ALLOW_A_2" FileName="*" />
</FileRules>
<!--Signers-- >
<Signers />
<!--Driver Signing Scenarios-- >
<SigningScenarios>
<SigningScenario Value="131" ID="ID_SIGNINGSCENARIO_DRIVERS_1" FriendlyName="Auto generated policy on 08-17-2015">
<ProductSigners>
<FileRulesRef><FileRuleRef RuleID="ID_ALLOW_A_1" /></FileRulesRef>
</ProductSigners>
</SigningScenario>
<SigningScenario Value="12" ID="ID_SIGNINGSCENARIO_WINDOWS" FriendlyName="Auto generated policy on 08-17-2015">
<ProductSigners>
<FileRulesRef><FileRuleRef RuleID="ID_ALLOW_A_2" /></FileRulesRef>
</ProductSigners>
</SigningScenario>
</SigningScenarios>
<UpdatePolicySigners />
<CiSigners />
<HvciOptions>0</HvciOptions>
<Settings>
<Setting Provider="PolicyInfo" Key="Information" ValueName="Name">
<Value><String>AllowAll</String></Value>
</Setting>
<Setting Provider="PolicyInfo" Key="Information" ValueName="Id">
<Value><String>041417</String></Value>
</Setting>
</Settings>
</SiPolicy>
XML 형식의 정책은 ConvertFrom-CiPolicy PowerShell cmdlet을 사용하여 바이너리 형식으로 컴파일할 수 있습니다:
기본 정책은 이름, 경로, 해시 또는 서명자(특정 EKU 포함 또는 제외)로 제한할 수 있을 뿐만 아니라 작업 모드(감사 또는 적용)로도 제한할 수 있는 세분화된 기능을 제공합니다.
보충 정책은 기본 정책의 확장으로 설계되었으며, 예를 들어 특정 워크스테이션 또는 서버 그룹에 정책을 적용하거나 적용하지 않을 수 있는 유연성을 제공합니다. 따라서 더 구체적이지만 기본 정책보다 더 관대할 수도 있습니다. 흥미로운 점은 2016년 이전에는 보조 정책이 특정 디바이스에 국한되지 않았기 때문에 언론에서 광범위하게 다루었던 MS16-094 및 MS16-100에 의해 수정된 완화 우회가 허용되었다는 점입니다.
이 정보를 염두에 두고 이 함수는 기본적으로 세 단계를 거치는데, 이를 보다 명확하게 설명하면 다음과 같습니다: 1. ci!SbParseAndVerifySignedSupplementalPolicy를 호출하여 시스템 호출에서 입력 버퍼를 구문 분석하고 유효하게 서명된 보충 정책인지 확인 2. ci!SbIsSupplementalPolicyBoundToDevice를 호출하여 보충 정책의 DeviceUnlockId를 현재 시스템의 것과 비교합니다(해당 값은 GUID가 있는 NtQuerySystemEnvironmentValueEx 시스템 호출을 사용하여 쉽게 검색할 수 있음). {EAEC226F-C9A3-477A-A826-DDC716CDC0E3}
3. 마지막으로 정책에서 보호 수준에 해당하는 정수(DWORD)와 (UNICODE_STRING) 디버그 권한이라는 두 가지 변수를 추출합니다.
XML 또는 PowerShell 스크립팅을 통해 정책 파일을 만들 수 있으므로 3 단계는 문제가 되지 않습니다. 2단계도 마찬가지입니다. SeSystemEnvironmentPrivilege 권한이 있는 한 NtSetSystemEnvironmentValueEx({EAEC226F-C9A3-477A-A826-DDC716CDC0E3})
syscall을 사용하여 DeviceUnlockId를 위조할 수 있기 때문입니다. 그러나 UnlockId는 재부팅 시 복원되는 휘발성 값이라는 점에 유의해야 합니다.
그러나 1 단계를 우회하는 것은 사실상 불가능합니다. - 특정 OID 1.3.6.1.4.1.311.10.3.6(즉, - MS NT5 Lab(szOID_NT5_CRYPTO)을 가진 Microsoft 소유 인증서의 개인 키를 소유해야 하며 - 앞서 언급한 인증서가 해지되거나 만료되지 않아야 합니다.
그렇다면 이제 우리는 어디로 가야 할까요? 이제 기존의 통념과 달리 커널 드라이버를 로드하는 추가 단계 없이 다른 프로세스에서 PPL 프로세스를 열 수 있음을 확인했습니다. 그러나 이러한 사용 사례는 매우 표적화된 컴퓨터에 이 기술을 사용할 수 있는 열쇠는 말 그대로 Microsoft만이 가지고 있기 때문에 틈새 시장이라는 점도 강조해야 합니다. 그럼에도 불구하고 이러한 사례는 디버깅 목적으로 CI를 사용하는 에어 갭의 좋은 예입니다.
공격적 TTD
참고: 다시 한 번 말씀드리지만, TTD.exe에는 상승된 권한이 필요하며, 아래에서 설명하는 모든 기법은 상승된 권한을 가정합니다.
이 연구를 통해 TTD의 잠재적으로 흥미로운 공격 및 방어 사용 사례를 발견했습니다.
추적 != 디버깅
TTD는 디버거가 아닙니다! 따라서 기본 디버깅 방지 검사를 수행하는 프로세스(예: IsDebuggerPresent() 사용(또는 PEB.BeingDebugged에 의존하는 다른 방법)의 경우 감지되지 않고 완벽하게 작동합니다. 다음 스크린샷은 간단한 메모장 프로세스에 TTD를 첨부하여 이 세부 사항을 설명합니다:
디버거에서 메모장 PEB에 있는 BeingDebugged 필드를 확인하면 플래그가 설정되지 않았음을 알 수 있습니다:
ProcLaunchMon의 흥미로운 사례
TTD가 제공하는 또 다른 흥미로운 트릭은 기본 제공 Windows 드라이버인 ProcLaunchMon.sys를 악용하는 것입니다. 서비스로 실행하는 경우(예 TTDService.exe)에서 서비스 인스턴스를 생성하고, 드라이버를 로드하고, .\com_microsoft_idna_ProcLaunchMon에서 사용 가능한 장치와 통신하여 새로 추적된 클라이언트를 등록하면 ttdrecord.dll이 서비스 인스턴스를 생성하고 드라이버를 로드합니다.
드라이버 자체는 TTD 서비스에 의해 생성된 새 프로세스를 모니터링한 다음 커널에서 직접 해당 프로세스를 일시 중단하는 데 사용되므로 생성 플래그 CREATE_SUSPENDED로 프로세스 생성만 모니터링하는 보호 기능을 우회합니다(예: 여기에 언급된 것처럼). 이 연구를 위해 기본 장치 드라이버 클라이언트를 개발했으며, 여기에서 확인할 수 있습니다.
CreateDump.exe
또 다른 재미있는 사실: 엄밀히 말해 TTD의 일부분은 아니지만 WinDbgX 패키지는 .NET 서명 바이너리를 제공하며, 그 이름은 기능을 완벽하게 요약하는 createdump.exe입니다. 이 바이너리는 "C:\Program Files\WindowsApps\Microsoft.WinDbg_*\createdump.exe" 에서 찾을 수 있습니다.
이 바이너리는 인자로 제공된 프로세스의 컨텍스트를 스냅샷하고 덤프하는 데 사용할 수 있으며, 다른 LOLBAS의 직계 혈통에 있습니다.
이는 자격 증명 덤핑과 같은 공격으로부터 보호하기 위해 정적 서명과 파일 이름 차단 목록 항목에 의존하는 것을 피하고 RunAsPPL, Credential Guard 또는 Elastic Endpoint의 자격 증명 강화와 같은 보다 강력한 접근 방식을 선호해야 할 필요성을 다시 한 번 강조합니다.
방어 TTD
TTD 차단
TTD는 매우 유용한 기능이지만, 프로덕션 서버나 워크스테이션과 같이 개발 또는 테스트용이 아닌 머신에서 활성화해야 하는 경우는 드뭅니다. 이 글을 쓰는 시점에서는 거의 문서화되지 않은 것으로 보이지만, ttdrecord.dll은 "HKEY_LOCAL_MACHINE\Software\Microsoft\TTD" 아래에 있는 레지스트리 키를 만들거나 업데이트하고 DWORD32 값 RecordingPolicy를 2로 업데이트하는 것만으로 조기 종료 시나리오가 가능합니다. TTD 서비스를 추가로 사용하려는 시도(TTD.exe, TTDInject.exe, TTDService.exe)가 중지되고 시도를 추적하기 위해 ETW 이벤트가 생성됩니다.
TTD 감지
모든 환경에서 TTD 사용을 막는 것은 너무 극단적일 수 있지만, TTD 사용을 감지할 수 있는 몇 가지 지표가 존재합니다. 추적 중인 프로세스에는 다음과 같은 속성이 있습니다:
- 하나의 스레드가 TTDRecordCPU.dll에서 코드를 실행합니다, 간단한 기본 제공 Windows 명령을 사용하여 확인할 수 있습니다: tasklist /m TTDRecordCPU.dll
- 이를 우회할 수 있지만, 기록된 프로세스의 상위 PID(또는 재귀 추적이 활성화된 경우 첫 번째 PID)는 TTD.exe 자체가 됩니다:
- 또한 _KPROCESS.InstrumentationCallback 포인터는 실행 파일의 TTDRecordCPU.dll BSS 섹션에 배치되도록 설정됩니다:
따라서 사용자 모드와 커널 모드 방법을 모두 통해 TTD에서 추적을 감지할 수 있습니다.
결론
이것으로 TTD에 초점을 맞춘 이번 '온-위크' 연구의 첫 번째 부분을 마무리합니다. TTD 에코시스템의 내부를 파헤치다 보니 잘 알려지지 않은 매우 흥미로운 메커니즘이 Windows에 내장되어 있는데, 이는 PPL 프로세스 추적과 같은 특정 에지 케이스에서 TTD를 작동시키는 데 필요합니다.
이 연구를 통해 PPL 프로세스를 표적으로 삼는 새로운 비밀 백도어가 밝혀지지는 않았지만, Windows에 내장된 미지의 기법이 발견되었습니다. 이 연구는 강력한 암호화에 기반한 모델(여기서는 CI.dll을 통해)의 중요성과 이를 적절히 구현할 경우 높은 수준의 보안을 유지하면서 많은 유연성을 제공할 수 있다는 점을 강조합니다.
이 시리즈의 두 번째 파트는 연구 중심이 아닌 실습 위주의 내용으로, On-Week의 일환으로 개발한 작은 도구를 공개할 예정입니다. 이는 Windows 샌드박스를 사용하여 TTD를 통한 바이너리 분석 프로세스를 지원합니다.
인정
이 연구가 이미 마무리되고 기사가 진행 중이던 중, 필자는 이와 유사한 주제와 동일한 기술(PPL 디버깅 토큰)에 관한 연구 결과를 알게 되었습니다. 이 연구는 루카스 조지(Synacktiv 소속)가 수행했으며, 그는 SSTIC 2022에서 자신의 연구 결과를 발표했습니다.