본문 바로가기
일상/개발자의 일상

[Project Zero 번역] Windows‌ ‌Exploitation‌ ‌Tricks:‌ ‌Spoofing‌ ‌Named‌ ‌Pipe‌ ‌Client‌ ‌PID‌

by Dev zeroday0619 2019.10.12

* 원본 출처: https://googleprojectzero.blogspot.com/2019/09/windows-exploitation-tricks-spoofing.html

 

Windows‌ ‌Exploitation‌ ‌Tricks:‌ ‌Spoofing‌ ‌Named‌ ‌Pipe‌ ‌Client‌ ‌PID‌

Posted by James Forshaw, Project Zero While researching the Access Mode Mismatch in IO Manager bug class I came across an interesti...

googleprojectzero.blogspot.com


IO Manager 버그 클래스의 액세스 모드 불일치를 조사하는 동안 서버가 연결된 클라이언트 PID를 쿼리할 수 있는 

명명된 파이프의 흥미로운 기능을 발견했습니다. 

이 기능은 Vista에서 도입되었으며 GetNamedPipeClientProcessId API를 통해 서버에 노출되어 API 핸들을 

파이프 서버에 전달하면 연결된 클라이언트의 PID를 다시 얻을 수 있습니다.

 

클라이언트 PID를 보안 시행 목적으로 사용하는 애플리케이션이 있어야 합니다. 

그러나 Windows에 설치된 PID를 보안과 관련된 모든 항목에 사용하는 첫 번째 응용 프로그램을 찾을 수 없습니다. 

타사 애플리케이션은 다른 문제이며 다른 연구자들은 신뢰할 수 없는 호출자가 

권한 있는 작업에 액세스하는 것을 방지하기 위해 PID를 사용하는 예를 찾았습니다. 

최근 예로는 Check Point Anti-Virus가 있습니다. 

이 PID에 의존하는 것은 위험하기 때문에 PID 값을 스푸핑하는 방법을 강조하여 개발자가 PID를 시행 메커니즘으로 

사용하는 것을 중지하고 연구자에게 이러한 위험한 검사를 활용하는 방법을 시연해야 한다고 결정했습니다.

 

C#에 작성된 클라이언트 PID를 사용한 간단한 보안 점검 예는 다음과 같습니다. 

이 코드는 명명된 파이프 서버를 생성하고 새 연결을 기다렸다가 

GetNamedPipeClientProcessId API를 호출합니다. 

API 호출이 성공하면 보안으로 호출됩니다.

PID에 대해 어느 것이 약간의 검증을 수행하는지 확인합니다. 

보안이 설정된 경우에만 해당됩니다.

SecurityCheck (highlighted) reales true를 선택하면 클라이언트의 통화가 처리됩니다.

using (var pipe = new NamedPipeServerStream("ABC"))
{
    pipe.WaitForConnection();

    if (!GetNamedPipeClientProcessId(pipe.SafePipeHandle, out int pid))
    {
        Console.WriteLine("Error getting PID");
        return;
    }
    else
    {
        Console.WriteLine("Connection from PID: {0}", pid);
        if (SecurityCheck(pid))
        {
            HandleClient(pipe);
        }
    }
}
Security Check가 정확하게 실시하는 것은 그다지 중요하지 않습니다.

예를 들어, 서버는 그 ID로 프로세스를 열어, 메인 실행 가능 파일을 조회하고 나서, 그 파일의 서명 체크를 실행합니다.중요한 것은 클라이언트가 GetNamedPipeClientProcessId에 의해 반환된 PID를 위장하고, 

클라이언트가 아닌 프로세스를 참조할 수 있으면, 

시큐리티 체크를 바이패스 해 서비스를 악용 할 수 있는 것입니다.

 

Where Does the PID Come From?


PID를 스푸핑하는 몇 가지 기술을 설명하기 전에 GetNamedPipeClientProcessId를 호출할 때 

PID의 값이 어디에서 오는지를 이해하는 것이 유용합니다. 

PID는 새 클라이언트 연결이 설정될 때 명명된 NPFS(관 파일 시스템 드라이버)에 의해 설정됩니다. 

Windows 10의 경우 이 프로세스는 NpCreateClientEnd 기능에서 처리됩니다. 

