When a signer acts as a coordinator, he will initiate BTC transactions to transfer the deposited BTC to the signer's multi-wallet. The structure of these transactions is as follows.
The transaction fees for these transactions are shared by all deposits. And the signer will check each deposit to ensure that the transaction fee does not exceed the user's expectations.
The bug now is that a malicious signer can initiate a BTC transaction without deposits, then all checks on deposits will be bypassed (including transaction fees). And, this BTC transaction will be paid by the multi-sign wallet.
The attacker can use this to make the multi-sign wallet lose all BTC, which will be rewarded to BTC miners. So the attacker can cooperate with BTC miners to steal all BTC.
Vulnerability Details
The signer/src/bitcoin/validation.rs::to_input_rows code is as follows.
pub fn to_input_rows(&self) -> Vec<BitcoinTxSigHash> {
// If any of the inputs or outputs fail validation, then the
// transaction is invalid, so we won't sign any of the inputs or
// outputs.
let is_valid_tx = self.is_valid_tx();
let validation_results = self.reports.deposits.iter().map(|(_, report)| {
report.validate(
self.chain_tip_height,
&self.tx,
self.tx_fee,
self.max_deposit_amount,
)
});
// just a sanity check
debug_assert_eq!(self.deposit_sighashes.len(), self.reports.deposits.len());
let deposit_sighashes = self
.deposit_sighashes
.iter()
.copied()
.zip(validation_results);
// We know the signers' input is valid. We started by fetching it
// from our database, so we know it is unspent and valid. Later,
// each of the signer's inputs were created as part of a
// transaction chain, so each one is unspent and locked by the
// signers' "aggregate" private key.
[(self.signer_sighash, InputValidationResult::Ok)]
.into_iter()
.chain(deposit_sighashes)
.map(|(sighash, validation_result)| BitcoinTxSigHash {
txid: sighash.txid.into(),
sighash: sighash.sighash.into(),
chain_tip: self.chain_tip,
prevout_txid: sighash.outpoint.txid.into(),
prevout_output_index: sighash.outpoint.vout,
prevout_type: sighash.prevout_type,
validation_result,
is_valid_tx,
will_sign: is_valid_tx && validation_result == InputValidationResult::Ok,
})
.collect()
}
Please see the above code. For signer, all checks are done based on deposits. But it does not check whether deposits is empty. An attacker can submit an attack transaction (as shown below) to drain all BTC in the multi-sign wallet.
And If the attacker cooperates with the BTC miner, the attacker can steal these BTC.
References
None
Proof of Concept
Proof of Concept
Base on: https://github.com/stacks-network/sbtc/releases/tag/0.0.9-rc4
Patch signer/src/config/mod.rs, add attacker flag config
/// The minimum bitcoin block height for which the sbtc signers will
/// backfill bitcoin blocks to.
pub sbtc_bitcoin_start_height: Option<u64>,
+ /// @audit;
+ pub audit_this_signer_is_attacker: Option<bool>,
}
impl Validatable for SignerConfig {
Patch signer/src/main.rs, load attacker flag
);
// Load the configuration file and/or environment variables.
- let settings = Settings::new(args.config)?;
+ let mut settings = Settings::new(args.config)?;
+ std::thread::sleep(std::time::Duration::from_millis(2000)); // wait for the `docker logs` command
+ settings.signer.audit_this_signer_is_attacker = match std::env::var("AUDIT_THIS_SIGNER_IS_ATTACKER") {
+ Ok(value) => Some(value.parse::<bool>().unwrap()),
+ _ => Some(false),
+ };
+ tracing::info!("@audit; audit_this_signer_is_attacker: {:?}", settings.signer.audit_this_signer_is_attacker);
signer::metrics::setup_metrics(settings.signer.prometheus_exporter_endpoint);
// Open a connection to the signer db.
Patch docker/docker-compose.yml, add attacker flag
Waiting for the sBTC contract to be deployed. Then run the poc9 tool. It will send 40 BTC to the signers BTC address and trigger deposits every 10 seconds.
cargo run -p signer --bin poc9
Wait until the trigger the coordinator is sbtc-signer-3. You can check the logs marked with "@audit;" to confirm that the attack was triggered
In BTC explorer, you will find that the BTC transaction initiated by sbtc-signer-3 is malicious. It does not carry any deposits and withdrawals, but only consumes BTC.