leonwxqian 发布的文章

一个Windbg/cdb极其缓慢的例子

最近做自动dump分析的时候,突然发现cdb慢到难以忍受,我启动的参数是:

"C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe" -y cache*D:\symbol -z %%s

而且我分析的dump的核心dump我也确定已经缓存到了d:\symbol中。但是cdb.exe依然以难以忍受的速度(平均30分钟分析一个文件),在一番抓包以后,我惊讶的发现cdb.exe仍然试图从一些网络服务器爬取数据。

很快我就开始怀疑_NT_SYMBOL_PATH的环境变量,果不其然,对于cdb.exe来说,这个环境变量居然优先于命令行传入的值。

我在_NT_SYMBOL_PATH设置的值为:

cache*D:\symbol;SRV*https://msdl.microsoft.com/download/symbols;SRV*https://chromium-browser-symsrv.commondatastorage.googleapis.com;\\PRIVATE_SYMBOL_SERVER

但很快就能发现不对的是,可以发现它在\PRIVATE_SYMBOL_SERVER遍历目录,通过查询MSDN(https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/symbol-path)得知,针对有结构的symbol server要加上SRV*,这样才能避免直接去目录下疯狂遍历搜索。 所以修正后的SYMBOL PATH应当为:

cache*D:\symbol;SRV*https://msdl.microsoft.com/download/symbols;SRV*https://chromium-browser-symsrv.commondatastorage.googleapis.com;SRV*\\PRIVATE_SYMBOL_SERVER

修改CppCheck,做自己的扫描器 - 03

上一篇文章中我们已经分析了,CppCheck是一个以Token为驱动的静态扫描器。让我们以一个简单的例子来说。

while(*src++ = *dst++)

此类代码一向被认为不安全(strcpy()的实现),但是CppCheck在默认情况下并不能对它进行报警。如果我们想添加一条规则,该怎么做呢?

在想好方案之前,我们先完善需求,除却上面这个情况,其实还有如下几个变体。 即dst和src自增顺序的差别。不过不论如何,产生的结果都是一样的——代码是有安全风险的。

    //while(*src++ = *dst++)
    //while(*++src = *++dst)
    //while(*src++ = *++dst)
    //while(*++src = *dst++)

不过代码的原理基本相同,我们从第一个例子开始,把它拆分为多个顺序的token:

0  while
1  (
2  *
3  oprand
4  ++
5  =
6  *
7  oprand
8  ++
9  )

在token迭代中,tok始终代表当前token,那么假设当前token是while,我们只需要依次检查后面的token即可。现在,相信你也能很快地写出代码来了。

在这之前,我们先定义一个错误报告函数,保持和其他函数类似结构即可:

void CheckBufferOverrun::potentialOutOfBoundsError(const Token *tok, const std::string &what, const bool show_size_info, const MathLib::bigint &supplied_size, const MathLib::bigint &actual_size)
{
    std::ostringstream oss;

    oss << what << " might have an out-of-bounds action. Try to check the length before assign.";
    if (show_size_info)
        oss << ": Supplied size " << supplied_size << " is larger than actual size " << actual_size;
    oss << '.';
    reportError(tok, Severity::error, "outOfBounds", oss.str(), CWE788, false);
}

然后,编写代码,运行程序:

    if (tok->str() == "while" &&
        tok->tokAt(1)->str() == "(" &&
        tok->tokAt(2)->str() == "*" &&
        tok->tokAt(4)->str() == "++" &&
        tok->tokAt(5)->str() == "=" &&
        tok->tokAt(6)->str() == "*" &&
        tok->tokAt(8)->str() == "++" &&
        tok->tokAt(9)->str() == ")") {

        potentialOutOfBoundsError(tok->tokAt(3), "Dangerous operation! Oprands in while() ", false, 0, 0);
    }

对给定代码:

int main()
{
 char a[2];
 char *b = "abcdefg";
 while(*a++ = *b++);

 return 0;
}

得到以下结果:

