零时科技 | CNVD-2020-30018,比特币存在逻辑缺陷漏洞

2020-05-06,国家信息安全漏洞共享平台发布编号为CNVD-2020-30018,比特币存在逻辑缺陷漏洞:。比特币在某一函数实现过程中存在代码逻辑缺陷漏洞。攻击者可以利用该漏洞消费他人账户的金额。...

2020-05-06,国家信息安全漏洞共享平台发布编号为CNVD-2020-30018,比特币存在逻辑缺陷漏洞: https://www.cnvd.org.cn/flaw/show/CNVD-2020-30018 。比特币在某一函数实现过程中存在代码逻辑缺陷漏洞。攻击者可以利用该漏洞消费他人账户的金额。

经分析,为原CVE-2010-5141漏洞,对此漏洞进行详细分析。

漏洞分析

CVE-2010-5141又被叫做比特币任意盗币漏洞。bitcon v0.3.3也存在此漏洞。

首先依然是先看script.cpp,在第1114-1134行的VerifySignature函数:

bool VerifySignature(const CTransaction& txFrom, const CTransaction& txTo, unsigned int nIn, int nHashType)
{
    assert(nIn < txTo.vin.size());
    const CTxIn& txin = txTo.vin[nIn];
    if (txin.prevout.n >= txFrom.vout.size())
        return false;
    const CTxOut& txout = txFrom.vout[txin.prevout.n];

    if (txin.prevout.hash != txFrom.GetHash())
        return false;

    if (!EvalScript(txin.scriptSig + CScript(OP_CODESEPARATOR) + txout.scriptPubKey, txTo, nIn, nHashType))
        return false;

    // Anytime a signature is successfully verified, it's proof the outpoint is spent,
    // so lets update the wallet spent flag if it doesn't know due to wallet.dat being
    // restored from backup or the user making copies of wallet.dat.
    WalletUpdateSpent(txin.prevout);

    return true;
}

VerifySignature函数在执行每笔交易时都会被调用,VerifySignature在执行时会调用EvalScript函数和CScript函数来进行签名验证。

VerifySignature函数的参数有txFrom即上一笔交易、txTo即正在进行的这笔交易等。

这里先看1125行,这个判断语句来判断EvalScript函数的返回值。如果EvalScript返回False则VerifySignature返回False并退出。

对EvalScript函数,第一个参数是txin.scriptSig(包含签名信息) +CScript(OP_CODESEPARATOR)(分隔操作码)+ txout.scriptPunKey(包含公钥信息、OP_CHECKSIG指令),这里我们可以分析出只要EvalScript函数返回值不为False,VerifySignature函数返回True那么这笔交易的签名就成功通过了验证。

接下来我们看EvalScript函数,由于EvalScript函数共有762行,这里就不全部展示,我们来看最后的返回返回值是如何确定的:

if (pvStackRet)
        *pvStackRet = stack;
    return (stack.empty() ? false : CastToBool(stack.back()));

根据return语句中的三目运算符,如果栈为空则返回false,若栈不为空则进入第21行CastToBool函数:

bool CastToBool(const valtype& vch)
{
    return (CBigNum(vch) != bnZero);
}

继续看return语句,这是一个布尔类型的函数,即只要栈顶元素!= bnZero,也就是栈顶不为零就会返回一个True。

到这里我们可以得出让EvalScript函数返回True的方法:

  1. 栈不为空

  2. 栈顶不为0

所以,如何来控制栈内存放的数据呢?这里来看一下OP_CHECKSIG操作码的执行过程:

case OP_CHECKSIG:
            case OP_CHECKSIGVERIFY:
            {
                // (sig pubkey -- bool)
                if (stack.size() < 2)
                    return false;

                valtype& vchSig    = stacktop(-2);
                valtype& vchPubKey = stacktop(-1);

                ////// debug print
                //PrintHex(vchSig.begin(), vchSig.end(), "sig: %s\n");
                //PrintHex(vchPubKey.begin(), vchPubKey.end(), "pubkey: %s\n");

                // Subset of script starting at the most recent codeseparator
                CScript scriptCode(pbegincodehash, pend);

                // Drop the signature, since there's no way for a signature to sign itself
                scriptCode.FindAndDelete(CScript(vchSig));

                bool fSuccess = CheckSig(vchSig, vchPubKey, scriptCode, txTo, nIn, nHashType);

                stack.pop_back();
                stack.pop_back();
                stack.push_back(fSuccess ? vchTrue : vchFalse);
                if (opcode == OP_CHECKSIGVERIFY)
                {
                    if (fSuccess)
                        stack.pop_back();
                    else
                        pc = pend;
                }
            }

第712行,CheckSig函数会对签名进行验证,如果验证失败fSuccess = false,则在第716行的三目运算符就会把一个vchFalse即0压入栈,这时虽然栈不为空,但是栈顶元素为0,CastToBool函数依然会返回false。

看起来好像这条路走不通,我们看看传入EvalScript函数主要的三个参数:

  • txin.scriptSig ::可控,签名信息
  • CScript(OP_CODESEPARATOR) :分割操作码
  • txout.scriptPubKey : 上一个交易的密钥,不可控。

看到这里。我们发现能够控制的参数就是这个txin.scriptSig,那如何来构造他达到我们的目的呢?跟进EvalScript函数来看看他是怎么执行的:

bool EvalScript(const CScript& script, const CTransaction& txTo, unsigned int nIn, int nHashType,
                vector<vector<unsigned char> >* pvStackRet)
{
    CAutoBN_CTX pctx;
    CScript::const_iterator pc = script.begin();
    CScript::const_iterator pend = script.end();
    CScript::const_iterator pbegincodehash = script.begin();
    vector<bool> vfExec;
    vector<valtype> stack;
    vector<valtype> altstack;
    if (pvStackRet)
        pvStackRet->clear();


    while (pc < pend)
    {
        bool fExec = !count(vfExec.begin(), vfExec.end(), false);

        //
        // Read instruction
        //
        opcodetype opcode;
        valtype vchPushValue;
        if (!script.GetOp(pc, opcode, vchPushValue))
            return false;

        if (fExec && opcode <= OP_PUSHDATA4)
            stack.push_back(vchPushValue);

根据EvalScript的函数定义可以发现txin.scriptSig是作为script执行的,在第24行使用它的GetOp方法来判断,如果GetOp返回值为True,且opcode <= OP_PUSHDATA4,就会把vchPushValue压入栈中。这里看看GetOp方法是如何定义的,GetOp方法位于script.h,482行:

bool GetOp(const_iterator& pc, opcodetype& opcodeRet, vector<unsigned char>& vchRet) const
    {
        opcodeRet = OP_INVALIDOPCODE;
        vchRet.clear();
        if (pc >= end())
            return false;

        // Read instruction
        unsigned int opcode = *pc++;
        if (opcode >= OP_SINGLEBYTE_END)
        {
            if (pc + 1 > end())
                return false;
            opcode <<= 8;
            opcode |= *pc++;
        }

        // Immediate operand
        if (opcode <= OP_PUSHDATA4)
        {
            unsigned int nSize = opcode;
            if (opcode == OP_PUSHDATA1)
            {
                if (pc + 1 > end())
                    return false;
                nSize = *pc++;
            }
            else if (opcode == OP_PUSHDATA2)
            {
                if (pc + 2 > end())
                    return false;
                nSize = 0;
                memcpy(&nSize, &pc[0], 2);
                pc += 2;
            }
            else if (opcode == OP_PUSHDATA4)
            {
                if (pc + 4 > end())
                    return false;
                memcpy(&nSize, &pc[0], 4);
                pc += 4;
            }
            if (pc + nSize > end())
                return false;
            vchRet.assign(pc, pc + nSize);
            pc += nSize;
        }

        opcodeRet = (opcodetype)opcode;
        return true;
    }

根据比特币wiki, https://en.bitcoin.it/wiki/Script 。的约定,OP_PUSHDATA4的操作码值为78,即第21行声明的nSize变量的值为78。

| Word | Opcode | Hex | Input | Output | Description | | ------------ | ------ | ---- | --------- | ------ | ------------------------------------------------------------ | | OP_PUSHDATA1 | 76 | 0x4c | (special) | data | The next byte contains the number of bytes to be pushed onto the stack. | | OP_PUSHDATA2 | 77 | 0x4d | (special) | data | he next two bytes contain the number of bytes to be pushed onto the stack. | | OP_PUSHDATA4 | 78 | 0x4e | (special) | data | The next four bytes contain the number of bytes to be pushed onto the stack. |

按照比特币wiki对OP_PUSHDATA4的描述,接下来的四个字节包含要压入堆栈的字节数。读起来比较拗口,我们看第36行,如果opcode == OP_PUSHDATA4,我们便把nSize存到以pc[0]开始,4字节大小的内存空间中,并把pc指针向右移4位。再看第45行,将pc 到 pc + nSize指向的数据压入栈中。也就是说, 接下来四个字节包含的数字,是要压入栈中的字节数。

所以我们只要在txin.scriptSig中注入一个OP_PUSHDATA4操作码,后面txout.scriptPunKey包含的公钥信息以及OP_CHECKSIG指令都会被压入栈中,遍历完指针时,最后进行判断:

  1. 栈是否为空?不为空

  2. 栈顶元素是否为0?不为0

于是EvalScript函数因为满足条件返回true,继而VerifySignature函数也返回true,签名验证被绕过了,就可以达到任意盗币的效果。

漏洞修复

在bitcoin 0.3.7,script.cpp中的1163行,修改了本来的EvalScript函数为VerifyScript函数:

bool VerifySignature(const CTransaction& txFrom, const CTransaction& txTo, unsigned int nIn, int nHashType)
{
    assert(nIn < txTo.vin.size());
    const CTxIn& txin = txTo.vin[nIn];
    if (txin.prevout.n >= txFrom.vout.size())
        return false;
    const CTxOut& txout = txFrom.vout[txin.prevout.n];

    if (txin.prevout.hash != txFrom.GetHash())
        return false;

    if (!VerifyScript(txin.scriptSig, txout.scriptPubKey, txTo, nIn, nHashType))
        return false;

    // Anytime a signature is successfully verified, it's proof the outpoint is spent,
    // so lets update the wallet spent flag if it doesn't know due to wallet.dat being
    // restored from backup or the user making copies of wallet.dat.
    WalletUpdateSpent(txin.prevout);

    return true;
}

在1114行,增加了一个叫做VerifyScript的函数:

bool VerifyScript(const CScript& scriptSig, const CScript& scriptPubKey, const CTransaction& txTo, unsigned int nIn, int nHashType)
{
    vector<vector<unsigned char> > stack;
    if (!EvalScript(stack, scriptSig, txTo, nIn, nHashType))
        return false;
    if (!EvalScript(stack, scriptPubKey, txTo, nIn, nHashType))
        return false;
    if (stack.empty())
        return false;
    return CastToBool(stack.back());
}

这里将scriptSig和scriptPubKey分别调用EvalScript进行验证,来防止注入操作码到scriptSig绕过后面的scriptPubKey验证。

2020-05-06,国家信息安全漏洞共享平台发布编号为CNVD-2020-30018,比特币存在逻辑缺陷漏洞: https://www.cnvd.org.cn/flaw/show/CNVD-2020-30018 。比特币在某一函数实现过程中存在代码逻辑缺陷漏洞。攻击者可以利用该漏洞消费他人账户的金额。

经分析,为原CVE-2010-5141漏洞,对此漏洞进行详细分析。

漏洞分析

CVE-2010-5141又被叫做比特币任意盗币漏洞。bitcon v0.3.3也存在此漏洞。

首先依然是先看script.cpp,在第1114-1134行的VerifySignature函数:

bool VerifySignature(const CTransaction& txFrom, const CTransaction& txTo, unsigned int nIn, int nHashType)
{
    assert(nIn < txTo.vin.size());
    const CTxIn& txin = txTo.vin[nIn];
    if (txin.prevout.n >= txFrom.vout.size())
        return false;
    const CTxOut& txout = txFrom.vout[txin.prevout.n];

    if (txin.prevout.hash != txFrom.GetHash())
        return false;

    if (!EvalScript(txin.scriptSig + CScript(OP_CODESEPARATOR) + txout.scriptPubKey, txTo, nIn, nHashType))
        return false;

    // Anytime a signature is successfully verified, it's proof the outpoint is spent,
    // so lets update the wallet spent flag if it doesn't know due to wallet.dat being
    // restored from backup or the user making copies of wallet.dat.
    WalletUpdateSpent(txin.prevout);

    return true;
}

VerifySignature函数在执行每笔交易时都会被调用,VerifySignature在执行时会调用EvalScript函数和CScript函数来进行签名验证。

VerifySignature函数的参数有txFrom即上一笔交易、txTo即正在进行的这笔交易等。

这里先看1125行,这个判断语句来判断EvalScript函数的返回值。如果EvalScript返回False则VerifySignature返回False并退出。

对EvalScript函数,第一个参数是txin.scriptSig(包含签名信息) +CScript(OP_CODESEPARATOR)(分隔操作码)+ txout.scriptPunKey(包含公钥信息、OP_CHECKSIG指令),这里我们可以分析出只要EvalScript函数返回值不为False,VerifySignature函数返回True那么这笔交易的签名就成功通过了验证。

接下来我们看EvalScript函数,由于EvalScript函数共有762行,这里就不全部展示,我们来看最后的返回返回值是如何确定的:

if (pvStackRet)
        *pvStackRet = stack;
    return (stack.empty() ? false : CastToBool(stack.back()));

根据return语句中的三目运算符,如果栈为空则返回false,若栈不为空则进入第21行CastToBool函数:

bool CastToBool(const valtype& vch)
{
    return (CBigNum(vch) != bnZero);
}

继续看return语句,这是一个布尔类型的函数,即只要栈顶元素!= bnZero,也就是栈顶不为零就会返回一个True。

到这里我们可以得出让EvalScript函数返回True的方法:

  1. 栈不为空

  2. 栈顶不为0

所以,如何来控制栈内存放的数据呢?这里来看一下OP_CHECKSIG操作码的执行过程:

case OP_CHECKSIG:
            case OP_CHECKSIGVERIFY:
            {
                // (sig pubkey -- bool)
                if (stack.size() < 2)
                    return false;

                valtype& vchSig    = stacktop(-2);
                valtype& vchPubKey = stacktop(-1);

                ////// debug print
                //PrintHex(vchSig.begin(), vchSig.end(), "sig: %s\n");
                //PrintHex(vchPubKey.begin(), vchPubKey.end(), "pubkey: %s\n");

                // Subset of script starting at the most recent codeseparator
                CScript scriptCode(pbegincodehash, pend);

                // Drop the signature, since there's no way for a signature to sign itself
                scriptCode.FindAndDelete(CScript(vchSig));

                bool fSuccess = CheckSig(vchSig, vchPubKey, scriptCode, txTo, nIn, nHashType);

                stack.pop_back();
                stack.pop_back();
                stack.push_back(fSuccess ? vchTrue : vchFalse);
                if (opcode == OP_CHECKSIGVERIFY)
                {
                    if (fSuccess)
                        stack.pop_back();
                    else
                        pc = pend;
                }
            }

第712行,CheckSig函数会对签名进行验证,如果验证失败fSuccess = false,则在第716行的三目运算符就会把一个vchFalse即0压入栈,这时虽然栈不为空,但是栈顶元素为0,CastToBool函数依然会返回false。

看起来好像这条路走不通,我们看看传入EvalScript函数主要的三个参数:

  • txin.scriptSig ::可控,签名信息
  • CScript(OP_CODESEPARATOR) :分割操作码
  • txout.scriptPubKey : 上一个交易的密钥,不可控。

看到这里。我们发现能够控制的参数就是这个txin.scriptSig,那如何来构造他达到我们的目的呢?跟进EvalScript函数来看看他是怎么执行的:

bool EvalScript(const CScript& script, const CTransaction& txTo, unsigned int nIn, int nHashType,
                vector<vector<unsigned char> >* pvStackRet)
{
    CAutoBN_CTX pctx;
    CScript::const_iterator pc = script.begin();
    CScript::const_iterator pend = script.end();
    CScript::const_iterator pbegincodehash = script.begin();
    vector<bool> vfExec;
    vector<valtype> stack;
    vector<valtype> altstack;
    if (pvStackRet)
        pvStackRet->clear();

    while (pc < pend)
    {
        bool fExec = !count(vfExec.begin(), vfExec.end(), false);

        //
        // Read instruction
        //
        opcodetype opcode;
        valtype vchPushValue;
        if (!script.GetOp(pc, opcode, vchPushValue))
            return false;

        if (fExec && opcode <= OP_PUSHDATA4)
            stack.push_back(vchPushValue);

根据EvalScript的函数定义可以发现txin.scriptSig是作为script执行的,在第24行使用它的GetOp方法来判断,如果GetOp返回值为True,且opcode <= OP_PUSHDATA4,就会把vchPushValue压入栈中。这里看看GetOp方法是如何定义的,GetOp方法位于script.h,482行:

bool GetOp(const_iterator& pc, opcodetype& opcodeRet, vector<unsigned char>& vchRet) const
    {
        opcodeRet = OP_INVALIDOPCODE;
        vchRet.clear();
        if (pc >= end())
            return false;

        // Read instruction
        unsigned int opcode = *pc++;
        if (opcode >= OP_SINGLEBYTE_END)
        {
            if (pc + 1 > end())
                return false;
            opcode <<= 8;
            opcode |= *pc++;
        }

        // Immediate operand
        if (opcode <= OP_PUSHDATA4)
        {
            unsigned int nSize = opcode;
            if (opcode == OP_PUSHDATA1)
            {
                if (pc + 1 > end())
                    return false;
                nSize = *pc++;
            }
            else if (opcode == OP_PUSHDATA2)
            {
                if (pc + 2 > end())
                    return false;
                nSize = 0;
                memcpy(&nSize, &pc[0], 2);
                pc += 2;
            }
            else if (opcode == OP_PUSHDATA4)
            {
                if (pc + 4 > end())
                    return false;
                memcpy(&nSize, &pc[0], 4);
                pc += 4;
            }
            if (pc + nSize > end())
                return false;
            vchRet.assign(pc, pc + nSize);
            pc += nSize;
        }

        opcodeRet = (opcodetype)opcode;
        return true;
    }

根据比特币wiki, https://en.bitcoin.it/wiki/Script 。的约定,OP_PUSHDATA4的操作码值为78,即第21行声明的nSize变量的值为78。

Word Opcode Hex Input Output Description
OP_PUSHDATA1 76 0x4c (special) data The next byte contains the number of bytes to be pushed onto the stack.
OP_PUSHDATA2 77 0x4d (special) data he next two bytes contain the number of bytes to be pushed onto the stack.
OP_PUSHDATA4 78 0x4e (special) data The next four bytes contain the number of bytes to be pushed onto the stack.

按照比特币wiki对OP_PUSHDATA4的描述,接下来的四个字节包含要压入堆栈的字节数。读起来比较拗口,我们看第36行,如果opcode == OP_PUSHDATA4,我们便把nSize存到以pc[0]开始,4字节大小的内存空间中,并把pc指针向右移4位。再看第45行,将pc 到 pc + nSize指向的数据压入栈中。也就是说, 接下来四个字节包含的数字,是要压入栈中的字节数。

所以我们只要在txin.scriptSig中注入一个OP_PUSHDATA4操作码,后面txout.scriptPunKey包含的公钥信息以及OP_CHECKSIG指令都会被压入栈中,遍历完指针时,最后进行判断:

  1. 栈是否为空?不为空

  2. 栈顶元素是否为0?不为0

于是EvalScript函数因为满足条件返回true,继而VerifySignature函数也返回true,签名验证被绕过了,就可以达到任意盗币的效果。

漏洞修复

在bitcoin 0.3.7,script.cpp中的1163行,修改了本来的EvalScript函数为VerifyScript函数:

bool VerifySignature(const CTransaction& txFrom, const CTransaction& txTo, unsigned int nIn, int nHashType)
{
    assert(nIn < txTo.vin.size());
    const CTxIn& txin = txTo.vin[nIn];
    if (txin.prevout.n >= txFrom.vout.size())
        return false;
    const CTxOut& txout = txFrom.vout[txin.prevout.n];

    if (txin.prevout.hash != txFrom.GetHash())
        return false;

    if (!VerifyScript(txin.scriptSig, txout.scriptPubKey, txTo, nIn, nHashType))
        return false;

    // Anytime a signature is successfully verified, it's proof the outpoint is spent,
    // so lets update the wallet spent flag if it doesn't know due to wallet.dat being
    // restored from backup or the user making copies of wallet.dat.
    WalletUpdateSpent(txin.prevout);

    return true;
}

在1114行,增加了一个叫做VerifyScript的函数:

bool VerifyScript(const CScript& scriptSig, const CScript& scriptPubKey, const CTransaction& txTo, unsigned int nIn, int nHashType)
{
    vector<vector<unsigned char> > stack;
    if (!EvalScript(stack, scriptSig, txTo, nIn, nHashType))
        return false;
    if (!EvalScript(stack, scriptPubKey, txTo, nIn, nHashType))
        return false;
    if (stack.empty())
        return false;
    return CastToBool(stack.back());
}

这里将scriptSig和scriptPubKey分别调用EvalScript进行验证,来防止注入操作码到scriptSig绕过后面的scriptPubKey验证。

本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 16分钟前
  • 阅读 ( 3 )
  • 学分 ( 0 )
  • 分类:安全/漏洞
我来评几句
登录后评论

已发表评论数()

相关站点

热门文章