Skip to content

Latest commit

 

History

History
255 lines (159 loc) · 22 KB

File metadata and controls

255 lines (159 loc) · 22 KB

四、高级 Python 模块

本章将使我们熟悉某些高级 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绑定到套接字。插座连接可在以下两种模式下使用:

  • 服务器
  • 客户

当套接字用作服务器时,服务器执行的步骤顺序如下:

  1. 创建一个套接字。
  2. 绑定到套接字。
  3. 听插座。
  4. 接受连接。
  5. 接收和发送数据。

另一方面,当套接字连接用作客户端以连接到服务器套接字时,步骤顺序如下:

  1. 创建一个套接字。
  2. 连接到插座。
  3. 接收和发送数据。

请看下面的代码片段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

现在我们已经了解了子处理、多处理等的基本知识,用 Python 实现一个基本的 TCP 反向 shell 非常简单。对于这个例子,rev_tcp.py,我们将使用基于 bash 的反向 TCP shell。在本书后面的章节中,我们将看到如何完全使用 Python 传递反向 shell:

需要注意的是,OS.dup2用于在 Python 中创建文件描述符的副本。stdin定义为文件描述符0stdout定义为文件描述符1stderr定义为文件描述符2。代码行OS.dup2(s.fileno(),0)指示我们应该创建stdin的副本,并将流量重定向到套接字文件,该文件恰好位于本地主机和端口1234(Netcat 正在侦听的端口)上。最后,我们在交互模式下调用 shell,由于我们没有指定stderrstdinstdout参数,默认情况下,参数将发送到系统级的stdinstdout,并再次映射到程序范围的套接字。因此,前面的代码段将以交互模式打开 shell 并将其传递给套接字。所有输入从插座中获取为stdin,所有输出通过stdout传递到插座。这可以通过查看生成的输出来验证。

总结

在本章中,我们讨论了 Python 的一些更高级的概念,这些概念允许我们提高吞吐量。我们讨论了多处理 Python 模块,以及如何使用它们来减少所花费的时间并提高我们的处理能力。在本章中,我们基本上涵盖了从 Python 进入渗透测试、自动化和各种网络安全用例领域所需的一切。应该指出的是,从现在开始,我们的重点将是应用我们迄今为止所研究的概念,而较少解释它们是如何工作的。因此,如果您有任何疑问,我强烈建议您在继续之前澄清这些问题。在下一章中,我们将讨论如何使用 Python 解析 PCAP 文件、自动化 Nmap 扫描等。对于所有的安全爱好者,让我们开始做生意吧。

问题

  1. 我们可以在 Python 中使用哪些其他多处理库?
  2. 如果线程实际上在同一个核心上执行,那么在 Python 中,线程在哪里变得有用呢?

进一步阅读