Verificação do FreeRDP usando o analisador PVS-Studio

Verificação do FreeRDP usando o analisador PVS-Studio
FreeRDP – uma implementação aberta do Remote Desktop Protocol (RDP), um protocolo que permite o controle remoto de um computador, desenvolvido pela Microsoft. O projeto suporta várias plataformas, incluindo Windows, Linux, macOS e até mesmo iOS e Android. Este projeto foi o escolhido como o primeiro em uma série de artigos dedicados à análise de clientes RDP usando o analisador estático PVS-Studio.

Um pouco de história

Projeto FreeRDP surgiu após a Microsoft abrir as especificações de seu protocolo proprietário RDP. Naquela época, existia o cliente rdesktop, cuja implementação é baseada em resultados de engenharia reversa.

Durante a implementação do protocolo, tornou-se cada vez mais difícil adicionar novas funcionalidades devido à arquitetura existente do projeto. Mudanças nela geraram conflitos entre os desenvolvedores, levando à criação do fork rdesktop – FreeRDP. A disseminação posterior do produto foi limitada pela licença GPLv2, motivo pelo qual foi tomada a decisão de relicenciar sob a Apache License v2. No entanto, nem todos concordaram em mudar a licença de seu código, então os desenvolvedores decidiram reescrever o projeto, resultando na base de código moderna que temos hoje.

Para mais detalhes sobre a história do projeto, você pode ler a nota no blog oficial: "A história do projeto FreeRDP".

Como ferramenta para identificar erros e potenciais vulnerabilidades no código, foi utilizado PVS-Studio. Este é um analisador estático de código para as linguagens C, C++, C# e Java, disponível nas plataformas Windows, Linux e macOS.

Neste artigo, são apresentados apenas os erros que me pareceram mais interessantes.

Vazamento de memória

V773 A função foi encerrada sem liberar o ponteiro ‘cwd’. Um vazamento de memória é possível. environment.c 84

DWORD GetCurrentDirectoryA(DWORD nBufferLength, LPSTR lpBuffer)
{
  char* cwd;
  ....
  cwd = getcwd(NULL, 0);
  ....
  if (lpBuffer == NULL)
  {
    free(cwd);
    return 0;
  }

  if ((length + 1) > nBufferLength)
  {
    free(cwd);
    return (DWORD) (length + 1);
  }

  memcpy(lpBuffer, cwd, length + 1);
  return length;
  ....
}

Este trecho foi retirado do subsistema winpr, que implementa uma camada de abstração WINAPI para sistemas não-Windows, ou seja, é uma leve analogia ao Wine. Aqui, podemos notar o vazamento: a memória alocada pela função getcwd, é liberada apenas ao lidar com casos especiais. Para corrigir o erro, é necessário adicionar uma chamada de free após memcpy.

Acesso fora dos limites do array

V557 O acesso ao array pode estar fora dos limites. O valor do índice ‘event->EventHandlerCount’ pode chegar a 32. PubSub.c 117

#define MAX_EVENT_HANDLERS  32

struct _wEventType
{
  ....
  int EventHandlerCount;
  pEventHandler EventHandlers[MAX_EVENT_HANDLERS];
};

int PubSub_Subscribe(wPubSub* pubSub, const char* EventName,
      pEventHandler EventHandler)
{
  ....
  if (event->EventHandlerCount <= MAX_EVENT_HANDLERS)
  {
    event->EventHandlers[event->EventHandlerCount] = EventHandler;
    event->EventHandlerCount++;
  }
  ....
}

Neste exemplo, um novo elemento é adicionado à lista, mesmo que o número de elementos tenha atingido o máximo. Basta substituir o operador <= para <, para não ultrapassar os limites da matriz.

Foi encontrada outra ocorrência desse tipo de erro:

  • V557 O estouro do array é possível. O valor do índice ‘iBitmapFormat’ pode chegar a 8. orders.c 2623

Erros de digitação

Fragmento 1

V547 A expressão ‘!pipe->In’ é sempre falsa. MessagePipe.c 63

