2016年12月22日木曜日

きつねさんでもわかるLLVM - kosen14s読書会

はじめに

この投稿はkosen14s読書会用の記事です。
Kosen14s読書会
kosen14s読書会では、何人かのメンバーが自由なジャンルの本を紹介します。前回はPractical Malware Analysisについて投稿しました。


きつねさんでもわかるLLVM


本記事で紹介する本は、柏木 餅子さんと風薬さんの著書「きつねさんでもわかるLLVM ~コンパイラを自作するためのガイドブック~」です。
きつねさんでもわかるLLVM ~コンパイラを自作するためのガイドブック~
表紙とタイトルを見て、「これならコンパイラが作れそう!」と思った時期もありました......
というのも、やはり内容は難しかったです。そして私はまだ完全に理解してません。そのため今回は、電卓を例にとってプログラミング言語がどのように表現されるかについて、かいつまんで説明します。ただ、あまり細かいところを理解していないのでところどころ説明に間違いがあるかもしれません。

電卓を作ってみる

こんな記事に辿り着く人はコンパイラを使ったことがある人がほとんどだと思います。コンパイラは入力されたソースコードを解析し、中間言語を生成したり実行ファイルを生成したり、とにかく人間が読めるコードを機械が解釈できるコードに変換するツールです。本書で使用するLLVMでは、ソースコードをレキシカルアナライザと呼ばれるプロセッサでトークンと呼ばれる意味を持った単語に分割されます。例えば次のような入力ソースコードがあったとします。
「4 / 3 * 3.14 * 10 ** 2」
レキシカルアナライザはこれを次のように分割します。
「4」「/」「*」「3.14」「*」「10」「**」「2」
そして、lex/yaccという言語(?)を使うと自分がどんな言語を作りたいかを規定するだけでこのトークン分割を自動でやってくれるのです。電卓程度ならこれを規定するのが非常に簡単です。例えば足し算「+」を定義したければ次のように書きます。
"+" {
    return OPERATOR_ADD;
}



OPERATOR_ADDなども別ファイルで自分で定義するので、四則演算だけでなく、複雑な演算やオリジナルの演算子を簡単に定義できてしまうのです。また、演算子以外にも「整数とは何か」や「小数とは何か」も定義する必要があります。例えば小数は次のように定義されます。
[0-9] *\.[0-9]* {
    double temp_box;
    sscanf(yytext, "%lf", &temp_box);
    yylval.double_value = temp_box;
    return DOUBLE_LITERAL;
}



先程の足し算より長くなりましたが、これは計算時に具体的な数字を参照するためにyytextなどを設定する必要があるからです。詳しくはぜひ本を読んでみてください。


このように「足し算とは」「整数とは」「文字列とは」「変数とは」「関数とは」などなど、トークンについて全て定義します。次にこれらトークンを解釈するのがパーサと呼ばれるプロセッサです。例えば「1+2*3」を計算するとき、人間は掛け算を優先します。また、「*は乗算である」だけでなく、「乗算とは何か」という具体的な処理を解釈するのがこのパーサyaccです。さらに、パーサはコンパイルエラーを出す必要があります。例えば「1+2*3*」という計算式はエラーとなります。ではエラーでない乗算とは何かというと、「A*B」のように「*」の両側に数字が付いていることです。数字というより、最終的に数字になる式がAやBにあたります。例えば「(1+2)*(3*4+1)」の場合、(1+2)と(3*4+1)をそれぞれA,Bとして認識させる必要があります。そういった表現の定義はBNFという記法で書かれます。先の例でAやBをtermとして定義します。(もちろんこれも自分で定義するのでtermじゃなくてhogeとかでもいいです。)
term
: primary_expression
| term OPERATOR_MUL primary_expression
{
    $$ = $1 * $3;
}
| term OPERATOR_DIV primary_expression
{
    $$ = $1 / $3;
}
;
一行目は「termとは何か」の定義開始です。二行目は「termとはprimary_expressionである」という意味です。ちなみにprimary_expressionは後で定義する「値」を表すものです。ここまでで「式termとは値primary_expressionである」と定義されました。三行目は「もしくはterm * primary_expressionである」という意味です。OPERATOR_MULは前にレキシカルアナライザのところで定義しました。ここで注目したいのが、termの定義にtermが入っているということです。これだとずっと自分自身を参照しそうですが、一行目の「termとはprimary_expressionである」があることにより、いつかはprimary_expressionに移るのです。「term * primary_expression」を宣言しましたが、これが返す具体的な値を各必要があります。これが四行目から六行目の括弧内にある「$$ = $1 * $3;」で、「返す値 = term * primary_expression」という意味です。$1とか$3は今解析しているトークンから何番目のトークンであるかを示します。同様にして除算も定義しました。加算や減算は優先順位が低いのでexpressionとして定義します。(最後に全体のコードを示します。)
さて、 「termとはprimary_expressionである」と定義したので、今度はprimary_expressionも定義します。これはただの値(今回は小数のみ)なので、次のようにかけます。
primary_expression
: DOUBLE_LITERAL
;
ここで始めてその項の評価が終わります。(ツリーの葉に到達する。)
詳しく知りたい方は是非本を読んでみることをおすすめしますが、今回の電卓の全体像を載せておきます。

