Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion inc/spbc-backups.php
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ function spbc_backup__files_with_signatures_handler()

$output = array('success' => true);

$files_to_backup = $wpdb->get_results('SELECT path, weak_spots, checked_heuristic, checked_signatures, status, severity FROM ' . SPBC_TBL_SCAN_FILES . ' WHERE weak_spots LIKE "%\"SIGNATURES\":%";', ARRAY_A);
$files_to_backup = $wpdb->get_results('SELECT path, weak_spots, checked_heuristic, checked_signatures, status, severity FROM ' . SPBC_TBL_SCAN_FILES . ' WHERE weak_spots LIKE "%\"SIGNATURES\":%" AND source != \'BINARY\';', ARRAY_A);

if (!is_array($files_to_backup) || !count($files_to_backup)) {
$output = array('success' => true);
Expand Down
21 changes: 21 additions & 0 deletions inc/spbc-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,12 @@ function spbc_settings__register()
'description' => __('Will search for known malicious signatures in files. Unknown files will be shown in the results only if both options heuristic analysis and signature analysis are enabled.', 'security-malware-firewall'),
'long_description' => true,
),
'scanner__binary_analysis' => array(
'type' => 'field',
'title' => __('Binary analysis', 'security-malware-firewall'),
'description' => __('Search for suspicious binary patterns in files.', 'security-malware-firewall'),
'long_description' => false,
),
'scanner__os_cron_analysis' => array(
'type' => 'field',
'title' => Scanner\OSCron\View\OSCronLocale::getInstance()->settings__option_title,
Expand Down Expand Up @@ -2383,6 +2389,15 @@ function spbc_field_scanner__prepare_data__files(&$table)
unset($row->actions['delete']);
}

// Binary files: restrict modification/analysis actions, allow only approve/delete/quarantine
if ( isset($row->source) && $row->source === 'BINARY' ) {
unset($row->actions['send']);
unset($row->actions['view']);
unset($row->actions['view_bad']);
unset($row->actions['replace']);
unset($row->actions['compare']);
}

$table->items[] = array(
'cb' => $row->fast_hash,
'uid' => $row->fast_hash,
Expand Down Expand Up @@ -3014,6 +3029,12 @@ function spbc_field_scanner()
. '</span> -> ';
}

if ($spbc->settings['scanner__binary_analysis']) {
echo '<span class="spbc_overall_scan_status_binary_analysis">'
. __('Binary analysis', 'security-malware-firewall')
. '</span> -> ';
}

