如何在不发疯的情况下为 Checkmarx 编写规则

嘿哈布尔!

在我们的工作中,我们公司经常使用各种静态代码分析工具(SAST)。 开箱即用,它们都工作正常。 当然,这一切都取决于项目和其中使用的技术,以及分析规则对这些技术的覆盖程度。 在我看来,选择 SAST 工具时最重要的标准之一是能够根据应用程序的具体情况对其进行自定义,即编写和更改分析规则,或者更常称为自定义查询。

如何在不发疯的情况下为 Checkmarx 编写规则

我们最常使用 Checkmarx - 一个非常有趣且功能强大的代码分析器。 在这篇文章中我将谈谈我为其编写分析规则的经验。

目录

输入

首先,我想推荐一篇关于为 Checkmarx 编写查询功能的俄语文章。 它于 2019 年底在 Habré 上发表,标题为: “你好,检查马克思!” 如何编写 Checkmarx SAST 查询并查找很酷的漏洞.

它详细研究了如何在 CxQL(Checkmarx 查询语言)中为某些测试应用程序编写第一个查询,并展示了分析规则如何工作的基本原理。

我不会重复其中描述的内容,尽管仍然存在一些交叉点。 在我的文章中,我将尝试编写一种“食谱集”,这是我在使用 Checkmarx 期间遇到的特定问题的解决方案列表。 很多这样的问题我不得不绞尽脑汁。 有时文档中没有足够的信息,有时甚至很难理解如何执行所需的操作。 我希望我的经验和不眠之夜不会白费,而这个“自定义查询食谱集”将为您节省几个小时或几个神经细胞。 那么,让我们开始吧!

有关规则的一般信息

首先,让我们看一下一些基本概念以及使用规则的过程,以便更好地理解接下来会发生什么。 而且因为文档没有对此进行任何说明,或者结构非常分散,这不是很方便。

  1. 扫描期间应用规则取决于开始时选择的预设(一组活动规则)。 您可以创建无限数量的预设,具体如何构建它们取决于您的流程的具体情况。 您可以按语言对它们进行分组,或为每个项目选择预设。 活动规则的数量影响扫描的速度和准确性。

    如何在不发疯的情况下为 Checkmarx 编写规则在 Checkmarx 界面中设置预设

  2. 这些规则是在名为 CxAuditor 的特殊工具中编辑的。 这是一个连接到运行 Checkmarx 的服务器的桌面应用程序。 该工具有两种操作模式:编辑规则和分析已执行扫描的结果。

    如何在不发疯的情况下为 Checkmarx 编写规则CxAudit接口

  3. Checkmarx 中的规则按语言划分,即每种语言都有自己的一组查询。 还有一些无论语言如何都适用的通用规则,这些是所谓的基本查询。 在大多数情况下,基本查询涉及搜索其他规则使用的信息。

    如何在不发疯的情况下为 Checkmarx 编写规则按语言划分规则

  4. 规则分为“可执行”和“不可执行”(已执行和未执行)。 在我看来,这不是一个正确的名字,但事实就是如此。 最重要的是,执行“可执行”规则的结果将显示在 UI 中的扫描结果中,而“不可执行”规则仅需要在其他请求中使用其结果(本质上,只是一个函数)。

    如何在不发疯的情况下为 Checkmarx 编写规则创建时确定规则类型

  5. 您可以创建新规则或补充/重写现有规则。 为了重写规则,您需要在树中找到它,右键单击并从下拉菜单中选择“覆盖”。 请务必记住,新规则最初并未包含在预设中,并且未处于活动状态。 要开始使用它们,您需要在仪器的“预设管理器”菜单中激活它们。 重写的规则保留其设置,也就是说,如果规则处于活动状态,它将保持如此状态并立即应用。

    如何在不发疯的情况下为 Checkmarx 编写规则预设管理器界面中的新规则示例

  6. 在执行过程中,会构建一棵请求“树”,这取决于什么。 首先执行收集信息的规则,然后执行使用信息的规则。 执行结果被缓存,因此如果可以使用现有规则的结果,那么最好这样做,这将减少扫描时间。

  7. 规则可以应用于不同级别:

  • 对于整个系统 - 将用于任何项目的任何扫描

  • 在团队级别(Team) - 将仅用于扫描所选团队中的项目。

  • 在项目级别 - 将应用于特定项目

    如何在不发疯的情况下为 Checkmarx 编写规则确定规则的应用级别

