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
CVE-2022-1015-1016 — Tradução para o espanhol dos CVE-2022-1015 e 1016 descobertos e documentados por David. | Kitploit
Ferramentas/GitHubGitHub/zanezhub/cve-2022-1015-1016
Escalada de PrivilégiosAnálise de VulnerabilidadesExploraçãoAprendizado e EducaçãoExploração de Binários
GitHubzanezhub/cve-2022-1015-1016

CVE-2022-1015-1016

Tradução para o espanhol dos CVE-2022-1015 e 1016 descobertos e documentados por David.

Ver Repositório
1há 4 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
Site

CVE-2022-1015 & CVE-2022-1026

Este README.md é uma tradução do blog do David. David encontrou os CVEs 1015 e 1016 no kernel do Linux. Você pode visitar a página web dele para ler o documento original.

Aqui estão suas redes sociais:

  • Twitter
  • Github

Uma análise das duas novas vulnerabilidades do Linux em nf_tables

Publicado em 2 de abril de 2022.

  • CVE-2022-1015 permite realizar um acesso out-of-bounds (fora dos limites) causado por validações insuficientes de argumentos de entrada, pode resultar em execução remota de código e em escalada de privilégio local.
  • CVE-2022-1016 está relacionado a uma inicialização pobre das variáveis alocadas na stack, o que pode ser usado para vazar uma ampla variedade de dados do kernel para o espaço do usuário (userspace).

Esses problemas deveriam ser exploráveis nas configurações padrão da versão mais recente do Ubuntu e do RHEL. Escrevi minha prova de conceito (PoC) do CVE-2022-1015 tendo como alvo a versão do kernel 5.16-rc3 do Arch Linux.

Este documento é direcionado a pessoas que tenham um conhecimento básico do kernel do Linux em termos de funcionalidade e segurança. Tentei tornar este documento amigável para pessoas que não tenham conhecimento com a stack de rede para torná-lo acessível a todos.

Aqui está um guia de leitura:

  • Se você está aqui simplesmente para ler sobre a vulnerabilidade, comece na Seção 4
  • Se você também quer um pouco de contexto sobre o subsistema do kernel, comece com a Seção 2
  • Se você está interessado em um pouco mais de contexto adicional, leia todo o documento

1. Contexto

Em meados de fevereiro, o programa de segurança do Google anunciou que continuaria seu programa de recompensas kCTF, oferecendo recompensas que vão de US$ 31.337 até 91.337 dólares por um exploit no kernel do Linux que possa escalar privilégios para o usuário root a partir de processos sem privilégios em um sandbox do nsjail.

Sendo um pobre estudante, obviamente isso chamou minha atenção. Esta era a minha primeira vez procurando uma vulnerabilidade do "mundo real", mas em minhas aventuras jogando CTF com minha equipe, me familiarizei com o kernel do Linux em termos de segurança. Depois de horas e horas com muito pouco ou quase nenhum progresso (mas com maior conhecimento sobre Linux) consegui encontrar algumas vulnerabilidades no módulo nf_tables.

Tristemente, no final do dia, percebi que este módulo não estava presente nas regras do kCTF do Google (portanto, não consegui nenhuma recompensa por essas duas vulnerabilidades). Mas obviamente, ainda assim as reportei e escrevi um exploit LPE (Escalada de Privilégios Local) para o CVE-2022-1015.

1.1 Identificando o objetivo e a estratégia de auditoria

Bem, então você decidiu que vai encontrar algumas vulnerabilidades no Linux. E agora? O Linux é um projeto gigantesco, e é bastante fácil não conseguir ver a floresta por causa das árvores (você foca tanto nos detalhes que perde a visão do que é realmente importante, não tem uma visão geral da situação). Para piorar as coisas, muitas partes não são documentadas e você precisa ler um monte de código para entender o que está acontecendo.

