patch_finder 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +3 -0
- data/README.md +25 -0
- data/Rakefile +6 -0
- data/bin/msu_finder +126 -0
- data/docs/bin/msu_finder.txt +38 -0
- data/lib/core/config.rb +19 -0
- data/lib/core/helper.rb +158 -0
- data/lib/core/thread_pool.rb +61 -0
- data/lib/engine/msu/constants.rb +37 -0
- data/lib/engine/msu/google.rb +141 -0
- data/lib/engine/msu/technet.rb +115 -0
- data/lib/msu.rb +234 -0
- data/patch_finder.gemspec +22 -0
- data/tools/extract_msu.bat +47 -0
- data/tools/list_dll.rb +220 -0
- metadata +102 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c3c8d250a7aa94b35b8f5aec0c944eac2c0a3935
|
4
|
+
data.tar.gz: 9c92cbd76ea88c27e15fcbecf668a88a645e53bf
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2a55fda811184d47d0ae322d70a5b3431192f3fe9c93ad8588a48031ecd95af6a97f9499caa0d194b4f22812a4f6d6f5e908081d2db36e2c79ed08d95f28b32d
|
7
|
+
data.tar.gz: bb45dcb3fed06f018861ae979f0dc362e8a838c825d3bbe1866ef86003d4244e764f9ed01c238810052c3286478fa249acb85faab5d5dbce6f03a56544f0ebc0
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# PatchFinder
|
2
|
+
|
3
|
+
PatchFinder is a toolkit to find software patches.
|
4
|
+
|
5
|
+
All download clients can be found in the bin directory. Each client is meant for a specific
|
6
|
+
type of patch. For example: msu_finder is used to find Microsoft updates (.msu).
|
7
|
+
|
8
|
+
For other tools that might be useful to examine the patches, please see the tools directory.
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
To install PatchFinder:
|
13
|
+
|
14
|
+
```
|
15
|
+
$ gem install patch_finder
|
16
|
+
```
|
17
|
+
|
18
|
+
## Usage
|
19
|
+
|
20
|
+
Each patch finding client can be found in the bin directory. To learn how to use one, please refer
|
21
|
+
to its ```-h``` option, or the [wiki page](https://github.com/wchen-r7/Patch-Finder/wiki).
|
22
|
+
|
23
|
+
## License
|
24
|
+
|
25
|
+
[BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause)
|
data/Rakefile
ADDED
data/bin/msu_finder
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
##
|
4
|
+
#
|
5
|
+
# This tool allows you to collect Microsoft patches.
|
6
|
+
# Once you have downloaded all the .msu patches, you can use tools/extract_msu.bat to
|
7
|
+
# automatically extract them.
|
8
|
+
#
|
9
|
+
##
|
10
|
+
|
11
|
+
lib = File.expand_path('../../lib', __FILE__)
|
12
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
13
|
+
|
14
|
+
require 'core/helper'
|
15
|
+
require 'core/config'
|
16
|
+
require 'optparse'
|
17
|
+
require 'msu'
|
18
|
+
|
19
|
+
class PatchFinderBin
|
20
|
+
|
21
|
+
include PatchFinder::Helper
|
22
|
+
|
23
|
+
attr_reader :args
|
24
|
+
|
25
|
+
def banner
|
26
|
+
@banner ||= lambda {
|
27
|
+
doc_path = File.join(PatchFinder::Config.doc_directory, 'msu_finder.txt')
|
28
|
+
read_file(doc_path)
|
29
|
+
}.call
|
30
|
+
end
|
31
|
+
|
32
|
+
def get_parsed_options
|
33
|
+
options = {}
|
34
|
+
|
35
|
+
parser = OptionParser.new do |opt|
|
36
|
+
opt.banner = banner
|
37
|
+
opt.separator ''
|
38
|
+
opt.separator 'Specific options:'
|
39
|
+
|
40
|
+
opt.on('-q', '--query <keyword>', 'Find advisories including this keyword') do |v|
|
41
|
+
options[:keyword] = v
|
42
|
+
end
|
43
|
+
|
44
|
+
opt.on('-s', '--search-engine <engine>', '(Optional) The type of search engine to use (Technet or Google). Default: Technet') do |v|
|
45
|
+
case v.to_s
|
46
|
+
when /^google$/i
|
47
|
+
options[:search_engine] = :google
|
48
|
+
when /^technet$/i
|
49
|
+
options[:search_engine] = :technet
|
50
|
+
else
|
51
|
+
fail OptionParser::InvalidOption, "Invalid search engine: #{v}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
opt.on('-r', '--regex <string>', '(Optional) Specify what type of links you want') do |v|
|
56
|
+
options[:regex] = v
|
57
|
+
end
|
58
|
+
|
59
|
+
opt.on('--apikey <key>', '(Optional) Google API key.') do |v|
|
60
|
+
options[:google_api_key] = v
|
61
|
+
end
|
62
|
+
|
63
|
+
opt.on('--cx <id>', '(Optional) Google search engine ID.') do |v|
|
64
|
+
options[:google_search_engine_id] = v
|
65
|
+
end
|
66
|
+
|
67
|
+
opt.on('-d', '--dir <string>', '(Optional) The directory to save the patches') do |v|
|
68
|
+
unless File.directory?(v)
|
69
|
+
fail OptionParser::InvalidOption, "Directory not found: #{v}"
|
70
|
+
end
|
71
|
+
|
72
|
+
options[:destdir] = v
|
73
|
+
end
|
74
|
+
|
75
|
+
opt.on_tail('-h', '--help', 'Show this message') do
|
76
|
+
$stderr.puts opt
|
77
|
+
exit
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
parser.parse!
|
82
|
+
|
83
|
+
if options.empty?
|
84
|
+
fail OptionParser::MissingArgument, 'No options set, try -h for usage'
|
85
|
+
elsif options[:keyword].nil? || options[:keyword].empty?
|
86
|
+
fail OptionParser::MissingArgument, '-q is required'
|
87
|
+
end
|
88
|
+
|
89
|
+
unless options[:search_engine]
|
90
|
+
options[:search_engine] = :technet
|
91
|
+
end
|
92
|
+
|
93
|
+
if options[:search_engine] == :google
|
94
|
+
if options[:google_api_key].nil? || options[:google_search_engine_id].empty?
|
95
|
+
fail OptionParser::MissingArgument, 'No API key set for Google'
|
96
|
+
elsif options[:google_search_engine_id].nil? || options[:google_search_engine_id].empty?
|
97
|
+
fail OptionParser::MissingArgument, 'No search engine ID set for Google'
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
options
|
102
|
+
end
|
103
|
+
|
104
|
+
def initialize
|
105
|
+
@args = get_parsed_options
|
106
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
107
|
+
print_error(e.message)
|
108
|
+
exit
|
109
|
+
end
|
110
|
+
|
111
|
+
def main
|
112
|
+
cli = PatchFinder::MSU.new
|
113
|
+
links = cli.find_msu_download_links(args)
|
114
|
+
if args[:destdir]
|
115
|
+
print_status("Download links found: #{links.length}")
|
116
|
+
print_status('Downloading files, please wait...')
|
117
|
+
download_files(links, args[:destdir])
|
118
|
+
else
|
119
|
+
print_status('Download links found:')
|
120
|
+
print_line(links * "\n")
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
bin = PatchFinderBin.new
|
126
|
+
bin.main
|
@@ -0,0 +1,38 @@
|
|
1
|
+
Usage: msu_finder [options]
|
2
|
+
|
3
|
+
The following example will download all IE updates to directory /tmp:
|
4
|
+
msu_finder -q "Internet Explorer" -d /tmp/
|
5
|
+
|
6
|
+
Searching advisories via Technet:
|
7
|
+
|
8
|
+
When you submit a query, the Technet search engine will first look it up from a product list,
|
9
|
+
and then return all the advisories that include the keyword you are looking for. If there's
|
10
|
+
no match from the product list, then the script will try a generic search. The generic method
|
11
|
+
also means you can search by MSB, KB, or even the CVE number.
|
12
|
+
|
13
|
+
Searching advisories via Google:
|
14
|
+
|
15
|
+
Searching via Google requires an API key and an Search Engine ID from Google. To obtain these,
|
16
|
+
make sure you have a Google account (such as Gmail), and then do the following:
|
17
|
+
|
18
|
+
1. Go to Google Developer's Console
|
19
|
+
1. Enable Custom Search API
|
20
|
+
2. Create a browser type credential. The credential is the API key.
|
21
|
+
2. Go to Custom Search
|
22
|
+
1. Create a new search engine
|
23
|
+
2. Under Sites to Search, set: technet.microsoft.com
|
24
|
+
3. In your search site, get the Search Engine ID under the Basics tab.
|
25
|
+
|
26
|
+
By default, Google has a quota limit of 1000 queries per day. You can raise this limit with
|
27
|
+
a fee.
|
28
|
+
|
29
|
+
The way this tool uses Google to find advisories is the same as doing the following manually:
|
30
|
+
[Query] site:technet.microsoft.com intitle:"Microsoft Security Bulletin" -"Microsoft Security Bulletin Summary"
|
31
|
+
|
32
|
+
Dryrun mode:
|
33
|
+
Without the -d flag (destination), the tool will not download the patches, it will just find you
|
34
|
+
the download links.
|
35
|
+
|
36
|
+
Patch Extraction:
|
37
|
+
After downloading the patch, you can use the extract_msu.bat tool to automatically extract
|
38
|
+
Microsoft patches.
|
data/lib/core/config.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module PatchFinder
|
2
|
+
module Config
|
3
|
+
|
4
|
+
# Returns the doc directory.
|
5
|
+
#
|
6
|
+
# @return [String]
|
7
|
+
def self.doc_directory
|
8
|
+
@doc_directory ||= File.expand_path(File.join(root_directory, '..', 'docs', 'bin'))
|
9
|
+
end
|
10
|
+
|
11
|
+
# Returns the root directory.
|
12
|
+
#
|
13
|
+
# @return [String]
|
14
|
+
def self.root_directory
|
15
|
+
@root_directory ||= File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
data/lib/core/helper.rb
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
module PatchFinder
|
4
|
+
module Helper
|
5
|
+
|
6
|
+
class PatchFinderException < RuntimeError; end
|
7
|
+
|
8
|
+
# Prints a debug message.
|
9
|
+
#
|
10
|
+
# @return [void]
|
11
|
+
def print_debug(msg = '')
|
12
|
+
$stderr.puts "[DEBUG] #{msg}"
|
13
|
+
end
|
14
|
+
|
15
|
+
# Prints a status message.
|
16
|
+
#
|
17
|
+
# @return [void]
|
18
|
+
def print_status(msg = '')
|
19
|
+
$stderr.puts "[*] #{msg}"
|
20
|
+
end
|
21
|
+
|
22
|
+
# Prints an error message.
|
23
|
+
#
|
24
|
+
# @return [void]
|
25
|
+
def print_error(msg = '')
|
26
|
+
$stderr.puts "[ERROR] #{msg}"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Prints a message.
|
30
|
+
#
|
31
|
+
# @return [void]
|
32
|
+
def print_line(msg = '')
|
33
|
+
$stdout.puts msg
|
34
|
+
end
|
35
|
+
|
36
|
+
# Sends an HTTP request.
|
37
|
+
# @note If the request fails, it will try 3 times before
|
38
|
+
# passing/raising an exception.
|
39
|
+
#
|
40
|
+
# @param uri [String] URI.
|
41
|
+
# @param ssl [Boolean] Forces SSL option.
|
42
|
+
# @return [Net::HTTPResponse]
|
43
|
+
def send_http_get_request(uri, ssl = false)
|
44
|
+
attempts = 1
|
45
|
+
u = URI.parse(uri)
|
46
|
+
res = nil
|
47
|
+
ssl = u.scheme == 'https' ? true : false
|
48
|
+
|
49
|
+
begin
|
50
|
+
Net::HTTP.start(u.host, u.port, use_ssl: ssl) do |cli|
|
51
|
+
req = Net::HTTP::Get.new(normalize_uri(u.request_uri))
|
52
|
+
req['Host'] = u.host
|
53
|
+
req['Content-Type'] = 'application/x-www-form-urlencoded'
|
54
|
+
res = cli.request(req)
|
55
|
+
end
|
56
|
+
rescue Net::ReadTimeout, IOError, EOFError, Errno::ECONNRESET,
|
57
|
+
Errno::ECONNABORTED, Errno::EPIPE, Net::OpenTimeout,
|
58
|
+
Errno::ETIMEDOUT => e
|
59
|
+
if attempts < 3
|
60
|
+
sleep(5)
|
61
|
+
attempts += 1
|
62
|
+
retry
|
63
|
+
else
|
64
|
+
raise e
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
res
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns the content of a file.
|
72
|
+
#
|
73
|
+
# @param file_path [String] File to read.
|
74
|
+
# @return [String]
|
75
|
+
def read_file(file_path)
|
76
|
+
return nil unless File.exist?(file_path)
|
77
|
+
|
78
|
+
buf = ''
|
79
|
+
|
80
|
+
File.open(file_path, 'rb') do |f|
|
81
|
+
buf = f.read
|
82
|
+
end
|
83
|
+
|
84
|
+
buf
|
85
|
+
end
|
86
|
+
|
87
|
+
# Downloads a file to a local directory.
|
88
|
+
# @note When the file is saved, the file name will actually include a timestamp
|
89
|
+
# in this format: [original filename]_[timestamp].[ext] to avoid
|
90
|
+
# name collision.
|
91
|
+
#
|
92
|
+
# @param uri [String] URI (to download)
|
93
|
+
# @param dest_dir [String] The folder to save the file.
|
94
|
+
# Make sure this folder exists.
|
95
|
+
# @return [void]
|
96
|
+
def download_file(uri, dest_dir)
|
97
|
+
begin
|
98
|
+
u = URI.parse(uri)
|
99
|
+
fname, ext = File.basename(u.path).scan(/(.+)\.(.+)/).flatten
|
100
|
+
dest_file = File.join(dest_dir, "#{fname}_#{Time.now.to_i}.#{ext}")
|
101
|
+
res = send_http_get_request(uri)
|
102
|
+
rescue Net::ReadTimeout, IOError, EOFError, Errno::ECONNRESET,
|
103
|
+
Errno::ECONNABORTED, Errno::EPIPE, Net::OpenTimeout,
|
104
|
+
Errno::ETIMEDOUT => e
|
105
|
+
print_error("#{e.message}: #{uri}")
|
106
|
+
return
|
107
|
+
end
|
108
|
+
|
109
|
+
save_file(res.body, dest_file)
|
110
|
+
print_status("Download completed for #{uri}")
|
111
|
+
end
|
112
|
+
|
113
|
+
# Downloads multiple files to a local directory.
|
114
|
+
# @note 3 clients are used to download all the links.
|
115
|
+
#
|
116
|
+
# @param files [Array] Full URIs.
|
117
|
+
# @param dest_dir [String] The folder to save the files.
|
118
|
+
# Make sure this folder exists.
|
119
|
+
# @return pvoid
|
120
|
+
def download_files(files, dest_dir)
|
121
|
+
pool = PatchFinder::ThreadPool.new(3)
|
122
|
+
|
123
|
+
files.each do |f|
|
124
|
+
pool.schedule do
|
125
|
+
download_file(f, dest_dir)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
pool.shutdown
|
130
|
+
|
131
|
+
sleep(0.5) until pool.eop?
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
# Saves a file to a specific location.
|
137
|
+
#
|
138
|
+
# @param data [String]
|
139
|
+
# @param dest_file [String]
|
140
|
+
# @return [void]
|
141
|
+
def save_file(data, dest_file)
|
142
|
+
File.open(dest_file, 'wb') do |f|
|
143
|
+
f.write(data)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Returns the normalized URI by modifying the double slashes.
|
148
|
+
#
|
149
|
+
# @param strs [Array] URI path.
|
150
|
+
# @return [String]
|
151
|
+
def normalize_uri(*strs)
|
152
|
+
new_str = strs * '/'
|
153
|
+
new_str = new_str.gsub!('//', '/') while new_str.index('//')
|
154
|
+
new_str
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module PatchFinder
|
2
|
+
class ThreadPool
|
3
|
+
|
4
|
+
attr_accessor :mutex
|
5
|
+
|
6
|
+
# Initializes the pool.
|
7
|
+
#
|
8
|
+
# @param size [Fixnum] Max number of threads to be running at the same time.
|
9
|
+
# @return [void]
|
10
|
+
def initialize(size)
|
11
|
+
@size = size
|
12
|
+
@mutex = Mutex.new
|
13
|
+
@jobs = Queue.new
|
14
|
+
@pool = Array.new(@size) do |i|
|
15
|
+
Thread.new do
|
16
|
+
Thread.current[:id] = i
|
17
|
+
catch(:exit) do
|
18
|
+
loop do
|
19
|
+
job, args = @jobs.pop
|
20
|
+
job.call(*args)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Adds a job to the queue.
|
28
|
+
#
|
29
|
+
# @param args [Array] Arguments.
|
30
|
+
# @param block [Proc] Code.
|
31
|
+
def schedule(*args, &block)
|
32
|
+
@jobs << [block, args]
|
33
|
+
end
|
34
|
+
|
35
|
+
# Shuts down all the jobs.
|
36
|
+
#
|
37
|
+
# @return [void]
|
38
|
+
def shutdown
|
39
|
+
@size.times do
|
40
|
+
schedule { throw :exit }
|
41
|
+
end
|
42
|
+
|
43
|
+
@pool.map(&:join)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns whether there's anything in the queue left.
|
47
|
+
#
|
48
|
+
# @return [boolean]
|
49
|
+
def eop?
|
50
|
+
@jobs.empty?
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
=begin
|
57
|
+
|
58
|
+
Nothings that might not work so great in the pool:
|
59
|
+
* Timeout
|
60
|
+
|
61
|
+
=end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module PatchFinder
|
2
|
+
module Engine
|
3
|
+
module MSU
|
4
|
+
|
5
|
+
MICROSOFT = 'https://www.microsoft.com'
|
6
|
+
DOWNLOAD_MSFT = 'https://download.microsoft.com'
|
7
|
+
TECHNET = 'https://technet.microsoft.com'
|
8
|
+
|
9
|
+
# These pattern checks need to be in this order.
|
10
|
+
ADVISORY_PATTERNS = [
|
11
|
+
# This works from MS14-001 until the most recent
|
12
|
+
{
|
13
|
+
check: '//div[@id="mainBody"]//div//h2//div//span[contains(text(), "Affected Software")]',
|
14
|
+
pattern: '//div[@id="mainBody"]//div//div[@class="sectionblock"]//table//a'
|
15
|
+
},
|
16
|
+
# This works from ms03-040 until MS07-029
|
17
|
+
{
|
18
|
+
check: '//div[@id="mainBody"]//ul//li//a[contains(text(), "Download the update")]',
|
19
|
+
pattern: '//div[@id="mainBody"]//ul//li//a[contains(text(), "Download the update")]'
|
20
|
+
},
|
21
|
+
# This works from sometime until ms03-039
|
22
|
+
{
|
23
|
+
check: '//div[@id="mainBody"]//div//div[@class="sectionblock"]//p//strong[contains(text(), "Download locations")]',
|
24
|
+
pattern: '//div[@id="mainBody"]//div//div[@class="sectionblock"]//ul//li//a'
|
25
|
+
},
|
26
|
+
# This works from MS07-030 until MS13-106 (the last update in 2013)
|
27
|
+
# The check is pretty short so if it kicks in too early, it tends to create false positives.
|
28
|
+
# So it goes last.
|
29
|
+
{
|
30
|
+
check: '//div[@id="mainBody"]//p//strong[contains(text(), "Affected Software")]',
|
31
|
+
pattern: '//div[@id="mainBody"]//table//a'
|
32
|
+
}
|
33
|
+
]
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'core/helper'
|
3
|
+
require_relative 'constants'
|
4
|
+
|
5
|
+
module PatchFinder
|
6
|
+
module Engine
|
7
|
+
module MSU
|
8
|
+
|
9
|
+
class GoogleClientException < RuntimeError ; end
|
10
|
+
|
11
|
+
class Google
|
12
|
+
|
13
|
+
include Helper
|
14
|
+
|
15
|
+
GOOGLEAPIS = 'https://www.googleapis.com'
|
16
|
+
|
17
|
+
attr_reader :api_key
|
18
|
+
attr_reader :search_engine_id
|
19
|
+
|
20
|
+
# API Doc:
|
21
|
+
# https://developers.google.com/custom-search/json-api/v1/using_rest
|
22
|
+
# Known bug:
|
23
|
+
# * Always gets 20 MSB results. Weird.
|
24
|
+
|
25
|
+
# Initializes the Google API client.
|
26
|
+
#
|
27
|
+
# @param opts [Hash]
|
28
|
+
# @option opts [String] :api_key Google API key.
|
29
|
+
# @option opts [String] :search_engine_id Google Search engine ID.
|
30
|
+
def initialize(opts = {})
|
31
|
+
@api_key = opts[:api_key]
|
32
|
+
@search_engine_id = opts[:search_engine_id]
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the MSB (advisories) numbers for a search keyword.
|
36
|
+
#
|
37
|
+
# @param keyword [String]
|
38
|
+
# @return [Array]
|
39
|
+
def find_msb_numbers(keyword)
|
40
|
+
msb_numbers = []
|
41
|
+
next_starting_index = 1
|
42
|
+
search_opts = {
|
43
|
+
keyword: keyword,
|
44
|
+
starting_index: next_starting_index
|
45
|
+
}
|
46
|
+
|
47
|
+
begin
|
48
|
+
while
|
49
|
+
results = search(search_opts)
|
50
|
+
items = results['items']
|
51
|
+
items.each do |item|
|
52
|
+
title = item['title']
|
53
|
+
msb = title.scan(/Microsoft Security Bulletin (MS\d\d\-\d\d\d)/).flatten.first
|
54
|
+
msb_numbers << msb.downcase if msb
|
55
|
+
end
|
56
|
+
|
57
|
+
next_starting_index = get_next_index(results)
|
58
|
+
next_page = results['queries']['nextPage']
|
59
|
+
|
60
|
+
# Google API Documentation:
|
61
|
+
# https://developers.google.com/custom-search/json-api/v1/using_rest
|
62
|
+
# "This role is not present if the current results are the last page.
|
63
|
+
# Note: This API returns up to the first 100 results only."
|
64
|
+
break if next_page.nil? || next_starting_index > 100
|
65
|
+
end
|
66
|
+
rescue GoogleClientException => e
|
67
|
+
print_error(e.message)
|
68
|
+
return msb_numbers.uniq
|
69
|
+
end
|
70
|
+
|
71
|
+
msb_numbers.uniq
|
72
|
+
end
|
73
|
+
|
74
|
+
# Searches the Google API.
|
75
|
+
#
|
76
|
+
# @param opts [Hash]
|
77
|
+
# @option opts [Fixnum] :starting_index
|
78
|
+
# @option opts [String] :keyword
|
79
|
+
# @return [Hash] JSON
|
80
|
+
def search(opts = {})
|
81
|
+
starting_index = opts[:starting_index]
|
82
|
+
|
83
|
+
search_string = URI.escape([
|
84
|
+
opts[:keyword],
|
85
|
+
'intitle:"Microsoft Security Bulletin"',
|
86
|
+
'-"Microsoft Security Bulletin Summary"'
|
87
|
+
].join(' '))
|
88
|
+
|
89
|
+
req_str = "#{GOOGLEAPIS}/customsearch/v1?"
|
90
|
+
req_str << "key=#{api_key}&"
|
91
|
+
req_str << "cx=#{search_engine_id}&"
|
92
|
+
req_str << "q=#{search_string}&"
|
93
|
+
req_str << "start=#{starting_index.to_s}&"
|
94
|
+
req_str << 'num=10&'
|
95
|
+
req_str << 'c2coff=1'
|
96
|
+
|
97
|
+
res = send_http_get_request(req_str)
|
98
|
+
results = parse_results(res)
|
99
|
+
if starting_index == 1
|
100
|
+
print_status("Number of search results: #{get_total_results(results)}")
|
101
|
+
end
|
102
|
+
|
103
|
+
results
|
104
|
+
end
|
105
|
+
|
106
|
+
# Returns the string data to JSON.
|
107
|
+
#
|
108
|
+
# @raise [GoogleClientException] The Google Search API returns an error.
|
109
|
+
# @param res [Net::HTTPResponse]
|
110
|
+
def parse_results(res)
|
111
|
+
j = JSON.parse(res.body)
|
112
|
+
|
113
|
+
if j['error']
|
114
|
+
message = j['error']['errors'].first['message']
|
115
|
+
reason = j['error']['errors'].first['reason']
|
116
|
+
fail GoogleClientException, "Google Search failed. #{message} (#{reason})"
|
117
|
+
end
|
118
|
+
|
119
|
+
j
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns totalResults
|
123
|
+
#
|
124
|
+
# @param j [Hash] JSON response.
|
125
|
+
# @return [Fixnum]
|
126
|
+
def get_total_results(j)
|
127
|
+
j['queries']['request'].first['totalResults'].to_i
|
128
|
+
end
|
129
|
+
|
130
|
+
# Returns startIndex
|
131
|
+
#
|
132
|
+
# @param j [Hash] JSON response.
|
133
|
+
# @return [Fixnum]
|
134
|
+
def get_next_index(j)
|
135
|
+
j['queries']['nextPage'] ? j['queries']['nextPage'].first['startIndex'] : 0
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'nokogiri'
|
3
|
+
require 'core/helper'
|
4
|
+
require_relative 'constants'
|
5
|
+
|
6
|
+
module PatchFinder
|
7
|
+
module Engine
|
8
|
+
module MSU
|
9
|
+
class Technet
|
10
|
+
|
11
|
+
include Helper
|
12
|
+
|
13
|
+
# Initializes the Technet client.
|
14
|
+
#
|
15
|
+
# @return [void]
|
16
|
+
def initialize
|
17
|
+
@firstpage ||= lambda {
|
18
|
+
uri = '/en-us/security/bulletin/dn602597.aspx'
|
19
|
+
res = send_http_get_request("#{TECHNET}#{uri}")
|
20
|
+
return res.body
|
21
|
+
}.call
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns the MSB (advisories) numbers for a search keyword.
|
25
|
+
#
|
26
|
+
# @param keyword [String]
|
27
|
+
# @return [Array]
|
28
|
+
def find_msb_numbers(keyword)
|
29
|
+
product_list_matches = get_product_dropdown_list.select { |p| Regexp.new(keyword) === p[:option_text] }
|
30
|
+
if product_list_matches.empty?
|
31
|
+
print_status('No match from the product list, attempting a generic search')
|
32
|
+
search_by_keyword(keyword)
|
33
|
+
else
|
34
|
+
product_names = []
|
35
|
+
ids = []
|
36
|
+
product_list_matches.each do |e|
|
37
|
+
ids << e[:option_value]
|
38
|
+
product_names << e[:option_text]
|
39
|
+
end
|
40
|
+
print_status("Matches from the product list (#{product_names.length}): #{ product_names * ', ' }")
|
41
|
+
search_by_product_ids(ids)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Searches the Technet engine.
|
46
|
+
#
|
47
|
+
# @param keyword [String]
|
48
|
+
# @return [Hash] JSON
|
49
|
+
def search(keyword)
|
50
|
+
req_str = "#{TECHNET}/security/bulletin/services/GetBulletins?"
|
51
|
+
req_str << "searchText=#{keyword}&"
|
52
|
+
req_str << 'sortField=0&'
|
53
|
+
req_str << 'sortOrder=1&'
|
54
|
+
req_str << 'currentPage=1&'
|
55
|
+
req_str << 'bulletinsPerPage=9999&'
|
56
|
+
req_str << 'locale=en-us'
|
57
|
+
|
58
|
+
res = send_http_get_request(req_str)
|
59
|
+
begin
|
60
|
+
return JSON.parse(res.body)
|
61
|
+
rescue JSON::ParserError
|
62
|
+
end
|
63
|
+
|
64
|
+
{}
|
65
|
+
end
|
66
|
+
|
67
|
+
# Searches for the MSBs (advisories) based on product IDs (as search keywords)
|
68
|
+
#
|
69
|
+
# @param ids [Array]
|
70
|
+
# @return [Array]
|
71
|
+
def search_by_product_ids(ids)
|
72
|
+
msb_numbers = []
|
73
|
+
|
74
|
+
ids.each do |id|
|
75
|
+
j = search(id)
|
76
|
+
msb = j['b'].collect { |e| e['Id'] }.map { |e| e.downcase }
|
77
|
+
msb_numbers.concat(msb)
|
78
|
+
end
|
79
|
+
|
80
|
+
msb_numbers
|
81
|
+
end
|
82
|
+
|
83
|
+
# Searches for the MSBs (advisories) based on a keyword.
|
84
|
+
#
|
85
|
+
# @param keyword [String]
|
86
|
+
# @return [Hash]
|
87
|
+
def search_by_keyword(keyword)
|
88
|
+
j = search(keyword)
|
89
|
+
j['b'].collect { |e| e['Id'] }.map { |e| e.downcase }
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns the Technet product list.
|
93
|
+
#
|
94
|
+
# @return [Array]
|
95
|
+
def get_product_dropdown_list
|
96
|
+
@product_dropdown_list ||= lambda {
|
97
|
+
list = []
|
98
|
+
|
99
|
+
page = ::Nokogiri::HTML(firstpage)
|
100
|
+
page.search('//div[@class="sb-search"]//select[@id="productDropdown"]//option').each do |product|
|
101
|
+
option_value = product.attributes['value'].value
|
102
|
+
option_text = product.text
|
103
|
+
next if option_value == '-1' # This is the ALL option
|
104
|
+
list << { option_value: option_value, option_text: option_text }
|
105
|
+
end
|
106
|
+
|
107
|
+
list
|
108
|
+
}.call
|
109
|
+
end
|
110
|
+
|
111
|
+
attr_reader :firstpage
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
data/lib/msu.rb
ADDED
@@ -0,0 +1,234 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'core/thread_pool'
|
3
|
+
require 'engine/msu/google'
|
4
|
+
require 'engine/msu/technet'
|
5
|
+
require 'engine/msu/constants'
|
6
|
+
|
7
|
+
module PatchFinder
|
8
|
+
class MSU
|
9
|
+
|
10
|
+
include Helper
|
11
|
+
|
12
|
+
MAX_THREADS = 10
|
13
|
+
|
14
|
+
# Returns the download links.
|
15
|
+
#
|
16
|
+
# @param args [Hash] Arguments created by the user.
|
17
|
+
# @return [Array]
|
18
|
+
def find_msu_download_links(args)
|
19
|
+
msb_numbers = collect_msbs(args)
|
20
|
+
|
21
|
+
unless msb_numbers.empty?
|
22
|
+
print_status("Advisories found (#{msb_numbers.length}): #{msb_numbers * ', '}")
|
23
|
+
print_status('Please wait while the download links are being collected...')
|
24
|
+
end
|
25
|
+
|
26
|
+
download_links = []
|
27
|
+
|
28
|
+
print_status("Max number of active collecting clients: #{MAX_THREADS}")
|
29
|
+
pool = PatchFinder::ThreadPool.new(MAX_THREADS)
|
30
|
+
|
31
|
+
msb_numbers.each do |msb|
|
32
|
+
pool.schedule do
|
33
|
+
links = collect_links_from_msb(msb, args[:regex])
|
34
|
+
pool.mutex.synchronize do
|
35
|
+
download_links.concat(links)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
pool.shutdown
|
41
|
+
|
42
|
+
sleep(0.5) until pool.eop?
|
43
|
+
|
44
|
+
download_links
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# Returns the MSBs (advisories) numbers.
|
50
|
+
#
|
51
|
+
# @param args [Hash]
|
52
|
+
# @return [Array]
|
53
|
+
def collect_msbs(args)
|
54
|
+
msb_numbers = []
|
55
|
+
|
56
|
+
case args[:search_engine]
|
57
|
+
when :technet
|
58
|
+
print_status("Searching advisories that include #{args[:keyword]} via Technet")
|
59
|
+
msb_numbers = technet_search(args[:keyword])
|
60
|
+
when :google
|
61
|
+
print_status("Searching advisories that include #{args[:keyword]} via Google")
|
62
|
+
msb_numbers = google_search(args[:keyword], args[:google_api_key], args[:google_search_engine_id])
|
63
|
+
end
|
64
|
+
|
65
|
+
msb_numbers
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns the download links for an advisory.
|
69
|
+
#
|
70
|
+
# @param msb [String]
|
71
|
+
# @param regex [Regexp] Search filter
|
72
|
+
# @return [Array]
|
73
|
+
def collect_links_from_msb(msb, regex = nil)
|
74
|
+
unless is_valid_msb?(msb)
|
75
|
+
print_error "Not a valid MSB format: #{msb}"
|
76
|
+
print_error 'Example of a correct one: ms15-100'
|
77
|
+
return []
|
78
|
+
end
|
79
|
+
|
80
|
+
res = download_advisory(msb)
|
81
|
+
|
82
|
+
if !has_advisory?(res)
|
83
|
+
print_error "The advisory cannot be found for #{msb}"
|
84
|
+
return []
|
85
|
+
end
|
86
|
+
|
87
|
+
links = get_details_aspx(res)
|
88
|
+
if links.length == 0
|
89
|
+
print_error "Unable to find download.microsoft.com links for #{msb}."
|
90
|
+
return []
|
91
|
+
else
|
92
|
+
print_status("Found #{links.length} affected products for #{msb}.")
|
93
|
+
end
|
94
|
+
|
95
|
+
link_collector = []
|
96
|
+
|
97
|
+
links.each do |link|
|
98
|
+
download_page = get_download_page(link)
|
99
|
+
download_links = get_download_links(download_page.body)
|
100
|
+
if regex
|
101
|
+
filtered_links = download_links.select { |l| Regexp.new(regex) === l }
|
102
|
+
link_collector.concat(filtered_links)
|
103
|
+
else
|
104
|
+
link_collector.concat(download_links)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
link_collector
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns the download page.
|
112
|
+
#
|
113
|
+
# @param link [string]
|
114
|
+
# @return [Net::Response]
|
115
|
+
def get_download_page(link)
|
116
|
+
res = send_http_get_request(link)
|
117
|
+
|
118
|
+
if res.header['Location']
|
119
|
+
return send_http_get_request("#{PatchFinder::Engine::MSU::MICROSOFT}/#{res.header['Location']}")
|
120
|
+
end
|
121
|
+
|
122
|
+
res
|
123
|
+
end
|
124
|
+
|
125
|
+
# Returns the MSB advisories found from Google.
|
126
|
+
#
|
127
|
+
# @param keyword [String]
|
128
|
+
# @param api_key [String]
|
129
|
+
# @param cx [String]
|
130
|
+
# @return [Array]
|
131
|
+
def google_search(keyword, api_key, cx)
|
132
|
+
search = PatchFinder::Engine::MSU::Google.new(api_key: api_key, search_engine_id: cx)
|
133
|
+
search.find_msb_numbers(keyword)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Returns the MSB advisories found from Technet.
|
137
|
+
#
|
138
|
+
# @param keyword [String]
|
139
|
+
# @return [Array]
|
140
|
+
def technet_search(keyword)
|
141
|
+
search = PatchFinder::Engine::MSU::Technet.new
|
142
|
+
search.find_msb_numbers(keyword)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Returns the advisory page.
|
146
|
+
#
|
147
|
+
# @param msb [String]
|
148
|
+
# @return [String]
|
149
|
+
def download_advisory(msb)
|
150
|
+
send_http_get_request("#{PatchFinder::Engine::MSU::TECHNET}/en-us/library/security/#{msb}.aspx")
|
151
|
+
end
|
152
|
+
|
153
|
+
# Returns the found details pages
|
154
|
+
#
|
155
|
+
# @param res [Res::Response]
|
156
|
+
# @return [Array]
|
157
|
+
def get_details_aspx(res)
|
158
|
+
links = []
|
159
|
+
|
160
|
+
page = res.body
|
161
|
+
n = ::Nokogiri::HTML(page)
|
162
|
+
|
163
|
+
appropriate_pattern = get_appropriate_pattern(n)
|
164
|
+
return links unless appropriate_pattern
|
165
|
+
|
166
|
+
n.search(appropriate_pattern).each do |anchor|
|
167
|
+
found_link = anchor.attributes['href'].value
|
168
|
+
if /https:\/\/www\.microsoft\.com\/downloads\/details\.aspx\?familyid=/i === found_link
|
169
|
+
begin
|
170
|
+
links << found_link
|
171
|
+
rescue ::URI::InvalidURIError
|
172
|
+
print_error "Unable to parse URI: #{found_link}"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
links
|
178
|
+
end
|
179
|
+
|
180
|
+
# Returns the downloads links
|
181
|
+
#
|
182
|
+
# @param page [String] HTML page
|
183
|
+
# @return [Array]
|
184
|
+
def get_download_links(page)
|
185
|
+
page = ::Nokogiri::HTML(page)
|
186
|
+
|
187
|
+
relative_uri = page.search('a').select { |a|
|
188
|
+
a.attributes['href'] && a.attributes['href'].value.include?('confirmation.aspx?id=')
|
189
|
+
}.first
|
190
|
+
|
191
|
+
return [] unless relative_uri
|
192
|
+
relative_uri = relative_uri.attributes['href'].value
|
193
|
+
|
194
|
+
res = send_http_get_request("#{PatchFinder::Engine::MSU::MICROSOFT}/en-us/download/#{relative_uri}")
|
195
|
+
n = ::Nokogiri::HTML(res.body)
|
196
|
+
|
197
|
+
n.search('a').select { |a|
|
198
|
+
a.attributes['href'] && a.attributes['href'].value.include?("#{PatchFinder::Engine::MSU::DOWNLOAD_MSFT}/download/")
|
199
|
+
}.map! { |a| a.attributes['href'].value }.uniq
|
200
|
+
end
|
201
|
+
|
202
|
+
# Returns a pattern that matches the advisory page.
|
203
|
+
#
|
204
|
+
# @param n [Nokogiri::HTML::Document]
|
205
|
+
# @return [String] If a match is found
|
206
|
+
# @return [NilClass] If no match found
|
207
|
+
def get_appropriate_pattern(n)
|
208
|
+
PatchFinder::Engine::MSU::ADVISORY_PATTERNS.each do |pattern|
|
209
|
+
if n.at_xpath(pattern[:check])
|
210
|
+
return pattern[:pattern]
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
nil
|
215
|
+
end
|
216
|
+
|
217
|
+
# Checks if the page is an advisory.
|
218
|
+
#
|
219
|
+
# @param res [Net::Response]
|
220
|
+
# @return [Boolean]
|
221
|
+
def has_advisory?(res)
|
222
|
+
!res.body.include?('We are sorry. The page you requested cannot be found')
|
223
|
+
end
|
224
|
+
|
225
|
+
# Checks if the string is a MSB format.
|
226
|
+
#
|
227
|
+
# @param msb [String]
|
228
|
+
# @return [Boolean]
|
229
|
+
def is_valid_msb?(msb)
|
230
|
+
/^ms\d\d\-\d\d\d$/i === msb
|
231
|
+
end
|
232
|
+
|
233
|
+
end
|
234
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'patch_finder'
|
7
|
+
spec.version = '1.0.1'
|
8
|
+
spec.authors = ['wchen-r7']
|
9
|
+
spec.email = ['wei_chen@rapid7.com']
|
10
|
+
spec.summary = 'Patch Finder'
|
11
|
+
spec.description = 'Generic Patch Finder'
|
12
|
+
spec.homepage = 'http://github.com/wchen-r7/patch-finder'
|
13
|
+
spec.license = 'BSD-3-clause'
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
16
|
+
spec.executables = Dir.glob('bin/*').map{ |f| File.basename(f) }
|
17
|
+
spec.require_paths = ["lib"]
|
18
|
+
|
19
|
+
spec.add_development_dependency "bundler", "~> 1.11"
|
20
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
21
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
22
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
@echo off
|
2
|
+
@setlocal EnableDelayedExpansion
|
3
|
+
|
4
|
+
@set arg=%~1
|
5
|
+
|
6
|
+
if [%arg%] == [] (
|
7
|
+
echo Argument Missing:
|
8
|
+
echo You must provide a directory that contains
|
9
|
+
echo all the Windows patches in *.msu format.
|
10
|
+
echo To Download patches manually, please go:
|
11
|
+
echo http://mybulletins.technet.microsoft.com/BulletinPages/Dashboard
|
12
|
+
exit /B
|
13
|
+
)
|
14
|
+
|
15
|
+
if not "!arg:~-1,1!" == "\" (
|
16
|
+
@set arg=!arg!\
|
17
|
+
)
|
18
|
+
|
19
|
+
|
20
|
+
for /f %%f in ('dir /B %arg%') DO (
|
21
|
+
@set fname=%%f
|
22
|
+
@set lastfourchars=!fname:~-4,4!
|
23
|
+
if "!lastfourchars!" == ".msu" (
|
24
|
+
rem The -15 length is specific to filenames generated by msu_finder, and filters out the following things:
|
25
|
+
rem * The time stamp: "_xxxxxxxxxx"
|
26
|
+
rem * The extension name: ".msu"
|
27
|
+
rem To make this script generic (no time stamp in the filename), instead of -15, you should do -4.
|
28
|
+
@set newname=!fname:~0,-15!
|
29
|
+
|
30
|
+
rem This is the destination path that contains the time stamp.
|
31
|
+
rem The use of time stamp allows the patches to avoid filename collision,
|
32
|
+
rem because sometimes even though the patches are different, they might have the
|
33
|
+
rem same name.
|
34
|
+
@set newdest=!fname:~0,-4!
|
35
|
+
|
36
|
+
mkdir %arg%!newdest!
|
37
|
+
mkdir %arg%!newdest!\extracted
|
38
|
+
expand /F:* %arg%!fname! %arg%!newdest!
|
39
|
+
expand /F:* %arg%!newdest!\!newname!.cab %arg%!newdest!\extracted
|
40
|
+
)
|
41
|
+
|
42
|
+
)
|
43
|
+
|
44
|
+
echo Done!
|
45
|
+
echo Now go to %arg%,
|
46
|
+
echo and then use the search feature from Windows to
|
47
|
+
echo find the files you're interested in.
|
data/tools/list_dll.rb
ADDED
@@ -0,0 +1,220 @@
|
|
1
|
+
# -*- coding: binary -*-
|
2
|
+
#!/usr/bin/env ruby
|
3
|
+
|
4
|
+
require 'optparse'
|
5
|
+
require 'find'
|
6
|
+
|
7
|
+
class String
|
8
|
+
def to_decimal
|
9
|
+
self.unpack("c*").reverse.pack("c*").unpack("N*").first
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_hex
|
13
|
+
self.unpack("c*").reverse.pack("c*")
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_printable_hex_32
|
17
|
+
self.unpack("c*").reverse.pack("c*").unpack("H*").first
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_printable_hex_64
|
21
|
+
self.unpack("s*").pack("s*").reverse.unpack("H*").first
|
22
|
+
end
|
23
|
+
|
24
|
+
def unicode
|
25
|
+
buf = ''
|
26
|
+
|
27
|
+
self.each_char do |c|
|
28
|
+
buf << "#{c}\x00"
|
29
|
+
end
|
30
|
+
|
31
|
+
buf
|
32
|
+
end
|
33
|
+
|
34
|
+
def ansi
|
35
|
+
buf = ''
|
36
|
+
|
37
|
+
self.each_char do |c|
|
38
|
+
next if c == "\x00"
|
39
|
+
buf << c
|
40
|
+
end
|
41
|
+
|
42
|
+
buf
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class DLLInformation
|
47
|
+
X64_86 = :x64_86
|
48
|
+
X86 = :x86
|
49
|
+
|
50
|
+
def initialize(file)
|
51
|
+
f = open(file, 'rb')
|
52
|
+
begin
|
53
|
+
@bin = f.read
|
54
|
+
ensure
|
55
|
+
f.close if f
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def cpu_type
|
60
|
+
@cpu_type ||= lambda {
|
61
|
+
type = @bin.scan(/\x50\x45\x00\x00(..)/).flatten.first || ''
|
62
|
+
type == "\x64\x86" ? X64_86 : X86
|
63
|
+
}.call
|
64
|
+
end
|
65
|
+
|
66
|
+
def base_address
|
67
|
+
case cpu_type
|
68
|
+
when X64_86
|
69
|
+
base_address_x64_86
|
70
|
+
when X86
|
71
|
+
base_address_x86
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def timestamp
|
76
|
+
pe_signature_offset = @bin[60,4].to_decimal
|
77
|
+
@bin[pe_signature_offset+8, 4].to_printable_hex_32
|
78
|
+
end
|
79
|
+
|
80
|
+
def version_info
|
81
|
+
signature_start = "...\x00\x00\x00"
|
82
|
+
signature_start << "VS_VERSION_INFO".unicode
|
83
|
+
signature_end = "\x00\x00\x00\x00\x09\x04\xb0\x04"
|
84
|
+
info = @bin.scan(/(#{signature_start}.+#{signature_end})/).flatten[0]
|
85
|
+
return '' if info.nil?
|
86
|
+
|
87
|
+
signature_start = "FileVersion".unicode
|
88
|
+
signature_start << "\x00\x00".unicode
|
89
|
+
signature_end = "InternalName".unicode
|
90
|
+
file_version = info.scan(/#{signature_start}(.+)\x00\x00.+#{signature_end}/).flatten[0]
|
91
|
+
return '' if file_version.nil?
|
92
|
+
|
93
|
+
file_version
|
94
|
+
end
|
95
|
+
|
96
|
+
def size
|
97
|
+
@bin.length
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def base_address_x86
|
103
|
+
@base_address_x86 ||= lambda {
|
104
|
+
pe_signature_offset = @bin[60,4].to_decimal
|
105
|
+
coff_header_offset = pe_signature_offset + 4 + 44 + 4
|
106
|
+
@bin[coff_header_offset, 4].to_printable_hex_32
|
107
|
+
}.call
|
108
|
+
end
|
109
|
+
|
110
|
+
def base_address_x64_86
|
111
|
+
@base_address_x64_86 ||= lambda {
|
112
|
+
pe_signature_offset = @bin.index("\x50\x45\x00\x00")
|
113
|
+
image_base_offset = pe_signature_offset + 48
|
114
|
+
@bin[image_base_offset, 8].to_printable_hex_64
|
115
|
+
}.call
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
class Table
|
121
|
+
def initialize(opts)
|
122
|
+
@path = opts[:path]
|
123
|
+
@size = opts[:size]
|
124
|
+
@version = opts[:version]
|
125
|
+
@base = opts[:base]
|
126
|
+
@timestamp = opts[:timestamp]
|
127
|
+
end
|
128
|
+
|
129
|
+
def show
|
130
|
+
puts '#{get_size} #{get_base} #{get_timestamp} #{get_version} #{get_path}'
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def get_size
|
136
|
+
s = [@size].pack("V*").unpack("H*")[0]
|
137
|
+
"0x#{s.rjust(8, '0')}"
|
138
|
+
end
|
139
|
+
|
140
|
+
def get_path
|
141
|
+
base = "#{Dir.pwd}/"
|
142
|
+
p = @path.gsub(/^#{base}/, '')
|
143
|
+
|
144
|
+
# Max out the length at 44 characters
|
145
|
+
p = "#{p[0,44]}..." if p.length > 44
|
146
|
+
|
147
|
+
p
|
148
|
+
end
|
149
|
+
|
150
|
+
def get_version
|
151
|
+
@version.ansi.scan(/^([0-9\.]+) /).flatten.first.ljust(16, ' ')
|
152
|
+
end
|
153
|
+
|
154
|
+
def get_base
|
155
|
+
"0x#{@base.ljust(16, ' ')}"
|
156
|
+
end
|
157
|
+
|
158
|
+
def get_timestamp
|
159
|
+
"0x#{@timestamp.rjust(8, '0')}"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def init_args
|
164
|
+
opts = {}
|
165
|
+
opt = OptionParser.new
|
166
|
+
opt.banner = "Usage: #{__FILE__} [options]"
|
167
|
+
opt.separator('')
|
168
|
+
opt.separator('Options:')
|
169
|
+
|
170
|
+
opt.on('-d', '--dll [string]', String, 'DLL to specify') do |n|
|
171
|
+
opts[:dll_name] = n
|
172
|
+
end
|
173
|
+
|
174
|
+
opt.on_tail('-h', '--help', 'Show usage') do
|
175
|
+
puts opt
|
176
|
+
exit(0)
|
177
|
+
end
|
178
|
+
|
179
|
+
opt.parse!
|
180
|
+
opts
|
181
|
+
end
|
182
|
+
|
183
|
+
def list_dll(dir, dll_name)
|
184
|
+
puts 'Size Base Timestamp Version Path'
|
185
|
+
|
186
|
+
Find.find(dir) do |p|
|
187
|
+
next if p !~ /\.dll$/i
|
188
|
+
next if dll_name && p !~ /#{dll_name}/
|
189
|
+
|
190
|
+
dll = DLLInformation.new(p)
|
191
|
+
size = dll.size
|
192
|
+
version = dll.version_info
|
193
|
+
base_address = dll.base_address
|
194
|
+
timestamp = dll.timestamp
|
195
|
+
|
196
|
+
t = Table.new({
|
197
|
+
:path => p,
|
198
|
+
:size => size,
|
199
|
+
:version => version,
|
200
|
+
:base => base_address,
|
201
|
+
:timestamp => timestamp
|
202
|
+
})
|
203
|
+
|
204
|
+
t.show
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def main(args)
|
209
|
+
base_dir = Dir.pwd
|
210
|
+
list_dll(base_dir, args[:dll_name])
|
211
|
+
end
|
212
|
+
|
213
|
+
args = init_args
|
214
|
+
begin
|
215
|
+
main(args)
|
216
|
+
rescue RuntimeError => e
|
217
|
+
puts e.message
|
218
|
+
exit(0)
|
219
|
+
rescue Interrupt
|
220
|
+
end
|
metadata
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: patch_finder
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- wchen-r7
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-03-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.11'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.11'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
description: Generic Patch Finder
|
56
|
+
email:
|
57
|
+
- wei_chen@rapid7.com
|
58
|
+
executables:
|
59
|
+
- msu_finder
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- Gemfile
|
64
|
+
- README.md
|
65
|
+
- Rakefile
|
66
|
+
- bin/msu_finder
|
67
|
+
- docs/bin/msu_finder.txt
|
68
|
+
- lib/core/config.rb
|
69
|
+
- lib/core/helper.rb
|
70
|
+
- lib/core/thread_pool.rb
|
71
|
+
- lib/engine/msu/constants.rb
|
72
|
+
- lib/engine/msu/google.rb
|
73
|
+
- lib/engine/msu/technet.rb
|
74
|
+
- lib/msu.rb
|
75
|
+
- patch_finder.gemspec
|
76
|
+
- tools/extract_msu.bat
|
77
|
+
- tools/list_dll.rb
|
78
|
+
homepage: http://github.com/wchen-r7/patch-finder
|
79
|
+
licenses:
|
80
|
+
- BSD-3-clause
|
81
|
+
metadata: {}
|
82
|
+
post_install_message:
|
83
|
+
rdoc_options: []
|
84
|
+
require_paths:
|
85
|
+
- lib
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
requirements: []
|
97
|
+
rubyforge_project:
|
98
|
+
rubygems_version: 2.4.8
|
99
|
+
signing_key:
|
100
|
+
specification_version: 4
|
101
|
+
summary: Patch Finder
|
102
|
+
test_files: []
|