Русский
Русский
English
Статистика
Реклама

Анализ кода

Loki 1.8 досье на молодой и подающий надежды Data Stealer

16.09.2020 12:14:05 | Автор: admin


В середине июня борьба с коронавирусом в Казахстане была в самом разгаре. Встревоженные ростом числа заболевших (тогда заразился даже бывший президент Нурсултан Назарбаев), местные власти решились вновь позакрывать все торгово-развлекательные центры, сетевые магазины, рынки и базары. И в этот момент ситуацией воспользовались киберпреступники, отправившие по российским и международным компаниям вредоносную рассылку.

Опасные письма, замаскированную под обращение министра здравоохранения Республики Казахстан, перехватила система Threat Detection System (TDS) Group-IB. Во вложении письма находились документы, при запуске которых на компьютер устанавливалась вредоносная программа из семейства Loki PWS (Password Stealer), предназначенная для кражи логинов и паролей с зараженного компьютера. В дальнейшем злоумышленники могут использовать их для получения доступа к почтовым аккаунтам для финансового мошенничества, шпионажа или продать на хакерских форумах.

В этой статье Никита Карпов, аналитик CERT-GIB, рассматривает экземпляр одного из самых популярных сейчас Data Stealerов Loki.

Сегодня мы рассмотрим одну из популярных версий бота 1.8. Она активно продается, а админ панель можно найти даже в открытом доступе: здесь.

Пример админ-панели:



Loki написан на языке C++ и является одним из самых популярных ВПО, используемых для похищения пользовательской информации с зараженного компьютера. Как и бич нашего времени вирусы-шифровальщики Data Stealer после попадания на компьютер жертвы с очень большой скоростью выполняют поставленную задачу ему не надо закрепляться и повышать свои привилегии в системе, он практически не оставляет времени на защиту от атаки. Поэтому в событиях с ВПО, которое похищает пользовательские данные, главную роль играет расследование инцидента.

Распаковка и получение работоспособного дампа ВПО


Распространение в большинстве случаев происходит через вложения в рассылках писем. Пользователь под видом легитимного файла загружает и открывает вложение, запуская работу ВПО.

По маркеру инжекта можно предположить о наличии Loader.


С помощью DIE получаем информацию, что исходный файл написан на VB6.


График энтропии свидетельствует о большом количестве зашифрованных данных.


При запуске первый процесс создает дочерний, совершает инжект и завершает свою работу. Второй процесс отвечает за работу ВПО. После небольшого промежутка времени останавливаем работу процесса и сохраняем дамп памяти. Чтобы подтвердить, что внутри дампа находится Loki, ищем внутри url командного центра, который в большинстве случаев оканчивается на fre.php.


Выполняем дамп фрагмента памяти, содержащего Loki, и производим корректировку PE-заголовка.

Работоспособность дампа проверим с помощью системы TDS Huntbox.


Функционал бота


В процессе исследования декомпилированного кода ВПО находим часть, содержащую четыре функции, идущие сразу после инициализации необходимых для работы библиотек. Разобрав каждую из них внутри, определяем их назначение и функциональность нашего ВПО.


Названия функций для удобства были переименованы в более информативные.
Функционал бота определяется двумя главными функциями:

  1. Data Stealer первая функция, отвечающая за похищение данных из 101 приложения и отправку на сервер.
  2. Downloader запрос от CnC (Command & Control) команд для исполнения.

Для удобства в таблице, приведенной ниже, представлены все приложения, из которых исследуемый экземпляр Loki пытается похитить данные.
ID функции Приложение ID функции Приложение ID функции Приложение
1 Mozilla Firefox 35 FTPInfo 69 ClassicFTP
2 Comodo IceDragon 36 LinasFTP 70 PuTTY/KiTTY
3 Apple Safari 37 FileZilla 71 Thunderbird
4 K-Meleon 38 Staff-FTP 72 Foxmail
5 SeaMonkey 39 BlazeFtp 73 Pocomail
6 Flock 40 NETFile 74 IncrediMail
7 NETGATE BlackHawk 41 GoFTP 75 Gmail notifier pro
8 Lunascape 42 ALFTP 76 Checkmail
9 Google Chrome 43 DeluxeFTP 77 WinFtp
10 Opera 44 Total Commander 78 Martin Prikryl
11 QTWeb Browser 45 FTPGetter 79 32BitFtp
12 QupZilla 46 WS_FTP 80 FTP Navigator
13 Internet Explorer 47 Mail Client configuration files 81 Mailing
(softwarenetz)
14 Opera 2 48 Full Tilt Poker 82 Opera Mail
15 Cyberfox 49 PokerStars 83 Postbox
16 Pale Moon 50 ExpanDrive 84 FossaMail
17 Waterfox 51 Steed 85 Becky!
18 Pidgin 52 FlashFXP 86 POP3
19 SuperPutty 53 NovaFTP 87 Outlook
20 FTPShell 54 NetDrive 88 Ymail2
21 NppFTP 55 Total Commander 2 89 Trojit
22 MyFTP 56 SmartFTP 90 TrulyMail
23 FTPBox 57 FAR Manager 91 .spn Files
24 sherrod FTP 58 Bitvise 92 To-Do Desklist
25 FTP Now 59 RealVNC
TightVNC
93 Stickies
26 NexusFile 60 mSecure Wallet 94 NoteFly
27 Xftp 61 Syncovery 95 NoteZilla
28 EasyFTP 62 FreshFTP 96 Sticky Notes
29 SftpNetDrive 63 BitKinex 97 KeePass
30 AbleFTP 64 UltraFXP 98 Enpass
31 JaSFtp 65 FTP Now 2 99 My RoboForm
32 Automize 66 Vandyk SecureFX 100 1Password
33 Cyberduck 67 Odin Secure FTP Expert 101 Mikrotik WinBox
34 Fullsync 68 Fling
На этом этапе завершен статический анализ ВПО и в следующем разделе рассмотрим, как Loki общается с сервером.

Сетевое взаимодействие


Для записи сетевого взаимодействия необходимо решить две проблемы:

  1. Командный центр доступен только на момент проведения атаки.
  2. Wireshark не фиксирует коммуникации бота в loopback, поэтому нужно пользоваться другими средствами.

Самое простое решение переадресовать адрес CnC, с которым Loki будет устанавливать коммуникацию, на localhost. Для бота сервер теперь доступен в любое время, хоть и не отвечает, но для записи коммуникаций бота это и не нужно. Для решения второй проблемы воспользуемся утилитой RawCap, которая позволяет записать в pcap необходимые нам коммуникации. Далее записанный pcap будем разбирать уже в Wireshark.


Перед каждой коммуникацией бот проверяет доступность CnC и, если он доступен открывает socket. Все сетевые коммуникации проходят на транспортном уровне по протоколу TCP, а на прикладном используется HTTP.

