Dispositivos para-virtualizados distorcidos no Hyper-V

 

 

 

Introdução

 

 

 

O Hyper-V é o backbone do Azure, sendo executado em seus Hosts para fornecer compartilhamento eficiente e justo de recursos, mas também isolamento. É por isso que, na equipe de pesquisa de vulnerabilidades do Windows , há anos trabalhamos em segundo plano para ajudar a proteger o Hyper-V. E por que a Microsoft convida pesquisadores de segurança em todo o mundo a enviar suas vulnerabilidades por meio do Programa de Recompensas Hyper-V para pagamento de até US $ 250.000. 

Para ajudar a envolver as pessoas no espaço de segurança do Hyper-V, no ano passado equipes internas da Microsoft publicaram alguns de seus trabalhos. 

Na BlackHat 2018, EUA Joe Bialek e Nicolas Joly apresentaram ” Um mergulho na arquitetura e vulnerabilidades do Hyper-V”“Eles cobriram uma visão geral de arquitetura do Hyper-V voltada para pesquisadores de segurança. Eles também discutiram algumas vulnerabilidades interessantes vistas no Hyper-V. 

Na mesma conferência, Jordan Rabet apresentou” Hardening Hyper-V através de pesquisa de segurança ofensiva “, onde discutiu em grande detalhe o processo de exploração do CVE-2017-0075 no VMSwitch, um componente do Hyper-V 

Em dezembro do ano passado, Saar Amar publicou um blog detalhado com os fundamentos para ser introduzido na pesquisa de segurança do Hyper-V.

Após o trabalho, gostaríamos de compartilhar uma nova história relacionada à segurança do Hyper-V para qualquer pessoa interessada em se introduzir na segurança do Hyper-V ou aprender mais. Recentemente, trabalhamos no Virtual PCI (VPCI), um dos dispositivos para-virtualizados disponíveis no Hyper-V, usado para expor hardware a máquinas virtuais. Como outros dispositivos para-virtualizados, ele usa o VMBus para comunicação entre partições. 

Neste blog, gostaríamos de compartilhar algumas de nossas aprendizagens, apresentar o VMBus e o VPCI, compartilhar uma estratégia para fuzzar o canal VMBus usado pela VPCI e discutir uma de nossas descobertas. Alguns dos conceitos e estratégias aqui podem ser usados ​​para trabalhar com outros dispositivos virtuais usando o VMBus no Hyper-V.

 

 

 

Visão geral do VMBus

 

 

 

 

 

 

O VMBus é um dos mecanismos usados ​​pelo Hyper-V para oferecer a para-virtualização. Em suma, é um dispositivo de barramento virtual que configura canais entre o convidado e o host. Esses canais fornecem a capacidade de compartilhar dados entre partições e dispositivos sintéticos de configuração. 

Nesta seção, apresentaremos a arquitetura VMBus, aprenderemos como os canais são oferecidos às partições e como os dispositivos sintéticos são configurados. 

A partição raiz (ou host) hospeda os VSP (Virtualization Service Providers – Provedores de Serviços de Virtualização) que se comunicam por meio do VMBus para manipular solicitações de acesso a dispositivos de partições filhas. Por outro lado, partições filhas (ou guests) usam Consumidores de Serviços de Virtualização (VSC) para redirecionar solicitações de dispositivos para o VSP sobre VMBus. Partições filhas exigem que os drivers VMBus e VSC usem as pilhas de dispositivos para-virtualizados.

Os canais VMBus permitem que os VSCs e VSPs transfiram dados principalmente por meio de dois buffers de anel: upstream e downstream. Esses buffers de anéis são mapeados em ambas as partições graças ao hipervisor, que também fornece interrupções sintéticas para conduzir a notificação entre partições quando há dados disponíveis. 

A arquitetura pode ser resumida no próximo diagrama:

 

 

 

Uma introdução mais detalhada ao VMBus pode ser encontrada nas apresentações vinculadas antes:

 

 

 

 

 

 

 

 

 

Como o VMBus permite a transmissão de dados relacionados a E / S entre o convidado potencialmente mal-intencionado e os drivers VSP no host, os mais recentes são os principais candidatos a caça e difusão de vulnerabilidades. Uma ideia geral para difundir dispositivos virtuais é encontrar o canal VMBus disponível para um VSC e usá-lo para enviar dados malformados para o VSP. 

Para isso, precisamos entender amplamente como os canais VMBus são disponibilizados para os VSCs. Vamos começar apresentando como o dispositivo VMBus é disponibilizado para o convidado. Do ponto de vista prático, se você implantar uma Máquina Virtual do Windows Generation 2 (convidado iluminado), poderá localizar o dispositivo VMBus exposto no Gerenciador de dispositivos:

 

 

 

A exibição da conexão no Gerenciador de Dispositivos também revela que o VMBus é exposto ao convidado por meio da ACPI. De fato, sua descrição pode ser encontrada na Tabela de Descrição do Sistema Diferenciado (DSDT):

 

 

 

Dispositivo (\ _ SB.VMOD.VMBS)
{
    Nome (STA, 0x0F)
    Nome (_ADR, Zero)
    Nome (_DDN, "VMBUS")
    Nome (_HID, "VMBus")
    Nome (_UID, Zero)
    Método (_DIS, 0, NotSerialized)
    {
        E (STA, 0x0D, STA)
    }
    Método (_PS0, 0, NotSerialized)
    {
        Ou (STA, 0x0F, STA)
    }
    Método (_STA, 0, NotSerialized)
    {
        Retorno (STA)
    }
    Nome (_PS3, Zero)
    Nome (_CRS, ResourceTemplate ()
    {
        IRQ (Borda, ActiveHigh, Exclusivo) {5}
    })
}

 

 

 

