はじめに
みなさん、こんにちは。平和なサイバー空間の実現に向けて日々奮闘しているセキュリティ研究者、中島明日香です。今月号から
「脆弱性の原理と修正方法なんて、教科書的でつまらなそう」
連載の第1回目となる今回は、有名なデータ転送用のコマンドラインツールである
脆弱性とは何か?
連載初回であることもふまえ、最初に
たとえば、ユーザーから任意の数値の入力を受け付け、その入力された数値をもとにとある計算を行い、結果を表示するプログラムがあるとします。ここでもし、計算式の実装に間違いがあり、それゆえに計算結果が間違って表示される場合、単なるバグにあたります。ですが、その計算結果の数値が、内部でプログラムの実行を進めるための処理
脆弱性はサイバー攻撃の根本的な原因の1つであり、可能な限り作り込まないようにするべきものです。ただし、現状で完全になくすのは現実的には難しいものでもあり、著名なソフトウェアであっても頻繁に発見されています[1]。
もし脆弱性を発見した場合、現在は脆弱性調整機関にその情報を届け出ることが推奨されています[2]。届け出られた脆弱性は、脆弱性調整機関を通じてベンダーや開発者に報告され、修正するよう促されます。そして一般的に、脆弱性が修正された場合、個々の脆弱性を一意に識別するための識別子
今回の脆弱性:CVE-2019-5482
今回は、CVE番号で表すと
どこに脆弱性があるのか?
読者の中には、cURLコマンドを用いて、TFTPサーバ上に存在するファイルを自身のコンピュータにダウンロードしたことがある方もいると思います。
$ curl tftp://サーバのアドレス/test.jpg --output test.jpg (..略..) $ ls test.jpg
今回の脆弱性は、このようにデータを受信
$ curl --tftp-blksize 100 tftp://サーバのアドレス/test.jpg --output test.jpg
ただし、のちほど詳しく解説しますが、これは必ず発現するというわけではありません。条件として、TFTPサーバ側で指定のオプション

「blksizeオプションが含まれていない」
ほかにも実装により、blksizeオプションを欠落させてOACKを返答するサーバがあれば、今回の脆弱性が発現します。現実的にそのようなサーバがある可能性はかなり低いですが、攻撃者がそういった悪意あるサーバを用意することは可能かとは思います。
話を戻しますが、ではなぜデフォルトサイズの512バイトより小さいサイズを、オプションを使って指定することで脆弱性が発現するのでしょうか? 具体的な脆弱性の解説に移る前に、まず
ヒープバッファオーバーフローとは
まずバッファオーバーフローとは、
メモリには用途に応じてさまざまな種類の領域があり、その中でも、malloc関数などで動的に確保可能なメモリの領域のことを
ここで、

たとえばここで、確保されたバッファの近くに関数ポインタのデータが存在していた場合どうなるでしょうか。図3①のように、関数ポインタが示すアドレスを上書きすることができてしまいます。そして、もし関数ポインタのアドレスが上書きされた場合は、図3②のように、正常にその関数を呼び出せなくなってしまうのです。

