セキュアでない C コードを Valgrind で特定し、Snyk Code で修正する
C と C++ はソフトウェア開発における基礎であり続けています。これらの言語は、組み込み機器から製造、オペレーショナルテクノロジー (OT)、工業市場での高性能アプリケーションまで、幅広いシステムで使用されています。効率性、システムリソースに対する制御、パフォーマンスの高さから、ミッションクリティカルなプロジェクトに取り組んでいる開発者にとって不可欠なものになっています。
製造業と工業が経済成長の主な推進力である日本では、C と C++ は特に普及しています。日本の開発者はこれらの言語を使用して、自動車システムからファクトリーオートメーションまでのあらゆるものを支える、堅牢で効率性の高いソフトウェアを構築しています。C と C++ の精度と信頼性は、これらの産業で期待される高度な基準を維持するうえで不可欠です。
C と C++ におけるコードセキュリティの重要性
C と C++ は、比類のない制御とパフォーマンスを実現する一方、セキュリティに関する重大な課題もあります。自動メモリ管理などの組み込みの安全機能がないため、
バッファオーバーフローやUse-After-Freeやメモリリークなどの脆弱性の影響を受けやすくなっています。これらの脆弱性は単純で基本的なものですが、深刻な結果をもたらす場合があります。信頼性とセキュリティが最も重視される重要なソフトウェアでは特にそうです。
脆弱な C コードがメモリリークを発生させる
開発者が記述する可能性があり、メモリリークでのセキュリティ脆弱性を導くおそれがある脆弱な C コードの例を確認しましょう。
この C プログラムコードのセキュリティ脆弱性がわかりますか?
#include <stdio.h>
#include <stdlib.h>
void allocateMemory() {
int *ptr = (int *)malloc(10 * sizeof(int));
if (ptr == NULL) {
printf("Memory allocation failed\n");
return;
}
}
int main() {
allocateMemory();
return 0;
}
この program1.c
ファイルには、動的メモリ確保の脆弱性 (MISRA Dir 4.12, MISRA Rule 21.3) の例が含まれています。関数 allocateMemory()
でのセキュリティ脆弱性は、この関数は malloc()
を使用してメモリを確保しますが、メモリを解放しないため、メモリリークが発生することです。これは、動的メモリ確保を制限する MISRA ガイドラインに従うことで予防できます。
これは C プログラミングでよくあるミスで、プログラムで使用されるメモリが時間とともにどんどん増加するおそれがあります。その結果、最終的にはプログラムがメモリ不足になってクラッシュするか、システム上の他のプログラムがメモリ不足になる可能性があります。
int arr[10]
などを使用してメモリを静的に確保すればよいとは限りません。スタックにメモリを割り当てることができない場合があるからです。たとえば、ファイルをメモリに読み込む関数を記述しているとします。ランタイムまでファイルのサイズがわからないため、固定長配列を使用することはできません。代わりに、malloc
を使用すれば、ファイルのサイズがわかった段階で適切な量のメモリを正確に割り当てることができます。
最初にコンパイルしてから実行することで、プログラムを実行します。
gcc program1.c -o program1
./program1
プログラムでメモリリークが発生していますか?どうすれば修正できるでしょうか。
Valgrind を使用して C のセキュリティ脆弱性を発見する
Valgrind はプログラムでのメモリリークを発見するのに役立つ優れたツールです。Valgrind でプログラムを実行して、メモリリークが発生していないかを確認することができます。
システムに Valgrind をインストールします。Linux ベースのシステムを使用している場合は、パッケージマネージャーを使用してインストールできます。以下に Debian ベースの例を示します。
sudo apt-get install valgrind
注:macOS で ARM ベースのチップを使用している場合、Valgrind のサポートは利用できません。スキップして、Docker コンテナ内から Valgrind を実行します。
docker build -t "valgrind" . -f Dockerfile
次に、コンテナを実行して、現在のディレクトリをコンテナの /tmp
ディレクトリにマッピングします。
docker run -it -v $PWD:/tmp -w /tmp valgrind
次に、コンテナ内で program1.c
プログラムをコンパイルします。
gcc program1.c -o program1.app
これで、Valgrind でプログラムを実行して、メモリリークが発生していないか確認することができます。
valgrind --leak-check=full ./program1.app
Valgrind からの出力にメモリリークが表示されているはずです。
/tmp # valgrind --leak-check=full ./program1.app
==29== Memcheck, a memory error detector
==29== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==29== Using Valgrind-3.23.0 and LibVEX; rerun with -h for copyright info
==29== Command: ./program1
==29==
==29==
==29== HEAP SUMMARY:
==29== in use at exit: 40 bytes in 1 blocks
==29== total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==29==
==29== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==29== at 0x48E978C: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-arm64-linux.so)
==29== by 0x108823: allocateMemory (in /tmp/program1)
==29== by 0x108857: main (in /tmp/program1)
==29==
==29== LEAK SUMMARY:
==29== definitely lost: 40 bytes in 1 blocks
==29== indirectly lost: 0 bytes in 0 blocks
==29== possibly lost: 0 bytes in 0 blocks
==29== still reachable: 0 bytes in 0 blocks
==29== suppressed: 0 bytes in 0 blocks
==29==
==29== For lists of detected and suppressed errors, rerun with: -s
==29== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
Valgrind は優れたツールですが、場当たり的な開発者はこのワークフローを拡張することはできません。また、Valgrind で分析する成熟したプログラムをコンパイルしてビルドする必要もあります。
Snyk Code を使用すれば、コンパイル作業さえ行わずにコードベースでこの脆弱性を特定できます。拡張機能をインストールしてファイルを開くだけで脆弱性が表示されるのです。Snyk Code は、機械学習の手法を適用して静的コードを特定する静的コード分析ツールであり、ビルドとコンパイルの手順が必要ありません。このアプローチにより、コードの脆弱性がないか Snyk がスキャンする際に、信頼性が高く誤検出率の低いスピーディーなフィードバックループが実現します。
パストラバーサル、バッファオーバーフロー、その他の C の脆弱性を検出する
Snyk Code が活用する SAST エンジンでは、malloc
のメモリリーク以外の脆弱性タイプも検出できます。
より複雑な例を見ていきましょう。この例では、セキュリティの脆弱性を導く C でのコードの使い方がいくつか示されています。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(int argc, char *argv[]) {
// get the filename from the first command line argument
char *filename = argv[1];
// append the filename to the current directory
char path[50] = "./";
strcat(path, filename);
FILE *file = fopen(path, "r");
if (file == NULL) {
printf("Error opening file!\n");
exit(1);
}
// read the contents of the file into memory and print the size of the file:
fseek(file, 0, SEEK_END);
long fsize = ftell(file);
fseek(file, 0, SEEK_SET);
char *string = malloc(fsize + 1);
fread(string, fsize, 1, file);
free(string);
printf("Size of the file: %ld\n", fsize);
printf("Contents of the file: %s\n", string);
if (string != NULL) {
free(string);
}
if (fsize == 0) {
string[0] = 'A';
}
fclose(file);
return 0;
}
上記の C プログラムの例には脆弱性があちこちに存在しますが、明確にするためにセキュアでないコードを紹介する非常に簡潔で単純な方法です。さらに、実際のコードベースでも容易に起こり得る、気付かぬうちに行われるコードの使い方がいくつかあります。
プログラムがコンパイルされたら、そのプログラムを実行できます。
./program3.app "text.txt"
同じディレクトリに text.txt
という名前のファイルがあり、そのファイルが空でない場合、プログラムはファイルを読み取ってその内容を出力します。
例:
ファイルのサイズ: 44
ファイルの内容: FROM alpine:latest
RUN apk add g++ valgrind
問題ないように見えます。ディレクトリ構造をトラバースするファイルを渡したらどうなるでしょうか。
./program3.app "../../../../../etc/passwd"
このプログラムにはパストラバーサルの脆弱性が存在するため、攻撃者はシステム上の機密ファイルを読み取ることが可能になります。
その他の脆弱性をテストするには、以下を試します。
存在しないファイルを渡す
空のファイルを渡す
長すぎるファイル名またはフルパスを渡す (50 文字以上)
このようなセキュリティの問題のいくつかは、開発者の C プログラミングスキルをはるかに上回るものです。たとえばパストラバーサルに対しては、安全なメモリ管理や十分に築かれた C 開発の専門知識よりも、アプリケーションセキュリティへの意識が必要になります。
幸いにも、プログラムのコードを IDE に貼り付ければ、Snyk の拡張機能が、セキュアでないコーディング規約やアプリケーションセキュリティに関する一般的な問題がないか C コードを分析し、それらをインラインコードコンテキストとともにすぐに報告してくれます。
C および C++ コードを Snyk で保護する
製造業と工業分野では、セキュリティ侵害によって、運用上のダウンタイム、 安全上の危険、経済的損失など、破滅的な結果がもたらされる可能性があります。C および C++ でのコードセキュリティの確保は、ベストプラクティスではなく、必須のことなのです。そこで出番となるのが Snyk などのプラットフォームです。このようなプラットフォームを利用すれば、開発者は開発プロセスの早期に脆弱性を特定し、修正することができます。