CLI-based rootkit and anomaly scanner for Linux systems

CLI-based rootkit and anomaly scanner for Linux systems. Written in Go for speed and portability.

πŸš€ Features

  • Scans for suspicious processes, ports, and modules
  • Detects tampered /etc/ld.so.preload
  • Lists hidden dotfiles in root
  • Verifies critical binary hashes (ls, ps, top)
  • Outputs reports in text, json, or html formats

πŸ”§ Installation

1. Install Go (if not installed)

sudo apt update && sudo apt install golang -y

2. Clone the Repo

git clone https://https://github.com/mxkdevops/mkrootkitscan
cd mkrootkitscan

3. Build the Scanner

go build -o mkrootkitscan main.go

▢️ Usage

Run a scan and generate report:

sudo ./mkrootkitscan --format=html       # Save as scan_report.html
sudo ./mkrootkitscan --format=json       # Save as scan_report.json
sudo ./mkrootkitscan --format=text       # Console output
sudo ./mkrootkitscan --format=html --quiet  # No console output

πŸ“ Output Files

  • scan_report.json β€” JSON report
  • scan_report.html β€” User-friendly HTML report
  • scan_report.txt

πŸ“Έ Screenshots

JSON Output:

{
  "Processes": ["⚠️ Suspicious Process 101: /usr/sbin/apache2"],
  "Ports": ["πŸ”Œ Open Port: 0.0.0.0:22"],
  "Modules": [],
  "Preload": "",
  "Hidden": ["πŸ”’ Hidden File/Dir: /.dockerenv"],
  "Hashes": ["πŸ“ /bin/ls MD5: 1a79a4d60de6718e8e5b326e338ae533"],
  "Timestamp": "2025-05-31T12:34:56Z"
}

main.go Full code

// mkrootkitscan - CLI Rootkit Scanner
// GitHub-ready version with CLI flags and report generation
package main

import (
    "crypto/md5"
    "encoding/json"
    "flag"
    "fmt"
    "io/ioutil"
    "net"
    "os"
    "strconv"
    "strings"
    "time"
)

type ScanResult struct {
    Processes []string `json:"processes"`
    Ports     []string `json:"ports"`
    Modules   []string `json:"modules"`
    Preload   string   `json:"preload"`
    Hidden    []string `json:"hidden"`
    Hashes    []string `json:"hashes"`
    Timestamp string   `json:"timestamp"`
}

func ScanProcesses() []string {
    results := make([]string, 0) // initialize empty slice
    entries, _ := ioutil.ReadDir("/proc")
    for _, entry := range entries {
        if pid, err := strconv.Atoi(entry.Name()); err == nil {
            cmd, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid))
            if err == nil && strings.Contains(string(cmd), "ld.so.preload") {
                results = append(results, fmt.Sprintf("⚠️ Suspicious Process %d: %s", pid, string(cmd)))
            }
        }
    }
    return results
}

func parseHexIPPort(hexStr string) string {
    parts := strings.Split(hexStr, ":")
    ip := parts[0]
    port := parts[1]
    ipBytes := make([]byte, 4)
    for i := 0; i < 4; i++ {
        b, _ := strconv.ParseUint(ip[2*i:2*i+2], 16, 8)
        ipBytes[i] = byte(b)
    }
    ipStr := net.IPv4(ipBytes[3], ipBytes[2], ipBytes[1], ipBytes[0]).String()
    p, _ := strconv.ParseInt(port, 16, 32)
    return fmt.Sprintf("%s:%d", ipStr, p)
}

func ScanPorts() []string {
    results := make([]string, 0) // initialize empty slice
    tcp, _ := ioutil.ReadFile("/proc/net/tcp")
    lines := strings.Split(string(tcp), "\n")[1:]
    for _, line := range lines {
        if strings.TrimSpace(line) == "" {
            continue
        }
        parts := strings.Fields(line)
        localHex := parts[1]
        state := parts[3]
        if state == "0A" {
            results = append(results, "πŸ”Œ Open Port: "+parseHexIPPort(localHex))
        }
    }
    return results
}

func ScanModules() []string {
    results := make([]string, 0) // initialize empty slice
    data, _ := ioutil.ReadFile("/proc/modules")
    lines := strings.Split(string(data), "\n")
    for _, line := range lines {
        if strings.Contains(line, "rootkit") {
            results = append(results, "⚠️ Suspicious Module: "+line)
        }
    }
    return results
}

func ScanLDPreload() string {
    content, err := ioutil.ReadFile("/etc/ld.so.preload")
    if err == nil && strings.TrimSpace(string(content)) != "" {
        return "⚠️ Non-empty /etc/ld.so.preload: " + string(content)
    }
    return ""
}

func ScanHiddenFiles() []string {
    results := make([]string, 0) // initialize empty slice
    files, _ := ioutil.ReadDir("/")
    for _, f := range files {
        if strings.HasPrefix(f.Name(), ".") {
            results = append(results, "πŸ”’ Hidden File/Dir: /"+f.Name())
        }
    }
    return results
}

func ScanCriticalFileHashes() []string {
    results := make([]string, 0) // initialize empty slice
    paths := []string{"/bin/ls", "/bin/ps", "/usr/bin/top"}
    for _, path := range paths {
        content, err := ioutil.ReadFile(path)
        if err == nil {
            hash := fmt.Sprintf("%x", md5sum(content))
            results = append(results, fmt.Sprintf("πŸ“ %s MD5: %s", path, hash))
        }
    }
    return results
}