さらに、もしこの関数ポインタのアドレスを
実際の脆弱性箇所について
脆弱性の概要が理解できたところで、実際の脆弱性箇所を見ていきます。今回は脆弱性の種類がヒープバッファオーバーフローとわかっているので、最初に注目すべきは
脆弱性が発現する箇所
今回は、親切にもcURLの公式Webサイトに、この情報が掲載されていました。読んでみると、tftp.
static CURLcode tftp_receive_packet(struct connectdata *conn)
{
struct Curl_sockaddr_storage fromaddr;
curl_socklen_t fromlen;
CURLcode result = CURLE_OK;
struct Curl_easy *data = conn->data;
tftp_state_data_t *state = (tftp_state_data_t *)conn->proto.tftpc;
struct SingleRequest *k = &data->req;
/* Receive the packet */
fromlen = sizeof(fromaddr);
state->rbytes = (int)recvfrom(state->sockfd,
(void *)state->rpacket.data,
state->blksize + 4,
0,
(struct sockaddr *)&fromaddr,
&fromlen);
recvfromは簡単に言えば、指定されたソケット上のデータを受信しそれを指定のバッファに格納する関数で、リスト2、表1のように引数を取ります。
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr,
socklen_t *addrlen);
番号 | 引数 | 意味 |
---|---|---|
第1引数 | int sockfd | ソケット記述子 |
第2引数 | void *buf | 受信したデータを格納するためのバッファへのポインタ |
第3引数 | size_ |
受信するデータのサイズ |
第4引数 | int flags | フラグ |
第5引数 | struct sockaddr *src_ |
送信元アドレス |
第6引数 | socklen_ |
送信元アドレス長 |
引数は6つありますが、ここで大事なのは、recvfrom関数ではソケットを通じてデータを受信する際に、第2引数で指定したバッファに対し、第3引数で指定したサイズでデータを受信して格納するということです。リスト1では、第2引数で指定しているバッファはstate->rpacket.
で、第3引数で指定しているデータサイズはstate->blksize + 4
です。
では、このバッファはいったいどこで確保されているのでしょうか? そして受信するデータサイズは、どのようにしてstate->blksize
に指定されているのでしょうか?
オーバーフローするバッファ
解析した結果、同じくtftp.blksize + 2 + 2
にあたるサイズのメモリをヒープ領域から確保しています。
if(!state->rpacket.data) {
state->rpacket.data = calloc(1, blksize + 2 + 2);
では次に、メモリを確保する際に利用されたblksize変数とは何者なのかを見ていきます。まず、リスト4に示すように、blksizeはint型変数として定義され、最初はTFTP_
static CURLcode tftp_connect(struct connectdata *conn, bool *done)
{
tftp_state_data_t *state;
int blksize;
blksize = TFTP_BLKSIZE_DEFAULT;
そしてこのTFTP_
#define TFTP_BLKSIZE_DEFAULT 512
#define TFTP_BLKSIZE_MIN 8
#define TFTP_BLKSIZE_MAX 65464
#define TFTP_OPTION_BLKSIZE "blksize"
ここまで読み解いた結果だと、リスト3では
/* alloc pkt buffers based on specified blksize */
if(conn->data->set.tftp_blksize) {
blksize = (int)conn->data->set.tftp_blksize;
if(blksize > TFTP_BLKSIZE_MAX ¦¦ blksize < TFTP_BLKSIZE_MIN)
return CURLE_TFTP_ILLEGAL;
}
まとめると、curlコマンド実行時にtftp-blksizeオプションを利用した場合、オプションで指定したブロック・
補足になりますが、calloc関数を用いて、バッファを確保する際にblksize + 2 + 2
というサイズの指定になっているのは、実際にデータを送受信する際には、データの前に2バイトで表現される2つのヘッダ情報
state->blksize
の中身は?
引き続きtftp_state->blksize
に格納しています。
state->blksize = blksize;
このサイズが、最終的にrecvfrom関数
ではcURL側でこのようなOACKを処理する際、いったい何が起きているのでしょうか?state->blksize
にデフォルトサイズ
static CURLcode tftp_parse_option_ack(tftp_state_data_t *state,
const char *ptr,
int len)
{
const char *tmp = ptr;
struct Curl_easy *data = state->conn->data;
/* if OACK doesn't contain blksize option, the default (512) must be used */ ←★
state->blksize = TFTP_BLKSIZE_DEFAULT;
つまりこれは、ユーザーが指定したブロック・state->blksize
の値が強制的に512となってしまうのです。
まとめると、ユーザーがデフォルトサイズ以外のブロック・
脆弱性の修正方法
では、この脆弱性が実際にどのように修正されたかを見ていきます。この脆弱性に対する修正は2019年9月に行われました[4]。修正はtftp.
確保するバッファのサイズをデフォルトサイズに
最初にtftp_
int need_blksize;
そして、このneed_need_
のサイズで、ヒープ領域からメモリ
need_blksize = blksize;
/* default size is the fallback when no OACK is received */
if(need_blksize < TFTP_BLKSIZE_DEFAULT)
need_blksize = TFTP_BLKSIZE_DEFAULT;
- state->rpacket.data = calloc(1, blksize + 2 + 2); ←削除
+ state->rpacket.data = calloc(1, need_blksize + 2 + 2); ←追加
ここで筆者が疑問に思ったのが
サーバからOACKが返ってこない場合も想定
その疑問を解決するヒントは、リスト8のコメント部分にありました。コメントには
つまり修正者は、脆弱性の発現の要因であるOACK内のblksizeオプションの有無を気にする以前に、
詳しく説明すると、
リスト10の箇所でも、上記の方針で修正されているのが見て取れます。具体的にはstate->blksize
には、最初デフォルトサイズの512を格納していますが、これもサーバから問題なくOACKが
state->blksize
もここではTFTP_BLKSIZE_DEFALT(512)に- state->blksize = blksize; ←削除
+ state->blksize = TFTP_BLKSIZE_DEFAULT; /* Unless updated by OACK response */ ←追加
まとめ
今回の脆弱性は、ユーザーがデフォルトのブロック・
さらに勉強したい人向け
さらに勉強したい方に、関連する書籍などを紹介します。まず
それではみなさん、また次回お会いしましょう!