Eu comecei tentando ter uma perspectiva detalhada do modelo de segurança do Linux. Encontrar um bug é uma coisa; mas encontrar um bom bug é outra bem diferente. Afinal, nem todos os bugs são criados iguais:

  • Se um bug requer privilégios root, não existe um limite de segurança significativo (a menos que a kernel module signing esteja ativada)
    • Algumas coisas que vêm à mente são muitos dos módulos dos sistemas de arquivos (virtuais). Apenas o usuário root inicial pode montar esses sistemas de arquivos. A exceção está em vfe que especifica FS_USERNS_MOUNT, nesse caso você pode montá-los no user namespace.
  • Se um bug não pode ser acessado através de chamadas de sistema, provavelmente não será explorável.
    • Isso se aplica a muitos drivers de hardware, já que você não tem acesso físico à máquina. Os drivers de rede de baixo nível ainda podem ser um bom alvo se você puder, p. ex., enviar dados via bluetooth ou 802.11.ac.
    • Obviamente isso depende do cenário em que você se encontra.
  • Muitos bugs requerem CAP_SYS_ADMIN ou CAP_NET_ADMIN.
    • Os user namespaces (espaços de nomes) estão ativados por padrão, então isso não é um problema.
    • Caso contrário, primeiro você terá que fazer uma escalada de privilégios para o namespace (espaço de nomes) do usuário root dentro de um contêiner.
  • Nem todos os módulos estarão presentes no seu alvo.
    • O Linux é um pedaço de software excepcionalmente altamente configurável, portanto todas as configurações podem variar de uma grande quantidade de formas.
    • A configuração do kernel geralmente pode ser acessada em /proc/config.gz. Os módulos podem ser carregados como (=m) ou compilados separadamente e carregados em tempo de execução (=y).

Essas restrições nos ajudam a conhecer os limites dos sistemas de arquivos nos quais podemos procurar vulnerabilidades. Acho que é uma boa ideia você dedicar seu tempo tentando planejar seu ataque ao alvo desejado.

Já aprendi minha lição sobre o ponto anterior. Como mencionei, o módulo nf_tables não estava carregado na instância que nos foi apresentada pelo kCTF. Eu poderia ter percebido isso desde o início e ter poupado a decepção :p. Por outro lado, provavelmente você não estaria lendo este blog agora se eu tivesse percebido antes, suponho que as coisas acabaram dando certo no final.

Uma explicação para o COS, fork do Linux otimizado para contêineres do Google, não ter nf_tables pode ser encontrada aqui e aqui.

1.2 nf_tables: por quê?

Depois de avaliar os pontos mencionados anteriormente, decidi que minha melhor rota para começar provavelmente seria olhar o código fonte da rede. Muitas das funcionalidades interessantes lá precisam de CAP_NET_ADMIN, mas como mencionei, isso na verdade não é um problema. Pelo contrário, suspeito que os componentes que requerem capacidades especiais são geralmente menos seguros, pois os desenvolvedores do kernel podem ter uma falsa sensação de segurança.

Também fiz o esforço de escolher o sistema de arquivos do qual queria saber mais; dessa forma, mesmo que você não encontre nenhum bug, ainda assim poderá aprender um monte de coisas interessantes.

Investiguei muitos sistemas de arquivos de rede, mas não encontrei nada importante. Depois de navegar pelo subdiretório net/, me deparei com o módulo nf_tables. Parecia um pouco complexo, então decidi dedicar um tempo para aprender sobre ele.

2. Introdução ao netfilter

Netfilter (net/netfilter) é um subsistema de arquivos de rede bastante grande no kernel. Em resumo, o netfilter coloca hooks através dos módulos de rede que outros módulos podem registrar manipuladores. Quando um hook é alcançado, o controle é delegado a esses manipuladores, e eles podem operar com a respectiva estrutura de pacotes de rede. Os manipuladores podem aceitar, descartar e modificar pacotes.


4. CVE-2022-1015