Depois que o VMBus estiver pronto, para cada canal oferecido pela partição raiz, o convidado criará um novo nó na árvore de dispositivos. O fluxo resumido (e genérico) é:

 

 

 

  1. A partição raiz oferece um canal.
  2. A oferta é entregue ao convidado por meio de uma interrupção sintética.
  3. No convidado, por causa da interrupção, uma consulta de relação de barramento é injetada no sistema PnP.
  4. No convidado, o driver VMBus cria um novo objeto de dispositivo físico (PDO) para a pilha de dispositivos. As informações da oferta são salvas no contexto PDO.
  5. O driver de dispositivo (por exemplo, VPCI) cria um novo Objeto de Dispositivo Funcional (FDO) para a pilha de dispositivos. A rotina usada para criar os objetos FDO, por exemplo AddDevice no caso de um driver Plug and Play, é um bom ponto para encontrar o código que aloca e abre o novo canal VMBus.

 

 

 

Um depurador de kernel e o comando “ !devnode” podem ser usados ​​para listar os dispositivos disponíveis no topo do VMBus dentro de um convidado:

 

 

 

0: kd>! Devnode 0 1
Dumping IopRootDeviceNode (= 0xffffe28c76fbd9e0)
DevNode 0xffffe28c76fbd9e0 para PDO 0xffffe28c76e6b830
  InstancePath é "HTREE \ ROOT \ 0"
  Estado = DeviceNodeStarted (0x308)
  Estado anterior = DeviceNodeEnumerateCompletion (0x30d)
  .
  .
  .
  DevNode 0xffffe28c76ed19b0 para PDO 0xffffe28c76ecfd80
    InstancePath é "ROOT \ ACPI_HAL \ 0000"
    Estado = DeviceNodeStarted (0x308)
    Estado anterior = DeviceNodeEnumerateCompletion (0x30d)
    DevNode 0xffffe28c76f17c00 para PDO 0xffffe28c76eeed30
      InstancePath é "ACPI_HAL \ PNP0C08 \ 0"
      ServiceName é "ACPI"
      Estado = DeviceNodeStarted (0x308)
      Estado anterior = DeviceNodeEnumerateCompletion (0x30d)
      DevNode 0xffffe28c76e9e8b0 para PDO 0xffffe28c76f52900
        InstancePath é "ACPI \ ACPI0004 \ 0"
        Estado = DeviceNodeStarted (0x308)
        Estado anterior = DeviceNodeEnumerateCompletion (0x30d)
        DevNode 0xffffe28c76f5b8b0 para PDO 0xffffe28c76f54d60
          InstancePath é "ACPI \ PNP0003 \ 3 & fdac00f & 0"
          Estado = DeviceNodeInitialized (0x302)
          Estado anterior = DeviceNodeUninitialized (0x301)
        DevNode 0xffffe28c76f5bbe0 para PDO 0xffffe28c76f59c30
          InstancePath é "ACPI \ VMBus \ 0"
          ServiceName é "vmbus"
          Estado = DeviceNodeStarted (0x308)
          Estado anterior = DeviceNodeEnumerateCompletion (0x30d)
          .
          .
          .
          DevNode 0xffffe28c78629340 para PDO 0xffffe28c78625c90
            InstancePath é "VMBUS \ {44c4f61d-4444-4400-9d52-802e27ede19f} \ {7f7e8f36-7342-4531-a380-d3a9911f80bf}"
            ServiceName é "vpci"
            Estado = DeviceNodeStarted (0x308)
            Estado anterior = DeviceNodeEnumerateCompletion (0x30d)
            .
            .

 

 

 

Agora que estabelecemos o VMBus como um vetor de ataque interessante e aprendemos como usá-lo, podemos discutir um dos dispositivos virtuais que fazem uso dele: VPCI.

 

 

 

Caso de uso: VPCI

 

 

 

O VPCI é um driver de barramento virtualizado usado para expor o hardware a máquinas virtuais. Cenários usando VPCI incluem SR-IOV e DDA . É importante ressaltar que o VPCI só será exposto ao convidado se houver um dispositivo virtual que o exija (e isso deve ser configurado pelo host).

Nesta seção, aprenderemos como localizar o canal VMBus usado pelo VPCI e como usá-lo para enviar dados arbitrários ao VSP. Também fornecemos o esqueleto de um driver do Windows para ilustrar a ideia.

 

 

 

Como explicado anteriormente, todo dispositivo para-virtualizado exigirá um par VSC e VSP. No caso do VPCI, identificaremos o componente VSC como VPCI e o componente VSP como VPCIVSP. O VPCI é gerenciado pelo driver vpci.sys no convidado. Por outro lado, o vpcivsp.sys gerencia o componente VPCIVSP no host. Para a análise atual, estamos usando o vpci.sys versão 10.0.17134.228.

 

 

 

Encontrando o canal VMBus

 

 

 

Como foi apresentado anteriormente, a inicialização de um novo FDO é um bom ponto para começar a procurar alocação de canais VMBus. 
Como o VPCI é um driver do Kernel-Mode Driver Framework (KMDF), estamos interessados ​​na chamada e WdfDriverCreate, especificamente, no parâmetro DriverConfig:

 

 

 

