CVE-2010-5141比特币任意盗币漏洞


笔者注:

前段时间看到CNVD发了一个新洞,CNVD-2020-30018:https://www.cnvd.org.cn/flaw/show/CNVD-2020-30018,比特币存在逻辑缺陷漏洞。一看漏洞细节不就是这个远古的比特币任意盗币漏洞,两家的编年号差了十年,不由得让人感到唏嘘。故把之前写过的分析文章整理整理,也是给自己一个警示。

苟日新,日日新,又日新。

–《礼记·大学》

漏洞分析

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 >* pvStackRet)
{
    CAutoBN_CTX pctx;
    CScript::const_iterator pc = script.begin();
    CScript::const_iterator pend = script.end();
    CScript::const_iterator pbegincodehash = script.begin();
    vector vfExec;
    vector stack;
    vector 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& 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指令都会被压入栈中,遍历完指针。由于此时栈不为空且栈顶元素不为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 > 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验证。

参考文献

[1]https://en.bitcoin.it/wiki/Common_Vulnerabilities_and_Exposures

[2]https://mp.weixin.qq.com/s/VT8P3_RQGgLKLDgnxPfeXg


Author: Hyun Wen
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source Hyun Wen !
  TOC