Depois de algumas horas navegando pela API do nf_tables (net/netfilter/nf_tables_api.c) para começar a entender como ela funciona exatamente, decidi dar uma olhada na validação lógica dos registros que o usuário envia, e encontrei alguns comportamentos suspeitos. Depois de pensar se estava ficando louco ou não, escrevi um pequeno PoC (prova de conceito) para tentar ativar a vulnerabilidade que encontrei: uma vulnerabilidade conhecida como OOB ou fora dos limites, que permite ler e escrever na memória stack.

Depois de encontrar uma maneira de vazar os endereços do kernel, tomar controle do ponteiro de memória foi bastante fácil. Depois de um pouco de ROP (programação orientada a retorno), e a shell com privilégios root se tornou realidade.

4.1 Root

Cada vez que a rotina init de uma expressão precisa analisar um registro de uma mensagem de usuário do netlink, a rotina nft_parse_register_load ou nft_parse_register_store é chamada dependendo se é um registro fonte ou um registro de destino. Adicionei alguns comentários:```c int nft_parse_register_load(const struct nlattr *attr, u8 *sreg, u32 len) {

root@kitploit:~
/* Given a netlink attribute and the length
 * that is required to read the requested data,
 * write a register index to `sreg` or return
 * an error on failure. */

u32 reg;
int err;


reg = nft_parse_register(attr);
err = nft_validate_register_load(reg, len);
if (err < 0)
    return err;

/* Write resulting index to the nft_expr.data structure. */
*sreg = reg;
return 0;

}


static unsigned int nft_parse_register(const struct nlattr attr) { / Convert a register to an index in nft_regs */

root@kitploit:~
unsigned int reg;

/* Get specified register from netlink attribute */
reg = ntohl(nla_get_be32(attr));

switch (reg) {
/* If it's 0 to 4 inclusive,      
 * it's an OG 16-byte register and we need to
 * multiply the index by 4 (4*4=16) */
case NFT_REG_VERDICT...NFT_REG_4:
    return reg * NFT_REG_SIZE / NFT_REG32_SIZE;

/* Else we subtract 4, since we need to account
 * for the OG registers above. */
default:
    return reg + NFT_REG_SIZE / NFT_REG32_SIZE - NFT_REG32_00;
}

/* So supplied values of 1, 2, 3, 4 map to
 * OG 16-byte registers, with indices 4, 8,
 * 12, 16
 * Supplied values of 5, 6, 7 overlap the verdict,
 * 8,9,10,11   overlap with OG register 1
 * 12,13,14,15 overlap with OG register 2
 * etc. */

}


static int nft_validate_register_load(enum nft_registers reg, unsigned int len) { /* We can never read from the verdict register, * so bail out if the index is 0,1,2,3 */ if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE) return -EINVAL;

root@kitploit:~
/* Invalid operation, bail out */
if (len == 0)
    return -EINVAL;

/* If there would be an OOB access whenever
 * `reg` is taken as index and `len` bytes are read,
 * bail out.
 * sizeof_field(struct nft_regs, data) == 0x50 */
if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data)) 
    return -ERANGE;

return 0;

}

root@kitploit:~
As variantes `*_store` são virtualmente idênticas, exceto que permitem escrever no *verbdict* sob algumas condições.

Depois de revisar a última validação, algo está realmente fora do lugar aqui:```c
if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data))

Isto parece ser um estouro de inteiro, não acham? Se podemos fazer com que reg contenha algum valor multiplicado multiplicado por 4 que gere um overflow quando somado a len, podemos satisfazer as condições. Em nft_parse_register_load, o último byte valioso de reg ainda é escrito ao ponteiro u8 *sreg, caindo em nosso nft_expr que é usado posteriormente como um índice.```c *sreg = reg;

root@kitploit:~
¿De verdade podemos? `reg` é um `enum nft_registers` na validação da rotina, de qualquer forma. Podemos passar valores que tenham um intervalo entre `0x00000001` até `0xfffffffb` inclusive, o intervalo de `nft_parse_register`; mas será que `reg` é um valor de 32 bits em `nft_validate_register_load`? Sabe-se que os compiladores podem reduzir os *enum types* se um tipo menor pode representar todos os valores. Vamos obter uma segunda opinião.

Obtido do manual do GCC:```
The integer type compatible with each enumerated type (C90 6.5.2.2, C99 and C11 6.7.2.2).
Normally, the type is unsigned int if there are no negative values 
in the enumeration, otherwise int. If -fshort-enums is specified,
then if there are negative values it is the first 
of signed char, short and int that can represent all the values, 
otherwise it is the first of unsigned char, unsigned short and unsigned int 
that can represent all the values.

