CodeQL之CWE-401(1)

The software does not sufficiently track and release allocated memory after it has been used, which slowly consumes remaining memory.

介绍#

CWE-401,其实也就是内存漏释放的情况,一般来说我们在代码中使用mallocnew申请的内存,都需要被释放才行,否则就会出现memory leak等问题

在用codeql找这类漏洞时,可以分为两种情况,第一种是内存申请后,完全没有释放,比如下面的情况

1
2
3
4
5
6
int main(int argc, char* argv[]) {
int *buff = malloc(SIZE * sizeof(int));
int status = 0;
... //code that does not free buff
return status; //buff is never closed
}

第二种是有的路径释放了,而有的路径上没有释放,如下

1
2
3
4
5
6
7
8
9
int* f() {
try {
int *buff = malloc(SIZE*sizeof(int));
do_stuff(buff);
return buff;
} catch (int do_stuff_exception) {
return NULL; //returns NULL on error, but does not free memory
}
}

可以看到如果正常进行,会进行释放,如果出现了错误,在错误处理环节并没有释放。

之所以分为两类,因为在写查询脚本的时候,是有所区别的,本次只先将第一类

#

第一类比较简单,还是以opensslRSA_new为例

思路1#

思路1还是从我们已知的API标出发。

假设我们有一条先验知识——所有使用RSA_new分配的内存空间,都需要调用RSA_free释放。

首先我们来康康一共有多少RSA_new(因为用最新的codeql和openssl重新编译了)

1
2
3
4
5
6
import cpp
predicate check_name(Function func) { func.getName().matches("RSA_new") }

from FunctionCall func
where check_name(func.getTarget())
select func,func.getLocation()

没有任何意外,还是17处

image-20201113194306329

我们其实还是选择在同一个函数内做一个数据流分析,思路上很清晰,source点是对RSA_new函数的调用,sink点我们没法直接定到函数,但是我们可以知道sink点是RSA_free函数的一个参数,所以我们可以直接写出下面的查询语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import cpp
import semmle.code.cpp.controlflow.Guards
import semmle.code.cpp.dataflow.DataFlow

predicate check_name_pre(Function func) { func.getName().matches("RSA_new") }

predicate check_name_post(Function func) {func.getName().matches("RSA_free")}

from DataFlow::Node source,DataFlow::Node sink,FunctionCall fc
where
exists( |
DataFlow::localFlow(source, sink) and
source.asExpr() instanceof FunctionCall and
check_name_pre(source.asExpr().(FunctionCall).getTarget()) and
check_name_post(fc.getTarget()) and
fc.getAnArgument()=sink.asExpr()
)
select source.asExpr(),source.asExpr().getLocation()

结果是有14个RSA_new存在对应的RSA_free

image-20201113194857126

也就是所有的17个结果中的1、3、17没有进行free

第一个同样也是误报,原因和之前那个一样,codeql没有正确的识别出loopargs[i].rsa_key[testnum]的所有sink点。

第二个也是误报如下,原因在于我们只看了函数内的数据流关系,pval是函数指针形式传进来的,它在全局数据流上,还是存在释放的,这也提醒我们,在函数内找这类漏洞时,为了避免误报,应该先忽略掉这么两类情况:

  • 第一类:申请后的内存给了函数参数
  • 第二类:申请后的内存指针,通过return传给了调用它的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static int rsa_cb(int operation, ASN1_VALUE **pval, const ASN1_ITEM *it,
void *exarg)
{
if (operation == ASN1_OP_NEW_PRE) {
*pval = (ASN1_VALUE *)RSA_new();
if (*pval != NULL)
return 2;
return 0;
} else if (operation == ASN1_OP_FREE_PRE) {
RSA_free((RSA *)*pval);
*pval = NULL;
return 2;
} else if (operation == ASN1_OP_D2I_POST) {
if (((RSA *)*pval)->version != RSA_ASN1_VERSION_MULTI) {
/* not a multi-prime key, skip */
return 1;
}
return (rsa_multip_calc_product((RSA *)*pval) == 1) ? 2 : 0;
}
return 1;
}

第三个是我们的老朋友了,还是上次找到的Missing Check的地方,不过和第二个一样,同样也是一个误报

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int rsa_setkey(RSA** key, unsigned char* ctext, int idx)
{
int clen = 0;

*key = RSA_new();
if (*key != NULL)
switch (idx) {
case 0:
clen = key1(*key, ctext);
break;
case 1:
clen = key2(*key, ctext);
break;
case 2:
clen = key3(*key, ctext);
break;
}
return clen;
}

思路2#

思路2依旧是基于统计的方法,稍微改改脚本就行

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
import cpp
import semmle.code.cpp.controlflow.Guards
import semmle.code.cpp.dataflow.DataFlow

predicate check_name_pre(Function func) { func.getName().matches("RSA_new") }

predicate check_name_post(Function func) {func.getName().matches("RSA_free")}

from FunctionCall func,int total,int freed,DataFlow::Node sc,DataFlow::Node sk
where
total = count(FunctionCall fc_pre | check_name_pre(fc_pre.getTarget())) and
freed = count(DataFlow::Node source |
exists(DataFlow::Node sink,FunctionCall fc_post|
DataFlow::localFlow(source, sink) and
source.asExpr() instanceof FunctionCall and
check_name_pre(source.asExpr().(FunctionCall).getTarget()) and
check_name_post(fc_post.getTarget()) and
fc_post.getAnArgument()=sink.asExpr()
)) and
freed!=total and
freed*100/total > 80 and
exists(|
DataFlow::localFlow(sc, sk) and
sc.asExpr() instanceof FunctionCall and
check_name_pre(sc.asExpr().(FunctionCall).getTarget()) and
check_name_post(func.getTarget()) and
func.getAnArgument()=sk.asExpr()
)
select sc.asExpr(),sc.asExpr().getLocation(),total,freed

结果如下

image-20201114093555826

总结#

总结来说,对于CWE-401,在函数内部进行查找的时候,需要去掉函数参数,返回值这两类情况,感兴趣的小伙伴可以继续修改,把这两类情况加入查询脚本,进一步降低误报率。

参考资料#

评论