Risk Warning: Beware of illegal fundraising in the name of 'virtual currency' and 'blockchain'. — Five departments including the Banking and Insurance Regulatory Commission
Information
Discover
Search
Login
简中
繁中
English
日本語
한국어
ภาษาไทย
Tiếng Việt
BTC
ETH
HTX
SOL
BNB
View Market
Paradigm CTF 2023解题报告
Salus Insights
特邀专栏作者
2024-02-16 10:53
This article is about 10548 words, reading the full article takes about 16 minutes
Salus安全团队在Paradigm 2023 CTF中共解决了13项挑战,在1011支队伍中以3645.60的分数获得第九名,并受邀成为Paradigm CTF 2024的客座作者。在这篇博文中,我们将介绍我们在比赛期间解决的所有挑战。

Paradigm CTF It is the top and most well-known online competition for smart contract hackers in the blockchain industry. It is organized by Paradigm, a top investment company in web3. The CTF topic consists of multiple challenges created by Sumczsun and invited guest authors. The goal of each challenge is to hack or attack a technical problem to solve it.

During the competition, contestants will complete a series of software puzzle challenges. Points will be awarded for each challenge that a participant solves correctly or obtains the highest score before the challenge period ends. For the King of the Hill Challenge, scoring will be based on the Elo scoring system. The number of points each participant will receive for correctly solving the challenge will not be known until after the challenge period ends.

The Salus security team solved a total of 13 challenges and finished ninth out of 1,011 teams with a score of 3645.60.And was invited to be a guest author at Paradigm CTF 2024.In this blog post we will cover all the challenges we solved during the competition.

Challenges solved

  • Hello World

  • Black Sheep

  • 100% 

  • Dai++

  • DoDont

  • Grains of Sand

  • Suspicious Charity

  • Token Locker

  • Skill Based Game

  • Enterprise Blockchain

  • Dragon Tyrant

  • Hopping Into Place

  • Oven

1. Hello World

The goal of this challenge is to ensure that the target address has at least 13.37 more ETH balances than before.

We created two contracts: a test contract SolveTest and a contract to perform operations Solve. The SolveTest contract verifies that the challenge has been solved by setting up an initial environment and executing a test attack.The Solve contract transfers funds to the target address through the selfdestruct operation in the killMySelf() function, thereby achieving the purpose of increasing the ETH balance of the target address.

2. Black Sheep

The goal of this challenge is to withdraw all ETH from the BANK contract. The vulnerability exists in the WITHDRAW() function. Because the CHECKSIG() function does not handle the return value correctly, in some cases it ends execution directly without pushing any value onto the stack, causing the return value to be incorrectly read as CHECKVALUE(). Results of the. Our solution is to write a Solver contract that exploits the vulnerability of the WITHDRAW() function and ensures that CHECKVALUE() returns 0, so that the WITHDRAW() function executes successfully and extracts all ETH from the BANK contract.

Vulnerability analysis

We studied the WITHDRAW() function, which first executes the CHECKVALUE() and CHECKSIG() functions in sequence, and then sends all the ETH of the contract to msg.sender based on the execution results. in,The CHECKSIG() function does not handle function return values ​​correctly.This function needs to push a result onto the stack as the return value before ending the function execution. However, in some cases, the function ends execution directly without pushing any value onto the stack, causing the return value to be mistakenly read as the first element on the top of the stack, which is the execution result of the CHECKVALUE() function. Due to a design flaw in the CHECKSIG() function, even if signature verification fails, the WITHDRAW() function can be made to succeed by ensuring that the CHECKVALUE() function returns 0.

In the CHECKSIG() function, call the WITHDRAW() function using the input parameters (bytes 32, uint 8, bytes 32, bytes 32) to call address 0x 1. This contract is a precompiled contract whose function is to recover the public key address based on parameters. There are two checks here. The first is to check if the signature is valid. If the staticcall executes successfully, it means the signature is valid, so the contents of the input parameters are not important. The correctness of the public key is checked in the second. If the public key address is incorrect, it does not fall back but jumps directly to the end of the function. This function has a return value, and according to normal execution, a result needs to be pushed onto the stack as the return value before ending function execution.

However, if execution ends directly, no value is pushed onto the stack. This will cause the return value to be incorrectly read as the first element on the top of the stack, which is the result of CHECKVALUE().Therefore, as long as the execution result of the CHECKVALUE() function returns 0, the WITHDRAW() function can execute smoothly and successfully send 10 ETH to msg.sender.

