DCL39-C. 信頼境界を越えて構造体を渡すとき情報漏えいしない
C 言語規格のセクション 6.7.2.1 では、構造体データのメモリ上の配置について論じている。ビットフィールドでないメンバのアラインメントについては実装依存であること、また、構造体データの内部や末尾にパディングデータが置かれる可能性があることが規定されている。さらに、構造体中のメンバの初期化によってパディングデータも初期化されるとは限らない、と記載されている。C 言語規格のセクション 6.2.6.1 パラグラフ 6 では以下のように記載されている[ISO/IEC 9899:2011]:
「構造体型やユニオン型のオブジェクト(あるいはそのメンバオブジェクト)に値を代入するとき、パディングバイトの部分の値は不定である。」
さらに、ビットフィールドが置かれる記憶域のなかにはパディングビットが含まれる可能性がある。自動記憶域に置かれるオブジェクトでは、これらのパディングビットの値は不定であり、センシティブな情報の漏えいにつながる可能性がある。
信頼境界の外部に構造体へのポインタを渡すときは、構造体中のパディングバイトやパディングビットを通じてセンシティブな情報が漏えいしないよう、注意しなければならない。
違反コード
この違反コード例はカーネル空間で実行されるという想定である。構造体 arg
のデータをユーザ空間にコピーする。しかし、構造体メンバのアラインメントの都合などにより、構造体データのなかにはパディングデータが含まれている可能性がある。パディングデータの部分にセンシティブな情報が含まれている場合、ユーザ空間へのコピーによって情報漏えいが発生する可能性がある。
#include <stddef.h>
struct test {
int a;
char b;
int c;
};
/* データをユーザ空間に安全にコピーする */
extern int copy_to_user(void *dest, void *src, size_t size);
void do_stuff(void *usr_buf) {
struct test arg = {.a = 1, .b = 2, .c = 3};
copy_to_user(usr_buf, &arg, sizeof(arg));
}
違反コード (memset()
)
memset()
関数を使えば、パディング部分を明示的に初期化できる。
#include <string.h>
struct test {
int a;
char b;
int c;
};
/* データをユーザ空間に安全にコピーする */
extern int copy_to_user(void *dest, void *src, size_t size);
void do_stuff(void *usr_buf) {
struct test arg;
/* パディング部分含め全体を 0 初期化 */
memset(&arg, 0, sizeof(arg));
arg.a = 1;
arg.b = 2;
arg.c = 3;
copy_to_user(usr_buf, &arg, sizeof(arg));
}
しかし、コンパイラによっては、arg.b = 2
という代入文に対して、32ビットレジスタの下位1バイトに値2
を設定し、上位3バイトは何もいじらないまま、レジスタの値(32ビット)をメモリに書き出す、という操作を行うコードが生成されるかもしれない。言語規格には適合しているコードであるが、レジスタの上位3バイトに残されている情報がユーザに漏えいすることになる。
適合コード
この適合コードでは、構造体のデータを配列に詰め込んでから信頼境界の外にコピーしている。
#include <stddef.h>
#include <string.h>
struct test {
int a;
char b;
int c;
};
/* データをユーザ空間に安全にコピーする */
extern int copy_to_user(void *dest, void *src, size_t size);
void do_stuff(void *usr_buf) {
struct test arg = {.a = 1, .b = 2, .c = 3};
/* May be larger than strictly needed */
unsigned char buf[sizeof(arg)];
size_t offset = 0;
memcpy(buf + offset, &arg.a, sizeof(arg.a));
offset += sizeof(arg.a);
memcpy(buf + offset, &arg.b, sizeof(arg.b));
offset += sizeof(arg.b);
memcpy(buf + offset, &arg.c, sizeof(arg.c));
offset += sizeof(arg.c);
/* 残りは 0 初期化する */
memset(buff + offset, 0, sizeof(arg) - offset);
copy_to_user(usr_buf, buf, offset /* コピーするデータのサイズ */);
}
このコードでは、パディングデータが初期化されないままコピーされてしまうことはない。
重要: ユーザ空間にコピーされる構造体データはchar
配列に詰め込まれた状態になっているので、実際には copy_to_user()
関数はユーザメモリ空間上にパディングデータを含むデータ列として元の構造体データを復元する必要があるだろう。
適合コード (パディングバイト)
パディングバイトを構造体メンバとして明示的に宣言する方法もある。ただし、この方法はコンパイラ実装と実行環境のメモリアーキテクチャに依存するため、可搬性を犠牲にすることになる。以下は x86-32 アーキテクチャ向けのコードである。
#include <assert.h>
#include <stddef.h>
struct test {
int a;
char b;
char padding_1, padding_2, padding_3;
int c;
};
/* データをユーザ空間に安全にコピーする */
extern int copy_to_user(void *dest, void *src, size_t size);
void do_stuff(void *usr_buf) {
/* c は最後のパディングバイトのすぐ次に置かれていること */
static_assert(offsetof(struct test, c) ==
offsetof(struct test, padding_3) + 1,
"Structure contains intermediate padding");
/* 構造体の末尾にパディングデータが存在しないこと */
static_assert(sizeof(struct test) ==
offsetof(struct test, c) + sizeof(int),
"Structure contains trailing padding");
struct test arg = {.a = 1, .b = 2, .c = 3};
arg.padding_1 = 0;
arg.padding_2 = 0;
arg.padding_3 = 0;
copy_to_user(usr_buf, &arg, sizeof(arg));
}
C 言語標準の static_assert()
マクロの第1引数は定数式、第2引数はエラーメッセージとして使われる文字列である。第1引数の式はコンパイル時に評価され、その結果が false である場合はコンパイルを中断し、エラーメッセージを表示する。(詳細は DCL03-C. 定数式の値をテストするには静的アサートを使う を参照。) 構造体 struct
のメンバとしてパディングバイトを明示的に追加することで、コンパイラによるパディングバイトの暗黙的な追加は行われず、2つの静的アサートで確認している条件式が成立する。条件式が成立するかどうかは、コンパイラ実装や実行環境ごとに異なることに注意。
適合コード (構造体データの詰め込み—GCC)
GCC では、構造体の宣言に __attribute__((__packed__))
属性を付加することにより、個々のメンバに明示的にアラインメント指定をつけないかぎり、パディングバイトが追加されなくなる。
#include <stddef.h>
struct test {
int a;
char b;
int c;
} __attribute__((__packed__));
/* データをユーザ空間に安全にコピーする */
extern int copy_to_user(void *dest, void *src, size_t size);
void do_stuff(void *usr_buf) {
struct test arg = {.a = 1, .b = 2, .c = 3};
copy_to_user(usr_buf, &arg, sizeof(arg));
}
適合コード (構造体データの詰め込み—Microsoft Visual Studio)
Microsoft Visual Studio では、コンパイラが内部状態として持っている詰め込みに関する設定を #pragma pack()
によって変更することで、パディングバイト追加に関する挙動を制御できる[MSDN]。また、__declspec(align())
で明示的にアラインメントを指定できる。以下の適合コードでは、パディングバイトをなくすために pragma pack()
で 1 を指定している。
#include <stddef.h>
#pragma pack(push, 1) /* 1 バイト */
struct test {
int a;
char b;
int c;
};
#pragma pack(pop)
/* データをユーザ空間に安全にコピーする */
extern int copy_to_user(void *dest, void *src, size_t size);
void do_stuff(void *usr_buf) {
struct test arg = {1, 2, 3};
copy_to_user(usr_buf, &arg, sizeof(arg));
}
pack
の指定は、その後に置かれた struct
宣言のメモリ配置に影響する。
違反コード
この違反コード例もカーネル空間で実行されるという想定で、構造体 struct test
のデータをユーザ空間にコピーする。ビットフィールドメンバの合計ビット数は unsigned
オブジェクトのビット数に足りないため、構造体データのなかにはパディングビットが含まれる。構造体のメンバとしてフィールド名なしのビットフィールドが宣言されているため、この後に宣言されているビットフィールドメンバは、最初のビットフィールドメンバとは別のストレージ単位に配置される。構造体のなかのパディングビットにはセンシティブな情報が含まれる可能性があり、ユーザ空間へのコピーによって漏えいする危険がある。例えば、カーネル空間で使われているポインタの値が、初期化されないままのパディングビットに残されていたら、ユーザ空間にコピーされたデータからポインタの値を再構築されるかもしれない。
#include <stddef.h>
struct test {
unsigned a : 1;
unsigned : 0;
unsigned b : 4;
};
/* データをユーザ空間に安全にコピーする */
extern int copy_to_user(void *dest, void *src, size_t size);
void do_stuff(void *usr_buf) {
struct test arg = { .a = 1, .b = 10 };
copy_to_user(usr_buf, &arg, sizeof(arg));
}
適合コード
以下の適合コードでは、フィールド名なし長さ 0 のビットフィールドを削除し、パディングビットをその値まで含めて明示的に宣言するという手法をとっている。
#include <assert.h>
#include <limits.h>
#include <stddef.h>
struct test {
unsigned a : 1;
unsigned padding1 : sizeof(unsigned) * CHAR_BIT - 1;
unsigned b : 4;
unsigned padding2 : sizeof(unsigned) * CHAR_BIT - 4;
};
/* 適切な長さのパディングビットを追加していることを確認 */
static_assert(sizeof(struct test) == sizeof(unsigned) * 2,
"Incorrect number of padding bits for type: unsigned");
/* データをユーザ空間に安全にコピーする */
extern int copy_to_user(void *dest, void *src, size_t size);
void do_stuff(void *usr_buf) {
struct test arg = { .a = 1, .padding1 = 0, .b = 10, .padding2 = 0 };
copy_to_user(usr_buf, &arg, sizeof(arg));
}
この適合コードはコンパイラ実装と実行環境のメモリアーキテクチャに依存しているため、可搬性がないことに注意。また、構造体にパディングビットを明示的に宣言するとともに、この適合コードの static_assert()
で行っているように、余計なパディングビットが追加されていないかどうかを検証するコードも一緒に入れておくことが重要である。例えば 64 ビットアーキテクチャである DEC Alpha では、整数型は32ビット、1 ストレージ単位は64ビットとなっている。
さらに、この適合コードでは、unsigned int
型オブジェクトのメモリ表現にパディングビットが含まれていないことを前提にしている。また、「INT35-C. 整数型の精度を正しく求める」で紹介している、ビット数を求めるコードは使えないことに注意。ビットフィールドのビット幅は整数定数式で指定しなければならないからだ。
ビットフィールドとパディングビットの問題については、どのような手法を使っても 100% 可搬性があるとはいえないため、注意深く検討する必要がある。
リスク評価
C 言語規格ではパディングデータの値は未規定であり、センシティブな情報が含まれる可能性がある。パディングデータを含む構造体オブジェクトへのポインタを他の関数に渡すことから、情報漏えいにつながる可能性がある。
ルール |
深刻度 |
可能性 |
修正コスト |
優先度 |
レベル |
---|---|---|---|---|---|
DCL39-C |
低 |
低 |
高 |
P1 |
L3 |
自動検出(最新の情報はこちら)
ツール |
バージョン |
チェッカー |
説明 |
---|---|---|---|
Axivion Bauhaus Suite |
6.9.0 |
CertC-DCL39 | パディングを含む構造体を検出する。とくに信頼境界を越えて渡されるもの。 |
Klocwork |
2018
|
PORTING.STORAGE.STRUCT PORTING.STRUCT.BOOL |
|
Parasoft C/C++test |
2020.2 |
CERT_C-DCL39-a |
ユーザ空間にデータをコピーする関数に、構造体へのポインタを渡さない |
R2020a |
CERT C: Rule DCL39-C | Checks for information leak via structure padding (rule partially covered) | |
PRQA QA-C |
9.7 |
4941, 4942, 4943 |
|
PRQA QA-C++ |
4.4
|
4941, 4942, 4943 |
|
関連する脆弱性
Linux カーネルの多くの脆弱性はこのルールへの違反である。例えば CVE-2010-4083 は、システムコール semctl()
の脆弱性で、権限を持っていない一般ユーザがカーネルスタック上の値を取得できるというものである。スタックに置かれた semid_ds struct
構造体の多くのメンバフィールドがクリアされないままコピーされていた。
QEMU-KVM
の脆弱性 CVE-2010-3881 では、データ構造中のパディングデータや予約済みフィールドがクリアされないままユーザ空間にコピーされていた。ホスト OS 上の /dev/kvm
にアクセスする権限を持っているユーザは、この脆弱性を使ってカーネルスタック上の情報を取得することができた。
act_police
の脆弱性 CVE-2010-3477 では、トラフィック制御のためのコードで使われている構造体が適切にクリアされないため、ユーザ空間で動作するアプリケーションがカーネルメモリの情報を取得できた。
関連するガイドライン
Taxonomy |
Taxonomy item |
Relationship |
---|---|---|
CERT C コーディングスタンダード | DCL03-C. 定数式の値をテストするには静的アサートを使う | Prior to 2018-01-12: CERT: Unspecified Relationship |
参考資料
[ISO/IEC 9899:2011] | 6.2.6.1, "General" 6.7.2.1, "Structure and Union Specifiers" |
[Graff 2003] |
|
[Sun 1993] |
|
翻訳元
これは以下のページを翻訳したものです。
DCL39-C. Avoid information leakage when passing a structure across a trust boundary (revision 100)