适合初学者的“词典”

我将从一些引起我疑问的事情开始,我还将展示一些可以显着简化生活的技术。

列表操作

- вычитание одного из другого (list2 - list1)
* пересечение списков (list1 * list2)
+ сложение списков (list1 + list2)

& (логическое И) - объединяет списки по совпадению (list1 & list2), аналогично пересечению (list1 * list2)
| (логическое ИЛИ) - объединяет списки по широкому поиску (list1 | list2)

Со списками не работает:  ^  &&  ||  %  / 

所有找到的物品

在扫描的语言中,您可以获得 Checkmarx 已识别的绝对所有元素的列表(字符串、函数、类、方法等)。 这是一些可以通过以下方式访问的对象空间 All。 即搜索具有特定名称的对象 searchMe,您可以在所有找到的对象中按名称进行搜索:

// Такой запрос выдаст все элементы
result = All;

// Такой запрос выдаст все элементы, в имени которых присутствует “searchMe“
result = All.FindByName("searchMe");

但是,如果您需要使用另一种语言进行搜索,但由于某种原因未包含在扫描中(例如 Android 项目中的 groovy),您可以通过变量扩展我们的对象空间:

result = AllMembers.All.FindByName("searchMe");

流量分析功能

这些函数在许多规则中使用,下面是它们含义的一个小备忘单:

// Какие данные second влияют на first.
// Другими словами - ТО (second) что влияет на  МЕНЯ (first).
result = first.DataInfluencedBy(second);

// Какие данные first влияют на second.
// Другими словами - Я (first) влияю на ТО (second).
result = first.DataInfluencingOn(second);

获取文件名/路径

有几个属性可以从查询结果中获取(找到条目的文件名、字符串等),但文档没有说明如何获取和使用它们。 因此,为了做到这一点,您需要访问 LinePragma 属性,我们需要的对象将位于其中:

// Для примера найдем все методы
CxList methods = Find_Methods();

// В методах найдем по имени метод scope
CxList scope = methods.FindByName("scope");

// Таким образом можо получить путь к файлу
string current_filename = scope.GetFirstGraph().LinePragma.FileName;

// А вот таким - строку, где нашлось срабатывание
int current_line = scope.GetFirstGraph().LinePragma.Line;

// Эти параметры можно использовать по разному
// Например получить все объекты в файле
CxList inFile = All.FindByFileName(current_filename);

// Или найти что происходит в конкретной строке
CxList inLine = inFile.FindByPosition(current_line);

值得记住的是 FileName 实际上包含文件的路径,因为我们使用了该方法 GetFirstGraph.

执行结果

CxQL里面有一个特殊的变量 result,它返回执行您的书面规则的结果。 它会立即初始化,您可以将中间结果写入其中,并在工作时更改和完善它们。 但是,如果规则内没有对此变量或函数进行赋值 return——执行结果永远为零。

以下查询执行结果不会向我们返回任何内容,并且始终为空:

// Находим элементы foo
CxList libraries = All.FindByName("foo");

但是,将执行结果分配给魔术变量 result 后,我​​们将看到此调用返回给我们的内容:

// Находим элементы foo
CxList libraries = All.FindByName("foo");

// Выводим, как результат выполнения правила
result = libraries

// Или еще короче
result = All.FindByName("foo");

使用其他规则的结果

Checkmarx 中的规则可以类似于常规编程语言中的函数来调用。 编写规则时,您很可能会使用其他查询的结果。 例如,不需要每次都搜索代码中的所有方法调用,只需调用想要的规则即可:

// Получаем результат выполнения другого правила
CxList methods = Find_Methods();

// Ищем внутри метод foo. 
// Второй параметр false означает, что ищем без чувствительности к регистру
result = methods.FindByShortName("foo", false);

这种方法允许您缩短代码并显着减少规则执行时间。

解决问题

记录

使用该工具时,有时不可能立即编写所需的查询,您必须进行试验,尝试不同的选项。 对于这种情况,该工具提供了日志记录,其调用方式如下:

// Находим что-то
CxList toLog = All.FindByShortName("log");

// Формируем строку и отправляем в лог
cxLog.WriteDebugMessage (“number of DOM elements =” + All.Count);