wMessagePipe* MessagePipe_New()
{
  ....
  pipe->In = MessageQueue_New(NULL);
  if (!pipe->In)
    goto error_in;

  pipe->Out = MessageQueue_New(NULL);
  if (!pipe->In) // <=
    goto error_out;
  ....
}

Aqui vemos um erro de digitação comum: na segunda condição, está sendo feita a verificação da mesma variável que na primeira. Provavelmente, o erro foi causado por uma cópia de código mal sucedida.

Fragmento 2

V760 Foram encontrados dois blocos de texto idênticos. O segundo bloco começa na linha 771. tsg.c 770

typedef struct _TSG_PACKET_VERSIONCAPS
{
  ....
  UINT16 majorVersion;
  UINT16 minorVersion;
  ....
} TSG_PACKET_VERSIONCAPS, *PTSG_PACKET_VERSIONCAPS;

static BOOL TsProxyCreateTunnelReadResponse(....)
{
  ....
  PTSG_PACKET_VERSIONCAPS versionCaps = NULL;
  ....
  /* MajorVersion (2 bytes) */
  Stream_Read_UINT16(pdu->s, versionCaps->majorVersion);
  /* MinorVersion (2 bytes) */
  Stream_Read_UINT16(pdu->s, versionCaps->majorVersion);
  ....
}

Mais um erro de digitação: o comentário no código sugere que deveria chegar do fluxo minorVersion, porém a leitura está acontecendo na variável com o nome majorVersion. No entanto, não estou familiarizado com o protocolo, então isso é apenas uma suposição.

Fragmento 3

V524 É estranho que o corpo da função ‘trio_index_last’ seja totalmente equivalente ao corpo da função ‘trio_index’. triostr.c 933

/**
   Find first occurrence of a character in a string.
   ....
 */
TRIO_PUBLIC_STRING char *
trio_index
TRIO_ARGS2((string, character),
     TRIO_CONST char *string,
     int character)
{
  assert(string);
  return strchr(string, character);
}

/**
   Find last occurrence of a character in a string.
   ....
 */
TRIO_PUBLIC_STRING char *
trio_index_last
TRIO_ARGS2((string, character),
     TRIO_CONST char *string,
     int character)
{
  assert(string);
  return strchr(string, character);
}

De acordo com o comentário, a função trio_index encontra a primeira ocorrência de um caractere na string, enquanto trio_index_last encontra a última. Mas os corpos dessas funções são idênticos! Provavelmente, isso é um erro de digitação, e a função trio_index_last deveria usar strrchr em vez de strchr. Então, o comportamento será o esperado.

Fragmento 4

V769 O ponteiro ‘data’ na expressão é igual a nullptr. O valor resultante das operações aritméticas sobre esse ponteiro é sem sentido e não deve ser usado. nsc_encode.c 124

static BOOL nsc_encode_argb_to_aycocg(NSC_CONTEXT* context,
                                      const BYTE* data,
                                      UINT32 scanline)
{
  ....
  if (!context || data || (scanline == 0))
    return FALSE;
  ....
  src = data + (context->height - 1 - y) * scanline;
  ....
}

Parece que aqui o operador de negação foi acidentalmente esquecido ! perto de data. Estranho que isso não tenha sido notado.

Fragmento 5

V517 O padrão ‘if (A) {…} else if (A) {…}’ foi detectado. Há probabilidade de presença de erro lógico. Verifique as linhas: 213, 222. rdpei_common.c 213

BOOL rdpei_write_4byte_unsigned(wStream* s, UINT32 value)
{
  BYTE byte;

  if (value <= 0x3F)
  {
    ....
  }
  else if (value <= 0x3FFF)
  {
    ....
  }
  else if (value > 16) & 0x3F;
    Stream_Write_UINT8(s, byte | 0x80);
    byte = (value >> 8) & 0xFF;
    Stream_Write_UINT8(s, byte);
    byte = (value & 0xFF);
    Stream_Write_UINT8(s, byte);
  }
  else if (value > 24) & 0x3F;
    Stream_Write_UINT8(s, byte | 0xC0);
    byte = (value >> 16) & 0xFF;
    Stream_Write_UINT8(s, byte);
    byte = (value >> 8) & 0xFF;
    Stream_Write_UINT8(s, byte);
    byte = (value & 0xFF);
    Stream_Write_UINT8(s, byte);
  }
  ....
}