В таблице ниже представлены заголовки пакета, которые стандартно использует Loki.
Поле Значение Описание
User-agent Mozilla/4.08 (Charon; Inferno) Характерный юзерагент для Loki
Accept */*
Content-Type application/octet-stream
Content-Encoding binary
Content-Key 7DE968CC Результат хеширования предыдущих заголовков (хеширование происходит кастомным алгоритмом CRC с полиномом 0xE8677835)
Connection close
Обратим внимание на body пакета:

  1. Структура записанных данных зависит от версии бота, и в более ранних версиях отсутствуют поля, которые отвечают за опции шифрования и компрессии.
  2. По типу запроса сервер определяет, как обрабатывать полученные данные. Существует 7 типов данных, которые может прочитать сервер:
    • 0x26 Похищенные данные кошельков
    • 0x27 Похищенные данные приложений
    • 0x28 Запрос команд от сервера
    • 0x29 Выгрузка похищенного файла
    • 0x2A POS
    • 0x2B Данные кейлоггера
    • 0x2C Скриншот
  3. В исследованном экземпляре присутствовали только 0x27, 0x28 и 0x2B.
  4. В каждом запросе есть общая информация о боте и зараженной системе, по которой сервер идентифицирует все отчеты по одной машине, а после идет информация, которая зависит от типа запроса.
  5. В последней версии бота реализовано только сжатие данных, а поля с шифрованием заготовлены на будущее и не обрабатываются сервером.
  6. Для сжатия данных используется открытая библиотека APLib.

При формировании запроса с похищенными данными бот выделяет буфер размером 0x1388 (5000 байт). Структура запросов 0x27 представлена в таблице ниже:
Смещение Размер Значение Описание
0x0 0x2 0x0012 Версия бота
0x2 0x2 0x0027 Тип запроса (отправка похищенных данных)
0x4 0xD ckav.ru Binary ID (также встречается значение XXXXX11111)
0x11 0x10 - Имя пользователя
0x21 0x12 - Имя компьютера
0x33 0x12 - Доменное имя компьютера
0x45 0x4 - Разрешение экрана (ширина и высота)
0x49 0x4 -
0x4D 0x2 0x0001 Флаг прав пользователя (1, если администратор)
0x4F 0x2 0x0001 Флаг идентификатора безопасности (1, если установлен)
0x51 0x2 0x0001 Флаг разрядности системы (1, если x64)
0x53 0x2 0x0006 Версия Windows (major version number)
0x55 0x2 0x0001 Версия Windows (minor version number)
0x57 0x2 0x0001 Дополнительная информация о системе (1 = VER_NT_WORKSTATION)
0x59 0x2 -
0x5B 0x2 0x0000 Отправлялись ли похищенные данные
0x5D 0x2 0x0001 Использовалось ли сжатие данных
0x5F 0x2 0x0000 Тип сжатия
0x61 0x2 0x0000 Использовалось ли шифрование данных
0x63 0x2 0x0000 Тип шифрования
0x65 0x36 - MD5 от значения регистра MachineGuid
0x9B - - Сжатые похищенные данные
Второй этап взаимодействия с сервером начинается после закрепления в системе. Бот отправляет запрос с типом 0x28, структура которого представлена ниже:

Размер буфера: 0x2BC (700 байт)
Смещение Размер Значение Описание
0x0 0x2 0x0012 Версия бота
0x2 0x2 0x0028 Тип запроса (запрос команд от командного центра)
0x4 0xD ckav.ru Binary ID (также встречается значение XXXXX11111)
0x11 0x10 - Имя пользователя
0x21 0x12 - Имя компьютера
0x33 0x12 - Доменное имя компьютера
0x45 0x4 - Разрешение экрана (ширина и высота)
0x49 0x4 -
0x4D 0x2 0x0001 Флаг прав пользователя (1, если администратор)
0x4F 0x2 0x0001 Флаг идентификатора безопасности (1, если установлен)
0x51 0x2 0x0001 Флаг разрядности системы (1, если x64)
0x53 0x2 0x0006 Версия Windows (major version number)
0x55 0x2 0x0001 Версия Windows (minor version number)
0x57 0x2 0x0001 Дополнительная информация о системе (1 = VER_NT_WORKSTATION)
0x59 0x2 0xFED0
0x5B 0x36 - MD5 от значения регистра MachineGuid
После запроса бот ожидает получить ответ от сервера, содержащий количество и сами команды. Возможные варианты команд получены с помощью статического анализа декомпилированного кода ВПО и представлены ниже.

Размер буфера: 0x10 (16 байт) + 0x10 (16 байт) за каждую команду в пакете.
HTTP- заголовок (начало данных) \r\n\r\n [0D 0A 0D 0A] 4 байта
Суммарная длина данных - - 4 байта
Количество команд 2 [00 00 00 02] 4 байта
Команда Пропускаемые байты
4 байта
Команды
4 байта
Пропускаемые байты
4 байта
Длина передаваемой строки
4 байта
Передаваемая строка
(пример)
#0
Загрузка и запуск EXE-файла
[00 00 00 00] [00 00 00 00] [00 00 00 00] [00 00 00 23] www.notsogood.site/malicious.exe
#1
Загрузка библиотеки DLL
[00 00 00 00] [00 00 00 01] [00 00 00 00] [00 00 00 23] www.notsogood.site/malicious.dll
#2
Загрузка EXE-файла
[00 00 00 00] [00 00 00 02] [00 00 00 00] [00 00 00 23] www.notsogood.site/malicious.exe
#8
Удаление базы данных хешей (HDB file)
[00 00 00 00] [00 00 00 08] [00 00 00 00] [00 00 00 00] -
#9
Старт кейлоггера
[00 00 00 00] [00 00 00 09] [00 00 00 00] [00 00 00 00] -
#10
Похищение данных и отправка на сервер
[00 00 00 00] [00 00 00 0A] [00 00 00 00] [00 00 00 00] -
#14
Завершение работы Loki
[00 00 00 00] [00 00 00 0E] [00 00 00 00] [00 00 00 00] -
#15
Загрузка новой версии Loki и удаление старой
[00 00 00 00] [00 00 00 0F] [00 00 00 00] [00 00 00 23] www.notsogood.site/malicious.exe
#16
Изменение частоты проверки ответа от сервера
[00 00 00 00] [00 00 00 10] [00 00 00 00] [00 00 00 01] 5
#17
Удалить Loki и завершить работу
[00 00 00 00] [00 00 00 11] [00 00 00 00] [00 00 00 00] -

Парсер сетевого трафика


Благодаря проведенному анализу у нас есть вся необходимая информация для парсинга сетевых взаимодействий Loki.

Парсер реализован на языке Python, на вход получает pcap-файл и в нем находит все коммуникации, принадлежащие Loki.

Для начала воспользуемся библиотекой dkpt для поиска всех TCP-пакетов. Для получения только http-пакетов поставим фильтр на используемый порт. Среди полученных http-пакетов отберем те, что содержат известные заголовки Loki, и получим коммуникации, которые необходимо распарсить, чтобы извлечь из них информацию в читаемом виде.

for ts, buf in pcap:    eth = dpkt.ethernet.Ethernet(buf)    if not isinstance(eth.data, dpkt.ip.IP):        ip = dpkt.ip.IP(buf)    else:        ip = eth.data     if isinstance(ip.data, dpkt.tcp.TCP):        tcp = ip.data        try:            if tcp.dport == 80 and len(tcp.data) > 0:  # HTTP REQUEST                if str(tcp.data).find('POST') != -1:                    http += 1                    httpheader = tcp.data                    continue                else:                    if httpheader != "":                        print('Request information:')                         pkt = httpheader + tcp.data                        httpheader = ""                        if debug:                            print(pkt)                        req += 1                        request = dpkt.http.Request(pkt)                        uri = request.headers['host'] + request.uri                        parsed_payload['Network']['Source IP'] = socket.inet_ntoa(ip.src)                        parsed_payload['Network']['Destination IP'] = socket.inet_ntoa(ip.dst)                        parsed_payload_same['Network']['CnC'] = uri                        parsed_payload['Network']['HTTP Method'] = request.method                         if uri.find("fre.php"):                            print("Loki detected!")                        pt = parseLokicontent(tcp.data, debug)                        parsed_payload_same['Malware Artifacts/IOCs']['User-Agent String'] = request.headers['user-agent']                         print(json.dumps(parsed_payload, ensure_ascii=False, sort_keys=False, indent=4))                        parsed_payload['Network'].clear()                        parsed_payload['Compromised Host/User Data'].clear()                        parsed_payload['Malware Artifacts/IOCs'].clear()                        print("----------------------")            if tcp.sport == 80 and len(tcp.data) > 0:  # HTTP RESPONCE                resp += 1                if pt == 40:                    print('Responce information:')                    parseC2commands(tcp.data, debug)                    print("----------------------")                    pt = 0        except(dpkt.dpkt.NeedData, dpkt.dpkt.UnpackError):            continue

Во всех запросах Loki первые 4 байта отвечают за версию бота и тип запроса. По этим двум параметрам определяем, как будем обрабатывать данные.

def parseLokicontent(data, debug):    index = 0     botV = int.from_bytes(data[0:2], byteorder=sys.byteorder)    parsed_payload_same['Malware Artifacts/IOCs']['Loki-Bot Version'] =  botV     payloadtype = int.from_bytes(data[2:4], byteorder=sys.byteorder)    index = 4    print("Payload type: : %s" % payloadtype)    if payloadtype == 39:        parsed_payload['Network']['Traffic Purpose'] =  "Exfiltrate Application/Credential Data"        parse_type27(data, debug)    elif payloadtype == 40:        parsed_payload['Network']['Traffic Purpose'] = "Get C2 Commands"        parse_type28(data, debug)    elif payloadtype == 43:        parsed_payload['Network']['Traffic Purpose'] = "Exfiltrate Keylogger Data"        parse_type2b(lb_payload)    elif payloadtype == 38:        parsed_payload['Network']['Traffic Purpose'] = "Exfiltrate Cryptocurrency Wallet"    elif payloadtype == 41:        parsed_payload['Network']['Traffic Purpose'] = "Exfiltrate Files"    elif payloadtype == 42:        parsed_payload['Network'].['Traffic Purpose'] = "Exfiltrate POS Data"    elif payloadtype == 44:        parsed_payload['Network']['Traffic Purpose'] = "Exfiltrate Screenshots"     return payloadtype

Следующим в очереди будет разбор ответа от сервера. Для считывания только полезной информации ищем последовательность \r\n\r\n, которая определяет конец заголовков пакета и начало команд от сервера.

def parseC2commands(data, debug):    word = 2    dword = 4    end = data.find(b'\r\n\r\n')    if end != -1:        index = end + 4        if (str(data).find('<html>')) == -1:            if debug:                print(data)            fullsize = getDWord(data, index)            print("Body size: : %s" % fullsize)            index += dword            count = getDWord(data, index)            print("Commands: : %s" % count)            if count == 0:                print('No commands received')            else:                index += dword                for i in range(count):                    print("Command: %s" % (i + 1))                     id = getDWord(data, index)                    print("Command ID: %s" % id)                    index += dword                     type = getDWord(data, index)                    print("Command type: %s" % type)                    index += dword                     timelimit = getDWord(data, index)                    print("Command timelimit: %s" % timelimit)                    index += dword                     datalen = getDWord(data, index)                    index += dword                     command_data = getString(data, index, datalen)                    print("Command data: %s" % command_data)                    index += datalen        else:            print('No commands received')    return None

На этом заканчиваем разбор основной части алгоритма работы парсера и переходим к результату, который получаем на выходе. Вся информация выводится в json-формате.

Ниже представлены изображения результата работы парсера, полученные из коммуникаций различных ботов, с разными CnC и записанные в разных окружениях.

Request information:Loki detected!Payload type: 39Decompressed data: {'Module': {'Mozilla Firefox'}, 'Version': {0}, 'Data': {'domain': {'https://accounts.google.com'}, 'username': {'none@gmail.com'}, 'password': {'test'}}}{'Module': {'NppFTP'}, 'Version': {0}, 'Data': {b'<?xml version="1.0" encoding="UTF-8" ?>\r\n<NppFTP defaultCache="%CONFIGDIR%\\Cache\\%USERNAME%@%HOSTNAME%" outputShown="0" windowRatio="0.5" clearCache="0" clearCachePermanent="0">\r\n    <Profiles />\r\n</NppFTP>\r\n'}}{    "Network": {        "Source IP": "-",        "Destination IP": "185.141.27.187",        "HTTP Method": "POST",        "Traffic Purpose": "Exfiltrate Application/Credential Data",        "First Transmission": true    },    "Compromised Host/User Data": {},    "Malware Artifacts/IOCs": {}}

Выше представлен пример запроса на сервер 0x27 (выгрузка данных приложений). Для тестирования были созданы аккаунты в трех приложениях: Mozilla Firefox, NppFTP и FileZilla. У Loki существует три варианта записи данных приложений:

  1. В виде SQL-базы данных (парсер сохраняет базу данных и выводит все доступные строки в ней).
  2. В открытом виде, как у Firefox в примере.
  3. В виде xml-файла, как у NppFTP и FileZilla.

Request information:Loki detected!Payload type: 39No data stolen{    "Network": {        "Source IP": "-",        "Destination IP": "185.141.27.187",        "HTTP Method": "POST",        "Traffic Purpose": "Exfiltrate Application/Credential Data",        "First Transmission": false    },    "Compromised Host/User Data": {},    "Malware Artifacts/IOCs": {}}

Второй запрос имеет тип 0x28 и запрашивает команды от сервера.

Responce information:Body size: 26Commands: 1Command: 1Command ID: 0Command type: 9Command timelimit: 0Command data: 35

Пример ответа от CnC, который отправил в ответ одну команду на старт кейлоггера. И последующая выгрузка данных кейлоггера.

Request information:Loki detected!Payload type: : 43{    "Network": {        "Source IP": "-",        "Destination IP": "185.141.27.187",        "HTTP Method": "POST",        "Traffic Purpose": "Exfiltrate Keylogger Data"    },    "Compromised Host/User Data": {},    "Malware Artifacts/IOCs": {}}

В конце работы парсер выводит информацию, которая содержится в каждом запросе от бота (информацию о боте и о системе), и количество запросов и ответов, связанных с Loki в pcap-файле.

General information:{    "Network": {        "CnC": "nganyin-my.com/chief6/five/fre.php"    },    "Compromised Host/User Description": {        "User Name": "-",        "Hostname": "-",        "Domain Hostname": "-",        "Screen Resolution": "1024x768",        "Local Admin": true,        "Built-In Admin": true,        "64bit OS": false,        "Operating System": "Windows 7 Workstation"    },    "Malware Artifacts/IOCs": {        "Loki-Bot Version": 18,        "Binary ID": "ckav.ru",        "MD5 from GUID": "-",        "User-Agent String": "Mozilla/4.08 (Charon; Inferno)"    }}Requests: 3Responces: 3 


Полный код парсера доступен по ссылке: github.com/Group-IB/LokiParser

Заключение


В этой статье мы ближе познакомились с ВПО Loki, разобрали его функционал и реализовали парсер сетевого трафика, который значительно упростит процесс анализа инцидента и поможет понять, что именно было похищено с зараженного компьютера. Хотя разработка Loki все еще продолжается, была слита только версия 1.8 (и более ранние), именно с этой версией специалисты по безопасности сталкиваются каждый день.

В следующей статье мы разберем еще один популярный Data Stealer, Pony, и сравним эти ВПО.

Indicator of Compromise (IOCs):


Urls:

  • nganyin-my.com/chief6/five/fre.php
  • wardia.com.pe/wp-includes/texts/five/fre.php
  • broken2.cf/Work2/fre.php
  • 185.141.27.187/danielsden/ver.php
  • MD5 hash: B0C33B1EF30110C424BABD66126017E5
  • User-Agent String: Mozilla/4.08 (Charon; Inferno)
  • Binary ID: ckav.ru
Подробнее..

Строим безопасную разработку в ритейлере. Опыт одного большого проекта

17.09.2020 10:15:44 | Автор: admin
Некоторое время назад мы закончили строить процесс безопасной разработки на базе нашего анализатора кода приложений в одной из крупнейших российских ритейловых компаний. Не скроем, этот опыт был трудным, долгим и дал мощнейший рывок для развития как самого инструмента, так и компетенций нашей команды разработки по реализации таких проектов. Хотим поделиться с вами этим опытом в серии статей о том, как это происходило на практике, на какие грабли мы наступали, как выходили из положения, что это дало заказчику и нам на выходе. В общем, расскажем о самом мясе внедрения. Сегодня речь пойдет о безопасной разработке порталов и мобильных приложений ритейлера.


Для начала в целом про проект. Мы выстроили процесс безопасной разработки в крупной торговой компании, в которой ИТ-подразделение имеет огромный штат сотрудников и разделено на множество направлений, минимально коррелирующих между собой. Условно эти направления можно разделить на 3 основные группы. Первая, очень большая группа, это кассовое ПО, которое написано преимущественно на языке Java (90% проектов). Вторая, самая обширная с точки зрения объема кода группа систем это SAP-приложения. И наконец, третий блок представлял собой сборную солянку из порталов и мобильных приложений: разного рода внешние сайты для клиентов компании, мобильные приложения к этим сайтам, а также внутренние ресурсы мобильные приложения и веб-порталы для персонала ритейлера.

Общую задачу заказчик проекта департамент ИБ сформулировал достаточно стандартным образом для всех трех групп: мы хотим на выходе иметь меньше уязвимостей и безопасную разработку всех систем, создаваемых внутри компании. Но на практике в каждом конкретном подразделении все выглядело совсем иначе, чем у других коллег, потому что на каждом шаге внедрения безопасной разработки нам приходилось идти на миллион разных компромиссов. Одни нюансы помогали выстроить процесс, а другие, наоборот, мешали. В итоге нам все же удалось создать более-менее общий подход для большинства проектов.

Этот подход мы сформулировали максимально просто: сканируется наиболее актуальный для всех разработчиков код. Если говорить в терминах Gitflow, а все группы проектов, за исключением SAP, вели ветки разработки в Gitflow, сканируется основная ветка разработки по расписанию.

Но, как всегда, из любого правила бывают исключения: общий подход не везде мог быть применен as is по ряду причин. Во-первых, наш инструмент (анализатор кода) имеет несколько ограничений, обусловленных тем, что мы хотим иметь возможность при необходимости делать наиболее глубокий анализ некоторых языков программирования. Так, в случае с Java анализ по байткоду гораздо более глубокий, чем по исходному коду. Соответственно, для сканирования Java-проектов требовалась предварительная сборка байткода и лишь затем его отправка на анализ. В случае с C++, Objective C и приложениями для iOS анализатор встраивался в процесс на этапе сборки. Также мы должны были учесть различные индивидуальные требования со стороны разработчиков всех проектов. Ниже расскажем, как мы выстроили процесс для порталов и мобильных приложений.

Порталы и мобильные приложения


Вроде бы все эти приложения объединены в одну логическую группу, но на самом деле в них была жуткая каша. Одних порталов было больше 120-ти (!). Компания очень большая, со множеством бизнес, административных и технических подразделений, и периодически каждое из них решает, что ему нужен свой портал и мобильное приложение. Этот портал и приложение создаются, ими какое-то время пользуются, а потом благополучно забрасывают. В результате на начальном этапе нам пришлось провести для заказчика инвентаризацию, поскольку даже разработчики этих приложений не имели единого списка кодовых баз. Например, для управления репозиториями в этой группе разработчики использовали два GitLab, администраторами которых являлись разные люди. Кроме того, среди порталов и мобильных приложений существенная часть проектов была реализована с помощью внешней разработки. Поэтому, когда подходило время релиза, зачастую подрядчики передавали компании исходные коды новой версии чуть ли не на флешке. В итоге компания имела зоопарк различных приложений и полный беспорядок в их коде. Нам пришлось составить список всех проектов, найти всех ответственных за них техвладельцев, тимлидов, а затем согласовать с основным заказчиком департаментом ИБ, какие из них мы будем анализировать.

В итоге мы выбрали для анализа продакшн-системы и софт, находящийся в поддержке, архивные системы не трогали вообще. Ряд внутренних приложений считались некритичными, так как они не могли нанести компании какой-либо финансовый ущерб, и для анализа выбраны не были. Например, система управления упаковщиками внутри одного склада или грузчиками. Ничего уязвимого для внешних клиентов компании в них нет, и их взлом кем-то из внутренних сотрудников лишь доставит ряду подразделений мелкие внутренние неудобства.

Приоритетной задачей для этой группы ПО ИБ-служба сформулировала внедрение анализа кода на уязвимости, а разработчики выстроить удобный для них процесс проверок, интегрированный в циклы разработки.

Интеграция по стандартной схеме


В качестве системы контроля версий в группе порталов и мобильных приложений использовался GitLab двух разных версий.


Настройка интеграции с GitLab

Далеко не во всех приложениях применялась CI/CD, и там, где ее не было, нам приходилось настаивать на ее применении. Потому что если вы хотите по-настоящему автоматизировать процесс проверки кода на уязвимости (а не просто в ручном режиме загружать ссылку на анализ), чтобы система сама скачивала ее в репозиторий и сама выдавала результаты нужным специалистам, то без установки раннеров вам не обойтись. Раннеры в данном случае это агенты, которые в автоматическом режиме связываются с системами контроля версий, скачивают исходный код и отправляют в Solar appScreener на анализ.

Разработчики группы порталов и мобильных приложений хотели организовать безопасную разработку как полуавтоматизированный процесс, чтобы код сканировался на уязвимости без какого-либо участия с их стороны. Чтобы офицер безопасности верифицировал результаты анализа на уязвимости и ставил разработчикам задачи в Jira, если считал уязвимости критичными, или же отправлял их разработчикам для получения разъяснений. Разработчики решали бы, нужно ли срочно исправлять уязвимость или нет. И если нужно, планировали бы, в какой релиз смогут включить исправления.

В качестве баг-трэкера в основном использовалась Jira, в которую анализатор кода автоматически поставлял информацию о найденных уязвимостях.


Настройка интеграции с Jira

В редких случаях тим-лиды сами смотрели результаты сканирования и заводили задачи в Jira вручную.


Создание задачи в Jira

Такие случаи мы тоже прописали в регламенте как отдельную особенность. В некоторых проектах вообще все исправления обсуждались в Slack или в Telegram, а задачи ставились в режиме реального времени.

В итоге процесс безопасной разработки после внедрения Solar appScreener стал выглядеть так: порталы ежедневно проверяются на наличие изменений в коде основной ветки разработки. Если основная, наиболее актуальная ветка не обновлялась в течение суток, то ничего не происходит. Если же она обновилась, то запускается отправка этой ветки на анализ в соответствующий проект для этого репозитория. Репозиторию в GitLab соответствовал определенный проект в анализаторе кода, и именно в нем сканировалась основная ветка. После этого офицер безопасности просматривал результаты анализа, верифицировал их и заводил задачи на исправления в Jira.


Результаты анализа и созданные в Jira задачи на исправление уязвимостей

Начинали исправлять уязвимости, как правило, с критических, от которых нужно было избавляться срочно. Когда такие уязвимости заканчивались, команда переходила к исправлению новых обнаруженных в коде ошибок. А уже на третьем этапе, например, в рамках закрытия какого-либо технического долга, ликвидировали и старые оставшиеся уязвимости.

Нестандартное в стандартном


В этом, на первый взгляд, не таком уж и сложном процессе имелось два серьезных ограничения. Во-первых, для анализа Android-приложений (то есть написанных на Java) нам нужна была сборка. А во-вторых, для iOS нужны были машины с MacOS, на которых устанавливался бы наш агент и имелась бы среда, которая позволяла бы собирать приложения. С Android-приложениями мы разобрались довольно просто: написали свои части в уже имеющиеся у разработчиков скрипты, которые запускались так же по расписанию. Наши части скриптов предварительно запускали сборку проекта в наиболее широкой конфигурации, которая направлялась в Solar appScreener на анализ. Для проверки же iOS-приложений мы устанавливали на Mac-машину наш MacOS-агент, который производил сборку кода и так же через GitLab CI отправлял код в анализатор на сканирование. Далее, как и в случае с другими видами ПО, офицер безопасности просматривал результаты анализа, верифицировал их и заводил задачи на исправления в Jira.

К порталам и мобильным приложениям мы также относили любые проекты, написанные на Java, их мы собирали и анализировали по аналогичной схеме.

В тех проектах, где не было CI/CD, что было обязательным для нас условием, мы просто говорили: Ребята, хотите анализировать собирайте в ручном режиме и загружайте в сканер сами. Если у вас нет Java или JVM-подобных языков Scala, Kotlin и прочих, то можете просто загружать код в репозиторий по ссылке, и все будет хорошо.

Сложности проекта


Как видно из вышесказанного, в этом стеке приложений основной проблемой было отсутствие во многих проектах CI/CD. Разработчики часто делали сборки вручную. Мы начали интеграцию нашего анализатора с порталов Sharepoint на языке C#. Сейчас C# более-менее перешел на Linux-системы, хотя и не совсем полноценные. А когда проект был в самом разгаре, этот язык еще работал на Windows, и нам приходилось ставить агент на Windows для GitLab. Это было настоящим испытанием, поскольку наши специалисты привыкли использовать Linux-команды. Необходимы были особенные решения, например, в каких-то случаях нужно было указывать полный путь до exe-файла, в каких-то нет, что-то надо было заэкранировать и т.п. И уже после реализации интеграции c Sharepoint команда проекта мобильного приложения на PHP сказала, что у них тоже нет раннера и они хотят использовать C#-овский. Пришлось повторять операции и для них.

Резюме


В результате, несмотря на столь гетерогенный парк технологий, команд и процессов, нам удалось сгруппировать основные случаи этого кейса в несколько пайплайнов, автоматизировать их исполнение, где уместно, и внедрить. На нашем примере мы смогли убедиться, что:

  • внедряемое нами решение достаточно взрослое, чтобы проявлять нужную гибкость для построения процессов DevSecOps в кардинально разных средах внедрения. Гибкость достигается за счёт большого набора встроенных и кастомных интеграций, без которых трудозатраты на внедрение возросли бы в разы или сделали бы его невозможным;
  • настройка нужной автоматизации и последующий разбор результатов не требуют необъятного количества трудозатрат даже при огромном скоупе работ. Согласование и построение внедряемых процессов и полная их автоматизация возможны усилиями небольшой экспертной группы из 3-4 человек;
  • внедрение средств автоматической проверки кода и практик DevSecOps позволяет выявить недостатки текущих процессов DevOps и становится поводом для их настройки, улучшения, унификации и регламентирования. В конечном счёте получается win-win ситуация для всех участников процесса от рядовых разработчиков до топ-менеджеров отделов инженерии и ИБ.

Напомним: это первая часть цикла статей о построении процесса безопасной разработки в крупном ритейлере. В следующем посте мы раскроем подробности реализации этого проекта в группе приложений семейства SAP.

А был ли у вас свой опыт реализации подобных проектов? Будем рады, если вы поделитесь с нами своими кейсами внедрения практик безопасной разработки в комментариях!

Автор: Иван Старосельский, руководитель отдела эксплуатации и автоматизации информационных систем
Подробнее..

Почему обзоры кода это хорошо, но недостаточно

23.09.2020 18:06:49 | Автор: admin
image1.png

Обзоры кода однозначно нужны и полезны. Это возможность передать знания, обучение, контроль выполнения задачи, улучшение качества и оформления кода, исправление ошибок. Причем можно замечать высокоуровневые ошибки, связанные с используемой архитектурой и алгоритмами. В общем всё хорошо, но люди быстро устают. Поэтому статический анализ великолепно дополняет обзоры и помогает выявлять разнообразнейшие неприметные на глаз ошибки и опечатки. Рассмотрим хороший пример на эту тему.

Попробуйте найти ошибку в коде функции, взятой из библиотеки structopt:

static inline bool is_valid_number(const std::string &input) {  if (is_binary_notation(input) ||      is_hex_notation(input) ||      is_octal_notation(input)) {    return true;  }  if (input.empty()) {    return false;  }  std::size_t i = 0, j = input.length() - 1;  // Handling whitespaces  while (i < input.length() && input[i] == ' ')    i++;  while (input[j] == ' ')    j--;  if (i > j)    return false;  // if string is of length 1 and the only  // character is not a digit  if (i == j && !(input[i] >= '0' && input[i] <= '9'))    return false;  // If the 1st char is not '+', '-', '.' or digit  if (input[i] != '.' && input[i] != '+' && input[i] != '-' &&      !(input[i] >= '0' && input[i] <= '9'))    return false;  // To check if a '.' or 'e' is found in given  // string. We use this flag to make sure that  // either of them appear only once.  bool dot_or_exp = false;  for (; i <= j; i++) {    // If any of the char does not belong to    // {digit, +, -, ., e}    if (input[i] != 'e' && input[i] != '.' &&        input[i] != '+' && input[i] != '-' &&        !(input[i] >= '0' && input[i] <= '9'))      return false;    if (input[i] == '.') {      // checks if the char 'e' has already      // occurred before '.' If yes, return false;.      if (dot_or_exp == true)        return false;      // If '.' is the last character.      if (i + 1 > input.length())        return false;      // if '.' is not followed by a digit.      if (!(input[i + 1] >= '0' && input[i + 1] <= '9'))        return false;    }    else if (input[i] == 'e') {      // set dot_or_exp = 1 when e is encountered.      dot_or_exp = true;      // if there is no digit before 'e'.      if (!(input[i - 1] >= '0' && input[i - 1] <= '9'))        return false;      // If 'e' is the last Character      if (i + 1 > input.length())        return false;      // if e is not followed either by      // '+', '-' or a digit      if (input[i + 1] != '+' && input[i + 1] != '-' &&          (input[i + 1] >= '0' && input[i] <= '9'))        return false;    }  }  /* If the string skips all above cases, then  it is numeric*/  return true;}

Чтобы случайно сразу не прочитать ответ, добавлю картинку.

image2.png

Не знаю, нашли Вы ошибку или нет. Даже если нашли, то уверен согласитесь, что найти такую опечатку непросто. Тем более, вы знали, что в функции есть ошибка. Если не знать, то сложно заставить внимательно читать и проверять весь этот код.

В таких ситуациях статический анализатор кода отлично дополнит классический обзор кода. Анализатор не устаёт и дотошно проверит весь код. Как результат, в этой функции анализатор PVS-Studio замечает аномалию и выдаёт предупреждение:

V560 A part of conditional expression is always false: input[i] <= '9'. structopt.hpp 1870

Для тех, кто не заметил ошибку, дам пояснения. Самое главное:

else if (input[i] == 'e') {  ....  if (input[i + 1] != '+' && input[i + 1] != '-' &&      (input[i + 1] >= '0' && input[i] <= '9'))      return false;}

Вышестоящее условие проверяет, что i-тый элемент является буквой 'e'. Соответственно следующая проверка input[i] <= '9' не имеет смысла. Результат второй проверки всегда false, о чём и предупреждает инструмент статического анализа. Причина ошибки проста: человек поспешил и опечатался, забыв написать +1.

Фактически получается, что функция не до конца выполняет свою работу по проверке корректности введённых чисел. Правильный вариант:

else if (input[i] == 'e') {  ....  if (input[i + 1] != '+' && input[i + 1] != '-' &&      (input[i + 1] >= '0' && input[i + 1] <= '9'))      return false;}

Интересное наблюдение. Эту ошибку можно рассматривать как разновидность "эффекта последней строки". Ошибка допущена в самом последнем условии функции. В конце внимание программиста ослабло, и он допустил эту малозаметную ошибку.


Если вам понравится статья про эффект последней строки, то рекомендую познакомиться с другими аналогичными наблюдениями: 0-1-2, memset, сравнения.

Всем пока. Ставлю лайк тем, кто самостоятельно нашёл ошибку.
Подробнее..

Строим безопасную разработку в ритейлере. Опыт интеграции с кассовым ПО GK

26.11.2020 10:08:34 | Автор: admin
Что самое сложное в проектной работе? Пожалуй, свести к общему знаменателю ожидания от процесса и результата у заказчика и исполнителя. Когда мы начинали внедрять безопасную разработку в группе GK-приложений (кассового ПО) крупного ритейлера, то на входе имели вагон времени и задачи снижения уязвимостей в коде. А вот что и как нам пришлось решать на практике, мы вам расскажем под катом.
Кстати, это уже третий пост, в котором мы делимся своим опытом выстраивания процесса безопасной разработки для крупного ритейлера. Если пропустили, можете прочитать первые две части: о безопасной разработке порталов и мобильных приложений и о безопасной разработке в группе приложений SAP.



О проекте


С этого скоупа мы начинали, когда пришли к заказчику. Согласно стартовому плану проекта, у нас были довольно вольготные сроки его реализации. Мы очень радовались, наивно думая, что сможем вести несколько проектов одновременно. В итоге мы почти все время уделяли именно кассовому ПО, потому что все оказалось гораздо сложнее. В целом, это стало самым сложным направлением интеграции, поскольку здесь требовалась наиболее серьезная автоматизация процесса.

Во-первых, мы столкнулись с мощной бюрократической машиной. Каждое действие требовало постоянных согласований, внесения в них правок, отчётов, их исправлений, запросов, ожиданий ответов и получений уточнений, которые меняли подходы и требовали серьёзных трудозатрат. А во-вторых, с тем, что у представителей заказчика не было ни желания, ни времени нам помогать.

GK


GK это по сути фреймворк, на основе которого можно создавать свою кастомизацию кассового ПО. Заказчик несколько лет назад выкупил у GK некоторую часть исходного кода этого софта для создания собственной версии, соответственно, это направление представляло собой очень большую группу разработки. Большая часть ПО написана на языке Java (90% проектов). Встречались и некоторые проекты, написанные на C++ с использованием довольно древних библиотек, поскольку аппаратное обеспечение было заточено под этот стек технологий. Для Windows под Windows Server 2008 и под набор библиотек для Visual Studio, для которых данная версия ОС была последней совместимой.

В группе имелись свои жесткие процессы разработки, свои релизные циклы. Процесс ветвления был похож на Gitflow с основной веткой разработки и feature-ветками под разнообразные задачи. Однако feature-ветки могли быть в статусе разработки месяцами (часто готовые к merge, но заброшенные) и вливались прямо перед релизом, в который они по плану должны были войти.

Помимо внедрения в CI/CD мы еще внедрялись и в Jira, для чего нам пришлось создать отдельную сущность для классификации задач уязвимость. Этот вид задачи позволял заводить уязвимости внутри Jira в соответствии с процессом разработки и работы с таск-трекером.

Направление GK имело длинный процесс разработки, когда создается feature на какой-либо функционал, а потом эта feature-ветка может жить очень долго отдельно от основной ветки разработки. Ее могут верифицировать, но не могут влить в master или development почти вплоть до самого релиза ввиду технических сложностей регрессионного тестирования и т.п.

Решение для интеграции


Когда мы пришли в группу GK внедрять Solar appScreener, мы были удивлены огромным количеством кода в проектах этого направления и предложили стандартный вариант сканирования основной ветки разработки. Разработчики сказали, что они тоже хотят получать информацию об уязвимостях и тоже хотят пользоваться анализатором, но не так, как офицеры безопасности. Им нужна была автоматизация проверки на уязвимости: единая точка входа в виде GitLab, где можно видеть все изменения. Чтобы на сканирование автоматически запускался код не только основной ветки разработки, но и всех побочных веток. При этом нужен был автоматизированный запуск из Jira и по просьбе заказчика полуавтоматизированный из Jenkins и GitLab.

В итоге мы решили, что самым простым способом анализа ветки (в особенности для долгих feature-веток) будет запуск сканирования таким образом, чтобы задача в Jira при ее полном завершении (финальном push-е) переводилась в статус in review. После чего она попадала к специалисту, который может принять изменения и дать добро на старт сканирования кода на уязвимости. Иначе анализ кода по каждому push-у разработчика в репозитории создал бы адскую нагрузку и на людей, и на технику. Разработчики комитили код в репозитории не реже раза в пять минут, и при этом проекты порой содержали по 3,5 млн строк кода, одно сканирование которого, соответственно, длилось 6-8 часов. Такое никакими ресурсами не покроешь.

Стек технологий


Теперь немного о том, какой набор технологий использовался в проекте. GitLab в качестве системы контроля версий, Jenkins в качестве CI/CD-системы, Nexus в качестве хранилища артефактов и Jira в качестве системы отслеживания задач. При этом сами разработчики взаимодействовали только с Jira и с GitLab, тогда как с Jenkins инженеры, а с Solar appScreener безопасники. Вся информация об уязвимостях в коде присутствовала в GitLab и Jira.

Поэтому пришлось организовать процесс сканирования следующим образом: в Jira задача переводилась в in review через Jenkins, так как была необходима сборка. Имелся целый стек из Jenkins-плагинов и скриптов, которые получали веб-хук из Jira. То есть помимо вида задачи уязвимость нам пришлось создавать в Jira еще и веб-хуки, которые при изменении проекта в GK отправлялись в Jenkins и требовали дополнительных шагов по настройке и серьёзной доработке двусторонней интеграции. Процесс был таким: когда в GK обновлялась какая-либо задача, Jira проверяла, соответствует ли эта задача тем, которые должны отправляться в Jenkins, а именно является ли это задачей разработки. И только после этого веб-хук уходил в Jenkins, который парсил веб-хук и на основе тела запроса понимал, какому репозиторию в GitLab и какой группе работ (job-ов) нужно запускаться.

Разработчики GK выставили ряд требований при заведении задач в Jira, и мы старались их использовать по полной, чтобы получить максимальное количество информации. Благодаря этому мы смогли правильно матчить репозитории в GitLab и ветку, которую мы должны были стянуть из конкретного репозитория для ее запуска на анализ. В требованиях мы жестко прописали, что ветка в GitLab обязательно должна содержать в названии номер задачи в Jira. В принципе, это best practice, который используется повсеместно в самой задаче должны быть метки, маркирующие, к какой части приложения/системы относится проблема.

На основе веб-хука, в котором содержалось название проекта, метки, поля с компонентами, а также ключ самой задачи и т.п., происходило автоматическое определение необходимости запуска всех задач. Часть проектов получала ответ всех значений удовлетворительно и после этого отправлялась на сканирование. А именно: поступал запрос в GitLab, проверялось наличие в нем данной ветки разработки, и при подтверждении система запускала сканирование новой части кода этой ветки в Solar appScreener.

Учитываем пожелания



Кадр из мультфильма Вовка в тридевятом царстве

Поскольку уязвимостей в GK-проектах было десятки тысяч, разработчики не хотели видеть их все. Меж тем в коде содержались порой критичные уязвимости: вставленные в код пароли, отраженные межсайтовые скриптинги, SQL-инъекции и т.п.

Разработчики планировали исправлять уязвимости в бэклоге, а прежде всего хотели видеть изменения: то есть появившиеся в новом коде уязвимости по сравнению с кодом предыдущей версии.

Однако реализованные в нашем продукте механизмы не подразумевают инкрементальный анализ для Java: анализируется байт-код, и невозможно проверить только измененные файлы, потому что теряется связность кода. Изменился один файл, использующий несколько библиотек или других пакетов, которые принадлежат тому же исходному коду, тогда и вся информация об уязвимости теряется. Поэтому мы сразу обозначили заказчику, что в данном случае возможен анализ исходного кода только целиком, и предложили следующую схему.

Как и для остальных проектов компании, мы выстроили для GK соответствие проекта в GitLab проекту в Solar appScreener, а дополнительно провели корреляцию с веткой разработки. Чтобы разработчикам и офицерам ИБ было проще искать, мы создали файл с реестром названия группы репозиториев, конкретного проекта из этой группы и ветки в Solar appScreener.

Кроме того, разработчики хотели видеть комментарии с изменениями сразу в GitLab, и вот как мы это сделали.

Интеграция с GitLab


Сначала мы сканировали основную ветку разработки для каждого репозитория, от которой шла ветка с изменениями. Поскольку разработчики хотели видеть комментарии с изменениями сразу в GitLab, мы договорились, что если ветка переведена в статус in review, это значит, что уже есть соответствующий merge request на ее вливание. Система проверяла, что merge request был действительно открыт, и если это подтверждалось, то Solar appScreener анализировал основную ветку разработки. Затем он сканировал ветку с изменениями, строил diff, выгружал его, получал только новые уязвимости, брал из них критичные (наш инструмент позволяет настраивать уровни критичности) и преобразовывал их описание в формат текстовых сообщений со ссылкой на данное вхождение в конкретном проекте в Solar appScreener (см.скриншот ниже). Эти сообщения отправлялись в GitLab в виде дискуссии от имени технической учетной записи. В merge request вместе с измененным кодом можно было увидеть непосредственно строку срабатывания. Её сопровождал адресованный конкретному разработчику комментарий в GitLab типа вот здесь содержится уязвимость, пожалуйста, поправьте или закройте ее.