但值得记住的是,该方法仅接受输入 字符串,因此无法显示第一个操作的结果的找到元素的完整列表。 第二个选项用于调试,时常分配给魔术变量 result 查询结果,看看会发生什么。 这种方法不是很方便;您需要确保之后的代码中没有对此进行覆盖或操作 result 或者直接注释下面的代码。 或者,您可能像我一样,忘记从现成的规则中删除几个此类调用,并想知道为什么没有任何效果。

更方便的方法是调用该方法 return 与所需的参数。 在这种情况下,规则的执行将结束,我们将能够看到我们编写的结果发生了什么:

// Находим что-то
CxList toLog = All.FindByShortName("log");

// Выводим результат выполнения
return toLog

//Все, что написано дальше не будет выполнено
result = All.DataInfluencedBy(toLog)

登录问题

在某些情况下,您无法访问 CxAudit 工具(用于编写规则)。 造成这种情况的原因可能有很多,包括崩溃、Windows 突然更新、BSOD 和其他我们无法控制的不可预见的情况。 在这种情况下,有时数据库中存在未完成的会话,这会阻止您再次登录。 要修复它,您需要运行多个查询:

对于 8.6 之前的 Checkmarx:

// Проверяем, что есть залогиненые пользователи, выполнив запрос в БД
SELECT COUNT(*) FROM [CxDB].[dbo].LoggedinUser WHERE [ClientType] = 6;
 
// Если что-то есть, а на самом деле даже если и нет, попробовать выполнить запрос
DELETE FROM [CxDB].[dbo].LoggedinUser WHERE [ClientType] = 6;

对于 8.6 之后的 Checkmarx:

// Проверяем, что есть залогиненые пользователи, выполнив запрос в БД
SELECT COUNT(*) FROM LoggedinUser WHERE (ClientType = 'Audit');
 
// Если что-то есть, а на самом деле даже если и нет, попробовать выполнить запрос
DELETE FROM [CxDB].[dbo].LoggedinUser WHERE (ClientType = 'Audit');

书写规则

现在我们到了最有趣的部分。 当您开始在 CxQL 中编写规则时,您通常缺少的不是文档,而是解决某些问题和描述查询一般工作流程的活生生的示例。

我将尽力让那些开始深入研究查询语言的人的生活变得更轻松,并给出几个使用自定义查询来解决某些问题的示例。 其中一些非常通用,几乎无需更改即可在您的公司中使用,另一些则更具体,但也可以通过更改代码来使用它们以适应您的应用程序的具体情况。

那么,以下是我们最常遇到的问题:

问题: 规则执行结果中有多个Flow,其中一个Flow是另一个Flow的嵌套,必须保留其中一个。

解决方案: 事实上,有时 Checkmarx 会显示多个可能重叠的数据流,并且是其他数据流的缩短版本。 对于这种情况有一个特殊的方法 减少流量。 根据参数,它将选择最短或最长的流:

// Оставить только длинные Flow
result = result.ReduceFlow(CxList.ReduceFlowType.ReduceSmallFlow);

// Оставить только короткие Flow
result = result.ReduceFlow(CxList.ReduceFlowType.ReduceBigFlow);

问题: 展开工具反应的敏感数据列表

解决方案: Checkmarx 有基本规则,其结果被许多其他查询使用。 通过使用特定于您的应用程序的数据补充其中一些规则,您可以立即改进扫描结果。 以下是帮助您入门的示例规则:

General_privacy_violation_list

让我们添加几个在应用程序中用于存储敏感信息的变量:

// Получаем результат выполнения базового правила
result = base.General_privacy_violation_list();

// Ищем элементы, которые попадают под простые регулярные выражения. Можно дополнить характерными для вас паттернами.
CxList personalList = All.FindByShortNames(new List<string> {
	"*securityToken*", "*sessionId*"}, false);

// Добавляем к конечному результату
result.Add(personalList);

问题: 使用密码展开变量列表

解决方案: 我建议立即注意在代码中定义密码的基本规则,并向其中添加公司常用的变量名称列表。

密码_隐私_违规_列表

CxList allStrings = All.FindByType("String"); 
allStrings.Add(All.FindByType(typeof(StringLiteral))); 
allStrings.Add(Find_UnknownReference());
allStrings.Add(All.FindByType(typeof (Declarator)));
allStrings.Add(All.FindByType(typeof (MemberAccess)));
allStrings.Add(All.FindByType(typeof(EnumMemberDecl))); 
allStrings.Add(Find_Methods().FindByShortName("get*"));