We hope that the execution result of the CHECKVALUE() function is 0, that is, the top element of the stack is 0. We only need to satisfy 0x 10 > callvalue to make the call operation fail.

solution

We wrote the Solver contract to withdraw money from the Bank contract. The ETH in the Bank contract is sent to the Solver contract through the call operation in the WITHDRAW() function. The specific process is as follows:

  1. In the solve() function in the Solver contract, call the WITHDRAW() function of the Bank contract to initiate the withdrawal operation.

  2. In the WITHDRAW() function, the CHECKVALUE() function is executed first.Since our callvalue is 5 wei (less than 0x 10), it will jump to the over label.

  3. In the over tag, callvalue * 2 (that is, 10 wei) will be sent to the caller (that is, the Solver contract). Because in the fallback function of the Solver contract,If the amount of Ether received is equal to 10 wei, the transaction will be rolled back, so the call operation in the over tag will fail and the CHECKVALUE() function returns 0.

  4. WITHDRAW() function continues execution,Send the entire balance of the Bank contract to the caller (that is, the Solver contract). This is achieved through the line of code selfbalance caller gas call,Among them, selfbalance is the balance of the contract, caller is the address of the caller, and gas call is the operation that initiates the call.

  5. If this call operation is successful, the entire balance of the Bank contract will be sent to the Solver contract. If this operation fails, it will jump directly to the noauth label and perform the revert operation to roll back the transaction.

3. 100% 

The goal of this challenge is that the ETH balance of both SPLIT and _splitsById[ 0 ].wallet must be 0. The vulnerability exists in the distribute() function, which only validates the parameters by comparing the hash of the abi.encodePacked result, but since accounts and percentages are dynamically typed, they can be adjusted during the allocation process. Our solution is to extract more ETH than we deposited by manipulating the accounts and percentages arrays, taking advantage of insufficient argument validation in the distribute() function.

Vulnerability analysis

The distribute() function of the Split contract can be used to distribute specific assets based on the account and percentage specified when creating the SplitWallet. After allocation, users can withdraw money based on the value stored in balances.However, the distribute() function suffers from insufficient parameter validation. This function only validates the parameters by comparing the hash of the abi.encodePacked result, while accounts and percentages are dynamically typed. Therefore, during the allocation process, we can slightly adjust the accounts and percentages.

When creating SplitWallet{id: 0}, the account for the first index was accidentally left blank.

So we can extract all ETH from SplitWallet{id: 0} using modified accounts and percentages, but not distribute it to anyone, while keeping the hash unchanged (note that the array elements are padded to 32 bytes).

Similarly, we can use the hash collision caused by abi.encodePacked to withdraw more ETH than deposited to drain the Split.

solution

We mainly wrote the solve function to empty the ETH balance of SPLIT and _splitsById[ 0 ].wallet.The key to the entire solution is to extract more ETH without violating the hash verification mechanism by manipulating the accounts and percentages arrays and leveraging the behavior of the distribute function.The specific ideas are as follows:

  1. By adjusting the accounts and percentages arrays, you can control the allocation of ETH. Here we use an accounts array with only one address (our address) and a percentages array with two elements.

  2. Use the split.distribute function to withdraw ETH from SplitWallet to our account. This step is achieved by appropriately adjusting the parameters in the distribute function to ensure that we can receive ETH.

  3. Next, create a Split instance and set our address as the recipient.

  4. Deposit a certain amount of ETH through the split.deposit function, and then use the split.distribute function again to withdraw more ETH.

  5. Finally, call the split.withdraw function to withdraw all ETH from the Split contract to complete the challenge.

4. Dai++

The goal of this challenge is to get the total supply of Stablecoin to exceed 10^12* 10^18. The vulnerability is that when the AccountManager contract uses ClonesWithImmutableArgs to create a new account, the length limit of the immutable parameters is ignored, resulting in a damaged contract being deployed when the parameter length exceeds 65535 bytes. Our solution was to create an account with parameters that were too long, turning the increaseDebt() function into a phantom function, thus bypassing the health check and allowing large amounts of stablecoins to be minted without increasing debt.

Vulnerability analysis

Only accounts authorized by the SystemConfiguration contract can mint stablecoins. Only the owner of SystemConfiguration can update the system contract (that is, authorize the account), and the AccountManager contract is the only authorized contract.