/*
%{ ...%}で囲まれた行はC言語そのままとして解釈されます。
したがって、%{ ... %}内は解析時にスキップされ、
コンパイル時にコンパイラが読むだけです。
*/
%{
/* stdio.hはキーボードからの入力に必要です。 */
#include <stdio.h>
/* "y.tab.h"はyaccコマンド実行時に出力されます。 */
#include "y.tab.h"
/* 必要な関数だけど使わないので空 */
int yywrap(void) { return 1; }
%}
/*
%% ... %%で囲まれた行はlexに解析されます。
ここには自分の作りたいプログラム言語の規則を書きます。
*/
%%
/**** 次のようにyaccでは「演算子」を定義します。
「足し算」とは...
"+" : "+"である
詳細な計算方法はcalc.yで定義します。
*/
"+" {
/* ここには上の定義のもと「足し算」と判断された場合の処理を書きます */
return OPERATOR_ADD;
}
"-" {
return OPERATOR_SUB;
}
/* returnだけの場合、次のように{}を省略できます。 */
"*" return OPERATOR_MUL;
"/" return OPERATOR_DIV;
"\n" return EOL;
/**** 次のようにyaccでは「整数とは」を定義する必要があります。
整数とは...
[1~9] --> 文字の1~9が最初にある
[0-9]* --> 文字の0~9が続く('*'が繰り返しを意味する)
*/
[1-9][0-9]* {
/*** ここには上の定義のもと「整数」と判断された場合の処理を書きます。 */
double temp_box;
/* 今回は実数しか使わないので実数として読み込みます */
/* 現在の文字(yytext)を整数(%d)としてtemp_boxに入力します。 */
sscanf(yytext, "%lf", &temp_box);
/* 現在のint_valueは上で取得した整数です。 */
yylval.double_value = temp_box;
return DOUBLE_LITERAL;
}
/* 上の定義だと0が整数にならないので定義します。 */
"0" {
yylval.double_value = 0.0; // "0"は0.0
return DOUBLE_LITERAL;
}
/**** 実数とは...
[0-9]* --> 文字の0~9が続く
\. --> 文字の"."がある('.'は正規表現で意味を持つので'\.'と書きます)
[0-9]* --> 文字の0~9が続く
*/
[0-9]*\.[0-9]* {
/*** ここには上の定義のもと「実数」と判断された場合の処理を書きます。 */
double temp_box;
/* 現在の文字(yytext)を実数(%lf)としてtemp_boxに入力します。 */
sscanf(yytext, "%lf", &temp_box);
/* 現在のdouble_valueは上で取得した実数です。 */
yylval.double_value = temp_box;
return DOUBLE_LITERAL;
}
/*
DOUBLE_LITERALやOPERATOR_ADDなどの定数は
calc.yの方で定義するので、それと同じ名前であれば
何でも構いません。
yytextやyylvalもcalc.yの方で定義します。
yylval内のdouble_valueなどもcalc.yで定義します。
*/
%%
view raw calc.l hosted with ❤ by GitHub
/*
%{ ...%}で囲まれた行はC言語そのままとして解釈されます。
したがって、%{ ... %}内は解析時にスキップされ、
コンパイル時にコンパイラが読むだけです。
*/
%{
#include <stdio.h>
#include <stdlib.h>
%}
// 現在解析中の具体的な値が入ります
%union {
// プログラミング言語などでは型がたくさんあるので
// 構造体などをここに入れますが、
// 今回は実数しか使わないようにしました。
double double_value;
}
%token <int_value> INT_LITERAL // 例えばcalc.lで整数と判定されたら、
%token <double_value> DOUBLE_LITERAL // INT_LITERALを受け取ることになります。
%token OPERATOR_ADD // 演算子にも名前を付けておきます。
OPERATOR_SUB // これらもcalc.lで使う用なので、
OPERATOR_MUL // ここでは宣言だけをしておきます。
OPERATOR_DIV
EOL // 行の終端すら定義します
//
// %type <int_value>
%type <double_value> expression term primary_expression
/*
%% ... %%で囲まれた行はlexに解析されます。
ここには自分の作りたいプログラム言語の規則を書きます。
*/
%%
//// 次のようにyaccでは「ソースコードとは」すら定義する必要があります
// 「ソースコード」とは...
// line --> 1行
// | --> または
// source_code line --> 「ソースコード」「1行」で構成される
source_code
: line
| source_code line
;
//// 次のようにyaccでは「行とは」すら定義する必要があります
// 「行」とは...
// expression --> 何かしらの式がある
// EOL --> 改行文字がある
line
: expression EOL
{
//// ここには「行である」であると判定された際の処理を書きます。
// 行判定されたら解析結果($1)を出力します。
printf(">> %lf\n", $1);
}
;
//// 上で使った「何かしらの式」(expression)を定義します
// 「何かしらの式」(expression)とは...
// term --> 項である
// | --> または
// expression OPERATOR_ADD term --> 「何かしらの式」 + 項 である
// | --> または
// expression OPERATOR_SUB term --> 「何かしらの式」 - 項 である
expression
: term
| expression OPERATOR_ADD term
{
//// ここには入力文字が「expression + term」であると判定された際の処理を書きます。
// $1 : expression
// $2 : OPERATOR_ADD
// $3 : term
// 結果 = expression + term
$$ = $1 + $3;
}
| expression OPERATOR_SUB term
{
$$ = $1 - $3;
}
;
//// もちろん「項」も定義します。
// 「項」(term)とは...
// primary_expression --> 「1つの項」である
// | --> または
// term OPERATOR_MUL primary_expression --> 「項」 * 「1つの項」である
// | --> または
// term OPERATOR_DIV primary_expression --> 「項」 / 「1つの項」である
term
: primary_expression
| term OPERATOR_MUL primary_expression
{
$$ = $1 * $3;
}
| term OPERATOR_DIV primary_expression
{
$$ = $1 / $3;
}
;
// 「1つの項」(primary_expression)とは...
// DOUBLE_LITERAL --> 実数である
primary_expression
: DOUBLE_LITERAL
;
%%
/*
ここにコンパイルエラー発生時の処理を書きます。
*/
int yyerror(char const* str)
{
extern char *yytext;
fprintf(stderr, "Error near %s\n", yytext);
return 0;
}
/*
ここにコンパイラを書きます。
今回は電卓なので、標準入力から文字を受け取って計算結果を出力します。
*/
int main(void)
{
// yaccが勝手に解析してくれる関数
extern int yyparse(void);
// コンパイルすべきファイル
extern FILE *yyin;
// 今回はファイルではなくstdinからの入力
yyin = stdin;
if (yyparse()) {
// エラー発生
fprintf(stderr, "ERROR!\n");
exit(1);
}
return 0;
}
view raw calc.y hosted with ❤ by GitHub
# calc.binを出力するコマンド
calc.bin: y.tab.c lex.yy.c # 下のコマンドに必要なファイル
cc -o calc.bin y.tab.c lex.yy.c
# y.tab.cを出力するコマンド
y.tab.c: calc.y # 下のコマンドに必要なファイル
yacc -dv calc.y
# lex.yy.cを出力するコマンド
lex.yy.c: calc.l # 下のコマンドに必要なファイル
lex calc.l
# ゴミを削除
clean:
rm y.output y.tab.c y.tab.h
rm lex.yy.c
view raw Makefile hosted with ❤ by GitHub
実行するとこんな感じで計算できます。



