Skip to main content

ValgrindでCコードの脆弱性を特定し、Snyk Codeで修正する

著者
feature snyk supply chain purple

2024年9月24日

0 分で読めます

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は優れたツールですが、日々C開発に携わる開発者にとって、このワークフローは拡張性に乏しく、解析のたびに本格的なプログラムをコンパイルしてビルドする必要があります。

Snyk Codeを使えば、コンパイルを行わなくてもコードベース内のこの脆弱性を特定できます。拡張機能をインストールしてファイルを開くだけで、脆弱性が表示されます。Snyk Codeは、ビルドやコンパイルの手順を必要としないソースコードを機械学習技術で解析する、静的コード解析ツールです。この方法により、Snykがコードをスキャンしてセキュリティ脆弱性を検出する際、信頼性が高く、誤検知が少ない、迅速なフィードバックを得られます。

メモリを割り当てた後に解放していないことを示すSnykの警告が表示された、Cプログラムのコードエディター。

パストラバーサル、バッファオーバーフローなど、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という空でないファイルがあれば、プログラムはそのファイルを読み込み、内容を出力します。

たとえば、次のようになります。

Size of the file: 44
Contents of the file: FROM alpine:latest
RUN apk add g++ valgrind

問題なさそうです。ディレクトリ構造をさかのぼるファイルを指定したら、どうなるでしょうか?

$ ./program3.app "../../../../../etc/passwd"

このプログラムにはパストラバーサルの脆弱性があり、攻撃者がシステム上の機密ファイルを読み取れる可能性があります。

ほかの脆弱性をテストするには、次の入力を試してください。

  • 存在しないファイルを指定する

  • 空のファイルを指定する

  • 長すぎるファイル名またはフルパス(50文字超)を指定する

こうしたセキュリティの問題の中には、開発者のCプログラミングスキルだけでは対処できないものもあります。たとえばパストラバーサルへの対策には、安全なメモリ管理や確立されたC開発の専門知識に加えて、アプリケーションセキュリティに関する理解も必要です。

幸い、IDEにプログラムのコードを貼り付けると、Snyk拡張機能がCコードを解析し、安全でないコーディング慣行や一般的なアプリケーションセキュリティの問題を検出します。問題箇所をコード内に示しながら、すばやく報告します。 

VS Codeに表示されたCコードで、Snykがバッファオーバーフロー、二重解放、解放後使用の脆弱性を検出しています。

SnykでCおよびC++コードを保護する

製造業や産業分野でセキュリティ侵害が発生すると、業務の停止、安全上の危険、経済的損失など、壊滅的な結果につながる可能性があります。CおよびC++のコードセキュリティを確保することは、単なるベストプラクティスではなく、必須事項です。開発者が開発プロセスの早い段階で脆弱性を特定し、修正できるよう、Snykが支援します。

最先端のインテリジェンスでコードを保護

わずか30分で、Snyk CodeのSAST機能を幅広くご紹介します。