Таким образом, между этими двумя сканированиями мы выявляли изменения, которые привнес разработчик. Если количество уязвимостей увеличивалось, это обязательно отражалось в комментариях отчёта. Такая схема была очень удобна для всех участников, потому что в GitLab сразу же появлялись неразрешенные дискуссии, а это значит, что merge request влить нельзя. И ответственный за approve merge requestа видел, что разработчик допустил уязвимости, и их нужно закрыть.

Интеграция с Active Directory


Чтобы просматривать уязвимости через Solar appScreener, разработчикам нужны были права. Поэтому помимо всей интеграции с системами разработки и со всеми тремя скоупами мы еще интегрировались и с сервисами Active Directory. Для нас некоторую сложность представляло то, что у заказчика был чудовищно большой AD с миллионом вложенных групп, что потребовало множества оптимизаций. То есть помимо внедрения CI/CD нам приходилось в процессе решать много сопутствующих задач настройки, адаптации и оптимизации (см. скриншот ниже).



Что касается интеграции с Active Directory, разработчики GK не хотели настраивать параметры доступа в ряд проектов Solar appScreener непосредственно в AD, потому что они не были владельцами этого ресурса. В компании процесс выдачи прав доступа выглядел следующим образом: если разработке нужно было внести нового человека в группу, приходилось писать заявку в техподдержку. Там определяли, кому именно отправлять данный запрос, кто отвечает за выдачу подобных прав, действительно ли можно выдать этому человеку права, авторизованы они или нет и т.п.

Это трудоемкий процесс, которым разработчики не управляли, в отличие от GitLab.Поэтому они попросили организовать интеграцию с AD следующим образом. Они разрешают видеть уязвимости не только техвладельцам системы и офицерам безопасности, как это было сделано для других направлений, но и сделать матчинг прав в GitLab. То есть если человек имеет права разработчика и выше (maintainer, владелец репозитория), то он может просматривать соответствующие проекты в Solar appScreener, которые относятся к этому репозиторию. Когда в Solar appScreener создавался новый проект, система запрашивала имена всех, кто состоит в GitLab в данном репозитории с уровнем доступа выше разработчика. Список выгружался, поступало обращение в анализатор кода, который в свою очередь отправлял запрос в AD с добавлением списка отобранных людей. Все, кто состоял в данном репозитории в GitLab, получали автоматические права через свои AD-учетки на доступ к проекту в Solar appScreener. В конце всей цепочки генерился pdf-файл с отчетом по уязвимостям из анализатора кода. В отчете содержался diff, который рассылался списку пользователей проектов GK на электронную почту.

Резюме


Реализованная интеграция с кассовым ПО GK это пример весьма масштабного проекта внедрения и автоматизации в компании процесса безопасной разработки, который был выстроен не только вокруг службы информационной безопасности, но и вокруг разработчиков. Причем в этой группе проектов так же, как и в других группах, параллельно шли сканирования основной ветки разработки: офицеры безопасности тоже заводили задачи в Jira и их верифицировали. Уязвимости заносились в бэклог и позже исправлялись.

Разработчики получили для себя удобный инструмент, встроенный в привычную среду и workflow. Так как они выбрали для себя порог из топ критических уязвимостей, которые довольно редко возникают, их нагрузка несильно возросла после внедрения. В итоге довольны оказались все участники проекта: и те, кто стремился повысить безопасность кода, и те, кому приходилось исправлять уязвимости.

Если у вас есть собственный опыт реализации практик безопасной разработки, пожалуйста, поделитесь им в комментариях!

Напомним: это третья часть цикла статей о построении процесса безопасной разработки в крупном ритейлере. В следующем посте будем подводить итоги работы над этим масштабным проектом: поговорим о том, чему мы научились в процессе, и какое развитие получил наш инструмент благодаря такому опыту.

Автор: Иван Старосельский, руководитель отдела эксплуатации и автоматизации информационных систем
Подробнее..

Строим безопасную разработку в ритейлере. Итоги одного большого проекта

22.12.2020 10:22:28 | Автор: admin
Эта статья завершающая в цикле материалов о нашем опыте выстраивания процесса безопасной разработки для крупного ритейлера. Если пропустили, можете прочитать первые части: о безопасной разработке порталов и мобильных приложений, о безопасной разработке в группе приложений SAP и о встраивании в процесс разработки кассового ПО. Настало время собрать шишки, которые мы набили подвести итоги.

Все было классно. И на этом мы могли бы закончить, если бы не одно но. При планировании мы некорректно оценили время интеграции. Спойлер: многое пошло не так, в результате внедрение усложнилось, сроки сдвинулись. Спойлер 2: учесть все факторы риска было невозможно. И вот с этого момента давайте поподробнее.




Lessons to Be Learned


1. Учитываем регламенты и бюрократию


Чем больше заказчик, тем больше у него регламентов и бюрократии, которые созданы облегчить все, но на деле только усложняют. О первый камень мы споткнулись на этапе конкурса. Нас попросили подготовить генеральный план еще до того, как мы успели ознакомиться со всеми внутренними требованиями и нюансами, а также устройством компании изнутри, которые впоследствии на многое повлияли. Это было требованием директората, без которого внедрение не случилось бы. Мы согласились и, оценив риски, подписались под довольно свободные сроки, как нам тогда показалось.

На старте заказчик предупредил нас, что у него есть регламент, согласно которому внедрения должны проходить через три стандартных этапа: разработка, тестирование и продакшн. Расскажем на примере, почему эти правила на самом деле ничего не облегчают.

Нам нужно было внедриться в большое количество репозиториев мобильных приложений, но не во всех из них был настроен CI, и поэтому не везде можно было посмотреть, как собирается код. Поэтому мы сначала получали доступ к исходному коду (к сожалению, только на чтение). Доступа к инфраструктуре сборки и тестирования у нас не было, поэтому мы пытались собрать его у себя локально, писали внутренние инструкции для воспроизводимости и страховки от бас-фактора, пытались автоматизировать сборку и запуск анализа, постоянно консультировались с разработчиками ритейлера. Наконец, когда мы локально прогнали все анализы исходного (или собранного кода) в Solar appScreener, все финализировали и пришли к заказчику с готовым планом, выяснилось непредвиденное. Некоторые репозитории изменились (соответственно, наши подходы устарели), а часть тимлидов просто отказалась давать доступ к коду, потребовав инструкции по интеграции, чтобы внедрить все самостоятельно.

Получилось, что какую-то работу мы проделали зря, какую-то передали на сторону заказчика. И если бы мы миновали бюрократический процесс, существенно сэкономили бы время. Как мы увидели на примере внутренних разработчиков позже, избежать его было можно. Ближе к концу проекта мы убедились, что могли бы так же игнорировать эти требования без какого-либо ущерба проекту и репутации. Но к тому моменту интеграция уже перешла в финальную фазу отладки. Мы сделали вывод, что в будущем будем договариваться, чтобы с нас снимали требования о жестком соответствии регламентам, если их несоблюдение никак не влияет на проект. Также мы поняли, что необходимо было настаивать на большем вовлечении со стороны ритейлера. Так мы могли узнать о значимых изменениях раньше и более гибко на них отреагировать. И самый главный вывод: при планировании сроков обязательно нужно оценивать зарегламентированность крупных организаций как веский фактор риска.

2. Текучка существенный тормоз процесса


В большой компании большая текучка кадров еще одна причина, по которой нас периодически отбрасывало назад по временной петле. Мы несколько раз достигали договоренностей с ответственными лицами, которые впоследствии уходили из компании. Поэтому нам приходилось либо заново договариваться с новым сотрудником о соблюдении ранее утверждённых договорённостей, либо общаться с вышестоящим руководителем (в тех случаях, когда замену ему еще не нашли).

Однажды нас застали врасплох категоричным ответом: Я считаю, что это решение будет неверным. Мы так делать не будем. Это случилось, когда мы уже договорились о полной передаче поддержки баз данных MariaDB с руководителем отдела администрирования баз данных. Перед самой сдачей проекта, когда мы все согласовали, выяснилось, что в отделе сменился руководитель. И хотя все договоренности с прежним сотрудником были запротоколированы, согласно регламенту, нам пришлось пойти на компромисс и взять администрирование на себя в рамках техподдержки всего продукта. Оказалось, что заказчик не берет на поддержку MariaDB, хотя в проектной документации не было упоминаний об этом.

От таких случаев полностью не застрахуешься, поэтому чтобы выйти из ситуации, нужно, с одной стороны, уметь жестко фиксировать решения и следовать им, с другой стороны, искать точки соприкосновения и идти навстречу друг другу.

3. Погружение в специфику избавит от множества проблем


На этапе внедрения мы нередко сталкивались с недочетами в планировании. Поскольку менеджмент ритейлера попросил подготовить генеральный план до утверждения проекта, а после утверждения мы были вынуждены идти четко по намеченному курсу, возникали сложности. На старте мы ожидали, что внутри компании все будет устроено плюс-минус единообразно (не считая SAP), но это оказалось совсем не так. Отличались инфраструктура и способы ее развертывания, регламенты по разработке, уставы, ограничения в приобретении продуктов и т.д. Мы прекрасно понимали, что столкнемся с различными технологиями в разработке, но надеялись на единые подходы к процессам и единые концепции, и вот в этом мы ошиблись.

Завершив проект, мы пришли к выводу, что теперь на этапе разведки и анализа мы будем рассматривать каждое подразделение enterprise как отдельную компанию и глубоко погружаться в его специфику. Определять, какие черты общие, а какие нет. После этого внедрения мы сформировали общий опросник, который позволяет понять, как происходит процесс разработки, как разработчики пишут свой код, как они применяют и выкатывают изменения, какая у них инфраструктура для продакшн-среды и др.


Интеграция такого масштаба это партнерские отношения, в которых все стороны должны договариваться и уметь гибко реагировать на изменения. Тем не менее, чтобы получить более или менее четкий план, лучше выбрать итеративный подход в его составлении.

Изменения в продукте по результатам проекта


1. Интеграция с AD


До этого проекта интеграция Solar appScreener с Active Directory не была оптимизирована под высокие нагрузки. Она начинала тормозить при подключении к каталогам с тысячами пользователей, с ней было не очень удобно работать. В процессе развития проекта систему доработали так, что никаких нареканий к ней уже не возникало. Теперь мы сможем интегрироваться с любой подобной системой, и проблем с производительностью у нас не будет.

2. Jira


Мы также серьезно улучшили все нюансы, связанные с Jira. До проекта мы полагались на дефолтную реализацию Jira, но у данной ритейловой компании Jira была очень серьезно переписана: добавлены различные поля, изменена дефолтная механика создания задач, все изначальные шаблоны. Поэтому мы внедрили в Solar appScreener инструмент, который позволяет подстраиваться под любой вид интеграции, потому что имеет возможность редактировать тело создаваемой задачи, которая посылается в Jira по API из Solar appScreener. Это очень гибкий инструмент.

Например, когда мы настраивали интеграцию с SAP, то столкнулись с тем, что наша старая интеграция с Jira выгружает какие-то дефолтные поля задачи, ты пытаешься создать задачу, и это не получается. Думаешь, что происходит?. Открываешь Jira, а там все не так, Jira по API выдает абсолютно не те дефолтные поля, не того типа данных, которые на самом деле должны содержаться. Но поскольку на эту реализацию у заказчика были завязаны процессы, то проблемы надо было решать на стороне Solar appScreener.

В результате в SAP нужно было заранее определить поля estimation time to do this task. Estimation time приходит как string, заходишь и видишь в нем два поля. А если открыть XML, то обнаруживается, что в этих полях данные должны быть описаны определенным образом. То есть это уже, по сути, словарь со множеством значений, где нужно сразу прописать время, например, one day в формате именно 1d. Ты это исправляешь, пытаешься запустить интеграцию и опять что-то не работает. Смотришь и понимаешь: какое-то поле, которое выгрузилось как обязательное, действительно числится как обязательное. Но при этом в самом интерфейсе его заполнять не нужно. Пару раз вот так потыкавшись, ты в итоге реализуешь интеграцию, но если бы у нас не было возможности гибкой настройки любого вида полей, то мы бы не смогли настроить интеграцию с такой кастомной версией Jira.



3. Изменение архитектуры приложения


Параллельно с этим проектом мы серьезно модернизировали архитектуру нашего приложения. Это потребовало существенных трудозатрат, так как необходимо было перейти от парадигмы цельного монолита, нагрузка от работы которого полностью ложилась на сервер, на классическую модель backend-frontend. Из-за чего пришлось применять другие подходы во всей разработке, проектировании, создании своего API и изменении стека технологий. В итоге это решение существенно упростило создание автоматизации так как именно наш новый API стал её основным инструментом и точкой входа.

Для себя мы выяснили, что есть очень много запросов на переход с MariaDB/MySQL, функционала которой было достаточно для работы приложения, на PostreSQL. Особенно со стороны крупного бизнеса, для которого эта база данных является одной из наиболее распространенных благодаря широкому функционалу и хорошей поддержке. Так, заказчик в определенный момент отказался от поддержки MariaDB, и их подразделение баз данных перешло на работу исключительно с PostgreSQL. Поэтому нам пришлось внедрять поддержку этой базы данных.

Резюме


В целом, этот заказчик, будучи крупной компанией, очень сильно помог развитию нашего продукта. Это серьезный боевой кейс, который привнес много понимания, как должно эволюционировать решение. По итогам внедрения мы сформулировали подходы к оценке рисков и планированию проекта в сегменте enterprise и задекларировали опросник для сбора информации об инфраструктуре заказчика.

Проект выявил узкие места, и мы осознали, как с ними бороться в будущем. Наша команда поняла для себя, как должна выглядеть архитектура Solar appScreener через год, чтобы продукт улучшался, расширялись его варианты поставки. Многое для себя выяснили с точки зрения улучшения пользовательского опыта работы с продуктом. В некоторых местах не хватало поиска по пользователям. Нужно было дать возможность добавить пользователя в локальную группу из LDAP и других протоколов сразу в нескольких, связанных с панелью администрирования местах (в настройках конкретного проекта, в настройках пользователя и т.п.).

Все участники этого проекта с нашей стороны сейчас занимаются развитием продукта, и у них есть общее понимание, как он должен масштабироваться на крупные внедрения процесса безопасной разработки. Мы работаем над очередной ещё более глобальной модернизацией архитектуры всего приложения, которое позволит его масштабировать и изменять более гибко, сделает более универсальной развёртку и проще взаимодействие для пользователей.

Это заключительная часть цикла статей о построении процесса безопасной разработки в крупном ритейлере. Будем рады ответить на вопросы и узнать о вашем опыте реализации практик безопасной разработки в комментариях!

Автор: Иван Старосельский, руководитель отдела эксплуатации и автоматизации информационных систем
Подробнее..

Особенности киберохоты как Hunter Stealer пытался угнать Telegram-канал База

08.02.2021 16:12:50 | Автор: admin

С ноября 2020 года участились случаи похищения аккаунтов у популярных Telegram-каналов. Недавно эксперты CERT-GIB установили тип вредоносной программы, с помощью которой хакеры пытались угнать учетку у Никиты Могутина, сооснователяпопулярного Telegram-канала База (320 тысяч подписчиков). В той атаке использовался стилер Hunter, который Могутину прислали под видом рекламы образовательной платформы. Сам стилер нацелен на устройства под управлением ОС Windows, поэтому атакующие так настойчиво просили журналиста открыть архив на рабочем компьютере, а не с iPhone, чего он и правильно делать не стал. Hunter "умеет" автоматически собирать учетные записи на зараженном компьютере, затем отсылает их злоумышленнику и раньше его, к примеру, часто использовали для кражи учетных данных у игроков GTA. Никита Карпов, младший вирусный аналитик CERT-GIB, провел анализ вредоносного файла и рассказал об особенностях киберохоты на популярные Telegram-каналы .

Кейс Базы

Стилер написан на C++ и рассылается владельцам каналов под видом предложения рекламы. Например, экземпляр, разбираемый в этой статье, маскировался под рекламу от GeekBrains.