In the AccountManager contract, only valid accounts can mint stablecoins. At the same time, the debt on the account will also increase.

In the increaseDebt() function, if the account is not healthy after the debt is increased, the transaction will fail. However, the player does not have enough ETH to mint 10^12 stablecoins and keep the account healthy.

It is worth noting that AccountManager uses ClonesWithImmutableArgs to create new accounts. When interacting with an account, immutable parameters will be read from calldata to save gas costs. But there is a comment in ClonesWithImmutableArgs: @dev data cannot exceed 65535 bytes because 2 bytes are used to store the data length.

Since immutable parameters are stored in the code area of ​​the created proxy contract, during deployment the code size will be calculated based on the data length. However, the code size that should be returned is also stored in 2 bytes. therefore,If runSize exceeds 65535 bytes, a broken contract may be deployed.We can treat the increaseDebt() function as a phantom function to ignore this call.

The existing parameter length is 20 + 20 + 32 = 72 bytes, the length of encoded recoveryAddresses will be a multiple of 32 bytes.

solution

We wrote the Solve contract and exploited the vulnerability of the AccountManager contract to mint a large number of stable coins.

  1. First, create a new Account containing an unusually long parameter by calling the AccountManagers openAccount function. This is accomplished by passing an empty address array of length 2044. This caused the internally created proxy contract to become corrupted due to parameter length exceeding the expected 65535 byte limit.

  2. To ensure that the parameter length is correct, the calculation formula 72 + 2044 * 32 + 2 + 0x 43 - 11 = 65538 is used. Here 72 is the existing parameter length, 2044 * 32 is the encoded length of recoveryAddresses, 2 is the number of bytes to store the data length, 0x 43 is the bytecode length in the creation phase, 11 is the bytecode length when the runtime contract is created . The calculation result 65538 exceeds the maximum length 65535, soA broken contract is created on deployment

  3. Use the newly created corrupted Account to mint a large number of stablecoins through the mintStablecoins function. Due to damage to the account contract,The increaseDebt function (which should increase the account debt) will not actually be executed correctly, allowing stablecoins to be minted without adding any debt.

5. DoDont

The goal of this challenge is to steal all WETH in the DVM (Proxy Voting Mechanism) project. The vulnerability is located in the init function of DVM.sol, which lacks call restrictions, allowing anyone to change the BASE_TOKEN and QUOTE_TOKEN addresses. Our solution exploits this vulnerability by changing these addresses to token contracts we control during the flash loan process, bypassing the balance check of the flash loan mechanism.

Vulnerability analysis

After a quick review of this DVM project, we noticedThe init function in DVM.sol lacks any calling restrictions.This is the root cause of the problem.

We can call the init() function at any time to change BASE_TOKEN and QUOTE_TOKEN, these are the base token addresses of the flash loans in the challenge. Exploiting such a vulnerability in flash loans is easy because we just needDuring the flash loan process, change BASE_TOKEN and QUOTE_TOKEN to the token contract addresses they control. This allows them to control the balance during the flash loan period, bypassing the balance check in the flash loan mechanism.

solution

We created the Solve contract to interact with the challenge contract. Create an Exploit contract to perform the attack. The contract first uses the flashLoan function to obtain the WETH balance, and then calls init through the DVMFlashLoanCall function to change the addresses of BASE_TOKEN and QUOTE_TOKEN to the controlled token contract. In this way, we can bypass the balance check of the flash loan mechanism and ultimately steal all WETH in the DVM.

6.Grains of Sand

The goal of this challenge is to reduce the GoldReserve (XGR) balance in the token store by at least 11111 × 10^8. The loophole is that GoldReserve tokens are charged a fee when transferred, but the token store does not support tokens with transfer fees. Our solution is to drain the store of tokens by repeatedly depositing and withdrawing GoldReserve tokens (both operations have transfer fees).

Vulnerability analysis

The private chain where this challenge lies is forked from block 18437825 of the Ethereum mainnet.

GoldReserve (XGR) tokens are subject to a fee when transferred, but tokens with transfer fees are not supported by the Token Store. Therefore, we can drain coins from the store by repeatedly depositing and withdrawing them.

Now we need to get some GoldReserve tokens first! Through the trade() function, we can exchange signatures for $XGR.

Trading orders can be partially filled. passDune, we can find unexpired GoldReserve token orders. Fortunately, there were two orders with large amounts of unsold coins.

solution

