2021-12-07 22:47:41 +01:00
|
|
|
#include "content/views/view_yara.hpp"
|
2021-02-26 13:35:19 +01:00
|
|
|
|
2022-02-01 18:09:40 +01:00
|
|
|
#include <hex/api/content_registry.hpp>
|
|
|
|
|
2021-08-29 22:15:18 +02:00
|
|
|
#include <hex/helpers/utils.hpp>
|
2021-09-08 15:18:24 +02:00
|
|
|
#include <hex/helpers/file.hpp>
|
2022-03-04 11:36:37 +01:00
|
|
|
#include <hex/helpers/fs.hpp>
|
2021-02-26 13:35:19 +01:00
|
|
|
|
|
|
|
#include <yara.h>
|
|
|
|
#include <filesystem>
|
|
|
|
#include <thread>
|
|
|
|
|
2021-12-07 22:47:41 +01:00
|
|
|
namespace hex::plugin::builtin {
|
2021-02-26 13:35:19 +01:00
|
|
|
|
2021-12-07 22:47:41 +01:00
|
|
|
ViewYara::ViewYara() : View("hex.builtin.view.yara.name") {
|
2021-02-26 13:35:19 +01:00
|
|
|
yr_initialize();
|
|
|
|
|
|
|
|
this->reloadRules();
|
2022-01-13 14:34:19 +01:00
|
|
|
|
|
|
|
ContentRegistry::FileHandler::add({ ".yar" }, [](const auto &path) {
|
2022-03-04 11:36:37 +01:00
|
|
|
for (const auto &destPath : fs::getDefaultPaths(fs::ImHexPath::Yara)) {
|
|
|
|
if (fs::copyFile(path, destPath / path.filename(), std::fs::copy_options::overwrite_existing)) {
|
2022-01-13 14:34:19 +01:00
|
|
|
View::showMessagePopup("hex.builtin.view.yara.rule_added"_lang);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
});
|
2021-02-26 13:35:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
ViewYara::~ViewYara() {
|
|
|
|
yr_finalize();
|
|
|
|
}
|
|
|
|
|
|
|
|
void ViewYara::drawContent() {
|
2021-12-07 22:47:41 +01:00
|
|
|
if (ImGui::Begin(View::toWindowName("hex.builtin.view.yara.name").c_str(), &this->getWindowOpenState(), ImGuiWindowFlags_NoCollapse)) {
|
2021-02-26 13:35:19 +01:00
|
|
|
|
2021-12-07 22:47:41 +01:00
|
|
|
ImGui::TextUnformatted("hex.builtin.view.yara.header.rules"_lang);
|
2021-02-26 13:35:19 +01:00
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
if (this->m_rules.empty()) {
|
2022-01-15 14:14:53 +01:00
|
|
|
ImGui::TextFormattedColored(ImVec4(0.92F, 0.25F, 0.2F, 1.0F), "{}", "hex.builtin.view.yara.no_rules"_lang);
|
2021-02-26 13:35:19 +01:00
|
|
|
|
2021-12-07 22:47:41 +01:00
|
|
|
if (ImGui::Button("hex.builtin.view.yara.reload"_lang)) this->reloadRules();
|
2021-02-26 13:35:19 +01:00
|
|
|
} else {
|
2022-05-27 20:42:07 +02:00
|
|
|
ImGui::BeginDisabled(this->m_matching);
|
|
|
|
{
|
2021-12-07 22:47:41 +01:00
|
|
|
if (ImGui::BeginCombo("hex.builtin.view.yara.header.rules"_lang, this->m_rules[this->m_selectedRule].first.c_str())) {
|
2021-02-26 13:35:19 +01:00
|
|
|
for (u32 i = 0; i < this->m_rules.size(); i++) {
|
|
|
|
const bool selected = (this->m_selectedRule == i);
|
2021-09-23 22:57:19 +02:00
|
|
|
if (ImGui::Selectable(this->m_rules[i].first.c_str(), selected))
|
2021-02-26 13:35:19 +01:00
|
|
|
this->m_selectedRule = i;
|
|
|
|
|
|
|
|
if (selected)
|
|
|
|
ImGui::SetItemDefaultFocus();
|
|
|
|
}
|
|
|
|
ImGui::EndCombo();
|
|
|
|
}
|
|
|
|
ImGui::SameLine();
|
2021-12-07 22:47:41 +01:00
|
|
|
if (ImGui::Button("hex.builtin.view.yara.reload"_lang)) this->reloadRules();
|
|
|
|
if (ImGui::Button("hex.builtin.view.yara.match"_lang)) this->applyRules();
|
2022-05-27 20:42:07 +02:00
|
|
|
}
|
|
|
|
ImGui::EndDisabled();
|
2021-02-26 13:35:19 +01:00
|
|
|
|
|
|
|
if (this->m_matching) {
|
|
|
|
ImGui::SameLine();
|
2021-12-07 22:47:41 +01:00
|
|
|
ImGui::TextSpinner("hex.builtin.view.yara.matching"_lang);
|
2021-02-26 13:35:19 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ImGui::NewLine();
|
2021-12-07 22:47:41 +01:00
|
|
|
ImGui::TextUnformatted("hex.builtin.view.yara.header.matches"_lang);
|
2021-02-26 13:35:19 +01:00
|
|
|
ImGui::Separator();
|
|
|
|
|
2022-03-15 23:48:49 +01:00
|
|
|
auto matchesTableSize = ImGui::GetContentRegionAvail();
|
|
|
|
matchesTableSize.y *= 3.75 / 5.0;
|
|
|
|
matchesTableSize.y -= ImGui::GetTextLineHeightWithSpacing();
|
|
|
|
|
|
|
|
if (ImGui::BeginTable("matches", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_Sortable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY, matchesTableSize)) {
|
2021-02-26 13:35:19 +01:00
|
|
|
ImGui::TableSetupScrollFreeze(0, 1);
|
2021-12-07 22:47:41 +01:00
|
|
|
ImGui::TableSetupColumn("hex.builtin.view.yara.matches.identifier"_lang);
|
|
|
|
ImGui::TableSetupColumn("hex.builtin.view.yara.matches.variable"_lang);
|
2022-02-02 00:36:09 +01:00
|
|
|
ImGui::TableSetupColumn("hex.builtin.common.address"_lang);
|
|
|
|
ImGui::TableSetupColumn("hex.builtin.common.size"_lang);
|
2021-02-26 13:35:19 +01:00
|
|
|
|
|
|
|
ImGui::TableHeadersRow();
|
|
|
|
|
2022-03-15 23:48:49 +01:00
|
|
|
if (!this->m_matching) {
|
|
|
|
ImGuiListClipper clipper;
|
|
|
|
clipper.Begin(this->m_matches.size());
|
2021-02-26 13:35:19 +01:00
|
|
|
|
2022-03-15 23:48:49 +01:00
|
|
|
while (clipper.Step()) {
|
2022-03-27 00:01:28 +01:00
|
|
|
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) {
|
2022-05-27 20:42:07 +02:00
|
|
|
auto &[identifier, variableName, address, size, wholeDataMatch, highlightId, tooltipId] = this->m_matches[i];
|
2022-03-15 23:48:49 +01:00
|
|
|
ImGui::TableNextRow();
|
2021-02-26 13:35:19 +01:00
|
|
|
ImGui::TableNextColumn();
|
2022-03-15 23:48:49 +01:00
|
|
|
ImGui::PushID(i);
|
|
|
|
if (ImGui::Selectable("match", false, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowItemOverlap)) {
|
|
|
|
ImHexApi::HexEditor::setSelection(address, size);
|
|
|
|
}
|
|
|
|
ImGui::PopID();
|
|
|
|
ImGui::SameLine();
|
|
|
|
ImGui::TextUnformatted(identifier.c_str());
|
2021-02-26 13:35:19 +01:00
|
|
|
ImGui::TableNextColumn();
|
2022-03-15 23:48:49 +01:00
|
|
|
ImGui::TextUnformatted(variableName.c_str());
|
|
|
|
|
|
|
|
if (!wholeDataMatch) {
|
|
|
|
ImGui::TableNextColumn();
|
|
|
|
ImGui::TextFormatted("0x{0:X} : 0x{1:X}", address, address + size - 1);
|
|
|
|
ImGui::TableNextColumn();
|
|
|
|
ImGui::TextFormatted("0x{0:X}", size);
|
|
|
|
} else {
|
|
|
|
ImGui::TableNextColumn();
|
|
|
|
ImGui::TextFormattedColored(ImVec4(0.92F, 0.25F, 0.2F, 1.0F), "{}", "hex.builtin.view.yara.whole_data"_lang);
|
|
|
|
ImGui::TableNextColumn();
|
|
|
|
ImGui::TextUnformatted("");
|
|
|
|
}
|
2021-02-26 13:35:19 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-15 23:48:49 +01:00
|
|
|
clipper.End();
|
|
|
|
}
|
2021-02-26 13:35:19 +01:00
|
|
|
|
|
|
|
ImGui::EndTable();
|
|
|
|
}
|
2022-03-15 23:48:49 +01:00
|
|
|
|
|
|
|
auto consoleSize = ImGui::GetContentRegionAvail();
|
|
|
|
|
|
|
|
if (ImGui::BeginChild("##console", consoleSize, true, ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_HorizontalScrollbar)) {
|
|
|
|
ImGuiListClipper clipper(this->m_consoleMessages.size());
|
|
|
|
while (clipper.Step())
|
2022-03-27 00:01:28 +01:00
|
|
|
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) {
|
2022-03-15 23:48:49 +01:00
|
|
|
const auto &message = this->m_consoleMessages[i];
|
|
|
|
|
|
|
|
if (ImGui::Selectable(message.c_str()))
|
|
|
|
ImGui::SetClipboardText(message.c_str());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ImGui::EndChild();
|
2021-02-26 13:35:19 +01:00
|
|
|
}
|
|
|
|
ImGui::End();
|
|
|
|
}
|
|
|
|
|
2022-03-15 23:48:49 +01:00
|
|
|
void ViewYara::clearResult() {
|
2022-05-27 20:42:07 +02:00
|
|
|
for (const auto &match : this->m_matches) {
|
|
|
|
ImHexApi::HexEditor::removeBackgroundHighlight(match.highlightId);
|
|
|
|
ImHexApi::HexEditor::removeTooltip(match.tooltipId);
|
|
|
|
}
|
2022-03-15 23:48:49 +01:00
|
|
|
|
|
|
|
this->m_matches.clear();
|
|
|
|
this->m_consoleMessages.clear();
|
|
|
|
}
|
|
|
|
|
2021-02-26 13:35:19 +01:00
|
|
|
void ViewYara::reloadRules() {
|
|
|
|
this->m_rules.clear();
|
|
|
|
|
2022-03-04 20:52:39 +01:00
|
|
|
for (const auto &path : fs::getDefaultPaths(fs::ImHexPath::Yara)) {
|
2021-09-23 22:57:19 +02:00
|
|
|
if (!fs::exists(path))
|
|
|
|
continue;
|
2021-02-26 13:35:19 +01:00
|
|
|
|
2022-03-04 11:36:37 +01:00
|
|
|
std::error_code error;
|
|
|
|
for (const auto &entry : std::fs::recursive_directory_iterator(path, error)) {
|
2021-09-23 22:57:19 +02:00
|
|
|
if (entry.is_regular_file() && entry.path().extension() == ".yar") {
|
2022-03-04 20:52:39 +01:00
|
|
|
this->m_rules.emplace_back(std::fs::relative(entry.path(), std::fs::path(path)).string(), entry.path().string());
|
2021-09-23 22:57:19 +02:00
|
|
|
}
|
|
|
|
}
|
2021-02-26 13:35:19 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ViewYara::applyRules() {
|
2022-03-15 23:48:49 +01:00
|
|
|
this->clearResult();
|
2022-02-02 21:08:46 +01:00
|
|
|
|
2021-02-26 13:35:19 +01:00
|
|
|
this->m_matching = true;
|
|
|
|
|
|
|
|
std::thread([this] {
|
2021-12-16 23:48:52 +01:00
|
|
|
if (!ImHexApi::Provider::isValid()) return;
|
|
|
|
|
|
|
|
auto provider = ImHexApi::Provider::get();
|
2022-02-01 22:09:44 +01:00
|
|
|
auto task = ImHexApi::Tasks::createTask("hex.builtin.view.yara.matching", provider->getActualSize());
|
2021-02-26 13:35:19 +01:00
|
|
|
|
|
|
|
YR_COMPILER *compiler = nullptr;
|
|
|
|
yr_compiler_create(&compiler);
|
2021-09-23 22:57:19 +02:00
|
|
|
ON_SCOPE_EXIT {
|
|
|
|
yr_compiler_destroy(compiler);
|
|
|
|
this->m_matching = false;
|
|
|
|
};
|
|
|
|
|
|
|
|
yr_compiler_set_include_callback(
|
2022-01-24 20:53:17 +01:00
|
|
|
compiler,
|
2022-03-27 00:01:28 +01:00
|
|
|
[](const char *includeName, const char *, const char *, void *userData) -> const char * {
|
2022-01-24 20:53:17 +01:00
|
|
|
auto currFilePath = static_cast<const char *>(userData);
|
2021-09-23 22:57:19 +02:00
|
|
|
|
2022-03-04 11:36:37 +01:00
|
|
|
fs::File file((std::fs::path(currFilePath).parent_path() / includeName).string(), fs::File::Mode::Read);
|
2022-01-24 20:53:17 +01:00
|
|
|
if (!file.isValid())
|
|
|
|
return nullptr;
|
2021-02-26 13:35:19 +01:00
|
|
|
|
2022-02-01 22:09:44 +01:00
|
|
|
auto size = file.getSize();
|
2022-01-24 20:53:17 +01:00
|
|
|
char *buffer = new char[size + 1];
|
|
|
|
file.readBuffer(reinterpret_cast<u8 *>(buffer), size);
|
|
|
|
buffer[size] = 0x00;
|
2021-09-23 22:57:19 +02:00
|
|
|
|
2022-01-24 20:53:17 +01:00
|
|
|
return buffer;
|
|
|
|
},
|
|
|
|
[](const char *ptr, void *userData) {
|
2022-03-27 00:01:28 +01:00
|
|
|
hex::unused(userData);
|
|
|
|
|
2022-01-24 20:53:17 +01:00
|
|
|
delete[] ptr;
|
|
|
|
},
|
|
|
|
this->m_rules[this->m_selectedRule].second.data());
|
2021-09-23 22:57:19 +02:00
|
|
|
|
|
|
|
|
2022-03-04 11:36:37 +01:00
|
|
|
fs::File file(this->m_rules[this->m_selectedRule].second, fs::File::Mode::Read);
|
2021-09-08 15:18:24 +02:00
|
|
|
if (!file.isValid()) return;
|
2021-02-26 13:35:19 +01:00
|
|
|
|
2021-09-08 15:18:24 +02:00
|
|
|
if (yr_compiler_add_file(compiler, file.getHandle(), nullptr, nullptr) != 0) {
|
2022-03-15 23:48:49 +01:00
|
|
|
std::string errorMessage(0xFFFF, '\x00');
|
|
|
|
yr_compiler_get_error_message(compiler, errorMessage.data(), errorMessage.size());
|
|
|
|
hex::trim(errorMessage);
|
|
|
|
|
|
|
|
ImHexApi::Tasks::doLater([this, errorMessage] {
|
|
|
|
this->clearResult();
|
|
|
|
|
|
|
|
this->m_consoleMessages.push_back("Error: " + errorMessage);
|
|
|
|
});
|
|
|
|
|
2021-02-26 13:35:19 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
YR_RULES *rules;
|
|
|
|
yr_compiler_get_rules(compiler, &rules);
|
2021-09-23 22:57:19 +02:00
|
|
|
ON_SCOPE_EXIT { yr_rules_destroy(rules); };
|
2021-02-26 13:35:19 +01:00
|
|
|
|
|
|
|
YR_MEMORY_BLOCK_ITERATOR iterator;
|
|
|
|
|
|
|
|
struct ScanContext {
|
2022-03-04 20:52:39 +01:00
|
|
|
Task *task = nullptr;
|
2021-02-26 13:35:19 +01:00
|
|
|
std::vector<u8> buffer;
|
2022-03-04 20:52:39 +01:00
|
|
|
YR_MEMORY_BLOCK currBlock = {};
|
2021-02-26 13:35:19 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
ScanContext context;
|
2022-02-01 22:09:44 +01:00
|
|
|
context.task = &task;
|
|
|
|
context.currBlock.base = 0;
|
2022-01-24 20:53:17 +01:00
|
|
|
context.currBlock.fetch_data = [](auto *block) -> const u8 * {
|
|
|
|
auto &context = *static_cast<ScanContext *>(block->context);
|
2021-02-26 13:35:19 +01:00
|
|
|
|
2021-09-21 02:29:54 +02:00
|
|
|
auto provider = ImHexApi::Provider::get();
|
2021-02-26 13:35:19 +01:00
|
|
|
|
2021-12-15 22:52:35 +01:00
|
|
|
context.buffer.resize(context.currBlock.size);
|
2021-02-26 13:35:19 +01:00
|
|
|
|
|
|
|
if (context.buffer.empty()) return nullptr;
|
|
|
|
|
2021-12-15 22:52:35 +01:00
|
|
|
block->size = context.currBlock.size;
|
|
|
|
|
2021-12-15 21:13:45 +01:00
|
|
|
provider->read(context.currBlock.base + provider->getBaseAddress(), context.buffer.data(), context.buffer.size());
|
2021-02-26 13:35:19 +01:00
|
|
|
|
|
|
|
return context.buffer.data();
|
|
|
|
};
|
|
|
|
iterator.file_size = [](auto *iterator) -> u64 {
|
2022-03-27 00:01:28 +01:00
|
|
|
hex::unused(iterator);
|
|
|
|
|
2021-12-15 22:52:35 +01:00
|
|
|
return ImHexApi::Provider::get()->getActualSize();
|
2021-02-26 13:35:19 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
iterator.context = &context;
|
2022-02-01 22:09:44 +01:00
|
|
|
iterator.first = [](YR_MEMORY_BLOCK_ITERATOR *iterator) -> YR_MEMORY_BLOCK *{
|
2022-01-24 20:53:17 +01:00
|
|
|
auto &context = *static_cast<ScanContext *>(iterator->context);
|
2021-02-26 13:35:19 +01:00
|
|
|
|
|
|
|
context.currBlock.base = 0;
|
|
|
|
context.currBlock.size = 0;
|
|
|
|
context.buffer.clear();
|
|
|
|
iterator->last_error = ERROR_SUCCESS;
|
|
|
|
|
|
|
|
return iterator->next(iterator);
|
|
|
|
};
|
2022-01-24 20:53:17 +01:00
|
|
|
iterator.next = [](YR_MEMORY_BLOCK_ITERATOR *iterator) -> YR_MEMORY_BLOCK * {
|
|
|
|
auto &context = *static_cast<ScanContext *>(iterator->context);
|
2021-02-26 13:35:19 +01:00
|
|
|
|
|
|
|
u64 address = context.currBlock.base + context.currBlock.size;
|
|
|
|
|
2022-02-01 22:09:44 +01:00
|
|
|
iterator->last_error = ERROR_SUCCESS;
|
|
|
|
context.currBlock.base = address;
|
|
|
|
context.currBlock.size = ImHexApi::Provider::get()->getActualSize() - address;
|
2021-02-26 13:35:19 +01:00
|
|
|
context.currBlock.context = &context;
|
2021-12-16 23:48:52 +01:00
|
|
|
context.task->update(address);
|
2021-02-26 13:35:19 +01:00
|
|
|
|
|
|
|
if (context.currBlock.size == 0) return nullptr;
|
|
|
|
|
|
|
|
return &context.currBlock;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2022-03-15 23:48:49 +01:00
|
|
|
struct ResultContext {
|
|
|
|
std::vector<YaraMatch> newMatches;
|
|
|
|
std::vector<std::string> consoleMessages;
|
|
|
|
};
|
2021-02-26 13:35:19 +01:00
|
|
|
|
2022-03-15 23:48:49 +01:00
|
|
|
ResultContext resultContext;
|
2021-02-26 13:35:19 +01:00
|
|
|
|
2022-03-15 23:48:49 +01:00
|
|
|
yr_rules_scan_mem_blocks(
|
|
|
|
rules, &iterator, 0, [](YR_SCAN_CONTEXT *context, int message, void *data, void *userData) -> int {
|
|
|
|
auto &results = *static_cast<ResultContext *>(userData);
|
|
|
|
|
|
|
|
switch (message) {
|
|
|
|
case CALLBACK_MSG_RULE_MATCHING:
|
|
|
|
{
|
|
|
|
auto rule = static_cast<YR_RULE *>(data);
|
|
|
|
|
|
|
|
YR_STRING *string;
|
|
|
|
YR_MATCH *match;
|
|
|
|
|
|
|
|
if (rule->strings != nullptr) {
|
|
|
|
yr_rule_strings_foreach(rule, string) {
|
|
|
|
yr_string_matches_foreach(context, string, match) {
|
2022-05-27 20:42:07 +02:00
|
|
|
results.newMatches.push_back({ rule->identifier, string->identifier, u64(match->offset), size_t(match->match_length), false, 0, 0 });
|
2022-03-15 23:48:49 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2022-05-27 20:42:07 +02:00
|
|
|
results.newMatches.push_back({ rule->identifier, "", 0, 0, true, 0, 0 });
|
2022-01-24 20:53:17 +01:00
|
|
|
}
|
2021-02-26 13:35:19 +01:00
|
|
|
}
|
2022-03-15 23:48:49 +01:00
|
|
|
break;
|
|
|
|
case CALLBACK_MSG_CONSOLE_LOG:
|
|
|
|
{
|
|
|
|
results.consoleMessages.emplace_back(static_cast<const char *>(data));
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
2021-02-26 13:35:19 +01:00
|
|
|
}
|
|
|
|
|
2022-01-24 20:53:17 +01:00
|
|
|
return CALLBACK_CONTINUE;
|
|
|
|
},
|
2022-03-15 23:48:49 +01:00
|
|
|
&resultContext,
|
2022-01-24 20:53:17 +01:00
|
|
|
0);
|
2021-02-26 13:35:19 +01:00
|
|
|
|
2022-02-02 21:08:46 +01:00
|
|
|
|
2022-03-15 23:48:49 +01:00
|
|
|
ImHexApi::Tasks::doLater([this, resultContext] {
|
|
|
|
this->m_matches = resultContext.newMatches;
|
|
|
|
this->m_consoleMessages = resultContext.consoleMessages;
|
|
|
|
|
2022-05-27 20:42:07 +02:00
|
|
|
constexpr static color_t YaraColor = 0x70B4771F;
|
2022-03-15 23:48:49 +01:00
|
|
|
for (auto &match : this->m_matches) {
|
2022-05-27 20:42:07 +02:00
|
|
|
match.highlightId = ImHexApi::HexEditor::addBackgroundHighlight({ match.address, match.size }, YaraColor);
|
|
|
|
match.tooltipId = ImHexApi::HexEditor::addTooltip({ match. address, match.size }, hex::format("{0} [{1}]", match.identifier, match.variable), YaraColor);
|
2022-03-15 23:48:49 +01:00
|
|
|
}
|
|
|
|
});
|
2021-02-26 13:35:19 +01:00
|
|
|
}).detach();
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|