セキュリティ・キャンプ2016に応募した

応募した

セキュリティキャンプに応募しました.
年齢制限があるので今年で最後です.
悔いのないように頑張って原稿を書き上げました.
さらに悔いの残らないようにブログに掲載しておきます.
合否結果は6月中旬までお待ちください.
※2016年6月14日追記 残念ながら参加見送りとなりました
※2016年6月16日追記 欠員補充により参加できることになりました

共通問題1

この設問は省略します.

あなたが今まで作ってきたものにはどのようなものがありますか? いくつでもいいので、ありったけ自慢してください。

reminder,思い継ぎ語り,タニタンv1.0・v1.1,迷路自動取得ソフト,マイクロマウスタイマー,タニタンv2.0H,STM32 writer,デレステ自動演奏機,について超簡単にまとめました.

それをどのように作りましたか?ソフトウェアの場合にはどんな言語で作ったのか、どんなライブラリを使ったのかなども教えてください。

それぞれ数文でまとめました.

開発記のブログなどあれば、それも教えてください。コンテストなどに出品したことがあれば、それも教えてください。

省略します.

共通問題2

あなたが経験した中で印象に残っている技術的な壁はなんでしょうか?(例えば、C言語プログラムを複数ファイルに分割する方法)

近年で一番印象的だった壁はマングリングです.
組み込みで,初めてC++での開発をしようとしたときの話です.割り込みに関する部分のプログラムを追加したところ,定義しているはずの関数が見つからないというリンカエラーが発生しました.プログラムの見直しや構造の見直しなどいろいろと試してみたのですが,2週間ほど何もわからずに苦しんでいました.開発はC++ですが,自動生成されるファイルは一部がC言語やアセンブリで書かれていたため,これらのファイルの間でマズいことが起きているのではないかと考えましたが,具体的に何が悪いのかわかりませんでした.また,所属していたサークルにC++で開発をしている人がおらず,身近に解決手段を聞ける相手がいませんでした.

また、その壁を乗り越えるために取った解決法を具体的に教えてください。(例えば、知人に勧められた「○○」という書籍を読んだ)

マングリングに悩まされていた問題ですが,たまたま大学に来られていたサークルOBの人にプログラムを見てもらい,マングリングではないかとアドバイスを受けました.ひとまず対処法を検索し,C++はコンパイルの際にシンボル名が変わることが原因であるため,シンボル名が変わらないように対策すれば良いことがわかりました.
原因はわかりましたが,実際にどのようなことが起こっているか確認したかったので,簡単なプログラムを作成して実験してみました.作成したのは次のコードです.

1
2
3
4
5
6
int test_func(){
return 0;
}
int main(){
return test_func();
}

これをmainc.c,maincpp.cppの2つのファイルに分けて保存し,gccとrx-elf-gcc(Renesas RXシリーズマイコン用のコンパイラ)の2種類でコンパイルし,その中身を解析してみました.実行したコマンドは次の通りです.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$ gcc mainc.c -o gccmainc.o
$ g++ maincpp.cpp -o gccmaincpp.o
$ rx-elf-gcc mainc.c -o rxmainc.o
$ rx-elf-g++ maincpp.cpp -o rxmaincpp.o

$ objdump -x gccmainc.o
(中略)
SYMBOL TABLE:
(中略)
0000000000000000 g F .text 000000000000000b test_func
(後略)

$ objdump -x gccmaincpp.o
(中略)
SYMBOL TABLE:
(中略)
0000000000000000 g F .text 000000000000000b _Z9test_funcv
(後略)

$ rx-elf-objdump -x rxmainc.o
(中略)
SYMBOL TABLE:
(中略)
00000000 g F P 0000000f _test_func
(後略)

$ rx-elf-objdump -x rxmaincpp.o
(中略)
SYMBOL TABLE:
(中略)
00000000 g F P 0000000f __Z9test_funcv
(後略)

これらの実行結果から,C++で実装された関数のシンボルがマングルされていることを確認しました.また,コンパイラによって関数の頭に”_”がついたりつかなかったりするということも確認しました.
ここで,先ほど調べた解決法を試してみました.次のコードをmaindemangle.cppとして保存し,コンパイルした後に解析してみました.