On some targets, -fshort-enums is the default; this is determined by the ABI.

TL;DR? Depende da ABI e do possível grau de otimização. Não consegui encontrar nenhuma evidência concreta se esta opção está ativada por padrão nas builds do Linux.

Mas o assembler nunca mente. Vamos dar uma olhada:```objdump.x86asm 0000000000001b60 <nft_parse_register_load>: 1b60: e8 00 00 00 00 call 1b65 <nft_parse_register_load+0x5> 1b65: 55 push rbp 1b66: 8b 47 04 mov eax,DWORD PTR [rdi+0x4] 1b69: 0f c8 bswap eax 1b6b: 89 c7 mov edi,eax 1b6d: 8d 48 fc lea ecx,[rax-0x4] 1b70: c1 e7 04 shl edi,0x4 1b73: 48 89 e5 mov rbp,rsp 1b76: c1 ef 02 shr edi,0x2 1b79: 83 f8 04 cmp eax,0x4 1b7c: 89 f8 mov eax,edi 1b7e: 0f 47 c1 cmova eax,ecx 1b81: 85 d2 test edx,edx 1b83: 74 13 je 1b98 <nft_parse_register_load+0x38> 1b85: 83 f8 03 cmp eax,0x3 1b88: 76 0e jbe 1b98 <nft_parse_register_load+0x38> 1b8a: 8d 14 82 lea edx,[rdx+rax*4] 1b8d: 83 fa 50 cmp edx,0x50 1b90: 77 0d ja 1b9f <nft_parse_register_load+0x3f> 1b92: 88 06 mov BYTE PTR [rsi],al 1b94: 5d pop rbp 1b95: 31 c0 xor eax,eax 1b97: c3 ret
1b98: b8 ea ff ff ff mov eax,0xffffffea 1b9d: 5d pop rbp 1b9e: c3 ret
1b9f: b8 de ff ff ff mov eax,0xffffffde 1ba4: 5d pop rbp 1ba5: c3 ret

root@kitploit:~
As chamadas de funções estão alinhadas bastante bem. As operações importantes estão em `1b8a`:```objdump.x86asm
lea    edx, [rdx+rax*4]
cmp    edx, 0x50
ja     1b9f <nft_parse_register_load+0x3f>
mov    BYTE PTR [rsi], al

rax é o resultado de ntf_parse_register, rdx é len fornecida, e rsi é o ponteiro sreg. Já nos livramos das dúvidas.

nft_parse_register_store mostra o mesmo comportamento. Enquanto os registros viverem na stack, nossa vulnerabilidade OOB obviamente será relativa à stack. Isso é bom, porque com um pouco de sorte, poderemos sobrescrever e retornar memória diretamente.

Para dar um exemplo de uma entrada vulnerável, um registro de 0xfffffffb e um comprimento de 0x20, vai avaliar 0xfffffffb * 4 + 0x20 = 0x0c < 0x50. Após a validação (u8)0xfffffffb = 0xfb será escrito em *sreg.

Embora haja um problema: existem expressões que nos permitam usar um comprimento que possa causar um overflow quando a soma for realizada? Depois de um pouco de pesquisa, encontrei que nft_bitwise e nft_payload permitem que você forneça seu próprio comprimento, de 0x00 até 0xff. Muitas outras expressões parecem ter comprimentos estáticos que são muito pequenos.

Por enquanto isso parece promissor. O próximo passo é pegar essas exploit primitives (capacidade genérica ganha durante um exploit) e usá-las.

4.2 Examinando as exploit primitives