// Дополняем дефолтный список переменных
List < string > pswdIncludeList = new List<string>{"*password*", "*psw", "psw*", "pwd*", "*pwd", "*authKey*", "pass*", "cipher*", "*cipher", "pass", "adgangskode", "benutzerkennwort", "chiffre", "clave", "codewort", "contrasena", "contrasenya", "geheimcode", "geslo", "heslo", "jelszo", "kennwort", "losenord", "losung", "losungswort", "lozinka", "modpas", "motdepasse", "parol", "parola", "parole", "pasahitza", "pasfhocal", "passe", "passord", "passwort", "pasvorto", "paswoord", "salasana", "schluessel", "schluesselwort", "senha", "sifre", "wachtwoord", "wagwoord", "watchword", "zugangswort", "PAROLACHIAVE", "PAROLA CHIAVE", "PAROLECHIAVI", "PAROLE CHIAVI", "paroladordine", "verschluesselt", "sisma",
                "pincode",
								"pin"};
								
List < string > pswdExcludeList = new List<string>{"*pass", "*passable*", "*passage*", "*passenger*", "*passer*", "*passing*", "*passion*", "*passive*", "*passover*", "*passport*", "*passed*", "*compass*", "*bypass*", "pass-through", "passthru", "passthrough", "passbytes", "passcount", "passratio"};

CxList tempResult = allStrings.FindByShortNames(pswdIncludeList, false);
CxList toRemove = tempResult.FindByShortNames(pswdExcludeList, false);
tempResult -= toRemove;
tempResult.Add(allStrings.FindByShortName("pass", false));

foreach (CxList r in tempResult)
{
	CSharpGraph g = r.data.GetByIndex(0) as CSharpGraph;
	if(g != null && g.ShortName != null && g.ShortName.Length < 50)
	{
		result.Add(r);
	}
}

问题: 添加 Checkmarx 不支持的已使用框架

解决方案: Checkmarx中的所有查询都是按语言划分的,因此您需要为每种语言添加规则。 以下是此类规则的一些示例。

如果使用库来补充或替换标准功能,则可以轻松地将它们添加到基本规则中。 那么每个使用它的人都会立即了解新的介绍。 例如,Android 中用于日志记录的库是 Timber 和 Loggi。 在基本包中,没有识别非系统调用的规则,因此如果密码或会话标识符进入日志,我们将不会知道。 让我们尝试将此类方法的定义添加到 Checkmarx 规则中。

使用 Timber 库进行日志记录的测试代码示例:

package com.death.timberdemo;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

import timber.log.Timber;

public class MainActivity extends AppCompatActivity {
    private static final String TAG = MainActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Timber.e("Error Message");
        Timber.d("Debug Message");

        Timber.tag("Some Different tag").e("And error message");
    }
}

下面是 Checkmarx 请求的示例,它允许您添加调用 Timber 方法的定义作为应用程序数据的退出点:

查找Android输出

// Получаем результат выполнения базового правила
result = base.Find_Android_Outputs();

// Дополняем вызовами, которые приходят из библиотеки Timber
CxList timber = All.FindByExactMemberAccess("Timber.*") +
    All.FindByShortName("Timber").GetMembersOfTarget();

// Добавляем к конечному результату
result.Add(timber);

您还可以添加相邻规则,但这一条与 Android 登录直接相关:

查找AndroidLog_Outputs

// Получаем результат выполнения базового правила
result = base.Find_Android_Log_Outputs();

// Дополняем вызовами, которые приходят из библиотеки Timber
result.Add(
  All.FindByExactMemberAccess("Timber.*") +
  All.FindByShortName("Timber").GetMembersOfTarget()
);

另外,如果 Android 应用程序使用 工作经理 对于异步工作,最好通过添加从任务获取数据的方法来额外通知 Checkmarx getInputData:

查找Android阅读

// Получаем результат выполнения базового правила
result = base.Find_Android_Read();

// Дополняем вызовом функции getInputData, которая используется в WorkManager
CxList getInputData = All.FindByShortName("getInputData");

// Добавляем к конечному результату
result.Add(getInputData.GetMembersOfTarget());

问题: 在 iOS 项目的 plist 中搜索敏感数据

解决方案: iOS 经常使用扩展名为 .plist 的特殊文件来存储各种变量和值。 不建议在这些文件中存储密码、令牌、密钥和其他敏感数据,因为可以毫无问题地从设备中提取它们。

Plist 文件具有肉眼不明显的功能,但对 Checkmarx 来说很重要。 让我们编写一条规则来搜索我们需要的数据并告诉我们是否在某处提到了密码或令牌。

此类文件的示例,其中包含用于与后端服务通信的令牌:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>DeviceDictionary</key>
	<dict>
		<key>phone</key>
		<string>iPhone 6s</string>
	</dict>
	<key>privatekey</key>
	<string>MIICXAIBAAKBgQCqGKukO1De7zhZj6+</string>
</dict>
</plist>

还有 Checkmarx 的规则,它有几个在编写时应考虑到的细微差别:

// Используем результат выполнения правила по поиску файлов plist, чтобы уменьшить время работы правила и 
CxList plist = Find_Plist_Elements();

// Инициализируем новую переменную
CxList dictionarySettings = All.NewCxList();

// Теперь добавим поиск всех интересующих нас значений. В дальнейшем можно расширять этот список.
// Для поиска значений, как ни странно, используется FindByMemberAccess - поиск обращений к методам. Второй параметр внутри функции, false, означает, что поиск нечувствителен к регистру
dictionarySettings.Add(plist.FindByMemberAccess("privatekey", false));
dictionarySettings.Add(plist.FindByMemberAccess("privatetoken", false));

// Для корректного поиска из-за особенностей структуры plist - нужно искать по типу "If statement"
CxList ifStatements = plist.FindByType(typeof(IfStmt));

// Добавляем в результат, перед этим получив родительский узел - для правильного отображения
result = dictionarySettings.FindByFathers(ifStatements);

问题: 在 XML 中查找信息

解决方案: Checkmarx 具有非常方便的功能,可用于处理 XML 以及搜索值、标签、属性等。 但不幸的是,文档中存在一个错误,导致没有一个示例有效。 尽管该缺陷已在最新版本的文档中消除,但如果您使用早期版本的文档,请务必小心。

这是文档中的一个不正确的示例:

// Код работать не будет
result = All.FindXmlAttributesByNameAndValue("*.app", 8, “id”, "error- section", false, true);

作为执行尝试的结果,我们将收到一个错误: All 没有这样的方法...这是真的,因为有一个特殊的、单独的对象空间来使用处理 XML 的函数 - cxXPath。 这是在 Android 中查找允许使用 HTTP 流量的设置的正确查询:

// Правильный вариант с использованием cxXPath
result = cxXPath.FindXmlAttributesByNameAndValue("*.xml", 8, "cleartextTrafficPermitted", "true", false, true);

让我们更详细地看一下,因为所有函数的语法都是相似的,当你弄清楚其中一个后,你只需要选择你需要的一个即可。 所以,根据参数依次:

  • "*.xml"— 要搜索的文件的掩码

  • 8 — 应用规则的语言的 id

  • "cleartextTrafficPermitted"— xml 中的属性名称

  • "true" — 该属性的值

  • false — 搜索时使用正则表达式

  • true — 表示搜索将忽略大小写,即不区分大小写

例如,我们使用了一条规则,从安全角度来看,Android 中允许通过 HTTP 协议与服务器进行通信的网络连接设置是错误的。 包含属性的设置示例 cleartextTrafficPermitted 有意义 true:

<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">example.com</domain>
        <trust-anchors>
            <certificates src="@raw/my_ca"/>
        </trust-anchors>
        <domain-config cleartextTrafficPermitted="true">
            <domain includeSubdomains="true">secure.example.com</domain>
        </domain-config>
    </domain-config>
</network-security-config>

问题: 按文件名/路径限制结果

解决方案: 在与 Android 移动应用程序开发相关的大型项目之一中,我们遇到了确定混淆设置的规则的误报。 事实上,开箱即用的规则在文件中搜索 build.gradle 负责为应用程序的发布版本应用混淆规则的设置。

但在大型项目中有时会有子文件 build.gradle,它指的是项目中包含的库。 特殊之处在于,即使这些文件没有表明需要混淆,编译期间也会应用父程序集文件的设置。

