为什么编译器警告不够用
在对OpenVPN2进行安全审查时,我们面临一个艰巨的挑战:大约2500个隐式转换编译器警告中,哪些实际上可能导致漏洞?为此,我们创建了一个新的CodeQL查询,将标记的隐式转换数量减少到仅20个。下面介绍我们如何构建查询、学到了什么,以及如何在你的代码上运行这些查询。我们的查询可在GitHub上获取,更多细节请参阅我们的完整案例研究论文。
为什么编译器警告不够用
现代编译器通过-Wconversion
等标志检测隐式转换,但由于无法区分哪些转换是良性的、哪些对安全是危险的,可能会生成大量警告。当我们使用转换检测标志编译OpenVPN2时,发现了数千个警告:
GCC 14.2.0:使用-Wconversion -Wsign-conversion -Wsign-compare
报告了2,698个警告
Clang 19.1.7:使用-Wsign-compare -Wsign-conversion -Wimplicit-int-conversion -Wshorten-64-to-32
报告了2,422个警告
手动审查2500多个发现是不现实的,大多数警告都突出显示了良性转换。挑战不在于识别转换,而在于确定哪些转换引入了安全漏洞。
转换何时对安全重要
C语言宽松的类型系统允许隐式转换,即编译器自动更改变量的类型以使代码编译。并非所有转换都有问题,但这种行为为漏洞创造了空间。一个有问题的情况是转换结果用于改变数据。为了更好地理解数据改变可能存在的问题,我们将其分为三类:截断、重新解释和扩展。
以下是每个类别的简明示例(更多细节请参阅完整论文):
1
2
3
4
5
|
unsigned int x = 0x80000000;
unsigned char a = x; // 截断
int b = x; // 重新解释
uint64_t c = b; // 扩展
|
上述示例都是通过相同类型的转换改变的:如同赋值转换。C程序员经常遇到另外两种类型的转换。
通常算术转换发生在对不同类型的变量进行操作和协调时:
1
2
3
|
unsigned short header_size = 0x13;
int offset = 0x37;
return header_size + offset; // 通常算术转换
|
整数提升发生在对单个变量进行一元位运算、算术运算或移位运算时:
1
2
|
uint8_t val = 0x13;
int val2 = (~val) >> 3; // 整数提升
|
通过将转换类型与上述数据改变类型结合,我们可以创建一个表格来澄清哪些隐式转换应进一步分析可能的安全问题。
|
截断 |
重新解释 |
扩展 |
如同赋值转换 |
可能 |
可能 |
可能 |
整数提升 |
不可能 |
不可能 |
可能 |
通常算术转换 |
不可能 |
可能 |
可能 |
构建实用的CodeQL查询
回到我们对OpenVPN2的安全审查,我们遇到了2500多个标记隐式转换的编译器警告。我们没有手动审查数千个警告,而是通过迭代优化构建了一个CodeQL查询。每一步都改进了查询,消除了误报类别,同时保留了我们在安全方面关心的语义。
步骤0:学习现有的CodeQL查询
在编写新查询之前,我们想回顾可能相关或有用的现有查询。我们找到了三个查询,但像Goldilocks一样,我们发现没有一个符合我们的需求。每个查询要么噪声太大,要么只检查转换的子集。
cpp/conversion-changes-sign
:988个发现。仅检测隐式无符号到有符号整数转换,且仅过滤具有常量值的转换。
cpp/jsf/av-rule-180
:6,750个发现。仅检测最多32位类型,且不报告与扩展相关的问题。
cpp/sign-conversion-pointer-arithmetic
:1个发现。仅检查类型转换用于指针算术的情况。还涵盖显式转换。
步骤1:查找所有有问题的转换(7000+发现)
我们的初始查询找到了每个隐式整数转换,在OpenVPN2代码库中返回了7000多个结果:
1
2
3
4
5
6
7
8
9
10
11
|
import cpp
from IntegralConversion cast, IntegralType fromType, IntegralType toType
where
cast.isImplicit()
and fromType = cast.getExpr().getExplicitlyConverted().getUnspecifiedType()
and toType = cast.getUnspecifiedType()
and fromType != toType
and not toType instanceof BoolType
select cast, "Implicit cast from " + fromType + " to " + toType
|
这预期很宽泛,因此我们更新它以过滤我们实际感兴趣的情况,将结果减少到5,725:
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
|
and (
// 截断
fromType.getSize() > toType.getSize()
or
// 重新解释
(
fromType.getSize() = toType.getSize()
and
(
(fromType.isUnsigned() and toType.isSigned())
or
(fromType.isSigned() and toType.isUnsigned())
)
)
or
// 扩展
(
fromType.getSize() < toType.getSize()
and
(
(fromType.isSigned() and toType.isUnsigned())
or
// 不安全提升
exists(ComplementExpr complement |
complement.getOperand().getConversion*() = cast
)
)
)
)
and not (
// 跳过算术运算中的转换
fromType.getSize() <= toType.getSize() // 应始终成立
and exists(BinaryArithmeticOperation arithmetic |
(arithmetic instanceof AddExpr or arithmetic instanceof SubExpr or arithmetic instanceof MulExpr)
and arithmetic.getAnOperand().getConversion*() = cast
)
|
步骤2:消除可证明安全的常量(1,017个发现)
许多转换涉及永远不会引起问题的编译时常量:
1
2
|
uint32_t safe_value = 42;
uint16_t result = safe_value; // 安全转换
|
我们创建了一个新的谓词来建模常量值的安全范围:
1
2
3
4
5
6
7
8
9
|
import semmle.code.cpp.rangeanalysis.RangeAnalysisUtils
predicate isSafeConstant(Expr cast, IntegralType toType) {
exists(float knownValue |
knownValue = cast.getValue().toFloat()
and knownValue <= typeUpperBound(toType)
and knownValue >= typeLowerBound(toType)
)
}
|
通过检查常量在预期范围内并过滤安全相等性检查,此过滤器将发现减少到1,017个。
步骤3:应用范围分析(435个发现)
CodeQL的范围分析可以确定变量可能的最小值和最大值。我们逐步应用了不同类型的范围分析:
SimpleRangeAnalysis
将查询减少到913个结果。
ExtendedRangeAnalysis
的类与我们新创建的ConstantBitwiseOrExprRange
类结合,将结果减少到886个。
CodeQL的SimpleRangeAnalysis
是过程内的,但我们有处理一些简单过程间情况的想法,例如这个:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
static inline bool
is_ping_msg(const struct buffer *buf)
{
// buf_string_match的唯一调用
return buf_string_match(buf, ping_string, 16);
}
static inline bool
buf_string_match(const struct buffer *src, const void *match, int size)
{
if (size != src->len)
{
return false;
}
// size总是安全转换
return memcmp(BPTR(src), match, size) == 0;
}
|
通过扩展SimpleRangeAnalysisDefinition
类以约束函数参数,我们将发现减少到575个!
通过使用基于IR的RangeAnalysis
,我们进一步将发现减少到435个,但显著增加了查询的运行时间。更多具体细节请参阅论文。
步骤4:建模代码库特定知识(254个发现)
我们为OpenVPN2、C标准库和OpenSSL中的函数创建了模型,这些模型绑定了它们的返回值。这些简单的添加通过消除与已知安全函数相关的发现进一步改进了范围分析。这种领域特定知识将我们的发现减少到254个。
以下是这些新函数模型的两个示例:
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
|
private class BufLenFunc extends SimpleRangeAnalysisExpr, FunctionCall {
BufLenFunc() {
this.getTarget()
.getName()
.matches([
"buf_len", "buf_reverse_capacity", "buf_forward_capacity", "buf_forward_capacity_total"
])
}
override float getLowerBounds() { result = 0 }
override float getUpperBounds() { result = typeUpperBound(this.getExpectedReturnType()) }
override predicate dependsOnChild(Expr child) { none() }
}
private class OpenSSLFunc extends SimpleRangeAnalysisExpr, FunctionCall {
OpenSSLFunc() {
this.getTarget()
.getName()
.matches([
"EVP_CIPHER_get_block_size", "cipher_ctx_block_size", "EVP_CIPHER_CTX_get_block_size",
"EVP_CIPHER_block_size", "HMAC_size", "hmac_ctx_size", "EVP_MAC_CTX_get_mac_size",
"EVP_CIPHER_CTX_mode", "EVP_CIPHER_CTX_get_mode", "EVP_CIPHER_iv_length",
"cipher_ctx_iv_length", "EVP_CIPHER_key_length", "EVP_MD_size", "EVP_MD_get_size",
"cipher_kt_iv_size", "cipher_kt_block_size", "EVP_PKEY_get_size", "EVP_PKEY_get_bits",
"EVP_PKEY_get_security_bits"
])
}
override float getLowerBounds() { result = 0 }
override float getUpperBounds() { result = 32768 }
override predicate dependsOnChild(Expr child) { none() }
|
步骤5:关注用户控制的输入(20个发现)
最后,我们使用污点跟踪和FlowSource
类提供的源来识别涉及用户控制数据的转换,这些是最可能被利用的漏洞来源。此最终过滤器将我们减少到仅20个高优先级案例进行手动审查。
分析这些剩余案例后,我们发现OpenVPN2上下文中没有可被利用的案例。没有漏洞,但无论如何这是一场胜利:我们检查了OpenVPN2的所有隐式转换,节省了大量手动审查时间,现在我们有一个可重用的CodeQL查询供任何人在其C代码库上使用。
保护你的代码免受静默故障影响
采取以下步骤检测C代码库中有问题的隐式转换:
- 对你的C代码库运行我们的CodeQL查询以消除最紧急的问题。
- 将我们的查询添加到构建系统中以持续查找隐式转换错误。
- 建立最小化或消除隐式转换的编码标准。
- 记录并证明非显而易见的显式转换。
- 一旦项目足够成熟,打开
-Wconversion -Wsign-compare
编译器标志并将相关警告视为错误。
隐式转换代表了开发人员意图和编译器行为之间的根本不匹配。虽然C的宽松方法可能看起来方便,但它为难以在代码审查中发现的安全漏洞创造了机会。
我们从OpenVPN2分析中得到的关键见解是,大多数隐式转换是良性的,识别危险转换的子集需要复杂的分析。通过将编译器警告与针对性静态分析和一致的编码实践相结合,你可以显著减少对这些不可见安全缺陷的暴露。
如果你喜欢这篇文章,请分享:
X
LinkedIn
GitHub
Mastodon
Hacker News