[TH] Paradigm CTF 2022 Writeup

For English version, please visit: https://inspexco.medium.com/paradigm-ctf-2022-writeup-2ce290cd9287

ทีมงานของ Inspex ได้เข้าร่วมการแข่งขัน Paradigm CTF 2022 ในชื่อทีม _C0FFEE และคว้าอันดับที่ 15 ของโลกจากผู้เข้าแข่งขันทั้งหมด 445 ทีม ด้วยคะแนนรวม 2,465.1 คะแนน

โจทย์ในครั้งนี้ค่อนข้างหลากหลาย มีทั้งโจทย์ที่เป็น EVM, Solana, และ Cairo ซึ่งกำลังเป็นที่นิยมอยู่ในปัจจุบัน โดยทางทีมสามารถแก้โจทย์ได้ทั้งสิ้น 11 ข้อดังนี้:

· EVM

RANDOM
RESCUE
SOURCECODE
TRAPDOOOR
MERKLEDROP

· Solana

OTTER-WORLD
SOLHANA-1
SOLHANA-2 (Solved Locally)

· Cairo

RIDDLE-OF-THE-SPHINX
CAIRO-PROXY
CAIRO-AUCTION

สามารถดู write-up ของโจทย์แต่ละข้อที่ทางทีมแก้ได้ด้านล่างเลยครับ

EVM

RANDOM

CHALLENGE: RANDOM
AUTHOR: Riley Holterhus
TAGS: SANITY CHECK
DESCRIPTION: I’m thinking of a number between 4 and 4
ACCESS: nc 34.66.135.107 31337
RESOURCES:
https://github.com/paradigmxyz/paradigm-ctf-infrastructure
https://ctf.paradigm.xyz/resources/random.zip

สำหรับโจทย์ข้อนี้ เป็นโจทย์เบื้องต้นสำหรับ EVM โดยมีการให้ไฟล์มาอยู่ 2 contract ดังนี้

จากที่เห็นใน Setup contract ว่าตัวแปร solved ใน Random contract ต้องถูกเปลี่ยนให้เป็น true โดยวิธีการเปลี่ยนสามารถทำได้โดยการเรียก function solve() โดยส่ง parameter ที่มีค่าเหมือนกับผลลัพธ์ของ function _getRandomNumber() หรือก็คือ 4

จากการเชื่อมต่อไปที่ server ของโจทย์ เราจะได้รับ RPC URL, private key, และ address ของ Setup contract

ในการผ่านข้อนี้ เราจะต้องมี address ของ Random contract ที่สามารถหาได้จากการเรียก random() function ของ Setup contract โดยใช้ cast ดังนี้:

จาก output จะพบว่า Random contract อยู่ที่ address 0x31a91af1e135324f344f96757703aa11ff214e51 จากนั้นเราจะสามารถเรียก solve() function เพื่อผ่านโจทย์ข้อนี้

เมื่อ transaction ถูกส่งไปเรียบร้อย เราสามารถไปที่ server อีกครั้งเพื่อเอา flag มาได้เลย

Flag: PCTF{IT5_C7F_71M3}

RESCUE

CHALLENGE: RESCUE
AUTHOR: Riley Holterhus
TAGS: PWN
DESCRIPTION: I accidentally sent some WETH to a contract, can you help me?
ACCESS: nc 34.123.187.206 31337
RESOURCES:
https://github.com/paradigmxyz/paradigm-ctf-infrastructure
https://ctf.paradigm.xyz/resources/rescue.zip

จาก Setup contract จะเห็นว่า 10 $WETH ถูกส่งให้ MasterChefHelper contract ใน constructor โดยเป้าหมายของโจทย์ข้อนี้คือการทำให้ จำนวน $WETH ใน MasterChefHelper contract เป็น 0

MasterChefHelper contract มี function swapTokenForPoolToken() ที่จะรับเหรียญไปแบ่งครึ่งและ swap แต่ละกองเป็นแต่ละ token จากนั้นจึงนำ token เหล่านั้นมาเพิ่มเป็น liquidity

เราจะเห็นว่าเมื่อ function _addLiquidity() นั้นถูกเรียก มันจะส่ง token ทั้งสองไปเท่าจำนวนทั้งหมดที่ MasterChefHelper contract ถืออยู่เพื่อคำนวณจำนวน LP ที่จะได้รับ ดังนั้นเราสามารถใช้กระบวนการเพิ่ม liquidity นี้โดยให้ token ฝั่งนึงเป็น $WETH ก็จะสามารถดึง $WETH ที่ติดใน contract มาใช้งานได้

อย่างไรก็ตามในการ swap token ตั้งต้นให้เป็น 2 token จะยังมีเงื่อนไขที่ต้องคำนึงถึงอยู่ดังนี้:

  1. หนึ่งใน 2 token ปลายทางจะต้องเป็น $WETH เพื่อจะดึง $WETH ที่ค้างอยู่
  2. ชนิดของ tokenIn จะต้องเป็น token อื่นนอกเหนือจาก 2 token ปลายทางเพราะ router ไม่รองรับการ swap เป็น token เดียวกัน
  3. จะต้องมี liquidity เพียงพอต่อการ swap ทั้งจาก tokenIn เป็น tokenOut0 และจาก tokenIn เป็น tokenOut1

เพื่อหาค่า tokenIn และ poolId ที่ต้องการ เราจึงสร้าง instance และทำการเขียน web3 script เพื่อดึงข้อมูลแต่ละ pool ที่มีอยู่ออกมา

ผลลัพธ์จากการรัน script ข้างต้นได้ดังนี้

พบว่า token 0x5dbcF33D8c2E976c6b560249878e6F1491Bca25c สามารถ swap เป็น $WETH (0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2) ได้ ผ่าน pool id 25, และยังสามารถ swap เป็น token 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 ผ่าน pool id 26.

แล้วยังพบว่า pool id 21 นั้นเป็นคู่ token 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 และ 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2

ตัว token 0x5dbcF33D8c2E976c6b560249878e6F1491Bca25c นั้นสามารถเป็น tokenIn ได้ตรงตามที่ต้องการ โดยจะใช้ ค่า poolId เป็น 21

เพื่อจะนำ $WETH ทั้งหมดออกจาก MasterChefHelper contract นั้น ก่อนที่ function _addLiquidity() จะถูกเรียก เราจะต้องทำให้ contract มีจำนวน token เป็นอัตราส่วนเดียวกับ liquidity pool หรือจะให้มีสัดส่วนของ token 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 มากกว่า เพื่อให้ $WETH ทั้งหมดถูกส่งไป liquidity pool

ดังนั้นเราจึงเขียน contract เพื่อช่วยในการทำขั้นตอนเหล่านี้

  1. ฝาก 11 $ETH เพื่อรับ 11$WETH
  2. ซื้อ token 0x5dbcF33D8c2E976c6b560249878e6F1491Bca25c ด้วย 0.1 $WETH
  3. ซื้อ token 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 ด้วย 10.9 $WETH
  4. ส่ง token 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 ไปที่ MasterChefHelper contract เพื่อเพิ่มอัตราส่วนของ token ให้มากขึ้น
  5. เรียก function swapTokenForPoolToken() ใน MasterChefHelper contract

ใช้ forge เพื่อ deploy contract

ส่ง 11 $ETH ไปให้ contract ที่ deploy แล้วเรียก function solve()

จากนั้นเชื่อมต่อไปยัง server เพื่อรับ flag

Flag: PCTF{MuCH_4PPr3C1473_53r}

SOURCECODE

CHALLENGE: SOURCECODE
DESCRIPTION: Fixed point EVM bytecode
ACCESS: nc 34.136.156.228 31337
RESOURCES:
https://github.com/paradigmxyz/paradigm-ctf-infrastructure
https://ctf.paradigm.xyz/resources/sourcecode.zip

สำหรับข้อนี้ เราจะต้องเขียน smart contract ที่ส่ง output ออกมาเป็น bytecode ของ smart contract นั้นเอง โดยห้ามใช้ opcode ที่ถูกระบุไว้ใน safe() function

ในตอนแรก ทางทีมตั้งใจเขียน smart contract ขึ้นมาเพื่อ return ส่วนของ code โดยใช้ CODECOPY opcode

แต่ก็พบว่าไม่สามารถทำได้ เพราะ CODESIZE (0x38) และ CODECOPY (0x39) opcode ถูกห้ามไว้ใน safe() function.

ทางทีมรู้ว่าโปรแกรมที่ return ตัวเองออกมาเป็น output นั้นเรียกว่า “quine” เมื่อทดลองหาดู จึงเจอ quine ของ EVM ที่มีคนเคยเขียนไว้แล้ว ชื่อว่า quine.etk

Ref: https://gist.github.com/karmacoma-eth/220b58b7cd32d649fa1a15e70b6d8bff