As duas últimas condições são idênticas: aparentemente, alguém esqueceu de verificá-las após a cópia. Pelo código, é evidente que a última parte trabalha com valores de quatro bytes, então pode-se supor que a última condição deveria ser value <= 0x3FFFFFFF.

Foi encontrada outra ocorrência desse tipo de erro:

  • V517 O padrão 'if (A) {...} else if (A) {...}' foi detectado. Há uma probabilidade de erro lógico presente. Verifique as linhas: 169, 173. file.c 169

Verificação de dados de entrada

Fragmento 1

V547 A expressão 'strcat(target, source) != NULL' é sempre verdadeira. triostr.c 425

TRIO_PUBLIC_STRING int
trio_append
TRIO_ARGS2((target, source),
     char *target,
     TRIO_CONST char *source)
{
  assert(target);
  assert(source);
  
  return (strcat(target, source) != NULL);
}

A verificação do resultado da execução da função neste exemplo é incorreta. A função strcat retorna um ponteiro para a versão final da string, ou seja, o primeiro parâmetro passado. Neste caso, é alvo. No entanto, se ele for igual a NULL, então a verificação é tardia, pois na função strcat ocorrerá a desreferenciação.

Fragmento 2

V547 A expressão 'cache' é sempre verdadeira. glyph.c 730

typedef struct rdp_glyph_cache rdpGlyphCache;

struct rdp_glyph_cache
{
  ....
  GLYPH_CACHE glyphCache[10];
  ....
};

void glyph_cache_free(rdpGlyphCache* glyphCache)
{
  ....
  GLYPH_CACHE* cache = glyphCache->glyphCache;

  if (cache)
  {
    ....
  }
  ....
}

Neste caso, a variável cache recebe o endereço de um array estático glyphCache->glyphCache. Portanto, a verificação if (cache) pode ser descartada.

Erro de gerenciamento de recursos

V1005 O recurso foi adquirido usando a função 'CreateFileA' mas foi liberado utilizando a função incompatível 'fclose'. certificate.c 447

