
Exploit pour débordement de tampon basé sur la pile trouvé dans le binaire conn-indicator du routeur TP-Link Archer AX50
Le routeur TP-Link Archer AX50 est vulnérable à un débordement de tampon basé sur la pile dans sa version de firmware 1.0.14 Build 20240108 rel.42655(4555), conduisant à une exécution de code à distance à la fois côté LAN et côté WAN. Cette vulnérabilité a la même cause racine que CVE-2020-10881, découverte par l'équipe Flashback et largement détaillée dans leur série de vidéos à ce sujet (voir références). Cependant, le processus d'exploitation diffère légèrement, donc un nouvel exploit a dû être écrit.

Cette vulnérabilité se produit dans le binaire conn-indicator, chargé de vérifier si le routeur est connecté à Internet en envoyant périodiquement des requêtes DNS et en écoutant leurs réponses sur un port UDP aléatoire entre 32000 et 61000.
La fonction qui reçoit et traite d'abord ces paquets de réponse DNS est TPDns_RecvAndResolve(), située à 0x00405e3c. Lors de la réception d'un paquet avec recvfrom, il est stocké dans buf, qui a une taille de 2960 octets. Ensuite, elle vérifie que le code de retour est correct (RCODE == 0) et vérifie les nombres de questions et de réponses dans le paquet (QDCOUNT et ANCOUNT). Pour traiter les réponses, elle appelle process_resolved_IP() et lui passe un pointeur vers buf, answer_ptr, qui est un pointeur vers l'emplacement des réponses dans le paquet à l'intérieur de buf, le nombre de réponses (ANCOUNT) et d'autres drapeaux :
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 fonction process_resolved_IP(), située à 0x00405818, parcourt chaque réponse et appelle DNS_answer_parser() pour chacune d'elles. Elle passe en arguments le même pointeur vers buf, answer, qui est un pointeur vers la réponse en cours d'analyse dans le paquet original à buf, et un pointeur vers current_answer, qui est un tampon de taille 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 fonction DNS_answer_parser(), située à 0x004054e0, analyse chaque réponse individuellement, qui consiste en un nom de domaine représenté comme <len><domaine><len><domaine>.... Par exemple, example.com serait représenté par 7example3com. La fonction boucle sur chaque paire <len><domaine> qui constitue le nom de domaine dans la réponse et vérifie que <len> est inférieur à 63 (domain_name & 0xc0 != 0). Ensuite, elle appelle memcpy et copie <len> octets de answer, correspondant à <domaine>, dans current_answer. Elle répète le processus pour la paire suivante de <len><domaine> dans le nom de domaine.
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 );
}
Étant donné que current_answer ne fait que 256 octets, il est possible pour un attaquant d'envoyer un paquet avec une réponse contenant un nom de domaine suffisamment long pour déborder le tampon.
Le processus d'exploitation est très similaire à celui expliqué par l'équipe Flashback pour CVE-2020-10881 dans leur série de vidéos à ce sujet (voir références), avec quelques différences clés :
move, car maintenant le registre qui est déplacé vers $a2 comme argument count pour memcpy() est $s1 au lieu de $s0.i dans process_resolved_IP() est chargée dans $s5 au lieu de $s8. Ensuite, lorsqu'elle est comparée à $v1, $v1 est prise depuis $sp+616, qui est très loin du tampon débordé, donc la pile a dû être corrompue plus loin à partir de la réponse et de la commande./tmp, donc toutes les écritures et lectures doivent être effectuées là. Cela rend impossible que le shell inverse fasse moins de 62 caractères, donc pour le déployer sur la cible, il doit être servi depuis un serveur web et une commande pour le télécharger et l'exécuter doit être envoyée à la cible.Avec cela à l'esprit, je vais expliquer l'ensemble du processus quand même.
Le binaire conn-indicator a NX activé et ASLR sur tout sauf lui-même :