Checking d:\test.c ...
[d:\test.c:5]: (error) Dangerous operation! Oprands in while()  might have an out-of-bounds action. Try to check the length before assign..

那么,再让我们继续“完善”代码,因为这里实际上我们并没有判断自增的对象是什么,查看token的细节,我们可以发现它是有记录token类型的,而只需要调用getTokType()==Variable,valueType()==CHAR就可以进行简单判断:

-       mValueType  0x03867260 {sign=UNKNOWN_SIGN (0) type=CHAR (7) bits=0 ...} ValueType *
        sign    UNKNOWN_SIGN (0)    ValueType::Sign
        type    CHAR (7)    ValueType::Type
        bits    0   unsigned int
        pointer 1   unsigned int
        constness   0   unsigned int

修改完以后,就可以在保证结果正确的前提下,同时减少误报率了。 当然,*(int*)++ = *(int*)++ 一样会导致安全问题,如果需要的话,也可以去除对ValueType::CHAR的比较。

   if (tok->str() == "while" &&
        tok->tokAt(1)->str() == "(" &&
        tok->tokAt(2)->str() == "*" &&
        tok->tokAt(3)->tokType() == Token::eVariable &&
        tok->tokAt(3)->valueType()->type == ValueType::CHAR &&
        tok->tokAt(4)->str() == "++" &&
        tok->tokAt(5)->str() == "=" &&
        tok->tokAt(6)->str() == "*" &&
        tok->tokAt(7)->tokType() == Token::eVariable &&
        tok->tokAt(7)->valueType()->type == ValueType::CHAR &&
        tok->tokAt(8)->str() == "++" &&
        tok->tokAt(9)->str() == ")") {

        potentialOutOfBoundsError(tok->tokAt(3), "Dangerous operation! Oprands in while() ", false, 0, 0);
    }
    // 文章来源nul.pw 作者leonwxqian

其余几个也大同小异,只需调整修改即可。
这便是使用token来进行安全检查的一种方法了,CppCheck还提供很多方式,条条大路通罗马,通过token来检查是其中一种最简单的方案了,如果你也有抽象好的安全规则,不妨通过token的方式来制作一个检测器进行一番检测。

修改CppCheck,做自己的扫描器 - 02

填坑。
CppCheck在解析代码时,会将代码视作不同的Token。Token分类大致如下:

enum Type {
    eVariable, eType, eFunction, eKeyword, eName,
    eNumber, eString, eChar, eBoolean, eLiteral, eEnumerator, 
    eArithmeticalOp, eComparisonOp, eAssignmentOp, eLogicalOp, eBitOp, eIncDecOp, eExtendedOp, 
    eBracket, 
    eOther,
    eNone
};

1、 名称类:变量名 , 类型名 , 函数名, 语言关键字, 其他无法识别的名字
2、 字面量:数字,字符串,字符,布尔值,用户定义的常量(c++11),枚举
3、 操作符:数学操作符,比较符号,赋值符号,逻辑符号,位运算,自增自减符号,扩展符号
4、 括号:{, }, <, >
5、 其他

这些token在保存时都是通过字符串的方式保存的,所以在比较的时候,也是通过字符串比较的形式,来确定是否和规则匹配。

先看一个典型的例子:

bool Token::Match(const Token *tok, const char pattern[], unsigned int varid)

以及它的调用者;

