code4rena-2023-10-brahma-m02
[M-02] SubAccount operator can steal funds via the gas refund mechanism
Summary
Safe 월렛가 가스 환급에 사용하는 파라미터는 Validator에게 서명받아 인증하지 않는다. 때문에 서브 계정 운영자가 마음대로 설정할 수 있고, baseGas 나 gasPrice 등을 조작하면 해당 서브 계정이 소유한 ETH 또는 ERC20을 탈취할 수 있다.
Keyword
signature, lack of input validation, gnosis safe, wallet, integration, gas refund, theft
Vulnerability
서브 계정 운영자는 콘솔 계정이 활성화한 특정 트랜잭션을 서브 계정(Safe 월렛)에게 실행시킬 수 있는 권한이 있다. 서브 계정 운영자가 서브 계정에게 실행시킬 작업과 파라미터를 요청하면 Validator가 이를 서명하여 준다. 운영자는 이 서명을 사용하여 서브 계정의 execTransaction를 실행한다. Safe 월렛은 Brahma의 컨트랙트에게 서명 확인을 요청하고, Validator의 서명이 유효함을 확인하면 실행을 허용한다.
isPolicySignatureValid 함수에서 서명을 확인한다. 서명 대상으로 사용되는 데이터에 주목하자.
function isPolicySignatureValid(
address account,
address to,
uint256 value,
bytes memory data,
Enum.Operation operation,
bytes calldata signatures
) external view returns (bool) {
// Get nonce from safe
uint256 nonce = IGnosisSafe(account).nonce();
// Build transaction struct hash
bytes32 transactionStructHash = TypeHashHelper._buildTransactionStructHash(
TypeHashHelper.Transaction({
@> to: to,
@> value: value,
@> data: data,
@> operation: uint8(operation),
@> account: account,
@> executor: address(0),
@> nonce: nonce
})
);
// Validate signature
@> return isPolicySignatureValid(account, transactionStructHash, signatures);
}
function isPolicySignatureValid(address account, bytes32 transactionStructHash, bytes calldata signatures)
public
view
returns (bool)
{
// Get policy hash from registry
bytes32 policyHash =
@> PolicyRegistry(AddressProviderService._getRegistry(_POLICY_REGISTRY_HASH)).commitments(account); // @audit-info 호출자(운영자)가 가능한 작업 조회
if (policyHash == bytes32(0)) {
revert NoPolicyCommit();
}
// Get expiry epoch and validator signature from signatures
@> (uint32 expiryEpoch, bytes memory validatorSignature) = _decompileSignatures(signatures);
// Ensure transaction has not expired
if (expiryEpoch < uint32(block.timestamp)) {
revert TxnExpired(expiryEpoch);
}
// Build validation struct hash
bytes32 validationStructHash = TypeHashHelper._buildValidationStructHash(
TypeHashHelper.Validation({
@> transactionStructHash: transactionStructHash,
@> policyHash: policyHash,
expiryEpoch: expiryEpoch
})
);
// Build EIP712 digest with validation struct hash
@> bytes32 txnValidityDigest = _hashTypedData(validationStructHash);
address trustedValidator = AddressProviderService._getAuthorizedAddress(_TRUSTED_VALIDATOR_HASH);
// Empty Signature check for EOA signer
if (trustedValidator.code.length == 0 && validatorSignature.length == 0) {
// TrustedValidator is an EOA and no trustedValidator signature is provided
revert InvalidSignature();
}
// Validate signature
@> return SignatureCheckerLib.isValidSignatureNow(trustedValidator, txnValidityDigest, validatorSignature);
}이때, 서브 계정 운영자가 서명하는 데이터에는 포함되지 않지만 execTransaction 함수에서 사용하는 파라미터가 있다. 이 파라미터는 가스 비용을 환불해주는 기능에서 사용하는 파라미터이다. 서명에서 이를 확인하지 않으므로, 서브 계정 운영자가 마음대로 설정 가능하다.
uint256 safeTxGasuint256 baseGasuint256 gasPriceaddress gasTokenaddress payable refundReceiver
function execTransaction(
address to,
uint256 value,
bytes calldata data,
Enum.Operation operation,
@> uint256 safeTxGas,
@> uint256 baseGas,
@> uint256 gasPrice,
@> address gasToken,
@> address payable refundReceiver,
bytes memory signatures
) public payable virtual override returns (bool success) {
bytes32 txHash;다음은 Safe 월렛의 코드이다. 요청한 작업을 실행한 후 가스가 남았다면 월렛 소유(서브 계정 소유) 토큰을 사용하여 환급해준다. 이 때 호출자가 넘긴 safeTxGas, baseGas, gasPrice 를 기반으로 돌려줄 토큰의 수를 계산한다. 임의로 baseGas 나 gasPrice를 조작하면 서브 계정이 소유한 모든 토큰(gasToken를 address(0)으로 하면 네이티브, 아니면 ERC20)을 refundReceiver 에게 전달하여 빼낼 수 있다.
function execTransaction(
...
) public payable virtual override returns (bool success) {
...
if (gasleft() < ((safeTxGas * 64) / 63).max(safeTxGas + 2500) + 500) revertWithError("GS010");
// Use scope here to limit variable lifetime and prevent `stack too deep` errors
{
uint256 gasUsed = gasleft();
// If the gasPrice is 0 we assume that nearly all available gas can be used (it is always more than safeTxGas)
// We only substract 2500 (compared to the 3000 before) to ensure that the amount passed is still higher than safeTxGas
success = execute(to, value, data, operation, gasPrice == 0 ? (gasleft() - 2500) : safeTxGas);
gasUsed = gasUsed.sub(gasleft());
// If no safeTxGas and no gasPrice was set (e.g. both are 0), then the internal tx is required to be successful
// This makes it possible to use `estimateGas` without issues, as it searches for the minimum gas where the tx doesn't revert
if (!success && safeTxGas == 0 && gasPrice == 0) revertWithError("GS013");
// We transfer the calculated tx costs to the tx.origin to avoid sending it to intermediate contracts that have made calls
uint256 payment = 0;
if (gasPrice > 0) {
@> payment = handlePayment(gasUsed, baseGas, gasPrice, gasToken, refundReceiver);
}
if (success) emit ExecutionSuccess(txHash, payment);
else emit ExecutionFailure(txHash, payment);
}
...
}
function handlePayment(
uint256 gasUsed,
uint256 baseGas,
uint256 gasPrice,
address gasToken,
address payable refundReceiver
) private returns (uint256 payment) {
// solhint-disable-next-line avoid-tx-origin
address payable receiver = refundReceiver == address(0) ? payable(tx.origin) : refundReceiver;
if (gasToken == address(0)) {
// For native tokens, we will only adjust the gas price to not be higher than the actually used gas price
payment = gasUsed.add(baseGas).mul(gasPrice < tx.gasprice ? gasPrice : tx.gasprice);
@> (bool refundSuccess, ) = receiver.call{value: payment}("");
if (!refundSuccess) revertWithError("GS011");
} else {
payment = gasUsed.add(baseGas).mul(gasPrice);
@> if (!transferToken(gasToken, receiver, payment)) revertWithError("GS012");
}
}Impact
서브 계정 운영자가 서브 계정에 있는 모든 ETH 또는 ERC20을 꺼내갈 수 있다.
Mitigation
safeTxGas, baseGas, gasPrice, gasToken, refundReceiver 파라미터도 Validator가 서명을 한다.
Memo
토큰 탈취임에도 Medium 심각도인게 의문.
Gnosis safe 는 자주 통합되므로 사용법을 자세하게 파악하는게 좋겠다. 이 취약점을 찾아서 메모도 달아뒀는데 Safe 월렛에 대한 이해도가 떨어져서 이슈화 하지 못해 아쉬움이 있다. Safe wallet과 통합한 여러 서비스의 이슈를 찾아보면서 어떤 점을 주의해야 할지 파악할 필요가 있겠다.
tags: bughunting, brahma, smart contract, solidity, signature, lack-of-input-validation-vul, gas refund, gnosis safe, wallet, integration, crypto theft, severity medium