summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMathieu Deous2022-04-15 22:02:16 +0200
committerGitHub2022-04-15 22:02:16 +0200
commit61126b35771eaa7537757362f264dbc8b6a32ed7 (patch)
tree9732a52f1c39c8ae3d8a1a35e8cd9e45d7f2cfea
parent98fb222eb0a878df2abb6b13386a5ebc46b835c3 (diff)
Rewrite shell script in Go
-rw-r--r--.github/workflows/test.yml35
-rw-r--r--.gitignore2
-rw-r--r--.travis.yml21
-rw-r--r--Makefile24
-rw-r--r--README.md47
-rw-r--r--debian/changelog101
-rw-r--r--debian/compat1
-rw-r--r--debian/conffiles0
-rw-r--r--debian/control14
-rw-r--r--debian/copyright7
-rw-r--r--debian/files1
-rw-r--r--debian/nbs-phpmalwarefinder.dirs1
-rw-r--r--debian/nbs-phpmalwarefinder.install12
-rwxr-xr-xdebian/rules12
-rw-r--r--go.mod10
-rw-r--r--go.sum7
-rwxr-xr-xphp-malware-finder/phpmalwarefinder96
-rw-r--r--php-malware-finder/phpmalwarefinder.go372
-rwxr-xr-xphp-malware-finder/tests.sh2
19 files changed, 461 insertions, 304 deletions
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..f29e422
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,35 @@
1name: Test Suite
2
3on:
4 push:
5 branches:
6 - master
7 pull_request:
8 branches:
9 - master
10
11jobs:
12 test:
13 name: Test
14 runs-on: ubuntu-latest
15 steps:
16 - name: Checkout code
17 uses: actions/checkout@v2
18
19 - name: Setup Go
20 uses: actions/setup-go@v2
21 with:
22 go-version: '^1.17'
23
24 # apt repos don't have YARA v4.2, install it from git
25 - name: Install YARA
26 run: |
27 git clone --depth 1 https://github.com/virustotal/yara.git
28 cd yara
29 bash ./build.sh
30 sudo make install
31 cd ..
32
33 - name: Run tests
34 run: |
35 LD_LIBRARY_PATH=/usr/local/lib make tests
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..639d072
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
1php-malware-finder/phpmalwarefinder
2.idea
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 132447d..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,21 +0,0 @@
1language: c
2
3addons:
4 apt:
5 packages:
6 - devscripts
7 - fakeroot
8 - debhelper
9
10install:
11 - git clone --depth 1 https://github.com/plusvic/yara.git yara3
12 - cd yara3
13 - bash ./build.sh
14 - ./configure
15 - make
16 - cp ./yara ../php-malware-finder/
17 - cd ..
18
19script:
20 - make tests
21 - make deb
diff --git a/Makefile b/Makefile
index 1fa1a91..931f4e7 100644
--- a/Makefile
+++ b/Makefile
@@ -1,19 +1,15 @@
1VERSION=1.0 1.PHONY: clean deps tests
2DEBVER := $(shell sed 's,[/\.].*,,' < /etc/debian_version)
3 2
4tests: 3all: php-malware-finder/phpmalwarefinder
5 @cd ./php-malware-finder && bash ./tests.sh
6 4
7debclean: 5php-malware-finder/phpmalwarefinder:
8 rm -rf php-malware-finder/debian 6 go build -o php-malware-finder/phpmalwarefinder php-malware-finder/phpmalwarefinder.go
9 rm -f *.build *.changes *.deb
10 7
11extract: 8clean:
12 cp -r debian php-malware-finder 9 rm -f php-malware-finder/phpmalwarefinder
13 git checkout php-malware-finder/php.yar
14 10
15rpm: 11deps:
16 @echo "no rpm build target for now, feel free to submit one" 12 go mod tidy -v
17 13
18deb: debclean extract 14tests: php-malware-finder/phpmalwarefinder
19 cd php-malware-finder && debuild -b -us -uc --lintian-opts -X po-debconf --profile debian 15 @cd ./php-malware-finder && bash ./tests.sh
diff --git a/README.md b/README.md
index 1b60ce1..6ae0b07 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
1[![Build Status](https://travis-ci.org/jvoisin/php-malware-finder.svg?branch=master)](https://travis-ci.org/jvoisin/php-malware-finder) 1![Test Suite](https://github.com/jvoisin/php-malware-finder/actions/workflows/test.yml/badge.svg)
2 2
3# PHP Malware Finder 3# PHP Malware Finder
4 4
@@ -54,38 +54,39 @@ Detection is performed by crawling the filesystem and testing files against a
54[set](https://github.com/jvoisin/php-malware-finder/blob/master/php-malware-finder/php.yar) 54[set](https://github.com/jvoisin/php-malware-finder/blob/master/php-malware-finder/php.yar)
55of [YARA](http://virustotal.github.io/yara/) rules. Yes, it's that simple! 55of [YARA](http://virustotal.github.io/yara/) rules. Yes, it's that simple!
56 56
57Instead of using an *hash-based* approach, 57Instead of using a *hash-based* approach,
58PMF tries as much as possible to use semantic patterns, to detect things like 58PMF tries as much as possible to use semantic patterns, to detect things like
59"a `$_GET` variable is decoded two times, unzipped, 59"a `$_GET` variable is decoded two times, unzipped,
60and then passed to some dangerous function like `system`". 60and then passed to some dangerous function like `system`".
61 61
62## Installation 62## Installation
63- [Install Yara](https://yara.readthedocs.io/en/stable/gettingstarted.html#compiling-and-installing-yara). 63- Install Go (using your package manager, or [manually](https://go.dev/doc/install))
64This is also possible via some Linux package managers: 64- Install libyara >= 4.2 (using your package manager, or [from source](https://yara.readthedocs.io/en/stable/gettingstarted.html))
65 - Debian: `sudo apt-get install yara` 65- Download php-malware-finder: `git clone https://github.com/jvoisin/php-malware-finder.git`
66 - Red Hat: `yum install yara` (requires the [EPEL repository](https://fedoraproject.org/wiki/EPEL)) 66- Build php-malware-finder: `cd php-malware-finder && make`
67
68You can also compile it from source:
69
70```
71git clone git@github.com:VirusTotal/yara.git
72cd yara/
73YACC=bison ./configure
74make
75```
76
77- Download php-malware-finder `git clone https://github.com/jvoisin/php-malware-finder.git`
78 67
79## How to use it? 68## How to use it?
80 69
81``` 70```
82$ ./phpmalwarefinder -h 71$ ./phpmalwarefinder -h
83Usage phpmalwarefinder [-cfhtvl] <file|folder> ... 72Usage:
84 -c Optional path to a rule file 73 phpmalwarefinder [OPTIONS] [Target]
85 -f Fast mode 74
86 -h Show this help message 75Application Options:
87 -t Specify the number of threads to use (8 by default) 76 -r, --rules-dir= Rules location (default: /etc/phpmalwarefinder or .)
88 -v Verbose mode 77 -a, --show-all Display all matched rules
78 -f, --fast Enable YARA's fast mode'
79 -R, --rate-limit= Max. filesystem ops per second, 0 for no limit (default: 0)
80 -v, --verbose Verbose mode
81 -w, --workers= Number of workers to spawn for scanning (default: 32)
82 -L, --long-lines Check long lines
83 -c, --exclude-common Do not scan files with common extensions
84 -i, --exclude-imgs Do not scan image files
85 -x, --exclude-ext= Additional file extensions to exclude
86 -u, --update Update rules
87
88Help Options:
89 -h, --help Show this help message
89``` 90```
90 91
91Or if you prefer to use `yara`: 92Or if you prefer to use `yara`:
diff --git a/debian/changelog b/debian/changelog
deleted file mode 100644
index e169478..0000000
--- a/debian/changelog
+++ /dev/null
@@ -1,101 +0,0 @@
1nbs-phpmalwarefinder (0.3.4-1~deb) oldstable; urgency=medium
2
3 * new upstream version :
4 - update the whitelists
5 - new rules to prevent bypasses
6 - readme improvement
7
8 -- jre <jre@nbs-system.com> Mon, 07 Nov 2016 14:26:22 +0100
9
10nbs-phpmalwarefinder (0.3.3-1~deb) oldstable; urgency=medium
11
12 * new upstream version :
13 - add a strrev-based detection
14 - update the whitelists
15 - add a new fancy logo
16 * improve the release process
17
18 -- jvo <jvo@nbs-system.com> Mon, 24 Oct 2016 10:02:32 +0200
19
20nbs-phpmalwarefinder (0.3.2-1~deb) oldstable; urgency=medium
21
22 * new upstream version :
23 - whitelists are now split into files, each for one CMS
24 - a custom whitelist is available for users to add their own
25 - a mass whitelist helper has been added
26 * Added the custom whitelist to conffiles to prevent package upgrade from
27 overwriting users modification.
28
29 -- jre <jre@nbs-system.com> Fri, 29 Jul 2016 09:47:56 +0200
30
31nbs-phpmalwarefinder (0.3.1-1~deb) oldstable; urgency=medium
32
33 * new upstream version :
34 - rules for visbot detection
35 - now detecting base64 encoded string USER_AGENT
36 - debian squeeze support dropped
37 - some false positives fixes
38
39 -- jre <jre@nbs-system.com> Thu, 19 May 2016 15:22:47 +0200
40
41nbs-phpmalwarefinder (0.3.0-1~deb) oldstable; urgency=medium
42
43 * rules files refactoring :
44 - php-malware-finder now comes with asp malware detection
45 - rules have been split in different files to avoid false positives
46
47 * The -l option allows language specific checks, for now only ASP and PHP
48 are supported.
49 * The -u option now allows to update rules without having to upgrade the
50 package.
51
52 -- jre <jre@nbs-system.com> Thu, 14 Apr 2016 16:04:14 +0200
53
54nbs-phpmalwarefinder (0.2.2-1~deb) oldstable; urgency=medium
55
56 * new rules : bad_php.yara to find bad coding practices
57 * malwares.yara now comes with posix_* functions detection, new hard-coded
58 strings as well as php:// filter
59 * The TooShort rule has been improved to reduce FP
60
61 -- jre <jre@nbs-system.com> Mon, 15 Feb 2016 15:48:06 +0100
62
63nbs-phpmalwarefinder (0.2.1-1~deb) oldstable; urgency=medium
64
65 * docroot-checker.sh added, helpful for both first and periodic security
66 scan.
67
68 -- jre <jre@nbs-system.com> Mon, 01 Feb 2016 11:08:08 +0100
69
70nbs-phpmalwarefinder (0.2.0-2~deb) oldstable; urgency=medium
71
72 * New detection rules added
73
74 -- sbl <sbl@nbs-system.com> Thu, 28 Jan 2016 14:58:45 +0200
75
76nbs-phpmalwarefinder (0.2.0-1~deb) oldstable; urgency=medium
77
78 * Now supports whitelist using yara hash function
79 * New detection rules added (tested against
80 https://github.com/tennc/webshell malware collection)
81
82 -- jre <jre@nbs-system.com> Fri, 09 Oct 2015 14:58:45 +0200
83
84nbs-phpmalwarefinder (0.1.1-1~deb) oldstable; urgency=medium
85
86 * new dependecy on util-linux since the script is using ionice
87 * postinst script added to create diff folder
88
89 -- jre <jre@nbs-system.com> Tue, 28 Apr 2015 15:07:12 +0200
90
91nbs-phpmalwarefinder (0.1.1-1~deb) oldstable; urgency=medium
92
93 * new signature to detect malware in footer and header
94
95 -- jre <jre@nbs-system.com> Tue, 14 Apr 2015 14:40:05 +0000
96
97nbs-phpmalwarefinder (0.1) UNRELEASED; urgency=medium
98
99 * Initial release.
100
101 -- jvoisin <jvo@nbs-system.com> Tue, 24 Mar 2015 11:10:36 +0100
diff --git a/debian/compat b/debian/compat
deleted file mode 100644
index 7ed6ff8..0000000
--- a/debian/compat
+++ /dev/null
@@ -1 +0,0 @@
15
diff --git a/debian/conffiles b/debian/conffiles
deleted file mode 100644
index e69de29..0000000
--- a/debian/conffiles
+++ /dev/null
diff --git a/debian/control b/debian/control
deleted file mode 100644
index b50454f..0000000
--- a/debian/control
+++ /dev/null
@@ -1,14 +0,0 @@
1Source: nbs-phpmalwarefinder
2Section: utils
3Priority: optional
4Maintainer: Security team <secu@nbs-system.com>
5Build-Depends: debhelper (>= 8)
6Standards-Version: 3.9.5
7Vcs-Git: https://github.com/nbs-system/php-malware-finder
8Vcs-Browser: https://github.com/nbs-system/php-malware-finder
9
10Package: nbs-phpmalwarefinder
11Architecture: any
12Depends: nbs-yara, wget, nbs-python-yara, python
13Description: yara-based php webshell finder
14 PhpMalwareFinder is a webshell and malware hunter using yara and signatures.
diff --git a/debian/copyright b/debian/copyright
deleted file mode 100644
index 6bec77a..0000000
--- a/debian/copyright
+++ /dev/null
@@ -1,7 +0,0 @@
1Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
2Upstream-Name: phpmalwarefinder
3Source: https://github.com/nbs-system/php-malware-finder
4
5Files: *
6Copyright 2015 Julien (jvoisin) Voisin <jvo@nbs-system.com>
7License: GPLv3
diff --git a/debian/files b/debian/files
deleted file mode 100644
index 23f95ef..0000000
--- a/debian/files
+++ /dev/null
@@ -1 +0,0 @@
1nbs-phpmalwarefinder_0.1_amd64.deb utils optional
diff --git a/debian/nbs-phpmalwarefinder.dirs b/debian/nbs-phpmalwarefinder.dirs
deleted file mode 100644
index 61a8d27..0000000
--- a/debian/nbs-phpmalwarefinder.dirs
+++ /dev/null
@@ -1 +0,0 @@
1etc/phpmalwarefinder/ \ No newline at end of file
diff --git a/debian/nbs-phpmalwarefinder.install b/debian/nbs-phpmalwarefinder.install
deleted file mode 100644
index 748222d..0000000
--- a/debian/nbs-phpmalwarefinder.install
+++ /dev/null
@@ -1,12 +0,0 @@
1whitelists/custom.yar etc/phpmalwarefinder/whitelists
2whitelists/drupal.yar etc/phpmalwarefinder/whitelists
3whitelists/magento2.yar etc/phpmalwarefinder/whitelists
4whitelists/phpmyadmin.yar etc/phpmalwarefinder/whitelists
5whitelists/prestashop.yar etc/phpmalwarefinder/whitelists
6whitelists/symfony.yar etc/phpmalwarefinder/whitelists
7whitelists/wordpress.yar etc/phpmalwarefinder/whitelists
8utils/generate_whitelist.py usr/bin/
9utils/mass_whitelist.py usr/bin/
10php.yar etc/phpmalwarefinder
11whitelist.yar etc/phpmalwarefinder
12phpmalwarefinder usr/bin/
diff --git a/debian/rules b/debian/rules
deleted file mode 100755
index bcf500a..0000000
--- a/debian/rules
+++ /dev/null
@@ -1,12 +0,0 @@
1#!/usr/bin/make -f
2
3BUILDDIR=debian/build
4
5override_dh_auto_clean: #fuck you debian
6
7override_dh_auto_build:
8
9%:
10 dh $@
11
12.PHONY: build
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..39b2f36
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,10 @@
1module github.com/jvoisin/php-malware-finder
2
3go 1.17
4
5require (
6 github.com/hillu/go-yara/v4 v4.2.0
7 github.com/jessevdk/go-flags v1.5.0
8)
9
10require golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..aa0af83
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,7 @@
1github.com/hillu/go-yara/v4 v4.2.0 h1:C0YycpDYXMlOsN4kbFhvGmfNiaTgpXoLQRS1oUME9ak=
2github.com/hillu/go-yara/v4 v4.2.0/go.mod h1:rkb/gSAoO8qcmj+pv6fDZN4tOa3N7R+qqGlEkzT4iys=
3github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
4github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
5golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
6golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng=
7golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/php-malware-finder/phpmalwarefinder b/php-malware-finder/phpmalwarefinder
deleted file mode 100755
index a6de360..0000000
--- a/php-malware-finder/phpmalwarefinder
+++ /dev/null
@@ -1,96 +0,0 @@
1#!/usr/bin/env bash
2
3YARA=$(type -P yara)
4CONFIG_PATH='/etc/phpmalwarefinder/php.yar'
5
6if [ ! -x "$YARA" ]
7then
8 YARA='./yara'
9 if [ ! -x "$YARA" ]
10 then
11 echo 'Unable to find yara in your $PATH, and in the current directory.'
12 exit 0
13 fi
14fi
15
16if [ ! -f "$CONFIG_PATH" ]
17then
18 CONFIG_PATH="$(dirname "$0")/php.yar"
19fi
20
21needle_in_haystack() {
22
23 needle=$(mktemp)
24 grep -E '(PasswordProtection|Websites|TooShort|NonPrintableChars)' $1 > $needle
25 if [ ! "$(wc -l "$needle" | awk '{print $1}')" = "0" ]; then
26 echo "================================================="
27 echo "You should take a look at the files listed below:"
28 cat "$needle"
29 fi;
30 rm "$needle"
31}
32
33show_help() {
34 cat << EOF
35Usage ${0##*/} [-cfhtvl] <file|folder> ...
36 -c Optional path to a rule file
37 -f Fast mode
38 -h Show this help message
39 -t Specify the number of threads to use (8 by default)
40 -v Verbose mode
41EOF
42}
43
44OPTIND=1
45while getopts "c:fht:v" opt; do
46 case "$opt" in
47 c)
48 CONFIG_PATH=${OPTARG}
49 ;;
50 f)
51 OPTS="${OPTS} -f"
52 ;;
53 h)
54 show_help
55 exit 0
56 ;;
57 t)
58 OPTS="${OPTS} --threads=${OPTARG}"
59 ;;
60 v)
61 OPTS="${OPTS} -s"
62 ;;
63 '?')
64 show_help
65 exit 1
66 ;;
67 esac
68done
69shift "$((OPTIND-1))"
70
71if [ ! -e "${CONFIG_PATH}" ]
72then
73 echo "The configuration file ${CONFIG_PATH} doesn't exist. Please give me a valid file."
74 exit 1
75fi
76
77if [ -z "$@" ]
78then
79 show_help
80 exit 1
81fi
82
83
84# Include correct yara rule
85OPTS="${OPTS} -r ${CONFIG_PATH}"
86
87# Copy outpout to temporary file
88output=$(mktemp)
89# delete trailing slash for directories to prevent double slash (issue #40)
90target=$(echo "$@" | sed s'#/$##')
91# Execute rules
92# Using $-interpolation and quotes to support a target with whitespaces
93$YARA $OPTS "$target" |tee $output
94
95needle_in_haystack "$output"
96rm "$output"
diff --git a/php-malware-finder/phpmalwarefinder.go b/php-malware-finder/phpmalwarefinder.go
new file mode 100644
index 0000000..799df60
--- /dev/null
+++ b/php-malware-finder/phpmalwarefinder.go
@@ -0,0 +1,372 @@
1package main
2
3import (
4 "bytes"
5 "fmt"
6 "io"
7 "io/ioutil"
8 "log"
9 "net/http"
10 "os"
11 "path"
12 "path/filepath"
13 "strings"
14 "time"
15
16 "github.com/hillu/go-yara/v4"
17 "github.com/jessevdk/go-flags"
18)
19
20const RulesURI = "https://raw.githubusercontent.com/jvoisin/php-malware-finder/master/php-malware-finder/"
21const RulesFile = "php.yar"
22const DefaultDir = "/etc/phpmalwarefinder"
23const ScanMaxDuration = time.Duration(60)
24const TooShort = "TooShort"
25const TooShortMaxLines = 2
26const TooShortMinChars = 300
27const DangerousMatchWeight = 2
28const DangerousMinScore = 3
29const FileBufferSize = 32 * 1024 // 32KB
30const YaraMaxThreads = 32
31
32var args struct { // command-line arguments specs using github.com/jessevdk/go-flags
33 RulesDir string `short:"r" long:"rules-dir" description:"Rules location (default: /etc/phpmalwarefinder or .)"`
34 ShowAll bool `short:"a" long:"show-all" description:"Display all matched rules"`
35 Fast bool `short:"f" long:"fast" description:"Enable YARA's fast mode"`
36 RateLimit int `short:"R" long:"rate-limit" description:"Max. filesystem ops per second, 0 for no limit" default:"0"`
37 Verbose bool `short:"v" long:"verbose" description:"Verbose mode"`
38 Workers int `short:"w" long:"workers" description:"Number of workers to spawn for scanning" default:"32"`
39 LongLines bool `short:"L" long:"long-lines" description:"Check long lines"`
40 ExcludeCommon bool `short:"c" long:"exclude-common" description:"Do not scan files with common extensions"`
41 ExcludeImgs bool `short:"i" long:"exclude-imgs" description:"Do not scan image files"`
42 ExcludedExts []string `short:"x" long:"exclude-ext" description:"Additional file extensions to exclude"`
43 Update bool `short:"u" long:"update" description:"Update rules"`
44 Positional struct {
45 Target string
46 } `positional-args:"yes"`
47}
48var scanFlags yara.ScanFlags
49var stoppedWorkers int
50var lineFeed = []byte{'\n'}
51var dangerousMatches = map[string]struct{}{
52 "PasswordProtection": {},
53 "Websites": {},
54 "TooShort": {},
55 "NonPrintableChars": {},
56}
57var excludedDirs = [...]string{
58 "/.git/", "/.hg/", "/.svn/", "/.CVS/",
59}
60var excludedExts = map[string]struct{}{}
61var commonExts = [...]string{
62 ".js", ".coffee", ".map", ".min", ".css", ".less", // static files
63 ".zip", ".rar", ".7z", ".gz", ".bz2", ".xz", ".tar", ".tgz", // archives
64 ".txt", ".csv", ".json", ".rst", ".md", ".yaml", ".yml", // plain text
65 ".so", ".dll", ".bin", ".exe", ".bundle", // binaries
66}
67var imageExts = [...]string{
68 ".png", ".jpg", ".jpeg", ".gif", ".svg", ".bmp", ".ico",
69}
70var scannedFilesCount int
71
72// handleError is a generic error handler which displays the error message to the user and exits if required.
73func handleError(err error, exit bool) {
74 if err != nil {
75 log.Println("[ERROR]", err)
76 if exit {
77 os.Exit(1)
78 }
79 }
80}
81
82// updateRules downloads latest YARA rules from phpmalwarefinder GitHub repository.
83// Download location is either `args.RulesDir`, `/etc/phpmalwarefinder`, or the current directory.
84func updateRules() {
85 if args.Verbose {
86 log.Println("[DEBUG] updating ruleset")
87 }
88
89 downloadFile := func(uri string) []byte {
90 resp, err := http.Get(uri)
91 handleError(err, true)
92 defer func() {
93 err := resp.Body.Close()
94 handleError(err, false)
95 }()
96 data, err := ioutil.ReadAll(resp.Body)
97 handleError(err, true)
98 return data
99 }
100 writeFile := func(dst string, data []byte) {
101 err := ioutil.WriteFile(dst, data, 0440)
102 handleError(err, true)
103 }
104
105 rulesFiles := [...]string{
106 RulesFile,
107 "whitelist.yar", "whitelists/drupal.yar", "whitelists/magento1ce.yar",
108 "whitelists/magento2.yar", "whitelists/phpmyadmin.yar", "whitelists/prestashop.yar",
109 "whitelists/symfony.yar", "whitelists/wordpress.yar"}
110
111 // download rules
112 for _, rule := range rulesFiles {
113 rulesUri := RulesURI + rule
114 data := downloadFile(rulesUri)
115 outPath := path.Join(args.RulesDir, rule)
116 writeFile(outPath, data)
117 log.Println("[INFO] updated rule:", rule)
118 }
119}
120
121// fileStats takes a file path as argument and returns its lines and characters count.
122// File reading is done using a 32KB buffer to minimize memory usage.
123func fileStats(filepath string) (int, int, error) {
124 f, err := os.Open(filepath)
125 handleError(err, true)
126 defer func() {
127 err := f.Close()
128 handleError(err, false)
129 }()
130 charCount, lineCount := 0, 0
131 buf := make([]byte, FileBufferSize)
132 for {
133 chunkSize, err := f.Read(buf)
134 charCount += chunkSize
135 lineCount += bytes.Count(buf[:chunkSize], lineFeed)
136 switch {
137 case err == io.EOF:
138 return charCount, lineCount, nil
139 case err != nil:
140 return charCount, lineCount, err
141 }
142 }
143}
144
145// makeScanner creates a YARA scanner with the appropriate options set.
146func makeScanner(rules *yara.Rules) *yara.Scanner {
147 scanner, err := yara.NewScanner(rules)
148 handleError(err, true)
149 scanner.SetFlags(scanFlags)
150 scanner.SetTimeout(ScanMaxDuration)
151 return scanner
152}
153
154// processFiles reads file paths from the `targets` channel, scans it, and writes matches to the `results` channel.
155// Scanning is done using YARA `rules`, and using `fileStats` if `args.LongLines` is set.
156// `ticker` is a `time.Time` object created with `time.Tick` used to throttle file scans to minimize impact on I/O.
157func processFiles(rules *yara.Rules, targets <-chan string, results chan<- map[string][]yara.MatchRule, ticker <-chan time.Time) {
158 scanner := makeScanner(rules)
159 for target := range targets {
160 <-ticker
161 scannedFilesCount++
162 result := map[string][]yara.MatchRule{target: {}}
163
164 if args.LongLines {
165 charCount, lineCount, err := fileStats(target)
166 handleError(err, false)
167 if lineCount <= TooShortMaxLines && charCount >= TooShortMinChars {
168 tooShort := yara.MatchRule{Rule: TooShort}
169 result[target] = append(result[target], tooShort)
170 }
171 }
172
173 var matches yara.MatchRules
174 err := scanner.SetCallback(&matches).ScanFile(target)
175 if err != nil {
176 log.Println("[ERROR]", err)
177 continue
178 }
179 for _, match := range matches {
180 result[target] = append(result[target], match)
181 }
182 results <- result
183 }
184 stoppedWorkers++
185 if stoppedWorkers == args.Workers {
186 close(results)
187 }
188}
189
190// scanDir recursively crawls `dirName`, and writes file paths to the `targets` channel.
191// Files sent to `targets` are filtered according to their extensions.
192func scanDir(dirName string, targets chan<- string, ticker <-chan time.Time) {
193 visit := func(pathName string, fileInfo os.FileInfo, err error) error {
194 <-ticker
195 if !fileInfo.IsDir() {
196 for _, dir := range excludedDirs {
197 if strings.Contains(pathName, dir) {
198 return nil
199 }
200 }
201 fileExt := filepath.Ext(fileInfo.Name())
202 if _, exists := excludedExts[fileExt]; !exists {
203 targets <- pathName
204 }
205 }
206 return nil
207 }
208 err := filepath.Walk(dirName, visit)
209 handleError(err, false)
210 close(targets)
211}
212
213func main() {
214 startTime := time.Now()
215 _, err := flags.Parse(&args)
216 handleError(err, true)
217
218 // check rules path
219 if args.RulesDir == "" {
220 args.RulesDir = DefaultDir
221 if _, err := os.Stat(args.RulesDir); os.IsNotExist(err) {
222 args.RulesDir, _ = os.Getwd()
223 sigFile := path.Join(args.RulesDir, RulesFile)
224 if _, err = os.Stat(sigFile); os.IsNotExist(err) {
225 handleError(fmt.Errorf("no rules in %s or %s", DefaultDir, args.RulesDir), true)
226 }
227 }
228 }
229 if args.Verbose {
230 log.Println("[DEBUG] rules directory:", args.RulesDir)
231 }
232
233 // update rules if required
234 if args.Update {
235 updateRules()
236 os.Exit(0)
237 }
238
239 // add custom excluded file extensions
240 if args.ExcludeCommon {
241 for _, commonExt := range commonExts {
242 excludedExts[commonExt] = struct{}{}
243 }
244 }
245 if args.ExcludeImgs || args.ExcludeCommon {
246 for _, imgExt := range imageExts {
247 excludedExts[imgExt] = struct{}{}
248 }
249 }
250 for _, ext := range args.ExcludedExts {
251 if string(ext[0]) != "." {
252 ext = "." + ext
253 }
254 excludedExts[ext] = struct{}{}
255 }
256 if args.Verbose {
257 extList := make([]string, len(excludedExts))
258 i := 0
259 for ext := range excludedExts {
260 extList[i] = ext[1:]
261 i++
262 }
263 log.Println("[DEBUG] excluded file extensions:", strings.Join(extList, ","))
264 }
265
266 // load YARA rules
267 rulePath := path.Join(args.RulesDir, RulesFile)
268 data, _ := ioutil.ReadFile(rulePath)
269 rules, _ := yara.Compile(string(data), nil)
270 if args.Verbose {
271 log.Println("[DEBUG] ruleset loaded:", rulePath)
272 }
273
274 // set YARA scan flags
275 if args.Fast {
276 scanFlags = yara.ScanFlags(yara.ScanFlagsFastMode)
277 } else {
278 scanFlags = yara.ScanFlags(0)
279 }
280
281 // check if requested threads count is not greater than YARA's MAX_THREADS
282 if args.Workers > YaraMaxThreads {
283 log.Printf("[WARNING] workers count too high, using %d instead of %d\n", YaraMaxThreads, args.Workers)
284 args.Workers = YaraMaxThreads
285 }
286
287 // scan target
288 if f, err := os.Stat(args.Positional.Target); os.IsNotExist(err) {
289 handleError(err, true)
290 } else {
291 if args.Verbose {
292 log.Println("[DEBUG] scan workers:", args.Workers)
293 log.Println("[DEBUG] target:", args.Positional.Target)
294 }
295 if f.IsDir() { // parallelized folder scan
296 // create communication channels
297 targets := make(chan string)
298 results := make(chan map[string][]yara.MatchRule)
299
300 // rate limit
301 var tickerRate time.Duration
302 if args.RateLimit == 0 {
303 tickerRate = time.Nanosecond
304 } else {
305 tickerRate = time.Second / time.Duration(args.RateLimit)
306 }
307 ticker := time.Tick(tickerRate)
308 if args.Verbose {
309 log.Println("[DEBUG] delay between fs ops:", tickerRate.String())
310 }
311
312 // start consumers and producer workers
313 for w := 1; w <= args.Workers; w++ {
314 go processFiles(rules, targets, results, ticker)
315 }
316 go scanDir(args.Positional.Target, targets, ticker)
317
318 // read results
319 matchCount := make(map[string]int)
320 var keepListing bool
321 var countedDangerousMatch bool
322 for result := range results {
323 for target, matchedSigs := range result {
324 keepListing = true
325 matchCount[target] = 0
326 countedDangerousMatch = false
327 for _, sig := range matchedSigs {
328 matchCount[target] += DangerousMatchWeight
329 if !countedDangerousMatch {
330 if _, exists := dangerousMatches[sig.Rule]; exists {
331 matchCount[target]++
332 }
333 countedDangerousMatch = true
334 }
335 if keepListing {
336 log.Printf("[WARNING] match found: %s (%s)\n", target, sig.Rule)
337 if !args.ShowAll {
338 keepListing = false
339 }
340 }
341 }
342 }
343 }
344 for target, count := range matchCount {
345 if count >= DangerousMinScore {
346 log.Println("[WARNING] dangerous file found:", target)
347 }
348 }
349 } else { // single file mode
350 scannedFilesCount++
351 var matches yara.MatchRules
352 scanner := makeScanner(rules)
353 err := scanner.SetCallback(&matches).ScanFile(args.Positional.Target)
354 handleError(err, true)
355 for _, match := range matches {
356 log.Println("[WARNING] match found:", args.Positional.Target, match.Rule)
357 if args.Verbose {
358 for _, matchString := range match.Strings {
359 log.Printf("[DEBUG] match string for %s: 0x%x:%s: %s\n", args.Positional.Target, matchString.Offset, matchString.Name, matchString.Data)
360 }
361 }
362 if !args.ShowAll {
363 break
364 }
365 }
366 }
367 if args.Verbose {
368 endTime := time.Now()
369 log.Printf("[DEBUG] scanned %d files in %s\n", scannedFilesCount, endTime.Sub(startTime).String())
370 }
371 }
372}
diff --git a/php-malware-finder/tests.sh b/php-malware-finder/tests.sh
index f53097d..f8c5109 100755
--- a/php-malware-finder/tests.sh
+++ b/php-malware-finder/tests.sh
@@ -7,7 +7,7 @@ type yara 2>/dev/null 1>&2 || (echo "[-] Please make sure that yara is installed
7 7
8CPT=0 8CPT=0
9run_test(){ 9run_test(){
10 NB_DETECTED=$(${PMF} -v "$SAMPLES"/"$1" | grep -c "$2" 2>/dev/null) 10 NB_DETECTED=$(${PMF} -v -a "$SAMPLES"/"$1" 2>&1 | grep -c "$2" 2>/dev/null)
11 11
12 if [[ "$NB_DETECTED" != 1 ]]; then 12 if [[ "$NB_DETECTED" != 1 ]]; then
13 echo "[-] $2 was not detected in $1, sorry" 13 echo "[-] $2 was not detected in $1, sorry"