1
2
3
4
5
6
7
8
9
extern "C" {
int test_func(){
return 0;
}
}

int main(){
return test_func();
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ g++ maindemangle.cpp -o gccmaindemangle.o
$ rx-elf-g++ maindemangle.cpp -o gccmaindemangle.o

$ objdump -x maindemangle.o
(中略)
SYMBOL TABLE:
(中略)
00000000004004a6 g F .text 000000000000000b test_func
(後略)

$ rx-elf-objdump -x maindemangle.o
(中略)
SYMBOL TABLE:
(中略)
fff40168 g F .text 0000000f _test_func
(後略)

無事にシンボルをCと同じ名前にすることができました.
これらを踏まえてプログラムを修正したところ,無事にリンカエラーが解消しました.

その壁を今経験しているであろう初心者にアドバイスをするとしたら、あなたはどんなアドバイスをしますか?

マングリングによるエラーに悩んでいる人がいるとしたら,まずは自分のブログなどを紹介し,解決法を教えます.解決した後に,objdumpという便利なツールがあるということを教え,自分のプログラムを解析させます.
いきなり解決法を提示するというのには理由があります.私は今ソフトウェアだけでなく「ものづくり」をしていますが,やはり物が動かなくてはモチベーションも続きません.そのため,はじめはできるだけ解決を優先し,モチベーションを高めてあげることが重要だと考えています.
しかし,解決法の提示だけではあくまで一つの対処法を学んだだけに過ぎません.今後のことを考えると,解決へ至るプロセスや,調査方法などにも触れさせるべきだと思います.次に似たような問題に直面したときに,自力で解決できるようにすべきだからです.
今回の例では,objdumpでシンボルを確認することで,マングリングを目で見ることができます.それだけでなく,シンボル以外にも様々な情報を得ることができることに気づくことができます.何気なく動かしていた自分のプログラムが,実は様々なライブラリやソフトウェアなどによって支えられていて初めて動いていることを知ることは,コンピュータに強い興味を抱かせる理由の一つとなりました.初心者には,プログラムの内部を見てみることで新しい興味を抱き,単にトラブルの解決法を知るだけでなく,より多くの知識を広げていってほしいと考えています.

共通問題3

あなたが今年のセキュリティ・キャンプで受講したいと思っている講義は何ですか?(複数可)そこで、どのようなことを学びたいですか?なぜそれを学びたいのですか?

すべての講義を受けたいのですが,そういうわけにもいかないので,2つだけピックアップして述べます.

  1. 6-C 車載LAN上を流れるメッセージの解析
    昨年,驚くべき脆弱性情報を目にしました.CVE-2015-5611,クライスラー社の車がインターネットを通じて遠隔操作され,ブレーキやハンドル操作まで乗っ取られるという脆弱性です.これまではインターネットとは程遠い存在だった車が,近距離ネットワークを構築したり,インターネットに接続したりするように開発が進められています.制御も複雑化しており,複数のECUと複数のセンサなどが通信しあって動作しています.このような技術の進歩は喜ぶべきことですが,同時に強い恐れを抱いています.なぜなら,人の命を預かる車が乗っ取られたときの被害が,あまりに甚大であるからです.自動車は,これまではセキュリティと無縁だと思っていましたが,これからのセキュリティ問題に大きく関わってくるキーワードであると思っています.そこで,車に関わるこの講義に興味を惹かれました.
    今回の講義はCANの通信解析ということですが,CANにもセキュリティ上の問題が指摘されています.盗聴はもちろん,なりすましや不正メッセージの送信なども行えるということを知りました.CANにはエンジンやブレーキなどを制御するECUも接続されているため,攻撃を受けると致命的な問題が発生します.私はこれまで,動作の確認という意味でSPIバス通信の解析をしたことはありますが,攻撃やその防御策を考える意図で解析をしたことはありません.バスに対する攻撃と防御を学ぶ第一歩としてこの講義を受講したいです.
    攻撃されたときの深刻性は,自動車はもちろん大きなものでありますが,他の機械でも大きな問題となる可能性があります.私はこの講義を受講して攻撃者や防御者という立場での解析法を学び,将来技術者となったときに万全のセキュリティ対策を施せるようになりたいです.

  2. 3-A Webアプリケーションの脆弱性の評価と発見
    この講義を受けたいと思った理由は2つあります.
    まず1つ目ですが,より幅広い分野の技術者がWebアプリケーションのセキュリティについて学ぶ必要があると考えているからです.近年流行しているIoTにより,あらゆるデバイスがインターネットに接続するようになってきました.このため,多くのサーバやWebアプリケーションを用意する必要があります.サーバやWebアプリケーションが増えるということは,それだけ攻撃の対象が増えることを意味します.したがって,ソフトウェアのみならず,ハードウェアを扱う技術者にとっても,Webアプリケーションに関するセキュリティ知識が必須であると考えます.
    次に2つ目の理由ですが,脆弱性やその対策法について,より実践的な経験を得たいと思っているからです.私は自宅にサーバを設置していくつかのサービスを運用しています.運営する上で,書籍を読みセキュリティ対策について勉強をしてきたつもりです.基本的な知識や,代表的な攻撃の手法,その対策法など,これまで知らなかったことを学ぶことができました.しかし,新たな脆弱性を発見するためには何をすべきかわかっていません.この講義を受講して,脆弱性の検出プロセスや,そもそも脆弱性が発生する根本的な原因を学びたいと考えています.

Webアプリケーションのセキュリティはどのような分野においても必要不可欠だと思います.私は将来どんな職種になるかわかりませんがここで得た知識は必ず役に立ちますし,仕事を通して社会利益に貢献したいです.また将来の仕事以外でも,趣味での開発やブログ等での情報発信は今後も引き続き行っていくつもりです.ここで得た知識を元に,よい技術者としての発信を続けていきたいと思っています.以上の理由から,私はこの講義を受講したいです.

あなたがセキュリティ・キャンプでやりたいことは何ですか?身につけたいものは何ですか?(複数可)自由に答えてください。

このセキュリティキャンプでやりたいことは,主に2つあります.

  1. セキュリティに対する意欲を,今後も伸ばしていきたいと思っています.そのため,セキュリティへの意欲が高い学生の方々や専門の講師の方々と知り合い,技術的な交流をしたいです.
    私は大学入学前からコンピュータネットワークに強い興味があり,入学時には応用情報技術者試験に合格しました.しかし,大学には情報・セキュリティに関する活動を行っているクラブ・サークルがなかったため,ロボット製作をメイン活動としているサークルに入部しました.このサークルに入部したことは自分にとってプラスとなりました.サークルOBには全国トップクラスの技術力を持っている方々がいらっしゃり,現在も著名企業でご活躍されています.こういった方々と知り合い,頻繁にお話を聞くことができるようになったため,ハードウェアについての知識を多く得ることができました.これまではソフトウェア一本でしたが,ハードウェアや組み込み系に強い人々と交流することがいい刺激となり,ハードについても強い興味が出てきました.
    一方で,私はネットワークやセキュリティについて話し合える知り合いがほしいと常々思っています.近年ではIoTといったワードをよく聞きますが,ハードウェアとネットワークはもはや切り離せない関係にあり,同時にセキュリティは必要不可欠です.私は,今はハードをメインに活動していますが,元々はネットワークに興味があり勉強をしてきました.いずれの領域においてもセキュリティは共通して重要な要素です.したがって,ハード面のみならずソフト面でも強い意欲を伸ばしていくために,このキャンプに参加し,ソフトウェアやセキュリティに強い関心がある人たちと知り合い,互いに刺激し合えるような関係を作りたいと思っています.

  2. セキュリティについて,適切な指導を受けて力を伸ばしたいです.
    私はこれまでずっと,独学で勉強をしてきました.残念ながらソフトウェアやセキュリティに強い知り合いがおらず,本やインターネットで調べて得た知識を吸収してきただけです.自力で勉強することは大切なことですが,これだけではだめだとも思っています.独学では,手を動かして課題に取り組んだ際にフィードバックを得ることができません.また,最新のセキュリティについても,書籍ではなかなか得ることができません.セキュリティの分野で活躍されているプロの方々からの指導を受けることで,このような知見を得たいと考えています.同時に,セキュリティ問題に取り組む姿勢や考え方なども吸収したいです.

選択問題1

ローカル変数はスタック領域に,mallocを使った動的なメモリ確保はヒープ領域にメモリが確保されることは予め知っていましたが,今回のプログラムにはなんの違和感も感じませんでした.そこで,まずは与えられたプログラムを手元の環境で実行してみました.
ただし今回の実行環境は次の通りであり,以降の問題でも特筆していなければこの環境で動作させています.

1
2
3
4
$ uname -a
Linux arch_thinkpad 4.5.1-1-ARCH #1 SMP PREEMPT Thu Apr 14 19:19:32 CEST 2016 x86_64 GNU/Linux
$ gcc --version
gcc (GCC) 5.3.0

実行結果は次の通りです.

1
2
3
$ ./1-1
hoge address = 0x7ffe049bf300
fuga address = 0x1038010

fuga addressが明らかに違うことが判明しました.そこで,mallocの動作を調べることにしました.
manでmallocについての情報を見ると,mallocは内部でsbrkを用いてヒープからメモリを割り当てていることがわかりました.また同時に,MMAP_THRESHOLDバイトより大きな領域を確保する場合,glibcのmallocは代わりにmmapを用いることがわかりました.sbrkとmmapでどの領域からメモリを確保しているのか,実際に試してみました.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(){
int fp = open("/dev/zero", O_RDONLY);
void* p1;
void* p2;
if((p1 = mmap(NULL, 1, PROT_WRITE, MAP_PRIVATE, fp, 0)) == MAP_FAILED) printf("ERROR\n");
p2 = sbrk(1);
printf("mmap: %p\n", p1);
printf("sbrk: %p\n", p2);
return 0;
}

このプログラムは,p1にmmapでメモリを割り当て,p2にsbrkでメモリを割り当て,それぞれのアドレスを表示するプログラムです.
実行結果は次の通りです.

1
2
3
$ ./1-3
mmap: 0x7f39b4c2c000
sbrk: 0x157f000

この結果から,sbrkとmmapでは確保されるメモリ領域が異なることが判明しました.手元で課題のコードを動作させたときのfuga addressが,このプログラムのsbrkで確保したアドレスに似通っているため,実は内部でsbrkが呼ばれていたのではないかと考えました.
mallocのmanページをもう少し読むと,malloptを使ってMMAP_THRESHOLDが変更可能であると書かれていました.そこで,課題のコードを少し書き換え,MMAP_THRESHOLDを0,つまり常にmmapを使うようにコードを変更してみました.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
int main(int argc, char **argv){
char c;
int hoge[10];
int *fuga;
mallopt( M_MMAP_THRESHOLD, 0 );
fuga = malloc(1);
printf("hoge address = %p\n", hoge);
printf("fuga address = %p\n", fuga);
free(fuga);
return 0;
}

このコードの実行結果は次の通りです.

1
2
3
$ ./1-4
hoge address = 0x7ffd8c819780
fuga address = 0x7fde0b040010

課題文中の実行結果と似た出力となりました.また,今回コード中で指定したMMAP_THRESHOLDを,環境変数で指定できることがわかったので,実行してみました.出力結果は省略しますが,次のコマンドを順に実行すると,先ほどと同様の出力が得られました.なおプログラム実行後に環境変数は通常の値に戻しておきました.

1
2
3
$ export MALLOC_MMAP_THRESHOLD_=0
$ ./1-1
$ export MALLOC_MMAP_THRESHOLD_=131072

これらの結果から,課題文中の実行結果は,何らかの理由で環境変数MALLOC_MMAPTHRESHOLDが0になっている環境で実行されたのではないかと考えます.この環境では,割当を解除したメモリ空間を将来的な割当に再利用できなくなるため,不効率である可能性があります.

選択問題4

次のコードをC言語で作成しました.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

struct packet{
char Magic [2];
char Source[20];
char Destination[20];
uint32_t DataLength;
char* Data;
} __attribute__((packed));

int strcmp_upper(const char* s1, const char* s2){
int i=0;
while(toupper(s1[i]) == toupper(s2[i])){
if(s1[i++] == '\0') return 0;
}
return 1;
}

int chk1(struct packet* p){
if(strncmp(p->Magic, "RH", 2) == 0) return 1;
else return 0;
}

int chk2(struct packet* p){
if(!strcmp_upper(p->Source, "rise-san")) return 1;
if(!strcmp_upper(p->Source, "cocoa-san")) return 1;
return 0;
}

int chk3(struct packet* p){
if(!strcmp_upper(p->Destination, "Chino-chan")) return 1;
if(!strcmp_upper(p->Destination, "Chino")) return 1;
return 0;
}

int chk4(struct packet* p){
if((!strcmp(p->Source, "cocoa-san"))&&(!strcmp(p->Destination, "Chino"))) return 0;
return 1;
}

int chk5(struct packet* p){
if(strstr(p->Data, "BlueMountain") != NULL) return 1;
if(strstr(p->Data, "Columbia") != NULL) return 1;
if(strstr(p->Data, "OriginalBlend") != NULL) return 1;
return 0;
}

int chk6(struct packet* p){
if(strstr(p->Data, "DandySoda") != NULL) return 0;
if(strstr(p->Data, "FrozenEvergreen") != NULL) return 0;
return 1;
}

uint32_t convEndian(uint32_t t){
uint32_t ret = 0;
ret = (t<<24)&0xFF000000;
ret |= (t<<8)&0x00FF0000;
ret |= (t>>8)&0x0000FF00;
ret |= (t>>24)&0x000000FF;
return ret;
}

int readPacket(FILE* fp, struct packet* p){
if(fread(p, 1, 46, fp) != 46) return 0;
p->DataLength = convEndian(p->DataLength);
if(p->Data != NULL){
free(p->Data);
p->Data = NULL;
}
p->Data = malloc((size_t)(p->DataLength)+1);
memset(p->Data, 0, p->DataLength+1);
if(fread(p->Data, 1, p->DataLength, fp) != p->DataLength) return 0;
return 1;
}

int main(){
struct packet* p = malloc(sizeof(struct packet));
memset(p, 0, 50);
p->Data = NULL;
FILE* fp = fopen("pyonpyon.rh", "r");
while(readPacket(fp, p)){
if(chk1(p)&&chk2(p)&&chk3(p)&&chk4(p)&&chk5(p)&&chk6(p))
printf("PASS\n");
else
printf("REJECTED\n");
}
if(p->Data != NULL){
free(p->Data);
p->Data = NULL;
}
free(p);
fclose(fp);
return 0;
}

実行結果は次の通りとなりました.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
$ ./4-1c
PASS
PASS
PASS
PASS
PASS
PASS
PASS
PASS
PASS
REJECTED
PASS
PASS
PASS
REJECTED
REJECTED
PASS
PASS
PASS
REJECTED
REJECTED
REJECTED
PASS
PASS
PASS
REJECTED
REJECTED
REJECTED
REJECTED
PASS
PASS
PASS
REJECTED
REJECTED
REJECTED
REJECTED
REJECTED
PASS
PASS
PASS
REJECTED
REJECTED
REJECTED
REJECTED
REJECTED
REJECTED

念の為バイナリを確認してみましたが,正しく動作しているようでした.

続いて,このプログラムが使用したメモリ使用量と動作時間(CPUサイクル)を測定しました.まずメモリ使用量ですが,/usr/bin/timeを用いて計測し,次のような結果となりました.

1
2
3
$ /usr/bin/time -f "%M KB" ./4-1c
(./4-1c実行結果は省略)
1350 KB

また動作時間については,rdtscp命令を利用して,消費したCPUサイクル数を計測することにしました.
提出したコードに次のようなコードを追加し,動作時間(CPUサイクル数)を測定しました.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned long long rdtsc() {
unsigned long long ret;
__asm__ volatile ("rdtscp" : "=A" (ret));
return ret;
}

int main(){
struct packet* p;

unsigned long long st, et;
st = rdtsc();

p = malloc(sizeof(struct packet));
(中略)
fclose(fp);

et = rdtsc();
printf("%lld, %lld, %lld\n", st, et, et-st);

return 0;
}

このプログラムを実行すると,次の出力が得られました.

1
2
3
$ 4-1c-calc
(実行結果は省略)
3752867116, 3753241296, 374180

したがって,今回のプログラムのサイクル数が374180サイクルという実行結果を得られました.

選択問題5

はじめにJIS規格におけるOSの定義を示しておきます.
「プログラムの実行を制御するソフトウェアであって,資源割り振り,スケジューリング,入出力制御,データ管理などのサービスを提供するもの」
これを踏まえたうえで,組み込み開発での体験を元にした,自分なりの考えをまとめます.

私は,組み込みを始めるまではWindows向けのアプリケーションなどを作成していました.その時は,プログラムは慣れさえあれば簡単なものだと勘違いしていました.用意された関数を呼べば線を描画でき,画像も表示できます.関数を呼べばシリアルからデータを送信でき,時間待ちも簡単にできます.そんなに高度なことはしていませんでしたが,ハードについて全然知識を持ち合わせていなくても,このようなプログラムを書くことができていました.
大学に入り,組込み開発をするようになってから,この認識が大きく変わりました.普段の組込み開発ではOSは使用せず,いわゆるベアメタルでの開発をしています.プログラムは唯一のものしか無く,入出力制御は直接レジスタを操作し,データ管理のシステムは自分で整備します.私個人は,このようなプログラムを作ることは楽しいと感じています.直接レジスタを操作し,直接マイコンを操作しているように思うからです.同時に,これまでのプログラミングに関しての知識は,まったくの不十分であることも実感しました.例えば,C言語でLinux向けのアプリケーションを書く時のことを考えます.1秒待ちたい場合は,”sleep(1);”と書くだけで時間を待つことができます.一方,組み込みで1秒の時間待ちをしたい場合,自分で専用の関数を作る必要があります(もちろん標準で用意されている場合もありますが).このときには,マイコン周辺回路の一つであるタイマを利用する必要があります.タイマのレジスタを設定し,割り込み周期を設定し,種々の設定を行い,タイマをスタートさせる.これだけの長い処理を書くことで初めて時間待ちができるようになります.当然,設定するためには相応のハードウェアの知識が必要となります.
先ほどの例で,Linuxアプリケーションでは”sleep(1);”だけで良いと書きました.これはC標準ライブラリに用意されているから使えるわけですが,その根本にはOSが関わっています.ハードウェアを強く意識させること無く,シグナルという概念を使ってプログラムを作ることができるようになっています.OSの機能の,一番大きな役割ではないかと考えます.

つまり,OSとは
「低レベルの操作を統括し抽象化することにより,ユーザーには低レベル操作を意識させることなくプログラムの開発を進められるようにする仕組み」
であると考えます.

選択問題8

問題文を見た時,とても不思議なコードだと感じました.objdumpは何度も使ったことがありますが,これほどpushqが連続した逆アセンブル結果を見たことがなかったからです.
いくつか気になるところがあったので,次の点に注意しながらプログラムを解析していくことにしました.

  1. 0x4000a2で64bit長のデータをスタックに格納している.何か重要なデータかもしれない.
  2. 0x400110で,このプログラム中唯一比較演算をしている.
  3. 0x400129に,このプログラム中唯一の分岐命令がある.

複雑な動作をしているため,問題文と同じプログラムを作成して検証してみることにしました.8-5.sというファイルにアセンブリコードを保存しています.ただし,0x400129のjne命令でのジャンプ先アドレスを問題文と同じようにできませんでした.そのためジャンプ先アドレスを変更しています.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ as -march=corei7 8-5.s -o 8-5.o
$ ld 8-5.o -o 8-5_run
$ objdump -d 8-5_run
8-5_run: ファイル形式 elf64-x86-64
セクション .text の逆アセンブル:
0000000000400078 <_start>:
400078: 90 nop
400079: 90 nop
40007a: 90 nop
40007b: 90 nop
40007c: 90 nop
40007d: 90 nop
40007e: eb 00 jmp 400080 <main>
0000000000400080 <main>:
400080: 68 19 01 40 00 pushq $0x400119
400085: 6a 01 pushq $0x1
400087: 68 06 01 40 00 pushq $0x400106
(中略)
400126: 48 ff c9 dec %rcx
400129: 0f 85 01 00 00 00 jne 400130 <main+0xb0>
40012f: c3 retq
400130: 41 5a pop %r10
400132: c3 retq

このプログラムをgdbを使って実行しました.

1
2
3
4
5
6
$ gdb 8-5_run -q
Reading symbols from 8-5_run...done.
(gdb) run
Starting program: /home/nonoho/ownCloud/Event/SecurityCamp2016/8-5_run
12345678
[Inferior 1 (process 8199) exited with code 01]

おそらくsyscallで標準入力を読んでいると予想されるので,”12345678”を入力しました.ステータスが1で終了しているので,何か問題があったのではないかと考えられます.

続いて,ステップ実行をしてどのようにプログラムが進んでいるのかを確かめてみます.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ gdb 8-5_run -q
Reading symbols from 8-5_run...done.
(gdb) start
Temporary breakpoint 1 at 0x400080: file 8-5.s, line 13.
Starting program: /home/nonoho/ownCloud/Event/SecurityCamp2016/8-5_run
Temporary breakpoint 1, main () at 8-5.s:13
13 push $0x400119
(gdb) set logging on
main () at 8-5.s:14
14 push $0x1
(gdb) si
(以降siを繰り返す)
(gdb) si
main () at 8-5.s:61
61 syscall
(gdb)
[Inferior 1 (process 8750) exited with code 01]

出力されたgdb.txtファイルを見ながら,プログラムの流れを予想してみました.ただし,ここに掲載したログは見やすさのために,不要な行を飛ばしています.

  1. はじめはスタックにデータを格納しているだけです.ただし注意すべきことがあります.今回の場合,スタックに格納されるのは8バイト単位です.したがって,pushqを実行するたびにスタックポインタ(%rsp)は8ずつ小さい値を指すようになります.
    13 push $0x400119
    14 push $0x1
    15 push $0x400106
    (中略)
    41 push $0x400106
    42 push $0x0
    43 push $0x400102
  2. 次に,0x400101に到達したときの動作を考えます.ret命令が実行された時,スタック最上位に格納されているアドレスにジャンプし,スタックポインタを8バイト増やします.したがって,今回はスタック最上位の0x400102にジャンプすることになります.
    44 ret
  3. 続いてレジスタにデータを格納し,システムコールを呼び出しています.この時,システムコール番号を%raxに,引数を順に%rdi,%rsi,%rdx,%r10に格納します.では,今回はどのシステムコールが呼び出されたかを確認します.

    main () at 8-5.s:61
    61 syscall
    (gdb) i r
    rax 0x0 0
    rbx 0x0 0
    rcx 0x0 0
    rdx 0x8 8
    rsi 0x7fffffffdfd8 140737488347096
    rdi 0x0 0
    rbp 0x0 0x0
    rsp 0x7fffffffdff8 0x7fffffffdff8
    r8 0x0 0
    r9 0x0 0
    r10 0x0 0
    r11 0x0 0
    r12 0x0 0
    r13 0x0 0
    r14 0x0 0
    r15 0x0 0
    rip 0x400119 0x400119
    eflags 0x202 [ IF ]
    cs 0x33 51
    ss 0x2b 43
    ds 0x0 0
    es 0x0 0
    fs 0x0 0
    gs 0x0 0

    %raxに0が入っているため,予想通りreadが呼ばれています.readの引数は,ssize_t read(int fd, void *buf, size_t count);となっています.したがって,ファイルディスクリプタは0(stdin),格納先の先頭アドレスは0x7fffffffdfd8,読み出すバイト数は8バイトであることがわかります.

  4. 続いて,次の命令が実行されました.
    62 ret
    51 pop %rbp
    52 ret
    53 pop %rcx
    54 ret
    このときの%rcxの値は7になっていることを確認しました.
  5. この後,次の命令が8回実行されていました.
    59 xorb $0x55,(%rsi,%rcx,1)
    60 ret
    68 dec %rcx
    69 jne 0x400130
    71 pop %r10
    72 ret
    55 add %rbp,%rsp
    56 ret
    %rcxをデクリメントし,0になるまで繰り返しているようです.また,1回ループするごとにxor演算をしています.この動作をメモリダンプして確認しました.
    確認時には次の命令を実行しました.
    (gdb) x/128 (0x7fffffffe0a0-(128*4))
    ただし,文字数と見やすさの都合で,ここには該当箇所のみ記載します.

    1) xor実行前
    0x7fffffffdfc0: 0x0040011c 0x00000000 0x35343332 0x0a383736
    2) xor1回実行後
    0x7fffffffdfc0: 0x0040011c 0x00000000 0x35343332 0x5f383736
    3) xor2回実行後
    0x7fffffffdfc0: 0x0040011c 0x00000000 0x35343332 0x5f6d3736
    4) xor8回実行後
    0x7fffffffdfc0: 0x0040011c 0x00000000 0x60616667 0x5f6d6263

    この結果から,先程のシステムコールで入力した値の,すべてのバイトを,0x55との排他的論理和をとっていることがわかりました.

  6. 最後に,次の命令が実行されてプログラムが終了していました.
    49 pop %rdi
    50 ret
    45 pop %rax
    46 ret
    57 cmp %rax,(%rsi)
    58 ret
    45 pop %rax
    46 ret
    69 jne 0x400130
    71 pop %r10
    72 ret
    49 pop %rdi
    50 ret
    61 syscall
    cmp命令実行前の,レジスタの値とメモリダンプを確認しました.

    (gdb) i r
    rax 0x63391a67251b1536 7149774913733858614
    rbx 0x0 0
    rcx 0x0 0
    rdx 0x8 8
    rsi 0x7fffffffdfc8 140737488347080
    rdi 0x0 0
    rbp 0xffffffffffffffe0 0xffffffffffffffe0
    rsp 0x7fffffffe058 0x7fffffffe058
    r8 0x0 0
    r9 0x0 0
    r10 0x400102 4194562
    r11 0x302 770
    r12 0x0 0
    r13 0x0 0
    r14 0x0 0
    r15 0x0 0
    rip 0x400110 0x400110
    eflags 0x202 [ IF ]
    cs 0x33 51
    ss 0x2b 43
    ds 0x0 0
    es 0x0 0
    fs 0x0 0
    gs 0x0 0

    %raxの値と,%rsiの指すメモリ内容を比較しています.この時,%rsiの指すアドレスには次の値が入っていることが,先ほどのダンプ結果からわかっています.
    0x7fffffffdfc8: 0x60616667 0x5f6d6263
    この値は,プログラム実行時に入力した値を,0x5555555555555555でxor演算した値と等価です.今回は,入力した”12345678”と予めセットされている値が一致しなかったため,ステータスコード1で終了しました.そこで,2つの値が一致するような入力値を求めると,”c@Np2Ol6”であることがわかりました.

ここまでを踏まえて,もう一度プログラムを動作させてみます.

1
2
3
4
5
6
7
8
9
10
11
12
13
$ gdb ./8-5_run -q
Reading symbols from ./8-5_run...done.
(gdb) run
Starting program: /home/nonoho/ownCloud/Event/SecurityCamp2016/8-5_run
12345678
[Inferior 1 (process 13998) exited with code 01]

$ gdb ./8-5_run -q
Reading symbols from ./8-5_run...done.
(gdb) run
Starting program: /home/nonoho/ownCloud/Event/SecurityCamp2016/8-5_run
c@Np2Ol6
[Inferior 1 (process 14003) exited normally]

“c@Np2Ol6”を入力すると,ステータスコードが0で終了しました.

以上の調査より,このプログラムは,パスワード”c@Np2Ol6”を認証するプログラムであると考えられます.
このプログラムについての個人的な感想ですが,このプログラムは難読化のためにあえて読みづらいコードにしているのではないかと感じました.あわせて,認証するパスワードもxor演算を施しておくことで,解析しづらくしているのではないかと思います.