Se pudermos definir o tipo de poder que nosso exploit pode nos dar, explorar essa vulnerabilidade deve ser mais fácil. Então, me dê um pouco da sua paciência porque vamos ver um pouco de aritmética.

Há três pontos que podemos usar para nosso overflow para a multiplicação de registro, já que isso é multiplicado por 4 = 2^2: 2^32 - 1, 2^31 - 1 e 2^30 - 1 (respectivamente 0xffffffff, 0x7fffffff, e 0x3fffffff). Esses valores podem ir diminuindo até que somemos nosso máximo comprimento permitido, depois de ser multiplicada por quatro isso não resultará em um overflow. Outro ponto a se levar em conta é que não podemos usar valores maiores que 0xfffffffb, como mencionado anteriormente.

Dado um comprimento específico, os valores de byte menos significativos que podem permitir um overflow usando esse comprimento formarão nosso intervalo de índices OOB que podemos usar.

Afinal, não importa quais pontos de overflow sejam usados. Tome por exemplo os seguintes valores com um LSB (bit menos significativo) de 0xf0:``` 0xfffffff0 * 4 = 0xffffffc0 0x7ffffff0 * 4 = 0xffffffc0 0x3ffffff0 * 4 = 0xffffffc0

root@kitploit:~
De agora em diante, vamos usar valores de registro próximos a `0x7fffffff`.

Anteriormente falamos sobre `nft_payload` e `nft_bitwise`. Algumas propriedades dessas expressões são:

* `nft_payload` só pode realizar escritas *OOB*, enquanto `nft_bitwise` pode realizar escritas e leituras *OOB*.

* `nft_payload` pode fazer escritas *OOB* de até 0xff bytes de dados arbitrários.

* `nft_bitwise` na verdade só pode escrever até `0x40` bytes de dados arbitrários e pode ler apenas `0x40` bytes de dados que estejam na *stack* do espaço do registrador.
  
  * `nft_bitwise` requer um `sreg` e um `dreg`, os quais precisam passar pela validação com o mesmo valor de comprimento.
  
  * Temos apenas `0x40` bytes de espaço de registro, então queremos ler ou escrever do espaço de registro, mas não podemos passar pela validação com um comprimento maior que `0x40`.

Podemos usar um valor de comprimento maior para `nft_bitwise`, mas isso significa que `sreg` e `dreg` precisam estar fora dos limites, o que não seria muito útil para nossos propósitos. Então, por enquanto trabalharemos com o comprimento de `0x40`.

Tendo tudo isso em conta, que tipos de *exploits* poderemos usar?

`nft_bitwise` tem um comprimento máximo de `0x40`. Isso significa que o valor do registro multiplicado por quatro deve ser pelo menos `0xffffffc0`. O maior valor que podemos obter multiplicando por quatro é `0xfffffffb`, e como `0xfffffffb + 0x40 = 0x3b <= 0x50` isso passará na validação.

`0x7ffffff0 * 4 = 0xffffffc0`: o limite inferior é `0xf0`.
`0x7fffffff * 4 = 0xfffffffb`: o limite superior é `0xff`.

Traduzindo para [*byte offsets*](https://es.wikipedia.org/wiki/Offset_(inform%C3%A1tica)):```
0xc1 * 4        = 0x304
0xeb * 4 + 0xff = 0x4ab

nft_payload pode escrever fora dos limites através dos offsets [0x304, 0x4ab] desde struct nft_regs.

Agora que tudo isso está esclarecido, o que está realmente na stack nestes offsets?

A rotina nft_do_chain pode ser chamada através de muitos caminhos de código. Existem muitos fatores que mudarão a forma da stack antes do stack frame (quadro da stack) de nft_do_chain:

  • Se o chain hook for um input ou output.

    • Se tivermos um chain hook configurado como input, o hook será ativado no contexto softirq do dispositivo de rede respectivo com a stack softirq.
    • Se tivermos um chain hook configurado como output, o hook será ativado no contexto de syscall (chamada de sistema) send* com a stack de syscall.
  • O protocolo que estamos usando.

    • Enviar um pacote IP bruto terá um call stack bastante diferente de p. ex. um pacote UDP.

