Maleabilidad de las transacciones “Legacy”
Cómo demostrar la maleabilidad de las transacciones Legacy
Autor
EntrePlanctonyBallenas.
Twitter para correcciones, comentarios o sugerencias: @entreplanctony1
El presente tutorial fue elaborado para el Seminario socrático de Mastering Bitcoin a través de @libreriadesatoshi.
En el siguiente enlace puedes encontrar la documentación de referencia:
Transaction malleability
Maleabilidad de las transacciones
🛈 Nota: esta vulnerabilidad solo aplica a transacciones legacy, si decides intentar los pasos aquí descritos debes crear una transacción en una wallet sin descriptors y con direcciones legacy.
Una vez que una transacción es firmada con una llave privada, el contenido de la transacción no puede alterarse ya que cualquier modificación al código hexadecimal de la transacción produciría una firma criptográfica completamente distinta.
Sin embargo, es posible resolver el script de desbloqueo (en el arreglo de vin) alterando el formato de la firma misma, sin invalidar la solución de la transacción mediante los OP_CODE. Esto se puede lograr debido a que la firma criptográfica no es parte del hash que se debe firmar y los scripts de desbloqueo son públicos para cualquiera que quiera consultarlos.
Consideremos el siguiente script de bloqueo como ejemplo:
2 OP_ADD 4 OP_EQUAL
Podemos resolver este script usando el valor 2 como script de desbloqueo, pero hay varias otras formas de expresar el valor correcto de este script, aquí algunos ejemplos:
OP_2
OP_PUSH1 0x02
OP_PUSH2 0x0002
OP_PUSH3 0x000002
...
OP_PUSHDATA1 0x0102
OP_PUSHDATA1 0x020002
...
OP_PUSHDATA2 0x000102
OP_PUSHDATA2 0x00020002
...
OP_PUSHDATA4 0x0000000102
OP_PUSHDATA4 0x000000020002
...
Al utilizar el valor 2 como script de desbloqueo y calcular el hash de la transacción entera obtendríamos un TXID válido, sin embargo cualquier valor distinto de 2 , por ejemplo OP_PUSHDATA2 0x000102 producirá un hash completamente distinto y por lo tanto un TXID totalmente distinto al del emisor original de la transacción.
Esto podría representar un peligro bajo ciertas condiciones especificas en las que el emisor esta conectado a un nodo “deshonesto” que decida publicar la nueva transacción en lugar de la original.
Veamos un ejemplo con una transacción en regtest.
Mutando una transacción Legacy
Lo primero es decodificar una transacción legacy para encontrar el script de desbloqueo en el arreglo de vin, puedes apoyarte en el Ejemplo para Decodificar una transacción a mano para encontrar el script de desbloqueo de tu transacción.
Como yo tengo una transacción que usamos en Comprobando una firma de Transacción, voy a usarla para este ejemplo, la información subrayada es el script de desbloqueo.
020000000179d5220fa628d9fd463fa6fb5cf45df7b49b81467a6eb7fd7d948e3a6ec0f22b000000006a47304402207363809dea44ee307f4ce74e6b51b451984625d9212bf8f30bb5862311bfcb8302201312bce412e43550cb4002eab96967abdfc527f161bb47181c86473b0df7e0d5012103865176c3174200de1b60f0dae80f3ed28c920fe78bff02eb0d028d65fcaa45deffffffff0200e1f505000000001976a914b47a6f225525f7f4674952ce91ddbc8ef755aee188ac1fb23f71000000001976a914a16ef9a4f940070c8360f7a9bb8853756d5ca2c988ac00000000
La traducción del script seleccionado podemos obtenerla con el comando decodescript de bitcoin-core .
NOTA: Se debe omitir el primer byte 6a ya que este representa el tamaño total del script dentro de la transacción.
$ bitcoin-cli decodescript 47304402207363809dea44ee307f4ce74e6b51b451984625d9212bf8f30bb5862311bfcb8302201312bce412e43550cb4002eab96967abdfc527f161bb47181c86473b0df7e0d5012103865176c3174200de1b60f0dae80f3ed28c920fe78bff02eb0d028d65fcaa45de|jq .asm
"304402207363809dea44ee307f4ce74e6b51b451984625d9212bf8f30bb5862311bfcb8302201312bce412e43550cb4002eab96967abdfc527f161bb47181c86473b0df7e0d501
03865176c3174200de1b60f0dae80f3ed28c920fe78bff02eb0d028d65fcaa45de"
Decodificando a mano el script de desbloqueo
Podemos decodificar el script de bloqueo/desbloqueo de una manera similar a como decodificamos usando el campo “scriptsize” dentro de una transacción.
Comenzando por el tamaño del script y continuando con cada uno de los bytes del script, cada byte puede ser un valor o una representación de un OP_CODE. Los OP_CODE son valores en hexadecimal que están reservados para interpretar un Operador especifico, definidos en el archivo script.h del código fuente de Bitcoin Core.
Para el caso específico del script que queremos decodificar:
6a
Este primer byte representa el “scriptsize” y determina el tamaño total del script. Son 106 bytes.
47
Tamaño del primer argumento dentro del script es de 71 bytes (hex).
304402207363809dea44ee307f4ce74e6b51b451984625d9212bf8f30bb5862311bfcb8302201312bce412e43550cb4002eab96967abdfc527f161bb47181c86473b0df7e0d501
Como esta transacción es de tipo P2PKH este valor es la firma del contenido de la transacción.
21
Tamaño del siguiente argumento dentro del script que es de 33 bytes (hex).
03865176c3174200de1b60f0dae80f3ed28c920fe78bff02eb0d028d65fcaa45de
De nuevo, como la transacción es P2PKH este valor representa la llave pública utilizada para confirmar la firma.
Modificando el script
Para modificar el script basta con agregar un OP_CODE que cambie el formato en el que presentamos la información del script sin alterar el resultado. Vamos a utilizar el siguiente OP_CODE que se define en el wiki de bitcoin.
OP_PUSHDATA2 - Representado por el valor 4d
La descripción de este operador indica que los siguientes 2 bytes escritos en little endian contienen el numero de bytes que se van a ingresar a la pila del script.
Para agregar OP_PUSHDATA2 en nuestro script debemos introducir el código 4d al inicio de nuestro script.
Si aplicamos este OP_PUSHDATA2 a la firma del script original tendríamos que cambiar el valor 47 que es la longitud de la firma y representarlo en 2 bytes en formato little endian, esto quedaría como 4700
Finalmente, hemos agregado un total de 2 bytes nuevos a la longitud completa de nuestro script por lo que ahora debemos también modificar el valor de scriptsize al inicio del script ya que este ahora mide 108 bytes, este valor en hexadecimal es de 6c.
Nuestro script “mutado” quedaría de la siguiente forma:
6c4d4700304402207363809dea44ee307f4ce74e6b51b451984625d9212bf8f30bb5862311bfcb8302201312bce412e43550cb4002eab96967abdfc527f161bb47181c86473b0df7e0d5012103865176c3174200de1b60f0dae80f3ed28c920fe78bff02eb0d028d65fcaa45de
Si decodificamos el script “mutado” con decodescript vemos que el resultado del “asm” es exactamente el mismo que teníamos con el script original. NOTA: Se debe omitir el primer byte 6c ya que este representa el tamaño total del script dentro de la transacción.
$ bitcoin-cli decodescript 4d4700304402207363809dea44ee307f4ce74e6b51b451984625d9212bf8f30bb5862311bfcb8302201312bce412e43550cb4002eab96967abdfc527f161bb47181c86473b0df7e0d5012103865176c3174200de1b60f0dae80f3ed28c920fe78bff02eb0d028d65fcaa45de |jq .asm
"304402207363809dea44ee307f4ce74e6b51b451984625d9212bf8f30bb5862311bfcb8302201312bce412e43550cb4002eab96967abdfc527f161bb47181c86473b0df7e0d501
03865176c3174200de1b60f0dae80f3ed28c920fe78bff02eb0d028d65fcaa45de"
Nuestra transacción “mutada” quedaria de la siguiente forma:
020000000179d5220fa628d9fd463fa6fb5cf45df7b49b81467a6eb7fd7d948e3a6ec0f22b000000006c4d4700304402207363809dea44ee307f4ce74e6b51b451984625d9212bf8f30bb5862311bfcb8302201312bce412e43550cb4002eab96967abdfc527f161bb47181c86473b0df7e0d5012103865176c3174200de1b60f0dae80f3ed28c920fe78bff02eb0d028d65fcaa45deffffffff0200e1f505000000001976a914b47a6f225525f7f4674952ce91ddbc8ef755aee188ac1fb23f71000000001976a914a16ef9a4f940070c8360f7a9bb8853756d5ca2c988ac00000000
Verificando el TXID
Este seria el TXID de la transacción original:
$ bitcoin-cli decoderawtransaction 020000000179d5220fa628d9fd463fa6fb5cf45df7b49b81467a6eb7fd7d948e3a6ec0f22b000000006a47304402207363809dea44ee307f4ce74e6b51b451984625d9212bf8f30bb5862311bfcb8302201312bce412e43550cb4002eab96967abdfc527f161bb47181c86473b0df7e0d5012103865176c3174200de1b60f0dae80f3ed28c920fe78bff02eb0d028d65fcaa45deffffffff0200e1f505000000001976a914b47a6f225525f7f4674952ce91ddbc8ef755aee188ac1fb23f71000000001976a914a16ef9a4f940070c8360f7a9bb8853756d5ca2c988ac00000000 |jq .txid
"4cdc31f444242a99411e65361dab78f677a68bfa1e4f22f488c684811c30f16d"
Y este sería el TXID de la transacción mutada:
$ bitcoin-cli decoderawtransaction 020000000179d5220fa628d9fd463fa6fb5cf45df7b49b81467a6eb7fd7d948e3a6ec0f22b000000006c4d4700304402207363809dea44ee307f4ce74e6b51b451984625d9212bf8f30bb5862311bfcb8302201312bce412e43550cb4002eab96967abdfc527f161bb47181c86473b0df7e0d5012103865176c3174200de1b60f0dae80f3ed28c920fe78bff02eb0d028d65fcaa45deffffffff0200e1f505000000001976a914b47a6f225525f7f4674952ce91ddbc8ef755aee188ac1fb23f71000000001976a914a16ef9a4f940070c8360f7a9bb8853756d5ca2c988ac00000000 |jq .txid
"4c24cff20d652543346ea9922b5f630c179240649a1e67a1817d6f20ff5611ad"
🎉 ¡Hemos confirmado que se puede modificar el TXID de una transacción Legacy!
Este problema se resuelve con segwit gracias a que los datos del script de desbloqueo no se utilizan para calcular el TXID de una transacción como lo vimos en el Ejemplo para Decodificar una transacción a mano