แต่ว่า quine.etk มีการใช้ CALLVALUE opcode ที่ถูกห้ามไว้

ทางทีมเลยใช้แนวคิดเดียวกัน และดัดแปลง smart contract โดยเปลี่ยน callvalue opcode เป็น opcode อื่นเพื่อจุดประสงค์เดียวกัน คือการ PUSH ค่า 0 ขึ้นไปบน stack จากนั้นทำการ pad byte ที่เหลือด้วย STOP opcode

จากนั้นจึงเปลี่ยน PUSH16 เป็น PUSH32 ให้พอดีกับขนาดของ bytecode ของ dupper.bc

เมื่อ compile เสร็จ เราก็จะได้ bytecode ออกมาในรูปแบบ hex

เชื่อมต่อไปที่ server เพื่อเริ่มต้น instance ของ โจทย์

จากนั้นเรียก challenge() function เพื่อดู address ของ Challenge contract

เมื่อได้ address มา เราก็จะสามารถเรียก solve() function โดยใช้ bytecode ที่เราสร้างขึ้นมาเป็น parameter ได้เลย

หลังจาก transaction ถูกส่งไปเรียบร้อย เราก็สามารถเชื่อมต่อไปที่ server อีกครั้งเพื่อเอา flag

Flag: PCTF{QUiNE_QuiNe_qU1n3}

TRAPDOOOR

CHALLENGE: TRAPDOOOR
AUTHOR: samczsun
TAGS: PWN
DESCRIPTION: In theoretical computer science and cryptography, a trapdoor function is a function that is easy to compute in one direction, yet difficult to compute in the opposite direction (finding its inverse) without special information, called the “trapdoor”.
ACCESS: nc 35.68.217.8 31337
RESOURCES:
https://github.com/paradigmxyz/paradigm-ctf-infrastructure
https://ctf.paradigm.xyz/resources/trapdooor.zip

โจทย์ในข้อนี้ ต้องการให้เราเขียน smart contract เพื่อหาตัวประกอบจำนวนเฉพาะของเลขที่สุ่มขึ้นมาให้ถูกต้อง โดยเราสามารถส่ง bytecode ไปที่ server และ server จะทำการตรวจสอบผลลัพธ์โดยใช้ forge

