Created
June 26, 2013 10:39
-
-
Save charlie5/5866475 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/ruby1.8 -w | |
# Try to find running processes using different methods, and report | |
# processes found through some means but not through others. | |
# | |
# Exit code 2 means something fishy was detected. | |
# | |
# Exit code 1 means something went wrong. | |
# Copyright 2009 by Johan Walles, johan.walles@gmail.com | |
# | |
# This program is free software: you can redistribute it and/or modify | |
# it under the terms of the GNU General Public License as published by | |
# the Free Software Foundation, either version 3 of the License, or | |
# (at your option) any later version. | |
# | |
# This program is distributed in the hope that it will be useful, | |
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
# GNU General Public License for more details. | |
# | |
# You should have received a copy of the GNU General Public License | |
# along with this program. If not, see <http://www.gnu.org/licenses/>. | |
require 'set' | |
require 'dl/import' | |
require 'dl/struct' | |
# Support for libc functions not covered by the standard Ruby | |
# libraries | |
module LibC | |
extend DL::Importable | |
dlload "libc.so.6" | |
# PID scanning functions | |
extern "int getsid(int)" | |
extern "int sched_getscheduler(int)" | |
extern "int sched_getparam(int, void*)" | |
extern "int sched_rr_get_interval(int, void*)" | |
extern "int sched_getaffinity(int, int, void*)" | |
# We want to ask sysinfo about the number of active PIDs. This | |
# result struct has been taken from the sysinfo(2) Linux man page. | |
SysInfoData = struct [ | |
"long uptime", | |
"long loads[3]", | |
"long totalram", | |
"long freeram", | |
"long sharedram", | |
"long bufferram", | |
"long totalswap", | |
"long freeswap", | |
"short procs", | |
"long padding[42]" | |
] | |
extern "int sysinfo(void *)" | |
end | |
# A piece of scratch memory where system calls can fill in | |
# information. What's written here is not interesting, it's just that | |
# some of the PID scanning functions need a memory area to write into. | |
scratch = "\0" * 1024 | |
# Make sure our scratch buffer is large enough for sched_getaffinity() | |
while LibC.sched_getaffinity(0, scratch.length, scratch) == -1 | |
scratch = "\0" * (scratch.length * 2) | |
end | |
$proc_parent_pids = nil | |
$proc_tasks = nil | |
$ps_pids = nil | |
def setup | |
# List all parent processes pointed out by anybody in /proc | |
$proc_parent_pids = Set.new | |
proc_dir = Dir.new("/proc") | |
proc_dir.each do |proc_entry| | |
next unless File.directory?("/proc/" + proc_entry) | |
next unless proc_entry =~ /^[0-9]+$/ | |
status_file = File.new("/proc/#{proc_entry}/status") | |
begin | |
status_file.each_line do |line| | |
line.chomp! | |
next unless line =~ /^ppid:\s+([0-9]+)$/i | |
ppid = $1.to_i | |
$proc_parent_pids << ppid | |
break | |
end | |
ensure | |
status_file.close | |
end | |
end | |
proc_dir.close | |
# List all thread pids in /proc | |
$proc_tasks = {} | |
proc_dir = Dir.new("/proc") | |
proc_dir.each do |proc_entry| | |
next unless File.directory?("/proc/" + proc_entry) | |
next unless proc_entry =~ /^[0-9]+$/ | |
task_dir = Dir.new("/proc/#{proc_entry}/task") | |
begin | |
task_dir.each do |task_pid| | |
next unless task_pid =~ /^[0-9]+$/ | |
# "true" means we *have* an executable | |
exe = true | |
begin | |
# Store the *name* of the executable | |
exe = File.readlink("/proc/#{proc_entry}/task/#{task_pid}/exe") | |
rescue Errno::EACCES, Errno::ENOENT | |
# This block intentionally left blank | |
end | |
$proc_tasks[task_pid.to_i] = exe | |
end | |
ensure | |
task_dir.close | |
end | |
end | |
proc_dir.close | |
# List all PIDs displayed by ps | |
$ps_pids = {} | |
$ps_pids.default = false | |
ps_stdout = IO.popen("ps axHo lwp,cmd", "r") | |
ps_pid = ps_stdout.pid | |
ps_stdout.each_line do |ps_line| | |
ps_line.chomp! | |
next unless ps_line =~ /^\s*([0-9]+)\s+([^ ]+).*$/ | |
pid = $1.to_i | |
exe = $2 | |
$ps_pids[pid] = $2 | |
end | |
ps_stdout.close | |
# Remove the ps process itself from our pid list | |
$ps_pids.delete ps_pid | |
end | |
# This array contains named PID detectors. Given a PID to examine they | |
# can say: | |
# * true (it exists) | |
# * "some string" (it exists, and here is its name) | |
# * false (it doesn't exist) | |
# * nil (don't know) | |
$pid_detectors = [ | |
["ps", proc { |pid| | |
# Does "ps" list this pid? | |
$ps_pids[pid] | |
}], | |
["/proc", proc { |pid| | |
# Is there a /proc entry for this pid? | |
unless File.directory?("/proc/#{pid}") | |
break | |
end | |
begin | |
File.readlink("/proc/#{pid}/exe") | |
rescue Errno::ENOENT, Errno::EACCES | |
# Process exists, but we cannot read its exe symlink | |
true | |
end | |
}], | |
["/proc tasks", proc { |pid| | |
# Is there a /proc/1234/tasks/pid directory for | |
# this pid? | |
$proc_tasks[pid] | |
}], | |
["/proc parent", proc { |pid| | |
# Does any /proc entry point this pid out as a | |
# parent pid? | |
if $proc_parent_pids.include? pid | |
true | |
else | |
nil | |
end | |
}], | |
["getsid()", proc { |pid| | |
LibC.getsid(pid) != -1 | |
}], | |
["getpgid()", proc { |pid| | |
exists = true | |
begin | |
Process.getpgid(pid) | |
rescue | |
exists = false | |
end | |
exists | |
}], | |
["getpriority()", proc { |pid| | |
exists = true | |
begin | |
Process.getpriority(Process::PRIO_PROCESS, pid) | |
rescue | |
exists = false | |
end | |
exists | |
}], | |
["sched_getparam()", proc { |pid| | |
LibC.sched_getparam(pid, scratch) != -1 | |
}], | |
["sched_getaffinity()", proc { |pid| | |
LibC.sched_getaffinity(pid, scratch.length, scratch) != -1 | |
}], | |
["sched_getscheduler()", proc { |pid| | |
LibC.sched_getscheduler(pid) != -1 | |
}], | |
["sched_rr_get_interval()", proc { |pid| | |
LibC.sched_rr_get_interval(pid, scratch) != -1 | |
}] | |
] | |
found_something = false | |
# Scan PIDs and report those found by some means but not others | |
# | |
# Returns a map of pids->warning strings | |
def get_suspicious_pids(pids_to_scan = nil) | |
if pids_to_scan == nil | |
pid_max = File.new("/proc/sys/kernel/pid_max").gets.to_i | |
pids_to_scan = (1..pid_max) | |
end | |
return_me = Hash.new | |
pids_to_scan.each do |pid| | |
pid_exists = {} | |
$pid_detectors.each do |pid_detector| | |
detector_name = pid_detector[0] | |
detector_proc = pid_detector[1] | |
pid_exists[detector_name] = detector_proc.call(pid) | |
end | |
# Is there consensus about the existence of this process? | |
suspicious = false | |
existence_consensus = nil | |
pid_exists.values.each do |existence| | |
# Always over-write "don't know" | |
existence_consensus = existence if existence_consensus == nil | |
# This one is undecisive, skip it | |
next if existence == nil | |
# Does the result of this test match the consensus? | |
if existence_consensus == false | |
unless existence == false | |
suspicious = true | |
break | |
end | |
else | |
# Anything but "false" is considered to be true, can be a string | |
# with a process name in it for example | |
if existence == false | |
suspicious = true | |
break | |
end | |
end | |
end | |
if suspicious | |
# Put output in a string and add it to the return result | |
message = "Suspicious PID #{pid}:" | |
$pid_detectors.each do |detector| | |
detector_name = detector[0] | |
detector_result = pid_exists[detector_name] | |
next if detector_result == nil | |
description = "" | |
description = | |
" (\"#{detector_result}\")" if detector_result.class == String | |
message += | |
sprintf("\n %s %s%s", | |
detector_result ? "Seen by" : "Not seen by", | |
detector_name, | |
description) | |
end | |
return_me[pid] = message | |
end | |
end | |
return return_me | |
end | |
## | |
## Main program starts here | |
## | |
puts "Scanning for hidden processes..." | |
setup | |
# Verify PID count between ps and sysinfo() | |
sysinfo = LibC::SysInfoData.malloc | |
if LibC.sysinfo(sysinfo) == -1 | |
STDERR.puts "Error: failed calling sysinfo()" | |
exit 1 | |
end | |
if sysinfo.procs != $ps_pids.size | |
$stderr.puts "ps and sysinfo() process count mismatch:" | |
$stderr.puts " ps: #{$ps_pids.size} processes" | |
$stderr.puts " sysinfo(): #{sysinfo.procs} processes" | |
found_something = true | |
end | |
suspicious_pids = get_suspicious_pids | |
unless suspicious_pids.empty? | |
# Filter out false positives by testing all positives again. False | |
# positives occur when we race with processes starting up or | |
# shutting down. | |
setup | |
still_suspicious_pids = get_suspicious_pids(suspicious_pids.keys) | |
still_suspicious_pids.keys.sort.each do |still_suspicious_pid| | |
warning_text = suspicious_pids[still_suspicious_pid] | |
next unless warning_text | |
found_something = true | |
$stderr.puts warning_text | |
end | |
end | |
if found_something | |
exit 2 | |
else | |
puts "No hidden processes found!" | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment