CodeQL 编写经验(一):规则编写语法树分析和调用分析等通用技巧

通用技巧

通配符 %

在内置类型 stringmatches 方法中使用,如 "anythingstring%".matches("%string\\%")

*+ 的作用

常用于 getASuccessor()getQualifier()getEnclosingFunction() 等可以多次调用的方法, * 表示 0 或更多次, + 表示 1 或更多次。

参考:Recursion — CodeQL (github.com)

控制迭代次数

TODO

规则库中的 Def 类

/** A definition of a stack variable. */
library class Def extends DefOrUse {
  Def() { definition(_, this) }

  override SemanticStackVariable getVariable(boolean isDef) {
    definition(result, this) and isDef = true
  }
}

/**
 * Holds if `def` is a (potential) assignment to stack variable `v`. That is,
 * the variable may hold another value in the control-flow node(s)
 * following `def` than before.
 */
predicate definition(SemanticStackVariable v, Expr def) {
  def = v.getInitializer().getExpr()
  or
  variableAccessedAsValue(v.getAnAccess(), def.(Assignment).getLValue().getFullyConverted())
  or
  variableAccessedAsValue(v.getAnAccess(), def.(CrementOperation).getOperand().getFullyConverted())
  or
  exists(AsmStmt asmStmt |
    def = asmStmt.getAChild() and
    def = v.getAnAccess().getParent*()
  )
  or
  definitionByReference(v.getAnAccess(), def)
}

可以看出, Def 类只会是初始化中的 getExpr() 内容,或直接的赋值或自增自减。若需要跟踪到 Def 使用的函数调用,则可以按照类似下面的操作:

def = funcall 
or exists(Assignment assign |
    assign.getRValue().getAChild*() = funcall
    and assign = def
)

规则库中的空指针解引用相关谓语复用

semmle.code.cpp.controlflow.Dereferenced 中的 predicate dereferencedByOperation(Expr op, Expr e) 对几种解引用的情况都做了定义,可以复用。

semmle.code.cpp.controlflow.Nullness 中的 predicate checkedNull(Variable var, ControlFlowNode node)predicate checkedValid(Variable var, ControlFlowNode node) 分别表示在当前结点 var 可能为空或不为空。

AST 分析

获取重载的运算符

被重载的运算符不再是 Operation 类型,而是 FuncationCall 类。

