Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
regreSSHion — Este é um POC que escrevi para a CVE-2024-6387 | Kitploit
Ferramentas/GitHubGitHub/teamos-hub/regresshion
Análise de VulnerabilidadesExploraçãoTestes de PenetraçãoRed TeamingFerramenta de Acesso RemotoExploração de Binários
GitHubteamos-hub/regresshion

regreSSHion

Este é um POC que escrevi para a CVE-2024-6387

Ver Repositório
1há 2 anosAinda não revisado

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar

Qualys Security Advisory

regreSSHion: RCE no servidor do OpenSSH, em sistemas Linux baseados em glibc (CVE-2024-6387)

======================================================================== Conteúdo

Resumo SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3 (Debian 3.0r6, de 2005)

  • Teoria
  • Prática
  • Temporização SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3 (Ubuntu 6.06.1, de 2006)
  • Teoria, primeira tentativa
  • Teoria, segunda tentativa
  • Prática
  • Temporização SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2 (Debian 12.5.0, de 2024)
  • Teoria
  • Prática
  • Temporização Rumo a um exploit para amd64 Correções e mitigações Agradecimentos Cronologia

======================================================================== Resumo

root@kitploit:~
Tudo o que é preciso é um salto de fé
    -- The Interrupters, "Leap of Faith"

Nota preliminar: o OpenSSH é um dos softwares mais seguros do mundo; esta vulnerabilidade é um deslize em uma implementação que, fora isso, é quase impecável. Seu design e código de defesa em profundidade são um modelo e uma inspiração, e agradecemos aos desenvolvedores do OpenSSH por seu trabalho exemplar.

Descobrimos uma vulnerabilidade (uma condição de corrida no manipulador de sinais) no servidor do OpenSSH (sshd): se um cliente não autenticar dentro de LoginGraceTime segundos (120 por padrão, 600 em versões antigas do OpenSSH), o manipulador SIGALRM do sshd é chamado assincronamente, mas esse manipulador de sinais chama várias funções que não são seguras para sinais assíncronos (por exemplo, syslog()). Essa condição de corrida afeta o sshd em sua configuração padrão.

Ao investigar, percebemos que essa vulnerabilidade é, na verdade, uma regressão do CVE-2006-5051 ("Condição de corrida no manipulador de sinais no OpenSSH anterior à versão 4.4 permite que atacantes remotos causem negação de serviço (queda) e, possivelmente, executem código arbitrário"), que foi relatado em 2006 por Mark Dowd.

Essa regressão foi introduzida em outubro de 2020 (OpenSSH 8.5p1) pelo commit 752250c ("infraestrutura de log revisada para OpenSSH"), que removeu acidentalmente um "#ifdef DO_LOG_SAFE_IN_SIGHAND" de sigdie(), uma função que é chamada diretamente pelo manipulador SIGALRM do sshd. Em outras palavras:

  • OpenSSH < 4.4p1 é vulnerável a essa condição de corrida no manipulador de sinais, se não tiver recebido backport de correção contra CVE-2006-5051, ou não tiver sido corrigido contra CVE-2008-4109, que foi uma correção incorreta para CVE-2006-5051;

  • 4.4p1 <= OpenSSH < 8.5p1 não é vulnerável a essa condição de corrida no manipulador de sinais (porque o "#ifdef DO_LOG_SAFE_IN_SIGHAND" que foi adicionado a sigdie() pelo patch para CVE-2006-5051 transformou essa função insegura em uma chamada segura _exit(1));

  • 8.5p1 <= OpenSSH < 9.8p1 é vulnerável novamente a essa condição de corrida no manipulador de sinais (porque o "#ifdef DO_LOG_SAFE_IN_SIGHAND" foi removido acidentalmente de sigdie()).

Essa vulnerabilidade é explorável remotamente em sistemas Linux baseados em glibc, onde syslog() chama funções não seguras para sinais assíncronos (por exemplo, malloc() e free()): uma execução remota de código não autenticada como root, porque afeta o código privilegiado do sshd, que não é isolado em sandbox e executa com privilégios totais. Não investigamos nenhuma outra libc ou sistema operacional; mas o OpenBSD notadamente não é vulnerável, porque seu manipulador SIGALRM chama syslog_r(), uma versão de syslog() mais segura para sinais assíncronos, inventada pelo OpenBSD em 2001.

Para explorar essa vulnerabilidade remotamente (até onde sabemos, o CVE-2006-5051 nunca foi explorado com sucesso antes), nos inspiramos em um artigo visionário, "Delivering Signals for Fun and Profit", publicado em 2001 por Michal Zalewski:

https://lcamtuf.coredump.cx/signals.txt

No entanto, enfrentamos imediatamente três grandes problemas:

  • Do ponto de vista teórico, precisamos encontrar um caminho de código útil que, se interrompido no momento certo pelo SIGALRM, deixe o sshd em um estado inconsistente, e devemos então explorar esse estado inconsistente dentro do manipulador SIGALRM.

  • Do ponto de vista prático, precisamos encontrar uma maneira de alcançar esse caminho de código útil no sshd e maximizar nossas chances de interrompê-lo no momento certo.

  • Do ponto de vista de temporização, precisamos encontrar uma maneira de aumentar ainda mais nossas chances de interromper esse caminho de código útil no momento certo, remotamente.

