
Writeup de desafio CTF pwn explorando CVE-2021-4034 (pkexec) através de manipulação de heap, com engenharia reversa baseada em Ghidra e um binário auxiliar customizado shelly.so.
Este é um desafio CTF do tipo pwn que escrevi em C e que exige que o usuário explore a vulnerabilidade CVE-2021-4034. Os jogadores recebem 2 binários no diretório challenge deste repositório. O binário chal implementa o desafio CTF e o shelly.so é um binário auxiliar.
No momento em que este writeup foi escrito, o Dockerfile ainda não está completo. O Dockerfile é necessário para implantar este desafio durante um CTF ao vivo, mas não localmente: Você ainda pode emular este desafio localmente configurando as permissões de usuário e instalando os pacotes vulneráveis da seguinte forma:
libpolkit-gobject-1-0=0.105-26ubuntu1 libpolkit-agent-1-0=0.105-26ubuntu1 policykit-1=0.105-26ubuntu1.flag.txt pertencente a root:root no diretório atual.challenge para o diretório atual.chal como o usuário não privilegiado.Executar o binário chal nos dá uma ideia vaga do que esse binário faz:```
WELCOME TO THE HUB CTRL+ALT+DELICIOUS
We're not just a sandwich hub. We are the beacon of flavors, serving a symphony in every byte
Brincar com o tamanho da entrada na função `Add` e os índices das funções `Cancel` não nos dá nada especial (sem overflow ou segmentation fault). No entanto, algumas coisas interessantes:
- Parece que os pedidos são colocados numa lista (talvez uma lista ligada) e são indexados a partir de 0?
- Há este `Order number: 0x7ffde93681f0` que parece imprimir alguma localização na pilha?
- Além disso, o programa cria um diretório com o nome que introduzimos juntamente com 3 executáveis, um dos quais é o ficheiro binário auxiliar que nos é dado:```sh
peasant@Tin-VM:~/Desktop$ ls
chal chal.c Dockerfile shelly.so solve.py tin
peasant@Tin-VM:~/Desktop$ ls -l tin/
total 28
-rwxrwx--- 1 peasant vboxsf 5 Feb 4 14:33 notes
-rwxrwx--- 1 peasant vboxsf 20 Feb 4 14:33 recipe
-rwxr-x--- 1 peasant vboxsf 16488 Feb 4 14:33 shelly.so
peasant@Tin-VM:~/Desktop$ cat tin/notes
0000
peasant@Tin-VM:~/Desktop$ cat tin/recipe
aaaa
bbbb
cccc
dddc
Os executáveis contêm nossa entrada.
Você pode brincar com as outras funções e torcer para encontrar alguns bugs (o que é provável), mas vou direto ao ponto e abrir o programa no Ghidra.
Comparando as strings que aparecem durante a execução do programa e as strings presentes no Ghidra, podemos renomear algumas das funções FUN_* para nomes familiares:```C
undefined8 main(void)
{ int iVar1; size_t sVar2; undefined2 *puVar3; long in_FS_OFFSET; int opt; int local_1c; char *local_18; long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28); local_18 = "/recipe"; print("WELCOME TO THE HUB CTRL+ALT+DELICIOUS\n"); print( "We're not just a sandwich hub. We are the beacon of flavors, serving a symphony in every by te\n\n" ); while( true ) { print("1. ENTER THE HUB\n"); print("2. QUIT\n"); __isoc99_scanf(&DAT_001030c5,&opt); getc(stdin); if (opt != 1) break; printf("Order number: %p\n",&local_18); print("Enter your name: "); __isoc99_scanf(&DAT_0010334f,&DAT_00105120); sVar2 = strlen(&DAT_00105120); puVar3 = (undefined2 *)malloc(sVar2 + 2); DAT_00105100 = puVar3; *puVar3 = 0x2f2e; *(undefined *)(puVar3 + 1) = 0; strcpy((char *)(DAT_00105100 + 1),&DAT_00105120); iVar1 = FUN_001022f0(DAT_00105100,&DAT_00105060); if (iVar1 == -1) { mkdir((char *)DAT_00105100,0x1c0); } DAT_00105140 = 0; order_cnt = 0; for (local_1c = 0; local_1c < 10; local_1c = local_1c + 1) { *(undefined8 *)(&ptr_array + (long)local_1c * 8) = 0; } main_menu(); } print("Come again :)\n");
O `printf("Order number: %p\n",&local_18);` **imprime a localização de uma variável local na pilha.**
Uma verificação rápida no gdb mostra-nos que o endereço vazado é o endereço do ponteiro para a string constante `/recipe`:```gdb
...
Order number: 0x7fffffffdfc0
...
gef➤ x/gx 0x7fffffffdfc0
0x7fffffffdfc0: 0x0000555555559020
gef➤ x/s 0x0000555555559020
0x555555559020: "/recipe"
Vemos uma chamada mkdir, que cria um diretório com o nome da nossa entrada no diretório atual.
Isso é consistente com nossa observação quando executamos o programa. Em seguida, ele inicializa alguma variável antes de chamar a função main_menu, que se parece com algo como:```C
while( true ) {
while( true ) {
while( true ) {
while( true ) {
while( true ) {
while( true ) {
print("1. ADD NEW ORDER\n");
print("2. EDIT ORDER\n");
print("3. SHOW ORDER\n");
print("4. CANCEL ORDER\n");
print("5. CHECKOUT\n");
print("6. DONE\n");
__isoc99_scanf(&DAT_001030c5,&local_40);
getc(stdin);
if (local_40 != 1) break;
add_order();
}
if (local_40 != 2) break;
edit_order();
}
if (local_40 != 3) break;
show_order();
}
if (local_40 != 4) break;
cancel_order();
}
if (local_40 != 5) break;
checkout();
}
if (local_40 == 6) break;
if (local_40 == 0x539) {
print(
"\nGORDON RAMSAY: Finally, a worthy opponent, our battle will be legendary! I BET YOU CAN 'T GUESS THE SECRET RECIPE.\n"
);
fgets(inp,0x20,stdin);
getrandom(random-bytes,0x10,0);
for (local_3c = 0; local_3c < 0x10; local_3c = local_3c + 1) {
if (inp[local_3c] != random-bytes[local_3c]) {
print("...Nuh Uh!...\n");
/* WARNING: Subroutine does not return */
exit(0);
}
print("...Ooh Yes.. sCruMpTioUs...");
}
print("Fine... I'll give you a taste.\n");
FUN_00101504();
}
Se você não está familiarizado com REV, esta é a descompilação para a declaração `switch` em C. Há uma opção interessante, também conhecida como `0x539`. Ela permite que os jogadores adivinhem `0x10` bytes aleatórios. Se todos os bytes forem iguais, então chama `FUN_00101504();` que chama `system("cat flag.txt");`. Caso contrário, o programa encerra.
No entanto, fazer bruteforce de 16 bytes aleatórios equivale a tentar todas as 256**16 = 340282366920938463463374607431768211456 possibilidades. Boa sorte com essa, lol.
Mesmo que você passe por todas as possibilidades, o programa ainda não tem privilégios, então não pode ler a flag. Esta declaração `cat flag.txt` é intencional, não apenas para enganar jogadores inexperientes a escolherem esta opção de menu `0x539`, mas também para impedir que os jogadores simplesmente chamem essa função para ler a flag, algo que exploraremos mais adiante.
***```C
int iVar1;
undefined8 *puVar2;
void *pvVar3;
undefined8 *ptr2;
if (order_cnt < 10) {
puVar2 = (undefined8 *)malloc(0x30);
print("Pick your bread: ");
readline(puVar2 + 1,8);
print("Select your spread: ");
readline(puVar2 + 2,8);
print("Choose your veg: ");
readline(puVar2 + 3,8);
print("Slam your meat & egg: ");
readline(puVar2 + 4,9);
iVar1 = order_cnt;
pvVar3 = malloc(0x30);
*(void **)(&ptr_array + (long)iVar1 * 8) = pvVar3;
*puVar2 = *(undefined8 *)(&ptr_array + (long)order_cnt * 8);
print("Any side notes for the cook? ");
readline(*puVar2,0x30);
puVar2[5] = 0;
if (DAT_00105140 != (undefined8 *)0x0) {
for (ptr2 = DAT_00105140; ptr2[5] != 0; ptr2 = (undefined8 *)ptr2[5]) {
}
ptr2[5] = puVar2;
puVar2 = DAT_00105140;
}
DAT_00105140 = puVar2;
order_cnt = order_cnt + 1;
}
...
Se algumas das variáveis não parecem fáceis de ler assim para você, é porque eu dediquei um tempo para renomeá-las, o que você deve fazer durante a engenharia reversa para acompanhar as coisas. Tudo bem, vamos aos pontos principais:
puVar2 é um chunk malloc de tamanho 0x30.puVar3, que tem 8 bytes e aponta para as notes do chunk atual.puVar2[5] = 0; sobrescreve nosso transbordamento de 1 byte para NULL de qualquer maneira.DAT_00105140 == 0, apenas a definimos como o novo chunk, então isso pode ser um ponteiro head para a lista?... print("Enter order index: "); __isoc99_scanf(&DAT_001030c5,&local_20); getc(stdin); if ((local_20 < 0) || (order_cnt <= local_20)) { print("Invalid index!\n"); } else { local_18 = head; for (local_1c = 0; local_1c != local_20; local_1c = local_1c + 1) { local_18 = (undefined8 *)local_18[5]; } print("Pick your bread: "); readline(local_18 + 1,8); print("Select your spread: "); readline(local_18 + 2,8); print("Choose your veg: "); readline(local_18 + 3,8);WE print("Slam your meat & egg: "); readline(local_18 + 4,9); print("Any side notes for the cook? "); readline(*local_18,0x30); } ...
Existe uma verificação para o nosso índice de entrada, então não podemos editar locais arbitrários.
**No entanto, a leitura de 9 bytes no 4º campo que transborda 1 byte no 5º campo ainda está lá!!! Podemos sobrescrever 1 byte no ponteiro `nxt`**
***
## Show```C
...
if (local_18 != (undefined8 *)0x0) {
printf("%s, %s, %s, %s, %s\n",local_18 + 1,local_18 + 2,local_18 + 3,local_18 + 4,*local_18);
}
...
%s imprimirá até o caractere NULL, portanto, se nosso 4º campo tiver 8 bytes de comprimento, então o 4º %s imprimirá o 4º campo do nosso chunk + o valor do ponteiro nxt no 5º campo.
=> Obtemos um vazamento de heap!!!
Nada de interessante aqui.
strcpy(local_d8,dir_name); sVar2 = strlen(dir_name); strcpy(local_d8 + sVar2,"/recipe"); creat(local_d8,0x1c0); iVar1 = open(local_d8,2); if (iVar1 == -1) { print("Error opening file f.\n"); /* WARNING: Subroutine does not return / exit(0); } chmod(local_d8,0x1f8); strcpy(local_98,dir_name); sVar2 = strlen(dir_name); strcpy(local_98 + sVar2,"/notes"); printf("%s %s\n","/notes",local_98); creat(local_98,0x1c0); __fd = open(local_98,2); if (__fd == -1) { print("Error opening file f_notes.\n"); / WARNING: Subroutine does not return */ exit(0); } chmod(local_98,0x1f8);
Então ele cria os arquivos `recipe` e `notes` no diretório com o nome do nosso input. As instruções `chmod` definem esses arquivos no modo executável: `0x1f8` e `0x1c0` são `0700` e `0770` em base octal, respectivamente.```C
...
for (local_ec = 0; (local_e0 != (char **)0x0 && (local_ec < order_cnt)); local_ec = local_ec + 1)
{
sVar2 = strlen((char *)(local_e0 + 1));
write(iVar1,local_e0 + 1,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 2));
write(iVar1,local_e0 + 2,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 3));
write(iVar1,local_e0 + 3,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 1));
write(iVar1,local_e0 + 4,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen(*local_e0);
write(__fd,*local_e0,sVar2);
write(__fd,&DAT_00103127,1);
local_e0 = (char **)local_e0[5];
}
iVar1 = close(iVar1);
if (-1 < iVar1) {
iVar1 = close(__fd);
if (-1 < iVar1) {
local_58 = 0x2f706d742f207063;
local_50 = 0x732e796c6c656873;
local_48 = 0x206f;
local_40 = 0;
local_38 = 0;
local_30 = 0;
local_28 = 0;
local_20 = 0;
strcpy((char *)((long)&local_48 + 2),dir_name);
system((char *)&local_58);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
}
...
Este grande bloco de código bagunçado basicamente escreve o conteúdo de nossos chunks para esses arquivos, depois executa o comando cp /tmp/shelly.so dir_name, onde dir_name é o nome que damos ao programa no início.
void FUN_00101e84(void)
{ long in_FS_OFFSET; char *local_18; long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28); puts("HOLD UP, LET HIM COOK."); local_18 = (char *)0x0; execve("/usr/bin/pkexec",&local_18,(char **)&ptr_array); if (local_10 != *(long )(in_FS_OFFSET + 0x28)) { / WARNING: Subroutine does not return */ __stack_chk_fail(); } return; }
Esta é uma função interessante. Ela chama `pkexec` com `NULL argv` e o ponteiro do array `notes` como variáveis de ambiente, que é o comando vulnerável no nosso CVE-2021-4034. Isso sugere que temos que criar nosso `notes` e redirecionar a execução para esta função de alguma forma?
***
## shelly.so
Abrir isso no Ghidra nos dirá que é obviamente uma biblioteca `set-UID-root`, semelhante às usadas no exploit do CVE.
***
# Resumo
Alguns pontos principais:
1. Temos um vazamento de pilha no início do programa (Order number) que contém o endereço da string constante `/recipe` usada na criação do arquivo `recipe`.
2. O campo 0 de um chunk é o seu ponteiro `notes`.
3. O campo 5 de um chunk é o ponteiro `nxt` para o próximo chunk na lista encadeada.
4. `Show` pode vazar um endereço do heap que é o campo 5 de um chunk.
5. `Edit` pode sobrescrever 1 byte no campo 5. **ESTE É O ÚNICO BUG DE ESCRITA ARBITRÁRIA!!**
6. `FUN_00101e84` chama `pkexec` com NULL argv e as variáveis de ambiente que controlamos (`notes` array).
***
# Exploração
## Layout do chunk de heap
Uma inspeção rápida no gdb pode nos dizer o deslocamento entre diferentes chunks no nosso programa:```gdb
...
Pick your bread: aaaa
Select your spread: a
Choose your veg: a
Slam your meat & egg: a
Any side notes for the cook? 0000
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
1
Pick your bread: bbbb
Select your spread: b
Choose your veg: b
Slam your meat & egg: b
Any side notes for the cook? 1111
...
gef➤ search-pattern aaaa
[+] Searching 'aaaa' in memory
[+] In '[heap]'(0x55555555a000-0x55555557b000), permission=rw-
0x55555555aae8 - 0x55555555aaec → "aaaa"
gef➤ x/30gx 0x55555555aae8-0x18
0x55555555aad0: 0x0000000000000000 0x0000000000000041
0x55555555aae0: 0x000055555555ab20 0x0000000061616161
0x55555555aaf0: 0x0000000000000061 0x0000000000000061
0x55555555ab00: 0x0000000000000061 0x000055555555ab60
0x55555555ab10: 0x0000000000000000 0x0000000000000041
0x55555555ab20: 0x0000000030303030 0x0000000000000000
0x55555555ab30: 0x0000000000000000 0x0000000000000000
0x55555555ab40: 0x0000000000000000 0x0000000000000000
0x55555555ab50: 0x0000000000000000 0x0000000000000041
0x55555555ab60: 0x000055555555aba0 0x0000000062626262
0x55555555ab70: 0x0000000000000062 0x0000000000000062
0x55555555ab80: 0x0000000000000062 0x0000000000000000
0x55555555ab90: 0x0000000000000000 0x0000000000000041
0x55555555aba0: 0x0000000031313131 0x0000000000000000
0x55555555abb0: 0x0000000000000000 0x0000000000000000
Portanto, os chunks têm tamanho 0x40 (incluindo 0x10 bytes de metadados), e estão separados (do início do chunk atual ao início do próximo chunk) por um offset de 0x40+0x40 = 0x80. Isso ocorre porque as notes são alocadas sempre que alocamos um chunk, então elas sempre ficam entre chunks consecutivos.
Antes de entrarmos no exploit, como abusamos dos bugs de estouro de 1 byte para escrever em um endereço arbitrário? Podemos usar a seguinte estratégia:
nxt do 2º chunk (2º ponto na seção Sumário).
C.B = C-0x80.B igual a x.x + 8 no 5º campo do chunk atual. O ponteiro nxt resultante será B + 8.
A = 0x55555555aae0, então o endereço vazado seria B = 0x55555555ab60 e x = 0xe8.A em 0x55555555ab08 com , o ponteiro resultante será , que não é o que queríamos.Existe uma maneira de saltar para esta função, a qual não vou discutir. Mas antes de tentar isso, dê uma olhada no Dockerfile. Percebe algo sobre flag.txt?```
...
chown root:root /home/ctf/flag.txt
...
USER peasant
CMD ["/home/ctf/start.sh"]
***ELE É PROPRIEDADE DO ROOT, ENQUANTO O PROGRAMA É EXECUTADO E PROPRIEDADE DE UM USUÁRIO NÃO PRIVILEGIADO***.
Portanto, mesmo se você executar "cat flag.txt", ele não imprimirá a flag porque o programa não tem permissão para acessar o arquivo. Isso implica que precisamos escalar para root para ler a flag.
## 2ª maneira - CVE-2021-4034
Para explorar a CVE-2021-4034, precisamos da seguinte configuração:
1. Um diretório com o nome `GCONV_PATH=.`
2. Neste diretório, precisamos de um arquivo executável que seja o nome do diretório onde nosso arquivo de configuração `gconv-modules` estará. Seja este `recipe` em nosso desafio.
3. No diretório `recipe`, precisamos de um arquivo chamado `gconv-modules` com o conteúdo que controlamos, e uma biblioteca dinâmica que nos dará um shell (talvez este seja o binário fornecido `shelly.so`)?
4. Então, em nosso `gconv-modules`, precisamos ter o mesmo CHARSET do passo 5 abaixo, e o nome da nossa biblioteca dinâmica `shelly.so` assim: `module UTF-8// SHELLY// shelly 2`.
5. Após tudo isso, chamamos `pkexec` com argv NULL e o array de variáveis de ambiente criado = `{"recipe", "PATH=GCONV_PATH=.", "CHARSET=SHELLY", "SHELL=shelly", NULL}`.
Agora, exploramos este desafio para obter a configuração acima.
***
### Objetivo 1-2
Os passos 1-2 são fáceis: só precisamos fazer login com um nome de usuário `GCONV_PATH=.` para criar este diretório. Então fazemos um `checkout` para criar o executável `recipe` neste diretório. Depois retornamos ao primeiro menu.
***
### Objetivo 3-4
Fazemos login com o nome de usuário `recipe` para criar este diretório. Agora, *precisamos* criar um arquivo chamado `gconv-modules`, mas `checkout` cria apenas os arquivos `recipe` e `notes`.
E se sobrescrevêssemos a localização da memória para a string `/notes` para se tornar `/gconv-modules`? Isso poderia funcionar, mas requer que essa memória seja gravável. Vamos verificar isso no gdb:```gdb
gef➤ search-pattern /notes
[+] Searching '/notes' in memory
[+] In '/home/peasant/Desktop/chal'(0x555555559000-0x55555555a000), permission=rw-
0x555555559010 - 0x555555559016 → "/notes"
And it is writable!
How do we perform the write?
/recipe is./recipe to /notes is 0x10. Subtract this from the /recipe address in step 1 to get /notes address./notes to /gconv-modules.As to writing the correct content to the gconv-modules file, we know that at checkout, the notes content of the chunks will be written to /notes, which is now /gconv-modules. To be sure, we can just provide the string module UTF-8// SHELLY// shelly 2 to every notes.
So now, whenever we call checkout, it will create 2 files recipe and gconv-modules and write some module UTF-8// SHELLY// shelly 2 (we use shelly because that's the name of the .so set-uid-root library given) lines to the gconv-modules file. It will also copy a shelly.so into the same directory.
notes are the corrsponding environment variables. The notes pointer array will have 4 pointers to these 4 notes and an ending NULL pointer, which is exactly what we wanted.RIP for the main function to our secret function FUN_00101e84.
RIP is? Remember our leaked stack address at the beginning of the program? Go into gdb to find out its offset to the RIP, then add that offset to the actual leaked address to get the RIP.main is? Remember our leaked address of /recipe from step 2 of Goal 3-4? Do the same thing here!Solve script with comments is provided on this repo. Feel free to contact me if you have any questions :)
xnxt0x55555555**ab**e8 != 0x55555555**aa**e8B + 8 em vez de C. Editar o chunk B para que seu 1º campo (bread) contenha o endereço target para o qual queremos escrever.B + 8. O ponteiro notes deste 3º chunk é o target! Portanto, o que escrevermos em notes será escrito em target!