ついでに括弧付き計算や累乗にも対応したバージョンです。
%{
#include <stdio.h>
#include "y.tab.h"
int yywrap(void) { return 1; }
%}
%%
"+" return OPERATOR_ADD;
"-" return OPERATOR_SUB;
"*" return OPERATOR_MUL;
"/" return OPERATOR_DIV;
"^" return OPERATOR_POW;
[1-9][0-9]* {
double temp_box;
sscanf(yytext, "%lf", &temp_box);
yylval.double_value = temp_box;
return DOUBLE_LITERAL;
}
"0" {
yylval.double_value = 0.0;
return DOUBLE_LITERAL;
}
[0-9]*\.[0-9]* {
double temp_box;
sscanf(yytext, "%lf", &temp_box);
yylval.double_value = temp_box;
return DOUBLE_LITERAL;
}
[ \t] ; // [追加]タブや空白は無視
"\n" return EOL;
%%
view raw calc.l hosted with ❤ by GitHub
%{
#include <stdio.h>
#include <stdlib.h>
%}
%union {
double double_value;
}
%token <int_value> INT_LITERAL
%token <double_value> DOUBLE_LITERAL
%token OPERATOR_ADD
OPERATOR_SUB
OPERATOR_MUL
OPERATOR_DIV
OPERATOR_POW // [追加]累乗
EOL
%type <double_value> expression
expression_addsub
expression_muldiv
expression_power
expression_term
%%
source_code
: line
| source_code line
;
line
: expression EOL
{
printf(">> %lf\n", $1);
}
;
/* [変更]
expressionやらtermやらややこしいので、
expression_演算子
に変更。
優先順位が高いほどexpression_termに近く、
優先順位が同じものは同じexpressionに記述する。
*/
expression
: expression_addsub
;
expression_addsub
: expression_muldiv
| expression_addsub OPERATOR_ADD expression_muldiv
{
$$ = $1 + $3;
}
| expression_addsub OPERATOR_SUB expression_muldiv
{
$$ = $1 - $3;
}
;
expression_muldiv
: expression_power
| expression_muldiv OPERATOR_MUL expression_power
{
$$ = $1 * $3;
}
| expression_muldiv OPERATOR_DIV expression_power
{
$$ = $1 / $3;
}
;
expression_power
: expression_term
| expression_power OPERATOR_POW expression_term
{
int i;
double ret;
// 累乗とは
for(i = 0, ret = 1.0; i < (int)$3; i++) {
ret *= $1;
}
$$ = ret;
}
;
expression_term
: DOUBLE_LITERAL
;
%%
int yyerror(char const* str)
{
extern char *yytext;
fprintf(stderr, "Error near %s\n", yytext);
return 0;
}
int main(void)
{
// yaccが勝手に解析してくれる関数
extern int yyparse(void);
// コンパイルすべきファイル
extern FILE *yyin;
// 今回はファイルではなくstdinからの入力
yyin = stdin;
if (yyparse()) {
// エラー発生
fprintf(stderr, "ERROR!\n");
exit(1);
}
return 0;
}
view raw calc.y hosted with ❤ by GitHub

