【Go黑帽子】使用Golang编写一个TCP扫描器(高级篇)
前言
上一篇文章:【Go黑帽子】使用Golang编写一个TCP扫描器(基础篇)
上文中代码的其余书写格式:
package main
import (
"fmt"
"net"
)
func main() {
IP := "scanme.nmap.org"
Port := "80"
address := IP + ":" + Port
connection, err := net.Dial("tcp", address)
if err == nil {
//RemoteAddr returns the remote network address, if known.
fmt.Println("[+] Connection established..", connection.RemoteAddr().String())
connection.Close()
}
}
// 运行结果:
// [+] Connection established.. 45.33.32.156:80
package main
import (
"fmt"
"net"
)
func main() {
IP := "scanme.nmap.org"
for i := 0; i < 100; i++ {
address := fmt.Sprintf(IP+":%d", i)
connection, err := net.Dial("tcp", address)
if err == nil {
//RemoteAddr returns the remote network address, if known.
fmt.Printf("[+] Connection established.. PORT %v %v\n", i, connection.RemoteAddr().String())
connection.Close()
}
}
}
// 运行结果:
// [+] Connection established.. PORT 22 [2600:3c01::f03c:91ff:fe18:bb2f]:22
// [+] Connection established.. PORT 80 [2600:3c01::f03c:91ff:fe18:bb2f]:80
在本篇文章中,我们要解决在使用goroutine时,其造成代码运行速度过快导致数据包仍在端口中运行而无法获得准确的结果。
一、使用WaitGroup进行同步扫描
WaitGroup是sync包中的一个结构体,可以控制并发线程的安全。
通过如下方法创建:
var wg sync.WaitGroup
我们需要使用到其中的Add(int)
、Done()
和Wait()
。
Add(int)
会按照输入的数字递增内部的计数器;
Done()
使计数器减1;
Wait()
起到阻塞代码运行的作用,只有内部计数器达到0之后,代码才会进一步执行。
我们使用上述知识点便可以确保主goroutine等待其余gorourine完成:
package main
import (
"fmt"
"net"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 1; i <= 1024; i++ {
//通过wg.Add(1)递增计数器
wg.Add(1)
go func(j int) {
//defer wg.Done()延迟调用使计数器递减
defer wg.Done()
address := fmt.Sprintf("scanme.nmap.org:%d", j)
conn, err := net.Dial("tcp", address)
//如果端口已关闭或已过滤,结束本次循环
if err != nil {
return
}
conn.Close()
fmt.Printf("%d open\n", j)
}(i)
}
//通过Wait()来阻塞代码的执行
wg.Wait()
}
二、使用工人池进行端口扫描
使用goroutine池管理正在进行的并发工作,可以避免结果不一致的情况。
先用for循环创建一定数量的资源池,然后在main()线程中使用通道提供工作:
func worker(ports chan int, wg *sync.WaitGroup) {
//使用range连续地从通道ports接收数据,直到该通道被关闭
for p := range ports {
fmt.Println(p)
wg.Done()
}
}
通道用于接收工作,而WaitGroup则用于跟踪单个工作的完成情况。
在main()中,使用make()创建一个通道。为了可以在不等待接收器读取数据的情况下向其发送数据,我们需要对该通道进行缓冲。
完整代码如下:
package main
import (
"fmt"
"sync"
)
func worker(ports chan int, wg *sync.WaitGroup) {
for p := range ports {
fmt.Println(p)
wg.Done()
}
}
func main() {
ports := make(chan int, 100)
var wg sync.WaitGroup
for i := 0; i < cap(ports); i++ {
go worker(ports, &wg)
}
for i := 1; i <= 1024; i++ {
wg.Add(1)
ports <- i
}
wg.Wait()
close(ports)
}
上述代码可以打印出相应的数字,但是打印顺序是随机的。
只需要在函数func worker(ports chan int, wg *sync.WaitGroup)
中加入前面讲过的扫描代码,就可以成为新的端口扫描器。对应的端口扫描器代码如下:
package main
import (
"fmt"
"net"
"sync"
)
func worker(ports chan int, wg *sync.WaitGroup) {
for p := range ports {
address := fmt.Sprintf("scanme.nmap.org:%d", p)
connection, err := net.Dial("tcp", address)
//端口连接成功
if err == nil {
fmt.Printf("[+] Connection established.. PORT %v %v\n", p, connection.RemoteAddr().String())
connection.Close()
}
//端口已关闭或已过滤
if err != nil {
//do nothing
}
wg.Done()
}
}
func main() {
ports := make(chan int, 100)
var wg sync.WaitGroup
for i := 0; i < cap(ports); i++ {
go worker(ports, &wg)
}
for i := 1; i <= 1024; i++ {
wg.Add(1)
ports <- i
}
wg.Wait()
close(ports)
}
//运行结果:
// [+] Connection established.. PORT 22 [2600:3c01::f03c:91ff:fe18:bb2f]:22
// [+] Connection established.. PORT 80 45.33.32.156:80
三、多通道通信
为了使扫描到的端口按照顺序打印到屏幕上,我需要使用单独的线程将端口扫描的结果传回主线程。
需要对端口顺序进行排序,我们可以将扫描到的端口号存入到切片中,使用以下函数即可对切片中的整数以递增的方式进行排序:
//Ints sorts a slice of ints in increasing order.
func sort.Ints(x []int)
修改优化后的代码如下:
package main
import (
"fmt"
"net"
"sort"
)
// 接收两个通道
func worker(ports, results chan int) {
for p := range ports {
address := fmt.Sprintf("scanme.nmap.org:%d", p)
connection, err := net.Dial("tcp", address)
if err != nil {
//如果端口关闭,则发送0
results <- 0
continue
}
connection.Close()
//如果端口处于打开状态,则发送端口号
results <- p
}
}
func main() {
//ports通道的第二个参数为100
//如果一直发送数据,则该通道可以容纳100个数据项
ports := make(chan int, 100)
//创建一个单独的通道用于将结果从func worker(ports chan int, results chan int)传递到主线程
results := make(chan int)
//使用切片储存结果,以便进行排序
var openports []int
for i := 0; i < cap(ports); i++ {
go worker(ports, results)
}
go func() {
for i := 1; i <= 1024; i++ {
ports <- i
}
}()
for i := 0; i < 1024; i++ {
//将通道中的值传给port
port := <-results
if port != 0 {
openports = append(openports, port)
}
}
close(ports)
close(results)
//Ints sorts a slice of ints in increasing order.
//以递增的顺序对切片内的整数进行排序
sort.Ints(openports)
for _, port := range openports {
fmt.Printf("%d open\n", port)
}
}
//运行结果:
// 22 open
// 80 open
在本程序中,我们使用“Channel 的发送和接收是阻塞的”的原理,完全消除了对WaitGroup的依赖。
后记
当然,如果要实现更高效的端口扫描,kali中的nmap是个不错的选择。