原理介绍
TCP全连接端口扫描器是最基础的扫描器,它的原理是调用Socket
的connect
函数连接到目标IP的特定端口上,如果连接成功说明端口是开放的,如果连接失败,说明端口没有开放。
Go语言的net
包提供的Dial
与DialTimeout
函数,对传统的socket
函数进行了封装,无论想创建什么协议的连接,都只需要调用这两个函数即可。这两个函数的区别是DialTimeout
增加了超时时间。
普通扫描器
以下代码片段利用DialTimeout
实现了一个Connect
方法,可以判断一个端口是否开放
func Connect(ip string,port int)(net.Conn,error) {
conn,err := net.DialTimeout("tcp",fmt.Sprintf("%v:%v",ip,port),2*time.Second)
defer func() {
if conn != nil{
_ = conn.Close()
}
}()
return conn,err
}
这里封装一个GetIpList
函数,可以根据输入的ipList
返回一个[]net.IP
的切片
func GetIpList(ips string)([]net.IP,error) {
addressList,err := iprange.ParseList(ips)
if err != nil{
return nil, err
}
list := addressList.Expand()
return list,err
}
多端口的处理需要支持",“与”-“分割的端口列表,可以使用strings
包的Split
函数先分割以”,“连接的ipList
,然后再分割以”-"连接的ipList,最后返回一个[]int
切片
func GetPorts(selection string)([]int,error) {
ports := []int{}
if selection == ""{
return ports,nil
}
ranges := strings.Split(selection,",")
for _,r := range ranges{
r = strings.TrimSpace(r)
if strings.Contains(r,"-"){
parts := strings.Split(r,"-")
if len(parts) != 2{
return nil, fmt.Errorf("Invaild port selection sequment: '%s'",r)
}
p1,err := strconv.Atoi(parts[0])
if err != nil{
return nil,fmt.Errorf("Invaild port number: '%s'",parts[0])
}
p2,err := strconv.Atoi(parts[1])
if err != nil{
return nil,fmt.Errorf("Invaild port number: '%s'",parts[1])
}
if p1 > p2{
return nil,fmt.Errorf("Invaild port range:%d-%d",p1,p2)
}
for i := p1;i <= p2;i++{
ports = append(ports,i)
}
}else{
if port, err := strconv.Atoi(r);err!=nil{
return nil,fmt.Errorf("Invaild port number: '%s'",r)
}else{
ports = append(ports,port)
}
}
}
return ports,nil
}
主函数和导入的相应包
func main() {
if len(os.Args) == 3{
ipList := os.Args[1]
portList := os.Args[2]
ips,err := GetIpList(ipList)
_ = err
ports,err := GetPorts(portList)
for _,ip := range ips{
for _, port := range ports{
_, err := Connect(ip.String(),port)
if err != nil{
continue
}
fmt.Printf("ip: %v,port: %v is open \n",ip,port)
}
}
}else{
fmt.Printf("%v iplist port \n",os.Args[0])
}
}
import (
"fmt"
"github.com/malfunkt/iprange"
"net"
"os"
"strconv"
"strings"
"time"
)
运行结果,因为现在完成的TCP全连接端口扫描器是单线程扫描器,扫描速度非常慢,不适合用在实际的扫描任务中。因此我们需要将它改装为高并发的扫描器,扫描速度可以与Nmap媲美。
高并发扫描器(一)
Go语言是原生支持并发的语言,它的并发是通过协程实现的。实现的步骤是:
- 生成扫描任务列表:首先解析出需要扫描的IP与端口的切片,然后将需要扫描的IP与端口列表放入一个
[]map[string]int
中,map的key为IP地址,value为端口,[]map[string]int
表示所有需要扫描的IP与端口对的切片。
func GenerateTask(ipList []net.IP, ports []int) ([]map[string]int, int) {
tasks := make([]map[string]int, 0)
for _, ip := range ipList {
for _, port := range ports {
ipPort := map[string]int{ip.String(): port}
tasks = append(tasks, ipPort)
}
}
return tasks, len(tasks)
}
- 分割扫描任务:根据并发数将需要扫描的
[]map[string]int
切片分割成组,以便按组进行并发扫描。
func AssigningTasks(tasks []map[string]int) {
scanBatch := len(tasks) / vars.ThreadNum
for i := 0; i < scanBatch; i++ {
curTask := tasks[vars.ThreadNum*i : vars.ThreadNum*(i+1)]
RunTask(curTask)
}
if len(tasks)%vars.ThreadNum > 0 {
lastTasks := tasks[vars.ThreadNum*scanBatch:]
RunTask(lastTasks)
}
}
len(tasks)%vars.ThreadNum >0表示len(tasks) / vars.ThreadNum不能整除,还有剩余的任务列表需要进行处理。
- 按组执行扫描任务:分别将每组扫描任务传入具体的扫描任务中,扫描任务函数利用
sync.WaitGroup
实现并发扫描,在扫描的过程中将结果保存到一个并发安全的map中。
func AssigningTasks(tasks []map[string]int) {
scanBatch := len(tasks) / vars.ThreadNum
for i := 0; i < scanBatch; i++ {
curTask := tasks[vars.ThreadNum*i : vars.ThreadNum*(i+1)]
RunTask(curTask)
}
if len(tasks)%vars.ThreadNum > 0 {
lastTasks := tasks[vars.ThreadNum*scanBatch:]
RunTask(lastTasks)
}
}
- 展示扫描结果:所有扫描任务完成后,输出保存在并发安全map中的扫描结果。
func SaveResult(ip string, port int, err error) error {
// fmt.Printf("ip: %v, port: %v,err: %v, goruntineNum: %v\n", ip, port, err, runtime.NumGoroutine())
if err != nil {
return err
}
v, ok := vars.Result.Load(ip)
if ok {
ports, ok1 := v.([]int)
if ok1 {
ports = append(ports, port)
vars.Result.Store(ip, ports)
}
} else {
ports := make([]int, 0)
ports = append(ports, port)
vars.Result.Store(ip, ports)
}
return err
}
func PrintResult() {
vars.Result.Range(func(key, value interface{}) bool {
fmt.Printf("ip:%v\n", key)
fmt.Printf("ports: %v\n", value)
fmt.Println(strings.Repeat("-", 100))
return true
})
}
在main函数中分别调用任务生成、任务分配与结果展示的函数即可
package main
import (
"fmt"
"os"
"runtime"
"sec-dev-in-action-src/scanner/tcp-connect-scanner1/scanner"
"sec-dev-in-action-src/scanner/tcp-connect-scanner1/util"
)
func main() {
if len(os.Args) == 3 {
ipList := os.Args[1]
portList := os.Args[2]
ips, err := util.GetIpList(ipList)
ports, err := util.GetPorts(portList)
_ = err
task, _ := scanner.GenerateTask(ips, ports)
scanner.AssigningTasks(task)
scanner.PrintResult()
} else {
fmt.Printf("%v iplist port\n", os.Args[0])
}
}
func init() {
runtime.GOMAXPROCS(runtime.NumCPU())
}
vars.go
package vars
import "sync"
var (
ThreadNum = 5000
Result *sync.Map
)
func init() {
Result = &sync.Map{}
}
util.go
package util
import (
"fmt"
"net"
"strconv"
"strings"
"github.com/malfunkt/iprange"
)
func GetPorts(selection string) ([]int, error) {
ports := []int{}
if selection == "" {
return ports, nil
}
ranges := strings.Split(selection, ",")
for _, r := range ranges {
r = strings.TrimSpace(r)
if strings.Contains(r, "-") {
parts := strings.Split(r, "-")
if len(parts) != 2 {
return nil, fmt.Errorf("Invalid port selection segment: '%s'", r)
}
p1, err := strconv.Atoi(parts[0])
if err != nil {
return nil, fmt.Errorf("Invalid port number: '%s'", parts[0])
}
p2, err := strconv.Atoi(parts[1])
if err != nil {
return nil, fmt.Errorf("Invalid port number: '%s'", parts[1])
}
if p1 > p2 {
return nil, fmt.Errorf("Invalid port range: %d-%d", p1, p2)
}
for i := p1; i <= p2; i++ {
ports = append(ports, i)
}
} else {
if port, err := strconv.Atoi(r); err != nil {
return nil, fmt.Errorf("Invalid port number: '%s'", r)
} else {
ports = append(ports, port)
}
}
}
return ports, nil
}
func GetIpList(ips string) ([]net.IP, error) {
addressList, err := iprange.ParseList(ips)
if err != nil {
return nil, err
}
list := addressList.Expand()
return list, err
}
运行效果如下,扫描速度甚至比nmap还快
高并发扫描器(二)
这个扫描器虽然已经实现了并发扫描,但对协程的控制不够精细,每组扫描任务都会瞬间启动大量的协程,然后逐渐关闭,而不是一个平滑的过程。这种方法可能会瞬间将服务器的CPU占满,我们可以使用sync.WaitGroup
与channel
配合实现了新的并发方式
func RunTask(tasks []map[string]int) {
wg := &sync.WaitGroup{}
//创建一个buffer为vars.ThreadNum*2的channel
taskChan := make(chan map[string]int,vars.ThreadNum*2)
//创建vars.ThreadNum个协程
for i:= 0;i < vars.ThreadNum;i++{
go Scan(taskChan,wg)
}
//生产者,不断地往taskChan channel发送数据,知道channel阻塞
for _,task := range tasks{
wg.Add(1)
taskChan <- task
}
close(taskChan)
wg.Wait()
}
func Scan(taskChan chan map[string]int,wg*sync.WaitGroup) {
//每个协程都从channel中读取数据后开始扫描并保存存入数据库
for task := range taskChan{
for ip,port := range task{
err := SaveResult(Connect(ip,port))
_ = err
wg.Done()
}
}
}
RunTask函数不断地将扫描任务发送到taskChan中,Scan会不断地消费taskChan中的数据。