CVE-2012-2836 CVE-2009-3895 libexif 整数溢出

libexif-0.6.14.

CVE-2012-2836

-s 123跑几分钟,出了13个crash,栈回溯分一下类,一共4种路径

崩溃分析

crash 0:
调用exif_data_load_data_thumbnail函数时size参数明显过大,且正好是unsigned int的最大值,猜测是个整数溢出或错误的类型转换(int32_t转uint32_t).

崩溃指令可以看出是memmove时的越界读取,逻辑上符合size过大可能出现的情况.

crash 5:

崩溃的原因是从buf中取元素.
发现调用exif_get_short时参数buf是错误的地址,且与exif_data_load_data的d_orig地址非常相似,相差0xFFFFFFFF+3,同样让人联想到整数溢出类型的漏洞.

crash 8:
和crash0到达memmove的路径不同,其他相同.

crash 10:

漏洞分析

Crash 0

跟着链子来到exif_data_load_data_thumbnail函数.众所周知,缓冲区溢出关注四个变量,destination大小,source大小,写入destination的偏移,读取source的偏移.如果指定了偏移,就意味着比较时的运算,有运算就有Integer Overflow的风险.
函数开始的Check中明显存在offset+size的上溢出问题,导致越界读取.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void
exif_data_load_data_thumbnail (ExifData *data, const unsigned char *d,
unsigned int ds, ExifLong offset, ExifLong size)
{
if (ds < offset + size) {
exif_log (data->priv->log, EXIF_LOG_CODE_DEBUG, "ExifData",
"Bogus thumbnail offset and size: %i < %i + %i.",
(int) ds, (int) offset, (int) size);
return;
}
if (data->data)
exif_mem_free (data->priv->mem, data->data);
data->size = size;
data->data = exif_data_alloc (data, data->size);
if (!data->data)
return;
memcpy (data->data, d + offset, data->size);
}

如何正确的Check overflow?引用《Secure Coding in C and C++》中的描述.

Precondition.Addition of unsigned integers can result in an integer overflowif the sum of the left-hand side (LHS) and right-hand side (RHS) of an additionoperation is greater than UINT_MAXfor addition of inttype and ULLONG_MAXforaddition of unsignedlonglong type.
Addition of signed integers is more complicated, as shown in Table 5–5.As you test for these preconditions, make sure that the test itself does notoverflow. The tests in Table 5–5 are guaranteed not to overflow for appropri-ately signed values.

Postcondition.Another solution to detecting integer overflow is to performthe addition and then evaluate the results of the operation. For example, to testfor overflow of signed integers, let sum=lhs+rhs. If lhsis non-negative andsum<rhs, an overflow has occurred. Similarly, if lhsis negative and sum>rhs,an overflow has occurred. In all other cases, the addition operation succeedswithout overflow. For unsigned integers, if the sum is smaller than either oper-and, an overflow has occurred.

官方的修补方案:

1
2
3
4
5
6
7
/* Sanity checks */
if ((o + s < o) || (o + s < s) || (o + s > ds) || (o > ds)) {
exif_log (data->priv->log, EXIF_LOG_CODE_DEBUG, "ExifData",
"Bogus thumbnail offset (%u) or size (%u).",
o, s);
return;
}

crash 8

同样的check问题.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void
exif_mnote_data_olympus_load (ExifMnoteData *en,
const unsigned char *buf, unsigned int buf_size)
{
......
if (o + s > buf_size) continue;

/* Sanity check */
n->entries[i].data = exif_mem_alloc (en->mem, s);
if (!n->entries[i].data) continue;
n->entries[i].size = s;
memcpy (n->entries[i].data, buf + o, s);
......
}

官方修补方案

1
2
3
4
5
6
7
8
if ((dataofs + s < dataofs) || (dataofs + s < s) || 
(dataofs + s > buf_size)) {
exif_log (en->log, EXIF_LOG_CODE_DEBUG,
"ExifMnoteOlympus",
"Tag data past end of buffer (%u > %u)",
dataofs + s, buf_size);
continue;
}

crash 5

同样的check问题.

1
2
3
4
5
6
7
8
9
10
11
12
void
exif_data_load_data (ExifData *data, const unsigned char *d_orig,
unsigned int ds_orig)
{
......
/* IFD 1 offset */
if (offset + 6 + 2 > ds) {
return;
}
n = exif_get_short (d + 6 + offset, data->priv->order);
......
}

