Trust Fund

Security Innovation Blockchain CTF Writeup (ภาษาไทย)

Teerasak R.
2 min readJun 10, 2022

Contract’s Code

Solution

อันดับแรก ทำความเข้าใจก่อนว่า Contract นี้ทำอะไร และมีการทำงานยังไงบ้าง จุดสำคัญที่เกี่ยวกับการส่ง ETH (เป้าหมายของเรา) มีอยู่จุดเดียวใน Contract คือ function withdraw()

ทีนี้ลองดูลึกลงไปว่า function นี้มันทำงานอะไรบ้าง จะเห็นว่ามีการเรียก function checkIfYearHasPasses() ก่อน ซึ่งจะนับจำนวนการถอน คูณด้วย 365 วัน สรุปง่าย ๆ ก็คือ Contract นี้ตั้งใจให้ถอนได้แค่ปีละครั้ง ครั้งละ 1 ใน 10 ของเงินทั้งหมด ดูจาก allowancePerYear คือค่า ETH ที่ส่งเข้าไปตอน deploy หารด้วย 10 และบังคับโอนเงินกลับหา caller ทีละ 1 ใน 10

หมายความว่าจะเอา ETH ทั้งหมดออกมาต้องใช้เวลา 10 ปี! แต่เดี๋ยวก่อน ลองสังเกตดู if(msg.sender.call.value(allowancePerYear)()) ตรงนี้คือการโอน 1 ใน 10 ส่วนของ ETH ไปหา caller ถ้าโอนสำเร็จ จะได้ return เป็น true แล้วจะไปทำเงื่อนไขที่อยู่ภายใน if ต่อ ซึ่งก็คือการ mark ว่าปีนี้โอนไปแล้ว ปีหน้าถึงจะโอนครั้งต่อไปได้

ช่องโหว่อยู่ตรงนี้แหละ ลองดูดี ๆ

เฉลย ตรงจุดนี้มีช่องโหว่ที่เรียกว่า Reentrancy Attack ครับ ช่องโหว่นี้คืออะไรขออนุญาตเว้นการอธิบาย เพื่อไม่ให้นอกประเด็นจนเกินไป (เพราะค่อนข้างยาว และเข้าใจยากอยู่หน่อย) ลองอ่านตาม link ต่อไปนี้ดูครับ

กลับมาต่อที่การโจมตี อธิบายหลักการโจมตีของ Reentracy Attack คร่าว ๆ คือ เราจำเป็นต้องเขียน Smart Contract (ขอเรียกว่า SC) ขึ้นมาเพื่อไป call ตัว Contract โจทย์อีกทีนึง ซึ่ง SC ที่เราต้องเขียนจะมีการทำงาน 3 ขั้นตอนหลัก ๆ

  1. call withdraw()
  2. เขียน fallback หรือ receive function เพื่อให้ trigger เมื่อมี ETH ถูกโอนเข้ามา (จาก Contract โจทย์) แล้วให้ call withdraw() ซ้อนกลับไปเพื่อให้ Contract โจทย์โอน ETH มาแล้วก็ call withdraw() ซ้อนในซ้อนกลับไปเพื่อให้ Contract โจทย์โอน ETH มาให้อีก แล้ว call withdraw() ซ้อนในซ้อนในซ้อน... ทำแบบนี้ไปเรื่อย ๆ จนกว่า ETH ใน Contract โจทย์เหลือ 0
  3. โอน ETH ที่ SC ของเรารับโอนมาทั้งหมด กลับมาหา address ของเรา

จากที่อธิบายมาถ้านำมาเขียนเป็น Contract จะได้แบบนี้

ส่วนว่าจะ compile และ deploy SC นี้ด้วย software อะไร วิธีไหน ก็ขึ้นอยู่กับถนัดได้เลยครับ เช่น Truffle Hardhad หรือ Remix IDE โดยตอน deploy จะต้องใส่ address ของ Contract โจทย์ลงไปด้วยเพื่อ set เป้าหมาย

เมื่อ deploy SC ไปบน network เรียบร้อยแล้ว อย่าลืมว่า Contract โจทย์ inherit มาจาก CtfFramework.sol อีกทีซึ่งโดยปกติมันจะบังคับว่า caller จะต้องมาจากเราเท่านั้น ให้ไป call ctf_challenge_add_authorized_sender() โดยใส่ address ของ SC ที่เพิ่ง deploy ลงไปให้โจทย์ข้อนี้อนุญาตให้ถูก call จาก adddress ของ SC เพิ่มได้นั่นเองครับ

จากนั้นก็ call pwn() เป็นอันเรียบร้อย ;D

Tips: อย่าลืมเพิ่ม gas limit นะครับ เพราะมันจะ recursive call หลายรอบ ทำให้ตัว MetaMask เองอาจจะประมาณ gas ที่ต้องใช้ออกมาได้ไม่ตรง จะกลายเป็นว่า transaction fail เพราะ out of gas

Lesson Learned

ข้อนี้คือช่องโหว่ Reentrancy Attack อย่างชัดเจน ดังนั้นวิธีแก้ไขคืออ้างอิงจาก https://swcregistry.io/docs/SWC-107 ได้เลย โดยผมจะสรุปคร่าว ๆ คือ

  • จัดการ internal state ต่าง ๆ ให้เรียบร้อยก่อนแล้วจึงจะ call ออกไป external
  • ใช้ ReentrancyGuard ของ OpenZeppelin กับฟังก์ชันที่ต้องการ make sure ว่าจะไม่สามารถ call ซ้อน ๆ หรือ re-entrancy ได้ ***แต่ว่าไม่ควรจะใช้มากเกินความจำเป็น เพราะจะกลายเป็นว่าผู้ใช้งานทั่วไปเองก็ต้องจ่าย gas fee มากขึ้นไปด้วย***

--

--

Teerasak R.

Just publish for fun on interests, not a full-time writer. I’m happy if you love my stories.