
Exploit per il buffer overflow basato sullo stack trovato nel binario conn-indicator del router TP-Link Archer AX50
Il router TP-Link Archer AX50 è vulnerabile a uno stack-based buffer overflow nella versione del firmware 1.0.14 Build 20240108 rel.42655(4555), portando all'esecuzione remota di codice sia sul lato LAN che sul lato WAN. Questa vulnerabilità ha la stessa causa principale di CVE-2020-10881, scoperta dal team Flashback e ampiamente descritta nella loro serie di video a riguardo (vedi riferimenti). Tuttavia, il processo di sfruttamento differisce un po', quindi è stato necessario scrivere un nuovo exploit.

Questa vulnerabilità si verifica nel binario conn-indicator, che ha il compito di verificare se il router è connesso a Internet inviando periodicamente query DNS e ascoltando le relative risposte su una porta UDP casuale tra 32000 e 61000.
La funzione che riceve e processa per prima questi pacchetti di risposta DNS è TPDns_RecvAndResolve(), situata a 0x00405e3c. Quando riceve un pacchetto con recvfrom, questo viene memorizzato in buf, che ha una dimensione di 2960 byte. Poi verifica che il codice di ritorno sia corretto (RCODE == 0) e controlla i numeri di domande e risposte nel pacchetto (QDCOUNT e ANCOUNT). Per processare le risposte, chiama process_resolved_IP() e le passa un puntatore a buf, answer_ptr, che è un puntatore alla posizione delle risposte all'interno del pacchetto in buf, il numero di risposte (ANCOUNT) e altri flag:
undefined4 * TPDns_RecvAndResolve(int socket,void *param_2,int param_3){
[...]
byte buf [2960];
[...]
while( true ) {
recv_bytes = recvfrom(socket,buf + total_recv_bytes,0xb90 - total_recv_bytes,0,&sStack_50,
local_38);
piVar2 = __errno_location();
if (recv_bytes == 0) goto RECV_ERROR;
if (recv_bytes < 0) break;
total_recv_bytes = total_recv_bytes + recv_bytes;
if (2959 < total_recv_bytes) goto PROCESS_DNS_RESP;
}
[...]
/* Check that ANCOUNT is not 0 (answer contains at least one domain) */
puVar5 = (undefined4 *)0x0;
if (buf._6_2_ != 0) {
local_40 = 0;
puVar5 = process_resolved_IP(buf,answer_ptr,(uint)buf._6_2_,&local_3c,&local_40);
answer_ptr = answer_ptr + local_40;
}
[...]
La funzione process_resolved_IP(), situata a 0x00405818, scorre ogni risposta e chiama DNS_answer_parser() per ciascuna di esse. Passa come argomenti lo stesso puntatore a buf, answer, che è un puntatore alla risposta corrente in fase di parsing all'interno del pacchetto originale in buf, e un puntatore a current_answer, che è un buffer di dimensione 256:
undefined4 * process_resolved_IP(byte *buf,byte *answer_ptr,uint ANCOUNT,undefined4 *param_4,int *param_5){
[...]
byte current_answer [256];
ushort answer_flags [5];
i = 0;
puVar9 = (undefined4 *)0x0;
puVar7 = (undefined4 *)0x0;
answer = answer_ptr;
do {
/* Check if all answers have been parsed already */
if (i == ANCOUNT) {
if (param_4 != (undefined4 *)0x0) {
*param_4 = puVar7;
}
if (param_5 != (int *)0x0) {
*param_5 = (int)answer - (int)answer_ptr;
}
return puVar9;
}
bytes_processed = DNS_answer_parser(buf,answer,current_answer,1);
memcpy(answer_flags,answer + bytes_processed,10);
uVar1 = answer_flags._4_4_;
bytes_processed = bytes_processed + 10;
uVar6 = (uint)answer_flags[4];
uVar8 = (uint)answer_flags[0];
if (uVar8 == 2) {
LAB_00405924:
DNS_answer_parser(buf,answer + bytes_processed,abStack_240,1);
}
else if (uVar8 < 3) {
pbVar2 = answer + bytes_processed;
if (uVar8 == 1) {
sprintf((char *)abStack_240,"%u.%u.%u.%u",(uint)*pbVar2,(uint)pbVar2[1],(uint)pbVar2[2],
(uint)pbVar2[3]);
}
}
else {
if (uVar8 == 5) goto LAB_00405924;
if (uVar8 == 0x1c) {
inet_ntop(10,answer,(char *)abStack_240,0xff);
}
}
answer = answer + bytes_processed + uVar6;
[...]
i = i + 1;
} while( true );
}
La funzione DNS_answer_parser(), situata a 0x004054e0, fa il parsing di ogni risposta individualmente, che consiste in un nome di dominio rappresentato come <len><domain><len><domain>.... Ad esempio, example.com verrebbe rappresentato come 7example3com. La funzione itera su ogni coppia di <len><domain> che costituisce il nome di dominio nella risposta e verifica che <len> sia minore di 63 (domain_name & 0xc0 != 0). Poi chiama memcpy e copia <len> byte di answer, corrispondenti a <domain>, in current_answer. Ripete il processo per la coppia successiva di <len><domain> nel nome di dominio.
int DNS_answer_parser(byte *buf,byte *answer,byte *current_answer,int flag){
int iVar1;
uint __n;
int iVar2;
uint uVar3;
ushort flag_and_offset;
byte domain_name;
iVar2 = 0;
do {
domain_name = *answer;
__n = (uint)domain_name;
iVar1 = 1;
if (__n == 0) {
*current_answer = 0;
LAB_004055b0:
return iVar2 + iVar1;
}
/* Check if compression mode is used */
if ((domain_name & 0xc0) != 0) {
flag_and_offset = CONCAT11(domain_name,answer[1]);
DNS_answer_parser(buf,buf + (flag_and_offset & 0x3fff),current_answer,flag);
iVar1 = 2;
goto LAB_004055b0;
}
uVar3 = __n + 1;
if (flag == 0) {
*current_answer = '.';
memcpy(current_answer + 1,answer + 1,__n);
__n = uVar3;
}
else {
memcpy(current_answer,answer + 1,__n);
}
answer = answer + uVar3;
current_answer = current_answer + __n;
iVar2 = iVar2 + uVar3;
flag = 0;
} while( true );
}
Poiché current_answer è lungo solo 256 byte, è possibile per un attaccante inviare un pacchetto con una risposta contenente un nome di dominio abbastanza grande da mandare in overflow il buffer.
Il processo di sfruttamento è molto simile a quello spiegato dal team Flashback per CVE-2020-10881 nella loro serie di video a riguardo (vedi riferimenti), con alcune differenze chiave:
move, poiché ora il registro che viene spostato in $a2 come argomento count per memcpy() è $s1 invece di $s0.i in process_resolved_IP() viene caricata in $s5 invece che in $s8. Poi, quando viene confrontata con $v1, $v1 viene prelevato da $sp+616, che è molto lontano dal buffer andato in overflow, quindi lo stack doveva essere corrotto in una posizione ancora più lontana dalla risposta e dal comando./tmp, quindi tutte le scritture e letture devono essere fatte lì. Questo rende impossibile che la reverse shell sia più corta di 62 caratteri, quindi per distribuirla sul target deve essere servita da un web server e deve essere inviato al target un comando per scaricarla ed eseguirla.Con questo in mente, spiegherò comunque l'intero processo.
Il binario conn-indicator ha NX abilitato e ASLR su tutto tranne che su se stesso: