見出し画像

第3章 プログラムの制御はいかにして乗っ取られるか

ソフトウェアの仕組み

ソフトウェアとは、「コンピュータ用語でハードウェアと対比されて使われるものであり、ハードウェアに指令を出したり何らかの処理や動作を行うもの」です。ソフトウェアには様々な種類があり基本的なものを挙げると以下のようになります。
・アプリケーションソフトウェア
    特定のタスクを実行することを目的としたソフトウェアのこと。Wordや
    Excelなど
・OS(オペレーティングシステム)
    基本ソフトウェアとも呼ばれPCなどの基本的な操作(ファイル操作、メ
    モリ管理、ユーザーインターフェイスなど)を制御する。Mac OSなど
・ミドルウェア
    OSとアプリケーションの中間に位置し、OSではできない複雑な処理(サ
    ーバやデータベースとのやりとりなど)を行う。
アプリケーションやOSなど身近なものも多いと思います。ソフトウェアにはハードウェアと違って大きな特徴があります。それは、複製コストが0であることです。ソフトウェアは簡単にコピーアンドペーストで複製することができます。
このような現在の生活に欠かせないソフトウェアは一般的には「ソースコード」と呼ばれるコンピュータに指示出しをする手順書によって作成されています。ソースコードを書くための言語をプログラミング言語と言います。このプログラミング言語のおかげで私たちはコンピュータに指示出しをしたり新たなソフトウェアを開発したりできます。しかし、コンピュータは人間が書いたプログラミングコードをそのまま理解することはできません。ソースコードはあくまで人間が理解するために作られたもので、それをコンピュータに理解させるためにはソースコードを機械語で書かれた実行ファイル形式に変換しなければいけません。この実行ファイル形式に変換する作業はコンパイラと呼ばれるツールで行われます。

プログラミングの概要

プログラミングとは先ほど述べたように、コンピュータ用の手順書を作成するための言語です。たくさんの種類の言語があり、一定数のユーザーがいる言語でも250種類程度、ユーザー数が少ないものも含めると1000種類以上になると言われています。この項ではC言語をもとに必要最低限のプログラミングの知識を説明します。

example.c
_______________________________________________
1: #include<stdio.h>
2: int main(int arc, char*argv[])
3: {
4:   puts("HelloWorld!");
5:   puts(argue[1])
6:   return 0;
7: }
_______________________________________________
本書より(1: などは行番号)

上部に書かれているexample.cというのがこのソースコード(手順書)のファイル名で、.cというのはこの手順書がC言語によって書かれていることを表しています。このソースコードは「HelloWorld!」とユーザーが任意に入力した文字列が出力される指示が書かれています。3〜4行目のputs()が文字列を表示する機能を持っています。これを実行するとこのような結果になります。

(base) user@MacBook-Air C_programing % gcc -o example example.c
(base) user@MacBook-Air C_programing % ./example AAAA          
HelloWorld
AAAA

一行目のgcc …で先ほど説明したコンパイルを行なっています。次の行でexample.cを実行すると、手順書どうりに「HelloWorld」と私が入力した「AAAA」という文字列が表示されています。ちなみに、このようにキーボードで入力した指令から出力を得るという方式をCUI(Character User Interface)と呼びます。クリックやマウスの動かし方で操作する(出力を得る)方式をGUI(Graphical User Interface)と呼び、こちらの方が馴染み深いかもしれません。次に、このプログラムに関して詳しくみていきます。

関数の概念

プログラムにおいて関数とは処理の塊のことを言います。例えば予定を管理するアプリケーションだったら、予定を入力する処理A、消去する処理B、編集する処理C、保存する処理D、、、など多岐にわたる処理の塊が集まって一つのアプリを構成しているわけです。このような関数を実行することを「関数を呼び出す」と表現します。
関数において重要な言葉として、「引数」と「戻り値」というものがあります。引数とは関数が受け取るデータで、戻り値とは関数が返却するデータです。これは数学の関数とほぼ同じだと思います。y=2xという関数にx=2を代入したらy=4が出てくる、そんなイメージです。
C言語で関数は以下のように定義(作る)ことができます。

戻り値の型 関数名(引数の型 引数名)
{
 (処理内容)
 return 戻り値
}

本文より

まず、一行目で関数の名前や引数、戻り値の型を設定します。型とはデータの形式のことで、整数型や文字列型などがあります。コンピュータは柔軟ではないのでこのデータは整数ですよと教えてあげないと何のデータか理解できないのです。{}内がこの関数で行われる処理で、その中に関数で行う処理内容や戻り値を記述します。また、example.cで使われていたputs関数画面に文字を出力する関数なのですが、これらは使用頻度が高いためC言語の標準ライブラリで予め用意されています。いちいち関数の内容を記述せずに使うことができるということです。ただし、puts関数を使用するためには#include<stdio.h>とソースコード上部に記述する必要があります。