NTSTATUS WdfDriverCreate (
  PDRIVER_OBJECT DriverObject,
  PCUNICODE_STRING RegistryPath,
  PWDF_OBJECT_ATTRIBUTES DriverAttributes, 
  PWDF_DRIVER_CONFIG DriverConfig,
  Driver WDFDRIVER *
);

 

 

 

O parâmetro DriverConfig é interessante porque é um ponteiro para uma WDF_DRIVER_CONFIGestrutura, onde podemos encontrar a EvtDriverDeviceAddfunção de retorno de chamada:

 

 

 

typedef struct _WDF_DRIVER_CONFIG {
  Tamanho ULONG;
  PFN_WDF_DRIVER_DEVICE_ADD EvtDriverDeviceAdd;
  PFN_WDF_DRIVER_UNLOAD EvtDriverUnload;
  ULONG DriverInitFlags;
  ULONG DriverPoolTag;
} WDF_DRIVER_CONFIG, * PWDF_DRIVER_CONFIG;

 

 

 

EvtDriverDeviceAddé chamado pelo gerenciador PnP para executar a inicialização do dispositivo quando um novo dispositivo é encontrado. 

No caso de VPCI é FdoDeviceAdd:

 

 

 

 

 

 

 

 

 

Durante a FdoDeviceAddVPCI, o novo canal VMBus será alocado com uma chamada para VmbChannelAllocate:

 

 

 

 

VmbChannelAllocateprotótipo pode ser encontrado no cabeçalho público vmbuskernelmodeclientlibapi.h . O ponteiro para o canal alocado é retornado dentro do terceiro parâmetro:

 

 

 

/// \ page VmbChannelAllocate VmbChannelAllocate
/// Aloca um novo canal VMBus com parâmetros padrão e retornos de chamada. o
O canal /// pode ser inicializado usando as rotinas VmbChannelInit * antes
/// sendo ativado com VmbChannelEnable. O canal deve ser liberado com
/// VmbChannelCleanup.
///
/// \ param ParentDeviceObject Um ponteiro para o dispositivo pai.
/// \ param IsServer Se o novo canal deve ser um ponto final do servidor.
/// \ param Channel Retorna um ponteiro para um canal alocado.
_IRQL_requires_ (PASSIVE_LEVEL)
NTSTATUS
VmbChannelAllocate (
    _In_ PDEVICE_OBJECT ParentDeviceObject,
    _In_ BOOLEAN IsServer,
    _Out_ _At _ (* Canal, __drv_allocatesMem (Mem)) Canal VMBCHANNEL *
    );

 

 

 

Para entender melhor como o canal é alocado e a referência armazenada, vamos analisar primeiro a chamada para FdoCreateVmBusChannela partir de FdoDeviceAdd:

 

 

 

__int64 __fastcall FdoDeviceAdd (__ int64 a1, __int64 a2)
{
  __int64 v5; // rbx
  assinado int v6; // esi
  .
  .
  .
  // WdfObjectGetTypedContextWorker, semelhante ao WdfObjectGetTypedContext
  v5 = (* (__ int64 (__fastcall **) (__ int64)) (WdfFunctions_01015 + 1616)) (WdfDriverGlobals); 
  .
  .
  .
  v6 = FdoCreateVmbusChannel ((_ QWORD *) v5);
  .
  .
  .
 }

 

 

 

O primeiro argumento FdoCreateVmbusChannelé o contexto do dispositivo FDO. FdoCreateVmbusChannelirá chamar VmbChannelAllocatee salvar a referência para o VMBCHANNEL alocado na pilha (variável local):

 

 

 

