零时科技 | 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函数: ```c++ 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行,这里就不全部展示,我们来看最后的返回返回值是如何确定的: ```c++ if (pvStackRet) *pvStackRet = stack; return (stack.empty() ? false : CastToBool(stack.back())); ``` 根据return语句中的三目运算符,如果栈为空则返回false,若栈不为空则进入第21行CastToBool函数: ```c++ bool CastToBool(const valtype& vch) { return (CBigNum(vch) != bnZero); } ``` 继续看return语句,这是一个布尔类型的函数,即只要栈顶元素!= bnZero,也就是栈顶不为零就会返回一个True。 到这里我们可以得出让EvalScript函数返回True的方法: 1. 栈不为空 2. 栈顶不为0 所以,如何来控制栈内存放的数据呢?这里来看一下OP_CHECKSIG操作码的执行过程: ```c++ 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函数来看看他是怎么执行的: ```c++ 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行: ```c++ 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函数: ```c++ 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的函数: ```c++ 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 &lt; 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() &lt; 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&lt;vector&lt;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&lt;bool> vfExec;
    vector&lt;valtype> stack;
    vector&lt;valtype> altstack;
    if (pvStackRet)
        pvStackRet->clear();

    while (pc &lt; 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 &lt;= 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&lt;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 &lt;&lt;= 8;
            opcode |= *pc++;
        }

        // Immediate operand
        if (opcode &lt;= 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 &lt; 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&lt;vector&lt;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-06-02 12:56
  • 阅读 ( 1277 )
  • 学分 ( 29 )
  • 分类:安全

评论