if (Token::Match(tok2, "strncpy|strncat ( %varid% ,", arrayInfo.declarationId())) {

tok2是一个Token对象
pattern为 strncpy|strncat ( %varid% 。
pattern中的 | 代表多重比较,即:multiCompare。从当前token开始依次比较,以空格为界限。也即第一次比较到strncat之后停止。
multiCompare的参数为%varid%(目标参数)。
multiCompare会一直比较,直到token耗尽或者传入的pattern到头,最后给出比较结果。

除了%varid%,multiCompare也支持许多比较,具体可以参考源代码:token.cpp -> multiComparePercent。
可以看到,在匹配、抽取值时,并不需要传入完整的token。

所以,对我们编写CppCheck规则时,了解Token的内容十分重要。

以一个实际的例子来看:

    const MathLib::biguint total_size = arrayInfo.num(0) * arrayInfo.element_size();
    if (printWarning && printInconclusive && Token::Match(tok2, "strncpy|memcpy|memmove ( %varid% , %str% , %num% )", arrayInfo.declarationId())) {
        if (Token::getStrLength(tok2->tokAt(4)) >= total_size) {
            const MathLib::biguint num = MathLib::toULongNumber(tok2->strAt(6));
            if (total_size == num)
                bufferNotZeroTerminatedError(tok2, tok2->strAt(2), tok2->str());
        }
    }

可以看到它:
1、 解析strncpy/memcpy/memmove并抽取varid(目标参数)、str(源字符串)、num(长度即操作数)。
2、 获取第5个token,即%str%处的token,获取它指向内容的长度。
3、 如果源操作数的长度大于等于缓冲区长度(由arrayInfo获取),代表可能潜在有问题。
4、 获取第7个token,即%num%处的token,获取它的值。
5、 如果缓冲区总大小等于长度,则报告缓冲区非零结尾的错误。

测试一下,给定测试用例:

int main()
{
 char a[10];
 memcpy(a, "abcdefghij", 10);

 return 0;
}

打开这三个提示信息以方便我们调试。为了方便我就不改全局的了,只在函数里面直接硬编码修改为true。

const bool printPortability = true; //mSettings->isEnabled(Settings::PORTABILITY);
const bool printWarning = true; //mSettings->isEnabled(Settings::WARNING);
const bool printInconclusive = true; // mSettings->inconclusive;

代码在我们指定的位置停下,调用栈如下:

    cppcheck-core.dll!CheckBufferOverrun::checkScope_inner(const Token * tok, const CheckBufferOverrun::ArrayInfo & arrayInfo) 行 1005   C++
>   cppcheck-core.dll!CheckBufferOverrun::checkScope(const Token * tok, std::map<unsigned int,CheckBufferOverrun::ArrayInfo,std::less<unsigned int>,std::allocator<std::pair<unsigned int const ,CheckBufferOverrun::ArrayInfo> > > arrayInfos) 行 939   C++
    cppcheck-core.dll!CheckBufferOverrun::checkGlobalAndLocalVariable() 行 1242  C++
    cppcheck-core.dll!CheckBufferOverrun::runSimplifiedChecks(const Tokenizer * tokenizer, const Settings * settings, ErrorLogger * errorLogger) 行 79   C++
    cppcheck-core.dll!CppCheck::checkSimplifiedTokens(const Tokenizer & tokenizer) 行 593    C++
    cppcheck-core.dll!CppCheck::checkFile(const std::basic_string<char,std::char_traits<char>,std::allocator<char> > & filename, const std::basic_string<char,std::char_traits<char>,std::allocator<char> > & cfgname, std::basic_istream<char,std::char_traits<char> > & fileStream) 行 430 C++
    cppcheck-core.dll!CppCheck::check(const std::basic_string<char,std::char_traits<char>,std::allocator<char> > & path) 行 83   C++

可以发现:
1) arrayInfo,为我们定义的char a[10]

-       arrayInfo   {mNum={ size=1 } mVarName="a" mElementSize=1 ...}   const CheckBufferOverrun::ArrayInfo &
-       mNum    { size=1 }  std::vector<__int64,std::allocator<__int64> >
        [capacity]  1   int
+       [allocator] allocator   std::_Compressed_pair<std::allocator<__int64>,std::_Vector_val<std::_Simple_types<__int64> >,1>
        [0] 10  __int64
+       [原始视图]  {...}   std::vector<__int64,std::allocator<__int64> >
+       mVarName    "a" std::basic_string<char,std::char_traits<char>,std::allocator<char> >
        mElementSize    1   __int64
        mDeclarationId  1   unsigned int

2) token的层级列表(向后):

-       tok 0x02bfed58 "memcpy" const Token *
+       mTokensFrontBack    0x00afed90 "int" - "}"  TokensFrontBack *
+       mStr    "memcpy"    std::basic_string<char,std::char_traits<char>,std::allocator<char> >
-       mNext   0x02bfedf0 "("  Token *
+       mTokensFrontBack    0x00afed90 "int" - "}"  TokensFrontBack *
+       mStr    "(" std::basic_string<char,std::char_traits<char>,std::allocator<char> >
-       mNext   0x02bfee88 "a"  Token *
+       mTokensFrontBack    0x00afed90 "int" - "}"  TokensFrontBack *
+       mStr    "a" std::basic_string<char,std::char_traits<char>,std::allocator<char> >
-       mNext   0x02e677e8 ","  Token *
+       mTokensFrontBack    0x00afed90 "int" - "}"  TokensFrontBack *
+       mStr    "," std::basic_string<char,std::char_traits<char>,std::allocator<char> >
-       mNext   0x02e679b0 "\"abcdefghij\"" Token *

3)token向前,可以看到这东西基本就是一个双向链表:

-       mPrevious   0x02bfe838 ";"  Token *
+       mTokensFrontBack    0x00afed90 "int" - "}"  TokensFrontBack *
+       mStr    ";" std::basic_string<char,std::char_traits<char>,std::allocator<char> >
+       mNext   0x02bfed58 "memcpy" Token *
-       mPrevious   0x02bfe7a0 "]"  Token *
+       mTokensFrontBack    0x00afed90 "int" - "}"  TokensFrontBack *
+       mStr    "]" std::basic_string<char,std::char_traits<char>,std::allocator<char> >
+       mNext   0x02bfe838 ";"  Token *
-       mPrevious   0x02bfe708 "10" Token *

所以,如果我们不想仅仅依靠正则式写那些简单的规则,就需要自己抽象出一个常见的模式,并根据模式和token来编写规则。token会逐个遍历,所以不用担心跑不到你想要找的函数那里(参考CheckBufferOverrun::checkScope)。

最后需要注意的是CppCheck是按类型来扫描,而不是按代码顺序扫描,所以由于声明全局变量顺序的问题,很有可能你会发现某个靠后的代码先行被提示问题,这都是正常的结果。

修改CppCheck,做自己的扫描器 - 01

CppCheck是一个静态语法扫描器,官方介绍:

Static analysis of C/C++ code. Checks for: memory leaks, mismatching allocation-deallocation, buffer overrun, and many more. The goal is 0% false positives. See http://cppcheck.sourceforge.net for more information.

CppCheck是一个开源工程,要对其进行修改,最好还是理清楚它的工作流程。首先,让我们对一个常见的问题进行检查,代码如下:

int a[10];
a[10] = 0;

对这个越界代码进行扫描,可以发现如下调用栈处报警:

>   cppcheck-core.dll!CheckBufferOverrun::arrayIndexOutOfBoundsError(const Token * tok, const CheckBufferOverrun::ArrayInfo & arrayInfo, const std::vector<ValueFlow::Value,std::allocator<ValueFlow::Value> > & index) 行 142   C++
    cppcheck-core.dll!CheckBufferOverrun::valueFlowCheckArrayIndex(const Token * const tok, const CheckBufferOverrun::ArrayInfo & arrayInfo) 行 878  C++
    cppcheck-core.dll!CheckBufferOverrun::bufferOverrun() 行 1593    C++
    cppcheck-core.dll!CheckBufferOverrun::runChecks(const Tokenizer * tokenizer, const Settings * settings, ErrorLogger * errorLogger) 行 91 C++
    cppcheck-core.dll!CppCheck::checkNormalTokens(const Tokenizer & tokenizer) 行 563    C++
    cppcheck-core.dll!CppCheck::checkFile(const std::basic_string<char,std::char_traits<char>,std::allocator<char> > & filename, const std::basic_string<char,std::char_traits<char>,std::allocator<char> > & cfgname, std::basic_istream<char,std::char_traits<char> > & fileStream) 行 416 C++
    cppcheck-core.dll!CppCheck::check(const std::basic_string<char,std::char_traits<char>,std::allocator<char> > & path) 行 83   C++
    cppcheck.exe!CppCheckExecutor::check_internal(CppCheck & cppcheck, int __formal, const char * const * argv) 行 871   C++
    cppcheck.exe!CppCheckExecutor::check(int argc, const char * const * argv) 行 198 C++
    cppcheck.exe!main(int argc, char * * argv) 行 95 C++

简单分析一下,基本是这几行起了作用:

    cppcheck-core.dll!CheckBufferOverrun::runChecks(const Tokenizer * tokenizer, const Settings * settings, ErrorLogger * errorLogger) 行 91 C++
    cppcheck-core.dll!CppCheck::checkNormalTokens(const Tokenizer & tokenizer) 行 563    C++
>   cppcheck-core.dll!CppCheck::checkFile(const std::basic_string<char,std::char_traits<char>,std::allocator<char> > & filename, const std::basic_string<char,std::char_traits<char>,std::allocator<char> > & cfgname, std::basic_istream<char,std::char_traits<char> > & fileStream) 行 416 C++

注意第一行:

void CppCheck::checkNormalTokens(const Tokenizer &tokenizer)
{
    // call all "runChecks" in all registered Check classes
    for (std::list<Check *>::const_iterator it = Check::instances().begin(); it != Check::instances().end(); ++it) {
        if (mSettings.terminated())
            return;

        if (tokenizer.isMaxTime())
            return;

        Timer timerRunChecks((*it)->name() + "::runChecks", mSettings.showtime, &S_timerResults);
        (*it)->runChecks(&tokenizer, &mSettings, this);
    }

也就是说,it从Check::instances()中遍历所有项目。
Check::instances中有:

    名称  值   类型
▶   [0] 0x01557438 {cppcheck-core.dll!Check64BitPortability instance} {...} Check * {Check64BitPortability}
▶   [1] 0x01557494 {cppcheck-core.dll!CheckAssert instance} {...}   Check * {CheckAssert}
▶   [2] 0x015574ec {cppcheck-core.dll!CheckAutoVariables instance} {...}    Check * {CheckAutoVariables}
▶   [3] 0x01557550 {cppcheck-core.dll!CheckBool instance} {...} Check * {CheckBool}
▶   [4] 0x015575b8 {cppcheck-core.dll!CheckBoost instance} {...}    Check * {CheckBoost}
▶   [5] 0x01557614 {cppcheck-core.dll!CheckBufferOverrun instance} {...}    Check * {CheckBufferOverrun}
▶   [6] 0x01557774 {cppcheck-core.dll!CheckFunctions instance} {...}    Check * {CheckFunctions}
▶   [7] 0x0155768c {cppcheck-core.dll!CheckClass instance} {mSymbolDatabase=0x00000000 <NULL> } Check * {CheckClass}
▶   [8] 0x0155771c {cppcheck-core.dll!CheckCondition instance} {...}    Check * {CheckCondition}
▶   [9] 0x01557864 {cppcheck-core.dll!CheckExceptionSafety instance} {...}  Check * {CheckExceptionSafety}
▶   [10]    0x015578bc {cppcheck-core.dll!CheckIO instance} {...}   Check * {CheckIO}
▶   [11]    0x01557974 {cppcheck-core.dll!CheckLeakAutoVar instance} {...}  Check * {CheckLeakAutoVar}
▶   [12]    0x01557a7c {cppcheck-core.dll!CheckMemoryLeakNoVar instance4} {...} Check * {CheckMemoryLeakNoVar}
▶   [13]    0x01557a0c {cppcheck-core.dll!CheckMemoryLeakInClass instance2} {...}   Check * {CheckMemoryLeakInClass}
▶   [14]    0x015579d4 {cppcheck-core.dll!CheckMemoryLeakInFunction instance1} {...}    Check * {CheckMemoryLeakInFunction}
▶   [15]    0x01557a44 {cppcheck-core.dll!CheckMemoryLeakStructMember instance3} {...}  Check * {CheckMemoryLeakStructMember}
▶   [16]    0x01557b34 {cppcheck-core.dll!CheckNullPointer instance} {...}  Check * {CheckNullPointer}
▶   [17]    0x01557bb0 {cppcheck-core.dll!CheckOther instance} {...}    Check * {CheckOther}
▶   [18]    0x01557d20 {cppcheck-core.dll!CheckStl instance} {...}  Check * {CheckStl}
▶   [19]    0x01557cbc {cppcheck-core.dll!CheckSizeof instance} {...}   Check * {CheckSizeof}
▶   [20]    0x015577ec {cppcheck-core.dll!CheckString instance} {...}   Check * {CheckString}
▶   [21]    0x01557e98 {cppcheck-core.dll!CheckType instance} {...} Check * {CheckType}
▶   [22]    0x01557f00 {cppcheck-core.dll!CheckUninitVar instance} {...}    Check * {CheckUninitVar}
▶   [23]    0x01557f70 {cppcheck-core.dll!CheckUnusedFunctions CheckUnusedFunctions::instance} {mFunctions={ size=0 } ...}  Check * {CheckUnusedFunctions}
▶   [24]    0x01557ff8 {cppcheck-core.dll!CheckUnusedVar instance} {mIsRecordTypeWithoutSideEffectsMap={ size=0 } ...}  Check * {CheckUnusedVar}
▶   [25]    0x01557c64 {cppcheck-core.dll!CheckPostfixOperator instance} {...}  Check * {CheckPostfixOperator}
▶   [26]    0x01558070 {cppcheck-core.dll!CheckVaarg instance} {...}    Check * {CheckVaarg}

鉴于代码表明_instalces这是一个static变量,所以必然有代码调用了Check::instances().XXXX向它推送代码。

std::list<Check *> &Check::instances()
{
#ifdef __SVR4
    // Under Solaris, destructors are called in wrong order which causes a segmentation fault.
    // This fix ensures pointer remains valid and reachable until program terminates.
    static std::list<Check *> *_instances= new std::list<Check *>;
    return *_instances;
#else
    static std::list<Check *> _instances;
    return _instances;
#endif
}

查找所有的代码以后,只有构造函数有相关代码:

Check::Check(const std::string &aname)
    : mTokenizer(nullptr), mSettings(nullptr), mErrorLogger(nullptr), mName(aname)
{
    for (std::list<Check*>::iterator i = instances().begin(); i != instances().end(); ++i) {
        if ((*i)->name() > aname) {
            instances().insert(i, this);
            return;
        }
    }
    instances().push_back(this);
}

下断点,重新执行。

    cppcheck-core.dll!Check::Check(const std::basic_string<char,std::char_traits<char>,std::allocator<char> > & aname) 行 30 C++
    cppcheck-core.dll!Check64BitPortability::Check64BitPortability() 行 46   C++
>   cppcheck-core.dll!`anonymous namespace'::`dynamic initializer for 'instance''() 行 41    C++

可以看到这样的调用栈,再往下就是程序的初始化代码了,一层层看。

1:最上层可以看到一个类似全局变量的代码

// Register this check class (by creating a static instance of it)
namespace {
    Check64BitPortability instance;
}

2:可以看到检测的类都是公开继承Check的

class CPPCHECKLIB Check64BitPortability : public Check {
public:
    /** This constructor is used when registering the Check64BitPortability */
    Check64BitPortability() : Check(myName()) {
    }

3:随后代码走到Check中,该类被加入。

再保留断点继续执行,可以发现所有的Check都是这样加入的,因为这些Check类都是全局变量,所以程序一启动就会自动添加进去。等他们执行完了才会进入main。