Acredito que você pode obter muitas variações de call stacks usando diferentes combinações de protocolos, interfaces e localizações de hooks. Por enquanto estaremos usando um chain hook configurado como um output com um pacote UDP.

Diagrama del stac con output y UDP

Design da stack e os alcances fora dos limites em nft_do_chain quando um pacote UDP enviado alcança um hook configurado para output

4.3 Filtragem de informações de um canal lateral (side-channel)

Para poder criar um exploit estável primeiro teremos que filtrar o endereço da imagem do kernel.

O endereço da imagem do kernel tem 9 bits de entropia (medida da incerteza existente diante de um conjunto de mensagens, do qual vai ser recebido apenas um), o que significa que há 512 posições diferentes nas quais o kernel pode ser carregado. Dependendo do cenário do seu ataque, há uma probabilidade de 1 em 512 de conseguir que o ataque funcione corretamente; mas seria melhor se pudéssemos conseguir um exploit mais estável disso.

O passo mais simples é tentar usar nossa capacidade de leitura fora dos limites que nos conseguiu nft_bitwise para copiar alguns dos dados da stack para nossos registradores. Já que o intervalo total que podemos ler tem um comprimento de 0x7c bytes, existe uma probabilidade bastante boa de que o endereço do kernel esteja lá.

Alcance fora dos limites de nft_bitwise

Alcance fora dos limites de nft_bitwise