구현은 대략 다음과 같습니다.

 
NTSTATUS NpCreateClientEnd(PFILE_OBJECT ServerPipe,
 KPROCESSOR_MODE AccessMode, PFILE_FULL_EA_INFORMATION EaBuffer) {
 // ...
 if (!EaBuffer) {
  DWORD value = PsGetThreadProcessId();
  NpSetAttributeInList(ServerPipe, PIPE_ATTRIBUTE_PID, &value);
  value = PsGetThreadSessionId();
  NpSetAttributeInList(ServerPipe, PIPE_ATTRIBUTE_SID, &value);
 } else {
  if (AccessMode != KernelMode)
   return STATUS_ACCESS_DENIED;
  LPWSTR computer_name;
  NpLocateEa(EaBuffer, "ClientComputerName", &computer_name);
  NpSetAttributeInList(ServerPipe, PIPE_ATTRIBUTE_NAME, computer_name);
  DWORD value;
  NpLocateEa(EaBuffer, "ClientProcessId", &value);
  NpSetAttributeInList(ServerPipe, PIPE_ATTRIBUTE_PID, &value);
  NpLocateEa(EaBuffer, "ClientSessionId", &value);
  NpSetAttributeInList(ServerPipe, PIPE_ATTRIBUTE_SID, &value);
 }
 // ...
}

PID(및 관련 세션 ID 및 컴퓨터 이름) 값은 NpSetAtribe를 통해 일반 속성 메커니즘을 사용하여 설정됩니다.

InList 기능입니다. 

속성 목록에 저장된 값은 문서화되지 않은 FSCTL_PIPE_GET_CONNECTION_ATTRIBUTE 코드를 

서버 파이프에 발급하여 파일 시스템 제어 요청을 검색합니다.

속성을 설정할 때 두 가지 옵션이 있습니다. 

첫째, 파일 생성 요청에 ASE(Extended At Attribe) 버퍼가 제공되지 않으면 현재 프로세스에서 

PID 및 세션 ID를 가져옵니다. 

 

이 작업은 진행 중인 클라이언트 연결을 생성할 때 수행되는 일반적인 작업입니다. 

두 번째 옵션은 로컬 SMB 서버에서 사용하며, 드라이버는 EA 버퍼를 지정하여 SMB 서버가 

클라이언트의 컴퓨터 이름, 추가 PID 및 세션 ID와 같은 연결 정보를 지정할 수 있도록 합니다. 

일반 사용자 모드 프로세스는 임의 EA 버퍼를 지정할 수 있으므로 이 코드는 또한 작업이 

커널 모드에서 수행되는지 확인합니다. 

모드 점검은 일반 사용자 모드 응용 프로그램이 값을 스푸핑하지 못하도록 해야 합니다.


스푸핑 기법입니다.

 

PID 설정 방법에 대한 지식을 바탕으로 PID의 값을 스푸핑하는 몇 가지 기법에 대해 설명하겠습니다. 

각각의 기술에는 주의사항이 있습니다. 

모든 기술은 Windows 10 1903에서 실행되는 것으로 확인되었습니다.
별도의 언급이없는 한 하위 수준에서도 작동해야합니다.

 

로컬 SMB 및 NTFS 마운트 지점을 통한 파이프 열기

 

필자의 IO Manager 블로그 게시물에서 설명한대로 이전 액세스 모드를 KernelMode로 설정하는 

적절한 초기자를 찾을 수 있으면 NPFS 드라이버가 연결 속성 스푸핑을 방지하기 위해 

수행하는지 확인하지 않아도됩니다. 

 

CVE-2018-0749를 수정하기 전에 임의의 로컬 NTFS 마운트 지점을 설정하고 

모든 로컬 SMB 요청을 NPFS를 포함한 모든 장치로 리디렉션 할 수있었습니다. 

일반적으로 커널처럼 파일을 직접 열면 불가능합니다. 

볼륨이 아닌 대상에 연결하지 마십시오. SMB 파일 열기 요청에서도 임의의 EA 버퍼를 지정할 수 있으므로 로컬 클라이언트는 PID를 포함하여 완전히 스푸핑 된 값으로 명명 된 파이프 연결을 열 수 있습니다.

CVE-2018-0749가 수리되면 기술적으로 더 이상 활용할 수 없게 되었습니다. 

Windows 10 1709 이후 NTFS 마운트 지점 대상에 대한 커널의 처리가 변경되어 

명명된 파이프 장치뿐만 아니라 기존의 파일 시스템 볼륨에도 재지정할 수 있게 되었습니다. 

따라서 로컬 SMB 서버, 마운트 지점 및 적절한 EA 버퍼를 사용하여 임의 PID를 스푸핑할 수 있습니다. 

다음 C# 예제에서는 "ABC"라는 파이프를 열 때 클라이언트 PID를 1234로 스푸핑하는 방법을 보여 줍니다.