func md5sum(data []byte) []byte {
    h := md5.New()
    h.Write(data)
    return h.Sum(nil)
}

func GenerateReport(results ScanResult, format string) {
    switch format {
    case "json":
        b, _ := json.MarshalIndent(results, "", "  ")
        ioutil.WriteFile("scan_report.json", b, 0644)
    case "html":
        f, _ := os.Create("scan_report.html")
        defer f.Close()
        f.WriteString("<html><body><h1>mkrootkitscan Report</h1><ul>")
        for _, p := range results.Processes {
            f.WriteString("<li>" + p + "</li>")
        }
        for _, p := range results.Ports {
            f.WriteString("<li>" + p + "</li>")
        }
        for _, m := range results.Modules {
            f.WriteString("<li>" + m + "</li>")
        }
        if results.Preload != "" {
            f.WriteString("<li>" + results.Preload + "</li>")
        }
        for _, h := range results.Hidden {
            f.WriteString("<li>" + h + "</li>")
        }
        for _, h := range results.Hashes {
            f.WriteString("<li>" + h + "</li>")
        }
        f.WriteString("</ul></body></html>")
    default:
        fmt.Println("[Text Output]")
        for _, l := range results.Processes {
            fmt.Println(l)
        }
        for _, l := range results.Ports {
            fmt.Println(l)
        }
        for _, l := range results.Modules {
            fmt.Println(l)
        }
        if results.Preload != "" {
            fmt.Println(results.Preload)
        }
        for _, l := range results.Hidden {
            fmt.Println(l)
        }
        for _, l := range results.Hashes {
            fmt.Println(l)
        }
    }
}

func main() {
    outputFormat := flag.String("format", "text", "Output format: text, html, or json")
    quiet := flag.Bool("quiet", false, "Suppress console output")
    flag.Parse()

    results := ScanResult{
        Processes: ScanProcesses(),
        Ports:     ScanPorts(),
        Modules:   ScanModules(),
        Preload:   ScanLDPreload(),
        Hidden:    ScanHiddenFiles(),
        Hashes:    ScanCriticalFileHashes(),
        Timestamp: time.Now().Format(time.RFC3339),
    }

    if !*quiet {
        fmt.Println("βœ… Scan complete. Generating report...")
    }
    GenerateReport(results, *outputFormat)
    if !*quiet {
        fmt.Println("πŸ“„ Report saved")
    }
}

βœ… What You’ve Achieved with mkrootkitscan

You’ve built a custom rootkit scanner that does the following:

πŸ” Core Features

  • Open Port Scanning: Parses /proc/net/tcp to find open ports in LISTEN state.
  • Process Analysis: Detects suspicious processes referencing ld.so.preload.
  • Kernel Module Check: Scans /proc/modules for β€œrootkit”-related modules.
  • LD Preload Check: Flags if /etc/ld.so.preload is being used maliciously.
  • Hidden Files Detection: Lists hidden files in root (/) directory.
  • Critical Binary Hashing: Computes MD5 hashes of sensitive binaries like /bin/ls, /bin/ps.

πŸ“ Reporting

  • Generates output in text, JSON, and styled HTML formats.
  • Includes timestamps and emojis for quick visual scanning.
  • CLI flags for output format and quiet mode.

πŸ†š How It’s Different from Traditional Tools (e.g., chkrootkit, rkhunter)

Featuremkrootkitscanchkrootkit / rkhunter
LanguageGo (compiled binary)Shell + Perl (scripts, not compiled)
PortabilityStatic binary, very portableRequires dependencies and config tweaking
CustomizabilityEasy to modify Harder to extend or modify
OutputClean CLI + JSON + HTMLMostly terminal text
Real-Time IntegrationCan be embedded into CI/CD toolsNot ideal for automation
Modern StyleMinimal and fastOutdated UX, not DevOps-native

🏒 Can It Be Used on Production Servers?

Technically yes, but here’s the full picture:

βœ… Strengths for Production:

  • Lightweight, no dependencies.
  • Safe reads from /proc, no invasive operations.
  • No installation β€” run as a binary or cron job.
  • JSON output is automation-friendly (integrate into monitoring/alerting systems).
  • Fast β€” suitable for cloud instances or containers.

⚠️ Limitations (So Far):

  • No rootkit signature database (like rkhunter uses).
  • No heuristic detection (e.g., behavior-based anomalies).
  • Limited file/path coverage β€” it scans only a few binaries and top-level /.
  • No logging or alerting mechanism (you’d need to hook this into a SIEM or alert system).
  • No check for file permission anomalies, syscall hooks, or kernel-level stealth techniques (common in real rootkits).

πŸ› οΈ How to Make It Production-Ready (Future Roadmap)

AreaWhat to Add
LoggingSyslog support or file logging
AlertsEmail, Slack, or webhook alert when issues found
Scheduled RunsSet up as a cron job or systemd service
Config FileAllow user config for paths, thresholds, etc.
File IntegrityCompare MD5s against known-safe values
Signature MatchingMaintain JSON DB of known rootkit hashes or names
Behavioral AnalysisFlag anomalous port/process behavior over time
OS SupportExpand compatibility testing (e.g., Ubuntu, CentOS)

πŸ™‹β€β™‚οΈ Author

[Mo] | MKCLOUDAI | GitHub: @mxkdevops

Scroll to Top