Operation一级子类Operation二级子类例子(未重载)
AssignmentAssignExpr见下一节
AssignOperation见下一节
BlockAssignExpr见下一节
BinaryOperationBinaryArithmeticOperationAddExpr c = a + b; RemExpr c = a % b; MaxExpr c = a >? b;
BinaryBitwiseOperationBitwiseAndExpr unsigned c = a & b; LShiftExpr unsigned c = a << b;
BinaryLogicalOperationLogicalAndExpr if (a && b) { } LogicalOrExpr `if (a
ComparisonOperationEqualityOperation: A C/C++ equality operation, that is, either == or !=. RelationalOperation: A C/C++ relational operation, that is, one of <=, <, >, or >=.
SpaceshipExprauto c = (a <=> b);
UnaryOperationAddressOfExprint *ptr = &var;
PointerDereferenceExprint var = *varptr;
UnaryArithmeticOperationCrementOperation operator++ or operator--; UnaryMinusExpr b = -a
UnaryBitwiseOperationComplementExpr unsigned c = ~a;
UnaryLogicalOperationNotExpr c = !a;
ConditionalExpra = (b > c ? d : e);

获取重载后的运算符:

from FunctionCall call
where call.getTarget().getName().matches("operator%")
select call

一般为 operation-> operation[] operation++ operator delete 等格式。

声明、定义、初始化和赋值

概念和实现:

  • 声明:只是规定了变量的类型和名字,而没有进行内存分配。 CodeQL 中的类是 DeclarationDeclarationEntryDeclaration 常用 getNamegetQualifiedNameDeclarationEntry 是不同文件中各自的实际声明点,常用 getDeclaration 。对于各派生类若声明都在同一文件,则 DeclarationEntry 也只有一个。
  • 定义:不仅规定了变量的类型和名字,而且进行了内存分配,也可能会对量进行初始化。 CodeQL 中定义是包含在声明中的。
  • 初始化:当对象在创建时获得了一个特定值。 CodeQL 中的类是 Initializer ,常用方法 getDeclarationgetExpr ,分别用于获取其中的声明和初始化的内容。
  • 赋值: CodeQL 中的类是 Assignment ,包含 AssignExprAssignOperationBlockAssignExpr 三个子类。

关于声明,注意 DeclarationDeclarationEntry 没有 getEnclosingFunction() 获取不到其所在的函数,需要经过 DeclStmt 中转。如果要获取 int i; 中的变量 i ,需要使用 VariableDeclarationEntry.getVariable() ,其中 VariableDeclarationEntryDeclarationEntry 的子类。

一般有显式初始化 int i = func(x); 和先声明和定义(默认初始化)后再赋值 int i; i = func(x); 两种情况。对于第一种显式初始化的情况,即为类 Initializer ,在 CodeQL 中调用 getDeclaration() 可以获取到 i ,调用 getExpr() 可以获取到 call to func 。如果需要获得完整的 DeclStmt 类型的声明语句,即 int i = func(x) ,则需要借助 getADeclarationEntry()getADeclaration() 来搭建 DeclarationDeclStmt 的桥梁。

DeclStmt getDeclStmt(Declaration decl){
    // decl.getADeclarationEntry() = result.getADeclarationEntry()
    decl = result.getADeclaration() // 更全
}
DeclStmt getDeclStmt(Initializer init){
    init.getDeclaration().getADeclarationEntry() = result.getADeclarationEntry()
}
DeclStmt getDeclStmtSimple(Initializer init){
    init.getDeclaration() = result.getADeclaration()
}
Initializer getDeclStmtInitializer(DeclStmt stmt){
    result.getDeclaration().getADeclarationEntry() = stmt.getADeclarationEntry()
}
Assignment一级子类Assignment二级子类例子(未重载)
AssignExprA non-overloaded assignment operation with the operator =. 如 a = b; 不包括 int a = b; 这是 Initializer
AssignOperationAssignArithmeticOperationA non-overloaded arithmetic assignment operation on a non-pointer lvalue: +=, -=, *=, /= and %=.
AssignBitwiseOperationA non-overloaded bitwise assignment operation: &=, `
AssignPointerAddExprA non-overloaded += pointer assignment expression
AssignPointerSubExprA non-overloaded -= pointer assignment expression.
BlockAssignExpr

数组聚合字面值 ArrayAggregateLiteral

ArrayAggregateLiteral 类型指的是如 {{1, 2}, {3, 4}} 这样的数组,一般用于赋值语句的右侧。比如在给一个 std::map 初始化时,调用初始化列表(Initializer List)构造函数,就将这样的数组聚合字面值转化为了初始化列表。比如下面的例子,大括号内的就可以通过 ArrayAggregateLiteral 访问到:

std::map<Point, double, PointCmp> mag = {
      { {5, -12}, 13 },
      { {3, 4},   5 },
      { {-8, -15}, 17 }
  };

对于遍历 ArrayAggregateLiteral 其每一个成员:

from ArrayAggregateLiteral arrayliteral, int i, Expr expr
where i >= 0 and i < arrayliteral.getArraySize()
and arrayliteral.getElementExpr(i) = expr

匹配成员变量/函数和对应的 qualifier

getQualifier() 用于获取对象成员变量对应的对象(类型限定符),可以是通过 ->. 获取,比如 ptr->x(*ptr).x 分别会得到 ptr*ptr ,对于智能指针 smart_ptr->x ,会得到对 operator-> 的函数调用。这个方法一般对 FieldAccessFunctionCall 使用, FieldAccessDotFieldAccessPointerFieldAccess 两种。 FieldAccessVariableAccess 子类型。

exists(Expr expr, Expr target |
    expr.(VariableAccess) = target
    or expr.(VariableAccess).getQualifier*() = target
)
exists(Expr expr, Expr target |
    expr.(FunctionCall).getQualifier() = target
)

而对于更上层的表达式如 ptr->value == temp ,若想要获取到其中 operation 的 operand ,则获取的是 valueFieldAccess 这个位置。

获取成员的类和模板类