BOOL certificate_data_replace(rdpCertificateStore* certificate_store,
                              rdpCertificateData* certificate_data)
{
  HANDLE fp;
  ....
  fp = CreateFileA(certificate_store->file, GENERIC_READ | GENERIC_WRITE, 0,
                   NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  ....
  if (size < 1)
  {
    CloseHandle(fp);
    return FALSE;
  }
  ....
  if (!data)
  {
    fclose(fp);
    return FALSE;
  }
  ....
}

O descritor de arquivo fp, criado pela chamada da função CreateFile, foi erroneamente fechado pela função fclose da biblioteca padrão, e não CloseHandle..

Condições idênticas

V581 As expressões condicionais das instruções 'if' situadas lado a lado são idênticas. Verifique as linhas: 269, 283. ndr_structure.c 283

void NdrComplexStructBufferSize(PMIDL_STUB_MESSAGE pStubMsg,
 unsigned char* pMemory, PFORMAT_STRING pFormat)
{
 ....
 if (conformant_array_description)
 {
 ULONG size;
 unsigned char array_type;
 array_type = conformant_array_description[0];
 size = NdrComplexStructMemberSize(pStubMsg, pFormat);
 WLog_ERR(TAG, "aviso: NdrComplexStructBufferSize array_type: "
 "0xX não implementado", array_type);
 NdrpComputeConformance(pStubMsg, pMemory + size,
 conformant_array_description);
 NdrpComputeVariance(pStubMsg, pMemory + size,
 conformant_array_description);
 MaxCount = pStubMsg->MaxCount;
 ActualCount = pStubMsg->ActualCount;
 Offset = pStubMsg->Offset;
 }

 if (conformant_array_description)
 {
 unsigned char array_type;
 array_type = conformant_array_description[0];
 pStubMsg->MaxCount = MaxCount;
 pStubMsg->ActualCount = ActualCount;
 pStubMsg->Offset = Offset;
 WLog_ERR(TAG, "aviso: NdrComplexStructBufferSize array_type: "
 "0xX não implementado", array_type);
 }
 ....
}

Talvez este exemplo não seja um erro. No entanto, ambas as condições contêm mensagens idênticas, uma das quais provavelmente pode ser removida.

Limpeza de ponteiros nulos

V575 O ponteiro nulo é passado para a função 'free'. Inspecione o primeiro argumento. smartcard_pcsc.c 875

WINSCARDAPI LONG WINAPI PCSC_SCardListReadersW(
  SCARDCONTEXT hContext,
  LPCWSTR mszGroups,
  LPWSTR mszReaders,
  LPDWORD pcchReaders)
{
  LPSTR mszGroupsA = NULL;
  ....
  mszGroups = NULL; /* mszGroups não é suportado pelo pcsc-lite */

  if (mszGroups)
    ConvertFromUnicode(CP_UTF8,0, mszGroups, -1, 
                       (char**) &mszGroupsA, 0,
                       NULL, NULL);

  status = PCSC_SCardListReaders_Internal(hContext, mszGroupsA,
                                          (LPSTR) &mszReadersA,
                                          pcchReaders);

  if (status == SCARD_S_SUCCESS)
  {
    ....
  }

  free(mszGroupsA);
  ....
}

Na função free é possível passar um ponteiro nulo e o analisador sabe disso. Mas se uma situação se revela onde o ponteiro é sempre passado como nulo, como neste fragmento, um aviso será emitido.

Ponteiro mszGroupsA inicialmente é igual a NULL e não é inicializado em nenhum outro lugar. O único ramo de código onde o ponteiro poderia ser inicializado é inatingível.

Houve outras mensagens desse tipo:

  • V575 O ponteiro nulo é passado para a função 'free'. Inspecione o primeiro argumento. license.c 790
  • V575 O ponteiro nulo é passado para a função 'free'. Inspecione o primeiro argumento. rdpsnd_alsa.c 575

É provável que essas variáveis esquecidas surjam durante o processo de refatoração e podem ser simplesmente removidas.

Possível estouro

V1028 Possível estouro. Considere fazer o cast dos operandos, e não do resultado. makecert.c 1087

// openssl/x509.h
ASN1_TIME *X509_gmtime_adj(ASN1_TIME *s, long adj);

struct _MAKECERT_CONTEXT
{
  ....
  int duration_years;
  int duration_months;
};

typedef struct _MAKECERT_CONTEXT MAKECERT_CONTEXT;

int makecert_context_process(MAKECERT_CONTEXT* context, ....)
{
  ....
  if (context->duration_months)
    X509_gmtime_adj(after, (long)(60 * 60 * 24 * 31 *
      context->duration_months));
  else if (context->duration_years)
    X509_gmtime_adj(after, (long)(60 * 60 * 24 * 365 *
      context->duration_years));
  ....
}

Fazer o cast do resultado para long não é uma proteção contra estouro, uma vez que o próprio cálculo ocorre usando o tipo int.

Desreferenciamento de ponteiro na inicialização

V595 O ponteiro 'context' foi utilizado antes de ser verificado contra nullptr. Verifique as linhas: 746, 748. gfx.c 746

static UINT gdi_SurfaceCommand(RdpgfxClientContext* context,
                               const RDPGFX_SURFACE_COMMAND* cmd)
{
  ....
  rdpGdi* gdi = (rdpGdi*) context->custom;

  if (!context || !cmd)
    return ERROR_INVALID_PARAMETER;
  ....
}

Aqui está o ponteiro contexto é desreferenciado na inicialização — antes de sua verificação.

Foram encontrados outros erros desse tipo:

  • V595 O ponteiro ‘ntlm’ foi utilizado antes de ser verificado contra nullptr. Verifique as linhas: 236, 255. ntlm.c 236
  • V595 O ponteiro ‘contexto’ foi utilizado antes de ser verificado contra nullptr. Verifique as linhas: 1003, 1007. rfx.c 1003
  • V595 O ponteiro ‘rdpei’ foi utilizado antes de ser verificado contra nullptr. Verifique as linhas: 176, 180. rdpei_main.c 176
  • V595 O ponteiro ‘gdi’ foi utilizado antes de ser verificado contra nullptr. Verifique as linhas: 121, 123. xf_gfx.c 121

Condição sem sentido

V547 A expressão ‘rdp->state >= CONNECTION_STATE_ACTIVE’ é sempre verdadeira. connection.c 1489

int rdp_server_transition_to_state(rdpRdp* rdp, int state)
{
  ....
  switch (state)
  {
    ....
    case CONNECTION_STATE_ACTIVE:
      rdp->state = CONNECTION_STATE_ACTIVE;          // state >= CONNECTION_STATE_ACTIVE)     // Activate, client->activated, client);

        if (!client->activated)
          return -1;
      }
    ....
  }
  ....
}