Данный стилер продается на нескольких форумах и активно рекламируется самим продавцом.

Функционал

Функционал стилера реализован в методах, представленных ниже:

В процессе работы каждого метода похищенные данные записываются во временный файл со случайными именем и расширением:

Полученные временные файлы помещаются в архив внутри памяти процесса и затем удаляются.

Discord

Для похищения сессии Discord стилер ищет папку \discord\Local Storage\leveldb\ и копирует содержимое каждого файла во временный файл.

В архиве файлы записываются в папку с названием приложения и сохраняют изначальное имя файла Application\File.name.

Для примера была создана папка \discord\Local Storage\leveldb\ с файлом T.token.

Скриншот рабочего пространства

Для получения длины и ширины экрана используется win API GetSystemMetrics(). GetDC(0) используется, чтобы получить handle всего экрана, и с помощью CreateCompatibleBitmap() создается bitmap-объект, который сохраняется в файл Desktop.png.

Сбор файлов

Стилер получает путь USERPROFILE = C:\Users\USERNAME и собирает все файлы с расширениями из списка:

  • .txt

  • .rdp

  • .docx

  • .doc

  • .cpp

  • .h

  • .hpp

  • .lua (скрипты для GTA SAMP)

Steam

Для стилера интересны файлы с расширением .ssfn в папке Software\Valve\Steam\ssfn и все файлы из папки Steam/config/.

Для обхода защиты Steam Guard и доступа к аккаунту необходимо, чтобы пароль был сохранен в Steam-клиенте (галочка Запомнить пароль). Также нужны файлы с компьютера жертвы, которые собирает стилер:

  • файл с расширением .ssfn

  • config.vdf

  • loginusers.vdf

  • SteamAppData.vdf

Telegram

Стилер проверяет наличие Telegram среди процессов и получает расположение исполняемого файла "Telegram.exe". Далее программа проверяет, есть ли passcode в папке key_datas, и, если он установлен стилер начинает похищать данные других приложений. Если же Telegram не защищен passcodeом, то стилер похищает из папки \tdata\ файл, начинающийся с D877F783D5D3EF8C (в конце может быть любой символ), и файл, находящийся в \tdata\ D877F783D5D3EF8C и начинающийся с map (в конце может быть любой символ). Этих двух файлов будет достаточно для похищения сессии Telegram.

GTA SAMP

Данный стилер изначально ориентирован на игроков в GTA SAMP и похищение их аккаунтов. Об этом говорят файлы, собираемые с компьютера жертвы (скрипты, которые могут относиться к игре) и файлы, собираемые из \Documents\GTA San Andreas User Files\SAMP\. Для стилера интересны следующие файлы:

  • USERDATA.DAT хранит информацию о серверах, записывается в SAMP\servers.fav

  • chatlog.txt записывается в "SAMP\chatlog.txt"

Браузеры

Учетные данные пользователей браузера Mozilla Firefox стилер похищает из файла logins.json, который ищет в \Mozilla\Firefox\Profiles. Из браузеров Google Chrome и Chromium собираются:

  • данные о местоположении

  • учетные данные от аккаунтов на ресурсах

  • данные для автозаполнения форм

  • данные карт

  • cookies

  • аккаунты Facebook

Данные криптокошельков

Для всех криптокошельков алгоритм одинаковый:

  • Расшифровывается путь, связанный с кошельком:

    • Atomic \Atomic\Local Storage\leveldb\

    • Electrum \Electrum\wallets

    • Ethereum \Ethereum\keystore

    • Zcash \Zcash\

    • Exodus \Exodus\exodus.wallet\

  • Содержимое всех файлов переносится во временные файлы и помещается в архиве в папку с именем кошелька:

Бот нашел тестовый файл в папке.

И переместил его в архив.

Отчет

После сбора всех пользовательских данных создается файл "readme.txt", содержащий информацию о похищенных данных.

Для всех приложений в отчете отмечается, получилось ли похитить данные.

Но для криптокошельков всегда отмечается -. Это может свидетельствовать о том, что похищенные данные кошельков не отправляются клиенту, а остаются у владельца сервера.

Все собранные файлы помещаются в архив и отправляются на CnC.

Взаимодействие с CnC

Адрес командного центра также находится в зашифрованных строках:

Сетевое взаимодействие происходит через IPv6 или, если он недоступен через IPv4 по протоколу TCP. Для отправки архива с похищенными данными открывается сокет с портом 0x8AE = 2222.

В виде админ-панели выступает Telegram-бот, который находится на VDS (Virtual Dedicated Server). Похищенные данные изначально попадают к владельцу сервера, а затем отправляются клиенту через Telegram-бота.

В дампе трафика, полученного из отчета модуля THF Polygon, видим передачу архива.

Шифрование

Алгоритм шифрования, используемый для защиты строк:

  • На вход функции дешифровки подаются три параметра:

    • XOR-константа для данного слова

    • константа для инициализации KSA (Key Scheduling Algorithm)

    • длина строки

  • Для генерации ключа используются два последовательных KSA. Первый ключ генерируется по длине нужной строки из константы и ключа размером 32 байта, который генерируется алгоритмом MurMurHash2 по 4 байта. Второй получает результат работы первого и еще один сгенерированный через MurMurHash2 ключ.

  • Для каждого символа ключа, полученного из второго KSA, выполняется операция XOR с константным значением.

Реализация алгоритма MurMurHash2Реализация алгоритма MurMurHash2

Противодействие анализу

Для противодействия анализу используются следующие методы:

  • шифрование строк, разобранное выше

  • определение наличия отладки через win API-вызов IsDebuggerPresent()

Продавец и разработчик

В объявлениях на форумах продавец Hunter указывает свой контакт в Telegram как @NoFex228. К этому аккаунту привязан профиль Александра ХХ. во ВКонтакте, где обнаружено несколько постов, связанных с различными стилерами и продажей аккаунтов из GTA SAMP. Telegram-аккаунт разработчика указан во всех отчетах бота как @karelli. К нему привязан телефонный номер, связанный со страницей "ВКонтакте" некоего Анатолия ХХ.

Заключение

Хотя Hunter Stealer не отличается ни обширным функционалом, ни серьезным противодействием анализу, вредоносное ПО, похищающее пользовательские данные, способно нанести серьезный ущерб. Опасность такого ВПО в скорости выполнения задач и простом доступе к ценной информации.

Если у владельца Telegram-канала не включен код-пароль приложения, то злоумышленникам не составит труда восстановить сессию и сменить владельца канала. Так что вывод прост: не открывать подозрительные сообщения, архивы и ссылки и не пользоваться ОС, где стилеры чувствуют себя как дома.

Что делать, если заражение уже произошло?

Пока компьютер инфицирован, устанавливать код-пароль нет смысла, так как вредоносная программа может обладать возможностями клавиатурного шпиона. К работе на этом ПК можно возвращаться только после нейтрализации вредоносного кода.

Для администратора необходимо оперативно завершить активный сеанс на инфицированном компьютере и с помощью другого устройства (например, смартфона) сменить облачный пароль в настройках конфиденциальности, установить код-пароль на всех других доверенных устройствах.

Подробнее..

CodeQL SAST своими руками (и головой). Часть 1

09.02.2021 16:13:11 | Автор: admin

Привет Хабр!

Как вы все уже знаете, в области безопасности приложений без статических анализаторов исходного кода (SAST) совсем никуда. SAST-сканеры занимаются тем, что проверяют код приложения на различные типы программных ошибок, которые могут скомпрометировать систему, предоставить злоумышленнику непредвиденные возможности для доступа к данным, либо для нарушения работы приложения. В основном анализ безопасности кода строится на изучении его семантической структуры, путей прохождения данных от момента пользовательского ввода до обработки. Однако есть и обычная для таких инструментов возможность поиска наиболее часто встречающихся небезопасных паттернов.

Ввиду того, что каждое приложение небезопасно по-своему, очень сложно разработать анализатор, который бы одинаково хорошо работал для каждого из них. К тому же большая часть решений, которые занимаются анализом исходного кода, страдает от различных недостатков, таких как цена, непредсказуемость результатов, большое количество ложных срабатываний (как ложно-положительных, так и ложно-отрицательных) и недостаточные возможности для кастомизации проверок.

В этой статье я расскажу о CodeQL, интересном инструменте и языке для анализа исходного кода, который активно набирает популярность и выглядит весьма перспективным. CodeQL помогает стать на один шаг ближе к исправлению вышеописанных недостатков обычных анализаторов кода. Он будет полезен как аналитикам информационной безопасности, так и в целом разработчикам, которые хотят повысить качество кода.

Также я активно призываю попробовать этот инструмент на своих собственных кодовых базах и помочь совместно создать сообщество CodeQL-экспертов (и вообще специалистов по анализу кода) для обмена опытом и наработками, обсуждения проблем и их возможных решений - ссылка в конце статьи.

Содержание

1. Что такое CodeQL
2. Сценарии использования CodeQL
3. Демонстрация работы
4. Консоль LGTM
5. Установка CodeQL
6. Кодовая база
7. Как выглядит простой CodeQL запрос
8. Что дальше?
9. Дополнительные материалы

Что такое CodeQL

CodeQL это open-source инструмент и язык запросов, немного напоминающий SQL и позволяющий программным образом обращаться к тем или иным участкам кода и выполнять заданные аналитиками проверки графа потоков данных/управления и структуры исходного кода в целом. Аналогом этому подходу являются конфигурируемые правила поиска уязвимостей в инструментах SAST (Static Application Security Testing).

Проще говоря, это инструмент, при помощи которого можно проверить некоторые гипотезы относительно кода или идущих через него данных. Например можно составить запросы, которые проверят есть ли путь для данных от места пользовательского ввода до небезопасного участка кода, где эти данные выводятся. С точки зрения исследования кода на качество можно, например, найти все функции, принимающие на вход более 5 аргументов или найти пустые циклы for/while.

В конечном итоге такой анализ, будучи автоматизированным, оказывается очень полезным для упрощения процесса оценки качества и безопасности разрабатываемого кода, а в долгосрочной перспективе приводит к общему повышению зрелости приложения.

Изначально CodeQL это разработка компании Semmle, которая в сентябре 2020 была куплена GitHub и внедрена в их Security Lab. С этого момента развитием продукта занимается сам GitHub и небольшое сообщество энтузиастов. На данный момент официальный репозиторий содержит суммарно свыше 2000 QL-запросов, которые покрывают большое количество разнообразных проблем с кодом, начиная от поиска некорректных регулярных выражений в JavaScript и заканчивая обнаружением использования небезопасных криптографических алгоритмов в коде на C++.

Одним из преимуществ CodeQL является то, что он может не просто искать проблемные участки кода по определенному шаблону (как, например, инструмент SemGrep), но также и понимать структуру кода на уровне отдельных инструкций и выражений, отличать вызов функции от вызова метода класса, а также отслеживать путь прохождения значения через выражения и дальнейшие операции с этими выражениями (например присвоение значения переменной, вызов функции с этой переменной в качестве параметра и последующее присвоение результата новой переменной).

На данный момент с разной степенью полноты поддерживаются следующие языки: C/C++, C#, Java, Go, Python, JavaScript/TypeScript. Помимо этого для каждого языка есть набор поддерживаемых фреймворков, упрощающий написание запросов.

CodeQL предоставляется в нескольких вариантах:

  1. Консольная утилита, позволяющая встроить проверки в CI/CD цикл и осуществлять сканирование кода из командной строки.

  2. Расширение для Visual Studio Code для удобного написания и ad-hoc исполнения запросов.

  3. Онлайн-консоль LGTM, позволяющая писать запросы и проводить тестовое сканирование приложения из заданного GitHub-репозитория.

Кроме этого можно подключить сканирование своих репозиториев непосредственно в CI/CD на GitHub.

Всё ещё не убеждены попробовать? Тогда подкину дополнительную мотивацию. GitHub проводит соревнования CTF и вознаграждаемые bug bounty программы для энтузиастов, которые предлагают новые запросы, помогающие обнаруживать как уже известные и документированные уязвимости (CVE), так и 0-day уязвимости.

Сценарии использования CodeQL

Давайте посмотрим, какие есть потенциальные варианты использования CodeQL в нашем проекте:

  1. Самый простой сценарий состоит в том, что мы просто запускаем сканер со всем набором стандартных запросов и разбираем результаты, среди которых будут и проблемы с качеством кода, и проблемы с безопасностью.

  2. Сценарий посложнее включает в себя два этапа запуска сканера со встроенными запросами. Первый только с запросами, относящимися к качеству кода (пустые блоки, избыточные комментарии, большое количество параметров функции и т. п.), а второй с запросами непосредственно на проверку безопасности. В дальнейшем результаты разбирают две независимые группы ответственных сотрудников.

  3. Самый продуктивный сценарий включает в себя модификацию базовых запросов и/или написание собственных новых запросов, исходя из специфики конкретного приложения и появляющихся угроз.
    Например при выходе очередной 0-day уязвимости аналитик безопасности может составить запрос, который будет проверять все проекты на ее наличие. Или при анализе дефектов, найденных в процессе внутреннего аудита, каждый такой дефект может быть переписан на языке QL, чтобы не допустить проблем в других проектах.

  4. Также CodeQL может быть использован для исследования кода в целом (например определить все точки входа в приложение, чтобы впоследствии эту информацию передать аналитикам, занимающимся динамическим анализом приложения).

Язык QL очень гибок и позволяет решить большое количество задач, связанных с анализом кода, при этом давая инструменты, чтобы точечно отсекать потенциальные места возникновения ложных результатов.

Демонстрация работы

Впрочем давайте сразу посмотрим, как выглядит синтаксис запроса и результат работы CodeQL на примере запроса в консоли LGTM.

Ниже представлен простой вариант запроса, при помощи которого мы ищем все пустые блоки кода на некоем тестовом приложении. Дальше в статье мы более детально посмотрим что есть что здесь.

Простой запрос CodeQL по поиску пустых блоковПростой запрос CodeQL по поиску пустых блоковОбнаруженный пустой участок кодаОбнаруженный пустой участок кода

Или более сложный случай, когда мы ищем проблемы с Cross-Site Scripting:

Запрос CodeQL, обнаруживающий XSS путём отслеживания путей недоверенных данныхЗапрос CodeQL, обнаруживающий XSS путём отслеживания путей недоверенных данных

Результат включает в себя все промежуточные шаги (в данном примере только стартовый и конечный), которые привели данные от места ввода до вывода и участки кода, в которых заключена проблема:

А вот так тот же результат выглядит в расширении для VSCode:

На приведенном скриншоте мы видим инструкции CodeQL (верхняя панель), которые отслеживают путь данных от удалённых точек ввода пользовательских данных (например параметры GET-запросов) до конструкций кода, способных отобразить эти недоверенные данные пользователю. При этом отдельной инструкцией isSanitizer указывается, что в коде присутствует санитизирующая функция и соответственно если поток подозрительных данных проходит через эту функцию, он не должен дальше восприниматься как недоверенный. Это один из нескольких способов, которыми мы можем уменьшить количество заведомо ложных срабатываний.

В свою очередь, в результатах выполнения запроса (нижняя панель) мы можем посмотреть участок кода, где в приложении появляются недоверенные данные (т. н. source) и участок кода, где они выводятся (т. н. sink).

Консоль LGTM

Чтобы поэкспериментировать с языком без локальной установки пакета CodeQL можно воспользоваться онлайн-консолью LGTM (Looks Good To Me). Она включает в себя простой редактор запросов и возможность выполнить этот запрос на уже предустановленных кодовых базах нескольких open-source проектов, либо на своем GitHub-проекте.

Давайте сразу попробуем исполнить простейшие запросы и начать практическое знакомство с CodeQL:

  1. Переходим в онлайн-консоль: https://lgtm.com/query/.

  2. Выбираем в качестве языка JavaScript, а в качестве проекта meteor/meteor.

  3. Копируем нижеприведенные запросы.

  4. Нажимаем Run и смотрим результаты в панели под полем ввода кода.