Declaration 类具有方法 getDeclaringType() ,用于获取声明此成员的类。而 Function 类的一个父类就是 Declaration ,可以使用此方法 getDeclaringType() 。对于模板类,因为具体类名中会将模板 T 实例化,因此查询时使用通配符,如 getDeclaringType().getName().matches("A%")

内存分配和释放相关

对于 new 和 delete 常用类 NewOrNewArrayExprNewExprNewArrayExprDeleteExprDeleteArrayExpr ,一般都未重载。获取分配的类型使用 getAllocatedType(),会得到 intint[5] 这样的结果;获取分配时初始化的内容使用 getInitializer() ,对于 new int(4) 得到结果4 ,对于 new std::vector(4) 得到一个函数调用 std::vector::vector(size_t) 参数为 4 ;获取释放的对象使用 getExpr()

至于 malloc 和 free 可以通过对 mallocfree 的函数调用来获取。

不使用 operator newoperator new[]operator deleteoperator delete[] ,以及类 AllocationExprDeallocationExpr 。这个对应的是具体的内存分配函数,即 operator newoperator delete 此类函数。

函数指针调用和普通函数调用

在调用函数指针时,类型不是 FunctionCall 而是普通的 Call ,因为它是 call to expression。

Call 有两种类型: ExprCallFunctionCall ,其中 ExprCall 就是通过函数指针调用,它还有一个子类是 VariableCall ,这种情况对应函数指针是来自具体的变量的情况。对于 VariableCall ,需要通过 getVariable() 方法获取具体调用的函数指针名字。

CodeQL 的 cpp 库中(cpp/ql/lib/semmle/code/cpp/pointsto/CallGraph.qll 即 semmle.code.cpp.pointsto.PointsTo.CallGraph)有个函数 resolvedCall(Call call, Function called) 包含了以下的情况。(还有其他库可以参考文档)

predicate resolvedCall(Call call, Function called) {
  call.(FunctionCall).getTarget() = called
  or
  call.(DestructorCall).getTarget() = called
  or
  exists(ExprCall ec, TargetPointsToExpr pte |
    ec = call and ec.getExpr() = pte and pte.pointsTo() = called
  )
  or
  exists(TargetPointsToExpr pte |
    call.getQualifier() = pte and
    pte.resolve() = called
  )
}

获取函数的 return

通过 ReturnStmt.getExpr() 获取具体的内容,如对于 return 1+2; 可以获取到 1+2 这个 Expr

from ReturnStmt retstmt
where retstmt.getExpr().getType() instanceof IntType
select retstmt

通过 Function.getType() 获取函数返回类型,具体的类型判断可以用:

from Function func
where func.getType() instanceof BoolType
// where func.getType().getName() = "bool"
select func

循环中的语句和表达式提取

使用 LoopgetAChild() 方法只能获取到如 for 或 while 的括号中的每个语句和整个循环体,整个循环体会作为一整个 Stmt

如果要访问循环体中的每一个语句:

from Loop loop, Stmt stmt
where stmt = loop.getStmt().getAChild*()
select stmt

或取循环体中的每一个表达式(不包含 Loop.getCondition() 中的表达式):

from Loop loop, Expr expr
where expr.getEnclosingElement+() = loop.getStmt()
select expr

如果要访问每一个表达式,建议写成:

from Loop loop, Expr expr
where expr.getEnclosingElement+() = loop
select expr

调用关系分析

追踪表达式所在函数的调用关系

可以逆向寻找表达式所在的函数被调用的地方,但是需要注意这种情况无法从作为函数指针的内部去 getACallToThisFunction 。因为指针中的内容是不确定的,无法确定某一个使用函数指针的地方具体使用的是什么函数,所以无法逆向从函数内部找到它作为函数指针被调用的位置。

predicate isCalled(Expr expr, Function func){
    func.getName() = "Caller"
    and (expr.getEnclosingFunction() = func
    or expr.getEnclosingFunction().getACallToThisFunction().getEnclosingFunction() = func)
}

这里 getEnclosingFunction()getEnclosingElement() 和 joern 的 .inAst 有点类似; getACallToThisFunction() 则和 joern 的 .callIn 类似; getACallToThisFunction().getEnclosingFunction() 和 joern 的 .caller 类似。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值