__int64 __fastcall FdoCreateVmbusChannel (_QWORD * FdoContext)
{
  v1 = FdoContext;
.
.
.
  __int64 vpciChannel; // [rsp + 70h] [rbp + 10h]
.
.
.
  v5 = VmbChannelAllocate (v3, 0i64 e vpciChannel);

 

 

 

Neste ponto, o canal foi alocado, mas ainda não pode ser usado, pois ele deve ser aberto primeiro. Um cliente VSC abre um canal oferecido com uma chamada para VmbChannelEnable.

 

 

 

O protótipo da função também está incluído no cabeçalho vmbuskernelmodeclientlibapi.h :

 

 

 

/// \ page VmbChannelEnable VmbChannelEnable
/// Ativa um canal que está no estado desativado conectando-se a vmbus e
/// oferecendo ou abrindo um canal (o que for apropriado para o terminal
/// tipo).
///
/// Veja \ ref state_model.
///
/// \ param Channel Uma alça para o canal. Alocado por \ ref VmbChannelAllocate.
_Must_inspect_result_
NTSTATUS
VmbChannelEnable (
    _In_ Canal VMBCHANNEL
    );

 

 

 

No Windows 10 Redstone 4 (1803) a chamada para VmbChannelEnableacontecer também em FdoCreateVmbusChannel. Depois disso, a referência ao canal é salva no contexto do FDO:

 

 

 

  v5 = VmbChannelEnable (vpciChannel);
  if (v5> = 0)
  {
    v1 [3] = vpciChannel;
    retornar 0i64;
  }

 

 

 

Envio de dados através do canal VMBus

 

 

 

Agora que entendemos como o VPCI configura seu canal VMBus, uma estratégia simples para obter uma referência e usá-lo para fuzzing é usar um driver de filtro superior para VPCI. 

Quando a pilha de dispositivos VPCI FDO é criada, nosso driver será chamado pelo gerenciador PnP. Nesse ponto, o canal VMBus já foi alocado e habilitado pelo FdoDeviceAdd e podemos acessá-lo através do Contexto VPCI FDO.

 

 

 

Vamos ver como fazer isso com um driver. O primeiro passo é fornecer um arquivo INF para instalar nosso driver de filtro para o dispositivo VPCI. As partes importantes do INF foram destacadas. Leve em conta que:

 

 

 

  • wvpci.inf é o INF para o driver VPCI.
  • O ID de hardware VPCI é VMBUS \ {44C4F61D-4444-4400-9D52-802E27EDE19F}

 

 

 

;
; BlogDriver.inf
;

[Versão]
Signature = "$ WINDOWS NT $"
Classe = sistema
ClassGuid = {4d36e97d-e325-11ce-bfc1-08002be10318}
Provedor =% ManufacturerName%
DriverVer =
CatalogFile = BlogDriver.cat

[DestinationDirs]
DefaultDestDir = 12

[SourceDisksNames]
1 =% DiskName% ,,, ""

[SourceDisksFiles]
BlogDriver.sys = 1

[Fabricante]
% ManufacturerName% = Standard, NT $ ARCH $

[Standard.NT $ ARCH $]
% BlogDriver.DeviceDesc% = Install_Section, VMBUS \ {44C4F61D-4444-4400-9D52-802E27EDE19F}

[Install_Section.NT]
Incluir = wvpci.inf
Necessidades = Vpci_Device_Child.NT
CopyFiles = BlogDriver_Files

[BlogDriver_Files]
BlogDriver.sys

[Install_Section.NT.HW]
Incluir = wvpci.inf
Necessidades = Vpci_Device_Child.NT.HW
AddReg = BlogDriver_AddReg

[BlogDriver_AddReg]
HKR ,, "UpperFilters", 0x00010000, "BlogDriver"

[Install_Section.NT.Services]
Incluir = wvpci.inf
Necessidades = Vpci_Device_Child.NT.Services
AddService = BlogDriver ,, BlogDriver_Service_Child

[BlogDriver_Service_Child]
DisplayName =% BlogDriver.SvcDesc%
ServiceType = 1; SERVICE_KERNEL_DRIVER
StartType = 3; SERVICE_DEMAND_START
ErrorControl = 1; SERVICE_ERROR_NORMAL
ServiceBinary =% 12% \ BlogDriver.sys

[Cordas]
ManufacturerName = "TestManufacturer"
ClassName = ""
DiskName = "Disco de origem do BlogDriver"
BlogDriver.DeviceDesc = "Barramento PCI Virtual do Microsoft Hyper-V (com Filtro)"
BlogDriver.SvcDesc = "Barramento PCI Virtual Microsoft Hyper-V (com filtro)"

 

 

 

Agora vamos ver o esqueleto inicial do driver de filtro. Alguns esclarecimentos primeiro:

 

 

 

  • AddDevicerotina cria o objeto de dispositivo de filtro e o anexa ao VPCI FDO. Uma referência ao canal VPCI VMBus é salva na extensão do dispositivo para facilitar o acesso.
  • Neste esqueleto todos os IRPs são apenas passados ​​através da pilha de dispositivos, não queremos modificar o comportamento de VPCI, apenas acessar seu canal VMBus.

 

 

 

O esqueleto completo pronto para construir e jogar pode ser encontrado neste repo . 
Depois de instalar o driver no guest, a pilha VPCI mostra nosso driver de filtro:

 

 

 

0: kd>! Devstack ffff8407f64cbad0
  ! DevObj! DrvObj! DevExt ObjectName
  ffff8407f2379de0 \ Driver \ BlogDriver ffff8407f2379f30  
> ffff8407f64cbad0 \ Driver \ vpci ffff8407fa4e42f0  
  ffff8407f62e1c90 \ Driver \ vmbus ffff8407f62e2310 00000024
! DevNode ffff8407f2fe26b0:
  DeviceInst é "VMBUS \ {44c4f61d-4444-4400-9d52-802e27ede19f} \ {7f7e8f36-7342-4531-a380-d3a9911f80bf}"
  ServiceName é "vpci"

 

 

 

Neste ponto, estamos prontos para enviar dados e fuzz através do canal. Existem várias APIs públicas disponíveis para enviar pacotes através de um canal VMBus. Um deles é VmbChannelSendSynchronousRequest. É uma das APIs usadas pelo VPCI e requer apenas uma referência ao VMBCHANNEL para começar a funcionar. A declaração está disponível no cabeçalho vmbuskernelmodeclientlibapi.h . Nós destacamos onde usar o VMBCHANNEL:

 

 

 

/// \ page VmbChannelSendSynchronousRequest VmbChannelSendSynchronousRequest
/// Envia um pacote para o terminal oposto e espera por uma resposta.
///
/// Os clientes podem ligar com qualquer combinação de parâmetros. A raiz só pode ligar
/// this if * Tempo limite == 0 e o \ ref VMBUS_CHANNEL_FORMAT_FLAG_WAIT_FOR_COMPLETION
/// flag não está definido.
///
/// \ param Channel Uma alça para o canal. Alocado por \ ref VmbChannelAllocate.
/// \ param Buffer Data to send.
/// \ param BufferSize Tamanho do buffer em bytes.
/// \ param ExternalDataMdl Opcionalmente, um MDL descrevendo um buffer adicional para
/// enviar.
/// \ param Flags Sinalizadores padrão.
/// \ param CompletionBuffer Buffer para armazenar os resultados do pacote de conclusão.
/// \ param CompletionBufferSize Tamanho do CompletionBuffer em bytes. Devemos ser
/// arredondado para os 8 bytes mais próximos, caso contrário a chamada falhará. No sucesso,
/// retorna o número de bytes escritos em CompletionBuffer.
/// \ param Timeout Opcionalmente, um tempo limite no estilo de KeWaitForSingleObject.
/// Após este tempo, o pacote será cancelado. Se definido para um
/// timeout de 0, este pacote não será enfileirado se não couber no
/// buffer de anel.
///
/// \ retorna STATUS_SUCCESS
/// \ retorna STATUS_BUFFER_OVERFLOW - O pacote não se encaixou no buffer e
/// não foi enfileirado.
/// \ retorna STATUS_CANCELLED - O pacote foi cancelado.
/// \ retorna STATUS_DEVICE_REMOVED - O canal está sendo desligado.
_When_ (Timeout == NULL || Timeout-> QuadPart! = 0 ||
       (Sinalizadores & VMBUS_CHANNEL_FORMAT_FLAG_WAIT_FOR_COMPLETION)! = 0,
       _IRQL_requires_ (PASSIVE_LEVEL))
_When_ (Tempo limite! = NULL && Tempo limite-> QuadPart == 0 &&
       (Sinalizadores & VMBUS_CHANNEL_FORMAT_FLAG_WAIT_FOR_COMPLETION) == 0,
        _IRQL_requires_max_ (DISPATCH_LEVEL))
NTSTATUS
VmbChannelSendSynchronousRequest (
    _In_ VMBCHANNEL Channel,
    _In_reads_bytes_ (BufferSize) Buffer PVOID,
    _In_ UINT32 BufferSize,
    _In_opt_ PMDL ExternalDataMdl,
    _In_ UINT32 Flags,
    _Out_writes_bytes_to_opt _ (* CompletionBufferSize, * CompletionBufferSize)
                                    PVOID CompletionBuffer,
    _Inout_opt_ _Pre_satisfies _ (* _ Curr_% 8 == 0)
                                    PUINT32 CompletionBufferSize,
    _In_opt_ PLARGE_INTEGER tempo limite
    );

 

 

 

Existem outras APIs publicamente disponíveis e documentadas em vmbuskernelmodeclientlibapi.h :

 

 

 

  • VmbPacketSend
  • VmbPacketSendWithExternalMdl
  • VmbPacketSendWithExternalPfns

 

 

 

Antes de usar qualquer um desses métodos no seu driver, lembre-se de fazer um link para o vmbkmcl.lib:

 

 

 

 

 

 

Procurar referências a esses métodos no VPCI pode ajudar a analisar e entender melhor as interações com o VSP. Outro recurso que pode ser útil para entender a comunicação é ler os Serviços de Integração do Linux . A implementação do cliente (VSC) para Linux pode ser encontrada em pci-hyperv.c.

 

 

 

Encontrando o ponto de entrada de dados não confiáveis ​​no VSP

 

 

 

Nesta seção, introduziremos o processamento de pacotes no lado do VSP. Usaremos o VPCI como exemplo para aprender como localizar o ponto de entrada para manipular pacotes VMBus de entrada. Não discutiremos os detalhes sobre as comunicações do Virtual PCI, mas está fora do escopo deste blog. Para esta análise, estamos usando vpcivsp.sys 10.0.17134.228.

 

 

 

Para qualquer terminal VMBus, os pacotes recebidos de um canal dispararão o EvtChannelProcessPacketretorno de chamada, conforme explicado na documentação disponível no cabeçalho vmbuskernelmodeclientlibapi.h :

 

 

 

/// \ page EvtVmbChannelProcessPacket EvtVmbChannelProcessPacket
/// \ b EvtVmbChannelProcessPacket
/// \ param Channel Uma alça para o canal. Alocado por \ ref VmbChannelAllocate.
/// \ param Packet Este contexto de conclusão será usado para identificar este pacote para o KMCL quando a transação puder ser retirada.
/// \ param Buffer Contém o pacote que foi enviado pelo endpoint oposto. Não contém os cabeçalhos VMBus e KMCL.
/// \ param BufferLength O tamanho do Buffer em bytes.
/// \ param Flags Consulte VMBUS_CHANNEL_PROCESS_PACKET_FLAGS.
/// 
/// Este retorno de chamada é invocado quando um pacote chega no buffer de toques de entrada.
/// Para cada invocação desta função, o implementador deve eventualmente chamar
/// \ ref VmbChannelPacketComplete.
///
/// Este retorno de chamada pode ser chamado em DISPATCH_LEVEL ou menos, a menos que o canal
/// foi configurado para adiar o processamento de pacotes para um segmento de trabalho. Vejo
/// \ ref VmbChannelSetIncomingProcessingAtPassive para mais informações.
///\código
typedef
_Function_class_ (EVT_VMB_CHANNEL_PROCESS_PACKET)
_IRQL_requires_max_ (DISPATCH_LEVEL)
VAZIO
EVT_VMB_CHANNEL_PROCESS_PACKET (
    _In_ VMBCHANNEL Channel,
    _In_ VMBPACKETCOMPLETION Packet,
    _In_reads_bytes_ (BufferLength) Buffer PVOID,
    _In_ UINT32 BufferLength,
    _In_ UINT32 sinalizadores
    );

 

 

 

O retorno de chamada para o processamento do método é definido com uma chamada para VmbChannelInitSetProcessPacketCallbacks. Também é declarado em vmbuskernelmodeclientlibapi.h :

 

 

 

/// \ page VmbChannelInitSetProcessPacketCallbacks VmbChannelInitSetProcessPacketCallbacks
/// Define retornos de chamada para o processamento de pacotes. Somente significativo se a fila KMCL
/// gerenciamento não é suprimido. TODO: Tornar a sentença anterior mais precisa.
///
/// Note que ProcessPacketCallback será invocado para cada pacote que
/// é recebido. ProcessingCompleteCallback será invocado toda vez que o
/// buffer de anel contendo transições de pacotes recebidos de não vazios para vazios,
/// após a última invocação de ProcessPacketCallback em um único lote.
///
/// \ param Channel Uma alça para o canal. Alocado por \ ref VmbChannelAllocate.
/// \ param ProcessPacketCallback Um retorno de chamada que será chamado quando um pacote for
/// pronto para processamento.
/// \ param ProcessingCompleteCallback Opcionalmente, um retorno de chamada que será chamado
/// quando o processamento de um lote de pacotes foi concluído.
///
/// \ return STATUS_SUCCESS - função completada com sucesso
/// \ return STATUS_INVALID_PARAMETER_1 - o parâmetro do canal era inválido ou estava em um estado inválido (desativado)
NTSTATUS
VmbChannelInitSetProcessPacketCallbacks (
    _In_ VMBCHANNEL Channel,
    _In_ PFN_VMB_CHANNEL_PROCESS_PACKET ProcessPacketCallback,
    _In_opt_ PFN_VMB_CHANNEL_PROCESSING_COMPLETE ProcessingCompleteCallback
    );

 

 

 

Com as informações acima, o método de processamento de pacotes para o VPCI VSP pode ser encontrado facilmente. Em vpcivsp.sys apenas procure por referências a VmbChannelInitSetProcessPacketCallbacks. O método de processamento é VirtualBusChannelProcessPacket:

 

 

 

 

 

 

A análise do processamento de pacotes está fora do escopo do blog, mas esperamos que as dicas iniciais tenham sido fornecidas para pesquisadores dispostos a investir nessa área.

 

 

 

Resultados de difusão. Um exemplo – CVE-2018-0965

 

 

 

Com a abordagem explicada acima, desenvolvemos um fuzzer para direcionar o processamento de pacotes no VPCI. Nesta seção vamos analisar um dos bugs atingidos pelo fuzzer que foi recentemente corrigido e aprender o tipo de problemas que podem ser encontrados envolvendo a comunicação entre partições através dos canais VMBus. 

O CVE-2018-0965 é um RCE pertencente ao Nível 1 do Programa de Recompensas do Hyper-V . A referência à assessoria oficial .

 

 

 

O bug vivido no método de processamento de pacotes para o VPCI VSP. Por diffing ( diáfora foi usado) contra os vpcivsp.sys patched (10.0.17134.285) do método VirtualBusChannelProcessPacketpode ser identificados como modificado:

 

 

 

 

 

 

Ao olhar as mudanças no VirtualBusChannelProcessPacketinteressante é encontrado:

 

 

 

 

 

 

A chamada para VirtualBusLookupDevicefoi movida de fora de uma condição para a ramificação interna. Vamos rever o código vulnerável com mais contexto. Primeiro, o código interessante:

 

 

 

void __fastcall VirtualBusChannelProcessPacket (__ int64 a1, __int64 a2, __int64 a3, não assinado int a4)
{
  Unsigned int v4; // er15
  __int64 v5; // rsi
  __int64 v7; // rax
  struct _KEVENT * v11; // rbx
  int v12; // edi
  unsigned int v13; // ecx
  .
  .
  .
  v4 = a4;
  v5 = a3;
  v13 = * (_ DWORD *) v5;
  v7 = VmbChannelGetPointer (a1);
  v11 = (struct _KEVENT *) v7;
  .
  .
  .
  if (v13 == 1112080407)
  {
    if (v11 [3] .Header.SignalState <0x10002u)
    {
      v36 = 54;
    }
    outro
    {
      if (v4 <0x50)
      {
        v12 = -1073741789;
        v14 = 53;
        ir para LABEL_26;
      }
      v45 = VirtualBusLookupDevice (v11, * (_ DWORD *) (v5 + 4));
      v46 = (volátil assinado __int32 *) v45;
      if (! v45)
      {
        v41 = 57;
        goto LABEL_71;
      }
      if (* (_ WORD *) (v5 + 12) <= 0x20u)
      {
        v47 = VirtualDeviceCreateSingleInterrupt (v45, v5 e v69);
        memset (& v73, 0, 0x50ui64);
        ...
        v73 = v47;
        ...
        VmbChannelPacketComplete (v6, & v73, 80i64);
        v34 = v46;
        goto LABEL_50;
      }
      v36 = 56;
    }
  }
.
.
.
  Retorna;

LABEL_50:
  VirtualDeviceDereference (v34, v32, v33);
  Retorna;
}

 

 

 

Agora vamos recuperar a definição do processamento de pacotes callback ( EvtVmbChannelProcessPacket) do cabeçalho público e reescrever o código acima com argumentos nomeados:

 

 

 

void __fastcall VirtualBusChannelProcessPacket (Canal VMBCHANNEL, Pacote VMBPACKETCOMPLETION, Buffer PVOID,
                                               UINT32 BufferLength, UINT32 Flags)
{
  Unsigned int v4; // er15
  __int64 v5; // rsi
  __int64 v7; // rax
  struct _KEVENT * v11; // rbx
  int v12; // edi
  unsigned int v13; // ecx
.
.
.
  v4 = BufferLength;
  v5 = Buffer;
  v13 = * (_ DWORD *) v5;
  v7 = VmbChannelGetPointer (canal);
  v11 = (struct _KEVENT *) v7;
.
.
.
  if (v13 == 1112080407)
  {
    if (v11 [3] .Header.SignalState <0x10002u)
    {
      v36 = 54;
    }
    outro
    {
      if (v4 <0x50)
      {
        v12 = -1073741789;
        v14 = 53;
        ir para LABEL_26;
      }
      v45 = VirtualBusLookupDevice (v11, * (_ DWORD *) (v5 + 4));
      v46 = (volátil assinado __int32 *) v45;
      if (! v45)
      {
        v41 = 57;
        goto LABEL_71;
      }
      if (* (_ WORD *) (v5 + 12) <= 0x20u)
      {
        v47 = VirtualDeviceCreateSingleInterrupt (v45, v5 e v69);
        memset (& v73, 0, 0x50ui64);
        ...
        v73 = v47;
        ...
        VmbChannelPacketComplete (v6, & v73, 80i64);
        v34 = v46;
        goto LABEL_50;
      }
      v36 = 56;
    }
  }
.
.
.
  Retorna;
.
.
.
LABEL_50:
  VirtualDeviceDereference (v34, v32, v33);
  Retorna;
}

 

 

 

Vale a pena esclarecer que o terceiro parâmetro, Buffer, aponta para os dados controlados pelo invasor vindos do canal VPCI. O quarto parâmetro, BufferLength, é o tamanho do Buffer em bytes. 

A variável local identificada como v13 é atribuída a partir da primeira DWORD do PacketBuf e posteriormente comparada com a constante 1112080407 (0x42490017). Observando o código do Linux Integration Services, a constante pode ser facilmente identificada como PCI_CREATE_INTERRUPT_MESSAGE2. Isso significa que PacketBuf, neste caso, está apontando para uma pci_create_interrupt2estrutura:

 

 

 

struct pci_message {
  tipo u32;
} __packed;

/ *
 * Os números de função são de 8 bits no Express, conforme interpretado por ARI,
 * que é tudo isso motorista faz. Esta representação é aquela usada em
 * Windows, que é o que se espera ao enviar este e para trás com
 * a partição pai do Hyper-V.
 * /
união win_slot_encoding {
  struct {
    u32 dev: 5;
    u32 func: 3;
    u32 reservado: 24;
  } bits;
  slot u32;
} __packed;

/ **
 * struct hv_msi_desc2 - versão 1.2 do hv_msi_desc
 * @vector: entrada do IDT
 * @delivery_mode: Conforme definido no Programmer's da Intel
 * Manual de Referência, Volume 3, Capítulo 8.
 * @vector_count: Número de entradas contíguas no
 * Tabela de Descritores de Interrupções que são
 * ocupado por esta mensagem sinalizada
 * Interromper Para "MSI", como definido pela primeira vez
 * no PCI 2.2, isso pode ser entre 1 e
 * 32. Para "MSI-X", conforme definido pela primeira vez em PCI
 * 3.0, este deve ser 1, como cada tabela MSI-X
 * entrada teria seu próprio descritor.
 * @processor_count: número de bits habilitados na matriz.
 * @processor_array: todos os processadores virtuais de destino.
 * /
struct hv_msi_desc2 {
  vetor u8;
  u8 delivery_mode;
  u16 vector_count;
  u16 processor_count;
  u16 processor_array [32];
} __packed;

struct pci_create_interrupt2 {
  struct pci_message message_type;
  união win_slot_encoding wslot;
  struct hv_msi_desc2 int_desc;
} __packed;

 

 

 

Nos permite escrever o código vulnerável novamente com mais informações:

 

 

 

void __fastcall VirtualBusChannelProcessPacket (Canal VMBCHANNEL, Pacote VMBPACKETCOMPLETION, Buffer PVOID,
                                               UINT32 BufferLength, UINT32 Flags)
{
  Unsigned int v4; // er15
  pci_ceate_interrupt2 * createInterrupt; // rsi
  __int64 v7; // rax
  struct _KEVENT * v11; // rbx
  int v12; // edi
  unsigned int messageType; // ecx
.
.
.
  v4 = BufferLength;
  createInterrupt = Buffer;
  messageType = createInterrupt-> message_type.type;
  v7 = VmbChannelGetPointer (canal);
  v11 = (struct _KEVENT *) v7; // Parece que a análise da IDA não entendeu a v7.
.
.
.
  if (messageType == PCI_CREATE_INTERRUPT_MESSAGE2)
  {
    if (v11 [3] .Header.SignalState <0x10002u) // Parece que a análise do IDA não entendeu a v7 / v11.
    {
      v36 = 54;
    }
    outro
    {
      if (v4 wslot.slot); 
      v46 = (volátil assinado __int32 *) v45;
      if (! v45)
      {
        v41 = 57;
        goto LABEL_71;
      }
      if (createInterrupt-> int_desc.processor_count <= 0x20u)
      {
        v47 = VirtualDeviceCreateSingleInterrupt (v45, createInterrupt, & v69);
        memset (& v73, 0, 0x50ui64);
        ...
        v73 = v47;
        ...
        VmbChannelPacketComplete (v6, & v73, 80i64);
        v34 = v46;
        goto LABEL_50;
      }
      v36 = 56;
    }
  }
.
.
.
  Retorna;
.
.
.
LABEL_50:
  VirtualDeviceDereference (v34, v32, v33);
  Retorna;
}

 

 

 

Como resumo, na versão vulnerável, um PCI_CREATE_INTERRUPT_MESSAGE2pacote com um valor de processor_count maior que 0x20 pode forçar um fluxo para o qual VirtualBusLookupDeviceé chamado mas, depois de falhar na condição, retorna sem chamar VirtualDeviceDereference
Vamos verificar ambos VirtualBusLookupDeviceVirtualBusDereferencena versão vulnerável do vpcivsp.sys. Começando com VirtualBusLookupDevice:

 

 

 

assinado __int64 __fastcall VirtualBusLookupDevice (struct _KEVENT * a1, int a2)
{
  struct _KEVENT * v2; // rsi
  int v3; // ebp
  struct _KEVENT * v4; // rbx
  char v5; // di
  assinado __int64 v6; // rcx
  _LIST_ENTRY * i; // rax
  assinado __int64 v8; // rbx

  v2 = a1 + 2;
  v3 = a2;
  v4 = a1;
  v5 = 0;
  KeWaitForSingleObject (& a1 [2], 0, 0, 0, 0i64);
  v6 = (assinado __int64) & v4 [1] .Header.WaitListHead;
  para (i = v4 [1] .Header.WaitListHead.Flink;; i = i-> Flink)
  {
    v8 = (assinado __int64) & i [-12] .Blink;
    if (i == (_LIST_ENTRY *) v6)
      pausa;
    if (* (_ DWORD *) (v8 + 408) == v3 && (* (_ DWORD *) (v8 + 1820) e 0x80u)! = 0)
    {
      _InterlockedIncrement ((volátil assinado __int32 *) (v8 + 200));
      v5 = 1;
      pausa;
    }
  }
  KeSetEvent (v2, 0, 0);
  return v8 & - (assinado __int64) (v5! = 0);
}

 

 

 

Sabemos, da análise anterior, que:

 

 

 

  • O segundo argumento é o slot do dispositivo.
  • O primeiro argumento foi mal entendido como um _KEVENT. Aponta para um objeto que foi salvo no contexto do canal. Muito provavelmente um mais complexo, que contém _KEVENTum campo.

 

 

 

Vamos analisar o código novamente depois de renomear:

 

 

 

__int64 __fastcall chamado VirtualBusLookupDevice (__ int64 a1, int slot)
{
  struct _KEVENT * v2; // rsi
  int v3; // ebp
  __int64 v4; // rbx
  char v5; // di
  assinado __int64 v6; // rcx
  _QWORD * i; // rax
  assinado __int64 v8; // rbx

  v2 = (struct _KEVENT *) (a1 + 48);
  v3 = slot;
  v4 = a1;
  v5 = 0;
  KeWaitForSingleObject ((PVOID) (a1 + 48), 0, 0, 0, 0i64);
  v6 = v4 + 32;
  para (i = * (_ QWORD **) (v4 + 32);; i = (_QWORD *) * i)
  {
    v8 = (assinado __int64) (i - 23);
    if (i == (_QWORD *) v6)
      pausa;
    if (* (_ DWORD *) (v8 + 408) == v3 && (* (_ DWORD *) (v8 + 1820) e 0x80u)! = 0)
    {
      _InterlockedIncrement ((volátil assinado __int32 *) (v8 + 200));
      v5 = 1;
      pausa;
    }
  }
  KeSetEvent (v2, 0, 0);
  return v8 & - (assinado __int64) (v5! = 0);
}

 

 

 

  • O método funciona com o objeto apontado pelo primeiro argumento. Dado o nome do método VirtualBusLookupDevice, podemos imaginar que é o barramento virtual.
  • Um _KEVENTdentro do barramento virtual é usado para sincronização.
  • Um contêiner é armazenado no deslocamento 32 do objeto de barramento virtual.
  • O loop principal é iterar sobre o contêiner, provavelmente uma lista.
  • Dentro do loop, a v8 mantém a referência para todos os objetos dentro do contêiner.
  • O campo no deslocamento 408 é comparado com o id do slot. O palpite é que estamos interagindo com uma lista de dispositivos.
  • Se um dispositivo correspondente for encontrado, seu campo no deslocamento 200 será incrementado e uma referência será retornada. O campo no deslocamento 200 se parece com uma contagem de referência e um campo de tamanho de 32 bits.

 

 

 

Vamos para VirtualDeviceDereferenceagora. Como lembrete, o primeiro argumento é o ponteiro retornado por VirtualBusLookupDevice(mais provavelmente um dispositivo):

 

 

 

 

 

 

Na desmontagem acima, VirtualDeviceDereferencedecrementa o campo no deslocamento 200 (identificado como uma contagem de referência potencial antes). Se a contagem de referência chegar a 0 VirtualDeviceDestroy, o dispositivo será liberado:

 

 

 

void __fastcall VirtualDeviceDestroy (PVOID P, __int64 a2, __int64 a3)
{
  char * v3; // rbx


  v3 = (char *) P;
  //
  // Muitas coisas...
  //
  ExFreePoolWithTag (v3, 0x49435056u);
}

 

 

 

Para resumir. Enviando pacotes PCI_CREATE_INTERRUPT_MESSAGE2, com um processor_count maior que 0x20, a contagem de referência do dispositivo pode ser excedida e o objeto do dispositivo liberado inesperadamente, levando a uma situação perigosa se referências pendentes ao dispositivo forem deixadas… mas isso é uma história para outro blog 😊

 Este artigo é uma tradução livre de: https://blogs.technet.microsoft.com/srd/2019/01/28/fuzzing-para-virtualized-devices-in-hyper-v