main関数

関数の中には特殊な関数があり、main関数もその一つです。main関数とは、プログラムの中に必ず一つだけ存在していなくてはならないと決められている関数です。main関数は処理の開始点を教えていくれる関数で、ソースコードを実行する時にコンピュータはmain関数から処理を実行します。main関数も引数と戻り値を持っていますが、main関数の引数を特別にコマンドライン引数と呼びます。コマンドラインで入力されたデータがmain関数の引数になるからです。入力されたデータはデータを保存する領域に保存されます。example.cを例に取り説明します。

example_ver2.c
________________________________________________
#include <stdio.h>
int main(int argc, char*argv[])
{
    puts("HelloWorld");
    printf("argc = %d\n", argc);
    puts(argv[0]);
    puts(argv[1]);
    puts(argv[2]);
    return 0;
}

example.cを上記のように変えてみました。実行すると、

(base) user@MacBook-Air C_programing % ./example AAA BBB CCC   
HelloWorld
argc = 4
./example
AAA
BBB

このようになります。main関数の引数はargcとargvでargcは整数型、argvは文字列型の引数を受け付けます。

コマンドライン引数が保存される様子

コマンドライン引数は上のように、入力された文字列がスペース区切りでargv[]に保存され、argcにはコマンドライン引数の数が保存されます。example_ver2を実行した結果を見ると、それぞれ保存されたデータが表示されているのがわかります。このように、main関数はコマンドラインでの入力を引数に持ちます。

変数

今まで関数の概略について説明してきましたが、プログラミングをする上でもう一つ大事な概念として変数というものがあります。変数とはデータを保存する箱のようなものです。箱それぞれには名前がついており、その名前を変数名と呼びます。こちらのソースコードを見てみましょう。

variable.c
______________________________________________________
1: #include <stdio.h>
2: 
3: int main(int argc, char*argv[])
4: {
5:     int year = 2016;
6:     char c = 'A';
7:     char buffer[5] = "ABCD";
8:     
9:     printf("year is %d\n", year);
10:    puts(buffer);
11:    return 0;
12: }
________________________________
本文より

ソースコード中のyearやc、そしてbufferが変数で、intやcharは変数の型を表しています。五行目の一文は、yearという整数型の変数に2016という数字を代入しますよという意味です。6行目はcという文字型の変数にAという文字を代入しています。また、七行目はbufferという配列にABCDという文字列を代入しています。buffer[5]と定義すると4つの文字まで保存することが可能で、4つの文字と改行を示す\nが配列に保存されます。このソースコードを実行するとこのようになります。

(base) user@MacBook-Air C_programing % ./variable                
year is 2016
ABCD

yearには2016が、bufferにはABCDが保存されているのでその二つを出力すると保存されていたデータが出力されます。

バッファオーバーフローの脆弱性とは

概要

バッファオーバーフローの脆弱性はソフトウェアの脆弱性の中で一番典型的なものです。この脆弱性はコンピュータ上で用意されたメモリ領域に対してそれ以上の大きさのデータが書き込めてしま得ことに起因します。この脆弱性を利用するとサービスの停止やサイトの改竄ができてしまいます。2000年1月には官公庁のサイトがバッファオバーフローの脆弱性を利用した攻撃により改ざんされてしまったという事例が発生しています。ここではメモリ領域の一種であるスタック領域に許容量以のデータが入力されることで起きるスタックバッファオーバーフローについて説明していきます。

スタック領域とスタックバッファオーバーフロー

スタック領域とは各関数の中だけで利用されるデータを保存する領域で、関数を呼び出すときに欠かせないものです。スタック領域でのデータの扱われ方にはあるルールが存在します。

スタック領域のデータの扱い方とアドレス

スタック領域は後入れ先出しの法則でデータの保存と取り出しを行なっています。後に入れたデータから順に取り出していくということです。データには賞味期限はないので大丈夫ですね。また、データが保存されるごとにそのデータに対してアドレスが付与されるという性質もあります。このような特徴を持つスタック領域ですが、関数を呼び出す時の役割としては以下の三つがあります。

1: 因数の受け渡し

スタック領域には引き渡された引数が保存されます。引数が複数ある場合保存される順番は、上図のように右側の引数からスタック領域に保存されます。

2: 関数の戻りアドレスの保存

ソースコードによってはある関数(function1)から他の関数(function2)に処理を移し再びfunction1に戻るということもあります。そのときのためにfunction1のアドレスを保存しておく必要があり、そのアドレスはスタック領域に保存されます(上図)。道を歩いているときにある地点の住所を保存しておき、その住所を見ながら元の地点に戻るといったイメージです。

3: フレームポインタの保存