if ($spbc->settings['scanner__schedule_send_heuristic_suspicious_files']) {
echo '<span class="spbc_overall_scan_status_schedule_send_heuristic_suspicious_files">'
. __('Schedule suspicious files sending', 'security-malware-firewall')
Expand Down
2 changes: 1 addition & 1 deletion js/spbc-scanner-plugin.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/spbc-scanner-plugin.min.js.map

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions js/src/spbc-scanner-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class SpbcMalwareScanner {/* eslint-disable-line no-unused-vars */
'auto_cure',
'os_cron_analysis',
'db_trigger_analysis',
'binary_analysis',
'outbound_links',
'frontend_analysis',
'important_files_listing',
Expand Down Expand Up @@ -242,6 +243,7 @@ class SpbcMalwareScanner {/* eslint-disable-line no-unused-vars */
case 'frontend_analysis': this.amount = spbcSettings.frontendAnalysisAmount; break;
case 'signature_analysis': this.amount = 10; data.status = 'UNKNOWN,MODIFIED,OK,INFECTED,ERROR'; break;
case 'heuristic_analysis': this.amount = 4; data.status = 'UNKNOWN,MODIFIED,OK,INFECTED,ERROR'; break;
case 'binary_analysis': this.amount = 4; data.status = 'UNKNOWN,MODIFIED,OK,INFECTED,ERROR'; break;
case 'schedule_send_heuristic_suspicious_files': this.amount = 1; break;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<?php

namespace CleantalkSP\SpbctWP\Scanner\BinaryCheckModule;

class BinaryCheckModule
{
/**
* @var int Total count of binary files for analysis
*/
private $total_count = 0;

/**
* @var int Count of scanned files
*/
private $scanned_count = 0;

/**
* @var array Statuses of scanned files
*/
private $statuses = array();

/**
* Runs the BinaryCheckModule to find malicious binary files.
* @return array Analysis results
*/
public function run()
{
$this->total_count = 0;
$this->scanned_count = 0;
$this->statuses = array();

$binary_files = $this->getBinaryFiles();
$this->total_count = count($binary_files);
$this->analyzeBinaryFiles($binary_files);

return array(
'success' => true,
'end' => 1,
'total_count' => $this->total_count,
'scanned_count' => $this->scanned_count,
'statuses' => $this->statuses,
);
}

/**
* @return array
*/
public function getBinaryFiles()
{
global $wpdb;

$binary_files = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM " . SPBC_TBL_SCAN_FILES . " WHERE source = %s",
'BINARY'
)
);

return $binary_files ?: array();
}

/**
* @param array $binary_files
* @return void
*/
private function analyzeBinaryFiles($binary_files)
{
foreach ($binary_files as $binary_file) {
$path = realpath(ABSPATH . $binary_file->path);
if (!$path || !file_exists($path)) {
$this->incrementStatus('SKIPPED_NOT_FOUND');
continue;
}

if (!is_readable($path)) {
$this->incrementStatus('SKIPPED_NOT_READABLE');
continue;
}

$isSuspicious = $this->analyzeBinaryFile($path);
$this->scanned_count++;

if ($isSuspicious) {
$this->markAsSuspicious($binary_file->path);
$this->incrementStatus('SUSPICIOUS');
} else {
$this->incrementStatus('OK');
}
}
}

/**
* @param string $status
* @return void
*/
private function incrementStatus($status)
{
if (!isset($this->statuses[$status])) {
$this->statuses[$status] = 0;
}
$this->statuses[$status]++;
}

private function analyzeBinaryFile($binary_file_path)
{
// Read first bytes of the file (we need at least 4 bytes for all checks)
$handle = fopen($binary_file_path, 'rb');
if (!$handle) {
return false;
}

// Read first 4 bytes
$first_bytes = fread($handle, 4);
fclose($handle);

if (strlen($first_bytes) < 4) {
return false;
}

// Method 1: Check using ord() to get byte values
$byte1 = ord($first_bytes[0]);
$byte2 = ord($first_bytes[1]);
$byte3 = ord($first_bytes[2]);
$byte4 = ord($first_bytes[3]);

// Check for ELF (0x7F followed by "ELF" = 0x45 0x4C 0x46)
if ($byte1 === 0x7F && $byte2 === 0x45 && $byte3 === 0x4C && $byte4 === 0x46) {
return true; // ELF file detected
}

// Check for PE/MZ (Windows executable: 0x4D 0x5A = "MZ")
if ($byte1 === 0x4D && $byte2 === 0x5A) {
return true; // PE file detected
}

// Check for MACHO (macOS/iOS executables)
// 32-bit big-endian: 0xFE 0xED 0xFA 0xCE
// 32-bit little-endian: 0xCE 0xFA 0xED 0xFE
// 64-bit big-endian: 0xFE 0xED 0xFA 0xCF
// 64-bit little-endian: 0xCF 0xFA 0xED 0xFE
$macho_signatures = array(
array(0xFE, 0xED, 0xFA, 0xCE), // 32-bit big-endian
array(0xCE, 0xFA, 0xED, 0xFE), // 32-bit little-endian
array(0xFE, 0xED, 0xFA, 0xCF), // 64-bit big-endian
array(0xCF, 0xFA, 0xED, 0xFE), // 64-bit little-endian
);

foreach ($macho_signatures as $signature) {
if ($byte1 === $signature[0] && $byte2 === $signature[1] &&
$byte3 === $signature[2] && $byte4 === $signature[3]) {
return true; // MACHO file detected
}
}

return false;
}

private function markAsSuspicious($binary_file_path)
{
global $wpdb;

$wpdb->query(
$wpdb->prepare(
"UPDATE " . SPBC_TBL_SCAN_FILES . " SET status = 'INFECTED', severity = 'SUSPICIOUS' WHERE path = %s",
$binary_file_path
)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@ public static function bulkSendFileForAnalysis($fast_hashes_list = array())
$sql_result = $wpdb->get_results(
'SELECT fast_hash FROM ' . SPBC_TBL_SCAN_FILES . '
WHERE last_sent IS NULL
AND status NOT IN ("APPROVED_BY_USER","APPROVED_BY_CT","APPROVED_BY_CLOUD","DENIED_BY_CT")',
AND status NOT IN ("APPROVED_BY_USER","APPROVED_BY_CT","APPROVED_BY_CLOUD","DENIED_BY_CT")
AND source != \'BINARY\'',
ARRAY_A
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,11 @@ public static function viewFile($file_id)
return array('error' => 'FILE_NOT_FOUND');
}

// Binary files cannot be viewed as text
if (isset($file_info['source']) && $file_info['source'] === 'BINARY') {
return array('error' => esc_html__('Binary files cannot be viewed. These files contain executable code that is not human-readable.', 'security-malware-firewall'));
}

$file_path = $file_info['status'] == 'QUARANTINED' ? $file_info['q_path'] : $root_path . $file_info['path'];

if (!file_exists($file_path)) {
Expand Down
65 changes: 60 additions & 5 deletions lib/CleantalkSP/SpbctWP/Scanner/ScannerQueue.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
use CleantalkSP\SpbctWP\Scanner\ScanningStagesModule\Stages\OutboundLinks;
use CleantalkSP\SpbctWP\Scanner\ScanningStagesModule\Stages\ScheduleSendHeuristicSuspiciousFiles;
use CleantalkSP\SpbctWP\Scanner\ScanningStagesModule\Stages\SignatureAnalysis;
use CleantalkSP\SpbctWP\Scanner\ScanningStagesModule\Stages\BinaryAnalysis;
use CleantalkSP\SpbctWP\Scanner\BinaryCheckModule\BinaryCheckModule;
use CleantalkSP\SpbctWP\Scanner\Stages\CureStage;
use CleantalkSP\SpbctWP\Scanner\Stages\SendResultsStage;
use CleantalkSP\SpbctWP\Scanner\Stages\SignatureAnalysis\SignatureAnalysisFacade;
Expand All @@ -55,6 +57,7 @@ class ScannerQueue
'get_approved_hashes',
'signature_analysis',
'heuristic_analysis',
'binary_analysis',
'schedule_send_heuristic_suspicious_files',
'auto_cure_backup',
'auto_cure',
Expand Down Expand Up @@ -859,7 +862,7 @@ public function countFilesByStatusAndChecked($status = '', $caller = '')
$query =
'SELECT COUNT(fast_hash) AS cnt'
. ' FROM ' . SPBC_TBL_SCAN_FILES
. ' WHERE ' . $caller . " = '0' AND status IN (" . $status . ');';// No need to validate or sanitize, already did
. ' WHERE ' . $caller . " = '0' AND status IN (" . $status . ") AND source != 'BINARY'";// No need to validate or sanitize, already did
$result = $this->db->fetch($query);

return $result !== null
Expand Down Expand Up @@ -952,7 +955,7 @@ public function file_system_analysis($offset = null, $amount = null, $path_to_sc
//should be offset
$detected_at = current_time('timestamp');
$sql_hat = 'INSERT INTO ' . SPBC_TBL_SCAN_FILES
. ' (`path`, `size`, `perms`, `mtime`, `fast_hash`, `full_hash`, `detected_at`, `checked_heuristic`) VALUES ';
. ' (`path`, `size`, `perms`, `mtime`, `fast_hash`, `full_hash`, `detected_at`, `checked_heuristic`, `source`) VALUES ';

foreach ( $scanner->output_files as $_key => $file ) {
// skip restored files as is
Expand All @@ -973,6 +976,9 @@ public function file_system_analysis($offset = null, $amount = null, $path_to_sc
$file['checked_heuristic'] = $ext === 'js' ? 1 : 0;
//JS files end

// if extension is not set, set 'source' to 'binary'
$file['source'] = ! $ext ? 'BINARY' : '';

if ( ! spbc_check_ascii($file['path']) ) {
$sql_query__values_non_ascii[] = '(\'' . implode('\',\'', $file) . '\')';
} else {
Expand Down Expand Up @@ -1425,7 +1431,7 @@ public function signature_analysis($status = 'UNKNOWN,MODIFIED,OK,INFECTED,ERROR
$files = $this->db->fetchAll(
'SELECT path, source_type, source, version, status, checked_heuristic, checked_signatures, fast_hash, real_full_hash, full_hash, weak_spots, difference, severity, size, error_msg'
. ' FROM ' . SPBC_TBL_SCAN_FILES
. " WHERE checked_signatures = 0 AND status IN ($status)"
. " WHERE checked_signatures = 0 AND status IN ($status) AND source != 'BINARY'"
. " LIMIT 1000"
);

Expand Down Expand Up @@ -1612,7 +1618,7 @@ public function heuristic_analysis($status = 'UNKNOWN,MODIFIED,OK,INFECTED,ERROR
$files = $this->db->fetchAll(
'SELECT path, source_type, source, version, status, checked_heuristic, checked_signatures, fast_hash, real_full_hash, full_hash, weak_spots, difference, severity, size, error_msg'
. ' FROM ' . SPBC_TBL_SCAN_FILES
. " WHERE checked_heuristic = 0 AND status IN ($status)"
. " WHERE checked_heuristic = 0 AND status IN ($status) AND source != 'BINARY'"
. " LIMIT 1000"
);

Expand Down Expand Up @@ -1815,7 +1821,8 @@ public function schedule_send_heuristic_suspicious_files() // phpcs:ignore PSR1.
. ' AND checked_heuristic = 1 '
. ' AND weak_spots NOT LIKE "%SIGNATURES%" '
. ' AND status NOT IN ("APPROVED_BY_USER", "APPROVED_BY_CT", "APPROVED_BY_CLOUD")'
. ' AND (pscan_pending_queue IS NULL OR pscan_pending_queue = 0); '
. ' AND (pscan_pending_queue IS NULL OR pscan_pending_queue = 0) '
. ' AND source != \'BINARY\'; '
);

$count = (int)$result_db;
Expand Down Expand Up @@ -1855,6 +1862,54 @@ public function schedule_send_heuristic_suspicious_files() // phpcs:ignore PSR1.
);
}

public function binary_analysis() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
{
$output = array();

// Initialize scanning stages storage
$scanning_stages_storage = new ScanningStagesStorage();
$scanning_stages_storage->converter->loadCollection();
$stage_data_obj = $scanning_stages_storage->getStage(BinaryAnalysis::class);

// Run binary check module
$binary_check_module = new BinaryCheckModule();
$result = $binary_check_module->run();

// Update stage data
$stage_data_obj->set('total_count_files_for_analysis', $result['total_count']);
$stage_data_obj->set('count_files_to_check', $result['total_count']);
$stage_data_obj->set('scanned_count_files', $result['scanned_count']);
$stage_data_obj->set('statuses', $result['statuses']);

// Save stage data to DB
$scanning_stages_storage->saveToDb();

// Adding to log
ScanningLogFacade::writeToLog(
'<b>' . $stage_data_obj::getTitle() . '</b> ' . $stage_data_obj->getDescription()
);

// Prepare output
$output['success'] = $result['success'];
$output['end'] = $result['end'];
$output['processed'] = $result['scanned_count'];
$output['total'] = $result['total_count'];
$output['stage_data_for_logging'] = array(
'title' => $stage_data_obj::getTitle(),
'description' => $stage_data_obj->getDescription()
);

// Accordion interactivity
$suspicious_count = isset($result['statuses']['SUSPICIOUS']) ? $result['statuses']['SUSPICIOUS'] : 0;
$refresh_data = array(
'do_refresh' => !empty($suspicious_count),
'control_tab' => 'files',
);
$output['interactivity_data'] = ScannerInteractivityData::prepare(__FUNCTION__, $refresh_data);

return $output;
}

public function auto_cure_backup() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
{
return spbc_backup__files_with_signatures(true);
Expand Down
Loading