fbpx

Sony PlayStation Vita (PS Vita) – Trinity: Escape do emulador de PSP

Sony PlayStation Vita (PS Vita) – Trinity: Escape do emulador de PSP

 

* Trinity * é uma exploração totalmente encadeada para o * PS Vita ™ * que consiste em seis vulnerabilidades únicas. É baseado em uma década de conhecimento e pesquisa. O código fonte do * Trinity * pode ser encontrado [aqui] (<https://github.com/TheOfficialFloW/Trinity>).

## Índice

– [Sumário] (# Sumário)
– [Introdução] (# introdução)
– [MIPS Kernel Exploit] (# mips-kernel-exploit)
* [Type Confusion] (# confusão de tipo)
* [Condição de corrida com busca dupla] (# condição de corrida com busca dupla)
– [PSP Emulator Escape] (# psp-emulator-escape)
* [Stack Smash] (# stack-smash)
* [Leitura arbitrária do CSC] (# csc-arbitrary-read)
* [Criando um sistema RPC] (# design-a-rpc-system)
– [ARM Kernel Exploit] (# arm-kernel-exploit)
* [Divulgação da pilha] (# divulgação da pilha)
* [Estouro de pilha] (# estouro de pilha)
– [Pós-exploração] (# pós-exploração)
– [Conclusão] (# conclusão)
– [Créditos] (# créditos)

## Introdução

Como o * PS Vita ™ * é o sucessor do * PSP ™ *, que era o * computador de mão mais popular da época, era natural oferecer compatibilidade com versões anteriores. Portanto, o PS Vita veio com um processador MIPS integrado além do processador ARM principal. Um firmware ligeiramente adaptado do PSP foi usado como software e (felizmente) isso também trouxe falhas de design e vulnerabilidades. Entre outros, foi possível [logon falso em executáveis] (<http://wololo.net/talk/viewtopic.php?f=5&t=1381&sid=b0b8c9563372a67a18946785dffe1f9c>) que permitia a execução de código de usuário sem nenhum esforço. Além disso, como o processador MIPS não tinha acesso direto a dispositivos de hardware, o emulador PSP usou o HLE pelo RPC via [Kermit] (<https://wiki.henkaku.xyz/vita/Kermit>). Isso expôs essencialmente uma superfície de ataque em potencial.

Este artigo apresenta os erros que encontrei neste protocolo. Suas descobertas finalmente me motivaram a encontrar mais bugs para encadear e escalar privilégios no ring0. O resultado final foi uma fuga do emulador PSP da área de usuário do MIPS para o kernel ARM.

## Exploração do kernel do MIPS

A história do cracking de PSP é bem conhecida. Foi a cena homebrew mais ativa e reuniu tantos talentos que trabalharam juntos para liberar a fera. O PSP foi literalmente desmontado, pesquisado e explorado de todas as maneiras possíveis. Eu recomendo que você assista aos mais de [10 anos de conversa do CCC] (<https://media.ccc.de/v/24c3-2209-en-playstation_portable_cracking>) no PSP por nenhum outro homem que o próprio James Forshaw ! Também sugiro que você leia os seguintes slides do [The Naked PSP] (<https://uofw.github.io/upspd/docs/software/naked_psp.pdf>).

### Confusão de tipo

Se você ler os slides, poderá ter notado o seguinte trecho de código:

“ c
void * cntladdr = 0x88000000 + ((uid >> 5) & ~ 3);

Em essência, os UIDs para objetos do kernel são simplesmente codificações de seus endereços de kernel. Se você passar esse UID para alguns syscalls que o decodificarem, ele primeiro fará verificações de integridade e verificará se o formato está correto e, em seguida, trabalhará com o objeto. Essa estrutura é muito fácil de falsificar. Surpreendentemente, ninguém tentou explorar essa falha de design na época e foi explorado pela primeira vez pelo qwikrazor87 por volta de 2015. Um artigo de terceiros pode ser lido [aqui] (<https://github.com/GUIDOBOT/kxploits/blob/ master / (3.50) sceKernelDeleteThread / explanation.txt>).

Em particular, a estrutura de dados UID consiste em metadados, nome, tamanho, etc. e mais importante, mantém uma lista duplamente vinculada que conecta pais e filhos. Quando o objeto UID é excluído, sua entrada é desvinculada da lista da seguinte maneira:

“ c
// https://github.com/uofw/uofw/blob/master/src/sysmem/uid.c#L744
s32 obj_do_delete (SceSysmemUidCB * uid, SceSysmemUidCB * uidWithFunc __attribute __ ((não utilizado)), int funcId __attribute __ ((não utilizado)), va_list ap __attribute __ ((não utilizado))
{
uid-> meta = NULL;
uid-> PARENT0-> nextChild = uid-> nextChild;
uid-> uid = 0;
uid-> nextChild-> PARENT0 = uid-> PARENT0;
uid-> nextChild = uid;
uid-> PARENT0 = uid;
FreeSceUIDnamestr (uid-> nome);
FreeSceUIDobjectCB (uid);
retornar 0;
}

Note que se conseguirmos controlar `uid-> PARENT0` e` uid-> nextChild`, podemos escrever um endereço arbitrário em um local arbitrário.

A exploração desse bug é simples:

1. Plante um objeto UID falso no kernel.
2. Codifique esse objeto UID.
3. Exclua o objeto UID.

Basicamente, o que você pode fazer com essa primitiva é sobrescrever um ponteiro de função no kernel e fazê-lo apontar para alguma função na terra do usuário. Então, podemos invocá-lo e executar nosso código no modo kernel.

Temos que ignorar quaisquer atenuações de segurança? Não, não há! Zero! Não há SMAP / SMEP, KASLR, randomização efetiva, NX, nada! No entanto, isso é compreensível – lembre-se de que este é um dispositivo de 10 anos de idade, tão seguro quanto o PS4 😉

Depois que o qwikrazor87 lançou essa façanha, a Sony, obviamente, não poderia simplesmente mudar todo o design. Em vez disso, eles adicionaram algumas atenuações, como XOR’ing `uid-> uid` com uma semente aleatória, ou detectando que o objeto UID estava dentro da região de heap.
Essas mitigações foram bastante eficazes. Como você teria que plantar 2 ^ 32 objetos UID diferentes para adivinhar com sucesso a semente aleatória. Além disso, os dados de plantio nessa região de heap não eram muito óbvios, pois eram usados ​​apenas pelos internos do kernel.

Depois de tentar várias coisas, notei que quando você aloca um novo objeto UID, seu nome é salvo nessa região de heap. O nome pode ter no máximo 32 caracteres e, felizmente, para nós, esses bytes são suficientes para falsificar com êxito nosso objeto UID no kernel. A única coisa com a qual precisamos nos preocupar é que não podemos usar nenhum caractere NULL nesse objeto falso, caso contrário, não poderemos copiar completamente os dados no kernel. O seguinte trecho de código mostra como isso foi alcançado:

“ c
// Plante a estrutura de dados UID no kernel como string
u32 string [] = {LIBC_CLOCK_OFFSET – 4, 0x88888888, 0x88016dc0, encrypted_uid, 0x88888888, 0x10101010, 0, 0};
SceUID plantid = sceKernelAllocPartitionMemory (PSP_MEMORY_PARTITION_USER, (char *) & string, PSP_SMEM_Low, 0x10, NULL);

Note que `LIBC_CLOCK_OFFSET` é o ponteiro de função que queremos sobrescrever com o endereço 0x88888888`. A única coisa que resta a fazer é determinar o `encrypted_uid`. Felizmente, qwikrazor87 encontrou uma boa exploração de leitura arbitrária.

Condição de corrida de busca dupla

O syscall `sceNpCore_8AFAB4A0 ()` é usado para buscar algumas strings de uma lista. Como argumentos, podemos especificar o índice dessa matriz, o buffer de saída e o comprimento da saída. Veja o código abaixo:

“ c
typedef struct {
int len;
char * string;
} SceNpCoreString;

estático SceNpCoreString * g_00000D98;

static int sub_000005D8 (int * input, char * string, int length) {
int index;

índice = entrada [1];
if (índice> = 9)
retornar 0x80550203;

if (g_00000D98 [index] .len> = comprimento)
retornar 0x80550202;

strcpy (string, g_00000D98 [index] .string);

retornar g_00000D98 [input [1]]. len;
}

int sceNpCore_8AFAB4A0 (int * input, char * string, int length) {

res = sub_000005D8 (entrada, sequência, comprimento);

return res;
}

Observe que todos esses argumentos vêm da área de usuário e não são salvos primeiro em algum buffer do kernel. Além disso, observe que o índice `input [1]` é buscado duas vezes.

Isso abre uma nova possibilidade para um invasor: e se `input [1]` mudar entre o horário da verificação e o tempo de uso (um problema do TOCTTOU)?
Essa janela muito pequena nos permite passar primeiro um índice válido e, logo após a verificação, alterá-lo rapidamente para um índice maior que 9 e obter um acesso fora dos limites. Esta é uma condição de corrida de dupla busca 🙂

“ c
volátil estático int em execução;
volátil estático int idx;
entrada int estática [3];

static int racer (argumentos SceSize, void * argp) {
corrida = 1;

enquanto (em execução) {
entrada [1] = 0;
sceKernelDelayThread (10);
entrada [1] = idx;
sceKernelDelayThread (10);
}

retornar sceKernelExitDeleteThread (0);
}

read_kernel_word u32 estático (endereço u32) {
SceUID thid = sceKernelCreateThread (“”, racer, 8, 0x1000, 0, NULL);
if (thid <0)
retornar 0;

sceKernelStartThread (thid, 0, NULL);

char string [8];
int round = 0;

idx = -83; // deslocamento relativo 0xB00 em np_core.prx (0xD98 + (-83 << 3))

int i;
para (i = 0; i <100000; i ++) {
u32 res = sceNpCore_8AFAB4A0 (entrada, string, sizeof (string));
if (res! = 5 && res! = 0x80550203) {
switch (round) {
caso 0:
rodada = 1;
idx = (endereço – (res – 0xA74) – 0xD98) >> 3;
quebrar;
caso 1:
correndo = 0;
return res;
}
}
}

correndo = 0;
retornar 0;
}

Observe que fazemos a corrida duas vezes. Primeiro, execute para saber onde `g_00000D98` está localizado no kernel (apenas o primeiro módulo` SceSysmem` está no endereço constante 0x88000000, todas as outras bases são aleatórias). Em seguida, na segunda execução, tentamos retornar os dados do endereço desejado. No nosso caso, este é o local da semente aleatória.

Conectando-o à vulnerabilidade anterior, agora podemos executar o código no kernel do MIPS da seguinte maneira:

“ c
semente u32 = read_kernel_word (SYSMEM_SEED_OFFSET);
if (! semente)
retorno -1;

SceUID uid = ((((FAKE_UID_OFFSET & 0x00ffffff) >> 2) << 7) | 0x1;
SceUID encrypted_uid = uid ^ seed;

// Dados da planta

// Substitua o ponteiro da função em LIBC_CLOCK_OFFSET com 0x88888888
sceKernelFreePartitionMemory (uid);

// Salta para a função do kernel
REDIRECT_FUNCTION (0x08888888, função do kernel);

// Executar função do kernel
sceKernelLibcClock ();

Fácil né? 🙂

Escape do emulador PSP

Por que hackear o emulador de PSP de qualquer maneira? Por que não o WebKit? O emulador de PSP é executado com privilégios de sistema que são equivalentes ao root. Ao ganhar controle sobre o emulador, estamos expostos a quase * ALL * syscalls, ao contrário do processo WebKit que está na área de proteção. Da mesma forma, o jailbreak anterior [h-encore] (<https://theofficialflow.github.io/2018/09/11/h-encore.html>) explorou uma vulnerabilidade de gamesave, de forma que poderia invocar os syscalls do NGS.

Como mencionado anteriormente, o processador MIPS tem acesso a dispositivos de hardware pelo HLE. Ele usa o RPC para enviar comandos para o processador ARM, que lida com eles e envia respostas de volta. Algumas pesquisas sobre essa interface de comunicação já haviam sido realizadas em 2012 por [Davee] (<https://www.lolhax.org/2012/03/29/kermit/>).

Naquela época, tentei encontrar maneiras de vazar a memória ARM para o processador MIPS, mas, devido ao fato de eu ter zero informações sobre o código HLE e nenhuma maneira de depurar, desisti rapidamente.

Após o lançamento do [HENkaku] (<http://henkaku.xyz/>), peguei novamente a idéia de escapar do emulador de PSP e escrevi um fuzzer de caixa preta para esse protocolo.

### Stack Smash

O fuzzer, residente no processador MIPS, basicamente escolheu comandos e argumentos aleatórios (números inteiros e ponteiros válidos para buffers que também contêm valores aleatórios) e os enviou pelo protocolo Kermit. Um manipulador de exceção foi instalado no lado do host para capturar possíveis falhas.

Eu rapidamente encontrei várias referências de ponteiro NULL. A maioria ocorreu devido à conversão da memória PSP em memória PS Vita, não tendo sido verificada quanto ao sucesso. Portanto, se um endereço inválido for passado para `ConvertAddress ()`, o servidor RPC continuará trabalhando com o ponteiro NULL.

Essas foram realmente boas notícias, porque se houvesse desreferências simples de ponteiro NULL no código, isso significava que não havia sido auditado o suficiente. Portanto, há grandes chances de haver outros bugs (mais sérios) disponíveis.

Após várias execuções e lista negra de comandos desinteressantes, entrei em pânico em `__stack_chk_fail ()`. Naquele momento eu sabia que venci. Foi um esmagamento de pilha no comando `KERMIT_CMD_ADHOC_CREATE`. Descoberto em 26/05/2018.

Aqui está o código vulnerável:

“ c
typedef struct {
uint8_t mac [6];
canal uint8_t;
uint8_t bufsize;
uint8_t buf [0];
} KermitAdhocCreateParam; // 0x70

static int remoteNetAdhocCreate (KermitAdhocCreateParam * param) {
uintptr_t canary = __stack_chk_guard;

int res;
char buf [0x114];

memset (buf, 0, sizeof (buf));
memcpy (buf + 0x98, param-> buf, param-> bufsize);

if (canário! = __stack_chk_guard)
__stack_chk_fail ();

return res;
}

static int ScePspemuRemoteNet (SceSize args, void * argp) {
uint32_t cmd;
uint64_t res;
Solicitação KermitRequest **; // sp + 0x14

enquanto (1) {
cmd = WaitAndGetRequest (KERMIT_MODE_WLAN, & request);

switch (cmd) {

caso KERMIT_CMD_ADHOC_CREATE:
void * param = (void *) ConvertAddress (request-> args [0], 0x3, 0x70);
res = remoteNetAdhocCreate (param);
WritebackCache (param, 0x70);
quebrar;


}

ReturnValue (KERMIT_MODE_WLAN, solicitação, res);
}

retornar 0;
}

 

Como você pode ver facilmente, o bug está em `memcpy ()` onde o comprimento `param-> bufsize` é passado sem ser validado. Este campo tem 8 bits de largura e, portanto, pode manter o valor 0xff no máximo. Note que `buf` tem 0x114 bytes de tamanho e que` param-> buf` é copiado para compensar 0x98. A matemática simples gera que, copiando mais de 0x7c bytes, podemos sobrecarregar esse buffer e substituir os dados da pilha (principalmente o valor do registro LR).

Quantos bytes podemos controlar após esse buffer? Como `param-> bufsize` pode ter no máximo 0xff, podemos controlar 0x83 bytes após esse buffer, que é espaço suficiente para plantar e executar cadeias ROP.

Mas espere, por que tão confiante? Lembre-se, ainda não temos controle por PC. Simplesmente pressionamos `__stack_chk_fail ()`, o que não leva a lugar nenhum, desde que não encontremos uma maneira de vazar o valor aleatório de `__stack_chk_guard`.

Esta foi provavelmente a parte mais difícil de toda a cadeia e me levou uma semana inteira para resolver. Eu estava procurando por buffers não inicializados na pilha que vazariam o canário da pilha e os endereços de retorno para derrotar o ASLR. Surpreendentemente, eu não consegui encontrar um bug desse tipo e parecia que a Sony fez questão de não esquecer de `memset ()` qualquer buffer lá. Após uma semana inteira de engenharia reversa e escavação em todos os comandos, encontrei um bug muito interessante que nos permitia ler memória arbitrária. Descoberto em 04/06/2018.

### Leitura arbitrária do CSC

A vulnerabilidade está em um dos comandos do mecanismo de mídia responsável pela conversão do espaço de cores de YCbCr em RGBA. O comando copia uma linha escolhida pelo usuário em um buffer de estrutura, no entanto, não desinfeta a verificação do número da linha.

O erro é o seguinte:

“ c
dst = framebuf;
src = pYCbCr + linha * largura;

memcpy (dst, src, tamanho Y);
dst + = tamanho Y;
src + = tamanho Y;

memcpy (dst, src, Cb_size);
dst + = tamanho Cb;
src + = tamanho Cb;

memcpy (dst, src, Cr_size);
dst + = tamanho_do_Cr;
src + = tamanho do Cr;

csc (pRGBA, framebuf, …);

Se definirmos `pYCbCr` como NULL (usando qualquer endereço inválido), poderemos copiar arbitrariamente a memória no buffer de quadros especificado por` row * width`. Em seguida, a conversão do espaço de cores será aplicada nesse buffer e o resultado será enviado de volta ao processador MIPS.

Podemos aprender algo sobre os dados após a conversão? Obviamente, basta convertê-lo novamente para YCbCr. Infelizmente, se queremos ignorar a proteção da pilha, precisamos descobrir o valor exato do canário. Se não obtivermos o valor certo, o aplicativo irá travar. Portanto, nenhuma aproximação é permitida.

Como eu não sabia muito sobre esse algoritmo, dei uma olhada rápida na wikipedia: [YCbCr] (<https://en.wikipedia.org/wiki/YCbCr>). A última seção sobre conversão de JPEG revela que o RGB é calculado da seguinte maneira:


R = Y + 1.402 * (Cr – 128)
G = Y – 0,344136 * (Cb – 128) – 0,714136 * (Cr – 128)
B = Y + 1.722 * (Cb – 128)

É simples ver que, se Cr e Cb são 128, temos:


R = Y
G = Y
B = Y

Como podemos conseguir isso? Felizmente, isso pode ser feito com facilidade, devido ao fato de o framebuffer ser alocado no endereço constante 0x66a00000 no CDRAM e não ser limpo após a conversão.

Isso pode ser explorado da seguinte maneira:

1. Preencha o buffer de moldura YCbCr com o valor 128.
2. Copie a memória de nossa localização arbitrária no componente Y.
3. Copie exatamente a mesma memória no buffer de quadros (src = dst).
4. Aplique a conversão do espaço de cores. Agora R = G = B = Y.
5. Leia cada quarto byte do buffer de saída.

Abaixo está a implementação da primitiva de leitura arbitrária:

“ c
static void jpeg_csc (void * pRGBA, const void * pYCbCr, int width, int height, int iFrameWidth, modo int, int addr) {
int trabalho = 0;
_sceKernelDcacheWritebackInvalidateAll ();
_sceMeRequest (ME_CMD_CSC_INIT, trabalho, pRGBA, pYCbCr, largura, altura, modo, 0, iFrameWidth);
_sceMeRequest (ME_CMD_CSC_ROW, trabalho, endereço / largura, modo);
}

int _read_native (void * dst, u32 src, size_t len) {
int k1 = pspSdkSetK1 (0);

size_t i;
para (i = 0; i <ALINHAR (len, 16); i + = 16) {
u8 * temp = (u8 *) 0xABCD0000;
memset (temp, 128, 3 * 8 * 16);
jpeg_csc (temp, (vazio *) 0x08000000, 8, 16, 8, 0b0101, NATIVE (temp));
jpeg_csc (temp, (vazio *) 0x08000000, 1, 0, 1, 0b0101, src + i);
jpeg_csc (temp, (vazio *) 0x08000000, 8, 16, 8, 0b0101, 0x66a00000);

size_t j;
for (j = 0; j <MIN (len – i, 16); j ++)
((u8 *) dst) [i + j] = temperatura [j * 4];
}

pspSdkSetK1 (k1);
retornar 0;
}

Esta é uma primitiva muito poderosa e nos permite ler todas as bases .text e descobrir efetivamente a variável `__stack_chk_guard`. A única preocupação é que ainda não sabemos nada sobre o layout da memória. Esse é um problema em potencial, porque se lermos uma memória inválida, obteremos apenas uma falha de segmentação. Felizmente, o segmento de dados. Do módulo `ScePspemu` tem mais de 16 MB e está sempre nos endereços 0x81100X00 ou 0x81200X00.

– Suponha que o segmento .data esteja em 0x81100X00. Em seguida, podemos ler 0x81201000 com segurança, porque isso ainda está claramente dentro do segmento.
– Por outro lado, se estiver em 0x81200X00, o endereço 0x81201000 fica um pouco depois do início do segmento, portanto é claro que é válido.

Portanto, iniciando em 0x81201000 e revertendo para trás, podemos verificar um determinado valor mágico (alguma constante única) e, quando encontrarmos essa constante, poderemos determinar o endereço real do segmento .data. Com essas informações, podemos descobrir facilmente todas as outras bases:

– ScePspemu .data → ScePspemu .text → SceLibKernel .text → SceLibKernel .data → __stack_chk_guard

Bom, agora estamos prontos para um pouco de ROP ‘n’ Roll!

### Projetando um sistema RPC

Agora que recuperamos com êxito o canário de pilha, podemos forjar um estouro que contenha o canário de pilha correto no lugar certo. Em seguida, podemos emitir o comando vulnerável stack smash e, finalmente, obter o controle do PC, viva!

Sejamos masoquistas e empilhemos o pivô em uma grande cadeia de ROP – ou projetamos um sistema RPC adequado, que nos permite chamar funções e syscalls arbitrárias do processador MIPS. Essa é uma idéia melhor, já que ter a capacidade de executar lógica no processador MIPS é mais atraente do que gravar loops condicionais ou aritmética complicada em cargas de carga ROP puras.

A propriedade pura das explorações de quebra de pilha é que ela é determinística. Não precisamos confiar em métodos pesados ​​de pulverização de pilha, onde só podemos obter o controle do PC com uma certa probabilidade. Portanto, nossa idéia é escrever uma pequena cadeia ROP que possa invocar uma função ou syscall arbitrária toda vez que acionarmos o estouro.

Como nosso estouro está dentro da sub-rotina `remoteNetAdhocCreate ()`, também substituiremos a pilha na super rotina `ScePspemuRemoteNet ()`. Um problema disso é que iremos corromper o ponteiro `request`:

“ c
Solicitação KermitRequest **; // sp + 0x14

Isso deve ser restaurado pela cadeia ROP no tempo de execução, para que, após sair da cadeia, a seguinte chamada não falhe:

“ c
ReturnValue (KERMIT_MODE_WLAN, solicitação, res);

Outra preocupação é que precisamos restaurar o contador do programa e o ponteiro da pilha para seus locais originais após sair da cadeia ROP.

Lembre-se de que temos apenas 0x83 bytes de pilha disponível. Infelizmente, isso é muito pouco para:

1. Obtenha o ponteiro da pilha
2. Executar função arbitrária
3. Restaurar SP + 0x14
4. Defina o ponteiro da pilha para o local antigo
5. Volte ao endereço correto

No entanto, como o ponteiro da pilha é sempre o mesmo (porque o segmento `ScePspemuRemoteNet ()` simplesmente dorme até receber um novo comando), podemos separar os requisitos acima e descobrir o ponteiro da pilha apenas uma vez (pule a etapa 2). Então, para cada chamada arbitrária, podemos pular a etapa 1, como já a conhecemos. O benefício dessa abordagem é que agora podemos empilhar o pivô em uma cadeia maior de ROP e não precisamos nos preocupar com o número de gadgets que usamos. Isso pode ser feito copiando a cadeia ROP maior para `SP-sizeof (ROPchain)` e empilhar o pivô lá. Essa cadeia ROP específica pode então executar uma função arbitrária com argumentos arbitrários, depois restaurar o ponteiro `request` e retornar para` ScePspemu_remoteNetAdhocCreate_lr` como último gadget. Depois que esse último gadget for acionado, ele chegará ao mesmo endereço de pilha que estava antes. Isso tem o benefício de não precisarmos empilhar o pivô mais uma vez e poder retomar a execução com segurança.

Surpreendente, agora podemos chamar syscalls arbitrários do ARM do processador MIPS. De fato, agora podemos escrever homebrews híbridos que possuem gráficos nativos, funcionalidade de toque etc. enquanto ainda estiver sendo executado no processador MIPS! Confira [rpc.c] (<https://github.com/TheOfficialFloW/Trinity/blob/master/eboot/rpc.c>) para obter esta implementação interessante.

## Exploração do kernel do ARM

Como mencionei antes, o emulador de PSP é executado com privilégios de sistema equivalentes ao root. Basicamente, com essa exploração, já podemos acessar todos os sistemas de arquivos, registros de alterações etc. Infelizmente, os privilégios do sistema não são suficientes para alocar páginas RWX. Portanto, ainda não somos capazes de executar homebrews nativos. Ainda precisamos de uma exploração do kernel.

Meu objetivo era encontrar vulnerabilidades do kernel que eram acionáveis ​​apenas com privilégios de sistema. O motivo é que seria um desperdício se usássemos vulnerabilidades que pudessem ser acessadas com privilégios de usuário. Eles devem ser mantidos melhor para explorações do WebKit / Savedata.

### Divulgação da pilha

Antes de apresentar uma explicação detalhada da próxima exploração do kernel, mostrarei primeiro esta pequena. Descoberto em 09/10/2018.

“ c
estático uint32_t dword_8100D200, dword_8100D204;

int ksceUdcdGetDeviceInfo (void * info) {
if (! sub_810042A8 (2))
retornar 0x80243003;

* (uint32_t *) (info + 0x00) = dword_8100D200;
* (uint32_t *) (info + 0x04) = dword_8100D204;

retornar 0;
}

int sceUdcdGetDeviceInfo (void * info) {
int res, estado;
char k_info [0x40];

ENTER_SYSCALL (estado);

if (! ksceSblACMgrIsShell (0) &&
! ksceSblACMgrIsMiniSettingsForQA () &&
! ksceSblACMgrIsAllowedUsbSerial ()) {
EXIT_SYSCALL (estado);
retornar 0x80010058;
}

if (! info) {
EXIT_SYSCALL (estado);
retornar 0x8024300A;
}

res = ksceUdcdGetDeviceInfo (k_info);
se (res> = 0)
ksceKernelMemcpyKernelToUser (informações, k_info, sizeof (k_info));

EXIT_SYSCALL (estado);
retornar res> = 0;
}

 

O erro é óbvio: 0x40 bytes de pilha são alocados para `k_info`, no entanto, apenas 8 bytes são inicializados por` ksceUdcdGetDeviceInfo () `. O restante é deixado não inicializado. Em seguida, essas informações são copiadas de volta para o usuário. Portanto, os últimos 0x38 bytes conterão informações do quadro de pilha anterior. Para explorar isso, primeiro chamamos alguns syscalls que colocam coisas interessantes na pilha, como o próprio ponteiro da pilha ou endereços de retorno, depois chamamos `sceUdcdGetDeviceInfo ()` para recuperar essas informações.

Usando o endereço do módulo do kernel obtido, podemos ignorar o KASLR e criar cadeias ROP do kernel. Essa vulnerabilidade deve ser incluída em nossa cadeia, porque a próxima exploração que mostrarei, não é poderosa o suficiente para vazar dados do kernel e espera endereços predeterminados do kernel.

Estouro de Heap

Enquanto trabalhava na exploração de quebra de pilha, notei que a WLAN não funcionaria mais depois de acionar o estouro várias vezes. Apenas alguns meses depois, decidi dar uma olhada. Eu descobri rapidamente que `remoteNetAdhocCreate ()` chamaria um certo comando WLAN com os parâmetros corrompidos e depois esquecerei de liberar memória heap em caso de falha.

Novamente, usando a mesma mentalidade de antes: se existe um erro, provavelmente existe mais erros (graves). Na verdade, isso me levou a um comando WLAN diferente que sofreu um erro semelhante ao da vulnerabilidade de esmagamento de pilha. Descoberto em 26/09/2018.

Desta vez, é um comprimento de 32 bits que é passado para `memcpy ()` que não é validado. É acionável pelo comando WLAN `0x50120004` (com buf controlável`):

“ c
memcpy (trabalho + 0x28, buf + 0x10, * (uint32_t *) (buf + 0xc));

A diferença aqui é que o `work` não reside na pilha, mas na pilha. Infelizmente, esse heap é usado apenas pela pilha de rede; portanto, não há muito o que fazer. Além disso, é o mesmo heap que tinha a [vulnerabilidade Use-After-Free] (<https://blog.xyz.is/2016/vita-netps-ioctl.html>). A Sony corrigiu essa vulnerabilidade e adicionou [mitigações de exploração] (<https://blog.xyz.is/2017/363-fix.html>) para garantir a integridade do ponteiro da função no objeto do soquete. Portanto, precisamos pensar em outras estratégias para obter o controle do PC.

Eu fiz a engenharia reversa de sua implementação `malloc () / free ()` e logo desenvolvi um método diferente para explorar o estouro de heap. Meu objetivo era lançar um ataque de desvinculação.

Primeiro, deixe-me apresentar sua implementação de heap. O heap mantém uma lista “livre” duplamente vinculada e uma lista “ocupada”. O heap cresce do endereço alto para o baixo, em vez do contrário, como normalmente.

Finalmente, existem alguns invariantes de heap:

1. Todo pedaço livre é vinculado e todo pedaço ocupado é vinculado.
2. Não há dois blocos livres adjacentes e sempre devem ser mesclados.
3. Existem cookies (constantes) que indicam se um pedaço está sendo usado no momento ou não.
4. Existe um cookie (constante) no final de cada parte ocupada que é verificada como válida em `free ()`.

Abaixo está um diagrama que mostra a parte superior da pilha. Observe que o pedaço de 0x800 bytes representa o buffer `work` e o pedaço de 0x1000 bytes representa o buffer` buf` (consulte o trecho de código acima para referência). Ambos são relativamente grandes, portanto, eles não conseguem encontrar nenhum lugar no heap (após uma reinicialização a frio) e, portanto, fazem o heap crescer de forma que eles possam ocupar o topo do heap (lembre-se de que cresce para trás).

! [] (initial.png)

 

Como você pode ver, ambos estão marcados como ocupados. O pedaço de 0x800 bytes não tem um ponteiro anterior para qualquer lugar, porque está no topo da pilha. Esta é a constelação antes do estouro.

O próximo diagrama mostra a constelação após o estouro. Lembre-se de que copiamos do pedaço de 0x1000 bytes para o pedaço de 0x800.

! [] (overflow.png)

Observe que o cookie `MaAk` ainda está intacto, mesmo após o estouro. Isso não é uma barreira para nós, porque é constante e podemos facilmente falsificá-lo. Além disso, observe que agora há um novo bloco livre falso com tamanho de 0xfe0 bytes e que o tamanho do bloco de 0x1000 bytes foi reduzido a zero. Obviamente, as quatro caixas vermelhas não podem ser invisíveis: elas representam indicadores que podemos controlar arbitrariamente.

Como nosso buffer de estouro não contém parâmetros válidos especificados pelo comando, a sub-rotina notará isso rapidamente e será resgatada. Na interrupção, o pedaço de 0x800 bytes será liberado primeiro, depois o pedaço de 0x1000 (agora zero) bytes será liberado.

Lembre-se da primeira invariante: se liberarmos um pedaço, ele desvinculará sua entrada da lista de ocupada e se adicionará à lista livre. No entanto, ele não possui indicadores para a lista grátis! Como podemos descobrir a parte livre anterior e a próxima da lista? Na verdade, isso não é um problema, porque:

– O próximo bloco ocupado lógico também é o próximo bloco físico (o que significa que o próximo ponteiro no cabeçalho simplesmente aponta para o final do bloco atual).
– Ou não. Então, o próximo pedaço físico deve ser um pedaço livre.

No segundo caso, terminamos. Já encontramos um pedaço gratuito e podemos simplesmente combinar com o nosso pedaço atual.

No primeiro caso, no entanto, teremos que continuar a iteração e seguir o próximo pedaço lógico até estarmos no fundo da pilha, ou encontraremos um pedaço cujo próximo pedaço lógico não seja igual ao próximo pedaço físico.

Abaixo está a implementação da Sony em `free ()` que encontra o próximo pedaço livre usando o pedaço ocupado atual:

“ c
chunk_header_t * curr, * next;

curr = …;

enquanto (1) {
next = (chunk_header_t *) ((char *) curr + curr-> tamanho);
if (curr-> next! = next)
quebrar;
curr = curr-> next;
}

if (próximo <g_heap_end) {
// próximo é um pedaço livre
} mais {
// nenhum bloco livre após a corrente encontrada
// portanto curr deve ser o final da lista livre
}

De volta ao nosso estouro: quando liberarmos o pedaço corrompido de 0x800 bytes, ele seguirá o próximo ponteiro para o pedaço encolhido de 0x1000 bytes e descobrirá que o próximo ponteiro não aponta para o próximo pedaço físico. Portanto, ele realmente pensará que o pedaço livre falso que nós plantamos é um pedaço livre válido e, como resultado, o pedaço 0x800 finalmente apontará para o endereço arbitrário.

Agora prenda a respiração, o endereço arbitrário armazenará o ponteiro no bloco 0x800 no campo `next`, que é essencialmente o nosso ataque de desvinculação! Em outras palavras, podemos sobrescrever um ponteiro arbitrário do kernel pelo ponteiro para o bloco de 0x800 bytes, cujos dados controlamos.

Este diagrama mostra o efeito de liberar o pedaço de 0x800 bytes:

! [] (free1.png)

Observe que ainda não houve coalescência, pois ainda há um pedaço ocupado entre os dois pedaços livres.

Depois que o pedaço de 0x800 bytes tiver sido liberado, ele continuará liberando o pedaço de 0x1000 bytes:

! [] (free2.png)

Como ilustrado acima, o cabeçalho da lista ocupada agora aponta para um endereço arbitrário, e o campo `prev` nesse endereço agora armazena NULL. Portanto, destruímos completamente a estrutura de dados – felizmente o ponteiro da cauda da lista de ocupados ainda está intacto, portanto, poderemos recuperar o ponteiro da cabeça.

Finalmente, para respeitar a segunda invariante, `free ()` funde os pedaços 0x808 e 0x8:

! [] (merge1.png)

Nada de interessante aconteceu durante a coalescência, mas como ainda existem dois pedaços adjacentes, ele deve se fundir novamente:

! [] (merge2.png)

Et voilà, ocorreu um segundo ataque de desvinculação! O ponteiro `prev` da caixa na parte inferior agora aponta para o bloco livre de bytes 0x800 (agora na verdade 0x1830).

No geral, temos (na seguinte ordem):

“ c
* (uint32_t *) (arbitrary_top – offsetof (chunk_header_t, next)) = trabalho;
* (uint32_t *) (arbitrary_right – offsetof (chunk_header_t, prev)) = NULL;
* (uint32_t *) (arbitrary_bottom – offsetof (chunk_header_t, prev)) = trabalho;

onde os dados no `work` são quase totalmente controláveis ​​por nós. Agora podemos escolher o mesmo endereço para esses locais arbitrários, de modo que ele primeiro escreva o ponteiro em `work`, depois em` NULL` e, finalmente, em `work`

Agora, a pergunta é para que devemos usá-lo? Felizmente, há um alvo fácil. O código a seguir é usado para alocar esse buffer de trabalho de 0x800 bytes:

“ c
v29 = (* (int (__fastcall **) (int, assinado int, assinado int)) (* (_ DWORD *) (v4 + 0x580) + 0x638)) (
* (_ DWORD *) (v4 + 0x580) + 0x630,
0x800,
4);

Se substituirmos os dados em `v4 + 0x580` por` work`, a função colocada em `work + 0x638` poderá ser desreferenciada e executada com` work + 0x630` como argumento. Perfeito Se emitirmos o comando WLAN mais uma vez, uma função arbitrária com dados arbitrários como primeiro argumento poderá ser chamada. Como função, escolhemos o gadget `SceSysmem_ldm_r0_r4_sl_ip_sp_pc` para que possamos empilhar o pivô em nossa cadeia ROP do kernel.

O restante da exploração é auto-explicativo e pode ser encontrado [aqui] (<https://github.com/TheOfficialFloW/Trinity/blob/master/eboot/arm.c>). Ao chamar o syscall `sceWlanGetConfiguration ()` antes de `sceUdcdGetDeviceInfo ()`, poderemos vazar o endereço de `v4`. Da mesma forma, chamando `sceRtcConvertLocalTimeToUtc ()` antes de `sceUdcdGetDeviceInfo ()`, poderemos vazar o endereço da pilha do kernel e um endereço de retorno para o `SceSysmem`.

## Pós-exploração

A coisa mais importante a fazer quando temos execução de código do kernel é recuperar a estrutura de dados da pilha, caso contrário, assim que os soquetes forem usados ​​novamente, isso resultará em uma falha. O que precisamos corrigir?

– Como alteramos o tamanho do bloco ocupado de 0x1000 para 0, o tamanho da lista livre não foi incrementado corretamente quando o bloco 0x1000 foi liberado. Portanto, devemos incrementar o tamanho da lista livre em 0x1000.
– O pedaço antes do cabeçalho da lista de ocupado no estado inicial é na verdade um pedaço grande e livre. Coalescer com esse grande pedaço, no entanto, não aconteceu. Devemos recuperar isso incrementando o tamanho do pedaço livre em `0x1830 + sizeof (chunk_header_t)`.
– O cabeçalho da lista ocupada é inválido. Podemos recuperar isso usando a lista ocupada e iterar para trás até encontrarmos um pedaço ocupado que possui um próximo ponteiro inválido.
– Como sequestramos o fluxo de controle enquanto uma trava foi mantida, precisamos desbloqueá-la para evitar conflitos.
– Por último, mas não menos importante, devemos recuperar o ponteiro `* (_ DWORD *) (v4 + 0x580)` que substituímos.

## Conclusão

Essa foi a cadeia de exploração mais legal que eu já escrevi e certamente também o meu projeto mais orgulhoso. Gostei de explorar essas novas superfícies de ataque e isso me deu nostalgia, pois combinou uma década de conhecimento e pesquisa pela comunidade PSP / PS Vita. Esse projeto também concluiu o final do meu trabalho na cena do PS Vita e espero que minha redação inspire outras pessoas a começar com a engenharia reversa, encontrando vulnerabilidades e exploração. Acredito que só estou aqui onde estou hoje, graças a esse tipo de artigo e acredito que todos vocês podem conseguir o mesmo, se quiserem.

## Créditos

– Obrigado a qwikrazor87 pela exploração de leitura do kernel do MIPS.
– Obrigado à equipe Molécula por suas pesquisas anteriores no PS Vita.

 

11 de novembro 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.