侧边栏壁纸
  • 累计撰写 71 篇文章
  • 累计创建 87 个标签
  • 累计收到 5 条评论

目 录CONTENT

文章目录

长安链共识异常注入兼容方案设计

KunkkaWu
2024-01-04 / 0 评论 / 0 点赞 / 3,418 阅读 / 3,198 字 / 正在检测是否收录...

1. 背景介绍

目前consensus-attack库,基于TBFT共识算法的异常注入功能已实现。但是需要依赖于对consensus-tbft代码库的引入。通过gomonkey对原consensus-tbft代码库中的函数实现打桩替换。使得chainmaker-go在运行时,原本的共识逻辑被篡改,从而实现共识算法的异常注入。

1.1 兼容问题

由于目前gomonkey提供的打桩函数替换功能,需要提供原始函数。因此consensus-attack在对gomonkey使用的时候,需要引入consensus-tbft代码库。

目前consensus-tbft代码库:300版本和23x版本差异比较大。如果需要支持对这两个版本的共识算法实现异常注入,那么就需要引入不同版本的consensus-tbft代码库。但是不同版本的代码库对common等库的依赖有冲突。

因此,如何解决:

  1. 共识异常注入兼容两个不同版本 ?
  2. 共识异常注入支持多种不同的共识算法?

2. pclntab & moduledata

pclntab 全名是 Program Counter Line Table,可直译为 程序计数器行数映射表, 在 Go 中也叫 Runtime Symbol Table, 所以我把它里面包含的信息叫做 RTSI(Runtime Symbol Information)

Moduledata 在 Go 二进制文件中也是一个更高层次的数据结构,它包含很多其他结构的索引信息,可以看作是 Go 二进制文件中 RTSI(Runtime Symbol Information) 和 RTTI(Runtime Type Information) 的 地图

moduledata中的ftab []functab中保存了所有函数的地址。因此,可以通过遍历函数地址,获取到我们所需要的目标函数。

因此,可以使用moduledata来根据函数名称获取函数的地址,就可以避免对原始公式算法包的引入。

3. 方案设计

3.1 Golang版本兼容设计

由于不同版本的Golang,在对moduledata的实现和使用上是有差异的。因此针对不同版本的Golang在实现对moduledata解析所提供的功能方法也是有所差异的。因此,通过定义接口来约束moduledata解析器。

3.2 Moduledata解析器接口定义

// ModuledataParser 通过moduledata获取运行时函数信息
type ModuledataParser interface {
    // GetFuncUintPtrByName 通过函数名获取函数起始地址
    GetFuncUintPtrByName(string) uintptr
    // GetVarUintPtrByName 通过变量名称获取变量地址
    GetVarUintPtrByName(string) uintptr
    // SupportVersions 获取当前支持的golang版本
    SupportVersions() []string
}

3.3 不同版本实现Moduledata解析器

文件 parser118.go 针对go1.18版本的moduledata解析器实现:

var (
    // parser118 支持的版本列表
    parser118SupportVersion = []string{Version118, Version119}
)

// parser118 实现了ModuledataParser接口
type parser118 struct {
    supportVersions []string
}

func NewParser118() ModuledataParser {
    return &parser118{
        supportVersions: parser118SupportVersion,
    }
}

// GetFuncUintPtrByName 通过函数名称获取函数起始地址
func (h *parser118) GetFuncUintPtrByName(name string) uintptr {
    for dataP := &firstmoduledata; dataP != nil; dataP = dataP.next {
        for i := 0; i < len(dataP.ftab); i++ {
            fp := runtime.FuncForPC(uintptr(dataP.ftab[i].entryoff) + dataP.text)
            if name == fp.Name() {
                //file, line := fp.FileLine(uintptr(dataP.filetab[i]))
                //fmt.Printf("name: %s, entry: %x, file:%s, line:%d\n", fp.Name(), fp.Entry(), file, line)
                entry := fp.Entry()
                return entry
            }
        }
    }
    return 0
}

// GetVarUintPtrByName 通过变量名称获取变量的地址
func (h *parser118) GetVarUintPtrByName(name string) uintptr {
    return 0
}

// SupportVersions 支持的版本列表
func (h *parser118) SupportVersions() []string {
    return h.supportVersions
}

3.4 版本控制设计

实现的每个版本moduledata解析器,都需要注册到Version控制器中,版本控制器提供方法根据当前Golang的版本,选择出适合的moduledata解析器。

// versionCtrl 版本控制器
type versionCtrl struct {
    parsers []func() ModuledataParser
}

// ChooseHackerByGoVersion 根据当前环境go版本,选择对应的hacker
func (v *versionCtrl) ChooseParserByGoVersion() ModuledataParser {
    versionParts := strings.Split(runtime.Version(), ".")
    major, _ := strconv.Atoi(versionParts[0][2:])
    minor, _ := strconv.Atoi(versionParts[1])
    if major < 1 || (major == 1 && minor < 18) {
        // 版本小于1.18
        return v.getParserByVersion(Version116)
    }
    return v.getParserByVersion(Version118)
}

func (v *versionCtrl) getParserByVersion(version string) ModuledataParser {
    for _, hack := range v.parsers {
        hacker := hack()
        versions := hacker.SupportVersions()
        for _, ver := range versions {
            if ver == version {
                return hacker
            }
        }
    }
    return nil
}

3.5 数据注入设计

通用的异常注入框架,不再需要对共识算法和版本的代码引入。但是需要实现通用的接口,来实现注入的数据源。

任意一个共识算法的注入攻击,都必须先实现接口提供攻击的对象等信息。

const (
    // InjectTypeFunc 注入类型为函数或者方法
    InjectTypeFunc = InjectType("func")
    // InjectTypeVar 注入类型为 变量
    InjectTypeVar = InjectType("var")
)

// InjectType 注入类型
type InjectType string

type Injection interface {
    // InjectReplacerMap 攻击函数映射: 目标函数名 => 新函数
    // 例如:main.(*person).say => {Type:"func", Replacer:attackSay()}
    InjectReplacerMap() map[string]InjectReplacer
    // ConsensusType 共识类型
    ConsensusType() consensus.Type
    // ConsensusVersion 共识版本
    ConsensusVersion() consensus.Version
}

// InjectReplacer 替换的新结构
// Type:func 时: Replacer : attackSay()
// Type: var 时:Replacer:var
type InjectReplacer struct {
    Type     InjectType
    Replacer interface{}
}

对于任意一个共识算法的注入,需要实现接口Injection。 其中InjectReplacerMap() 方法约束了,攻击目标名称和替换后新的对象。

  • InjectReplacerMaptypefunc的时候, Replacer为替换的函数。
  • InjectReplacerMaptypevar的时候, Replacer为替换的变量。

3.6 注入攻击设计

共识异常注入consensusHacker 需要实现Hacker接口。接口中约束了四个方法。

// hacker 注入攻击
type hacker interface {
    // Attack 攻击指定的函数
    Attack(targetName string)
    // Recover 撤销指定函数的攻击
    Recover(targetName string)
    // AttackingList 正在被攻击的函数列表
    AttackingList() []string
    // RecoverAll 撤销所有攻击
    RecoverAll()
}

consensusHacker的实现示例:

func NewConsensusHacker(injection consensus.Injection, logger *logger.CMLogger) *consensusHacker {
    ch := &consensusHacker{
        logger: logger,
    }
    // 检查注入的共识和版本是否合法
    ty := injection.ConsensusType()
    ver := injection.ConsensusVersion()
    if !consensus.CheckTypeAndVersionValid(ty, ver) {
        ch.logger.Errorf("check type [%s] and version [%s] failed! ", ty, ver)
        return nil
    }
    ch.ConsensusInjection = injection
    // 通过版本控制器,获取合适的解析器
    versionCtrl := parser.NewVersionCtrl()
    ch.Parser = versionCtrl.ChooseParserByGoVersion()
    ch.logger.Infof("New Consensus Hacker For [%s : %s]", ty, ver)
    return ch
}

// consensusHacker 共识算法入侵
type consensusHacker struct {
    ConsensusInjection consensus.Injection
    Parser             parser.ModuledataParser
    monkeyPatches      map[string]*monkey.Patches
    logger             *logger.CMLogger
}

// Attack 攻击目标函数
func (c *consensusHacker) Attack(targetName string) {
    funcMap := c.ConsensusInjection.InjectReplacerMap()
    if f, exist := funcMap[targetName]; exist {
        var uintPtr uintptr
        switch f.Type {
        case consensus.InjectTypeFunc:
            uintPtr = c.Parser.GetFuncUintPtrByName(targetName)
            // TODO 对 f.Replacer 类型检查

        case consensus.InjectTypeVar:
            uintPtr = c.Parser.GetVarUintPtrByName(targetName)
            // TODO 对 f.Replacer 类型检查
        }
        c.monkeyPatches[targetName] = monkey.ApplyUintPtr(uintPtr, f)
    }
}

// Recover 恢复目标函数
func (c *consensusHacker) Recover(targetName string) {
    if patches, exist := c.monkeyPatches[targetName]; exist {
        patches.Reset()
        delete(c.monkeyPatches, targetName)
    }
}

// RecoverAll 恢复所有正在攻击的函数
func (c *consensusHacker) RecoverAll() {
    for _, patches := range c.monkeyPatches {
        patches.Reset()
    }
    c.monkeyPatches = make(map[string]*monkey.Patches)
}

// AttackingList 获取正在被攻击的函数名称列表
func (c *consensusHacker) AttackingList() []string {
    var names []string
    for targetName, _ := range c.monkeyPatches {
        names = append(names, targetName)
    }
    return names
}

3.7 gomonkey改造

当前调研和使用的gomonkey如果要打桩注入,target目标,需要传入函数或者方法。因此,要实现上述通过函数名称实现对目标函数打桩注入的话,需要基于gomonkey底层提供的replace方法,实现一个ApplyUintPtr(target uintptr, double interface{}) *Patches 方法。

通过moduledata解析器,对函数名称解析处 uintptr地址,再通过新实现的ApplyUintPtr()方法,完成对目标函数的打桩注入。

4. Consensus-attack代码调整

4.1 路由

需要首先定义一个路由组,然后注册到initRouterGroup

// initTBFTRouterGroup 初始化TBFT路由组
func initTBFTRouterGroup() {
   tbftGroup := &routerGroup{name: "/consensus/tbft"}
   tbftGroup.register(http.MethodPost, "/attack", ConsensusHandler.TBFTAttackHandler)
   tbftGroup.register(http.MethodPost, "/recover", ConsensusHandler.TBFTRecoverHandler)
   tbftGroup.register(http.MethodGet, "/help", ConsensusHandler.TBFTHelper)
   tbftGroup.register(http.MethodGet, "/list", ConsensusHandler.TBFTAttackList)
   routerGroupList = append(routerGroupList, tbftGroup)
}


// 新增路由组时,参考initTBFTRouterGroup方法
func initRouterGroup() {
   initTBFTRouterGroup()

   initTxpoolNormalRouterGroup()
   initSyncRouterGroup()
}

每个路由组下,固定4个接口:/attack/recover/help/list.

  • /attack : 攻击接口,对该模块进行攻击,具体攻击函数通过参数传递并识别
  • /recover: 恢复接口,对该模块的攻击回复到原始状态
  • /help: 帮助接口,可以查看该模块的攻击详细帮助文档
  • /list: 列表接口,可以查看该模块下支持的攻击功能列表

4.2 Handler

每个模块的路由都需要绑定到对应的handler上,例如上述的TBFTRouterGroup,就需要绑定到tbft_handler中,这个文件位于modules/consensus/handler/下。

handler中会定义两个列表,分别用户存储支持攻击的函数列表

var (
    TBFTAttackHandles   = make(map[string]types.AttackHandler)
    TBFTRecoverHandlers = make(map[string]types.RecoverHandler)
)

func init() {
    TBFTAttackHandles = map[string]types.AttackHandler{
        types.VoteRandomHash:                &VoteRandomHashHandler{},
        types.VoteNilOnSuccess:              &VoteNilOnSuccessHandler{},
        types.VoteNoneOnSuccess:             &VoteNoneOnSuccessHandler{},
        types.VoteSuccessOnFail:             &VoteSuccessOnFailHandler{},
        types.ForgeNodeId:                   &ForgeNodeIdHandler{},
        types.WrongSignatureFormat:          &WrongSignatureFormatHandler{},
        types.WrongSignatureContent:         &WrongSignatureContentHandler{},
        types.ProposalAtWrongTime:           &ProposalAtWrongTimeHandler{},
        types.ProposalRandomHash:            &ProposalRandomHashHandler{},
        types.SendDifferentProposalsToNodes: &SendDifferentProposalsToNodesHandler{},
    }
    TBFTRecoverHandlers = map[string]types.RecoverHandler{
        types.RecoverAll:                    &RecoverAllHandler{},
        types.VoteRandomHash:                &RecoverVoteRandomHashHandler{},
        types.VoteNilOnSuccess:              &RecoverVoteNilOnSuccessHandler{},
        types.VoteNoneOnSuccess:             &RecoverVoteNoneOnSuccessHandler{},
        types.VoteSuccessOnFail:             &RecoverVoteSuccessOnFailHandler{},
        types.ForgeNodeId:                   &RecoverForgeNodeIdHandler{},
        types.WrongSignatureFormat:          &RecoverWrongSignatureFormatHandler{},
        types.WrongSignatureContent:         &RecoverWrongSignatureContentHandler{},
        types.ProposalAtWrongTime:           &RecoverProposalAtWrongTimeHandler{},
        types.ProposalRandomHash:            &RecoverProposalRandomHashHandler{},
        types.SendDifferentProposalsToNodes: &RecoverSendDifferentProposalsToNodesHandler{},
    }
}

在handler中通过实现TBFTAttackHandler,TBFTRecoverHandler,TBFTAttackListTBFTHelper来实现在路由中绑定的方法。

TBFTAttackHandler的实现中,首先会根据版本号,选择一个共识注入实例。然后根据传参targetTBFTAttackHandles中找到处理该攻击目标的具体功能handler。然后调用执行。

// TBFTAttackHandler 共识攻击处理
func TBFTAttackHandler(c *gin.Context) {
    // 获取攻击实例
    attackConsensus, err := getAttackConsensusByVersion(c)
    if err != nil {
        utils.Response(c, err)
        return
    }

    target := c.PostForm("target")
    if h, exist := TBFTAttackHandles[target]; exist {
        err = h.Handle(c, attackConsensus)
        utils.Response(c, err)
        return
    }

    utils.RenderJson(c, "invalid target!")
}

4.3 Attack攻击

首先基于模块,定义了一个具体Attack接口,具体的攻击需要实现这个接口。

type Attack interface {
    //ForeachFuncName -
    ForeachFuncName() error
    //GetAttacks Get the list of current attack methods
    GetAttacks() ([]string, error)
    //VoteRandomHash vote for random hash
    VoteRandomHash() error
    // 根据实际场景可以添加更多接口定义
    ……
}

实现Attack接口,需要引入attack_engine, 然后具体攻击通过将攻击的函数名称和替换函数attackMap map[string]attackReplace,提交给attack_engine来完成注入攻击。

// InjectionAttack - main attack struct
// implementation of attack_engine.InjectionAttack
type InjectionAttack struct {
    attackMap map[string]attackReplace
    logger    attack_engine.Logger
    engine    attack_engine.AttackEngine
}

因此,需要预先对每一个攻击的目标函数名称和新的函数,初始化到attackMap中。

const (
    TBFTNewVote                   = "chainmaker.org/chainmaker/tbft-engine.NewVote"
    TBFTCreatePrevoteConsensusMsg = "chainmaker.org/chainmaker/tbft-engine.createPrevoteConsensusMsg"
    TBFTVerifyBatchMsg            = "chainmaker.org/chainmaker/consensus-tbft/v3.(*Verifier).VerifyBatchMsg"
    TBFTSignVote                  = "chainmaker.org/chainmaker/consensus-tbft/v3.(*Verifier).SignVote"
    TBFTIsProposer                = "chainmaker.org/chainmaker/tbft-engine.(*ConsensusEngine).isProposer"
    TBFTSignBatchMsg              = "chainmaker.org/chainmaker/consensus-tbft/v3.(*Verifier).SignBatchMsg"
    TBFTCreateProposalTBFTMsg     = "chainmaker.org/chainmaker/tbft-engine.createProposalTBFTMsg"
    TBFTBroadCastNetMsgFunc1      = "chainmaker.org/chainmaker/consensus-tbft/v3.(*NetHandler).BroadCastNetMsg.func1"
    TBFTHasTwoThirdAny            = "chainmaker.org/chainmaker/tbft-engine.(*VoteSet).hasTwoThirdsAny"
)

var (
    attackReplacerMap = map[string]attackReplace{
        types.VoteRandomHash: {
            targetName: TBFTNewVote,
            injectType: attack_engine.InjectTypeFunc,
            placer:     NewVoteWithRandomHash,
        },
        types.VoteNilOnSuccess: {
            targetName: TBFTNewVote,
            injectType: attack_engine.InjectTypeFunc,
            placer:     NewVoteWithNilHash,
        },
        
        ……

需要注意的是:“chainmaker.org/chainmaker/tbft-engine.NewVote” 需要的完整的函数路径,可以通过

5. 代码位置

代码库 分支 说明
tbft-engine v1.0.2_qc_attack 针对tbft引擎,防止内联
consensus-tbft v3.0.0_qc_attack_new 针对tbft共识注入,函数抽离
chainmaker-go v3.0.0_qc_attack 针对sync注入,函数抽离
consensus-attack develop_attack_engine 注入功能主程序,引入注入引擎
attack-engine v1.0.0 注入引擎

上述代码库tbft-engine,consensus-tbft,chainmaker-go 为了支持注入,对代码抽离独立函数操作,用于支持注入。

6. 其他

6.1 防止内联

需要注入的函数,或者为了注入抽离成独立的函数,有可能被被Go的编译器优化内联上上一层函数中。从而导致因找不到函数的uintptr,注入失败。因此,需要对这些函数禁止编译器内联。

禁止内联
增加注释: //go:noinline

示例:

//go:noinline
func NewVote(typ tbftpb.VoteType, voter string, height uint64, round int32, hash []byte) *tbftpb.Vote {
    return &tbftpb.Vote{
        Type:     typ,
        Voter:    voter,
        Sequence: height,
        Round:    round,
        Hash:     hash,
    }
}

6.2 注入函数格式统一

  • 函数注入: 可以直接使用函数的方式注入
  • 方法注入: 注入函数的第一个参数,必须是方法对应的结构体对象。否则,可能导致无法捕获新函数参数

建议

  1. 不需要抽离函数: 注入的新函数,第一个参数为结构体对象
  2. 需要抽离函数: 抽离成函数,而不是结构体方法
0

评论区