We created the Solve contract to interact with the challenge contract. First, trade to obtain some GoldReserve tokens through the trade() function. Then use the deposit and withdrawal mechanism in the token store to repeatedly operate to reduce the token balance in the token store. In this way, GoldReserve tokens can be successfully drained from the token store, meeting the conditions of the challenge.

7.Suspicious Charity

The goal of this challenge is to manipulate the price cache in a Python script to affect the price and liquidity calculations of a token. The vulnerability in the challenge arises from a Python script caching token addresses in a pool based on names. When these names are constructed using string(uint 8), values ​​above 0x 80 become the same in Python, leading to incorrect caching. Our solution is to create two trading pairs: one is a high-price and low-liquidity trading pair, which is used to update tokenPrice in the cache; the other is a low-price, high-liquidity trading pair, which is updated in the same name pool tokenAmount. Through this method, using miscalculations in Python scripts, we successfully manipulated the token price and liquidity, ultimately achieving the goal of stealing all WETH in the DVM.

Vulnerability analysis

The problem stems from a Python script caching token addresses in a pool based on names, which are constructed using string(uint 8). We have noticed,When values ​​exceed 0x80, they become identical in Python scripts, which can lead to incorrect caching. In the Python scripts get_pair_prices function, this resulted in incorrect price calculations.

We first created 78 useless trading pairs, and then created two manipulated trading pairs to launch the attack.

The first trading pair, which is characterized by high price and low liquidity, updates the tokenPrice in the cache. Subsequently, the second trading pair with low price and high liquidity updates tokenAmount in the same name pool. As the daemon continues to run, the donation value it accumulates reaches a rather high number.

solution

Create an Exploit contract to complete the challenge. The contract first creates some useless token trading pairs, then creates a high price low liquidity trading pair and a low price high liquidity trading pair. In this way, you canManipulating the price cache in Python scripts causes errors in the calculation of token prices and liquidity under certain conditions.After completing the challenge, transfer the accumulated value to the specified address.

8.Token Locker

The goal of this challenge is to exploit the vulnerability of the UNCX_ProofOfReservesV2_UniV3 contract to steal NFTs in the contract. The vulnerability is that the lock() function allows users to lock liquidity in the contract, but the nftPositionManager parameter in the LockParams structure received by the function can be replaced with a malicious contract. This allows us to control the location and liquidity of NFTs through a custom NFT location manager. Our solution is to create a TokenLockerExploit contract, which operates the lock function in the UNCX_ProofOfReservesV2_UniV3 contract and uses the CustomNftPositionManager contract to manipulate the position and liquidity of the NFT. In this way, we can transfer and control the assets in the NFT contract, and ultimately successfully empty the funds in the contract.

Vulnerability analysis

This problem originates from the contract UNCX_ProofOfReservesV2_UniV3, which is actually a fork of the 0x7f5C649856F900d15C83741f45AE46f5C6858234 contract on the Ethereum mainnet. After a quick review of the code, we need to take a closer look at the external functions that the user can interact with, specifically the lock() function.

In the UNCX_ProofOfReservesV2_UniV3 contract, the lock() function allows users to protect their liquidity by locking it in the contract. This function provides two options: users can convert the NFT to the full scope and claim the associated fees, which will then be returned to the requester, or they can leverage an already existing position.

This function receives the structure LockParams as input parameters, specifically nftPositionManager.

INonfungiblePositionManager The availability of nftPositionManager means that we can enter our contract, which will then return UNCX_ProofOfReservesV2_UniV3 from external calls that need to drain the contract.

During the execution of the lock() function, the _convertPositionToFullRange() function may be called. Highlighted below are the weak points.

We just need to pass parameters like this:

  1. mintParams.token 0 // nftPositionManager returns the address of the real Uniswap position manager

  2. address(_nftPositionManager) // Customize the address of nftPositionManager

  3. mintParams.amount 1 Desired // We should pass the NFT ID we want to drain.

Because ERC 721 and ERC 20 have the same transfer() function, the following expression in the _convertPositionToFullRange() function results inTransfer own NFT to malicious nftPositionManager:

solution

We created the TokenLockerExploit contract to steal NFTs. This contract realizes the emptying of contract funds by manipulating the lock() function in the UNCX_ProofOfReservesV2_UniV3 contract and manipulating the position and liquidity of NFT through the CustomNftPositionManager contract.

9. Skill Based Game

The goal of this challenge is to deplete all funds on the 0xA65D59708838581520511d98fB8b5d1F76A96cad Ethereum mainnet by winning consecutive BlackJack games. The vulnerability of the challenge is that the BlackJack game contracts dealing function (Deck.deal()) relies on block attributes (such as block.number and block.timestamp) to simulate randomness, which may make the results predictable. Our solution is to create an Attacker contract to simulate the card dealing process and decide whether to make an actual bet based on the predicted outcome.

Vulnerability analysis

In order to complete this challenge, we need to know in advance the cards that will be drawn so that we can make informed decisions about which games to play. Now, let’s take a closer look at how the contract governs the dealing of cards. Players need to call the deal() function, and checkGameResult() must be triggered at the end:

The dealing process is handled within the Deck.deal() function.This method of generating randomness relies on block properties and certain variables,As demonstrated by the code snippet below.This implementation introduces a vulnerability that allows the results to be predicted.

The process of dealing cards involves calculating the blockhash, the player address, the number of cards dealt, and the hash of block.timestamp. This is a well-known way of imitating randomness, simply by waiting for the required block, recalculating the game outcome based on new data, and if the game outcome matches our requirements, then we have to play.

solution

We created an Attacker contract using the Deck library to perform the attack. The contract first simulates the card dealing process and then decides whether to make an actual bet based on the predicted results.

At this point, we only need to repeatedly execute the play() function in this contract, using 5 ether as the value, until the funds in the BLACKJACK contract are exhausted. Here is the script to achieve this:

10. Enterprise Blockchain

The goal of this challenge is to extract at least 10 FlagTokens from the l1 Bridge on the L1 chain. The vulnerability of the challenge is that the L2 node can crash when processing a specific ADMIN precompiled contract call, causing the L2 node to restart and load from the previous state. Our solution is to exploit this vulnerability and first send remote messages from L2 to L1 to transfer FlagTokens to L1, and then trigger the L2 node to crash and restart. In this way, even if the status of the L2 node is restored to that before the transfer occurred, the funds have been successfully transferred to L1, and the funds on L2 have not been reduced, thus achieving the goal of the challenge.

Vulnerability analysis

There are two chains here.

(1) The challenge contract is deployed on L1.Initially, there are 100 FlagTokens (18 decimals) in l1 Bridge.

Users can use the bridge to transfer funds between chains. The relay will listen to the SendRemoteMessage event on both chains and forward the message to the target chain.

In order to issue the SendRemoteMessage event, we can call the sendRemoteMessage() function, and the transaction to be performed on the other chain can be customized.

Since L2 RPC is also provided and the player owns some ether, we can send a remote message from L2 to L1 and transfer the tokens from the l1 Bridge to the user.

but,the sendRemoteMessage() The function is not intended for public use, it is expected to only transfer funds between chains via ethOut() / ERC 20 Out().

(2) A SimpleMultiSigGov contract is deployed on the L2 chain, located at address 0x 31337. It can be used to interact with the precompiled contract ADMIN.

ADMINs precompiled contract has a fn_dump_state() function, the operations within which may lead to undefined behavior. First, x.len() should be greater than 0x 10, otherwise the program will panic due to out-of-bounds index when i == x.len(). states is a raw pointer to a slice [u 8 ], which is 16 bytes on x 86-64. The counting unit of states.offset is slice. Since the maximum value of i is 0x 10 , the minimum memory that should be allocated is 0x 110 (16 * (0x 10 + 1)) instead of 0x 100 . therefore,If x.len() is greater than 0x 10, the program will write to unallocated memory states.offset(0x 10).

When calling fn_dump_state(),If x.len() > 0x 10, it will cause the L2 node to crash.anvil service will be available soonRestartand from the previously dumped stateLoading status

The status dump interval is 5 seconds, but as long as the repeater catches the SendRemoteMessage event, it will forward the message.If the L2 node crashes when a new cross-chain transfer transaction is included in the block but the latest state has not been dumped, then the message will be forwarded to L1, and the state of L2 can only be restored to the state before the transfer occurred. In this case, the user can transfer funds to L1 without spending any funds in L2.

Only SimpleMultiSigGov at 0x 31337 can interact with ADMIN, but we were unable to obtain any valid signatures to execute the transaction. In addition, we can use the state coverage set to temporarily overwrite the code at 0x 31337 and simulate the call.

ADMINs admin_func_run() function is the entry point. To call the fn_dump_state() function, the first two bytes should be 0x 0204.

solution

Using pwn and other tools, we can perform a series of operations to trigger a crash of the L2 node, and then perform a cross-chain transfer when the L2 node restarts and loads from its previous state. This way we can move funds to L1 without actually spending any funds on L2.This process requires precise timing control and operation of the L2 node status.

11. Dragon Tyrant 

This challenge is a game against a dragon. Defeat the dragon to win and complete the challenge. There are two key vulnerabilities in this challenge:

(1) Predictable random numbers:The random number generation process of the game can be predicted, and this random number determines the attack/defense decision, thereby affecting the outcome of the game. The games random number generator relies on predictable seeds that can be obtained in advance by monitoring specific blockchain transactions (resolveRandomness). Our solution is to first monitor and collect enough seed information through the transaction pool listener, and then use this information to predict the next seed.

(2) Logical loopholes:Only when players equip legendary swords and shields can they maximize their attack value and defense. The game contract allows players to pass in their own store contract address to purchase equipment, and the mechanism to verify whether this custom store is legitimate is based on comparing the store contracts codehash rather than its address. This means that if players are able to create a contract with the same codehash as the official store but a different constructor, they can bypass the normal purchase process and price limits. Our solution exploits this vulnerability by creating a custom store contract to purchase legendary swords and shields, bypassing the high purchase costs.

Game background

This challenge background is a mini-game. in the gameThere is a dragon with super strength/physique (high attack power/defense power) and 60 health points.and you as,The protagonist has randomly generated weak attributes and 1 health point,This dragon needs to be defeated. Both you and the dragon are ERC 721 tokens, when the dragon is in battlefails and is subsequently destroyedhour(Solution check), challenge equals success.

You have to fight, fightThere are up to 256 mini-rounds. On each turn, you and the dragon canChoose attack or defensedamage calculationSummarized as follows:

After each small round, the health points of both parties will be reduced by the corresponding damage. When a partys health reaches 0, it will be destroyed and the game will end. If the health points of both parties reach 0,The attacking party - the player - will be destroyed

The dragon and players attack/defense attributes areCalculated based on respective attributes and equipment. Each side can equip one weapon and one shield.There is some equipment for sale in the shop, including a very powerful sword. The dragon will not be equipped with anything, and the player initially has1000 ETH

There are two places in the game where random number generators are used. one forDetermine player attributes, the other forDetermine dragons attack/defense decisionsUsing an ECC-based random number generator,seedProvided by off-chain

Attack/Defense Decisions

To defeat the dragon, we need to reduce its health to 0 while maintaining our only health. Looking at the attack/defense matrix, this means we cannot let attack/attack scenarios happen. During the attack/attack turn, both players health will be reduced to 0, causing the player to fail. This is because the attack attributes of both sides are much higher than the health of the other side. Since Defense-Defense rounds are similar to NOP, we can only rely on Attack/Defense rounds and Defense/Attack rounds.

Since the attack/defense decisions of both parties areSubmit in advanceYes, we need to know the dragons choice beforehand to avoid attacking/attacking rounds. This requires us to predictRandom number generation, and then we need to predictrandom seed. Fortunately, there is aPython libraryThe output of Pythons random module can be predicted once it observes approximately 20 k bits of output from the generator.

So how do we provide this library with the 20 k-bit output of a Python random module that we dont have access to? It turns out that we canCast any number of players, each minting transaction willTrigger the off-chain seed provider to submit a random seedWe can capture the seed by monitoring these transactions in the pending transaction pool. In fact, we found that after capturing 78 minted seeds, we could predict random seeds:

Since the ECC random number generator is deterministic, we can predict the dragons attack/defense decisions. We always do the opposite of dragons. If the dragon attacks, we defend. If the dragon defends, we attack.

Attack/Defense Attributes

Without any equipment, our modest attributes will cause us to fail in battle. The dragon has an attack property of type(uint 40).max and a defense property of type(uint 40).max - 1. Without any equipment, when we attack and the dragon defends, we do no damage to the dragon. When the dragon attacks and we defend, we immediately fail.

Naturally, we turned our attention tolegendary sword. With this sword, our attack attribute will reach type(uint 40).max, allowing us to cause 1 HP damage to the dragon when we attack and the dragon defends. If we repeat this process 60 times, the dragon will die. There is hope.

