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
24 changes: 24 additions & 0 deletions inc/spbc-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,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' => true,
),
'scanner__os_cron_analysis' => array(
'type' => 'field',
'title' => Scanner\OSCron\View\OSCronLocale::getInstance()->settings__option_title,
Expand Down Expand Up @@ -2324,6 +2330,14 @@ function spbc_field_scanner__prepare_data__files(&$table)
unset($row->actions['delete']);
}

// Binary files: leave only view/delete/quarantine actions
if ( isset($row->source) && $row->source === 'BINARY' ) {
unset($row->actions['send']);
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 @@ -2978,6 +2992,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 Expand Up @@ -4881,6 +4901,10 @@ function spbc_settings__get_description()
'title' => __('Signature analysis', 'security-malware-firewall'),
'desc' => __('Code signatures — it\'s a code sequence a malicious program consists of. Signatures are being added to the database after analysis of the infected files. Search for such malicious code sequences is performed in scanning by signatures. If any part of code matches a virus code from the database, such files would be marked as critical.', 'security-malware-firewall')
),
'scanner__binary_analysis' => array(
'title' => __('Binary analysis', 'security-malware-firewall'),
'desc' => __('Search for suspicious binary patterns in files.', 'security-malware-firewall')
),
'scanner__auto_cure' => array(
'title' => __('Cure malware', 'security-malware-firewall'),
'desc' => __('It cures infected files automatically if the scanner knows cure methods for these specific cases. If the option is disabled then when the scanning process ends you will be presented with several actions you can do to the found files: Cure. Malicious code will be removed from the file. Replace. The file will be replaced with the original file. Delete. The file will be put in quarantine. Do nothing. Before any action is chosen, backups of the files will be created and if the cure is unsuccessful it\'s possible to restore each file.', '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;
}

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

if ($isCritical) {
$this->markAsCritical($binary_file->path);
$this->incrementStatus('CRITICAL');
} 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 markAsCritical($binary_file_path)
{
global $wpdb;

$wpdb->query(
$wpdb->prepare(
"UPDATE " . SPBC_TBL_SCAN_FILES . " SET status = 'INFECTED', severity = 'CRITICAL' 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 @@ -341,6 +341,23 @@ public static function viewFile($file_id)
return array('error' => 'FILE_NOT_READABLE');
}

// Binary files: render as hex dump (first chunk only)
if (isset($file_info['source']) && $file_info['source'] === 'BINARY') {
$file_text = self::renderBinaryHexDump($file_path);
if (empty($file_text)) {
return array('error' => 'FILE_EMPTY');
}

return array(
'success' => true,
'file' => $file_text,
'file_path' => $file_path,
'difference' => $file_info['difference'],
'weak_spots' => $file_info['weak_spots'],
'exec_time' => round(microtime(true) - $time_start),
);
}

$file = file($file_path);
if (!$file) {
return array('error' => 'FILE_EMPTY');
Expand All @@ -365,4 +382,75 @@ public static function viewFile($file_id)
'exec_time' => round(microtime(true) - $time_start),
);
}

/**
* Render the first bytes of a binary file as a classic hex dump.
* Each output line: offset | 16 hex bytes | ASCII gutter.
*
* @param string $file_path
* @return array<int,string>
*/
private static function renderBinaryHexDump($file_path)
{
$max_bytes = 4096;
$bytes_per_line = 16;

$handle = @fopen($file_path, 'rb');
if (!$handle) {
return array();
}

$data = @fread($handle, $max_bytes);
$file_size = @filesize($file_path);
@fclose($handle);

if ($data === false || $data === '') {
return array();
}

$len = strlen($data);
$rows = array();
$line_no = 1;

for ($offset = 0; $offset < $len; $offset += $bytes_per_line) {
$chunk = substr($data, $offset, $bytes_per_line);
$hex_parts = array();
$ascii = '';
$chunk_len = strlen($chunk);
for ($i = 0; $i < $bytes_per_line; $i++) {
if ($i < $chunk_len) {
$byte = ord($chunk[$i]);
$hex_parts[] = sprintf('%02X', $byte);
$ascii .= ($byte >= 0x20 && $byte <= 0x7E) ? chr($byte) : '.';
} else {
$hex_parts[] = ' ';
$ascii .= ' ';
}
}
$hex_block = implode(' ', array_slice($hex_parts, 0, 8))
. ' '
. implode(' ', array_slice($hex_parts, 8, 8));

$rows[$line_no] = sprintf(
'%08X %s |%s|',
$offset,
$hex_block,
htmlspecialchars($ascii)
);
$line_no++;
}

if (is_int($file_size) && $file_size > $len) {
$rows[$line_no] = sprintf(
'... %s',
sprintf(
esc_html__('truncated, showing first %1$d of %2$d bytes', 'security-malware-firewall'),
$len,
$file_size
)
);
}

return $rows;
}
}
Loading