Hoje é o nosso dia! Existem dois:``` gef➤ x/bx 0xffffffff815b49c1 0xffffffff815b49c1 <import_iovec+49>: 0xc9 gef➤ x/bx 0xffffffff819ac3ec 0xffffffff819ac3ec <copy_msghdr_from_user+92>: 0xba

root@kitploit:~
Escrever isso nos registradores é uma coisa, mas extraí-los é outra. Após investigar, parece que não existe uma forma fácil de ler diretamente os registradores quando `nft_do_chain` está em execução.

No meu relatório original para [email protected], fui informado de que a expressão `nft_dynset` por um mantenedor do netfilter, que tem suporte para [*dynamic sets*](https://en.wikipedia.org/wiki/Dynamic_set) que podem atuar como uma espécie de banco de dados que pode escrever e ler através de diferentes execuções de `nft_do_chain`. Aparentemente, o `nft_payload` também tem a capacidade de escrever no próprio pacote, eu não percebi isso.

Em vez disso, decidi continuar com meu [*side-channel attack*](https://es.wikipedia.org/wiki/Ataque_de_canal_lateral). Devido à natureza do `nf_tables`, você pode causar efeitos colaterais. Na verdade, você poderia dizer que nem sequer são efeitos colaterais, mas efeitos primários.

Criando regras que descartam ou aceitam o pacote com base no valor do endereço de memória do kernel que estamos copiando, aos poucos podemos deduzir qual é o valor examinando se os pacotes que enviamos também foram recebidos.

1. Criar um *socket* UDP que recebe pacotes em `127.0.0.1:9999`:
* Deveria receber pacotes em uma thread diferente.

* Uma mensagem deve ser enviada de volta para cada pacote que receber.
2. Adicione uma regra que:
   
   1. Copia o endereço do kernel para os registradores com `nft_bitwise`.
   
   2. Usa `nft_cmp_expr` para comparar o endereço com uma constante.
   
   3. Descartar um pacote se a comparação avaliada for verdadeira.

3. Envie um pacote UDP para `127.0.0.1:9999`
   
   1. Podemos determinar um pouco de informação sobre o endereço do kernel baseado em se recebemos uma mensagem de volta.

4. Repita 2 e 3 com os valores adequados até que você tenha informação suficiente para determinar a informação por si só.

![](https://assets.kitploit.com/production/public/readmes/35022/71fabf101d1c962c5434bff7babccd3f294cd3d278765f1b03121429c5417b12.png)

Ainda existem algumas ressalvas. Por exemplo, o pacote que recebemos também poderia ser descartado sem nenhum aviso prévio. Para mitigar isso, podemos adicionar uma redução de ruído, para a qual precisaremos de uma *base chain* e uma *auxiliary regular chain*.

*Rule in base chain:*

| #   | Expressão            | Argumentos                                                                                               | Comentário                                                                                                 |
| --- | -------------------- | -------------------------------------------------------------------------------------------------------- |:---------------------------------------------------------------------------------------------------------- |
| 0   | `nft_payload`        | base=NFT_PAYLOAD_TRANSPORT_HEADER<br/>offset=offsetof(udphdr, dport)<br/>len=sizeof_field(udphdr, dport) | Escrever a porta de destino do pacote no registrador 8.                                                   |
| 1   | `nft_cmp_expr`       | op=NFT_CMP_EQ<br/>sreg=8<br/>data=9999                                                                   | Comparar a porta de destino com `9999`, e retornar `NFT_BREAK` se o resultado não for igual.                  |
| 2   | `nft_payload`        | base=NFT_PAYLOAD_INNER_HEADER<br/>offset=0<br/>len=8                                                     | Escrever os primeiros oito bytes do pacote no registrador 8.                                                |
| 3   | `nft_cmp_expr`       | op=NFT_CMP_EQ<br/>sreg=8<br/>data=0xdeadbeef0badc0de                                                     | Comparar os primeiros oito bytes com o valor mágico, e retornar `NFT_BREAK` se não for igual.                   |
| 4   | `nft_immediate_expr` | verdict=NFT_JUMP<br/>chain=aux_chain                                                                     | Já que a regra ainda está avaliando, as condições devem coincidir, e chamar nossa *auxiliary chain*. |

*Rule in auxiliary chain:*

| #   | Expressão       | Argumentos                                                              | Comentário                                                                                                                                                                                  |
| --- | --------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0   | `nft_bitwise`   | op=NFT_BITWISE_RSHIFT<br/>data=SHIFT_AMT<br/>dreg=OOB_OFFSET<br/>sreg=8 | Escreve o endereço do kernel nos registradores usando a leitura fora dos limites, deslocado pelos bits de `SHIFT_AMT` para obter o byte do endereço desejado no registrador correto. |
| 1   | `nft_cmp`       | op=NFT_CMP_GT<br/>sreg=ADDRESS_OFFSET<br/>data=COMPARAND                | Comparar o byte do endereço do kernel com `COMPARAND`, retornar `NFT_BREAK` se este resultado não for igual.                                                                           |
| 2   | `nft_immediate` | verdict=NFT_DROP                                                        | Descartar o pacote se o byte do endereço for maior que `COMPARAND`.                                                                                                                       |

Revisando a porta de destino e comparando os primeiros oito bytes internos do *header* com um valor mágico, podemos ativar os efeitos colaterais para os pacotes que quisermos.

Mudando dinamicamente `COMPARAND` podemos fazer uma busca binária para encontrar o byte do endereço do kernel em tempo `O(log(n))`. Mudando dinamicamente `SHIFT_AMT` para os próximos múltiplos de oito podemos nos mover para o próximo byte de memória e começar de novo.

#### 4.3.1 Filtrar pseudocódigo

Um pouco de código em Python para filtrar o endereço de memória. O engraçado é que eu facilmente poderia ter implementado isso em Python. Lembrem-se de que nem sempre vocês precisam fazer seus exploits para um kernel em C :p```python
'''
Asumimos que un hilo secundario está recibiendo
paquetes UDP en 127.0.0.1:9999 y todo lo relacionado
con nf_tables ya está configurado
p. ej. table, base y auxiliary chain
'''

def leak_byte(pos):
    s = socket.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
    s.settimeout(200) # 200ms debería ser más que suficiente
    s.bind(("127.0.0.1", 1234))

    # buscar los límites
    low = 0, high = 255

    while True:
        mid = (low + high) // 2

        # si encontramos el valor, lo regresamos 
        if low == high:
            s.close()
            return mid

        set_leak_rule(SHIFT_AMT=pos*8, COMPARAND=mid)

        # Enviar el paquete y activar la auxiliary chain
        s.sendto(pack(0xdeadbeef0badc0de), ("127.0.0.1", 9999))

        # El hilo secundario regresa a 127.0.0.1:1234
        res = s.recvfrom(0x2000)

        if not res:
            '''
            nuestro paquete fue soltado
            ya que no se regresó nada en los 200ms
            lo que significa que 

            byte to leak >= mid
            el byte a filtrar es mayor o igual a mid (127)
            '''
            low = mid
        else:
            '''
            [sanity check o prueba de cordura]

            se usa para evaluar rápidamente si 
            el valor a calcular es siquiera posible

            https://es.wikipedia.org/wiki/Prueba_de_cordura
            '''

            if res != b"MSG_OK":
                print("Something went wrong")
                return None

            '''
            Nuestro paquete fue aceptado, lo que
            significa que 

            byte to leak < mid
            byte a filtrar es menor a mid (127)
            '''
            high = mid - 1

leak_bytes = lambda: [leak_byte(i*8) for i in range(4)]

4.4 Execução arbitrária de código (Arbitrary code execution)

Agora que conseguimos o leak, a execução arbitrária de código deve ser muito fácil. A escrita fora dos limites de nft_payload deveria poder escrever um ataque RoP em cadeia para a stack, certo?

Não. Não tivemos muita sorte, pelo menos neste kernel em particular. A escrita fora dos limites de nft_payload quase na sua totalidade se alinha com o stack frame da rotina de udp_sendmsg. O endereço de udp_sendmsg está no offset +0x2f8 relativo aos registradores, esta localização é muito baixa para ser alcançada com nft_payload ou nft_bitwise (podemos começar a escrever começando no offset +0x304, tão perto...). O endereço inet_sendmsg está localizado no offset +0x4a8. Tecnicamente podemos alcançá-lo (e sobrescrever os três bytes inferiores), mas há um stack canary (técnica utilizada para detecção de um stack buffer overflow antes que a execução de código malicioso possa acontecer) no endereço +0x0458 que também precisamos sobrescrever para conseguir isso. Isso obviamente faria o kernel travar, então fazer isso não é uma opção.

Consegui usar este método em outra compilação do kernel, mas parece que tentar fazer o mesmo para o kernel que estou usando para este blog será um pouco mais difícil.

Agora, talvez possamos fazer um pouco de contrived stack frame hacking para sobrescrever as variáveis locais em udp_sendmsg. Também poderíamos tentar sobrescrever o verdict chain pointer, usando um valor dos registros p. ex. 0x7fffff00 (acho que isso poderia ser uma técnica legal; levando em conta o desafio).

Vamos tentar mudando a base chain hook que usamos. Estávamos usando uma chain output, e se a mudássemos para uma de input?

O diagrama do alcance fora dos limites em nft_do_chain se um pacote UDP enviado alcança o input hook

Isso parece um pouco melhor! Podemos sobrescrever o endereço de retorno do frame de __netif_receive_skb_one_core (offset +0x328), que retorna para __netif_receive_skb. Como está relativamente perto da altura do alcance fora dos limites do nosso nft_payload, podemos fazer com que nosso índice OOB (alcance fora dos limites) aponte diretamente para este endereço de retorno, evitando o stack canary no offset +0x310. O offset +0x328 se traduz no índice 0xca.

Para ativar a sobrescrita do endereço de retorno, criamos uma nova chain input na tabela e adicionamos a ela uma regra com um nft_payload que escreve 0xff bytes a partir do cabeçalho interno do pacote para o índice 0xca. Depois enviamos um pacote com o payload, e bum.

🥳 🥳 🥳 🥳 🥳

Baixar ferramenta
  • Você pode usar /proc/modules e /proc/kallsyms, mas nem sempre são confiáveis, pois os módulos podem ser carregados dinamicamente no kernel (p. ex. request_module).
  • Se você não tem certeza, escreva um pequeno programa que tente interagir com o módulo.