How can we afford this sword, it costs 1 million ETH and we only have 1000 ETH? It turns out that when weEquip this sword, the game allows us to pass in the store contract ourselves, and as long asThe store contract has been previously approved by the owner of the factory contract, the game will continue happily. Upon closer inspection, it turns out that this check is not done by verifying the store contract address, but byCompare codehash of store contractsto complete. This means that as long as we pass in a store contract with the same codehash, we can continue. Because extcodehash does not include a constructor,We could create our own item shop with the same code but a different constructor and use it to equip our players with swords.

This method works. Using the fake store with the following constructor we can get the legendary sword as well as the new legendary shield:

With these two pieces of legendary equipment, we will implement the attack properties of type(uint 40).max and the defense properties of type(uint 40).max. When the dragon attacks, we dont lose any HP, and when we attack, we deal 1 HP of damage to the dragon.

solution

Here is the step-by-step process for the solution:

  1. Mint player tokens into our own wallet.

  2. Deploy the fake shop and use it to equip the player with two pieces of legendary gear.

  3. Deploy the attacker contract, as required by the challenge. This contract will take over player tokens, initiate battles, and provide players with attack/defense decisions.

  4. Transfer player tokens to the attacker contract.

  5. Start the pending transaction pool listener to monitor resolveRandomness transactions. It captures seeds and predicts the next seed after gathering enough information.

  6. Mint 78 additional Player Tokens.

  7. At this point, the pool listener should have collected enough information to predict the next seed.

  8. The predicted seed is fed into a random number generator to determine the dragons attack/defense decisions.

  9. Reverse the dragons decision string bitwise to derive the players decision. When queried, the attacker contract will provide the players decision.

  10. The attacker contract launches an attack, resulting in the dragons failure.

12. Hopping Into Place

The goal of this challenge is to withdraw all funds from a cross-chain bridge contract. The vulnerability exists in the _additionalDebit() function. When this function calculates the bonders liability, if challengePeriod is set to 0, no debt will be added. Our solution is to exploit this vulnerability by setting challengePeriod to 0 so that numTimeSlots is also 0, thereby preventing the debt from increasing. Next, we use the bondTransferRoot() function to withdraw any amount of tokens, since the getDebitAndAdditionalDebit() function loses its original functionality in this case, resulting in the debt not increasing. In this way, we successfully drained the funds in the cross-chain bridge.

Vulnerability analysis

In this challenge, our identity is the governor, so we can change some configurations of the cross-chain bridge.

The root of the problem lies in the _additionalDebit() function, we noticedThe debt is added in the if statement. This means that if numTimeSlots equals 0, the statement is not executed.The bonders liability does not increase. Obviously, this design is unreasonable; the increase in debt should not be skipped under any circumstances.

We can take advantage of this and achieve the condition that numTimeSlots is 0 by setting challengePeriod to 0.

In this way, the getDebitAndAdditionalDebit function loses its additional functionality,No matter what we do, the debt will not increase.

This also affects the requirePositiveBalance modifier, which requires that after the function is executed, our credit must be greater than the increased debt. However, since the function loses its extra functionality, our debt remains the same. this meansWe can use the function modified by this modifier to drain cross-chain bridges.

Finally, lets look at the logic in bondTransferRoot. This function sets totalAmount to the callers debt and adds totalAmount to transferRoots for extraction. Therefore, we can use this function to withdraw any number of tokens.

solution

We wrote several key contracts to prevent the addition of additional debt by setting the challenge period to 0 to withdraw funds from the cross-chain bridge. Each contract implements specific functions:

  1. Exploit contract: This is the main contract of the attack and is responsible for executing the entire attack process. It is first associated with the challenge contract Challenge, and then manipulates the cross-chain bridge IBridge through a series of operations, and finally achieves the purpose of withdrawing funds.

  2. MockMessageWapper contract: This contract simulates the process of cross-chain message delivery. In actual applications, it does not perform any effective operations, but serves as a placeholder, allowing the exploit contract to simulate the cross-chain interaction process.

  3. Solve Contract: This contract inherits from CTFSolver and is used to interact with the challenge contract Challenge in the Capture The Flag (CTF) challenge. It is mainly responsible for calling the exploit method of the Exploit contract to perform the attack, and confirming whether the challenge has been resolved after the attack is successful.

  4. IBridge interface: This is an interface that defines cross-chain bridge contract methods. It includes the cross-chain bridge operation methods used in the Exploit contract, such as adding guarantors, setting challenge deadlines, binding transfer roots, withdrawing funds, etc.

  5. IChallenge interface: This interface defines the methods in the challenge contract Challenge, allowing the Exploit contract to access the cross-chain bridge address in the challenge.