Para focar nesses três problemas sem ter que enfrentar imediatamente todas as proteções modernas dos sistemas operacionais (em particular, ASLR e NX), decidimos explorar primeiro versões antigas do OpenSSH, em i386, e depois, com base nessa experiência, versões recentes:

  • Primeiro, "SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3", de "debian-30r6-dvd-i386-binary-1_NONUS.iso": esta é a primeira versão do Debian que tem separação de privilégios habilitada por padrão e que foi corrigida contra todas as vulnerabilidades críticas daquela época (em particular, CVE-2003-0693 e CVE-2002-0640).

    Para explorar esta versão remotamente, interrompemos uma chamada a free() com SIGALRM (dentro do código de análise de chave pública do sshd), deixamos o heap em um estado inconsistente e exploramos esse estado inconsistente durante outra chamada a free(), dentro do manipulador SIGALRM.

    Em nossos experimentos, leva em média ~10.000 tentativas para vencer essa condição de corrida; ou seja, com 10 conexões (MaxStartups) aceitas a cada 600 segundos (LoginGraceTime), leva em média ~1 semana para obter um shell root remoto.

  • Segundo, "SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3", de "ubuntu-6.06.1-server-i386.iso": esta é a última versão do Ubuntu que ainda é vulnerável ao CVE-2006-5051 ("Condição de corrida no manipulador de sinais no OpenSSH anterior à versão 4.4").

    Para explorar esta versão remotamente, interrompemos uma chamada a pam_start() com SIGALRM, deixamos uma das estruturas do PAM em um estado inconsistente e exploramos esse estado inconsistente durante uma chamada a pam_end(), dentro do manipulador SIGALRM.

    Em nossos experimentos, leva em média ~10.000 tentativas para vencer essa condição de corrida; ou seja, com 10 conexões (MaxStartups) aceitas a cada 120 segundos (LoginGraceTime), leva em média ~1-2 dias para obter um shell root remoto.

  • Por fim, "SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2", de "debian-12.5.0-i386-DVD-1.iso": esta é a versão estável atual do Debian, e ela é vulnerável à regressão do CVE-2006-5051.

    Para explorar esta versão remotamente, interrompemos uma chamada a malloc() com SIGALRM (dentro do código de análise de chave pública do sshd), deixamos o heap em um estado inconsistente e exploramos esse estado inconsistente durante outra chamada a malloc(), dentro do manipulador SIGALRM (mais precisamente, dentro de syslog()).

    Em nossos experimentos, leva em média ~10.000 tentativas para vencer essa condição de corrida, portanto ~3-4 horas com 100 conexões (MaxStartups) aceitas a cada 120 segundos (LoginGraceTime). Em última análise, leva em média ~6-8 horas para obter um shell root remoto, porque só conseguimos adivinhar corretamente o endereço da glibc metade das vezes (devido ao ASLR).

Esta pesquisa ainda está em andamento:

  • temos como alvo apenas máquinas virtuais, não servidores bare-metal, em um enlace de rede majoritariamente estável (~10ms de jitter de pacotes);

  • estamos convencidos de que vários aspectos de nossos exploits podem ser muito melhorados;

  • começamos a trabalhar em um exploit para amd64, que é muito mais difícil por causa do ASLR mais forte.

Poucos dias depois de começarmos nosso trabalho em amd64, notamos o seguinte relatório de bug (no Bugzilla público do OpenSSH), sobre um deadlock no manipulador SIGALRM do sshd:

https://bugzilla.mindrot.org/show_bug.cgi?id=3690

Portanto, decidimos entrar em contato imediatamente com os desenvolvedores do OpenSSH (para informá-los de que esse deadlock é causado por uma vulnerabilidade explorável), colocamos nosso trabalho em amd64 em espera e começamos a escrever este advisory.

======================================================================== SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3 (Debian 3.0r6, de 2005)


Teoria

root@kitploit:~
Mas isso não é típico de mim, estou me libertando
    -- The Interrupters, "Haven't Seen the Last of Me"

O manipulador SIGALRM desta versão do OpenSSH chama packet_close(), que chama buffer_free(), que chama xfree() e, portanto, free(), que não é segura para sinais assíncronos:


