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”) 这一行,查看各变量结果如下:

此时能看到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++,问题不大。