前回 は、VSCode(Visual Studio Code)で、CodeQL を使う方法について調べました。
CodeQL の挙動を理解するには、対象のソースコードを少し変更して、CodeQL の結果を確認する、を繰り返すことが必要そうです。しかし、これまで対象のソースコードとして、使いたいと考えていた Use-After-Free は、1関数しかなく、データフロー解析を試すことを考えると、少し使いにくいと感じました。そこで、今回は、GitHub に公開されている OSS のうち、軽めの HTTPサーバの実装である Tinyhttpd を調査します。これが CodeQL の対象ソースコードとして、使いやすいといいのですが、まだ分かりません。
それでは、やっていきます。
はじめに
「セキュリティ」の記事一覧です。良かったら参考にしてください。
セキュリティの記事一覧
以下は、Tinyhttpd の GitHub です。
github.com
CodeQL を実行するためのソースコードとして、簡単すぎず、難しすぎず、かつ、外部から入力を受けるようなものを探しました。ChatGPT にいろいろ相談したところ、このソースコードを推薦されました。
なお、現在の実行環境は、Windows11 に、WSL2 で、Ubuntu 24.04 を入れた環境です。
Tinyhttpdの概要
Tinyhttpd は、シンプルなマルチスレッド対応の簡易 HTTPサーバで、GET/POST を受け付け、静的ファイル配信と CGI実行を行うことができます。
Tinyhttpdの動作を確認する
まずは、GitHub からソースコードを取得して、動作確認していきます。
クローンとビルドを行います。ビルドでは、いくつか警告が出ますが、まずは気にしないことにします。また、ビルドにより、Tinyhttpd の本体である httpd という実行ファイルと、簡易クライアント?の client という実行ファイルが生成されます。
$ git clone https://github.com/EZLippi/Tinyhttpd.git
$ cd Tinyhttpd/
$ make
では、実際に動かしていきます。
実行すると、4000番ポートでクライアントからのアクセスを待っている状態になります。
$ ./httpd
httpd running on port 4000
ここで、ビルドで生成されたクライアントを動かしてみます。
エラーになりました。
$ ./client
oops: client1: Connection refused
ソースコードの simpleclient.c を見てみます。
ポート番号が、9734 となっており、サーバの設定(4000)と異なっていました。4000番ポートに修正して再ビルドします。
再度実行してみます。
何も反応がありません。
$ ./client
^C
仕方ないので、VSCode で、サーバ側をデバッガで動かして、動作を確認しました。
クライアントは、1文字だけ、'A' を送っていて、それは受信できていました。その後、サーバは、次の文字を待ち続けていました。
クライアントを終了すると、サーバの recv関数から応答が返り、何も受信しなかったことが分かります。
このクライアントソフトは、何を目的としているのか分かりません。おそらくデバッグ用途だと思います(まともな動作は、そもそも期待してない)。
では、このクライアントソフトで動作確認するのは諦めて、普通のブラウザを使って確認したいと思います。
ブラウザを起動して、http://localhost:4000/
にアクセスします。すると、以下の画面が表示されました。ここまでは、正しく動作してるようです。
ブラウザの表示画面
入力フォームに red と入力して、送信ボタンをクリックしてみます。
redを入力した後のブラウザの表示画面
うまく動いていないようです。
いくつか修正しました。
まずは、htdocs/color.cgi と、htdocs/check.cgi に実行権限がなかったので、以下を実行しました。
$ chmod +x htdocs/check.cgi
$ chmod +x htdocs/color.cgi
htdocs/color.cgi と、htdocs/check.cgi の内容を見ると、シェバン行が #!/usr/local/bin/perl -Tw
となってますが、私の環境では、perl は、/usr/bin/perl
だったので、パスを修正しました。
まだ動かないので、CGIファイルを直接実行してみました。
ChatGPT に原因を聞いてみると、CGIモジュールがインストールされてないということでした。
$ REQUEST_METHOD=GET QUERY_STRING=color=red htdocs/color.cgi
Can't locate CGI.pm in @INC (you may need to install the CGI module) (@INC entries checked: /etc/perl /usr/local/lib/x86_64-linux-gnu/perl/5.38.2 /usr/local/share/perl/5.38.2 /usr/lib/x86_64-linux-gnu/perl5/5.38 /usr/share/perl5 /usr/lib/x86_64-linux-gnu/perl-base /usr/lib/x86_64-linux-gnu/perl/5.38 /usr/share/perl/5.38 /usr/local/lib/site_perl) at htdocs/color.cgi line 4.
BEGIN failed--compilation aborted at htdocs/color.cgi line 4.
CGIモジュールをインストールします。
$ sudo apt install libcgi-pm-perl
もう一度、CGIファイルを実行してみます。
ちゃんと HTML が返ってきてるので、うまくいってるようです。
$ REQUEST_METHOD=GET QUERY_STRING=color=red htdocs/color.cgi
Content-Type: text/html; charset=ISO-8859-1
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-US" xml:lang="en-US">
<head>
<title>RED</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
</head>
<body bgcolor="red">
<h1>This is red</h1>
</body>
</html>
では、httpd を実行してみます。うまく動きました。
redを入力した後のブラウザの表示画面(2回目)
Tinyhttpdの詳細
ここからは、Tinyhttpd のソースコードを見ていきます。
main関数
まずは、main関数です。
コメントで、add と書いたところと、debug と書いたところは、私が追加したところです。add の方は、通常は初期化されるところなので、入れておきました。debug の方は、クライアントが接続してきたときに、クライアントの情報を表示しています。
startup関数は後述しますが、TCPサーバとしてのソケットの初期化を行っています。その後、while の無限ループに入ります。
ループ内では、accept関数で、クライアントからの接続を受け付け、クライアントごとに、 pthread_create関数で、スレッドを作成して、そのスレッドにクライアントの処理を任せています。
int main(void)
{
int server_sock = -1;
u_short port = 4000;
int client_sock = -1;
struct sockaddr_in client_name;
socklen_t client_name_len = sizeof(client_name);
pthread_t newthread;
server_sock = startup(&port);
printf("httpd running on port %d\n", port);
memset(&client_name, 0, sizeof(client_name));
while (1)
{
client_sock = accept(server_sock,
(struct sockaddr *)&client_name,
&client_name_len);
if (client_sock == -1)
error_die("accept");
{
char buf[INET_ADDRSTRLEN] = {};
inet_ntop(AF_INET, &client_name.sin_addr, buf, sizeof(buf));
printf("accept from %s:%u\n", buf, ntohs(client_name.sin_port));
fflush(stdout);
}
if (pthread_create(&newthread , NULL, (void *)accept_request, (void *)(intptr_t)client_sock) != 0)
perror("pthread_create");
}
close(server_sock);
return(0);
}
startup関数
次は、startup関数です。
普通の TCPサーバの実装です。上位関数からポート番号 0 で指定された場合は、動的にポートを割り当てています。
listen関数で、クライアントを 5つまで受け付けています。
int startup(u_short *port)
{
int httpd = 0;
int on = 1;
struct sockaddr_in name;
httpd = socket(PF_INET, SOCK_STREAM, 0);
if (httpd == -1)
error_die("socket");
memset(&name, 0, sizeof(name));
name.sin_family = AF_INET;
name.sin_port = htons(*port);
name.sin_addr.s_addr = htonl(INADDR_ANY);
if ((setsockopt(httpd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))) < 0)
{
error_die("setsockopt failed");
}
if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
error_die("bind");
if (*port == 0)
{
socklen_t namelen = sizeof(name);
if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)
error_die("getsockname");
*port = ntohs(name.sin_port);
}
if (listen(httpd, 5) < 0)
error_die("listen");
return(httpd);
}
accept_request関数
accept_request関数は、先ほど、pthread_create関数で作成したスレッド関数です。私の方で、いくつかコメントを付けています。
get_line関数で、クライアントから受信した 1行を取得しています(numchars は取得した文字数)。その後の printf関数は私が追加しました。
最初の while文は、スペースが見つかるまで、文字列を取得しています。先頭は、GET か POST を期待しています。いずれにもマッチしない場合は、エラーを返す unimplemented関数を実行しています。
GET or POST で、POST なら cgiフラグをセットしておきます。その後、空白を読み飛ばした後、URL を読み出します。
次は、GET の場合の処理です。先ほど取得した URL を解析します。URL に ?
が含まれている場合は、?
を \0
に変更して、query_stringポインタに、?
の次の位置に設定して、cgiフラグをセットしておきます。
sprintf関数で、要求されたパスを、htdocsディレクトリ内のパスとして、path変数に格納しています。末尾が /
の場合、index.html の文字列を追加しておきます。
stat関数で、要求されたパスのファイルが存在しない場合、受信データを全て読み出した後、エラーを表示する not_found関数を実行します。
ファイルが存在した場合、かつ、ディレクトリだった場合は、index.html の文字列を追加します。そのファイルが実行可能なファイルだったら、cgiフラグをセットします。
最後に、cgiフラグがセットされていたら、execute_cgi関数を実行し、そうでなければ、serve_file関数を実行します。
void accept_request(void *arg)
{
int client = (intptr_t)arg;
char buf[1024];
size_t numchars;
char method[255];
char url[255];
char path[512];
size_t i, j;
struct stat st;
int cgi = 0;
char *query_string = NULL;
numchars = get_line(client, buf, sizeof(buf));
printf("get_line(): numchars=%ld\n", numchars);
i = 0; j = 0;
while (!ISspace(buf[i]) && (i < sizeof(method) - 1))
{
method[i] = buf[i];
i++;
}
j=i;
method[i] = '\0';
if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
{
unimplemented(client);
return;
}
if (strcasecmp(method, "POST") == 0)
cgi = 1;
i = 0;
while (ISspace(buf[j]) && (j < numchars))
j++;
while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < numchars))
{
url[i] = buf[j];
i++; j++;
}
url[i] = '\0';
if (strcasecmp(method, "GET") == 0)
{
query_string = url;
while ((*query_string != '?') && (*query_string != '\0'))
query_string++;
if (*query_string == '?')
{
cgi = 1;
*query_string = '\0';
query_string++;
}
}
sprintf(path, "htdocs%s", url);
if (path[strlen(path) - 1] == '/')
strcat(path, "index.html");
if (stat(path, &st) == -1) {
while ((numchars > 0) && strcmp("\n", buf))
numchars = get_line(client, buf, sizeof(buf));
not_found(client);
}
else
{
if ((st.st_mode & S_IFMT) == S_IFDIR)
strcat(path, "/index.html");
if ((st.st_mode & S_IXUSR) ||
(st.st_mode & S_IXGRP) ||
(st.st_mode & S_IXOTH) )
cgi = 1;
if (!cgi)
serve_file(client, path);
else
execute_cgi(client, path, method, query_string);
}
close(client);
}
get_line関数
get_line関数は、クライアントから受信した 1行を上位に返します。
通常の HTTP のデータは、改行コードが、\r\n
ですが、この関数は、\r
や、\n
にも対応しています。どの改行コードを受信したとしても、buf に格納するのは \n
です。
途中のところは、コメントで説明を付けておきました。末尾に、\0
を入れて、この関数は終了します。
int get_line(int sock, char *buf, int size)
{
int i = 0;
char c = '\0';
int n;
while ((i < size - 1) && (c != '\n'))
{
n = recv(sock, &c, 1, 0);
if (n > 0)
{
if (c == '\r')
{
n = recv(sock, &c, 1, MSG_PEEK);
if ((n > 0) && (c == '\n'))
recv(sock, &c, 1, 0);
else
c = '\n';
}
buf[i] = c;
i++;
}
else
c = '\n';
}
buf[i] = '\0';
return(i);
}
serve_file関数
accept_request関数から呼ばれている serve_file関数です。CGIファイルではない、通常の HTMLファイルを表示します。
whileループ前に、buf に設定しているのは、while の条件を満たすためだと思います。do while を使えばいいだけのような気がします。リクエストの残りを全て読むために、読み飛ばしています。
上位から与えられたファイルパスに対して、ファイルオープンして、headers関数と、cat関数を呼び出しています。
void serve_file(int client, const char *filename)
{
FILE *resource = NULL;
int numchars = 1;
char buf[1024];
buf[0] = 'A'; buf[1] = '\0';
while ((numchars > 0) && strcmp("\n", buf))
numchars = get_line(client, buf, sizeof(buf));
resource = fopen(filename, "r");
if (resource == NULL)
not_found(client);
else
{
headers(client, filename);
cat(client, resource);
}
fclose(resource);
}
serve_file関数から最初に呼び出されている headers関数です。
HTTP のレスポンスのヘッダ部分を送信しています。ヘッダの末尾には、\r\n
だけの行が必要だったと思いますので、そこもちゃんと送信しています。
void headers(int client, const char *filename)
{
char buf[1024];
(void)filename;
strcpy(buf, "HTTP/1.0 200 OK\r\n");
send(client, buf, strlen(buf), 0);
strcpy(buf, SERVER_STRING);
send(client, buf, strlen(buf), 0);
sprintf(buf, "Content-Type: text/html\r\n");
send(client, buf, strlen(buf), 0);
strcpy(buf, "\r\n");
send(client, buf, strlen(buf), 0);
}
cat関数
serve_file関数から呼び出されている、もう1つの関数の cat関数です。
HTTP のレスポンスのボディ部分を送信しています。こちらも、do while が好きじゃないのか、fgets関数が、2回登場します。
ファイルの内容をそのまま出力しているだけですね。
void cat(int client, FILE *resource)
{
char buf[1024];
fgets(buf, sizeof(buf), resource);
while (!feof(resource))
{
send(client, buf, strlen(buf), 0);
fgets(buf, sizeof(buf), resource);
}
}
execute_cgi関数
accept_request関数から呼ばれている、もう 1つの関数の serve_file関数です。CGIプログラムを実行します。
これまでで一番難しい関数ですね。
最初は、GET と POST で分岐します。GET の方は、リクエストボディが存在しないので、残りのリクエストを読み飛ばします。POST の方は、Content-Length を見つけたときは、その値を読み取り、\n が出現した時点でループを抜けます(受信データには、リクエストボディが残っている)。
次に、この後、fork で、子プロセスを生成しますが、親子のプロセス間で通信するために、ここで、パイプを 2つ生成します。子プロセスは実際に CGIファイルを実行します。cgi_output は、子プロセスの出力用(親プロセスは入力となる)で、cgi_input は、子プロセスの入力用(親プロセスは出力となる)です。パイプは読み書き両用はできないので、読み込み用、書き込み用の 2つを用意する必要があります。
fork関数を実行すると、子プロセスが生成されます。親プロセス、子プロセス、ともに、fork関数の呼び出しから戻ります。親プロセス側は、fork関数の戻り値として、子プロセスのプロセスID が返って、子プロセス側は、fork関数の戻り値は 0 になります。これにより、同じソースコードでも、親プロセスと子プロセスで処理を分けることができます。
その後、両方のプロセスが、HTTPリクエストの応答を返しています。うーん、この場合は親プロセスだけが応答を返せばいいと思うのですが、、、ちょっと理由が分かりません。
まず、pid が 0 の場合の子プロセス側の処理から見ていきます。
まず、dup2関数で、子プロセスの書き込み用のパイプを、標準出力として複製します。これにより、子プロセスが標準出力に出力する(printf関数などを使って)と、親プロセスとのパイプにも書き込まれることになります。さらに、dup2関数を実行して、子プロセスの読み取り用のパイプを標準入力として複製します。その後、使用しない方のパイプ(親プロセスが使用する)をクローズしておきます。
次は、putenv関数を使って、環境変数の設定を行います。REQUEST_METHOD に GET or POST を設定します。その後、GET の場合は、QUERY_STRING に ?
以降の内容を設定します。POST の場合は、CONTENT_LENGTH に先ほど取得した内容を設定します。
最後に、execl関数を使って、CGIファイルを実行します。
次に、親プロセス側です。最初に、子プロセス側と同様に、使わないパイプをクローズします。
POST の場合、リクエストボディを Content-Length の分だけ読み出し、子プロセスに送信します。あとは、子プロセスから書き込みがあると、その内容をクライアントに送信するループを実行します。CGIプログラムが終了すると、ループを抜けて、パイプをクローズして、子プロセスの終了ステータスを読み出して終了します。
void execute_cgi(int client, const char *path,
const char *method, const char *query_string)
{
char buf[1024];
int cgi_output[2];
int cgi_input[2];
pid_t pid;
int status;
int i;
char c;
int numchars = 1;
int content_length = -1;
buf[0] = 'A'; buf[1] = '\0';
if (strcasecmp(method, "GET") == 0)
while ((numchars > 0) && strcmp("\n", buf))
numchars = get_line(client, buf, sizeof(buf));
else if (strcasecmp(method, "POST") == 0)
{
numchars = get_line(client, buf, sizeof(buf));
while ((numchars > 0) && strcmp("\n", buf))
{
buf[15] = '\0';
if (strcasecmp(buf, "Content-Length:") == 0)
content_length = atoi(&(buf[16]));
numchars = get_line(client, buf, sizeof(buf));
}
if (content_length == -1) {
bad_request(client);
return;
}
}
else
{
}
if (pipe(cgi_output) < 0) {
cannot_execute(client);
return;
}
if (pipe(cgi_input) < 0) {
cannot_execute(client);
return;
}
if ( (pid = fork()) < 0 ) {
cannot_execute(client);
return;
}
sprintf(buf, "HTTP/1.0 200 OK\r\n");
send(client, buf, strlen(buf), 0);
if (pid == 0)
{
char meth_env[255];
char query_env[255];
char length_env[255];
dup2(cgi_output[1], STDOUT);
dup2(cgi_input[0], STDIN);
close(cgi_output[0]);
close(cgi_input[1]);
sprintf(meth_env, "REQUEST_METHOD=%s", method);
putenv(meth_env);
if (strcasecmp(method, "GET") == 0) {
sprintf(query_env, "QUERY_STRING=%s", query_string);
putenv(query_env);
}
else {
sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
putenv(length_env);
}
execl(path, NULL);
exit(0);
} else {
close(cgi_output[1]);
close(cgi_input[0]);
if (strcasecmp(method, "POST") == 0)
for (i = 0; i < content_length; i++) {
recv(client, &c, 1, 0);
write(cgi_input[1], &c, 1);
}
while (read(cgi_output[0], &c, 1) > 0)
send(client, &c, 1, 0);
close(cgi_output[0]);
close(cgi_input[1]);
waitpid(pid, &status, 0);
}
}
htdocs/index.htmlファイル
トップページの index.htmlファイルです。
color.cgi が使われています。
<HTML>
<TITLE>Index</TITLE>
<BODY>
<P>Welcome to J. David's webserver.
<H1>CGI demo
<FORM ACTION="color.cgi" METHOD="POST">
Enter a color: <INPUT TYPE="text" NAME="color">
<INPUT TYPE="submit">
</FORM>
</BODY>
</HTML>
htdocs/color.cgi
index.html で実行されている color.cgi です。
perl は、かなり前にやってましたが、すっかり忘れたので、ChatGPT に解説してもらいました。
シェバンのオプションの T は、taint(汚染)チェックを有効化している、とのことで、セキュリティ強化の意味だそうです。もう1つのオプションの w は、警告を有効化しているそうです。
use strict は、文法の厳格化で、use CGI は、CGIモジュールの読み込みだそうです。
あとは、コメントに書きました。
#!/usr/bin/perl -Tw
use strict;
use CGI;
my($cgi) = new CGI;
print $cgi->header;
my($color) = "blue";
$color = $cgi->param('color') if defined $cgi->param('color');
print $cgi->start_html(-title => uc($color),
-BGCOLOR => $color);
print $cgi->h1("This is $color");
print $cgi->end_html;
おわりに
今回は、GitHub に公開されている OSS のうち、軽めの HTTPサーバの実装である Tinyhttpd を調査しました。実際に動かすまでに、いろいろと対応する必要があったので、少し時間がかかりました。また、関数の説明については、主要なところは書きましたが、必要に応じて追加しようと思います。
次回は、Tinyhttpd に手を加えて、CodeQL の結果がどう変わっていくのかを調査したいと思います。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。