Простой запрос, отображающий все места в анализируемом исходном коде, где объявляются классы выглядит так:

import javascriptfrom ClassExpr ceselect ce

Более сложный запрос, который покажет нам все места в файле client.js, где происходит вызов функции eval(), а также аргументы этой функции:

import javascriptfrom CallExpr callwhere call.getCalleeName() = "eval" and call.getLocation().getFile().getRelativePath().matches("%client.js")select call, call.getAnArgument()

Еще более продвинутые запросы позволяют задать точку входа данных (например конкретный параметр некоторой функции) и точку выхода (например параметр той же функций eval()) для того, чтобы отследить проходят ли данные от начальной до конечной точки. Но про такие запросы мы поговорим в следующей части статьи.

Установка CodeQL

Для использования в своих проектах на постоянной основе консоль LGTM не очень удобна, поэтому есть смысл установить CodeQL CLI и библиотеки локально.

Процесс установки всего пакета в целом несложен, но требует понимания ряда нюансов.

Простой вариант, с которого можно начать, выглядит так:

  1. Установить VSCode и CodeQL extension.

  2. Скачать и распаковать CodeQL CLI в директорию, например, codeql.

  3. Прописать путь до директории codeql в %PATH%.

  4. Скачать стартовый воркспейс VSCode для работы с CodeQL (впоследствии можно будет сделать свой, но для начала работы можно использовать готовый):
    git clone https://github.com/github/vscode-codeql-starter/

    git submodule update --init --remote

    В нем мы будем работать (то есть писать наши запросы) в папке, соответствующей интересующему нас языку (например для JS это codeql-custom-queries-javascript).

  5. Скачиваем тестовую кодовую базу (то есть базу, в которой определенным образом хранятся все необходимые данные о коде и внутренних взаимосвязях в нем, о чем подробнее будет рассказано ниже), например (для JS) https://github.com/githubsatelliteworkshops/codeql/releases/download/v1.0/esbenabootstrap-pre-27047javascript.zip
    Чуть ниже мы посмотрим как создавать свои собственные кодовые базы для наших проектов.

  6. Опционально распаковываем архив с кодовой базой.

  7. В VSCode выбираем Open workspace и открываем файл стартового воркспейса.

  8. В VSCode на закладке CodeQL добавляем папку (или архив) с кодовой базой, против которой будет запускаться анализ кода.

  9. Готово. Теперь в соответствующей папке воркспейса (см. шаг 4) можно открыть файл example.ql и в нем начать писать свои запросы.

  10. Исполняем тестовый запрос и убеждаемся, что всё работает

import javascriptfrom Expr eselect Wazzup!

Кодовая база

В CodeQL весь анализируемый код должен быть специальным образом организован в т. н. кодовую базу, к которой мы будем впоследствии выполнять запросы. В ней содержится полное иерархическое представление этого кода, включая абстрактное синтаксическое дерево (AST), граф потока данных и граф потока управления. Языковые библиотеки CodeQL задают классы, которые добавляют уровень абстракции относительно таблиц в этой базе. Другими словами у нас появляется возможность писать запросы к кодовой базе, используя принципы ООП. Это как раз та особенность, которая отличает CodeQL от инструментов, которые ищут проблемы в коде при помощи заранее заданных шаблонов и regex'ов.

Кодовая база CodeQL также включает в себя архив с исходным кодом и его зависимостями. Этот же исходный код потом используется, когда мы смотрим результаты выполнения запроса.

Для разных языков процесс создания базы немного отличается. Например создание кодовой базы для JS в папке my-js-codebase выполняется следующей командой в директории, которая содержит исходный код:

codeql database create my-js-codebase --language=javascript

Для компилируемых языков требуется, чтобы в системе был соответствующий компилятор и сборщик (например Maven для Java)

Следующий шаг загрузить информацию о базе в VSCode. Это делается в редакторе на соответствующей вкладке CodeQL Choose Database from Folder

К сожалению механизма обновления кодовой базы не существует, поэтому если в исходный код вносятся какие-либо изменения, необходимо пересоздавать всю базу целиком.

Как выглядит простой CodeQL запрос

Давайте разберем, из чего вообще состоит типичный запрос в CodeQL на примере кодовой базы на языке JavaScript.

Самый базовый запрос, который выводит все jQuery-функции с именем $ (типа $(arg1, arg2)) и их первый аргумент, выглядит так, как показано ниже. Вы можете самостоятельно выполнить его для любой кодовой базы с jQuery:

/*** @name QueryName* @kind problem* @id my_id_1*/// метаданныеimport javascript // Выражение для подключения библиотеки CodeQL для работы с конструкциями JavaScript.// Также есть возможность подключения других библиотек для работы с различными фреймворками и технологиями.// Например semmle.javascript.NodeJS или semmle.javascript.frameworks.HTTP.from CallExpr dollarCall, Expr dollarArg // Объявление переменной dollarCall типа CallExpr и переменной dollarArg типа Expr.// CallExpr - это класс из стандартной библиотеки, представляющий коллекцию всех вызовов функций в интересующем нас коде.// Expr - класс, представляющий коллекцию всех выражений. Например в Object.entries = function(obj) выражениями являются //   вся строка целиком, Object, Object.entries, entries, function(obj), obj.where dollarCall.getCalleeName() = "$"// Логические формулы, которые мы применяем к объявленным переменным.// Мы проверяем, что результат выполнения предиката (т.е. логической инструкции) getCaleeName() (который возвращает название // вызываемой функции) нашего объекта dollarCall (который содержит все вызовы функций) равен "$"and dollarArg = dollarCall.getArgument(0)// Эта логическая формула операцией AND соединяется с предыдущей и уточняет условие, применяемое в запросе.// В итоге мы из всех вызовов функций, в названии которых есть $ выбираем в переменную //  dollarArg первые аргументы (как сущности, а не как конкретные значения аргументов).select dollarCall, dollarArg // указание на то, какие выражения (значение каких переменных или предикатов) мы хотим вывести в результате.

Как вы можете заметить, синтаксис языка схож с синтаксисом SQL, но при этом в основе лежат концепции ООП. В последующих частях мы познакомимся чуть поглубже с нюансами, терминами и идеями, которые лежат в основе CodeQL.

Что дальше?

В этой вводной статье мы немного прикоснулись к тому, что такое CodeQL и чем он может быть нам полезен, как начать с ним работать и начать создавать свои первые запросы.

Рекомендую поэкспериментировать с запросами на разных приложениях (либо собственных, либо open-source) хотя бы в онлайн-консоли LGTM.

Помимо это есть два неплохих обучающих мини-курса от самих разработчиков, которые помогут вам лучше понять основы и базовые механики CodeQL. Они не требуют знания соответствующих языков, но раскрывают возможности CodeQL для анализа кода. Советую их посмотреть и при желании пройти до конца:

https://lab.github.com/githubtraining/codeql-u-boot-challenge-(cc++) интерактивный курс по работе с CodeQL на примере C/C++

https://lab.github.com/githubtraining/codeql-for-javascript:-unsafe-jquery-plugin интерактивный курс по анализу JavaScript Bootstrap с помощью CodeQL.

Плюс к этому для начала подойдет очень полезный двухчасовой мастер-класс от GitHub, на котором рассматривается база CodeQL и где при помощи лектора зритель учится шаг за шагом писать запрос для поиска небезопасной десериализации в коде Java-приложения (фреймворк XStream):

Наши же дальнейшие планы включают в себя написание продолжения статьи, в котором мы более детально рассмотрим терминологию CodeQL, познакомимся с новым типом запросов, обсудим дополнительные механизмы и инструкции, которые позволят более гибко и точно анализировать исходный код и ряд других важных вещей.

It is dangerous to go alone! CodeQL инструмент достаточно сложный, с большим количеством нюансов и, к сожалению, с пока еще не очень большой экспертизой в мире. Поэтому мы бы хотели уже сейчас заняться развитием русскоязычного сообщества экспертов в CodeQL для обмена опытом и совместного решения проблем (которые, разумеется, тоже существуют). Для этой цели мы создали отдельный канал в Telegram, посвященный обсуждениям нюансов этого инструмента и расширению круга экспертизы. Там же мы публикуем новости, обучающие материалы и другую информацию по CodeQL.

Присоединяйтесь - https://t.me/codeql !

Дополнительные материалы

Нижеприведенные ссылки помогут найти основную часть информации по языку, его стандартным библиотекам и возможностям:

https://help.semmle.com/codeql/ общая помощь по CodeQL от изначальных разработчиков.
https://help.semmle.com/QL/ql-handbook/ референс по синтаксису языка.
https://help.semmle.com/QL/learn-ql/ детальная помощь по CodeQL для разных языков.
https://securitylab.github.com/get-involved информация по тому, как можно узнать больше про CodeQL, помочь его развитию, а также по тому, как получить инвайт в Slack-канал (англоязычный) с ведущими экспертами со всего мира и разработчиками самого CodeQL.

Подробнее..

Проблемы в процессах непрерывной доставки и развертывании программного продукта

04.08.2020 16:15:07 | Автор: admin
Статью подготовил Брюханов Константин, руководитель курса CI/CD. В ней Константин раскрыл ряд проблемных моментов, связанных доставкой развертыванием кода программного продукта в IT-компаниях, и собрал рекомендации из числа лучших международных практик.



В айти-эксплуатации самым востребованным направлением является именно наладка и обеспечение непрерывной доставки и развертывания. Технологии и методологии постоянно развиваются, совершенствуются инструменты. В связи с этим последние требования к доставке и развертыванию включают в себя готовность и непрерывность тестирования изменений, подготовку и настройку тестовой среды для команды обеспечения качества.

Технологический прорыв и свободно-распространяемое ПО привели к тому, что подход к организации процессов CI/CD значительно изменился. Переход на новые принципы сильно повлиял на корпоративную культуру, востребованные навыки сотрудников и сами принципы работы в организациях, что привело к масштабным переменам в мире разработки ПО.

Облачные решения становятся все более приоритетными. Для непрерывной поставки ПО необходимо эффективное взаимодействие групп разработки, тестирования и эксплуатации, а облачная среда отлично подходит для такого взаимодействия.

Однако этап развертывания, выполняемый в сложной распределенной топологии, подвержен ошибкам и, как правило, требует ручного поиска и устранения неисправностей. Этап развертывания продуктов в рамках процесса непрерывной поставки часто создает узкие места и отрицательно влияет на эффективность процесса DevOps.

Непрерывная поставка позволяет достичь автоматизации тестирования инкрементальных изменений ПО и оперативного развертывания обновлений максимально эффективными и безопасными методами. Такой подход дает пользователям уверенность, что в производственной среде используется самая свежая версия кода, и изменения, вносимые программистами, могут попадать к клиентам за считанные часы или даже минуты.

Рассмотрим наиболее общий сценарий реализации CI/CD в проекте:

  1. Команда разработки выпускает новую версию продукта (новый функционал или исправление ошибок предыдущего релиза).
  2. Сервис непрерывной интеграции (CI) проверяет новый код рядом тестов, включающих несколько уровней тестирования, такие как синтаксические, модульные и регрессионные тесты.
  3. В случае успеха сервис непрерывной интеграции готовит новую сборку программного продукта и совершает системный вызов к сервису, осуществляющему непрерывную доставку (CD).
  4. Совместно с сервисом непрерывного развертывания (часто, этот функционал выполняет один и тот же сервис) автоматическим образом поднимается сущность, отвечающая за промежуточное развертывание (staging), где в условиях близких к производственным проводятся дополнительные интеграционные, дымовые и нагрузочные тесты.
  5. После успешного проведения stage-тестов система CI/CD доставляет новую версию продукта на производственные мощности, где выполняется бесшовное развертывание параллельной инсталляции продукта.
  6. Пользовательский трафик переключается на новую версию ПО.


Данный сценарий является наиболее общим и покрывает большинство нужд команд разработки и эксплуатации, но всё же имеет некоторые проблемы, например:

  1. Замена файлов. Зачастую требуется обновление или замена конфигурационных файлов или перегенерация какого-то статичного контента. В этом случае пользователи могут получать ошибки, пока не произошло переключения трафика со старой версии ПО на новую. В случае же, когда развертывание закончилось неудачей, есть риск несовместимости файлов.
  2. Обновление системных ресурсов. Предположим, ваша система конфигурируется под самообслуживание, настраиваются планировщики и системные демоны. Во время развертывания новой версии у вас могут заменяться команды и префиксы для системных вызовов. Однако существует вероятность, что какие-то из этих функций потребуются для работающей сейчас версии приложения, из-за чего оно не будет должным образом обслужено и работа сервера нарушится.
  3. Конфигурирование баз данных. Изменения конфигурации базы данных порождают наиболее сложные проблемы. Обновление решения приводит к изменениям структуры таблиц, пользовательских полномочий, сохраненных данных и т.д. При выполнении развертывания база данных может стать недоступна для работающего приложения на время применения новых миграций. Также существует вероятность ошибки развертывания, из-за чего база данных уже не будет соответствовать требуемой для нормальной работы приложения, и даже повторное развертывание прежней версии не решит проблему, т.к. потребуется разработка специальных обратных миграций.


Стоит заметить, что перечисленные выше проблемы могут возникнуть даже в близкой к идеальной среде, но одна из основных сложностей внедрения методологии DevOps состоит в том, что нет единой картины, как должен выглядеть процесс непрерывной доставки и развертывания продукта. Многие IT-компании знают о DevOps слишком мало, иногда и вовсе не понимают этой методологии, в других же уже есть исторически сложившиеся решения, поверх которых приходится выстраивать новые процессы. С учетом высоких требований к квалификации Devops-специалистов, и их нехваткой на рынке труда работодатель зачастую вынужден использовать уже имеющиеся у него ресурсы и отдавать Devops-задачи начинающим инженерам. Как итог, в системе получается еще больше слабых мест.

При использовании CI/CD без правильного понимания методологии, без аналитического подхода к построению инфраструктуры и методов доставки кода вытекают следующие проблемы:

1. Человеческий фактор. Первый и самый существенный риск связан с человеческим фактором. Представим себе ситуацию, когда необходимо настроить ещё несколько серверов, аналогичных существующим. Если специалист, который производил предыдущие установки или настройки в данный момент недоступен по каким-либо причинам (болен, уволился и т.д.) и не подготовил подробных инструкций, то ситуация значительно осложняется. В этом случае каждый новый специалист должен изучить весь процесс настройки сервера полностью, при этом у него нет права на ошибку. А кроме того, нельзя точно оценить, сколько времени уйдет на настройку, и гарантировать ее успех.

Сюда же относятся риски связанные с тем, что автор метода допустил ошибку, забыл покрыть тестами процессы или просто-то что-то не учел, а его преемник этого не заметил.

Не стоит также забывать, что нередко в компаниях разрабатываются несколько проектов, а отдел IT-эксплуатации обычно один, и один инженер эксплуатации обслуживает несколько проектов. Если нет единой схемы и концепции, то процессы в разных командах будут выстроены по-разному, что значительно затруднит последующее развитие Devops в компании и сделает высокий порог вхождения инженера эксплуатации в другой проект, где уже используются процессы, отличные от тех, с которыми он работал ранее.

2. Неидемпотентные сценарии. Идемпотентность является важнейшим атрибутом сценариев непрерывной доставки и развертывания кода особенно в условиях развертывания инфраструктуры. Инженер должен быть уверен, что каждый раз при выполнении сценариев результат будет однозначно гарантированным, ожидаемым и неизменным при повторном проигрывании того же сценария. Зачастую при внедрении Devops в компании инженеры стараются разработать бизнес-решение и могут не учесть идемпотентность или же просто не знать об этом требовании. В этом случае компания получает бомбу замедленного действия, т.к. существует вероятность доставить на производственные мощности неожидаемый код. Например, если кто-то обновил модуль системы управления конфигурациями для одного проекта, и тем самым повлиял на остальные, где этого не ожидают.