因此,任务是切断属于库的子文件中的触发器。 可以通过线路的存在来识别它们 apply 'com.android.library'.

文件中的示例代码 build.gradle,这决定了混淆的需要:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 24
    buildToolsVersion "24.0.2"
    defaultConfig {
        ...
    }

    buildTypes {
        release {
            minifyEnabled true
            ...
        }
    }
}

dependencies {
  ...
}

文件示例 build.gradle 对于项目中包含的没有此设置的库:

apply plugin: 'android-library'

dependencies {
  compile 'com.android.support:support-v4:18.0.+'
}

android {
  compileSdkVersion 14
  buildToolsVersion '17.0.0'
  ...
}

Checkmarx 的规则:

ProGuard混淆未使用

// Поиск метода release среди всех методов в Gradle файлах
CxList releaseMethod = Find_Gradle_Method("release");

// Все объекты из файлов build.gradle
CxList gradleBuildObjects = Find_Gradle_Build_Objects();

// Поиск того, что находится внутри метода "release" среди всех объектов из файлов build.gradle
CxList methodInvokesUnderRelease = gradleBuildObjects.FindByType(typeof(MethodInvokeExpr)).GetByAncs(releaseMethod);

// Ищем внутри gradle-файлов строку "com.android.library" - это значит, что данный файл относится к библиотеке и его необходимо исключить из правила
CxList android_library = gradleBuildObjects.FindByName("com.android.library");

// Инициализация пустого массива
List<string> libraries_path = new List<string> {};

// Проходим через все найденные "дочерние" файлы
foreach(CxList library in android_library)
{
    // Получаем путь к каждому файлу
	string file_name_library = library.GetFirstGraph().LinePragma.FileName;
    
    // Добавляем его в наш массив
	libraries_path.Add(file_name_library);
}

// Ищем все вызовы включения обфускации в релизных настройках
CxList minifyEnabled = methodInvokesUnderRelease.FindByShortName("minifyEnabled");

// Получаем параметры этих вызовов
CxList minifyValue = gradleBuildObjects.GetParameters(minifyEnabled, 0);

// Ищем среди них включенные
CxList minifyValueTrue = minifyValue.FindByShortName("true");

// Немного магии, если не нашли стандартным способом :D
if (minifyValueTrue.Count == 0) {
	minifyValue = minifyValue.FindByAbstractValue(abstractValue => abstractValue is TrueAbstractValue);
} else {
    // А если всё-таки нашли, то предыдущий результат и оставляем
	minifyValue = minifyValueTrue;	
}

// Если не нашлось таких методов
if (minifyValue.Count == 0)
{
    // Для более корректного отображения места срабатывания в файле ищем или buildTypes или android
	CxList tempResult = All.NewCxList();
	CxList buildTypes = Find_Gradle_Method("buildTypes");
	if (buildTypes.Count > 0) {
		tempResult = buildTypes;
	} else {
		tempResult = Find_Gradle_Method("android");
	}
	
	// Для каждого из найденных мест срабатывания проходим и определяем, дочерний или основной файлы сборки
	foreach(CxList res in tempResult)
	{
        // Определяем, в каком файле был найден buildType или android методы
		string file_name_result = res.GetFirstGraph().LinePragma.FileName;
        
        // Если такого файла нет в нашем списке "дочерних" файлов - значит это основной файл и его можно добавить в результат
		if (libraries_path.Contains(file_name_result) == false){
			result.Add(res);
		}
	}
}

这种方法非常通用,不仅适用于 Android 应用程序,而且适用于需要确定结果是否属于特定文件的其他情况。

问题: 如果语法不完全支持,请添加对第三方库的支持

解决方案: 在编写代码的过程中使用的各种框架的数量简直是惊人的。 当然,Checkmarx 并不总是知道它们的存在,我们的任务是教它理解某些方法专门属于这个框架。 有时,由于框架使用非常常见的函数名称,并且不可能明确确定特定调用与特定库的关系,因此情况会变得复杂。

困难在于此类库的语法并不总是能够正确识别,您必须进行试验以避免出现大量误报。 有多种选择可以提高扫描精度并解决问题:

  • 第一个选项,我们确信该库用于特定项目,并且可以在团队级别应用该规则。 但是,如果团队决定采取不同的方法或使用多个函数名称重叠的库,我们可能会得到大量误报的不太令人愉快的图片

  • 第二个选项是搜索明确导入了库的文件。 通过这种方法,我们可以确保我们需要的库完全在这个文件中使用。

  • 第三种选择是结合使用上述两种方法。

