本章将使我们熟悉某些高级 Python 模块,这些模块在涉及响应时间、处理速度、互操作性和通过网络发送数据等参数时非常方便。我们将在线程和进程的帮助下研究 Python 中的并行处理。我们还将阅读有关在 IPC 和子流程的帮助下在流程之间建立通信的内容。之后,我们将探索 Python 中的套接字编程,并通过实现反向 TCP 外壳进入网络安全领域。本章将涵盖以下主题:
-
线程多任务处理
-
带进程的多任务处理
-
子流程
-
socket 编程基础
-
用 Python 实现反向 TCP 外壳
线程是一个轻量级进程,它与其父进程共享相同的地址和内存空间。它在处理器内核上并行运行,从而为我们提供了并行性和多任务处理能力。它与父进程共享相同的地址和内存空间,这使得多任务处理的整个操作非常轻量级,因为不涉及上下文切换开销。在上下文切换中,当计划执行新进程时,操作系统需要保存前一进程的状态,包括进程 ID、指令指针、返回地址等。
这是一项耗时的活动。由于使用线程进行多任务处理不需要创建新的进程来实现并行性,因此线程在多任务处理活动中提供了非常好的性能。正如在 Java 中,我们有Thread
类或可运行接口来实现线程一样,在 Python 中,我们可以使用Thread
模块来实现线程。在 Python 中实现线程通常有两种方法:一种是 Java 风格的,另一种是 Python 风格的。让我们来看看两者。
下面的代码显示了类似 Java 的实现,其中我们对 threading 类进行了子类化,并重写了run()
方法。我们将希望与线程并行运行的逻辑或任务放在run()
方法中:
import threading
>>> class a(threading.Thread):
... def __init__(self):
... threading.Thread.__init__(self)
... def run(self):
... print("Thread started")
...
>>> obj=a()
>>> obj.start()
Thread started
这里,我们得到了一个方法(run()
,在本例中,该方法是并行执行的。这就是 Python 用它的另一种线程方法所探索的,在这种方法中,我们可以在线程的帮助下使任何方法并行执行。我们可以使用我们选择的任何方法,并且该方法可以接受任何参数。
下面的代码片段显示了使用线程的另一种方法。在这里,我们可以看到我们通常定义了一个add(num1,num2)
方法,然后将其与线程一起使用:
>>> import threading
>>> def add(num1,num2):
... print(num1 + num2)
...
>>> for i in range(5):
... t=threading.Thread(target=add,args=(i,i+1))
... t.start()
...
1
3
5
7
9
for
循环创建一个线程对象t
。调用start()
方法时,调用创建线程对象时在目标参数中指定的方法。在前面的例子中,我们已经将add()
方法传递给线程实例。要传递给要通过线程调用的方法的参数作为元组在args
参数下传递。add()
方法通过线程调用五次,输出打印在屏幕上,如上例所示。
必须注意的是,线程是从主程序调用的,在线程完全执行之前,主程序不会退出(默认情况下)。原因是主程序默认以非恶魔模式调用线程,这使得线程在前台运行,而不是等待它在后台运行。因此,非恶魔线程是在前台运行的线程,导致主程序等待正在运行的线程完成其执行。另一方面,恶魔线程是在后台运行的线程,因此不会导致主程序等待它完成执行。请看以下示例:
从前面的代码片段可以看出,当我们创建并执行一个非恶魔线程(默认)时,在打印Main Ended
之后,终端窗口会停止 4 秒,等待ND
线程完成其执行。当它结束时,我们会收到一条Exit Non Demonic
消息,即主程序退出时。在此之前,主程序不会退出。
让我们看看在后台运行的恶魔线程是如何改变的:
在前面的代码片段中,我们看到了如何使用恶魔线程。有趣的是,主程序没有等待恶魔线程完成执行。恶魔线程在后台运行,当它完成时,主线程已经从内存中退出,因此我们没有看到屏幕上打印的Exit :Daemonic
消息。在本例中,我们使用的是日志模块。默认情况下,日志模块将登录到stdout
,在我们的例子中,它恰好是终端。
正如我们在上一节中看到的,默认情况下,主线程将等待线程执行。尽管如此,主方法的代码仍将被执行,因为主线程将在与子线程不同的处理器内核上运行。在某些情况下,我们可能希望按照子线程的执行周期控制主线程的执行。假设我们希望主线程的一部分代码只在子线程执行之后执行。这可以通过join()
方法实现。如果我们在线程 T 上从主线程 M 的第 X 行调用它,那么在 T 线程完成其执行之前,主线程的第 X+1 行将不会执行。换句话说,我们将主线程的尾部与线程 T 连接起来,因此主线程的执行将暂停,直到 T 完成。请看下面的示例,其中我们使用线程枚举和join()
以三个线程为一批执行线程。
在退出之前,主程序必须验证所有线程是否已执行:
以下屏幕截图描述了上述代码的输出:
尽管线程之间是独立执行的,但在许多情况下,线程需要相互通信,例如,一个线程只需要在另一个线程到达某个点时启动任务。假设我们正在处理生产者和消费者问题,其中一个线程(生产者)负责将项目放入队列。生产者线程需要向使用者线程发送一条消息,以便知道它可以使用队列中的数据。这可以通过 Python 中的线程事件来实现。调用threading.event()
返回一个事件实例,可以使用set()
方法设置,使用clear()
方法重置。
在下面的代码块中,我们将看到一个示例,其中一个线程将递增一个计数器。当计数器值达到 5 时,需要另一个线程执行操作。必须注意的是,事件还有一个wait()
方法,等待事件被阻止或设置。事件可以等待一个超时时间间隔,也可以无限期地等待,但一旦设置标志为true
,则wait()
方法实际上不会阻止线程的执行。以下代码对此进行了描述:
在许多情况下,多个线程需要共享一个资源。我们希望确保,如果一个线程正在更改对象的状态,那么另一个线程必须等待。为了避免不一致的结果,必须在更改共享资源的状态之前锁定该资源。状态更改后,应释放锁。Python 提供了线程锁来实现这一点。请看下面的代码片段Thread_locking.py
,它演示了线程锁定和并发控制:
前面的代码片段显示了线程锁定。这里,count
是多个线程尝试更新的共享变量。第一个输出没有锁定机制(注释掉了第 16 行和第 22 行)。当没有锁定到位时,可以看到thread_3
在获取锁定时读取的值为 1,与thread_4
的情况相同。每个线程将计数的值增加 1,但在thread_4
结束时,计数的值是 3。从我们使用锁定时获得的第二个输出可以看出,在更新共享资源counter
时,没有其他线程可以实际读取它,因此获得的结果是一致的。
与线程模块一样,多处理模块也用于提供多任务处理功能。线程模块实际上有点欺骗性:它在 Python 中的实现实际上不是用于并行处理,而是用于在单核上进行分时处理。在解释器级别,默认的 Python 实现CPython不是线程安全的。无论何时使用线程,都会有一个全局解释器锁(GIL)放置在 Python 线程中访问的对象上。这个锁以分时方式执行线程,为每个线程提供少量的时间,因此在我们的程序中没有性能增益。因此,开发多处理模块是为了向 Python 生态系统提供并行处理。这通过在多个处理器内核之间产生负载来减少执行时间。请看以下使用多处理的代码:
>>> import multiprocessing
>>> def process_me(id):
... print("Process " +str(id))
...
>>> for i in range(5):
... p=multiprocessing.Process(target=process_me,args=(i,))
... p.start()
>>> Process 0
>>> Process 1
>>> Process 2
>>> Process 3
>>> Process 4
import multiprocessing as mp
>>> class a(mp.Process):
... def __init__(self):
... threading.Thread.__init__(self)
... def run(self):
... print("Process started")
...
>>> obj=a()
>>> obj.start()
Process started
前面的代码片段表示多处理的两种实现:简单方法和基于类的方法。
我们已经研究了什么是恶魔线程和非恶魔线程。同样的原则也适用于过程。恶魔进程在后台运行而不阻塞主进程,而非恶魔进程在前台运行。如下例所示:
从前面的代码片段可以看出,当我们创建并执行一个非恶魔进程(默认选项),如输出 1 和第 20 行所示,在打印Main Ended
后,终端窗口会停止 4 秒钟,同时等待非恶魔进程完成其执行。当它结束时,我们得到Exit Non Daemonic
消息,即主程序退出时。在第二种情况下(如输出 2 中所示),主程序不会等待恶魔进程完成其执行。守护进程在后台运行,当它完成时,主线程已经从内存中退出。因此,我们没有看到屏幕上打印的Exit :Daemonic
消息。
我们看到的关于线程连接和枚举的相同理论也可以应用于进程。进程可以连接到主线程或另一个进程,其连接方式是,在连接的进程完成之前,另一个线程不会退出。除了连接和枚举之外,我们还可以在 Python 中显式终止进程。
看看下面的代码片段,它演示了前面的概念。下面代码的目标是生成几个进程,并使主进程等待 10 秒钟,以使生成的进程完成执行。如果未完成,则仍在运行的将在退出之前终止:
前面的代码Join_enumerate_terminate.py
相当简单;我们所做的与之前使用线程所做的相同。这里唯一的区别是,我们只应用 join 操作 3 秒钟,因此我们故意得到一些处于活动状态的进程。然后,我们通过对这些进程应用terminate()
来杀死它们。
多处理库最酷的特性之一是池。这使我们能够将任务均匀地分布在所有处理器核心上,而不必担心一次活动运行的进程数量。这意味着该模块能够在批处理中生成一组进程。假设我们将批处理大小定义为 4,这是我们可能拥有的处理器内核数。这意味着,在任何时候,可以执行的最大进程数是四个,如果其中一个进程完成了执行,这意味着我们现在有三个正在运行的进程,模块会自动选择下一组进程,使批处理大小再次等于四个。该过程将继续,直到我们完成分布式任务或明确定义条件为止。
看看下面的例子,我们需要在八个不同的文件中写入 800 万条记录(每个文件中有 100 万条记录)。我们有一个四核处理器来执行这项任务。理想情况下,我们需要两次生成一批四个进程,以便每个进程在文件中写入 100 万条记录。由于我们有四个核心,我们希望每个核心执行我们任务的不同部分。如果我们选择同时生成八个进程,我们将在上下文切换方面浪费一些时间,因此我们需要明智地使用处理器和处理能力以获得最大吞吐量:
在前面的代码Multiprocess_pool.py
中,我们正在第 30 行创建一个多处理池。我们将池的大小定义为size=mp.cpu_count()
,在我们的例子中是4
,因此我们定义了一个大小为 4 的池。我们需要创建 8 个文件,每个文件包含 100 万条记录。我们使用一个for
循环来定义八个进程,这些进程将通过调用所创建的池对象上的apply_async()
发送到池对象。apply_async()
方法需要我们希望以多处理模块作为参数的进程执行的方法的名称。第二个参数是传递给我们希望执行的方法的参数。请注意,当进程与池模块一起执行时,它还具有从方法返回数据的功能。
从输出中可以看出,任何时候都不会同时执行超过四个进程。也可验证第一道工序为Forkpoolworker4
。当批大小为 3 时,模块会立即生成另一个进程。这可以通过输出进行验证,输出在第(1)节的第六行说明Started process Poolworker4
。
请注意,两个批处理是并行执行的。每个进程需要 13 到 14 秒,但由于它们是并行执行的,每个核心上有一个,因此每个批的总批执行时间为 14 秒。因此,对于两个批次,总时间为 28 秒。可以清楚地看到,通过使用并行,我们仅在 28 秒内解决了问题。如果我们采用顺序或线程方法,总时间将接近*(138)=104秒。作为练习,你自己试试看。
现在,让我们再举一个例子,展示池模块的另一个功能维度。假设作为我们需求的一部分,我们需要解析所创建的 800 万个文件中的四个,这些文件的 ID%1700
为零。然后,我们必须将所有四个文件的结果合并到一个不同的文件中。这是分布式处理和结果聚合的一个很好的例子:进程不仅应该并行读取文件,还必须聚合结果。这有点类似于 Hadoop 的 MapReduce 问题。在典型的 map reduce 问题中,有两组操作:
- 映射:这涉及到在分布式系统中跨多个节点拆分一个巨大的数据集。每个节点处理它接收的数据块。
- Reduce:这是聚合操作,返回来自每个节点的映射阶段的输出,并根据逻辑最终聚合并返回结果。
我们在这里也在做同样的事情,唯一的区别是我们使用处理器内核来代替节点:
从前面的代码片段中可以看出,借助于Pool
模块的map()
方法,我们可以使多个进程在不同的文件上并行工作,然后合并所有结果并将其作为单个结构发送。这些进程并行执行,record_id %1700
为我们返回零的记录将返回给我们。最后,我们将聚合结果保存在Modulo_1700_agg
文件中。这是多处理模块的一个非常强大的功能,如果使用得当,可以大幅减少处理时间和聚合时间。
从另一个流程调用外部流程称为子流程。在这种情况下,进程之间的通信是在 OS 管道的帮助下进行的。换句话说,如果流程 B 将流程 a 作为子流程调用,那么流程 B 可以向其传递输入,还可以通过操作系统管道从中读取输出。当涉及到自动化渗透测试以及使用 Python 调用其他工具和实用程序时,此模块至关重要。Python 提供了一个非常强大的模块subprocess
来处理子流程。请看下面的代码片段Subprocessing.py
,它显示了如何使用子流程调用名为ls
的系统命令:
在前面的代码片段中,我们使用subprocess.Popen()
方法调用subprocess
。调用或调用subprocess
的其他方法很少,例如call()
,但我们这里讨论的是Popen
。这是因为Popen
方法返回将生成的进程的进程 ID,这反过来使我们能够很好地控制该进程。Popen
方法接受许多参数,其中第一个参数实际上是要在操作系统级别执行的命令。命名参数包括stderr=subprocess.PIPE
,这意味着如果外部程序或脚本产生错误,则该错误必须重定向到操作系统管道,父进程必须从该管道读取错误。stdout=subprocess.PIPE
建议子进程将产生的输出也必须通过管道发送到父进程。shell=True
建议无论给出什么命令,第一个参数必须被视为shell
命令,如果它有一些参数,它们必须作为要调用的进程的参数传递。最后,如果我们希望父进程读取子进程产生的输出和错误,我们必须在被调用的subprocess
上调用communicate()
方法。communicate()
方法打开subprocess
管道,通信从子进程写入管道一端开始,父进程从另一端读取。必须注意的是,communicate()
方法将使父进程等待子进程完成。该方法返回一个元组,其输出位于第 0 个索引,std error 位于第 1 个索引。
应该注意的是,我们永远不应该在实际示例中使用shell=True
,因为这会使应用容易受到 shell 注入的影响。避免使用以下行:
>>> subprocess.Popen(command, shell=True) #This would remove everything !!
请看下面的示例,其中我们将使用shell=False
。对于shell=False
,我们调用的进程/命令的命令和参数必须作为列表单独传递。让我们试着用shell=False
执行ls -l
:
这就是我们在子流程模块的帮助下用 Python 执行外部流程的方式。
当我们谈到套接字时,我们指的是 TCP 和 UDP 套接字。套接字连接只是 IP 地址和端口号的组合。在端口上运行的每个服务都在内部实现和使用套接字。
例如,我们的 web 服务器总是侦听端口80
(默认情况下),打开一个与外部世界的套接字连接,并使用 IP 地址和端口80
绑定到套接字。插座连接可在以下两种模式下使用:
- 服务器
- 客户
当套接字用作服务器时,服务器执行的步骤顺序如下:
- 创建一个套接字。
- 绑定到套接字。
- 听插座。
- 接受连接。
- 接收和发送数据。
另一方面,当套接字连接用作客户端以连接到服务器套接字时,步骤顺序如下:
- 创建一个套接字。
- 连接到插座。
- 接收和发送数据。
请看下面的代码片段server_socket.py
,它在端口80
实现了一个 TCP 服务器套接字:
在前面的例子中,我们使用socket.socket
语句创建了一个套接字。这里,socket.AF_INET
表示 IPv4 协议,socket.SOCK_STREAM
建议使用基于流的套接字数据包,这些数据包只不过是 TCP 流。bind()
方法将元组作为参数,第一个参数是本地 IP 地址。您应该将其替换为您的个人 IP 或127.0.0.1
。给 tuple 的第二个参数是 port,它反过来调用bind()
方法。然后,我们开始监听套接字,最后开始一个接受客户端连接的循环。请注意,该方法创建一个单线程服务器,这意味着如果任何其他客户端连接,它必须等待活动客户端断开连接。send ()
和recv()
方法是不言自明的。
现在,让我们创建一个基本的客户端套接字代码client_socket.py
,它连接到先前创建的服务器并向其传递消息:
客户端和服务器套接字产生的输出如下所示:
以下是我们如何使用 UDP 的套接字连接:
sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
现在我们已经了解了子处理、多处理等的基本知识,用 Python 实现一个基本的 TCP 反向 shell 非常简单。对于这个例子,rev_tcp.py
,我们将使用基于 bash 的反向 TCP shell。在本书后面的章节中,我们将看到如何完全使用 Python 传递反向 shell:
需要注意的是,OS.dup2
用于在 Python 中创建文件描述符的副本。stdin
定义为文件描述符0
、stdout
定义为文件描述符1
、stderr
定义为文件描述符2
。代码行OS.dup2(s.fileno(),0)
指示我们应该创建stdin
的副本,并将流量重定向到套接字文件,该文件恰好位于本地主机和端口1234
(Netcat 正在侦听的端口)上。最后,我们在交互模式下调用 shell,由于我们没有指定stderr
、stdin
和stdout
参数,默认情况下,参数将发送到系统级的stdin
和stdout
,并再次映射到程序范围的套接字。因此,前面的代码段将以交互模式打开 shell 并将其传递给套接字。所有输入从插座中获取为stdin
,所有输出通过stdout
传递到插座。这可以通过查看生成的输出来验证。
在本章中,我们讨论了 Python 的一些更高级的概念,这些概念允许我们提高吞吐量。我们讨论了多处理 Python 模块,以及如何使用它们来减少所花费的时间并提高我们的处理能力。在本章中,我们基本上涵盖了从 Python 进入渗透测试、自动化和各种网络安全用例领域所需的一切。应该指出的是,从现在开始,我们的重点将是应用我们迄今为止所研究的概念,而较少解释它们是如何工作的。因此,如果您有任何疑问,我强烈建议您在继续之前澄清这些问题。在下一章中,我们将讨论如何使用 Python 解析 PCAP 文件、自动化 Nmap 扫描等。对于所有的安全爱好者,让我们开始做生意吧。
- 我们可以在 Python 中使用哪些其他多处理库?
- 如果线程实际上在同一个核心上执行,那么在 Python 中,线程在哪里变得有用呢?