É fácil notar que a primeira condição não faz sentido devido à atribuição do valor correspondente anteriormente.

Análise incorreta da string

V576 Formato incorreto. Considere verificar o terceiro argumento real da função ‘sscanf’. Um ponteiro para o tipo unsigned int é esperado. proxy.c 220

V560 Uma parte da expressão condicional é sempre verdadeira: (rc >= 0). proxy.c 222

static BOOL check_no_proxy(....)
{
  ....
  int sub;
  int rc = sscanf(range, "%u", &sub);

  if ((rc == 1) && (rc >= 0))
  {
    ....
  }
  ....
}

O analisador para este fragmento gera imediatamente 2 advertências. O especificador %u espera uma variável do tipo unsigned int, mas a variável sub tem tipo int. Vemos a verificação suspeita: a condição à direita não faz sentido, pois no início é uma comparação com um.

Verificações desordenadas

V547 A expressão ‘status == 0x00090314’ é sempre falsa. ntlm.c 299

BOOL ntlm_authenticate(rdpNtlm* ntlm, BOOL* pbContinueNeeded)
{
  ....
  if (status != SEC_E_OK)
  {
    ....
    return FALSE;
  }

  if (status == SEC_I_COMPLETE_NEEDED)            // <=
    status = SEC_E_OK;
  else if (status == SEC_I_COMPLETE_AND_CONTINUE) // <=
    status = SEC_I_CONTINUE_NEEDED;
  ....
}

As condições marcadas sempre serão falsas, pois a execução chegará à segunda condição apenas quando status == SEC_E_OK. O código correto pode ser assim:

if (status == SEC_I_COMPLETE_NEEDED)
  status = SEC_E_OK;
else if (status == SEC_I_COMPLETE_AND_CONTINUE)
  status = SEC_I_CONTINUE_NEEDED;
else if (status != SEC_E_OK)
{
  ....
  return FALSE;
}

Conclusão

Assim, a verificação do projeto identificou vários problemas, mas apenas a parte mais interessante deles foi descrita no artigo. Os desenvolvedores do projeto podem verificar o projeto por conta própria, solicitando uma chave de licença temporária no site. PVS-Studio. Também houve falsos positivos, trabalhar nos quais ajudará a melhorar o analisador. No entanto, a análise estática é importante se você deseja não apenas aumentar a qualidade do código, mas também reduzir o tempo de busca por erros, e o PVS-Studio pode ajudar nisso.

Verificação do FreeRDP usando o analisador PVS-Studio

Se você quiser compartilhar este artigo com um público de língua inglesa, por favor, use o link para a tradução: Sergey Larin. Verificando o FreeRDP com o PVS-Studio

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster