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等库的依赖有冲突。
因此,如何解决:
- 共识异常注入兼容两个不同版本 ?
- 共识异常注入支持多种不同的共识算法?
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()
方法约束了,攻击目标名称和替换后新的对象。
- 当
InjectReplacerMap
中type
为func
的时候,Replacer
为替换的函数。 - 当
InjectReplacerMap
中type
为var
的时候,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
,TBFTAttackList
和TBFTHelper
来实现在路由中绑定的方法。
在TBFTAttackHandler
的实现中,首先会根据版本号,选择一个共识注入实例。然后根据传参target
从TBFTAttackHandles
中找到处理该攻击目标的具体功能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 注入函数格式统一
- 函数注入: 可以直接使用函数的方式注入
- 方法注入: 注入函数的第一个参数,必须是方法对应的结构体对象。否则,可能导致无法捕获新函数参数
建议
- 不需要抽离函数: 注入的新函数,第一个参数为结构体对象
- 需要抽离函数: 抽离成函数,而不是结构体方法
评论区