Virtual Box Driver Mapper 분석

Virtual Box Driver Loader

[0x00] Concept

게임 해킹에서 취약한 드라이버를 이용하여 서명되지 않은 드라이버를 로드하는 것은 매우 흔합니다.

이번 포스팅은 Virtual Box 의 취약한 코드를 이용하여 서명되지 않은 드라이버를 로드하는 내용을 설명합니다.

KPP 를 Runtime 중 우회가 가능한 라이브러리를 참조하여 포스팅합니다.

치팅 툴을 직접 개발하고 테스트하며, Virtual Box 익스플로잇을 통한 드라이버 매핑을 이용했고, Copy And Paste 하여 사용했습니다. 늦은 감이 있지만 분석을 진행하기로 하였습니다.

취약한 VBoxDrv.sys 의 해시 값은 아래와 같습니다.

  • MD5 Hash : EAEA9CCB40C82AF8F3867CD0F4DD5E9D

다양한 구글링을 통해 확인하였을 때, hfiref0x 의 깃헙에서 취약점에 대한 간단한 내용을 확인할 수 있었습니다.

다름 아닌 Turla 키워드 였습니다.

Turla 는 러시아의 APT 그룹으로 유행하던 커널 드라이버 로더를 개발한 그룹으로 확인되었습니다.

해당 Turla Driver Loader 를 통해 CVE-2008-3431 내용과 연관있었습니다. 다만 드라이버 매핑 시에는 해당 취약점과는 다른 IOCTL CODE 를 이용하게 됩니다.

각 사용된 구조체는 위에서 VirtualBox 소스코드(rev. 9000) 에서 확인 가능합니다.

직접 개발한 드라이버 로더의 경우에 악용될 우려가 있으므로 공유하지 않습니다.

[0x01] VBoxDrv.sys Analysis

VirtualBox 는 오픈소스이기 때문에 소스를 확인하며 분석하였습니다.

취약한 드라이버의 정확한 리비전을 알지 못했기 때문에, CVE Number가 발급된 연도의 리비전을 찾아 확인하였고 해당 리비전은 9000 입니다.

분석 내용은 해당 드라이버의 동작 내용입니다.

[-] DriverEntry

IDA를 이용하여 드라이버를 초기화 루틴을 확인하면 아래와 같습니다.

NTSTATUS __stdcall DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  RtlInitUnicodeString(&DestinationString, L"\\\\Device\\\\VBoxDrv");
  rc = IoCreateDevice(DriverObject, 0x1108u, &DestinationString, 0x22u, 0, 0, &DeviceObject);// Create device obj
  if ( rc >= 0 )
  {
    RtlInitUnicodeString(&SymbolicLinkName, L"\\\\DosDevices\\\\VBoxDrv");
    rc = IoCreateSymbolicLink(&SymbolicLinkName, &DestinationString);
    if ( rc >= 0 )
    {
      DevExt = DeviceObject->DeviceExtension;
      memset(DevExt, 0, sizeof(SUPDRVDEVEXT));
      Err = supdrvInitDevExt(DevExt);
      if ( Err )
      {
        rc = VBoxDrvNtErr2NtStatus(Err);
      }
      else
      {
        DevExt->fForceAsyncTsc = supdrvDetermineAsyncTsc(&u64DiffCores);
        rc = VBoxDrvNtGipInit(DevExt);
        if ( rc >= 0 )
        {
          DriverObject->DriverUnload = sub_1400008B0;
          DriverObject->MajorFunction[0] = sub_140000980;
          DriverObject->MajorFunction[2] = sub_140000390;
          DriverObject->MajorFunction[14] = VBoxDrvNtDeviceControl;
          DriverObject->MajorFunction[3] = NotSupportIRP;
          DriverObject->MajorFunction[4] = NotSupportIRP;
          return 0;
        }
        supdrvDeleteDevExt(DevExt);
      }
      IoDeleteSymbolicLink(&SymbolicLinkName);
    }
    IoDeleteDevice(DeviceObject);
    if ( rc >= 0 )
      rc = 0xC000000D;
  }
  return rc;
}

특이한 점은 Device Extension 을 사용한다는 점입니다. 해당 기능은 디바이스 오브젝트를 생성할 때 항상 사용하지 않았었으나, 동작을 보면 원하는 사이즈 만큼 사용할 수 있고 내가 원하는 구조로 사용이 가능해보입니다.

MSDN 내 서술된 내용을 확인하면 아래와 같은 용도로 사용된다고 되어 있습니다.

  • 장치 상태 정보를 유지
  • 드라이버에서 사용하는 SpinLock 과 같은 커널 오브젝트와 시스템 리소스에 대한 저장소 제공
  • 드라이버 I/O 작업 시 시스템 공간에 존재해야 하는 모든 데이터를 보유

번역을 한 탓에 더 어렵지만, 고유 디바이스에서 사용 가능한 데이터 임은 틀림없다고 보고 있습니다.

별도의 중요한 로직은 존재하지 않는 것으로 보이며, 바로 DeviceIoControl 을 확인합니다.

[-] VBoxDrvNtDeviceControl

IDA를 이용하여 해당 함수를 확인하면 아래와 같습니다.

NTSTATUS __fastcall DeviceIoControl(DEVICE_OBJECT *DevObj, IRP *Irp)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  pDevExt = DevObj->DeviceExtension;
  pStack = Irp->Tail.Overlay.CurrentStackLocation;
  pSession = (PSUPDRVSESSION)pStack->FileObject->FsContext;
  IoCtlCode = pStack->Parameters.DevIoControl.IoControlCode;
  if ( IoCtlCode != 0x228303 && IoCtlCode != 0x228307 && IoCtlCode != 0x22830B )// SUP_IOCTL_FAST_DO_RAW_RUN, SUP_IOCTL_FAST_DO_HWACC_RUN, SUP_IOCTL_FAST_DO_NOP
    return VBoxDrvNtDeviceControlSlow(DevObj->DeviceExtension, pSession, Irp, pStack);
  CurrentIRQL = KeGetCurrentIrql();
  __writecr8(2ui64);                            // Chagne Dispatch level
  rc = supdrvIOCtlFast(IoCtlCode, pDevExt, pSession);
  __writecr8(CurrentIRQL);
  Irp->IoStatus.Status = 0;
  Irp->IoStatus.Information = 4i64;
  *Irp->UserBuffer = rc;
  IofCompleteRequest(Irp, 0);
  return 0;
}

IOCTL CODE 에 따라 분기되는 두 개의 함수가 존재합니다. SUP_IOCTL_FAST_DO_RAW_RUN, SUP_IOCTL_FAST_DO_HWACC_RUN, SUP_IOCTL_FAST_DO_NOP 코드는 드라이버 매핑에 사용되는 코드가 아니므로, 취약점과 관련있는 함수는 VBoxDrvNtDeviceControlSlow 로 보입니다.

사용하진 않지만 susdrvIOCtlFast 함수를 확인하면 아래와 같습니다.

__int64 __fastcall supdrvIOCtlFast(__int64 IoCtlCode, SUPDRVDEVEXT *DevExt, SUPDRVSESSION *pSession)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  if ( pSession->pVM )
  {
    v3 = DevExt->VMMR0EntryFast;
    if ( v3 )
    {
      v4 = IoCtlCode - 0x228303;                // SUP_IOCTL_FAST_DO_RAW_RUN
      if ( !v4 )
        return v3(pSession->pVM, 0i64);
      v5 = v4 - 4;                              // SUP_IOCTL_FAST_DO_HWACC_RUN
      if ( !v5 )
        return v3(pSession->pVM, 1i64);
      if ( v5 == 4 )                            // SUP_IOCTL_FAST_DO_NOP
        return v3(pSession->pVM, 2i64);
    }
  }
  return 0xFFFFFFE0i64;
}

[-] VBoxDrvNtDeviceControlSlow

IDA에서 해당 함수를 확인하면 아래와 같습니다.

NTSTATUS __fastcall VBoxDrvNtDeviceControlSlow(SUPDRVDEVEXT *DevExt, SUPDRVSESSION *pSession, IRP *pIRP, _IO_STACK_LOCATION *pStack)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  cbOut_1 = 0;
  if ( IoIs32bitProcess(pIRP) || (pStack->Parameters.DevIoControl_.IoControlCode & 3) != 0 )
  {
    Status = 0xC00000BB;
  }
  else
  {
    InputLength = pStack->Parameters.DevIoControl_.InputBufferLength;
    pHdr = pIRP->AssociatedIrp.SystemBuffer;
    if ( InputLength >= sizeof(SUPREQHDR)
      && InputLength == pHdr->cbIn
      && pStack->Parameters.DevIoControl_.OutputBufferLength == pHdr->cbOut
      && !supdrvIOCtl(
            pStack->Parameters.DevIoControl_.IoControlCode,
            DevExt,
            pSession,
            pIRP->AssociatedIrp.SystemBuffer) )
    {
      cbOut = pHdr->cbOut;
      OutputLength = pStack->Parameters.DevIoControl_.OutputBufferLength;
      Status = 0;
      cbOut_1 = pHdr->cbOut;
      if ( cbOut > OutputLength )
      {
        cbOut_1 = pStack->Parameters.DevIoControl_.OutputBufferLength;
        DbgPrint(
          "VBoxDrvLinuxIOCtl: too much output! %#x > %#x; uCmd=%#x!\\n",
          cbOut,
          OutputLength,
          pStack->Parameters.DevIoControl_.IoControlCode);
      }
    }
    else
    {
      Status = 0xC000000D;
    }
  }
  pIRP->IoStatus.Status = Status;
  pIRP->IoStatus.Information = cbOut_1;
  IofCompleteRequest(pIRP, 0);
  return Status;
}

해당 함수에서는 InputBufferLengthOutputBufferLength 에 대한 간단한 검증이 존재합니다.

여기서 알 수 있는 점은 아래와 같습니다.

  • 버퍼 전달 방식의 경우 METHOD_BUFFERED 이면 안됨
  • 위의 내용으로 METHOD_NEITHER 방식으로 구현된 것으로 보임
  • 버퍼 길이에 대한 검증은 존재함
  • SystemBuffer 의 경우 SUPREQHDR 라는 구조로 되어있는 포인터

이제 실제 사용 가능한 IOCTL CODE를 확인할 수 있는 supdrvIOCtl 함수를 확인 할 차례입니다.

해당 함수는 꽤 큰 함수입니다. 각 제어 코드에 따른 분석을 진행합니다.

매핑에 이용되는 제어 코드와 사용되는 구조는 아래와 같습니다.

  • SUP_IOCTL_COOKIE(SUPCOOKIE)(0x228204)
  • SUP_IOCTL_LDR_OPEN(SUPLDROPEN)(0x228214)
  • SUP_IOCTL_LDR_LOAD(SUPLDRLOAD)(0x228218)
  • SUP_IOCTL_CALL_VMMR0(SUPCALLVMMR0)(0x228224)