3. Хранение чувствительных данных и организация доступа. Один из самых важных моментов в Devops-подходе к хранению секретных данных, ограничению прав, организации сетевого и пользовательского доступа. До сих пор нет единых принятых практик и инструментов, позволяющих решить эту проблему, и инженерам приходится каждый раз проводить исследования в зависимости от текущей организации инфраструктуры и принятых методов ограничения доступа. По этой причине внедрение методологии Devops на предприятии усложняется тем, что нельзя однозначно найти решение под свой частный случай, а применение чужих практик не всегда гарантирует безопасность.

4. Определенная модель бюджетирования, более подходящая для Waterfall методологии.

5. Высокие требования к безопасности. Следовательно, невозможно разместить инфраструктуру национальных IT-проектов в зоне ответственности коммерческих, иностранных компаний, например, Amazon, Microsoft.

6. Большой объем legacy code, legacy infrastructure, которые необходимо поддерживать. Необходимость интеграции с большим количеством устаревших систем.

Таким образом процесс построения Devops на предприятии может сопровождаться рядом проблем и не всегда решать задачи, для которых создан.

Первым важным шагом является отказ от отношения к серверам, как трудно настраиваемому, уникальному элементу инфраструктуры, переход от ручной настройки сервера к автоматизированному, централизованному управлению инфраструктурой. Процесс настройки каждого сервера должен быть описан в виде конфигурации, легко читаемой, изменяемой, и готовой к многократному безопасному переиспользованию, обеспечивающему однозначный гарантированный результат. Примерами промышленных систем-оркестраторов являются Chef или Ansible. Данные системы позволяют управлять большим количеством серверов с минимальными затратами.

Следующим важным шагом является применение автоматизированного тестирования, чтобы для как можно большего покрыть функционал разрабатываемого кода (как программного, так и инфраструктурного). Иначе говоря, имея развернутую инфраструктуру, но без автоматизированного тестирования, узким местом процесса разработки будет своевременная проверка функционала. Автоматизирование процесса тестирования должно начинаться с собственно написания кода ПО (модульное тестирование), применения первичных тестов на сервере, который отвечает за сборку ПО, а также тест конфигурации серверов. Это позволит снизить нагрузку на команду обеспечения качества ПО и значительно снизит время прохождения ПО на конвейере.

Заключительным логичным шагом является централизованный сбор и анализ лог-файлов со всех серверов для своевременного оповещения всех заинтересованных лиц и проактивном наблюдении за состоянием инфраструктуры в целом.

Приведенные выше рекомендации позволят построить устойчивую масштабируемую инфраструктуру, способную работать в интенсивном процессе разработки. Внедрение DevOps требует вовлеченности в процесс каждого участника от отделов тестирования и разработки до менеджеров и отдела эксплуатации. На каждом этапе требуется постоянный ретроспективный анализ процесса, ведь в результате случайных ошибок при изменении конфигурации полностью останавливается работа систем. Необходимо улучшать телеметрию, чтобы эффективнее обнаруживать ошибки и осуществлять восстановление, а так же для защиты конвейера развертывания и достижения целей по изменению управления. Это позволит получать максимальную поддержку со стороны руководства при внедрении инициатив DevOps, создать более оживленную и дружелюбную рабочую среду, чтобы любой участник смог учиться в течение всего времени это не только поможет каждому исполнителю достичь целей, но и приведет организацию к успеху.

За 3 месяца на нашем онлайн-курсе CI/CD вы сформируете понимание архитектуры облачных провайдеров, изучите автоматизацию анализа кода и поиска уязвимостей и научитесь настраивать процессы сборки, тестирования и установки приложения от трех крупнейших провайдеров. Программа рассчитана на специалистов с опытом администрирования специальный вступительный тест поможет сориентироваться, хватит ли вам подготовки для обучения.



Читать ещё:


Подробнее..

Анализатор C на первом курсе миф, иллюзия или выдумка?

06.11.2020 12:09:31 | Автор: admin
Для программистов настали тяжёлые времена. Хотя Утечка Памяти была уничтожена valgrind-ом, оставшиеся силы UB преследовали программистов по всей галактике.

Избегая встречи с грозными знаковыми переполнениями, группа борцов за свободу, ведомая Кириллом Бриллиантовым, Глебом Соловьевым и Денисом Лочмелисом, обустроила новый секретный репозиторий.

Тёмная владычица UB неинициализированная переменная, одержимая желанием сломать все программы галактики, разослала тысячи раздражающих ошибок в самые далекие уголки космоса


Мы трое студентов бакалавриата Прикладная математика и информатика в Питерской Вышке. В качестве учебного проекта во втором полугодии мы решили написать UB-tester анализатор кода на С++.




Вступление


Реальная история: контест идет, времени остается мало, кодить задачки надо быстро. Именно в тот момент, когда разум участника охвачен быстрыми алгоритмами и тикающими секундами, С++ начинает выходить из-под контроля. В итоге программист старательно и очень долго ищет ошибку в реализации алгоритма, в неразобранном крайнем случае, в себе. Но пропускает случайно набранный неправильный индекс при обращении к массиву, неожиданное переполнение при арифметических операциях или заезженную неинициализированную переменную: ошибку или опечатку непосредственно в коде.

С такой ситуацией каждый из нас сталкивался лично. Поэтому в качестве проекта по С++ мы решили создать инструмент, с помощью которого будет удобно находить такие ошибки.

Небольшое введение в контекст
Анализаторы кода делятся на две группы: статические и динамические. Статические анализируют код без реального выполнения программы. Динамические, напротив, пытаются извлечь информацию во время ее исполнения. Самые известные примеры статический clang static analyzer с большим функционалом, его платный аналог PVS studio, а также динамические valgrind и sanitizer.

Нам предстояло выбрать основной метод обнаружения ошибок. Источником вдохновения послужил достопочтенный С-шный макрос assert. Если программист регулярно проверяет инварианты с помощью assert-а, то поиск ошибки при отладке становится намного более локализованной и простой задачей. Но вряд ли кто-то в здравом уме использовал бы такую проверку постоянно, обращаясь к массиву или складывая два int-а. Мы решили создать инструмент, который будет анализировать пользовательский код, а затем заменять ошибкоопасные места на написанные нами обертки, в случае чего выбрасывающие пользователю подробный лог ошибки. К примеру:


Пример: программа, складывающая два числа типа int, может вызывать UB при переполнении знакового числа, а ее обработанная версия нет.

Безусловно, производить такие замены в исходном коде пользователя далеко не вежливо, поэтому результатом работы нашей программы является новая версия оригинального кода со вставленными обертками.

Такой подход к обнаружению ошибок оказался удачным или, по крайней мере, интересным. Пользователь получил возможность автоматически тестировать свою программу на предмет простых UB без лишних усилий и затрат времени.

Финальной версии не было суждено стать новым valgrind-ом, а функционалу покорить сердца миллионов, но процесс разработки оказался весьма захватывающим. Про это мы и расскажем далее.

Сlang и AST


Первым шагом работы ub-tester является нахождение всех ошибкоопасных мест в программе. Парсить код на C++ вручную и с нуля путь в могилу (или по крайней мере в отдельный проект), поэтому нам предстояло подобрать инструменты для решения этой задачи. Пожалуй, самым известным и наиболее практичным инструментом по разбору исходного кода на плюсах является clang, поэтому мы выбрали его в качестве основы для нашего проекта. Сlang большая библиотека, поэтому остановимся только на важных для понимания статьи понятиях.

Сперва стоит сказать об AST (Abstract Syntax Tree). Это структура данных дерево (как можно было догадаться из названия), внутренние вершины которого соответствуют операторам языка, а листья операндам. Вот пример AST для простой программы.

Исходный код:
if (a > b) {print(a);} else { print(b);}

AST:


Описанная абстракция и используется в clang-е для представления исходного кода. Такой структуры данных достаточно для обнаружения ошибкоопасных мест. К примеру, чтобы найти в программе теоретически возможные index out of bounds, нужно изучить все вершины AST, соответствующие операторам обращения к массиву по индексу.

Теперь дело остается за малым: получить доступ к AST. В clang-е существует несколько механизмов для решения этой задачи. Мы пользовались Visitor-ами.

В clang-е реализован базовый класс RecursiveASTVisitor, осуществляющий дфсный обход AST. Каждый Visitor должен от него наследоваться, чтобы уметь путешествовать по дереву. Далее, чтобы выполнять специфичные для определенных вершин операции, класс-наследник должен иметь набор методов вида Visit***, где *** название узла AST. Оказываясь в вершине дерева, Visitor вызывает соответствующий ее названию метод Visit***, затем рекурсивно спускается в ее детей. Таким образом, чтобы создать своего Visitor-а, достаточно реализовать методы обработки конкретных вершин.

Вот пример небольшого класса, который посещает все бинарные операторы и выводит на экран их местоположения в коде.

class FindBinaryOperatorVisitor : public clang::RecursiveASTVisitor<FindBinaryOperatorVisitor> { public:bool VisitBinaryOperator(clang::BinaryOperator* BinOp) { std::cout << BinOp->getBeginLoc() << \n;return true;}};

Теперь можно со спокойной душой переходить к следующему пункту: к замене участков исходного кода.

CodeInjector


Сразу скажем, что, вероятно, мы избрали далеко не лучшее решение этой задачи. Мы разработали класс CodeInjector, который
  • лениво применяет предоставленные ему замены исходного кода: сохраняет все пришедшие ему запросы, а затем разом их выполняет в конце обработки файла;
  • никак не связан с clang-ом: сам открывает файл на чтение, сам ищет нужные строки и так далее.

Другое решение использовать уже готовый инструмент для рефакторинга кода (класс Rewriter) внутри clang-а. Почему же мы от него отказались? Проблема в том, что иногда нужно изменять один и тот же участок кода несколько раз: на одну и ту же позицию в коде может прийтись сразу несколько ошибкоопасных мест, что потребует нескольких замен.

Простой пример. Есть такое выражение:
arr[1] + arr[2];

Пусть мы сначала захотим поменять сложение на ASSERT_SUM, получим следующее:
ASSERT_SUM(arr[1], arr[2]);

Кроме того, мы хотим проверить, что не произойдет выход за границы массива. Снова обратимся к этому же участку кода:
ASSERT_SUM(ASSERT_IOB(arr, 1), ASSERT_IOB(arr, 2));

Наши Visitor-ы работают в полном неведении друг о друге, поэтому организовывать им сложную коммуникацию, чтобы они могли совершать замены, как-то договариваясь, категорически неудобно. Поскольку некоторые замены можно производить только последовательно и с постоянно разрастающимися операндами, то встроенный инструмент clang-а не сможет помочь он не поддерживает такой функционал.

