本文概述:为了方便初学者进行架构学习,文章从基础开始讲起,以比特币、以太坊、超级账本的架构开始讲起(包括了一部分源码分析)。之后针对P2P现存安全问题进行分析,包括概念层的女巫攻击、Eclipse攻击以及落地DDos攻击。之后依照密码学的知识,详细剖析了部分安全问题的解决方法。
文章为原创,是我在经过大量的文献阅读已经源码分析后总结的知识,其中可能涉及到部分密码学等不容易理解的问题。大家有需要讨论的请在下方留言!希望我的分析能给大家带来收获!

一、三大应用的P2P架构详情

要学会攻击就要懂得详细的机制。而P2P中最精妙的地方就在于其涉及的巧妙之处。具体我们看他们是如何一步一步打造出“区块链P2P网络”这个大厦的。

1 比特币中P2P架构

比特币开启了区块链时代,任何节点开启客户端后即可实现去中心化可信任的比特币交易。然而分布式网络最大的特点就是能够做到无中心化、能够任意接入网络、离开网络等。为了能够做到上述要求,比特币设计了如下三种节点发现方式。

(1)种子节点的采用

如同我上一篇文章所介绍的内容一样,比特币采用了“全分布式非结构化”的网络---区块链安全—区块链P2P网络安全密码协议分析。为了方便节点间获取整个网络的详细信息,比特币设定了一部分期稳定的节点硬编码至代码中。这些节点在初始启动时,提供最初接入网络的入口节点。新节点通过这些稳定节点作为中介连接其他节点,以达到获取整个网络节点地址的所有列表。这些地址又被称为比特币的初始化加载种子地址。

(2)地址广播手法

当新加入的节点连入网络后,其就可以以这个节点作为中介来帮助其他新点接入网络(老带新政策)。这个帮助的过程称为地址广播。包括:

  • 主动广播(push)
在比特币源码中,其采用了Net.AdvertiseLocal()函数push自己。这是单项过程,其他节点接收到推送的地址信息后会将内容保存在本地并且不会做出回应。
  • 主动拉取(pull)
主动将自身地址告诉周围节点还不足以保证network中所有的节点均获得网络结构。这里还需要进行主动拉取的过程。

在区块链源码中,比特币通过Net.GetAddresses( )方法主动拉去地址。节点A只向周围节点发出请求(例如向B请求),并接受到B发回的其本地存储的所有收录的地址信息。并且此请求只进行一次(避免网络资源浪费)。同时网路中所有的节点均可以向周围节点发出请求。PS:**这里就特别像计算机网络中的洪范,我认为是有所借鉴**

比特币的两种地址获取方式是独立的,这样可以防止攻击者通过伪造大量假的地址并广泛散播,导致正常节点无法接入比特币网络或接入虚假网络。

(3) 地址数据库方法

在比特币节点的本地中,存在地址数据库的概念。为了避免每次接入网络都进行大量的地址申请,比特币客户端使用的是levelDB格式储存数据将获取的节点信息保存在 peers.dat 本地文件中。A节点会定时向周围节点发送Ping指令,其余节点收到后会进行随机数回复。倘若节点持续了超过20min没有响应,那么A就会认为其邻居已经断开。除此之外,比特币客户端启动后会发起定时循环任务,检查连接节点是否在线,并将失败的节点保存在 banlist.dat 本地文件中。

2 以太坊中P2P架构

比特币作为区块链的鼻祖对后续的应用有着先天的影响。以太坊就收到了比特币架构的影响。所以以 太坊不仅能够实现类似比特币的交易系统,更希望构建基于区块链的生态环境,拓展依赖于以太坊衍生的分布式应用。由上文我们知道,以太坊使用了“全分布式结构化”的网络架构,这些应用的实现依赖于节点可以根据需要精确查找另一个节点地址的功能,而非结构化 P2P网络结构无法满足。我们下面就谈一谈以太坊是如何发现周围节点的。

(1)种子节点

如同比特币的设计理念相同,以太坊的初始也是有其“硬编码种子节点”。它由节点信息、网络地址 URL 和数据协议三部分构成:节点信息、网络地址url、tcp端口号或者udp发现端口号。入下图所示。


初次之外,除了上图中的几个官方种子节点外,以太坊为了提高网络搭建的效率,选取了优质的个人节点并打包成static-nodes.json 文件后发布。以太坊用户下载后可以连 接到更多国内节点,加快以太坊国内网络速度。类似的配置文 件还有 trusted-nodes.json 保存信任节点信息,这两个文件不限定内容,只限定文件名及格式,以固定文件名的形式硬编码保存在配置文件内,配置的节点会在以太坊启动时加载。

(2)地址数据库

以太坊同样采用了地址数据库来保存历史节点。其使用leveldb作为历史文件格式,并生成多个*.ldb文件。对于首次连接以太坊网络的节点,地址数据库是空的,后续随着地址广播,逐渐填满地址列表。而对于多次连接的节点,其启动使用了loadSeedNodes( )方法将种子节点和历史数据一起读取,快速高效连接以太坊 P2P 网络。

3 HyperLedger Fabric中P2P架构

而我们通过之前的知识了解到,区块链与以太坊均是公链的形式存在。而我们在实际的开发项目应用中很少会采取公链的形式。为了实现解决某系特殊场合的要求,超级账本孕育而生。虽然 Fabric 没有实现去中心化,却可以通过划分不 同节点之间的工作负载实现优化网络效率。为了保证区块链网 络的安全性、可信赖性及可测量性,Fabric 采用 Gossip 作为 P2P 网络传播协议。

在Fabric源码中我们可以看到:

而Gossip 支持超级节点网络架构,超级节点具有稳定的网络服务和计算处理能力。超级节点负责Fabric的交易排序和新区块广播功能,维护 Fabric 网络信息更新、节点列表管理等内容。

简单来说,Gossip协议如同上述区块链、以太坊中的方法一样,它保证了新区块能够顺利的加入网络。

下面我们就具体的分析一下Fabric中的源码。

(1)种子节点
Fabric采用的不是硬编码的形式,而是配置文件的形式通过 core.yaml 配置文件进行配置。

而这种机制可以保证系统在无需重启的情况下更新配置信息并使其生效。

而Fabric中种子节点的加载流程如下:

  • 首先节点启动Gossip 服务 NewGossipService( )方法,在服务 启动过程中调用 g.connect2BootstrapPeers( )方法加载种子节点。入下图:
func NewGossipService(conf *Config, s *grpc.Server, sa api.SecurityAdvisor,
    mcs api.MessageCryptoService, selfIdentity api.PeerIdentityType,
    secureDialOpts api.PeerSecureDialOpts) Gossip {
    var err error

    lgr := util.GetLogger(util.LoggingGossipModule, conf.ID)

    g := &gossipServiceImpl{
        selfOrg:               sa.OrgByPeerIdentity(selfIdentity),
        secAdvisor:            sa,
        selfIdentity:          selfIdentity,
        presumedDead:          make(chan common.PKIidType, presumedDeadChanSize),
        disc:                  nil,
        mcs:                   mcs,
        conf:                  conf,
        ChannelDeMultiplexer:  comm.NewChannelDemultiplexer(),
        logger:                lgr,
        toDieChan:             make(chan struct{}, 1),
        stopFlag:              int32(0),
        stopSignal:            &sync.WaitGroup{},
        includeIdentityPeriod: time.Now().Add(conf.PublishCertPeriod),
    }
    g.stateInfoMsgStore = g.newStateInfoMsgStore()

    g.idMapper = identity.NewIdentityMapper(mcs, selfIdentity, func(pkiID common.PKIidType, identity api.PeerIdentityType) {
        g.comm.CloseConn(&comm.RemotePeer{PKIID: pkiID})
        g.certPuller.Remove(string(pkiID))
    }, sa)

    if s == nil {
        g.comm, err = createCommWithServer(conf.BindPort, g.idMapper, selfIdentity, secureDialOpts, sa)
    } else {
        g.comm, err = createCommWithoutServer(s, conf.TLSCerts, g.idMapper, selfIdentity, secureDialOpts, sa)
    }

    if err != nil {
        lgr.Error("Failed instntiating communication layer:", err)
        return nil
    }

    g.chanState = newChannelState(g)
    g.emitter = newBatchingEmitter(conf.PropagateIterations,
        conf.MaxPropagationBurstSize, conf.MaxPropagationBurstLatency,
        g.sendGossipBatch)

    g.discAdapter = g.newDiscoveryAdapter()
    g.disSecAdap = g.newDiscoverySecurityAdapter()
    g.disc = discovery.NewDiscoveryService(g.selfNetworkMember(), g.discAdapter, g.disSecAdap, g.disclosurePolicy)
    g.logger.Info("Creating gossip service with self membership of", g.selfNetworkMember())

    g.certPuller = g.createCertStorePuller()
    g.certStore = newCertStore(g.certPuller, g.idMapper, selfIdentity, mcs)

    if g.conf.ExternalEndpoint == "" {
        g.logger.Warning("External endpoint is empty, peer will not be accessible outside of its organization")
    }

    go g.start()
    go g.connect2BootstrapPeers()

    return g
}
  • 之后通过g.conf.BootstrapPeers( )方法读取了core.yaml配置文件并获得了bootstrap超级节点的值。
func (g *gossipServiceImpl) connect2BootstrapPeers() {
    for _, endpoint := range g.conf.BootstrapPeers {
        endpoint := endpoint
        identifier := func() (*discovery.PeerIdentification, error) {
            remotePeerIdentity, err := g.comm.Handshake(&comm.RemotePeer{Endpoint: endpoint})
            if err != nil {
                return nil, errors.WithStack(err)
            }
            sameOrg := bytes.Equal(g.selfOrg, g.secAdvisor.OrgByPeerIdentity(remotePeerIdentity))
            if !sameOrg {
                return nil, errors.Errorf("%s isn't in our organization, cannot be a bootstrap peer", endpoint)
            }
            pkiID := g.mcs.GetPKIidOfCert(remotePeerIdentity)
            if len(pkiID) == 0 {
                return nil, errors.Errorf("Wasn't able to extract PKI-ID of remote peer with identity of %v", remotePeerIdentity)
            }
            return &discovery.PeerIdentification{ID: pkiID, SelfOrg: sameOrg}, nil
        }
        g.disc.Connect(discovery.NetworkMember{
            InternalEndpoint: endpoint,
            Endpoint:         endpoint,
        }, identifier)
    }

}
  • 随后启动连接 g.disc.Connect( )方法,将 endpoint 作为参 数传入。

  • 使用 d.createMembershipRequest( )方法生成请求信息。

  • 将 endpoint 和自身 PKIid 组合,利用 go d.sendUntilAcked( )
    方法将 req 信息发送至对应 endpoint。

func (d *gossipDiscoveryImpl) Connect(member NetworkMember, id identifier) {
    for _, endpoint := range []string{member.InternalEndpoint, member.Endpoint} {
        if d.isMyOwnEndpoint(endpoint) {
            d.logger.Debug("Skipping connecting to myself")
            return
        }
    }

    d.logger.Debug("Entering", member)
    defer d.logger.Debug("Exiting")
    go func() {
        for i := 0; i < maxConnectionAttempts && !d.toDie(); i++ {
            id, err := id()
            if err != nil {
                if d.toDie() {
                    return
                }
                d.logger.Warningf("Could not connect to %v : %v", member, err)
                time.Sleep(d.reconnectInterval)
                continue
            }
            peer := &NetworkMember{
                InternalEndpoint: member.InternalEndpoint,
                Endpoint:         member.Endpoint,
                PKIid:            id.ID,
            }
            m, err := d.createMembershipRequest(id.SelfOrg)
            if err != nil {
                d.logger.Warningf("Failed creating membership request: %+v", errors.WithStack(err))
                continue
            }
            req, err := m.NoopSign()
            if err != nil {
                d.logger.Warningf("Failed creating SignedGossipMessage: %+v", errors.WithStack(err))
                continue
            }
            req.Nonce = util.RandomUInt64()
            req, err = req.NoopSign()
            if err != nil {
                d.logger.Warningf("Failed adding NONCE to SignedGossipMessage %+v", errors.WithStack(err))
                continue
            }
            go d.sendUntilAcked(peer, req)
            return
        }

    }()
}
  • 得到范围信息并更新地址数据库。

(2)地址数据库详情

Fabric 采用超级节点的形式,都需要接入超级节点。在加载地址的时候,节点只需要验证super节点的存在并去那里取地址即可。节点通过解析收到 消息,检查节点是否正常,维护节点列表,不仅如此还定时与 连接节点通信,一旦被连接节点超过配置时间没有响应,则将 其移出节点列表,加入离线列表。

(3)地址广播

通过函数g.syncDiscovery( )进行循环查找节点。并选择其中的n个节点进行pusb列表。之后通过每个节点的合作使地址传至整个网络。

二、P2P网络攻击

1 女巫攻击

在公有链中,由于没有身份认证的机构,所以用户创建节点是不需要代价的,也就是说攻击者可以十分轻易地去伪造身份加入网络。之后他们会试图去获取网络中大量的节点IP,并根据此做出一些恶意行为,例如发出虚假节点信息、误导节点间的正常信息传递以及延缓矿工的挖矿进程等。

Sybil攻击(女巫攻击)最初是由Douceur在点对点网络环境中提出的,他指出这种攻击破坏了分布式存储系统中的冗余机制,并提出直接身份验证和间接身份验证两种验证方式。后来,Chris Karlof等人指出Sybil攻击对传感器网络中的路由机制同样存在着威胁。

当攻击者掌握了足够多的节点信息后,其就可以发起女巫攻击。

当大量的Sybil节点进入网络中,这些节点就可以假冒比特币的全节点并故意不进行相应请求。这也就使得其他节点必须进行等待,从而拖慢整个网络的进程。具体来说,Sybil攻击对区块链网络的影响主要体现在以下几个方面,详细内容可以查看区块链攻击

  • 1 虚假节点加入:在遵循区块链网络协议的基础上,任何网络节点都可以向区块链网络发送节点加入请求消息;收到请求消息的区块链节点会立即做出响应,回复其邻居节点信息。利用这个过程,Sybil攻击者就可以获取大量的区块链网络节点信息来分析区块链网络拓扑,以便更高效地对区块链网络进行攻击或破坏。

  • 2 误导区块链网络节点的路由选择:节点间路由信息的实时交互是保证区块链网络正常运行的关键因素之一。节点只需定时地向其邻居节点宣告自己的在线情况,就能保证自己被邻居节点加入到其路由表中。恶意的Sybil入侵者通过这个过程,可以入侵正常区块链节点的路由表,误导其路由选择。

  • 3 虚假资源发布:Sybil攻击者一旦入侵区块链网络节点的路由表,就可以随意发布自己的虚假资源。

而攻击者将大量的Sybil节点注入网络后,它就可以进一步进行攻击。例如Eclipse攻击。

2 Eclipse攻击

此攻击通常要配合Sybil攻击。攻击者通过侵占网络中节点的路由,将足够多的虚假节点添加到某个受害者周围,从而将正常节点隔离在区块链外部。入下图:

联盟链常用的拜占庭容错算法PBFT能够抵抗的拜占庭节点数是N≥3f+1。因此,在具有身份认证的区块链中,节点的数量的比例是非常重要的。而女巫攻击就是直接针对这种特性——直接在节点的数量上做文章。

当某节点被Eclipse攻击时,节点也就被攻击者变相的控制了,它会给你施加一层朦胧的现象,让节点处于攻击者的幻术之中。之后恶意节点就可以进一步实施路由欺骗、拒绝服务等。

Eclipse攻击对区块链网络的影响十分重大。对于区块链网络来说,Eclipse攻击破坏了网络的拓扑结构,减少了节点数目,使得区块链网络资源共享的效率大大降低,在极端情况下,它能完全控制整个区块链网络,把它分隔成若干个区块链网络区域。对于受害的区块链节点来说,它们在未知的情况下脱离了区块链网络,所有区块链网络请求消息都会被攻击者劫持,所以它们得到的回复信息大部分都是虚假的,无法进行正常的资源共享或下载。

而网络攻击都是层层递进的,攻击会进行扩展。

  • 1 区块竞争

    在区块链系统中,多名矿工会同时发现区块,这时候就会有区块竞争的情况产生。正常的结果是一个区块被写入区块链,而给与发现它的人奖励。然而另一个“孤块”将会被忽略,发现他的矿工就不会得到奖励。所以我们可以使用Eclipse攻击来达到上述效果。比如当恶意节点们侵蚀了正常节点后,一旦攻击者发现了竞争区块,它就会使用Eclipse攻击的节点疯狂访问竞争对手,导致正常区块无法及时的在区块链网络中同步。
  • 2 自私挖矿攻击

作为一个普通矿机并且找到它们后立刻向网络出块,攻击者可以选择性的出块,有时牺牲他自己的收人但常常突然出更多的块,从而迫使剩余的网络丢块和失去收入。这在短期内减少了攻击者的收入,但这减少其他所有人更多的收入,因此中立的节点现在有动机加入攻击者的队伍来增加他们的收入。最终,攻击者的队伍扩大到超过50%的大小,可能给了攻击者对这个网络的高度控制权。

当攻击者挖出新的区块后,并不会立即发布到网上,而是实现保存起来。这也就使其余的矿工仍然努力的浪费资源挖这个区块。**但是读者会问:这难道没有风险吗?万一其他的挖矿者挖到后抢先发布了怎么办?**

所以,这个攻击就要求攻击者时刻对其他矿池记性监督工作。当预测到其他诚实矿池要广播新区块时,他就利用被Eclipse攻击的节点来拖慢这个进程,同时抢先广播其他所私藏的区块。

更进一步的分析请参考文章:自私挖矿

3 DDos攻击

根据百度百科介绍:DDos为

分布式`拒绝服务攻击`指借助于客户/服务器技术,将多个计算机联合起来作为攻击平台,对一个或多个目标发动DDoS攻击,从而成倍地提高拒绝服务攻击的威力。通常,攻击者使用一个偷窃帐号将DDoS主控程序安装在一个计算机上,在一个设定的时间主控程序将与大量代理程序通讯,代理程序已经被安装在网络上的许多计算机上。代理程序收到指令时就发动攻击。利用客户/服务器技术,主控程序能在几秒钟内激活成百上千次代理程序的运行。

传统的DDoS攻击分为两步:第一步利用病毒、木马、缓冲区溢出等攻击手段入侵大量主机,形成僵尸网络;第二部通过僵尸网络发起DoS攻击。由于各种条件限制,攻击的第一步成为制约DDoS攻击规模和效果的关键。由于第一步的难度极大,所以DDos攻击也没有十分的常见。

而在区块链中,DDos又不同于常见的中心化系统。新型的DDoS攻击不需要建立僵尸网络即可发动大规模攻击,不仅成本低、威力巨大,而且还能确保攻击者的隐秘性。

我们知道,大型的区块链平台,如比特币系统,具有百万级的用户同时在线。而这些节点在挖矿的同时提供了大量计算、存储资源。如果我们能够利用系统中的这些大量可用资源发动攻击的话,那么效果将是十分惊人的。而由于区块链的特性,本来系统中的流量就十分巨大,那么我们稍微进行一些攻击就足以导致系统瘫痪。

对于区块链来说,DDos攻击可以分为下面两种:

  • 1 主动攻击
  • 2 被动攻击

主动攻击:通过主动向网络中发送大量的虚假消息,通过区块链的push机制使反射节点瞬间收到大量的通知消息。由于区块链节点的不易于分析和记录,所以攻击可以通过假冒源地址避过IP检查,使得追踪定位攻击源更加困难。并且大量的流量流经网络,会导致网络的路由功能的下降。

被动攻击:被动攻击不同于主动,它就像服务器那样静静的等待鱼上钩。也就是说他会等待来自其它节点的查询请求,再通过返回虚假响应来进行攻击。在真实环境中,攻击者常常部署多个攻击节点、在一个响应消息中多次包含目标主机、结合其它协实现漏洞。与上面的攻击相对立,此攻击基于了pull的机制,由于这种攻击时长处于等待,所以攻击危害不算很大。

三、密码学防御方案--协同多方计算

在保卫区块链P2P网络安全时,我们要预防一下几种挑战:

1,反Sybil攻击

我们所说的反Sybil攻击就是要确保在共识机制中的投票参与者都有且仅有一个账户。如果参与者有多个账户的话会存在投票的不公平行为出现。反Sybil攻击的解决方案有,中心化的身份预言机,或者社交验证,它是依赖于人和人之间的社交网络来进行验证。

2,共谋挑战

正如人类社会那样,当村中投票选举村长的时候,领导者会到处拉选票。有可能也有贿赂等现象出现,谁给的好处多谁就当选等。在区块链的投票中也存在类似的威胁。当攻击者控制了大量的伪节点时,他就有可能操控整个投票机制。那如何解决这个问题呢?

如果阻止这种攻击,就需要让任何人没有办法向别人证明自己是怎么投票的。“如果我们设计一种机制,让被投票的人不知道这个票你最终投给了谁,这样的贿赂形式也就不存在了。当然这一点比较难以实现。”在密码学中,存在一种算法——MPC(多方计算),除了最终的投票结果之外,所有参与者都无法看到保密的信息和计算的过程。

那么什么是安全多方计算呢?

其来源于经典应用场景,在此协议中,两方或者多方参与者基于其自己的私钥进行计算。并且他们都不想知道其他人的信息。下面我们就结合两个场景具体的讲述一下安全多方计算的具体内容。

  • 1 平均工资问题

A,B,C,D四个人想知道他们的平均工资,但是任何人都不想暴露自己的工资。如何做?

1,Alice生成一个随机数,将其与自己的工资相加,用Bob的公钥加密发送给Bob
2,Bob用自己的私钥解密,加进自己的工资,然后用Carol的公钥加密发送给Carol
3,Carol用自己的私钥解密,加进自己的工资,然后用Dave的公钥加密发送给Dave
4,Dave用自己的私钥解密,加进自己的工资,然后用Alice的公钥加密发送给Alice
5,Alice用自己的私钥解密,减去原来的随机数得到工资总和
6,Alice将工资总和除以人数得到平均工资,宣布结果

假设上述节点均诚实(尤其是A),那么我们可以用安全多方计算解决问题。任何人都没有暴露自己的工资,也知道了自己最终的结果。

那么如何防止A节点作恶呢?

此时可以使用“比特位承诺”。运用比特承诺协议让Alice向Bob传送他的随机数协议结束后,Bob可以获知Alice的工资。

  • 2 密码学家晚餐问题
场景描述:

三个密码学家(AliceBobCarol)坐在他们最喜欢的三星级餐馆准备吃晚餐。

业务逻辑:
侍者通知他们晚餐需匿名支付账单其中一个密码学家可能正在付账
可能已由美国国家安全局NSA付账他们彼此尊重匿名付账的权利,但又需要知道是不是NSA在付账。

系统目标:
如何确定三者之一在付账同事又要保护付账者的匿名性???? ?

一个简单有效的解决方案

1 每个密码学家将菜单放置于左边而互相隔离开来
2 每个人只能看到自己和右边密码学家的结果
3 每个密码学家在他和右边密码学家之间抛掷一枚硬币
4 每个密码学家广播她能看到的两枚硬币是同一面还是不同的一面

“如果有一个密码学家付账,则他说相反的结果”

判定结果:

  • 桌上说“不同”的人数为奇数:某个密码学家在付账

  • 桌上说“不同”的人数为偶数:NSA在付账

简单分析的话,加入是NSA付账,那么ABC均为合法的点。那么A--相同;B--不同;C--不同。不同的为偶数,那么NSA付账没毛病。

加入A付款,那么ABC均说不同。“不同”为奇数,那么某个密码学家付款也没毛病。

但是并不知道具体的付账人是谁。所以有一下公式:

Crypt(i)  与Coin(i)分别表示密码学家与投硬币结果。

Crypt(i) 付款输出= Coin(i-1)  or Coin(i)

Crypt(i) 没付款输出= Coin(i-1)  or Coin(i) or 1

即A是能够知道详细的结果的。

这些个问题可以放入我们的区块链中进行类比。我们知道我们想知道系统中是否存在作恶点,但是我们又不能把每个节点的投票结果拿到,所以我们可以仿照上述的多方计算进行协议设置。这样我们就可以解决一些作恶问题了。

四、参考链接

本稿为原创稿件,转载请标明出处。谢谢。

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