ตอนนี้กระแส blockchain, smart contract, DeFi, yield farming มาแรงมากๆ ถึงจุดพีคสุดๆ ใครๆก็เข้ามาฟาร์มกัน เอาเงินไปลงไว้ใน pool เพื่อเอา rewards ใน DeFi โครงการต่างๆ ที่เพิ่มจำนวนขึ้นอย่างมากมาย แต่!! เดี๋ยวก่อนนนนนน เราจะรู้ได้ยังไงว่า pool นั้นหรือ DeFi โครงการนั้นๆ เขาจะไม่โกงเรา หรือเอาเงินเราไป เพราะเงินออกจากกระเป๋าเราไปอยู่ใน pool เขาแล้วนะ!! เราอาจจะเคยได้ยินกันมาบ้างว่า เฮ้ยพี่ DeFi นี่มันเปลี่ยนโลกการเงินเลยนะ โลกการเงินยุคใหม่ไม่ต้องมีตัวกลางทางการเงินแล้ว ยุคนี้เขาไม่เชื่อคนกลางกันแล้ว เขาใช้ blockchain, smart contract ปลอดภัย code deploy ไปแล้วไม่มีใครแก้ได้ ยุคนี้เขาเชื่อใน code กันพี่ code is law ครับ แหมมม เท่จริงๆ 555+ นี่มันยุคทองของคนทำงานสาย IT โดยแท้เลย โชคดีที่ผมก็ทำงานสายนี้ 😀 ซึ่งที่น้องเขากล่าวมานั้นในทางเทคโนโลยีก็ถูกต้องครับ แต่ก็ต้องถามว่าแล้วได้อ่าน code นั้นหรือยังว่ามันทำงานอะไรบ้าง 555+ ซึ่งวันนี้ผมจะมา demo code ที่ hacker หรือ malicious dev แอบฝั่ง backdoor ไว้ในโครงการของตัวเอง เพื่อแก้เงื่อนไขหรือรัน functions หรือ contracts ใดๆก็ได้ โดยที่ไม่ต้องแก้ไข code ซึ่ง code ที่เราเห็น ที่เราเข้าใจกับ code ที่มันทำงานจริง มันอาจจะไม่เหมือนกัน แน่นอนว่า hacker ต้องพยายามซ่อน malicious code ให้เนียนที่สุดอยู่แล้ว ในตัวอย่างนี้ ผมจะเปลี่ยนเงื่อนไข code ในรูปจาก true เป็น false หรือในภาษาคนคือ จากที่ code บอกว่าเหรียญ Chick Token นี้จะไม่สามารถเสกเพิ่มได้อีก ให้เป็นเสกเพิ่มได้ มาดูกันครับ
หมายเหตุ: ในบทความนี้ผมจะยกตัวอย่างให้เข้าใจง่ายที่สุด และตัดส่วนที่ไม่จำเป็นออก เพื่อให้ผู้อ่านทั่วไปสามารถเข้าถึงได้ง่าย ซึ่งคำอธิบายมันอาจจะไม่ตรงกับ technically เบื้องหลังทั้งหมด เพื่อให้คนทั่วไปเข้าใจง่ายและแน่นอนว่า เวลา hacker เอาไปใช้จริงมันจะซับซ้อนกว่านี้มาก
เริ่มจากใน code มีแค่ 2 ส่วน
1. constructor() ในส่วนนี้ ถ้าอ่านผ่านๆดู ก็อาจจะงงๆ และคิดว่าไม่น่ามีอะไร เพรามีการแทรก inline assembly แค่ 2 บรรทัด 0x77, jump (แต่อันนี้แหละคือทีเด็ดเลย)
2. function finishMinting() ในส่วนนี้ก็เป็น public function เพื่อให้คนทั่วไปมา read smart contract ได้ว่า เหรียญตัวนี้มันยัง mint หรือเสกเหรียญเพิ่มได้อีกไหมซึ่งใน code มัน return true ซึ่งหมายความว่าเหรียญนี้จะไม่สามารถเสกเพิ่มได้อีกแล้ว คนทั่วไปที่มาอ่าน smart contract นี้ก็จะคิดว่าเหรียญนี้ supply ไม่มีเพิ่มแล้ว ราคาเหรียญน่าจะดีอะไรประมาณนั้น
ทีนี้ hacker หรือ malicious dev ที่ต้องการที่จะมาเปลี่ยนเงื่อนไขตรงนี้ให้เป็น false เขาจะทำยังไง ?
Tadaaaaaaa !!!!
hacker เขาก็จะใส่ malicious code หรือ payload เข้ามาทาง input ของ constructor() นั้นเอง (เวลาโปรแกรมที่เขียนในภาษา solidity ทำงานใน evm มันจะทำงานส่วน constructor() นี้ก่อนเพื่อเตรียม environment ภายใน evm และเรียบเรียง byte code ที่จะ execute)
แล้ว payload เราจะหน้าตาเป็นยังไง ? ตามรูปด้านล่างเลย
ซึ่งมันก็คือ function finishMinting() นั้นแหละแค่เปลี่ยนเป็น return false แต่เราต้องทำ contract Hacked นี้ให้เป็น opcode ก่อนซึ่งผมแปลงให้แล้ว ได้ opcode ตามนี้
0x608060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680637d64bcb4146044575b600080fd5b348015604f57600080fd5b5060566070565b604051808215151515815260200191505060405180910390f35b6000809050905600a165627a7a72305820329e250e20885dc970f44d0f3ac378ee92832325fbdf9245af9d91187b35a7e6002900000000000000000000000000000000000000000000000000000000
แต่เราจะเอา opcode นี้ไปใส่เป็น input ใน constructor() เลยไม่ได้นะต้องมีการเขียน assembly ใน payload เพิ่มอีกนิด
ย้อนกลับมาที่ code
assembly {
0x77
jump
}
เจ้า assembly code 2 บรรทัดนี้สรุปมันทำอะไร? สิ่งที่มันทำคือ
push 0x77 ลง stack แล้ว jump ไปที่ top of stack หรือ offset ที่ 0x77 นั้นเอง ท่านี้คุ้นๆไหม? classic hacking technique สอนกันมาเป็นสิบปี นี่ไงที่เขาว่า ภาษาเปลี่ยน platform เปลี่ยน technology เปลี่ยน แต่ fundamental เหมือนเดิม เพราะฉะนั้นพื้นฐานสำคัญ ในมหาลัยก็ควรตั้งใจเรียนนะครับน้องๆ 😀
มาต่อกันครับ ทีนี้ malicious dev รู้ล่วงหน้าอยู่แล้วว่า input ที่จะส่งเข้าไปใน construction() จะไปอยู่ในตำแหน่งที่ 0x77 แต่ยังขาด opcode อีกส่วนหนึ่งที่จะมาเชื่อมต่อให้ code ข้ามไปรัน payload ได้อย่างราบรื่นโดยไม่ error คือ 0x5b6100c060c7f3 <- opcode เพิ่มเติมในส่วนนี้คือ 5b กำหนดว่าจุดนี้เป็นจุดที่ถูกต้องที่ jump มา push size of payload ลง stack และ return ซึ่ง Ethereum VM (EVM) opcode นี้สามารถศึกษาได้เพิ่มเติมตาม link สำหรับสาย hardcore https://github.com/crytic/evm-opcodes
เราจะได้ final payload ตามนี้ 0x5b6100c060c7f3608060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680637d64bcb4146044575b600080fd5b348015604f57600080fd5b5060566070565b604051808215151515815260200191505060405180910390f35b6000809050905600a165627a7a72305820329e250e20885dc970f44d0f3ac378ee92832325fbdf9245af9d91187b35a7e6002900000000000000000000000000000000000000000000000000000000
ได้เวลาที่เราจะมา deploy smart contract พร้อม payload แล้วตามรูปด้านล่าง
หลังจาก deploy เสร็จก็มาลองกด read smart contract function finishMinting() ตามรูปด้านล่าง
ได้ผลเป็น false ทั้งที่ smart contract code เขียนเป็น true ตามรูปด้านล่าง
สำหรับ demo นี้ผมได้ปล่อย code ไว้ที่ https://github.com/MAYASEVEN/Sample-Blockchain-Smart-Contract-Backdoor
สามารถเข้าไปศึกษาและลองทำตามบทความนี้ได้เลยครับ โดย IDE, compiler และ debuger ผมใช้ https://remix.ethereum.org/ ทำงานบน web browser ได้เลย
สรุป บทความนี้ได้แชร์ 1 ในเทคนิคการโจมตี smart contract เพื่อเข้ามาเปลี่ยนแปลงการทำงานของ smart contract ซึ่งยังมีอีกหลายวิธี บทความนี้เพื่อการศึกษาเท่านั้นและแสดงให้เห็นถึงความเสี่ยง ที่อาจจะเกิดขึ้นได้ ไม่ว่าจะเป็น dev จงใจวาง backdoor หรือ smart contract เขียนมามีช่องโหว่ hacker เข้ามาเปลี่ยนแปลงการทำงาน จากที่มันควรจะเป็นได้ ทั้งนี้จะเห็นว่ามันไม่ง่ายเลยที่เราจะสามารถรีวิว smart contract code ด้วยตัวเราเองได้ การทำ smart contract audit จากองค์กรที่น่าเชื่อถือก็น่าจะสามารถลดความเสี่ยงลงมาได้แต่ไม่มีอะไร 100% ทั้งนี้ไม่อยากให้ตื่นตระหนกหรือประมาทจนเกินไป การลงทุนกับโครงการ DeFi ต่างๆควรศึกษาหลายๆองค์ประกอบ DYOR เยอะๆ เพราะบนโลก decentralized finance มันก็มีข้อเสีย คือเราต้องดูแลตัวเอง มีปัญหาขึ้นมา ก็ยากที่หน่วยงานไหนจะช่วยเราได้
ไอเดียบทความนี้ผมได้มาจากโจทย์แข่ง CTF สำหรับผู้ที่อยากศึกษาเพิ่มเติมผมแนบ link ไว้ให้อีก 2 คลิปครับ