Hacking
home

CodeQL - glibc one-day case

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
복사
upperBoundlowerBound 는 해당 맴버변수가 가질 수 있는 최대(최소)값을 불러온다.
즉, 해당 함수는 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이 여는 파일의 크기에 따라 할당되는 메모리의 크기가 달라진다.