Skip to main content

C/C++の脆弱性を気軽に学ぶ入門

著者
Headshot of Aviad Hahami

Aviad Hahami

feature vulnerabilities orange

2022年4月15日

0 分で読めます

ルーモス!

Snykが管理外の依存関係(主にC/C++ライブラリ)への対応を発表したことを受け、C以外のコミュニティの皆さんにも、Cの世界に潜むよくある高リスクの脅威を紹介したいと思います(わかりましたか?)。この記事は、CおよびC++の脆弱性について、その仕組みや引き起こす問題、修正方法を解説する「入門ガイド」です。

見つけにくい脆弱性とその発見場所

CとC++(この記事では、以降「C」として両方を指します)は、低水準プログラミング言語とされています。低水準言語と高水準言語(JS、Pythonなど)を比べると、主な違いはコンピューターのメモリ管理の仕組みにあります。高水準言語ではメモリの割り当て、使用、解放が管理されますが、Cではその責任を開発者が担います。そのため、処理のパフォーマンスや実装の最適化を細かく制御できますが、この種の言語ならではのさまざまな問題が生じる可能性もあります。

バッファオーバーフロー(CWE-121)と境界外書き込み(CWE-787)

バッファオーバーフローは、メモリ関連の脆弱性の中でもおそらく最も悪名高いものです。悪用は複雑な場合がありますが、脆弱性そのものは単純です。割り当てられたバッファの容量を超えて書き込むことです。厳密には、「バッファオーバーフロー」は通常、脆弱性の実際の悪用を指します。ただし、「スタックベースのバッファオーバーフロー」と「境界外書き込み」は本質的に同じ弱点であるため、ここではまとめて解説します。

バッファをオーバーフローさせても、常にスタック(またはヒープ)にそのまま「コードを書き込める」わけではないため、脆弱性の悪用は困難な場合があります。この問題を強化・緩和するため、これまでにさまざまな対策が導入されてきました。こうした対策は、OS、カーネルの機能、コンパイラーなど多くの要素を考慮してコードを保護します。たとえば、ASLR(アドレス空間配置のランダム化)、スタックカナリア、DEP(データ実行防止)などがあります。いずれも、バッファオーバーフローのようなメモリ破壊のバグを防ぐことを目的としています。実行時にこれらの対策のいずれかが検知されると、OSは実行を停止してSEGFAULTを発生させるため、脆弱性の悪用はより複雑になります。

こうした脆弱性とその悪用例を1つ紹介します。次のCプログラムを見てください。

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>

int main(int argc, char **argv)
  volatile int modified;
  char buffer[64];

  modified = 0;
  gets(buffer);

  if(modified != 0) {
    printf("you have changed the 'modified' variable\n");
  } else {
    printf("Try again?\n");
  }
}

サンプル提供: 0xRick、 掲載元: 本人のブログ

Cを知らなくても、コードを見ればbufferには64文字分のバッファが割り当てられ、modifiedは整数型だとわかります。ここまでは簡単ですね。

また、9行目でmodifiedに0が設定され、12行目で値が0かどうかを確認していることがわかります。お察しのとおり、目的は値を0以外にすることです。? 10行目ではgets 関数を使い、標準入力(=ユーザー入力)からbuffer変数に読み込んでいます。getsに馴染みがない方のために説明すると、この関数は改行文字(\n)を読み取るまで、標準入力から指定されたバッファにデータを読み込みます。

(上記のリンク先のYouTube動画を見たので)スタックは低位アドレス側に向かって伸びること、またスタックのデータ構造上、メモリ内ではmodifiedがbufferの「下」にあることがわかっています。つまり、bufferに64文字を超えて書き込むと、modifiedの値を上書きし始めます!これこそが、魔法のバッファオーバーフローです!

この具体例はデモなので、大きな害はありません。しかし、パスワード変数や、コンピューターがアクセスするURLの値を上書きできてしまったらどうなるでしょうか。対策は簡単です。「終端文字」があるかどうかだけでなく、入力の長さも確認するfgets関数を使うことが推奨されています。

解放後使用(CWE-416)

解放後使用の脆弱性は、その名のとおり、変数への参照が解放された後にその変数を使用すると発生します。ソフトウェアの処理フローにおけるメモリ管理の不備が原因で、変数がクリアされた後に使用されることで、予期しない動作やアプリケーションへの想定外の影響が生じます。

実際の脆弱性とその悪用を確認するには、チャレンジでUAF脆弱性を悪用する方法を解説したLiveOverflowの動画がおすすめです。

整数オーバーフロー/アンダーフロー(CWE-190、CWE-191)

整数オーバーフローとアンダーフローは、コンピューターにおける数値の表現方法に起因する、似た種類のバグ(そして後に脆弱性)です。

変数の型や、コンピューターで数値がどのように表現されるかを詳しく掘り下げることはしませんが、数値の表現方法には主に符号付きと符号なしの2種類があります。符号付き変数では負の数と正の数を表せますが、符号なし変数が格納できるのは正の数のみです。

整数のオーバーフローとは、コンピューターに格納しようとした値が、格納可能な最大値より大きい状態です。整数のアンダーフローとは、格納しようとした値が最小値より小さい状態です(たとえば、符号なし整数に負の数を格納しようとする場合)。