302 grace_alarm_handler(int sig) 303 { ... 307 packet_close();

329 packet_close(void) 330 { ... 341 buffer_free(&input); 342 buffer_free(&output); 343 buffer_free(&outgoing_packet); 344 buffer_free(&incoming_packet);

35 buffer_free(Buffer *buffer) 36 { 37 memset(buffer->buf, 0, buffer->alloc); 38 xfree(buffer->buf); 39 }

51 xfree(void *ptr) 52 { 53 if (ptr == NULL) 54 fatal("xfree: NULL pointer given as argument"); 55 free(ptr); 56 }

Consequentemente, começamos a ler o código do malloc da glibc deste Debian (2.2.5), para ver se uma primeira chamada a free() pode ser interrompida pelo SIGALRM e explorada durante uma segunda chamada a free() dentro do manipulador SIGALRM (nas linhas 341-344, acima). Como o malloc dessa glibc não é endurecido contra a técnica unlink() pioneira de Solar Designer em 2000, rapidamente identificamos um caminho de código interessante em chunk_free() (que é chamado internamente por free()):


1028 struct malloc_chunk 1029 { 1030 INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). / 1031 INTERNAL_SIZE_T size; / Size in bytes, including overhead. / 1032 struct malloc_chunk fd; /* double links -- used only if free. / 1033 struct malloc_chunk bk; 1034 };

2516 #define unlink(P, BK, FD)
2517 {
2518 BK = P->bk;
2519 FD = P->fd;
2520 FD->bk = BK;
2521 BK->fd = FD;
2522 } \

3160 chunk_free(arena ar_ptr, mchunkptr p) .... 3164 { 3165 INTERNAL_SIZE_T hd = p->size; / its head field / .... 3177 sz = hd & ~PREV_INUSE; 3178 next = chunk_at_offset(p, sz); 3179 nextsz = chunksize(next); .... 3230 if (!(inuse_bit_at_offset(next, nextsz))) / consolidate forward / 3231 { .... 3241 unlink(next, bck, fwd); .... 3244 } 3245 else 3246 set_head(next, nextsz); / clear inuse bit */ .... 3251 frontlink(ar_ptr, p, sz, idx, bck, fwd);

Para explorar esse caminho de código, organizamos o heap do sshd para ter o seguinte layout (chunk_X, chunk_Y e chunk_Z são blocos de memória alocados com malloc(), e p, s, f, b são seus campos prev_size, size, fd e bk):

-----|---+---------------|---+---------------|---+---------------|----- ... |p|s|f|b| chunk_X |p|s|f|b| chunk_Y |p|s|f|b| chunk_Z | ... -----|---+---------------|---+---------------|---+---------------|----- |<------------->| user data

  • Primeiro, se uma chamada a free(chunk_Y) for interrompida pelo SIGALRM depois da linha 3246, mas antes da linha 3251, então chunk_Y já está marcado como livre (porque o bit PREV_INUSE de chunk_Z é limpo na linha 3246), mas ainda não foi vinculado à sua lista duplamente encadeada (na linha 3251): em outras palavras, os ponteiros fd e bk de chunk_Y ainda contêm dados do usuário (dados controlados pelo atacante).

  • Segundo, se (dentro do manipulador SIGALRM) packet_close() chamar free(chunk_X), então o bloco de código nas linhas 3230-3244 é executado (porque chunk_Y está marcado como livre) e chunk_Y é desvinculado (unlink) (na linha 3241): uma chamada primitiva aa4bmo (quase sobrescrita espelhada arbitrária de 4 bytes), porque os ponteiros fd e bk de chunk_Y ainda são controlados pelo atacante. Para mais informações sobre a técnica unlink() e a primitiva aa4bmo:

    https://www.openwall.com/articles/JPEG-COM-Marker-Vulnerability#exploit http://phrack.org/issues/61/6.html#article

  • Por último, com essa primitiva aa4bmo, sobrescrevemos o ponteiro de função __free_hook da glibc (esta versão antiga do Debian não tem ASLR, nem NX) com o endereço do nosso shellcode no heap, alcançando assim execução remota de código durante a próxima chamada a free() em packet_close().


Prática

root@kitploit:~
Agora eles estão assumindo o controle e têm controle total
    -- The Interrupters, "Liberty"

Para montar esse ataque contra o sshd, interrompemos uma chamada a free() dentro do código de análise do sshd de uma chave pública DSA (ou seja, a linha 144 abaixo é o nosso free(chunk_Y)) e a exploramos durante uma das chamadas a free() em packet_close() (ou seja, uma das linhas 341-344 acima é o nosso free(chunk_X)):


136 buffer_get_bignum2(Buffer *buffer, BIGNUM *value) 137 { 138 u_int len; 139 u_char *bin = buffer_get_string(buffer, &len); ... 143 BN_bin2bn(bin, len, value); 144 xfree(bin); 145 }

Inicialmente, no entanto, nunca conseguimos vencer essa condição de corrida (ou seja, interromper a chamada a free() na linha 144 no momento certo). Eventualmente, percebemos que poderíamos melhorar muito nossas chances de vencer essa corrida: o código de análise de chave pública DSA nos permite chamar free() quatro vezes (nas linhas 704-707 abaixo) e, além disso, o sshd nos permite tentar seis autenticações de usuário (AUTH_FAIL_MAX); se qualquer uma dessas 24 chamadas a free() for interrompida no momento certo, alcançamos execução remota de código dentro do manipulador SIGALRM.


678 key_from_blob(u_char *blob, int blen) 679 { ... 693 switch (type) { ... 702 case KEY_DSA: 703 key = key_new(type); 704 buffer_get_bignum2(&b, key->dsa->p); 705 buffer_get_bignum2(&b, key->dsa->q); 706 buffer_get_bignum2(&b, key->dsa->g); 707 buffer_get_bignum2(&b, key->dsa->pub_key);

Com essa melhoria, finalmente vencemos a condição de corrida após ~1 mês: ficamos felizes (e dançamos a dança do root shell), mas também sentimos que ainda havia espaço para melhorias.


Temporização

root@kitploit:~
Não se preocupe, apenas espere e veja
    -- The Interrupters, "Haven't Seen the Last of Me"

Portanto, implementamos a seguinte estratégia de temporização tripla:

  • Não esperamos até o último momento para enviar nosso pacote de chave pública DSA (bastante grande) ao sshd: em vez disso, enviamos o pacote inteiro menos um byte (o último byte) muito antes do LoginGraceTime, e enviamos o último byte no último instante, para minimizar os efeitos dos atrasos de rede. (E desabilitamos o algoritmo de Nagle.)

  • Acompanhamos o tempo de ida e volta mediano (enviando regularmente pacotes que produzem uma resposta do sshd) e acompanhamos a diferença entre o momento em que esperamos que nossa conexão seja fechada pelo sshd (essencialmente o momento em que recebemos o primeiro byte do banner do sshd, mais LoginGraceTime) e o momento em que nossa conexão é realmente fechada pelo sshd, e ajustamos nossa temporização de acordo (ou seja, o momento em que enviamos o último byte do nosso pacote DSA).

  • Essas diferenças de tempo nos permitem rastrear dessincronizações de relógio e atrasos de rede, que apresentam padrões previsíveis ao longo do tempo: experimentamos regressões lineares e spline, mas, no final, nada funcionou melhor do que simplesmente reutilizar a medição mais recente. Possivelmente, aprendizado profundo (deep learning) possa produzir resultados ainda melhores; isso fica como exercício para o leitor interessado.

  • Mais importante ainda, aumentamos ainda mais nossas chances de vencer essa condição de corrida ajustando lentamente nossa temporização por meio de feedback involuntário do sshd:

    • se recebermos uma resposta (SSH2_MSG_USERAUTH_FAILURE) ao nosso pacote de chave pública DSA, então o enviamos cedo demais (o sshd teve tempo de receber nosso pacote no filho sem privilégios, analisá-lo, enviá-lo ao filho privilegiado, analisá-lo lá e enviar uma resposta de volta para nós);

    • se não conseguirmos nem enviar o último byte do nosso pacote DSA, então esperamos demais (o sshd já recebeu o SIGALRM e fechou nossa conexão);

    • se conseguirmos enviar o último byte do nosso pacote DSA e não recebermos nenhuma resposta antes de o sshd fechar nossa conexão, então nossa temporização estava razoavelmente precisa.

Esse feedback nos permite mirar o que chamamos de janela de corrida "grande": acertá-la não garante que venceremos a condição de corrida, mas dentro dessa janela grande estão as 24 janelas de corrida "pequenas" (dentro das 24 chamadas a free()) que, se acertadas, garantem que venceremos a condição de corrida.

Com essas melhorias, leva em média ~10.000 tentativas para vencer essa condição de corrida; ou seja, com 10 conexões (MaxStartups) aceitas a cada 600 segundos (LoginGraceTime), leva em média ~1 semana para obter um shell root remoto.

======================================================================== SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3 (Ubuntu 6.06.1, de 2006)


Teoria, primeira tentativa

root@kitploit:~
Eu durmo quando o sol começa a nascer
    -- The Interrupters, "Alien"

O manipulador SIGALRM desta versão do OpenSSH não chama mais packet_close(); além disso, a glibc deste Ubuntu (2.3.6) sempre adquire um bloqueio obrigatório ao entrar nas funções da família malloc (mesmo em um processo single-threaded como o sshd), o que nos impede de interromper uma chamada a uma das funções malloc e depois explorá-la durante outra chamada a essas funções (elas sempre entrariam em deadlock). Precisamos encontrar outra solução.

O CVE-2006-5051 menciona um double-free no GSSAPI, mas o GSSAPI (ou Kerberos) não está habilitado por padrão, então isso não parece muito atraente. Por outro lado, o PAM está habilitado por padrão, e pam_end() é chamado pelo manipulador SIGALRM do sshd (e, claro, não é seguro para sinais assíncronos). Portanto, procuramos uma função do PAM que, se interrompida pelo SIGALRM no momento certo, deixasse as estruturas internas do PAM em um estado inconsistente, explorável durante pam_end() no manipulador SIGALRM. Encontramos pam_set_data():


33 int pam_set_data( 34 pam_handle_t *pamh, .. 37 void (*cleanup)(pam_handle_t *pamh, void *data, int error_status)) 38 { 39 struct pam_data *data_entry; .. 57 } else if ((data_entry = malloc(sizeof(*data_entry)))) { .. 65 data_entry->next = pamh->data; 66 pamh->data = data_entry; .. 74 data_entry->cleanup = cleanup; ------------------------------------------------------------------------Se esta função for interrompida por SIGALRM depois da linha 66 mas antes da linha 74, então data_entry já está ligado às estruturas do PAM (pamh), mas o seu campo cleanup (um ponteiro para função) ainda não foi inicializado (visto que o malloc() na linha 57 não inicializa a sua memória). Se conseguirmos controlar cleanup (através de restos de alocações anteriores da heap), então podemos executar código arbitrário quando pam_end() (dentro do handler de SIGALRM) chama _pam_free_data() (na linha 118):


104 void _pam_free_data(pam_handle_t *pamh, int status) 105 { 106 struct pam_data *last; 107 struct pam_data *data; ... 112 data = pamh->data; 113 114 while (data) { 115 last = data; 116 data = data->next; 117 if (last->cleanup) { 118 last->cleanup(pamh, last->data, status);

Isto teria sido um exploit extremamente simples; infelizmente, nós ignorámos completamente que pam_set_data() só pode ser chamado a partir de módulos PAM: se o interrompermos com SIGALRM, então pamh->caller_is ainda é _PAM_CALLED_FROM_MODULE, caso em que pam_end() retorna imediatamente, sem nunca chamar _pam_free_data(). De volta à estaca zero.


Teoria, segunda tentativa

root@kitploit:~
Não desistir, não é o que fazemos
    -- The Interrupters, "Title Holder"

Reparámos que, na linha 601 abaixo, o sshd passa um ponteiro para o seu ponteiro global sshpam_handle diretamente para pam_start() (que é chamado uma vez por conexão):


202 static pam_handle_t *sshpam_handle = NULL;

584 sshpam_init(Authctxt *authctxt) 585 { ... 600 sshpam_err = 601 pam_start(SSHD_PAM_SERVICE, user, &store_conv, &sshpam_handle);

Decidimos então analisar o próprio pam_start(): se for interrompido por SIGALRM, poderá deixar a estrutura apontada por sshpam_handle num estado inconsistente, que poderia então ser explorado dentro do handler de SIGALRM, quando "pam_end(sshpam_handle, sshpam_err)" é chamado.


18 int pam_start ( .. 22 pam_handle_t **pamh) 23 { .. 32 if ((*pamh = calloc(1, sizeof(**pamh))) == NULL) { ... 110 if ( _pam_init_handlers(*pamh) != PAM_SUCCESS ) {

319 int _pam_init_handlers(pam_handle_t *pamh) 320 { ... 398 retval = _pam_parse_conf_file(pamh, f, pamh->service_name, PAM_T_ANY

66 static int _pam_parse_conf_file(pam_handle_t *pamh, FILE *f .. 73 { ... 252 res = _pam_add_handler(pamh, must_fail, other

581 int _pam_add_handler(pam_handle_t *pamh ... 585 { ... 755 the_handlers = (other) ? &pamh->handlers.other : &pamh->handlers.conf; ... 767 handler_p = &the_handlers->authenticate; ... 874 if ((*handler_p = malloc(sizeof(struct handler))) == NULL) { ... 886 (*handler_p)->next = NULL;

Na linha 32, pam_start() define imediatamente o sshpam_handle do sshd para um bloco de memória alocado com calloc(); isto é seguro, porque calloc() inicializa esta memória a zero. Por outro lado, se _pam_add_handler() (que é chamado várias vezes por pam_start()) for interrompido por SIGALRM depois da linha 874 mas antes da linha 886, então uma estrutura alocada com malloc() é ligada em pamh, mas o seu campo next ainda não foi inicializado. Se conseguirmos controlar next (através de restos de alocações anteriores da heap), então podemos passar um ponteiro arbitrário para free() durante a chamada a pam_end() (dentro do handler de SIGALRM), na linha 1020 (e linha 1017) abaixo:


11 int pam_end(pam_handle_t *pamh, int pam_status) 12 { .. 31 if ((ret = _pam_free_handlers(pamh)) != PAM_SUCCESS) {

925 int _pam_free_handlers(pam_handle_t *pamh) 926 { ... 954 _pam_free_handlers_aux(&(pamh->handlers.conf.authenticate));

1009 void _pam_free_handlers_aux(struct handler **hp) 1010 { 1011 struct handler *h = *hp; 1012 struct handler last; .... 1015 while (h) { 1016 last = h; 1017 _pam_drop(h->argv); / This is all alocated in a single chunk */ 1018 h = h->next; 1019 memset(last, 0, sizeof(*last)); 1020 free(last); 1021 }

Uma vez que o malloc da glibc deste Ubuntu já está protegido contra a antiga técnica unlink(), decidimos transformar o nosso free() arbitrário no House of Mind do Malloc Maleficarum (versão fastbin): libertamos (free()) o nosso próprio chunk NON_MAIN_ARENA, apontamos a nossa arena falsa para o .got.plt do sshd (o sshd deste Ubuntu tem ASLR mas não PIE), e sobrescrevemos a entrada de _exit() com o endereço do nosso shellcode na heap (a heap deste Ubuntu ainda é executável por predefinição). Para mais informações sobre o Malloc Maleficarum:

https://seclists.org/bugtraq/2005/Oct/118


Prática

root@kitploit:~
Aprendi tudo da maneira difícil
    -- The Interrupters, "The Hard Way"

Para montar este ataque contra o sshd, enfrentámos inicialmente três problemas:

  • O House of Mind exige que armazenemos o ponteiro para a nossa arena falsa no endereço 0x08100000 na heap; mas será que conseguimos armazenar dados controlados pelo atacante num endereço tão alto? Como o sshd chama pam_start() logo no início da autenticação do utilizador, não controlamos nada exceto o próprio nome de utilizador; felizmente, um nome de utilizador com ~128KB (mais curto que DEFAULT_MMAP_THRESHOLD) permite-nos armazenar os nossos próprios dados no endereço 0x08100000.

  • O campo size do nosso chunk falso NON_MAIN_ARENA não pode ser demasiado grande (para passar as verificações de segurança do free()); ou seja, deve conter bytes nulos. Mas o nosso nome de utilizador longo é uma string terminada em nulo que não pode conter bytes nulos; felizmente, lembramo-nos de que _pam_free_handlers_aux() coloca a zero as estruturas que liberta (free()) (linha 1019 acima): por isso, "remendamos" o campo size do nosso chunk falso com esse memset(0), e só depois chamamos free() a ele.

  • Temos de sobreviver a várias chamadas a free() (nas linhas 1017 e 1020 acima) antes do free() do nosso chunk falso NON_MAIN_ARENA. Transformamos estes free() em no-ops, apontando-os para chunks falsos IS_MMAPPED: free() chama munmap_chunk(), que chama munmap(), que falha porque estes chunks falsos IS_MMAPPED estão desalinhados; efetivamente um no-op, porque as falhas de assert() não são aplicadas na glibc deste Ubuntu.

Por fim, o nosso nome de utilizador longo também nos permite controlar o campo next potencialmente não inicializado de 20 estruturas diferentes (através de restos de cópias temporárias do nosso nome de utilizador longo), porque pam_start() chama _pam_add_handler() várias vezes; ou seja, a nossa grande janela de condição de corrida contém 20 pequenas janelas de condição de corrida.


Temporização

root@kitploit:~
Os mesmos truques que usaram antes
    -- The Interrupters, "Divide Us"

Para este ataque contra o Ubuntu 6.06.1, simplesmente reutilizámos a estratégia de temporização que usámos contra o Debian 3.0r6: são precisas ~10.000 tentativas em média para vencer a condição de corrida, e com 10 conexões (MaxStartups) aceites por cada 120 segundos (LoginGraceTime), leva em média ~1-2 dias a obter uma shell root remota.

Nota: como a glibc deste Ubuntu assume sempre um bloqueio obrigatório ao entrar nas funções da família malloc, um atacante azarado poderá causar deadlock em todas as 10 ligações MaxStartups antes de obter uma shell root; não tentámos contornar este problema porque o nosso objetivo final era explorar uma versão moderna do OpenSSH de qualquer forma.

======================================================================== SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2 (Debian 12.5.0, de 2024)


Teoria

root@kitploit:~
Agora estás pronto, enfrenta os demónios de frente
    -- The Interrupters, "Be Gone"

O handler de SIGALRM desta versão do OpenSSH não chama packet_close() nem pam_end(); na realidade, chama apenas uma função interessante, syslog():


358 grace_alarm_handler(int sig) 359 { ... 370 sigdie("Timeout before authentication for %s port %d", 371 ssh_remote_ipaddr(the_active_state), 372 ssh_remote_port(the_active_state));

96 #define sigdie(...) sshsigdie(FILE, func, LINE, 0, SYSLOG_LEVEL_ERROR, NULL, VA_ARGS)

451 sshsigdie(const char *file, const char *func, int line, int showfunc, 452 LogLevel level, const char *suffix, const char *fmt, ...) 453 { ... 457 sshlogv(file, func, line, showfunc, SYSLOG_LEVEL_FATAL, 458 suffix, fmt, args);

464 sshlogv(const char *file, const char *func, int line, int showfunc, 465 LogLevel level, const char *suffix, const char *fmt, va_list args) 466 { ... 489 do_log(level, forced, suffix, fmt2, args);

337 do_log(LogLevel level, int force, const char *suffix, const char *fmt, 338 va_list args) 339 { ... 419 syslog(pri, "%.500s", fmtbuf);

As nossas duas questões-chave, então, são: o syslog() da glibc deste Debian (2.36) chama funções async-signal-unsafe, como malloc() e free()? E se sim, esta glibc ainda assume um bloqueio obrigatório ao entrar nas funções da família malloc?

  • Felizmente para nós, atacantes, a resposta à nossa primeira pergunta é sim; se, e somente se, o syslog() dentro do handler de SIGALRM for a primeira chamada a syslog(), então __localtime64_r() (que é chamado por syslog()) chama malloc(304) para alocar uma estrutura FILE (na linha 166) e chama malloc(4096) para alocar um buffer de leitura interno (na linha 186):

28 __localtime64_r (const __time64_t *t, struct tm *tp) 29 { 30 return __tz_convert (*t, 1, tp);

567 __tz_convert (__time64_t timer, int use_localtime, struct tm *tp) 568 { ... 577 tzset_internal (tp == &_tmbuf && use_localtime);

367 tzset_internal (int always) 368 { ... 405 __tzfile_read (tz, 0, NULL);

105 __tzfile_read (const char *file, size_t extra, char **extrap) 106 { ... 109 FILE *f; ... 166 f = fopen (file, "rce"); ... 186 if (__builtin_expect (__fread_unlocked ((void *) &tzhead, sizeof (tzhead), 187 1, f) != 1, 0)

Nota: como não controlamos nada sobre estas alocações malloc() (nem a sua ordem, nem os seus tamanhos, nem o seu conteúdo), encarámos o "rce" na linha 166 como um bom presságio muito necessário.

  • E felizmente para nós, a resposta à nossa segunda pergunta é não; desde outubro de 2017, as funções malloc da glibc já não assumem qualquer bloqueio, quando em modo single-threaded (como o sshd):

    https://sourceware.org/git?p=glibc.git;a=commit;h=a15d53e2de4c7d83bda251469d92a3c7b49a90db https://sourceware.org/git?p=glibc.git;a=commit;h=3f6bb8a32e5f5efd78ac08c41e623651cc242a89 https://sourceware.org/git?p=glibc.git;a=commit;h=905a7725e9157ea522d8ab97b4c8b96aeb23df54

Além disso, esta versão do Debian sofre da fraqueza de ASLR descrita nos seguintes excelentes artigos de blog (por Justin Miller e Mathias Krause, respetivamente):

https://zolutal.github.io/aslrnt/ https://grsecurity.net/toolchain_necromancy_past_mistakes_haunting_aslr

Concretamente, no caso do sshd em i386, cada mapeamento de memória é randomizado normalmente (o PIE do sshd, a heap, a maioria das bibliotecas, a stack), mas a própria glibc é sempre mapeada ou no endereço 0xb7200000 ou no endereço 0xb7400000; por outras palavras, podemos adivinhar corretamente o endereço da glibc metade das vezes (um pequeno preço a pagar para derrotar o ASLR). No nosso exploit, assumimos que a glibc está mapeada no endereço 0xb7400000, porque é ligeiramente mais comum do que 0xb7200000.

A nossa próxima pergunta é: quais os caminhos de código dentro das funções malloc da glibc que, se interrompidos por SIGALRM no momento certo, deixam a heap num estado inconsistente, explorável durante uma das chamadas malloc() dentro do handler de SIGALRM?

Encontrámos vários caminhos de código interessantes (e surpreendentes!), mas o que escolhemos envolve apenas tamanhos relativos, não endereços absolutos (ao contrário de vários caminhos de código dentro de unlink_chunk(), por exemplo); esta diferença pode revelar-se crucial para um futuro exploit em amd64. Este caminho de código, dentro de malloc(), divide um chunk livre grande (victim) em dois chunks mais pequenos; o primeiro chunk é devolvido ao chamador de malloc() (na linha 4345) e o segundo chunk (remainder) é ligado a uma lista não ordenada de chunks livres (nas linhas 4324-4327):


1449 #define set_head(p, s) ((p)->mchunk_size = (s))

3765 _int_malloc (mstate av, size_t bytes) 3766 { .... 3798 nb = checked_request2size (bytes); .... 4295 size = chunksize (victim); .... 4300 remainder_size = size - nb; .... 4316 remainder = chunk_at_offset (victim, nb); .... 4320 bck = unsorted_chunks (av); 4321 fwd = bck->fd; .... 4324 remainder->bk = bck; 4325 remainder->fd = fwd; 4326 bck->fd = remainder; 4327 fwd->bk = remainder; .... 4337 set_head (victim, nb | PREV_INUSE | 4338 (av != &main_arena ? NON_MAIN_ARENA : 0)); 4339 set_head (remainder, remainder_size | PREV_INUSE); .... 4343 void *p = chunk2mem (victim); .... 4345 return p;

  • Se este caminho de código for interrompido por SIGALRM depois da linha 4327 mas antes da linha 4339, então o chunk remainder desta divisão já está ligado à lista não ordenada de chunks livres (linhas 4324-4327), mas o seu campo size (mchunk_size) ainda não foi inicializado (linha 4339).

  • Se conseguirmos controlar o seu campo size (através de restos de alocações anteriores da heap), então podemos tornar este chunk remainder maior e fazê-lo sobrepor-se a outros chunks da heap e, portanto, corromper a memória da heap quando este chunk remainder ampliado e sobreposto for eventualmente alocado com malloc() e escrito (dentro do handler de SIGALRM).

A nossa última pergunta, então, é: dado que não controlamos nada sobre as chamadas malloc() dentro do handler de SIGALRM, o que podemos sobrescrever na heap para obter execução de código arbitrário antes de o sshd chamar _exit() (em sshsigdie())?

Como __tzfile_read() (dentro do handler de SIGALRM) aloca com malloc() uma estrutura FILE na heap (na linha 166 acima), e como as estruturas FILE têm um longo historial de abuso para execução de código arbitrário, decidimos apontar a nossa corrupção da heap para esta estrutura FILE. Isto é, no entanto, mais fácil de dizer do que de fazer: a nossa corrupção da heap é muito limitada, e as estruturas FILE foram significativamente reforçadas ao longo dos anos (por IO_validate_vtable() e PTR_DEMANGLE(), por exemplo).

Eventualmente, concebemos a seguinte técnica (que parece ser específica da glibc em i386 -- a glibc em amd64 não parece usar _vtable_offset de todo):

  • com a nossa corrupção limitada da heap, sobrescrevemos o campo _vtable_offset (um único char com sinal) da estrutura FILE de __tzfile_read();

  • as funções libio da glibc procurarão, portanto, o ponteiro vtable desta estrutura FILE (um ponteiro para um array de ponteiros de função) num offset não nulo (o nosso _vtable_offset sobrescrito), em vez do offset zero predefinido;

  • nós (atacantes) conseguimos facilmente controlar este ponteiro vtable falso (através de restos de alocações anteriores da heap), porque a estrutura FILE à volta deste offset não é explicitamente inicializada por fopen();

  • para passar nas verificações de segurança da glibc, o nosso ponteiro vtable falso tem de apontar para algum lugar na secção __libc_IO_vtables: decidimos apontá-lo para a vtable de fluxos de caracteres largos, _IO_wfile_jumps (ou seja, para 0xb761b740, já que assumimos que a glibc está mapeada no endereço 0xb7400000);

  • como resultado, __fread_unlocked() (na linha 186 acima) chama _IO_wfile_underflow() (em vez de _IO_file_underflow()), que chama um ponteiro de função (__fct) que basicamente vem de uma estrutura cujo ponteiro (_codecvt) é ainda outro campo da estrutura FILE;

  • nós (atacantes) conseguimos facilmente controlar este ponteiro _codecvt (através de restos de alocações anteriores da heap, porque este campo da estrutura FILE não é explicitamente inicializado por fopen()), o que também nos permite controlar o ponteiro de função __fct.

Em resumo, ao sobrescrever um único byte (_vtable_offset) da estrutura FILE alocada com malloc() por fopen(), podemos chamar o nosso próprio ponteiro de função __fct e executar código arbitrário durante __fread_unlocked().


Prática

root@kitploit:~
Eu queria-o perfeito, sem uma única ruga
    -- The Interrupters, "In the Mirror"

Para montar este ataque contra o filho privilegiado do sshd, vamos primeiro imaginar a seguinte disposição da heap (os "XXX" são chunks "barreira" que nos permitem fazer buracos na heap; por exemplo, pequenos chunks com fugas de memória):

---|----------------------------------------------|---|------------|---

XXXlarge holeXXXsmall holeXXX
~8KB320B
  • pouco antes de o sshd receber o SIGALRM, alocamos com malloc() um chunk de ~4KB que divide o grande buraco de ~8KB em dois chunks mais pequenos:

---|-----------------------|----------------------|---|------------|---

XXXlarge allocated chunkfree remainder chunkXXXsmall holeXXX
~4KB~4KB320B
  • mas se este malloc() for interrompido por SIGALRM depois da linha 4327 mas antes da linha 4339, então o chunk remainder desta divisão já está ligado à lista não ordenada de chunks livres, mas o seu campo size está sob o nosso controlo (através de restos de alocações anteriores da heap), e este chunk remainder artificialmente ampliado sobrepõe-se ao pequeno buraco seguinte:---|-----------------------|----------------------|---|------------|--- XXX| large allocated chunk | real remainder chunk |XXX| small hole |XXX ---|-----------------------|----------------------|---|------------|--- | ~4KB |<------------------------------------->| artificially enlarged remainder chunk

  • quando o manipulador de SIGALRM chama syslog() e, portanto, __tzfile_read(), fopen() faz malloc() do pequeno buraco para a sua estrutura FILE, e __fread_unlocked() faz malloc() de um buffer de leitura de 4KB, dividindo assim o chunk remanescente ampliado em dois (o buffer de leitura de 4KB e um pequeno chunk remanescente):

---|-----------------------|----------------------|---|------------|--- XXX| large allocated chunk | |XXX| FILE |XXX ---|-----------------------|----------------------|---|--|---------|--- | ~4KB |<--------------------------->|<------->| 4KB read buffer remainder

  • portanto, sobrescrevemos partes da estrutura FILE com o cabeçalho interno deste pequeno chunk remanescente: mais precisamente, sobrescrevemos o _vtable_offset do FILE com o terceiro byte do campo bk deste cabeçalho, que é um ponteiro para a lista não ordenada de chunks livres, 0xb761d7f8 (ou seja, sobrescrevemos _vtable_offset com 0x61);

  • então, como explicado na subseção "Theory", __fread_unlocked() chama _IO_wfile_underflow() (em vez de _IO_file_underflow()), que chama o nosso próprio ponteiro de função __fct (através do nosso próprio ponteiro _codecvt) e executa o nosso código arbitrário.

    Nota: ainda não explicamos como passar de forma fiável de um ponteiro _codecvt controlado para um ponteiro de função __fct controlado; faremos isso, mas primeiro precisamos resolver um problema mais premente.

De fato, aprendemos com o nosso trabalho em versões mais antigas do OpenSSH que nunca venceremos esta condição de corrida do manipulador de sinais se a nossa grande janela de corrida contiver apenas uma pequena janela de corrida. Consequentemente, implementamos a seguinte estratégia, com base no seguinte layout de heap:

---|------------|---|------------|---|------------|---|------------|---

XXXlarge hole 1XXXsmall hole 1XXXlarge hole 2XXXsmall hole 2...

O último pacote que enviamos para o sshd (pouco antes da entrega do SIGALRM) força o sshd a realizar a seguinte sequência de chamadas malloc(): malloc(~4KB), malloc(304), malloc(~4KB), malloc(304), etc.

1/ O nosso primeiro malloc(~4KB) divide o buraco grande 1 em dois:

  • se esta primeira divisão for interrompida pelo SIGALRM no momento certo, então o fopen() dentro do manipulador de SIGALRM faz malloc() do buraco pequeno 1 para a sua estrutura FILE, e conseguimos execução de código arbitrário como explicado acima;

  • se não, então nós próprios fazemos malloc() do buraco pequeno 1 com o nosso primeiro malloc(304), e:

2/ O nosso segundo malloc(~4KB) divide o buraco grande 2 em dois:

  • se esta segunda divisão for interrompida pelo SIGALRM no momento certo, então o fopen() dentro do manipulador de SIGALRM faz malloc() do buraco pequeno 2 para a sua estrutura FILE, e conseguimos execução de código arbitrário como explicado acima;

  • se não, então nós próprios fazemos malloc() do buraco pequeno 2 com o nosso segundo malloc(304), etc.

Conseguimos criar 27 pares desses buracos grandes e pequenos no heap do sshd (28 excederia PACKET_MAX_SIZE, 256KB): a nossa grande janela de corrida agora contém 27 pequenas janelas de corrida! Conseguir este layout de heap complexo foi extremamente doloroso e demorado, mas os dois destaques são:

  • Abusamos do código de análise de chaves públicas do sshd para realizar sequências arbitrárias de chamadas malloc() e free() (nas linhas 1805 e 573):

1754 cert_parse(struct sshbuf *b, struct sshkey *key, struct sshbuf *certbuf) 1755 { .... 1797 while (sshbuf_len(principals) > 0) { .... 1805 if ((ret = sshbuf_get_cstring(principals, &principal, .... 1820 key->cert->principals[key->cert->nprincipals++] = principal; 1821 }

562 cert_free(struct sshkey_cert *cert) 563 { ... 572 for (i = 0; i < cert->nprincipals; i++) 573 free(cert->principals[i]);

  • Não conseguimos encontrar uma fuga de memória para os nossos pequenos chunks de "barreira"; em vez disso, usamos chunks do tcache (que nunca são realmente liberados, porque o seu bit inuse nunca é limpo) como chunks de "barreira" improvisados.

Para alcançar este layout de heap de forma fiável, enviamos cinco pacotes de chave pública diferentes para o sshd (os pacotes a/ a d/ podem ser enviados muito antes do SIGALRM; a maior parte do pacote e/ também pode ser enviada muito antes do SIGALRM, mas o seu último byte tem de ser enviado no último momento):

a/ Fazemos malloc() e free() de uma variedade de chunks do tcache, para garantir que as alocações de heap que não controlamos acabam nesses chunks do tcache e não interferem com o nosso layout de heap cuidadosamente elaborado.

b/ Fazemos malloc() e free() de chunks de vários tamanhos, para criar os nossos 27 pares de buracos grandes e pequenos (e os correspondentes chunks de "barreira").

c/ Fazemos malloc() e free() de chunks de ~4KB e chunks de 320B, para:

  • escrever o cabeçalho falso (o campo de tamanho grande) do nosso potencial chunk remanescente ampliado, no meio dos nossos buracos grandes;

  • escrever o rodapé falso do nosso potencial chunk remanescente ampliado, no final dos nossos buracos pequenos (para passar nas verificações de segurança da glibc);

  • escrever os nossos ponteiros falsos de vtable e _codecvt, dentro dos nossos buracos pequenos (que são potenciais estruturas FILE).

d/ Fazemos malloc() e free() de uma string muito grande (quase 256KB), para garantir que os nossos buracos grandes e pequenos sejam removidos da lista não ordenada de chunks livres e colocados nos seus respectivos bins do malloc.

e/ Forçamos o sshd a realizar a nossa sequência final de chamadas malloc() (malloc(~4KB), malloc(304), malloc(~4KB), malloc(304), etc), para abrir as nossas 27 pequenas janelas de corrida.

Os leitores atentos podem ter reparado que ainda não abordamos (literal e figurativamente) o problema do _codecvt. Na verdade, _codecvt é um ponteiro para uma estrutura (_IO_codecvt) que contém um ponteiro para uma estrutura (__gconv_step) que contém o ponteiro de função __fct que nos permite executar código arbitrário. Para controlar __fct de forma fiável através de _codecvt, simplesmente apontamos _codecvt para um dos bins do malloc da glibc, que convenientemente contém um ponteiro para um dos nossos chunks livres no heap, que contém o nosso próprio ponteiro de função __fct para código arbitrário da glibc (todos esses endereços da glibc são conhecidos por nós, porque assumimos que a glibc está mapeada no endereço 0xb7400000).


Cronometragem

root@kitploit:~
Estamos ficando sem tempo
    -- The Interrupters, "As We Live"

Ao implementarmos este terceiro exploit, ficou claro que não podíamos simplesmente reutilizar a estratégia de cronometragem que tínhamos usado contra as duas versões mais antigas do OpenSSH: nunca estávamos vencendo esta nova condição de corrida. Eventualmente, percebemos porquê:

  • O sshd demora muito tempo (~10ms) a analisar a nossa quinta e última chave pública (pacote e/ acima); por outras palavras, a nossa grande janela de corrida é demasiado grande (as nossas 27 pequenas janelas de corrida são como agulhas num palheiro).

  • O user_specific_delay() que foi introduzido recentemente (OpenSSH 7.8p1) atrasa a resposta do sshd ao nosso último pacote de chave pública em até ~9ms e, portanto, destrói a nossa estratégia de cronometragem baseada em realimentação.

Como resultado, desenvolvemos uma estratégia de cronometragem completamente diferente:

  • de vez em quando, enviamos o nosso último pacote de chave pública com um pequeno erro que produz uma resposta de erro (linhas 138-142 abaixo), logo antes da chamada a sshkey_from_blob() que analisa a nossa chave pública;

  • de vez em quando, enviamos o nosso último pacote de chave pública com outro pequeno erro que produz uma resposta de erro (linhas 151-155 abaixo), logo após a chamada a sshkey_from_blob() que analisa a nossa chave pública;

  • a diferença entre estes dois tempos de resposta é o tempo que o sshd demora a analisar a nossa última chave pública, e isto permite-nos cronometrar com precisão a transmissão dos nossos últimos pacotes (para garantir que o sshd tem tempo para analisar a nossa chave pública no filho não privilegiado, enviá-la para o filho privilegiado e começar a analisá-la lá, antes da entrega do SIGALRM).


88 userauth_pubkey(struct ssh *ssh, const char method) 89 { ... 138 if (pktype == KEY_UNSPEC) { 139 / this is perfectly legal */ 140 verbose_f("unsupported public key algorithm: %s", pkalg); 141 goto done; 142 } 143 if ((r = sshkey_from_blob(pkblob, blen, &key)) != 0) { 144 error_fr(r, "parse key"); 145 goto done; 146 } ... 151 if (key->type != pktype) { 152 error_f("type mismatch for decoded key " 153 "(received %d, expected %d)", key->type, pktype); 154 goto done; 155 }

Com esta mudança de estratégia, são necessárias ~10.000 tentativas em média para vencer a condição de corrida; ou seja, com 100 conexões (MaxStartups) aceitas por 120 segundos (LoginGraceTime), são necessárias ~3-4 horas em média para vencer a condição de corrida, e ~6-8 horas para obter uma shell root remota (por causa do ASLR).

======================================================================== Rumo a um exploit para amd64

root@kitploit:~
Qual é o seu plano para amanhã?
    -- The Interrupters, "Take Back the Power"

Decidimos visar o Rocky Linux 9 (um derivado do Red Hat Enterprise Linux 9), a partir de "Rocky-9.4-x86_64-minimal.iso", por duas razões:

  • a sua versão do OpenSSH (8.7p1) é vulnerável a esta condição de corrida do manipulador de sinais e a sua glibc está sempre mapeada num múltiplo de 2MB (por causa da fraqueza do ASLR discutida na subseção "Theory" anterior), o que torna as sobrescritas parciais de ponteiros muito mais poderosas;

  • a função syslog() (que não é segura para async-signal, mas é chamada pelo manipulador de SIGALRM do sshd) desta versão da glibc (2.34) chama internamente __open_memstream(), que faz malloc() de uma estrutura FILE no heap, e também chama calloc(), realloc() e free() (o que nos dá alguma liberdade tão necessária).

Com uma corrupção de heap como primitiva, duas estruturas FILE com malloc() no heap e 21 bits fixos nos endereços da glibc, acreditamos que esta condição de corrida do manipulador de sinais é explorável em amd64 (provavelmente não em ~6-8 horas, mas esperamos que em menos de uma semana). Só o tempo dirá.

Nota lateral: descobrimos que o Ubuntu 24.04 não re-randomiza o ASLR dos seus filhos sshd (é randomizado apenas uma vez, no arranque); identificamos a origem no patch abaixo, que desativa o rexec_flag do sshd. Isto é geralmente uma má ideia, mas no caso particular desta condição de corrida do manipulador de sinais, impede que o sshd seja explorável: o syslog() dentro do manipulador de SIGALRM não chama nenhuma das funções malloc, porque nunca é a primeira chamada a syslog().

https://git.launchpad.net/ubuntu/+source/openssh/tree/debian/patches/systemd-socket-activation.patch

======================================================================== Patches e mitigação

root@kitploit:~
A tempestade chegou e passou
    -- The Interrupters, "Good Things"

Em 6 de junho de 2024, esta condição de corrida do manipulador de sinais foi corrigida pelo commit 81c1099 ("Add a facility to sshd(8) to penalise particular problematic client behaviours"), que moveu o código não seguro para async-signal do manipulador de SIGALRM do sshd para o processo listener do sshd, onde pode ser tratado de forma síncrona:

https://github.com/openssh/openssh-portable/commit/81c1099d22b81ebfd20a334ce986c4f753b0db29

Como esta correção faz parte de um commit grande (81c1099), por cima de um commit de defesa em profundidade ainda maior (03e3de4, "Start the process of splitting sshd into separate binaries"), pode ser difícil fazer backport. Nesse caso, a própria condição de corrida do manipulador de sinais pode ser corrigida removendo ou comentando o código não seguro para async-signal da função sshsigdie(); por exemplo:


sshsigdie(const char *file, const char *func, int line, int showfunc, LogLevel level, const char *suffix, const char *fmt, ...) { #if 0 va_list args;

root@kitploit:~
    va_start(args, fmt);
    sshlogv(file, func, line, showfunc, SYSLOG_LEVEL_FATAL,
        suffix, fmt, args);
    va_end(args);

#endif _exit(1); }

Por fim, se o sshd não puder ser atualizado ou recompilado, esta condição de corrida do manipulador de sinais pode ser corrigida simplesmente definindo LoginGraceTime para 0 no arquivo de configuração. Isto torna o sshd vulnerável a uma negação de serviço (o esgotamento de todas as conexões MaxStartups), mas torna-o seguro contra a execução remota de código apresentada neste advisory.

======================================================================== Agradecimentos

Agradecemos aos desenvolvedores do OpenSSH pelo seu excelente trabalho e estreita colaboração nesta versão. Agradecemos também à distros@openwall. Por fim, dedicamos este advisory a Sophia d'Antoine.

======================================================================== Linha do tempo

2024-05-19: Contatamos os desenvolvedores do OpenSSH. Seguiram-se iterações sucessivas de patches e revisões de patches.

2024-06-20: Contatamos a distros@openwall.

2024-07-01: Data de divulgação coordenada.

Baixar ferramenta
~8KB
320B
~8KB
320B