Introduction
Github의 보안 연구원들이 실제로 CodeQL을 활용해 alloca함수의 취약점(정확히는 안전하지 않은 사용으로 인한 취약점)을 발견한 과정을 재현해보았다.
취약점을 요약하자면 glibc내부에서 스택에 메모리 할당을 위해 alloca()(__builtin_alloca)라는 함수를 사용하는데, 해당 함수에는 할당할 스택의 영역이 사용가능한지(oob가 일어나는지) 검사하는 루틴이 없기 때문에, __libc_use_alloca 라는 함수를 먼저 호출해 검사를 해야하지만, 특정 코드에서 해당 검증 루틴이 없어서 발생한 취약점이다.
1. OOBWrite
먼저 해당 함수 호출부에서 OOBWrite가 일어나는지를 확인하는 함수를 작성해보자:
predicate isOOBAlloca(FunctionCall call_alloca) {
exists(Expr sizeArg |
sizeArg = call_alloca.getArgument(0).getFullyConverted() and
(upperBound(sizeArg) >= 65536 or lowerBound(sizeArg) < 0)
)
}
SQL
복사
upperBound 및 lowerBound 는 해당 맴버변수가 가질 수 있는 최대(최소)값을 불러온다.
즉, 해당 함수는 alloca함수가 호출될때 사용되는 size 인자의 최대(최소)값을 체크해 oobwrite의 가능성을 확인한다.
2. Using Check Function
위에서 언급 했듯이 alloca함수를 사용하기 전 __libc_use_alloca 함수를 호출해야 하기 때문에 해당 함수를 호출하는지 확인하는 함수를 작성해보자:
predicate isSafeAlloca(FunctionCall call_alloca) {
exists(FunctionCall call_use_alloca, DataFlow::Node source, DataFlow::Node sink, GuardCondition guard_conditon
| guard_conditon.controls(call_alloca.getBasicBlock(), _)
and call_use_alloca.getTarget().getName() = "__libc_use_alloca"
and DataFlow::localFlow(source, sink)
and source.asExpr() = call_use_alloca.getBasicBlock().getANode()
and sink.asExpr() = guard_conditon.getAChild*()
)
}
SQL
복사
먼저 guard_conditon란 제어문의 조건식을 취하는 클래스인데, 맴버 함수 controls 를 사용해 alloca 함수의 basic block을 호출하는 조건식을 지정한다.
이후 DataFlow클래스의 localFlow 맴버함수를 이용해 call_use_alloca 함수를 호출하고, guard_conditon을 통과하는 경우를 필터링한다.
3. Full Code
import cpp
import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis
import semmle.code.cpp.dataflow.TaintTracking
import semmle.code.cpp.models.interfaces.DataFlow
import semmle.code.cpp.controlflow.Guards
import semmle.code.cpp.dataflow.DataFlow
import DataFlow::PathGraph
predicate isSafeAlloca(FunctionCall call_alloca) {
exists(FunctionCall call_use_alloca, DataFlow::Node source, DataFlow::Node sink, GuardCondition guard_conditon
| guard_conditon.controls(call_alloca.getBasicBlock(), _)
and call_use_alloca.getTarget().getName() = "__libc_use_alloca"
and DataFlow::localFlow(source, sink)
and source.asExpr() = call_use_alloca.getBasicBlock().getANode()
and sink.asExpr() = guard_conditon.getAChild*()
)
}
predicate isOOBAlloca(FunctionCall call_alloca) {
exists(Expr sizeArg |
sizeArg = call_alloca.getArgument(0).getFullyConverted() and
(upperBound(sizeArg) >= 65536 or lowerBound(sizeArg) < 0)
)
}
class StrlenFunction extends DataFlowFunction {
StrlenFunction() { this.getName().matches("%str%len%") }
override predicate hasDataFlow(FunctionInput i, FunctionOutput o) {
i.isInParameter(0) and o.isOutReturnValue()
}
}
// Track taint through `__getdelim`.
class GetDelimFunction extends DataFlowFunction {
GetDelimFunction() { this.getName().matches("%get%delim%") }
override predicate hasDataFlow(FunctionInput i, FunctionOutput o) {
i.isInParameter(3) and o.isOutParameterPointer(0)
}
}
class Config extends TaintTracking::Configuration {
Config() { this = "fopen_to_alloca_taint" }
override predicate isSource(DataFlow::Node source) {
exists(FunctionCall call_fopen
| call_fopen.getTarget().getName() = "_IO_new_fopen"
and source.asExpr() = call_fopen
)
}
override predicate isSink(DataFlow::Node sink) {
exists(FunctionCall call_alloca, Expr size
| call_alloca.getTarget().getName() = "__builtin_alloca"
and not isSafeAlloca(call_alloca)
and isOOBAlloca(call_alloca)
and call_alloca.getArgument(0).getFullyConverted() = size
and sink.asExpr() = size
)
}
}
from Config cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink, source, sink, "fopen flows to alloca"
SQL
복사
Global TaintTracking을 이용해 fopen 호출 이후에 호출되는 alloca함수 중 oob의 가능성이 있고, SafeAlloca함수가 사용되지 않는 경우를 필터링 하는데, 중요한점은 source의 표현식이 fopen의 호출부이고, sink의 표현식이 alloca함수의 인자라는점이다.
즉, alloca함수의 인자가 fopen에 의해 정해지는(영향을 받는)조건을 필터링한다.
⇒ fopen이 여는 파일의 크기에 따라 할당되는 메모리의 크기가 달라진다.