【py网安武器库】第二期:开发简易ping工具【原创】
任务目标
1、理解ICMP协议的原理
2、实现代码,尽可能多的实现探测主机是否存活的功能
3.扩展任务:使用多线程技术提升探测速度
实验环境:python3.9 pycharm
前言
来到了的系列分享的第二篇,这次我们还是要从认识一个新的网络协议出发,开发相应的python脚本,并且学习如何使用多线程来提高效率.
ICMP协议
ICMP(Internet Control Message Protocol)Internet控制报文协议。它是TCP/IP协议簇的一个子协议,用于在IP主机、路由器之间传递控制消息。控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用。ICMP使用IP的基本支持,就像ICMP是一个更高级别的协议,但是,ICMP实际上是IP的一个组成部分,必须由每个IP模块实现。
图文来源:百度百科
icmp工作流程简介
让我们通过手绘的ICMP图了解一下ICMP协议的流程
ICMP报文情况
ICMP报文包含在IP数据报中,IP报头在ICMP报文的最前面。一个ICMP报文包括IP报头(至少20字节)、ICMP报头(至少八字节)和ICMP报文(属于ICMP报文的数据部分)。当IP报头中的协议字段值为1时,就说明这是一个ICMP报文。ICMP报头如下图所示。
如下图:
ICMP报文格式具体由RFC 777 [4] ,RFC 792 [2] 规范。
ICMP大概分为两类报文:
一类是通知出错原因 ;一类是用于诊断查询
以下是icmp基本报文类型表:
类型(十进制) | 内容 |
---|---|
0 | 回送应答 |
3 | 目标不可达 |
4 | 原点抑制 |
5 | 重定向或改变路由 |
8 | 回送请求 |
9 | 路由器公告 |
10 | 路由器请求 |
11 | 超时 |
17 | 地址子网请求 |
18 | 地址子网应答 |
常见的icmp报文
相应请求
我们用的ping操作中就包括了相应请求(类型字段值为8)和应答(类型字段值为0)ICMP报文。
过程:
一台主机向一个节点发送一个类型字段值为8的ICMP报文,如果途中没有异常(如果没有被路由丢弃,目标不回应ICMP或者传输失败),则目标返回类型字段值为0的ICMP报文,说明这台主机存在。
目标不可达,源抑制和超时报文
这三种报文的格式是一样的。
(1)目标不可到达报文(类型值为3)在路由器或者主机不能传递数据时使用。
例如:我们要连接对方一个不存在的系统端口(端口号小于1024)时,将返回类型字段值3、代码字段值为3的ICMP报文。
常见的不可到达类型还有网络不可到达(代码字段值为0)、主机不可达到(代码字段值为1)、协议不可到达(代码字段值为2)等等。
(2)源抑制报文(类型字段值为4,代码字段值为0)则充当一个控制流量的角色,通知主机减少数据报流量。由于ICMP没有回复传输的报文,所以只要停止该报文,主机就会逐渐恢复传输速率。
(3)无连接方式网络的问题就是数据报会丢失,或者长时间在网络游荡而找不到目标,或者拥塞导致主机在规定的时间内无法重组数据报分段,这时就要触发ICMP超时报文的产生。
超时报文(类型字段值为11)的代码域有两种取值:代码字段值为0表示传输超时,代码字段值为1表示分段重组超时。
时间戳请求
时间戳请求报文(类型值字段13)和时间戳应答报文(类型值字段14)用于测试两台主机之间数据报来回一次的传输时间。
传输时,主机填充原始时间戳,接受方收到请求后填充接受时间戳后以类型值字段14的报文格式返回,发送方计算这个时间差。
(有些系统不响应这种报文)
脚本开发:实现探测主机存活
我们可以利用python自带的scapy模块中的内容实现ICMP协议的请求和答复
Scapy学习
Scapy
是一个可以让用户发送、侦听和解析并伪装网络报文的python程序。这些功能可以用于制作侦测、扫描和攻击网络的工具。
换言之,Scapy
是一个强大的操纵报文的交互程序。它可以伪造或者解析多种协议的报文,还具有发送、捕获、匹配请求和响应这些报文以及更多的功能。Scapy
可以轻松地做到像扫描(scanning)、路由跟踪(tracerouting)、探测(probing)、单元测试(unit tests)、攻击(attacks)和发现网络(network discorvery)这样的传统任务。它可以代替hping
,arpspoof
,arp-sk
,arping
,p0f
甚至是部分的Namp
,tcpdump
和tshark
的功能。
scapy信息
官网:scapy
用户手册:scapy docs
函数汇总
函数 | 作用 |
---|---|
数据包 | 生成数据包 |
IP | IP数据包 |
TCP | TCP数据包,基于IP |
ICMP | ICMP数据包,基于IP |
srp(pkt) | 发送二层数据包,并等待响应。 |
srp1(pkt) | 发送第二层数据包,并返回响应的数据包 |
sr(pkt) | 发送三层数据包,返回两个结果,分别是接收到响应的数据包和未收到响应的数据包。 |
send | 发送数据包,三层 |
sendp | 发送数据包,两层 |
sr1 | 发送三层数据包并接收一个数据包 |
scapy源码
>>> packet =Ether()/IP(src='192.168.1.113',dst='192.168.1.1')/ICMP()
>>> packet.show()
###[ Ethernet ]###
dst= 88:25:93:d2:6e:fa
src= 00:0c:29:62:44:de
type= 0x800
###[ IP ]###
version= 4
ihl= None
tos= 0x0
len= None
id= 1
flags=
frag= 0
ttl= 64
proto= icmp
chksum= None
src= 192.168.1.113
dst= 192.168.1.1
\options\
###[ ICMP ]###
type= echo-request
code= 0
chksum= None
id= 0x0
seq= 0x0
>>> p = srp1(packet)
Begin emission:
.Finished sending 1 packets.
*
Received 2 packets, got 1 answers, remaining 0 packets
>>> p.show()
###[ Ethernet ]###
dst= 00:0c:29:62:44:de
src= 88:25:93:d2:6e:fa
type= 0x800
###[ IP ]###
version= 4
ihl= 5
tos= 0x0
len= 28
id= 36140
flags=
frag= 0
ttl= 64
proto= icmp
chksum= 0x69f2
src= 192.168.1.1
dst= 192.168.1.113
\options\
###[ ICMP ]###
type= echo-reply
code= 0
chksum= 0xffff
id= 0x0
seq= 0x0
###[ Padding ]###
load= '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
>>> p.getlayer(ICMP) #获取ICMP层
<ICMP type=echo-reply code=0 chksum=0xffff id=0x0 seq=0x0 |<Padding load='\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' |>>
>>> p.getlayer(ICMP).fields['type'] #获取type的值
0
ipaddress模块
同时,python还自带ipaddress模块.不得不说python真是脚本开发yyds
>>> import ipaddress
>>> ip = list(ipaddress.ip_network('192.168.1.0/30'))
>>> ip
[IPv4Address('192.168.1.0'),
IPv4Address('192.168.1.1'),
IPv4Address('192.168.1.2'),
IPv4Address('192.168.1.3')]
>>> for i in ip:
...: print(i)
...:
192.168.1.0
192.168.1.1
192.168.1.2
192.168.1.3
>>> ip = ipaddress.ip_network('192.168.1.1')
>>> for i in ip:
...: print(i)
...:
192.168.1.1
以上是我们需要使用到的自带模块的介绍,站在巨人的肩膀上,我们的 脚本编写工作就大大的简化了。
首先定义两个函数,实现icmp请求和主机存活探索
def icmp_rqst(ip_dst,iface=None)
pkt=Ether()/IP(dst=ip_dst)/ICMP(type=8):
res=srp1(pkt,timeout=2,verbose=False)
return res
这个函数实现了icmp请求
pkt变量用于利用scary获取icmp报文.采用srp1的方式返回响应包赋值给res变量,最终将res变量返回。
def icmp_scan(ip_dst)
req=icmp_request(ip_dst)
if req:
type =req.getlayer('ICMP').fields['type']
print('[+]',ip_dst,':',type,' Host is up')
else:
pass
这个函数实现了主机存活探索。首先利用编写好的icmp_rqst函数获取icmp报文,然后通过if选择结构。存活的主机则输出host is up 。
扩展:增加扫描子网功能
def main(network)
network= list(ipaddress.ip_network(network))
for ip in network:
icmp_scan(ip)
使用list函数将包含所有子网地址字符串变成每一个子网为一个元素的列表。然后枚举扫描。
list()函数
list()函数 将字符串变成列表
eg:
a = (1,2) #tuple
b = {"1":2,"3":3} #dict
c = {1,2,3} #set
d = range(2,10,2) #range
print(list(a))
print(list(b))
print(list(c))
print(list(d))
输出:
[1, 2]
['1', '3']
[1, 2, 3]
[2, 4, 6, 8]
扩展:实现多线程提高效率
Python 命令行工具 argparse
argparse在我现在的理解中有三大功能
1用来实现通过命令行传参
2用来实现通过命令行定义函数
3用来定义参数
最基本的用法
import argparse
parser = argparse.ArgumentParser()
parser.parse_args()
当执行了 parse_args() 之后默认情况类似于这样:
在执行 parse_args() 之前,所有追加到命令行的参数都不会生效。所以说命令行工具argparse的使用声明一定要在所有需要这个我们通过命令行传入的函数之前。
设置默认参数函数
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("echo")
args = parser.parse_args()
print args.echo
定义可选参数
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-v", "--verbosity", help="increase output verbosity")
args = parser.parse_args()
if args.verbosity:
print "verbosity turned on"
如果用省略方法 -v赋值的话,最终–verbosity 值会传递到这个完整的参数中去,如果没有后面的 –verbosity 只有 -v 的话,那么值会可以通过 args.v 得到。
Python多线程编程 threading 模块
什么是线程
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
线程与进程
Python自己没有线程和进程,Python中调用的操作系统的线程和进程。
-
操作系统帮助开发者操作硬件。
- 程序员写好代码在操作系统上运行(依赖解释器)。
进程与线程的区别:
进程是cpu资源分配的最小单元,线程是cpu计算的最小单元。
一个进程中可以有多个线程。
对于python来说他的进程和线程和其他语言有差异是有GIL锁。GIL锁保证一个进程中同一时刻只有一个线程被cpu调度。
python threading模块学习
本文我们学习创建thread类,传递一个参数的基本方法。
三部曲:
首先实例化thread类
其次start方法开始线程
最后join方法可以让主线程等待所有的线程执行完毕
1. def icmp_scan(network): threads = [] length =len(network) for ip in network: t=threading.Thread(target=icmp_rqst,arg=(str(ip),)) threads.append(t) #实例化2个Thread类,传递函数及其参数,并将线程对象放入一个列表中 for i in range(length): threads[i].start() #循环 开始线程 for i in range(length): threads[i].join() ##循环 join()方法可以让主线程等待所有的线程都执行完毕。
最终代码
本代码的多线程优化采取的是创建Thread类,传递一个函数的方法,我们先实例化thread类,然后执行线程。此时icmp_rqst 完成了生成icmp报告并且判断主机是否存活的双重功能。而scan函数用于实例化多个线程并且采用多线程执行。具体请看注释
# -*- coding:utf-8 -*-
from scapy.all import IP, ICMP, srp1,Ether
import threading
import argparse
import ipaddress
import os
import sys
#判断主机是否存活
def icmp_rqst(ip_dst,iface=None):
pkt=Ether()/IP(dst=ip_dst)/ICMP(type=8)
res=srp1(pkt,timeout=2,verbose=False)
if res:
print('[+]',ip_dst,':',type,' Host is up')
def icmp_scan(network):
threads = []
length =len(network)
for ip in network:
t=threading.Thread(target=icmp_rqst,args=(str(ip),))
threads.append(t)
for i in range(length):
threads[i].start()
for i in range(length):
threads[i].join()
def main():
parser = argparse.ArgumentParser()
parser.add_argument('network', help='eg:192.168.1.0/24')
args = parser.parse_args()
network= list(ipaddress.ip_network(args.network))
icmp_scan(network)
if __name__ == '__main__':
main()
报错解决:安装npcap
测试程序时出现了这个好讨厌的bug
上网搜了一波 winpcap早已不更新了,解决的办法很简单,安装Npcap
下载地址:
https://nmap.org/npcap/#download
进入官网后,win系统下载红线标注的即可
下载之后安装即可
安装完重新运行就没有报错啦。
测试结果
找到源程序文件夹。打开设置,找到路径
在命令行中进入该目录
由于pycharm运行不知道咋带参数,cmd又没装scapy,所以又跑去装了一波scapy,这里记录一下
scapy安装教程
在github上下载压缩包
下载路径:https://github.com/secdev/scapy
解压后 cd进入其目录,运行(我的python3默认为python。python2为python2)
Python setup.py install
等待安装即可
安装完成后,输入scapy出现下述页面即为安装成功。
突然想起上一次做红队任务时对同程进行了信息搜集,那就用同程的服务器测试一下我的工具把(嘻嘻嘻同程dbqdbq)
先确认服务器能够ping通,再使用我们的工具。返回host is up。
后记
通过这次学习,我们可以深入的了解icmp协议的原理,并且利用icmp协议实现了 简单主机存活工具的开发。
预告:下一篇教大家利用python自定义开发简单的端口扫描工具,敬请期待。