フレームポインタの説明の前にスタックフレームの説明をします。スタック領域は関数ごとに用意され、関数ごとのスタック領域をスタックフレームと呼びます。複数人で洗濯物を行うときに人数分の洗濯籠を用意して服がごっちゃにならないようにするのと同じ感じです。ある関数での処理が終わり、別の関数を呼び出すとき使用するスタックフレームも別の関数用のものでなくてはいけません。つまり別の関数を呼び出すとき、その関数用のスタックフレームも一緒に呼び出しているのです。スタックフレーム呼び出し時に使われるアドレスのようなものをフレームポインタと言い、これも戻りアドレスを保存するのと同じように保存されます。

スタックバッファオーバーフロー時の挙動

先ほども述べたように、スタックバッファオーバーフローとはスタック領域ないに許容量を超えるデータが保存されてしまったときに起こるものです。その挙動を実際のソースコードを用いて説明していきます。

ソースコードは本書を改良したもの

main関数とmy_func関数で構成されているソースコードです。まず、コマンドラインから入力された引数をmain関数が受け取ります①。次に、my_func関数が呼び出され(13行目)引数が変数であるbufferに保存されます②。そして、bufferが出力されたのちmain関数に戻ります③。main関数に戻ると"complete"という文字が出力され処理が完了します。このソースコードを実行するとこのような結果になります。

(base) user@MacBook-Air C_programing % ./bof AAA       
AAA
complete

このときスタック領域はこのようになっています。

処理内容とスタック領域の概要

引数が渡されたとき、引数AAAがmain関数のスタック領域に保存されます。次に、my_func関数を呼び出すときにmain関数の戻りアドレスが保存されます。その後、main関数のフレームポインタも保存されます。my_funcの処理によりbuffer領域が作成され引数AAAが保存されます。
このbuffer領域に許容量以上のデータだ保存されたらどうなるでしょうか?

(base) user@MacBook-Air C_programing % ./bof AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

my_func_was_called
buffer_was_set
zsh: trace trap  ./bof 

試しに引数として大量のAを入力してみました。すると、trace trapというエラーが出ました。ちなみに、わかりやすいようにソースコードを編集しています。

#include <stdio.h>
#include <string.h>

void my_func(char*argv)
{
    printf("my_func_was_called\n"); ⏪追加箇所
    char buffer[100];
    printf("buffer_was_set\n"); ⏪追加箇所
    strcpy(buffer, argv);
    printf("buffer_was_copied\n"); ⏪追加箇所
    puts(buffer);
}

int main(int argc, char*argv[])
{
    my_func(argv[1]);
    printf("complete\n");
    return 0;
}


my_func_was_calledとbuffer_was_copiedが表示されているので、my_funcの呼び出しと引数をbufferにコピーすることは完了しているということです。コピーが完了していないので、バッファオーバーフローが起きているかはわかりませんが、このときバッファオーバーフローが起きたと仮定して話を進めていきます。(おそらく引数が用意したbufferの容量を超えているためコピーの前に処理が終わったのだと思います。)このとき、buffer領域に指定した容量を超えるデータが保存されたので、スタック領域はこのようになっています。

引数に大量の文字が入力されたので、bufferに引数を保存するときフレームポインタや戻りアドレスが大量のデータにより上書きされてしまっています。そのため、my_func関数からmain関数に戻ることができずプログラムが不正終了してしまうのです。不正終了するだけならいいのですが、入力の仕方によっては戻りアドレスを都合のいいように書き換えることができます。この性質を利用すれば、攻撃者はプログラムを不正終了させるだけでなく任意のコードを実行できてしまいます。これに関しては次の項目で説明します。

シェルコードを用いたバッファオーバーフローの悪用

任意のコードを実行するために攻撃者はシェルコードと呼ばれるものを利用します。シェルコードとは、コンピュータ上で任意の動作を行わせるための命令の断片です。シェルとはOSを構成するソフトウェアの一つでプログラムの実行状態を管理やストレージを操作するためのものです。このシェルを乗っとて実行するコードが多いため、シェルコードと呼ばれています。シェルコードは意見意味不明な呪文のような文字列で、Pythonなどを用いて作成できます。先ほどのプログラムがシェルコードを利用した手法によって攻撃されると下図のようになります。

シェルコード(攻撃者により書かれた任意の指令)とシェルコードの戻りアドレスが引数に渡されるとスタック領域が書き換えられてしまいます。攻撃者は、スタック領域の戻りアドレスの部分がシェルコードの戻りアドレスになるように穴埋めを使い調整しているので、my_func関数からmain関数に戻るときに参照するアドレスがシェルコードのアドレスになってしまします。つまり、攻撃者が用意したシェルコードが実行されてしまうのです。

これがこの章で議論されているバッファオーバーフローの概要です。冒頭で述べた事例以外でも色々なところでこの脆弱性が原因となった被害が出ているようです。


この記事が気に入ったらサポートをしてみませんか?