実行するとこんな感じで計算できます。

さいごに

数年前にもこの本を手に取ったのですが、当時は何一つ分からずに放置していました。しばらくして読んでみると内容が全然違うとうに思えて面白いですね。まだまともな変数も実装できないですが、ちょっとずつ勉強して、いつかまともな言語を作れたらと思います。

2016年12月11日日曜日

SECCON 2016 Online予選に参加しました

結果

既に本選参加権は持っていたので、今回はいつものメンバーと分断して気楽にチームdogeとして参加しました。得点は700点で順位は125位でした。オンライン予選は初参加ですが、予想以上に難しかったです。今回は自分が解いた「VoIP」「Memory Analysis」「Anti-Debugging」「randomware」について軽くWriteupを書きます。

VoIP

タイトル通りVoIPを使用した電話のパケットを記録したデータを解析する問題です。Wiresharkにはこれを解析する機能が備わっているので、それで通話内容を再生できます。(LinuxのWiresharkでは再生機能がありませんでした。)あとは喋っているフラグを聞けばいいのですが、「V」を「B」と聞き間違えていたので結構時間がかかりました。気付くまでの間は次のMemory Analysisに取り組んでいました。
フラグ:FLAG{9001IVR}

Memory Analysis

これもタイトル通りで、Windowsのメモリダンプを解析する問題です。どれかというとForensicが得意分野なので何としても解かなければならない問題です。問題文は、「偽物のsvchost.exeが接続しようとしている宛先はどこですか」というものです。

$ volatility --file=forensic_100.raw imageinfo
[snip]
Suggested Profile(s) : WinXPSP2x86, WinXPSP3x86 (Instantiated with WinXPSP2x86)
[snip]
まぁ問題文から分かりますが、Windowsだそうです。とりあえずsvchost.exeについて調べる訳ですが、pstreeすると結構たくさんのsvchost.exeがありました。
$ volatility --file=forensic_100.raw --profile=WinXPSP2x86 pstree
上のコマンドの実行結果より、svchost.exeという名前のプロセスはPIDが1036,936,1088,848,1320,1776,1704の7個あります。次にこれらのプロセスのうち偽物を判別するために、dlllistを使用します。
 $ volatility --file=forensic_100.raw --profile=WinXPSP2x86 dlllist --pid=1036,936,1088,848,1320,1776,1704
すると、PIDが1776のsvchost.exeはパスが他と違って"C:\Windows\svchost.exe"である他、ロードされているDLLも他と大きく異なります。そこでこれをダンプして、stringsで接続先が見つからないかを試します。
$ volatility --file=forensic_100.raw --profile=WinXPSP2x86 procdump --pid=1776 -D ./
$ strings executable.1776.exe | grep http
C:\Program Files\Internet Explorer\iexplore.exe http://crattack.tistory.com/entry/Data-Science-import-pandas-as-pd
それらしいものが見つかりました。しかし、このサイトにはデータは存在しません。そこでhostsファイルを調べます。
$ volatility --file=forensic_100.raw --profile=WinXPSP2x86 filescan | grep hosts
0x000000000217b748      1      0 R--rw- \Device\HarddiskVolume1\WINDOWS\system32\drivers\etc\hosts
$ volatility --file=forensic_100.raw --profile=WinXPSP2x86 dumpfiles -Q 0x000000000217b748 --name -D ./
$ cat file.None.0x819a3008.hosts.dat
[snip]
153.127.200.178    crattack.tistory.com
実際の接続先が153.127.200.178であることが判明したので、こちらの方にアクセスします。ファイルをダウンロードすると中にフラグが書いてあります。
フラグ:SECCON{_h3110_w3_h4ve_fun_w4rg4m3_}

Anti-Debugging

アンチデバッグ系にはそこそこ慣れていたので簡単でした。ひたすらアンチデバッグ技術が登場するので、それらを掻い潜るとフラグを表示してくれます。普通に実行したらダメなの?と思いましたがVMware上にしかWindowsを持っておらず、VMware検知もあったのでデバッガで回避することに。OllyDbgやらIDAやらをいろんな方法で検知してきますが、ひたすらZFを変更し続けるとスキップできます。最後に0除算(?)で例外を発生させているので注意。(フラグは忘れました。)

randomware

これもフォレンジックなので個人的に解きたかった問題です。開始18時間くらいでうっかり寝ていて、起きたらスッキリして解けました。これはQEMUのディスクイメージが渡されるのですが、とりあえずrawに変換してFTKで中身を見ました。いろいろ見ていると「h1dd3n_s3cr3t_f14g.jpg」と「getflag」というファイルがあったのでここから着手することに。h1dd3n_s3cr3t_f14g.jpgの方はバイナリはJPEGのものとは大きく異なり、0から0xFFでXORしてみるも全てfileコマンドではdataとなりました。getflagはLinux用の32bitバイナリで、IDA Proで解析しました。問題を解くのに必要な大体の流れば次のようになります。

1) /dev/urandomから0x400バイトを取得 --> encrypt_keyに格納
2) 再帰的に特定の条件にマッチするファイルを選ぶ
3) 2)のファイル全体を0x400バイトのencrypt_keyでXOR

ということで、明らかにXORされたっぽいファイルを探します。「h1dd3n_s3cr3t_f14g.jpg」以外に、「blocklist.xml」「revocations.txt」「user-extension-html.xml」などのファイルがXORされていました。問題は、いずれも平文が無いということです。元の文が無いと数学的に考えて解読できないじゃないか!と思っていましたが、例えばXMLの最初が「<?xml version...」みたいな文字で始まることを考えると最初の部分のencrypt_keyは取得できます。
次のポイントは、blocklist.xmlおよびrevocations.txtはfirefox等が作成するファイルで、作者が用意したものではないという点です。実際これらのファイル名で調べると、内容はサイトによってバラバラであるものの、「だいたいこういうことが書いてある」ということは分かります。また、user-extension-html.xmlについては結構最初の部分が固定されているので、これで100バイトくらいはencrypt_keyを求められることになります。
これだけencrypt_keyを求められたら、反対にblocklist.xmlおよびrevocations.txtの最初の100バイトくらいも復元できます。解いてるときはこの辺で時間かかったのですが、ここでは最終的に確立したやり方だけを説明します。
revocations.txtは内容がBase64です。また、ファイル全体をBase64エンコードしている訳ではなく、意味のあるブロックごとにエンコードしています。したがって、例えばencrypt_keyにより最初の数バイト「MMGExCzAJ」くらいさえ分かれば、ネット上に転がっているrevocations.txtから同じブロックを探して、続きを予測することができます。続きが分かるということは、反対にencrypt_keyの分からなかった部分が徐々に分かっていくということです。この「blocklist.xml」<--encrypt_key-->「revocations.txt」のやりとりを繰り返すことでencrypt_keyを0x400バイト全て求めることができました。
正攻法かは分かりません。
フラグ:SECCON{This is Virtual FAT too} (お肉の写真とともに)

まとめ

挑戦した中でも「checker」なんかは解き方が分かってたのにコードでミスしてて諦めてしまいました。(「動かない」+「300pt」=「自分が間違ってる」)他にも「こんなの解けたじゃん」みたいな問題が500pt分くらいはあったので、もったいないなぁという感じでした。何はともあれ楽しかったです。運営のみなさん、参加者のみなさん、お疲れ様でした!