官方修补:
虽然看起来还是有溢出,但”ds is restricted to 16 bit above, so offset is restricted too, and offset+8 should not overflow”.

1
2
3
/* Sanity check the offset, being careful about overflow */
if (offset > ds || offset + 6 + 2 > ds)
return;

crash 10

同样的check问题.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void
exif_mnote_data_canon_load (ExifMnoteData *ne,
const unsigned char *buf, unsigned int buf_size)
{
......
if (o + s > buf_size) return;

/* Sanity check */
n->entries[i].data = exif_mem_alloc (ne->mem, sizeof (char) * s);
if (!n->entries[i].data) return;
n->entries[i].size = s;
memcpy (n->entries[i].data, buf + o, s);
......
}

官方修补:

1
2
3
4
5
6
7
if ((dataofs + s < s) || (dataofs + s < dataofs) || (dataofs + s > buf_size)) {
exif_log (ne->log, EXIF_LOG_CODE_DEBUG,
"ExifMnoteCanon",
"Tag data past end of buffer (%u > %u)",
dataofs + s, buf_size);
continue;
}

anoher

以及一个没有打出来的load函数中的check洞.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void
exif_mnote_data_pentax_load (ExifMnoteData *en,
const unsigned char *buf, unsigned int buf_size)
{
......
if (o + s > buf_size) return;

/* Sanity check */
n->entries[i].data = exif_mem_alloc (en->mem, sizeof (char) * s);
if (!n->entries[i].data) return;
n->entries[i].size = s;
memcpy (n->entries[i].data, buf + o, s);
......
}

官方修补:

1
2
3
4
5
6
7
if ((dataofs + s < dataofs) || (dataofs + s < s) ||
(dataofs + s > buf_size)) {
exif_log (en->log, EXIF_LOG_CODE_DEBUG,
"ExifMnoteDataPentax", "Tag data past end "
"of buffer (%u > %u)", dataofs + s, buf_size);
continue;
}

CodeQL

为该漏洞模式编写CodeQL查询,漏洞建模为: 被ExifLoader->buf污染的转换后可能发生溢出的比较表达式.
编写整洁的CodeQL查询需要非常熟悉标准库,而笔者显然不是,所以编写的查询比较繁琐.

下方的查询共报出8个结果,命中5个漏洞点中的2个.
误报原因在于对ds变量取值范围分析不精,误报点不会发生溢出.
漏报的漏洞均来自exif-mnote-data-XXX.这三个函数是通过d->methods.load (d, buf, buf_size)的函数指针调用,猜测是污点分析时在函数指针的调用时中断.

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
 import cpp
import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis
import semmle.code.cpp.dataflow.new.DataFlow
import semmle.code.cpp.dataflow.new.TaintTracking

class PFFiledAccess extends FieldAccess
{
PFFiledAccess()
{
exists(Access fa
| (fa instanceof FieldAccess
or fa instanceof PointerFieldAccess)
and this=fa)
}
}

module FlowConfig implements DataFlow::ConfigSig
{
predicate isSource(DataFlow::Node source)
{
exists(PFFiledAccess pffa
| pffa.getQualifier().(VariableAccess).getTarget().getUnderlyingType().stripType().getName()="_ExifLoader"
and pffa.getTarget().getName() in ["buf"]
and source=DataFlow::exprNode(pffa))

}

predicate isSink(DataFlow::Node sink)
{
exists(Expr e
|(not e.getAChild().getType() instanceof PointerType)
and (convertedExprMightOverflow(e))
and e.getParent() instanceof RelationalOperation
and sink=DataFlow::exprNode(e))
}

}

module Flow = TaintTracking::Global<FlowConfig>;

from DataFlow::Node source, DataFlow::Node sink,Expr sinke
where Flow::flow(source, sink)
and sinke = sink.asExpr()
select source,sink,sinke.getChild(0).toString()+sink.toString()+sinke.getChild(1).toString(),sink.getLocation() as location order by location

我像这样添加边,但查询结果没有变化

1
2
3
4
5
6
predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ)
{
exists( ExprCall ec
|ec.getAnArgument() = pred.asExpr()
and ec.getTarget().getAParameter().getAnAccess() = succ.asExpr())
}

进一步测试并查询文档发现,对于VariableCall,getTarget谓词不能获取到目标.

Gets the target of the call, as best as makes sense for this kind of call. The precise meaning depends on the kind of call it is: - For a call to a function, it’s the function being called. - For a C++ method call, it’s the statically resolved method. - For an Objective C message expression, it’s the statically resolved method, and it might not exist. - For a variable call, it never exists.

