
CVE-2026-39259
Projeto: https://github.com/alexfru/SmallerC
A implementação de scanf do SmallerC não impõe um limite superior às leituras de string quando %s ou %[ é usado sem uma largura de campo explícita na string de formato. O runtime continua gravando no buffer de destino até encontrar espaço em branco ou EOF, independentemente do tamanho real alocado do buffer. Quaisquer bytes além do limite vão diretamente para a pilha, sobrescrevendo o que o compilador colocou acima do buffer — variáveis locais, registradores salvos, endereço de retorno.
Esta não é uma classe de vulnerabilidade nova. Leituras de string sem limite no scanf são documentadas desde os primórdios da linguagem C, e qualquer ferramenta de análise estática competente vai sinalizá-las. O que torna isso digno de relato no contexto específico do SmallerC é o ambiente-alvo. O SmallerC é projetado para DOS e alvos embarcados bare-metal — plataformas que, por definição, não fornecem canários de pilha, ASLR, bits NX ou qualquer uma das mitigações que dificultam a exploração em sistemas modernos. A mesma primitiva que exigiria um esforço de pesquisa significativo para se tornar um exploit funcional em um binário Linux endurecido torna-se consideravelmente mais tratável em um programa DOS executado em uma pilha plana e previsível.
O buffer tem 16 bytes. A entrada tem 20 bytes sem espaços em branco. A conversão %s não tem especificador de largura, então sscanf lê todos os 20 bytes mais um terminador nulo — 21 bytes no total — para uma alocação de 16 bytes. Os 5 bytes além do limite corrompem a memória de pilha adjacente. O que exatamente é corrompido depende das decisões de layout de pilha do compilador para aquela função específica, mas a sobrescrição em si é determinística e incondicional toda vez que esse caminho de código é executado com essa entrada.
Prova de Conceito
#include <stdioh>
#include <stringh>
/*
* Build with SmallerC targeting DOS or bare-metal
* Demonstrates unbounded %s write past a fixed stack buffer
*
* buffer is 16 bytes payload is 20 non-whitespace bytes
* sscanf writes 21 bytes (20 + null terminator) into buffer
* corrupting 5 bytes of adjacent stack memory
*
* To observe the corruption inspect stack memory after the call:
* the 5 bytes immediately above buffer will contain 'A' (0x41)
*/
int main() {
char buffer[16];
char canary[8];
memset(buffer 0x00 sizeof(buffer));
memset(canary 0xCC sizeof(canary)); /* marker to detect overwrite */
printf("[*] canary before: ");
for (int i = 0; i < 8; i++) printf("%02x " (unsigned char)canary[i]);
printf("\n");
sscanf("AAAAAAAAAAAAAAAAAAAA" "%s" buffer); /* 20 bytes into 16-byte buffer */
printf("[*] canary after: ");
for (int i = 0; i < 8; i++) printf("%02x " (unsigned char)canary[i]);
printf("\n");
if (memcmp(canary "\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC" 8) != 0)
printf("[!] stack corruption confirmed canary overwritten\n");
else
printf("[-] canary intact (stack layout placed it elsewhere)\n");
return 0;
}
Saída esperada em um build afetado:
[*] canary before: cc cc cc cc cc cc cc cc
[*] canary after: 41 41 41 41 41 cc cc cc
[!] stack corruption confirmed canary overwritten
A posição do canário em relação ao buffer depende do layout de pilha do compilador. Se a saída mostrar o canário intacto, a sobrescrição ainda está ocorrendo — ela está atingindo outra coisa acima do buffer. Ajuste o reprodutor inspecionando o stack frame real com um depurador para localizar onde os 5 bytes corrompidos caem.
Uma correção que vale a pena fazer explicitamente: alguns relatos nesta classe de vulnerabilidade tentam demonstrar controle do endereço de retorno anexando um endereço-alvo após um byte nulo no payload, como "AAAAAAAAAAAAAAAAAAA\x00\x90\x04\x08". Isso não funciona. A conversão %s no sscanf trata \x00 como um terminador de string e interrompe a leitura imediatamente ao encontrá-lo. Os bytes após o byte nulo nunca são processados. Demonstrar controle real do endereço de retorno exige entregar a sobrescrição sem um byte nulo na porção crítica do payload, o que, por sua vez, exige conhecer o layout exato da pilha do binário alvo — a distância do buffer até o endereço de retorno salvo, se o compilador inseriu qualquer padding e quais restrições de alinhamento se aplicam. Nada disso surge automaticamente deste reprodutor.
O que o reprodutor estabelece com clareza é a própria primitiva de corrupção. A gravação fora dos limites é real, reproduzível e não depende de nenhuma condição de corrida ou timing. Em um alvo DOS ou embarcado, onde o layout da pilha é estático e previsível entre builds, ir dessa primitiva para um exploit funcional é um esforço de pesquisa realista, e não um exercício teórico.
O cenário afetado é restrito, mas não forjado. Um programa precisa ser compilado com o SmallerC, usar análise da família scanf com um especificador %s ou %[ sem limite, gravar em um buffer de pilha de tamanho fixo e aceitar entrada de uma fonte que o atacante possa influenciar. Todas as quatro condições precisam ser verdadeiras simultaneamente. Programas que usam larguras de campo corretas — %15s para um char[16] — não são afetados. Programas que não analisam entrada controlada pelo atacante não são afetados. O problema é um defeito na forma como o runtime do SmallerC lida com a restrição de largura ausente, mas só se torna uma preocupação de segurança quando o código da aplicação expõe esse defeito a entrada não confiável.
No lado da aplicação, a correção é direta: especifique uma largura de campo que deixe espaço para o terminador nulo — %15s para um buffer de 16 bytes, %63s para um buffer de 64 bytes. Isso é prática padrão em C e totalmente suportado pela sintaxe da string de formato. No lado do projeto SmallerC, o trabalho mais duradouro é adicionar testes de regressão que cubram tanto o comportamento limitado quanto o ilimitado de %s e %[ em scanf, sscanf e fscanf, verificando se larguras de campo explícitas são realmente respeitadas na implementação e documentando o padrão inseguro de forma proeminente. Um diagnóstico em nível de compilador que alerte quando %s ou %[ aparecer sem uma largura de campo em um literal de string de formato preveniria proativamente essa classe de erro e seria uma adição significativa ao toolchain.
A severidade é Média quando entrada externa alcança o caminho de código vulnerável. Cai para Baixa quando a entrada é local ou não privilegiada. O ambiente-alvo — especificamente a ausência de mitigações modernas de exploração nas plataformas pretendidas do SmallerC — é o que separa isto de um aviso genérico de "não use scanf sem limite" e torna relevante relatar no nível do projeto, em vez de tratar puramente como uso indevido na camada de aplicação.
Crédito: Yousif Wazni