fbpx

Sony PlayStation Vita 3.65 / 3.67 / 3.68 – kernel ‘h-encore’ e modificações de usuário

Sony PlayStation Vita 3.65 / 3.67 / 3.68 – kernel ‘h-encore’ e modificações de usuário

 

# h-encore write-up

* h-encore *, onde * h * significa hacks e homebrews, é o segundo jailbreak público do * PS Vita ™ *, que suporta os mais novos firmwares 3.65, 3.67 e 3.68. Ele permite que você faça modificações no kernel e no usuário, mude a velocidade do relógio, instale plugins, execute homebrews e muito mais.

## Introdução

Bem-vindo ao meu artigo sobre a cadeia de exploração * h-encore *. Neste artigo, quero apresentar as medidas de segurança do PS Vita, explicar como encontrei as vulnerabilidades, como as explorei e como as contornei usando técnicas modernas.
Espero que seja uma leitura interessante para iniciantes e especialistas, e para as pessoas na cena e fora dela.

## Exploração da terra do usuário

* h-encore * usa um ponto de entrada diferente do seu antecessor [HENkaku] (https://github.com/henkaku/henkaku). Em vez de uma exploração do WebKit, ela está usando uma exploração de gamesave. A razão disso é que após o firmware 3.30, aproximadamente, a Sony introduziu `sceKernelInhibitLoadingModule ()` em seu navegador, o que nos impediu de carregar módulos adicionais. Essa limitação é crucial, pois foi a única maneira de obtermos mais syscalls (do que o navegador usa), pois eles são randomizados na inicialização e atribuídos apenas aos slots do syscall se algum módulo do usuário os importar.

A razão pela qual uma exploração de gamesave é possível nesse sistema é porque os jogos que foram desenvolvidos com um SDK 2.60 e inferior foram compilados como um executável vinculado estaticamente, portanto, o endereço de carregamento deles é sempre o mesmo, ou seja, “ 0x81000000 ”, e eles não podem ser realocado para outra região. Eles também não têm a proteção de pilha ativada por padrão, o que significa que, se pudermos empilhar smash em um jogo, poderemos executar ROP com prazer.

#### Localizando uma exploração de gamesave

Inicialmente, implementei a exploração do kernel em cima de uma exploração de gamesave diferente que encontrei alguns anos atrás. Este jogo, no entanto, é caro e leva um tempo no jogo até que ele possa desencadear a exploração. Portanto, não é adequado para o público. Para alcançar um público maior, eu queria ter uma [demo sem drm] (https://wololo.net/talk/viewtopic.php?t=38342) como ponto de entrada do usuário, pois eles podem ser instalados em qualquer dispositivo usando [psvimgtools] (https://github.com/yifanlu/psvimgtools).
Procurando explorações de gamesave é um processo chato, você apenas confunde os gamesaves escrevendo coisas aleatórias em locais aleatórios até obter uma falha (a melhor aposta é estender as cordas e espero que você possa quebrar a pilha), então perguntei a [Freakler] (https: //twitter.com/freakler94) para me ajudar nisso. Ele então encontrou uma falha promissora no bittersmile do jogo, causada por uma instrução de gravação em que o conteúdo e o destino podiam ser controlados.

#### Estouro de buffer

O bug depende do analisador do jogo bittersmile. O gamesave é na verdade um arquivo de texto e o jogo lê linha por linha e os copia para uma lista de buffers. No entanto, ele não valida o comprimento, portanto, se colocarmos o delimitador `\ n` longe, de forma que a linha seja maior do que o buffer pode suportar, obteremos um estouro clássico de buffer.
Se esse buffer estiver na pilha, podemos substituí-lo pelo endereço de retorno e executar diretamente nossa cadeia ROP. No entanto, está na seção de dados, mas, felizmente, para nós, o conteúdo após o buffer é realmente a lista que continha destinos para outras linhas. Isso significa que se invadirmos a lista e redirecionarmos o buffer, poderemos copiar a próxima linha para onde quisermos e, portanto, permitir-nos uma primitiva de gravação arbitrária (consulte [generate.py] (https://github.com/TheOfficialFloW/ h-encore / blob / master / scripts / generate.py)).

#### ASLR parcial

Parece que o ASLR está ativado apenas para executáveis ​​/ módulos. Normalmente, isso não é um problema, pois o executável é a primeira coisa alocada e sua randomização será propagada e afetará as alocações subsequentes (por exemplo, pilhas de encadeamentos).
Porém, como nosso executável é carregado estaticamente, todas as outras alocações serão determinísticas. Portanto, podemos escolher nosso endereço de destino para estar em uma pilha de threads e substituir um endereço de retorno.

#### Programação orientada a retorno

O PS Vita nos impede de executar o código nas seções de dados; portanto, devemos aplicar uma técnica chamada “programação orientada a retorno” ou ROP brevemente para executar o código. A técnica é inteligente: como só podemos executar o código que está em uma seção de texto, simplesmente usaremos o código existente para executar o que quisermos. Como é que isso funciona?
Quando chamamos sub-rotinas, o registrador de link `lr ‘, segurando o endereço de retorno, é empurrado para a pilha ao entrar e é acionado na saída para retornar ao ponto em que foi chamado.
Portanto, se conseguirmos sobrescrever o endereço de retorno, podemos controlar o contador do programa `pc` e fazê-lo retornar a qualquer lugar que desejar. Se este “algum lugar” consistir em algumas instruções úteis que terminam em um `pop {…, pc}`, podemos novamente controlar o endereço de retorno e apontar para o próximo “gadget”. Por indução, isso pode ser continuado para sempre (até ficarmos sem pilha) e é isso que chamamos de cadeia ROP.

#### Gravando cadeias ROP

Cadeias estáticas de ROP podem ser produzidas usando qualquer linguagem de script ou mesmo manualmente com um editor hexadecimal, pois é apenas uma questão de encadear dispositivos. Normalmente alguém escreveria apenas uma pequena cadeia para chamar `system ()` ou `mprotect ()`. Infelizmente, é proibido alocar memória executável em um processo sem privilégios; consequentemente, o que queremos alcançar deve ser implementado inteiramente no ROP.
Decidi usar o GNU Assembler para compilar minha cadeia ROP, uma vez que é flexível e permite configurar dados com precisão. Também é possível definir macros que podem ser usadas para implementar determinadas funcionalidades por cadeias ROP menores (consulte [macros.S] (https://github.com/TheOfficialFloW/h-encore/blob/master/include/macros.S )).

Essas macros nos permitem expressar o código ROP de maneira simples e legível. Considere este código em C:

“ c
// Carrega stage2
system_dat_fd = sceIoOpen (saveata0_system_dat_path, SCE_O_RDONLY, 0);
sceIoRead (system_dat_fd, STAGE2_ADDRESS – (0x48 + 0x4 + _end – _start), STAGE2_SIZE);
sceIoClose (system_dat_fd);
“ “

Usando macros, ele pode ser implementado da seguinte maneira no ROP. Este é um trecho de código do stage1 que carrega o stage2 em uma pilha maior (consulte [stage1.S] (https://github.com/TheOfficialFloW/h-encore/blob/master/stage1/stage1.S):

“ c
// Carrega stage2
call_vvv sceIoOpen, savedata0_system_dat_path, SCE_O_RDONLY, 0
store_rv ret, system_dat_fd
call_lvv sceIoRead, system_dat_fd, STAGE2_ADDRESS – (0x48 + 0x4 + _end – _start), STAGE2_SIZE
call_l sceIoClose, system_dat_fd
“ “

onde `l` significa que o argumento será carregado / desreferenciado,` v` que é um valor / constante e `r` que é o valor de retorno (em ARM, o valor de retorno é gravado no registro` r0`).

## Exploração do kernel

A vulnerabilidade do kernel, que está sendo usada no * h-encore *, está no módulo SceNgs, um mecanismo de áudio projetado para jogos para produzir efeitos sonoros. A vulnerabilidade foi descoberta em 04/02/2018 e foi explorada com sucesso quatro dias depois.

### Falhas de design

Enquanto olhava para o módulo SceNgs, me deparei com os syscalls `sceNgsVoiceDefGet * ()`, que retornavam ponteiros do kernel “criptografados” que foram xor’ed com o valor `0x9e28dcce`. Esse é um design muito ruim e, em vez disso, a Sony deveria ter retornado um UID ou algo assim. Após uma rápida pesquisa por esse valor, aprendi que ele foi usado pelos syscalls `sceNgsRackGetRequiredMemorySize ()` e `sceNgsRackInit ()`. Um de seus parâmetros é uma estrutura do tipo `SceNgsRackDescription` que requer que o campo` pVoiceDefn` seja definido para qualquer um desses ponteiros criptografados.

Abaixo estão as estruturas mencionadas (campos desinteressantes foram omitidos):

“ c
typedef struct SceNgsVoiceDefinition {
SceUInt32 uMagic; // 0x00
SceUInt32 uFlags; // 0x04
SceUInt32 uSize; // 0x08
SceInt32 nSomeOffset; // 0x0c
// …
SceInt32 nVoicePresetsOffset; // 0x30
SceUInt32 uNumVoicePresets; // 0x34
// …
} SceNgsVoiceDefinition; // 0x40

typedef struct SceNgsRackDescription {
const struct SceNgsVoiceDefinition * pVoiceDefn;
// …
} SceNgsRackDescription;
“ “

E aqui está um trecho de pseudo-código de como essas estruturas são lidas:

“ c
SceNgsRackDescription rackDesc;
SceNgsVoiceDefinition voiceDefn;
// …
if (! copyin (& rackDesc, pRackDesc, sizeof (SceNgsRackDescription), 1)) {
vá para error_invalid_param;
}
if (rackDesc.pVoiceDefn == NULL) {
vá para error_invalid_param;
}
rackDesc.pVoiceDefn ^ = SCE_NGS_VOICE_DEFINITION_XOR;
if (rackDesc.pVoiceDefn == NULL) {
vá para error_invalid_param;
}
if (! is_valid_vaddr (rackDesc.pVoiceDefn, sizeof (SceNgsVoiceDefinition))) {
vá para error_invalid_param;
}
if (! copyin (& voiceDefn, rackDesc.pVoiceDefn, sizeof (SceNgsVoiceDefinition), 0)) {
vá para error_invalid_param;
}
if (voiceDefn.uMagic! = SCE_NGS_VOICE_DEFINITION_MAGIC) {
vá para error_invalid_param;
}
// …
error_invalid_param:
retornar SCE_NGS_ERROR_INVALID_PARAM;
“ “

Como podemos ver, o `pRackDesc` é copiado do usuário para o kernel, e o campo` pVoiceDefn` é decodificado. Está até checando se o ponteiro do kernel é válido, o que é a nossa vantagem, como veremos mais adiante. Finalmente, se todos os campos forem válidos, ele continuará trabalhando com `voiceDefn`.
Como controlamos `pRackDesc-> pVoiceDefn` e sabemos o valor xor, é possível passar uma definição de voz falsa, desde que esteja na memória do kernel. A razão disso é que o PS Vita possui medidas de mitigação SMEP / SMAP equivalentes que impedem o kernel de ler / gravar implicitamente a memória do usuário ou executar o código executável do usuário.

#### Verificações insuficientes

O syscall do rack é realmente grande e quase todos os campos na definição de voz são desinteressantes e não podem ser explorados, exceto `nVoicePresetsOffset` e` uNumVoicePresets`.
Eles mantêm informações em uma lista de predefinições do seguinte tipo (observe que todas as compensações são relativas à predefinição e o deslocamento à predefinição é relativo à definição de voz):

“ c
typedef struct SceNgsVoicePreset {
SceInt32 nNameOffset;
SceUInt32 uNameLength;
SceInt32 nPresetDataOffset;
SceUInt32 uSizePresetData;
SceInt32 nBypassFlagsOffset;
SceUInt32 uNumBypassFlags;
} SceNgsVoicePreset;
“ “

Abaixo está um trecho de pseudo-código interessante da syscall gigantesca:

“ c
void * SceNgsBlock;
Predefinições SceNgsVoicePreset *;
// …
// SceNgsBlock é alocado com um tamanho determinado por todas as predefinições e alguns outros campos
// …
SceNgsBlock + = 0x148;
SceNgsBlock + = (voiceDefn.nNumVoicePresets * sizeof (SceNgsVoicePreset));
predefinições = (SceNgsVoicePreset *) ((char *) voiceDefn + voiceDefn-> nVoicePresetsOffset);

for (size_t i = 0; i <voiceDefn-> uNumVoicePresets; i ++) {
// …
if (predefinições [i] .nPresetDataOffset! = 0 &&
predefinições [i] .uSizePresetData! = 0) {
memcpy (SceNgsBlock, & presets [i] + presets [i] .nPresetDataOffset, predefinições [i] .uSizePresetData);
SceNgsBlock + = predefinições [i] .uSizePresetData;
}
// …
}
“ “

É importante mencionar que nenhuma verificação em `uSizePresetData` está sendo feita ao resumir os tamanhos predefinidos. Portanto, um ataque possível é usar um tamanho negativo na última predefinição para que o tamanho da alocação resultante seja menor do que o pretendido. Este é um primitivo de estouro de pilha que ganhamos. Por exemplo, ter predefinições com o tamanho 0x1000, 0x1000 e -0x1000 resultará em uma alocação apenas do tamanho 0x1000 e, durante a cópia, a segunda predefinição deve sobrecarregar o buffer.
O problema aqui é obviamente como parar o `memcpy ()` no tamanho negativo, pois um tamanho negativo como número inteiro não assinado é realmente muito grande (-1 == 0xFFFFFFFF).
Fui inspirado por uma técnica semelhante explicada por [CTurt] (https://cturt.github.io/dlclose-overflow.html) que propôs usar um thread diferente para interromper o estouro. No entanto, antes que eu pudesse tentar isso, eu já havia feito uma prova de conceito para confirmar o estouro da pilha e, para minha surpresa, ela estourou, mas a terceira predefinição não foi totalmente copiada e, portanto, não causou uma falha de segmentação.

memcpy, ou mais como memecpy

Sim, a Sony não conseguiu implementar uma memória correta. Em vez de copiar uma enorme carga de dados ao obter um tamanho negativo, ele simplesmente copia alguns bytes e termina. Sério, eu esperaria erros em qualquer lugar, exceto na função libc mais básica. Sua implementação incorreta está disponível no userland e no kernel, talvez até no TrustZone.

Aqui está o pseudo-código de sua implementação. Suponho que ele foi escrito em assembly, em vez de código C, pois contém instruções altamente otimizadas.

“ c
void memcpy (void * dst, const void * src, size_t len) {
void * end;

if (len> = 32) {
fim = dst + len; // [1]
while ((uintptr_t) dst e 3) {
* (uint8_t *) dst = * (uint8_t *) src;
src + = sizeof (uint8_t);
dst + = sizeof (uint8_t);
len – = sizeof (uint8_t);
}
while ((uintptr_t) dst e 31) {
* (uint32_t *) dst = * (uint32_t *) src;
src + = sizeof (uint32_t);
dst + = sizeof (uint32_t);
len – = sizeof (uint32_t);
}
if (fim> = dst + 32) {// [2]
// cópia vetorizada
// …
}
}
while ((int32_t) len> = sizeof (uint32_t)) {// [3]
* (uint32_t *) dst = * (uint32_t *) src;
src + = sizeof (uint32_t);
dst + = sizeof (uint32_t);
len – = sizeof (uint32_t);
}
if (len & 2) {
* (uint16_t *) dst = * (uint16_t *) src;
src + = sizeof (uint16_t);
dst + = sizeof (uint16_t);
}
if (len & 1) {
* (uint8_t *) dst = * (uint8_t *) src;
}
}
“ “

– O primeiro bug está em [1]. Se `len` for negativo, a adição com` dst` produzirá um valor menor que `dst` devido a um estouro de número inteiro e, como conseqüência, a comparação em [2] resultará em falsa, não importa se é um sinal assinado ou comparação não assinada e, portanto, acredita que há menos de 32 bytes para copiar.
– O segundo bug está em [3] onde o comprimento é comparado como um número inteiro assinado. Portanto, um comprimento negativo simplesmente ignorará o loop de cópia.

Graças a esses erros, podemos explorar a vulnerabilidade anterior de maneira diferente, usando um comprimento negativo para iterar para trás e, portanto, permitir uma primitiva de gravação fora dos limites.

### Exploração

#### Ignorando SMAP

Nosso objetivo é controlar o conteúdo de `pVoiceDefn`, no entanto, como o kernel não pode implicitamente ler a memória do usuário, precisamos plantar nossos dados na memória do kernel. Esta é uma tarefa fácil: por causa do SMAP, os syscalls usam `copyin ()` para copiar buffers de usuário para a pilha do kernel e acessá-los posteriormente. Ao retornar ao usuário, a pilha do kernel não é limpa devido a razões de desempenho, portanto, quaisquer dados que foram copiados do usuário permanecerão lá. Um syscall em potencial que podemos usar para esse fim é `sceIoDevctl ()`, que pode copiar até 0x3ff bytes para a pilha do kernel (o que esse syscall está realmente fazendo não é relevante e podemos apenas passar algumas entradas inválidas para fazer com que ele retorne erro). Isso é grande o suficiente para manter nossa definição de voz e, mais importante, está profundamente localizada dentro da pilha do kernel, de forma que os syscalls subsequentes não possam corrompê-la. Mas onde está a pilha do kernel?

#### Obtendo o endereço base da pilha do kernel

A verificação `is_valid_vaddr ()` é útil porque, se passarmos um ponteiro de kernel inválido, não causará uma falha de segmentação, mas retornará um erro. Graças a isso, podemos configurar nossa definição de voz falsa e plantá-la na pilha do kernel, e então encontrar seu endereço iterando através da memória do kernel usando `sceNgsRackGetRequiredMemorySize ()`:

“ c
int ret = 0;
uint32_t kstack_base = KSTACK_BASE_START;

Faz {
rack_desc.pVoiceDefn = (struct SceNgsVoiceDefinition *) (kstack_base + DEVCTL_STACK_FRAME) ^ SCE_NGS_VOICE_DEFINITION_XOR;
ret = sceNgsRackGetRequiredMemorySize (sys_handle, & rack_desc, & buffer_info.size);
if (ret == SCE_NGS_ERROR_INVALID_PARAM)
kstack_base + = KSTACK_BASE_STEP;
} while (ret == SCE_NGS_ERROR_INVALID_PARAM);
“ “

#### Implementando um loop condicional no ROP

Escrever ROP não é tão simples quanto escrever montagem. Uma instrução só pode ser implementada com mais de uma dúzia de gadgets. Portanto, gostaríamos de manter nosso código o mais simples possível e eliminar a adição e a condição redundantes dentro do loop:

“ c
int ret = 0;
uint32_t kstack_base = KSTACK_BASE_START – KSTACK_BASE_STEP + KSTACK_DEVCTL_INDATA_OFFSET;

Faz {
kstack_base + = KSTACK_BASE_STEP;
rack_desc.pVoiceDefn = (struct SceNgsVoiceDefinition *) (kstack_base ^ SCE_NGS_VOICE_DEFINITION_XOR);
ret = sceNgsRackGetRequiredMemorySize (sys_handle, & rack_desc, & buffer_info.size);
} while (ret == SCE_NGS_ERROR_INVALID_PARAM);

kstack_base – = KSTACK_DEVCTL_INDATA_OFFSET;
“ “

“ c
// Definir informações predefinidas na definição de voz
store_vv PRESET_LIST_OFFSET, voice_def_buf + 0x30
store_vv 2, voice_def_buf + 0x34

// Definir predefinições
set_preset 0, 0, – (0x148 + 2 * 0x18) + COPYOUT_PARAMS_OFFSET
set_preset 1, FAKE_COPYOUT_OFFSET, FAKE_COPYOUT_SIZE

// Substitui dst, src e len do copyout
call_vvv memset, voice_def_buf + FAKE_COPYOUT_OFFSET, 0, FAKE_COPYOUT_SIZE
store_vv sysmem_base, voice_def_buf + FAKE_COPYOUT_OFFSET + 0x04 // dst
add_lv kstack_base, KSTACK_SYSMEM_OFFSET
store_rv ret, voice_def_buf + FAKE_COPYOUT_OFFSET + 0x08 // src
store_vv 4, voice_def_buf + FAKE_COPYOUT_OFFSET + 0x1c // len

// Exploração do gatilho
trigger_exploit

// Obter endereço base do SceSysmem
load_add_store sysmem_base, sysmem_base, SCE_SYSMEM_BASE
“ “

Observe como a primeira predefinição é usada para mover o ponteiro de destino para trás e como a segunda é usada para substituir o destino por dados controlados.

#### Escolhendo um destino OOB

Minha primeira idéia é substituir um ponteiro de função de qualquer objeto cuja invocação possamos controlar por algum syscall. Nosso SceNgsBlock é alocado com `ksceKernelAllocMemBlock ()`, portanto, nosso destino também deve ser alocado da mesma maneira, para que seus blocos de memória sejam consecutivos. Não encontrei nada naquele momento, então pedi a [xyz] (https://twitter.com/pomfpomfpomf3) alguns conselhos. Ele me lembrou que os threads de usuário possuem uma pilha de usuário e de kernel, e como a pilha de kernel é alocada com `ksceKernelAllocMemBlock ()`, é possível visá-lo.

#### Controlando a gravação OOB

Eu criei muitos threads e li seus endereços base de pilha do kernel para ver se eles estavam perto do SceNgsBlock e notei algumas coisas:

– As pilhas do kernel têm 0x1000 bytes de tamanho e estão localizadas em endereços ímpares como “0xXXXX1000, 0xXXXX3000, 0xXXXX5000, etc.”.
– Quando o intervalo `0xXXXX4000-0xXXXX6000` está livre, as pilhas do kernel escolhem a parte superior em `0xXXXX5000`.
– O bloco usado mais recentemente será utilizado se liberarmos um bloco e alocarmos outro com o mesmo tamanho.
– Os blocos serão eventualmente consecutivos.

Lembre-se de que nossa gravação fora dos limites pode manipular qualquer coisa que tenha um endereço mais baixo que o nosso bloco; portanto, queremos trazer a pilha do kernel para trás do SceNgsBlock. Uma estratégia para conseguir isso é a seguinte:

1. Pulverize vários blocos com um tamanho de 0x2000.
2. Após muitas alocações, os blocos acabarão se tornando consecutivos (e pares).
3. Libere o segundo último bloco para criar um buraco.
4. Crie uma linha para preencher esse buraco com a pilha do kernel.
5. Libere o último bloco e aloque nosso rack maligno.

O segmento gerado (vamos chamá-lo de segmento 2 e o segmento atual 1) agora deve simplesmente chamar qualquer syscall.
Esse syscall não deve terminar antes que sua pilha seja substituída. Para conseguir isso, podemos, por exemplo, adquirir um semáforo no encadeamento 2, sobrescrever a pilha do kernel no encadeamento 1 e finalmente liberar o semáforo no encadeamento 1 para fazer com que o syscall de aquisição termine e acione a execução ROP do kernel no meio do caminho.
No entanto, este é mais esforço do que o necessário e pode ser feito de uma maneira mais simples, usando o syscall `sceKernelDelayThread ()` para fazer com que o thread espere um pouco. Eu escolhi 100ms, o que é tempo suficiente para o segmento 1 seqüestrar a pilha do kernel do segmento 2 antes que ele retorne ao usuário.
A razão pela qual ele funciona é porque o syscall de atraso chama algumas sub-rotinas que aumentam a pilha (os registros salvos por chamada e o endereço de retorno são empurrados para a pilha); depois de 100 ms, a pilha diminui e os dados serão removidos da pilha. Se conseguirmos sobrescrever o endereço de retorno entre essas duas fases, podemos executar nossa cadeia ROP do kernel. Observe que, como a pilha cresce de alto a baixo e estamos atacando de cima, não precisamos lidar com cookies de pilha.

Execução de ROP do kernel

Como o endereço de retorno que substituímos está muito próximo ao final da pilha (porque o syscall `sceKernelDelayThread ()` não requer muita pilha), não temos muito espaço para nossa cadeia ROP. Portanto, temos que plantar nossa cadeia ROP do kernel em outro lugar e empilhar o pivô nela. Assim como plantamos a definição de voz falsa na pilha do kernel, podemos usar `sceIoDevctl ()` para plantar nossa cadeia ROP do kernel. Como estamos fazendo isso no mesmo encadeamento, não precisamos saber seu endereço e podemos simplesmente mover o ponteiro da pilha para trás de `kstack_base + 0x1000 – 0x64` para` kstack_base + 0x6f8` da seguinte maneira (consulte [krop.S] (https://github.com/TheOfficialFloW/h-encore/blob/master/stage2/krop.S):

“ c

diferença de .set, (KSTACK_SIZE – OVERWRITE_SIZE) – KSTACK_DEVCTL_INDATA_OFFSET
push 1, SceSysmem_pop_r3_r4_r5_r6_r7_pc // pc
push 1, SceSysmem_pop_r3_pc // r3
push 1, SceSysmem_sub_r1_r1_r3_bx_lr // r4
push 9, 0xDEADBEEF // r5
push 1, SceSysmem_pop_pc // r6
push 9, 0xDEADBEEF // r7
push 1, SceSysmem_add_r1_sp_bc_mov_r2_r6_blx_r3 // pc
push 0, 0x1c + 0xbc + diferença // r3
push 1, SceSysmem_blx_r4_pop_r4_pc // pc
push 9, 0xDEADBEEF // r4
push 1, SceSysmem_mov_sp_r1_blx_r2 // pc
“ “

Observe que `push` é uma macro que usa como base o primeiro argumento, que diz qual o tipo do próximo argumento, onde` 0: constant`, `1: SceSysmem gadget`,` 2: kernel stack (para operações RW) `e 9: dummy (não será escrito).

A cadeia ROP real do kernel aloca dois blocos de memória RW, copia a carga útil do kernel compactado no primeiro bloco e armazena a carga descomprimida no segundo bloco. Finalmente, ele marca o segundo bloco como executável, limpa os caches e o executa.

#### Pós-exploração

A tarefa da carga útil do kernel é corrigir o módulo ngs, pois ele trava ao sair do processo. Isso ocorre porque não deveríamos usar comprimentos negativos e o syscall do rack se comportaria de maneira diferente. Não descobri o que estava acontecendo, pois a função era grande demais para entendê-la completamente.
Depois de estabilizar o kernel, ele instala alguns syscalls e patches de assinatura personalizados, para que possa iniciar o menu de autoinicialização. Isso instalará e carregará o [taiHEN] (https://github.com/yifanlu/taiHEN) para aplicar patches ao sistema e nos permitir executar homebrews, plugins e aplicar outros mods e ajustes.

## Patch da Sony

Uma atualização de firmware foi lançada em 11/09/2018, 72 dias após o lançamento do * h-encore *, que corrigiu a exploração do kernel usando as constantes `0x9e28dcc0-0x9e28dce0` para as definições de voz. Um ponteiro arbitrário do kernel não pode mais ser passado. No entanto, o bug do memcpy e a exploração do usuário ainda não foram corrigidos. Decidi não deixar a parte memcpy de fora da redação, pois acho que não há outro bug que dependa dela.

## Conclusão

A Sony realmente fez um ótimo trabalho neste console, se comparado a outros produtos, e foi apenas azar que esses bugs estivessem coincidentemente nos lugares errados. Encontrar e analisá-los foi interessante e uma boa experiência de aprendizado para mim.
Embora as vulnerabilidades fossem fáceis de explorar, contornar suas mitigações era um desafio, especialmente porque eu não havia escrito nenhuma cadeia de ROP antes e nunca era tão grande.
Também gostei de trabalhar neste artigo e espero que um dia possa terminar minha segunda cadeia de exploração e apresentar essa para você.
Enquanto isso, vou olhar o kernel do PS4 🙂

## Créditos

Agradecimentos a [SpecterDev] (https://twitter.com/SpecterDev) e [abertschi] (https://twitter.com/andrinbertschi) por lerem sobre o artigo e me fornecerem feedback.

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.