零时科技 | 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 < 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的方法:
-
栈不为空
-
栈顶不为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指令都会被压入栈中,遍历完指针时,最后进行判断:
-
栈是否为空?不为空
-
栈顶元素是否为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-06-02 12:56
- 阅读 ( 1277 )
- 学分 ( 29 )
- 分类:安全
评论