前兩天看了amass關於dns枚舉的實現,當然關於加速dns枚舉的還有ksubdomain這個項目,今天花了幾分鐘看了下實現
閱讀基於 https://github.com/boy-hack/ksubdomain/commit/9a2f2967eb8fb5c155b22393b9241f4cd6a02dc4
分析
首先從入口點開始看 https://github.com/boy-hack/ksubdomain/blob/main/cmd/ksubdomain/enum.go#L55-L109
Action: func(c *cli.Context) error {
if c.NumFlags() == 0 {
cli.ShowCommandHelpAndExit(c, "enum", 0)
}
var domains []string
// handle domain
if c.String("domain") != "" {
domains = append(domains, c.String("domain"))
}
if c.String("domainList") != "" {
dl, err := core.LinesInFile(c.String("domainList"))
if err != nil {
gologger.Fatalf("讀取domain文件失敗:%s\n", err.Error())
}
domains = append(dl, domains...)
}
levelDict := c.String("level-dict")
var levelDomains []string
if levelDict != "" {
dl, err := core.LinesInFile(levelDict)
if err != nil {
gologger.Fatalf("讀取domain文件失敗:%s,請檢查--level-dict參數\n", err.Error())
}
levelDomains = dl
} else if c.Int("level") > 2 {
levelDomains = core.GetDefaultSubNextData()
}
opt := &options.Options{
Rate: options.Band2Rate(c.String("band")),
Domain: domains,
FileName: c.String("filename"),
Resolvers: options.GetResolvers(c.String("resolvers")),
Output: c.String("output"),
Silent: c.Bool("silent"),
Stdin: c.Bool("stdin"),
SkipWildCard: c.Bool("skip-wild"),
TimeOut: c.Int("timeout"),
Retry: c.Int("retry"),
Method: "enum",
OnlyDomain: c.Bool("only-domain"),
NotPrint: c.Bool("not-print"),
Level: c.Int("level"),
LevelDomains: levelDomains,
}
opt.Check()
r, err := runner.New(opt)
if err != nil {
gologger.Fatalf("%s\n", err.Error())
return nil
}
r.RunEnumeration()
r.Close()
return nil
},
具體的實現細節就不關注了,可以看到入口點只是讀取了一些配置,繼續進入 RunEnumeration
看看
func (r *runner) RunEnumeration() {
ctx, cancel := context.WithCancel(r.ctx)
defer cancel()
go r.recvChanel(ctx) // 啓動接收線程
for i := 0; i < 3; i++ {
go r.sendCycle(ctx) // 發送線程
}
go r.handleResult(ctx) // 處理結果,打印輸出
var isLoadOver bool = false // 是否加載文件完畢
t := time.NewTicker(1 * time.Second)
defer t.Stop()
for {
select {
case <-t.C:
r.PrintStatus()
if isLoadOver {
if r.hm.Length() == 0 {
gologger.Printf("\n")
gologger.Infof("掃描完畢")
return
}
}
case <-r.fisrtloadChanel:
go r.retry(ctx) // 遍歷hm,依次重試
isLoadOver = true
}
}
}
首先是啓動了一個接收dns resp數據包的協程,然後啓動了三個發送dns req數據包的協程,還有一個線程 handleResult
用來輸出結果
剩下的我們先不關注,可以思考一下,啓動一個發送協程,一個接收協程,一個用來打印結果,單純這三個協程我們肯定是沒法控制整個程序的停止的,因爲接收協程肯定是需要一個死循環去讀取。
所以我們看看下來的控制,isLoadOver
比較關鍵,可以看到它由 fisrtloadChanel
來控制,我們找找它是在哪裏被賦值的
type runner struct {
ether *device.EtherTable //本地網卡信息
hm *statusdb.StatusDb
options *options2.Options
limit ratelimit.Limiter
handle *pcap.Handle
successIndex uint64
sendIndex uint64
recvIndex uint64
faildIndex uint64
sender chan string
recver chan core.RecvResult
freeport int
dnsid uint16 // dnsid 用於接收的確定ID
maxRetry int // 最大重試次數
timeout int64 // 超時xx秒後重試
ctx context.Context
fisrtloadChanel chan string // 數據加載完畢的chanel
startTime time.Time
domains []string
}
func New(options *options2.Options) (*runner, error) {
var err error
version := pcap.Version()
r := new(runner)
gologger.Infof(version + "\n")
r.options = options
r.ether = GetDeviceConfig()
r.hm = statusdb.CreateMemoryDB()
gologger.Infof("DNS:%s\n", options.Resolvers)
r.handle, err = device.PcapInit(r.ether.Device)
if err != nil {
return nil, err
}
// 根據發包總數和timeout時間來分配每秒速度
allPacket := r.loadTargets()
if options.Level > 2 {
allPacket = allPacket * int(math.Pow(float64(len(options.LevelDomains)), float64(options.Level-2)))
}
calcLimit := float64(allPacket/options.TimeOut) * 0.85
if calcLimit < 1000 {
calcLimit = 1000
}
limit := int(math.Min(calcLimit, float64(options.Rate)))
r.limit = ratelimit.New(limit) // per second
gologger.Infof("Rate:%dpps\n", limit)
r.sender = make(chan string, 99) // 多個協程發送
r.recver = make(chan core.RecvResult, 99) // 多個協程接收
freePort, err := freeport.GetFreePort()
if err != nil {
return nil, err
}
r.freeport = freePort
gologger.Infof("FreePort:%d\n", freePort)
r.dnsid = 0x2021 // set dnsid 65500
r.maxRetry = r.options.Retry
r.timeout = int64(r.options.TimeOut)
r.ctx = context.Background()
r.fisrtloadChanel = make(chan string)
r.startTime = time.Now()
go func() {
for _, msg := range r.domains {
r.sender <- msg
if options.Method == "enum" && options.Level > 2 {
r.iterDomains(options.Level, msg)
}
}
r.domains = nil
r.fisrtloadChanel <- "ok"
}()
return r, nil
}
我們可以看到它是用來在數據全部發往 sender
後的一個標識位,可以看到 New
函數是用來初始化限速器,timeout等等。
然後我們繼續回到之前的代碼看看
case <-r.fisrtloadChanel:
go r.retry(ctx) // 遍歷hm,依次重試
isLoadOver = true
可以看到當數據全部發往 sender
後,將會調用retry
func (r *runner) retry(ctx context.Context) {
for {
// 循環檢測超時的隊列
now := time.Now()
r.hm.Scan(func(key string, v statusdb.Item) error {
if r.maxRetry > 0 && v.Retry > r.maxRetry {
r.hm.Del(key)
atomic.AddUint64(&r.faildIndex, 1)
return nil
}
if int64(now.Sub(v.Time)) >= r.timeout {
// 重新發送
r.sender <- key
}
return nil
})
length := 1000
time.Sleep(time.Millisecond * time.Duration(length))
}
}
可以看到具體邏輯是:判斷是否達到最大重試次數,如果沒有就重新入隊去進行dns請求,如果達到最大次數則從緩存中刪除它。
然後繼續往下看,可以看到 isLoadOver = true
,然後可以看
if isLoadOver {
if r.hm.Length() == 0 {
gologger.Printf("\n")
gologger.Infof("掃描完畢")
return
}
}
可以看到當 isLoadOver == true && r.hm.Length() == 0
時,會停止掃描退出。也就是所有的子域名枚舉完成或達到最大重試次數,則退出。
看完了控制邏輯,我們可以看看具體的發送包和接收包的函數了
首先看看發送
func (r *runner) sendCycle(ctx context.Context) {
for domain := range r.sender {
r.limit.Take()
v, ok := r.hm.Get(domain)
if !ok {
v = statusdb.Item{
Domain: domain,
Dns: r.choseDns(),
Time: time.Now(),
Retry: 0,
DomainLevel: 0,
}
r.hm.Add(domain, v)
} else {
v.Retry += 1
v.Time = time.Now()
v.Dns = r.choseDns()
r.hm.Set(domain, v)
}
send(domain, v.Dns, r.ether, r.dnsid, uint16(r.freeport), r.handle)
atomic.AddUint64(&r.sendIndex, 1)
}
}
可以看到首先是發送速率的控制,然後從緩存中獲取生成的子域名,如果沒有代表第一次跑,初始化一個Item丟給send去發包,如果已經存在則重試次數加一,然後重新選擇dns服務器,然後丟給send發包。
具體的發包函數我們就不看了,ksubdomain
是採用的 gopacket
直接構造dns包然後使用網卡發包,目的爲了提速
然後看看接收函數
func (r *runner) recvChanel(ctx context.Context) error {
...
parser := gopacket.NewDecodingLayerParser(
layers.LayerTypeEthernet, ð, &ipv4, &ipv6, &udp, &dns)
var data []byte
var decoded []gopacket.LayerType
for {
data, _, err = handle.ReadPacketData()
if err != nil {
continue
}
err = parser.DecodeLayers(data, &decoded)
if err != nil {
continue
}
if !dns.QR {
continue
}
if dns.ID != r.dnsid {
continue
}
atomic.AddUint64(&r.recvIndex, 1)
if len(dns.Questions) == 0 {
continue
}
subdomain := string(dns.Questions[0].Name)
r.hm.Del(subdomain)
if dns.ANCount > 0 {
atomic.AddUint64(&r.successIndex, 1)
result := core.RecvResult{
Subdomain: subdomain,
Answers: dns.Answers,
}
r.recver <- result
}
}
}
我摘取了一部分,可以看到具體邏輯就是不斷從網卡中獲取然後解析dns返回包,然後從緩存中刪除該子域名並放入 recver
chan,這個chan主要是用來讀取並輸出的。
整體邏輯大體上就是這樣。
總結
整體加速思路其實和amass有點像
amass是使用了一些額外的技巧來達到同步調用,使用單個udp連接來完成,一個協程用來寫,一個用來讀,而ksubdomain直接調用網卡驅動繞過了操作系統,可以突破操作系統的發包限制,會更快一些,amass對於udp到tcp的dns請求做了一些適配