각 구조에는 SUPREQHDR 구조를 포함하고 있습니다.

취약점 내용에선 해당 코드에 대한 언급이 존재하지 않았습니다. 때문에 왜 로더에서 SUP_IOCTL_COOKIE 를 이용하는지 이해하지 못했으나 분석하며 그 이유를 알 수 있었습니다.

__int64 __fastcall supdrvIOCtl(__int64 IoCtlCode, SUPDRVDEVEXT *DevExt, SUPDRVSESSION *pSession, SUPCOOKIE *pHdr)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  ReqHdrFlag = pHdr->Hdr.fFlags;
  if ( (ReqHdrFlag & 0xFF0000FF) == 0x42000042  // SUPREQHDR_FLAGS_MAGIC_MASK, SUPREQHDR_FLAGS_MAGIC
    && (CbIn = pHdr->Hdr.cbIn, CbIn >= sizeof(SUPREQHDR))
    && (CbOut = pHdr->Hdr.cbOut, CbOut >= sizeof(SUPREQHDR)) )
  {
    if ( IoCtlCode == SUP_IOCTL_COOKIE )
    {
      if ( pHdr->Hdr.u32Cookie != 0x69726F74 )  // SUPCOOKIE_INITIAL_COOKIE
      {
        DbgPrint("SUP_IOCTL_COOKIE: bad cookie %#lx\\n", pHdr->Hdr.u32Cookie);
        return 0xFFFFFFFEi64;
      }
    }
    else if ( *&pHdr->Hdr.u32Cookie != __PAIR64__(pSession->u32Cookie, DevExt->u32Cookie) )
    {
      DbgPrint("vboxdrv: bad cookie %#lx / %#lx.\\n", pHdr->Hdr.u32Cookie, pHdr->Hdr.u32SessionCookie);
      return 0xFFFFFFFEi64;
    }
...

드라이버에서 생성된 쿠키 값을 이용하여 또 하나의 유효성을 체크하는 것으로 보였습니다.

위에서 언급했듯이 각 제어 코드에서 사용되는 버퍼의 구조는 SUPREQHDR 구조를 포함하고 있습니다. 때문에 위와 같은 유효성 검증이 가능하지만 해당 값을 알맞게 맞춰 입력을 전달하면 우회가 가능합니다.

  1. 먼저 SUPREQHDR.fFlags 를 확인합니다. SUPREQHDR_FLAGS_MAGIC_MASK(0xFF0000FF) 를 이용하여 전달된 버퍼 헤더의 플래그가 SUPREQHDR_FLAGS_MAGIC(0x42000042) 인지 확인합니다.
  2. 헤더의 입력 사이즈가 SUPREQHDR 사이즈와 동일한지 확인합니다.
  3. 헤더의 출력 사이즈가 SUPREQHDR 사이즈와 동일한지 확인합니다.

위의 3개의 조건이 만족하면 다음과 같은 루틴이 존재합니다.

  1. 제어 코드가 SUP_IOCTL_COOKIE 인 경우 버퍼 헤더의 u32Cookie 값이 0x69726F74("tori") 인지 확인합니다. (조건에 해당하는 코드는 아래에서 계속됩니다.)
  2. 제어 코드가 SUP_IOCTL_COOKIE 가 아닌 경우, 버퍼 헤더의 u32Cookie 값과 u32SessionCookie 값을 현재 SUPDRVDEVEXTu32CookieSUPDRVSESSIONu32Cookie 의 값을 비교합니다. 다른 경우 에러를 리턴합니다.

IDA 에서는 __PARI64__ 를 통해 표현되어 잘못된 분석이 될 수 있습니다. 실제 구문은 아래와 같습니다.

mov     ecx, [rbx]        ; u32Cookie in Request header
cmp     ecx, [r13+18h]
jnz     loc_140004F12
mov     eax, [rbp+8]
cmp     [rbx+4], eax      ; u32SessionCookie in Request header

이러한 과정을 모두 통과하면 제어코드에 의한 루틴들이 실행됩니다.

위에서 언급한 것과 같이 그렇다면 왜 SUP_IOCTL_COOKIE 를 먼저 실행하는가에 대한 답변은 다음 코드에 있습니다.

case SUP_IOCTL_COOKIE:
        if ( CbIn != 0x30 || CbOut != 0x38 )
        {
          DbgPrint(
            "( ((0x00000022) << 16) | ((( 0x0002 )) << 14) | (((1) | 128) << 2) | (0) ): Invalid input/output sizes. cbIn"
            "=%ld expected %ld. cbOut=%ld expected %ld.\\n",
            CbIn,
            0x30i64);
          goto LABEL_173;
        }
        if ( !strncmp(pHdr->u.In.szMagic, "The Magic Word!", 0x10ui64) )// SUPCOOKIE_MAGIC
        {
          MinVersion = pHdr->u.In.u32MinVersion;
          if ( MinVersion <= 0x70002 && (MinVersion & 0xFFFF0000) == 0x70000 )// SUPDRVIOC_VERSION
          {
            pHdr->u.Out.u32Cookie = DevExt->u32Cookie;
            SessionCookie = pSession->u32Cookie;
            pHdr->Hdr.rc = 0;
            pHdr->u.Out.u32SessionCookie = SessionCookie;
            pHdr->u.Out.u32SessionVersion = 0x70002;// SUPDRVIOC_VERSION
            result = 0i64;
            pHdr->u.Out.u32DriverVersion = 0x70002;// SUPDRVIOC_VERSION
            pHdr->u.Out.pSession = pSession;
            pHdr->u.Out.cFunctions = 0x53;
          }
          else
          {
            DbgPrint(
              "SUP_IOCTL_COOKIE: Version mismatch. Requested: %#x  Min: %#x  Current: %#x\\n",
              pHdr->u.Out.cFunctions,
              MinVersion,
              0x70002i64);
            pHdr->u.Out.u32Cookie = 0xFFFFFFFF;
            pHdr->u.Out.pSession = 0i64;
            pHdr->u.Out.cFunctions = 0;
            pHdr->u.Out.u32SessionCookie = 0xFFFFFFFF;
            pHdr->u.Out.u32SessionVersion = 0xFFFFFFFF;
            pHdr->u.Out.u32DriverVersion = 0x70002;
            pHdr->Hdr.rc = 0xFFFFFFF5;
            result = 0i64;
          }
        }
        else
        {
          DbgPrint("SUP_IOCTL_COOKIE: invalid magic %.16s\\n", &pHdr->u);
          pHdr->Hdr.rc = 0xFFFFFFFD;
          result = 0i64;
        }
        return result;
...

각 조건은 아래와 같습니다.

  1. 헤더 내 사이즈 변수에 대한 검증(입력)
  2. 헤더 내 사이즈 변수에 대한 검증(출력)
  3. 헤더의 입력 버퍼 내 szMagic 의 값이 "The Magic Word!" 인지 검증
  4. 헤더의 입력 버퍼 내 u32MinVersion 의 값이 SUPDRVIOC_VERSION(0x70002) 인지 검증

위의 조건을 모두 충족하게 되면 출력 버퍼에 첫 검증에서 필요했던 쿠키 값과 세션 쿠키 값을 저장하고 루틴이 종료됩니다.

로더에서 사용되는 제어코드를 사용하기 위해 초기화 과정으로 볼 수 있습니다. 각 구조와 내용만 안다면 해당 드라이버에 구현되어 있는 모든 제어코드를 마음대로 호출하고 이용할 수 있게 되었습니다.

[-] supdrvIOCtl(SUP_IOCTL_LDR_OPEN)

첫 쿠키 값과 세션 쿠키 값에 대한 검증, 그리고 길이에 대한 검증 외의 검증은 각 제어코드 요청에 따른 값에 대한 검증만이 존재합니다. 아래는 SUP_IOCTL_LDR_OPEN 에 대한 케이스 입니다.

case SUP_IOCTL_LDR_OPEN:
        if ( CbIn == 0x40 && CbOut == 0x28 )
        {
          ImageSize = pHdr->u.In.cbImage;
          if ( !ImageSize )                     // valid check image size(>0)
          {
            DbgPrint("SUP_IOCTL_LDR_OPEN: %s\\n", "pReq->u.In.cbImage > 0");
            pHdr->Hdr.rc = 0xFFFFFFFE;
            return 0xFFFFFFFEi64;
          }
          if ( ImageSize >= 0x1000000 )         // valid check image size(< 0x1000000)
          {
            DbgPrint("SUP_IOCTL_LDR_OPEN: %s\\n", "pReq->u.In.cbImage < _1M*16");
            pHdr->Hdr.rc = 0xFFFFFFFE;
            return 0xFFFFFFFEi64;
          }
          ImageName = pHdr->u.In.szName;        // valid check szName
          if ( !pHdr->u.In.szName[0] )
          {
            DbgPrint("SUP_IOCTL_LDR_OPEN: %s\\n", "pReq->u.In.szName[0]");
            pHdr->Hdr.rc = -2;
            return 0xFFFFFFFEi64;
          }
          if ( !memchr(pHdr->u.In_.szName, 0, 0x20ui64) )// valid check szName(length&last null)
          {
            DbgPrint("SUP_IOCTL_LDR_OPEN: %s\\n", "memchr(pReq->u.In.szName, '\\\\0', sizeof(pReq->u.In.szName))");
            pHdr->Hdr.rc = -2;
            return 0xFFFFFFFEi64;
          }
          v20 = *ImageName;
          if ( !*ImageName )
            goto LABEL_68;
          while ( 1 )                           // valid check szName(check special characters)
          {
            ++ImageName;
            v21 = ";:()[]{}/\\\\|&*%#@!~`\\"'";
            v22 = 59;
            do
            {
              ++v21;
              if ( v22 == v20 )
              {
                DbgPrint(
                  "SUP_IOCTL_LDR_OPEN: %s\\n",
                  "!supdrvCheckInvalidChar(pReq->u.In.szName, \\";:()[]{}/\\\\\\\\|&*%#@!~`\\\\\\"'\\")");
                pHdr->Hdr.rc = -2;
                return 0xFFFFFFFEi64;
              }
              v22 = *v21;
            }
            while ( *v21 );
            v20 = *ImageName;
            if ( !*ImageName )
            {
LABEL_68:
              pHdr->Hdr.rc = supdrvIOCtl_LdrOpen(DevExt, pSession, pHdr);
              return 0i64;
            }
          }
        }
        DbgPrint(
          "( ((0x00000022) << 16) | ((( 0x0002 )) << 14) | (((5) | 128) << 2) | (0) ): Invalid input/output sizes. cbIn=%"
          "ld expected %ld. cbOut=%ld expected %ld.\\n",
          CbIn,
          64i64);

먼저 해당 제어 코드의 역할을 이해하기 전에, 간단한 검증 로직을 확인하면 아래와같습니다.

  • 입력 버퍼 내 cbImage 의 사이즈 체크(0보다 크고, 0x1000000 보다 작아야 함)
  • szName은 20자 이하여야 함
  • 특수문자를 허용하지 않음

위와 같이 간단한 검증을 거친 후 supdrvIOCtl_LdrOpen 을 통해 코드가 실행됩니다.

supdrvIOCtl_LdrOpen

해당 함수의 경우 실제 코드를 확인합니다.

static int supdrvIOCtl_LdrOpen(PSUPDRVDEVEXT pDevExt, PSUPDRVSESSION pSession, PSUPLDROPEN pReq)
{
    PSUPDRVLDRIMAGE pImage;
    unsigned        cb;
    void           *pv;
    LogFlow(("supdrvIOCtl_LdrOpen: szName=%s cbImage=%d\\n", pReq->u.In.szName, pReq->u.In.cbImage));

    RTSemFastMutexRequest(pDevExt->mtxLdr);
    for (pImage = pDevExt->pLdrImages; pImage; pImage = pImage->pNext)
    {
        if (!strcmp(pImage->szName, pReq->u.In.szName))
        {
            pImage->cUsage++;
            pReq->u.Out.pvImageBase   = pImage->pvImage;
            pReq->u.Out.fNeedsLoading = pImage->uState == SUP_IOCTL_LDR_OPEN;
            supdrvLdrAddUsage(pSession, pImage);
            RTSemFastMutexRelease(pDevExt->mtxLdr);
            return VINF_SUCCESS;
        }
    }

    cb = pReq->u.In.cbImage + sizeof(SUPDRVLDRIMAGE) + 31;
    pv = RTMemExecAlloc(cb);
    if (!pv)
    {
        RTSemFastMutexRelease(pDevExt->mtxLdr);
        Log(("supdrvIOCtl_LdrOpen: RTMemExecAlloc(%u) failed\\n", cb));
        return VERR_NO_MEMORY;
    }

    pImage = (PSUPDRVLDRIMAGE)pv;
    pImage->pvImage         = RT_ALIGN_P(pImage + 1, 32);
    pImage->cbImage         = pReq->u.In.cbImage;
    pImage->pfnModuleInit   = NULL;
    pImage->pfnModuleTerm   = NULL;
    pImage->uState          = SUP_IOCTL_LDR_OPEN;
    pImage->cUsage          = 1;
    strcpy(pImage->szName, pReq->u.In.szName);

    pImage->pNext           = pDevExt->pLdrImages;
    pDevExt->pLdrImages     = pImage;

    supdrvLdrAddUsage(pSession, pImage);

    pReq->u.Out.pvImageBase = pImage->pvImage;
    pReq->u.Out.fNeedsLoading = true;
    RTSemFastMutexRelease(pDevExt->mtxLdr);
    return VINF_SUCCESS;
}

정확히 이해한게 맞는지 모르지만, 드라이버로 전달되는 입력 버퍼의 멤버가 단순히 이미지 사이즈와 이미지 이름이라는 점과 별도의 이미지 버퍼는 없다는 점에서 이미지 사이즈 만큼의 메모리 할당과 해당 메모리를 식별 가능한 문자열을 이미지 이름으로 사용하는 것으로 보입니다.

첫 반복문에서는 드라이버 진입점에서 디바이스 확장을 통해 할당되고 SUPDRVDEVEXT 구조로 이루어진 pDevExt 변수를 이용하여 요청된 이미지 이름과 동일한 이미지가 존재하는지 확인합니다.

여기서 제가 이해한 바는 다음과 같습니다.

  1. VirtualBox 의 경우 이미지라는 개념을 이용하여 커널에 특정 메모리를 할당하고 이를 디바이스 확장(Device Extension)을 이용하여 해당 커널 메모리에 저장하고 사용한다.
  2. 해당하는 pLdrImagesSUPLDRIMAGE 로 된 가변적으로 할당되어 있으며, 해당 메모리에 대한 정보를 가지고 있다.

SUP_IOCTL_LDR_OPEN 은 이미지가 존재한다면 해당 이미지에 맞는 정보를 출력 버퍼를 통해 유저모드 애플리케이션으로 전달을하게 되고, 없는 경우 생성하여 해당 정보를 전달하게 됩니다.

[-] supdrvIOCtl(SUP_IOCTL_LDR_LOAD)

SUP_IOCTL_LDR_OPEN 으로 미루어 볼 때 LOAD 의 의미는 OPEN 을 통해 새로 할당받거나 이미 만들어진 이미지 메모리에 값을 복사하는 행위가 발생할 것으로 예상됩니다.

즉 유저 애플리케이션에서 전달받은 이미지를 커널에 로드하는 역할으로 이해할 수 있습니다.

...
case SUP_IOCTL_LDR_LOAD:
        if ( CbIn < 0x70 )
        {
          DbgPrint("Name: %s\\n", "pReq->Hdr.cbIn >= sizeof(*pReq)");
          pHdr->Hdr.rc = 0xFFFFFFFE;
          return 0xFFFFFFFEi64;
        }
        v23 = pHdr->u.In.cbImage;
        if ( CbIn == v23 + 104 && CbOut == 24 )
        {
          v24 = pHdr->u.In.cSymbols;
          if ( v24 > 0x4000 )
          {
            DbgPrint("SUP_IOCTL_LDR_LOAD: %s\\n", "pReq->u.In.cSymbols <= 16384");
            pHdr->Hdr.rc = -2;
            return 0xFFFFFFFEi64;
          }
          if ( v24 )
          {
            v25 = pHdr->u.In.offSymbols;
            if ( v25 >= v23 || v25 + 8 * v24 > v23 )
            {
              DbgPrint("SUP_IOCTL_LDR_LOAD: offSymbols=%#lx cSymbols=%#lx cbImage=%#lx\\n", v25, v24, v23);
              pHdr->Hdr.rc = -2;
              return 0xFFFFFFFEi64;
            }
          }
          v26 = pHdr->u.In.cbStrTab;
          if ( v26 )
          {
            v27 = pHdr->u.In.offStrTab;
            if ( v27 >= v23 || v27 + v26 > v23 || v26 > v23 )
            {
              DbgPrint("SUP_IOCTL_LDR_LOAD: offStrTab=%#lx cbStrTab=%#lx cbImage=%#lx\\n", v27, v26, v23);
              pHdr->Hdr.rc = -2;
              return 0xFFFFFFFEi64;
            }
          }
          if ( !v24 )
          {
LABEL_92:
            pHdr->Hdr.rc = supdrvIOCtl_LdrLoad(DevExt, pSession, pHdr);
            return 0i64;
          }
          v28 = 0;
          v29 = &pHdr->u.In.achImage[pHdr->u.In.offSymbols];
          v30 = v29;
          while ( 1 )
          {
            if ( *(v30 + 1) >= pHdr->u.In.cbImage )
            {
              DbgPrint(
                "SUP_IOCTL_LDR_LOAD: sym #%ld: symb off %#lx (max=%#lx)\\n",
                v28,
                *&v29[8 * v28 + 4],
                pHdr->u.In.cbImage);
              goto LABEL_96;
            }
            v31 = pHdr->u.In.cbStrTab;
            if ( *v30 >= v31 )
              break;
            if ( !memchr(&pHdr->u.In.achImage[*v30 + pHdr->u.In.offStrTab], 0, v31 - *v30) )
            {
              DbgPrint(
                "SUP_IOCTL_LDR_LOAD: sym #%ld: unterminated name! (%#lx / %#lx)\\n",
                v28,
                *&v29[8 * v28],
                pHdr->u.In.cbImage);
LABEL_96:
              pHdr->Hdr.rc = -2;
              return 0xFFFFFFFEi64;
            }
            ++v28;
            v30 += 8;
            if ( v28 >= pHdr->u.In.cSymbols )
              goto LABEL_92;
          }
          DbgPrint("SUP_IOCTL_LDR_LOAD: sym #%ld: name off %#lx (max=%#lx)\\n", v28, *&v29[8 * v28], pHdr->u.In.cbImage);
          goto LABEL_96;
        }
        DbgPrint(
          "SUP_IOCTL_LDR_LOAD: Invalid input/output sizes. cbIn=%ld expected %ld. cbOut=%ld expected %ld.\\n",
          CbIn,
          (v23 + 104));
LABEL_173:
        pHdr->Hdr.rc = 0xFFFFFFFE;
        result = 0xFFFFFFFEi64;
        break;
...

해당 코드는 명확히 이해하지 못했습니다. 앞 부분 검증의 경우 다른 제어 코드와 다를 바가 없습니다. 다만 심볼이라는 개념이 존재하는데 이것이 실제 우리가 알고 있는 심볼인지는 의문을 갖게 됩니다.

일단 cSymbols 변수를 통해 심볼의 개수를 확인하는 것을 확인할 수 있으며, 0x4000(16384) 개를 초과하게 되면 코드가 실행되지 않습니다.

본인이 참고한 로더에서는 해당 값을 설정하지 않음으로써 실제 동작 루틴인 supdrvIOCtl_LdrLoad 이 바로 실행되게 됩니다.

supdrvIOCtl_LdrLoad

LOAD 동작은 생각보다 단순합니다. OPEN 을 통해 새로 할당되었거나 기존에 있던 이미지에 입력 버퍼에 있는 값을 복사해줍니다. 이 과정에서 값이 있는지에 대한 유무와 사이즈 검증이 존재합니다.

__int64 __fastcall supdrvIOCtl_LdrLoad(SUPDRVDEVEXT *DevExt, SUPDRVSESSION *pSession, SUPLDRLOAD *pReq)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  RTSemFastMutexRequest(DevExt->mtxLdr);
  pUsage = pSession->pLdrUsage;
  if ( !pUsage )
  {
LABEL_4:
    RTSemFastMutexRelease(DevExt->mtxLdr);
    return 0xFFFFFFFCi64;
  }
  while ( 1 )
  {
    pImage = pUsage->pImage;
    if ( pImage->pvImage == pReq->u.In.pvImageBase )// found image
      break;
    pUsage = pUsage->pNext;
    if ( !pUsage )
      goto LABEL_4;
  }
  ImageSize = pImage->cbImage;
  ReqImageSize = pReq->u.In.cbImage;            // input image size
  pImage_1 = pImage;
  if ( ImageSize == ReqImageSize )
  {
    if ( pImage->uState != SUP_IOCTL_LDR_OPEN )
    {
      RTSemFastMutexRelease(DevExt->mtxLdr);
      return 0xFFFFFFF7i64;
    }
    EntryPointType = pReq->u.In.eEPType;
    if ( EntryPointType                         // SUPLDRLOADEP_NOTHING
      && (EntryPointType != SUPLDRLOADEP_VMMR0
       || !pReq->u.In.EP.VMMR0.pvVMMR0
       || (v13 = pReq->u.In.EP.VMMR0.pvVMMR0EntryInt) == 0i64
       || (v14 = pReq->u.In.EP.VMMR0.pvVMMR0EntryFast) == 0i64
       || (v15 = pReq->u.In.EP.VMMR0.pvVMMR0EntryEx) == 0i64
       || (v16 = pImage_1->pvImage, v13 - v16 >= ReqImageSize)
       || v14 - v16 >= ReqImageSize
       || v15 - v16 >= ReqImageSize)
      || (v17 = pReq->u.In.pfnModuleInit) != 0i64 && (v17 - pImage_1->pvImage) >= ReqImageSize
      || (v18 = pReq->u.In.pfnModuleTerm) != 0i64 && (v18 - pImage_1->pvImage) >= ReqImageSize )
    {
      RTSemFastMutexRelease(DevExt->mtxLdr);
      return 0xFFFFFFFEi64;
    }
    memmove(pImage_1->pvImage, pReq->u.In.achImage, ImageSize);// Copy memory
    pImage_1->uState = SUP_IOCTL_LDR_LOAD;
    pImage_1->pfnModuleInit = pReq->u.In.pfnModuleInit;
    pImage_1->pfnModuleTerm = pReq->u.In.pfnModuleTerm;
    pImage_1->offSymbols = pReq->u.In.offSymbols;
    pImage_1->cSymbols = pReq->u.In.cSymbols;
    pImage_1->offStrTab = pReq->u.In.offStrTab;
    pImage_1->cbStrTab = pReq->u.In.cbStrTab;
    if ( pReq->u.In.eEPType == SUPLDRLOADEP_VMMR0 )// SUPLDRLOADEP_VMMR0, supdrvLdrSetR0EP
    {
      VMMR0EntryEx = pReq->u.In.EP.VMMR0.pvVMMR0EntryEx;
      VMMR0EntryFast = pReq->u.In.EP.VMMR0.pvVMMR0EntryFast;
      VMMR0EntryInt = pReq->u.In.EP.VMMR0.pvVMMR0EntryInt;
      VMMR0 = pReq->u.In.EP.VMMR0.pvVMMR0;
      rc = 0;
      if ( DevExt->pvVMMR0 )
      {
        if ( DevExt->pvVMMR0 != VMMR0
          || DevExt->VMMR0EntryInt != VMMR0EntryInt
          || DevExt->VMMR0EntryFast != VMMR0EntryFast
          || DevExt->VMMR0EntryEx != VMMR0EntryEx )
        {
          rc = 0xFFFFFFFE;
        }
      }
      else
      {
        DevExt->pvVMMR0 = VMMR0;
        DevExt->VMMR0EntryInt = VMMR0EntryInt;
        DevExt->VMMR0EntryFast = VMMR0EntryFast;
        DevExt->VMMR0EntryEx = VMMR0EntryEx;
      }
      if ( rc < 0 )
        goto LABEL_38;
    }
    else                                        // SUPLDRLOADEP_NOTHING
    {
      rc = 0;
    }
    pfnModuleInit = pImage_1->pfnModuleInit;
    if ( pfnModuleInit )
    {
      rc = pfnModuleInit();
      if ( !rc )
      {
LABEL_40:
        RTSemFastMutexRelease(DevExt->mtxLdr);
        return rc;
      }
      if ( DevExt->pvVMMR0 == pImage_1->pvImage )// supdrvLdrUnsetR0EP
      {
        DevExt->pvVMMR0 = 0i64;
        DevExt->VMMR0EntryInt = 0i64;
        DevExt->VMMR0EntryFast = 0i64;
        DevExt->VMMR0EntryEx = 0i64;
      }
    }
LABEL_38:
    if ( rc )
      pImage_1->uState = SUP_IOCTL_LDR_OPEN;
    goto LABEL_40;
  }
  RTSemFastMutexRelease(DevExt->mtxLdr);
  return 0xFFFFFFFCi64;
}

사이즈와 각 변수에 대한 매우 간단한 검증이 끝나면, 입력 버퍼에 존재하는 ArchImage 변수에서 이미지 사이즈만큼 커널에 생성하거나 이미 존재하는 이미지에 복사합니다.(SUP_IOCTL_LDR_OPEN 참조)

복사를 한 후, 이미지의 상태 값을 SUP_IOCTL_LDR_LOAD 상태로 변경하며, 입력 버퍼의 값들을 복사하기 시작합니다.

EPType 이라는 것이 존재하는데 별도로 살펴보진 않았지만 예상컨데 EntryPoint Type 으로 예상됩니다. 해당 타입이 SUPLDRLOADEP_VMMR0 인 경우에는 디바이스 확장 포인터에 해당 값들을 세팅합니다.

이제 로드 된 이미지를 실행만 하면 되는 것으로 보입니다.

[-] supdrvIOCtl(SUP_IOCTL_CALL_VMR0)

...
case SUP_IOCTL_CALL_VMR0:
        if ( CbIn == 0x30 )
        {
          if ( CbOut != 0x30 )
          {
            DbgPrint(
              "SUP_IOCTL_CALL_VMMR0: Invalid input/output sizes. cbIn=%ld expected %ld. cbOut=%ld expected %ld.\\n",
              48i64,
              48i64);
            goto LABEL_173;
          }
          VMMR0EntryEx= DevExt->VMMR0EntryEx;
          if ( v32 )
            pHdr->Hdr.rc = (VMMR0EntryEx)(pHdr->u.In.pVMR0, pHdr->u.In.uOperation, 0i64, pHdr->u.In.u64Arg); // Call
          else
            pHdr->Hdr.rc = 0xFFFFFFEA;
          result = 0i64;
        }
...

별도의 함수 처리는 없습니다. 디바이스 확장 포인터에서 VMMR0EntryEx 를 호출합니다.

이 때 넘어가는 파라미터는 다음과 표현할 수 있습니다.

vboxstatus VMMR0EntryEx(PVM pVM, unsigned uOperation, PSUPVMMR0REQHDR pReq, uint64_t u64Arg);

[0x02] PoC

간단히 서명되지 않은 드라이버가 매핑된 모습입니다.

0: kd> dt_DRIVER_OBJECT FFFFC302D92AEE20
nt!_DRIVER_OBJECT
   +0x000 Type             : 0n4
   +0x002 Size             : 0n336
   +0x008 DeviceObject     : (null) 
   +0x010 Flags            : 4
   +0x018 DriverStart      : (null) 
   +0x020 DriverSize       : 0
   +0x028 DriverSection    : (null) 
   +0x030 DriverExtension  : 0xffffc302`d92aef70 _DRIVER_EXTENSION
   +0x038 DriverName       : _UNICODE_STRING "\\Driver\\00000054"
   +0x048 HardwareDatabase : (null) 
   +0x050 FastIoDispatch   : (null) 
   +0x058 DriverInit       : 0xffffc302`dacf3c10     long  +ffffc302dacf3c10
   +0x060 DriverStartIo    : (null) 
   +0x068 DriverUnload     : (null) 
   +0x070 MajorFunction    : [28] 0xfffff807`19d364c0     long  nt!IopInvalidDeviceRequest+0

0: kd> u 0xffffc302`dacf3c10 l20
ffffc302`dacf3c10 4053            push    rbx
ffffc302`dacf3c12 4883ec20        sub     rsp,20h
ffffc302`dacf3c16 4883254a45000000 and     qword ptr [ffffc302`dacf8168],0
ffffc302`dacf3c1e 488bd9          mov     rbx,rcx
ffffc302`dacf3c21 48890d60440000  mov     qword ptr [ffffc302`dacf8088],rcx
ffffc302`dacf3c28 baf8000000      mov     edx,0F8h
ffffc302`dacf3c2d 33c9            xor     ecx,ecx
ffffc302`dacf3c2f ff15db330000    call    qword ptr [ffffc302`dacf7010]
ffffc302`dacf3c35 4889052c450000  mov     qword ptr [ffffc302`dacf8168],rax
ffffc302`dacf3c3c 4885c0          test    rax,rax
ffffc302`dacf3c3f 751b            jne     ffffc302`dacf3c5c
ffffc302`dacf3c41 ba410100c0      mov     edx,0C0000141h
ffffc302`dacf3c46 488d0d23260000  lea     rcx,[ffffc302`dacf6270]
ffffc302`dacf3c4d e8ae130000      call    ffffc302`dacf5000
ffffc302`dacf3c52 b8010000c0      mov     eax,0C0000001h
ffffc302`dacf3c57 e9e0000000      jmp     ffffc302`dacf3d3c
ffffc302`dacf3c5c 4c8bcb          mov     r9,rbx
ffffc302`dacf3c5f 4c8d052a260000  lea     r8,[ffffc302`dacf6290]
ffffc302`dacf3c66 33d2            xor     edx,edx
ffffc302`dacf3c68 33c9            xor     ecx,ecx
ffffc302`dacf3c6a ff1590330000    call    qword ptr [ffffc302`dacf7000]
ffffc302`dacf3c70 8b05fa430000    mov     eax,dword ptr [ffffc302`dacf8070]
ffffc302`dacf3c76 a802            test    al,2
ffffc302`dacf3c78 741d            je      ffffc302`dacf3c97
ffffc302`dacf3c7a 4c8d052f260000  lea     r8,[ffffc302`dacf62b0]
ffffc302`dacf3c81 33d2            xor     edx,edx
ffffc302`dacf3c83 33c9            xor     ecx,ecx
ffffc302`dacf3c85 ff1575330000    call    qword ptr [ffffc302`dacf7000]
ffffc302`dacf3c8b e8f0f7ffff      call    ffffc302`dacf3480
ffffc302`dacf3c90 488905a1440000  mov     qword ptr [ffffc302`dacf8138],rax
ffffc302`dacf3c97 488b0dca440000  mov     rcx,qword ptr [ffffc302`dacf8168]
ffffc302`dacf3c9e 488d05cb430000  lea     rax,[ffffc302`dacf8070]

[0x03] Conclusion

현재 Github에 존재하는 다양한 소스코드를 참조하면 커널 내 온전하게 서명되지 않은 드라이버를 로드할 수 있고 이를 사용할 수 있습니다. 😆

[0x04] Reference

  1. VirtualBox source code
    1. https://www.virtualbox.org/browser/vbox/trunk/src/VBox/HostDrivers/Support/SUPDRVIOC.h
    2. https://www.virtualbox.org/browser/vbox/trunk/src/VBox/HostDrivers/Support/SUPDRVShared.c
    3. https://www.virtualbox.org/browser/vbox/trunk/src/VBox/HostDrivers/Support/SUPDRV.h