
Exploit für einen Stack-basierten Pufferüberlauf, der im conn-indicator-Binärprogramm im TP-Link Archer AX50 Router gefunden wurde
Der TP-Link Archer AX50 Router ist anfällig für einen stack-basierten Pufferüberlauf in seiner Firmware Version 1.0.14 Build 20240108 rel.42655(4555), was zu Remote-Code-Ausführung sowohl im LAN als auch im WAN führt. Diese Schwachstelle hat dieselbe Ursache wie CVE-2020-10881, die vom Flashback-Team gefunden und in ihrer Videoserie darüber ausführlich beschrieben wurde (siehe Referenzen). Der Ausnutzungsprozess unterscheidet sich jedoch etwas, sodass ein neuer Exploit geschrieben werden musste.

Diese Schwachstelle tritt in der Binärdatei conn-indicator auf, die dafür zuständig ist, zu überprüfen, ob der Router mit dem Internet verbunden ist, indem sie regelmäßig DNS-Anfragen sendet und auf deren Antworten an einem zufälligen UDP-Port zwischen 32000 und 61000 lauscht.
Die Funktion, die diese DNS-Antwortpakete empfängt und zuerst verarbeitet, ist TPDns_RecvAndResolve(), die sich bei 0x00405e3c befindet. Beim Empfangen eines Pakets mit recvfrom wird es in buf gespeichert, das eine Größe von 2960 Bytes hat. Dann wird geprüft, ob der Rückgabecode korrekt ist (RCODE == 0), und die Anzahl der Fragen und Antworten im Paket (QDCOUNT und ANCOUNT) wird überprüft. Um die Antworten zu verarbeiten, wird process_resolved_IP() aufgerufen, dem ein Zeiger auf buf, answer_ptr (ein Zeiger auf die Position der Antworten im Paket innerhalb von buf), die Anzahl der Antworten (ANCOUNT) und weitere Flags übergeben werden:
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;
}
[...]
Die Funktion process_resolved_IP(), bei 0x00405818, durchläuft jede Antwort und ruft für jede DNS_answer_parser() auf. Ihr werden als Argumente derselbe Zeiger auf buf, answer (ein Zeiger auf die aktuelle Antwort, die innerhalb des ursprünglichen Pakets in buf geparst wird) und ein Zeiger auf current_answer (ein Puffer der Größe 256) übergeben:
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 );
}
Die Funktion DNS_answer_parser(), bei 0x004054e0, parst jede Antwort einzeln, die aus einem Domainnamen in der Form <len><domain><len><domain>... besteht. Als Beispiel würde example.com als 7example3com dargestellt werden. Die Funktion durchläuft jedes Paar <len><domain>, das den Domainnamen in der Antwort bildet, und prüft, ob <len < 63 ist (domain_name & 0xc0 != 0). Dann ruft sie memcpy auf und kopiert <len> Bytes von answer, die <domain> entsprechen, in current_answer. Sie wiederholt den Vorgang für das nächste Paar <len><domain> im Domainnamen.
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 );
}
Da current_answer nur 256 Byte lang ist, kann ein Angreifer ein Paket mit einer Antwort senden, die einen ausreichend großen Domainnamen enthält, um den Puffer zu überlaufen.
Der Ausnutzungsprozess ist dem vom Flashback-Team in ihrer Videoserie zu CVE-2020-10881 erläuterten (siehe Referenzen) sehr ähnlich, mit einigen wesentlichen Unterschieden:
move-Instruktion, da nun das Register, das nach $a2 als count-Argument für memcpy() verschoben wird, $s1 anstelle von $s0 ist.i in process_resolved_IP() wird in $s5 statt in $s8 geladen. Wenn sie dann mit $v1 verglichen wird, stammt $v1 von $sp+616, was sehr weit vom überlaufenen Puffer entfernt ist, sodass der Stack weiter über die Antwort und den Befehl hinaus beschädigt werden musste./tmp schreibgeschützt, daher müssen alle Schreib- und Lesevorgänge dort erfolgen. Dies macht es unmöglich, dass die Reverse Shell weniger als 62 Zeichen lang ist. Um sie auf dem Ziel zu platzieren, muss sie von einem Webserver bereitgestellt und ein Befehl zum Herunterladen und Ausführen an das Ziel gesendet werden.Trotzdem werde ich den gesamten Prozess erläutern.
Die Binärdatei conn-indicator hat NX aktiviert und ASLR ist bei allem außer sich selbst aktiviert: