セキュリティキャンプ2018[脆弱性・マルウェア解析コース]のアセンブリwrite up

こんにちは~!今日はセキュリティキャンプの合格発表の日でしたね。CTF初心者勉強会に参加していたのですが当落が気になって全然集中出来ませんでした(笑)
結果はダメでした。去年は適当に書いたし知識もあまりなかったので落ちても仕方がないと思っていたのですが、今年は「好きな脆弱性について述べる問題」と「アセンブリを読み解く問題」の2問は字数制限の4096文字で書いたり他の問題もある程度書いたつもりですがセキュリティキャンプ課題はやはり厳しいですね。今年は応募総数が約400人で合格者が80人みたいです。
反省点もいくつかあり、1つ目は問7のイメージファイルの修復をする問題が全然解けなかったことです。これが一番の敗因であると考えています。またアセンブリを読み解くのに2週間くらいかけたこともあります。つまり知識不足ということです。これはCTFを通して勉強していきます。
もう1つはブログやgithubを全然活用していなかったこと。「作ってきたプログラムについて述べてください(問1)」と「ブログやgithubがあれば教えてください(問3)」は毎年聞かれていることを知りながらgithubもprivateとして使っていてpublicには何も投げていないしブログも全く書いていないとなると、いくら回答ページにこういうものを作りましたと説明しても審査員側は分からない、少なくともイメージしにくいですよね。これは今すぐ出来ることなので来年に向けて習慣化させていこうと思います。
そして3つ目に、そもそも作ってきたソフトウェア(問1)や解析したソフトウェア(問2)が少なかったこと。作ったソフトウェアはまだしも、ソフトウェアを解析したことはありませんでした。その時点で問2と問7は不利です。今年はゲーム解析を最低1回経験をして来年に書きやすいようにしていこうと思います。

自分の反省はここまでにして、下に自分が1番力を入れたアセンブリ読解の課題回答文を下に貼り付けようと思います。出てきた文字列はたぶん間違っていると思うのですが誰も投稿してる人がいないため間違っているのかも分かりません。個人的に問7のフォレンジックの問題の解き方はめっちゃ気になるので誰か投稿してほしいです。

[問題]