13. Oven

The goal of this challenge is to recover a hidden FLAG value. The core of the challenge is the fiat_shamir() function, which uses the custom hash function custom_hash() to generate a random number and then uses this number to participate in the calculation. The key vulnerability is located in the fiat_shamir() function, especially in the expression r=(v - c * FLAG) mod (p-1), which involves known r, c, p values ​​and unknown FLAG values. The solution is to transform the problem into a lattice problem and then use the lattice basis reduction algorithm (LLL algorithm) to find the FLAG value.

Vulnerability analysis

Code function: Users can obtain the random signature of FLAG, and the logic for generating random signatures is located in the fiat_shamir() function. A custom hash function custom_hash is used to generate the hash value, which calls four different hashing algorithms, so its randomness cannot be cracked at present.

In addition, the fiat_shamir transformation is a very important tool in cryptography. Its core is to use a hash algorithm to generate random numbers and add randomness to the encryption protocol. A typical application of FS transformation is to introduce non-interactivity into zero-knowledge proof systems and then build protocols such as snark and stark.

From the source code, we can obtain t, r, p, g, y and other information, but in fact c can be calculated using the custom_hash() function. Therefore, the vulnerability is concentrated in the fiat_shamir() function, which is the functional part that signs FLAG, focusing on: r=(v - c * FLAG) mod (p-1). For this equation, the information we can currently obtain is that r, c, and p are all known values, and the number of FLAG bits has been determined: assert FLAG.bit_length()<384 。 It can be related to the HNP problem (with variable modulus) proposed by Dan Boneh in 1996 and can be attacked using standard lattice algorithms.For a more detailed cryptanalysis of lattice-based attacks, please refer toRelated papers

The problem is r=(v - c * FLAG) mod (p-1) in the code. Since r, c, and p are all known values, then:

  1. First, mathematically transform the above equation: r-v+c*FLAG= 0 mod (p-1), where only v and FLAG are unknowns.

  2. Second, construct the lattice:, where K is the upper bound of FLAG and all blank spaces are 0.

  3. According to Babais CVP solution algorithm, there must be a solution vector j=[l1, l2, l3, FLAG, 1], making jM=jk true.

  4. Note that jk is a short vector in the lattice, so we can find this short vector in polynomial time using the LLL algorithm. Note that each element of a short vector can be represented by 64 bits, so the upper bound K= 2^64 is determined.

Tip: Here is a note on data volume issues. How do we know how many sets of data are needed to recover FLAG? This requires using the Gaussian heuristic to estimate the shortest vector length, and the required target vector norm is smaller than this length. However, since this is the context of a CTF competition, usually three to four or five sets of data can be used initially. If not, you can use the method above for precise calculations. here,We collected five sets of data for backup, but only three sets of data were actually used to solve FLAG.

solution

Our code needs to run in the sage-python environment. The main ideas are as follows:

  1. Construct the lattice: First construct a specific lattice, which contains the known p, c, r values ​​and the unknown FLAG value. This grid is transformed from the above equation.

  2. Using the LLL algorithm: Apply the LLL algorithm to find short vectors in the lattice. The LLL algorithm is an efficient algorithm that can find in polynomial time a basis vector of a lattice that is mathematically related to the solution of the original problem.

  3. Recovering FLAG: Once the short vector is found, the value of FLAG can be extracted from it. Since elements in short vectors can be represented in 64 bits, this places an upper limit on the size of FLAG.

From competition to practice

The Salute team gained valuable experience in the Paradigm CTF 2023 competition, which is nowEnhanced smart contract auditing services provided by Salus Securityimportant parts of. If you need top-notch smart contract auditing services, feel free tocontact us. We are committed to providing comprehensive and efficient support for your needs.

Safety
smart contract
Paradigm
Welcome to Join Odaily Official Community
AI Summary
Back to Top
Salus安全团队在Paradigm 2023 CTF中共解决了13项挑战,在1011支队伍中以3645.60的分数获得第九名,并受邀成为Paradigm CTF 2024的客座作者。在这篇博文中,我们将介绍我们在比赛期间解决的所有挑战。
Author Library
Download Odaily App
Let Some People Understand Web3.0 First
IOS
Android