Мы заметили, что схемы замен состоят из чередования кусков, которые мы оставляем, и кусков, которые мы заменяем. Проиллюстрируем на примере: выражение: arr[1+2]. В нем есть два ошибкоопасных места: обращение к массиву и операция сложения. Должны произойти следующие замены:
  • arr[1+2] заменяется на ASSERT_IOB(arr, 1 + 2)
  • ASSERT_IOB(arr, 1 + 2) заменяется на ASSERT_IOB(arr, ASSERT_SUM(1, 2))
    arr[1 + 2] ~~~~~> ASSERT_IOB(arr, 1 + 2) ~~~~~~> ASSERT_IOB(arr, ASSERT_SUM(1, 2)




Далее мы, внутри команды, договорились об интерфейсе для взаимодейстия с CodeInjector-ом, основанном на этой идее.

Все готово! Можно приступать к самому интересному работе с конкретными видами UB.

Функционал


Наша программа ориентирована на поиск и выявление UB, а также операций в коде, результат которых может быть не совсем очевидным для пользователя. К таким относятся, например, неявные преобразования во время вычисления арифметических выражений.

Основные ошибки, выявляемые нашей программой:
  • выход за границы С-шного массива;
  • UB в целочисленной арифметике;
  • обращение к неинициализированной переменной.

Подробно про некоторые аспекты функционала мы расскажем далее.

Целочисленная арифметика


Известнейшие представители UB арифметике целочисленные переполнения в самых разных операциях. Есть и более изощренные UB, которыми могут похвастаться, например, остаток от деления и особенно битовые сдвиги. Кроме того, часто можно встретить составные операторы присваивания (+= и другие) и операторы инкремента/декремента. Во время их использования происходит целая цепочка неявных преобразований типов, которая и может повлечь крайне неожиданный и нежеланный результат. Да и неявные касты сами по себе могут служить большой бедой.

Так или иначе, общее правило для всех UB в арифметике их сложно найти, легко проглядеть (никаких segfault-ов!) и невозможно простить.

Важно: впредь и далее речь пойдет про стандарт С++17, на который и ориентирован ub-tester. Он также поддерживает C++20, но из-за того, что в нем исчезли некоторые виды UB в арифметике (а новых не добавилось), про него говорить не так интересно.

int a = INT_MIN, b = -1, z = 0;int test1 = a + b; // overflowint test2 = a * b; // overflowint test3 = a << 5; // lhs is negative => UBint test5 = 5 % z; // % by 0 => UBint test6 = --a; // overflow

Мы много работали с целочисленными вычислениями, дробные пока остались в стороне. В первую очередь, из-за их отличия в представлении в железе, соответственно, и в методах обработки ошибок. Насколько мы знаем, самым удачным вариантом борьбы с UB в арифметике с плавающей точкой является проверка специальных флагов с помощью fenv.h, но дальше первого знакомства мы не зашли. В любом случае, и целочисленного случая хватило, чтобы один из членов нашей команды вполне увлекательно вычислился провел половину учебного года. Перейдем к сути.

Основным этапом являлось создание самой обертки. Весь ее интерфейс это макросы, которые разворачиваются в вызовы шаблонных функций или методов шаблонного класса. Такая функция или метод проверяет, корректно ли будет произведено вычисление, и в случае отсутствия проблем совершает его, возвращая ожидаемый результат. Самый простой пример проверка оператора сложения.

template <typename T>ArithmCheckRes checkSum(T Lhs, T Rhs) {  FLT_POINT_NOT_SUPPORTED(T);  if (Rhs > 0 && Lhs > std::numeric_limits<T>::max() - Rhs)    return ArithmCheckRes::OVERFLOW_MAX;  if (Rhs < 0 && Lhs < std::numeric_limits<T>::lowest() - Rhs)    return ArithmCheckRes::OVERFLOW_MIN;  return ArithmCheckRes::SAFE_OPERATION;}

В случае работы с префиксными операторами и операторами составного присваивания функции же возвращают нужное значение по ссылке в соответствии с документацией. В итоге, обертки над арифметическими операторами являются эквивалентами своих оригиналов с точностью не только до результата, но и до категории значения.

Как видно на примере оператора сложения, проверки по сути своей являются просто последовательностью условий, методично повторяющих формулировки документации языка. Во время ее изучения встретились и более интересные случаи UB.

Например
Например, неопределенное поведение во время взятия остатка INT_MIN % (-1), вызванное тем, что результат не помещается в int. Если же вы, как и я, всегда немного осторожничали с битовыми сдвигами то не зря. Битовый сдвиг влево может похвастаться рекордом по количеству различных UB, которые может вызывать. К примеру, сдвиг на отрицательное число бит это неопределенное поведение. Сдвиг на большее число бит, чем есть в типе тоже. А если попробовать битово сдвинуть влево отрицательное число, то до C++20, безусловно, снова UB. Но если вам повезло, и ваше число в знаковом типе неотрицательно, то корректность результата будет зависеть от возможности представить его в беззнаковой версии, что тоже можно не удержать в голове. В общем, хоть эти правила и понятные, но, в случае неаккуратного с ними обращения, последствия могут оказаться весьма плачевными.

int a = INT_MIN, b = -1;int test = a % b; // -INT_MIN > INT_MAX => UBint test1 = a << 5; // lhs is negative => UBint test2 = 5 << b; // rhs is negative => UBint32_t c = 1, d = 33;int32_t test3 = c << d; // rhs is >= size of c in bits => UBint test4 = 2e9 << c; // test = -294967296, not UBint test5 = 2e9 << (c + 1); // (2e9 << 2) > UINT_MAX => UB


Также было необходимо реализовать проверку целочисленного приведения типов для вообще осмысленности всей арифметики неявные конвертации могут тайком пробраться к вам в вычисления и без лишнего шума изменить результат. К проверке. Обнаружение самих неявных преобразований оставили на AST, оно прекрасно их все отображает. Что фактически хотелось: научиться проверять, представимо ли число в новом типе, к которому происходит конвертация. Конечно, достаточно просто сравнить число с его новыми границами. Но в каком типе производить это сравнение, чтобы все операнды принимали свои фактические значения? Потребовалось разобраться со знаковостью типов согласно документации, затем просто выбрать более широкий. Проверка приведения типов особенно мощно выстрелит далее.

Самыми интересными в работе оказались операторы составного присваивания и инкремента/декремента за счет неявной цепочки особенно неявных преобразований. Простой пример.

Вы с другом пошли в лес и решили прибавить к чару единицу Красиво прибавить, то есть ++c. Будет ли вас ожидать в следующем примере UB?

char c = 127;++c;

И тут понеслось. Во-первых, безобидный префиксный инкремент решил вычисляться наподобие своему старшему собрату, то есть как составное присваивание c += 1. Для этого он привел c и 1 к общему типу, в данном случае к int-у. Затем честно посчитал сумму. И перед записью результата обратно в c, не забыл вызвать неявное преобразование к char-у. Уф.

И UB не случилось. Действительно, само сложение произошло в большом типе int, где число 128 законно существует. Но при этом результат все равно получился с изюминкой, благодаря неявному приведению результата к char-у. Более того, такое приведение чисто теоретически могло закончится не -128, а чем-то особенным в зависимости от компилятора документация это допускает, ведь тип знаковый.

В итоге, из леса получилось выбраться живыми и вам, и другу, причем с большой вероятностью даже с ожидаемым результатом. Но путь оказался весьма неожиданным и интересным.

В случае с операторами составного присваивания AST не жалеет информации, поэтому проследить цепочку преобразований типов несложно. Далее несложно и проверить корректность вычисления в одном типе, затем неявное приведение к исходному (обе такие проверки уже есть в арсенале). Однако по поводу инкремента/декремента AST отказалось сотрудничать, скрыв всю кухню преобразований. Решением было внимательно прочитать документацию С++, а затем начать цепочку приведений с std::common_type от типа аргумента и int-a (типа единицы). Искушенный clang-ом читатель может поймать нас за руку, заметив, что именно по поводу таинственных ++ и AST, вообще говоря, знает, возможно переполнение или нет.

Однако мы здесь собрались не просто так

Читатель ловит нас за руку, в то время как AST делает нашу работу

Вопреки названию нашего проекта, ub-tester находит не только UB. Мотивацией арифметических проверок были не только ошибки, влекущие за собой обессмысливание всей программы, но и неприметные на первый взгляд источники неожиданных результатов (при этом корректных с точки зрения С++). Поэтому информации о возможности/невозможности UB в операторах инкремента/декремента мало, проверка должна уметь детектировать еще и сужающее неявное преобразование и уметь рассказывать про это пользователю. По этой причине в случае ++ и пришлось дополнительно поработать, чтобы добиться желаемого функционала.


Вывод ub-tester-а на примере с char c = 127; ++c;

Небольшой паноптикум арифметики закончился, дальше новые угрозы.

Неинициализированные переменные


Каждому, кто знаком с C++, известно (но зачастую это анти-интуитивно для новичков, пришедших из других языков): если объявить переменную базового типа, но не проинициализировать её никаким значением, а потом в неё посмотреть возникает UB. В то же время, такой синтаксис бывает удобен и иногда даже необходим, к примеру, для использования `cin` или `scanf`, поэтому мы решили побороться с этим типом ошибок.

Самое простое и бесполезное решение: просто добавить всем таким объявлениям переменных значение по умолчанию, но во-первых, так пользователю не станет проще найти ошибку, а во-вторых, в коде появляются магические числа, что считается плохой практикой. Другого тривиального решения C++ не предлагает, и потому в итоге мы остановились на следующем: мы заменим типы переменных на обертку, поддерживающую информацию о том, была внутренняя переменная проинициализирована или нет. Базовое описание такого класса выглядит примерно вот так:

template <typename T>class UBSafeType final {public:  UBSafeType() : Value_{}, IsInit_{false} {}  UBSafeType(T t) : Value_{t}, IsInit_{true} {}  T assertGetValue_() const;  T& assertGetRef();  T& setValue_(T Val);  private:  T Value_;  bool IsInit_;};

А затем возникают вопросы. Как мы понимаем, когда происходит именно чтение значения переменной, а когда она просто упоминается каким-либо другим образом? Суть в том, что согласно стандарту, UB возникает именно в момент lvalue to rvalue cast, а его Clang AST выделяет в отдельную вершину `ImplicitCastExpr` нужного типа. Хорошо, тогда более сложный вопрос: пусть есть `scanf`, читающий несколько переменных, но пользователь дал некорректный ввод и прочиталась только одна переменная. Тогда как понять, что происходит внутри наших оберток?

После некоторых размышлений волевым усилием отвечаем: никак. Если нет доступа к исходному коду функции, которая принимает переменную по ссылке или указателю, и при этом на момент вызова этой функции переменная не была проинициализирована, мы можем только сказать пользователю: Эй, у тебя тут потенциально опасная ситуация, но мы за это не отвечаем, а затем про эту переменную забыть.

Чтобы понять, доступны ли нам исходники этой функции, мы заранее пройдемся по AST всех переданных файлов и поищем в них определения вызываемых функций. Если исходники есть они будут обработаны нашим анализатором, поэтому такой функции можно смело передавать обертку целиком и, таким образом, сохранить необходимую информацию.


Пример: обработанная версия программы, читающая IP-адрес по шаблону при помощи scanf, не может обнаружить UB, возникающее при выводе результата на экран

Перейдем к тому, как мы использовали AST. По вершинам `VarDecl` мы находим объявления переменных и заменяем их на конструкторы оберток, по операторам присваивания находим собственно присваивания, которые и являются инициализациями. Интереснее с доступом к значению: информацию о переменной содержат вершины `DeclRefExpr`, но мы-то ищем `ImplicitCastExpr`, находящийся выше в дереве. При этом принципиально важно обработать случаи доступа по значению (с кастом) и по ссылке (без него) по-разному. На помощь приходит технология Parent-ов, позволяющая подниматься по дереву независимо от обхода Visitor-ов. (Передаю привет неполной документации ParentMapContext, из-за которой пришлось долго гулять по исходникам clang-а). Увы, это палка о двух концах из-за потенциального обхода целого дерева асимптотика нашего анализатора возрастает до квадратной, и время работы самого анализатора ухудшается значительно. А что же делать, если обращение к переменной по ссылке? Возможно, эта ссылка куда-то передается, возможно, сохраняется в новую переменную И что делать с полями классов? А вложенных классов?

int main() {  int a = 5;  int& b{a};  int c = b++ + (int&)a;  (b = c) = (b);}


Пример: страшный сон исследователей неинициализированных переменных и его AST

Не будем утомлять читателя миллионом вопросов, которые мы сами себе задавали и вынуждены были искать ответы. Скажем только, что ключевой сложностью оказалось отсутствие какого бы то ни было источника информации, в котором были бы собраны все пререквизиты и ценные мысли, объясняющие, каким вообще образом стоит реализовывать данную идею. Как следствие, пришлось наступать на все старательно разложенные языком C++ и clang-ом грабли, и по мере осмысления всё большего количества тонкостей и деталей задачи, глобальное представление об этой части проекта сильно видоизменялось и обрастало подробностями и замысловатостями. На продумывание и размышления ушло много больше времени, чем на саму реализацию.

Результаты


Полномасштабное тестирование мы, к сожалению, не успели провести. Тем не менее, мы проверяли, как наша программа работает, на 1) отдельных изощренных ситуациях, 2) примерах простеньких программ на C++ из интернета, 3) своих решениях контестов и домашек. Вот пример преобразования, получающегося в результате работы нашего анализатора:

Пример до:
#include "../../include/UBTester.h"#include <iostream>int main() {  int a, b;  std::cin >> a;    char c = 89;  c = 2 * c; // unsafe conversion    int arr[10];  arr[a] = a; // possible index out of bounds    a = b; // uninit variable}

Пример после:
#include "../../include/UBTester.h"#include <iostream>int main() {  UBSafeType<int> a, b;  std::cin >> ASSERT_GET_REF_IGNORE(a);    UBSafeType<char> c(IMPLICIT_CAST(89, int, char));  ASSERT_SET_VALUE(c, IMPLICIT_CAST(ASSERT_BINOP(Mul, 2, IMPLICIT_CAST(c, char, int), int, int), int, char)); // unsafe conversion    UBSafeCArray<int, 10> arr(ASSERT_INVALID_SIZE(std::vector<int>({10})));  ASSERT_IOB(arr, ASSERT_GET_VALUE(a)) = ASSERT_GET_VALUE(a); // possible index out of bounds    ASSERT_SET_VALUE(a, ASSERT_GET_VALUE(b)); // uninit variable}

Видно, что код достаточно сильно раздувается в объеме. Впрочем, мы считаем, что сохранить читабельность на каком-то уровне нам удалось, а главное, ошибки действительно находятся!


Пример: вот так работает код, приведенный выше, на входных данных 5 и 15

Ради интереса мы также проверили, насколько медленнее работают более-менее реальные программы после обработки нашим анализатором. В качестве примера мы взяли собственную реализацию алгоритма Флойда. Результаты оказались неутешительными:


В заключение скажем: перед нами не стояло цели создать готовый продукт нам было важно столкнуться с LLVM, clang и С++ и разбиться насмерть. Возможно, результат вышел не самым практичным, но чтение документаций и стандарта оказалось занимательным и особенно полезным времяпрепровождением. Будьте осторожны при использовании С++ и да хранит вас Бьёрн Страуструп.

Ссылка на гитхаб
Подробнее..

Как определить размер переменных во время выполнения Go-программы

27.06.2020 22:14:45 | Автор: admin
Аннотация: в заметке рассматривается один из способов анализа потребления памяти компонентами Go-приложения.
Зачастую в памяти программы хранятся структуры данных, которые изменяют свой размер динамически, по ходу работы программы. Примером такой структуры может быть кэш данных или журнал работы программы или данные, получаемые от внешних систем. При этом может возникнуть ситуация, когда потребление памяти растёт, возможностей оборудования не хватает, а конкретный механизм утечки не ясен.
Основным способом профилирования Go-приложений является подключение инструмента pprof из пакета net/http/pprof. В результате можно получить таблицу или граф с распределением памяти в работающей программе. Но использование этого инструмента требует очень больших накладных расходов и может быть неприменимо, особенно если вы не можете запустить несколько экземпляров программы с реальными данными.
В таком случае возникает желание измерить потребление памяти объектами программы по запросу, чтобы, например, отобразить статистику системы или передать метрики в систему мониторинга. Однако средствами языка это в общем случае невозможно. В Go нет инструментов для определения размера переменных во время работы программы.
Поэтому я решил написать небольшой пакет, который предоставляет такую возможность. Основным инструментом является рефлексия (пакет reflection). Всех интересующихся вопросом такого профилирования приложения приглашаю к дальнейшему чтению.


Сначала нужно сказать пару слов по поводу встроенных функций
unsafe.Sizeof(value)
и
reflect.TypeOf(value).Size()

Эти функции эквивалентны и зачастую в Интернете именно их рекомендуют для определения размера переменных. Но эти функции возвращают не размер фактической переменной, а размер в байтах для контейнера переменной (грубо размер указателя). К примеру, для переменной типа int64 эти функции вернут корректный результат, поскольку переменная данного типа содержит фактическое значение, а не ссылку на него. Но для типов данных, содержащих в себе указатель на фактическое значение, вроде слайса или строки, эти функции вернут одинаковое для всех переменных данного типа значение. Это значение соответствует размеру контейнера, содержащего ссылку на данные переменной. Проиллюстрирую примером:
func main() {s1 := "ABC"s2 := "ABCDEF"arr1 := []int{1, 2}arr2 := []int{1, 2, 3, 4, 5, 6}fmt.Printf("Var: %s, Size: %v\n", s1, unsafe.Sizeof(s1))fmt.Printf("Var: %s, Size: %v\n", s2, unsafe.Sizeof(s2))fmt.Printf("Var: %v, Size: %v\n", arr1, reflect.TypeOf(arr1).Size())fmt.Printf("Var: %v, Size: %v\n", arr2, reflect.TypeOf(arr2).Size())}

В результате получим:
Var: ABC, Size: 16Var: ABCDEF, Size: 16Var: [1 2], Size: 24Var: [1 2 3 4 5 6], Size: 24

Как видите, фактический размер переменной не вычисляется.
В стандартной библиотеке есть функция binary.Size() которая возвращает размер переменной в байтах, но только для типов фиксированного размера. То есть если в полях вашей структуры встретится строка, слайс, ассоциативный массив или просто int, то функция не применима. Однако именно эту функция я взял за основу пакета size, в котором попытался расширить возможности приведённого выше механизма на типы данных без фиксированного размера.
Для определения размера объекта во время работы программы необходимо понять его тип, вместе с типами всех вложенных объектов, если это структура. Итоговая структура, которую необходимо анализировать, в общем случае представляется в виде дерева. Поэтому для определения размера сложных типов данных нужно использовать рекурсию.
Таким образом вычисление объёма потребляемой памяти для произвольного объекта представляется следующим образом:
  • алгоритм определение размера переменной простого (не составного) типа;
  • рекурсивный вызов алгоритма для элементов массивов, полей структур, ключей и значений ассоциативных массивов;
  • определение бесконечных циклов;

Чтобы определить фактический размер переменной простого типа (не массива или структуры), можно использовать приведённую выше функцию Size() из пакета reflection. Эта функция корректно работает для переменных, содержащих фактическое значение. Для переменных, являющихся массивами, строками, т.е. содержащих ссылки на значение нужно пройтись по элементам или полям и вычислить значение каждого элемента.
Для анализа типа и значения переменной пакет reflection упаковывает переменную в пустой интерфейс (interface{}). В Go пустой интерфейс может содержать любой объект. Кроме того, интерфейс в Go представлен контейнером, содержащим два поля: тип фактического значения и ссылку на фактическое значение.
Именно отображение анализируемого значения в пустой интерфейс и обратно послужило основанием для названия самого приёма reflection.
Для лучшего понимания работы рефлексии в Go рекомендую статью Роба Пайка в официальном блоге Go. Перевод этой статьи был на Хабре.
В конечном итоге был разработан пакет size, который можно использовать в своих программах следующим образом:
package mainimport ("fmt""github.com/DmitriyVTitov/size")func main() {a := struct {a intb stringc boold int32e []bytef [3]int64}{a: 10,                    // 8 bytesb: "Text",                // 4 bytesc: true,                  // 1 byted: 25,                    // 4 bytese: []byte{'c', 'd', 'e'}, // 3 bytesf: [3]int64{1, 2, 3},     // 24 bytes}fmt.Println(size.Of(a))}// Output: 44

Замечания:
  • На практике вычисление размера структур объёма около 10 ГБайт с большой вложенностью занимает 10-20 минут. Это результат того, что рефлексия довольно дорогая операция, требующая упаковки каждой переменной в пустой интерфейс и последующий анализ (см. статью по ссылке выше).
  • В результате сравнительно невысокой скорости, пакет следует использовать для примерного определения размера переменных, поскольку в реальной системе за время анализа большой структуры фактические данные наверняка успеют измениться. Либо обеспечивайте исключительный доступ к данным на время расчёта с помощью мьютекса, если это допустимо.
  • Программа не учитывает размер контейнеров для массивов, интефейсов и ассоциативных массивов (это 24 байта для массива и слайса, 8 байт для map и interface). Поэтому, если у вас большое количество таких элементов небольшого размера, то потери будут существенными.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru