Contents

排查一个同名函数引发的问题

1.前景提要

在一次代码安全审计完成后,安全部门提出一个历史遗留问题需要修改。修改的方法也很简单,只需要调用库里的函数判断一下是否有问题即可。库最终会被打包到libcgibase.so中 库函数实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 判断zip压缩包中是否存在软链接(注:只有确认有软链接才返回true,包括异常在内的其它情况均返回false)
bool is_symlink_in_zip(const char *path)
{
	if (!path) {
		return false;
	}
	
	string cmd = "";
	string result = "";
	bool ret = false;
	// unzip -Z -l跟ls -l类似,如果有软链接,则会有lrwxrwxrwx之类的以l开头的文件属性。
	ret = sprintf(cmd, "unzip -Z -l %s 2>/dev/null | tail -n +3 | grep '^l' | wc -l", safeArg(path).c_str());
	if (ret == false) {
		return false;
	}
	ret = ExecuteShell(cmd.c_str(), result);
	if (ret == false) {
		return false;
	}
	if (result == "0") {
		return false;
	}
	return true;
}

2.问题复现

在a.cpp中调用is_symlink_in_zip函数,不管压缩包中是否带有软链接,此函数的返回值都是true。 通过gdb调试断在 if(result="0”) 这一行,查看各变量结果如下:

/images/same_name_1.png

此时能看到result变量的结果为0, 但是长度为2,最后整个执行流跳到了return true 这一行。

3.问题排查

当时定了好几个排查方向:包括编译器问题、有头文件在全局作用域里实现了string operator==(const char*)的重载 、ExecuteShell函数修改result变量时把它写坏了。

3.1 问题1的排查尝试

使用干净的编译环境,问题依旧;使用其他版本的编译器,然后在另一个对应的系统中运行,问题也依旧。问题卡住,进行不下去。

3.2 问题2的排查尝试

使用grep在代码仓库里全局搜索重载符号operator==, 只有Cstring这个类在全局作用域内重载了==操作符,但是result是一个string类型的变量,所以不影响。

3.3 问题3的排查尝试

刚开始时并没有怀疑ExecuteShell函数的问题,因为b.cpp使用了同一个库(libcgibase.so)里的同一个函数(is_symlink_in_zip),经调试发现b.cpp没有出现上述问题。这时候回顾了一下libcgibase.so的实现cgibase.cpp文件,发现其并没有实现ExecuteShell这个函数,然后再查看了一下a.cpp,发现其实现了一个静态函数ExecuteShell。再去看了看b.cpp文件,发现其也实现了一个静态函数ExecuteShell。这时候已经可以明确了is_symlink_in_zip函数中使用的ExecuteShell其实都是由调用者实现的。

这时候对比一下两个cpp的ExecuteShell函数实现,如下:

a.cpp中的实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/*执行shell命令
 *输入pstrShellCommand:要执行的命令
 *输出strShellOut:命令结果
 *成功则返回ture,否则false
 */
 bool ExecuteShell(const char *pstrShellCommand, string &strShellOut)
{
	if(pstrShellCommand == NULL)
		return false;
	strShellOut = "";
	bool bReadError = false;

	FILE *pShellFile = popen(pstrShellCommand, "r");
	if (pShellFile == NULL)
	{
		return false;
	}

	while (!feof(pShellFile))
	{
		char szShellOutPut[32];
		memset(szShellOutPut, 0, sizeof(szShellOutPut));
		int ret = fread(szShellOutPut, sizeof(char), sizeof(szShellOutPut), pShellFile);
		if (ret != sizeof(szShellOutPut) && ferror(pShellFile))
		{
			bReadError = true;
			break;
		}
		strShellOut += szShellOutPut;
	}
	pclose(pShellFile);
	pShellFile = NULL;

	if (bReadError)
	{
		return false;
	}
	else
	{
		//删除最后一个回车符
		if (strShellOut.length() > 0)
		{
			if (strShellOut[strShellOut.length()-1] == '\n')
			{
				strShellOut[strShellOut.length()-1] = 0;
			}

		}
		return true;
	}
}

b.cpp中的ExecuteShell实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
bool ExecuteShell(const char *pstrShellCommand, std::string &strShellOut)
{
    assert(pstrShellCommand != NULL);
    strShellOut = "";
    
    // 执行Shell命令
    FILE *fShellFile = popen(pstrShellCommand, "r");
    if (fShellFile == NULL)
    {
        return false;
    }
    
    // 读取Shell 脚本的输出
    while (!feof(fShellFile))
    {
        char szShellOutPut[32];                                                    
        memset(szShellOutPut, 0, sizeof(szShellOutPut));                          
        size_t ret = fread(szShellOutPut, sizeof(char), sizeof(szShellOutPut) - 1, fShellFile);
        if (ret < sizeof(szShellOutPut)-1 && ferror(fShellFile))
        {
            // 关闭文件
            pclose(fShellFile);
            return false;
        }
        strShellOut += szShellOutPut;
    }
    
    // 删除掉最后一个回车符
    if (strShellOut.length() > 0)
    {
        if (strShellOut[strShellOut.length() - 1] == '\n')
        {
            strShellOut.erase(strShellOut.begin() + strShellOut.length() - 1);
        }
    }

	pclose(fShellFile);
	fShellFile = NULL;
    return true;
}

其他的逻辑大体相同,问题主要出现在删除最后一行回车符这个实现上。 a.cpp中的实现是将strShellOut变量最后一个字符改成’\0’,b.cpp中的实现是去除strShellOut变量最后一个回车符。a.cpp实现中的strShellOut变量的size其实没变的。这时候就能解释为啥使用gdb调试的时候result.size()为2,但是其内容为 0 了。

4. 总结

总结起来就是拿着写c的思维去写c++,问题不大。