以下にDebian 8.10(amd64)上で動作するプログラムchal00のmain関数の逆アセンブル結果があります
("objdump -d chal00"の出力結果のうち、main関数の箇所を抜粋しました)。
このプログラムは、コマンドライン引数としてある特定の文字列を指定されたときのみ実行結果が0となり、
それ以外の場合は実行結果が1となります。
この実行結果が0となる特定の文字列を探し、その文字列を得るまでに考えたことや試したこと、
使ったツール、抱いた感想等について詳細に報告してください。
```
00000000004003c0 <main>:
  4003c0:       48 b8 0f 0e 0d 0b 00    movabs $0xc0601000b0d0e0f,%rax
  4003c7:       01 06 0c
  4003ca:       83 ff 02                cmp    $0x2,%edi
  4003cd:       48 89 44 24 f0          mov    %rax,-0x10(%rsp)
  4003d2:       48 b8 04 05 08 0a 02    movabs $0x70903020a080504,%rax
  4003d9:       03 09 07
  4003dc:       48 89 44 24 f8          mov    %rax,-0x8(%rsp)
  4003e1:       b8 01 00 00 00          mov    $0x1,%eax
  4003e6:       75 59                   jne    400441 <main+0x81>
  4003e8:       48 8b 56 08             mov    0x8(%rsi),%rdx
  4003ec:       31 c0                   xor    %eax,%eax
  4003ee:       89 c1                   mov    %eax,%ecx
  4003f0:       48 ff c0                inc    %rax
  4003f3:       80 7c 02 ff 00          cmpb   $0x0,-0x1(%rdx,%rax,1)
  4003f8:       75 f4                   jne    4003ee <main+0x2e>
  4003fa:       83 f9 08                cmp    $0x8,%ecx
  4003fd:       b8 01 00 00 00          mov    $0x1,%eax
  400402:       75 3d                   jne    400441 <main+0x81>
  400404:       48 8b 32                mov    (%rdx),%rsi
  400407:       31 c0                   xor    %eax,%eax
  400409:       30 c9                   xor    %cl,%cl
  40040b:       48 89 f2                mov    %rsi,%rdx
  40040e:       48 d3 ea                shr    %cl,%rdx
  400411:       83 e2 0f                and    $0xf,%edx
  400414:       0f b6 54 14 f0          movzbl -0x10(%rsp,%rdx,1),%edx
  400419:       48 d3 e2                shl    %cl,%rdx
  40041c:       83 c1 04                add    $0x4,%ecx
  40041f:       48 09 d0                or     %rdx,%rax
  400422:       83 f9 40                cmp    $0x40,%ecx
  400425:       75 e4                   jne    40040b <main+0x4b>
  400427:       48 33 05 92 ff ff ff    xor    -0x6e(%rip),%rax        # 4003c0 <main>
  40042e:       48 ba 85 03 0e 67 b3    movabs $0x600967b3670e0385,%rdx
  400435:       67 09 60
  400438:       48 39 d0                cmp    %rdx,%rax
  40043b:       0f 95 c0                setne  %al
  40043e:       0f b6 c0                movzbl %al,%eax
  400441:       c3                      retq

[回答]


まず逆アセンブル結果からAT&T記法が使われていることが分かります。1行目では%rax に0xc0601000b0d0e0f の即値が代入されています。2行目では%ediが$0x2と比較してフラグが生成されています。3行目で*(%rsp-0x10) に2行目の%raxが代入されます。4行目で%raxに$0x70903020a080504の即値が代入されます。5行目で*(%rsp-0x8)で4行目の%raxが代入されています。6行目では2行目で出されたフラグレジスタによって分岐されるjump命令があります。Jneはjump not equal の略称であるニーモニックで、これはプログラミング言語演算子である「!=」と同じです。つまり6行目では2行目で%edi = 2ではなかったら、アドレス400441にjumpするということです。アドレス400441ではretqとあるのでmainの終わりを意味していると思いました。%edi はデスティニーションレジスタといい、それの32bitレジスタです。これはC言語コマンドライン引数で言う、argcに相当します。なので2行目と6行目の意味は、コマンドライン引数が2つでなければ処理を終えるということなります。7行目では%rdx = *(%rsi + 0x8)という式になっています。%rsiということなのでソースインデックスの64ビットレジスタ、これはC言語コマンドライン引数で **argv に相当します。1アドレスに1バイトが割り当てられるので*(rsi)の8つ上のアドレスということを意味しています。*(%rsi) はargv[0]のポインタですので、その8つ上ということで64ビット、つまり8バイト上のargv[1]のポインタということになります。私は最初、反対に*(%rsi + 0x8) = *argc であると勘違いしていましたが、読み進めていくうちに違いに気づき、調べなおして理解しました。8行目は%eaxと%eaxをxorしているので0になります。9行目は8行目の%eaxを%ecxに代入しているだけです。10行目は%raxをインクリメントしています。プログラミング言語のように表すと%rax += 1です。11行目は*(%rdx-1+%rax)と0x0を1バイト単位で比較しています。%raxとありますが1バイト単位の比較なので、下位ビットの%eaxと同様に見ても大丈夫と判断しました。なので*(%rdx -1 + %eax)という式に変えます。%eaxは9行目で1インクリメントされているので、1が代入されています。つまり*(%rdx -1 + 1) = *(%rdx)となり、これはargv[1]の1文字目であります。12行目でjneのアドレス4003eeに戻るとあるので、*(%rdx-1 + %eax) が0でない限り、9行目から12行目をループします。9行目に戻ると%ecxには1が代入されます。10行目に戻り、%raxがインクリメントされ、2となります。11行目に戻り、*(%rdx + 1) と 0を比較しています。これはargv[1]の2文字目です。ここで0でなかったら再び9行目に戻ります。というようにループしていくと思うんですが、最初なにしてるんだって思いました。しかし、ちょっと考えてすぐにC言語入門の配列とポインタで勉強した、あることを思い出しました。配列の最後には必ずエスケープシーケンスと呼ばれる制御文字の0が入っていることです。先に次に行を見ると、13行目、%ecxと0x8を比較しています。14行目は%eaxに即値0x1を代入しているだけです。そして15行目で13行目で生成したフラグレジスタによって、アドレス400441にジャンプ する、つまりmain処理を終えます。ここまで読んで、9行目から12行目のループの意味がわかります。制御文字0まで、どんどん%ecxが1ずつ更新されていって、%ecx が8 かどうか比較しているので、ここではコマンドライン引数に入れる文字の長さを測っています。1文字1バイトなので、文字数は8文字であると断定します。16行目からは28行目との間でループ処理に入ります。(アドレス400404~アドレス400425)。まずmovzbl(move zero-extension from byte to long)はどういう命令かというとソースオペランドをデスティネーションオペランドにゼロ拡張してコピーするという命令です。ゼロ拡張はサイズの小さい方からサイズの大きい型に変換するときに足りない部分をゼロで埋めるという操作をします。またここはバイトオーダーにも注意しなければなりません。バイトオーダーとはCPUがメモリに2バイト以上のデータを格納する時の順番のことで、逆転しない方をビッグエンディアン、逆転する方をリトルエンディアンと呼ばれていて、この2種類に分けられています。X64はリトルエンディアンで逆転します。この逆転が起きるのはレジスタからメモリ、もしくはメモリからレジスタへの操作を行うときです。ただし文字列は1バイトのデータの配列なので、メモリ上でも見た目通りの順番で入ります。例えば、’ABCDEFGH’という文字列はメモリ上では41 42 43 44 45 46 47 48という配置になりますが、レジスタに入れるとこれが逆転していることをGDBでも確認できました。つまり、./a.out ABCDEFGHを実行すると、%rsi = 0x4847464544434241が入ると考えられます。16行目から処理の内容を理解しやすいようにコマンドライン引数のargv[1]に’ABCDEFGH’を与えたと仮定してアセンブリを読み進めていきたいと思います。まず%rsi = *(%rdx)ということで上に書いたとおりここでは0x4847464544434241が入ります。17行目で%eax がxorにより0になり、18行目も同様に%clがxorされ0が代入されます。%cl は%ecxの下位8ビットです。後々影響が出てきそうなのでメモしました。そして19行目で%rdx に%rsiの値をコピーします。そして後の二つの行は、まず右シフトで%cl分下位ビットを削りand %0xfという論理積で上位ビットを削っています。20行目の1回目の処理は%cl=0なので、なにもしません。21行目で%edx = %edx and $0xfということで、まず%edx は%rdxの下位32ビットなので、%edx= 0x44434241という値が入っていて16進数一桁分を取り出しているので今回は1が%edxに入ります。そして22行目、式は%edx = *(%rsp-0x10 + %edx)となります。この1バイト単位で処理するので%rdxは%edxと書いても差し支えないと判断しました。Mainの最初の方で*(%rsp-0x10)に即値を入れていたことから、%rsp-0x10が配列のアドレスで%edxはインデックスの役割をしていると考えられます。冒頭で記述しなかったのですが、最初にデータを入れた時は8バイトのデータとしてメモリに書き込んでいるので、ここでもリトルエンディアンが働いていて逆転します。見やすく直すとdata = {0x0f,0x0e,0x0d.0x0b,0x00,0x01,0x06,0x0c}という順番で並んでいると考えられます。ひとまず23行目に続きます。%rdx に%cl回数分左シフトしています。22行目では%cl に4が足されます。そして23行目では%rax = %rax or %rdxしています。%raxは17行目で0が代入されているので、これは結果を保持するための処理ということが考えられます。data に入っているのは全て4bitしかないのでorをしても、それ以外の部分の値が崩れることはありません。2回目のループでは%clが4なので16進数で一桁分右にずれます。そしてand $0xfで上位ビットを削られるので次は%edx=4となります。よって%edx はゼロ拡張されdata配列の要素5番目が入ります。あとは左シフトで%rdxを戻して%raxとorいて16進数で下位の2桁目にmovzblで抜き出した値を入れます。3回目からも同じであとは%raxが埋まるまでループします。あと少しなので先にループを抜けた後の処理を見てみます。アドレス400427では%rax = %rax xor *(%rip-0x6e)ということで、この*(%rip-0x6e)はmainのアドレス4003c0と注があるのでアドレス4003c0を見てみます。%ripって今いるメモリを表していると前に勉強したのですが、その意味がこの部分で実感できました。さて、アドレス4003c0を見ると一番最初の処理のことでした。つまりアドレス400427は%rax = %rax(前のループで抜き出していた値) xor $0f0e0d0b0001060cという式になると考えます。前のループで抜き出した値はまだわからないので次に進みます。アドレス40042eでは%rdx に$0x600967b3670e0385の即値を代入しています。次の行では%rax と%rdxを比較しています。そして次の行(アドレス40043b)では、ニーモニックがsetneなので(set not equalの略。条件が満たされる場合は1、満たされない場合は0を返す)%rax == %rdxの時だけ0を返してくれます。最後に%eax に %al をmovzblするということで%eaxがゼロ拡張され処理を終えます。%rdxもリトルエンディアンが働いているので$0x85030e67b3670960となっている。この%rdxとアドレス400427の即値$0x0f0e0d0b0001060cから逆算して、ループで抜き出した値を計算してみました。ですが明らかに違う。一応書くと出た数値は0x8a0d036cb3660fccでした。原因を1つ1つ見直してたどり着いたのが*(%rip-0x6e)の行でした。ripは64bitのプログラムカウンタなので、つまり正しくは*(rip-0x6e) = 0x48b80f0e0d0b0001なので、リトルエンディアンが働いてxor rax(ループで取り出した値) ,0x01000b0d0e0fb848をしています。同時にアドレス40042eの機械語とアセンブリ部分に目を向けると、逆アセンブラが勝手に逆順にしてくれているのが分かります。よって0x01000b0d0e0fb848と0x600967b3670e0385を逆算します。
0x600967b3670e0885を2進数に直すと
110000000001001011001111011001101100111000011100000100010000101
0x01000b0d0e0fb848を2進数に直すと
000000100000000000010110000110100001110000011111011100001001000
Xorすると0xc1b0016fbe6c0961となりました。data
= {0x0f,0x0e,0x0d.0x0b,0x00,0x01,0x06,0x0c}の文字で構成されています。最初に%raxに入った値は1です。つまり文字列の値はAsciiで16進数下位桁で5ということが分かります。次は6なので、インデックス6なので文字列の値は16進数上位桁で6です。次はと思ったら!!0x9?!!だと。。。計算は間違ってないはずなんですが、。しかしやり方は得絶対合っている!という自信があったので他に見落としているものを考えます。ずっと気になっていた行があります。アドレス4003dcの*(%rsp-0x8) = $0x70903020a080504です。これ1回も使ってないよなーと思っていて、ちょうど0x09の値をここで見つけます。おそらくですが、int型は4バイトなので2つ上のメモリから$0x70903020a080504を入れていると考えます。つまりdata[] = {0x0f,0x0e,0x0d.0x0b,0x00,0x01,0x06,0x0c, 0x04,0x05,0x08,0x0a,0x02,0x03,0x09,0x07}という配列になっているのではないかと考えます。すると3桁目はe、次に4、7、6、1、3、0、6、5、4、4、3、5、7となると考えます。つまり0x7534456031674e65となり文字列に変換するとu4E’1gNeとなりました。なんか、こういうのってメッセージあるかなって思うので(CTFとか)不安になったのですがひとまず文字列を出せました。なんとか形にはできたので自分的には満足です。





以上長々と書きましたがアセンブリは最近勉強し始めたばっかりで結構時間かかりました。でもこの課題を通してアセンブリにだいぶ慣れました。来年はもっとスラスラ読めるようになっていると思うので冒頭の反省点を改善して来年またチャレンジします。