どちらの場合も、結果は同様です。値が「一周」して格納可能な範囲の先頭または末尾から数え直されるため、値が変わります。オーバーフローでは値が0から始まり、アンダーフローでは格納可能な最大値から始まります(つまり、8ビットの符号なし整数は256[10進数]に一周します)。

この図は、変数の型と、それぞれが保持できる値を示しています。

共通の縦軸上に、長さの異なる6本の赤い横棒を円形の端点とともに示した図。

整数オーバーフローがバッファオーバーフローにつながった悪用例が、OpenSSH v3.3で発見されました(CVE-2002-0639)。

次のコードを見てみましょう。

nresp = packet_get_int();
if (nresp > 0) {
 response = xmalloc(nresp*sizeof(char*));
 for (i = 0; i < nresp; i++)
  response[i] = packet_get_string(NULL);
}

nrespが1073741824で、sizeof(char*)が4(一般的なポインターサイズ)の場合、nresp*sizeof(char*)はオーバーフローし、値が0になります。そのため、xmalloc()は0バイトのバッファを受け取り、割り当てます。続くループでは、割り当てられていないメモリ領域に書き込むため、ヒープバッファオーバーフローが発生します。これを利用して、攻撃者が任意のコードを実行する可能性があります。

ヌルポインターの参照外し(CWE-467)

参照外しとは、あるアドレスにある値に対して操作を行うことです。この脆弱性をわかりやすく説明するため、例を見てみましょう。

#include <stddef.h>

void main(){
        int *x = NULL;
        *x = 1;
}

Cの標準規格によると、上記のコードを実行すると「未定義動作」が発生する可能性があります。ただし、ほとんどの実装ではSEGFAULTが発生します。これは、ソフトウェアが制限されたメモリ領域にアクセスしようとし、メモリアクセス違反を起こしたことを意味します。その結果、OSによってソフトウェアの実行が終了されます。

境界外読み取り(CWE-125)

境界外読み取りは、「指定された場所やバッファの範囲外を読み取る」ことで発生します。この脆弱性の結果は、最悪の場合でもシステムクラッシュ、最悪の場合はアプリ内の情報(他のユーザーのパスワードなど)の漏えいにつながります。どちらも避けたい事態です。

この脆弱性の例として、PureFTPdアプリケーションのコードを見てみましょう。17行目に注目してください。s1の長さがs2より大きい場合、8行目ではs1の長さ分だけ反復処理を行うため、10行目でアクセスする情報がs2の範囲を超えてしまいます。その結果、境界外読み取りが発生します。

int pure_memcmp(const void *const b1_, const void *const b2_, size_t len)
  {
   const unsigned char *b1 = (const unsigned char *) b1_;
   const unsigned char *b2 = (const unsigned char *) b2_;
   size_t i;
   unsigned char d = (unsigned char) 0 U;
   for (i = 0 U; i < len; i++)
   {
     d |= b1[i] ^ b2[i];
   }
   return (int)((1 &((d - 1) >> 8)) - 1);
}

int pure_strcmp(const char *const s1, const char *const s2)
{
  return pure_memcmp(s1, s2, strlen(s1) + 1 U);
}

このバグにはCVE-2020-9365が割り当てられています。レポートを読むことができます。

まとめと次のステップ

この記事を通じて、C/C++の脆弱性がどのようなものか、通常どこに潜み、どんな形で現れるかについて、おおまかに理解できたでしょう。こうした悪用例の中には、一見すると複雑に思えるものもあります。しかし、理解を深めることでソフトウェアの内部構造への知識が広がり、重大なバグの回避や防止に役立ちます。

上記の整数オーバーフローの説明で示したように、脆弱性は連鎖して、悪意ある攻撃に悪用される脆弱な攻撃チェーンを生み出すことがあります。

Snyk Open SourceでC/C++に対応した今後も、CおよびC++の脆弱性の発見、悪用、修正方法を紹介するコンテンツをお届けしていきます。

オープンソースの依存関係を安全に

開発者を第一に考えたSnykのツールなら、脆弱なオープンソースの依存関係やその推移的依存関係を、ワンクリックで修正するプルリクエストを作成できます。

続きを読む

Blog

フロンティアモデルは脆弱性を発見した。攻撃者だけがエクスプロイトチェーンを見つけた。

静的解析で欠陥は見つかりましたが、ライブ攻撃テストで侵害につながる連鎖を実証できたのは唯一でした。Evo COS、Claude Security、Claude Code Securityを比較します。

feature insights context
Blog

自律型攻撃はすでに始まっている。防御もそのスピードに追いつかなければならない。

自律型攻撃者によって、防御に使える時間は短くなっています。継続的な検出、修復、検証、予防で、セキュリティチームが攻撃に歩調を合わせる方法をご紹介します。

Blog

脆弱性のバックログはもはや技術的負債ではなく、攻撃対象領域です

増え続ける脆弱性のバックログは、単なる技術的負債ではありません。攻撃対象領域そのものです。古いリスクの前提、攻撃の自動化、そして連鎖する検出結果によって、なぜ新たなアプローチが求められているのかを解説します。