前回 は、CodeQL の挙動を理解するために、Tinyhttpd を対象として、OverflowStatic.ql をいろいろ変更して、その結果を確認しました。
今回は、CERT C というセキュアコーディングについて調べたいと思います。まず、CERT C のルールを理解して、実際に、違反しているコードを実装してみて、挙動を確認してみたいと思います。
それでは、やっていきます。
はじめに
「セキュリティ」の記事一覧です。良かったら参考にしてください。
セキュリティの記事一覧
以下は、「CERT C コーディングスタンダード」を公開している、JPCERT コーディネーションセンター(JPCERT/CC)のサイトです。JPCERT/CC とは、Japan Computer Emergency Response Team Coordination Center の略で、コンピュータセキュリティに関わる事象への対応を事業内容とした、非営利の一般社団法人です。
www.jpcert.or.jp
JPCERT/CC のサイトで公開している「CERT C コーディングスタンダード」のページは以下です。
www.jpcert.or.jp
「CERT C コーディングスタンダード」は、カーネギーメロン大学が公開している、以下の「SEI CERT C Coding Standard」を翻訳してくれたものです。
wiki.sei.cmu.edu
「SEI CERT C Coding Standard」は更新が続いてますが、JPCERT/CC の「CERT C コーディングスタンダード」は、完全に追従して更新しているわけではなさそうです。
とはいえ、日本語の方が読みやすいので、「CERT C コーディングスタンダード」はありがたいです。01. プリプロセッサ(PRE)から、14. 並行性(CON)と、49. 雑則(MSC)、50. POSIX(POS)の 16 のカテゴリに分かれています。「SEI CERT C Coding Standard」を見ると、51. Microsoft Windows(WIN)が追加されています。
今回は、少し難しかった、04. 整数(INT)の INT02-C をまとめておこうと思います。
INT02-C:整数変換のルールを理解する
C言語で、整数型で、型が異なる変数の演算の場合、暗黙的に型変換が行われます。ここでは、その変換規則について解説されています。
この変換規則は、「整数拡張」、「整数変換の順位」、「通常の算術型変換」で構成されています。
これらを順番に見ていき、必要に応じて、実際にソースコードを動かして、動作を確認していきたいと思います。
整数拡張
int より小さな整数型は、演算する前に、まず、整数拡張されます。元の型の全ての値を intで 表現できる場合は、小さな型から int型に変換します。それ以外の場合は、unsigned int に変換されます。演算とは、通常の演算に加えて、単項の「+」、「-」、「~」、シフト演算を含みます。
int より小さな整数型で、unsigned int に変換される場合というのは、int が 16bit の場合だそうです。int と short が 16bit なので、unsigned short の場合、int の範囲を超える可能性のある型ということで、unsigned int に変換されるとのことです。しかし、現在は、ほとんどの環境で、int は 32bit なので、あまり気にしなくて良さそうです。
整数拡張の具体的なケース
まず、演算前に int型に変換されるソースコードが掲載されているので、実際に動かしてみます。
#include <stdio.h>
#include <stdint.h>
#include <limits.h>
void integer_promotions( void )
{
signed char cresult, c1, c2, c3;
c1 = 100;
c2 = 3;
c3 = 4;
cresult = c1 * c2 / c3;
printf( "c1(100) * c2(3) / c3(4) -> %d\n", cresult );
}
int main( int argc, void *argv[] )
{
integer_promotions();
return 0;
}
ビルドして、実行します。
ちゃんと、整数拡張が行われていることが確認できました。演算が終わった後、cresult に格納されるときに、singed char の範囲になりますが、今回は 75 なので、そのまま格納されます。
$ gcc -o int02c.out int02c.c
$ ./int02c.out
c1(100) * c2(3) / c3(4) -> 75
整数拡張で問題が発生するケース
続いて、整数拡張の引き起こす問題に関する違反コードと適合コードが掲載されていたので、実際に動かしてみます。
#include <stdio.h>
#include <stdint.h>
#include <limits.h>
void integer_promotions_NG( void )
{
uint8_t port = 0x5a;
uint8_t result_8 = ( ~port ) >> 4;
printf( "( ~port(0x5a) ) >> 4 -> 0x%02x\n", result_8 );
printf( "~port -> 0x%x, ~(uint8_t)port -> 0x%x, ~port >> 4 -> 0x%x\n", ~port, ~(uint8_t)port, ~port >> 4 );
}
void integer_promotions_OK( void )
{
uint8_t port = 0x5a;
uint8_t result_8 = (uint8_t)( ~port ) >> 4;
printf( "(uint8_t)( ~port(0x5a) ) >> 4 -> 0x%02x\n", result_8 );
}
int main( int argc, void *argv[] )
{
integer_promotions_NG();
integer_promotions_OK();
return 0;
}
ビルドして、実行します。
$ gcc -o int02c.out int02c.c
$ ./int02c.out
( ~port(0x5a) ) >> 4 -> 0xfa
~port -> 0xffffffa5, ~(uint8_t)port -> 0xffffffa5, ~port >> 4 -> 0xfffffffa
(uint8_t)( ~port(0x5a) ) >> 4 -> 0x0a
これについては、どういう型変換が行われるか、分かりにくいので、以下の表が掲載されていました。
| 式 |
種類 |
値 |
備考 |
| port |
uint8_t |
0x5a |
|
| ~port |
int |
0xffffffa5 |
|
| ~port >> 4 |
int |
0x0ffffffa |
値がマイナスかどうかは処理系定義である |
| result_8 |
uint8_t |
0xfa |
|
こういう実装は、今回のことを知らずに、やってしまいそうなので注意が必要ですね。
整数変換の順位
簡単に言うと、各型は、順位付けされるということです。その順位とは、以下となります。
- long long int > long int > int > short int > signed char
- 符号無し整数型は、対応する同じ符号付き整数型と同じ順位を持つ
これらの順位は、次の算術型変換に使われます。
通常の算術型変換
演算が行われるとき、まず、整数拡張が行われた後、以下の型変換が行われます。
- 同じ型:両方のオペランドが同じ型をもつ場合、更なる型変換は行わない
- 両方符号付き or 両方符号無し:そうではない場合、両方のオペランドが符号付き整数型、又は、両方のオペランドが符号無し整数型の場合、整数変換の順位の低い方の型を、高い方の型に変換する
- 符号付き≦符号無し:そうではない場合、符号無し整数型のオペランドが、他方のオペランドの整数変換の順位より高い、又は、等しい順位をもつならば、符号付き整数型のオペランドを、符号無し整数型のオペランドの型に変換する
- 符号付き>符号無し:そうではない場合、符号付き整数型のオペランドの型が、符号無し整数型のオペランドの型のすべての値を表現できるならば、符号無し整数型のオペランドを、符号付き整数型のオペランドの型に変換する
符号付き>符号無し:そうではない場合、両方のオペランドを、符号付き整数型のオペランドの型に対応する符号無し整数型に変換する
と 2. は分かりやすいです。3. 以降について考えていきます。
3. 符号付き≦符号無しの場合
まず、3. の違反コードと適合コードが掲載されていましたので、実際に動かしてみます。
si(-1)と ui(1)と比較して、その結果を表示するコードです。普通に考えると、ui(1)の方が大きいので、si < ui の結果は、true(1)になりますが、違反コードの結果は、false(0)になっています。これは、「3. 符号付き≦符号無し」の場合、符号付き整数型のオペランドを符号無し整数型オペランドの型に変換されるため、si(-1)が unsigned int に変換され、UINT_MAX になるためです。
これは、うっかり見逃してしまうケースなので、非常に注意したいところです。
#include <stdio.h>
#include <stdint.h>
#include <limits.h>
void arithmetic_conversions_3_NG( void )
{
int si = -1;
unsigned int ui = 1;
printf( "si(-1) < ui(1) -> %d\n", si < ui );
printf( "si -> %u, UINT_MAX -> %u\n", si, UINT_MAX );
}
void arithmetic_conversions_3_OK( void )
{
int si = -1;
unsigned int ui = 1;
printf( "si(-1) < (int)ui(1) -> %d\n", si < (int)ui );
}
void arithmetic_conversions_3_OK_2( void )
{
int si = -1;
unsigned int ui = 1;
printf( "si(-1) < 0 || si(-1) < ui(1) -> %d\n", si < 0 || si < ui );
}
int main( int argc, void *argv[] )
{
arithmetic_conversions_3_NG();
arithmetic_conversions_3_OK();
arithmetic_conversions_3_OK_2();
return 0;
}
ビルドして、実行します。期待通りの結果となりました。
$ gcc -o int02c.out int02c.c
$ ./int02c.out
si(-1) < ui(1) -> 0
si -> 4294967295, UINT_MAX -> 4294967295
si(-1) < (int)ui(1) -> 1
si(-1) < 0 || si(-1) < ui(1) -> 1
4. 符号付き>符号無しの場合
こちらは、大きな問題になるケースはないと思います。しいて言えば、直感的に、符号無しの方に変換されると思ってしまう、ことでしょうか。
以下の例では、符号付きの long型と、符号無しの unsigned int型の計算をしています。直感的に、long型が、符号無し型に変換されて、負の値の場合は、巨大な値になると思ってしまうところですが、実際は、unsigned int型のすべての値は、long型で表現できるので、unsigned int型が long型に変換されて、普通に計算されます。
#include <stdio.h>
#include <stdint.h>
#include <limits.h>
void arithmetic_conversions_4_or_5( void )
{
long sl = -1;
unsigned int ui = 1;
printf( "arithmetic_conversions_4_or_5(): long sl(-1) < unsigned int ui(1) -> %d\n", sl < ui );
}
int main( int argc, void *argv[] )
{
arithmetic_conversions_4_or_5();
return 0;
}
ビルドして、実行します。想定通りの結果になりました。
$ gcc -o int02c.out int02c.c
$ ./int02c.out
long sl(-1) < unsigned int ui(1) -> 1
5. 符号付き>符号無しの場合
以下は、問題の発生が想定されるソースコードです。
符号付きの型が long で、符号無しの型が unsigned int であり、long型、unsigned int型が 32bit の場合、long型は、unsigned int のすべての値を表現できないので、「5. 符号付き>符号無し」となり、符号無しに変換されることになります。
問題となるケースは、上の 4. 符号付き>符号無しの場合 で実行したソースコードを 32bit環境で実行した場合になります。
gcc で 32bitアプリをビルドできるように、-m32 をやってみます。
先ほどとは異なり、判定が false になりました。
$ sudo apt install gcc-multilib g++-multilib
$ gcc -m32 -o int02c.out int02c.c
$ file int02c.out
int02c.out: ELF 32-bit LSB pie executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=3fe0d15851e963a4a28fbc0ac128e44e79652ddf, for GNU/Linux 3.2.0, not stripped
$ ./int02c.out
long sl(-1) < unsigned int ui(1) -> 0
32bit環境を作る方法(Docker、Raspi 32bitOS)
-m32 を付けてビルドする以外の方法を考えます。
ChatGPT に、聞いたところ、Docker で 32bitコンテナで出来るよ、と言われました。そこで、WSL の Ubuntu24.04 に Docker をインストールして、以下のように、Debian の i386 のコンテナを起動しました。
$ docker run -it --platform linux/386 debian:stable-slim bash
Unable to find image 'debian:stable-slim' locally
stable-slim: Pulling from library/debian
cb61988601c6: Pull complete
5a84de9ab148: Download complete
Digest: sha256:7cb087f19bcc175b96fbe4c2aef42ed00733a659581a80f6ebccfd8fe3185a3d
Status: Downloaded newer image for debian:stable-slim
root@37c3abad6667:/
build-essential、nano をインストールして、上記のコードを実行してみました。同様に、誤った判定となりました。
root@37c3abad6667:/# apt install build-essential
root@37c3abad6667:~# apt install nano
root@37c3abad6667:~# nano int02c.c
root@37c3abad6667:~# gcc -o int02c.out int02c.c
root@37c3abad6667:~# ./int02c.out
long sl(-1) < unsigned int ui(1) -> 0
Raspi4 に、32bit OS の Raspberry Pi OS を焼いて、動かしてみました。同じく、0 が出力されて、誤った判定となりました。
おわりに
今回は、C言語における整数の暗黙の型変換について、CERT C に書かれているルールを実際に動かして、結果を確認してみました。やはり、書かれていることを実際に動かしてみると、理解が進みますね。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。