举个例子,让我们看看一个小圈子里众所周知的图书馆 浮油 对于 Scala 编程语言,即功能 拼接文字值。 一般来说,要将参数传递给 SQL 查询,必须使用运算符 $,它将数据替换为预执行的 SQL 查询。 也就是说,实际上它是Java中Prepared Statement的直接类似物。 但是,如果需要动态构造 SQL 查询,例如需要传递表名,则可以使用运算符 #$,它将直接将数据替换到查询中(几乎像字符串连接)。

示例 кода:

// В общем случае - значения, контролируемые пользователем
val table = "coffees"
sql"select * from #$table where name = $name".as[Coffee].headOption

Checkmarx 尚不知道如何检测 Splicing Literal Values 的使用并跳过运算符 #$,所以让我们尝试教它识别潜在的 SQL 注入并突出显示代码中的正确位置:

// Находим все импорты
CxList imports = All.FindByType(typeof(Import));

// Ищем по имени, есть ли в импортах slick
CxList slick = imports.FindByShortName("slick");

// Некоторый флаг, определяющий, что импорт библиотеки в коде присутствует
// Для более точного определения - можно применить подход с именем файла
bool not_empty_list = false;
foreach (CxList r in slick)
{
    // Если встретили импорт, считаем, что slick используется
	not_empty_list = true;
}

if (not_empty_list) {
    // Ищем вызовы, в которые передается SQL-строка
	CxList sql = All.FindByShortName("sql");
	sql.Add(All.FindByShortName("sqlu"));
	
	// Определяем данные, которые попадают в эти вызовы
	CxList data_sql = All.DataInfluencingOn(sql);
	
	// Так как синтакис не поддерживается, можно применить подход с регулярными выражениями
	// RegExp стоит использовать крайне осторожно и не применять его на большом количестве данных, так как это может сильно повлиять на производительность
	CxList find_possible_inj = data_sql.FindByRegex(@"#$", true, true, true);

    // Избавляемся от лишних срабатываний, если они есть и выводим в результат
	result = find_possible_inj.FindByType(typeof(BinaryExpr));
}

问题: 在开源库中搜索使用的易受攻击的函数

解决方案: 许多公司使用开源监控工具(OSA 实践)来检测开发的应用程序中易受攻击的库版本的使用情况。 有时无法将此类库更新为安全版本。 在某些情况下存在功能限制,而在其他情况下根本没有安全版本。 在这种情况下,SAST 和 OSA 实践的结合将有助于确定代码中未使用导致利用漏洞的函数。

但有时,尤其是考虑 JavaScript 时,这可能不是一个完全微不足道的任务。 下面是一个解决方案,可能并不理想,但仍然有效,使用组件中的漏洞示例 lodash 在方法中 template и *set.

测试 JS 文件中潜在易受攻击的代码的示例:

/**
 * Template example
 */

'use strict';
var _ = require("./node_modules/lodash.js");


// Use the "interpolate" delimiter to create a compiled template.
var compiled = _.template('hello <%= js %>!');
console.log(compiled({ 'js': 'lodash' }));
// => 'hello lodash!'

// Use the internal `print` function in "evaluate" delimiters.

var compiled = _.template('<% print("hello " + js); %>!');
console.log(compiled({ 'js': 'lodash' }));
// => 'hello lodash!'

当直接在 html 中连接时:

<!DOCTYPE html>
<html>
<head>
    <title>Lodash Tutorial</title>
    <script src="./node_modules/lodash.js"></script>
    <script type="text/javascript">
  // Lodash chunking array
        nums = [1, 2, 3, 4, 5, 6, 7, 8, 9];

        let c1 = _.template('<% print("hello " + js); %>!');
        console.log(c1);

        let c2 = _.template('<% print("hello " + js); %>!');
        console.log(c2);
    </script>
</head>
<body></body>
</html>

我们正在寻找所有易受攻击的方法,这些方法在漏洞中列出:

// Ищем все строки: в которых встречается строка lodash (предполагаем, что это объявление импорта библиотеки
CxList lodash_strings = Find_String_Literal().FindByShortName("*lodash*");