找了下标准库发现没找到相关的实现,自己写一个.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Function targetOfVariableCall(VariableCall vc) 
{
exists(Assignment a,FunctionAccess fa
|vc.getExpr().(ValueFieldAccess).getQualifier().(VariableAccess).getTarget().getAnAccess() = a.getLValue().(ValueFieldAccess).getQualifier()
and a.getRValue()=fa
and result=fa.getTarget())
}

predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ)
{
exists( VariableCall ec
|ec.getAnArgument() = pred.asExpr()
and targetOfVariableCall(ec).getAParameter().getAnAccess() = succ.asExpr())
}

最终查询到13个结果,5个漏洞点均命中.

CVE-2009-3895

漏洞分析

为之前的漏洞点打上Patch,继续Fuzz.
打出一个Crash.

跟到漏洞点,发现此时e->components的值是2147483664.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int
exif_data_load_data_entry (ExifData *data, ExifEntry *entry,
const unsigned char *d,
unsigned int size, unsigned int offset)
{
for (i = 0; i < e->components; i++)
exif_set_short (
e->data + i *
exif_format_get_size (
EXIF_FORMAT_SHORT), o,
(ExifShort) exif_get_long (
e->data + i *
exif_format_get_size (
EXIF_FORMAT_LONG), o));
e->format = EXIF_FORMAT_SHORT;
e->size = e->components *
exif_format_get_size (e->format);
}

而在进行下方操作时,发生越界读取.由于i(uint32_t)作为循环变量,上限是由可控的e->components决定的,在乘法运算中可能会发生整数溢出.

1
exif_get_long (e->data + i *exif_format_get_size (EXIF_FORMAT_LONG), o)

然而这里的整数正溢出是如何直接造成越界读取的呢?之前几个漏洞是整数溢出导致checker失效,也就是说直接导致越界读写的是绕过检测的恶意offset和读取大小超过了缓冲区实际大小.
来看下这里的缓冲区是怎么分配的,还是大小乘数量,虽然这里参与运算的entry->components是64位,但s是uint32_t,截断后与溢出发生同样效果.

1
2
3
4
5
6
7
8
9
10
11
12
13
//仅展示相关代码

entry->components = exif_get_long (d + offset + 4, data->priv->order);

s = exif_format_get_size (entry->format) * entry->components;
if (!s)
return 0;
if (s > 4)
doff = exif_get_long (d + offset + 8, data->priv->order);
else
doff = offset + 8;

entry->data = exif_data_alloc (data, s);

从道理上来说,分配和写入时使用同样的表达式,即使都发生溢出,但缓冲区的大小和(偏移+)写入的大小是匹配的,不会造成问题.然而这里是使用的循环变量i进行运算,由于分配时溢出实际分配到的缓冲区较小,则在i从0开始递增的过程中,这里不会发生溢出,偏移逐渐超过缓冲区大小造成越界读取.

所以实际的漏洞点应该是分配缓冲区时的整数溢出.
官方修补:

1
2
3
if ((s < entry->components) || (s == 0)){
return 0;
}

CodeQL

为什么之前的CodeQL查询漏掉了这个漏洞?下列是原因.

  1. 之前的Sink建模为条件表达式,而这里显然不是.
  2. CodeQL的污点并不会从一次具有污染性的FieldAccess传播到该Field所有的FieldAccess(如果这样建模污染就太广了).
  3. CodeQL的污点不会从循环终止条件传播到循环计数器.

处于练习的目的,我最小化的连上了相关的边.成功发现了该漏洞,但查询结果数也来到了171条.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
predicate classFieldTaintStep(DataFlow::Node pred,DataFlow::Node succ,string fn) 
{
exists(PFFiledAccess pffa
|pffa = pred.asExpr()
and pffa.getTarget().getName()=fn
and pffa.getTarget().getAnAccess() = succ.asExpr())
}

predicate forGuardTaintStep(DataFlow::Node pred,DataFlow::Node succ)
{
exists(ForStmt fs, LoopCounter lc
|fs.getCondition().(RelationalOperation).getAChild() = lc.getAnAccess()
and fs.getCondition().(RelationalOperation).getAChild() = pred.asExpr()
and lc.getAnAccess()=succ.asExpr())
}
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2024 翰青HanQi

请我喝杯咖啡吧~

支付宝
微信