ทางทีมทราบว่าใน forge สามารถใช้คำสั่งพิเศษที่เรียกว่า cheatcodes (https://book.getfoundry.sh/forge/cheatcodes) ได้ เราเลยเขียน smart contract ที่ใช้ฟังก์ชั่น envString() เพื่อดึงค่า FLAG environment variable ออกมา

โดยเราจะเห็น output จาก server เป็นตัวเลขที่ได้จากคำตอบของการหาตัวประกอบเท่านั้น เราเลยใช้วิธีแบ่งค่าของ FLAG ออกเป็นสองส่วน และ encode ไว้ในค่าของตัวประกอบทั้งสองตัว

ทำการ compile ตัว contract ที่เขียนขึ้นมา และรันเพื่อให้ได้ runtime bytecode

จากนั้นจึงเชื่อมต่อไปยัง server ของโจทย์ และส่ง runtime bytecode ไป

จากผลลัพธ์ จะเห็นว่าเราไม่ได้แยกตัวประกอบได้อย่างถูกต้อง แต่ output ที่ส่งออกมานั้นคือ bytes ของ flag ที่เรา encode ไว้ เราสามารถใช้ Python เพื่อ decode ค่ากลับมาเป็น string ได้ดังนี้

Flag: PCTF{d0n7_y0u_10v3_f1nd1n9_0d4y5_1n_4_c7f}

MERKLEDROP

CHALLENGE: MERKLEDROP
AUTHOR: Riley Holterhus
TAGS: PWN
DESCRIPTION: Were you whitelisted?
ACCESS: nc 35.188.148.32 31337
RESOURCES:
https://github.com/paradigmxyz/paradigm-ctf-infrastructure
https://ctf.paradigm.xyz/resources/merkledrop.zip

โจทย์ข้อนี้เป็นข้อเกี่ยวกับการแจก airdrop โดยใช้ Merkle tree ในการตรวจสอบผู้รับ airdrop

File structure

  • contracts/Setup.sol จะมี 2 contract คือ Token และ Setup ที่ไว้ใช้ deploy MerkleDistributor contract และมี function isSolved() ไว้ตรวจผลของโจย์
  • contracts/MerkleDistributor.sol จะเป็น contract ที่ถือ airdrop reward และมี function claim() ไว้แจกจ่าย airdrop
  • contracts/MerkleProof.sol เป็น Library ที่มี function verify() ไว้เพื่อตรวจสอบความถูกต้องของ Merkle tree.
  • tree.json เป็น list ของ address, claim amount และ index ของผู้ที่ได้รับ airdrop ใน Merkle tree

เป้าหมายของโจทย์นี้คือการดึง airdrop reward ทั้งหมดออกจาก contract โดยที่ไม่ได้มาจากการ claim airdrop ทั้งหมด (ด้วยวิธีปกติ)

ใน MerkleProof contract จะมีแค่ verify() function ตามปกติซึ่งไว้ใช้เพื่อตรวจสอบความถูกต้องของข้อมูลที่ส่งเข้ามา

มีจุดที่น่าสนใจใน MerkleDistributor contract โดย function claim() นั้นรับ index, address, amount, และ array ของ merkleProof เพื่อทำการ claim airdrop และจะโอน reward ไปให้ address ที่ใส่เข้ามาเท่ากับ amount ที่ใส่เข้ามา

โดยตอนนี้เราสามารถผ่านเงื่อนไขแรกได้ด้วยข้อมูลจากไฟล์ tree.json แต่ยังติดเงื่อนไขที่สอง

ถ้าหากลองดูให้ดี จะเห็นว่า MerkleDistributor contract และ MerkleProof contract นั้นมีการใช้งาน function keccak256() ที่ไม่เหมือนกัน

โจทย์มีการใช้ function abi.encodePacked() ในทั้ง MerkleDistributor contract และ MerkleProof contract

ใน MerkleDistributor contract function abi.encodePacked() นั้นรับ uint256(index), address(account), และ uint96(amount) โดยผลลัพธ์จะมีขนาด = 32 bytes(uint256) + 20 bytes (address) + 12 bytes (uint96) = 64 bytes

ทำนองเดียวกันกับ MerkleProof contract ตัวผลลัพธ์ของ function abi.encodePacked() จะมีขนาด 32 bytes (byte32) + 32 bytes (byte32) = 64 bytes ซึ่งผลลัพธ์ของทั้งคู่นั้น “บังเอิญ(?)” เท่ากันพอดี

ดังนั้นเราสามารถใช้ proof ที่ต่อกัน (ผลลัพธ์จาก abi.encodePacked() ใน MerkleProof) แทน leaf ได้ โดย จะแบ่งเป็น uint256(index), address(account) และ uint96(amount) และใช้ในการเรียก function claim() เพราะใน function verify() นั้นไม่สามารถแยกระหว่าง leaf หรือ node ได้ ทำให้ผ่านการตรวจได้

หากเราสามารถหา node ที่มีค่า index, address, และ amount ที่อยู่ใน Merkle tree และ มีค่า amount รวมกับ claim อื่น ๆ ที่เหลือแล้วเท่ากับ reward ทั้งหมดพอดี ก็จะสามารถผ่านข้อนี้ได้

เราจึงเขียนโปรแกรมด้วยภาษา Go เพื่อหา node ที่เข้ากันกับเงื่อนไขทั้งหมดข้างต้น

หลังจากรันโปรแกรมก็ได้ผลลัพธ์ที่ต้องการ เราจะต้องทำการ claim 2 รอบ โดยในครั้งแรกจะต้อง claim ด้วยค่าต่อไปนี้

  • index: 0xd43194becc149ad7bf6db88a0ae8a6622e369b3367ba2cc97ba1ea28c407c442
  • account: 0x000000000000000000000000d48451c19959e2d9bd4e620fbe88aa5f6f7ea72a
  • amount: 0xf40f0c122ae08d2207b
  • proofs: [0x8920c10a5317ecff2d0de2150d5d18f01cb53a377f4c29a9656785a22a680d1d, 0xc999b0a9763c737361256ccc81801b6f759e725e115e4a10aa07e63d27033fde, 0x842f0da95edb7b8dca299f71c33d4e4ecbb37c2301220f6e17eef76c5f386813, 0x0e3089bffdef8d325761bd4711d7c59b18553f14d84116aecb9098bba3c0a20c, 0x5271d2d8f9a3cc8d6fd02bfb11720e1c518a3bb08e7110d6bf7558764a8da1c5]

ค่า index จะเป็นค่าของ proof แรก 32 bytes ส่วนค่า account จะเป็น 20 bytes แรกของ proof ที่สอง และค่า amount จะเป็น 12 bytes ที่เหลือของ proof ที่สอง

ภาพนี้แสดงถึงการทำ Merkle tree verification เมื่อทำการ claim ที่ index 37

ส่วนภาพต่อไปนี้แสดงถึงการทำ Merkle tree verification เมื่อทำการ claim โดยใช้ 0xd43194becc149ad7bf6db88a0ae8a6622e369b3367ba2cc97ba1ea28c407c442 เป็นค่า index, ใช้ 0xd48451c19959e2d9bd4e620fbe88aa5f6f7ea72a เป็นค่า account และใช้ 0xf40f0c122ae08d2207b เป็นค่า amount

โดยค่าที่ได้จากการคำนวณ node จะเท่ากับ 0x225de26d438b50d8... เหมือนเดิม ซึ่งจะยังคงถูกต้องเหมือนในภาพก่อนหน้า

เมื่อเจอค่าที่ต้องการแล้วเราก็นำมาเขียน contract เพื่อทำการแก้โจทย์ข้อนี้

เชื่อมต่อไป server เพื่อ สร้าง instance

จากนั้น deploy Exploit contract

เรียก function merkleDistributor() เพื่อหา address ของ MerkleDistributor contract ที่ถูก deploy

ขั้นต้อนสุดท้ายเรียก function exploit() พร้อมกับ address ของ MerkleDistributor contract

เชื่อมต่อ server อีกครั้งเพื่อรับ flag

Flag: PCTF{N1C3_Pr00F_8r0}

Solana

OTTER-WORLD

CHALLENGE: OTTER-WORLD
AUTHOR: NotDeGhost
TAGS: SANITY-CHECK
DESCRIPTION: Otter World!
ACCESS: nc 34.72.24.70 8080
RESOURCES:
https://github.com/paradigmxyz/paradigm-ctf-infrastructure
https://ctf.paradigm.xyz/resources/otter-world.tar.gz

โจทย์ข้อนี้เป็นโจทย์ทดสอบเบื้องต้นสำหรับ Solana

เป้าหมายของข้อนี้คือการเรียก get_flag() function ด้วยค่า magic parameter ที่ถูกต้องซึ่งก็คือ 0x1337 * 0x7331

โดยในโจทย์มีการให้โครงสำหรับการเขียน script เพื่อแก้โจทย์ไว้เรียบร้อยแล้วดังนี้

ตามที่มีโน้ตไว้ว่า /* TODO */ ที่บรรทัดที่ 22 เราสามารถเติม * 0x7331 เข้าไปเพื่อให้ตรงกับค่าของ magic ที่ต้องการได้เลย

หลังจากนั้น เราต้องไปแก้ไข IP address ของ server ที่เราจะไปเชื่อมต่อด้วย

และเมื่อรัน script ก็จะได้รับ flag กลับมา

Flag: PCTF{0tt3r_w0r1d_8c01j3}

SOLHANA-1

CHALLENGE: SOLHANA-1
AUTHOR: hana (@dumbcontract2)
TAGS: PWN
DESCRIPTION: theres a brand new ponzi scheme in town that lets people deposit and withdraw their bitcoin. why would they want to do this? no one knows
ACCESS: nc 35.188.83.0 3000
RESOURCES:
https://github.com/paradigmxyz/paradigm-ctf-infrastructure
https://ctf.paradigm.xyz/resources/solhana-ctf.tar.gz

โจทย์ข้อนี้มีการให้ source code ของ server ที่ตรวจสอบ account state เพื่อเช็คว่าสามารถแก้โจทย์ได้ถูกต้องหรือไม่ โดยจะส่ง flag กลับมาเมื่อ account state ตรงตามเงื่อนไข

File Structure

  • chain/: source code ของ Solana program ที่ใช้เป็นโจทย์ เราสามารถอ่านและวิเคราะห์ช่องโหว่ของโจทย์ได้จากไฟล์ในนี้
  • client/: โครงสำหรับการเขียน script เพื่อเชื่อมต่อไปที่ server และทำการโจมตี
  • elf/: ไฟล์โจทย์ Solana program ในรูปแบบ BPF binary ที่ถูก compile แล้ว สามารถใช้เพื่อทดสอบในฝั่ง local ได้โดยใช้ solana-test-validator
  • server/: ไฟล์ server ของโจทย์ที่ทำหน้าที่ setup ตัวโจทย์

ข้อ SOLHANA-1 นี้คือ platform ที่เปิดให้ user สามารถมาฝากและถอน token ได้

โดยเงื่อนไขที่จะผ่านข้อนี้ได้คือต้องทำให้ balance ของ deposit_account กลายเป็น 0

ซึ่งในตอนแรก Satoshi จะ mint 10 Bitcoin และฝาก (ผ่าน deposit() function) ไปยัง deposit_account ดังนั้นเราจึงต้องเอา Bitcoin ส่วนนี้ออกจาก account ให้ได้

เราลองมาดูที่ Solana program ของโจทย์ว่า user สามารถทำอะไรได้บ้าง

  • setup_for_player() — สร้าง state account ที่เก็บข้อมูลสำหรับ player
  • deposit() — ฝาก token จาก depositor account ไปยัง deposit account และ mint (สร้าง) voucher token ให้กับ depositor_voucher account.
  • withdraw() — ถอน token ที่ฝากไว้จาก deposit account กลับมาที่ depositor account และ burn (ทำลาย) voucher token จาก depositor_voucher account.

โดยเราจะพอเดาได้ว่าการที่จะผ่านข้อนี้น่าจะต้องเล่นกับ deposit() และ withdraw() function เนื่องจาก 2 functions นี้จะทำให้ balance ของ account เปลี่ยนแปลงได้

โดยใน withdraw() function นั้นเราสามารถที่จะถอน Bitcoin ของออกไปให้คนอื่นได้ (ทำให้ balance เป็น 0) แต่ว่าจะมี requirement คือ จะต้องมี voucher token ใน depositor_voucher_account ก่อนเพื่อใช้สำหรับ burn ซึ่ง ณ ตอนนี้ player ไม่มี voucher token ใน account เลย

แล้วถ้าเกิดว่า เราสร้าง voucher ปลอมขึ้นมาล่ะ จะสามารถใช้ withdraw ได้่มั้ย เราลองมาดูที่ code ในส่วนที่ตรวจสอบ account กันว่า attack scenario ของเราสามารถทำได้หรือไม่

จาก code จะเห็นว่า ตัวโปรแกรมตรวจสอบแค่ว่า voucher_mint account มี mint::authority = state และ token::decimals = deposit_mint.decimals หรือไม่

ดังนั้นเราสามารถโจมตีได้ด้วยขั้นตอนต่อไปนี้

  1. สร้าง voucher program ปลอมขึ้นมาที่มีจำนวนทษนิยมเท่ากับ Bitcoin (6 decimals)
  2. Mint อย่างน้อย 10 voucher ให้กับ player เพื่อใช้ในการ burn ระหว่างการถอน
  3. โอน authority ของ account ไปให้ state account
  4. ถอนเงินจาก deposit_account account โดยใช้ voucher ปลอมที่สร้างขึ้นมา

โดย script การโจมตีที่ทางทีมใช้เพื่อผ่านโจทย์เป็นดังนี้

เมื่อรัน script ก็จะได้ flag ออกมา

Flag: PCTF{unsafe_voucher_vouched_vouchsafely}

SOLHANA-2

CHALLENGE: SOLHANA-2
AUTHOR: hana (@dumbcontract2)
TAGS: PWN
DESCRIPTION: theres a brand new ponzi scheme in town that lets people deposit and withdraw their ethereum. why would they want to do this? no one knows
ACCESS: nc 35.188.83.0 3000
RESOURCES:
https://github.com/paradigmxyz/paradigm-ctf-infrastructure
https://ctf.paradigm.xyz/resources/solhana-ctf.tar.gz

ในโจทย์ SOLHANA-2 นั้นจะเหมือนกับข้อ SOLHANA-1 เพิ่มเติมคือมี liquidity pool และ function swap แบบ AMM

เราได้แก้โจทย์ข้อนี้ผ่านการจำลองใน local แต่ด้วยเหตุผลบางอย่างทำให้ไม่สามารถแก้แบบเดียวกันเพื่อรับ flag จากฝั่ง server ได้ ทั้งนี้รายละเอียดต่อไปนี้จึงอาจจะไม่ตรงกับ intended solution แต่เราหวังว่ามันจะช่วยให้คุณเข้าใจโจทย์ข้อนี้มากขึ้น

เงื่อนไขในการผ่านข้อนี้คือต้องทำให้จำนวนรวม token ของทั้ง 3 pool จะต้องน้อยกว่าหรือเท่ากับ 150_000_000

โจทย์จะเริ่มต้นมาด้วย 3 liquidity token pool ไว้สำหรับทำการ swap โดยประกอบไปด้วย wo_eth (8 decimals), so_eth (6 decimals), and st_eth (8 decimals) และให้จำนวน token มา 100_000_000 ในแต่ละ pool account โดยสำหรับ player อย่างเราจะได้แค่ 1_000 ในแต่ละ account

เรามาเริ่มที่ function add_pool() และ swap()

ใน function add_pool() จะเพิ่ม pool ใหม่เข้าไปใน pool array ของ state account โดยไม่ได้ตรวจสอบว่าจะทับของเก่าที่มีอยู่หรือไม่ นี่อาจจะเป็นจุดที่ทำให้เกิดปัญหาในข้อนี้ก็เป็นได้

ส่วนใน function swap() นั้น ระหว่างการ swap token จะมีการ validate ตัว token amount ในกรณีที่ input และ output token มีจำนวน decimal ต่างกัน และ pool account ที่ส่งเข้ามาจะต้องมีอยู่ใน pool array ของ state account จากนั้นจะโอน input token ไปที่ pool account แล้วจึงโอน output token จาก pool account กลับไปให้ user

อ่านมาถึงตรงนี้ก็สงสัยขึ้นมาว่า ถ้าเราเรียก function add_pool() เพื่อทำการ add fake token (8 decimals) เข้าไปใน pool array ของ state account ซึ่งจะส่งผลให้เราสามารถที่จะ swap fake token เป็น token อื่นได้ โดยเราจะต้อง mint fake token ให้ player ด้วยเพื่อนที่จะมี balance ไป swap

จากนั้นก็เรียก function swap() เพื่อ swap fake token ของเราเป็น token อื่นทั้งหมด ในที่นี้จะเป็น token wo_eth และ token st_eth ซึ่งจะส่งผลให้เราดึงจำนวน token ทั้งหมด ออกจาก pool account ดังนั้น ยอดจำนวนรวม token ในแต่ละ pool จะเป็น 0 (wo_eth), 100_000_000 (so_eth), 0 (st_eth) และผลรวมจะมีค่าเท่ากับ 100_000_000 ซึ่งน้อยกว่า 150_000_000 ตรงตามที่โจทย์ต้องการ

เราเขียน script ด้านล่างเพื่อแก้โจทย์ข้อนี้และทดสอบบน local

Cairo

RIDDLE-OF-THE-SPHINX

CHALLENGE: RIDDLE-OF-THE-SPHINX
DESCRIPTION: What walks on four legs in the morning, two legs in the afternoon, three legs in the evening, and no legs at night?
ACCESS: nc 35.193.19.12 31337
RESOURCES:
https://github.com/paradigmxyz/paradigm-ctf-infrastructure
https://ctf.paradigm.xyz/resources/riddle-of-the-sphinx.zip

โจทย์ข้อนี้เป็นโจทย์ทดสอบเบื้องต้นสำหรับ Cairo (Starknet) โดยจะมีไฟล์ riddle.cairo และ chal.py มาให้ดังต่อไปนี้

โดยการที่จะผ่านข้อนี้ไป เราจะต้องเซ็ตค่า _solution ให้เป็น “man” (0x6d616e) ตามที่มีการเช็คใน function checker ใน chal.py

เมื่อรู้เป้าหมายของโจทย์แล้วเราจึงทำการสร้าง instance ของโจทย์และเขียน script โดยในที่นี้เราจะใช้ “starknet.py” เพื่อเรียก function solve() ด้วยค่า 0x6d616e

หลังจากรัน script แล้วเราก็จะสามารถ connect กลับไปที่ server อีกครั้งเพื่อรับ flag ของข้อนี้ได้

Flag: PCTF{600D_1UCK_H4V3_FUN}

CAIRO-PROXY

CHALLENGE: CAIRO-PROXY
DESCRIPTION: Just a simple proxy contract
ACCESS: nc 35.226.167.223 31337
RESOURCES:
https://github.com/paradigmxyz/paradigm-ctf-infrastructure
https://ctf.paradigm.xyz/resources/cairo-proxy.zip

โจทย์ข้อนี้จะมีไฟล์ Cairo ทั้งหมด 3 ไฟล์มาให้ ประกอบไปด้วย:

  • almost_erc20.cairo: ERC20 token implement ด้วย Cairo
  • proxy.cairo: proxy contract ใช้เพื่อเรียก function ใน implementation contract ผ่าน fallback function โดย state ต่าง ๆ จะถูกเก็บใน proxy contract
  • utils.cairo: utility contract ที่มี function ในการเขียนและอ่าน storage โดยตรง

เมื่อเราลอง compile proxy.cairo contract แล้วจะเห็นว่า function auth_write_storage() นั้นได้ถูก import เข้ามาด้วยจาก utils.cairo contract ด้วย ทำให้เรา สามารถเขียนข้อมูลลงไปใน storage ไหนก็ได้

จากไฟล์ chal.py จะเห็นว่าเป้าหมายของเราคือการทำให้ balance ของ player เท่ากับ 50000e18

จากนั้น ทำการสร้าง instance และ เขียน script เรียก function auth_write_storage() เพื่อทำการเซ็ตค่า balance ของ player ให้เป็น 50000e18

หลังจากรัน script แล้วก็จะสามารถเชื่อมต่อเพื่อเข้าไปรับ flag ของข้อนี้ได้

Flag: PCTF{d3f4u17_pu811c_5721k35_4941n}

CAIRO-AUCTION

CHALLENGE: CAIRO-AUCTION
AUTHOR: BitBaseBit, and Mauricio Perdomo
TAGS: PWN
DESCRIPTION: Just a simple auction contract
ACCESS: nc 34.132.254.35 31337
RESOURCES:
https://github.com/paradigmxyz/paradigm-ctf-infrastructure
https://ctf.paradigm.xyz/resources/cairo-auction.zip

โจทย์ข้อนี้จะเป็นการจำลอง auction platform ด้วย Cairo โดยเป้าหมายคือเป็นผู้ชนะการประมูลให้ได้ จากไฟล์ chal.py จะเห็นได้ว่ามีการจำลองผู้เข้าร่วมประมูลคนอื่นอีก 2 คนที่เสนอราคาที่ 100000e6 tokens ในขณะที่โจทย์ได้เตรียมไว้ให้เราแค่ 50000e6 tokens

จะเป็นผู้ชนะการประมูลได้จะต้องเรียก function raise_bid() เพื่อให้ new_balance มีค่ามากกว่า winning_bid ปัจจุบัน

ในขณะที่ function unlock_funds() มีไว้เพื่อปลดล็อคเงินที่ใช้ประมูล โดยไปจะหักลบจำนวนในกระเป๋าประมูลหรือค่า _auctionBalances

แต่ว่า Uint256 ใน Cairo คือ struct ของ felt 2 ตัว (https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/cairo/common/uint256.cairo#L9-L14) โดยค่า felt ใน Cairo เป็นค่า Integer ที่สามารถ เป็นค่าติดลบได้หากไม่มีการตรวจสอบให้ดี

หากเราทำการเรียก function unlock_funds() โดยใส่ amount เป็นค่าติดลบ -100000000001 เพื่อที่จะเพิ่มค่า _auctionBalances (ลบด้วยค่าติดลบจะเป็นการบวก) ให้มากกว่า winning_bid ปัจจุบัน
หลังจากนั้นก็ทำการเรียก function raise_bid() ด้วยค่า 0 จะเป็นการเสนอเงินเพิ่ม 0 แต่ในเมื่อค่า _auctionBalances ของเราได้เพิ่มเป็น 100000000001 ไปแล้วจะทำให้เราชนะการประมูลได้

เมื่อรู้ดังนี้เราจึงสร้าง instance และทำการเขียน script ตามที่คิดไว้โดยค่า -100000000001 จะมีค่าเท่ากับ max felt (2**251+17*2**192+1) ลบด้วย 100000000001 แล้วเซ็ตไปใน low bit ของ Uint256 amount

Flag: PCTF{y0u_7h0u9h7_17_w45_4_p21m171v3_7yp3_8u7_17_w45_m3_d10}

About Inspex

Inspex is formed by a team of cybersecurity experts highly experienced in various fields of cybersecurity. We provide blockchain and smart contract professional services at the highest quality to enhance the security of our clients and the overall blockchain ecosystem.

For any business inquiries, please contact us via Twitter, Telegram, contact@inspex.co

--

--

Cybersecurity professional service, specialized in blockchain and smart contract auditing https://twitter.com/InspexCo

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Inspex

Cybersecurity professional service, specialized in blockchain and smart contract auditing https://twitter.com/InspexCo