// Ищем все данные: которые взаимодействуют с этими строками
CxList data_on_lodash = All.InfluencedBy(lodash_strings);


// Задаем список уязвимых методов
List<string> vulnerable_methods = new List<string> {"template", "*set"};

// Ищем все наши уязвимые методы, которые перечисленны в уязвимостях и отфильтровываем их только там, где они вызывались
CxList vulnerableMethods = All.FindByShortNames(vulnerable_methods).FindByType(typeof(MethodInvokeExpr));

//Находим все данные: которые взаимодействуют с данными методами
CxList vulnFlow = All.InfluencedBy(vulnerableMethods);

// Если есть пересечение по этим данным - кладем в результат
result = vulnFlow * data_on_lodash;

// Формируем список путей по которым мы уже прошли, чтобы фильтровать в дальнейшем дубли
List<string> lodash_result_path = new List<string> {};

foreach(CxList lodash_result in result)
{
    // Очередной раз получаем пути к файлам
	string file_name = lodash_result.GetFirstGraph().LinePragma.FileName;
	lodash_result_path.Add(file_name);
}

// Дальше идет часть относящаяся к html файлам, так как в них мы не можем проследить откуда именно идет вызов
// Формируем массив путей файлов, чтобы быть уверенными, что срабатывания уязвимых методов были именно в тех файлах, в которых объявлен lodash
List<string> lodash_path = new List<string> {};
foreach(CxList string_lodash in lodash_strings)
{
	string file_name = string_lodash.GetFirstGraph().LinePragma.FileName;
	lodash_path.Add(file_name);
}

// Перебираем все уязвимые методы и убеждаемся, что они вызваны в тех же файлах, что и объявление/включение lodash
foreach(CxList method in vulnerableMethods)
{
	string file_name_method = method.GetFirstGraph().LinePragma.FileName;
	if (lodash_path.Contains(file_name_method) == true && lodash_result_path.Contains(file_name_method) == false){
		result.Add(method);
	}
}

// Убираем все UknownReferences и оставляем самый "длинный" из путей, если такие встречаются
result = result.ReduceFlow(CxList.ReduceFlowType.ReduceSmallFlow) - result.FindByType(typeof(UnknownReference));

问题: 搜索应用程序中嵌入的证书

解决方案: 应用程序(尤其是移动应用程序)使用证书或密钥访问各种服务器或验证 SSL-Pinning 的情况并不罕见。 从安全角度来看,将此类内容存储在代码中并不是最佳实践。 让我们尝试编写一个规则来搜索存储库中的类似文件:

// Найдем все сертификаты по маске файла
CxList find_certs = All.FindByShortNames(new List<string> {"*.der", "*.cer", "*.pem", "*.key"}, false);

// Проверим, где в приложении они используются
CxList data_used_certs = All.DataInfluencedBy(find_certs);

// И для мобильных приложений - можем поискать методы, где вызывается чтение сертификатов
// Для других платформ и приложений могут быть различные методы
CxList methods = All.FindByMemberAccess("*.getAssets");

// Пересечение множеств даст нам результат по использованию локальных сертификатов в приложении
result = methods * data_used_certs;

问题: 在应用程序中查找受损的令牌

解决方案: 通常需要撤销代码中存在的受损令牌或其他重要信息。 当然,将它们存储在源代码中并不是一个好主意,但情况有所不同。 借助 CxQL 查询,查找这样的内容非常容易:

// Получаем все строки, которые содержатся в коде
CxList strings = base.Find_Strings();

// Ищем среди всех строк нужное нам значение. В примере токен в виде строки "qwerty12345"
result = strings.FindByShortName("qwerty12345");

结论

我希望本文对那些刚开始熟悉 Checkmarx 工具的人有所帮助。 也许那些长期编写自己的规则的人也会在本指南中找到有用的东西。

不幸的是,目前缺乏可以在 Checkmarx 规则开发过程中收集新想法的资源。 这就是我们创建的原因 Github 上的存储库,我们将在这里发布我们的工作,以便每个使用 CxQL 的人都可以在其中找到有用的东西,并且也有机会与社区分享他们的工作。 该存储库正在填充和构建内容,因此欢迎贡献者!

谢谢你!

来源: habr.com

添加评论