前回 は、CodeQL の挙動を理解するために、Tinyhttpd を対象として、OverflowStatic.ql をいろいろ変更して、その結果を確認しました。
今回は、CERT C というセキュアコーディングについて調べたいと思います。まず、CERT C のルールを理解して、実際に、違反しているコードを実装してみて、挙動を確認してみたいと思います。
それでは、やっていきます。
はじめに
「セキュリティ」の記事一覧です。良かったら参考にしてください。
・第2回:Ghidraで始めるリバースエンジニアリング(使い方編)
・第3回:VirtualBoxにParrotOS(OVA)をインストールする
・第4回:tcpdumpを理解して出力を正しく見れるようにする
・第5回:nginx(エンジンエックス)を理解する
・第6回:Python+Flask(WSGI+Werkzeug+Jinja2)を動かしてみる
・第7回:Python+FlaskのファイルをCython化してみる
・第8回:shadowファイルを理解してパスワードを解読してみる
・第9回:安全なWebアプリケーションの作り方(徳丸本)の環境構築
・第10回:Vue.jsの2.xと3.xをVue CLIを使って動かしてみる(ビルドも行う)
・第11回:Vue.jsのソースコードを確認する(ビルド後のソースも見てみる)
・第12回:徳丸本:OWASP ZAPの自動脆弱性スキャンをやってみる
・第13回:徳丸本:セッション管理を理解してセッションID漏洩で成りすましを試す
・第14回:OWASP ZAPの自動スキャン結果の分析と対策:パストラバーサル
・第15回:OWASP ZAPの自動スキャン結果の分析と対策:クロスサイトスクリプティング(XSS)
・第16回:OWASP ZAPの自動スキャン結果の分析と対策:SQLインジェクション
・第17回:OWASP ZAPの自動スキャン結果の分析と対策:オープンリダイレクト
・第18回:OWASP ZAPの自動スキャン結果の分析と対策:リスク中すべて
・第19回:CTF初心者向けのCpawCTFをやってみた
・第20回:hashcatの使い方(GPU実行時間の見積りとパスワード付きZIPファイル)
・第21回:Scapyの環境構築とネットワークプログラミング
・第22回:CpawCTF2にチャレンジします(クリア状況は随時更新します)
・第23回:K&Rのmalloc関数とfree関数を理解する
・第24回:C言語、アセンブラでシェルを起動するプログラムを作る(ARM64)
・第25回:機械語でシェルを起動するプログラムを作る(ARM64)
・第26回:入門セキュリhttps://github.com/SECCON/SECCON2017_online_CTF.gitティコンテスト(CTFを解きながら学ぶ実践技術)を読んだ
・第27回:x86-64 ELF(Linux)のアセンブラをGDBでデバッグしながら理解する(GDBコマンド、関連ツールもまとめておく)
・第28回:入門セキュリティコンテスト(CTFを解きながら学ぶ実践技術)のPwnable問題をやってみる
・第29回:実行ファイルのセキュリティ機構を調べるツール「checksec」のまとめ
・第30回:setodaNote CTF Exhibitionにチャレンジします(クリア状況は随時更新します)
・第31回:常設CTFのksnctfにチャレンジします(クリア状況は随時更新します)
・第32回:セキュリティコンテストチャレンジブックの「Part2 pwn」を読んだ
・第33回:セキュリティコンテストチャレンジブックの「付録」を読んでx86とx64のシェルコードを作った
・第34回:TryHackMeを始めてみたけどハードルが高かった話
・第35回:picoCTFを始めてみた(Beginner picoMini 2022:全13問完了)
・第36回:picoCTF 2024:Binary Exploitationの全10問をやってみた(Hardの1問は後日やります)
・第37回:picoCTF 2024:Reverse Engineeringの全7問をやってみた(Windowsプログラムの3問は後日やります)
・第38回:picoCTF 2024:General Skillsの全10問をやってみた
・第39回:picoCTF 2024:Web Exploitationの全6問をやってみた(最後の2問は解けず)
・第40回:picoCTF 2024:Forensicsの全8問をやってみた(最後の2問は解けず)
・第41回:picoCTF 2024:Cryptographyの全5問をやってみた(最後の2問は手つかず)
・第42回:picoCTF 2023:General Skillsの全6問をやってみた
・第43回:picoCTF 2023:Reverse Engineeringの全9問をやってみた
・第44回:picoCTF 2023:Binary Exploitationの全7問をやってみた(最後の1問は後日やります)
・第45回:書籍「セキュリティコンテストのためのCTF問題集」を読んだ
・第46回:書籍「詳解セキュリティコンテスト」のReversingを読んだ
・第47回:書籍「詳解セキュリティコンテスト」のPwnableのシェルコードを読んだ
・第48回:書籍「バイナリファイル解析 実践ガイド」を読んだ
・第49回:書籍「詳解セキュリティコンテスト」Pwnableのスタックベースエクスプロイトを読んだ
・第50回:書籍「詳解セキュリティコンテスト」Pwnableの共有ライブラリと関数呼び出しを読んだ
・第51回:picoCTF 2025:General Skillsの全5問をやってみた
・第52回:picoCTF 2025:Reverse Engineeringの全7問をやってみた
・第53回:picoCTF 2025:Binary Exploitationの全6問をやってみた
・第54回:書籍「詳解セキュリティコンテスト」Pwnableの仕様に起因する脆弱性を読んだ
・第55回:システムにインストールされたものと異なるバージョンのglibcを使う方法
・第56回:書籍「詳解セキュリティコンテスト」Pwnableのヒープベースエクスプロイトを読んだ
・第57回:書籍「解題pwnable」の第1章「準備」を読んだ
・第58回:書籍「解題pwnable」の第2章「login1(スタックバッファオーバーフロー1)」を読んだ
・第59回:書籍「解題pwnable」の第3章「login2(スタックバッファオーバーフロー2)」を読んだ
・第60回:書籍「解題pwnable」の第4章「login3(スタックバッファオーバーフロー3)」を読んだ
・第61回:書籍「解題pwnable」の第5章「rot13(書式文字列攻撃)」を読んだ
・第62回:GitHubが開発した静的解析ツール(脆弱性検出ツール)のCodeQLを使ってみる
・第63回:CodeQL(静的解析ツール)で使われるクエリの選ばれ方を調べた
・第64回:CodeQL(静的解析ツール)のクエリの書き方を調べた
・第65回:CodeQL(静的解析ツール)で使われているアラートクエリの中身を調べる
・第66回:CodeQL(静的解析ツール)で使われているパスクエリの中身を調べる
・第67回:CodeQL(静的解析ツール)をVSCodeで使う方法を理解する
・第68回:CodeQL(静的解析ツール)の挙動を確認するための対象ソースコードとしてTinyhttpdを調査する
・第69回:Tinyhttpdを使用してCodeQL(静的解析ツール)のクエリの挙動を確認する
・第70回:セキュアコーディング:CERT C INT02-C 「整数変換のルールを理解する」を調べる ← 今回
以下は、「CERT C コーディングスタンダード」を公開している、JPCERT コーディネーションセンター(JPCERT/CC)のサイトです。JPCERT/CC とは、Japan Computer Emergency Response Team Coordination Center の略で、コンピュータセキュリティに関わる事象への対応を事業内容とした、非営利の一般社団法人です。
JPCERT/CC のサイトで公開している「CERT C コーディングスタンダード」のページは以下です。
「CERT C コーディングスタンダード」は、カーネギーメロン大学が公開している、以下の「SEI CERT C Coding Standard」を翻訳してくれたものです。
「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> // 整数拡張 (Integer Promotions) void integer_promotions( void ) { signed char cresult, c1, c2, c3; c1 = 100; c2 = 3; c3 = 4; // 普通に考えると、c1 * c2 で、singned char の上限の 127 を超えるように見えるが、 // 整数拡張が行われるため、そうはならない // // c1、c2、c3 は、それぞれ int型に整数拡張される // 100 * 3 = 300、300 / 4 = 75 になり、cresult に格納される 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> // 整数拡張 (Integer Promotions) 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> // 通常の算術型変換 (Arithmetic Conversions) void arithmetic_conversions_3_NG( void ) { int si = -1; unsigned int ui = 1; // 3. 符号付き≦符号無し -> 符号付きが、符号無しに変換される // -1 は、符号無しに変換されると、UINT_MAX に変換されてしまう 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; // 3. 符号付き≦符号無し -> 符号付きが、符号無しに変換される // int同士(1. 同じ型)なので型変換されなくなる // ただし、これは、ui が int で表現できる値であることが // あらかじめ、分かっている場合にのみ、使える実装であることに注意 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; // 3. 符号付き≦符号無し -> 符号付きが、符号無しに変換される // ui が int で表現できる値であるかどうかが分からない場合の実装 // 掲載されている適合コードでは、2番目の si に (unsigned)si と // キャストされていたが、型変換で unsigned に変換されるので不要だと思う 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; // signed 64-bit or signed 32bit (値 = -1) unsigned int ui = 1; // unsigned 32-bit (値 = 1) // 4. 符号付き>符号無し ※符号付きの型が、符号無しの型の全ての値を表現できる場合 // 5. 符号付き>符号無し ※符号付きの型が、符号無しの型の全ての値を表現できない場合 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 に書かれているルールを実際に動かして、結果を確認してみました。やはり、書かれていることを実際に動かしてみると、理解が進みますね。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。