다음 유형 중 일부를 사용하려면 NtApiDotNet 라이브러리를 참조해야 합니다.

 
EaBuffer ea = new EaBuffer();
ea.AddEntry("ClientComputerName", "FAKE\0", EaBufferEntryFlags.None);
ea.AddEntry("ClientProcessId", 1234, EaBufferEntryFlags.None);
ea.AddEntry("ClientSessionId", new byte[8], EaBufferEntryFlags.None);
using (var m = NtFile.Create(@"\??\c:\pipes", null, 
            FileAccessRights.GenericWrite | FileAccessRights.Delete,
            FileAttributes.Normal, FileShareMode.All, 
            FileOpenOptions.DirectoryFile | FileOpenOptions.DeleteOnClose,
            FileDisposition.Create, null))
{
    m.SetMountPoint(@"\??\pipe", "");
    using (var p = NtFile.Create(@"\??\UNC\localhost\c$\pipes\ABC", 
                          FileAccessRights.MaximumAllowed,
                          FileShareMode.None, 
                          FileOpenOptions.None, FileDisposition.Open, 
                          ea))
    {
        Console.WriteLine("Opened Pipe");
    }
}

이 기법을 사용하면 NPFS에서 PID를 설정하기위한 초기 옵션을 따를 수 있습니다. 

특히 EA 버퍼가 설정되지 않은 경우 현재 PID가 사용됩니다. 

SMB 서버가 시스템 프로세스에서 실행되면 클라이언트 PID를 값 4로 설정하게됩니다. 

PID에 대해 임의의 값을 이미 지정할 수있는 경우에는 그다지 유용하지 않습니다.

장점 :
      임의 PID (및 원하는 경우 세션 ID 및 컴퓨터 이름)를 속일 가능성이 있습니다.
단점 :
      마운트 지점이 필요하고 로컬 SMB 서버에 액세스해야 샌드 박스에서 악용 할 수 없습니다.  
      Windows 10 1709 이상에서만 작동합니다.

로컬 SMB를 통한 파이프 열기

Windows 10 1709 이전 버전의 Windows에서 실행중인 경우 모두 손실되지 않습니다. 올바른 방법을 통해 로컬 SMB 서버를 사용하여 명명 된 파이프를 열었을 경우 (예 : 경로 //localhost/pipe/ABC) SMB 서버가 PID 속성을

설정하지 않았다고 가정 할 수 있습니다.

서버 드라이버를 간략히 살펴보면 실제로 설정했음을 알 수 있습니다.

특히 고정 된 값으로 설정합니다. Windows 10 1903에서이 값은 65279 / 0xFEFF입니다.

고정 값은 클라이언트가 전송 한 SMB2 프로토콜 헤더에서 가져옵니다. 

헤더는 Microsoft에 의해 문서화됩니다. 

그러나 문서는 PID로 사용 된 값을 포함하는 필드를 "예약 (4 바이트)"으로보고합니다. 

클라이언트는이 필드를 0으로 설정해야합니다. 서버는 수신시이 필드를 무시할 수 있습니다. 

다행스럽게도 Wireshark 설명서는 좀 더 유용합니다. 기본값은 0xFEFF 인 프로세스 ID입니다. 

명명 된 파이프를 열 때 Wireshark에서 SMB 트래픽을 캡처하면 고정 된 값이 표시됩니다.

 

출처: https://googleprojectzero.blogspot.com/2019/09/windows-exploitation-tricks-spoofing.html

 

서버가 값을 확인하지 않으므로 임의의 값으로 설정할 수 있지만 

내장 Windows 클라이언트에서는 0xFEFF에서 값을 변경할 수 없습니다. 

자체 SMB2 클라이언트를 작성하거나 IMPacket과 같은 기존 클라이언트를 사용하지 않고도

이를 이용이 가능한가?

Windows가 PID 값을 재사용하고 프로세스 중 하나가 올바른 PID를 가질 때까지 보안 검사 요구 사항을 

충족시키는 적절한 프로세스를 생성한다는 사실을 남용 할 수 있습니다.

현재 모든 Windows 버전에서 PID를 4의 배수로 정렬하므로 ID가 65279 인 프로세스를 실제로 만들 수는 없지만 

서버가 65279에서 OpenProcess를 호출하면 PID 65276을 반올림하여 만들 수 있습니다. 

또한 스레드 ID는 PID와 동일한 풀에서 가져 오기 때문에 운이 좋지 않고 원하는 ID로 스레드를 작성할 수 있습니다. 

PID를 통한 사이클링은 특히 최신 버전의 Windows에서 PID의 반 임의 할당 패턴에서 오랜 시간이 걸릴 수 있지만 

악용 될 수 있습니다.

PID 사이클링의 간단한 예는 다음과 같습니다.

 
while (true)
{
    using (var p = Process.Start("target.exe"))
    {
        if (p.Id == 65276)
        {
            break;
        }
        p.Kill();
    }
}

ID 65276으로 적절한 프로세스가 생성되면 SMB 서버를 통해 명명 된 파이프에 연결할 수 있으며 

서버가 PID를 열면 스푸핑 된 프로세스를 얻게됩니다.

장점 :
       모든 버전의 Windows에서 작동합니다.
       SMB2 프로토콜을 다시 구현하려는 경우 PID를 임의로 스푸핑 할 수 있습니다.
단점 :
       로컬 SMB 서버에 액세스해야하므로 샌드 박스에서 악용 할 수 없습니다.

       클라이언트를 다시 구현하더라도 앱 컨테이너 샌드 박스에서 로컬 호스트에 액세스하거나

       적절한 인증 자격 증명을 얻지 못할 수 있습니다.

     

      서버의 보안 검사가 OpenProcess에서 PID를 사용하고 실행중인 PID 번호와 직접 비교하지 않는 경우에만

      작동합니다.
      서버 보안 검사를 무시하기 위해 올바른 ID로 실행중인 적절한 프로세스를 얻는 것은

      매우 느리거나 어려울 수 있습니다.

Opening Pipe in One Process. Using Pipe in Another.

이 기술은 클라이언트 연결이 열리면 PID가 고정되어 파이프를 읽고 쓰는 프로세스가 

동일한 PID를 가질 필요가 없다는 사실을 사용합니다. 

하나의 프로세스에서 파이프 클라이언트를 작성하고 새 하위 프로세스를 시작한 후 

해당 하위 프로세스에 핸들을 복제하여이를 활용할 수 있습니다. 

이제 오프닝 프로세스가 종료되면 PID가 해제되고 PID 사이클링 공격을 다시 수행 할 수 있습니다. 

PID가 재사용되면 하위 프로세스는 필요에 따라 파이프 작업을 수행 할 수 있습니다. 

초기 개방은 파이프 핸들을 전달하기 위해 프로세스 작성에서 핸들 상속을 사용하는 다음 C #과 같습니다.

 
using (var pipe = new NamedPipeClientStream(".", "ABC", 
             PipeAccessRights.ReadWrite,
             PipeOptions.None, TokenImpersonationLevel.Impersonation,   
             HandleInheritability.Inheritable))
{
    int pid = Process.GetCurrentProcess().Id;
    IntPtr handle = pipe.SafePipeHandle.DangerousGetHandle();
    ProcessStartInfo start_info = new ProcessStartInfo();
    start_info.FileName = "program.exe";
    start_info.Arguments = $"{handle} {pid}";
    start_info.UseShellExecute = false;
    Process.Start(start_info);
}

그런 다음 하위 프로세스에서 다음 코드는 부모 프로세스가 종료 될 때까지 기다렸다가 일치 할 때까지

PID를 재활용 한 다음 파이프를 사용합니다.

 
int ppid = int.Parse(args[1]);
Process.GetProcessById(ppid).WaitForExit();
RecycleProcessId(ppid);
var handle = new SafePipeHandle(new IntPtr(int.Parse(args[0])), true);
using (var pipe = new NamedPipeClientStream(PipeDirection.InOut, 
                 false, true, handle))
{
    pipe.WriteByte(0);
}

이 접근 방식의 한 가지 큰 문제는 서비스가 PID 검사를 수행하는 위치에 따라 다릅니다. 

연결 후 바로 확인하면 확인하기 전에 PID를 재활용 할 시간이 충분하지 않을 수 있습니다. 

그러나 파이프에 요청한 후 (예 : 파이프에 데이터 쓰기) 점검을 수행 한 경우 

PID가 재순환 될 때까지 점검을 수행 할 수 있습니다.

SMB 서버에서 설정 한 고정 값과 달리 다른 PID를 사용하여 여러 개의 개별 연결을 작성하여 

올바른 재활용 PID를 칠 가능성을 최대화 할 수 있습니다. 

연결할 수있는 수는 서버가 지원하는 동시 파이프 인스턴스 수에 따라 다릅니다.

장점 :
       모든 버전의 Windows에서 작동합니다.
       명명 된 파이프를 열 수 있으면 샌드 박스에서 작동해야합니다.
       PID 재활용 가능성을 극대화하기 위해 다른 PID를 가진 여러 파이프를 생성 할 수 있습니다.
단점 :
       PID 확인은 연결을 즉시 따라갈 수 없으며 대신 악용 될 수있는 서비스 수를 제한하는

       초기 읽기 / 쓰기 작업 후에 이루어져야합니다.

 

결론
      명명 된 파이프 클라이언트 PID를 보안 적용 메커니즘으로 사용하는 서비스를 이용하려는 경우 이러한 기술 중

      하나만으로 충분합니다.

      PID 값을 임의로 스푸핑하는 기능이없는 경우에도이 PID는 실제 클라이언트,

      파이프를 연 프로세스 만 반영 할 필요가 없으므로 보안 결정에 의존해서는 안됩니다.

 

번역: 20191012

 

댓글0